Verwenden von Schnittstellen in Go

Abgeschlossen

Bei Schnittstellen in Go handelt es sich um einen Datentyp, der verwendet wird, um das Verhalten anderer Typen darzustellen. Eine Schnittstelle ist wie eine Blaupause oder ein Vertrag, den ein Objekt erfüllen soll. Wenn Sie Schnittstellen verwenden, wird Ihre Codebasis flexibler und anpassungsfähiger, da Sie Code schreiben, der nicht an eine bestimmte Implementierung gebunden ist. Deshalb können Sie die Funktionalität eines Programms schnell erweitern. In diesem Modul erfahren Sie, warum dies so ist.

Im Gegensatz zu Schnittstellen in anderen Programmiersprachen werden Vorgaben von Schnittstellen in Go implizit erfüllt. Go bietet keine Schlüsselwörter zum Implementieren einer Schnittstelle. Wenn Sie also mit Schnittstellen in anderen Programmiersprachen vertraut sind, Go jedoch noch nie verwendet haben, ist dieser Gedanke vermutlich zunächst verwirrend.

In diesem Modul arbeiten Sie mit mehreren Beispielen, um Schnittstellen in Go kennenzulernen. So können Sie erfahren, wie Sie sie bestmöglich nutzen.

Deklarieren einer Schnittstelle

Eine Benutzeroberfläche in Go ähnelt einer Blaupause. Dies ist ein abstrakter Typ, der nur die Methoden einschließt, die ein bestimmter Typ besitzen oder implementieren muss.

Angenommen, Sie möchten eine Schnittstelle in Ihrem Geometriepaket erstellen, das angibt, welche Methoden von einer Form implementiert werden müssen. Sie könnten eine Schnittstelle folgendermaßen definieren:

type Shape interface {
    Perimeter() float64
    Area() float64
}

Die Shape-Schnittstelle bedeutet, dass jeder Typ, den Sie als Shape-Schnittstelle betrachten, die Methoden Perimeter() und Area() haben muss. Wenn Sie zum Beispiel eine Square-Struktur erstellen, müssen beide Methoden implementiert werden, nicht nur eine. Beachten Sie außerdem, dass eine Schnittstelle keine Implementierungsdetails für diese Methoden enthält, z. B. zur Berechnung des Umfangs und des Flächeninhalts einer Form. Es handelt sich einfach um einen Vertrag. Für Formen wie Dreiecke, Kreise und Quadrate kann der Flächeninhalt und der Umfang auf unterschiedliche Weise berechnet werden.

Implementieren einer Schnittstelle

Wie zuvor erläutert wurde, gibt es in Go keine Schlüsselwörter zum Implementieren einer Schnittstelle. Die Vorgaben einer Schnittstelle in Go werden implizit von einem Typ erfüllt, wenn dieser über alle Methoden verfügt, die für eine Schnittstelle erforderlich sind.

Erstellen Sie nun eine Square-Struktur, die beide Methoden aus der Shape-Schnittstelle aufweist. Sehen Sie sich dazu den folgenden Beispielcode an:

type Square struct {
    size float64
}

func (s Square) Area() float64 {
    return s.size * s.size
}

func (s Square) Perimeter() float64 {
    return s.size * 4
}

Wie Sie feststellen können, stimmt die Methodensignatur der Square-Struktur mit der Signatur der Shape-Schnittstelle überein. Eine andere Schnittstelle hat jedoch möglicherweise einen anderen Namen, weist jedoch dieselben Methoden auf. Wie oder wann weiß Go darüber Bescheid, welche Schnittstelle von einem konkreten Typ implementiert wird? Go erhält diese Information bei Verwendung, also zur Laufzeit.

Sie könnten folgenden Code schreiben, um zu demonstrieren, wie Schnittstellen verwendet werden:

func main() {
    var s Shape = Square{3}
    fmt.Printf("%T\n", s)
    fmt.Println("Area: ", s.Area())
    fmt.Println("Perimeter:", s.Perimeter())
}

Wenn Sie das vorherige Programm ausführen, wird die folgende Ausgabe angezeigt:

main.Square
Area:  9
Perimeter: 12

An dieser Stelle macht es keinen Unterschied, ob Sie eine Schnittstelle verwenden oder nicht. Erstellen Sie nun einen anderen Typ, z. B. Circle, und sehen Sie sich dann an, warum Schnittstellen hilfreich sind. Hier finden Sie den Code für die Circle-Struktur:

type Circle struct {
    radius float64
}

func (c Circle) Area() float64 {
    return math.Pi * c.radius * c.radius
}

func (c Circle) Perimeter() float64 {
    return 2 * math.Pi * c.radius
}

Gestalten Sie die main()-Funktion nun um, und erstellen Sie eine Funktion, die den Typ des Objekts ausgibt, das von der Funktion empfangen wird, sowie den Flächeninhalt und den Umfang. Sehen Sie sich dazu das folgende Beispiel an:

func printInformation(s Shape) {
    fmt.Printf("%T\n", s)
    fmt.Println("Area: ", s.Area())
    fmt.Println("Perimeter:", s.Perimeter())
    fmt.Println()
}

Beachten Sie, dass die printInformation-Funktion Shape als Parameter aufweist. Sie können ein Square- oder ein Circle-Objekt an diese Funktion senden, und es funktioniert, obwohl die Ausgabe anders ist. Ihre main()-Funktion sieht nun folgendermaßen aus:

func main() {
    var s Shape = Square{3}
    printInformation(s)

    c := Circle{6}
    printInformation(c)
}

Beachten Sie, dass für das c-Objekt nicht angegeben wird, dass es sich um ein Shape-Objekt handelt. Die printInformation-Funktion erwartet jedoch ein Objekt, das die in der Shape-Schnittstelle definierten Methoden implementiert.

Wenn Sie das Programm ausführen, sollte die folgende Ausgabe angezeigt werden:

main.Square
Area:  9
Perimeter: 12

main.Circle
Area:  113.09733552923255
Perimeter: 37.69911184307752

Beachten Sie, dass kein Fehler zurückgegeben wird. Außerdem variiert die Ausgabe je nach empfangenen Objekttypen. Zudem sehen Sie, dass der Objekttyp in der Ausgabe keinerlei Informationen zur Shape-Schnittstelle liefert.

Der Vorteil der Verwendung von Schnittstellen besteht darin, dass die printInformation-Funktion für jeden neuen Typ oder jede neue Implementierung von Shape nicht geändert werden muss. Wie bereits erwähnt, gibt es flexiblere und einfachere Möglichkeiten, um Ihren Code zu erweitern, wenn Sie Schnittstellen verwenden.

Implementieren einer Stringer-Schnittstelle

Ein einfaches Beispiel für die Erweiterung vorhandener Funktionen ist die Verwendung einer Stringer-Schnittstelle. Dabei handelt es sich um eine Schnittstelle, die über eine String()-Methode verfügt. Sehen Sie sich dazu das folgende Beispiel an:

type Stringer interface {
    String() string
}

Die fmt.Printf-Funktion verwendet diese Schnittstelle, um Werte auszugeben, das heißt, Sie können eine benutzerdefinierte String()-Methode schreiben, um eine benutzerdefinierte Zeichenfolge auszugeben. Sehen Sie sich dazu das folgende Beispiel an:

package main

import "fmt"

type Person struct {
    Name, Country string
}

func (p Person) String() string {
    return fmt.Sprintf("%v is from %v", p.Name, p.Country)
}
func main() {
    rs := Person{"John Doe", "USA"}
    ab := Person{"Mark Collins", "United Kingdom"}
    fmt.Printf("%s\n%s\n", rs, ab)
}

Wenn Sie das vorherige Programm ausführen, wird die folgende Ausgabe angezeigt:

John Doe is from USA
Mark Collins is from United Kingdom

Wie Sie sehen können, haben Sie einen benutzerdefinierten Typ (eine Struktur) verwendet, um eine benutzerdefinierte Version der String()-Methode zu schreiben. Das ist eine übliche Methode, eine Schnittstelle in Go zu implementieren. Beispiele finden sich in vielen Programmen, die Sie sich im Folgenden genauer ansehen werden.

Erweitern einer vorhandenen Implementierung

Angenommen, Sie verfügen über den folgenden Code, und Sie möchten seine Funktionalität erweitern, indem Sie eine benutzerdefinierte Implementierung einer Writer-Methode schreiben, die dafür zuständig ist, bestimmte Daten zu bearbeiten.

Durch Verwendung des folgenden Codes können Sie ein Programm erstellen, dass die GitHub-API nutzt, um drei Repositorys von Microsoft abzurufen:

package main

import (
    "fmt"
    "io"
    "net/http"
    "os"
)

func main() {
    resp, err := http.Get("https://api.github.com/users/microsoft/repos?page=15&per_page=3")
    if err != nil {
        fmt.Println("Error:", err)
        os.Exit(1)
    }

    io.Copy(os.Stdout, resp.Body)
}

Wenn Sie den vorangehenden Code ausführen, erhalten Sie eine Ausgabe ähnlich der folgenden. Diese wurde für eine bessere Lesbarkeit abgekürzt:

[{"id":276496384,"node_id":"MDEwOlJlcG9zaXRvcnkyNzY0OTYzODQ=","name":"-Users-deepakdahiya-Desktop-juhibubash-test21zzzzzzzzzzz","full_name":"microsoft/-Users-deepakdahiya-Desktop-juhibubash-test21zzzzzzzzzzz","private":false,"owner":{"login":"microsoft","id":6154722,"node_id":"MDEyOk9yZ2FuaXphdGlvbjYxNTQ3MjI=","avatar_url":"https://avatars2.githubusercontent.com/u/6154722?v=4","gravatar_id":"","url":"https://api.github.com/users/microsoft","html_url":"https://github.com/micro
....

Beachten Sie, dass der io.Copy(os.Stdout, resp.Body)-Aufruf derjenige ist, der die Inhalte im Terminal ausgibt, die Sie über den Aufruf der GitHub-API erhalten. Stellen Sie sich vor, Sie möchten Ihre eigene Implementierung schreiben, um die Inhalte zu kürzen, die im Terminal angezeigt werden. Wenn Sie sich die Quelle der io.Copy-Funktion ansehen, sehen Sie Folgendes:

func Copy(dst Writer, src Reader) (written int64, err error)

Wenn Sie sich den ersten Parameter (dst Writer) genauer ansehen, können Sie feststellen, dass Writer eine Schnittstelle ist:

type Writer interface {
    Write(p []byte) (n int, err error)
}

Sie können den Quellcode des io-Pakets untersuchen, bis Sie herausfinden, wo Copy die Write-Methode aufruft. Wir befassen uns aber erst einmal nicht weiter mit dieser Untersuchung.

Da Writer eine Schnittstelle ist und es sich um ein Objekt handelt, das von der Copy-Funktion erwartet wird, können Sie eine benutzerdefinierte Implementierung der Write-Methode schreiben. Deshalb können Sie die Inhalte anpassen, die im Terminal ausgegeben werden.

Der erste Schritt für das Implementieren einer Schnittstelle ist das Erstellen eines benutzerdefinierten Typs. In diesem Fall können Sie eine leere Struktur erstellen, da Sie Ihre benutzerdefinierte Write-Methode einfach nur schreiben müssen. Sehen Sie sich dazu das folgende Beispiel an:

type customWriter struct{}

Nun können Sie Ihre benutzerdefinierte Write-Funktion schreiben. Sie müssen außerdem eine Struktur schreiben, die die API-Antwort im JSON-Format für ein Golang-Objekt analysiert. Sie können die JSON-to-Go-Website verwenden, um eine Struktur aus JSON-Nutzdaten zu erstellen. Die Write-Methode könnte also folgendermaßen aussehen:

type GitHubResponse []struct {
    FullName string `json:"full_name"`
}

func (w customWriter) Write(p []byte) (n int, err error) {
    var resp GitHubResponse
    json.Unmarshal(p, &resp)
    for _, r := range resp {
        fmt.Println(r.FullName)
    }
    return len(p), nil
}

Schließlich müssen Sie die main()-Funktion so bearbeiten, dass sie Ihr benutzerdefiniertes Objekt verwendet. Sehen Sie sich dazu das folgende Beispiel an:

func main() {
    resp, err := http.Get("https://api.github.com/users/microsoft/repos?page=15&per_page=5")
    if err != nil {
        fmt.Println("Error:", err)
        os.Exit(1)
    }

    writer := customWriter{}
    io.Copy(writer, resp.Body)
}

Wenn Sie das Programm ausführen, sollte die folgende Ausgabe angezeigt werden:

microsoft/aed-blockchain-learn-content
microsoft/aed-content-nasa-su20
microsoft/aed-external-learn-template
microsoft/aed-go-learn-content
microsoft/aed-learn-template

Die Ausgabe sieht nun besser aus. Dies ist der benutzerdefinierten Write-Methode geschuldet, die Sie geschrieben haben. Hier sehen Sie die endgültige Version des Programms:

package main

import (
    "encoding/json"
    "fmt"
    "io"
    "net/http"
    "os"
)

type GitHubResponse []struct {
    FullName string `json:"full_name"`
}

type customWriter struct{}

func (w customWriter) Write(p []byte) (n int, err error) {
    var resp GitHubResponse
    json.Unmarshal(p, &resp)
    for _, r := range resp {
        fmt.Println(r.FullName)
    }
    return len(p), nil
}

func main() {
    resp, err := http.Get("https://api.github.com/users/microsoft/repos?page=15&per_page=5")
    if err != nil {
        fmt.Println("Error:", err)
        os.Exit(1)
    }

    writer := customWriter{}
    io.Copy(writer, resp.Body)
}

Schreiben einer benutzerdefinierten Server-API

Sehen Sie sich nun einen weiteren Anwendungsfall für Schnittstellen an, der hilfreich für Sie sein könnte, wenn Sie eine Server-API erstellen. Die übliche Methode, einen Webserver zu programmieren, erfolgt über die Verwendung einer http.Handler-Schnittstelle aus dem net/http-Paket. Diese ähnelt dem folgenden Code (Sie müssen diesen Code nicht schreiben):

package http

type Handler interface {
    ServeHTTP(w ResponseWriter, r *Request)
}

func ListenAndServe(address string, h Handler) error

Sie sehen, dass die ListenAndServe-Funktion eine Serveradresse erwartet, z. B. http://localhost:8000, und eine Instanz von Handler, die die Antwort des Aufrufs an die Serveradresse verteilt.

Erstellen Sie nun das folgende Programm, und sehen Sie es sich im Detail an:

package main

import (
    "fmt"
    "log"
    "net/http"
)

type dollars float32

func (d dollars) String() string {
    return fmt.Sprintf("$%.2f", d)
}

type database map[string]dollars

func (db database) ServeHTTP(w http.ResponseWriter, req *http.Request) {
    for item, price := range db {
        fmt.Fprintf(w, "%s: %s\n", item, price)
    }
}

func main() {
    db := database{"Go T-Shirt": 25, "Go Jacket": 55}
    log.Fatal(http.ListenAndServe("localhost:8000", db))
}

Bevor Sie sich den vorherigen Code genauer ansehen, führen Sie ihn jedoch folgendermaßen aus:

go run main.go

Wenn Sie keine Ausgabe erhalten, ist das ein gutes Zeichen. Öffnen Sie nun http://localhost:8000 in einem neuen Browserfenster, oder führen Sie den folgenden Befehl in Ihrem Terminal aus:

curl http://localhost:8000

Jetzt sollte die folgende Ausgabe angezeigt werden:

Go T-Shirt: $25.00
Go Jacket: $55.00

Sehen Sie sich den vorangehenden Code genau an, um zu verstehen, was geschieht, und um beobachten zu können, wie leistungsfähig Go-Schnittstellen sind. Sie erstellen zunächst einen benutzerdefinierten Typ für einen float32-Typ. Es soll eine benutzerdefinierte Implementierung der String()-Methode geschrieben werden, die Sie später verwenden.

type dollars float32

func (d dollars) String() string {
    return fmt.Sprintf("$%.2f", d)
}

Danach wird die Implementierung der ServeHTTP-Methode geschrieben, die von http.Handler verwendet werden kann. Beachten Sie, dass hier noch mal ein benutzerdefinierter Typ erstellt wird. Dieses Mal handelt es sich jedoch um eine Zuordnung, nicht um eine Struktur. Als Nächstes schreiben Sie die ServeHTTP-Methode, indem der database-Typ als Empfänger verwendet wird. Für die Implementierung dieser Methode werden die Daten vom Empfänger verwendet, es wird eine Schleife für die Daten ausgeführt und die einzelnen Elemente werden ausgegeben.

type database map[string]dollars

func (db database) ServeHTTP(w http.ResponseWriter, req *http.Request) {
    for item, price := range db {
        fmt.Fprintf(w, "%s: %s\n", item, price)
    }
}

Schließlich wird in der main()-Funktion ein database-Typ instanziiert und der Typ wird mit mehreren Werten initialisiert. Der HTTP-Server wird mithilfe der http.ListenAndServe-Funktion gestartet. Dabei wurde die Serveradresse definiert, einschließlich des Ports, der verwendet werden soll, und des db-Objekts, das eine benutzerdefinierte Version der ServeHTTP-Methode implementiert. Deshalb verwendet Go Ihre Implementierung dieser Methode, wenn Sie das Programm ausführen. So werden Schnittstellen in einer Server-API also verwendet und implementiert.

func main() {
    db := database{"Go T-Shirt": 25, "Go Jacket": 55}
    log.Fatal(http.ListenAndServe("localhost:8000", db))
}

Einen anderen Anwendungsfall für Schnittstellen in einer Server-API lernen Sie kennen, wenn Sie die http.Handle-Funktion verwenden. Weitere Informationen finden Sie im Beitrag Schreiben von Webanwendungen auf der Go-Website.