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

[インデックス 10589] ファイルの概要

このコミットは、Go言語の標準ライブラリである encoding/gob パッケージにおける挙動の修正に関するものです。具体的には、構造体のエクスポートされていない(unexported)フィールドの型情報が、gob エンコーダによって誤って送信される問題を解決しています。これにより、gob エンコーディングの堅牢性と、エクスポートされていないフィールドを持つ構造体の扱いが改善されます。

コミット

commit 30775f67e7d5e897d4d9aafe8ab84a5f65550ce4
Author: Rob Pike <r@golang.org>
Date:   Fri Dec 2 00:02:24 2011 -0800

    encoding/gob: don't send type info for unexported fields
    Fixes #2517.
    
    R=golang-dev, dsymonds
    CC=golang-dev
    https://golang.org/cl/5440079

GitHub上でのコミットページへのリンク

https://github.com/golang/go/commit/30775f67e7d5e897d4d9aafe8ab84a5f65550ce4

元コミット内容

encoding/gob: don't send type info for unexported fields Fixes #2517.

R=golang-dev, dsymonds CC=golang-dev https://golang.org/cl/5440079

変更の背景

この変更は、Go言語の encoding/gob パッケージが、構造体のエクスポートされていないフィールドの型情報までエンコードしようとしていたバグ(Issue #2517)を修正するために行われました。Go言語では、構造体のフィールドが小文字で始まる場合、それはエクスポートされていない(unexported)フィールドとみなされ、パッケージ外からは直接アクセスできません。gob エンコーダは、データ構造をシリアライズする際に、その型情報も一緒に送信します。しかし、エクスポートされていないフィールドは、通常、シリアライズの対象外とすべきであり、その型情報まで送信することは不適切でした。

特に、chan (チャネル) や func (関数) のような型は、gob でのエンコードがサポートされていません。もしエクスポートされていないこれらの型のフィールドが構造体に含まれていた場合、gob エンコーダがその型情報を送信しようとすると、エンコードエラーが発生していました。このバグは、開発者が意図せずエクスポートされていないチャネルや関数フィールドを持つ構造体を gob でエンコードしようとした際に、予期せぬエラーを引き起こす可能性がありました。

このコミットは、gob エンコーダがエクスポートされていないフィールドの型情報を送信しないようにすることで、この問題を解決し、gob の堅牢性を向上させ、より柔軟なデータ構造のシリアライズを可能にすることを目的としています。

前提知識の解説

Go言語におけるエクスポートされた(Exported)フィールドとエクスポートされていない(Unexported)フィールド

Go言語では、識別子(変数名、関数名、型名、構造体フィールド名など)の最初の文字が大文字であるか小文字であるかによって、その可視性(visibility)が決定されます。

  • エクスポートされた(Exported)識別子: 最初の文字が大文字の場合、その識別子はパッケージ外からアクセス可能です。これは、他の言語における public に相当します。
  • エクスポートされていない(Unexported)識別子: 最初の文字が小文字の場合、その識別子は定義されたパッケージ内からのみアクセス可能です。これは、他の言語における privateinternal に相当します。

encoding/gob のようなシリアライズメカニズムでは、通常、エクスポートされたフィールドのみがシリアライズの対象となります。これは、エクスポートされていないフィールドが内部的な状態を表すことが多く、外部に公開すべきではないためです。

encoding/gob パッケージ

encoding/gob は、Go言語のデータ構造をバイナリ形式でエンコード(シリアライズ)およびデコード(デシリアライズ)するための標準パッケージです。主にGoプログラム間でのデータ交換や、永続化のために使用されます。gob は、データだけでなく、その型情報も一緒にエンコードするため、受信側は事前に型を知らなくてもデータをデコードできるという特徴があります。

gob の主な特徴は以下の通りです。

  • 自己記述的: データストリームには型情報が含まれるため、受信側は送信側と同じ型定義を持っていなくてもデコードできます。
  • 効率的: バイナリ形式であり、JSONやXMLに比べてコンパクトで高速です。
  • Go固有: Goの型システムに特化しており、構造体、スライス、マップなどのGoのデータ型を直接エンコードできます。
  • エクスポートされたフィールドのみ: デフォルトでは、構造体のエクスポートされたフィールドのみがエンコード/デコードの対象となります。

reflect パッケージ

reflect パッケージは、Go言語の実行時リフレクション機能を提供します。これにより、プログラムは自身の構造を検査し、実行時に変数の型や値を操作することができます。

  • reflect.Type: Goの型の情報を表します。Kind() メソッドでその型が構造体、配列、スライスなど、どのような種類であるかを取得できます。
  • reflect.Value: Goの変数の値を表します。
  • Type.NumField(): 構造体のフィールド数を返します。
  • Type.Field(i): 構造体の i 番目のフィールドの StructField を返します。
  • StructField.Name: フィールド名を返します。
  • StructField.Type: フィールドの型を reflect.Type として返します。

このコミットでは、reflect パッケージを使用して構造体のフィールドを走査し、そのフィールドがエクスポートされているかどうかを isExported 関数で判定しています。

技術的詳細

このコミットの核心は、encoding/gob パッケージの Encoder 型の sendActualType メソッドにおける変更です。このメソッドは、gob ストリームに実際の型情報を送信する役割を担っています。

変更前は、reflect.Struct 型の処理において、構造体のすべてのフィールドを無条件に走査し、それぞれのフィールドの型情報を enc.sendType メソッドで送信していました。

// 変更前
case reflect.Struct:
    for i := 0; i < st.NumField(); i++ {
        enc.sendType(w, state, st.Field(i).Type)
    }

この挙動が問題でした。Goの慣習として、エクスポートされていないフィールド(小文字で始まるフィールド)は、パッケージ内部でのみ使用されるべきであり、外部に公開されるシリアライズデータには含まれるべきではありません。しかし、gob はこれらのフィールドの型情報まで送信しようとしていました。特に、chanfunc のような gob でエンコードできない型がエクスポートされていないフィールドとして存在する場合、型情報の送信自体がエラーを引き起こしていました。

このコミットでは、sendActualType メソッド内のループに条件分岐が追加されました。st.Field(i).NameisExported 関数でチェックし、そのフィールドがエクスポートされている場合にのみ enc.sendType を呼び出すように変更されました。

// 変更後
case reflect.Struct:
    for i := 0; i < st.NumField(); i++ {
        if isExported(st.Field(i).Name) { // ここが追加された条件
            enc.sendType(w, state, st.Field(i).Type)
        }
    }

isExported 関数は、Go言語の reflect パッケージの内部で定義されているヘルパー関数で、識別子の最初の文字が大文字であるかどうかをチェックすることで、その識別子がエクスポートされているかどうかを判定します。この変更により、gob エンコーダは、エクスポートされていないフィールドの型情報をストリームに含めなくなり、chanfunc のようなエンコード不可能な型がエクスポートされていないフィールドとして存在しても、エンコードエラーが発生しなくなりました。

テストケース TestUnexportedChan が追加され、この修正が正しく機能することを確認しています。このテストでは、エクスポートされていないチャネルフィールドを持つ構造体を gob でエンコードし、エラーが発生しないことを検証しています。

コアとなるコードの変更箇所

変更は src/pkg/encoding/gob/encoder.go ファイルの Encoder 型の sendActualType メソッド内で行われています。

--- a/src/pkg/encoding/gob/encoder.go
+++ b/src/pkg/encoding/gob/encoder.go
@@ -119,7 +119,9 @@ func (enc *Encoder) sendActualType(w io.Writer, state *encoderState, ut *userTyp
 	switch st := actual; st.Kind() {
 	case reflect.Struct:
 		for i := 0; i < st.NumField(); i++ {
-			enc.sendType(w, state, st.Field(i).Type)
+			if isExported(st.Field(i).Name) {
+				enc.sendType(w, state, st.Field(i).Type)
+			}
 		}
 	case reflect.Array, reflect.Slice:
 		enc.sendType(w, state, st.Elem())

また、src/pkg/encoding/gob/encoder_test.go に新しいテストケースが追加されています。

--- a/src/pkg/encoding/gob/encoder_test.go
+++ b/src/pkg/encoding/gob/encoder_test.go
@@ -662,3 +662,19 @@ func TestSequentialDecoder(t *testing.T) {
 		}
 	}
 }
+
+// Should be able to have unrepresentable fields (chan, func) as long as they
+// are unexported.
+type Bug2 struct {
+	A int
+	b chan int
+}
+
+func TestUnexportedChan(t *testing.T) {
+	b := Bug2{23, make(chan int)}
+	var stream bytes.Buffer
+	enc := NewEncoder(&stream)
+	if err := enc.Encode(b); err != nil {
+		t.Fatalf("error encoding unexported channel: %s", err)
+	}
+}

コアとなるコードの解説

src/pkg/encoding/gob/encoder.go の変更

sendActualType 関数は、gob エンコーダが型情報をストリームに書き込む際に呼び出されます。reflect.Struct のケースでは、構造体の各フィールドを反復処理します。

変更前は、for i := 0; i < st.NumField(); i++ ループ内で、st.Field(i).Type を直接 enc.sendType に渡していました。これは、エクスポートされているかどうかにかかわらず、すべてのフィールドの型情報を送信することを意味していました。

変更後は、if isExported(st.Field(i).Name) という条件が追加されました。

  • st.Field(i).Name は、現在のフィールドの名前(文字列)を返します。
  • isExported 関数は、Go言語の reflect パッケージの内部で定義されている関数で、与えられた識別子名がエクスポートされている(つまり、最初の文字が大文字である)場合に true を返します。

この条件により、enc.sendType は、エクスポートされたフィールドの型情報に対してのみ呼び出されるようになります。これにより、エクスポートされていないフィールド(特に chanfunc のようなエンコード不可能な型)の型情報が gob ストリームに誤って含まれることがなくなり、関連するエンコードエラーが回避されます。

src/pkg/encoding/gob/encoder_test.go の追加テスト

TestUnexportedChan という新しいテスト関数が追加されました。

  1. Bug2 という構造体が定義されています。この構造体は、エクスポートされた int 型のフィールド A と、エクスポートされていない chan int 型のフィールド b を持ちます。
  2. Bug2 のインスタンス b が作成され、b.b には新しいチャネルが割り当てられます。
  3. bytes.Buffer を使用して gob エンコードの出力先となるメモリバッファを作成します。
  4. NewEncoder を使用して Encoder インスタンスを作成します。
  5. enc.Encode(b) を呼び出して、Bug2 インスタンスをエンコードします。
  6. if err := enc.Encode(b); err != nil でエンコード中にエラーが発生しなかったことを確認します。もしエラーが発生した場合、t.Fatalf でテストを失敗させます。

このテストは、エクスポートされていないチャネルフィールドを持つ構造体が、エラーなく gob エンコードできることを検証しています。これは、コミットによって修正された問題が実際に解決されたことを示す重要なテストです。

関連リンク

  • Go Issue #2517: encoding/gob: don't send type info for unexported fields - https://github.com/golang/go/issues/2517
  • Go Code Review: https://golang.org/cl/5440079 - このコミットのコードレビューページ。

参考にした情報源リンク

  • Go言語の公式ドキュメント: encoding/gob パッケージ - https://pkg.go.dev/encoding/gob
  • Go言語の公式ドキュメント: reflect パッケージ - https://pkg.go.dev/reflect
  • Go言語の可視性ルール(Exported/Unexported identifiers)に関する一般的な情報源。