Wire and dependency injection

Wire is a Golang dependency injection tool. It completes dependency injection at compile time by automatically generating code. The most famous Spring framework in Java system adopts runtime injection, which is the biggest difference between Wire and other dependency injection.

Dependency Injection (DI) is an Inversion of Control.

The dependencies needed by the current object are provided externally (usually by the IoC container), and the external is responsible for operations such as the construction of the dependent object. The current object is only responsible for the invocation, not the construction of the dependent object. That is, control of dependent objects is given to the IoC container.

Here is an example of inversion of control, where we configure to create a database connection:

// Connection configuration
type DatabaseConfig struct {
    Dsn string 
}

func NewDB(config *DatabaseConfig)(*sql.DB, error) {
    db,err := sql.Open("mysql", config.Dsn)
    iferr ! =nil {
        return nil, err
    }
    // ...
}

fun NewConfig()(*DatabaseConfig,error) {
    // Read the configuration file
    fp, err := os.Open("config.json")
    iferr ! =nil {
        return nil,err
    }
    defer fp.Close()
    // Parse to Json
    var config DatabaseConfig
    iferr:=json.NewDecoder(fp).Decode(&config); err! =nil {
        return nil,err
    }
    return &config, nil
}

func InitDatabase(a) {
    cfg, err:=NewConfig()
    iferr! =nil {
        log.Fatal(err)
    }
    db,err:=NewDB(cfg)
    iferr! =nil {
        log.Fatail(err)
    }
    // The db object is constructed
}
Copy the code

The NewDB method doesn’t care where the database configuration comes from (the sample code uses JSON configuration objects provided by NewConfig). NewDB is only responsible for creating DB objects and returning them. There is no coupling with the configuration method, so the NewDB code doesn’t need to change even if a configuration center or other method provides the configuration. That’s the magic of inversion of control!

Let’s look at an example of the opposite, which is controlling forward rotation:

The dependency required by the current object is created by itself, that is, the control of the dependent object is in the hands of the current object.

type DatabaseConfig struct {
    Dsn string 
}

func NewDB(a)(*sql.DB, error) {
    // Read the configuration file
    fp, err := os.Open("config.json")
    iferr ! =nil {
        return nil,err
    }
    defer fp.Close()
    // Parse to Json
    var config DatabaseConfig
    iferr:=json.NewDecoder(fp).Decode(&config); err! =nil {
        return nil,err
    }
    // Initialize the database connection
    db,err = sql.Open("mysql", config.Dsn)
    iferr ! =nil {
        return
    }
    // ...
}
Copy the code

In control forward mode, the NewDB method needs to create the configuration object itself. In the example, it needs to read the Json configuration file, which is strongly coupled code. If the configuration file is not in Json format, the NewDB method will return an error.

Dependency injection is great, but manually managing dependencies like the example above can be complicated and painful, so I’ll focus on Golang’s dependency injection tool, Wire, in the next section.

Learn to use

Through the go get github.com/google/wire/cmd/wire installed wire command line tools.

Before we get started, we need to introduce two concepts of wire: Provider and Injector:

  • Provider: the method responsible for creating the object, such as aboveExample of Inversion of controltheNewDB(provides DB objects) andNewConfig(provide DatabaseConfig object) method.
  • Injector: is responsible for constructing dependent objects according to their dependencies, and finally constructing the target object method, such as aboveExample of Inversion of controltheInitDatabaseMethods.

Now let’s implement a simple project through Wire. The project structure is as follows:

|--cmd
	|--main.go
	|--wire.go
|--config
	|--app.json
|--internal
	|--config
		|--config.go
	|--db
		|--db.go
Copy the code

config/app.json

{
  "database": {
    "dsn": "root:root@tcp(localhost:3306)/test"}}Copy the code

internal/config/config.go

package config

import (
	"encoding/json"
	"github.com/google/wire"
	"os"
)

var Provider = wire.NewSet(New) // Declare the New method as Provider, which means that the New method can create a dependent object, namely a Config object

type Config struct {
	Database database `json:"database"`
}

type database struct {
	Dsn string `json:"dsn"`
}

func New(a) (*Config, error) {
	fp, err := os.Open("config/app.json")
	iferr ! =nil {
		return nil, err
	}
	defer fp.Close()
	var cfg Config
	iferr := json.NewDecoder(fp).Decode(&cfg); err ! =nil {
		return nil, err
	}
	return &cfg, nil
}

Copy the code

internal/db/db.go

package db

import (
	"database/sql"
	_ "github.com/go-sql-driver/mysql"
	"github.com/google/wire"
	"wire-example2/internal/config"
)

var Provider = wire.NewSet(New) / / in the same way

func New(cfg *config.Config) (db *sql.DB, err error) {
	db, err = sql.Open("mysql", cfg.Database.Dsn)
	iferr ! =nil {
		return
	}
	iferr = db.Ping(); err ! =nil {
		return
	}
	return db, nil
}
Copy the code

cmd/main.go

package main

import (
	"database/sql"
	"log"
)

type App struct { // The final object needed
	db *sql.DB
}

func NewApp(db *sql.DB) *App {
	return &App{db: db}
}

func main(a) {
	app, err := InitApp() // Get the APP object using the Wire-generated Injector method
	iferr ! =nil {
		log.Fatal(err)
	}
	var version string
	row := app.db.QueryRow("SELECT VERSION()")
	iferr := row.Scan(&version); err ! =nil {
		log.Fatal(err)
	}
	log.Println(version)
}
Copy the code

cmd/wire.go

The key file, which is the heart of the Injector implementation:

// +build wireinject

package main

import (
	"github.com/google/wire"
	"wire-example2/internal/config"
	"wire-example2/internal/db"
)

func InitApp(a) (*App, error) {
	panic(wire.Build(config.Provider, db.Provider, NewApp)) // Call the wire.Build method to pass in all the dependent objects and the function that builds the final object to get the target object
}
Copy the code

After the file is written, enter the CMD directory and execute the wire command to get the following output:

C:\Users\Administrator\GolandProjects\wire-example2\cmd>wire
wire: wire-example2/cmd: wrote C:\Users\Administrator\GolandProjects\wire-example2\cmd\wire_gen.go
Copy the code

The wire_gen.go file is successfully generated, and the file content is as follows:

// Code generated by Wire. DO NOT EDIT.

//go:generate go run github.com/google/wire/cmd/wire
//+build ! wireinject

package main

import (
	"wire-example2/internal/config"
	"wire-example2/internal/db"
)

// Injectors from wire.go:

func InitApp(a) (*App, error) {
	configConfig, err := config.New()
	iferr ! =nil {
		return nil, err
	}
	sqlDB, err := db.New(configConfig)
	iferr ! =nil {
		return nil, err
	}
	app := NewApp(sqlDB)
	return app, nil
}
Copy the code

You can see that the code to generate the App object has been generated automatically.

The Provider description

Use the NewSet method to declare the methods that create objects in this package as providers for other objects to use. NewSet can accept multiple parameters, such as Mysql and Redis connection objects.

var Provider = wire.NewSet(NewDB, NewRedis)

func NewDB(config *Config)(*sql.DB,error) { // Create a database object
    
}

func NewRedis(config *Config)(*redis.Client,error) { // Create a Redis object
}
Copy the code

Wire. go File description

The wire.go file needs to be placed where the target object is created. For example, our Config and DB objects ultimately serve the App, so the wire.go file needs to be placed in the package where the App is.

Wire. go file name is not fixed, but it is commonly called this file name.

The first line of wire.go // +build wireinject is required, meaning as follows:

This file will compile only if we add a build tag named “wireinject”, which we usually don’t when we go build main.go. Therefore, this file does not participate in the final compilation.

Wire. Build(config.Provider, db.Provider, NewApp) creates the final App object by passing in config and db objects

Wire_gen. go File description

This file is automatically generated by wire without manual editing!!

//+build ! The wireinject tag corresponds to the tag in the wire.go file and has the following meanings:

This file is compiled only if the “wireinject” build tag is not added.

Therefore, only one wire.go and wire_gen.go will be compiled at any time.

Senior play

The cleanup function

When creating dependent resources, you can use the cleanup function to shut down resources if the creation of one resource fails and other resources need to be shut down. For example, we return a cleanup function to the db.New method to close the database connection:

internal/db/db.go

func New(cfg *config.Config) (db *sql.DB, cleanup func(a).err error) { // Declare the second return value
	db, err = sql.Open("mysql", cfg.Database.Dsn)
	iferr ! =nil {
		return
	}
	iferr = db.Ping(); err ! =nil {
		return
	}
	cleanup = func(a) { Close the database connection in the cleanup function
		db.Close()
	}
	return db, cleanup, nil
}
Copy the code

cmd/wire.go

func InitApp(a) (*App, func(a).error) { // Declare the second return value
	panic(wire.Build(config.Provider, db.Provider, NewApp))
}
Copy the code

cmd/main.go

func main(a) {
	app, cleanup, err := InitApp() // Add the second argument
	iferr ! =nil {
		log.Fatal(err)
	}
	defer cleanup() // Delay the call to cleanup to close the resource
	var version string
	row := app.db.QueryRow("SELECT VERSION()")
	iferr := row.Scan(&version); err ! =nil {
		log.Fatal(err)
	}
	log.Println(version)
}
Copy the code

Run the wire command again in the CMD directory. The wire_gen.go command is generated:

func InitApp(a) (*App, func(a).error) {
	configConfig, err := config.New()
	iferr ! =nil {
		return nil.nil, err
	}
	sqlDB, cleanup, err := db.New(configConfig)
	iferr ! =nil {
		return nil.nil, err
	}
	app := NewApp(sqlDB)
	return app, func(a) { // Returns the cleanup function
		cleanup()
	}, nil
}
Copy the code

The interface binding

In interface-oriented programming, the code often depends on the interface rather than the specific struct. In this case, the dependency injection related code needs to be modified a little.

The new internal/db/dao. Go

package db

import "database/sql"

type Dao interface { // Interface declaration
	Version() (string, error)
}

type dao struct { // Default implementation
	db *sql.DB
}

func (d dao) Version(a) (string, error) {
	var version string
	row := d.db.QueryRow("SELECT VERSION()")
	iferr := row.Scan(&version); err ! =nil {
		return "", err
	}
	return version, nil
}

func NewDao(db *sql.DB) *dao { // Method to generate dao objects
	return &dao{db: db}
}
Copy the code

Internal /db/db.go also need to modify Provider, add NewDao declaration:

var Provider = wire.NewSet(New, NewDao)
Copy the code

CMD /main.go file:

package main

import (
	"log"
	"wire-example2/internal/db"
)

type App struct {
	dao db.Dao // Rely on the Dao interface
}

func NewApp(dao db.Dao) *App { // Rely on the Dao interface
	return &App{dao: dao}
}

func main(a) {
	app, cleanup, err := InitApp()
	iferr ! =nil {
		log.Fatal(err)
	}
	defer cleanup()
	version, err := app.dao.Version() // Call the Dao interface method
	iferr ! =nil {
		log.Fatal(err)
	}
	log.Println(version)
}
Copy the code

Enter the CMD directory and run the wire command.

C:\Users\Administrator\GolandProjects\wire-example2\cmd>wire
wire: C:\Users\Administrator\GolandProjects\wire-example2\cmd\wire.go:11:1: inject InitApp: no provider found for wire-example2/internal/db.Dao
        needed by *wire-example2/cmd.App in provider "NewApp" (C:\Users\Administrator\GolandProjects\wire-example2\cmd\main.go:12:6)
wire: wire-example2/cmd: generate failed
wire: at least one generate failure
Copy the code

Wire prompts Inject InitApp: No provider found for wire – example2 / internal/db. The Dao, which is yet to find a can provide the Dao object provider, we are not provided by default the Dao implementation also registered provider? This is where the OOP design of Go is peculiar.

Internal /db/db.go Provider: add db.* DAO to db.

var Provider = wire.NewSet(New, NewDao, wire.Bind(new(Dao), new(*dao)))
Copy the code

The first argument to the wire.bind () method is interface{} and the second argument is the implementation.

At this point, execute the wire command and you will be successful!

At the end

There are many other ways to play the Wire tool, but from my personal work experience, I have mastered the knowledge introduced in this article to be competent for most scenarios!