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

[インデックス 19498] ファイルの概要

このコミットは、Go言語の標準ライブラリ compress/gzip パッケージにおいて、gzip.ReaderReset メソッドが未初期化の 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.digestnil であるために発生していました。

このコミットは、この問題を解決し、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() uint32Reset() などのメソッドを持つハッシュ計算インターフェースです。Reset() メソッドはハッシュの状態を初期値に戻します。
  • オブジェクトの再利用: プログラムにおいて、頻繁に生成・破棄されるオブジェクトがある場合、それらをプールしておき、必要に応じて再利用することで、メモリのアロケーションとガベージコレクションのオーバーヘッドを削減し、パフォーマンスを向上させる手法です。sync.Pool などがこの目的で利用されますが、Reset メソッドを持つ型も同様の目的で再利用されます。

技術的詳細

このコミットの核心は、gzip.ReaderReset メソッドにおける z.digest フィールドの扱いを変更した点にあります。

変更前の Reset メソッドは、常に z.digest.Reset() を呼び出していました。しかし、gzip.ReaderNewReader 関数を介さずに、例えば 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.digestnil である(つまり、Reader がまだ NewReader で初期化されていない)場合は、新しく crc32.NewIEEE() を呼び出して digest フィールドを初期化します。z.digest が既に初期化されている(NewReader を介して作成された、または以前の Reset 呼び出しで初期化された)場合は、これまで通り z.digest.Reset() を呼び出して、既存のハッシュ計算器の状態をリセットします。

これにより、Reset メソッドは、Reader インスタンスがどのような状態であっても安全に呼び出せるようになり、Reader の柔軟な再利用が可能になりました。

また、この変更を検証するために、gunzip_test.goTestInitialReset という新しいテストケースが追加されています。このテストは、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.goReader.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 テストは、この修正が意図通りに機能することを確認します。

  1. var r ReaderReader インスタンスを宣言し、明示的に NewReader を呼び出さずに未初期化の状態で開始します。
  2. r.Reset(...) を呼び出し、未初期化の Reader に対して Reset が成功することを確認します。
  3. io.Copy を使ってGZIPデータを読み込み、エラーが発生しないこと、そして非圧縮データが期待通りであることを検証します。

このテストは、Reset メソッドが nildigest フィールドを適切に処理し、Reader が正常に機能することを示す重要な役割を果たしています。

関連リンク

参考にした情報源リンク