Go言語の関数を整理 〜クロージャ・コールバック・無名関数とは?〜
目次
はじめに
Go を書いていて「クロージャ」「無名関数」「コールバック関数」……似たような言葉がたくさん出てきて、「違いは何?」「どんな時に使い分ければいい?」と迷うことはありませんか?
この記事では、Go の関数を整理した上で、特にクロージャとコールバック関数に着目しています。
1. Go の関数の基礎知識
Go では関数を「値」として扱うことができます。 まずは基本のおさらいから。
通常の関数
func add(a, b int) int {
return a + b
}
無名関数(Anonymous Function)
その場で関数を定義し、変数に入れたり、直接渡したりできます。
f := func(x int) int {
return x * 2
}
fmt.Println(f(3)) // 6
メソッド(Method)
構造体の「ふるまい」を実装する場合はこちら。
type Counter struct {
n int
}
func (c *Counter) Increment() {
c.n++
}
2. クロージャとは何か?
クロージャとは「関数の外で宣言された変数を、関数の中で参照・更新できる関数」のことです。
// --- クロージャの基本例 ---
func adder() func(int) int {
sum := 0 // adder()関数のスコープにだけ存在する「sum」
return func(x int) int {
sum += x // 呼び出すたびにsumが加算される
return sum // 現在のsumを返す
}
}
func main() {
a := adder() // aは「sumという状態」を覚えた関数
fmt.Println(a(1)) // 1(sum = 0+1)
fmt.Println(a(2)) // 3(sum = 1+2)
fmt.Println(a(10)) // 13(sum = 3+10)
}
この例のaは、「sum」という**関数の外側の変数をずっと記憶し続けている関数(=クロージャ)**です。
詳細解説
a := adder()で「sum=0 の状態」を持った関数を返す。a(1)を呼ぶと、sum が 1 になり 1 を返す。a(2)を呼ぶと、前回の sum=1 に 2 が加算され、3 を返す。- さらに
a(10)を呼ぶと、sum は 13 になる。
なぜ sum はリセットされないのか?
この「sum」という変数は、a という関数が生きている限り、その中でずっと維持されます。なぜなら関数は、中のコードが外の変数にアクセスすると参照を維持し、その変数もヒープ上に移動されてデータがメモリ上に保持され続けるためです。
3. クロージャと構造体メソッドの「適材適所」
クロージャは状態(変数)を保持できる便利な仕組みですが、その 状態に複数の操作(例:加算とリセット)を行いたい場合はコードの可読性が下がると思います。その場合は構造体+メソッドを使う方がシンプルだと考えています。
クロージャで複数操作を持たせる例
func adder() (func(int) int, func()) {
num := 0
return func(i int) int {
num += i
return num
}, func() {
num = 0
}
}
func main() {
a, reset := adder()
fmt.Println(a(1)) // 1
reset()
fmt.Println(a(2)) // 2
}
「add」と「reset」を一つの関数でセットにして返すため、 扱いが複雑になりがちです。
構造体+メソッドで書く場合(より明快!)
type Adder struct {
num int
}
func (a *Adder) Add(i int) int {
a.num += i
return a.num
}
func (a *Adder) Reset() {
a.num = 0
}
func main() {
a := &Adder{}
fmt.Println(a.Add(1)) // 1
a.Reset()
fmt.Println(a.Add(2)) // 2
}
こちらは「状態(num)」と「動作(Add, Reset)」が分かれ、 コードもシンプルでメンテしやすくなります。
このように、 「一時的なカウンタやシンプルな状態」ならクロージャで十分ですが、 「複数の操作や可読性」を求めるなら構造体+メソッドを選ぶのが良いのかなと思いました。
4. コールバック関数とは何か?
コールバック関数は、「関数を引数として渡し、途中や特定のタイミングで呼び出してもらう」パターンが多いと思います。
コールバック関数の実用例
例えば配列やスライスの各要素に好きな処理を適用したい場合、コールバック関数が活躍します。
// スライスの各要素をprint+コールバック
func process(items []int, f func(int)) {
for idx, item := range items {
fmt.Printf("[%d]要素: %d\n", idx, item)
f(item)
}
}
func main() {
nums := []int{1, 2, 3}
// 例えば各要素を2倍にして出力する無名関数(クロージャも可)
process(nums, func(n int) {
fmt.Println("値×2:", n*2)
})
}
// 出力例:
// [0]要素: 1
// 値×2: 2
// [1]要素: 2
// 値×2: 4
// [2]要素: 3
// 値×2: 6
まとめ
- クロージャは“変数を覚えている関数”
- コールバック関数は“呼び出し先で差し込む関数”
- 状態管理・可読性を考え、「目的に合った関数」を選ぼう!
- 状態管理や複数の操作をまとめたい場合は、クロージャよりも構造体+メソッドの方がシンプルかつ拡張性も高いことを意識して、ケースごとに選択しましょう。