Goでのメソッドの定義

前書き

Functionsを使用すると、ロジックを繰り返し可能なプロシージャに編成して、実行するたびに異なる引数を使用できます。 関数を定義する過程で、同じデータに対して毎回複数の関数が動作することがよくあります。 Goはこのパターンを認識し、methodsと呼ばれる特定のタイプのインスタンスを操作することを目的とした、methodsと呼ばれる特別な関数を定義できます。 型にメソッドを追加すると、データが何であるかだけでなく、そのデータの使用方法も伝達できます。

メソッドの定義

メソッドを定義するための構文は、関数を定義するための構文に似ています。 唯一の違いは、メソッドのレシーバーを指定するために、funcキーワードの後に​​パラメーターを追加することです。 レシーバーは、メソッドを定義したい型の宣言です。 次の例では、構造体型のメソッドを定義しています。

package main

import "fmt"

type Creature struct {
    Name     string
    Greeting string
}

func (c Creature) Greet() {
    fmt.Printf("%s says %s", c.Name, c.Greeting)
}

func main() {
    sammy := Creature{
        Name:     "Sammy",
        Greeting: "Hello!",
    }
    Creature.Greet(sammy)
}

このコードを実行すると、出力は次のようになります。

OutputSammy says Hello!

NameおよびGreetingstringフィールドを持つCreatureという構造体を作成しました。 このCreatureには、Greetという単一のメソッドが定義されています。 レシーバー宣言内で、Creatureのインスタンスを変数cに割り当て、fmt.PrintfでグリーティングメッセージをアセンブルするときにCreatureのフィールドを参照できるようにしました。 。

他の言語では、メソッド呼び出しの受信者は通常、キーワードによって参照されます(例: thisまたはself)。 Goでは、レシーバーは他の変数と同様に変数であると見なされるため、好きな名前を自由に付けることができます。 コミュニティがこのパラメーターに推奨するスタイルは、レシーバータイプの最初の文字の小文字バージョンです。 この例では、レシーバータイプがCreatureであったため、cを使用しました。

mainの本体内に、Creatureのインスタンスを作成し、そのNameフィールドとGreetingフィールドに値を指定しました。 ここでは、型の名前とメソッドの名前を.と結合し、最初の引数としてCreatureのインスタンスを指定することにより、Greetメソッドを呼び出しました。

Goは、次の例に示すように、構造体のインスタンスでメソッドを呼び出す、より便利な別の方法を提供します。

package main

import "fmt"

type Creature struct {
    Name     string
    Greeting string
}

func (c Creature) Greet() {
    fmt.Printf("%s says %s", c.Name, c.Greeting)
}

func main() {
    sammy := Creature{
        Name:     "Sammy",
        Greeting: "Hello!",
    }
    sammy.Greet()
}

これを実行すると、出力は前の例と同じになります。

OutputSammy says Hello!

この例は前の例と同じですが、今回はdot notationを使用して、sammy変数に格納されているCreatureをレシーバーとして使用してGreetメソッドを呼び出しました。 これは、最初の例の関数呼び出しの簡略表記です。 標準ライブラリとGoコミュニティはこのスタイルを非常に好むため、前に示した関数呼び出しスタイルはほとんど見られません。

次の例は、ドット表記がより一般的な理由の1つを示しています。

package main

import "fmt"

type Creature struct {
    Name     string
    Greeting string
}

func (c Creature) Greet() Creature {
    fmt.Printf("%s says %s!\n", c.Name, c.Greeting)
    return c
}

func (c Creature) SayGoodbye(name string) {
    fmt.Println("Farewell", name, "!")
}

func main() {
    sammy := Creature{
        Name:     "Sammy",
        Greeting: "Hello!",
    }
    sammy.Greet().SayGoodbye("gophers")

    Creature.SayGoodbye(Creature.Greet(sammy), "gophers")
}

このコードを実行すると、出力は次のようになります。

OutputSammy says Hello!!
Farewell gophers !
Sammy says Hello!!
Farewell gophers !

以前の例を変更してSayGoodbyeという別のメソッドを導入し、Greetを変更してCreatureを返すようにして、そのインスタンスでさらにメソッドを呼び出せるようにしました。 mainの本体では、最初にドット表記を使用し、次に関数呼び出しスタイルを使用して、sammy変数のメソッドGreetおよびSayGoodbyeを呼び出します。

どちらのスタイルでも同じ結果が出力されますが、ドット表記を使用した例の方がはるかに読みやすくなっています。 ドットのチェーンは、メソッドが呼び出される順序も示します。機能スタイルはこの順序を逆にします。 SayGoodbye呼び出しにパラメーターを追加すると、メソッド呼び出しの順序がさらにあいまいになります。 ドット表記の明快さは、標準ライブラリとGoエコシステム全体で見つかるサードパーティパッケージの両方で、Goでメソッドを呼び出すための好ましいスタイルである理由です。

ある値で動作する関数を定義するのではなく、型のメソッドを定義することは、Goプログラミング言語にとって特別な意味があります。 メソッドは、インターフェースの背後にある中心概念です。

インターフェース

Goで任意の型にメソッドを定義すると、そのメソッドが型のmethod setに追加されます。 メソッドセットは、メソッドとしてそのタイプに関連付けられた関数のコレクションであり、Goコンパイラによって使用されて、あるタイプをインターフェイスタイプの変数に割り当てることができるかどうかを判断します。 interface typeは、型がそれらのメソッドの実装を提供することを保証するためにコンパイラーによって使用されるメソッドの仕様です。 インターフェイスの定義で見つかったものと同じ名前、同じパラメータ、同じ戻り値を持つメソッドを持つすべての型は、そのインターフェイスのimplementと呼ばれ、そのインターフェイスの型を持つ変数に割り当てることができます。 以下は、標準ライブラリからのfmt.Stringerインターフェースの定義です。

type Stringer interface {
  String() string
}

fmt.Stringerインターフェースを実装するタイプの場合、stringを返すString()メソッドを提供する必要があります。 このインターフェイスを実装すると、タイプのインスタンスをfmtパッケージで定義された関数に渡すときに、タイプを希望どおりに正確に出力できます(「プリティプリント」と呼ばれることもあります)。 次の例では、このインターフェイスを実装する型を定義しています。

package main

import (
    "fmt"
    "strings"
)

type Ocean struct {
    Creatures []string
}

func (o Ocean) String() string {
    return strings.Join(o.Creatures, ", ")
}

func log(header string, s fmt.Stringer) {
    fmt.Println(header, ":", s)
}

func main() {
    o := Ocean{
        Creatures: []string{
            "sea urchin",
            "lobster",
            "shark",
        },
    }
    log("ocean contains", o)
}

コードを実行すると、次の出力が表示されます。

Outputocean contains : sea urchin, lobster, shark

この例では、Oceanという新しい構造体タイプを定義しています。 Oceanimplementインターフェイスと呼ばれます。これはOceanStringと呼ばれるメソッドを定義しているためです。このメソッドはパラメーターを受け取らず、stringを返します。 mainで、新しいOceanを定義し、それをlog関数に渡しました。この関数は、最初にstringを出力し、次にfmt.Stringerを実装するものを出力します。 s。 Oceanfmt.Stringerによって要求されたすべてのメソッドを実装するため、Goコンパイラーではここでoを渡すことができます。 log内では、fmt.Printlnを使用します。これは、パラメーターの1つとしてfmt.Stringerが検出されると、OceanStringメソッドを呼び出します。

OceanString()メソッドを提供しなかった場合、logメソッドが引数としてfmt.Stringerを要求するため、Goはコンパイルエラーを生成します。 エラーは以下のようになります。

Outputsrc/e4/main.go:24:6: cannot use o (type Ocean) as type fmt.Stringer in argument to log:
        Ocean does not implement fmt.Stringer (missing String method)

Goはまた、提供されたString()メソッドがfmt.Stringerインターフェースによって要求されたものと正確に一致することを確認します。 そうでない場合は、次のようなエラーが生成されます。

Outputsrc/e4/main.go:26:6: cannot use o (type Ocean) as type fmt.Stringer in argument to log:
        Ocean does not implement fmt.Stringer (wrong type for String method)
                have String()
                want String() string

これまでの例では、値レシーバーでメソッドを定義しました。 つまり、メソッドの関数呼び出しを使用する場合、メソッドが定義されたタイプを参照する最初のパラメーターは、pointerではなく、そのタイプの値になります。 そのため、受け取った値はデータのコピーであるため、メソッドに提供されたインスタンスに対して行った変更は、メソッドの実行が完了すると破棄されます。 ポインターレシーバーでメソッドを型に定義することもできます。

ポインター受信機

ポインターレシーバーでメソッドを定義するための構文は、値レシーバーでメソッドを定義するのとほぼ同じです。 違いは、レシーバー宣言の型の名前の前にアスタリスク(*)を付けることです。 次の例では、ポインターレシーバーのメソッドを型に定義しています。

package main

import "fmt"

type Boat struct {
    Name string

    occupants []string
}

func (b *Boat) AddOccupant(name string) *Boat {
    b.occupants = append(b.occupants, name)
    return b
}

func (b Boat) Manifest() {
    fmt.Println("The", b.Name, "has the following occupants:")
    for _, n := range b.occupants {
        fmt.Println("\t", n)
    }
}

func main() {
    b := &Boat{
        Name: "S.S. DigitalOcean",
    }

    b.AddOccupant("Sammy the Shark")
    b.AddOccupant("Larry the Lobster")

    b.Manifest()
}

この例を実行すると、次の出力が表示されます。

OutputThe S.S. DigitalOcean has the following occupants:
     Sammy the Shark
     Larry the Lobster

この例では、Nameoccupantsを使用してBoatタイプを定義しました。 他のパッケージのコードで、AddOccupantメソッドを使用して占有者のみを追加するように強制したいので、フィールド名の最初の文字を小文字にすることで、occupantsフィールドをエクスポートしないようにしました。 また、AddOccupantを呼び出すと、Boatのインスタンスが変更されることを確認する必要があります。そのため、ポインターレシーバーでAddOccupantを定義しました。 ポインターは、そのタイプのコピーではなく、そのタイプの特定のインスタンスへの参照として機能します。 AddOccupantBoatへのポインターを使用して呼び出されることを知っていると、変更が持続することが保証されます。

main内で、Boat*Boat)へのポインターを保持する新しい変数bを定義します。 このインスタンスでAddOccupantメソッドを2回呼び出して、2人の乗客を追加します。 Manifestメソッドは、Boat値で定義されます。これは、その定義では、レシーバーが(b Boat)として指定されているためです。 mainでは、Goがポインターを自動的に逆参照してBoat値を取得できるため、Manifestを呼び出すことができます。 ここでのb.Manifest()は、(*b).Manifest()と同等です。

インターフェイスタイプである変数に値を割り当てようとする場合、メソッドがポインターレシーバーまたは値レシーバーのどちらで定義されているかは重要な意味を持ちます。

ポインターレシーバーとインターフェイス

インターフェイス型の変数に値を割り当てると、Goコンパイラは割り当てられている型のメソッドセットを調べて、インターフェイスが期待するメソッドがあることを確認します。 ポインターを受け取るメソッドは、値を受け取るメソッドではできないレシーバーを変更できるため、ポインターレシーバーと値レシーバーのメソッドセットは異なります。

次の例は、2つのメソッドの定義を示しています。1つは型のポインターレシーバーで、もう1つはその値レシーバーです。 ただし、この例でも定義されているインターフェイスを満たすことができるのは、ポインターレシーバーのみです。

package main

import "fmt"

type Submersible interface {
    Dive()
}

type Shark struct {
    Name string

    isUnderwater bool
}

func (s Shark) String() string {
    if s.isUnderwater {
        return fmt.Sprintf("%s is underwater", s.Name)
    }
    return fmt.Sprintf("%s is on the surface", s.Name)
}

func (s *Shark) Dive() {
    s.isUnderwater = true
}

func submerge(s Submersible) {
    s.Dive()
}

func main() {
    s := &Shark{
        Name: "Sammy",
    }

    fmt.Println(s)

    submerge(s)

    fmt.Println(s)
}

コードを実行すると、次の出力が表示されます。

OutputSammy is on the surface
Sammy is underwater

この例では、Dive()メソッドを持つ型を予期するSubmersibleというインターフェイスを定義しました。 次に、NameフィールドとisUnderwaterメソッドを使用してSharkタイプを定義し、Sharkの状態を追跡しました。 ポインターレシーバーのDive()メソッドをSharkに定義し、isUnderwatertrueに変更しました。 また、値レシーバーのString()メソッドを定義して、fmt.Printlnで受け入れられるfmt.Stringerインターフェイスを使用して、fmt.Printlnを使用してSharkの状態をクリーンに出力できるようにしました。先ほど見たs。 また、Submersibleパラメータを受け取る関数submergeを使用しました。

*SharkではなくSubmersibleインターフェイスを使用すると、submerge関数は型によって提供される動作のみに依存できます。 これにより、SubmarineWhale、またはその他の将来の水生生物に対して新しいsubmerge関数を作成する必要がなくなるため、submerge関数の再利用性が向上します。まだ考えていません。 Dive()メソッドを定義している限り、submerge関数で使用できます。

main内で、Sharkへのポインターである変数sを定義し、すぐにsfmt.Printlnで出力しました。 これは、出力の最初の部分であるSammy is on the surfaceを示しています。 ssubmergeに渡し、sを引数としてfmt.Printlnを再度呼び出して、出力の2番目の部分であるSammy is underwaterを確認しました。

s*SharkではなくSharkに変更した場合、Goコンパイラは次のエラーを生成します。

Outputcannot use s (type Shark) as type Submersible in argument to submerge:
    Shark does not implement Submersible (Dive method has pointer receiver)

Goコンパイラは、SharkにはDiveメソッドがあり、ポインタレシーバーで定義されているだけであることを教えてくれます。 独自のコードでこのメッセージが表示された場合、修正は、値の型が割り当てられている変数の前に&演算子を使用して、インターフェイスの型へのポインターを渡すことです。

結論

Goでメソッドを宣言することは、最終的には、さまざまなタイプの変数を受け取る関数を定義することと同じです。 working with pointersの同じルールが適用されます。 Goは、この非常に一般的な関数定義にいくつかの便利さを提供し、これらをインターフェイスタイプによって推論できるメソッドのセットに収集します。 メソッドを効果的に使用すると、コード内のインターフェイスを操作してテスト容易性を向上させ、コードの将来の読者のために組織を改善できます。

Goプログラミング言語全般について詳しく知りたい場合は、How To Code in Go seriesを確認してください。