Go で関数を引数に渡す・戻り値で返す
Go では関数は第一級オブジェクトとして扱われる。つまり、関数を変数に代入するだけでなく、別の関数の引数として渡したり、戻り値として返したりできる。この特性を活かすと、処理のカスタマイズや共通ロジックの抽出が柔軟に行えるようになる。
関数型の定義
関数を引数や戻り値にするには、関数の型(シグネチャ)を明示する必要がある。Go では func(引数の型) 戻り値の型 という形式で関数型を表現する。
// int を受け取って int を返す関数型
var transform func(int) int
// 2 つの int を受け取って bool を返す関数型
var compare func(int, int) booltype を使って関数型に名前を付けることも可能だ。名前を付けると、コードの意図が明確になり、同じ型を何度も書く手間も省ける。
type Predicate func(int) bool
func filter(nums []int, pred Predicate) []int {
var result []int
for _, n := range nums {
if pred(n) {
result = append(result, n)
}
}
return result
}関数を引数に渡す
関数を引数として受け取る関数を「高階関数」と呼ぶ。典型的な例がフィルタリング処理だ。
func filter(nums []int, fn func(int) bool) []int {
var result []int
for _, n := range nums {
if fn(n) {
result = append(result, n)
}
}
return result
}
func main() {
numbers := []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
evens := filter(numbers, func(n int) bool {
return n%2 == 0
})
fmt.Println(evens) // [2 4 6 8 10]
greaterThan5 := filter(numbers, func(n int) bool {
return n > 5
})
fmt.Println(greaterThan5) // [6 7 8 9 10]
}filter 関数自体はフィルタの「仕組み」だけを担い、何を残すかという「判定基準」は呼び出し側が関数として渡す。こうすることで、ロジックの再利用性が大幅に高まる。
標準ライブラリでの高階関数
Go の標準ライブラリにも、関数を引数に取る API が多数存在する。代表的なのが sort.Slice だ。
people := []struct {
Name string
Age int
}{
{"Alice", 30},
{"Bob", 25},
{"Charlie", 35},
}
sort.Slice(people, func(i, j int) bool {
return people[i].Age < people[j].Age
})
fmt.Println(people)
// [{Bob 25} {Alice 30} {Charlie 35}]ソートのアルゴリズムは sort.Slice が担当し、比較のルールだけを無名関数で注入する。この設計パターンにより、あらゆる型のスライスを任意の基準でソートできる。
関数を戻り値として返す
関数を返す関数を作ると、設定を事前に組み込んだ「カスタム関数」を生成できる。
func newGreeter(greeting string) func(string) string {
return func(name string) string {
return greeting + ", " + name + "!"
}
}
func main() {
hello := newGreeter("Hello")
konnichiwa := newGreeter("こんにちは")
fmt.Println(hello("Alice")) // Hello, Alice!
fmt.Println(konnichiwa("太郎")) // こんにちは, 太郎!
}返された関数は外側の変数 greeting をクロージャとして保持しているため、呼び出すたびに事前に設定した挨拶文が使われる。
greet(“Hello”, “Alice”) のように、呼び出しのたびに設定値を渡す必要がある
hello := newGreeter(“Hello”) で設定を固定し、hello(“Alice”) だけで済む
ミドルウェアパターン
Web 開発でよく使われるのが、HTTP ハンドラをラップするミドルウェアパターンだ。関数を受け取って関数を返すことで、共通処理を差し込める。
func loggingMiddleware(next http.HandlerFunc) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
log.Printf("%s %s", r.Method, r.URL.Path)
next(w, r)
}
}
func hello(w http.ResponseWriter, r *http.Request) {
fmt.Fprintln(w, "Hello, World!")
}
func main() {
http.HandleFunc("/", loggingMiddleware(hello))
http.ListenAndServe(":8080", nil)
}loggingMiddleware は hello ハンドラをラップし、リクエストのログを出力してから元のハンドラを呼び出す。この構造はチェーンのように連結でき、認証チェックやエラーリカバリなど複数のミドルウェアを積み重ねることも可能だ。
map と reduce の実装
Go には組み込みの map や reduce 関数は存在しないが、高階関数を使えば簡単に実装できる。
func mapInts(nums []int, fn func(int) int) []int {
result := make([]int, len(nums))
for i, n := range nums {
result[i] = fn(n)
}
return result
}
func reduce(nums []int, initial int, fn func(int, int) int) int {
acc := initial
for _, n := range nums {
acc = fn(acc, n)
}
return acc
}
func main() {
nums := []int{1, 2, 3, 4, 5}
doubled := mapInts(nums, func(n int) int { return n * 2 })
fmt.Println(doubled) // [2 4 6 8 10]
sum := reduce(nums, 0, func(a, b int) int { return a + b })
fmt.Println(sum) // 15
}Go はオブジェクト指向や関数型に偏らない実用主義の言語だが、関数を値として扱える仕組みにより、必要に応じて関数型のアプローチも取り入れられる。高階関数はコードの抽象度を上げすぎると可読性が落ちるため、チームの慣習やプロジェクトの方針に合わせて適度に活用するのがよい。