前回はGo言語が提供する io.Writer
を紹介しました。 今回と次回は、それと対になる io.Reader
を中心に、仲間のインタフェースをいくつか紹介していきます。
「Go言語ではスクリプト言語並にかんたんにファイル読み込みやネットワークアクセスができる」、という説明を見かけたことがある方も多いでしょう。 確かにGo言語には、ファイル読み込みやネットワークアクセスの結果が1行で得られる、次のような関数が用意されています。
ioutil.WriteFile()
: これだけを使ってファイルに書き込めるioutil.ReadFile()
: これだけを使ってファイルから読み込めるhttp.Get()
: これだけを使ってHTTPのGETメソッドでデータを受け取れるhttp.Post()
: これだけを使ってHTTPのPOSTメソッドでデータを送れる
実はこれらのAPIは、今回の記事の主役である io.Reader
や前回の io.Writer
を隠蔽して、特定の用途でかんたんに使えるようにしたものです。 これらのAPIと比べると、低レベルな io.Reader
と io.Writer
は、同じことをするのに手間が少しかかります。 しかし、取り扱い方がわかり、使いこなせるようになれば、より多くのことができるようになります。
ソフトウェアは基本的に、「何らかの入力に対して、加工を行ってから、出力する」というパイプ構造をしています。 したがって、入出力を思い通りに扱うことは、ソフトウェア開発者にとってなくてはならないスキルです。 それが低レベルを知ることの強みのひとつといえるでしょう。
書き込みに比べると読み込みのほうがトピックが多く、機能も多いため、 io.Reader
まわりについては2回に分けてお届けします。 今回はその第1回ということで、Go言語における低レベルな入力の仕組みについて次の構成で説明していきます。
- まずは本文のための前提知識として、
io.Reader
とその仲間たちの紹介 - 少ないコード量で
io.Reader
からデータを効率良く読み込むための補助的な関数群 io.Reader
を満たす構造体で特に頻繁に使われるものの紹介(標準入力、ファイル、ソケット、メモリのバッファ)
後編である次回は、次のトピックをお届けする予定です。
- バイナリ解析に便利な機能群
- テキスト解析に便利な機能群
- ちょっと抽象的な
io.Reader
の構造体
io.Reader
とio.Writer
、その仲間たち
前回は、出力先の種類(ファイルか、画面か、バッファか、ネットワークか)にかかわらず、 データを出力するという機能がGo言語のインタフェースという仕組みで抽象化されていることを見ました。 そのインタフェースの名前が io.Writer
でした。
同じように、プログラムで外部からデータを読み込むための機能もGo言語のインタフェースとして抽象化されています。 そのインタフェースの名前が io.Reader
です。
Go言語のインタフェースは、前回説明したように、メソッド宣言をまとめたものです。 io.Reader
には、次のような形式の Read()
メソッドが宣言されています。
func Read(p []byte) (n int, err error)
引数である p
は、読み込んだ内容を一時的に入れておくバッファです。 あらかじめメモリを用意しておいて、それを使います。 Go言語でメモリを確保するには、組み込み関数の make
を使うとよいでしょう。
次の例は、io.Reader
インタフェースを満たす何らかの型の変数 r
があったとして、そこから make
を使って用意した1024バイトの入力用バッファ buffer
へとデータを読み込んでくる例です。
// 1024バイトのバッファをmakeで作る
buffer := make([]byte, 1024)
// sizeは実際に読み込んだバイト数、errはエラー
size, err := r.Read(buffer)
前回の書き込みに比べて、バッファを用意してその長さを管理したり、ちょっとめんどくさいですね。 バッファの管理をしつつ、何度も Read
メソッドを読んでデータを最後まで読み込むなど、読み込み処理を書くたびに同じようなコードを書かなければなりません。 実際、このメソッドだけでプログラムを開発するのは大変です(C言語でファイル読み込み処理を書いたことがある方は身にしみていると思います)。
そのためGo言語では、低レベルなインタフェースだけでなく、それをかんたんに扱うための機能も豊富に提供されています。 次の節では、それらの補助機能を見ていきましょう。
io.Reader
の補助関数
先ほど見たように、 io.Reader
をそのまま使うのは多少不便なため、入力を扱うときは補助関数を使うことになります。 PythonやRubyであれば、そうした補助的なメソッドもすべてファイルのオブジェクトが持っていたりしますが、Go言語では特別なもの以外はこのような外部のヘルパー関数を使って実現します。
読み込みの補助関数
おそらく、いちばん利用するのは ioutil.ReadAll()
でしょう。 これは終端記号に当たるまですべてのデータを読み込んで返します。 メモリに収まらないかもしれないようなケースでは使えませんが、多くの場合はこれでいけます。
// すべて読み込む
buffer, err := ioutil.ReadAll(reader)
一方、決まったバイト数だけ確実に読み込みたいという場合には io.ReadFull()
を使います。 これを使うと、指定したバッファのサイズ分まで読み込めない場合にエラーが返ってきます。 サイズが決まっているバイナリデータの読み込みで使います。
// 4バイト読み込めないとエラー
buffer := make([]byte, 4)
size, err := io.ReadFull(reader, buffer)
この io.ReadFull()
と似ていますが、最低読み込みバイト数を指定しつつ、それ以上のデータも読む、 io.ReadAtLeast()
というものもあります(筆者は使ったことがありません)。
コピーの補助関数
io.Reader
から、 io.Writer
にそのままデータを渡したいときに使うのがコピー系の補助関数です。 いちばんよく使うのは、すべてを読みつくして書き込む io.Copy()
でしょう。 ファイルを開いてそのままHTTPで転送したいとか、ハッシュ値を計算したいとか、いろいろなケースで使います。 io.CopyN()
を使えばコピーするバイト数を指定できます。
// すべてコピー
writeSize, err := io.Copy(writer, reader)
// 指定したサイズだけコピー
writeSize, err := io.CopyN(writer, reader, size)
あらかじめコピーする量が決まっていてムダなバッファを使いたくない場合や、何度もコピーするのでバッファを使いまわしたい場合もあるでしょう。 そんな場合には io.CopyBuffer()
を使うと自分で作った作業バッファを渡すことができます。 デフォルトでは io.Copy
は32KBのバッファを内部で確保して使います。
// 8KBのバッファを使う
buffer := make([]byte, 8 * 1024)
io.CopyBuffer(writer, reader, buffer)
io.Reader
を満たす構造体で、よく使うもの
前回は io.Writer
を満たす構造体をいくつか紹介しました。 今回は io.Reader
について、インタフェースを満たす構造体を見ていきましょう。
読み込みと書き込みでインタフェースが分かれているので、読み込みや書き込みごとに違うクラスを駆使する必要があるように思われるかもしれません。 しかし、Go言語の構造体の多くは、読みと書きの両方のインタフェースを備えています。そのため、紹介する顔ぶれは前回と一部重複しています。
以降、構造体の種類ごとに Read()
メソッドの使い方を見ていきますが、 各項目の冒頭には、その構造体の名前と満たしている入出力関連インタフェースを表にしてまとめておきます。 たとえばファイルの入出力に関連する os.File
構造体は、 これまで紹介したio.Reader
、io.Writer
、io.Seeker
、io.Closer
の各インタフェースを満たしているので、 そのすべてにチェックが入っています。
標準入力
変数 | io.Reader |
io.Writer |
io.Seeker |
io.Closer |
---|---|---|---|---|
os.Stdin |
✔ | ✔ |
標準入力に対応するオブジェクトが os.Stdin
です。このプログラムをそのまま実行すると入力待ちになり、以降はEnterキーが押されるたびに結果が返ってきます。Ctrl+D(WindowsはCtrl+Z)で終了します。
package main
import (
"fmt"
"io"
"os"
)
func main() {
for {
buffer := make([]byte, 5)
size, err := os.Stdin.Read(buffer)
if err == io.EOF {
fmt.Println("EOF")
break
}
fmt.Printf("size=%d input='%s'\n", size, string(buffer))
}
}
あるいは、次のように実行すると、自分自身のソースコードを決まったバッファサイズ(ここでは5)ごとに区切って表示します。
> go run stdin.go < stdin.go
プログラムを単体で実行すると、入力待ちでブロックしてしまいます(つまり、入力がくるまで実行が完全停止してしまいます)。 Go言語の Read()
はタイムアウトのような仕組みもなく、このブロックを避けることはできません 1 。
他の言語だと、ブロックするAPIとブロックしない(ノンブロッキングの)APIの両方が用意されていることも多いのですが、Go言語の場合は並列処理機構が便利に使えるので、それを使ってノンブロッキングな処理を書きます。 具体的には、ゴルーチンと呼ばれる軽量スレッドを別に作ってそこで読み込みを行い、読み込んだ文字列を処理するコードにはチャネルという仕組みを使って渡すのが定石です。
なお、Go言語の並列処理については将来の連載で記事にする予定です。 また、 os.Stdin
の入力がキーボードか、上記の例のように他のプロセスに接続されているのかを判定する方法がありますが、これについてもプロセス周辺のトピックとして今後紹介する予定です。
ファイル入力
構造体 | io.Reader |
io.Writer |
io.Seeker |
io.Closer |
---|---|---|---|---|
os.File |
✔ | ✔ | ✔ | ✔ |
ファイルからの入力には、今までに何度も出てきた os.File
構造体を使います。 新規作成は os.Create()
関数で行っていましたが、 os.Open()
関数を使うと既存のファイルを開くことができます。 内部的にはこの2つの関数は os.OpenFile()
関数のフラグ違いのエイリアスで、同じシステムコールが呼ばれています。
func Open(name string) (*File, error) {
return OpenFile(name, O_RDONLY, 0)
}
func Create(name string) (*File, error) {
return OpenFile(name, O_RDWR|O_CREATE|O_TRUNC, 0666)
}
ファイルの読み込みは次のとおりです。さきほど紹介した io.Copy()
を使って標準出力にファイルの内容をすべて書き出しています。
package main
import (
"io"
"os"
)
func main() {
file, err := os.Open("file.go")
if err != nil {
panic(err)
}
defer file.Close()
io.Copy(os.Stdout, file)
}
なお、ファイルを一度開いたら Close()
する必要があります。 Go言語ではこのような「確実に行う後処理」を実行するのに便利な仕組みがあります。 defer file.Close()
と書かれているところがその機能です。 defer
は、現在のスコープが終了したら、その後ろに書かれている行の処理を実行します。
ファイルの作成、読み込みと、インタフェース間のデータコピーの方法がわかったので、ファイルをコピーする処理もかんたんに書けますね。ぜひ挑戦してみてください。
インターネット通信
インタフェース | io.Reader |
io.Writer |
io.Seeker |
io.Closer |
---|---|---|---|---|
net.Conn |
✔ | ✔ | ✔ |
インターネット上でのデータのやり取りは、送信データを送信者側から見ると書き込みで、受信者側から見ると読み込みです。 書き込みについては前回 io.Writer
で説明しました。 下記のコードは、前回紹介したのと同じコードですが、これで受信側からすれば読み込みになります。
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)
}
net.Dial
で返される conn
が net.Conn
型で、これを io.Copy
を使って標準出力にコピーすることでデータを一括で読み込んでいます。 この場合、読み込まれるのは生のHTTPの通信内容そのものです。
この方法はシンプルではありますが、HTTPを読み込むプログラムを開発するたびにRFCに従ってパース処理を実装するのは効率的ではありません。 Go言語では、HTTPのレスポンスをパースする http.ReadResponse()
関数が用意されています。 この関数に bufio.Reader
でラップした net.Conn
を渡すと、 http.Response
構造体のオブジェクトが返されます。 bufio.Reader
でラップするには、 bufio.NewReader()
関数を呼びます。 このオブジェクトはHTTPのヘッダーやボディーなどに分解されているため、プログラムでの利用がとてもかんたんです。
package main
import (
"bufio"
"fmt"
"io"
"net"
"net/http"
"os"
)
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"))
res, err := http.ReadResponse(bufio.NewReader(conn), nil)
// ヘッダーを表示してみる
fmt.Println(res.Header)
// ボディーを表示してみる。最後にはClose()すること
defer res.Body.Close()
io.Copy(os.Stdout, res.Body)
}
参考までに、HTTPリクエストの作成に使う構造体もあります。 その構造体のメンバー変数を使ってヘッダーを追加するなどしてから、最後に Write()
メソッドを呼ぶことで、サーバにリクエストを送信できます。
req, err := http.NewRequest("GET", "http://ascii.jp", nil)
req.Write(conn)
ですが、よりいっそう高級なAPIも提供されているため、このように自分で Write()
メソッドを呼ぶのはかなりのレアケースです。 実際、ネットで「Go言語/インターネットアクセス」などのキーワードで検索して情報が大量に出てくるのは高級なAPIを利用する方法のほうです。
メモリに蓄えた内容をio.Reader
として読み出すバッファ
前回の記事では、書き込まれた内容をメモリに保持しておく bytes.Buffer
を紹介しました。 これは io.Reader
としても使えます。 読み出しに使えるものとしては、これ以外に bytes.Reader
と strings.Reader
もありますが、ほとんどのケースではこれらを使い分ける必要はありません bytes.Buffer
だけ覚えておけば問題はないでしょう。
ただ、次回紹介するバイナリデータの解析に使う io.SectionReader
だけは io.Reader
ではなく、 io.ReaderAt
というちょっと違うインタフェースのReaderを必要とします。 そのときだけは、マイナーな bytes.Reader
と strings.Reader
が役に立ちます。
初期化の方法は何通りかあります。それぞれ、初期データが必要かどうかや、初期化データの型に応じて使い分けます。
// 空のバッファ
var buffer1 bytes.Buffer
// バイト列で初期化
buffer2 := bytes.NewBuffer([]{byte{0x10, 0x20, 0x30})
// 文字列で初期化
buffer3 := bytes.NewBufferString("初期文字列")
このうち、最初の初期化だけはポインタではなくて実体なので、 io.Writer
や io.Reader
の引数に渡すときは &buffer1
のようにポインタ値を取り出す必要があります。
あまり使わないほうの bytes.Reader
と strings.Reader
の初期化も紹介します。
// bytes.Readerはbytes.NewReaderで作成
bReader1 := bytes.NewReader([]byte{0x10, 0x20, 0x30})
bReader2 := bytes.NewReader([]byte("文字列をバイト配列にキャストして設定")
// strings.Readerはstrings.NewReader()関数で作成
sReader := strings.NewReader("Readerの出力内容は文字列で渡す")
io.Reader
とio.Writer
を考えるためのモデル図
C++やJava、Node.jsでは、各言語で定義されたインタフェースを使ったデータ入出力の機構を「ストリーム」と呼んでいます。 Go言語ではストリームという言い方はしませんが、 io.Reader
と io.Writer
をデータが流れるパイプとして使うことができます。
データの流れを組み立てる道具として、これまで紹介してきた関数や構造体をモデル図にしてみました。 丸いコネクタが io.Reader
の送受信、三角形のコネクタが io.Writer
の送受信を表しています。 データはすべて左から右に流れます。
次回はこのストリームを自由自在に組み立てるための道具をさらにいろいろ紹介します。
クイズ
io.Reader
とio.Writer
の使い方がいろいろわかったところで、組み合わせて遊んでみましょう。
Q1. ファイルのコピー
古いファイル(old.txt
)を新しいファイル(new.txt
)にコピーしてみましょう。
今回の記事のサンプルを見てみれば難しくないと思います。
このプログラムを改造して実用的なコマンドにしてみたいと思われる方は、コマンドラインオプションでファイル名を渡せるようにするとよいでしょう。 本連載の範囲からは外れるので詳細は省きますが、 os.Args
が文字列配列でオプションが格納されます。 また、標準ライブラリではflag
パッケージを使うとオプションのパース処理がより便利に行えます。
Q2. テスト用の適当なサイズのファイルを作成
ファイルを作成してランダムな内容で埋めてみましょう。
本来は暗号用の機能ですが、crypto/rand
パッケージをインポートすると、rand.Reader
というio.Reader
が使えます。 このReaderはランダムなバイトを延々と出力し続ける無限長のファイルのような動作をします。 これを使って、1024バイトの長さのバイナリファイルを作ってみましょう。
ヒントですが、io.Copy()
を使ってはいけません。io.Copy()
はReaderの終了まですべて愚直にコピーしようとします。 そしてrand.Reader
には終わりはありません。 後はわかりますよね?
Q3. zipファイルの書き込み
OSのデバイスにリンクされたio.Writer
やio.Reader
は、1つのファイルやデバイスと1対1に対応しています。 Go言語が提供するライブラリには、1つのファイルで複数のio.Writer
やio.Reader
の仲間で構成されているものもあります。 複数ファイルを格納するアーカイブフォーマットのtarやzipファイルや、インターネットのマルチパート形式(ブラウザのフォームによって作られるデータやファイルを複数格納するデータ構造)をサポートするmime/multipart
パッケージの構造体は、中に格納されるひとつひとつの要素がio.Writer
やio.ReadCloser
(後述)になっています。
archive/zip
` パッケージを使ってzipファイルを作成してみましょう。 出力先のファイルの Writer
(以下のコードの file
)をまず作って、 それをzip.NewWriter()
関数に渡すと、zipファイルの書き込み用の構造体ができます。 最後にClose()
を確実に呼ぶ必要があるので、作成した場所でdefer
を使いましょう。
zipWriter := zip.NewWriter(file)
defer zipWriter.Close()
この構造体そのものはio.Writer
ではありませんが、Create()
メソッドを呼ぶと、個別のファイルを書き込むためのio.Writer
が返ってきます。
writer, err := zipWriter.Create("newfile.txt")
上記の例では、 newfile.txt
という実際のファイルが、最初に作った出力先のファイル file
へと圧縮されます。 では、実際のファイルではなく、文字列strings.Reader
を使ってzipファイルを作成するにはどうすればいいでしょうか。考えてみてください。
Q4. zipファイルをウェブサーバからダウンロード
zipファイルの出力先は単なるio.Writer
です。そのため、前回紹介したウェブサーバで、zipファイルを作成してそのままダウンロードさせるといったことも可能です。 ウェブサーバにブラウザでアクセスしたらファイルがダウンロードされるようにしてみましょう。
この場合は、次のようにContent-Type
ヘッダーを使ってファイルの種類がzipファイルであることをブラウザに教えてあげる必要があります。 必須ではありませんが、ファイル名も指定できます。
func handler(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/zip")
w.Header().Set("Content-Disposition", "attachment; filename=ascii_sample.zip")
}
余談:複合インタフェース
インタフェース | io.Reader |
io.Writer |
io.Seeker |
io.Closer |
---|---|---|---|---|
io.ReadWriter | ✔ | ✔ | ||
io.ReadSeeker | ✔ | ✔ | ||
io.ReadCloser | ✔ | ✔ | ||
io.WriteSeeker | ✔ | ✔ | ||
io.WriteCloser | ✔ | ✔ | ||
io.ReadWriteSeeker | ✔ | ✔ | ✔ | |
io.ReadWriteCloser | ✔ | ✔ | ✔ |
今回の記事の冒頭で、 io.Writer
と io.Reader
の仲間のインタフェースをいくつか紹介しました。 実際には、 io.Closer
だけを実装した構造体を扱うことは基本的にありません。 これらのインタフェースについては、 io.Reader
や io.Writer
と組み合わさった構造体しかないため、APIで要求される引数や返り値の型としては、次の表にあるような複合インタフェースが使用されます。 たとえば、 io.ReadCloser
であれば、 Read()
メソッドと Close()
メソッドの両方を持つオブジェクトが要求されることがわかります。
引数に io.ReadCloser
が要求されているが、今あるオブジェクトは io.Reader
しか満たしていない、ということもたまにあります。 たとえば、ソケット読み込み用の関数を作成していて、その関数の引数は io.ReadCloser
だが、ユニットテストには io.Reader
インタフェースを持つ strings.Reader
や bytes.Buffer
を使いたい、といったケースが考えられます。 その場合は ioutil.NopCloser
関数を使うと、ダミーの Close()
メソッドを持って io.ReadCloser
のフリをする(ただし Close()
しても何も起きない)ラッパーオブジェクトが得られます。
import (
"io"
"io/ioutil"
"strings"
)
var reader io.Reader = strings.NewReader("テストデータ")
var readCloser io.ReadCloser = ioutil.NopCloser(reader)
バッファリングが入ってしまいますが、 bufio.NewReadWriter()
関数を使うと、個別の io.Reader
と io.Writer
をつなげて、 io.ReadWriter
型のオブジェクトを作ることができます。
import (
"bufio"
)
var readWriter io.ReadWriter = bufio.NewReadWriter(reader, writer)
今回のまとめと次回予告
今回はio.Reader
の仲間たちと、io.Reader
と一緒に使う補助関数、具体的なサンプルをいくつか紹介しました。 書き込みと読み込みとでシステムコールは対称ですが、ほとんどの場合、書き込みと比べると読み込みのほうが複雑な機能が求められます。 そのため、読み込みについては補助関数の機能や種類も豊富です。
次回は、io.Reader
の応用編として、バイナリデータの解析、テキストデータの解析、より複雑なストリームを構築するための抽象度の高い構造体を紹介します。
クイズの答え
Q1. ファイルのコピー
package main
import (
"io"
"os"
)
func main() {
oldFile, err := os.Open("old.txt")
if err != nil {
panic(err)
}
defer oldFile.Close()
newFile, err := os.Create("new.txt")
if err != nil {
panic(err)
}
defer newFile.Close()
io.Copy(newFile, oldFile)
}
Q2. テスト用の適当なサイズのファイルを作成
package main
import (
"crypto/rand"
"io"
"os"
)
func main() {
file, err := os.Create("rand.txt")
if err != nil {
panic(err)
}
defer file.Close()
io.CopyN(file, rand.Reader, 1024)
}
Q3. zipファイルの書き込み
package main
import (
"archive/zip"
"io"
"os"
"strings"
)
func main() {
// zipの内容を書き込むファイル
file, err := os.Create("sample.zip")
if err != nil {
panic(err)
}
defer file.Close()
// zipファイル
zipWriter := zip.NewWriter(file)
defer zipWriter.Close()
// ファイルの数だけ書き込み
a, err := zipWriter.Create("a.txt")
if err != nil {
panic(err)
}
io.Copy(a, strings.NewReader("1つめのファイルのテキストです"))
b, err := zipWriter.Create("b.txt")
if err != nil {
panic(err)
}
io.Copy(b, strings.NewReader("2つめのファイルのテキストです"))
}
Q4. zipファイルをウェブサーバからダウンロード
package main
import (
"archive/zip"
"io"
"net/http"
"strings"
)
func handler(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/zip")
w.Header().Set("Content-Disposition", "attachment; filename=ascii_sample.zip")
// zipファイル
zipWriter := zip.NewWriter(w)
defer zipWriter.Close()
// ファイルの数だけ書き込み
a, err := zipWriter.Create("a.txt")
if err != nil {
panic(err)
}
io.Copy(a, strings.NewReader("1つめのファイルのテキストです"))
b, err := zipWriter.Create("b.txt")
if err != nil {
panic(err)
}
io.Copy(b, strings.NewReader("2つめのファイルのテキストです"))
}
func main() {
http.HandleFunc("/", handler)
http.ListenAndServe(":8080", nil)
}
注釈
1.ネットワークアクセスの net.Conn
だけにはタイムアウトがあります↩
この連載の記事
-
第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) - この連載の一覧へ