Goでのメソッドの定義

Go

前書き

https://www.digitalocean.com/community/tutorials/how-to-define-and-call-functions-in-go [関数]を使用すると、実行するたびに異なる引数を使用できる繰り返し可能なプロシージャにロジックを編成できます。 関数を定義する過程で、同じデータに対して毎回複数の関数が動作することがよくあります。 Goはこのパターンを認識し、_methods_と呼ばれる特別な関数を定義できます。その目的は、_receiver_と呼ばれる特定のタイプのインスタンスを操作することです。 型にメソッドを追加すると、データが何であるかだけでなく、そのデータがどのように使用されるべきかも伝えることができます。

メソッドの定義

メソッドを定義するための構文は、関数を定義するための構文に似ています。 唯一の違いは、メソッドの受信者を指定するための `+ 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`と + Greeting`の + string`フィールドを持つ + 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!",
   }

}

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

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 main() {
   sammy := Creature{
       Name:     "Sammy",
       Greeting: "Hello!",
   }



}

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

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_は、型がそれらのメソッドの実装を提供することを保証するためにコンパイラが使用するメソッドの仕様です。 インターフェースの定義にあるものと同じ名前、同じパラメーター、および同じ戻り値を持つメソッドを持つ型は、そのインターフェースを「実装」し、そのインターフェースの型を持つ変数に割り当てることができます。 以下は標準ライブラリの `+ 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 」という新しい構造体型を定義しています。 「 Ocean 」は「 fmt.Stringer 」インターフェースを実装するという。「 Ocean 」は「 String 」と呼ばれるメソッドを定義するもので、「 String 」はパラメーターをとらず、「 string 」を返す。 ` main `で、新しい ` Ocean `を定義し、それを ` log `関数に渡しました。この関数は、最初に ` string `を出力してから、 ` fmt.Stringer `を実装するものを取得します。 Goコンパイラでは、「 Ocean 」が「 fmt.Stringer 」によって要求されたすべてのメソッドを実装するため、ここで「 o 」を渡すことができます。 ` log `内では、パラメーターの1つとして ` fmt.Stringer`に遭遇すると` + Ocean + + String + メソッドを呼び出す + fmt.Println In`を使用します。

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

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

これまでの例では、値レシーバーでメソッドを定義しました。 つまり、メソッドの機能呼び出しを使用する場合、メソッドが定義されたタイプを参照する最初のパラメーターは、https://www.digitalocean.com/community/conceptual_articlesではなく、そのタイプの値になります/ understand-pointers-in-go [ポインター]。 そのため、受け取った値はデータのコピーであるため、メソッドに提供されたインスタンスに対して行った変更は、メソッドの実行が完了すると破棄されます。 ポインターレシーバーでメソッドを型に定義することもできます。

ポインター受信機

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

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

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

+ main +`内で、 `+ Boat ++ * Boat +)へのポインタを保持する新しい変数 `+ b `を定義します。 このインスタンスで ` AddOccupant `メソッドを2回呼び出して、2人の乗客を追加します。 ` Manifest `メソッドは、 ` Boat `値で定義されます。その定義では、レシーバーは `(b Boat)`として指定されているためです。 「 main 」では、神が自動的にポインターを逆参照して「 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 `に定義し、 ` isUnderwater `を ` true `に変更しました。 また、値レシーバーの ` String()`メソッドを定義し、 ` fmt.Stringer `インターフェースで ` fmt.Println `を使用して ` Shark `の状態をきれいに印刷できるようにしました。先ほど見た fmt.Println + `。 また、 `+ Submersible `パラメーターを受け取る関数 ` submerge +`を使用しました。

`+ * Shark `ではなく、 ` Submersible `インターフェイスを使用すると、 ` submerge `関数が型によって提供される動作のみに依存することができます。 これにより、「 Submarine 」、「 Whale 」、またはまだ考えていない将来の水生生物のために新しい「 submerge 」関数を記述する必要がないため、「 submerge 」関数がより再利用可能になります。 。 それらが ` Dive()`メソッドを定義している限り、 ` submerge +`関数で使用できます。

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

`+ 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でメソッドを宣言することは、最終的には、さまざまなタイプの変数を受け取る関数を定義することと同じです。 https://www.digitalocean.com/community/conceptual_articles/understanding-pointers-in-go [ポインターの操作]と同じルールが適用されます。 Goは、この非常に一般的な関数定義にいくつかの便利さを提供し、これらをインターフェイスタイプによって推論できるメソッドのセットに収集します。 メソッドを効果的に使用すると、コード内のインターフェイスを操作してテスト容易性を向上させ、コードの将来の読者のために組織を改善できます。

Goプログラミング言語全般について詳しく知りたい場合は、https://www.digitalocean.com/community/tutorial_series/how-to-code-in-go [Goのコーディング方法]をご覧ください。 。

前の投稿:GOPATHを理解する
次の投稿:Dockerエコシステム:スケジューリングとオーケストレーション