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

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

このコミットは、Goコンパイラ(cmd/gc)におけるdeferおよびgoステートメントの誤用に関するエラーメッセージの明確化を目的としています。具体的には、defergoの引数として型変換や無効な関数呼び出しが指定された場合に、より分かりやすいエラーメッセージを出力するように改善されています。これにより、開発者はdefergoの誤った使用方法を迅速に特定し、修正できるようになります。

コミット

commit 79a16a3b70f0de2efbd45952a51e9b30524d7ad3
Author: Russ Cox <rsc@golang.org>
Date:   Fri Feb 1 21:02:15 2013 -0500

    cmd/gc: clearer error for defer/go of conversion or invalid function call
    
    Fixes #4654.
    
    R=ken2
    CC=golang-dev
    https://golang.org/cl/7229072

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

https://github.com/golang/go/commit/79a16a3b70f0de2efbd45952a51e9b30524d7ad3

元コミット内容

cmd/gc: clearer error for defer/go of conversion or invalid function call

このコミットは、deferまたはgoステートメントが型変換や無効な関数呼び出しで使用された場合に、より明確なエラーメッセージを提供するようにGoコンパイラを修正します。

関連するIssue: #4654

変更の背景

Go言語のdeferおよびgoステートメントは、それぞれ関数の終了時実行とゴルーチン(並行処理)の開始に使用されます。これらのステートメントは、引数として関数呼び出しを期待します。しかし、Goの初期のコンパイラでは、defergoの引数に、関数呼び出しではない式(例えば、型変換の結果や、戻り値が破棄されるべき組み込み関数の呼び出し)が指定された場合に、エラーメッセージが不明瞭であったり、誤解を招くものであったりする問題がありました。

特に、Issue #4654では、defer int(0)のような型変換をdeferの引数に指定した場合に、コンパイラが「defer requires function call, not conversion」(deferは関数呼び出しを必要とし、型変換ではない)という明確なエラーメッセージではなく、より一般的な「not used」(使用されていない)というメッセージを出力してしまう問題が報告されていました。同様に、go append(x, 1)のように、戻り値が破棄される組み込み関数の呼び出しに対しても、より適切なエラーメッセージが求められていました。

このコミットは、これらの問題を解決し、開発者がdefergoの誤用をより簡単に理解し、修正できるようにするために導入されました。

前提知識の解説

Go言語のdeferステートメント

deferステートメントは、それを囲む関数がreturnする直前、またはパニックが発生して関数が終了する直前に、指定された関数呼び出しを遅延実行するために使用されます。これは、リソースの解放(ファイルのクローズ、ロックの解除など)や、パニックからの回復(recover関数と組み合わせて)によく利用されます。deferの引数は、常に関数呼び出しである必要があります。

例:

func readFile(filename string) {
    f, err := os.Open(filename)
    if err != nil {
        log.Fatal(err)
    }
    defer f.Close() // 関数終了時にファイルをクローズ
    // ファイルの読み込み処理
}

Go言語のgoステートメント

goステートメントは、新しいゴルーチン(軽量な並行実行スレッド)を開始するために使用されます。goの引数もまた、常に関数呼び出しである必要があります。この関数呼び出しは、現在のゴルーチンとは独立して並行して実行されます。

例:

func doSomething() {
    fmt.Println("Doing something...")
}

func main() {
    go doSomething() // 新しいゴルーチンでdoSomethingを実行
    fmt.Println("Main function continues...")
    time.Sleep(time.Second) // ゴルーチンが実行されるのを待つ
}

Goコンパイラ(cmd/gc

cmd/gcは、Go言語の公式コンパイラの一部であり、Goソースコードを機械語に変換する役割を担っています。コンパイルプロセスには、字句解析、構文解析、型チェック、最適化、コード生成などが含まれます。このコミットで変更されているsrc/cmd/gc/const.csrc/cmd/gc/fmt.csrc/cmd/gc/typecheck.cは、それぞれ定数処理、フォーマット、型チェックのフェーズを担当するファイルです。

型変換と組み込み関数

Go言語では、int(0)のように型名を関数のように使用して、ある型から別の型へ値を変換することができます。また、len(), cap(), append(), make(), new(), complex(), real(), imag(), unsafe.Alignof(), unsafe.Offsetof(), unsafe.Sizeof()などの組み込み関数は、値を返しますが、その戻り値が常に使用されるとは限りません。defergoの引数としてこれらの関数が呼び出された場合、その戻り値は破棄されることになります。

抽象構文木 (AST) と Node構造体

Goコンパイラは、ソースコードを解析する際に、その構造を抽象構文木(AST)として内部的に表現します。ASTの各要素はNode構造体で表されます。Node構造体には、そのノードの種類(opフィールド)、関連する型情報(typeフィールド)、値(valフィールド)、そして重要なorigフィールドが含まれます。origフィールドは、最適化や型チェックの過程でノードが変換されたり書き換えられたりした場合に、そのノードの元の形を保持するために使用されます。これにより、エラーメッセージを生成する際に、元のソースコードに近い形で問題箇所を報告することが可能になります。

技術的詳細

このコミットの主要な変更は、src/cmd/gc/typecheck.cにおける型チェックロジックの強化と、src/cmd/gc/const.cおよびsrc/cmd/gc/fmt.cにおける補助的な変更です。

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

  • checkdefergo関数の導入: このコミットの最も重要な変更点は、checkdefergoという新しい静的関数が導入されたことです。この関数は、deferまたはgoステートメントの引数として渡された式が、有効な関数呼び出しであるかどうかをチェックします。

    • OCALLINTER, OCALLMETH, OCALLFUNC, OCLOSE, OCOPY, ODELETE, OPANIC, OPRINT, OPRINTN, ORECOVERなどの明示的な関数呼び出しや、defer/goで許可されている組み込み関数は「OK」と判断されます。
    • OAPPEND, OCAP, OCOMPLEX, OIMAG, OLEN, OMAKE, OMAKESLICE, OMAKECHAN, OMAKEMAP, ONEW, OREAL, OLITERAL(型変換やunsafeパッケージの関数を含む)などの、戻り値を持つ組み込み関数がdefer/goの引数として使用された場合、yyerror("%s discards result of %N", what, n->left)というエラーメッセージを出力するように変更されました。これは、これらの関数が値を返すにもかかわらず、defer/goのコンテキストではその戻り値が破棄されることを明確に示します。
    • OLITERALノードがOCONV(型変換)のorigノードを持つ場合、またはその他の無効なオペレーションの場合、yyerror("%s requires function call, not conversion", what)というエラーメッセージを出力します。これにより、defer int(0)のようなケースで、より具体的なエラーメッセージが表示されるようになります。
  • ODEFEROPROCの型チェックの変更: typecheck関数内で、ODEFERdeferステートメント)とOPROCgoステートメント)の処理が変更されました。

    • 以前はtypecheck(&n->left, Etop)またはtypecheck(&n->left, Etop|Eproc)が呼び出されていましたが、これにErv(式が値を返すことを示すフラグ)が追加され、typecheck(&n->left, Etop|Erv)またはtypecheck(&n->left, Etop|Eproc|Erv)となりました。これにより、defergoの引数となる式が値を返す可能性があることを型チェッカーに伝え、その後のcheckdefergo関数での詳細なチェックを可能にしています。
    • typecheckの呼び出し後、n->left->diagが設定されていない(つまり、すでにエラーが報告されていない)場合にのみcheckdefergo(n)が呼び出されるようになりました。これにより、重複したエラーメッセージの出力を防ぎます。
  • yyerrorメッセージの変更: 一般的な「%N not used」エラーメッセージが、「%N evaluated but not used」に変更されました。これは、式が評価されたがその結果が使用されなかったという状況をより正確に表現しています。

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

  • saveorig関数の導入: saveorigという新しいヘルパー関数が導入されました。この関数は、ノードnorigフィールドがn自身を指している場合(つまり、まだ元のノードが複製されていない場合)に、nのコピーを作成してn->origに設定します。これにより、ノードが変換された後でも、元のノードの情報を保持し、エラーメッセージなどで元の式を正確に表示できるようになります。
  • isconstretsettruesetfalseなどの関数でsaveorigが使用されるようになりました。特に、retセクションでは、OCONVノードのダンプが追加されており、デバッグ時に型変換の元のノードを確認しやすくなっています。

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

  • Vconv関数の変更: 複素数リテラルのフォーマットが改善されました。mpcmpfltc関数を使用して実部または虚部がゼロの場合の表示がより簡潔になるように調整されています。例えば、虚部がゼロの場合は実部のみが表示されるようになります。 また、fmtprint(fp, "<%d>", v->ctype)fmtprint(fp, "<ctype=%d>", v->ctype)に変更され、デバッグ出力の可読性が向上しています。
  • exprfmt関数の変更: OLITERALノードの処理において、n->val.ctype == CTNILかつn->orig != Nかつn->orig != nの場合に、n->origを再帰的にexprfmtで処理するように変更されました。これにより、型付けされたnilリテラルが元の裸のnilとして表示されるようになり、エラーメッセージの可読性が向上します。

テストファイルの変更

  • test/fixedbugs/issue4463.go: このテストファイルでは、godeferの引数としてappend, cap, complex, imag, len, make, new, real, unsafe.Alignof, unsafe.Offsetof, unsafe.Sizeofなどの組み込み関数が使用された場合の期待されるエラーメッセージが、「not used」から「discards result」に変更されました。これは、コンパイラがこれらのケースをより具体的に識別し、適切なエラーメッセージを出力するようになったことを反映しています。
  • test/fixedbugs/issue4654.go (新規追加): この新しいテストファイルは、Issue #4654で報告された問題、すなわちdefer int(0)go string([]byte("abc"))のような型変換がdefer/goの引数として使用された場合に、期待されるエラーメッセージ「defer requires function call, not conversion」や「go requires function call, not conversion」が正しく出力されることを検証します。また、discards resultエラーの様々なケースもテストされています。

これらの変更により、Goコンパイラはdefergoステートメントの誤用に対して、より具体的で分かりやすいエラーメッセージを提供するようになり、開発者のデバッグ体験が向上しました。

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

src/cmd/gc/const.c

+static Node*
+saveorig(Node *n)
+{
+
+	Node *n1;
+
+	if(n == n->orig) {
+		// duplicate node for n->orig.
+		n1 = nod(OLITERAL, N, N);
+		n->orig = n1;
+		*n1 = *n;
+	}
+	return n->orig;
+}

src/cmd/gc/typecheck.c

@@ -1539,12 +1548,15 @@ reswitch:
 
 	case ODEFER:
 	\tok |= Etop;
-\t\ttypecheck(&n->left, Etop);\n+\t\ttypecheck(&n->left, Etop|Erv);\n+\t\tif(!n->left->diag)\n+\t\t\tcheckdefergo(n);\n \t\tgoto ret;
 
 	case OPROC:
 	\tok |= Etop;
-\t\ttypecheck(&n->left, Etop|Eproc);\n+\t\ttypecheck(&n->left, Etop|Eproc|Erv);\n+\t\tcheckdefergo(n);\n \t\tgoto ret;
@@ -1687,6 +1699,56 @@ out:
 	*np = n;
 }
 
+static void
+checkdefergo(Node *n)
+{
+	char *what;
+	
+	what = "defer";
+	if(n->op == OPROC)
+		what = "go";
+
+	switch(n->left->op) {
+	case OCALLINTER:
+	case OCALLMETH:
+	case OCALLFUNC:
+	case OCLOSE:
+	case OCOPY:
+	case ODELETE:
+	case OPANIC:
+	case OPRINT:
+	case OPRINTN:
+	case ORECOVER:
+		// ok
+		break;
+	case OAPPEND:
+	case OCAP:
+	case OCOMPLEX:
+	case OIMAG:
+	case OLEN:
+	case OMAKE:
+	case OMAKESLICE:
+	case OMAKECHAN:
+	case OMAKEMAP:
+	case ONEW:
+	case OREAL:
+	case OLITERAL: // conversion or unsafe.Alignof, Offsetof, Sizeof
+		if(n->left->orig != N && n->left->orig->op == OCONV)
+			goto conv;
+		yyerror("%s discards result of %N", what, n->left);
+		break;
+	default:
+	conv:
+		if(!n->diag) {
+			// The syntax made sure it was a call, so this must be
+			// a conversion.
+			n->diag = 1;
+			yyerror("%s requires function call, not conversion", what);
+		}
+		break;
+	}
+}
+
 static void
 implicitstar(Node **nn)
 {

test/fixedbugs/issue4463.go

--- a/test/fixedbugs/issue4463.go
+++ b/test/fixedbugs/issue4463.go
@@ -45,17 +45,17 @@ func F() {
 	(println("bar"))
 	(recover())
 
-	go append(a, 0)			// ERROR "not used"
-	go cap(a)			// ERROR "not used"
-	go complex(1, 2)		// ERROR "not used"
-	go imag(1i)			// ERROR "not used"
-	go len(a)			// ERROR "not used"
-	go make([]int, 10)		// ERROR "not used"
-	go new(int)			// ERROR "not used"
-	go real(1i)			// ERROR "not used"
-	go unsafe.Alignof(a)		// ERROR "not used"
-	go unsafe.Offsetof(s.f)		// ERROR "not used"
-	go unsafe.Sizeof(a)		// ERROR "not used"
+	go append(a, 0)			// ERROR "discards result"
+	go cap(a)			// ERROR "discards result"
+	go complex(1, 2)		// ERROR "discards result"
+	go imag(1i)			// ERROR "discards result"
+	go len(a)			// ERROR "discards result"
+	go make([]int, 10)		// ERROR "discards result"
+	go new(int)			// ERROR "discards result"
+	go real(1i)			// ERROR "discards result"
+	go unsafe.Alignof(a)		// ERROR "discards result"
+	go unsafe.Offsetof(s.f)		// ERROR "discards result"
+	go unsafe.Sizeof(a)		// ERROR "discards result"
 
 	go close(c)
 	go copy(a, a)
@@ -65,17 +65,17 @@ func F() {
 	go println("bar")
 	go recover()
 
-	defer append(a, 0)		// ERROR "not used"
-	defer cap(a)			// ERROR "not used"
-	defer complex(1, 2)		// ERROR "not used"
-	defer imag(1i)			// ERROR "not used"
-	defer len(a)			// ERROR "not used"
-	defer make([]int, 10)		// ERROR "not used"
-	defer new(int)			// ERROR "not used"
-	defer real(1i)			// ERROR "not used"
-	defer unsafe.Alignof(a)		// ERROR "not used"
-	defer unsafe.Offsetof(s.f)	// ERROR "not used"
-	defer unsafe.Sizeof(a)		// ERROR "not used"
+	defer append(a, 0)			// ERROR "discards result"
+	defer cap(a)			// ERROR "discards result"
+	defer complex(1, 2)		// ERROR "discards result"
+	defer imag(1i)			// ERROR "discards result"
+	defer len(a)			// ERROR "discards result"
+	defer make([]int, 10)		// ERROR "discards result"
+	defer new(int)			// ERROR "discards result"
+	defer real(1i)			// ERROR "discards result"
+	defer unsafe.Alignof(a)		// ERROR "discards result"
+	defer unsafe.Offsetof(s.f)	// ERROR "discards result"
+	defer unsafe.Sizeof(a)		// ERROR "discards result"
 
 	defer close(c)
 	defer copy(a, a)

コアとなるコードの解説

saveorig関数 (src/cmd/gc/const.c)

saveorig関数は、コンパイラの内部表現であるASTノードのorigフィールドを適切に管理するためのユーティリティです。Goコンパイラは、最適化や型チェックの過程でASTノードを変換したり書き換えたりすることがあります。このとき、元のノードの情報を失わないように、origフィールドに元のノードのコピーを保存します。

if(n == n->orig)という条件は、ノードnがまだ変換されておらず、origフィールドが自分自身を指している場合に真となります。この場合、nの現在の状態をn1という新しいノードに複製し、n->origをこの複製されたn1に設定します。これにより、後続の処理でnがさらに変更されても、n->origを通じて元の式(ソースコード上の形)にアクセスできるようになります。これは、エラーメッセージを生成する際に、ユーザーが書いた元のコードに近い形で問題箇所を指摘するために非常に重要です。

checkdefergo関数とtypecheckの変更 (src/cmd/gc/typecheck.c)

checkdefergo関数は、deferまたはgoステートメントの引数がGo言語のセマンティクスに合致しているかを検証する中心的なロジックです。

  1. what変数の設定: deferまたはgoのどちらのコンテキストでエラーが発生したかをメッセージに含めるために、what変数を設定します。
  2. switch文によるオペレーションの分類: n->left->opdefer/goの引数となる式のオペレーション)に基づいて、処理を分岐します。
    • 許可される関数呼び出し: OCALLINTER, OCALLMETH, OCALLFUNCなどの通常の関数呼び出しや、close, copy, delete, panic, print, println, recoverといったdefer/goで許可されている組み込み関数は、// okとして処理を続行します。
    • 戻り値が破棄される組み込み関数: append, cap, complex, imag, len, make, new, real, unsafe.Alignof, unsafe.Offsetof, unsafe.Sizeofなどの組み込み関数は、値を返しますが、defer/goのコンテキストではその戻り値が使用されません。これらの場合、yyerror("%s discards result of %N", what, n->left)というエラーメッセージを出力します。これにより、「go append(a, 0)」のようなコードに対して、「go discards result of append」という、より具体的で分かりやすいエラーメッセージが表示されるようになります。
    • 型変換や無効な関数呼び出し: OLITERALノードがOCONV(型変換)のorigノードを持つ場合(例: int(0))、またはその他のswitchケースに該当しない無効なオペレーションの場合、convラベルにジャンプします。convブロックでは、yyerror("%s requires function call, not conversion", what)というエラーメッセージを出力します。これにより、「defer int(0)」のようなコードに対して、「defer requires function call, not conversion」という明確なエラーメッセージが表示されるようになります。n->diag = 1は、このノードに対してすでにエラーが報告されたことをマークし、重複エラーを防ぎます。

typecheck関数におけるODEFEROPROCの変更は、checkdefergo関数が正しく機能するための前提条件です。typecheck(&n->left, Etop|Erv)のようにErvフラグを追加することで、defer/goの引数となる式が値を返す可能性があることを型チェッカーに明示的に伝えます。これにより、コンパイラは式の評価結果が使用されるべきかどうかをより正確に判断し、checkdefergo関数で詳細なセマンティックチェックを実行できるようになります。

これらの変更は、Goコンパイラがdefergoステートメントの誤用をより正確に検出し、開発者にとって理解しやすいエラーメッセージを提供することで、コードの品質と開発効率を向上させることに貢献しています。

関連リンク

参考にした情報源リンク