Go のマップの存在チェック(コンマOK イディオム)

マップから値を取得するとき、そのキーが存在するかどうかを確認したい場面がよくある。Go では「コンマOK イディオム」を使ってこれを実現する。

存在しないキーの問題

マップに存在しないキーでアクセスすると、値の型のゼロ値が返る。

ages := map[string]int{
    "Alice": 30,
}

fmt.Println(ages["Bob"]) // 0

これだと「Bob の年齢が 0」なのか「Bob が存在しない」のか区別できない。

コンマOK イディオム

マップの値を取得するとき、2つ目の戻り値でキーの存在を確認できる。

ages := map[string]int{
    "Alice": 30,
    "Bob":   0,
}

age, ok := ages["Bob"]
fmt.Println(age, ok) // 0 true

age, ok = ages["Charlie"]
fmt.Println(age, ok) // 0 false

ok が true ならキーが存在し、false なら存在しない。これで「値が 0」と「キーがない」を区別できる。

条件分岐での使い方

よくあるパターンとして、if 文と組み合わせて使う。

if age, ok := ages["Alice"]; ok {
    fmt.Printf("Alice is %d years old\n", age)
} else {
    fmt.Println("Alice not found")
}

この書き方だと age 変数のスコープが if ブロック内に限定されるため、コードがすっきりする。

存在確認だけしたい場合

値が不要で存在確認だけしたいときは、ブランク識別子を使う。

if _, ok := ages["Alice"]; ok {
    fmt.Println("Alice exists")
}

実践的な例

ユーザーのキャッシュを管理する例を見てみよう。

type User struct {
    ID   int
    Name string
}

var userCache = make(map[int]*User)

func GetUser(id int) (*User, error) {
    // キャッシュを確認
    if user, ok := userCache[id]; ok {
        return user, nil
    }
    
    // キャッシュになければDBから取得
    user, err := fetchUserFromDB(id)
    if err != nil {
        return nil, err
    }
    
    // キャッシュに保存
    userCache[id] = user
    return user, nil
}

値がゼロ値かどうかの判定

コンマOK イディオムを使わないと、意図しない動作になることがある。

コンマOK なし

値がゼロ値のとき、キーが存在するのかどうか判断できない。

コンマOK あり

キーの存在を明確に判定できる。ゼロ値が有効な値として使われる場合に必須。

counts := map[string]int{
    "apple":  5,
    "banana": 0, // 在庫ゼロだが存在する
}

// 悪い例
if counts["banana"] == 0 {
    fmt.Println("banana not found or out of stock")
}

// 良い例
if count, ok := counts["banana"]; ok {
    if count == 0 {
        fmt.Println("banana is out of stock")
    }
} else {
    fmt.Println("banana not found")
}

コンマOK イディオムは Go のマップを扱う上で必須の知識だ。キーの存在を確認するときは常にこの方法を使うようにしよう。