Organização de código é um dos pontos principais para a manutenção, adição de novas regras de negócio e também de aprendizado. Um projeto, que ao longo do seu ciclo de desenvolvimento sofreu com mudanças drásticas, tende a ter um código menos organizado. E poder organizá-lo durante o desenvolvimento traz diversos benefícios.
Pensando em como organizar um projeto ou código, noto que muitas vezes nós programadores deixamos passar pequenos detalhes, que podem até parecer supérfluos, e muitas vezes simples, mas que podem trazer um ganho real de como estruturamos o nosso projeto.
Dessa vez, eu venho apresentar uma maneira simples de lidar com erros e logs em um projeto em Golang, o que vai ser demonstrado aqui também poderá ser aplicado em qualquer outra linguagem, framework ou projeto. Não é algo exclusivo em Golang, mas é um padrão que vejo em diversos projetos que trabalhei, em PHP, Python, Javascript e outros.
O projeto
O projeto ou a aplicação em questão estava fazendo o tratamento de um erro retornado pelo banco de dados, o erro é retornado, mas também cria um log desse mesmo erro.
Vamos ao exemplo (de momento não se preocupe, algumas partes foram omitidas, mas tudo estará em um repositório no final do artigo):
package movies import ( "encoding/json" "github.com/labstack/echo/v4" bolt "go.etcd.io/bbolt" ) type Datasource struct { db *bolt.DB logger echo.Logger } var bucketName = []byte("movies") func NewDatasource(db *bolt.DB, logger echo.Logger) *Datasource { return &Datasource{db: db, logger: logger} } func (ds *Datasource) Store(m Movie) (Movie, error) { err := ds.db.Update(func(tx *bolt.Tx) error { bucket, err := tx.CreateBucketIfNotExists(bucketName) if err != nil { return err } entry, err := json.Marshal(m) if err != nil { return err } return bucket.Put([]byte(m.ID), entry) }) if err != nil { ds.logger.Error(err) return m, err } return m, nil }
Como foi comentado, independente do erro retornado, um log será criado:
if err != nil { ds.logger.Error(err) return m, err }
Olhando rapidamente parece que não é nada fora do comum, mas isso é algo recorrente em diversos projetos que trabalhei.
Mas isso é um problema?
Mesmo parecendo inofensivo, isso nos faz pensar em alguns pontos:
- É necessário criar um log uma vez que o erro é retornado?
- Erros como esse de banco de dados devem ser apresentados diretamente ao cliente (api client)?
- Como a aplicação lida com os erros retornados?
Existem pelo menos três pontos onde podem ser melhorados, vamos a cada um deles em passos separados, fazendo melhorias pontuais.
1. Removendo a criação do log
Sendo honesto, essa etapa é a mais simples de todas, apenas vamos remover a criação de log e toda a dependência em torno dele.
diff --git a/internal/movies/datasource.go b/internal/movies/datasource.go --- a/internal/movies/datasource.go +++ b/internal/movies/datasource.go @@ -3,19 +3,17 @@ package movies import ( "encoding/json" - "github.com/labstack/echo/v4" bolt "go.etcd.io/bbolt" ) type Datasource struct { - db *bolt.DB - logger echo.Logger + db *bolt.DB } var bucketName = []byte("movies") -func NewDatasource(db *bolt.DB, logger echo.Logger) *Datasource { - return &Datasource{db: db, logger: logger} +func NewDatasource(db *bolt.DB) *Datasource { + return &Datasource{db: db} } func (ds *Datasource) Store(m Movie) (Movie, error) { @@ -33,7 +31,6 @@ func (ds *Datasource) Store(m Movie) (Movie, error) { return bucket.Put([]byte(m.ID), entry) }) if err != nil { - ds.logger.Error(err) return m, err }
Mas fica o questionamento, não vamos criar logs em caso de erros?
Sim, o log de erro será criado, vamos delegar essa funcionalidade para a camada de tratamento de erros (error handling), que é umas das partes cruciais de toda a aplicação. Por agora não vamos nos aprofundar nisso, baby steps.
2. Tratando e apresentando erros para os clientes (api clients)
Agora começamos uma parte bem interessante. Para esse trecho deveríamos identificar o tipo de erro retornado e apresentar de uma maneira mais simples para o cliente, omitindo o erro original do banco de dados.
Ao invés de responder para o cliente com a mensagem de erro database is in read-only mode
, podemos retornar unable to store the movie
, omitindo o erro original.
No Golang é possível encapsular um erro para que contenha o erro a ser apresentado e erro original, a biblioteca padrão da linguagem já nos dá essa funcionalidade, mas de forma mais simplista, é o caso do fmt.Errorf
, mas para o nosso caso vamos criar o nosso próprio "wrap" de erro:
diff --git a/internal/errors/public.go b/internal/errors/public.go --- /dev/null +++ b/internal/errors/public.go @@ -0,0 +1,29 @@ +package errors + +type publicErr struct { + err error + msg string +} + +func (e *publicErr) Error() string { + return e.err.Error() +} + +func (e *publicErr) Public() string { + return e.msg +} + +func (e *publicErr) Unwrap() error { + return e.err +} + +func Public(err error, msg string) error { + if err == nil { + return nil + } + + return &publicErr{ + err: err, + msg: msg, + } +}
Esse novo wrap irá encapsular o erro original do banco de dados, e aceitará uma mensagem de error mais agradável que será exibida para o cliente.
diff --git a/internal/movies/datasource.go b/internal/movies/datasource.go --- a/internal/movies/datasource.go +++ b/internal/movies/datasource.go @@ -4,6 +4,8 @@ import ( "encoding/json" bolt "go.etcd.io/bbolt" + + ierr "github.com/faabiosr/go-movies-demo/internal/errors" ) type Datasource struct { @@ -31,7 +33,7 @@ func (ds *Datasource) Store(m Movie) (Movie, error) { return bucket.Put([]byte(m.ID), entry) }) if err != nil { - return m, err + return m, ierr.Public(err, "unable to save the movie") } return m, nil
Como ainda não estamos lidando com esse novo tipo de erro, o erro original ainda será exibido pois a função Error
do nosso wrap retorna o erro original. Para isso necessitamos criar a camada que lida com os erros de fato, ela sim, irá identificar o publicErr
wrap, criar o log do erro original e também apresentar a mensagem pública do erro.
3. Error handling
Agora já não temos nenhum log na camada de negócio e também já preparamos o erro com o publicErr
wrap, só faltará identificar o erro e apresentar.
Para esse exemplo, estamos usando o framework Echo, ele internamente tem a sua própria camada que lida com os erros, mas vamos criar uma nova.
diff --git a/internal/errors/handler.go b/internal/errors/handler.go new file mode 100644 index 0000000..e2ad2d1 --- /dev/null +++ b/internal/errors/handler.go @@ -0,0 +1,42 @@ +package errors + +import ( + "errors" + "net/http" + + "github.com/labstack/echo/v4" +) + +func ErrorHandler(logger echo.Logger) echo.HTTPErrorHandler { + return func(err error, ec echo.Context) { + var status int + var msg string + + if ee := new(echo.HTTPError); errors.As(err, &ee) { + status = ee.Code + msg = ee.Error() + + var pe interface { + Public() string + } + + if errors.As(ee.Message.(error), &pe) { + msg = pe.Public() + } + } + + if ec.Response().Committed { + return + } + + if ec.Request().Method == http.MethodHead { + err = ec.NoContent(status) + } else { + err = ec.JSON(status, echo.Map{"message": msg}) + } + + if err != nil { + logger.Error(err) + } + } +}
O ErrorHandler
irá verificar se o tipo de erro retornado é o padrão do Echo
, e depois se o erro definido segue a interface do Public
, com isso a nova mensagem será definida.
4. Usando o novo Error Handling
Tudo pronto, o último passo é usar o novo pacote e ver o resultado, no exemplo abaixo, temos um único endpoint que retornará erro, em caso de o banco de dados estiver no modo leitura.
package main import ( "net/http" "github.com/labstack/echo/v4" mw "github.com/labstack/echo/v4/middleware" glog "github.com/labstack/gommon/log" bolt "go.etcd.io/bbolt" ierr "github.com/faabiosr/go-movies-demo/internal/errors" "github.com/faabiosr/go-movies-demo/internal/movies" ) func main() { e := echo.New() e.HTTPErrorHandler = ierr.ErrorHandler(e.Logger) e.Logger.SetLevel(glog.INFO) e.Use(mw.Logger()) db, err := bolt.Open("catalog.db", 0o400, &bolt.Options{ReadOnly: true}) if err != nil { e.Logger.Fatal(err) } ds := movies.NewDatasource(db) e.POST("/movies", func(ec echo.Context) error { m := movies.Movie{} if err := ec.Bind(&m); err != nil { return err } m, err := ds.Store(m) if err != nil { return echo.NewHTTPError(http.StatusInternalServerError, err) } return ec.JSON(http.StatusCreated, m) }) e.Logger.Fatal(e.Start(":8000")) }
Ao executar o app, e fazer uma chamada, esse será o resultado final:
Log de erros interno do servidor:
{"time":"2025-05-26T20:28:58.679194618+02:00","id":"","remote_ip":"127.0.0.1","host":"localhost:8000","method":"POST","uri":"/movies","user_agent":"HTTPie/3.2.2","status":500,"error":"code=500, message=database is in read-only mode","latency":61539,"latency_human":"61.539µs","bytes_in":0,"bytes_out":39}
Resposta dada ao cliente http:
HTTP/1.1 500 Internal Server Error Content-Length: 39 Content-Type: application/json Date: Mon, 26 May 2025 18:28:58 GMT { "message": "unable to save the movie" }
Conclusão
Bem, é isso, tentei ao máximo resumir o conteúdo, e dar exemplos práticos ao longo do caminho. O isolamento de pequenas partes do nosso código facilitarão ao desenvolver uma nova funcionalidade, e claro, a nossa vida como dev.
A aplicação completa usada no artigo está em https://github.com/faabiosr/go-movies-demo, você encontrará tudo lá.
Se você está afim de ter o seu próprio servidor, aqui vai um link de créditos para brincar na Digital Ocean e criar os seus droplets.
Recomendo a leitura das referências abaixo para entender um pouco mais sobre os erros no Golang:
Top comments (0)