[インデックス 15226] ファイルの概要
このコミットは、Go言語の標準ライブラリioパッケージ内のCopyN関数の戻り値の振る舞いを文書化し、テストを強化するものです。具体的には、CopyNが要求されたバイト数nを正確にコピーできた場合に、エラーがnilであることを保証する変更が加えられています。
コミット
commit d6331b447fea50eca7ea6bd06370d0e028bdfdbf
Author: Brad Fitzpatrick <bradfitz@golang.org>
Date: Wed Feb 13 13:52:00 2013 -0800
io: document and test new CopyN return behavior
Changed accidentally in 28966b7b2f0c (CopyN using Copy).
Updating docs to be consistent with 29bf5ff5064e (ReadFull & ReadAtLeast)
R=rsc
CC=golang-dev
https://golang.org/cl/7314069
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/d6331b447fea50eca7ea6bd06370d0e028bdfdbf
元コミット内容
このコミットは、io.CopyN関数のドキュメントを更新し、その実装に小さな修正を加え、さらに新しいテストケースを追加しています。
変更されたファイル:
src/pkg/io/io.go:CopyN関数のドキュメントと実装の修正。src/pkg/io/io_test.go:CopyNの新しい振る舞いを検証するためのテストケースの追加。
変更の背景
このコミットの背景には、io.CopyN関数の以前の変更(コミット28966b7b2f0cでCopyNがCopyを使用するように変更された際)によって、その戻り値の振る舞いが意図せず変更されてしまったという経緯があります。特に、CopyNが要求されたバイト数nを正確にコピーできた場合でも、基になるReaderが同時にEOFなどのエラーを返した場合に、CopyNもエラーを返してしまう可能性がありました。
Goのioパッケージにおける慣習として、ReadFullやReadAtLeastといった関数は、「要求された量のデータが完全に読み取れた場合、エラーはnilであるべき」という原則に従っています。たとえ読み取り元がその読み取りでEOFを返したとしても、要求されたバイト数が満たされていれば、それは成功とみなされます。このコミットは、CopyNもこの慣習に沿った振る舞いをするように修正し、その振る舞いを明確に文書化することを目的としています。
前提知識の解説
このコミットを理解するためには、以下のGo言語のioパッケージに関する基本的な知識が必要です。
io.Readerインターフェース: データを読み取るためのインターフェースで、Read(p []byte) (n int, err error)メソッドを持ちます。nは読み取ったバイト数、errは発生したエラーです。errがio.EOFの場合、データの終端に達したことを示します。io.Writerインターフェース: データを書き込むためのインターフェースで、Write(p []byte) (n int, err error)メソッドを持ちます。nは書き込んだバイト数、errは発生したエラーです。io.Copy(dst Writer, src Reader) (written int64, err error):srcからdstへデータをコピーする関数です。srcがEOFを返すか、エラーが発生するまでコピーを続けます。io.LimitReader(r Reader, n int64) Reader: 指定されたバイト数nまでしか読み取らないReaderを返します。nバイト読み取ると、それ以降のRead呼び出しはEOFを返します。io.EOF:ioパッケージで定義されているエラーで、入力の終端(End Of File)に達したことを示します。io.ReadFull(r Reader, buf []byte) (n int, err error):bufが完全に満たされるまでrから読み取ろうとします。len(buf)バイトを正確に読み取れた場合、errはnilになります。それより少ないバイト数しか読み取れなかった場合はエラーを返します。io.ReadAtLeast(r Reader, buf []byte, min int) (n int, err error):minバイト以上を読み取ろうとします。minバイト以上を読み取れた場合、errはnilになります。
これらの関数は、Goのioパッケージにおけるエラーハンドリングの慣習、特に「要求された操作が完全に成功した場合、エラーはnilであるべき」という原則を体現しています。
技術的詳細
このコミットの技術的な核心は、io.CopyN関数の戻り値のセマンティクスを、io.ReadFullやio.ReadAtLeastといった他のioパッケージの関数と一貫させることにあります。
CopyN(dst Writer, src Reader, n int64) (written int64, err error)は、srcからdstへ最大nバイトをコピーする関数です。
変更前のCopyNは、内部でio.Copy(dst, io.LimitReader(src, n))を呼び出していました。io.LimitReaderは、指定されたバイト数nを読み取ると、それ以降のRead呼び出しでio.EOFを返します。しかし、srcがちょうどnバイトを読み取った時点でEOFを返した場合、io.Copyはwritten = nとerr = io.EOFを返す可能性がありました。
Goのioパッケージの慣習では、要求されたバイト数(この場合はnバイト)が完全に処理された場合、エラーはnilであるべきです。io.EOFは、それ以上データがないことを示すエラーであり、要求された量が満たされた場合には、成功とみなされるべきです。
このコミットでは、この慣習に合わせるために、CopyNの内部に以下のロジックが追加されました。
written, err = Copy(dst, LimitReader(src, n))
if written == n {
return n, nil
}
このif文は、Copyがnバイトを正確にコピーできた場合、たとえCopyがio.EOFなどのエラーを返していたとしても、CopyNとしてはnバイトコピー成功、エラーnilとして返すことを保証します。これにより、CopyNの振る舞いがReadFullやReadAtLeastと一貫し、「written == nであるのはerr == nilである場合のみ」という新しいドキュメントの記述と合致するようになります。
また、この変更を検証するために、io_test.goにwantedAndErrReaderというカスタムReaderが追加されました。このReaderは、Readメソッドが常に要求されたバイト数を返しつつ、同時に非nilのエラーを返すように実装されています。これにより、CopyNがnバイトをコピーしつつ、基になるReaderからエラーを受け取った場合の挙動を正確にテストできるようになりました。
コアとなるコードの変更箇所
src/pkg/io/io.go
--- a/src/pkg/io/io.go
+++ b/src/pkg/io/io.go
@@ -292,14 +292,16 @@ func ReadFull(r Reader, buf []byte) (n int, err error) {
// CopyN copies n bytes (or until an error) from src to dst.
// It returns the number of bytes copied and the earliest
-// error encountered while copying. Because Read can
-// return the full amount requested as well as an error
-// (including EOF), so can CopyN.
+// error encountered while copying.
+// On return, written == n if and only if err == nil.
//
// If dst implements the ReaderFrom interface,
// the copy is implemented using it.
func CopyN(dst Writer, src Reader, n int64) (written int64, err error) {
written, err = Copy(dst, LimitReader(src, n))
+ if written == n {
+ return n, nil
+ }
if written < n && err == nil {
// src stopped early; must have been EOF.
err = EOF
src/pkg/io/io_test.go
--- a/src/pkg/io/io_test.go
+++ b/src/pkg/io/io_test.go
@@ -6,6 +6,7 @@ package io_test
import (
"bytes"
+ "errors"
"fmt"
. "io"
"strings"
@@ -89,6 +90,12 @@ func (w *noReadFrom) Write(p []byte) (n int, err int) {
return w.w.Write(p)
}
+type wantedAndErrReader struct{}
+
+func (wantedAndErrReader) Read(p []byte) (int, error) {
+ return len(p), errors.New("wantedAndErrReader error")
+}
+
func TestCopyNEOF(t *testing.T) {
// Test that EOF behavior is the same regardless of whether
// argument to CopyN has ReadFrom.
@@ -114,6 +121,16 @@ func TestCopyNEOF(t *testing.T) {
if n != 3 || err != EOF {
t.Errorf("CopyN(bytes.Buffer, foo, 4) = %d, %v; want 3, EOF", n, err)
}
+
+ n, err = CopyN(b, wantedAndErrReader{}, 5)
+ if n != 5 || err != nil {
+ t.Errorf("CopyN(bytes.Buffer, wantedAndErrReader, 5) = %d, %v; want 5, nil", n, err)
+ }
+
+ n, err = CopyN(&noReadFrom{b}, wantedAndErrReader{}, 5)
+ if n != 5 || err != nil {
+ t.Errorf("CopyN(noReadFrom, wantedAndErrReader, 5) = %d, %v; want 5, nil", n, err)
+ }
}
func TestReadAtLeast(t *testing.T) {
コアとなるコードの解説
src/pkg/io/io.go の変更
-
ドキュメントの更新:
CopyN関数のコメントにOn return, written == n if and only if err == nil.という一文が追加されました。これは、「戻り値としてwrittenがnと等しいのは、errがnilである場合のみである」という、この関数の新しい(そして意図された)振る舞いを明確に示しています。これにより、ユーザーはCopyNがnバイトをコピーできた場合にエラーをチェックする必要がないことを理解できます。 -
実装の修正:
written, err = Copy(dst, LimitReader(src, n))の直後に、以下の条件分岐が追加されました。if written == n { return n, nil }このコードは、
Copy関数がnバイトを正常にコピーできた場合(written == n)、CopyNは常にnとnilエラーを返すことを保証します。これにより、基になるLimitReaderやsrcがnバイト読み取った直後にEOFを返したとしても、CopyNは成功として扱われます。これは、ReadFullやReadAtLeastといった他のioパッケージの関数が、要求された量のデータが完全に読み取れた場合にnilエラーを返すというGoの慣習に沿ったものです。
src/pkg/io/io_test.go の変更
-
wantedAndErrReader構造体の追加:type wantedAndErrReader struct{} func (wantedAndErrReader) Read(p []byte) (int, error) { return len(p), errors.New("wantedAndErrReader error") }この新しい型は、
io.Readerインターフェースを実装しています。そのReadメソッドは、常に要求されたバッファの長さ(len(p))を読み取ったバイト数として返し、同時にカスタムエラー文字列"wantedAndErrReader error"を持つ非nilのエラーを返します。この特殊なReaderは、CopyNがnバイトをコピーしつつ、基になるRead操作からエラーを受け取るという、まさにこのコミットで修正されたエッジケースをテストするために設計されました。 -
TestCopyNEOFテスト関数の拡張: 既存のTestCopyNEOF関数に、wantedAndErrReaderを使用した新しいテストケースが追加されました。n, err = CopyN(b, wantedAndErrReader{}, 5) if n != 5 || err != nil { t.Errorf("CopyN(bytes.Buffer, wantedAndErrReader, 5) = %d, %v; want 5, nil", n, err) } n, err = CopyN(&noReadFrom{b}, wantedAndErrReader{}, 5) if n != 5 || err != nil { t.Errorf("CopyN(noReadFrom, wantedAndErrReader, 5) = %d, %v; want 5, nil", n, err) }これらのテストケースは、
CopyNがwantedAndErrReaderから5バイトをコピーしようとしたときに、writtenが5でerrがnilになることを期待しています。これは、io.goで追加されたif written == n { return n, nil }というロジックが正しく機能していることを検証します。noReadFromラッパーを使用しているのは、dstがio.ReaderFromインターフェースを実装しているかどうかにかかわらず、この振る舞いが一貫していることを確認するためです。
これらの変更により、io.CopyNはより予測可能で、Goのioパッケージの他の関数と一貫性のある振る舞いをするようになり、そのセマンティクスがドキュメントによって明確にされました。
関連リンク
- Go言語の
ioパッケージのドキュメント: https://pkg.go.dev/io - Goのコードレビューシステム (Gerrit) の変更リスト: https://golang.org/cl/7314069
参考にした情報源リンク
- コミットメッセージに記載されている関連コミット:
28966b7b2f0c(CopyN using Copy): このコミットによってCopyNの振る舞いが意図せず変更されたとされています。29bf5ff5064e(ReadFull & ReadAtLeast):CopyNのドキュメントがこれらの関数と一貫するように更新されたとされています。
- Go言語の
ioパッケージのソースコード: https://github.com/golang/go/tree/master/src/io - Go言語の
errorsパッケージのドキュメント: https://pkg.go.dev/errors