要約
- 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