要約
- goroutineのfor-selectパターンでは、ゼロ値を受信して予期せず無限ループすることがある
- 受信チャネルから値を受け取る時は、第2返り値でゼロ値チェックをしよう
for {
select{
case v, ok := <-ch:
if ok {
// 正常系処理
} else {
// 異常系処理
}
}
}
発生状況
無限ループが発生したコードがこちら。
package main
import (
"bufio"
"context"
"fmt"
"io"
"os"
"strings"
"time"
)
func main() {
if err := start(); err != nil {
fmt.Fprintf(os.Stderr, "Error: %s\n", err)
os.Exit(1)
}
}
func start() error {
bc := context.Background()
timeout := 5 * time.Second // 制限時間5秒
ctx, cancel := context.WithTimeout(bc, timeout)
defer cancel()
// wordCh := input(os.Stdin)
wordCh := input(strings.NewReader("hey"))
for {
select {
case input := <-wordCh:
fmt.Printf(">> %s\n", input)
case <-ctx.Done():
fmt.Println("Done")
return nil
}
}
}
func input(r io.Reader) <-chan string {
wordCh := make(chan string)
go func() {
s := bufio.NewScanner(r)
for s.Scan() {
wordCh <- s.Text()
}
close(wordCh)
}()
return wordCh
}
標準入力(os.Stdin
)から値を受け取るgoroutineを起動し、チャネル経由でその値を無限ループ内に渡して、標準出力に出すコード。
無限ループ内でselect-caseを使い、送信チャネルからの値を待つ、いわゆる(?)for-selectパターン。
処理を簡素化するため、標準入力を静的なモック(strings.NewReader("hey")
)に差し替えている。
問題は、チャネル経由で1回しか値を送信をしていないのに、制限時間(5秒)まで無限ループが止まらないこと。
実行結果
timeout running program
>> hey
>>
>>
>>
>>
>>
>>
........
原因
チャネルがCloseされた後に、ゼロ値を受信していた。
After calling close, and after any previously sent values have been received,
receive operations will return the zero value for the channel's type without blocking.
close(ch)
でチャネルを閉めた後に受信チャネルを呼ぶと、処理をブロックせずにゼロ値が返される。
無限ループ内で閉めた受信チャネルを呼んでいたので、制限時間まで処理が続いていた。
解決方法
受信チャネルの第2返り値には真偽値が入っていて、
そのチャネルを通ってきた値が、送信チャネルから帰ってきた値(true)なのか、ゼロ値(false)なのかという判別をすることができる。
x, ok := <-ch
The value of ok is true if the value received was delivered by a successful send operation to the channel,
or false if it is a zero value generated because the channel is closed and empty.
送信チャネルから送られた値のみを処理するようにif文を追記。
for {
select {
case input, ok := <-wordCh:
if ok {
fmt.Print(">> %s",input)
} else {
return nil
}
case <-ctx.Done():
fmt.Println("Done")
return nil
}
}
実行結果
>> hey