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: