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

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

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

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

スポンサーリンク

背景

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

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

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

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

インスタンス化(Instantiations)

ジェネリック関数またはジェネリック型は、型パラメータに型引数を代入することによってインスタンス化される [Go 1.18]。インスタンス化は2つのステップで進行する:

  1. 各型引数が、ジェネリック宣言における対応する型パラメータに代入される。この代入は、型パラメータリスト自体およびそのリスト内の型を含む、関数または型の宣言全体にわたって行われる。
  2. 代入の後、各型引数は対応する型パラメータの制約(必要であればインスタンス化された制約)を満足しなければならない。そうでなければインスタンス化は失敗する。

型のインスタンス化は新しい非ジェネリックの名前付き型を生成する。関数のインスタンス化は新しい非ジェネリックの関数を生成する。

型パラメータリスト       型引数            代入後

[P any]                int               int は any を満足する
[S ~[]E, E any]        []int, int        []int は ~[]int を満足する, int は any を満足する
[P io.Writer]          string            不正: string は io.Writer を満足しない
[P comparable]         any               any は comparable を満足する(ただし実装はしない)

ジェネリック関数を使用する際、型引数は明示的に提供するか、関数が使用される文脈から部分的または完全に推論することができる。推論可能であれば、以下の場合に型引数リストを完全に省略できる:

  • 通常の引数で呼び出される場合
  • 既知の型を持つ変数に代入される場合
  • 別の関数への引数として渡される場合
  • 結果として返される場合

その他のすべての場合では、(部分的な場合もある)型引数リストが存在しなければならない。型引数リストが存在しないか部分的である場合、欠けているすべての型引数は関数が使用される文脈から推論可能でなければならない。

// sum はその引数の合計(文字列の場合は連結)を返す。
func sum[T ~int | ~float64 | ~string](x... T) T { … }

x := sum                       // 不正: x の型が不明
intSum := sum[int]             // intSum は func(x... int) int 型を持つ
a := intSum(2, 3)              // a は int 型の値 5 を持つ
b := sum[float64](2.0, 3)      // b は float64 型の値 5.0 を持つ
c := sum(b, -1)                // c は float64 型の値 4.0 を持つ

type sumFunc func(x... string) string
var f sumFunc = sum            // var f sumFunc = sum[string] と同じ
f = sum                        // f = sum[string] と同じ

部分的な型引数リストは空であってはならない。少なくとも最初の引数が存在しなければならない。リストは型引数の完全なリストの接頭辞であり、残りの引数は推論に委ねられる。大まかに言えば、型引数は「右から左へ」省略できる。

func apply[S ~[]E, E any](s S, f func(E) E) S { … }

f0 := apply[]                  // 不正: 型引数リストは空であってはならない
f1 := apply[[]int]             // S の型引数を明示的に提供、E の型引数は推論される
f2 := apply[[]string, string]  // 両方の型引数を明示的に提供

var bytes []byte
r := apply(bytes, func(byte) byte { … })  // 両方の型引数が関数の引数から推論される

ジェネリック型に対しては、すべての型引数を常に明示的に提供しなければならない。


解説

インスタンス化ってなに?

ジェネリクスの関数や型は、型パラメータに「穴」が空いた設計図のようなものです。インスタンス化とは、その穴に具体的な型を当てはめて、実際に使える関数や型を作ることです。

func min[T ~int|~float64](x, y T) T {
    if x < y { return x }
    return y
}

// インスタンス化:T に int を当てはめる
min[int](3, 5)  // → int 用の min 関数ができて呼ばれる

たとえるなら、クッキーの型抜き(ジェネリック関数)で、実際に生地を抜く(インスタンス化する)ようなものです。型抜きだけでは食べられませんが、生地を抜けばクッキーになります。

インスタンス化の2つのステップ

インスタンス化は内部的に2段階で行われます。

ステップ1:型引数を当てはめる

// [S ~[]E, E any] に []int, int を当てはめると...
// S → []int、E → int に置き換わる

面白いのは、型パラメータリスト自体にも代入が行われる点です。上の例では、S の制約 ~[]EEint に置き換わり、~[]int になります。

ステップ2:制約を満たすかチェック

// 置き換え後:
// []int は ~[]int を満足する? → はい ✅
// int は any を満足する?     → はい ✅
// → インスタンス化成功!

もし制約を満たさなければ、コンパイルエラーになります。

// string は io.Writer を満足しない → インスタンス化失敗!
func writeAll[W io.Writer](writers ...W) { ... }
writeAll[string]("hello")  // コンパイルエラー!

型推論:型引数を省略できる

毎回型引数を明示的に書くのは面倒です。Go のコンパイラは多くの場合、文脈から型引数を推論してくれます。

func sum[T ~int | ~float64 | ~string](x... T) T { ... }

// 明示的に指定
b := sum[float64](2.0, 3)

// 型推論に任せる(引数 b が float64 なので T は float64 と推論される)
c := sum(b, -1)

型推論が効くのは以下の4つの場面です。

1. 通常の引数で呼び出すとき

sum(1, 2, 3)  // 引数が int → T は int と推論

2. 既知の型の変数に代入するとき

type sumFunc func(x... string) string
var f sumFunc = sum  // f の型から T は string と推論

3. 別の関数の引数として渡すとき

func apply(f func(int, int) int) { ... }
apply(min)  // apply の引数の型から T は int と推論

4. 戻り値として返すとき

func getMin() func(int, int) int {
    return min  // 戻り値の型から T は int と推論
}

型推論が効かない場合

推論の手がかりがない場合は、明示的に書く必要があります。

x := sum  // コンパイルエラー! T が何なのかわからない

x の型も指定されておらず、引数もないので、コンパイラは T を推論できません。

intSum := sum[int]  // OK! 明示的に指定すれば大丈夫

部分的な型引数リスト

型引数は「右から左へ」省略できます。つまり、左側の型パラメータだけ指定して、残りは推論に任せることができます。

func apply[S ~[]E, E any](s S, f func(E) E) S { ... }

// S だけ指定、E は S から推論される
f1 := apply[[]int]             // S=[]int → E=int と推論

// 両方指定(省略なし)
f2 := apply[[]string, string]

// 両方省略(引数から推論)
var bytes []byte
r := apply(bytes, func(b byte) byte { return b + 1 })

ただし、空の型引数リスト [] は書けません。

f0 := apply[]  // コンパイルエラー! 空はダメ

「部分的に省略する」とは、あくまで右側を省略することです。左側を省略して右側だけ書くことはできません。

// E だけ指定して S を省略、ということはできない
// apply[_, int](...)  ← こういう書き方はない

ジェネリック型は推論できない

関数と違い、ジェネリック型のインスタンス化では型引数を省略できません。 すべて明示的に書く必要があります。

type Pair[A, B any] struct {
    First  A
    Second B
}

// 型引数は必ず明示する
p := Pair[int, string]{First: 1, Second: "hello"}

// これはできない
// p := Pair{First: 1, Second: "hello"}  // コンパイルエラー!

関数は引数の値から型を推論できますが、型の宣言では「値」が登場するのがずっと後(リテラルを作るとき)なので、コンパイラが推論するタイミングがないのです。

comparable と any の関係(再確認)

原文のテーブルの最後の行に注目してください。

[P comparable]         any               any は comparable を満足する(ただし実装はしない)

これは以前学んだ内容の復習です。Go 1.20 の例外ルールにより、anycomparable 制約を「満足する」のでインスタンス化は成功しますが、「実装はしない」ため実行時にパニックする可能性があります。この微妙な区別を忘れないようにしましょう。

おわりに 

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

よっしー
よっしー

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

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

コメント

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