【Go言語】bufio.Scannerの静かな突然死に3営業日ハマった
tl;dr
bufio.Scanner
でScan
が終わってもErr
はちゃんと見ましょう。以上。
問題
継続的に書き込みのあるTCP接続からの読み込みを、bufio.Scanner
をつかって華麗に逐次読み込みしていた。が、どうやらそれが静かに切断され、クライアント(こちら)側のローカルでの処理が先走っているように見える挙動があった。
res, err := ExecRemoteLongScript() defer res.Body.Close() scanner := bufio.NewScanner(res.Body) for scanner.Scan() { // do something for scanner.Bytes() } fmt.Println("done")
みたいなので、RemoteScript
(概念)が完全に終了していないのに、クライアント(こちら)側でfmt.Println("done")
に到達してしまっている。
再現と原因
すごく色々なレイヤーのある話だったので、色々調査したんですが、最終的に再現できたのはこれでした。
package main import ( "bufio" "fmt" "io" ) func dummyExecRemoteScript() io.Reader { r, w := io.Pipe() text := "Test" go func() { for i := 0; i < 20; i++ { text += text w.Write([]byte(text + "\n")) } r.Close() }() return r } func main() { body := dummyExecRemoteScript() scanner := bufio.NewScanner(body) i := 0 for scanner.Scan() { fmt.Println(i, len(scanner.Bytes())) i++ } fmt.Println("done") }
0から19までナンバリングされたレスポンス断片が出力されるはずなんですが、なぜか12番までしか出力されません https://play.golang.org/p/DDeu8kfrU5D。なんでやろなーと思って( ゚д゚)ハッ!っと気づいたのが、bufio.Scanner.Err
です。
ということで、
for scanner.Scan() {
fmt.Println(i, len(scanner.Bytes()))
i++
}
+ fmt.Println("Err", scanner.Err())
fmt.Println("done")
としたら、これがビンゴでした。 https://play.golang.org/p/HlR7SpCTSkc
0 8 1 16 2 32 3 64 4 128 5 256 6 512 7 1024 8 2048 9 4096 10 8192 11 16384 12 32768 Err bufio.Scanner: token too long done
Scanning stops unrecoverably at EOF, the first I/O error, or a token too large to fit in the buffer. When a scan stops, the reader may have advanced arbitrarily far past the last token. Programs that need more control over error handling or large tokens, or must run sequential scans on a reader, should use bufio.Reader instead.
https://golang.org/pkg/bufio/#Scanner
解決
上記の通り、bufio.Scannerではなく、bufio.Readerを使って同じ内容を書き直します。
func main() { body := dummyExecRemoteScript() reader := bufio.NewReader(body) i := 0 for { b, err := reader.ReadBytes('\n') if err != nil { if err == io.EOF || err == io.ErrClosedPipe { break } panic(err) } fmt.Println(i, len(b)) i++ } fmt.Println("done") }
https://play.golang.org/p/vmctCOfK9nM
まとめ
bufio.Scanner
は便利だけど、Scan
が終わってもErr
は必ず見るようにしましょう
自戒
DRYな備忘録として
追記 ⛳
コードゴルフできるところみつけたんでちょっと変えた
- i := 0 - for { + for i := 0; ; i++ { b, err := reader.ReadBytes('\n') if err != nil { if err == io.EOF || err == io.ErrClosedPipe { break } panic(err) } fmt.Println(i, len(b)) - i++ }