Top 25 questions d'entretien Go : Guide complet pour développeurs

Préparez vos entretiens Go avec les 25 questions les plus posées. Goroutines, channels, interfaces, concurrence et patterns avancés avec exemples de code.

Questions d'entretien Go - Guide complet de préparation

Les entretiens techniques Go testent la compréhension des concepts fondamentaux du langage : concurrence, gestion mémoire et patterns idiomatiques. Ce guide couvre les 25 questions les plus fréquentes avec des réponses détaillées et des exemples de code.

Conseil de préparation

Go valorise la simplicité et la lisibilité. Les recruteurs cherchent des réponses concises démontrant une compréhension profonde plutôt que des solutions complexes.

Fondamentaux du langage Go

1. Quelle est la différence entre var et := ?

La déclaration var permet de spécifier explicitement le type et fonctionne au niveau package. L'opérateur := infère le type automatiquement mais ne fonctionne qu'à l'intérieur des fonctions.

declaration.gogo
package main

// Niveau package - var obligatoire
var globalConfig = "production"

func main() {
    // var avec type explicite
    var count int = 10

    // var avec inférence de type
    var name = "Alice"

    // Déclaration courte - uniquement dans les fonctions
    age := 25

    // Déclaration multiple
    var (
        host = "localhost"
        port = 8080
    )
}

La déclaration courte := est préférée dans les fonctions pour sa concision, tandis que var reste nécessaire pour les variables de package.

2. Comment fonctionne le système de types en Go ?

Go utilise un typage statique avec inférence de types. Le langage distingue les types valeur (copiés lors de l'assignation) des types référence (partagent la même structure sous-jacente).

types.gogo
package main

import "fmt"

func main() {
    // Types valeur - copie complète
    a := [3]int{1, 2, 3}
    b := a          // Copie du tableau
    b[0] = 100      // Ne modifie pas a
    fmt.Println(a)  // [1 2 3]

    // Types référence - partagent les données
    slice1 := []int{1, 2, 3}
    slice2 := slice1    // Même tableau sous-jacent
    slice2[0] = 100     // Modifie aussi slice1
    fmt.Println(slice1) // [100 2 3]

    // Maps sont aussi des références
    m1 := map[string]int{"a": 1}
    m2 := m1
    m2["a"] = 100
    fmt.Println(m1["a"]) // 100
}

Les arrays sont des types valeur, tandis que slices, maps et channels sont des types référence.

3. Expliquez la différence entre arrays et slices

Les arrays ont une taille fixe définie à la compilation. Les slices sont des vues dynamiques sur un array sous-jacent avec trois composants : pointeur, longueur et capacité.

arrays_slices.gogo
package main

import "fmt"

func main() {
    // Array - taille fixe, type valeur
    arr := [5]int{1, 2, 3, 4, 5}

    // Slice - vue sur l'array
    slice := arr[1:4]  // [2 3 4]
    fmt.Printf("len=%d, cap=%d\n", len(slice), cap(slice))
    // len=3, cap=4

    // Modification affecte l'array original
    slice[0] = 20
    fmt.Println(arr) // [1 20 3 4 5]

    // Création directe avec make
    dynamic := make([]int, 3, 10)
    // len=3, cap=10

    // Append peut réallouer
    dynamic = append(dynamic, 1, 2, 3, 4, 5)
}

Les slices sont le type privilégié pour les collections dynamiques en Go.

4. Comment fonctionne l'instruction defer ?

defer planifie l'exécution d'une fonction à la fin de la fonction englobante. Les appels différés s'empilent et s'exécutent en ordre LIFO (Last In, First Out).

defer.gogo
package main

import (
    "fmt"
    "os"
)

func main() {
    // Ordre LIFO
    defer fmt.Println("1")
    defer fmt.Println("2")
    defer fmt.Println("3")
    // Affiche: 3, 2, 1
}

// Cas d'usage typique : libération de ressources
func readFile(path string) ([]byte, error) {
    file, err := os.Open(path)
    if err != nil {
        return nil, err
    }
    defer file.Close() // Toujours exécuté

    // Lecture du fichier...
    return os.ReadFile(path)
}

// Attention : les arguments sont évalués immédiatement
func deferArgs() {
    x := 10
    defer fmt.Println(x) // Capture 10
    x = 20
    // Affiche: 10
}

defer garantit l'exécution même en cas de panic, ce qui en fait l'outil idéal pour le nettoyage des ressources.

5. Qu'est-ce qu'une interface en Go ?

Une interface définit un ensemble de méthodes. Tout type implémentant ces méthodes satisfait implicitement l'interface, sans déclaration explicite.

interfaces.gogo
package main

import "fmt"

// Définition d'interface
type Writer interface {
    Write([]byte) (int, error)
}

// Type qui implémente Writer implicitement
type FileLogger struct {
    path string
}

func (f *FileLogger) Write(data []byte) (int, error) {
    // Écriture dans le fichier
    fmt.Println("Writing to", f.path)
    return len(data), nil
}

// Interface vide - accepte tout type
func printAny(v interface{}) {
    fmt.Printf("Type: %T, Value: %v\n", v, v)
}

// Type assertion
func process(w Writer) {
    // Vérification de type
    if fl, ok := w.(*FileLogger); ok {
        fmt.Println("FileLogger with path:", fl.path)
    }
}

L'implémentation implicite d'interfaces permet un découplage fort entre les packages.

Concurrence et Goroutines

6. Qu'est-ce qu'une goroutine et comment diffère-t-elle d'un thread ?

Une goroutine est un thread léger géré par le runtime Go. Elle utilise quelques Ko de stack (contre plusieurs Mo pour un thread OS) et le scheduler Go multiplexe des milliers de goroutines sur quelques threads système.

goroutines.gogo
package main

import (
    "fmt"
    "sync"
    "time"
)

func main() {
    var wg sync.WaitGroup

    // Lancement de 1000 goroutines
    for i := 0; i < 1000; i++ {
        wg.Add(1)
        go func(id int) {
            defer wg.Done()
            time.Sleep(100 * time.Millisecond)
            fmt.Printf("Goroutine %d terminée\n", id)
        }(i) // Passage de i par valeur
    }

    wg.Wait()
    fmt.Println("Toutes les goroutines terminées")
}
Piège courant

Ne pas oublier de passer les variables de boucle par valeur à la goroutine. Sinon, toutes les goroutines peuvent capturer la même valeur finale.

7. Expliquez le fonctionnement des channels

Les channels permettent la communication et synchronisation entre goroutines. Ils peuvent être buffered (avec capacité) ou unbuffered (synchrones).

channels.gogo
package main

import "fmt"

func main() {
    // Channel unbuffered - bloque jusqu'à réception
    ch := make(chan int)

    go func() {
        ch <- 42 // Bloque jusqu'à lecture
    }()

    value := <-ch // Reçoit la valeur
    fmt.Println(value)

    // Channel buffered - ne bloque pas tant que non plein
    buffered := make(chan string, 2)
    buffered <- "premier"
    buffered <- "second"
    // buffered <- "troisième" // Bloquerait

    fmt.Println(<-buffered) // "premier"
    fmt.Println(<-buffered) // "second"
}

Les channels unbuffered garantissent la synchronisation, tandis que les buffered permettent le découplage temporel.

8. Comment utiliser select avec plusieurs channels ?

select permet d'attendre sur plusieurs opérations de channel simultanément. La première opération prête est exécutée, avec choix aléatoire en cas d'égalité.

select.gogo
package main

import (
    "fmt"
    "time"
)

func main() {
    ch1 := make(chan string)
    ch2 := make(chan string)

    go func() {
        time.Sleep(100 * time.Millisecond)
        ch1 <- "depuis ch1"
    }()

    go func() {
        time.Sleep(200 * time.Millisecond)
        ch2 <- "depuis ch2"
    }()

    // Attente avec timeout
    for i := 0; i < 2; i++ {
        select {
        case msg := <-ch1:
            fmt.Println(msg)
        case msg := <-ch2:
            fmt.Println(msg)
        case <-time.After(500 * time.Millisecond):
            fmt.Println("Timeout")
        }
    }

    // Select non-bloquant avec default
    select {
    case msg := <-ch1:
        fmt.Println(msg)
    default:
        fmt.Println("Pas de message disponible")
    }
}

select est l'outil fondamental pour gérer la concurrence en Go de manière élégante.

9. Comment éviter les race conditions ?

Les race conditions surviennent quand plusieurs goroutines accèdent aux mêmes données sans synchronisation. Go offre plusieurs mécanismes de protection.

race_conditions.gogo
package main

import (
    "fmt"
    "sync"
    "sync/atomic"
)

// Solution 1: Mutex
type SafeCounter struct {
    mu    sync.Mutex
    count int
}

func (c *SafeCounter) Increment() {
    c.mu.Lock()
    defer c.mu.Unlock()
    c.count++
}

// Solution 2: RWMutex pour lectures fréquentes
type Cache struct {
    mu   sync.RWMutex
    data map[string]string
}

func (c *Cache) Get(key string) string {
    c.mu.RLock()         // Plusieurs lecteurs simultanés
    defer c.mu.RUnlock()
    return c.data[key]
}

func (c *Cache) Set(key, value string) {
    c.mu.Lock()          // Un seul writer
    defer c.mu.Unlock()
    c.data[key] = value
}

// Solution 3: atomic pour compteurs simples
var atomicCounter int64

func incrementAtomic() {
    atomic.AddInt64(&atomicCounter, 1)
}

func main() {
    // Détection: go run -race main.go
    counter := SafeCounter{}
    var wg sync.WaitGroup

    for i := 0; i < 1000; i++ {
        wg.Add(1)
        go func() {
            defer wg.Done()
            counter.Increment()
        }()
    }

    wg.Wait()
    fmt.Println("Count:", counter.count)
}

L'option -race du compilateur détecte les race conditions à l'exécution.

10. Expliquez le pattern worker pool

Le pattern worker pool limite la concurrence en créant un nombre fixe de goroutines qui traitent les tâches d'une queue.

worker_pool.gogo
package main

import (
    "fmt"
    "sync"
    "time"
)

func worker(id int, jobs <-chan int, results chan<- int, wg *sync.WaitGroup) {
    defer wg.Done()
    for job := range jobs {
        fmt.Printf("Worker %d traite job %d\n", id, job)
        time.Sleep(100 * time.Millisecond) // Simulation travail
        results <- job * 2
    }
}

func main() {
    const numJobs = 10
    const numWorkers = 3

    jobs := make(chan int, numJobs)
    results := make(chan int, numJobs)

    var wg sync.WaitGroup

    // Démarrage des workers
    for w := 1; w <= numWorkers; w++ {
        wg.Add(1)
        go worker(w, jobs, results, &wg)
    }

    // Envoi des jobs
    for j := 1; j <= numJobs; j++ {
        jobs <- j
    }
    close(jobs)

    // Attente et fermeture results
    go func() {
        wg.Wait()
        close(results)
    }()

    // Collecte des résultats
    for result := range results {
        fmt.Println("Résultat:", result)
    }
}

Ce pattern évite la surcharge mémoire et CPU liée à la création de trop nombreuses goroutines.

Prêt à réussir tes entretiens Go ?

Entraîne-toi avec nos simulateurs interactifs, fiches express et tests techniques.

Gestion d'erreurs et Panic/Recover

11. Comment gérer les erreurs en Go ?

Go utilise des valeurs de retour explicites pour les erreurs, sans exceptions. Par convention, l'erreur est le dernier paramètre retourné.

errors.gogo
package main

import (
    "errors"
    "fmt"
)

// Erreurs sentinelles pour comparaison
var (
    ErrNotFound     = errors.New("ressource non trouvée")
    ErrUnauthorized = errors.New("accès non autorisé")
)

// Type d'erreur personnalisé
type ValidationError struct {
    Field   string
    Message string
}

func (e *ValidationError) Error() string {
    return fmt.Sprintf("validation %s: %s", e.Field, e.Message)
}

func validateAge(age int) error {
    if age < 0 {
        return &ValidationError{
            Field:   "age",
            Message: "doit être positif",
        }
    }
    return nil
}

func main() {
    // Vérification basique
    if err := validateAge(-5); err != nil {
        // Type assertion pour erreur personnalisée
        var valErr *ValidationError
        if errors.As(err, &valErr) {
            fmt.Printf("Champ: %s\n", valErr.Field)
        }
    }

    // Comparaison avec erreur sentinelle
    err := findUser("unknown")
    if errors.Is(err, ErrNotFound) {
        fmt.Println("Utilisateur introuvable")
    }
}

func findUser(id string) error {
    // Wrapping d'erreur avec contexte
    return fmt.Errorf("findUser %s: %w", id, ErrNotFound)
}

Le wrapping avec %w permet de chaîner les erreurs tout en conservant la possibilité de tester l'erreur originale.

12. Quand utiliser panic et recover ?

panic interrompt l'exécution normale et remonte la stack. recover capture le panic dans un defer et permet de reprendre l'exécution.

panic_recover.gogo
package main

import "fmt"

func safeOperation() (err error) {
    defer func() {
        if r := recover(); r != nil {
            err = fmt.Errorf("recovered from panic: %v", r)
        }
    }()

    riskyOperation()
    return nil
}

func riskyOperation() {
    // Simule une opération qui peut panic
    panic("quelque chose s'est mal passé")
}

// Cas d'usage légitime : validation à l'initialisation
func MustCompileRegex(pattern string) *Regexp {
    r, err := regexp.Compile(pattern)
    if err != nil {
        panic(err) // Erreur de programmation
    }
    return r
}

func main() {
    err := safeOperation()
    if err != nil {
        fmt.Println("Erreur récupérée:", err)
    }
    fmt.Println("Programme continue")
}
Règle d'or

Utiliser panic uniquement pour les erreurs de programmation (invariants violés). Pour les erreurs attendues (fichier manquant, réseau), toujours retourner une erreur.

Structs, Méthodes et Embedding

13. Quelle est la différence entre value receiver et pointer receiver ?

Un value receiver reçoit une copie de la struct, tandis qu'un pointer receiver reçoit une référence et peut modifier l'original.

receivers.gogo
package main

import "fmt"

type Counter struct {
    value int
}

// Value receiver - travaille sur une copie
func (c Counter) GetValue() int {
    return c.value
}

// Pointer receiver - modifie l'original
func (c *Counter) Increment() {
    c.value++
}

// Pointer receiver si struct est grande (évite copie)
type LargeStruct struct {
    data [1000]int
}

func (l *LargeStruct) Process() {
    // Évite de copier 8000 octets
}

func main() {
    c := Counter{value: 0}
    c.Increment() // Go convertit automatiquement
    fmt.Println(c.GetValue()) // 1

    // Attention avec les interfaces
    var _ fmt.Stringer = &c // OK si méthode sur *Counter
}

Règle : si une méthode utilise un pointer receiver, toutes les méthodes du type devraient aussi l'utiliser pour la cohérence.

14. Comment fonctionne l'embedding en Go ?

L'embedding permet d'inclure un type dans un autre, héritant de ses méthodes et champs. Ce n'est pas de l'héritage classique mais de la composition.

embedding.gogo
package main

import "fmt"

type Logger struct {
    prefix string
}

func (l *Logger) Log(msg string) {
    fmt.Printf("[%s] %s\n", l.prefix, msg)
}

// Embedding du Logger
type Service struct {
    *Logger // Embedding par pointeur
    name    string
}

func NewService(name string) *Service {
    return &Service{
        Logger: &Logger{prefix: name},
        name:   name,
    }
}

func main() {
    svc := NewService("API")

    // Méthode promue - accès direct
    svc.Log("Démarrage")

    // Accès explicite aussi possible
    svc.Logger.Log("Explicite")

    // Champ promu
    fmt.Println(svc.prefix) // "API"
}

L'embedding permet de construire des compositions flexibles tout en évitant la rigidité de l'héritage.

15. Comment implémenter le pattern singleton en Go ?

Le package sync offre sync.Once pour garantir l'exécution unique d'une initialisation, même avec des goroutines concurrentes.

singleton.gogo
package main

import (
    "fmt"
    "sync"
)

type Database struct {
    connectionString string
}

var (
    instance *Database
    once     sync.Once
)

func GetDatabase() *Database {
    once.Do(func() {
        fmt.Println("Initialisation unique")
        instance = &Database{
            connectionString: "postgres://...",
        }
    })
    return instance
}

func main() {
    // Appels concurrents - une seule initialisation
    var wg sync.WaitGroup
    for i := 0; i < 10; i++ {
        wg.Add(1)
        go func() {
            defer wg.Done()
            db := GetDatabase()
            fmt.Printf("Instance: %p\n", db)
        }()
    }
    wg.Wait()
}

sync.Once est thread-safe et plus élégant que l'utilisation d'un mutex avec double-check locking.

Context et Annulation

16. À quoi sert le package context ?

Le package context gère les deadlines, signaux d'annulation et valeurs request-scoped à travers l'arbre d'appels.

context.gogo
package main

import (
    "context"
    "fmt"
    "time"
)

func main() {
    // Context avec timeout
    ctx, cancel := context.WithTimeout(
        context.Background(),
        2*time.Second,
    )
    defer cancel() // Toujours appeler cancel

    result := make(chan string, 1)

    go func() {
        // Simulation d'opération longue
        time.Sleep(3 * time.Second)
        result <- "terminé"
    }()

    select {
    case res := <-result:
        fmt.Println(res)
    case <-ctx.Done():
        fmt.Println("Timeout:", ctx.Err())
    }
}

// Propagation dans les fonctions
func fetchData(ctx context.Context, url string) ([]byte, error) {
    // Vérification précoce
    if ctx.Err() != nil {
        return nil, ctx.Err()
    }

    req, err := http.NewRequestWithContext(ctx, "GET", url, nil)
    if err != nil {
        return nil, err
    }

    // Le client HTTP respecte le context
    resp, err := http.DefaultClient.Do(req)
    // ...
}

Toute fonction potentiellement longue devrait accepter un context.Context en premier paramètre.

17. Comment gérer l'annulation gracieuse d'un programme ?

Les signaux système comme SIGINT et SIGTERM peuvent être capturés pour permettre un arrêt propre.

graceful_shutdown.gogo
package main

import (
    "context"
    "fmt"
    "os"
    "os/signal"
    "syscall"
    "time"
)

func main() {
    // Context annulé sur signal
    ctx, stop := signal.NotifyContext(
        context.Background(),
        syscall.SIGINT,
        syscall.SIGTERM,
    )
    defer stop()

    // Démarrage du serveur
    server := startServer()

    // Attente du signal
    <-ctx.Done()
    fmt.Println("\nArrêt en cours...")

    // Timeout pour shutdown gracieux
    shutdownCtx, cancel := context.WithTimeout(
        context.Background(),
        5*time.Second,
    )
    defer cancel()

    if err := server.Shutdown(shutdownCtx); err != nil {
        fmt.Println("Erreur shutdown:", err)
    }

    fmt.Println("Arrêt terminé")
}

Ce pattern garantit que les connexions en cours se terminent proprement avant l'arrêt.

Testing et Benchmarks

18. Comment écrire des tests en Go ?

Le package testing intégré offre les fonctionnalités de base. Les tests résident dans des fichiers *_test.go.

calculator_test.gogo
package calculator

import "testing"

func TestAdd(t *testing.T) {
    result := Add(2, 3)
    if result != 5 {
        t.Errorf("Add(2, 3) = %d; want 5", result)
    }
}

// Tests tabulaires
func TestAddTableDriven(t *testing.T) {
    tests := []struct {
        name     string
        a, b     int
        expected int
    }{
        {"positifs", 2, 3, 5},
        {"négatifs", -1, -1, -2},
        {"mixte", -1, 5, 4},
        {"zéro", 0, 0, 0},
    }

    for _, tt := range tests {
        t.Run(tt.name, func(t *testing.T) {
            result := Add(tt.a, tt.b)
            if result != tt.expected {
                t.Errorf("Add(%d, %d) = %d; want %d",
                    tt.a, tt.b, result, tt.expected)
            }
        })
    }
}

Les tests tabulaires (table-driven tests) sont le pattern idiomatique en Go pour tester plusieurs cas.

19. Comment écrire des benchmarks ?

Les benchmarks utilisent testing.B et s'exécutent avec go test -bench.

benchmark_test.gogo
package main

import (
    "strings"
    "testing"
)

func BenchmarkStringConcat(b *testing.B) {
    for i := 0; i < b.N; i++ {
        var s string
        for j := 0; j < 100; j++ {
            s += "a"
        }
    }
}

func BenchmarkStringBuilder(b *testing.B) {
    for i := 0; i < b.N; i++ {
        var sb strings.Builder
        for j := 0; j < 100; j++ {
            sb.WriteString("a")
        }
        _ = sb.String()
    }
}

// Résultat typique:
// BenchmarkStringConcat-8      50000    28000 ns/op
// BenchmarkStringBuilder-8   1000000     1200 ns/op

Les benchmarks révèlent les différences de performance entre implémentations.

Génériques (Go 1.18+)

20. Comment utiliser les génériques en Go ?

Go 1.18 a introduit les paramètres de type, permettant d'écrire du code générique tout en conservant la sécurité des types.

generics.gogo
package main

import "fmt"

// Fonction générique
func Map[T, U any](slice []T, fn func(T) U) []U {
    result := make([]U, len(slice))
    for i, v := range slice {
        result[i] = fn(v)
    }
    return result
}

// Contrainte de type personnalisée
type Number interface {
    int | int64 | float64
}

func Sum[T Number](values []T) T {
    var sum T
    for _, v := range values {
        sum += v
    }
    return sum
}

// Type générique
type Stack[T any] struct {
    items []T
}

func (s *Stack[T]) Push(item T) {
    s.items = append(s.items, item)
}

func (s *Stack[T]) Pop() (T, bool) {
    if len(s.items) == 0 {
        var zero T
        return zero, false
    }
    item := s.items[len(s.items)-1]
    s.items = s.items[:len(s.items)-1]
    return item, true
}

func main() {
    // Utilisation
    doubled := Map([]int{1, 2, 3}, func(n int) int {
        return n * 2
    })
    fmt.Println(doubled) // [2 4 6]

    fmt.Println(Sum([]int{1, 2, 3, 4, 5})) // 15

    stack := &Stack[string]{}
    stack.Push("hello")
    stack.Push("world")
    val, _ := stack.Pop()
    fmt.Println(val) // "world"
}

Les génériques éliminent le besoin de code dupliqué ou de l'utilisation de interface{}.

Modules et Dépendances

21. Comment fonctionne le système de modules Go ?

Les modules Go gèrent les dépendances avec versioning sémantique. Le fichier go.mod définit le module et ses dépendances.

go.mod exemplego
module github.com/user/myproject

go 1.21

require (
    github.com/gin-gonic/gin v1.9.1
    github.com/lib/pq v1.10.9
)

// Commandes essentielles:
// go mod init github.com/user/project
// go mod tidy        - nettoie les dépendances
// go get package@v1.2.3 - ajoute/met à jour
// go mod vendor      - copie localement
bash
# Mise à jour des dépendances
go get -u ./...           # Toutes les dépendances
go get -u=patch ./...     # Patchs uniquement

Le fichier go.sum contient les checksums cryptographiques pour garantir l'intégrité des dépendances.

22. Comment structurer un projet Go ?

La structure standard suit les conventions de la communauté, sans imposer de règles strictes.

text
myproject/
├── cmd/
│   └── api/
│       └── main.go      # Point d'entrée
├── internal/            # Code privé au module
│   ├── handler/
│   ├── service/
│   └── repository/
├── pkg/                 # Code réutilisable externe
├── go.mod
├── go.sum
└── README.md

Le dossier internal est spécial : son contenu ne peut pas être importé par d'autres modules.

Questions Avancées

23. Comment fonctionne le garbage collector en Go ?

Go utilise un garbage collector concurrent, tri-color mark-and-sweep, optimisé pour une faible latence.

gc_optimization.gogo
package main

import "runtime"

func main() {
    // Configuration du GC
    // GOGC=100 (défaut) - déclenche GC quand heap double

    // Forcer un GC
    runtime.GC()

    // Statistiques mémoire
    var stats runtime.MemStats
    runtime.ReadMemStats(&stats)

    println("Alloc:", stats.Alloc)
    println("NumGC:", stats.NumGC)
    println("PauseTotalNs:", stats.PauseTotalNs)
}

// Techniques d'optimisation
// 1. Réutiliser les allocations avec sync.Pool
// 2. Pré-allouer les slices avec make([]T, 0, cap)
// 3. Éviter les conversions string/[]byte répétées
// 4. Utiliser des pointeurs pour grandes structs

La variable d'environnement GODEBUG=gctrace=1 affiche les traces du GC.

24. Expliquez le scheduler Go

Le scheduler Go utilise un modèle M:N mapping N goroutines sur M threads système, avec trois entités : G (goroutine), M (thread), P (processeur logique).

scheduler.gogo
package main

import (
    "fmt"
    "runtime"
)

func main() {
    // Nombre de processeurs logiques (P)
    fmt.Println("GOMAXPROCS:", runtime.GOMAXPROCS(0))

    // Nombre de goroutines actives
    fmt.Println("NumGoroutine:", runtime.NumGoroutine())

    // Céder le processeur à d'autres goroutines
    runtime.Gosched()

    // Modèle M:P:G
    // - G: goroutine (stack légère ~2KB)
    // - M: thread OS (machine)
    // - P: processeur logique (contexte d'exécution)
    //
    // Chaque P a une queue locale de G
    // Work stealing quand queue vide
}

Le scheduler est préemptif depuis Go 1.14, évitant qu'une goroutine monopolise un P.

25. Comment optimiser les performances en Go ?

L'optimisation commence par le profiling pour identifier les goulots d'étranglement.

profiling.gogo
package main

import (
    "os"
    "runtime/pprof"
)

func main() {
    // CPU profiling
    f, _ := os.Create("cpu.prof")
    pprof.StartCPUProfile(f)
    defer pprof.StopCPUProfile()

    // Code à profiler...

    // Memory profiling
    mf, _ := os.Create("mem.prof")
    defer mf.Close()
    pprof.WriteHeapProfile(mf)
}

// Analyse: go tool pprof cpu.prof

// Techniques d'optimisation courantes:
// 1. Éviter les allocations dans les boucles chaudes
// 2. Utiliser sync.Pool pour objets réutilisables
// 3. Préférer []byte à string pour mutations
// 4. Utiliser bufio pour I/O
// 5. Batch les opérations DB
Règle d'optimisation

Mesurer avant d'optimiser. Le profiling révèle souvent des surprises sur les vrais goulots d'étranglement.

Conclusion

Ces 25 questions couvrent les concepts fondamentaux testés en entretien Go :

Checklist de préparation :

  • ✅ Maîtrise des goroutines et channels
  • ✅ Compréhension des interfaces implicites
  • ✅ Gestion d'erreurs idiomatique
  • ✅ Utilisation correcte de context
  • ✅ Patterns de concurrence (mutex, worker pool)
  • ✅ Testing et benchmarking
  • ✅ Connaissance des génériques Go 1.18+

La clé du succès en entretien Go : démontrer une compréhension des compromis entre simplicité et performance, et savoir quand utiliser chaque pattern de concurrence.

Passe à la pratique !

Teste tes connaissances avec nos simulateurs d'entretien et tests techniques.

Tags

#go
#golang
#interview
#concurrence
#goroutines

Partager

Articles similaires