[インデックス 16804] ファイルの概要
このコミットでは、Go言語の標準ライブラリio
パッケージ内のSectionReader
の挙動が変更され、Seeker
インターフェースのセマンティクスがより明確に文書化されました。具体的には、src/pkg/io/io.go
とsrc/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.File
、strings.Reader
、bytes.Reader
といった他の標準的なリーダー実装では、終端を超えてシークすることが許容されており、その場合でもエラーとはなりません。
この不一致は、開発者が異なるリーダータイプを扱う際に予期せぬ挙動やエラーに遭遇する可能性がありました。例えば、ファイル全体を読み込むことを期待してSectionReader
を使用している場合でも、内部的にSeek
が終端を超えて行われると、不必要なエラーが発生し、コードの複雑さが増す可能性がありました。
このコミットは、SectionReader
のSeek
メソッドが他の標準的なリーダーと同様に、終端を超えたシークを許可するように変更することで、この一貫性の問題を解決することを目的としています。これにより、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.File
、strings.Reader
、bytes.Reader
といった他のGoの標準的なリーダーは、終端を超えたシークを許可し、その後の読み取りでio.EOF
を返すという挙動を示していました。このコミットは、SectionReader
もこの一貫した挙動に従うように修正しました。
技術的詳細
このコミットの技術的な変更点は、主にio.go
内のSectionReader.Seek
メソッドのロジックと、io_test.go
に追加されたテストケースにあります。
io.go
の変更点
-
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 }
-
SectionReader.Seek
メソッドのロジック変更:SectionReader.Seek
メソッド内で、計算されたoffset
がs.limit
(セクションの終端)を超えているかどうかをチェックする条件が削除されました。変更前:
if offset < s.base || offset > s.limit { return 0, errOffset }
変更後:
if offset < s.base { return 0, errOffset }
この変更により、
offset
がs.base
(セクションの開始)より小さい場合のみエラーが返されるようになり、s.limit
を超えてシークすることが許可されるようになりました。これにより、SectionReader
は、os.File
やbytes.Reader
などと同様に、データの終端を超えてシークしてもエラーを返さず、その位置を有効なものとして扱うようになります。
io_test.go
の変更点
新しいテスト関数TestSectionReader_Seek
が追加されました。このテストは、SectionReader
のSeek
メソッドがbytes.NewReader
(strings.NewReader
と同様の挙動を示す)のSeek
メソッドと一貫した挙動を示すことを検証します。
- 広範囲なシークテスト:
whence
(0, 1, 2)とoffset
(-3から4まで)の様々な組み合わせで、bytes.Reader
とSectionReader
のSeek
結果(新しいオフセットとエラー)を比較しています。これにより、負のオフセットや、セクションの範囲内外へのシークが正しく処理されることを確認しています。 - 終端を超えたシークと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`の一般的な概念