Deadlock em Go: como acontece, como ler e como prevenir
Veja por que goroutines travam em espera circular, quais erros de revisão causam isso, como ler a mensagem fatal do Go e como prevenir.
intermediárioPublicada em · 26 min de leitura
Imagine dois motoristas num beco estreito de mão única, vindo de lados opostos. Nenhum dos dois quer dar ré, porque o outro também não dá, e os dois ficam ali, educados e imóveis, esperando. Cada um, isoladamente, está fazendo a coisa certa. O problema está no conjunto.
Em Go, o beco é um canal ou um mutex, e a educação dos motoristas é o código que espera sem prazo. é exatamente isso: um grupo de goroutines que espera umas pelas outras, de um jeito que nenhuma consegue avançar. Quando todas as goroutines do programa entram nesse estado, o runtime percebe, encerra o processo e imprime fatal error: all goroutines are asleep - deadlock!.
Esta aula cobre o conceito, as quatro condições que o tornam possível (sem recitar a teoria), os padrões de erro que aparecem em revisão de código, a mensagem do runtime, como achar a goroutine presa e como escrever código que não entra nesse estado. Todos os exemplos foram executados com Go 1.27.2 em macOS (arm64). As saídas coladas aqui são as que apareceram, com os caminhos de arquivo encurtados.
Mapa da aula
| Parte | Seções | O que você vai aprender |
|---|---|---|
| 1. Conceito e motivo | 1 a 3 | O que é a , por que o Go derruba o programa e como as quatro condições aparecem em código |
| 2. Padrões de erro | 4 | Canal sem receptor, locks em ordem inversa, WaitGroup sem Done, espera circular entre canais e lock duplo |
| 3. A mensagem do runtime | 5 | Como o runtime decide que travou, por que não é um e quando o programa continua vivo, travado |
| 4. Diagnóstico | 6 | Dump de goroutines com e perfil com |
| 5. Prevenção | 7 | Ordem fixa de locks, timeout com context, buffer com critério, defer e WaitGroup.Go |
| 6. Prática e referência | 8 a 11 | Exercícios com resposta de referência, o que não foi confirmado, glossário, fontes e relacionados |
Pré-requisitos: Goroutine (como lançar trabalho concorrente), Channels (envio e recebimento que bloqueiam) e sync (Mutex e WaitGroup). O WaitGroup tem a sua própria aula em waitGroup. A aula sobre Race Condition trata do problema vizinho, o acesso a dados compartilhados sem proteção, que costuma levar a um lock mal usado.
1. O que é um deadlock
Bloquear, em Go, é normal. Quando você lê de um canal vazio, a goroutine para e espera. Quando tenta pegar um mutex que outra goroutine já segura, ela para e espera. Nada disso é erro por si só, porque alguém deve enviar ou liberar logo em seguida. O deadlock começa quando quem deveria liberar também está parado, esperando algo de alguém que também está parado.
A forma mais útil de pensar nisso é a espera circular. Se a goroutine A espera a B, a B espera a A, e nenhuma das duas tem como sair sozinha, então as duas ficam presas para sempre. Não importa quantas goroutines existam no programa: se o ciclo se fecha e as demais também estão paradas, o programa inteiro para.
Vale notar que um deadlock não gasta CPU. As goroutines ficam estacionadas pelo runtime, sem girar em laço. É isso que torna o problema traiçoeiro em produção: o processo continua de pé, o uso de CPU fica perto de zero e a requisição simplesmente nunca volta. Quem olha só o consumo de recursos não vê nada de errado.
2. Por que o Go derruba o programa em vez de esperar
Em linguagens em que locks e threads são a única ferramenta, um deadlock congela o programa sem aviso. Se ele estiver rodando em produção, a primeira pista costuma ser um usuário reclamando de lentidão, horas depois. Uma falha silenciosa costuma ser mais difícil de diagnosticar do que uma falha ruidosa.
O runtime do Go enxerga todas as goroutines ao mesmo tempo. Quando nenhuma delas pode mais avançar e não há nada que possa acordá-las, esperar não serve para nada. Por isso o Go escolhe falhar alto: encerra o processo e diz o que aconteceu. A detecção não precisa ser ligada por você, ela faz parte do runtime.
Existe um limite importante. A detecção só acontece quando o programa inteiro parou. Se uma parte continua rodando, o runtime não tem motivo para reclamar, e a goroutine presa vira um vazamento silencioso. A seção 5 mostra exemplos disso.
3. As quatro condições, na prática
Em 1971, Coffman, Elphick e Shoshani descreveram quatro condições que, juntas, tornam o deadlock possível em sistemas com recursos compartilhados. A teoria serve para raciocinar sobre um bug, não para decorar. Cada condição corresponde a algo que o código faz ou deixa de fazer, e é por isso que a prevenção da seção 7 funciona.
As quatro precisam estar presentes ao mesmo tempo. Se uma faltar, o deadlock não acontece. Mas a presença das quatro não garante o deadlock, porque ele depende da ordem em que o escalonador intercala as goroutines. Alguns exemplos desta aula usam time.Sleep justamente para forçar essa intercalação e tornar o erro reproduzível.
Um recurso só pode ser usado por uma goroutine por vez. Um sync.Mutex existe para isso. Um canal sem buffer também tem essa propriedade, já que cada envio se encontra com um único recebimento. Sem exclusão mútua não há disputa, e sem disputa não há deadlock. O problema é que a exclusão protege os dados, então você não deve removê-la. O que se pode mudar é o restante.
A goroutine segura um recurso enquanto pede outro. Na figura 1, a goroutine A segura o lock A e fica esperando o lock B. Ela não solta o A para tentar de novo mais tarde. Segura e espera, e a outra goroutine faz exatamente o mesmo.
O recurso só sai das mãos de quem o segura quando essa pessoa o libera. O scheduler do Go consegue tirar uma goroutine da CPU para dar a vez a outra, mas isso não tira um lock de ninguém. Um Unlock só acontece porque o código o escreveu. Por isso o runtime não resolve sozinho um lock que nunca é liberado.
Espera circular
A última condição fecha o ciclo: A espera B, e B espera A. Ela é a mais fácil de quebrar no código, porque depende apenas da ordem em que os recursos são pedidos.
A figura 1 mostra o mesmo ciclo visto de cima. As setas contínuas são posse, ou seja, qual lock cada goroutine segura. As tracejadas em rosa são espera, e o símbolo de ✕ marca o ponto onde a espera bloqueia. Repare que cada goroutine está parada em um único lock. A parada é do conjunto: basta uma das quatro setas mudar de direção para o ciclo se desfazer.
4. Padrões de erro que aparecem em código real
Os exemplos a seguir são os que mais aparecem em revisão de código. Todos rodam sozinhos e as saídas são as que o programa imprimiu.
Canal sem receptor
Um canal sem buffer só completa um envio quando alguém recebe do outro lado no mesmo instante. É como entregar uma encomenda que só pode ser recebida pelo destinatário em mãos: se ele não está em casa, o entregador fica na porta para sempre. O código abaixo tenta enviar sem ninguém para ler.
package main
import "fmt"
func main() {
ch := make(chan int)
fmt.Println("antes do envio")
ch <- 1
fmt.Println("depois do envio")
}
Saída (código de saída 2):
antes do envio
fatal error: all goroutines are asleep - deadlock!
goroutine 1 [chan send]:
main.main()
…/01canal/main.go:8 +0x70
Repare no motivo entre colchetes: [chan send] diz que a goroutine estava parada tentando enviar. O mesmo problema existe no lado do recebimento, e a saída fica igual, trocando o motivo para [chan receive]:
package main
import "fmt"
func main() {
ch := make(chan int)
fmt.Println("antes da leitura")
x := <-ch
fmt.Println("recebi", x)
}
A saída traz goroutine 1 [chan receive]: no mesmo formato. Em revisão, esses dois motivos são os primeiros a procurar quando um programa cai com essa mensagem.
A figura 2 põe os dois casos lado a lado com o mesmo canal. Na linha de cima, a seta rosa termina num ✕: o envio não tem para onde ir. Na linha de baixo, uma goroutine está lendo, e os pacotes verdes mostram o valor atravessando o canal. A diferença não está no canal, e sim na existência de quem recebe no mesmo momento.
Dois mutex em ordem inversa
Dois cozinheiros dividem uma faca e um ralador. Um pega a faca e pede o ralador; o outro pega o ralador e pede a faca. Nenhum deles larga o que tem enquanto espera, e a cozinha para. É a espera circular com locks. O programa abaixo faz isso com duas goroutines.
package main
import (
"fmt"
"sync"
"time"
)
func main() {
var muA, muB sync.Mutex
var wg sync.WaitGroup
wg.Add(2)
go func() {
defer wg.Done()
muA.Lock()
fmt.Println("g1 pegou A")
time.Sleep(100 * time.Millisecond)
muB.Lock()
fmt.Println("g1 pegou B")
muB.Unlock()
muA.Unlock()
}()
go func() {
defer wg.Done()
muB.Lock()
fmt.Println("g2 pegou B")
time.Sleep(100 * time.Millisecond)
muA.Lock()
fmt.Println("g2 pegou A")
muA.Unlock()
muB.Unlock()
}()
wg.Wait()
fmt.Println("fim")
}
Saída (trecho, com os frames do runtime removidos):
g2 pegou B
g1 pegou A
fatal error: all goroutines are asleep - deadlock!
goroutine 1 [sync.WaitGroup.Wait]:
main.main()
…/03mutex/main.go:33
goroutine 19 [sync.Mutex.Lock]:
main.main.func1()
…/03mutex/main.go:18
goroutine 20 [sync.Mutex.Lock]:
main.main.func2()
…/03mutex/main.go:28
Os time.Sleep de 100 ms servem para forçar a intercalação: a primeira goroutine pega A, a segunda pega B, e cada uma tenta o lock que a outra tem. Sem o sleep, uma goroutine costuma terminar antes de a outra começar e o programa passa. Esse é o motivo de o erro aparecer em produção e sumir no teste: a janela de intercalação é um acaso.
A correção é simples e aparece no código abaixo. A única mudança está na segunda goroutine, que passa a pegar A antes de B, como a primeira.
go func() {
defer wg.Done()
muA.Lock()
fmt.Println("g2 pegou A")
time.Sleep(100 * time.Millisecond)
muB.Lock()
fmt.Println("g2 pegou B")
muB.Unlock()
muA.Unlock()
}()
Com essa troca, o mesmo programa termina e imprime fim. A ordem das duas primeiras linhas pode mudar de uma execução para outra; o que não muda é o fato de o programa terminar.
WaitGroup sem Done
Um WaitGroup funciona como uma lista de presença com contador: cada Add soma uma pessoa, cada Done tira uma, e o Wait só libera quando o contador chega a zero. Se alguém vai embora sem avisar na portaria, o contador nunca zera e a festa não acaba.
package main
import (
"fmt"
"sync"
)
func main() {
var wg sync.WaitGroup
wg.Add(1)
go func() {
fmt.Println("trabalho feito")
// esqueceu wg.Done()
}()
wg.Wait()
fmt.Println("fim")
}
Saída:
trabalho feito
fatal error: all goroutines are asleep - deadlock!
goroutine 1 [sync.WaitGroup.Wait]:
main.main()
…/05wg/main.go:15
Uma variação traiçoeira é passar o WaitGroup por valor. A goroutine recebe uma cópia, chama Done na cópia, e o contador original continua em um. O go vet avisa sobre isso antes de você rodar, e o aviso merece ser levado a sério:
package main
import (
"fmt"
"sync"
)
func trabalho(wg sync.WaitGroup) { // wg passado por valor: é uma cópia
fmt.Println("trabalho feito")
wg.Done()
}
func main() {
var wg sync.WaitGroup
wg.Add(1)
go trabalho(wg)
wg.Wait()
fmt.Println("fim")
}
Saída do go vet, antes da execução:
trabalho passes lock by value: sync.WaitGroup contains sync.noCopy
call of trabalho copies lock value: sync.WaitGroup contains sync.noCopy
Saída da execução:
trabalho feito
fatal error: all goroutines are asleep - deadlock!
goroutine 1 [sync.WaitGroup.Wait]:
main.main()
…/06wgcopia/main.go:17
O detalhe de como o WaitGroup guarda o contador está na aula waitGroup.
Espera circular entre canais
Não é preciso mutex para formar um ciclo. Duas goroutines podem esperar uma pela outra usando canais, como duas pessoas parando numa porta, cada uma dizendo “depois de você”.
package main
import "fmt"
func main() {
a := make(chan struct{})
b := make(chan struct{})
go func() {
<-a // espera a main liberar A
b <- struct{}{}
}()
<-b // main espera a goroutine liberar B
a <- struct{}{}
fmt.Println("fim")
}
Saída:
fatal error: all goroutines are asleep - deadlock!
goroutine 1 [chan receive]:
main.main()
…/07circular/main.go:12
goroutine 19 [chan receive]:
main.main.func1()
…/07circular/main.go:9
A main espera B. A goroutine espera A. Cada uma só envia depois de receber, então nenhuma envia primeiro. O ciclo tem dois elos, mas poderia ter cinco: o que conta é que ele se feche.
Lock duplo na mesma goroutine
O sync.Mutex do Go não é reentrante. Ele se parece com um banheiro trancado: a mesma pessoa que já está dentro não consegue entrar de novo com a mesma chave, e fica do lado de fora da própria porta. Em código, o erro costuma aparecer quando uma função chama outra que também pega o mesmo lock.
package main
import (
"fmt"
"sync"
)
func main() {
var mu sync.Mutex
mu.Lock()
fmt.Println("primeiro lock ok")
mu.Lock()
fmt.Println("segundo lock ok")
mu.Unlock()
mu.Unlock()
}
Saída:
primeiro lock ok
fatal error: all goroutines are asleep - deadlock!
goroutine 1 [sync.Mutex.Lock]:
main.main()
…/08lockduplo/main.go:12
5. A mensagem do runtime
Como o Go decide que travou
A decisão está na função checkdead, em runtime/proc.go do código-fonte do Go 1.27.2. Em resumo, o runtime conta as threads do sistema operacional que não estão ociosas. Se alguma estiver executando uma goroutine ou esperando numa chamada de sistema, o runtime não conclui que o programa travou. Se todas estiverem ociosas, o runtime procura timers pendentes em cada processador lógico. Só quando não há thread ocupada e nenhum pendente é que ele chama fatal("all goroutines are asleep - deadlock!").
A figura 3 segue essa ordem. A goroutine bloqueia, e a primeira pergunta é se alguma thread ainda está ocupada. Um sim à direita significa que o programa segue, porque outra goroutine pode rodar. A segunda pergunta é sobre timers: time.Sleep, time.After e tickers contam como timers pendentes, e um timer pendente garante que alguma coisa ainda vai acordar. Só o caminho de baixo, com dois nãos seguidos, termina no em rosa.
Por que é fatal e não panic
A mensagem diz fatal error, não panic. A diferença importa. Um panic pode ser capturado com recover; um fatal error não. Para confirmar, o programa abaixo coloca um defer com recover na main e depois trava:
package main
import "fmt"
func main() {
defer func() {
r := recover()
fmt.Println("defer rodou, recover devolveu:", r)
}()
ch := make(chan int)
<-ch
}
Saída:
fatal error: all goroutines are asleep - deadlock!
goroutine 1 [chan receive]:
main.main()
…/18recover/main.go:11
O defer não roda, e a mensagem defer rodou nunca aparece. O processo termina com código de saída 2. O mesmo vale para o erro vizinho fatal error: sync: unlock of unlocked mutex, que acontece quando você chama Unlock num mutex já livre. Ele também não é deadlock, mas segue a mesma regra: encerra o processo sem chance de recuperação.
Quando o programa continua vivo, travado
Como o diagnóstico depende de não haver nenhum timer pendente, um laço com time.Sleep basta para esconder um deadlock. O programa abaixo deixa a main presa num canal, mas uma goroutine acorda a cada segundo.
package main
import (
"fmt"
"time"
)
func main() {
ch := make(chan int)
go func() {
for { // um timer sempre pendente: o runtime nunca conclui que tudo travou
time.Sleep(time.Second)
}
}()
fmt.Println("main esperando para sempre")
<-ch
}
Depois de três segundos, o processo ainda estava vivo, sem nenhuma mensagem de erro além da primeira linha. Ele só saiu porque o experimento o encerrou. Esse caso é comum em código real: um laço de monitoramento ou de retentativa mantém o processo em pé e tapa o deadlock da main.
Uma leitura de entrada também pode atrasar o diagnóstico, porque a goroutine bloqueada numa chamada de sistema ocupa uma thread. O exemplo abaixo lê da entrada padrão e depois trava a main. Ele foi executado com a entrada aberta por três segundos ((sleep 3) | ./programa):
package main
import (
"bufio"
"fmt"
"os"
)
func main() {
go func() {
bufio.NewReader(os.Stdin).ReadString('\n') // bloqueado numa syscall
fmt.Println("leitura terminou")
}()
ch := make(chan int)
<-ch
}
Saída, em 3 segundos:
leitura terminou
fatal error: all goroutines are asleep - deadlock!
goroutine 1 [chan receive]:
main.main()
…/14stdin/main.go:15
A mensagem só saiu quando a entrada fechou. Antes disso, o runtime enxergava uma thread ocupada e não concluía nada.
6. Como encontrar a goroutine travada
Quando o programa trava sem cair, você precisa ver o estado de cada goroutine. Há dois caminhos práticos, e eles se complementam.
Dump de stacks com SIGQUIT
O sinal SIGQUIT pede ao Go que imprima o estado de todas as goroutines na saída de erro e depois encerre o processo. No terminal, Ctrl+\ envia esse sinal. Para um processo que você não controla no terminal, use kill -QUIT <pid>. O programa abaixo tem um lock que nunca é liberado e um canal que nunca recebe, com a main dormindo num laço de time.Sleep, para que o runtime não reclame.
package main
import (
"fmt"
"sync"
"time"
)
func main() {
var mu sync.Mutex
mu.Lock()
go func() {
fmt.Println("worker: tentando pegar o lock")
mu.Lock() // nunca solta
}()
go func() {
ch := make(chan int)
<-ch // parada num canal
}()
fmt.Println("main rodando, com um timer ativo")
for {
time.Sleep(time.Second)
}
}
Depois de 70 segundos, enviei kill -QUIT ao processo. Trecho da saída:
SIGQUIT: quit
PC=… m=0 sigcode=0
goroutine 1 gp=… m=nil [sleep]:
main.main()
…/12sigquit/main.go:22
goroutine 35 gp=… m=nil [sync.Mutex.Lock]:
main.main.func1()
…/12sigquit/main.go:14
goroutine 36 gp=… m=nil [chan receive]:
main.main.func2()
…/12sigquit/main.go:18
O dump completo traz também goroutines do próprio runtime, como as que cuidam do coletor de lixo. Você pode ignorá-las. As que interessam são as que têm main. no nome, e o motivo entre colchetes diz onde cada uma parou. O motivo [sleep] é da main, que está no laço de espera.
Repare que não aparece nenhum , N minutes ao lado dos motivos. O runtime só registra há quanto tempo uma goroutine está parada quando o coletor de lixo passa por ela (runtime/mgcmark.go). Nos 70 segundos deste experimento nenhum ciclo de coleta cobriu essas goroutines, então a anotação não saiu. Quando ela aparece, [chan receive, 5 minutes] indica uma espera longa, que merece atenção.
Perfil de goroutines com pprof
O pacote runtime/pprof gera o perfil dentro do próprio programa. O exemplo abaixo cria três goroutines presas num lock e despeja o perfil de goroutines no formato de texto:
package main
import (
"fmt"
"os"
"runtime/pprof"
"sync"
"time"
)
func main() {
var mu sync.Mutex
mu.Lock()
for i := 0; i < 3; i++ {
go func() {
mu.Lock() // nunca consegue
mu.Unlock()
}()
}
time.Sleep(100 * time.Millisecond)
fmt.Println("goroutines paradas, despejando perfil:")
pprof.Lookup("goroutine").WriteTo(os.Stdout, 1)
}
Saída (trecho, caminhos encurtados e endereços removidos):
goroutines paradas, despejando perfil:
goroutine profile: total 4
3 @ …
# internal/sync.runtime_SemacquireMutex+0x27 …/runtime/sema.go:95
# internal/sync.(*Mutex).lockSlow+0x173 …/internal/sync/mutex.go:149
# sync.(*Mutex).Lock+0x1c …/sync/mutex.go:46
# main.main.func1+0x20 …/13pprof/main.go:16
O número antes do @ conta quantas goroutines têm exatamente aquela pilha. O 3 @ diz que três goroutines estão no mesmo lock, o que é a assinatura de : muitas goroutines disputando um recurso.
Para um servidor, o pprof funciona pela rede. Basta importar net/http/pprof com o import em branco, como no exemplo a seguir, e o endpoint /debug/pprof/goroutine passa a responder. O programa de teste escuta em localhost:6161 e tem o mesmo lock preso.
package main
import (
"fmt"
"net/http"
_ "net/http/pprof"
"sync"
"time"
)
func main() {
go func() {
fmt.Println(http.ListenAndServe("localhost:6161", nil))
}()
var mu sync.Mutex
mu.Lock()
for i := 0; i < 3; i++ {
go func() {
mu.Lock()
mu.Unlock()
}()
}
time.Sleep(60 * time.Second)
}
Com o programa rodando, go tool pprof -top consulta o endpoint e imprime uma tabela. Trecho da saída:
Fetching profile over HTTP from http://localhost:6161/debug/pprof/goroutine
File: 17pprofhttp
Type: goroutine
Showing nodes accounting for 6, 85.71% of 7 total
flat flat% sum% cum cum%
5 71.43% 71.43% 5 71.43% runtime.gopark
1 14.29% 85.71% 1 14.29% runtime.goroutineProfileWithLabels
0 0% 85.71% 3 42.86% internal/sync.(*Mutex).Lock
0 0% 85.71% 3 42.86% internal/sync.(*Mutex).lockSlow
0 0% 85.71% 3 42.86% internal/sync.runtime_SemacquireMutex
0 0% 85.71% 3 42.86% main.main.func2
0 0% 85.71% 1 14.29% net.(*TCPListener).Accept
A coluna cum mostra quantas goroutines passam por aquela função. Três em sync.(*Mutex).Lock apontam para o gargalo sem precisar ler stack por stack. A aula internals/PProf explica o pprof de CPU e de memória em detalhe; aqui usamos apenas o perfil de goroutines.
A figura 4 resume os dois caminhos a partir do mesmo ponto, um programa travado sem erro. O ramo de cima usa o sinal SIGQUIT, que gera o dump com um estado por goroutine. O ramo de baixo usa o endpoint do pprof, que devolve um perfil agregado. O primeiro serve para ver uma goroutine específica; o segundo, para ver a forma do problema quando há centenas delas.
7. Como prevenir
Cada prática abaixo quebra uma das quatro condições da seção 3 ou dá uma saída para uma espera que não termina.
Ordem fixa de locks
Quando todo o código pega os locks na mesma ordem, o ciclo da figura 1 não pode se formar. A regra é escrever a ordem em algum lugar que todos vejam e segui-la sem exceção. A versão corrigida do exemplo de cozinheiros, em que as duas goroutines pegam A antes de B, terminou sem erro (seção 4). Num sistema com muitos locks, a ordem costuma ser um critério fixo, como o id do recurso. O exercício 2 da seção 8 mostra esse critério numa transferência bancária.
O cuidado é que a ordem pode ficar escondida em chamadas de função. Se transfere(a, b) pega A e B, e outra parte do código pega B e chama outra função que pega A, o ciclo pode aparecer sem nenhuma linha óbvia. Por isso vale documentar a ordem no próprio tipo.
Defer logo depois do Lock
defer mu.Unlock() na linha seguinte ao Lock garante a liberação em todos os caminhos de saída, inclusive em retornos antecipados. Esquecer o Unlock não é erro de compilação, e o Go não avisa. Nenhum dos checadores padrão encontra um lock que nunca é liberado: o -race procura corrida de dados, e o go vet encontrou a cópia do WaitGroup na seção 4, mas não a ordem dos locks.
Timeout com context
Um select com ctx.Done() dá uma saída para a espera. O exemplo abaixo ninguém vai alimentar o canal, e a função devolve um erro quando o prazo termina:
package main
import (
"context"
"fmt"
"time"
)
func consulta(ctx context.Context, ch <-chan string) (string, error) {
select {
case v := <-ch:
return v, nil
case <-ctx.Done():
return "", ctx.Err()
}
}
func main() {
ch := make(chan string) // ninguém vai enviar
ctx, cancel := context.WithTimeout(context.Background(), 200*time.Millisecond)
defer cancel()
v, err := consulta(ctx, ch)
fmt.Printf("valor=%q erro=%v\n", v, err)
}
Saída:
valor="" erro=context deadline exceeded
O timeout não corrige a causa, que continua sendo um envio que ninguém faz. Ele transforma um congelamento silencioso em um erro que o chamador consegue tratar, registrar e devolver. A aula Context mostra como propagar o cancelamento por uma cadeia de chamadas.
O sync.Mutex também tem TryLock, que tenta pegar o lock sem bloquear. A documentação do pacote observa que os usos corretos são raros, e ele só resolve o caso de quem pode desistir e tentar de novo. Para a maioria dos locks, a ordem fixa é a solução mais simples.
Buffer, com critério
Um canal com buffer deixa um envio completar sem recebimento imediato, o que resolve o exemplo de canal sem receptor quando a mesma goroutine envia e depois recebe:
package main
import "fmt"
func main() {
ch := make(chan int, 1)
ch <- 42
fmt.Println(<-ch)
}
Saída:
42
O buffer, porém, só adia o problema. Ele tem uma capacidade, e quando enche o envio seguinte trava do mesmo jeito. O programa abaixo tenta dois envios num buffer de tamanho um sem nenhum recebimento:
package main
import "fmt"
func main() {
ch := make(chan int, 1)
ch <- 1
fmt.Println("primeiro envio ok")
ch <- 2 // o buffer já está cheio e ninguém lê
fmt.Println("segundo envio ok")
}
Saída:
primeiro envio ok
fatal error: all goroutines are asleep - deadlock!
goroutine 1 [chan send]:
main.main()
…/10buffer/main.go:9
Use buffer quando você sabe quantos envios vão acontecer e conhece a capacidade necessária, como no caso de um fan-out em que cada goroutine envia exatamente uma resposta. Usar buffer como remendo para um recebimento que falta só esconde o erro até o dia em que o número de mensagens passa da capacidade.
Add antes do go e Done garantido
Chame wg.Add antes de lançar a goroutine, nunca dentro dela, e faça o Done com defer. Em Go 1.25 e versões posteriores, WaitGroup.Go combina as duas coisas: ele soma ao contador, lança a goroutine e chama Done quando a função termina. O exemplo abaixo usa essa forma e soma 1, 2 e 3 de forma protegida:
package main
import (
"fmt"
"sync"
)
func main() {
var wg sync.WaitGroup
var mu sync.Mutex
total := 0
for i := 1; i <= 3; i++ {
wg.Go(func() {
mu.Lock()
defer mu.Unlock()
total += i
})
}
wg.Wait()
fmt.Println("total:", total)
}
Saída:
total: 6
Observe que a variável i é capturada pela closure. A partir do Go 1.22 cada iteração do laço tem a sua própria variável, então o exemplo soma 1, 2 e 3. A versão exata em que WaitGroup.Go apareceu não foi reconferida nesta aula, como registrado na seção 9.
8. Exercícios
Tente resolver antes de olhar a resposta. Cada exercício tem a solução de referência, executada.
Exercício 1: o produtor que trava
O programa abaixo deveria imprimir 0, 1 e 2, mas trava. Explique por quê e corrija sem remover o buffer.
package main
import "fmt"
func produz(ch chan int) {
for i := 0; i < 3; i++ {
ch <- i
}
fmt.Println("produtor terminou")
}
func main() {
ch := make(chan int, 1)
produz(ch)
for v := range ch {
fmt.Println(v)
}
}
Resposta de referência: a função produz roda na própria goroutine main, antes do laço de leitura. O buffer tem capacidade 1, então o primeiro envio cabe e o segundo trava, porque ninguém está lendo ainda. A correção é lançar o produtor numa goroutine e fechar o canal ao final, para que o range tenha um fim:
package main
import "fmt"
func produz(ch chan int) {
defer close(ch)
for i := 0; i < 3; i++ {
ch <- i
}
}
func main() {
ch := make(chan int)
go produz(ch)
for v := range ch {
fmt.Println(v)
}
}
Saída:
0
1
2
Nesse caso, quem fecha o canal é quem envia, porque é quem sabe quando não há mais envios.
Exercício 2: transferência entre contas
Duas goroutines fazem transferências entre as contas A e B em sentidos opostos. Escreva a transferência de forma que não haja deadlock. O total de saldo precisa ser preservado.
Resposta de referência: a regra é pegar os dois locks sempre na mesma ordem global. Aqui a ordem é o id da conta, do menor para o maior, independentemente de quem é origem e de quem é destino.
package main
import (
"fmt"
"sync"
)
type Conta struct {
mu sync.Mutex
id int
saldo int
}
func transfere(de, para *Conta, valor int) {
primeiro, segundo := de, para
if para.id < de.id {
primeiro, segundo = para, de // ordem global: sempre pelo menor id
}
primeiro.mu.Lock()
defer primeiro.mu.Unlock()
segundo.mu.Lock()
defer segundo.mu.Unlock()
de.saldo -= valor
para.saldo += valor
}
func main() {
a := &Conta{id: 1, saldo: 1000}
b := &Conta{id: 2, saldo: 1000}
var wg sync.WaitGroup
for i := 0; i < 1000; i++ {
wg.Add(2)
go func() { defer wg.Done(); transfere(a, b, 1) }()
go func() { defer wg.Done(); transfere(b, a, 1) }()
}
wg.Wait()
fmt.Println("total:", a.saldo+b.saldo, "a:", a.saldo, "b:", b.saldo)
}
Saída:
total: 2000 a: 1000 b: 1000
Como foram feitas 1000 transferências em cada sentido, o saldo final de cada conta volta ao valor inicial. O que deve ficar constante em qualquer execução é o total de 2000.
Exercício 3: três réplicas e a primeira resposta
Lance três consultas simuladas com atrasos diferentes, devolva a primeira resposta e garanta que as outras não fiquem presas.
Resposta de referência: o canal tem buffer de tamanho três, igual ao número de consultas. Assim, cada goroutine consegue gravar a sua resposta e terminar, mesmo que ninguém leia depois.
package main
import (
"fmt"
"time"
)
func consulta(nome string, atraso time.Duration) string {
time.Sleep(atraso)
return nome
}
func main() {
ch := make(chan string, 3) // buffer de 3: cada réplica consegue gravar e terminar
replicas := []struct {
nome string
atraso time.Duration
}{{"r1", 300 * time.Millisecond}, {"r2", 100 * time.Millisecond}, {"r3", 500 * time.Millisecond}}
for _, r := range replicas {
go func(nome string, atraso time.Duration) {
ch <- consulta(nome, atraso)
}(r.nome, r.atraso)
}
fmt.Println("primeira resposta:", <-ch)
time.Sleep(600 * time.Millisecond) // espera as lentas terminarem
fmt.Println("fim, respostas ainda no buffer:", len(ch))
}
Saída:
primeira resposta: r2
fim, respostas ainda no buffer: 2
Sem o buffer, as respostas de r1 e r3 ficariam presas no envio para sempre, depois que a main já tivesse retornado. O time.Sleep da main existe só para mostrar que as lentas terminaram. Num programa real, você esperaria com wg.Wait ou com ctx.
Pergunta de entrevistaComo você previne deadlock em Go?
Sigo uma ordem fixa para pegar locks, e uso timeouts com select para não esperar para sempre. Garanto que quem envia para um canal tem um receptor, e que todo Lock tem seu Unlock, de preferência com defer. Para diagnosticar, leio o dump de goroutines, que mostra em que ponto cada uma está parada. Em código de aplicação, prefiro canais e filas a locks feitos à mão.
9. O que não foi confirmado
As saídas com concorrência mudam de uma execução para outra. Na rodada usada aqui, a ordem de g1 pegou A e g2 pegou B trocou em um dos exemplos. A mensagem de deadlock e os motivos entre colchetes não mudaram.
Todos os testes rodaram com Go 1.27.2 em macOS arm64. Os nomes de motivo como sync.Mutex.Lock e sync.WaitGroup.Wait podem ser diferentes em versões mais antigas, e não testei outras versões.
A anotação de minutos no dump do SIGQUIT não apareceu nos 70 segundos do experimento. Pelo código-fonte, ela depende de um ciclo do coletor de lixo. Não medi quanto tempo leva para ela aparecer num processo real.
O SIGQUIT foi testado no macOS. Não testei Linux nem Windows, e o Ctrl+\ pode se comportar de outra forma em outros terminais.
A versão em que WaitGroup.Go foi introduzida não foi reconferida nesta aula. A informação de Go 1.25 que aparece no texto veio da documentação que eu tinha consultado antes, não de uma verificação feita hoje.
A citação do artigo de 1971 foi conferida por busca, sem abrir a página da ACM. As afirmações sobre vazamento de memória causado por goroutines presas são qualitativas: não medi consumo de memória nesta aula.
10. Glossário
- deadlock: estado em que um conjunto de goroutines espera umas pelas outras e nenhuma consegue avançar.
- espera circular: a condição de deadlock em que o grafo de espera forma um ciclo.
- exclusão mútua: regra de que um recurso só é usado por uma goroutine de cada vez.
- posse e espera: uma goroutine segura um recurso enquanto pede outro.
- sem preempção: o recurso só é liberado por quem o segura, nunca tirado à força.
- fatal error: erro do runtime que encerra o processo com código 2 e que
recovernão captura. - panic: erro que pode ser capturado com
recoverdentro de umdefer. - syscall: chamada que o programa faz ao sistema operacional, como ler da entrada padrão.
- timer: agendamento de tempo, como
time.Sleep,time.Afterou um ticker, que garante que algo ainda vai acordar. - SIGQUIT: sinal que pede ao Go o dump do estado de todas as goroutines e o encerramento do processo.
- goroutine dump: lista das stacks de cada goroutine, com o motivo da espera entre colchetes.
- pprof: ferramenta de perfil do Go. Neste texto, usada para o perfil de goroutines.
- contenção: muitas goroutines disputando o mesmo recurso ao mesmo tempo.
- Mutex não reentrante: mutex que não pode ser pego de novo pela goroutine que já o tem.
11. Fontes consultadas em 2026-11-03
- Go 1.27.2, código-fonte local da toolchain (Homebrew):
runtime/proc.go(funçãocheckdead),runtime/traceback.go,runtime/mgcmark.goeruntime/extern.go(GOTRACEBACK). Consultado em 2026-11-03 a partir dego env GOROOT. - Documentação local da toolchain, consultada em 2026-11-03 com
go doc:sync.WaitGroup.Go,sync.Mutex.TryLock,context.WithTimeout,runtime/pprof.Lookupenet/http/pprof. Versões públicas: pkg.go.dev/sync, pkg.go.dev/context, pkg.go.dev/runtime/pprof e pkg.go.dev/net/http/pprof. - Wikipedia (inglês), “Deadlock (computer science)”, consultada em 2026-11-03 para a lista das quatro condições: en.wikipedia.org/wiki/Deadlock_(computer_science).
- E. G. Coffman Jr., M. J. Elphick e A. Shoshani, “System Deadlocks”, ACM Computing Surveys, vol. 3, n. 2, 1971, doi:10.1145/356586.356588. Conferido por busca em 2026-11-03, sem abrir a página da ACM.