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

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

このコミットは、Goコンパイラ(gc)とgoコマンドの挙動を改善し、純粋なGoパッケージにおける前方宣言(forward declaration)をコンパイル時にエラーとして検出するように変更します。これにより、リンク時ではなく、より早い段階で問題を特定できるようになり、開発体験が向上します。特にinit関数に対する前方宣言は常にエラーとなり、その他の関数については、goコマンドが純粋なGoパッケージに対して-=フラグをgcに渡すことで、この新しいエラー検出が有効になります。

コミット

commit 04098d88fa1b4d41557ac6824a528d092d562936
Author: Russ Cox <rsc@golang.org>
Date:   Sat Dec 22 16:46:46 2012 -0500

    cmd/gc: make forward declaration in pure Go package an error
    
    An error during the compilation can be more precise
    than an error at link time.
    
    For 'func init', the error happens always: you can't forward
    declare an init func because the name gets mangled.
    
    For other funcs, the error happens only with the special
    (and never used by hand) -= flag, which tells 6g the
    package is pure go.
    
    The go command now passes -= for pure Go packages.
    
    Fixes #3705.
    
    R=ken2
    CC=golang-dev
    https://golang.org/cl/6996054

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

https://github.com/golang/go/commit/04098d88fa1b4d41557ac6824a528d092d562936

元コミット内容

cmd/gc: make forward declaration in pure Go package an error

An error during the compilation can be more precise
than an error at link time.

For 'func init', the error happens always: you can't forward
declare an init func because the name gets mangled.

For other funcs, the error happens only with the special
(and never used by hand) -= flag, which tells 6g the
package is pure go.

The go command now passes -= for pure Go packages.

Fixes #3705.

R=ken2
CC=golang-dev
https://golang.org/cl/6996054

変更の背景

この変更の背景には、Go言語における前方宣言の扱いと、それによって発生する可能性のある問題がありました。Go言語では、通常、関数や変数は使用する前に定義されている必要があります。しかし、特定の状況下(特にC言語との連携やアセンブリコードを含むパッケージなど)では、前方宣言のような形式でシンボルが参照されることがありました。

以前のGoコンパイラでは、純粋なGoパッケージ(Cやアセンブリコードを含まないパッケージ)において、関数本体が提供されていない前方宣言のような記述があった場合、コンパイル時にはエラーにならず、リンク時に未定義シンボルエラーとして検出されることがありました。この「リンク時エラー」は、コンパイル時エラーに比べて問題の特定とデバッグが困難になる傾向があります。

特にinit関数に関しては、Goの仕様上、特別な初期化処理のために使用され、その名前が内部的にマングル(変更)されるため、前方宣言のような形で定義することはできませんでした。しかし、これがコンパイル時に明確なエラーとして扱われていなかったため、開発者が意図しない挙動に遭遇する可能性がありました。

このコミットは、これらの問題を解決し、純粋なGoパッケージにおける前方宣言をコンパイル時に早期に検出することで、より正確で分かりやすいエラーメッセージを提供し、開発者のデバッグ体験を向上させることを目的としています。具体的には、Issue 3705で報告された問題に対応しています。

前提知識の解説

このコミットを理解するためには、以下のGo言語およびGoツールチェインに関する前提知識が必要です。

  • 前方宣言 (Forward Declaration): プログラミング言語において、関数や変数を使用する前にその存在と型を宣言すること。Go言語では、通常、関数や変数は使用する前に定義されている必要がありますが、C言語との連携など、特定の文脈では前方宣言に似た状況が発生することがあります。例えば、関数シグネチャだけが宣言され、本体が別の場所(または別の言語のコード)で定義されるようなケースです。

  • 純粋なGoパッケージ (Pure Go Package): C言語のコード(.cファイル)、アセンブリコード(.sファイル)、SWIGで生成されたファイルなど、Go以外のソースコードを含まないGoパッケージを指します。Goのビルドシステムは、これらの異なる種類のソースファイルを区別して処理します。

  • func init(): Go言語の特別な関数で、パッケージが初期化される際に自動的に実行されます。各パッケージは複数のinit関数を持つことができ、これらはパッケージ内のすべての変数が初期化された後に、宣言順に実行されます。init関数は引数を取らず、戻り値もありません。また、明示的に呼び出すことはできません。その性質上、init関数は前方宣言されるべきではありません。

  • gc (Go Compiler): Go言語の公式コンパイラの一つで、Goソースコードを機械語に変換します。このコミットの時点では、6g(amd64アーキテクチャ向け)のようなツールチェインの一部として機能していました。gcは、Goのソースファイルを解析し、中間表現を生成し、最終的に実行可能なバイナリを生成する役割を担います。

  • go コマンド: Go言語のビルド、テスト、実行、パッケージ管理などを行うための主要なコマンドラインツールです。go buildgo runなどのサブコマンドを通じて、Goプロジェクトのライフサイクルを管理します。goコマンドは、内部的にgcのようなコンパイラツールを呼び出します。

  • コンパイル時エラー vs. リンク時エラー:

    • コンパイル時エラー: ソースコードがコンパイラによって機械語に変換される過程で検出されるエラーです。構文エラー、型エラー、未定義の変数参照などがこれに該当します。コンパイル時にエラーが検出されると、実行可能なバイナリは生成されません。
    • リンク時エラー: コンパイルされたオブジェクトファイルやライブラリが結合されて最終的な実行可能ファイルが作成される「リンク」の段階で検出されるエラーです。最も一般的なのは、参照されている関数や変数の定義が見つからない「未定義シンボル」エラーです。リンク時エラーは、コンパイルが成功した後で発生するため、問題の根本原因を特定するのが難しい場合があります。

このコミットは、リンク時エラーとして現れていた問題をコンパイル時エラーに昇格させることで、開発者がより早く、より正確に問題を把握できるようにすることを目指しています。

技術的詳細

このコミットの技術的な核心は、Goコンパイラ(gc)とgoコマンドが連携して、純粋なGoパッケージにおける前方宣言をコンパイル時に検出するメカニズムを導入した点にあります。

  1. pure_go フラグの導入 (src/cmd/gc/go.h, src/cmd/gc/lex.c): gcコンパイラ内部に、現在コンパイルしているパッケージが純粋なGoパッケージであるかどうかを示すpure_goという新しいフラグが導入されました。

    • src/cmd/gc/go.h: pure_goというint型のグローバル変数が宣言されています。
    • src/cmd/gc/lex.c: main関数内で、コマンドライン引数として渡されるdebug['=']フラグ(-=)の値に基づいてpure_go変数が設定されます。debug['=']が設定されていればpure_go1となり、そうでなければ0となります。このdebug['=']は、Goコンパイラに「このパッケージは完全にGoで書かれており、Cやアセンブリコードを含まない」ことを伝えるための特別なフラグです。
  2. 前方宣言の検出ロジックの追加 (src/cmd/gc/pgen.c): gcコンパイラのコード生成フェーズ(pgen.c)において、関数本体(fn->nbody)がnil(つまり、関数シグネチャのみが存在し、本体がない)である場合に、エラーを発生させるロジックが追加されました。

    • 以前は、関数本体がない場合は単にreturnしていました。
    • 変更後、fn->nbody == nilの場合に以下の条件をチェックします。
      • pure_goフラグがtrueである場合(純粋なGoパッケージの場合)。
      • または、関数名がinit·で始まる場合(init関数である場合)。
    • これらの条件のいずれかが満たされる場合、yyerror("missing function body", fn);を呼び出してコンパイルエラーを発生させます。init関数は名前がマングルされるため、init·というプレフィックスで識別されます。
  3. goコマンドによる-=フラグの伝達 (src/cmd/go/build.go): goコマンドのビルドロジック(src/cmd/go/build.go)が変更され、gcコンパイラを呼び出す際に、適切な条件で-=フラグを渡すようになりました。

    • gcメソッド内で、コンパイル対象のパッケージ(p)がCgoファイル、Cファイル、アセンブリファイル、Sysoファイル、SWIGファイルなど、Go以外のソースファイルを含まない場合(extFiles == 0)に、gcargs"-="を追加します。
    • ただし、一部の標準パッケージ(os, runtime/pprof, sync, time)は、runtimeパッケージによって提供されるバックグラウンドの機能のために前方宣言のような形式を持つことがあるため、これらのパッケージは例外として扱われ、extFilesが強制的に1以上と見なされ、-=フラグが渡されないようになっています。これは、これらのパッケージがGo以外のコードに依存しているかのように振る舞うためです。

この一連の変更により、goコマンドが純粋なGoパッケージをコンパイルする際にgc-=フラグを渡し、gcはそのフラグとinit関数の特殊性を利用して、関数本体のない前方宣言をコンパイル時にエラーとして検出できるようになりました。これにより、リンク時まで待つことなく、開発の早い段階で問題を特定し、修正することが可能になります。

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

このコミットで変更された主要なファイルは以下の通りです。

  • doc/progs/error.go: Goのドキュメントに含まれるサンプルコードで、エラー処理の例を示しています。このコミットでは、Open関数の定義にpanic(1)が追加され、// OMIT// STOP OMITコメントで囲まれています。これは、ドキュメント生成ツールがこの部分を省略して表示するためのマーカーであり、このコミットの主要な機能変更とは直接関係ありませんが、テストやドキュメントの整合性を保つための変更です。

  • src/cmd/gc/go.h: Goコンパイラgcのヘッダーファイルです。

    • EXTERN int pure_go; という行が追加され、pure_goというグローバル変数が宣言されました。これは、現在コンパイル中のパッケージが純粋なGoパッケージであるかどうかを示すフラグです。
  • src/cmd/gc/lex.c: Goコンパイラgcの字句解析器およびメインエントリポイントを含むファイルです。

    • main関数内で、コマンドライン引数debug['=']-=フラグ)の値をpure_go変数に割り当てる行が追加されました。これにより、gcコンパイラはgoコマンドから渡される-=フラグを認識し、pure_goの状態を設定できるようになります。
  • src/cmd/gc/pgen.c: Goコンパイラgcのコード生成フェーズに関連するファイルです。

    • compile関数内で、関数ノードfnの本体(fn->nbody)がnilである場合の処理が変更されました。
    • 以前は単にreturnしていましたが、変更後はpure_goフラグがtrueであるか、または関数名がinit·で始まる場合にyyerror("missing function body", fn);を呼び出してコンパイルエラーを発生させるようになりました。
  • src/cmd/go/build.go: goコマンドのビルドロジックを定義するファイルです。

    • gcToolchain構造体のgcメソッド内で、gcコンパイラに渡す引数(gcargs)に"-="フラグを追加するロジックが追加されました。
    • 具体的には、パッケージがCgoファイル、Cファイル、アセンブリファイルなど、Go以外のソースファイルを含まない場合(extFiles == 0)に"-="フラグが追加されます。
    • ただし、os, runtime/pprof, sync, timeといった特定の標準パッケージは例外として扱われ、これらのパッケージではextFilesが強制的に1以上と見なされ、"-="フラグは渡されません。これは、これらのパッケージが内部的にGo以外のコードに依存しているかのように振る舞うためです。

これらの変更が連携することで、純粋なGoパッケージにおける前方宣言がコンパイル時に検出され、より明確なエラーメッセージが提供されるようになります。

コアとなるコードの解説

以下に、主要なコード変更箇所の詳細な解説を示します。

src/cmd/gc/go.h

--- a/src/cmd/gc/go.h
+++ b/src/cmd/gc/go.h
@@ -937,6 +937,7 @@ EXTERN	int	funcdepth;
 EXTERN	int	typecheckok;
 EXTERN	int	compiling_runtime;
 EXTERN	int	compiling_wrappers;
+EXTERN	int	pure_go;
 
 EXTERN	int	nointerface;
 EXTERN	int	fieldtrack_enabled;

この変更は、Goコンパイラgcのグローバル変数としてpure_goを宣言しています。EXTERNキーワードは、この変数が他のファイルで定義されていることを示唆しています。この変数は、現在コンパイル中のパッケージが純粋なGoコードのみで構成されているかどうかを示すフラグとして使用されます。

src/cmd/gc/lex.c

--- a/src/cmd/gc/lex.c
+++ b/src/cmd/gc/lex.c
@@ -293,8 +293,9 @@ main(int argc, char *argv[])
 	if(argc < 1)
 		usage();
 
-	// special flag to detect compilation of package runtime
-	compiling_runtime = debug['+'];
+	// special flags used during build.
+	compiling_runtime = debug['+']; // detect compilation of package runtime
+	pure_go = debug['=']; // package is completely go (no C or assembly)
 
 	pathname = mal(1000);
 	if(getwd(pathname, 999) == 0)

main関数内で、debug配列(コンパイラのデバッグフラグを格納)から=キーの値を取得し、それを新しく導入されたpure_go変数に割り当てています。debug['=']は、gcコンパイラに-=というコマンドラインフラグが渡された場合にtrue(または非ゼロ)になります。これにより、gcコンパイラは、goコマンドが「このパッケージは純粋なGoである」と判断した場合に、その情報を内部的に保持できるようになります。

src/cmd/gc/pgen.c

--- a/src/cmd/gc/pgen.c
+++ b/src/cmd/gc/pgen.c
@@ -29,16 +29,19 @@ compile(Node *fn)
 		throwreturn = sysfunc("throwreturn");
 	}
 
-\tif(fn->nbody == nil)
-\t\treturn;
+\tlno = setlineno(fn);\n+\n+\tif(fn->nbody == nil) {\n+\t\tif(pure_go || memcmp(fn->nname->sym->name, "init·", 6) == 0)\n+\t\t\tyyerror("missing function body", fn);\n+\t\tgoto ret;\n+\t}\n 
 \tsaveerrors();
 
 \t// set up domain for labels
 \tclearlabels();
 
-\tlno = setlineno(fn);\n-\
 \tcurfn = fn;
 \tdowidth(curfn->type);

compile関数は、Goの関数をコンパイルする主要なロジックを含んでいます。この変更は、関数ノードfnの本体(fn->nbody)がnilである場合(つまり、関数シグネチャは存在するが、その実装がない場合)の挙動を変更しています。

  • 以前は、関数本体がない場合は単にreturnしていました。これは、Cgoやアセンブリコードで実装される関数など、Goソースファイルに本体がないケースを許容するためでした。
  • 変更後、if(fn->nbody == nil)ブロック内で、さらに条件が追加されました。
    • pure_gotrueである場合(つまり、goコマンドがこのパッケージを純粋なGoパッケージと判断し、gc-=フラグを渡した場合)。
    • または、関数名がinit·で始まる場合(init関数はGoの仕様上、前方宣言されるべきではないため、常にエラーとします)。
    • これらの条件のいずれかが満たされる場合、yyerror("missing function body", fn);を呼び出してコンパイルエラーを発生させます。これにより、「関数本体がありません」という明確なエラーメッセージがコンパイル時に出力されるようになります。

src/cmd/go/build.go

--- a/src/cmd/go/build.go
+++ b/src/cmd/go/build.go
@@ -1311,6 +1311,8 @@ func (gcToolchain) linker() string {
 	return tool(archChar + "l")
 }
 
+var rsc = flag.Bool("rsc", false, "rsc")
+
 func (gcToolchain) gc(b *builder, p *Package, obj string, importArgs []string, gofiles []string) (ofile string, err error) {
 	out := "_go_." + archChar
 	ofile = obj + out
@@ -1321,6 +1323,21 @@ func (gcToolchain) gc(b *builder, p *Package, obj string, importArgs []string, g
 		gcargs = append(gcargs, "-+")
 	}
 
+	// If we're giving the compiler the entire package (no C etc files), tell it that,
+	// so that it can give good error messages about forward declarations.
+	// Exceptions: a few standard packages have forward declarations for
+	// pieces supplied behind-the-scenes by package runtime.
+	extFiles := len(p.CgoFiles) + len(p.CFiles) + len(p.SFiles) + len(p.SysoFiles) + len(p.SwigFiles) + len(p.SwigCXXFiles)
+	if p.Standard {
+		switch p.ImportPath {
+		case "os", "runtime/pprof", "sync", "time":
+			extFiles++
+		}
+	}
+	if extFiles == 0 {
+		gcargs = append(gcargs, "-=")
+	}
+
 	args := stringList(tool(archChar+"g"), "-o", ofile, buildGcflags, gcargs, "-D", p.localPrefix, importArgs)
 	for _, f := range gofiles {
 		args = append(args, mkAbs(p.Dir, f))\n

goコマンドのgcメソッドは、Goコンパイラ(gc)を呼び出す際の引数を構築します。

  • extFilesという変数が導入され、コンパイル対象のパッケージがGo以外のソースファイル(Cgo、C、アセンブリ、Syso、SWIGファイル)をどれだけ含んでいるかを計算します。
  • p.Standardtrue(標準ライブラリパッケージである場合)かつ、インポートパスがos, runtime/pprof, sync, timeのいずれかである場合、extFilesをインクリメントしています。これは、これらの標準パッケージが内部的にruntimeパッケージによって提供される機能(Go以外のコードで実装されている可能性がある)に依存しているため、純粋なGoパッケージとして扱わないようにするための例外処理です。
  • extFiles0である場合(つまり、Go以外のソースファイルが全く含まれていない純粋なGoパッケージであると判断された場合)、gcargs"-="フラグが追加されます。このフラグは、前述のsrc/cmd/gc/lex.cpure_go変数に設定され、src/cmd/gc/pgen.cでの前方宣言検出ロジックを有効にします。

これらの変更により、goコマンドはパッケージの性質を正確に判断し、必要に応じてgcコンパイラに適切なフラグを渡すことで、純粋なGoパッケージにおける前方宣言の早期検出を実現しています。

関連リンク

参考にした情報源リンク

  • Go言語の公式ドキュメント
  • Go言語のソースコード
  • Go言語のIssueトラッカー (Issue 3705)
  • Goコンパイラに関する一般的な情報源 (例: Go compiler internals, Go toolchain documentation)
  • 前方宣言、コンパイル時エラー、リンク時エラーに関する一般的なプログラミング概念の解説