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

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

このコミットは、Go言語の標準ライブラリであるcompress/gzipパッケージに、Writer型のためのFlushメソッドを追加するものです。これにより、gzip圧縮されたデータを書き込む際に、バッファリングされたデータを強制的に下層のライターに書き出す機能が提供されます。特に、ネットワークプロトコルなど、リアルタイム性やパケット単位でのデータ転送が求められるシナリオにおいて重要な機能強化となります。

コミット

commit 4432be3b288976fd3f8a398801803020ec78c7f5
Author: Brad Fitzpatrick <bradfitz@golang.org>
Date:   Tue Apr 2 09:07:43 2013 -0700

    compress/gzip: add Writer.Flush to call flate.Writer's Flush
    
    From a discussion on golang-nuts.
    
    R=golang-dev, dsymonds, nigeltao, coocood, adg
    CC=golang-dev
    https://golang.org/cl/8251043

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

https://github.com/golang/go/commit/4432be3b288976fd3f8a398801803020ec78c7f5

元コミット内容

このコミットは、compress/gzipパッケージのWriter型にFlushメソッドを追加し、その内部で利用しているflate.WriterFlushメソッドを呼び出すように変更します。この変更は、golang-nutsメーリングリストでの議論から生まれたものです。

変更されたファイルは以下の通りです。

  • doc/go1.1.html: Go 1.1のリリースノートにcompress/gzipパッケージのWriter.Flushメソッドの追加を記述。
  • src/pkg/compress/gzip/gzip.go: gzip.Writer構造体にFlushメソッドを実装し、内部のcompressorフィールドの型をio.WriteCloserから*flate.Writerに変更。
  • src/pkg/compress/gzip/gzip_test.go: Writer.Flushメソッドの動作を検証するためのテストケースTestWriterFlushを追加。

変更の背景

Go言語のcompress/gzipパッケージは、gzip形式でのデータ圧縮と伸長を提供します。しかし、このコミット以前のgzip.Writerには、バッファリングされた圧縮データを強制的に下層のライターに書き出すためのFlushメソッドがありませんでした。

一般的なデータストリーム処理において、特にネットワーク通信のようなリアルタイム性が求められるシナリオでは、書き込み操作が即座に下層のI/Oに反映されることが重要です。データが内部バッファに留まったままだと、受信側がデータを完全に受け取ることができず、プロトコルのタイムアウトや不完全なパケットの再構築といった問題が発生する可能性があります。

この問題はgolang-nutsメーリングリストで議論され、gzip.WriterにもFlush機能が必要であるという結論に至りました。これにより、ユーザーは必要に応じて圧縮データを明示的にフラッシュし、下層のストリームに確実に書き出すことができるようになります。これは、例えばHTTPのチャンク転送エンコーディングやWebSocketのようなプロトコルで、圧縮されたデータをリアルタイムに送信する際に不可欠な機能です。

前提知識の解説

Gzip圧縮

Gzip (GNU zip) は、主に単一ファイルの圧縮に使用されるファイルフォーマットおよびデータ圧縮プログラムです。内部的にはDEFLATEアルゴリズム(LZ77とハフマン符号化の組み合わせ)を使用しており、これはRFC 1951で定義されています。Gzipは、HTTP通信におけるコンテンツエンコーディング(Content-Encoding: gzip)としても広く利用されており、ウェブページの転送量を削減し、表示速度を向上させるのに貢献しています。

ZlibライブラリとZ_SYNC_FLUSH

Zlibは、DEFLATE圧縮アルゴリズムを実装したオープンソースのデータ圧縮ライブラリです。多くのプログラミング言語やアプリケーションで利用されており、gzipもzlibの機能を利用しています。 Zlibには、圧縮ストリームのフラッシュに関するいくつかのモードがあります。

  • Z_NO_FLUSH: デフォルトの動作。可能な限り多くのデータをバッファリングし、最適な圧縮率を目指します。
  • Z_SYNC_FLUSH: 現在バッファリングされているすべての出力データを強制的に下層のストリームに書き出します。この際、圧縮状態は維持されるため、後続のデータも同じ圧縮ストリームで処理を続けることができます。これは、ネットワークプロトコルでパケットの境界をマークしたり、部分的なデータを送信したりするのに役立ちます。
  • Z_FULL_FLUSH: Z_SYNC_FLUSHと同様にすべてのデータをフラッシュしますが、さらに圧縮状態をリセットします。これにより、後続のデータは独立した新しいブロックとして圧縮されます。これは、エラー回復やランダムアクセスが必要な場合に有用ですが、圧縮効率は低下します。

このコミットで追加されたgzip.Writer.Flushは、zlibのZ_SYNC_FLUSHに相当する機能を提供します。

Go言語のioパッケージ

Go言語のioパッケージは、I/Oプリミティブを提供します。

  • io.Writerインターフェース: Write([]byte) (n int, err error)メソッドを持つ型が実装するインターフェースで、バイトスライスを書き込む機能を提供します。
  • io.WriteCloserインターフェース: io.Writerio.CloserClose() errorメソッドを持つ)の両方を実装するインターフェースです。

Go言語のcompress/flateパッケージ

Go言語のcompress/flateパッケージは、DEFLATE圧縮アルゴリズムの実装を提供します。gzip.Writerは内部的にflate.Writerを使用して実際のDEFLATE圧縮を行っています。flate.Writerは、Flush()メソッドを持っており、これが今回のgzip.Writer.Flushの実装の基盤となります。

ネットワークプロトコルとバッファリング

ネットワーク通信では、効率のためにデータは通常、内部バッファに蓄積されてからまとめて送信されます。しかし、一部のプロトコルでは、特定のイベントやタイムアウト時に、バッファリングされたデータを強制的に送信する必要があります。例えば、HTTP/1.1のチャンク転送エンコーディングでは、サーバーはレスポンスボディを小さなチャンクに分割して送信できます。この際、各チャンクの送信後にバッファをフラッシュすることで、クライアントはデータを受信次第処理を開始できます。Flushメソッドは、このようなシナリオで、圧縮されたデータがバッファに留まることなく、即座にネットワークに送られることを保証するために不可欠です。

技術的詳細

このコミットの技術的な核心は、compress/gzipパッケージのWriter型にFlushメソッドを追加し、その内部実装をcompress/flateパッケージのflate.WriterFlushメソッドに委譲することです。

  1. gzip.Writer構造体の変更: 以前のgzip.Writer構造体では、内部の圧縮器を保持するフィールドcompressorio.WriteCloser型として宣言されていました。

    type Writer struct {
        // ...
        compressor io.WriteCloser
        // ...
    }
    

    しかし、io.WriteCloserインターフェースにはFlush()メソッドが定義されていません。flate.WriterFlush()メソッドを持っていますが、インターフェース型ではそのメソッドを直接呼び出すことができません。 このコミットでは、compressorフィールドの型を具体的な*flate.Writerに変更しました。

    type Writer struct {
        // ...
        compressor *flate.Writer // 変更点: io.WriteCloser から *flate.Writer へ
        // ...
    }
    

    この変更により、gzip.Writerは内部のflate.Writerインスタンスに直接アクセスし、そのFlush()メソッドを呼び出すことが可能になります。

  2. Writer.Flush()メソッドの実装: 新しく追加されたFlush()メソッドは以下のロジックで実装されています。

    // Flush flushes any pending compressed data to the underlying writer.
    //
    // It is useful mainly in compressed network protocols, to ensure that
    // a remote reader has enough data to reconstruct a packet. Flush does
    // not return until the data has been written. If the underlying
    // writer returns an error, Flush returns that error.
    //
    // In the terminology of the zlib library, Flush is equivalent to Z_SYNC_FLUSH.
    func (z *Writer) Flush() error {
        if z.err != nil {
            return z.err
        }
        if z.closed {
            return nil
        }
        if z.compressor == nil {
            // If no data has been written yet, the compressor might not be initialized.
            // Writing nil bytes will initialize it without actually writing data.
            z.Write(nil)
        }
        z.err = z.compressor.Flush() // 内部のflate.WriterのFlushを呼び出す
        return z.err
    }
    
    • まず、既存のエラー(z.err)がある場合はそれを返します。
    • Writerが既に閉じられている(z.closedがtrue)場合は、何もしません。
    • z.compressornilの場合(つまり、まだデータが書き込まれておらず、圧縮器が初期化されていない場合)、z.Write(nil)を呼び出して圧縮器を初期化します。Write(nil)はデータを書き込みませんが、内部状態を適切に設定します。
    • 最後に、内部のz.compressor*flate.Writer型)のFlush()メソッドを呼び出し、その結果をz.errに格納して返します。
  3. ドキュメントの更新: doc/go1.1.htmlに、Go 1.1の変更点としてcompress/gzipパッケージにWriter.Flushメソッドが追加されたことが明記されました。これにより、ユーザーは新しい機能の存在を認識できます。

  4. テストケースの追加: src/pkg/compress/gzip/gzip_test.goTestWriterFlushという新しいテスト関数が追加されました。このテストは、Flushメソッドが正しく動作することを確認します。具体的には、Flushを呼び出す前後のバッファサイズを比較し、データが正しくフラッシュされていること、そしてFlushが複数回呼び出されても期待通りの動作をすること(例えば、データが書き込まれていない状態でFlushを呼び出してもエラーにならないこと)を検証しています。

これらの変更により、gzip.Writerはより柔軟になり、特にストリーミング圧縮が必要なアプリケーションにおいて、データの制御性が向上しました。

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

src/pkg/compress/gzip/gzip.go

--- a/src/pkg/compress/gzip/gzip.go
+++ b/src/pkg/compress/gzip/gzip.go
@@ -28,7 +28,7 @@ type Writer struct {
 	Header
 	w          io.Writer
 	level      int
-	compressor io.WriteCloser
+	compressor *flate.Writer // 変更点: 型がio.WriteCloserから*flate.Writerへ
 	digest     hash.Hash32
 	size       uint32
 	closed     bool
@@ -191,6 +191,28 @@ func (z *Writer) Write(p []byte) (int, error) {
 	return n, z.err
 }
 
+// Flush flushes any pending compressed data to the underlying writer.
+//
+// It is useful mainly in compressed network protocols, to ensure that
+// a remote reader has enough data to reconstruct a packet. Flush does
+// not return until the data has been written. If the underlying
+// writer returns an error, Flush returns that error.
+//
+// In the terminology of the zlib library, Flush is equivalent to Z_SYNC_FLUSH.
+func (z *Writer) Flush() error {
+	if z.err != nil {
+		return z.err
+	}
+	if z.closed {
+		return nil
+	}
+	if z.compressor == nil {
+		z.Write(nil)
+	}
+	z.err = z.compressor.Flush() // 新しいFlushメソッドの実装
+	return z.err
+}
+
 // Close closes the Writer. It does not close the underlying io.Writer.
 func (z *Writer) Close() error {
 	if z.err != nil {

src/pkg/compress/gzip/gzip_test.go

--- a/src/pkg/compress/gzip/gzip_test.go
+++ b/src/pkg/compress/gzip/gzip_test.go
@@ -157,3 +157,43 @@ func TestLatin1RoundTrip(t *testing.T) {
 		}
 	}\n
 }\n
+\n+func TestWriterFlush(t *testing.T) { // 新しいテストケース
+\tbuf := new(bytes.Buffer)\n
+\n+\tw := NewWriter(buf)\n+\tw.Comment = \"comment\"\n+\tw.Extra = []byte(\"extra\")\n+\tw.ModTime = time.Unix(1e8, 0)\n+\tw.Name = \"name\"\n+\n+\tn0 := buf.Len()\n+\tif n0 != 0 {\n+\t\tt.Fatalf(\"buffer size = %d before writes; want 0\", n0)\n+\t}\n+\n+\tif err := w.Flush(); err != nil {\n+\t\tt.Fatal(err)\n+\t}\n+\n+\tn1 := buf.Len()\n+\tif n1 == 0 {\n+\t\tt.Fatal(\"no data after first flush\")\n+\t}\n+\n+\tw.Write([]byte(\"x\"))\n+\n+\tn2 := buf.Len()\n+\tif n1 != n2 {\n+\t\tt.Fatalf(\"after writing a single byte, size changed from %d to %d; want no change\", n1, n2)\n+\t}\n+\n+\tif err := w.Flush(); err != nil {\n+\t\tt.Fatal(err)\n+\t}\n+\n+\tn3 := buf.Len()\n+\tif n2 == n3 {\n+\t\tt.Fatal(\"Flush didn\'t flush any data\")\n+\t}\n+}\n```

## コアとなるコードの解説

### `gzip.go`における変更

1.  **`compressor`フィールドの型変更**:
    `Writer`構造体の`compressor`フィールドの型が`io.WriteCloser`から`*flate.Writer`に変更されました。これは、`io.WriteCloser`インターフェースには`Flush()`メソッドが定義されていないため、`flate.Writer`が持つ`Flush()`メソッドを直接呼び出すために必要な変更です。これにより、`gzip.Writer`は内部のDEFLATE圧縮器のフラッシュ機能を直接制御できるようになります。

2.  **`Flush()`メソッドの追加**:
    -   `func (z *Writer) Flush() error`: このメソッドが`gzip.Writer`に追加されました。
    -   `if z.err != nil { return z.err }`: 以前の操作でエラーが発生している場合、そのエラーをすぐに返します。
    -   `if z.closed { return nil }`: `Writer`が既に`Close()`されている場合、それ以上の操作は不要なので`nil`を返します。
    -   `if z.compressor == nil { z.Write(nil) }`: これは重要な初期化ロジックです。`gzip.Writer`は、最初の`Write`呼び出しが行われるまで内部の`flate.Writer`(`z.compressor`)を遅延初期化する場合があります。`Flush`が`Write`より先に呼び出された場合、`z.compressor`が`nil`のままになる可能性があります。`z.Write(nil)`を呼び出すことで、実際にデータを書き込むことなく`z.compressor`が確実に初期化されるようにします。
    -   `z.err = z.compressor.Flush()`: 内部の`flate.Writer`の`Flush()`メソッドを呼び出します。この`flate.Writer.Flush()`が、バッファリングされた圧縮データを下層の`io.Writer`に強制的に書き出す実際の処理を行います。その結果発生したエラーは`z.err`に格納され、メソッドの戻り値として返されます。

### `gzip_test.go`における変更

1.  **`TestWriterFlush`関数の追加**:
    このテスト関数は、`Writer.Flush()`メソッドの動作を検証するために追加されました。
    -   `buf := new(bytes.Buffer)`: 圧縮されたデータが書き込まれるメモリバッファを作成します。
    -   `w := NewWriter(buf)`: `gzip.Writer`の新しいインスタンスを作成し、`buf`を書き込み先として設定します。
    -   初期状態でのバッファサイズが0であることを確認します。
    -   `w.Flush()`を呼び出し、エラーがないことを確認します。この時点ではまだデータが書き込まれていないため、`Flush`は圧縮器を初期化するだけで、バッファにはヘッダ情報などが書き込まれる可能性があります。
    -   `n1 := buf.Len()`: 最初の`Flush`後のバッファサイズを記録します。これが0でないことを確認し、`Flush`が何らかのデータを(例えばgzipヘッダ)書き出したことを検証します。
    -   `w.Write([]byte("x"))`: 1バイトのデータを書き込みます。この時点では、データは内部バッファに保持され、すぐにフラッシュされないことが期待されます。
    -   `n2 := buf.Len()`: 1バイト書き込んだ後のバッファサイズを記録します。`n1`と`n2`が同じであることを確認し、`Write`がすぐにフラッシュしないことを検証します。
    -   `if err := w.Flush(); err != nil { ... }`: 再度`Flush()`を呼び出します。
    -   `n3 := buf.Len()`: 2回目の`Flush`後のバッファサイズを記録します。
    -   `if n2 == n3 { t.Fatal("Flush didn't flush any data") }`: `n2`と`n3`が異なることを確認し、2回目の`Flush`が実際にバッファリングされていた「x」の圧縮データなどをフラッシュしたことを検証します。

このテストは、`Flush`メソッドが期待通りに動作し、バッファリングされたデータを強制的に書き出す機能を提供していることを保証します。

## 関連リンク

-   Go言語の`compress/gzip`パッケージのドキュメント: [https://pkg.go.dev/compress/gzip](https://pkg.go.dev/compress/gzip)
-   Go言語の`compress/flate`パッケージのドキュメント: [https://pkg.go.dev/compress/flate](https://pkg.go.dev/compress/flate)
-   Go言語の`io`パッケージのドキュメント: [https://pkg.go.dev/io](https://pkg.go.dev/io)
-   RFC 1951 - DEFLATE Compressed Data Format Specification version 1.3: [https://www.rfc-editor.org/rfc/rfc1951](https://www.rfc-editor.org/rfc/rfc1951)
-   RFC 1952 - GZIP File Format Specification version 4.3: [https://www.rfc-editor.org/rfc/rfc1952](https://www.rfc-editor.org/rfc/rfc1952)

## 参考にした情報源リンク

-   golang-nutsメーリングリストの議論 (コミットメッセージに記載の`https://golang.org/cl/8251043`はGoのChange Listへのリンクであり、議論の直接的なリンクではないが、関連する情報源として参照)
-   Zlib Manual: [https://www.zlib.net/manual.html](https://www.zlib.net/manual.html) (特に`Z_SYNC_FLUSH`に関する説明)
-   Go言語のソースコード(`src/pkg/compress/gzip/gzip.go`および`src/pkg/compress/gzip/gzip_test.go`)