グローバルナビゲーションへ

本文へ

フッターへ

お役立ち情報Blog



<Goのパッケージ放浪記> ioパッケージに定義されている「Readerインターフェイス」について

今日は、Goのパッケージのソースを覗いてみたいと思います。
普段からさまざまなパッケージを利用しますが、その実装はどうなっているのかを把握し、よりGoらしいコードを書けるようになることが目的です。

今回はioパッケージのReaderインターフェイスまわりを読んでいきます。

今回覗いていくソースのバージョンは「go1.14.4」です。

Readerインターフェイス

まず、最初に定義されているのがReaderインターフェイスです。

// Reader is the interface that wraps the basic Read method.
//
// Read reads up to len(p) bytes into p. It returns the number of bytes
// read (0 <= n <= len(p)) and any error encountered. Even if Read
// returns n < len(p), it may use all of p as scratch space during the call.
// If some data is available but not len(p) bytes, Read conventionally
// returns what is available instead of waiting for more.
//
// When Read encounters an error or end-of-file condition after
// successfully reading n > 0 bytes, it returns the number of
// bytes read. It may return the (non-nil) error from the same call
// or return the error (and n == 0) from a subsequent call.
// An instance of this general case is that a Reader returning
// a non-zero number of bytes at the end of the input stream may
// return either err == EOF or err == nil. The next Read should
// return 0, EOF.
//
// Callers should always process the n > 0 bytes returned before
// considering the error err. Doing so correctly handles I/O errors
// that happen after reading some bytes and also both of the
// allowed EOF behaviors.
//
// Implementations of Read are discouraged from returning a
// zero byte count with a nil error, except when len(p) == 0.
// Callers should treat a return of 0 and nil as indicating that
// nothing happened; in particular it does not indicate EOF.
//
// Implementations must not retain p.
type Reader interface {
	Read(p []byte) (n int, err error)
}

実装時は以下のような挙動が求められています。

  • 引数に与えられた p に、 len(p) バイトまで読み込む(pに書き込まれる)
  • 読み込んだバイト数とエラーを返す
    • 読み込んだバイト数は、 0 <= n <= len(p) 以下になる
  • 呼び出し元は、エラーで処理を振り分けるより先に、 n > 0 なのかを判別すること
    • EOF(error型)を返す場合があるので
  •  len(p) == 0 の場合を除いて、 n を0、errをnilで返すことは推奨されない
    • EOFの場合も nil を返さずにEOF(error型)を返す
  • 実装するときに p を保持してはいけない

LimitedReaderでReaderインターフェイスの実装を確認する

下部にReaderインターフェイスを実装したLimitedReader構造体とそれを利用するLimitReader関数が定義されています。
LimitReader関数は、LimitedReader構造体のポインタを返します。

// LimitReader returns a Reader that reads from r
// but stops with EOF after n bytes.
// The underlying implementation is a *LimitedReader.
func LimitReader(r Reader, n int64) Reader { return &LimitedReader{r, n} }

// A LimitedReader reads from R but limits the amount of
// data returned to just N bytes. Each call to Read
// updates N to reflect the new amount remaining.
// Read returns EOF when N <= 0 or when the underlying R returns EOF.
type LimitedReader struct {
	R Reader // underlying reader
	N int64  // max bytes remaining
}

LimitReader関数の引数には、Readerインターフェイスを実装している任意のReaderと読み込むバイト数を指定します。
LimitedReader構造体のReadメソッドもReaderインターフェイスの実装になっています。

func (l *LimitedReader) Read(p []byte) (n int, err error) {
    // Nが0以下の場合、0とEOF(error)を返す
	if l.N <= 0 {
		return 0, EOF
    }
    // pの大きさがl.Nより大きい場合は、pを0~Nまでに縮小する
	if int64(len(p)) > l.N {
		p = p[0:l.N]
    }
    // RにのReadメソッドにpを渡す
    n, err = l.R.Read(p)
    // 読み込んだバイト数だけNから引く
	l.N -= int64(n)
	return
}
 p の大きさを変更したのちに、LimitedReader構造体が持つReaderのReadメソッドに処理を移譲しているのがわかります。

個人的に気になったのは、 p の長さを比較する部分です。
 len はintを返す仕様ですが、intは処理系によって32ビットか64ビットになります。

LimitedReaderのようなデータを扱う構造体では、明示的にint64を指定しているので、 int64に変換して長さを比較しているのだと思います。

余談ですが、積極的に処理系に依存しない型を使っていってもいいのかというと・・・。

When you need an integer value you should use int unless you have a specific reason to use a sized or unsigned integer type.

「A Tour of Go」の中で、特別な理由がない限りはintを使えと書いてあります。

そして、最後にLimitedReaderから呼び出されるReadの実装として、stringsパッケージのReader構造体のReadメソッドを見てみます。

// A Reader implements the io.Reader, io.ReaderAt, io.Seeker, io.WriterTo,
// io.ByteScanner, and io.RuneScanner interfaces by reading
// from a string.
// The zero value for Reader operates like a Reader of an empty string.
type Reader struct {
	s        string
	i        int64 // current reading index
	prevRune int   // index of previous rune; or < 0
}

func (r *Reader) Read(b []byte) (n int, err error) {
	// 読み込んだインデックスが、stringの長さより大きいときにEOF(error)を返す
	if r.i >= int64(len(r.s)) {
		return 0, io.EOF
	}
	r.prevRune = -1
	// bにr.sのr.iから最後までをコピーする
	n = copy(b, r.s[r.i:])
	// r.iにコピーした要素数を加算する
	r.i += int64(n)
	return
}

builtinパッケージのcopy関数を呼んで、 b にコピーして読み込みを実装しています。
読み込み済みのインデックスとsの長さを比較しているので、Readの初回呼び出し以降は、 0, EOF を返すようになっています。

Readerを利用する側のコードを確認する

今度はReaderを利用している例として、bufferパッケージのReadFrom関数の実装を見てみます。

まずはこの関数が実装しているioパッケージのReaderFromインターフェイスを確認します。

// ReaderFrom is the interface that wraps the ReadFrom method.
//
// ReadFrom reads data from r until EOF or error.
// The return value n is the number of bytes read.
// Any error except io.EOF encountered during the read is also returned.
//
// The Copy function uses ReaderFrom if available.
type ReaderFrom interface {
	ReadFrom(r Reader) (n int64, err error)
}
  •  n で読み込んだバイト数を返す
  • errはEOFを除いてエラーを返す
    • EOFはエラーとして返さない
// ReadFrom reads data from r until EOF and appends it to the buffer, growing
// the buffer as needed. The return value n is the number of bytes read. Any
// error except io.EOF encountered during the read is also returned. If the
// buffer becomes too large, ReadFrom will panic with ErrTooLarge.
func (b *Buffer) ReadFrom(r io.Reader) (n int64, err error) {
	b.lastRead = opInvalid
	for {
		i := b.grow(MinRead)
		b.buf = b.buf[:i]
		m, e := r.Read(b.buf[i:cap(b.buf)])
		if m < 0 {
			panic(errNegativeRead)
		}

		b.buf = b.buf[:i+m]
		n += int64(m)
		if e == io.EOF {
			return n, nil // e is EOF, so return nil explicitly
		}
		if e != nil {
			return n, e
		}
	}
}

引数でReaderを受け取り、Readメソッドを呼び出しています。
呼び出した後は、先ほどのReaderインターフェイスに書いてあった通りに、まずmでバイト数を確認してします。
そのあとにエラーを確認して、エラーがEOFの場合は nil を、その他の場合はそのまま返すようになっています。
ReaderFromインターフェイスの仕様を意識した実装になっていることがわかります。

まとめ

今回はioパッケージのReaderインターフェイスを中心にコードを覗いてみました。
ほんの少しのコードリーディングでも学ぶものが多かったです。
どのようにインターフェイスを利用しているのか、いつint64を使うのか、EOFをどのように扱っているのかなど今後の実装の参考になりそうです。

この記事を書いた人

tkr2f
tkr2f事業開発部 web application engineer
2008年にアーティスへ入社。
システムエンジニアとして、SI案件のシステム開発に携わる。
その後、事業開発部の立ち上げから自社サービスの開発、保守をメインに従事。
ドメイン駆動設計(DDD)を中心にドメインを重視しながら、保守可能なソフトウェア開発を探求している。
この記事のカテゴリ

FOLLOW US

最新の情報をお届けします