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

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

このコミットは、Goコンパイラ(cmd/gc)におけるバグ修正です。具体的には、クロージャ内で宣言されたブランク識別子(_)の初期化が、誤ってパッケージのinit()関数の一部として処理されてしまう問題を解決します。これにより、コンパイル時のクラッシュや、実行時の予期せぬ動作を防ぎます。

コミット

commit 0d0d57ccfe95e679005542c2dd572fc549256079
Author: Rémy Oudompheng <oudomphe@phare.normalesup.org>
Date:   Sun Jun 2 23:54:34 2013 +0200

    cmd/gc: do not corrupt init() with initializers of _ in closures.
    
    Fixes #5607.
    
    R=golang-dev, daniel.morsing, r, dsymonds
    CC=golang-dev
    https://golang.org/cl/9952043

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

https://github.com/golang/go/commit/0d0d57ccfe95e679005542c2dd572fc549256079

元コミット内容

このコミットは、Goコンパイラ(cmd/gc)が、クロージャ内で定義されたブランク識別子(_)の初期化処理を誤ってパッケージのinit()関数に含めてしまうというバグを修正するものです。この問題は、GoのIssue 5607として報告されていました。

具体的には、init()関数はパッケージレベルの初期化を担当しますが、クロージャ内のローカルな変数の初期化がここに混入することで、コンパイラが予期しない状態になり、クラッシュしたり、不正なコードを生成したりする可能性がありました。この修正は、sinit.c内の初期化処理ロジックを調整することで、この誤った挙動を防ぎます。

変更の背景

Go言語では、各パッケージはinit()関数を持つことができ、これはパッケージがインポートされた際に自動的に実行され、パッケージレベルの初期化を行います。また、Goはブランク識別子(_)を使用して、変数を宣言しつつその値を破棄する機能を提供します。

このコミットが修正する問題は、Goコンパイラが、クロージャ(匿名関数)内でブランク識別子を使って変数を初期化する際に、その初期化処理を誤ってパッケージのinit()関数に組み込んでしまうというものでした。

例えば、以下のようなコードがあったとします。

package main

var Test = func() {
	var mymap = map[string]string{"a": "b"}

	var innerTest = func() {
		// ここでブランク識別子を使用
		var _, x = mymap["a"]
		println(x)
	}
	innerTest()
}

func main() {}

この場合、innerTestクロージャ内のvar _, x = mymap["a"]という行の初期化処理が、本来init()関数とは無関係であるにもかかわらず、コンパイラによってinit()関数の一部として扱われていました。これにより、init()関数が実行される際に、本来ローカルスコープで解決されるべき変数(mymapなど)が未定義の状態であったり、関数呼び出しの深さ(funcdepth)が一致しなかったりして、コンパイラがクラッシュしたり、実行時にpanicを引き起こすような不正なコードが生成されたりする可能性がありました。

このバグは、特にPanic()のような関数呼び出しの結果をブランク識別子に代入する場合に顕著で、init()関数内でPanic()が呼び出されてしまい、プログラムが起動時にクラッシュするという問題が発生していました。コミットに含まれるテストケースtest/fixedbugs/issue5607.goは、この問題を再現し、修正が正しく機能することを確認するために追加されました。

前提知識の解説

Goの init() 関数

Go言語において、init()関数は特別な関数です。

  • 自動実行: 各パッケージは0個以上のinit()関数を持つことができ、これらはパッケージがインポートされた際に、main()関数が実行される前に自動的に呼び出されます。
  • 初期化の順序: パッケージの初期化は、インポートの依存関係に基づいて行われます。依存するパッケージが先に初期化され、その後に依存されるパッケージが初期化されます。同じパッケージ内に複数のinit()関数がある場合、それらはファイル名の辞書順で実行されます。
  • 用途: 主に、パッケージレベルの変数の複雑な初期化、外部リソースのセットアップ(データベース接続など)、プログラム起動前の検証などに使用されます。

Goのクロージャ

Goにおけるクロージャは、関数リテラル(匿名関数)の一種で、それが定義された環境(外側の関数スコープ)の変数を「キャプチャ」して使用できる機能です。

  • 変数キャプチャ: クロージャは、自身の本体が実行される際に、外側のスコープにある変数を参照したり変更したりできます。これらの変数は、クロージャが定義された時点のコピーではなく、参照としてキャプチャされます。
  • 匿名関数: 名前を持たない関数として定義され、変数に代入したり、他の関数の引数として渡したり、即座に実行したりできます。

Goのブランク識別子 (_)

ブランク識別子(アンダースコア _)は、Go言語において、値を使用しないことを明示的に示すために使用される特別な識別子です。

  • 値の破棄: 変数を宣言する際に、その値が必要ない場合や、複数の戻り値を持つ関数から一部の値のみを使用したい場合に、その値をブランク識別子に代入することで破棄できます。
  • コンパイルエラーの回避: Goでは、宣言されたローカル変数が使用されない場合、コンパイルエラーになります。ブランク識別子を使用することで、このエラーを回避しつつ、不要な値を無視できます。
  • : _, err := someFunc() のように、エラーだけをチェックしたい場合などに使われます。

Goコンパイラ (gc) と sinit.c

Go言語の公式コンパイラは、通常gc(Go Compiler)と呼ばれます。これはGoツールチェーンの一部であり、Goのソースコードを機械語に変換する役割を担います。

  • src/cmd/gc: Goコンパイラのソースコードが格納されているディレクトリです。
  • sinit.c: このファイルは、Goコンパイラのバックエンドの一部であり、特にGoプログラムの初期化処理(init()関数の生成や、グローバル変数の初期化順序の決定など)に関連するロジックを含んでいます。コンパイラがAST(抽象構文木)を走査し、初期化が必要なノードを特定し、それらを適切な順序でinit()関数に組み込む処理が行われます。

このコミットは、sinit.c内の初期化処理ロジックが、クロージャ内のブランク識別子の初期化を誤ってグローバルなinit()処理に含めてしまうという、コンパイラの内部的なバグを修正するものです。

技術的詳細

このコミットの核心は、src/cmd/gc/sinit.cファイル内のinit1関数の変更にあります。init1関数は、Goコンパイラがプログラムの初期化順序を決定し、init()関数に含めるべき初期化処理を収集する際に使用される重要な関数です。

変更前は、init1関数内でブランク識別子(isblank(n))を持つノードが検出された場合、そのノードがinit()関数に含めるべき初期化処理であるかどうかを判断する際に、そのノードが関数内部にあるかどうかを適切にチェックしていませんでした。

具体的には、以下の条件が変更されました。

変更前:

if(isblank(n) && n->defn != N && n->defn->initorder == InitNotStarted) {

変更後:

if(isblank(n) && n->curfn == N && n->defn != N && n->defn->initorder == InitNotStarted) {

追加された条件は n->curfn == N です。

  • n: 現在処理している抽象構文木(AST)のノードを表します。
  • n->curfn: ノードが属する現在の関数を表すポインタです。もしノードがどの関数にも属していない(つまり、グローバルスコープにある)場合、n->curfnN(NULL)になります。

この変更により、init1関数は、ブランク識別子の初期化処理をinit()関数に含めるかどうかを判断する際に、その初期化がグローバルスコープで行われている場合に限定するようになりました。

変更のメカニズム:

  1. 問題の特定: 以前のロジックでは、クロージャ内でブランク識別子に値が代入される場合(例: var _, x = someFunc())、isblank(n)は真となり、その初期化処理がinit()関数に誤って組み込まれていました。しかし、クロージャ内の変数はローカルスコープに属するため、init()関数で初期化されるべきではありませんでした。
  2. n->curfn == Nの追加: この条件を追加することで、コンパイラは、現在処理しているノードが「現在の関数(curfn)に属していない」、つまり「グローバルスコープにある」場合にのみ、そのブランク識別子の初期化をinit()関数に含めるように制限します。
  3. 問題の解決: これにより、クロージャ内のローカルなブランク識別子の初期化がinit()関数に誤って混入することがなくなり、init()関数が不正なコンテキストで実行されることによるクラッシュや、誤ったコード生成が防止されます。

この修正は、Goコンパイラの初期化処理の正確性を向上させ、特にクロージャとブランク識別子を組み合わせた場合の堅牢性を高めるものです。

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

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

--- a/src/cmd/gc/sinit.c
+++ b/src/cmd/gc/sinit.c
@@ -50,8 +50,11 @@ init1(Node *n, NodeList **out)
 	case PFUNC:
 		break;
 	default:
-		if(isblank(n) && n->defn != N && n->defn->initorder == InitNotStarted) {
+		if(isblank(n) && n->curfn == N && n->defn != N && n->defn->initorder == InitNotStarted) {
+			// blank names initialization is part of init() but not
+			// when they are inside a function.
 			n->defn->initorder = InitDone;
+			if(debug['%']) dump("nonstatic", n->defn);
 			*out = list(*out, n->defn);
 		}
 		return;
@@ -62,7 +65,7 @@ init1(Node *n, NodeList **out)
 	if(n->initorder == InitPending) {
 		if(n->class == PFUNC)
 			return;
-		
+
 		// if there have already been errors printed,
 		// those errors probably confused us and
 		// there might not be a loop.  let the user
@@ -128,7 +131,7 @@ init1(Node *n, NodeList **out)
 				if(debug['j'])
 					print("%S\n", n->sym);
 				if(!staticinit(n, out)) {
-if(debug['%']) dump("nonstatic", n->defn);
+					if(debug['%']) dump("nonstatic", n->defn);
 					*out = list(*out, n->defn);
 				}
 			} else if(0) {
@@ -149,6 +152,7 @@ if(debug['%']) dump("nonstatic", n->defn);
 			n->defn->initorder = InitDone;
 			for(l=n->defn->rlist; l; l=l->next)
 				init1(l->n, out);
+			if(debug['%']) dump("nonstatic", n->defn);
 			*out = list(*out, n->defn);
 			break;
 		}

test/fixedbugs/issue5607.go の追加:

// run

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

// Issue 5607: generation of init() function incorrectly
// uses initializers of blank variables inside closures.

package main

var Test = func() {
	var mymap = map[string]string{"a": "b"}

	var innerTest = func() {
		// Used to crash trying to compile this line as
		// part of init() (funcdepth mismatch).
		var _, x = mymap["a"]
		println(x)
	}
	innerTest()
}

var Test2 = func() {
	// The following initializer should not be part of init()
	// The compiler used to generate a call to Panic() in init().
	var _, x = Panic()
	_ = x
}

func Panic() (int, int) {
	panic("omg")
	return 1, 2
}

func main() {}

コアとなるコードの解説

src/cmd/gc/sinit.c の変更は、init1関数内の条件式に n->curfn == N を追加したことです。

  • init1関数: この関数は、Goコンパイラがプログラムの初期化処理(特にグローバル変数の初期化やinit()関数の呼び出し)をどのように順序付けるかを決定するロジックの一部です。AST(抽象構文木)のノードを走査し、初期化が必要な要素を特定します。
  • isblank(n): 現在のノードnがブランク識別子(_)であるかどうかをチェックします。
  • n->defn != N: ノードnが定義(defn)を持っていることを確認します。これは、変数の宣言や初期化に関連するノードであることを示唆します。
  • n->defn->initorder == InitNotStarted: その定義の初期化順序がまだ開始されていない状態であることを確認します。これにより、初期化が重複して処理されるのを防ぎます。
  • n->curfn == N (追加された条件): これが最も重要な変更点です。n->curfnは、現在のノードが属する関数へのポインタです。N(NULL)であるということは、そのノードがどの関数スコープにも属しておらず、グローバルスコープにあることを意味します。

変更の意図: この変更の目的は、ブランク識別子の初期化が、グローバルスコープで発生している場合にのみ、パッケージのinit()関数に含められるようにすることです。

変更前は、isblank(n)が真であれば、たとえそのブランク識別子がクロージャのような関数内部で宣言・初期化されていても、init()関数に組み込まれる可能性がありました。これは、init()関数がパッケージレベルの初期化を担当するというGoの設計原則に反し、ローカルスコープの変数がグローバルな初期化処理に影響を与えるという予期せぬ副作用を引き起こしていました。

n->curfn == Nを追加することで、コンパイラは、ブランク識別子の初期化が本当にパッケージレベルで行われているのかどうかを厳密にチェックするようになります。これにより、test/fixedbugs/issue5607.goで示されているような、クロージャ内のブランク識別子の初期化がinit()関数に誤って含まれてしまい、コンパイルエラーや実行時パニックを引き起こす問題を解決しています。

test/fixedbugs/issue5607.goは、このバグを再現するためのテストケースです。

  • Test変数内のクロージャでは、var _, x = mymap["a"]という行が、以前はinit()関数の一部としてコンパイルされようとしていました。mymapはローカル変数であるため、init()関数からはアクセスできず、funcdepth mismatch(関数呼び出しの深さの不一致)などのコンパイルエラーやクラッシュを引き起こしていました。
  • Test2変数内のクロージャでは、var _, x = Panic()という行が、以前はinit()関数に組み込まれ、プログラム起動時にPanic()が呼び出されてしまう問題がありました。

このコミットによって、これらのテストケースは正しくコンパイルされ、実行されるようになります。

関連リンク

参考にした情報源リンク

  • Go言語の公式ソースコードリポジトリ: https://github.com/golang/go
  • Go言語のIssueトラッカー(過去のIssueを検索する際に利用): https://github.com/golang/go/issues
  • Go言語のコンパイラ設計に関する一般的な情報(Goのブログやドキュメントなど)
  • C言語のポインタと構造体に関する一般的な知識(sinit.cがC言語で書かれているため)
  • 抽象構文木(AST)に関する一般的な知識