第8章: マップ - ハッシュテーブルの深層理解

学習目標

この章を終えると、以下ができるようになります:

  • マップの内部実装(ハッシュテーブル)を理解できる
  • バケット構造とコリジョン処理を説明できる
  • マップのイテレーション順序がランダムな理由を理解できる
  • マップの落とし穴を回避し、最適なパフォーマンスを実現できる
  • sync.Mapの使い方と通常のmapとの違いを理解できる

---

イントロダクション: マップとは何か

🔑 根本的な問い: なぜマップは O(1) で要素にアクセスできるのか?

答えは、ハッシュテーブルという巧妙なデータ構造にあります。

他のデータ構造との比較

┌───────────────┬─────────┬─────────┬──────────┬──────────┐
│  操作         │ 配列    │ スライス│ マップ   │  理由    │
├───────────────┼─────────┼─────────┼──────────┼──────────┤
│ 検索          │ O(n)    │ O(n)    │ O(1)*    │ ハッシュ │
│ 挿入          │ O(1)**  │ O(1)*   │ O(1)*    │ 直接配置 │
│ 削除          │ O(n)    │ O(n)    │ O(1)*    │ マーク   │
│ 順序保証      │ ○       │ ○       │ ×        │ 仕様     │
└───────────────┴─────────┴─────────┴──────────┴──────────┘

* 平均計算量(最悪: O(n))
** インデックスがわかっている場合

💡 重要: マップの O(1) は平均的な場合であり、最悪の場合は O(n) になる可能性があります。

---

マップの基本

マップの作成

// 宣言(nilマップ)
var m map[string]int
// m["key"] = 1  // panic: assignment to entry in nil map

// makeで作成
m := make(map[string]int)
m["Alice"] = 25
m["Bob"] = 30

// リテラルで初期化
ages := map[string]int{
    "Alice": 25,
    "Bob":   30,
    "Carol": 28,
}

// 空マップ
m := map[string]int{}  // makeと同じ

// 容量を指定(最適化のヒント)
m := make(map[string]int, 100)  // 100要素分のヒント

メモリレイアウト: マップはどこに存在するのか

🔑 マップの内部構造:

Goのマップは、ランタイムで管理されるハッシュテーブルです。

マップ変数(スタック):
┌──────────────────────┐
│ hmap ポインタ        │──┐
└──────────────────────┘  │
        8バイト            │
                           ↓
ヒープ上の hmap 構造体:
┌────────────────────────────────────┐
│ count:    3    (要素数)          │
│ B:        1    (バケット数: 2^B) │
│ buckets:  *bmap (バケット配列)  │
│ oldbuckets: nil                    │
│ ... その他のフィールド ...         │
└────────────────────────────────────┘
         │
         ↓
バケット配列:
┌─────────┬─────────┐
│ bucket0 │ bucket1 │
└─────────┴─────────┘

hmap 構造体の詳細

Goランタイムの実装(簡略版):

// runtime/map.go
type hmap struct {
    count     int    // マップのサイズ(len(m)の結果)
    flags     uint8  // 状態フラグ(イテレーション中など)
    B         uint8  // バケット数の指数(バケット数 = 2^B)
    noverflow uint16 // オーバーフローバケット数の概算
    hash0     uint32 // ハッシュシード(ランダム化)

    buckets    unsafe.Pointer // バケット配列(2^B個)
    oldbuckets unsafe.Pointer // リハッシュ時の古いバケット
    nevacuate  uintptr         // リハッシュの進行状況

    extra *mapextra // オーバーフロー情報
}

💡 重要なフィールド:

  • B: バケット数は 2^B(2のべき乗)
  • hash0: ランダムなシード(セキュリティ対策)
  • buckets: 実際のデータを格納するバケット配列

---

ハッシュテーブルの仕組み

ハッシュ関数とは

🔑 ハッシュ関数の役割:

  • 任意のキー → 固定長の整数(ハッシュ値)
  • 同じキー → 常に同じハッシュ値
  • 異なるキー → できるだけ異なるハッシュ値
  • キー              ハッシュ関数         ハッシュ値(64ビット)
    "Alice"    ────→   hash()    ────→   0x7A3B9C...
    "Bob"      ────→   hash()    ────→   0x2F1E8D...
    "Carol"    ────→   hash()    ────→   0x9C4A2B...
    

    バケットの選択

    ハッシュ値からバケットインデックスを計算:

    hash := hash_function(key)
    bucketIndex := hash & (2^B - 1)  // 下位Bビットを使用
    

    💡 なぜ hash % 2^B ではなく hash & (2^B - 1) なのか?

    2^B - 1 は全ビットが1:
    B=3 の場合: 2^3 - 1 = 7 = 0b00000111
    
    hash & 0b00000111 は、下位3ビットを取り出す高速な操作
    (modulo演算より速い)
    
    例:
    hash = 0b10110101
         & 0b00000111
         = 0b00000101 = 5  ← バケットインデックス
    

    バケット構造の詳細

    🔑 bmap(バケット)構造:

    // runtime/map.go
    type bmap struct {
        // tophash[i] は keys[i] のハッシュ値の上位8ビット
        tophash [8]uint8
    
        // 以下のフィールドは実際にはコンパイル時に動的に生成される
        // keys    [8]keytype
        // values  [8]valuetype
        // overflow *bmap  // オーバーフローバケットへのポインタ
    }
    

    実際のメモリレイアウト:

    1つのバケット (bmap):
    ┌────────────────────────────────────────┐
    │ tophash[0..7]: ハッシュ上位8ビット     │
    ├────────────────────────────────────────┤
    │ keys[0..7]:    キー配列(最大8個)     │
    ├────────────────────────────────────────┤
    │ values[0..7]:  値配列(最大8個)       │
    ├────────────────────────────────────────┤
    │ overflow:      オーバーフローへのポインタ│
    └────────────────────────────────────────┘
    
    各バケットは最大8個のキーバリューペアを格納
    

    💡 なぜ8個なのか?

  • キャッシュライン: 現代のCPUのL1キャッシュライン(通常64バイト)に収まる
  • パフォーマンス: 線形探索が許容範囲
  • メモリ効率: 無駄が少ない

完全な例: マップへの挿入

m := make(map[string]int)
m["Alice"] = 25

内部で起こること:

Step 1: ハッシュ値を計算
hash("Alice") = 0x7A3B9C...
                 ^^^^^^^^  上位8ビット(tophash)
                        ^^^  下位ビット(バケット選択)

Step 2: バケットを選択
B = 2 の場合(4個のバケット)
bucketIndex = hash & 0b11 = 2

バケット配列:
┌─────────┬─────────┬─────────┬─────────┐
│ bucket0 │ bucket1 │ bucket2 │ bucket3 │
└─────────┴─────────┴─────────┴─────────┘
                       ↑
                     ここに配置

Step 3: bucket2 の空きスロットに格納
bucket2:
┌──────────────────────────┐
│ tophash: [0x7A, _, _, ...] │ ← ハッシュ上位8ビット
│ keys:    ["Alice", ...]    │
│ values:  [25, ...]         │
└──────────────────────────┘

---

コリジョン(衝突)処理

コリジョンとは

🔑 定義: 異なるキーが同じバケットにマップされること

hash("Alice") = 0x...02
hash("Bob")   = 0x...02  ← 同じバケット!

バケット2に両方格納される必要がある

チェイン法(Goの実装)

Goはオープンチェイン法を使用:

同じバケットに複数の要素:
bucket2:
┌─────────────────────────────────┐
│ tophash: [0x7A, 0x2F, _, ...]   │
│ keys:    ["Alice", "Bob", ...]  │
│ values:  [25, 30, ...]          │
└─────────────────────────────────┘

バケットが満杯の場合:
bucket2:
┌─────────────────────────────────┐
│ tophash: [0x7A, 0x2F, ...(8個)] │
│ keys:    ["Alice", "Bob", ...]  │
│ values:  [25, 30, ...]          │
│ overflow: ─────────────────────┐│
└──────────────────────────────┼──┘
                                ↓
オーバーフローバケット:
┌─────────────────────────────────┐
│ tophash: [0x9C, _, ...]         │
│ keys:    ["Carol", ...]         │
│ values:  [28, ...]              │
│ overflow: nil                   │
└─────────────────────────────────┘

要素の検索プロセス

value := m["Alice"]

内部動作:

Step 1: ハッシュ値を計算
hash("Alice") = 0x7A3B9C...
tophash = 0x7A
bucketIndex = hash & mask = 2

Step 2: bucket2 を探索
bucket2:
┌─────────────────────────────────┐
│ tophash: [0x7A, 0x2F, 0x9C, ...] │
│          ↑
│          0x7A と一致!
│
│ keys[0] == "Alice" ? → Yes!
│ return values[0] → 25
└─────────────────────────────────┘

💡 tophash を先にチェックすることで、
   高速な整数比較でフィルタリングできる

⚠️ 重要: tophashが一致しても、キーの完全一致を確認する必要がある(ハッシュ衝突の可能性)

---

マップの拡張(リハッシュ)

いつ拡張されるのか

🔑 拡張条件:

// runtime/map.go の条件(簡略版)
if count > 8 * (2^B) {
    // 負荷率 > 6.5 → 拡張
    grow()
}

💡 負荷率(Load Factor):

負荷率 = 要素数 / バケット数

例:
要素数 = 20
バケット数 = 4(B=2)
負荷率 = 20 / 4 = 5.0

Goの目標負荷率: ~6.5

拡張プロセス

拡張前: B=2(4バケット)
┌─────────┬─────────┬─────────┬─────────┐
│ bucket0 │ bucket1 │ bucket2 │ bucket3 │
└─────────┴─────────┴─────────┴─────────┘

拡張後: B=3(8バケット)
┌─────────┬─────────┬─────────┬─────────┬─────────┬─────────┬─────────┬─────────┐
│ bucket0 │ bucket1 │ bucket2 │ bucket3 │ bucket4 │ bucket5 │ bucket6 │ bucket7 │
└─────────┴─────────┴─────────┴─────────┴─────────┴─────────┴─────────┴─────────┘

各要素は新しいバケットに再配置される

漸進的リハッシュ

⚠️ 重要: Goは一度に全部リハッシュしない

hmap 構造体:
┌──────────────────────────┐
│ B: 3                     │  ← 新しいサイズ
│ buckets: ────┐           │  ← 新しいバケット配列
│ oldbuckets: ─┼─┐         │  ← 古いバケット配列(まだ存在)
└──────────────┼─┼─────────┘
               │ │
               ↓ ↓
         新 / 旧 バケット

💡 漸進的リハッシュの利点:

  • 大きいマップでも一時停止が短い
  • 読み書き操作の遅延が均一
  • メモリの急激な増加を防ぐ

リハッシュの動作:

マップ操作のたびに、少しずつ移行:

読み取り操作:
1. oldbuckets から探す
2. buckets から探す
3. 見つかった方を返す

書き込み操作:
1. oldbuckets の対応バケットを移行
2. 新しい buckets に書き込む

数回の操作後:
oldbuckets の全要素が buckets に移行完了
→ oldbuckets を解放

---

マップのイテレーション順序

なぜランダムなのか

🔑 3つの理由:

理由1: ハッシュテーブルの性質

キーの挿入順序とバケットの順序は無関係:

挿入順序:
Alice → Bob → Carol

バケット配置(ハッシュ値に依存):
bucket0: Carol
bucket1: Alice
bucket2: Bob

理由2: リハッシュ

拡張前:
bucket0: [Alice, Bob]
bucket1: [Carol]

拡張後: キーが再配置される
bucket0: [Alice]
bucket1: [Carol]
bucket2: []
bucket3: [Bob]

理由3: 意図的なランダム化

// Goの実装:イテレーションの開始位置をランダム化
startBucket := rand.Intn(2^B)

💡 なぜわざわざランダム化?

順序に依存したコードを書くのを防ぐため。

// 悪い例: 順序に依存
m := map[string]int{"a": 1, "b": 2, "c": 3}
for k := range m {
    if k == "a" {
        // "a" が最初に来ると仮定 → 危険!
    }
    break
}

イテレーションの内部動作

for key, value := range m {
    // ...
}

内部処理:
1. ランダムな開始バケットを選択
2. 各バケットを順番に走査
3. 各バケット内の8スロットを走査
4. オーバーフローバケットも走査
5. 全バケット走査後、開始地点に戻る

図解:
┌─────────┬─────────┬─────────┬─────────┐
│ bucket0 │ bucket1 │ bucket2 │ bucket3 │
└─────────┴─────────┴─────────┴─────────┘
              ↑
        ランダム開始(例: bucket1)

走査順序: bucket1 → bucket2 → bucket3 → bucket0

ソート済みイテレーション

順序が必要な場合:

// キーを取り出してソート
keys := make([]string, 0, len(m))
for k := range m {
    keys = append(keys, k)
}
sort.Strings(keys)

// ソート順に処理
for _, k := range keys {
    fmt.Printf("%s: %d\n", k, m[k])
}

メモリレイアウト:

元のマップ(順序なし):
m: bucket0[Carol→28], bucket1[Alice→25], bucket2[Bob→30]

keys スライス(ソート済み):
["Alice", "Bob", "Carol"]

イテレーション:
keys[0] = "Alice" → m["Alice"] = 25
keys[1] = "Bob"   → m["Bob"]   = 30
keys[2] = "Carol" → m["Carol"] = 28

---

マップの削除

deleteの動作

delete(m, "Alice")

内部処理:

Step 1: バケットを特定
hash("Alice") = 0x7A3B9C...
bucketIndex = 2

Step 2: bucket2 内を探索
bucket2:
┌─────────────────────────────────┐
│ tophash: [0x7A, 0x2F, _, ...]   │
│ keys:    ["Alice", "Bob", ...]  │
│ values:  [25, 30, ...]          │
└─────────────────────────────────┘
          ↑
       0x7A を発見

Step 3: 削除をマーク
bucket2:
┌─────────────────────────────────┐
│ tophash: [empty, 0x2F, _, ...]  │ ← tophash[0] = empty
│ keys:    ["", "Bob", ...]       │ ← keys[0] をクリア
│ values:  [0, 30, ...]           │ ← values[0] をゼロ化
└─────────────────────────────────┘

💡 実装の詳細:

  • 要素は物理的に削除されない(スロットは空きマーク)
  • count(要素数)は減少
  • メモリは即座に解放されない(GCが後で回収)

存在しないキーの削除

delete(m, "Nonexistent")  // パニックしない(何も起こらない)

🔑 重要: deleteは安全な操作(存在チェック不要)

---

マップの容量

makeの容量指定

m := make(map[string]int, 100)  // 容量ヒント

内部で起こること:

// runtime/map.go の疑似コード
func makemap(hint int) *hmap {
    // 必要なバケット数を計算
    B := 0
    for overLoadFactor(hint, B) {
        B++
    }

    // 2^B 個のバケットを割り当て
    h := &hmap{
        B:       uint8(B),
        buckets: makeBucketArray(B),
        hash0:   fastrand(),
    }

    return h
}

💡 容量指定の効果:

容量なし:
m := make(map[string]int)
→ B=0(1バケット)から開始
→ 要素追加で複数回リハッシュ

容量あり:
m := make(map[string]int, 100)
→ B=4(16バケット)で開始
→ リハッシュ回数が減る

---

マップの型制約

キーに使える型

🔑 比較可能な型のみ:

// OK: 比較可能な型
map[string]int        // string
map[int]string        // int
map[float64]bool      // float64(注意が必要)
map[*int]string       // ポインタ
map[struct{x int}]int // 比較可能な構造体
map[interface{}]int   // 空インターフェース

⚠️ NG: 比較不可能な型:

// コンパイルエラー
// map[[]int]string       // スライス
// map[map[string]int]int // マップ
// map[func()]int         // 関数

float64 をキーに使う際の注意

m := map[float64]string{}
m[0.1] = "A"

// 浮動小数点の精度問題
val := 0.3 - 0.2  // 理論上 0.1 だが...
fmt.Println(m[val])  // "" (見つからない可能性)

fmt.Println(0.3 - 0.2 == 0.1)  // false!

💡 推奨: float64 キーは避けるか、文字列に変換する

// 解決策: 整数に変換
m := map[int]string{}
key := int(value * 100)  // 小数点2桁を整数化
m[key] = "A"

---

マップの落とし穴

落とし穴1: nilマップへの書き込み

⚠️ 問題:

var m map[string]int  // nil マップ
m["key"] = 1          // panic!

実行時の動作:

Step 1: m は nil ポインタ
m: nil

Step 2: マップに書き込もうとする
runtime が nil チェック
→ panic: assignment to entry in nil map

🔑 解決策:

// 方法1: make で初期化
m := make(map[string]int)
m["key"] = 1  // OK

// 方法2: リテラルで初期化
m := map[string]int{}
m["key"] = 1  // OK

💡 nil マップの読み取りは安全:

var m map[string]int  // nil
value := m["key"]     // OK(ゼロ値を返す)
_, ok := m["key"]     // OK(ok=false)
len(m)                // OK(0を返す)

落とし穴2: マップは参照型

m1 := map[string]int{"a": 1}
m2 := m1  // 参照をコピー

m2["a"] = 99
fmt.Println(m1["a"])  // 99 (m1も変わる!)

メモリレイアウト:

スタック:
┌────────────┐
│ m1: 0x1000 │─┐
└────────────┘ │
┌────────────┐ │
│ m2: 0x1000 │─┤ ← 同じアドレスを指す
└────────────┘ │
               ↓
ヒープ:
0x1000: hmap{count:1, ...}
         └→ buckets → ["a": 99]

🔑 解決策: マップをコピーする

// ディープコピー
m2 := make(map[string]int, len(m1))
for k, v := range m1 {
    m2[k] = v
}

m2["a"] = 99
fmt.Println(m1["a"])  // 1 (変わらない)

落とし穴3: イテレーション中の変更

⚠️ 問題:

m := map[string]int{"a": 1, "b": 2, "c": 3}

// 削除は一般的にOK
for k := range m {
    if m[k] == 2 {
        delete(m, k)  // 安全
    }
}

// 追加は予測不可能
for k := range m {
    m[k+"_new"] = m[k]  // 危険!
    // 追加した要素がイテレーションに含まれるかは未定義
}

🔑 解決策: キーを先に収集

keys := make([]string, 0, len(m))
for k := range m {
    keys = append(keys, k)
}

// 収集したキーを使って追加
for _, k := range keys {
    m[k+"_new"] = m[k]  // 安全
}

落とし穴4: 並行アクセス

⚠️ 問題: マップは並行安全ではない

m := make(map[string]int)

// 競合状態(race condition)
go func() {
    m["key"] = 1  // 書き込み
}()
go func() {
    m["key"] = 2  // 書き込み
}()
// → panic または不正な状態

Goのランタイムは競合を検出:

fatal error: concurrent map writes

goroutine 1:
マップに書き込み中

goroutine 2:
同時にマップに書き込み

→ ランタイムが検出してパニック

🔑 解決策1: sync.Mutex

var mu sync.Mutex
var m = make(map[string]int)

// 書き込み
mu.Lock()
m["key"] = 1
mu.Unlock()

// 読み込み
mu.Lock()
value := m["key"]
mu.Unlock()

🔑 解決策2: sync.RWMutex(読み込みが多い場合)

var mu sync.RWMutex
var m = make(map[string]int)

// 書き込み
mu.Lock()
m["key"] = 1
mu.Unlock()

// 読み込み(複数同時OK)
mu.RLock()
value := m["key"]
mu.RUnlock()

---

sync.Map: 並行安全なマップ

sync.Map の特徴

🔑 通常のmap vs sync.Map:

┌────────────────┬──────────────────┬────────────────────┐
│  特性          │  map + Mutex     │  sync.Map          │
├────────────────┼──────────────────┼────────────────────┤
│ 型安全         │  ○               │  × (interface{})   │
│ パフォーマンス │  単純            │  最適化済み        │
│ ユースケース   │  汎用            │  特定パターン      │
│ API            │  シンプル        │  やや複雑          │
└────────────────┴──────────────────┴────────────────────┘

sync.Map の内部構造

sync.Map:
┌────────────────────────────────────┐
│ read:  atomic.Value                │ ← 読み取り専用マップ(高速)
│        └→ readOnly{m: map[...]}    │
│                                    │
│ dirty: map[interface{}]*entry      │ ← 書き込み用マップ
│ mu:    sync.Mutex                  │ ← dirtyの保護用
│ misses: int                        │ ← キャッシュミスカウント
└────────────────────────────────────┘

読み取り: read から(ロックなし)
書き込み: dirty へ(ロックあり)

sync.Map の使い方

var m sync.Map

// 書き込み
m.Store("Alice", 25)
m.Store("Bob", 30)

// 読み込み
if value, ok := m.Load("Alice"); ok {
    age := value.(int)  // 型アサーション必須
    fmt.Println("Alice:", age)
}

// 削除
m.Delete("Bob")

// 存在すれば読み込み、なければ書き込み
actual, loaded := m.LoadOrStore("Carol", 28)
if loaded {
    fmt.Println("Carol already exists:", actual)
} else {
    fmt.Println("Carol added with age 28")
}

// 存在すれば削除して値を返す
if value, loaded := m.LoadAndDelete("Alice"); loaded {
    fmt.Println("Deleted:", value)
}

// イテレーション
m.Range(func(key, value interface{}) bool {
    fmt.Printf("%s: %v\n", key, value)
    return true  // false を返すと中断
})

sync.Map の最適なユースケース

sync.Map が適している:

  • 読み取りが多い (read-heavy)
  • 書き込みが少ない
  • キーのセットが安定 (頻繁に追加削除しない)

sync.Map が不適:

  • 書き込みが多い (write-heavy)
  • 型安全が重要
  • 複雑な操作 (トランザクションなど)

// 適している例: 設定キャッシュ
type ConfigCache struct {
    cache sync.Map
}

func (c *ConfigCache) Get(key string) (string, bool) {
    value, ok := c.cache.Load(key)
    if !ok {
        return "", false
    }
    return value.(string), true
}

func (c *ConfigCache) Set(key, value string) {
    c.cache.Store(key, value)
}

// 不適な例: 頻繁な更新
// 通常の map + Mutex の方が良い

---

マップの実用パターン

パターン1: セット(集合)の実装

// map[T]bool でセット
type StringSet map[string]bool

func (s StringSet) Add(value string) {
    s[value] = true
}

func (s StringSet) Remove(value string) {
    delete(s, value)
}

func (s StringSet) Contains(value string) bool {
    return s[value]  // 存在しなければ false
}

func (s StringSet) Size() int {
    return len(s)
}

// 使用例
set := make(StringSet)
set.Add("apple")
set.Add("banana")
set.Add("apple")  // 重複は無視

fmt.Println(set.Contains("apple"))  // true
fmt.Println(set.Size())              // 2

💡 省メモリ版: struct{} を使う

// map[T]struct{} はメモリ効率が良い
type StringSet map[string]struct{}

func (s StringSet) Add(value string) {
    s[value] = struct{}{}  // 空の構造体(0バイト)
}

func (s StringSet) Contains(value string) bool {
    _, exists := s[value]
    return exists
}

メモリ比較:

┌──────────────────┬──────────────┬──────────────┐
│  型              │  キー8B      │  値          │
├──────────────────┼──────────────┼──────────────┤
│ map[string]bool  │ 8バイト      │ 1バイト      │
│ map[string]struct{}│ 8バイト    │ 0バイト      │
└──────────────────┴──────────────┴──────────────┘

1000要素のセット:
map[string]bool:     ~9000バイト
map[string]struct{}: ~8000バイト
節約: ~11%

パターン2: 頻度カウント

func countWords(text string) map[string]int {
    counts := make(map[string]int)
    words := strings.Fields(text)

    for _, word := range words {
        counts[word]++  // 存在しなければ0、存在すれば増加
    }

    return counts
}

text := "apple banana apple cherry banana apple"
counts := countWords(text)
// map[apple:3 banana:2 cherry:1]

パターン3: グルーピング

type Person struct {
    Name string
    City string
}

func groupByCity(people []Person) map[string][]Person {
    groups := make(map[string][]Person)

    for _, p := range people {
        groups[p.City] = append(groups[p.City], p)
    }

    return groups
}

people := []Person{
    {"Alice", "Tokyo"},
    {"Bob", "Osaka"},
    {"Carol", "Tokyo"},
}

groups := groupByCity(people)
// map[Tokyo:[{Alice Tokyo} {Carol Tokyo}] Osaka:[{Bob Osaka}]]

パターン4: メモ化(キャッシュ)

type Cache struct {
    mu    sync.RWMutex
    store map[string]Result
}

type Result struct {
    Value interface{}
    Time  time.Time
}

func (c *Cache) Get(key string) (interface{}, bool) {
    c.mu.RLock()
    defer c.mu.RUnlock()

    result, ok := c.store[key]
    if !ok {
        return nil, false
    }

    // TTL チェック
    if time.Since(result.Time) > 5*time.Minute {
        return nil, false
    }

    return result.Value, true
}

func (c *Cache) Set(key string, value interface{}) {
    c.mu.Lock()
    defer c.mu.Unlock()

    c.store[key] = Result{
        Value: value,
        Time:  time.Now(),
    }
}

---

マップのパフォーマンス最適化

最適化1: 容量の事前確保

// 悪い例
m := make(map[string]int)
for i := 0; i < 1000000; i++ {
    m[fmt.Sprintf("key%d", i)] = i
}

// 良い例
m := make(map[string]int, 1000000)
for i := 0; i < 1000000; i++ {
    m[fmt.Sprintf("key%d", i)] = i
}

パフォーマンス差:

リハッシュ回数:
悪い例: log₂(1000000) ≈ 20回
良い例: 0-1回

実行時間:
悪い例: ~500ms
良い例: ~300ms  ← 40%高速化

最適化2: マップ vs スライス

小さいデータセット(<100要素)では、スライスの方が速い場合がある:

// マップ: O(1) だが定数コストが高い
m := map[string]int{
    "Alice": 25,
    "Bob":   30,
}
age := m["Alice"]

// スライス: O(n) だが小さいnでは速い
type KeyValue struct {
    Key   string
    Value int
}
slice := []KeyValue{
    {"Alice", 25},
    {"Bob", 30},
}
for _, kv := range slice {
    if kv.Key == "Alice" {
        age := kv.Value
        break
    }
}

ベンチマーク結果(概算):

┌───────────┬─────────┬─────────┬─────────┐
│  要素数   │  マップ │ スライス│  推奨   │
├───────────┼─────────┼─────────┼─────────┤
│  10       │  20ns   │  10ns   │ slice   │
│  100      │  25ns   │  50ns   │ map     │
│  1000     │  30ns   │  500ns  │ map     │
└───────────┴─────────┴─────────┴─────────┘

最適化3: キーの型

// 遅い: string キー(動的メモリ、ハッシュコスト高)
m1 := map[string]int{
    "key1": 1,
    "key2": 2,
}

// 速い: int キー(固定サイズ、ハッシュコスト低)
m2 := map[int]int{
    1: 1,
    2: 2,
}

// 中間: 小さい構造体キー
type Key struct {
    A, B int
}
m3 := map[Key]int{
    {1, 2}: 1,
}

---

まとめ

この章では、Goのマップについて深く学びました。

重要ポイント

🔑 内部構造:

  • ハッシュテーブル(バケット配列)
  • 各バケットは最大8要素
  • オーバーフローチェーン
  • 漸進的リハッシュ

🔑 特性:

  • 平均 O(1) の操作
  • イテレーション順序はランダム
  • 負荷率 ~6.5 で拡張
  • 並行安全ではない

🔑 落とし穴:

  • nil マップへの書き込み
  • 参照型(共有される)
  • イテレーション中の変更
  • 並行アクセス

ベストプラクティス

推奨:

  • makeで容量を事前確保
  • キーの存在チェックは2番目の戻り値
  • 並行アクセスには Mutex または sync.Map
  • struct{} を使って省メモリなセット
  • 順序が必要ならキーをソート

非推奨:

  • nil マップへの書き込み
  • イテレーション順序への依存
  • 並行アクセスの保護なし
  • float64 キー(精度問題)

---

次の章では、構造体について学びます。構造体はGoにおける最も重要なデータ型の一つです。