Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

[インデックス 14470] ファイルの概要

このコミットは、Go言語の標準ライブラリであるbytesパッケージとstringsパッケージ内のReader型におけるWriteToメソッドの挙動を修正するものです。具体的には、読み取るべきデータが0バイトの場合にWriteToメソッドが返す戻り値が、io.EOFではなくnilとなるように変更されました。これにより、io.Copyなどの操作において、空のリーダーからのコピーが期待通りに動作するようになります。

コミット

commit c8fa7dcc25cc7655abf55b541149ad248c9830f2
Author: Brad Fitzpatrick <bradfitz@golang.org>
Date:   Sun Nov 25 09:04:13 2012 -0800

    bytes, strings: fix Reader WriteTo return value on 0 bytes copied
    
    Fixes #4421
    
    R=golang-dev, dave, minux.ma, mchaten, rsc
    CC=golang-dev
    https://golang.org/cl/6855083

GitHub上でのコミットページへのリンク

https://github.com/golang/go/commit/c8fa7dcc25cc7655abf55b541149ad248c9830f2

元コミット内容

bytes, strings: fix Reader WriteTo return value on 0 bytes copied Fixes #4421

このコミットは、bytesおよびstringsパッケージのReader型が持つWriteToメソッドが、コピーされるバイト数が0の場合に誤った戻り値を返すバグを修正します。具体的には、io.EOFではなくnilを返すように変更されました。

変更の背景

Go言語のioパッケージには、データの読み書きを抽象化するためのインターフェースが定義されています。特にio.Readerio.Writerは基本的なインターフェースであり、io.Copy関数はこれらを利用して効率的にデータをコピーします。

io.WriterToインターフェースは、WriteTo(w io.Writer) (n int64, err error)というメソッドを定義しており、これは自身の内容をio.Writerに書き込む責任を持つ型が実装します。このメソッドの規約として、書き込みが成功し、エラーが発生しなかった場合は、書き込んだバイト数とnilエラーを返すことが期待されます。もし、書き込むべきデータが全くない場合(例えば、空のReaderから読み取る場合)は、0バイトを書き込み、エラーは発生しないため、(0, nil)を返すのが正しい挙動です。

しかし、このコミット以前のbytes.Readerstrings.ReaderWriteToメソッドは、内部のデータが空である場合(つまり、読み取るべきバイトが残っていない場合)に、(0, io.EOF)を返していました。これは、io.Copyなどの関数がio.EOFを「ストリームの終端に達した」というシグナルとして解釈するため、空のリーダーからのコピーが正しく完了したと認識されない可能性がありました。特に、io.Copyio.EOFをエラーとして扱わず、コピーが完了したことを示すために使用しますが、WriteToio.EOFを返した場合、io.Copyは内部でそのio.EOFを処理し、最終的にnilを返すことが期待されます。しかし、WriteToio.EOFを返してしまうと、io.Copyの内部ロジックによっては、予期せぬ挙動を引き起こす可能性がありました。

この不整合を修正し、io.WriterToの規約に厳密に従うことで、より堅牢で予測可能なI/O操作を実現するためにこの変更が行われました。

前提知識の解説

  • io.Readerインターフェース: Read(p []byte) (n int, err error)メソッドを持つインターフェースです。データをバイトスライスpに読み込み、読み込んだバイト数nとエラーerrを返します。データの終端に達した場合はio.EOFを返します。

  • io.Writerインターフェース: Write(p []byte) (n int, err error)メソッドを持つインターフェースです。バイトスライスpのデータを書き込み、書き込んだバイト数nとエラーerrを返します。

  • io.WriterToインターフェース: WriteTo(w io.Writer) (n int64, err error)メソッドを持つインターフェースです。このインターフェースを実装する型は、自身の内容をwに書き込む責任を持ちます。io.Copy関数は、ソースがio.WriterToを実装している場合、効率のためにこのメソッドを優先的に使用します。

  • io.Copy(dst io.Writer, src io.Reader) (written int64, err error): srcからdstへデータをコピーする関数です。srcio.WriterToを実装している場合、src.WriteTo(dst)を呼び出します。dstio.ReaderFromを実装している場合、dst.ReadFrom(src)を呼び出します。それ以外の場合は、内部でバッファを使ってsrc.Readdst.Writeを繰り返します。

  • io.EOF: ioパッケージで定義されているエラー変数で、入力の終端に達したことを示します。通常、Readメソッドがこれ以上読み取るデータがない場合に返されます。

技術的詳細

このコミットの核心は、io.WriterToインターフェースのセマンティクスにあります。WriteToメソッドは、書き込み操作の結果としてエラーが発生した場合にのみ非nilのエラーを返す必要があります。もし書き込むべきデータが全くなく、かつ書き込み処理自体に問題がない場合、それは成功した操作と見なされ、書き込んだバイト数0とnilエラーを返すのが正しい挙動です。

以前の実装では、bytes.Readerstrings.ReaderWriteToメソッドは、内部の読み取り位置r.iがデータの長さlen(r.s)以上になった場合、つまり読み取るべきデータが残っていない場合に、以下のようにio.EOFを返していました。

// 変更前 (bytes/reader.go および strings/reader.go)
func (r *Reader) WriteTo(w io.Writer) (n int64, err error) {
	r.prevRune = -1
	if r.i >= len(r.s) {
		return 0, io.EOF // ここが問題
	}
	// ... 実際の書き込み処理 ...
}

この挙動は、io.Copyio.WriterToを呼び出す際に、io.EOFをエラーとしてではなく、コピーが完了したことを示すシグナルとして扱うというio.Copyの内部ロジックと衝突する可能性がありました。io.Copyは、Readメソッドがio.EOFを返した場合にコピーを終了し、最終的にnilエラーを返します。しかし、WriteToio.EOFを返すと、io.Copyはそれをエラーとして処理してしまうか、あるいはio.Copyの呼び出し元がio.EOFを予期しないエラーとして受け取ってしまう可能性がありました。

修正後のコードでは、このreturn 0, io.EOFreturn 0, nilに変更されました。

// 変更後 (bytes/reader.go および strings/reader.go)
func (r *Reader) WriteTo(w io.Writer) (n int64, err error) {
	r.prevRune = -1
	if r.i >= len(r.s) {
		return 0, nil // 修正点
	}
	// ... 実際の書き込み処理 ...
}

この変更により、空のReaderからWriteToを呼び出した場合でも、エラーは発生せず、0バイトが書き込まれたことを示す(0, nil)が返されるようになります。これはio.WriterToの規約に合致し、io.Copyなどの上位のI/O関数がより予測可能に動作することを保証します。

テストコードもこの変更に合わせて更新されました。特にbytes/reader_test.goにはTestReaderCopyNothingという新しいテストが追加され、空のリーダーからio.Copyを実行した場合の挙動が、WriteToメソッドの有無にかかわらず一貫していることを検証しています。

コアとなるコードの変更箇所

このコミットによる主要なコード変更は、以下の2つのファイルに集中しています。

  1. src/pkg/bytes/reader.go
  2. src/pkg/strings/reader.go

それぞれのファイルで、WriteToメソッド内の以下の行が変更されました。

変更前:

		return 0, io.EOF

変更後:

		return 0, nil

また、テストファイルも変更されています。

  1. src/pkg/bytes/reader_test.go

    • TestReaderWriteTo関数内のループ条件がi := 3; i < 30; i += 3からi := 0; i < 30; i += 3に変更され、長さ0のケースもテストされるようになりました。
    • エラーメッセージがより詳細になりました。
    • TestReaderCopyNothingという新しいテスト関数が追加されました。このテストは、io.Copyが空のReaderからコピーを行う際に、WriteToメソッドが実装されている場合とそうでない場合(io.Readerインターフェースのみを実装している場合)で、戻り値のnerrが一貫していることを検証します。
  2. src/pkg/strings/reader_test.go

    • TestWriteTo関数内のループ条件がi := 0; i < len(str); i++からi := 0; i <= len(str); i++に変更され、長さ0のケースもテストされるようになりました。
    • エラーメッセージがより詳細になりました。

コアとなるコードの解説

src/pkg/bytes/reader.go および src/pkg/strings/reader.go の変更

Reader構造体は、バイトスライス(bytes.Readerの場合)または文字列(strings.Readerの場合)を読み取り可能なデータソースとして扱います。WriteToメソッドは、このReaderの内容をio.Writerに書き出すためのものです。

変更されたコードブロックは以下の部分です。

func (r *Reader) WriteTo(w io.Writer) (n int64, err error) {
	r.prevRune = -1 // ルーンのキャッシュを無効化
	if r.i >= len(r.s) { // 現在の読み取り位置がデータの長さを超えている場合(つまり、読み取るデータがない場合)
		return 0, nil // 0バイトを書き込み、エラーなしを返す
	}
	// ... (以下、実際のデータ書き込みロジック) ...
}
  • r.i >= len(r.s): これは、Readerが既にデータの終端に達しているか、あるいは最初から空のデータで初期化されているかを確認する条件です。
  • return 0, nil: この行が修正の核心です。以前はreturn 0, io.EOFとなっていました。io.WriterToの規約では、書き込みが成功し、エラーがない場合はnilを返すことになっています。読み取るべきデータが0バイトであることは、エラーではなく、単に書き込むデータがないという正常な状態です。したがって、nilを返すのが正しい挙動となります。

src/pkg/bytes/reader_test.go の変更

TestReaderWriteToの変更は、ループの開始条件をi = 0にすることで、長さ0のバイトスライス(空のリーダー)に対するWriteToの挙動もテスト対象に含めるようにしました。

最も重要な追加はTestReaderCopyNothingです。

// verify that copying from an empty reader always has the same results,
// regardless of the presence of a WriteTo method.
func TestReaderCopyNothing(t *testing.T) {
	type nErr struct {
		n   int64
		err error
	}
	type justReader struct {
		io.Reader
	}
	type justWriter struct {
		io.Writer
	}
	discard := justWriter{ioutil.Discard} // hide ReadFrom

	var with, withOut nErr
	// NewReader(nil) は空のbytes.Readerを作成
	// io.Copy は src が io.WriterTo を実装している場合、WriteTo を優先的に呼び出す
	with.n, with.err = io.Copy(discard, NewReader(nil))
	// justReader は io.Reader インターフェースのみを実装し、WriteTo は実装しない
	// io.Copy は Read メソッドを繰り返し呼び出すフォールバックパスを使用する
	withOut.n, withOut.err = io.Copy(discard, justReader{NewReader(nil)})
	if with != withOut {
		t.Errorf("behavior differs: with = %#v; without: %#v", with, withOut)
	}
}

このテストは、以下の2つのシナリオでio.Copyの挙動を比較しています。

  1. NewReader(nil): bytes.Readerio.WriterToを実装しているため、io.Copyは内部でbytes.Reader.WriteToを呼び出します。
  2. justReader{NewReader(nil)}: justReaderio.Readerインターフェースのみを実装しており、io.WriterToは実装していません。この場合、io.Copyio.ReaderReadメソッドを繰り返し呼び出す一般的なコピーロジックを使用します。

このテストの目的は、空のリーダーからコピーする場合に、io.WriterToの実装の有無にかかわらず、io.Copyが同じ結果((0, nil))を返すことを保証することです。これは、WriteTo(0, nil)を返すように修正されたことで初めて可能になります。もしWriteTo(0, io.EOF)を返していた場合、withwithOutの結果が異なってしまい、テストは失敗するでしょう。

関連リンク

参考にした情報源リンク

  • Go言語のソースコード (GitHub): https://github.com/golang/go
  • Go言語のio.WriterToインターフェースの仕様に関する議論 (GoコミュニティのメーリングリストやIssueトラッカーなど、当時の情報源を想定)
    • 残念ながら、Fixes #4421で示されたIssueは、現在のGoのIssueトラッカーでは見つかりませんでした。これは、GoのIssueトラッカーが移行されたり、古いIssueがアーカイブされたりしたためと考えられます。しかし、このコミットメッセージとコード変更自体が、当時の問題と解決策を明確に示しています。
    • 一般的なio.WriterToの規約については、Goの公式ドキュメントやGoコミュニティでの議論で確認できます。