In-depth analysis of several very mainstream dependency injection frameworks in golang, with implementation cases and principle analysis

What is Dependency Injection?
Dependency injection, the English full name is dependency injection, abbreviated as DI.

Encyclopedia explanation:
Dependency injection means that during the running of the program, if you need to call another object for assistance, you do not need to create a callee in the code, but rely on external injection.

Dependency injection is a classic design pattern that can effectively resolve complex object dependencies in a project.

For languages ​​with reflection functions, it is more convenient to implement dependency injection. There are several well-known open source libraries for dependency injection in Golang, such as google/wire, uber-go/dig, and facebookgo/inject.


When writing a program in a programming language, such as java, many classes are written, and these classes call each other to complete a specific function.

For example, to get data from MySQL, a MySQL operation class is required.

Write the mysql operation class for the first time:

class MySQL{
    
    
}

To obtain data from mysql, the configuration information such as the user name, password, address, etc. of the mysql database is also needed. Continue to write the MySQL class:

package com.demo.mysql

class MySQL {
    
    
    getMySQLConfig() {
    
    
        port = 3306;
        username = "xxx";
        password = "xxx";
    }
    
    initMySQL(){
    
    }
   
    querySQL(){
    
    }
}

Thinking further, what's wrong with the above MySQL operation program?

One of the principles of programming is: single responsibility

In other words, it is best for a class to only do one thing.

According to this principle, look at the MySQL class, which contains methods for obtaining database configuration data and operating MySQL, which is not a single responsibility.
Can the database configuration data be obtained in it, can it be extracted separately and represented by a class? sure.

Because most of the MySQL configuration data is read from the file, the above MySQL class is hard-coded, which is also unreasonable.
The source of the configuration file can be a yml format file, a toml format file, or a remote file.

Write the mysql operation class for the second time:
modify the above class and add a class to obtain the database configuration:

package com.demo.mysql

class MySQLConfig {
    
    
      getMySQLConfig() {
    
    
        // 从配置文件获取 mysql 配置数据
    }
}

The class to get the data becomes:

package com.demo.mysql

class MySQL {
    
    
    initMySQL(){
    
    
     // 获取数据库的配置信息
     mysqlconfig = new MySQLConfig();
    }
   
    querySQL(){
    
    }
}

Think about it, what's wrong with the rewritten class above?
To obtain the configuration information of mysql, do you need to create a new one in the MySQL class and instantiate it? If it is not in the same package, you must introduce the configuration class before instantiating it. Can it be optimized here, of course.

Directly inject the database configuration class into the MySQL operation class. This is dependency injection.

What is dependency? What is injection?
Who does the mysql operation class depend on? Depends on the database configuration class.
Inject what? Inject the database configuration class into the mysql operation class.
Injection is an action to inject a class into another class.
Dependency is a relationship, a class relationship, a class needs to rely on another class to fully function.
To complete data operations, the mysql operation class needs to rely on the database configuration class. Inject the database configuration class into the mysql operation class to complete the operation class function.

The third time to write mysql operation class:
pseudo code example:

package com.demo.mysql

class MySQL {
    
    
    private MySQLConfig config
    MySQL(MySQLConfig mysqlconfig) {
    
     // 数据库配置类这里注入到mysql操作类里
        config = mysqlconfig
    }
    initMySQL(){
    
    
    
    }
   
    querySQL(){
    
    }
}

Inject the database configuration class into the mysql operation class.

Anyone who writes java knows that there is a spring family bucket in the java framework, and the spring framework package has two cores, one of which is IoC and the other is aop.

The full name of IoC: Inversion of Control, Inversion of Control.

This inversion of control is also one of the principles of object-oriented programming.

But this inversion of control is difficult to understand. If you understand it in combination with the above DI, it will be easier to understand.
Think of DI as a concrete implementation of the IoC programming principles.

Dependency injection can also be understood from other software design ideas:
1) Separation of concerns
2) High cohesion and low coupling

For the operation of the database mysql and the configuration information of mysql, these two can be independent and separated from each other.

When to use dependency injection
When your project is not large in scale, there are not many files, and a file call only needs to pass in a small number of dependent objects, then using dependency injection will make the program cumbersome.

When the scale becomes larger and the use of a single object needs to call multiple dependent objects, and these dependencies have their own dependent objects, object creation becomes cumbersome at this time, then dependency injection can come into play at this time.

Wire concept description

Wire Introduction
Wire is a dependency injection code generation tool implemented in Go language that is open sourced by Google. It can generate corresponding dependency injection Go code according to the code you write.

Different from other dependency injection tools, such as uber's dig and facebook's inject, these two tools use reflection to implement dependency injection, and they are runtime dependency injection.

Wire is a dependency injection that generates code when compiling code, and is a compile-time dependency injection that injects dependency code during compilation. Moreover, during code generation, if there is a problem with dependency injection, an error will occur when generating the dependent code, and the problem can be reported instead of having to wait until the code is running to expose the problem.

Provider and injector
First, you need to understand two core concepts of wire: provider and injector.

From the example of java simulation dependency injection above, the steps of dependency injection can be simplified:

First: need to create a new class instance
Second: "inject" the New class instance into the class that needs to use it through the constructor or other methods
Third: Use the New instance in the class

From the above steps to understand the two core concepts of wire provider and injector.

Provider is equivalent to the class instance from New above.
The injector is equivalent to aggregating the required dependent functions before the "injection" action, and generating dependencies based on the aggregated functions.

provider: Provide an object.
injector: Responsible for generating new programs based on object dependencies.

provider
provider is an ordinary Go function, which can be understood as a constructor of an object. Provides the "artifacts" for generating the injector function below.

See the example below, from the go blog.

This blog was published on 2018.10.9, some information may be a bit old, and then refer to the github guide, this guide was last updated on 2021.1.26.

The following NewUserStore() function can be seen as a provider. This function needs to pass in *Config and *mysql.DB 2 parameters.

// NewUserStore 是一个 provider for *UserStore,*UserStore 依赖 *Config,*mysql.DB
func NewUserStore(cfg *Config, db *mysql.DB) (*UserStore, error) {
    
    ... ...}

// NewDefaultConfig 是一个 provider for *Config,没有任何依赖
func NewDefaultConfig() *Config {
    
    ...}

// NewDB 是 *mysql.DB 的一个 provider ,依赖于数据库连接信息 *ConnectionInfo
func NewDB(info *ConnectionInfo) (*mysql.DB, error){
    
    ...}

Providers can be combined into a set of provider sets. This is useful for providers that are often used together. They can be grouped together using the wire.NewSet method:

var SuperSet = wire.NewSet(NewUserStore, NewDefaultConfig)

It is also possible to add other provider sets to a provider set:

import (
    “example.com/some/other/pkg”
)

// ... ...
var MegaSet = wire.NewSet(SuperSet, pkg.OtherSet)

wire.NewSet() function:

This function can group related providers together and use them. Of course, it can also be used alone, such as var Provider = wire.NewSet(NewDB).

The return value of this NewSet function can also be used as a parameter of other NewSet functions, such as the above SuperSet used as a parameter.

Injector
We write a program to combine these providers (such as the initUserStore() function in the example below), and the wire command in wire will call providers in the order of dependencies to generate a more complete function. This is the injector.

First, write the signature function that generates the injector, and then use the wire command to generate the corresponding function.

Examples are as follows:

// +build wireinject

func initUserStore(info *ConnectionInfo) (*UserStore, error) {
    
    
    wire.Build(SuperSet, NewDB) // 声明获取 UserStore 需要调用哪些 provider 函数
    return nil, nil
}

Then use the wire command to generate the injector function from the above initUserStore function, and the generated function corresponds to the file name wire_gen.go.

wire command:

You can generate the injector by invoking Wire in the package directory。

The injector code can be generated by using the wire command directly under the package that generates the injector function.

wire.Build() function:

Its parameters can be one or more providers organized by wire.NewSet(), or providers can be used directly.

wire uses
the wire structure and method list:

func Build(...interface{
    
    }) string
type Binding
	func Bind(iface, to interface{
    
    }) Binding
type ProvidedValue
	func InterfaceValue(typ interface{
    
    }, x interface{
    
    }) ProvidedValue
	func Value(interface{
    
    }) ProvidedValue
type ProviderSet
	func NewSet(...interface{
    
    }) ProviderSet
type StructFields
	func FieldsOf(structType interface{
    
    }, fieldNames ...string) StructFields
type StructProvider
	func Struct(structType interface{
    
    }, fieldNames ...string) StructProvider

wire installation

go get github.com/google/wire/cmd/wire

Quick Start
Example 1
Create a basics folder first, then use go mod init basics in basics, create a new go.mod, and import wire into go.mod: require github.com/google/wire v0.5.0.

The entire folder directory structure:
insert image description here
Define providers
Create a new basics.go file under the basics folder and write the following code:

package main

import (
	"context"
	"errors"
)

type Student struct {
    
    
	ClassNo int
}

// NewStudent 就是一个 provider,返回一个 Student
func NewStudent() Student {
    
    
	return Student{
    
    ClassNo: 10}
}

type Class struct {
    
    
	ClassNo int
}

// NewClass 就是一个 provider,返回一个 Class
func NewClass(stu Student) Class {
    
    
	return Class{
    
    ClassNo: stu.ClassNo}
}

type School struct {
    
    
	ClassNo int
}

// NewSchool 是一个 provider,返回一个 School
// 与上面 provider 不同的是,它还返回了一个错误信息
func NewSchool(ctx context.Context, class Class) (School, error) {
    
    
	if class.ClassNo == 0 {
    
    
		return School{
    
    }, errors.New("cannot provider school when class is 0")
	}
	return School{
    
    ClassNo: class.ClassNo}, nil
}

Define injector
to create a new file wire.go, the code is as follows:

// +build wireinject

package main

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

var SuperSet = wire.NewSet(NewStudent, NewClass, NewSchool)

func initSchool() (School, error) {
    
    
	wire.Build(SuperSet)
	return School{
    
    }, nil
}

// +build wireinject, this line of code must be declared at the top of the package, indicating that this is an injector ready to be compiled

Use the wire command to generate the injector function code
Use the wire command to generate the injector code, execute the wire command in the basics directory:

$ wire
wire: D:\work\mygo\go-practice2\di\wire\basics\wire.go:9:1: inject initSchool: no provider found for context.Context needed by basics.School in provider set "SuperSet" (D:\work\mygo\go-practice2\di\wire\basics\wire.go:7:16)

wire: basics: generate failed
wire: at least one generate failure

An error was reported, look at the displayed error message, the most important thing is this line of information:

inject initSchool: no provider found for context.Context needed by basics.School in provider set "SuperSet"

Let's take a look at the initSchool function, it does not provide context.Context. Let's modify the function, import the context package, and add the parameter context.Context to the initSchool function:

func initSchool(ctx context.Context) (School, error) {
    
    
	wire.Build(SuperSet)
	return School{
    
    }, nil
}

Then use the command wire to compile:

$ wire
wire: basics: wrote D:\work\mygo\go-practice2\di\wire\basics\wire_gen.go

Generated injector code, wire_gen.go file:

// Code generated by Wire. DO NOT EDIT.

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

package main

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

// Injectors from wire.go:

func initSchool(ctx context.Context) (School, error) {
    
    
	student := NewStudent()
	class := NewClass(student)
	school, err := NewSchool(ctx, class)
	if err != nil {
    
    
		return School{
    
    }, err
	}
	return school, nil
}

// wire.go:

var SuperSet = wire.NewSet(NewStudent, NewClass, NewSchool)

The steps used by wire:

Write the provider first.
Then write the injector: organize related providers together to form a ProviderSet.
Finally, compile with the wire command: wire will generate code according to the dependencies between providers.
wire.NewSet function:

It can aggregate providers. Function 1 classification: A group of related providers can be written together to form a ProviderSet. Function 1 extends the second function to avoid too many providers that are difficult to manage.

wite.Build function:

func Build(...interface{
    
    }) string

Its argument is a variable-length list of providers. Group all related providers together and generate injector function code. It is a template function that generates the injector function.

Binding interface#
Example 1 above binds the structure and constructor. What if there is an interface interface involved? For example, the following code,

type Fooer interface {
    
    
    Hello()
}

type Foo struct{
    
    }

func (f Foo)Hello() {
    
    
    fmt.Println("hello")
}

func Bar struct{
    
    }

func NewBar() Bar {
    
    
    return Bar{
    
    }
}

There is an interface Fooer, how to bind this? At this time, you can use the [wire.Bind](wire/wire.go at v0.5.0 · google/wire · GitHub) function:

var bind = wire.Bind(new(Fooer), new(Foo))
var set = wire.NewSet(bind, NewBar)

// or
var set = wire.NewSet(wire.Bind(new(Fooer), new(Foo)), NewBar)

struct providers
struct can also be used directly as a provider. If the provider of the structure is only used for field assignment, then the function wire.Struct can be used for assignment.

type Foo int
type Bar int

func NewFoo() Foo {
    
    /* ... */}
func NewBar() Bar {
    
    /* ... */}

type FooBar struct {
    
    
    MyFoo Foo
    MyBar Bar
}

var set = wire.NewSet(
	NewFoo,
    NewBar,
    wire.Struct(new(FooBar), "MyFoo", "MyBar"),
)

More information:
https://github.com/google/wire
https://github.com/google/wire/blob/main/docs/guide.md
https://go.dev/blog/wire
https: //pkg.go.dev/github.com/google/wire#pkg-index


Dependency injection based on facebookgo/inject focuses on the following points:

The background of dependency injection and the problems it solves
The use of facebookgo/inject
The defect of facebookgo/inject
The background of dependency injection
For slightly more complex projects, we often encounter complex dependencies between objects. Manually managing and initializing these management relationships will be extremely cumbersome. Dependency injection can help us automatically implement dependency management and object attribute assignment, freeing us from these cumbersome dependency management.

Taking a common HTTP service as an example, we often divide the code into Controller, Service and other levels when developing the background. as follows:

type UserController struct {
    
    
	UserService *UserService
	Conf        *Conf
}

type PostController struct {
    
    
	UserService *UserService
	PostService *PostService
	Conf        *Conf
}

type UserService struct {
    
    
	Db   *DB
	Conf *Conf
}

type PostService struct {
    
    
	Db *DB
}

type Server struct {
    
    
    UserApi *UserController
    PostApi *PostController
}

In the above code example, there are two Controllers: UserController and PostController, which are used to receive related request logic of users and articles respectively. In addition, there will be Service related classes, Conf configuration files, DB connections, etc.

There are complex dependencies between these objects, which brings some troubles to the initialization of the project. For the above code, the corresponding initialization logic will probably be like this:

func main() {
    
    
	conf := loadConf()
	db := connectDB()

	userService := &UserService{
    
    
		Db:   db,
		Conf: conf,
	}

	postService := &PostService{
    
    
		Db: db,
	}

	userHandler := &UserController{
    
    
		UserService: userService,
		Conf:        conf,
	}

	postHandler := &PostController{
    
    
		UserService: userService,
		PostService: postService,
		Conf:        conf,
	}

	server := &Server{
    
    
		UserApi: userHandler,
		PostApi: postHandler,
	}

	server.Run()
}

A large section of logic is used for object initialization, and when there are more and more interfaces, the entire initialization process will be extremely lengthy and complicated.

For the above problems, dependency injection can be perfectly solved.

The use of facebookgo/inject
Next, we try to use facebookgo/inject to transform this code into dependency injection. as follows:

type UserController struct {
    
    
	UserService *UserService `inject:""`
	Conf        *Conf        `inject:""`
}

type PostController struct {
    
    
	UserService *UserService `inject:""`
	PostService *PostService `inject:""`
	Conf        *Conf        `inject:""`
}

type UserService struct {
    
    
	Db   *DB   `inject:""`
	Conf *Conf `inject:""`
}

type PostService struct {
    
    
	Db *DB `inject:""`
}

type Server struct {
    
    
	UserApi *UserController `inject:""`
	PostApi *PostController `inject:""`
}

func main() {
    
    
	conf := loadConf() // *Conf
	db := connectDB() // *DB

	server := Server{
    
    }

	graph := inject.Graph{
    
    }

	if err := graph.Provide(
		&inject.Object{
    
    
			Value: &server,
		},
		&inject.Object{
    
    
			Value: conf,
		},
		&inject.Object{
    
    
			Value: db,
		},
	); err != nil {
    
    
		panic(err)
	}

	if err := graph.Populate(); err != nil {
    
    
		panic(err)
	}

	server.Run()
}

First of all, every field that needs to be injected needs to be tagged with inject: "". The so-called dependency injection, the dependency here refers to the fields contained in the object, and the injection means that other programs will help you assign values ​​​​to these fields.

Second, we create a graph object using inject.Graph{}. This graph object will be responsible for managing and injecting all objects. As for why it is called Graph, in fact, the term is very vivid, because the dependency relationship between various objects is indeed like a graph.

Next, we use graph.Provide() to provide the object to be injected to the graph.

graph.Provide(
	&inject.Object{
    
    
		Value: &server,
	},
	&inject.Object{
    
    
		Value: &conf,
	},
	&inject.Object{
    
    
		Value: &db,
	},
);

Finally, the Populate function is called to start the injection.

As you can see from the code, we have provided three objects to the Graph in total. We provide the server object because it is a top-level object. The conf and db objects are provided because all objects depend on them, so it can be said that they are the basic objects.

But what about other objects? For example, what about UserApi and UserService? We didn't call Provide on the graph. So how do they complete the assignment and injection?

In fact, it can be easily seen from the following object dependency diagram.

insert image description here
It can be seen from this dependency graph that the conf and db objects belong to the root node, and all objects depend on and contain them. The server is a leaf node, and no other objects depend on it.

What we need to provide to the Graph are root nodes and leaf nodes, and for intermediate nodes, it can be derived from root nodes and leaf nodes. Graph will automatically inject the intermediate node Provide into Graph through the inject: "" tag.

For the above example, let's analyze in depth what happens when Populate is performed inside the Graph:

Graph first parses the server object and finds that it has two fields marked as inject: UserApi and PostApi. Its types UserController and PostController, these two types never appear in Graph. Therefore, Graph will automatically call Provide on this field and provide it to Graph.
When parsing UserApi, it is found that there are still two fields marked as inject: UserService and Conf. For UserService, a type that has not been registered in the Graph, it will be provided automatically. For Conf, Graph has been registered before, so just assign the registered object to this field directly.
The next step is to continue to parse step by step until there is no field tagged as inject.
The above is the entire dependency injection process.

The thing to note here is that in our example above, injected in this way, all objects in it are equivalent to singleton objects. That is, a type, only one instance object exists in Graph. For example, UserService in UserController and PosterController are actually the same object.

After our main function is transformed with inject, it will become very concise. And even if the business becomes more and more complex and there are more and more Handlers and Services, the injection logic in this main function will not change unless a new root node object appears.

Of course, for Graph, we can not only provide the root node and leaf node, we can also provide an instance of UserService by ourselves, which has no effect on the operation of Graph. It's just that the root node and leaf nodes are only provided, and the code will look more concise.

Advanced usage of inject
When we declare a tag, in addition to the default usage of inject: "", there are three other advanced usages:

inject: "private". private injection.
inject: "inline". Inline injection.
inject: "object_name". Named injection, where the object_name can be any name.
private (private injection)
As we mentioned above, by default, all objects are singleton objects. There can only be one instance object of a type. But it is also possible not to use a singleton object, and private provides this possibility.

For example:

type UserController struct {
    
    
	UserService *UserService `inject:"private"`
	Conf        *Conf        `inject:""`
}

Declare the UserService property in UserController as private injection. In this way, when graph encounters a private tag, it will automatically create a new UserService object and assign it to this field.

In this way, there are two instances of UserService in the Graph at the same time, one is the global instance of UserService, which is used by the default inject: "". One is dedicated to the UserService in the UserController instance.

But in actual development, there seems to be relatively few such private scenarios. In most cases, the default singleton object is enough.

inline (inline injection)
By default, the properties that need to be injected must be *Struct. But it can also be declared as a normal object. For example:

将 UserController 中的 UserService 属性声明为 private 注入。这样的话,graph 遇到 private 标签时,会自动的 new 一个全新的 UserService 对象,将其赋值给该字段。

这样 Graph 中就同时存在了两个 UserService 的实例,一个是 UserService 的全局实例,给默认的 inject:"" 使用。一个是专门给 UserController 实例中的 UserService 使用。

但在实际开发中,这种 private 的场景似乎也比较少,大部分情况下,默认的单例对象就足够了。

inline (内联注入)
默认情况下,需要注入的属性必须得是 *Struct。但是也是可以声明为普通对象的。例如:

type UserController struct {
    
    
	UserServ

Note that the type of UserService here is not a *UserService pointer type, but a common struct type. The struct type has value semantics in Go, so of course there is no singleton problem here.

Named injection
If we need to inject proprietary object instances into certain fields, then we may use named injection. The method of use is to write a unique name in the tag of inject. as follows:

type UserController struct {
    
    
	UserService UserService `inject:"named_service"`
	Conf        *Conf       `inject:""`
}

Of course, this name must not be named private and inline, these two are reserved words for inject.

At the same time, we must provide this named instance into the graph, so that the graph can connect the two objects.

graph.Provide(
	&inject.Object{
    
    
		Value: &namedService,
		Name: "named_service",
	},
);

In addition to injecting objects, maps can also be injected. as follows:

type UserController struct {
    
    
	UserService UserService       `inject:"inline"`
	Conf        *Conf             `inject:""`
	UserMap     map[string]string `inject:"private"`
}

It should be noted that the injection tag of the map must be inject: "private".

Defects of facebookgo/inject
facebookgo/inject is of course very useful, as long as you declare the inject: "" tag and provide a few objects, all dependencies can be injected automatically.

But due to the language design of Golang itself, facebookgo/inject also has some defects and short boards:

All fields that need to be injected need to be public. This is also a limitation of Golang, which cannot assign values ​​to private properties. So only public fields can be injected. But this will make the code a little less elegant. After all, we don't want to make many variables public.

Only attribute assignment can be performed, and initialization functions cannot be executed. facebookgo/inject will only help you inject good objects and assign values ​​to each attribute. But many times, we often need to perform other actions after the object assignment is completed. But for this requirement scenario, facebookgo/inject cannot support it well.

The reasons for these two problems are summarized as follows: Golang has no constructor.


  1. Dependency Injection and Inversion of Control
    Under normal circumstances, calling a function or method is an active and direct behavior of the caller. The caller clearly knows what the name of the called function is and what types of parameters it has, and calls it directly and actively; including object Initialization is also explicit direct initialization. The so-called "inversion of control" is to turn this active behavior into an indirect behavior. The caller does not directly call the function or object, but indirectly calls and initializes with the help of framework code. This behavior is called "inversion of control". Turn", the inversion of control can decouple the caller and the called party.

In general, the program that uses the library is the program that actively calls the function of the library, but the program that uses the framework is often driven by the framework, and the business code written under the framework is driven by the framework. This mode is "inversion of control". ".
"Dependency injection" is a method to achieve "inversion of control", which is to achieve inversion of control through injected parameters or instances.
Where is the value of inversion of control? In a word, "decoupling" allows the framework code of inversion of control to read the configuration and dynamically build objects.
Inversion of control is a way to solve complex problems, especially in web frameworks, it provides a good way for routing and flexible injection of middleware.
2. inject
inject is the implementation of dependency injection in Go language, which realizes dependency injection of structure (struct) and function. First think about how to call a function through a function name of type string. The conceivable method is to use map to implement a mapping from a string to a function. code show as below:

package main

func f1() {
    
    
	println("f1")
}
func f2() {
    
    
	println("f2")
}

func main() {
    
    
	funcs := make(map[string]func())
	funcs["f1"] = f1
	funcs["f2"] = f2

	funcs["f1"]()
	funcs["f2"]()
}

First of all, the first problem is that the Value type of map is written as func(), which cannot be applied to functions with different parameter and return value types. The solution is to define the Value of the map as an interface{} empty interface type, but it needs to be implemented with type assertion or reflection. The inject package realizes the injection and calling of functions by means of reflection. code show as below:

package main

import (
	"fmt"
	"github.com/codegangsta/inject"
)

type S1 interface{
    
    }
type S2 interface{
    
    }

func Format(name string, company S1, level S2, age int) {
    
    
	fmt.Printf("name=%s, company=%s, level=%s, age=%d!\n", name, company, level, age)
}

func main() {
    
    
	//控制实例的创建
	inj := inject.New()
	//实参注入
	inj.Map("Tom")
	inj.MapTo("tencent", (*S1)(nil))
	inj.MapTo("T4", (*S2)(nil))
	inj.Map(23)

	//函数反转调用
	inj.Invoke(Format)

}

inject provides a general function of injecting parameters to call functions, and inject.New() is equivalent to creating a control instance, which is used to implement injection calls to functions. The inject package not only provides injection of functions, but also implements injection of struct types. For example:

package main

import (
	"fmt"
	"github.com/codegangsta/inject"
)

type S1 interface{
    
    }
type S2 interface{
    
    }
type Staff struct {
    
    
	Name    string `inject`
	Company S1     `inject`
	Level   S2     `inject`
	Age     int    `inject`
}

func main() {
    
    
	//创建被注入实例
	s := Staff{
    
    }
	//控制实例的创建
	inj := inject.New()
	//初始化注入值
	inj.Map("tom")
	inj.MapTo("tencent", (*S1)(nil))
	inj.MapTo("T4", (*S2)(nil))
	inj.Map(23)
	//实现对 struct 注入
	inj.Apply(&s)
	//打印结果
	fmt.Printf("s = %v\n", s)
}

  1. Analysis of inject implementation principle
    The inject package has only 2 files, one is inject.go file and one inject_test.go file. Among them, inject.go is short and concise, only 157 lines of code including comments and blank lines, but it provides a perfect implementation of dependency injection.
    The entry function New
    inject.New() function builds a specific type of injector instance as the internal injection engine, and returns an interface of type Injector. This embodies an interface-oriented design idea: expose the interface method externally, and hide the internal implementation internally.
// New returns a new Injector.
func New() Injector {
    
    
	return &injector{
    
    
		values: make(map[reflect.Type]reflect.Value),
	}
}

The New method is used to initialize the injector struct and returns a pointer to the injector struct, but this return value is wrapped by the Injector interface.
Interface Design
Inject.go code defines 4 interfaces, including a parent interface and three sub-interfaces, as follows:

type Injector interface {
    
    
    Applicator //抽象生成注入结构实例的接口
    Invoker //抽象函数调用的接口
    TypeMapper //抽象注入参数的接口
    SetParent(Injector) //实现一个注入实例链,下游的能覆盖上游的类型
}

type Applicator interface {
    
    
    Apply(interface{
    
    }) error //Appky方法实现对结构的注入
}

type Invoker interface {
    
    
    Invoke(interface{
    
    }) ([]reflect.Value, error) //Invoke方法是对被注入实参函数的调用
}

type TypeMapper interface {
    
    
    // 基于调用reflect.TypeOf得到的类型映射interface{
    
    }的值。
    Map(interface{
    
    }) TypeMapper
    // 基于提供的接口的指针映射interface{
    
    }的值。
    // 该函数仅用来将一个值映射为接口,因为接口无法不通过指针而直接引用到。
    MapTo(interface{
    
    }, interface{
    
    }) TypeMapper
    // 为直接插入基于类型和值的map提供一种可能性。
    // 它使得这一类直接映射成为可能:无法通过反射直接实例化的类型参数,如单向管道。
    Set(reflect.Type, reflect.Value) TypeMapper
    // 返回映射到当前类型的Value. 如果Type没被映射,将返回对应的零值。
    Get(reflect.Type) reflect.Value
}

The Injector interface is the parent interface of the Applicator, Invoker, and TypeMapper interfaces, so the type that implements the Injector interface must also implement the Applicator, Invoker, and TypeMapper interfaces:

The Applicator interface only specifies the Apply member, which is used to inject the struct.
The Invoker interface only specifies the Invoke member, which is used to execute the callee.
The TypeMapper interface specifies three members. Both Map and MapTo are used to inject parameters, but they have different usages. Get is used to obtain the injected parameters when calling.
In addition, Injector also specifies the behavior of SetParent, which is used to set the parent Injector, in fact, it is equivalent to finding inheritance. That is to say, when obtaining the injected parameters through the Get method, it will go back to the parent. This is a recursive process until the parameters are found or terminated by nil.

Injector exposes all methods to external users, and these methods can be classified into two categories:

The first type of method is to initialize the parameter injection, and the injection of the field of the structure type and the parameter injection of the function are unified into a set of method implementation; the second type
is the special injection implementation, which is to generate the structure object and call the function method respectively.
Note: Regardless of the actual parameters of the function or the fields of the structure, they are all stored in the map of type map[reflecct.Type] reflect.Value inside the inject.

The processing flow of the entire inject package is as follows:

Create an injection engine through inject.New(), the injection engine is hidden, and the return is the Injector interface type variable.
Call the TypeMapper interface (Injector embedded TypeMapper) method to inject the field value of the struct or the actual parameter value of the function. Call the
Invoker method to execute the injection function, or call the Applicator interface method to obtain the injected structure instance.

Internal implementation:
first look at the data structure of the injector. The injector is the only struct defined in the inject package. All operations are performed based on the injector struct, which has two members values ​​and parent. Values ​​is used to save the injected parameters. It is a map with reflect.Type as the key and reflect.Value as the value. Understanding this will help you understand Map and MapTo.

type injector struct {
    
    
	values map[reflect.Type]reflect.Value
	parent Injector
}

// InterfaceOf dereferences a pointer to an Interface type.
// It panics if value is not an pointer to an interface.
func InterfaceOf(value interface{
    
    }) reflect.Type {
    
    
	t := reflect.TypeOf(value)

	for t.Kind() == reflect.Ptr {
    
    
		t = t.Elem()
	}

	if t.Kind() != reflect.Interface {
    
    
		panic("Called inject.InterfaceOf with a value that is not a pointer to an interface. (*MyInterface)(nil)")
	}

	return t
}

The values ​​stored in values ​​can be the field type and value injected into the struct, or the type and value of the function argument. Note that values ​​is a map whose key is reflect.Type. If the field types of a structure are the same, the parameters injected later will overwrite the previous parameters. The way to avoid this is to use the MapTo method to avoid overwriting by abstracting an interface type.

Although the InterfaceOf method has only a few lines of implementation code, it is the core of the Injector. The parameter of the InterfaceOf method must be a pointer to an interface type, otherwise a panic is raised. The return type of the InterfaceOf method is reflect.Type. Note: The member values ​​of the injector is a map with the reflect.Type type as the key. The function of this method is actually just to get the type of the parameter, not to care about its value. The InterfaceOf method is used to get the parameter type, regardless of what value it specifically stores.

func (i *injector) MapTo(val interface{
    
    }, ifacePtr interface{
    
    }) TypeMapper {
    
    
	i.values[InterfaceOf(ifacePtr)] = reflect.ValueOf(val)
	return i
}

The role of the parent in the injector is to implement multiple injection engines, which form a chain.

The injection of the injector to the function is implemented as follows:

// Invoke attempts to call the interface{
    
    } provided as a function,
// providing dependencies for function arguments based on Type.
// Returns a slice of reflect.Value representing the returned values of the function.
// Returns an error if the injection fails.
// It panics if f is not a function
func (inj *injector) Invoke(f interface{
    
    }) ([]reflect.Value, error) {
    
    
	t := reflect.TypeOf(f) //获取函数类型的Type

	// 构造一个存放函数实参Value值的数组
	var in = make([]reflect.Value, t.NumIn()) //Panic if t is not kind of Func
	//使用反射获取函数实参reflect.Type,逐个去injector中查找注入的Value值
	for i := 0; i < t.NumIn(); i++ {
    
    
		argType := t.In(i)
		val := inj.Get(argType)
		if !val.IsValid() {
    
    
			return nil, fmt.Errorf("Value not found for type %v", argType)
		}

		in[i] = val
	}

	//反射调用函数
	return reflect.ValueOf(f).Call(in), nil
}

The Invoke method is used to dynamically execute the function. Of course, parameters can be injected through Map or MapTo before execution, because the function executed through Invoke will take out the injected parameters, and then call it through the Call method in the reflect package. The parameter f received by Invoke is an interface type, but the underlying type of f must be func, otherwise it will panic.

**Apply method is used to inject the fields of struct, and the parameter is a pointer to the underlying type of structure. **The premise of being injectable is: the field must be exported (that is, the field name starts with a capital letter), and the tag of this field is set to inject.

// Maps dependencies in the Type map to each field in the struct
// that is tagged with 'inject'.
// Returns an error if the injection fails.
func (inj *injector) Apply(val interface{
    
    }) error {
    
    
	v := reflect.ValueOf(val)

	for v.Kind() == reflect.Ptr {
    
    
		v = v.Elem()
	}

	if v.Kind() != reflect.Struct {
    
    
		return nil // Should not panic here ?
	}

	t := v.Type()

	for i := 0; i < v.NumField(); i++ {
    
    
		f := v.Field(i)
		structField := t.Field(i)
		if f.CanSet() && (structField.Tag == "inject" || structField.Tag.Get("inject") != "") {
    
    
			ft := f.Type()
			v := inj.Get(ft)
			if !v.IsValid() {
    
    
				return fmt.Errorf("Value not found for type %v", ft)
			}

			f.Set(v)
		}

	}

	return nil
}

Comparison of Map and MapTo methods
** Both Map and MapTo methods are used to inject parameters, which are stored in the member values ​​of the injector. **The functions of these two methods are exactly the same, the only difference is that the Map method uses the type of the parameter value itself as the key, and the MapTo method has an additional parameter that can specify a specific type as the key. But the second parameter ifacePtr of the MapTo method must be an interface pointer type, because ifacePtr will eventually be used as a parameter of the InterfaceOf method.

// Maps the concrete value of val to its dynamic type using reflect.TypeOf,
// It returns the TypeMapper registered in.
func (i *injector) Map(val interface{
    
    }) TypeMapper {
    
    
	i.values[reflect.TypeOf(val)] = reflect.ValueOf(val)
	return i
}

func (i *injector) MapTo(val interface{
    
    }, ifacePtr interface{
    
    }) TypeMapper {
    
    
	i.values[InterfaceOf(ifacePtr)] = reflect.ValueOf(val)
	return i
}

// Maps the given reflect.Type to the given reflect.Value and returns
// the Typemapper the mapping has been registered in.
func (i *injector) Set(typ reflect.Type, val reflect.Value) TypeMapper {
    
    
	i.values[typ] = val
	return i
}

func (i *injector) Get(t reflect.Type) reflect.Value {
    
    
	val := i.values[t]

	if val.IsValid() {
    
    
		return val
	}

	// no concrete types found, try to find implementors
	// if t is an interface
	if t.Kind() == reflect.Interface {
    
    
		for k, v := range i.values {
    
    
			if k.Implements(t) {
    
    
				val = v
				break
			}
		}
	}

	// Still no type found, try to look it up on the parent
	if !val.IsValid() && i.parent != nil {
    
    
		val = i.parent.Get(t)
	}

	return val

}

func (i *injector) SetParent(parent Injector) {
    
    
	i.parent = parent
}

Why is there a MapTo method? Because the injected parameters are stored in a map with type as the key, it is conceivable that when there are more than one parameter of the same type in a function, the parameter injected after executing Map will overwrite the previous one passed through Map Injected parameters.

The SetParent method is used to specify the parent Injector for an Injector. The Get method retrieves the corresponding value from the values ​​member of the injector through reflect.Type, it may check whether the parent is set, until it finds or returns an invalid value, and finally the return value of the Get method will be verified by the IsValid method.

Inject is very indirect for function injection calls, which is to obtain function parameters from the injector and then call the function.

The sample code is as follows:

package main

import (
    "fmt"
    "github.com/codegangsta/inject"
)

type SpecialString interface{
    
    }

type TestStruct struct {
    
    
    Name   string `inject`
    Nick   []byte
    Gender SpecialString `inject`
    uid    int           `inject`
    Age    int           `inject`
}

func main() {
    
    
    s := TestStruct{
    
    }
    inj := inject.New()
    inj.Map("张三")
    inj.MapTo("男", (*SpecialString)(nil))
    inj2 := inject.New()
    inj2.Map(26)
    inj.SetParent(inj2)
    inj.Apply(&s)
    fmt.Println("s.Name =", s.Name)
    fmt.Println("s.Gender =", s.Gender)
    fmt.Println("s.Age =", s.Age)
}

  1. Advantages and disadvantages of reflection Advantages
    : versatility, flexibility
    Disadvantages: reflection is fragile, reflection is obscure, reflection has some performance loss (providing the ability to dynamically modify the program state, it must not be a direct address reference, but to Construct an abstraction layer with runtime)
    The best practice of reflection: 1. Use reflection inside the library or framework instead of exposing the reflection structure to the caller; 2. Framework code only considers the use of reflection, and general business does not need to abstract to 3. Do not use reflection techniques unless there is no other way.

Guess you like

Origin blog.csdn.net/u014374009/article/details/129003378