【Go入門】ioパッケージ ~ 入出力処理の抽象化と共通化

シェアする

こんにちは。Go入門ブログの第24回です。

前回の記事で、GoでシンプルなWEBサーバを構築し、HTTPリクエストに対しレスポンスを返す方法を解説しました。
その中で、HTMLを出力するために、io.WriteStringという関数を使用しました。

【Go入門】net/httpパッケージを使ったWEBサーバの構築とHTTPリクエストハンドラの実装

ioパッケージは、Goにおける入力処理と出力処理の基本となるパッケージです。
今回の記事では、このioパッケージについて解説したいと思います。

ioパッケージとは – io.Readerとio.Writer

ioパッケージは、Goの入出力処理における基本的な型とインターフェースを提供する標準パッケージです。
その中心要素が、io.Readerio.Writerという2つのインターフェースです。

io.Readerは入力処理の、io.Writerは出力処理の、それぞれ基本となるインターフェースです。それぞれ以下のように、単一のメソッドReadWriteが定義されています。

type Reader interface {
     Read(p []byte) (n int, err os.Error)
}
type Writer interface {
     Write(p []byte) (n int, err os.Error)
}

io.Reader – 入力処理の基本インターフェース

io.Readerは、データを読み込む為の機能であり、何らかのバイト列の入力を抽象化します。
ラップされているメソッドReadの引数であるpは、読込内容を一時的に格納するバッファです。

io.Readerの実装例

io.Reader型を取り扱うケースは、ioパッケージ以外の機能に実装された機能を使用してio.Readerを取得する場合がほとんどです。

io.Readerインターフェースを満たす型の代表的な例の一つとして、以前の記事で学習したos.File型が挙げられます。

【Go入門】osパッケージ ~ ファイルとディレクトリの操作

os.File型でReadメソッドを使用できることを覚えておられますでしょうか。
このReadメソッドはio.Readerインターフェースを満たします。
したがってos.File型はio.Reader型を満たしており、io.Reader型として扱うことができます。

この他にも、これまでの記事では未解説の内容ですが、以下のような型もio.Readerインターフェースに準拠しています。

  • stringsパッケージのstrings.Reader型(文字列から読み込みを行う)
  • bytesパッケージのbytes.Buffer型(メモリへ書き込まれたデータを読み込む)
  • netパッケージのnet.Conn型(ネットワーク接続情報を読み込む)

このように、ファイルやメモリ、ネットワーク等からの入力処理を、すべてio.Readerインターフェースという共通の型で取り扱うことで、入力処理の抽象化と共通化を実現しているのです。

io.Readerを返す関数

もちろんioパッケージ自体にも、io.Readerを返す関数は存在します。
しかし、それらの関数は、既存のReaderの内容から新たなReaderを返すものです。

代表的なものとして、io.LimitReader関数とio.MultiReader関数が挙げられます。

io.LimitReader関数は、第1引数で指定したReaderの内容から、第2引数で指定したバイト数まで読み込みを行うReaderを返します。

func LimitReader(r Reader, n int64) Reader

io.MultiReader関数は、任意の数のReaderを引数として渡すことができます。
与えた順にReaderの内容を読み込み、連結したReaderを返します。

func MultiReader(readers …Reader) Reader
io.Readerから読み込みを行うための関数

ここまではReader型のオブジェクトを返す関数を見てきました。
Reader型から読み込みを行うための関数も、ioパッケージに用意されています。
代表的なものとして、io.ReadAtLeast関数とio.ReadFull関数が挙げられます。

io.ReadAtLeast関数は、読み込むバイト数の最小値を指定して読み込みを行う関数です。

第1引数で指定したReaderの内容を、第2引数で指定したバイトスライスへ読み込みます。
この際、読み込むバイト数は第3引数で指定した数値以上となります。
指定したバイト数を読み込めなかった場合はエラーが返されます。

func ReadAtLeast(r Reader, buf []byte, min int) (n int, err os.Error)

io.ReadFull関数は、バイトスライスのバッファサイズいっぱいまで正確に読み込みを行う関数です。
第1引数で指定したReaderの内容を、第2引数で指定したバイトスライスへ読み込みます。
この際、読み込むバイト数は、第2引数で指定したバイトスライスのバッファサイズとなります。
読み込まれたバイト数がバッファサイズより少ない場合はエラーが返されます。

func ReadFull(r Reader, buf []byte) (n int, err os.Error)
サンプルコード

説明が長くなってしまいましたが、ここまでの内容を踏まえてサンプルコードをみてみましょう。

package main

import (
    "fmt"
    "io"
    "strings"
)

func main() {
    // strings.NewReaderを使用してio.Readerインターフェースに準拠するstrings.Reader型を生成する
    r1 := strings.NewReader("This is 1st Reader.\n")
    r2 := strings.NewReader("This is 2nd Reader.\n")
    r3 := strings.NewReader("This is 3rd Reader.\n")

    // io.LimitReaderでr1から読み込み
    lr := io.LimitReader(r1, 15)

    // io.MultiReaderでr2とr3を連結
    mr := io.MultiReader(r2, r3)

    // 20バイト、30バイトのバイトスライスを宣言
    buf1 := make([]byte, 20)
    buf2 := make([]byte, 30)

    // io.ReadAtLeast関数でlrから10バイト以上をbuf1に読み込む
    size1, _ := io.ReadAtLeast(lr, buf1, 10)
    // buf1を出力
    fmt.Printf("%s (%dバイト読込)\n", string(buf1), size1)

    // io.ReadFull関数でmrをbuf2に読み込む
    size2, _ := io.ReadFull(mr, buf2)
    // buf2を出力
    fmt.Printf("%s (%dバイト読込)", string(buf2), size2)

}

まず11-13行目で、stringsパッケージのNewReader関数を使用して、3つのstrings.Reader型を生成しています。

次に16,19行目では、この3つのstrings.Reader型から、io.LimitReader関数とio.MultiReader関数によって新たに2つのio.Reader型を生成しています。

26-28行目では、io.ReadAtLeast関数を使ってio.Readerからの読み込みを行い、読み込んだ結果を出力しています。
31-33行目では、io.ReadFull関数を使い、同様にio.Readerからの読み込みと出力を行っています。

このサンプルの実行結果は以下のようになります。

$ go run main.go
This is 1st Rea (15バイト読込)
This is 2nd Reader.
This is 3r (30バイト読込)

io.Writer – 出力処理の基本インターフェース

io.Writerは、データを読み込む為の機能であり、何らかのバイト列の出力を抽象化します。
ラップされているメソッドWriteの引数であるpは、書込内容を一時的に格納するバッファです。

io.Writerの実装例

io.Writerにも、io.Readerと同様のことが言えます。
つまり、os.File型をはじめとした様々な型がio.Writerインターフェースに準拠したwriteメソッドを持っており、それによってio.Writerが出力処理の抽象化と共通化の役割を果たしているということです。

io.Writerインターフェースを満たす例としてしては、前述のos.File型bytes.Buffer型net.Conn型の他、net.httpパッケージのhttp.ResponseWriter型などが挙げられます。

io.Writerを返す関数

ioパッケージ内でio.Writerを返す関数はio.MultiWriterです。

io.MultiWriter関数は、任意の数のWriterを引数として渡すことができ、与えられたすべてのWriterに対して書き込みを行う新たなWriterを作成して返します。

func MultiWriter(writers …Writer) Writer
io.Writerに書き込みを行うための関数

Writer型に書き込みを行うための関数としては、io.WriteString関数があります。

io.WriteString関数は、Writer型に文字列リテラルで記述された内容を書き込む関数です。
第1引数に渡されたWriterに対し、第2引数の文字列内容を書き込みます。
返り値は書き込んだバイト数とエラーです。

func WriteString(w Writer, s string) (n int, err os.Error)
サンプルコード

それでは、実際にio.Writerを使用する例を見てみましょう。
前回の記事でHTML書き出しを行った例を以下のように改変してみます。

package main

import (
    "io"
    "net/http"
    "os"
)

func hogeHandler(w http.ResponseWriter, req *http.Request) {

    // 新規ファイルを作成
    trgFile, _ := os.Create("writer.txt")
    defer trgFile.Close()

    // io.MultiWriterでhttpレスポンス、ファイル、標準出力に書き込みを行うWriterを作成
    mw := io.MultiWriter(w, trgFile, os.Stdout)

    // io.WriteStringでmwに書き込みを実行
    io.WriteString(mw, `
    <!DOCTYPE html>
    <html lang="ja">
    <head>
      <meta charset="UTF-8">
      <title>Go | io.Writer</title>
    </head>
    <body>
      <h1>Hello, World!</h1>
      <p>io.MultiWriterを使用すると、複数のWriter型へ同じ内容を書き込むことができます。</p>
      <p>書き込みの実行にはio.WriteStringを使用します。</p>
    </body>
    </html>
`)
}

func main() {
    // "/hoge"へのリクエストを関数で処理する
    http.HandleFunc("/hoge", hogeHandler)
    // localhost:8080でサーバー処理開始
    http.ListenAndServe(":8080", nil)
}

16行目のMultiWriter関数で、http.ResponseWriter、新規作成したファイル、標準出力に書き込みを行うWriterを受け取っています。os.Stdoutはosパッケージで定義されており、標準出力を示します。

19行目のWriteString関数で、このWriterに対しHTMLテキストの書き込みを行っています。

このサンプルを実行し、ブラウザでhttp://localhost:8080/hogeにアクセスすると、記述したHTMLが返されることを確認できます。
同時に、実行ファイルと同じパスにwriter.txtが作成され、HTMLテキストが出力されています。
更に標準テキストにも、同一の内容が出力されます。

io.Copy

もうひとつ、ioパッケージに実装される機能で使用頻度の高いものとして、io.Copy関数が挙げられます。

io.Copy関数は、ReaderからWriterへのコピーを行う関数です。
第1引数にWriterを、第2に引数にReaderをそれぞれ指定することができ、受け取ったReaderからWriterにデータをそのまま渡します。返り値はコピーしたバイト数とエラーです。

func Copy(w Writer, r Reader) (written int64, err os.Error)

さっそく使用例を見てみましょう。
先ほどWriterのサンプルコードで作成したテキストファイルを使用し、ファイルの複製を行うサンプルです。

package main

import (
    "fmt"
    "io"
    "os"
)

func main() {
    // コピー元のファイルをオープン
    orgFile, err := os.Open("writer.txt")
    if err != nil {
        panic(err)
    }
    defer orgFile.Close()

    // コピー先の新規ファイルをオープン
    newFile, err := os.Create("copy.txt")
    if err != nil {
        panic(err)
    }
    defer newFile.Close()

    // ファイルの内容をコピー
    bytes, err := io.Copy(newFile, orgFile)
    if err != nil {
        panic(err)
    }

    fmt.Printf("コピー完了: %dバイト", bytes)

}

このサンプルを実行すると、実行ファイルと同じパスにcopy.txtが作成され、writer.txtと同一の内容が書き込まれていることが確認できます。
また標準テキストには、以下の内容が出力されます。

コピー完了: 391バイト

終わりに

ioパッケージ自体はさほど多くの機能を持たず、単体で使用することはさほど多くありませんが、Goの入出力処理の基本となる知識であることは間違いありません。
抽象度の高い内容も多く理解が難しい点もあったかもしれませんが、Reader・Writerという基本インターフェースがioパッケージに実装されていることは抑えておきましょう。

スポンサーリンク
スポンサーリンク
スポンサーリンク

シェアする

フォローする

スポンサーリンク
スポンサーリンク