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

[インデックス 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ファイルで構成されています。

  1. test/fixedbugs/bug437.dir/one.go
  2. test/fixedbugs/bug437.dir/two.go
  3. test/fixedbugs/bug437.go

これらのファイルは、それぞれ異なるパッケージに属し、以下のような関係性を持っています。

  • one.go (パッケージ one):

    • I1というインターフェースを定義しています。このインターフェースはf()という小文字で始まる(エクスポートされていない)メソッドを一つ持ちます。
    • S1という構造体を定義し、I1インターフェースのf()メソッドを実装しています。これによりS1I1インターフェースを満たします。
    • F1という関数を定義し、I1インターフェース型の引数を受け取ります。
  • two.go (パッケージ two):

    • oneパッケージをインポートしています。
    • S2という構造体を定義しています。このS2one.S1を埋め込みフィールドとして持ちます。Goの埋め込みのルールにより、S2one.S1のメソッド(この場合はf())を「昇格」させ、自身もf()メソッドを持つことになります。結果として、S2one.I1インターフェースを満たします。
  • bug437.go (パッケージ main):

    • oneパッケージとtwoパッケージの両方をインポートしています。
    • Fという関数を定義しています。この関数はone.I1インターフェース型の引数i1を受け取ります。
    • F関数内で型スイッチswitch v := i1.(type)を使用し、i1two.S2型である場合のケースを処理しています。このcase two.S2:のブロック内で、one.F1(v)を呼び出しています。ここでvtwo.S2型ですが、one.F1one.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.gotwo.gobug437.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 oneoneというパッケージを定義します。
    • type I1 interface { f() }f()というエクスポートされていないメソッドを持つインターフェースI1を定義します。このf()が小文字である点が重要です。
    • type S1 struct {}:空の構造体S1を定義します。
    • func (s S1) f() {}S1型がI1インターフェースのf()メソッドを実装します。これによりS1I1インターフェースを満たします。
    • func F1(i1 I1) {}I1インターフェース型の引数を受け取る関数F1を定義します。
  • two.go:

    • package twotwoというパッケージを定義します。
    • import "./one"oneパッケージをインポートします。
    • type S2 struct { one.S1 }one.S1を埋め込んだ構造体S2を定義します。Goの埋め込みの性質により、S2one.S1のメソッドセット(この場合はf())を継承し、自身もf()メソッドを持つことになります。したがって、S2one.I1インターフェースを満たします。
  • bug437.go:

    • package main:実行可能なメインパッケージを定義します。
    • import ( "./one"; "./two" )onetwoの両パッケージをインポートします。
    • 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:i1two.S2型である場合のケースです。
      • one.F1(v):ここでvtwo.S2型ですが、one.F1one.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言語の型システムに関する一般的な情報源