Dependency injection in GO

04/21/2020 dependency injection container singleton multiton


Dependency injection in GO

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

Logger example

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

We may say, that every service in GO doesn't implement all his logic. Sometimes it requires any other services and rely 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 entity that does his work good instead of writing your on 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 program that have large amount of entities having a lot of links between them.
If there are a lot of linked entities, there a lot of their initialization code. Such code with proper logic structure makes service difficult to support.

Service with more dependencies

Let's image that we are developing serice that has to do 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 it's own initialization, that can require other entities. For

// 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 и logger.

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

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

Let's

Copy init code

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

It will work, but to copy code is 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 not finished with GetDB - it will create new connection for each call.

Singleton

In case of db we need the 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
}

Poof 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 — mulition.



package db

var pool *DBPool

type DBPool struct {
	lock sync.Mutex
	pool map[string]*sql.DB
}

func init() {
	if pool == nil {
		pool = &DBPool{
			pool:make(map[string]*sql.DB)
		}
	}
}

func GetDB(dbAlias string, cfg *config.Config) (*sql.DB, error) {
	pool.lock.Lock()
	defer pool.lock.Unlock()

	db, ok := pool[dbAlias]
	if ok {
		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[dbAlias].DbHost, 
		configStruct[dbAlias].DbPort, 
		configStruct[dbAlias].DbUser, 
		configStruct[dbAlias].DbName, 
		configStruct[dbAlias].DbPass)
	if err != nil {
		return nil, err
	}

	pool[dbAlias] = db

	return db, nil
}

On small number of entities these patterns work good.
But if there are a 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)

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

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

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

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

В 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 the review about each of them below. There is a 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 use see how we can create logger that depends on config. (It it almost the original example from dig readme).

c := dig.New()
err := c.Provide(func() (*Config, error) {
	// In real program there should 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.

// создаем еще один логгер
err = c.Provide(
	func(cfg *Config) *log.Logger {
		return log.New(os.Stdout, cfg.Prefix, 0)
	}, 
	dig.Name("logger2"), // передаем опцию имени
)
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 no 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 is 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 case 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 fo 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 make me feel a little bit uncomfortable.

sarulabs/di

sarulabs/di looks like dig, but don'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 came 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.



builder, err := di.NewBuilder()
if err != nil {
	panic(err)
}

err = builder.Add(di.Def{
	Name: "config",
	Build: func(ctn di.Container) (interface{}, error) {
		var cfg Config
		err := json.Unmarshal([]byte(`{"prefix": "[foo] "}`), &cfg)
		return &cfg, err
	},
})
if err != nil {
	panic(err)
}

err = builder.Add(di.Def{
	Name: "logger",
	Build: func(ctn di.Container) (interface{}, error) {
		var cfg *Config
		err = ctn.Fill("config", &cfg)
		return log.New(os.Stdout, cfg.Prefix, 0), nil
	},
	Close: func(obj interface{}) error {
		if _, ok := obj.(*log.Logger); ok {
			fmt.Println("logger close")
		}
		return nil
	},
})
if err != nil {
	panic(err)
}

But also sarulabs/di has a bonus — one can specify not creation function only, but also a container destroy hook function. di container destroy starts with DeleteWithSubContainers call and can 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 befored di doesn't use reflect and also don'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 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"
)

// Шаблон для генерации 
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 a 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 suср expressive as GO code. So i also move all constructor logic to the constructor functions to just call these constructor functions from templates.

There is full results table:

uber-go/dig

  • Dependencies format: GO code, anonymous functions with parameters
  • GO code generation: No
  • Typing: Strict, but also reflect is used
  • Code reduction: Maximum

elliotchance/dingo

  • Dependencies format: YAML
  • GO code generation: Yes
  • Typing: Strict
  • Code reduction: Maximum, but there is a mixins of GO code in YAML

elliotchance/dingo

  • Dependencies format: YAML
  • GO code generation: Yes
  • Typing: Strict
  • Code reduction: Maximum, but there is a mixins of GO code in YAML

sarulabs/di

  • Dependencies format: GO code, declaration of Build functions with manual parameters getting.
  • GO code generation: No, but sarulabs/dingo allows that.
  • Typing: All deps as stored as interface{}. But sarulabs/dingo offers strictly typed container.
  • Code reduction: Good, but we have to get deps from container.

google/wire

  • Dependencies format: GO code — templates of constructor functions.
  • GO code generation: Yes.
  • Typing: Strict.
  • Code reduction: Maximum.