[インデックス 18901] ファイルの概要
このコミットは、Go言語の標準ライブラリであるbytes
パッケージとstrings
パッケージ内のReader
型におけるUnreadRune
メソッドのバグ修正に関するものです。具体的には、UnreadRune
が直前の操作がReadRune
でなかった場合にエラーを返すように修正されています。これにより、UnreadRune
の予期せぬ動作を防ぎ、APIの契約をより厳密に遵守するようになります。
コミット
commit a509026ff0010dc29068983bd748c1360e692602
Author: Rui Ueyama <ruiu@google.com>
Date: Wed Mar 19 09:00:58 2014 -0700
strings, bytes: fix Reader.UnreadRune
UnreadRune should return an error if previous operation is not
ReadRune.
Fixes #7579.
LGTM=bradfitz
R=golang-codereviews, bradfitz
CC=golang-codereviews
https://golang.org/cl/77580046
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/a509026ff0010dc29068983bd748c1360e692602
元コミット内容
strings, bytes: fix Reader.UnreadRune
UnreadRune
は、直前の操作がReadRune
でなかった場合にエラーを返すようにすべきである。
Fixes #7579.
変更の背景
Go言語のio
パッケージには、データの読み書きを行うための様々なインターフェースが定義されています。その中でも、io.Reader
はバイト列を読み込むための基本的なインターフェースであり、io.ByteReader
やio.RuneReader
はそれぞれ1バイトや1Unicodeコードポイント(rune)を読み込むための拡張インターフェースです。
bytes.Reader
とstrings.Reader
は、それぞれバイトスライスと文字列をデータソースとして、これらのio
インターフェースを実装しています。これらのリーダーには、読み込んだデータを「元に戻す」ためのUnreadByte
やUnreadRune
といったメソッドが提供されています。
しかし、UnreadRune
メソッドのこれまでの実装では、直前の操作がReadRune
でなかった場合でもエラーを返さずに、不正な状態になる可能性がありました。これは、UnreadRune
が「直前に読み込んだruneをストリームに戻す」という操作であるため、直前にReadRune
以外の操作(例えばRead
、ReadByte
、Seek
など)が行われていた場合、どのruneを戻すべきか不明確になり、予期せぬ動作やデータ破損につながる恐れがありました。
この問題は、GoのIssue #7579として報告されており、このコミットはその問題を解決するために行われました。UnreadRune
の正しいセマンティクスは、直前の操作がReadRune
であった場合にのみ成功し、それ以外の場合はエラーを返すことです。これにより、リーダーの状態の一貫性が保たれ、より堅牢なコードの記述が可能になります。
前提知識の解説
このコミットを理解するためには、以下のGo言語の概念と標準ライブラリの知識が必要です。
1. io
パッケージとリーダーインターフェース
Go言語のio
パッケージは、I/Oプリミティブを提供します。
io.Reader
:Read(p []byte) (n int, err error)
メソッドを持つインターフェースで、バイトスライスp
にデータを読み込みます。io.ByteReader
:ReadByte() (byte, error)
メソッドを持つインターフェースで、1バイトを読み込みます。io.RuneReader
:ReadRune() (r rune, size int, err error)
メソッドを持つインターフェースで、1つのUnicodeコードポイント(rune)とそのバイトサイズを読み込みます。io.Seeker
:Seek(offset int64, whence int) (int64, error)
メソッドを持つインターフェースで、リーダーの読み取り位置を変更します。
2. bytes.Reader
とstrings.Reader
bytes.Reader
:[]byte
(バイトスライス)を読み取り元とするリーダーです。io.Reader
,io.ByteReader
,io.RuneReader
,io.Seeker
などを実装しています。strings.Reader
:string
(文字列)を読み取り元とするリーダーです。bytes.Reader
と同様に、io.Reader
,io.ByteReader
,io.RuneReader
,io.Seeker
などを実装しています。
これらのリーダーは、内部に現在の読み取り位置を示すインデックス(r.i
)と、直前にReadRune
で読み込んだruneの開始位置を記憶するためのフィールド(r.prevRune
)を持っています。
3. UnreadRune
メソッドのセマンティクス
UnreadRune
メソッドは、io.RuneReader
インターフェースを実装する型が提供する可能性のあるメソッドで、直前にReadRune
によって読み込まれたruneをストリームに「戻す」ことを目的としています。これにより、次にReadRune
が呼び出されたときに、同じruneが再度読み込まれるようになります。
重要なのは、UnreadRune
は直前の操作がReadRune
であった場合にのみ有効であるという点です。もし直前の操作がReadRune
以外であった場合、UnreadRune
はどのruneを戻すべきかを知ることができません。このような状況でUnreadRune
が呼び出された場合、エラーを返すのが正しい振る舞いです。これは、io.Reader
や関連するインターフェースの設計原則に基づいています。
4. r.prevRune
フィールド
bytes.Reader
とstrings.Reader
の内部には、prevRune
というフィールドが存在します。このフィールドは、ReadRune
が呼び出された際に、読み込んだruneの開始インデックスを記憶するために使用されます。UnreadRune
はこのprevRune
の値を利用して、読み取り位置を元に戻します。
このコミットの修正の核心は、ReadRune
以外の操作が行われた際に、このprevRune
フィールドを無効な値(通常は-1
)にリセットすることです。これにより、UnreadRune
が呼び出されたときにprevRune
が有効な値でなければエラーを返すというロジックが正しく機能するようになります。
技術的詳細
このコミットの技術的な詳細は、bytes.Reader
とstrings.Reader
の各読み取りメソッドにおけるr.prevRune
フィールドの管理方法の変更に集約されます。
Goのio
パッケージにおけるUnreadRune
の一般的なセマンティクスは、直前の操作がReadRune
であった場合にのみ成功するというものです。それ以外の操作(Read
、ReadAt
、ReadByte
、Seek
など)がReadRune
の後に実行された場合、UnreadRune
はエラーを返すべきです。
このコミット以前のbytes.Reader
とstrings.Reader
の実装では、ReadRune
以外のメソッドが呼び出された際にr.prevRune
が適切にリセットされていませんでした。そのため、例えばReadRune
の後にReadByte
が呼び出された場合でも、r.prevRune
にはReadRune
が読み込んだruneの開始位置が残ったままでした。この状態でUnreadRune
を呼び出すと、UnreadRune
はr.prevRune
が有効であると判断し、誤って読み取り位置を戻してしまう可能性がありました。
このコミットでは、この問題を解決するために、ReadRune
以外のすべての読み取り操作(Read
、ReadAt
、ReadByte
)および位置変更操作(Seek
)の開始時に、r.prevRune
を無効な値である-1
に設定するように変更されました。
具体的には、以下のメソッドの冒頭にr.prevRune = -1
が追加されています。
func (r *Reader) Read(b []byte) (n int, err error)
func (r *Reader) ReadAt(b []byte, off int64) (n int, err error)
func (r *Reader) ReadByte() (b byte, err error)
func (r *Reader) Seek(offset int64, whence int) (int64, error)
また、ReadRune
メソッドのio.EOF
を返すパス(つまり、読み取り対象のデータがもうない場合)でもr.prevRune = -1
が設定されるようになりました。これは、ReadRune
がエラーを返した場合も、その後のUnreadRune
が成功しないようにするためです。
この変更により、r.prevRune
は常に直前の操作がReadRune
であった場合にのみ有効な値を持つようになります。UnreadRune
メソッドは、r.prevRune
が-1
である場合にエラーを返すロジックを既に持っているため、この修正によってUnreadRune
の動作が期待通りになります。
さらに、この修正の正しさを検証するために、bytes/reader_test.go
とstrings/strings_test.go
に新しいテストケースTestUnreadRuneError
が追加されました。このテストは、ReadRune
の後にRead
、ReadAt
、ReadByte
、UnreadRune
(二重呼び出し)、Seek
、WriteTo
といった操作を行い、その後にUnreadRune
を呼び出した場合にエラーが返されることを確認します。これにより、UnreadRune
のセマンティクスが正しく実装されたことが保証されます。
コアとなるコードの変更箇所
このコミットにおけるコアとなるコードの変更は、src/pkg/bytes/reader.go
とsrc/pkg/strings/reader.go
内の複数のメソッドにr.prevRune = -1
の行が追加された点です。
src/pkg/bytes/reader.go
--- a/src/pkg/bytes/reader.go
+++ b/src/pkg/bytes/reader.go
@@ -30,6 +30,7 @@ func (r *Reader) Len() int {
}
func (r *Reader) Read(b []byte) (n int, err error) {
+ r.prevRune = -1 // 追加
if len(b) == 0 {
return 0, nil
}
@@ -38,11 +39,11 @@ func (r *Reader) Read(b []byte) (n int, err error) {
}
n = copy(b, r.s[r.i:])
r.i += n
- r.prevRune = -1 // 削除
return
}
func (r *Reader) ReadAt(b []byte, off int64) (n int, err error) {
+ r.prevRune = -1 // 追加
if off < 0 {
return 0, errors.New("bytes: invalid offset")
}
@@ -57,12 +58,12 @@ func (r *Reader) ReadAt(b []byte, off int64) (n int, err error) {
}
func (r *Reader) ReadByte() (b byte, err error) {
+ r.prevRune = -1 // 追加
if r.i >= len(r.s) {
return 0, io.EOF
}
b = r.s[r.i]
r.i++
- r.prevRune = -1 // 削除
return
}
@@ -77,6 +78,7 @@ func (r *Reader) UnreadByte() error {
func (r *Reader) ReadRune() (ch rune, size int, err error) {
if r.i >= len(r.s) {
+ r.prevRune = -1 // 追加
return 0, 0, io.EOF
}
r.prevRune = r.i
@@ -100,6 +102,7 @@ func (r *Reader) UnreadRune() error {
// Seek implements the io.Seeker interface.
func (r *Reader) Seek(offset int64, whence int) (int64, error) {
+ r.prevRune = -1 // 追加
var abs int64
switch whence {
case 0:
src/pkg/strings/reader.go
同様の変更がstrings/reader.go
にも適用されています。
--- a/src/pkg/strings/reader.go
+++ b/src/pkg/strings/reader.go
@@ -29,6 +29,7 @@ func (r *Reader) Len() int {
}
func (r *Reader) Read(b []byte) (n int, err error) {
+ r.prevRune = -1 // 追加
if len(b) == 0 {
return 0, nil
}
@@ -37,11 +38,11 @@ func (r *Reader) Read(b []byte) (n int, err error) {
}
n = copy(b, r.s[r.i:])
r.i += n
- r.prevRune = -1 // 削除
return
}
func (r *Reader) ReadAt(b []byte, off int64) (n int int, err error) {
+ r.prevRune = -1 // 追加
if off < 0 {
return 0, errors.New("strings: invalid offset")
}
@@ -56,12 +57,12 @@ func (r *Reader) ReadAt(b []byte, off int64) (n int, err error) {
}
func (r *Reader) ReadByte() (b byte, err error) {
+ r.prevRune = -1 // 追加
if r.i >= len(r.s) {
return 0, io.EOF
}
b = r.s[r.i]
r.i++
- r.prevRune = -1 // 削除
return
}
@@ -76,6 +77,7 @@ func (r *Reader) UnreadByte() error {
func (r *Reader) ReadRune() (ch rune, size int, err error) {
if r.i >= len(r.s) {
+ r.prevRune = -1 // 追加
return 0, 0, io.EOF
}
r.prevRune = r.i
@@ -99,6 +101,7 @@ func (r *Reader) UnreadRune() error {
// Seek implements the io.Seeker interface.
func (r *Reader) Seek(offset int64, whence int) (int64, error) {
+ r.prevRune = -1 // 追加
var abs int64
switch whence {
case 0:
テストファイルの追加
src/pkg/bytes/reader_test.go
とsrc/pkg/strings/strings_test.go
に、TestUnreadRuneError
という新しいテスト関数が追加されました。このテストは、ReadRune
の後にRead
、ReadAt
、ReadByte
、UnreadRune
(二重呼び出し)、Seek
、WriteTo
といったUnreadRune
の前提条件を壊す可能性のある操作を実行し、その後にUnreadRune
を呼び出した場合にエラーが返されることを検証します。
// bytes/reader_test.go および strings/strings_test.go に追加されたテスト
var UnreadRuneErrorTests = []struct {
name string
f func(*Reader)
}{
{"Read", func(r *Reader) { r.Read([]byte{}) }},
{"ReadAt", func(r *Reader) { r.ReadAt([]byte{}, 0) }},
{"ReadByte", func(r *Reader) { r.ReadByte() }},
{"UnreadRune", func(r *Reader) { r.UnreadRune() }}, // 二重UnreadRuneのテスト
{"Seek", func(r *Reader) { r.Seek(0, 1) }},
{"WriteTo", func(r *Reader) { r.WriteTo(&Buffer{}) }}, // bytes.Buffer または bytes.Buffer{}
}
func TestUnreadRuneError(t *testing.T) {
for _, tt := range UnreadRuneErrorTests {
reader := NewReader([]byte("0123456789")) // stringsの場合は NewReader("0123456789")
if _, _, err := reader.ReadRune(); err != nil {
// should not happen
t.Fatal(err)
}
tt.f(reader) // ReadRune以外の操作を実行
err := reader.UnreadRune()
if err == nil {
t.Errorf("Unreading after %s: expected error", tt.name)
}
}
}
コアとなるコードの解説
このコミットの核心は、Reader
構造体のprevRune
フィールドの厳密な管理にあります。
bytes.Reader
とstrings.Reader
は、内部に以下のフィールドを持っています。
s []byte
またはs string
: 読み取り元のデータ。i int64
: 現在の読み取り位置(インデックス)。prevRune int
: 直前にReadRune
で読み込んだruneの開始インデックス。ReadRune
以外の操作が行われた場合や、UnreadRune
が呼び出された場合は-1
にリセットされます。
UnreadRune
メソッドは、prevRune
が-1
でない場合にのみ、i
をprevRune
の値に戻し、prevRune
を-1
にリセットします。もしprevRune
が-1
であれば、UnreadRune
はエラー(ErrInvalidUnread
)を返します。
このコミット以前は、Read
、ReadAt
、ReadByte
、Seek
といったメソッドが呼び出されても、prevRune
はリセットされませんでした。そのため、ReadRune
の後にこれらのメソッドが呼び出されても、prevRune
には以前のReadRune
の開始位置が残ったままでした。この状態でUnreadRune
を呼び出すと、prevRune
が-1
ではないため、UnreadRune
は成功してしまい、誤った位置にリーダーのインデックスを戻してしまう可能性がありました。これは、UnreadRune
が「直前のReadRune
操作を元に戻す」というセマンティクスに反します。
今回の修正では、Read
、ReadAt
、ReadByte
、Seek
の各メソッドの冒頭でr.prevRune = -1
と明示的に設定することで、これらの操作が行われた直後にはprevRune
が必ず無効な状態になるようにしました。これにより、これらの操作の後にUnreadRune
が呼び出された場合、prevRune
が-1
であるため、UnreadRune
は正しくエラーを返すようになります。
また、ReadRune
がio.EOF
を返す場合(つまり、読み取るデータがない場合)にもr.prevRune = -1
を設定するようになりました。これは、ReadRune
がエラーを返した後にUnreadRune
が呼び出されても、それが成功しないようにするためです。
この変更は、bytes.Reader
とstrings.Reader
のUnreadRune
メソッドの堅牢性を高め、APIの契約をより厳密に遵守させるための重要な修正です。これにより、これらのリーダーを使用するコードがより予測可能で安全になります。
関連リンク
- Go Issue #7579:
bytes, strings: Reader.UnreadRune should return error if previous operation is not ReadRune
- Gerrit Change-Id:
I2222222222222222222222222222222222222222
(コミットメッセージに記載のhttps://golang.org/cl/77580046
に対応するGerritのChange-Id)
参考にした情報源リンク
- Go言語の
io
パッケージのドキュメント: - Go言語の
bytes
パッケージのドキュメント: - Go言語の
strings
パッケージのドキュメント: - Go言語の
Reader
インターフェースに関する一般的な情報源 (例: Go by Example - Readers): - Go言語のUnicodeとruneに関する情報源:
- Go言語のテストに関する情報源: