MFormations
Modern Go Engineering

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 :

  1. Passé : début avec Go (projet, formation)
  2. Présent : projets actuels, stack technique
  3. 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 :

  1. Décrire l'échec (sans blâmer)
  2. Identifier la cause racine
  3. Expliquer ce qui a été appris
  4. 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 (*itab ou *_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 :

  1. Oublier de cancel() → fuite de goroutines
  2. context.WithValue pour des paramètres optionnels (magique, à éviter)
  3. Passer nil comme contexte → utiliser context.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 :

  1. Utiliser des channels (CSP)
  2. Utiliser sync.Mutex, sync.RWMutex
  3. Utiliser sync/atomic pour les opérations atomiques
  4. 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 defer pour :
    1. Middleware HTTP (recoverer)
    2. Goroutines autonomes (ne pas faire crasher tout le programme)
    3. 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.Pool pour 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 -run pour 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 :

  1. Structurer vos réponses (STAR pour les comportementales)
  2. Montrer votre compréhension des mécanismes internes (GC, scheduler)
  3. Proposer des architectures adaptées (system design)
  4. Citer des patterns Go spécifiques (channels, contexts, fan-in/out)