Preface#
During my participation in a project for Star Horizon, I was inspired by my teammates and discovered the amazing tool called Wire.
Wire is a Golang dependency injection solution developed and open-sourced by Google. It generates new files by interpreting the original files and uses Go Build Injector to implement code differentiation during the compilation phase.
According to the Go Blog, Wire was first used in Google's open-source Go Cloud project.
Application#
Scenario#
We typically nest Services within the Client when building an SDK, where the Client depends on the Services.
However, a common issue is that as the number of Services increases, the code becomes increasingly cluttered, and adding a new Service may require dozens of lines of code changes. Once new developers join the project, it's easy to overlook some necessary code changes, leading to the emergence of bugs.
Google naturally faces this issue as well, given its massive scale. To address this, they created Wire to solve the dependency injection problem.
Installation#
First, we need to introduce the github.com/google/wire
package in the project for identification, while also installing it globally:
# Add dependency in the project
go get -u github.com/google/wire
# Globally install the Wire command-line program
go install github.com/google/wire/cmd/wire@latest
Then, we can call the command-line program using wire
:
The process of dependency injection with Wire
Of course, we need to complete the following steps before calling it.
Differences#
Traditional Dependency Injection Method#
Typically, when configuring Client
in pkg, we would perform the following operations:
// service/foo.go
package service
import "fmt"
type Foo interface {
Foo()
}
type foo struct {
}
func (f *foo) Foo() {
fmt.Println("foo")
}
func NewFoo() Foo {
return &foo{}
}
// service/bar.go
package service
import "fmt"
type Bar interface {
Bar()
}
type bar struct {
}
func (b *bar) Bar() {
fmt.Println("bar")
}
func NewBar() Bar {
return &bar{}
}
// service/client.go
package service
type Client struct {
Foo Foo
Bar Bar
}
func NewClient(foo Foo, bar Bar) *Client {
return &Client{
Foo: foo,
Bar: bar,
}
}
// main.go
package main
import "wire_tutorial/service"
func main() {
foo := service.NewFoo()
bar := service.NewBar()
client := service.NewClient(foo, bar)
client.Foo.Foo()
client.Bar.Bar()
}
At this point, compiling and executing in the current directory will yield the following result:
In the current scenario, the
Client
depends onFoo
andBar
. This is a very smallClient
, so it may not fully reflect the issues of dependency injection.
Using Wire#
Based on the original code, we added service/wire.go
and modified main.go
:
// service/wire.go
package service
import "github.com/google/wire"
func BuildClient() *Client {
wire.Build(NewClient, NewFoo, NewBar)
return nil
}
// main.go
package main
import "wire_tutorial/service"
func main() {
client := service.BuildClient()
client.Foo.Foo()
client.Bar.Bar()
}
In this case, if you compile and run, you will find that since the properties Foo
and Bar
of Client
are both nil
, it will directly report an error: panic: runtime error: invalid memory address or nil pointer dereference
.
At this point, we execute wire ./...
in the terminal:
wire ./...
# or
go run github.com/google/wire/cmd/wire@latest ./...
You will find that Wire has created a new file: service/wire_gen.go
The content of this generated file is as follows:
// Code generated by Wire. DO NOT EDIT.
//go:generate go run github.com/google/wire/cmd/wire
//go:build !wireinject
// +build !wireinject
// Note that the two lines above are called Injector, which means that when executing go build, if the wireinject arg is included, the current file will not be compiled.
package service
// Injectors from wire.go:
func BuildClient() *Client {
serviceFoo := NewFoo()
serviceBar := NewBar()
client := NewClient(serviceFoo, serviceBar)
return client
}
At this point, if you compile again, you will find that it still cannot run normally because the BuildClient() *Client
function in service/wire.go
and service/wire_gen.go
is in conflict. At this point, we can follow Google's example to avoid compiling both files during project packaging by using an Injector:
// service/wire.go
//go:build wireinject
// +build wireinject
package service
import "github.com/google/wire"
func BuildClient() *Client {
wire.Build(NewClient, NewFoo, NewBar)
return nil
}
Now, only when compiling with the wireinject
parameter will the current file be included in the software.
Recompiling, we find that the program can output normally, and the configuration of Wire is complete.
Compilation directory
In the future, you will only need to regenerate service/wire_gen.go
when modifying service/wire.go
, so according to Google's documentation, you can bring service/wire_gen.go
into the version control system instead of regenerating it every time.
More#
Parameter Passing#
Wire has limitations on parameter passing.
For example, in the following code situation, it cannot generate the wire_gen.go
file:
// service/client.go
package service
import "fmt"
type Client struct {
Foo Foo
Bar Bar
}
var Debug bool
func NewClient(foo Foo, bar Bar) *Client {
if Debug {
fmt.Println("debug mode")
}
return &Client{
Foo: foo,
Bar: bar,
}
}
// service/wire.go
//go:build wireinject
// +build wireinject
package service
import "github.com/google/wire"
func BuildClient() *Client {
wire.Build(Debug, NewClient, NewFoo, NewBar)
return &Client{}
}
// service/main.go
package main
import (
"flag"
"wire_tutorial/service"
)
var (
debug bool
)
func init() {
flag.BoolVar(&debug, "debug", false, "debug mode")
flag.Parse()
}
func main() {
service.Debug = debug
client := service.BuildClient()
client.Foo.Foo()
client.Bar.Bar()
}
At this point, executing wire ./...
will produce the following error:
This is because Wire treats functions like NewClient as Providers, while the Debug variable is not a valid Provider.
We just need to do the following:
// service/client.go
package service
import "fmt"
type Client struct {
Foo Foo
Bar Bar
}
// Note that the global variable Debug has been moved into the function parameters
func NewClient(debug bool, foo Foo, bar Bar) *Client {
if debug {
fmt.Println("debug mode")
}
return &Client{
Foo: foo,
Bar: bar,
}
}
// service/wire.go
//go:build wireinject
// +build wireinject
package service
import "github.com/google/wire"
// Here we added a parameter named debug that matches the type name above
func BuildClient(debug bool) *Client {
wire.Build(NewClient, NewFoo, NewBar)
return &Client{}
}
// main.go
package main
import (
"flag"
"wire_tutorial/service"
)
var (
debug bool
)
func init() {
flag.BoolVar(&debug, "debug", false, "debug mode")
flag.Parse()
}
func main() {
client := service.BuildClient(debug)
client.Foo.Foo()
client.Bar.Bar()
}
At this point, re-executing wire ./...
will result in service/wire_gen.go
becoming as follows:
// Code generated by Wire. DO NOT EDIT.
//go:generate go run github.com/google/wire/cmd/wire
//go:build !wireinject
// +build !wireinject
package service
// Injectors from wire.go:
func BuildClient(debug bool) *Client {
serviceFoo := NewFoo()
serviceBar := NewBar()
client := NewClient(debug, serviceFoo, serviceBar)
return client
}
At this point, the functionality can be used normally.
Acknowledgments#
The documentation provided by Google is useful for understanding the basic principles of Wire.
- https://github.com/google/wire/blob/main/_tutorial/README.md
- https://github.com/google/wire/blob/main/docs/guide.md
- https://go.dev/blog/wire
- https://github.com/google/wire/blob/main/docs/best-practices.md
@topjohncian helped me understand Wire and its basic usage.