Verwenden von Methoden in Go

Abgeschlossen

Eine Methode in Go ist ein spezieller Typ einer Funktion, jedoch mit einem wichtigen Unterschied: Sie müssen vor dem Funktionsname einen zusätzlichen Parameter einschließen. Dieser zusätzliche Parameter wird als Empfänger bezeichnet.

Methoden sind hilfreich, wenn Sie Funktionen gruppieren und an einen benutzerdefinierten Typ binden möchten. Dieser Ansatz in Go ähnelt dem Erstellen einer Klasse in anderen Programmiersprachen, da er es Ihnen ermöglicht, bestimmte Features des objektorientieren Programmiermodells (OOP) zu implementieren, darunter Einbettung, Überladung und Kapselung.

Damit Sie sich eine Vorstellung davon machen können, warum Methoden in Go wichtig sind, erfahren Sie hier nun, wie Sie eine Methode deklarieren.

Deklarieren von Methoden

Bisher haben Sie Strukturen nur als weiteren benutzerdefinierten Typ verwendet, den Sie in Go erstellen können. In diesem Modul wird gezeigt, wie Sie durch das Hinzufügen von Methoden Verhaltensweisen für von Ihnen erstellte Strukturen hinzufügen können.

Die Syntax zum Deklarieren einer Methode ist ähnlich der folgenden:

func (variable type) MethodName(parameters ...) {
    // method functionality
}

Bevor Sie eine Methode deklarieren können, müssen Sie jedoch eine Struktur erstellen. Angenommen, Sie möchten ein Geometriepaket erstellen. Als Teil des Pakets möchten Sie eine Dreiecksstruktur namens triangle erstellen. Dann möchten Sie eine Methode verwenden, um den Umfang dieses Dreiecks zu berechnen. In Go lässt sich dies folgendermaßen darstellen:

type triangle struct {
    size int
}

func (t triangle) perimeter() int {
    return t.size * 3
}

Die Struktur sieht wie eine reguläre Struktur aus, die perimeter()-Funktion verfügt jedoch über einen zusätzlichen Parameter des Typs triangle vor dem Funktionsname. Mithilfe dieses Empfängers können Sie die Funktion folgendermaßen aufrufen, wenn Sie die Struktur verwenden:

func main() {
    t := triangle{3}
    fmt.Println("Perimeter:", t.perimeter())
}

Wenn Sie versuchen, die perimeter()-Funktion wie gewohnt aufzurufen, funktioniert das nicht. Dies liegt daran, dass die Funktionssignatur einen Empfänger benötigt. Die einzige Möglichkeit, diese Methode aufzurufen, besteht also darin, zunächst eine Struktur zu deklarieren, die Ihnen den Zugriff auf die Methode ermöglicht. Sie könnten sogar denselben Namen für eine Methode verwenden, solange diese zu einer anderen Struktur gehört. Sie können beispielsweise folgendermaßen eine square-Struktur mit einer perimeter()-Funktion erstellen:

package main

import "fmt"

type triangle struct {
    size int
}

type square struct {
    size int
}

func (t triangle) perimeter() int {
    return t.size * 3
}

func (s square) perimeter() int {
    return s.size * 4
}

func main() {
    t := triangle{3}
    s := square{4}
    fmt.Println("Perimeter (triangle):", t.perimeter())
    fmt.Println("Perimeter (square):", s.perimeter())
}

Wenn Sie den vorangehenden Code ausführen, tritt ein Fehler auf. Die folgende Ausgabe wird zurückgegeben:

Perimeter (triangle): 9
Perimeter (square): 16

Der Compiler bestimmt für die zwei Aufrufe der perimeter()-Funktion auf Grundlage des Empfängertyps, welche Funktion aufgerufen werden soll. Dieses Verhalten sorgt für Konsistenz und kurze Funktionsnamen in Paketen und verhindert, dass der Paketname als Präfix eingeschlossen wird. In der nächsten Lerneinheit, in der es um Schnittstellen gehen wird, erhalten Sie weitere Informationen, warum dieses Verhalten eine Rolle spielt.

Zeiger in Methoden

Es gibt Zeiten, in denen eine Methode eine Variable aktualisieren muss. Wenn das Argument für die Methode zu groß ist, sollten Sie es auch nicht kopieren. In diesen Fällen müssen Sie Zeiger verwenden, um die Adresse einer Variable zu übergeben. In einem vorherigen Modul, in dem Zeiger erläutert wurden, haben Sie erfahren, dass Go jedes Mal, wenn Sie eine Funktion in Go aufrufen, eine Kopie der einzelnen Argumentwerte erstellt, um die Werte verwenden zu können.

Dasselbe Verhalten tritt zutage, wenn Sie die Empfängervariable in einer Methode aktualisieren müssen. Angenommen, Sie möchten beispielsweise eine neue Methode erstellen, mit der die Größe des Dreiecks verdoppelt werden kann. In diesem Fall müssen Sie wie im folgenden Beispiel einen Zeiger in der Empfängervariable verwenden:

func (t *triangle) doubleSize() {
    t.size *= 2
}

Folgendermaßen können Sie überprüfen, ob die Methode funktioniert:

func main() {
    t := triangle{3}
    t.doubleSize()
    fmt.Println("Size:", t.size)
    fmt.Println("Perimeter:", t.perimeter())
}

Wenn Sie den vorherigen Code ausführen, sollte die folgende Ausgabe angezeigt werden:

Size: 6
Perimeter: 18

Sie benötigen keinen Zeiger in der Empfängervariable, wenn die Methode nur auf die Informationen des Empfängers zugreift. Per Go-Konvention ist es jedoch üblich, dass alle Methoden einer Struktur über einen Zeigerempfänger verfügen, sobald eine Struktur einen Zeigerempfänger aufweist, auch wenn eine Methode eigentlich keinen benötigt.

Deklarieren von Methoden für andere Typen

Ein entscheidender Aspekt für Methoden besteht darin, sie für alle Typen zu definieren, nicht nur für benutzerdefinierte Typen wie Strukturen. Sie können eine Struktur jedoch nicht für einen Typ definieren, der zu einem anderen Paket gehört. Deshalb können Sie keine Methode für einen einfachen Typ wie string erstellen.

Dennoch gibt es eine Möglichkeit, einen benutzerdefinierten Typ aus einem einfachen Typ zu erstellen und diesen dann so zu verwenden, als wäre es der einfache Typ. Angenommen, Sie möchten zum Beispiel eine Methode erstellen, mit der eine Zeichenfolge mit Kleinbuchstaben in eine Zeichenfolge in Großschreibung transformiert wird. Dazu würden Sie Code ähnlich dem folgenden schreiben:

package main

import (
    "fmt"
    "strings"
)

type upperstring string

func (s upperstring) Upper() string {
    return strings.ToUpper(string(s))
}

func main() {
    s := upperstring("Learning Go!")
    fmt.Println(s)
    fmt.Println(s.Upper())
}

Wenn Sie den vorherigen Code ausführen, wird die folgende Ausgabe angezeigt:

Learning Go!
LEARNING GO!

Beachten Sie, wie Sie das neue Objekt s verwenden können, als wäre es eine Zeichenfolge, wenn dessen Wert das erste Mal ausgegeben wird. Wenn Sie dann die Upper-Methode aufrufen, gibt s alle Buchstaben in Großschreibung mit dem Typ einer Zeichenfolge zurück.

Einbetten von Methoden

In einem früheren Modul haben Sie erfahren, dass Sie eine Eigenschaft in einer Struktur verwenden und dieselbe Eigenschaft in einer anderen Struktur einbetten können. Sie können Eigenschaften aus einer Struktur also wiederverwenden, um Wiederholungen zu vermeiden und für Konsistenz in Ihrer Codebasis zu sorgen. Ein ähnlicher Ansatz ist für Methoden möglich. Sie können Methoden der eingebetteten Struktur selbst dann aufrufen, wenn der Empfänger ein anderer ist.

Angenommen, Sie möchten beispielsweise eine neue Dreiecksstruktur mit einer Logik erstellen, die eine Farbe einschließt. Zusätzlich möchten Sie weiterhin die zuvor deklarierte Dreiecksstruktur verwenden. Die Dreiecksstruktur mit Farbe würde also folgendermaßen aussehen:

type coloredTriangle struct {
    triangle
    color string
}

Danach könnten Sie folgendermaßen die coloredTriangle-Struktur initialisieren und die perimeter()-Methode aus der triangle-Struktur aufrufen. Sie könnten sogar auf die dazugehörigen Felder zugreifen:

func main() {
    t := coloredTriangle{triangle{3}, "blue"}
    fmt.Println("Size:", t.size)
    fmt.Println("Perimeter", t.perimeter())
}

Fahren Sie fort, und schließen Sie die vorherigen Änderungen in Ihr Programm ein, um sich ein Bild von der Funktionsweise von Einbettungen zu machen. Wenn Sie das Programm mit einer main()-Methode wie der vorherigen ausführen, sollten Sie die folgende Ausgabe erhalten:

Size: 3
Perimeter 9

Wenn Sie mit einer OOP-Sprache wie Java oder C++ vertraut sind, sind Sie vielleicht der Meinung, dass die triangle-Struktur einer Basisklasse ähnelt und es sich bei coloredTriangle um eine Unterklasse handelt. Dies ist jedoch nicht der Fall. Stattdessen gibt der Go-Compiler die perimeter()-Methode weiter, indem eine Wrappermethode erstellt wird, die folgendermaßen aussieht:

func (t coloredTriangle) perimeter() int {
    return t.triangle.perimeter()
}

Beachten Sie, dass der Empfänger coloredTriangle ist. Dieser ruft die perimeter()-Methode aus dem Dreiecksfeld auf. Ein Vorteil ist, dass Sie die vorherige Methode nicht erstellen müssen. Sie könnten die Methode zwar selbst erstellen, Go erstellt die Methode jedoch unter der Haube für Sie. Das vorherige Beispiel wurde nur zu Lernzwecken behandelt.

Überladungsmethoden

Kehren Sie zum triangle-Beispiel zurück, das zuvor erläutert wurde. Was geschieht, wenn Sie die Implementierung der perimeter()-Methode in der coloredTriangle-Struktur ändern möchten? Sie können nicht zwei Funktionen mit demselben Namen verwenden. Da Methoden jedoch einen zusätzlichen Parameter (den Empfänger) benötigen, dürfen Sie eine Methode mit demselben Namen verwenden, solange sie für den Empfänger spezifisch ist, den Sie verwenden möchten. Wenn Sie diese Unterscheidung nutzen, können Sie Methoden überladen.

Anders formuliert: Sie könnten die Wrappermethode wie zuvor beschrieben erstellen, wenn Sie dieses Verhalten ändern möchten. Wenn der Umfang eines farbigen Dreiecks dem doppelten Umfang eines normalen Dreiecks entspricht, sähe der Code in etwa wie folgt aus:

func (t coloredTriangle) perimeter() int {
    return t.size * 3 * 2
}

Wenn Sie keine weiteren Änderungen in der zuvor geschriebenen main()-Methode vornehmen, würde der Code nun wie folgt aussehen:

func main() {
    t := coloredTriangle{triangle{3}, "blue"}
    fmt.Println("Size:", t.size)
    fmt.Println("Perimeter", t.perimeter())
}

Wenn Sie den Code ausführen, erhalten Sie eine andere Ausgabe:

Size: 3
Perimeter 18

Wenn Sie die perimeter()-Methode weiterhin aus der triangle-Struktur aufrufen müssen, können Sie dies durch einen expliziten Zugriff darauf tun. Sehen Sie sich dazu das folgende Beispiel an:

func main() {
    t := coloredTriangle{triangle{3}, "blue"}
    fmt.Println("Size:", t.size)
    fmt.Println("Perimeter (colored)", t.perimeter())
    fmt.Println("Perimeter (normal)", t.triangle.perimeter())
}

Wenn Sie diesen Code ausführen, erhalten Sie möglicherweise die folgende Ausgabe:

Size: 3
Perimeter (colored) 18
Perimeter (normal) 9

Wie Sie vielleicht bereits festgestellt haben, können Sie in Go eine Methode überschreiben und bei Bedarf dennoch weiterhin auf die ursprüngliche Methode zugreifen.

Kapselung in Methoden

Von Kapselung spricht man, wenn die aufrufende Funktion (bzw. der Client) eines Objekts nicht auf eine Methode zugreifen kann. In anderen Programmiersprachen verwenden Sie in der Regel die Schlüsselwörter private oder public vor dem Methodennamen. In Go müssen Sie nur einen großgeschrieben Bezeichner verwenden, um eine Methode zu einer öffentlichen Methode zu machen. Mit einem kleingeschriebenen Bezeichner machen Sie eine Methode zu einer privaten Methode.

Kapselung wird in Go nur zwischen Paketen verwendet. Anders formuliert bedeutet das, dass Sie nur Implementierungsdetails in einem anderen Paket ausblenden können, nicht das Paket selbst.

Testen Sie dies, indem Sie ein neues Paket geometry erstellen und die Dreiecksstruktur dorthin verschieben. Sehen Sie sich dazu das folgende Beispiel an:

package geometry

type Triangle struct {
    size int
}

func (t *Triangle) doubleSize() {
    t.size *= 2
}

func (t *Triangle) SetSize(size int) {
    t.size = size
}

func (t *Triangle) Perimeter() int {
    t.doubleSize()
    return t.size * 3
}

Folgendermaßen könnten Sie das vorherige Paket verwenden:

func main() {
    t := geometry.Triangle{}
    t.SetSize(3)
    fmt.Println("Perimeter", t.Perimeter())
}

Die folgende Ausgabe sollte angezeigt werden:

Perimeter 18

Wenn Sie versuchen, das size-Feld oder die doubleSize()-Methode in der main()-Funktion aufzurufen, wird im Programm die panic-Funktion genutzt:

func main() {
    t := geometry.Triangle{}
    t.SetSize(3)
    fmt.Println("Size", t.size)
    fmt.Println("Perimeter", t.Perimeter())
}

Wenn Sie den vorherigen Code ausführen, wird der folgende Fehler zurückgegeben:

./main.go:12:23: t.size undefined (cannot refer to unexported field or method size)