倭マン's BLOG

くだらない日々の日記書いてます。 たまにプログラミング関連の記事書いてます。 書いてます。

Go 言語の io パッケージにある具象 Reader, Writer を使ってみる

Go 言語のいろいろなパッケージを使ってみるシリーズ(目次)。 今回は io パッケージに定義されている具象 io.Reader, io.Writer を見ていきます。 io パッケージに定義されているインターフェース型については「Go 言語の io パッケージに定義されているインターフェース型を一気見する」を参照。

【この記事の内容】

io パッケージに定義されている具象 io.Reader, io.Writer

io パッケージには io.Reader/io.Writer をラップして各種機能を提供する具象 io.Reader, io.Writer がいくつか定義されています。 それらはまた io.Reader/io.Writer のいずれかを実装しているので、使い方は簡単です:

// io.Reader 型、io.Reader を返すメソッド
func MultiReader(readers ...Reader) Reader
func TeeReader(r Reader, w Writer) Reader

type LimitedReader
func LimitReader(r Reader, n int64) Reader

type SectionReader
func NewSectionReader(r ReaderAt, off int64, n int64) *SectionReader

// io.Writer を返すメソッド
func MultiWriter(writers ...Writer) Writer

// パイプ
func Pipe() (*PipeReader, *PipeWriter)
type PipeReader
type PipeWriter

io.Reader を返すメソッド

io.MultiReader 関数
io.MultiReader 関数が返す io.Reader オブジェクトは、指定した複数の io.Reader から順に内容を読み込みます:

  r0 := strings.NewReader("Hello, world0!\n")
  r1 := strings.NewReader("Hello, world1!\n")
  r2 := strings.NewReader("Hello, world2!\n")

  // io.MultiReader
  mr := io.MultiReader(r0, r1, r2)

  content, err = ioutil.ReadAll(mr)
  if err != nil { log.Fatal(err) }
  fmt.Println(string(content))
  // Output:
  // Hello, world0!
  // Hello, world1!
  // Hello, world2!

まぁ特に難しくはありませんね。

io.TeeReader 関数
io.TeeReader 関数が返す io.Reader オブジェクトは、読み込んだ内容を指定した io.Writer に書き込みます:

  logFile, _ := os.Create("log.txt")
  defer logFile.Close()

  // io.TeeReader
  tr := io.TeeReader(strings.NewReader("Hello, world!\n"), logFile)

  content, err = ioutil.ReadAll(tr)
  if err != nil { log.Fatal(err) }
  fmt.Println(string(content))
  // Output:
  // Hello, world!

io.Reader から読み取った「Hello, world!」という文字列は標準出力に表示されますが、読み込んだ時点で log.txt ファイルに読み取った内容が書き込まれます:

// log.txt ファイル
Hello, world!

io.LimitReader 関数
io.LimitReader 関数は、指定したバイト数だけを読み取れる io.Reader を返します:

  // io.LimitReader
  lr := io.LimitReader(strings.NewReader("Hello, world!"), 5)

  content, err := ioutil.ReadAll(lr)
  if err != nil { log.Fatal(err) }
  fmt.Println(string(content))
  // Output:
  // Hello

引数として渡した io.Reader の内容「Hello, world!」のうち、5バイトの「Hello」のみが読み取れます。

io パッケージには、以下のように定義される LimitedReader 型も公開されているので

type LimitedReader interface{
  R Reader
  N int64
}

同じことを構造体の作成を使ってもできます:

  // LimitedReader
  lr := &io.LimitedReader{strings.NewReader("Hello, world!"), 5}

  content, err := ioutil.ReadAll(lr)
  if err != nil { log.Fatal(err) }
  fmt.Println(string(content))
  // Output:
  // Hello

まぁ、LimitedReader 型に Read メソッド以外の便利なメソッドが定義されているわけでもないので、普通に LimitReader 関数を使っておく方が無難かと思いますが。

io.SectionReader 関数
io.SectionReader は指定した位置から指定したバイト数だけ読み取ります。 インスタンスを取得するには NewSectionReader 関数を使います。 io.SectionReader には以下のようなメソッド

  • func (s *SectionReader) Read(p []byte) (n int, err error)
  • func (s *SectionReader) ReadAt(p []byte, off int64) (n int, err error)
  • func (s *SectionReader) Seek(offset int64, whence int) (int64, error)
  • func (s *SectionReader) Size() int64

が定義されていて、io.Reader, io.ReaderAt, io.Seek インターフェースを実装しています。 まぁ、ランダムアクセスできるリーダーって感じですかね。 使い方は以下のようになります:

  // SectionReader
  sr := io.NewSectionReader(strings.NewReader("Hello, world!"), 3, 5)

  content, err = ioutil.ReadAll(sr)
  if err != nil { log.Fatal(err) }
  fmt.Println(string(content))
  // Output:
  // lo, w

引数の io.Reader の内容「Hello, world!」に対して、位置3から5バイト分を読み取って「lo, w」と表示します。 引数の io.Reader に指定した内容分のバイトが含まれていなければ、ある分だけの内容を読み取ります。

io.Writer を返すメソッド

io.MultiWriter 関数
io.MultiWriter 関数が返す io.Writer オブジェクトは、複数の io.Writer に同じ内容を書き出します:

  logFile, _ := os.Create("log.txt")
  defer logFile.Close()

  // MultiWriter
  mw := io.MultiWriter(os.Stdout, logFile)
  io.WriteString(mw, "Hello, world!")
    // io.WriteString 関数は io.Writer に文字列を書き出す
  // Output:
  // Hello, world!

これを実行すると、標準出力に「Hello, world!」と表示され、log.txt ファイルにも同じ内容が書き込まれます。

io.Pipe 関数

io.Pipe 関数はパイプされた io.Reader/io.Writer の組を返します。 パイプされたファイルの組を返す、同様の関数 os.Pipe 関数は「Go 言語の os パッケージにある File 型を使ってみる (1) : os.File オブジェクトを取得する関数」で使ってみました。 そこで見たサンプルコードとほぼ同じコードを io.Pipe 関数を使って書いてみると、以下のようになります:

  // io.Pipe 関数
  r, w := io.Pipe()

  go func(){
    for i := 0; i < 10; i++ {
      io.WriteString(w, fmt.Sprintf("Hello, world%d!\n", i))
        // io.WriteString で io.Writer に文字列を書き込む 
      time.Sleep(100*time.Millisecond)
    }
    if err := w.Close(); err != nil { log.Fatal(err) }
  }()

  bs := make([]byte, 20)
    for {
      time.Sleep(1*time.Second)
      n, err := r.Read(bs)
      if err == io.EOF {
        break
      } else if err != nil {
        log.Panic(err)
      }
		
      fmt.Print(string(bs[:n]))
    }

    if err := r.Close(); err != nil { log.Fatal(err) }

コードは大体同じですが io.Writer は(*os.File と違って)WriteString メソッドを持っていないので、io.WriteString 関数を使って文字列を書き込んでいる箇所が少し違います*1。 io.Pipe 関数と os.Pipe 関数の使い分けがイマイチよく分かりませんが、ファイルを使いたい場合以外は io.Pipe 関数を使っておけばいいんでしょうかね?*2

今回は io パッケージに定義されている具象 io.Reader, io.Writer の使い方を見てきました。 まぁ、汎用的な用途のあるものばかりなので、使い方に迷うことはなさそうです。 次回は io パッケージに定義されているパッケージ関数を使ってみます。

プログラミング言語Go (ADDISON-WESLEY PROFESSIONAL COMPUTING SERIES)

プログラミング言語Go (ADDISON-WESLEY PROFESSIONAL COMPUTING SERIES)

*1:以前の os.Pipe 関数のコードを、io.Pipe 関数の場合でも同じように動くように少し修正しました。

*2:io.Pipe 関数で作成したパイプはバッファを持たずに読み書きをブロックすると書いてありますが、os.Pipe 関数の方は特に何も書いてませんね。 上記のサンプルコードと os.Pipe の同様のコードを動かすと少々動作に違いがあるので、何かしらの使い分けはした方がよいかと思いますが。