[インデックス 1868] ファイルの概要
コミット
このコミット 86145611b0ad8c6ef6923f65f8a4fd39f07f69d7
は、Go言語のランタイムにおいて、nil
マップに対する range
操作を許可するように変更を加えるものです。具体的には、nil
マップを range
でイテレートしようとした際に、パニックを起こすことなく、ループ本体が一度も実行されないように振る舞いを修正しています。
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/86145611b0ad8c6ef6923f65f8a4fd39f07f69d7
元コミット内容
commit 86145611b0ad8c6ef6923f65f8a4fd39f07f69d7
Author: Russ Cox <rsc@golang.org>
Date: Mon Mar 23 18:32:37 2009 -0700
allow range on nil maps
R=ken
OCL=26663
CL=26663
変更の背景
Go言語の初期のバージョンでは、nil
マップに対して range
キーワードを使ってイテレーションを試みると、ランタイムパニックが発生していました。これは、マップが初期化されていない状態(nil
)であるにもかかわらず、イテレーションを開始しようとすることで、内部的なデータ構造への不正なアクセスが発生するためです。
しかし、Go言語の設計思想として、可能な限りパニックを避け、より予測可能な挙動を提供することが重視されます。特に、スライスやチャネルといった他の組み込み型では、nil
の状態であっても range
でイテレートした場合にパニックを起こさず、単にループ本体が実行されないという挙動が一般的でした。マップだけがこの一貫性から外れていたため、開発者にとっては不便であり、また予期せぬパニックの原因となる可能性がありました。
このコミットは、このような一貫性の欠如を解消し、nil
マップに対する range
操作も他の nil
スライスやチャネルと同様に、パニックを起こさずに安全に処理されるようにするために導入されました。これにより、開発者は nil
チェックを明示的に行わなくても、nil
マップを安心して range
で扱えるようになり、コードの簡潔性と堅牢性が向上します。
前提知識の解説
Go言語のマップ (map)
Go言語のマップは、キーと値のペアを格納するハッシュテーブルの実装です。make
関数で初期化するか、マップリテラルで宣言・初期化することで使用できます。マップは参照型であり、変数がマップを指し示していない状態を nil
と呼びます。
var m map[string]int // m は nil マップ
m = make(map[string]int) // m は空のマップ
m = map[string]int{"a": 1} // m は初期化され、要素を持つマップ
nil
の概念
Go言語における nil
は、ポインタ、チャネル、関数、インターフェース、マップ、スライスといった参照型の「ゼロ値」です。これは、それらの変数がまだ有効なメモリ上のオブジェクトを指していない状態を示します。nil
の参照型に対して操作を行おうとすると、多くの場合ランタイムパニックが発生します。
range
キーワード
range
キーワードは、Go言語でスライス、配列、文字列、マップ、チャネルをイテレート(反復処理)するために使用されます。
- スライス/配列/文字列: インデックスと要素のコピーを返します。
- マップ: キーと値のコピーを返します。イテレーションの順序は保証されません。
- チャネル: チャネルがクローズされるまで値を受信します。
range
は、イテレート対象が nil
の場合でも、スライスやチャネルではパニックを起こさずにループ本体が実行されないという挙動を示します。このコミット以前は、マップだけがこの挙動から外れていました。
Goランタイムとハッシュマップの実装
Go言語のマップは、内部的にはランタイム(src/runtime
ディレクトリ内のC言語またはGo言語のコード)で実装されています。ハッシュテーブルのデータ構造や、要素の追加、削除、検索、イテレーションといった操作は、このランタイムコードによって管理されます。
src/runtime/hashmap.c
: マップの基本的な操作(作成、要素の追加・削除、イテレーションなど)をC言語で実装しているファイルです。Hmap
: マップの内部表現を表す構造体です。hash_iter
: マップのイテレーション状態を保持する構造体です。sys·mapiterinit
: マップのイテレーションを開始する際に呼び出されるランタイム関数です。この関数は、イテレータを初期化し、最初の要素を準備します。
技術的詳細
このコミットの技術的な核心は、Goランタイムの src/runtime/hashmap.c
ファイルにある sys·mapiterinit
関数に nil
マップのチェックを追加した点です。
sys·mapiterinit
関数は、Goのコードで for ... range map
のようなイテレーションが開始される際に、ランタイムによって呼び出される内部関数です。この関数は、イテレーションに必要な hash_iter
構造体を初期化します。
変更前は、h
(マップのポインタ) が nil
であるかどうかのチェックが行われていませんでした。そのため、nil
マップに対して sys·mapiterinit
が呼び出されると、nil
ポインタ h
を介して内部データにアクセスしようとし、結果としてセグメンテーション違反などのランタイムパニックを引き起こしていました。
このコミットでは、sys·mapiterinit
関数の冒頭に以下のチェックが追加されました。
if(h == nil) {
it->data = nil;
return;
}
このコードは、渡されたマップのポインタ h
が nil
であるかどうかを確認します。もし h
が nil
であれば、イテレータの data
フィールドを nil
に設定し、すぐに return
します。これにより、イテレーションは開始されず、hash_next
などの後続の処理も実行されません。結果として、nil
マップに対する range
ループは、ループ本体が一度も実行されることなく正常に終了するようになります。
it->data = nil;
の設定は重要です。hash_next
関数は、イテレータの data
フィールドが nil
でない限り、次の要素を探し続けます。nil
マップの場合、data
を nil
に設定することで、イテレーションが開始される前に「要素がない」状態を明示的に示し、ループが即座に終了するようにします。
この変更は、Go言語のランタイムレベルで行われるため、Go言語で書かれたすべてのプログラムに透過的に適用されます。開発者は、nil
マップを range
で安全に扱えるようになり、コードの記述がより簡潔になります。
コアとなるコードの変更箇所
src/runtime/hashmap.c
--- a/src/runtime/hashmap.c
+++ b/src/runtime/hashmap.c
@@ -870,6 +870,10 @@ sys·mapassign2(Hmap *h, ...)\
void
sys·mapiterinit(Hmap *h, struct hash_iter *it)
{
+ if(h == nil) {
+ it->data = nil;
+ return;
+ }
hash_iter_init(h, it);
it->data = hash_next(it);
if(debug) {
test/map.go
--- a/test/map.go
+++ b/test/map.go
@@ -487,4 +487,10 @@ func main() {
fmt.Printf("update mipM[%d][%d] = %i\\n", i, i, mipM[i][i]);
}
}\
+
+ // test range on nil map
+ var mnil map[string] int;
+ for x, y := range mnil {
+ panic("range mnil");
+ }
}
コアとなるコードの解説
src/runtime/hashmap.c
の変更
sys·mapiterinit
関数は、マップのイテレーションを開始するGoランタイムの内部関数です。
追加された以下の4行がこのコミットの核心です。
if(h == nil) {
it->data = nil;
return;
}
if(h == nil)
: これは、イテレーション対象のマップh
がnil
であるかどうかをチェックしています。h
はHmap
型のポインタで、Goのマップの内部表現を指します。it->data = nil;
:it
はhash_iter
型のポインタで、マップのイテレータの状態を保持します。it->data
は現在のイテレーションで取得されるべき要素のデータへのポインタです。nil
マップの場合、要素は存在しないため、data
をnil
に設定します。これにより、イテレーションループが次の要素を探す際に、要素がないことを認識し、即座に終了するようになります。return;
:nil
マップの場合、これ以上イテレータを初期化したり、要素を探したりする必要がないため、関数をすぐに終了します。
この変更により、nil
マップに対する range
操作は、パニックを起こすことなく、ループ本体が一度も実行されないという、Go言語の他の nil
スライスやチャネルと同様の一貫した挙動を示すようになります。
test/map.go
の変更
テストファイル test/map.go
には、nil
マップに対する range
の新しいテストケースが追加されています。
// test range on nil map
var mnil map[string] int;
for x, y := range mnil {
panic("range mnil");
}
var mnil map[string] int;
:mnil
という名前のstring
からint
へのマップを宣言しています。マップは宣言時に初期化されない場合、そのゼロ値はnil
となります。したがって、mnil
はnil
マップです。for x, y := range mnil { ... }
:nil
であるmnil
マップに対してrange
ループを実行しています。panic("range mnil");
: この行は、もしnil
マップに対するrange
ループの本体が実行されてしまった場合に、テストを失敗させるためのものです。
このテストケースの意図は、このコミットによる変更が正しく機能していることを確認することです。つまり、nil
マップに対して range
を実行しても、ループ本体(panic("range mnil");
の行)が実行されないことを期待しています。もしこの panic
が発生すれば、それは変更が正しく適用されていないか、意図しない挙動が発生していることを意味します。このテストが成功するということは、nil
マップに対する range
がパニックを起こさずに、ループ本体をスキップして正常に終了することを示します。
関連リンク
- Go言語のマップに関する公式ドキュメント: https://go.dev/blog/maps
- Go言語の
range
キーワードに関する公式ドキュメント: https://go.dev/tour/flowcontrol/10
参考にした情報源リンク
- Go言語のソースコード (特に
src/runtime/hashmap.c
): https://github.com/golang/go/blob/master/src/runtime/hashmap.go (現在のGoバージョンではhashmap.c
はhashmap.go
に置き換えられています。しかし、当時の実装はC言語でした。) - Go言語のテストコード (特に
test/map.go
): https://github.com/golang/go/blob/master/test/map.go - Go言語の
nil
について: https://go.dev/doc/effective_go#zero_value - Go言語の
range
ループのセマンティクスに関する議論 (当時のメーリングリストやIssueトラッカーなど、具体的なリンクはコミット情報からは特定できませんが、Goの設計に関する一般的な情報源として): https://groups.google.com/g/golang-nuts - Go言語のランタイムに関する一般的な情報: https://go.dev/doc/go1.18#runtime (Goのバージョンアップに伴いランタイムの詳細は変化しますが、基本的な概念は共通です。)
- Go言語の歴史と設計思想: https://go.dev/doc/faq I have generated the detailed technical explanation in Markdown format, following all the specified instructions and chapter structure. The output is provided to standard output as requested.