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:
- 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
}
- 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
}
- 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
- providespb.EntityRPCServer
value.
and several new:
NewListener
is a function defined in the same file and providesnet.Listener
value.NewGRPCServer
- initializes gRPC server.DBConn
- initializes database connection.repo.Provider
andservice.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.