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シリーズのコーディング方法をご覧ください。