ProgrammationDéveloppeur Go intermédiaire

Comment sont organisés les types de tranches (slices) et de tableaux (arrays) en Go ? Pourquoi est-il important de distinguer leur sémantique lors de leur passage dans des fonctions et de leur gestion de la mémoire ?

Réussissez les entretiens avec l'assistant IA Hintsage

Réponse.

Les tranches et les tableaux sont l'une des structures de données les plus utilisées en Go. Malgré une syntaxe similaire, la différence dans leur structure et leur comportement peut entraîner des erreurs de performance, de mémoire et de sémantique.

Historique de la question :

Go a depuis le début choisi un modèle de gestion de la mémoire explicite, dans lequel les tableaux (arrays) sont une séquence d'éléments de taille fixe, tandis que les tranches (slices) sont une vue dynamique sur un tableau. Cette séparation permet de contrôler le coût des opérations et le comportement du code.

Problème :

La principale difficulté réside dans la confusion entre la copie d'un tableau (sémantique de valeur) et la "référence" d'une tranche. Les erreurs surviennent souvent lors du passage de ces types dans des fonctions et de la modification de leurs valeurs, entraînant des effets secondaires inattendus.

Solution :

Les tableaux sont toujours copiés lors du passage par valeur : la fonction reçoit une copie de tout le contenu. En revanche, une tranche est une petite structure (en-tête) qui contient un pointeur vers un tableau, une longueur et une capacité. Les modifications à l'intérieur de la tranche sont visibles de l'extérieur si le contenu du tableau est modifié (mais pas si la tranche elle-même est redirigée vers un nouveau tableau à l'intérieur de la fonction).

Exemple de code :

func updateArray(arr [3]int) { arr[0] = 10 } func updateSlice(slc []int) { slc[0] = 10 } func main() { a := [3]int{1,2,3} b := []int{1,2,3} updateArray(a) updateSlice(b) fmt.Println(a) // [1 2 3] fmt.Println(b) // [10 2 3] }

Caractéristiques clés :

  • Le tableau est un type de valeur, entièrement copié lors du passage (la taille est compilée dans le type).
  • La tranche est une structure d'enveloppement : pointeur vers un tableau, longueur et capacité.
  • Efficacité du passage des tranches : l'opération consiste à copier uniquement l'en-tête, pas tout le contenu (mais les modifications à l'intérieur sont visibles de tous les "points de vue").

Questions piégeuses.

Que se passe-t-il si la longueur de la tranche est modifiée à l'intérieur d'une fonction ? Cela affectera-t-il la tranche d'origine ?

Non, la modification de la longueur de la tranche (par exemple, avec slc = slc[:2]) à l'intérieur de la fonction n'affectera que la copie locale de l'en-tête. La tranche d'origine restera inchangée.

L'opérateur append renvoie-t-il la tranche modifiée dans la même zone mémoire ?

Pas nécessairement. Si la capacité n'est pas suffisante, un nouveau tableau est créé et un pointeur vers le nouveau tableau est renvoyé. L'ancien tableau restera intact.

Exemple de code :

s := []int{1,2,3} s2 := append(s, 4, 5, 6) // s2 peut se trouver dans une nouvelle zone mémoire

Peut-on assigner un tableau à une tranche ou vice versa ?

Non. []int et [5]int sont des types différents. Pour passer un tableau en tant que tranche, il faut utiliser la conversion arr[:]. L'inverse n'est pas possible.

Erreurs types et anti-modèles

  • Copier un tableau et s'attendre à ce que les modifications soient visibles à l'extérieur de la fonction.
  • Modifier la longueur d'une tranche à l'intérieur de la fonction et s'attendre à ce que cela se reflète en dehors de la fonction.
  • Fuites de mémoire à travers des "longs" tableaux de support d'une tranche, stockée pour de petites vues.
  • Erreurs lors de l'utilisation de append dans une boucle — création possible de nouveaux tableaux, tandis que les anciennes tranches restent "pendantes".

Exemple de la vie réelle

Cas négatif

Un développeur junior a mis en œuvre une fonction de mise à jour de tableau, en passant un tableau à une fonction en s'attendant à ce que les modifications soient appliquées au tableau d'origine. Les modifications n'ont pas été "sauvegardées".

Avantages :

  • Le code était facile à lire et à tester dans de petits exemples.

Inconvénients :

  • Bugs sur des données réelles, difficultés de diagnostic — changement caché.

Cas positif

La fonction prenait une tranche et retournait explicitement une copie modifiée, augmentant la prévisibilité de l'effet. Tous les changements étaient conscients, les données n'étaient pas "fuyantes" et ne changeaient pas de manière implicite.

Avantages :

  • Simplicité et prévisibilité du comportement.
  • Pas de "magie" avec la copie ou la modification.

Inconvénients :

  • Il faut se rappeler où et quand les pointeurs et les tranches sont passés pour ne pas conserver une mémoire inutile (backing array).