Préparation entretien Go

Questions d'entretien pour développeur backend Go

Une sélection de questions Go pour développeurs backend, regroupées par thèmes et issues du même catalogue que la pratique EngineerSpeak.

Commencer un entretien IA GoAucune carte bancaire requise. 1 session gratuite disponible.
Préparation aux entretiens techniques en anglaisUn mode où les non-natifs peuvent s'entraîner à passer des entretiens techniques.

Système de types

1Expliquez comment fonctionnent les valeurs zéro (zero values) de Go pour les types intégrés et les types de type référence, et pourquoi elles sont importantes lorsqu'on déclare des variables sans initialisation explicite.

En Go, une variable déclarée sans initialiseur explicite est automatiquement initialisée à la valeur zéro de son type. Les types numériques deviennent 0, bool devient false, string devient "", et les tableaux ou structs sont mis à zéro élément par élément ou champ par champ. Les types de type pointeur ou référence comme les pointeurs, slices, maps, channels, fonctions et interfaces ont nil comme valeur zéro. Cela compte car les variables Go et les champs de struct omis démarrent dans un état déterministe plutôt que de contenir des données indéterminées, et de nombreuses API sont conçues pour que la valeur zéro soit un défaut utile, même si certaines valeurs nil nécessitent encore une initialisation avant certaines opérations.

package main

import "fmt"

type Config struct {
	Port    int
	Debug   bool
	Name    string
	Tags    []string
	Options map[string]string
}

func main() {
	var c Config
	fmt.Printf("port=%d debug=%v name=%q tags==nil:%v options==nil:%v\n",
		c.Port, c.Debug, c.Name, c.Tags == nil, c.Options == nil)
}
Essayer de répondre à cette question avec un coach IA

2Comment Go gère-t-il l'égalité des structs et que se passe-t-il lorsqu'une struct contient des champs non comparables ?

Les valeurs de struct en Go peuvent être comparées avec `==` et `!=` uniquement lorsque chaque champ de la struct est comparable. L'égalité compare les champs correspondants selon la règle d'égalité propre à chaque champ. Si une struct contient un champ non comparable tel qu'une slice, une map ou une fonction, le type struct n'est pas comparable, et comparer deux valeurs de ce type struct avec `==` est une erreur à la compilation. Pour de telles structs, utilisez une logique de comparaison personnalisée ou un helper d'égalité en profondeur approprié, surtout dans les tests.

package main

import "fmt"

type UserID struct {
    Name string
    Age  int
}

type UserProfile struct {
    Name string
    Tags []string
}

func main() {
    a := UserID{"Ann", 30}
    b := UserID{"Ann", 30}
    fmt.Println(a == b)

    x := UserProfile{Name: "Ann", Tags: []string{"go"}}
    y := UserProfile{Name: "Ann", Tags: []string{"go"}}
    _, _ = x, y
    // fmt.Println(x == y) // compile error: struct containing []string cannot be compared
}
Essayer de répondre à cette question avec un coach IA

3Expliquez l'immuabilité des strings en Go et la relation entre string, []byte, bytes.Buffer et strings.Builder.

Une `string` Go est une séquence immuable d'octets, souvent du texte UTF-8 mais pas nécessairement de l'UTF-8 valide. On ne peut pas modifier une string en place ; pour changer le contenu, on convertit généralement en `[]byte` pour des éditions au niveau octet ou en `[]rune` pour des éditions au niveau point de code, puis on reconvertit. Les conversions normales entre `string` et `[]byte` copient les données et peuvent allouer, donc des conversions répétées ou des concatenations répétées dans des boucles peuvent être coûteuses. `strings.Builder` est optimisé pour construire efficacement des strings, tandis que `bytes.Buffer` est un buffer d'octets mutable utile pour les données orientées octets et les E/S, et peut aussi produire une string.

package main

import (
    "bytes"
    "fmt"
    "strings"
)

func main() {
    var sb strings.Builder
    sb.WriteString("hello")
    sb.WriteByte(' ')
    sb.WriteString("world")
    fmt.Println(sb.String())

    var buf bytes.Buffer
    buf.Write([]byte{0x48, 0x69})
    buf.WriteByte('!')
    fmt.Println(buf.String())

    s := "cat"
    b := []byte(s) // normally copies
    b[0] = 'b'
    fmt.Println(s, string(b))
}
Essayer de répondre à cette question avec un coach IA

4Expliquez en quoi nil fonctionne différemment pour les pointeurs, slices, maps, channels, fonctions et interfaces en Go.

En Go, nil est la valeur zéro des pointeurs, slices, maps, channels, fonctions et interfaces, mais les opérations sur ces valeurs nil diffèrent selon le type. Un pointeur nil peut être comparé à nil, mais le déréférencer provoque un panic. Une slice nil a une longueur et une capacité de 0 et peut être parcourue avec range et étendue avec append. Une map nil peut être lue et parcourue avec range, mais une affectation y provoque un panic. Envoyer ou recevoir sur un channel nil bloque indéfiniment, et fermer un channel nil provoque un panic. Appeler une fonction nil provoque un panic. Une interface n'est nil que lorsqu'elle n'a ni type dynamique ni valeur dynamique ; une interface contenant une valeur nil typée, comme un pointeur nil, n'est pas elle-même nil.

package main

import "fmt"

type MyErr struct{}

func (*MyErr) Error() string { return "my error" }

func returnsTypedNil() error {
	var e *MyErr = nil
	return e
}

func main() {
	var p *int = nil
	var s []int = nil
	var m map[string]int = nil
	var err error = returnsTypedNil()

	fmt.Println(p == nil)
	fmt.Println(len(s), cap(s), s == nil)
	fmt.Println(m["missing"])
	fmt.Println(err == nil)
}
Essayer de répondre à cette question avec un coach IA

5Quels sont les types comparables en Go, et comment les règles de comparabilité affectent-elles les clés de map, l'égalité et les contraintes des generics ?

Les types comparables en Go sont les types dont les valeurs peuvent être comparées avec `==` et `!=`. Les types de base, les pointeurs, les channels, les interfaces, ainsi que les structs/arrays dont les champs ou éléments sont comparables, sont comparables ; les slices, maps et fonctions ne sont pas comparables sauf avec `nil`. Les clés de map doivent être comparables. L'égalité suit les règles de comparaison du type, et la comparaison d'interfaces dépend des valeurs concrètes dynamiques ; si une interface comparée contient une valeur dynamique non comparable, la comparaison provoque un panic. En generics, la contrainte prédéclarée `comparable` permet aux paramètres de type d'être comparés avec `==`/`!=` et d'être utilisés comme clés de map.

package main

import "fmt"

type Point struct{ X, Y int }      // comparable
type Bag struct{ Items []string }  // not comparable because of slice field

func Contains[T comparable](xs []T, target T) bool {
    for _, x := range xs {
        if x == target {
            return true
        }
    }
    return false
}

func main() {
    p1, p2 := Point{1, 2}, Point{1, 2}
    fmt.Println(p1 == p2)

    m := map[Point]string{p1: "value"}
    fmt.Println(m[p2])

    fmt.Println(Contains([]string{"a", "b"}, "b"))

    var a any = []int{1}
    var b any = []int{1}
    _, _ = a, b
    // fmt.Println(a == b) // panic: comparing uncomparable type []int
}
Essayer de répondre à cette question avec un coach IA

6Comment Go représente-t-il les bytes, les runes et le texte encodé en UTF-8, et pourquoi len(s) peut-il différer du nombre de caractères visibles par l'utilisateur ?

En Go, `byte` est un alias de `uint8` et représente un octet brut, tandis que `rune` est un alias de `int32` et représente un point de code Unicode. Une `string` est une séquence d'octets en lecture seule, couramment du texte encodé en UTF-8 mais pouvant contenir des octets arbitraires. `len(s)` renvoie le nombre d'octets, pas de runes ni de caractères visibles par l'utilisateur. L'indexation d'une string renvoie un octet ; parcourir une string avec range décode l'UTF-8 et produit des index d'octets plus des runes. `len(s)` peut différer du nombre de caractères visibles car l'UTF-8 peut utiliser plusieurs octets par point de code et car un caractère visible par l'utilisateur peut être composé de plusieurs points de code, comme des marques combinantes ou des séquences d'emoji.

package main

import "fmt"

func main() {
    s := "é🙂"

    fmt.Println(len(s))         // bytes: é is 2 bytes, 🙂 is 4 bytes
    fmt.Println(len([]rune(s))) // Unicode code points

    fmt.Printf("first byte: %x\n", s[0])

    for i, r := range s {
        fmt.Printf("byte index %d: %q U+%04X\n", i, r, r)
    }
}
Essayer de répondre à cette question avec un coach IA

Structures de données

7Décrivez la différence entre les tableaux (arrays) et les slices en Go, y compris le comportement de la longueur, de la capacité et du stockage sous-jacent.

Un tableau en Go a une longueur fixe qui fait partie de son type, comme [3]int ; il stocke ses éléments directement, et assigner ou passer un tableau copie toute la valeur du tableau. Une slice, comme []int, est un petit descripteur sur un tableau sous-jacent : conceptuellement elle contient un pointeur vers les éléments, une longueur et une capacité. La longueur d'une slice est le nombre d'éléments visibles ; sa capacité est le nombre d'éléments utilisables depuis le début de la slice jusqu'à la fin du tableau de support. Les slices sont flexibles : le reslicing modifie le descripteur, et append peut réutiliser le même tableau sous-jacent si la capacité le permet ou en allouer un nouveau sinon.

package main

import "fmt"

func main() {
	a := [3]int{1, 2, 3}
	b := a
	b[0] = 99
	fmt.Println(a, b)

	s := []int{1, 2, 3}
	t := s
	t[0] = 99
	fmt.Println(s, t)
	fmt.Println(len(s), cap(s))
}
Essayer de répondre à cette question avec un coach IA

8Comment se comporte le type map de Go concernant les types de clés, les clés absentes, les maps nil et l'ordre d'itération ?

Les types de clés d'une map Go doivent être comparables ; les slices, maps et fonctions ne peuvent pas être utilisées directement comme clés. La recherche d'une clé absente renvoie la valeur zéro du type des éléments, c'est pourquoi la forme comma-ok (`v, ok := m[k]`) sert à distinguer l'absence d'une valeur zéro présente. Une map nil peut être lue et parcourue avec range, mais une affectation provoque un panic ; il faut l'initialiser avant d'écrire. L'ordre d'itération d'une map n'est pas spécifié et le code ne doit pas en dépendre.

package main

import "fmt"

func main() {
    counts := map[string]int{"a": 0, "b": 2}

    fmt.Println(counts["missing"]) // zero value for int

    v, ok := counts["a"]
    fmt.Println(v, ok) // present even though value is zero

    var m map[string]int
    fmt.Println(m["x"]) // read from nil map is OK
    // m["x"] = 1     // panic: assignment to entry in nil map

    m = make(map[string]int)
    m["x"] = 1

    for k, v := range counts {
        fmt.Println(k, v) // order is not guaranteed
    }
}
Essayer de répondre à cette question avec un coach IA

9Décrivez comment le reslicing et l'affectation de slices peuvent faire partager le même tableau sous-jacent à plusieurs slices, et quels bugs cela peut créer.

Une valeur de slice est un en-tête pointant dans un tableau sous-jacent. Assigner une slice ou la passer à une fonction ne copie que cet en-tête, pas les éléments. Le reslicing crée un autre en-tête pointant vers une plage du même tableau de support. Par conséquent plusieurs slices peuvent aliaser le même stockage : modifier un élément via une slice peut être visible via une autre, et faire un append sur une slice peut écraser des données visibles par une autre si elle a encore de la capacité disponible. Les bugs incluent des mutations surprenantes, des résultats corrompus, la rétention de grands tableaux de support via de petites sous-slices, et des data races lorsque des alias sont utilisés concurrentiellement. Pour éviter le partage non intentionnel, faites une copie défensive avec copy ou append([]T(nil), s...), ou limitez la capacité avec une expression de slice complète avant append.

package main

import "fmt"

func main() {
	base := []int{1, 2, 3, 4}
	a := base[:2] // len 2, cap 4
	b := base[2:] // len 2, cap 2

	a[0] = 99
	fmt.Println(base, a, b)

	a = append(a, 77) // reuses base's backing array, overwrites base[2]
	fmt.Println(base, a, b)

	c := append([]int(nil), base[:2]...) // defensive copy
	c[0] = 42
	fmt.Println(base, c)
}
Essayer de répondre à cette question avec un coach IA

10Expliquez la croissance des slices lors d'append à un niveau conceptuel et les implications de performance des réallocations répétées.

Lorsque append ajoute des éléments à une slice, il écrit dans le tableau de support existant si la slice a assez de capacité. Si la capacité est insuffisante, Go alloue un plus grand tableau de support, copie les éléments existants, écrit les nouveaux éléments, et renvoie un en-tête de slice pointant vers le nouveau stockage. La politique exacte de croissance dépend de l'implémentation, mais conceptuellement la capacité croît suffisamment pour rendre les append répétés efficaces en amorti. Les réallocations répétées coûtent tout de même du CPU pour la copie, créent des allocations, augmentent la pression sur le GC, et peuvent rompre le partage avec d'anciens alias de slice. Si vous connaissez la taille attendue, préallouez avec make([]T, 0, n) lors d'une construction par append ou make([]T, n) lors d'un remplissage par index pour réduire les réallocations.

package main

func collectNoPrealloc(input []int) []int {
	var out []int
	for _, v := range input {
		out = append(out, v*2)
	}
	return out
}

func collectPrealloc(input []int) []int {
	out := make([]int, 0, len(input))
	for _, v := range input {
		out = append(out, v*2)
	}
	return out
}
Essayer de répondre à cette question avec un coach IA

Sémantique du langage

11Comment fonctionne l'identifiant blanc en Go pour les valeurs non utilisées, les imports et les vérifications d'interface à la compilation ?

L'identifiant blanc `_` est un placeholder en écriture seule. Lui assigner une valeur la jette et ne crée pas de variable utilisable. On l'utilise pour ignorer des valeurs de retour ou des variables de boucle non nécessaires, pour importer un package uniquement pour ses effets de bord avec `import _ "pkg"`, et pour faire des vérifications d'implémentation d'interface à la compilation telles que `var _ io.Reader = (*MyReader)(nil)`. Un blank import exécute tout de même l'initialisation du package importé. Une assignation de vérification d'interface échoue à la compilation si le method set du type concret ne satisfait pas l'interface.

package main

import "fmt"

func lookup() (string, bool) {
    return "gopher", true
}

func main() {
    name, _ := lookup() // ignore the bool
    fmt.Println(name)

    for i, _ := range []int{10, 20} {
        fmt.Println(i)
    }
}
Essayer de répondre à cette question avec un coach IA

Paquets

12Comment fonctionne l'ordre d'initialisation des packages en Go, y compris les fonctions init et les dépendances importées ?

Go initialise les packages dans l'ordre des dépendances. Les dépendances importées d'un package sont initialisées avant le package importateur. Au sein d'un package, les variables de niveau package sont initialisées avant toute fonction `init`, l'initialisation des variables étant ordonnée par dépendance et ordre de déclaration tels que définis par le langage. Ensuite, les fonctions `init` du package s'exécutent automatiquement ; un package peut avoir plusieurs fonctions `init`, et elles ne peuvent pas être appelées directement. Chaque package n'est initialisé qu'une fois. Pour un exécutable, le graphe d'imports est d'abord initialisé, puis le package `main` est initialisé, et enfin `main.main` est appelé.

// a/a.go
package a

import "fmt"

var V = func() int {
    fmt.Println("a var")
    return 1
}()

func init() { fmt.Println("a init") }

// main.go
package main

import (
    "fmt"
    "example/a"
)

var M = func() int {
    fmt.Println("main var", a.V)
    return 2
}()

func init() { fmt.Println("main init") }

func main() { fmt.Println("main") }
Essayer de répondre à cette question avec un coach IA

13Expliquez les règles de visibilité des packages en Go, y compris les identifiants exportés et la convention du répertoire internal/.

En Go, la visibilité des packages est contrôlée par le nommage des identifiants, et non par des mots-clés d'accès. Un identifiant dont le nom commence par une lettre Unicode majuscule est exporté et peut être référencé depuis d'autres packages ; les autres identifiants sont non exportés et utilisables uniquement au sein du même package. Cela s'applique aux fonctions, types, méthodes, variables, constantes et champs de structures. Les packages utilisent les identifiants exportés pour définir leur API publique et garder les détails d'implémentation non exportés. Séparément, un package situé sous un répertoire `internal/` ne peut être importé que par du code dont le chemin d'import se trouve dans l'arbre parent de ce répertoire `internal` ; cela est appliqué par la toolchain Go.

// module example.com/app

// internal/store/store.go
package store

type Client struct { // exported type
    dsn string // unexported field
}

func New(dsn string) *Client { return &Client{dsn: dsn} } // exported
func parseDSN(s string) string { return s }                // unexported

// cmd/api/main.go -- allowed: inside example.com/app tree
package main

import "example.com/app/internal/store"

func main() {
    c := store.New("db")
    _ = c
    // c.dsn is not accessible here: field is unexported.
}

// Code outside example.com/app cannot import example.com/app/internal/store.
Essayer de répondre à cette question avec un coach IA

Flux de contrôle

14Comment fonctionne defer en Go, y compris l'ordre d'exécution, le moment d'évaluation des arguments et l'interaction avec les valeurs de retour ?

`defer` planifie l'exécution d'un appel de fonction lorsque la fonction englobante se termine, qu'elle sorte par un return normal ou par le déroulement d'un panic. Plusieurs appels différés s'exécutent dans l'ordre dernier entré, premier sorti (LIFO). La valeur de fonction différée et ses arguments sont évalués immédiatement lorsque l'instruction `defer` s'exécute, mais l'appel lui-même a lieu plus tard. Avec des valeurs de retour nommées, une instruction return assigne d'abord les valeurs de retour, puis les fonctions différées s'exécutent, de sorte qu'une closure différée peut observer ou modifier les variables de résultat nommées avant que l'appelant ne les reçoive. Cela rend `defer` utile pour le nettoyage, comme fermer des fichiers, déverrouiller des mutex et libérer des ressources.

package main

import "fmt"

func f() (result int) {
    x := 1
    defer fmt.Println("arg evaluated now:", x)
    defer func() {
        result++
        fmt.Println("deferred closure sees x later:", x)
    }()
    x = 2
    return 10
}

func main() {
    fmt.Println("return:", f())
}
Essayer de répondre à cette question avec un coach IA

Gestion des erreurs

15Expliquez le modèle de gestion des erreurs en Go et les façons conventionnelles de créer, retourner et vérifier les erreurs.

Go traite les erreurs comme des valeurs ordinaires, pas comme des exceptions. L'interface intégrée `error` est satisfaite par tout type disposant d'une méthode `Error() string`. Les fonctions retournent conventionnellement une `error` comme dernier résultat, où `nil` signifie succès et une erreur non nulle signifie que l'appelant doit gérer ou propager l'échec. Les erreurs simples sont couramment créées avec `errors.New`, les erreurs formatées avec `fmt.Errorf`, et les appelants vérifient habituellement `if err != nil { ... }`.

package main

import (
	"errors"
	"fmt"
)

func findUser(id int) (string, error) {
	if id <= 0 {
		return "", errors.New("invalid user id")
	}
	if id == 42 {
		return "", fmt.Errorf("user %d not found", id)
	}
	return "alice", nil
}

func handler(id int) error {
	name, err := findUser(id)
	if err != nil {
		return fmt.Errorf("find user: %w", err)
	}
	fmt.Println(name)
	return nil
}
Essayer de répondre à cette question avec un coach IA

16Comment la récupération après panique (panic recovery) doit-elle être gérée dans les services backend en Go, notamment ce qui se produit lorsqu'une goroutine panique et dans quels cas un processus doit récupérer ou au contraire s'arrêter brusquement ?

Un `panic` déroule la pile de la goroutine courante en exécutant ses fonctions différées (`defer`). `recover` ne fonctionne que lorsqu'il est appelé depuis une fonction différée dans cette même goroutine ; une goroutine ne peut pas intercepter le `panic` d'une autre goroutine. Si un `panic` n'est pas intercepté, le processus plante. Dans les services backend, la récupération doit généralement être placée aux frontières d'isolation, telles que les gestionnaires de requêtes, les middlewares RPC (Remote Procedure Call) ou les points d'entrée des goroutines de traitement (workers), afin qu'une requête ou une tâche en échec ne fasse pas planter tout le service. En revanche, si un `panic` risque d'avoir corrompu l'état partagé ou compromis l'intégrité du processus, il est plus sûr de laisser le processus planter et redémarrer plutôt que de récupérer l'erreur et de continuer aveuglément.

func Recover(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        defer func() {
            if rec := recover(); rec != nil {
                log.Printf("panic: %v\n%s", rec, debug.Stack())
                http.Error(w, "internal server error", http.StatusInternalServerError)
            }
        }()
        next.ServeHTTP(w, r)
    })
}
Essayer de répondre à cette question avec un coach IA

Concurrence

17Que sont les canaux nil en Go, et comment peuvent-ils casser accidentellement du code ou désactiver intentionnellement des cas de select ?

Un canal nil est une variable de type channel dont la valeur est nil, souvent parce qu’elle n’a pas été initialisée avec make ou qu’elle a été explicitement mise à nil. Envoyer vers ou recevoir depuis un canal nil bloque indéfiniment. Dans un select, un cas impliquant un canal nil n’est jamais prêt, donc assigner nil à une variable de canal peut désactiver intentionnellement ce cas. Utiliser accidentellement un canal nil peut faire pendre des goroutines ou empêcher la logique de select de traiter les événements attendus.

var in <-chan int = source
for in != nil {
    select {
    case v, ok := <-in:
        if !ok {
            in = nil // disables this receive case
            continue
        }
        fmt.Println(v)
    case <-ctx.Done():
        return
    }
}
Essayer de répondre à cette question avec un coach IA

18En quoi les opérations atomiques de sync/atomic diffèrent-elles de la synchronisation basée sur des mutex, et quand sont-elles appropriées ?

sync/atomic fournit des opérations indivisibles sur des emplacements mémoire individuels, telles que load, store, add, swap et compare-and-swap, avec des garanties de synchronisation/ordonnancement mémoire. Un mutex protège une section critique, donc il peut garder du code arbitraire et des invariants impliquant plusieurs lectures, écritures ou champs. Les atomics conviennent à un état simple et indépendant tel que des compteurs, des drapeaux, des numéros de séquence, ou des structures lock-free soigneusement conçues. Préférez un mutex lorsque les opérations sont composées, que plusieurs valeurs doivent rester cohérentes, ou que la version atomique serait difficile à raisonner ou à prouver correcte.

package main

import (
    "sync"
    "sync/atomic"
)

var requests atomic.Int64

func recordRequest() {
    requests.Add(1) // one independent counter update
}

type Account struct {
    mu      sync.Mutex
    balance int
    limit   int
}

func (a *Account) Withdraw(n int) bool {
    a.mu.Lock()
    defer a.mu.Unlock()

    // The check and update must be one protected invariant.
    if a.balance-n < -a.limit {
        return false
    }
    a.balance -= n
    return true
}
Essayer de répondre à cette question avec un coach IA

19Comment faut-il concevoir la propriété des canaux et le cycle de vie des goroutines pour éviter les fuites de goroutines ?

Concevez les goroutines avec un propriétaire explicite, un signal d’arrêt clair et un chemin de sortie garanti. Le côté producteur possède en général la fermeture d’un canal, surtout d’un canal de sortie ; les récepteurs ne doivent pas fermer un canal tant que des expéditeurs peuvent encore être actifs. Tout envoi, réception, boucle, timer ou appel externe bloquant doit soit être garanti de se terminer, soit pouvoir se débloquer sur annulation, généralement via context.Context ou un canal done. Utilisez WaitGroup, errgroup ou une coordination similaire afin d’attendre les workers et de ne fermer les canaux qu’après la sortie des expéditeurs.

package worker

import (
    "context"
    "sync"
)

type Job int
type Result int

func StartWorkers(ctx context.Context, jobs <-chan Job, n int) <-chan Result {
    results := make(chan Result)

    var wg sync.WaitGroup
    wg.Add(n)
    for i := 0; i < n; i++ {
        go func() {
            defer wg.Done()
            for {
                select {
                case <-ctx.Done():
                    return
                case j, ok := <-jobs:
                    if !ok {
                        return
                    }
                    r := Result(j * 2)
                    select {
                    case results <- r:
                    case <-ctx.Done():
                        return
                    }
                }
            }
        }()
    }

    go func() {
        wg.Wait()
        close(results) // close after all senders are done
    }()

    return results
}
Essayer de répondre à cette question avec un coach IA

20Quelles sont les causes courantes de fuites de goroutines dans les services Go, et comment les détecter et les corriger en production ?

Les fuites de goroutines courantes dans les services Go proviennent de goroutines bloquées indéfiniment sur des émissions ou des réceptions de canaux, en attente d'autres opérations bloquantes sans mécanisme d'annulation, d'E/S (I/O) bloquées sans délai d'expiration (deadline), de boucles en arrière-plan ou de tickers qui ne s'arrêtent jamais, et de goroutines liées à une requête qui survivent à celle-ci. En production, on surveille une croissance continue du nombre de goroutines ainsi que les symptômes associés, puis on inspecte les dumps de goroutines ou les profils de goroutines `pprof` pour identifier où elles sont bloquées. Corriger la fuite consiste à modifier le code pour permettre à ces goroutines de se terminer : ajouter des annulations et des délais d'expiration, arrêter les tickers, fermer correctement les canaux, éviter les goroutines détachées de la requête et limiter la concurrence si nécessaire.

func handler(w http.ResponseWriter, r *http.Request) {
    go func() {
        result := <-slowCh // may block forever
        _ = result
    }()
}
Essayer de répondre à cette question avec un coach IA