A Tour of GoのMethodsを理解する

2022/05/06に公開約10,800字

対象者とこの記事を書いた背景

A Tour of GoのMethodsを読んでみたが、結局理解できなかった方向けの記事です。
僕は初めてGoを勉強して、ポインタって何?レシーバーって何?みたいな状態だったので、まったく理解出来ずにいました。
再度A Tour of GoのMethodsを一通り読んでみたので、自分の頭の中を整理しつつ、誰かに教えるようなつもりで記事を書こうと思った次第です。

Methods

対象のページ

https://go-tour-jp.appspot.com/methods/1

Goには、クラス( class )のしくみはありませんが、型にメソッド( method )を定義できます。
メソッドは、特別なレシーバ( receiver )引数を関数に取ります。
レシーバは、 func キーワードとメソッド名の間に自身の引数リストで表現します。

ここまでは分かりやすいかと思います。
他の言語ではクラスと呼ばれる オブジェクトを作る設計書 のようなものに対して、メソッドを定義することができます。しかし、Goの場合にはクラスの仕組みがないため、型にメソッドを定義することができるそうです。

構文は funcキーワードメソッド名の間自身の引数リスト で表現します。

type Person struct {
    Name string
}

// これがメソッド
func (p Person) Hello() string {
    return fmt.Sprintf("Hello %s", p.Name)
}

func main() {
    p := Person{Name: "Yamamoto"}
    fmt.Println(p.Hello()) // "Hello Yamamoto"
}

上の例では、Helloメソッドp という名前の Person型 のレシーバーを持っていることになります。

レシーバーとは、他の言語で言い換えると このクラスにこのメソッドを定義します と宣言しているような状態です。(どこにも詳しい説明が見つけられなかったので、定義を知っている方がいたら教えてください🙇‍♂️)

Goでいうと 型に対してメソッドを定義できる ので、レシーバーを宣言することにより この型に対してこのメソッドを定義します という意味になり、 Person がレシーバーとなります。そのため、 p.Name として構造体のフィールドを参照できるというわけです。

ちなみにこのレシーバーの宣言方法は 値レシーバー と呼ばれるものです。
もう一つレシーバーには ポインタレシーバー というものが出てくるので、あとで説明します。


Methods are functions

メソッドは、レシーバ引数を伴う関数であることから以下のようにも定義することができます。

type Person struct {
    Name string
}

// レシーバ引数を伴う関数を定義
func Hello(p Person) string {
    return fmt.Sprintf("Hello %s", p.Name)
}

func main() {
    p := Person{Name: "Yamamoto"}
    fmt.Println(Hello(p)) // "Hello Yamamoto"
}

ここはあまり難しくはないですね。


Pointer receivers

レシーバーはポインタレシーバーとして宣言することができます。
ポインタは メモリ上のアドレス情報 のことです。ポインタを操作することで、ポインタが指す値を直接変更することができます。

以上を踏まえてポインタレシーバーとは?を考えると、 「メソッドを使い、一度宣言した変数の値を変更したいときに使う」 ものであると想像できます。

例えば Yamamoto としている部分を Suzuki に変更したいとします。この場合にポインタレシーバーを使うことで、意図した挙動を取ることができます。

比較するためにまずは値レシーバを使ってみます。この場合には意図した挙動にはならないです。

type Person struct {
    Name string
}

func (p Person) Hello() string {
    return fmt.Sprintf("Hello %s", p.Name)
}

// 値レシーバで名前を変更しようとする
func (p Person) ChangeName(name string) {
    p.Name = name
}

func main() {
    p := Person{Name: "Yamamoto"}
    fmt.Println(p.Hello()) // Hello Yamamoto

    p.ChangeName("Suzuki")
    fmt.Println(p.Hello()) // Hello Yamamoto → Suzukiになっていない
}

次は値レシーバーの部分をポインタレシーバーにしてみます。

type Person struct {
    Name string
}

func (p Person) Hello() string {
    return fmt.Sprintf("Hello %s", p.Name)
}

// ポインタレシーバで名前を変更する
func (p *Person) ChangeName(name string) {
    p.Name = name
}

func main() {
    p := Person{Name: "Yamamoto"}
    fmt.Println(p.Hello()) // Hello Yamamoto

    p.ChangeName("Suzuki")
    fmt.Println(p.Hello()) // Hello Suzuki
}

ポインタレシーバーを使うことによって p.Nameを変更することができました。

値レシーバーの場合は値渡しとなりコピーを触っていることになるため、意図した挙動にはならず、
ポインタレシーバは参照渡しなので、上書きすることが可能となっています。


Pointers and functions

https://go-tour-jp.appspot.com/methods/5

ここで、 Abs と Scale メソッドは関数として書きなおしてあります。
再度、line 16から * を消してください。 なぜ振る舞いが変わったのかわかりますか? コンパイルするために、さらに何が必要でしょうか。
(よくわからなくても、次のページに行きましょう)

A Tour of Goを最初からなぞってきた段階では、この問いに答えることができなかったです...だから(よくわからなくても、次のページに行きましょう)と書いてあるんですかね。


Methods and pointer indirection

https://go-tour-jp.appspot.com/methods/6
このセクションの説明では個人的に分かりにくかったので、言いたかったであろう内容を自分の言葉で説明してみます。

下の2つの呼び出しを比べると、ポインタを引数に取る関数は、ポインタを渡す必要があることに気がつくでしょう:

var v Vertex
ScaleFunc(v, 5)  // Compile error!
ScaleFunc(&v, 5) // OK

まずはここの説明です。
ポインタを引数に取る関数は、ポインタを渡す必要がある ですが、
Methods are functions で説明した メソッドは、レシーバ引数を伴う関数である という特徴から、
以下コードのようにメソッドを通常の関数のように書くことができる方法についての説明になっています。

type Person struct {
    Name string
}

// ポインタを引数にとる関数を定義
func Hello(p *Person) string {
    return fmt.Sprintf("Hello %s", p.Name)
}

func main() {
    p := Person{Name: "Yamamoto"}
    fmt.Println(Hello(&p)) // "Hello Yamamoto"
}

このように、ポインタを引数にとる関数を使う場合に、引数の型が *Person になっていることから ポインタを渡す必要がある というのは理解しやすいと思います。
*Person はポインタ型なので、 Hello(&p) とすることで、 p のポインタを渡していることになります。

ここでmain関数の fmt.Println(Hello(&p))fmt.Println(Hello(p)) に変更してみる(ポインタではなく値を渡す)と、コンパイルエラーになります。

次の段落の説明に移ります。

メソッドがポインタレシーバである場合、呼び出し時に、変数、または、ポインタのいずれかのレシーバとして取ることができます:

var v Vertex
v.Scale(5)  // OK
p := &v
p.Scale(10) // OK

v.Scale(5) のステートメントでは、 v は変数であり、ポインタではありません。 メソッドでポインタレシーバが自動的に呼びだされます。 Scale メソッドはポインタレシーバを持つ場合、Goは利便性のため、 v.Scale(5) のステートメントを (&v).Scale(5) として解釈します。

これは、先ほどのような

// ポインタを引数にとる関数を定義
func Hello(p *Person) string {
    return fmt.Sprintf("Hello %s", p.Name)
}

といった書き方ではなく、

// ポインタレシーバでメソッドを定義
func (p *Person) Hello() string {
    return fmt.Sprintf("Hello %s", p.Name)
}

ポインタレシーバーでメソッドを定義したときの挙動の説明になります。
再度引用になりますが、

メソッドがポインタレシーバである場合、呼び出し時に、変数、または、ポインタのいずれかのレシーバとして取ることができます:

という説明は、 「メソッドがポインタレシーバである場合、呼び出し時に変数またはポインタのどちらからでも呼び出せる」 ということを言っています。
つまり

type Person struct {
    Name string
}

// ポインタレシーバでメソッドを定義
func (p *Person) Hello() string {
    return fmt.Sprintf("Hello %s", p.Name)
}

func main() {
    p := Person{Name: "Yamamoto"}
    fmt.Println(p.Hello()) // 変数から呼び出している
  
    // ポインタからの呼び出し方法1
    p2 := Person{Name: "Suzuki"}
    fmt.Println((&p2).Hello()) // ポインタから呼び出している

    // ポインタからの呼び出し方法2
    p3 := &Person{Name: "Sato"}
    fmt.Println(p3.Hello()) // ポインタから呼び出している
}

このように、変数またはポインタから Hello関数 を呼び出すことができます。

少し話が戻りますが、ポインタを引数に取る関数の実行方法として ポインタを引数に取る関数は、ポインタを渡す必要がある という部分で、 ポインタを渡す必要がある ことは説明しましたし、理解しやすかったと思います。ポインタ型の引数に対して、その型のポインタを渡せば良かったので。

しかし、今回の場合は 変数 または ポインタ から実行することができます。なぜかというと

メソッドでポインタレシーバが自動的に呼びだされます。 メソッドはポインタレシーバを持つ場合、Goは利便性のため、 v.Scale(5) のステートメントを (&v).Scale(5) として解釈します。

とあります。つまり、変数からメソッドを呼び出した場合、Goが良しなに変数のポインタから呼び出すようにしているということです。
コードで表すと

// ポインタレシーバでメソッドを定義
func (p *Person) Hello() string {
    return fmt.Sprintf("Hello %s", p.Name)
}

func main() {
    p := Person{Name: "Yamamoto"}
    fmt.Println(p.Hello()) // 変数pからメソッド呼び出し
    // 変数pではなく、実際には (&p).Hello() とポインタで実行されている
}

そのため、 メソッドがポインタレシーバである場合、呼び出し時に変数またはポインタのどちらからでも呼び出せる ということになります。

このセクションをまとめると

  1. ポインタを引数に取る関数は ポインタ を渡す必要がある
// ポインタを引数にとる関数を定義
func Hello(p *Person) string {
    return fmt.Sprintf("Hello %s", p.Name)
}

func main() {
    p := Person{Name: "Yamamoto"}
    fmt.Println(Hello(&p)) // ポインタを渡す
}
  1. メソッドがポインタレシーバである場合は 変数 または ポインタ からメソッドを呼び出せる
    • Goがよしなに変数のポインタから実行するようにしてくれるため
// ポインタレシーバでメソッドを定義
func (p *Person) Hello() string {
    return fmt.Sprintf("Hello %s", p.Name)
}

func main() {
    p := Person{Name: "Yamamoto"}
    fmt.Println(p.Hello()) // 変数から呼び出している (&p).Hello() で実行される
    
    // ポインタからの呼び出し方法1
    p2 := Person{Name: "Suzuki"}
    fmt.Println((&p2).Hello()) // ポインタから呼び出している

    // ポインタからの呼び出し方法2
    p3 := &Person{Name: "Sato"}
    fmt.Println(p3.Hello()) // ポインタから呼び出している
}

Methods and pointer indirection (2)

次は、変数の引数を取る関数の場合です。

変数の引数を取る関数は、特定の型の変数を取る必要があります:

var v Vertex
fmt.Println(AbsFunc(v))  // OK
fmt.Println(AbsFunc(&v)) // Compile error!

これは簡単ですね。変数を引数に取るのですから、素直に関数の実行時に 変数 を渡せば良いです。

// 変数を引数にとる関数
func Hello(p Person) string {
    return fmt.Sprintf("Hello %s", p.Name)
}

func main() {
    p := Person{Name: "Yamamoto"}
    fmt.Println(Hello(p)) // 変数を渡す
}

次に

メソッドが変数レシーバである場合、呼び出し時に、変数、または、ポインタのいずれかのレシーバとして取ることができます:

var v Vertex
fmt.Println(v.Abs()) // OK
p := &v
fmt.Println(p.Abs()) // OK

の説明ですが、 「メソッドが値レシーバーである場合、呼び出し時に変数またはポインタのどちらからでも呼び出せる」 ということを言っています。
先ほども同じ説明を見た気がしますね。ポインタレシーバーの説明と同じことを言っています。
つまり、以下のようにメソッドを呼び出すことができます。

func (p Person) Hello() string {
    return fmt.Sprintf("Hello %s", p.Name)
}

func main() {
    p := Person{Name: "Yamamoto"}
    fmt.Println(p.Hello()) // 変数から呼び出せる
    
    p2 := &Person{Name: "Yamamoto"}
    fmt.Println(p.Hello()) // ポインタから呼び出せる
}

つまりポインタからメソッドを呼び出した場合、Goが良しなにポインタの実体(変数)から呼び出すようにしているということです。
コードで表すと

func (p Person) Hello() string {
    return fmt.Sprintf("Hello %s", p.Name)
}

func main() {   
    p := &Person{Name: "Yamamoto"}
    fmt.Println((*p).Hello()) // ポインタの実体からメソッドを呼び出し
}

ということです。
そのため、 メソッドが値レシーバである場合、呼び出し時に 変数またはポインタのどちらからでも呼び出せる ということになります。

このセクションをまとめると

  1. 変数を引数に取る関数は 変数 を渡す必要がある
// 変数を引数にとる関数を定義
func Hello(p Person) string {
    return fmt.Sprintf("Hello %s", p.Name)
}

func main() {
    p := Person{Name: "Yamamoto"}
    fmt.Println(Hello(p)) // 変数を渡す
}
  1. メソッドが値レシーバである場合は 変数 または ポインタ からメソッドを呼び出せる
    • Goがよしなにポインタの実体(変数)から実行するようにしてくれるため
// 値レシーバでメソッドを定義
func (p Person) Hello() string {
    return fmt.Sprintf("Hello %s", p.Name)
}

func main() {
    p := Person{Name: "Yamamoto"}
    fmt.Println(p.Hello()) // 変数から呼び出している
    
    // ポインタからの呼び出し方法
    p2 := &Person{Name: "Suzuki"}
    fmt.Println(p2.Hello()) // ポインタから呼び出している (*p2).Hello() と実行されるため
}

Methods and pointer indirectionMethods and pointer indirection(2) の説明が長くなりましたが、つまりは

  1. 関数の引数にレシーバーを伴う場合は、 値レシーバー(変数レシーバー)であれば変数 を、 ポインタレシーバーであればポインター を渡す必要がある。
  2. メソッドが値レシーバーでもポインタレシーバーでも、 変数 または ポインター からメソッドを呼べる

ということです。
関数の引数にレシーバーを伴う場合 よりも素直にメソッドを定義した方がGoがよしなにやってくれるので楽かもしれません。何より余計な引数を渡す必要がないのでコードの見通しが良いと思いました。


Choosing a value or pointer receiver

タイトルを直訳すると「値またはポインターのレシーバーを選択する」となります。
続きの解説ではポインタレシーバーを使う前提で話が進んでいるので、値レシーバーを積極的に使うことはあまりないのでしょうか...
この疑問に関しては次の解説が答えになっているのかもしれません。

ふたつに、メソッドの呼び出し毎に変数のコピーを避けるためです。 例えば、レシーバが大きな構造体である場合に効率的です。

また以下の

一般的には、値レシーバ、または、ポインタレシーバのどちらかですべてのメソッドを与え、混在させるべきではありません。

と公式が言っていることを考えると、値を変更したいときに値レシーバでは値を変更できないため、ポインタレシーバーを使うことを推奨しているということでしょうか...

このあたりは詳しい方が見ていたらぜび教えていただきたいです🙇‍♂️

ふたたびPointers and functions へ

https://go-tour-jp.appspot.com/methods/5

ここで、 Abs と Scale メソッドは関数として書きなおしてあります。
再度、line 16から * を消してください。 なぜ振る舞いが変わったのかわかりますか? コンパイルするために、さらに何が必要でしょうか。
(よくわからなくても、次のページに行きましょう)

ここまで理解できれば、この振る舞いが変わった理由がわかるかと思います。
コードを引用します。

package main

import (
    "fmt"
    "math"
)

type Vertex struct {
    X, Y float64
}

func Abs(v Vertex) float64 {
    return math.Sqrt(v.X*v.X + v.Y*v.Y)
}

func Scale(v Vertex, f float64) {
    v.X = v.X * f
    v.Y = v.Y * f
}

func main() {
    v := Vertex{3, 4}
    Scale(&v, 10)
    fmt.Println(Abs(v))
}

16行目のScale関数の第一引数であるvをポインタレシーバーから値レシーバーに変更しました。するとコンパイルエラーになります。
引数の値の型が変数になっており、Scale関数の実行時にポインタを渡しているためコンパイルエラーになってしまいます。

これをコンパイルするためには、Scale関数の実行時に変数を渡してあげる必要があります。

func main() {
    v := Vertex{3, 4}
    Scale(v, 10) // ここをポインタから変数に変更
    fmt.Println(Abs(v))
}

するとコンパイルすることができます。

しかし、このScale関数は値を10倍することを目的としていたはずなのに、コンパイル後の結果は5となっており、Scale関数の役目を果たすことができませんでした。
このように、フィールドの値を変更したい場合には、ポインタレシーバーを使う必要があるということです。

そもそもメソッドを関数にすることにメリットはあるのか?

この記事を書いていてふと思いました。メソッドを関数化するメリットはあるのか?と。
メソッドを関数化した場合にはポインタを渡すのか、変数を渡すのかを意識しなければいけませんし、間違っていればコンパイルエラーになります。
メソッドであれば値レシーバーの場合でも、ポインタレシーバーの場合でもGoがよしなにしてくれます。
また、関数にした場合には引数が増えてしまうことも考えるとできればメソッドで統一するのが良いと思ったのですが、
これに関しても詳しい方がいらしたら教えていただきたいです🙇‍♂️


まとめ

これでMethodsに関する解説を終わりたいと思います。
何言ってんの...?理解できない...みたいな方の手助けになれば嬉しいです。

また、日本語的に変な部分や公式と異なる解釈をしてしまっている部分などありましたらコメントいただけると大変嬉しいです。

Discussion

ログインするとコメントできます