We use Google/Wire [3] for dependency injection in Kratos-Layout [2], the default project template of the microservices framework Kratos V2 [1], and recommend developers to use this tool when maintaining their projects.

Wire seems counterintuitive at first glance, leading to many students not understand why or how to use it (including me), so this article is to help you understand the use of wire.

What

Wire [4] is an open source dependency injection code generation tool for the Go language by Google. It generates Dependency Injection Go code based on your code.

Unlike other dependency injection tools that rely on reflection, Wire can report problems with dependency injection at compile time (code generation, to be precise). If there is a problem with dependency injection, it can be reported at compile time, rather than at run time, making it easier to debug.

Why

Understanding Dependency Injection

What is Dependency Injection? Why Dependency Injection? Dependency Injection is Java Toxic (No)

Dependency Injection (DI), understood as a pattern of code construction, makes your code easier to maintain by writing in this way.

For many of the ideas of software design patterns and architectures, it’s hard to understand why they have to go round and round and do complex gymnastics and implement them in strange ways. They usually just throw in a sample and say this is nice and elegant, but by omiting the derivation of how the pattern was developed, we only see the result, which makes it difficult to understand. So let’s try to destruct the process and see how and why the code evolved into the DI pattern so that we can better understand what it means to use DI.

What is dependency?

Dependency here is a noun, not a dependency on a software package (like the stuff tucked into node\_modules), but a dependency on some other external module (object/instance) that a module (object/instance) in the software depends on.

Where is it injected?

Dependent modules are injected (passed as parameters) into the module when the module is created.

What does DI look like? What does DI look like?

The following use GO pseudocode to do an example, understand the spirit can be.

Let’s say you’re working on a Web application that has a simple interface. The initial project code might look something like this:

# The following is pseudocode. Type App struct {} # assume that this method will match and handle requests such as GET /biu/<id> func (a *App) getData (id string) string {# todo: write your data query return "some data" } func NewApp() *App { return &App{} } app := App() app.Run()

What you need to do is take a MySQL file, check the data from it by ID, and return it. To connect to MySQL, let’s say that we already have a method called “newMySQLClient” that returns the client to you, initializes it with an address to get the database connection, and let’s say that it has a method called “Exec” that gives you execution parameters.

Instead of DI, a dependency instance is passed through a global variable

One way to write it is to initialize the client globally on the outside, and then the App calls it directly.


var mysqlUrl = "mysql://blabla"
var db = NewMySQLClient(mysqlUrl)


type App struct {

}

func (a *App) GetData(id string) string {
    data := db.Exec("select data from biu where id = ? limit 1", id)
    return data
}


func NewApp() *App {
    return &App{}
}
func main() {
    app := App()
    app.Run()
}

There is no dependency injection. The app relies on the global variable db, which is a bad way to do it. The DB object is dangerously floating in the global scope and exposed to other modules under the package. (Imagine what would happen if other code in the package secretly replaced your DB variable at runtime.)

Instead of DI, create a dependency instance in the App’s initialization method

Another way to do it is like this:

type App struct {
    db *MySQLClient
}

func (a *App) GetData(id string) string {
    data := a.db.Exec("select data from biu where id = ? limit 1", id)
    return data
}


func NewApp() *App {
    return &App{db: NewMySQLClient(mysqlUrl)}
}
func main() {
    app := NewApp("mysql://blabla")
    app.Run()
}

This is a slightly better approach, the DB is tucked into the app, so it’s safer to have irrelevant code outside of the app touch it, but it’s still not a dependency injection, it’s creating a dependency internally, and you’ll see the problems that come with that.

Boss: We need to store our data in another place (need to change implementation)

Your boss heard it out of nowhere — Redis is so quick, why don’t we read our data from Redis instead? At this time, your heart is a little bit broken, but after all, if you want a meal, just bite the bullet and change the code above.

type App struct {
    ds *RedisClient
}

func (a *App) GetData(id string) string {
    data := a.ds.Do("GET", "biu_"+id)
    return data
}


func NewApp() *App {
    return &App{ds: NewRedisClient(redisAddr)}
}

func main() {
    app := NewApp("redis://ooo")
    app.Run()
}

There are basically 3 changes above:

  1. In the App initialization method, I’ve changed it to initialize redisClient
  2. When fetching data from get\_data, the run method is used, and the query statement is changed
  3. When the App is instantiated, the argument passed in is changed to the Redis address
Boss: how about we change a place to save again? / Mock it out/Mock it out/Mock it out

The boss’s thinking is always broad, and two days later he wanted to switch to Postgres. Or you can write some test code for your App that only tests the logic in the interface. Usually we don’t want to put another database on the side, so we need to mock out the data source and make it return data directly to the requesting handler to test it accordingly.

What happens in this case? Change the code inside? It’s not scientific.

Interface oriented programming

An important idea is to program for interfaces, not implementations.

What does implementation-oriented programming mean? For example, in the example above, the mysqlclient exec\_sql executes an SQL, instead of the redisclient do executes a GET instruction. Because each client’s interface design is different, each implementation has to be changed.

Programming for interfaces, on the other hand, is a completely different idea. Let’s not just write code for whatever the boss wants to use. The first thing to anticipate is that the implementation of this data source is likely to be changed, so you should be prepared at the outset (design).

Design of the interface

Python has a concept called duck-typing, which means that if you sound like a duck, walk like a duck and swim like a duck, you are a duck. Quack, walk and swim are our agreed duck interfaces, and if you fully implement these interfaces, we can treat you like a duck.

In our example above, whether MySQL implementation or Redis implementation, they all have a common function: use an ID, look up a data, then this is the common interface.

We can contract an interface called DataSource that must have a method called getById that accepts an ID and returns a string

type DataSource interface {
    GetById(id string) string
}

Then we can encapsulate each data source separately and implement the interface according to this interface definition, so that the part of our App that handles the request can call the getById method stably. However, as long as the underlying data implementation implements the interface of DataSource, it can be replaced in a fancy way, without changing the code inside the App.

// Enclose redis type redis struct {r *RedisClient} func NewRedis(addr string) *redis {return &redis{db: NewRedisClient(addr)} } func (r *redis) GetById(id string) string { return r.r.Do("GET", "Biu_ "+id)} struct mysql_struct {m *MySQLClient} func newMySQL (addr string) *redis {return &mysql{db: NewMySQLClient(addr)} } func (m *mysql) GetById(id string) string { return r.m.Exec("select data from biu where id = ? Limit 1", id)} type App struct {ds dataSource} func NewApp(addr string) *App {return &App{ds: Return &app {ds: newRedisClient (addr)}} return &app {ds: newRedisClient (addr)}

Since both data sources implement the DataSource interface, you can simply create one and insert it into your App. Which one you want to use?

Wait a minute. What seems to be missing

Addr as a parameter, isn’t that a little simple? Typically initializing a database connection may have a bunch of parameters in a YAML file that need to be parsed into a struct and then passed to the corresponding New method.

The configuration file might look like this:

Redis: addr: 127.0.0.1:6379 read_timeout: 0.2s write_timeout: 0.2s

The analytic structure looks like this:

type RedisConfig struct {
 Network      string             `json:"network,omitempty"`
 Addr         string             `json:"addr,omitempty"`
 ReadTimeout  *duration.Duration `json:"read_timeout,omitempty"`
 WriteTimeout *duration.Duration `json:"write_timeout,omitempty"`
}

As a result, your newApp method might look like this:

func NewApp() *App { var conf *RedisConfig yamlFile, err := ioutil.ReadFile("redis_conf.yaml") if err ! = nil { panic(err) } err = yaml.Unmarshal(yamlFile, &conf) if err ! = nil { panic(err) } return &App{ds: NewRedisClient(conf)} }

NewApp says, stop, you young people don’t talk about military ethics, my responsibility is to create an App instance, I just need a DataSource to register in, I don’t care how this DataSource comes from, why does such a lump of code to handle conf put in my place, I don’t care if you get the configuration file from a network request or read it from a local disk, I just want to put the App together and throw it out and get off work.

Dependency Injection is finally here to stay

Remember how we talked about dependency injection? Dependent modules are injected (passed as arguments) into the initialization function when the module is created. With this model, NewApp can leave work early. So we initialize NewRedis or NewMySQL outside, and we throw the DataSource directly to NewApp.

So that’s it

func NewApp(ds DataSource) *App {
    return &App{ds: ds}
}

The code that reads the configuration file to initialize the Redis is thrown into the method that initializes the DataSource

func NewRedis() DataSource { var conf *RedisConfig yamlFile, err := ioutil.ReadFile("redis_conf.yaml") if err ! = nil { panic(err) } err = yaml.Unmarshal(yamlFile, &conf) if err ! = nil { panic(err) } return &redis{r: NewRedisClient(conf)} }

Furthermore, the NewRedis method doesn’t even need to care about how the file is read. Its responsibility is only to initialize a DataSource from conf, so you can continue to extract the read config code and make NewRedis to receive a conf. Output a DataSource

func GetRedisConf() *RedisConfig
func NewRedis(conf *RedisConfig) DataSource

Since the entire assembly process was previously scattered under the main function, we pulled it out to make a separate initApp method. And finally your App initialization logic looks like this

func initApp() *App {
    c := GetRedisConf()
    r := NewRedis(c)
    app := NewApp(r)
    return app
}

func main() {
    app := initApp()
    app.Run()
}

You can then modify your underlying implementation (read the implementation of the configuration file, and which DataSource to look up the data with) any way you want by implementing the DataSource interface, changing the way you read the configuration file, and changing the way you create the DataSource, without having to change a whole bunch of code each time. This makes your code hierarchy much clearer and easier to maintain.

This is called dependency injection.

Problems with manual dependency injection

This piece of code above, the various instances of initialization, and then in accordance with the needs of each initialization method, the final structure of the app of this piece of code, is the process of injection dependency.

c := GetRedisConf()
r := NewRedis(c)
app := NewApp(r)

There’s only one DataSource right now, so the handwritten injection process is fine, but once you have too many things to maintain, So if you have a newApp like this newApp (r *Redis, es * es, us *UserSerivce, db *MySQL) *App and then you have a userService like this userService (pg *Postgres, Mm *Memcached), which creates multiple layers of dependencies that need to be injected, and is cumbersome to write by hand.

This is where a dependency injection tool like Wire comes in — all it does is generate code to help you inject dependencies, but you need to create (initialize) the actual dependency instances yourself.

How

The main problem with Wire is that you can’t learn it from the documentation. Anyway, when I first read the document, I had no idea what it was and what it was for. But from what we’ve just done, you should get a sense of why dependency injection is used, and what Wire does here — it generates code to help you inject dependencies, whereas the actual dependency instance needs to be created (initialized) yourself.

Now it’s a little bit clearer.

The first step is to implement a wire.go file with an Injector defined.

// +build wireinject

func initApp() (*App) {
 panic(wire.Build(GetRedisConf, NewRedis, SomeProviderSet, NewApp))
}

Then implement the providers separately.

After running the wire command, it will scan the entire project and generate a wire_gen.go file for you. If you don’t implement something well, it will report an error.

Did you get it?

To understand the

Hold on, don’t give up on the treatment, let’s explain how to do it with the magic Chinese programming.

Who compiles it?

The initApp method above, which the documentation calls Injector, because of the // +build WireObject comment in the first line of the file, the wire.go file will only be read by wire, and the Go compiler will not care about it when compiling the code. What you actually read is the generated wire\_gen.go file.

Providers are part of your code and will definitely be involved in the compilation process.

What the hell is Injector?

Injector is what you want in the end — the initializer for the final App object, which is the initApp method from the previous example.

Think of it as you go to the Golden Arches, walk through the door, see the ordering machine, crackle a bunch, and finally type out a list.

// +build Wireinject func a bag of junk food () a bag of junk food {panic(wire.build (a Big Mac meal, a double cod burger meal, a box of Chicken McNuggets, a junk food package)}

This is the monad that you click on, it doesn’t compile, the code that actually compiles is generated by Wire for you.

What the hell is Provider?

Providers are methods for creating dependencies, such as NewRedis and NewApp in the previous example.

As you can see, this is what the Golden Arches waiters and cooks do: the Golden Arches cooks need to provide the services to make these foods — implement these instance initialization methods.

Func a box of Chicken McNuggets () a box of Chicken McNuggets {} Func a bag of junk food to go (a Big Mac combo, a double burger cod combo, a box of Chicken McNuggets) {}

Wire also has a concept called ProviderSet, which is a set of providers that package your Big Mac meal because you’re usually too lazy to order: I’ll have a Coke, a bag of fries, and a Big Mac. If you want to just poke it, you can have a Big Mac. This package is called the ProviderSet, an agreed set of recipes, otherwise your list (Injector Build) will be super long and you’ll be in trouble and the waiter will be tired to watch.

Take one of the meals for example

Var a Big Mac = wire.NewSet(a glass of coke, a bag of chips, Func a coke () a coke {} func a bag of French fries () a bag of French fries {} func a Big Mac () a Big Mac {} func

What does the Wire tool do?

The important thing is to generate code to help you inject dependencies.

In the case of the Golden Arches, Wire is a waiter who orders your food and asks colleagues to prepare it for you and then packages it up to you. This intermediate coordination of the build process is called dependency injection.

The nice thing about this is that, for Golden Arches, if they suddenly change their Coke supplier, they can just replace it with a new Coke, and return it with a new Coke, and they don’t have to change anything for the customer. For customers, the order content can be changed. For example, I don’t want chicken McNuggets today, or I want to add something else. I just need to change my order (as long as Golden Arches can make it), and then generate it again through wire, without paying attention to how the waiter makes the order.

Now you should have a general idea of the uses and benefits of Wire.

conclusion

Let’s go back to the Golden Arch and recap the process of dependency injection with Wire.

1. Define the Injector

Create a wire.go file, define the instance initialization function you eventually want to use such as initApp (Injector), specify what it returns *App, Panic (wire.build (newRedis, someProviderSet, newApp)) in the method lists which instance initialization methods (i.e. Provider)/group initialization methods (ProviderSet) it depends on.

2. Define ProviderSet (if any)

The providerSet is a set of initialization functions designed to reduce the amount of code needed to organize the dependencies of various modules more clearly. You don’t have to, but you have to write a bunch of things in Injector. Var someProviderSet = wire.newSet (NewES,NewDB) defines which providers are included in the ProviderSet

3. Implement various providers

Providers are initialization methods that you need to implement yourself, such as newApp, NewRedis, newMySQL, getConfig, etc. Pay attention to their respective input and output

4. Generate code

Execute the wire command to generate code, the tool will scan your code, according to your Injector definition to organize the execution order of various providers, and automatically according to the type requirements of the providers to execute the order and arrange parameter passing. If any Provider requirements are not met, it will be reported at the terminal and will continue to repair and execute Wire until the wire_gen.go file is successfully generated. Then you can use the initApp as normal to write your subsequent code.

If you need to replace the implementation, modify the Injector accordingly, implement the required Provider, and regenerate it.

The code it generates is actually something like the one we had to write by hand

func initApp() *App {  // injector
    c := GetRedisConf() // provider
    r := NewRedis(c)  // provider
    app := NewApp(r) // provider
    return app
}

As our example is relatively simple, it can’t show advantages through wire generation, but if our software is complex and has many levels of dependency, it is undoubtedly more convenient and accurate to use wire to automatically generate injection logic.

5. Advanced usage

Wire also has more functions, such as cleanup, bind, etc. Please refer to the official documentation to use it.

Finally, in fact, a few more toss, will use, I hope this article can help you to a certain extent.

The resources

[1]

kratos v2: https://github.com/go-kratos/…

[2]

kratos-layout: https://github.com/go-kratos/…

[3]

google/wire: https://github.com/google/wire

[4]

wire: https://github.com/google/wire

[5]

Di: https://zh.wikipedia.org/wiki…

– END –

Go language advanced

Go language advanced, hand in hand with your ability to achieve advanced! Focus on microservice architecture, middleware, and GO engineering best practices.

7 pieces of original content

The public,