標準愚痴出力

個人的なIT作業ログです。もしかしたら一般的に参考になることが書いているかもしれません(弱気

io.Reader のプリプロセッサな io.Reader を作る

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 でやるのにどうするんだと思っていたのですが、なるほどなぁ…です。