Go言語のmapでマップ(連想配列)を使う方法をわかりやすく解説

スポンサーリンク
スポンサーリンク
amazon
スマイルSALE
--:--:--
ad. 価格範囲を指定して商品を探せます

mapの概要

キーと値のペアをまとめて管理するGoの予約語

map

概要 mapは、一意となる「キー(Key)」とそれに対応する「値(Value)」をペアにして格納し、任意のキーを指定することで目的のデータを高速に検索・追加・削除できる組み込みの連想配列(ハッシュテーブル)を定義するために使用します。

  • 内部的にはハッシュ関数を利用したデータ構造であり、データ量が増えても特定の要素へのアクセスが非常に高速に行える。
  • キーの型は一意性を持ち、値に対応するデータを格納する。
  • 動的に要素を追加・削除できる。

mapの考え方をイメージで理解

基本的なmapの使い方

以下のコードは、mapを使ってキーと値を関連付ける基本的な例です。

package main

import "fmt"

func main() {
    // mapの定義
    ages := map[string]int{
        "太郎": 25,
        "花子": 30,
        "次郎": 20,
    }

    fmt.Println("太郎の年齢:", ages["太郎"])
}

解説:

  • map[string]intは、キーがstring型で、値がint型のマップを定義します。
  • ages["太郎"]で「太郎」の年齢(25)を取得します。
  • 要素を複数行に並べて記述する場合、最後の要素の末尾にも必ずカンマ(,)を付与しなければならないのがGo言語の厳格な構文ルールです。

実行結果:

太郎の年齢: 25

要素の追加・削除

mapには新しい要素を追加したり、既存の要素を削除することができます。

package main

import "fmt"

func main() {
    ages := make(map[string]int) // 空のmapを作成

    // 要素の追加
    ages["太郎"] = 25
    ages["花子"] = 30

    fmt.Println("追加後:", ages)

    // 要素の削除
    delete(ages, "太郎")

    fmt.Println("削除後:", ages)
}

解説:

  • make(map[string]int)を使って空のマップを作成します。make を経由せずに var ages map[string]int とだけ宣言すると値は nil になり、その状態のまま要素を代入しようとするとプログラムがクラッシュするため、必ず初期化が必要です。
  • ages["太郎"] = 25のようにして新しい要素を追加します。
  • delete(ages, "太郎")で「太郎」のデータを削除します。

実行結果:

追加後: map[太郎:25 花子:30]
削除後: map[花子:30]

要素の存在確認(「カンマ ok」イディオム)

存在しないキーを指定してマップから値を取り出しようとした場合、Go言語はエラーを返さず、そのデータ型の「初期値(ゼロ値)」を返却します。そのため、キーがマップ内に存在するかどうかを確認するには、2つの戻り値を受け取ります。

package main

import "fmt"

func main() {
    ages := map[string]int{"太郎": 25, "花子": 30}

    age, exists := ages["次郎"]
    if exists {
        fmt.Println("次郎の年齢:", age)
    } else {
        fmt.Println("次郎のデータは存在しません")
    }
}

解説:

  • age, exists := ages["次郎"]で、キー「次郎」が存在するかどうかを確認できます。
  • マップの取得式を多重代入の形式にすると、2番目の変数に「キーが存在していれば true、なければ false」のブール値が格納されます。これを利用しないと、「元々データとして年齢が 0 のデータ」と「登録自体がないデータ(取得結果は自動的に 0 になる)」の判別がつかなくなります。

実行結果:

次郎のデータは存在しません

マップのループ処理

for rangeを使うと、マップの全要素を走査できます。

package main

import "fmt"

func main() {
    ages := map[string]int{"太郎": 25, "花子": 30, "次郎": 20}

    for name, age := range ages {
        fmt.Println(name, "の年齢:", age)
    }
}

解説:

  • for name, age := range agesで、マップのキーと値を順番に取得できます。
  • マップの走査順序は保証されません。Goのランタイムは、ハッシュテーブルの内部構造に依存したプログラムを記述されるのを防ぐため、ループ開始時に 出力順序を故意にシャッフル する非決定的挙動を仕様として採用しています。

実行結果(順序はランダム):

太郎 の年齢: 25
花子 の年齢: 30
次郎 の年齢: 20

学術的・設計的注意点

  • スレッド安全性(並行処理)の完全な欠如: map は単体では マルチスレッド(並行処理)に対して安全ではありません。複数のゴルーチンが、適切な同期制御を行わずに同一のマップオブジェクトに対して同時に書き込み(あるいは一方が書き込み、もう一方が読み込み)を行うと、Goランタイムは即座に検知して fatal error: concurrent map writes と出力し、アプリケーションをその場で強制クラッシュさせます。これを回避するためには、sync.Mutex を用いてマップへのアクセスを直列化するか、あるいは sync.Map パッケージの利用を検討する必要があります。
  • メモリの解放特性(要素を削除してもメモリは縮小しない): delete 関数を使用してマップから大量の要素を削除したとしても、そのマップが確保したシステム上の内部メモリバケット(容量)は 自動的には縮小(解放)されません。大量のデータを一時的にマップに詰め込んでからクリアするような処理を繰り返すと、メモリの高止まり現象を招きます。マップ全体のメモリを完全にクリーンアップしてOSに返却したい場合は、古いマップ変数を破棄して、再度 make を使って新しいマップを1から生成し直すアプローチが必要です。

よくある質問

Q: マップのサイズを事前に設定できますか?
A: はい、事前設定(初期容量の確保)は可能です。固定サイズにロックすることはできませんが、make(map[string]int, 100) のように第2引数に数値を渡すことで、内部バケットの領域をあらかじめ拡張された状態で生成できます。これにより、要素追加時の メモリ再割り当てオーバーヘッドを完全に無くす ことができるため、実務上のパフォーマンスチューニングとして多用されます。
Q: マップの要素を安全に削除する方法は?
A: delete(map, key)を使えば安全に削除できます。指定したキーが最初からマップ内に存在しなかったとしても、ランタイムエラー等は発生せず何も処理されずにスルーされる安全な設計です。
Q: マップの要素数を取得する方法は?
A: 組み込みの len(map) を使うと、現在のマップの要素数(格納されているペアの数)を取得できます。なお、スライスとは異なり cap(map)(キャパシティ判定)は存在せず、呼び出すとエラーになります。
Q: マップのキーの型に使えないものは?
A: スライスやマップ、関数などはキーとして使えません。Go言語の仕様では、キーの型は 比較演算子(== や !=)で評価可能な型 でなければならないと厳格に定められています。構造体については、含まれるすべてのフィールドの型が比較演算子に対応している場合に限り、マップのキーとして正当に利用できます。

まとめ

  • mapはキーと値のペアを管理するハッシュテーブルベースの組み込みデータ構造。
  • 未初期化の nil マップへの書き込みは即時クラッシュするため、make 関数かリテラル構文による実体の初期化が必要。
  • キーの存在を確認するには2つの戻り値(カンマ ok イディオム)を使う。
  • マップのループ処理では 走査順序が毎回完全にランダム化される ため、順序に依存したアルゴリズムを組まない設計配慮が求められる。
  • 並行アクセスに対する安全機構は内蔵されておらず、データ書き込みの衝突はランタイムパニックによって厳格に停止される。