Inyección de dependencias en Go: Patrones y buenas prácticas
Domine los patrones DI para código de Go testeable
Inyección de dependencias (DI) es un patrón de diseño fundamental que promueve código limpio, testable y mantenible en aplicaciones de Go.
Ya sea que estés construyendo APIs REST, implementando patrones de base de datos multiinquilino, o trabajando con bibliotecas ORM, entender la inyección de dependencias mejorará significativamente la calidad de tu código.

¿Qué es la inyección de dependencias?
La inyección de dependencias es un patrón de diseño donde los componentes reciben sus dependencias de fuentes externas en lugar de crearlas internamente. Este enfoque desacopla componentes, haciendo que tu código sea más modular, testable y mantenible.
En Go, la inyección de dependencias es especialmente poderosa debido a la filosofía de diseño basada en interfaces del lenguaje. La satisfacción implícita de interfaces en Go significa que puedes cambiar fácilmente las implementaciones sin modificar el código existente.
¿Por qué usar la inyección de dependencias en Go?
Mejora de la testabilidad: Al inyectar dependencias, puedes reemplazar fácilmente las implementaciones reales con mocks o dobles de prueba. Esto te permite escribir pruebas unitarias que sean rápidas, aisladas y no requieran servicios externos como bases de datos o APIs.
Mejor mantenibilidad: Las dependencias se vuelven explícitas en tu código. Cuando miras una función constructora, inmediatamente ves qué requiere un componente. Esto hace que la base de código sea más fácil de entender y modificar.
Desacoplamiento suave: Los componentes dependen de abstracciones (interfaces) en lugar de implementaciones concretas. Esto significa que puedes cambiar implementaciones sin afectar el código dependiente.
Flexibilidad: Puedes configurar diferentes implementaciones para diferentes entornos (desarrollo, pruebas, producción) sin cambiar tu lógica de negocio.
Inyección por constructor: La forma Go
La manera más común e idiomática de implementar la inyección de dependencias en Go es a través de funciones constructoras. Estas suelen tener nombres como NewXxx y aceptan dependencias como parámetros.
Ejemplo básico
Aquí hay un ejemplo simple que demuestra la inyección por constructor:
// Definir una interfaz para el repositorio
type UserRepository interface {
FindByID(id int) (*User, error)
Save(user *User) error
}
// El servicio depende de la interfaz del repositorio
type UserService struct {
repo UserRepository
}
// El constructor inyecta la dependencia
func NewUserService(repo UserRepository) *UserService {
return &UserService{repo: repo}
}
// Los métodos usan la dependencia inyectada
func (s *UserService) GetUser(id int) (*User, error) {
return s.repo.FindByID(id)
}
Este patrón hace claro que UserService requiere un UserRepository. No puedes crear un UserService sin proporcionar un repositorio, lo que previene errores en tiempo de ejecución por dependencias faltantes.
Múltiples dependencias
Cuando un componente tiene múltiples dependencias, simplemente agrégalas como parámetros del constructor:
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,
}
}
Diseño de interfaces para la inyección de dependencias
Uno de los principios clave al implementar la inyección de dependencias es el Principio de Inversión de Dependencias (DIP): los módulos de alto nivel no deben depender de módulos de bajo nivel; ambos deben depender de abstracciones.
En Go, esto significa definir interfaces pequeñas y enfocadas que representen solo lo que tu componente necesita:
// Bueno: interfaz pequeña y enfocada
type PaymentProcessor interface {
ProcessPayment(amount float64) error
}
// Malo: interfaz grande con métodos innecesarios
type PaymentService interface {
ProcessPayment(amount float64) error
RefundPayment(id string) error
GetPaymentHistory(userID int) ([]Payment, error)
UpdatePaymentMethod(userID int, method PaymentMethod) error
// ... muchos más métodos
}
La interfaz más pequeña sigue el Principio de Segregación de Interfaces—los clientes no deben depender de métodos que no usan. Esto hace que tu código sea más flexible y más fácil de probar.
Ejemplo real: abstracción de base de datos
Cuando trabajas con bases de datos en aplicaciones de Go, a menudo necesitas abstraer las operaciones de base de datos. Aquí es cómo la inyección de dependencias ayuda:
// Interfaz de base de datos - abstracción de alto nivel
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)
}
// El repositorio depende de la abstracción
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()
// ... analizar filas
}
Este patrón es especialmente útil al implementar patrones de base de datos multiinquilino, donde podrías necesitar cambiar entre diferentes implementaciones de base de datos o estrategias de conexión.
El patrón de raíz de composición
La Raíz de Composición es donde ensambles todas tus dependencias en el punto de entrada de la aplicación (normalmente main). Esto centraliza la configuración de dependencias y hace explícito el gráfico de dependencias.
func main() {
// Inicializar dependencias de infraestructura
db := initDatabase()
logger := initLogger()
// Inicializar repositorios
userRepo := NewUserRepository(db)
orderRepo := NewOrderRepository(db)
// Inicializar servicios con dependencias
emailSvc := NewEmailService(logger)
paymentSvc := NewPaymentService(logger)
userSvc := NewUserService(userRepo, logger)
orderSvc := NewOrderService(orderRepo, emailSvc, logger, paymentSvc)
// Inicializar controladores HTTP
userHandler := NewUserHandler(userSvc)
orderHandler := NewOrderHandler(orderSvc)
// Configurar rutas
router := setupRouter(userHandler, orderHandler)
// Iniciar servidor
log.Fatal(http.ListenAndServe(":8080", router))
}
Este enfoque hace claro cómo está estructurada tu aplicación y de dónde provienen las dependencias. Es especialmente valioso al construir APIs REST en Go, donde necesitas coordinar múltiples capas de dependencias.
Marcos de inyección de dependencias
Para aplicaciones más grandes con gráficos de dependencias complejos, gestionar dependencias manualmente puede volverse incómodo. Go tiene varios marcos de DI que pueden ayudarte:
Google Wire (DI en tiempo de compilación)
Wire es una herramienta de inyección de dependencias en tiempo de compilación que genera código. Es tipo seguro y no tiene sobrecarga en tiempo de ejecución.
Instalación:
go install github.com/google/wire/cmd/wire@latest
Ejemplo:
// 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
}
Wire genera el código de inyección de dependencias en tiempo de compilación, asegurando la seguridad de tipos y eliminando la sobrecarga de reflexión en tiempo de ejecución.
Uber Dig (DI en tiempo de ejecución)
Dig es un marco de inyección de dependencias en tiempo de ejecución que usa reflexión. Es más flexible pero tiene algunos costos en tiempo de ejecución.
Ejemplo:
import "go.uber.org/dig"
func main() {
container := dig.New()
// Registrar proveedores
container.Provide(NewDB)
container.Provide(NewUserRepository)
container.Provide(NewUserService)
container.Provide(NewUserHandler)
// Invocar función que necesita dependencias
err := container.Invoke(func(handler *UserHandler) {
// Usar handler
})
if err != nil {
log.Fatal(err)
}
}
¿Cuándo usar marcos?
Usa un marco cuando:
- Tu gráfico de dependencias es complejo con muchos componentes interdependientes
- Tienes múltiples implementaciones de la misma interfaz que necesitan seleccionarse según la configuración
- Quieres resolución automática de dependencias
- Estás construyendo una aplicación grande donde la conexión manual se vuelve propensa a errores
Usa DI manual cuando:
- Tu aplicación es pequeña o mediana
- El gráfico de dependencias es simple y fácil de seguir
- Quieres mantener las dependencias mínimas y explícitas
- Prefieres código explícito sobre código generado
Pruebas con inyección de dependencias
Uno de los principales beneficios de la inyección de dependencias es la mejora de la testabilidad. Aquí es cómo DI hace más fácil las pruebas:
Ejemplo de prueba unitaria
// Implementación de prueba
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
}
// Prueba usando el 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)
}
Esta prueba se ejecuta rápidamente, no requiere una base de datos y prueba tu lógica de negocio en aislamiento. Cuando trabajas con bibliotecas ORM en Go, puedes inyectar repositorios de prueba para probar la lógica del servicio sin configuración de base de datos.
Patrones comunes y buenas prácticas
1. Usar segregación de interfaces
Mantén las interfaces pequeñas y enfocadas en lo que el cliente realmente necesita:
// Bueno: El cliente solo necesita leer usuarios
type UserReader interface {
FindByID(id int) (*User, error)
FindByEmail(email string) (*User, error)
}
// Interfaz separada para escritura
type UserWriter interface {
Save(user *User) error
Delete(id int) error
}
2. Devolver errores desde constructores
Los constructores deben validar dependencias y devolver errores si la inicialización falla:
func NewUserService(repo UserRepository) (*UserService, error) {
if repo == nil {
return nil, errors.New("el repositorio de usuarios no puede ser nulo")
}
return &UserService{repo: repo}, nil
}
3. Usar contexto para dependencias con ámbito de solicitud
Para dependencias específicas de solicitud (como transacciones de base de datos), pásalas mediante contexto:
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. Evitar la sobreinyección
No inyectes dependencias que sean verdaderos detalles de implementación interna. Si un componente crea y gestiona sus propios objetos auxiliares, eso está bien:
// Bueno: El helper interno no necesita inyección
type UserService struct {
repo UserRepository
// Cache interno - no necesita inyección
cache map[int]*User
}
func NewUserService(repo UserRepository) *UserService {
return &UserService{
repo: repo,
cache: make(map[int]*User),
}
}
5. Documentar dependencias
Usa comentarios para documentar por qué se necesitan dependencias y cualquier restricción:
// UserService maneja la lógica de negocio relacionada con usuarios.
// Requiere un UserRepository para el acceso a datos y un Logger para
// el seguimiento de errores. El repositorio debe ser seguro para hilos si se usa
// en contextos concurrentes.
func NewUserService(repo UserRepository, logger Logger) *UserService {
// ...
}
¿Cuándo NO usar la inyección de dependencias?
La inyección de dependencias es una herramienta poderosa, pero no siempre es necesaria:
Saltar DI para:
- Objetos simples o estructuras de datos
- Funciones auxiliares o utilidades internas
- Scripts de uso único o utilidades pequeñas
- Cuando la instanciación directa es más clara y simple
Ejemplo de cuando NO usar DI:
// Estructura simple - no se necesita DI
type Point struct {
X, Y float64
}
func NewPoint(x, y float64) Point {
return Point{X: x, Y: y}
}
// Utilidad simple - no se necesita DI
func FormatCurrency(amount float64) string {
return fmt.Sprintf("$%.2f", amount)
}
Integración con el ecosistema de Go
La inyección de dependencias funciona de forma sinérgica con otros patrones y herramientas de Go. Cuando construyes aplicaciones que usan la biblioteca estándar de Go para web scraping o generar informes en PDF, puedes inyectar estos servicios en tu lógica de negocio:
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,
}
}
Esto te permite cambiar entre implementaciones de generación de PDF o usar mocks durante las pruebas.
Conclusión
La inyección de dependencias es una piedra angular al escribir código Go mantenible y testable. Siguiendo los patrones descritos en este artículo—inyección por constructor, diseño basado en interfaces y el patrón de raíz de composición—creará aplicaciones más fáciles de entender, probar y modificar.
Comienza con la inyección manual por constructor para aplicaciones pequeñas a medianas, y considera marcos como Wire o Dig a medida que crece tu gráfico de dependencias. Recuerda que el objetivo es claridad y testabilidad, no complejidad por sí misma.
Para más recursos de desarrollo en Go, consulta nuestro Hoja de trucos de Go.
Enlaces útiles
- Hoja de trucos de Go
- Alternativas a Beautiful Soup para Go
- Generar PDF en GO - Bibliotecas y ejemplos
- Patrones de base de datos multiinquilino con ejemplos en Go
- ORM para usar en GO: GORM, sqlc, Ent o Bun?
- Construyendo APIs REST en Go: Guía completa
Recursos externos
- Cómo usar la inyección de dependencias en Go - freeCodeCamp
- Mejores prácticas para la inversión de dependencias en Golang - Relia Software
- Guía práctica de inyección de dependencias en Go - Relia Software
- Google Wire - Inyección de dependencias en tiempo de compilación
- Uber Dig - Inyección de dependencias en tiempo de ejecución
- Principios SOLID en Go - Lexicón de patrones de software