panic: assignment to entry in nil map

原因

Go のプリミティブ型の一つ map は値型だと勘違いしていて、こんな runtime error と出会いました。

var m map[int]int // a map variable
m[0] = 1 // panic: assignment to entry in nil map

map は参照型でした。nil 値 (nil map) があります。実体は make か literal で作ります。

var a = make(map[int]int) // make an (initialized) map value
a[0] = 1
var b = map[int]int{} // a map literal value
b[0] = 1

注意点

Java 等の参照型主体の言語を使う人にはむしろ自然に見えるかもしれないが、次のような挙動は流石にきもいので気をつけて使ったほうが良さそう。

var m = *new(map[int]int)
println(m[0]) // 0
m[0] = 1 // panic: assignment to entry in nil map

new は non-nil な値を返しますが initialized value は作られず、nil map は empty map のように振る舞い、0で呼び出すと既定動作として初期値が返ります。そのあと代入して初めて落ちます。nil map は内部表現の実体が無く要素を追加することだけができず、挙動は似ていても nil map と empty map は区別されます。

var a = map[int]int(nil)
var b = map[int]int{}
println(a[0], a == nil) // 0 true
println(b[0], b == nil) // 0 false
// println(a == b) // invalid operation: a == b (map can only be compared to nil)
a[0] = 1
b[0] = 1 // panic: assignment to entry in nil map

なお、参照型なので普通に変数や引数にコピーを取ると実体は共有されます。

var a = map[int]int{}
a[0] = 1
var b = map[int]int(a)
b[0] = 2
println(a[0]) // 2

動作は仕様 Map types にさっくりと書いてあります。他には slice や channel も参照型で、それらも変数や new の初期値は nil で、当然 empty とも区別されます。

言い訳

なぜ勘違いしたかというと、多くの参照型を扱う言語と違って、Go にはポインタ型と強い型付けがあり C のように値とポインタを区別して扱うので、参照として扱いたい部分にはポインタを使うことで実現でき、特別な理由がない限り言語としては参照型は必要ないからです。ユーザ定義型 struct, interface だけでなく array も値型です (string, function は immutable)。

特別な理由とは例えば、slice は pointer と同じく本質的に他の値を参照する機能を持っているので参照型(逆に C 等のポインタと配列型がもつ配列を参照する機能は Go の pointer にはない)。channel は、通信は普通実体を共有し、実体の違うコピーが取れるのは直感に反するので、毎回ポインタを使うコストも掛かる uncopyable な型を作るよりは参照型の方が妥当。とかたぶんそんな感じです。

一方 map は、C++ ののように連想配列自体は値型で実現できます。Go にポインタ型がある以上それなりの理由がないと参照型ににはならないと思っていたので値型だと思い込んでいました。array が値型でも map は参照型の方が良いという人はいるんでしょうか。