[インデックス 14293] ファイルの概要
このコミットは、Go言語のencoding/binary
パッケージにおいて、構造体のエンコードおよびデコード時に、ブランク識別子_
で命名されたフィールド(いわゆる「ブランクフィールド」)をスキップする機能を追加するものです。これにより、固定サイズのバイナリフォーマットや、C言語の構造体との相互運用性において、パディング目的で挿入されたフィールドを適切に処理できるようになります。
コミット
commit 27c990e7946d69fecc0c823e54f7d7da631ed1a5
Author: Robert Griesemer <gri@golang.org>
Date: Thu Nov 1 12:39:20 2012 -0700
encoding/binary: skip blank fields when (en/de)coding structs
- minor unrelated cleanups
- performance impact in the noise
benchmark old ns/op new ns/op delta
BenchmarkReadSlice1000Int32s 83462 83346 -0.14%
BenchmarkReadStruct 4141 4247 +2.56%
BenchmarkReadInts 1588 1586 -0.13%
BenchmarkWriteInts 1550 1489 -3.94%
BenchmarkPutUvarint32 39 39 +1.02%
BenchmarkPutUvarint64 142 144 +1.41%
benchmark old MB/s new MB/s speedup
BenchmarkReadSlice1000Int32s 47.93 47.99 1.00x
BenchmarkReadStruct 16.90 16.48 0.98x
BenchmarkReadInts 18.89 18.91 1.00x
BenchmarkWriteInts 19.35 20.15 1.04x
BenchmarkPutUvarint32 101.90 100.82 0.99x
BenchmarkPutUvarint64 56.11 55.45 0.99x
Fixes #4185.
R=r, rsc
CC=golang-dev
https://golang.org/cl/6750053
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/27c990e7946d69fecc0c823e54f7d7da631ed1a5
元コミット内容
encoding/binary: skip blank fields when (en/de)coding structs
- minor unrelated cleanups
- performance impact in the noise
benchmark old ns/op new ns/op delta
BenchmarkReadSlice1000Int32s 83462 83346 -0.14%
BenchmarkReadStruct 4141 4247 +2.56%
BenchmarkReadInts 1588 1586 -0.13%
BenchmarkWriteInts 1550 1489 -3.94%
BenchmarkPutUvarint32 39 39 +1.02%
BenchmarkPutUvarint64 142 144 +1.41%
benchmark old MB/s new MB/s speedup
BenchmarkReadSlice1000Int32s 47.93 47.99 1.00x
BenchmarkReadStruct 16.90 16.48 0.98x
BenchmarkReadInts 18.89 18.91 1.00x
BenchmarkWriteInts 19.35 20.15 1.04x
BenchmarkPutUvarint32 101.90 100.82 0.99x
BenchmarkPutUvarint64 56.11 55.45 0.99x
Fixes #4185.
R=r, rsc
CC=golang-dev
https://golang.org/cl/6750053
変更の背景
この変更は、Goのencoding/binary
パッケージが、構造体内の特定のフィールドを適切に処理できないという問題(Issue #4185)を解決するために行われました。具体的には、Goの構造体において、ブランク識別子_
(アンダースコア)で命名されたフィールドや、エクスポートされていない(小文字で始まる)フィールドが存在する場合、encoding/binary
パッケージがそれらをスキップせず、エンコード/デコード処理が失敗するか、意図しない動作を引き起こす可能性がありました。
このようなブランクフィールドは、特に以下のようなシナリオで重要になります。
- パディング: C言語などで定義された固定サイズのバイナリ構造体とGoの構造体を相互運用する場合、アライメントのために明示的なパディングバイトが必要になることがあります。Goでは、このようなパディングを
_
フィールドとして表現することが一般的です。 - 予約済みフィールド: 将来の拡張のために予約された領域や、特定のプロトコルで定義された未使用のバイト列を表現する場合にも、
_
フィールドが利用されます。
このコミット以前は、encoding/binary
はこれらのフィールドを通常のフィールドと同様に扱おうとし、結果としてエラーになったり、バイナリデータの読み書きが期待通りに行われないという問題がありました。この変更により、encoding/binary
はブランクフィールドを「スキップすべき」ものとして認識し、読み込み時にはその分のバイトを読み飛ばし、書き込み時にはゼロ値で埋めることで、これらのユースケースに対応できるようになりました。
前提知識の解説
encoding/binary
パッケージ
encoding/binary
パッケージは、Goの基本的なデータ型(整数、浮動小数点数、ブール値など)や、それらを含む構造体を、バイト列との間で変換(エンコード/デコード)するための機能を提供します。ネットワークプロトコル、ファイルフォーマット、または異なるシステム間でのデータ交換など、バイトオーダー(エンディアン)が重要なバイナリデータの扱いに特化しています。
binary.Read(r io.Reader, order ByteOrder, data interface{}) error
:io.Reader
からバイトを読み込み、指定されたバイトオーダーに従ってdata
(ポインタである必要がある)にデコードします。binary.Write(w io.Writer, order ByteOrder, data interface{}) error
:data
の値を指定されたバイトオーダーに従ってバイト列にエンコードし、io.Writer
に書き込みます。
Goのreflect
パッケージ
encoding/binary
パッケージは、Goのreflect
パッケージを内部で利用して、interface{}
型のdata
引数に渡された値の型情報を実行時に動的に検査し、その構造体のフィールドを列挙して処理します。reflect
パッケージは、Goの型システムをプログラム的に操作するための強力なツールであり、以下のような機能を提供します。
reflect.ValueOf(i interface{}) reflect.Value
: インターフェース値のreflect.Value
表現を返します。reflect.Value.Kind()
: 値の具体的な種類(reflect.Struct
,reflect.Int
,reflect.Slice
など)を返します。reflect.Value.NumField()
: 構造体のフィールド数を返します。reflect.Value.Field(i int) reflect.Value
: 構造体のi
番目のフィールドのreflect.Value
を返します。reflect.Value.CanSet()
: そのreflect.Value
が変更可能(セット可能)であるかどうかを返します。これは、エクスポートされたフィールド(大文字で始まるフィールド)や、ポインタを介してアクセスされる値に対してtrue
を返します。
Goにおけるブランク識別子_
(アンダースコア)
Go言語では、ブランク識別子_
は、変数を宣言したがその値を使用しない場合や、関数の戻り値の一部を無視する場合など、意図的に値を破棄することを示すために使用されます。構造体のフィールド名として_
を使用することも可能で、これはそのフィールドがGoのプログラムロジックからは直接アクセスされないことを示します。
例えば、以下のような構造体は、_
フィールドをパディングとして使用する典型的な例です。
type Header struct {
Magic uint32
Version uint16
_ uint16 // Padding to align next field on a 4-byte boundary
Length uint32
}
この_ uint16
フィールドは、Magic
とVersion
の後に2バイトのパディングを挿入し、Length
フィールドが4バイト境界に配置されるようにするために使われます。
技術的詳細
このコミットの主要な技術的変更点は、encoding/binary
パッケージがreflect
を使用して構造体を処理する際に、ブランクフィールドを特別扱いするロジックを追加したことです。
-
Read
およびWrite
関数のコメント更新:Read
関数とWrite
関数のドキュメントに、ブランクフィールドの扱いに関する説明が追加されました。Read
の場合:「構造体への読み込み時、ブランク(_
)フィールド名のフィールドデータはスキップされます。つまり、ブランクフィールド名はパディングに使用できます。」Write
の場合:「構造体の書き込み時、ブランク(_
)フィールド名のフィールドにはゼロ値が書き込まれます。」
-
decoder
とencoder
のリファクタリング: 以前はdecoder
とencoder
がそれぞれ独立した構造体でしたが、共通の基盤となるcoder
構造体が導入され、type decoder coder
およびtype encoder coder
として定義されるようになりました。これはコードの重複を減らし、保守性を向上させるためのマイナーなクリーンアップです。 -
ブランクフィールドの検出とスキップロジック:
decoder.value
およびencoder.value
メソッド内で、構造体のフィールドをイテレートする際に、各フィールドがブランクフィールドであるかどうかを判定するロジックが追加されました。// binary.go (decoder.value, encoder.value 共通のロジック) if v := v.Field(i); v.CanSet() || t.Field(i).Name != "_" { // 通常のフィールドとして処理 d.value(v) // または e.value(v) } else { // ブランクフィールドとしてスキップ d.skip(v) // または e.skip(v) }
v.CanSet()
: この条件は、フィールドがエクスポートされている(大文字で始まる)か、またはポインタを介してセット可能である場合にtrue
を返します。encoding/binary
は通常、セット可能なフィールドのみを処理します。t.Field(i).Name != "_"
: この条件は、フィールド名がブランク識別子_
ではない場合にtrue
を返します。- この
||
(OR)条件により、フィールドがCanSet()
であるか、またはフィールド名が_
ではない場合にのみ、通常のエンコード/デコード処理(d.value(v)
またはe.value(v)
)が実行されます。 - それ以外の場合(つまり、
CanSet()
がfalse
で、かつフィールド名が_
である場合)、新しく追加されたskip
メソッドが呼び出されます。この最適化は、StructField
情報を毎回生成するコストを避けるために行われています。
-
skip
メソッドの実装:decoder.skip(v reflect.Value)
: デコーダの場合、スキップするフィールドのサイズ分だけ内部バッファ(d.buf
)のポインタを進めます。これにより、そのフィールドのバイトデータは読み飛ばされます。func (d *decoder) skip(v reflect.Value) { d.buf = d.buf[dataSize(v):] }
encoder.skip(v reflect.Value)
: エンコーダの場合、スキップするフィールドのサイズ分だけ内部バッファ(e.buf
)をゼロで埋め、その後バッファのポインタを進めます。これにより、ブランクフィールドに対応するバイト列がゼロで埋められます。func (e *encoder) skip(v reflect.Value) { n := dataSize(v) for i := range e.buf[0:n] { e.buf[i] = 0 } e.buf = e.buf[n:] }
-
テストケースの追加:
binary_test.go
にTestBlankFields
という新しいテスト関数が追加されました。このテストは、BlankFields
という構造体(ブランクフィールドを含む)を定義し、それが正しくエンコード/デコードされることを検証します。特に、書き込み時にブランクフィールドがゼロで埋められ、読み込み時にそれらがスキップされることを確認しています。
ベンチマーク結果は、この変更がパフォーマンスに大きな影響を与えないことを示しており、ほとんどのケースで「ノイズの範囲内」とされています。
コアとなるコードの変更箇所
src/pkg/encoding/binary/binary.go
--- a/src/pkg/encoding/binary/binary.go
+++ b/src/pkg/encoding/binary/binary.go
@@ -125,6 +125,9 @@ func (bigEndian) GoString() string { return "binary.BigEndian" }
// of fixed-size values.
// Bytes read from r are decoded using the specified byte order
// and written to successive fields of the data.
+// When reading into structs, the field data for fields with
+// blank (_) field names is skipped; i.e., blank field names
+// may be used for padding.
func Read(r io.Reader, order ByteOrder, data interface{}) error {
// Fast path for basic types.
if n := intDestSize(data); n != 0 {
@@ -154,7 +157,7 @@ func Read(r io.Reader, order ByteOrder, data interface{}) error {
return nil
}
- // Fallback to reflect-based.
+ // Fallback to reflect-based decoding.
var v reflect.Value
switch d := reflect.ValueOf(data); d.Kind() {
case reflect.Ptr:
@@ -181,6 +184,8 @@ func Read(r io.Reader, order ByteOrder, data interface{}) error {
// values, or a pointer to such data.
// Bytes written to w are encoded using the specified byte order
// and read from successive fields of the data.
+// When writing structs, zero values are are written for fields
+// with blank (_) field names.
func Write(w io.Writer, order ByteOrder, data interface{}) error {
// Fast path for basic types.
var b [8]byte
@@ -239,6 +244,8 @@ func Write(w io.Writer, order ByteOrder, data interface{}) error {
_, err := w.Write(bs)
return err
}\n+\n+\t// Fallback to reflect-based encoding.\n \tv := reflect.Indirect(reflect.ValueOf(data))\n \tsize := dataSize(v)\n \tif size < 0 {\n@@ -300,15 +307,13 @@ func sizeof(t reflect.Type) int {\n return -1\n }\n \n-type decoder struct {\n+type coder struct {\n order ByteOrder\n buf []byte\n }\n \n-type encoder struct {\n-\torder ByteOrder\n-\tbuf []byte\n-}\n+type decoder coder\n+type encoder coder\n \n func (d *decoder) uint8() uint8 {\n \tx := d.buf[0]\
@@ -379,9 +384,19 @@ func (d *decoder) value(v reflect.Value) {\n \t\t}\n \n \tcase reflect.Struct:\n+\t\tt := v.Type()\n \t\tl := v.NumField()\n \t\tfor i := 0; i < l; i++ {\n-\t\t\td.value(v.Field(i))\n+\t\t\t// Note: Calling v.CanSet() below is an optimization.\n+\t\t\t// It would be sufficient to check the field name,\n+\t\t\t// but creating the StructField info for each field is\n+\t\t\t// costly (run \"go test -bench=ReadStruct\" and compare\n+\t\t\t// results when making changes to this code).\n+\t\t\tif v := v.Field(i); v.CanSet() || t.Field(i).Name != \"_\" {\n+\t\t\t\td.value(v)\n+\t\t\t} else {\n+\t\t\t\td.skip(v)\n+\t\t\t}\n \t\t}\n \n \tcase reflect.Slice:\
@@ -435,9 +450,15 @@ func (e *encoder) value(v reflect.Value) {\n \t\t}\n \n \tcase reflect.Struct:\n+\t\tt := v.Type()\n \t\tl := e.NumField()\n \t\tfor i := 0; i < l; i++ {\n-\t\t\te.value(v.Field(i))\n+\t\t\t// see comment for corresponding code in decoder.value()\n+\t\t\tif v := v.Field(i); v.CanSet() || t.Field(i).Name != \"_\" {\n+\t\t\t\te.value(v)\n+\t\t\t} else {\n+\t\t\t\te.skip(v)\n+\t\t\t}\n \t\t}\n \n \tcase reflect.Slice:\
@@ -492,6 +513,18 @@ func (e *encoder) value(v reflect.Value) {\n \t}\n }\n \n+func (d *decoder) skip(v reflect.Value) {\n+\td.buf = d.buf[dataSize(v):]\n+}\n+\n+func (e *encoder) skip(v reflect.Value) {\n+\tn := dataSize(v)\n+\tfor i := range e.buf[0:n] {\n+\t\te.buf[i] = 0\n+\t}\n+\te.buf = e.buf[n:]\n+}\n+\n // intDestSize returns the size of the integer that ptrType points to,\n // or 0 if the type is not supported.\
src/pkg/encoding/binary/binary_test.go
--- a/src/pkg/encoding/binary/binary_test.go
+++ b/src/pkg/encoding/binary/binary_test.go
@@ -120,18 +120,14 @@ func testWrite(t *testing.T, order ByteOrder, b []byte, s1 interface{}) {
checkResult(t, "Write", order, err, buf.Bytes(), b)
}\n \n-func TestBigEndianRead(t *testing.T) { testRead(t, BigEndian, big, s) }\n-\n-func TestLittleEndianRead(t *testing.T) { testRead(t, LittleEndian, little, s) }\n-\n-func TestBigEndianWrite(t *testing.T) { testWrite(t, BigEndian, big, s) }\n-\n-func TestLittleEndianWrite(t *testing.T) { testWrite(t, LittleEndian, little, s) }\n+func TestLittleEndianRead(t *testing.T) { testRead(t, LittleEndian, little, s) }\n+func TestLittleEndianWrite(t *testing.T) { testWrite(t, LittleEndian, little, s) }\n+func TestLittleEndianPtrWrite(t *testing.T) { testWrite(t, LittleEndian, little, &s) }\n \n+func TestBigEndianRead(t *testing.T) { testRead(t, BigEndian, big, s) }\n+func TestBigEndianWrite(t *testing.T) { testWrite(t, BigEndian, big, s) }\n func TestBigEndianPtrWrite(t *testing.T) { testWrite(t, BigEndian, big, &s) }\n \n-func TestLittleEndianPtrWrite(t *testing.T) { testWrite(t, LittleEndian, little, &s) }\n-\n func TestReadSlice(t *testing.T) {\n slice := make([]int32, 2)\n err := Read(bytes.NewBuffer(src), BigEndian, slice)\
@@ -147,20 +143,75 @@ func TestWriteSlice(t *testing.T) {\n func TestWriteT(t *testing.T) {\n buf := new(bytes.Buffer)\n ts := T{}\n-\terr := Write(buf, BigEndian, ts)\n-\tif err == nil {\n-\t\tt.Errorf(\"WriteT: have nil, want non-nil\")\n+\tif err := Write(buf, BigEndian, ts); err == nil {\n+\t\tt.Errorf(\"WriteT: have err == nil, want non-nil\")\n \t}\n \n \ttv := reflect.Indirect(reflect.ValueOf(ts))\n \tfor i, n := 0, tv.NumField(); i < n; i++ {\n-\t\terr = Write(buf, BigEndian, tv.Field(i).Interface())\n-\t\tif err == nil {\n-\t\t\tt.Errorf(\"WriteT.%v: have nil, want non-nil\", tv.Field(i).Type())\n+\t\tif err := Write(buf, BigEndian, tv.Field(i).Interface()); err == nil {\n+\t\t\tt.Errorf(\"WriteT.%v: have err == nil, want non-nil\", tv.Field(i).Type())\n \t\t}\n \t}\n }\n \n+type BlankFields struct {\n+\tA uint32\n+\t_ int32\n+\tB float64\n+\t_ [4]int16\n+\tC byte\n+\t_ [7]byte\n+\t_ struct {\n+\t\tf [8]float32\n+\t}\n+}\n+\n+type BlankFieldsProbe struct {\n+\tA uint32\n+\tP0 int32\n+\tB float64\n+\tP1 [4]int16\n+\tC byte\n+\tP2 [7]byte\n+\tP3 struct {\n+\t\tF [8]float32\n+\t}\n+}\n+\n+func TestBlankFields(t *testing.T) {\n+\tbuf := new(bytes.Buffer)\n+\tb1 := BlankFields{A: 1234567890, B: 2.718281828, C: 42}\n+\tif err := Write(buf, LittleEndian, &b1); err != nil {\n+\t\tt.Error(err)\n+\t}\n+\n+\t// zero values must have been written for blank fields\n+\tvar p BlankFieldsProbe\n+\tif err := Read(buf, LittleEndian, &p); err != nil {\n+\t\tt.Error(err)\n+\t}\n+\n+\t// quick test: only check first value of slices\n+\tif p.P0 != 0 || p.P1[0] != 0 || p.P2[0] != 0 || p.P3.F[0] != 0 {\n+\t\tt.Errorf(\"non-zero values for originally blank fields: %#v\", p)\n+\t}\n+\n+\t// write p and see if we can probe only some fields\n+\tif err := Write(buf, LittleEndian, &p); err != nil {\n+\t\tt.Error(err)\n+\t}\n+\n+\t// read should ignore blank fields in b2\n+\tvar b2 BlankFields\n+\tif err := Read(buf, LittleEndian, &b2); err != nil {\n+\t\tt.Error(err)\n+\t}\n+\tif b1.A != b2.A || b1.B != b2.B || b1.C != b2.C {\n+\t\tt.Errorf(\"%#v != %#v\", b1, b2)\n+\t}\n+}\n+\n type byteSliceReader struct {\n remain []byte\n }\
コアとなるコードの解説
binary.go
の変更点
-
Read
およびWrite
関数のドキュメント更新: これらの関数のコメントに、ブランクフィールドの扱いに関する新しい動作が明記されました。これは、ユーザーがencoding/binary
パッケージを使用する際の重要な情報となります。 -
coder
型の導入とdecoder
/encoder
のリファクタリング: 以前はdecoder
とencoder
がそれぞれorder ByteOrder
とbuf []byte
を持つ独立した構造体でしたが、これらを共通のcoder
構造体としてまとめ、type decoder coder
とtype encoder coder
とすることで、コードの簡潔性と再利用性が向上しました。機能的な変更というよりは、内部的な構造の改善です。 -
decoder.value
およびencoder.value
におけるブランクフィールド処理: これがこのコミットの最も重要な変更点です。構造体のフィールドを処理するループ内で、以下の条件が追加されました。if v := v.Field(i); v.CanSet() || t.Field(i).Name != "_" { // ... 通常のエンコード/デコード処理 ... } else { // ... ブランクフィールドのスキップ処理 ... }
v.Field(i)
: 現在処理している構造体フィールドのreflect.Value
を取得します。v.CanSet()
: このフィールドがGoのプログラムから値を設定できる(つまり、エクスポートされている)かどうかをチェックします。t.Field(i).Name != "_"
: このフィールドの名前がブランク識別子_
ではないかをチェックします。- この条件式は、「もしフィールドが設定可能であるか、またはフィールド名が
_
ではないならば」という論理を表します。- フィールドが設定可能(エクスポートされている)であれば、それは通常のフィールドとして扱われます。
- フィールドが設定不可能(エクスポートされていない)であっても、その名前が
_
でなければ、それは通常のフィールドとして扱われます(ただし、CanSet()
がfalse
のため、encoding/binary
は通常そのフィールドをスキップします。このコミットは_
フィールドの明示的なスキップを追加しています)。 - 重要なのは、
CanSet()
がfalse
であり、かつフィールド名が_
である場合です。 この場合にのみ、else
ブロックが実行され、skip
メソッドが呼び出されます。これにより、encoding/binary
は明示的にブランクフィールドを無視するようになります。
-
skip
メソッドの追加:func (d *decoder) skip(v reflect.Value)
:decoder
の場合、dataSize(v)
(フィールドのバイトサイズ)分だけ内部バッファd.buf
の先頭を切り詰めます。これにより、対応するバイトデータが読み飛ばされ、次のフィールドの処理に進みます。func (e *encoder) skip(v reflect.Value)
:encoder
の場合、dataSize(v)
分だけ内部バッファe.buf
の該当部分をゼロで埋めます。その後、バッファの先頭を切り詰めます。これにより、ブランクフィールドに対応するバイト列がゼロで埋められ、バイナリ出力の整合性が保たれます。
binary_test.go
の変更点
-
既存テストの並び替え: 既存のテスト関数(
TestBigEndianRead
,TestLittleEndianRead
など)の宣言順序が変更されました。これは機能的な変更ではありません。 -
TestWriteT
の修正: エラーチェックのロジックがよりGoらしい書き方(if err := ...; err == nil
)に修正されました。 -
BlankFields
およびBlankFieldsProbe
構造体の追加:BlankFields
:uint32
,float64
,byte
といった通常のフィールドと、int32
,[4]int16
,[7]byte
, 匿名構造体(その中に[8]float32
)といった様々な型のブランクフィールド_
を含む構造体です。BlankFieldsProbe
:BlankFields
と全く同じメモリレイアウトを持つが、すべてのフィールドに名前が付いている構造体です。これは、BlankFields
をエンコード/デコードした際に、ブランクフィールドに対応するメモリ領域に何が書き込まれたか(特にゼロ値が書き込まれたか)を検証するために使用されます。
-
TestBlankFields
関数の追加: この新しいテストは、ブランクフィールドのエンコード/デコードが正しく機能することを包括的に検証します。BlankFields
のインスタンスb1
を作成し、Write
でバイト列にエンコードします。- そのバイト列を
BlankFieldsProbe
のインスタンスp
にRead
でデコードします。ここで、p
のブランクフィールドに対応する部分(P0
,P1
,P2
,P3.F
)がすべてゼロ値であることをアサートします。これは、Write
がブランクフィールドをゼロで埋めたことを確認します。 - 次に、
p
をWrite
でエンコードします。 - そのバイト列を再び
BlankFields
のインスタンスb2
にRead
でデコードします。ここで、b2
の通常のフィールド(A
,B
,C
)が元のb1
の値と一致することをアサートします。これは、Read
がブランクフィールドを正しくスキップし、通常のフィールドを正しくデコードしたことを確認します。
これらの変更により、encoding/binary
パッケージは、Goの構造体におけるブランクフィールドを、パディングや予約済み領域として適切に扱えるようになり、より柔軟なバイナリデータ処理が可能になりました。
関連リンク
- Go Issue #4185: encoding/binary: allow padding fields in structs
- Go Change List (CL) 6750053: encoding/binary: skip blank fields when (en/de)coding structs
参考にした情報源リンク
- Go言語公式ドキュメント:
encoding/binary
パッケージ - Go言語公式ドキュメント:
reflect
パッケージ - Go言語におけるブランク識別子(
_
)に関する一般的な情報源- (例: Go言語のチュートリアルや公式ブログ記事など)