Recentemente, entrei em um novo projeto na Colorkrew que usa o Golang como linguagem de backend. Quase todo mundo da equipe, inclusive eu, não tinha experiência com o Golang, então tivemos que começar do zero. Aprender o Golang tem sido uma experiência gratificante até agora. Afinal, é uma das linguagens de programação mais requisitadas atualmente. No entanto, escrever código em Golang não é só flores. Por mais sem opinião que eu tenha, ainda tenho minha cota de reclamações, sendo a maior a forma recomendada de lidar com erros no Golang.
De acordo com o tutorial oficial do Golang, quando ocorrem erros, as funções devem devolvê-los, junto com os valores regulares retornados. Aqui está um exemplo do tutorial do Golang com pequenos ajustes:
Pacote principal
importar (
"erros"
"FMT"
"tronco"
)
Hello retorna uma saudação para a pessoa nomeada.
func Hello(string de nome) (*string, error) {
se nome == "" {
return nil, erros. Novo ("nome vazio")
}
mensagem := fmt. Sprintf("Oi, %v. Bem-vindo!", nome)
Retorno & mensagem, nil
}
A função 'Hello' recebe uma string chamada 'name' como parâmetro. Ela cria uma 'mensagem' usando o 'nome' dado e a retorna. 'name' ser uma string vazia é inaceitável e considerado um erro. A função 'Hello' primeiro verifica se o nome é uma string vazia. Se a string estiver vazia, então 'Hello' retorna um erro 'nul' e 'nome vazio'. Caso contrário, retorna uma mensagem real construída com o parâmetro 'name' e um erro vazio ('nil').
Como o Golang exige que um erro seja devolvido ao chamador de uma função como parte do valor retornado da função, os engenheiros da Golang precisam retornar um erro, camada por camada, até que queiram lidar com ele. Como resultado, o código Golang inevitavelmente está repleto do seguinte padrão repetitivo de erro.
if err != nil {
Retorne a nada, err
}
Esse estilo de tratamento de erros difere das convenções padrão de linguagens de programação, onde funções geram erros para serem tratados em blocos de captura. A convenção tradicional não exige que engenheiros passem erros camada por camada. Engenheiros podem capturar e lidar livremente com erros em qualquer camada que quiserem, resultando em uma base de código menos prolixa e menos repetitiva.
Em defesa de Golang, sua filosofia de manejo de erros exige que os engenheiros decidam onde e como lidar com cada erro. Isso diminui a probabilidade de falhas do sistema devido à supervisão dos programadores. Além disso, tratar um erro como parte do valor retornado de uma função é propício para que engenheiros escrevam funções puras, que são mais previsíveis do que funções impuras. Aumentar erros faz com que o programa se desvie do fluxo normal e crie efeitos colaterais, tornando o código menos previsível. Devolver erros elimina esse problema e facilita a escrita de funções puras, produzindo código que os engenheiros podem facilmente raciocinar.
Quanto mais eu copiava e colava o padrão de erros acima, mais eu me perguntava: existe uma maneira mais elegante de lidar com erros sem violar os princípios da programação funcional? Então, um dia, percebi que a resposta para essa pergunta é bem simples: use 'Either', um tipo de dado comumente usado para lidar com erros em linguagens de programação funcional como Scala.
O que diabos é 'Either'? 'Either' é uma união disjunta, e uma instância dela pode ser 'Esquerda' ou 'Direita'. Convencionalmente, armazenaríamos informações de erro em 'Esquerda' e valores válidos em 'Direita'. Em outras palavras, quando um 'Either' é um 'Left', é um erro contendo uma mensagem de erro. Quando um 'Either' é um 'Right', ele contém um valor válido.
Vamos reescrever a função 'Olá' acima no Scala com 'Either'.
def hello (nome: String): Either[String, String] = {
if (name.isEmpty()) {
return Left("nome vazio")
}
retorne à direita(s"Oi, $name. Bem-vindo!")
}
No Scala, você também pode omitir a palavra-chave return e reescrever a função Hello conforme segue:
def hello (nome: String): Either[String, String] = {
if (name.isEmpty()) {
Esquerda ("nome vazio")
} else {
Certo(s"Oi, $name. Bem-vindo!")
// }
// }
A primeira diferença perceptível é que a função 'olá' do Scala tem apenas um valor retornado, ao contrário dos dois da função Golang. Em segundo lugar, podemos ter certeza de que a função 'olá' do Scala retorna um dos dois tipos possíveis: 'Left' com uma mensagem de erro ou 'Right' contendo uma mensagem válida de boas-vindas. Não temos o mesmo grau de certeza ao invocar a função 'Hello' do Golang. De acordo com sua definição, existem apenas duas combinações possíveis de valores retornados: '(nulo, erro)' e '(mensagem, nenhum)'. Mas tecnicamente há mais duas combinações possíveis que podemos inferir a partir da dica do tipo: '(nul, nul)' e '(mensagem, erro)'. Ao chamar a função de 'Hello' do Golang, precisamos raciocinar sobre as quatro combinações possíveis, o que adiciona mais complexidade do que o 'Either' do Scala.
Além de simplificar os valores de retorno das funções, 'Either' também simplifica o tratamento de erros. Vamos expandir o exemplo anterior. Queremos construir uma nova função, 'VIPHello', que pega um parâmetro chamado 'name' e chama a função 'Hello' com ele. Se a função 'Hello' retornar um erro, 'VIPHello' retorna esse erro. Se a função 'Hello' retorna uma mensagem válida, 'VIPHello' adiciona texto adicional à mensagem e retorna a mensagem resultante. No Golang, implementaríamos isso da seguinte forma.
Pacote principal
importar (
"erros"
"FMT"
"tronco"
)
Hello retorna uma saudação para a pessoa nomeada.
func Hello(string de nome) (*string, error) {
se nome == "" {
return nil, erros. Novo ("nome vazio")
}
mensagem := fmt. Sprintf("Oi, %v. Bem-vindo!", nome)
Retorno & mensagem, nil
}
func VIPHello(string de nome) (*string, error) {
helloMessage, err := Hello(nome)
if err != nil {
Retorne a nada, err
}
Mensagem := FMT. Sprintf("%v Por favor, aceite nossa sincera gratidão", *oláMensagem)
Retorno & mensagem, nil
}
Como você pode ver, precisamos usar o padrão de erro que mencionei acima. Imagine o quão prolixo seu código fica se você tiver que copiar e colar o boilerplate de erro toda vez que invocar uma função falível. Enquanto isso, podemos implementar a mesma função no Scala de forma mais elegante, graças ao 'Either' e seu método 'map'.
def hello (nome: String): Either[String, String] = {
if (name.isEmpty()) {
return Left("nome vazio")
}
retorne à direita(s"Oi, $name. Bem-vindo!")
}
def vipHello (nome: String): Either[String, String] = {
retorne hello(name).map((direita) => s"$right Por favor, aceite nossa sincera gratidão)
}
Uma frase simples, porém legível (para programadores do Scala)! Deixe-me explicar como funciona. Se o valor retornado da função 'olá' for 'Esquerda', o método 'mapa' simplesmente o retornaria. A mágica do método 'mapa' do Either acontece quando o valor retornado da função 'olá' é um 'Certo'. Ele extrai a mensagem válida do 'Certo', adiciona a mensagem adicional conforme especificado pela função passada e envolve a mensagem resultante com 'Certo' antes de retornar. Essa função realiza tudo o que seu equivalente Golang faz, mas de forma mais limpa e elegante. Mais importante ainda, não há necessidade de copiar e colar o boilerplate de erro em todo lugar!
Obrigado por ouvir meu desabafo sobre minha insatisfação com o estilo de tratamento de erros do Golang! Ainda sou encantado com muitos outros aspectos do Golang, como sua simplicidade e desempenho excepcional, mas gostaria de não ter que ficar copiando e colando o maldito código de erros por todo lado ou escrever funções que retornam valores válidos e erros separadamente.
Espero que este artigo possa inspirar programadores talentosos do Golang a repensar o estilo de manejo de erros do Golang e, possivelmente, explorar a possibilidade de introduzir o 'Either' no Golang.