はじめに
io.Readerとio.Writerを満たす型として、bytesパッケージのbytes.Buffer型が存在します。
また、stringsパッケージのstrings.Reader型はio.Readerを満たします。
本章では、これらの型について解説します。
bytesパッケージのbytes.Buffer型
まずは、bytes.Buffer型の構造体の中身を確認してみましょう。
type Buffer struct {
buf []byte
// (略)
}
出典:[https://go.googlesource.com/go/+/go1.16.3/src/bytes/buffer.go#20]
構造として特筆すべきなのは、中身にバイト列を持っているだけです。
これだけだったら「そのまま[]byteを使えばいいじゃないか」と思うかもしれませんが、パッケージ特有の型を新しく定義することによって、メソッドを好きに付けられるようになります。
というわけで、bytes.BufferにはReadメソッド、Writeメソッドがついています。
これによってio.Readerとio.Writerを満たすようになっています。
Writeメソッド
Writeメソッドは、レシーバーのバッファの「中に」データを書き込むためのメソッドです。
使用例を以下に示します。
// bytes.Bufferを用意
// (bytes.Bufferは初期化の必要がありません)
var b bytes.Buffer
b.Write([]byte("Hello"))
// バッファの中身を確認してみる
fmt.Println(b.String())
// (出力)
// Hello
参考:pkg.go.dev - bytes#Buffer-Example
Readメソッド
Readメソッドは、レシーバーバッファから「中を」読み取るためのメソッドです。
// 中にデータを入れたバッファを用意
var b bytes.Buffer
b.Write([]byte("World"))
// plainの中にバッファの中身を読み取る
plain := make([]byte, 10)
b.Read(plain)
// 読み取り後のバッファの中身と読み取り結果を確認
fmt.Println("buffer: ", b.String())
fmt.Println("output:", string(plain))
// buffer:
// output: World
バッファの中からはWorldというデータが見えなくなり、きちんと変数plainに読み込みが成功しています。
stringsパッケージのstrings.Reader型
stringsパッケージは、文字列を置換したり辞書順でどっちが先か比べたりという単なる便利屋さんだけではないのです。
bytes.Buffer型と同じく、文字列型をパッケージ独自型でカバーすることで、io.Readerに代入できるようにした型も定義されているのです。
そんな独自型strings.Reader型は、構造体内部に文字列を内包しています。
type Reader struct {
s string
// (略)
}
出典:[https://go.googlesource.com/go/+/go1.16.3/src/strings/reader.go#17]
これはReadメソッドをもつ、io.Readerインターフェースを満たす構造体です。
strings.Reader型にWriteメソッドはないので、io.Writerは満たしません。
Readメソッド
Readメソッドは、文字列から「中を」読み取るためのメソッドです。
使用例を示します。
// NewReader関数から
// strings.Reader型のrdを作る
str := "Hellooooooooooooooooooooooooooo!"
rd := strings.NewReader(str)
// rowの中に読み取り
row := make([]byte, 10)
rd.Read(row)
// 読み取り結果確認
fmt.Println(string(row)) // Helloooooo
これらの使いどころ
「バイト列や文字列をio.Reader・io.Writerに入れられるようにしたところで何が嬉しいの?」という疑問を持った方もいるかと思います。
ここからはそんな疑問に対して、ここで紹介した型の使い所を一つ紹介したいと思います。
テストをスマートに書く
ioの章で書いたTranslateIntoGerman関数を思い出してください。
// 引数rで受け取った中身を読み込んで
// Hello → Guten Tagに置換する関数
func TranslateIntoGerman(r io.Reader) string {
data := make([]byte, 300)
len, _ := r.Read(data)
str := string(data[:len])
result := strings.ReplaceAll(str, "Hello", "Guten Tag")
return result
}
この関数のテストを書くとき、皆さんならどうするでしょうか。
「io.Readerを満たすものしか引数に渡せない……テストしたい内容が書いてあるファイルを1個1個用意するか…?」と思ったこともいるでしょう。
ですが「ファイルを1個1個用意する」とかいう面倒な方法をせずとも、strings.Reader型を使うことで、テスト内容をコード内で用意することができるのです。
func Test_Translate(t *testing.T) {
// テストしたい内容を文字列ベースで用意
tests := []struct {
name string
arg string
want string
}{
{
name: "normal",
arg: "Hello, World!",
want: "Guten Tag, World!",
},
{
name: "repeat",
arg: "Hello World, Hello Golang!",
want: "Guten Tag World, Guten Tag Golang!",
},
}
// TranslateIntoGerman関数には
// strings.NewReader(tt.args)で用意したstrings.Reader型を渡す
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := TranslateIntoGerman(strings.NewReader(tt.arg)); got != tt.want {
t.Errorf("got %v, but want %v", got, tt.want)
}
})
}
}