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

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

このコミットは、Go言語の標準ライブラリであるbufioパッケージ内のReader.Peekメソッドの挙動を修正するものです。具体的には、Peekメソッドが有効なデータを返す場合でも、不適切にエラーを返してしまうバグ(Issue 3022)を修正することを目的としています。これにより、Peekが期待通りに動作し、呼び出し元が不要なエラー処理を行う必要がなくなります。

コミット

  • コミットハッシュ: 88f8af127ab675b94e18f161c59415edd92110e9
  • Author: Brad Fitzpatrick bradfitz@golang.org
  • Date: Thu Feb 16 10:15:36 2012 +1100

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

https://github.com/golang/go/commit/88f8af127ab675b94e18f161c59415edd92110e9

元コミット内容

bufio: don't return errors from good Peeks

Fixes #3022

R=golang-dev, r
CC=golang-dev
https://golang.org/cl/5674060

変更の背景

bufioパッケージのReader.Peekメソッドは、内部バッファから指定されたバイト数(n)を読み込まずに「覗き見」する機能を提供します。このメソッドは、次に読み込まれるデータの一部を事前に確認したい場合に非常に便利です。しかし、このコミットが修正するバグ(Issue 3022)により、Peekメソッドが実際に要求されたバイト数分のデータを正常に提供できた場合でも、誤ってエラーを返してしまうという問題がありました。

具体的には、Peekは内部バッファのサイズや、基となるリーダーからの読み込み状況に応じて、io.EOFErrBufferFullといったエラーを返すことがあります。しかし、このバグが存在するバージョンでは、Peekが要求されたnバイト分のデータを正常に提供できたにもかかわらず、以前の読み込み操作で発生したエラー(例えば、基となるリーダーが既にEOFに達していた場合など)を不適切に伝播させてしまうことがありました。

この挙動は、Peekの本来の意図に反しており、呼び出し元が不要なエラー処理を記述したり、Peekの戻り値の信頼性が低下したりする原因となっていました。このコミットは、Peekが「良い(good)」データを提供できた場合には、エラーを返さないようにすることで、この問題を解決し、Peekメソッドの堅牢性と使いやすさを向上させています。

前提知識の解説

bufioパッケージ

bufioパッケージは、I/O操作をバッファリングすることで効率化するためのGo言語の標準ライブラリです。ディスクI/OやネットワークI/Oなど、低速なI/O操作を直接行う代わりに、メモリ上のバッファを介してデータを読み書きすることで、システムコールの回数を減らし、パフォーマンスを向上させます。

bufio.Reader

bufio.Readerは、バッファリングされた読み込み操作を提供する型です。内部にバッファを持ち、基となるio.Readerからデータを読み込み、そのバッファを介して呼び出し元にデータを提供します。これにより、一度に多くのデータを読み込んでバッファに格納し、その後の小さな読み込み要求に対してはバッファから直接データを提供することで、効率的な読み込みを実現します。

Peekメソッド

func (b *Reader) Peek(n int) ([]byte, error)

Peekメソッドは、bufio.Readerの重要なメソッドの一つです。このメソッドは、次に読み込まれるであろうnバイトのデータを、実際に読み込みポインタを進めることなく(つまり、データを消費することなく)返します。これにより、呼び出し元はストリームの先頭を「覗き見」して、次にどのようなデータが来るかを判断し、それに基づいて適切な処理を行うことができます。

Peekは以下の2つの値を返します。

  • []byte: 覗き見したデータ。要求されたnバイト分のデータがバッファに存在しない場合、利用可能な最大バイト数が返されます。
  • error: エラーが発生した場合に返されます。主なエラーとしては、以下のものがあります。
    • io.EOF: 基となるリーダーがファイルの終端に達した場合。
    • bufio.ErrBufferFull: 要求されたnバイト分のデータをバッファに格納できない場合(バッファサイズが不足している場合)。

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

Go言語では、エラーは関数の最後の戻り値としてerror型の値で返されます。慣習として、エラーが発生しなかった場合はnilが返されます。呼び出し元は、返されたエラーがnilかどうかをチェックすることで、操作が成功したか失敗したかを判断します。

技術的詳細

このコミットの主要な変更は、src/pkg/bufio/bufio.go内のReader.Peekメソッドのロジック修正と、その修正を検証するためのテストケースの追加です。

bufio.goの変更点

変更前のPeekメソッドの関連部分は以下のようになっていました。

func (b *Reader) Peek(n int) ([]byte, error) {
    // ... (前略) ...
    if m > n {
        m = n
    }
    err := b.readErr() // ここで以前のエラーを取得
    if m < n && err == nil {
        err = ErrBufferFull
    }
    return b.buf[b.r : b.r+m], err
}

このコードでは、まずb.readErr()を呼び出して、以前の読み込み操作で発生したエラー(例えば、基となるリーダーが既にEOFに達していた場合など)を取得していました。その後、m < n(要求されたnバイト分のデータがバッファに存在しない)かつerr == nil(以前のエラーがない)の場合にのみErrBufferFullを設定していました。

問題は、m < nの条件が満たされない場合、つまりm >= nの場合(要求されたnバイト分のデータがバッファに存在し、Peekが成功したと見なせる場合)でも、b.readErr()で取得した以前のエラーがそのまま返されてしまう点にありました。これが「良いPeek」でもエラーが返される原因でした。

修正後のPeekメソッドの関連部分は以下のようになります。

func (b *Reader) Peek(n int) ([]byte, error) {
    // ... (前略) ...
    if m > n {
        m = n
    }
    var err error // エラー変数を初期化
    if m < n { // 要求されたnバイト分のデータがバッファに存在しない場合のみエラーを考慮
        err = b.readErr() // 以前のエラーを取得
        if err == nil { // 以前のエラーがない場合、バッファ不足のエラーを設定
            err = ErrBufferFull
        }
    }
    return b.buf[b.r : b.r+m], err
}

この修正では、err変数をnilで初期化し、m < n(要求されたバイト数に満たない)の場合にのみエラーを考慮するロジックに変更されています。

  • m < nの場合:
    • b.readErr()で以前のエラーを取得します。
    • もし以前のエラーがnilであれば、ErrBufferFullを設定します。
    • 以前のエラーが存在すれば、そのエラーが返されます(例: io.EOF)。
  • m >= nの場合:
    • errは初期値のnilのままとなり、Peekはエラーを返さずに成功します。

これにより、Peekが要求されたバイト数分のデータを正常に提供できた場合には、以前のエラーが伝播されることなく、nilが返されるようになります。

bufio_test.goの変更点

このコミットでは、修正されたPeekメソッドの挙動を検証するために、TestPeek関数に新しいテストケースが追加されています。

追加されたテストケースは、dataAndEOFReaderというカスタムのio.Reader実装を使用しています。このリーダーは、指定されたデータを一度だけ読み込み、その後すぐにio.EOFを返すように設計されています。

type dataAndEOFReader string

func (r dataAndEOFReader) Read(p []byte) (int, error) {
    return copy(p, r), io.EOF
}

このdataAndEOFReaderを使ってbufio.Readerを初期化し、Peekメソッドがどのように振る舞うかをテストしています。

// Test for issue 3022, not exposing a reader's error on a successful Peek.
buf = NewReaderSize(dataAndEOFReader("abcd"), 32)
if s, err := buf.Peek(2); string(s) != "ab" || err != nil {
    t.Errorf(`Peek(2) on "abcd", EOF = %q, %v; want "ab", nil`, string(s), err)
}
if s, err := buf.Peek(4); string(s) != "abcd" || err != nil {
    t.Errorf(`Peek(4) on "abcd", EOF = %q, %v; want "abcd", nil`, string(s), err)
}
// ... (後略) ...

このテストでは、"abcd"というデータを持つdataAndEOFReaderを基にbufio.Readerを作成し、以下のことを確認しています。

  1. Peek(2)を呼び出した場合、"ab"が返され、エラーはnilであること。
  2. Peek(4)を呼び出した場合、"abcd"が返され、エラーはnilであること。

これらのテストケースは、基となるリーダーがio.EOFを返している状況でも、Peekが要求されたバイト数分のデータを正常に提供できた場合には、エラーを返さないという修正後の挙動を正確に検証しています。

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

src/pkg/bufio/bufio.goReader.Peekメソッド内の以下の部分が変更されました。

--- a/src/pkg/bufio/bufio.go
+++ b/src/pkg/bufio/bufio.go
@@ -106,9 +106,12 @@ func (b *Reader) Peek(n int) ([]byte, error) {
  	if m > n {
  		m = n
  	}
-	err := b.readErr()
-	if m < n && err == nil {
-		err = ErrBufferFull
+	var err error
+	if m < n {
+		err = b.readErr()
+		if err == nil {
+			err = ErrBufferFull
+		}
 	}
  	return b.buf[b.r : b.r+m], err
 }

コアとなるコードの解説

変更の核心は、エラーの伝播ロジックの変更にあります。

変更前は、err := b.readErr()によって、Peekが呼び出される前に基となるリーダーで発生した可能性のあるエラー(例えば、io.EOF)を無条件に取得していました。そして、m < n(要求されたバイト数に満たない)かつerr == nilの場合にのみErrBufferFullを設定していました。このロジックでは、m >= n(要求されたバイト数が全て提供できた)場合でも、b.readErr()で取得したエラーがそのまま返されてしまう問題がありました。

変更後は、var err errorerrnilで初期化し、エラーを考慮する条件をif m < nの中に限定しました。

  • if m < n(要求されたバイト数に満たない場合):
    • err = b.readErr(): ここで初めて以前のエラーを取得します。
    • if err == nil { err = ErrBufferFull }: 以前のエラーがなければ、バッファが不足していることを示すErrBufferFullを設定します。
  • m >= n(要求されたバイト数が全て提供できた場合):
    • このif m < nブロックには入らないため、errは初期値のnilのままとなり、Peekはエラーを返さずに成功します。

この変更により、Peekメソッドは、実際に要求されたバイト数分のデータを正常に提供できた場合には、基となるリーダーの以前のエラー状態に関わらず、nilを返すようになります。これにより、Peekのセマンティクスがより直感的になり、呼び出し元はPeekがデータを返した場合にはエラーがないことを信頼できるようになりました。

関連リンク

  • Go Change-ID: https://golang.org/cl/5674060 (このコミットに対応するGoのコードレビューページ)
  • Go Issue 3022: https://golang.org/issue/3022 (このコミットが修正したバグのトラッキングページ)

参考にした情報源リンク

  • コミットメッセージとコードの変更点
  • Go言語のbufioパッケージのドキュメント (Go標準ライブラリ)
  • Go言語のエラーハンドリングに関する一般的な知識
  • io.Readerインターフェースに関する知識
  • io.EOFエラーに関する知識
  • bufio.ErrBufferFullエラーに関する知識