1. ktnyt

    Posted

    ktnyt
Changes in title
+io.Readerをすこれ
Changes in tags
Changes in body
Source | HTML | Preview
@@ -0,0 +1,282 @@
+# 0. なんで io.Reader?
+
+去年のアドベントカレンダーではGo4までだったのが今年はなんとGo7までできており、Goへの関心が高まっているのはいちGo好きとしてはうれしい限りです。Goの良さは色々なところにあります、それは例えばシンプルな言語仕様だったり、標準ライブラリの充実度だったり、様々なサポートツール(`go get`、`go vet`、`goimports`、`gorename` など)だったり。こういった側面はしばしばGoの良さとして語られますし私自身もそれには同意します。一方でそれらのわかりやすい利点の裏に隠れてしまっている影の立役者がいると思っています。そう、それこそが`io.Reader`です。`io.Reader` はその使い方を正しく理解するだけで**Goのインターフェースという仕組みの強力さがわかる**と思っています。
+
+
+
+# 1. io.Readerって?
+
+一言で言ってしまえば**バイト列を読み出すためのインターフェース**です。Goを書いたことがある方はどこかで一度はコードの中で `io.Reader` に触れたことがあると思います。たとえその覚えがなくても `io.Reader` はGoを書いていれば様々な場所で出てきます。例えばファイル入力のとき、`f, err := os.Open(filename)` の返り値 `f` は `var f *os.File` という型ですが、この`*os.File` は `io.Reader` インターフェースを持っています。他に頻出する例としては `http.Response` の `Body` メンバが `io.ReadCloser` というインターフェースを持っていますがこれは `io.Reader` と `io.Closer` の複合的なインターフェースです。
+
+
+
+## io.Readerインターフェース
+
+Goの[標準ライブラリ](https://golang.org/pkg/io/)の中で `io.Reader` は以下のように定義されています(コメント略)。
+
+```Go
+type Reader interface {
+ Read(p []byte) (n int, err error)
+}
+```
+
+初めてこの定義を見る方は「え、たったこれだけ?」と思われるかもしれませんが、これだけです。 `Read([]byte) (int, error)` というシグネチャを持ったメソッドを実装しているありとあらゆる型が `io.Reader` に該当します。これで `io.Reader` がGoの様々な場面で出てくるという主張に納得していただけるのではないかと思います。ただし、このインターフェースを見ただけでは直感的に `Read` メソッドがどういった振る舞いをするのかは自明ではないでしょう。というのも `Read` メソッドを直接叩くことはあまりなく、たいていのユースケースにおいて例えばファイルを読むときは `*bufio.Scanner` を使って一行ずつ読み込んだり、HTTPのレスポンスボディは `ioutil.ReadAll` に渡して全部まるっと読んでしまえばいいというケースが多いのです。シンプルなユースケースであればごそっとバイトスライスに読み込んでしまえばよさそうですが、巨大なファイルを開く場合やストリームから継続的にデータを読み出すような使い方をする場合は `io.Reader` を使わざるを得ないので `io.Reader` を介するAPI設計は重宝します。
+
+
+
+## Readメソッドの振る舞い
+
+では具体的に `Read` メソッドが何をするか(することを期待されるか)を手続き的にご説明します。
+
+1. **与えられたバイトスライス `p []byte` を先頭から埋めていく。**
+2. **埋まったバイト数 `n` と、埋める過程で発生したエラー `err` を返す。**
+3. **`n < len(p)` の場合、`err != nil` である可能性がある。**
+4. **組 `(0, nil)` を返すことは*非推奨*。**
+
+
+
+たとえばファイルから先頭 `m` バイトを読みたい場合は以下のように記述します。
+
+```Go
+f, err := os.Open(filename)
+p := make([]byte, m) // 1. スライス確保
+n, err := f.Read(p) // 2. Read実行
+if m < n { // 3. 返り値処理
+ log.Fatalf("%dバイト読もうとしましたが、%dバイトしか読めませんでした\n", n, m)
+}
+if err != nil { // 4. エラー処理
+ log.Fatalf("読込中にエラーが発生しました:%v\n", err)
+}
+```
+
+1. `Read` メソッドは**可能な限り渡されたスライスの全長を埋める**ことを期待されるため、あらかじめ読みたいバイト数分のサイズを持ったスライスを作成しておきます。
+2. スライスの実態はポインタなのでこれをそのまま `Read` メソッドに渡すと中でこのスライスの中身を埋めてくれます。
+3. Readで**エラーが発生していても `n > 0` バイト埋められている可能性がある**ため先にその処理を行います。Goは多くの場合真っ先にエラー処理をすることが多いですが、`Read` メソッドの返り値処理はその数少ない例外と言えるでしょう。ここでは要求したバイト数が読めなかったため `log.Fatalf` を呼んでいますが、場合によっては `p[:n]` でなにかの処理をすることも想定できるでしょう(バッファリングなど)。
+4. 最後にエラーを処理します。このとき**`n < m` でも `err == nil` の可能性がある**ことと、 **`n == 0 && err == nil` であっても `EOF` ではない**ことに注意しましょう(この組の返り値が非推奨になっている所以です)。
+
+
+
+# 2. io.Readerの良さ
+
+`io.Reader` というインターフェースと `Read` メソッドの振る舞いをご理解いただいた上でなぜ `io.Reader` が素晴らしいのかを語っていきます。前述の通り `io.Reader` はGoのあらゆる場面で出てくるインターフェースですが、その理由は**圧倒的な汎用性**にあります。このことを説明するのにもってこいなのが標準ライブラリに実装されている [`bufio.Reader`](https://golang.org/pkg/bufio/#Reader) です。
+
+
+
+## bufio.Readerとは
+
+`io.Reader` がインターフェースであるのに対して `bufio.Reader` は `io.Reader` を満たした構造体です。パッケージ名の `bufio` が示唆する通り `bufio.Reader` は `io.Reader` オブジェクトにバッファリングの機能を実装します。公式ドキュメントも以下の通り:
+
+> Reader implements buffering for an io.Reader object.
+
+バッファリングの恩恵についてはこちらの[GopherConの発表](https://www.youtube.com/watch?v=nok0aYiGiYA)をご覧ください。ここでは発表の中で使われている単語数カウントのプログラムを借りつつ `io.Reader` の便利さの面にアプローチしていきます。まず単語数カウントのプログラムを作成していきます。
+
+```Go
+package main
+
+import (
+ "fmt"
+ "io"
+ "log"
+ "os"
+ "unicode"
+)
+
+var p [1]byte
+
+func readbyte(r io.Reader) (rune, error) {
+ n, err := r.Read(p[:])
+ if n > 0 {
+ return rune(p[0]), nil
+ }
+ return 0, err
+}
+
+func main() {
+ filename := os.Args[1]
+ f, err := os.Open(filename)
+ if err != nil {
+ log.Fatalf("cannot open file %q: %v", filename, err)
+ }
+ defer f.Close()
+
+ words := 0
+ inword := false
+
+ for {
+ r, err := readbyte(f)
+ if unicode.IsSpace(r) {
+ if inword {
+ words++
+ }
+ inword = false
+ } else {
+ inword = true
+ }
+
+ if err == io.EOF {
+ break
+ }
+
+ if err != nil {
+ log.Fatalf("read failed: %v", err)
+ }
+ }
+
+ fmt.Printf("%q: %d words\n", filename, words)
+}
+
+```
+
+リンク先の発表のコードとは異なる部分がいくつかあります。
+
+1. `io.Reader` から1バイト読み込む際にエラーがあってもそのバイトをチェックする仕組みを追加
+2. 単語の定義を `wc` コマンドと統一
+
+ベンチマークのために[Moby Dickのテキスト](https://gist.githubusercontent.com/ktnyt/734e32aab75a4f7df06538dac9f00a5a/raw/8da85d5acabc53fd66af17c252701b0ba395e6c1/moby.txt)を使用します。
+
+```sh
+$ go build slow_wc.go
+$ time ./slow_wc moby.txt
+"moby.txt": 215822 words
+./slow_wc moby.txt 0.61s user 0.76s system 97% cpu 1.404 total
+$ time wc -w moby.txt
+ 215822 moby.txt
+wc -w moby.txt 0.01s user 0.00s system 77% cpu 0.019 total
+```
+
+`wc` の100倍くらい時間がかかっています。これは `*os.File` の `Read` メソッドが呼び出されるたびにシステムコールが発生しているのが原因なので、バッファリングによってパフォーマンスの改善が期待されます。先程のコードを次のように変更します。
+
+```diff
+--- slow_wc.go 2019-11-24 03:40:14.000000000 +0900
+@@ -1,6 +1,7 @@
+ package main
+
+ import (
++ "bufio"
+ "fmt"
+ "io"
+ "log"
+@@ -26,11 +27,13 @@
+ }
+ defer f.Close()
+
++ b := bufio.NewReader(f)
++
+ words := 0
+ inword := false
+
+ for {
+- r, err := readbyte(f)
++ r, err := readbyte(b)
+ if unicode.IsSpace(r) {
+ if inword {
+ words++
+```
+
+```sh
+$ go build ./fast_wc.go
+$ time ./fast_wc moby.txt
+"moby.txt": 215822 words
+./fast_wc moby.txt 0.03s user 0.01s system 87% cpu 0.045 total
+```
+
+`wc` の2倍程度の実行時間まで落ちました。コードの変化としてはファイルオブジェクトを `bufio.Reader` で包んで `readbyte` 関数に渡すオブジェクトを変更しただけですがここまでの差が出てきます。`bufio.Reader` の中ではデフォルトで4096バイトごとに読み込みを行うので `*os.File` の `Read` メソッドの呼び出しが `readbyte` の呼び出し4096回ごとに1回になったと捉えられます。
+
+ここでポイントなのは `readbyte` 関数に一切手を加えなくて良かったことです。よく知っている方は `bufio.Reader` には `ReadByte` というメソッドがあるのでわざわざ `readbyte` を使う必要がないじゃないかと仰るかもしれませんがそれはその通りです(※というかむしろこの例では `unicode.IsSpace` を呼んでいるので本来は `ReadRune` を使うべきです)。しかしその議論は本質的ではなく、ここで主張したいことは、**`io.Reader` を受け取ることを前提に設計されたAPIは非常に頑強である**ということです。
+
+上記の安全性も含めて記述するのであれば私であれば以下のようなコードを書くでしょう。
+
+```Go
+package main
+
+import (
+ "bufio"
+ "fmt"
+ "io"
+ "log"
+ "os"
+ "unicode"
+)
+
+func wordcount(r io.Reader) (int, error) {
+ words, inword := 0, false
+ b := bufio.NewReader(r)
+
+ for {
+ c, _, err := b.ReadRune()
+
+ if unicode.IsSpace(c) {
+ if inword {
+ words++
+ }
+ inword = false
+ } else {
+ inword = true
+ }
+
+ if err == io.EOF {
+ return words, nil
+ }
+
+ if err != nil {
+ return 0, err
+ }
+ }
+}
+
+func main() {
+ filename := os.Args[1]
+ f, err := os.Open(filename)
+ if err != nil {
+ log.Fatalf("cannot open file %q: %v", filename, err)
+ }
+ defer f.Close()
+
+ words, err := wordcount(f)
+ if err != nil {
+ log.Fatalf("read failed: %v", err)
+ }
+ fmt.Printf("%q: %d words\n", filename, words)
+}
+```
+
+このように単語数カウントを `wordcount` という `io.Reader` を受け取る関数にしてしまうことで、**ファイルだけでなくHTTPのレスポンスボディを含むありとあらゆる `io.Reader` に適用できる**ようになります。このように `io.Reader` をうまく組み込んでいくことで **`io.Reader` を扱う様々なライブラリとシームレスに連携**することができます。その1つの例が [golang.org/x/text/transform](https://godoc.org/golang.org/x/text/transform) です。
+
+
+
+## io.Readerへの操作
+
+シンプルな例として `io.Reader` から読み出す際に改行を落とすようなケースを考えます。[golang.org/x/text/transform](https://godoc.org/golang.org/x/text/transform) 内の `transform.Reader` を使うと `io.Reader` に `transform.Transformer` を適用して様々な変換を行うことができます。文字単位で扱う場合には [golang.org/x/text/runes](https://godoc.org/golang.org/x/text/runes) 内に便利な `transform.Transformer` がいくつか定義されているためこちらを利用します。
+
+```Go
+func IsNewline(r rune) bool { return r == '\n' }
+
+func RemoveNewline(r io.Reader) io.Reader {
+ s := runes.Predicate(IsNewline)
+ t := runes.Remove(s)
+ return transform.NewReader(r, t)
+}
+```
+
+たったこれだけであらゆる `io.Reader` 型のオブジェクトに `RemoveNewline` を適用するだけで改行をすべて落とす `io.Reader` を生成できます。
+
+
+
+## io.Readerは強力
+
+ここまでお読みいただいたらもうお気づきかと思いますが、`bufio.Reader` や `transform.Reader` などは `io.Reader` を加工しますがそれ自体がまた `io.Reader` になっています。そのため、その他世の中に存在するありとあらゆる `io.Reader` を要求する仕組みと連携することができるのです。このように `io.Reader` は非常に**シンプルで役割が明快**でありつつ**強力なよくデザインされたインターフェース**なのです。
+
+手前味噌にはなりますが、私が個人的に開発しているパーサコンビネータライブラリ [pars](https://github.com/ktnyt/pars) では内部状態を表す `pars.State` は `io.Reader` を受け取って必要なときに必要なだけそこから読み出してパーサを適用する設計になっています。Goで書かれたパーサコンビネータライブラリの先駆者として直接のデザインキューとなっている [vektah/goparsify](https://github.com/vektah/goparsify) やそのインスピレーションとなっている [prataprc/goparsec](https://github.com/prataprc/goparsec) などがありますが、いずれも文字列やバイトスライスを受け取るAPI設計になっています。今私が開発しているバイオインフォマティクスの分野では非常に巨大なファイルを扱うことがよくあるため、こういったAPIは微妙に使い勝手が悪いのです。加えて、[vektah/goparsify](https://github.com/vektah/goparsify) では空白を無視する仕組みがパーサの状態の中に実装されていますが、`io.Reader` を受け取る設計にすることでその必要はなくなります。文字列リテラル内の空白以外をすべて落とすような `io.Reader` のマニピュレータを作って `par.State` に渡してあげれば、特別な仕組みをパーサ状態内に作り込むことなく簡単に空白を除いてパーサを適用するといったこともできるのです。
+
+
+
+# 3. まとめ
+
+`io.Reader` は `Read(p []byte) (n int, err error)` というメソッドを持った標準ライブラリに実装された何のひねりもない実にシンプルインターフェースです。その反面、`io.Reader` は**Goという言語の哲学**や**インターフェースという機能の強力さ**をその身に宿した**最強のインターフェース**だと思っています。ここではカバーしませんでしたが対となるインターフェースである `io.Writer` と合わせて「**Goを語る上で欠かせない、言語仕様の外にある最も重要な概念**」だと言ってしまっても過言ではないというのが私の持論です。
+
+Goのインターフェースを使いこなすのは非常に難しいですが、コミュニティとしても広く受け入れられて使われている `io.Reader` を積極的に使ってみることでその利便性に気がつけるという側面もあるのではないかと思います。Go初心者から脱却する第一歩としてぜひ `io.Reader` を使い倒してみてください。
+
+1. **`io.Reader` を知り**
+2. **`io.Reader` を使い**
+3. **`io.Reader` で作りましょう**