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

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

このコミットは、Go言語の標準ライブラリである archive/zip パッケージにおける *zip.Writer 型から Write メソッドを削除し、その実装詳細を隠蔽することを目的としています。これにより、*zip.Writer が直接書き込みを行うべきではないという設計意図が明確化され、APIの健全性が向上します。

コミット

commit 04868b28ac5b3ff608a58b4dbb7daa87f75fd660
Author: Andrew Gerrand <adg@golang.org>
Date:   Tue Feb 14 10:47:48 2012 +1100

    archive/zip: hide Write method from *Writer type
    
    This was an implementation detail that snuck into the public interface.
    *Writer.Create gives you an io.Writer, the *Writer itself was never
    meant to be written to.
    
    R=golang-dev, dsymonds, r
    CC=golang-dev
    https://golang.org/cl/5654076

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

https://github.com/golang/go/commit/04868b28ac5b3ff608a58b4dbb7daa87f75fd660

元コミット内容

archive/zip: hide Write method from *Writer type

This was an implementation detail that snuck into the public interface.
*Writer.Create gives you an io.Writer, the *Writer itself was never
meant to be written to.

変更の背景

この変更の背景には、Go言語の archive/zip パッケージにおける *zip.Writer 型の設計意図と、それが誤って公開インターフェースに露出してしまった問題があります。

zip.Writer はZIPアーカイブを作成するための主要な型ですが、その内部では io.Writer インターフェースを実装する countWriter という構造体を埋め込んでいました。Go言語の埋め込み(embedding)の仕組みにより、埋め込まれた型のメソッドは外側の型(この場合は zip.Writer)のメソッドとして「昇格」し、外部から直接呼び出すことが可能になります。

この場合、countWriter が持つ Write メソッドが *zip.Writer の公開メソッドとして利用可能になっていました。しかし、zip.Writer の設計思想としては、ZIPアーカイブ全体への直接の書き込みは想定されていませんでした。代わりに、*zip.WriterCreate メソッド(または CreateHeader メソッド)を呼び出すことで、個々のファイルエントリへの書き込みを行うための io.Writer インターフェースが返されるべきでした。ユーザーは、この返された io.Writer を通じてファイルの内容を書き込むことが期待されていました。

*zip.Writer が直接 Write メソッドを持つことは、以下のような問題を引き起こす可能性がありました。

  1. 誤解を招くAPI: ユーザーが *zip.Writer に対して直接 Write を呼び出すと、ZIPアーカイブの構造が壊れたり、意図しないデータが書き込まれたりする可能性がありました。これは、Write メソッドがZIPファイルフォーマットの特定のセクション(例えば、ファイルデータ部分)にのみ適用されるべき内部的な操作であったためです。
  2. 不適切な使用: *zip.Writer に直接書き込むことは、ZIPフォーマットの整合性を保つ上で必要なヘッダー情報や中央ディレクトリの管理をスキップしてしまうことになり、結果として破損したZIPファイルが生成されるリスクがありました。
  3. 設計意図の曖昧化: *zip.Writerio.Writer のように振る舞うことで、Create メソッドが返す io.Writer との役割分担が不明瞭になり、APIの利用方法が混乱する原因となっていました。

このコミットは、この「実装詳細が公開インターフェースに漏れ出してしまった」状態を修正し、*zip.Writer のAPIをより意図に沿ったものにするために行われました。具体的には、countWriter を直接埋め込むのではなく、ポインタとして保持することで、その Write メソッドが *zip.Writer の公開メソッドとして昇格しないように変更されました。これにより、コンパイラが誤った使用を検出し、開発者が正しいAPI(Create メソッドが返す io.Writer)を使用するように促すことができます。

Go 1のリリースを控えていた時期であり、APIの安定性と正確性を確保するための重要な変更の一つでした。

前提知識の解説

このコミットを理解するためには、以下のGo言語の概念とZIPファイルフォーマットに関する基本的な知識が必要です。

1. Go言語の構造体の埋め込み (Embedding)

Go言語では、構造体の中に別の構造体をフィールド名なしで宣言することで、「埋め込み」を行うことができます。埋め込まれた構造体のメソッドは、外側の構造体のメソッドとして「昇格」し、外側の構造体のインスタンスから直接呼び出すことができます。これは、継承に似た振る舞いを実現しますが、Goでは「コンポジション(合成)による再利用」を推奨しています。

例:

type Inner struct {
    Value int
}

func (i Inner) Method() {
    fmt.Println("Inner method called")
}

type Outer struct {
    Inner // Inner構造体を埋め込み
}

func main() {
    o := Outer{}
    o.Method() // InnerのMethodがOuterのメソッドとして呼び出せる
}

このコミットでは、zip.WritercountWriter を埋め込んでいたため、countWriterWrite メソッドが zip.WriterWrite メソッドとして公開されていました。

2. io.Writer インターフェース

Go言語の io パッケージは、I/O操作のための基本的なインターフェースを提供します。io.Writer はその中でも最も基本的なインターフェースの一つで、データを書き込むための単一のメソッド Write を定義しています。

type Writer interface {
    Write(p []byte) (n int, err error)
}

このインターフェースは、ファイル、ネットワーク接続、バッファなど、様々な出力先への書き込みを抽象化するために広く使用されます。zip.WriterCreate メソッドが io.Writer を返すのは、ユーザーが個々のZIPエントリにデータを書き込むための標準的な方法を提供するためです。

3. ZIPファイルフォーマットの基本

ZIPファイルは、複数のファイルやディレクトリを一つのアーカイブにまとめるための一般的なフォーマットです。その構造は比較的複雑で、主に以下の要素で構成されます。

  • ローカルファイルヘッダー (Local File Header): 各ファイルエントリの先頭にあり、ファイル名、圧縮方法、圧縮・非圧縮サイズ、CRC-32チェックサムなどの情報を含みます。
  • ファイルデータ (File Data): 実際のファイルの内容(圧縮されている場合もある)。
  • データ記述子 (Data Descriptor): ローカルファイルヘッダーの後に続くオプションのセクションで、圧縮・非圧縮サイズやCRC-32チェックサムが事前に不明な場合に使用されます。
  • 中央ディレクトリ (Central Directory): ZIPファイルの末尾に位置し、アーカイブ内のすべてのファイルエントリに関する情報(ローカルファイルヘッダーとほぼ同じ情報に加えて、ローカルファイルヘッダーへのオフセットなど)を一元的に管理します。これにより、ZIPファイル全体をスキャンせずに特定のファイルエントリを見つけることができます。
  • 中央ディレクトリ終了レコード (End of Central Directory Record): ZIPファイルの最後のセクションで、中央ディレクトリの開始位置、サイズ、エントリ数などの情報を含みます。

zip.Writer は、これらの複雑な構造を適切に管理し、整合性のあるZIPファイルを生成する責任を負います。Write メソッドが直接 zip.Writer に存在すると、これらの内部的なフォーマット管理ロジックを迂回してしまい、ZIPファイルの破損につながる可能性がありました。

4. 公開インターフェースと実装詳細

ソフトウェア設計において、公開インターフェース(Public API)と実装詳細(Implementation Details)を明確に区別することは非常に重要です。

  • 公開インターフェース: 外部のユーザーが利用することを意図した部分です。安定しており、変更は慎重に行われるべきです。
  • 実装詳細: 内部的な動作やデータ構造であり、外部からは見えないようにすべき部分です。これらは、公開インターフェースに影響を与えることなく、自由に(または比較的自由に)変更できます。

このコミットは、Write メソッドが本来は zip.Writer の実装詳細の一部であるべきだったにもかかわらず、Goの埋め込みの仕組みによって誤って公開インターフェースに露出してしまった問題を修正しています。実装詳細を隠蔽することで、APIの利用方法が明確になり、将来的な内部変更が容易になります。

技術的詳細

このコミットの技術的な核心は、Go言語の構造体の埋め込みの挙動を利用して、意図しないメソッドの公開を防ぐ点にあります。

src/pkg/archive/zip/writer.go における Writer 構造体の定義が変更されました。

変更前:

type Writer struct {
    countWriter // countWriterを直接埋め込み
    dir    []*header
    last   *fileWriter
    closed bool
}

この定義では、countWriter がフィールド名なしで埋め込まれています。Goの言語仕様により、countWriter が持つすべてのメソッド(この場合は Write メソッド)は、Writer 型のメソッドとして「昇格」し、*Writer のインスタンスから直接呼び出すことが可能になります。

例えば、w := NewWriter(...) とした後に w.Write(...) と呼び出すことができていました。しかし、前述の通り、*zip.Writer はZIPアーカイブ全体への直接の書き込みを意図しておらず、個々のファイルエントリへの書き込みは *zip.Writer.Create が返す io.Writer を通じて行うべきでした。

変更後:

type Writer struct {
    cw     *countWriter // countWriterへのポインタをフィールドとして保持
    dir    []*header
    last   *fileWriter
    closed bool
}

この変更では、countWriter を直接埋め込む代わりに、*countWriter 型のフィールド cw を明示的に宣言しています。これにより、countWriter のメソッドは Writer 型に昇格しなくなります。つまり、w := NewWriter(...) とした後に w.Write(...) を呼び出そうとすると、コンパイルエラーになります。

countWriterWrite メソッドにアクセスする必要がある場合は、明示的に w.cw.Write(...) のようにフィールド cw を介してアクセスする必要があります。これは、countWriterWriter の内部的な実装詳細であることを明確にし、外部から直接アクセスされることを防ぎます。

この変更に伴い、writer.go 内の Writer 構造体のメソッド(Close, CreateHeader など)や、NewWriter 関数内で countWriter のインスタンスを初期化する箇所も、w.count から w.cw.count へ、write(w, ...) から write(w.cw, ...) へと変更されています。これは、Writer 構造体から countWriter のフィールドやメソッドにアクセスする際に、明示的に cw フィールドを介する必要があるためです。

また、doc/go1.htmldoc/go1.tmpl にも、この変更に関するドキュメントが追加されています。Go 1のリリースノートの一部として、*zip.Writer から Write メソッドが削除されたこと、そしてそれが誤って公開されていた実装詳細であったことが明記されています。これにより、Go 1への移行を検討している開発者に対して、このAPI変更の意図と、既存コードの修正が必要になる可能性があることが伝えられています。

この変更は、Go言語のAPI設計における「明示性」と「意図の明確化」の原則を反映しています。埋め込みは便利な機能ですが、意図しないAPIの公開につながる可能性があるため、慎重に使用すべきであるという教訓を示しています。

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

このコミットにおける主要なコード変更は、src/pkg/archive/zip/writer.go に集中しています。

src/pkg/archive/zip/writer.go

  1. Writer 構造体の定義変更:

    --- a/src/pkg/archive/zip/writer.go
    +++ b/src/pkg/archive/zip/writer.go
    @@ -19,7 +19,7 @@ import (
    
     // Writer implements a zip file writer.
     type Writer struct {
    -	countWriter
    +	cw     *countWriter
     	dir    []*header
     	last   *fileWriter
     	closed bool
    

    countWriter を直接埋め込む代わりに、*countWriter 型のフィールド cw を導入。

  2. NewWriter 関数の初期化変更:

    --- a/src/pkg/archive/zip/writer.go
    +++ b/src/pkg/archive/zip/writer.go
    @@ -29,7 +29,7 @@ type header struct {
    
     // NewWriter returns a new Writer writing a zip file to w.
     func NewWriter(w io.Writer) *Writer {
    -	return &Writer{countWriter: countWriter{w: bufio.NewWriter(w)}}
    +	return &Writer{cw: &countWriter{w: bufio.NewWriter(w)}}
     }
    

    Writer の初期化時に、cw フィールドに *countWriter のインスタンスを割り当てるように変更。

  3. Writer メソッド内の countWriter へのアクセス変更: Close メソッド内で w.countwrite(w, ...) となっていた箇所が、w.cw.countwrite(w.cw, ...) に変更されています。これは、countWriter が直接埋め込まれたフィールドではなくなったため、明示的に cw フィールドを介してアクセスする必要があるためです。

    --- a/src/pkg/archive/zip/writer.go
    +++ b/src/pkg/archive/zip/writer.go
    @@ -52,42 +52,42 @@ func (w *Writer) Close() (err error) {
    
     	// write central directory
    -	start := w.count
    +	start := w.cw.count
     	for _, h := range w.dir {
    -		write(w, uint32(directoryHeaderSignature))
    -		write(w, h.CreatorVersion)
    -		write(w, h.ReaderVersion)
    -		write(w, h.Flags)
    -		write(w, h.Method)
    -		write(w, h.ModifiedTime)
    -		write(w, h.ModifiedDate)
    -		write(w, h.CRC32)
    -		write(w, h.CompressedSize)
    -		write(w, h.UncompressedSize)
    -		write(w, uint16(len(h.Name)))
    -		write(w, uint16(len(h.Extra)))
    -		write(w, uint16(len(h.Comment)))
    -		write(w, uint16(0)) // disk number start
    -		write(w, uint16(0)) // internal file attributes
    -		write(w, h.ExternalAttrs)
    -		write(w, h.offset)
    -		writeBytes(w, []byte(h.Name))
    -		writeBytes(w, h.Extra)
    -		writeBytes(w, []byte(h.Comment))
    +		write(w.cw, uint32(directoryHeaderSignature))
    +		write(w.cw, h.CreatorVersion)
    +		write(w.cw, h.ReaderVersion)
    +		write(w.cw, h.Flags)
    +		write(w.cw, h.Method)
    +		write(w.cw, h.ModifiedTime)
    +		write(w.cw, h.ModifiedDate)
    +		write(w.cw, h.CRC32)
    +		write(w.cw, h.CompressedSize)
    +		write(w.cw, h.UncompressedSize)
    +		write(w.cw, uint16(len(h.Name)))
    +		write(w.cw, uint16(len(h.Extra)))
    +		write(w.cw, uint16(len(h.Comment)))
    +		write(w.cw, uint16(0)) // disk number start
    +		write(w.cw, uint16(0)) // internal file attributes
    +		write(w.cw, h.ExternalAttrs)
    +		write(w.cw, h.offset)
    +		writeBytes(w.cw, []byte(h.Name))
    +		writeBytes(w.cw, h.Extra)
    +		writeBytes(w.cw, []byte(h.Comment))
     	}
    -	end := w.count
    +	end := w.cw.count
    
     	// write end record
    -	write(w, uint32(directoryEndSignature))
    -	write(w, uint16(0))          // disk number
    -	write(w, uint16(0))          // disk number where directory starts
    -	write(w, uint16(len(w.dir))) // number of entries this disk
    -	write(w, uint16(len(w.dir))) // number of entries total
    -	write(w, uint32(end-start))  // size of directory
    -	write(w, uint32(start))      // start of directory
    -	write(w, uint16(0))          // size of comment
    +	write(w.cw, uint32(directoryEndSignature))
    +	write(w.cw, uint16(0))          // disk number
    +	write(w.cw, uint16(0))          // disk number where directory starts
    +	write(w.cw, uint16(len(w.dir))) // number of entries this disk
    +	write(w.cw, uint16(len(w.dir))) // number of entries total
    +	write(w.cw, uint32(end-start))  // size of directory
    +	write(w.cw, uint32(start))      // start of directory
    +	write(w.cw, uint16(0))          // size of comment
    
    -	return w.w.(*bufio.Writer).Flush()
    +	return w.cw.w.(*bufio.Writer).Flush()
     }
    

    同様に、CreateHeader メソッド内でも w.countw.cw.count に、writeHeader(w, fh)writeHeader(w.cw, fh) に変更されています。

    --- a/src/pkg/archive/zip/writer.go
    +++ b/src/pkg/archive/zip/writer.go
    @@ -119,8 +119,8 @@ func (w *Writer) CreateHeader(fh *FileHeader) (io.Writer, error) {
    
     	fw := &fileWriter{
    -		zipw:      w,
    -		compCount: &countWriter{w: w},
    +		zipw:      w.cw,
    +		compCount: &countWriter{w: w.cw},
     		crc32:     crc32.NewIEEE(),
     	}
     	switch fh.Method {
    @@ -139,12 +139,12 @@ func (w *Writer) CreateHeader(fh *FileHeader) (io.Writer, error) {
    
     	h := &header{
     		FileHeader: fh,
    -		offset:     uint32(w.count),
    +		offset:     uint32(w.cw.count),
     	}
     	w.dir = append(w.dir, h)
     	fw.header = h
    
    -	if err := writeHeader(w, fh); err != nil {
    +	if err := writeHeader(w.cw, fh); err != nil {
     		return nil, err
     	}
    

doc/go1.html および doc/go1.tmpl

Go 1のリリースノートに、この変更に関する説明が追加されています。

--- a/doc/go1.html
+++ b/doc/go1.html
@@ -855,6 +855,18 @@ few programs beyond the need to run <code>go fix</code>.
 This category includes packages that are new in Go 1.
 </p>

+<h3 id="archive_zip">The archive/zip package</h3>
+
+<p>
+In Go 1, <a href="/pkg/archive/zip/#Writer"><code>*zip.Writer</code></a> no
+longer has a <code>Write</code> method. Its presence was a mistake.
+</p>
+
+<p>
+<i>Updating:</i> What little code is affected will be caught by the compiler
+and must be updated by hand. Such code is almost certainly incorrect.
+</p>
+
 <h3 id="crypto_aes_des">The crypto/aes and crypto/des packages</h3>

 <p>

同様の変更が doc/go1.tmpl にも適用されています。これは、Go 1の公式ドキュメントの一部として、このAPI変更がユーザーに通知されることを保証します。

コアとなるコードの解説

このコミットのコアとなるコード変更は、zip.Writer 構造体における countWriter の扱い方の変更です。

変更前:

type Writer struct {
    countWriter // countWriterを直接埋め込み
    // ...
}

この形式では、WritercountWriter のすべてのフィールドとメソッドを「継承」します。特に、countWriterio.Writer インターフェースを実装しているため、その Write メソッドが *Writer の公開メソッドとして利用可能になっていました。これは、Goの埋め込み機能の意図された挙動ですが、このケースでは *Writer が直接書き込みを行うべきではないという設計意図に反していました。

変更後:

type Writer struct {
    cw     *countWriter // countWriterへのポインタをフィールドとして保持
    // ...
}

この変更により、countWriterWriter の内部的なフィールド cw として扱われます。cw*countWriter 型のポインタです。この場合、countWriter のメソッドは Writer 型に自動的に昇格しません。したがって、*Writer のインスタンスから直接 Write メソッドを呼び出すことはできなくなり、コンパイルエラーが発生するようになります。

Writer の内部で countWriter の機能(例えば、書き込まれたバイト数をカウントする count フィールドや、実際の書き込みを行う Write メソッド)にアクセスする必要がある場合は、明示的に w.cw.countw.cw.Write(...) のように cw フィールドを介してアクセスする必要があります。

例えば、Close メソッド内の以下の変更を見てみましょう。

変更前:

start := w.count // w.count は埋め込まれた countWriter の count フィールドに直接アクセス
// ...
write(w, uint32(directoryHeaderSignature)) // write 関数は io.Writer を引数にとるため、w (Writer) が io.Writer として渡される
// ...
return w.w.(*bufio.Writer).Flush() // w.w は埋め込まれた countWriter の w フィールドに直接アクセス

変更後:

start := w.cw.count // w.cw.count と明示的にアクセス
// ...
write(w.cw, uint32(directoryHeaderSignature)) // write 関数に w.cw (countWriter) を明示的に渡す
// ...
return w.cw.w.(*bufio.Writer).Flush() // w.cw.w と明示的にアクセス

この変更は、countWriterWriter の内部的なヘルパー構造体であり、その機能は Writer の公開APIの一部ではないことを明確に示しています。これにより、zip.Writer のAPIはよりクリーンになり、ユーザーは Create メソッドが返す io.Writer を通じてのみファイルコンテンツを書き込むべきであるという設計意図が強制されます。

Go 1のドキュメントにこの変更が明記されていることも重要です。これにより、既存のコードがこの変更によって影響を受ける可能性があることを開発者に警告し、必要に応じて手動で修正するよう促しています。これは、Go言語が後方互換性を重視しつつも、APIの健全性と正確性を向上させるために必要な変更を行う姿勢を示しています。

関連リンク

参考にした情報源リンク

  • Go 1 Release Notes: The archive/zip package (コミットで追加されたドキュメントの内容)
  • Go言語の構造体の埋め込みに関する公式ドキュメントやチュートリアル
  • io.Writer インターフェースに関するGo言語のドキュメント
  • ZIPファイルフォーマットの仕様に関する一般的な情報源 (例: Wikipedia, PKWAREのAppNote.txt)
  • Go言語のAPI設計原則に関する議論や記事