Por que Go optou pelo retorno explícito de erros e por panic-recover em vez do lançamento de exceções com try-catch

Go deliberadamente não oferece suporte a try-catch e suporta apenas a sintaxe panic-recover. Isso tem gerado insatisfação entre os muitos desenvolvedores acostumados ao tratamento de erros com try-catch. Então, por que o try-catch não foi incluído? Isso ocorre porque o try-catch apresenta vários problemas.
try-catch-finally
A estrutura try-catch (try-catch) é uma construção destinada a tratar situações de erro e de exceção que podem ocorrer durante a execução do programa (em tempo de execução). Além disso, o bloco finally contém o código que deve ser executado incondicionalmente, independentemente da ocorrência de exceções.
Tratamento de Erros e Responsabilidade
Nas décadas de 1980 e 1990, o tratamento de erros era muito simples. As mensagens de erro se resumiam a "disquete cheio", "nenhum disquete na unidade" e "sem permissão de gravação no disquete". Nessa época, quando um erro ocorria, os desenvolvedores o propagavam (throw) até o ponto de tratamento de erros, centralizando o processamento. Nesse cenário, a estrutura try-catch funcionava de forma eficiente.
Contudo, com o passar do tempo, a situação mudou. A lógica de negócio tornou-se complexa, surgiram bancos de dados e transações, era preciso chamar inúmeras APIs pela rede e interpretar muitas mensagens de requisição e, até mesmo, com o advento da programação concorrente, tornou-se necessário tratar erros em threads diferentes da principal.
Os erros tornaram-se complexos demais para serem tratados em um único lugar, e nenhum ponto isolado passou a ser capaz de se responsabilizar por todos eles. É aqui que o try-catch passa a apresentar um problema grave.
A Transferência de Responsabilidade do try-catch
Em suma, o try-catch é um método pelo qual o sujeito que gera um erro transfere a responsabilidade pela sua ocorrência (o tratamento posterior) a outra parte. Essa parte pode ser o bloco catch, o método pai, o pai do pai do pai do pai... ou qualquer outro elemento. Em outras palavras, em um mundo onde o tratamento de erros se torna cada vez mais numeroso e complexo, a solução adotada pelo try-catch é simplesmente "alguém vai resolver". Observe o código abaixo.
1try {
2 data = readFile("hello.txt");
3 structuredData = parseData(data);
4 insertDBStatus(structuredData[1]);
5 startHTTPServer(structuredData[2]);
6} catch (Exception e) {
7 e.printStackTrace();
8}
O problema desse código é que não fica claro qual é o responsável por processar o printStackTrace, e tampouco é possível saber em qual trecho o erro ocorreu. Além disso, quanto mais lógica houver dentro do bloco try, pior a situação se torna. Ainda assim, paradoxalmente, quanto mais complexo o desenvolvimento se tornava, mais os desenvolvedores se viciavam em estruturas try-catch que transferem responsabilidades; deixaram de refletir sobre o tratamento de erros, sua noção de responsabilidade se enfraqueceu e, por fim, esqueceram a essência do tratamento de erros e de exceções. Então, como o Golang resolveu esse problema?
panic e recover
Uma das vantagens da linguagem Go é possuir diversos mecanismos que, em vez de conduzir o desenvolvedor por caminhos equivocados, o ajudam a se tornar um bom desenvolvedor. O par panic-recover é um exemplo disso.
À primeira vista, panic e recover não parecem diferentes do try-catch, mas diferem no ponto de que, quando ocorre um problema, a responsabilidade não é transferida para o exterior. Em Go, quando ocorre um panic, o valor não é simplesmente repassado para fora; a solução deve ser feita no próprio local onde o panic ocorreu. Isso atribui ao desenvolvedor a responsabilidade pelo tratamento de erros, levando-o a refletir com mais profundidade sobre onde, por quem e como esse erro deve ser tratado. Trata-se de garantir autonomia ao usuário, ao mesmo tempo em que se deixa espaço para a reflexão.
Além disso, a escolha da palavra panic também é notavelmente acertada: diferentemente de try-recover, o par panic-recover impõe ao desenvolvedor a pressão de que deve ser usado apenas em situações de erro evidente, e não em situações meramente excepcionais. Naturalmente, o desenvolvedor não abusa de recover e o utiliza apenas onde é necessário. Isso contribui muito para a escrita de código conciso em Go.