🦄

Go言語の関数を整理 〜クロージャ・コールバック・無名関数とは?〜

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

まとめ

  • クロージャは“変数を覚えている関数”
  • コールバック関数は“呼び出し先で差し込む関数”
  • 状態管理・可読性を考え、「目的に合った関数」を選ぼう!
  • 状態管理や複数の操作をまとめたい場合は、クロージャよりも構造体+メソッドの方がシンプルかつ拡張性も高いことを意識して、ケースごとに選択しましょう。