Por que eu gosto do tratamento de erros de Go

Elton Minetto
Inside PicPay
Published in
4 min readMar 24, 2023

--

Um dos assuntos mais polêmicos da linguagem Go é a forma como são tratados os erros. Lembro quando comecei a trabalhar com a linguagem, em meados de 2015, depois de ter usado PHP por alguns anos e me surpreendi com o fato dela não fazer uso do famoso try/catch.

Depois de passado o primeiro impacto fui atrás de entender o motivo e a resposta oficial é "em Go erros são cidadãos de primeira classe". Você é responsável por tratá-los explicitamente.

Vou tentar mostrar um exemplo para ilustrar isso. Recentemente esbarrei com o seguinte código em Java:

private JsonNode get(String query) {
try {
SimpleHttp.Response response = SimpleHttp.doGet(query, this.session)
.connectionRequestTimeoutMillis(Integer.parseInt(model.get(ConsumerProviderConstants.TIMEOUT_CONSUMER)))
.connectTimeoutMillis(Integer.parseInt(model.get(ConsumerProviderConstants.CONN_TIMEOUT_CONSUMER)))
.socketTimeOutMillis(Integer.parseInt(model.get(ConsumerProviderConstants.SOCKET_TIMEOUT_CONSUMER)))
.header(HttpHeaders.CONTENT_TYPE, "application/json; charset=utf-8")
.asResponse();
switch (response.getStatus()) {
case HttpStatus.SC_OK:
return response.asJson();
default:
logger.errorf("private get(%s) - ResponseBody: {%s} - StatusCode(%d)", query, response.asString(),
response.getStatus());
return null;
}
} catch (Exception e) {
throw new ServiceException("Error on retrieve data");
}
}

Cheguei até esse código depois de encontrar algumas ocorrências de “Error on retrieve data” nos logs da aplicação.

Qual é o problema com o código acima? Olhando apenas o código é impossível saber exatamente qual trecho está causando o erro. Pode ser no SimpleHttp.doGet, no model.get, no Integer.parseInt, etc.

Foi preciso uma refatoração para que possamos tornar o código mais robusto. Ele ficou +- assim (mais refatorações podem ser feitas, mas para esse exemplo é o suficiente):

public int tryParseInt(String stringToParse, int defaultValue, String varName) {
try {
return Integer.parseInt(stringToParse);
} catch (NumberFormatException ex) {
logger.errorf("tryParseInt(String %s, varName %s)", stringToParse, varName);
return defaultValue;
}
}

private JsonNode get(String query) {
try {
SimpleHttp.Response response = SimpleHttp.doGet(query, this.session)
.connectionRequestTimeoutMillis(
this.tryParseInt(model.get(ConsumerProviderConstants.TIMEOUT_CONSUMER), 1000,
ConsumerProviderConstants.TIMEOUT_CONSUMER))
.connectTimeoutMillis(this.tryParseInt(model.get(ConsumerProviderConstants.CONN_TIMEOUT_CONSUMER),
1000, ConsumerProviderConstants.CONN_TIMEOUT_CONSUMER))
.socketTimeOutMillis(this.tryParseInt(model.get(ConsumerProviderConstants.SOCKET_TIMEOUT_CONSUMER),
5000, ConsumerProviderConstants.SOCKET_TIMEOUT_CONSUMER))
.header(HttpHeaders.CONTENT_TYPE, "application/json; charset=utf-8")
.asResponse();

if (response == null) {
logger.errorf("Response was return null");
throw new ServiceException("Error response null");
}

switch (response.getStatus()) {
case HttpStatus.SC_OK:
return response.asJson();
default:
logger.errorf("private get(%s) - ResponseBody: {%s} - StatusCode(%d)", query, response.asString(),
response.getStatus());
return null;
}
} catch (Exception e) {
logger.errorf("ConsumerService Response error (%s)", e);
throw new ServiceException("Error on retrieve data");
}
}

Bem melhor, pois agora identificamos que o erro estava acontecendo no momento da conversão da variável de ambiente TIMEOUT_CONSUMER para inteiro, pois ela estava vazia. Um ponto de atenção em relação a esse novo código é que agora a função tryParseInt conhece e trata a NumberFormatException. Isso gerou um acoplamento entre nossa função e a classe Integer. Se em algum momento o nome desta exception mudar, ou se uma nova começar a ser lançada é possível que seja necessário alterarmos o nosso código para adequar a essa mudança.

Mas e o que isso tem a ver com Go? O problema apresentado aqui não é exclusivo de Java ou mesmo do conceito de try/catch. Muito menos da pessoa que escreveu o código ou de quem revisou (aliás, eu fui uma das pessoas que revisou esse código e deixei essa melhoria passar). O meu ponto é que Java (ou outra linguagem) permite essa construção.

Enquanto isso, Go, por ser uma linguagem fortemente opinativa torna esse tipo de situação muito mais difícil de ocorrer. Essa é minha interpretação do código acima em Go:

package main

import (
"encoding/json"
"fmt"
"io/ioutil"
"net/http"
"strconv"
"time"
)

const (
timeoutConsumer = "timeout.consumer"
)

type model struct{}

// just to emulate the model
func (m model) get(name string) string {
return "0"
}

func main() {
json, err := getJSON("http://url_to_be_consumed")
if err != nil {
panic(err) // handle error
}
fmt.Println(json)
}

func getJSON(query string) (string, error) {
m := model{}
tc, err := strconv.Atoi(m.get(timeoutConsumer))
if err != nil {
return "", fmt.Errorf("error converting timeout: %w", err)
}
req, err := http.NewRequest(http.MethodGet, query, nil)
if err != nil {
return "", fmt.Errorf("error creating request: %w", err)
}
req.Header.Set("Content-Type", "application/json; charset=utf-8")

client := http.Client{
Timeout: time.Duration(tc) * time.Second,
}
res, err := client.Do(req)
if err != nil {
return "", fmt.Errorf("error executing request: %w", err)
}
if res == nil {
return "", fmt.Errorf("empty response")
}
if res.StatusCode != http.StatusOK {
return "", fmt.Errorf("expected %d received %d", http.StatusOK, res.StatusCode)
}

resBody, err := ioutil.ReadAll(res.Body)
if err != nil {
return "", fmt.Errorf("error reading body: %w", err)
}
var result map[string]any
err = json.Unmarshal(resBody, &result)
if err != nil {
return "", fmt.Errorf("error parsing json: %w", err)
}
jsonStr, err := json.Marshal(result)
if err != nil {
return "", fmt.Errorf("error converting json: %w", err)
}
return string(jsonStr), nil
}

Você pode argumentar que o código ficou bem mais verboso. Mais linhas para escrever a mesma funcionalidade, e "OMG, aquele monte de if/else!!!". Concordo com esse argumento. Realmente o código ficou mais longo, mas a leitura dele ficou muito mais óbvia. O fato de cada função sempre retornar um erro obriga quem está escrevendo o código a tratá-lo ou ignorá-lo, o que ficaria bem visível. Por exemplo, seria possível trocar o trecho:

tc, err := strconv.Atoi(m.get(timeoutConsumer))
if err != nil {
return "", fmt.Errorf("error converting timeout: %w", err)
}

Por:

tc, _ := strconv.Atoi(m.get(timeoutConsumer))

Mas nesse caso qualquer pessoa poderia, no code review, fazer a observação: "Ei, você está simplesmente ignorando o erro de conversão? Tem certeza que é correto fazer isso?". E como vimos neste exemplo, erros de conversão podem realmente acontecer :)

Fica aqui meu relato do motivo pelo qual eu gosto da forma como Go trata seus erros. Está longe de ser perfeito, pois existem outras linguagens que fazem isso de maneira diferente, de fato é verboso. Mas eu prefiro escrever mais linhas e ter uma maior facilidade de manter o código no futuro.

Obrigado ao colega Bruno Souza pela revisão neste post.

--

--

Elton Minetto
Inside PicPay

Teacher, speaker, Principal Software Engineer @ PicPay. https://eltonminetto.dev. Google Developer Expert in Go