As mentioned in the first article, in order to make our code testable, we need to build our objects in a dependency injection way, and we usually do dependency injection in Main. go, which causes main.go to become bloated. Main. go sacrifices its supposedly slim frame to make unit testing work. This article introduces dependency Injection framework (WIRE), which aims to help Main. go get back in shape.

Bloated main

Doing dependency injection in main.go means that in the initialization code we manage:

  1. The initialization order of dependencies
  2. Relationships between dependencies

For small projects, the number of dependencies is small, the initialization code is not large, and there is no need to introduce a dependency injection framework. But for medium to large projects with a lot of dependencies, the initialization code is smelly and long, and the readability and maintainability becomes very poor. Feel free to:

func main() {config := NewConfig() // config dependency db, err := ConnectDatabase(config)iferr ! Nil {panic(err)} // PersonRepository dependency db PersonRepository := NewPersonRepository(db) // PersonService dependency configuration and PersonRepository personService := NewPersonService(config, PersonService server := NewServer(config, PersonService) server.run ()}Copy the code

Practice has shown that modifying initialization code with a large number of dependencies can be tedious and time-consuming. At this point, we need a dependency injection framework to help simplify initialization code.

The code from above: blog.drewolson.org/dependency-…

Use the dependency injection framework — WIRE

What is wire?

Wire is Google’s open source dependency injection framework. Or to quote the official phrase: “Wire is a code generation tool that automates connecting components using dependency injection”.

github.com/google/wire

Why wire?

In addition to Wire, Go’s dependency injection framework also includes Uber’s Dig and Facebook’s Inject, both of which use reflection to implement Runtime dependency injection. While WIRE uses code generation to achieve compile-time dependency injection. The performance penalty of using reflection is secondary, and more importantly it makes the code difficult to trace and debug (reflection disables Ctrl+ left keys…). . The wire generated code is in line with the programmer’s general usage of the code, is very easy to understand and debug. The advantages of Wire can be explained in more detail on the official blog: blog.golang.org/wire

How does it work?

Refer to the official blog: blog.golang.org/wire

Wire has two basic concepts: Provider and Injector.

provider

Provider is a common Go function. It can be considered as a constructor of an object. We use provider to tell wire about the dependency of the object:

// NewUserStore is the provider of *UserStore, Func NewUserStore(CFG *Config, DB * mysql.db) (*UserStore, error) {... } // NewDefaultConfig is a provider of *Config, without dependencies func NewDefaultConfig() *Config {... Func NewDB(info ConnectionInfo) (* mysql.db, error) {... } // UserStoreSet optional, you can use wire.NewSet to combine dependencies that would normally be used together. var UserStoreSet = wire.NewSet(NewUserStore, NewDefaultConfig)Copy the code

injector

Injector is a function generated by wire. We used injector to get the object or value we needed by calling injector. Injector calls the provider function in order of dependency:

// File: wire_gen.go // Code generated by Wire. DO NOT EDIT. //go:generate wire //+build ! Wireinject // initUserStore is an injector generated by wire func initUserStore(info ConnectionInfo) (*UserStore, Error) {// * provider of Config defaultConfig := NewDefaultConfig() // * provider of mysql.db DB, err := NewDB(info)iferr ! = nil {return// *UserStore provider function UserStore, err := NewUserStore(defaultConfig, db)iferr ! = nil {return nil, err
    }
    return userStore, nil
}
Copy the code

The Injector has done this for us by initializing dependencies sequentially, so we just need to call the initUserStore method in main.go to get the object we want.

So how did wire know how to generate injector? We need to write a function that tells it:

  • Defines the injector’s function signature
  • Used in functionswire.BuildThe Injector method lists the providers needed to generate the Injector

Such as:

// initUserStore used for injector func initUserStore(info ConnectionInfo) (*UserStore, Error) {// Wire. Build specifies which provider function wire.Build(UserStoreSet, NewDB) to call to get a UserStore.returnNil, nil // These return values wire doesn't care. }Copy the code

With the above function, wire has learned how to generate an Injector. The steps for generating the Injector from wire are described as follows:

  1. Determine the function signature of the generated Injector function:func initUserStore(info ConnectionInfo) (*UserStore, error)
  2. The first argument to the perceived return value is*UserStore
  3. checkwire.BuildList, find*UserStoreThe provider:NewUserStore
  4. Signed by a functionfunc NewUserStore(cfg *Config, db *mysql.DB)Learned thatNewUserStoreDepends on the*Config, and*mysql.DB
  5. checkwire.BuildList, find*Configand*mysql.DBThe provider:NewDefaultConfigandNewDB
  6. Signed by a functionfunc NewDefaultConfig() *ConfigLearned that*ConfigThere’s nothing else to rely on.
  7. Signed by a functionfunc NewDB(info *ConnectionInfo) (*mysql.DB, error)Learned that*mysql.DBDepends on theConnectionInfo.
  8. checkwire.BuildList, not foundConnectionInfoProvider, but the injector function signature has found a matching input parameter typeNewDBInto the refs.
  9. The second argument to the perceived return value iserror
  10. .
  11. The Injector function has been assembled by calling the provider function in sequence according to the dependency relationship.

Take a chestnut

Chestnut portal: Wire-examples

Pay attention to

Until the release of this article, the official state of wire project is alpha, is not suitable for production environment, API may change. Although alpha, its main function is to generate dependency injection code for us. The generated code is very easy to understand, even if the API changes, under the premise of good version control, will not have a bad impact on the generation environment. I think it’s safe to use.

To summarize

This is the last article in this series. Based on the basic idea of unit testing, we introduce table-driven testing methods, Gomock, Potency, wire, all of them can be used to write a good unit test. I hope you found this series useful.