📚 背景知識(読んでから問題へ)
Goのmapはキーと値を対応付けるハッシュテーブルです。スライスと同じく参照型(内部的にはポインタを持つ構造体)で、宣言方法にはいくつかの流儀があります。
var m1 map[string]int // nilマップ(読み取りは可能だが書き込むとpanic)
m2 := map[string]int{} // 空マップ(書き込み可能)
m3 := make(map[string]int) // 空マップ(書き込み可能、上と同じ)
m4 := map[string]int{"a": 1} // マップリテラルで初期化
ここで初心者がまずつまずくのが「宣言しただけのnilマップ」への書き込みです。var m1 map[string]intは「マップ型のゼロ値」であるnilを代入した状態で、読み取り(存在しないキーへのアクセス)は安全にゼロ値を返しますが、書き込み(m1["x"] = 1)を行うとpanicします。これはスライスのnilスライスがappend可能なのとは対照的な挙動で、Go初心者が最初に踏む地雷の一つです。
もう一つ重要なのが「カンマOKイディオム」による存在確認です。
v, ok := m["key"]
Goのマップは、存在しないキーにアクセスすると値の型のゼロ値(intなら0、stringなら"")を返します。これは「キーが存在しなかったのか」「たまたま値が0だったのか」を区別できないという問題を生みます。そこでGoはv, ok := m[key]という2値返却の構文を用意し、okがtrueならキーが存在した、falseなら存在しなかったことを明示的に判定できるようにしています。これは「暗黙のnullやエラーで例外を投げるのではなく、戻り値として明示的に状態を表現する」というGoの一貫した設計思想の表れです。
削除にはdelete(m, key)を使います(キーが存在しなくてもpanicしません)。また、マップの反復順序は意図的にランダム化されており、for k, v := range mのたびに順序が変わり得ます。これは「開発者が反復順序に依存したコードを書いてしまうバグ」を言語レベルで未然に防ぐためのGoの設計判断です。
📝 問題
単語の出現回数をカウントする関数wordCountを実装してください。
要件:
wordCount(text string) map[string]intを実装する。textはスペース区切りの単語列(例:"go is fun go is simple")を受け取り、各単語の出現回数をmap[string]intとして返すstrings.Fieldsを使って単語に分割すること(連続する空白も正しく扱えるようにするため)- マップは
makeを使って明示的に初期化すること(nilマップに書き込まないこと) mostFrequent(counts map[string]int) (string, int)を実装する。カウント結果を受け取り、最も出現回数が多い単語とその回数を返す。同数の単語が複数ある場合はどれを返しても構わないが、必ず1つに決定すること(for rangeの順序がランダムであることを踏まえ、単純な「最大値を見つけたら更新」ロジックで実装すればよい)main関数で、text := "go is fun go is simple go is powerful"をwordCountにかけ、結果とmostFrequentの出力を表示すること
go run main.go でそのまま実行できる、完全な1つのプログラムとして提出してください。
🔍 ヒント(段階的開示)
ヒント1 — 方向性
strings.Fields(text)は空白(スペース・タブ・改行など)で区切って[]stringを返します。strings.Split(text, " ")と違い、連続する空白があっても空文字列の要素を生まないため、単語カウントにはFieldsの方が適しています- マップへのカウント加算は「キーがまだ存在しない場合、ゼロ値の
0から始まる」というGoのマップの性質をそのまま使えます(存在確認をわざわざ書く必要はありません)
ヒント2 — アプローチ
counts := make(map[string]int)
for _, word := range strings.Fields(text) {
counts[word]++ // 存在しなければ 0 から始まり 1 になる。カンマOKイディオムは不要
}
mostFrequentでは、マップを1周しながら「これまでの最大値」を保持する変数と比較していく素朴なループで十分です。
ヒント3 — コード骨格
package main
import (
"fmt"
"strings"
)
func wordCount(text string) map[string]int {
counts := make(map[string]int)
for _, word := range strings.Fields(text) {
counts[word]++
}
return counts
}
func mostFrequent(counts map[string]int) (string, int) {
var bestWord string
var bestCount int
for word, count := range counts {
if count > bestCount {
bestWord = word
bestCount = count
}
}
return bestWord, bestCount
}
func main() {
text := "go is fun go is simple go is powerful"
counts := wordCount(text)
// counts を表示...
word, count := mostFrequent(counts)
// 結果を表示...
}
✅ 模範解答
package main
import (
"fmt"
"sort"
"strings"
)
// wordCount はスペース区切りのtextを受け取り、各単語の出現回数を返す。
func wordCount(text string) map[string]int {
counts := make(map[string]int)
for _, word := range strings.Fields(text) {
counts[word]++
}
return counts
}
// mostFrequent はcountsの中から最も出現回数が多い単語とその回数を返す。
func mostFrequent(counts map[string]int) (string, int) {
var bestWord string
var bestCount int
for word, count := range counts {
if count > bestCount {
bestWord = word
bestCount = count
}
}
return bestWord, bestCount
}
func main() {
text := "go is fun go is simple go is powerful"
counts := wordCount(text)
// マップは反復順序が不定なので、表示だけ安定させるためにキーをソートする
keys := make([]string, 0, len(counts))
for k := range counts {
keys = append(keys, k)
}
sort.Strings(keys)
fmt.Println("--- 出現回数 ---")
for _, k := range keys {
fmt.Printf("%s: %d\n", k, counts[k])
}
word, count := mostFrequent(counts)
fmt.Printf("--- 最頻出単語 ---\n%s: %d回\n", word, count)
// カンマOKイディオムの確認: 存在しないキー
if v, ok := counts["python"]; !ok {
fmt.Printf("\"python\" は出現しない(ゼロ値は %d だが ok=%v で判定できる)\n", v, ok)
}
}
▶ 実行結果を見る(go run main.go)
--- 出現回数 ---
fun: 1
go: 3
is: 3
powerful: 1
simple: 1
--- 最頻出単語 ---
go: 3回
"python" は出現しない(ゼロ値は 0 だが ok=false で判定できる)
※ goとisはどちらも3回で同数のため、mostFrequentの単純な>比較ロジックでは先に見つかった方(rangeの反復順序に依存)が返ります。今回の実行例ではgoが返っていますが、実行のたびにisが返る可能性もあります(マップの反復順序がランダム化されているため)。
🪜 Step-by-Step 解説
makeで書き込み可能なマップを用意するcounts := make(map[string]int)
var counts map[string]intと書いてしまうとnilマップになり、counts[word]++の時点でpanicします。マップに書き込む予定がある場合は、必ずmakeかマップリテラル(map[string]int{})で初期化することがGoの鉄則です。
strings.Fieldsで単語に分割し、カウントするfor _, word := range strings.Fields(text) {
counts[word]++
}
counts[word]++は「counts[word]を読み取り、1を足して、書き戻す」という操作です。wordが初出の場合、マップは値の型(int)のゼロ値である0を返すため、追加の存在確認なしに0 + 1 = 1から自然にカウントが始まります。この「ゼロ値から自然にスタートできる」性質は、カウンタ・集計処理でマップを扱う際の定石です。
mostFrequentで最大値を探すvar bestWord string
var bestCount int
for word, count := range counts {
if count > bestCount {
bestWord = word
bestCount = count
}
}
bestCountの初期値は0(intのゼロ値)なので、少なくとも1回以上出現した単語であれば必ず1回目のループでbestCountが更新されます。for word, count := range countsはマップの反復構文で、キーと値を同時に受け取れます。
if v, ok := counts["python"]; !ok {
// "python" はカウント対象に出現しなかった
}
counts["python"]単体では0が返り、それが「0回出現した」のか「そもそも存在しない」のか区別できません。v, ok := counts[key]のok(bool)を見ることで、この曖昧さを解消できます。
💡 設計思想・なぜこう書くのか
counts[word]++のように、存在確認と初期化を省略して自然にコードが書けるのは、Goのあらゆる型がゼロ値を持ち、未初期化状態でも安全に扱えるという「ゼロ値の哲学」が一貫して適用されているためです。v, ok := m[key]というカンマOKイディオムを言語標準の構文として用意しています。これは例外機構を使わず、戻り値というシンプルな仕組みで「値の有無」という状態を表現するという、Go全体に共通する設計哲学(errorの扱い方とも共通する)です。nilマップへの書き込みがpanicになる一方、nilマップからの読み取りは安全という非対称な挙動は一見ややこしく見えますが、「読み取り専用で使う可能性があるならゼロ値のまま渡せて安全、書き込むなら明示的にmakeせよ」という、意図を明確にプログラマに求める設計だと理解すると腑に落ちます。🌐 他言語との比較
| 観点 | Go | Java | Python | JavaScript |
|---|---|---|---|---|
| 宣言・初期化 | make(map[K]V) または map[K]V{}(varだけだとnilで書き込み不可) | new HashMap<>() | {} または dict() | {} または new Map() |
| 存在確認 | v, ok := m[key](カンマOKイディオム) | map.containsKey(key) または Optional | key in dict または dict.get(key) | map.has(key) または obj[key] !== undefined |
| 未存在キーの読み取り | 型のゼロ値を返す(panicしない) | nullを返す(NullPointerExceptionの温床) | KeyError(dict[key]の場合)またはNone(.get()の場合) | undefinedを返す |
| 反復順序 | 意図的にランダム化される | HashMapは不定、LinkedHashMapは挿入順 | Python 3.7+ は挿入順を保証 | Mapは挿入順を保証、プレーンオブジェクトはほぼ挿入順 |
GoのマップはPythonのdictやJavaScriptのMapと似た感覚で使えますが、「反復順序が保証されない」点と「未初期化(nil)状態への書き込みがpanicする」点は特有の注意点です。特にPython経験者はdict[key]が存在しないとKeyErrorになる感覚に慣れているため、Goの「ゼロ値を静かに返す」挙動に最初は戸惑いやすく、逆に「本当に存在するか」を確認したい場面でカンマOKイディオムを忘れがちです。
🏆 実務での使いどころ
- 集計・カウント処理: ログの出現頻度集計、アクセス数カウント、単語頻度分析など、今回の
wordCountのようなパターンは実務でも頻出です - セット(集合)の代用: Goには専用の
Set型がないため、map[T]struct{}やmap[T]boolで集合を表現するのが定石です(struct{}はメモリを消費しないため、存在確認だけしたい場合に好まれます) - キャッシュ・メモ化: 計算結果をキーで引けるようにキャッシュする際、カンマOKイディオムで「キャッシュにヒットしたか」を判定するパターンが頻出します
- 設定値のオーバーライド判定: デフォルト値とユーザー指定値をマップで管理し、「ユーザーが明示的に指定したか(ok=true)」と「デフォルトのゼロ値のままか」を区別する場面で使われます
⚠️ よくある誤解・ミス
| 誤解・ミス | なぜ起こるか | 正しい理解 |
|---|---|---|
var m map[string]intのままm[key] = 1と書いてpanicする | スライスのnilスライスにappendできることと混同する | マップのnil値は読み取り専用。書き込みが必要ならmakeかマップリテラルで明示的に初期化する |
if counts[key] != 0だけでキーの存在を判定してしまう | ゼロ値と「存在しない」を同じものと錯覚する | 値が本当に0である可能性と「そもそも存在しない」を区別したいなら、必ずv, ok := m[key]のカンマOKイディオムを使う |
for rangeの反復順序が毎回同じだと思い込み、順序に依存したロジックを書く | 他言語(Python 3.7+やJS)のマップ/オブジェクトが挿入順を保つ感覚をそのまま持ち込む | Goのマップの反復順序は意図的にランダム化されている。順序が必要なら別途キーをスライスに集めてsortする |
マップの値を直接変更しようとして構造体フィールドにアクセスできずコンパイルエラーになる(例: m[key].Field = x) | マップの値はアドレス取得不可(addressable でない)ことを知らない | マップの値型が構造体の場合、v := m[key]; v.Field = x; m[key] = vのように一度取り出して書き戻すか、値をポインタ型(map[K]*T)にする |
🚀 次のステップ
- 発展: 今回の
mostFrequentは同数の場合の挙動が実行のたびに変わり得ます。「同数の場合はアルファベット順で先に来る方を優先する」ように、キーをスライスに集めてsortしてから最頻出を決定するロジックに書き換えてみましょう - 次回予告: Day 007 — 関数の基礎(複数戻り値・可変長引数)(多値返却・variadic関数、実装問題)