Entrevista Go: encontre vazamentos de goroutines e explique o cancelamento
Quick Overview
Resolva um exercício original de importação em memória: o consumidor retorna no primeiro erro e abandona um envio. Compare o código defeituoso e o corrigido, reproduza dois testes de regressão e explique context, fechamento de channels, buffer e limites do race detector.
Em uma entrevista Go, encontrar uma goroutine vazando exige explicar quem deixou de progredir e por quê. “Faltou usar context” não basta quando o programa já recebe um contexto, mas continua preso em uma operação que nunca observa o cancelamento. Antes de acrescentar workers, siga o trabalho até o encerramento de cada goroutine criada.
Este guia usa um importador fictício de preços para acompanhar uma falha de envio, corrigir seu ciclo de vida e verificar a saída do produtor. Para praticar a explicação em voz alta, comece por Review concurrent code quality e diga qual operação pode bloquear, quem a desbloqueia e quem possui cada recurso.
Limite da evidência: as regras citadas vêm da documentação oficial de Go. O código, a entrada e os testes são exercícios originais, não questões vazadas nem relatos de candidatos. As recomendações de preparação são nossa interpretação. Os exemplos foram verificados localmente com Go 1.27.1 em macOS ARM64; não representam um endpoint HTTP ou uma importação em banco implementados.

O contrato do importador antes de olhar o código
Imagine um serviço que recebe linhas de preços em centavos. Um produtor converte cada string para inteiro e envia um resultado com número da linha, preço e erro. O consumidor interrompe a validação ao encontrar o primeiro erro. Neste exercício, o produtor possui o channel de saída e deve encerrá-lo quando terminar, inclusive quando houver cancelamento.
O conjunto "bad", "250", "300" é pequeno de propósito. A primeira linha é inválida; as outras continuam disponíveis na entrada. Nenhum preço é gravado. Converter uma string em inteiro também não resolve toda a validação de negócio: aceitar preço negativo, limitar valores ou validar moeda exigiria regras adicionais. Não invente essas regras durante o diagnóstico do bloqueio.
Verifique duas condições: o consumidor parou e o produtor encerrou seu trabalho. A primeira não garante a segunda. O artigo oficial sobre pipelines e cancelamento explica que um estágio abandonado pelo consumidor pode ficar bloqueado ao enviar. A goroutine precisa sair; a coleta de lixo não substitui esse encerramento.
Há uma precondição importante: o chamador mantém o slice e suas strings sem alterações concorrentes até o produtor terminar. O exemplo não copia a entrada. Caso outra goroutine modifique o slice enquanto ele é lido, teremos um problema adicional de acesso compartilhado, que a correção do envio não resolve.
O número da linha é importante para devolver um erro útil sem perder sua associação com a entrada. O valor zero sozinho não distingue um preço válido de uma conversão malsucedida: examine Err. Um resultado com erro pode aparecer no fluxo, e o consumidor decide interromper; o produtor corrigido não presume essa decisão. Teste essa separação consumindo todas as linhas e, depois, parando no primeiro erro. Se o contrato de negócio exigir rejeitar todo o lote, ainda será necessário desenhar uma etapa de aplicação que respeite essa regra.
Encontre o envio que perdeu seu consumidor
A versão defeituosa recebe ctx, mas não o utiliza. Este trecho é o núcleo do problema; Result contém Line, Price e Err. O channel é sem buffer e o produtor só alcança os defer depois de sair da função.
for i, row := range rows {
price, err := strconv.Atoi(row)
out <- Result{Line: i + 1, Price: price, Err: err}
}
O consumidor recebe a primeira linha, percebe o erro e retorna. Mesmo que execute cancel(), o próximo envio continua sendo um envio comum. Não existe uma regra da linguagem que injete uma exceção na goroutine ou interrompa automaticamente essa instrução porque o contexto foi cancelado.
| Passo | Consumidor | Produtor | Consequência |
|---|---|---|---|
| 1 | Espera um resultado. | Converte a linha 1 e envia seu erro. | O primeiro encontro entre envio e recebimento acontece. |
| 2 | Encontra o erro e para de receber. | Continua para a linha 2. | A decisão do consumidor ainda não encerrou o produtor. |
| 3 | Cancela o contexto. | Tenta enviar o preço 250 em out. | O envio não consulta ctx.Done(). |
| 4 | Já retornou. | Permanece sem receptor. | close(out) e o sinal de término não são alcançados. |
Essa sequência é um traço possível e suficiente para expor o defeito. Não precisamos afirmar uma ordem universal de escalonamento. O defeito está no envio que, após o consumidor sair, não possui receptor nem alternativa de encerramento.
Uma resposta oral clara seria: “O produtor pode ficar no segundo envio. Cancelar o contexto sinaliza uma intenção, mas este envio não observa o sinal. Vou tornar esse ponto de bloqueio cancelável e verificar que a goroutine realmente terminou.” Isso identifica mecanismo, correção e evidência em vez de apenas citar uma API.
Corrija o ponto de bloqueio e exponha o término
A documentação de context descreve o sinal de cancelamento em Done() e a consulta a Err(). Neste exercício, o chamador cria e cancela o contexto; o produtor o observa. O channel done separado permite testar a saída sem depender de uma contagem global de goroutines.
package importcheck
import (
"context"
"strconv"
)
type Result struct {
Line int
Price int
Err error
}
func Stream(ctx context.Context, rows []string) (<-chan Result, <-chan struct{}) {
out := make(chan Result)
done := make(chan struct{})
go func() {
defer close(done)
defer close(out)
for i, row := range rows {
if ctx.Err() != nil {
return
}
price, err := strconv.Atoi(row)
result := Result{Line: i + 1, Price: price, Err: err}
select {
case <-ctx.Done():
return
case out <- result:
}
}
}()
return out, done
}
O select permite abandonar um envio que não consegue progredir quando o cancelamento chega. A verificação anterior evita iniciar a próxima conversão quando o cancelamento já é observado. Não atribua a ela uma garantia mais forte: o contexto pode ser cancelado depois dessa consulta, enquanto a conversão ou o envio está prestes a ocorrer.
A especificação de Go sobre select não dá prioridade ao caso de cancelamento quando mais de um caso pode prosseguir. Se um receptor estiver pronto ao mesmo tempo que ctx.Done(), um resultado ainda pode ser enviado. O contrato é encerramento cooperativo, não “zero resultados após o instante exato do cancelamento”.
A conversão usa strconv.Atoi, que devolve um inteiro e um erro. Ela é uma operação síncrona e não recebe contexto. Neste exemplo pequeno, termina normalmente antes de observarmos novamente o cancelamento. Se a etapa fosse uma chamada de rede ou leitura longa, seria necessário examinar a própria operação e sua API de interrupção; envolver tudo em uma goroutine não basta.
O consumidor também precisa cumprir sua parte
Um consumidor que retorna cedo deve sinalizar a interrupção. Uma forma de organizar o uso é criar o contexto filho com context.WithCancel, garantir defer cancel() e cancelar explicitamente ao encontrar uma linha inválida. Depois, aguarde done se o contrato da operação exigir que não reste trabalho associado antes do retorno.
Esperar done enquanto ninguém consome out, sem cancelar, recria uma dependência circular: o produtor espera enviar e o consumidor espera que o produtor termine. A ordem importa. Primeiro permita a saída por cancelamento; depois espere a confirmação. Se escolher consumir todos os resultados, o fechamento normal também permitirá concluir.
Na função mostrada, o produtor fecha seus dois channels. Os defer executam em ordem inversa: out fecha antes de done. Assim, observar done fechado permite saber que a função produtora saiu e seu fechamento de saída já ocorreu. O consumidor recebe channels somente de leitura, o que comunica a direção de uso na assinatura.
Não feche out pelo lado do consumidor para “desbloquear” a goroutine. O produtor ainda pode tentar enviar, e enviar para um channel fechado provoca panic. Com vários produtores, o desenho de fechamento precisaria de coordenação adicional para aguardar todos os envios. Essa arquitetura não é necessária no exemplo de produtor único e não deve ser acrescentada sem motivo.

Faça o teste falhar na versão antiga
Um teste útil deve deixar de consumir no ponto que expõe o defeito. Se o teste sempre drena a saída antes de conferir o término, até a versão antiga poderá passar. O caminho crítico abaixo recebe somente o primeiro erro, cancela e aguarda o sinal do produtor sem continuar recebendo resultados.
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
out, done := Stream(ctx, []string{"bad", "250", "300"})
first := <-out
if first.Line != 1 || first.Err == nil {
t.Fatal("expected invalid first row")
}
cancel()
select {
case <-done:
case <-time.After(time.Second):
t.Fatal("producer did not exit after cancellation")
}
Esse trecho pertence a um teste: requer testing, time e a preparação de limpeza. Ao testar deliberadamente a implementação antiga, o conjunto completo drena out apenas no cleanup, depois da verificação, para não deixar a goroutine defeituosa viva no processo de testes. Essa limpeza não pode ocorrer antes, pois esconderia justamente o envio abandonado.
A suíte original executou dois testes de regressão contra a versão antiga: interrupção após a linha inválida e cancelamento sem consumidor. Ambos falharam na espera pelo término. A versão corrigida passou sete testes, incluindo entrada vazia, todas as linhas válidas, contexto já cancelado, erro reportado na segunda linha e cem iterações de cancelamento concorrente.
O timeout de um segundo é um limite de segurança do teste local, não um requisito de latência de produção. Ele evita que uma regressão trave a suíte indefinidamente. A implementação revela a ausência de saída no envio; o teste reproduz a falha dentro do limite configurado. Essa execução não estabelece um limite universal de escalonamento.
Baixe os arquivos completos do exercício original, extraia o ZIP e execute go test -race -count=1 -v na pasta do módulo. A variante antiga do exercício é selecionada com PRACHUB_OLD=1 nos dois testes de regressão. Essa seleção existe somente no harness; o código corrigido não depende dessa variável de ambiente.
Buffer, data race e cancelamento são perguntas diferentes
Adicionar buffer de capacidade um pode permitir que a segunda linha seja enviada. A terceira pode então bloquear. Aumentar a capacidade até caber toda a entrada finita evita esse bloqueio específico, mas não demonstra cancelamento nem resolve um fluxo cujo tamanho não é conhecido. Defina o orçamento de memória e o contrato antes de tratar buffer como solução.
Backpressure também não é sinônimo de defeito. Se o consumidor continua ativo, um envio aguardando pode limitar naturalmente a velocidade do produtor. O problema do caso é a ausência de um receptor futuro e de uma saída alternativa. Na entrevista, explique essa diferença para não recomendar uma fila ilimitada como resposta automática a qualquer espera.
Uma data race é outra categoria de problema. O detector de races de Go observa acessos conflitantes nos caminhos executados. A suíte corrigida passou com -race, mas isso não prova que todos os escalonamentos possíveis estejam livres de defeitos. Também não transforma o detector em uma verificação geral de vazamento ou deadlock.
Se uma importação real gravasse resultados antes de cancelar, seria preciso discutir efeitos parciais, transações e repetição segura. Aqui não existe banco, commit ou endpoint HTTP. Encerrar a goroutine não desfaz efeitos externos. Manter essa fronteira explícita é parte de uma boa resposta técnica, principalmente quando o entrevistador amplia o caso.
Perguntas para aprofundar a explicação
Use estes destinos públicos do PracHub como exercícios adicionais. São uma seleção editorial, não um programa oficial de entrevista Go. Reescrever uma solução em Go exige preservar seu contrato; trocar a linguagem não elimina problemas de propriedade, concorrência ou término.
| Pergunta completa | Foco do ensaio |
|---|---|
| Review concurrent code quality | Identifique operações bloqueantes e a saída de cada goroutine. |
| Debug a Concurrent Job Scheduler | Desenhe dependências de progresso antes de modificar sincronização. |
| Make a Web Crawler Concurrent with Thread-Safe Visited State | Separe segurança do estado compartilhado de encerramento do trabalho. |
| Build an API aggregator with concurrency and retries | Propague prazo e cancelamento às operações que fazem I/O. |
| Stream and Cancel Agent Responses Without Unsafe Retries | Diferencie parar o fluxo, confirmar término e repetir efeitos com segurança. |
Ensaie três mudanças: o consumidor não lê nada, lê tudo ou retorna depois do primeiro erro. Para cada uma, explique quem cancela, quem fecha e qual observação confirma o encerramento. Acrescente depois uma operação externa e diga qual garantia deixou de estar demonstrada pelo exemplo em memória.
Continue com Debug a Concurrent Job Scheduler. Desenhe primeiro o caminho de espera, depois proponha uma alteração pequena e um teste que falhe antes dela. Termine apontando uma propriedade que os testes executados ainda não provam.
Comments (0)