[インデックス 11685] ファイルの概要
このコミットは、Go言語の標準ライブラリである encoding/binary パッケージにおける TotalSize 関数の可視性(エクスポート状態)を変更し、内部関数として隠蔽することを目的としています。これにより、encoding/binary パッケージのAPIがよりクリーンになり、reflect パッケージへの不必要な依存が外部に露出することを防ぎます。
コミット
commit 52ebadd3569b31ce423d4868ac9aa54a373aa1ad
Author: Rob Pike <r@golang.org>
Date: Wed Feb 8 14:09:20 2012 +1100
encoding/binary: hide TotalSize
The function has a bizarre signature: it was the only public function there
that exposed the reflect package. Also, its definition is peculiar and hard to
explain. It doesn't merit being exported.
This is an API change but really, it should never have been exported and
it's certain very few programs will depend on it: it's too weird.
Fixes #2846.
R=golang-dev, gri, bradfitz
CC=golang-dev
https://golang.org/cl/5639054
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/52ebadd3569b31ce423d4868ac9aa54a373aa1ad
元コミット内容
encoding/binary: hide TotalSize
TotalSize 関数は奇妙なシグネチャを持っており、reflect パッケージを公開する唯一のパブリック関数でした。また、その定義は独特で説明が困難でした。エクスポートされるべきではありません。
これはAPIの変更ですが、本来エクスポートされるべきではなく、ごく少数のプログラムしかこれに依存していないはずです。あまりにも奇妙な関数でした。
Fixes #2846.
変更の背景
この変更の背景には、Go言語のAPI設計における原則と、encoding/binary パッケージの特定の関数の特性があります。
-
APIのクリーンアップ:
encoding/binaryパッケージは、Goのプリミティブ型とバイトシーケンス間の変換を扱うためのものです。TotalSize関数は、その目的から逸脱し、reflectパッケージを直接外部に露出させていました。これは、パッケージの責務を曖昧にし、APIの整合性を損なうものでした。Goの設計哲学では、APIはシンプルで予測可能であるべきであり、不必要な複雑さや内部実装の詳細を外部に晒すべきではありません。 -
reflectパッケージの露出:TotalSize関数はreflect.Value型を引数にとり、Goの「リフレクション」機能を利用していました。リフレクションは強力な機能ですが、パフォーマンスオーバーヘッドがあり、型安全性を損なう可能性があるため、慎重に扱うべきです。特に、encoding/binaryのような低レベルのバイナリ操作パッケージで、リフレクションが外部APIとして露出しているのは適切ではないと判断されました。 -
関数の「奇妙さ」と説明の困難さ: コミットメッセージにあるように、
TotalSize関数のシグネチャと定義は「奇妙で説明が困難」でした。これは、その機能がパッケージの主要な目的と合致しておらず、一般的なユースケースではほとんど必要とされないことを示唆しています。このような関数がエクスポートされていると、ユーザーが誤解したり、不適切に使用したりするリスクがありました。 -
Go 1 リリースに向けたAPIの安定化: このコミットはGo 1のリリース前に行われています。Go 1は、Go言語の最初の安定版であり、そのAPIは長期にわたって互換性が維持されることが約束されていました。そのため、この時期には、将来的な互換性の問題を引き起こす可能性のある、設計上不適切と判断されたAPI要素を修正する作業が活発に行われていました。
TotalSizeのような「奇妙な」関数を隠蔽することは、Go 1のAPIをより堅牢で保守しやすいものにするための重要なステップでした。 -
Issue #2846 の解決: このコミットは、GoのIssueトラッカーで報告されていた #2846 を修正するものです。このIssueは、
encoding/binaryパッケージのAPIに関する議論であり、TotalSize関数の存在が問題視されていたことを示しています。
これらの背景から、TotalSize 関数を非公開(内部関数)にすることは、encoding/binary パッケージの設計を改善し、Go言語全体のAPI品質を高めるための合理的な判断であったと言えます。
前提知識の解説
このコミットを理解するためには、以下のGo言語の概念とパッケージに関する知識が必要です。
-
encoding/binaryパッケージ:- Goの標準ライブラリの一つで、Goのプリミティブ型(整数、浮動小数点数など)とバイトシーケンスの間でデータを変換するための機能を提供します。
- 主に、ネットワークプロトコルやファイルフォーマットなど、バイナリデータを扱う際に使用されます。
- バイトオーダー(エンディアンネス、
binary.BigEndianやbinary.LittleEndian)を指定して、バイト列の解釈方法を制御できます。 binary.Readやbinary.Writeといった関数が主要なAPIです。これらはio.Readerやio.Writerインターフェースと連携し、ストリームからの読み書きを可能にします。
-
reflectパッケージ (リフレクション):- Goの標準ライブラリの一つで、プログラムの実行時に型情報(
reflect.Type)や値(reflect.Value)を検査・操作する機能を提供します。 - これにより、コンパイル時には型が不明なデータに対しても、動的に操作を行うことができます。
- 例えば、構造体のフィールド名やタグを読み取ったり、メソッドを動的に呼び出したりする際に利用されます。
reflect.TypeOf(i interface{}) Typeは任意のインターフェース値の動的な型を返します。reflect.ValueOf(i interface{}) Valueは任意のインターフェース値の動的な値を返します。- 注意点: リフレクションは非常に強力ですが、以下の理由から乱用すべきではありません。
- パフォーマンスオーバーヘッド: 静的な型付けされたコードに比べて実行時コストが高くなります。
- 型安全性: コンパイル時の型チェックをバイパスするため、実行時エラー(パニック)のリスクが高まります。
- 可読性・保守性: リフレクションを多用したコードは、理解やデバッグが難しくなる傾向があります。
- 通常、リフレクションは、JSONエンコーディング/デコーディング、ORM、テストフレームワークなど、特定の汎用的なライブラリやフレームワークの内部で利用されることがほとんどです。アプリケーションコードで直接使用することは稀です。
- Goの標準ライブラリの一つで、プログラムの実行時に型情報(
-
エクスポートされた識別子と非エクスポート識別子:
- Go言語では、識別子(変数名、関数名、型名など)の最初の文字が大文字である場合、その識別子はパッケージ外からアクセス可能(エクスポートされている)になります。
- 最初の文字が小文字である場合、その識別子はパッケージ内でのみアクセス可能(エクスポートされていない、非公開)になります。
- API設計において、外部に公開すべきでない内部実装の詳細は、小文字で始まる識別子として定義し、隠蔽することが推奨されます。
-
APIの安定性 (Go 1):
- Go 1は、Go言語の最初の安定版リリースであり、そのAPIは将来にわたって互換性が維持されることが強く約束されました。
- Go 1リリース前の期間は、APIの最終調整と安定化が行われる重要な時期でした。この時期に行われた変更は、長期的なGoエコシステムの健全性を確保するためのものでした。
これらの知識を前提とすることで、TotalSize 関数がなぜエクスポートされるべきではなかったのか、そしてその変更がGo言語の設計原則にどのように合致しているのかを深く理解できます。
技術的詳細
このコミットの技術的な核心は、encoding/binary パッケージ内の TotalSize 関数を非公開の dataSize 関数にリネームし、その呼び出し箇所を更新することです。
-
TotalSize関数の役割:- コミット前の
TotalSize関数は、reflect.Value型の引数を受け取り、その値がバイナリ形式で占めるメモリ上のサイズ(バイト数)を計算していました。 - この関数は、
encoding/binaryパッケージのReadおよびWrite関数内で、データのサイズを事前に決定するために使用されていました。 - 例えば、
Read関数は、ストリームから読み込むべきバイト数を事前に知るためにTotalSizeを呼び出し、Write関数は、書き込むべきバイト数を決定するために同様に呼び出していました。
- コミット前の
-
reflectパッケージの利用:TotalSize関数は、引数としてreflect.Valueを受け取っていました。これは、Goのリフレクション機能を利用していることを意味します。- リフレクションを使用することで、
TotalSizeは、コンパイル時に型が不明な任意のGoの値(構造体、配列、スライスなど)の内部構造を動的に検査し、そのサイズを計算することができました。 - しかし、
reflectパッケージを直接外部に露出させることは、encoding/binaryパッケージのAPIを複雑にし、リフレクションのパフォーマンスオーバーヘッドや型安全性の問題が外部ユーザーにも影響を及ぼす可能性がありました。
-
変更の理由:
- APIのクリーンアップ:
TotalSizeは、encoding/binaryパッケージの主要な目的(Goの型とバイトシーケンス間の変換)から逸脱しており、reflectパッケージの内部的な利用を外部に晒していました。これを非公開にすることで、パッケージのAPIがよりシンプルで、その責務に集中したものになります。 - 不必要な露出の排除:
TotalSizeは、encoding/binaryパッケージの内部実装の詳細であり、外部ユーザーが直接呼び出す必要はほとんどありませんでした。このような内部ヘルパー関数をエクスポートすることは、APIの混乱を招きます。 - 「奇妙なシグネチャ」:
reflect.Valueを引数にとるシグネチャは、encoding/binaryパッケージの他の関数(通常はinterface{}や具体的な型を扱う)とは異質でした。これにより、APIの一貫性が損なわれていました。
- APIのクリーンアップ:
-
具体的な変更内容:
src/pkg/encoding/binary/binary.go内で、func TotalSize(v reflect.Value) intをfunc dataSize(v reflect.Value) intにリネームしました。これにより、関数は小文字で始まるため、パッケージ外からはアクセスできなくなります。Read関数とWrite関数内でTotalSize(v)を呼び出していた箇所をdataSize(v)に変更しました。src/pkg/encoding/binary/binary_test.go内のテストコードでも、同様にTotalSizeの呼び出しをdataSizeに変更しました。doc/go1.htmlおよびdoc/go1.tmplのGo 1リリースノートに、binary.TotalSizeがエクスポートされなくなった旨の記述が追加されました。これは、この変更がGo 1のAPI互換性に関する重要な情報であることを示しています。
この変更により、encoding/binary パッケージは、その主要な機能に集中し、内部的なリフレクションの利用を隠蔽することで、より堅牢で使いやすいAPIを提供できるようになりました。
コアとなるコードの変更箇所
このコミットにおけるコアとなるコードの変更は、主に src/pkg/encoding/binary/binary.go ファイルに集中しています。
-
TotalSize関数のリネームとコメントの変更:--- a/src/pkg/encoding/binary/binary.go +++ b/src/pkg/encoding/binary/binary.go @@ -253,7 +253,11 @@ func Write(w io.Writer, order ByteOrder, data interface{}) error { return err } -func TotalSize(v reflect.Value) int { +// dataSize returns the number of bytes the actual data represented by v occupies in memory. +// For compound structures, it sums the sizes of the elements. Thus, for instance, for a slice +// it returns the length of the slice times the element size and does not count the memory +// occupied by the header. +func dataSize(v reflect.Value) int { if v.Kind() == reflect.Slice { elem := sizeof(v.Type().Elem()) if elem < 0 {TotalSize関数がdataSizeにリネームされました。これにより、関数名が小文字で始まるため、パッケージ外からはアクセスできない非公開関数となります。- 関数のコメントが更新され、その内部的な役割と、
reflect.Valueが表すデータのメモリ上のサイズを計算する目的が明確に記述されました。特に、スライスの場合にはヘッダのメモリは含まれないことが明記されています。
-
Read関数内の呼び出し箇所の変更:--- a/src/pkg/encoding/binary/binary.go +++ b/src/pkg/encoding/binary/binary.go @@ -163,7 +163,7 @@ func Read(r io.Reader, order ByteOrder, data interface{}) error { default: return errors.New("binary.Read: invalid type " + d.Type().String()) }\n -\tsize := TotalSize(v) +\tsize := dataSize(v) if size < 0 { return errors.New("binary.Read: invalid type " + v.Type().String()) }Read関数内でTotalSize(v)を呼び出していた箇所がdataSize(v)に変更されました。
-
Write関数内の呼び出し箇所の変更:--- a/src/pkg/encoding/binary/binary.go +++ b/src/pkg/encoding/binary/binary.go @@ -242,7 +242,7 @@ func Write(w io.Writer, order ByteOrder, data interface{}) error { \treturn err }\n \tv := reflect.Indirect(reflect.ValueOf(data))\n -\tsize := TotalSize(v) +\tsize := dataSize(v) if size < 0 { return errors.New("binary.Write: invalid type " + v.Type().String()) }Write関数内でTotalSize(v)を呼び出していた箇所がdataSize(v)に変更されました。
-
テストコードの変更:
src/pkg/encoding/binary/binary_test.go内でも、TotalSizeの呼び出しがdataSizeに変更されています。--- a/src/pkg/encoding/binary/binary_test.go +++ b/src/pkg/encoding/binary/binary_test.go @@ -187,7 +187,7 @@ func BenchmarkReadStruct(b *testing.B) { bsr := &byteSliceReader{} var buf bytes.Buffer Write(&buf, BigEndian, &s)\n -\tn := TotalSize(reflect.ValueOf(s)) +\tn := dataSize(reflect.ValueOf(s)) b.SetBytes(int64(n))\n t := s b.ResetTimer()
これらの変更により、TotalSize は encoding/binary パッケージの外部からは見えなくなり、内部的なヘルパー関数としてのみ利用されるようになりました。
コアとなるコードの解説
このコミットのコアとなるコードは、src/pkg/encoding/binary/binary.go 内の TotalSize 関数のリネームと、その呼び出し箇所の変更です。
変更前:
func TotalSize(v reflect.Value) int {
// ... implementation ...
}
func Read(r io.Reader, order ByteOrder, data interface{}) error {
// ...
size := TotalSize(v) // Public function call
// ...
}
func Write(w io.Writer, order ByteOrder, data interface{}) error {
// ...
size := TotalSize(v) // Public function call
// ...
}
変更後:
// dataSize returns the number of bytes the actual data represented by v occupies in memory.
// For compound structures, it sums the sizes of the elements. Thus, for instance, for a slice
// it returns the length of the slice times the element size and does not count the memory
// occupied by the header.
func dataSize(v reflect.Value) int { // Renamed to be unexported
// ... implementation ...
}
func Read(r io.Reader, order ByteOrder, data interface{}) error {
// ...
size := dataSize(v) // Internal function call
// ...
}
func Write(w io.Writer, order ByteOrder, data interface{}) error {
// ...
size := dataSize(v) // Internal function call
// ...
}
解説:
-
TotalSizeからdataSizeへのリネーム:- Go言語の命名規則では、関数名や変数名の最初の文字が大文字の場合、その識別子はパッケージ外にエクスポートされ、小文字の場合、パッケージ内部でのみ使用可能な非公開(unexported)となります。
TotalSizeは大文字で始まるため、encoding/binaryパッケージの外部からbinary.TotalSize()として呼び出すことが可能でした。dataSizeは小文字で始まるため、encoding/binaryパッケージの内部でのみ使用可能となり、外部からは直接呼び出すことができなくなります。- このリネームは、
TotalSizeが本来、encoding/binaryパッケージの内部的なヘルパー関数であり、外部に公開されるべきではないという設計上の判断に基づいています。
-
reflect.Valueの扱い:dataSize(旧TotalSize) 関数は、引き続きreflect.Valueを引数として受け取ります。これは、この関数がGoのリフレクション機能を利用して、任意のGoの値のメモリサイズを動的に計算する必要があるためです。- しかし、この変更により、
reflect.Valueを直接扱うAPIがパッケージの外部に露出することがなくなりました。外部ユーザーは、binary.Readやbinary.Writeのような高レベルのAPIを通じて、interface{}型のデータを渡すだけでよく、内部でリフレクションがどのように使われているかを意識する必要がなくなります。これは、APIの抽象度を高め、使いやすさを向上させます。
-
ReadおよびWrite関数への影響:binary.Readとbinary.Writeは、引き続きinterface{}型のdata引数を受け取ります。これらの関数は、内部でreflect.ValueOf(data)を呼び出してreflect.Valueを取得し、それをdataSize関数に渡します。- この変更は、
ReadおよびWrite関数の外部APIには影響を与えません。ユーザーはこれまで通りこれらの関数を使用できますが、内部で呼び出されるサイズ計算関数が非公開になっただけです。
-
コメントの追加:
dataSize関数には、その役割を明確にするための詳細なコメントが追加されました。特に、「スライスの場合、要素のサイズにスライスの長さを掛けたものを返し、ヘッダが占めるメモリはカウントしない」という点が明記されています。これは、この関数がバイナリエンコーディングにおける「データの実体」のサイズを計算するものであり、Goの内部的なデータ構造(例えば、スライスのヘッダ情報)のサイズを含まないことを示しています。
このコミットは、Go言語のAPI設計における「シンプルさ」「責務の分離」「内部実装の隠蔽」という原則を遵守するための典型的な例と言えます。
関連リンク
- Go Issue 2846: https://github.com/golang/go/issues/2846
- Go CL 5639054: https://golang.org/cl/5639054 (Goの変更リスト、このコミットに対応するレビュープロセス)
参考にした情報源リンク
- Go言語
encoding/binaryパッケージ公式ドキュメント: https://pkg.go.dev/encoding/binary - Go言語
reflectパッケージ公式ドキュメント: https://pkg.go.dev/reflect - Go言語におけるリフレクションの解説記事 (例: Medium, dev.to など、検索結果から適切なものを選択):
- Go言語のAPI設計原則に関する情報 (例: Go公式ブログ、Effective Goなど):
- https://go.dev/doc/effective_go#names (命名規則について)
- https://go.dev/blog/go1compat (Go 1の互換性保証について)
- Go言語のIssueトラッカー (GitHub): https://github.com/golang/go/issues
- Go言語のコードレビューシステム (Gerrit): https://go.googlesource.com/go/+/refs/heads/master (CLリンクのベースURL)