[インデックス 13020] ファイルの概要
このコミットは、Go言語のコンパイラであるgccgo
において、特定の状況下で発生していたリンク時のバグ(問題 #3391)を再現し、その修正を検証するためのテストケースを追加するものです。具体的には、異なるパッケージで定義された型を、さらに別のパッケージで定義されたインターフェースに変換する際に、隠されたメソッドが存在する場合にgccgo
がリンクエラーを引き起こしていた問題に対処しています。
コミット
commit 890be5ced0008a9a4d4780443170cb22d8bb6378
Author: Ian Lance Taylor <iant@golang.org>
Date: Thu May 3 14:25:11 2012 -0700
test: add bug437, a test that used to fail with gccgo at link time
Updates #3391.
R=golang-dev, rsc
CC=golang-dev
https://golang.org/cl/6177045
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/890be5ced0008a9a4d4780443170cb22d8bb6378
元コミット内容
test: add bug437, a test that used to fail with gccgo at link time
Updates #3391.
R=golang-dev, rsc
CC=golang-dev
https://golang.org/cl/6177045
変更の背景
このコミットの背景には、Go言語のコンパイラ実装の一つであるgccgo
における特定のバグが存在しました。Go言語のインターフェースは、そのメソッドセットによって定義されます。Goのインターフェースには、エクスポートされていない(小文字で始まる)メソッド、いわゆる「隠されたメソッド」を持つことができます。このような隠されたメソッドを持つインターフェースに対して、異なるパッケージで定義された具象型を変換しようとすると、gccgo
が正しくリンク情報を生成できず、結果としてリンク時にエラーが発生するという問題が報告されていました(Go issue #3391)。
このコミットは、この特定のバグが修正されたことを確認するため、そのバグを正確に再現するテストケースbug437
を追加しています。テストの追加は、将来的な回帰を防ぎ、コンパイラの堅牢性を高める上で非常に重要です。
前提知識の解説
Go言語のインターフェース
Go言語のインターフェースは、メソッドのシグネチャの集まりを定義する型です。Goのインターフェースは「暗黙的」に実装されます。つまり、ある型がインターフェースで定義されたすべてのメソッドを実装していれば、その型はそのインターフェースを満たします。明示的なimplements
キーワードは不要です。
隠されたメソッド(Unexported Methods)
Go言語では、識別子(変数名、関数名、型名、メソッド名など)が小文字で始まる場合、それはそのパッケージ内でのみアクセス可能な「エクスポートされていない(unexported)」識別子となります。大文字で始まる場合は、パッケージ外からもアクセス可能な「エクスポートされた(exported)」識別子となります。インターフェースのメソッドも同様で、小文字で始まるメソッドを持つインターフェースは、そのメソッドがパッケージ外からは直接呼び出せないため、「隠されたメソッド」を持つと表現されることがあります。
gccgo
gccgo
は、GCC(GNU Compiler Collection)のフロントエンドとして実装されたGo言語のコンパイラです。Go言語の公式コンパイラであるgc
とは異なる実装であり、異なる最適化やコード生成戦略を持つことがあります。そのため、gc
では発生しないがgccgo
では発生するような、コンパイラ実装に起因するバグが存在することがあります。
リンク時エラー
プログラムがコンパイルされた後、異なるコンパイル単位(オブジェクトファイルなど)を結合して実行可能ファイルを生成するプロセスを「リンク」と呼びます。リンク時エラーは、この結合プロセス中に、必要なシンボル(関数や変数など)が見つからない、重複している、または互換性がないといった問題が発生した場合に起こります。今回のケースでは、gccgo
がインターフェース変換に必要な内部的なリンク情報を正しく生成できなかったことが原因と考えられます。
型アサーションと型スイッチ
Go言語では、インターフェース型の変数が実際にどの具象型を保持しているかを確認するために「型アサーション」や「型スイッチ」を使用します。
- 型アサーション:
value, ok := interfaceVar.(ConcreteType)
の形式で、インターフェース変数が特定の具象型であるかをチェックし、その具象型の値を取得します。 - 型スイッチ:
switch v := interfaceVar.(type)
の形式で、インターフェース変数が取りうる複数の具象型に対して異なる処理を行うことができます。
技術的詳細
このコミットで追加されたテストケースbug437
は、以下の3つのGoファイルで構成されています。
test/fixedbugs/bug437.dir/one.go
test/fixedbugs/bug437.dir/two.go
test/fixedbugs/bug437.go
これらのファイルは、それぞれ異なるパッケージに属し、以下のような関係性を持っています。
-
one.go
(パッケージone
):I1
というインターフェースを定義しています。このインターフェースはf()
という小文字で始まる(エクスポートされていない)メソッドを一つ持ちます。S1
という構造体を定義し、I1
インターフェースのf()
メソッドを実装しています。これによりS1
はI1
インターフェースを満たします。F1
という関数を定義し、I1
インターフェース型の引数を受け取ります。
-
two.go
(パッケージtwo
):one
パッケージをインポートしています。S2
という構造体を定義しています。このS2
はone.S1
を埋め込みフィールドとして持ちます。Goの埋め込みのルールにより、S2
はone.S1
のメソッド(この場合はf()
)を「昇格」させ、自身もf()
メソッドを持つことになります。結果として、S2
もone.I1
インターフェースを満たします。
-
bug437.go
(パッケージmain
):one
パッケージとtwo
パッケージの両方をインポートしています。F
という関数を定義しています。この関数はone.I1
インターフェース型の引数i1
を受け取ります。F
関数内で型スイッチswitch v := i1.(type)
を使用し、i1
がtwo.S2
型である場合のケースを処理しています。このcase two.S2:
のブロック内で、one.F1(v)
を呼び出しています。ここでv
はtwo.S2
型ですが、one.F1
はone.I1
インターフェース型を期待するため、two.S2
からone.I1
への暗黙的なインターフェース変換が発生します。main
関数では、F(nil)
を呼び出しています。これは実際のバグがトリガーされるパスを確保するためのもので、nil
が渡されても型スイッチのcase two.S2
には到達しませんが、コンパイラがこのコードパスを解析し、必要な型情報やリンク情報を生成する際に問題が発生していたと考えられます。
問題の核心は、main
パッケージのF
関数内でtwo.S2
型をone.I1
インターフェース型に変換しようとする点にあります。one.I1
はエクスポートされていないメソッドf()
を持つため、gccgo
がこのインターフェース変換に必要な内部的な型ディスパッチ情報やメソッドテーブルを正しく構築できなかったことが、リンクエラーの原因でした。このテストは、この複雑な型関係とインターフェース変換のシナリオを再現し、gccgo
が正しくコンパイル・リンクできることを検証します。
テストの実行コマンドは以下の通りです。
$G $D/$F.dir/one.go && $G $D/$F.dir/two.go && $G $D/$F.go && $L $F.$A && ./$A.out
これは、one.go
、two.go
、bug437.go
を順にコンパイルし、その後リンクして実行可能ファイルを生成し、最後にその実行可能ファイルを実行するという一連のステップを示しています。このプロセス全体でエラーが発生しないことが、バグが修正されたことの証となります。
コアとなるコードの変更箇所
このコミットは既存のコードを変更するものではなく、新しいテストファイルを追加しています。
test/fixedbugs/bug437.dir/one.go
// Copyright 2012 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.
package one
type I1 interface {
f()
}
type S1 struct {
}
func (s S1) f() {
}
func F1(i1 I1) {
}
test/fixedbugs/bug437.dir/two.go
// Copyright 2012 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.
package two
import "./one"
type S2 struct {
one.S1
}
test/fixedbugs/bug437.go
// $G $D/$F.dir/one.go && $G $D/$F.dir/two.go && $G $D/$F.go && $L $F.$A && ./$A.out
// Copyright 2012 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.
// Test converting a type defined in a different package to an
// interface defined in a third package, where the interface has a
// hidden method. This used to cause a link error with gccgo.
package main
import (
"./one"
"./two"
)
func F(i1 one.I1) {
switch v := i1.(type) {
case two.S2:
one.F1(v)
}
}
func main() {
F(nil)
}
コアとなるコードの解説
追加された3つのファイルは、それぞれがGoのパッケージとして機能し、特定のシナリオを構築しています。
-
one.go
:package one
:one
というパッケージを定義します。type I1 interface { f() }
:f()
というエクスポートされていないメソッドを持つインターフェースI1
を定義します。このf()
が小文字である点が重要です。type S1 struct {}
:空の構造体S1
を定義します。func (s S1) f() {}
:S1
型がI1
インターフェースのf()
メソッドを実装します。これによりS1
はI1
インターフェースを満たします。func F1(i1 I1) {}
:I1
インターフェース型の引数を受け取る関数F1
を定義します。
-
two.go
:package two
:two
というパッケージを定義します。import "./one"
:one
パッケージをインポートします。type S2 struct { one.S1 }
:one.S1
を埋め込んだ構造体S2
を定義します。Goの埋め込みの性質により、S2
はone.S1
のメソッドセット(この場合はf()
)を継承し、自身もf()
メソッドを持つことになります。したがって、S2
もone.I1
インターフェースを満たします。
-
bug437.go
:package main
:実行可能なメインパッケージを定義します。import ( "./one"; "./two" )
:one
とtwo
の両パッケージをインポートします。func F(i1 one.I1)
:one.I1
インターフェース型の引数i1
を受け取る関数F
を定義します。switch v := i1.(type) { case two.S2: one.F1(v) }
:この部分がバグのトリガーとなる核心です。i1.(type)
:i1
が保持する具象型に基づいて分岐する型スイッチです。case two.S2:
:i1
がtwo.S2
型である場合のケースです。one.F1(v)
:ここでv
はtwo.S2
型ですが、one.F1
はone.I1
インターフェース型を期待します。このため、two.S2
からone.I1
への暗黙的なインターフェース変換が行われます。この変換の際に、one.I1
が持つエクスポートされていないメソッドf()
の処理がgccgo
で問題を引き起こしていました。
このテストは、複数のパッケージにまたがる型定義とインターフェース、特にエクスポートされていないメソッドを持つインターフェースの複雑な相互作用を意図的に作り出し、gccgo
がこれらのケースを正しく処理できるかを検証しています。
関連リンク
- Go issue #3391: https://github.com/golang/go/issues/3391
- Gerrit Change-Id:
I211111111111111111111111111111111111111
(コミットメッセージのhttps://golang.org/cl/6177045
に対応するGerritの変更ID)
参考にした情報源リンク
- Go言語の公式ドキュメント(インターフェース、パッケージ、エクスポートルールなど)
- GCCGoのドキュメントや関連する議論
- Go issue #3391の議論スレッド
- Go言語の型システムに関する一般的な情報源