My External Storage

Feb 22, 2021 - 3 minute read - Comments - go

Go1.16で追加されたio#ReadAll関数から読むストリーミング中のバッファ拡張の仕方

実質Futureさんの記事の引用なんだけれど自分用メモ。

TL;DR

  • Go1.16でio/ioutil#ReadAll関数がio#ReadAll関数に書き直された
  • io#ReadAll関数はbytes#Buffer型を利用せずにバッファ管理を行なっている
    • b = append(b, 0)[:len(b)]
  • appendを使ったDRYなコード。appendの性能向上や最適化の恩恵に預かれるすごいコード
    • ただ自分で使う機会はあまりないかもしれない

Go1.16で追加されたio#ReadAll

先日リリースされたGo1.16ではio/ioutil#ReadAll関数がio#ReadAll関数に書き直された。

ReadAll関数はio.Readerインターフェイスからすべてのデータを読み出す関数だ。

func ReadAll(r Reader) ([]byte, error)

移植されたReadAll関数は内部の実装がすべて書き換えられている。
Go1.15以前のio/ioutil#ReadAll関数はbytesパッケージに依存していたが、bytesパッケージはioパッケージをimportしており循環参照になるため利用できないからだ。

https://github.com/golang/go/blob/go1.16/src/bytes/buffer.go#L9-L13

// bytes/buffer.go の冒頭
import (
	"errors"
	"io"
	"unicode/utf8"
)

io/ioutil#ReadAllの実装

@rscさんによって追加されたCLは次のCLになる。

該当コードはこちら。

https://github.com/golang/go/blob/go1.16/src/io/io.go#L622-L642

// ReadAll reads from r until an error or EOF and returns the data it read.
// A successful call returns err == nil, not err == EOF. Because ReadAll is
// defined to read from src until EOF, it does not treat an EOF from Read
// as an error to be reported.
func ReadAll(r Reader) ([]byte, error) {
	b := make([]byte, 0, 512)
	for {
		if len(b) == cap(b) {
			// Add more capacity (let append pick how much).
			b = append(b, 0)[:len(b)]
		}
		n, err := r.Read(b[len(b):cap(b)])
		b = b[:len(b)+n]
		if err != nil {
			if err == EOF {
				err = nil
			}
			return b, err
		}
	}
}

この中のb = append(b, 0)[:len(b)]が凄まじいなと思った。

append関数を利用してバッファを拡張する

最初読んだとき何しているのかわからなかったが、ここでやっているのは次の処理だ。

  1. 1バイトだけ追加するappend関数を実行する
    • 直前のlen(b) == cap(b)より、バッファはキャパシティ不足
  2. バッファのキャパシティがappend関数によって拡張され0が追加される
  3. 0がひとつだけ増えたバッファだが[:len(b)]によって元の長さのスライスになる。

以下の点で「さすがすぎる…」という感想を持った。

  • たった一行のスマートなコード
  • ポインタいじったりカーネルに近い操作をしているわけではないシンプルなコード
  • パット見よくわからないがちゃんと解説コメントが書いてある
  • DRY
  • append関数に対するパフォーマンスチューニングや最適化に依存できる

一度みたら書けるが自分ではこのやり方は思いつかないなと思った。

終わりに

同様な状況を自分で実装するとき、大抵はbytes#Buffer型を利用しているか確保すべきバッファサイズが自明だろう。 よってなかなか独自型でストリームデータを扱うことはないかもしれないが、なにかあったときはスッと実装したいイディオムだった。

というかGo1.16全然触れていないので早く触らなければ!

余談

標準パッケージもpkg.go.devで見るようになった認識だ。 しかしpkg.go.devだと追加されたバージョンがわからないので、まだgolang.org/pkg見ていたほうがいいのかな?

参考

関連記事