Learn how to use the di
package by building a simple application that processes HTTP requests.
The full tutorial code is available here.
Before starting, you can enable tracing to get more information about
the library lifecycle. The di
package includes the default tracer that
prints output using the standard log
package:
func main() {
di.SetTracer(&di.StdTracer{})
//...
}
First, we need to provide ways to build two fundamental
types: http.Server
and http.ServeMux
. Let's create simple
functional constructors that build them:
// NewServer builds an HTTP server with the 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 are optional func([dep1, dep2, depN]) (result, [cleanup, error])
Now we can teach the container to build these types in three ways:
Using the preferred functional option style:
// create container
container, err := di.New(
di.Provide(NewServer),
di.Provide(NewServeMux),
)
if err != nil {
// handle error
}
Next, we can resolve the built server from the container. To do this, define
the variable of the resolved type and pass the variable pointer to the Resolve
function.
If no error occurs, we can use the variable.
// declare type variable
var server *http.Server
// resolving
err := container.Resolve(&server)
if err != nil {
// handle error
}
server.ListenAndServe()
The container creates singletons for combinations of the same type and tags.
As an alternative to resolve, we can use the Invoke()
function of
the Container
. It builds dependencies and calls the provided function. The Invoke
function can return an 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 the di.Invoke()
container option to call some
initialization code.
container, err := di.New(
di.Provide(NewServer),
di.Invoke(StartServer),
)
if err != nil {
// handle error
}
The container runs all invoke functions
in the order they were
declared. If one of them fails, the compilation fails.
Resulting dependencies will be lazy-loaded. If no one requests a type from the container, it won't be constructed.
You can provide an implementation as an interface. Use di.As()
for this.
The arguments of this option must be a pointer(s) to an interface like
new(http.Handler)
.
di.Provide(NewServeMux, di.As(new(http.Handler)))
This syntax with
new
can look strange, but I haven't found a better way to specify the interface.Create an issue if you know a better way ;)
Updated server constructor:
// NewServer creates an HTTP server with the provided mux as handler.
func NewServer(handler http.Handler) *http.Server {
return &http.Server{
Handler: handler,
}
}
Final 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 the container uses *http.ServeMux
as the implementation of http.Handler
.
Interface usage contributes to writing more testable code.
The container automatically groups the same types into a []<type>
slice. It
works with di.As()
too. For example, di.As(new(http.Handler)
automatically creates a group []http.Handler
.
Let's add some HTTP controllers using this feature. The main function of controllers is registering routes. First, create an interface for it.
// Controller is an interface that can register its routes.
type Controller interface {
RegisterRoutes(mux *http.ServeMux)
}
Next, create implementations for this interface.
// OrderController is an HTTP controller for orders.
type OrderController struct {}
// NewOrderController creates an 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 them to the writer.
func (a *OrderController) RetrieveOrders(writer http.ResponseWriter, request *http.Request) {
// implementation
}
// UserController is an HTTP endpoint for users.
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 them using the writer.
func (e *UserController) RetrieveUsers(writer http.ResponseWriter, request *http.Request) {
// implementation
}
Just like in the example with interfaces, we will use the 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 the []Controller
group in our mux. 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