🔩 ねじき教室 Go と Web の教室

map とゼロ値

読了目安 約3分

map の走査順序が保証されないことを実行して確かめ、宣言しただけの map への書き込みが panic する理由をゼロ値と nil から理解します。

この章の目次

前章の slice と同じく、map も中身はポインタです。 そのことが、走査の順序と、宣言しただけの変数への書き込みで表に出ます。

map の計算量と順序

map の読み書きが平均 O(1) であることは、競プロの感覚どおりです。 ただし、map を range で走査する順序は保証されず、Go は実行のたびに順序をわざと変えます。

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 するのは、行き先のない紙を持ったまま「そこに置いてこい」と言われるからです。 読み取りは、行き先がないと分かった時点でゼロ値を返せば済みます。

Go
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 で作ってから使います。

Go
m := make(map[string]int) // 空の map を確保する

これで行き先ができ、書き込めるようになります。

競プロの道具との対応

競プロで使っていたものGo中身
C++ の vectorslice動的配列。ポインタと長さと容量の 3 つ組
C++ の unordered_map、Python の dictmapハッシュテーブル。平均 O(1) で順序なし
C++ の map(順序付き)対応する組み込み型なしキーを slice に取り出してソートする

試してみよう

順序が保証されないなら、必要なときは自分で並べます。 次のコードを、キーの順(apple, banana, cherry)で出力するように直してください。

Go
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 はキーだけを取り出せます。appendkeys に足してください。 直したら何度か実行して、毎回同じ順序で出ることを確かめてください。