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

[インデックス 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関数の以前の変更(コミット28966b7b2f0cCopyNCopyを使用するように変更された際)によって、その戻り値の振る舞いが意図せず変更されてしまったという経緯があります。特に、CopyNが要求されたバイト数nを正確にコピーできた場合でも、基になるReaderが同時にEOFなどのエラーを返した場合に、CopyNもエラーを返してしまう可能性がありました。

Goのioパッケージにおける慣習として、ReadFullReadAtLeastといった関数は、「要求された量のデータが完全に読み取れた場合、エラーはnilであるべき」という原則に従っています。たとえ読み取り元がその読み取りでEOFを返したとしても、要求されたバイト数が満たされていれば、それは成功とみなされます。このコミットは、CopyNもこの慣習に沿った振る舞いをするように修正し、その振る舞いを明確に文書化することを目的としています。

前提知識の解説

このコミットを理解するためには、以下のGo言語のioパッケージに関する基本的な知識が必要です。

  • io.Readerインターフェース: データを読み取るためのインターフェースで、Read(p []byte) (n int, err error)メソッドを持ちます。nは読み取ったバイト数、errは発生したエラーです。errio.EOFの場合、データの終端に達したことを示します。
  • io.Writerインターフェース: データを書き込むためのインターフェースで、Write(p []byte) (n int, err error)メソッドを持ちます。nは書き込んだバイト数、errは発生したエラーです。
  • io.Copy(dst Writer, src Reader) (written int64, err error): srcからdstへデータをコピーする関数です。srcEOFを返すか、エラーが発生するまでコピーを続けます。
  • 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)バイトを正確に読み取れた場合、errnilになります。それより少ないバイト数しか読み取れなかった場合はエラーを返します。
  • io.ReadAtLeast(r Reader, buf []byte, min int) (n int, err error): minバイト以上を読み取ろうとします。minバイト以上を読み取れた場合、errnilになります。

これらの関数は、Goのioパッケージにおけるエラーハンドリングの慣習、特に「要求された操作が完全に成功した場合、エラーはnilであるべき」という原則を体現しています。

技術的詳細

このコミットの技術的な核心は、io.CopyN関数の戻り値のセマンティクスを、io.ReadFullio.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.Copywritten = nerr = io.EOFを返す可能性がありました。

Goのioパッケージの慣習では、要求されたバイト数(この場合はnバイト)が完全に処理された場合、エラーはnilであるべきです。io.EOFは、それ以上データがないことを示すエラーであり、要求された量が満たされた場合には、成功とみなされるべきです。

このコミットでは、この慣習に合わせるために、CopyNの内部に以下のロジックが追加されました。

	written, err = Copy(dst, LimitReader(src, n))
	if written == n {
		return n, nil
	}

このif文は、Copynバイトを正確にコピーできた場合、たとえCopyio.EOFなどのエラーを返していたとしても、CopyNとしてはnバイトコピー成功、エラーnilとして返すことを保証します。これにより、CopyNの振る舞いがReadFullReadAtLeastと一貫し、「written == nであるのはerr == nilである場合のみ」という新しいドキュメントの記述と合致するようになります。

また、この変更を検証するために、io_test.gowantedAndErrReaderというカスタムReaderが追加されました。このReaderは、Readメソッドが常に要求されたバイト数を返しつつ、同時に非nilのエラーを返すように実装されています。これにより、CopyNnバイトをコピーしつつ、基になる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 の変更

  1. ドキュメントの更新: CopyN関数のコメントにOn return, written == n if and only if err == nil.という一文が追加されました。これは、「戻り値としてwrittennと等しいのは、errnilである場合のみである」という、この関数の新しい(そして意図された)振る舞いを明確に示しています。これにより、ユーザーはCopyNnバイトをコピーできた場合にエラーをチェックする必要がないことを理解できます。

  2. 実装の修正: written, err = Copy(dst, LimitReader(src, n))の直後に、以下の条件分岐が追加されました。

    	if written == n {
    		return n, nil
    	}
    

    このコードは、Copy関数がnバイトを正常にコピーできた場合(written == n)、CopyNは常にnnilエラーを返すことを保証します。これにより、基になるLimitReadersrcnバイト読み取った直後にEOFを返したとしても、CopyNは成功として扱われます。これは、ReadFullReadAtLeastといった他のioパッケージの関数が、要求された量のデータが完全に読み取れた場合にnilエラーを返すというGoの慣習に沿ったものです。

src/pkg/io/io_test.go の変更

  1. 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は、CopyNnバイトをコピーしつつ、基になるRead操作からエラーを受け取るという、まさにこのコミットで修正されたエッジケースをテストするために設計されました。

  2. 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)
    	}
    

    これらのテストケースは、CopyNwantedAndErrReaderから5バイトをコピーしようとしたときに、writtenが5でerrnilになることを期待しています。これは、io.goで追加されたif written == n { return n, nil }というロジックが正しく機能していることを検証します。noReadFromラッパーを使用しているのは、dstio.ReaderFromインターフェースを実装しているかどうかにかかわらず、この振る舞いが一貫していることを確認するためです。

これらの変更により、io.CopyNはより予測可能で、Goのioパッケージの他の関数と一貫性のある振る舞いをするようになり、そのセマンティクスがドキュメントによって明確にされました。

関連リンク

参考にした情報源リンク

  • コミットメッセージに記載されている関連コミット:
    • 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