veilletech.fr
3 sept. Feed du jour
#08 GO Article

Go 1.27 prouve vos fuites de goroutines

Une goroutine oubliée ne crie jamais. Maintenant elle est nommée.

Go 1.27 ajoute le profil goroutineleak, exposé par runtime/pprof et net/http/pprof. Plutôt qu'une heuristique de durée, il s'appuie sur le ramasse-miettes pour prouver qu'une goroutine attend un canal ou un verrou devenu inatteignable — technique issue d'un article ASPLOS 2025 co-écrit avec Uber.

3 min de lectureavancévidéo 1:14
Partager
Sommaire6 sections
  1. Ce qui se passe
  2. Le cas d'école
  3. Le relever
  4. Comment il le prouve
  5. Les limites, qui comptent autant
  6. À retenir

Ce qui se passe

Une goroutine est fuitée quand elle est bloquée et que les conditions de son déblocage ne peuvent plus être réunies. Elle ne plante pas, elle n'apparaît nulle part, et elle retient sa pile et la mémoire qu'elle référence — avec, à la clé, du travail supplémentaire pour le ramasse-miettes, particulièrement sensible sous GOMEMLIMIT.

Les outils existants s'arrêtaient à la porte de la production. goleak et le paquet synctest (Go 1.25) instrumentent des tests. Un profil de goroutines ordinaire, lui, ne sait pas distinguer une goroutine fuitée d'une goroutine temporairement bloquée en grand nombre par conception — un pic de trafic sur un microservice ressemble exactement à une fuite.

Go 1.27 ajoute le profil goroutineleak, précis et sans faux positifs, au prix d'une couverture partielle.

Le cas d'école

Go
ch := make(chan result)
for _, w := range ws {
    go func() {
        res, err := processWorkItem(w)
        ch <- result{res, err}
    }()
}
for range len(ws) {
    r := <-ch
    if r.err != nil {
        return nil, r.err // les émetteurs restants bloquent pour toujours
    }
    results = append(results, r.res)
}

Le canal n'est pas tamponné : au retour anticipé, les goroutines qui n'ont pas encore émis restent bloquées à vie. Le correctif tient en un argument — make(chan result, len(ws)).

Le relever

Si net/http/pprof est déjà en place, il n'y a rien à faire : le point de terminaison /debug/pprof/goroutineleak est disponible.

Terminal
curl http://localhost:6060/debug/pprof/goroutineleak > leak.prof
go tool pprof leak.prof
TEXT
Type: goroutineleak
(pprof) list processWorkItems
Total: 116
ROUTINE ======================== main.processWorkItems.func1
         0        116 (flat, cum)   100% of Total
         .        116     33:      ch <- result{res, err}

La ligne fautive est nommée, avec le nombre de goroutines coincées dessus.

Comment il le prouve

La définition retenue est inductive : une goroutine est vivante si elle n'est bloquée par aucune primitive, ou si au moins une primitive qui la bloque est référencée par une autre goroutine vivante. Le reste est fuité.

C'est exactement une question d'accessibilité mémoire, que le ramasse-miettes sait déjà résoudre. Le GC tricolore concurrent est donc détourné : seules les goroutines non bloquées servent de racines de marquage, puis les goroutines bloquées sur une primitive marquée sont ajoutées comme racines à leur tour, jusqu'à ce que plus rien ne s'ajoute. Celles qui n'ont jamais été ajoutées sont déclarées fuitées — puis remises comme racines pour que le cycle marque normalement la mémoire.

Les limites, qui comptent autant

À retenir

  1. Le profil est un complément de production à goleak et synctest, pas leur remplaçant.
  2. Il ne détecte que le blocage sur primitives Go de premier ordre — le reste demande toujours de l'analyse humaine.
  3. Travail issu d'une collaboration entre l'université d'Aarhus, Washington University in St. Louis et Uber (Saioc et al., ASPLOS 2025).

Source : Go Blog