Go Wiki: よくある間違い
目次
はじめに
新しいプログラマーがGoを使い始める時や、経験豊富なGoプログラマーが新しい概念を使い始める時に、多くの人が犯す共通の誤りがあります。ここでは、メーリングリストやIRCで頻繁に見られるいくつかの間違いの網羅的ではないリストを示します。
ループイテレータ変数への参照の使用
注:以下のセクションはGo < 1.22に適用されます。Goバージョン >= 1.22では、イテレーションにスコープされた変数が使用されます。詳細については、Go 1.22でのforループの修正を参照してください。
Goでは、ループイテレータ変数は各ループイテレーションで異なる値を取る単一の変数です。これは非常に効率的ですが、誤って使用すると意図しない動作につながる可能性があります。例えば、次のプログラムを見てください。
func main() {
var out []*int
for i := 0; i < 3; i++ {
out = append(out, &i)
}
fmt.Println("Values:", *out[0], *out[1], *out[2])
fmt.Println("Addresses:", out[0], out[1], out[2])
}
予期せぬ結果が出力されます
Values: 3 3 3
Addresses: 0x40e020 0x40e020 0x40e020
説明:各イテレーションで、`i`のアドレスを`out`スライスに追加していますが、同じ変数であるため、最終的に`i`に割り当てられた最後の値を含む同じアドレスを追加しています。解決策の1つは、ループ変数を新しい変数にコピーすることです。
for i := 0; i < 3; i++ {
+ i := i // Copy i into a new variable.
out = append(out, &i)
}
プログラムの新しい出力は期待通りです。
Values: 0 1 2
Addresses: 0x40e024 0x40e028 0x40e032
説明:行`i := i`は、ループ変数`i`を、forループボディブロックにスコープされた新しい変数(これも`i`と呼ばれます)にコピーします。新しい変数のアドレスが配列に追加され、forループボディブロックよりも長生きします。各ループイテレーションで新しい変数が作成されます。
この例は少し分かりやすいかもしれませんが、同じ予期せぬ動作が他のケースではより隠れている可能性があります。例えば、ループ変数が配列で、参照がスライスである場合などです。
func main() {
var out [][]int
for _, i := range [][1]int{{1}, {2}, {3}} {
out = append(out, i[:])
}
fmt.Println("Values:", out)
}
出力
Values: [[3] [3] [3]]
ループ変数がゴルーチンで使用されている場合でも、同じ問題が発生する可能性があります(次のセクションを参照)。
ループイテレータ変数でのゴルーチンの使用
注:以下のセクションはGo < 1.22に適用されます。Goバージョン >= 1.22では、イテレーションにスコープされた変数が使用されます。詳細については、Go 1.22でのforループの修正を参照してください。
Goで反復処理を行う際、ゴルーチンを使用してデータを並列処理しようとすることがあります。例えば、クロージャを使用して次のようなコードを書くかもしれません。
for _, val := range values {
go func() {
fmt.Println(val)
}()
}
上記のforループは、期待どおりに動作しない可能性があります。なぜなら、その`val`変数は実際には、各スライス要素の値を取る単一の変数だからです。クロージャはすべてその1つの変数にのみバインドされているため、このコードを実行すると、ゴルーチンがループの終了後に実行を開始する可能性が高いため、各値が順番に表示されるのではなく、すべてのイテレーションで最後の要素が印刷される可能性が非常に高いです。
そのクロージャループを正しく書く方法は次のとおりです。
for _, val := range values {
go func(val interface{}) {
fmt.Println(val)
}(val)
}
クロージャに`val`をパラメータとして追加することで、`val`は各イテレーションで評価され、ゴルーチンのスタックに配置されます。そのため、各スライス要素は、最終的に実行されるときにゴルーチンから利用可能になります。
ループ本体内で宣言された変数はイテレーション間で共有されないため、クロージャ内で個別に利用できることにも注意することが重要です。次のコードは、共通のインデックス変数`i`を使用して個別の`val`を作成し、期待される動作をもたらします。
for i := range valslice {
val := valslice[i]
go func() {
fmt.Println(val)
}()
}
このクロージャをゴルーチンとして実行しない場合、コードは期待どおりに実行されることに注意してください。次の例は、1から10までの整数を出力します。
for i := 1; i <= 10; i++ {
func() {
fmt.Println(i)
}()
}
クロージャはすべて同じ変数(この場合は`i`)を閉じ込めていますが、変数が変更される前に実行されるため、望ましい動作になります。https://go.dokyumento.jp/doc/faq#closures_and_goroutines
次のような別の類似した状況を見つけるかもしれません。
for _, val := range values {
go val.MyMethod()
}
func (v *val) MyMethod() {
fmt.Println(v)
}
上記の例もvaluesの最後の要素を出力します。理由はクロージャと同じです。問題を解決するには、ループ内で別の変数を宣言します。
for _, val := range values {
newVal := val
go newVal.MyMethod()
}
func (v *val) MyMethod() {
fmt.Println(v)
}
このコンテンツはGo Wikiの一部です。