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!
と呼ばれる構造体を作成しました Creature と string のフィールド Name と Greeting. これ Creature 単一のメソッドが定義されています。 Greet. レシーバー宣言内で、次のインスタンスを割り当てました Creature 変数に c のフィールドを参照できるように Creature で挨拶メッセージを組み立てるとき fmt.Printf.
他の言語では、メソッド呼び出しの受信者は通常、キーワードによって参照されます(例: this また self). Goは、レシーバーを他のレシーバーと同じように変数と見なすため、好きな名前を付けることができます。 コミュニティがこのパラメーターに推奨するスタイルは、レシーバータイプの最初の文字の小文字バージョンです。 この例では、 c レシーバータイプが Creature.
の体内 main、のインスタンスを作成しました Creature およびその指定値 Name と Greeting 田畑。 を呼び出しました Greet タイプの名前とメソッドの名前を . のインスタンスを提供します Creature 最初の引数として。
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!
この例は前の例と同じですが、今回はドット表記を使用して Greet を使用する方法 Creature に保存 sammy 受信者としての変数。 これは、最初の例の関数呼び出しの省略表記です。 標準ライブラリと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 メソッドを呼び出します Greet と SayGoodbye に sammy 最初にドット表記を使用し、次に関数呼び出しスタイルを使用する変数。
どちらのスタイルも同じ結果を出力しますが、ドット表記を使用した例の方がはるかに読みやすくなっています。 ドットのチェーンは、メソッドが呼び出されるシーケンスも示します。ここで、機能スタイルはこのシーケンスを反転します。 パラメータの追加 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 実装と言われています fmt.Stringer インターフェースのため Ocean と呼ばれるメソッドを定義します String、これはパラメータを受け取らず、 string. の main、新しいを定義しました Ocean そしてそれを log 関数は、 string 最初に印刷し、次に実装するものを印刷します fmt.Stringer. Goコンパイラを使用すると、 o ここに Ocean によって要求されたすべてのメソッドを実装します fmt.Stringer. 内部 log、 を使用しております fmt.Println、を呼び出します String の方法 Ocean 遭遇したとき fmt.Stringer そのパラメータの1つとして。
もしも 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
これまでの例では、値レシーバーでメソッドを定義しました。 つまり、メソッドの機能呼び出しを使用する場合、メソッドが定義されたタイプを参照する最初のパラメーターは、ポインターではなく、そのタイプの値になります。 したがって、受け取った値はデータのコピーであるため、メソッドに提供されたインスタンスに加えた変更は、メソッドの実行が完了すると破棄されます。 型へのポインタレシーバーでメソッドを定義することも可能です。
ポインターレシーバー
ポインターレシーバーでメソッドを定義するための構文は、値レシーバーでメソッドを定義するのとほぼ同じです。 違いは、レシーバー宣言の型の名前の前にアスタリスク(*). 次の例では、型へのポインターレシーバーのメソッドを定義しています。
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
この例では、 Boat でタイプする Name と occupants. 他のパッケージのコードに、 AddOccupant メソッドなので、 occupants フィールド名の最初の文字を小文字にすることにより、フィールドがエクスポートされません。 また、その呼び出しを確認したい AddOccupant のインスタンスを引き起こします Boat 変更する必要があるため、 AddOccupant ポインタ受信機で。 ポインタは、そのタイプのコピーではなく、そのタイプの特定のインスタンスへの参照として機能します。 知っています AddOccupant へのポインタを使用して呼び出されます Boat 変更が持続することを保証します。
内部 main、新しい変数を定義します。 b、へのポインタを保持します Boat (*Boat). を呼び出します AddOccupant このインスタンスで2回メソッドを実行して、2人の乗客を追加します。 The Manifest メソッドはで定義されています Boat その定義では、レシーバーは次のように指定されているため、値 (b Boat). の main、まだ電話をかけることができます Manifest Goはポインタを自動的に逆参照して、 Boat 価値。 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
この例では、 Submersible それはタイプが Dive() 方法。 次に、 Shark でタイプする Name フィールドと isUnderwater の状態を追跡する方法 Shark. 私たちは定義しました Dive() ポインタレシーバのメソッド Shark 変更した isUnderwater に true. また、 String() 値受信機の状態をきれいに印刷できるようにする方法 Shark を使用して fmt.Println を使用して fmt.Stringer によって受け入れられるインターフェース fmt.Println 先ほど見たものです。 関数も使用しました submerge それはかかります Submersible パラメータ。
を使用して Submersible ではなくインターフェース *Shark を許可します submerge タイプによって提供される動作のみに依存する関数。 これにより、 submerge 新しいものを書く必要がないので、より再利用可能な機能 submerge のための関数 Submarine、 Whale、または私たちがまだ考えていない他の将来の水生生物。 彼らが定義する限り Dive() メソッド、それらはで使用することができます submerge 関数。
内部 main 変数を定義しました s それはへのポインタです Shark すぐに印刷されます s と fmt.Println. これは、出力の最初の部分を示しています。 Sammy is on the surface. 合格しました s に submerge そして呼ばれる fmt.Println 再び s 印刷された出力の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シリーズのコーディング方法をご覧ください。