Hoe je interfaces in Go kunt gebruiken

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:

type Stringer interface {
    String() string
}

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:

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

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:

main.go
...
type Article struct {
	Title string
	Author 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:

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

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:

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

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:

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

Na het runnen van het programma zien we de volgende uitvoer:

Output
The "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:

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 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:

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 hebben we een interface gecreerd genaamd Stringer:

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

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:

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

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:

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.

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:

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
}

Omdat elk type de Area methode declareert, kunnen we een interface definieren dat die behavior definieert. We definieren de volgende Sizer interface:

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

We definieren een functie genaamd Less die twee Sizer’s accepteert en de kleinste teruggeeft:

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

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:

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

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`:

main.go
...
type Shaper interface {
	Sizer
	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`:

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

Als we het programma uitvoeren, krijgen we de volgende uitvoer:

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

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