Pular para o conteúdo principal
Tratando erros em goroutines

Tratando erros em goroutines

7 min de leitura

Arquivado emLinguagem de Programação Goem

Aprenda dois padrões para propagação de erros de goroutines concorrentes — o result struct e o error channel separado — e quando escolher cada um.

Uma goroutine não pode return um erro para quem a chamou. O go dispara e esquece: ele inicia a execução em um caminho de agendamento separado, e os valores de retorno da função são descartados. Se algo der errado dentro da goroutine, o erro não tem para onde ir — a menos que você construa um caminho explicitamente.

Isso não é um caso de borda raro. Código concorrente que busca dados, escreve em storage ou chama serviços externos precisa comunicar falhas tanto quanto código sequencial. Ignorar essa realidade leva a falhas silenciosas: a goroutine encontra um erro, engole-o, e quem chamou recebe um resultado parcial ou vazio sem nenhuma indicação de que algo deu errado.

Os dois padrões abordados aqui — o result struct e o error channel separado — são as formas padrão de resolver esse problema.

O anti-padrão: falha silenciosa

Antes de ver as soluções, vale ser concreto sobre o problema. Aqui está um código que parece razoável, mas descarta erros silenciosamente:

Se algum http.Get falhar, results[i] permanece com seu zero value — uma string vazia — e fetchAll retorna um slice com lacunas. Quem chama não tem como distinguir uma resposta vazia bem-sucedida de um erro de rede. Essa é a falha silenciosa: a função completa, o caller segue em frente, e o erro desaparece.

Resultados parciais parecem sucesso

Funções que retornam resultados parciais em caso de erro são mais difíceis de debugar do que funções que não retornam nada e apresentam um erro claro. Quem chama pode não perceber as lacunas até muito depois, ou pode agir com base na saída corrompida. Propagação explícita de erros é sempre preferível.

Padrão 1: o result struct

A solução mais limpa para a maioria dos casos é definir um struct que contém tanto o resultado quanto o erro, e então enviar valores desse tipo por um único channel. A goroutine sempre envia exatamente um valor — seja um resultado com erro nil, seja um resultado zerado com um erro não-nil — e quem chama percorre o channel para coletá-los.

O channel é buffered com len(urls), então cada goroutine pode enviar seu resultado sem bloquear, independentemente de quando as outras terminam. O loop for range urls coleta exatamente a mesma quantidade de valores que foram enviados — um por goroutine — então o channel é completamente drenado e a função retorna somente após todas as goroutines terem terminado.

Quem chama então itera sobre os resultados e trata sucessos e falhas separadamente:

É direto: um channel, um tipo, um loop. O resultado de cada goroutine é contabilizado, e nem sucesso nem falha são implícitos.

Sempre inclua contexto identificador no result

Quando múltiplas goroutines rodam concorrentemente, os resultados chegam em ordem arbitrária. Incluir a URL (ou um índice, ou um ID) no result struct permite ao caller associar cada resultado à sua entrada sem depender de ordenação.

Padrão 2: error channel separado

Uma alternativa é usar dois channels — um para resultados e outro para erros — e fazer o caller fazer select sobre ambos. Esse padrão aparece quando o tipo do resultado já está definido e você não quer envolvê-lo em um novo struct, ou quando a lógica de tratamento de erros é genuinamente separada da lógica de processamento dos resultados.

Quem chama precisa drenar os dois channels. Uma abordagem comum é coletar um número conhecido de respostas usando um contador:

O select alterna entre os dois channels, processando o que estiver pronto primeiro. Após len(urls) iterações, todas as goroutines enviaram exatamente uma mensagem para um dos dois channels, e ambos estão completamente drenados.

Cada goroutine deve enviar para exatamente um channel

No padrão de channels separados, cada goroutine deve enviar um valor para o channel de resultados ou para o channel de erros — nunca para ambos, e nunca para nenhum. Se uma goroutine puder sair sem enviar nada, o loop coletor vai bloquear indefinidamente aguardando um valor que nunca vem.

Comparando os dois padrões

Ambos os padrões propagam erros corretamente. A escolha entre eles é principalmente sobre clareza de API e como quem chama quer consumir a saída.

Result structChannels separados
ChannelsUmDois
Acoplamento resultado–erroSempre juntosDesacoplados
Loop do callerrange ou for únicoselect sobre dois channels
Tipo retornadoStruct personalizadoTipos existentes inalterados
OrdenaçãoArbitrária, mas explícita via campoArbitrária, contexto perdido

O result struct é geralmente o melhor padrão padrão. É mais difícil de usar incorretamente: quem chama recebe um valor que contém tanto o resultado quanto o contexto daquele resultado. Não há como acidentalmente drenar apenas um channel e deixar goroutines bloqueadas.

O error channel separado faz sentido quando:

  • O tipo do resultado já está definido e adicionar um struct wrapper poluiria a API
  • Erros e resultados são genuinamente processados por partes diferentes do código
  • Você está construindo um pipeline onde erros convergem de múltiplos estágios e um error channel dedicado se encaixa naturalmente no design

Em ambos os casos, a disciplina-chave é a mesma: toda goroutine que pode falhar deve ter um caminho garantido para que essa falha chegue ao caller. Falhas silenciosas não são um problema exclusivo do Go — elas aparecem sempre que concorrência se combina com erros ignorados — mas o modelo de channels do Go oferece as ferramentas para tornar cada falha explícita.