Wie man Schnittstellen in Go verwendet

Einführung

Die Schreibung flexibler, wiederverwendbarer und modularer Code ist entscheidend für die Entwicklung flexibler Programme. Arbeiten in diesem Stil gewährleistet, dass der Code einfacher zu maintainen ist, indem es notwendig ist, nicht gleiche Änderungen an mehreren Stellen durchzuführen. Wie Sie dies erreichen, hängt von der Sprache ab. Zum Beispiel verwendet erben eine häufig angewandte Methode, die in Sprachen wie Java, C++, C# und mehr verwendet wird.

Entwickler können dieselben Designziele auch durch Zusammensetzung erreichen. Zusammensetzung ist eine Methode, um Objekte oder Datentypen in komplexere zu kombinieren. Dies ist die Methode, die Go verwendet, um Codeerneuerung, Modularität und Flexibilität zu fördern. Interfaces in Go stellen eine Art Organisierungsverfahren für komplexe Zusammensetzungen bereit, und das Erlernen deren Nutzung ermöglicht Ihnen, häufig verwendbarer, wiederverwendbarer Code zu schreiben.

In diesem Artikel werden wir lernen, wie man eigene benutzerdefinierte Typen zusammensetzt, die gemeinsame Verhaltensweisen aufweisen, was uns die Wiederverwendung unseres Codes ermöglicht. Wir werden auch lernen, wie man Interfaces für eigene benutzerdefinierte Typen implementiert, die Interfaces anderer Pakete erfüllen.

Definition eines Verhaltens

Eine der Kernimplementierungen von Komposition ist die Verwendung von Schnittstellen. Eine Schnittstelle definiert das Verhalten eines Typs. Eine der am meisten verwendeten Schnittstellen in der Go-Standardbibliothek ist die fmt.Stringer-Schnittstelle:

type Stringer interface {
    String() string
}

Der erste Zeile des Codes definiert ein type namens Stringer. Es ist dann eine interface. Genau wie bei der Definition eines Structs, verwendet Go die geschweiften Klammern ({}), um die Definition der Schnittstelle zu umschließen. Im Vergleich zur Definition von Structs definieren wir lediglich das Verhalten der Schnittstelle; d.h., „was kann dieser Typ tun“.

Bei der Stringer-Schnittstelle ist das einzige Verhalten die String()-Methode. Diese Methode nimmt keine Argumente entgegen und gibt eine Zeichenfolge zurück.

Nun sehen wir ein Beispielcode, der die fmt.Stringer-Schnittstelle aufweist:

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())
}

Der erste Schritt besteht darin, einen neuen Typ namens Article zu erstellen. Dieser Typ hat ein Title– und ein Author-Feld und beide sind vom Typ String Datentyp:

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

Danach definieren wir eine Methode namens String auf dem Typ Article. Die String-Methode wird eine Zeichenfolge zurückgeben, die das Article-Typ repräsentiert:

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 unserem mainFunktion eine Instanz des Typs Article und stellen sie der Variable a zu. Wir geben die Werte "Understanding Interfaces in Go" für das Feld Title und "Sammy Shark" für das Feld Author ein:Danach drucken wir das Ergebnis aus dem String-Methode aus, indem wir fmt.Println mit dem Ergebnis der a.String()-Methode aufrufen:

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

Nachdem Sie das Programm ausführen, sehen Sie folgenden Output:

main.go
...
fmt.Println(a.String())

Bisher haben wir noch keine Interface verwendet, aber wir haben einen Typ definiert, der ein Verhalten hatte. Dieses Verhalten passt zur fmt.Stringer-Interface. Nächstens schauen wir uns an, wie wir dieses Verhalten nutzen können, um unser Code mehr flexibel zu machen.

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

Definition einer Interface

Nun haben wir unser Typ definiert mit dem gewünschten Verhalten, jetzt schauen wir an, wie wir dieses Verhalten nutzen können.

Zum Anfang müssen wir festlegen, was wir brauchen, um die String-Methode von dem Typ Article in einer Funktion zu调用:

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 schreiben wir eine neue Funktion namens Print, die einen Artikel als Argument akzeptiert. Beachten Sie, dass die Print-Funktion nur die String-Methode aufruft. Daher könnten wir stattdessen eine Schnittstelle definieren, um sie zu übergeben:

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 namens Stringer definiert:

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

Die Stringer-Schnittstelle hat nur eine Methode, die String() heißt und eine string zurückgibt. Eine Methode ist eine spezielle Funktion, die im Rahmen einer bestimmten Typdefinition in Go definiert ist. Im Gegensatz zu einer Funktion kann eine Methode nur von Instanzen des Typs, für den sie definiert wurde, aufgerufen werden.

Wir updaten dann die Signatur der Print-Methode, um eine Stringer zu akzeptieren, und nicht eine konkrete Typdefinition von Artikel. Weil der Compiler weiß, dass eine Stringer-Schnittstelle die String-Methode definiert, wird er nur Typen akzeptieren, die auch die String-Methode haben.

Nun können wir die Print-Methode mit allen Typen verwenden, die die Stringer-Schnittstelle erfüllen. Schauen wir uns ein zweites Beispiel an:

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 erstellen jetzt eine zweite Typdefinition namens Buch. Sie hat ebenfalls die String-Methode definiert. Dies bedeutet, dass sie auch die Stringer-Schnittstelle erfüllt. Daher können wir sie auch an unsere Print-Funktion senden:

Output
The "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 ein einziges Interface verwenden kann. Allerdings kann ein Interface mehr als eine Behandlung definieren. In diesem Abschnitt schauen wir uns an, wie wir unsere Interfaces mehr flexibel machen können, indem wir mehrere Methoden deklarieren.

Mehrere Verhalten in einem Interface

Ein zentraler Grundsatz beim Schreiben von Go-Code ist es, kleine, konzise Typen zu schreiben und sie dann zusammenzusetzen, um komplexere, größere Typen zu erstellen. Das gleiche gilt auch für die Komposition von Interfacen. Um zu sehen, wie wir einen Interface aufbauen, beginnen wir zunächst mit dem Definieren nur eines Interfaces. Wir definieren zwei Formen, eine Circle und eine Square, die jeweils eine Methode namens Area definieren, die den geometrischen Bereich ihrer jeweiligen Formen zurückgibt:

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 Area-Methode definiert, können wir eine Schnittstelle definieren, die dieses Verhalten definiert. Wir definieren folgendes Sizer-Interface:

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

Wir definieren dann eine Funktion namens Less, die zwei Sizer als Argumente akzeptiert und das kleinste als Ergebnis zurückgibt:

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

Aufmerksamkeit merken Sie, dass wir nicht nur beide Argumente vom Typ Sizer akzeptieren, sondern auch das Ergebnis als Sizer zurückgeben. Dies bedeutet, dass wir nicht mehr einen Square oder einen Circle zurückgeben, sondern das Interface von Sizer.

Schließlich werden wir ausgeben, was den kleinsten Bereich hatte:

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

Nun werden wir jedem Typ ein weiteres Verhalten hinzufügen. Diesmal werden wir die Methode String() hinzufügen, die eine Zeichenkette zurückgibt. Dies erfüllt die fmt.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())
}

Weil sowohl der Typ Circle als auch der Typ Square sowohl die Methode Area als auch die Methode String implementieren, können wir nun eine weitere Schnittstelle erstellen, um dieses breitere Verhalten zu beschreiben. Um dies zu tun, erstellen wir eine Schnittstelle namens Shaper. Wir komponieren diese aus der Sizer-Schnittstelle und der fmt.Stringer-Schnittstelle:

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

Hinweis: Es ist idiomatisch, Ihre Schnittstelle am Ende mit er zu beenden, wie z.B. fmt.Stringer, io.Writer, usw. Deshalb haben wir unsere Schnittstelle Shaper und nicht Shape benannt.

Nun können wir eine Funktion namens PrintArea definieren, die ein Shaper als Argument nimmt. Dies bedeutet, dass wir sowohl die Methode Area als auch die Methode String auf das übergebene Wert 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:

Output
area 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 nun gesehen, wie wir kleinere Schnittstellen erstellen und aus ihnen größere aufbauen können, wie wir es benötigen. Obwohl wir die größere Schnittstelle gleich von Anfang an verwendet und ihr all unsere Funktionen übergeben hätten können, ist es eine gute Praxis, nur die kleinste Schnittstelle an eine Funktion zu senden, die benötigt wird. Dies führt typischerweise zu klareren Code, da jede Schnittstelle, die eine bestimmte kleinere Schnittstelle akzeptiert, nur dafür intendiert ist, mit diesem definierten Verhalten zu arbeiten.

Beispielsweise, wenn wir Shaper an die Funktion Less übergeben, könnten wir davon ausgehen, dass sowohl der Area als auch der String-Methode aufgerufen werden wird. Allerdings, da wir nur den Area-Methode aufrufen wollen, macht die Funktion Less deutlicher, da wir wissen, dass wir nur den Area-Methode von jedem Argument aufrufen können, das ihr zugeführt wird.

Fazit

Wir haben erkannt, wie die Erstellung kleinerer Schnittstellen und deren Aufbau zu größeren Schnittstellen uns erlaubt, nur das zu teilen, was wir einer Funktion oder Methode notwendig haben. Wir haben auch gelernt, dass wir unsere Schnittstellen aus anderen Schnittstellen zusammenstellen können, einschließlich solcher, die aus anderen Paketen definiert sind, nicht nur aus unseren Paketen.

Wenn Sie mehr über die Go-Programmiersprache erfahren möchten, schauen Sie sich die gesamte How To Code in Go Reihe an.

Source:
https://www.digitalocean.com/community/tutorials/how-to-use-interfaces-in-go