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

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

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

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

スポンサーリンク

背景

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

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

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

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

型制約(Type constraints)

型制約とは、対応する型パラメータに許容される型引数の集合を定義し、その型パラメータの値がサポートする操作を制御するインターフェースである [Go 1.18]。

TypeConstraint = TypeElem .

制約が interface{E} という形式のインターフェースリテラルであり、E が埋め込み型要素(メソッドではない)である場合、型パラメータリストにおいては、便宜上、囲んでいる interface{ … } を省略してもよい:

[T []P]                      // = [T interface{[]P}]
[T ~int]                     // = [T interface{~int}]
[T int|string]               // = [T interface{int|string}]
type Constraint ~int         // 不正: ~int は型パラメータリスト内にない

事前宣言されたインターフェース型 comparable は、厳密に比較可能なすべての非インターフェース型の集合を表す [Go 1.18]。

インターフェースは型パラメータでなくても比較可能(comparable)ではあるが、厳密に比較可能(strictly comparable)ではないため、comparable を実装しない。ただし、comparable を満足する(satisfy)ことはできる。

int                          // comparable を実装する(int は厳密に比較可能)
[]byte                       // comparable を実装しない(スライスは比較できない)
interface{}                  // comparable を実装しない(上記参照)
interface{ ~int | ~string }  // 型パラメータ専用: comparable を実装する(int, string 型は厳密に比較可能)
interface{ comparable }      // 型パラメータ専用: comparable を実装する(comparable は自分自身を実装する)
interface{ ~int | ~[]byte }  // 型パラメータ専用: comparable を実装しない(スライスは比較可能ではない)
interface{ ~struct{ any } }  // 型パラメータ専用: comparable を実装しない(フィールド any は厳密に比較可能ではない)

comparable インターフェース、および comparable を(直接的または間接的に)埋め込むインターフェースは、型制約としてのみ使用できる。値や変数の型として、あるいは他の非インターフェース型の構成要素として使用することはできない。


翻訳パートはそのままで、解説パートを書き直します。


解説

型制約ってなに?

Go 1.18から「ジェネリクス(総称型)」という機能が使えるようになりました。これは、関数や型を作るとき、「あとから好きな型を当てはめられる」ようにする仕組みです。

ただ、「なんでもOK」だと困ることがあります。たとえば関数の中で + を使いたいのに、渡された型が足し算できない型だったら……? そこで登場するのが型制約です。「この関数には、こういう型だけ渡していいよ」とルールを決めるためのものですね。

// T には int か string だけ渡せるよ、という制約をつけた関数
func PrintValue[T int | string](v T) {
    fmt.Println(v)
}

PrintValue(42)        // OK! int だから大丈夫
PrintValue("hello")   // OK! string だから大丈夫
PrintValue(3.14)      // コンパイルエラー! float64 はダメ

型制約の正体は、実はインターフェースです。ただし、普段よく見る「メソッドの集合を定義するインターフェース」とは少し違って、「許可する型の集合」を定義するインターフェースになっています。

interface{...} は省略できる

型パラメータを書くとき、本来は [T interface{~int}] のように interface{...} で囲む必要があります。でも毎回書くのは面倒ですよね。そこで、型パラメータリスト [T ...] の中に限り、この囲みを省略できるようになっています。

// この2つはまったく同じ意味です
func F1[T interface{~int}](v T) {}
func F2[T ~int](v T) {}            // こっちのほうがスッキリ!

ただし注意点がひとつ。型パラメータリストのでは省略できません。

type Constraint interface{ ~int }  // OK
type Constraint ~int               // コンパイルエラー!

~int ってどういう意味?

~(チルダ)は「その型を元にして作られた型も全部OK」という意味です。

Goでは type MyInt int のように、既存の型をもとに新しい型を作れます。~int と書くと、int だけでなく MyInt のような派生型もまとめて許可できます。

type MyInt int  // int をもとに作った新しい型

func Double[T ~int](v T) T {
    return v * 2
}

Double(MyInt(5))  // OK! MyInt の元は int だからセーフ
Double(5)         // OK! int そのものも当然セーフ

もし ~ をつけずに int とだけ書くと、int 型ぴったりしか受け付けません。MyInt は通らなくなります。この違いは地味ですが、ライブラリを作るときにはとても大事です。

comparable ってなに?

comparable は、Goにあらかじめ用意されている特別な型制約で、「==!= で比較できる型」を意味します。

もう少し身近に言うと、mapのキーに使える型と考えるとわかりやすいです。mapのキーには比較できる型しか使えませんよね。それと同じ条件です。

// スライスの中に target があるか調べる関数
func Contains[T comparable](slice []T, target T) bool {
    for _, v := range slice {
        if v == target {  // == が使えることが保証されている!
            return true
        }
    }
    return false
}

Contains([]int{1, 2, 3}, 2)          // OK!
Contains([]string{"a", "b"}, "a")    // OK!
// Contains([][]int{{1}}, []int{1})  // エラー! スライスは == で比較できない

ちょっとややこしい話:「実装する」と「満足する」

ここは正直、Go仕様の中でも難しいところです。ゆっくりいきましょう。

interface{}(= any)について考えてみます。interface{} 型の値は、実行時に == で比較すること自体はできます。でも、中身がスライスだったりするとパニック(実行時エラー)を起こします。つまり、いつでも安全に比較できるとは限らないんですね。

この「いつでも安全に比較できる」ことを、Goの仕様では**厳密に比較可能(strictly comparable)**と呼んでいます。

ここで2つの言葉が出てきます。

言葉ざっくり言うと
implements(実装する)型制約のチェックに通る → 型引数として渡せる
satisfies(満足する)普通のインターフェース代入として互換性がある

たとえ話で説明しますね。

  • 「実装する」 → 運転免許を持っている。確実に安全に運転できる。
  • 「満足する」 → 運転席に座ることはできる。でも免許を持っているかはわからない。

interface{} は「運転席に座れる(satisfy)」けど「免許は持っていない(implementしない)」状態です。型制約の場面では免許が求められるので、interface{}comparable の型引数として渡すことはできません。

comparable は型制約専用

最後にもうひとつ大事なルール。comparable は型制約としてしか使えません。普通の変数の型としては使えないんです。

// OK! 型制約として使う
func F[T comparable](v T) {}

// コンパイルエラー! 変数の型にはできない
var x comparable

なぜかというと、comparable は「こういう性質を持つ型の集合」という概念であって、具体的なひとつの型ではないからです。「果物」というカテゴリーで変数は作れないけど、「りんご」なら作れる、というイメージに近いですね。

おわりに 

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

よっしー
よっしー

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

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

コメント

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