
Go é uma linguagem simples. Propositalmente simples. Não herança, não generics complexos (até 1.18), não exceções. Essa simplicidade é uma vantagem – mas também pode levar a código bagunçado se você não aplicar padrões de projeto adequados.
Padrões de projeto não são receitas de bolo. São soluções testadas para problemas recorrentes. Em Go, aplicamos padrões de forma idiomática, sem forçar paradigmas de outras linguagens.
Neste post, vamos explorar os padrões de projeto mais úteis para Go: injeção de dependência, interfaces pequenas, Repository, Factory, Strategy, e como evitar armadilhas comuns. Tudo com exemplos práticos e testáveis.
Por que padrões de projeto em Go?
Muitos desenvolvedores vindos de Java ou C# tentam recriar padrões “clássicos” em Go – e falham. O segredo é adaptar, não traduzir literalmente.
Princípios Go-friendly:
- Interfaces pequenas – quanto menor, melhor (ex:
io.Reader,io.Writer). - Composição sobre herança – use struct embedding, não hierarquias complexas.
- Erros como valores – não exceções, então padrões mudam.
- Concorrência via canais – padrões de design para goroutines.
1. Injeção de Dependência (DI) sem frameworks
Go não precisa de frameworks DI pesados (como Spring). A injeção é feita manualmente, via construtores e interfaces.
Exemplo ruim (acoplamento rígido)
type UserService struct {
db *sql.DB
}
func NewUserService() *UserService {
db, _ := sql.Open("postgres", "hardcoded-conn")
return &UserService{db: db}
}
Testar isso exige banco real. Péssimo.
Exemplo bom (injeção via construtor)
type UserRepository interface {
FindByID(ctx context.Context, id string) (*User, error)
}
type PostgresUserRepository struct {
db *sql.DB
}
func (r *PostgresUserRepository) FindByID(ctx context.Context, id string) (*User, error) {
// implementação real
}
type UserService struct {
repo UserRepository
}
func NewUserService(repo UserRepository) *UserService {
return &UserService{repo: repo}
}
Teste fácil:
type MockUserRepository struct {
user *User
err error
}
func (m *MockUserRepository) FindByID(ctx context.Context, id string) (*User, error) {
return m.user, m.err
}
func TestUserService(t *testing.T) {
mockRepo := &MockUserRepository{user: &User{ID: "1", Name: "Test"}}
svc := NewUserService(mockRepo)
// testa...
}
2. Repository Pattern
O padrão Repository isola a lógica de acesso a dados. É natural em Go:
type UserRepository interface {
Save(ctx context.Context, user *User) error
FindByID(ctx context.Context, id string) (*User, error)
FindByEmail(ctx context.Context, email string) (*User, error)
Delete(ctx context.Context, id string) error
}
// Implementação com PostgreSQL
type PGUserRepository struct {
db *sql.DB
}
func (r *PGUserRepository) Save(ctx context.Context, user *User) error {
_, err := r.db.ExecContext(ctx, "INSERT INTO users ...", user.ID, user.Name)
return err
}
// Implementação com Redis (cache)
type RedisUserRepository struct {
client *redis.Client
}
Benefício: Troque a implementação de banco sem alterar o serviço.
3. Factory Pattern
Criação de objetos complexos sem expor a lógica. Em Go, funções com prefixo New são fábricas.
// Simple factory
func NewUser(name, email string) *User {
return &User{
ID: uuid.New().String(),
Name: name,
Email: email,
CreatedAt: time.Now(),
}
}
// Factory com configuração (Functional Options pattern)
type ServerConfig struct {
Port int
Timeout time.Duration
}
type ServerOption func(*ServerConfig)
func WithPort(port int) ServerOption {
return func(c *ServerConfig) {
c.Port = port
}
}
func WithTimeout(timeout time.Duration) ServerOption {
return func(c *ServerConfig) {
c.Timeout = timeout
}
}
func NewServer(opts ...ServerOption) *http.Server {
cfg := &ServerConfig{Port: 8080, Timeout: 30 * time.Second}
for _, opt := range opts {
opt(cfg)
}
return &http.Server{
Addr: fmt.Sprintf(":%d", cfg.Port),
ReadTimeout: cfg.Timeout,
WriteTimeout: cfg.Timeout,
}
}
// Uso
server := NewServer(WithPort(9090), WithTimeout(60*time.Second))
Esse padrão Functional Options é idiomático e muito usado em bibliotecas Go famosas (como gRPC, Uber Zap).
4. Strategy Pattern
Permite mudar o comportamento de um objeto em tempo de execução. Em Go, use interfaces.
// Strategy interface
type PaymentStrategy interface {
Pay(amount float64) error
}
// Concrete strategies
type CreditCardStrategy struct {
cardNumber string
}
func (c *CreditCardStrategy) Pay(amount float64) error {
fmt.Printf("Pagando %.2f via cartão %s\n", amount, c.cardNumber)
return nil
}
type PixStrategy struct {
pixKey string
}
func (p *PixStrategy) Pay(amount float64) error {
fmt.Printf("Pagando %.2f via PIX chave %s\n", amount, p.pixKey)
return nil
}
// Context
type Checkout struct {
strategy PaymentStrategy
}
func (c *Checkout) SetStrategy(s PaymentStrategy) {
c.strategy = s
}
func (c *Checkout) ProcessOrder(amount float64) error {
return c.strategy.Pay(amount)
}
5. Worker Pool Pattern (para concorrência)
Go é famoso por concorrência. O padrão worker pool usa canais e goroutines.
type WorkerPool struct {
tasks chan Task
wg sync.WaitGroup
}
func NewWorkerPool(workers int) *WorkerPool {
wp := &WorkerPool{
tasks: make(chan Task, 100),
}
for i := 0; i < workers; i++ {
wp.wg.Add(1)
go wp.worker()
}
return wp
}
func (wp *WorkerPool) worker() {
defer wp.wg.Done()
for task := range wp.tasks {
task.Execute()
}
}
func (wp *WorkerPool) Submit(task Task) {
wp.tasks <- task
}
func (wp *WorkerPool) Stop() {
close(wp.tasks)
wp.wg.Wait()
}
6. Pipeline Pattern (processamento de dados)
Ideal para transformações em estágios, usando canais.
func generate(nums ...int) <-chan int {
out := make(chan int)
go func() {
for _, n := range nums {
out <- n
}
close(out)
}()
return out
}
func square(in <-chan int) <-chan int {
out := make(chan int)
go func() {
for n := range in {
out <- n * n
}
close(out)
}()
return out
}
func main() {
pipeline := square(generate(1, 2, 3, 4))
for result := range pipeline {
fmt.Println(result)
}
}
7. Padrões para evitar em Go
❌ Herança profunda – Go não tem herança de classes. Use composição.
❌ Singleton com sync.Once sem necessidade – Injeção de dependência é melhor.
❌ Visitor Pattern – Muito verboso em Go. Prefira type switches ou funções.
❌ Abstract Factory – Complexo demais. Use funções simples.
Interfaces pequenas: o segredo do design em Go
A biblioteca padrão do Go é uma aula. Veja io.Reader:
type Reader interface {
Read(p []byte) (n int, err error)
}
Apenas um método. Isso permite que qualquer coisa que tenha Read seja usada onde io.Reader é esperado.
Regra: Quanto menor a interface, mais útil e reutilizável.
Testabilidade com interfaces
Interfaces são a chave para testes unitários em Go. Em vez de usar o banco real, implemente uma interface e use mock.
type EmailSender interface {
Send(to, subject, body string) error
}
type MockEmailSender struct{}
func (m *MockEmailSender) Send(to, subject, body string) error {
// apenas loga, não envia
return nil
}
Armadilhas comuns e como evitar
| Armadilha | Solução |
|---|---|
| Interfaces gigantes | Divida em interfaces pequenas (Interface Segregation) |
Retornar interface{} | Use generics (Go 1.18+) ou tipos específicos |
| Acoplamento de pacotes circulares | Use interfaces no pacote que depende, não no provedor |
| Uso excessivo de ponteiros | Use valores para dados pequenos, ponteiros para mutáveis grandes |
nil receiver em métodos | Verifique if x == nil no início do método |
Conclusão: Padrões sim, dogma não
Padrões de projeto em Go são ferramentas, não regras. Aplique injeção de dependência para testabilidade, use interfaces pequenas para flexibilidade, adote functional options para configuração, e abuse da concorrência com canais.
O código Go idiomático é simples, direto e fácil de modificar. Na Jacobus Software, seguimos esses princípios para entregar sistemas que são um prazer de manter – mesmo anos depois.
🧱 Quer elevar a qualidade e manutenibilidade do seu código Go?
Nossos especialistas revisam sua base, aplicam padrões de projeto e ensinam seu time a escrever Go limpo, testável e de alta performance.
