[インデックス 11312] ファイルの概要
このコミットは、Go言語の標準ライブラリであるbytes.Buffer
におけるメモリ不足時の挙動を改善し、ioutil.ReadFile
が巨大なファイルを読み込む際に発生する可能性のあるメモリ不足エラーをより適切に処理するように変更するものです。具体的には、bytes.Buffer
内部で発生するメモリ割り当て失敗時のパニックをbytes.ErrTooLarge
という特定のエラーに変換し、ioutil.ReadFile
がそのパニックを捕捉してエラーとして返すように修正しています。
コミット
commit b0d2713b77f80986f688d18bd0df03ed56d6e7b5
Author: Rob Pike <r@golang.org>
Date: Sat Jan 21 09:46:59 2012 -0800
bytes.Buffer: restore panic on out-of-memory
Make the panic detectable, and use that in ioutil.ReadFile to
give an error if the file is too big.
R=golang-dev, minux.ma, bradfitz
CC=golang-dev
https://golang.org/cl/5563045
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/b0d2713b77f80986f688d18bd0df03ed56d6e7b5
元コミット内容
bytes.Buffer: restore panic on out-of-memory
Make the panic detectable, and use that in ioutil.ReadFile to give an error if the file is too big.
(日本語訳)
bytes.Buffer
: メモリ不足時のパニックを復元する
そのパニックを検出可能にし、ioutil.ReadFile
でそれを利用して、ファイルが大きすぎる場合にエラーを返すようにする。
変更の背景
Go言語では、メモリ割り当てに失敗した場合、通常はランタイムパニック(runtime: out of memory
)が発生します。しかし、bytes.Buffer
のようなデータ構造が内部でメモリを動的に確保する際、このランタイムパニックが直接発生すると、呼び出し元でそれを特定のエラーとして捕捉し、適切に処理することが困難でした。
このコミット以前のbytes.Buffer
の実装では、メモリ割り当てに失敗した場合にnil
を返したり、ErrTooLarge
を直接返したりする試みが見られましたが、これはGoのメモリ割り当ての基本的な挙動(失敗時にパニックする)と整合性が取れていませんでした。
この変更の背景には、以下の課題がありました。
- メモリ不足時の挙動の不明瞭さ:
bytes.Buffer
がメモリを使い果たした場合、どのようなエラーが返されるのか、あるいはパニックするのかが明確でなく、予測しにくい挙動でした。 ioutil.ReadFile
での巨大ファイル処理:ioutil.ReadFile
はファイル全体をメモリに読み込むため、非常に大きなファイルを読み込もうとするとメモリ不足に陥る可能性があります。この際、単なるランタイムパニックではなく、アプリケーションが捕捉してユーザーに「ファイルが大きすぎます」といった具体的なエラーメッセージを提示できるようなメカニズムが必要でした。- パニックの検出可能性: Goの
panic
/recover
メカニズムは、予期せぬエラーからの回復や、特定の状況下でのエラー伝播に利用されます。しかし、ランタイムが引き起こす一般的なOOMパニックは、特定の型を持たないため、recover
で捕捉してもそれがOOMによるものかを判別するのが困難でした。
このコミットは、bytes.Buffer
がメモリ不足に陥った際に、bytes.ErrTooLarge
という特定のパニックを意図的に発生させることで、このパニックを検出可能にし、ioutil.ReadFile
のような上位レイヤーでそれを捕捉して、よりユーザーフレンドリーなエラーに変換できるようにすることを目的としています。
前提知識の解説
このコミットを理解するためには、以下のGo言語の概念が重要です。
-
panic
とrecover
:panic
: Go言語におけるpanic
は、プログラムの通常の実行フローを中断させるメカニズムです。これは、回復不可能なエラー(例: 配列の範囲外アクセス、nil
ポインタ参照)や、プログラマーが意図的に「これ以上続行できない」と判断した場合に発生させます。panic
が発生すると、現在の関数の実行が停止し、遅延関数(defer
)が実行され、呼び出しスタックを遡ってpanic
が伝播していきます。recover
:recover
は、defer
関数内で呼び出される組み込み関数です。panic
が発生してdefer
関数が実行された際にrecover
を呼び出すと、そのpanic
を捕捉し、プログラムの実行フローを再開させることができます。recover
は、panic
が発生した際にpanic
に渡された値を返します。defer
関数内でない場所でrecover
を呼び出しても、nil
が返され、効果はありません。- エラー処理との違い: Goでは通常、エラーは
error
インターフェースを返すことで明示的に処理されます。panic
は、より深刻な、通常はプログラムを終了させるべき状況で使用されますが、recover
と組み合わせることで、特定のパニックを捕捉し、エラーに変換するといった高度なエラーハンドリングパターンを実装することも可能です。
-
bytes.Buffer
:bytes.Buffer
は、可変長のバイトシーケンスを扱うためのバッファです。io.Reader
やio.Writer
インターフェースを実装しており、バイトデータの読み書き、追加、切り詰めなどの操作を効率的に行えます。- 内部的には、
[]byte
スライスを使用してデータを保持します。データが追加されて容量が不足すると、内部のスライスは自動的に拡張されます。この拡張時に新しい、より大きなスライスをmake
関数で割り当てる必要があります。
-
io/ioutil
パッケージとioutil.ReadFile
:io/ioutil
パッケージは、I/O操作に関するユーティリティ関数を提供します。ioutil.ReadFile(filename string) ([]byte, error)
は、指定されたファイルの内容をすべて読み込み、バイトスライスとして返します。この関数は、比較的小さなファイルを読み込むのに便利ですが、ファイルサイズが大きい場合はメモリを大量に消費する可能性があります。
-
メモリ割り当てとOOM (Out Of Memory):
- Goプログラムがメモリを要求する際(例:
make
関数によるスライスやマップの作成)、システムに利用可能なメモリが不足している場合、メモリ割り当ては失敗します。 - Goランタイムは、メモリ割り当てに失敗すると、通常は
runtime: out of memory
というメッセージとともにパニックを発生させ、プログラムを終了させます。
- Goプログラムがメモリを要求する際(例:
技術的詳細
このコミットの技術的な核心は、bytes.Buffer
のメモリ割り当てロジックと、panic
/recover
メカニズムの巧妙な利用にあります。
-
bytes.Buffer
のmakeSlice
関数におけるパニックの導入:bytes.Buffer
の内部では、バッファの容量を増やす必要がある際にmakeSlice
というヘルパー関数が呼び出されます。この関数は、指定されたサイズのバイトスライスをmake([]byte, n)
で作成します。- 変更前は、
make([]byte, n)
がnil
を返す可能性を考慮していましたが、Goのmake
関数はメモリ割り当てに失敗した場合にnil
を返すのではなく、ランタイムパニックを発生させます。 - このコミットでは、
makeSlice
関数にdefer
とrecover
を導入しています。func makeSlice(n int) []byte { // ... defer func() { if recover() != nil { panic(ErrTooLarge) } }() return make([]byte, n) }
- これにより、
make([]byte, n)
がメモリ不足でランタイムパニックを起こした場合、defer
関数がそのパニックを捕捉します。そして、捕捉したパニックがnil
でない(つまり実際にパニックが発生した)場合、bytes.ErrTooLarge
という特定のパニックを再発生させます。 - この
ErrTooLarge
は、bytes
パッケージで定義されたerror
型の変数であり、panic
にerror
型の値を渡すことで、recover
で捕捉した際にその型をチェックできるようになります。
-
bytes.Buffer
のgrow
、Write
、ReadFrom
関数からのエラーハンドリングの削除:grow
、Write
、ReadFrom
といったbytes.Buffer
のメソッドは、内部でmakeSlice
を呼び出してメモリを確保します。- 変更前は、これらのメソッド内で
makeSlice
がnil
を返した場合や、その他のメモリ不足の兆候に対してErrTooLarge
を返すようなエラーハンドリングロジックが含まれていました。 - このコミットでは、
makeSlice
がErrTooLarge
パニックを発生させるようになったため、これらのメソッドから冗長なエラーチェック(例:if buf == nil { return -1 }
)が削除されました。これにより、メモリ不足の状況ではこれらのメソッドが直接ErrTooLarge
パニックを伝播するようになります。
-
ioutil.ReadFile
(内部のreadAll
)でのパニックの捕捉とエラーへの変換:ioutil.ReadFile
は内部でreadAll
関数を呼び出し、bytes.Buffer
を使用してファイルの内容を読み込みます。readAll
関数にもdefer
とrecover
が導入されました。func readAll(r io.Reader, capacity int64) (b []byte, err error) { buf := bytes.NewBuffer(make([]byte, 0, capacity)) defer func() { e := recover() if e == nil { return } if panicErr, ok := e.(error); ok && panicErr == bytes.ErrTooLarge { err = panicErr // Convert panic to error } else { panic(e) // Re-panic other panics } }() _, err = buf.ReadFrom(r) return buf.Bytes(), err }
- この
defer
関数は、buf.ReadFrom(r)
の実行中にbytes.Buffer
から伝播してきたbytes.ErrTooLarge
パニックを捕捉します。 - 捕捉したパニックが
bytes.ErrTooLarge
型であると判別できた場合、それをreadAll
関数の戻り値であるerr
変数に代入し、パニックをエラーに変換して正常な(ただしエラーを伴う)リターンパスに乗せます。 bytes.ErrTooLarge
以外のパニック(例: 別のプログラミングミスによるパニック)は、そのまま再パニックさせ、プログラムの異常終了を促します。
-
テストケースの変更:
bytes/buffer_test.go
のTestHuge
テストは、巨大なデータをbytes.Buffer
に書き込むことでメモリ不足をシミュレートします。- 変更前は、
b.Write(big)
がエラーを返すことを期待していましたが、変更後はb.Write(big)
がbytes.ErrTooLarge
パニックを発生させることを期待するように修正されました。テストもdefer
とrecover
を使ってこのパニックを捕捉し、期待通りのパニックが発生したかを検証します。
この一連の変更により、bytes.Buffer
のメモリ不足は、ランタイムのOOMパニックではなく、bytes.ErrTooLarge
という特定のパニックとして伝播するようになり、ioutil.ReadFile
のような上位の関数でこれを捕捉し、error
として適切に処理できるようになりました。これにより、巨大なファイルを読み込もうとした際のユーザー体験が向上し、より堅牢なアプリケーションを構築できるようになります。
コアとなるコードの変更箇所
src/pkg/bytes/buffer.go
makeSlice
関数にdefer
とrecover
を追加し、メモリ割り当て失敗時のランタイムパニックをbytes.ErrTooLarge
パニックに変換。
grow
, Write
, ReadFrom
関数から、メモリ割り当て失敗時のnil
チェックとエラー返却ロジックを削除。
--- a/src/pkg/bytes/buffer.go
+++ b/src/pkg/bytes/buffer.go
@@ -33,7 +33,7 @@ const (
opRead // Any other read operation.
)
-// ErrTooLarge is returned if there is too much data to fit in a buffer.
+// ErrTooLarge is passed to panic if memory cannot be allocated to store data 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;
@@ -73,8 +73,7 @@ func (b *Buffer) Reset() { b.Truncate(0) }\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+// If the buffer can't grow it will panic with ErrTooLarge.
func (b *Buffer) grow(n int) int {
m := b.Len()
// If buffer is empty, reset to recover space.
@@ -88,9 +87,6 @@ func (b *Buffer) grow(n int) int {
} else {
// not enough space anywhere
buf = makeSlice(2*cap(b.buf) + n)
- if buf == nil {
- return -1
- }
copy(buf, b.buf[b.off:])
}
b.buf = buf
@@ -102,6 +98,8 @@ func (b *Buffer) grow(n int) int {
// Write appends the contents of p to the buffer. The return\n // value n is the length of p; err is always nil.\n+// If the buffer becomes too large, Write will panic with\n+// ErrTooLarge.
func (b *Buffer) Write(p []byte) (n int, err error) {
b.lastRead = opInvalid
m := b.grow(len(p))
@@ -146,9 +144,6 @@ func (b *Buffer) ReadFrom(r io.Reader) (n int64, err error) {
// not enough space using beginning of buffer;
// double buffer capacity
newBuf = makeSlice(2*cap(b.buf) + MinRead)
- if newBuf == nil {
- return n, ErrTooLarge
- }
}
copy(newBuf, b.buf[b.off:])
b.buf = newBuf[:len(b.buf)-b.off]
@@ -167,14 +162,14 @@ func (b *Buffer) ReadFrom(r io.Reader) (n int64, err error) {\n return n, nil // err is EOF, so return nil explicitly\n }\n
-// makeSlice allocates a slice of size n, returning nil if the slice cannot be allocated.
+// makeSlice allocates a slice of size n. If the allocation fails, it panics\n+// with ErrTooLarge.
func makeSlice(n int) []byte {
- if n < 0 {
- return nil
- }
- // Catch out of memory panics.
+ // If the make fails, give a known error.
defer func() {\n- recover()\n+ if recover() != nil {
+ panic(ErrTooLarge)
+ }
}()
return make([]byte, n)
}
src/pkg/bytes/buffer_test.go
TestHuge
テストを修正し、bytes.Buffer
への大量書き込みがErrTooLarge
パニックを引き起こすことを期待するように変更。defer
とrecover
を使用してパニックを捕捉し、検証。
--- a/src/pkg/bytes/buffer_test.go
+++ b/src/pkg/bytes/buffer_test.go
@@ -392,13 +392,18 @@ func TestHuge(t *testing.T) {
if testing.Short() {
return
}
+ // We expect a panic.
+ defer func() {
+ if err, ok := recover().(error); ok && err == ErrTooLarge {
+ return
+ } else {
+ t.Error(`expected "too large" error; got`, err)
+ }
+ }()
b := new(Buffer)
big := make([]byte, 500e6)
for i := 0; i < 1000; i++ {
- if _, err := b.Write(big); err != nil {
- // Got error as expected. Stop
- return
- }
+ b.Write(big)
}
- t.Error("error expected")
+ t.Error("panic expected")
}
src/pkg/io/ioutil/ioutil.go
readAll
関数にdefer
とrecover
を追加し、bytes.ErrTooLarge
パニックを捕捉してerror
に変換するように変更。他のパニックは再パニックさせる。
--- a/src/pkg/io/ioutil/ioutil.go
+++ b/src/pkg/io/ioutil/ioutil.go
@@ -14,9 +14,22 @@ import (
// readAll reads from r until an error or EOF and returns the data it read
// from the internal buffer allocated with a specified capacity.
-func readAll(r io.Reader, capacity int64) ([]byte, error) {
+func readAll(r io.Reader, capacity int64) (b []byte, err error) {
buf := bytes.NewBuffer(make([]byte, 0, capacity))
- _, err := buf.ReadFrom(r)
+ // If the buffer overflows, we will get bytes.ErrTooLarge.
+ // Return that as an error. Any other panic remains.
+ defer func() {
+ e := recover()
+ if e == nil {
+ return
+ }
+ if panicErr, ok := e.(error); ok && panicErr == bytes.ErrTooLarge {
+ err = panicErr
+ } else {
+ panic(e)
+ }
+ }()
+ _, err = buf.ReadFrom(r)
return buf.Bytes(), err
}
コアとなるコードの解説
このコミットの核心は、Go言語のpanic
とrecover
メカニズムを、特定のランタイムエラー(メモリ不足)をアプリケーションレベルのエラーに変換するために利用している点です。
-
bytes.Buffer
のmakeSlice
関数:- この関数は、
bytes.Buffer
が内部で使用するバイトスライスを実際に割り当てる部分です。 make([]byte, n)
は、要求されたサイズのメモリを割り当てます。もしシステムに十分なメモリがない場合、Goランタイムはruntime: out of memory
というパニックを発生させます。defer func() { if recover() != nil { panic(ErrTooLarge) } }()
というコードは、このランタイムパニックを捕捉します。recover()
がnil
でない場合(つまりパニックが発生した場合)、元のランタイムパニックを破棄し、代わりにbytes.ErrTooLarge
という、より具体的で型付けされたパニックを再発生させます。- これにより、
bytes.Buffer
のメモリ割り当て失敗は、常にbytes.ErrTooLarge
という予測可能なパニックとして外部に伝播するようになります。
- この関数は、
-
ioutil.ReadFile
(内部のreadAll
関数):ioutil.ReadFile
は、ファイルの内容をbytes.Buffer
に読み込みます。readAll
関数内のdefer
ブロックは、bytes.Buffer
からのパニックを捕捉するために設置されています。if panicErr, ok := e.(error); ok && panicErr == bytes.ErrTooLarge
という条件は、捕捉したパニックがerror
型であり、かつそれがbytes.ErrTooLarge
と同一であるかを厳密にチェックします。- もし条件が真であれば、
err = panicErr
として、パニックを通常のerror
戻り値に変換します。これにより、ioutil.ReadFile
の呼び出し元は、if err != nil
という通常のGoのエラーハンドリングパターンでメモリ不足エラーを処理できるようになります。 else { panic(e) }
の部分は重要です。これは、bytes.ErrTooLarge
以外のパニック(例えば、bytes.Buffer
とは無関係な、より深刻なプログラミングエラーによるパニック)は、このreadAll
関数では処理せず、そのまま上位に再パニックさせることを意味します。これにより、意図しないパニックが隠蔽されるのを防ぎ、プログラムの健全性を保ちます。
この変更は、Go言語におけるエラーとパニックの使い分けの好例を示しています。回復可能な、あるいは特定の状況下で予期されるエラー(ファイルが大きすぎる)はerror
として処理されるべきであり、回復不可能な、あるいは予期せぬエラー(一般的なOOMやプログラミングミス)はpanic
として処理されるべきです。このコミットは、ランタイムパニックを特定のアプリケーションエラーに「昇格」させることで、より堅牢でユーザーフレンドリーなエラーハンドリングを実現しています。
関連リンク
- Go言語の
panic
とrecover
に関する公式ドキュメントやチュートリアル - Go言語の
bytes.Buffer
に関する公式ドキュメント - Go言語の
io/ioutil
パッケージに関する公式ドキュメント - Go言語のエラーハンドリングに関するベストプラクティス
参考にした情報源リンク
- Go言語の
panic
とrecover
について - Go言語の
bytes
パッケージドキュメント - Go言語の
io/ioutil
パッケージドキュメント - Go言語におけるエラー処理の考え方
- golang/go GitHubリポジトリ
- Go CL 5563045 (コミットメッセージに記載されている変更リストへのリンク)