Menu

WaitGroup em Golang: Add, Done, Wait e um worker pool

Como o sync.WaitGroup espera um conjunto de goroutines terminar: as regras de Add, Done e Wait, por que ele precisa ser passado por ponteiro, como coletar resultados e erros e um worker pool construído com ele.

Esta página tem editores executáveis - edite, execute e veja a saída na hora.

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 main precisa estar recebendo resultados enquanto os workers executam. Se o main chamasse wg.Wait() diretamente antes de ler, os workers ficariam bloqueados enviando para results, nunca chegariam ao Done, e tudo entraria em deadlock. É por isso que o Wait executa na sua própria goroutine.
  • O close(results) só acontece depois do Wait, 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

NecessidadeUse
Esperar N goroutines, resultados em posições indexadassync.WaitGroup
Esperar uma goroutineum channel done ou o próprio channel de resultado
Resultados enviados conforme terminamum channel, fechado depois do wg.Wait()
Parar tudo no primeiro erroerrgroup.WithContext
Parar tudo em um timeout ou cancelamento de quem chamacontext.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. O Wait pode retornar antes de ele executar.
  • Esquecer o Done em um retorno antecipado. Sempre use defer wg.Done().
  • Passar o WaitGroup por valor. Use um ponteiro; o go vet aponta a cópia.
  • Esperar na mesma goroutine que precisa esvaziar um channel. Mova o wg.Wait() e o close para uma goroutine separada.
  • Reutilizar um WaitGroup antes de o Wait anterior ter retornado. Comece um novo ciclo de chamadas Add só depois de o Wait terminar.

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.

Coddy programming languages illustration

Aprenda a programar com o Coddy

COMEÇAR