- (アドベントカレンダー、穴が開いたようなので、埋めさせていただきます)
- (12/24晩、ベンチマーク結果などを追記しました)
任意の 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
でやるのにどうするんだと思っていたのですが、なるほどなぁ…です。