So verwenden Sie Schnittstellen in Go

Einführung

Das Schreiben von flexiblem, wiederverwendbarem und modularem Code ist für die Entwicklung vielseitiger Programme von entscheidender Bedeutung. Wenn Sie auf diese Weise arbeiten, wird sichergestellt, dass der Code einfacher zu warten ist, da nicht an mehreren Stellen dieselbe Änderung vorgenommen werden muss. Wie Sie dies erreichen, ist von Sprache zu Sprache unterschiedlich. Beispielsweise istinheritance ein gängiger Ansatz, der in Sprachen wie Java, C ++, C # und anderen verwendet wird.

Entwickler können dieselben Entwurfsziele auch durchcomposition erreichen. Komposition ist eine Möglichkeit, Objekte oder Datentypen zu komplexeren zu kombinieren. Dies ist der Ansatz, mit dem Go die Wiederverwendung, Modularität und Flexibilität von Code fördert. Interfaces in Go bieten eine Methode zum Organisieren komplexer Kompositionen. Wenn Sie lernen, wie man sie verwendet, können Sie gemeinsamen, wiederverwendbaren Code erstellen.

In diesem Artikel erfahren Sie, wie Sie benutzerdefinierte Typen mit allgemeinem Verhalten erstellen, damit wir unseren Code wiederverwenden können. Wir werden auch lernen, wie wir Schnittstellen für unsere eigenen benutzerdefinierten Typen implementieren, die Schnittstellen erfüllen, die aus einem anderen Paket definiert wurden.

Verhalten definieren

Eine der Kernimplementierungen der Komposition ist die Verwendung von Schnittstellen. Eine Schnittstelle definiert ein Verhalten eines Typs. Eine der am häufigsten verwendeten Schnittstellen in der Go-Standardbibliothek ist diefmt.Stringer-Schnittstelle:

type Stringer interface {
    String() string
}

Die erste Codezeile definiert eintype, das alsStringer bezeichnet wird. Es heißt dann, dass es eininterface ist. Genau wie beim Definieren einer Struktur verwendet Go geschweifte Klammern ({}), um die Definition der Schnittstelle zu umgeben. Im Vergleich zum Definieren von Strukturen definieren wir nur diebehavior der Schnittstelle. das heißt, "was kann dieser Typ tun".

Bei der SchnittstelleStringer ist das einzige Verhalten die MethodeString(). Die Methode akzeptiert keine Argumente und gibt eine Zeichenfolge zurück.

Schauen wir uns als nächstes einen Code an, der das Verhalten vonfmt.Stringeraufweist:

main.go

package main

import "fmt"

type Article struct {
    Title string
    Author string
}

func (a Article) String() string {
    return fmt.Sprintf("The %q article was written by %s.", a.Title, a.Author)
}

func main() {
    a := Article{
        Title: "Understanding Interfaces in Go",
        Author: "Sammy Shark",
    }
    fmt.Println(a.String())
}

Als erstes erstellen wir einen neuen Typ namensArticle. Dieser Typ hat einTitle- und einAuthor-Feld und beide gehören zur Zeichenfolgedata type:

main.go

...
type Article struct {
    Title string
    Author string
}
...

Als nächstes definieren wir einmethod namensString für den TypArticle. Die MethodeString gibt eine Zeichenfolge zurück, die den TypArticledarstellt:

main.go

...
func (a Article) String() string {
    return fmt.Sprintf("The %q article was written by %s.", a.Title, a.Author)
}
...

Dann erstellen wir in unserenmainfunction eine Instanz vom TypArticle und weisen sie denvariable zu, diea genannt werden. Wir geben die Werte von"Understanding Interfaces in Go" für das FeldTitle und"Sammy Shark" für das FeldAuthor an:

main.go

...
a := Article{
    Title: "Understanding Interfaces in Go",
    Author: "Sammy Shark",
}
...

Dann drucken wir das Ergebnis der MethodeString aus, indem wirfmt.Println aufrufen und das Ergebnis des Methodenaufrufsa.String() übergeben:

main.go

...
fmt.Println(a.String())

Nach dem Ausführen des Programms wird die folgende Ausgabe angezeigt:

OutputThe "Understanding Interfaces in Go" article was written by Sammy Shark.

Bisher haben wir keine Schnittstelle verwendet, aber wir haben einen Typ erstellt, der ein Verhalten aufweist. Dieses Verhalten stimmte mit derfmt.Stringer-Schnittstelle überein. Als nächstes wollen wir sehen, wie wir dieses Verhalten nutzen können, um unseren Code wiederverwendbarer zu machen.

Schnittstelle definieren

Nachdem wir unseren Typ mit dem gewünschten Verhalten definiert haben, können wir untersuchen, wie dieses Verhalten verwendet wird.

Bevor wir dies tun, schauen wir uns jedoch an, was wir tun müssten, wenn wir die MethodeStringvom TypArticlein einer Funktion aufrufen möchten:

main.go

package main

import "fmt"

type Article struct {
    Title string
    Author string
}

func (a Article) String() string {
    return fmt.Sprintf("The %q article was written by %s.", a.Title, a.Author)
}

func main() {
    a := Article{
        Title: "Understanding Interfaces in Go",
        Author: "Sammy Shark",
    }
    Print(a)
}

func Print(a Article) {
    fmt.Println(a.String())
}

In diesem Code fügen wir eine neue Funktion namensPrint hinzu, die einArticle als Argument verwendet. Beachten Sie, dass die FunktionPrintnur die MethodeStringaufruft. Aus diesem Grund könnten wir stattdessen eine Schnittstelle definieren, die an die Funktion übergeben wird:

main.go

package main

import "fmt"

type Article struct {
    Title string
    Author string
}

func (a Article) String() string {
    return fmt.Sprintf("The %q article was written by %s.", a.Title, a.Author)
}

type Stringer interface {
    String() string
}

func main() {
    a := Article{
        Title: "Understanding Interfaces in Go",
        Author: "Sammy Shark",
    }
    Print(a)
}

func Print(s Stringer) {
    fmt.Println(s.String())
}

Hier haben wir eine Schnittstelle namensStringer erstellt:

main.go

...
type Stringer interface {
    String() string
}
...

DieStringer-Schnittstelle verfügt nur über eine Methode namensString(), diestring zurückgibt. Amethod ist eine spezielle Funktion, die für einen bestimmten Typ in Go gilt. Im Gegensatz zu einer Funktion kann eine Methode nur von der Instanz des Typs aufgerufen werden, für den sie definiert wurde.

Wir aktualisieren dann die Signatur der MethodePrint, umStringer und keinen konkreten Typ vonArticle zu verwenden. Da der Compiler weiß, dass eineStringer-Schnittstelle dieString-S-Methode definiert, akzeptiert er nur Typen, die auch dieString-S-Methode haben.

Jetzt können wir diePrint-Methode mit allem verwenden, was dieStringer-Schnittstelle erfüllt. Erstellen wir einen anderen Typ, um dies zu demonstrieren:

main.go

package main

import "fmt"

type Article struct {
    Title  string
    Author string
}

func (a Article) String() string {
    return fmt.Sprintf("The %q article was written by %s.", a.Title, a.Author)
}

type Book struct {
    Title  string
    Author string
    Pages  int
}

func (b Book) String() string {
    return fmt.Sprintf("The %q book was written by %s.", b.Title, b.Author)
}

type Stringer interface {
    String() string
}

func main() {
    a := Article{
        Title:  "Understanding Interfaces in Go",
        Author: "Sammy Shark",
    }
    Print(a)

    b := Book{
        Title:  "All About Go",
        Author: "Jenny Dolphin",
        Pages:  25,
    }
    Print(b)
}

func Print(s Stringer) {
    fmt.Println(s.String())
}

Wir fügen nun einen zweiten Typ mit dem NamenBook hinzu. Es ist auch die MethodeStringdefiniert. Dies bedeutet, dass es auch dieStringer-Schnittstelle erfüllt. Aus diesem Grund können wir es auch an unserePrint-Funktion senden:

OutputThe "Understanding Interfaces in Go" article was written by Sammy Shark.
The "All About Go" book was written by Jenny Dolphin. It has 25 pages.

Bisher haben wir gezeigt, wie man nur eine einzige Schnittstelle verwendet. Für eine Schnittstelle kann jedoch mehr als ein Verhalten definiert werden. Als nächstes werden wir sehen, wie wir unsere Schnittstellen vielseitiger machen können, indem wir mehr Methoden deklarieren.

Mehrere Verhaltensweisen in einer Schnittstelle

Eine der Grundregeln beim Schreiben von Go-Code besteht darin, kleine, präzise Typen zu schreiben und sie zu größeren, komplexeren Typen zusammenzusetzen. Gleiches gilt für die Zusammenstellung von Schnittstellen. Um zu sehen, wie wir eine Schnittstelle aufbauen, definieren wir zunächst nur eine Schnittstelle. Wir definieren zwei Formen,Circle undSquare, und beide definieren eine Methode namensArea. Diese Methode gibt den geometrischen Bereich ihrer jeweiligen Formen zurück:

main.go

package main

import (
    "fmt"
    "math"
)

type Circle struct {
    Radius float64
}

func (c Circle) Area() float64 {
    return math.Pi * math.Pow(c.Radius, 2)
}

type Square struct {
    Width  float64
    Height float64
}

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

type Sizer interface {
    Area() float64
}

func main() {
    c := Circle{Radius: 10}
    s := Square{Height: 10, Width: 5}

    l := Less(c, s)
    fmt.Printf("%+v is the smallest\n", l)
}

func Less(s1, s2 Sizer) Sizer {
    if s1.Area() < s2.Area() {
        return s1
    }
    return s2
}

Da jeder Typ die MethodeAreadeklariert, können wir eine Schnittstelle erstellen, die dieses Verhalten definiert. Wir erstellen die folgendeSizer-Schnittstelle:

main.go

...
type Sizer interface {
    Area() float64
}
...

Wir definieren dann eine Funktion namensLess, die zweiSizer benötigt und die kleinste zurückgibt:

main.go

...
func Less(s1, s2 Sizer) Sizer {
    if s1.Area() < s2.Area() {
        return s1
    }
    return s2
}
...

Beachten Sie, dass wir nicht nur beide Argumente als TypSizer akzeptieren, sondern das Ergebnis auch alsSizer zurückgeben. Dies bedeutet, dass wir nicht mehr einSquare oder einCircle zurückgeben, sondern die Schnittstelle vonSizer.

Schließlich drucken wir aus, was den kleinsten Bereich hatte:

Output{Width:5 Height:10} is the smallest

Als Nächstes fügen wir jedem Typ ein anderes Verhalten hinzu. Dieses Mal fügen wir dieString()-Methode hinzu, die eine Zeichenfolge zurückgibt. Dies erfüllt diefmt.Stringer-Schnittstelle:

main.go

package main

import (
    "fmt"
    "math"
)

type Circle struct {
    Radius float64
}

func (c Circle) Area() float64 {
    return math.Pi * math.Pow(c.Radius, 2)
}

func (c Circle) String() string {
    return fmt.Sprintf("Circle {Radius: %.2f}", c.Radius)
}

type Square struct {
    Width  float64
    Height float64
}

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

func (s Square) String() string {
    return fmt.Sprintf("Square {Width: %.2f, Height: %.2f}", s.Width, s.Height)
}

type Sizer interface {
    Area() float64
}

type Shaper interface {
    Sizer
    fmt.Stringer
}

func main() {
    c := Circle{Radius: 10}
    PrintArea(c)

    s := Square{Height: 10, Width: 5}
    PrintArea(s)

    l := Less(c, s)
    fmt.Printf("%v is the smallest\n", l)

}

func Less(s1, s2 Sizer) Sizer {
    if s1.Area() < s2.Area() {
        return s1
    }
    return s2
}

func PrintArea(s Shaper) {
    fmt.Printf("area of %s is %.2f\n", s.String(), s.Area())
}

Da sowohl derCircle- als auch derSquare-Typ sowohl dieArea- als auch dieString-Methode implementieren, können wir jetzt eine weitere Schnittstelle erstellen, um diesen breiteren Satz von Verhalten zu beschreiben. Dazu erstellen wir eine Schnittstelle mit dem NamenShaper. Wir werden dies aus derSizer-Schnittstelle und derfmt.Stringer-Schnittstelle zusammensetzen:

main.go

...
type Shaper interface {
    Sizer
    fmt.Stringer
}
...

[.note] #Note: Es wird als idiomatisch angesehen, zu versuchen, Ihre Schnittstelle zu benennen, indem Sie miter enden, z. B.fmt.Stringer,io.Writer usw. Aus diesem Grund haben wir unsere SchnittstelleShaper und nichtShape genannt.
#

Jetzt können wir eine Funktion namensPrintArea erstellen, die einShaper als Argument verwendet. Dies bedeutet, dass wir beide Methoden für den übergebenen Wert sowohl für die MethodeArea als auch für die MethodeString aufrufen können:

main.go

...
func PrintArea(s Shaper) {
    fmt.Printf("area of %s is %.2f\n", s.String(), s.Area())
}

Wenn wir das Programm ausführen, erhalten wir die folgende Ausgabe:

Outputarea of Circle {Radius: 10.00} is 314.16
area of Square {Width: 5.00, Height: 10.00} is 50.00
Square {Width: 5.00, Height: 10.00} is the smallest

Wir haben jetzt gesehen, wie wir kleinere Schnittstellen erstellen und bei Bedarf zu größeren aufbauen können. Wir hätten mit der größeren Schnittstelle beginnen und sie an alle unsere Funktionen übergeben können, aber es wird als bewährte Methode angesehen, nur die kleinste Schnittstelle an eine Funktion zu senden, die benötigt wird. Dies führt in der Regel zu klarerem Code, da alles, was eine bestimmte kleinere Schnittstelle akzeptiert, nur mit diesem definierten Verhalten funktionieren soll.

Wenn wir beispielsweiseShaper an die FunktionLess übergeben haben, können wir davon ausgehen, dass sowohl die MethodenArea als auchString aufgerufen werden. Da wir jedoch nur die MethodeArea aufrufen möchten, wird die FunktionLessklar, da wir wissen, dass wir nur die MethodeAreaeines an sie übergebenen Arguments aufrufen können.

Fazit

Wir haben gesehen, wie wir durch das Erstellen kleinerer Schnittstellen und deren Erweiterung auf größere nur das teilen können, was wir für eine Funktion oder Methode benötigen. Wir haben auch gelernt, dass wir unsere Schnittstellen aus anderen Schnittstellen zusammensetzen können, einschließlich derer, die aus anderen Paketen und nicht nur aus unseren Paketen definiert wurden.

Wenn Sie mehr über die Programmiersprache Go erfahren möchten, lesen Sie die gesamtenHow To Code in Go series.