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 の一部です。