- A verbosidade no tratamento de erros em Go está entre as principais reclamações dos usuários há muito tempo
- Várias propostas de melhoria sintática (como check/handle, try, operador
? etc.) foram discutidas e testadas, mas todas foram rejeitadas por falta de consenso suficiente na comunidade
- O amplo impacto sobre código, ferramentas, documentação etc. causado por mudanças na linguagem, junto com o princípio de preservar a simplicidade característica de Go, são fatores centrais nessa decisão
- A clareza do modelo atual, a facilidade de depuração e a preferência de parte dos usuários tornam fraca a justificativa para introduzir uma mudança sintática
- Não há planos de mudar a sintaxe de tratamento de erros em um futuro previsível, e as propostas relacionadas serão encerradas sem investigação adicional
Levantando o problema da verbosidade no tratamento de erros em Go
- Uma das reclamações antigas sobre Go é que a sintaxe de tratamento de erros é excessivamente verbosa
- Um exemplo representativo é o padrão
if err != nil, que aparece repetidamente no código
- Quanto mais chamadas de API um programa exige, mais esse padrão se destaca, chegando ao ponto de haver mais código de tratamento de erros do que lógica real
- Em pesquisas anuais com usuários, essa reclamação continua aparecendo entre as principais menções
Consulta com a comunidade e propostas iniciais
- A equipe de Go sempre valorizou o feedback da comunidade e continuou pesquisando formas de melhorar o tratamento de erros
- Em 2018, nas discussões do projeto Go 2, Russ Cox organizou formalmente os pontos centrais do problema de tratamento de erros
- Surgiu a proposta do mecanismo
check e handle de Marcel van Lohuizen
- A proposta incluía comparação com linguagens semelhantes e análise de várias alternativas
- Embora essa abordagem realmente tornasse o código mais conciso, ela não foi adotada por aumentar a complexidade
A proposta try e o que veio depois
- Em 2019, foi apresentada a proposta da função embutida
try, bem mais simplificada
- Apenas a funcionalidade de
check foi levada para o código, sem handle
- A proposta foi criticada por esconder o fluxo de controle e acabou descartada em meio à reação negativa da comunidade
- Essa experiência deixou claro o risco de propostas prontas sem feedback suficiente
- Em mudanças de grande porte, ficou evidente a importância de ouvir opiniões mais amplas ainda na fase inicial de design
Novas tentativas e várias propostas
- Inúmeras variações e abordagens alternativas de tratamento de erros continuaram surgindo na comunidade
- Ian Lance Taylor organizou o cenário em uma umbrella issue, e exemplos continuaram sendo reunidos no Go Wiki, em blogs e em outros espaços
- Em 2024, surgiu uma proposta para aplicar o operador
? emprestado de Rust
- Em pequenos testes de usabilidade, houve feedback de que ele parecia intuitivo, mas, mais uma vez, não se chegou a consenso diante da diversidade de opiniões
O impasse da discussão e a conclusão
- Houve mais de três propostas formais e informais, e centenas de propostas da comunidade, mas todas foram rejeitadas por falta de empatia coletiva e consenso suficiente
- Nem mesmo o grupo interno de arquitetos de Go tem alinhamento sobre a direção a seguir
- Até que haja mudança no cenário ou formação de um consenso especial, foi decidido interromper as tentativas de alterar a sintaxe de tratamento de erros
Principais argumentos em defesa da manutenção do modelo atual
- Se açúcar sintático tivesse sido incluído no design inicial da linguagem, talvez não houvesse controvérsia, mas hoje existe um ecossistema acostumado a esse modelo há 15 anos de uso
- A introdução de nova sintaxe inevitavelmente traria preocupação com a diferença de estilo entre código antigo e novo e a quebra de consistência entre usuários existentes e novos
- Isso também está alinhado com a filosofia de design de Go de não fazer a mesma coisa de várias maneiras e com o princípio de priorizar simplicidade e consistência
- A permissão de redeclaração na declaração curta de variáveis (
:=) também é uma mudança secundária surgida por causa do tratamento de erros
- A sintaxe explícita de tratamento de erros (via
if) tem vantagens intuitivas para leitura de código, depuração e definição de breakpoints
- Mudanças na linguagem também representam um peso grande em termos de escopo e custo real da alteração, afetando código, documentação, ferramentas e mais
Melhorias alternativas e direção futura
- O fortalecimento da biblioteca padrão (por exemplo, com a introdução de
cmp.Or) pode reduzir parte do código repetitivo
- IDEs e ferramentas de desenvolvimento, com dobramento de código, autocompletar, uso de LLMs etc., podem amenizar na prática parte dessa verbosidade
- Em grupos importantes de usuários de Go (como participantes do evento Google Cloud Next), predomina uma visão negativa sobre a necessidade de mudar a linguagem
- Quanto mais a pessoa usa Go, menos tende a sentir esse problema de verbosidade na prática
Argumentos a favor da necessidade de melhorar a sintaxe
- Com base no feedback dos usuários, ainda existe demanda por melhorias na sintaxe de tratamento de erros
- Uma sintaxe de tratamento de erros que não apenas reduza caracteres, mas aumente a clareza também pode contribuir para melhorar qualidade e segurança do código
- É necessário pesquisar com mais precisão o tratamento de erros que realmente exerce um papel relevante, e não apenas a verificação simples de erros
Conclusão final e política daqui para frente
- Reconhecendo que até agora não houve consenso relevante nem mudança prática, declara-se que, em um futuro previsível, todas as discussões e propostas de mudança sintática na linguagem para tratamento de erros serão interrompidas
- O processo anterior de discussão e pesquisa acabou contribuindo indiretamente para melhorias no ecossistema e nos processos de Go
- Se no futuro surgir uma definição mais clara do problema e um consenso mais sólido, a discussão poderá ser retomada
- Por ora, a diretriz será manter a robustez e a simplicidade do próprio Go em vez de investir em novas tentativas
1 comentários
Comentários do Hacker News
Se alguém quiser sugerir facilmente que a equipe do Go poderia ter escolhido outras alternativas, gostaria muito que antes desse uma olhada no wiki Go2ErrorHandlingFeedback ou na busca de issues no GitHub. Quase todas as ideias já propostas foram discutidas seriamente, e, como usuário que aprecia a abordagem transparente da equipe do Go, tenho muito prazer em usar Go todos os dias
O rascunho do documento de design menciona C++, Rust e Swift, mas é difícil encontrar algo como do-notation/for-comprehensions/monadic-let de linguagens funcionais como Haskell/Scala/OCaml, que é o que eu estava procurando. A equipe do Go parece mestre em design de linguagem, mas no fim acaba esbarrando nas limitações de uma tipagem estática sem polimorfismo paramétrico, como Java, e não consegue chegar a uma solução para o problema de tratamento de erros. Acho que isso vem de uma limitação do projeto fundamental da linguagem
Mesmo sendo um documento escrito por pessoas inteligentes e experientes, é muito curioso que soluções como os mônadas Maybe/Either de Haskell e o operador bind (do-notation) não sejam mencionados em lugar nenhum. Na prática isso não é difícil nem pedante, e é uma forma muito elegante e comprovada de propagar erros com segurança, então não entendo por que a comunidade Go não tentou incorporar isso. Sou grato por essa página existir, mas é difícil entender como uma solução tão famosa foi ignorada
Quase toda linguagem oferece abordagens diversas e melhores, então fico curioso por que justamente no Go esse problema ganha tanto destaque. Não sei se é apenas falta de consenso, ou se existe alguma característica específica do Go que faz com que soluções de outras linguagens não se encaixem bem
Um fenômeno frequente nas críticas ao Go é que pessoas relativamente não especialistas tendem a partir do pressuposto de que os desenvolvedores de Go entendem menos de linguagens do que elas. Na realidade, na maioria dos casos, os desenvolvedores de Go têm muito mais experiência e sabem muito mais. O não especialista vê uma linguagem com muitos recursos e acha automaticamente que ela é melhor, mas ignora que o importante é equilibrar bem o conjunto como um todo
Acho que os usuários se beneficiam do conservadorismo do Go ao ser cauteloso com a adição de novos recursos à linguagem. No caso do Swift, há tantas mudanças de recursos que ele fica difícil de aprender e, mesmo em Macs novos, às vezes nem um projeto simples compila. Como palavras-chave continuam aumentando e mudando, o Swift perde em continuidade de uso; comparado a isso, a consistência é um ponto forte do Go
Uma vez, tive um caso excepcional em que uma função Go esperava que uma função interna gerasse erro, e, se ela não gerasse, aí sim a função deveria falhar. Nessa estrutura incomum, eu precisava ramificar com
if err == nil, mas por hábito acabei escrevendoif err != nil, e demorei bastante para encontrar o erro por estar acostumado demais com o padrão de sempre. Fiquei pensando que, se houvesse suporte sintático no nível da linguagem para distinguir o uso frequente deif err != nildo uso raro deif err == nil, isso poderia reduzir esse tipo de enganoif err == nil, adiciono um comentário// invertedpara destacar o padrão. Seria bom se a linguagem tratasse isso automaticamente, mas, por enquanto, esse método já ajuda a tornar a distinção mais claraif err == nil { return ... }pode acabar parecendo ainda mais estranho no código. Há quem prefira o tratamento de erros atual do Go justamente por ele ser claro e fácil de lerif fruit != "Apple", então o argumento é que isso, no fundo, não é um problema exclusivo de tratamento de erros, mas um problema geral de ramificação por estado. Erros, no fim, também são tratados como apenas mais um valor de estadoif err != nilcomo se fosse um símbolo especial, fazendo-o se misturar mais ao fundo e deixando apenas usos diferentes, comoif err == nil, em maior destaque, o que poderia ajudar a evitar erros no nível do editorif err … {, melhorando a legibilidadeGosto do tratamento explícito de erros do Go. Eu simplesmente entendo que uma função ou sempre tem sucesso (
minimal error) ou pode falhar. Se uma função pode falhar, isso precisa ser tratado antes de prosseguir para a próxima etapa. Em muitas linguagens, com tratamento por exceções, quando ocorre um erro ele vai subindo pela stack até ser capturado, o que muitas vezes só informa onde aconteceu, sem dar pistas realmente úteis. No Go, é possível ter opções claras: 1) ignorar o erro 2) retornar imediatamente quando houver erro 3) encapsular o erro adicionando informações úteis 4) interpretar um erro específico e ramificar com base nele (por exemplo, converter para 404). No Go2, eu gostaria de experimentar adicionar um tipoResult<Value, Failure>ou tipos de erro mais específicos e enumeráveis. Acho mais adequado introduzir isso no Go 2 por causa da compatibilidade com o Go 1No começo eu não gostava da forma como o Go trata erros, mas depois de ler o post errors-are-values e começar a usar
panic(err)nos lugares certos, passei a ficar bastante satisfeito. Para estados anormais que o código pai não deveria tratar diretamente, usar panic reduziu muito a quantidade de ramificações de erro no código. Essa forma de gerenciar erros tem ajudado bastante no trabalho real-etry/catch/finallyem C#, achei inovador, mas hoje acabo preferindo uma lógica simples como a do Go. Mesmo com mais linhas de código (Loc), considero uma vantagem o fato de o fluxo ficar claroQuando se diz que a verbosidade some rápido assim que você realmente trata o erro, isso faz surgir a dúvida se gerar manualmente um stack trace é mesmo “tratar” o erro. Pela definição do Go, então exceções também não seriam tratamento? Essa é uma contestação bem-humorada
Não gosto que este texto trate o problema do tratamento de erros no Go simplesmente como “a sintaxe é verbosa”. Acho que os problemas reais incluem: 1) erros podem ser omitidos silenciosamente ou ignorados por engano com facilidade 2) resultados de função não podem ser passados ou armazenados com facilidade como valores 3) erros aninhados como
errors.Isse encaixam de forma estranha no sistema de tipos 4) fazer switching sobre erros é difícil 5) há muito uso de sentinel value na biblioteca padrão 6) a compatibilidade com generics é ruim, o que acaba gerando necessidade de pacotesEm Elixir (e Erlang), funções normalmente retornam tuplas
{:ok, result}ou{:error, description}. Graças à sintaxewithdo Elixir, o tratamento de erros pode ficar agrupado na parte inferior do bloco, melhorando bastante a legibilidade. Se o Go adotasse algo parecido com uma instruçãowith, daria para melhorar a leitura deixando a execução seguir apenas quando o erro fornile colocando um bloco de tratamento de erro no finalNão entendo por que não seguem logo o estilo do Rust. Especialmente agora que existem generics, já seria possível implementar algo parecido sem muita demora. Não concordo com a crítica de que o operador
?do Rust, embora conveniente, incentivaria ignorar erros. Na prática, o Go está cheio de casos em que valores de erro podem ser ignorados sem nem gerar erro de compilação. Para realmente evitar esse tipo de engano, seria preciso forçar retornos com um tipoResult, no estilo do Rust. Se isso vira polêmica em nome da conveniência, então talvez também fosse coerente proibirpanic, segundo essa visão mais forteResultporque não tem sum types e tem esse projeto peculiar de exigir zero value para todos os tipos?” faria as pessoas “pararem de usar erros encapsulados”, há a contraposição de que daria perfeitamente para desenhar um recurso desses de forma a incentivar wrappingAcho que linguagem não deve ser projetada marcando itens de checklist como no Rust ao discutir adoção de recursos; ela precisa ser desenhada dentro de uma consistência geral. Só porque todos os itens de uma lista foram marcados não significa que devam ser introduzidos, se isso não combinar com a essência da linguagem