Injeção de Dependência em Go Sem Frameworks
Vindo de Java ou C#, você pode querer usar um container de DI em Go. Não faça isso. A simplicidade do Go torna injeção de dependência manual não apenas viável, mas preferível. Frameworks como Spring resolvem problemas que Go não tem.
Aqui está como fazer DI em Go da forma idiomática.
Por Que Go Não Precisa de Containers de DI
Em Java, você precisa de containers de DI porque:
- Construtores são verbosos (sem parâmetros nomeados)
- Sem funções de primeira classe
- Configuração por annotations é a norma
- Grafos de objetos complexos com gerenciamento de ciclo de vida
Go não tem nenhum desses problemas:
- Struct literals com campos nomeados
- Funções de primeira classe
- Explícito é melhor que mágica
- Ciclos de vida de objetos simples (cria, usa, pronto)
A filosofia Go: se você pode ver o código, você pode entendê-lo. Containers de DI escondem a fiação.
Padrão 1: Injeção por Construtor
A base de DI em Go. Dependências vão no construtor.
// repository.go
type UserRepository struct {
db *sql.DB
}
func NewUserRepository(db *sql.DB) *UserRepository {
return &UserRepository{db: db}
}
func (r *UserRepository) GetByID(ctx context.Context, id string) (*User, error) {
// Usa r.db
}
// service.go
type UserService struct {
repo *UserRepository
cache Cache
mailer Mailer
}
func NewUserService(repo *UserRepository, cache Cache, mailer Mailer) *UserService {
return &UserService{
repo: repo,
cache: cache,
mailer: mailer,
}
}
Main como Composition Root
Toda fiação acontece em main(). Este é seu composition root—o único lugar onde você vê como tudo se conecta.
// main.go
func main() {
// Infraestrutura
db, err := sql.Open("postgres", os.Getenv("DATABASE_URL"))
if err != nil {
log.Fatal(err)
}
defer db.Close()
redisClient := redis.NewClient(&redis.Options{
Addr: os.Getenv("REDIS_URL"),
})
defer redisClient.Close()
smtpClient := smtp.NewClient(os.Getenv("SMTP_HOST"))
// Repositories
userRepo := NewUserRepository(db)
orderRepo := NewOrderRepository(db)
// Services
cache := NewRedisCache(redisClient)
mailer := NewSMTPMailer(smtpClient)
userService := NewUserService(userRepo, cache, mailer)
orderService := NewOrderService(orderRepo, userService)
// HTTP handlers
userHandler := NewUserHandler(userService)
orderHandler := NewOrderHandler(orderService)
// Router
mux := http.NewServeMux()
mux.Handle("/users", userHandler)
mux.Handle("/orders", orderHandler)
log.Fatal(http.ListenAndServe(":8080", mux))
}
Tudo é explícito. Sem mágica, sem reflection, sem arquivos XML. Abra main.go e você vê exatamente como sua app está conectada.
Padrão 2: Segregação de Interface
Defina interfaces onde são consumidas, não onde são implementadas. Isso é chave para código Go testável.
// ERRADO: Interface grande definida pelo implementador
// user/repository.go
type Repository interface {
GetByID(ctx context.Context, id string) (*User, error)
GetByEmail(ctx context.Context, email string) (*User, error)
Create(ctx context.Context, user *User) error
Update(ctx context.Context, user *User) error
Delete(ctx context.Context, id string) error
List(ctx context.Context, filter Filter) ([]*User, error)
Count(ctx context.Context) (int, error)
}
// BOM: Interface pequena definida pelo consumidor
// auth/service.go
type UserGetter interface {
GetByEmail(ctx context.Context, email string) (*User, error)
}
type AuthService struct {
users UserGetter // Só precisa de um método
}
func NewAuthService(users UserGetter) *AuthService {
return &AuthService{users: users}
}
Benefícios:
- Fácil de mockar em testes (um método para implementar)
- Dependências claras (você vê exatamente o que é necessário)
- Pacotes desacoplados (sem definições de interface compartilhadas)
// auth/service_test.go
type mockUserGetter struct {
user *User
err error
}
func (m *mockUserGetter) GetByEmail(ctx context.Context, email string) (*User, error) {
return m.user, m.err
}
func TestAuthService_Login(t *testing.T) {
mock := &mockUserGetter{
user: &User{ID: "123", Email: "test@example.com"},
}
svc := NewAuthService(mock)
// Testa...
}
Padrão 3: Functional Options
Quando construtores têm muitos parâmetros opcionais, use functional options.
type Server struct {
host string
port int
timeout time.Duration
maxConns int
logger Logger
metrics Metrics
tlsConfig *tls.Config
}
// Option é uma função que configura Server
type Option func(*Server)
func WithHost(host string) Option {
return func(s *Server) {
s.host = host
}
}
func WithPort(port int) Option {
return func(s *Server) {
s.port = port
}
}
func WithTimeout(d time.Duration) Option {
return func(s *Server) {
s.timeout = d
}
}
func WithLogger(l Logger) Option {
return func(s *Server) {
s.logger = l
}
}
func WithTLS(config *tls.Config) Option {
return func(s *Server) {
s.tlsConfig = config
}
}
func NewServer(opts ...Option) *Server {
// Valores padrão
s := &Server{
host: "localhost",
port: 8080,
timeout: 30 * time.Second,
maxConns: 100,
logger: defaultLogger,
metrics: noopMetrics,
}
// Aplica options
for _, opt := range opts {
opt(s)
}
return s
}
// Uso
server := NewServer(
WithHost("0.0.0.0"),
WithPort(443),
WithTLS(tlsConfig),
WithLogger(zapLogger),
)
Quando Usar Functional Options
- Muitos parâmetros opcionais (3+)
- Padrões sensatos existem para a maioria dos parâmetros
- API pública onde compatibilidade retroativa importa
- Configuração estilo builder onde ordem não importa
Não use para:
- Construtores simples com 1-3 parâmetros obrigatórios
- Código interno onde legibilidade vence flexibilidade
Padrão 4: Structs de Configuração
Para configuração complexa, uma struct de config é mais clara que muitas options:
type DatabaseConfig struct {
Host string
Port int
Database string
Username string
Password string
MaxOpenConns int
MaxIdleConns int
ConnMaxLifetime time.Duration
}
func NewDatabase(cfg DatabaseConfig) (*sql.DB, error) {
dsn := fmt.Sprintf("host=%s port=%d user=%s password=%s dbname=%s",
cfg.Host, cfg.Port, cfg.Username, cfg.Password, cfg.Database)
db, err := sql.Open("postgres", dsn)
if err != nil {
return nil, err
}
db.SetMaxOpenConns(cfg.MaxOpenConns)
db.SetMaxIdleConns(cfg.MaxIdleConns)
db.SetConnMaxLifetime(cfg.ConnMaxLifetime)
return db, nil
}
// Uso
db, err := NewDatabase(DatabaseConfig{
Host: "localhost",
Port: 5432,
Database: "myapp",
Username: "app",
Password: os.Getenv("DB_PASSWORD"),
MaxOpenConns: 25,
MaxIdleConns: 5,
ConnMaxLifetime: 5 * time.Minute,
})
Structs de config funcionam bem com parsing de ambiente:
func LoadDatabaseConfig() DatabaseConfig {
return DatabaseConfig{
Host: env.GetString("DB_HOST", "localhost"),
Port: env.GetInt("DB_PORT", 5432),
Database: env.GetString("DB_NAME", "myapp"),
Username: env.GetString("DB_USER", "postgres"),
Password: env.GetString("DB_PASSWORD", ""),
MaxOpenConns: env.GetInt("DB_MAX_OPEN_CONNS", 25),
}
}
Padrão 5: Wire para Aplicações Grandes
Para aplicações com muitas dependências, Wire do Google gera o código de fiação.
// wire.go
//go:build wireinject
package main
import "github.com/google/wire"
func InitializeApp() (*App, error) {
wire.Build(
// Infraestrutura
NewDatabase,
NewRedisClient,
// Repositories
NewUserRepository,
NewOrderRepository,
// Services
NewUserService,
NewOrderService,
// Handlers
NewUserHandler,
NewOrderHandler,
// App
NewApp,
)
return nil, nil
}
Execute wire e ele gera wire_gen.go:
// wire_gen.go (gerado)
func InitializeApp() (*App, error) {
db, err := NewDatabase()
if err != nil {
return nil, err
}
redisClient := NewRedisClient()
userRepository := NewUserRepository(db)
orderRepository := NewOrderRepository(db)
userService := NewUserService(userRepository)
orderService := NewOrderService(orderRepository, userService)
userHandler := NewUserHandler(userService)
orderHandler := NewOrderHandler(orderService)
app := NewApp(userHandler, orderHandler)
return app, nil
}
Provider Sets para Organização
Agrupe providers relacionados:
var DatabaseSet = wire.NewSet(
NewDatabase,
NewUserRepository,
NewOrderRepository,
)
var ServiceSet = wire.NewSet(
NewUserService,
NewOrderService,
NewPaymentService,
)
var HandlerSet = wire.NewSet(
NewUserHandler,
NewOrderHandler,
)
func InitializeApp() (*App, error) {
wire.Build(
DatabaseSet,
ServiceSet,
HandlerSet,
NewApp,
)
return nil, nil
}
Quando Usar Wire
- Aplicações grandes com 20+ dependências injetáveis
- Projetos em equipe onde fiação consistente importa
- Segurança em tempo de compilação é importante (Wire falha em compile time se fiação está errada)
Não use para:
- Aplicações pequenas a médias
- Projetos de aprendizado
- Quando fiação explícita em
main()ainda é gerenciável
Anti-Padrões para Evitar
1. Variáveis Globais
// ERRADO: Estado global
var db *sql.DB
var userRepo *UserRepository
func init() {
db, _ = sql.Open("postgres", os.Getenv("DATABASE_URL"))
userRepo = NewUserRepository(db)
}
func GetUser(id string) (*User, error) {
return userRepo.GetByID(context.Background(), id)
}
// CERTO: Injete dependências
type Handler struct {
users *UserRepository
}
func (h *Handler) GetUser(w http.ResponseWriter, r *http.Request) {
user, err := h.users.GetByID(r.Context(), id)
// ...
}
2. Service Locator
// ERRADO: Padrão service locator
type Container struct {
services map[string]any
}
func (c *Container) Get(name string) any {
return c.services[name]
}
func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
userService := container.Get("userService").(*UserService) // Erro em runtime se faltando
// ...
}
// CERTO: Dependências explícitas
type Handler struct {
users *UserService
}
func NewHandler(users *UserService) *Handler {
return &Handler{users: users} // Erro em compile-time se faltando
}
3. Dependências Escondidas
// ERRADO: Dependência escondida em time.Now
func (s *Service) CreateOrder(userID string) (*Order, error) {
return &Order{
ID: uuid.New().String(),
UserID: userID,
CreatedAt: time.Now(), // Dependência escondida, difícil testar
}
}
// CERTO: Injete função de tempo
type Service struct {
now func() time.Time
}
func NewService(now func() time.Time) *Service {
if now == nil {
now = time.Now
}
return &Service{now: now}
}
func (s *Service) CreateOrder(userID string) (*Order, error) {
return &Order{
ID: uuid.New().String(),
UserID: userID,
CreatedAt: s.now(), // Testável!
}
}
Testando com DI
A recompensa de DI adequado é testes fáceis:
func TestOrderService_Create(t *testing.T) {
// Stub de dependências
userGetter := &stubUserGetter{
user: &User{ID: "user-1", Name: "Test"},
}
orderRepo := &stubOrderRepo{}
nowFunc := func() time.Time {
return time.Date(2025, 1, 1, 0, 0, 0, 0, time.UTC)
}
// Cria service com dependências de teste
svc := NewOrderService(userGetter, orderRepo, nowFunc)
// Testa
order, err := svc.Create(context.Background(), CreateOrderRequest{
UserID: "user-1",
Items: []Item{{SKU: "ABC", Qty: 2}},
})
require.NoError(t, err)
assert.Equal(t, "user-1", order.UserID)
assert.Equal(t, nowFunc(), order.CreatedAt)
}
Escolhendo Sua Abordagem
- DI Manual: Apps pequenas-médias, dependências claras, controle total
- Functional Options: Muitos params opcionais, APIs públicas, bibliotecas
- Structs de Config: Configuração complexa, setup baseado em ambiente
- Wire: Apps grandes (20+ deps), projetos em equipe, segurança compile-time
Pontos-Chave
-
Go não precisa de containers de DI. Fiação manual é explícita, debugável e rápida.
-
Main é seu composition root. Toda fiação acontece lá—um lugar para ver o quadro completo.
-
Defina interfaces no consumidor. Interfaces pequenas são fáceis de mockar e reduzem acoplamento.
-
Functional options para params opcionais. Ótimo para APIs públicas, exagero para construtores simples.
-
Wire para aplicações grandes. Gera código de fiação correto, pega erros em compile time.
-
Evite globals e service locators. Eles escondem dependências e dificultam testes.
-
Injete tudo que é testável. Tempo, aleatoriedade, serviços externos—se você pode querer controlar em testes, injete.
A melhor DI em Go não é framework nenhum. Apenas construtores, interfaces e fiação explícita no main. Quando isso fica difícil de gerenciar, Wire gera o boilerplate mantendo tudo explícito.