O padrão básico
O sync.WaitGroup conta goroutines em execução. Add aumenta a contagem, Done a diminui, Wait bloqueia até ela ser zero.
Os três downloads executam de forma concorrente, então o programa leva cerca de 10 ms em vez de 30. Os resultados saem na ordem da entrada porque cada goroutine escreve só no seu próprio índice de sizes, e o main só os lê depois do Wait.
O valor zero de um WaitGroup já está pronto para uso. Sem construtor.
As três regras
Chame Add antes do go, não dentro da goroutine. Se a goroutine chama Add ela mesma, o main pode chegar ao Wait antes de qualquer goroutine começar, ver a contagem em zero e retornar sem que o trabalho tenha começado. Quando você sabe a contagem de antemão, um único wg.Add(len(files)) antes do laço é equivalente.
Chame Done com defer na primeira linha da goroutine. Uma goroutine que retorna cedo por causa de um erro, ou que causa panic, ainda decrementa o contador. Um Done faltando deixa o Wait bloqueado para sempre. Se for a única goroutine restante, o runtime reporta fatal error: all goroutines are asleep com sync.WaitGroup.Wait no trace.
Nunca copie um WaitGroup depois do primeiro uso. Passe *sync.WaitGroup para funções, ou capture a variável em uma closure como acima.
Passando um WaitGroup para uma função
Quando o corpo da goroutine é uma função com nome, passe um ponteiro:
Com wg sync.WaitGroup como parâmetro por valor, cada worker chamaria Done na sua própria cópia e o main ficaria bloqueado no Wait para sempre. O go vet pega isso antes de você executar qualquer coisa:
./main.go:8:24: worker passes lock by value: sync.WaitGroup contains sync.noCopy
Um design mais limpo tira a concorrência de dentro de worker: deixe-a ser uma função comum e faça a contabilidade de Add/Done na closure de quem chama. Aí worker fica fácil de testar e de chamar de forma síncrona.
Contador negativo
Done é Add(-1). Se a contagem ficar abaixo de zero, o programa causa panic:
A saída é recovered: sync: negative WaitGroup counter. A causa de sempre é uma goroutine com defer wg.Done() que também chama wg.Done() explicitamente em algum caminho.
Coletando erros
Um WaitGroup só conta. Para erros, dê a cada goroutine a sua própria posição e inspecione-as depois do Wait:
O errors.Join (Go 1.20) ignora valores nil e devolve nil se todos forem nil, então ele combina "um erro por goroutine" sem nenhuma contabilidade extra.
Se você quiser interromper o trabalho restante assim que uma goroutine falhar, use golang.org/x/sync/errgroup. Ele é um WaitGroup mais o primeiro erro mais um context que é cancelado na falha, e g.SetLimit(n) limita a concorrência. Ele fica fora da biblioteca padrão, então não roda no editor desta página:
g, ctx := errgroup.WithContext(ctx)
for _, h := range hosts {
g.Go(func() error { return checkCtx(ctx, h) })
}
if err := g.Wait(); err != nil {
return err // the first error; ctx was cancelled for the others
}
Um worker pool
Um número fixo de goroutines lendo jobs de um channel mantém a concorrência limitada, não importa quantos jobs existam. O WaitGroup diz quando todos os workers terminaram, que é quando o channel de resultados pode ser fechado.
A ordem das três peças importa:
- O
mainprecisa estar recebendo resultados enquanto os workers executam. Se omainchamassewg.Wait()diretamente antes de ler, os workers ficariam bloqueados enviando pararesults, nunca chegariam aoDone, e tudo entraria em deadlock. É por isso que oWaitexecuta na sua própria goroutine. - O
close(results)só acontece depois doWait, então nenhum worker consegue enviar em um channel fechado. - Quem alimenta os jobs também executa em uma goroutine, então alimentar e coletar se sobrepõem.
Qual worker tratou qual job muda de uma execução para outra, então o programa ordena por job antes de imprimir. Tudo o que ele imprime é determinístico.
WaitGroup, channel ou errgroup
| Necessidade | Use |
|---|---|
| Esperar N goroutines, resultados em posições indexadas | sync.WaitGroup |
| Esperar uma goroutine | um channel done ou o próprio channel de resultado |
| Resultados enviados conforme terminam | um channel, fechado depois do wg.Wait() |
| Parar tudo no primeiro erro | errgroup.WithContext |
| Parar tudo em um timeout ou cancelamento de quem chama | context.Context mais um WaitGroup ou errgroup |
O Go 1.25 traz wg.Go(func() { ... }), que faz o Add(1) e o Done adiado por você. Código para o Go 1.24 e anteriores, incluindo o editor desta página, usa a forma explícita mostrada acima.
Erros comuns
wg.Add(1)dentro da goroutine. OWaitpode retornar antes de ele executar.- Esquecer o
Doneem um retorno antecipado. Sempre usedefer wg.Done(). - Passar o WaitGroup por valor. Use um ponteiro; o
go vetaponta a cópia. - Esperar na mesma goroutine que precisa esvaziar um channel. Mova o
wg.Wait()e oclosepara uma goroutine separada. - Reutilizar um WaitGroup antes de o
Waitanterior ter retornado. Comece um novo ciclo de chamadasAddsó depois de oWaitterminar.
Perguntas frequentes
Como o sync.WaitGroup funciona em Go?
Um WaitGroup é um contador. wg.Add(n) o aumenta, wg.Done() o diminui em um e wg.Wait() bloqueia até ele chegar a zero. Chame Add antes de iniciar cada goroutine, defer wg.Done() dentro dela e Wait onde você precisa de tudo terminado.
Devo passar um WaitGroup por valor ou por ponteiro?
Por ponteiro (*sync.WaitGroup), ou deixe as goroutines o capturarem em uma closure. Uma cópia tem o seu próprio contador, então o Done na cópia nunca chega ao original e o Wait bloqueia para sempre. O go vet reporta o erro como "passes lock by value".
O que causa "sync: negative WaitGroup counter"?
Mais chamadas de Done do que de Add. Normalmente uma goroutine chama Done duas vezes (uma com defer e outra explicitamente), ou o Add(1) é pulado em algum caminho. O programa causa panic, já que o contador não consegue mais dizer nada verdadeiro.
Como obter erros de goroutines iniciadas com um WaitGroup?
Um WaitGroup não carrega resultados nem erros. Dê a cada goroutine a sua própria posição em um slice de erros e combine-os depois do Wait (por exemplo com errors.Join), ou use golang.org/x/sync/errgroup, cujo Wait devolve o primeiro erro e pode cancelar as outras por meio de um context.