Chapitre 22
22 - Interviews
> Préparation complète aux entretiens techniques Go : 30 questions comportementales, 30 questions techniques, et 3 études de cas système.
Cours 22 : Préparation aux entretiens Go
300+ lignes de questions et réponses d'entretien.
Partie 1 : Questions Comportementales (30)
Q01. "Parlez-moi de vous et de votre expérience avec Go."
Réponse attendue : Structurez en 3 parties :
- Passé : début avec Go (projet, formation)
- Présent : projets actuels, stack technique
- Futur : ce que vous voulez apprendre/apporter
Exemple :
"J'ai découvert Go en 2020 lors d'un projet de migration d'un service Python vers Go pour des raisons de performance. J'ai été séduit par la simplicité du langage et l'excellente gestion de la concurrence. Depuis, j'ai travaillé sur 3 microservices Go en production, gérant des pics à 50k req/s. Aujourd'hui, je veux approfondir les patterns de concurrence avancés et contribuer à des projets open source Go."
Q02. "Décrivez un projet complexe que vous avez mené en Go."
Méthode STAR :
- Situation : contexte
- Tâche : objectif
- Action : ce que vous avez fait
- Résultat : mesures, impact
Exemple :
"Nous devions remplacer un système de traitement de logs legacy qui traitait 1M msg/s avec une latence de 500ms. J'ai conçu une architecture en pipeline Go avec worker pool et backpressure. Résultat : latence réduite à 50ms, consommation mémoire réduite de 60%, et code 3x plus court."
Q03. "Comment gérez-vous le conflit au sein d'une équipe technique ?"
Points clés :
- Écouter toutes les parties prenantes
- Se baser sur des données objectives
- Proposer des compromis (A/B testing, POC)
- Focus sur l'objectif commun
Q04. "Parlez-moi d'un échec technique et de ce que vous en avez appris."
Structure :
- Décrire l'échec (sans blâmer)
- Identifier la cause racine
- Expliquer ce qui a été appris
- Montrer comment vous avez changé
Exemple technique Go :
"J'ai déployé un service sans configurer ReadTimeout. En production, un client malveillant a ouvert des connexions sans envoyer de données, créant une fuite de goroutines. Le serveur est tombé après 10 minutes. J'ai appris à toujours configurer ReadTimeout, WriteTimeout, et à monitorer le nombre de goroutines."
Q05-Q30 (25 questions supplémentaires)
Q05. Comment priorisez-vous les tâches techniques ? Q06. Décrivez votre processus de revue de code idéal. Q07. Comment introduisez-vous une nouvelle technologie (ex: Go) dans une équipe ? Q08. Parlez-moi d'un incident de production critique. Q09. Comment gérez-vous une deadline irréaliste ? Q10. Quelle est votre plus grande contribution technique ? Q11. Comment restez-vous à jour avec l'écosystème Go ? Q12. Décrivez votre processus de débogage. Q13. Comment gérez-vous un membre d'équipe qui résiste au changement ? Q14. Quelle est votre approche du mentoring technique ? Q15. Comment équilibrez-vous vitesse et qualité ? Q16. Parlez-moi d'une fois où vous avez dû dire "non" à une demande. Q17. Comment mesurez-vous la performance de votre équipe ? Q18. Décrivez votre setup de développement Go idéal. Q19. Comment documentez-vous votre code et votre architecture ? Q20. Quelle est votre expérience avec les tests en production ? Q21. Comment gérez-vous la dette technique ? Q22. Parlez-moi d'une migration technique complexe. Q23. Comment abordez-vous la sécurité dans vos applications Go ? Q24. Décrivez une situation où vous avez dû apprendre rapidement. Q25. Quelle est votre philosophie sur les dépendances externes ? Q26. Comment contribuez-vous à la communauté technique ? Q27. Parlez-moi d'un projet open source Go que vous admirez. Q28. Comment gérez-vous le stress avant une mise en production ? Q29. Quelle est votre méthode pour estimer des tâches complexes ? Q30. Où vous voyez-vous dans 5 ans avec Go ?
Partie 2 : Questions Techniques Go (30)
T01 - Goroutines et Scheduling
Question : "Expliquez comment le scheduler Go fonctionne. Quelle est la différence entre le scheduling coopératif et préemptif ?"
Réponse : Le scheduler Go utilise le modèle M:N (M goroutines sur N threads OS). Depuis Go 1.14, le scheduler est préemptif : une goroutine peut être interrompue à n'importe quel point sûr (safe point) par le garbage collector ou le sysmon.
Points clés :
- G (goroutine) : stack dynamique (~2KB initial)
- M (machine) : thread OS
- P (processor) : contexte d'exécution (GOMAXPROCS)
- Work stealing : un P volant des goroutines à un P surchargé
- Hand off : si un M bloque (syscall), le P est passé à un autre M
// Exemple : GOMAXPROCS influence le parallélisme
runtime.GOMAXPROCS(4) // 4 threads OS maximum
T02 - Garbage Collector
Question : "Expliquez le GC Go. Comment fonctionne le tri-color mark-and-sweep ?"
Réponse : Go utilise un GC concurrent tri-color mark-and-sweep avec write barrier.
- White : objets non visités (à collecter)
- Grey : objets visités mais dont les enfants ne sont pas visités
- Black : objets visités dont tous les enfants sont visités
- Phase 1 - Mark Setup : STW très court (< 500µs)
- Phase 2 - Concurrent Mark : avec les goroutines, write barrier
- Phase 3 - Mark Termination : STW court
- Phase 4 - Sweep : concurrent, libère la mémoire
Tuning : GOGC=100 (default), GOGC=200 réduit la fréquence du GC mais augmente la mémoire.
T03 - Interfaces
Question : "Comment les interfaces sont-elles représentées en mémoire ? Quelle est la différence entre une interface nil et un pointeur nil dans une interface ?"
Réponse : Une interface Go est une structure à deux champs :
- type : pointeur vers le type concret (
*itabou*_type) - data : pointeur vers la valeur concrète
var w io.Writer // w = {type: nil, data: nil} → w == nil true
var b *bytes.Buffer // b = nil
w = b // w = {type: *bytes.Buffer, data: nil} → w == nil FALSE !
Piège classique : une interface n'est nil que si son type ET sa data sont nil.
func returnsNil() error {
var e *os.PathError = nil
return e // L'interface retournée n'est PAS nil !
}
T04 - Channels
Question : "Quelle est la différence entre un channel bufférisé et non-bufférisé ? Quand utiliser chacun ?"
Réponse :
- Non-bufférisé (
make(chan T)) : synchronisation garantie. L'envoi bloque jusqu'à réception. - Bufférisé (
make(chan T, N)) : découplage. L'envoi bloque seulement si le buffer est plein.
Cas d'usage :
- Non-bufférisé : signaux, synchronisation, fan-in/fan-out
- Bufférisé : pipelines avec taux de production/consommation variable
- Attention : un channel bufférisé peut masquer des bugs (deadlock retardé)
// Channel non-bufférisé : synchronisation
done := make(chan struct{})
go func() { work(); close(done) }()
<-done // Attend la fin
// Channel bufférisé : découplage
jobs := make(chan Job, 100)
for i := 0; i < 10; i++ {
go worker(jobs)
}
for _, j := range allJobs {
jobs <- j
}
T05 - Context
Question : "Comment utiliser le package context ? Quels sont les pièges courants ?"
Réponse :
// Création
ctx := context.Background() // racine
ctx := context.TODO() // quand le contexte n'est pas encore décidé
ctx, cancel := context.WithCancel(ctx)
ctx, cancel := context.WithTimeout(ctx, 5*time.Second)
ctx := context.WithValue(ctx, key, val) // ⚠️ à éviter pour les paramètres
// Propagation
func handler(ctx context.Context, req Request) error {
ctx, cancel := context.WithTimeout(ctx, 3*time.Second)
defer cancel() // TOUJOURS appeler cancel pour libérer les ressources
return doWork(ctx)
}
Pièges :
- Oublier de
cancel()→ fuite de goroutines context.WithValuepour des paramètres optionnels (magique, à éviter)- Passer
nilcomme contexte → utilisercontext.TODO()à la place
T06 - sync.WaitGroup vs errgroup
Question : "Comparez sync.WaitGroup et errgroup. Quand utiliser l'un plutôt que l'autre ?"
Réponse :
// sync.WaitGroup : simple synchronisation
var wg sync.WaitGroup
for i := 0; i < 5; i++ {
wg.Add(1)
go func() {
defer wg.Done()
work()
}()
}
wg.Wait()
// errgroup : synchronisation + propagation d'erreur + annulation
g, ctx := errgroup.WithContext(context.Background())
for i := 0; i < 5; i++ {
g.Go(func() error {
select {
case <-ctx.Done():
return ctx.Err()
default:
return work()
}
})
}
if err := g.Wait(); err != nil {
log.Fatal(err)
}
errgroup est préférable quand vous avez besoin de :
- Propager la première erreur
- Annuler les autres tâches en cas d'erreur
- Utiliser un contexte partagé
T07 - Data Races
Question : "Comment détecter et prévenir les data races en Go ?"
Réponse :
Détection :
go run -race main.go
go test -race ./...
Prévention :
- Utiliser des channels (CSP)
- Utiliser
sync.Mutex,sync.RWMutex - Utiliser
sync/atomicpour les opérations atomiques - Contexte : interfaces pour le confinement lexical
// Data race
var counter int
go func() { counter++ }()
go func() { counter++ }() // race !
// Correction avec Mutex
var mu sync.Mutex
go func() { mu.Lock(); counter++; mu.Unlock() }()
// Correction avec atomic
var counter atomic.Int64
go func() { counter.Add(1) }()
// Correction avec channel
ch := make(chan int, 1)
ch <- 0 // valeur initiale
go func() { ch <- <-ch + 1 }()
T08 - Panic et Recover
Question : "Quand faut-il utiliser panic/recover en Go ?"
Réponse :
- panic : pour des erreurs vraiment irrécupérables (initialisation, erreur de programmation)
- recover : dans des
deferpour :- Middleware HTTP (recoverer)
- Goroutines autonomes (ne pas faire crasher tout le programme)
- Services critiques (logging + cleanup)
// Exemple : recover dans un worker
func worker() {
defer func() {
if r := recover(); r != nil {
log.Printf("worker panicked: %v", r)
// Cleanup et redémarrage
go worker()
}
}()
// code du worker
}
T09 - sync.Pool
Question : "À quoi sert sync.Pool et comment l'utiliser correctement ?"
Réponse :
sync.Pool maintient un pool d'objets réutilisables pour réduire les allocations.
var pool = sync.Pool{
New: func() interface{} {
return &bytes.Buffer{}
},
}
func process(data []byte) string {
buf := pool.Get().(*bytes.Buffer)
buf.Reset()
defer pool.Put(buf)
buf.Write(data)
return buf.String()
}
Points importants :
- Les objets peuvent être collectés par le GC à tout moment
- Ne pas supposer qu'un objet remis dans le pool est toujours disponible
- Utile pour les buffers temporaires, les connexions, les objets lourds
T10 - Escape Analysis
Question : "Qu'est-ce que l'escape analysis et comment influence-t-elle la performance ?"
Réponse : L'escape analysis détermine si une variable peut être allouée sur la stack (rapide) ou doit être sur le heap (GC).
// Sur la stack (efficace)
func sum() int {
a := 1 // n'escape pas
b := 2 // n'escape pas
return a + b
}
// Sur le heap (GC overhead)
func newUser() *User {
u := User{} // escape car retourné par pointeur
return &u
}
// Indirect escape
func print() {
u := User{}
fmt.Println(u) // u escape car passé à interface{}
}
Pour minimiser les allocations :
- Préférer les receivers valeur aux pointeurs
- Éviter les allocations dans les boucles chaudes
- Utiliser
sync.Poolpour les objets temporaires
T11 - Testing table-driven
Question : "Quel est le pattern table-driven testing et pourquoi est-il idiomatique en Go ?"
Réponse :
func TestParse(t *testing.T) {
tests := []struct {
name string
input string
want int
wantErr bool
}{
{name: "simple", input: "42", want: 42},
{name: "negative", input: "-5", want: -5},
{name: "invalid", input: "abc", wantErr: true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := Parse(tt.input)
if (err != nil) != tt.wantErr {
t.Errorf("Parse(%q) error = %v, wantErr %v", tt.input, err, tt.wantErr)
}
if got != tt.want {
t.Errorf("Parse(%q) = %d, want %d", tt.input, got, tt.want)
}
})
}
}
Avantages :
- Structure claire et extensible
- Facile d'ajouter des cas
- Chaque sous-test est indépendant
- Compatible avec
-runpour exécuter un cas spécifique
T12-T30 (Questions supplémentaires)
T12. "Expliquez le modèle mémoire Go et le principe happens-before."
T13. "Comment implémenter un pattern singleton thread-safe en Go ?"
T14. "Quels sont les pièges de range avec les goroutines et closures ?"
T15. "Comparez database/sql, pgx, et gorm. Quand utiliser chacun ?"
T16. "Expliquez le fonctionnement de select et le choix aléatoire entre les cas."
T17. "Comment faire un graceful shutdown d'un serveur gRPC ?"
T18. "Quelle est la différence entre go build, go install, et go run ?"
T19. "Comment fonctionne la résolution de dépendances dans Go modules ?"
T20. "Expliquez le pattern accept interfaces, return structs."
T21. "Comment gérez-vous les migrations de base de données en Go ?"
T22. "Quelle est la différence entre un type alias et une type definition ?"
T23. "Comment fonctionne le build cache de Go ? Quand utiliser -count=1 ?"
T24. "Expliquez les build tags et leur utilité pour les tests d'intégration."
T25. "Comment implémenter un rate limiter concurrent en Go ?"
T26. "Quelle est l'utilité de runtime.NumGoroutine() dans le debugging ?"
T27. "Comparez le http.ServerMux standard vs chi ou gorilla/mux."
T28. "Comment sécuriser une API Go contre les attaques courantes (XSS, CSRF) ?"
T29. "Expliquez le mécanisme de embed en Go 1.16."
T30. "Comment instrumenter une application Go avec OpenTelemetry ?"
Partie 3 : System Design
Cas 1 : URL Shortener (bit.ly, tinyurl)
Exigences fonctionnelles :
- Générer une URL courte unique à partir d'une longue
- Rediriger l'URL courte vers l'originale
- Option d'expiration personnalisable
- Analytics (clics, géo, referrer)
Exigences non-fonctionnelles :
- 100M URLs par mois
- 10K reads/s en pic
- Latence < 100ms (95e percentile)
- HA (99.99%)
Design Go :
┌─────────────┐
Client ──POST──► │ API Gateway │
└──────┬──────┘
│
┌─────────────┼─────────────┐
▼ ▼ ▼
┌──────────┐ ┌──────────┐ ┌──────────┐
│ Shorten │ │ Redirect │ │Analytics │
│ Service │ │ Service │ │ Service │
└────┬─────┘ └────┬─────┘ └────┬─────┘
│ │ │
┌────▼────────────▼────────────▼────┐
│ Redis Cache │
└────────────────┬──────────────────┘
│
┌────────────────▼──────────────────┐
│ PostgreSQL (Sharded) │
└───────────────────────────────────┘
Génération d'ID :
// Base62 encoding
const base62 = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
func Encode(id uint64) string {
var buf []byte
for id > 0 {
buf = append(buf, base62[id%62])
id /= 62
}
return string(buf)
}
// Avec Twitter Snowflake pour l'ID unique
type Snowflake struct {
mu sync.Mutex
timestamp int64
nodeID int64
sequence int64
}
Scaling :
- Read replicas PostgreSQL
- Redis cluster pour le cache
- Sharding par hash de l'ID court
- CDN (CloudFront, Cloudflare) pour les analytics
Cas 2 : Real-time Chat Platform
Exigences :
- Messages instantanés (latence < 200ms)
- Historique persistant
- Groupes et channels
- Typing indicators, read receipts
- Support mobile + web
Design Go :
┌─────────────┐
Client WebSocket ──►│ LB/Proxy │
└──────┬──────┘
│
┌────────────┼────────────┐
▼ ▼ ▼
┌──────────┐ ┌──────────┐ ┌──────────┐
│ Chat Svc │ │ Chat Svc │ │ Chat Svc │
│ Node 1 │ │ Node 2 │ │ Node N │
└────┬─────┘ └────┬─────┘ └────┬─────┘
│ │ │
┌────▼────────────▼────────────▼────┐
│ Redis Pub/Sub │
└────────────────┬──────────────────┘
│
┌────────────────▼──────────────────┐
│ PostgreSQL/NoSQL │
└───────────────────────────────────┘
WebSocket Hub :
type Hub struct {
rooms map[string]map[*Client]bool
register chan *Client
mu sync.RWMutex
}
type Client struct {
room string
send chan Message
conn *websocket.Conn
}
func (h *Hub) Run() {
for {
select {
case client := <-h.register:
h.mu.Lock()
if _, ok := h.rooms[client.room]; !ok {
h.rooms[client.room] = make(map[*Client]bool)
}
h.rooms[client.room][client] = true
h.mu.Unlock()
}
}
}
Cas 3 : Distributed Event Processing Pipeline
Exigences :
- 1M events/sec throughput
- Exactly-once processing (ou at-least-once)
- Analytics en temps réel
- Scalabilité horizontale
Design Go :
Producers ──► Kafka ──► Event Processor ──► Sink
│
┌─────┴─────┐
│ │
Windowed Alerting
Aggregator Engine
│ │
└─────┬─────┘
▼
ClickHouse
/ BigQuery
Event Processor :
type Processor struct {
consumer *kafka.Consumer
window *SlidingWindow
alert *AlertEngine
batch *BatchWriter
}
func (p *Processor) Process(ctx context.Context) error {
for {
msg, err := p.consumer.ReadMessage(ctx)
if err != nil {
return err
}
event, err := parseEvent(msg.Value)
if err != nil {
p.metrics.Errors.Inc()
continue
}
// Exactly-once via idempotency key
if p.isDuplicate(event.ID) {
continue
}
// Enrichissement
p.enrich(event)
// Fenêtre glissante
p.window.Add(event)
// Alerting
p.alert.Evaluate(event)
// Batch write
p.batch.Add(event)
p.metrics.Processed.Inc()
}
}
Patterns Go utilisés :
- Fan-out/Fan-in pour le traitement parallèle
- errgroup pour la coordination
- Semaphore pattern pour le backpressure
- sync.Pool pour la réutilisation des buffers
Ces trois sections couvrent l'ensemble des sujets typiques d'un entretien Go senior. La clé est de :
- Structurer vos réponses (STAR pour les comportementales)
- Montrer votre compréhension des mécanismes internes (GC, scheduler)
- Proposer des architectures adaptées (system design)
- Citer des patterns Go spécifiques (channels, contexts, fan-in/out)