[インデックス 11911] ファイルの概要
このコミットは、Go言語の標準ライブラリであるbytes
パッケージにReader
型を新しく追加するものです。bytes.Reader
は、バイトスライス([]byte
)からデータを読み取るためのio.Reader
、io.Seeker
、io.ByteScanner
、およびio.RuneScanner
インターフェースを実装します。これは、既存のstrings.Reader
が文字列(string
)に対して提供する機能のバイトスライス版と考えることができます。
コミット
commit 977e8d145bca078936176f73f884bb4b7da037b7
Author: Brad Fitzpatrick <bradfitz@golang.org>
Date: Wed Feb 15 11:06:16 2012 +1100
bytes: add Reader
copy of strings/reader{,_test}.go with
a few s/string/[]byte/
Fixes #2861
R=adg, rsc, r
CC=golang-dev
https://golang.org/cl/5666044
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/977e8d145bca078936176f73f884bb4b7da037b7
元コミット内容
このコミットの目的は、Go言語のbytes
パッケージにReader
型を追加することです。これは、strings
パッケージのReader
型(strings.Reader
)をベースにしており、文字列ではなくバイトスライスを扱うように変更されています。具体的には、strings/reader.go
とstrings/reader_test.go
のコードをコピーし、文字列(string
)をバイトスライス([]byte
)に置き換えることで実装されています。
この変更は、GoのIssue #2861を修正するものです。また、コードレビューはadg
、rsc
、r
によって行われ、golang-dev
メーリングリストにもCCされています。関連するGoのコードレビュー(CL)のリンクはhttps://golang.org/cl/5666044
ですが、このCL番号は公開されているGoのGerritリポジトリでは見つかりませんでした。
変更の背景
Go言語では、io
パッケージが提供するインターフェース(io.Reader
、io.Writer
など)を通じて、様々なデータソースからの読み書きを抽象化しています。既存のstrings.Reader
は、メモリ上の文字列をio.Reader
として扱うことを可能にしていましたが、同様の機能がバイトスライスに対しても求められていました。
バイトスライスは、ファイルの内容、ネットワークからのデータ、または他の処理結果など、様々なバイナリデータを表現するためによく使用されます。これらのバイトスライスをio.Reader
として扱うことで、io.Copy
やbufio.Scanner
など、io.Reader
インターフェースを受け入れる既存の多くの関数やライブラリとシームレスに連携できるようになります。
このbytes.Reader
の追加により、開発者はバイトスライスをあたかもファイルやネットワークストリームのように扱うことができ、Goの強力なio
パッケージのエコシステムを最大限に活用できるようになります。特に、バイトスライスから特定のオフセットで読み取りを開始したり、読み取り位置をシークしたりする機能は、バイナリデータの解析や処理において非常に有用です。
コミットメッセージにあるFixes #2861
は、この機能の必要性を示すGoのIssueが存在したことを示唆しています。具体的なIssue #2861の内容は不明ですが、bytes.Reader
が提供する機能(バイトスライスからの読み取り、シーク、バイト/ルーン単位の読み取り)が、当時のGo開発者コミュニティで求められていたことを示しています。
前提知識の解説
このコミットを理解するためには、Go言語の以下の基本的な概念とインターフェースについて理解しておく必要があります。
-
io.Reader
インターフェース:Read(p []byte) (n int, err error)
メソッドを持つインターフェースです。- データソースからバイトを読み取り、
p
に書き込みます。 n
は読み取ったバイト数、err
はエラー(通常は読み取りが終了したことを示すio.EOF
)を返します。- Goにおける入力操作の基本的な抽象化です。
-
io.Seeker
インターフェース:Seek(offset int64, whence int) (int64, error)
メソッドを持つインターフェースです。- データソースの現在の読み取り/書き込みオフセットを変更します。
offset
は移動するバイト数、whence
はオフセットの基準位置(io.SeekStart
、io.SeekCurrent
、io.SeekEnd
)を指定します。io.SeekStart
(0): オフセットはファイルの先頭からの絶対位置。io.SeekCurrent
(1): オフセットは現在の位置からの相対位置。io.SeekEnd
(2): オフセットはファイルの末尾からの相対位置。- 戻り値は新しいオフセットの絶対位置です。
-
io.ByteScanner
インターフェース:ReadByte() (byte, error)
とUnreadByte() error
メソッドを持つインターフェースです。ReadByte
は次の1バイトを読み取ります。UnreadByte
は最後に読み取ったバイトを「読み取り解除」し、次の読み取り操作で同じバイトが返されるようにします。これは通常、パーサーなどでトークンを読み進める際に、読みすぎた場合に前の状態に戻るために使用されます。
-
io.RuneScanner
インターフェース:ReadRune() (r rune, size int, err error)
とUnreadRune() error
メソッドを持つインターフェースです。ReadRune
は次のUTF-8エンコードされたルーン(Unicodeコードポイント)を読み取ります。r
はルーン、size
はそのルーンをエンコードするために使用されたバイト数です。UnreadRune
は最後に読み取ったルーンを「読み取り解除」します。io.ByteScanner
のUnreadByte
と同様の目的で使用されます。
-
io.EOF
:io.EOF
は、io.Reader
のRead
メソッドが、それ以上読み取るデータがない場合に返す特別なエラー値です。Read
メソッドは、読み取ったバイト数n
が0でerr
がio.EOF
の場合、ストリームの終わりに達したことを示します。- ただし、
Read
メソッドは、一部のバイトを読み取った後にストリームの終わりに達した場合でも、n > 0
とerr == nil
を返すことがあります。この場合、次のRead
呼び出しでio.EOF
が返されます。この挙動は、bytes.Reader
を含む多くのio.Reader
実装で共通しており、Goのio
パッケージの設計原則に基づいています。
-
strings.Reader
:strings
パッケージに存在する型で、文字列(string
)をio.Reader
、io.Seeker
、io.ByteScanner
、io.RuneScanner
として扱うことができます。bytes.Reader
は、このstrings.Reader
の機能をバイトスライスに拡張したものです。
技術的詳細
bytes.Reader
は、バイトスライスからデータを読み取るための構造体であり、io.Reader
、io.Seeker
、io.ByteScanner
、io.RuneScanner
インターフェースを実装します。
Reader
構造体
type Reader struct {
s []byte // 読み取り元のバイトスライス
i int // 現在の読み取りインデックス
prevRune int // 前回のルーン読み取りの開始インデックス; または < 0
}
s
: 読み取り元のバイトスライスです。このスライスは変更されません。i
: 現在の読み取り位置を示すインデックスです。s[i:]
が未読のデータを示します。prevRune
:ReadRune
が最後に読み取ったルーンの開始インデックスを保持します。UnreadRune
が正しく機能するために使用されます。Read
やReadByte
が呼び出された場合、またはUnreadRune
が呼び出された後は-1
にリセットされます。
メソッドの実装
-
Len() int
:- 未読のバイト数を返します。
len(r.s) - r.i
で計算されます。
-
Read(b []byte) (n int, err error)
:io.Reader
インターフェースの実装です。r.s
の現在の位置r.i
からb
にバイトをコピーします。b
の長さが0の場合、n=0, err=nil
を返します。r.i
がr.s
の長さに達している(つまり、読み取るデータがない)場合、n=0, err=io.EOF
を返します。- コピーされたバイト数
n
だけr.i
を進めます。 prevRune
を-1
にリセットします。
-
ReadByte() (b byte, err error)
:io.ByteScanner
インターフェースの実装です。r.s
の現在の位置r.i
から1バイトを読み取ります。- 読み取るデータがない場合、
err=io.EOF
を返します。 r.i
を1進めます。prevRune
を-1
にリセットします。
-
UnreadByte() error
:io.ByteScanner
インターフェースの実装です。- 最後に読み取ったバイトを「読み取り解除」します。
r.i
が0の場合(スライスの先頭にいる場合)、エラーを返します。r.i
を1戻します。prevRune
を-1
にリセットします。
-
ReadRune() (ch rune, size int, err error)
:io.RuneScanner
インターフェースの実装です。r.s
の現在の位置から次のUTF-8エンコードされたルーンを読み取ります。- 読み取るデータがない場合、
err=io.EOF
を返します。 prevRune
に現在のr.i
を保存します。utf8.DecodeRune
を使用してルーンとバイトサイズをデコードします。r.i
をデコードされたルーンのバイトサイズだけ進めます。
-
UnreadRune() error
:io.RuneScanner
インターフェースの実装です。- 最後に読み取ったルーンを「読み取り解除」します。
prevRune
が-1
の場合(前回の操作がReadRune
でなかった場合)、エラーを返します。r.i
をprevRune
の値に戻します。prevRune
を-1
にリセットします。
-
Seek(offset int64, whence int) (int64, error)
:io.Seeker
インターフェースの実装です。whence
に基づいて新しい絶対オフセットabs
を計算します。io.SeekStart
(0):abs = offset
io.SeekCurrent
(1):abs = int64(r.i) + offset
io.SeekEnd
(2):abs = int64(len(r.s)) + offset
abs
が負の場合、または1<<31
(約2GB)を超える場合、エラーを返します。これは、int
型でインデックスを扱うGoの内部的な制約によるものです。r.i
をint(abs)
に設定します。prevRune
を-1
にリセットします。
-
NewReader(b []byte) *Reader
:- 指定されたバイトスライス
b
から読み取る新しいReader
を初期化して返します。 - 初期の読み取りインデックス
i
は0、prevRune
は-1
に設定されます。
- 指定されたバイトスライス
テストの実装 (reader_test.go
)
reader_test.go
では、bytes.Reader
のSeek
メソッドとRead
メソッドの組み合わせを中心にテストが行われています。様々なオフセットとwhence
の組み合わせでSeek
を呼び出し、その後のRead
操作が期待通りの結果を返すかを確認しています。また、負のオフセットや範囲外のオフセットに対するエラーハンドリングもテストされています。
テストケースは構造体のスライスとして定義されており、各テストケースはoff
(オフセット)、seek
(whence
)、n
(読み取るバイト数)、want
(期待される読み取り結果)、wantpos
(期待されるシーク後の位置)、seekerr
(期待されるシークエラーメッセージ)を含んでいます。これにより、網羅的かつ簡潔なテストが実現されています。
コアとなるコードの変更箇所
このコミットでは、以下の2つの新しいファイルが追加されています。
src/pkg/bytes/reader.go
:bytes.Reader
型とそのメソッドの定義。- 追加行数: 110行
src/pkg/bytes/reader_test.go
:bytes.Reader
のテストコード。- 追加行数: 58行
合計で168行が追加されています。既存のファイルへの変更はありません。
コアとなるコードの解説
src/pkg/bytes/reader.go
このファイルは、bytes.Reader
の核心部分を定義しています。
// Copyright 2012 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package bytes
import (
"errors"
"io"
"unicode/utf8"
)
// A Reader implements the io.Reader, io.Seeker, io.ByteScanner, and
// io.RuneScanner interfaces by reading from a byte slice.
// Unlike a Buffer, a Reader is read-only and supports seeking.
type Reader struct {
s []byte // 読み取り元のバイトスライス
i int // 現在の読み取りインデックス
prevRune int // 前回のルーン読み取りの開始インデックス; または < 0
}
// Len returns the number of bytes of the unread portion of the
// slice.
func (r *Reader) Len() int {
if r.i >= len(r.s) {
return 0
}
return len(r.s) - r.i
}
func (r *Reader) Read(b []byte) (n int, err error) {
if len(b) == 0 {
return 0, nil
}
if r.i >= len(r.s) {
return 0, io.EOF
}
n = copy(b, r.s[r.i:])
r.i += n
r.prevRune = -1 // Read操作でprevRuneをリセット
return
}
func (r *Reader) ReadByte() (b byte, err error) {
if r.i >= len(r.s) {
return 0, io.EOF
}
b = r.s[r.i]
r.i++
r.prevRune = -1 // ReadByte操作でprevRuneをリセット
return
}
func (r *Reader) UnreadByte() error {
if r.i <= 0 {
return errors.New("bytes.Reader: at beginning of slice")
}
r.i--
r.prevRune = -1 // UnreadByte操作でprevRuneをリセット
return nil
}
func (r *Reader) ReadRune() (ch rune, size int, err error) {
if r.i >= len(r.s) {
return 0, 0, io.EOF
}
r.prevRune = r.i // ReadRune操作でprevRuneを保存
if c := r.s[r.i]; c < utf8.RuneSelf { // ASCII文字の高速パス
r.i++
return rune(c), 1, nil
}
ch, size = utf8.DecodeRune(r.s[r.i:])
r.i += size
return
}
func (r *Reader) UnreadRune() error {
if r.prevRune < 0 {
return errors.New("bytes.Reader: previous operation was not ReadRune")
}
r.i = r.prevRune // prevRuneに戻す
r.prevRune = -1 // UnreadRune操作でprevRuneをリセット
return nil
}
// Seek implements the io.Seeker interface.
func (r *Reader) Seek(offset int64, whence int) (int64, error) {
var abs int64
switch whence {
case 0: // io.SeekStart
abs = offset
case 1: // io.SeekCurrent
abs = int64(r.i) + offset
case 2: // io.SeekEnd
abs = int64(len(r.s)) + offset
default:
return 0, errors.New("bytes: invalid whence")
}
if abs < 0 {
return 0, errors.New("bytes: negative position")
}
// 32-bit intの最大値を超えるオフセットはエラーとする
// これは、内部のインデックスr.iがint型であるため
if abs >= 1<<31 {
return 0, errors.New("bytes: position out of range")
}
r.i = int(abs) // intにキャスト
return abs, nil
}
// NewReader returns a new Reader reading from b.
func NewReader(b []byte) *Reader { return &Reader{b, 0, -1} }
このコードは、Reader
構造体の定義と、io
パッケージの各種インターフェースを実装するメソッド群で構成されています。特に注目すべきは、Read
、ReadByte
、ReadRune
、Seek
といった読み取り操作を行うメソッドが、prevRune
フィールドを適切に管理している点です。これにより、UnreadByte
やUnreadRune
が正しく機能し、読み取り位置を元に戻すことが可能になります。Seek
メソッドでは、int
型のインデックスの制約から、オフセットが1<<31
(約2GB)を超える場合にエラーを返すようになっています。
src/pkg/bytes/reader_test.go
このファイルは、bytes.Reader
の機能が期待通りに動作するかを検証するためのテストコードです。
// Copyright 2012 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package bytes_test
import (
. "bytes" // bytesパッケージをインポートし、そのエクスポートされた識別子を直接使用できるようにする
"os"
"testing"
)
func TestReader(t *testing.T) {
r := NewReader([]byte("0123456789")) // テスト対象のReaderを初期化
tests := []struct {
off int64 // Seekのオフセット
seek int // Seekのwhence
n int // Readのバイト数
want string // Readの期待結果
wantpos int64 // Seek後の期待位置
seekerr string // Seekの期待エラーメッセージ
}{
// 各種SeekとReadの組み合わせをテスト
{seek: os.SEEK_SET, off: 0, n: 20, want: "0123456789"}, // 全体を読み取る
{seek: os.SEEK_SET, off: 1, n: 1, want: "1"}, // 1バイト目から1バイト読み取る
{seek: os.SEEK_CUR, off: 1, wantpos: 3, n: 2, want: "34"}, // 現在位置から1進んで2バイト読み取る
{seek: os.SEEK_SET, off: -1, seekerr: "bytes: negative position"}, // 負のオフセットでエラー
{seek: os.SEEK_SET, off: 1<<31 - 1}, // 巨大なオフセット(エラーにならない境界値)
{seek: os.SEEK_CUR, off: 1, seekerr: "bytes: position out of range"}, // 範囲外のオフセットでエラー
{seek: os.SEEK_SET, n: 5, want: "01234"}, // シークなしで5バイト読み取る
{seek: os.SEEK_CUR, n: 5, want: "56789"}, // 現在位置から5バイト読み取る
{seek: os.SEEK_END, off: -1, n: 1, wantpos: 9, want: "9"}, // 末尾から1戻って1バイト読み取る
}
for i, tt := range tests {
pos, err := r.Seek(tt.off, tt.seek) // Seekを実行
if err == nil && tt.seekerr != "" {
t.Errorf("%d. want seek error %q", i, tt.seekerr)
continue
}
if err != nil && err.Error() != tt.seekerr {
t.Errorf("%d. seek error = %q; want %q", i, err.Error(), tt.seekerr)
continue
}
if tt.wantpos != 0 && tt.wantpos != pos { // シーク後の位置を検証
t.Errorf("%d. pos = %d, want %d", i, pos, tt.wantpos)
}
buf := make([]byte, tt.n)
n, err := r.Read(buf) // Readを実行
if err != nil {
t.Errorf("%d. read = %v", i, err)
continue
}
got := string(buf[:n])
if got != tt.want { // 読み取り結果を検証
t.Errorf("%d. got %q; want %q", i, got, tt.want)
}
}
}
このテストコードは、bytes.Reader
の主要な機能であるSeek
とRead
の連携を広範囲にわたって検証しています。特に、Seek
のwhence
引数(os.SEEK_SET
, os.SEEK_CUR
, os.SEEK_END
)の異なる組み合わせや、エラーケース(負のオフセット、範囲外のオフセット)が網羅されています。テストはテーブル駆動型テストの形式で記述されており、各テストケースが独立して実行され、期待される結果と比較されます。
関連リンク
参考にした情報源リンク
- Go言語の
io.Reader
とio.EOF
の挙動に関する議論(Issue #21852): https://github.com/golang/go/issues/21852 - Go言語の
io.Reader
とio.EOF
の挙動に関する議論(Issue #59253): https://github.com/golang/go/issues/59253- これらのIssueは、
bytes.Reader.Read
がio.EOF
を返すタイミングに関する一般的なGoのio.Reader
の挙動について議論しており、bytes.Reader
の設計思想を理解する上で参考になります。
- これらのIssueは、