Go 匿名関数とクロージャ
匿名関数とクロージャ(Closures)は、Go言語において極めて強力で表現力豊かな機能です。これらを使用することで、名前を付けずに関数を定義したり、周囲の環境(スコープ)にある変数を「キャプチャ(捕獲)」したりすることが可能になります。
これは、動的で再利用可能なコードブロックの記述や、関数型プログラミング(Functional Programming)のパラダイムを実現するための扉を開くものです。
本章では、匿名関数の書き方から、クロージャの低レイヤーにおける「記憶」メカニズムの正体までを徹底的に解き明かしていきます。
1. 匿名関数
その名の通り、匿名関数(他の多くの言語ではラムダ式(Lambda Expressions)とも呼ばれます)は、名前を持たない関数のことです。ある局所的な場所で簡潔な使い切りのロジックを実行したいとき、わざわざ名前を付けてパッケージレベルで宣言するのが煩雑な場合に、匿名関数が真価を発揮します。
1.1 匿名関数の定義と呼び出し
Goでは、func キーワード、パラメータリスト、戻り値を記述し、関数名を省略することで定義できます。これを変数に代入して呼び出すか、定義直後に実行(即時実行関数:IIFE)することができます。
package main
import "fmt"
func main() {
// 方法 1:匿名関数を変数に代入する
add := func(x, y int) int {
return x + y
}
// 変数名を介してこの匿名関数を呼び出す
result := add(5, 3)
fmt.Println("代入による呼び出し結果:", result) // 出力: 8
// 方法 2:定義直後に呼び出す (波括弧の末尾の括弧に注目)
result2 := func(x, y int) int {
return x * y
}(5, 3) // この (5, 3) が即座に渡される引数
fmt.Println("即時実行結果:", result2) // 出力: 15
}1.2 匿名関数を引数として渡す (コールバック関数)
Goにおいて、関数は「第一級オブジェクト (First-class citizens)」です。これは、関数が通常の int や string 変数と同じように、どこにでも受け渡しができることを意味します。他の関数を引数として受け取る関数は、高階関数 (Higher-order functions) と呼ばれます。
package main
import "fmt"
// operate は 2 つの数値と、「具体的な演算ルールを定義した」関数を引数として受け取ります
func operate(x, y int, operation func(int, int) int) int {
return operation(x, y) // 渡された関数を実行
}
func main() {
// 引き算を実行する匿名関数を渡す
result1 := operate(10, 5, func(a, b int) int {
return a - b
})
fmt.Println("引き算の結果:", result1) // 出力: 5
// 足し算を実行する匿名関数を渡す
result2 := operate(10, 5, func(a, b int) int {
return a + b
})
fmt.Println("足し算の結果:", result2) // 出力: 15
}このパターンは非常に強力です。具体的な「振る舞い」をパラメータとして動的に注入することで、コードの柔軟性を劇的に向上させます。
1.3 関数から別の関数を返す
同様に、関数は別の匿名関数を実行結果として返すこともできます。これは「ファクトリパターン(Factory Pattern)」と呼ばれ、特定の挙動を持つカスタマイズされた関数を生成するために使用されます。
package main
import "fmt"
// multiplier はファクトリ関数であり、倍率 factor を受け取り、
// カスタマイズされた匿名関数を返します。
func multiplier(factor int) func(int) int {
return func(x int) int {
return x * factor
}
}
func main() {
// 「5倍にする」専門の関数を作る
multiplyByFive := multiplier(5)
// 「10倍にする」専門の関数を作る
multiplyByTen := multiplier(10)
fmt.Println(multiplyByFive(3)) // 出力: 15
fmt.Println(multiplyByTen(3)) // 出力: 30
}上記のコードを注意深く考察してみてください。multiplier(5) の実行が終了したとき、ローカル変数である factor は破棄されるはずです。しかし、なぜ返された multiplyByFive 関数は、その後の呼び出し時でも factor が 5 であることを「記憶」しているのでしょうか? ここで登場するのが、極めて重要な概念である「クロージャ」です。
2. クロージャ (Closures)
クロージャとは、匿名関数に加えて、それが作成された時にアクセス可能だった外部変数の参照(Reference)をセットにしたものです。簡単に言えば、クロージャはコードロジックだけでなく、周囲の環境にある変数も「パッケージング」して持ち去るため、記憶能力を持つのです。
2.1 変数キャプチャのメモリメカニズム
クロージャが外部変数をキャプチャする際、その時点の変数の値の「コピー」ではなく、その変数の「メモリアドレス(参照)」をキャプチャします。これは、もし外部変数の値が後から変更された場合、クロージャ内部でも最新の値を感知できることを意味します。逆に、クロージャ内部で変数を変更すれば、外部からもその変更が見えます。
古典的な「カウンタ」の例を見てみましょう。
package main
import "fmt"
// counter はクロージャ関数を返します。このクロージャはインクリメントロジックだけでなく、
// 外側のローカル変数 count も密かに「バインド」しています。
func counter() func() int {
count := 0 // このローカル変数がクロージャにキャプチャされる
return func() int {
count++ // クロージャを呼び出すたびに、外側の count を修正している
return count
}
}
func main() {
// 2 つの完全に独立したカウンタ・インスタンスを作成
increment1 := counter()
increment2 := counter()
fmt.Println(increment1()) // 出力: 1 (count が 1 になる)
fmt.Println(increment1()) // 出力: 2 (count が 2 になる)
// increment2 は自分専用の独立したクロージャ環境と count 変数を持つ
fmt.Println(increment2()) // 出力: 1
fmt.Println(increment1()) // 出力: 3
fmt.Println(increment2()) // 出力: 2
}ここで、counter 関数が実行を終えて終了した後でも、内部で返された匿名関数が依然として count を参照しているため、count のライフサイクル(寿命)が延長されます。その値はクロージャの記憶の中に安全に「カプセル化」されているのです。これが状態管理 (State Management) の高度な形態です。
2.2 クラシックな罠:クロージャと for ループ
これは Go 言語の初心者(時にはベテランも)が最も陥りやすい罠です。クロージャは参照をキャプチャするため、for ループ内で複数のクロージャを作成し、それらがすべて同じループカウンタ変数を参照している場合、予期せぬ挙動が発生します。
package main
import "fmt"
func main() {
var functions []func() // 関数を格納するスライス
for i := 0; i < 5; i++ {
// 誤った方法:ループ変数 i を直接クロージャ内でキャプチャしている
functions = append(functions, func() {
fmt.Println("誤った i:", i)
})
}
// ループ終了時、i の値はすでに 5 になっている。
// この状態でスライスに保存された関数を順番に実行すると:
for _, f := range functions {
f() // 悲劇!すべて 5 がプリントされる
}
fmt.Println("--- 正しい解決策の境界線 ---")
var functions2 []func()
for i := 0; i < 5; i++ {
// 正しい方法:ループごとにローカルなコピー i を作成する
// 外側の i を、現在のコードブロックに属する新しい i にコピーする
i := i
functions2 = append(functions2, func() {
fmt.Println("正しい i:", i) // ここでキャプチャされるのはローカルなコピー
})
}
for _, f := range functions2 {
f() // 正しく 0, 1, 2, 3, 4 と出力される
}
}メカニズムの解析: 最初の誤ったコードでは、すべての匿名関数が i という同じメモリアドレスを指しています。それらが最後に実際に実行されるとき、そのアドレスを見に行くと、そこにはループ終了後の最終的な値である 5 が格納されているのです。
一方、正しいコードでは、ループのたびに新しく宣言された i := i(変数のシャドウイング)によって、各ループ回にのみ属する独立した変数が作成されます。このとき、各クロージャは自分専用のコピーをキャプチャするため、互いに干渉しなくなります。
(注:最新の Go 1.22+ バージョンでは、公式に for ループの低レイヤーなセマンティクス(意味論)が修正され、この問題はデフォルトで解消されています。しかし、古いコードを理解し保守するためには、この原理を把握しておくことが極めて重要です!)