Un entretien Go senior n'est pas un quiz de syntaxe. Quand on recrute un ingénieur senior, personne ne cherche à savoir si vous pouvez réciter la boucle for à trois clauses ou si vous vous souvenez que append peut réallouer. On veut savoir si vous êtes capable de traquer une fuite de goroutine, de défendre votre manière de gérer les erreurs et de concevoir une API avec laquelle l'équipe pourra vivre pendant deux ans. Les questions sont volontairement ouvertes. L'intervieweur observe votre façon de raisonner, il ne vérifie pas si vous avez appris la bibliothèque standard par cœur.
Ce guide couvre les sujets qui distinguent les candidats seniors des profils intermédiaires, avec du code exécutable pour chacun. Il s'appuie sur ce que les développeurs Go en poste disent construire et sur ce qui leur pose problème, pas sur des listes de « 50 questions incontournables » glanées sur le web.
En bref
- La concurrence est le sujet où la préparation rapporte le plus. Les deux choses que les développeurs Go construisent le plus sont des services API/RPC (75 %) et des outils en ligne de commande (62 %), et les deux reposent fortement sur les goroutines, les channels et
context(Go Developer Survey 2024 H2). - La principale difficulté signalée est d'écrire du code idiomatique, pas l'absence de fonctionnalités. 33 % des répondants citent « s'assurer que notre code Go suit les bonnes pratiques et les idiomes » comme leur plus grande difficulté, plus que pour n'importe quelle fonctionnalité manquante du langage (2025 Go Developer Survey).
- Les data races existent en production, attendez-vous donc à en rencontrer en entretien. Le race detector d'Uber a trouvé environ 2 000 data races dans leur monorepo Go, et 210 ingénieurs en ont corrigé environ 1 100 en six mois (Uber Engineering).
- Maîtrisez parfaitement
errors.Is,errors.Aset%w. 28 % des répondants affirment qu'il manque à Go une fonctionnalité qu'ils appréciaient dans un autre langage, et la gestion des erreurs arrive en tête de cette liste. Les intervieweurs examinent donc de près votre façon de la traiter (2025 Go Developer Survey). - Les génériques font partie du programme, mais ce sont aussi un piège. Ils sont arrivés avec Go 1.18, que l'équipe Go a qualifié de « plus grand changement jamais apporté au langage » (Go Blog). Le signal senior, c'est de savoir quand ne pas les utiliser.
- Visez la toolchain actuelle. Go 1.26 est la dernière version stable (notes de version de Go 1.26). Les intervieweurs remarquent quand vous décrivez le comportement d'une ancienne version.
Table des matières
- Ce qu'un entretien Go senior évalue vraiment
- Concurrence : le cœur de tout entretien Go senior
- Context : annulation et échéances
- Gérer les erreurs comme un senior
- Génériques : savoir quand y recourir
- Interfaces et conception d'API
- L'épreuve de live coding : écrire un rate limiter
- Questions fréquentes
- Commencez votre parcours Go senior
Ce qu'un entretien Go senior évalue vraiment
Aucune enquête de référence ne classe « les questions d'entretien Go les plus fréquentes », et vous devriez vous méfier de tout blog qui avance un pourcentage précis. Ce que vous pouvez faire, c'est partir de ce à quoi les développeurs Go consacrent leur temps. L'enquête 2024 a montré que 75 % des répondants construisent des services API ou RPC et 62 % des outils en ligne de commande (Go Developer Survey 2024 H2). Ce travail est concurrent, dépendant du réseau et plein de possibilités d'échec, c'est pourquoi les entretiens reviennent sans cesse à la concurrence, à context et à la gestion des erreurs.
L'enquête 2025 a interrogé les développeurs sur leurs plus grandes difficultés. 33 % citent le respect des bonnes pratiques et des idiomes. 28 % citent une fonctionnalité appréciée dans un autre langage et absente de Go, le plus souvent la gestion des erreurs, les types somme et la sécurité face aux pointeurs nil. 26 % citent la difficulté à trouver des modules fiables (2025 Go Developer Survey). Dans un entretien senior, l'entreprise vérifie en bonne partie que vous avez assimilé les idiomes qu'un tiers de la communauté trouve difficiles.
L'entretien est donc surtout un test de jugement. Choisissez-vous la bonne primitive de concurrence, ou lancez-vous une goroutine par réflexe ? Savez-vous expliquer pourquoi vous avez encapsulé une erreur au lieu de la journaliser et de passer à autre chose ? Pouvez-vous dire « Je n'utiliserais pas de génériques ici » et le justifier ?
Concurrence : le cœur de tout entretien Go senior
C'est sur la concurrence que les candidats de niveau intermédiaire perdent des offres. L'erreur classique consiste à lancer une goroutine par unité de travail, sans limite, sans chemin d'erreur et sans moyen de l'arrêter. Les candidats seniors partent d'un modèle borné.
Les goroutines coûtent peu, et c'est ce qui rend l'approche naïve tentante. Extrait de la FAQ de Go :
La pile initiale fait environ 2KB sur la plupart des plateformes et le runtime la gère, c'est pourquoi un programme peut faire tourner des centaines de milliers de goroutines en même temps. Peu coûteux ne veut pas dire gratuit, cependant. Des goroutines sans limite consomment de la mémoire, submergent les services en aval et masquent les fuites. La solution habituelle est un worker pool.
Worker pools
Un worker pool lance un nombre fixe de goroutines et leur fournit du travail via un channel. C'est la réponse standard à « traiter 10 000 URL sans ouvrir 10 000 sockets en même temps ».
example.gogopackage main import ( "fmt" "sync" ) // fetchStatus stands in for any bounded I/O call (HTTP, DB, RPC). func fetchStatus(url string) string { return "200 " + url } func main() { urls := []string{"a", "b", "c", "d", "e"} const workers = 3 jobs := make(chan string) results := make(chan string) var wg sync.WaitGroup for range workers { wg.Go(func() { for url := range jobs { results <- fetchStatus(url) } }) } // Close results once every worker has returned. go func() { wg.Wait() close(results) }() go func() { for _, url := range urls { jobs <- url } close(jobs) }() for r := range results { fmt.Println(r) } }
Les intervieweurs guettent trois détails. Vous fermez jobs pour que les boucles range des workers se terminent. Vous utilisez un sync.WaitGroup pour savoir à quel moment il est sûr de fermer results. Et vous fermez results depuis une goroutine séparée pour que la boucle principale puisse le vider. Si ces trois points sont justes, vous avez montré que vous comprenez l'ensemble. Deux détails plus récents montrent que vous suivez l'évolution de la toolchain. for range workers itère sur un entier, ce qui fonctionne depuis Go 1.22. wg.Go, ajouté dans Go 1.25, remplace l'ancienne paire wg.Add(1) plus defer wg.Done(), ce qui élimine le bug de WaitGroup le plus courant : oublier l'une des deux moitiés de la paire.
Data races et race detector
Attendez-vous à une question du type « qu'est-ce qui ne va pas dans ce code ? » avec une race à la clé. Les races apparaissent constamment dans le code de production, pas seulement en entretien. Uber a publié les chiffres. Son monorepo Go compte environ 50 millions de lignes de code réparties sur environ 2 100 services Go distincts. Son race detector a trouvé près de 2 000 data races, et 210 ingénieurs en ont corrigé environ 1 100 en six mois (Uber Engineering).
La race d'école est un compteur partagé :
example.gogo// BROKEN: concurrent writes to count are a data race. func countBroken(items []int) int { count := 0 var wg sync.WaitGroup for _, n := range items { wg.Add(1) go func() { defer wg.Done() if n%2 == 0 { count++ // unsynchronized write } }() } wg.Wait() return count }
Le bon correctif dépend de la forme du problème. Un simple compteur appelle sync/atomic ou un sync.Mutex. Pour une agrégation, un channel fonctionne souvent mieux : chaque goroutine rapporte son résultat et une seule goroutine détient le total. Une réponse senior nomme le compromis. Un mutex est plus lisible, les atomics sont plus rapides en cas de contention, et un channel (« partager la mémoire en communiquant ») supprime complètement l'état partagé. Ajoutez ensuite « Je le lancerais avec go test -race », car ce réflexe détecte les races avant qu'elles n'arrivent en production.
Fan-out, fan-in
Le fan-out répartit le travail entre plusieurs goroutines, et le fan-in fusionne leurs résultats dans un seul channel. On l'utilise pour des I/O parallèles qui alimentent un consommateur unique, et il s'associe naturellement au worker pool ci-dessus. Les candidats trébuchent généralement sur l'étape de fusion. Fermer le channel fusionné nécessite un WaitGroup sur les producteurs, exactement comme pour le channel results du worker pool. Si vous savez expliquer pourquoi les deux modèles ferment leur sortie de la même manière, vous montrez que vous comprenez la propriété des channels et que vous ne vous contentez pas de répéter un snippet.
Context : annulation et échéances
Les services Go transmettent context.Context tout au long de la pile d'appels, et les intervieweurs vous demanderont de l'utiliser correctement. Les règles : context est le premier paramètre, il s'appelle ctx, et vous ne le stockez jamais dans un struct. Il transporte l'annulation, les échéances et les valeurs propres à la requête à travers les frontières d'API.
La question de relance la plus fréquente est « comment arrêter un worker bloqué sur un appel lent ? ». Vous faites un select à la fois sur le travail et sur ctx.Done() :
example.gogofunc process(ctx context.Context, jobs <-chan string) error { for { select { case <-ctx.Done(): return ctx.Err() // context.Canceled or DeadlineExceeded case job, ok := <-jobs: if !ok { return nil // channel closed, work done } if err := handle(ctx, job); err != nil { return fmt.Errorf("handling %q: %w", job, err) } } } }
Renvoyer ctx.Err() indique à l'appelant pourquoi vous vous êtes arrêté, ce qui lui permet de distinguer une échéance dépassée d'une annulation volontaire. Les réponses plus faibles ignorent le context ou ne le vérifient qu'en haut de la boucle. Une goroutine coincée dans handle ne voit alors jamais l'annulation. Les bons candidats mentionnent aussi que le parent doit appeler la fonction cancel renvoyée par context.WithCancel ou context.WithTimeout, généralement avec defer cancel(), pour libérer les ressources.
Préparez-vous à « quelle est la différence entre context.WithCancel et context.WithTimeout ? ». Le premier annule quand vous appelez cancel. Le second annule aussi dès qu'une échéance est dépassée. Dans les deux cas, vous devez faire defer cancel(), car laisser fuir la goroutine interne d'un context est aussi une fuite de ressources.
Gérer les erreurs comme un senior
La gestion des erreurs est la fonctionnalité que les développeurs Go regrettent le plus par rapport à d'autres langages. En 2025, 28 % des répondants ont déclaré qu'il manquait à Go une fonctionnalité qu'ils appréciaient ailleurs, et la gestion des erreurs arrivait en tête de cette liste (2025 Go Developer Survey). C'est cette frustration qui pousse les intervieweurs à la tester. Ils veulent voir que vous traitez les erreurs comme des valeurs porteuses de sens, pas comme du bruit que l'on journalise puis que l'on oublie.
La base consiste à encapsuler avec %w pour que les appelants puissent inspecter la chaîne :
example.gogovar ErrNotFound = errors.New("not found") func loadUser(ctx context.Context, id string) (*User, error) { row, err := db.Query(ctx, id) if err != nil { // Wrap, don't replace. The caller can still see the root cause. return nil, fmt.Errorf("loadUser %s: %w", id, err) } if row == nil { return nil, fmt.Errorf("loadUser %s: %w", id, ErrNotFound) } return row, nil }
Ensuite, errors.Is vérifie une valeur sentinelle, et errors.As déballe l'erreur vers un type concret :
example.gogouser, err := loadUser(ctx, id) if errors.Is(err, ErrNotFound) { http.Error(w, "user not found", http.StatusNotFound) return } var validationErr *ValidationError if errors.As(err, &validationErr) { http.Error(w, validationErr.Field+" is invalid", http.StatusBadRequest) return }
Soyez prêt à expliquer quand utiliser l'un ou l'autre. Une sentinelle avec errors.Is convient quand l'appelant a seulement besoin de savoir quelle erreur s'est produite. Une erreur typée avec errors.As convient quand l'appelant a besoin des données qu'elle contient, comme un nom de champ ou un code de statut. N'encapsulez avec %w que lorsque l'erreur encapsulée fait réellement partie du contrat de votre API, car un excès d'encapsulation expose des détails d'implémentation. Dites aussi explicitement que panic est réservé aux bugs du programmeur et aux échecs irrécupérables au démarrage, pas aux situations attendues comme un enregistrement introuvable. Utiliser panic pour une erreur ordinaire est un moyen rapide d'échouer à un entretien senior.
Génériques : savoir quand y recourir
Les génériques sont arrivés avec Go 1.18. Voici comment l'équipe Go les a annoncés :
Les intervieweurs posent des questions sur les génériques pour savoir si vous êtes capable de retenue. La réponse honnête est que la plupart du code n'en a pas besoin, et que les packages slices et maps de la bibliothèque standard couvrent déjà les cas courants. Utilisez des paramètres de type quand vous devriez sinon dupliquer la même logique pour plusieurs types, ou vous rabattre sur interface{} et perdre la sûreté du typage.
Un bon cas d'usage est un helper contraint que la bibliothèque standard ne fournit pas déjà :
example.gogoimport "cmp" // Clamp constrains v to the range [lo, hi] for any ordered type. func Clamp[T cmp.Ordered](v, lo, hi T) T { if v < lo { return lo } if v > hi { return hi } return v }
La contrainte cmp.Ordered du package standard cmp limite T aux types qui supportent < et >. Le choix de votre exemple en dit long lui aussi. N'écrivez pas de Max générique. Go dispose des builtins max et min depuis la 1.21, et slices.Max et slices.Min gèrent les slices, donc une version maison laisse penser que vous sortez les génériques par habitude. Clamp justifie son paramètre de type, car rien dans la bibliothèque standard ne fait la même chose. Avant les génériques, une fonction comme celle-ci exigeait soit une copie par type, soit interface{} avec des assertions de type susceptibles de paniquer à l'exécution. Désormais, c'est le compilateur qui vérifie. Mais si la fonction ne traite jamais que des int, écrivez-la pour int. La généralisation prématurée est un code smell en Go comme ailleurs, et l'intervieweur espère que vous le direz.
Interfaces et conception d'API
Les questions sur les interfaces évaluent votre sens de la conception, qui représente une grande partie de ce que « senior » veut dire. Le proverbe Go à connaître ici est « accept interfaces, return structs » (acceptez des interfaces, renvoyez des structs). Une fonction doit accepter l'interface la plus étroite qu'elle utilise réellement et renvoyer un type concret, pour que les appelants gardent leurs options ouvertes.
example.gogo// Good: accepts the minimal behavior it needs. func Copy(dst io.Writer, src io.Reader) (int64, error) { return io.Copy(dst, src) }
Copy ne se soucie pas de savoir si src est un fichier, une connexion réseau ou un buffer. Comme elle accepte un io.Reader, elle fonctionne avec tous. C'est pourquoi io.Reader et io.Writer, avec une seule méthode chacune, sont les interfaces les plus réutilisées du langage.
Quelques autres points méritent d'être soulevés. Gardez les interfaces petites, idéalement une ou deux méthodes. Définissez-les dans le package qui les consomme plutôt que dans celui qui les implémente, pour éviter que les packages dépendent les uns des autres sans raison. Et perdez l'habitude, courante dans d'autres langages, de placer une interface devant tout. Un struct avec une seule implémentation n'a pas encore besoin d'interface. Ajoutez-en une quand une deuxième implémentation ou un test double en a réellement besoin. Les intervieweurs repèrent les abstractions construites sur des spéculations, et c'est précisément le genre de code auquel ont affaire les 33 % qui peinent avec les idiomes (2025 Go Developer Survey).
L'épreuve de live coding : écrire un rate limiter
L'épreuve en direct demande généralement quelque chose de petit mais concurrent. Les rate limiters reviennent souvent, car ils combinent goroutines, channels, gestion du temps et nettoyage. Un limiteur à token bucket basé sur time.Ticker est assez court pour être écrit pendant l'entretien et vous donne beaucoup de matière à discussion :
example.gogotype Limiter struct { tokens chan struct{} stop chan struct{} } func NewLimiter(perSecond int) *Limiter { l := &Limiter{ tokens: make(chan struct{}, perSecond), // burst capacity stop: make(chan struct{}), } ticker := time.NewTicker(time.Second / time.Duration(perSecond)) go func() { defer ticker.Stop() for { select { case <-ticker.C: select { case l.tokens <- struct{}{}: // refill one token default: // bucket full, drop the tick } case <-l.stop: return } } }() return l } // Allow reports whether a request may proceed right now. func (l *Limiter) Allow() bool { select { case <-l.tokens: return true default: return false } } func (l *Limiter) Close() { close(l.stop) }
Le code compte moins que la façon dont vous en parlez. Faites remarquer que le channel bufferisé tokens vous donne la capacité de burst sans effort supplémentaire. Expliquez que le select interne avec un default abandonne les ticks de recharge quand le bucket est plein au lieu de bloquer. Mentionnez que Close arrête la goroutine d'arrière-plan pour qu'elle ne fuie pas, et que ticker.Stop() libère le timer. Évoquez ensuite l'alternative. En production, vous utiliseriez généralement golang.org/x/time/rate plutôt que d'écrire cela vous-même, et savoir quand choisir la solution standard plutôt qu'une solution maison fait partie du jugement pour lequel on vous recrute.
Questions fréquentes
Quels sujets reviennent le plus en entretien Go senior ?
La concurrence, context, la gestion des erreurs, les interfaces et les génériques, à peu près dans cet ordre. La pondération suit ce que construisent les développeurs Go. 75 % écrivent des services API/RPC et 62 % des outils CLI (Go Developer Survey 2024 H2), et ces deux types de travail sont concurrents et pleins de possibilités d'échec. Aucune statistique publiée et crédible ne classe la fréquence de questions précises, alors méfiez-vous de tout blog qui prétend en avoir une.
Faut-il connaître les génériques pour un entretien Go ?
Vous devez les comprendre, et il est encore plus important de savoir quand ne pas les utiliser. Les génériques sont arrivés avec Go 1.18, le plus grand changement jamais apporté au langage (Go Blog), ils font donc partie du programme. La plupart du code Go idiomatique les évite toutefois encore, et les candidats seniors marquent des points en expliquant que les packages standard slices et maps couvrent la plupart des besoins.
Quelle importance a le race detector ?
Assez pour que les intervieweurs s'attendent à ce que vous l'évoquiez sans qu'on vous le demande. Le race detector d'Uber a trouvé près de 2 000 data races dans son monorepo Go, et 210 ingénieurs en ont corrigé environ 1 100 en six mois (Uber Engineering). Dire « Je lancerais go test -race » montre à l'intervieweur que vous avez travaillé sur du code de production.
Quelle version de Go faut-il viser ?
La toolchain stable actuelle, c'est-à-dire Go 1.26 au début de 2026 (notes de version de Go 1.26). Décrire le comportement d'anciennes versions trahit des connaissances datées, par exemple affirmer qu'il faut une boucle à trois clauses pour itérer sur un entier. La forme for range n fonctionne depuis Go 1.22.
Vaut-il encore la peine de se spécialiser en Go ?
Oui. 17,4 % des développeurs professionnels déclarent avoir beaucoup travaillé avec Go au cours de l'année écoulée (Stack Overflow Developer Survey 2025). Dans l'enquête officielle, 91 % des répondants se disent satisfaits du langage, et 63 % très satisfaits (2025 Go Developer Survey). La demande en ingénieurs Go seniors suit cette adoption.
Commencez votre parcours Go senior
Lire sur ces sujets ne signifie pas que vous saurez les écrire sous pression. Les entretiens seniors récompensent les automatismes : un worker pool que vous tapez sans réfléchir, une chaîne d'erreurs que vous encapsulez par réflexe, un générique dont vous savez vous passer. Vous y arrivez en écrivant du vrai Go chaque jour sur la toolchain actuelle, avec un retour sur le caractère idiomatique de votre code. Ce retour est ce qu'il y a de plus difficile à trouver, et les idiomes sont précisément ce qui pose le plus de difficultés à 33 % de la communauté Go.
LevelUpGo est conçu pour cela. Chaque leçon est un exercice dans le navigateur qui s'exécute sur la dernière version stable de Go et note votre code automatiquement. Vous vous entraînez ainsi de la manière dont l'entretien vous évalue, au lieu de vous contenter de lire.
Voici où pratiquer chaque sujet de cet article :
- Concurrence,
contextet channels : le cours Go Fundamentals construit les goroutines, les channels et l'annulation à partir des principes de base. Il est gratuit pour commencer et ne demande pas de carte bancaire. - Gestion des erreurs, interfaces et idiomes : le parcours Clean Go Code propose des exercices notés sur
errors.Is/errors.As, les petites interfaces et le sens de la conception abordé dans cet article. - La feuille de route LevelUpGo complète montre comment les cours progressent des fondamentaux jusqu'à la conception de niveau senior.
Commencez le cours gratuit, écrivez du Go chaque jour jusqu'à l'entretien, et vous arriverez en étant capable d'expliquer votre raisonnement au lieu de deviner.
