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:
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:
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:
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:
Dann erstellen wir in unserem main
–Funktion 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:
Nachdem Sie das Programm ausführen, sehen Sie folgenden Output:
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.
OutputThe "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调用:
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:
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:
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:
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:
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 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:
Da jeder Typ die Area
-Methode definiert, können wir eine Schnittstelle definieren, die dieses Verhalten definiert. Wir definieren folgendes Sizer
-Interface:
Wir definieren dann eine Funktion namens Less
, die zwei Sizer
als Argumente akzeptiert und das kleinste als Ergebnis zurückgibt:
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:
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:
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:
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 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