1番、よくやるやつ(やばめ)
for _, user := range users { go func() { user.SomeHeavy() }() }
非同期で生成されるクロージャの中で、参照されるuserはループ内スコープのそれなので、たとえばループのn番目とn+1番目の処理において、同じn+1番目のユーザに重複してSomeHeavy()を実行してしまう危険性がある。ぜんぜん閉包じゃない。今回の主題は、この潜在的なエンバグポイントを、どうすれば事前に防げるか、防げるコーディング規約があるか、みたいな興味。
2番、よくやるやつ(だいたい)
for _, user := range users { go func(u *User) { u.SomeHeavy() }(user) }
ちゃんとクロージャ内でのスコープとなるように渡す。これなら1番の問題は起きない。
そもそも、1番のように書けなければ問題無いんだけど、コンパイルは普通に通るし、書いてもgo vetが叱ってくれないような気がする(叱ってくれる?)。
3番、なるべく名前のある関数にすればいいんではないか?
doHeavyToUser := func(u *User) { u.SomeHeavy() } for _, user := range users { go doHeavyToUser(user) }
これで1番のときみたいな問題はなるべく起きないようになった気がする
でも、ループでgoroutineつくるとき、ほとんどの場合「たくさん作ったgoroutineが全員帰ってくるまで待つ」みたいなことしたいので、以下4番のように書くことが多い
4番、sync.WaitGroupつかう(主題とはかんけいなく、よくやるよね、っていうコード)
var g sync.WaitGroup for _, user := range users { // カウンタ増やす g.Add(1) go func(u *User) { u.SomeHeavy() // カウンタ減らす g.Done() }(user) } // カウンタがゼロになるまで待つ g.Wait() fmt.Println("おわったー")
これも含めて、3番のように書こうと思ったら、以下5番のように書かざるをえない?
5番、抜き出した名前付き関数にWaitGroupも渡してしまう
doHeavyToUser := func(u *User, wg sync.WaitGroup) { u.SomeHeavy() wg.Done() } var g sync.WaitGroup for _, user := range users { g.Add(1) doHeavyToUser(user, g) } g.Wait() fmt.Println("おわったー")
逆に見通し悪くないか。なお、doHeavyToUserをループ生成しているメソッドの外で定義すると、逆に以下のように叱られる
doHeavyToUser passes Lock by value: sync.WaitGroup contains sync.Mutex
「doHeavyToUser関数は処理をLockしちゃうかもよ」というgo vetの親切心。
現実的には4番がいちばん分かりやすいのかな。uの部分をuserと書かないように気をつけるしかないのかな...
DRY