bufio の Peek() を理解していなかった

Go でコードを書いていて「おや、これは…？」と頭の中がハテナで一杯になる現象に出くわして、[@nobonoboさん](https://twitter.com/nobonobo)や[@mattn](https://twitter.com/mattn_jp/)さんに教えてもらったのでまとめておきます。ありがとうございます！　もし間違いがあればご指摘ください。

## 問題のコード

これは `goquery` というライブラリを使って `<title>` タグのテキストを抜き出そうとしているコードです。が、これは正しくテキストが抜き出せません。（説明のために色んな処理を省いたり、変なコードの書き方になっています…）

```go
res, err := http.Get("http://example.com/")
if err != nil {
	fmt.Printf("err: %v\n", err)
}
defer res.Body.Close()

br := bufio.NewReader(res.Body)
_, _ := br.Peek(1024)

doc, _ := goquery.NewDocumentFromReader(res.Body)
title := doc.Find("title").Text()
fmt.Printf("title: %v\n", title)
```

以下のように `bufio` をそのまま渡すと期待通りテキストが抜き出せます。

```go
doc, _ := goquery.NewDocumentFromReader(br)
```

これは `res.Body` がすでに一定量 `Read()` されていて EOF になっているのが原因です。空っぽの `res.Body` を渡されても `goquery` （正しくは `html.Parse()` ）は困っちゃうわけです。

なので、現実的にはあまりないケースですが `<title>` までにものすごく長い文字列が入っていたら取得できるケースもあります。

### bufio Reader.Peek

`Peek()` の以下のドキュメントを読んで、これは `Read()` とは違いファイルポインタは先頭にあるんだなぁ程度の理解をしていました。

> Peek returns the next n bytes without advancing the reader.

- [bufio/#Reader.Peek](https://golang.org/pkg/bufio/#Reader.Peek)

実際に試した所、期待通りの動きもしていたので安心していました。

```go
br := bufio.NewReader(res.Body)
_, _ = br.Peek(1024)

buf := make([]byte, 1024)
br.Read(buf)
fmt.Printf("buf: %v\n", string(buf))
// ちゃんと先頭から Read できている
```

ファイルポインタがずれていないなら、 `res.Body` を使いまわして `Read()` しても大丈夫だろうと思っていたのが落とし穴でした。

<blockquote class="twitter-tweet" data-lang="ja"><p lang="ja" dir="ltr">実際はもっと大きいのを先読みして貯めてるので1回 Peek したら(実際の Body の方は)既に EOF になってるはずです。</p>&mdash; mattn (@mattn_jp) <a href="https://twitter.com/mattn_jp/status/938616409432604672?ref_src=twsrc%5Etfw">2017年12月7日</a></blockquote>
<script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

というわけで、`bufio` のソースコードを読んでみました。

```go
func (b *Reader) Peek(n int) ([]byte, error) {

  // snip...

	for b.w-b.r < n && b.w-b.r < len(b.buf) && b.err == nil {
		b.fill() // b.w-b.r < len(b.buf) => buffer is not full
```

- https://github.com/golang/go/blob/617fc0ffd3dabd24a43a713af0a05a7f93516d36/src/bufio/bufio.go#L126-L132

`Peek()` の中で `fill()` が呼ばれます。

```go
func (b *Reader) fill() {
  
  // snip...
  
	// Read new data: try a limited number of times.
	for i := maxConsecutiveEmptyReads; i > 0; i-- {
		n, err := b.rd.Read(b.buf[b.w:])
```

- https://github.com/golang/go/blob/617fc0ffd3dabd24a43a713af0a05a7f93516d36/src/bufio/bufio.go#L86-L100

なるほど、ここで `Read()` している。読み込んでいるサイズは `defaultBufSize = 4096` と定義されています。つまり、 4KB ほどすでに読まれていて元々の `res.Body` は欠損しているという感じになります。

`bufio` には `NewReaderSize()` という読み込むサイズを指定できるものがあったので、これで試してみます。小さいサイズ（`<title>` に届かないレベル）を指定すれば、 `res.Body` が欠損していたとしても `<title>` が読み込めるということになります。

以下のように 16 に指定してみると、期待通りテキストが取得できました！

```go
br := bufio.NewReaderSize(res.Body, 16)
_, _ = br.Peek(1024)

doc, _ := goquery.NewDocumentFromReader(res.Body)
title := doc.Find("title").Text()
fmt.Printf("title: %v\n", title)
```

なお `minReadBufferSize` で最小のサイズが 16 に指定されているので、それ以下の数値を指定しても挙動は変わりません。

### EOF が返ってきてもデータがあるケース

<blockquote class="twitter-tweet" data-conversation="none" data-lang="ja"><p lang="ja" dir="ltr">あとオマケですが、bufio.Reader の Read はデータと EOF を同時に返すケースがあるので、EOF が来たから読まないとしてるとデータを逃してしまう場合がありますのでご注意を。</p>&mdash; mattn (@mattn_jp) <a href="https://twitter.com/mattn_jp/status/938617179750072320?ref_src=twsrc%5Etfw">2017年12月7日</a></blockquote>
<script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

気になったので試してみました。dummy.txtは33バイトの適当なファイルです。

```go
url := "http://localhost/dummy.txt"
res, _ := http.Get(url)
for {
	buf := make([]byte, 32)
	num, err := res.Body.Read(buf)
	fmt.Printf("read: %v\tnum: %v\terr: %v\n", string(buf), num, err)

	if err == io.EOF {
		break
	}
}
```

実行してみると、たしかに EOF と共にデータが帰ってきてますね。EOF だけを終了条件にするのではなく、読み取ったバイト数もちゃんと見るようにしましょう。

```
read: 01234567890123456789012345678901	num: 32	err: <nil>
read: 2	num: 1	err: EOF
```

非常に初歩的なところだと思いますが、よく使うような部分だと思うので少しでも理解が進んで良かったです。あらためて[@nobonoboさん](https://twitter.com/nobonobo) [@mattn](https://twitter.com/mattn_jp/)さん ありがとうございます！