/ go

Lazy Providers in Dig

If you haven't yet read my previous post on depedency injection in Go, please do so first. This post describes a simple technique to make dig's DI container even more powerful.

All values provided by dig's container are singletons. The provider is called the first time the value is required and the result is cached. All subsequent providers that depend on this value used the cached result. This behavior is usually very desireable but there are times when we want a fresh value to be produced by the container. Often this is useful for data that is scoped by a lifecycle that is shorter than the lifecycle of the application itself (say, the lifecycle of an HTTP request inside of an HTTP server).

Some DI containers provide explicit functionality to achieve this kind of behavior. Dig doesn't because it focused on a small, simple API (a good decision, in my opinion). We've been given enough, however, to implement this functionality ourselves.

Let's start with some example code.

package main

import (
  "fmt"
  "math/rand"
  "time"

  "go.uber.org/dig"
)

type RequestContext struct {
  RequestId int
}

func NewRequestContext() *RequestContext {
  return &RequestContext{rand.Intn(1000)}
}

type RequestHandler struct {
  requestContext *RequestContext
}

func (handler *RequestHandler) HandleRequest() {
  fmt.Println(handler.requestContext.RequestId)
}

func NewRequestHandler(rc *RequestContext) *RequestHandler {
  return &RequestHandler{rc}
}

func main() {
  rand.Seed(time.Now().UnixNano())

  container := dig.New()

  container.Provide(NewRequestContext)
  container.Provide(NewRequestHandler)

  container.Invoke(func(handler *RequestHandler) {
    handler.HandleRequest()
  })
  
  container.Invoke(func(handler *RequestHandler) {
    handler.HandleRequest()
  })
}

If we run the code as-is, we'll see the same number printed twice. We'd like to maintain a singleton RequestHandler but have a new RequestContext each time HandleRequest is called. How can we accomplish this? We simply provide a thunk (wrapper function) of the value we want rather than the value itself. We can then use this thunk each time HandleRequest is called. Let's try it.

package main

import (
  "fmt"
  "math/rand"
  "time"

  "go.uber.org/dig"
)

type RequestContext struct {
  RequestId int
}

func NewRequestContext() *RequestContext {
  return &RequestContext{rand.Intn(1000)}
}

type RequestHandler struct {
  requestContextProvider func() *RequestContext
}

func (handler *RequestHandler) HandleRequest() {
  requestContext := handler.requestContextProvider()

  fmt.Println(requestContext.RequestId)
}

func NewRequestHandler(rcp func() *RequestContext) *RequestHandler {
  return &RequestHandler{rcp}
}

func main() {
  rand.Seed(time.Now().UnixNano())

  container := dig.New()

  container.Provide(func() func() *RequestContext {
    return NewRequestContext
  })
  container.Provide(NewRequestHandler)

  container.Invoke(func(handler *RequestHandler) {
    handler.HandleRequest()
  })

  container.Invoke(func(handler *RequestHandler) {
    handler.HandleRequest()
  })
}

Now, when we run the above code we'll see two different numbers printed out each time[1]. This works recursively, meaning you can have whole subtrees of your container's dependency graph generated for individual lifecycles inside of your app.


  1. Yes, I am aware that there is a 1 in 1000 chance that these numbers will be the same. If you run it and they are the same, you win a prize. ↩︎

Lazy Providers in Dig
Share this