Utiliser des interfaces dans Go

Effectué

Les interfaces dans Go sont un type de données utilisé pour représenter le comportement d’autres types. Une interface est comme un blueprint ou un contrat qu’un objet doit satisfaire. Quand vous utilisez des interfaces, votre base de code devient plus flexible et plus adaptable, car vous écrivez du code qui n’est pas lié à une implémentation particulière. Vous pouvez ainsi étendre rapidement les fonctionnalités d’un programme. Vous allez en comprendre la raison dans ce module.

Contrairement aux interfaces dans d’autres langages de programmation, les interfaces dans Go sont satisfaites implicitement. Go ne fournit pas de mots clés pour implémenter une interface. Si vous connaissez les interfaces dans d’autres langages de programmation et que vous débutez avec Go, cette idée peut être déroutante.

Dans ce module, nous utilisons plusieurs exemples pour explorer les interfaces dans Go et montrer comment en tirer le meilleur parti.

Déclarer une interface

Une interface dans Go est semblable à un blueprint. C’est un type abstrait qui inclut seulement les méthodes qu’un type concret doit posséder ou implémenter.

Supposons que vous voulez créer une interface dans votre package de géométrie, qui indique les méthodes qu’une forme doit implémenter. Vous pouvez définir une interface comme celle-ci :

type Shape interface {
    Perimeter() float64
    Area() float64
}

L’interface Shape signifie que les types dont vous voulez qu’ils prennent en compte Shape doivent avoir à la fois les méthodes Perimeter() et Area(). Par exemple, quand vous créez un struct Square, il doit implémenter les deux méthodes, et non pas seulement une des deux. Notez aussi qu’une interface ne contient pas les détails d’implémentation de ces méthodes (par exemple pour calculer le périmètre et la surface d’une forme). Il s’agit simplement d’un contrat. Les formes comme les triangles, les cercles et les carrés nécessitent des calculs différents pour leur surface et leur périmètre.

Implémenter une interface

Comme nous l’avons vu précédemment, dans Go, vous n’avez pas de mot clé pour implémenter une interface. Une interface dans Go est satisfaite implicitement par un type quand il a toutes les méthodes demandées par une interface.

Créons un struct Square qui a les deux méthodes de l’interface Shape, comme le montre l’exemple de code suivant :

type Square struct {
    size float64
}

func (s Square) Area() float64 {
    return s.size * s.size
}

func (s Square) Perimeter() float64 {
    return s.size * 4
}

Notez que la signature de la méthode du struct Square correspond à la signature de l’interface Shape. Cependant, une autre interface peut avoir un nom différent, mais les mêmes méthodes. Comment ou quand Go sait-il quelle interface est implémentée par un type concret ? Go le sait quand vous l’utilisez, au moment de l’exécution.

Pour montrer comment les interfaces sont utilisées, vous pourriez écrire le code ci-dessous :

func main() {
    var s Shape = Square{3}
    fmt.Printf("%T\n", s)
    fmt.Println("Area: ", s.Area())
    fmt.Println("Perimeter:", s.Perimeter())
}

Quand vous exécutez le programme précédent, vous obtenez la sortie suivante :

main.Square
Area:  9
Perimeter: 12

À ce stade, que vous utilisiez ou non une interface, il n’y a aucune différence. Nous allons créer un autre type, comme Circle, puis expliquer pourquoi les interfaces sont utiles. Voici le code pour le struct Circle :

type Circle struct {
    radius float64
}

func (c Circle) Area() float64 {
    return math.Pi * c.radius * c.radius
}

func (c Circle) Perimeter() float64 {
    return 2 * math.Pi * c.radius
}

À présent, nous allons refactoriser la fonction main() et créer une fonction pour imprimer le type de l’objet qu’elle reçoit, ainsi que sa surface et son périmètre, comme suit :

func printInformation(s Shape) {
    fmt.Printf("%T\n", s)
    fmt.Println("Area: ", s.Area())
    fmt.Println("Perimeter:", s.Perimeter())
    fmt.Println()
}

Notez que la fonction printInformation a Shape comme paramètre. Vous pouvez envoyer un objet Square ou Circle à cette fonction, ce qui fonctionne même si la sortie sera différente. Votre fonction main() se présente maintenant comme suit :

func main() {
    var s Shape = Square{3}
    printInformation(s)

    c := Circle{6}
    printInformation(c)
}

Notez que pour l’objet c, nous ne spécifions pas qu’il s’agit d’un objet Shape. Cependant, la fonction printInformation attend un objet qui implémente les méthodes qui sont définies dans l’interface Shape.

Quand vous exécutez le programme, vous obtenez normalement la sortie suivante :

main.Square
Area:  9
Perimeter: 12

main.Circle
Area:  113.09733552923255
Perimeter: 37.69911184307752

Notez que vous ne recevez pas d’erreur et que la sortie varie selon le type d’objet qu’elle reçoit. Vous pouvez aussi voir que le type d’objet dans la sortie n’indique rien quant à l’interface Shape.

L’avantage de l’utilisation des interfaces est que, pour chaque nouveau type ou implémentation de Shape, la fonction printInformation ne doit pas être modifiée. Comme nous l’avons dit précédemment, votre code devient plus flexible et plus facile à étendre quand vous utilisez des interfaces.

Implémenter une interface Stringer

Un exemple simple d’extension d’une fonctionnalité existante est d’utiliser Stringer, qui est une interface avec une méthode String(), comme ceci :

type Stringer interface {
    String() string
}

La fonction fmt.Printf utilise cette interface pour afficher des valeurs. Vous pouvez alors écrire votre méthode String() personnalisée pour afficher une chaîne personnalisée, comme ceci :

package main

import "fmt"

type Person struct {
    Name, Country string
}

func (p Person) String() string {
    return fmt.Sprintf("%v is from %v", p.Name, p.Country)
}
func main() {
    rs := Person{"John Doe", "USA"}
    ab := Person{"Mark Collins", "United Kingdom"}
    fmt.Printf("%s\n%s\n", rs, ab)
}

Quand vous exécutez le programme précédent, vous obtenez la sortie suivante :

John Doe is from USA
Mark Collins is from United Kingdom

Comme vous pouvez le voir, vous avez utilisé un type personnalisé (un struct) pour écrire une version personnalisée de la méthode String(). Cette technique est une façon courante d’implémenter une interface dans Go. Vous en trouverez des exemples dans de nombreux programmes, comme nous allons le voir.

Étendre une implémentation existante

Supposons que vous avez le code suivant et que vous voulez étendre ses fonctionnalités en écrivant une implémentation personnalisée d’une méthode Writer qui est chargée de manipuler des données.

En utilisant le code suivant, vous pouvez créer un programme qui consomme l’API GitHub pour obtenir trois dépôts auprès de Microsoft :

package main

import (
    "fmt"
    "io"
    "net/http"
    "os"
)

func main() {
    resp, err := http.Get("https://api.github.com/users/microsoft/repos?page=15&per_page=3")
    if err != nil {
        fmt.Println("Error:", err)
        os.Exit(1)
    }

    io.Copy(os.Stdout, resp.Body)
}

Quand vous exécutez le code précédent, vous obtenez un résultat semblable à celui-ci (raccourci pour une meilleure lisibilité) :

[{"id":276496384,"node_id":"MDEwOlJlcG9zaXRvcnkyNzY0OTYzODQ=","name":"-Users-deepakdahiya-Desktop-juhibubash-test21zzzzzzzzzzz","full_name":"microsoft/-Users-deepakdahiya-Desktop-juhibubash-test21zzzzzzzzzzz","private":false,"owner":{"login":"microsoft","id":6154722,"node_id":"MDEyOk9yZ2FuaXphdGlvbjYxNTQ3MjI=","avatar_url":"https://avatars2.githubusercontent.com/u/6154722?v=4","gravatar_id":"","url":"https://api.github.com/users/microsoft","html_url":"https://github.com/micro
....

Notez que l’appel io.Copy(os.Stdout, resp.Body) est celui qui imprime sur le terminal le contenu obtenu de l’appel à l’API GitHub. Supposons que vous voulez écrire votre propre implémentation pour raccourcir le contenu que vous voyez dans le terminal. Quand vous examinez la source de la fonction io.Copy, vous voyez :

func Copy(dst Writer, src Reader) (written int64, err error)

Si vous examinez plus en détail le premier paramètre, dst Writer, vous remarquez que Writer est une interface :

type Writer interface {
    Write(p []byte) (n int, err error)
}

Vous pouvez continuer à explorer le code source du package io afin de trouver l’emplacement où Copy appelle la méthode Write, mais nous laissons cela pour l’instant.

Comme Writer est une interface et qu’il s’agit d’un objet attendu par la fonction Copy, vous pouvez écrire votre implémentation personnalisée de la méthode Write. Par conséquent, vous pouvez personnaliser le contenu que vous affichez sur le terminal.

La première chose dont vous avez besoin pour implémenter une interface est de créer un type personnalisé. Dans le présent, vous pouvez créer un struct vide, car vous devez simplement écrire votre méthode Write personnalisée, comme suit :

type customWriter struct{}

Vous êtes maintenant prêt à écrire votre fonction Write personnalisée. Vous devez également écrire un struct pour analyser la réponse de l’API au format JSON dans un objet Golang. Vous pouvez utiliser le site JSON-to-Go pour créer un struct à partir d’une charge utile JSON. La méthode Write peut donc se présenter comme suit :

type GitHubResponse []struct {
    FullName string `json:"full_name"`
}

func (w customWriter) Write(p []byte) (n int, err error) {
    var resp GitHubResponse
    json.Unmarshal(p, &resp)
    for _, r := range resp {
        fmt.Println(r.FullName)
    }
    return len(p), nil
}

Enfin, vous devez modifier la fonction main() pour qu’elle utilise votre objet personnalisé, comme suit :

func main() {
    resp, err := http.Get("https://api.github.com/users/microsoft/repos?page=15&per_page=5")
    if err != nil {
        fmt.Println("Error:", err)
        os.Exit(1)
    }

    writer := customWriter{}
    io.Copy(writer, resp.Body)
}

Quand vous exécutez le programme, vous obtenez normalement la sortie suivante :

microsoft/aed-blockchain-learn-content
microsoft/aed-content-nasa-su20
microsoft/aed-external-learn-template
microsoft/aed-go-learn-content
microsoft/aed-learn-template

La sortie est maintenant améliorée, grâce à la méthode Write personnalisée que vous avez écrite. Voici la version finale du programme :

package main

import (
    "encoding/json"
    "fmt"
    "io"
    "net/http"
    "os"
)

type GitHubResponse []struct {
    FullName string `json:"full_name"`
}

type customWriter struct{}

func (w customWriter) Write(p []byte) (n int, err error) {
    var resp GitHubResponse
    json.Unmarshal(p, &resp)
    for _, r := range resp {
        fmt.Println(r.FullName)
    }
    return len(p), nil
}

func main() {
    resp, err := http.Get("https://api.github.com/users/microsoft/repos?page=15&per_page=5")
    if err != nil {
        fmt.Println("Error:", err)
        os.Exit(1)
    }

    writer := customWriter{}
    io.Copy(writer, resp.Body)
}

Écrire une API serveur personnalisée

Pour finir, nous allons explorer un autre cas d’utilisation des interfaces que vous pouvez trouver utile si vous créez une API serveur. La façon habituelle d’écriture un serveur web consiste à utiliser l’interface http.Handler du package net/http, qui se présente comme ceci (vous n’avez pas à écrire ce code) :

package http

type Handler interface {
    ServeHTTP(w ResponseWriter, r *Request)
}

func ListenAndServe(address string, h Handler) error

Notez que la fonction ListenAndServe attend une adresse de serveur, comme http://localhost:8000, et une instance de Handler qui distribue la réponse de l’appel à l’adresse du serveur.

Créons et explorons le programme suivant :

package main

import (
    "fmt"
    "log"
    "net/http"
)

type dollars float32

func (d dollars) String() string {
    return fmt.Sprintf("$%.2f", d)
}

type database map[string]dollars

func (db database) ServeHTTP(w http.ResponseWriter, req *http.Request) {
    for item, price := range db {
        fmt.Fprintf(w, "%s: %s\n", item, price)
    }
}

func main() {
    db := database{"Go T-Shirt": 25, "Go Jacket": 55}
    log.Fatal(http.ListenAndServe("localhost:8000", db))
}

Avant d’explorer le code précédent, nous allons l’exécuter comme ceci :

go run main.go

Si vous n’obtenez aucune sortie, c’est bon signe. À présent, ouvrez http://localhost:8000 dans une nouvelle fenêtre de navigateur ou, dans votre terminal, exécutez la commande suivante :

curl http://localhost:8000

Vous devez maintenant obtenir la sortie suivante :

Go T-Shirt: $25.00
Go Jacket: $55.00

Passons en revue avec attention le code précédent pour comprendre ce qu’il fait et pour observer la puissance des interfaces Go. Tout d’abord, vous commencez par créer un type personnalisé pour un type float32, avec l’idée d’écrire une implémentation personnalisée de la méthode String(), que vous utilisez ultérieurement.

type dollars float32

func (d dollars) String() string {
    return fmt.Sprintf("$%.2f", d)
}

Nous avons ensuite écrit l’implémentation de la méthode ServeHTTP que le http.Handler peut utiliser. Notez comment nous avons à nouveau créé un type personnalisé, mais il s’agit cette fois-ci d’un mappage et non pas d’un struct. Ensuite, nous avons écrit la méthode ServeHTTP en utilisant le type de database comme récepteur. L’implémentation de cette méthode utilise les données du récepteur, effectue une boucle sur celles-ci, puis affiche chaque élément de données.

type database map[string]dollars

func (db database) ServeHTTP(w http.ResponseWriter, req *http.Request) {
    for item, price := range db {
        fmt.Fprintf(w, "%s: %s\n", item, price)
    }
}

Enfin, dans la fonction main(), nous avons instancié un type database et nous l’avons initialisé avec certaines valeurs. Nous avons démarré le serveur HTTP en utilisant la fonction http.ListenAndServe, où nous avons défini l’adresse du serveur, y compris le port à utiliser et l’objet db qui implémente une version personnalisée de la méthode ServeHTTP. Quand vous exécutez le programme, Go utilise votre implémentation de cette méthode. C’est ainsi que vous utilisez et que vous implémentez une interface dans une API de serveur.

func main() {
    db := database{"Go T-Shirt": 25, "Go Jacket": 55}
    log.Fatal(http.ListenAndServe("localhost:8000", db))
}

Vous pouvez trouver un autre cas d’usage pour les interfaces dans une API serveur quand vous utilisez la fonction http.Handle. Pour plus d’informations, consultez le billet Écrire des applications web sur le site Go.