Goのマップは内部的には1つのポインタのようなものである。ポインタの値そのものがnilのマップと、実体がメモリ上に割り当てられているがその中身が空のマップとは、似て非なるものである。nilマップを読もうとするとそれは空のマップのように振る舞うが、空のマップとは異なりnilマップには値をセットすることはできない。
var m1 map[string]int // m1はnilマップ
m2 := make(map[string]int) // m2は空のマップ
Goのマップでは、存在しないキーを参照したときには値のゼロ値(デフォルトの値)が返ってくることになっている。たとえば下のようなマップで存在しないキーを参照すると0が返ってくる。0はint型のゼロ値だ。
m := make(map[string]int) // キーがstring、値がintのマップ
i := m["nosuchkey"] // iは0
マップのゼロ値はnilマップだ。だからマップのマップをアクセスしたとき、キーが存在しないと、nilマップが返ってくることになる。
var m map[string]map[string]int
// vはmap[string]int型のnilマップ
v := m["foo"]
// wはmap[string]int型のnilマップにアクセスした結果
// (値のゼロ値 -- つまりこの場合int型の0)
w := m["foo"]["bar"]
さて、map[string]map[string]intは多次元マップだったわけだけど、値を読むぶんにはだいたい期待通りに動いていると言っていいと思う。しかし値をセットしようと思うとこの多次元マップは期待通りにはうまく動いてくれない。どうしてかというと、nilマップには(空のマップとは異なり)値をセットするすることができないからだ。
var m map[string]map[string]int
// m["foo"]がnilなのでnilマップにセットできずパニックする
m["foo"]["bar"] = 42
この制限を回避するためにはかなり回りくどい書き方をしなければいけない。nilマップにセットしようとするとエラーになるわけだから、親のマップのキーが存在しているかどうかをまずチェックして、存在していなければ空のマップを割り当ててそれをそのキーにセットすればよい。とはいえどうみてもこの書き方は面倒だ。
m := make(map[string]map[string]int)
if _, exist := m["foo"]; !exist {
m["foo"] = make(map[string]int)
}
m["foo"]["bar"] = 42
ではどうすればいいかというと、多次元マップの代わりに1次元マップを使えばよい。複数のキーをまとめた構造体を作ってそれを1次元マップのキーにすることによって、実質的に多次元マップのようなものを単純なマップで実現することができる。コードで表すとこういうようになる。
type key struct {
k1, k2 string
}
m := make(map[key]int)
m[key{"foo", "bar"}] = 42
多次元マップのキーが1つの構造体に圧縮されて1次元になっているのがわかると思う。このテクニックを使うと多次元マップと同等の機能を簡単に実現することができる。