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

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

このコミットは、Go言語の標準ライブラリ encoding/gob パッケージにおける互換性の問題を修正するためのものです。具体的には、以前のコミット 6348067 によって導入された変更が、既存のgobエンコーディングとの互換性を損ねていたため、その変更を元に戻し、さらに詳細なコメントを追加して状況を説明しています。

コミット

commit 7b73251d3d1153485b7f78d53ce7cb86b1b4d762
Author: Rob Pike <r@golang.org>
Date:   Mon Jul 23 13:34:46 2012 -0700

    encoding/gob: revert 6348067, which broke compatibility
    Add commentary to explain better what's going on, but the
    code change is a simple one-line reversal to the previous
    form.
    
    R=rsc
    CC=golang-dev
    https://golang.org/cl/6428072

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

https://github.com/golang/go/commit/7b73251d3d1153485b7f78d53ce7cb86b1b4d762

元コミット内容

このコミットは、以前のコミット 6348067 を元に戻すものです。元のコミット 6348067 の内容は、ポインタ型(例: *T1)のgobエンコーディングにおける型名の扱いを変更しようとしたものと推測されます。具体的には、ポインタの指す基底型が名前付き型である場合、その型名に完全なインポートパスを含めるように変更しようとした可能性があります。しかし、この変更が既存のgobデータとの互換性を破壊したため、このコミットで元に戻されました。

変更の背景

Go言語の encoding/gob パッケージは、Goのデータ構造をシリアライズおよびデシリアライズするためのメカニズムを提供します。gobは、型情報をデータストリームに含めることで、異なるGoプログラム間でのデータ交換を可能にします。この型情報は、Goの reflect パッケージを使用して実行時に取得されます。

問題は、ポインタ型(例: *p.T1)の型名がgobストリームにどのように書き込まれるかという点にありました。理想的には、型名はその型の完全な識別子、つまりパッケージのインポートパスを含むべきです(例: *full/p.T1)。しかし、過去のgobの実装では、ポインタ型の型名からパッケージのインポートパスが省略され、単にパッケージ名のみが含まれていました(例: *p.T1)。

以前のコミット 6348067 は、この「欠落した完全パス」の問題を修正しようとしました。つまり、ポインタ型の型名も完全なインポートパスを含むように変更しようとしたのです。しかし、この変更は、既にこの「欠落した完全パス」の形式でエンコードされた既存のgobデータとの互換性を破壊しました。gobデコーダは、エンコードされた型名と、デコード時に期待される型名が一致しない場合、デコードに失敗します。

このコミットは、この互換性の破壊を修正するために、6348067 で行われた変更を元に戻し、元の(互換性のある)動作に戻すことを目的としています。同時に、この互換性の問題と、なぜ完全なパスを含めることができないのかについての詳細な説明をコードコメントとして追加しています。

前提知識の解説

Go言語の encoding/gob パッケージ

encoding/gob パッケージは、Goのプログラム間でGoのデータ構造をエンコード(シリアライズ)およびデコード(デシリアライズ)するためのバイナリ形式を提供します。主な特徴は以下の通りです。

  • 自己記述型: gobストリームは、データだけでなく、そのデータの型情報も含まれています。これにより、受信側は送信側がどのような型のデータを送ってきたかを事前に知る必要がありません。
  • 効率性: バイナリ形式であり、テキストベースのエンコーディング(JSONなど)よりも効率的です。
  • Go固有: Goの型システムに密接に統合されており、構造体、スライス、マップ、プリミティブ型などを直接エンコードできます。
  • gob.Register: gobエンコーダ/デコーダが認識できるように、カスタム型を登録するために使用されます。これにより、インターフェース型を介して具体的な型をエンコード/デコードできるようになります。

Go言語の reflect パッケージ

reflect パッケージは、Goのプログラムが実行時に自身の構造を検査(リフレクション)することを可能にします。これにより、型情報、フィールド、メソッドなどを動的に操作できます。

  • reflect.TypeOf(value): 任意のGoの値の動的な型情報を返します。返される reflect.Type オブジェクトは、型の名前、種類(Kind)、要素型(ポインタの場合)などの情報を提供します。
  • reflect.Type.Name(): 型の名前を返します。名前付き型(type MyInt intMyInt など)の場合、その名前を返します。匿名型(struct{} など)の場合、空文字列を返します。
  • reflect.Type.Kind(): 型の基本的な種類(reflect.Int, reflect.Struct, reflect.Ptr など)を返します。
  • reflect.Type.Elem(): ポインタ、配列、スライス、マップ、チャネルの要素型を返します。例えば、*intElem()int の型情報を返します。

型の命名とインポートパス

Go言語では、型はパッケージ内で定義され、そのパッケージのインポートパスと型名によって一意に識別されます。例えば、github.com/user/repo/mypackage パッケージで定義された MyStruct 型は、完全には github.com/user/repo/mypackage.MyStruct と識別されます。

encoding/gob は、この完全な識別子を使用して型をエンコードすることが理想的です。これにより、異なるパッケージで同じ名前の型が定義されている場合でも、正しく区別できます。

技術的詳細

このコミットの核心は、encoding/gob パッケージが型名をどのように生成し、それが互換性にどう影響するかという点にあります。

Register 関数は、gobエンコーダ/デコーダが特定の型を認識できるように登録するために使用されます。この関数内で、reflect.TypeOf(value) を使用して値の型情報を取得し、その型名(rt.String()rt.Name())をgobストリームに書き込むための識別子として使用します。

問題は、ポインタ型(例: *T1)の型名を生成するロジックにありました。

元の(互換性のある)動作では、*T1 のようなポインタ型の場合、rt.Name() は空文字列を返します(ポインタ型自体は名前付き型ではないため)。しかし、rt.Kind() == reflect.Ptr でポインタであることが検出された場合、その要素型(rt.Elem())を取得して、その要素型が名前付き型であれば、その名前を使用するというロジックが期待されます。

しかし、過去のgobの実装では、ポインタ型の場合に rt = pt.Elem() とすべき箇所が rt = pt のままになっていました。つまり、ポインタの要素型ではなく、ポインタ型自体の情報を使って型名を構築しようとしていたため、ポインタの指す基底型のパッケージパスが欠落した形で型名が生成されていました。

例えば、package p で定義された type T1 struct {} がある場合:

  • T1 の型名: full/p.T1 (正しい)
  • *T1 の型名: *p.T1 (本来は *full/p.T1 となるべきだが、full/ が欠落している)

この「欠落した完全パス」の形式でエンコードされたgobデータが既に多数存在するため、6348067rt = pt.Elem() に修正しようとしたところ、既存のデータとの互換性が失われました。

このコミットは、この互換性の問題を解決するために、rt = pt.Elem() への変更を元に戻し、rt = pt のままにしています。これにより、既存のgobデータとの互換性が維持されます。

同時に、コード内に詳細なコメントを追加し、この互換性の問題と、なぜ理想的な型名(完全なインポートパスを含む)を使用できないのかを説明しています。これは、将来の開発者が同じ問題を再導入したり、この設計上の決定を理解したりするのに役立ちます。

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

変更は src/pkg/encoding/gob/type.go ファイルの Register 関数内にあります。

--- a/src/pkg/encoding/gob/type.go
+++ b/src/pkg/encoding/gob/type.go
@@ -749,13 +749,29 @@ func Register(value interface{}) {
 	rt := reflect.TypeOf(value)
 	name := rt.String()
 
-	// But for named types (or pointers to them), qualify with import path.
+	// But for named types (or pointers to them), qualify with import path (but see inner comment).
 	// Dereference one pointer looking for a named type.
 	star := ""
 	if rt.Name() == "" {
 		if pt := rt; pt.Kind() == reflect.Ptr {
 			star = "*"
-			rt = pt.Elem()
+			// NOTE: The following line should be rt = pt.Elem() to implement
+			// what the comment above claims, but fixing it would break compatibility
+			// with existing gobs.
+			//
+			// Given package p imported as "full/p" with these definitions:
+			//     package p
+			//     type T1 struct { ... }
+			// this table shows the intended and actual strings used by gob to
+			// name the types:
+			//
+			// Type      Correct string     Actual string
+			//
+			// T1        full/p.T1          full/p.T1
+			// *T1       *full/p.T1         *p.T1
+			//
+			// The missing full path cannot be fixed without breaking existing gob decoders.
+			rt = pt
 		}
 	}
 	if rt.Name() != "" {

コアとなるコードの解説

変更の中心は、if pt := rt; pt.Kind() == reflect.Ptr ブロック内の rt = pt.Elem()rt = pt に戻した点です。

  • rt = pt.Elem() (変更前/元に戻された変更): これは、ポインタ型 pt の要素型(つまり、ポインタが指す基底型)を取得しようとするものです。例えば、*T1 の場合、pt.Elem()T1reflect.Type を返します。これにより、T1 が名前付き型であれば、その完全なパッケージパスを含む型名(例: full/p.T1)を取得し、ポインタのプレフィックス * を付けて *full/p.T1 のような型名を生成することが意図されていました。

  • rt = pt (変更後/現在の状態): これは、ポインタ型 pt そのものを rt に再代入しています。つまり、ポインタの要素型を「デリファレンス」せずに、ポインタ型自体の情報を使って型名を構築しようとします。結果として、rt.Name() は空文字列を返し、rt.String()*p.T1 のような形式(パッケージパスが省略された形式)を生成することになります。

この変更は、6348067 で導入された rt = pt.Elem() が、既存のgobデータとの互換性を破壊したため、その変更を元に戻すものです。

追加されたコメントは非常に重要です。

// NOTE: The following line should be rt = pt.Elem() to implement
// what the comment above claims, but fixing it would break compatibility
// with existing gobs.
//
// Given package p imported as "full/p" with these definitions:
//     package p
//     type T1 struct { ... }
// this table shows the intended and actual strings used by gob to
// name the types:
//
// Type      Correct string     Actual string
//
// T1        full/p.T1          full/p.T1
// *T1       *full/p.T1         *p.T1
//
// The missing full path cannot be fixed without breaking existing gob decoders.

このコメントは、以下の点を明確にしています。

  1. 理想的な動作: 本来であれば rt = pt.Elem() とすべきであること。
  2. 互換性の問題: しかし、そうすると既存のgobデータとの互換性が失われること。
  3. 具体例: T1*T1 の型名が、理想的な形式と実際の形式でどのように異なるかを示す表。
  4. 結論: 既存のgobデコーダを壊すことなく、この「欠落した完全パス」の問題を修正することはできないこと。

このコメントは、なぜこの一見「不完全な」動作が維持されているのかを説明し、将来の変更が互換性を損なわないようにするための重要な警告として機能します。

関連リンク

参考にした情報源リンク

  • Go言語の公式ドキュメント
  • Go言語のソースコード (src/pkg/encoding/gob/type.go)
  • Go言語のコードレビューシステム (Gerrit) の変更リスト (https://golang.org/cl/6428072)
  • Go言語のコミュニティでの議論(関連する可能性のあるフォーラムやメーリングリストのアーカイブ)
  • Go言語におけるシリアライゼーションとデシリアライゼーションに関する一般的な情報源
  • Go言語のリフレクションに関するチュートリアルや解説記事
  • encoding/gob の互換性に関するGoのIssueや議論(もしあれば)
  • 6348067 コミットに関する情報(もし公開されていれば)