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

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

このコミットは、Go言語の標準ライブラリ image/gif パッケージにおけるリソースリークの修正に関するものです。具体的には、GIF画像をデコードする際に内部的に作成される lzw.ReaderClose メソッドが呼び出されていなかった問題を解決しています。これにより、ファイルディスクリプタやメモリなどのリソースが適切に解放されるようになります。

コミット

commit 48936e46a1d2d92858c5ae044a6247bdccf067c9
Author: Nigel Tao <nigeltao@golang.org>
Date:   Fri Jul 5 10:12:13 2013 +1000

    image/gif: close the lzw.Reader we create.
    
    The lzw.NewReader doc comment says, "It is the caller's responsibility
    to call Close on the ReadCloser when finished reading."
    
    Thanks to Andrew Bonventre for noticing this.
    
    R=r, dsymonds, adg
    CC=andybons, golang-dev
    https://golang.org/cl/10821043

GitHub上でのコミットページへのリンク

https://github.com/golang/go/commit/48936e46a1d2d92858c5ae044a6247bdccf067c9

元コミット内容

image/gif パッケージにおいて、内部で生成される lzw.ReaderClose メソッドが呼び出されていなかったため、リソースが適切に解放されない問題を修正しました。lzw.NewReader のドキュメントに「読み込みが完了したら ReadCloserClose を呼び出すのは呼び出し元の責任である」と明記されているにもかかわらず、これが守られていませんでした。

変更の背景

この変更の背景には、Go言語の lzw パッケージが提供する lzw.Reader の利用規約があります。lzw.NewReader 関数は io.ReadCloser インターフェースを実装したオブジェクトを返します。io.ReadCloser インターフェースは、io.Readerio.Closer の両方のインターフェースを組み合わせたものであり、Close() メソッドを持つことを意味します。

lzw.NewReader のドキュメントには、明確に「It is the caller's responsibility to call Close on the ReadCloser when finished reading.」(読み込みが完了したら ReadCloserClose を呼び出すのは呼び出し元の責任である)と記載されています。これは、lzw.Reader が内部的にファイルディスクリプタやネットワーク接続、またはその他のシステムリソースを保持している可能性があり、それらを適切に解放するためには Close() メソッドの呼び出しが不可欠であることを示唆しています。

このコミット以前の image/gif パッケージのコードでは、GIF画像をデコードする際に lzw.NewReader を使用して lzw.Reader インスタンスを作成していましたが、そのインスタンスに対して Close() メソッドを呼び出す処理が欠落していました。この欠落により、GIF画像のデコード処理が完了しても、lzw.Reader が保持していたリソースが解放されずに残り続ける、いわゆるリソースリークが発生する可能性がありました。

Andrew Bonventre氏がこの問題に気づき、Nigel Tao氏が修正を行ったことで、image/gif パッケージの堅牢性とリソース管理の正確性が向上しました。

前提知識の解説

LZW (Lempel-Ziv-Welch) 圧縮

LZWは、データ圧縮アルゴリズムの一種で、特にGIF画像フォーマットで広く使用されています。このアルゴリズムは、入力データから繰り返し出現するシーケンス(パターン)を辞書に登録し、そのシーケンスをより短いコードに置き換えることでデータを圧縮します。デコード時には、このコードを辞書を使って元のシーケンスに展開します。

Go言語の io パッケージ

Go言語の io パッケージは、I/Oプリミティブを提供します。このコミットに関連する重要なインターフェースは以下の通りです。

  • io.Reader: データを読み込むためのインターフェースで、Read(p []byte) (n int, err error) メソッドを持ちます。
  • io.Closer: リソースを閉じるためのインターフェースで、Close() error メソッドを持ちます。ファイルディスクリプタ、ネットワーク接続、メモリバッファなど、使用後に解放する必要があるリソースを持つオブジェクトがこのインターフェースを実装します。
  • io.ReadCloser: io.Readerio.Closer の両方を組み合わせたインターフェースです。つまり、データを読み込むことができ、かつリソースを閉じることができるオブジェクトがこのインターフェースを実装します。

Go言語の defer ステートメント

defer ステートメントは、Go言語の非常に強力な機能の一つです。defer に続く関数呼び出しは、その関数がリターンする直前(パニックが発生した場合でも)に実行されることを保証します。これは、リソースの解放(ファイルのクローズ、ロックの解除など)を確実に行うために非常に便利です。

例えば、ファイルを開いて読み込む場合、エラーが発生してもファイルを確実に閉じるために defer file.Close() のように記述します。

file, err := os.Open("example.txt")
if err != nil {
    return err
}
defer file.Close() // 関数がリターンする直前にfile.Close()が実行される
// ファイルの読み込み処理

image/gif パッケージ

image/gif パッケージは、Go言語の標準ライブラリの一部であり、GIF画像をエンコードおよびデコードするための機能を提供します。このパッケージは、GIFファイルの構造を解析し、ピクセルデータやカラーパレットなどの情報をGoの image.Image インターフェースに変換します。GIF画像はLZW圧縮を使用しているため、このパッケージは内部的に lzw パッケージを利用して圧縮データのデコードを行います。

技術的詳細

このコミットの技術的な核心は、lzw.Readerio.ReadCloser インターフェースを実装しているにもかかわらず、その Close() メソッドが呼び出されていなかった点にあります。

src/pkg/image/gif/reader.go ファイルの decoder 構造体の decode メソッド内で、GIF画像のピクセルデータを読み込むために lzw.NewReader が使用されていました。

lzwr := lzw.NewReader(br, lzw.LSB, int(litWidth))
if _, err = io.ReadFull(lzwr, m.Pix); err != nil {
    // ...
}

ここで作成された lzwrlzw.Reader 型であり、これは io.ReadCloser インターフェースを満たします。しかし、このコードブロックのどこにも lzwr.Close() の呼び出しがありませんでした。

lzw.Reader は、その性質上、内部的にバッファやその他の状態を保持しており、これらは Close() メソッドが呼び出されることで適切にクリーンアップされることが期待されます。Close() が呼び出されない場合、これらのリソースはガベージコレクションによって最終的に解放されるかもしれませんが、それはいつになるか保証されません。特に、ファイルディスクリプタのようなOSリソースは、OSがプロセスごとに開ける数に制限があるため、リークが続くとシステム全体のパフォーマンスに影響を与えたり、新しいファイルを開けなくなったりする可能性があります。

この修正では、defer lzwr.Close() を追加することで、decode メソッドがどのような経路で終了しても(正常終了、エラーによる早期リターン、パニックなど)、lzwr.Close() が確実に実行されるようにしました。これにより、lzw.Reader が使用していたリソースが即座に解放され、リソースリークの問題が解消されます。

これは、Go言語における「リソースは defer を使って確実にクリーンアップする」というイディオムの典型的な例であり、堅牢なコードを書く上で非常に重要なプラクティスです。

コアとなるコードの変更箇所

--- a/src/pkg/image/gif/reader.go
+++ b/src/pkg/image/gif/reader.go
@@ -190,6 +190,7 @@ func (d *decoder) decode(r io.Reader, configOnly bool) error {
 			// A wonderfully Go-like piece of magic.
 			br := &blockReader{r: d.r}
 			lzwr := lzw.NewReader(br, lzw.LSB, int(litWidth))
+			defer lzwr.Close()
 			if _, err = io.ReadFull(lzwr, m.Pix); err != nil {
 				if err != io.ErrUnexpectedEOF {
 					return err

コアとなるコードの解説

変更は src/pkg/image/gif/reader.go ファイルの decoder 構造体の decode メソッド内、具体的には190行目付近にあります。

元のコードでは、以下の行で lzw.Reader のインスタンス lzwr が作成されていました。

lzwr := lzw.NewReader(br, lzw.LSB, int(litWidth))

この lzwr は、GIF画像のピクセルデータをLZW圧縮からデコードするために使用されます。しかし、この lzwr が保持するリソースを解放するための Close() メソッドの呼び出しがありませんでした。

追加された行は以下の通りです。

defer lzwr.Close()

この defer ステートメントは、lzwr.Close() の呼び出しを、現在の関数 (decode メソッド) がリターンする直前まで遅延させます。これにより、decode メソッドが正常に完了した場合でも、途中でエラーが発生して早期リターンした場合でも、lzwr.Close() が確実に実行されることが保証されます。

この修正により、lzw.Reader が内部的に使用していたバッファやその他のリソースが適切に解放され、リソースリークが防止されます。これは、Go言語におけるリソース管理のベストプラクティスに従った、シンプルかつ効果的な修正です。

関連リンク

参考にした情報源リンク