Retour au cours

backend / go

Slices en profondeur

Leçon 71 exercice

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) et cap() (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 append qui é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érationEffet sur la mémoireRisque
s[a:b]Partage le tableau sous-jacentModifier sous peut modifier nombres
append sous la capacitéÉcrit directement dans le tableau existantPeut écraser des données d'un autre slice qui partage ce tableau
append au-delà de la capacitéAlloue un nouveau tableau, copieLe slice résultant devient indépendant de l'original
copy(dst, src)Copie réelle et immédiateAucun 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.

go
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

1 disponible
1

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.

Résoudre l’exercice →