search
LoginSignup
2

More than 3 years have passed since last update.

posted at

updated at

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

  • (アドベントカレンダー、穴が開いたようなので、埋めさせていただきます)
  • (12/24晩、ベンチマーク結果などを追記しました)

任意の io.Reader を受け取って、それを加工した結果を、別の io.Reader として読み取れるようにしたい時、どうするのがベストな方法だろうか。

具体的には、当初「文字コード(ShiftJIS or UTF8)を UTF8 に変換するフィルターを作る」といったことをしたかったのだが、文字コード変換まで含めるとコードの本当に説明したい箇所がぼやけるので、本文書では簡単に「行番号を付加する」といった例を用いて「io.Reader to io.Reader なフィルターの出来るだけお手軽な作り方」を検討した結果を報告したいと思う。

io.Pipe で作る

加工処理を別の goroutine で行い、io.Pipe の Writer の方に加工結果を出力する。ユーザは io.Pipe の Reader 側を使えばよい。

lnum1.go
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" を使う

こういうフィルター処理に最適なライブラリを探していたところ、よいものが見つかった。

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

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
What you can do with signing up
2