[インデックス 12230] ファイルの概要
このコミットは、Go言語の標準ライブラリ archive/zip
パッケージにおいて、ZIPファイルのヘッダー情報をバイト配列に書き込む際の処理を改善するものです。具体的には、uint16
や uint32
といった固定長の整数値をバイト配列に書き込む際に、手動でオフセットを計算して指定する代わりに、新しいヘルパー型 writeBuf
とそのメソッドを導入することで、オフセット管理を内部に隠蔽し、コードの可読性と保守性を向上させています。
コミット
commit eb825b58ccbda0f748406a6cf9f76833774ab30e
Author: Andrew Gerrand <adg@golang.org>
Date: Mon Feb 27 17:37:59 2012 +1100
archive/zip: use smarter putUintXX functions to hide offsets
R=bradfitz, r, dsymonds, kyle
CC=golang-dev
https://golang.org/cl/5701055
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/eb825b58ccbda0f748406a6cf9f76833774ab30e
元コミット内容
archive/zip: use smarter putUintXX functions to hide offsets
R=bradfitz, r, dsymonds, kyle
CC=golang-dev
https://golang.org/cl/5701055
変更の背景
Go言語の archive/zip
パッケージは、ZIPアーカイブの読み書きを扱うための標準ライブラリです。ZIPファイルフォーマットは、ヘッダーやデータが特定のバイトオフセットに固定長の整数値として格納されるバイナリ形式です。
このコミット以前は、archive/zip/writer.go
内で、putUint16
や putUint32
といったヘルパー関数を使用して、[]byte
スライス(バイト配列)の特定の位置に整数値を書き込んでいました。この際、各値の書き込み位置(オフセット)を手動で計算し、b[0:]
, b[4:]
, b[6:]
のように明示的に指定する必要がありました。
このような手動でのオフセット管理は、以下のような問題を引き起こす可能性があります。
- エラーの発生しやすさ: オフセットの計算ミスや、フィールドの追加・削除によるオフセットのずれが発生しやすく、バグの原因となりやすい。
- 可読性の低下: コードを読んだ際に、各値がどのオフセットに書き込まれているのかを理解するために、ZIPフォーマットの仕様や前の書き込み処理を追う必要があり、コードの意図が直感的に分かりにくい。
- 保守性の低下: ZIPフォーマットの変更や、フィールドの順序変更があった場合、関連するすべてのオフセット指定を修正する必要があり、変更作業が煩雑になる。
このコミットは、これらの問題を解決し、より安全で、可読性が高く、保守しやすいコードを実現するために行われました。
前提知識の解説
ZIPファイルフォーマットの基本構造
ZIPファイルは、複数のファイルやディレクトリを単一のアーカイブにまとめるための一般的なフォーマットです。その構造は、主に以下の要素で構成されます。
- ローカルファイルヘッダー (Local File Header): 各ファイルデータの直前に配置され、ファイル名、圧縮・非圧縮サイズ、CRC-32チェックサムなどの情報を含みます。
- ファイルデータ (File Data): 実際のファイルの内容(圧縮されている場合もある)です。
- データ記述子 (Data Descriptor): ローカルファイルヘッダーに圧縮・非圧縮サイズやCRC-32情報が含まれていない場合(ストリーミング書き込みなど)に、ファイルデータの後に配置されます。
- セントラルディレクトリヘッダー (Central Directory Header): ZIPファイル内のすべてのファイルに関するメタデータ(ファイル名、圧縮方法、ファイルサイズ、ローカルファイルヘッダーへのオフセットなど)を一元的に管理します。ZIPファイルの末尾近くにまとめて配置されます。
- セントラルディレクトリ終了レコード (End of Central Directory Record): セントラルディレクトリの開始位置、サイズ、エントリ数などの情報を含み、ZIPファイルの終端を示します。
これらのヘッダーやレコード内では、様々な情報が固定長の整数値として格納されます。例えば、サイズやオフセットは uint32
(4バイト) で、バージョンやフラグは uint16
(2バイト) で表現されることが多いです。ZIPフォーマットでは、これらの多バイト整数はリトルエンディアン形式で格納されます。これは、最下位バイトが最初に、最上位バイトが最後に格納される方式です。
Go言語の []byte
スライスとポインタ
Go言語の []byte
はバイトのスライス(動的配列)を表します。これは、バイナリデータを扱う上で非常に重要な型です。スライスは、基となる配列の一部を参照するビューのようなもので、len
(長さ)と cap
(容量)を持ちます。
このコミットで導入される writeBuf
型は []byte
のエイリアスであり、そのメソッドはレシーバとして *writeBuf
(スライスへのポインタ) を受け取ります。これにより、メソッド内でスライスの基となるポインタを更新し、スライスが参照する範囲を「進める」ことができます。例えば、*b = (*b)[2:]
は、スライス b
が参照する範囲を先頭から2バイト分進める操作です。これにより、次に書き込むべき位置が自動的に更新されます。
Go言語におけるリフレクションとパフォーマンス
Go言語の encoding/binary
パッケージには、binary.Write
関数があり、構造体やプリミティブ型をバイト配列に書き込むことができます。しかし、この関数は内部でリフレクション(実行時に型情報を検査・操作する機能)を使用します。リフレクションは非常に強力な機能ですが、一般的にパフォーマンスオーバーヘッドが大きいため、特にパフォーマンスが重視される場面(例えば、大量のバイナリデータを処理するI/O操作など)では、手動でのバイト操作や、このコミットで導入されるようなカスタムのヘルパー関数が好まれることがあります。
コミットメッセージのコメント // We use this helper instead of encoding/binary's Write to avoid reflection. // It's easy enough, anyway.
は、このパフォーマンス上の考慮事項を明確に示しています。
技術的詳細
このコミットの主要な変更点は、src/pkg/archive/zip/writer.go
に新しい型 writeBuf
とそのメソッド uint16
および uint32
を導入したことです。
writeBuf
型の定義
type writeBuf []byte
writeBuf
は []byte
のエイリアスとして定義されています。これにより、[]byte
のすべての特性を継承しつつ、カスタムメソッドを追加することができます。
uint16
メソッド
func (b *writeBuf) uint16(v uint16) {
(*b)[0] = byte(v)
(*b)[1] = byte(v >> 8)
*b = (*b)[2:]
}
このメソッドは、uint16
型の v
を現在の writeBuf
の先頭2バイトにリトルエンディアン形式で書き込みます。
(*b)[0] = byte(v)
:v
の最下位バイトを現在のスライスの先頭に書き込みます。(*b)[1] = byte(v >> 8)
:v
を8ビット右シフト(上位バイトを取り出す)し、そのバイトを現在のスライスの2番目のバイトに書き込みます。*b = (*b)[2:]
: スライスb
が参照する範囲を先頭から2バイト分進めます。これにより、次の書き込みは自動的に新しい開始位置から行われます。
uint32
メソッド
func (b *writeBuf) uint32(v uint32) {
(*b)[0] = byte(v)
(*b)[1] = byte(v >> 8)
(*b)[2] = byte(v >> 16)
(*b)[3] = byte(v >> 24)
*b = (*b)[4:]
}
このメソッドは、uint32
型の v
を現在の writeBuf
の先頭4バイトにリトルエンディアン形式で書き込みます。
(*b)[0]
から(*b)[3]
までに、v
の各バイトをリトルエンディアン順に書き込みます。*b = (*b)[4:]
: スライスb
が参照する範囲を先頭から4バイト分進めます。
変更の適用
これらの新しいメソッドは、Writer.Close()
関数内のセントラルディレクトリヘッダー、セントラルディレクトリ終了レコード、および writeHeader
関数内のローカルファイルヘッダーの書き込み処理に適用されています。
変更前:
var b [directoryHeaderLen]byte
putUint32(b[:], uint32(directoryHeaderSignature))
putUint16(b[4:], h.CreatorVersion)
// ...
if _, err := w.cw.Write(b[:]); err != nil {
ここでは、b
という固定サイズのバイト配列を宣言し、putUintXX
関数に b
のスライスを渡す際に、b[4:]
のように手動でオフセットを指定していました。
変更後:
var buf [directoryHeaderLen]byte
b := writeBuf(buf[:])
b.uint32(uint32(directoryHeaderSignature))
b.uint16(h.CreatorVersion)
// ...
b = b[4:] // skip disk number start and internal file attr (2x uint16)
// ...
if _, err := w.cw.Write(buf[:]); err != nil {
変更後では、buf
という固定サイズのバイト配列を宣言し、そのスライスを writeBuf
型にキャストして b
に代入します。その後、b.uint32()
や b.uint16()
のようにメソッドを呼び出すことで、オフセットの管理が writeBuf
の内部で行われるようになります。
b = b[4:]
のような手動でのスライス操作が残っている箇所もありますが、これは特定のフィールド(例: ディスク番号や内部ファイル属性)をスキップするために意図的に行われているもので、個々の値の書き込みにおけるオフセット管理は隠蔽されています。最終的に、buf
全体が w.cw.Write(buf[:])
によって書き込まれます。
reader_test.go
の変更は、テストコード内の t.Errorf
を t.Fatalf
に変更する軽微なものです。これは、ファイルカウントが期待値と異なる場合にテストを即座に終了させることで、後続のエラーメッセージが冗長になるのを防ぐための改善です。
コアとなるコードの変更箇所
src/pkg/archive/zip/writer.go
新しい型とメソッドの追加
// We use this helper instead of encoding/binary's Write to avoid reflection.
// It's easy enough, anyway.
type writeBuf []byte
func (b *writeBuf) uint16(v uint16) {
(*b)[0] = byte(v)
(*b)[1] = byte(v >> 8)
*b = (*b)[2:]
}
func (b *writeBuf) uint32(v uint32) {
(*b)[0] = byte(v)
(*b)[1] = byte(v >> 8)
(*b)[2] = byte(v >> 16)
(*b)[3] = byte(v >> 24)
*b = (*b)[4:]
}
Writer.Close()
メソッド内の変更 (セントラルディレクトリヘッダーの書き込み部分)
--- a/src/pkg/archive/zip/writer.go
+++ b/src/pkg/archive/zip/writer.go
@@ -51,24 +51,25 @@ func (w *Writer) Close() error {
// write central directory
start := w.cw.count
for _, h := range w.dir {
- var b [directoryHeaderLen]byte
- putUint32(b[:], uint32(directoryHeaderSignature))
- putUint16(b[4:], h.CreatorVersion)
- putUint16(b[6:], h.ReaderVersion)
- putUint16(b[8:], h.Flags)
- putUint16(b[10:], h.Method)
- putUint16(b[12:], h.ModifiedTime)
- putUint16(b[14:], h.ModifiedDate)
- putUint32(b[16:], h.CRC32)
- putUint32(b[20:], h.CompressedSize)
- putUint32(b[24:], h.UncompressedSize)
- putUint16(b[28:], uint16(len(h.Name)))
- putUint16(b[30:], uint16(len(h.Extra)))
- putUint16(b[32:], uint16(len(h.Comment)))
- // skip two uint16's, disk number start and internal file attributes
- putUint32(b[38:], h.ExternalAttrs)
- putUint32(b[42:], h.offset)
- if _, err := w.cw.Write(b[:]); err != nil {
+ var buf [directoryHeaderLen]byte
+ b := writeBuf(buf[:])
+ b.uint32(uint32(directoryHeaderSignature))
+ b.uint16(h.CreatorVersion)
+ b.uint16(h.ReaderVersion)
+ b.uint16(h.Flags)
+ b.uint16(h.Method)
+ b.uint16(h.ModifiedTime)
+ b.uint16(h.ModifiedDate)
+ b.uint32(h.CRC32)
+ b.uint32(h.CompressedSize)
+ b.uint32(h.UncompressedSize)
+ b.uint16(uint16(len(h.Name)))
+ b.uint16(uint16(len(h.Extra)))
+ b.uint16(uint16(len(h.Comment)))
+ b = b[4:] // skip disk number start and internal file attr (2x uint16)
+ b.uint32(h.ExternalAttrs)
+ b.uint32(h.offset)
+ if _, err := w.cw.Write(buf[:]); err != nil {
Writer.Close()
メソッド内の変更 (セントラルディレクトリ終了レコードの書き込み部分)
--- a/src/pkg/archive/zip/writer.go
+++ b/src/pkg/archive/zip/writer.go
@@ -84,16 +85,16 @@ func (w *Writer) Close() error {
end := w.cw.count
// write end record
- var b [directoryEndLen]byte
- putUint32(b[:], uint32(directoryEndSignature))
- putUint16(b[4:], uint16(0)) // disk number
- putUint16(b[6:], uint16(0)) // disk number where directory starts
- putUint16(b[8:], uint16(len(w.dir))) // number of entries this disk
- putUint16(b[10:], uint16(len(w.dir))) // number of entries total
- putUint32(b[12:], uint32(end-start)) // size of directory
- putUint32(b[16:], uint32(start)) // start of directory
+ var buf [directoryEndLen]byte
+ b := writeBuf(buf[:])
+ b.uint32(uint32(directoryEndSignature))
+ b = b[4:] // skip over disk number and first disk number (2x uint16)
+ b.uint16(uint16(len(w.dir))) // number of entries this disk
+ b.uint16(uint16(len(w.dir))) // number of entries total
+ b.uint32(uint32(end - start)) // size of directory
+ b.uint32(uint32(start)) // start of directory
// skipped size of comment (always zero)
- if _, err := w.cw.Write(b[:]); err != nil {
+ if _, err := w.cw.Write(buf[:]); err != nil {
return err
}
writeHeader
関数内の変更 (ローカルファイルヘッダーの書き込み部分)
--- a/src/pkg/archive/zip/writer.go
+++ b/src/pkg/archive/zip/writer.go
@@ -163,19 +164,20 @@ func (w *Writer) CreateHeader(fh *FileHeader) (io.Writer, error) {\n }\n \n func writeHeader(w io.Writer, h *FileHeader) error {\n-\tvar b [fileHeaderLen]byte\n-\tputUint32(b[:], uint32(fileHeaderSignature))\n-\tputUint16(b[4:], h.ReaderVersion)\n-\tputUint16(b[6:], h.Flags)\n-\tputUint16(b[8:], h.Method)\n-\tputUint16(b[10:], h.ModifiedTime)\n-\tputUint16(b[12:], h.ModifiedDate)\n-\tputUint32(b[14:], h.CRC32)\n-\tputUint32(b[18:], h.CompressedSize)\n-\tputUint32(b[22:], h.UncompressedSize)\n-\tputUint16(b[26:], uint16(len(h.Name)))\n-\tputUint16(b[28:], uint16(len(h.Extra)))\n-\tif _, err := w.Write(b[:]); err != nil {\
+\tvar buf [fileHeaderLen]byte
+\tb := writeBuf(buf[:])
+\tb.uint32(uint32(fileHeaderSignature))\n+\tb.uint16(h.ReaderVersion)\n+\tb.uint16(h.Flags)\n+\tb.uint16(h.Method)\n+\tb.uint16(h.ModifiedTime)\n+\tb.uint16(h.ModifiedDate)\n+\tb.uint32(h.CRC32)\n+\tb.uint32(h.CompressedSize)\n+\tb.uint32(h.UncompressedSize)\n+\tb.uint16(uint16(len(h.Name)))\n+\tb.uint16(uint16(len(h.Extra)))\n+\tif _, err := w.Write(buf[:]); err != nil {\
\t\treturn err\n \t}\
\tif _, err := io.WriteString(w, h.Name); err != nil {\
fileWriter.close()
メソッド内の変更 (データ記述子の書き込み部分)
--- a/src/pkg/archive/zip/writer.go
+++ b/src/pkg/archive/zip/writer.go
@@ -219,11 +221,12 @@ func (w *fileWriter) close() error {\
fh.UncompressedSize = uint32(w.rawCount.count)
// write data descriptor
- var b [dataDescriptorLen]byte
- putUint32(b[:], fh.CRC32)
- putUint32(b[4:], fh.CompressedSize)
- putUint32(b[8:], fh.UncompressedSize)
- _, err := w.zipw.Write(b[:])
+ var buf [dataDescriptorLen]byte
+ b := writeBuf(buf[:])
+ b.uint32(fh.CRC32)
+ b.uint32(fh.CompressedSize)
+ b.uint32(fh.UncompressedSize)
+ _, err := w.zipw.Write(buf[:])
return err
}
src/pkg/archive/zip/reader_test.go
テストコードの変更
--- a/src/pkg/archive/zip/reader_test.go
+++ b/src/pkg/archive/zip/reader_test.go
@@ -165,7 +165,7 @@ func readTestZip(t *testing.T, zt ZipTest) {
t.Errorf("%s: comment=%q, want %q", zt.Name, z.Comment, zt.Comment)
}\
if len(z.File) != len(zt.File) {
- t.Errorf("%s: file count=%d, want %d", zt.Name, len(z.File), len(zt.File))
+ t.Fatalf("%s: file count=%d, want %d", zt.Name, len(z.File), len(zt.File))
}
// test read of each file
コアとなるコードの解説
このコミットの核心は、writeBuf
型とその uint16
, uint32
メソッドの導入により、バイナリデータへの書き込みパターンを抽象化した点にあります。
変更前のコードでは、以下のようなパターンが繰り返されていました。
var b [SomeLen]byte
putUint32(b[:], someValue1)
putUint16(b[4:], someValue2) // オフセットを明示的に指定
putUint32(b[6:], someValue3) // オフセットを明示的に指定
// ...
この方式では、putUintXX
関数自体はオフセットを受け取りますが、呼び出し側で常に正しいオフセットを計算して渡す責任がありました。これは、フィールドの追加や削除があった場合に、連鎖的にオフセットの修正が必要となり、バグの温床となりやすかったのです。
変更後のコードでは、以下のようなパターンに変わりました。
var buf [SomeLen]byte
b := writeBuf(buf[:]) // writeBuf 型の変数 b を初期化
b.uint32(someValue1) // b の内部オフセットが自動的に進む
b.uint16(someValue2) // b の内部オフセットが自動的に進む
b.uint32(someValue3) // b の内部オフセットが自動的に進む
// ...
この新しいアプローチでは、writeBuf
のメソッドが呼び出されるたびに、内部でスライスの参照範囲が自動的に進められます。これにより、呼び出し側は次に書き込むべき値だけを渡し、オフセットの管理は writeBuf
型の責任となります。これにより、コードはより「宣言的」になり、何が書き込まれるのかが明確になります。
b = b[4:]
のように手動でスライスを進める箇所が残っているのは、ZIPフォーマットの特定のフィールド(例: ディスク番号や内部ファイル属性)がスキップされるべき場合に対応するためです。これらのフィールドは、現在の実装では常にゼロとして扱われ、明示的に書き込む必要がないため、単にスライスをその分だけ進めることで対応しています。
この変更は、Go言語における「慣用的な」バイナリデータ操作の一例を示しています。encoding/binary
パッケージのような汎用的なツールも存在しますが、特定のフォーマット(この場合はZIP)に特化した、よりパフォーマンスが高く、かつ安全なカスタムヘルパーを実装することが、Goの設計思想に合致する場合があることを示唆しています。リフレクションを避けることで、実行時のオーバーヘッドを削減し、より高速なI/O処理を実現しています。
reader_test.go
の変更は、テストの堅牢性を高めるための小さな改善です。t.Errorf
はエラーを報告しますが、テストの実行は継続します。一方、t.Fatalf
はエラーを報告した上で、現在のテスト関数を即座に終了させます。ファイルカウントが期待値と異なるという致命的なエラーの場合、後続のテストが意味をなさない可能性があるため、t.Fatalf
を使用して早期にテストを終了させることで、デバッグの効率を向上させています。
関連リンク
- Go言語
archive/zip
パッケージのドキュメント: https://pkg.go.dev/archive/zip - ZIPファイルフォーマットの仕様 (PKWARE): https://pkware.cachefly.net/webdocs/casestudies/APPNOTE.TXT
- Go言語
encoding/binary
パッケージのドキュメント: https://pkg.go.dev/encoding/binary
参考にした情報源リンク
- Go CL 5701055: archive/zip: use smarter putUintXX functions to hide offsets: https://golang.org/cl/5701055 (元の変更リスト)
- Go言語の
[]byte
スライスに関する一般的な情報源 - リトルエンディアンとビッグエンディアンに関する一般的な情報源
- Go言語におけるリフレクションとパフォーマンスに関する一般的な情報源