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

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

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

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

スポンサーリンク

背景

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

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

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

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

値の表現

事前宣言された型の値(インターフェース any および error については後述)、配列、および構造体は自己完結型です。このような値はそれぞれ、すべてのデータの完全なコピーを内包しており、該当する型の変数は値全体を格納します。たとえば、配列変数は配列のすべての要素のための記憶領域(変数)を提供します。それぞれのゼロ値は値の型に固有のものであり、決して nil にはなりません。

nil でないポインタ、関数、スライス、マップ、およびチャネルの値は、複数の値によって共有される可能性のある、基となるデータへの参照を含んでいます。

  • ポインタ値は、ポインタの基底型の値を保持している変数への参照です。
  • 関数値は、(匿名である可能性のある)関数とクロージャでキャプチャされた変数への参照を含みます。
  • スライス値は、スライスの長さ、容量、および基となる配列への参照を含みます。
  • マップまたはチャネルの値は、そのマップまたはチャネルの実装固有のデータ構造への参照です。

インターフェース値は、インターフェースの動的型に応じて、自己完結型になる場合もあれば、基となるデータへの参照を含む場合もあります。事前宣言された識別子 nil は、参照を含む可能性のある型のゼロ値です。

複数の値が基となるデータを共有している場合、一方の値を変更すると、もう一方の値も変わる可能性があります。たとえば、スライスの要素を変更すると、その配列を共有するすべてのスライスにおいて、基となる配列の該当要素が変更されます。


解説

たとえ話:「コピー渡し」と「地図の共有」

Go における値の表現方法を理解するために、書類のやり取りで考えてみましょう。


📄 自己完結型(配列・構造体など)

会社の書類をコピー機でコピーして手渡すイメージです。受け取った人がそのコピーに何を書き込んでも、元の書類はまったく影響を受けません。データは完全に独立したコピーとして存在しています。


🗺️ 参照型(スライス・マップ・ポインタなど)

こちらは、会社内で共有しているGoogle マップのリンクを渡すイメージです。リンクを受け取った人が地図上のメモを編集すると、同じリンクを持つ全員が変更後の地図を見ることになります。データは一か所にあり、複数の人が「参照」しているだけです。


コード例

package main

import "fmt"

func main() {
    // ==========================================
    // 【自己完結型】配列はコピーが渡される
    // ==========================================
    original := [3]int{1, 2, 3}
    copied := original // 配列全体の完全なコピーが作られる

    copied[0] = 999 // コピー側を変更しても…
    fmt.Println("original:", original) // → [1 2 3](元の配列は変わらない)
    fmt.Println("copied:  ", copied)   // → [999 2 3]

    // ==========================================
    // 【参照型】スライスは基となる配列を共有する
    // ==========================================
    base := []int{10, 20, 30}
    shared := base // 基となる配列への参照をコピーしているだけ

    shared[0] = 999 // shared 側を変更すると…
    fmt.Println("base:  ", base)   // → [999 20 30](base も変わる!)
    fmt.Println("shared:", shared) // → [999 20 30]

    // ==========================================
    // 【参照型】マップも参照を共有する
    // ==========================================
    m1 := map[string]int{"apple": 100}
    m2 := m1 // m2 は m1 と同じマップを指している

    m2["apple"] = 999 // m2 を変更すると…
    fmt.Println("m1:", m1) // → map[apple:999](m1 も変わる!)
    fmt.Println("m2:", m2) // → map[apple:999]

    // ==========================================
    // 【ゼロ値】参照型のゼロ値は nil
    // ==========================================
    var s []int           // スライスのゼロ値
    var m map[string]int  // マップのゼロ値
    fmt.Println(s == nil) // → true
    fmt.Println(m == nil) // → true

    var arr [3]int // 配列のゼロ値は nil ではない
    fmt.Println(arr) // → [0 0 0](nil にはならない)
}

よくある間違い・注意点

❌ 間違い1:スライスを関数に渡せば「コピー」されると思い込む

func double(s []int) {
    s[0] = s[0] * 2 // 呼び出し元のデータも変わってしまう!
}

func main() {
    nums := []int{1, 2, 3}
    double(nums)
    fmt.Println(nums) // → [2 2 3](意図せず変更されている)
}

スライス変数をそのまま渡しても、基となる配列は共有されたままです。元のデータを守りたい場合は copy() を使って明示的にコピーしましょう。

func doubleSafe(s []int) {
    safe := make([]int, len(s))
    copy(safe, s) // 基となる配列を別途コピーする
    safe[0] = safe[0] * 2
    fmt.Println("safe:", safe)
}

❌ 間違い2:nil スライスと空のスライスの混同

var s1 []int        // nil スライス(参照なし)
s2 := []int{}       // 空スライス(参照あり、要素数ゼロ)

fmt.Println(s1 == nil) // → true
fmt.Println(s2 == nil) // → false(空であっても nil ではない)
fmt.Println(len(s1), len(s2)) // → 0 0(長さは同じ)

JSON エンコードなど、nil と空スライスを区別するライブラリもあります。意図を明確にするよう意識しましょう。


❌ 間違い3:構造体の中にスライス・マップが含まれる場合

type Team struct {
    Members []string
}

t1 := Team{Members: []string{"Alice", "Bob"}}
t2 := t1 // 構造体はコピーされるが…

t2.Members[0] = "Charlie" // スライスの中身は共有されている!
fmt.Println(t1.Members) // → [Charlie Bob](t1 も変わってしまう)

構造体自体はコピーされても、内部の参照型フィールド(スライス・マップ)は基となるデータを共有し続けます。「ディープコピー」が必要な場合は、フィールドを個別にコピーする処理を書きましょう。


まとめ

データの扱いゼロ値
配列・構造体・基本型値全体をコピー(自己完結)型固有の値(nil にならない)
スライス長さ・容量+基となる配列への参照nil
マップ内部データ構造への参照nil
ポインタ変数のアドレスへの参照nil
チャネル内部データ構造への参照nil
インターフェース動的型による(自己完結または参照)nil

Go では「コピーして渡す」か「参照を渡す」かが型によって決まっており、それが予期せぬ副作用を生む原因にもなります。参照型を扱うときは「誰かと同じデータを共有している」という意識を常に持つことが、バグを防ぐ最大のコツです。焦らず一つひとつ確認しながら進めていきましょう! 💪

おわりに 

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

よっしー
よっしー

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

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

コメント

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