Dependency Injection in Go using Wire

26 Nov 2022
Dependency Injection in Go using Wire

Dependency Injection(DI) is a technique used in programming that helps you to decouple the external logic of your implementation. A very common use case is to build APIs, use database to power these APIs, etc. But your logic in the implementation shouldn’t really change if one of the data sources change right? This is where dependency injection helps in decoupling logic. Therefore DI helps in producing flexible and loosely coupled code.

Talking in more technical terms - Dependency injection is a design pattern that decouples the usage of a particular object from its creation (i.e. time the object is initialized).

You might have heard of the popular SOLID principle. The ‘D’ in SOLID stands for dependency inversion which states that classes should be open to extension thereby they should depend on interface instead of concrete classes (avoid tight coupling).

You may ask but how does this help? Basically it allows you to switch easily the implementation of some dependency. For example you have a user service, you could change DB dependency from postgres to mongoDB or any other database. The same principle is used in unit testing where you use mock implementation to test instead of real one.

In Go terminology, the responsibility for building dependencies doesn’t lie with struct rather on interfaces. This also enables to use different implementation of the interface based on use case.

Do explore articles on Golang and System Design. You’ll learn something new 💡


Understanding with Example

Let’s begin with a basic code with some structs and functions.

package main

import "fmt"

type UserRepository struct{}

func (ur *UserRepository) Save(name string) {
	fmt.Println("Saving user: ", name)
}

func NewUserRepository() *UserRepository {
	return &UserRepository{}
}

type NotifService struct{}

func (ns *NotifService) Send(message string) {
	fmt.Println(message)
}

func NewNotifService() *NotifService {
	return &NotifService{}
}

type SignupService struct {
}

func (s *SignupService) UserSignup() {
	//initialize the repository
	userRepository := NewUserRepository()

	//store the data
	userRepository.Save("Mohit")

	// initialize the notification service
	notifSvc := NewNotifService()

	// send welcome notification
	notifSvc.Send("Welcome to Mohit's blog")

	// more operation ...

	return
}

func main() {
	fmt.Println("Aim: learning dependency injection")
	signupService := SignupService{}
	signupService.UserSignup()
}

Here we are creating a user signup service where we create dependencies of repository and notifications and execute operations that are expected on user signup. The issue here is you’re creating objects for each dependency whenever a signup method is called. You cannot change notification from say mobile push notification to a whatsapp notification without modifying the UserSignup function.


One step forward

Let’s update our code so that we don’t create new objects on each user signup call. That’s bad right?

package main

import "fmt"

type UserRepository struct{}

func (ur *UserRepository) Save(name string) {
	fmt.Println("Saving user: ", name)
}

func NewUserRepository() *UserRepository {
	return &UserRepository{}
}

type NotifService struct{}

func (ns *NotifService) Send(message string) {
	fmt.Println(message)
}

func NewNotifService() *NotifService {
	return &NotifService{}
}

type SignupService struct {
	userRepository *UserRepository
	notifSvc       *NotifService
}

func NewSignupService(userRepository *UserRepository, notifSvc *NotifService) *SignupService {
	return &SignupService{
		userRepository: userRepository,
		notifSvc:       notifSvc,
	}
}

func (s *SignupService) UserSignup() {
  // notice no initialization of dependencies required here.
  
	//store the data
	s.userRepository.Save("Mohit")

	// send welcome notification
	s.notifSvc.Send("Welcome to Mohit's blog")

	// more operation ...
	return
}

func main() {
	fmt.Println("Aim: learning dependency injection")

	// initialize once
	userRepository := NewUserRepository()
	notifSvc := NewNotifService()
	signupService := NewSignupService(userRepository, notifSvc)

	// use the methods
	signupService.UserSignup()
}

The major benefit we get here is that we can explicitly choose when to create new instances of our dependencies vs when to reuse the same instance. I could create a new instance with whatsapp notification implementation (assume we add interface) instead of using current implementation.


Make it better with DI Containers

Ok so we solved problem of creating vs using dependency. I’ll leave using interface in above code as an exercise for you folks. The next problem which comes as your code complexity grows is maintaining dependencies. At my workplace CRED, in of key services where we manage bill payments we have over 10 dependencies at places. Imagine managing such dependencies at multiple places. Trust me, it’s real pain!

This is where a Dependency injection container comes in allowing us to automatically update dependencies without defining it manually in our code. We’ll use an open source package called wire . This package is maintained by the google team. There are lot of other popular uber-go/dig and many more that you can use as well. For this example we’ll use wire.

Let’s add wire.go file.

//go:build wireinject

package main

import "github.com/google/wire"

func Initialize() *SignupService {
	panic(wire.Build(
		NewSignupService,
		NewUserRepository,
		NewNotifService,
	))
}

We now need to use this Initialize method in our main function.

package main

import (
	"github.com/google/wire"
)

// old code from before
// ...

func main() {
    fmt.Println("Aim: learning dependency injection")
    userSignupSvc := Initialize()
    userSignupSvc.UserSignup()
}
  1. To tell go that wiring should happen only when wire is building and not in other workflows we add buildTag.
  2. We’ve replaced the creation of userRepository and notifService with a Initialize() call . Inside this function we pass all of the constructors in our application to wire.Build. These arguments can be functions and structs.

To see this working, let’s quickly install the wire package

go install github.com/google/wire/cmd/wire@latest

# now run wire command
wire

This creates wire_gen.go file.

// Code generated by Wire. DO NOT EDIT.

//go:generate go run github.com/google/wire/cmd/wire
//go:build !wireinject
// +build !wireinject

package main

// Injectors from wire.go:

func Initialize() *SignupService {
	userRepository := NewUserRepository()
	notifService := NewNotifService()
	signupService := NewSignupService(userRepository, notifService)
	return signupService
}

Boom! Wire automatically generates the code we manually wrote before. Imagine how much manual code you would save in complex codebases.

Also to add in wire_gen.go you can see the go:generate comment. If you’re not aware about go generate - it helps in automating running tools to generate source code before compilation. So for this case, you could simply run go generate ./... and files would be regenerated by wire.


How does wire work?

You saw how wire_gen.go was created the code. How did wire know about what functions to add?

Wire basically relies on code generation and reflection to analyze the source code and produce injection code. So wire command looks for files tagged with the //+build wireinject build constraint. Once it knows about these files, it looks inside them for any function that had a call to wire.Build. Finally, it looks at all the arguments and return type of that function. It creates code based on arguments and return type of each function and passes this information to wire where it’s used as input to the dependency graph.

For more curious one’s wire relies on code generation and not reflection like other similar packages.You can also see how topological sort (also DFS: depth first search) is used to build a dependency graph internally in the code. See those graph algorithms are actually used 😛

There are more functions provided by wire you can read more on the go blog.


Resources

Books to learn Golang

Liked the article? Consider supporting me ☕️


I hope you learned something new. Feel free to suggest improvements ✔️

Follow me on Twitter for updates and resources. Let’s connect!

Keep exploring 🔎 Keep learning 🚀

Liked the content? Do support :)

Paypal - Mohit Khare
Buy me a coffee