[インデックス 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 = offsetio.SeekCurrent(1):abs = int64(r.i) + offsetio.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は、