前書き
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
および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!",
}
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
という新しい構造体タイプを定義しています。 Ocean
はimplementインターフェイスと呼ばれます。これはOcean
がString
と呼ばれるメソッドを定義しているためです。このメソッドはパラメーターを受け取らず、string
を返します。 main
で、新しいOcean
を定義し、それをlog
関数に渡しました。この関数は、最初にstring
を出力し、次にfmt.Stringer
を実装するものを出力します。 s。 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
これまでの例では、値レシーバーでメソッドを定義しました。 つまり、メソッドの関数呼び出しを使用する場合、メソッドが定義されたタイプを参照する最初のパラメーターは、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
この例では、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
の状態を追跡しました。 ポインターレシーバーのDive()
メソッドをShark
に定義し、isUnderwater
をtrue
に変更しました。 また、値レシーバーのString()
メソッドを定義して、fmt.Println
で受け入れられるfmt.Stringer
インターフェイスを使用して、fmt.Println
を使用してShark
の状態をクリーンに出力できるようにしました。先ほど見たs。 また、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でメソッドを宣言することは、最終的には、さまざまなタイプの変数を受け取る関数を定義することと同じです。 working with pointersの同じルールが適用されます。 Goは、この非常に一般的な関数定義にいくつかの便利さを提供し、これらをインターフェイスタイプによって推論できるメソッドのセットに収集します。 メソッドを効果的に使用すると、コード内のインターフェイスを操作してテスト容易性を向上させ、コードの将来の読者のために組織を改善できます。
Goプログラミング言語全般について詳しく知りたい場合は、How To Code in Go seriesを確認してください。