6.6 KiB
Recipes — uber-go/dig
End-to-end examples that go beyond the SKILL.md basics. Each recipe is self-contained and shows a real wiring problem.
HTTP server with route group
package main
import (
"fmt"
"log"
"net/http"
"go.uber.org/dig"
)
// Each handler contributes one route to the "routes" group.
type RouteResult struct {
dig.Out
Route Route `group:"routes"`
}
type Route struct {
Pattern string
Handler http.Handler
}
func NewHealthRoute() RouteResult {
return RouteResult{Route: Route{
Pattern: "/health",
Handler: http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
w.WriteHeader(http.StatusOK)
}),
}}
}
func NewUserRoute(repo *UserRepo) RouteResult {
return RouteResult{Route: Route{
Pattern: "/users",
Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
users, err := repo.List(r.Context())
if err != nil {
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
return
}
fmt.Fprintf(w, "%d users", len(users))
}),
}}
}
// The server consumes every Route registered to "routes".
type ServerParams struct {
dig.In
Routes []Route `group:"routes"`
}
func NewServer(p ServerParams) *http.Server {
mux := http.NewServeMux()
for _, r := range p.Routes {
mux.Handle(r.Pattern, r.Handler)
}
return &http.Server{Addr: ":8080", Handler: mux}
}
func main() {
c := dig.New()
must(c.Provide(NewDB)) // *sql.DB
must(c.Provide(NewUserRepo)) // *UserRepo
must(c.Provide(NewHealthRoute)) // adds to group
must(c.Provide(NewUserRoute)) // adds to group
must(c.Provide(NewServer))
err := c.Invoke(func(srv *http.Server) error {
log.Println("listening on", srv.Addr)
return srv.ListenAndServe()
})
if err != nil {
log.Fatal(err)
}
}
func must(err error) {
if err != nil {
panic(err)
}
}
Two databases (read-write + read-only)
type DBResult struct {
dig.Out
Primary *sql.DB `name:"primary"`
ReadOnly *sql.DB `name:"readonly"`
}
func NewDatabases(cfg *Config) (DBResult, error) {
rw, err := sql.Open("postgres", cfg.PrimaryDSN)
if err != nil {
return DBResult{}, fmt.Errorf("primary: %w", err)
}
ro, err := sql.Open("postgres", cfg.ReadOnlyDSN)
if err != nil {
rw.Close()
return DBResult{}, fmt.Errorf("readonly: %w", err)
}
return DBResult{Primary: rw, ReadOnly: ro}, nil
}
type RepoParams struct {
dig.In
Writer *sql.DB `name:"primary"`
Reader *sql.DB `name:"readonly"`
}
func NewUserRepo(p RepoParams) *UserRepo {
return &UserRepo{w: p.Writer, r: p.Reader}
}
Provide as interface (dig.As) to hide concrete types
type Cache interface {
Get(key string) (string, bool)
Set(key, value string)
}
type RedisCache struct {
client *redis.Client
metrics *Metrics // an internal field consumers should not see
}
func NewRedisCache(client *redis.Client, m *Metrics) *RedisCache {
return &RedisCache{client: client, metrics: m}
}
func (c *RedisCache) Get(key string) (string, bool) { /* ... */ }
func (c *RedisCache) Set(key, value string) { /* ... */ }
func main() {
c := dig.New()
must(c.Provide(NewRedisClient))
must(c.Provide(NewMetrics))
// Consumers see Cache, never *RedisCache or its internals.
must(c.Provide(NewRedisCache, dig.As(new(Cache))))
must(c.Invoke(func(cache Cache) {
cache.Set("hello", "world")
}))
}
Request-scoped dependencies
A child scope inherits its parent's providers but adds request-local ones:
root := dig.New()
must(root.Provide(NewLogger))
must(root.Provide(NewDB))
must(root.Provide(NewHandler)) // *Handler is shared; the scope inherits it
func handle(w http.ResponseWriter, req *http.Request) {
scope := root.Scope("request")
// Request-scoped values
must(scope.Provide(func() *http.Request { return req }))
must(scope.Provide(func() RequestID { return RequestID(req.Header.Get("X-Request-ID")) }))
must(scope.Decorate(func(l *zap.Logger) *zap.Logger {
return l.With(zap.String("request_id", req.Header.Get("X-Request-ID")))
}))
err := scope.Invoke(func(h *Handler) error {
return h.Serve(w, req)
})
if err != nil {
http.Error(w, err.Error(), 500)
}
}
The decorator only applies inside the request scope — sibling scopes (other in-flight requests) keep their own logger.
Optional dependency for graceful degradation
type WorkerParams struct {
dig.In
DB *sql.DB
Tracer trace.Tracer `optional:"true"` // app still boots without OTel
}
func NewWorker(p WorkerParams) *Worker {
w := &Worker{db: p.DB}
if p.Tracer != nil {
w.tracer = p.Tracer
} else {
w.tracer = trace.NewNoopTracerProvider().Tracer("noop")
}
return w
}
Reach for optional only when the dependency is genuinely optional — a missing DB hidden behind optional becomes a nil-pointer panic at first use.
Decorate to add cross-cutting behavior
// Wrap the *sql.DB with a metrics-recording wrapper everywhere.
must(c.Decorate(func(db *sql.DB, m *Metrics) *sql.DB {
return wrapWithMetrics(db, m)
}))
// Wrap the logger with service tags.
must(c.Decorate(func(log *zap.Logger, cfg *Config) *zap.Logger {
return log.With(
zap.String("service", cfg.ServiceName),
zap.String("env", cfg.Env),
)
}))
Decorators are scope-local. A decorator on the root applies everywhere; a decorator on a child scope only applies to that subtree.
DryRun for graph validation in tests
func TestWiringIsValid(t *testing.T) {
c := dig.New(dig.DryRun(true))
// Register everything main() registers
must := func(err error) {
require.NoError(t, err)
}
must(c.Provide(NewConfig))
must(c.Provide(NewLogger))
must(c.Provide(NewDB))
must(c.Provide(NewServer))
// Invoke the composition root: dig validates types without running constructors.
require.NoError(t, c.Invoke(func(*http.Server) {}))
}
This catches "no provider for *X" failures at build time instead of in production.
Visualizing a failed graph
err := c.Invoke(run)
if err != nil {
f, _ := os.Create("graph.dot")
defer f.Close()
_ = dig.Visualize(c, f, dig.VisualizeError(err))
log.Fatalf("wiring failed (graph in graph.dot): %v", err)
}
// Render: dot -Tpng graph.dot -o graph.png
VisualizeError highlights the missing edges in red — much faster than reading the wrapped error chain.