Go 言語のクロージャーとは? 無名関数の使い方と注意点

Go では func(引数…){ … } のように名前を付けずにその場で関数リテラルを作る書き方を「無名関数(anonymous function または closure)」と呼びます。

  • 即席でハンドラーを定義したい
  • 外側の変数を包み込んで(= クロージャ)あとで実行したい

── そんな時に便利です。

状態をもつ関数

  • 通常、状態を持たせる時は、構造体を用意し、メソッドを作成する必要がある
  • ただ、構造体自体に意味がないなら、以下のようにクロージャーを定義・使用することで、冗長な構造体を書かずに済む
package main

import "fmt"

// クロージャーは、関数が定義されたときの環境(スコープ)を「覚えている」仕組み
// つまり、関数の外側にある変数にアクセスできる関数のこと
func store() func(int) int {
	// 外側の関数の変数
	sum := 0
	// ↓ クロージャー関数
	return func(i int) int {
		// 内側の関数が外側の変数xを参照している
		sum += i
		return sum
	}
}

func main() {
	// クロージャーを変数に束縛する
	s1 := store()
	s2 := store()

	// クロージャーを呼び出す
	fmt.Println(s1(1))
	fmt.Println(s1(2))
	fmt.Println(s1(3))
	fmt.Println("別のクロージャー")
	fmt.Println(s2(4))
	fmt.Println(s2(5))
	fmt.Println(s2(6))
}

ミドルウェアの生成:異なるシグネチャ(型)を揃える

無名関数を使用することで、用意した関数の型とシグネチャが合わない時も無名関数を使用することで、シグネチャを揃えることができます。

シナリオ:既存のビジネスロジック関数を HTTP ハンドラーとして使いたい場合

既存の関数(シグネチャが合わない)

// 既存のビジネスロジック関数
func calculatePrice(productID string, quantity int) (float64, error) {
    // 商品価格計算のロジック
    basePrice := 100.0
    total := basePrice * float64(quantity)
    return total, nil
}

func getUserProfile(userID string) (string, error) {
    // ユーザープロファイル取得のロジック
    return fmt.Sprintf("User profile for ID: %s", userID), nil
}

❌ 無名関数を使用しない場合(コンパイルエラー)

func main() {
    // これはコンパイルエラーになる
    // calculatePriceのシグネチャ: func(string, int) (float64, error)
    // 期待されるシグネチャ: func(http.ResponseWriter, *http.Request)

    http.HandleFunc("/price", calculatePrice) // ❌ エラー!
    //                        ^^^^^^^^^^^
    // cannot use calculatePrice (type func(string, int) (float64, error))
    // as type func(http.ResponseWriter, *http.Request) in argument

    http.HandleFunc("/user", getUserProfile) // ❌ エラー!
    //                       ^^^^^^^^^^^^^^
    // 同様のエラー
}

✅ 無名関数を使用した場合(正常動作)

func main() {
    // 無名関数でシグネチャを合わせる
    http.HandleFunc("/price", func(w http.ResponseWriter, r *http.Request) {
        // HTTPリクエストからパラメータを取得
        productID := r.URL.Query().Get("product_id")
        quantityStr := r.URL.Query().Get("quantity")
        quantity, err := strconv.Atoi(quantityStr)
        if err != nil {
            http.Error(w, "Invalid quantity", http.StatusBadRequest)
            return
        }

        // 既存関数を呼び出し
        price, err := calculatePrice(productID, quantity)
        if err != nil {
            http.Error(w, err.Error(), http.StatusInternalServerError)
            return
        }

        // レスポンスを返す
        fmt.Fprintf(w, "Total price: %.2f", price)
    })

    http.HandleFunc("/user", func(w http.ResponseWriter, r *http.Request) {
        // HTTPリクエストからパラメータを取得
        userID := r.URL.Query().Get("user_id")

        // 既存関数を呼び出し
        profile, err := getUserProfile(userID)
        if err != nil {
            http.Error(w, err.Error(), http.StatusInternalServerError)
            return
        }

        // レスポンスを返す
        fmt.Fprintln(w, profile)
    })

    http.ListenAndServe(":8080", nil)
}

より複雑な例:認証付きハンドラー

既存の関数

// 既存の認証関数
func authenticate(username, password string) bool {
    return username == "admin" && password == "secret"
}

// 既存のデータ取得関数
func getSecretData(userID string) (map[string]interface{}, error) {
    return map[string]interface{}{
        "data": "secret information",
        "user": userID,
    }, nil
}

❌ 無名関数なしの場合

func main() {
    // これらは全てコンパイルエラー
    http.HandleFunc("/login", authenticate)    // ❌ エラー
    http.HandleFunc("/secret", getSecretData)  // ❌ エラー
}

✅ 無名関数ありの場合

func main() {
    // ログインハンドラー
    http.HandleFunc("/login", func(w http.ResponseWriter, r *http.Request) {
        username := r.FormValue("username")
        password := r.FormValue("password")

        // 既存の認証関数を使用
        if authenticate(username, password) {
            fmt.Fprintln(w, "Login successful")
        } else {
            http.Error(w, "Login failed", http.StatusUnauthorized)
        }
    })

    // シークレットデータハンドラー
    http.HandleFunc("/secret", func(w http.ResponseWriter, r *http.Request) {
        userID := r.Header.Get("User-ID")

        // 既存のデータ取得関数を使用
        data, err := getSecretData(userID)
        if err != nil {
            http.Error(w, err.Error(), http.StatusInternalServerError)
            return
        }

        // JSONレスポンス
        w.Header().Set("Content-Type", "application/json")
        json.NewEncoder(w).Encode(data)
    })

    http.ListenAndServe(":8080", nil)
}

項目 無名関数なし 無名関数あり
コンパイル ❌ エラー ✅ 成功
既存関数の再利用 ❌ 不可能 ✅ 可能
HTTP パラメータ処理 ❌ 不可能 ✅ 可能
エラーハンドリング ❌ 不可能 ✅ 可能
レスポンス形成 ❌ 不可能 ✅ 可能

このように、無名関数を使用することで、既存のビジネスロジックを変更することなく、HTTP ハンドラーとして利用できるようになります。これにより、コードの再利用性と保守性が大幅に向上します。

ルーチンで無名関数から外部変数を参照することの問題

1. 競合状態(Race Condition)

package main

import (
    "fmt"
    "sync"
    "time"
)

// ❌ 問題のあるコード
func badExample() {
    var counter int
    var wg sync.WaitGroup

    for i := 0; i < 1000; i++ {
        wg.Add(1)
        go func() {
            defer wg.Done()
            counter++ // 複数のゴルーチンが同じ変数に同時アクセス
        }()
    }

    wg.Wait()
    fmt.Printf("Counter: %d\n", counter) // 期待値1000だが、実際は不定
}

// ✅ 改善されたコード
func goodExample() {
    var counter int
    var wg sync.WaitGroup
    var mu sync.Mutex

    for i := 0; i < 1000; i++ {
        wg.Add(1)
        go func() {
            defer wg.Done()
            mu.Lock()
            counter++ // ミューテックスで保護
            mu.Unlock()
        }()
    }

    wg.Wait()
    fmt.Printf("Counter: %d\n", counter) // 正確に1000
}

2. 変数の予期しない共有

// ❌ 問題のあるコード:ループ変数の共有
func badLoopExample() {
    var wg sync.WaitGroup

    for i := 0; i < 5; i++ {
        wg.Add(1)
        go func() {
            defer wg.Done()
            fmt.Printf("Value: %d\n", i) // 全て同じ値(5)を出力する可能性
        }()
    }

    wg.Wait()
}

// ✅ 改善されたコード:値を明示的に渡す
func goodLoopExample() {
    var wg sync.WaitGroup

    for i := 0; i < 5; i++ {
        wg.Add(1)
        go func(val int) { // パラメータとして渡す
            defer wg.Done()
            fmt.Printf("Value: %d\n", val) // 期待通りの値を出力
        }(i)
    }

    wg.Wait()
}

3. スライスの共有による問題

// ❌ 問題のあるコード
func badSliceExample() {
    data := []int{1, 2, 3, 4, 5}
    var wg sync.WaitGroup

    for i := 0; i < len(data); i++ {
        wg.Add(1)
        go func() {
            defer wg.Done()
            data[i] = data[i] * 2 // 競合状態 + インデックス範囲外エラーの可能性
        }()
    }

    wg.Wait()
}

// ✅ 改善されたコード
func goodSliceExample() {
    data := []int{1, 2, 3, 4, 5}
    var wg sync.WaitGroup

    for i := 0; i < len(data); i++ {
        wg.Add(1)
        go func(index int, slice []int) { // 値を明示的に渡す
            defer wg.Done()
            slice[index] = slice[index] * 2
        }(i, data)
    }

    wg.Wait()
}

4. Web アプリケーションでの実例

// ❌ 危険なコード:HTTPハンドラーでの共有変数
func badWebExample() {
    requestCount := 0 // 共有変数

    http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
        go func() {
            requestCount++ // 競合状態
            fmt.Printf("Request count: %d\n", requestCount)
        }()

        fmt.Fprintln(w, "Hello World")
    })
}

// ✅ 安全なコード:適切な同期化
func goodWebExample() {
    var requestCount int64

    http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
        go func() {
            atomic.AddInt64(&requestCount, 1) // アトミック操作
            count := atomic.LoadInt64(&requestCount)
            fmt.Printf("Request count: %d\n", count)
        }()

        fmt.Fprintln(w, "Hello World")
    })
}

5. チャネルを使った解決方法

// ✅ チャネルを使った安全なアプローチ
func channelExample() {
    jobs := make(chan int, 100)
    results := make(chan int, 100)

    // ワーカーゴルーチン
    for w := 1; w <= 3; w++ {
        go func(id int) {
            for job := range jobs {
                result := job * 2 // 外部変数に依存しない
                results <- result
            }
        }(w)
    }

    // ジョブを送信
    for j := 1; j <= 9; j++ {
        jobs <- j
    }
    close(jobs)

    // 結果を受信
    for r := 1; r <= 9; r++ {
        <-results
    }
}

対策方法一覧

1. 値渡し

go func(val int) {
    // valは各ゴルーチンで独立
}(externalVar)

2. 同期プリミティブ

var mu sync.Mutex
go func() {
    mu.Lock()
    // 共有リソースへの安全なアクセス
    mu.Unlock()
}()

3. アトミック操作

go func() {
    atomic.AddInt64(&counter, 1)
}()

4. チャネル

ch := make(chan int)
go func() {
    ch <- computeValue() // チャネル経由で安全に通信
}()

ゴルーチンで外部変数を参照する際の主な問題:

1. データ競合: 複数のゴルーチンが同じメモリ位置に同時アクセス
2. 予期しない共有: 変数が意図せず共有される
3. デバッグの困難さ: 非決定的な動作により再現が困難

これらの問題を避けるため、値渡し適切な同期化チャネルなどを使用することが推奨されます。

まとめ

Go 言語のクロージャーは、柔軟性と再利用性の高いコードを記述するための強力なツールです。特に、無名関数を使用することで、既存の関数をラップし、新しい文脈やシグネチャに適応させることができます。ただし、クロージャーを使用する際には、外部変数の参照に伴う競合状態や予期しない動作に注意が必要です。

競合状態を防ぐためには、値渡し、同期プリミティブ(ミューテックスやアトミック操作)、またはチャネルを活用することが重要です。これにより、ゴルーチンの安全性が確保され、信頼性の高い並行処理が可能になります。

さらに、HTTP ハンドラーのような現実的なシナリオにおいても、無名関数を活用することで、既存のビジネスロジックを効率的に再利用することができます。

このように、Go 言語のクロージャーは、プログラムの簡潔性と保守性を高めるだけでなく、複雑なタスクをより直感的に実現する手段を提供します。適切な注意を払いながら使用することで、その潜在能力を最大限に引き出すことができるでしょう。

よかったらシェアしてね!
  • URLをコピーしました!

この記事を書いた人

はじめまして、「知識を愛する者」愛知郎です。

五反田でエンジニアとして活動してます。

こちらでは、私が最近学んだこと発信しています。

目次