DRYな備忘録

Don't Repeat Yourself.

【Go言語】bufio.Scannerの静かな突然死に3営業日ハマった

tl;dr

  • bufio.ScannerScanが終わっても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++
    }

みんなのGo言語[現場で使える実践テクニック]

みんなのGo言語[現場で使える実践テクニック]