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

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

このコミットは、Go言語の標準ライブラリ io パッケージ内の SectionReader 型の ReadAt メソッドにおけるバグを修正するものです。具体的には、SectionReader.ReadAt が、読み取り対象のセクションの終端に到達し、かつ提供されたバッファが完全に埋まらなかった場合に、正しく io.EOF を返さないという問題に対処しています。

コミット

commit 31eedd7f3ee26a22677f057bf61e4143e9e322cc
Author: Shenghou Ma <minux.ma@gmail.com>
Date:   Thu Dec 13 18:36:24 2012 +0800

    io: SectionReader.ReadAt should return EOF when buf is not fully read
    Fixes #4392.
    
    R=golang-dev, rsc
    CC=golang-dev
    https://golang.org/cl/6858062

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

https://github.com/golang/go/commit/31eedd7f3ee26a22677f057bf61e4143e9e322cc

元コミット内容

io: SectionReader.ReadAt should return EOF when buf is not fully read
Fixes #4392.

R=golang-dev, rsc
CC=golang-dev
https://golang.org/cl/6858062

変更の背景

このコミットは、Go言語のIssue #4392 を修正するために行われました。io.SectionReader は、基となる ReaderAt から特定のセクション(範囲)のみを読み取るための構造体です。その ReadAt メソッドは、指定されたオフセットからデータを読み込み、提供されたバイトスライス(バッファ)に書き込みます。

問題は、SectionReader が読み取り対象とするセクションの終端に到達し、かつ ReadAt に渡されたバッファが完全に埋まらなかった場合に発生していました。本来、このような状況では、読み取れたバイト数と共に io.EOF (End Of File) エラーを返すのがGoの io パッケージにおける一般的な慣習です。しかし、修正前の SectionReader.ReadAt は、バッファが完全に埋まらなかった場合でも EOF を返さず、単に読み取れたバイト数と nil エラーを返していました。

これにより、SectionReader を利用する側が、データの終端に達したことを正確に検知できず、予期せぬ動作や無限ループを引き起こす可能性がありました。このコミットは、この EOF の振る舞いをGoの io パッケージの他の部分と一貫させることを目的としています。

前提知識の解説

  • io パッケージ: Go言語の標準ライブラリで、I/Oプリミティブを提供します。ファイル、ネットワーク接続、メモリバッファなど、様々なソースからの読み書きを抽象化するためのインターフェースと実装が含まれています。
  • io.ReaderAt インターフェース:
    type ReaderAt interface {
        ReadAt(p []byte, off int64) (n int, err error)
    }
    
    ReadAt メソッドは、p に最大 len(p) バイトを読み込み、off で指定されたオフセットから開始します。読み取られたバイト数 n とエラー err を返します。ReadAt は、基となるデータソースのオフセットを変更しないという特性があります。
  • io.SectionReader: SectionReader は、ReaderAt インターフェースを実装する型で、基となる ReaderAt から特定のセクション(開始オフセット off から長さ n までの範囲)のみを読み取るように制限します。これは、大きなファイルやデータストリームの一部を扱う際に便利です。 NewSectionReader(r ReaderAt, off int64, n int64) 関数で作成されます。
  • io.EOF: io.EOF は、入力の終端に到達したことを示すエラー変数です。ReadReadAt のような読み取り操作が、それ以上読み取るデータがない場合に、読み取れたバイト数と共に io.EOF を返すのがGoの慣習です。これは、エラーとして扱われますが、通常は正常な終了条件として解釈されます。

技術的詳細

修正前の SectionReader.ReadAt のロジックでは、読み取り要求されたバイト数(len(p))が、SectionReader が扱うセクションの残りのバイト数(max)よりも大きい場合、p のサイズを max に切り詰めてから基となる ReaderAtReadAt を呼び出していました。

// 修正前 (簡略化)
func (s *SectionReader) ReadAt(p []byte, off int64) (n int, err error) {
    off += s.base
    if max := s.limit - off; int64(len(p)) > max {
        p = p[0:max] // ここでバッファが切り詰められる
    }
    return s.r.ReadAt(p, off) // 基のReaderAtのReadAtを呼び出す
}

このコードの問題点は、s.r.ReadAt(p, off)nil エラーを返した場合でも、pmax に切り詰められた結果、元の p のサイズよりも少ないバイトしか読み取れなかった場合に、io.EOF を返すべきであるにもかかわらず、それを返していなかったことです。

Goの io パッケージの慣習では、ReadReadAt は、要求されたバイト数よりも少ないバイトしか読み取れなかった場合、かつそれ以上読み取るデータがない場合に io.EOF を返します。SectionReader の場合、そのセクションの終端に到達したことが「それ以上読み取るデータがない」状態に相当します。

このコミットは、この慣習に従うように SectionReader.ReadAt の動作を変更しました。

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

src/pkg/io/io.goSectionReader.ReadAt メソッドに以下の変更が加えられました。

--- a/src/pkg/io/io.go
+++ b/src/pkg/io/io.go
@@ -468,6 +468,11 @@ func (s *SectionReader) ReadAt(p []byte, off int64) (n int, err error) {
  	off += s.base
  	if max := s.limit - off; int64(len(p)) > max {
  		p = p[0:max]
+		n, err = s.r.ReadAt(p, off)
+		if err == nil {
+			err = EOF
+		}
+		return n, err
  	}
  	return s.r.ReadAt(p, off)
  }

コアとなるコードの解説

変更は SectionReader.ReadAt メソッド内の if ブロック内で行われています。

 	if max := s.limit - off; int64(len(p)) > max {
 		p = p[0:max]
+		n, err = s.r.ReadAt(p, off) // 基のReaderAtからデータを読み込む
+		if err == nil {             // 基のReaderAtがエラーを返さなかった場合
+			err = EOF               // SectionReaderとしてはEOFを返す
+		}
+		return n, err               // 読み取れたバイト数とエラーを返す
 	}

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

  1. バッファの切り詰め: if int64(len(p)) > max の条件は、要求された読み取りサイズ len(p) が、SectionReader が扱うセクションの残りのバイト数 max を超えていることを意味します。この場合、p = p[0:max] によって、実際に読み取れる最大バイト数に合わせてバッファ p が切り詰められます。
  2. 基の ReadAt の呼び出し: 切り詰められた p と計算されたオフセット off を使って、基となる s.r (元の ReaderAt) の ReadAt メソッドが呼び出されます。この呼び出しの結果、実際に読み取れたバイト数 n とエラー err が得られます。
  3. EOF の判定と返却:
    • if err == nil の条件は、基の ReaderAt が読み取り中にエラーを発生させなかったことを意味します。
    • しかし、この if ブロックに入っているということは、SectionReader が扱うセクションの終端に到達したため、要求されたバッファを完全に埋めることができなかった状況です。
    • したがって、基の ReaderAt がエラーを返さなかったとしても、SectionReader の観点からはデータの終端に達したと判断し、err = EOF を設定します。
  4. 結果の返却: 最後に、実際に読み取れたバイト数 n と、適切に設定されたエラー err (nil または EOF) が返されます。

この修正により、SectionReader.ReadAt は、セクションの終端に到達し、かつバッファが完全に埋まらなかった場合に、Goの io パッケージの慣習に従って io.EOF を返すようになり、より予測可能で堅牢な動作を実現しました。

また、src/pkg/io/io_test.go には、この修正を検証するための新しいテストケース TestSectionReader_ReadAt が追加されています。このテストは、様々なシナリオ(空のデータ、セクションの終端での読み取り、バッファが完全に埋まらないケースなど)で SectionReader.ReadAt が期待通りのバイト数とエラー(特に EOF)を返すことを確認しています。

関連リンク

参考にした情報源リンク