[インデックス 19498] ファイルの概要
このコミットは、Go言語の標準ライブラリ compress/gzip パッケージにおいて、gzip.Reader の Reset メソッドが未初期化の Reader インスタンスに対しても機能するように改善するものです。これにより、gzip.Reader の再利用性が向上し、リソースの効率的な利用が可能になります。
コミット
commit 68bbf9d4642e7df8523a06b0cff37b64ea5fba57
Author: Ian Lance Taylor <iant@golang.org>
Date: Tue Jun 3 15:40:12 2014 -0700
compress/gzip: allow Reset on Reader without NewReader
Fixes #8126.
LGTM=bradfitz
R=golang-codereviews, bradfitz
CC=golang-codereviews
https://golang.org/cl/103020044
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/68bbf9d4642e7df8523a06b0cff37b64ea5fba57
元コミット内容
compress/gzip: allow Reset on Reader without NewReader
Fixes #8126.
LGTM=bradfitz
R=golang-codereviews, bradfitz
CC=golang-codereviews
https://golang.org/cl/103020044
変更の背景
この変更は、GoのIssue 8126「compress/gzip: Reset should work on an uninitialized Reader.」に対応するものです。
compress/gzip パッケージの Reader 型は、GZIP圧縮されたデータを読み込むためのインターフェースを提供します。通常、Reader インスタンスは gzip.NewReader(r io.Reader) 関数を使って作成されます。この関数は内部で Reader のフィールドを初期化し、特にCRC32チェックサムを計算するための digest フィールドも初期化します。
Reader には Reset(r io.Reader) メソッドがあり、これは既存の Reader インスタンスを再利用して、新しい入力ストリームからGZIPデータを読み込むために設計されています。これにより、新しい Reader を毎回アロケートするオーバーヘッドを避けることができます。
しかし、Issue 8126で報告された問題は、NewReader を介さずに直接 var r Reader のように宣言された未初期化の Reader インスタンスに対して Reset メソッドを呼び出すと、パニックが発生するというものでした。これは、Reset メソッドが z.digest.Reset() を呼び出す際に、z.digest が nil であるために発生していました。
このコミットは、この問題を解決し、Reset メソッドが NewReader を通さずに作成された Reader インスタンスに対しても安全に呼び出せるようにすることで、Reader の再利用性をさらに高めることを目的としています。
前提知識の解説
compress/gzipパッケージ: Go言語の標準ライブラリの一部で、GZIP形式の圧縮データ(RFC 1952で定義)を読み書きするための機能を提供します。gzip.Reader: GZIP圧縮されたデータを読み込むための型です。io.Readerインターフェースを実装しており、Readメソッドを通じて非圧縮データを読み出すことができます。io.Readerインターフェース: Go言語における基本的なI/Oインターフェースの一つで、データを読み出すためのRead([]byte) (n int, err error)メソッドを定義しています。Reset(r io.Reader) errorメソッド:gzip.Readerのメソッドで、既存のReaderインスタンスを初期状態に戻し、新しいio.Readerからデータを読み込めるように再設定します。これにより、オブジェクトの再利用が可能になり、ガベージコレクションの負荷を軽減できます。crc32.NewIEEE():hash/crc32パッケージの関数で、IEEEポリノミアル(標準的なCRC-32)を使用して新しいCRC32ハッシュを計算するhash.Hash32インターフェースを実装したインスタンスを返します。GZIP形式では、データの整合性チェックのためにCRC32チェックサムが使用されます。hash.Hash32インターフェース:Write([]byte) (n int, err error)、Sum32() uint32、Reset()などのメソッドを持つハッシュ計算インターフェースです。Reset()メソッドはハッシュの状態を初期値に戻します。- オブジェクトの再利用: プログラムにおいて、頻繁に生成・破棄されるオブジェクトがある場合、それらをプールしておき、必要に応じて再利用することで、メモリのアロケーションとガベージコレクションのオーバーヘッドを削減し、パフォーマンスを向上させる手法です。
sync.Poolなどがこの目的で利用されますが、Resetメソッドを持つ型も同様の目的で再利用されます。
技術的詳細
このコミットの核心は、gzip.Reader の Reset メソッドにおける z.digest フィールドの扱いを変更した点にあります。
変更前の Reset メソッドは、常に z.digest.Reset() を呼び出していました。しかし、gzip.Reader が NewReader 関数を介さずに、例えば var r Reader のように宣言された場合、z.digest フィールドは初期値である nil のままです。この状態で z.digest.Reset() を呼び出すと、nil ポインタに対するメソッド呼び出しとなり、ランタイムパニック(panic: runtime error: invalid memory address or nil pointer dereference)が発生していました。
このコミットでは、Reset メソッドの冒頭に以下の条件分岐を追加することでこの問題を解決しています。
if z.digest == nil {
z.digest = crc32.NewIEEE()
} else {
z.digest.Reset()
}
この変更により、Reset メソッドが呼び出された際に、z.digest が nil である(つまり、Reader がまだ NewReader で初期化されていない)場合は、新しく crc32.NewIEEE() を呼び出して digest フィールドを初期化します。z.digest が既に初期化されている(NewReader を介して作成された、または以前の Reset 呼び出しで初期化された)場合は、これまで通り z.digest.Reset() を呼び出して、既存のハッシュ計算器の状態をリセットします。
これにより、Reset メソッドは、Reader インスタンスがどのような状態であっても安全に呼び出せるようになり、Reader の柔軟な再利用が可能になりました。
また、この変更を検証するために、gunzip_test.go に TestInitialReset という新しいテストケースが追加されています。このテストは、var r Reader で宣言された未初期化の Reader インスタンスに対して Reset メソッドを呼び出し、その後にGZIPデータを正しく読み込めることを確認しています。
コアとなるコードの変更箇所
変更は主に src/pkg/compress/gzip/gunzip.go ファイルの Reader.Reset メソッドにあります。
--- a/src/pkg/compress/gzip/gunzip.go
+++ b/src/pkg/compress/gzip/gunzip.go
@@ -94,7 +94,11 @@ func NewReader(r io.Reader) (*Reader, error) {
// This permits reusing a Reader rather than allocating a new one.
func (z *Reader) Reset(r io.Reader) error {
z.r = makeReader(r)
- z.digest.Reset()
+ if z.digest == nil {
+ z.digest = crc32.NewIEEE()
+ } else {
+ z.digest.Reset()
+ }
z.size = 0
z.err = nil
return z.readHeader(true)
また、この変更を検証するためのテストコードが src/pkg/compress/gzip/gunzip_test.go に追加されています。
--- a/src/pkg/compress/gzip/gunzip_test.go
+++ b/src/pkg/compress/gzip/gunzip_test.go
@@ -353,3 +353,17 @@ func TestIssue6550(t *testing.T) {
// ok
}
}
+
+func TestInitialReset(t *testing.T) {
+ var r Reader
+ if err := r.Reset(bytes.NewReader(gunzipTests[1].gzip)); err != nil {
+ t.Error(err)
+ }
+ var buf bytes.Buffer
+ if _, err := io.Copy(&buf, &r); err != nil {
+ t.Error(err)
+ }
+ if s := buf.String(); s != gunzipTests[1].raw {
+ t.Errorf("got %q want %q", s, gunzipTests[1].raw)
+ }
+}
コアとなるコードの解説
gunzip.go の Reader.Reset メソッドにおける変更は、z.digest フィールドが nil である可能性を考慮に入れたものです。
if z.digest == nilブロック:- これは、
ReaderインスタンスがNewReader関数を介さずに、例えばvar r Readerのように直接宣言された場合に実行されます。 - この場合、
z.digestはまだ初期化されていないためnilです。 z.digest = crc32.NewIEEE()によって、新しくCRC32ハッシュ計算器が作成され、z.digestフィールドに割り当てられます。これにより、後続のGZIPデータ読み込みにおけるCRC32チェックサム計算の準備が整います。
- これは、
elseブロック (z.digest.Reset()):- これは、
Readerインスタンスが既にNewReaderを介して初期化されているか、または以前のReset呼び出しでz.digestが初期化されている場合に実行されます。 - この場合、既存の
z.digestインスタンスのReset()メソッドが呼び出され、ハッシュ計算器の状態がリセットされます。これにより、新しい入力ストリームに対するCRC32計算が最初から行われるようになります。
- これは、
この変更により、Reset メソッドは、Reader インスタンスの初期化状態に関わらず、常に安全かつ正しく動作するようになりました。
gunzip_test.go に追加された TestInitialReset テストは、この修正が意図通りに機能することを確認します。
var r ReaderでReaderインスタンスを宣言し、明示的にNewReaderを呼び出さずに未初期化の状態で開始します。r.Reset(...)を呼び出し、未初期化のReaderに対してResetが成功することを確認します。io.Copyを使ってGZIPデータを読み込み、エラーが発生しないこと、そして非圧縮データが期待通りであることを検証します。
このテストは、Reset メソッドが nil の digest フィールドを適切に処理し、Reader が正常に機能することを示す重要な役割を果たしています。
関連リンク
- Go Issue 8126: https://github.com/golang/go/issues/8126
- Go CL 103020044: https://golang.org/cl/103020044
参考にした情報源リンク
- Go Issue 8126のGitHubページ: https://github.com/golang/go/issues/8126
- Go言語の
compress/gzipパッケージドキュメント: https://pkg.go.dev/compress/gzip - Go言語の
hash/crc32パッケージドキュメント: https://pkg.go.dev/hash/crc32 - Go言語の
ioパッケージドキュメント: https://pkg.go.dev/io - RFC 1952 (GZIPファイル形式仕様): https://www.rfc-editor.org/rfc/rfc1952