map とゼロ値
読了目安 約3分
map の走査順序が保証されないことを実行して確かめ、宣言しただけの map への書き込みが panic する理由をゼロ値と nil から理解します。
この章の目次
前章の slice と同じく、map も中身はポインタです。 そのことが、走査の順序と、宣言しただけの変数への書き込みで表に出ます。
map の計算量と順序
map の読み書きが平均 O(1) であることは、競プロの感覚どおりです。
ただし、map を range で走査する順序は保証されず、Go は実行のたびに順序をわざと変えます。
package main
import "fmt"
func main() {
stock := map[string]int{"apple": 3, "banana": 5, "cherry": 7}
for i := 0; i < 3; i++ {
for name, n := range stock {
fmt.Printf("%s=%d ", name, n)
}
fmt.Println()
}
}同じ map を 3 回走査しているのに、順序が揃わないはずです(偶然揃ったら何度か実行してください)。 「たまたま挿入順に出てきた」ことに依存したコードは、別の実行で壊れます。
ゼロ値と nil
Go では、変数を宣言しただけでゼロ値(型ごとに決まった初期値)が入ります。 int なら 0、string なら空文字列です。
では、ポインタを含む slice や map のゼロ値は何でしょうか。 指し示す先がまだ決まっていないので、「どこも指していない」という状態になります。 これを表す値が nil です。
住所の紙でいえば、白紙の紙です。 紙は手元にあるので変数としては存在しますが、行き先がありません。
- slice と map のゼロ値は nil
- nil の slice は寛容で、
lenは 0 を返し、appendもできる - nil の map は読み取れるが、書き込むと panic する
書き込みだけが panic するのは、行き先のない紙を持ったまま「そこに置いてこい」と言われるからです。 読み取りは、行き先がないと分かった時点でゼロ値を返せば済みます。
package main
import "fmt"
func main() {
var m map[string]int
fmt.Println("読み取りは動く:", m["a"]) // ゼロ値の 0 が返る
m["a"] = 1 // ここで panic する
fmt.Println("ここには到達しない")
}書き込む map は、slice と同じく make で作ってから使います。
m := make(map[string]int) // 空の map を確保するこれで行き先ができ、書き込めるようになります。
競プロの道具との対応
| 競プロで使っていたもの | Go | 中身 |
|---|---|---|
| C++ の vector | slice | 動的配列。ポインタと長さと容量の 3 つ組 |
| C++ の unordered_map、Python の dict | map | ハッシュテーブル。平均 O(1) で順序なし |
| C++ の map(順序付き) | 対応する組み込み型なし | キーを slice に取り出してソートする |
試してみよう
順序が保証されないなら、必要なときは自分で並べます。 次のコードを、キーの順(apple, banana, cherry)で出力するように直してください。
package main
import (
"fmt"
"sort"
)
func main() {
stock := map[string]int{"cherry": 7, "apple": 3, "banana": 5}
keys := []string{}
// ここで keys に stock のキーを集める
sort.Strings(keys)
for _, k := range keys {
fmt.Printf("%s=%d\n", k, stock[k])
}
}ヒント: for k := range stock はキーだけを取り出せます。append で keys に足してください。
直したら何度か実行して、毎回同じ順序で出ることを確かめてください。