Inyección de Dependencias en Go: de la teoría al código real
La Inyección de Dependencias (DI) es uno de los patrones de diseño clave que ayuda a crear código débilmente acoplado, testeable y mantenible. En Go, con su filosofía de simplicidad y claridad, la inyección de dependencias se puede implementar tanto manualmente como con bibliotecas. En este artículo, analizaremos cómo aplicar correctamente DI en Golang en la práctica, veremos el enfoque manual y también la popular biblioteca google/wire.
1. ¿Por qué es necesaria la Inyección de Dependencias en Go?
Imagina que estás escribiendo un servicio HTTP para trabajar con usuarios. Sin DI, tu código podría verse así:
package main
import "database/sql"
type UserHandler struct { db *sql.DB}
func NewUserHandler() *UserHandler { db, _ := sql.Open("postgres", "conn_string") return &UserHandler{db: db}}
func (h *UserHandler) GetUser(id int) (*User, error) { // trabajo con h.db}Los problemas son evidentes:
- Acoplamiento fuerte — el handler crea la base de datos por sí mismo.
- Dificultad para testear — es imposible reemplazar
dbpor un mock. - Violación del Principio de Responsabilidad Única — el handler es responsable tanto de crear dependencias como de la lógica de negocio.
DI resuelve estos problemas pasando las dependencias desde el exterior:
type UserHandler struct { db Database}
func NewUserHandler(db Database) *UserHandler { return &UserHandler{db: db}}
// Ahora se puede reemplazar fácilmente por un mocktype Database interface { GetUser(id int) (*User, error)}2. Inyección manual de dependencias: simplicidad y control
Go no requiere frameworks para DI. La forma más simple y transparente es pasar las dependencias a través de los constructores. Consideremos un ejemplo con tres niveles de aplicación: handler → service → repository.
// repository/user.gotype UserRepository interface { FindByID(id int) (*User, error)}
type postgresUserRepo struct { db *sql.DB}
func NewPostgresUserRepo(db *sql.DB) *postgresUserRepo { return &postgresUserRepo{db: db}}
func (r *postgresUserRepo) FindByID(id int) (*User, error) { // implementación}// service/user.gotype UserService struct { repo UserRepository}
func NewUserService(repo UserRepository) *UserService { return &UserService{repo: repo}}
func (s *UserService) GetUser(id int) (*User, error) { return s.repo.FindByID(id)}// handler/user.gotype UserHandler struct { svc *UserService}
func NewUserHandler(svc *UserService) *UserHandler { return &UserHandler{svc: svc}}
func (h *UserHandler) HandleGetUser(w http.ResponseWriter, r *http.Request) { // trabajo con h.svc}Ahora el ensamblaje de la aplicación ocurre en main.go:
func main() { db, _ := sql.Open("postgres", os.Getenv("DB_URL")) repo := NewPostgresUserRepo(db) svc := NewUserService(repo) handler := NewUserHandler(svc) http.HandleFunc("/user", handler.HandleGetUser) log.Fatal(http.ListenAndServe(":8080", nil))}Ventajas del DI manual:
- Transparencia total — ves todas las dependencias explícitamente.
- Sin magia ni dependencias innecesarias en el proyecto.
- Ideal para proyectos pequeños y medianos.
Desventajas: cuando la aplicación crece (50+ componentes), el ensamblaje manual se vuelve voluminoso y propenso a errores.
3. Automatización con google/wire
La biblioteca github.com/google/wire genera código para ensamblar dependencias. No es un contenedor en tiempo de ejecución, sino un generador de código, lo que se alinea completamente con la filosofía de Go.
Instalación:
go get github.com/google/wir