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

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

このコミットは、Go言語の標準ライブラリ strings パッケージ内の Reader 型に Seek メソッドを追加するものです。これにより、strings.Readerio.Seeker インターフェースを満たすようになり、特に http.ServeContent のような、io.ReadSeeker を要求する関数との互換性が向上します。

コミット

commit 396170da9b622e1a4866d9f28552aee008274ed5
Author: Brad Fitzpatrick <bradfitz@golang.org>
Date:   Thu Feb 9 17:28:41 2012 +1100

    strings: add Seek method to Reader
    
    strings.Reader is already stateful and read-only.
    
    This permits a *Reader with http.ServeContent.
    
    R=golang-dev, r, rsc, rsc
    CC=golang-dev
    https://golang.org/cl/5639068

GitHub上でのコミットページへのリンク

https://github.com/golang/go/commit/396170da9b622e1a4866d9f28552aee008274ed5

元コミット内容

strings パッケージの Reader 型に Seek メソッドを追加します。strings.Reader は既に状態を持ち、読み取り専用です。この変更により、*Readerhttp.ServeContent と共に使用できるようになります。

変更の背景

この変更の主な背景は、strings.Readernet/http パッケージの http.ServeContent 関数と互換性を持たせることです。http.ServeContent は、HTTPレスポンスとしてコンテンツを効率的に提供するための関数であり、そのコンテンツソースとして io.ReadSeeker インターフェースを実装した型を要求します。

従来の strings.Readerio.Readerio.ByteScannerio.RuneScanner インターフェースを実装していましたが、io.Seeker インターフェースは実装していませんでした。そのため、文字列データを http.ServeContent で直接提供することができませんでした。

Seek メソッドを追加することで、strings.Readerio.Seeker インターフェースも満たすようになり、結果として io.ReadSeeker インターフェースも満たすことになります。これにより、メモリ上の文字列データをファイルのようにシーク可能な形で http.ServeContent に渡すことが可能になり、HTTPのRangeリクエストへの対応や、適切なヘッダー(Content-Type, Content-Length, Last-Modifiedなど)の自動設定、キャッシュの効率的な利用といった http.ServeContent の利点を享受できるようになります。

前提知識の解説

io.Reader インターフェース

io.Reader はGo言語の io パッケージで定義されている最も基本的なインターフェースの一つです。データを読み取るための単一のメソッド Read を持ちます。

type Reader interface {
    Read(p []byte) (n int, err error)
}

Read メソッドは、データを p に読み込み、読み込んだバイト数 n とエラー err を返します。ストリームからの順次読み取りを抽象化します。

io.Seeker インターフェース

io.Seeker は、データストリーム内の現在の読み書き位置を変更する機能を提供するインターフェースです。ファイルやその他のランダムアクセス可能なデータソースを操作する際に特に有用です。

type Seeker interface {
    Seek(offset int64, whence int) (int64, error)
}
  • offset (int64): 移動する距離を指定します。
  • whence (int): offset の計算基準となる参照点を指定します。os パッケージで定義されている以下の定数を使用します。
    • os.SEEK_SET: ファイルの先頭からの絶対位置。
    • os.SEEK_CUR: 現在位置からの相対位置。
    • os.SEEK_END: ファイルの末尾からの相対位置(通常は負のオフセットで使用)。
  • 戻り値: 新しいオフセット(ファイルの先頭からの絶対位置)とエラーを返します。

io.Seeker を実装することで、データストリーム内の任意の位置に移動し、そこから読み取りや書き込みを開始できるようになります。

io.ReadSeeker インターフェース

io.ReadSeekerio.Readerio.Seeker の両方の機能を組み合わせたインターフェースです。これにより、データの読み取りと、読み取り位置のシークの両方が可能になります。

type ReadSeeker interface {
    Reader
    Seeker
}

strings.Reader

strings.Reader は、Go言語の strings パッケージで提供される型で、文字列からデータを読み取るための io.Reader の実装です。内部的には文字列と現在の読み取り位置を保持しており、文字列の内容をバイトストリームとして扱うことができます。このコミット以前は、順次読み取りのみが可能でした。

http.ServeContent 関数

net/http パッケージの http.ServeContent 関数は、HTTPレスポンスとしてコンテンツを効率的に提供するためのユーティリティ関数です。この関数は、io.ReadSeeker インターフェースを実装したコンテンツソースを受け取ります。

http.ServeContent の主な利点は以下の通りです。

  • Rangeリクエストの処理: HTTPの Range ヘッダーを適切に解釈し、クライアントがコンテンツの一部のみを要求した場合に、その部分だけを返します。これにより、大きなファイルのダウンロード再開や、動画のストリーミングなどが効率的に行えます。
  • ヘッダーの自動設定: Content-TypeContent-LengthLast-Modified などの適切なHTTPヘッダーを自動的に設定します。
  • キャッシュのサポート: If-Modified-Since などの条件付きリクエストを処理し、コンテンツが変更されていない場合は 304 Not Modified ステータスを返すことで、クライアント側のキャッシュを効率的に利用します。

技術的詳細

このコミットでは、strings.Reader 型に Seek メソッドが追加され、io.Seeker インターフェースが実装されました。

Seek メソッドの実装

Seek メソッドは、offsetwhence の値に基づいて、strings.Reader の内部的な読み取り位置 r.i を更新します。

  1. whence の処理:

    • os.SEEK_SET (0): abs = offset。オフセットがファイルの先頭からの絶対位置として扱われます。
    • os.SEEK_CUR (1): abs = int64(r.i) + offset。オフセットが現在の読み取り位置 r.i からの相対位置として扱われます。
    • os.SEEK_END (2): abs = int64(len(r.s)) + offset。オフセットが文字列の末尾からの相対位置として扱われます。len(r.s) は文字列全体の長さです。
    • 上記以外の whence の値が指定された場合は、"strings: invalid whence" というエラーを返します。
  2. 位置の検証:

    • 計算された絶対位置 abs が負の場合 (abs < 0) は、"strings: negative position" というエラーを返します。
    • 計算された絶対位置 abs1<<31 (2GB) 以上の場合 (abs >= 1<<31) は、"strings: position out of range" というエラーを返します。これは、r.iint 型であるため、その最大値を超える位置へのシークを防ぐためのチェックです。
  3. 読み取り位置の更新:

    • 検証を通過した場合、r.i = int(abs) として、strings.Reader の内部的な読み取り位置 r.i を更新します。
    • 更新された絶対位置 abs を返します。

Len() メソッドの修正

Len() メソッドは、未読部分のバイト数を返します。このコミットでは、r.i >= len(r.s) の場合に 0 を返すガード句が追加されました。これにより、既に文字列の末尾を超えて読み取り位置がある場合に、負の値ではなく 0 が返されるようになり、より堅牢になりました。

Read() メソッドの修正

Read() メソッドは、バイトスライス b にデータを読み込みます。このコミットでは、len(b) == 0 の場合に 0, nil を返すガード句が追加されました。これは、空のスライスに読み込もうとした場合に、すぐに 0 バイト読み込み、エラーなしで終了することを保証します。

テストの追加

src/pkg/strings/reader_test.go という新しいテストファイルが追加され、Seek メソッドの動作が詳細にテストされています。これには、様々な offsetwhence の組み合わせ、エラーケース(負の位置、範囲外の位置)、そしてシーク後の Read 動作の検証が含まれています。

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

--- a/src/pkg/strings/reader.go
+++ b/src/pkg/strings/reader.go
@@ -10,7 +10,7 @@ import (
 	"unicode/utf8"
 )
 
-// A Reader implements the io.Reader, io.ByteScanner, and
+// A Reader implements the io.Reader, io.Seeker, io.ByteScanner, and
 // io.RuneScanner interfaces by reading from a string.
 type Reader struct {
 	s        string
@@ -21,10 +21,16 @@ type Reader struct {
 // Len returns the number of bytes of the unread portion of the
 // string.
 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
 	}
@@ -87,6 +93,29 @@ func (r *Reader) UnreadRune() error {
 	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:
+		abs = offset
+	case 1:
+		abs = int64(r.i) + offset
+	case 2:
+		abs = int64(len(r.s)) + offset
+	default:
+		return 0, errors.New("strings: invalid whence")
+	}
+	if abs < 0 {
+		return 0, errors.New("strings: negative position")
+	}
+	if abs >= 1<<31 {
+		return 0, errors.New("strings: position out of range")
+	}
+	r.i = int(abs)
+	return abs, nil
+}
+
 // NewReader returns a new Reader reading from s.
 // It is similar to bytes.NewBufferString but more efficient and read-only.
 func NewReader(s string) *Reader { return &Reader{s, 0, -1} }
--- /dev/null
+++ b/src/pkg/strings/reader_test.go
@@ -0,0 +1,58 @@
+// 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 strings_test
+
+import (
+	"os"
+	"strings"
+	"testing"
+)
+
+func TestReader(t *testing.T) {
+	r := strings.NewReader("0123456789")
+	tests := []struct {
+		off     int64
+		seek    int
+		n       int
+		want    string
+		wantpos int64
+		seekerr string
+	}{
+		{seek: os.SEEK_SET, off: 0, n: 20, want: "0123456789"},
+		{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: "strings: negative position"},
+		{seek: os.SEEK_SET, off: 1<<31 - 1},
+		{seek: os.SEEK_CUR, off: 1, seekerr: "strings: position out of range"},
+		{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"},
+	}
+
+	for i, tt := range tests {
+		pos, err := r.Seek(tt.off, tt.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)
+		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)
+		}
+	}
+}

コアとなるコードの解説

src/pkg/strings/reader.go

  • type Reader struct のコメント変更: // A Reader implements the io.Reader, io.ByteScanner, and から // A Reader implements the io.Reader, io.Seeker, io.ByteScanner, and に変更され、io.Seeker インターフェースを実装したことが明示されています。

  • func (r *Reader) Len() int の変更: if r.i >= len(r.s) { return 0 } が追加されました。これにより、読み取り位置 r.i が文字列の長さを超えている場合、未読バイト数は 0 であると正確に報告されます。以前は len(r.s) - r.i が負の値になる可能性がありました。

  • func (r *Reader) Read(b []byte) (n int, err error) の変更: if len(b) == 0 { return 0, nil } が追加されました。これは、読み込み先のバイトスライス b が空の場合、何も読み込まずに 0 バイトと nil エラーを返すという一般的な io.Reader の振る舞いに準拠するためのものです。

  • func (r *Reader) Seek(offset int64, whence int) (int64, error) の追加: これがこのコミットの主要な変更点です。

    • whence の値に応じて、新しい絶対位置 abs を計算します。
      • os.SEEK_SET (0): abs = offset
      • os.SEEK_CUR (1): abs = 現在の読み取り位置 + offset
      • os.SEEK_END (2): abs = 文字列の長さ + offset
    • 無効な whence の値が与えられた場合、"strings: invalid whence" エラーを返します。
    • 計算された abs が負の場合、"strings: negative position" エラーを返します。
    • 計算された abs1<<31 (約2GB) 以上の場合、"strings: position out of range" エラーを返します。これは、r.iint 型であり、その範囲を超える位置へのシークを防ぐためです。
    • すべてのチェックを通過した場合、r.iint(abs) に設定し、新しい絶対位置 abs を返します。

src/pkg/strings/reader_test.go

  • 新規ファイルの追加: strings.ReaderSeek メソッドの動作を検証するためのテストケースが多数含まれています。
  • TestReader 関数:
    • strings.NewReader("0123456789")Reader インスタンスを作成します。
    • tests スライスには、様々な offsetwhence、期待される読み取りバイト数 n、期待される読み取り結果 want、期待される最終位置 wantpos、そして期待されるエラーメッセージ seekerr の組み合わせが定義されています。
    • 各テストケースでは、r.Seek を呼び出し、返された位置とエラーが期待通りであるかを検証します。
    • その後、r.Read を呼び出し、読み取られたデータが期待通りであるかを検証します。
    • 特に、負のオフセットや範囲外のオフセットに対するエラーハンドリングがテストされています。

関連リンク

参考にした情報源リンク

  • Go http.ServeContent の解説:
    • https://vertexaisearch.cloud.google.com/grounding-api-redirect/AUZIYQH055hLtGeLmJk8kJRV_pUyQ8_zH5JM9-f_7tJqvhXXXaMRdUVFgV0cHcAZNuUre91i26_Jpz7iwJlGTpwkiagjBnP3h2xHmYgoSUg0f3KUvL_7YoNGYKOaxGjKUVzQebU9wvPG-P2DzUAMG1nM3pKfLfIzx0G-ba06D5ckNn25WW4iuA==
    • https://vertexaisearch.cloud.google.com/grounding-api-redirect/AUZIYQF4SyoqHjhQ98v_ln-hrnZk1Q5aQiEPCVSMtat_DBANgXe9L6Vs-BljJyBjjKzL8RqzAFVv3bbCzb3OVO-LpZbBWAMmD4dekmqyzThyHVb26jje0oyqHfRo5K3LbW-58rwlSMbFMn6QTw==
    • https://vertexaisearch.cloud.google.google.com/grounding-api-redirect/AUZIYQHTg20iwTbxWFSxP-8lK-VnZ_pLZiU49Wsv0cTZAuLtL6qsqQuDPtzjzXpDtJeaLxTUZzEXDkLddgBvHIliQKllB4LbZrfZWnHgBV7dayp6mejNMP67upaECdLylKK1hzhgXiKrJAPmVyOQhQ==
  • Go io.Seeker インターフェースの解説:
    • https://vertexaisearch.cloud.google.com/grounding-api-redirect/AUZIYQEyYABSyAOuWIVm1EmH9BOL6p17psKVTifl-z_dq_XcEpk78exOBXnN_pKk-e-YaC34I0HiYJntyANGN_MqaMxOetIpBQF76gPt1tf0VdiQKrrGp09tzmEXULW_IWEpxh1gPJsAjbq2w-67_Mr5sX0BQrHc-EA==
    • https://vertexaisearch.cloud.google.com/grounding-api-redirect/AUZIYQGxZyrelfomP1SeysQ9Vmm0O1s21McmiLpckCZJHJbD4548gbP7Amqta2-ILabOcKUmd7b30QaAO-72OhKXxk-5XdsyNLahKr9BVSLWRh7pOzXCNUJ2Gb808pX3oyN7InL2D0GUZg==
    • https://vertexaisearch.cloud.google.com/grounding-api-redirect/AUZIYQETzaUGa-X6cj9NALa189MmIvActDgGRzc0wWbJX2iaz3Yt7x_QPCKt0GRicdjtkSX-Ag4WFOOPatljrUYFPEMPJ6AFY54Z3P-KdfLcpg8pzto6mk3bpGIaD_s2FOquaj-ZU-m_isV9jDS3GtCztht3tolvD_HUuzVxkaAKqVIttg==
    • https://vertexaisearch.cloud.google.com/grounding-api-redirect/AUZIYQHYclaks7U9PbfSCzgNfCQVDeOwlhW_QtkpOaz-I4_oHazqQmf3rAOMvJM92wopQC6jRZdhUGEDeXSGDXumVTYd4QMsy4Es6Bz2mYpHAjhSbP-Pow5GsL99DLrfZL2uozttqNQpFVwltEMWK4AvuXhKnWaKmMmROwA89sO2Drw0_zV9sb0EMGja1tbLbaD92D5sNHtlTIByd0fsRlSu58BcjXI2qXYZQgQ69VfvBKQRU1qxA==
    • https://vertexaisearch.cloud.google.com/grounding-api-redirect/AUZIYQGnHhQs4k15883udFKTyXPI0oBo1NtwI-m6eIXHRmPHCn6tOvACB4YtxqAqBl66yjYY9Ol8BfksQJnZnYoPcVxfkgnt_R88MkDmRnmp2uvI7OcFFBY3JioPVtGM1Cs09_5k66DXwBK2NvnyJRUjt3tX3tD9pjCj-DxcVHUctMVIa0K4JDiHBcG9aAa1oiIOR6vh0I0U1Bo=