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.Stringer
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())
}
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 TypArticle
darstellt:
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 unserenmain
function 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 MethodeString
vom TypArticle
in 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 FunktionPrint
nur die MethodeString
aufruft. 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 MethodeString
definiert. 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 MethodeArea
deklariert, 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 FunktionLess
klar, da wir wissen, dass wir nur die MethodeArea
eines 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.