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

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

このコミットは、Go言語の標準ライブラリであるarchive/zipパッケージにおける重要な修正を扱っています。具体的には、OpenReader関数で開かれたZipファイルのディスクリプタが適切に閉じられないというリソースリークの問題を解決しています。

コミット

commit ad0e8b31d82f2a220cd98463014a79211d173df7
Author: Dmitry Chestnykh <dchest@gmail.com>
Date:   Mon Nov 7 16:33:53 2011 +1100

    archive/zip: actually close file opened with OpenReader.
    
    R=golang-dev, adg
    CC=golang-dev
    https://golang.org/cl/5341044

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

https://github.com/golang/go/commit/ad0e8b31d82f2a220cd98463014a79211d173df7

元コミット内容

archive/zip: actually close file opened with OpenReader.

このコミットは、OpenReader関数によって開かれたファイルが実際に閉じられるようにするための修正です。

変更の背景

Go言語のarchive/zipパッケージは、Zipアーカイブの読み書きをサポートするための標準ライブラリです。Zipファイルは、複数のファイルやディレクトリを単一のアーカイブにまとめるための一般的な形式であり、多くのアプリケーションで利用されます。

このコミットが行われる前、archive/zipパッケージのOpenReader関数には潜在的なリソースリークの問題がありました。OpenReader関数は、指定されたZipファイルを開き、その内容を読み取るためのReadCloser構造体を返します。しかし、このReadCloser構造体は、内部で開いたファイルディスクリプタへの参照を保持していませんでした。

その結果、ReadCloserClose()メソッドが呼び出されても、基となるファイルディスクリプタが閉じられず、システムのリソース(ファイルハンドル)が解放されない状態になっていました。これは、特に多数のZipファイルを連続して処理するようなアプリケーションにおいて、ファイルハンドルの枯渇やパフォーマンスの低下を引き起こす可能性がありました。

この問題は、Go言語におけるリソース管理のベストプラクティス、特にio.Closerインターフェースの実装とdeferステートメントの適切な使用に関する理解の重要性を示しています。リソース(ファイル、ネットワーク接続など)は、使用後に必ず解放されるべきであり、そうしないとシステム全体の安定性や効率に悪影響を及ぼします。

前提知識の解説

Zipファイルフォーマット

Zipファイルは、データ圧縮とアーカイブのための一般的なファイルフォーマットです。複数のファイルやディレクトリを単一のファイルにまとめることができます。Zipファイルは、主に以下の要素で構成されます。

  • ローカルファイルヘッダ (Local File Header): 各ファイルのエントリの先頭にあり、ファイル名、圧縮方法、圧縮サイズ、非圧縮サイズなどの情報を含みます。
  • ファイルデータ (File Data): 実際のファイルの内容(圧縮されている場合もある)です。
  • データ記述子 (Data Descriptor): ローカルファイルヘッダにCRC-32、圧縮サイズ、非圧縮サイズが含まれていない場合に使用されます。
  • セントラルディレクトリファイルヘッダ (Central Directory File Header): Zipファイル内のすべてのファイルエントリに関する情報(ファイル名、圧縮方法、ファイルサイズ、ローカルヘッダへのオフセットなど)をまとめて含みます。これにより、Zipファイル全体をスキャンせずに特定のエントリにアクセスできます。
  • セントラルディレクトリレコードの終わり (End of Central Directory Record): Zipファイルの末尾にあり、セントラルディレクトリの開始位置やエントリ数などの情報を含みます。

Goのarchive/zipパッケージは、これらの構造を抽象化し、Goのio.Readerio.Writerインターフェースを通じてZipファイルの内容にアクセスできるようにします。

Go言語のarchive/zipパッケージ

archive/zipパッケージは、GoプログラムでZipアーカイブを操作するための機能を提供します。主な構造体と関数には以下のようなものがあります。

  • zip.Reader: Zipアーカイブを読み取るための構造体。
  • zip.Writer: Zipアーカイブを書き込むための構造体。
  • zip.File: Zipアーカイブ内の個々のファイルエントリを表す構造体。
  • zip.OpenReader(name string) (*ReadCloser, error): 指定されたパスのZipファイルを開き、ReadCloserを返します。
  • ReadCloser: zip.Readerio.Closerインターフェースを組み合わせた構造体で、Zipアーカイブの読み取りと、関連するリソースのクローズを可能にします。

io.Closerインターフェースとdefer

Go言語では、ファイル、ネットワーク接続、データベース接続などのシステムリソースを扱う際に、使用後にそれらを適切に解放することが非常に重要です。これを実現するために、Goはio.Closerというシンプルなインターフェースを提供しています。

  • io.Closerインターフェース:

    type Closer interface {
        Close() error
    }
    

    このインターフェースは、Close()というメソッドを1つだけ持ち、リソースを閉じ、エラーが発生した場合はそれを返します。多くのGoの標準ライブラリ(os.File, net.Conn, bufio.Readerなど)は、このio.Closerインターフェースを実装しています。

  • defer: defer文は、Go言語の強力な機能の一つで、関数がリターンする直前に指定された関数呼び出しを実行することを保証します。これは、リソースの解放(ファイルのクローズ、ロックの解除など)を確実に行うために非常によく使用されます。

    func readFile(filename string) ([]byte, error) {
        f, err := os.Open(filename)
        if err != nil {
            return nil, err
        }
        defer f.Close() // 関数が終了する前にf.Close()が呼び出されることを保証
    
        data, err := io.ReadAll(f)
        if err != nil {
            return nil, err
        }
        return data, nil
    }
    

    deferを使用することで、エラーパスや複数のリターンポイントがある場合でも、リソースのクローズを忘れる心配がなくなります。

リソースリーク (Resource Leak)

リソースリークとは、プログラムがシステムリソース(メモリ、ファイルハンドル、ネットワークソケット、データベース接続など)を割り当てた後、そのリソースを適切に解放しないままにしてしまう状態を指します。リソースリークが発生すると、以下のような問題が引き起こされる可能性があります。

  • システムリソースの枯渇: 利用可能なファイルハンドルやメモリが使い果たされ、新しいリソースを割り当てられなくなる。
  • パフォーマンスの低下: 不要なリソースがシステムに残り続けることで、システムのオーバーヘッドが増加し、全体的なパフォーマンスが低下する。
  • プログラムのクラッシュ: リソースの枯渇が原因で、プログラムが予期せぬエラーで終了する。
  • セキュリティ上の問題: 開かれたままのリソースが、悪意のある攻撃者に利用される可能性がある。

このコミットの背景にある問題は、まさにファイルディスクリプタのリソースリークであり、OpenReaderが返したReadCloserが基となるファイルを閉じられないために発生していました。

技術的詳細

このコミットの核心は、archive/zipパッケージのOpenReader関数が、開いたファイルディスクリプタ(*os.File型)を、返される*ReadCloser構造体の内部に適切に保持していなかったという点にあります。

OpenReader関数は、内部でos.Open(name)を呼び出してファイルを開き、その結果得られる*os.Fileオブジェクトをfという変数に格納します。その後、このfを使ってzip.NewReader(f, size)を呼び出し、zip.Readerを作成します。最終的に、このzip.ReaderfをラップしたReadCloser構造体を返そうとします。

しかし、修正前のコードでは、ReadCloser構造体はzip.Readerを埋め込んでいましたが、*os.Fileオブジェクトf自体への参照を保持していませんでした。ReadCloserClose()メソッドは、埋め込まれたzip.ReaderClose()メソッドを呼び出すことを期待しますが、zip.Readerはファイルの内容を読み取るためのものであり、基となる*os.Fileを閉じる責任は通常ありません。*os.Fileを閉じる責任は、それを開いたエンティティ、またはそのファイルディスクリプタを所有するエンティティにあります。

このコミットでは、ReadCloser構造体にf *os.Fileというフィールドを追加し、OpenReader関数内でr.f = fという行を追加することで、開いたファイルディスクリプタへの参照をReadCloser構造体自身が保持するようにしました。これにより、ReadCloserClose()メソッドが呼び出された際に、このr.fを適切に閉じることができるようになり、リソースリークが解消されました。

また、テストコードreader_test.goの変更も重要です。以前は単にdefer z.Close()としていましたが、Close()メソッドはエラーを返す可能性があるため、そのエラーを適切にチェックするように変更されました。これは、Go言語におけるエラーハンドリングのベストプラクティスに従ったもので、リソースのクローズが失敗した場合にその情報を捕捉し、テストで報告できるようにします。

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

src/pkg/archive/zip/reader.go

--- a/src/pkg/archive/zip/reader.go
+++ b/src/pkg/archive/zip/reader.go
@@ -60,6 +60,7 @@ func OpenReader(name string) (*ReadCloser, error) {
 		f.Close()
 		return nil, err
 	}
+	r.f = f
 	return r, nil
 }

src/pkg/archive/zip/reader_test.go

--- a/src/pkg/archive/zip/reader_test.go
+++ b/src/pkg/archive/zip/reader_test.go
@@ -98,7 +98,11 @@ func readTestZip(t *testing.T, zt ZipTest) {
 	if err == FormatError {
 		return
 	}
-	defer z.Close()
+	defer func() {
+		if err := z.Close(); err != nil {
+			t.Errorf("error %q when closing zip file", err)
+		}
+	}()
 
 	// bail here if no Files expected to be tested
 	// (there may actually be files in the zip, but we don't care)

コアとなるコードの解説

src/pkg/archive/zip/reader.go の変更

  • r.f = f の追加: OpenReader関数内で、os.Open(name)によって開かれたファイルディスクリプタfが、返されるReadCloser構造体rのフィールドfに代入されています。 修正前は、ReadCloser構造体はzip.Readerを埋め込んでいましたが、*os.Fileへの直接的な参照を持っていませんでした。この変更により、ReadCloserインスタンスが、自身が管理するべき基となるファイルディスクリプタへの参照を明示的に保持するようになります。 これにより、ReadCloserClose()メソッドが呼び出された際に、このr.fを安全に閉じることが可能になり、ファイルディスクリプタのリソースリークが解消されます。

src/pkg/archive/zip/reader_test.go の変更

  • defer z.Close() から defer func() { ... }() への変更: テスト関数readTestZip内で、ReadCloserインスタンスzを閉じるためのdefer文が変更されました。 以前は単純にdefer z.Close()としていましたが、Close()メソッドはエラーを返す可能性があるため、そのエラーを捕捉し、テストの失敗として報告するように修正されました。 新しいコードでは、無名関数をdeferすることで、z.Close()の戻り値であるerrをチェックし、もしエラーが発生していればt.Errorfを使ってテストエラーとして記録します。これは、リソースのクローズ操作が成功したかどうかをテストで検証するための堅牢なアプローチであり、Go言語におけるエラーハンドリングのベストプラクティスに沿っています。

これらの変更により、archive/zipパッケージはより堅牢になり、OpenReader関数を使用する際にファイルディスクリプタが適切に閉じられることが保証されるようになりました。

関連リンク

参考にした情報源リンク

  • Go CL 5341044: archive/zip: actually close file opened with OpenReader. (このコミットの元の変更リスト): https://golang.org/cl/5341044
  • Go言語の公式ドキュメント
  • Go言語に関する一般的なプログラミング知識とベストプラクティス
  • Zipファイルフォーマットに関する一般的な情報