Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

[インデックス 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 パッケージの特定の関数の特性があります。

  1. APIのクリーンアップ: encoding/binary パッケージは、Goのプリミティブ型とバイトシーケンス間の変換を扱うためのものです。TotalSize 関数は、その目的から逸脱し、reflect パッケージを直接外部に露出させていました。これは、パッケージの責務を曖昧にし、APIの整合性を損なうものでした。Goの設計哲学では、APIはシンプルで予測可能であるべきであり、不必要な複雑さや内部実装の詳細を外部に晒すべきではありません。

  2. reflect パッケージの露出: TotalSize 関数は reflect.Value 型を引数にとり、Goの「リフレクション」機能を利用していました。リフレクションは強力な機能ですが、パフォーマンスオーバーヘッドがあり、型安全性を損なう可能性があるため、慎重に扱うべきです。特に、encoding/binary のような低レベルのバイナリ操作パッケージで、リフレクションが外部APIとして露出しているのは適切ではないと判断されました。

  3. 関数の「奇妙さ」と説明の困難さ: コミットメッセージにあるように、TotalSize 関数のシグネチャと定義は「奇妙で説明が困難」でした。これは、その機能がパッケージの主要な目的と合致しておらず、一般的なユースケースではほとんど必要とされないことを示唆しています。このような関数がエクスポートされていると、ユーザーが誤解したり、不適切に使用したりするリスクがありました。

  4. Go 1 リリースに向けたAPIの安定化: このコミットはGo 1のリリース前に行われています。Go 1は、Go言語の最初の安定版であり、そのAPIは長期にわたって互換性が維持されることが約束されていました。そのため、この時期には、将来的な互換性の問題を引き起こす可能性のある、設計上不適切と判断されたAPI要素を修正する作業が活発に行われていました。TotalSize のような「奇妙な」関数を隠蔽することは、Go 1のAPIをより堅牢で保守しやすいものにするための重要なステップでした。

  5. Issue #2846 の解決: このコミットは、GoのIssueトラッカーで報告されていた #2846 を修正するものです。このIssueは、encoding/binary パッケージのAPIに関する議論であり、TotalSize 関数の存在が問題視されていたことを示しています。

これらの背景から、TotalSize 関数を非公開(内部関数)にすることは、encoding/binary パッケージの設計を改善し、Go言語全体のAPI品質を高めるための合理的な判断であったと言えます。

前提知識の解説

このコミットを理解するためには、以下のGo言語の概念とパッケージに関する知識が必要です。

  1. encoding/binary パッケージ:

    • Goの標準ライブラリの一つで、Goのプリミティブ型(整数、浮動小数点数など)とバイトシーケンスの間でデータを変換するための機能を提供します。
    • 主に、ネットワークプロトコルやファイルフォーマットなど、バイナリデータを扱う際に使用されます。
    • バイトオーダー(エンディアンネス、binary.BigEndianbinary.LittleEndian)を指定して、バイト列の解釈方法を制御できます。
    • binary.Readbinary.Write といった関数が主要なAPIです。これらは io.Readerio.Writer インターフェースと連携し、ストリームからの読み書きを可能にします。
  2. reflect パッケージ (リフレクション):

    • Goの標準ライブラリの一つで、プログラムの実行時に型情報(reflect.Type)や値(reflect.Value)を検査・操作する機能を提供します。
    • これにより、コンパイル時には型が不明なデータに対しても、動的に操作を行うことができます。
    • 例えば、構造体のフィールド名やタグを読み取ったり、メソッドを動的に呼び出したりする際に利用されます。
    • reflect.TypeOf(i interface{}) Type は任意のインターフェース値の動的な型を返します。
    • reflect.ValueOf(i interface{}) Value は任意のインターフェース値の動的な値を返します。
    • 注意点: リフレクションは非常に強力ですが、以下の理由から乱用すべきではありません。
      • パフォーマンスオーバーヘッド: 静的な型付けされたコードに比べて実行時コストが高くなります。
      • 型安全性: コンパイル時の型チェックをバイパスするため、実行時エラー(パニック)のリスクが高まります。
      • 可読性・保守性: リフレクションを多用したコードは、理解やデバッグが難しくなる傾向があります。
    • 通常、リフレクションは、JSONエンコーディング/デコーディング、ORM、テストフレームワークなど、特定の汎用的なライブラリやフレームワークの内部で利用されることがほとんどです。アプリケーションコードで直接使用することは稀です。
  3. エクスポートされた識別子と非エクスポート識別子:

    • Go言語では、識別子(変数名、関数名、型名など)の最初の文字が大文字である場合、その識別子はパッケージ外からアクセス可能(エクスポートされている)になります。
    • 最初の文字が小文字である場合、その識別子はパッケージ内でのみアクセス可能(エクスポートされていない、非公開)になります。
    • API設計において、外部に公開すべきでない内部実装の詳細は、小文字で始まる識別子として定義し、隠蔽することが推奨されます。
  4. APIの安定性 (Go 1):

    • Go 1は、Go言語の最初の安定版リリースであり、そのAPIは将来にわたって互換性が維持されることが強く約束されました。
    • Go 1リリース前の期間は、APIの最終調整と安定化が行われる重要な時期でした。この時期に行われた変更は、長期的なGoエコシステムの健全性を確保するためのものでした。

これらの知識を前提とすることで、TotalSize 関数がなぜエクスポートされるべきではなかったのか、そしてその変更がGo言語の設計原則にどのように合致しているのかを深く理解できます。

技術的詳細

このコミットの技術的な核心は、encoding/binary パッケージ内の TotalSize 関数を非公開の dataSize 関数にリネームし、その呼び出し箇所を更新することです。

  1. TotalSize 関数の役割:

    • コミット前の TotalSize 関数は、reflect.Value 型の引数を受け取り、その値がバイナリ形式で占めるメモリ上のサイズ(バイト数)を計算していました。
    • この関数は、encoding/binary パッケージの Read および Write 関数内で、データのサイズを事前に決定するために使用されていました。
    • 例えば、Read 関数は、ストリームから読み込むべきバイト数を事前に知るために TotalSize を呼び出し、Write 関数は、書き込むべきバイト数を決定するために同様に呼び出していました。
  2. reflect パッケージの利用:

    • TotalSize 関数は、引数として reflect.Value を受け取っていました。これは、Goのリフレクション機能を利用していることを意味します。
    • リフレクションを使用することで、TotalSize は、コンパイル時に型が不明な任意のGoの値(構造体、配列、スライスなど)の内部構造を動的に検査し、そのサイズを計算することができました。
    • しかし、reflect パッケージを直接外部に露出させることは、encoding/binary パッケージのAPIを複雑にし、リフレクションのパフォーマンスオーバーヘッドや型安全性の問題が外部ユーザーにも影響を及ぼす可能性がありました。
  3. 変更の理由:

    • APIのクリーンアップ: TotalSize は、encoding/binary パッケージの主要な目的(Goの型とバイトシーケンス間の変換)から逸脱しており、reflect パッケージの内部的な利用を外部に晒していました。これを非公開にすることで、パッケージのAPIがよりシンプルで、その責務に集中したものになります。
    • 不必要な露出の排除: TotalSize は、encoding/binary パッケージの内部実装の詳細であり、外部ユーザーが直接呼び出す必要はほとんどありませんでした。このような内部ヘルパー関数をエクスポートすることは、APIの混乱を招きます。
    • 「奇妙なシグネチャ」: reflect.Value を引数にとるシグネチャは、encoding/binary パッケージの他の関数(通常は interface{} や具体的な型を扱う)とは異質でした。これにより、APIの一貫性が損なわれていました。
  4. 具体的な変更内容:

    • src/pkg/encoding/binary/binary.go 内で、func TotalSize(v reflect.Value) intfunc 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 ファイルに集中しています。

  1. 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 が表すデータのメモリ上のサイズを計算する目的が明確に記述されました。特に、スライスの場合にはヘッダのメモリは含まれないことが明記されています。
  2. 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) に変更されました。
  3. 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) に変更されました。
  4. テストコードの変更: 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()
    

これらの変更により、TotalSizeencoding/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
    // ...
}

解説:

  1. TotalSize から dataSize へのリネーム:

    • Go言語の命名規則では、関数名や変数名の最初の文字が大文字の場合、その識別子はパッケージ外にエクスポートされ、小文字の場合、パッケージ内部でのみ使用可能な非公開(unexported)となります。
    • TotalSize は大文字で始まるため、encoding/binary パッケージの外部から binary.TotalSize() として呼び出すことが可能でした。
    • dataSize は小文字で始まるため、encoding/binary パッケージの内部でのみ使用可能となり、外部からは直接呼び出すことができなくなります。
    • このリネームは、TotalSize が本来、encoding/binary パッケージの内部的なヘルパー関数であり、外部に公開されるべきではないという設計上の判断に基づいています。
  2. reflect.Value の扱い:

    • dataSize (旧 TotalSize) 関数は、引き続き reflect.Value を引数として受け取ります。これは、この関数がGoのリフレクション機能を利用して、任意のGoの値のメモリサイズを動的に計算する必要があるためです。
    • しかし、この変更により、reflect.Value を直接扱うAPIがパッケージの外部に露出することがなくなりました。外部ユーザーは、binary.Readbinary.Write のような高レベルのAPIを通じて、interface{} 型のデータを渡すだけでよく、内部でリフレクションがどのように使われているかを意識する必要がなくなります。これは、APIの抽象度を高め、使いやすさを向上させます。
  3. Read および Write 関数への影響:

    • binary.Readbinary.Write は、引き続き interface{} 型の data 引数を受け取ります。これらの関数は、内部で reflect.ValueOf(data) を呼び出して reflect.Value を取得し、それを dataSize 関数に渡します。
    • この変更は、Read および Write 関数の外部APIには影響を与えません。ユーザーはこれまで通りこれらの関数を使用できますが、内部で呼び出されるサイズ計算関数が非公開になっただけです。
  4. コメントの追加:

    • dataSize 関数には、その役割を明確にするための詳細なコメントが追加されました。特に、「スライスの場合、要素のサイズにスライスの長さを掛けたものを返し、ヘッダが占めるメモリはカウントしない」という点が明記されています。これは、この関数がバイナリエンコーディングにおける「データの実体」のサイズを計算するものであり、Goの内部的なデータ構造(例えば、スライスのヘッダ情報)のサイズを含まないことを示しています。

このコミットは、Go言語のAPI設計における「シンプルさ」「責務の分離」「内部実装の隠蔽」という原則を遵守するための典型的な例と言えます。

関連リンク

参考にした情報源リンク