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