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

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

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

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

スポンサーリンク

背景

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

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

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

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

型の同一性

2つの型は「同一(”the same”)」であるか、「異なる」かのどちらかです。

名前付き型は、常に他のあらゆる型とは異なります。それ以外の場合、2つの型は、その基底型リテラルが構造的に等価であれば同一です。つまり、同じリテラル構造を持ち、対応するコンポーネントが同一の型を持つということです。詳細は以下のとおりです。

  • 2つの配列型は、要素型が同一で、配列長が同じであれば同一です。
  • 2つのスライス型は、要素型が同一であれば同一です。
  • 2つの構造体型は、同じフィールドの並びを持ち、対応するフィールドのペアが同じ名前・同一の型・同一のタグを持ち、かつ両方ともが埋め込みフィールドであるか、または両方ともが埋め込みフィールドでない場合に同一です。異なるパッケージからのエクスポートされていないフィールド名は常に異なります。
  • 2つのポインタ型は、基底型が同一であれば同一です。
  • 2つの関数型は、パラメータと戻り値の数が同じで、対応するパラメータ型および戻り値の型が同一であり、かつ両方の関数が可変長引数であるかまたはどちらも可変長引数でない場合に同一です。パラメータ名と戻り値名は一致している必要はありません。
  • 2つのインターフェース型は、同じ型セットを定義していれば同一です。
  • 2つのマップ型は、キーの型と要素型が同一であれば同一です。
  • 2つのチャネル型は、要素型が同一で、方向が同じであれば同一です。
  • 2つのインスタンス化された型は、定義された型とすべての型引数が同一であれば同一です。

以下の宣言が与えられた場合、

type (
    A0 = []string
    A1 = A0
    A2 = struct{ a, b int }
    A3 = int
    A4 = func(A3, float64) *A0
    A5 = func(x int, _ float64) *[]string

    B0 A0
    B1 []string
    B2 struct{ a, b int }
    B3 struct{ a, c int }
    B4 func(int, float64) *B0
    B5 func(x int, y float64) *A1

    C0 = B0
    D0[P1, P2 any] struct{ x P1; y P2 }
    E0 = D0[int, string]
)

以下の型は同一です。

A0、A1、および []string
A2 と struct{ a, b int }
A3 と int
A4、func(int, float64) *[]string、および A5

B0 と C0
D0[int, string] と E0
[]int と []int
struct{ a, b *B5 } と struct{ a, b *B5 }
func(x int, y float64) *[]string、func(int, float64) (result *[]string)、および A5

B0B1 は、それぞれ別々の型定義によって作られた新しい型であるため、異なります。func(int, float64) *B0func(x int, y float64) *[]string は、B0[]string と異なるため、異なります。P1P2 は、異なる型パラメータであるため、異なります。D0[int, string]struct{ x int; y string } は、前者がインスタンス化された定義型であり、後者は型リテラルであるため、異なります(ただし、代入は可能です)。


解説

たとえ話:「同じ設計図」と「同じ名前の別会社」

型の同一性を理解するには、設計図と会社のブランドで考えると整理しやすくなります。


📐 構造が同じ = 同一(型リテラルの場合)

「縦10cm・横20cm・穴あき」という設計図が2枚あるとします。製造元も名前も関係なく、設計図の内容がまったく同じなら、同じものとみなされます。Go の型リテラル([]stringstruct{ a, b int } など)はこのルールで判断されます。


🏢 名前付き型 = 別ブランドは常に別物

一方、「ユニクロのTシャツ」と「無印良品のTシャツ」は、素材や形が同じでも別の商品です。Go の型定義(type B0 A0 のような = なしの定義)で作られた名前付き型は、たとえ内部構造が同じでも、別々の定義から生まれた時点で常に異なる型とみなされます。


コード例

package main

import "fmt"

type (
    // === 型エイリアス(= あり)===
    A0 = []string              // []string の別名にすぎない
    A1 = A0                    // A0 の別名 → 辿ると []string
    A2 = struct{ a, b int }    // 構造体リテラルの別名
    A3 = int                   // int の別名

    // === 型定義(= なし)===
    B0 A0        // A0(= []string)を元にした新しい型
    B1 []string  // []string を元にした新しい型(B0 とは別物!)
    B2 struct{ a, b int } // A2 と構造は同じだが別の型
    B3 struct{ a, c int } // フィールド名が違う(b → c)

    C0 = B0 // B0 の型エイリアス → B0 と同一
)

// ジェネリック型定義
type D0[P1, P2 any] struct{ x P1; y P2 }

type E0 = D0[int, string] // D0[int, string] のエイリアス

func main() {
    // ==========================================
    // 【同一】型エイリアスは常に元の型と同一
    // ==========================================
    var a0 A0 = []string{"hello"}
    var a1 A1 = a0    // A0 と A1 は同一型なので代入できる
    var raw []string = a1 // []string とも同一なので代入できる
    fmt.Println(raw)  // → [hello]

    // ==========================================
    // 【異なる】別々の型定義から生まれた型
    // ==========================================
    var b0 B0 = B0{"world"}
    var b1 B1 = []string{"world"}

    // b0 = b1 // ← コンパイルエラー!B0 と B1 は別の型
    // 明示的な型変換なら可能
    b0 = B0(b1)
    fmt.Println(b0) // → [world]

    // ==========================================
    // 【同一】構造体リテラルは構造が同じなら同一
    // ==========================================
    type TempStruct = struct{ a, b int }
    var a2 A2 = struct{ a, b int }{1, 2}
    var ts TempStruct = a2 // 構造が同じリテラル型は同一
    fmt.Println(ts)        // → {1 2}

    // ==========================================
    // 【関数型】パラメータ名は問わない
    // ==========================================
    // 以下はすべて同一の型:func(int, float64) *[]string
    var f1 func(int, float64) *[]string
    var f2 func(x int, y float64) *[]string          // 名前あり
    var f3 func(a int, _ float64) *[]string           // 名前が違う
    _ = f1
    _ = f2
    _ = f3
    // f1, f2, f3 はすべて代入可能(パラメータ名は型同一性に影響しない)

    // ==========================================
    // 【ジェネリクス】インスタンス化された型
    // ==========================================
    var d0 D0[int, string] = D0[int, string]{x: 1, y: "hi"}
    var e0 E0 = d0 // E0 は D0[int, string] のエイリアスなので同一
    fmt.Println(e0) // → {1 hi}

    // D0[int, string] と struct{ x int; y string } は異なる型
    // (ただし代入は可能)
    var literal struct{ x int; y string } = struct{ x int; y string }{1, "hi"}
    // d0 = D0[int, string](literal) // ← 代入は可能だが型は異なる
    fmt.Println(literal)
}

よくある間違い・注意点

❌ 間違い1:構造が同じなら型定義でも同一だと思い込む

type Meter float64
type Kilogram float64

func main() {
    var m Meter = 1.80
    var k Kilogram = 60.0

    // m = k // ← コンパイルエラー!
    // 基底型は両方 float64 でも、型定義が別なら別の型

    // 明示的な変換なら可能(が、意味的には危険!)
    m = Meter(k) // 単位が違うのに代入できてしまう
    fmt.Println(m) // → 60
}

型定義を使う目的のひとつは「意味の違うものを型で区別する」ことです。コンパイラが守ってくれる安全柵を、型変換で自ら壊さないように注意しましょう。


❌ 間違い2:構造体のフィールド名・タグが微妙に違う

type S1 = struct {
    Name string `json:"name"`
}

type S2 = struct {
    Name string `json:"Name"` // タグが違う(大文字)
}

func main() {
    var s1 S1
    var s2 S2
    // s1 = s2 // ← コンパイルエラー!タグが異なるため別の型
    _ = s1
    _ = s2
}

フィールド名・型・順序だけでなく、構造体タグも型同一性の判定に含まれます。JSON タグのような些細な違いでも、型が異なると判断されます。


❌ 間違い3:異なるパッケージの非公開フィールドを同一視する

// パッケージ pkg1
type Data struct {
    id int // 小文字(非公開)
}

// パッケージ pkg2
type Data struct {
    id int // 同じ名前・型でも、パッケージが違えば別フィールド
}

// pkg1.Data と pkg2.Data は異なる型

非公開フィールド(小文字始まり)はパッケージ名もセットで同一性が判定されます。外から見ると同じ構造に見えても、型としては別物になるため注意が必要です。


❌ 間違い4:インスタンス化された型と型リテラルを同一視する

type Box[T any] struct{ value T }

func main() {
    var b Box[int] = Box[int]{value: 42}

    // 構造は同じに見えるが…
    var literal struct{ value int } = struct{ value int }{value: 42}

    // b = Box[int](literal) // ← コンパイルエラー!異なる型
    // ただし、代入互換性(assignability)はある場合も
    fmt.Println(b, literal)
}

D0[int, string]struct{ x int; y string } のように、見た目が同じでも「定義された型」と「型リテラル」は別物です。代入できるかどうか(assignability)と型が同一かどうか(identity)は、別の概念として区別して覚えましょう。


まとめ

種別同一性の判定ルール
型エイリアス= あり)元の型と常に同一
型定義= なし)別々の定義 → 常に異なる
型リテラル[]T など)構造が等価なら同一
構造体フィールド名・型・タグ・順序・埋め込みがすべて一致で同一
関数型パラメータ数・型・可変長かどうかが一致で同一(名前は不問)
ジェネリクス定義型とすべての型引数が同一なら同一
非公開フィールドパッケージが異なれば常に異なる

「型が同一かどうか」と「代入できるかどうか」は別の話です。型の同一性はより厳格なルールで、代入互換性(assignability)はより緩やかなルールです。まずは「名前付き型は別物、構造で判断されるのはリテラル型だけ」という大原則を頭に入れておくと、細かいルールも自然と整理されてきますよ! 🎯

おわりに 

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

よっしー
よっしー

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

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

コメント

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