Injeção de Dependência no Go: Padrões & Boas Práticas
Mestre os padrões DI para código Go testável
Injeção de dependência (DI) é um padrão de design fundamental que promove código limpo, testável e mantível em aplicações Go.
Seja você construindo APIs REST, implementando padrões de banco de dados multi-tenant, ou trabalhando com bibliotecas ORM, compreender a injeção de dependência melhorará significativamente a qualidade do seu código.

O que é Injeção de Dependência?
A injeção de dependência é um padrão de design onde os componentes recebem suas dependências de fontes externas, em vez de criá-las internamente. Esta abordagem desacopla componentes, tornando seu código mais modular, testável e mantível.
No Go, a injeção de dependência é particularmente poderosa devido à filosofia de design baseada em interfaces da linguagem. A satisfação implícita de interfaces no Go significa que você pode facilmente trocar implementações sem modificar o código existente.
Por que usar Injeção de Dependência no Go?
Melhor testabilidade: Ao injetar dependências, você pode facilmente substituir implementações reais por mocks ou doubles de teste. Isso permite que você escreva testes unitários rápidos, isolados e que não requerem serviços externos como bancos de dados ou APIs.
Melhor manutenibilidade: As dependências tornam-se explícitas no seu código. Quando você olha para uma função construtora, você imediatamente vê o que um componente requer. Isso torna o código mais fácil de entender e modificar.
Desacoplamento: Os componentes dependem de abstrações (interfaces) em vez de implementações concretas. Isso significa que você pode mudar implementações sem afetar o código dependente.
Flexibilidade: Você pode configurar diferentes implementações para diferentes ambientes (desenvolvimento, teste, produção) sem alterar sua lógica de negócios.
Injeção por Construtor: A forma Go
A maneira mais comum e idiomática de implementar a injeção de dependência no Go é através de funções construtoras. Estas geralmente são nomeadas NewXxx e aceitam dependências como parâmetros.
Exemplo Básico
Aqui está um exemplo simples demonstrando a injeção por construtor:
// Definir uma interface para o repositório
type UserRepository interface {
FindByID(id int) (*User, error)
Save(user *User) error
}
// O serviço depende da interface do repositório
type UserService struct {
repo UserRepository
}
// Construtor injeta a dependência
func NewUserService(repo UserRepository) *UserService {
return &UserService{repo: repo}
}
// Métodos usam a dependência injetada
func (s *UserService) GetUser(id int) (*User, error) {
return s.repo.FindByID(id)
}
Este padrão torna claro que UserService requer um UserRepository. Você não pode criar um UserService sem fornecer um repositório, o que impede erros de tempo de execução devido a dependências ausentes.
Múltiplas Dependências
Quando um componente tem múltiplas dependências, basta adicioná-las como parâmetros do construtor:
type EmailService interface {
Send(to, subject, body string) error
}
type Logger interface {
Info(msg string)
Error(msg string, err error)
}
type OrderService struct {
repo OrderRepository
emailSvc EmailService
logger Logger
paymentSvc PaymentService
}
func NewOrderService(
repo OrderRepository,
emailSvc EmailService,
logger Logger,
paymentSvc PaymentService,
) *OrderService {
return &OrderService{
repo: repo,
emailSvc: emailSvc,
logger: logger,
paymentSvc: paymentSvc,
}
}
Design de Interface para Injeção de Dependência
Um dos princípios-chave ao implementar a injeção de dependência é o Princípio da Inversão de Dependência (DIP): módulos de alto nível não devem depender de módulos de baixo nível; ambos devem depender de abstrações.
No Go, isso significa definir interfaces pequenas e focadas que representam apenas o que seu componente precisa:
// Bom: Interface pequena e focada
type PaymentProcessor interface {
ProcessPayment(amount float64) error
}
// Ruim: Interface grande com métodos desnecessários
type PaymentService interface {
ProcessPayment(amount float64) error
RefundPayment(id string) error
GetPaymentHistory(userID int) ([]Payment, error)
UpdatePaymentMethod(userID int, method PaymentMethod) error
// ... muitos outros métodos
}
A interface menor segue o Princípio da Segregação de Interface—os clientes não devem depender de métodos que não usam. Isso torna seu código mais flexível e mais fácil de testar.
Exemplo Prático: Abstração de Banco de Dados
Quando trabalha com bancos de dados em aplicações Go, você frequentemente precisa abstrair operações de banco de dados. Aqui está como a injeção de dependência ajuda:
// Interface do banco de dados - abstração de alto nível
type DB interface {
Query(ctx context.Context, query string, args ...interface{}) (Rows, error)
Exec(ctx context.Context, query string, args ...interface{}) (Result, error)
BeginTx(ctx context.Context) (Tx, error)
}
// Repositório depende da abstração
type UserRepository struct {
db DB
}
func NewUserRepository(db DB) *UserRepository {
return &UserRepository{db: db}
}
func (r *UserRepository) FindByID(ctx context.Context, id int) (*User, error) {
query := "SELECT id, name, email FROM users WHERE id = $1"
rows, err := r.db.Query(ctx, query, id)
if err != nil {
return nil, err
}
defer rows.Close()
// ... parse rows
}
Este padrão é especialmente útil ao implementar padrões de banco de dados multi-tenant, onde você pode precisar alternar entre diferentes implementações de banco de dados ou estratégias de conexão.
O Padrão Raiz de Composição
O Padrão Raiz de Composição é onde você monta todas as suas dependências no ponto de entrada da aplicação (normalmente main). Isso centraliza a configuração de dependência e torna o gráfico de dependência explícito.
func main() {
// Inicializar dependências de infraestrutura
db := initDatabase()
logger := initLogger()
// Inicializar repositórios
userRepo := NewUserRepository(db)
orderRepo := NewOrderRepository(db)
// Inicializar serviços com dependências
emailSvc := NewEmailService(logger)
paymentSvc := NewPaymentService(logger)
userSvc := NewUserService(userRepo, logger)
orderSvc := NewOrderService(orderRepo, emailSvc, logger, paymentSvc)
// Inicializar manipuladores HTTP
userHandler := NewUserHandler(userSvc)
orderHandler := NewOrderHandler(orderSvc)
// Montar rotas
router := setupRouter(userHandler, orderHandler)
// Iniciar servidor
log.Fatal(http.ListenAndServe(":8080", router))
}
Este abordagem torna claro como sua aplicação está estruturada e de onde vêm as dependências. É particularmente valiosa ao construir APIs REST em Go, onde você precisa coordenar múltiplas camadas de dependências.
Frameworks de Injeção de Dependência
Para aplicações maiores com gráficos de dependência complexos, gerenciar dependências manualmente pode se tornar trabalhoso. O Go tem vários frameworks de DI que podem ajudar:
Google Wire (DI em Tempo de Compilação)
Wire é uma ferramenta de injeção de dependência em tempo de compilação que gera código. É tipo seguro e não tem sobrecarga em tempo de execução.
Instalação:
go install github.com/google/wire/cmd/wire@latest
Exemplo:
// wire.go
//go:build wireinject
// +build wireinject
package main
import "github.com/google/wire"
func InitializeApp() (*App, error) {
wire.Build(
NewDB,
NewUserRepository,
NewUserService,
NewUserHandler,
NewApp,
)
return &App{}, nil
}
O Wire gera o código de injeção de dependência em tempo de compilação, garantindo segurança de tipo e eliminando a sobrecarga de reflexão em tempo de execução.
Uber Dig (DI em Tempo de Execução)
Dig é um framework de injeção de dependência em tempo de execução que usa reflexão. É mais flexível, mas tem algum custo em tempo de execução.
Exemplo:
import "go.uber.org/dig"
func main() {
container := dig.New()
// Registrar provedores
container.Provide(NewDB)
container.Provide(NewUserRepository)
container.Provide(NewUserService)
container.Provide(NewUserHandler)
// Invocar função que precisa de dependências
err := container.Invoke(func(handler *UserHandler) {
// Usar handler
})
if err != nil {
log.Fatal(err)
}
}
Quando Usar Frameworks
Use um framework quando:
- Seu gráfico de dependência é complexo com muitos componentes interdependentes
- Você tem várias implementações da mesma interface que precisam ser selecionadas com base na configuração
- Você quer resolução automática de dependências
- Você está construindo uma aplicação grande onde o wiring manual se torna propenso a erros
Mantenha a DI manual quando:
- Sua aplicação é pequena a média
- Seu gráfico de dependência é simples e fácil de seguir
- Você quer manter as dependências mínimas e explícitas
- Você prefere código explícito em vez de código gerado
Testando com Injeção de Dependência
Um dos principais benefícios da injeção de dependência é a melhor testabilidade. Aqui está como a DI torna o teste mais fácil:
Exemplo de Teste Unitário
// Implementação de mock para teste
type mockUserRepository struct {
users map[int]*User
err error
}
func (m *mockUserRepository) FindByID(id int) (*User, error) {
if m.err != nil {
return nil, m.err
}
return m.users[id], nil
}
func (m *mockUserRepository) Save(user *User) error {
if m.err != nil {
return m.err
}
m.users[user.ID] = user
return nil
}
// Teste usando o mock
func TestUserService_GetUser(t *testing.T) {
mockRepo := &mockUserRepository{
users: map[int]*User{
1: {ID: 1, Name: "John", Email: "john@example.com"},
},
}
service := NewUserService(mockRepo)
user, err := service.GetUser(1)
assert.NoError(t, err)
assert.Equal(t, "John", user.Name)
}
Este teste executa rapidamente, não requer um banco de dados e testa sua lógica de negócios isoladamente. Ao trabalhar com bibliotecas ORM em Go, você pode injetar repositórios mock para testar a lógica do serviço sem configuração de banco de dados.
Padrões Comuns e Boas Práticas
1. Use a Segregação de Interface
Mantenha interfaces pequenas e focadas no que o cliente realmente precisa:
// Bom: O cliente só precisa ler usuários
type UserReader interface {
FindByID(id int) (*User, error)
FindByEmail(email string) (*User, error)
}
// Interface separada para escrita
type UserWriter interface {
Save(user *User) error
Delete(id int) error
}
2. Retorne Erros dos Construtores
Os construtores devem validar dependências e retornar erros se a inicialização falhar:
func NewUserService(repo UserRepository) (*UserService, error) {
if repo == nil {
return nil, errors.New("user repository cannot be nil")
}
return &UserService{repo: repo}, nil
}
3. Use Context para Dependências de Escopo de Requisição
Para dependências específicas de requisição (como transações de banco de dados), passe-as via context:
type ctxKey string
const dbKey ctxKey = "db"
func WithDB(ctx context.Context, db DB) context.Context {
return context.WithValue(ctx, dbKey, db)
}
func DBFromContext(ctx context.Context) (DB, bool) {
db, ok := ctx.Value(dbKey).(DB)
return db, ok
}
4. Evite a Injeção Excessiva
Não injete dependências que são verdadeiras detalhes de implementação interna. Se um componente cria e gerencia seus próprios objetos auxiliares, está tudo bem:
// Bom: O helper interno não precisa de injeção
type UserService struct {
repo UserRepository
// Cache interno - não precisa de injeção
cache map[int]*User
}
func NewUserService(repo UserRepository) *UserService {
return &UserService{
repo: repo,
cache: make(map[int]*User),
}
}
5. Documente as Dependências
Use comentários para documentar por que as dependências são necessárias e quaisquer restrições:
// UserService lida com a lógica de negócios relacionada aos usuários.
// Ele requer um UserRepository para acesso a dados e um Logger para
// rastreamento de erros. O repositório deve ser thread-safe se usado
// em contextos concorrentes.
func NewUserService(repo UserRepository, logger Logger) *UserService {
// ...
}
Quando NÃO usar Injeção de Dependência
A injeção de dependência é uma ferramenta poderosa, mas nem sempre é necessária:
Pule a DI para:
- Objetos de valor simples ou estruturas de dados
- Funções auxiliares ou utilitárias internas
- Scripts de uso único ou utilitários pequenos
- Quando a instânciação direta for mais clara e simples
Exemplo de quando NÃO usar DI:
// Estrutura simples - não há necessidade de DI
type Point struct {
X, Y float64
}
func NewPoint(x, y float64) Point {
return Point{X: x, Y: y}
}
// Utilitário simples - não há necessidade de DI
func FormatCurrency(amount float64) string {
return fmt.Sprintf("$%.2f", amount)
}
Integração com o Ecossistema Go
A injeção de dependência funciona de forma seamless com outros padrões e ferramentas Go. Ao construir aplicações que usam bibliotecas padrão do Go para raspagem de web ou geração de relatórios PDF, você pode injetar esses serviços na sua lógica de negócios:
type PDFGenerator interface {
GenerateReport(data ReportData) ([]byte, error)
}
type ReportService struct {
pdfGen PDFGenerator
repo ReportRepository
}
func NewReportService(pdfGen PDFGenerator, repo ReportRepository) *ReportService {
return &ReportService{
pdfGen: pdfGen,
repo: repo,
}
}
Isso permite que você troque implementações de geração de PDF ou use mocks durante os testes.
Conclusão
A injeção de dependência é uma pedra fundamental para escrever código Go mantível e testável. Ao seguir os padrões apresentados neste artigo — injeção por construtor, design baseado em interface e o padrão raiz de composição — você criará aplicações que são mais fáceis de entender, testar e modificar.
Comece com a injeção manual por construtor para aplicações pequenas a médias, e considere frameworks como Wire ou Dig à medida que seu gráfico de dependência crescer. Lembre-se de que o objetivo é clareza e testabilidade, não complexidade por si só.
Para mais recursos de desenvolvimento Go, consulte nossa Folha de Dicas Go.
Links Úteis
- Folha de Dicas Go
- Alternativas ao BeautifulSoup para Go
- Gerando PDF em GO - Bibliotecas e exemplos
- Padrões de Banco de Dados Multi-Tenant com exemplos em Go
- ORM a usar em GO: GORM, sqlc, Ent ou Bun?
- Construindo APIs REST em Go: Guia completo
Recursos Externos
- Como usar Injeção de Dependência no Go - freeCodeCamp
- Melhores práticas para inversão de dependência em Golang - Relia Software
- Guia prático para injeção de dependência no Go - Relia Software
- Google Wire - Injeção de Dependência em Tempo de Compilação
- Uber Dig - Injeção de Dependência em Tempo de Execução
- Princípios SOLID no Go - Lexicon de Padrões de Software