di

package module
v0.14.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Sep 7, 2026 License: MIT Imports: 14 Imported by: 0

README

di

CI Go Reference

A dependency-injection container for Go 1.27+, built on generic methods. Register services with s.Provide(...) or hand over a plain constructor with s.Wire[T](NewT), and resolve them with s.Get[T](). No code generation, no dependencies, and reflection only where you ask for it: Wire reads a constructor's signature so that Validate can check the graph before anything is built.

The guide walks through one application top to bottom, a file at a time: how it is structured and what it looks like. How it works is the other direction: what happens between s.Get[T]() and a value, with diagrams.

app := di.New()
app.Provide(func(s *di.Scope) *DB { return s.Must(sql.Open("postgres", dsn)) }).
    OnStop(func(ctx context.Context, db *DB) error { return db.Close() })
app.Wire[*Repo](NewRepo) // func NewRepo(db *DB) *Repo

repo, err := app.Resolve[*Repo]()
  • Keys are Go types. No naming scheme, no string collisions between packages.
  • Constructors return T, not (T, error). A missing dependency or a failed constructor unwinds to the enclosing Resolve or Start as an error that names the full dependency path and the registration site.
  • Typed lifecycle. OnStart, OnDrain, OnStop and Worker hooks are typed on the service. Nothing is discovered by sniffing interfaces.
  • Deterministic shutdown. Reverse build order, child scopes first, every error reported.
  • Scopes for requests and tests. A child scope sees its parent and can shadow it.
  • Explicit overrides. Replacing a registration is Override(). A second registration without it is rejected, naming both, so one module cannot rewire another unnoticed. Composing over one is Wrap, which keeps what it wraps, hooks and lifetime included.
  • The graph is inspectable. Dependencies are recorded as constructors resolve them, so Explain[T] prints what a service was built from and what needed it, and Graph exports the whole thing as Graphviz DOT.
  • The graph can be checked before it is built. A constructor handed to Wire declares its dependencies by its parameters, and Validate walks them: a dependency nothing provides, a cycle, or a singleton that would build a request-scoped service in the wrong scope is reported without a constructor running.

Installation

go get github.com/floatdrop/di

Requires Go 1.27 or newer. Editor support for generic methods needs gopls v0.23 or newer.

Quick start

Every embedded example in this document is a compiled program under examples/, built in CI.

// 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)
}

Guide

Registration

Each registration returns a typed Binding[T]. Its methods refine 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 Wiring plain constructors.
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.

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 *Repo

The compiler checks that *Repo satisfies Reader, and the two keys share one instance because the constructor returns the same pointer. Declare it Scoped() as well when the target is. With Wire the constructor's result need only be assignable to the key, so app.Wire[Reader](NewRepo) serves the interface directly; a constructor that does not is rejected at registration.

Rules the container enforces:

  • A child scope may shadow a key its parent provides. Within one scope, a second registration of a key must be marked Override(); it then serves the key and inherits its eagerness. An unmarked duplicate, or an Override() with nothing to override, is rejected at the next resolution, naming the registrations involved. The marker is how a test substitutes a fake.
  • Once a key has served a value it can no longer be replaced, in the scope that owns it or in any scope that resolved through it. Replacing it would leave one key with two live values, so it panics instead. A resolution that failed built nothing and leaves the key re-registerable.
  • Combinations that cannot be honoured are rejected when the scope is first resolved, whatever order the methods were called in: Eager on a scoped binding, and Scoped on a Value.
Wiring plain constructors

Provide takes a closure, which pulls its dependencies with s.Get and can do anything else it likes; 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 reads its parameters as the dependencies. The build resolves each one exactly as the closure would have, from the same scope, so lifetimes, hooks, cycles and error paths are unchanged. What changes is that the dependencies are known at registration, and Validate can walk them with nothing built:

// 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, since that is where it is built. A Scoped binding is built in whichever scope resolves it, so it is checked as if resolved from the scope calling Validate, and what that 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, to have those checked.

Wire reads the signature with reflection once, at registration, and a constructor of the wrong shape is rejected there with the other configuration errors. The build calls it through reflect.Call, which costs about 150ns and two allocations per build over a closure; a warm Get is the same code for both. 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 conditional dependency, stays a Provide closure.

Wrapping a service

Wrap[T] composes over whatever serves T when it is called: the latest registration in this scope, or the one an ancestor provides. Its function takes the value being wrapped first and its other dependencies after it, and returns T or (T, error), read as Wire reads a constructor. The wrapped registration keeps its hooks and lifetime; it is built first, as the wrapper's dependency, and so stopped after it. Wrappers chain in registration order. This is what uber/fx calls Decorate.

// 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

Rules the container enforces:

  • A wrapper takes the lifetime of what it wraps: over a Scoped service it is one per resolving scope. Scoped() on the wrapper itself puts one wrapper per resolving scope around a shared singleton.
  • A wrapper in a child scope wraps the parent's value for that child and its descendants. The parent and its other children keep the original.
  • Nothing to wrap is rejected when Wrap is called, and a group cannot be wrapped: its members are read with All. A key this scope has already resolved cannot be wrapped afterwards, as it cannot be overridden, since callers hold the unwrapped value.
  • Override() after a wrapper replaces it and everything it wrapped. A registration some wrapper composes over cannot be overridden while that wrapper stands, in its scope or a descendant's.
Resolution
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
Scopes

A child scope resolves through its parent, reuses the parent's singletons, and owns the lifecycle of 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 must see child-scoped values is declared Scoped(): one instance per resolving scope, built there.

Groups

Group() makes a registration a member of the group for its type rather than the binding for it, and All resolves every member across the scope chain. A health endpoint is the usual case: 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 lifetime and hooks, and a plain registration of the same type is neither shadowed by the group nor part of it.

More than one instance of a type

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.

// 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 Wire for the selector rather than Provide. Its parameter declares the key, so Validate reports the key in Owed — the obligation the resolving scope carries — and Validate(di.Provided[T]()) discharges it as a request scope would. A Provide closure 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 = *DB is the same reflect.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 a Store is wanted. It is still a separate key: registering Cold does not serve Store.
Modules

A module is a function that registers into a scope. Use applies modules in order and attributes each registration to the module that made it:

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 first

Without the 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().

Privacy is Go's. Keys are types, so a service whose type is unexported can be named, and therefore resolved, overridden, shadowed or wrapped, only by the package that declares it. 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 and check the private node. What a test can override is exactly what is exported: the contract, or the configuration the private service is built from.

Lifecycle

Start builds every Eager binding, then runs OnStart hooks in build order. If a constructor or hook fails, Start stops the scope and returns both errors; that rolls back exactly the services that started, child scopes included. A service built after Start runs its OnStart as part of being built, so nothing is handed out unstarted.

Stop runs in three phases. It drains, then stops child scopes, then runs OnStop hooks in reverse build order, and returns every failure joined. A service is stopped only when its stop is owed: its OnStart succeeded, or it has no OnStart to pair with, in which case OnStop is a plain destructor. Afterwards the scope and everything under it refuses to resolve, with di.ErrStopped.

Stop is idempotent and safe to call concurrently: only the first call tears the scope down, and the others wait for it and report its result. A hook must therefore not call Stop on its own scope or an ancestor, which would be a wait on itself; call Shutdown, which never blocks.

OnStart should return once the service is ready rather than run it: a server binds its listener in the hook, so a busy port fails Start, and serves in a goroutine.

Draining

OnDrain runs before anything is stopped, from the innermost scope outwards and in reverse build order, while every scope still resolves normally. It is where a service stops taking new work and waits for the work it already 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, so shutting the server down from OnStop would race the teardown of the scopes those handlers still use, and a request in flight would fail with di.ErrStopped before the server finished waiting for it. Draining first keeps handlers' scopes and dependencies alive until they return.

Workers

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 runs in its own goroutine from the moment the service starts. Its context is cancelled when the service stops, and Stop waits for it within the stop deadline. Returning an error calls Shutdown, so a worker that dies takes the application down rather than leaving it half alive, even if it reports the failure only once shutdown is under way. The one error that does not count is context.Canceled after cancellation, which means only that the worker stopped.

Request scopes

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's Use accepts it too. dihttp.Module registers one, so a server's constructor takes it as a dependency like any other:

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. The type is declared Scoped() when it needs the request and once for the application when it does not, and Handle follows either. A handler written by hand reaches the scope with di.FromContext(r.Context()). Outside the container, dihttp.NewMiddleware(app) makes a middleware directly.

Services that depend on the request are declared once, in the root, as Scoped(); they are built per request, cached 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 if you are not using net/http. A complete service with a worker, a health endpoint, request scopes and graceful shutdown is in examples/app.

Graceful shutdown

Run is the main-function helper. It starts the scope, blocks until the context is cancelled, SIGINT or SIGTERM arrives, or Shutdown is called, then stops everything with a bounded context. A second signal during the stop cancels that context, so a hung hook cannot keep the process alive.

// 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)
	}
}
Observability
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, plus one per Shutdown. Each event names the service, its scope, its module if it has one, the registration site, the duration and the error, if any.

Inspecting the graph

A Provide closure's dependencies are learned by watching it resolve them, and a Wire 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] is the dependency tree of one service, with each node's lifetime, its scope, how far through its lifecycle it is and where it was registered, 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; they are 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.svg

Modules is the report for the person composing an 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 known only when they run. A need only a resolving scope can provide is owed, as Validate reports it. This is the guide application's, 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.Module

None of the three builds anything. A service that has not been resolved is reported with its registration and left alone; if it was registered with Wire, the dependencies it declares 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, which is the other half of the known limitation: for closures the graph is what ran, not what could run.

*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
// 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())
}
Testing your application

di.Test wires the production graph into a fresh scope and stops it when the test ends, failing the test if a stop hook errors. Override what you need before anything is resolved, marking 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)
	}
}

Performance

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 . -benchmem

Versioning

While 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.

Contributing

Alongside one regression test per historical defect, generative suites guard 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

License

MIT

Documentation

Overview

Package di is a dependency-injection container for Go 1.27+ built on generic methods.

Services are registered on a Scope and resolved from it by type:

app := di.New()
app.Value(Config{DSN: "postgres://localhost/app"})
app.Provide(func(s *di.Scope) *DB { return s.Must(sql.Open("postgres", s.Get[Config]().DSN)) }).
	OnStop(func(ctx context.Context, db *DB) error { return db.Close() })
app.Provide(func(s *di.Scope) *Repo { return &Repo{db: s.Get[*DB]()} })

repo, err := app.Resolve[*Repo]()

Keys are Go types, so there is no naming scheme and no collisions between packages. Constructors return T rather than (T, error): inside a constructor, Scope.Get and Scope.Must abort on failure and the error surfaces from the enclosing Scope.Resolve, Scope.Start or Scope.Run with the dependency path and the registration site.

Lifetimes

A binding is a singleton by default, cached in the scope that registered it. Binding.Scoped makes it one instance per resolving scope, built there so it can see that scope's values, which is how request-scoped services are declared once in the root. Binding.Group and Scope.All handle groups, and Scope.Maybe resolves optional dependencies. An interface is served by a constructor that returns the implementation: s.Provide(func(s *Scope) Reader { return s.Get[*Repo]() }).

Scopes

Scope.Child creates a scope that resolves through its parent, reuses the parent's singletons and owns the lifecycle of what it builds. A child may shadow a key its parent provides; within one scope, a second registration of a key must be marked Binding.Override, or the next resolution rejects it naming both sites. That marker is the test seam: wire the production graph into a fresh scope, then override what you want faked before anything is resolved (Test does the bookkeeping). For HTTP, github.com/floatdrop/di/dihttp.Middleware gives each request a child scope holding the *http.Request, reachable through FromContext.

Modules

A Module is a function that registers into a scope, and Scope.Use applies modules in order. Registrations are attributed to the module that made them, so a collision between two modules is reported as one: "*app.DB is provided at storage (wire.go:12) and again at caching (cache.go:8)".

Lifecycle

Binding.OnStart, Binding.OnDrain and Binding.OnStop are typed hooks. Scope.Start builds Binding.Eager bindings and runs start hooks in build order, rolling back on failure; services built later start as part of being built. Scope.Stop first drains, which lets work already in flight finish while the scope still resolves, then stops child scopes, then services in reverse build order, and afterwards the scope refuses to resolve anything. Binding.Worker runs a long-lived function that is cancelled on stop. Scope.Run ties it together for a main function: start, wait for a signal or Scope.Shutdown, stop with a deadline. Scope.Observe reports every step for logging and metrics.

Inspecting the graph

A constructor's dependencies are recorded as it resolves them, so the graph is known for whatever has been built. Scope.Explain renders one service's dependency tree, with the registration site, lifetime and scope of each node, and what needed it. Scope.Graph renders everything built in a scope and its descendants as Graphviz DOT.

Concurrency

A Scope is safe to use from many goroutines, including from goroutines a constructor starts for itself: the resolution path is immutable, so branches that run in parallel share nothing. A constructor may also keep the Scope it was handed and resolve through it later, once its own service is built; the finished part of that path is no longer a dependency, so such a resolution is not a cycle. A service is built once however many resolutions race for it, and a resolution of a running scope returns only a service whose start step has finished. A cycle is reported as ErrCycle even when the two halves are being built concurrently.

Three re-entrancy limits apply. A goroutine started by a constructor must use Scope.Resolve rather than Scope.Get, because Get reports failure by panicking and that panic cannot unwind to the enclosing call from another goroutine. An Binding.OnStart hook must not resolve a service that depends on the one being started: the hook already holds the value, and waiting for itself cannot make progress. And no hook may call Scope.Stop on its own scope or an ancestor, because Stop waits for the very step the hook is running; call Scope.Shutdown, which never blocks.

Example
package main

import (
	"context"
	"fmt"

	"github.com/floatdrop/di"
)

type Server struct{ repo *Repo }

func main() {
	app := di.New()
	app.Value(Config{DSN: "pg://primary"})
	app.Provide(func(s *di.Scope) *DB { return &DB{dsn: s.Get[Config]().DSN} }).
		OnStop(func(ctx context.Context, db *DB) error { fmt.Println("close", db.dsn); return nil })
	app.Provide(func(s *di.Scope) *Repo { return &Repo{db: s.Get[*DB]()} })
	app.Provide(func(s *di.Scope) Reader { return s.Get[*Repo]() })
	app.Provide(func(s *di.Scope) *Server { return &Server{repo: s.Get[*Repo]()} }).Eager().
		OnStart(func(context.Context, *Server) error { fmt.Println("listening"); return nil }).
		OnStop(func(context.Context, *Server) error { fmt.Println("stopped"); return nil })

	if err := app.Start(context.Background()); err != nil {
		panic(err)
	}
	fmt.Println("read:", app.Get[Reader]().Read())
	_ = app.Stop(context.Background())
}
Output:
listening
read: pg://primary
stopped
close pg://primary

Index

Examples

Constants

This section is empty.

Variables

View Source
var (
	ErrNotProvided = errors.New("not provided")
	ErrCycle       = errors.New("dependency cycle")
	ErrStopped     = errors.New("scope stopped")
)

Functions

func WithScope

func WithScope(ctx context.Context, s *Scope) context.Context

WithScope attaches s to ctx so handlers and their callees can reach it with FromContext.

Types

type Binding

type Binding[T any] struct {
	// contains filtered or unexported fields
}

Binding is the typed handle returned by Provide/Value. Its methods refine the registration; they must be called before the first resolution from this scope.

func (Binding[T]) Eager

func (b Binding[T]) Eager() Binding[T]

Eager builds the service during Start rather than on first use.

Eagerness belongs to the key, not the registration: it means the service exists by the time Start returns. Overriding an eager binding therefore keeps the key eager and builds the replacement; a replacement with a per-scope lifetime, which cannot be built once at Start, is rejected.

func (Binding[T]) Group added in v0.7.0

func (b Binding[T]) Group() Binding[T]

Group makes the binding a member of the multi-binding group for T instead of the binding for T: it neither shadows nor is shadowed by another registration of T, and the members are read back together with s.All[T](). A member keeps its own lifetime and hooks.

func (Binding[T]) OnDrain added in v0.4.0

func (b Binding[T]) OnDrain(f func(context.Context, T) error) Binding[T]

OnDrain runs before anything is stopped: Stop drains the whole tree, from the innermost scope outwards and in reverse build order, while every scope still resolves normally. It is where a service stops accepting new work and waits for the work it already has, such as an HTTP server that must finish in-flight requests whose handlers still need their request scope. Anything those handlers build, including a request scope of their own, is drained before the phase ends. Use OnStop for the release that follows.

func (Binding[T]) OnStart

func (b Binding[T]) OnStart(f func(context.Context, T) error) Binding[T]

Typed lifecycle hooks: no interface sniffing, no reflection.

OnStart runs once the service is built, and only a hook that returns normally starts it: one that panics fails the start step, like a panicking constructor, and the service is never served.

func (Binding[T]) OnStop

func (b Binding[T]) OnStop(f func(context.Context, T) error) Binding[T]

func (Binding[T]) Override added in v0.8.0

func (b Binding[T]) Override() Binding[T]

Override declares that this registration replaces an earlier one of the same key in the same scope. Without it a second registration of a key is rejected at the next resolution, naming both sites, because a duplicate that wins silently is how one module reroutes another module's wiring without anyone noticing. With it the later registration serves the key, and inherits its eagerness, which is the test seam:

s := di.Test(t, app.Production)
s.Value(&DB{DSN: "sqlite://memory"}).Override()

There must be something to override in this scope, or that is rejected too: a fake for a service that has since been renamed would otherwise be a registration nobody resolves, and the test would pass against production wiring. A child scope shadows its parent without Override, since that is a different registry rather than a replacement. A key that has already served a value cannot be overridden at all.

func (Binding[T]) Scoped

func (b Binding[T]) Scoped() Binding[T]

Scoped makes the binding one-per-scope: each scope that resolves it gets its own instance, built in that scope (so it can see that scope's values) and stopped with it. Declare request-scoped services once in the root and resolve them through the request scope.

Example
package main

import (
	"context"
	"fmt"

	"github.com/floatdrop/di"
)

type Session struct{ ID string }

func main() {
	app := di.New()
	// Declared once in the root, built once per scope that resolves it.
	app.Provide(func(s *di.Scope) *Session { return &Session{ID: s.Get[string]()} }).Scoped()

	for _, id := range []string{"a1", "b2"} {
		req := app.Child("request")
		req.Value(id)
		first, again := req.Get[*Session](), req.Get[*Session]()
		fmt.Println(first.ID, first == again)
		_ = req.Stop(context.Background())
	}
}
Output:
a1 true
b2 true

func (Binding[T]) Worker added in v0.7.0

func (b Binding[T]) Worker(f func(context.Context, T) error) Binding[T]

Worker registers a long-running function for T, such as a consumer loop. It is started in its own goroutine once the service starts and its context is cancelled when the service stops; Stop waits for it to return, bounded by its own context. A hook that outlasts that deadline is reported by Stop, and OnStop then waits for it rather than releasing the value underneath a worker still reading it.

Returning a non-nil error calls Shutdown with it, stopping the application, even if the scope was already stopping: a worker may fail, flush while the scope winds down, and only then report. The exception is context.Canceled from a hook that was already cancelled, which is a worker reporting the cancellation and nothing else. A hook that wants to stay quiet during shutdown should return nil.

Example
package main

import (
	"context"
	"fmt"

	"github.com/floatdrop/di"
)

type Queue struct{ jobs chan string }

func main() {
	app := di.New()
	done := make(chan string, 1)
	app.Provide(func(*di.Scope) *Queue { return &Queue{jobs: make(chan string, 1)} }).Eager().
		Worker(func(ctx context.Context, q *Queue) error {
			for {
				select {
				case job := <-q.jobs:
					done <- "processed " + job
				case <-ctx.Done():
					return nil // cancelled by Stop
				}
			}
		})

	ctx := context.Background()
	if err := app.Start(ctx); err != nil {
		panic(err)
	}
	app.Get[*Queue]().jobs <- "email"
	fmt.Println(<-done)
	fmt.Println("stop:", app.Stop(ctx)) // waits for the worker to return
}
Output:
processed email
stop: <nil>

type Event

type Event struct {
	Kind     EventKind
	Service  string // the service, e.g. "*github.com/acme/app.DB"; empty for shutdown
	Scope    string // name of the scope that owns the instance
	Site     string // file:line of the registration; empty for shutdown
	Module   string // the Module the service was registered from; empty when none
	Duration time.Duration
	Err      error
}

Event describes one lifecycle step. Observers receive it after the step completes, with its duration and error if any.

type EventKind

type EventKind string

EventKind classifies an Event.

const (
	EventBuild    EventKind = "build"    // a constructor ran
	EventStart    EventKind = "start"    // an OnStart hook ran
	EventDrain    EventKind = "drain"    // an OnDrain hook ran
	EventStop     EventKind = "stop"     // a Worker hook was cancelled and/or an OnStop hook ran
	EventShutdown EventKind = "shutdown" // Shutdown was called
)

type Module added in v0.8.0

type Module func(*Scope)

A Module is a unit of wiring: a function that registers into a scope. Modules compose by ordinary function composition, and Scope.Use applies them in order. Every registration a module makes is attributed to it, so a collision between two modules is reported as one.

type RunOption

type RunOption func(*runConfig)

RunOption configures Run.

func StopTimeout

func StopTimeout(d time.Duration) RunOption

StopTimeout bounds how long Stop may take once Run decides to exit. The default is 15 seconds.

type Scope

type Scope struct {
	// contains filtered or unexported fields
}

Scope is a container. A Scope value handed to a constructor is a view over the same state that carries the current resolution path.

func FromContext

func FromContext(ctx context.Context) (*Scope, bool)

FromContext returns the scope attached with WithScope, if any.

func New

func New() *Scope

func Test added in v0.2.0

func Test(tb TB, wire ...Module) *Scope

Test returns a scope for a test: the modules register the graph under test, and the scope is stopped when the test ends, failing it if a stop hook errors. Override what you need faked after wiring and before resolving, saying so:

s := di.Test(t, app.Production)
s.Value(&DB{DSN: "sqlite://memory"}).Override()
repo := s.Get[*Repo]()

func (*Scope) All

func (s *Scope) All[T any]() []T

All resolves the multi-binding group for T across the scope chain. Members are singletons (or Scoped if so marked) with the same lifecycle as any other binding.

func (*Scope) Child

func (s *Scope) Child(name string) *Scope

Child creates a scope that resolves through s. Stopping s stops its children first.

A child made inside a constructor carries that constructor's resolution path, so a cycle through it is reported rather than deadlocking. The path goes inert when the constructor returns (see resolver.done), so a child kept for later, such as a request scope, resolves as an independent branch.

func (*Scope) Context

func (s *Scope) Context() context.Context

Context returns the context passed to Start (or Run) on this scope or the nearest started ancestor, so constructors can dial with a deadline. Before Start it returns context.Background().

func (*Scope) Explain added in v0.8.0

func (s *Scope) Explain[T any]() string

Explain renders what T resolves to and what it was built from: the dependency tree, each node with its lifetime, its scope, the state of its lifecycle and where it was registered, followed by what needed it.

What has been built has a recorded tree, because a constructor's dependencies are recorded as it resolves them. A service that has not been built is reported as such, with its registration; if it was registered with Wire, the dependencies it declares 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" lists the unbuilt services that declare it. A closure that has not run ends its branch, since nothing is known about it yet, and so does a key nothing provides. Explain resolves nothing and builds nothing; it commits pending registrations the way a resolution from this scope would, so a configuration this scope would reject is reported here by the same panic.

A key served by a group is explained member by member. A dependency reached twice, as in a diamond, is expanded once and named on later visits, so the tree stays finite and the repeat is visibly the same instance.

func (*Scope) Get

func (s *Scope) Get[T any]() T

Get resolves T. Inside a constructor, failure unwinds to the enclosing Resolve/Start call and becomes an error; at top level it panics. In a goroutine a constructor started, use Resolve instead: that panic has no enclosing call to unwind to and would take the process down.

func (*Scope) Graph added in v0.8.0

func (s *Scope) Graph() string

Graph renders everything built in this scope and its descendants as Graphviz DOT: one box per instance, one cluster per scope that holds any, and an arrow from each instance to what its constructor resolved.

It reads the graph and changes nothing, not even the pending registrations, so it is safe to call from a handler or a hook. Nodes are numbered in the order the scopes were created and the instances were built, so the same run of the same program renders the same document. A scope that has been stopped no longer holds its instances and contributes nothing.

The detail is deliberately thin -- a registration site would not fit in a box. Use Explain for one service in full.

func (*Scope) Maybe

func (s *Scope) Maybe[T any]() (T, bool)

Maybe resolves T if it is provided anywhere in the scope chain.

func (*Scope) Modules added in v0.14.0

func (s *Scope) Modules() string

Modules renders the modules registered into this scope and its ancestors: what each provides, what it 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, as a request scope provides the request, is reported as owed, the way Validate reports it. A dependency a module serves for itself is not a module dependency and is left out. Registrations made outside any module are grouped as "registered directly".

Like Explain, it builds nothing and commits pending registrations the way a resolution would, so a configuration this scope would reject is reported by the same panic.

func (*Scope) Must

func (s *Scope) Must[T any](v T, err error) T

Must unwraps a (value, error) pair inside a constructor:

db := s.Must(sql.Open("postgres", dsn))

A non-nil error aborts the constructor and surfaces from the enclosing Resolve, Start or Run. Outside a constructor it panics with the error.

func (*Scope) Observe

func (s *Scope) Observe(fn func(Event))

Observe registers fn to receive lifecycle events from this scope and every scope under it. Use it for logging and metrics.

Example
package main

import (
	"context"
	"errors"
	"fmt"

	"github.com/floatdrop/di"
)

type Cache struct{}

func main() {
	app := di.New()
	app.Observe(func(ev di.Event) {
		fmt.Println(ev.Kind, ev.Service, ev.Err)
	})
	app.Provide(func(*di.Scope) *Cache { return &Cache{} }).
		OnStop(func(context.Context, *Cache) error { return errors.New("flush failed") })

	app.Get[*Cache]()
	_ = app.Stop(context.Background())
}
Output:
build *github.com/floatdrop/di_test.Cache <nil>
stop *github.com/floatdrop/di_test.Cache di: stopping *github.com/floatdrop/di_test.Cache: flush failed

func (*Scope) Provide

func (s *Scope) Provide[T any](ctor func(*Scope) T) Binding[T]

Provide registers a lazily built singleton. T is inferred from the constructor's return type; dependencies are pulled with s.Get[...]().

func (*Scope) Resolve

func (s *Scope) Resolve[T any]() (v T, err error)

Resolve is the error-returning entry point.

func (*Scope) Run

func (s *Scope) Run(ctx context.Context, opts ...RunOption) error

Run starts the scope and blocks until ctx is cancelled, a termination signal arrives, or Shutdown is called. It then stops the scope with a bounded context; a second signal during the stop cancels that context so a hung hook cannot keep the process alive. Run returns the Start error, the error passed to Shutdown, and any Stop errors, joined. A worker that died on its own is reported once, whether it reached Run as the cause or as a Stop error.

func (*Scope) Shutdown

func (s *Scope) Shutdown(cause error)

Shutdown asks a running Run to stop and records the cause it should return. It never blocks, may be called from any goroutine, and the first call wins. It propagates to ancestor scopes, so a service in a child scope can stop the application.

func (*Scope) Start

func (s *Scope) Start(ctx context.Context) error

Start builds every Eager binding in registration order, then runs the start step of everything built so far, in build order. If a constructor or a start step fails, the scope is stopped, which rolls back exactly the services that did start, child scopes included. A service that was built but never started is not stopped, so acquire resources in OnStart rather than in the constructor when the binding declares one.

After Start returns, a service built later runs its start step as part of being built, so lazily resolved services start too. Start may be called once, and builds only this scope's own Eager bindings: a child scope's are built by that child's Start.

func (*Scope) Stop

func (s *Scope) Stop(ctx context.Context) error

Stop winds the scope down in three phases. First it drains: OnDrain hooks run from the innermost scope outwards, in reverse build order, while every scope still resolves, so work already in flight can finish and still reach its dependencies. A service or child scope that phase brings into being is drained too, before anything is marked stopped. Then the scope is marked stopped and child scopes are stopped. Then OnStop hooks run in reverse build order (dependents first).

A service is stopped only if it started, or if it declares no OnStart, in which case OnStop is a plain destructor. Every failure is reported.

Stop is synchronous. It waits out whatever another goroutine is still running for a service it is tearing down -- a start step in flight, a drain hook another Stop began, a Worker hook being cancelled -- so when it returns, the teardown has happened and its failures are in the error. A teardown outlives the call in one case, when ctx expires first: the missed deadline is reported here, and the release is finished once the outstanding step returns, on a goroutine of its own, reaching observers rather than this caller. A constructor that finishes after the scope has stopped is likewise undone by whichever goroutine was resolving it.

Afterwards the scope and its descendants refuse to resolve anything, with ErrStopped; that includes a resolution that was already waiting when the scope stopped, so a closed service is never handed out. Stopping a child scope also detaches it from its parent, so per-request scopes are released once stopped.

Stop is idempotent, and concurrent calls are safe: only the first tears the scope down, and the others wait for it and report its result, bounded by their own context. Two Stop calls that meet at one scope, as a child and its parent do, wait for each other phase by phase, so neither starts releasing what the other's hooks are still using.

Because Stop waits, a hook must not call Stop on its own scope or an ancestor: it would be waiting for the step it is itself running. Stopping a sibling, or a scope below the hook's own, is allowed. A hook that passes on the context it was given gets an error saying so; one that passes a context of its own is not recognised, and waits until that context expires. Call Shutdown, which never blocks.

func (*Scope) Use added in v0.8.0

func (s *Scope) Use(mods ...Module)

Use applies each module to this scope. A registration made while a module runs -- directly, or from a child the module opens, or later from a constructor the module registered -- carries that module's name, which is the name of the function: register modules as named functions rather than closures, or the name is the enclosing function's.

func (*Scope) Validate added in v0.9.0

func (s *Scope) Validate(stubs ...Stub) Validation

Validate checks the wiring visible from this scope without building anything. A singleton is checked against the scope that registered it, since that is where it is built. A Scoped binding is checked as if resolved from this scope, and what this scope does not provide for it is reported as Owed rather than as an error, because a descendant may:

v := app.Validate() // *http.Request is owed to a request scope

The stubs say what such a descendant will hold, so that the check can be made from the application scope as that descendant would make it. With stubs the caller has described the resolving scope, and a dependency neither this scope nor the stubs provide is an error:

err := app.Validate(di.Provided[*http.Request]()).Err()

Like Explain, Validate commits pending registrations the way a resolution would, so a configuration this scope would reject is reported by the same panic.

func (*Scope) Value

func (s *Scope) Value[T any](v T) Binding[T]

Value registers an already-built instance.

func (*Scope) Wire added in v0.9.0

func (s *Scope) Wire[T any](ctor any) Binding[T]

Wire registers a lazily built singleton from a constructor of any arity, whose parameters are its dependencies:

s.Wire[*Server](NewServer) // func NewServer(cfg Config, repo *Repo) *Server

ctor must be a non-variadic function returning T, or T and an error, and is read with reflection once, here. Each parameter type is resolved from the same scope view a Provide closure would see, so lifetimes, cycles, hooks and error paths are unchanged; what Wire adds is that the dependencies are known at registration, before anything is built. A non-nil error from ctor aborts the build exactly as s.Must does.

T cannot be inferred from an untyped argument, so it is spelled out, and a constructor whose result is not assignable to T is rejected here, with the other configuration errors. A concrete constructor may therefore serve an interface key directly: s.Wire[Repository](NewPGRepo). The build calls ctor through reflect, which costs about 150ns and two allocations per build over a Provide closure; a warm Get is the same code for both.

func (*Scope) Wrap added in v0.10.0

func (s *Scope) Wrap[T any](fn any) Binding[T]

Wrap registers a wrapper over the registration that serves T when Wrap is called: the latest one in this scope, or the one an ancestor provides. fn takes the value being wrapped first and its other dependencies after it, read with reflection as Wire reads a constructor, and returns T, or T and an error:

s.Wrap[Store](func(next Store, c *Cache) Store { return &caching{next, c} })

What is wrapped keeps its registration, hooks and lifetime: it is built first, as the wrapper's dependency, and so stopped after it. The wrapper serves T from this scope down. In a child scope it wraps the parent's value for that child and its descendants and leaves the parent and its other children as they were, which is what uber/fx calls Decorate. Wrappers chain in registration order, and a wrapper takes the lifetime of what it wraps; Scoped() on the wrapper makes it one per resolving scope over a shared inner value. An Override registered afterwards replaces the wrapper and everything it wrapped. Nothing to wrap is rejected here, and a group cannot be wrapped: its members are read with All. A key this scope has already resolved is rejected at the next resolution, as an Override is, since callers already hold the unwrapped value.

type Stub added in v0.11.0

type Stub struct {
	// contains filtered or unexported fields
}

Stub names a key the scope resolving a Scoped binding will provide, for Validate to take as given. Make one with Provided.

func Provided added in v0.11.0

func Provided[T any]() Stub

Provided is a Stub for T: the resolving scope will hold a T, as a request scope holds an *http.Request.

type TB added in v0.2.0

type TB interface {
	Helper()
	Cleanup(func())
	Errorf(format string, args ...any)
}

TB is the subset of testing.TB that Test needs.

type Validation added in v0.9.0

type Validation struct {
	// Errors are the failures the declared graph proves: a dependency nothing
	// provides, a cycle among Wire constructors, or a singleton that would
	// build a Scoped service in its own scope, where that service's
	// dependencies are not provided. Each wraps ErrNotProvided or ErrCycle.
	Errors []error
	// Owed lists the dependencies of Scoped bindings that this scope does not
	// provide. A Scoped service is built in the scope that resolves it, so
	// these are left to that scope: call Validate from there, or say what it
	// will hold with Provided stubs, and they are checked as errors instead.
	Owed []string
	// Unchecked lists the Provide constructors in the chain, whose
	// dependencies are known only once they run.
	Unchecked []string
}

Validation is what Validate found.

func (Validation) Err added in v0.9.0

func (v Validation) Err() error

Err joins Errors, or is nil when the declared graph proves no failure.

Directories

Path Synopsis
Package dihttp connects a di.Scope to net/http.
Package dihttp connects a di.Scope to net/http.
examples
app command
A complete service: request scopes through middleware, a background worker with a Worker hook, a health endpoint, and graceful shutdown.
A complete service: request scopes through middleware, a background worker with a Worker hook, a health endpoint, and graceful shutdown.
explain command
Inspecting the graph: what a service was built from, and what needed it.
Inspecting the graph: what a service was built from, and what needed it.
guide/cmd/api command
Command api is the application: it composes the modules, checks the graph, and runs until a signal arrives.
Command api is the application: it composes the modules, checks the graph, and runs until a signal arrives.
guide/internal/api
Package api serves HTTP.
Package api serves HTTP.
guide/internal/cache
Package cache puts a cache in front of the store.
Package cache puts a cache in front of the store.
guide/internal/config
Package config reads the settings the application starts with.
Package config reads the settings the application starts with.
guide/internal/mail
Package mail sends messages from a background worker.
Package mail sends messages from a background worker.
guide/internal/storage
Package storage owns the database connection and the store built on it.
Package storage owns the database connection and the store built on it.
instances command
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.
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.
quickstart command
Quick start: register a few services, start and stop the application.
Quick start: register a few services, start and stop the application.
scopes command
Scopes: a child scope sees everything in its parent and can shadow it.
Scopes: a child scope sees everything in its parent and can shadow it.
server command
Graceful shutdown of an HTTP server.
Graceful shutdown of an HTTP server.
testing
Package app shows how to substitute dependencies in tests: wire the production graph into a fresh scope, then override before resolving.
Package app shows how to substitute dependencies in tests: wire the production graph into a fresh scope, then override before resolving.
wire command
Wire: plain constructors whose parameters are their dependencies, and a graph that is checked before anything is built.
Wire: plain constructors whose parameters are their dependencies, and a graph that is checked before anything is built.
wrap command
Wrap: compose over a service without replacing it.
Wrap: compose over a service without replacing it.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL