Einführung
MitFunctions können Sie Logik in wiederholbaren Prozeduren organisieren, die bei jeder Ausführung unterschiedliche Argumente verwenden können. Beim Definieren von Funktionen werden Sie häufig feststellen, dass mehrere Funktionen jedes Mal auf dasselbe Datenelement angewendet werden. Go erkennt dieses Muster und ermöglicht es Ihnen, spezielle Funktionen zu definieren, die alsmethods bezeichnet werden und deren Zweck darin besteht, Instanzen eines bestimmten Typs zu bearbeiten, die alsreceiver bezeichnet werden. Durch Hinzufügen von Methoden zu Typen können Sie nicht nur mitteilen, was die Daten sind, sondern auch, wie diese Daten verwendet werden sollen.
Eine Methode definieren
Die Syntax zum Definieren einer Methode ähnelt der Syntax zum Definieren einer Funktion. Der einzige Unterschied besteht darin, dass nach dem Schlüsselwortfunc
ein zusätzlicher Parameter hinzugefügt wird, um den Empfänger der Methode anzugeben. Der Empfänger ist eine Deklaration des Typs, für den Sie die Methode definieren möchten. Das folgende Beispiel definiert eine Methode für einen Strukturtyp:
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)
}
Wenn Sie diesen Code ausführen, lautet die Ausgabe:
OutputSammy says Hello!
Wir haben eine Struktur namensCreature
mitstring
Feldern für aName
und aGreeting
erstellt. Für diesesCreature
ist eine einzige Methode definiert,Greet
. Innerhalb der Empfängerdeklaration haben wir die Instanz vonCreature
der Variablenc
zugewiesen, damit wir beim Zusammenstellen der Begrüßungsnachricht infmt.Printf
auf die Felder vonCreature
verweisen können .
In anderen Sprachen wird der Empfänger von Methodenaufrufen typischerweise durch ein Schlüsselwort (z. this
oderself
). Go betrachtet den Empfänger als eine Variable wie jede andere. Sie können ihn also beliebig benennen. Der von der Community bevorzugte Stil für diesen Parameter ist eine Kleinbuchstabenversion des ersten Zeichens des Empfängertyps. In diesem Beispiel haben wirc
verwendet, da der EmpfängertypCreature
war.
Innerhalb des Körpers vonmain
haben wir eine Instanz vonCreature
erstellt und Werte für die FelderName
undGreeting
angegeben. Wir haben hier dieGreet
-Methode aufgerufen, indem wir den Namen des Typs und den Namen der Methode mit einem.
verbunden und die Instanz vonCreature
als erstes Argument angegeben haben.
Go bietet eine andere, bequemere Methode zum Aufrufen von Methoden für Instanzen einer Struktur, wie in diesem Beispiel dargestellt:
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()
}
Wenn Sie dies ausführen, entspricht die Ausgabe dem vorherigen Beispiel:
OutputSammy says Hello!
Dieses Beispiel ist identisch mit dem vorherigen, aber dieses Mal haben wirdot notation verwendet, um dieGreet
-Methode mit den in der Variablensammy
als Empfänger gespeichertenCreature
aufzurufen. Dies ist eine Kurzschreibweise für den Funktionsaufruf im ersten Beispiel. Die Standardbibliothek und die Go-Community bevorzugen diesen Stil so sehr, dass Sie den zuvor gezeigten Funktionsaufrufstil selten sehen werden.
Das nächste Beispiel zeigt einen Grund, warum die Punktnotation häufiger vorkommt:
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")
}
Wenn Sie diesen Code ausführen, sieht die Ausgabe folgendermaßen aus:
OutputSammy says Hello!!
Farewell gophers !
Sammy says Hello!!
Farewell gophers !
Wir haben die früheren Beispiele geändert, um eine andere Methode namensSayGoodbye
einzuführen, undGreet
geändert, umCreature
zurückzugeben, damit wir weitere Methoden für diese Instanz aufrufen können. Im Hauptteil vonmain
rufen wir die MethodenGreet
undSayGoodbye
für die Variablesammy
zuerst unter Verwendung der Punktnotation und dann unter Verwendung des funktionalen Aufrufstils auf.
Beide Stile geben die gleichen Ergebnisse aus, aber das Beispiel mit Punktnotation ist weitaus besser lesbar. Die Punktkette gibt auch die Reihenfolge an, in der Methoden aufgerufen werden, wobei der Funktionsstil diese Reihenfolge invertiert. Das Hinzufügen eines Parameters zum Aufruf vonSayGoodbye
verdeckt die Reihenfolge der Methodenaufrufe weiter. Die Klarheit der Punktnotation ist der Grund dafür, dass dies der bevorzugte Stil zum Aufrufen von Methoden in Go ist, sowohl in der Standardbibliothek als auch unter den Paketen von Drittanbietern, die Sie im gesamten Go-Ökosystem finden.
Das Definieren von Methoden für Typen hat im Gegensatz zum Definieren von Funktionen, die mit einem bestimmten Wert arbeiten, eine andere besondere Bedeutung für die Programmiersprache Go. Methoden sind das Kernkonzept hinter Schnittstellen.
Schnittstellen
Wenn Sie eine Methode für einen beliebigen Typ in Go definieren, wird diese Methode zu denmethod set des Typs hinzugefügt. Der Methodensatz ist die Sammlung von Funktionen, die diesem Typ als Methoden zugeordnet sind und vom Go-Compiler verwendet werden, um zu bestimmen, ob einer Variablen mit einem Schnittstellentyp ein Typ zugewiesen werden kann. Eininterface type ist eine Spezifikation von Methoden, die vom Compiler verwendet werden, um sicherzustellen, dass ein Typ Implementierungen für diese Methoden bereitstellt. Jeder Typ, der Methoden mit demselben Namen, denselben Parametern und denselben Rückgabewerten wie in der Definition einer Schnittstelle enthält, wird alsimplementdieser Schnittstelle bezeichnet und darf Variablen mit dem Typ dieser Schnittstelle zugewiesen werden. Das Folgende ist die Definition derfmt.Stringer
-Schnittstelle aus der Standardbibliothek:
type Stringer interface {
String() string
}
Damit ein Typ diefmt.Stringer
-Schnittstelle implementieren kann, muss er eineString()
-Methode bereitstellen, diestring
zurückgibt. Durch die Implementierung dieser Schnittstelle kann Ihr Typ genau so gedruckt werden, wie Sie es möchten (manchmal auch als "hübsch gedruckt" bezeichnet), wenn Sie Instanzen Ihres Typs an Funktionen übergeben, die im Paketfmt
definiert sind. Das folgende Beispiel definiert einen Typ, der diese Schnittstelle implementiert:
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)
}
Wenn Sie den Code ausführen, wird folgende Ausgabe angezeigt:
Outputocean contains : sea urchin, lobster, shark
In diesem Beispiel wird ein neuer Strukturtyp mit dem NamenOcean
definiert. Ocean
wird alsimplement diefmt.Stringer
-Schnittstelle bezeichnet, daOcean
eine Methode namensString
definiert, die keine Parameter akzeptiert undstring
zurückgibt. Inmain
haben wir ein neuesOcean
definiert und es an einelog
-Funktion übergeben, die zuerststring
druckt, gefolgt von allem, wasfmt.Stringer
implementiert. s. Mit dem Go-Compiler können wir hiero
übergeben, daOcean
alle vonfmt.Stringer
angeforderten Methoden implementiert. Innerhalb vonlog
verwenden wirfmt.Println
, die dieString
-Methode vonOcean
aufruft, wenn sie auffmt.Stringer
als einen ihrer Parameter trifft.
WennOcean
keineString()
-Methode bereitstellen würde, würde Go einen Kompilierungsfehler erzeugen, da dielog
-Methode einfmt.Stringer
als Argument anfordert. Der Fehler sieht folgendermaßen aus:
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 stellt außerdem sicher, dass die bereitgestellte MethodeString()
genau mit der von der Schnittstellefmt.Stringer
angeforderten übereinstimmt. Wenn dies nicht der Fall ist, wird ein Fehler erzeugt, der folgendermaßen aussieht:
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
In den bisherigen Beispielen haben wir Methoden zum Wertempfänger definiert. Das heißt, wenn wir den funktionalen Aufruf von Methoden verwenden, ist der erste Parameter, der sich auf den Typ bezieht, für den die Methode definiert wurde, ein Wert dieses Typs und nicht einpointer. Folglich werden alle Änderungen, die wir an der an der Methode bereitgestellten Instanz vornehmen, verworfen, wenn die Ausführung der Methode abgeschlossen ist, da der empfangene Wert eine Kopie der Daten ist. Es ist auch möglich, Methoden auf dem Zeigerempfänger auf einen Typ zu definieren.
Zeigerempfänger
Die Syntax zum Definieren von Methoden auf dem Zeigerempfänger ist nahezu identisch mit dem Definieren von Methoden auf dem Wertempfänger. Der Unterschied besteht darin, dass dem Namen des Typs in der Empfängerdeklaration ein Sternchen (*
) vorangestellt wird. Das folgende Beispiel definiert eine Methode auf dem Zeigerempfänger auf einen Typ:
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()
}
Sie sehen die folgende Ausgabe, wenn Sie dieses Beispiel ausführen:
OutputThe S.S. DigitalOcean has the following occupants:
Sammy the Shark
Larry the Lobster
In diesem Beispiel wurde einBoat
-Typ mitName
undoccupants
definiert. Wir möchten Code in anderen Paketen zwingen, nur Insassen mit der MethodeAddOccupant
hinzuzufügen, daher haben wir das Feldoccupants
nicht exportiert, indem wir den ersten Buchstaben des Feldnamens in Kleinbuchstaben geschrieben haben. Wir möchten auch sicherstellen, dass durch das Aufrufen vonAddOccupant
die Instanz vonBoat
geändert wird, weshalb wirAddOccupant
auf dem Zeigerempfänger definiert haben. Zeiger verweisen eher auf eine bestimmte Instanz eines Typs als auf eine Kopie dieses Typs. Wenn Sie wissen, dassAddOccupant
mit einem Zeiger aufBoat
aufgerufen wird, wird sichergestellt, dass alle Änderungen bestehen bleiben.
Innerhalb vonmain
definieren wir eine neue Variable,b
, die einen Zeiger aufBoat
(*Boat
) enthält. In dieser Instanz rufen wir die MethodeAddOccupant
zweimal auf, um zwei Passagiere hinzuzufügen. Die MethodeManifest
wird anhand des WertsBoat
definiert, da der Empfänger in seiner Definition als(b Boat)
angegeben wird. Inmain
können wir immer nochManifest
aufrufen, da Go den Zeiger automatisch dereferenzieren kann, um den Wert vonBoat
zu erhalten. b.Manifest()
entspricht hier(*b).Manifest()
.
Ob eine Methode in einem Zeigerempfänger oder in einem Wertempfänger definiert ist, hat wichtige Auswirkungen auf den Versuch, Variablen, die Schnittstellentypen sind, Werte zuzuweisen.
Zeigerempfänger und -schnittstellen
Wenn Sie einer Variablen mit einem Schnittstellentyp einen Wert zuweisen, überprüft der Go-Compiler den Methodensatz des zugewiesenen Typs, um sicherzustellen, dass er über die von der Schnittstelle erwarteten Methoden verfügt. Die Methodensätze für den Zeigerempfänger und den Wertempfänger sind unterschiedlich, da Methoden, die einen Zeiger empfangen, ihren Empfänger ändern können, wohingegen diejenigen, die einen Wert empfangen, dies nicht können.
Das folgende Beispiel zeigt die Definition von zwei Methoden: Eine auf dem Zeigerempfänger eines Typs und eine auf seinem Wertempfänger. Nur der Zeigerempfänger kann jedoch die in diesem Beispiel definierte Schnittstelle erfüllen:
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)
}
Wenn Sie den Code ausführen, wird folgende Ausgabe angezeigt:
OutputSammy is on the surface
Sammy is underwater
In diesem Beispiel wurde eine Schnittstelle namensSubmersible
definiert, die Typen mit einerDive()
-Methode erwartet. Wir haben dann einenShark
-Typ mit einemName
-Feld und einerisUnderwater
-Methode definiert, um den Zustand derShark
zu verfolgen. Wir haben eineDive()
-Methode auf dem Zeigerempfänger aufShark
definiert, dieisUnderwater
auftrue
modifiziert. Wir haben auch dieString()
-Methode des Wertempfängers definiert, damit der Status derShark
mithilfe vonfmt.Println
sauber gedruckt werden kann, indem die vonfmt.Println
akzeptiertefmt.Stringer
-Schnittstelle verwendet wird. s, die wir uns früher angesehen haben. Wir haben auch eine Funktionsubmerge
verwendet, die einenSubmersible
-Parameter akzeptiert.
Wenn Sie dieSubmersible
-Schnittstelle anstelle von*Shark
verwenden, kann diesubmerge
-Funktion nur vom Verhalten eines Typs abhängen. Dies macht diesubmerge
-Funktion wiederverwendbarer, da Sie keine neuensubmerge
-Funktionen fürSubmarine
, aWhale
oder andere zukünftige Wasserbewohner schreiben müssten, die wir haben. ' Ich habe noch nicht daran gedacht. Solange sie die MethodeDive()
definieren, können sie mit der Funktionsubmerge
verwendet werden.
Innerhalb vonmain
haben wir eine Variables
definiert, die ein Zeiger aufShark
ist, und soforts
mitfmt.Println
gedruckt. Dies zeigt den ersten Teil der Ausgabe,Sammy is on the surface
. Wir habens
ansubmerge
übergeben und dannfmt.Println
erneut mits
als Argument aufgerufen, um den zweiten Teil der Ausgabe zu sehen,Sammy is underwater
.
Wenn wirs
inShark
anstatt in*Shark
ändern würden, würde der Go-Compiler den Fehler erzeugen:
Outputcannot use s (type Shark) as type Submersible in argument to submerge:
Shark does not implement Submersible (Dive method has pointer receiver)
Der Go-Compiler teilt uns hilfreich mit, dassShark
eineDive
-Methode hat, die nur auf dem Zeigerempfänger definiert ist. Wenn Sie diese Nachricht in Ihrem eigenen Code sehen, besteht die Korrektur darin, einen Zeiger auf den Schnittstellentyp zu übergeben, indem Sie den Operator&
vor der Variablen verwenden, der der Werttyp zugewiesen ist.
Fazit
Die Deklaration von Methoden in Go unterscheidet sich letztendlich nicht von der Definition von Funktionen, die unterschiedliche Variablentypen erhalten. Es gelten die gleichen Regeln fürworking with pointers. Go bietet einige Vorteile für diese äußerst häufige Funktionsdefinition und fasst diese in Gruppen von Methoden zusammen, die anhand von Schnittstellentypen begründet werden können. Wenn Sie Methoden effektiv einsetzen, können Sie mit den Schnittstellen in Ihrem Code arbeiten, um die Testbarkeit zu verbessern und eine bessere Organisation für zukünftige Leser Ihres Codes zu hinterlassen.
Wenn Sie mehr über die Programmiersprache Go im Allgemeinen erfahren möchten, lesen Sie unsereHow To Code in Go series.