[インデックス 15066] ファイルの概要
このコミットは、Go言語の標準ライブラリio
パッケージにおけるReadFull
およびReadAtLeast
関数の挙動を修正し、特定の条件下でのエラーハンドリングを改善するものです。具体的には、これらの関数が要求されたバイト数を完全に読み取った場合に、err == nil
(エラーなし)を保証するように変更されています。これにより、以前は曖昧だったAPIの挙動が明確化され、開発者がより予測可能なコードを書けるようになります。
コミット
commit 662ff5421287e3738587a9eb01fa50e080e48582
Author: Russ Cox <rsc@golang.org>
Date: Thu Jan 31 13:46:12 2013 -0800
io: guarantee err == nil for full reads in ReadFull and ReadAtLeast
This is a backwards compatible API change that fixes broken code.
In Go 1.0, ReadFull(r, buf) could return either len(buf), nil or len(buf), non-nil.
Most code expects only the former, so do that and document the guarantee.
Code that was correct before is still correct.
Code that was incorrect before, by assuming the guarantee, is now correct too.
The same applies to ReadAtLeast.
Fixes #4544.
R=golang-dev, bradfitz, minux.ma
CC=golang-dev
https://golang.org/cl/7235074
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/662ff5421287e3738587a9eb01fa50e080e48582
元コミット内容
このコミットの目的は、「ReadFull
およびReadAtLeast
関数において、完全な読み取りが成功した場合にerr == nil
を保証する」ことです。
Go 1.0では、ReadFull(r, buf)
がlen(buf), nil
(成功)またはlen(buf), non-nil
(成功したがエラーも発生)の両方を返す可能性がありました。しかし、ほとんどのコードは前者の挙動(成功したらエラーはnil
)を期待しており、この期待に反する挙動がバグの原因となることがありました。
この変更は後方互換性のあるAPI変更であり、既存の壊れたコードを修正します。以前から正しく書かれていたコードは引き続き正しく動作し、この保証を前提として誤って書かれていたコードも、この変更によって正しく動作するようになります。
同様の変更がReadAtLeast
にも適用されます。
この変更はIssue #4544を修正するものです。
変更の背景
Go 1.0のio.ReadFull
およびio.ReadAtLeast
関数は、指定されたバイト数を読み取った場合でも、同時に非nil
のエラー(例えばio.EOF
)を返す可能性がありました。これは、io.Reader
インターフェースのRead
メソッドが、読み取ったバイト数とエラーを同時に返すことができるという性質に起因します。
例えば、ReadFull
がバッファ全体を埋めるのに成功したが、その読み取り操作の直後にストリームの終端(EOF)に達した場合、Go 1.0ではlen(buf), io.EOF
のような戻り値が返される可能性がありました。多くの開発者は、関数が要求されたすべてのデータを正常に読み取った場合、エラーは発生しない(err == nil
)と直感的に期待します。この期待と実際の挙動の乖離が、アプリケーションコードにおけるバグや予期せぬ動作の原因となっていました。
このコミットは、このような開発者の直感とAPIの挙動のギャップを埋めることを目的としています。ReadFull
やReadAtLeast
が「完全な読み取り」に成功した場合には、エラーをnil
にすることで、APIのセマンティクスをより明確にし、開発者がより堅牢で予測可能なコードを記述できるようにします。これは、Go言語の「エラーは明示的に扱う」という哲学に沿いつつも、一般的なユースケースにおける利便性と安全性を向上させるための改善です。
前提知識の解説
このコミットを理解するためには、Go言語のio
パッケージにおける以下の基本的な概念と関数について理解しておく必要があります。
-
io.Reader
インターフェース: Go言語におけるデータの読み取り操作を抽象化する最も基本的なインターフェースです。type Reader interface { Read(p []byte) (n int, err error) }
Read
メソッドは、p
に最大len(p)
バイトのデータを読み込み、読み込んだバイト数n
とエラーerr
を返します。n > 0
の場合、p[:n]
にデータが読み込まれています。n == 0
の場合、データが読み込まれなかったことを意味します。これは、エラーが発生したか、またはEOFに達したことを示します。err == nil
の場合、エラーは発生していません。err != nil
の場合、エラーが発生しました。n > 0
とerr != nil
が同時に返されることもあります。これは、一部のデータが読み込まれたが、その後にエラーが発生したことを示します。この挙動が、本コミットの背景にある問題の根源でした。
-
io.EOF
:io.Reader
のRead
メソッドが、それ以上読み取るデータがないことを示すために返す特別なエラーです。通常、Read
が0, io.EOF
を返した場合、ストリームの終端に達したことを意味します。しかし、n > 0, io.EOF
が返されることもあり、これは「一部のデータを読み取ったが、その読み取りでストリームの終端に達した」ことを意味します。 -
io.ErrUnexpectedEOF
:io
パッケージの関数が、期待よりも早くEOFに達した場合に返すエラーです。例えば、ReadFull
が要求されたバイト数をすべて読み取る前にEOFに達した場合に返されます。 -
io.ErrShortBuffer
:io.ReadAtLeast
関数において、min
引数(最低限読み取るべきバイト数)が提供されたバッファのサイズlen(buf)
よりも大きい場合に返されるエラーです。 -
io.ReadAtLeast(r Reader, buf []byte, min int) (n int, err error)
:r
からbuf
にデータを読み込み、少なくともmin
バイトを読み取ることを試みます。n
は実際に読み込んだバイト数です。n >= min
であれば、成功とみなされます。len(buf) < min
の場合、ErrShortBuffer
を返します。min
バイトを読み取る前にEOFに達した場合、ErrUnexpectedEOF
を返します。- このコミット以前は、
n >= min
であってもerr != nil
が返される可能性がありました。
-
io.ReadFull(r Reader, buf []byte) (n int, err error)
:io.ReadAtLeast
の特殊なケースで、ReadAtLeast(r, buf, len(buf))
として実装されています。つまり、buf
全体を埋めるまでデータを読み取ることを試みます。n
は実際に読み込んだバイト数です。n == len(buf)
であれば、成功とみなされます。len(buf)
バイトを読み取る前にEOFに達した場合、ErrUnexpectedEOF
を返します。- このコミット以前は、
n == len(buf)
であってもerr != nil
が返される可能性がありました。
このコミットは、特にReadAtLeast
とReadFull
が「成功」とみなされる条件(それぞれn >= min
とn == len(buf)
)を満たした場合に、err
が必ずnil
になるように変更することで、APIのセマンティクスをより明確にし、開発者の期待に沿うようにします。
技術的詳細
このコミットの技術的な核心は、io.ReadAtLeast
関数のエラー処理ロジックの変更にあります。io.ReadFull
はio.ReadAtLeast
を内部的に呼び出しているため、ReadAtLeast
の変更がReadFull
にも影響します。
変更前のReadAtLeast
の関連部分のコードは以下のようでした(簡略化)。
func ReadAtLeast(r Reader, buf []byte, min int) (n int, err error) {
// ... (読み取りループ) ...
if err == EOF {
if n >= min {
err = nil // minバイト以上読み込めていればEOFをnilにする
} else if n > 0 {
err = ErrUnexpectedEOF // minバイト未満でEOFならErrUnexpectedEOF
}
}
return
}
このロジックの問題点は、r.Read()
がn > 0
とerr != nil
(例えばio.EOF
以外のエラー)を同時に返した場合に、そのerr
がif err == EOF
の条件に合致せず、そのまま返されてしまう可能性があったことです。特に、n >= min
が満たされているにもかかわらず、r.Read()
が返した非nil
のエラーがそのままReadAtLeast
の戻り値として伝播してしまうケースがありました。これは、開発者が「必要なバイト数を読み取れたならエラーはnil
であるべき」と期待する挙動と異なりました。
変更後のReadAtLeast
の関連部分のコードは以下のようになります。
func ReadAtLeast(r Reader, buf []byte, min int) (n int, err error) {
// ... (読み取りループ) ...
if n >= min { // 必要なバイト数を読み取れた場合
err = nil // エラーをnilに上書きする
} else if n > 0 && err == EOF { // 必要なバイト数に満たないが、一部読み込み後にEOFに達した場合
err = ErrUnexpectedEOF // ErrUnexpectedEOFを返す
}
return
}
この変更のポイントは以下の通りです。
-
n >= min
の場合のerr = nil
の強制: 最も重要な変更は、n >= min
(つまり、要求された最低限のバイト数を読み取れた場合)という条件が満たされたときに、いかなるエラーもnil
に上書きするという点です。これにより、io.Reader
の実装がn > 0
と非nil
のエラーを同時に返した場合でも、ReadAtLeast
としては「成功」とみなし、エラーを返さないという明確な保証が提供されます。これは、APIのセマンティクスを「成功した読み取りにはエラーがない」という直感的な期待に合わせるためのものです。 -
ErrUnexpectedEOF
の条件の明確化:n > 0 && err == EOF
という条件は、「一部のバイトは読み取れたが、min
バイトに満たない状態でEOFに達した」というケースを正確に捉えています。この場合にErrUnexpectedEOF
を返すことで、部分的な読み取りとEOFによる終了を明確に区別します。
この変更により、ReadAtLeast
およびReadFull
のドキュメントにも「On return, n >= min if and only if err == nil.
」(ReadAtLeast
の場合)および「On return, n == len(buf) if and only if err == nil.
」(ReadFull
の場合)という保証が明記されました。これは、これらの関数が成功した読み取りに対しては常にnil
エラーを返すという、明確な契約を開発者に提供します。
テストコードの変更も重要です。io_test.go
では、dataAndEOFBuffer
というヘルパー型がdataAndErrorBuffer
にリファクタリングされ、io.EOF
だけでなく任意のカスタムエラーをRead
メソッドが返すように拡張されました。これにより、ReadAtLeast
がn > 0
と非nil
エラーを同時に返すio.Reader
の実装に対して、新しいエラーハンドリングロジックが正しく機能するかどうかを検証するテストが追加されています。特にTestReadAtLeastWithDataAndError
は、io.EOF
以外のエラーがn >= min
の条件でnil
に上書きされることを確認しています。
コアとなるコードの変更箇所
このコミットにおけるコアとなるコードの変更は、主にsrc/pkg/io/io.go
内のReadAtLeast
関数と、それに関連するsrc/pkg/io/io_test.go
内のテストコードです。
src/pkg/io/io.go
--- a/src/pkg/io/io.go
+++ b/src/pkg/io/io.go
@@ -262,6 +262,7 @@ func WriteString(w Writer, s string) (n int, err error) {
// If an EOF happens after reading fewer than min bytes,
// ReadAtLeast returns ErrUnexpectedEOF.
// If min is greater than the length of buf, ReadAtLeast returns ErrShortBuffer.
+// On return, n >= min if and only if err == nil.
func ReadAtLeast(r Reader, buf []byte, min int) (n int, err error) {
if len(buf) < min {
return 0, ErrShortBuffer
@@ -271,12 +272,10 @@ func ReadAtLeast(r Reader, buf []byte, min int) (n int, err error) {
nn, err = r.Read(buf[n:])
n += nn
}
- if err == EOF {
- if n >= min {
- err = nil
- } else if n > 0 {
- err = ErrUnexpectedEOF
- }
+ if n >= min {
+ err = nil
+ } else if n > 0 && err == EOF {
+ err = ErrUnexpectedEOF
}
return
}
@@ -286,6 +285,7 @@ func ReadAtLeast(r Reader, buf []byte, min int) (n int, err error) {
// The error is EOF only if no bytes were read.
// If an EOF happens after reading some but not all the bytes,
// ReadFull returns ErrUnexpectedEOF.
+// On return, n == len(buf) if and only if err == nil.
func ReadFull(r Reader, buf []byte) (n int, err error) {
return ReadAtLeast(r, buf, len(buf))
}
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"
+ "fmt"
. "io"
"strings"
"testing"
@@ -120,22 +121,30 @@ func TestReadAtLeast(t *testing.T) {
testReadAtLeast(t, &rb)
}
-// A version of bytes.Buffer that returns n > 0, EOF on Read
+// A version of bytes.Buffer that returns n > 0, err on Read
// when the input is exhausted.
-type dataAndEOFBuffer struct {
+type dataAndErrorBuffer struct {
+ err error
bytes.Buffer
}
-func (r *dataAndEOFBuffer) Read(p []byte) (n int, err error) {
+func (r *dataAndErrorBuffer) Read(p []byte) (n int, err error) {
n, err = r.Buffer.Read(p)
if n > 0 && r.Buffer.Len() == 0 && err == nil {
- err = EOF
+ err = r.err
}
return
}
func TestReadAtLeastWithDataAndEOF(t *testing.T) {
- var rb dataAndEOFBuffer
+ var rb dataAndErrorBuffer
+ rb.err = EOF
+ testReadAtLeast(t, &rb)
+}
+
+func TestReadAtLeastWithDataAndError(t *testing.T) {
+ var rb dataAndErrorBuffer
+ rb.err = fmt.Errorf("fake error")
testReadAtLeast(t, &rb)
}
@@ -169,8 +178,12 @@ func testReadAtLeast(t *testing.T, rb ReadWriter) {
}
rb.Write([]byte("4"))
n, err = ReadAtLeast(rb, buf, 2)
- if err != ErrUnexpectedEOF {
- t.Errorf("expected ErrUnexpectedEOF, got %v", err)
+ want := ErrUnexpectedEOF
+ if rb, ok := rb.(*dataAndErrorBuffer); ok && rb.err != EOF {
+ want = rb.err
+ }
+ if err != want {
+ t.Errorf("expected %v, got %v", want, err)
}
if n != 1 {
t.Errorf("expected to have read 1 bytes, got %v", n)
コアとなるコードの解説
src/pkg/io/io.go
の変更
ReadAtLeast
関数のエラー処理ロジックが大幅に変更されています。
変更前:
if err == EOF {
if n >= min {
err = nil
} else if n > 0 {
err = ErrUnexpectedEOF
}
}
このロジックは、r.Read()
が返したエラーがEOF
である場合にのみ、そのエラーをnil
にしたりErrUnexpectedEOF
に変換したりしていました。しかし、r.Read()
がEOF
以外のエラー(例えばネットワークエラーなど)をn > 0
と共に返した場合、このif
ブロックは実行されず、元の非nil
エラーがそのまま返されてしまう可能性がありました。これは、n >= min
が満たされていてもエラーが返されるという、開発者の期待に反する挙動につながっていました。
変更後:
if n >= min {
err = nil
} else if n > 0 && err == EOF {
err = ErrUnexpectedEOF
}
この新しいロジックは、よりシンプルかつ堅牢です。
if n >= min { err = nil }
: この行が最も重要な変更です。読み込んだバイト数n
がmin
以上である場合(つまり、要求された最低限のバイト数を読み取れた場合)、err
変数を強制的にnil
に設定します。 これにより、io.Reader
の実装がn > 0
と非nil
エラーを同時に返した場合でも、ReadAtLeast
としては「成功」とみなし、エラーを返さないという明確な保証が提供されます。これは、APIのセマンティクスを「成功した読み取りにはエラーがない」という直感的な期待に合わせるためのものです。else if n > 0 && err == EOF { err = ErrUnexpectedEOF }
: この行は、min
バイトに満たないが、一部のバイトは読み込み済みで、かつEOF
に達した場合にErrUnexpectedEOF
を返すという、以前のロジックの意図を引き継いでいます。これは、部分的な読み取りとEOFによる終了を明確に区別するために重要です。
ReadFull
関数については、その実装がreturn ReadAtLeast(r, buf, len(buf))
であるため、ReadAtLeast
の変更が直接適用されます。また、両関数のコメントに「On return, n >= min if and only if err == nil.
」および「On return, n == len(buf) if and only if err == nil.
」という保証が追加され、APIの契約が明確化されました。
src/pkg/io/io_test.go
の変更
テストコードも、新しいエラーハンドリングロジックを検証するために更新されています。
-
dataAndEOFBuffer
からdataAndErrorBuffer
へのリファクタリング: 以前はEOF
のみを返すように設計されていたヘルパー構造体dataAndEOFBuffer
が、dataAndErrorBuffer
に名称変更され、任意のerror
を返すことができるようにerr error
フィールドが追加されました。これにより、io.EOF
以外のエラーがn > 0
と共に返されるシナリオをテストできるようになります。 -
TestReadAtLeastWithDataAndError
の追加: この新しいテスト関数は、dataAndErrorBuffer
を使用して、io.EOF
ではないカスタムエラー(例:fmt.Errorf("fake error")
)がRead
メソッドから返された場合に、ReadAtLeast
が正しく動作するかを検証します。特に、n >= min
の条件が満たされたときに、このカスタムエラーがnil
に上書きされることを確認します。 -
testReadAtLeast
内の期待エラーの動的な設定:testReadAtLeast
関数内で、期待されるエラーwant
が動的に設定されるようになりました。これは、dataAndErrorBuffer
がio.EOF
以外のエラーを返す場合、そのエラーがReadAtLeast
によってErrUnexpectedEOF
に変換されるのではなく、n >= min
の条件が満たされない限りそのまま伝播することをテストするためです。
これらの変更により、ReadAtLeast
およびReadFull
が、完全な読み取りが成功した場合には常にnil
エラーを返すという新しい保証が、テストによっても裏付けられるようになりました。
関連リンク
- Go言語
io
パッケージのドキュメント: https://pkg.go.dev/io - Go言語
io.Reader
インターフェース: https://pkg.go.dev/io#Reader - Go言語
io.ReadFull
関数: https://pkg.go.dev/io#ReadFull - Go言語
io.ReadAtLeast
関数: https://pkg.go.dev/io#ReadAtLeast - Go言語
io.EOF
変数: https://pkg.go.dev/io#EOF - Go言語
io.ErrUnexpectedEOF
変数: https://pkg.go.dev/io#ErrUnexpectedEOF - Go言語
io.ErrShortBuffer
変数: https://pkg.go.dev/io#ErrShortBuffer
参考にした情報源リンク
- Go Issue #4544: io: ReadFull and ReadAtLeast should guarantee err == nil for full reads: https://github.com/golang/go/issues/4544
- Go CL 7235074: io: guarantee err == nil for full reads in ReadFull and ReadAtLeast: https://golang.org/cl/7235074
- Go言語の公式ドキュメント (pkg.go.dev)
- Go言語のソースコード (github.com/golang/go)
- Go言語のエラーハンドリングに関する一般的な情報源