[インデックス 11300] ファイルの概要
このコミットは、Go言語の標準ライブラリ bytes
パッケージ内の Buffer
型におけるメモリ割り当ての挙動を改善するものです。具体的には、bytes.Buffer
が非常に大きなバッファを確保しようとしてシステムメモリが不足した場合に、これまでのプログラムクラッシュ(パニック)ではなく、明示的なエラー ErrTooLarge
を返すように変更されました。これにより、アプリケーションはメモリ不足によるバッファオーバーフローをより堅牢に処理できるようになります。
コミット
commit 696bf79350b5cb0e977def1fc98ba6d6c8bd829f
Author: Rob Pike <r@golang.org>
Date: Fri Jan 20 13:51:49 2012 -0800
bytes.Buffer: turn buffer size overflows into errors
Fixes #2743.
R=golang-dev, gri, r
CC=golang-dev
https://golang.org/cl/5556072
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/696bf79350b5cb0e977def1fc98ba6d6c8bd829f
元コミット内容
bytes.Buffer: turn buffer size overflows into errors
Fixes #2743.
このコミットは、bytes.Buffer
がバッファサイズの上限を超えた場合に、パニックではなくエラーを返すように変更します。GoのIssue #2743を修正します。
変更の背景
Go言語の bytes.Buffer
は、可変長のバイトシーケンスを扱うための便利な型です。内部的にはバイトスライス []byte
を保持し、必要に応じてその容量を自動的に拡張します。しかし、これまでの実装では、bytes.Buffer
が非常に大きなメモリ領域を確保しようとした際に、システムがその要求に応えられない場合(例えば、利用可能なメモリが不足している場合)、Goの組み込み関数 make
がパニック(panic)を引き起こし、プログラム全体がクラッシュするという問題がありました。
この挙動は、予期せぬプログラム終了につながるため、堅牢なアプリケーション開発においては望ましくありませんでした。特に、ユーザーからの入力やネットワークからのデータなど、サイズが予測できないデータを扱う場合、悪意のある入力や単なる大量データによってサービスが停止する可能性がありました。
このコミットは、この問題を解決するために、メモリ割り当ての失敗をパニックとしてではなく、Goの標準的なエラー処理メカニズムである error
型を介して通知するように変更することを目的としています。これにより、開発者は bytes.Buffer
の操作が失敗した場合に、それを捕捉し、適切に処理(例えば、エラーログの記録、ユーザーへの通知、代替処理の実行など)できるようになります。
この変更は、GoのIssue #2743で議論されていた問題に対応するものです。
前提知識の解説
Go言語の bytes.Buffer
bytes.Buffer
は、Go言語の bytes
パッケージで提供される、可変長のバイトバッファです。io.Reader
および io.Writer
インターフェースを実装しており、バイトデータの読み書きに非常に便利です。例えば、文字列の結合、ネットワークデータのバッファリング、ファイル内容の構築などに広く利用されます。内部的には []byte
スライスを保持し、データが追加されると必要に応じてスライスの容量を増やします。
Go言語におけるメモリ割り当てと make
Go言語では、スライス、マップ、チャネルなどの組み込み型を初期化し、メモリを割り当てるために make
関数を使用します。例えば、make([]byte, size, capacity)
は、指定されたサイズと容量を持つバイトスライスを生成し、そのためのメモリをヒープに割り当てます。
重要な点として、make
関数は、要求されたメモリをシステムが割り当てられない場合に、ランタイムパニックを引き起こす可能性があります。これは、Goプログラムが予期せず終了する原因となります。
Go言語のエラーハンドリング
Go言語では、エラーは通常、関数の戻り値として error
型を返すことで処理されます。error
は組み込みのインターフェースであり、Error() string
メソッドを持ちます。これにより、エラーが発生したことを呼び出し元に伝え、呼び出し元は if err != nil
のような慣用的なパターンでエラーをチェックし、適切に対応することができます。パニックは、回復不能なエラーやプログラマーの論理的誤りを示すために使用されるべきであり、通常の実行フローで発生する可能性のあるエラー(例えば、ファイルが見つからない、ネットワーク接続が切れたなど)には error
を使用するのがGoの慣習です。
Go言語の panic
と recover
panic
は、Goプログラムの通常の実行フローを中断させるランタイムエラーです。これは、配列の範囲外アクセスやnilポインタのデリファレンスなど、回復不能な状況で発生します。パニックが発生すると、現在の関数の実行が停止し、遅延関数(defer
で登録された関数)が実行され、その後呼び出しスタックを遡ってパニックが伝播します。
recover
は、defer
関数内で呼び出された場合にのみ有効で、伝播しているパニックを捕捉し、プログラムの実行を再開させることができます。recover
がパニックを捕捉すると、panic
に渡された値が返され、プログラムはクラッシュせずに続行できます。これは、特定のクリティカルな操作において、予期せぬパニックから回復するためのメカニズムとして使用されます。
技術的詳細
このコミットの核心は、bytes.Buffer
が内部でメモリを確保する際に make
関数が引き起こす可能性のあるパニックを捕捉し、それを ErrTooLarge
という明示的なエラーに変換して呼び出し元に返す点にあります。
-
ErrTooLarge
の導入:bytes.Buffer
がメモリ割り当てに失敗したことを示す新しいエラー変数ErrTooLarge
が定義されました。var ErrTooLarge = errors.New("bytes.Buffer: too large")
-
makeSlice
ヘルパー関数の追加:makeSlice
という新しいヘルパー関数が導入されました。この関数は、指定されたサイズのバイトスライスを安全に割り当てる役割を担います。func makeSlice(n int) []byte { if n < 0 { return nil } // Catch out of memory panics. defer func() { recover() }() return make([]byte, n) }
この関数は、以下の重要なロジックを含んでいます。
n < 0
の場合、nil
を返します。これは、無効なサイズ要求に対するガードです。defer func() { recover() }()
ブロックが設定されています。これにより、make([]byte, n)
がメモリ不足でパニックを引き起こした場合でも、そのパニックが捕捉され、プログラムのクラッシュが防がれます。パニックが捕捉された場合、makeSlice
はnil
を返します。- 正常にメモリが割り当てられた場合、
make([]byte, n)
が返されます。
-
grow
メソッドの変更:bytes.Buffer
の内部メソッドであるgrow
は、バッファの容量を拡張するために使用されます。このメソッドは、新しいmakeSlice
ヘルパー関数を使用するように変更されました。- 以前は
make([]byte, 2*cap(b.buf)+n)
を直接呼び出していましたが、これがmakeSlice(2*cap(b.buf) + n)
に置き換えられました。 makeSlice
がnil
を返した場合(メモリ割り当て失敗または負のサイズ)、grow
メソッドは-1
を返すようになりました。この-1
は、呼び出し元にバッファの拡張が不可能であることを伝えます。
- 以前は
-
Write
,WriteString
,ReadFrom
,WriteByte
メソッドの変更: これらのメソッドは、bytes.Buffer
にデータを書き込む主要なインターフェースです。これらのメソッドは、内部でgrow
メソッドを呼び出して必要な容量を確保します。- 変更後、これらのメソッドは
grow
の戻り値をチェックするようになりました。grow
が-1
を返した場合(またはReadFrom
の場合はmakeSlice
がnil
を返した場合)、これらのメソッドはErrTooLarge
をエラーとして返します。これにより、メモリ割り当ての失敗がパニックではなく、エラーとしてアプリケーションに通知されるようになります。
- 変更後、これらのメソッドは
-
テストケースの追加:
src/pkg/bytes/buffer_test.go
にTestHuge
という新しいテストケースが追加されました。このテストは、bytes.Buffer
が非常に大きなデータを扱う際の挙動を検証します。testing.Short()
をチェックし、go test -short
で実行された場合はスキップされます。これは、このテストが大量のメモリを消費するためです。- 約500MBのバイトスライスを繰り返し
bytes.Buffer
に書き込むことで、意図的にメモリ割り当ての限界に挑戦します。 - このテストは、最終的に
ErrTooLarge
が返されることを期待しており、もしエラーが返されずに処理が完了してしまった場合はテストが失敗します。これにより、新しいエラーハンドリングメカニズムが正しく機能していることが保証されます。
これらの変更により、bytes.Buffer
は、メモリ割り当ての失敗に対してより予測可能で堅牢な挙動を示すようになり、Goアプリケーションの安定性が向上しました。
コアとなるコードの変更箇所
src/pkg/bytes/buffer.go
--- a/src/pkg/bytes/buffer.go
+++ b/src/pkg/bytes/buffer.go
@@ -33,6 +33,9 @@ const (
opRead // Any other read operation.
)
+// ErrTooLarge is returned if there is too much data to fit in a buffer.
+var ErrTooLarge = errors.New("bytes.Buffer: too large")
+
// Bytes returns a slice of the contents of the unread portion of the buffer;
// len(b.Bytes()) == b.Len(). If the caller changes the contents of the
// returned slice, the contents of the buffer will change provided there
@@ -68,8 +71,10 @@ func (b *Buffer) Truncate(n int) {
// b.Reset() is the same as b.Truncate(0).\n func (b *Buffer) Reset() { b.Truncate(0) }\n \n-// Grow buffer to guarantee space for n more bytes.\n-// Return index where bytes should be written.\n+// grow grows the buffer to guarantee space for n more bytes.\n+// It returns the index where bytes should be written.\n+// If the buffer can't grow, it returns -1, which will\n+// become ErrTooLarge in the caller.\n func (b *Buffer) grow(n int) int {\n m := b.Len()\n // If buffer is empty, reset to recover space.\n@@ -82,7 +87,10 @@ func (b *Buffer) grow(n int) int {\n buf = b.bootstrap[0:]\n } else {\n // not enough space anywhere\n- buf = make([]byte, 2*cap(b.buf)+n)\n+ buf = makeSlice(2*cap(b.buf) + n)\n+ if buf == nil {\n+ return -1\n+ }\n copy(buf, b.buf[b.off:])
}\n b.buf = buf\n@@ -97,6 +105,9 @@ func (b *Buffer) grow(n int) int {\n func (b *Buffer) Write(p []byte) (n int, err error) {\n b.lastRead = opInvalid\n m := b.grow(len(p))\n+ if m < 0 {\n+ return 0, ErrTooLarge\n+ }\n return copy(b.buf[m:], p), nil\n }\n \n@@ -105,6 +116,9 @@ func (b *Buffer) WriteString(s string) (n int, err error) {\n func (b *Buffer) WriteString(s string) (n int, err error) {\n b.lastRead = opInvalid\n m := b.grow(len(s))\n+ if m < 0 {\n+ return 0, ErrTooLarge\n+ }\n return copy(b.buf[m:], s), nil\n }\n \n@@ -133,7 +147,10 @@ func (b *Buffer) ReadFrom(r io.Reader) (n int64, err error) {\n newBuf = b.buf[0 : len(b.buf)-b.off]\n } else {\n // not enough space at end; put space on end\n- newBuf = make([]byte, len(b.buf)-b.off, 2*(cap(b.buf)-b.off)+MinRead)\n+ newBuf = makeSlice(2*(cap(b.buf)-b.off) + MinRead)[:len(b.buf)-b.off]\n+ if newBuf == nil {\n+ return n, ErrTooLarge\n+ }\n }\n copy(newBuf, b.buf[b.off:])\n b.buf = newBuf\n@@ -152,6 +169,18 @@ func (b *Buffer) ReadFrom(r io.Reader) (n int64, err error) {\n return n, nil // err is EOF, so return nil explicitly\n }\n \n+// makeSlice allocates a slice of size n, returning nil if the slice cannot be allocated.\n+func makeSlice(n int) []byte {\n+ if n < 0 {\n+ return nil\n+ }\n+ // Catch out of memory panics.\n+ defer func() {\n+ recover()\n+ }()\n+ return make([]byte, n)\n+}\n+\n // WriteTo writes data to w until the buffer is drained or an error\n // occurs. The return value n is the number of bytes written; it always\n // fits into an int, but it is int64 to match the io.WriterTo interface.\n@@ -179,6 +208,9 @@ func (b *Buffer) WriteByte(c byte) error {\n func (b *Buffer) WriteByte(c byte) error {\n b.lastRead = opInvalid\n m := b.grow(1)\n+ if m < 0 {\n+ return ErrTooLarge\n+ }\n b.buf[m] = c\n return nil\n }\n```
### `src/pkg/bytes/buffer_test.go`
```diff
--- a/src/pkg/bytes/buffer_test.go
+++ b/src/pkg/bytes/buffer_test.go
@@ -386,3 +386,19 @@ func TestReadEmptyAtEOF(t *testing.T) {
t.Errorf("wrong count; got %d want 0", n)
}
}\n+\n+func TestHuge(t *testing.T) {\n+ // About to use tons of memory, so avoid for simple installation testing.\n+ if testing.Short() {\n+ return\n+ }\n+ b := new(Buffer)\n+ big := make([]byte, 500e6)\n+ for i := 0; i < 1000; i++ {\n+ if _, err := b.Write(big); err != nil {\n+ // Got error as expected. Stop\n+ return\n+ }\n+ }\n+ t.Error("error expected")\n+}\n```
## コアとなるコードの解説
### `src/pkg/bytes/buffer.go` の変更点
1. **`ErrTooLarge` の定義**:
`var ErrTooLarge = errors.New("bytes.Buffer: too large")`
この行は、`bytes.Buffer` が許容範囲を超えるデータを扱おうとした場合に返される新しいエラーを定義しています。これにより、メモリ割り当ての失敗がパニックではなく、Goのエラーインターフェースを介して明示的に通知されるようになります。
2. **`grow` 関数の変更**:
`grow` 関数は、バッファに `n` バイトを追加するための十分なスペースを確保する役割を担います。
以前は `buf = make([]byte, 2*cap(b.buf)+n)` のように直接 `make` を呼び出していましたが、これが `buf = makeSlice(2*cap(b.buf) + n)` に変更されました。
さらに、`if buf == nil { return -1 }` というチェックが追加されました。これは、`makeSlice` がメモリ割り当てに失敗して `nil` を返した場合に、`grow` 関数も `-1` を返すようにすることで、呼び出し元にエラーを伝播させるためのものです。
3. **`Write`, `WriteString`, `WriteByte` 関数の変更**:
これらの関数は、`grow` 関数を呼び出して必要なバッファ容量を確保します。
`m := b.grow(...)` の呼び出しの後、`if m < 0 { return ..., ErrTooLarge }` というチェックが追加されました。これは、`grow` が `-1` を返した場合(つまり、バッファの拡張に失敗した場合)に、これらの関数が `ErrTooLarge` をエラーとして返すようにするものです。これにより、これらの書き込み操作がメモリ不足で失敗した場合に、パニックではなくエラーが返されるようになります。
4. **`ReadFrom` 関数の変更**:
`ReadFrom` 関数も、内部で新しいバッファを確保する際に `makeSlice` を使用するように変更されました。
`newBuf = makeSlice(2*(cap(b.buf)-b.off) + MinRead)[:len(b.buf)-b.off]` のように `makeSlice` が使用され、`if newBuf == nil { return n, ErrTooLarge }` というチェックが追加されました。これにより、`ReadFrom` が大量のデータを読み込む際にメモリ割り当てに失敗した場合も、`ErrTooLarge` が返されるようになります。
5. **`makeSlice` ヘルパー関数の追加**:
```go
func makeSlice(n int) []byte {
if n < 0 {
return nil
}
defer func() {
recover()
}()
return make([]byte, n)
}
```
この関数は、このコミットの最も重要な部分です。
* `if n < 0 { return nil }`: 負のサイズが指定された場合は、無効な要求として `nil` を返します。
* `defer func() { recover() }()`: この `defer` ステートメントは、`make([]byte, n)` がメモリ不足によってパニックを引き起こした場合に、そのパニックを捕捉するために使用されます。`recover()` がパニックを捕捉すると、プログラムはクラッシュせずに実行を続行し、`makeSlice` は `nil` を返します。
* `return make([]byte, n)`: 実際にバイトスライスを割り当てます。この操作が成功すれば、割り当てられたスライスが返されます。
### `src/pkg/bytes/buffer_test.go` の変更点
1. **`TestHuge` 関数の追加**:
```go
func TestHuge(t *testing.T) {
if testing.Short() {
return
}
b := new(Buffer)
big := make([]byte, 500e6) // 500MB
for i := 0; i < 1000; i++ {
if _, err := b.Write(big); err != nil {
// Got error as expected. Stop
return
}
}
t.Error("error expected")
}
```
このテストは、`bytes.Buffer` が非常に大きなデータを扱う際の挙動を検証します。
* `testing.Short()` チェックにより、通常のテスト実行ではスキップされ、明示的に `go test -short=false` などで実行された場合にのみ実行されます。これは、このテストが大量のメモリを消費するためです。
* 500MBのバイトスライス `big` を作成し、それを1000回(合計500GB)`bytes.Buffer` に書き込もうとします。
* この操作は、システムメモリの限界に達し、最終的に `bytes.Buffer` が `ErrTooLarge` を返すことを期待しています。
* `if _, err := b.Write(big); err != nil { ... return }` の部分で、エラーが返されたらテストを終了します。これは、期待通りのエラー処理が行われたことを意味します。
* もしループが最後まで実行され、エラーが一度も返されなかった場合(つまり、`bytes.Buffer` が500GBものメモリを割り当てようとして成功してしまったか、パニックしてしまった場合)、`t.Error("error expected")` が呼び出され、テストは失敗します。これにより、`ErrTooLarge` が適切に発生することが保証されます。
これらの変更により、`bytes.Buffer` は、メモリ割り当ての失敗に対してより予測可能で堅牢な挙動を示すようになり、Goアプリケーションの安定性が向上しました。
## 関連リンク
* Go Issue #2743: [https://github.com/golang/go/issues/2743](https://github.com/golang/go/issues/2743)
このコミットが修正したGoのIssueです。元の議論や問題の詳細が確認できます。
## 参考にした情報源リンク
* Go言語の公式ドキュメント: `bytes` パッケージ
* Go言語の公式ドキュメント: `errors` パッケージ
* Go言語の公式ドキュメント: `panic` と `recover`
* Go言語の公式ドキュメント: `make`
* Go言語の公式ドキュメント: `io` パッケージ
* Go言語のテストに関するドキュメント: `testing` パッケージ
* Go言語のソースコード (特に `src/pkg/bytes/buffer.go` と `src/pkg/bytes/buffer_test.go`)
* Go言語のコミット履歴