Inleiding
Het schrijven van flexibel, herhalbaar en modulair code is crucial voor het ontwikkelen van verschillende programma’s. Werken op deze manier zorgt ervoor dat de code gemakkelijker wordt onderhouden door het vermijden van het nodig zijn om dezelfde wijziging meerdere malen aan te brengen. Hoe u dit accomplishiert verschilt per taal. Bijvoorbeeld, erfenis is een algemeen aanpak die wordt gebruikt in talen als Java, C++, C# en meer.
Ontwikkelaars kunnen dezelfde ontwerpgoals ook behalen door compositie. Compositie is een manier om objecten of data typen samen te voegen in meer complexe typen. Dit is de aanpak die Go gebruikt om codehergebruik, modulariteit en flexibiliteit te promoten. Interfaces in Go verschaffen een methode om complexe composities te organiseren, en het leren van hoe ze te gebruiken zal u toestaan om algemeen herhalbare code te maken.
In dit artikel zullen we leren hoe we eigen types kunnen componeren met algemeen gedrag, wat ons toestaat ons code te herhalen. We zullen ook leren hoe we interfaces kunnen implementeren voor onze eigen aangepaste typen die interfaces definiëren vanuit een andere pakket.
Definiëren van een Gedrag
Eén van de kernimplementaties van compositie is het gebruik van interfaces. Een interface definieert een gedrag van een type. Een van de meest gebruikte interfaces in de standaardbibliotheek van Go is de fmt.Stringer
interface:
De eerste lijn van code definieert een type
genaamd Stringer
. Het stelt vervolgens vast dat het een interface
is. Net zoals bij het definiëren van een struct, gebruikt Go haken ({}
) om de definitie van het interface omheen te zetten. In vergelijking met het definiëren van structs, definiëren we alleen het gedrag van het interface; d.w.z., “wat kan dit type doen”.
In het geval van de Stringer
interface, is het enige gedrag de String()
methode. Deze methode neemt geen argumenten en geeft een string terug.
Vervolgens laten we kijken naar wat code die het fmt.Stringer
gedrag heeft:
Het eerste dat we doen is het maken van een nieuw type genaamd Artikel
. Dit type heeft een Titel
en een Auteur
veld, en beide zijn van het type string:
Vervolgens definiëren we een methode
genaamd String
op het type Artikel
. De String
methode zal een string teruggeven die het type Artikel
representeert:
Daarom, in onze main
functie, maken we een instantie van het Article
type en stellen we hem toe aan de variabele genaamd a
. We geven de waarden van "Onderstanding Interfaces in Go"
voor het veld Title
en "Sammy Shark"
voor het veld Author
:
Dan printen we het resultaat uit de methode String
door fmt.Println
te gebruiken en door het resultaat van de methode a.String()
te overdragen:
Na het runnen van het programma zien we de volgende uitvoer:
OutputThe "Understanding Interfaces in Go" article was written by Sammy Shark.
Tot nu toe hebben we nog geen interface gebruikt, maar wel een type gemaakt dat een behoorlijkheid had. Dat gedrag matchede met de fmt.Stringer
interface. Nu laten we zien hoe we die behoorlijkheid kunnen gebruiken om onze code meer hergebruikbaar te maken.
Defineren van een Interface
Nu dat we onze type definiet hebben met gewoontelijke gedrag, kijken we naar hoe we dat gedrag kunnen gebruiken.
Maar voordat we dat doen, lees je wel wat we moeten doen als we de String
-methode van de Article
-type in een functie wouden willen invoegen:
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 deze code voegen we een nieuwe functie toe genaamd Print
die een Article
als argument accepteert. Bekijk dat de enige ding die de Print
-functie doet is de String
-methode aanroepen. Omdat er maar één methode is met de naam String
, kunnen we ook een interface definieren om te passeren aan de functie:
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 hebben we een interface gecreerd genaamd Stringer
:
De Stringer
-interface heeft slechts één methode, genaamd String()
die terug een string
retourneert. Een methode is een speciaal soort functie in Go die gedefinieerd is voor een specifieke type. Verschillend van een functie, kan een methode alleen worden aangeroepen vanuit een instantie van het type waarin ze is gedefinieerd.
Wij updaten de signatuur van de Print
-methode om een Stringer
te accepteren, en niet een concreet type van Article
. Omwille van de compiler weten dat een Stringer
-interface de String
-methode definieert, zal hij alleen accepteren dat typen die ook de String
-methode hebben.
Nu kunnen we de Print
-methode gebruiken met alles wat de Stringer
-interface vervult. Laten we een tweede type aanbrengen om dit te demonstreren:
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())
}
We maken nu een tweede type genaamd Book
. Het heeft ook de String
-methode gedefinieerd. Dankzij dit beschikt het ook over de Stringer
-interface. Omwille van deze reden kunnen we ook Print
aanroepen met een object van Book
:
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.
Tot nu toe hebben we gezegd hoe je een enkele interface kunt gebruiken. Een interface kan echter meer dan één gedefinieerd gedrag hebben. Nu zien we hoe je je interfaces meer flexibel kunt maken door meer methode declaraties te definieren.
Meerdere gedrag in een interface
Een van de fundamentele tenetten van het schrijven van Go-code is om kleine, concise typen te schrijven en ze te componeren tot groter, meer complexe typen. Dezelfde principes gelden voor het samenstellen van interfaces. Om te zien hoe je interfaces bouwt, beginnen we eerst met een interface die alleen één methode definieert. We definieren twee vormen, een Circle
en een Square
, die elk een methode genaamd Area
definieren. Deze methode zal teruggeven wat de geometrische oppervlakte van hun respectieve vormen is:
Omdat elk type de Area
methode declareert, kunnen we een interface definieren dat die behavior definieert. We definieren de volgende Sizer
interface:
We definieren een functie genaamd Less
die twee Sizer
’s accepteert en de kleinste teruggeeft:
Zichtbaar is dat we niet alleen beide argumenten als het type Sizer
accepteren, maar ook de resultaat teruggeven als Sizer
ook. Dit betekent dat we niet meer een Square
of een Circle
teruggeven, maar de interface van Sizer
.
Uiteindelijk print je uit wat het kleinste oppervlak was:
Output{Width:5 Height:10} is the smallest
Vervolgens laten we aan elk type nog een gedrag toevoegen. Deze keer voegen we de methode `String()` toe die een string teruggeeft. Dit zal de `fmt.Stringer` interface aanvolden:
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())
}
Omdat zowel het type `Circle` als het type `Square` beide methodes `Area` en `String` implementeren, kunnen we nu een andere interface aanmaken om dat bredere set van gedrag te beschrijven. Om dit te doen, maken we een interface genaamd `Shaper`. We zullen dit samenstellen uit de interface `Sizer` en de interface `fmt.Stringer`:
Notitie: Het is gebruikelijk om uw interface naam te eindigen op `er`, zoals `fmt.Stringer`, `io.Writer`, enzovoort. Daarom hebben we ons interface `Shaper` genoemd in plaats van `Shape`.
Nu kunnen we een functie aanmaken genaamd `PrintArea` die een `Shaper` als argument neemt. Dit betekend dat we aan het doorgegeven waarden beide methodes kunnen aanroepen voor de methode `Area` en `String`:
Als we het programma uitvoeren, krijgen we de volgende uitvoer:
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
We hebben nu gezien hoe we kleinere interfaces kunnen maken en ze opbouwen tot grotere interfaces als nodig. Hoewel we konden beginnen met de grotere interface en die aan alle onze functies doorsturen, wordt het geacht de beste praktijk om maar de kleinste interface naar een functie te sturen die nodig is. Dit resulteert typisch in duidelijkere code, aangezien er aanvaard wordt dat iets dat een specifieke kleinere interface accepteert alleen met dat gedefinieerde gedrag werkt.
Bijvoorbeeld, als we Shaper
doorsturen naar de functie Less
, kunnen we aannemen dat het beide de Area
en String
methoden gaat aanroepen. Echter, omdat we alleen de Area
methoden willen aanroepen, maakt de functie Less
duidelijker, want we weten dat we enkel de Area
methoden van elk argument kunnen aanroepen dat aan hem wordt doorgegeven.
Conclusie
We hebben gezien hoe het mogelijk is om kleinere interfaces te maken en ze op te bouwen tot grotere interfaces, waardoor we alleen maar de nodige informatie kunnen delen met een functie of methode. We hebben ook geleerd dat we onze interfaces kunnen samenstellen uit andere interfaces, inclusief die die zijn gedefinieerd in andere pakketten, niet alleen in onze eigen pakketten.
Wilt u meer leren over de programmeertaal Go? Bekijk dan de volledige reeks How To Code in Go.
Source:
https://www.digitalocean.com/community/tutorials/how-to-use-interfaces-in-go