Padrões de Desenvolvimento em Golang
Estrutura de Projetos​
Organização de Diretórios​
Nossos projetos seguem a estrutura padrão do Go com algumas convenções especÃficas:
projeto/
├ ── cmd/ # Ponto de entrada da aplicação
│ └── main.go
├── internal/ # Código privado da aplicação
│ ├── config/ # Configurações
│ ├── domain/ # Entidades e interfaces de repositório
│ │ ├── entity/
│ │ └── repository/
│ ├── dto/ # Data Transfer Objects
│ ├── errors/ # Erros customizados
│ ├── gateway/ # Integrações externas
│ ├── handler/ # Handlers HTTP/gRPC
│ ├── infra/ # Infraestrutura
│ │ ├── cache/
│ │ ├── db/
│ │ ├── repository/
│ │ └── server/
│ ├── middleware/ # Middlewares
│ ├── service/ # Lógica de negócio
│ ├── usecase/ # Casos de uso
│ └── utils/ # Utilitários
├── pkg/ # Código público/reutilizável
│ ├── config/
│ ├── metrics/
│ └── pb/ # Protocol Buffers
├── vendor/ # Dependências (go mod vendor)
├── go.mod
├── go.sum
├── Makefile
├── Dockerfile
└── README.md
Arquitetura em Camadas​
Seguimos Clean Architecture com as seguintes camadas:
- Domain: Entidades e interfaces de repositório
- UseCase: Regras de negócio e orquestração
- Service: Coordenação de casos de uso
- Handler: Camada de apresentação (HTTP/gRPC)
- Repository: Acesso a dados
- Gateway: Comunicação com serviços externos
Convenções de Nomenclatura​
Pacotes​
- Nomes em lowercase, sem underscores
- Nomes curtos e descritivos
- Exemplos:
config,repository,usecase,handler
Variáveis e Funções​
- camelCase para variáveis privadas:
userService,dbConnection - PascalCase para exportadas:
NewUserService,GetByID - Constantes em PascalCase ou UPPER_SNAKE_CASE:
const (
StatusHired = "hired"
UserStatusSandbox UserStatus = "sandbox"
)
Structs e Interfaces​
- PascalCase para tipos exportados
- Interfaces geralmente terminam com sufixo descritivo ou sem sufixo
- Exemplos:
type UserService struct {}
type UserRepository interface {}
type GatewayKeycloak interface {}
Métodos​
- Receivers com 1-2 letras minúsculas:
func (s *UserService) CreateUser(ctx context.Context, request *dto.UserCreateRequest) {}
func (u UseCaseImpl) GetShipper(ctx context.Context, registerNumber string) {}
Padrões de Código​
Configuração​
Utilizamos envconfig para carregar variáveis de ambiente:
package config
import "github.com/kelseyhightower/envconfig"
type Config struct {
Debug bool `envconfig:"debug"`
HTTPPort int `envconfig:"http_port" default:"80"`
GRPCPort int `envconfig:"grpc_port" default:"50050"`
DBHost string `envconfig:"db_host"`
DBPort string `envconfig:"db_port"`
DBUser string `envconfig:"db_user"`
DBPass string `envconfig:"db_pass"`
DBName string `envconfig:"db_name"`
AddrRedis string `envconfig:"redis_addr"`
}
func New() (*Config, error) {
cfg := &Config{}
if err := envconfig.Process("app_prefix", cfg); err != nil {
return nil, err
}
return cfg, nil
}
Entidades​
package entity
import "time"
type User struct {
ID int64
Description string
ClientID string
ExternalID string
CompanyID int64
Status UserStatus
CreatedAt time.Time
UpdatedAt time.Time
}
type UserStatus string
const (
UserStatusSandbox UserStatus = "sandbox"
UserStatusProduction UserStatus = "production"
)
Repositórios​
Interfaces no pacote domain/repository, implementações em infra/repository usando sqlc:
// domain/repository/user_repository.go
package repository
type UserRepository interface {
GetByID(ctx context.Context, id int64) (*entity.User, *errors.CustomError)
Create(ctx context.Context, user *entity.User) *errors.CustomError
Update(ctx context.Context, user *entity.User) *errors.CustomError
}
// infra/repository/user_repository.go
package repository
import (
"github.com/freterapido/api/internal/infra/db/sqlc"
)
type userRepository struct {
queries *sqlc.Queries
}
func NewUserRepository(queries *sqlc.Queries) repository.UserRepository {
return &userRepository{queries: queries}
}
func (r *userRepository) GetByID(ctx context.Context, id int64) (*entity.User, *errors.CustomError) {
user, err := r.queries.GetUserByID(ctx, id)
if err != nil {
return nil, errors.NewCustomError(codes.Internal, errors.ErrInfraError, "erro ao buscar usuário", err)
}
return mapToEntity(user), nil
}
SQLC - Configuração​
Arquivo sqlc.yaml na raiz do projeto:
version: "2"
sql:
- engine: "mysql"
queries: "internal/infra/db/sql/queries"
schema: "internal/infra/db/sql/schema"
gen:
go:
package: "sqlc"
out: "internal/infra/db/sqlc"
emit_json_tags: true
emit_interface: false
emit_empty_slices: true
Estrutura de diretórios para sqlc:
internal/infra/db/
├── sql/
│ ├── schema/ # Schemas SQL
│ │ ├── 001_users.sql
│ │ └── 002_groups.sql
│ └── queries/ # Queries SQL
│ ├── users.sql
│ └─ ─ groups.sql
├── sqlc/ # Código gerado pelo sqlc
│ ├── db.go
│ ├── models.go
│ ├── users.sql.go
│ └── groups.sql.go
└── database.go
Exemplo de query SQL (internal/infra/db/sql/queries/users.sql):
-- name: GetUserByID :one
SELECT * FROM users WHERE id = ? LIMIT 1;
-- name: ListUsers :many
SELECT * FROM users ORDER BY created_at DESC;
-- name: CreateUser :execresult
INSERT INTO users (
description, client_id, company_id, status
) VALUES (
?, ?, ?, ?
);
-- name: UpdateUser :exec
UPDATE users
SET description = ?, status = ?, updated_at = NOW()
WHERE id = ?;
-- name: DeleteUser :exec
DELETE FROM users WHERE id = ?;
Services​
package service
type UserService struct {
cfg *config.Config
repositories *repository.Repository
useCases usecase.UseCase
}
func NewUserService(
cfg *config.Config,
repositories *repository.Repository,
useCases usecase.UseCase,
) *UserService {
return &UserService{
cfg: cfg,
repositories: repositories,
useCases: useCases,
}
}
func (s *UserService) CreateUser(ctx context.Context, request *dto.UserCreateRequest) (*dto.UserResponse, *errors.CustomError) {
logger := logrus.WithFields(logrus.Fields{
"service": "UserService",
"method": "CreateUser",
})
// Lógica do serviço
return response, nil
}
Tratamento de Erros​
Utilizamos erros customizados com códigos gRPC:
package errors
import "google.golang.org/grpc/codes"
type CustomError struct {
Code codes.Code
Type string
Message string
Err error
}
func NewCustomError(code codes.Code, errType, message string, err error) *CustomError {
return &CustomError{
Code: code,
Type: errType,
Message: message,
Err: err,
}
}
var (
ErrBusinessValidation = "business_validation"
ErrInfraError = "infra_error"
ErrInternalError = "internal_error"
)
Logging​
Utilizamos logrus para logging estruturado:
logger := logrus.WithFields(logrus.Fields{
"method": "CreateUser",
"user_id": userID,
"company_id": companyID,
})
logger.Info("Iniciando criação de usuário")
logger.WithError(err).Error("Erro ao criar usuário")
logger.Warn("Usuário não encontrado")
Context​
Sempre passar context.Context como primeiro parâmetro:
func (s *Service) GetUser(ctx context.Context, id int64) (*entity.User, error) {
// implementação
}
Pacotes Comuns Utilizados​
Pacotes Internos​
-
go-common: Biblioteca interna com utilitários compartilhados
cache: Wrapper para Redisqueue: Wrapper para AWS SQSstorage: Abstração para S3/Localstrutils: Manipulação de stringstimeutils: Utilitários de tempotypes: Tipos customizados (CPF/CNPJ, CEP)httputils: Helpers HTTPoccurrences: Status de rastreamentoiotrack: Logging de comunicação entre serviços
-
sdk-integracao: SDK para integrações com embarcadores e plataformas
- Gerenciamento de contas (Create/Update/List)
- Validação de acesso
- Cotação e contratação de fretes
- Webhooks (NF-e)
- CRON jobs para rastreio
- Helpers e utilitários
-
sdk-transportadora: SDK para integrações com transportadoras
- Gerenciamento de contas
- Solicitação e cancelamento de coletas
- Cotação
- Webhooks
- CRON jobs de rastreio
-
gormq: Cliente RabbitMQ customizado
Pacotes Externos Principais​
Configuração e CLI​
"github.com/kelseyhightower/envconfig" // Variáveis de ambiente
"github.com/spf13/cobra" // CLI commands
Banco de Dados​
"github.com/go-sql-driver/mysql" // Driver MySQL
"github.com/sqlc-dev/sqlc" // Gerador de código SQL type-safe
HTTP e Web​
"github.com/go-chi/chi/v5" // Router HTTP
"github.com/grpc-ecosystem/grpc-gateway/v2/runtime" // gRPC Gateway
gRPC​
"google.golang.org/grpc"
"google.golang.org/protobuf"
"google.golang.org/genproto/googleapis/api"
Cache e Mensageria​
"github.com/go-redis/redis/v8" // Redis
"github.com/rabbitmq/amqp091-go" // RabbitMQ
Validação​
"github.com/go-playground/validator/v10"
Logging e Métricas​
"github.com/sirupsen/logrus" // Logging estruturado
"github.com/prometheus/client_golang" // Métricas Prometheus
Utilitários​
"github.com/google/uuid" // UUID
"github.com/golang-migrate/migrate/v4" // Migrations
Padrões de Inicialização​
Main.go com Cobra​
package main
import (
"github.com/sirupsen/logrus"
"github.com/spf13/cobra"
)
var rootCmd = &cobra.Command{
Short: "Application Name",
}
func main() {
logger := logrus.WithField("function", "main")
logger.Info("START APPLICATION")
cfg, err := config.Load()
if err != nil {
logger.WithError(err).Error("failed to load config")
panic(err)
}
dbConn, err := db.NewConnection(cfg)
if err != nil {
logger.WithError(err).Error("failed to connect to database")
panic(err)
}
defer dbConn.Close()
// Inicializar dependências
repositories := repository.NewRepository(dbConn)
useCases := usecase.NewUseCase(cfg, repositories)
services := service.NewService(cfg, repositories, useCases)
// Adicionar comandos
rootCmd.AddCommand(runServer(cfg, services))
rootCmd.AddCommand(runWorker(cfg, services))
rootCmd.AddCommand(runMigration(dbConn))
if err := rootCmd.Execute(); err != nil {
panic(err)
}
}
func runServer(cfg *config.Config, services *service.Service) *cobra.Command {
return &cobra.Command{
Use: "server",
Short: "Run HTTP/gRPC server",
Run: func(cmd *cobra.Command, args []string) {
var exit = make(chan error)
go (func() { exit <- runHTTPServer(cfg, services) })()
go (func() { exit <- runGRPCServer(cfg, services) })()
if err := <-exit; err != nil {
panic(err)
}
},
}
}
Testes​
Estrutura de Testes​
package service
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
)
func TestUserService_CreateUser(t *testing.T) {
// Arrange
mockRepo := new(MockUserRepository)
service := NewUserService(cfg, mockRepo, useCases)
mockRepo.On("Create", mock.Anything, mock.Anything).Return(nil)
// Act
result, err := service.CreateUser(ctx, request)
// Assert
assert.NoError(t, err)
assert.NotNil(t, result)
mockRepo.AssertExpectations(t)
}
Boas Práticas​
- Sempre use context.Context como primeiro parâmetro
- Defer para cleanup:
defer conn.Close(),defer cancel() - Logging estruturado com campos relevantes
- Erros customizados com contexto adequado
- Validação de entrada em DTOs
- Concorrência segura com mutexes quando necessário
- Goroutines para operações assÃncronas não crÃticas
- Channels para comunicação entre goroutines
- Timeouts e cancelamento de contexto
- Métricas Prometheus para observabilidade
Makefile Padrão​
.PHONY: help
help:
@echo "Comandos disponÃveis:"
@echo " make run - Executa a aplicação"
@echo " make test - Executa os testes"
@echo " make build - Compila a aplicação"
@echo " make docker-build - Cria imagem Docker"
.PHONY: run
run:
go run cmd/main.go server
.PHONY: test
test:
go test -v ./...
.PHONY: build
build:
go build -o bin/app cmd/main.go
.PHONY: docker-build
docker-build:
docker build -t app:latest .
.PHONY: migrate-up
migrate-up:
go run cmd/main.go migrate-up
.PHONY: migrate-down
migrate-down:
go run cmd/main.go migrate-down
Dockerfile Padrão​
FROM golang:1.24-alpine AS builder
WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build -o /app/server cmd/main.go
FROM alpine:latest
RUN apk --no-cache add ca-certificates tzdata
WORKDIR /root/
COPY --from=builder /app/server .
EXPOSE 8080
CMD ["./server", "server"]