🙆‍♀️

Golang クロージャーの基本とfor 文での注意点

2024/09/04に公開
2

はじめに

このページでは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 にアクセスできるクロージャーを形成しています。counter1counter2 は別々のクロージャーであり、それぞれ独立した 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

tenkohtenkoh

こんにちは!

クロージャーの注意点とfor rangeでの問題

Go1.22以降では挙動が変更されているので、バージョンを明記された方が読者に混乱を与えないと思いました。
Go1.22からはループごとに新しい変数が使われるようになったので、最初の例でも意図通りの挙動になります。

skrikztsskrikzts

tenkohさん
コメントありがとうございます!

Go1.22からはループごとに新しい変数が使われるようになった

そうだったんですね、、知らなかった、、
わかりにくい処理だったのでよかったです。
更新しときます!ありがとうございます!