Definieren von Methoden in Go

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üsselwortfuncein 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 vonSayGoodbyeverdeckt 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 Paketfmtdefiniert 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.Stringerimplementiert. 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.Stringerangeforderten ü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 MethodeAddOccupanthinzuzufügen, daher haben wir das Feldoccupantsnicht 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 MethodeAddOccupantzweimal auf, um zwei Passagiere hinzuzufügen. Die MethodeManifest wird anhand des WertsBoatdefiniert, 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.Printlnakzeptiertefmt.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 Funktionsubmergeverwendet 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.