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

[インデックス 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言語の panicrecover

panic は、Goプログラムの通常の実行フローを中断させるランタイムエラーです。これは、配列の範囲外アクセスやnilポインタのデリファレンスなど、回復不能な状況で発生します。パニックが発生すると、現在の関数の実行が停止し、遅延関数(defer で登録された関数)が実行され、その後呼び出しスタックを遡ってパニックが伝播します。 recover は、defer 関数内で呼び出された場合にのみ有効で、伝播しているパニックを捕捉し、プログラムの実行を再開させることができます。recover がパニックを捕捉すると、panic に渡された値が返され、プログラムはクラッシュせずに続行できます。これは、特定のクリティカルな操作において、予期せぬパニックから回復するためのメカニズムとして使用されます。

技術的詳細

このコミットの核心は、bytes.Buffer が内部でメモリを確保する際に make 関数が引き起こす可能性のあるパニックを捕捉し、それを ErrTooLarge という明示的なエラーに変換して呼び出し元に返す点にあります。

  1. ErrTooLarge の導入: bytes.Buffer がメモリ割り当てに失敗したことを示す新しいエラー変数 ErrTooLarge が定義されました。

    var ErrTooLarge = errors.New("bytes.Buffer: too large")
    
  2. 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) がメモリ不足でパニックを引き起こした場合でも、そのパニックが捕捉され、プログラムのクラッシュが防がれます。パニックが捕捉された場合、makeSlicenil を返します。
    • 正常にメモリが割り当てられた場合、make([]byte, n) が返されます。
  3. grow メソッドの変更: bytes.Buffer の内部メソッドである grow は、バッファの容量を拡張するために使用されます。このメソッドは、新しい makeSlice ヘルパー関数を使用するように変更されました。

    • 以前は make([]byte, 2*cap(b.buf)+n) を直接呼び出していましたが、これが makeSlice(2*cap(b.buf) + n) に置き換えられました。
    • makeSlicenil を返した場合(メモリ割り当て失敗または負のサイズ)、grow メソッドは -1 を返すようになりました。この -1 は、呼び出し元にバッファの拡張が不可能であることを伝えます。
  4. Write, WriteString, ReadFrom, WriteByte メソッドの変更: これらのメソッドは、bytes.Buffer にデータを書き込む主要なインターフェースです。これらのメソッドは、内部で grow メソッドを呼び出して必要な容量を確保します。

    • 変更後、これらのメソッドは grow の戻り値をチェックするようになりました。grow-1 を返した場合(または ReadFrom の場合は makeSlicenil を返した場合)、これらのメソッドは ErrTooLarge をエラーとして返します。これにより、メモリ割り当ての失敗がパニックではなく、エラーとしてアプリケーションに通知されるようになります。
  5. テストケースの追加: src/pkg/bytes/buffer_test.goTestHuge という新しいテストケースが追加されました。このテストは、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言語のコミット履歴