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

[インデックス 10208] ファイルの概要

このコミットは、Go言語の標準ライブラリであるbufioパッケージ内のbufio.goファイルと、そのテストファイルであるbufio_test.goに変更を加えています。具体的には、bufio.ReaderReadLineメソッドの挙動を、公式ドキュメントに記載されている内容と一致させるための修正と、その修正を検証するためのテストケースの追加が行われています。

コミット

  • コミットハッシュ: bd43eac30318e062635792d44c185ac037ef98fa
  • Author: Brad Fitzpatrick bradfitz@golang.org
  • Date: Wed Nov 2 08:30:50 2011 -0700

GitHub上でのコミットページへのリンク

https://github.com/golang/go/commit/bd43eac30318e062635792d44c185ac037ef98fa

元コミット内容

    bufio: return nil line from ReadLine on error, as documented
    
    R=golang-dev, adg
    CC=golang-dev
    https://golang.org/cl/5316069

変更の背景

このコミットの背景には、bufio.Reader.ReadLineメソッドの実際の挙動が、その公式ドキュメントに記載されている仕様と異なっていたという問題があります。ドキュメントでは、エラーが発生した場合にline(読み込んだ行のバイトスライス)がnilを返すことが明記されていたにもかかわらず、実際の実装では、エラー時に空のバイトスライス([]byte{})が返される可能性がありました。

Go言語では、nilスライスと空スライスは異なる意味を持ちます。nilスライスは「データがない」状態を表し、空スライスは「データはあるが、その長さが0である」状態を表します。ReadLineのような関数において、エラー時にnilを返すというドキュメントの約束は、呼び出し元がエラー発生時にデータが存在しないことを明確に判断できるようにするために重要です。この不一致は、ReadLineの利用者が予期しない挙動に遭遇したり、エラーハンドリングを誤ったりする原因となる可能性がありました。

このコミットは、このドキュメントと実装の間の乖離を解消し、ReadLineメソッドがエラー時に常にnillineを返すように修正することで、APIの整合性と信頼性を向上させることを目的としています。

前提知識の解説

Go言語のbufioパッケージ

bufioパッケージは、Go言語の標準ライブラリの一部であり、バッファリングされたI/O操作を提供します。これにより、ディスクやネットワークからの読み書きの効率が向上します。特に、小さな読み書き操作が頻繁に行われる場合に、システムコールを減らすことでパフォーマンスを改善します。

  • bufio.Reader: io.Readerインターフェースをラップし、バッファリングされた読み込み機能を提供します。
  • bufio.Writer: io.Writerインターフェースをラップし、バッファリングされた書き込み機能を提供します。

bufio.Reader.ReadLineメソッド

ReadLineメソッドは、bufio.Readerの重要なメソッドの一つで、入力から1行を読み込みます。そのシグネチャは以下の通りです。

func (b *Reader) ReadLine() (line []byte, isPrefix bool, err error)
  • line []byte: 読み込んだ行のバイトスライスを返します。行末の改行文字は含まれません。
  • isPrefix bool: 行が長すぎてバッファに収まらなかった場合にtrueを返します。この場合、行は複数のチャンクに分割され、後続のReadLine呼び出しで残りのチャンクを読み込む必要があります。
  • err error: 読み込み中にエラーが発生した場合にエラーを返します。ファイルの終端(EOF)に達した場合はio.EOFを返します。

このメソッドのドキュメントには、「エラーが発生した場合、linenilになる」という重要な記述があります。これが今回のコミットの修正点となります。

Go言語のエラーハンドリング

Go言語では、エラーは関数の戻り値として明示的に扱われます。慣例として、関数は最後の戻り値としてerror型の値を返します。エラーがない場合はnilを返します。呼び出し元は、このエラー値をチェックすることで、操作が成功したか失敗したかを判断します。

今回のケースでは、ReadLineがエラーを返す際に、linenilであるべきか、空スライスであるべきかという点が問題となりました。ドキュメントがnilを要求している以上、実装もそれに従うべきであるという原則に基づいています。

技術的詳細

このコミットの技術的な核心は、bufio.Reader.ReadLineメソッドが、エラー発生時にlineの戻り値をnilに設定するという、ドキュメントに明記された挙動を保証することです。

元の実装では、ReadLineが内部的に行を読み込んだ結果、lineの長さが0になった場合(例えば、空行を読み込んだ場合や、エラーが発生して何も読み込めなかった場合)、lineが空のバイトスライス([]byte{})のまま返される可能性がありました。特に、エラーが発生した際に、linenilではなく空スライスとして返されるケースが存在したことが問題でした。

このコミットでは、以下の条件が同時に満たされた場合にlineを明示的にnilに設定するロジックが追加されました。

  1. len(line) == 0: 読み込んだ行の長さが0である。これは、空行を読み込んだ場合や、エラーによって何も読み込めなかった場合に発生します。
  2. err != nil: 読み込み中にエラーが発生している。

この修正により、ReadLineがエラーを返す際には、lineが必ずnilとなることが保証されます。これにより、ReadLineの呼び出し元は、linenilであるかどうかをチェックするだけで、エラー発生時に有効なデータが返されていないことを確実に判断できるようになります。

また、この変更を検証するために、bufio_test.goに新しいテストケースTestReadLineNonNilLineOrErrorが追加されました。このテストは、ReadLinelineerrの両方を同時に非nilで返すことがないことを確認します。これは、「非nillineが返される場合はエラーがない(またはio.EOFのみ)、エラーがある場合はlinenilである」というReadLineの契約を強制するものです。

コアとなるコードの変更箇所

src/pkg/bufio/bufio.go

--- a/src/pkg/bufio/bufio.go
+++ b/src/pkg/bufio/bufio.go
@@ -312,6 +312,9 @@ func (b *Reader) ReadLine() (line []byte, isPrefix bool, err error) {
 	}\n \n 	if len(line) == 0 {\n+\t\tif err != nil {\n+\t\t\tline = nil\n+\t\t}\n \t\treturn\n \t}\n \terr = nil

src/pkg/bufio/bufio_test.go

--- a/src/pkg/bufio/bufio_test.go
+++ b/src/pkg/bufio/bufio_test.go
@@ -698,6 +698,17 @@ func TestLinesAfterRead(t *testing.T) {\n \t}\n }\n \n+func TestReadLineNonNilLineOrError(t *testing.T) {\n+\tr := NewReader(strings.NewReader(\"line 1\\n\"))\n+\tfor i := 0; i < 2; i++ {\n+\t\tl, _, err := r.ReadLine()\n+\t\tif l != nil && err != nil {\n+\t\t\tt.Fatalf(\"on line %d/2; ReadLine=%#v, %v; want non-nil line or Error, but not both\",\n+\t\t\t\ti+1, l, err)\n+\t\t}\n+\t}\n+}\n+\n type readLineResult struct {\n \tline     []byte\n \tisPrefix bool\n```

## コアとなるコードの解説

### `src/pkg/bufio/bufio.go`の変更

`ReadLine`メソッド内の変更は非常にシンプルですが、その影響は大きいです。

```go
 	if len(line) == 0 {
+		if err != nil {
+			line = nil
+		}
 		return
 	}

このコードブロックは、ReadLineが読み込みを終え、lineスライスの長さが0であると判断された直後に実行されます。 追加されたif err != nilの条件は、もし読み込み中に何らかのエラーが発生していた場合(例えば、io.EOF以外のエラーや、io.EOFと同時に何も読み込めなかった場合)に真となります。 この条件が真の場合、line = nilという行が実行され、lineスライスが明示的にnilに設定されます。

これにより、ReadLineのドキュメントに記載されている「エラーが発生した場合、linenilになる」という仕様が厳密に守られるようになります。以前は、エラーが発生してもlineが空スライス([]byte{})のまま返される可能性がありましたが、この修正によってその可能性が排除されました。

src/pkg/bufio/bufio_test.goの変更

新しいテストケースTestReadLineNonNilLineOrErrorは、ReadLineの修正された挙動を検証するために追加されました。

func TestReadLineNonNilLineOrError(t *testing.T) {
	r := NewReader(strings.NewReader("line 1\n"))
	for i := 0; i < 2; i++ {
		l, _, err := r.ReadLine()
		if l != nil && err != nil {
			t.Fatalf("on line %d/2; ReadLine=%#v, %v; want non-nil line or Error, but not both",
				i+1, l, err)
		}
	}
}

このテストは、以下のシナリオをシミュレートします。

  1. strings.NewReader("line 1\n")を使って、1行のデータとそれに続くEOFを持つReaderを作成します。
  2. ループを2回実行します。
    • 1回目のループでは、「line 1」が読み込まれます。このとき、lは非nil[]byte("line 1"))で、errnilであるべきです。
    • 2回目のループでは、EOFに達するため、ReadLinelnilerrio.EOFとして返すことが期待されます。

テストの核心はif l != nil && err != nilという条件です。これは、「lineが非nilであり、かつerrも非nilである」という状況を検出します。ReadLineのドキュメントと修正された挙動によれば、この状況は発生してはなりません。つまり、lineが有効なデータを持つ場合はエラーはnilであり、エラーがある場合はlinenilであるべきです。

もしこの条件が真になった場合、t.Fatalfが呼び出され、テストは失敗します。これは、ReadLineがドキュメントに反する挙動をしたことを示します。このテストの追加により、ReadLineの契約が将来にわたって維持されることが保証されます。

関連リンク

参考にした情報源リンク

  • Go言語 bufio パッケージ公式ドキュメント: https://pkg.go.dev/bufio
  • Go言語 bufio.Reader.ReadLine メソッド公式ドキュメント: https://pkg.go.dev/bufio#Reader.ReadLine
  • Go言語におけるnilスライスと空スライスの違いに関する情報 (一般的なGo言語のドキュメントやブログ記事など)
    • 例: https://go.dev/blog/slices-intro (スライスに関する公式ブログ記事)
    • 例: https://yourbasic.org/golang/nil-slice-empty-slice/ (nilスライスと空スライスの違いに関する解説記事)
    • (上記は一般的な情報源の例であり、特定の記事を直接参照したわけではありませんが、これらの概念はGoプログラミングの基礎として広く知られています。)