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

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

このコミットは、Go言語の型システムを扱う go/types パッケージにおける改善です。具体的には、構造体(struct)に組み込み型(predeclared types)を埋め込んだ際のフィールド解決の挙動を修正し、より正確にGo言語の仕様に準拠するように変更されています。

コミット

commit 1caaff6b5a4e9c1d8d8ac19e89e1f740d919f3b6
Author: Robert Griesemer <gri@golang.org>
Date:   Mon Feb 25 20:42:29 2013 -0800

    go/types: embedded fields can be predeclared types
    
    R=adonovan, r
    CC=golang-dev
    https://golang.org/cl/7376055

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

https://github.com/golang/go/commit/1caaff6b5a4e9c1d8d8ac19e89e1f740d919f3b6

元コミット内容

go/types: embedded fields can be predeclared types

R=adonovan, r
CC=golang-dev
https://golang.org/cl/7376055

変更の背景

Go言語の構造体埋め込み(struct embedding)は、他の型を匿名フィールドとして構造体内に含めることで、その型のフィールドやメソッドを外部の構造体に「昇格」させる機能です。これにより、コードの再利用性とコンポジション(合成)を促進します。

このコミット以前の go/types パッケージの型チェッカーは、構造体に埋め込まれたフィールドが、intfloat32 のようなGoの組み込み型(predeclared types)である場合に、そのフィールドの解決を正しく処理できていませんでした。Goの仕様では、組み込み型も匿名フィールドとして埋め込むことができ、その場合、埋め込まれた型の名前(例: int)を使ってフィールドにアクセスできます。しかし、型チェッカーの実装が、埋め込まれた型がメソッドや構造体フィールドを持つ「ユーザー定義の命名された型(NamedType)」であると暗黙的に仮定していたため、組み込み型が埋め込まれた際に不適切な処理が行われ、コンパイルエラーや誤った型解決が発生する可能性がありました。

この変更は、go/types パッケージがGo言語の構造体埋め込みのセマンティクスを完全にサポートし、組み込み型が埋め込まれた場合でも正しくフィールドを解決できるようにするために導入されました。

前提知識の解説

Go言語の型システム

Go言語は静的型付け言語であり、コンパイル時に厳密な型チェックが行われます。go/types パッケージは、Goコンパイラの重要な一部であり、ソースコードの抽象構文木(AST)を解析し、型の整合性、識別子の解決、メソッドセットの構築など、Goプログラムのセマンティックな分析を担当します。

構造体埋め込み (Struct Embedding)

Go言語の構造体は、他の構造体やインターフェースを匿名フィールドとして埋め込むことができます。これにより、埋め込まれた型のフィールドやメソッドが、外側の構造体のフィールドやメソッドとして「昇格」されます。例えば:

type Inner struct {
    A int
}

type Outer struct {
    Inner // Inner型を埋め込み
    B string
}

func main() {
    o := Outer{Inner: Inner{A: 10}, B: "hello"}
    fmt.Println(o.A) // InnerのフィールドAに直接アクセスできる
}

この機能は、継承ではなくコンポジションを通じてコードの再利用を実現するGoの設計思想を反映しています。

組み込み型 (Predeclared Types)

Go言語には、int, float32, string, bool など、言語仕様によってあらかじめ定義されている基本的な型があります。これらは「組み込み型」または「事前宣言型」と呼ばれます。これらの型は通常、メソッドを持たず、構造体フィールドも持ちません(プリミティブな値そのものであるため)。

go/types パッケージのフィールド解決ロジック

go/types パッケージは、構造体のフィールドやメソッドをルックアップする際に、埋め込みの階層を辿って解決を行います。これは、lookupFieldlookupFieldBreadthFirst といった関数によって実現されます。これらの関数は、特定の名前を持つフィールドやメソッドが、埋め込まれた型を通じてアクセス可能かどうかを判断します。

技術的詳細

このコミットの核心は、go/types パッケージ内のフィールドルックアップロジックの改善です。特に、lookupFieldBreadthFirst 関数と lookupField 関数において、埋め込まれた型が「命名された型(*NamedType)」であるかどうかを明示的にチェックする条件が追加されました。

以前の実装では、埋め込まれたフィールドの型を deref(f.Type).(*NamedType) のように *NamedType に型アサートしようとしていました。これは、埋め込まれる型が常にユーザー定義の命名された型(例:struct, interface)であるという暗黙の仮定があったためと考えられます。しかし、Goの仕様では、intfloat32 のような組み込み型も匿名フィールドとして埋め込むことが可能です。

組み込み型は *NamedType ではありません。intBasic 型であり、*NamedType への型アサートは失敗し、nil を返します。この nil が後続の処理で適切に扱われない場合、パニックや誤った型解決につながる可能性がありました。

新しいコードでは、if t, _ := deref(f.Type).(*NamedType); t != nil { ... } というパターンが導入されています。これは、deref(f.Type) の結果を *NamedType に型アサートし、その結果が nil でない(つまり、実際に命名された型である)場合にのみ、その型を next リストに追加して、さらにフィールドルックアップの対象とします。

この変更により、go/types は以下の挙動を実現します:

  1. 組み込み型の埋め込みの無視: intfloat32 のような組み込み型が埋め込まれた場合、それらはメソッドや構造体フィールドを持たないため、フィールドルックアップの対象として next リストに追加されなくなります。これにより、無関係な型を処理しようとする試みが回避されます。
  2. 正確な型解決: ユーザー定義の命名された型(構造体やインターフェースなど)が埋め込まれた場合は、これまで通り正しくフィールドルックアップの対象となり、そのフィールドやメソッドが外側の構造体に昇格されます。
  3. 仕様への準拠: Go言語の仕様で許容されている「組み込み型の埋め込み」が、型チェッカーによって正しく解釈され、コンパイルが成功するようになります。

この修正は、go/types パッケージがGo言語のセマンティクスをより正確に反映し、コンパイラの堅牢性を向上させるための重要なステップです。

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

変更は主に src/pkg/go/types/operand.go ファイルの lookupFieldBreadthFirst 関数と lookupField 関数にあります。

--- a/src/pkg/go/types/operand.go
+++ b/src/pkg/go/types/operand.go
@@ -300,7 +300,11 @@ func lookupFieldBreadthFirst(list []embeddedType, name QualifiedName) (res looku
 					// this level, f.Type appears multiple times at the next
 					// level.
 					if f.IsAnonymous && res.mode == invalid {
-						next = append(next, embeddedType{deref(f.Type).(*NamedType), e.multiples})
+						// Ignore embedded basic types - only user-defined
+						// named types can have methods or have struct fields.
+						if t, _ := deref(f.Type).(*NamedType); t != nil {
+							next = append(next, embeddedType{t, e.multiples})
+						}
 					}
 				}
 
@@ -377,7 +381,11 @@ func lookupField(typ Type, name QualifiedName) lookupResult {
 				// Possible optimization: If the embedded type
 				// is a pointer to the current type we could
 				// ignore it.
-				next = append(next, embeddedType{typ: deref(f.Type).(*NamedType)})\n
+				// Ignore embedded basic types - only user-defined
+				// named types can have methods or have struct fields.
+				if t, _ := deref(f.Type).(*NamedType); t != nil {
+					next = append(next, embeddedType{typ: t})
+				}
 			}
 		}
 		if len(next) > 0 {

また、src/pkg/go/types/testdata/decls3.src には、この変更によって正しく処理されるようになる新しいテストケースが追加されています。

--- a/src/pkg/go/types/testdata/decls3.src
+++ b/src/pkg/go/types/testdata/decls3.src
@@ -44,6 +44,28 @@ func issue4355() {
 	_ = t /* ERROR "no single field or method" */ .X
 }
 
+// Embedded fields can be predeclared types.
+
+func _() {
+	type T0 struct{
+		int
+		float32
+		f int
+	}
+	var x T0
+	_ = x.int
+	_ = x.float32
+	_ = x.f
+
+	type T1 struct{
+		T0
+	}
+	var y T1
+	_ = y.int
+	_ = y.float32
+	_ = y.f
+}
+
 // Borrowed from the FieldByName test cases in reflect/all_test.go.
 
 type D1 struct {

コアとなるコードの解説

変更されたコードは、go/types パッケージが構造体のフィールドをルックアップする際の内部ロジックを改善しています。

operand.go の変更点

lookupFieldBreadthFirstlookupField は、構造体内のフィールドや埋め込まれた型を探索し、指定された名前のフィールドを見つけるための関数です。

変更前のコードでは、埋め込まれたフィールド f の型 f.Typederef 関数でポインタを解決した後、直接 (*NamedType) に型アサートしていました。これは、埋め込まれる型が常にユーザー定義の命名された型(例: 構造体、インターフェース)であるという前提に基づいていた可能性があります。

変更後のコードでは、以下の行が追加されています。

// Ignore embedded basic types - only user-defined
// named types can have methods or have struct fields.
if t, _ := deref(f.Type).(*NamedType); t != nil {
    next = append(next, embeddedType{t, e.multiples}) // または embeddedType{typ: t}
}

このコードスニペットの解説:

  • deref(f.Type): 埋め込まれたフィールド f の型がポインタ型である場合、そのポインタが指す基底の型を取得します。
  • t, _ := ... (*NamedType): deref(f.Type) の結果を *NamedType に型アサートしようとします。
    • もし deref(f.Type) の結果が実際に *NamedType であれば、t にその値が代入され、_ (ok値) は true になります。
    • もし deref(f.Type) の結果が *NamedType ではない(例: intfloat32 のような組み込み型)場合、t はその型のゼロ値(この場合は nil)になり、_ (ok値) は false になります。
  • if t != nil: この条件は、型アサートが成功し、t が有効な *NamedType である場合にのみ、その型を next リストに追加して、さらにフィールドルックアップの対象とすることを保証します。

これにより、intfloat32 のような組み込み型が埋め込まれた場合、それらは *NamedType ではないため tnil となり、この if ブロック内のコードは実行されません。つまり、組み込み型はフィールドルックアップの対象から除外され、型チェッカーが不適切な処理を試みることを防ぎます。

decls3.src のテストケース

追加されたテストケースは、この修正の意図を明確に示しています。

type T0 struct{
    int
    float32
    f int
}
var x T0
_ = x.int     // 組み込み型 int を匿名フィールドとして埋め込み、アクセス
_ = x.float32 // 組み込み型 float32 を匿名フィールドとして埋め込み、アクセス
_ = x.f       // 通常のフィールドアクセス

type T1 struct{
    T0 // T0を埋め込み
}
var y T1
_ = y.int     // T0を介して int にアクセス
_ = y.float32 // T0を介して float32 にアクセス
_ = y.f       // T0を介して f にアクセス

このテストケースは、intfloat32 といった組み込み型が構造体に匿名フィールドとして埋め込まれた場合でも、その型名を使ってフィールドにアクセスできることを検証しています。このコミット以前は、このようなコードが型チェックで問題を引き起こしていた可能性がありますが、修正後は正しくコンパイルされるようになります。

関連リンク

参考にした情報源リンク

  • Go言語の公式ドキュメント
  • Go言語のソースコード(特に src/pkg/go/types ディレクトリ)
  • Go言語のコミット履歴とコードレビューコメント (CL 7376055)
  • Go言語の構造体埋め込みに関する一般的な解説記事
  • Go言語の型アサーションに関する情報