Après la série Winery, voici un autre exercice d’entretien en Go, encore plus court : Interfaces 🔌.
Deux interfaces, trois implémentations, un test à faire passer. Sur le papier, cinq minutes.
Sauf que deux des trois implémentations sont buguées, et que ni le compilateur ni le runtime ne lèvent le petit doigt.
C’est tout l’intérêt de l’exo : les interfaces Go échouent en silence. 🤫
💡 Tout le code est sur GitHub.
🎯 Le décor
Deux interfaces minimalistes : une source de valeurs, un puits de valeurs.
type Input interface {
NextValue(cycle int) (int, error)
}
type Output interface {
SendValue(value int) error
}
Le test range trois implémentations dans une slice de any, puis, à chaque cycle, va chercher les Input pour produire une valeur et la pousse dans chaque Output :
func TestInterfaces(t *testing.T) {
inlist := NewInList(1, 3, 5, 7, 11)
instream := NewInStream(strings.NewReader("2 4 6 8 0"))
var outlist OutList
interfaces := []any{&inlist, instream, &outlist}
for cycle := 0; cycle < 5; cycle++ {
for i, src := range interfaces {
input, isInput := src.(Input) // ⬅️ qui est un Input ?
if !isInput {
continue
}
val, err := input.NextValue(cycle)
assert.NoErrorf(t, err, "interface %d ...", i)
for j, dest := range interfaces {
output, isOutput := dest.(Output) // ⬅️ qui est un Output ?
if !isOutput {
continue
}
assert.NoErrorf(t, output.SendValue(val), "interface %d ...", j)
}
}
}
assert.Equal(t, []int{1, 2, 3, 4, 5, 6, 7, 8, 11, 0}, outlist.Result())
}
L’assertion finale dit tout : les valeurs de inlist (1, 3, 5, 7, 11) et de instream (2, 4, 6, 8, 0) doivent s’entrelacer dans outlist. Pour que ça tombe juste, il faut que les deux entrées produisent et que la sortie stocke. Trois conditions, et deux qui ne sont pas remplies. 🎯
Le cœur du sujet : l'assertion de type
src.(Input) et dest.(Output) demandent, à l'exécution : ce type implémente-t-il l'interface ?.
Si oui, on l'utilise ; si non, continue.Aucune erreur, aucun log.
Une implémentation qui rate l'interface d'un cheveu est simplement sautée sans un bruit 👉 tout le piège de l'exo est là.
🧩 Les trois implémentations
InStream est l’implémentation de référence, celle qui est correcte. À garder en tête pour comparer :
// ✅ receiver pointeur, signature (int, error) : implémente bien Input
func (in *InStream) NextValue(cycle int) (int, error) {
var val int
_, err := in.stream.Seek(int64(cycle*2), io.SeekStart)
if err == nil {
_, err = fmt.Fscan(in.stream, &val)
}
return val, err
}
Les deux autres, InList et OutList, ont chacune un défaut invisible. 👇
🐛 Bug 1 — InList n’implémente pas Input
// Implements the Input interface ← le commentaire ment 🙃
func (in *InList) NextValue(cycle int) int {
return in.l.Get(cycle)
}
Le commentaire l’affirme, mais c’est faux. Input réclame NextValue(cycle int) (int, error).
Ici, il manque la valeur de retour error : une méthode dont la signature diffère, ne serait-ce que d’un type de retour, est une méthode différente.
En Go, l'implémentation d'interface est structurelle et implicite
Pas de implements Input à écrire, donc pas de compilateur pour vous rappeler à l'ordre quand vous ratez la cible. Le type « implémente » l'interface uniquement si sa méthode a exactement la bonne signature. L'astuce pour transformer ce piège silencieux en erreur de compilation : une assertion à vide, var _ Input = (*InList)(nil). Si InList décroche, le build casse net, au bon endroit :
// 🔑 Ne pas hésiter à faire l'assertion de type pour vérifier dès la compilation que ça casse
var _ Input = (*InList)(nil) // ❌ Ne compile pas ici 🫠
var _ Input = (*InStream)(nil) // ✅ Compile bien
var _ Output = (*OutList)(nil) // ✅ Compile bien
for cycle := 0; cycle < 5; cycle++ {
for i, src := range interfaces {
// iterate over all inputs
input, isInput := src.(Input)
if !isInput {
Conséquence : src.(Input) renvoie false pour l’inlist, le continue l’écarte, et ses valeurs 1, 3, 5, 7, 11 ne sont jamais produites. Le test ne plante pas, il rend juste un résultat incomplet.
// ✅ Le fix : aligner la signature sur l'interface
func (in *InList) NextValue(cycle int) (int, error) {
return in.l.Get(cycle), nil
}
🐛 Bug 2 — OutList mute une copie et perd tout
// Implements the Output interface
func (out OutList) SendValue(value int) error {
out.l.Append(value)
return nil
}
Ici, la signature est bonne : *OutList implémente bien Output, l’assertion dest.(Output) passe, SendValue est appelée. Et pourtant Result() renvoie nil. 🤔
Le coupable est le receiver valeur : func (out OutList) SendValue(...). À chaque appel, out est une copie de la struct OutList. out.l.Append(value) ajoute donc la valeur à la liste de la copie, aussitôt jetée au retour de la méthode.
L’OutList d’origine, elle, ne bouge pas d’un octet 👉 les cinq valeurs envoyées disparaissent une à une.
// ✅ Le fix : receiver pointeur, on mute la vraie struct
func (out *OutList) SendValue(value int) error {
out.l.Append(value)
return nil
}
Receiver valeur vs pointeur : le piège du method set
Pourquoi l'assertion passe malgré le receiver valeur ? Parce que le method set de *OutList inclut les méthodes à receiver valeur. &outlist satisfait donc Output dans les deux cas. La différence ne se voit pas au typage, elle se voit à l'exécution : receiver valeur = mutation sur une copie = donnée perdue.
C'est le pendant « comportement » du bug 1 : là, le type ne matchait pas ; ici, il matche mais ne fait rien d'utile.
Règle simple : dès qu'une méthode modifie l'état du receiver, elle prend un pointeur. SendValue accumule, donc pointeur, sans hésiter.
✅ Résultat
Deux corrections, une par implémentation, et le test passe :
--- PASS: TestInterfaces
- 🔌 Bug 1 — une signature qui diffère de l’interface = un type qui ne l’implémente pas. L’assertion échoue en silence, la source est ignorée.
- 📦 Bug 2 — un receiver valeur sur une méthode qui mute = une mutation perdue dans une copie. Le type implémente l’interface, mais ne fait rien.
La leçon tient en une phrase : en Go, satisfaire une interface ne garantit ni la bonne signature attendue par l’appelant, ni un comportement utile. Le typage structurel et le choix du receiver se combinent pour produire deux bugs que ni go build ni go vet ne signalent. Le meilleur garde-fou reste l’assertion var _ Input = (*InList)(nil), à écrire dès qu’on prétend implémenter une interface. 🛡️
Remarque : En Go, une convention dans le langage fait que normalement on ne mixe pas receiver valeur et pointeur.
Même si ce n’est pas une règle stricte, c’est une bonne pratique à suivre par expérience.Pour quelle raison ❓Cela permet d’identifier très rapidement les structures immuables des structures mutables.
Par expérience, souvent les structures de données (ex : DTO) sont immuables tandis que les structures comportementales (ex : stratégies) sont mutables.
À bientôt ! 🔧

