Em Go, o tratamento de erros não é feito através de exceções.
Em vez disso, os erros são valores normais de tipos que implementam a interface error incorporada.
A interface error é muito reduzida.
Contém apenas um método, Error(), que devolve a mensagem de erro como uma string.
type error interface {
Error() string
}
Sempre que defines uma função em que pode ocorrer um erro durante a execução que tenha de chegar a quem a chama, tens de incluir error como um dos tipos de retorno.
Se a função tiver vários valores devolvidos, por convenção o error é sempre o último.
func DoSomething() (int, error) {
// ...
}
Não tens de implementar sempre a interface de erro tu próprio.
Para criar um erro simples, podes usar a função errors.New(), que faz parte do pacote errors da biblioteca padrão.
A única coisa que tens de passar é a mensagem de erro como string, e o errors.New() encarrega-se de criar um valor que contém a tua mensagem e implementa a interface error.
Se a função devolver um erro, é boa prática devolver o valor zero para todos os outros valores de retorno:
func DoSomething() (SomeStruct, int, error) {
// ...
return SomeStruct{}, 0, errors.New("failed to calculate result")
}
Não deves assumir que todas as funções devolvem valores zero nos outros valores de retorno quando há um erro. É boa prática assumir que não é seguro usar nenhum dos outros valores de retorno se for devolvido um erro.
Por exemplo, imagina que uma função estava a tentar ler de um ficheiro e devolve uma slice de bytes meio preenchida e um erro. Se reutilizares essa slice de bytes devolvida no teu código, assumindo que está sempre vazia porque houve um erro, podes deparar-te com bugs subtis.
As únicas exceções são os casos em que a documentação da função indica claramente que os outros valores de retorno são relevantes no caso de um erro.
Vê a [documentação de Write num buffer][buffer-write] para veres um exemplo.
Certifica-te de que a mensagem de erro que forneces é o mais específica possível, pois os erros não incluem stack traces por predefinição. Por convenção, a mensagem de erro deve começar com uma letra minúscula e não terminar com um ponto final.
Se quiseres usar esse erro em vários sítios (ou quiseres tornar o erro disponível para quem consome o teu pacote), deves declarar uma variável para o erro em vez de usares errors.New em linha.
Por convenção, o nome da variável deve começar por Err ou err (consoante seja exportada ou não).
Estas variáveis de erro são frequentemente chamadas erros sentinela.
import "errors"
var ErrNotFound = errors.New("resource was not found")
func DoSomething() error {
// ...
return ErrNotFound
}
Devolve nil no erro para sinalizar que não houve erros durante a execução da função:
func Foo() (int, error) {
return 10, nil
}
Se precisares de construir uma mensagem de erro mais complexa a partir de algumas variáveis, podes recorrer à formatação de strings.
O Go tem uma função especial, fmt.Errorf, para esse efeito.
Constrói a string da mensagem de erro e devolve um erro.
input := 123
action := "UPDATE"
err := fmt.Errorf("invalid input %d for action %s", input, action)
err.Error()
// => "invalid input 123 for action UPDATE"
Se chamares uma função que devolve um erro, é comum guardar o valor do erro numa variável chamada err.
Antes de usares o resultado real da função, tens de verificar que não houve erro.
Podemos usar == e != para comparar o erro com nil, e sabemos que houve um erro quando err não é nil.
Para evitar aninhar o "happy path" do teu código, os casos de erro devem ser tratados primeiro, normalmente através de um retorno antecipado. Vê este famoso artigo do Mat Ryer para mais detalhes.
// Do this:
func myFunc() error {
file, err := os.Open("./users.csv")
if err != nil {
return err
}
// do something with file
}
// Not this:
func myFunc() error {
file, err := os.Open("./users.csv")
if err != nil {
// handle err
} else {
// do something with file
}
}
// Also not this:
func myFunc() error {
file, err := os.Open("./users.csv")
if err == nil {
// do something with file
}
// handle err
}
Na maioria das vezes, o erro é devolvido ao longo da pilha de funções, como mostra o primeiro exemplo acima. Outra forma de tratar o erro pode ser registá-lo e continuar com outra operação. É boa prática devolver o erro ou registá-lo, nunca ambos.
Como a maioria das funções em Go inclui um erro como um dos valores de retorno, vais ver e usar o padrão if err != nil por todo o lado no código Go.
Se quiseres que o teu erro inclua mais informação do que apenas a string da mensagem de erro, podes criar um tipo de erro personalizado.
Como referido anteriormente, tudo o que implementa a interface error (ou seja, que tem um método Error() string) pode servir como erro em Go.
Normalmente, usa-se uma struct para criar um tipo de erro personalizado.
Por convenção, os nomes dos tipos de erro personalizados devem terminar em Error.
Além disso, é melhor definir o método Error() string com um recetor de ponteiro; vê este comentário no Stackoverflow para perceberes o raciocínio.
Nota que isto significa que tens de devolver um ponteiro para o teu erro personalizado; caso contrário, não contará como error, porque o valor não-ponteiro não fornece o método Error() string.
type MyCustomError struct {
message string
details string
}
func (e *MyCustomError) Error() string {
return fmt.Sprintf("%s, details: %s", e.message, e.details)
}
func someFunction() error {
// ...
return &MyCustomError{
message: "...",
details: "...",
}
}