いろいろ備忘録日記

主に .NET とか Go とか Flutter とか Python絡みのメモを公開しています。

Goメモ-25 (メソッド, Methods, Tour of Go)

概要

Tour of Go の - Methods についてのサンプル。

tour.golang.org

Goには、クラスの仕組みがありません。C#やJavaやPythonなどのように class を定義することが出来ないです。

可能なのは、struct で構造体を定義すること(interfaceもありますが、それは別の回で)です。

構造体は、データ構造を定義するだけです。

type Hoge struct{
    X, Y int
}

上の構造体はXとYというフィールドを持っています。属性はあるけど、操作はありません。

操作する処理を作る場合は関数を定義します。

func Sum(h Hoge) int {
    return h.X + h.Y
}

オブジェクト指向な言語の場合、上記のような処理はメソッドにしてしまいますね。

class Hoge:
    def __init__(self, x=0, y=0):
        self.x = x
        self.y = y
    def sum(self):
        return self.x + self.y

h = Hoge()
h.x = 10
h.y = 20
print(h.sum())

同じようなイメージで、ある構造体に操作を付けたい場合にGoにはメソッドという仕組みがあります。

先程の関数

func Sum(h Hoge) int {
    return h.X + h.Y
}

は、ただの関数です。これをHoge構造体のメソッドにするには以下のようにします。

func (h Hoge) Sum() int {
    return h.X + h.Y
}

どの言語でも、メソッドにはレシーバが必要になります。

Pythonの場合は言語仕様として明示的に self というレシーバを引数で受け取るようにしないといけません。(selfの部分はどんな名前でもいいのですが、慣習でselfが多い)

C# の場合、引数にはいませんが、暗黙的にレシーバは渡されていて this という名前で利用できます。

Go の場合は、Pythonに近くて関数名の前にレシーバを書きます。上の関数でいう (h Hoge) の部分です。

レシーバが指定されているので、オブジェクト.メソッド() という形で呼び出しができます。

レシーバが分からないとどのオブジェクトに対して処理すればいいのか分かりません。

なので、メソッドではない普通の関数の場合は関数に対して「このデータを処理して」と引数で渡します。

メソッドの場合、レシーバーを指定するので、処理してほしいデータにドットで関数呼び出しを付けます。

// 関数の場合
Sum(h)

// メソッドの場合
h.Sum()

関数内部で、対象となるデータ(構造体)の情報を取得したり操作したりするには、レシーバにアクセスします。

package main

import "fmt"

type Hoge struct {
    X, Y int
}

func (h Hoge) Sum() int {
    return h.X + h.Y
}

func main() {
    h := Hoge{
        X: 10,
        Y: 20,
    }
 
    fmt.Println(h.Sum())
}

こんな感じになります。

レシーバの種類 (値レシーバとポインタレシーバ)

Goにはレシーバのタイプが2つあります。

一つは値レシーバ。上記サンプルで出てきていたメソッドは値レシーバーを使っています。

func (h Hoge) Sum() int {
    return h.X + h.Y
}

もう一つは、ポインタレシーバ。上記のメソッドをポインタレシーバにすると以下になります。

func (h *Hoge) Sum() int {
}

レシーバの部分が (h *Hoge) に変わっています。レシーバをポインタで受け取るからポインタレシーバです。

h.Sum() としたときに、値レシーバを持つメソッドの場合は h がコピーされてレシーバとしてメソッドに渡ります。

ポインタレシーバを持つメソッドの場合は、 h のポインタがコピーされてレシーバとしてメソッドに渡ります。

使い分けですが、個人的には大抵以下のようにしています。

  • レシーバの値を取得のみして処理することが可能なメソッドは 値レシーバ
  • メソッド内でレシーバの属性を変更することが必要な場合は ポインタレシーバ

私自身でいうとあまり値レシーバは使っていません。ほとんどポインタレシーバです。

(C#やJavaやPython使っているので、そっちの方がしっくり来るからかもしれませんが)

例えば、Hoge構造体の属性を変更するための Change って名前のメソッドを定義した場合に

func (h Hoge) Change(x, y int) {
    h.X, h.Y = x, y
}

と値レシーバのメソッドにしていると

h.X = 10
h.Y = 20

h.Change(98, 99)

fmt.Println(h.X, h.Y)

で、10 20 と表示されます。値レシーバなので、変わりません。

func (h *Hoge) Change(x, y int) {
    h.X, h.Y = x, y
}

とポインタレシーバのメソッドにしていると

h.X = 10
h.Y = 20

h.Change(98, 99)

fmt.Println(h.X, h.Y)

で、98 99 と表示されます。

サンプル

package tutorial

import "fmt"

type (
    point struct {
        x, y int
    }

    // int の 拡張型。C#などの拡張メソッドな感じを実現したい場合は
    // このように既存型の別名をつけて、それにメソッドを付与する。
    myInt int
)

// point 型の コンストラクタ 関数です
func newPoint(x, y int) *point {
    return &point{x, y}
}

// 自身の状態を指定された値に変更します
func (p *point) Change(x, y int) {
    p.x, p.y = x, y
}

// fmt.Stringer インターフェースの実装です
func (p *point) String() string {
    return fmt.Sprintf("X:%v\tY:%v", p.x, p.y)
}

func (i *myInt) Double() myInt {
    return (*i) * 2
}

func (i *myInt) ToInt() int {
    return int(*i)
}

func (i *myInt) String() string {
    val := int(*i)
    return fmt.Sprintf("myInt: %v", val)
}

// Method は、 Tour of Go - Methods (https://tour.golang.org/methods/1) の サンプルです。
func Method() error {
    // ------------------------------------------------------------
    // Go言語のメソッド
    // Go言語には、クラスの仕組みは無いが、型にメソッドを定義することができる。
    // 感じ的には、Pythonに似ていて、レシーバを明示的に定義する必要がある。
    // レシーバは、funcキーワードと関数名の間に定義する。
    // レシーバの型を、実体にするかポインタにするかで挙動が変わることがある。
    // 通常は、レシーバが持つ状態を変化させることが多くなるため
    // ポインタレシーバにすることが多い。
    //
    // ポインタレシーバにしている場合、普通であれば、その型のポインタの場合のみ
    // 利用できるメソッドという意味になるが、Go言語では利便性のために
    // 変数の状態で、ポインタレシーバを要求するメソッドを呼び出した場合でも
    // メソッド側でポインタレシーバが自動的に呼び出されるようになっている。
    // つまり、呼び出そうとしているメソッドがポインタレシーバを要求しているかどうかは
    // 気にしなくて良くなっている.
    //   内部的には、 変数p がポインタレシーバを要求するメソッドを呼び出した場合に
    //   (&p).xxxx() として解釈される。
    //
    // 同じ理屈が変数レシーバの場合にも適用される。
    // つまり、ポインタの状態で変数レシーバを要求するメソッドを呼び出した場合
    // 内部的に (*p).xxxx() と解釈される。
    //
    // ポインタレシーバが使われる大きな理由は以下の2つ。
    // 1. メソッドがレシーバが指す先の変数を変更するため
    // 2. メソッド呼び出しの際に変数のコピーを避けるため。
    //    変数レシーバの場合は呼び出しの度に変数のコピーが発生するため
    //    巨大な構造体の場合にオーバーヘッドが大きい。
    //    ポインタレシーバの場合は、ポインタをコピーするだけとなる。
    //
    // 一般的には、変数レシーバ、または、ポインタレシーバのどちらかで
    // 全てのメソッドを定義するべきあり、混在させるべきではない。
    // 混在させている場合、インターフェースを利用した際にちょっと面倒になるため。
    // ------------------------------------------------------------
    var (
        p *point
        i myInt
    )

    p = newPoint(10, 20)
    fmt.Println(p) // 内部で fmt.Stringer インターフェースのメソッドが呼ばれる

    p.Change(30, 40)
    fmt.Println(p)

    i = 100
    fmt.Println(i)

    i = i.Double() // 変数の状態で呼び出しているが、Go言語側で (&i).Double() と解釈される.
    fmt.Println(i)

    i2 := i.ToInt()
    fmt.Printf("i[type:%T val:%v] i2[type:%T val:%v]\n", i, i, i2, i2)

    return nil
}

try-golang/tutorial_gotour_18_method.go at master · devlights/try-golang · GitHub

実行すると以下な感じ。

[Name] "tutorial_gotour_method"
X:10    Y:20
X:30    Y:40
100
200
i[type:tutorial.myInt val:200] i2[type:int val:200]

過去の記事については、以下のページからご参照下さい。

  • いろいろ備忘録日記まとめ

devlights.github.io

サンプルコードは、以下の場所で公開しています。

  • いろいろ備忘録日記サンプルソース置き場

github.com

github.com

github.com