AHdark

AHdark Blog

Senior high school student with a deep passion for coding. Driven by a love for problem-solving, I’m diving into algorithms while honing my skills in TypeScript, Rust, and Golang.
telegram
tg_channel
twitter
github

Golang uses Wire for dependency injection

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.

google/wire

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:

image

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:

image

In the current scenario, the Client depends on Foo and Bar. This is a very small Client, 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

image

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.

image

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:

image

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.

@topjohncian helped me understand Wire and its basic usage.

Loading...
Ownership of this post data is guaranteed by blockchain and smart contracts to the creator alone.