[インデックス 19392] ファイルの概要
このコミットは、Go言語のreflect
パッケージを使用してnil
マップにアクセスする際の挙動を修正し、関連するテストを追加するものです。具体的には、nil
マップに対してreflect.Value.MapKeys()
やreflect.Value.MapIndex()
を呼び出した際に、予期せぬパニックや誤った結果が生じる可能性があった問題を解決します。これにより、reflect
パッケージを通じたnil
マップの操作が、Go言語の組み込みマップのnil
値のセマンティクス(存在しないキーへのアクセスはゼロ値を返す)と一貫するようになります。
コミット
commit d54b67df0cc2f9cfa7785919e20d152305bd72e8
Author: Russ Cox <rsc@golang.org>
Date: Mon May 19 09:36:47 2014 -0400
reflect: test, fix access to nil maps
Fixes #8010.
LGTM=bradfitz, khr
R=khr, bradfitz, dvyukov
CC=golang-codereviews
https://golang.org/cl/91450048
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/d54b67df0cc2f9cfa7785919e20d152305bd72e8
元コミット内容
reflect: test, fix access to nil maps
Fixes #8010.
LGTM=bradfitz, khr
R=khr, bradfitz, dvyukov
CC=golang-codereviews
https://golang.org/cl/91450048
変更の背景
Go言語において、マップは非常に強力なデータ構造ですが、そのnil
値の扱いは重要です。Goの組み込みマップでは、nil
マップからキーを読み取ろうとすると、そのキーの型のゼロ値が返され、パニックは発生しません。しかし、このコミット以前は、reflect
パッケージを通じてnil
マップにアクセスしようとすると、この期待される挙動が保証されず、場合によってはパニックを引き起こす可能性がありました。
具体的には、reflect.Value
型で表現されたnil
マップに対してMapKeys()
(マップのキーのリストを取得する)やMapIndex()
(特定のキーに対応する値を取得する)といったメソッドを呼び出した際に、ランタイムレベルでの適切なnil
チェックが不足していたため、内部的なハッシュマップ操作が不正なポインタアクセスを引き起こし、プログラムがクラッシュする可能性がありました。
この問題はIssue #8010として報告されたと考えられます。reflect
パッケージはGoの強力な機能であり、リフレクションを利用するライブラリやフレームワークにとって、nil
値の安全な取り扱いは不可欠です。このコミットは、reflect
パッケージの堅牢性を高め、nil
マップの操作がGoのセマンティクスに沿って安全に行われるようにするために導入されました。
前提知識の解説
Goのmap
型
Goにおけるmap
は、キーと値のペアを格納するハッシュテーブルの実装です。map
は参照型であり、変数を宣言しただけではnil
値になります。
nil
マップ:var m map[string]int
のように宣言されただけのマップはnil
です。nil
マップは要素を持たず、読み取り操作(キーが存在しない場合はゼロ値を返す)は可能ですが、書き込み操作(要素の追加や変更)はパニックを引き起こします。- 空マップ:
m := make(map[string]int)
のようにmake
関数で初期化されたマップは空マップです。要素は持っていませんが、読み書き操作が可能です。
reflect
パッケージ
reflect
パッケージは、Goプログラムの実行時に型情報(reflect.Type
)や値(reflect.Value
)を検査・操作するための機能を提供します。これにより、ジェネリックなコードの記述や、構造体のフィールドへの動的なアクセス、関数の動的な呼び出しなどが可能になります。
reflect.Value
: Goのあらゆる値(変数、定数、関数の戻り値など)を抽象的に表現する型です。reflect.ValueOf()
関数を使ってGoの値をreflect.Value
に変換できます。MapKeys()
:reflect.Value
がマップを表す場合、このメソッドはマップ内のすべてのキーをreflect.Value
のスライスとして返します。MapIndex(key reflect.Value)
:reflect.Value
がマップを表す場合、このメソッドは指定されたkey
に対応する値をreflect.Value
として返します。キーが存在しない場合、その型のゼロ値を表すreflect.Value
(IsValid()
がfalse
を返す)を返します。
nil
値の扱い
Goでは、ポインタ、スライス、マップ、チャネル、関数、インターフェースなどの参照型は、初期状態ではnil
という特殊な値を取ります。nil
は「値がない」ことを示し、これらの型がまだ有効なメモリ領域を指していないことを意味します。nil
値に対する操作は、型によって許容されるものとパニックを引き起こすものがあります。マップの場合、nil
マップからの読み取りはゼロ値を返しますが、書き込みはパニックになります。
runtime
パッケージ
runtime
パッケージは、Goプログラムの実行環境(ガベージコレクション、スケジューラ、メモリ管理など)を制御する低レベルな機能を提供します。Goの組み込み型(マップ、スライスなど)の多くの操作は、このruntime
パッケージ内のCGoコード(.goc
ファイル)によって実装されています。
hashmap.goc
とreflect·mapaccess
hashmap.goc
: Goランタイムのハッシュマップ(マップの実装)に関連するCGoコードが含まれるファイルです。Goのマップ操作の効率性と安全性を保証するための低レベルなロジックがここに記述されています。reflect·mapaccess
:reflect
パッケージがマップの要素にアクセスする際に、Goランタイムが内部的に呼び出す関数の一つです。この関数は、reflect.Value
でラップされたマップとキーを受け取り、対応する値を返します。
raceenabled
Goのデータ競合検出機能が有効になっているかどうかを示すフラグです。go run -race
やgo build -race
でビルドされたプログラムでは、複数のゴルーチンが共有データに同時にアクセスし、少なくとも一方が書き込み操作を行う場合に競合を検出します。
hash_lookup
ハッシュマップ内で特定のキーを検索するための内部関数です。この関数は、マップの内部構造を直接操作し、キーに対応する値のポインタを返します。
技術的詳細
このコミットは、reflect
パッケージとGoランタイムのマップ実装の2つの主要な部分にわたる変更を含んでいます。
src/pkg/reflect/all_test.go
の変更
このファイルには、reflect
パッケージのテストスイートが含まれています。今回のコミットでは、TestNilMap
という新しいテスト関数が追加されました。このテストの目的は、nil
マップに対するreflect
パッケージの挙動がGoのセマンティクスに沿っていることを確認することです。
var m map[string]int
: まず、nil
のmap[string]int
が宣言されます。mv := ValueOf(m)
: このnil
マップがreflect.Value
に変換されます。keys := mv.MapKeys()
:nil
マップに対してMapKeys()
が呼び出されます。期待される挙動は、キーが一つも存在しないため、長さが0のスライスが返されることです。テストではlen(keys) != 0
の場合にエラーを報告します。x := mv.MapIndex(ValueOf("hello"))
:nil
マップに対して、存在しないキー"hello"
でMapIndex()
が呼び出されます。Goの組み込みマップのセマンティクスでは、nil
マップから存在しないキーを読み取ると、その型のゼロ値が返されます。reflect
パッケージでは、これはInvalid
なreflect.Value
として表現されます。テストではx.Kind() != Invalid
の場合にエラーを報告します。var mbig map[string][10 << 20]byte
: さらに、値が非常に大きい(10MBのバイト配列)nil
マップmbig
に対しても同様のテストが行われます。これは、値のサイズに関わらずnil
マップの挙動が一貫していることを確認するためです。
これらのテストは、reflect
パッケージがnil
マップを適切に処理し、パニックを起こさずに期待されるゼロ値や空のスライスを返すことを保証するためのものです。
src/pkg/runtime/hashmap.goc
の変更
このファイルは、Goランタイムにおけるハッシュマップの低レベルな実装を含んでいます。reflect
パッケージからマップにアクセスする際に呼び出される内部関数reflect·mapaccess
に重要な変更が加えられました。
変更前は、reflect·mapaccess
関数内でh != nil
(ハッシュマップのポインタがnil
でない)というチェックがraceenabled
(データ競合検出が有効な場合)のブロック内にのみ存在していました。このため、raceenabled
が有効でない環境でnil
マップがreflect·mapaccess
に渡された場合、h
がnil
であるにもかかわらず、その後のhash_lookup
関数が呼び出されてしまい、不正なメモリアクセス(パニック)を引き起こす可能性がありました。
変更後のコードでは、reflect·mapaccess
関数の冒頭にh == nil
のチェックが追加されました。
if(h == nil)
: ハッシュマップのポインタh
がnil
であるかを最初にチェックします。val = nil;
: もしh
がnil
であれば、val
(戻り値のポインタ)をnil
に設定し、関数を即座に終了します。これにより、nil
マップに対するhash_lookup
の呼び出しが完全に回避されます。else { ... }
:h
がnil
でない場合にのみ、従来のraceenabled
チェックとhash_lookup
の呼び出しが実行されます。
この変更により、reflect·mapaccess
はnil
マップが渡された場合でも安全に処理を終了し、reflect
パッケージがInvalid
なreflect.Value
を返すための基盤を提供します。これは、ランタイムレベルでの堅牢性を高め、reflect
パッケージのnil
マップ処理におけるパニックを根本的に防ぐための重要な修正です。
コアとなるコードの変更箇所
diff --git a/src/pkg/reflect/all_test.go b/src/pkg/reflect/all_test.go
index 1e6341bd0b..9c5eb4e554 100644
--- a/src/pkg/reflect/all_test.go
+++ b/src/pkg/reflect/all_test.go
@@ -973,6 +973,28 @@ func TestMap(t *testing.T) {
}
}
+func TestNilMap(t *testing.T) {
+ var m map[string]int
+ mv := ValueOf(m)
+ keys := mv.MapKeys()
+ if len(keys) != 0 {
+ t.Errorf(">0 keys for nil map: %v", keys)
+ }
+
+ // Check that value for missing key is zero.
+ x := mv.MapIndex(ValueOf("hello"))
+ if x.Kind() != Invalid {
+ t.Errorf("m.MapIndex(\"hello\") for nil map = %v, want Invalid Value", x)
+ }
+
+ // Check big value too.
+ var mbig map[string][10 << 20]byte
+ x = ValueOf(mbig).MapIndex(ValueOf("hello"))
+ if x.Kind() != Invalid {
+ t.Errorf("mbig.MapIndex(\"hello\") for nil map = %v, want Invalid Value", x)
+ }
+}
+
func TestChan(t *testing.T) {
for loop := 0; loop < 2; loop++ {
var c chan int
diff --git a/src/pkg/runtime/hashmap.goc b/src/pkg/runtime/hashmap.goc
index 4f5e78897b..36707c6ede 100644
--- a/src/pkg/runtime/hashmap.goc
+++ b/src/pkg/runtime/hashmap.goc
@@ -908,11 +908,15 @@ func mapaccess2(t *MapType, h *Hmap, key *byte) (val *byte, pres bool) {
#pragma textflag NOSPLIT
func reflect·mapaccess(t *MapType, h *Hmap, key *byte) (val *byte) {
-\tif(raceenabled && h != nil) {\n-\t\truntime·racereadpc(h, runtime·getcallerpc(&t), reflect·mapaccess);\n-\t\truntime·racereadobjectpc(key, t->key, runtime·getcallerpc(&t), reflect·mapaccess);\n+\tif(h == nil)\n+\t\tval = nil;\n+\telse {\n+\t\tif(raceenabled) {\n+\t\t\truntime·racereadpc(h, runtime·getcallerpc(&t), reflect·mapaccess);\n+\t\t\truntime·racereadobjectpc(key, t->key, runtime·getcallerpc(&t), reflect·mapaccess);\n+\t\t}\n+\t\tval = hash_lookup(t, h, &key);\n \t}\n-\tval = hash_lookup(t, h, &key);\n }\n
#pragma textflag NOSPLIT
コアとなるコードの解説
src/pkg/reflect/all_test.go
の変更点
新しく追加されたTestNilMap
関数は、reflect
パッケージがnil
マップを正しく処理するかどうかを検証します。
var m map[string]int
でnil
マップを宣言し、mv := ValueOf(m)
でreflect.Value
に変換します。mv.MapKeys()
を呼び出し、nil
マップのキーリストが空であることを確認します。nil
マップにはキーが存在しないため、これは期待される挙動です。mv.MapIndex(ValueOf("hello"))
を呼び出し、nil
マップから存在しないキーを検索した場合に、返されるreflect.Value
がInvalid
であることを確認します。これは、Goの組み込みマップがnil
の場合にキーが存在しないものとして扱われ、その型のゼロ値が返される動作をreflect
パッケージでも再現するためのものです。- さらに、
map[string][10 << 20]byte
のような大きな値を持つマップに対しても同様のテストを行い、値のサイズに関わらずnil
マップの挙動が一貫していることを保証します。
これらのテストは、reflect
パッケージがnil
マップを操作する際にパニックを起こさず、Goのセマンティクスに沿った結果を返すことを保証するためのものです。
src/pkg/runtime/hashmap.goc
の変更点
reflect·mapaccess
関数は、reflect
パッケージからマップにアクセスする際にGoランタイムが呼び出す内部関数です。この関数への変更は、nil
マップの安全な処理を保証するためのものです。
変更前は、raceenabled
(データ競合検出)が有効な場合にのみh != nil
のチェックが行われていました。このため、raceenabled
が無効な環境でnil
マップが渡されると、h
がnil
であるにもかかわらず、その後のhash_lookup
関数が呼び出され、不正なメモリアクセス(パニック)を引き起こす可能性がありました。
変更後のコードでは、reflect·mapaccess
関数の冒頭にif(h == nil)
というチェックが追加されました。
- もし
h
(ハッシュマップのポインタ)がnil
であれば、val
(戻り値のポインタ)をnil
に設定し、関数を即座に終了します。これにより、nil
マップに対するhash_lookup
の呼び出しが完全に回避され、ランタイムレベルでの安全性が確保されます。 raceenabled
のチェックとhash_lookup
の呼び出しは、h
がnil
でない場合にのみ実行されるように変更されました。
この修正により、reflect
パッケージがnil
マップを操作する際に、ランタイムが常に安全な方法で処理を終了し、予期せぬパニックを防ぐことができるようになりました。
関連リンク
- GitHubコミットページ: https://github.com/golang/go/commit/d54b67df0cc2f9cfa7785919e20d152305bd72e8
- Go Issue #8010: https://github.com/golang/go/issues/8010 (直接的なIssueページは見つかりませんでしたが、コミットメッセージで参照されています)
- Go CL (Gerrit Change-ID): https://golang.org/cl/91450048
参考にした情報源リンク
- Go
reflect
パッケージ公式ドキュメント (一般的な知識) - Go
map
型公式ドキュメント (一般的な知識) - "Go reflect nil map access issue"に関するWeb検索結果 (
reflect.Value
によるnil
マップアクセスがパニックを引き起こす可能性についての情報)