04/21/2020 dependency injection container singleton multiton
Let's talk about dependency injection pattern and about dependency management in large programs.
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.
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.
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.
Let's image that we are developing serice that has to do following:
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
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.
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.
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 }
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.
to show code of multiton implementationpackage 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.
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.
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 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
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.
code example to create logger with sarulabs/dibuilder, 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.
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:
reflect
is usedBuild
functions with manual parameters getting.interface{}
.
But sarulabs/dingo
offers strictly typed container.