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

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

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

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

スポンサーリンク

背景

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

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

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

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

基底型

すべての型 T には**基底型(underlying type)**があります。T が事前宣言されたブール型、数値型、文字列型のいずれか、またはリテラル型である場合、対応する基底型は T 自身です。それ以外の場合、T の基底型は、T の宣言においてその T が参照している型の基底型となります。型パラメータにおける基底型は、その型制約の基底型となり、これは常にインターフェースです。

type (
    A1 = string
    A2 = A1
)

type (
    B1 string
    B2 B1
    B3 []B1
    B4 B3
)

func f[P any](x P) { … }

stringA1A2B1B2 の基底型はいずれも string です。[]B1B3B4 の基底型はいずれも []B1 です。P の基底型は interface{} です。


解説

たとえ話:「素材」と「製品」の関係

「基底型」という概念は、素材と製品の関係で考えるとわかりやすくなります。

たとえば、製菓工場を想像してください。原材料は「小麦粉」です。その小麦粉を使って「パン生地」を作り、さらに「食パン」を作るとします。名前や形は変わっていますが、突き詰めるとすべて「小麦粉」が原材料です。

Go の型も同じです。どれだけ型に別名を付けたり、新しい型として定義し直したりしても、その型の「大元の素材(=基底型)」は変わりません。


型エイリアスと型定義の違いを先に整理しよう

基底型を理解するために、まず2つの構文の違いを押さえましょう。

構文意味
type A1 = string型エイリアスA1string の完全な別名(同一の型)
type B1 string型定義string を基にした新しい型を作る

コード例

package main

import "fmt"

// ========================================
// 型エイリアス(= を使う)
// ========================================
type (
    A1 = string // A1 は string そのもの(別名にすぎない)
    A2 = A1     // A2 も辿ると string → 基底型は string
)

// ========================================
// 型定義(= を使わない)
// ========================================
type (
    B1 string // string を元に新しい型 B1 を定義
    B2 B1     // B1 を元に新しい型 B2 を定義(辿ると string)
    B3 []B1   // []B1 を元に新しい型 B3 を定義
    B4 B3     // B3 を元に新しい型 B4 を定義(辿ると []B1)
)

func main() {
    // ----------------------------------------
    // 型エイリアスは string と完全に互換
    // ----------------------------------------
    var a1 A1 = "hello" // A1 は string そのものなので代入できる
    var a2 A2 = a1      // A2 も string なので代入できる
    var s string = a2   // string 変数にもそのまま代入できる
    fmt.Println(s)      // → hello

    // ----------------------------------------
    // 型定義は「新しい型」なので暗黙的な代入は不可
    // ----------------------------------------
    var b1 B1 = "world" // B1 は string ベースの新しい型
    // var b2 string = b1  // ← コンパイルエラー!型が異なる
    var b2 string = string(b1) // 明示的な型変換が必要
    fmt.Println(b2)            // → world

    // B2 の基底型も string(B1 を辿ると string)
    var b3 B2 = B2(b1) // B1 → B2 も明示的な変換が必要
    fmt.Println(b3)    // → world

    // ----------------------------------------
    // スライスを基にした型定義
    // ----------------------------------------
    var b4 B3 = []B1{"a", "b", "c"} // B3 の基底型は []B1
    var b5 B4 = B4(b4)              // B4 の基底型も []B1(B3 を辿ると []B1)
    fmt.Println(b5)                 // → [a b c]
}

// ----------------------------------------
// 型パラメータの基底型は interface{}
// ----------------------------------------
func f[P any](x P) {
    // P の基底型は interface{}(any = interface{} のエイリアス)
    // P は実行時に具体的な型に置き換えられるが、
    // 基底型としては常に interface{} として扱われる
    fmt.Println(x)
}

よくある間違い・注意点

❌ 間違い1:型エイリアスと型定義を混同する

type MyString1 = string // エイリアス:string と同じ型
type MyString2 string   // 定義:string とは別の新しい型

func printString(s string) {
    fmt.Println(s)
}

func main() {
    var m1 MyString1 = "hello"
    var m2 MyString2 = "world"

    printString(m1) // OK:MyString1 は string そのもの
    // printString(m2) // ← コンパイルエラー!
    printString(string(m2)) // 型変換すれば OK
}

= があるかないかだけの違いに見えますが、振る舞いは大きく異なります。エイリアスは「ただの別名」、型定義は「新しい型の誕生」と覚えましょう。


❌ 間違い2:基底型が同じなら代入できると思い込む

type Celsius    float64
type Fahrenheit float64

func main() {
    c := Celsius(100.0)
    // f := Fahrenheit(c) // ← コンパイルエラー!
    // 基底型が同じ float64 でも、型定義が異なる型同士の直接変換は不可

    // 一度 float64 に変換してから Fahrenheit に変換する
    f := Fahrenheit(float64(c))
    fmt.Println(f) // → 100
}

基底型が一致していても、型定義が異なれば別の型として扱われます。Go の型システムは「見た目ではなく定義」を厳格に区別します。


❌ 間違い3:型パラメータの基底型を誤解する

func printLen[P any](x P) {
    // P の基底型は interface{} なので、
    // string のメソッド(len など)は直接呼び出せない
    // fmt.Println(len(x)) // ← コンパイルエラー!

    // 型制約を使って基底型を絞り込む必要がある
    fmt.Println(x)
}

// 制約を使って基底型を絞り込む例
type Stringer interface {
    ~string // 基底型が string である型すべてを受け入れる
}

func printAsString[P Stringer](x P) {
    fmt.Println(string(x)) // 基底型が string なので変換できる
}

~string という記法は「基底型が string である型すべて」を意味します。B1B2 のような型定義も含めて受け入れたい場合に使う、強力な制約表現です。


まとめ

ケース基底型
string(事前宣言型)string 自身
type A1 = string(エイリアス)string(A1 は string と同一)
type B1 string(型定義)string(宣言先を辿る)
type B2 B1(型定義の連鎖)string(B1 → string と辿る)
type B3 []B1(リテラル型)[]B1(リテラル型はそれ自身)
type B4 B3(型定義の連鎖)[]B1(B3 → []B1 と辿る)
型パラメータ Pinterface{}(型制約の基底型)

「基底型を辿る」というイメージが大切です。型定義の連鎖がどれだけ長くても、一番大元の素材にたどり着くまで遡ったものが基底型です。この概念はジェネリクスの型制約(~T 記法)とも深く関わっており、Go の型システムを正しく理解するための重要な土台になります。焦らず、サンプルコードを手元で動かしながら確認してみてください! 🚀

おわりに 

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

よっしー
よっしー

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

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

コメント

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