[インデックス 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