Introducción: ¿Por qué es importante Dependency Injection en Go?
Go (Golang) es un lenguaje que valora la simplicidad y la claridad. Sin embargo, a medida que un proyecto crece, surge la necesidad de gestionar las dependencias entre componentes. Dependency Injection (DI) o inyección de dependencias es un patrón de diseño que ayuda a crear código débilmente acoplado, testeable y fácil de mantener. A diferencia de los frameworks con magia de reflexión (como Spring en Java), Go ofrece un enfoque más pragmático: pasar dependencias a través de interfaces y constructores.
En este artículo, analizaremos cómo implementar DI en Go sin bibliotecas de terceros, utilizando interfaces puras, y también veremos contenedores DI populares. Aprenderás cómo la inyección de dependencias ayuda a escribir pruebas unitarias y a seguir los principios SOLID.
Fundamentos: Interfaces e inyección a través del constructor
En Go no hay clases, pero hay interfaces — son contratos de comportamiento. Son la base para DI. En lugar de crear una implementación concreta dentro de una estructura, la pasamos desde el exterior.
Ejemplo sin DI (acoplamiento fuerte)
package main
import "fmt"
// Logger — implementación concretatype Logger struct{}
func (l *Logger) Log(msg string) { fmt.Println("LOG:", msg)}
// Service — depende fuertemente de Loggertype Service struct { logger *Logger}
func NewService() *Service { return &Service{logger: &Logger{}}}
func (s *Service) DoWork() { s.logger.Log("Trabajo completado")}
func main() { svc := NewService() svc.DoWork()}Problema: Service no se puede probar sin crear un Logger real. No es posible reemplazarlo por un mock u otro logger.
Ejemplo con DI a través de una interfaz
package main
import "fmt"
// LogInterface — abstracción para el loggingtype LogInterface interface { Log(msg string)}
// ConsoleLogger — implementacióntype ConsoleLogger struct{}
func (l *ConsoleLogger) Log(msg string) { fmt.Println("LOG:", msg)}
// Service — ahora depende de la interfaztype Service struct { logger LogInterface}
// NewService — constructor con inyección de dependenciafunc NewService(logger LogInterface) *Service { return &Service{logger: logger}}
func (s *Service) DoWork() { s.logger.Log("Trabajo completado")}
func main() { logger := &ConsoleLogger{} svc := NewService(logger) svc.DoWork()}Ahora Service funciona con cualquier objeto que implemente LogInterface. Esto simplifica las pruebas: podemos pasar un stub (mock).
Técnica avanzada: Contenedores DI en Go
Cuando el proyecto crece, pasar dependencias manualmente se vuelve tedioso. Aquí es donde los contenedores DI vienen al rescate: bibliotecas que resuelven automáticamente el grafo de dependencias. En Go son populares google/wire (de Google) y uber-go/dig (de Uber).
Ejemplo con google/wire
Wire utiliza generación de código, lo que elimina la reflexión en tiempo de ejecución — esto está en el espíritu de Go.
// wire.go//go:build wireinject// +build wireinject
package main
import "github.com/google/wire"
func InitializeService() *Service { wire.Build(NewService, NewConsoleLogger) return nil}Después de ejecutar wire, se generará código que creará un ConsoleLogger y lo pasará a NewService.
Ejemplo con uber-go/dig
Dig funciona a través de reflexión, pero es muy conveniente para el ensamblaje dinámico.
package main
import ( "go.uber.org/dig")
func main() { container := dig.New()
// Registramos los constructores container.Provide(NewConsoleLogger) container.Provide(NewService)
// Invocamos una función que utiliza las dependencias