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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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é.
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.
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.
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 { ... }`.
16How should panic recovery be handled in Go backend services, including what happens when a goroutine panics and when a process should recover versus crash?
A panic unwinds the current goroutine, running its deferred functions. `recover` only works when called from a deferred function in that same goroutine; one goroutine cannot recover another goroutine's panic. If a panic is not recovered, the process crashes. In backend services, recovery should usually be placed at isolation boundaries such as request handlers, RPC middleware, or worker goroutine entrypoints so one failing request or job does not take down the whole service. But if a panic may have corrupted shared state or made process integrity untrustworthy, it is safer to let the process crash and restart rather than recover and continue blindly.
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.
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.
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.
20What are common causes of goroutine leaks in Go services, and how do you detect and fix them in production?
Common goroutine leaks in Go services come from goroutines blocked forever on channel sends or receives, waiting on other blocking operations without cancellation, stuck I/O without deadlines, background loops or tickers that never stop, and request-scoped goroutines that outlive the request. In production, you look for sustained growth in goroutine count and related symptoms, then inspect goroutine dumps or pprof goroutine profiles to see where goroutines are stuck. Fixing the leak means changing the code so those goroutines can exit: add cancellation and deadlines, stop tickers, close channels correctly, avoid detached request goroutines, and bound concurrency where needed.