Comment utiliser les interfaces dans Go

introduction

L'écriture de code flexible, réutilisable et modulaire est essentielle pour développer des programmes polyvalents. En travaillant de cette façon, le code est plus facile à gérer en évitant de devoir effectuer les mêmes modifications à plusieurs endroits. La façon dont vous accomplissez cela varie d'une langue à l'autre. Par exemple,inheritance est une approche courante utilisée dans des langages tels que Java, C ++, C #, etc.

Les développeurs peuvent également atteindre ces mêmes objectifs de conception grâce àcomposition. La composition est un moyen de combiner des objets ou des types de données en des types plus complexes. C'est l'approche que Go utilise pour promouvoir la réutilisation du code, la modularité et la flexibilité. Les interfaces de Go fournissent une méthode pour organiser des compositions complexes et apprendre à les utiliser vous permettra de créer du code commun réutilisable.

Dans cet article, nous allons apprendre à composer des types personnalisés ayant des comportements communs, ce qui nous permettra de réutiliser notre code. Nous allons également apprendre à implémenter des interfaces pour nos propres types personnalisés qui satisferont les interfaces définies à partir d’un autre package.

Définir un comportement

L'une des principales implémentations de la composition est l'utilisation d'interfaces. Une interface définit un comportement d'un type. L'une des interfaces les plus couramment utilisées dans la bibliothèque standard Go est l'interfacefmt.Stringer:

type Stringer interface {
    String() string
}

La première ligne de code définit untype appeléStringer. Il déclare alors qu'il s'agit d'uninterface. Tout comme la définition d'une structure, Go utilise des accolades ({}) pour entourer la définition de l'interface. Par rapport à la définition de structures, nous définissons uniquement lesbehavior de l’interface; c'est-à-dire «que peut faire ce type».

Dans le cas de l'interfaceStringer, le seul comportement est la méthodeString(). La méthode ne prend aucun argument et retourne une chaîne.

Ensuite, examinons un code qui a le comportementfmt.Stringer:

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

La première chose que nous faisons est de créer un nouveau type appeléArticle. Ce type a un champTitle et un champAuthor et tous deux sont de la chaînedata type:

main.go

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

Ensuite, nous définissons unmethod appeléString sur le typeArticle. La méthodeString retournera une chaîne qui représente le typeArticle:

main.go

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

Ensuite, dans nosmainfunction, nous créons une instance de typeArticle et l'affectons auxvariable appelésa. Nous fournissons les valeurs de"Understanding Interfaces in Go" pour le champTitle et de"Sammy Shark" pour le champAuthor:

main.go

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

Ensuite, nous imprimons le résultat de la méthodeString en appelantfmt.Println et en passant le résultat de l'appel de la méthodea.String():

main.go

...
fmt.Println(a.String())

Après avoir exécuté le programme, vous verrez la sortie suivante:

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

Jusqu'à présent, nous n'avons pas utilisé d'interface, mais nous avons créé un type ayant un comportement. Ce comportement correspondait à l'interfacefmt.Stringer. Voyons maintenant comment nous pouvons utiliser ce comportement pour rendre notre code plus réutilisable.

Définir une interface

Maintenant que notre type est défini avec le comportement souhaité, nous pouvons examiner comment utiliser ce comportement.

Avant de faire cela, cependant, voyons ce que nous aurions besoin de faire si nous voulions appeler la méthodeString à partir du typeArticle dans une fonction:

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

Dans ce code, nous ajoutons une nouvelle fonction appeléePrint qui prend unArticle comme argument. Notez que la seule chose que fait la fonctionPrint est d'appeler la méthodeString. Pour cette raison, nous pourrions définir une interface à transmettre à la fonction:

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

Ici, nous avons créé une interface appeléeStringer:

main.go

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

L'interfaceStringer n'a qu'une seule méthode, appeléeString() qui renvoie unstring. Unmethod est une fonction spéciale qui est étendue à un type spécifique dans Go. Contrairement à une fonction, une méthode ne peut être appelée qu'à partir de l'instance du type sur lequel elle a été définie.

Nous mettons ensuite à jour la signature de la méthodePrint pour prendre unStringer, et non un type concret deArticle. Comme le compilateur sait qu'une interfaceStringer définit la méthodeString, il n'acceptera que les types qui ont également la méthodeString.

Nous pouvons maintenant utiliser la méthodePrint avec tout ce qui satisfait l'interfaceStringer. Créons un autre type pour illustrer ceci:

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

Nous ajoutons maintenant un deuxième type appeléBook. Il a également défini la méthodeString. Cela signifie qu'il satisfait également l'interfaceStringer. Pour cette raison, nous pouvons également l'envoyer à notre fonctionPrint:

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.

Jusqu'à présent, nous avons montré comment utiliser une seule interface. Cependant, une interface peut avoir plus d'un comportement défini. Ensuite, nous verrons comment rendre nos interfaces plus polyvalentes en déclarant davantage de méthodes.

Comportements multiples dans une interface

L’un des principes de base de l’écriture du code Go consiste à écrire des types petits et concis et à les composer en types plus grands et plus complexes. La même chose est vraie lors de la composition des interfaces. Pour voir comment nous construisons une interface, commençons par définir une seule interface. Nous allons définir deux formes, aCircle etSquare, et elles définiront toutes deux une méthode appeléeArea. Cette méthode renverra la zone géométrique de leurs formes respectives:

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
}

Étant donné que chaque type déclare la méthodeArea, nous pouvons créer une interface qui définit ce comportement. Nous créons l'interfaceSizer suivante:

main.go

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

On définit ensuite une fonction appeléeLess qui prend deuxSizer et renvoie le plus petit:

main.go

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

Notez que nous acceptons non seulement les deux arguments comme typeSizer, mais nous renvoyons également le résultat sous forme deSizer. Cela signifie que nous ne retournons plus unSquare ou unCircle, mais l'interface deSizer.

Enfin, nous imprimons ce qui avait la plus petite surface:

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

Ensuite, ajoutons un autre comportement à chaque type. Cette fois, nous allons ajouter la méthodeString() qui renvoie une chaîne. Cela satisfera l'interfacefmt.Stringer:

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

Étant donné que les typesCircle etSquare implémentent à la fois les méthodesArea etString, nous pouvons maintenant créer une autre interface pour décrire cet ensemble plus large de comportements. Pour ce faire, nous allons créer une interface appeléeShaper. Nous allons composer ceci de l'interfaceSizer et de l'interfacefmt.Stringer:

main.go

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

[.note] #Note: Il est considéré idiomatique d'essayer de nommer votre interface en se terminant parer, commefmt.Stringer,io.Writer, etc. C'est pourquoi nous avons nommé notre interfaceShaper, et nonShape.
#

Nous pouvons maintenant créer une fonction appeléePrintArea qui prend unShaper comme argument. Cela signifie que nous pouvons appeler les deux méthodes sur la valeur transmise à la fois pour les méthodesArea etString:

main.go

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

Si nous exécutons le programme, nous recevrons le résultat suivant:

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

Nous avons maintenant vu comment nous pouvons créer des interfaces plus petites et les construire au besoin. Alors que nous aurions pu commencer avec une interface plus grande et la transmettre à toutes nos fonctions, il est considéré comme une pratique exemplaire de n’envoyer que la plus petite interface à une fonction nécessaire. Cela se traduit généralement par un code plus clair, car tout ce qui accepte une interface spécifique plus petite n'a pour intention que de fonctionner avec ce comportement défini.

Par exemple, si nous avons passéShaper à la fonctionLess, nous pouvons supposer qu'elle va appeler les méthodesArea etString. Cependant, puisque nous avons uniquement l'intention d'appeler la méthodeArea, cela rend la fonctionLess claire car nous savons que nous ne pouvons appeler la méthodeArea que de tout argument qui lui est passé.

Conclusion

Nous avons vu comment la création d'interfaces plus petites et la construction de plus grandes interfaces nous permettent de ne partager que ce dont nous avons besoin pour une fonction ou une méthode. Nous avons également appris que nous pouvons composer nos interfaces à partir d’autres interfaces, y compris celles définies à partir d’autres packages, et pas seulement de nos packages.

Si vous souhaitez en savoir plus sur le langage de programmation Go, consultez l'intégralité desHow To Code in Go series.