Um context em Go avisa cada goroutine de um trabalho quando parar. Aprenda cancelamento, timeouts e valores de requisição, e depois monte um worker pool, um pipeline, fan-out, um errgroup e um semáforo que nunca vazam.
Iniciar uma goroutine é fácil. Pará-la na hora certa é a parte difícil. Um usuário fecha a aba do navegador, uma chamada ao banco de dados demora demais, ou uma de cinco requisições paralelas falha. Em cada caso, algumas goroutines deveriam desistir, e alguma coisa precisa avisá-las.
Em Go, essa coisa é o context.Context. Este post cobre primeiro cancelamento, timeouts e valores com escopo de requisição, e depois cinco padrões de concorrência construídos sobre eles. Ele parte do princípio de que você já viu goroutines, channels, select e sync.WaitGroup nas três partes anteriores. Todo programa abaixo rodou no Go 1.26, e a saída foi colada da execução.
Um context é um sinal que diz pare
Um context é um valor que você passa para uma função para que ela saiba quando parar de trabalhar. O menor context útil vem de context.WithCancel:
package main
import (
"context"
"fmt"
)
func main() {
ctx, cancel := context.WithCancel(context.Background())
fmt.Println("before:", ctx.Err())
cancel()
<-ctx.Done() // closed now, so this returns at once
fmt.Println("after:", ctx.Err())
cancel() // calling it again does nothing
fmt.Println("again:", ctx.Err())
}
Ele imprime:
before: <nil>
after: context canceled
again: context canceled
context.Background() é o context raiz vazio. Ele nunca termina e não carrega nada. WithCancel o envolve e devolve duas coisas: um context novo e uma função cancel que o encerra.
Dois métodos dizem se um context terminou. ctx.Done() devolve um channel que é fechado quando o context termina. ctx.Err() devolve nil enquanto ele está vivo, e o motivo quando não está mais. Chamar cancel duas vezes é seguro. A segunda chamada não faz nada.
Explicado como se você tivesse dez anos
Imagine uma gerente começando um trabalho grande. Ela entrega um walkie-talkie para cada pessoa da equipe. Algumas dessas pessoas contratam ajudantes, e entregam a eles walkie-talkies na mesma frequência.
Quando a gerente diz “parem” no walkie-talkie dela, todo mundo que recebeu um nessa corrente escuta. As pessoas largam as ferramentas.
Um timeout é um despertador preso com fita a um walkie-talkie. Quando ele toca, diz “parem” por você, mesmo que ninguém tenha apertado o botão.
A versão precisa
Contexts formam uma árvore. Toda função With… recebe um pai e devolve um filho. Quando um context é cancelado, Go fecha o channel Done dele e depois cancela todos os filhos, e os filhos dos filhos, até o fim.
O cancelamento só viaja para baixo. Cancelar um filho nunca afeta o pai, nem os irmãos. A função cancel que você recebe também desfaz a ligação do filho com o pai, e é por isso que você sempre a chama, normalmente com defer cancel().
Onde a analogia falha: uma pessoa com um walkie-talkie não tem como deixar de ouvir. Uma goroutine tem. Fechar Done não para nada sozinho. Seu código precisa verificar ctx.Done() ou ctx.Err() e retornar. Uma goroutine que nunca olha simplesmente continua rodando.
Timeouts e deadlines
Um timeout é o motivo mais comum para um context terminar, e context.WithTimeout configura um em uma única chamada. A função abaixo respeita o cancelamento. Ela espera pelo trabalho ou por ctx.Done(), o que vier primeiro:
package main
import (
"context"
"errors"
"fmt"
"time"
)
// work pretends to do a job that takes d, but gives up if ctx ends first.
func work(ctx context.Context, d time.Duration) error {
select {
case <-time.After(d):
return nil
case <-ctx.Done():
return ctx.Err()
}
}
func main() {
ctx, cancel := context.WithTimeout(context.Background(), 20*time.Millisecond)
defer cancel()
err := work(ctx, 2*time.Second)
fmt.Println("slow job:", err)
fmt.Println(errors.Is(err, context.DeadlineExceeded), errors.Is(err, context.Canceled))
ctx2, cancel2 := context.WithTimeout(context.Background(), 2*time.Second)
defer cancel2()
fmt.Println("quick job:", work(ctx2, time.Millisecond))
past := time.Now().Add(-time.Minute)
ctx3, cancel3 := context.WithDeadline(context.Background(), past)
defer cancel3()
fmt.Println("deadline already gone:", work(ctx3, time.Millisecond))
}
Ele imprime:
slow job: context deadline exceeded
true false
quick job: <nil>
deadline already gone: context deadline exceeded
O primeiro trabalho precisa de 2 segundos e recebe 20 milissegundos, então o context ganha. ctx.Err() devolve context.DeadlineExceeded. As margens são largas de propósito. Uma diferença de 100 vezes garante que o resultado nunca depende de quão ocupada está a máquina.
Compare erros com errors.Is, como mostrou a parte sobre erros. context.Canceled significa que alguém chamou cancel. context.DeadlineExceeded significa que o tempo acabou. Código que tenta de novo costuma tratar os dois de forma diferente: um timeout pode valer outra tentativa, mas um cancelamento significa que quem chamou já foi embora.
WithDeadline recebe um ponto no tempo em vez de uma duração. WithTimeout(ctx, d) é exatamente WithDeadline(ctx, time.Now().Add(d)). Um deadline que já passou dá a você um context que acabou antes de a sua função começar.
time.After dentro de um select era uma pequena armadilha de memória, porque o timer continuava vivo até disparar. Desde o Go 1.23, um timer que nada referencia pode ser coletado pelo garbage collector, então esse padrão agora é seguro.
O cancelamento desce pela árvore
A árvore importa mais quando várias goroutines dividem um trabalho. Neste programa, root tem dois filhos. A tem um timeout de 20ms e dois workers abaixo dele, A1 e A2. B é um filho separado, com o próprio worker. Observe até onde chega o timeout de A, e até onde não chega:
Uma árvore de contexts com um timeout em um dos galhos. Quando o timer de 20ms de A dispara, A termina e o sinal desce até os workers A1 e A2, que param com DeadlineExceeded. B, o irmão, e root, o pai, continuam rodando. Só quando main cancela root o sinal chega a B.
Aqui estão esses passos em palavras, caso a animação não rode para você:
root,A,A1,A2eBestão todos vivos. Cada worker fica em um loop comselect, fazendo pequenas unidades de trabalho.- O timer de 20ms de
Adispara.Atermina, eA.Err()devolvecontext deadline exceeded. - O sinal desce.
A1eA2veem seus channelsDonefecharem, e os dois param com o mesmo erro. Berootnão são afetados. O cancelamento desceu a partir deA, sem subir atéroote sem ir para o lado, atéB.mainchamacancelRoot(). Agora o sinal desce derootatéB, que para comcontext canceled.
Este é o programa que a animação acompanha:
package main
import (
"context"
"fmt"
"time"
)
// worker does small units of work until its context ends.
func worker(ctx context.Context, name string, report chan<- string) {
tick := time.NewTicker(time.Millisecond)
defer tick.Stop()
for {
select {
case <-ctx.Done():
report <- name + " stopped: " + ctx.Err().Error()
return
case <-tick.C:
// one small unit of work
}
}
}
func main() {
root, cancelRoot := context.WithCancel(context.Background())
defer cancelRoot()
a, cancelA := context.WithTimeout(root, 20*time.Millisecond)
defer cancelA()
a1, cancelA1 := context.WithCancel(a)
defer cancelA1()
a2, cancelA2 := context.WithCancel(a)
defer cancelA2()
b, cancelB := context.WithCancel(root)
defer cancelB()
repA1 := make(chan string)
repA2 := make(chan string)
repB := make(chan string)
go worker(a1, "A1", repA1)
go worker(a2, "A2", repA2)
go worker(b, "B", repB)
fmt.Println(<-repA1)
fmt.Println(<-repA2)
fmt.Println("A:", a.Err())
fmt.Println("B:", b.Err())
fmt.Println("root:", root.Err())
cancelRoot()
fmt.Println(<-repB)
}
Ele imprime:
A1 stopped: context deadline exceeded
A2 stopped: context deadline exceeded
A: context deadline exceeded
B: <nil>
root: <nil>
B stopped: context canceled
Repare na ordem dos eventos em main. Ele espera os dois workers de A reportarem e só então verifica B e root. Os dois ainda são nil, mesmo com um galho inteiro da árvore já encerrado ao lado deles. Só cancelRoot() chega a B.
Cada worker é o formato a copiar. É um laço for em volta de um select, com um case para o trabalho e um case para ctx.Done(). Enquanto cada passo que bloqueia estiver dentro de um select assim, a goroutine sempre consegue ouvir “pare”.
Onde fica o ctx: primeiro parâmetro, nunca campo de struct
A biblioteca padrão e quase todo código Go concordam em uma convenção. Uma função que pode ser cancelada recebe um context.Context como primeiro parâmetro, chamado ctx:
func (s *Store) LoadUser(ctx context.Context, id int) (User, error)
Não guarde um context em uma struct. Um context pertence a uma chamada, como uma requisição HTTP, mas uma struct costuma durar muitas chamadas. Um context guardado acaba sendo o errado: cancelado cedo demais para a próxima requisição, ou nunca cancelado. Passá-lo explicitamente também deixa claro quais funções podem ser paradas.
Mais duas regras. Nunca passe um context nil. Se você ainda não tem um, use context.TODO(), que se comporta como Background() mas marca o ponto para depois. E sempre chame a função cancel. O go vet verifica essa segunda regra para você. Dado ctx, _ := context.WithTimeout(context.Background(), time.Second), ele reporta:
$ go vet .
main.go:10:7: the cancel function returned by context.WithTimeout should be called, not discarded, to avoid a context leak
Por que parou: Cause e AfterFunc
ctx.Err() só diz “canceled” ou “deadline exceeded”, o que muitas vezes é pouco para depurar. O Go 1.20 adicionou WithCancelCause, e o Go 1.21 adicionou WithTimeoutCause e AfterFunc:
package main
import (
"context"
"errors"
"fmt"
"time"
)
func main() {
ctx, cancel := context.WithCancelCause(context.Background())
cleaned := make(chan struct{})
context.AfterFunc(ctx, func() {
close(cleaned) // runs in its own goroutine once ctx ends
})
cancel(errors.New("server shutting down"))
<-cleaned
fmt.Println("cleanup ran")
fmt.Println("Err: ", ctx.Err())
fmt.Println("Cause:", context.Cause(ctx))
slow := errors.New("payment provider took too long")
ctx2, cancel2 := context.WithTimeoutCause(context.Background(), 20*time.Millisecond, slow)
defer cancel2()
<-ctx2.Done()
fmt.Println("Err: ", ctx2.Err())
fmt.Println("Cause:", context.Cause(ctx2))
}
Ele imprime:
cleanup ran
Err: context canceled
Cause: server shutting down
Err: context deadline exceeded
Cause: payment provider took too long
O cancel de WithCancelCause recebe um erro. ctx.Err() continua devolvendo context.Canceled, então as verificações existentes seguem funcionando, e context.Cause(ctx) devolve o seu erro. WithTimeoutCause faz o mesmo para um timeout. Em uma linha de log, “payment provider took too long” é muito mais útil do que “context deadline exceeded”.
context.AfterFunc registra uma função para rodar quando o context terminar. Ela roda na própria goroutine, e é por isso que main espera no channel cleaned antes de imprimir. Imprimir de dentro do callback entraria em corrida com a saída do próprio main. AfterFunc devolve uma função stop que cancela o registro do callback se ele ainda não rodou.
Valores com escopo de requisição
Um context também pode carregar valores, e context.WithValue é o jeito de dados como um ID de requisição atravessarem uma cadeia de chamadas sem entrar na assinatura de cada função:
package main
import (
"context"
"fmt"
)
// An unexported key type: no other package can make a key equal to this one.
type requestIDKey struct{}
func withRequestID(ctx context.Context, id string) context.Context {
return context.WithValue(ctx, requestIDKey{}, id)
}
func requestID(ctx context.Context) string {
id, ok := ctx.Value(requestIDKey{}).(string)
if !ok {
return "no-request-id"
}
return id
}
func loadUser(ctx context.Context, userID int) {
fmt.Printf("[%s] loading user %d\n", requestID(ctx), userID)
}
func main() {
ctx := withRequestID(context.Background(), "req-7f3a")
loadUser(ctx, 42)
loadUser(context.Background(), 43)
}
Ele imprime:
[req-7f3a] loading user 42
[no-request-id] loading user 43
A chave é um tipo struct vazio e privado. Dois pacotes que usam a string "id" como chave sobrescreveriam um ao outro. Uma chave de um tipo não exportado não colide com a chave de mais ninguém. ctx.Value devolve any, então você verifica o tipo com comma-ok e trata o caso em que o valor não existe.
Use valores só para dados com escopo de requisição. Ou seja, coisas que descrevem a requisição e cruzam fronteiras de API: um ID de requisição, um ID de trace, o usuário autenticado. Não use para um handle de banco de dados, um logger ou uma flag de configuração. Essas são dependências de verdade, e o lugar delas é em parâmetros ou campos de struct, onde o compilador consegue vê-las. Value não tem tipo, e é encontrado subindo a árvore um pai por vez, então dependências escondidas ali falham em tempo de execução, e não na hora do build.
Worker pool
Um worker pool roda um número fixo de goroutines que pegam jobs do mesmo channel. Ele limita quanto trabalho acontece ao mesmo tempo, não importa quantos jobs cheguem:
package main
import (
"cmp"
"fmt"
"slices"
"sync"
)
type result struct {
job, square int
}
func worker(jobs <-chan int, results chan<- result) {
for j := range jobs {
results <- result{job: j, square: j * j}
}
}
func main() {
jobs := make(chan int)
results := make(chan result)
var wg sync.WaitGroup
for range 3 {
wg.Go(func() { worker(jobs, results) })
}
go func() {
for j := range 9 {
jobs <- j + 1
}
close(jobs) // workers' range loops end
}()
go func() {
wg.Wait()
close(results) // main's range loop ends
}()
var all []result
for r := range results {
all = append(all, r)
}
slices.SortFunc(all, func(x, y result) int { return cmp.Compare(x.job, y.job) })
fmt.Println(len(all), "results")
fmt.Println(all)
}
Ele imprime:
9 results
[{1 1} {2 4} {3 9} {4 16} {5 25} {6 36} {7 49} {8 64} {9 81}]
Três workers dividem o channel jobs. Cada job vai para exatamente um deles. Dois closes fazem tudo terminar. Fechar jobs encerra o laço range de cada worker. Uma goroutine separada espera todos os workers e depois fecha results, o que encerra o laço em main.
Os workers terminam os jobs na ordem que o scheduler escolher, então os resultados chegam em uma ordem diferente a cada execução. Cada resultado carrega o número do seu job, e ordenar por ele deixa a saída igual todas as vezes. Imprimir os resultados à medida que chegam não deixaria.
Pipeline
Um pipeline é uma cadeia de estágios ligados por channels, em que cada estágio é uma goroutine que lê de um channel e escreve no próximo. Este tem três: um gerador, um que eleva ao quadrado e um que soma. O gerador nunca para sozinho, então o context é o único jeito de desligá-lo:
package main
import (
"context"
"fmt"
)
// naturals sends 1, 2, 3, ... until ctx ends. It never stops on its own.
func naturals(ctx context.Context) <-chan int {
out := make(chan int)
go func() {
defer close(out)
for n := 1; ; n++ {
select {
case out <- n:
case <-ctx.Done():
return
}
}
}()
return out
}
// square sends the square of every value it receives, until in closes or ctx ends.
func square(ctx context.Context, in <-chan int) <-chan int {
out := make(chan int)
go func() {
defer close(out)
for n := range in {
select {
case out <- n * n:
case <-ctx.Done():
return
}
}
}()
return out
}
// sumFirst adds up the first k values from in.
func sumFirst(in <-chan int, k int) int {
total := 0
for range k {
total += <-in
}
return total
}
func main() {
ctx, cancel := context.WithCancel(context.Background())
squares := square(ctx, naturals(ctx))
fmt.Println("sum of first 5 squares:", sumFirst(squares, 5))
cancel()
for range squares {
// drain until square closes its channel
}
fmt.Println("both stages stopped")
}
Ele imprime:
sum of first 5 squares: 55
both stages stopped
sumFirst pega cinco valores e retorna, deixando duas goroutines sem ninguém lendo a saída delas. Sem ctx, as duas ficariam bloqueadas em out <- … para sempre. Com ele, cancel() dá ao select de cada estágio uma segunda saída. Elas retornam, e o defer close(out) delas roda.
O laço vazio for range squares prova isso. Ele só termina quando square fecha o seu channel, e square só o fecha depois de parar. Se o cancelamento não funcionasse, o programa travaria ali em vez de imprimir a última linha.
Fan-out e fan-in
Fan-out significa várias goroutines lendo do mesmo channel para dividir o trabalho. Fan-in significa juntar os channels de saída separados delas de volta em um só:
package main
import (
"context"
"fmt"
"slices"
"sync"
)
func square(ctx context.Context, in <-chan int) <-chan int {
out := make(chan int)
go func() {
defer close(out)
for n := range in {
select {
case out <- n * n:
case <-ctx.Done():
return
}
}
}()
return out
}
// merge copies every value from every input onto one channel.
func merge(ctx context.Context, inputs ...<-chan int) <-chan int {
out := make(chan int)
var wg sync.WaitGroup
for _, in := range inputs {
wg.Go(func() {
for v := range in {
select {
case out <- v:
case <-ctx.Done():
return
}
}
})
}
go func() {
wg.Wait()
close(out)
}()
return out
}
func main() {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
src := make(chan int)
go func() {
defer close(src)
for n := range 10 {
src <- n + 1
}
}()
// fan out: three workers read the same channel
w1 := square(ctx, src)
w2 := square(ctx, src)
w3 := square(ctx, src)
// fan in: merge their outputs
var got []int
for v := range merge(ctx, w1, w2, w3) {
got = append(got, v)
}
slices.Sort(got)
fmt.Println(got)
}
Ele imprime:
[1 4 9 16 25 36 49 64 81 100]
Três estágios square leem de src, então os dez números são divididos entre eles. merge inicia uma goroutine por entrada, copia os valores para out e fecha out quando todas as entradas se esgotam. É o mesmo formato de WaitGroup seguido de close do worker pool.
Os valores juntados chegam intercalados, sem ordem fixa, então main os ordena antes de imprimir. Cada estágio também recebe ctx. Aqui o programa roda até o fim e nada é cancelado, mas se main parasse de ler antes, o cancel adiado liberaria todas as goroutines.
Um errgroup, construído a partir de WaitGroup
Uma necessidade comum é rodar várias tarefas ao mesmo tempo, parar todas quando uma falha e devolver esse primeiro erro. O pacote errgroup faz isso, mas ele fica em golang.org/x/sync, fora da biblioteca padrão. A ideia é pequena o bastante para construir com sync.WaitGroup, sync.Once e um context:
package main
import (
"context"
"fmt"
"sync"
"time"
)
// Group runs tasks in goroutines, keeps the first error,
// and cancels the shared context when that error happens.
type Group struct {
wg sync.WaitGroup
once sync.Once
err error
cancel context.CancelCauseFunc
}
func WithContext(parent context.Context) (*Group, context.Context) {
ctx, cancel := context.WithCancelCause(parent)
return &Group{cancel: cancel}, ctx
}
func (g *Group) Go(task func() error) {
g.wg.Go(func() {
if err := task(); err != nil {
g.once.Do(func() {
g.err = err
g.cancel(err)
})
}
})
}
func (g *Group) Wait() error {
g.wg.Wait()
g.cancel(g.err) // release the context even if nothing failed
return g.err
}
func main() {
g, ctx := WithContext(context.Background())
status := make([]string, 3)
for i := range 3 {
g.Go(func() error {
if i == 1 {
status[i] = "failed"
return fmt.Errorf("fetch %d: connection refused", i)
}
select {
case <-time.After(2 * time.Second):
status[i] = "finished"
return nil
case <-ctx.Done():
status[i] = "stopped early"
return ctx.Err()
}
})
}
err := g.Wait()
fmt.Printf("%q\n", status)
fmt.Println("first error:", err)
fmt.Println("cause:", context.Cause(ctx))
}
Ele imprime:
["stopped early" "failed" "stopped early"]
first error: fetch 1: connection refused
cause: fetch 1: connection refused
A tarefa 1 falha na hora. once.Do guarda o erro dela e cancela o context compartilhado com esse erro como causa. As tarefas 0 e 2 estão esperando um trabalho de 2 segundos, veem ctx.Done() fechar e param antes.
Essas duas também devolvem um erro, context.Canceled. Ele nunca substitui o verdadeiro. sync.Once roda a função exatamente uma vez, e qualquer outra chamada espera até essa primeira execução terminar. Quando uma tarefa cancelada chega a once.Do, o primeiro erro já está guardado. Então “first error” aqui significa o erro que causou o cancelamento, todas as vezes.
Cada tarefa escreve no seu próprio elemento de status, então nenhum mutex é necessário. main só lê o slice depois que Wait retorna.
Concorrência limitada com um semáforo
Às vezes você tem muitas tarefas, mas só pode rodar algumas de cada vez, porque uma API tem limite de requisições ou um banco de dados tem dez conexões. Um channel com buffer vira um semáforo simples, com uma vaga para cada unidade da sua capacidade:
package main
import (
"fmt"
"sync"
"time"
)
func main() {
const limit = 2
sem := make(chan struct{}, limit)
var (
mu sync.Mutex
running int
peak int
wg sync.WaitGroup
)
results := make([]int, 6)
for i := range 6 {
sem <- struct{}{} // take a slot; blocks while two are busy
wg.Go(func() {
defer func() { <-sem }() // give the slot back
mu.Lock()
running++
peak = max(peak, running)
mu.Unlock()
time.Sleep(5 * time.Millisecond) // pretend to call a slow service
results[i] = i * 10
mu.Lock()
running--
mu.Unlock()
})
}
wg.Wait()
fmt.Println(results)
fmt.Println("never more than", limit, "at once:", peak <= limit)
}
Ele imprime:
[0 10 20 30 40 50]
never more than 2 at once: true
Enviar para sem ocupa uma vaga. Com capacidade 2, o terceiro envio bloqueia até uma tarefa em andamento terminar e receber de sem para liberar sua vaga. Pegar a vaga antes de wg.Go garante que nunca existem mais de duas goroutines, e não seis goroutines com quatro delas esperando.
O programa imprime peak <= limit, e não peak. Se duas tarefas de fato se sobrepuseram depende do escalonamento. É provável, mas não é garantido. O limite, por outro lado, vale em toda execução. Para tornar a espera cancelável, pegue a vaga em um select com um case <-ctx.Done():.
Toda goroutine precisa de um jeito de parar
Uma goroutine bloqueada para sempre em um channel é um vazamento. Ela segura sua stack e tudo para o que aponta, e nada vai liberá-la. Aqui está o vazamento, ao lado da correção:
package main
import (
"context"
"fmt"
"time"
)
// leaky sends three values, with no way to give up.
func leaky(out chan<- int, exited chan<- struct{}) {
defer close(exited)
for i := range 3 {
out <- i
}
}
// fixed sends three values, but stops as soon as ctx ends.
func fixed(ctx context.Context, out chan<- int, exited chan<- struct{}) {
defer close(exited)
for i := range 3 {
select {
case out <- i:
case <-ctx.Done():
return
}
}
}
func report(name string, exited <-chan struct{}) {
select {
case <-exited:
fmt.Println(name, "goroutine exited")
case <-time.After(100 * time.Millisecond):
fmt.Println(name, "goroutine still stuck on its send")
}
}
func main() {
out := make(chan int)
exited := make(chan struct{})
go leaky(out, exited)
fmt.Println("leaky got", <-out) // take one value, then walk away
report("leaky", exited)
ctx, cancel := context.WithCancel(context.Background())
out2 := make(chan int)
exited2 := make(chan struct{})
go fixed(ctx, out2, exited2)
fmt.Println("fixed got", <-out2)
cancel() // walk away, but say so
report("fixed", exited2)
}
Ele imprime:
leaky got 0
leaky goroutine still stuck on its send
fixed got 0
fixed goroutine exited
Os dois produtores querem enviar três valores, e main pega só um de cada. leaky não tem como desistir, então fica parado em out <- 1 pelo resto do programa. report espera 100ms, muito mais do que uma goroutine precisa para sair, e a goroutine continua lá. fixed coloca o envio dentro de um select com ctx.Done(), então cancel() permite que ele retorne.
Antes de escrever go, responda a uma pergunta: o que faz esta goroutine retornar? Boas respostas são “o channel de entrada dela fecha”, “o context dela é cancelado” ou “ela termina um trabalho de tamanho limitado”. “Alguém lê a saída dela” só é uma boa resposta se esse alguém não puder parar antes.
O Go 1.26 adiciona um profile experimental goroutineleak ao runtime/pprof, que reporta goroutines bloqueadas em algo que mais nada consegue alcançar. Ele só existe quando você compila com GOEXPERIMENT=goroutineleakprofile. Sem isso, pprof.Lookup("goroutineleak") devolve nil, e chamar WriteTo nele causa panic, então verifique nil primeiro.
O que lembrar
- Um context carrega um sinal de parada, e às vezes um deadline, para baixo em uma árvore. O cancelamento vai só para os filhos, nunca sobe para os pais nem vai para os irmãos.
- Cancelar não para uma goroutine sozinho. Coloque cada passo que bloqueia em um
selectcomcase <-ctx.Done():e devolvactx.Err(). context.Canceledsignifica que alguém chamoucancel, econtext.DeadlineExceededsignifica que o tempo acabou. Usecontext.Causepara dizer o porquê.- Passe
ctxcomo primeiro parâmetro, nunca o guarde em uma struct e sempre chamecancel. Ogo vetpega um descartado. - Reserve os valores de context para dados com escopo de requisição, com um tipo de chave não exportado.
- Worker pools, pipelines, fan-in e errgroups terminam todos do mesmo jeito: um
WaitGroupespera, e então umcloseavisa quem lê que acabou. Ordene resultados que chegam sem ordem fixa. - Antes de cada instrução
go, saiba o que faz aquela goroutine retornar.
Toda goroutine que você inicia precisa de um jeito de parar, e um context costuma ser esse jeito.