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

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

このコミットは、Go言語の標準ライブラリであるbufioパッケージ内のNewBufReadSizeおよびNewBufWriteSize関数に、冪等性(idempotency)の特性を追加するものです。これにより、これらの関数が既にバッファリングされたリーダーまたはライターに対して呼び出された際に、不必要に新しいバッファリングオブジェクトを作成するのではなく、既存のオブジェクトを再利用するようになります。

コミット

commit ee19695cfc62f965a0586dc84719121feee8859e
Author: Rob Pike <r@golang.org>
Date:   Mon Apr 6 21:42:14 2009 -0700

    make NewBufRead etc. idempotent
    
    R=rsc
    DELTA=63  (59 added, 0 deleted, 4 changed)
    OCL=27143
    CL=27143
--
 src/lib/bufio.go      | 20 ++++++++++++++++----
 src/lib/bufio_test.go | 47 +++++++++++++++++++++++++++++++++++++++++++++++
 2 files changed, 63 insertions(+), 4 deletions(-)

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

https://github.com/golang/go/commit/ee19695cfc62f965a0586dc84719121feee8859e

元コミット内容

make NewBufRead etc. idempotent

R=rsc
DELTA=63  (59 added, 0 deleted, 4 changed)
OCL=27143
CL=27143

変更の背景

Go言語のbufioパッケージは、I/O操作の効率化のためにバッファリング機能を提供します。NewBufReadSizeNewBufWriteSizeのような関数は、指定されたio.Readerio.Writerをラップし、内部にバッファを持つ新しいBufReadBufWriteオブジェクトを返します。

このコミット以前は、これらの関数は常に新しいバッファリングオブジェクトを作成していました。しかし、もし既にバッファリングされたリーダーやライター(つまり、*bufio.BufRead*bufio.BufWriteのインスタンス)が引数として渡され、かつ要求されるバッファサイズが既存のバッファサイズよりも小さいか同等である場合、新しいオブジェクトを作成するのは無駄であり、メモリのオーバーヘッドや不必要なオブジェクトの階層化を招く可能性がありました。

この変更の背景には、このような冗長なオブジェクト生成を避け、より効率的で予測可能なAPIの振る舞いを実現するという目的があります。特に、ライブラリやフレームワークの内部でI/Oストリームが複数回バッファリングされる可能性がある場合に、この冪等性は重要となります。

前提知識の解説

このコミットの変更内容を理解するためには、以下のGo言語の概念とプログラミングの原則を理解しておく必要があります。

  1. io.Readerio.Writerインターフェース:

    • Go言語における基本的なI/Oの抽象化です。io.ReaderインターフェースはRead(p []byte) (n int, err error)メソッドを定義し、データソースからの読み込みを抽象化します。io.WriterインターフェースはWrite(p []byte) (n int, err error)メソッドを定義し、データシンクへの書き込みを抽象化します。ファイル、ネットワーク接続、メモリ上のデータなど、様々なI/O操作がこれらのインターフェースを通じて統一的に扱われます。
  2. bufioパッケージ:

    • bufioパッケージは、io.Readerio.Writerインターフェースをラップし、内部バッファを通じてI/O操作を効率化するための機能を提供します。これにより、システムコールやディスクアクセスなどの低レベルなI/O操作の回数を減らし、アプリケーションのパフォーマンスを向上させることができます。
    • BufReadはバッファリングされたリーダー、BufWriteはバッファリングされたライターです。
  3. 冪等性 (Idempotency):

    • プログラミングにおける冪等性とは、ある操作を複数回実行しても、その結果が1回実行した場合と同じになる性質を指します。例えば、データベースのレコードを更新する操作が冪等である場合、その更新クエリを何度実行しても、最終的なレコードの状態は同じになります。
    • このコミットの文脈では、NewBufReadSizeNewBufWriteSize関数が冪等であるとは、同じ基になるリーダー/ライターと十分なバッファサイズで複数回呼び出された場合、常に同じBufRead/BufWriteインスタンスを返すことを意味します。これにより、不必要なオブジェクトの生成やメモリ割り当てを防ぎます。
  4. 型アサーション (Type Assertion):

    • Go言語のインターフェース型から、そのインターフェースを実装している具体的な型に変換する操作です。value, ok := interfaceValue.(ConcreteType) の形式で使われます。
    • valueには変換された具体的な型の値が格納されます。
    • okはブール値で、型アサーションが成功したかどうか(つまり、interfaceValueが実際にConcreteTypeのインスタンスであったかどうか)を示します。このok変数を使って、安全に型アサーションの結果をチェックすることができます。

技術的詳細

このコミットの主要な変更点は、NewBufReadSizeNewBufWriteSize関数の内部に、引数として渡されたio.Readerまたはio.Writerが既に適切なbufioのバッファリングオブジェクトであるかどうかをチェックするロジックが追加されたことです。

具体的には、以下のステップが導入されました。

  1. 引数の型チェック:

    • NewBufReadSize関数では、引数rd io.Read*bufio.BufRead型であるかどうかを型アサーションb, ok := rd.(*BufRead)を使ってチェックします。
    • 同様に、NewBufWriteSize関数では、引数wr io.Write*bufio.BufWrite型であるかどうかをb, ok := wr.(*BufWrite)を使ってチェックします。
  2. 既存バッファのサイズチェック:

    • 型アサーションが成功し(oktrue)、かつ既存のバッファリングオブジェクトの内部バッファサイズ(len(b.buf))が、新しく要求されたsize以上である場合、関数は新しいオブジェクトを作成せずに、既存のbをそのまま返します。

このロジックにより、例えば以下のようなシナリオで効率が向上します。

// 最初のバッファリングリーダーの作成
reader := strings.NewReader("some data")
bufReader1, _ := bufio.NewBufReadSize(reader, 1024)

// 同じリーダーに対して、同じかより小さいバッファサイズで再度NewBufReadSizeを呼び出す
// このコミットにより、bufReader2はbufReader1と同一のインスタンスになる
bufReader2, _ := bufio.NewBufReadSize(bufReader1, 512) 

// bufReader1とbufReader2は同じオブジェクトを指す
fmt.Println(bufReader1 == bufReader2) // true

もし要求されたsizeが既存のバッファサイズよりも大きい場合は、新しいバッファリングオブジェクトが作成され、既存のオブジェクトをラップします。これは、より大きなバッファが必要な場合に、その要求を満たすために新しいバッファリング層が必要となるため、正しい振る舞いです。

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

変更は主にsrc/lib/bufio.goファイル内のNewBufReadSizeNewBufWriteSize関数に集中しています。

--- a/src/lib/bufio.go
+++ b/src/lib/bufio.go
@@ -54,12 +54,18 @@ type BufRead struct {
 }
 
 // NewBufReadSize creates a new BufRead whose buffer has the specified size,
-// which must be greater than zero.
+// which must be greater than zero.  If the argument io.Read is already a
+// BufRead with large enough size, it returns the underlying BufRead.
 // It returns the BufRead and any error.
-func NewBufReadSize(rd io.Read, size int) (b *BufRead, err *os.Error) {
+func NewBufReadSize(rd io.Read, size int) (*BufRead, *os.Error) {
 	if size <= 0 {
 		return nil, BadBufSize
 	}
+	// Is it already a BufRead?
+	b, ok := rd.(*BufRead);
+	if ok && len(b.buf) >= size {
+		return b, nil
+	}
 	b = new(BufRead);
 	b.buf = make([]byte, size);
 	b.rd = rd;
@@ -381,12 +387,18 @@ type BufWrite struct {
 }
 
 // NewBufWriteSize creates a new BufWrite whose buffer has the specified size,
-// which must be greater than zero.
+// which must be greater than zero. If the argument io.Write is already a
+// BufWrite with large enough size, it returns the underlying BufWrite.
 // It returns the BufWrite and any error.
-func NewBufWriteSize(wr io.Write, size int) (b *BufWrite, err *os.Error) {
+func NewBufWriteSize(wr io.Write, size int) (*BufWrite, *os.Error) {
 	if size <= 0 {
 		return nil, BadBufSize
 	}
+	// Is it already a BufWrite?
+	b, ok := wr.(*BufWrite);
+	if ok && len(b.buf) >= size {
+		return b, nil
+	}
 	b = new(BufWrite);
 	b.buf = make([]byte, size);
 	b.wr = wr;

また、この新しい振る舞いを検証するためのテストコードがsrc/lib/bufio_test.goに追加されています。

--- a/src/lib/bufio_test.go
+++ b/src/lib/bufio_test.go
@@ -330,3 +330,50 @@ func TestBufWrite(t *testing.T) {
 	}\n
 }\n
 
+func TestNewBufReadSizeIdempotent(t *testing.T) {
+	const BufSize = 1000;
+	b, err := NewBufReadSize(newByteReader(io.StringBytes("hello world")), BufSize);
+	if err != nil {
+		t.Error("NewBufReadSize create fail", err);
+	}
+	// Does it recognize itself?
+	b1, err2 := NewBufReadSize(b, BufSize);
+	if err2 != nil {
+		t.Error("NewBufReadSize #2 create fail", err2);
+	}
+	if b1 != b {
+		t.Error("NewBufReadSize did not detect underlying BufRead");
+	}
+	// Does it wrap if existing buffer is too small?
+	b2, err3 := NewBufReadSize(b, 2*BufSize);
+	if err3 != nil {
+		t.Error("NewBufReadSize #3 create fail", err3);
+	}
+	if b2 == b {
+		t.Error("NewBufReadSize did not enlarge buffer");
+	}
+}
+
+func TestNewBufWriteSizeIdempotent(t *testing.T) {
+	const BufSize = 1000;
+	b, err := NewBufWriteSize(newByteWriter(), BufSize);
+	if err != nil {
+		t.Error("NewBufWriteSize create fail", err);
+	}
+	// Does it recognize itself?
+	b1, err2 := NewBufWriteSize(b, BufSize);
+	if err2 != nil {
+		t.Error("NewBufWriteSize #2 create fail", err2);
+	}
+	if b1 != b {
+		t.Error("NewBufWriteSize did not detect underlying BufWrite");
+	}
+	// Does it wrap if existing buffer is too small?
+	b2, err3 := NewBufWriteSize(b, 2*BufSize);
+	if err3 != nil {
+		t.Error("NewBufWriteSize #3 create fail", err3);
+	}
+	if b2 == b {
+		t.Error("NewBufWriteSize did not enlarge buffer");
+	}
+}

コアとなるコードの解説

NewBufReadSize関数における変更

func NewBufReadSize(rd io.Read, size int) (*BufRead, *os.Error) {
	if size <= 0 {
		return nil, BadBufSize
	}
	// Is it already a BufRead?
	b, ok := rd.(*BufRead); // (A) 型アサーション
	if ok && len(b.buf) >= size { // (B) 冪等性チェック
		return b, nil // (C) 既存インスタンスの返却
	}
	b = new(BufRead);
	b.buf = make([]byte, size);
	b.rd = rd;
	return b, nil
}
  • (A) 型アサーション: b, ok := rd.(*BufRead);

    • この行は、入力されたio.Readインターフェースrdが、実際に*BufRead型(bufioパッケージのバッファリングリーダーの具体的な型)であるかどうかをチェックします。
    • もしrd*BufReadのインスタンスであれば、その値がbに代入され、oktrueになります。そうでなければ、okfalseになります。
  • (B) 冪等性チェック: if ok && len(b.buf) >= size { ... }

    • この条件式が、このコミットの核心です。
    • ok: rd*BufRead型であったことを確認します。
    • len(b.buf) >= size: 既存の*BufReadインスタンスbが持つ内部バッファ(b.buf)のサイズが、現在NewBufReadSize関数に要求されているsize以上であるかどうかをチェックします。
    • この両方の条件がtrueである場合、つまり、入力が既にバッファリングされたリーダーであり、かつそのバッファが要求されたサイズを満たしている場合、以下の処理に進みます。
  • (C) 既存インスタンスの返却: return b, nil

    • 条件(B)が満たされた場合、関数は新しいBufReadオブジェクトを作成することなく、既存のbインスタンスをそのまま返します。これにより、不必要なメモリ割り当てやオブジェクトの階層化が回避され、関数が冪等になります。

条件(B)が満たされない場合(例えば、rd*BufRead型ではない、または既存のバッファサイズが小さすぎる場合)、関数は以前と同様に新しいBufReadオブジェクトを作成して返します。

NewBufWriteSize関数における変更

NewBufWriteSize関数も同様のロジックで変更されています。io.Writeインターフェースに対して*BufWrite型のアサーションを行い、既存の*BufWriteインスタンスのバッファサイズが十分であれば、そのインスタンスを返します。

テストコードの解説

src/lib/bufio_test.goに追加されたテストは、この冪等性の振る舞いを具体的に検証します。

  • TestNewBufReadSizeIdempotent:

    1. まず、NewBufReadSizeを使って通常のBufReadインスタンスbを作成します。
    2. 次に、このbを引数として、同じBufSizeで再度NewBufReadSizeを呼び出し、b1を取得します。このとき、b1bと同一のインスタンスであることをb1 != bで確認します。これにより、既存のバッファが十分な場合に同じインスタンスが返されることをテストします。
    3. さらに、bを引数として、既存のバッファサイズよりも大きい2*BufSizeNewBufReadSizeを呼び出し、b2を取得します。このとき、b2bとは異なる新しいインスタンスであることをb2 == bで確認します。これにより、既存のバッファが不十分な場合に新しいインスタンスが作成されることをテストします。
  • TestNewBufWriteSizeIdempotent:

    • TestNewBufReadSizeIdempotentと同様のロジックで、NewBufWriteSize関数の冪等性を検証します。

これらのテストは、コミットによって導入された冪等性の振る舞いが期待通りに機能していることを保証します。

関連リンク

参考にした情報源リンク

  • Go言語のソースコード (golang/go GitHubリポジトリ): https://github.com/golang/go
  • Go言語の型アサーションに関する公式ドキュメントやチュートリアル (Go言語の公式ウェブサイトやEffective Goなど)
  • 冪等性に関する一般的なプログラミングの概念に関する情報源