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

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

このコミットは、Goコンパイラ(cmd/gc)におけるメソッド呼び出しの初期化順序に関するバグを修正するものです。以前のコンパイラでは、メソッド呼び出しが初期化順序の決定や依存関係のループ検出において適切に考慮されていなかったため、予期せぬ動作を引き起こす可能性がありました。この変更により、メソッド呼び出しが初期化プロセスに正しく組み込まれ、依存関係が正確に解決されるようになります。

コミット

commit 6cbf35c1721ff8b22da2176d4ae5f9a98ae98b40
Author: Rémy Oudompheng <oudomphe@phare.normalesup.org>
Date:   Mon Jul 30 09:14:49 2012 +0200

    cmd/gc: fix initialization order involving method calls.
    
    They were previously ignored when deciding order and
    detecting dependency loops.
    Fixes #3824.
    
    R=rsc, golang-dev
    CC=golang-dev, remy
    https://golang.org/cl/6455055

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

https://github.com/golang/go/commit/6cbf35c1721ff8b22da2176d4ae5f9a98ae98b40

元コミット内容

cmd/gc: fix initialization order involving method calls.

メソッド呼び出しが初期化順序の決定および依存関係のループ検出時に以前は無視されていた問題を修正します。 Issue #3824 を修正します。

変更の背景

Go言語では、プログラム内のグローバル変数やパッケージレベルの変数は、使用される前に適切に初期化される必要があります。この初期化の順序は、プログラムの正確な動作を保証するために非常に重要です。特に、ある変数の初期化が別の変数の値に依存する場合、コンパイラはその依存関係を正確に把握し、適切な順序で初期化を実行する必要があります。

このコミットが行われる以前のGoコンパイラ(cmd/gc)には、メソッド呼び出しが初期化順序の決定ロジックから漏れているというバグが存在しました。具体的には、メソッド呼び出しが初期化式に含まれている場合、コンパイラはそれを依存関係として正しく認識せず、初期化順序の決定や循環依存(デッドロックや無限ループの原因となる)の検出において無視していました。

この問題は、Go Issue #3824 として報告されました。test/fixedbugs/bug446.go に追加されたテストケースが示すように、メソッド呼び出しを含む変数の初期化が、そのメソッドが依存する他の変数の初期化よりも先に行われてしまう可能性がありました。これにより、未初期化の値が使用されたり、プログラムが予期せぬ結果になったりする可能性がありました。

このコミットは、この根本的な問題を解決し、Goプログラムの初期化セマンティクスが常に正しく、予測可能であることを保証するために導入されました。

前提知識の解説

Goコンパイラ (cmd/gc)

cmd/gc は、Go言語の公式コンパイラであり、Goソースコードを機械語に変換する役割を担っています。コンパイルプロセスには、構文解析、型チェック、最適化、コード生成など、多くの段階が含まれます。このコミットが関連するのは、特に「初期化順序の決定」と「依存関係の解決」というコンパイラの重要な機能です。

Go言語の初期化順序

Go言語では、パッケージレベルの変数(グローバル変数)は、init 関数と同様に、プログラムの実行開始前に特定の順序で初期化されます。初期化の順序は以下のルールに従います。

  1. インポートされたパッケージの初期化: パッケージがインポートされると、まずそのパッケージが初期化されます。依存関係がある場合、依存先のパッケージが先に初期化されます。
  2. パッケージレベル変数の初期化: 各パッケージ内で宣言されたパッケージレベル変数は、宣言順に初期化されます。ただし、変数の初期化式が他の変数に依存する場合、その依存関係が解決されるように順序が調整されます。
  3. init 関数の実行: パッケージレベル変数の初期化が完了した後、各パッケージの init 関数が宣言順に実行されます。

このコミットの文脈では、特に「パッケージレベル変数の初期化」における依存関係の解決が重要です。

依存関係のループ (Dependency Loops)

プログラミングにおいて、依存関係のループとは、AがBに依存し、BがAに依存するといった循環的な依存関係を指します。このようなループは、初期化順序を決定する上で問題を引き起こす可能性があります。例えば、Aを初期化するにはBが必要で、Bを初期化するにはAが必要な場合、どちらも初期化を開始できません。コンパイラはこのようなループを検出し、エラーとして報告する必要があります。

メソッド呼び出し (Method Calls)

Go言語では、構造体などの型に紐付けられた関数を「メソッド」と呼びます。メソッドはレシーバ引数(func (r T) Method()r T の部分)を持ち、そのレシーバの型の値に対して呼び出されます。

type MyType int

func (m MyType) GetValue() int {
    return int(m)
}

var x MyType = 10
var y = x.GetValue() // ここでメソッド呼び出しが発生

このコミットの核心は、y の初期化式 x.GetValue() のようなメソッド呼び出しが、コンパイラの初期化順序決定ロジックにおいて適切に扱われていなかった点にあります。

技術的詳細

この修正は、Goコンパイラのバックエンド、特に初期化順序を管理する部分に焦点を当てています。

Goコンパイラは、プログラム内のすべての初期化式を解析し、それらの間の依存関係をグラフとして構築します。その後、このグラフをトポロジカルソート(依存関係の順序を考慮してノードを並べるアルゴロジック)することで、正しい初期化順序を決定します。もし循環依存が検出された場合、コンパイラはエラーを報告します。

以前の実装では、メソッド呼び出しを含む式がこの依存関係グラフの構築時に適切に「ノード」として追加されていなかったか、あるいはその依存関係が正しく追跡されていませんでした。これにより、メソッド呼び出しが他の変数の初期化に依存しているにもかかわらず、その依存関係が無視され、誤った初期化順序が生成される可能性がありました。

このコミットは、主に以下の2つのファイルに変更を加えています。

  1. src/cmd/gc/reflect.c: このファイルは、Goの型システムとリフレクションに関連するコンパイラの内部処理を扱います。特に、メソッドの型情報を生成する methodfunc 関数が変更されています。この変更は、メソッドの型がその元のメソッド関数の名前(nname)へのリンクを持つようにすることで、初期化順序の決定時にメソッドが正しく識別され、その依存関係が追跡されるようにするための準備です。
  2. src/cmd/gc/sinit.c: このファイルは、Goプログラムの静的初期化(sinit は "static initialization" の略)のロジックを扱います。ここが初期化順序の決定と依存関係の解決の核心部分です。init1 および init2 関数は、初期化式を走査し、依存関係を収集する役割を担っています。このコミットでは、メソッド式(OTYPEPFUNC の組み合わせ、または ODOTMETH)が検出された際に、そのメソッドが依存する実体(n->type->nname)も初期化リストに追加されるように修正されています。これにより、コンパイラはメソッド呼び出しが持つ暗黙的な依存関係を正しく認識し、初期化順序の決定に含めることができるようになります。

test/fixedbugs/bug446.go は、このバグを再現し、修正が正しく機能することを確認するための新しいテストケースです。このテストは、メソッド呼び出しを含む変数が、そのメソッドが依存する他の変数の初期化後に正しく初期化されることを検証します。

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

src/cmd/gc/reflect.c

--- a/src/cmd/gc/reflect.c
+++ b/src/cmd/gc/reflect.c
@@ -130,7 +130,12 @@ methodfunc(Type *f, Type *receiver)
 		out = list(out, d);
 	}
 
-	return functype(N, in, out);
+	t = functype(N, in, out);
+	if(f->nname) {
+		// Link to name of original method function.
+		t->nname = f->nname;
+	}
+	return t;
 }

src/cmd/gc/sinit.c

--- a/src/cmd/gc/sinit.c
+++ b/src/cmd/gc/sinit.c
@@ -36,6 +36,10 @@ init1(Node *n, NodeList **out)
 	init1(n->right, out);
 	for(l=n->list; l; l=l->next)
 		init1(l->n, out);
+	if(n->left && n->type && n->left->op == OTYPE && n->class == PFUNC) {
+		// Definitions for method expressions are stored in type->nname.
+		init1(n->type->nname, out);
+	}
 
 	if(n->op != ONAME)
 		return;
@@ -170,6 +174,8 @@ init2(Node *n, NodeList **out)
 	
 	if(n->op == OCLOSURE)
 		init2list(n->closure->nbody, out);
+	if(n->op == ODOTMETH)
+		init2(n->type->nname, out);
 }

test/fixedbugs/bug446.go (新規追加)

// run

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

// Issue 3824.
// Method calls are ignored when deciding initialization
// order.

package main

type T int

func (r T) Method1() int { return a }
func (r T) Method2() int { return b }

// dummy1 and dummy2 must be initialized after a and b.
var dummy1 = T(0).Method1()
var dummy2 = T.Method2(0)

// Use a function call to force generating code.
var a = identity(1)
var b = identity(2)

func identity(a int) int { return a }

func main() {
	if dummy1 != 1 {
		panic("dummy1 != 1")
	}
	if dummy2 != 2 {
		panic("dummy2 != 2")
	}
}

コアとなるコードの解説

src/cmd/gc/reflect.c の変更

methodfunc 関数は、Goのメソッドの型を構築する際に呼び出されます。変更前は、単に引数と戻り値の型に基づいて関数型を返していました。 変更後、if(f->nname) の条件が追加され、もし元の関数型 f が名前(nname)を持っている場合、新しく作成されるメソッドの型 t にもその名前をリンクするようになりました。 この nname は、コンパイラがシンボルを識別し、依存関係を追跡するための重要な情報です。この変更により、メソッドの型がその実体である関数への参照を保持するようになり、sinit.c での依存関係の追跡が容易になります。

src/cmd/gc/sinit.c の変更

sinit.c は、Goの静的初期化のロジックを実装しています。

  1. init1 関数の変更: init1 関数は、初期化式を再帰的に走査し、初期化が必要なノードを収集します。 追加されたブロック if(n->left && n->type && n->left->op == OTYPE && n->class == PFUNC) は、型に紐付けられた関数(メソッド式)の定義を検出します。OTYPE は型ノード、PFUNC は関数ポインタまたはメソッドを表します。 この条件が真の場合、init1(n->type->nname, out); が呼び出されます。これは、メソッドの定義自体(n->type->nname に格納されている)も初期化リストに追加されるべき依存関係として認識されることを意味します。これにより、メソッドが使用される前にその定義が確実に処理されるようになります。

  2. init2 関数の変更: init2 関数も初期化の依存関係を処理しますが、init1 とは異なるフェーズで動作します。 追加された行 if(n->op == ODOTMETH) は、ドット記法によるメソッド呼び出し(例: T(0).Method1()T.Method2(0))を検出します。ODOTMETH は、レシーバを持つメソッド呼び出しを表すオペレーションコードです。 この条件が真の場合、init2(n->type->nname, out); が呼び出されます。これは、メソッド呼び出しが行われる際に、そのメソッドの実体(n->type->nname)が依存関係として考慮されることを保証します。これにより、メソッドが初期化される前に呼び出されることを防ぎます。

これらの変更により、コンパイラはメソッドの定義と呼び出しを初期化順序の決定プロセスに正しく組み込むことができるようになり、Issue #3824 で報告されたバグが修正されます。

test/fixedbugs/bug446.go の新規追加

このテストケースは、問題の核心をシンプルに示しています。 abidentity 関数によって初期化される変数です。 dummy1T(0).Method1() の結果で初期化され、Method1a に依存します。 dummy2T.Method2(0) の結果で初期化され、Method2b に依存します。

このテストの目的は、dummy1dummy2 が初期化される前に ab が正しく初期化されていることを確認することです。もし修正が適用されていなければ、コンパイラは Method1Method2ab に依存していることを認識せず、dummy1dummy2 が未初期化の ab の値を使用してしまい、テストが失敗する可能性がありました。

main 関数内の panic ステートメントは、dummy11 に、dummy22 に正しく初期化されていることを検証します。これは、ab がそれぞれ 12 に初期化された後にメソッドが呼び出された場合にのみ真となります。

関連リンク

  • Go Issue #3824 (このコミットによって修正された問題): https://github.com/golang/go/issues/3824 (ただし、このリンクは一般的なGoリポジトリのIssueトラッカーへのリンクであり、特定のIssue 3824のページに直接リンクしているわけではありません。GoのIssueは通常、golang.org/issue/XXXX の形式で参照されますが、このコミットの時点ではGitHubのIssueトラッカーが主要ではなかった可能性があります。)
  • Go Code Review (このコミットのレビューページ): https://golang.org/cl/6455055

参考にした情報源リンク

  • https://golang.org/cl/6455055 (Go Code Review)
  • Go言語の初期化順序に関する公式ドキュメント (一般的な情報源として)
  • Goコンパイラの内部構造に関する一般的な知識 (cmd/gc の役割、AST、型システムなど)
  • Go言語のメソッドに関する公式ドキュメント (一般的な情報源として)