Go: Сomparing dependency injection approaches

Konstantin Makarov
Dec 28, 2019 · 3 min read
ihippik ©

In this article, I wanted to discuss such a development pattern called dependency injection.
And on the example of the Go language, compare two approaches, namely, a couple of libraries that implement these approaches.

Dependency injection (DI) is the process of providing external dependency to a software component.

Let me quickly explain why this approach is needed for those who may not have heard about it.
Suppose we have a certain service that is configured by a certain config and works with the database through the repository abstraction:

type Service struct {
config *Config
repo Repository

We create an instance of this service for further work with it:

func NewService() *Service {
db,_:= DbConnect(cfg)
return &Service{
config: cfg,
repo: NewRepository(db)

Everything is fine. To create a new service you do not need anything except a configuration file!
But, what if tomorrow we want to get the configuration from the environment variables or Consul. Have to rewrite the service.
Then think about how we will test the repository? We will have to connect to a real database, instead of mock-testing!

As we all know, in order to write high-quality and well-tested code, we need to break the tight link between the software component
and required dependencies.
In our case, the dependencies will be passed to the specific component as the parameters of its constructor:

func NewService(config *Config, repo Repository) *Service {
return &Service{config: config, repo: repo}

And now, instead of the repository, we can pass the mock-object and easily test the service without connecting to a real database.
We can also fill out our config anywhere and anytime.
Now everything is convenient, but time is running out, our service is expanding, becoming more complicated, and similar services with their dependencies are being added to it.
From now on, you need to deal with the creation of all these dependencies in the right sequence and in the right quantities.
Create a configuration, create a connection to the database and pass it to the repository designer. Then all this is transferred to the constructor of the service.
This becomes a problem when the number of dependencies and the services themselves grows.

But happily, these problems have already been solved, we can only choose approach.

Uber’s reflection-based approach

The main thing to know here is the concept of a container -
a place where providers our objects are added methods that create configurations and repositories objects and from where you will receive a Service object.
In dig, these are the provide and invoke methods, respectively.

The used technique is as follows:

  • Create a container
container := dig.New()
  • Add providers
  • Provide a finished object of Service
container.Invoke(func(server *Service) {

Google’s code generation approach

Here the approach is similar, but there are differences in implementation:

  • We create wire.go in which we describe everything that we need to create our service
func InitializeService() *Service {
wire.Build(NewService, NewConfig, DbConnect, NewRepository)
return &Service{}
  • Using the wire utility, we generate everything that we needed to write with our hands
// wire_gen.go
func InitializeService() *Service {
cfg:= NewConfig()
db:= DbConnect(cfg)
return NewService(cfg,repo)


Rob Pike: Reflection is never clear

I advise you to turn to the Google library. Their detailed instructions will help you start using it as quickly as possible.

Konstantin Makarov

Written by

Software Engineer

Welcome to a place where words matter. On Medium, smart voices and original ideas take center stage - with no ads in sight. Watch
Follow all the topics you care about, and we’ll deliver the best stories for you to your homepage and inbox. Explore
Get unlimited access to the best stories on Medium — and support writers while you’re at it. Just $5/month. Upgrade