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

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

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

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

スポンサーリンク

背景

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

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

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

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

メソッドセット

型のメソッドセットは、その型のオペランドに対して呼び出すことができるメソッドを決定します。すべての型には、(空の場合もある)メソッドセットが関連付けられています。

  • 定義された型 T のメソッドセットは、レシーバ型 T で宣言されたすべてのメソッドで構成されます。
  • 定義された型 T へのポインタ(T はポインタでもインターフェースでもない)のメソッドセットは、レシーバ *T または T で宣言されたすべてのメソッドの集合です。
  • インターフェース型のメソッドセットは、そのインターフェースの型セットに含まれる各型のメソッドセットの共通部分です(結果として得られるメソッドセットは、通常インターフェースに宣言されたメソッドの集合そのものになります)。

埋め込みフィールドを含む構造体(および構造体へのポインタ)については、構造体型のセクションで説明するとおり、さらに追加のルールが適用されます。その他のすべての型は空のメソッドセットを持ちます。

メソッドセットにおいて、各メソッドは一意な空白でないメソッド名を持たなければなりません。


解説

たとえ話:「社員証と権限」の関係

メソッドセットとは、ある型が「どのメソッドを呼び出す権限を持っているか」を定めたルールです。会社の社員証と入室権限で考えてみましょう。


🪪 値型(T)の社員証

一般社員の社員証は、自分の部屋(T のメソッド)にしか入れません

🔑 ポインタ型(*T)の社員証

管理職の社員証は、一般社員の部屋(T のメソッド)にも、管理職専用の部屋(*T のメソッド)にも入れます。つまり *TT のメソッドセットをすべて含んだ上位互換です。

📋 インターフェースの入館証

ビルの受付が「この入館証を持つ人はこの部屋に入れる」と定義するのがインターフェースです。型セットに含まれるすべての型が共通して持つメソッドだけが、インターフェースのメソッドセットになります。


メソッドセットの全体像を図で確認

T のメソッドセット  ⊂  *T のメソッドセット

T   → receiver T で宣言されたメソッドのみ
*T  → receiver T で宣言されたメソッド
    + receiver *T で宣言されたメソッド

コード例

package main

import "fmt"

// ==========================================
// 定義された型とそのメソッド
// ==========================================

type Counter struct {
    count int
}

// receiver が T(値レシーバ)
// → T のメソッドセットと *T のメソッドセット、両方に含まれる
func (c Counter) Value() int {
    return c.count
}

// receiver が *T(ポインタレシーバ)
// → *T のメソッドセットにのみ含まれる(T のメソッドセットには含まれない)
func (c *Counter) Increment() {
    c.count++
}

// ==========================================
// インターフェース定義
// ==========================================

// Reader は Value() メソッドのみを要求する
type Reader interface {
    Value() int
}

// ReadWriter は Value() と Increment() の両方を要求する
type ReadWriter interface {
    Value() int
    Increment()
}

func main() {
    // ==========================================
    // 値型(Counter)のメソッドセット
    // メソッドセット = { Value() }
    // ==========================================
    c := Counter{count: 0}

    fmt.Println(c.Value()) // OK:Value は T のメソッドセットに含まれる

    // c.Increment() は呼び出せるように見えるが…
    c.Increment() // Go が自動的に (&c).Increment() に変換してくれる(シンタックスシュガー)
    fmt.Println(c.Value()) // → 1

    // ただし、インターフェースへの代入時は厳密にチェックされる
    var r Reader = c    // OK:Counter は Value() を持つ → Reader を実装
    fmt.Println(r.Value()) // → 1

    // var rw ReadWriter = c // ← コンパイルエラー!
    // Counter(値型)のメソッドセットは { Value() } のみ
    // Increment() は *Counter のメソッドセットにしか含まれない

    // ==========================================
    // ポインタ型(*Counter)のメソッドセット
    // メソッドセット = { Value(), Increment() }
    // ==========================================
    pc := &Counter{count: 0}

    var rw ReadWriter = pc // OK:*Counter は Value() も Increment() も持つ
    rw.Increment()
    fmt.Println(rw.Value()) // → 1

    var r2 Reader = pc // OK:*Counter は Reader も実装している
    fmt.Println(r2.Value()) // → 1

    // ==========================================
    // インターフェースのメソッドセット
    // 型セットに含まれる各型の共通部分
    // ==========================================
    type Adder interface {
        ~int | ~float64 // int か float64 を基底型とする型
    }
    // Adder の型セット = { int, float64 }
    // int のメソッドセット = {} (空)
    // float64 のメソッドセット = {} (空)
    // → 共通部分 = {} (空)
    // Adder 自体のメソッドセットは空
    // (型制約としては機能するが、メソッド呼び出しはできない)

    fmt.Println("メソッドセットのデモ完了!")
}

値レシーバとポインタレシーバの使い分け

package main

import "fmt"

type Temperature struct {
    celsius float64
}

// 値レシーバ:元のデータを変更しない(読み取り専用)
// → T と *T 両方のメソッドセットに含まれる
func (t Temperature) Celsius() float64 {
    return t.celsius
}

func (t Temperature) Fahrenheit() float64 {
    return t.celsius*9/5 + 32
}

// ポインタレシーバ:元のデータを変更する(書き込みあり)
// → *T のメソッドセットにのみ含まれる
func (t *Temperature) SetCelsius(c float64) {
    t.celsius = c
}

// インターフェース:値レシーバのメソッドのみを要求
type Thermometer interface {
    Celsius() float64
    Fahrenheit() float64
}

func printTemp(tm Thermometer) {
    fmt.Printf("%.1f℃ = %.1f℉\n", tm.Celsius(), tm.Fahrenheit())
}

func main() {
    t := Temperature{celsius: 100}

    // 値型でも *T 型でも Thermometer を実装できる
    // (要求されているのが値レシーバのメソッドのみのため)
    printTemp(t)   // → 100.0℃ = 212.0℉
    printTemp(&t)  // → 100.0℃ = 212.0℉

    // SetCelsius はポインタレシーバなので &t を使う必要がある
    t.SetCelsius(0) // Go が (&t).SetCelsius(0) に自動変換
    printTemp(t)    // → 0.0℃ = 32.0℉
}

よくある間違い・注意点

❌ 間違い1:値型がインターフェースを実装できないと混乱する

type Mover interface {
    Move() // ポインタレシーバで定義されたメソッドを要求
}

type Robot struct{ x int }

// ポインタレシーバで Move を定義
func (r *Robot) Move() {
    r.x++
}

func main() {
    r := Robot{}

    // r.Move() はシンタックスシュガーで呼び出せるが…
    r.Move() // OK(Go が &r に変換)

    // インターフェースへの代入は厳密
    // var m Mover = r  // ← コンパイルエラー!
    // Robot(値型)のメソッドセットに Move() は含まれない

    var m Mover = &r // ← ポインタなら OK
    m.Move()
    fmt.Println(r.x) // → 2
}

r.Move() が動くのは Go のシンタックスシュガーのおかげです。インターフェースへの代入はこの自動変換が効かないため、ポインタレシーバで定義したメソッドはポインタ型でしかインターフェースを満たせません


❌ 間違い2:メソッド名の重複

type MyType struct{}

func (m MyType) Hello() string { return "hello" }

// func (m *MyType) Hello() string { return "hi" } // ← コンパイルエラー!
// メソッドセット内でメソッド名は一意でなければならない
// T と *T のメソッドセットは統合されるため、同名は許されない

T*T のメソッドは同じ名前空間で管理されます。値レシーバとポインタレシーバで同じ名前のメソッドを定義することはできません


❌ 間違い3:インターフェースの型セットとメソッドセットを混同する

type MyInterface interface {
    ~int | ~string // 型セット(どの型を受け入れるか)
    String() string // メソッドセット(どのメソッドを要求するか)
}

// int は String() を持たないため MyInterface を実装できない
// string も String() を持たないため同様

// 型セットと要求メソッドの両方を満たす型だけが実装できる
type MyString string

func (s MyString) String() string { return string(s) }

// MyString は ~string の型セットに含まれ、String() も実装している
// → MyInterface を実装できる

インターフェースの「型セット」(~int | ~string のような記述)と「メソッドセット」(要求するメソッドの集合)は別の概念です。型がインターフェースを実装するには両方の条件を満たす必要があります


まとめ

メソッドセットの内容
定義型 Treceiver T で宣言されたメソッドのみ
ポインタ *Treceiver Treceiver *T で宣言されたメソッド
インターフェース型セットに含まれる各型のメソッドセットの共通部分
その他の型空(メソッドなし)

メソッドセットで最も重要なポイントは「値型 T のメソッドセットはポインタ型 *T のメソッドセットの部分集合」という関係です。ポインタレシーバのメソッドを持つ型をインターフェースに代入したいときは、必ずポインタ(&t)を使う必要があります。この非対称な関係に最初は戸惑うかもしれませんが、「ポインタは常に上位互換」と覚えておけばきっと大丈夫です! 🚀

おわりに 

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

よっしー
よっしー

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

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

コメント

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