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()関数は error
か EOF
に到達するまでは全てデータを読み込みます。
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 の使い方など紹介できたらと思います。