Code Fix

中級

クロージャがループ外の変数を共有キャプチャして最終値しか残らない

forループの外側で宣言した変数をクロージャの中で使い、そのクロージャをスライスに貯めていく場合、各クロージャは同じ変数を参照し続けます。実行時にはループ終了後の最終値だけが全クロージャに共通して見えます。

なぜエラーが出ないのか

出力: [3 3 3]
(エラーなし)
Goは何も報告しません。コンパイルも実行も正常に終わるコードだからです
出力: [3 3 3]
実際の出力 — 期待した結果と食い違っている箇所
見つけ方
エラーが出ないので、出力を目で確かめるしかありません。この種の誤りが最も発見が遅れます

このエラーが出る典型パターン

パターン1

 1  package main
 2  
 3  import "fmt"
 4  
 5  func main() {
 6  	var funcs []func() int
 7  	sum := 0
 8  	for i := 0; i < 3; i++ {
 9  		sum += i
10  		s := sum
11  		_ = s
12  		funcs = append(funcs, func() int { return sum })
                                                 ^
13  	}
14  	var results []int
15  	for _, f := range funcs {
16  		results = append(results, f())
17  	}
18  	fmt.Println(results)
19  }
出力: [3 3 3]

sumはループの外で宣言された1つの変数で、全クロージャに共有されます。ループの各回でs := sumのように新しい変数へコピーしてから使う必要があります。

直し方: sums にします。

この問題を解いてみる →

広告
広告スロット(未設定)

パターン2

 1  package main
 2  
 3  import "fmt"
 4  
 5  func main() {
 6  	var funcs []func() int
 7  	sum := 0
 8  	for i := 0; i < 3; i++ {
 9  		sum += i * 2
10  		s := sum
11  		_ = s
12  		funcs = append(funcs, func() int { return sum })
                                                 ^
13  	}
14  	var results []int
15  	for _, f := range funcs {
16  		results = append(results, f())
17  	}
18  	fmt.Println(results)
19  }
出力: [6 6 6]

加算の中身が変わっても、sumが1つの変数を全クロージャで共有している構造自体は変わりません。

直し方: sums にします。

この問題を解いてみる →

パターン3

 1  package main
 2  
 3  import "fmt"
 4  
 5  func main() {
 6  	var funcs []func() int
 7  	sum := 0
 8  	for i := 0; i < 3; i++ {
 9  		sum += 10
10  		s := sum
11  		_ = s
12  		funcs = append(funcs, func() int { return sum })
                                                 ^
13  	}
14  	var results []int
15  	for _, f := range funcs {
16  		results = append(results, f())
17  	}
18  	fmt.Println(results)
19  }
出力: [30 30 30]

iを使わない定数加算でも、共有変数を直接クロージャに使えば同じ問題が起きます。

直し方: sums にします。

この問題を解いてみる →

パターン4

 1  package main
 2  
 3  import "fmt"
 4  
 5  func main() {
 6  	var funcs []func() int
 7  	sum := 0
 8  	for i := 0; i < 3; i++ {
 9  		sum += i + 1
10  		s := sum
11  		_ = s
12  		funcs = append(funcs, func() int { return sum })
                                                 ^
13  	}
14  	var results []int
15  	for _, f := range funcs {
16  		results = append(results, f())
17  	}
18  	fmt.Println(results)
19  }
出力: [6 6 6]

クロージャが呼ばれるのはループが終わった後なので、共有変数の値は常に最終状態のまま観測されます。

直し方: sums にします。

この問題を解いてみる →

パターン5

 1  package main
 2  
 3  import "fmt"
 4  
 5  func main() {
 6  	var funcs []func() int
 7  	sum := 0
 8  	for i := 0; i < 4; i++ {
 9  		sum += i
10  		s := sum
11  		_ = s
12  		funcs = append(funcs, func() int { return sum })
                                                 ^
13  	}
14  	var results []int
15  	for _, f := range funcs {
16  		results = append(results, f())
17  	}
18  	fmt.Println(results)
19  }
出力: [6 6 6 6]ループは4回

ループの回数が増えても、共有変数を直接参照している限り全クロージャが同じ最終値を返します。

直し方: sums にします。

この問題を解いてみる →

よくある誤解

「クロージャを作った時点の値がそれぞれ個別に固定されるはず」という思い込みは誤りです。Go 1.22以降でforの各回のループ変数自体は新しいインスタンスになりましたが、ループの外で宣言してループ内で使い回している変数はこの対象外で、依然として全クロージャに共有されます。

まとめ

クロージャがループ外の変数を共有キャプチャして最終値しか残らないは中級でつまずきやすい項目です。上の5パターンを実際に手で直すと、エラーメッセージのどこを読めばよいかが掴めます。

演習をはじめる

関連するエラー

広告
広告スロット(未設定)