[インデックス 18986] ファイルの概要
このコミットは、Go言語の標準ライブラリであるbytes
パッケージとstrings
パッケージ内のReader
型に対する変更を含んでいます。具体的には、bytes.Reader
とstrings.Reader
が、それぞれバイトスライスと文字列からデータを読み取る際に使用する内部の読み取りインデックスの型を変更し、それに伴う関連メソッドの修正とテストの追加が行われています。
コミット
このコミットは、bytes
パッケージとstrings
パッケージのReader
型において、Seek
操作が1<<31
(約2GB)を超えるオフセットを扱えるようにするものです。これにより、大きなデータストリームやファイルに対するシーク操作の制限が緩和され、より大きなデータを効率的に処理できるようになります。
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/f074565158ed611d7324de8e1297b103b5ed23f9
元コミット内容
commit f074565158ed611d7324de8e1297b103b5ed23f9
Author: Brad Fitzpatrick <bradfitz@golang.org>
Date: Fri Mar 28 12:23:51 2014 -0700
bytes, strings: allow Reader.Seek past 1<<31
Fixes #7654
LGTM=rsc
R=rsc, dan.kortschak
CC=golang-codereviews
https://golang.org/cl/81530043
変更の背景
この変更の背景には、Go言語のbytes.Reader
およびstrings.Reader
が持つ、内部の読み取り位置を示すインデックスがint
型で定義されていたことによる制限がありました。Goにおけるint
型は、実行環境のアーキテクチャ(32ビットまたは64ビット)によってサイズが異なります。32ビットシステムではint
は32ビット整数として扱われるため、その最大値は2^31 - 1
(約2GB)となります。
これにより、bytes.Reader
やstrings.Reader
を使用して2GBを超えるバイトスライスや文字列を扱う際に、Seek
メソッドで2GBを超えるオフセットを指定しようとすると、「position out of range」というエラーが発生し、正しくシークできないという問題がありました。これは、特に大きなファイルやメモリマップされたデータ、ネットワークストリームなどを扱うアプリケーションにおいて、深刻な制約となっていました。
コミットメッセージにあるFixes #7654
は、この問題がGoのIssueトラッカーで報告されていたことを示唆しています。具体的なIssueの内容は確認できませんでしたが、このコミットは、この2GBのシーク制限を解消し、より大きなデータセットに対する柔軟な操作を可能にすることを目的としています。
前提知識の解説
io.Reader
とio.Seeker
インターフェース
Go言語では、データの読み書き操作はインターフェースによって抽象化されています。
io.Reader
: データを読み取るための基本的なインターフェースです。Read(p []byte) (n int, err error)
メソッドを持ち、バイトスライスp
にデータを読み込み、読み込んだバイト数n
とエラーerr
を返します。io.Seeker
: データの読み取り位置(オフセット)を変更するためのインターフェースです。Seek(offset int64, whence int) (int64, error)
メソッドを持ちます。offset
: シークするオフセット量。whence
: シークの基準位置。io.SeekStart
(ファイルの先頭から)、io.SeekCurrent
(現在の位置から)、io.SeekEnd
(ファイルの末尾から)のいずれかを指定します。- 戻り値は、新しい絶対オフセットとエラーです。
bytes.Reader
とstrings.Reader
は、これらのインターフェースを実装しており、バイトスライスや文字列をあたかもファイルのように扱うことができます。
Goにおけるint
とint64
int
: Goのint
型は、プラットフォームに依存する整数型です。32ビットシステムでは32ビット幅、64ビットシステムでは64ビット幅を持ちます。そのため、32ビットシステムではint
の最大値は約20億(2^31 - 1
)です。int64
:int64
型は、常に64ビット幅を持つ整数型です。そのため、その最大値は非常に大きく、約9×10^18(2^63 - 1
)です。
この違いが、本コミットの変更の核心となります。以前はint
で管理されていた読み取りインデックスが、int64
に変更されることで、32ビットシステムでも2GBを超えるオフセットを扱えるようになります。
1<<31
とは
1<<31
は、ビットシフト演算子を使った表現で、「1を左に31ビットシフトする」という意味です。これは2^31
に等しく、符号付き32ビット整数の最大値(2^31 - 1
)の次の値、つまりオーバーフローが発生する境界値を示します。以前のコードでは、この値がシーク可能な最大オフセットのチェックに使われていました。
技術的詳細
このコミットの主要な技術的変更点は、bytes.Reader
とstrings.Reader
の内部状態を管理するi
フィールド(現在の読み取りインデックス)の型をint
からint64
に変更したことです。この変更により、Reader
は2GBを超えるオフセットを正確に追跡できるようになりました。
型変更に伴い、以下の修正が行われています。
-
Reader
構造体のi
フィールドの型変更:type Reader struct { ... i int ... }
からtype Reader struct { ... i int64 ... }
へ変更。
-
Len()
メソッドの修正:r.i
とlen(r.s)
の比較において、len(r.s)
がint
型であるため、int64(len(r.s))
と明示的に型変換してr.i
(int64
)と比較するように変更。- 戻り値の計算でも、
int(int64(len(r.s)) - r.i)
のようにint
への型変換が行われています。これは、Len()
メソッドの戻り値がint
型であるためです。
-
Read()
、ReadByte()
、ReadRune()
、UnreadRune()
、WriteTo()
メソッドの修正:r.i
の更新(例:r.i += n
)において、n
やsize
がint
型であるため、int64(n)
やint64(size)
のようにint64
へ型変換してから加算するように変更。r.prevRune
は引き続きint
型ですが、UnreadRune()
でr.i
に代入する際にはint64(r.prevRune)
と型変換しています。
-
ReadAt()
メソッドの修正:copy(b, r.s[int(off):])
からcopy(b, r.s[off:])
へ変更。off
は既にint64
型であるため、不要なint
への型変換が削除されました。
-
Seek()
メソッドの修正:- 最も重要な変更点の一つです。以前は
abs >= 1<<31
というチェックがあり、2GBを超えるシークを禁止していました。このチェックが完全に削除されました。 r.i = int(abs)
という代入も、r.i = abs
と直接代入するように変更されました。これはabs
がint64
であり、r.i
もint64
になったためです。
- 最も重要な変更点の一つです。以前は
これらの変更により、bytes.Reader
とstrings.Reader
は、内部的に64ビットのインデックスを使用するようになり、理論上は最大で約9エクサバイト(2^63 - 1
バイト)までのオフセットを扱うことが可能になりました。これにより、Go言語で非常に大きなデータセットを扱う際の柔軟性とスケーラビリティが向上しました。
コアとなるコードの変更箇所
src/pkg/bytes/reader.go
および src/pkg/strings/reader.go
(共通の変更)
--- a/src/pkg/bytes/reader.go
+++ b/src/pkg/bytes/reader.go
@@ -16,17 +16,17 @@ import (
// Unlike a Buffer, a Reader is read-only and supports seeking.
type Reader struct {
s []byte
- i int // current reading index
+ i int64 // current reading index
prevRune int // index of previous rune; or < 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) {
+ if r.i >= int64(len(r.s)) {
return 0
}
- return len(r.s) - r.i
+ return int(int64(len(r.s)) - r.i)
}
func (r *Reader) Read(b []byte) (n int, err error) {
@@ -34,11 +34,11 @@ func (r *Reader) Read(b []byte) (n int, err error) {
if len(b) == 0 {
return 0, nil
}
- if r.i >= len(r.s) {
+ if r.i >= int64(len(r.s)) {
return 0, io.EOF
}
n = copy(b, r.s[r.i:])
- r.i += n
+ r.i += int64(n)
return
}
@@ -49,7 +49,7 @@ func (r *Reader) ReadAt(b []byte, off int64) (n int, err error) {
if off >= int64(len(r.s)) {
return 0, io.EOF
}
- n = copy(b, r.s[int(off):])
+ n = copy(b, r.s[off:])
if n < len(b) {
err = io.EOF
}
@@ -58,7 +58,7 @@ 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) {
+ if r.i >= int64(len(r.s)) {
return 0, io.EOF
}
b = r.s[r.i]
@@ -76,17 +76,17 @@ func (r *Reader) UnreadByte() error {
}
func (r *Reader) ReadRune() (ch rune, size int, err error) {
- if r.i >= len(r.s) {
+ if r.i >= int64(len(r.s)) {
r.prevRune = -1
return 0, 0, io.EOF
}
- r.prevRune = r.i
+ r.prevRune = int(r.i)
if c := r.s[r.i]; c < utf8.RuneSelf {
r.i++
return rune(c), 1, nil
}
ch, size = utf8.DecodeRune(r.s[r.i:])
- r.i += size
+ r.i += int64(size)
return
}
@@ -94,7 +94,7 @@ func (r *Reader) UnreadRune() error {
if r.prevRune < 0 {
return errors.New("bytes.Reader: previous operation was not ReadRune")
}
- r.i = r.prevRune
+ r.i = int64(r.prevRune)
r.prevRune = -1
return nil
}
@@ -116,17 +116,14 @@ func (r *Reader) Seek(offset int64, whence int) (int64, error) {
if abs < 0 {
return 0, errors.New("bytes: negative position")
}
- if abs >= 1<<31 {
- return 0, errors.New("bytes: position out of range")
- }
- r.i = int(abs)
+ r.i = abs
return abs, nil
}
// WriteTo implements the io.WriterTo interface.
func (r *Reader) WriteTo(w io.Writer) (n int64, err error) {
r.prevRune = -1
- if r.i >= len(r.s) {
+ if r.i >= int64(len(r.s)) {
return 0, nil
}
b := r.s[r.i:]
@@ -134,7 +131,7 @@ func (r *Reader) WriteTo(w io.Writer) (n int64, err error) {
if m > len(b) {
panic("bytes.Reader.WriteTo: invalid Write count")
}
- r.i += m
+ r.i += int64(m)
n = int64(m)
if m != len(b) && err == nil {
err = io.ErrShortWrite
src/pkg/bytes/reader_test.go
および src/pkg/strings/reader_test.go
(共通の変更)
--- a/src/pkg/bytes/reader_test.go
+++ b/src/pkg/bytes/reader_test.go
@@ -27,8 +27,8 @@ func TestReader(t *testing.T) {
{seek: os.SEEK_SET, off: 1, n: 1, want: "1"},
{seek: os.SEEK_CUR, off: 1, wantpos: 3, n: 2, want: "34"},
{seek: os.SEEK_SET, off: -1, seekerr: "bytes: negative position"},
- {seek: os.SEEK_SET, off: 1<<31 - 1},\n- {seek: os.SEEK_CUR, off: 1, seekerr: "bytes: position out of range"},\n+ {seek: os.SEEK_SET, off: 1 << 33, wantpos: 1 << 33},\n+ {seek: os.SEEK_CUR, off: 1, wantpos: 1<<33 + 1},\n {seek: os.SEEK_SET, n: 5, want: "01234"},
{seek: os.SEEK_CUR, n: 5, want: "56789"},
{seek: os.SEEK_END, off: -1, n: 1, wantpos: 9, want: "9"},
@@ -60,6 +60,16 @@ func TestReader(t *testing.T) {
}
}
+func TestReadAfterBigSeek(t *testing.T) {
+ r := NewReader([]byte("0123456789"))
+ if _, err := r.Seek(1<<31+5, os.SEEK_SET); err != nil {
+ t.Fatal(err)
+ }
+ if n, err := r.Read(make([]byte, 10)); n != 0 || err != io.EOF {
+ t.Errorf("Read = %d, %v; want 0, EOF", n, err)
+ }
+}
+
func TestReaderAt(t *testing.T) {
r := NewReader([]byte("0123456789"))
tests := []struct {
コアとなるコードの解説
Reader
構造体のi
フィールドの型変更
- 変更前:
i int
- 変更後:
i int64
これはこのコミットの最も根本的な変更です。i
はReader
が現在読み取っている位置を示すインデックスであり、この型をint
からint64
に変更することで、32ビットシステムにおける2GBの制限を克服し、より大きなオフセットを正確に表現できるようになりました。
Seek
メソッドからの範囲チェックの削除
- 変更前:
if abs >= 1<<31 { return 0, errors.New("bytes: position out of range") } r.i = int(abs)
- 変更後:
r.i = abs
以前は、計算された絶対オフセットabs
が1<<31
(約2GB)以上の場合にエラーを返す明示的なチェックがありました。このチェックが完全に削除されました。これにより、Seek
メソッドはint64
で表現可能な任意のオフセットを受け入れるようになります。また、r.i
がint64
になったため、abs
をint
にキャストする必要もなくなりました。
各種読み取りメソッドにおける型変換の調整
Len()
, Read()
, ReadByte()
, ReadRune()
, UnreadRune()
, WriteTo()
などのメソッドでは、r.i
がint64
になったことに伴い、len(r.s)
(int
型)や読み取ったバイト数n
(int
型)など、他のint
型の値との演算や代入を行う際に、明示的な型変換(例: int64(len(r.s))
やint64(n)
)が追加されました。これは、Goの型システムが厳格であるため、異なる型の間の演算には明示的な型変換が必要となるためです。
テストケースの追加と修正
- 既存テストの修正:
TestReader
内のシークテストケースが更新され、以前はエラーとなっていた1<<31
を超えるシークが、新しいテストでは成功するように変更されました(例:off: 1 << 33
)。これにより、変更が正しく機能していることが確認されます。 - 新しいテスト関数
TestReadAfterBigSeek
の追加: この新しいテストは、1<<31 + 5
という、以前の制限を超えるオフセットにシークした後、Read
操作が期待通りにio.EOF
を返すことを確認します。これは、シークが成功し、かつデータ範囲外へのシークが正しく処理されることを保証するための重要なテストです。
これらの変更により、bytes.Reader
とstrings.Reader
は、現代のシステムで扱われるような大容量のデータに対しても、より堅牢で柔軟なシーク機能を提供するようになりました。
関連リンク
- Go言語の
io
パッケージドキュメント: https://pkg.go.dev/io - Go言語の
bytes
パッケージドキュメント: https://pkg.go.dev/bytes - Go言語の
strings
パッケージドキュメント: https://pkg.go.dev/strings
参考にした情報源リンク
- Go言語の公式ドキュメント
- Go言語のソースコード
- (Go issue #7654は直接見つかりませんでしたが、コミットメッセージとコード変更からその意図を推測しました。)