I don't think there is single response for this question, but I'll share my approach on how I'm currently doing Dependency Injection on Go with go-gin (but should be the nearly the same with any other router).
From a business point of view, I have a struct that wraps all access to my services which are responsible for business rules/processing.
// WchyContext is an application-wide context
type WchyContext struct {
Health services.HealthCheckService
Tenant services.TenantService
... whatever
}
My services are then just interfaces.
// HealthCheckService is a simple general purpose health check service
type HealthCheckService interface {
IsDatabaseOnline() bool
}
Which have mulitple implementations, like MockedHealthCheck
, PostgresHealthCheck
, PostgresTenantService
and so on.
My router than depends on a WchyContext, which the code looks like this:
func GetMainEngine(ctx context.WchyContext) *gin.Engine {
router := gin.New()
router.Use(gin.Logger())
router.GET("/status", Status(ctx))
router.GET("/tenants/:domain", TenantByDomain(ctx))
return router
}`
Status
and TenantByDomain
act like a handler-factory, all it does is create a new handler based on given context, like this:
type statusHandler struct {
ctx context.WchyContext
}
// Status creates a new Status HTTP handler
func Status(ctx context.WchyContext) gin.HandlerFunc {
return statusHandler{ctx: ctx}.get()
}
func (h statusHandler) get() gin.HandlerFunc {
return func(c *gin.Context) {
c.JSON(200, gin.H{
"healthy": gin.H{
"database": h.ctx.Health.IsDatabaseOnline(),
},
"now": time.Now().Format("2006.01.02.150405"),
})
}
}
As you can see, my health check handler doesn't care about concrete implementation of my services, I just use it whatever is in the ctx.
The last part depends on current execution environment. During automated tests I create a new WchyContext
using mocked/stubbed services and send it to GetMainEngine, like this:
ctx := context.WchyContext{
Health: &services.InMemoryHealthCheckService{Status: false},
Tenant: &services.InMemoryTenantService{Tenants: []*models.Tenant{
&models.Tenant{ID: 1, Name: "Orange Inc.", Domain: "orange"},
&models.Tenant{ID: 2, Name: "The Triathlon Shop", Domain: "trishop"},
}}
}
router := handlers.GetMainEngine(ctx)
request, _ := http.NewRequest(method, url, nil)
response := httptest.NewRecorder()
router.ServeHTTP(response, request)
... check if response matches what you expect from your handler
And when you setup it to really listen to a HTTP port, the wiring up looks like this:
var ctx context.WchyContext
var db *sql.DB
func init() {
db, _ = sql.Open("postgres", os.Getenv("DATABASE_URL"))
ctx = context.WchyContext{
Health: &services.PostgresHealthCheckService{DB: db},
Tenant: &services.PostgresTenantService{DB: db}
}
}
func main() {
handlers.GetMainEngine(ctx).Run(":" + util.GetEnvOrDefault("PORT", "3000"))
}
There are a few things that I don't like about this, I'll probably refactor/improve it later, but it has been working well so far.
If you want to see full code reference, I'm working on this project here https://github.com/WeCanHearYou/wchy
Hope it can help you somehow.