今回は、Go言語がOS直上の低レイヤーを扱いやすくするために提供している io.Writer
インタフェースの紹介をします。Go言語がシステムプログラミングを簡単に行える言語でありつつも、それなりに少ない記述量で比較的高速で、それでいて多くのことが達成できるのは、これから説明するようなインタフェースにより、低レイヤーが扱いやすい構造になっているからです。インタフェースと、インタフェースに対して提供されるさまざまなサービス関数が、Go言語の安い、早い、うまいの秘密です。
io.WriterはOSが持つファイルのシステムコールの相似形
前回の記事では "Hello World!"
プログラムの関数呼び出しをデバッガーでたどり、最後にシステムコール syscall.Write()
が呼び出されているようすを見ました。OSでは、このシステムコールを、ファイルディスクリプタと呼ばれるものに対して呼びます。
ファイルディスクリプタは一種の識別子(数値)です。この数値を指定してシステムコールを呼び出すと、数値に対応するモノにアクセスできます。実際、前回最後に見たシステムコール syscall.Write()
もそのように利用されていました。引数の f.fd
が、ファイルディスクリプタに相当します。
func (f *File) write(b []byte) (n int, err error) {
for {
bcap := b
if needsMaxRW && len(bcap) > maxRW {
bcap = bcap[:maxRW]
}
m, err := fixCount(syscall.Write(f.fd, bcap))
n += m
:
}
}
ファイルディスクリプタに対応するモノは、通常のファイルに限られません。標準入出力、ソケット、OSやCPUに内蔵されている乱数生成の仕組みなど、本来ファイルではないものにもファイルディスクリプタが割り当てられ、どれもファイルと同じようにアクセスできます。
ファイルディスクリプタは、OSがカーネルのレイヤーで用意している抽象化の仕組みです。OSのカーネル内部のデータベースに、プロセスごとに実体が用意されます。OSは、プロセスが起動されるとまず3つの疑似ファイルを作成し、それぞれにファイルディスクリプタを割り当てます。0が標準入力、1が標準出力、2が標準エラー出力です。以降は、そのプロセスでファイルをオープンしたり、ソケットをオープンしたりするたびに、1ずつ大きな数値が割り当てられていきます。
このようにPOSIX系OSでは、可能な限りさまざまなものが「ファイル」として抽象化されています。ただし同じPOSIX系OSでも、Windowsだとソケットはファイルとして扱えなかったり、ファイル出力とコンソール出力でAPIが違っていたりして、システムによる違いも少なからずあります。そこでGo言語では、ファイルディスクリプタのような共通化の仕組みを言語レベルで模倣して整備し、OSによるAPIの差異を吸収しています。その一例が、今回の記事で取り上げる io.Writer
です。
システムプログラミングという連載タイトルから、ファイルディスクリプタを生で使ってシステムコールを扱ったりカーネルドライバを書いたりする方法を期待する人もいると思います。しかし今回の記事の主題は、カーネルのレイヤーで用意されているのと同じ仕組みを少し上級のレイヤーで言語レベルで模倣している io.Writer
を見ることで、Go言語のインタフェースという仕組みを理解することです。
Go言語でも、直接ファイルディスクリプタを指定してファイルを作り出す関数はあります。
file, err := os.NewFile(ファイルディスクリプタ, 名前)
このあたりは個別のファイル入出力、ネットワーク入出力について紹介するときに掘り下げていきたいと思っています。
io.Writerは「インタフェース」
前回の記事でシステムコール syscall.Write()
が呼び出されていたのは、 os/file.go
で次のように定義されている、Write()
という関数からでした。
func (f *File) Write(b []byte) (n int, err error) {
if f == nil {
return 0, ErrInvalid
}
n, e := f.write(b)
:
}
このように名前の前に型を付けて定義されている関数は、その型に対する メソッド です。つまりこの定義からは、 Write
が File
という型のデータに対するメソッドであることがわかります。
その File
は、通常のファイルを表すような型で、os
パッケージで定義されている 構造体 です。Go言語における構造体は、複数のデータ型を集めたデータ型で、いわば「データの塊」だといえます。
さらに、型名にアスタリスク「*
」が付いて *File
となっていますが、これは構造体そのものではなく、そのポインタに対してメソッドが呼ばれることを意味しています。ポインタと聞いて尻込みする人もいるかもしれませんが、怖がる必要はありません。ポインタが初心者殺しの異名を持つC言語とは異なり、Go言語では構造体がポインタであってもフィールド(構造体に含まれる各データ型)へのアクセスの方法は変わりませんし(いずれも「.
」演算子が使えます)、ガベージコレクタが適切に掃除してくれるため、ポインタであることを意識しないと読めないプログラムはあまりないでしょう。
さて、 os
パッケージの File
型には Write
というメソッドが定義されている、ということがわかったところで、この Write
の仕様に注目してみましょう。 (f *File)
の部分は、この定義が File
型の構造体へのポインタ f
に対するメソッドであることを示します。次の Write (b []byte) (n int, err error)
の部分は、「[]byte
型のデータを引数として受け取って int
型と err
型を返す」と読みます。 []byte
はバイト列、 int
は整数、 err
はエラーをそれぞれ表します。つまりこの Write
は、f
が指し示すファイルに対してバイト列 b
を書き込み、書き込んだバイト数 n
と、エラーが起きた場合はそのエラー error
を返す、といえます。
Goのバイト列と文字列
Goでは、バイト列と文字列を表す string
型(UTF-8形式ですが不正な文字コードの検出などはしてくれません)とは次のように相互に変換が可能です。
// byteArrayは[]byte{0x41, 0x53, 0x43, 0x49, 0x49}
byteArray := []byte("ASCII")
// strは"ASCII"
str := string([]byte{0x41, 0x53, 0x43, 0x49, 0x49})
ここで、POSIX系OSでは可能な限りさまざまなものが「ファイル」として抽象化されているという前節の話を思い出してください。「バイト列 b
を書き込み、書き込んだバイト数 n
と、エラーが起きた場合はそのエラー error
を返す」という振る舞いは、通常のファイルに限らず、さまざまなものに適用できそうですよね。そこで、同じ仕様のメソッドを持つ型を統一的に扱えると便利そうです。Go言語では、その場合に使える仕組みとして インタフェース という型が用意されています。
構造体が「データの塊」であるのに対し、インタフェースは「メソッド宣言の塊」であるような型だといえるでしょう。そして、上記のような仕様の Write
メソッドが宣言されているインタフェースが io.Writer
なのです。実際に io.Writer
インタフェースの定義を見てみましょう。
type Writer interface {
Write(p []byte) (n int, err error)
}
os/file.go
では、まさにこの形式で、 Write
が *File
のメソッドとして定義されていましたね。この節の冒頭で紹介した os/file.go
における Write()
の定義を再掲します。
func (f *File) Write(b []byte) (n int, err error) {
if f == nil {
return 0, ErrInvalid
}
n, e := f.write(b)
:
}
インタフェースで宣言されているすべてのメソッドが、データ型に対して定義されている場合、そのデータ型は「インタフェースを満たす」と表現します。いま見たように、 io.Writer
は Write()
メソッドの仕様が宣言されているインタフェースです。そして *File
というデータ型には、その仕様通りに定義された Write()
メソッドがあります。したがって、「*File
は io.Writer
インタフェースを満たす」といえます 1 。
io.Writerを使う構造体の例
io.Writer
インタフェースを満たす構造体はGo言語のさまざまなところで実装されています。また、このインタフェースを利用する関数やメソッドもたくさん作られています。インタフェースは、構造体と違って何かしら実体を持つものを表すのではなく、「どんなことができるか」を宣言しているだけです。そこで次は、実際にどのようなものが io.Writer
インタフェースを満たすのかを見ていきましょう。
以降の例はすべて実働可能なサンプルですので、前回のようにデバッガーを使って中を覗いていくこともできます。もっとお手軽な方法としては、IntelliJの「Go To -> Declaration」(定義場所に移動)機能があります。キーワードを選択して、右クリックのコンテキストメニューから試してみてください(あるいはWindowsではCtrl+B、Macでは⌘+B)。プログラムを実行することなく行える方法なので、Go言語でプログラムを書くときには頼れる機能です。ただし、この方法でインタフェースの実体に飛ぶことはできないので、デバッガーのほうが確実ではあります。
通常、Goのプログラムは package main
と書かれたパッケージ(ディレクトリ)に main
関数が1つだけの状態でないと実行ファイルをコンパイルできません。ただし、単独の1ファイルで完結したコードをIntelliJのRunコマンド(シフト+F10)やコマンドラインの「go run ソースファイル
」で実行するときは、同じディレクトリ内に main
関数がいくつあっても実行できます。
ファイル出力
まずは先ほどから見ている os.File
です。 os.File
のインスタンスは、os.Create()
(新規ファイルの場合)や os.Open()
(既存のファイルのオープン)などの関数で作ります。
package main
import (
"os"
)
func main() {
file, err := os.Create("test.txt")
if err != nil {
panic(err)
}
file.Write([]byte("os.File example\n"))
file.Close()
}
Write
が受け取るのは文字列ではなくてバイト列なので、変換を行ってから Write
メソッドに渡しています。実行すると、実行したフォルダに test.txt
というファイルができて、os.File example
という文字列が書き込まれます。日本語も使えます。
画面出力
次は、前回の記事でも出てきた画面への出力です。前回の fmt.Println
では、最終的に os.Stdout
の Write
メソッドを呼び出していました。それと等価なコードは次の通りです。
package main
import (
"os"
)
func main() {
os.Stdout.Write([]byte("os.Stdout example\n"))
}
書かれた内容を記憶しておくバッファ
ファイルや画面出力のようなOSが提供する出力先に出すだけが io.Writer
の機能ではありません。そのような例として Write()
メソッドで書き込まれた内容を淡々とためておいて後でまとめて結果を受け取れる bytes.Buffer
があります。
package main
import (
"bytes"
"fmt"
)
func main() {
var buffer bytes.Buffer
buffer.Write([]byte("bytes.Buffer example\n"))
fmt.Println(buffer.String())
}
この例では、いままでの例と同じように、バイト列に変換した文字列を Write()
メソッドに渡しています。しかし bytes.Buffer
には、特別に文字列を受け取れる WriteString
というメソッドもあります。そのため、 Write()
メソッドを呼んでいる行は次のように書き換えることもできますが、
buffer.WriteString("bytes.Buffer example\n")
WriteString
は io.Writer
のメソッドではないため、他の構造体では使えません。代わりに、次の io.WriteString
を使えばキャストは不要になります。
io.WriteString(buffer, "bytes.Buffer example\n")
インターネットアクセス
実のところ io.Writer
のような仕組みは大抵のプログラミング言語でも実装されているため、ここまでの例では物足りないと感じる方もいるでしょう。そこで次は io.Writer
でインターネットにアクセスしてみます。この辺から少しずつGoらしさが出てきます。
net.Dial()
関数を使うと、 net.Conn
という通信のコネクションを表すインタフェースが返ってきます。 net.Conn
は io.Writer
と io.Reader
のハイブリッドなインタフェースなので、 io.Writer
としても使うことができます。 net.Dial()
関数が返す net.Conn
インタフェースの実体は、 net.TCPConn
構造体のポインタです。
package main
import (
"io"
"os"
"net"
)
func main() {
conn, err := net.Dial("tcp", "ascii.jp:80")
if err != nil {
panic(err)
}
conn.Write([]byte("GET / HTTP/1.0\r\nHost: ascii.jp\r\n\r\n"))
io.Copy(os.Stdout, conn)
}
実行してみると何らかのHTMLが画面に表示されます。なお、最後の行は io.Reader
インタフェースを利用してサーバから返ってきたレスポンスを画面に出力しています。 io.Reader
は次回紹介します。
ちょっと高レイヤーになっていきますが、 http.ResponseWriter
というものもあります。ウェブサーバから、ブラウザに対してメッセージを書き込むのに使います。
package main
import (
"net/http"
)
func handler(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("http.ResponseWriter sample"))
}
func main() {
http.HandleFunc("/", handler)
http.ListenAndServe(":8080", nil)
}
このプログラムを起動すると、 :8080
ポートでウェブサーバが起動するので、ブラウザでアクセスしてみてください。
このように、相手がなんであろうと、 io.Writer
インタフェースを使うと、どれも同じ Write()
メソッドを使って書き出すことができます。
io.Writerのフィルタ
io.Writer
を受け取り、書き込まれたデータを加工して別の io.Writer
に書き出す構造体もいくつかあります。次の io.MultiWriter
は、複数の io.Writer
を受け取り、書き込まれた内容をすべてに同時に書き込むフィルタです。
package main
import (
"io"
"os"
)
func main() {
file, err := os.Create("multiwriter.txt")
if err != nil {
panic(err)
}
writer := io.MultiWriter(file, os.Stdout)
io.WriteString(writer, "io.MultiWriter example\n")
}
次のコードは書き込まれたデータをgzip圧縮して、あらかじめ渡されていた os.File
に中継します。
package main
import (
"compress/gzip"
"os"
)
func main() {
file, err := os.Create("test.txt.gz")
if err != nil {
panic(err)
}
writer := gzip.NewWriter(file)
writer.Header.Name = "test.txt"
writer.Write([]byte("gzip.Writer example\n"))
writer.Close()
}
ファイルの内容を加工するフィルタは圧縮以外にも、ハッシュ値の計算などがあります。
出力結果を一時的にためておいて、ある程度の分量ごとにまとめて書き出す bufio.Writer
というものもあります。メモリにためる io.Writer
も名前は Buffer
でしたが、さまざまな言語に登場する「バッファ付き出力」はこの構造体が機能を提供しています。 Flush()
メソッドを呼ぶと、後続の io.Writer
に書き出します。 Flush()
メソッドを呼ばないと、書き込まれたデータを腹に抱えたまま消滅してしまうので要注意です。
package main
import (
"bufio"
"os"
)
func main() {
buffer := bufio.NewWriter(os.Stdout)
buffer.WriteString("bufio.Writer ")
buffer.Flush()
buffer.WriteString("example\n")
buffer.Flush()
}
Flush()
を自動で呼び出す場合には、バッファサイズ指定の bufio.NewWriterSize(os.Stdout, バッファサイズ)
関数で bufio.Writer
を作成します。
C言語の場合は標準出力に書き出す printf()
関数はバッファリングを行います。標準エラー出力はリアルタイム性を重視したり、出力直後にプログラムが異常終了した場合にログが出力されないのを防ぐためにバッファリングをしない動作になっていました。Go言語の場合はどの出力もバッファリングを行いません。呼び出した回数だけシステムコールが呼び出されます。筆者の環境でベンチマークを取ったところ、バッファリングなし、2回に1回出力、10回に一回出力でそれぞれだいたい800ナノ秒、500ナノ秒、200ナノ秒程度でした。100回ぐらい出力する程度では、コマンドラインツールだと誤差の範囲でしょう。C言語ができた当時と比べると、OSのコードを呼び出して返ってくるまでのオーバーヘッドも大したことがないため、シンプルな実装にしたのかもしれません。
フィルタは実装してみてもそれほど複雑ではありません。mattnさんのブログ 2 には、PascalCaseに変換しつつ書き出すフィルタの実装が紹介されています。
フォーマットしてデータをio.Writerに書き出す
整形したデータを io.Writer
へと書き出す汎用の関数もあります。 fmt.Fprintf()
は、大抵のプログラミング言語にある、C言語の printf
のようなフォーマット出力のための関数です。フォーマット(2つめの引数)に従って、io.Writer
(最初の引数)にデータ(3つめ以降の引数)を書き出します。
Go言語にはなんでも表示できる %v
というフォーマット指定子があり、プリミティブ型でもそうじゃない型でもString()
メソッドがあればそれを表示に使って出力してくれます。これも fmt.Stringer
インタフェースとして定義されています。試しに日付を表示してみましょう。
package main
import (
"os"
"fmt"
"time"
)
func main() {
fmt.Fprintf(os.Stdout, "Write with os.Stdout at %v", time.Now())
}
JSONを整形して io.Writer
に書き出すこともできます。次の例ではコンソールに出力していますが、さきほどの io.Writer
の例と組み合わせれば、サーバにJSONを送ったり、ブラウザにJSONを返すことも簡単にできてしまいます。
package main
import (
"os"
"encoding/json"
)
func main() {
encoder := json.NewEncoder(os.Stdout)
encoder.SetIndent("", " ")
encoder.Encode(map[string]string{
"example": "encoding/json",
"hello": "world",
})
}
用途がもっと限定された構造体もあります。 net/http
パッケージの Request
構造体です。この構造体は、文字通りHTTPリクエストを取り扱う構造体です。クライアント側のリクエストを送るときにも使えますし、サーバ側でレスポンスを返すときにクライアントの情報をパースするのにも使えます。 io.Writer
に書き出すのは前者の用途です。
先ほど、サーバへのリクエストの通信の例ではHTTPプロトコルを手書きしましたが、この Request
構造体を使えばミスも減ります。この構造体の Write
メソッドを使わないといけないケースは実際には少ないですが、Transfer-Encoding: chunked
でチャンクに分けて送信したり、プロトコルのアップグレードで別のプロトコルと併用するようなHTTPリクエストを送るときには使うことになるでしょう(もっとも、そういったことをするケースもまれといえばまれですが)。
package main
import (
"os"
"net/http"
)
func main() {
request, err := http.NewRequest("GET", "http://ascii.jp", nil)
if err != nil {
panic(err)
}
request.Header.Set("X-TEST", "ヘッダーも追加できます")
request.Write(os.Stdout)
}
io.Writerの実装状況・利用状況を調べる
Goでは、JavaのインタフェースやC++の親クラスと異なり、「このインタフェースを持っています」という宣言を構造体側には一切書きません。構造体がインタフェースを満たすメソッドを持っているかどうかは、インタフェースの変数に構造体のポインタを代入したり、メソッドの引数にポインタを渡したりするときに、自動的に確認されます。そのため、どの構造体がこのインタフェースを満たしているかは、コードを単純に検索するだけでは探せません。
これを調べたいときに便利なツールが、Goをインストールしたときに一緒にインストールされるgodocというコマンドです。このツールは、golang.orgのパッケージのドキュメントをローカルでも見られるようにしてくれるツールです。ウェブサーバとして起動してブラウザで見るのが典型的な使い方ですが、起動時に次のように --analysis type
を付けることでインタフェースの分析を行ってくれます。
godoc -http ":6060" -analysis type
もしすでにGoを使っている方で、GOPATH以下にパッケージがたくさん入っている場合は、解析にものすごい時間がかかってしまいます。GOPATHを一時的に変更して実行するなどしてください。
# Linux/macOSなど
$ GOPATH=/ godoc -http ":6060" -analysis type
# Windows
> set GOPATH=C:\
> godoc -http ":6060" -analysis type
ブラウザで http://localhost:6060
を開くと、次のような golang.org
にそっくりのページが表示されます。このようにgodocはオフラインでドキュメントを見ることができて便利なので、覚えておいて損はありません。
上段のリンクから「Packages」を選び、左側のパッケージ一覧の中段あたりから io
を選んで開いてください。その後、インデックスの最後のほうにある Writer
にジャンプしてください。直リンクは http://localhost:6060/pkg/io/#Writer
になります。 -analysis type
を付けて実行すると、golang.orgにはない、 implements
という項目が追加されます。ここに io.Writer
を実装した構造体などの一覧が表示されます。Go 1.7では標準ライブラリだけで176項目あります。
io.Writer
を使用している箇所を簡単に調べることはできませんが、引数として io.Writer
を受け取る公開メソッド(fmt.Fprintln
など)を簡単に検索しただけでも100以上見つかります。 net.Conn
のような io.Writer
を内包するインタフェースも数多くあるため、総数はさらに多いでしょう。
io.Writer
関連の構造体がたくさんあるといっても、すべてを覚えておく必要はなく、そのつどリファレンスを引けばいいだけです。ですが、よく出てくるものは事前に存在を知って頭にインデックスを作っておくと、コードを読む際に楽ができるはずです。
次回予告
io.Writer
と対照の存在である io.Reader
について紹介していきます。こちらも低レベルなデバイスに対して、より高級な読み込みの機能を提供します。
注釈
-
Javaだとインタフェースの名前は
I~able
という命名規則で付けることが多いですが、Goの場合はWriter
のように~er
~or
のような名前にすることが多いようです。? - http://mattn.kaoriya.net/software/lang/go/20140501172821.htm?
筆者紹介――渋川よしき
C++/Python/Goを趣味で書くIT系企業のプログラマー(横浜ベイスターズCS進出おめでとうございます)。3児の父。Sphinx-Users.jpのファウンダー。著書に『Mithril』(オライリー・ジャパン)、翻訳に『エキスパートPythonプログラミング』(アスキーMW)、『アート・オブ・コミュニティ』(オライリー・ジャパン)など。
編集者紹介――鹿野桂一郎
ラムダノート株式会社 代表取締役、TechBooster CEO(Chief Editing Officer)。HaskellとSchemeとLaTeXでコンピュータとかネットワークとか数学の本を作るのをお手伝いする仕事。
この連載の記事
-
第20回
プログラミング+
Go言語とコンテナ -
第19回
プログラミング+
Go言語のメモリ管理 -
第18回
プログラミング+
Go言語と並列処理(3) -
第17回
プログラミング+
Go言語と並列処理(2) -
第16回
プログラミング+
Go言語と並列処理 -
第15回
プログラミング+
Go言語で知るプロセス(3) -
第14回
プログラミング+
Go言語で知るプロセス(2) -
第13回
プログラミング+
Go言語で知るプロセス(1) -
第12回
プログラミング+
ファイルシステムと、その上のGo言語の関数たち(3) -
第11回
プログラミング+
ファイルシステムと、その上のGo言語の関数たち(2) -
第10回
プログラミング+
ファイルシステムと、その上のGo言語の関数たち(1) - この連載の一覧へ