このページの本文へ

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

低レベルアクセスへの入り口(2):io.Reader前編

2016年10月19日 19時00分更新

文● 渋川よしき、編集● 鹿野桂一郎

  • この記事をはてなブックマークに追加
  • 本文印刷

 前回は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.Readerio.Writer は、同じことをするのに手間が少しかかります。 しかし、取り扱い方がわかり、使いこなせるようになれば、より多くのことができるようになります。

ioutil.ReadFile()はio.Readerを簡単に使えるように隠蔽したもの

 ソフトウェアは基本的に、「何らかの入力に対して、加工を行ってから、出力する」というパイプ構造をしています。 したがって、入出力を思い通りに扱うことは、ソフトウェア開発者にとってなくてはならないスキルです。 それが低レベルを知ることの強みのひとつといえるでしょう。

低レベルの機能を組み合わせて自分だけのオリジナルの入出力処理を作ろう

 書き込みに比べると読み込みのほうがトピックが多く、機能も多いため、 io.Reader まわりについては2回に分けてお届けします。 今回はその第1回ということで、Go言語における低レベルな入力の仕組みについて次の構成で説明していきます。

  • まずは本文のための前提知識として、 io.Reader とその仲間たちの紹介
  • 少ないコード量で io.Reader からデータを効率良く読み込むための補助的な関数群
  • io.Reader を満たす構造体で特に頻繁に使われるものの紹介(標準入力、ファイル、ソケット、メモリのバッファ)

 後編である次回は、次のトピックをお届けする予定です。

  • バイナリ解析に便利な機能群
  • テキスト解析に便利な機能群
  • ちょっと抽象的な io.Reader の構造体

io.Readerio.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.Readerio.Writerio.Seekerio.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 で返される connnet.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.Readerstrings.Reader もありますが、ほとんどのケースではこれらを使い分ける必要はありません bytes.Buffer だけ覚えておけば問題はないでしょう。

 ただ、次回紹介するバイナリデータの解析に使う io.SectionReader だけは io.Reader ではなく、 io.ReaderAt というちょっと違うインタフェースのReaderを必要とします。 そのときだけは、マイナーな bytes.Readerstrings.Reader が役に立ちます。

 初期化の方法は何通りかあります。それぞれ、初期データが必要かどうかや、初期化データの型に応じて使い分けます。

// 空のバッファ
var buffer1 bytes.Buffer
// バイト列で初期化
buffer2 := bytes.NewBuffer([]{byte{0x10, 0x20, 0x30})
// 文字列で初期化
buffer3 := bytes.NewBufferString("初期文字列")

 このうち、最初の初期化だけはポインタではなくて実体なので、 io.Writerio.Reader の引数に渡すときは &buffer1 のようにポインタ値を取り出す必要があります。

 あまり使わないほうの bytes.Readerstrings.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.Readerio.Writerを考えるためのモデル図

 C++やJava、Node.jsでは、各言語で定義されたインタフェースを使ったデータ入出力の機構を「ストリーム」と呼んでいます。 Go言語ではストリームという言い方はしませんが、 io.Readerio.Writer をデータが流れるパイプとして使うことができます。

 データの流れを組み立てる道具として、これまで紹介してきた関数や構造体をモデル図にしてみました。 丸いコネクタが io.Reader の送受信、三角形のコネクタが io.Writer の送受信を表しています。 データはすべて左から右に流れます。

データの流れを組み立てるものとして、これまでに登場した関数や構造体をモデル化

次回はこのストリームを自由自在に組み立てるための道具をさらにいろいろ紹介します。

クイズ

 io.Readerio.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.Writerio.Readerは、1つのファイルやデバイスと1対1に対応しています。 Go言語が提供するライブラリには、1つのファイルで複数のio.Writerio.Readerの仲間で構成されているものもあります。 複数ファイルを格納するアーカイブフォーマットのtarやzipファイルや、インターネットのマルチパート形式(ブラウザのフォームによって作られるデータやファイルを複数格納するデータ構造)をサポートするmime/multipartパッケージの構造体は、中に格納されるひとつひとつの要素がio.Writerio.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.Writerio.Reader の仲間のインタフェースをいくつか紹介しました。 実際には、 io.Closer だけを実装した構造体を扱うことは基本的にありません。 これらのインタフェースについては、 io.Readerio.Writer と組み合わさった構造体しかないため、APIで要求される引数や返り値の型としては、次の表にあるような複合インタフェースが使用されます。 たとえば、 io.ReadCloser であれば、 Read() メソッドと Close() メソッドの両方を持つオブジェクトが要求されることがわかります。

 引数に io.ReadCloser が要求されているが、今あるオブジェクトは io.Reader しか満たしていない、ということもたまにあります。 たとえば、ソケット読み込み用の関数を作成していて、その関数の引数は io.ReadCloser だが、ユニットテストには io.Reader インタフェースを持つ strings.Readerbytes.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.Readerio.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 だけにはタイムアウトがあります

カテゴリートップへ

この連載の記事
ピックアップ