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

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

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

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

スポンサーリンク

背景

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

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

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

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

代入可能性

V の値 x が型 T の変数に代入可能(「xT に代入可能」)であるのは、以下の条件のいずれかが満たされる場合です。

  • VT が同一である。
  • VT の基底型が同一であり、かつどちらも型パラメータではなく、V または T の少なくとも一方が名前付き型でない。
  • VT が同一の要素型を持つチャネル型であり、V が双方向チャネルであり、かつ V または T の少なくとも一方が名前付き型でない。
  • T がインターフェース型であり型パラメータではなく、かつ xT を実装している。
  • x が事前宣言された識別子 nil であり、T がポインタ型・関数型・スライス型・マップ型・チャネル型・インターフェース型のいずれかであり、かつ型パラメータでない。
  • x が型なし定数であり、型 T の値として表現可能である。

さらに、x の型 V または T が型パラメータである場合、以下の条件のいずれかが満たされるとき、x は型 T の変数に代入可能です。

  • x が事前宣言された識別子 nil であり、T が型パラメータであり、かつ xT の型セットに含まれる各型に代入可能である。
  • V が名前付き型でなく、T が型パラメータであり、かつ xT の型セットに含まれる各型に代入可能である。
  • V が型パラメータであり、T が名前付き型でなく、かつ V の型セットに含まれる各型の値が T に代入可能である。

解説

たとえ話:「差し込み口」と「プラグ」の互換性

代入可能性とは、ある値を別の型の変数に「差し込めるか」を判断するルールです。電源プラグとコンセントの互換性で考えてみましょう。


🔌 完全に同じ形(型が同一) 日本の2ピンプラグは日本の2ピンコンセントにそのまま刺さります。型が同一ならば、無条件で代入できます。

🔌 形は違うが変換できる(基底型が同じ、少なくとも一方がリテラル型) 海外の変換アダプターを使えば差し込める場合があります。名前付き型どうしは原則NG ですが、片方がリテラル型(無名の型)であれば、基底型が同じなら変換なしで代入できます。

🔌 インターフェース(汎用コンセント) 「USB-C 対応」と書かれたコンセントには、USB-C の形状さえ満たせばどんなデバイスでも差し込めます。インターフェースを実装していれば、具体的な型が何であれ代入可能です。


代入可能性の条件を整理する

前回学んだ「型の同一性」より緩やかなルールが代入可能性です。図で整理すると以下のようになります。

同一性(Identity)⊂ 代入可能性(Assignability)

型が同一なら必ず代入可能ですが、型が異なっていても代入できるケースがあります。


コード例

package main

import "fmt"

// 型定義の準備
type MyInt int
type MySlice []int

// インターフェース定義
type Stringer interface {
    String() string
}

// Stringer を実装する型
type MyName string

func (n MyName) String() string {
    return string(n)
}

// 双方向・受信専用チャネルの比較用
type MyChan = chan int // 名前付き型ではない(エイリアス)

func main() {
    // ==========================================
    // 条件1: V と T が同一
    // ==========================================
    var a int = 42
    var b int = a // int → int:同一型なので当然 OK
    fmt.Println(b) // → 42

    // ==========================================
    // 条件2: 基底型が同じ、少なくとも一方がリテラル型
    // ==========================================
    var m MyInt = 100

    // MyInt(名前付き)→ int(名前付き)は NG
    // var n int = m // ← コンパイルエラー!

    // リテラル型(無名)を介せば OK
    var slice1 MySlice = []int{1, 2, 3}
    var slice2 []int = slice1 // MySlice → []int([]int はリテラル型)
    fmt.Println(slice2) // → [1 2 3]

    // 逆も OK([]int → MySlice も片方がリテラル型)
    var slice3 MySlice = []int{4, 5, 6}
    fmt.Println(slice3) // → [4 5 6]

    _ = m

    // ==========================================
    // 条件3: チャネル(双方向 → 単方向)
    // ==========================================
    var bidir chan int = make(chan int, 1)   // 双方向チャネル
    var recv <-chan int = bidir              // 双方向 → 受信専用 OK
    var send chan<- int = bidir             // 双方向 → 送信専用 OK

    bidir <- 99
    fmt.Println(<-recv) // → 99
    _ = send

    // ==========================================
    // 条件4: インターフェースを実装している
    // ==========================================
    var s Stringer
    name := MyName("Gopher")
    s = name // MyName は Stringer を実装しているので代入 OK
    fmt.Println(s.String()) // → Gopher

    // ==========================================
    // 条件5: nil を参照型に代入
    // ==========================================
    var ptr *int = nil
    var sl []int = nil
    var mp map[string]int = nil
    var ch chan int = nil
    var fn func() = nil
    var iface Stringer = nil

    fmt.Println(ptr, sl, mp, ch, fn, iface) // → <nil> [] map[] <nil> <nil> <nil>

    // ==========================================
    // 条件6: 型なし定数
    // ==========================================
    var f float64 = 3    // 3 は型なし整数定数 → float64 に代入可能
    var c complex128 = 0 // 0 は型なし定数 → complex128 に代入可能
    const untyped = 255  // 型なし定数
    var u8 uint8 = untyped // 255 は uint8 で表現可能
    fmt.Println(f, c, u8) // → 3 (0+0i) 255
}

ジェネリクスにおける代入可能性

package main

import "fmt"

// 型セットを持つ制約
type Number interface {
    ~int | ~float64
}

// 型パラメータを使った関数
func assign[T Number](x T) T {
    return x
}

// nil を型パラメータに代入できるか
type Nilable interface {
    ~*int | ~[]int | ~map[string]int
}

func setNil[T Nilable](x T) T {
    // T の型セットに含まれる各型が nil を受け入れるなら代入可能
    var zero T // ゼロ値(nil に相当)
    return zero
}

func main() {
    // V が名前付き型でなく、T が型パラメータ
    result := assign(42)      // 型なし定数 42 → T(Number)に代入可能
    fmt.Println(result) // → 42

    // nil を型パラメータに代入
    var p *int = nil
    fmt.Println(setNil(p)) // → <nil>
}

よくある間違い・注意点

❌ 間違い1:名前付き型どうしは基底型が同じでも代入不可

type Celsius float64
type Fahrenheit float64

func main() {
    var c Celsius = 100.0
    // var f Fahrenheit = c // ← コンパイルエラー!
    // 両方とも名前付き型 → 少なくとも一方がリテラル型、の条件を満たさない

    // 解決策:明示的な型変換
    var f Fahrenheit = Fahrenheit(c)
    fmt.Println(f) // → 100
}

「基底型が同じ」という条件だけでは不十分です。少なくとも一方がリテラル型(名前なし)でなければなりません。名前付き型どうしは明示的な型変換が必要です。


❌ 間違い2:単方向チャネルから双方向チャネルへは代入不可

func main() {
    recv := make(<-chan int) // 受信専用チャネル

    // var bidir chan int = recv // ← コンパイルエラー!
    // 単方向 → 双方向への代入は不可(逆方向のみ許可)

    // 双方向 → 単方向なら OK
    bidir := make(chan int, 1)
    var r <-chan int = bidir // OK
    _ = r
}

チャネルの代入は双方向 → 単方向の一方通行です。受信専用・送信専用から双方向チャネルには代入できません。制限を「緩める」方向には戻せないと覚えましょう。


❌ 間違い3:型なし定数でも範囲外の値は代入不可

func main() {
    const big = 300
    // var u8 uint8 = big // ← コンパイルエラー!
    // 300 は uint8(0〜255)の範囲外なので表現不可能

    var u16 uint16 = big // uint16(0〜65535)なら OK
    fmt.Println(u16)     // → 300
}

型なし定数は「その型で表現可能である」という条件が必要です。値が型の範囲に収まっているかをコンパイル時に確認しましょう。


❌ 間違い4:nil はすべての型に代入できると思い込む

func main() {
    // var n int = nil    // ← コンパイルエラー!int は参照型ではない
    // var s string = nil // ← コンパイルエラー!string も参照型ではない
    // var a [3]int = nil // ← コンパイルエラー!配列も参照型ではない

    // nil を受け入れる型(参照型)
    var ptr *int = nil        // ポインタ: OK
    var sl []int = nil        // スライス: OK
    var mp map[string]int = nil // マップ: OK
    _ = ptr; _ = sl; _ = mp
}

nil を代入できるのはポインタ・関数・スライス・マップ・チャネル・インターフェースに限られます。基本型(intstringbool)や配列・構造体には代入できません。


まとめ

条件
型が同一intint
基底型が同じ+少なくとも一方がリテラル型MySlice[]int
双方向チャネル → 単方向チャネルchan int<-chan int
インターフェースを実装MyNameStringer
nil → 参照型nil*int[]intmap など
型なし定数 → 表現可能な型42float64uint8 など

「型の同一性」がパスポートの完全一致審査だとすれば、「代入可能性」はビザや渡航条件を考慮した入国審査のようなものです。条件を一つひとつ確認する癖をつけることで、コンパイルエラーに慌てることなく対処できるようになります。難しそうに見えますが、実際のコーディングで何度か遭遇するうちに自然と身につきますよ! 💪

おわりに 

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

よっしー
よっしー

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

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

コメント

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