Vol. 5 — № 02
The Go Loop · KiosqueNewsstand
Blog  
Un atelier Go · Édition d'ApprentissageA Go Workshop · Learning Edition

append, la réallocation qui surprend append, the realloc that surprises

append écrit parfois en place, parfois dans un tableau tout neuf — selon la capacité restante. Tant qu'on partage le cap, deux slices se marchent dessus ; dès qu'on réalloue, elles divergent. Le bug le plus subtil de Go tient dans cette frontière. append sometimes writes in place, sometimes into a brand-new array — depending on remaining capacity. While the cap is shared, two slices clobber each other; once it reallocates, they diverge. Go's subtlest bug lives on that boundary.

AudienceAudience
Dev qui append à une sous-slice et corrompt l'originale sans comprendre pourquoi Dev who appends to a sub-slice and corrupts the original without knowing why
Format
Self-paced
ChapitresChapters
5
Date
Juil 2027 Jul 2027
≈ 17 min ●●●○ appendRéallocationPiège

Chapitre 1 en accès libre — la suite (ch. 2 à 5) est réservée. Chapter 1 free to read — the rest (ch. 2–5) is members-only.

01CadrageFraming3 min

append n'ajoute pas à ta slice. Il en retourne une nouvelle — que tu dois récupérer.append doesn't add to your slice. It returns a new one — which you must keep.

Le numéro précédent a posé la slice comme une vue de trois mots sur un tableau, et l'a laissée immobile. append est l'opération qui la fait grandir — et c'est là que tout se complique. Le piège commence par sa signature : append ne modifie pas la slice en place, il en renvoie une, qu'il faut réassigner. La forme s = append(s, x) n'est pas un tic de style ; elle est obligatoire, parce que append peut être amené à changer les trois mots de l'en-tête. Pourquoi ? Parce qu'un tableau a une taille fixe. Tant qu'il reste de la place — de la capacité — append écrit dans le tableau existant et se contente d'augmenter la longueur. Mais dès que le tableau est plein, il n'y a pas de « l'agrandir sur place » : append doit allouer un tableau neuf, plus grand, y recopier tout, et te rendre un en-tête qui pointe désormais ailleurs. Deux comportements radicalement différents, derrière un seul appel — et c'est à toi de toujours récupérer le résultat, parce que tu ne sais pas lequel des deux vient d'avoir lieu.The previous issue laid down the slice as a three-word view over an array, and left it still. append is the operation that grows it — and that's where everything gets complicated. The trap starts with its signature: append doesn't modify the slice in place, it returns one, which you must reassign. The form s = append(s, x) isn't a style tic; it's mandatory, because append may have to change all three words of the header. Why? Because an array has a fixed size. While there's room — capacity — append writes into the existing array and merely bumps the length. But the moment the array is full, there's no 'grow it in place': append must allocate a new, larger array, copy everything into it, and hand you a header now pointing elsewhere. Two radically different behaviors behind a single call — and it's on you to always keep the result, because you don't know which of the two just happened.

Ignorer le retour : les ajouts s'évaporentIgnore the return: the additions evaporate
func push(s []int, x int) {
    s = append(s, x)     // met à jour la COPIE LOCALE de l'en-tête
}                        // ce nouvel en-tête meurt à la fin de push

s := make([]int, 0, 4)
push(s, 1)
push(s, 2)
fmt.Println(s, len(s))   // [] 0 — l'appelant n'a rien vu.
Réassigner le retour : la seule forme correcteReassign the return: the only correct form
func push(s []int, x int) []int {
    return append(s, x)  // on RENVOIE le nouvel en-tête
}

s := make([]int, 0, 4)
s = push(s, 1)           // … et l'appelant le RÉASSIGNE
s = push(s, 2)
fmt.Println(s, len(s))   // [1 2] 2 — la règle : le résultat remonte jusqu'à toi.
Deux chemins, selon la capacité restanteTwo paths, by remaining capacity
len < cap
écrit en place
même tableau · len + 1 · ptr inchangé
|
len == cap
réalloue
tableau neuf · copie tout · ptr différent

Le même append(s, x) emprunte l'un ou l'autre chemin selon une seule donnée : reste-t-il de la capacité ? Tu ne le vois pas dans le codecap n'apparaît nulle part — et pourtant c'est lui qui décide si ton tableau est partagé ou détaché. D'où l'unique règle de survie : s = append(s, x), toujours.The same append(s, x) takes one path or the other by a single datum: is there capacity left? You don't see it in the codecap appears nowhere — yet it decides whether your array is shared or detached. Hence the one survival rule: s = append(s, x), always.

Le réflexe du numéroThe issue's reflex

« append rend un en-tête ; je le réassigne, et je me méfie du tableau d'origine. » Le numéro 1 a montré le partage immobile ; celui-ci montre l'opération qui peut le rompre — ou pire, l'exploiter pour corrompre. Le bug le plus subtil de Go vit ici, sur la frontière entre « écrit en place » et « réalloue », une frontière qu'aucune ligne ne rend visible. Tout le numéro consiste à la rendre lisible."append returns a header; I reassign it, and I distrust the original array." Issue 1 showed the still sharing; this one shows the operation that can break it — or worse, exploit it to corrupt. Go's subtlest bug lives here, on the boundary between "writes in place" and "reallocates," a boundary no line makes visible. The whole issue is about making it readable.

Pourquoi pas « agrandir le tableau » ?Why not just 'grow the array'?

Parce qu'un tableau, en mémoire, est un bloc contigu de taille fixe : ses éléments se suivent sans trou. « L'agrandir » voudrait dire que les octets juste après lui sont libres — ce que rien ne garantit ; ils appartiennent peut-être déjà à autre chose. La seule opération sûre est donc d'allouer un bloc neuf, assez grand, et d'y recopier l'existant. C'est pour ça qu'append ne peut pas promettre d'écrire en place : il le fait quand la capacité pré-réservée le permet, et bascule sur une réallocation sinon. Toute la mécanique du numéro découle de cette contrainte physique — la mémoire contiguë ne s'étire pas, elle se recopie ailleurs.Because an array, in memory, is a fixed-size contiguous block: its elements follow with no gap. 'Growing it' would mean the bytes right after it are free — which nothing guarantees; they may already belong to something else. So the only safe operation is to allocate a fresh block, big enough, and copy the existing into it. That's why append can't promise to write in place: it does when pre-reserved capacity allows, and falls back to a reallocation otherwise. The whole issue's machinery follows from that physical constraint — contiguous memory doesn't stretch, it's recopied elsewhere.

🔒

La suite est réservée The rest is members-only

Le premier numéro est libre. Débloque tout The Go Loop — tous les volumes, à vie — pour 5 €, paiement unique. The first issue is free. Unlock all of The Go Loop — every volume, forever — for €5, one-time.

Retour au kiosqueBack to newsstand