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

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

このコミットは、Go言語の標準ライブラリであるbytesパッケージにReader型を新しく追加するものです。bytes.Readerは、バイトスライス([]byte)からデータを読み取るためのio.Readerio.Seekerio.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.gostrings/reader_test.goのコードをコピーし、文字列(string)をバイトスライス([]byte)に置き換えることで実装されています。

この変更は、GoのIssue #2861を修正するものです。また、コードレビューはadgrscrによって行われ、golang-devメーリングリストにもCCされています。関連するGoのコードレビュー(CL)のリンクはhttps://golang.org/cl/5666044ですが、このCL番号は公開されているGoのGerritリポジトリでは見つかりませんでした。

変更の背景

Go言語では、ioパッケージが提供するインターフェース(io.Readerio.Writerなど)を通じて、様々なデータソースからの読み書きを抽象化しています。既存のstrings.Readerは、メモリ上の文字列をio.Readerとして扱うことを可能にしていましたが、同様の機能がバイトスライスに対しても求められていました。

バイトスライスは、ファイルの内容、ネットワークからのデータ、または他の処理結果など、様々なバイナリデータを表現するためによく使用されます。これらのバイトスライスをio.Readerとして扱うことで、io.Copybufio.Scannerなど、io.Readerインターフェースを受け入れる既存の多くの関数やライブラリとシームレスに連携できるようになります。

このbytes.Readerの追加により、開発者はバイトスライスをあたかもファイルやネットワークストリームのように扱うことができ、Goの強力なioパッケージのエコシステムを最大限に活用できるようになります。特に、バイトスライスから特定のオフセットで読み取りを開始したり、読み取り位置をシークしたりする機能は、バイナリデータの解析や処理において非常に有用です。

コミットメッセージにあるFixes #2861は、この機能の必要性を示すGoのIssueが存在したことを示唆しています。具体的なIssue #2861の内容は不明ですが、bytes.Readerが提供する機能(バイトスライスからの読み取り、シーク、バイト/ルーン単位の読み取り)が、当時のGo開発者コミュニティで求められていたことを示しています。

前提知識の解説

このコミットを理解するためには、Go言語の以下の基本的な概念とインターフェースについて理解しておく必要があります。

  1. io.Readerインターフェース:

    • Read(p []byte) (n int, err error) メソッドを持つインターフェースです。
    • データソースからバイトを読み取り、pに書き込みます。
    • nは読み取ったバイト数、errはエラー(通常は読み取りが終了したことを示すio.EOF)を返します。
    • Goにおける入力操作の基本的な抽象化です。
  2. io.Seekerインターフェース:

    • Seek(offset int64, whence int) (int64, error) メソッドを持つインターフェースです。
    • データソースの現在の読み取り/書き込みオフセットを変更します。
    • offsetは移動するバイト数、whenceはオフセットの基準位置(io.SeekStartio.SeekCurrentio.SeekEnd)を指定します。
    • io.SeekStart (0): オフセットはファイルの先頭からの絶対位置。
    • io.SeekCurrent (1): オフセットは現在の位置からの相対位置。
    • io.SeekEnd (2): オフセットはファイルの末尾からの相対位置。
    • 戻り値は新しいオフセットの絶対位置です。
  3. io.ByteScannerインターフェース:

    • ReadByte() (byte, error)UnreadByte() error メソッドを持つインターフェースです。
    • ReadByteは次の1バイトを読み取ります。
    • UnreadByteは最後に読み取ったバイトを「読み取り解除」し、次の読み取り操作で同じバイトが返されるようにします。これは通常、パーサーなどでトークンを読み進める際に、読みすぎた場合に前の状態に戻るために使用されます。
  4. io.RuneScannerインターフェース:

    • ReadRune() (r rune, size int, err error)UnreadRune() error メソッドを持つインターフェースです。
    • ReadRuneは次のUTF-8エンコードされたルーン(Unicodeコードポイント)を読み取ります。rはルーン、sizeはそのルーンをエンコードするために使用されたバイト数です。
    • UnreadRuneは最後に読み取ったルーンを「読み取り解除」します。io.ByteScannerUnreadByteと同様の目的で使用されます。
  5. io.EOF:

    • io.EOFは、io.ReaderReadメソッドが、それ以上読み取るデータがない場合に返す特別なエラー値です。
    • Readメソッドは、読み取ったバイト数nが0でerrio.EOFの場合、ストリームの終わりに達したことを示します。
    • ただし、Readメソッドは、一部のバイトを読み取った後にストリームの終わりに達した場合でも、n > 0err == nilを返すことがあります。この場合、次のRead呼び出しでio.EOFが返されます。この挙動は、bytes.Readerを含む多くのio.Reader実装で共通しており、Goのioパッケージの設計原則に基づいています。
  6. strings.Reader:

    • stringsパッケージに存在する型で、文字列(string)をio.Readerio.Seekerio.ByteScannerio.RuneScannerとして扱うことができます。
    • bytes.Readerは、このstrings.Readerの機能をバイトスライスに拡張したものです。

技術的詳細

bytes.Readerは、バイトスライスからデータを読み取るための構造体であり、io.Readerio.Seekerio.ByteScannerio.RuneScannerインターフェースを実装します。

Reader構造体

type Reader struct {
	s        []byte // 読み取り元のバイトスライス
	i        int    // 現在の読み取りインデックス
	prevRune int    // 前回のルーン読み取りの開始インデックス; または < 0
}
  • s: 読み取り元のバイトスライスです。このスライスは変更されません。
  • i: 現在の読み取り位置を示すインデックスです。s[i:]が未読のデータを示します。
  • prevRune: ReadRuneが最後に読み取ったルーンの開始インデックスを保持します。UnreadRuneが正しく機能するために使用されます。ReadReadByteが呼び出された場合、またはUnreadRuneが呼び出された後は-1にリセットされます。

メソッドの実装

  1. Len() int:

    • 未読のバイト数を返します。
    • len(r.s) - r.iで計算されます。
  2. Read(b []byte) (n int, err error):

    • io.Readerインターフェースの実装です。
    • r.sの現在の位置r.iからbにバイトをコピーします。
    • bの長さが0の場合、n=0, err=nilを返します。
    • r.ir.sの長さに達している(つまり、読み取るデータがない)場合、n=0, err=io.EOFを返します。
    • コピーされたバイト数nだけr.iを進めます。
    • prevRune-1にリセットします。
  3. ReadByte() (b byte, err error):

    • io.ByteScannerインターフェースの実装です。
    • r.sの現在の位置r.iから1バイトを読み取ります。
    • 読み取るデータがない場合、err=io.EOFを返します。
    • r.iを1進めます。
    • prevRune-1にリセットします。
  4. UnreadByte() error:

    • io.ByteScannerインターフェースの実装です。
    • 最後に読み取ったバイトを「読み取り解除」します。
    • r.iが0の場合(スライスの先頭にいる場合)、エラーを返します。
    • r.iを1戻します。
    • prevRune-1にリセットします。
  5. ReadRune() (ch rune, size int, err error):

    • io.RuneScannerインターフェースの実装です。
    • r.sの現在の位置から次のUTF-8エンコードされたルーンを読み取ります。
    • 読み取るデータがない場合、err=io.EOFを返します。
    • prevRuneに現在のr.iを保存します。
    • utf8.DecodeRuneを使用してルーンとバイトサイズをデコードします。
    • r.iをデコードされたルーンのバイトサイズだけ進めます。
  6. UnreadRune() error:

    • io.RuneScannerインターフェースの実装です。
    • 最後に読み取ったルーンを「読み取り解除」します。
    • prevRune-1の場合(前回の操作がReadRuneでなかった場合)、エラーを返します。
    • r.iprevRuneの値に戻します。
    • prevRune-1にリセットします。
  7. 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.iint(abs)に設定します。
    • prevRune-1にリセットします。
  8. NewReader(b []byte) *Reader:

    • 指定されたバイトスライスbから読み取る新しいReaderを初期化して返します。
    • 初期の読み取りインデックスiは0、prevRune-1に設定されます。

テストの実装 (reader_test.go)

reader_test.goでは、bytes.ReaderSeekメソッドとReadメソッドの組み合わせを中心にテストが行われています。様々なオフセットとwhenceの組み合わせでSeekを呼び出し、その後のRead操作が期待通りの結果を返すかを確認しています。また、負のオフセットや範囲外のオフセットに対するエラーハンドリングもテストされています。

テストケースは構造体のスライスとして定義されており、各テストケースはoff(オフセット)、seekwhence)、n(読み取るバイト数)、want(期待される読み取り結果)、wantpos(期待されるシーク後の位置)、seekerr(期待されるシークエラーメッセージ)を含んでいます。これにより、網羅的かつ簡潔なテストが実現されています。

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

このコミットでは、以下の2つの新しいファイルが追加されています。

  1. src/pkg/bytes/reader.go: bytes.Reader型とそのメソッドの定義。
    • 追加行数: 110行
  2. 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パッケージの各種インターフェースを実装するメソッド群で構成されています。特に注目すべきは、ReadReadByteReadRuneSeekといった読み取り操作を行うメソッドが、prevRuneフィールドを適切に管理している点です。これにより、UnreadByteUnreadRuneが正しく機能し、読み取り位置を元に戻すことが可能になります。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の主要な機能であるSeekReadの連携を広範囲にわたって検証しています。特に、Seekwhence引数(os.SEEK_SET, os.SEEK_CUR, os.SEEK_END)の異なる組み合わせや、エラーケース(負のオフセット、範囲外のオフセット)が網羅されています。テストはテーブル駆動型テストの形式で記述されており、各テストケースが独立して実行され、期待される結果と比較されます。

関連リンク

参考にした情報源リンク

  • Go言語のio.Readerio.EOFの挙動に関する議論(Issue #21852): https://github.com/golang/go/issues/21852
  • Go言語のio.Readerio.EOFの挙動に関する議論(Issue #59253): https://github.com/golang/go/issues/59253
    • これらのIssueは、bytes.Reader.Readio.EOFを返すタイミングに関する一般的なGoのio.Readerの挙動について議論しており、bytes.Readerの設計思想を理解する上で参考になります。