onemuri.space

io.Writer とは

io.Writer って何?

golang を学び始めてから出現するようになった、 io.Writer って何を表現しているのでしょうか。今回は io.Writer について調べてみたのでそれをまとめます。

io.Writer は OS のシステムコールの模倣

OS はあるファイルや標準入出力にアクセスするためにファイルディスクリプタという数値の識別子を指定してシステムコールを呼び出します。OS の中核にあるカーネルが抽象化して用意してくれている数値なのですが、golang では言語レベルで模倣しています。その一例として io.Writer が存在しています。

io.Writer の実装を見てみると以下のようなインターフェースになっています。

type Writer interface {
    Write(p []byte) (n int, err error)
}

そして、golang でファイルを扱う時の型にFileというものがあるのですが、File を扱った、簡易的な実装について少し覗いてみます。

file, err := os.Create("sample.txt")
if err != nil {
    fmt.Println("error")
}
file.Write([]byte("sample"))

ファイルsample.txtを作成してsampleという文字列を書き込む処理ですが、file.Write()の Write()メソッドの実装を覗いてみます。

func (f *File) Write(b []byte) (n int, err error) {
    // ~~ 色々処理
    return n, err
}

io.Writer と File.Write は引数と返り値が同じですよね。

(b []byte) (n int, err error)

これは *Fileはio.Writerインターフェースを満たしていると言うことができます。

出力以外の io.Writer の用途

io.Writer はインターフェースなので実体はありません。前項の File.Write はファイルへの出力を行う機能を紹介しましたが、io.Writer はそれ以外にも機能を提供します。

内容を記憶するバッファ

Write()メソッドによって書き込まれた内容を記憶して読み出すことができます。

例 1: buffer

var buffer bytes.Buffer
buffer.Write([]byte("buffer\n"))
fmt.Println(buffer.String())

例 2: builder

var builder strings.Builder
builder.Write([]byte("builder\n"))
fmt.Println(builder.String())

インターネットアクセス

net.Dial()関数を使えば通信のコネクションを示す net.Conn インターフェースが返ってきます。net.Conn は Write(b []byte) (n int, err error) を持つため io.Writer インターフェースを満たします。

conn, err := net.Dial("tcp", "google.com:80")
if err != nil {
    panic(err)
}
io.WriteString(conn, "GET / HTTP/2.0\r\nHost: google.com\r\n\r\n")
io.Copy(os.Stdout, conn)

上記コードを実行すると HTML が返ってきます。

デコレータ

下記の例はテキストを gzip 圧縮して、file に渡すデコレータ(オブジェクトをラップして追加機能を実現する)です。

file, err := os.Create("decolator.txt.gz")
if err != nil {
    panic(err)
}
writer := gzip.NewWriter(file)
writer.Header.Name = "decolator.txt"
io.WriteString(writer, "gzip.writer\n")
writer.Close()

フォーマット

io.Writer は書き出しに関するインターフェースですが、ここではフォーマットして書き出す処理についてサンプルを載せておきます。

fmt.Fprintf(os.Stdout, "fprintf %v\n", time.Now())

最近では API 開発が広がっているので、JSON を扱うことも多いかと思います。なので、json エンコードも載せておきます。

encoder := json.NewEncoder(os.Stdout)
encoder.SetIndent("", "    ")
encoder.Encode(map[string]string{
    "name":  "test taro",
    "value": "test",
})

また、net/http パッケージの Request は HTTP リクエストのみを扱う構造体で、リクエスを送る時やレスポンスを返す時に使えます。

req, err := http.NewRequest("GET", "https://google.com", nil)
if err != nil {
    panic(err)
}
req.Header.Set("X-API-KEY", "some header")
req.Write(os.Stdout)

ちなみにインターネットアクセスの見出しの説明では

io.WriteString(conn, "GET / HTTP/2.0\r\nHost: google.com\r\n\r\n")

を用いましたが、HTTP リクエストを送る時は net/http パッケージの Request を使うことがほとんどだと思います。

まとめ

io.Writerの様々な用途とインターフェースに基づく概念の理解は前に進んだでしょうか。OS レベルでのファイルディスクリプタを利用したシステムコールを golang では言語レベルで模倣してファイルを扱ったり、出力に関するインターフェースを持っているという理解を僕はしました。

これまではio.Writerという文字を見る度に何これ?という気持ちなっていましたが、コード共に理解するとわかりやすいですね。次は io.Readerについても学んでみたいと思います。

参考

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

自己紹介用画像

Riki Akagi

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

自己紹介の詳細