Day 7: マップ - 解答例

課題1の解答

問題1-1: 商品価格マップ

アプローチ1: マップリテラルによる初期化(推奨)

package main

import "fmt"

func main() {
    // マップリテラルで初期値を設定
    // この方法が最も読みやすく、簡潔
    prices := map[string]int{
        "りんご": 100,
        "みかん": 80,
        "ぶどう": 200,
    }

    // rangeで全要素を反復処理
    // 順序は保証されないことに注意
    for name, price := range prices {
        fmt.Printf("%s: %d円\n", name, price)
    }
}

出力例:

りんご: 100円
みかん: 80円
ぶどう: 200円

アプローチ2: makeで初期化後に追加

package main

import "fmt"

func main() {
    // 空のマップを作成
    prices := make(map[string]int)

    // 一つずつ追加
    prices["りんご"] = 100
    prices["みかん"] = 80
    prices["ぶどう"] = 200

    for name, price := range prices {
        fmt.Printf("%s: %d円\n", name, price)
    }
}

使い分け:

  • 初期値が決まっている → リテラル
  • 動的に追加する → make

アプローチ3: ソート済み出力

package main

import (
    "fmt"
    "sort"
)

func main() {
    prices := map[string]int{
        "りんご": 100,
        "みかん": 80,
        "ぶどう": 200,
    }

    // キーをスライスに抽出してソート
    names := make([]string, 0, len(prices))
    for name := range prices {
        names = append(names, name)
    }
    sort.Strings(names)

    // ソートされた順序で出力
    for _, name := range names {
        fmt.Printf("%s: %d円\n", name, prices[name])
    }
}

出力(アルファベット順):

ぶどう: 200円
みかん: 80円
りんご: 100円

---

問題1-2: 存在確認

アプローチ1: カンマokイディオム(推奨)

package main

import "fmt"

func main() {
    prices := map[string]int{
        "りんご": 100,
        "みかん": 80,
    }

    // 存在するキーの確認
    // okがtrueの場合のみ処理
    if price, ok := prices["りんご"]; ok {
        fmt.Printf("りんご: %d円\n", price)
    }

    // 存在しないキーの確認
    // okがfalseなので、elseブロックが実行される
    if price, ok := prices["バナナ"]; ok {
        fmt.Printf("バナナ: %d円\n", price)
    } else {
        fmt.Println("バナナは見つかりません")
    }
}

よくある間違い:

// 間違い: 存在確認なしでアクセス
price := prices["バナナ"]
fmt.Println(price)  // 0が出力される(エラーではない!)

// 問題: 0が「存在しないキー」なのか「値が0」なのか区別できない

アプローチ2: ヘルパー関数化

package main

import "fmt"

// マップから値を安全に取得する関数
// 存在しない場合はデフォルト値を返す
func getOrDefault(m map[string]int, key string, defaultValue int) int {
    if value, ok := m[key]; ok {
        return value
    }
    return defaultValue
}

func main() {
    prices := map[string]int{
        "りんご": 100,
        "みかん": 80,
    }

    // 存在するキー
    fmt.Printf("りんご: %d円\n", getOrDefault(prices, "りんご", -1))

    // 存在しないキー(デフォルト値を返す)
    fmt.Printf("バナナ: %d円\n", getOrDefault(prices, "バナナ", -1))
}

アプローチ3: 複数キーの存在確認

package main

import "fmt"

func main() {
    prices := map[string]int{
        "りんご": 100,
        "みかん": 80,
    }

    // 複数のキーを確認
    items := []string{"りんご", "バナナ", "みかん", "ぶどう"}

    for _, item := range items {
        if price, ok := prices[item]; ok {
            fmt.Printf("✓ %s: %d円\n", item, price)
        } else {
            fmt.Printf("✗ %s: 見つかりません\n", item)
        }
    }
}

出力:

✓ りんご: 100円
✗ バナナ: 見つかりません
✓ みかん: 80円
✗ ぶどう: 見つかりません

---

課題2の解答

問題2-1: CRUD操作

基本的な実装

package main

import "fmt"

func main() {
    // Create(作成): 空のマップを作成
    prices := make(map[string]int)

    // Create(追加): 新しい要素を追加
    prices["りんご"] = 100
    prices["みかん"] = 80
    fmt.Println("追加後:", prices)

    // Update(更新): 既存の値を変更
    prices["りんご"] = 120
    fmt.Println("更新後:", prices)

    // Delete(削除): 要素を削除
    delete(prices, "みかん")
    fmt.Println("削除後:", prices)
}

より実践的な実装(エラーハンドリング付き)

package main

import (
    "errors"
    "fmt"
)

// PriceManager は商品価格を管理する構造体
type PriceManager struct {
    prices map[string]int
}

// NewPriceManager は新しいPriceManagerを作成
func NewPriceManager() *PriceManager {
    return &PriceManager{
        prices: make(map[string]int),
    }
}

// Add は新しい商品を追加(既存の場合はエラー)
func (pm *PriceManager) Add(name string, price int) error {
    if _, exists := pm.prices[name]; exists {
        return errors.New("商品は既に存在します")
    }
    if price < 0 {
        return errors.New("価格は0以上である必要があります")
    }
    pm.prices[name] = price
    return nil
}

// Update は既存の商品価格を更新
func (pm *PriceManager) Update(name string, price int) error {
    if _, exists := pm.prices[name]; !exists {
        return errors.New("商品が見つかりません")
    }
    if price < 0 {
        return errors.New("価格は0以上である必要があります")
    }
    pm.prices[name] = price
    return nil
}

// Delete は商品を削除
func (pm *PriceManager) Delete(name string) error {
    if _, exists := pm.prices[name]; !exists {
        return errors.New("商品が見つかりません")
    }
    delete(pm.prices, name)
    return nil
}

// Get は商品価格を取得
func (pm *PriceManager) Get(name string) (int, error) {
    if price, exists := pm.prices[name]; exists {
        return price, nil
    }
    return 0, errors.New("商品が見つかりません")
}

// List は全商品をリスト表示
func (pm *PriceManager) List() {
    if len(pm.prices) == 0 {
        fmt.Println("商品がありません")
        return
    }
    for name, price := range pm.prices {
        fmt.Printf("%s: %d円\n", name, price)
    }
}

func main() {
    pm := NewPriceManager()

    // 追加
    if err := pm.Add("りんご", 100); err != nil {
        fmt.Println("エラー:", err)
    }
    if err := pm.Add("みかん", 80); err != nil {
        fmt.Println("エラー:", err)
    }
    fmt.Println("=== 追加後 ===")
    pm.List()

    // 更新
    if err := pm.Update("りんご", 120); err != nil {
        fmt.Println("エラー:", err)
    }
    fmt.Println("\n=== 更新後 ===")
    pm.List()

    // 削除
    if err := pm.Delete("みかん"); err != nil {
        fmt.Println("エラー:", err)
    }
    fmt.Println("\n=== 削除後 ===")
    pm.List()

    // エラーケースのテスト
    fmt.Println("\n=== エラーケース ===")
    if err := pm.Add("りんご", 150); err != nil {
        fmt.Println("エラー:", err)  // 既に存在する
    }
    if err := pm.Update("バナナ", 120); err != nil {
        fmt.Println("エラー:", err)  // 存在しない
    }
}

---

問題2-2: 反復処理

アプローチ1: 基本的な反復

package main

import "fmt"

func main() {
    scores := map[string]int{
        "太郎": 85,
        "花子": 92,
        "次郎": 78,
    }

    // 全員のスコアを表示
    for name, score := range scores {
        fmt.Printf("%s: %d点\n", name, score)
    }
}

アプローチ2: 統計情報付き

package main

import "fmt"

func main() {
    scores := map[string]int{
        "太郎": 85,
        "花子": 92,
        "次郎": 78,
        "美咲": 88,
    }

    // 合計点と平均点を計算
    total := 0
    count := 0

    fmt.Println("=== 成績表 ===")
    for name, score := range scores {
        fmt.Printf("%s: %d点\n", name, score)
        total += score
        count++
    }

    if count > 0 {
        average := float64(total) / float64(count)
        fmt.Printf("\n合計: %d点\n", total)
        fmt.Printf("平均: %.1f点\n", average)
    }
}

アプローチ3: ランキング表示

package main

import (
    "fmt"
    "sort"
)

// スコア情報を保持する構造体
type ScoreEntry struct {
    Name  string
    Score int
}

func main() {
    scores := map[string]int{
        "太郎": 85,
        "花子": 92,
        "次郎": 78,
        "美咲": 88,
    }

    // マップをスライスに変換
    entries := make([]ScoreEntry, 0, len(scores))
    for name, score := range scores {
        entries = append(entries, ScoreEntry{Name: name, Score: score})
    }

    // スコアの降順でソート
    sort.Slice(entries, func(i, j int) bool {
        return entries[i].Score > entries[j].Score
    })

    // ランキング表示
    fmt.Println("=== ランキング ===")
    for rank, entry := range entries {
        fmt.Printf("%d位: %s (%d点)\n", rank+1, entry.Name, entry.Score)
    }
}

出力:

=== ランキング ===
1位: 花子 (92点)
2位: 美咲 (88点)
3位: 太郎 (85点)
4位: 次郎 (78点)

---

課題3の解答

問題3-1: 単語カウンター

アプローチ1: 基本実装

package main

import (
    "fmt"
    "strings"
)

func main() {
    text := "apple orange apple banana orange apple"
    // スペースで分割
    words := strings.Split(text, " ")

    // 単語の出現回数をカウント
    counter := make(map[string]int)
    for _, word := range words {
        counter[word]++  // 存在しない場合は0から開始される
    }

    // 結果を表示
    for word, count := range counter {
        fmt.Printf("%s: %d回\n", word, count)
    }
}

アプローチ2: 大文字小文字を区別しない

package main

import (
    "fmt"
    "strings"
)

func main() {
    text := "Apple orange APPLE Banana Orange apple"
    words := strings.Split(text, " ")

    counter := make(map[string]int)
    for _, word := range words {
        // 小文字に正規化してカウント
        normalizedWord := strings.ToLower(word)
        counter[normalizedWord]++
    }

    for word, count := range counter {
        fmt.Printf("%s: %d回\n", word, count)
    }
}

出力:

apple: 3回
orange: 2回
banana: 1回

アプローチ3: 高度な実装(句読点除去、ソート表示)

package main

import (
    "fmt"
    "sort"
    "strings"
    "unicode"
)

// 単語を正規化(小文字化、句読点除去)
func normalizeWord(word string) string {
    // 句読点を除去
    word = strings.TrimFunc(word, func(r rune) bool {
        return !unicode.IsLetter(r) && !unicode.IsNumber(r)
    })
    // 小文字化
    return strings.ToLower(word)
}

func main() {
    text := "Hello, world! Hello again. World of Go!"

    // 単語に分割
    words := strings.Fields(text)

    // カウント
    counter := make(map[string]int)
    for _, word := range words {
        normalized := normalizeWord(word)
        if normalized != "" {  // 空文字列は除外
            counter[normalized]++
        }
    }

    // 出現回数でソート
    type wordCount struct {
        word  string
        count int
    }

    entries := make([]wordCount, 0, len(counter))
    for word, count := range counter {
        entries = append(entries, wordCount{word, count})
    }

    // 出現回数の降順でソート
    sort.Slice(entries, func(i, j int) bool {
        if entries[i].count == entries[j].count {
            return entries[i].word < entries[j].word
        }
        return entries[i].count > entries[j].count
    })

    // 結果表示
    fmt.Println("=== 単語の出現頻度 ===")
    for _, entry := range entries {
        fmt.Printf("%s: %d回\n", entry.word, entry.count)
    }
}

出力:

=== 単語の出現頻度 ===
hello: 2回
world: 2回
again: 1回
go: 1回
of: 1回

---

問題3-2: 電話帳

アプローチ1: 基本実装

package main

import "fmt"

func main() {
    phoneBook := map[string]string{
        "太郎": "090-1111-2222",
        "花子": "090-3333-4444",
    }

    // 追加
    phoneBook["次郎"] = "090-5555-6666"

    // 検索
    name := "花子"
    if phone, ok := phoneBook[name]; ok {
        fmt.Printf("%s: %s\n", name, phone)
    }

    // 全件表示
    fmt.Println("\n=== 電話帳 ===")
    for name, phone := range phoneBook {
        fmt.Printf("%s: %s\n", name, phone)
    }
}

アプローチ2: 構造体を使った実装

package main

import (
    "fmt"
    "sort"
)

// 連絡先情報
type Contact struct {
    Name  string
    Phone string
    Email string
}

func main() {
    // 名前をキーとしたマップ
    contacts := map[string]Contact{
        "太郎": {Name: "太郎", Phone: "090-1111-2222", Email: "taro@example.com"},
        "花子": {Name: "花子", Phone: "090-3333-4444", Email: "hanako@example.com"},
    }

    // 新規追加
    contacts["次郎"] = Contact{
        Name:  "次郎",
        Phone: "090-5555-6666",
        Email: "jiro@example.com",
    }

    // 検索
    name := "花子"
    if contact, ok := contacts[name]; ok {
        fmt.Printf("名前: %s\n", contact.Name)
        fmt.Printf("電話: %s\n", contact.Phone)
        fmt.Printf("メール: %s\n", contact.Email)
    }

    // ソート済みで全件表示
    names := make([]string, 0, len(contacts))
    for name := range contacts {
        names = append(names, name)
    }
    sort.Strings(names)

    fmt.Println("\n=== 電話帳(五十音順)===")
    for _, name := range names {
        contact := contacts[name]
        fmt.Printf("%s: %s (%s)\n", contact.Name, contact.Phone, contact.Email)
    }
}

アプローチ3: 完全な電話帳システム

package main

import (
    "fmt"
    "sort"
    "strings"
)

type Contact struct {
    Name  string
    Phone string
    Email string
}

type PhoneBook struct {
    contacts map[string]Contact
}

func NewPhoneBook() *PhoneBook {
    return &PhoneBook{
        contacts: make(map[string]Contact),
    }
}

// 連絡先を追加
func (pb *PhoneBook) Add(contact Contact) {
    pb.contacts[contact.Name] = contact
}

// 名前で検索
func (pb *PhoneBook) FindByName(name string) (Contact, bool) {
    contact, ok := pb.contacts[name]
    return contact, ok
}

// 電話番号で検索
func (pb *PhoneBook) FindByPhone(phone string) []Contact {
    var results []Contact
    for _, contact := range pb.contacts {
        if contact.Phone == phone {
            results = append(results, contact)
        }
    }
    return results
}

// 部分一致検索
func (pb *PhoneBook) Search(query string) []Contact {
    query = strings.ToLower(query)
    var results []Contact

    for _, contact := range pb.contacts {
        if strings.Contains(strings.ToLower(contact.Name), query) ||
           strings.Contains(strings.ToLower(contact.Phone), query) ||
           strings.Contains(strings.ToLower(contact.Email), query) {
            results = append(results, contact)
        }
    }
    return results
}

// 削除
func (pb *PhoneBook) Delete(name string) bool {
    if _, ok := pb.contacts[name]; ok {
        delete(pb.contacts, name)
        return true
    }
    return false
}

// 全件表示(ソート済み)
func (pb *PhoneBook) List() {
    names := make([]string, 0, len(pb.contacts))
    for name := range pb.contacts {
        names = append(names, name)
    }
    sort.Strings(names)

    fmt.Println("=== 電話帳 ===")
    for _, name := range names {
        contact := pb.contacts[name]
        fmt.Printf("名前: %s\n", contact.Name)
        fmt.Printf("電話: %s\n", contact.Phone)
        fmt.Printf("メール: %s\n", contact.Email)
        fmt.Println("---")
    }
}

func main() {
    pb := NewPhoneBook()

    // データを追加
    pb.Add(Contact{Name: "太郎", Phone: "090-1111-2222", Email: "taro@example.com"})
    pb.Add(Contact{Name: "花子", Phone: "090-3333-4444", Email: "hanako@example.com"})
    pb.Add(Contact{Name: "次郎", Phone: "090-5555-6666", Email: "jiro@example.com"})

    // 全件表示
    pb.List()

    // 名前で検索
    fmt.Println("\n=== 名前検索: 花子 ===")
    if contact, found := pb.FindByName("花子"); found {
        fmt.Printf("%s: %s\n", contact.Name, contact.Phone)
    }

    // 部分一致検索
    fmt.Println("\n=== 検索: 'ro' を含む ===")
    results := pb.Search("ro")
    for _, contact := range results {
        fmt.Printf("%s: %s\n", contact.Name, contact.Email)
    }

    // 削除
    fmt.Println("\n=== 次郎を削除 ===")
    if pb.Delete("次郎") {
        fmt.Println("削除しました")
    }

    pb.List()
}

---

最適化とベストプラクティス

1. 容量の事前確保

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

// 良い例(サイズがわかっている場合)
counter := make(map[string]int, 10000)
for i := 0; i < 10000; i++ {
    counter[fmt.Sprintf("key%d", i)]++
}

2. ゼロ値の活用

// カウンターの実装
counter := make(map[string]int)
for _, word := range words {
    // 存在チェック不要!ゼロ値(0)から開始される
    counter[word]++
}

3. 安全な削除

// delete()は存在しないキーでも安全
delete(m, "nonexistent")  // エラーなし

// 削除前の存在確認が必要な場合
if _, ok := m[key]; ok {
    delete(m, key)
    fmt.Println("削除しました")
}

---

まとめ

学んだこと

  • 基本操作: 追加、取得、更新、削除
  • カンマokイディオム: 安全な値の取得
  • 反復処理: rangeでの全要素アクセス
  • 実践パターン: カウンター、検索、CRUD操作

よくある間違いと対策

間違い 対策
nilマップへの書き込み makeで初期化
存在確認なしでアクセス カンマokイディオム使用
順序を期待する キーをソートして反復
並行アクセス sync.Mutexを使用