( io.Reader のプリプロセッサな io.Reader を作る - Qiita より転載)
任意の io.Reader を受け取って、それを加工した結果を、別の io.Reader として読み取れるようにしたい時、どうするのがベストな方法だろうか。
具体的には、当初「文字コード(ShiftJIS or UTF8)を UTF8 に変換するフィルターを作る」といったことをしたかったのだが、文字コード変換まで含めるとコードの本当に説明したい箇所がぼやけるので、本文書では簡単に「行番号を付加する」といった例を用いて「io.Reader to io.Reader なフィルターの出来るだけお手軽な作り方」を検討した結果を報告したいと思う。
io.Pipe で作る
加工処理を別の goroutine で行い、io.Pipe の Writer の方に加工結果を出力する。ユーザは io.Pipe の Reader 側を使えばよい。
package main import ( "bufio" "fmt" "io" "os" ) func lnum(src io.Reader) io.Reader { in, out := io.Pipe() lnum := 0 go func() { br := bufio.NewReader(src) for { line, err := br.ReadBytes('\n') if err != nil { out.CloseWithError(err) return } lnum++ fmt.Fprintf(out, "%d: %s", lnum, line) } }() return in } func main() { sc := bufio.NewScanner(lnum(os.Stdin)) for sc.Scan() { fmt.Println(sc.Text()) } if err := sc.Err(); err != nil { fmt.Fprintln(os.Stderr, err) os.Exit(1) } }
「go run lnum1.go < ファイル名
」のようにして使う。この方法はお手軽で、標準ライブラリだけで完結する。ただ、goroutine を別途立ち上げるので(大したことはないだろうが)余分なリソースを使う点、また途中で読むのを止めた時に goroutine が残ってしまわないよう対策が必要な点などが課題だ。
"tidwall/transform" を使う
こういうフィルター処理に最適なライブラリを探していたところ、よいものが見つかった。
package main import ( "bufio" "fmt" "io" "os" "github.com/tidwall/transform" ) func lnum(src io.Reader) io.Reader { br := bufio.NewReader(src) lnum := 0 return transform.NewTransformer(func() ([]byte, error) { line, err := br.ReadString('\n') if err != nil { return nil, err } lnum++ return []byte(fmt.Sprintf("%d: %s", lnum, line)), nil }) } func main() { sc := bufio.NewScanner(lnum(os.Stdin)) for sc.Scan() { fmt.Println(sc.Text()) } if err := sc.Err(); err != nil { fmt.Fprintln(os.Stderr, err) os.Exit(1) } }
特に問題ないが、強いて言うならば標準ライブラリで完結しないくらい?(それは問題ちゃうやろう)
"golang.org/x/text/transform"
こちらは準・標準的なライブラリで、
type Transformer interface { Transform(dst, src []byte, atEOF bool) (nDst, nSrc int, err error) Reset() }
というインターフェイスで型を実装すれば、
transform.NewReader(元Reader, 実装したTransformerのインスタンス)
という形式で、変換を実装する io.Reader を作成してくれる。
が、これ Transform メソッドを実装するのが非常に面倒くさい。src や dst のサイズが任意ではなく、呼び出し元から決められているせいだ。一回、実装してみたが、dst があふれそうな時、別のバッファに残しておいて次回に処理するとか、なかなか大変だった(やり方が悪いのかもしれない)。
ただ、メモリアローケーション回数が少ないはずなので、速度的には有利で、うまく実装できれば非常に実行効率がよいフィルターが作れると思われる。
なんかよい実装例ないものか(誰か、わかりやすい日本語の解説記事書いてよ)
以上
追記
"tidwall/transform"
と似たようなものを作ろうとしたのですが、ベンチを取ると、やはり"tidwall/transform"
にはかないませんでした。
goos: windows goarch: amd64 pkg: github.com/zetamatta/go-texts/preprocessor Benchmark_filter-4 10000 109332 ns/op 18220 B/op 321 allocs/op Benchmark_transformer-4 10000 108897 ns/op 15627 B/op 319 allocs/op Benchmark_iopipe-4 10000 230557 ns/op 15874 B/op 323 allocs/op PASS ok github.com/zetamatta/go-texts/preprocessor 4.733s
ソースは https://github.com/zetamatta/go-texts/blob/master/preprocessor/preprocess_test.go を御覧ください
Benchmark_filter-4
… 自作品Benchmark_transformer-4
…"tidwall/transform"
Benchmark_iopipe-4
… io.Pipe と goroutine によるもの
io.Pipe と goroutine では仕組みが大掛かりなせいか、2倍近くコストがかかっています。自作のものは bytes.Buffer を2つ交互に使ってバッファリングしているのですが、あと一歩及びません。
"tidwall/transform"
のソースを見てみると、[]bytes を使って溢れたデータを管理しているのですが、領域を開放せずに長さだけゼロにする (bytes.Buffer)Reset
的な操作を
r.buf = r.buf[:0] // reset the read buffer, keeping it's capacity
とやっていました。同操作を[]byte
でやるのにどうするんだと思っていたのですが、なるほどなぁ…です。