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

[インデックス 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;
	}

このコードは、渡されたマップのポインタ hnil であるかどうかを確認します。もし hnil であれば、イテレータの data フィールドを nil に設定し、すぐに return します。これにより、イテレーションは開始されず、hash_next などの後続の処理も実行されません。結果として、nil マップに対する range ループは、ループ本体が一度も実行されることなく正常に終了するようになります。

it->data = nil; の設定は重要です。hash_next 関数は、イテレータの data フィールドが nil でない限り、次の要素を探し続けます。nil マップの場合、datanil に設定することで、イテレーションが開始される前に「要素がない」状態を明示的に示し、ループが即座に終了するようにします。

この変更は、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): これは、イテレーション対象のマップ hnil であるかどうかをチェックしています。hHmap 型のポインタで、Goのマップの内部表現を指します。
  • it->data = nil;: ithash_iter 型のポインタで、マップのイテレータの状態を保持します。it->data は現在のイテレーションで取得されるべき要素のデータへのポインタです。nil マップの場合、要素は存在しないため、datanil に設定します。これにより、イテレーションループが次の要素を探す際に、要素がないことを認識し、即座に終了するようになります。
  • 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 となります。したがって、mnilnil マップです。
  • for x, y := range mnil { ... }: nil である mnil マップに対して range ループを実行しています。
  • panic("range mnil");: この行は、もし nil マップに対する range ループの本体が実行されてしまった場合に、テストを失敗させるためのものです。

このテストケースの意図は、このコミットによる変更が正しく機能していることを確認することです。つまり、nil マップに対して range を実行しても、ループ本体(panic("range mnil"); の行)が実行されないことを期待しています。もしこの panic が発生すれば、それは変更が正しく適用されていないか、意図しない挙動が発生していることを意味します。このテストが成功するということは、nil マップに対する range がパニックを起こさずに、ループ本体をスキップして正常に終了することを示します。

関連リンク

参考にした情報源リンク