[インデックス 19281] ファイルの概要
このコミットは、Goコンパイラの一つであるgccgoが、構造体の埋め込みフィールドのメソッド解決において誤った動作をしていたバグを修正するためのテストケースを追加するものです。具体的には、test/fixedbugs/bug485.goという新しいテストファイルが追加され、このバグが再現されることを確認します。このテストは、Goの言語仕様に準拠した正しいメソッド解決が行われることを保証するためのものです。
コミット
commit d3764dd43511e6e9ca9fbca42506e097132a2f9a
Author: Ian Lance Taylor <iant@golang.org>
Date: Tue May 6 09:01:38 2014 -0400
test: add test that gccgo compiled incorrectly
LGTM=minux.ma
R=golang-codereviews, minux.ma
CC=golang-codereviews
https://golang.org/cl/94100045
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/d3764dd43511e6e9ca9fbca42506e097132a2f9a
元コミット内容
test: add test that gccgo compiled incorrectly
LGTM=minux.ma
R=golang-codereviews, minux.ma
CC=golang-codereviews
https://golang.org/cl/94100045
変更の背景
このコミットは、Go言語のコンパイラ実装の一つであるgccgoが、特定の状況下でGoの構造体埋め込み(embedding)におけるメソッド解決のルールを誤って解釈し、不正なコードを生成していた問題に対応するために行われました。
Go言語では、構造体内に型を埋め込むことで、その埋め込まれた型のフィールドやメソッドを、外側の構造体のフィールドやメソッドであるかのように直接アクセスできる機能を提供します。これは、継承に似た機能を提供しますが、Goの設計思想である「コンポジション(合成)による再利用」を促進するものです。
問題は、同じ型のフィールドが異なるレベルで埋め込まれている場合に発生しました。具体的には、ある型が直接構造体に埋め込まれており、かつ、その構造体がさらに別の構造体を埋め込んでおり、その埋め込まれた構造体も同じ型を埋め込んでいる、というようなシナリオです。Goの言語仕様では、このような場合にどのメソッドが優先されるかについて明確なルールが定められています。しかし、gccgoはこのルールを正しく適用できず、誤ったメソッドを呼び出してしまうバグを抱えていました。
このバグは、Goプログラムの実行時に予期せぬ動作やパニックを引き起こす可能性があり、Go言語のセマンティクスの一貫性を損なうものでした。そのため、この問題を特定し、修正を促すための再現テストケースがGoプロジェクトに追加されることになりました。
前提知識の解説
Goの構造体埋め込み (Struct Embedding)
Go言語の構造体埋め込みは、他の型のフィールドやメソッドを、あたかも自身のフィールドやメソッドであるかのように利用できる強力な機能です。これは、Goが継承の代わりに採用している「コンポジション」の主要なメカニズムの一つです。
例えば、以下のような構造体があるとします。
type Engine struct {
horsepower int
}
func (e Engine) Start() string {
return "Engine started"
}
type Car struct {
Engine // Engine型を埋め込み
brand string
}
func main() {
myCar := Car{
Engine: Engine{horsepower: 200},
brand: "Toyota",
}
fmt.Println(myCar.Start()) // Car型からEngineのStartメソッドを直接呼び出し
}
この例では、Car構造体はEngine型を埋め込んでいます。これにより、CarのインスタンスからEngineのStartメソッドを直接呼び出すことができます。
メソッドセットとメソッド解決 (Method Sets and Method Resolution)
Goの各型は「メソッドセット」と呼ばれる、その型に関連付けられたメソッドの集合を持っています。構造体埋め込みの場合、外側の構造体のメソッドセットには、埋め込まれた型のメソッドセットも含まれます。
メソッド解決のルールは以下のようになります。
- 直接定義されたメソッドの優先: ある型に直接定義されたメソッドは、埋め込まれた型から昇格(promote)された同名のメソッドよりも優先されます。
- 最も浅いレベルの優先: 複数の埋め込みレベルで同じ名前のメソッドが存在する場合、最も「浅い」(外側の構造体に近い)レベルで昇格されたメソッドが優先されます。
- 曖昧さの排除: 同じレベルで複数の同名メソッドが昇格される場合、それはコンパイルエラー(曖昧なセレクタ)となります。ただし、フィールド名とメソッド名が衝突する場合は、フィールドが優先されます。
このコミットで問題となったのは、特に「最も浅いレベルの優先」のルールがgccgoで正しく適用されていなかった点です。
技術的詳細
gccgoが抱えていた問題は、Goのメソッド解決ルール、特に「最も浅いレベルの優先」に関するものでした。コミットメッセージには「Gccgo chose the wrong embedded method when the same type appeared at different levels and the correct choice was not the first appearance of the type in a depth-first search.」とあります。
これを具体的に説明すると、以下のような状況で問題が発生していました。
- 多段階の埋め込み: 構造体
Bが構造体Aを埋め込み、さらにAが型Tを埋め込んでいる。 - 同じ型の直接埋め込み: 構造体
Bもまた、型Tを直接埋め込んでいる。 - メソッドの存在: 型
TにはMというメソッドが存在する。
この場合、BのインスタンスからMメソッドを呼び出す際、Goの言語仕様ではBに直接埋め込まれたTのMメソッドが優先されるべきです。なぜなら、これはBにとって最も「浅い」レベルにあるTだからです。しかし、gccgoは、おそらく内部的なシンボル解決のアルゴリズム(例えば深さ優先探索)の都合上、Bが埋め込んでいるAを介して見つかるTのMメソッドを誤って選択してしまっていたと考えられます。
つまり、Bのインスタンスからb.M()を呼び出したときに、gccgoはB.A.T.M()を呼び出してしまい、本来呼び出すべきB.T.M()を呼び出していなかった、ということです。この誤った解決は、プログラムの論理的な振る舞いを破壊し、予期せぬ結果をもたらす可能性がありました。
追加されたテストケースbug485.goは、まさにこのシナリオを再現し、Goの言語仕様に準拠した正しいメソッド解決が行われることを検証します。
コアとなるコードの変更箇所
追加されたテストファイル test/fixedbugs/bug485.go の内容です。
// run
// Copyright 2014 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// Gccgo chose the wrong embedded method when the same type appeared
// at different levels and the correct choice was not the first
// appearance of the type in a depth-first search.
package main
type embedded string
func (s embedded) val() string {
return string(s)
}
type A struct {
embedded
}
type B struct {
A
embedded
}
func main() {
b := &B{
A: A{
embedded: "a",
},
embedded: "b",
}
s := b.val()
if s != "b" {
panic(s)
}
}
コアとなるコードの解説
このテストコードは、gccgoのバグを再現し、Goの正しいメソッド解決ルールを検証するために設計されています。
-
type embedded stringとfunc (s embedded) val() string:embeddedという新しい型がstringのエイリアスとして定義され、この型にval()というメソッドが追加されています。このメソッドは、embedded型の値をstringとして返します。これは、埋め込みによって昇格されるメソッドのターゲットとなります。 -
type A struct { embedded }: 構造体Aはembedded型を埋め込んでいます。これにより、Aのインスタンスはembedded型のフィールドとval()メソッドを持つかのように振る舞います。 -
type B struct { A; embedded }: 構造体Bは、A型とembedded型を両方埋め込んでいます。Bは直接embedded型を埋め込んでいます。BはA型を埋め込んでおり、そのAもまたembedded型を埋め込んでいます。
この構造が、gccgoのバグを誘発するキーポイントです。
Bのインスタンスからval()メソッドを呼び出す際、Goの言語仕様ではBに直接埋め込まれたembedded型のval()メソッドが優先されるべきです。 -
func main()内のロジック:b := &B{ ... }でB型のインスタンスが作成されます。Aに埋め込まれたembeddedフィールドには値"a"が設定されます。Bに直接埋め込まれたembeddedフィールドには値"b"が設定されます。s := b.val(): ここがテストの核心です。bのval()メソッドが呼び出されます。Goの正しいメソッド解決ルールに従えば、Bに直接埋め込まれたembeddedフィールド(値は"b")のval()メソッドが呼び出されるべきです。if s != "b" { panic(s) }: 取得したsの値が"b"でなければパニックを起こします。これは、gccgoが誤ってA経由で埋め込まれたembeddedフィールド(値は"a")のval()メソッドを呼び出してしまった場合に、テストが失敗することを示します。
このテストは、Goのコンパイラが構造体埋め込みにおけるメソッド解決の優先順位(特に、より浅いレベルの埋め込みが優先されること)を正しく実装していることを確認するためのものです。
関連リンク
- Go CL 94100045: https://golang.org/cl/94100045
参考にした情報源リンク
- Go言語仕様 (The Go Programming Language Specification) - Struct types: https://go.dev/ref/spec#Struct_types
- Go言語仕様 (The Go Programming Language Specification) - Method sets: https://go.dev/ref/spec#Method_sets
- Go言語仕様 (The Go Programming Language Specification) - Selectors: https://go.dev/ref/spec#Selectors