Compile Time Dependency Injection with Golang

Siddhesh Tamhanekar
4 min readMay 7, 2023

--

Today, we are going to look at one of the library for dependency injection siddhesh-tamhanekar/di which is written by me.

If you are not familiar with what is dependency injection pls check below article written by Bhavya Karia to understand the basic concept.

A quick intro to Dependency Injection: what it is, and when to use it | by Bhavya Karia | We’ve moved to freeCodeCamp.org/news | Medium

Dependency Injection doesn’t need any library as itself. but to avoid repetitive manual writing boilerplate code, we use dependency injection libraries.

In Golang, there are mainly two types of dependency injection libraries are available as follows:

  • Runtime Dependency Injection
  • Compile Time dependency injection

Runtime Dependency Injection

In this approach, dependencies are registered at the program startup and resolved at the time when needed. the whole process starts once program execution is started. in Golang, reflection is mostly used by libraries for resolving runtime dependencies.

uber-go/dig and uber-go/fx are famous examples of runtime dependency injection libraries.

Compile Time Dependency Injection

In case of compile time dependency injection, dependency injection is done before compilation. due to this the instantiation errors are caught earlier in phase of compilation only. manually writing code for dependency injection is also comes under this approach.

google/wire is one of the popular library for compile time dependency injection.

All the above libraries are best at their place. If you are curious, you can check above links to find best fit for your application.

Let’s see how to use siddhesh-tamhanekar/di

# Installation
[project-root]#> go get github.com/siddhesh-tamhanekar/di@v0.0.2
[project-root]#> go install github.com/siddhesh-tamhanekar/di

# Usage
[project-root]#> <goroot>/bin/di

We will first write some code which has dependency chain and see how library helps to resolve the dependencies.

// main.go
package main

type Db struct {
dsn string
}

type UserRepo struct {
db *Db
}

type UserService struct{
userRepo *UserRepo
}

type UserHandler struct {
userService *UserService
}

func main() {
dsn := "file:test.db?cache=shared&mode=memory"
userHandler := NewUserHandler(dsn)
fmt.Println(userHandler)
}

We should note here we haven’t created NewUserHandler function yet. The library will create this function for us once we run the command <goroot>/bin/di.

but before running the command, we need to create one file named di.go which will provide information about dependencies to library.

// di.go
package main

import "github.com/siddhesh-tamhanekar/di"

func build() {
di.Build(UserHandler{})
}

All the generated code will be there in di_gen.go in each package

As we can see in above screenshot NewUserHandler has been generated and its simple function which simply initialize dependent structs and generated one method.

As of now by looking at NewUserHandler defination we can see it needs dsn as string parameter but passing everytime dsn as string and creating database instance each time when NewUserHandler is invoked is bad idea.

Let’s modify our code in order to avoid that.

// main.go 

var db *Db

func NewDb() Db {
if db != nil {
dsn := "file:test.db?cache=shared&mode=memory"
db = &Db{
dsn: dsn,
}
// code for connect database.
...
}
return *db
}

here we are trying to reuse db connection if db is already initialized.

Before creating any function library searches if there existing function already written which is prefix by “New” if found, library uses the existing function.

Now run the di command again and check generated file again.

We can see this time NewUserHandler is generated and it doesn't required any parameter and our NewDb function is detected by library and used instead of creating db struct by itself.

It looks like our NewDb function is not complete as while connecting database there could be error and our function doesn’t have that provision. we will modify NewDb function to handle error in main.go

// main.go 

var db *Db

func NewDb() (Db,error ){
if db != nil {
dsn := "file:test.db?cache=shared&mode=memory"
db = &Db{
dsn: dsn,
}
// code for connect database and in case of failure return error.
...
}
return *db,nil
}

As we can see generated file code, library detected one of the dependent function returns error and appropriately returned error.

I hope this article provides basic understanding of the di package, in upcoming articles we will discuss about how to handle interfaces and global variables in details. Stay tuned till then.

--

--