[インデックス 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操作の効率化のためにバッファリング機能を提供します。NewBufReadSizeやNewBufWriteSizeのような関数は、指定されたio.Readerやio.Writerをラップし、内部にバッファを持つ新しいBufReadやBufWriteオブジェクトを返します。
このコミット以前は、これらの関数は常に新しいバッファリングオブジェクトを作成していました。しかし、もし既にバッファリングされたリーダーやライター(つまり、*bufio.BufReadや*bufio.BufWriteのインスタンス)が引数として渡され、かつ要求されるバッファサイズが既存のバッファサイズよりも小さいか同等である場合、新しいオブジェクトを作成するのは無駄であり、メモリのオーバーヘッドや不必要なオブジェクトの階層化を招く可能性がありました。
この変更の背景には、このような冗長なオブジェクト生成を避け、より効率的で予測可能なAPIの振る舞いを実現するという目的があります。特に、ライブラリやフレームワークの内部でI/Oストリームが複数回バッファリングされる可能性がある場合に、この冪等性は重要となります。
前提知識の解説
このコミットの変更内容を理解するためには、以下のGo言語の概念とプログラミングの原則を理解しておく必要があります。
-
io.Readerとio.Writerインターフェース:- Go言語における基本的なI/Oの抽象化です。
io.ReaderインターフェースはRead(p []byte) (n int, err error)メソッドを定義し、データソースからの読み込みを抽象化します。io.WriterインターフェースはWrite(p []byte) (n int, err error)メソッドを定義し、データシンクへの書き込みを抽象化します。ファイル、ネットワーク接続、メモリ上のデータなど、様々なI/O操作がこれらのインターフェースを通じて統一的に扱われます。
- Go言語における基本的なI/Oの抽象化です。
-
bufioパッケージ:bufioパッケージは、io.Readerやio.Writerインターフェースをラップし、内部バッファを通じてI/O操作を効率化するための機能を提供します。これにより、システムコールやディスクアクセスなどの低レベルなI/O操作の回数を減らし、アプリケーションのパフォーマンスを向上させることができます。BufReadはバッファリングされたリーダー、BufWriteはバッファリングされたライターです。
-
冪等性 (Idempotency):
- プログラミングにおける冪等性とは、ある操作を複数回実行しても、その結果が1回実行した場合と同じになる性質を指します。例えば、データベースのレコードを更新する操作が冪等である場合、その更新クエリを何度実行しても、最終的なレコードの状態は同じになります。
- このコミットの文脈では、
NewBufReadSizeやNewBufWriteSize関数が冪等であるとは、同じ基になるリーダー/ライターと十分なバッファサイズで複数回呼び出された場合、常に同じBufRead/BufWriteインスタンスを返すことを意味します。これにより、不必要なオブジェクトの生成やメモリ割り当てを防ぎます。
-
型アサーション (
Type Assertion):- Go言語のインターフェース型から、そのインターフェースを実装している具体的な型に変換する操作です。
value, ok := interfaceValue.(ConcreteType)の形式で使われます。 valueには変換された具体的な型の値が格納されます。okはブール値で、型アサーションが成功したかどうか(つまり、interfaceValueが実際にConcreteTypeのインスタンスであったかどうか)を示します。このok変数を使って、安全に型アサーションの結果をチェックすることができます。
- Go言語のインターフェース型から、そのインターフェースを実装している具体的な型に変換する操作です。
技術的詳細
このコミットの主要な変更点は、NewBufReadSizeとNewBufWriteSize関数の内部に、引数として渡されたio.Readerまたはio.Writerが既に適切なbufioのバッファリングオブジェクトであるかどうかをチェックするロジックが追加されたことです。
具体的には、以下のステップが導入されました。
-
引数の型チェック:
NewBufReadSize関数では、引数rd io.Readが*bufio.BufRead型であるかどうかを型アサーションb, ok := rd.(*BufRead)を使ってチェックします。- 同様に、
NewBufWriteSize関数では、引数wr io.Writeが*bufio.BufWrite型であるかどうかをb, ok := wr.(*BufWrite)を使ってチェックします。
-
既存バッファのサイズチェック:
- 型アサーションが成功し(
okがtrue)、かつ既存のバッファリングオブジェクトの内部バッファサイズ(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ファイル内のNewBufReadSizeとNewBufWriteSize関数に集中しています。
--- 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に代入され、okはtrueになります。そうでなければ、okはfalseになります。
- この行は、入力された
-
(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)が満たされた場合、関数は新しい
条件(B)が満たされない場合(例えば、rdが*BufRead型ではない、または既存のバッファサイズが小さすぎる場合)、関数は以前と同様に新しいBufReadオブジェクトを作成して返します。
NewBufWriteSize関数における変更
NewBufWriteSize関数も同様のロジックで変更されています。io.Writeインターフェースに対して*BufWrite型のアサーションを行い、既存の*BufWriteインスタンスのバッファサイズが十分であれば、そのインスタンスを返します。
テストコードの解説
src/lib/bufio_test.goに追加されたテストは、この冪等性の振る舞いを具体的に検証します。
-
TestNewBufReadSizeIdempotent:- まず、
NewBufReadSizeを使って通常のBufReadインスタンスbを作成します。 - 次に、この
bを引数として、同じBufSizeで再度NewBufReadSizeを呼び出し、b1を取得します。このとき、b1がbと同一のインスタンスであることをb1 != bで確認します。これにより、既存のバッファが十分な場合に同じインスタンスが返されることをテストします。 - さらに、
bを引数として、既存のバッファサイズよりも大きい2*BufSizeでNewBufReadSizeを呼び出し、b2を取得します。このとき、b2がbとは異なる新しいインスタンスであることをb2 == bで確認します。これにより、既存のバッファが不十分な場合に新しいインスタンスが作成されることをテストします。
- まず、
-
TestNewBufWriteSizeIdempotent:TestNewBufReadSizeIdempotentと同様のロジックで、NewBufWriteSize関数の冪等性を検証します。
これらのテストは、コミットによって導入された冪等性の振る舞いが期待通りに機能していることを保証します。
関連リンク
- Go言語
bufioパッケージのドキュメント: https://pkg.go.dev/bufio - Go言語
ioパッケージのドキュメント: https://pkg.go.dev/io
参考にした情報源リンク
- Go言語のソースコード (golang/go GitHubリポジトリ): https://github.com/golang/go
- Go言語の型アサーションに関する公式ドキュメントやチュートリアル (Go言語の公式ウェブサイトやEffective Goなど)
- 冪等性に関する一般的なプログラミングの概念に関する情報源