map キーワードは、Go に組み込まれたハッシュテーブルを宣言します。map[K]V 型は K 型のキーを V 型の値に対応付け、検索、挿入、削除を平均して定数時間で行います。map は言語に組み込まれているので、パッケージをインポートしたり HashMap クラスをインスタンス化したりする必要はありません。リテラル構文、組み込み関数の make、delete、clear、len、そして for range のサポートが最初から用意されています(Go spec)。
要約
map[string]int{"/healthz": 1}またはmake(map[string]int)で map を作れます。var m map[string]intと書くとnilの map になります。nilの map を読むとゼロ値が返ります。書き込むとassignment to entry in nil mapで panic します。- 存在しないキーはゼロ値を返します。キーがあったかどうかを知りたいときは
v, ok := m[k]を使います。 - 反復順序は規定されておらず、ランダム化されています。順序が重要なときは
slices.Sorted(maps.Keys(m))でキーをソートします。 - キーは比較可能でなければなりません。文字列、数値、struct、配列は使えます。slice、map、関数は使えません。
- map の値は小さな記述子です。関数に渡すと、その関数は呼び出し側の map を書き換えられます。
- 値が struct の場合、
m[k].Field = xはコンパイルできません。コピーして書き換えてから格納し直すか、ポインタを格納します。 - map は並行書き込みに対して安全ではありません。
sync.Mutexで保護するか、sync.Mapが想定しているケースではそれを使います。 map[string]struct{}が Go の set です。mapsパッケージ(Go 1.21 以降)はClone、Equal、DeleteFunc、Keys、Collectを提供します。
Go で map を宣言して作成するには?
map 型は map[KeyType]ValueType と書きます。最初から入れておくエントリーがわかっているときは複合リテラルで、空の状態から始めるときは make で作成します。
example.gogopackage main import "fmt" func main() { statusText := map[int]string{ 200: "OK", 404: "Not Found", 503: "Service Unavailable", } hits := make(map[string]int) hits["/api/orders"]++ hits["/api/orders"]++ hits["/healthz"]++ fmt.Println(statusText[404], len(statusText)) fmt.Println(hits["/api/orders"], hits["/healthz"], len(hits)) }
example.texttextNot Found 3 2 1 2
hits["/api/orders"]++ は、まだ存在しないキーに対しても動きます。読み取りで 0 が返り、インクリメントで 1 になり、それが書き込まれます。カウンターに準備は要りません。
make には make(map[string]int, len(requests)) のように、省略可能なサイズのヒントを渡せます。ランタイムはおよそその数のエントリー分の領域を確保するので、値を詰めている間に map が拡張されずに済みます。ヒントは上限ではなく、map はそれを超えて拡張されます。len(m) はエントリー数を返します。cap は map には使えません。
空の map を得る 2 つの方法の違いと、map では var だけでは不十分な理由は Go の var と make の違いで比較しています。
nil の map に書き込むと panic するのはなぜか?
map 型のゼロ値は nil です。nil の map の裏にはハッシュテーブルがないので、新しいエントリーを置く場所がありません。これはたいてい、誰も初期化していない struct の map フィールドとして現れます。
example.gogopackage main import "fmt" type Router struct { routes map[string]string } func main() { var r Router fmt.Println(r.routes["/health"] == "", len(r.routes)) r.routes["/health"] = "healthHandler" }
example.texttexttrue 0 panic: assignment to entry in nil map
読み取りは動きます。nil の map は、検索、len、range、delete に対しては空の map と同じように振る舞うので、1 行目は普通に出力されます。panic するのは書き込みだけです。最初の書き込みより前に map を作成すれば直ります。普通はコンストラクターの中で作ります。
example.gogofunc NewRouter() *Router { return &Router{routes: make(map[string]string)} }
json.Unmarshal のようなデコーダーは、JSON オブジェクトを見つけると map を確保してくれるので、map[string]any のデコード先は nil のままで構いません。自分のコードでは make を呼ぶか、リテラルを使う必要があります。
Go の map にキーが存在するか確認するには?
存在しないキーで map をインデックスすると、値の型のゼロ値が返ります。カウンターならこれで問題ありません。誰もリクエストしていないパスのヒット数は 0 です。しかしほかの値では、ゼロ値が本当の違いを隠してしまいます。機能フラグの map は、オフのフラグにも、一度も定義されていないフラグにも false を返します。
comma-ok の形では、キーが存在したかどうかを表す真偽値が 2 つ目の戻り値として返ります。
example.gogopackage main import "fmt" func main() { flags := map[string]bool{ "new_checkout": true, "dark_mode": false, } for _, name := range []string{"new_checkout", "dark_mode", "beta_search"} { enabled, ok := flags[name] if !ok { fmt.Printf("%s: unknown flag\n", name) continue } fmt.Printf("%s: %v\n", name, enabled) } }
example.texttextnew_checkout: true dark_mode: false beta_search: unknown flag
ok が false になるのは beta_search だけです。これがないと、フラグ名のタイプミスで機能が黙ってオフになってしまいます。件数や合計のようにゼロ値がそのまま正しい答えになるなら、普通の形で足ります。「存在しない」と「ゼロ」で意味が変わるなら comma-ok を使います。
map のキーを削除し、map を空にするには?
delete(m, k) はキーを 1 つ削除します。存在しないキーを削除しても何も起きず、エラーにはなりません。Go 1.21 で追加された clear(m) は、すべてのエントリーを削除し、map の領域は確保したままにします(Go 1.21 リリースノート)。
example.gogocache := map[int]string{42: "[email protected]", 7: "[email protected]"} delete(cache, 42) delete(cache, 999) fmt.Println(len(cache), cache) clear(cache) fmt.Println(len(cache), cache == nil)
example.texttext1 map[7:[email protected]] 0 false
clear の後、map は空ですが引き続き使えます。そして、その map を参照しているすべての変数から空の map が見えます。代わりに cache = map[int]string{} と代入すると、新しい map を得るのはその変数だけです。
同じ map を range で回しながらキーを削除することは許されています。仕様では、反復中に削除されたエントリーは、そのループの後半で返されないと定められています(Go spec)。反復中にキーを追加することも許されていますが、新しいエントリーが同じループに現れるかどうかは決まっていません。
Go で map の反復順序がランダムなのはなぜか?
仕様では、map の反復順序は規定されておらず、反復のたびに同じになるとは保証されないと定められています。実際のランタイムは、range のたびにランダムな開始位置を選びます。hits に対する同じループを 3 回実行すると、次のように出力されました。
example.texttext/api/orders 41 /healthz 120 /api/users 17 /api/orders 41 /healthz 120 /api/users 17 /api/users 17 /api/orders 41 /healthz 120
このランダム化は意図的なものです。初期の Go では、小さな map の順序が安定しているように見え、その順序に依存するプログラムが現れました。実装が変わると、そうしたプログラムは壊れました。順序をランダム化することで、このバグは本番環境ではなくテストで表に出るようになります(Go blog, Go maps in action)。
ログの 1 行、レポート、テストなどで安定した順序が必要なときは、キーをソートします。Go 1.23 からは maps.Keys がイテレーターを返し、slices.Sorted がそれを 1 回の呼び出しで集めてソートします。
example.gogopackage main import ( "fmt" "maps" "slices" ) func main() { hits := map[string]int{"/api/orders": 41, "/healthz": 120, "/api/users": 17} for _, path := range slices.Sorted(maps.Keys(hits)) { fmt.Printf("%-12s %d\n", path, hits[path]) } }
example.texttext/api/orders 41 /api/users 17 /healthz 120
map 全体を fmt.Println で出力すると、キーはもともとソートされた順序で表示されます。先ほどの fmt.Println(cache) の出力が安定していたのはこのためです。encoding/json もマーシャル時に map のキーをソートします。ランダム化されるのは range だけです。
Go の map のキーにはどんな型を使えるのか?
キーの型は比較可能でなければなりません。つまり、その型に == と != が定義されている必要があります。文字列、整数、浮動小数点数、真偽値、ポインタ、チャネル、interface は比較可能です。すべてのフィールドや要素が比較可能な struct と配列も同様です。slice、map、関数は比較可能ではなく、コンパイラが拒否します。
example.gogoseen := map[[]string]bool{}
example.texttext./main.go:4:14: invalid map key type []string
struct のキーは、キーが複数の要素から成るときに便利です。次の例では、各エンドポイントのレイテンシーをメソッドとパスの組み合わせで管理しています。
example.gogotype Endpoint struct { Method string Path string } latencyMs := map[Endpoint]int{ {"GET", "/api/orders"}: 38, {"POST", "/api/orders"}: 112, } fmt.Println(latencyMs[Endpoint{"POST", "/api/orders"}])
このコードは 112 と出力します。struct はフィールドごとに比較されるので、メソッドとパスが同じ 2 つの Endpoint の値は同じエントリーを見つけます。"POST /api/orders" のようなキー文字列を組み立て、後でまた分割する手間はかかりません。ID のリストをキーにしたいときは、slice を固定長の配列に変換するか、1 つの文字列に連結します。
map[any]int のような interface のキーはコンパイルできますが、実行時に slice を挿入すると runtime error: hash of unhashable type []string で panic します。浮動小数点数のキーは使えますが、おすすめしません。NaN は自分自身と等しくないので、m[math.NaN()] = 1 を実行するたびに、二度と検索できない新しいエントリーが追加されます。
Go の map は参照渡しなのか?
Go はすべてを値渡しにし、map も例外ではありません。map 変数の値はハッシュテーブルを指す小さな記述子なので、それをコピーするとポインタがコピーされるだけで、エントリーはコピーされません。map を受け取った関数は、呼び出し側のエントリーを書き換えられます。一方、引数に新しい map を代入する関数は、自分のコピーを変えるだけです。
example.gogofunc recordStatus(counts map[int]int, status int) { counts[status]++ } func resetCounts(counts map[int]int) { counts = map[int]int{} } func main() { counts := map[int]int{} recordStatus(counts, 200) recordStatus(counts, 500) resetCounts(counts) fmt.Println(counts) }
example.texttextmap[200:1 500:1]
recordStatus は共有されたテーブルを通して書き込んだので、2 つのカウントが両方残っています。resetCounts は自分のローカル変数を置き換えただけで、呼び出し側には影響しませんでした。呼び出し側の map を空にするには、関数の中で clear(counts) を呼びます。go vet はこれを報告しないので、2 つ目の関数はエラーなくコンパイルされ、実際には何もしません。
struct のフィールドや戻り値でも、同じように共有が起きます。内部の map をそのまま返すゲッターがあると、呼び出し側がその内部状態を書き換えられます。呼び出し側に独自のコピーを渡したいときは maps.Clone(m) を返します。
map の中の struct のフィールドに代入できないのはなぜか?
map の値はアドレス指定可能ではありません。テーブルが拡張されるとランタイムがエントリーを移動することがあるので、Go は map の中に格納された値への参照を渡しません。そのため、次のコードはコンパイルできません。
example.gogotype Session struct { UserID int Requests int } sessions := map[string]Session{"s_91": {UserID: 42}} sessions["s_91"].Requests++
example.texttext./main.go:10:2: cannot assign to struct field sessions["s_91"].Requests in map
直し方は 2 つあります。値をコピーして取り出し、書き換えてから格納し直す方法と、ポインタを格納する方法です。後者では map はアドレスを保持し、struct はテーブルの外に置かれます。
example.gogos := sessions["s_91"] s.Requests++ sessions["s_91"] = s byToken := map[string]*Session{"s_91": {UserID: 42}} byToken["s_91"].Requests++
どちらの方法でも Requests は 1 になります。コピーして格納し直す形では、map がデータの唯一の所有者であり続けます。ポインタの形は更新が短く書け、大きな struct のコピーも避けられますが、存在しないキーでは nil ポインタが返るので、byToken["s_404"].Requests++ は panic します。先に comma-ok で確認してください。&sessions["s_91"] と書けないのも同じルールによるものです。
Go の map は並行処理で安全に使えるのか?
使えません。並行した読み取りは問題ありませんが、ほかの読み取りや書き込みと並行した書き込みはデータ競合になります。ランタイムはこうした競合の一部を検出し、recover では捕捉できない fatal error でプログラム全体を停止させます。共有のカウンター map をインクリメントする 8 つの goroutine は、あっという間にクラッシュします。
example.gogohits := map[string]int{} var wg sync.WaitGroup for range 8 { wg.Go(func() { for range 10000 { hits["/api/orders"]++ } }) } wg.Wait()
example.texttextfatal error: concurrent map writes
読み取りと書き込みが競合すると fatal error: concurrent map read and map write になります。HTTP サーバーでは各リクエストがそれぞれの goroutine で実行されるので、2 つのリクエストが共有の map に触れた時点でこれが起きます。ランタイムの検出で見逃される競合を捕まえるには、go test -race でテストを実行します。
よくある直し方は、map と sync.Mutex を 1 つの型にまとめ、ロックを取るメソッドを通してのみ map に触れるようにすることです。
example.gogotype HitCounter struct { mu sync.Mutex hits map[string]int } func NewHitCounter() *HitCounter { return &HitCounter{hits: make(map[string]int)} } func (c *HitCounter) Inc(path string) { c.mu.Lock() defer c.mu.Unlock() c.hits[path]++ } func (c *HitCounter) Get(path string) int { c.mu.Lock() defer c.mu.Unlock() return c.hits[path] }
同じ 8 つの goroutine が今度は最後まで終わり、c.Get("/api/orders") は 80000 を返します。読み取りが書き込みよりはるかに多く、各読み取りがそれなりの処理を行う場合は、sync.RWMutex を使い、Get では RLock を呼びます。
sync.Map は標準ライブラリにある並行 map です。ドキュメントによると、最適化されているのは 2 つのケースです。1 つは、一度書き込まれて何度も読まれるキー(増えるだけのキャッシュなど)で、もう 1 つは、各 goroutine が互いに重ならないキーの集合を扱う場合です(sync.Map のドキュメント)。それ以外のケースでは、通常の map と mutex の組み合わせのほうがたいていシンプルで速度も変わらず、キーと値の型も any にならずに済みます。
Go で set を作るには?
Go には set 型がありません。慣用的な方法は、値に空の struct を使う map です。struct{} は 0 バイトなので、map はキーだけを保持します。次の例では、受け取ったリクエストを ID で重複排除しています。
example.gogorequestIDs := []string{"req_7f", "req_a1", "req_7f", "req_c3", "req_a1"} seen := make(map[string]struct{}, len(requestIDs)) for _, id := range requestIDs { if _, dup := seen[id]; dup { fmt.Println("duplicate, skipping", id) continue } seen[id] = struct{}{} } fmt.Println(len(seen), "unique requests")
example.texttextduplicate, skipping req_7f duplicate, skipping req_a1 3 unique requests
map[string]bool でも同じことができ、if seen[id] と書けて comma-ok が要らないぶん、少し読みやすくなります。ただし、エントリーごとに 1 バイト余分にかかり、false で格納されたキーという 3 つ目の状態も生まれます。コードベースごとにどちらか一方に統一しましょう。struct{} と、チャネルでのシグナルとしてのもう 1 つの使い方は、Go の struct キーワードで解説しています。
maps パッケージは何をするのか?
maps パッケージはジェネリックなヘルパーとともに Go 1.21 で登場し、Go 1.23 でイテレーター関数が追加されました(pkg.go.dev/maps)。よく出てくるのは次の関数です。
example.gogodefaults := map[string]string{"region": "eu-west-1", "log_level": "info"} cfg := maps.Clone(defaults) cfg["log_level"] = "debug" cfg["debug_token"] = "tok_123" fmt.Println(maps.Equal(cfg, defaults)) maps.DeleteFunc(cfg, func(k, v string) bool { return strings.HasPrefix(k, "debug_") }) fmt.Println(slices.Sorted(maps.Keys(cfg))) fmt.Println(defaults["log_level"]) roles := maps.Collect(slices.All([]string{"admin", "editor", "viewer"})) fmt.Println(roles[1])
example.texttextfalse [log_level region] info editor
maps.Cloneは浅いコピーを作ります。cfgを変更してもdefaultsはそのままでした。値そのものはディープコピーされないので、slice を値に持つ map では slice が共有されたままになります。maps.Equalはキーと値を==で比較します。map はnilとしか比較できないので、2 つの map を==で直接比較することはできません。maps.DeleteFuncは、述語に一致するすべてのエントリーを削除します。maps.Keys、maps.Values、maps.Allはイテレーターを返します。slices.Sorted、slices.Collect、またはfor rangeループに渡して使います。maps.Collectは、任意のキーと値のイテレーターから map を作ります。この例では、slice をインデックスから値への map に変換しています。maps.Copy(dst, src)は、ある map を別の map にマージします。
Go の map はどう実装されているのか?
Go 1.24 から、組み込みの map は Swiss Table という設計を採用しています。これは、複数のスロットをまとめて調べるオープンアドレス法のハッシュテーブルです。リリースノートによると、map 操作の CPU オーバーヘッドが減りました(Go 1.24 リリースノート)。言語自体は変わっていないので、以前のバケット方式の map を前提に書いたコードもこれまでどおり動きます。
map が縮小しないのは今も同じです。エントリーを削除するとスロットは再利用できるようになりますが、メモリは保持されたままです。かつて 100 万件のセッションを保持していたキャッシュは、それらを削除した後もそのメモリを持ち続けます。clear でも解放されません。長期間使う map のサイズが一時的に膨らんでから減った場合は、残ったエントリーを maps.Clone や新しく make した map にコピーし、古い map はガベージコレクションに回収させます。
LevelUpGo で学ぶ
LevelUpGo では、ブラウザ上で本物の Go コードを実行する演習を通じて Go を学べます。Composite Types では、slice や struct と並んで map を扱い、comma-ok による検索、カウント、削除、安定した順序での反復を学びます。Concurrency Fundamentals では、goroutine、WaitGroup、そして map を goroutine 間で共有するときに必要になる mutex を扱います。残り 24 個の予約語については、Go のキーワード一覧:全 25 個の予約語を解説をご覧ください。
よくある質問
Go で map はキーワードですか?
はい。map は Go の 25 個の予約語の 1 つなので、変数名や関数名に map は使えません。map[string]int のような map 型、map リテラル、make の引数に登場します。
Go の map は順序付きですか?
いいえ。仕様では反復順序は規定されておらず、ランタイムは range のたびに順序をランダム化します。キーを順番に処理するには、slices.Sorted(maps.Keys(m)) でソートします。fmt.Println と json.Marshal は、もともと map のキーをソートして出力します。
Go の map はスレッドセーフですか?
いいえ。並行した読み取りは安全ですが、ほかの読み取りや書き込みと同時に書き込みが行われると、fatal error: concurrent map writes でプログラムがクラッシュすることがあります。sync.Mutex または sync.RWMutex で map を保護するか、一度だけ書き込むキャッシュや互いに重ならないキーの集合には sync.Map を使ってください。
Go で map の長さを取得するには?
len(m) を呼びます。エントリー数を返し、nil の map にも使えます。その場合は 0 を返します。map は cap をサポートしていません。
Go に set 型はありますか?
ありません。キーだけを保持する map[T]struct{} を使い、_, ok := set[k] で要素が含まれるかを確認します。comma-ok より if set[k] のほうが好みなら、map[T]bool も使えます。
存在しないキーを読むとどうなりますか?
0、""、false、nil といった値の型のゼロ値が返り、エラーにはなりません。存在しないキーと格納されたゼロ値を区別したいときは、2 値の形 v, ok := m[k] を使ってください。
出典
- The Go Programming Language Specification, Map types: https://go.dev/ref/spec#Map_types
- The Go Programming Language Specification, Index expressions: https://go.dev/ref/spec#Index_expressions
- The Go Programming Language Specification, Deletion of map elements: https://go.dev/ref/spec#Deletion_of_map_elements
- The Go Programming Language Specification, For statements with range clause: https://go.dev/ref/spec#For_range
- The Go Blog, Go maps in action: https://go.dev/blog/maps
- Go 1.21 release notes: https://go.dev/doc/go1.21
- Go 1.23 release notes: https://go.dev/doc/go1.23
- Go 1.24 release notes: https://go.dev/doc/go1.24
- maps package: https://pkg.go.dev/maps
- sync.Map: https://pkg.go.dev/sync#Map
