[インデックス 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.Writer
の Create
メソッド(または CreateHeader
メソッド)を呼び出すことで、個々のファイルエントリへの書き込みを行うための io.Writer
インターフェースが返されるべきでした。ユーザーは、この返された io.Writer
を通じてファイルの内容を書き込むことが期待されていました。
*zip.Writer
が直接 Write
メソッドを持つことは、以下のような問題を引き起こす可能性がありました。
- 誤解を招くAPI: ユーザーが
*zip.Writer
に対して直接Write
を呼び出すと、ZIPアーカイブの構造が壊れたり、意図しないデータが書き込まれたりする可能性がありました。これは、Write
メソッドがZIPファイルフォーマットの特定のセクション(例えば、ファイルデータ部分)にのみ適用されるべき内部的な操作であったためです。 - 不適切な使用:
*zip.Writer
に直接書き込むことは、ZIPフォーマットの整合性を保つ上で必要なヘッダー情報や中央ディレクトリの管理をスキップしてしまうことになり、結果として破損したZIPファイルが生成されるリスクがありました。 - 設計意図の曖昧化:
*zip.Writer
がio.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.Writer
が countWriter
を埋め込んでいたため、countWriter
の Write
メソッドが zip.Writer
の Write
メソッドとして公開されていました。
2. io.Writer
インターフェース
Go言語の io
パッケージは、I/O操作のための基本的なインターフェースを提供します。io.Writer
はその中でも最も基本的なインターフェースの一つで、データを書き込むための単一のメソッド Write
を定義しています。
type Writer interface {
Write(p []byte) (n int, err error)
}
このインターフェースは、ファイル、ネットワーク接続、バッファなど、様々な出力先への書き込みを抽象化するために広く使用されます。zip.Writer
の Create
メソッドが 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(...)
を呼び出そうとすると、コンパイルエラーになります。
countWriter
の Write
メソッドにアクセスする必要がある場合は、明示的に w.cw.Write(...)
のようにフィールド cw
を介してアクセスする必要があります。これは、countWriter
が Writer
の内部的な実装詳細であることを明確にし、外部から直接アクセスされることを防ぎます。
この変更に伴い、writer.go
内の Writer
構造体のメソッド(Close
, CreateHeader
など)や、NewWriter
関数内で countWriter
のインスタンスを初期化する箇所も、w.count
から w.cw.count
へ、write(w, ...)
から write(w.cw, ...)
へと変更されています。これは、Writer
構造体から countWriter
のフィールドやメソッドにアクセスする際に、明示的に cw
フィールドを介する必要があるためです。
また、doc/go1.html
と doc/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
-
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
を導入。 -
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
のインスタンスを割り当てるように変更。 -
Writer
メソッド内のcountWriter
へのアクセス変更:Close
メソッド内でw.count
やwrite(w, ...)
となっていた箇所が、w.cw.count
やwrite(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.count
がw.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を直接埋め込み
// ...
}
この形式では、Writer
は countWriter
のすべてのフィールドとメソッドを「継承」します。特に、countWriter
が io.Writer
インターフェースを実装しているため、その Write
メソッドが *Writer
の公開メソッドとして利用可能になっていました。これは、Goの埋め込み機能の意図された挙動ですが、このケースでは *Writer
が直接書き込みを行うべきではないという設計意図に反していました。
変更後:
type Writer struct {
cw *countWriter // countWriterへのポインタをフィールドとして保持
// ...
}
この変更により、countWriter
は Writer
の内部的なフィールド cw
として扱われます。cw
は *countWriter
型のポインタです。この場合、countWriter
のメソッドは Writer
型に自動的に昇格しません。したがって、*Writer
のインスタンスから直接 Write
メソッドを呼び出すことはできなくなり、コンパイルエラーが発生するようになります。
Writer
の内部で countWriter
の機能(例えば、書き込まれたバイト数をカウントする count
フィールドや、実際の書き込みを行う Write
メソッド)にアクセスする必要がある場合は、明示的に w.cw.count
や w.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 と明示的にアクセス
この変更は、countWriter
が Writer
の内部的なヘルパー構造体であり、その機能は Writer
の公開APIの一部ではないことを明確に示しています。これにより、zip.Writer
のAPIはよりクリーンになり、ユーザーは Create
メソッドが返す io.Writer
を通じてのみファイルコンテンツを書き込むべきであるという設計意図が強制されます。
Go 1のドキュメントにこの変更が明記されていることも重要です。これにより、既存のコードがこの変更によって影響を受ける可能性があることを開発者に警告し、必要に応じて手動で修正するよう促しています。これは、Go言語が後方互換性を重視しつつも、APIの健全性と正確性を向上させるために必要な変更を行う姿勢を示しています。
関連リンク
- GoLang Code Review: https://golang.org/cl/5654076
参考にした情報源リンク
- Go 1 Release Notes: The archive/zip package (コミットで追加されたドキュメントの内容)
- Go言語の構造体の埋め込みに関する公式ドキュメントやチュートリアル
io.Writer
インターフェースに関するGo言語のドキュメント- ZIPファイルフォーマットの仕様に関する一般的な情報源 (例: Wikipedia, PKWAREのAppNote.txt)
- Go言語のAPI設計原則に関する議論や記事