[インデックス 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)