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

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

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

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

スポンサーリンク

背景

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

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

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

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

メソッド宣言(Method declarations)

メソッドはレシーバを持つ関数である。メソッド宣言は、識別子(メソッド名)をメソッドに束縛し、そのメソッドをレシーバの基底型に関連付ける。

MethodDecl = "func" Receiver MethodName Signature [ FunctionBody ] .
Receiver   = Parameters .

レシーバは、メソッド名の前に置かれる追加のパラメータセクションによって指定される。このパラメータセクションは、可変長でない単一のパラメータ、すなわちレシーバを宣言しなければならない。その型は定義型 T または定義型 T へのポインタでなければならず、角括弧で囲まれた型パラメータ名のリスト [P1, P2, …] が後に続くことがある。T はレシーバ基底型と呼ばれる。レシーバ基底型はポインタ型やインターフェース型であってはならず、メソッドと同じパッケージ内で定義されていなければならない。メソッドはそのレシーバ基底型に束縛されると言い、メソッド名は型 T または *T のセレクタ内でのみ可視である。

ブランクでないレシーバ識別子は、メソッドシグネチャ内で一意でなければならない。レシーバの値がメソッド本体の中で参照されない場合、宣言においてその識別子を省略することができる。これは関数やメソッドのパラメータ一般にも同様に当てはまる。

基底型に対して、それに束縛されるメソッドのブランクでない名前は一意でなければならない。基底型が構造体型である場合、ブランクでないメソッド名とフィールド名は異なっていなければならない。

定義型 Point が与えられたとき、以下の宣言は

func (p *Point) Length() float64 {
	return math.Sqrt(p.x * p.x + p.y * p.y)
}

func (p *Point) Scale(factor float64) {
	p.x *= factor
	p.y *= factor
}

レシーバ型 *Point を持つメソッド LengthScale を基底型 Point に束縛する。

レシーバ基底型がジェネリック型である場合、レシーバの指定は、メソッドが使用するための対応する型パラメータを宣言しなければならない。これにより、レシーバの型パラメータがメソッドから利用可能になる。構文的には、この型パラメータ宣言はレシーバ基底型のインスタンス化のように見える。型引数は、宣言される型パラメータを表す識別子でなければならず、レシーバ基底型の型パラメータごとに1つずつ必要である。型パラメータ名は、レシーバ基底型の定義における対応するパラメータ名と一致する必要はなく、ブランクでないすべてのパラメータ名はレシーバパラメータセクションとメソッドシグネチャ内で一意でなければならない。レシーバの型パラメータ制約は、レシーバ基底型の定義によって暗黙的に決まる。対応する型パラメータは対応する制約を持つ。

type Pair[A, B any] struct {
	a A
	b B
}

func (p Pair[A, B]) Swap() Pair[B, A]  { … }  // レシーバは A, B を宣言する
func (p Pair[First, _]) First() First  { … }  // レシーバは First を宣言する。Pair の A に対応する

レシーバ型がエイリアスによって(またはエイリアスへのポインタによって)表される場合、そのエイリアスはジェネリックであってはならず、インスタンス化されたジェネリック型を表してはならない。これは直接的にも、別のエイリアスを経由して間接的にも、ポインタの間接参照の有無にかかわらず適用される。

type GPoint[P any] = Point
type HPoint        = *GPoint[int]
type IPair         = Pair[int, int]

func (*GPoint[P]) Draw(P)   { … }  // 不正: エイリアスはジェネリックであってはならない
func (HPoint) Draw(P)       { … }  // 不正: エイリアスはインスタンス化された型 GPoint[int] を表してはならない
func (*IPair) Second() int  { … }  // 不正: エイリアスはインスタンス化された型 Pair[int, int] を表してはならない

解説

メソッドってなに?

メソッドは、ひとことで言えば「ある型に紐づいた関数」です。普通の関数との違いは、レシーバと呼ばれる特別な引数を持つことです。

普通の関数とメソッドを比較してみましょう。

// 普通の関数:Point と関係はあるけど、紐づいてはいない
func Length(p Point) float64 {
    return math.Sqrt(p.x*p.x + p.y*p.y)
}

// メソッド:Point に紐づいている
func (p Point) Length() float64 {
    return math.Sqrt(p.x*p.x + p.y*p.y)
}

呼び出し方も変わります。

// 普通の関数
Length(myPoint)

// メソッド
myPoint.Length()

メソッドを使うと「この操作はこの型のもの」という関係が明確になります。他の言語でいう「クラスのメソッド」に近い感覚ですね。ただし Go にはクラスがないので、代わりにレシーバという仕組みで型と関数を結びつけています。

レシーバの書き方

レシーバは func とメソッド名の間に () で囲んで書きます。

func (p Point) Length() float64 { ... }
//    ^^^^^^^^ これがレシーバ

レシーバには2種類あります。

// 値レシーバ:元のデータのコピーを受け取る
func (p Point) Length() float64 { ... }

// ポインタレシーバ:元のデータそのものを指すポインタを受け取る
func (p *Point) Scale(factor float64) { ... }

使い分けの目安はシンプルです。

  • 値を変更しないメソッド → 値レシーバでOK
  • 値を変更するメソッド → ポインタレシーバが必要

Scale の例を見てみましょう。

func (p *Point) Scale(factor float64) {
    p.x *= factor  // 元のデータを直接変更している
    p.y *= factor
}

もしこれが値レシーバ (p Point) だったら、コピーの xy が変わるだけで、元の Point には何も起きません。ポインタレシーバにすることで、呼び出し元のデータを直接変更できるわけです。

レシーバに関するルール

いくつか制限があるので、まとめておきますね。

同じパッケージ内でなければならない

他のパッケージで定義された型にメソッドを追加することはできません。

// 自分のパッケージで定義した型ならOK
type MyPoint struct{ x, y float64 }
func (p MyPoint) Length() float64 { ... }  // OK!

// 標準ライブラリの型にメソッドを追加するのはNG
func (s string) Shout() string { ... }  // コンパイルエラー!

ポインタ型やインターフェース型はレシーバ基底型にできない

type MyPtr *int
func (p MyPtr) Double() { ... }  // コンパイルエラー! ポインタ型は基底型にできない

メソッド名とフィールド名は被ってはいけない

type User struct {
    Name string  // フィールド名が Name
}
func (u User) Name() string { ... }  // コンパイルエラー! Name は既にフィールドで使われている

使わないレシーバは名前を省略できる

func (Point) Origin() Point {
    return Point{0, 0}  // レシーバの値を使わないので名前を省略
}

ジェネリック型のメソッド

ジェネリック型にメソッドを定義するときは、レシーバにも型パラメータを書く必要があります。

type Pair[A, B any] struct {
    a A
    b B
}

// レシーバに [A, B] を書くことで、メソッド内で A, B が使える
func (p Pair[A, B]) Swap() Pair[B, A] {
    return Pair[B, A]{a: p.b, b: p.a}
}

ちょっとわかりにくいのは、レシーバの型パラメータ名は元の定義と同じでなくてもよいという点です。

// Pair の定義では A, B だが、メソッドでは First, _ と書いてもよい
func (p Pair[First, _]) First() First {
    return p.a
}

ここで _ は「この型パラメータは使わない」という意味です。Pair の2つ目の型パラメータ B に対応しますが、このメソッドでは不要なので捨てています。

エイリアスに関する制限

最後のエイリアスの話は、やや上級者向けの内容です。

type NewName = ExistingType という構文で型のエイリアス(別名)を作れますが、エイリアスに対してメソッドを定義するときには厳しい制限があります。

type GPoint[P any] = Point        // ジェネリックなエイリアス
type IPair         = Pair[int, int]  // インスタンス化された型のエイリアス

func (*GPoint[P]) Draw(P) { … }   // NG! エイリアスがジェネリック
func (*IPair) Second() int { … }   // NG! エイリアスがインスタンス化された型を指している

これらが禁止されているのは、「メソッドが本当はどの型に属するのか」が曖昧になるのを防ぐためです。普段の開発で出会うことはほとんどないので、「そういうルールがある」と頭の片隅に置いておけば十分です。

おわりに 

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

よっしー
よっしー

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

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

コメント

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