はじめに
本記事は, Goクイズ Advent Calender 2020の12/4用に書かれました。昨日は, @syumai さんのGo Quiz Advent Calendar【3日目】でした。
これから紹介するクイズは, 実際にアルバイト先で見つけたコードを一部改変したものです。
問題
以下のコードを実行すると, どのように出力されるでしょうか?
Playground
package main
import (
"fmt"
)
type character string
func sayHello(name *character) {
fmt.Printf("Hello, %s! ", *name)
}
func main() {
names := []character{"Alice", "Bob", "Carol", "Dave", "Eve"}
for _, name := range names {
go sayHello(&name)
}
}
- (何も出力されない)
- Hello, Alice! Hello, Bob! Hello, Carol! Hello, Dave! Hello, Eve!
- Hello, Alice! Hello, Bob! Hello, Carol! Hello, Dave! Hello, Eve!(順不同)
- Hello, Eve! Hello, Eve! Hello, Eve! Hello, Eve! Hello, Eve!
解答と解説
解答と解説
※2020/12/04追記:
こちらのクイズには不備があり, 全ての選択肢が発生する可能性があります。作問ミスです。申し訳ありませんでした......
@kyoh86 さん, ご指摘ありがとうございます。
TL;DR
- 呼び出し元の関数が終了すると, 呼び出し元によって生成されたgoroutineは終了する
- 対処法は, channelかsync.WaitGroupを用いること
解説
go sayHello(name)
とあるので, goroutineに関する並行処理が絡んでいると気づいた方が多かったのではないのでしょうか?
解答でも書いた通り, 全ての答えが発生し得ます。
なぜなら, main関数の終了によって, 新しく生成されたgoroutineが終了しますが, そのタイミングや順番は規定されていないためです。
Specificationには下記のように書かれています。
The function value and parameters are evaluated as usual in the calling goroutine, but unlike with a regular call, program execution does not wait for the invoked function to complete. Instead, the function begins executing independently in a new goroutine. When the function terminates, its goroutine also terminates. If the function has any return values, they are discarded when the function completes.
関数の値とパラメータは通常どおり呼び出し元のgoroutineで評価されますが, 通常の呼び出しとは異なり, プログラムの実行は呼び出された関数が完了することを待ちません。その代わり呼び出された関数は新しいgoroutineで独立して実行を始めます。もし, 呼び出し元の関数が終了すると呼び出されたgoroutineも終了します。その関数がいかなる返り値を持とうと関数の終了時に破棄されます。
これを起こさないために, 下記の通りsync.WaitGroup()
やchannel
を用いて非同期処理が終わるまである程度待つなどの対策が出来ます。
package main
import (
"fmt"
"sync"
)
type character string
func sayHello(name *character, wg *sync.WaitGroup) {
defer func() { wg.Done() }()
fmt.Printf("Hello, %s! ", *name)
}
func main() {
names := []character{"Alice", "Bob", "Carol", "Dave", "Eve"}
wg := &sync.WaitGroup{}
for _, name := range names {
wg.Add(1)
go sayHello(&name, wg)
}
wg.Wait()
}
package main
import (
"fmt"
)
type character string
func sayHello(name *character, quit chan bool) {
fmt.Printf("Hello, %s! ", *name)
quit <- true
}
func main() {
names := []character{"Alice", "Bob", "Carol", "Dave", "Eve"}
quit := make(chan bool)
for _, name := range names {
go sayHello(&name, quit)
// こちらにquitを書くと, 必ずAlice, Bob, Carol, Dave, Eveの順で出力される
// <-quit
}
for _, _ = range names {
<-quit
}
}
おわりに
こちらのコードはスクレイピング用のコードに潜んでいたバグでした。何度実行しても想定通りに動かないので, 何度か実行していた時に問題に気づけました。
以上で, Goクイズは終わりです。
ここまでお付き合いいただきありがとうございました!
明日は, @hajimehoshi さんのGo Quizです。
ありがとうございました!