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

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

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

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

スポンサーリンク

背景

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

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

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

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

メソッド値(Method values)

x の静的型が T であり、M が型 T のメソッドセットに含まれる場合、x.M はメソッド値と呼ばれる。メソッド値 x.M は、x.M のメソッド呼び出しと同じ引数で呼び出し可能な関数値である。式 x はメソッド値の評価時に評価されて保存される。保存されたコピーは、後で実行される可能性のある呼び出しにおいてレシーバとして使用される。

type S struct { *T }
type T int
func (t T) M() { print(t) }

t := new(T)
s := S{T: t}
f := t.M                    // レシーバ *t が評価されて f に格納される
g := s.M                    // レシーバ *(s.T) が評価されて g に格納される
*t = 42                     // f と g に格納されたレシーバには影響しない

T はインターフェース型でも非インターフェース型でもよい。

前述のメソッド式の議論と同様に、構造体型 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
var pt *T
func makeT() T

t.Mv

は以下の型の関数値を生成する

func(int) int

以下の2つの呼び出しは等価である:

t.Mv(7)
f := t.Mv; f(7)

同様に、式

pt.Mp

は以下の型の関数値を生成する

func(float32) float32

セレクタと同様に、ポインタを使用した値レシーバの非インターフェースメソッドへの参照は、そのポインタを自動的に間接参照する。pt.Mv(*pt).Mv と等価である。

メソッド呼び出しと同様に、アドレス指定可能な値を使用したポインタレシーバの非インターフェースメソッドへの参照は、その値のアドレスを自動的に取得する。t.Mp(&t).Mp と等価である。

f := t.Mv; f(7)   // t.Mv(7) と同様
f := pt.Mp; f(7)  // pt.Mp(7) と同様
f := pt.Mv; f(7)  // (*pt).Mv(7) と同様
f := t.Mp; f(7)   // (&t).Mp(7) と同様
f := makeT().Mp   // 不正: makeT() の結果はアドレス指定可能ではない

上記の例は非インターフェース型を使用しているが、インターフェース型の値からメソッド値を作成することも許可されている。

var i interface { M(int) } = myVal
f := i.M; f(7)  // i.M(7) と同様

解説

メソッド値ってなに?

前回のメソッド式と似ていますが、違いを押さえておきましょう。

  • メソッド式 T.Mv:型名から取り出す。レシーバを引数として渡す。
  • メソッド値 t.Mv値から取り出す。レシーバは既に束縛されている。
t := T{a: 10}

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

// メソッド値:レシーバ t が既に束縛されている
g := t.Mv         // func(int) int
g(7)              // t は書かなくていい

メソッド値は、特定のインスタンスに紐づいた関数を作るイメージです。「この tMv メソッド」という形で関数として持ち歩けます。

レシーバは作成時にコピーされる

ここが非常に重要なポイントです。メソッド値を作るとき、レシーバの値がその時点で評価されてコピーされます。あとから元の変数を変更しても、保存されたレシーバには影響しません。

type T int
func (t T) M() { fmt.Println(t) }

t := T(10)
f := t.M      // この時点の t の値(10)がコピーされる
t = 99        // 元の t を変更しても...
f()           // → 10 が出力される。保存された値は変わらない

原文の例を見てみましょう。

t := new(T)     // t は *T 型。*t は 0
s := S{T: t}
f := t.M        // レシーバ *t(= 0)が評価されて f に格納
g := s.M        // レシーバ *(s.T)(= 0)が評価されて g に格納
*t = 42         // 元の *t を 42 に変更
// でも f も g も、保存された値は 0 のまま!

このように、メソッド値は「スナップショット」を取るような動きをします。作った瞬間の状態が固定されると覚えておいてください。

ポインタと値の自動変換

メソッド値を作るときも、セレクタと同様に自動変換が働きます。

var t T
var pt *T = &t

f := pt.Mv    // 実際には (*pt).Mv として評価される(自動デリファレンス)
f := t.Mp     // 実際には (&t).Mp として評価される(自動アドレス取得)

普段は気にせずに書けますが、裏でこういう変換が行われていると知っておくと役立ちます。

「アドレス指定可能」の制限

ポインタレシーバのメソッド値を作るとき、元の値がアドレス指定可能でないとエラーになります。

func makeT() T { return T{} }

f := makeT().Mp  // コンパイルエラー!

なぜかというと、t.Mp は内部的に (&t).Mp に変換されますが、関数の戻り値は一時的な値なので & でアドレスを取れないからです。

「アドレス指定可能」とは、ざっくり言えば「変数や構造体のフィールド、配列の要素など、メモリ上に場所が確定しているもの」です。対して、関数の戻り値や式の結果は一時的な値なので、アドレスを取れません。

var t T
f := t.Mp        // OK! t は変数なのでアドレスを取れる

f := makeT().Mp  // NG! 関数の戻り値はアドレスを取れない

// 回避策:いったん変数に入れる
tmp := makeT()
f := tmp.Mp      // OK!

インターフェースでも使える

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

var r io.Reader = someFile
f := r.Read      // func([]byte) (int, error) 型
n, err := f(buf) // r.Read(buf) と同じ

この場合、実際にどのメソッドが呼ばれるかは、インターフェースの動的な型によって決まります。メソッド値を作った時点の動的な型のメソッドが呼ばれる、と覚えておけば大丈夫です。

メソッド値の使いどころ

メソッド値は、特定のインスタンスの振る舞いを関数として渡したいときに便利です。

// コールバックを登録するAPIがあるとする
func onClick(handler func()) { ... }

type Button struct{ id int }
func (b *Button) handle() { fmt.Println("clicked:", b.id) }

btn := &Button{id: 42}
onClick(btn.handle)  // メソッド値! btn に束縛された関数を渡せる

もしメソッド値がなかったら、わざわざクロージャで包む必要があります。

// メソッド値を使わない場合
onClick(func() { btn.handle() })

// メソッド値を使う場合(シンプル!)
onClick(btn.handle)

イベントハンドラやコールバックの登録、ゴルーチンで実行する処理などでよく使われるパターンです。

おわりに 

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

よっしー
よっしー

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

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

コメント

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