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

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

このコミットでは、Go言語の標準ライブラリioパッケージ内のSectionReaderの挙動が変更され、Seekerインターフェースのセマンティクスがより明確に文書化されました。具体的には、src/pkg/io/io.gosrc/pkg/io/io_test.goの2つのファイルが変更されています。

コミット

io: SectionReaderが終端を超えてシークできるようにし、Seekerのセマンティクスをより詳細に文書化

os.File、strings.Reader、bytes.Readerなど、終端を超えたシークを許可する他の実装との一貫性を保つ。

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

https://github.com/golang/go/commit/39100543ff629ef82bb814a03c6582f7158204a8

元コミット内容

commit 39100543ff629ef82bb814a03c6582f7158204a8
Author: Brad Fitzpatrick <bradfitz@golang.org>
Date:   Thu Jul 18 13:03:26 2013 +1000

    io: let SectionReader seek past the end; document Seeker semantics more
    
    Be consistent with os.File, strings.Reader, bytes.Reader, etc,
    which all allow seeks past the end.
    
    R=golang-dev, r
    CC=golang-dev
    https://golang.org/cl/11403043
---
 src/pkg/io/io.go      | 12 ++++++++----
 src/pkg/io/io_test.go | 42 +++++++++++++++++++++++++++++++++++-------\n 2 files changed, 43 insertions(+), 11 deletions(-)\n

変更の背景

このコミットの主な背景は、Go言語のioパッケージにおけるSeek操作の一貫性を向上させることです。以前のSectionReaderの実装では、ファイルの終端(またはセクションの終端)を超えてシークしようとするとエラーを返していました。しかし、os.Filestrings.Readerbytes.Readerといった他の標準的なリーダー実装では、終端を超えてシークすることが許容されており、その場合でもエラーとはなりません。

この不一致は、開発者が異なるリーダータイプを扱う際に予期せぬ挙動やエラーに遭遇する可能性がありました。例えば、ファイル全体を読み込むことを期待してSectionReaderを使用している場合でも、内部的にSeekが終端を超えて行われると、不必要なエラーが発生し、コードの複雑さが増す可能性がありました。

このコミットは、SectionReaderSeekメソッドが他の標準的なリーダーと同様に、終端を超えたシークを許可するように変更することで、この一貫性の問題を解決することを目的としています。これにより、io.Seekerインターフェースを実装する様々な型が、より予測可能で統一された方法で動作するようになります。また、Seekerインターフェース自体のドキュメントも更新され、この新しいセマンティクスが明確に記述されています。

前提知識の解説

io.Seekerインターフェース

io.Seekerは、Go言語のioパッケージで定義されているインターフェースの一つで、データストリーム内の現在位置を移動させる機能を提供します。このインターフェースは、以下のSeekメソッドを定義しています。

type Seeker interface {
    Seek(offset int64, whence int) (ret int64, err error)
}
  • offset: 移動するバイト数を示します。
  • whence: offsetの解釈方法を指定します。以下の定数が使用されます。
    • io.SeekStart (0): offsetはストリームの先頭からの絶対位置です。
    • io.SeekCurrent (1): offsetは現在の位置からの相対位置です。
    • io.SeekEnd (2): offsetはストリームの終端からの相対位置です(通常、負の値で指定されます)。

Seekメソッドは、新しいオフセット(ストリームの先頭からの絶対位置)と、エラーが発生した場合はエラーを返します。

io.SectionReader

io.SectionReaderは、基となるReaderAtからデータの「セクション」(特定の範囲)を読み取るための構造体です。これは、大きなファイルやデータストリームの一部だけを扱う場合に便利です。SectionReaderは、指定されたオフセット(off)から始まり、指定された長さ(n)までのデータを読み取ります。

SectionReaderは内部的に、元のReaderAtのオフセットと、セクションの開始オフセット(s.base)およびセクションの終了オフセット(s.limit)を管理しています。

シーク操作と「終端を超えたシーク」

ファイルやデータストリームに対するシーク操作は、読み書きの開始位置を変更するために行われます。通常、シーク位置はデータの範囲内に収まることが期待されますが、一部のリーダー実装では、データの終端を超えてシークすることが許可されています。

「終端を超えたシーク」とは、Seekメソッドによって設定される新しいオフセットが、実際のデータ長よりも大きい位置になることを指します。このようなシークが成功した場合、その後の読み取り操作は、シーク位置がデータの終端を超えているため、すぐにio.EOF(End Of File)エラーを返すことが一般的です。これは、データが存在しないことを示す標準的な挙動です。

このコミット以前のSectionReaderは、s.limit(セクションの終端)を超えてシークしようとするとエラーを返していました。しかし、os.Filestrings.Readerbytes.Readerといった他のGoの標準的なリーダーは、終端を超えたシークを許可し、その後の読み取りでio.EOFを返すという挙動を示していました。このコミットは、SectionReaderもこの一貫した挙動に従うように修正しました。

技術的詳細

このコミットの技術的な変更点は、主にio.go内のSectionReader.Seekメソッドのロジックと、io_test.goに追加されたテストケースにあります。

io.goの変更点

  1. Seekerインターフェースのドキュメント更新: SeekerインターフェースのSeekメソッドのコメントが更新され、特に「負のオフセットへのシークはエラーである」ことと、「任意の正のオフセットへのシークは合法であるが、その後のI/O操作の挙動は基となるオブジェクトの実装に依存する」という点が明記されました。これは、終端を超えたシークが合法であることを示唆しています。

    --- a/src/pkg/io/io.go
    +++ b/src/pkg/io/io.go
    @@ -91,10 +91,14 @@ type Closer interface {
     // Seek sets the offset for the next Read or Write to offset,
     // interpreted according to whence: 0 means relative to the origin of
     // the file, 1 means relative to the current offset, and 2 means
    -// relative to the end.  Seek returns the new offset and an Error, if
    +// relative to the end.  Seek returns the new offset and an error, if
     // any.
    +//
    +// Seeking to a negative offset is an error. Seeking to any positive
    +// offset is legal, but the behavior of subsequent I/O operations on
    +// the underlying object is implementation-dependent.
     type Seeker interface {
    -\tSeek(offset int64, whence int) (ret int64, err error)\n+\tSeek(offset int64, whence int) (int64, error)\n     }
    
  2. SectionReader.Seekメソッドのロジック変更: SectionReader.Seekメソッド内で、計算されたoffsets.limit(セクションの終端)を超えているかどうかをチェックする条件が削除されました。

    変更前:

    if offset < s.base || offset > s.limit {
        return 0, errOffset
    }
    

    変更後:

    if offset < s.base {
        return 0, errOffset
    }
    

    この変更により、offsets.base(セクションの開始)より小さい場合のみエラーが返されるようになり、s.limitを超えてシークすることが許可されるようになりました。これにより、SectionReaderは、os.Filebytes.Readerなどと同様に、データの終端を超えてシークしてもエラーを返さず、その位置を有効なものとして扱うようになります。

io_test.goの変更点

新しいテスト関数TestSectionReader_Seekが追加されました。このテストは、SectionReaderSeekメソッドがbytes.NewReaderstrings.NewReaderと同様の挙動を示す)のSeekメソッドと一貫した挙動を示すことを検証します。

  • 広範囲なシークテスト: whence(0, 1, 2)とoffset(-3から4まで)の様々な組み合わせで、bytes.ReaderSectionReaderSeek結果(新しいオフセットとエラー)を比較しています。これにより、負のオフセットや、セクションの範囲内外へのシークが正しく処理されることを確認しています。
  • 終端を超えたシークとEOFの検証: 特に重要なのは、sr.Seek(100, 0)のようにセクションの実際の長さ("foo"の3バイト)をはるかに超える位置にシークした後、sr.Read(make([]byte, 10))を実行して、n != 0 || err != EOFとなることを検証している点です。これは、終端を超えてシークした場合、その後の読み取り操作が0バイトを読み込み、io.EOFエラーを返すという期待される挙動を確認しています。

これらの変更により、SectionReaderはGoの他の標準的なリーダー実装と一貫したSeek挙動を持つようになり、より堅牢で予測可能なio操作が可能になりました。

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

src/pkg/io/io.go

--- a/src/pkg/io/io.go
+++ b/src/pkg/io/io.go
@@ -91,10 +91,14 @@ type Closer interface {
 // Seek sets the offset for the next Read or Write to offset,
 // interpreted according to whence: 0 means relative to the origin of
 // the file, 1 means relative to the current offset, and 2 means
-// relative to the end.  Seek returns the new offset and an Error, if
+// relative to the end.  Seek returns the new offset and an error, if
 // any.
+//
+// Seeking to a negative offset is an error. Seeking to any positive
+// offset is legal, but the behavior of subsequent I/O operations on
+// the underlying object is implementation-dependent.
 type Seeker interface {
-\tSeek(offset int64, whence int) (ret int64, err error)\n+\tSeek(offset int64, whence int) (int64, error)\n }
 
 // ReadWriter is the interface that groups the basic Read and Write methods.
 type ReadWriter interface {
@@ -426,7 +430,7 @@ func (s *SectionReader) Read(p []byte) (n int, err error) {
 var errWhence = errors.New("Seek: invalid whence")
 var errOffset = errors.New("Seek: invalid offset")
 
-func (s *SectionReader) Seek(offset int64, whence int) (ret int64, err error) {
+func (s *SectionReader) Seek(offset int64, whence int) (int64, error) {
 	switch whence {
 	default:
 		return 0, errWhence
@@ -437,7 +441,7 @@ func (s *SectionReader) Seek(offset int64, whence int) (ret int64, err error) {
 	case 2:
 		offset += s.limit
 	}
-\tif offset < s.base || offset > s.limit {\n+\tif offset < s.base {\n 		return 0, errOffset
 	}
 	s.off = offset

src/pkg/io/io_test.go

--- a/src/pkg/io/io_test.go
+++ b/src/pkg/io/io_test.go
@@ -260,7 +260,7 @@ func TestTeeReader(t *testing.T) {
 	}
 }
 
-func TestSectionReader_ReadAt(tst *testing.T) {
+func TestSectionReader_ReadAt(t *testing.T) {
 	dat := "a long sample data, 1234567890"
 	tests := []struct {
 		data   string
@@ -282,12 +282,40 @@ func TestSectionReader_ReadAt(tst *testing.T) {
 		{data: dat, off: 3, n: len(dat) / 2, bufLen: len(dat)/2 - 2, at: 2, exp: dat[5 : 5+len(dat)/2-2], err: nil},
 		{data: dat, off: 3, n: len(dat) / 2, bufLen: len(dat)/2 + 2, at: 2, exp: dat[5 : 5+len(dat)/2-2], err: EOF},
 	}
-\tfor i, t := range tests {\n-\t\tr := strings.NewReader(t.data)\n-\t\ts := NewSectionReader(r, int64(t.off), int64(t.n))\n-\t\tbuf := make([]byte, t.bufLen)\n-\t\tif n, err := s.ReadAt(buf, int64(t.at)); n != len(t.exp) || string(buf[:n]) != t.exp || err != t.err {\n-\t\t\ttst.Fatalf("%d: ReadAt(%d) = %q, %v; expected %q, %v", i, t.at, buf[:n], err, t.exp, t.err)\n+\tfor i, tt := range tests {\n+\t\tr := strings.NewReader(tt.data)\n+\t\ts := NewSectionReader(r, int64(tt.off), int64(tt.n))\n+\t\tbuf := make([]byte, tt.bufLen)\n+\t\tif n, err := s.ReadAt(buf, int64(tt.at)); n != len(tt.exp) || string(buf[:n]) != tt.exp || err != tt.err {\n+\t\t\tt.Fatalf("%d: ReadAt(%d) = %q, %v; expected %q, %v", i, tt.at, buf[:n], err, tt.exp, tt.err)\n \t\t}\n \t}\n }\n+\n+func TestSectionReader_Seek(t *testing.T) {\n+\t// Verifies that NewSectionReader's Seeker behaves like bytes.NewReader (which is like strings.NewReader)\n+\tbr := bytes.NewReader([]byte("foo"))\n+\tsr := NewSectionReader(br, 0, int64(len("foo")))\n+\n+\tfor whence := 0; whence <= 2; whence++ {\n+\t\tfor offset := int64(-3); offset <= 4; offset++ {\n+\t\t\tbrOff, brErr := br.Seek(offset, whence)\n+\t\t\tsrOff, srErr := sr.Seek(offset, whence)\n+\t\t\tif (brErr != nil) != (srErr != nil) || brOff != srOff {\n+\t\t\t\tt.Errorf("For whence %d, offset %d: bytes.Reader.Seek = (%v, %v) != SectionReader.Seek = (%v, %v)",\n+\t\t\t\t\twhence, offset, brOff, brErr, srErr, srOff)\n+\t\t\t}\n+\t\t}\n+\t}\n+\n+\t// And verify we can just seek past the end and get an EOF\n+\tgot, err := sr.Seek(100, 0)\n+\tif err != nil || got != 100 {\n+\t\tt.Errorf("Seek = %v, %v; want 100, nil", got, err)\n+\t}\n+\n+\tn, err := sr.Read(make([]byte, 10))\n+\tif n != 0 || err != EOF {\n+\t\tt.Errorf("Read = %v, %v; want 0, EOF", n, err)\n+\t}\n+}\n```

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

### `src/pkg/io/io.go`

*   **`Seeker`インターフェースのコメント変更**:
    `Seeker`インターフェースの`Seek`メソッドのコメントが修正され、`Seek`がエラーを返す条件がより明確になりました。特に、「負のオフセットへのシークはエラーである」ことと、「任意の正のオフセットへのシークは合法である」という点が追記されました。これにより、`SectionReader`が終端を超えてシークすることを許可する変更の意図が、インターフェースの定義レベルで明確に示されています。

*   **`SectionReader.Seek`メソッドの条件式変更**:
    `SectionReader`の`Seek`メソッド内のエラーチェックの条件式が、`if offset < s.base || offset > s.limit`から`if offset < s.base`に変更されました。
    *   変更前は、計算された`offset`がセクションの開始位置`s.base`より小さいか、またはセクションの終了位置`s.limit`より大きい場合に`errOffset`を返していました。これは、セクションの範囲外へのシークを厳密に制限していました。
    *   変更後は、`offset`が`s.base`より小さい場合のみエラーを返すようになりました。これにより、`offset`が`s.limit`を超えていてもエラーとはならず、シーク位置がセクションの終端を超えて設定されることが許可されます。この変更が、`SectionReader`が終端を超えてシークできるようにする核心的な部分です。

### `src/pkg/io/io_test.go`

*   **`TestSectionReader_Seek`関数の追加**:
    この新しいテスト関数は、`SectionReader`の`Seek`メソッドの新しい挙動を検証するために追加されました。
    1.  **`bytes.NewReader`との比較**: `bytes.NewReader`(`strings.NewReader`と同様に、終端を超えたシークを許可する)の`Seek`メソッドと`SectionReader`の`Seek`メソッドの挙動を比較しています。`whence`と`offset`の様々な組み合わせで両者の結果(新しいオフセットとエラー)が一致するかどうかを確認することで、`SectionReader`が他の標準的なリーダーと一貫した`Seek`挙動を持つことを保証しています。
    2.  **終端を超えたシーク後のEOF検証**: テストの後半では、`sr.Seek(100, 0)`のように、`SectionReader`のセクションの実際の長さ(この例では"foo"の3バイト)をはるかに超える位置にシークしています。その後、`sr.Read`を呼び出し、読み込まれたバイト数`n`が0であり、かつエラーが`io.EOF`であることを検証しています。これは、終端を超えてシークした場合、その後の読み取り操作がデータがないことを示す`io.EOF`を返すという、期待される標準的な挙動を確認するものです。

これらのコード変更により、`SectionReader`はより柔軟になり、Goの`io`パッケージ全体での`Seek`操作のセマンティクスが統一され、開発者にとってより予測しやすい挙動を提供するようになりました。

## 関連リンク

*   [https://golang.org/cl/11403043](https://golang.org/cl/11403043) (このコミットに対応するGerritの変更リスト)

## 参考にした情報源リンク

*   Go言語の`io`パッケージのドキュメント
*   Go言語の`os`パッケージのドキュメント
*   Go言語の`strings`パッケージのドキュメント
*   Go言語の`bytes`パッケージのドキュメント
*   Go言語のソースコード (特に`src/pkg/io/io.go`と`src/pkg/io/io_test.go`)
*   シーク操作における`SEEK_SET`, `SEEK_CUR`, `SEEK_END`の一般的な概念