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: