Goのbufio.Scannerは入力データの1行の長さが一定以上になるとスキャンをやめてしまう

Goのbufio.Scannerの落とし穴について。

概要

Goのbufio.Scannerはio.Readerを一行ずつ読み込んで行く時に非常に便利なライブラリなのだけど、タイトルの通り、入力データ(io.Reader)の1行の長さがScannerのバッファサイズを超えるとスキャンをやめてしまうという問題がある。バッファサイズはデフォルトでbufio.MaxScanTokenSize(65536)バイトとそれほど大きくないので、例えば大きめのCSVや各行にJSONが書かれているテキストファイルをScannerで処理するとこの問題に当たることがあるかもしれない。

以下は動作確認用コード。Go Playground上で実行してみたい方はこちらをどうぞ。

package main

import (
    "bufio"
    "fmt"
    "strings"
)

func main() {
    // 2行目が65537バイト(改行含む) > bufio.MaxScanTokenSize (65536)
    in := strings.NewReader("1st line\n" + strings.Repeat("X", 65536) + "\n3rd line\n")
    scanner := bufio.NewScanner(in)

    for scanner.Scan() {
        fmt.Println(scanner.Text())
    }

    if err := scanner.Err(); err != nil {
        fmt.Printf("Scanner error: %q\n", err)
    }
}

実行結果

1st line
Scanner error: "bufio.Scanner: token too long"

この場合、2行目が大きすぎてScannerのバッファに格納できないので、Scanner.Scan()がfalseを返してしまう。一応スキャン後にScanner.Err()を確認すればbufio.ErrTooLongが返ってくるので問題を検出することはできるが、わざわざErr()を手動で呼ぶ必要があるのが難点(実際Scan()後にErr()を確認しないコードを何度か見たことがある)。

対策

入力データの1行の最大長が事前に分かっている場合は、bufio.Scanner.Buffer()でバッファサイズを変更するとよい。

const (
    // 初期バッファサイズ
    initialBufSize = 10000
    // バッファサイズの最大値。Scannerは必要に応じこのサイズまでバッファを大きくして各行をスキャンする。
    // この値がinitialBufSize以下の場合、Scannerはバッファの拡張を一切行わず与えられた初期バッファのみを使う。
    maxBufSize = 1000000
)

scanner := bufio.NewScanner(in)
buf := make([]byte, initialBufSize)
scanner.Buffer(buf, maxBufSize)

入力行の最大長が事前に分からず、最大長を決め打ちできない場合は、Scannerを使うことはできない。代わりに以下のようにbufio.Reader.ReadBytes()などを使って1行ずつ読み込むという方法がある(Go Playgroundはこちら)。

package main

import (
    "bufio"
    "fmt"
    "io"
    "strings"
)

func main() {
    // 2行目が65537バイト(改行含む) > bufio.MaxScanTokenSize (65536)
    in := strings.NewReader("1st line\n" + strings.Repeat("X", 65536) + "\n3rd line\n")
    reader := bufio.NewReader(in)

    for {
        line, err := reader.ReadBytes('\n')
        if err != nil && err != io.EOF {
            fmt.Printf("Reader error: %q\n", err)
            return
        }

        // ReadBytes()がdelimiter('\n')を見つける前にEOFに到達した場合、
        // それまでに読み込まれた行のバイト列とio.EOFが返される。
        // 従って、入力の最後の行が'\n'で終わらない場合、err == io.EOFだけ確認してループをbreakしてしまうと
        // 最後の行を処理せず捨ててしまうことになるので注意。
        allLinesProcessed := err == io.EOF && len(line) == 0
        if allLinesProcessed {
            break
        }

        // ReadBytesが返したバイト列はdelimiter('\n')を含む
        print(string(line))
    }
}

実行結果

1st line
XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX (省略)
3rd line

一応目的は達成できるが、最終行が'\n'で終わらない場合を加味する必要があったりとScannerよりもコードが複雑化するので注意。