Golang クロージャーの基本とfor 文での注意点
はじめに
このページではGolangのクロージャーの基本から、その注意点、そしてfor range
構文を用いたクロージャーの正しい使い方までを記述します。
Golangのクロージャーとは?
クロージャー(closure)とは、関数の内部で定義された関数が、その外部の変数にアクセスできるようにする仕組みです。Golangでは、関数を変数に代入したり、引数として渡したり、戻り値として返すことができるため、クロージャーを利用することで、関数が状態を持ち、その状態を保持しながら実行することが可能です。
基本的なクロージャーの例
まずは、Golangにおける基本的なクロージャーの使い方を示します。
package main
import "fmt"
// カウンター関数を返すクロージャー
func counter() func() int {
count := 0
return func() int {
count++
return count
}
}
func main() {
// counter関数を呼び出し、クロージャーを生成
counter1 := counter()
fmt.Println(counter1()) // 出力: 1
fmt.Println(counter1()) // 出力: 2
fmt.Println(counter1()) // 出力: 3
// 新たなクロージャーを生成
counter2 := counter()
fmt.Println(counter2()) // 出力: 1
fmt.Println(counter2()) // 出力: 2
}
説明
この例では、counter
関数が匿名関数を返し、この匿名関数が外部の変数 count
にアクセスできるクロージャーを形成しています。counter1
と counter2
は別々のクロージャーであり、それぞれ独立した count
の値を持っています。
クロージャーの実用例
クロージャーはイベントハンドラやコールバック関数の実装など、実際のアプリケーションでも役立ちます。以下の例では、数値のリストをフィルタリングするクロージャーを使った関数を示します。
フィルタ関数の作成
リストをフィルタする関数を作成する例を見てみましょう。
package main
import "fmt"
// 特定の条件を満たすかどうかを判断する関数を返すクロージャー
func filter(predicate func(int) bool) func([]int) []int {
return func(numbers []int) []int {
var result []int
for _, n := range numbers {
if predicate(n) {
result = append(result, n)
}
}
return result
}
}
func main() {
isEven := func(n int) bool {
return n%2 == 0
}
// 偶数フィルタを作成
evenFilter := filter(isEven)
numbers := []int{1, 2, 3, 4, 5, 6}
fmt.Println(evenFilter(numbers)) // 出力: [2 4 6]
}
説明
filter
関数がクロージャーを返し、そのクロージャーが predicate
関数を使って数値のリストをフィルタリングします。このように、クロージャーを使うことで、柔軟なフィルタ関数を作成できます。
for range
での問題
クロージャーの注意点とクロージャーを使用する際の注意点として、変数のスコープが挙げられます。特に、for range
構文を使ったループ内でクロージャーを作成する場合、クロージャーがループ変数の参照を保持してしまい、期待通りの動作をしないことがあります。
for range
使用)
誤ったクロージャーの例 (以下は、for range
を使用したクロージャーの誤った例です。
package main
import "fmt"
func main() {
funcs := []func(){}
for _, i := range []int{0, 1, 2} {
// クロージャーがiの参照を保持するため、最終的なiの値が使われる
funcs = append(funcs, func() {
fmt.Println(i)
})
}
for _, f := range funcs {
f() // 出力: 2, 2, 2
}
}
この例では、クロージャーがすべて同じ i
の参照を保持しているため、ループが終了した時点の i
の値(2
)が出力されます。
修正例: 即時実行関数を利用
for range
におけるこの問題を修正するには、即時実行関数(IIFE: Immediately Invoked Function Expression)を使用して i
の値をクロージャー内に閉じ込める方法が有効です。
package main
import "fmt"
func main() {
funcs := []func(){}
for _, i := range []int{0, 1, 2} {
// 即時実行関数でiの値を閉じ込める
func(i int) {
funcs = append(funcs, func() {
fmt.Println(i)
})
}(i) // 現在のiの値を即時実行関数に渡す
}
for _, f := range funcs {
f() // 出力: 0, 1, 2
}
}
説明
この修正では、func(i int)
という匿名関数を即時に実行する形で使用し、その関数の引数として現在の i
の値を渡しています。これにより、各クロージャーはそれぞれ異なる i
のコピーを持ち、それを参照するようになります。
まとめ
Golangのクロージャーは、関数が外部の変数にアクセスし、状態を保持できる強力なツールです。ただし、for range
構文を使う場合は、変数のスコープに注意が必要です。即時実行関数(IIFE)を利用することで、クロージャーが正しいスコープの変数を保持できるようになります。
Discussion
こんにちは!
Go1.22以降では挙動が変更されているので、バージョンを明記された方が読者に混乱を与えないと思いました。
Go1.22からはループごとに新しい変数が使われるようになったので、最初の例でも意図通りの挙動になります。
tenkohさん
コメントありがとうございます!
そうだったんですね、、知らなかった、、
わかりにくい処理だったのでよかったです。
更新しときます!ありがとうございます!