Day 7: マップ - 解説

学習の目的

  • キー値ペアの管理: 関連するデータの紐付け
  • 高速な検索: O(1)での値の取得
  • カンマokイディオム: 安全な値の取得
  • ハッシュテーブルの理解: 内部動作の把握
  • 実践的パターン: 頻出する使用例の習得

---

概念の深掘り

1. マップのゼロ値とnil map

マップのゼロ値はnilです。nil mapには特別な振る舞いがあります。

var m map[string]int  // nil map

// 読み取りは可能(常にゼロ値を返す)
value := m["key"]  // 0
len(m)             // 0

// しかし書き込みはパニックを起こす
// m["key"] = 1  // panic: assignment to entry in nil map

// 使用前に初期化が必要
m = make(map[string]int)
m["key"] = 1  // OK

nil mapの使用例:

// 関数の戻り値としてnil mapを返す
func getUserPreferences(userID string) map[string]string {
    // ユーザーが見つからない場合
    if !userExists(userID) {
        return nil  // nil mapを返す
    }
    // 設定を返す
    return loadPreferences(userID)
}

// 呼び出し側
prefs := getUserPreferences("user123")
if prefs == nil {
    fmt.Println("ユーザーが見つかりません")
} else {
    // 設定を使用
    theme := prefs["theme"]
}

nil mapと空mapの違い:

var nilMap map[string]int        // nil
emptyMap := map[string]int{}     // 空だがnilではない
madeMap := make(map[string]int)  // 空だがnilではない

fmt.Println(nilMap == nil)    // true
fmt.Println(emptyMap == nil)  // false
fmt.Println(madeMap == nil)   // false

// どちらも長さは0
fmt.Println(len(nilMap))    // 0
fmt.Println(len(emptyMap))  // 0

// JSONエンコード時の違い
json.Marshal(nilMap)    // "null"
json.Marshal(emptyMap)  // "{}"

---

2. 存在しないキーへのアクセス

存在しないキーにアクセスすると、値の型のゼロ値が返されます。

m := map[string]int{"a": 1}

// 存在しないキーへのアクセス
value := m["b"]  // 0(intのゼロ値)
fmt.Println(value)

// 問題: 0が「存在しない」のか「値が0」なのか区別できない
m["c"] = 0  // 明示的に0を設定
fmt.Println(m["b"])  // 0(存在しない)
fmt.Println(m["c"])  // 0(値が0)

これが意図的なのか、キーが存在しないのか区別するためにカンマokイディオムを使います。

// カンマokイディオム
if value, ok := m["b"]; ok {
    fmt.Println("Found:", value)
} else {
    fmt.Println("Not found")
}

---

3. マップの内部構造

Goのマップはハッシュテーブルとして実装されています。

ハッシュテーブルの基本原理

キー → ハッシュ関数 → ハッシュ値 → バケット番号 → 値

例:

m := map[string]int{
    "apple":  100,
    "banana": 120,
}

// 内部的には:
// 1. "apple" → hash("apple") → 12345678
// 2. 12345678 % バケット数 → バケット#5
// 3. バケット#5に("apple", 100)を格納

バケット構造

バケット配列:
┌────────┬────────┬────────┬────────┬────────┐
│ Bucket │ Bucket │ Bucket │ Bucket │ Bucket │
│   0    │   1    │   2    │   3    │   4    │
└────────┴────────┴────────┴────────┴────────┘
              │
              ├─> ("apple", 100)
              ├─> ("apricot", 80)
              └─> (overflow bucket) → ...

重要な特性:

  • 平均O(1)アクセス: ハッシュ値で直接バケットに飛べる
  • 衝突処理: 同じバケットに複数の要素を格納
  • 動的リサイズ: 要素が増えるとバケット数を増やす

---

4. マップの計算量

操作 平均計算量 最悪計算量 備考
追加 O(1) O(n) リサイズ時
取得 O(1) O(n) 衝突時
削除 O(1) O(n) 衝突時
反復 O(n) O(n) 全要素を訪問

実測例:

func BenchmarkMapOperations(b *testing.B) {
    // 100万要素のマップ
    m := make(map[int]int, 1000000)
    for i := 0; i < 1000000; i++ {
        m[i] = i
    }

    b.Run("Get", func(b *testing.B) {
        for i := 0; i < b.N; i++ {
            _ = m[500000]  // ほぼ瞬時(O(1))
        }
    })

    b.Run("Insert", func(b *testing.B) {
        for i := 0; i < b.N; i++ {
            m[1000000+i] = i  // ほぼ瞬時(O(1))
        }
    })
}

---

アルゴリズム分析

パターン1: 頻度カウンター

問題: 配列内の各要素の出現回数を数える

アルゴリズム:

func countFrequency(items []string) map[string]int {
    counter := make(map[string]int)
    for _, item := range items {
        counter[item]++  // ゼロ値からカウント開始
    }
    return counter
}

計算量:

  • 時間: O(n) - 配列を1回走査
  • 空間: O(k) - kは異なる要素の数

最適化:

// 容量ヒント付き(要素数の見積もりがある場合)
func countFrequencyOptimized(items []string, expectedUnique int) map[string]int {
    counter := make(map[string]int, expectedUnique)
    for _, item := range items {
        counter[item]++
    }
    return counter
}

---

パターン2: Two Sum問題

問題: 配列から2つの数を見つけ、その合計が目標値になる組み合わせを返す

ナイーブな解法(二重ループ):

func twoSumNaive(nums []int, target int) []int {
    for i := 0; i < len(nums); i++ {
        for j := i + 1; j < len(nums); j++ {
            if nums[i]+nums[j] == target {
                return []int{i, j}
            }
        }
    }
    return nil
}
// 時間計算量: O(n²)

マップを使った最適化:

func twoSum(nums []int, target int) []int {
    // 値 → インデックスのマップ
    seen := make(map[int]int, len(nums))

    for i, num := range nums {
        complement := target - num
        if j, ok := seen[complement]; ok {
            return []int{j, i}
        }
        seen[num] = i
    }
    return nil
}
// 時間計算量: O(n)
// 空間計算量: O(n)

アルゴリズムの流れ:

入力: [2, 7, 11, 15], target=9

ステップ1: num=2, i=0
  complement=7, seenに7なし
  seen={2:0}

ステップ2: num=7, i=1
  complement=2, seenに2あり!
  return [0, 1]

---

パターン3: グループ化(アナグラム)

問題: 文字列の配列をアナグラムでグループ化

func groupAnagrams(strs []string) [][]string {
    // ソート済み文字列 → 元の文字列のリスト
    groups := make(map[string][]string)

    for _, str := range strs {
        // 文字列をソートしてキーにする
        key := sortString(str)
        groups[key] = append(groups[key], str)
    }

    // マップから値だけを抽出
    result := make([][]string, 0, len(groups))
    for _, group := range groups {
        result = append(result, group)
    }
    return result
}

func sortString(s string) string {
    runes := []rune(s)
    sort.Slice(runes, func(i, j int) bool {
        return runes[i] < runes[j]
    })
    return string(runes)
}

// 使用例
strs := []string{"eat", "tea", "tan", "ate", "nat", "bat"}
groups := groupAnagrams(strs)
// [["eat", "tea", "ate"], ["tan", "nat"], ["bat"]]

計算量:

  • 時間: O(n×k×log(k)) - n個の文字列、各長さk
  • 空間: O(n×k)

---

設計原則とパターン

1. マップをセットとして使う

Goには組み込みのセット型がありませんが、マップで代用できます。

// map[T]boolまたはmap[T]struct{}を使う
type Set map[string]struct{}

func NewSet() Set {
    return make(Set)
}

func (s Set) Add(item string) {
    s[item] = struct{}{}  // 空の構造体(メモリ0バイト)
}

func (s Set) Contains(item string) bool {
    _, ok := s[item]
    return ok
}

func (s Set) Remove(item string) {
    delete(s, item)
}

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

// 使用例
fruits := NewSet()
fruits.Add("apple")
fruits.Add("banana")
fruits.Add("apple")  // 重複は無視される

fmt.Println(fruits.Contains("apple"))   // true
fmt.Println(fruits.Contains("orange"))  // false
fmt.Println(fruits.Size())              // 2

struct{}を使う理由:

// boolは1バイト消費
setBool := make(map[string]bool)
setBool["key"] = true

// struct{}は0バイト
setStruct := make(map[string]struct{})
setStruct["key"] = struct{}{}

// 100万要素で約1MBの差

---

2. マップのマージ

複数のマップを1つに統合するパターン:

// 方法1: ループで結合
func mergeMaps(m1, m2 map[string]int) map[string]int {
    result := make(map[string]int, len(m1)+len(m2))

    // m1をコピー
    for k, v := range m1 {
        result[k] = v
    }

    // m2をコピー(重複する場合は上書き)
    for k, v := range m2 {
        result[k] = v
    }

    return result
}

// 方法2: 値を集約(加算)
func mergeMapsSum(m1, m2 map[string]int) map[string]int {
    result := make(map[string]int)

    for k, v := range m1 {
        result[k] += v
    }

    for k, v := range m2 {
        result[k] += v  // 既存の値に加算
    }

    return result
}

// 使用例
m1 := map[string]int{"a": 1, "b": 2}
m2 := map[string]int{"b": 3, "c": 4}

merged := mergeMaps(m1, m2)
// {"a": 1, "b": 3, "c": 4}

summed := mergeMapsSum(m1, m2)
// {"a": 1, "b": 5, "c": 4}

---

3. マップの反転

キーと値を入れ替えるパターン:

func reverseMap(m map[string]int) map[int]string {
    reversed := make(map[int]string, len(m))

    for k, v := range m {
        // 注意: 値が重複する場合、後勝ちになる
        reversed[v] = k
    }

    return reversed
}

// 使用例
original := map[string]int{
    "apple":  1,
    "banana": 2,
    "cherry": 3,
}

reversed := reverseMap(original)
// map[int]string{1: "apple", 2: "banana", 3: "cherry"}

重複値の処理:

func reverseMapMultiple(m map[string]int) map[int][]string {
    reversed := make(map[int][]string)

    for k, v := range m {
        reversed[v] = append(reversed[v], k)
    }

    return reversed
}

// 使用例
grades := map[string]int{
    "Alice": 90,
    "Bob":   85,
    "Carol": 90,
    "Dave":  85,
}

byScore := reverseMapMultiple(grades)
// map[int][]string{
//     90: []string{"Alice", "Carol"},
//     85: []string{"Bob", "Dave"},
// }

---

メンタルモデル

マップの理解を深めるイメージ

マップ = ロッカールーム

ロッカー番号(キー) → ロッカー(値)の関連付け

"Alice" → ロッカー#42 → バッグ
"Bob"   → ロッカー#17 → コート
"Carol" → ロッカー#8  → 靴

// 高速アクセス
name := "Alice"
locker := lockers[name]  // 直接ロッカー#42に行ける

ハッシュ関数 = ロッカー番号の決定方法

名前 → ハッシュ関数 → ロッカー番号

"Alice" → hash("Alice") → 42
"Bob"   → hash("Bob")   → 17

衝突 = 同じロッカー番号に複数の荷物

ロッカー#42:
  - "Alice"のバッグ
  - "Alicia"のコート(衝突)
  → リストで管理

---

カンマokイディオムのメンタルモデル

// 一般的なパターン
value, ok := m[key]

// 「valueとokの2つの情報を得る」
// value: キーに対応する値(存在しない場合はゼロ値)
// ok:    キーが存在するかどうか(true/false)

類似パターン:

// 型アサーション
value, ok := interfaceValue.(ConcreteType)

// チャネル受信
value, ok := <-channel

// どれも「値, 成功フラグ」の形式

---

よくある間違いと対策

間違い1: マップの反復中に要素を追加・削除

// 間違い: 危険!
m := map[string]int{"a": 1, "b": 2, "c": 3}
for k, v := range m {
    if v%2 == 0 {
        delete(m, k)  // 反復中の削除は安全だが注意が必要
    }
    m[k+"_new"] = v  // 追加した要素が反復に含まれるかは未定義
}

// 正しい方法: 削除するキーを記録してから削除
toDelete := make([]string, 0)
for k, v := range m {
    if v%2 == 0 {
        toDelete = append(toDelete, k)
    }
}
for _, k := range toDelete {
    delete(m, k)
}

---

間違い2: マップのコピー

// 間違い: 浅いコピー
m1 := map[string]int{"a": 1, "b": 2}
m2 := m1  // ポインタのコピー!

m2["a"] = 100
fmt.Println(m1["a"])  // 100(m1も変更される)

// 正しい方法: 深いコピー
m2 := make(map[string]int, len(m1))
for k, v := range m1 {
    m2[k] = v
}

m2["a"] = 100
fmt.Println(m1["a"])  // 1(m1は変更されない)

---

間違い3: マップの比較

// コンパイルエラー
m1 := map[string]int{"a": 1}
m2 := map[string]int{"a": 1}
// if m1 == m2 { }  // エラー: invalid operation

// 正しい方法: reflect.DeepEqual
import "reflect"
if reflect.DeepEqual(m1, m2) {
    fmt.Println("Equal")
}

// または手動で比較
func equalMaps(m1, m2 map[string]int) bool {
    if len(m1) != len(m2) {
        return false
    }
    for k, v1 := range m1 {
        if v2, ok := m2[k]; !ok || v1 != v2 {
            return false
        }
    }
    return true
}

---

実践的な応用例

例1: LRUキャッシュ

type LRUCache struct {
    capacity int
    cache    map[int]*list.Element
    list     *list.List
}

type entry struct {
    key   int
    value int
}

func NewLRUCache(capacity int) *LRUCache {
    return &LRUCache{
        capacity: capacity,
        cache:    make(map[int]*list.Element),
        list:     list.New(),
    }
}

func (c *LRUCache) Get(key int) int {
    if elem, ok := c.cache[key]; ok {
        c.list.MoveToFront(elem)
        return elem.Value.(*entry).value
    }
    return -1
}

func (c *LRUCache) Put(key, value int) {
    if elem, ok := c.cache[key]; ok {
        c.list.MoveToFront(elem)
        elem.Value.(*entry).value = value
        return
    }

    elem := c.list.PushFront(&entry{key, value})
    c.cache[key] = elem

    if c.list.Len() > c.capacity {
        oldest := c.list.Back()
        if oldest != nil {
            c.list.Remove(oldest)
            delete(c.cache, oldest.Value.(*entry).key)
        }
    }
}

---

例2: グラフの隣接リスト表現

type Graph struct {
    // ノード -> 隣接ノードのリスト
    adjacencyList map[string][]string
}

func NewGraph() *Graph {
    return &Graph{
        adjacencyList: make(map[string][]string),
    }
}

func (g *Graph) AddEdge(from, to string) {
    g.adjacencyList[from] = append(g.adjacencyList[from], to)
}

func (g *Graph) BFS(start string) []string {
    visited := make(map[string]bool)
    queue := []string{start}
    result := []string{}

    for len(queue) > 0 {
        node := queue[0]
        queue = queue[1:]

        if visited[node] {
            continue
        }

        visited[node] = true
        result = append(result, node)

        for _, neighbor := range g.adjacencyList[node] {
            if !visited[neighbor] {
                queue = append(queue, neighbor)
            }
        }
    }

    return result
}

---

セルフチェック問題

基礎レベル

  • nil mapに値を追加しようとするとどうなりますか?
  • 次のコードの出力は?
m := map[string]int{"a": 0}
fmt.Println(m["a"])
fmt.Println(m["b"])

  • マップの要素数を取得する関数は?
  • 中級レベル

  • なぜマップの反復順序は保証されないのですか?
  • 次のコードの問題点は?
m1 := map[string]int{"a": 1}
m2 := m1
m2["b"] = 2

  • map[string]boolmap[string]struct{}の違いは?
  • 上級レベル

  • Two Sum問題をO(n)で解く実装を説明してください。
  • マップを使ってLRUキャッシュを実装する場合、どのようなデータ構造と組み合わせますか?
  • 100万要素のマップで検索操作の平均時間計算量は?

---

Further Exploration: 高度なトピック

1. sync.Map による並行アクセス

標準のmapは並行安全ではありませんが、sync.Mapを使用すると複数のゴルーチンから安全にアクセスできます。

package main

import (
    "fmt"
    "sync"
)

func main() {
    var m sync.Map

    // 並行書き込み
    var wg sync.WaitGroup
    for i := 0; i < 10; i++ {
        wg.Add(1)
        go func(n int) {
            defer wg.Done()
            m.Store(fmt.Sprintf("key%d", n), n*10)
        }(i)
    }
    wg.Wait()

    // 読み取り
    m.Range(func(key, value interface{}) bool {
        fmt.Printf("%s: %v\n", key, value)
        return true
    })

    // LoadOrStore(存在しない場合のみ保存)
    actual, loaded := m.LoadOrStore("key5", 999)
    fmt.Printf("Loaded: %v, Value: %v\n", loaded, actual)
}

sync.Map vs Mutex + map:

// アプローチ1: Mutex + map
type MutexMap struct {
    mu sync.RWMutex
    m  map[string]int
}

func (mm *MutexMap) Get(key string) (int, bool) {
    mm.mu.RLock()
    defer mm.mu.RUnlock()
    val, ok := mm.m[key]
    return val, ok
}

// アプローチ2: sync.Map(読み取り主体の場合に有利)
var sm sync.Map

// いつsync.Mapを使うべきか:
// 1. 読み取りが圧倒的に多い(90%以上)
// 2. キーセットが比較的安定している
// 3. 型の柔軟性が必要(interface{})

// いつMutex + mapを使うべきか:
// 1. 書き込みが多い
// 2. 型安全性が重要
// 3. コードの可読性を重視

---

2. カスタムハッシュ関数の実装

独自のハッシュ可能な型を定義できます。

package main

import (
    "fmt"
    "hash/fnv"
)

// カスタム型
type Point struct {
    X, Y int
}

// Pointをマップのキーとして使用(比較可能)
func main() {
    points := map[Point]string{
        {X: 0, Y: 0}: "Origin",
        {X: 1, Y: 1}: "Diagonal",
    }

    fmt.Println(points[Point{0, 0}])  // "Origin"
}

// より高度な例:カスタムハッシュマップ
type CustomKey struct {
    Data string
}

func (ck CustomKey) Hash() uint64 {
    h := fnv.New64a()
    h.Write([]byte(ck.Data))
    return h.Sum64()
}

type CustomHashMap struct {
    buckets map[uint64][]entry
}

type entry struct {
    key   CustomKey
    value interface{}
}

func NewCustomHashMap() *CustomHashMap {
    return &CustomHashMap{
        buckets: make(map[uint64][]entry),
    }
}

func (chm *CustomHashMap) Set(key CustomKey, value interface{}) {
    hash := key.Hash()
    entries := chm.buckets[hash]

    for i, e := range entries {
        if e.key == key {
            entries[i].value = value
            return
        }
    }

    chm.buckets[hash] = append(entries, entry{key, value})
}

func (chm *CustomHashMap) Get(key CustomKey) (interface{}, bool) {
    hash := key.Hash()
    entries := chm.buckets[hash]

    for _, e := range entries {
        if e.key == key {
            return e.value, true
        }
    }

    return nil, false
}

---

3. メモリレイアウトと最適化

// 小さい値型(int, boolなど)
// 値が直接バケットに格納される
map[string]int

// 大きい値型
// ポインタのみがバケットに格納される
map[string][]byte

// 最適化テクニック:

// 1. 容量ヒント
m := make(map[string]int, 10000)  // リハッシュを最小化

// 2. 値のサイズを小さく保つ
// 悪い例:大きな構造体を直接格納
type LargeStruct struct {
    Data [1000]int
}
m := make(map[string]LargeStruct)  // 多くのメモリを消費

// 良い例:ポインタを格納
m := make(map[string]*LargeStruct)  // メモリ効率的

// 3. 不要なメモリの解放
// 削除してもバケットは縮小されない
for k := range m {
    delete(m, k)
}
// 新しいマップを作成してGCに任せる
m = make(map[string]int)

---

4. 他言語のマップ実装との比較

言語 デフォルト値 型安全性 並行安全性 パフォーマンス
Go ゼロ値 強い なし(sync.Map使用) 高速
Python KeyError 弱い GILで安全 中速
JavaScript undefined 弱い シングルスレッド 高速
Java null 強い ConcurrentHashMap 高速
Rust Option 最強 所有権で安全 最速

// Go
m := map[string]int{"a": 1}
value, ok := m["c"]  // 0, false

// Python (比較)
# m = {"a": 1}
# value = m.get("c", None)  # None

// JavaScript (比較)
// const m = new Map([["a", 1]]);
// const value = m.get("c");  // undefined

// Rust (比較)
// let mut m = HashMap::new();
// m.insert("a", 1);
// let value = m.get("c");  // Option::None

---

Production Considerations: 本番環境での注意点

1. 並行アクセスの安全性

// 危険な例
var cache = make(map[string]interface{})

func handler(w http.ResponseWriter, r *http.Request) {
    cache[r.URL.Path] = "data"  // データ競合!
}

// 安全な例1: sync.RWMutex
type SafeCache struct {
    mu    sync.RWMutex
    cache map[string]interface{}
}

func (sc *SafeCache) Get(key string) (interface{}, bool) {
    sc.mu.RLock()
    defer sc.mu.RUnlock()
    val, ok := sc.cache[key]
    return val, ok
}

// 安全な例2: チャネルベースのアクター
type CacheActor struct {
    cache map[string]interface{}
    ops   chan func(map[string]interface{})
}

func NewCacheActor() *CacheActor {
    ca := &CacheActor{
        cache: make(map[string]interface{}),
        ops:   make(chan func(map[string]interface{})),
    }
    go ca.run()
    return ca
}

func (ca *CacheActor) run() {
    for op := range ca.ops {
        op(ca.cache)
    }
}

---

2. メモリリークの防止

// 問題のあるコード
type Server struct {
    sessions map[string]*Session
}

func (s *Server) HandleRequest(sessionID string) {
    session := &Session{
        ID:        sessionID,
        CreatedAt: time.Now(),
        Data:      make([]byte, 1024*1024),  // 1MB
    }
    s.sessions[sessionID] = session
    // 古いセッションを削除していない!
}

// 改善版: TTLベースのクリーンアップ
type Server struct {
    sessions map[string]*Session
    mu       sync.RWMutex
}

func (s *Server) CleanupOldSessions() {
    ticker := time.NewTicker(5 * time.Minute)
    for range ticker.C {
        s.mu.Lock()
        now := time.Now()
        for id, session := range s.sessions {
            if now.Sub(session.CreatedAt) > 30*time.Minute {
                delete(s.sessions, id)
            }
        }
        s.mu.Unlock()
    }
}

---

3. パフォーマンスモニタリング

type MonitoredCache struct {
    cache    map[string]interface{}
    mu       sync.RWMutex
    hits     uint64
    misses   uint64
}

func (mc *MonitoredCache) Get(key string) (interface{}, bool) {
    mc.mu.RLock()
    val, ok := mc.cache[key]
    mc.mu.RUnlock()

    if ok {
        atomic.AddUint64(&mc.hits, 1)
    } else {
        atomic.AddUint64(&mc.misses, 1)
    }

    return val, ok
}

func (mc *MonitoredCache) Stats() map[string]uint64 {
    return map[string]uint64{
        "hits":   atomic.LoadUint64(&mc.hits),
        "misses": atomic.LoadUint64(&mc.misses),
        "size":   uint64(len(mc.cache)),
    }
}

// 定期的なレポート
go func() {
    ticker := time.NewTicker(1 * time.Minute)
    for range ticker.C {
        stats := cache.Stats()
        hitRate := float64(stats["hits"]) / float64(stats["hits"]+stats["misses"]) * 100
        log.Printf("Cache hit rate: %.2f%%, size: %d", hitRate, stats["size"])
    }
}()

---

Team Collaboration: チームでのコードレビューポイント

レビューチェックリスト

// ✓ 並行安全性
// ✗ 問題あり
var cache = make(map[string]int)
func updateCache(key string, val int) {
    cache[key] = val  // データ競合
}

// ✓ 問題なし
type Cache struct {
    mu sync.RWMutex
    m  map[string]int
}

// ✓ nil チェック
// ✗ 問題あり
var m map[string]int
m["key"] = 1  // panic

// ✓ 問題なし
m = make(map[string]int)
m["key"] = 1

// ✓ 存在確認
// ✗ 推奨されない
val := m["key"]  // ゼロ値が返る可能性

// ✓ 推奨
val, ok := m["key"]
if !ok {
    // キーが存在しない
}

// ✓ 容量ヒント
// ✗ 最適化の機会損失
m := make(map[string]int)
for i := 0; i < 10000; i++ {
    m[fmt.Sprintf("key%d", i)] = i
}

// ✓ 最適化済み
m := make(map[string]int, 10000)

---

コードレビューコメントの例

// レビュー前
type UserService struct {
    users map[string]User
}

func (us *UserService) GetUser(id string) User {
    return us.users[id]
}

// レビューコメント:
// 1. nil mapの可能性があります。コンストラクタを追加してください。
// 2. 存在しないユーザーの場合、ゼロ値が返りますが、
//    呼び出し側でエラーハンドリングできません。
// 3. 並行アクセスの可能性がある場合、sync.RWMutexを追加してください。

// レビュー後
type UserService struct {
    mu    sync.RWMutex
    users map[string]*User
}

func NewUserService() *UserService {
    return &UserService{
        users: make(map[string]*User),
    }
}

func (us *UserService) GetUser(id string) (*User, error) {
    us.mu.RLock()
    defer us.mu.RUnlock()

    user, ok := us.users[id]
    if !ok {
        return nil, fmt.Errorf("user not found: %s", id)
    }
    return user, nil
}

---

Career Growth: キャリアへの貢献

1. マップの理解がキャリアにもたらす価値

技術面接での優位性:

// 典型的な面接問題: Two Sum

// 初心者の解答(ブルートフォース)- O(n²)
func twoSum(nums []int, target int) []int {
    for i := 0; i < len(nums); i++ {
        for j := i + 1; j < len(nums); j++ {
            if nums[i]+nums[j] == target {
                return []int{i, j}
            }
        }
    }
    return nil
}

// 経験者の解答(ハッシュマップ)- O(n)
func twoSum(nums []int, target int) []int {
    seen := make(map[int]int)
    for i, num := range nums {
        complement := target - num
        if j, ok := seen[complement]; ok {
            return []int{j, i}
        }
        seen[num] = i
    }
    return nil
}

システム設計面接:

// 問題: URLショートナーの設計

type URLShortener struct {
    urlMap     map[string]string  // 短縮URL → 元のURL
    reverseMap map[string]string  // 元のURL → 短縮URL
    mu         sync.RWMutex
}

func (us *URLShortener) Shorten(longURL string) string {
    us.mu.Lock()
    defer us.mu.Unlock()

    if shortURL, ok := us.reverseMap[longURL]; ok {
        return shortURL
    }

    shortURL := generateShortURL()
    us.urlMap[shortURL] = longURL
    us.reverseMap[longURL] = shortURL

    return shortURL
}

func (us *URLShortener) Resolve(shortURL string) (string, error) {
    us.mu.RLock()
    defer us.mu.RUnlock()

    longURL, ok := us.urlMap[shortURL]
    if !ok {
        return "", errors.New("URL not found")
    }
    return longURL, nil
}

---

2. 学習パスとスキルレベル

レベル1: 初心者(1-3ヶ月)
├─ 基本操作をマスター
│  ├─ make, リテラル初期化
│  ├─ 追加、取得、削除
│  └─ rangeによる反復
├─ カンマokイディオムの理解
└─ 基本的なユースケース

レベル2: 中級者(3-12ヶ月)
├─ 時間・空間計算量の理解
├─ 並行安全性の実装
├─ パフォーマンス最適化
└─ 実践的なパターン

レベル3: 上級者(1-3年)
├─ 内部実装の深い理解
├─ 高度な並行制御
├─ システム設計への応用
└─ パフォーマンスチューニング

レベル4: エキスパート(3年以上)
├─ カスタム実装の設計
├─ 他言語との比較分析
├─ ベンチマーク駆動開発
└─ OSSへの貢献

---

3. 実務での応用例

// 実務例1: リアルタイム分析システム
type EventAggregator struct {
    userEvents map[string]map[string]int
    mu         sync.RWMutex
}

func (ea *EventAggregator) RecordEvent(userID, eventType string) {
    ea.mu.Lock()
    defer ea.mu.Unlock()

    if ea.userEvents[userID] == nil {
        ea.userEvents[userID] = make(map[string]int)
    }
    ea.userEvents[userID][eventType]++
}

// 実務例2: APIレート制限
type RateLimiter struct {
    limits map[string]*limitInfo
    mu     sync.RWMutex
}

type limitInfo struct {
    count     int
    resetTime time.Time
}

func (rl *RateLimiter) Allow(ip string, limit int, window time.Duration) bool {
    rl.mu.Lock()
    defer rl.mu.Unlock()

    now := time.Now()
    info, exists := rl.limits[ip]

    if !exists || now.After(info.resetTime) {
        rl.limits[ip] = &limitInfo{
            count:     1,
            resetTime: now.Add(window),
        }
        return true
    }

    if info.count >= limit {
        return false
    }

    info.count++
    return true
}

---

明日への準備

Day 8は「構造体」を学びます。構造体はマップとは異なり、型安全な複合データ型です。

マップと構造体の違い

// マップ: 動的なキー
user := map[string]string{
    "name":  "太郎",
    "email": "taro@example.com",
}
// 任意のキーを追加可能
user["age"] = "25"

// 構造体: 固定されたフィールド
type User struct {
    Name  string
    Email string
}
u := User{Name: "太郎", Email: "taro@example.com"}
// u.Age = "25"  // コンパイルエラー!

構造体の利点:

  • 型安全性(タイプミスを防ぐ)
  • IDEの補完が効く
  • メソッドを定義できる
  • メモリ効率が良い
  • ---

    まとめ

    概念 説明 重要度
    マップ キー値ペアのデータ構造 ★★★
    make マップの初期化 ★★★
    delete 要素の削除 ★★★
    カンマok 存在確認付きアクセス ★★★
    ハッシュテーブル 内部実装 ★★☆
    O(1)アクセス 平均時間計算量 ★★★

    重要ポイント

  • nil mapへの書き込みは禁止: 必ずmakeで初期化
  • カンマokイディオムを使う: 存在確認は必須
  • 反復順序は不定: ソートが必要な場合はキーを抽出
  • 並行アクセスは非安全: sync.Mutexやsync.Mapを使用
  • O(1)の高速アクセス: ハッシュテーブルの特性を活用
  • 学習の到達目標

  • [x] マップの基本操作をマスター
  • [x] カンマokイディオムを理解
  • [x] ハッシュテーブルの原理を把握
  • [x] よくあるパターンを実装できる
  • [x] パフォーマンス特性を理解

Day 8(構造体)で学ぶこと:

  • 独自の型を定義する
  • メソッドの概念を理解する
  • オブジェクト指向的な設計パターン
  • 値レシーバとポインタレシーバの使い分け