onemuri.space

io.Reader とは

io.Reader って何?

前回の io.Writer とはでは golang が提供する io.Writer について概念やコードレベルで調査してみましたが、今回はその対となる io.Reader について調査しようと思います。

io.Reader の実装を見てみる

io.Writer の時と同様に、io.Reader もその実装を見てみましょう。

type Reader interface {
    Read(p []byte) (n int, err error)
}

io.Reader は Read(p []byte) (n int, err error) を持つインターフェースです。io.Writer とほぼ同じですね。メソッド名が違うだけ。io.Writer は渡された[]byteを書き出すために用いていましたが、io.Reader は渡された []byteを読み込み一時的に入れておく箱です。

サンプルプログラムを載せておきます。

echo "sample" > sample.txt

sample.txt というファイルを作って、それを読み込んでみます

f, err := os.Open("sample.txt")
if err != nil {
    panic(err)
}

b := make([]byte, 512)
for {
    n, err := f.Read(b)
    if n == 0 {
        break
    }
    if err != nil {
        panic(err)
    }
    fmt.Println(string(b)) // sample
}

make 関数で適当にバッファを作って、for 文で取得するようなコードを書いてみましたが、面倒ですね。なので、少し便利なメソッドを紹介します。

ioutil.ReadAll

ioutil.ReadAll()関数は errorEOF に到達するまでは全てデータを読み込みます。

f, err := os.Open("sample.txt")
if err != nil {
    panic(err)
}

b, err := ioutil.ReadAll(f)
fmt.Println(string(b)) // sample

少しだけ実装を深ぼってみます。ioutil.ReadAll の実装は以下になっています。(ちなみにコメントメッセージに err か EOF までは読み出すということが書かれていますね。)

// ReadAll reads from r until an error or EOF and returns the data it read.
// A successful call returns err == nil, not err == EOF. Because ReadAll is
// defined to read from src until EOF, it does not treat an EOF from Read
// as an error to be reported.

func ReadAll(r io.Reader) ([]byte, error) {
    return readAll(r, bytes.MinRead)
}

ReadAll のなかで、private メソッドの readAll が呼び出されているので、readAll の中身も見てみましょう。

func readAll(r io.Reader, capacity int64) (b []byte, err error) {
    var buf bytes.Buffer
    defer func() {
        // error handling
    }()
    if int64(int(capacity)) == capacity {
        buf.Grow(int(capacity))
    }
    _, err = buf.ReadFrom(r)
    return buf.Bytes(), err
}

io.Writer とはでも出てきていた、bytes.Buffer を用いていますね。bytes.Buffer は内容を記憶するバッファの機能を持ちます。

*Buffer.ReadFrom()関数は引数に受け取った io.Readerから EOF になるまでファイルを読み込み、Buffer ポインタに格納します。*Buffer.ReadFromの実装を確認してみましょう。

func (b *Buffer) ReadFrom(r io.Reader) (n int64, err error) {
    b.lastRead = opInvalid
    for {
        i := b.grow(MinRead)
        b.buf = b.buf[:i]
        m, e := r.Read(b.buf[i:cap(b.buf)])
        if m < 0 {
            panic(errNegativeRead)
        }

        b.buf = b.buf[:i+m]
        n += int64(m)
        if e == io.EOF {
            return n, nil // e is EOF, so return nil explicitly
        }
        if e != nil {
            return n, e
        }
    }
}

b.grow(MinRead)512という定数なのですが、これは最小で 512 バイト分のバッファを使い読み込んでいくことになります。io.Reader のサイズ次第でバッファを大きくします。ぜひ一緒に公式も読んでみてください。

このようにして、 b.buf に読み込んだ bytes slice を追加していくのです。ReadFrom は Buffer ポインタをレシーバにしているので、b.buf の値も直接書きかわり、 readAll()メソッドの返り値として、buf.Bytes()で読み込んだ bytes slice を返してあげるのです。

最終的に、本項の最初にサンプルとして載せていた以下のコードの b の bytes slice の値へと渡されているのですね。

b, err := ioutil.ReadAll(f)

io.Reader の実装を見てみるの章で書いた for 文と*File.Read()の実装に比べると、僕たちは ioutil.ReadAll()を使うだけで読み込み処理ができるのでとても楽ですよね(内部実装を無視すれば)。

CSV を読み込んでみる

var csvData = `
"taro", 172, 72, "jp"
"john", 180, 70, "us"
"mike", 192, 100, "ing"
`

func main() {
    r := strings.NewReader(csvData)
    csvRerder := csv.NewReader(r)
    for {
        line, err := csvRerder.Read()
        if err == io.EOF {
            break
        }
        fmt.Printf("%v\n", line) // 1回目: [taro  172  72]
        fmt.Println(line[2])     // 72
    }
}

csvReader.Read()は1行ずつ読み込んで、string slice を返します。ちなみに、バックスラッシュ 「`」は golang において生文字リテラルを表現します。なので、改行はそのまま改行を示すため、改行文字(\n)を明示的に書く必要はありません。

先ほどの例では1行ずつ読み込んでいましたが、全ての行を一気に読み込むこともできます。その場合は csvReader.ReadAll() を使います。

func main() {
    r := csv.NewReader(strings.NewReader(csvData))
    r.LazyQuotes = true
    d, err := r.ReadAll()
    if err != nil {
        panic(err)
    }
    fmt.Printf("%+v\n", d) // [[taro  172  72  "jp"] [john  180  70  "us"] [mike  192  100  "ing"]]
}

io.Reader と io.Writer を用いてストリームを扱う

前回の記事で扱った io.Writer と 今回の記事のメインである io.Reader を用いてストリーム(データの入出力)を扱ってみましょう。

io.MultiReader

io.MultiReader は複数の io.Reader の入力をつなげるように読み込みます。

func main() {
    first := bytes.NewBufferString("first\n")
    second := bytes.NewBufferString("second\n")
    third := bytes.NewBufferString("third\n")

    r := io.MultiReader(first, second, third)
    io.Copy(os.Stdout, r)
}
// first
// second
// third

io.TeeReader

io.TeeReader は読み込んだデータを異なる io.Writer に書き出します。

first := bytes.NewBufferString("first\n")
second := bytes.NewBufferString("second\n")
third := bytes.NewBufferString("third\n")

var buf bytes.Buffer
r := io.MultiReader(first, second, third)
tee := io.TeeReader(r, &buf)
b, err := ioutil.ReadAll(tee)
if err != nil {
    panic(err)
}
fmt.Println(string(b))
// first
// second
// third

bs := make([]byte, 64)
tee.Read(bs)
fmt.Println(string(bs))
// 無し

fmt.Println(buf.String()) // bufferにデータが残っている
// first
// second
// third

このサンプルでは io.TeeReader によって読み込んだデータを ioutil.ReadAll で全て捨ててしまっても、buf にはデータを残す事ができます。

まとめ

いかがだったでしょうか。前回の io.Writer の記事と今回の io.Reader の記事で、データの入出力についての意識というか見え方が変わってきたのではないでしょうか。まだまだデータの流れを追えていない時もあるますが、こればっかりは使ってみて慣れるしかないのでしょう。

また機会があれば、API 開発において golang の io.Writer と io.Reader の使い方など紹介できたらと思います。

参考

Go ならわかるシステムプログラミング

自己紹介用画像

Riki Akagi

2019年からDeNAで働いています。GCP(CloudSQL・GAE・Cloud Function etc)とGoでAPI開発に勤んでいます。睡眠やエンジニアリングに関することに興味を持って過ごしているのでその情報を皆さんに共有していけたらなと思っています。

自己紹介の詳細