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

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

このコミットは、Go言語のテストスイートに新しいテストケースを追加するものです。具体的には、gccgoコンパイラが以前コンパイルに失敗した特定のコードパターンを捕捉するためのテストです。このテストは、異なるパッケージで定義された埋め込み型が、同じ非公開メソッド名を持つ場合に発生するコンパイルエラーを再現し、将来の回帰を防ぐことを目的としています。

コミット

commit 4182889a095f1c371e5c7d8d6162f75795a00ec0
Author: Ian Lance Taylor <iant@golang.org>
Date:   Wed Sep 18 16:30:38 2013 -0700

    test: add a test that gccgo failed to compile
    
    R=golang-dev, rsc
    CC=golang-dev
    https://golang.org/cl/13632057

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

https://github.com/golang/go/commit/4182889a095f1c371e5c7d8d6162f75795a00ec0

元コミット内容

test: add a test that gccgo failed to compile

R=golang-dev, rsc
CC=golang-dev
https://golang.org/cl/13632057

変更の背景

このコミットの背景には、gccgoコンパイラが特定のGoコードパターンを正しくコンパイルできないという問題がありました。具体的には、異なるパッケージに属する構造体が、同じ名前の非公開(unexported)メソッドを持ち、かつ一方が他方を埋め込んでいる場合に、gccgoがコンパイルエラーを引き起こすというバグが存在しました。

Go言語では、構造体への型の埋め込み(embedding)は、その埋め込まれた型のメソッドを「昇格(promote)」させ、外側の構造体のメソッドとして利用できるようにする機能です。この際、メソッド名が衝突した場合の解決ルールが存在しますが、非公開メソッドの場合、その解決がコンパイラによって異なる挙動を示すことがありました。

このバグは、Go言語の公式コンパイラ(gc)では問題なくコンパイルできるにもかかわらず、gccgoでは失敗するという、コンパイラ間の互換性の問題を示していました。このような問題は、Goプログラムの移植性や、異なるコンパイラ環境での安定性を損なうため、修正とテストによる回帰防止が不可欠でした。

このコミットは、その特定のコンパイル失敗ケースを再現するテストを追加することで、gccgoの将来のバージョンでこのバグが再発しないようにするためのものです。

前提知識の解説

Go言語のパッケージと可視性

Go言語では、コードは「パッケージ(package)」という単位で組織されます。パッケージは、関連する機能や型をまとめる論理的なグループです。

  • エクスポートされた識別子(Exported Identifiers): 識別子(変数名、関数名、型名、メソッド名など)の最初の文字が大文字である場合、その識別子はパッケージ外からアクセス可能です。これを「エクスポートされている」と言います。
  • 非公開(Unexported)識別子: 識別子の最初の文字が小文字である場合、その識別子は定義されたパッケージ内からのみアクセス可能です。これを「非公開」または「エクスポートされていない」と言います。

このコミットで問題となるのは、この「非公開」メソッドの扱いです。

Go言語の構造体の埋め込み(Embedding)

Go言語の構造体は、他の構造体やインターフェースを「埋め込む」ことができます。これは、継承とは異なり、埋め込まれた型のフィールドやメソッドが、外側の構造体のフィールドやメソッドであるかのように直接アクセスできるようになる機能です。

例:

package main

import "fmt"

type Inner struct {
    Value int
}

func (i Inner) Greet() {
    fmt.Println("Hello from Inner")
}

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

func main() {
    o := Outer{Inner: Inner{Value: 10}, Name: "World"}
    fmt.Println(o.Value) // Innerのフィールドに直接アクセス
    o.Greet()            // Innerのメソッドに直接アクセス
}

埋め込みによって、埋め込まれた型のメソッドは外側の構造体に「昇格」されます。もし外側の構造体自身が同じ名前のメソッドを持っている場合、外側の構造体のメソッドが優先されます。

gccgogcコンパイラ

Go言語には主に2つの主要なコンパイラ実装があります。

  1. gc (Go Compiler): これはGo言語の公式ツールチェインに含まれるデフォルトのコンパイラです。Go言語で書かれており、Goのリリースサイクルに合わせて開発されます。
  2. gccgo: これはGCC (GNU Compiler Collection) のフロントエンドとして実装されたGoコンパイラです。C++やJavaなど他の言語と同様に、GCCのインフラストラクチャを利用してGoコードをコンパイルします。gccgogcとは独立して開発されており、Go言語の仕様に準拠しつつも、実装の詳細やバグの特性が異なる場合があります。

このコミットは、gcでは問題ないがgccgoで問題が発生したケースを扱っています。これは、両コンパイラがGo言語の仕様をどのように解釈し、実装しているかの違いに起因する可能性があります。

技術的詳細

このコミットが追加するテストケースは、以下のシナリオを再現します。

  1. パッケージ p1:

    • S1という構造体を定義します。
    • S1に非公開メソッド f() を定義します。
    package p1
    
    type S1 struct{}
    
    func (s S1) f() {} // 非公開メソッド
    
  2. パッケージ p2:

    • p1パッケージをインポートします。
    • S2という構造体を定義し、その中にp1.S1を埋め込みます。
    • S2にも非公開メソッド f() を定義します。
    package p2
    
    import "./a" // 実際には "p1" のようなパスになる
    
    type S2 struct {
        p1.S1 // p1.S1を埋め込み
    }
    
    func (s S2) f() {} // 非公開メソッド
    

問題は、p2.S2p1.S1を埋め込んでいるため、p1.S1の非公開メソッドf()p2.S2に「昇格」される点にあります。しかし、p2.S2自身も同じ名前の非公開メソッドf()を持っています。

Go言語のメソッドセットのルールでは、埋め込みによって昇格されたメソッドと、外側の型で直接定義されたメソッドが衝突する場合、外側の型で直接定義されたメソッドが優先されます。しかし、非公開メソッドの場合、その可視性はパッケージ内に限定されます。

gccgoがこのケースでコンパイルに失敗したということは、gccgoが以下のいずれかの点でgcと異なる挙動を示したことを示唆しています。

  • 非公開メソッドの昇格と衝突解決の処理: gccgoが、異なるパッケージから昇格された非公開メソッドと、現在のパッケージで定義された非公開メソッドの間の名前解決を誤った可能性があります。特に、非公開メソッドはパッケージ外からはアクセスできないため、その衝突解決のロジックが複雑になることがあります。
  • シンボル解決またはリンケージの問題: コンパイル時に、p1.S1.fp2.S2.fという2つの異なる非公開メソッドが、gccgoの内部で正しく区別されなかったか、シンボルテーブルの管理に問題があった可能性があります。これにより、名前の衝突が解決できず、コンパイルエラーに至ったと考えられます。

このテストは、compiledirディレクティブを使用しており、これは複数のファイル(この場合はa.gob.go)を別々のパッケージとしてコンパイルし、それらが相互にインポートし合うシナリオをテストするためのものです。これにより、実際のマルチパッケージ環境でのコンパイラの挙動を検証できます。

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

このコミットでは、以下の3つのファイルが追加されています。

  1. test/fixedbugs/bug478.dir/a.go
  2. test/fixedbugs/bug478.dir/b.go
  3. test/fixedbugs/bug478.go

test/fixedbugs/bug478.dir/a.go

// Copyright 2013 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 p1

type S1 struct{}

func (s S1) f() {}

このファイルはp1パッケージを定義し、S1という空の構造体と、その構造体に紐付けられた非公開メソッドf()を定義しています。

test/fixedbugs/bug478.dir/b.go

// Copyright 2013 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 p2

import "./a"

type S2 struct {
	p1.S1
}

func (s S2) f() {}

このファイルはp2パッケージを定義し、p1パッケージ(./aとしてインポート)からS1型をインポートしています。そして、S2という構造体を定義し、その中にp1.S1を埋め込んでいます。さらに、S2にも非公開メソッドf()を定義しています。

test/fixedbugs/bug478.go

// compiledir

// Copyright 2013 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.

// Using the same unexported name for a method as a method on an
// imported embedded type caused a gccgo compilation failure.

package ignored

このファイルは、テストのメインエントリポイントです。 // compiledirディレクティブは、このテストがbug478.dirディレクトリ内のファイルを個別のパッケージとしてコンパイルすることをGoテストツールに指示します。 コメントには、このテストが「インポートされた埋め込み型上のメソッドと同じ非公開名を使用することが、gccgoのコンパイル失敗を引き起こした」という問題に対処するためのものであることが明記されています。

コアとなるコードの解説

このコミットのコアは、a.gob.goで定義されるGoコードの構造にあります。

  • a.go (p1パッケージ):

    • type S1 struct{}: シンプルな構造体S1を定義します。
    • func (s S1) f() {}: S1型にf()というメソッドを定義します。このメソッド名は小文字で始まるため、p1パッケージ内でのみ可視な非公開メソッドです。
  • b.go (p2パッケージ):

    • import "./a": p1パッケージをインポートします。Goのテストフレームワークでは、bug478.dirのようなディレクトリ構造の場合、./ap1パッケージを指します。
    • type S2 struct { p1.S1 }: S2構造体を定義し、その中にp1.S1を埋め込みます。これにより、p1.S1のフィールドやメソッドがS2のフィールドやメソッドとして昇格されます。
    • func (s S2) f() {}: S2型にもf()というメソッドを定義します。これも小文字で始まるため、p2パッケージ内でのみ可視な非公開メソッドです。

この設定が問題を引き起こすのは、p2.S2のメソッドセットに、以下の2つのf()メソッドが存在することになるためです。

  1. p2.S2自身で定義された非公開メソッドf()
  2. 埋め込まれたp1.S1から昇格された非公開メソッドf()

Go言語の仕様では、埋め込みによって昇格されたメソッドと、外側の型で直接定義されたメソッドが衝突する場合、外側の型で直接定義されたメソッドが優先されます。しかし、非公開メソッドの場合、その可視性はパッケージに限定されるため、p1.S1f()p2パッケージからは直接アクセスできません。

gccgoがこのケースでコンパイルに失敗したということは、gccgoがこの非公開メソッドの昇格と、異なるパッケージ間での名前解決のルールを正しく適用できなかったことを示しています。例えば、gccgop1.S1.fp2.S2.fを区別できずにシンボル衝突と見なしたり、あるいは非公開メソッドの可視性ルールと埋め込みによる昇格の相互作用を誤って処理した可能性があります。

このテストの追加により、gccgoの開発者はこの特定のバグを特定し、修正することができました。そして、このテストは将来のgccgoのバージョンで同様の回帰が発生しないことを保証するための重要な安全網となります。

関連リンク

参考にした情報源リンク

  • Go言語の公式ドキュメント
  • Go言語のソースコード(特にtest/fixedbugsディレクトリ内の他のテストケース)
  • Go言語のIssueトラッカーやメーリングリスト(golang.org/cl/13632057に関連する議論があれば)
  • GCCGoのドキュメントや関連するバグ報告
  • Go言語のメソッドセットと埋め込みに関する一般的な解説記事
  • Go言語のコンパイラ実装に関する技術ブログや論文(もしあれば) I have generated the commit explanation and outputted it to standard output as requested.