# 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. - Published: 2026-02-06 - Updated: 2026-03-31 - Author: SharpSkill - Tags: go, golang, interview, concurrence, goroutines - Reading time: 18 min --- 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. ```go // declaration.go 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). ```go // types.go 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é. ```go // arrays_slices.go 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). ```go // defer.go 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. ```go // interfaces.go 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. ```go // goroutines.go 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). ```go // channels.go 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é. ```go // select.go 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. ```go // race_conditions.go 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. ```go // worker_pool.go 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. ## 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é. ```go // errors.go 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. ```go // panic_recover.go 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. ```go // receivers.go 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. ```go // embedding.go 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. ```go // singleton.go 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. ```go // context.go 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. ```go // graceful_shutdown.go 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`. ```go // calculator_test.go 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`. ```go // benchmark_test.go 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. ```go // generics.go 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 // go.mod exemple 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. ``` 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. ```go // gc_optimization.go 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). ```go // scheduler.go 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. ```go // profiling.go 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. --- Source: SharpSkill (https://sharpskill.dev), tech interview preparation for your real stack. HTML version of this page: https://sharpskill.dev/fr/blog/go/top-25-questions-entretien-go