Go言語入門:言語仕様 -Vol.32-

スポンサーリンク
Go言語入門:言語仕様 -Vol.32- 用語解説
Go言語入門:言語仕様 -Vol.32-
この記事は約19分で読めます。
よっしー
よっしー

こんにちは。よっしーです(^^)

本日は、Go言語の言語仕様について解説しています。

スポンサーリンク

背景

Go言語を学び始めて、公式の「The Go Programming Language Specification(言語仕様書)」を開いてみたものの、「英語で書かれていて読むのが大変…」「専門用語ばかりで何を言っているのかわからない…」と感じたことはありませんか? 実は、多くのGo初心者が同じ壁にぶつかっています。

言語仕様書は、Go言語の「正式な取扱説明書」のような存在です。プログラミング言語がどのように動くのか、どんなルールで書くべきなのかが詳しく書かれていますが、その分、初めて読む人には難しく感じられるのも事実です。

そこでこの記事では、言語仕様書の導入部分を丁寧な日本語訳とともに、初心者の方でも理解しやすい補足説明を加えてお届けします。「強く型付けされている」「ガベージコレクション」「並行プログラミング」といった専門用語も、具体例を交えながらわかりやすく解説していきます。

言語仕様書は難しそうに見えますが、一つひとつの概念を丁寧に読み解いていけば、必ず理解できます。一緒に、Go言語の基礎をしっかり学んでいきましょう!

Map types(マップ型)

マップは、要素型と呼ばれる1つの型の要素の順序なしグループであり、キー型と呼ばれる別の型の一意のキーの集合によってインデックス付けされます。初期化されていないマップの値はnilです。

MapType = "map" "[" KeyType "]" ElementType .
KeyType = Type .

比較演算子==!=は、キー型のオペランドに対して完全に定義されている必要があります。したがって、キー型は関数、マップ、またはスライスであってはなりません。キー型がインターフェース型の場合、これらの比較演算子は動的キー値に対して定義されている必要があります。失敗すると実行時パニックが発生します。

map[string]int
map[*T]struct{ x, y float64 }
map[string]interface{}

マップ要素の数は長さと呼ばれます。マップmの場合、組み込み関数lenを使用して調べることができ、実行中に変更される可能性があります。要素は実行中に代入を使用して追加でき、インデックス式で取得できます。組み込み関数deleteclearで削除できます。

新しい空のマップ値は、組み込み関数makeを使用して作成され、マップ型とオプションの容量ヒントを引数として受け取ります:

make(map[string]int)
make(map[string]int, 100)

初期容量はサイズを制限しません。マップは、nilマップを除いて、格納されている項目の数に対応するために成長します。nilマップは、要素を追加できないことを除いて、空のマップと同等です。


解説

マップとは何か?

マップ(map) は、キーと値のペアを保存するデータ構造です。キーを使って値を素早く検索できます。

たとえ話: マップは「辞書」のようなものです。単語(キー)を引くと、その意味(値)がわかります。順番は関係なく、キーさえわかれば素早く値を見つけられます。

package main

import "fmt"

func main() {
    // マップの作成
    ages := map[string]int{
        "太郎": 25,
        "花子": 30,
        "次郎": 28,
    }
    
    // キーで値を取得
    fmt.Println("太郎の年齢:", ages["太郎"])  // 25
    
    // 新しい要素を追加
    ages["四郎"] = 22
    
    // 要素を削除
    delete(ages, "次郎")
    
    fmt.Println(ages)
}

1. マップの基本

マップの宣言と初期化

package main

import "fmt"

func main() {
    // 方法1: var宣言(nilマップ)
    var m1 map[string]int
    fmt.Println(m1 == nil)  // true
    
    // 方法2: make関数
    m2 := make(map[string]int)
    fmt.Println(m2 == nil)  // false
    
    // 方法3: 初期値付き
    m3 := map[string]int{
        "apple":  100,
        "banana": 80,
        "cherry": 120,
    }
    
    // 方法4: 容量ヒント付き
    m4 := make(map[string]int, 100)
    
    fmt.Println(m2, m3, m4)
}

nilマップと空のマップ

package main

import "fmt"

func main() {
    // nilマップ
    var m1 map[string]int
    
    // 空のマップ
    m2 := make(map[string]int)
    m3 := map[string]int{}
    
    fmt.Println(m1 == nil)  // true
    fmt.Println(m2 == nil)  // false
    fmt.Println(m3 == nil)  // false
    
    // nilマップは読み取りOK、書き込みNG
    fmt.Println(m1["key"])  // 0 (ゼロ値)
    // m1["key"] = 1  // パニック!
    
    // 空のマップは読み書きOK
    m2["key"] = 1
    fmt.Println(m2["key"])  // 1
}

2. 要素の追加・取得・削除

要素の追加と更新

package main

import "fmt"

func main() {
    m := make(map[string]int)
    
    // 要素の追加
    m["apple"] = 100
    m["banana"] = 80
    
    // 要素の更新
    m["apple"] = 120
    
    fmt.Println(m)  // map[apple:120 banana:80]
}

要素の取得

package main

import "fmt"

func main() {
    m := map[string]int{
        "apple":  100,
        "banana": 80,
    }
    
    // 存在するキー
    value1 := m["apple"]
    fmt.Println(value1)  // 100
    
    // 存在しないキー(ゼロ値が返る)
    value2 := m["cherry"]
    fmt.Println(value2)  // 0
    
    // 安全な取得(存在確認付き)
    value3, ok := m["apple"]
    if ok {
        fmt.Println("appleの値:", value3)  // appleの値: 100
    }
    
    value4, ok := m["cherry"]
    if !ok {
        fmt.Println("cherryは存在しません")
    }
}

要素の削除

package main

import "fmt"

func main() {
    m := map[string]int{
        "apple":  100,
        "banana": 80,
        "cherry": 120,
    }
    
    fmt.Println("削除前:", m)
    
    // deleteで削除
    delete(m, "banana")
    
    fmt.Println("削除後:", m)
    
    // 存在しないキーを削除しても何も起きない
    delete(m, "orange")  // OK
}

すべての要素を削除(Go 1.21+)

package main

import "fmt"

func main() {
    m := map[string]int{
        "apple":  100,
        "banana": 80,
        "cherry": 120,
    }
    
    fmt.Println("削除前:", m)
    
    // clearですべて削除
    clear(m)
    
    fmt.Println("削除後:", m)  // map[]
}

3. キー型の制約

キー型は比較可能でなければなりません。

使用できるキー型

package main

import "fmt"

func main() {
    // 基本型
    m1 := map[int]string{}
    m2 := map[string]int{}
    m3 := map[bool]int{}
    m4 := map[float64]string{}
    
    // ポインタ型
    type Person struct{ Name string }
    m5 := map[*Person]int{}
    
    // 構造体型(すべてのフィールドが比較可能)
    type Point struct{ X, Y int }
    m6 := map[Point]string{}
    
    // 配列型
    m7 := map[[3]int]string{}
    
    fmt.Println(m1, m2, m3, m4, m5, m6, m7)
}

使用できないキー型

package main

func main() {
    // ❌ スライス(比較不可)
    // m1 := map[[]int]string{}
    
    // ❌ マップ(比較不可)
    // m2 := map[map[string]int]string{}
    
    // ❌ 関数(比較不可)
    // m3 := map[func()]string{}
    
    // ❌ 比較不可能なフィールドを含む構造体
    // type Container struct {
    //     Data []int
    // }
    // m4 := map[Container]string{}
}

4. マップの走査

for-rangeループ

package main

import "fmt"

func main() {
    m := map[string]int{
        "apple":  100,
        "banana": 80,
        "cherry": 120,
    }
    
    // キーと値の両方
    for key, value := range m {
        fmt.Printf("%s: %d円\n", key, value)
    }
    
    // キーだけ
    for key := range m {
        fmt.Println("キー:", key)
    }
    
    // 値だけ
    for _, value := range m {
        fmt.Println("値:", value)
    }
}

順序は保証されない

package main

import "fmt"

func main() {
    m := map[string]int{
        "a": 1,
        "b": 2,
        "c": 3,
        "d": 4,
        "e": 5,
    }
    
    // 実行ごとに順序が変わる可能性がある
    for key := range m {
        fmt.Print(key, " ")
    }
    fmt.Println()
    
    // 順序を保証したい場合は、キーをソート
}

5. マップの長さ

len()関数

package main

import "fmt"

func main() {
    m := map[string]int{
        "apple":  100,
        "banana": 80,
        "cherry": 120,
    }
    
    // 要素数を取得
    fmt.Println("要素数:", len(m))  // 3
    
    // 要素を追加
    m["orange"] = 90
    fmt.Println("要素数:", len(m))  // 4
    
    // 要素を削除
    delete(m, "banana")
    fmt.Println("要素数:", len(m))  // 3
    
    // 空のマップ
    m2 := make(map[string]int)
    fmt.Println("要素数:", len(m2))  // 0
}

6. 実用例

例1: カウンター

package main

import "fmt"

func main() {
    text := "hello world"
    
    // 文字の出現回数をカウント
    counts := make(map[rune]int)
    
    for _, char := range text {
        counts[char]++
    }
    
    for char, count := range counts {
        fmt.Printf("%c: %d回\n", char, count)
    }
}

例2: ユーザー管理

package main

import "fmt"

type User struct {
    ID    int
    Name  string
    Email string
}

func main() {
    users := make(map[int]User)
    
    // ユーザーを追加
    users[1] = User{ID: 1, Name: "太郎", Email: "taro@example.com"}
    users[2] = User{ID: 2, Name: "花子", Email: "hanako@example.com"}
    users[3] = User{ID: 3, Name: "次郎", Email: "jiro@example.com"}
    
    // IDでユーザーを検索
    if user, ok := users[2]; ok {
        fmt.Printf("見つかりました: %s (%s)\n", user.Name, user.Email)
    }
    
    // ユーザーを削除
    delete(users, 3)
    
    // 全ユーザーを表示
    for id, user := range users {
        fmt.Printf("ID %d: %s\n", id, user.Name)
    }
}

例3: グループ化

package main

import "fmt"

type Student struct {
    Name  string
    Grade int
}

func main() {
    students := []Student{
        {"太郎", 1},
        {"花子", 2},
        {"次郎", 1},
        {"四郎", 2},
        {"五郎", 1},
    }
    
    // 学年でグループ化
    byGrade := make(map[int][]Student)
    
    for _, student := range students {
        byGrade[student.Grade] = append(byGrade[student.Grade], student)
    }
    
    // 結果を表示
    for grade, studentList := range byGrade {
        fmt.Printf("学年 %d:\n", grade)
        for _, student := range studentList {
            fmt.Printf("  - %s\n", student.Name)
        }
    }
}

例4: キャッシュ

package main

import "fmt"

type Cache struct {
    data map[string]string
}

func NewCache() *Cache {
    return &Cache{
        data: make(map[string]string),
    }
}

func (c *Cache) Get(key string) (string, bool) {
    value, ok := c.data[key]
    return value, ok
}

func (c *Cache) Set(key, value string) {
    c.data[key] = value
}

func (c *Cache) Delete(key string) {
    delete(c.data, key)
}

func main() {
    cache := NewCache()
    
    // データを保存
    cache.Set("user:1", "太郎")
    cache.Set("user:2", "花子")
    
    // データを取得
    if value, ok := cache.Get("user:1"); ok {
        fmt.Println("キャッシュヒット:", value)
    }
    
    // データを削除
    cache.Delete("user:2")
}

例5: 集合(Set)の実装

package main

import "fmt"

type Set map[string]struct{}

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

func (s Set) Add(item string) {
    s[item] = struct{}{}
}

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

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

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

func main() {
    set := NewSet()
    
    set.Add("apple")
    set.Add("banana")
    set.Add("cherry")
    set.Add("apple")  // 重複は無視される
    
    fmt.Println("サイズ:", set.Size())  // 3
    fmt.Println("appleを含む?", set.Contains("apple"))  // true
    fmt.Println("orangeを含む?", set.Contains("orange"))  // false
    
    set.Remove("banana")
    fmt.Println("サイズ:", set.Size())  // 2
}

7. マップとポインタ

マップは参照型なので、関数に渡しても元のマップが変更されます。

package main

import "fmt"

func addItem(m map[string]int, key string, value int) {
    m[key] = value
}

func main() {
    prices := map[string]int{
        "apple":  100,
        "banana": 80,
    }
    
    fmt.Println("追加前:", prices)
    
    addItem(prices, "cherry", 120)
    
    // 元のマップが変更される
    fmt.Println("追加後:", prices)
}

8. 同時実行の注意

マップは並行アクセスに安全ではありません

問題のあるコード

package main

import "sync"

func main() {
    m := make(map[string]int)
    
    var wg sync.WaitGroup
    
    // 複数のゴルーチンから同時アクセス(危険!)
    for i := 0; i < 100; i++ {
        wg.Add(1)
        go func(n int) {
            defer wg.Done()
            // 競合状態が発生する可能性がある
            m[fmt.Sprintf("key%d", n)] = n
        }(i)
    }
    
    wg.Wait()
}

sync.Mutexを使った安全なアクセス

package main

import (
    "fmt"
    "sync"
)

type SafeMap struct {
    mu sync.RWMutex
    m  map[string]int
}

func (sm *SafeMap) Set(key string, value int) {
    sm.mu.Lock()
    defer sm.mu.Unlock()
    sm.m[key] = value
}

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

func main() {
    sm := &SafeMap{
        m: make(map[string]int),
    }
    
    var wg sync.WaitGroup
    
    for i := 0; i < 100; i++ {
        wg.Add(1)
        go func(n int) {
            defer wg.Done()
            sm.Set(fmt.Sprintf("key%d", n), n)
        }(i)
    }
    
    wg.Wait()
    
    value, ok := sm.Get("key50")
    if ok {
        fmt.Println("key50:", value)
    }
}

まとめ: マップ型で覚えておくべきこと

マップの基本

  1. キーと値のペア: 順序なしの連想配列
  2. ゼロ値はnil: 初期化されていないマップはnil
  3. 参照型: 関数に渡しても元のマップが変更される
  4. キー型の制約: 比較可能な型のみ

宣言と初期化

// nilマップ
var m1 map[string]int

// 空のマップ
m2 := make(map[string]int)
m3 := map[string]int{}

// 初期値付き
m4 := map[string]int{
    "apple": 100,
    "banana": 80,
}

基本操作

// 追加・更新
m["key"] = value

// 取得
value := m["key"]
value, ok := m["key"]  // 安全な取得

// 削除
delete(m, "key")

// すべて削除(Go 1.21+)
clear(m)

// 長さ
len(m)

走査

// キーと値
for key, value := range m {
    fmt.Println(key, value)
}

// キーだけ
for key := range m {
    fmt.Println(key)
}

実用的なアドバイス

package main

import "fmt"

func main() {
    // 1. nilマップへの書き込みはパニック
    var m1 map[string]int
    // m1["key"] = 1  // パニック!
    
    // 2. makeで初期化
    m2 := make(map[string]int)
    m2["key"] = 1  // OK
    
    // 3. 存在確認
    if value, ok := m2["key"]; ok {
        fmt.Println("存在する:", value)
    }
    
    // 4. 容量ヒント(大量の要素を追加する場合)
    m3 := make(map[string]int, 1000)
    
    // 5. 並行アクセスには注意
    // sync.Mutexまたはsync.Mapを使う
    
    fmt.Println(m2, m3)
}

マップは、効率的なキー検索が必要な場面で非常に便利なデータ構造です。適切に使うことで、高速で読みやすいコードが書けます!

おわりに 

本日は、Go言語の言語仕様について解説しました。

よっしー
よっしー

何か質問や相談があれば、コメントをお願いします。また、エンジニア案件の相談にも随時対応していますので、お気軽にお問い合わせください。

それでは、また明日お会いしましょう(^^)

コメント

タイトルとURLをコピーしました