[インデックス 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 (コミットメッセージに記載されている変更リストへのリンク)