Dependency injection is one form of the broader technique of inversion of control. It is used to increase modularity of the program and make it extensible.
You can use standard pkg.go.dev and inline code comments or if you do not have experience with auto-wiring libraries as google/wire, uber-go/dig or another start with tutorial.
go get github.com/goava/di
Let's learn to use di by example. We will code a simple application that processes HTTP requests.
The full tutorial code is available here
To start, we will need to provide way to build for two fundamental types: http.Server
and http.ServeMux
. Let's create a simple functional constructors that build them:
// NewServer builds a http server with provided mux as handler.
func NewServer(mux *http.ServeMux) *http.Server {
return &http.Server{
Handler: mux,
}
}
// NewServeMux creates a new http serve mux.
func NewServeMux() *http.ServeMux {
return &http.ServeMux{}
}
Supported constructor signature:
// cleanup and error is a optional func([dep1, dep2, depN]) (result, [cleanup, error])
Now, we can teach the container to build these types in three style ways:
In preferred functional option style:
// create container
container, err := container.New(
di.Provide(NewServer),
di.Provide(NewServeMux),
)
if err != nil {
// handle error
}
Next, we can resolve the built server from the container. For this, define the
variable of resolved type and pass variable pointer to Resolve
function.
If resolved type not found or the process of building instance cause error.
If no error occurred, we can use the variable as if we had built it yourself.
// declare type variable
var server *http.Server
// resolving
err := container.Resolve(&server)
if err != nil {
// handle error
}
server.ListenAndServe()
Note that by default, the container creates instances as a singleton. But you can change this behaviour. See Prototypes.
As an alternative to resolving we can use Invoke()
function of Container
. It builds
dependencies and call provided function. Invoke function may return optional error.
// StartServer starts the server.
func StartServer(server *http.Server) error {
return server.ListenAndServe()
}
if err := container.Invoke(StartServer); err != nil {
// handle error
}
Also you can use di.Invoke()
container options for call some initialization code.
container, err := di.New(
di.Provide(NewServer),
di.Invoke(StartServer),
)
if err != nil {
// handle error
}
Container run all invoke functions on compile stage. If one of them failed (return error), compile cause error.
Result dependencies will be lazy-loaded. If no one requires a type from the container it will not be constructed.
Inject make possible to provide implementation as an interface.
// NewServer creates a http server with provided mux as handler.
func NewServer(handler http.Handler) *http.Server {
return &http.Server{
Handler: handler,
}
}
For a container to know that as an implementation of http.Handler
is
necessary to use, we use the option di.As()
. The arguments of this
option must be a pointer(s) to an interface like new(Endpoint)
.
This syntax may seem strange, but I have not found a better way to specify the interface.
Updated container initialization code:
container, err := di.New(
// provide http server
di.Provide(NewServer),
// provide http serve mux as http.Handler interface
di.Provide(NewServeMux, di.As(new(http.Handler)))
)
if err != nil {
// handle error
}
Now container uses provide *http.ServeMux
as http.Handler
in server
constructor. Using interfaces contributes to writing more testable code.
Container automatically groups all implementations of interface to
[]<interface>
group. For example, provide with
di.As(new(http.Handler)
automatically creates a group
[]http.Handler
.
Let's add some http controllers using this feature. Controllers have typical behavior. It is registering routes. At first, will create an interface for it.
// Controller is an interface that can register its routes.
type Controller interface {
RegisterRoutes(mux *http.ServeMux)
}
Now we will write controllers and implement Controller
interface.
// OrderController is a http controller for orders.
type OrderController struct {}
// NewOrderController creates a auth http controller.
func NewOrderController() *OrderController {
return &OrderController{}
}
// RegisterRoutes is a Controller interface implementation.
func (a *OrderController) RegisterRoutes(mux *http.ServeMux) {
mux.HandleFunc("/orders", a.RetrieveOrders)
}
// RetrieveOrders loads orders and writes it to the writer.
func (a *OrderController) RetrieveOrders(writer http.ResponseWriter, request *http.Request) {
// implementation
}
// UserController is a http endpoint for a user.
type UserController struct {}
// NewUserController creates a user http endpoint.
func NewUserController() *UserController {
return &UserController{}
}
// RegisterRoutes is a Controller interface implementation.
func (e *UserController) RegisterRoutes(mux *http.ServeMux) {
mux.HandleFunc("/users", e.RetrieveUsers)
}
// RetrieveUsers loads users and writes it using the writer.
func (e *UserController) RetrieveUsers(writer http.ResponseWriter, request *http.Request) {
// implementation
}
Just like in the example with interfaces, we will use di.As()
provide option.
container, err := di.New(
di.Provide(NewServer), // provide http server
di.Provide(NewServeMux), // provide http serve mux
// endpoints
di.Provide(NewOrderController, di.As(new(Controller))), // provide order controller
di.Provide(NewUserController, di.As(new(Controller))), // provide user controller
)
if err != nil {
// handle error
}
Now, we can use []Controller
group in our mux. See updated code:
// NewServeMux creates a new http serve mux.
func NewServeMux(controllers []Controller) *http.ServeMux {
mux := &http.ServeMux{}
for _, controller := range controllers {
controller.RegisterRoutes(mux)
}
return mux
}
The full tutorial code is available here
With container option Options()
you can group your functionality:
// account module
account := di.Options(
di.Provide(NewAccountController),
di.Provide(NewAccountRepository),
)
// auth module
auth := di.Options(
di.Provide(NewAuthController),
di.Provide(NewAuthRepository),
)
// build container
container, err := di.New(
account,
auth,
)
if err != nil {
// handle error
}
In some cases you have more than one instance of one type. For example two instances of database: master - for writing, slave - for reading.
First way is a wrapping types:
// MasterDatabase provide write database access.
type MasterDatabase struct {
*Database
}
// SlaveDatabase provide read database access.
type SlaveDatabase struct {
*Database
}
Second way is a using named definitions with di.WithName()
provide
option:
// provide master database
di.Provide(NewMasterDatabase, di.WithName("master"))
// provide slave database
di.Provide(NewSlaveDatabase, di.WithName("slave"))
If you need to resolve it from container use di.Name()
resolve
option.
var db *Database
container.Resolve(&db, di.Name("master"))
If you need to provide named definition in another constructor embed
di.Inject
.
// ServiceParameters
type ServiceParameters struct {
di.Inject
// use `di` tag for the container to know that field need to be injected.
MasterDatabase *Database `di:"master"`
SlaveDatabase *Database `di:"slave"`
}
// NewService creates new service with provided parameters.
func NewService(parameters ServiceParameters) *Service {
return &Service{
MasterDatabase: parameters.MasterDatabase,
SlaveDatabase: parameters.SlaveDatabase,
}
}
Also, di.Inject
with tag optional
provide ability to skip dependency if it not exists
in the container.
// ServiceParameter
type ServiceParameter struct {
di.Inject
Logger *Logger `di:"" optional:"true"`
}
Constructors that declare dependencies as optional must handle the case of those dependencies being absent.
You can use naming and optional together.
// ServiceParameter
type ServiceParameter struct {
di.Inject
StdOutLogger *Logger `di:"stdout"`
FileLogger *Logger `di:"file" optional:"true"`
}
To avoid constant constructor changes, you can also use di.Inject
. Note, that supported only
struct pointers as constructing result.
// Controller has some endpoints.
type Controller struct {
di.Inject
// fields must be public
Users UserService `di:""`
Friends FriendsService `di:""`
}
// NewController creates controller.
func NewController() *Controller {
return &Controller{}
}
Note, that such a constructor will be incorrect without using
di
If you want to create a new instance on each extraction use
di.Prototype()
provide option.
di.Provide(NewRequestContext, di.Prototype())
If a provider creates a value that needs to be cleaned up, then it can return a closure to clean up the resource.
func NewFile(log Logger, path Path) (*os.File, func(), error) {
f, err := os.Open(string(path))
if err != nil {
return nil, nil, err
}
cleanup := func() {
if err := f.Close(); err != nil {
log.Log(err)
}
}
return f, cleanup, nil
}
After container.Cleanup()
call, it iterate over instances and call
cleanup function if it exists.
container, err := di.New(
// ...
di.Provide(NewFile),
)
if err != nil {
// handle error
}
// do something
container.Cleanup() // file was closed