[インデックス 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など)
- 冪等性に関する一般的なプログラミングの概念に関する情報源