[インデックス 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.Reader
とio.Writer
は基本的なインターフェースであり、io.Copy
関数はこれらを利用して効率的にデータをコピーします。
io.WriterTo
インターフェースは、WriteTo(w io.Writer) (n int64, err error)
というメソッドを定義しており、これは自身の内容をio.Writer
に書き込む責任を持つ型が実装します。このメソッドの規約として、書き込みが成功し、エラーが発生しなかった場合は、書き込んだバイト数とnil
エラーを返すことが期待されます。もし、書き込むべきデータが全くない場合(例えば、空のReader
から読み取る場合)は、0バイトを書き込み、エラーは発生しないため、(0, nil)
を返すのが正しい挙動です。
しかし、このコミット以前のbytes.Reader
とstrings.Reader
のWriteTo
メソッドは、内部のデータが空である場合(つまり、読み取るべきバイトが残っていない場合)に、(0, io.EOF)
を返していました。これは、io.Copy
などの関数がio.EOF
を「ストリームの終端に達した」というシグナルとして解釈するため、空のリーダーからのコピーが正しく完了したと認識されない可能性がありました。特に、io.Copy
はio.EOF
をエラーとして扱わず、コピーが完了したことを示すために使用しますが、WriteTo
がio.EOF
を返した場合、io.Copy
は内部でそのio.EOF
を処理し、最終的にnil
を返すことが期待されます。しかし、WriteTo
がio.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
へデータをコピーする関数です。src
がio.WriterTo
を実装している場合、src.WriteTo(dst)
を呼び出します。dst
がio.ReaderFrom
を実装している場合、dst.ReadFrom(src)
を呼び出します。それ以外の場合は、内部でバッファを使ってsrc.Read
とdst.Write
を繰り返します。 -
io.EOF
:io
パッケージで定義されているエラー変数で、入力の終端に達したことを示します。通常、Read
メソッドがこれ以上読み取るデータがない場合に返されます。
技術的詳細
このコミットの核心は、io.WriterTo
インターフェースのセマンティクスにあります。WriteTo
メソッドは、書き込み操作の結果としてエラーが発生した場合にのみ非nil
のエラーを返す必要があります。もし書き込むべきデータが全くなく、かつ書き込み処理自体に問題がない場合、それは成功した操作と見なされ、書き込んだバイト数0とnil
エラーを返すのが正しい挙動です。
以前の実装では、bytes.Reader
とstrings.Reader
のWriteTo
メソッドは、内部の読み取り位置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.Copy
がio.WriterTo
を呼び出す際に、io.EOF
をエラーとしてではなく、コピーが完了したことを示すシグナルとして扱うというio.Copy
の内部ロジックと衝突する可能性がありました。io.Copy
は、Read
メソッドがio.EOF
を返した場合にコピーを終了し、最終的にnil
エラーを返します。しかし、WriteTo
がio.EOF
を返すと、io.Copy
はそれをエラーとして処理してしまうか、あるいはio.Copy
の呼び出し元がio.EOF
を予期しないエラーとして受け取ってしまう可能性がありました。
修正後のコードでは、このreturn 0, io.EOF
がreturn 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つのファイルに集中しています。
src/pkg/bytes/reader.go
src/pkg/strings/reader.go
それぞれのファイルで、WriteTo
メソッド内の以下の行が変更されました。
変更前:
return 0, io.EOF
変更後:
return 0, nil
また、テストファイルも変更されています。
-
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
インターフェースのみを実装している場合)で、戻り値のn
とerr
が一貫していることを検証します。
-
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
の挙動を比較しています。
NewReader(nil)
:bytes.Reader
はio.WriterTo
を実装しているため、io.Copy
は内部でbytes.Reader.WriteTo
を呼び出します。justReader{NewReader(nil)}
:justReader
はio.Reader
インターフェースのみを実装しており、io.WriterTo
は実装していません。この場合、io.Copy
はio.Reader
のRead
メソッドを繰り返し呼び出す一般的なコピーロジックを使用します。
このテストの目的は、空のリーダーからコピーする場合に、io.WriterTo
の実装の有無にかかわらず、io.Copy
が同じ結果((0, nil)
)を返すことを保証することです。これは、WriteTo
が(0, nil)
を返すように修正されたことで初めて可能になります。もしWriteTo
が(0, io.EOF)
を返していた場合、with
とwithOut
の結果が異なってしまい、テストは失敗するでしょう。
関連リンク
- Go言語の
io
パッケージのドキュメント: https://pkg.go.dev/io - Go言語の
bytes
パッケージのドキュメント: https://pkg.go.dev/bytes - Go言語の
strings
パッケージのドキュメント: https://pkg.go.dev/strings
参考にした情報源リンク
- Go言語のソースコード (GitHub): https://github.com/golang/go
- Go言語の
io.WriterTo
インターフェースの仕様に関する議論 (GoコミュニティのメーリングリストやIssueトラッカーなど、当時の情報源を想定)- 残念ながら、
Fixes #4421
で示されたIssueは、現在のGoのIssueトラッカーでは見つかりませんでした。これは、GoのIssueトラッカーが移行されたり、古いIssueがアーカイブされたりしたためと考えられます。しかし、このコミットメッセージとコード変更自体が、当時の問題と解決策を明確に示しています。 - 一般的な
io.WriterTo
の規約については、Goの公式ドキュメントやGoコミュニティでの議論で確認できます。
- 残念ながら、