[インデックス 15089] ファイルの概要
このコミットは、Goコンパイラ(cmd/gc
)におけるdefer
およびgo
ステートメントの誤用に関するエラーメッセージの明確化を目的としています。具体的には、defer
やgo
の引数として型変換や無効な関数呼び出しが指定された場合に、より分かりやすいエラーメッセージを出力するように改善されています。これにより、開発者はdefer
やgo
の誤った使用方法を迅速に特定し、修正できるようになります。
コミット
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の初期のコンパイラでは、defer
やgo
の引数に、関数呼び出しではない式(例えば、型変換の結果や、戻り値が破棄されるべき組み込み関数の呼び出し)が指定された場合に、エラーメッセージが不明瞭であったり、誤解を招くものであったりする問題がありました。
特に、Issue #4654では、defer int(0)
のような型変換をdefer
の引数に指定した場合に、コンパイラが「defer
requires function call, not conversion」(defer
は関数呼び出しを必要とし、型変換ではない)という明確なエラーメッセージではなく、より一般的な「not used」(使用されていない)というメッセージを出力してしまう問題が報告されていました。同様に、go append(x, 1)
のように、戻り値が破棄される組み込み関数の呼び出しに対しても、より適切なエラーメッセージが求められていました。
このコミットは、これらの問題を解決し、開発者がdefer
やgo
の誤用をより簡単に理解し、修正できるようにするために導入されました。
前提知識の解説
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.c
、src/cmd/gc/fmt.c
、src/cmd/gc/typecheck.c
は、それぞれ定数処理、フォーマット、型チェックのフェーズを担当するファイルです。
型変換と組み込み関数
Go言語では、int(0)
のように型名を関数のように使用して、ある型から別の型へ値を変換することができます。また、len()
, cap()
, append()
, make()
, new()
, complex()
, real()
, imag()
, unsafe.Alignof()
, unsafe.Offsetof()
, unsafe.Sizeof()
などの組み込み関数は、値を返しますが、その戻り値が常に使用されるとは限りません。defer
やgo
の引数としてこれらの関数が呼び出された場合、その戻り値は破棄されることになります。
抽象構文木 (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)
のようなケースで、より具体的なエラーメッセージが表示されるようになります。
-
ODEFER
とOPROC
の型チェックの変更:typecheck
関数内で、ODEFER
(defer
ステートメント)とOPROC
(go
ステートメント)の処理が変更されました。- 以前は
typecheck(&n->left, Etop)
またはtypecheck(&n->left, Etop|Eproc)
が呼び出されていましたが、これにErv
(式が値を返すことを示すフラグ)が追加され、typecheck(&n->left, Etop|Erv)
またはtypecheck(&n->left, Etop|Eproc|Erv)
となりました。これにより、defer
やgo
の引数となる式が値を返す可能性があることを型チェッカーに伝え、その後のcheckdefergo
関数での詳細なチェックを可能にしています。 typecheck
の呼び出し後、n->left->diag
が設定されていない(つまり、すでにエラーが報告されていない)場合にのみcheckdefergo(n)
が呼び出されるようになりました。これにより、重複したエラーメッセージの出力を防ぎます。
- 以前は
-
yyerror
メッセージの変更: 一般的な「%N not used
」エラーメッセージが、「%N evaluated but not used
」に変更されました。これは、式が評価されたがその結果が使用されなかったという状況をより正確に表現しています。
src/cmd/gc/const.c
の変更
saveorig
関数の導入:saveorig
という新しいヘルパー関数が導入されました。この関数は、ノードn
のorig
フィールドがn
自身を指している場合(つまり、まだ元のノードが複製されていない場合)に、n
のコピーを作成してn->orig
に設定します。これにより、ノードが変換された後でも、元のノードの情報を保持し、エラーメッセージなどで元の式を正確に表示できるようになります。isconst
、ret
、settrue
、setfalse
などの関数で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
: このテストファイルでは、go
やdefer
の引数として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コンパイラはdefer
やgo
ステートメントの誤用に対して、より具体的で分かりやすいエラーメッセージを提供するようになり、開発者のデバッグ体験が向上しました。
コアとなるコードの変更箇所
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言語のセマンティクスに合致しているかを検証する中心的なロジックです。
what
変数の設定:defer
またはgo
のどちらのコンテキストでエラーが発生したかをメッセージに含めるために、what
変数を設定します。switch
文によるオペレーションの分類:n->left->op
(defer
/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
関数におけるODEFER
とOPROC
の変更は、checkdefergo
関数が正しく機能するための前提条件です。typecheck(&n->left, Etop|Erv)
のようにErv
フラグを追加することで、defer
/go
の引数となる式が値を返す可能性があることを型チェッカーに明示的に伝えます。これにより、コンパイラは式の評価結果が使用されるべきかどうかをより正確に判断し、checkdefergo
関数で詳細なセマンティックチェックを実行できるようになります。
これらの変更は、Goコンパイラがdefer
やgo
ステートメントの誤用をより正確に検出し、開発者にとって理解しやすいエラーメッセージを提供することで、コードの品質と開発効率を向上させることに貢献しています。
関連リンク
- Go Issue #4654: cmd/gc: clearer error for defer/go of conversion or invalid function call
- Go Code Review 7229072: https://golang.org/cl/7229072
参考にした情報源リンク
- Go言語の公式ドキュメント: https://go.dev/
- Go言語の
defer
ステートメントに関するブログ記事: https://go.dev/blog/defer-panic-and-recover - Go言語の並行処理に関するドキュメント: https://go.dev/doc/effective_go#concurrency
- Goコンパイラのソースコード: https://github.com/golang/go
- Goコンパイラの内部構造に関する一般的な情報源 (例: Go compiler internals, Go AST):
- "Go compiler internals" (検索クエリ)
- "Go abstract syntax tree" (検索クエリ)
- Go言語のASTパッケージのドキュメント: https://pkg.go.dev/go/ast
- Go言語の型システムに関するドキュメント: https://go.dev/ref/spec#Types