Map
Map
辞書を引く場面を想像してください:単語(キー)を入力すると、すぐにその定義(値)を見つけられます。最初のページから最後のページまでめくる必要はありません — 目的のページに直接アクセスします。Goのmapはまさにそのような「辞書」です — キーで値を素早くルックアップできるO(1)の時間計算量を持つキー・バリューデータ構造です。
1. コアコンセプト
| コンセプト | 説明 |
|---|---|
| 定義 | map[KeyType]ValueType;KeyTypeは比較可能な型でなければならない(スライス、map、funcは不可) |
| 作成 | make(map[K]V)またはmap[K]V{}リテラル |
| 追加/更新 | m[key] = value(キーが存在しなければ追加、存在すれば上書き) |
| 読み取り | v := m[key] |
| 削除 | delete(m, key) |
| comma okパターン | v, ok := m[key];キーが存在しなければokはfalse |
| イテレーション | for k, v := range m(ランダム順序、信頼性なし) |
| 長さ | len(m) |
2. 基本構文/使い方
Mapの作成
// 方法1:makeを使用
m1 := make(map[string]int)
// 方法2:リテラルで初期化付き作成
m2 := map[string]int{
"apple": 5,
"banana": 3,
}
// 方法3:宣言後に代入
var m3 map[string]int // m3はここでnil。直接代入不可
m3 = make(map[string]int) // まず初期化
m3["cherry"] = 7 // その後代入
var m map[K]Vで宣言したmapはnilです — 書き込むとpanicしますが、読み取り(ゼロ値を返す)とlen()(0を返す)は問題ありません。
CRUD操作
m := map[string]int{"a": 1, "b": 2}
// 作成
m["c"] = 3
// 更新
m["a"] = 10
// 読み取り
v := m["a"] // v = 10
// 削除
delete(m, "b")
comma okパターン
m := map[string]int{"x": 42}
v, ok := m["x"] // v = 42, ok = true
v, ok = m["y"] // v = 0, ok = false(intのゼロ値を返す)
if _, exists := m["z"]; !exists {
fmt.Println("キー 'z' は存在しません")
}
_で無視してください:_, ok := m[key]。
3. コード例
例1:基本的な使い方(難易度⭐)
学生の成績テーブルを作成し、CRUD操作を行います。
package main
import "fmt"
func main() {
// 学生の成績mapを作成
scores := map[string]int{
"Alice": 90,
"Bob": 85,
}
// 学生を追加
scores["Charlie"] = 92
// Bobの成績を変更
scores["Bob"] = 88
// Aliceの成績を検索
fmt.Println("Aliceの成績:", scores["Alice"])
// Charlieを削除
delete(scores, "Charlie")
// 全学生の成績をイテレーション
for name, score := range scores {
fmt.Printf("%s: %d\n", name, score)
}
fmt.Println("学生数:", len(scores))
}
出力例(イテレーション順序は異なる場合があります):
Aliceの成績: 90
Bob: 88
Alice: 90
学生数: 2
例2:中級的な使い方(難易度⭐⭐)
comma okパターンによる安全なクエリ、mapのイテレーション、ネストされたmap。
package main
import "fmt"
func main() {
// ========== comma okパターン ==========
fruits := map[string]int{
"apple": 5,
"banana": 3,
}
// 安全なクエリ
if count, ok := fruits["apple"]; ok {
fmt.Printf("appleは%d個あります\n", count)
}
if count, ok := fruits["grape"]; !ok {
fmt.Println("grapeは存在しません。リストに追加します")
fruits["grape"] = 10
}
// ========== rangeイテレーション ==========
fmt.Println("\n全てのフルーツ:")
for fruit, count := range fruits {
fmt.Printf(" %s: %d\n", fruit, count)
}
// ========== ネストされたmap(mapのmap) ==========
// クラス -> 学生 -> 成績
classScores := map[string]map[string]int{
"Class A": {
"Alice": 90,
"Bob": 85,
},
"Class B": {
"Charlie": 92,
"Diana": 88,
},
}
// ネストされたmapを検索
if class, ok := classScores["Class A"]; ok {
if score, ok := class["Alice"]; ok {
fmt.Printf("\nClass A Aliceの成績: %d\n", score)
}
}
// ネストされたmapをイテレーション
fmt.Println("\n全クラスの成績:")
for class, students := range classScores {
fmt.Printf(" %s:\n", class)
for name, score := range students {
fmt.Printf(" %s: %d\n", name, score)
}
}
}
出力:
appleは5個あります
grapeは存在しません。リストに追加します
全てのフルーツ:
apple: 5
banana: 3
grape: 10
Class A Aliceの成績: 90
全クラスの成績:
Class A:
Alice: 90
Bob: 85
Class B:
Charlie: 92
Diana: 88
例3:総合応用(難易度⭐⭐⭐)
mapベースのデータ処理による単語頻度カウンターを実装します(ソート出力、高頻度単語の検出)。
package main
import (
"fmt"
"sort"
"strings"
)
// WordCounterはテキスト内の各単語の出現回数をカウントします
func WordCounter(text string) map[string]int {
// 小文字に変換し、空白で分割
words := strings.Fields(strings.ToLower(text))
// 単語頻度mapを作成
freq := make(map[string]int)
for _, word := range words {
// 句読点を除去(簡易処理)
word = strings.Trim(word, ".,!?;:\"'")
if word != "" {
freq[word]++
}
}
return freq
}
// TopNは最も頻度の高いN個の単語を返します(頻度降順)
func TopN(freq map[string]int, n int) []string {
// mapをソート可能なスライスに変換
type wordFreq struct {
word string
count int
}
// スライスを構築
pairs := make([]wordFreq, 0, len(freq))
for w, c := range freq {
pairs = append(pairs, wordFreq{w, c})
}
// 頻度降順でソート
sort.Slice(pairs, func(i, j int) bool {
if pairs[i].count == pairs[j].count {
return pairs[i].word < pairs[j].word // 頻度が同じ場合はアルファベット順
}
return pairs[i].count > pairs[j].count
})
// 上位N個を取得
if n > len(pairs) {
n = len(pairs)
}
result := make([]string, n)
for i := 0; i < n; i++ {
result[i] = fmt.Sprintf("%s(%d)", pairs[i].word, pairs[i].count)
}
return result
}
// MergeFreqは2つの単語頻度mapをマージします
func MergeFreq(a, b map[string]int) map[string]int {
result := make(map[string]int)
// aのデータをコピー
for k, v := range a {
result[k] = v
}
// bのデータを累積
for k, v := range b {
result[k] += v
}
return result
}
func main() {
text1 := "Go is great. Go is fast. Go is easy to learn."
text2 := "I love Go. Go makes programming fun and easy."
// 単語頻度をカウント
freq1 := WordCounter(text1)
freq2 := WordCounter(text2)
fmt.Println("テキスト1の単語頻度:")
for word, count := range freq1 {
fmt.Printf(" %s: %d\n", word, count)
}
fmt.Println("\nテキスト2の単語頻度:")
for word, count := range freq2 {
fmt.Printf(" %s: %d\n", word, count)
}
// 単語頻度をマージ
merged := MergeFreq(freq1, freq2)
// 上位5つの高頻度単語を見つける
fmt.Println("\nマージ後の上位5つの高頻度単語:")
top5 := TopN(merged, 5)
for i, item := range top5 {
fmt.Printf(" %d. %s\n", i+1, item)
}
// 総単語数をカウント
totalWords := 0
for _, count := range merged {
totalWords += count
}
fmt.Printf("\n総単語数: %d、ユニーク単語数: %d\n", totalWords, len(merged))
}
出力例:
テキスト1の単語頻度:
go: 3
is: 3
great: 1
fast: 1
easy: 1
to: 1
learn: 1
テキスト2の単語頻度:
i: 1
love: 1
go: 2
makes: 1
programming: 1
fun: 1
and: 1
easy: 1
マージ後の上位5つの高頻度単語:
1. go(5)
2. is(3)
3. easy(2)
4. and(1)
5. fast(1)
総単語数: 18、ユニーク単語数: 12
3. よくあるユースケース
ケース1:キャッシュ / 高速ルックアップ
mapを使ってシンプルなインメモリキャッシュを実装し、冗長な計算を回避します。
package main
import "fmt"
// フィボナッチ数列(キャッシュ付き)
var cache = map[int]int{0: 0, 1: 1}
func fib(n int) int {
// まずキャッシュを確認
if val, ok := cache[n]; ok {
return val
}
// キャッシュミス、計算してキャッシュに格納
result := fib(n-1) + fib(n-2)
cache[n] = result
return result
}
func main() {
for i := 0; i <= 10; i++ {
fmt.Printf("fib(%d) = %d\n", i, fib(i))
}
fmt.Println("\nキャッシュ内容:", cache)
}
ケース2:グループ統計
mapを使ってデータをグループ化してカウントします。
package main
import "fmt"
func main() {
// 学生リスト:名前 -> クラス
students := map[string]string{
"Alice": "Class A",
"Bob": "Class B",
"Charlie": "Class A",
"Diana": "Class B",
"Eve": "Class A",
}
// クラス別にグループ化
groups := make(map[string][]string)
for name, class := range students {
groups[class] = append(groups[class], name)
}
// グループ化結果を出力
for class, members := range groups {
fmt.Printf("%s (%d名): %v\n", class, len(members), members)
}
}
出力:
Class A (3名): [Alice Charlie Eve]
Class B (2名): [Bob Diana]
❓ よくある質問
質問1:なぜmapのイテレーション順序は毎回違うのですか?
Goは意図的にmapのイテレーション順序をランダム化しています。これは開発者がmapのイテレーション順序に依存することを防ぐためで、並行シナリオでは内部構造が変わる可能性があるからです。順序付きイテレーションが必要な場合は、まずキーをスライスに収集し、ソートしてからイテレーションしてください:
keys := make([]string, 0, len(m))
for k := range m {
keys = append(keys, k)
}
sort.Strings(keys)
for _, k := range keys {
fmt.Println(k, m[k])
}
質問2:mapはスレッドセーフですか?
いいえ。 Goのmapは並行セーフではありません。複数のgoroutineが同じmapに同時に読み書きするとpanicが発生します。解決策:
sync.Mutexまたはsync.RWMutexでmapを保護する- Go 1.9以降で提供される
sync.Mapを使用する(読み取りが多い、書き込みが少ないシナリオに適している)
// sync.Mapの使用
var m sync.Map
m.Store("key", "value")
v, ok := m.Load("key")
質問3:mapのキーとして使える型は何ですか?
mapのキーは比較可能な型でなければなりません。以下を含みます:
- 基本型:
int、float64、string、bool - ポインタ
- 配列(比較可能な要素を持つ)
- 構造体(全てのフィールドが比較可能)
キーとして使用できない型:slice、map、func。
質問4:mapに特定のキーが含まれているかチェックするには?
comma okパターンを使用してください:
if _, ok := m[key]; ok {
// キーが存在する
} else {
// キーが存在しない
}
値がゼロ値かどうかでキーの存在をチェックしないでください。ゼロ値が有効な値である可能性があるからです。
📖 まとめ
- mapはGoの組み込みキー・バリューデータ構造。キーで値を素早くルックアップ
makeまたはリテラルmap[K]V{}で作成。varで宣言したnilmapは読み取り専用- 追加/更新:
m[key] = value;読み取り:v := m[key];削除:delete(m, key) - comma okパターン(
v, ok := m[key])でキーの存在を安全にチェック for k, v := range mでイテレーション。順序はランダムで信頼性なし- mapは参照型。代入とパラメータ渡しはデータをコピーしない
- mapは並行セーフではない。複数goroutineの読み書きにはロックまたは
sync.Mapを使用 - map vs スライス:頻繁なルックアップにはmap、順序付きストレージにはスライス。両者はよく一緒に使用される
📝 演習
演習1(⭐)
5つのプログラミング言語とその発明年を格納するmapを作成し、以下を実行してください:
- 新しい言語を2つ追加
- ある言語の年を変更
- ある言語を削除
- comma okパターンで言語の存在をチェック
- 全内容をイテレーションして出力
演習2(⭐⭐)
シンプルな連絡先プログラムを実装してください:
map[string][]stringを定義。キーは連絡先名、値は電話番号のリスト- 関数を実装:連絡先追加、電話番号追加、連絡先検索、連絡先削除
- 全連絡先を最初の文字でグループ化して表示する関数を実装
// 期待される出力例:
// A: Alice - [13800001111, 13900002222]
// B: Bob - [13700003333]
演習3(⭐⭐⭐)
学生の成績管理システムを実装してください:
- ネストされた
map[string]map[string]float64(クラス -> 学生 -> 成績)を使用 - 関数を実装:成績追加、学生の全科目成績検索、クラス平均計算
- 関数を実装:全クラスの各科目で最高得点の学生を見つける
- 結果をフォーマット済みテーブルとして出力
// 期待される出力例:
// ========== クラス平均 ==========
// Class A: 87.5
// Class B: 91.2
//
// ========== 科目別最高得点 ==========
// 数学: Alice (98.0)
// 英語: Bob (95.0)



