The Wire dependency injection framework was briefly introduced in an earlier series of articles on unit testing. At the time wire was still in alpha, but recently wire released its first beta with some API changes and a promise not to break API compatibility unless absolutely necessary. In this article, I will not repeat some of the basic overview of wire: Go unit testing (4) – Dependency injection framework (WIRE). This article details the use of Wire and some best practices.

Complete examples of the code in this article can be found here: wire-examples

Installing

go get github.com/google/wire/cmd/wire
Copy the code

Quick Start

Let us first through a simple example, let friends have an intuitive understanding of wire. The following example shows a simple Wire dependency injection example:

$ ls
main.go  wire.go 
Copy the code

main.go

package main

import "fmt"

type Message struct {
	msg string
}
type Greeter struct {
	Message Message
}
typeStruct {Greeter Greeter} // NewMessage(MSG string) Message {returnMessage{MSG: MSG,}} // NewGreeter Greeter constructor func NewGreeter(m Message) Greeter {returnGreeter{Message: m}} // NewEvent Event constructorreturn Event{Greeter: g}
}
func (e Event) Start() {
	msg := e.Greeter.Greet()
	fmt.Println(msg)
}
func (g Greeter) Greet() Message {
	return// use wire before funcmain() {
	message := NewMessage("hello world") greeter := NewGreeter(message) event := NewEvent(greeter) event.start ()main() {
	event := InitializeEvent("hello_world")

	event.Start()
}*/

Copy the code

wire.go

// +build wireinject
// The build tag makes sure the stub is not built in the final build.

package main

import "github.com/google/wire"Func InitializeEvent(MSG string) Event{wire.Build(NewEvent, NewGreeter, NewMessage)returnEvent{} // return value has no real meaning, as long as it conforms to the function signature}Copy the code

Call the wire command to generate the dependency file:

$ wire
wire: github.com/DrmagicE/wire-examples/quickstart: wrote XXXX\github.com\DrmagicE\wire-examples\quickstart\wire_gen.go
$ ls
main.go  wire.go  wire_gen.go
Copy the code

Wire_gen. Go Wire generated file

// Code generated by Wire. DO NOT EDIT. //go:generate wire //+build ! wireinject package main // Injectors from wire.go: func InitializeEvent(msg string) Event { message := NewMessage(msg) greeter := NewGreeter(message) event := NewEvent(greeter)return event
}
Copy the code

Before use v. after use

. /* / use wire before funcmain() {
	message := NewMessage("hello world"Greeter := NewGreeter(message) event := NewEvent(greeter) event.start ()}*/ / use wire after funcmain() {
	event := InitializeEvent("hello_world")

	event.Start()
}
...
Copy the code

After using Wire, only one initialization method can be used to obtain the Event. Compared with before using wire, not only three lines of code are reduced, but also there is no need to care about the initialization sequence between dependencies.

Example portal: Quickstart

Provider & Injector

Provider and Injector are two core concepts of Wire.

provider: a function that can produce a value. These functions are ordinary Go code. injector: a function that calls providers in dependency order. With Wire, you write the injector’s signature, Then Wire generates the function’s body. github.com/google/wire…

Let the Wire know how to generate these dependent objects by providing the Provider function. The Wire generates a complete Injector function based on the injector function signature we defined. The Injector function is the final function we need, and it calls the provider in dependency order.

In the case of a quickstart, NewMessage NewGreeter, NewEvent is the provider, wire_gen. InitializeEvent function is injector in go, As you can see, the Injector generated the desired object Event by calling the Provider in dependency order.

The example above defines the injector function signature in wire.go, note that this is added in the first line of the file

// +build wireinject
...
Copy the code

Used to tell the compiler that the file does not need to be compiled. The Injector’s signature definition function specifies the provider used to generate the dependency by calling the wire.Build method:

Func InitializeEvent(MSG string) Event{wire.Build(NewEvent, NewGreeter, NewMessage) // <-- pass the provider functionreturnEvent{} // return value has no real meaning, as long as it conforms to the function signature}Copy the code

The return value of this method has no practical meaning and only needs to meet the requirements of the function signature.

Advanced features

The QuickStart example demonstrates the basic functionality of Wire, and this section introduces some advanced features.

The interface binding

According to the Dependence Inversion Principle, objects should rely on interfaces rather than directly on implementations.

Abstracting interface dependencies is better for unit testing! Mock Framework (Gomock) mock Framework (Gomock)

The dependencies in the QuickStart example are implementation-specific. Now let’s look at how interface dependencies are handled in Wire:

// UserService 
typeUserService struct {userRepo UserRepository // <-- UserService relies on UserRepository interface} // UserRepository A data warehouse interface for storing User objects, such as mysql,restful API....typeUserbyid (ID int) (*User, Error)} // NewUserService *UserService constructor func NewUserService(userRepo UserRepository) *UserService {return&userService {userRepo:userRepo,}} // mockUserRepo emulates a UserRepository implementationtypeMockUserRepo struct {foo string bar int} // GetUserByID UserRepository interface implementation func (u *mockUserRepo) GetUserByID(id int) (*User,error){return&user {}, nil} // NewMockUserRepo *mockUserRepo constructor func NewMockUserRepo(foo string,bar int) *mockUserRepo {return&mockUserRepo{ foo:foo, bar:bar, } // MockUserRepoSet bind *mockUserRepo to UserRepository var MockUserRepoSet = wire.NewSet(NewMockUserRepo,wire.Bind(new(UserRepository), new(*mockUserRepo)))Copy the code

In this case, UserService relies on the UserRepository interface, where mockUserRepo is an implementation of UserRepository, since in Go best practice it is recommended to return the implementation rather than the interface. So mockUserRepo’s Provider function returns the concrete type *mockUserRepo. Wire cannot automatically associate a concrete implementation with an interface; we need to display and declare the relationship between them. Bind *mockUserRepo to UserRepository via wire.NewSet and wire.Bind:

// MockUserRepoSet bind *mockUserRepo to UserRepository var MockUserRepoSet = wire.NewSet(NewMockUserRepo,wire.Bind(new(UserRepository), new(*mockUserRepo)))Copy the code

Defining the Injector function signature:

. func InitializeUserService(foo string, Bar int) * UserService {wire. Build (NewUserService MockUserRepoSet) using MockUserRepoSet / /return nil
}
...
Copy the code

Example portal: Binding-interfaces

Returns an error

In the previous examples, our Provider functions returned only one value, but in some cases, the provider function might check the input parameter and return error if the parameter was wrong. Wire also takes this into account, and the provider function can set the second argument of the return value to error:

/ / Config configurationtypeConfig struct {RemoteAddr string} // APIClient API clienttypeAPIClient struct {c Config} // NewAPIClient struct {c Config} Func NewAPIClient(c Config) (*APIClient,error) {// <-- set the second argument to errorif c.RemoteAddr == "" {
		return nil, errors.New("Remote address not set")}return &APIClient{
		c:c,
	},nil
}
// Service
typeStruct {client *APIClient} // NewService struct {client *APIClient} // NewService NewService(client *APIClient) *Service{return &Service{
		client:client,
	}
}
Copy the code

Similarly, the Injector function has been defined with the second return value set to error:

. Func InitializeClient(config config) (*Service, error) {// <-- error wire.Build(NewService,NewAPIClient)return nil,nil
}
...
Copy the code

Take a look at the Injector generated by wire:

func InitializeClient(config Config) (*Service, error) {
	apiClient, err := NewAPIClient(config)
	iferr ! = nil {// <-- if an error occurs in the order in which dependencies are constructed, the corresponding value is returned"Zero"And corresponding errorsreturn nil, err
	}
	service := NewService(apiClient)
	return service, nil
}
Copy the code

If an error occurs in the order in which the dependencies are constructed, the corresponding “zero value” and corresponding error are returned.

Example portal: return-error

Cleanup functions

When the provider generates an object that needs cleanup handling, such as closing a file or a database connection, you can still set the provider’s return value to do this:

// FileReader
typeFileReader struct {f *os.File} // NewFileReader *FileReader constructor, the second argument is cleanupfunction
func NewFileReader(filePath string) (*FileReader, func(), error){
	f, err := os.Open(filePath)
	iferr ! = nil {return nil,nil,err
	}
	fr := &FileReader{
	    f:f,
	}
	fn := func() {
	    log.Println("cleanup") 
	    fr.f.Close()
	}
	return fr,fn,nil
}
Copy the code

Just like returning an error, set the provider’s second return argument to func() to return cleanup function. In the example above, error is returned in the third argument, but this is optional:

Wire specifies the number and order of values returned by the provider:

  1. The first parameter is the dependent object that needs to be generated
  2. If two return values are returned, the second argument must be func() or error
  3. If three return values are returned, the second argument must be func() and the third argument must be error

Example portal: cleanup-functions

Provider set

When some providers are usually used together, you can use a Provider set to organize them, using the QuickStart example as a template to modify them slightly:

// NewMessage constructor func NewMessage(MSG string) Message {returnMessage{MSG: MSG,}} // NewGreeter Greeter constructor func NewGreeter(m Message) Greeter {returnGreeter{Message: m}} // NewEvent Event constructorreturn Event{Greeter: g}
}
func (e Event) Start() {MSG := LLDB etter.greet () FMT.Println(MSG)} Var EventSet = wire.NewSet(NewEvent, NewMessage, NewGreeter) // <--Copy the code

The above example combines the Event and its dependencies via wire.NewSet and is used as a whole in the Injector function signature definition:

func InitializeEvent(msg string) Event{
	//wire.Build(NewEvent, NewGreeter, NewMessage)
	wire.Build(EventSet) 
	return Event{}
}
Copy the code

Just pass the EventSet to wire.build.

Example portal: provider-set

Structure the provider

In addition to functions, constructs can also act as providers, similar to setter injection:

type Foo int
type Bar int

func ProvideFoo() Foo {
	return 1
}
func ProvideBar() Bar {
	return2}type FooBar struct {
	MyFoo Foo
	MyBar Bar
}
var Set = wire.NewSet(
	ProvideFoo,
	ProvideBar,
	wire.Struct(new(FooBar), "MyFoo"."MyBar"))
Copy the code

Wire. Struct specifies which fields are to be injected into the structure, or if all fields are:

var Set = wire.NewSet(
	ProvideFoo,
	ProvideBar,
	wire.Struct(new(FooBar), "*") // * Indicates that all fields are injectedCopy the code

The generated Injector function:

func InitializeFooBar() FooBar {
	foo := ProvideFoo()
	bar := ProvideBar()
	fooBar := FooBar{
		MyFoo: foo,
		MyBar: bar,
	}
	return fooBar
}
Copy the code

Example portal: struct-provider

Best Practices

To distinguish the type of

Since the Injector function does not allow duplicate parameter types, otherwise the Wire will not be able to distinguish these same parameter types. For example:

type FooBar struct {
	foo string
	bar string
}

func NewFooBar(foo string, bar string) FooBar {
	return FooBar{
	    foo: foo,  
	    bar: bar,
	}
}
Copy the code

Injector function signature definition:

Func InitializeFooBar(a string, b string) FooBar {wire.build (NewFooBar)return FooBar{}
}

Copy the code

If the injector was generated using the provider above,wire will report the following error:

provider has multiple parameters of type string
Copy the code

Because the input parameters are strings, wire cannot know the mapping between input parameters A and b and foobar. foo and foobar. bar. So we use different types to avoid conflicts:

type Foo string
type Bar string
type FooBar struct {
	foo Foo
	bar Bar
}

func NewFooBar(foo Foo, bar Bar) FooBar {
	return FooBar{
	    foo: foo,
	    bar: bar,
	}
}
Copy the code

Injector function signature definition:

func InitializeFooBar(a Foo, b Bar) FooBar {
	wire.Build(NewFooBar)
	return FooBar{}
}
Copy the code

The basic type and the generic interface type are the most likely to conflict. If they appear in provider functions, it is best to create a new alias to replace them (though no conflict has occurred yet). For example:

type MySQLConnectionString string
type FileReader io.Reader
Copy the code

Example portal distinguishing types

Options Structs

If a provider method contains many dependencies, you can avoid having too many arguments in the constructor by putting those dependencies in an Options structure:

type Message string

// Options
type Options struct {
	Messages []Message
	Writer   io.Writer
	Reader   io.Reader
}
typeGreeter struct {} // NewGreeter Greeter's provider method uses Options to avoid the constructor being too long. opts *Options) (*Greeter, error) {returnProvider var GreeterSet = wire.NewSet(wire.Struct(new(Options)),"*"), NewGreeter)
Copy the code

Injector function signature:

func InitializeGreeter(ctx context.Context, msg []Message, w io.Writer, r io.Reader) (*Greeter, error) {
	wire.Build(GreeterSet)
	return nil, nil
}
Copy the code

Example portal options-structs

Some disadvantages and limitations

Additional type definitions

Due to wire’s own limitations, the variable types in injector could not be duplicated, and many additional base type aliases needed to be defined.

Mock support is not friendly enough for now

Currently, the wire command does not recognize the provider function in the _test.go ending file. This means that if we need to use wire to inject our mock object in our test, we need to embed the mock object’s provider in our regular code, which is intrusive. But officials seem to have taken note of the issue, and those interested can follow the issue: github.com/google/wire…

More reference

Md Official Readme. md Official guide. Md Official Best-practices. Md