
Publicado el
- 8 min read
Como hacer inyección de dependencias en Go
¿Cansado de depender de variables globales para arrancar tu logger? ¿Deseas garantizar que todas tus capas se inicialicen en un único lugar y solo una vez? ¿Utilizas interfaces y structs para definir tus módulos, pero te enfrentas al desafío de pasar dependencias inicializadas entre ellos? En este artículo exploraremos diversas estrategias para la inyección de dependencias en Go. Desde enfoques sin librerías hasta aquellos que las incorporan, descubrirás los pros y contras de cada uno.
¿Por qué hacer inyección de dependencias en Go?
La inyección de dependencias en Go ofrece una metodología eficaz para mejorar la modularidad y mantenibilidad del código. Al evitar el uso de variables globales, promueve un diseño más limpio y cohesivo, facilitando la comprensión y modificación del código. Además, proporciona una mayor “testabilidad” (Palabra inventada que significa facilidad de testear) al permitir la sustitución sencilla de dependencias durante las pruebas unitarias, favoreciendo el desarrollo de software robusto y libre de acoplamientos innecesarios. La inyección de dependencias también fomenta la reutilización de componentes, al facilitar la conexión de módulos sin depender de implementaciones concretas.
Inyección de dependencias sin librerías en Go:
Para lograr hacer inyección de dependencias en Go sin librerías debemos tener en cuenta que es necesario el uso de interfaces y structs para la definición de cada uno de tus módulos, esto va a ser específicamente útil al momento de testear toda tu aplicación. La lógica es simple, una función New...()
o Provide...()
que cree una instancia de tu módulo e inicializarlos luego en algún fichero central de tu aplicación.
Veámoslo con un ejemplo, una API de libros básica con una db mockeada. Tenemos un controlador, un servicio y un repositorio, cada uno necesita del otro (dependencias), por ejemplo el controlador necesita un servicio y el servicio necesita un repositorio. Let’s dig into code.
Disclaimer: En estos ejemplos utilizo Gin Gonic para el servidor web, pero todos son realizables y adaptables para el framework/router que utilices 😄
book_repository.go
type IBooksRepository interface {
GetAll(ctx context.Context) ([]models.Book, error)
}
type BooksRepository struct {
DB []models.Book
}
func (r *BooksRepository) GetAll(ctx context.Context) ([]models.Book, error) {
return r.DB, nil
}
book_service.go
type IBooksService interface {
GetAll(ctx context.Context) ([]models.Book, error)
}
type BooksService struct {
Repo repositories.IBooksRepository
}
func (s *BooksService) GetAll(ctx context.Context) ([]models.Book, error) {
return s.Repo.GetAll(ctx)
}
book_controller.go
type IBooksController interface {
GetAll(c *gin.Context)
}
type BooksController struct {
Service services.IBooksService
}
func (c *BooksController) GetAll(ctx *gin.Context) {
books, _ := c.Service.GetAll(ctx)
ctx.JSON(200, gin.H{
"books": books,
})
}
Una forma fácil para inicializar y pasar todas las dependencias necesarias puede ser en el main.go
, de la siguiente forma:
func main() {
bookRepo := repositories.BooksRepository{DB: []models.Book{}}
bookSrv := services.BooksService{Repo: &bookRepo}
bookCtrl := controllers.BooksController{Service: &bookSrv}
r := gin.Default()
r.GET("/books", bookCtrl.GetAll)
r.Run(":8080")
}
Debes notar el orden en el que deben ser inicializados, de la dependencia más profunda a la más superficial.
Esto es perfecto si tienes un proyecto pequeño, pero entre más crezca vas a “ensuciar” más tu main.go
, que debería ser solo el punto de entrada y no contener nada más, ¿cómo mejoramos este código?
Podemos crear funciones para inicializar cada una de las dependencias, llamándolas New...()
o Provide...()
y moviéndolas a un nuevo file que se encargue de manejar todas tus dependencias.
En mi caso lo moví a un módulo llamado app y cree el file app.go
que contiene la función Run()
que inicializa todas las dependencias e inicializa el servidor. Las funciones que inicializan las dependencias las moví para un pkg llamado providers
.
func Run() {
mockedDB:= providers.ProvideDatabase()
bookRepo:= providers.ProvideBooksRepository(mockedDB)
bookService:= providers.ProvideBooksService(bookRepo)
bookController:= providers.ProvideBooksController(bookService)
router := providers.ProvideRouter()
providers.RegisterRoutes(router, bookController)
router.Run(":8080")
}
La estructura de archivos de este proyecto es la siguiente en este punto:
.
├── 📁 src
│ └── 📁 app Inyección de dependencias e inicializadores de la app.
│ └── 📁 providers Proveedores(funciones que inicializan structs) de todos los módulos.
│ │ └── 📄 controllers.go
│ │ └── 📄 services.go
│ │ └── 📄 repositories.go
│ │ └── 📄 router.go Funciones relacionadas al routero de la API.
│ └── 📄 app.go Acá va la función Run() que llama a todos los proveedores e inicializa el servidor.
│ └── 📄 router.go Configuración y ruteo del servidor web.
│ └── 📁 controller Controladores que reciben las requests.
│ └── 📁 service Lógica de negocio, modificaciones de modelos, etc.
│ └── 📁 repository Capa que accede a los datos.
│ └── 📄 main.go Llama a app.Run().
└──
El repositorio con todos los archivos lo tienes acá: sebasvil20/blog-di
Con esto, deberás tener una API corriendo en el puerto 8080 que cuando le pegas al endpoint GET /books
vas a recibir una lista en json con dos libros:
{"books":[{"id":1,"title":"El señor de los anillos","author":"J.R.R. Tolkien"},{"id":2,"title":"Cien años de soledad","author":"Gabriel García Márquez"}]}
Y eso sería de la inyección de dependencias manual, como puedes ver, es bastante simple y fácil de seguir, pero tiene un problema: cuando el proyecto crece se puede volver difícil de controlar y mantener, pasemos a ver más alternativas para hacer inyección de dependencias en Go.
DI con Uber/fx: Inyección de dependencias con fx
Reutilizaremos la mayoría del código del ejemplo pasado, solo cambiaremos las partes de la inyección de dependencias. Te dejo el repositorio con todos los ejemplos nuevamente: sebasvil20/blog-di.
Toda la documentación de uber fx y su repo: https://github.com/uber-go/fx
Como paso principal, para instalar uber/fx corres el siguiente comando:
go get go.uber.org/fx@v1
La lógica inicial es la misma que con la inyección manual de dependencias: Debes tener todo estructurado en interfaces y structs, una vez tengas bien definidas estas cosas, pasemos a lo especifico de Uber FX:
Lo primero que debemos entender es que uber FX inyecta las dependencias en tiempo de ejecución, y te da control sobre el ciclo de vida de tu aplicación, por ejemplo, que quieres hacer una vez se inicialicen tus dependencias o que quieres hacer una vez la app muera, esto es útil para casos donde necesitemos hacer cleanup de una base de datos por ejemplo.
Además del ciclo de vida, te deja organizar tus dependencias en “Módulos” que no son más que grupos de dependencias, así definimos un módulo para fx (En la carpeta pkg de providers
cree un file llamado books_module
):
var BooksModule = fx.Module("Books",
fx.Provide(func(db []models.Book) repositories.IBooksRepository {
return &repositories.BooksRepository{
DB: db,
}
}),
fx.Provide(func(repo repositories.IBooksRepository) services.IBooksService {
return &services.BooksService{
Repo: repo,
}
}),
fx.Provide(func(srv services.IBooksService) *controllers.BooksController {
return &controllers.BooksController{
Service: srv,
}
}),
)
fx.Module
recibe tanto el nombre del módulo como las funciones propias de fx como fx.Provide
o fx.Invoke
, el nombre del módulo sirve únicamente para loggear y hacer troubleshooting, lo vas a ver cuando inicialices la app.
fx.Provide
es una función que como su nombre indica, provee, esta la debes usar cuando el return de esa función lo van a usar en otras funciones, por ejemplo el servicio de libros, services.IBookService
, lo va a usar el controller para inyectárselo, por eso usamos fx.Provide
.
fx.Invoke
invoca funciones que no necesariamente lo que devuelven lo vamos a usar, pueden devolver un error (fx los maneja automáticamente si una de estas devuelve error). Esta función es perfecta para decorar cosas o simplemente inicializar algo que no devuelve nada.
Ahora en el app.go
podemos llamar al módulo e inicializar el resto de cosas que necesitemos. Además, como mencioné anteriormente, fx te deja controlar el ciclo de vida de las dependencias y es al menos necesario que definas el hook onStart
. El archivo app.go
quedaría así:
func Run() {
fx.New(
fx.Provide(providers.ProvideDatabase),
providers.BooksModule,
fx.Provide(providers.ProvideRouter),
fx.Invoke(providers.RegisterRoutes),
fx.Invoke(serve),
).Run()
}
func serve(lifecycle fx.Lifecycle, router * gin.Engine) {
lifecycle.Append(fx.Hook{
OnStart: func(context.Context) error {
go router.Run()
return nil
},
})
}
La función Run()
es la que creamos nosotros, acá invocamos e inicializamos todas las dependencias.
Primero creamos una instancia de fx con fx.New()
que recibe tantos parámetros (de tipo fx.Option
) como necesites. Nótese que en los casos de los módulos solo hacemos referencia al módulo, ya que adentro del módulo llamamos a fx.Provide
para las dependencias. Además, nótese la invocación (fx.Invoke
) de la función serve
, que recibe el ciclo de vida de fx y más parámetros que requieras, el resto de parámetros fx los resuelve automáticamente, tomando los return values de los fx.Provide
.
En la función serve
le sumamos un item al ciclo de vida de fx con lifecycle.Append
y le pasamos el hook que queremos, en nuestro caso queremos que cuando ya termine de resolver las dependencias, llame al router de gin e inicialice el servidor.
Nuestro main.go
queda igual que en la anterior, solamente llamamos al app.Run()
func main() {
app.Run()
}
Con esto deberías tener un servidor corriendo en el 8080 recibiendo peticiones correctamente, además con dependencias resueltas por uber fx.
El ejemplo completo por si quieres probar directamente lo encuentras en el repo: sebasvil20/blog-di.
Consideraciones finales
- Revisa el tamaño de tu app y que tanto piensas que va a escalar para tomar la decisión de que usar.
- Google/Wire es otra alternativa para lograr inyección de dependencias en Go, se diferencia de uber/fx con que google/wire hace la inyección en tiempo de compilado. ¡Te recomiendo que le des una mirada también!