注意点: ゴルーチンとスコープの罠

Go言語を利用していてハマった2つの問題点と、解決方法について紹介します。

目次

ループでgoroutineを利用するときの注意点

問題点

以下のコード例で紹介します。

func problem1() {
    numbers := []int{1, 2, 3}
    for _, n := range numbers {
        go func() {
            fmt.Println(n)
        }()
    }
}

このコードは、3つのgoroutineを立ち上げ、それぞれのgoroutineでnの値を表示します。1,2,3が出力されることを想定してましたが、以下のように3が3回表示されました。

3
3
3

goroutineが参照する変数nはforループの変数であり、その値は次のループで上書きされてしまうのが原因です。

解決方法

goroutine内で参照する変数を、goroutineの関数の引数として渡すことでスコープを分けています。

func problem1_fixed() {
    numbers := []int{1, 2, 3}
    for _, n := range numbers {
        go func(num int) {
            fmt.Println(num)
        }(n)
    }
}

この修正により、各goroutineは正しい値を参照し、期待通りの動作となります。

ループでポインタを利用するときの注意点

問題点

以下のコード例で紹介します。

type Post struct {
    ID    int
    Title string
}

func problem2() {
    posts1 := []Post{{1, "A"}, {2, "B"}, {3, "C"}}
    posts2 := make([]*Post, 0, len(posts1))
    for _, p := range posts1 {
        posts2 = append(posts2, &p)
    }

    for _, p := range posts2 {
        fmt.Println(p.Title)
    }
}

このコードはposts1からposts2へポストのポインタをコピーしています。しかし、このまま実行すると、posts2のすべての要素が最後の{3, "C"}を指してしまいます。そのため実行結果は以下のようになります。

C
C
C

これは、forループの変数pのアドレスがループごとに変わらず、最後の要素のアドレスがすべての要素に対してセットされてしまうためです。

解決方法

ポインタをスライスに保存する際は、そのポインタが指す元の要素のアドレスを直接取得する必要があります。

func problem2Fixed() {
    posts1 := []Post{{1, "A"}, {2, "B"}, {3, "C"}}
    posts2 := make([]*Post, 0, len(posts1))
    for i := range posts1 {
        posts2 = append(posts2, &posts1[i])
    }

    for _, p := range posts2 {
        fmt.Println(p.Title)
    }
}

この修正により、posts2は正しく各ポストのアドレスを保持し、期待通りの動作となります。

A
B
C

参考

よかったらシェアしてね!
目次