Le mot-clé interface déclare un ensemble de méthodes. Tout type qui possède ces méthodes satisfait automatiquement l'interface. Il n'existe ni mot-clé implements ni liste d'interfaces sur le type, si bien qu'un struct écrit des années avant l'interface peut tout de même la satisfaire. Depuis Go 1.18, une interface peut aussi lister des types en plus des méthodes, ce qui en fait une contrainte pour le code générique (Go spec).
En bref
type Notifier interface { Notify(...) error }déclare une interface. Tout type doté d'une méthodeNotifycorrespondante est unNotifier.- La satisfaction est implicite.
var _ Notifier = (*SlackNotifier)(nil)demande au compilateur de la vérifier au moment du build. - Une méthode avec un récepteur pointeur appartient à
*T, pas àT. Affecter unTéchoue avecmethod Notify has pointer receiver. - Gardez des interfaces petites et déclarez-les dans le package qui les utilise. Acceptez des interfaces, renvoyez des types concrets.
anyest un alias deinterface{}et contient une valeur de n'importe quel type. Récupérez la valeur avecv, ok := x.(T)ou un type switch.- Une interface ne vaut
nilque si son type et sa valeur sont tous deux non définis. Renvoyer un*ValidationErrornil en tant qu'errorvous donne unerr != nilqui vauttrue. - Les interfaces avec des listes de types comme
~int64 | ~float64sont des contraintes. Elles fonctionnent dans les listes de paramètres de type et nulle part ailleurs.
Comment déclarer une interface en Go ?
Écrivez type, un nom et interface suivi des signatures de méthodes. Voici un système d'alertes qui envoie le même message sur Slack et par e-mail :
example.gogopackage main import ( "context" "fmt" ) type Notifier interface { Notify(ctx context.Context, msg string) error } type SlackNotifier struct { Channel string } func (s SlackNotifier) Notify(ctx context.Context, msg string) error { fmt.Printf("slack #%s: %s\n", s.Channel, msg) return nil } type EmailNotifier struct { To string } func (e EmailNotifier) Notify(ctx context.Context, msg string) error { fmt.Printf("email %s: %s\n", e.To, msg) return nil } func alertAll(ctx context.Context, notifiers []Notifier, msg string) { for _, n := range notifiers { if err := n.Notify(ctx, msg); err != nil { fmt.Println("notify failed:", err) } } } func main() { notifiers := []Notifier{ SlackNotifier{Channel: "oncall"}, EmailNotifier{To: "[email protected]"}, } alertAll(context.Background(), notifiers, "payment API p99 above 2s") }
example.texttextslack #oncall: payment API p99 above 2s email [email protected]: payment API p99 above 2s
alertAll ne connaît que la méthode Notify. Peu lui importe de s'adresser à Slack, à l'e-mail ou à un client PagerDuty que quelqu'un ajoutera le mois prochain. Chaque valeur de la slice transporte son type concret avec elle, et l'appel part vers la méthode de ce type à l'exécution.
Une interface peut en embarquer d'autres. Les ensembles de méthodes embarqués sont fusionnés, et c'est ainsi que la bibliothèque standard construit io.ReadWriter à partir de io.Reader et io.Writer :
example.gogotype ReadWriter interface { Reader Writer }
Un type satisfait io.ReadWriter lorsqu'il possède à la fois Read et Write. C'est le cas de *os.File et net.Conn, mais pas de *strings.Reader, qui n'a pas de méthode Write.
Comment un type implémente-t-il une interface en Go ?
En ayant chaque méthode de l'interface dans son ensemble de méthodes, avec exactement la même signature. Go appelle cela la satisfaction implicite, et c'est la principale différence avec Java ou C#, où une classe doit nommer chaque interface qu'elle implémente.
Les packages restent ainsi découplés. SlackNotifier n'importe pas le package qui déclare Notifier, et n'a même pas besoin de savoir que Notifier existe. Vous pouvez écrire aujourd'hui une interface que des types de la bibliothèque standard ou d'un package tiers satisfont déjà.
Quand une méthode manque, le compilateur vous le signale à l'endroit où la valeur est utilisée comme interface :
example.gogotype PagerDutyNotifier struct { RoutingKey string } func (p PagerDutyNotifier) Send(ctx context.Context, msg string) error { return nil } var _ Notifier = PagerDutyNotifier{}
example.texttext./main.go:20:18: cannot use PagerDutyNotifier{} (value of struct type PagerDutyNotifier) as Notifier value in variable declaration: PagerDutyNotifier does not implement Notifier (missing method Notify)
Si la méthode existe mais que la signature diffère, l'erreur affiche les deux versions côte à côte :
example.texttext./main.go:16:18: cannot use PagerDutyNotifier{} (value of struct type PagerDutyNotifier) as Notifier value in variable declaration: PagerDutyNotifier does not implement Notifier (wrong type for method Notify) have Notify(string) error want Notify(context.Context, string) error
Comment vérifier à la compilation qu'un type implémente une interface ?
Déclarez une variable au niveau du package avec l'identifiant blanc :
example.gogovar _ Notifier = (*SlackNotifier)(nil)
Cette ligne n'alloue rien et n'exécute jamais de code. Elle demande seulement au compilateur si un *SlackNotifier peut être affecté à un Notifier, si bien que le build casse dès que quelqu'un renomme ou modifie Notify. Sans elle, vous ne le découvrez qu'à la première utilisation du type en tant que Notifier, qui peut se trouver dans un autre package. Le mot-clé var en Go présente la même astuce avec http.Handler.
Pourquoi un récepteur pointeur empêche-t-il de satisfaire une interface ?
Parce que la méthode appartient au type pointeur. Chaque type possède un ensemble de méthodes, et les règles sont les suivantes (Go spec) :
- L'ensemble de méthodes de
Tcontient les méthodes déclarées avec un récepteur valeur(t T). - L'ensemble de méthodes de
*Tcontient celles-ci ainsi que les méthodes déclarées avec un récepteur pointeur(t *T).
Un notifier qui regroupe les messages par lots doit se modifier lui-même, il utilise donc un récepteur pointeur :
example.gogotype BatchNotifier struct { mu sync.Mutex pending []string } func (b *BatchNotifier) Notify(ctx context.Context, msg string) error { b.mu.Lock() defer b.mu.Unlock() b.pending = append(b.pending, msg) return nil } func main() { var n Notifier = BatchNotifier{} fmt.Println(n) }
example.texttext./main.go:26:19: cannot use BatchNotifier{} (value of struct type BatchNotifier) as Notifier value in variable declaration: BatchNotifier does not implement Notifier (method Notify has pointer receiver)
La correction est var n Notifier = &BatchNotifier{}. Go refuse volontairement la version valeur. Une interface contenant un BatchNotifier contiendrait une copie, et Notify ajouterait des éléments à la slice pending de la copie sans que l'original ne change jamais. Passer Notify en récepteur valeur n'aide pas non plus. Cela copierait le sync.Mutex à chaque appel, et go vet le signale par Notify passes lock by value.
Un type doté de méthodes à récepteur pointeur doit être vérifié via son pointeur, comme dans var _ Notifier = (*BatchNotifier)(nil). La forme pointeur fonctionne aussi pour les types à récepteur valeur comme SlackNotifier, car un *T possède toutes les méthodes de T. C'est pourquoi (*T)(nil) est la façon habituelle d'écrire cette vérification.
Comment concevoir des interfaces en Go ?
Le proverbe Go dit : « plus l'interface est grande, plus l'abstraction est faible » (Go Proverbs). Les interfaces les plus utilisées de la bibliothèque standard ont une ou deux méthodes :
| Interface | Méthode | Implémentée par |
|---|---|---|
io.Reader | Read | fichiers, connexions réseau, corps HTTP, lecteurs gzip, bytes.Buffer |
io.Writer | Write | fichiers, http.ResponseWriter, fonctions de hachage, bytes.Buffer |
error | Error | *fs.PathError, *json.SyntaxError, vos propres types d'erreur |
fmt.Stringer | String | tout ce qui contrôle la façon dont fmt l'affiche |
http.Handler | ServeHTTP | routeurs, middlewares, http.HandlerFunc |
Une interface à une seule méthode est facile à satisfaire, donc beaucoup de types la satisfont. io.Copy fonctionne avec n'importe quelle source et n'importe quelle destination parce qu'il exige très peu. Une interface Storage à douze méthodes n'a généralement qu'une seule vraie implémentation, plus un mock à modifier chaque fois que l'interface change. http.Handler fonctionne de la même manière. Un middleware en prend un et en renvoie un, si bien que les middlewares Go de n'importe quel package s'empilent autour de n'importe quel routeur.
fmt.Stringer montre combien il en faut peu à un type pour s'intégrer à la bibliothèque standard. Donnez une méthode String à une énumération, et chaque verbe de fmt l'utilise :
example.gogotype OrderStatus int const ( Pending OrderStatus = iota Paid Refunded ) func (s OrderStatus) String() string { switch s { case Pending: return "pending" case Paid: return "paid" case Refunded: return "refunded" } return fmt.Sprintf("OrderStatus(%d)", int(s)) } func main() { fmt.Println(Paid) fmt.Printf("order 1001 is %v\n", Refunded) }
example.texttextpaid order 1001 is refunded
Où déclarer une interface Go ?
Dans le package qui l'utilise, pas dans celui qui l'implémente. Comme la satisfaction est implicite, le consommateur peut ne lister que les méthodes qu'il appelle.
Un service de remboursement n'a besoin que de charger une commande. Il déclare pour cela une interface à une méthode et la reçoit dans son constructeur :
example.gogotype orderGetter interface { GetOrder(ctx context.Context, id int64) (Order, error) } type RefundService struct { orders orderGetter } func NewRefundService(orders orderGetter) *RefundService { return &RefundService{orders: orders} } func (s *RefundService) CanRefund(ctx context.Context, id int64) (bool, error) { o, err := s.orders.GetOrder(ctx, id) if err != nil { return false, fmt.Errorf("load order %d: %w", id, err) } return o.Status == "paid", nil }
En production, vous passez le store Postgres, qui possède vingt autres méthodes que le service de remboursement ne voit jamais. Dans un test, une map suffit :
example.gogotype fakeOrders map[int64]Order func (f fakeOrders) GetOrder(ctx context.Context, id int64) (Order, error) { o, ok := f[id] if !ok { return Order{}, ErrNotFound } return o, nil } func main() { svc := NewRefundService(fakeOrders{1001: {ID: 1001, Status: "paid"}}) fmt.Println(svc.CanRefund(context.Background(), 1001)) fmt.Println(svc.CanRefund(context.Background(), 1002)) }
example.texttexttrue <nil> false load order 1002: order not found
fakeOrders est un type map défini avec une méthode (voir le mot-clé type en Go), le test n'a donc pas besoin de bibliothèque de mocks.
Que signifie « acceptez des interfaces, renvoyez des structs » ?
C'est l'autre moitié de la même idée. Les fonctions prennent des interfaces en paramètre, pour que les appelants puissent passer tout ce qui convient. Les constructeurs renvoient des types concrets comme *RefundService ou *PostgresStore, pour que les appelants obtiennent toutes les méthodes et tous les champs exportés, et puissent décider eux-mêmes de quelle petite interface ils ont besoin.
Renvoyer une interface depuis un constructeur masque les méthodes que l'interface ne liste pas, et oblige chaque appelant à utiliser l'unique abstraction que vous avez choisie. La principale exception est error. Les fonctions renvoient error plutôt que *ValidationError, et la section sur nil plus bas explique pourquoi.
Qu'est-ce que any en Go ?
any est un alias de interface{}, l'interface sans aucune méthode. Tout type possède au moins zéro méthode, donc une variable de type any peut contenir n'importe quelle valeur. any a été ajouté dans Go 1.18, et les deux écritures sont interchangeables (Go spec).
any apparaît là où le type n'est connu qu'à l'exécution. fmt.Println(a ...any) accepte des arguments de n'importe quel type, et json.Unmarshal vers une map[string]any produit des valeurs dont le type dépend du JSON.
Le prix à payer, c'est que le compilateur cesse de vérifier. Chaque lecture depuis un any exige une assertion de type, et une mauvaise supposition devient une erreur à l'exécution au lieu d'une erreur de compilation. Quand une fonction travaille sur plusieurs types mais que les mêmes opérations s'appliquent à tous, utilisez plutôt un paramètre de type. func Sum[T Amount](values []T) T, présenté dans la section sur les contraintes plus bas, conserve toute la vérification de types, alors que func Sum(values []any) any repousse chaque vérification à l'exécution.
Comment fonctionnent les assertions de type et les type switches ?
Une assertion de type extrait la valeur concrète d'une interface. La forme à deux valeurs ne panique jamais :
example.gogovar payload map[string]any body := `{"user_id": 42, "email": "[email protected]", "roles": ["admin", "billing"], "manager": null}` if err := json.Unmarshal([]byte(body), &payload); err != nil { log.Fatal(err) } email, ok := payload["email"].(string) fmt.Println(email, ok) id, ok := payload["user_id"].(int) fmt.Println(id, ok) fmt.Printf("%T\n", payload["user_id"])
example.texttext[email protected] true 0 false float64
La seconde assertion échoue parce que encoding/json décode chaque nombre JSON en float64 lorsque la cible est any. Avec ok, l'échec se traduit simplement par false et la valeur zéro. La forme à une valeur, payload["user_id"].(int), panique à la place :
example.texttextpanic: interface conversion: interface {} is float64, not int
Les assertions fonctionnent aussi avec des interfaces. r.(io.Writer) demande si la valeur contenue dans r possède une méthode Write, et la bibliothèque standard s'en sert pour détecter un comportement optionnel. io.Copy vérifie si la source est un io.WriterTo et, le cas échéant, emprunte son chemin plus rapide.
Un type switch enchaîne plusieurs assertions en une seule instruction. Il convient bien au même payload JSON, où une valeur peut être de l'un d'une poignée de types :
example.gogofunc describe(v any) string { switch v := v.(type) { case nil: return "null" case float64: return fmt.Sprintf("number %g", v) case string: return fmt.Sprintf("string of %d bytes", len(v)) case []any: return fmt.Sprintf("array of %d items", len(v)) case map[string]any: return fmt.Sprintf("object with %d keys", len(v)) default: return fmt.Sprintf("other %T", v) } } func main() { // payload decoded as above for _, key := range []string{"user_id", "email", "roles", "manager"} { fmt.Println(key+":", describe(payload[key])) } }
example.texttextuser_id: number 42 email: string of 15 bytes roles: array of 2 items manager: null
Dans chaque case, v a le type de ce case, donc len(v) compile pour la string, la slice et la map. Les type switches sur des erreurs fonctionnent de la même façon, mais ils ne voient que l'erreur la plus externe. Le mot-clé type en Go montre ce cas et explique pourquoi errors.As est généralement le meilleur outil pour les erreurs enveloppées.
Pourquoi mon erreur nil n'est-elle pas nil en Go ?
Une valeur d'interface a deux parties, un type dynamique et une valeur dynamique. Elle n'est égale à nil que si les deux sont non définis. Si vous stockez un pointeur nil dans une interface, la partie type est définie, donc l'interface n'est pas nil (Go FAQ).
Le piège se referme le plus souvent avec les types d'erreur personnalisés. Ici, la fonction garde une variable *ValidationError et la renvoie, qu'elle ait été affectée ou non :
example.gogopackage main import ( "fmt" "strings" ) type ValidationError struct { Field string } func (e *ValidationError) Error() string { return "invalid " + e.Field } func validate(email string) error { var verr *ValidationError if !strings.Contains(email, "@") { verr = &ValidationError{Field: "email"} } return verr } func main() { err := validate("[email protected]") if err != nil { fmt.Printf("validation failed: %v (%T)\n", err, err) return } fmt.Println("ok") }
example.texttextvalidation failed: <nil> (*main.ValidationError)
L'e-mail est valide et verr vaut nil, mais l'error renvoyée contient le type *main.ValidationError avec un pointeur nil à l'intérieur. err != nil compare l'interface entière à une interface sans type, donc le résultat est true. L'appelant rejette une inscription valide, et la ligne de log affiche <nil>, ce qui rend le bug difficile à repérer.
La correction consiste à renvoyer un nil littéral sur le chemin de succès et à ne jamais faire transiter un pointeur typé par un retour error :
example.gogofunc validate(email string) error { if !strings.Contains(email, "@") { return &ValidationError{Field: "email"} } return nil }
Déclarez le type de retour comme error, pas *ValidationError, et renvoyez nil directement. Les appelants qui ont besoin du champ l'obtiennent toujours avec errors.As, qui retrouve aussi l'erreur après qu'elle a été enveloppée :
example.gogoerr := fmt.Errorf("signup: %w", validate("ana.example.com")) var verr *ValidationError if errors.As(err, &verr) { fmt.Println("bad field:", verr.Field) }
Ce code affiche bad field: email. Le même piège existe pour n'importe quelle interface. Une variable var store *PostgresStore jamais initialisée, passée là où un orderGetter est attendu, produit elle aussi une interface non nil.
Le cas inverse est une interface réellement nil, comme un champ de struct de type Notifier que personne n'a renseigné. Appeler une méthode dessus panique avec invalid memory address or nil pointer dereference, car il n'y a aucun type sur lequel trouver la méthode. Un constructeur qui exige la dépendance, comme NewRefundService(orders), évite ce problème.
Comment utiliser les interfaces comme contraintes génériques ?
Depuis Go 1.18, une interface peut lister des types en plus des méthodes. Cette liste s'appelle un ensemble de types (type set), et ~T signifie « tout type dont le type sous-jacent est T » :
example.gogotype Cents int64 type Amount interface { ~int64 | ~float64 } func Sum[T Amount](values []T) T { var total T for _, v := range values { total += v } return total } func Dedupe[T comparable](ids []T) []T { seen := make(map[T]struct{}, len(ids)) out := make([]T, 0, len(ids)) for _, id := range ids { if _, ok := seen[id]; ok { continue } seen[id] = struct{}{} out = append(out, id) } return out } func main() { fmt.Println(Sum([]Cents{4999, 1250, 300})) fmt.Println(Dedupe([]string{"u_42", "u_7", "u_42"})) }
example.texttext6549 [u_42 u_7]
Amount autorise + parce que chaque type de son ensemble le prend en charge. comparable est une contrainte prédéclarée pour les types compatibles avec ==, ce dont une clé de map a besoin. Le compilateur rejette un type hors de l'ensemble et nomme le problème :
example.texttext./main.go:27:17: string does not satisfy Amount (string missing in ~int64 | ~float64)
Une interface avec une liste de types ne peut servir que de contrainte. Déclarer une variable de ce type échoue :
example.texttext./main.go:10:8: cannot use type Amount outside a type constraint: interface contains type constraints
Go a donc deux sortes d'interfaces qui partagent un même mot-clé. Les interfaces composées uniquement de méthodes fonctionnent partout, comme types de variables, paramètres et contraintes. Les interfaces avec des listes de types n'apparaissent qu'entre crochets. L'introduction aux generics du blog Go décrit comment les ensembles de types unifient les deux.
La place de LevelUpGo
LevelUpGo enseigne Go à travers des exercices qui exécutent du vrai code Go dans le navigateur. Interfaces & Polymorphism couvre la satisfaction implicite, les ensembles de méthodes, any, les assertions de type et la composition d'interfaces. Interface Design couvre les petites interfaces, leur déclaration là où elles sont utilisées et la façon d'éviter les interfaces qui n'existent que pour les mocks. Go Generics Masterclass va plus loin sur les contraintes et les ensembles de types. Le Training Ground propose de courts exercices autonomes sur les interfaces pour s'entraîner en dehors d'un cours. Pour les 24 autres mots réservés, consultez Les mots-clés Go : les 25 expliqués.
Questions fréquentes
interface est-il un mot-clé en Go ?
Oui. interface fait partie des 25 mots-clés réservés de Go, vous ne pouvez donc pas l'utiliser comme nom de variable ou de fonction. Il apparaît dans les déclarations de types interface comme type Notifier interface { ... }, dans le littéral interface{} et dans les contraintes en ligne comme [T interface{ ~int | ~string }].
Go a-t-il un mot-clé implements ?
Non. Un type satisfait une interface simplement en possédant toutes ses méthodes avec des signatures correspondantes. Pour que le compilateur vérifie qu'un type satisfait une interface, ajoutez var _ Notifier = (*SlackNotifier)(nil) à côté du type.
Quelle est la différence entre any et interface{} en Go ?
Aucune. any est un alias prédéclaré de interface{}, ajouté dans Go 1.18, les deux sont donc le même type. Le code récent utilise any parce qu'il est plus court, et gofmt -r 'interface{} -> any' peut réécrire le code plus ancien.
Pourquoi Go affiche-t-il « method has pointer receiver » ?
La méthode est déclarée sur *T, donc seul un *T la possède. Vous avez tenté d'utiliser une valeur T comme interface. Passez plutôt un pointeur, comme &BatchNotifier{}, ou changez la méthode en récepteur valeur si elle ne modifie pas le récepteur.
Pourquoi mon erreur n'est-elle pas nil alors que j'ai renvoyé un pointeur nil ?
Une interface ne vaut nil que si son type et sa valeur sont tous deux non définis. Renvoyer un *ValidationError nil depuis une fonction dont le type de retour est error fixe le type à *ValidationError, donc err != nil vaut true. Renvoyez un nil littéral sur le chemin de succès au lieu d'une variable pointeur typée.
Les interfaces Go peuvent-elles avoir des champs ?
Non. Les interfaces ne décrivent que des méthodes, ou des ensembles de types lorsqu'elles servent de contraintes. Si les appelants ont besoin d'une valeur, ajoutez une méthode getter comme ID() string, ou acceptez le struct concret au lieu d'une interface.
Sources
- The Go Programming Language Specification, Interface types : https://go.dev/ref/spec#Interface_types
- The Go Programming Language Specification, Method sets : https://go.dev/ref/spec#Method_sets
- The Go Programming Language Specification, Type assertions : https://go.dev/ref/spec#Type_assertions
- The Go Programming Language Specification, Type switches : https://go.dev/ref/spec#Type_switches
- Go FAQ, Why is my nil error value not equal to nil? : https://go.dev/doc/faq#nil_error
- The Go Blog, An Introduction To Generics : https://go.dev/blog/intro-generics
- Effective Go, Interfaces and other types : https://go.dev/doc/effective_go#interfaces_and_types
- Go Proverbs : https://go-proverbs.github.io/
- Package io : https://pkg.go.dev/io
