dev.ms

Golang, gRPC & Protobuf stories

Dependency Injection: Google Wire

Posted at — Jun 28, 2020

In software engineering, dependency injection is a technique in which an object receives other objects that it depends on

Source: Wikipedia

In the most of my applications Dependency Injection(DI) looks like this:

let’s say we have a gRPC server with one method called GetEntity which will be listening for incoming connections on 0.0.0.0:5000 and it will be returning entities from database.

Application consists of three parts:

  1. Database repository which talks directly to database and requires database connection.
package repo

import "database/sql"

type Entity struct {
	ID   int
	Name string
}

type Database interface {
	GetEntity(id int) (*Entity, error)
}

type DB struct {
	db *sql.DB
}

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

func (DB) GetEntity(id int) (*Entity, error) {
	return &Entity{}, nil
}
  1. Piece of business logic which requires database repo.
package service

import "wire/repo"

type Xlogic interface {
	GetEntity(id int) (*repo.Entity, error)
}

type Service struct {
	repo repo.Database
}

func New(r repo.Database) *Service {
	return &Service{repo: r}
}

func (s *Service) GetEntity(id int) (*repo.Entity, error) {
	entity, err := s.repo.GetEntity(id)
	if err != nil {
		return nil, err
	}

	// do something with entity...

	return entity, nil
}
  1. Transport package, higher level service or anything else requires business logic:
package rpci

import (
	"context"
	"wire/pb"
	"wire/service"

	"google.golang.org/grpc"
)

type Server struct {
	xlogic service.Xlogic
}

func New(x service.Xlogic, gsrv *grpc.Server) *Server {
	s := &Server{xlogic: x}
	pb.RegisterEntityRPCServer(gsrv, s)

	return s
}

func (s *Server) EntityByID(ctx context.Context, req *pb.EntityByIDRequest) (*pb.EntityByIDResponse, error) {
	xe, err := s.xlogic.GetEntity(int(req.Id))
	if err != nil {
		return nil, err
	}

	return &pb.EntityByIDResponse{Entity: &pb.Entity{
		Id:   int64(xe.ID),
		Name: xe.Name,
	}}, nil
}

and so on and so forth. We have a long call chain. The bigger and more complex application the longer call chain as well as initialization chain. Before we can use this application we have to initialize it, starting with database, then service and last one is transport level.

package main

import (
	"database/sql"
	"net"
	"wire/repo"
	"wire/rpci"
	"wire/service"

	"google.golang.org/grpc"
)


func main() {
  conn, err := sql.Open("mysql", "127.0.0.1:3306")
  if err != nil {
    panic(err)
  }
  db := repo.New(conn)
  service := service.New(db)
  grpcImpl := rpc.New(service)

  lis, err := net.Listen("tcp4", "0.0.0.0:5000")
  if err != nil {
    panic(err)
  }

  gsrv := grpc.NewServer()
  gsrv.Serve(lis)
}

it looks familiar, right? Initialize repo, inject it into service. Initialize service, inject it into transport (grpc). Initialize transport and listener, run gRPC server.

That’s how DI looks like.

For relatively small applications it works pretty well, but for big ones it can became a problem. For instance, in one of my current projects I have main function with 11 calls for functions with prefix New.... Some of them returns just one object, some returns an object and error which have to be tested for nil. In the same time initialization chain itself is pretty straightforward, i.e. we can automate its creation.

In a “perfect world” main function should be as short as possible, like this:

package main

func main() {
  app, err := initApp()
  must(err)

  must(app.Start())
}

func must(err error) {
  if err != nil {
    panic(err)
  }
}

and that is what we will try to achieve.

Google Wire: Automated Initialization in Go

So, we have two big parts in the code above:

Providers

Providers are functions which “provide” some value. In our example those functions are:

Function Provided value type
sql.Open *sql.DB
net.Listen net.Listener
repo.New repo.Database
service.New service.Xlogic
rpci.New pb.EntityRPCServer

and

Injectors

Injectors call providers in dependency order. It’s our main function at the moment and it will be initApp function as a result.

We have providers and we need to build an injector. That’s where Google Wire comes to play. Wire generates injector based on set of providers and dependency data type, i.e. having those providers we can generate our initApp function and Wire decide itself what the dependency order and where which dependency to pass.

How it works?

App structure

First of all, we have to define an App structure which will have a method called Start for starting our application and which initApp function will initialize.

package main

import (
	"net"
	"wire/rpci"

	"google.golang.org/grpc"
)

// App contains minimal list of dependencies to be able to start an application.
type App struct {
	// listener is a TCP listener which is used by gRPC server.
	listener net.Listener
	// gRPC serer itself.
	gsrv *grpc.Server
	// gRPC server implementation. It's not used here directly, but it must be
	// initialized for registering. gRPC server.
	rpcImpl *rpci.Server
}

// Start start gRPC server.
func (a App) Start() error {
	return a.gsrv.Serve(a.listener)
}

initApp function with providers set.

On second step we need to define an initApp function with all providers which are used in initialization chain.

// wire.go

// +build wireinject

package main

import (
	"database/sql"
	"net"
	"wire/repo"
	"wire/rpci"
	"wire/service"

	"github.com/google/wire"
	"google.golang.org/grpc"
)

func NewListener() (net.Listener, error) {
	return net.Listen("tcp4", "0.0.0.0:5000")
}

func NewGRPCServer() *grpc.Server {
	return grpc.NewServer()
}

func DBConn() (*sql.DB, error) {
	return sql.Open("mysql", "127.0.0.1:3306")
}

func initApp() (*App, error) {
	wire.Build(
		rpci.New,
		NewListener,
		NewGRPCServer,
		repo.Provider,
		DBConn,
		service.Provider,
		wire.Struct(new(App), "*"),
	)

	return &App{}, nil
}

wire.go is a source file used by Wire tool only. This file will be excluded from regular build, because it has build tag wireinject.

Function initApp contains on call for wire.Build with all necessary providers, one already mentioned: * rpci.New - provides pb.EntityRPCServer value.

and several new: * NewListener is a function defined in the same file and provides net.Listener value. * NewGRPCServer - initializes gRPC server. * DBConn - initializes database connection. * repo.Provider and service.Provider are variables defined in their packages like this:

package repo

import "github.com/google/wire"

var Provider = wire.NewSet(
	// here we binds concrete type *DB satisfies a dependency of type Database.
	New, wire.Bind(new(Database), new(*DB)),
)
package service

import "github.com/google/wire"

var Provider = wire.NewSet(
	// here we binds concrete type *Service satisfies a dependency of type Xlogic.
	New, wire.Bind(new(Xlogic), new(*Service)),
)

Last element of wire.Build list is wire.Struct(new(App), "*"), says to initialize all fields "*" of structure App. That’s pretty all we need.

Run Wire.

Third and final step is run wire tool which generate file wire_gen.go with next content:

// wire_gen.go

// Code generated by Wire. DO NOT EDIT.

//go:generate wire
//+build !wireinject

package main

import (
	"database/sql"
	"google.golang.org/grpc"
	"net"
	"wire/repo"
	"wire/rpci"
	"wire/service"
)

// Injectors from wire.go:

func initApp() (*App, error) {
	listener, err := NewListener()
	if err != nil {
		return nil, err
	}
	server := NewGRPCServer()
	db, err := DBConn()
	if err != nil {
		return nil, err
	}
	repoDB := repo.New(db)
	serviceService := service.New(repoDB)
	rpciServer := rpci.New(serviceService, server)
	app := &App{
		listener: listener,
		gsrv:     server,
		rpcImpl:  rpciServer,
	}
	return app, nil
}

// wire.go:

func NewListener() (net.Listener, error) {
	return net.Listen("tcp4", "0.0.0.0:5000")
}

func NewGRPCServer() *grpc.Server {
	return grpc.NewServer()
}

func DBConn() (*sql.DB, error) {
	return sql.Open("mysql", "127.0.0.1:3306")
}

All done! Now we can use initApp function to initialize our application as mentioned above.

Conclusion

Wire really simplifies application initialization. All you need is to build a list for providers each of which provide one concrete type, run the tool and use auto-generated functions.