Go言語

Go言語初心者がハマりがちなforループ内でのgoroutine(ゴルーチン)

どうも、駆け出しGopherです。

今回は、forループ内でgoroutine(ゴルーチン)を使う場合にGo言語初心者がハマりがちな罠について書きます。

結論から書くと

結論

forループ内で別のgoroutineでクロージャーを実行する場合には注意が必要!!

○ 正常に動く例 通常の関数をgoroutineで実行

特に何も意識する必要のないケースです。

package main

import (
    "fmt"
    "sync"
)

var wg sync.WaitGroup

func main() {
    names := []string{"Taro", "Ziro", "Saburo"}

    for _, name := range names {
        wg.Add(1)
        go sayHello(name)
    }
    wg.Wait()
    fmt.Println("DONE")

}

func sayHello(name string) {
    defer wg.Done()
    fmt.Printf("Hello, %s\n", name)
}

実行結果は想定通り

実行結果

Hello, Saburo
Hello, Ziro
Hello, Taro
DONE

× 正常に動かない例 クロージャーをgoroutineで実行

このパターンは注意が必要!!

package main

import (
    "fmt"
    "sync"
)

var wg sync.WaitGroup

func main() {
    names := []string{"Taro", "Ziro", "Saburo"}

    for _, name := range names {
        wg.Add(1)
        go func() {
            defer wg.Done()
            fmt.Printf("Hello, %s\n", name)
        }()
    }
    wg.Wait()
    fmt.Println("DONE")

}

一見、問題なさそうですがHello, Saburo」しか出力されません。

実行結果

Hello, Saburo
Hello, Saburo
Hello, Saburo
DONE

この例では、goroutineは反復変数 name を囲むクロージャーを実行しています。

goroutineの実行は任意のタイミングで行われるので、実行時の name の値は不確定
たいていのマシンではgoroutineが開始される前にforループが終了してしまいます。

つまり、何が起こっているかと言うと

注意ポイント

goroutine開始前にforループが終了
→ name 変数はスコープ外になる。
→ Goのランタイムは name が参照されているのを把握していて、参照できるようにヒープに移す。このとき、最後の値である「Saburo」への参照を保持したまま
→ 「Hello, Saburo」のみが出力される

go vet を使うと怪しいコードを指摘してくれて、「./prog.go:16:30: loop variable name captured by func literal 」と警告してくれます。

○ 正しく動くように修正

name 変数のコピーをクロージャーに渡して、forループ終了後も意図している値を保持できるようにする。

package main

import (
    "fmt"
    "sync"
)

var wg sync.WaitGroup

func main() {
    names := []string{"Taro", "Ziro", "Saburo"}
    for _, name := range names {
        wg.Add(1)
        go func(name string) {
            defer wg.Done()
            fmt.Printf("Hello, %s\n", name)
        }(name)
    }
    wg.Wait()
    fmt.Println("DONE")
}

実行結果は、

実行結果

Hello, Saburo
Hello, Ziro
Hello, Taro
DONE

引数としてnameを渡すことでname(ここでは文字列)のコピーが行われ、goroutine実行時に適切な値を参照することができるようになりました。

めでたし、めでたし!!

参考図書

-Go言語

© 2021 フリエン生活 Powered by AFFINGER5