Em Go, o tratamento de erros é explícito e integrado à linguagem de forma simples, mas poderosa. Diferente de linguagens que usam exceções, Go adota uma abordagem onde os erros são valores comuns que podem ser retornados e verificados. Isso incentiva o programador a lidar com cada erro de forma consciente, resultando em código mais previsível e fácil de depurar.

Nesta aula, vamos explorar o tipo error, a construção if err != nil, como criar seus próprios erros com errors.New e fmt.Errorf, e as convenções idiomáticas que tornam o código Go elegante e eficiente.

O tipo error

Em Go, o tipo error é uma interface pré-definida com um único método: Error() string. Qualquer tipo que implemente esse método satisfaz a interface error e pode ser usado como um valor de erro.

A definição da interface é:

type error interface {
    Error() string
}

Isso significa que qualquer valor que tenha um método Error() string pode ser tratado como um erro. Na prática, a biblioteca padrão fornece implementações prontas, mas você também pode criar seus próprios tipos de erro para fornecer mais contexto.

Exemplo de uso do tipo error:

package main

import "fmt"

func main() {
    var err error = fmt.Errorf("algo deu errado")
    fmt.Println(err) // chama o método Error()
}

Saída:

algo deu errado

if err != nil

O padrão mais comum em Go para tratar erros é verificar se o valor retornado por uma função é nil ou não. A maioria das funções que podem falhar retorna um error como último valor de retorno. Se o erro for nil, a operação foi bem-sucedida; caso contrário, o erro deve ser tratado.

Exemplo típico:

package main

import (
    "fmt"
    "os"
)

func main() {
    f, err := os.Open("arquivo.txt")
    if err != nil {
        fmt.Println("Erro ao abrir arquivo:", err)
        return
    }
    defer f.Close()
    // ... usa o arquivo
}

Não há exceções ou blocos try/catch. O fluxo de erro é explícito e fácil de seguir. É uma boa prática tratar o erro imediatamente após a chamada da função, em vez de adiá-lo. Isso torna o código mais legível e evita que erros passem despercebidos.

Às vezes, você pode querer ignorar um erro intencionalmente, mas isso deve ser raro. Use _ para descartar o valor do erro:

result, _ := algumaFuncao() // não recomendado

Evite essa prática, pois ela pode esconder problemas.

Criando erros (errors.New, fmt.Errorf)

A biblioteca padrão oferece duas formas principais de criar erros simples: errors.New e fmt.Errorf.

errors.New recebe uma string e retorna um error com essa mensagem. É útil para erros estáticos.

package main

import (
    "errors"
    "fmt"
)

func main() {
    err := errors.New("ocorreu um erro")
    fmt.Println(err)
}

Saída:

ocorreu um erro

fmt.Errorf permite formatar a mensagem de erro usando verbos de formatação, similar ao fmt.Sprintf. É útil para incluir valores dinâmicos na mensagem.

package main

import (
    "fmt"
)

func main() {
    nome := "João"
    idade := 30
    err := fmt.Errorf("usuário %s tem idade %d, não permitido", nome, idade)
    fmt.Println(err)
}

Saída:

usuário João tem idade 30, não permitido

Para erros mais complexos, você pode criar tipos personalizados que implementam a interface error. Isso permite adicionar informações extras, como códigos de erro ou campos estruturados.

type MyError struct {
    Code    int
    Message string
}

func (e *MyError) Error() string {
    return fmt.Sprintf("erro %d: %s", e.Code, e.Message)
}

func main() {
    err := &MyError{Code: 404, Message: "recurso não encontrado"}
    fmt.Println(err)
}

Saída:

erro 404: recurso não encontrado

Idiomático

Go tem convenções claras para tratamento de erros que são consideradas idiomáticas. Aqui estão as principais práticas:

  • Verifique erros imediatamente: Após uma chamada de função que retorna erro, verifique-o logo em seguida. Não acumule erros para depois.
  • Use múltiplos valores de retorno: Funções que podem falhar geralmente retornam o resultado e um erro. Isso é mais explícito do que lançar exceções.
  • Prefira sentinelas a booleanos: Em vez de retornar um booleano indicando sucesso, retorne um erro. Isso fornece mais contexto.
  • Não abuse de panic/recover: panic é para erros irrecuperáveis, como falhas de programação. Use erros para situações esperadas.
  • Encadeie erros com %w: A partir do Go 1.13, você pode usar %w em fmt.Errorf para criar uma cadeia de erros, permitindo que o erro original seja desempacotado com errors.Is ou errors.As.

Exemplo de encadeamento:

package main

import (
    "errors"
    "fmt"
)

func foo() error {
    return fmt.Errorf("foo: %w", errors.New("erro interno"))
}

func main() {
    err := foo()
    if errors.Is(err, errors.New("erro interno")) {
        fmt.Println("erro interno detectado")
    }
    fmt.Println(err)
}

Saída:

foo: erro interno

Outra prática idiomática é usar nomes de variáveis como err para erros. Evite nomes como error ou erro.

Por fim, lembre-se de que erros são valores: você pode armazená-los, compará-los e passá-los por funções. Trate-os com a mesma seriedade que qualquer outro dado.

Boas práticas e observações finais

Algumas dicas para escrever código Go robusto:

  • Sempre documente os erros que sua função pode retornar, especialmente se forem de tipos personalizados.
  • Use defer para garantir que recursos sejam liberados, mesmo em caso de erro.
  • Evite repetir mensagens de erro; crie erros reutilizáveis quando apropriado.
  • Em pacotes públicos, considere exportar tipos de erro para que os usuários possam fazer type assertion ou switch.
  • Teste cenários de erro tanto quanto cenários de sucesso.

O tratamento de erros em Go é uma das suas características mais distintivas. Dominá-lo é essencial para escrever código confiável e de fácil manutenção.

Referências

Exercícios

  1. Crie uma função que divide dois números float64 e retorna o resultado e um erro. Se o divisor for zero, retorne um erro com a mensagem "divisão por zero".

    ✓ Resposta:
    func divide(a, b float64) (float64, error) {
        if b == 0 {
            return 0, errors.New("divisão por zero")
        }
        return a / b, nil
    }
  2. Escreva um programa que tenta abrir um arquivo chamado "config.json" e, se houver erro, imprime "Erro: [mensagem do erro]" e encerra. Use if err != nil.

    ✓ Resposta:
    package main
    
    import (
        "fmt"
        "os"
    )
    
    func main() {
        _, err := os.Open("config.json")
        if err != nil {
            fmt.Println("Erro:", err)
            return
        }
        fmt.Println("Arquivo aberto com sucesso")
    }
  3. Crie um tipo de erro personalizado chamado ValidationError que tenha um campo Field (string) e Value (interface{}). Implemente o método Error() string para retornar "campo [Field] inválido: [Value]".

    ✓ Resposta:
    type ValidationError struct {
        Field string
        Value interface{}
    }
    
    func (e *ValidationError) Error() string {
        return fmt.Sprintf("campo %s inválido: %v", e.Field, e.Value)
    }
  4. Usando fmt.Errorf com %w, escreva uma função que chama outra função que retorna um erro e o encapsula com uma mensagem adicional. Depois, use errors.Is para verificar se o erro original está presente.

    ✓ Resposta:
    package main
    
    import (
        "errors"
        "fmt"
    )
    
    func inner() error {
        return errors.New("erro interno")
    }
    
    func outer() error {
        err := inner()
        if err != nil {
            return fmt.Errorf("outer: %w", err)
        }
        return nil
    }
    
    func main() {
        err := outer()
        if errors.Is(err, errors.New("erro interno")) {
            fmt.Println("erro interno encontrado")
        }
    }
  5. Escreva uma função que recebe uma string e retorna um erro se a string estiver vazia. Use errors.New para criar o erro. Em seguida, chame a função em main e trate o erro de forma idiomática.

    ✓ Resposta:
    package main
    
    import (
        "errors"
        "fmt"
    )
    
    func validate(s string) error {
        if s == "" {
            return errors.New("string vazia")
        }
        return nil
    }
    
    func main() {
        err := validate("")
        if err != nil {
            fmt.Println("Erro:", err)
            return
        }
        fmt.Println("String válida")
    }