[インデックス 1137] ファイルの概要
このコミットは、Go言語における変数宣言とスコープに関する重要な修正を導入しています。具体的には、変数のスコープがその完全な宣言(初期化ステートメントを含む)が完了した後に開始されることを明確にしました。これにより、特に内側のスコープで同名の変数を再宣言(シャドーイング)する際に、初期化式内で外側の変数を正しく参照できるようになります。
コミット
commit b1e8b5f5b715c9a4727bbfb1d32c852c7c8e9122
Author: Ian Lance Taylor <iant@golang.org>
Date: Mon Nov 17 12:19:02 2008 -0800
The scope rules have been clarified to indicate that a
variable may only be named after the complete declaration,
including the initialization statements.
R=gri
DELTA=61 (16 added, 45 deleted, 0 changed)
OCL=19343
CL=19376
---
test/bugs/bug095.go | 43 -------------------------------------------
test/golden.out | 6 ------
test/varinit.go | 20 ++++++++++++++++++++
3 files changed, 20 insertions(+), 49 deletions(-)
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/b1e8b5f5b715c9a4727bbfb1d32c852c7c8e9122
元コミット内容
Go言語のスコープ規則が明確化され、変数は初期化ステートメントを含む完全な宣言が完了した後にのみ名前が付けられる(参照可能になる)ように変更されました。
変更の背景
このコミットは、Go言語の初期段階における変数スコープと宣言のセマンティクスに関する曖昧さを解消するために行われました。特に、内側のスコープで外側のスコープの変数と同名の新しい変数を宣言し、その新しい変数を初期化する際に、初期化式内でどの変数が参照されるかという問題がありました。
以前の実装では、var x int = x + 1; のようなコードにおいて、右辺の x が宣言中の新しい x を参照してしまう可能性があり、これはプログラマの意図(外側の x を参照して初期化する)とは異なる動作を引き起こしていました。test/bugs/bug095.go はこの問題を示すテストケースであり、内側の x が 1 ではなく 2 になってしまうというバグを露呈していました。これは、新しい変数の名前が宣言の途中で「早すぎる」段階でスコープに入ってしまい、初期化式に影響を与えていたためです。
この挙動は、プログラマが期待する「シャドーイング」(内側の変数が外側の変数を隠蔽するが、初期化時には外側の変数を参照できる)とは異なり、混乱やバグの原因となっていました。このコミットは、変数の名前がその完全な宣言(初期化式を含む)が完了した後にのみ有効になるように規則を明確化することで、この問題を解決しました。これにより、初期化式内の同名変数は常に外側のスコープの変数を参照するようになります。
前提知識の解説
変数の宣言と初期化
Go言語では、変数は var キーワードを使って宣言され、同時に初期値を割り当てることができます。
例: var name type = expression
また、Goには「短い変数宣言」という便利な構文もあります。
例: name := expression
これは var name = expression と同等ですが、型推論が行われ、新しい変数を宣言する場合にのみ使用できます。
スコープ
スコープとは、プログラム内で変数が参照できる範囲のことです。Go言語では、変数のスコープは主にブロック({}で囲まれた領域)によって定義されます。関数、if文、forループ、switch文などはそれぞれ独自のブロックを持ち、その中で宣言された変数はそのブロック内でのみ有効です。
シャドーイング (Shadowing)
シャドーイングとは、内側のスコープで外側のスコープの変数と同じ名前の新しい変数を宣言することです。この場合、内側のスコープでは新しい変数が優先され、外側の同名変数は「隠蔽」されます。しかし、シャドーイングされた変数を初期化する際に、その初期化式内で外側の変数を参照できるかどうかが問題となることがあります。
このコミット以前は、Goのスコープ規則がこのシャドーイング時の初期化に関して曖昧であり、var x int = x + 1; のようなコードで、右辺の x が外側の x ではなく、宣言中の新しい x を参照してしまうという問題が発生していました。これは、新しい x の名前が、その初期化が完了する前にスコープに入ってしまっていたためです。
技術的詳細
このコミットの技術的な核心は、Goコンパイラが変数のスコープを決定するタイミングの変更にあります。以前は、変数の名前が宣言の開始と同時にスコープに入ってしまうような挙動がありました。これにより、var x int = x + 1; のような自己参照的な初期化式において、右辺の x が意図せず宣言中の新しい x を参照してしまい、その結果、未初期化の値(Goではゼロ値)が使われたり、予期せぬ計算結果になったりする可能性がありました。
この修正により、Goのコンパイラは、変数の名前がその完全な宣言(var キーワード、変数名、型、そして初期化式全体)が処理され、変数が完全に構築された後にのみスコープに入るように変更されました。これにより、初期化式内で使用される同名の識別子は、常にその宣言の外側にある既存の変数を参照することが保証されます。
具体的には、var x int = x + 1; のような宣言があった場合、右辺の x は、この新しい x がまだ完全にスコープに入っていないため、外側のスコープで定義されている x を参照します。新しい x は、この初期化式が評価され、値が割り当てられた後に初めて、そのブロック内で有効な識別子として認識されます。
この変更は、Go言語のセマンティクスをより直感的で予測可能なものにし、特にネストされたスコープでの変数シャドーイングの挙動を明確にしました。これにより、プログラマは初期化式で外側の変数を安全に参照できるようになり、test/bugs/bug095.go で示されたようなバグが解消されました。
コアとなるコードの変更箇所
このコミットでは、主にテストファイルが変更されています。これは、言語のセマンティクス(コンパイラの挙動)が変更された結果として、既存のバグを示すテストケースが不要になり、新しい正しい挙動を示すテストケースが追加されたためです。
-
test/bugs/bug095.goの削除: このファイルは、変更前のスコープ規則によって引き起こされるバグ(内側のxがx + 1で初期化される際に、外側のxではなく、宣言中の新しいxを参照してしまう問題)を再現するためのテストケースでした。修正によりこのバグが解消されたため、このテストファイルは削除されました。 -
test/golden.outの変更:test/golden.outは、Goのテストスイートにおける期待される出力(エラーメッセージなど)を記録するファイルです。bug095.goの削除に伴い、そのテストが生成していたエラー出力に関するエントリも削除されました。 -
test/varinit.goの追加: この新しいテストファイルは、修正後の正しいスコープ規則と変数初期化の挙動を検証するために追加されました。bug095.goと同様のコード構造を持ちながら、期待される結果が変更されており、内側のxが外側のxの値に基づいて正しく初期化されることを確認しています。
コアとなるコードの解説
削除された test/bugs/bug095.go の内容と問題点
package main
func main() {
var x int = 1;
if x != 1 { panic("found ", x, ", expected 1\\n"); }
{
var x int = x + 1; // scope of x starts too late
if x != 1 { panic("found ", x, ", expected 1\\n"); }
}
{
x := x + 1; // scope of x starts too late
if x != 1 { panic("found ", x, ", expected 1\\n"); }
}
}
このテストケースでは、まず main 関数内で x が 1 で初期化されます。
その後のブロック内で、var x int = x + 1; および x := x + 1; という形で、新しい x が宣言され、初期化されています。
コメント // scope of x starts too late は、このコミット以前のコンパイラの挙動を示唆しています。このコメントは、実際には「新しい x の名前が早すぎる段階でスコープに入ってしまい、初期化式内の x が外側の x ではなく、宣言中の新しい x を参照してしまった」という問題を表しています。
もし右辺の x が外側の x (値は 1) を参照していれば、内側の x は 1 + 1 = 2 となるはずです。しかし、このテストの if x != 1 というチェックと、コメントアウトされた実行結果 found 2, expected 1 から、内側の x が 2 になってしまい、テストが失敗していたことがわかります。これは、初期化式 x + 1 の x が、宣言中の新しい x を参照してしまい、その時点での x のゼロ値(0)に 1 を加算して 1 となるか、あるいは何らかの理由で 2 となってしまったことを示唆しています。特に panic("found ", x, ", expected 1\\n") のメッセージから、x が 2 になったことが原因でパニックが発生していたことが明確です。
追加された test/varinit.go の内容と正しい挙動
package main
func main() {
var x int = 1;
if x != 1 { panic("found ", x, ", expected 1\\n"); }
{
var x int = x + 1;
if x != 2 { panic("found ", x, ", expected 2\\n"); }
}
{
x := x + 1;
if x != 2 { panic("found ", x, ", expected 2\\n"); }\n }\n}\n```
この新しいテストファイルは、`bug095.go` とほぼ同じコードですが、内側の `if` 文の条件が `if x != 2` に変更されています。これは、スコープ規則の修正後、`var x int = x + 1;` および `x := x + 1;` の初期化式において、右辺の `x` が正しく外側のスコープの `x`(値は `1`)を参照するようになったことを示しています。
したがって、内側の `x` は `1 + 1 = 2` となり、`if x != 2` の条件が満たされず、テストが成功するようになりました。この変更は、Go言語の変数宣言とスコープに関するセマンティクスがより直感的で予測可能なものになったことを明確に示しています。
## 関連リンク
* Go言語の仕様 (The Go Programming Language Specification): Go言語の公式仕様は、スコープや宣言に関する詳細な規則を定義しています。このコミットは、その仕様の初期の明確化に貢献したものです。
* [https://go.dev/ref/spec](https://go.dev/ref/spec)
## 参考にした情報源リンク
* Go言語の公式ドキュメント
* Go言語のソースコードリポジトリ (GitHub)
* Go言語のコミット履歴
* Go言語の初期の設計に関する議論(Go mailing list archivesなど)
* Go言語における変数スコープとシャドーイングに関する一般的な解説記事