Goでのメソッドの定義
###序章
関数を使用すると、ロジックを繰り返し可能なプロシージャに編成して、実行するたびに異なる引数を使用できます。 関数を定義する過程で、複数の関数が毎回同じデータに対して動作する可能性があることに気付くことがよくあります。 Goはこのパターンを認識し、メソッドと呼ばれる特別な関数を定義できます。この関数の目的は、レシーバーと呼ばれる特定のタイプのインスタンスを操作することです。 タイプにメソッドを追加すると、データが何であるかだけでなく、そのデータをどのように使用するかを伝えることができます。
メソッドの定義
メソッドを定義するための構文は、関数を定義するための構文に似ています。 唯一の違いは、メソッドのレシーバーを指定するために、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
。
他の言語では、メソッド呼び出しの受信者は通常、キーワードによって参照されます(例: 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!
この例は前の例と同じですが、今回はドット表記を使用して、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で任意のタイプにメソッドを定義すると、そのメソッドがタイプのメソッドセットに追加されます。 メソッドセットは、そのタイプにメソッドとして関連付けられ、Goコンパイラが使用して、あるタイプをインターフェイスタイプの変数に割り当てることができるかどうかを判断する関数のコレクションです。 インターフェースタイプは、タイプがそれらのメソッドの実装を提供することを保証するためにコンパイラーによって使用されるメソッドの仕様です。 インターフェイスの定義で見つかったものと同じ名前、同じパラメータ、同じ戻り値を持つメソッドを持つタイプは、そのインターフェイスを実装と呼ばれ、そのインターフェイスのタイプの変数に割り当てることができます。 以下は、標準ライブラリからの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
はimplementfmt.Stringer
インターフェースと呼ばれます。これは、Ocean
がString
というメソッドを定義しているためです。このメソッドはパラメーターを受け取らず、[ X157X]。 main
では、新しいOcean
を定義し、それをlog
関数に渡しました。この関数は、最初にstring
を出力し、次に実装するものをすべて実行します。 fmt.Stringer
。 Ocean
はfmt.Stringer
によって要求されたすべてのメソッドを実装するため、Goコンパイラではここでo
を渡すことができます。 log
内では、fmt.Println
を使用します。これは、パラメーターの1つとしてfmt.Stringer
を検出すると、Ocean
のString
メソッドを呼び出します。
Ocean
がString()
メソッドを提供しなかった場合、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
これまでの例では、値レシーバーでメソッドを定義しました。 つまり、メソッドの機能呼び出しを使用する場合、メソッドが定義されたタイプを参照する最初のパラメーターは、ポインターではなく、そのタイプの値になります。 したがって、受け取った値はデータのコピーであるため、メソッドに提供されたインスタンスに加えた変更は、メソッドの実行が完了すると破棄されます。 型へのポインタレシーバーでメソッドを定義することも可能です。
ポインターレシーバー
ポインターレシーバーでメソッドを定義するための構文は、値レシーバーでメソッドを定義するのとほぼ同じです。 違いは、レシーバー宣言の型の名前の前にアスタリスク(*
)を付けることです。 次の例では、型へのポインターレシーバーのメソッドを定義しています。
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
フィールドをエクスポートしないようにしました。 また、AddOccupant
を呼び出すと、Boat
のインスタンスが変更されることを確認する必要があります。そのため、ポインターレシーバーでAddOccupant
を定義しました。 ポインタは、そのタイプのコピーではなく、そのタイプの特定のインスタンスへの参照として機能します。 AddOccupant
がBoat
へのポインターを使用して呼び出されることを知っていると、変更が持続することが保証されます。
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
の状態を追跡しました。 isUnderwater
をtrue
に変更するShark
へのポインターレシーバーでDive()
メソッドを定義しました。 また、バリューレシーバーのString()
メソッドを定義し、fmt.Stringer
インターフェイスを使用して、fmt.Println
を使用してShark
の状態をきれいに印刷できるようにしました。先ほど見たfmt.Println
。 また、Submersible
パラメーターを受け取る関数submerge
を使用しました。
*Shark
ではなくSubmersible
インターフェースを使用すると、submerge
関数は型によって提供される動作のみに依存することができます。 これにより、Submarine
、Whale
、またはその他の将来の水生生物用に新しいsubmerge
関数を作成する必要がなくなるため、submerge
関数の再利用性が向上します。まだ考えていない住民。 Dive()
メソッドを定義している限り、submerge
関数で使用できます。
main
内で、Shark
へのポインターである変数s
を定義し、すぐにs
をfmt.Println
で出力しました。 これは、出力の最初の部分Sammy is on the surface
を示しています。 s
をsubmerge
に渡し、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でメソッドを宣言することは、最終的には、さまざまなタイプの変数を受け取る関数を定義することと同じです。 ポインタの操作と同じルールが適用されます。 Goは、この非常に一般的な関数定義にいくつかの便利さを提供し、これらをインターフェイスタイプによって推論できる一連のメソッドに収集します。 メソッドを効果的に使用すると、コード内のインターフェイスを操作してテスト容易性を向上させ、コードの将来の読者のためにより良い編成を残すことができます。
Goプログラミング言語全般について詳しく知りたい場合は、Goシリーズのコーディング方法をご覧ください。