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

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

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

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

スポンサーリンク

背景

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

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

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

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

メソッド式(Method expressions)

M が型 T のメソッドセットに含まれる場合、T.MM と同じ引数にメソッドのレシーバを表す追加の引数が先頭に付加された、通常の関数として呼び出し可能な関数である。

MethodExpr   = ReceiverType "." MethodName .
ReceiverType = Type .

構造体型 T に2つのメソッドがあるとする。1つは型 T のレシーバを持つ Mv、もう1つは型 *T のレシーバを持つ Mp である。

type T struct {
	a int
}
func (tv  T) Mv(a int) int         { return 0 }  // 値レシーバ
func (tp *T) Mp(f float32) float32 { return 1 }  // ポインタレシーバ

var t T

T.Mv

Mv と等価な関数を生成するが、明示的なレシーバを第1引数として取る。そのシグネチャは以下のとおりである

func(tv T, a int) int

この関数は明示的なレシーバを伴って通常通り呼び出すことができる。したがって、以下の5つの呼び出しは等価である:

t.Mv(7)
T.Mv(t, 7)
(T).Mv(t, 7)
f1 := T.Mv; f1(t, 7)
f2 := (T).Mv; f2(t, 7)

同様に、式

(*T).Mp

は以下のシグネチャを持つ Mp を表す関数値を生成する

func(tp *T, f float32) float32

値レシーバを持つメソッドからは、明示的なポインタレシーバを持つ関数を導出することもできる。したがって

(*T).Mv

は以下のシグネチャを持つ Mv を表す関数値を生成する

func(tv *T, a int) int

このような関数はレシーバを間接参照して、基底のメソッドにレシーバとして渡す値を生成する。メソッドは関数呼び出しで渡されたアドレスの値を上書きしない。

最後のケース、つまりポインタレシーバのメソッドに対する値レシーバの関数は不正である。ポインタレシーバのメソッドは値型のメソッドセットに含まれないからである。

メソッドから導出された関数値は関数呼び出し構文で呼び出される。レシーバは呼び出しの第1引数として提供される。つまり、f := T.Mv が与えられたとき、ft.f(7) ではなく f(t, 7) として呼び出される。レシーバを束縛する関数を構築するには、関数リテラルまたはメソッド値を使用する。

インターフェース型のメソッドから関数値を導出することは許可されている。結果として得られる関数は、そのインターフェース型の明示的なレシーバを取る。


解説

メソッド式ってなに?

メソッド式は、メソッドを普通の関数として取り出す仕組みです。

Go では通常、メソッドは t.Mv(7) のように「レシーバ . メソッド名」の形で呼び出します。でもときには、メソッドを普通の関数のように扱いたいことがあります。そのとき使うのがメソッド式です。

type T struct{ a int }
func (t T) Mv(a int) int { return 0 }

var t T

// 通常のメソッド呼び出し
t.Mv(7)

// メソッド式:T.Mv で「レシーバを第1引数に取る関数」が得られる
T.Mv(t, 7)  // 同じ結果になる

T.Mv とすると、レシーバがただの引数に「昇格」した関数が得られます。つまり、こういうシグネチャの関数になります。

// もともとのメソッド: func (t T) Mv(a int) int
// メソッド式の結果:   func(t T, a int) int

レシーバが関数の第1引数として前に出てきただけ、と考えるとわかりやすいですね。

何が嬉しいの?

「普通にメソッドとして呼べばいいのに、なぜこんな仕組みがあるの?」と思うかもしれません。主な用途は2つあります。

1. 関数として変数に代入したいとき

f := T.Mv      // f は func(T, int) int 型
f(t, 7)        // 関数として呼び出せる

2. 関数を引数に取る API に渡したいとき

// 各要素を変換する関数があるとする
func Map[T, U any](s []T, f func(T) U) []U { ... }

// メソッドをそのまま渡せる
names := Map(users, User.GetName)

User.GetName と書くだけで、「ユーザーを受け取って名前を返す関数」として使えるわけです。

5つの等価な呼び出し

原文にある5つの呼び出しは、実は全部同じことをしています。

t.Mv(7)               // 普通のメソッド呼び出し
T.Mv(t, 7)            // メソッド式を直接呼ぶ
(T).Mv(t, 7)          // T を括弧で囲んでもOK
f1 := T.Mv; f1(t, 7)  // いったん変数に代入
f2 := (T).Mv; f2(t, 7) // 括弧つきでも同じ

表記の違いだけで、やっていることは全部同じです。「好きな書き方で書ける」というだけの話なので、実際には一番上の t.Mv(7) を使うのが普通です。

ポインタレシーバの場合

ポインタレシーバのメソッドは、(*T).Mp のように括弧つきで書きます。

func (tp *T) Mp(f float32) float32 { ... }

// メソッド式
g := (*T).Mp   // g は func(*T, float32) float32 型
g(&t, 3.14)

値レシーバ → ポインタレシーバの変換

ちょっと面白いのが、値レシーバのメソッドをポインタレシーバ版として取り出せることです。

func (tv T) Mv(a int) int { ... }  // 値レシーバ

h := (*T).Mv  // h のシグネチャは func(*T, int) int
h(&t, 7)      // ポインタを渡して呼べる

このとき、関数は渡されたポインタの先の値をコピーしてメソッドに渡します。つまり、メソッドの中で変更してもポインタの先の値は変わらないということです。これは元が値レシーバだったからですね。

逆方向(ポインタレシーバ → 値レシーバ)はできない

逆に、ポインタレシーバのメソッドから値レシーバ版を作ることはできません。

func (tp *T) Mp(f float32) float32 { ... }  // ポインタレシーバ

badFunc := T.Mp  // コンパイルエラー!

これが禁止されている理由は、値型 T のメソッドセットにポインタレシーバのメソッドは含まれないからです。

少し直感的に言うと、値 T を渡してもらっても、その値を「変更可能な元データ」として扱うことができません(ただのコピーなので)。ポインタレシーバのメソッドは元データを変更することを前提としているので、値だけ渡されても困ってしまうわけです。

メソッド式 vs メソッド値

ここで似た概念との違いに触れておきます。メソッド式とよく似たものに「メソッド値」というのがあり、混同しやすいので注意が必要です。

// メソッド式:レシーバを引数として渡す
f1 := T.Mv        // func(T, int) int
f1(t, 7)

// メソッド値:レシーバが束縛された関数
f2 := t.Mv        // func(int) int
f2(7)             // t は既に束縛されているので、引数不要
メソッド式メソッド値
書き方T.Mv(型名)t.Mv(値)
シグネチャレシーバが引数に含まれるレシーバが束縛されている
使う場面汎用的な関数として扱いたい特定のインスタンスの処理を渡したい

メソッド値については、仕様書の次の節で詳しく説明されます。

インターフェース型のメソッド式

インターフェース型からもメソッド式を作れます。

type Reader interface {
    Read(p []byte) (n int, err error)
}

f := Reader.Read   // func(Reader, []byte) (int, error)

結果の関数は、第1引数にインターフェース型の値を取ります。実際に呼び出されたときは、インターフェースの動的な型のメソッドが実行されます。使う場面は限られますが、「こういうこともできる」と知っておくと役立つ場面があるかもしれません。

おわりに 

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

よっしー
よっしー

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

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

コメント

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