A dependency-injection container for Go 1.27+. Constructors are plain functions, keys are Go types, and the container builds, starts and stops services in dependency order.
app := di.New()
app.Value(Config{DSN: "postgres://localhost/app"})
app.Wire[*DB](NewDB) // func NewDB(Config) (*DB, error)
app.Wire[*Repo](NewRepo) // func NewRepo(*DB) *Repo
repo := app.Get[*Repo]() // builds Config, then DB, then Repo, each onceA registration takes OnStart, OnStop and Worker hooks typed on the
service, and app.Run(ctx) starts everything in dependency order, waits for
a signal, and stops it in reverse. Child scopes hold what belongs to one
request or one test; a second registration of a key is rejected unless it
says Override(); and Explain, Graph and Modules show what was built,
from what, and by which module.
There is no code generation and no dependency outside the standard library.
Wire reads a constructor's signature once, with reflection, which is how
Validate can check the whole graph before a single constructor runs.
Provide takes a closure for the rare constructor that needs the scope
itself.
The guide walks through one application
file by file. How it works explains what happens between
Get and a value, with diagrams.
go get github.com/floatdrop/diRequires Go 1.27 or newer. Editor support for generic methods needs gopls v0.23 or newer.
// Quick start: register a few services, start and stop the application.
package main
import (
"context"
"fmt"
"log"
"github.com/floatdrop/di"
)
type Config struct{ DSN string }
type DB struct{ dsn string }
type Repo struct{ db *DB }
type Server struct{ repo *Repo }
// Plain constructors: their parameters are their dependencies.
func NewDB(cfg Config) *DB { return &DB{dsn: cfg.DSN} }
func NewRepo(db *DB) *Repo { return &Repo{db: db} }
func NewServer(repo *Repo) *Server { return &Server{repo: repo} }
func main() {
app := di.New()
app.Value(Config{DSN: "postgres://localhost/app"})
app.Wire[*DB](NewDB).
OnStop(func(ctx context.Context, db *DB) error { fmt.Println("db closed"); return nil })
app.Wire[*Repo](NewRepo)
app.Wire[*Server](NewServer).
Eager().
OnStart(func(ctx context.Context, srv *Server) error { fmt.Println("listening"); return nil }).
OnStop(func(ctx context.Context, srv *Server) error { fmt.Println("server stopped"); return nil })
ctx := context.Background()
if err := app.Start(ctx); err != nil {
log.Fatal(err)
}
defer func() {
if err := app.Stop(ctx); err != nil {
log.Println(err)
}
}()
fmt.Println("serving", app.Get[*Server]().repo.db.dsn)
}The sections follow the order an application comes together: register
services and resolve them, give them a lifecycle, split them into scopes,
compose them from modules, and then check and inspect the result. Every
embedded example is a compiled program under examples/, built
in CI.
Each registration returns a Binding[T]. Its methods adjust the
registration and must be called before the scope is first resolved.
| Call | Registers |
|---|---|
s.Provide(func(*di.Scope) T) |
A lazily built singleton. T is inferred. |
s.Wire[T](NewT) |
A lazily built singleton from a plain constructor. Its parameters are its dependencies; see Validate. |
s.Wrap[T](fn) |
A wrapper over what serves T: fn takes that value first, then its dependencies; see Wrapping a service. |
s.Value(v) |
An instance you already have. |
s.Use(mods...) |
What the modules register, attributed to them by name. |
| Method | Effect |
|---|---|
.Scoped() |
One instance per resolving scope, built and stopped there. |
.Group() |
A member of the group for T, read back with s.All[T](). |
.Eager() |
Build during Start, in registration order. |
.Override() |
Replace an earlier registration of T in this scope; a second one without it is rejected. |
.OnStart(f), .OnStop(f) |
Lifecycle hooks, f is func(context.Context, T) error. |
.OnDrain(f) |
Runs before anything is stopped, while the scope still resolves. |
.Worker(f) |
A long-running function, cancelled on stop. |
To get a service back, call the scope, from a constructor or from outside:
| Call | Returns |
|---|---|
s.Get[T]() |
T. Inside a constructor a failure unwinds to the caller; at top level it panics with the error. |
s.Resolve[T]() |
(T, error). Never panics on a wiring problem. |
s.Maybe[T]() |
(T, bool), for optional dependencies. |
s.All[T]() |
Every member of the group for T, across the scope chain. |
s.Must(v, err) |
v, or aborts the constructor with err. |
s.Context() |
The context passed to Start, so constructors can dial with a deadline. |
Errors wrap di.ErrNotProvided, di.ErrCycle or di.ErrStopped:
di: building *app.Repo (provided at app/wire.go:31): *app.DB: not provided (needed by [*app.Repo])
di: building *app.A (provided at ...): di: building *app.B (provided at ...): di: dependency cycle: [*app.A *app.B] -> *app.A
Provide takes a closure. It pulls its dependencies with s.Get and can do
anything else it likes, and the container learns what it needed by watching
it run. Wire takes a constructor as it is written, func(A, B) T or
func(A, B) (T, error), and its parameters are its dependencies. Each one is
resolved as the closure would have resolved it, from the same scope, so
lifetimes, hooks, cycles and errors behave the same. The difference is that
a wired constructor's dependencies are known before it runs, so
Validate can check them and Explain can draw them.
Wire reads the signature with reflection once, when the constructor is
registered, and rejects one of the wrong shape there. Building calls it
through reflect.Call, about 150 ns and two allocations more than a closure;
a warm Get is the same code for both. Two things to know: a slice parameter
is a key like any other, not the group for its element type; and a
constructor that needs the scope itself, for s.Context() or a dependency
chosen at run time, stays a Provide closure.
An interface is served by a constructor that returns the implementation:
app.Provide(func(s *di.Scope) Reader { return s.Get[*Repo]() })
app.Wire[Reader](NewRepo) // the same, when NewRepo returns *RepoThe compiler checks that *Repo satisfies Reader, and both keys share one
instance, since the constructor returns the same pointer. Mark it Scoped()
too when the target is. Wire needs only a result assignable to the key, so
app.Wire[Reader](NewRepo) serves the interface directly, and a constructor
whose result is not assignable is rejected at registration.
Three rules, all checked when the scope is next resolved:
- A second registration of a key in one scope must say
Override(). It then serves the key and inherits its eagerness. A duplicate without the marker, or anOverride()with nothing to override, is rejected, naming both registrations. A child scope shadows its parent's key without the marker, because that is a different scope. - Once a key has served a value it cannot be replaced, in the scope that owns it or in any scope that resolved through it. A resolution that failed built nothing, so the key stays open.
Eageron aScopedbinding, andScopedon aValue, are rejected whichever order the methods were called in.
Start builds every Eager binding, then runs OnStart hooks in build
order. If a constructor or hook fails, Start stops what had started, child
scopes included, and returns both errors. A service built after Start runs
its OnStart as it is built, so nothing is handed out unstarted.
Stop has three phases: drain, stop the child scopes, then run OnStop
hooks in reverse build order. Every failure is joined into the returned
error. A service is stopped when its OnStart succeeded or when it has no
OnStart, in which case OnStop is a plain destructor. After Stop, the
scope and everything under it refuses to resolve with di.ErrStopped.
Stop is safe to call twice or concurrently. The first call does the work;
the others wait for it and report its result. That is also why a hook must
not call Stop on its own scope or an ancestor, which would wait on itself.
Call Shutdown, which never blocks.
OnStart should return when the service is ready, not run it. A server binds
its listener in the hook, so a busy port fails Start, and serves in a
goroutine.
OnDrain runs before anything is stopped, innermost scope first and in
reverse build order, while every scope still resolves. It is where a service
stops taking new work and finishes what it has.
app.Wire[*http.Server](newServer).Eager().
OnDrain(func(ctx context.Context, srv *http.Server) error { return srv.Shutdown(ctx) }).
OnStop(func(ctx context.Context, srv *http.Server) error { return srv.Close() })An HTTP server is the case that needs it. Its handlers hold request scopes
under the application scope. Shutting the server down from OnStop would
race the teardown of those scopes, and a request in flight would fail with
di.ErrStopped before the server finished waiting for it. Draining first
keeps the handlers' scopes alive until they return.
Worker is for anything that loops until told to stop: consumers, pollers,
schedulers.
app.Wire[*Mailer](newMailer).Eager().Worker(func(ctx context.Context, m *Mailer) error {
return m.Loop(ctx) // returns when ctx is cancelled
})The function starts in its own goroutine when the service starts. Its
context is cancelled when the service stops, and Stop waits for it within
the stop deadline. A worker that returns an error calls Shutdown, so a dead
worker takes the application down instead of leaving it half alive, even when
the failure surfaces during shutdown. The one return that means nothing is
context.Canceled after cancellation: the worker stopped because it was told
to.
Run is the helper for main. It starts the scope and blocks until the
context is cancelled, SIGINT or SIGTERM arrives, or Shutdown is called.
Then it stops everything with a bounded context. A second signal during the
stop cancels that context, so a hung hook cannot keep the process alive.
examples/server/main.go, an HTTP server with OnStart, OnDrain and OnStop, run with a stop timeout
// Graceful shutdown of an HTTP server.
//
// Run starts the scope, waits for SIGINT/SIGTERM or a Shutdown call, then
// stops everything in reverse order with a bounded context. The server's
// OnDrain calls http.Server.Shutdown, which stops accepting connections and
// waits for in-flight requests until the stop context expires. Draining runs
// before anything is torn down, so those requests still have their scopes.
package main
import (
"context"
"errors"
"fmt"
"log"
"net"
"net/http"
"time"
"github.com/floatdrop/di"
)
type DB struct{ dsn string }
func main() {
app := di.New()
app.Wire[*DB](func() *DB { return &DB{dsn: "postgres://localhost/app"} }).
OnStop(func(ctx context.Context, db *DB) error { log.Println("db closed"); return nil })
app.Wire[http.Handler](func(db *DB) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
time.Sleep(2 * time.Second) // simulate slow work that must not be cut short
fmt.Fprintln(w, "served by", db.dsn)
})
})
app.Wire[*http.Server](func(h http.Handler) *http.Server { return &http.Server{Addr: ":8080", Handler: h} }).
Eager().
OnStart(func(ctx context.Context, srv *http.Server) error {
// Bind synchronously so a busy port fails Start; serve in the background.
ln, err := net.Listen("tcp", srv.Addr)
if err != nil {
return err
}
log.Println("listening on", ln.Addr())
go func() {
if err := srv.Serve(ln); !errors.Is(err, http.ErrServerClosed) {
app.Shutdown(err) // the listener died: stop the whole application
}
}()
return nil
}).
// OnDrain runs before anything is stopped, so handlers that are
// still running keep their scopes and dependencies.
OnDrain(func(ctx context.Context, srv *http.Server) error {
log.Println("draining")
return srv.Shutdown(ctx) // waits for in-flight requests, bounded by StopTimeout
}).
OnStop(func(ctx context.Context, srv *http.Server) error { return srv.Close() })
// Blocks until Ctrl-C, SIGTERM, or app.Shutdown. A second signal cancels
// the stop context so a hung hook cannot keep the process alive.
if err := app.Run(context.Background(), di.StopTimeout(10*time.Second)); err != nil {
log.Fatal(err)
}
}A child scope resolves through its parent, shares the parent's singletons, and owns what it builds itself.
// Scopes: a child scope sees everything in its parent and can shadow it.
package main
import (
"fmt"
"github.com/floatdrop/di"
)
type DB struct{ dsn string }
type User struct{ Name string }
type Handler struct {
db *DB
user *User
}
func NewHandler(db *DB, user *User) *Handler { return &Handler{db: db, user: user} }
func main() {
app := di.New()
app.Wire[*DB](func() *DB { return &DB{dsn: "postgres://localhost/app"} })
// One child per request: request-scoped values live here, shared
// singletons such as *DB are reused from app.
req := app.Child("request")
req.Value(&User{Name: "ada"})
req.Wire[*Handler](NewHandler)
h := req.Get[*Handler]()
fmt.Println(h.user.Name, "->", h.db.dsn)
fmt.Println("same db:", h.db == app.Get[*DB]())
}A singleton is built in the scope that registered it, so a child cannot
rewire a parent's singleton. A service that has to see a child's values is
marked Scoped(): one instance per resolving scope, built there.
A dihttp.Middleware gives each request a child scope holding the
*http.Request, attaches it to the request context, and stops it when the
handler returns. It has the usual func(http.Handler) http.Handler shape,
so a router accepts it too. dihttp.Module registers one, and a server's
constructor takes it like any other dependency:
import "github.com/floatdrop/di/dihttp"
app.Use(dihttp.Module, api.Module)
func NewServer(cfg Config, mw dihttp.Middleware) *http.Server {
mux := http.NewServeMux()
mux.Handle("GET /users/{id}", dihttp.Handle((*Users).Show))
mux.Handle("GET /healthz", dihttp.Handle((*Health).Check))
return &http.Server{Addr: cfg.Addr, Handler: mw(mux)}
}dihttp.Handle resolves a handler type from the request's scope and calls
the method. A method expression names both, so one type per resource, with a
method per route, keeps its dependencies in one place. Mark the type
Scoped() when it needs the request and leave it a singleton when it does
not; Handle follows either. A handler written by hand reaches the scope
with di.FromContext(r.Context()), and dihttp.NewMiddleware(app) makes a
middleware outside the container.
Services that depend on the request are declared once, in the root, as
Scoped(). They are built per request, kept for its duration, and stopped
with it:
app.Wire[*User](func(r *http.Request) *User { return &User{Name: r.Header.Get("X-User")} }).Scoped()di.WithScope and di.FromContext are the primitives when net/http is
not in the picture. A complete service with a worker, a health endpoint,
request scopes and graceful shutdown is in
examples/app.
Group() makes a registration one member of the group for its type instead
of the binding for it, and All resolves every member across the scope
chain. The usual case is a health endpoint: a group of checkers, and a
handler that decides what healthy means.
type Checker interface{ Check(ctx context.Context) error }
app.Wire[Checker](func(db *DB) Checker { return db }).Group()
mux.HandleFunc("GET /healthz", func(w http.ResponseWriter, r *http.Request) {
for _, c := range app.All[Checker]() {
if err := c.Check(r.Context()); err != nil {
http.Error(w, err.Error(), http.StatusServiceUnavailable)
return
}
}
fmt.Fprintln(w, "ok")
})All builds any member not built yet, so a checker's target is up by the
time it is asked. Members keep their own lifetimes and hooks. A plain
registration of the same type is neither shadowed by the group nor part of
it.
A key is a Go type, so two live instances of one type need two types. While
the set is fixed — a primary and a replica, two HTTP clients with different
timeouts — a defined type names each one, and embedding keeps the methods:
type Primary struct{ *DB }. The surrogate appears in constructor signatures
and nowhere else. Wire resolves it like any other parameter, so nothing has
to be annotated to say which instance feeds which argument, and Validate
and Explain see two distinct services rather than one type registered
twice.
When the set comes from configuration there are no types to write. Register
the instances as a group, fold them into a registry, and let the scope that
resolves the key say which one it wants: the selector is an ordinary
constructor whose parameter is the key, and Scoped() leaves the choice to
the resolving scope.
examples/instances/main.go, a primary and a replica by type, and configured shards by a scoped selector
// More than one instance of a type: a defined type names each one while the
// set is fixed, and a Scoped selector picks one when the set comes from
// configuration.
package main
import (
"context"
"fmt"
"github.com/floatdrop/di"
)
type DB struct{ dsn string }
func (d *DB) Query() string { return "query " + d.dsn }
// A fixed set: one defined type per instance. Embedding promotes the methods,
// so only the constructor below mentions the surrogate.
type Primary struct{ *DB }
type Replica struct{ *DB }
type Repo struct{ read, write *DB }
func NewRepo(p Primary, r Replica) *Repo { return &Repo{write: p.DB, read: r.DB} }
// A configured set: the shards are not known until the config is read, so no
// type can name them. They are registered as a group, folded into a registry,
// and picked by a value the resolving scope provides.
type Config struct{ Shards []string }
type Shard struct {
Name string
DB *DB
}
type Shards map[string]*DB
type ShardName string
func selectShard(want ShardName, all Shards) (*DB, error) {
db, ok := all[string(want)]
if !ok {
return nil, fmt.Errorf("no shard %q", want)
}
return db, nil
}
func main() {
cfg := Config{Shards: []string{"eu-1", "us-1"}}
app := di.New()
app.Value(cfg)
// Two databases, told apart by type. Each keeps its own hooks.
app.Value(Primary{&DB{dsn: "primary"}}).
OnStop(func(context.Context, Primary) error { fmt.Println("primary closed"); return nil })
app.Value(Replica{&DB{dsn: "replica"}}).
OnStop(func(context.Context, Replica) error { fmt.Println("replica closed"); return nil })
app.Wire[*Repo](NewRepo)
repo := app.Get[*Repo]()
fmt.Println("writes:", repo.write.Query())
fmt.Println("reads: ", repo.read.Query())
// One binding per configured shard, read back together as a registry.
for _, name := range cfg.Shards {
app.Value(Shard{Name: name, DB: &DB{dsn: name}}).Group()
}
app.Provide(func(s *di.Scope) Shards {
m := Shards{}
for _, sh := range s.All[Shard]() {
m[sh.Name] = sh.DB
}
return m
})
// The selector is an ordinary constructor: its ShardName parameter is the
// key, and Scoped() leaves the choice to the scope that resolves it.
app.Wire[*DB](selectShard).Scoped()
for _, tenant := range cfg.Shards {
req := app.Child(tenant)
req.Value(ShardName(tenant))
fmt.Println(tenant, "->", req.Get[*DB]().Query())
_ = req.Stop(context.Background())
}
// The root does not provide ShardName, so Validate reports it as owed by
// whichever scope resolves the shard rather than as a failure.
fmt.Println("owed:", app.Validate().Owed)
_ = app.Stop(context.Background())
}Rules and traps:
- Use
Wirefor the selector rather thanProvide. Its parameter declares the key, soValidatereports the key inOwed— the obligation the resolving scope carries — andValidate(di.Provided[T]())discharges it as a request scope would. AProvideclosure behaves identically at run time and declares nothing, so the missing key is found by the first request that needs it. - One key resolves to one value per scope. A caller that needs two shards at once reads the registry, or opens a scope for each.
- A type alias is not a new key.
type CacheDB = *DBis the samereflect.Type, and the second registration of it is rejected as a duplicate. A surrogate has to be a defined type. - A surrogate that embeds an interface satisfies that interface, so
type Cold struct{ Store }compiles wherever aStoreis wanted. It is still a separate key: registeringColddoes not serveStore.
A module is a function that registers into a scope. Use applies modules
in order and records which module made each registration:
func Storage(s *di.Scope) {
s.Provide(func(*di.Scope) *DB { return open(storageDSN) })
s.Wire[*Repo](NewRepo)
}
func Caching(s *di.Scope) {
s.Provide(func(*di.Scope) *DB { return open(cacheDSN) }) // also a *DB
s.Wire[*Cache](NewCache)
}
app := di.New()
app.Use(Storage, Caching)
app.Get[*Repo]()
// di: *app.DB is provided at app.Storage (storage.go:12) and again at
// app.Caching (caching.go:8): a second registration of a key must be marked
// Override() to replace the firstWithout that rule the second *DB would have won silently and rewired
Storage's *Repo to Caching's database. Two modules that each need a
*DB of their own declare distinct types, type CacheDB struct{ *DB }; a
module that means to replace another's registration says Override().
Keys are types, so a service whose type is unexported can be named, and
therefore resolved, overridden, shadowed or wrapped, only by its own package.
That is the whole privacy model. A module exports its contract and its
Module function and keeps the rest lowercase:
type db struct{ dsn string } // only this package can say Get[*db]()
func Module(s *di.Scope) {
s.Wire[*db](newDB).OnStop(func(_ context.Context, db *db) error { return db.Close() })
s.Wire[Store](newPGStore) // the exported contract
}Explain, Graph and Validate still see the private services. A test
can override exactly what is exported: the contract, or the configuration
the private service is built from.
Wrap[T] composes over whatever serves T at the time it is called: the
latest registration in this scope, or the one an ancestor provides. The
function takes the wrapped value first and its other dependencies after it,
and returns T or (T, error), read the way Wire reads a constructor.
The wrapped registration keeps its hooks and lifetime. It is built first, as
the wrapper's dependency, and stopped after it. Wrappers chain in
registration order. uber/fx calls this Decorate.
examples/wrap/main.go, the program that prints the output below
// Wrap: compose over a service without replacing it. The wrapped
// registration keeps its hooks and lifetime, and a wrapper in a child scope
// applies to that scope alone.
package main
import (
"context"
"fmt"
"github.com/floatdrop/di"
)
type Store interface{ Get(key string) string }
type PGStore struct{}
type Cache struct{ hits int }
type CachingStore struct {
next Store
cache *Cache
}
type TracingStore struct{ next Store }
func (*PGStore) Get(key string) string { return "row " + key }
func (c *CachingStore) Get(key string) string { c.cache.hits++; return c.next.Get(key) }
func (t *TracingStore) Get(key string) string { return "traced(" + t.next.Get(key) + ")" }
func NewPGStore() *PGStore { return &PGStore{} }
func NewCachingStore(next Store, c *Cache) Store { return &CachingStore{next: next, cache: c} }
func NewTracingStore(next Store) Store { return &TracingStore{next: next} }
func main() {
app := di.New()
app.Value(&Cache{})
app.Wire[Store](NewPGStore).
OnStop(func(context.Context, Store) error { fmt.Println("pg closed"); return nil })
// The first parameter is the value being wrapped; the rest are
// dependencies. The store keeps its OnStop, and is stopped after the
// wrapper, since it was built first.
app.Wrap[Store](NewCachingStore)
// A wrapper in a child scope wraps the parent's value for that scope and
// its descendants; the parent and its other children are untouched.
debug := app.Child("debug")
debug.Wrap[Store](NewTracingStore)
fmt.Println("app: ", app.Get[Store]().Get("1"))
fmt.Println("debug:", debug.Get[Store]().Get("1"))
fmt.Print(app.Explain[Store]())
_ = app.Stop(context.Background())
}app: row 1
debug: traced(row 1)
main.Store: singleton wrapper in root, built (provided at main.go:40)
├── main.Store: singleton in root, built (provided at main.go:34)
└── *main.Cache: value in root, built (provided at main.go:33)
needed by: main.Store in debug
pg closed
Three rules:
- A wrapper takes the lifetime of what it wraps, so a wrapper over a
Scopedservice is one per scope.Scoped()on the wrapper puts one wrapper per scope around a shared singleton. - A wrapper in a child scope applies to that child and its descendants. The parent and its other children keep the original.
Override()after a wrapper replaces the wrapper and everything it wrapped. A registration some wrapper composes over cannot be overridden while that wrapper stands. Nothing to wrap, a group, and a key this scope has already resolved are rejected.
di.Test wires the production graph into a fresh scope, stops it when the
test ends, and fails the test if a stop hook errors. Override what you need
before anything is resolved, and mark it Override().
package app
import (
"testing"
"github.com/floatdrop/di"
)
func TestRepo(t *testing.T) {
s := di.Test(t, Production) // production graph, stopped when the test ends
s.Value(&DB{DSN: "sqlite://memory"}).Override() // replaces the production *DB, and says so
repo := s.Get[*Repo]() // built against the fake DB
if repo.DB.DSN != "sqlite://memory" {
t.Fatalf("got %q", repo.DB.DSN)
}
}A wired constructor's dependencies are known at registration, so the graph
they form can be checked with nothing built. Validate walks it and reports
what would fail: a dependency nothing provides, a cycle, or a singleton that
would build a request-scoped service in the wrong scope. A closure's
dependencies are unknown until it runs, so Provide registrations are listed
as unchecked rather than checked.
examples/wire/main.go, the program that prints the output below
// Wire: plain constructors whose parameters are their dependencies, and a
// graph that is checked before anything is built.
package main
import (
"fmt"
"net/http"
"github.com/floatdrop/di"
)
type Config struct{ DSN string }
type DB struct{ dsn string }
type Repo struct{ db *DB }
type User struct{ name string }
type Handler struct {
repo *Repo
user *User
}
type Mailer struct{ user *User }
// The constructors know nothing about di.
func NewDB(cfg Config) *DB { return &DB{dsn: cfg.DSN} }
func NewRepo(db *DB) *Repo { return &Repo{db: db} }
func NewUser(r *http.Request) *User { return &User{name: r.Header.Get("X-User")} }
func NewHandler(repo *Repo, user *User) *Handler { return &Handler{repo: repo, user: user} }
func NewMailer(user *User) *Mailer { return &Mailer{user: user} }
func main() {
app := di.New()
app.Value(Config{DSN: "postgres://localhost/app"})
app.Wire[*DB](NewDB)
app.Wire[*Repo](NewRepo)
app.Wire[*User](NewUser).Scoped() // one per request scope, where the *http.Request is
app.Wire[*Handler](NewHandler).Scoped()
// Nothing has been built, but the constructors declared their edges, so
// Explain draws them, dashed, down to what only a request scope provides.
fmt.Print(app.Explain[*Handler]())
fmt.Println()
// Validate walks the same edges. From the application scope, *User needs
// an *http.Request that only a request scope provides: owed, not wrong.
v := app.Validate()
fmt.Println("errors:", v.Err())
fmt.Println("owed: ", v.Owed)
// Told what a request scope holds, the check is the one that scope
// would make, and nothing is owed.
fmt.Println("request scopes:", app.Validate(di.Provided[*http.Request]()).Err())
// A singleton depending on a request-scoped service would be built in
// app, where there is no request. A closure would fail on first use;
// the declared graph fails here.
app.Wire[*Mailer](NewMailer)
fmt.Println(app.Validate().Err())
}*main.Handler: scoped in root, not built (provided at main.go:35)
├╌╌ *main.Repo: singleton in root, not built (provided at main.go:33)
│ └╌╌ *main.DB: singleton in root, not built (provided at main.go:32)
│ └╌╌ main.Config: value in root, not built (provided at main.go:31)
└╌╌ *main.User: scoped in root, not built (provided at main.go:34)
└╌╌ *net/http.Request: not provided
errors: <nil>
owed: [*net/http.Request: needed by *main.User (scoped, provided at main.go:34)]
request scopes: <nil>
di: *net/http.Request: not provided in scope root (needed by [*main.Mailer *main.User]; *main.User is Scoped, so the singleton *main.Mailer would build it there)
| Call | Returns |
|---|---|
s.Validate() |
A Validation. Err() joins Errors, the failures the declared graph proves. Owed lists what a Scoped binding needs that this scope does not provide, left to the scope that resolves it. Unchecked lists the Provide closures. |
s.Validate(di.Provided[T]()...) |
The same check as the resolving scope would make it, told that it holds a T. With stubs nothing is owed: what neither the scope nor the stubs provide is an error. |
A singleton is checked against the scope that registered it, because that
is where it is built. A Scoped binding is built in whichever scope resolves
it, so Validate checks it as if the calling scope were that scope, and what
the calling scope does not provide is owed rather than wrong: a descendant
may provide it, as request scopes provide the request. Call Validate from
that descendant, or say what it will hold with di.Provided[T]() stubs.
A closure's dependencies are learned by watching it resolve them; a wired
constructor's are declared. What has been built has a recorded graph, what
was wired has a declared one, and three methods render them: Explain for
one service, Graph for everything built, and Modules for what each
module provides and needs.
Explain[T] prints the dependency tree of one service, with each node's
lifetime, scope, lifecycle phase and registration site, followed by what
needed it:
*main.Server: singleton in root, eager, started (provided at main.go:36)
├── *main.Repo: singleton in root, started (provided at main.go:34)
│ └── *main.DB: singleton in root, started (provided at main.go:33)
│ └── main.Config: value in root, started (provided at main.go:32)
└── *main.Cache: singleton in root, started (provided at main.go:35)
└── *main.DB: see above
*main.DB: singleton in root, started (provided at main.go:33)
└── main.Config: value in root, started (provided at main.go:32)
needed by: *main.Repo in root, *main.Cache in root
A dependency reached twice is expanded once, so a diamond is drawn as one. Registration sites are absolute paths, shortened above.
Graph renders everything built in a scope and its descendants as Graphviz
DOT, one cluster per scope:
go run ./examples/explain | dot -Tsvg > graph.svgModules is the report for whoever composes the application: which module
provides which keys, what each needs and which module serves it, what it
wraps, and which of its constructors are closures whose needs are unknown
until they run. A need only a resolving scope can provide is owed, as in
Validate. This is the guide application's report, before anything is
built:
config.Module
provides config.Config
storage.Module
provides *storage.db, storage.Store
needs config.Config ← config.Module
cache.Module
provides *cache.cache
wraps storage.Store ← storage.Module
mail.Module
provides *mail.Mailer
dihttp.Module
provides dihttp.Middleware
unchecked dihttp.Middleware (closures: needs known when they run)
api.Module
provides *api.Caller, *api.Users, *api.Health, *http.Server
needs *http.Request ← owed to a resolving scope
storage.Store ← cache.Module
*mail.Mailer ← mail.Module
config.Config ← config.Module
dihttp.Middleware ← dihttp.ModuleNone of the three builds anything. A service that has not been resolved is
shown with its registration and left alone. If it was registered with Wire,
its declared dependencies are drawn under it with dashed edges, each
continuing as a recorded tree where it has been built and as a declared one
where it has not, and declared by: names the unbuilt services that declare
it. A closure that has not run ends its branch: for closures the graph is
what ran, not what could run
(known limitation).
*main.Handler: scoped in root, not built (provided at main.go:35)
├╌╌ *main.Repo: singleton in root, not built (provided at main.go:33)
│ └╌╌ *main.DB: singleton in root, not built (provided at main.go:32)
│ └╌╌ main.Config: value in root, not built (provided at main.go:31)
└╌╌ *main.User: scoped in root, not built (provided at main.go:34)
└╌╌ *net/http.Request: not provided
examples/explain/main.go, the program that prints the first two trees in this section
// Inspecting the graph: what a service was built from, and what needed it.
//
// Dependencies are recorded as constructors resolve them, so Explain and
// Graph describe what actually happened rather than what was registered.
package main
import (
"context"
"fmt"
"log"
"github.com/floatdrop/di"
)
type Config struct{ DSN string }
type DB struct{ dsn string }
type Repo struct{ db *DB }
type Cache struct{ db *DB }
type Server struct {
repo *Repo
cache *Cache
}
func NewDB(cfg Config) *DB { return &DB{dsn: cfg.DSN} }
func NewRepo(db *DB) *Repo { return &Repo{db: db} }
func NewCache(db *DB) *Cache { return &Cache{db: db} }
func NewServer(repo *Repo, cache *Cache) *Server { return &Server{repo: repo, cache: cache} }
func main() {
app := di.New()
app.Value(Config{DSN: "postgres://localhost/app"})
app.Wire[*DB](NewDB)
app.Wire[*Repo](NewRepo)
app.Wire[*Cache](NewCache)
app.Wire[*Server](NewServer).Eager()
if err := app.Start(context.Background()); err != nil {
log.Fatal(err)
}
defer func() { _ = app.Stop(context.Background()) }()
// What the server was built from. *DB is reached through both the repo
// and the cache, and is expanded once.
fmt.Print(app.Explain[*Server]())
// And the other direction: what needed the database.
fmt.Println()
fmt.Print(app.Explain[*DB]())
// Everything built so far, as Graphviz DOT: dot -Tsvg > graph.svg
fmt.Println()
fmt.Print(app.Graph())
}app.Observe(func(ev di.Event) {
if ev.Kind == di.EventBuild {
buildDuration.WithLabelValues(ev.Service).Observe(ev.Duration.Seconds())
}
})Observers receive an Event for every constructor and every OnStart,
OnDrain and OnStop hook in the scope and its descendants, and one per
Shutdown. Each event names the service, its scope and module, the
registration site, the duration, and the error if any.
benchmarks/ is a separate module comparing this package with
samber/do on the same four-service graph, so
the library itself stays dependency-free. On an Apple M3 Max:
| Warm resolve | Cold register and build | |
|---|---|---|
di |
39 ns, 64 B, 2 allocs | 3.7 µs, 4.3 kB, 70 allocs |
do v2.1 |
130 ns, 192 B, 6 allocs | 6.5 µs, 11.5 kB, 120 allocs |
The cold figure counts the registration-site strings, so its byte total moves with how deep the source sits on disk; compare allocation counts across checkouts, not bytes.
cd benchmarks && go test -bench . -benchmemWhile the major version is 0, a minor bump may change behaviour. Every entry in CHANGELOG.md and on the releases page says whether an upgrade can break a caller.
There is one regression test per historical defect, and generative suites for the parts that proved easiest to get wrong: a property test over random registration sequences, a model-based test over random operation sequences checked against documented invariants and a lifecycle model, the same operations run in parallel lanes under the race detector, and fuzz targets over both.
go test -race ./...
go test -run '^$' -fuzz 'FuzzMachine$' -fuzztime 2m .
go test -race -run '^$' -fuzz FuzzMachineConcurrent -fuzztime 2m .The code blocks in this README are embedded from examples/ with
embedmd. After editing an example:
gofmt -w examples/ && go run github.com/campoy/embedmd@v1.0.0 -w README.md