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

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

このコミットは、Go言語の標準ライブラリであるbytesパッケージとstringsパッケージ内のReader型に対する変更を含んでいます。具体的には、bytes.Readerstrings.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.Readerstrings.Readerを使用して2GBを超えるバイトスライスや文字列を扱う際に、Seekメソッドで2GBを超えるオフセットを指定しようとすると、「position out of range」というエラーが発生し、正しくシークできないという問題がありました。これは、特に大きなファイルやメモリマップされたデータ、ネットワークストリームなどを扱うアプリケーションにおいて、深刻な制約となっていました。

コミットメッセージにあるFixes #7654は、この問題がGoのIssueトラッカーで報告されていたことを示唆しています。具体的なIssueの内容は確認できませんでしたが、このコミットは、この2GBのシーク制限を解消し、より大きなデータセットに対する柔軟な操作を可能にすることを目的としています。

前提知識の解説

io.Readerio.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.Readerstrings.Readerは、これらのインターフェースを実装しており、バイトスライスや文字列をあたかもファイルのように扱うことができます。

Goにおけるintint64

  • 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.Readerstrings.Readerの内部状態を管理するiフィールド(現在の読み取りインデックス)の型をintからint64に変更したことです。この変更により、Readerは2GBを超えるオフセットを正確に追跡できるようになりました。

型変更に伴い、以下の修正が行われています。

  1. Reader構造体のiフィールドの型変更:

    • type Reader struct { ... i int ... } から type Reader struct { ... i int64 ... } へ変更。
  2. Len()メソッドの修正:

    • r.ilen(r.s)の比較において、len(r.s)int型であるため、int64(len(r.s))と明示的に型変換してr.iint64)と比較するように変更。
    • 戻り値の計算でも、int(int64(len(r.s)) - r.i)のようにintへの型変換が行われています。これは、Len()メソッドの戻り値がint型であるためです。
  3. Read()ReadByte()ReadRune()UnreadRune()WriteTo()メソッドの修正:

    • r.iの更新(例: r.i += n)において、nsizeint型であるため、int64(n)int64(size)のようにint64へ型変換してから加算するように変更。
    • r.prevRuneは引き続きint型ですが、UnreadRune()r.iに代入する際にはint64(r.prevRune)と型変換しています。
  4. ReadAt()メソッドの修正:

    • copy(b, r.s[int(off):])からcopy(b, r.s[off:])へ変更。offは既にint64型であるため、不要なintへの型変換が削除されました。
  5. Seek()メソッドの修正:

    • 最も重要な変更点の一つです。以前はabs >= 1<<31というチェックがあり、2GBを超えるシークを禁止していました。このチェックが完全に削除されました。
    • r.i = int(abs)という代入も、r.i = absと直接代入するように変更されました。これはabsint64であり、r.iint64になったためです。

これらの変更により、bytes.Readerstrings.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

これはこのコミットの最も根本的な変更です。iReaderが現在読み取っている位置を示すインデックスであり、この型を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
    

以前は、計算された絶対オフセットabs1<<31(約2GB)以上の場合にエラーを返す明示的なチェックがありました。このチェックが完全に削除されました。これにより、Seekメソッドはint64で表現可能な任意のオフセットを受け入れるようになります。また、r.iint64になったため、absintにキャストする必要もなくなりました。

各種読み取りメソッドにおける型変換の調整

Len(), Read(), ReadByte(), ReadRune(), UnreadRune(), WriteTo()などのメソッドでは、r.iint64になったことに伴い、len(r.s)int型)や読み取ったバイト数nint型)など、他のint型の値との演算や代入を行う際に、明示的な型変換(例: int64(len(r.s))int64(n))が追加されました。これは、Goの型システムが厳格であるため、異なる型の間の演算には明示的な型変換が必要となるためです。

テストケースの追加と修正

  • 既存テストの修正: TestReader内のシークテストケースが更新され、以前はエラーとなっていた1<<31を超えるシークが、新しいテストでは成功するように変更されました(例: off: 1 << 33)。これにより、変更が正しく機能していることが確認されます。
  • 新しいテスト関数 TestReadAfterBigSeek の追加: この新しいテストは、1<<31 + 5という、以前の制限を超えるオフセットにシークした後、Read操作が期待通りにio.EOFを返すことを確認します。これは、シークが成功し、かつデータ範囲外へのシークが正しく処理されることを保証するための重要なテストです。

これらの変更により、bytes.Readerstrings.Readerは、現代のシステムで扱われるような大容量のデータに対しても、より堅牢で柔軟なシーク機能を提供するようになりました。

関連リンク

参考にした情報源リンク

  • Go言語の公式ドキュメント
  • Go言語のソースコード
  • (Go issue #7654は直接見つかりませんでしたが、コミットメッセージとコード変更からその意図を推測しました。)