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

go f(), la fonction que personne n'attend go f(), the function nobody waits for

Deux lettres — go — et une fonction part vivre sa vie en parallèle. Le code naïf croit avoir lancé un travail ; il a surtout perdu le fil. Si main finit avant elle, la goroutine meurt sans un mot. Lancer est trivial ; tout le modèle de Go tient dans la question d'après : qui l'attend ? Two letters — go — and a function leaves to live in parallel. Naive code thinks it started a job; mostly it lost the thread. If main finishes first, the goroutine dies without a word. Launching is trivial; all of Go's model lives in the next question: who waits for it?

AudienceAudience
Dev venant d'un autre langage, prêt à raisonner cycle de vie Dev from another language, ready to reason about lifecycle
Format
Self-paced
ChapitresChapters
5
Date
Juin 2026 Jun 2026
≈ 18 min ●○○○ GoroutinesConcurrenceCycle de vie
SommaireContents ·
01CadrageFraming3 min

Deux lettres — go — et une fonction part seule. Qui l'attend ?Two letters — go — and a function leaves alone. Who waits for it?

Trois lignes que tout le monde écrit le premier jour. Le programme compile, tourne, et n'imprime rien — ou presque. Ce n'est pas un bug de fmt : c'est le modèle entier de Go qui tient dans un seul go. Lancer une goroutine est trivial ; l'attendre est tout le sujet.Three lines everyone writes on day one. The program compiles, runs, and prints nothing — or almost. It's no fmt bug: it's all of Go's model fitting inside a single go. Launching a goroutine is trivial; waiting for it is the whole point.

Le code qu'on croit anodinThe code we think is harmless
func main() {
    go fmt.Println("hello from the goroutine")
    // main ne l'attend pas — il continue, puis se termine
}                 // ← ici le programme s'arrête : "hello" ne s'imprime (presque) jamais
Qui survit à qui ?Who outlives whom?
main
se termine · tue le processreturns · kills the process
go : lancée en //go: launched async
goroutine
jamais ordonnancéenever scheduled

Le go n'a pas exécuté Println : il l'a planifié. Quand main retourne, le runtime arrête tout — sans attendre la goroutine, qui n'a pas même eu son tour.The go didn't run Println: it scheduled it. When main returns, the runtime stops everything — without waiting for the goroutine, which never even got its turn.

Le réflexe du volumeThe volume's reflex

« Qui lance cette goroutine — et qui l'attend, qui l'arrête ? » Une goroutine n'a pas de valeur de retour qu'on récupère, pas de parent qui la join par défaut. Sa fin n'est garantie par personne : c'est à toi de tendre le fil qui la rejoint."Who launches this goroutine — and who waits for it, who stops it?" A goroutine has no return value you collect, no parent that joins it by default. Nobody guarantees its end: it's on you to run the thread that rejoins it.

Pourquoi ce numéro ouvre la série.Why this issue opens the series.

Tout le reste de Go en concurrence — channels, select, WaitGroup, context — est une réponse à cette question. On ne commence pas par la syntaxe des channels : on commence par un go qui part dans le vide. Comprends le cycle de vie d'une goroutine, et la concurrence cesse d'être une loterie.Everything else in concurrent Go — channels, select, WaitGroup, context — answers this question. We don't start with channel syntax: we start with a go vanishing into the void. Understand a goroutine's lifecycle, and concurrency stops being a lottery.

02La fuiteThe leak4 min

Un Sleep n'attend pas. Il parie sur le temps.A Sleep doesn't wait. It bets on time.

Le réflexe, devant un go qui n'imprime rien, c'est d'ajouter un time.Sleep — et soudain « ça marche ». Mais on n'a pas attendu la goroutine : on a parié qu'elle finirait avant le réveil. Et la goroutine qu'on n'attend pas a une seconde manière de finir : ne jamais finir.Faced with a go that prints nothing, the reflex is to add a time.Sleep — and suddenly 'it works'. But you didn't wait for the goroutine: you bet it would finish before the alarm. And the goroutine you don't wait for has a second way to end: never ending at all.

time.Sleep — un pari déguisé en synchronisationtime.Sleep — a bet dressed as synchronization
func main() {
    go fmt.Println("done")
    time.Sleep(100 * time.Millisecond) // « ça marche maintenant »
}                 // ...jusqu'au jour où le travail dure 101 ms

Rien ne plante, le test passe — sur ta machine, aujourd'hui. Mais Sleep n'établit aucun lien entre main et la goroutine : il fige juste un délai arbitraire. Trop court, tu perds le résultat ; trop long, tu ralentis tout le monde. Tu n'as pas synchronisé — tu as deviné.Nothing crashes, the test passes — on your machine, today. But Sleep creates no link between main and the goroutine: it just freezes an arbitrary delay. Too short, you lose the result; too long, you slow everyone down. You didn't synchronize — you guessed.

Une goroutine n'a que deux fins honnêtes : finir son travail, ou être annulée. « Tourner pour toujours » n'en est pas une.A goroutine has only two honest endings: finish its work, or be cancelled. "Run forever" is not one of them.
03Qui attend ?Who waits?5 min

Attendre, ce n'est pas patienter. C'est tendre un fil.Waiting isn't biding time. It's running a thread back.

Pour attendre une goroutine, il faut un canal de retour explicite — un objet que main et la goroutine partagent, et qui dit « c'est fini ». Go en offre deux idiomes : le compteur (sync.WaitGroup) quand on attend des tâches, et le channel quand on attend un signal. Aucun ne devine le temps : ils se synchronisent.To wait for a goroutine you need an explicit return path — an object that main and the goroutine share, that says 'it's done'. Go offers two idioms: the counter (sync.WaitGroup) when you wait on tasks, and the channel when you wait on a signal. Neither guesses at time: they synchronize.

sync.WaitGroup — compter les tâches en volsync.WaitGroup — counting tasks in flight
func main() {
    var wg sync.WaitGroup
    wg.Add(1)              // une tâche à attendre
    go func() {
        defer wg.Done()    // signale la fin, quoi qu'il arrive
        fmt.Println("done")
    }()
    wg.Wait()              // bloque jusqu'à ce que le compteur retombe à 0
}

Add(1) avant de lancer, Done() en defer pour qu'il parte même en cas de panic, Wait() qui bloque jusqu'au zéro. Pour N goroutines, on Add(N) et on Wait() une fois. Le temps n'apparaît plus nulle part : on attend un compteur, pas une horloge.Add(1) before launching, Done() in a defer so it fires even on panic, Wait() blocking until zero. For N goroutines, Add(N) and Wait() once. Time appears nowhere anymore: you wait on a counter, not a clock.

Le réflexe en une ligneThe reflex in one line

Pour chaque go que tu écris, demande-toi tout de suite : où est le Wait, le <-done ou le ctx qui le rejoint ? Si tu ne sais pas répondre, tu n'as pas lancé une tâche — tu as ouvert une fuite.For every go you write, ask immediately: where is the Wait, the <-done or the ctx that rejoins it? If you can't answer, you didn't start a task — you opened a leak.

04Le piègeThe trap4 min

Attendre la fin ne suffit pas. Qui touche quoi ?Waiting for the end isn't enough. Who touches what?

On a recousu les goroutines avec un WaitGroup — bien. Mais lancer correctement n'est que la moitié : dès que deux goroutines touchent la même donnée, on a une course. Le WaitGroup garantit qu'elles ont fini, pas qu'elles ne se sont pas marché dessus. C'est ici que le slogan de Go cesse d'être une jolie phrase.We stitched the goroutines back with a WaitGroup — good. But launching correctly is only half: the moment two goroutines touch the same datum, you have a race. The WaitGroup guarantees they finished, not that they didn't clobber each other. This is where Go's slogan stops being a pretty phrase.

total++ — l'opération qui n'est pas atomiquetotal++ — the operation that isn't atomic
total := 0
var wg sync.WaitGroup
for i := 0; i < 1000; i++ {
    wg.Add(1)
    go func() {
        defer wg.Done()
        total++            // 1000 goroutines, UNE case mémoire partagée
    }()
}
wg.Wait()
fmt.Println(total)         // 973 ? 1000 ? indéterminé — go run -race hurle

total++ est en réalité trois gestes — lire, ajouter, réécrire — et mille goroutines les entrelacent. Deux lisent 42, écrivent 43 : une incrémentation perdue. Le résultat change à chaque exécution. Ce n'est pas un crash, c'est pire : un programme qui ment, et que seul -race démasque.total++ is really three steps — read, add, write back — and a thousand goroutines interleave them. Two read 42, write 43: one increment lost. The result changes every run. It's not a crash, it's worse: a program that lies, exposed only by -race.

Le contre-exempleThe counter-example

Et parfois, la bonne réponse n'est aucune goroutine. Mille additions tiennent en une boucle séquentielle, en moins d'une microseconde, sans WaitGroup ni channel. Lancer du concurrent pour du travail trivial, c'est payer la coordination plus cher que le calcul. go n'est pas gratuit — il a un coût de pile, de scheduler, de synchronisation.And sometimes the right answer is no goroutine. A thousand additions fit in one sequential loop, in under a microsecond, with no WaitGroup or channel. Spawning concurrency for trivial work pays more for coordination than for the compute. go isn't free — it costs stack, scheduler, synchronization.

Quand un mutex vaut mieux qu'un channel.When a mutex beats a channel.

Communiquer n'est pas un dogme. Pour un simple compteur très sollicité, un sync.Mutex (ou sync/atomic) est plus court et plus rapide qu'un channel. La règle de Go est nuancée : « ne communique pas en partageant la mémoire » — mais quand l'état partagé est minuscule et chaud, protège-le. Le channel brille pour transférer la propriété, pas pour garder un entier.Communicating isn't dogma. For a single hot counter, a sync.Mutex (or sync/atomic) is shorter and faster than a channel. Go's rule is nuanced: 'don't communicate by sharing memory' — but when shared state is tiny and hot, guard it. The channel shines at transferring ownership, not at holding one integer.

Le -race ne trouve pas tous les bugs — seulement ceux qui se sont produits pendant le run. L'absence de course ne se teste pas, elle se conçoit.The -race detector finds not all bugs — only those that happened during the run. Race-freedom isn't tested, it's designed.
05Bilan & éditoWrap-up & editorial2 min

Quatre façons de rejoindre ce qu'on a lancé.Four ways to rejoin what you launched.

La goroutine n'est pas une fonctionnalité de plus à connaître : c'est l'unité qui fait tenir tout le reste de la concurrence en Go. Une fois le réflexe en place — pour chaque go, son fil de retour — channels, select et context se lisent comme ses conséquences.The goroutine isn't one more feature to learn: it's the unit that holds all the rest of Go's concurrency together. Once the reflex is in place — for every go, its return path — channels, select and context read as its consequences.

Tu veux…You want to…Le gesteThe moveCe que ça coûteWhat it costs
Attendre N tâchesWait for N taskssync.WaitGroup : Add avant, Done en defer, Wait. Vol 1 · №02.sync.WaitGroup: Add before, Done in defer, Wait. Vol 1 · №02.Rien si l'Add est au bon endroit. Un Add dans la goroutine, et le Wait peut filer trop tôt.Nothing if Add sits in the right place. Put Add inside the goroutine and Wait may slip past too early.
Récupérer un résultatCollect a resultUn channel : la goroutine ch <- v, le récepteur <-ch. La donnée change de main. Vol 2.A channel: the goroutine ch <- v, the receiver <-ch. The value changes hands. Vol 2.Un récepteur DOIT venir, sinon la goroutine fuit. Le sens du channel est un contrat.A receiver MUST come, or the goroutine leaks. The channel's direction is a contract.
Signaler « fini »Signal 'done'close(done) sur un chan struct : diffusé à tous les <-done. Vol 2 · №03.close(done) on a chan struct: broadcast to every <-done. Vol 2 · №03.Seul l'émetteur ferme. Fermer deux fois, ou envoyer après close, panique.Only the sender closes. Closing twice, or sending after close, panics.
Pouvoir annulerBe able to cancelcontext.Context : la goroutine écoute <-ctx.Done() et s'arrête. Vol 8.context.Context: the goroutine listens on <-ctx.Done() and stops. Vol 8.Il faut écouter Done() partout où l'on peut bloquer. Une goroutine sourde au contexte ne s'annule pas.You must listen on Done() everywhere you might block. A goroutine deaf to the context can't be cancelled.
« go » est le mot le plus court et le plus dangereux de Go. Pas parce qu'il lance — parce qu'on oublie de le rejoindre."go" is the shortest and most dangerous word in Go. Not because it launches — because we forget to rejoin it.
Mot de l'éditeurFrom the editor

« Simple » ne veut pas dire « facile »."Simple" does not mean "easy".

On vend souvent la concurrence de Go comme triviale : un mot-clé, et tu fais du parallèle. C'est vrai pour le lancement, et c'est exactement le piège. Tant qu'on voit go comme un raccourci gratuit — on en sème partout, on rapièce avec des Sleep, on partage la mémoire à la légère — on reconstruit à l'exécution, en data races et en fuites, les bugs qu'une discipline simple aurait évités. Le jour où chaque go s'accompagne, dans la même pensée, de la question « qui le rejoint, et qui peut l'annuler ? », la concurrence cesse d'être une loterie.Go's concurrency is often sold as trivial: one keyword, and you're parallel. That's true of launching, and that's exactly the trap. As long as you see go as a free shortcut — scattering it everywhere, patching with Sleeps, sharing memory carelessly — you rebuild at runtime, in data races and leaks, the very bugs a simple discipline would have avoided. The day every go comes, in the same thought, with the question 'who rejoins it, and who can cancel it?', concurrency stops being a lottery.

Prochain numéro : sync.WaitGroup en grand. Attendre des dizaines de goroutines sans rien perdre — et le piège de la variable de boucle qui, jusqu'à Go 1.22, transformait dix goroutines en dix copies du même bug.Next issue: sync.WaitGroup at scale. Waiting on dozens of goroutines without losing a thing — and the loop-variable trap that, until Go 1.22, turned ten goroutines into ten copies of the same bug.

Retour au kiosqueBack to newsstand