Dependency injection in GO

April 21, 2020 Dependency Injection Container Singleton Multiton


Dependency injection in GO

Let’s talk about dependency injection pattern and dependency management in large programs.

Logger example

In any program there is main.go which manages to initialize and start some service(s).

We may say that every service in GO doesn’t implement all its logic. Sometimes it requires other services and relies on them in particular parts of logic.

For example, logging is often delegated to some logger entity, for example zap:

type Server struct {
	logger *zap.Logger
}

func NewServer(logger *zap.Logger) *Server {
	return &Server{logger: logger}
}

func (s *Server) Handle() {
	// do some work
	s.logger.Info("request processed")
}

logger := //... logger initializing
NewServer(logger).Run() // service with logger initializing

It is good to reuse code and rely on an entity that does its work well instead of writing your own code.

Currently our Server is not logging by itself, Server relies on logger. In other words, logger became the dependency of Server. We saved logger as a property of Server. By doing it we injected logger as a dependency.

Definition

Dependency injection — pattern of composing entities, as a result of which the first (parent) entity is saved to the state of second (dependency) entity. Parent entity can call dependency entity when it is necessary.

Parent state change is important to distinguish dependency injection and external function call.

Without state change the basic “hello world” program can be mistakenly recognized as dependency injection:

func main() {
	fmt.Println("hello world")
}

There is no state in main function, so it is not dependency injection.

Issues

Why do I discuss dependency injection and which issues can be behind this topic?

Issues can appear in programs that have a large amount of entities having a lot of links between them. If there are a lot of linked entities, there is a lot of their initialization code. Such code with proper logic structure makes service difficult to support.

Service with more dependencies

Let’s imagine that we are developing a service that has to do the following:

  • database interaction;
  • perform external service calls;
  • logging;
  • loading and using config;

The service constructor should look like:

func NewService(
	db *sql.DB,
	bankClient *client.Bank,
	cfg *config.Config,
	logger *zap.Logger,
)

Also, every Service dependency requires its own initialization, that can require other entities. For example:

// Getting db connection
db, err := sql.Open("postgres", fmt.Sprintf(
	"host=%s port=%s user=%s dbname=%s sslmode=disable password=%s",
	configStruct.DbHost,
	configStruct.DbPort,
	configStruct.DbUser,
	configStruct.DbName,
	configStruct.DbPass)
)
if err != nil {
	log.Fatal(err)
}

To create bankClient we need cfg and logger.

Now let’s imagine there is a second service needed to be implemented in the same program, that also requires db, cfg, logger as dependencies. Let’s visualize the dependencies scheme:

deps2.svg

There is a lot of code to initialize the first service, but also we need to initialize the second.

Copy init code

We could just copy-paste db, cfg, logger init code for service2.

It will work, but copying code is a bad idea. More code to support, more mistake probability.

Let’s check other options.

Implement init code for each dep

For example we can implement db init function:

func GetDB(cfg *config.Config) (*sql.DB, error) {
	db, err := sql.Open("postgres", fmt.Sprintf(
		"host=%s port=%s user=%s dbname=%s sslmode=disable password=%s",
		configStruct.DbHost, configStruct.DbPort, configStruct.DbUser, configStruct.DbName, configStruct.DbPass)
	)
	if err != nil {
		return nil, err
	}

	return db, nil
}

It looks good and there will be no duplicate db init code. But we still need to implement that code for each reusable dep.

We still haven’t finished with GetDB - it will create a new connection for each call.

Singleton

In case of db we need a single instance.

Let’s implement it with singleton pattern:

package db

var db *sql.DB

func GetDB(cfg *config.Config) (*sql.DB, error) {
	if db != nil {
		return db, nil
	}

	var err error
	db, err = sql.Open("postgres", fmt.Sprintf(
		"host=%s port=%s user=%s dbname=%s sslmode=disable password=%s",
		configStruct.DbHost, configStruct.DbPort, configStruct.DbUser, configStruct.DbName, configStruct.DbPass)
	)
	if err != nil {
		return nil, err
	}

	return db, nil
}

Pool of singletons (multiton)

We can have connections to different database servers, it should be separate connections. But we still need each of them to be singleton. Let’s implement pool of singletons — multiton.

On a small number of entities these patterns work well. But if there are dozens of entity types even that simple code like singleton and multiton are hard to implement. In that case we could use some centralized logic that helps to build entities — dependency injector.

Dependency injection container (injector)

Using a separate entity to build and store other entities (injector) is pretty common in many programming languages. Container implements logic about creating each entity, storing and getting.

The focus in the program that uses container is moved from entity and its links to the container that helps to simplify the code.

container.svg

Sometimes container work is so predictable that one can specify dependencies in declarative format — XML, YAML.

In Symfony (PHP) service container is one of the central parts in the framework - even Symfony core components are designed to work with container. Symfony supports XML and YAML to declare dependencies.

In Spring (JAVA) dependency container can be configured by XML or annotations.

There are several libraries in GO implementing injector differently.

I used some of them and prepared a review about each of them below. There is source code about di libraries interaction in a separate github repository.

uber-go/dig

dig allows us to configure container by passing anonymous functions and uses reflect package.

One should use Provide method to add entity init function into container. The function should return the desired entity, or both entity and error.

Let’s see how we can create logger that depends on config. (It is almost the original example from dig readme).

c := dig.New()
err := c.Provide(func() (*Config, error) {
	// In real program there should be reading from the file, for example
	var cfg Config
	err := json.Unmarshal([]byte(`{"prefix": "[foo] "}`), &cfg)
	return &cfg, err
})
if err != nil {
	panic(err)
}

// Function to create logger by using config
err = c.Provide(func(cfg *Config) *log.Logger {
	return log.New(os.Stdout, cfg.Prefix, 0)
})
if err != nil {
	panic(err)
}

By using reflect package dig analyzes the types of returning value and the types of parameters. Using that data the links between entities are resolved.

To get entity from container there is Invoke method:

err = c.Invoke(func(l *log.Logger) {
	l.Print("You've been invoked")
})
if err != nil {
	panic(err)
}

On identical entity creation one should pass name parameter when calling Provide. Otherwise Provide will return error.

// creating another logger
err = c.Provide(
	func(cfg *Config) *log.Logger {
		return log.New(os.Stdout, cfg.Prefix, 0)
	},
	dig.Name("logger2"), // passing name option
)
if err != nil {
	panic(err)
}

Unfortunately getting named entity is not so simple — there is no name parameter in Invoke function. In the related github issue developers say that the issue is fixed, but not released yet. Currently one should use structure with tagged fields to invoke named entities:

c := dig.New()
c.Provide(username, dig.Name("username"))
c.Provide(password, dig.Name("password"))

err := c.Invoke(func(p struct {
	dig.In

	U string `name:"username"`
	P string `name:"password"`
}) {
	fmt.Println("user >>>", p.U)
	fmt.Println("pwd  >>>", p.P)
})

dig (and every injector library here) implements lazy loading of entities. Required entities are created only on Invoke call.

We could speak about that reflect is slow, but for container it doesn’t matter, because typically container is used once on program start.

As a result: named entities issue should be documented in dig main readme. In other cases it works perfectly as injector.

elliotchance/dingo

elliotchance/dingo works in a completely different way. One should specify YAML config in order to generate container’s GO-code. Let’s continue with logger-config example. Our YAML should look like:

services:
  Config:
    type: '*Config'
    error: return nil
    returns: NewConfig()
  Logger:
    type: '*log.Logger'
    import:
      - 'os'
    returns: log.New(os.Stdout, @{Config}.Prefix, 0)

To me YAML is not very comfortable to use here. You will see below, that some parts of YAML could be actually the parts of GO code. But to me the GO code is comfortable to be in *.go files — at least the IDE will check go syntax.

For every entity in YAML probably need to specify following:

  • imports — the list of imported libraries;
  • error — GO code, that should be called on error check;
  • returns — the part of GO code which will init and return the entity;

With returns I couldn’t decide: should I add big portion of GO code into the YAML, or should I create constructor function for each entity. Finally I moved all config construction logic to NewConfig function:

func NewConfig() (*Config, error) {
	var cfg Config
	err := json.Unmarshal([]byte(`{"prefix": "[foo] "}`), &cfg)
	return &cfg, err
}

When the YAML is ready, one should install dingo binary and call it in the project directory — go get -u github.com/elliotchance/dingo; dingo.

Code generation works fast. To me it looks like that the most settings from YAML are just directly copied into generated *.go file. So, generated file could be invalid. Generated code is placed in file dingo.go. Container is simple structure with fields for every entity with singleton logic:

type Container struct {
	Config	*Config
	Logger	*log.Logger
}

func (container *Container) GetLogger() *log.Logger {
	if container.Logger == nil {
		service := log.New(os.Stdout, container.GetConfig().Prefix, 0)
		container.Logger = service
	}
	return container.Logger
}

As a result: elliotchance/dingo helps to generate simple typed container from YAML, but putting GO code to YAML makes me feel a little bit uncomfortable.

sarulabs/di

sarulabs/di looks like dig, but doesn’t use reflect. All deps in di must have unique names.

The main difference is that in dig we don’t have to init dependencies of our entity, even from container — they just come as function parameters. In di we have to pull dependencies from container:

err = builder.Add(di.Def{
	Name: "logger",
	Build: func(ctn di.Container) (interface{}, error) {
		// Getting config from container to init logger
		var cfg *Config
		err = ctn.Fill("config", &cfg)
		if err != nil {
			return nil, err
		}

		// Init logger
		return log.New(os.Stdout, cfg.Prefix, 0), nil
	}
})

GO code that gets dependency from container is not big, but it will be copied between entities with similar dependencies.

But also sarulabs/di has a bonus — one can specify not only creation function, but also a container destroy hook function. di container destroy starts with DeleteWithSubContainers call and can be performed on program shutdown.

Close: func(obj interface{}) error {
	if _, ok := obj.(*log.Logger); ok {
		fmt.Println("logger close") // this code is called on logger destroy
	}
	return nil
}

As I mentioned before di doesn’t use reflect and also doesn’t store any information about entities types, that’s why we should use type assertion in Close function to get logger to original type.

There is also a bonus functionality sarulabs/dingo, from the same developer, that also provides strictly typed container and code generation.

As a result: di is great injector, but there is some code copying logic — to get dependency from container.

dig is better here.

google/wire

With wire we have to put construction function template code for each entity. We should place //+build wireinject comment to the beginning of such template files.

Then we should run go get github.com/google/wire/cmd/wire; wire which generates *_gen.go files for each template file. Generated code will contain real constructor functions that are generated from templates.

For our logger-config example the template of logger constructor will look like:

//+build wireinject

package main

import (
	"log"

	"github.com/google/wire"
)

// Template for generation
func GetLogger() (*log.Logger, error) {
	panic(wire.Build(NewLogger, NewConfig))
}

Generated code is put into *_gen.go and looks like:

import (
	"log"
)

// Injectors from wire.go:

func GetLogger() (*log.Logger, error) {
	config, err := NewConfig()
	if err != nil {
		return nil, err
	}
	logger := NewLogger(config)
	return logger, nil
}

As in elliotchance/dingo there is code generation in wire. But I didn’t manage to generate invalid GO code. In every invalid template situation wire outputs the errors and code is not generated.

There is one minus in wire — we have to implement constructor template by using wire package calls. And these calls are not so expressive as GO code. So I also move all constructor logic to the constructor functions to just call these constructor functions from templates.

Comparison table

LibraryDependencies formatGO code generationTypingCode reduction
uber-go/digGO code, anonymous functions with parametersNoStrict, but also reflect is usedMaximum
elliotchance/dingoYAMLYesStrictMaximum, but there is mixing of GO code in YAML
sarulabs/diGO code, declaration of Build functions with manual parameters gettingNo, but sarulabs/dingo allows thatAll deps are stored as interface{}. But sarulabs/dingo offers strictly typed containerGood, but we have to get deps from container
google/wireGO code — templates of constructor functionsYesStrictMaximum
Tags: