Skip to main content

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:

  1. Domain: Entidades e interfaces de repositório
  2. UseCase: Regras de negócio e orquestração
  3. Service: Coordenação de casos de uso
  4. Handler: Camada de apresentação (HTTP/gRPC)
  5. Repository: Acesso a dados
  6. 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 Redis
    • queue: Wrapper para AWS SQS
    • storage: Abstração para S3/Local
    • strutils: Manipulação de strings
    • timeutils: Utilitários de tempo
    • types: Tipos customizados (CPF/CNPJ, CEP)
    • httputils: Helpers HTTP
    • occurrences: Status de rastreamento
    • iotrack: 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​

  1. Sempre use context.Context como primeiro parâmetro
  2. Defer para cleanup: defer conn.Close(), defer cancel()
  3. Logging estruturado com campos relevantes
  4. Erros customizados com contexto adequado
  5. Validação de entrada em DTOs
  6. Concorrência segura com mutexes quando necessário
  7. Goroutines para operações assíncronas não críticas
  8. Channels para comunicação entre goroutines
  9. Timeouts e cancelamento de contexto
  10. 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"]

Referências​