backend / go
Slices en profondeur
Explication
Ce que vous allez apprendre
- Distinguer un tableau (taille fixe) d'un slice (dynamique), et savoir pourquoi le slice domine
- Comprendre la différence entre
len()(longueur actuelle) etcap()(capacité avant réallocation) - Savoir que le slicing (
s[a:b]) partage la mémoire avec le slice d'origine - Éviter le piège classique d'un
appendqui écrase silencieusement des données partagées - Copier réellement un slice avec
copy()quand l'indépendance des données est nécessaire
Dans quel contexte ?
Un développeur débogue un bug étrange en production : une fonction censée ne lire qu'une petite portion d'un gros tableau de commandes semble parfois modifier des données ailleurs dans ce tableau, sans qu'aucune ligne suspecte n'apparaisse dans la fonction elle-même. La cause, une fois trouvée, est presque toujours la même : un sous-slice qui partage la mémoire de son slice parent, et un append qui écrit par-dessus des données qu'on croyait indépendantes.
D'abord, un tableau Go a une taille fixée à la compilation
var tableau [5]int a une taille définitivement figée à 5 éléments : ce n'est pas la structure la plus utilisée au quotidien, car elle manque de flexibilité. La grande majorité du code Go utilise plutôt des slices, une vue dynamique construite au-dessus d'un tableau interne invisible.
Un slice se compose en réalité de trois informations discrètes : un pointeur vers le premier élément visible, une longueur (len), et une capacité (cap) qui indique jusqu'où le tableau sous-jacent peut encore grandir sans réallocation.
Une fois cette structure interne comprise, le comportement d'append devient logique
Tant que la capacité n'est pas dépassée, append ajoute l'élément directement dans le tableau sous-jacent existant. Dès que la capacité est dépassée, Go alloue un nouveau tableau plus grand (généralement le double), copie les données, et le slice résultant pointe alors vers cette nouvelle zone mémoire.
| Opération | Effet sur la mémoire | Risque |
|---|---|---|
s[a:b] | Partage le tableau sous-jacent | Modifier sous peut modifier nombres |
append sous la capacité | Écrit directement dans le tableau existant | Peut écraser des données d'un autre slice qui partage ce tableau |
append au-delà de la capacité | Alloue un nouveau tableau, copie | Le slice résultant devient indépendant de l'original |
copy(dst, src) | Copie réelle et immédiate | Aucun partage, totalement sûr |
Prérequis
Il faut connaître les bases des fonctions et des boucles for range (leçons précédentes) pour suivre les exemples de parcours de slices.
Il reste un piège très concret à connaître avant d'écrire du code de production
Prendre un sous-slice (a := original[0:2]) puis lui faire un append peut, si la capacité restante du tableau d'origine le permet, écrire directement dans les cases suivantes du tableau original — un effet de bord totalement invisible en lisant seulement le code de a.
Piège dangereux
Ce bug ne se produit que dans certaines conditions de capacité, ce qui le rend particulièrement traître : le code fonctionne parfaitement en test avec un petit jeu de données, puis casse en production avec un volume différent. La parade s'appelle le "full slice expression" : original[0:2:2] force la capacité du sous-slice à sa longueur exacte, garantissant qu'un append déclenchera toujours une nouvelle allocation.
Enfin, comment obtenir une copie réellement indépendante
copy(destination, source) copie physiquement les éléments d'un slice vers un autre, sans lien de mémoire partagée ensuite. C'est le réflexe à avoir dès qu'on veut modifier une donnée sans affecter l'original.
Bonne pratique
Utilise systématiquement make([]T, len(x)) suivi de copy() (ou slices.Clone depuis Go 1.21) quand une fonction reçoit un slice en paramètre et doit le modifier sans effet de bord pour l'appelant.
Maintenant que le comportement mémoire des slices n'a plus de secret, la prochaine leçon explore l'autre structure de données incontournable de Go, avec ses propres subtilités : les maps.
Commandes & code
Slices en profondeur
Un slice est une vue (pointeur + longueur + capacité) sur un tableau sous-jacent.
package main
import "fmt"
func main() {
// Tableau : taille fixe, rarement utilise directement
var tableau [5]int = [5]int{1, 2, 3, 4, 5}
// Slice : structure dynamique la plus utilisee en Go
nombres := []int{1, 2, 3, 4, 5}
fmt.Println(nombres, len(nombres), cap(nombres))
// make(type, longueur, capacite)
s := make([]int, 3, 10) // longueur 3, capacite 10
fmt.Println(s, len(s), cap(s))
// append : peut re-allouer si la capacite est depassee
s = append(s, 1, 2, 3)
fmt.Println(s, len(s), cap(s))
// Slicing : s[debut:fin] (fin exclu), partage la memoire sous-jacente !
sous := nombres[1:3]
fmt.Println(sous) // [2 3]
sous[0] = 99
fmt.Println(nombres) // [1 99 3 4 5] : la modif impacte le slice original
// Copier reellement (eviter le partage involontaire de memoire)
copie := make([]int, len(nombres))
copy(copie, nombres)
copie[0] = -1
fmt.Println(nombres, copie)
// Piege classique : append sur un sous-slice peut ecraser des donnees
original := []int{1, 2, 3, 4, 5}
a := original[0:2] // [1 2], capacite jusqu'a 5
a = append(a, 100) // ecrit dans original[2], PAS d'allocation nouvelle
fmt.Println(original) // [1 2 100 4 5] : original modifie !
// Solution : forcer une nouvelle allocation avec le "full slice expr"
b := original[0:2:2] // capacite limitee a 2
b = append(b, 999) // force une reallocation, n'affecte pas original
fmt.Println(original)
// Suppression d'un element (pas de methode native, pattern idiomatique)
supprimerIndex := func(s []int, i int) []int {
return append(s[:i], s[i+1:]...)
}
nombres2 := []int{10, 20, 30, 40}
nombres2 = supprimerIndex(nombres2, 1)
fmt.Println(nombres2) // [10 30 40]
// Slice de slices (matrice)
matrice := [][]int{
{1, 2, 3},
{4, 5, 6},
}
fmt.Println(matrice[1][2]) // 6
_ = tableau
}Résumé
len= longueur actuelle,cap= capacité avant réallocation.s[a:b]partage la mémoire du sous-jacent : attention aux effets de bord.s[a:b:c](full slice expression) limite la capacité pour éviter les collisions d'append.copy()fait une vraie copie indépendante ;append(s[:i], s[i+1:]...)supprime un élément.
Exercices pratiques
Mission : un lot de commandes VIP qui corrompt le reste du catalogue
Objectif : Diagnostiquer une écriture accidentelle par aliasing de slice, puis sécuriser une extraction de sous-lot sans effet de bord.
Contexte
Un batch nocturne extrait les 2 premières commandes d'une journée (commandes[0:2]) pour leur ajouter une étiquette VIP via append, sans toucher au reste. Depuis quelques jours, des commandes plus loin dans la liste commandes changent de valeur alors qu'aucun code ne les touche explicitement.