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

[インデックス 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の挙動のギャップを埋めることを目的としています。ReadFullReadAtLeastが「完全な読み取り」に成功した場合には、エラーをnilにすることで、APIのセマンティクスをより明確にし、開発者がより堅牢で予測可能なコードを記述できるようにします。これは、Go言語の「エラーは明示的に扱う」という哲学に沿いつつも、一般的なユースケースにおける利便性と安全性を向上させるための改善です。

前提知識の解説

このコミットを理解するためには、Go言語のioパッケージにおける以下の基本的な概念と関数について理解しておく必要があります。

  1. 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 > 0err != nilが同時に返されることもあります。これは、一部のデータが読み込まれたが、その後にエラーが発生したことを示します。この挙動が、本コミットの背景にある問題の根源でした。
  2. io.EOF: io.ReaderReadメソッドが、それ以上読み取るデータがないことを示すために返す特別なエラーです。通常、Read0, io.EOFを返した場合、ストリームの終端に達したことを意味します。しかし、n > 0, io.EOFが返されることもあり、これは「一部のデータを読み取ったが、その読み取りでストリームの終端に達した」ことを意味します。

  3. io.ErrUnexpectedEOF: ioパッケージの関数が、期待よりも早くEOFに達した場合に返すエラーです。例えば、ReadFullが要求されたバイト数をすべて読み取る前にEOFに達した場合に返されます。

  4. io.ErrShortBuffer: io.ReadAtLeast関数において、min引数(最低限読み取るべきバイト数)が提供されたバッファのサイズlen(buf)よりも大きい場合に返されるエラーです。

  5. 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が返される可能性がありました。
  6. 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が返される可能性がありました。

このコミットは、特にReadAtLeastReadFullが「成功」とみなされる条件(それぞれn >= minn == len(buf))を満たした場合に、errが必ずnilになるように変更することで、APIのセマンティクスをより明確にし、開発者の期待に沿うようにします。

技術的詳細

このコミットの技術的な核心は、io.ReadAtLeast関数のエラー処理ロジックの変更にあります。io.ReadFullio.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 > 0err != nil(例えばio.EOF以外のエラー)を同時に返した場合に、そのerrif 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
}

この変更のポイントは以下の通りです。

  1. n >= minの場合のerr = nilの強制: 最も重要な変更は、n >= min(つまり、要求された最低限のバイト数を読み取れた場合)という条件が満たされたときに、いかなるエラーもnilに上書きするという点です。これにより、io.Readerの実装がn > 0と非nilのエラーを同時に返した場合でも、ReadAtLeastとしては「成功」とみなし、エラーを返さないという明確な保証が提供されます。これは、APIのセマンティクスを「成功した読み取りにはエラーがない」という直感的な期待に合わせるためのものです。

  2. 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メソッドが返すように拡張されました。これにより、ReadAtLeastn > 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
	}

この新しいロジックは、よりシンプルかつ堅牢です。

  1. if n >= min { err = nil }: この行が最も重要な変更です。読み込んだバイト数nmin以上である場合(つまり、要求された最低限のバイト数を読み取れた場合)、err変数を強制的にnilに設定します。 これにより、io.Readerの実装がn > 0と非nilエラーを同時に返した場合でも、ReadAtLeastとしては「成功」とみなし、エラーを返さないという明確な保証が提供されます。これは、APIのセマンティクスを「成功した読み取りにはエラーがない」という直感的な期待に合わせるためのものです。
  2. 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 の変更

テストコードも、新しいエラーハンドリングロジックを検証するために更新されています。

  1. dataAndEOFBufferからdataAndErrorBufferへのリファクタリング: 以前はEOFのみを返すように設計されていたヘルパー構造体dataAndEOFBufferが、dataAndErrorBufferに名称変更され、任意のerrorを返すことができるようにerr errorフィールドが追加されました。これにより、io.EOF以外のエラーがn > 0と共に返されるシナリオをテストできるようになります。

  2. TestReadAtLeastWithDataAndErrorの追加: この新しいテスト関数は、dataAndErrorBufferを使用して、io.EOFではないカスタムエラー(例: fmt.Errorf("fake error"))がReadメソッドから返された場合に、ReadAtLeastが正しく動作するかを検証します。特に、n >= minの条件が満たされたときに、このカスタムエラーがnilに上書きされることを確認します。

  3. testReadAtLeast内の期待エラーの動的な設定: testReadAtLeast関数内で、期待されるエラーwantが動的に設定されるようになりました。これは、dataAndErrorBufferio.EOF以外のエラーを返す場合、そのエラーがReadAtLeastによってErrUnexpectedEOFに変換されるのではなく、n >= minの条件が満たされない限りそのまま伝播することをテストするためです。

これらの変更により、ReadAtLeastおよびReadFullが、完全な読み取りが成功した場合には常にnilエラーを返すという新しい保証が、テストによっても裏付けられるようになりました。

関連リンク

参考にした情報源リンク

  • 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言語のエラーハンドリングに関する一般的な情報源