di

package module
v0.15.0 Latest Latest
Warning

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

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

README

di

CI Go Reference

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 once

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

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

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

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.

Registering and resolving

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); see 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 *Repo

The 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 an Override() 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.
  • Eager on a Scoped binding, and Scoped on a Value, are rejected whichever order the methods were called in.
Optional dependencies

There is no optional marker on a parameter. A key is provided or it is not, and a dependency nothing provides is an error — so a service that can do without something says so by providing the absence instead.

The first way is a nil default. s.Value[*Cache](nil) provides the key with nothing in it, and *Cache stays an ordinary declared dependency: Validate checks the edge, Explain draws it, and a deployment that has a cache overrides it. The constructor takes the parameter as it takes any other and handles the nil.

For an interface, a null object goes further: provide an implementation that does nothing, and nothing downstream has a branch to write at all. Either way the absence is a registration with a call site, which is what Explain and Modules report, and the key really is provided — so the rules above still hold. An Override() before anything resolves is how the real one gets in, and once the nil has been served a later registration of *Cache in that scope is rejected, so the two halves of the program cannot end up disagreeing about whether there is a cache.

Branching on presence is the third way, and it is for a key nothing registers at all. s.Maybe[T]() returns (T, bool), where false means no scope in the chain provides T, and only a Provide closure can ask:

examples/optional/main.go, the program that prints the output below
// Optional dependencies: a key that a deployment may or may not have. A nil
// default and a null object keep every declared edge provided, so the graph
// still checks; Maybe answers presence when a constructor has to branch on a
// key nothing registered at all.
package main

import (
	"fmt"

	"github.com/floatdrop/di"
)

type Cache struct{ addr string }
type Tracer struct{}
type Store struct{ cache *Cache }
type Report struct{ line string }

type Metrics interface{ Count(string) }

type nopMetrics struct{}
type logMetrics struct{}

func (nopMetrics) Count(string)   {}
func (logMetrics) Count(n string) { fmt.Println("count:", n) }

// The constructors know nothing about di. A dependency that may be absent is
// a parameter like any other, and the nil is the absence.
func NewCache() *Cache          { return &Cache{addr: "localhost:6379"} }
func NewNopMetrics() nopMetrics { return nopMetrics{} }
func NewLogMetrics() logMetrics { return logMetrics{} }

func NewStore(c *Cache, m Metrics) *Store {
	m.Count("store.built")
	return &Store{cache: c}
}

// Something has to handle the absence, and this is where it happens.
func (s *Store) Get(key string) string {
	if s.cache == nil {
		return "db:" + key
	}
	return "cache:" + key
}

// A closure is what can ask whether a key is registered at all: Maybe is
// (T, bool), and false means nothing in this scope or above provides it.
func NewReport(s *di.Scope) *Report {
	line := s.Get[*Store]().Get("1")
	if _, ok := s.Maybe[*Tracer](); ok {
		line = "traced(" + line + ")"
	}
	return &Report{line: line}
}

// Base is the wiring every deployment shares. *Cache is provided as nil and
// Metrics as a null object, so *Store has no unprovided dependency and needs
// no di import to say it can do without them.
func Base(s *di.Scope) {
	s.Value[*Cache](nil)
	s.Wire[Metrics](NewNopMetrics)
	s.Wire[*Store](NewStore)
	s.Provide(NewReport)
}

func main() {
	plain := di.New()
	plain.Use(Base)

	// Every declared edge is provided, so there is nothing to report. The
	// closure is unchecked, which is what asking with Maybe costs.
	v := plain.Validate()
	fmt.Println("errors:   ", v.Err())
	fmt.Println("unchecked:", v.Unchecked)
	fmt.Println("plain:    ", plain.Get[*Report]().line)

	// A deployment that has a cache and a tracer registers them. The
	// defaults are overridden, and nothing that depends on them changes.
	full := di.New()
	full.Use(Base)
	full.Wire[*Cache](NewCache).Override()
	full.Wire[Metrics](NewLogMetrics).Override()
	full.Value(&Tracer{}) // a new key in this scope, so no marker is needed
	fmt.Println("full:     ", full.Get[*Report]().line)
	fmt.Print(full.Explain[*Store]())
}
errors:    <nil>
unchecked: [*main.Report (provided at main.Base (main.go:62))]
plain:     db:1
count: store.built
full:      traced(cache:1)
*main.Store: singleton in root, built (provided at main.go:61)
├── *main.Cache: singleton in root, built (provided at main.go:80)
└── main.Metrics: singleton in root, built (provided at main.go:81)
needed by: *main.Report in root

Three things to know:

  • A nil default makes the key present, so Maybe[*Cache]() reports it as provided — with a nil value. The two are answers to different questions: whether anything provides the key, and whether there is anything in it.
  • Maybe records nothing when the key is absent, so a registration of it afterwards is not rejected, and a constructor that already ran will not see it. A nil default is the better answer whenever the key can be named up front.
  • Asking with Maybe needs a closure, and a closure's dependencies are known only once it runs, so it is listed as unchecked by Validate instead of checked.

Without the default, the same graph fails as any missing dependency does. Validate says so with nothing built, because *Store was wired, and the resolution says so again with its path:

di: *main.Cache: not provided (needed by [*main.Store], provided at main.Base (main.go:61))
di: building *main.Report (...): di: building *main.Store (...): di: *main.Cache: not provided (needed by [*main.Report *main.Store])

A pointer parameter is not treated as optional on its own. Nearly every dependency in Go is a pointer or an interface, so that rule would make almost every wiring mistake a nil dereference inside a constructor rather than an error naming the resolution path, and would leave Validate with nothing to prove. Coming from fx or dig, this is the optional:"true" tag on a dig.In field; there are no parameter objects here, so the absence is registered rather than tagged.

Lifecycle

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.

Draining

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.

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 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 and Shutdown

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

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.

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

Groups

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.

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.

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 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.
Composing modules

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 first

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

Wrapping a service

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 Scoped service 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.
Testing

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)
	}
}
Checking and inspecting
Validate

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.

Explain, Graph and Modules

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

Modules 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.Module

None 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())
}
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, and one per Shutdown. Each event names the service and the import path of its type, its scope and module, the registration site, the duration, and the error if any.

For logging, dislog is that function already written against log/slog:

app.Observe(dislog.New(slog.Default()))

The event's kind is the message and the rest are attributes. A step that failed is logged at slog.LevelError with the error and the registration site, since that is what a failure is read with; anything else at slog.LevelInfo, or at the level dislog.Level sets — slog.LevelDebug is the usual second choice, because every build is worth a line while an application is being wired and noise once it works. dislog.Site() logs the site every time. The package imports nothing beyond log/slog, so any handler will do, including one that colours its output:

examples/observe/main.go, the program that prints the output below
// Observe: the container's lifecycle as log lines. dislog.New turns a
// *slog.Logger into the observer Observe takes, so the application says what
// it is doing as it builds, starts and stops.
package main

import (
	"context"
	"log/slog"
	"os"

	charm "github.com/charmbracelet/log"
	"github.com/floatdrop/di"
	"github.com/floatdrop/di/dislog"
	"github.com/floatdrop/di/examples/observe/internal/store"
)

type Repo struct{ db *store.DB }

func NewRepo(db *store.DB) *Repo { return &Repo{db} }

func main() {
	// dislog imports only log/slog, so any handler will do. This one is
	// charmbracelet/log, which is an slog handler that colours its output.
	logger := slog.New(charm.New(os.Stderr))

	app := di.New()

	// Observers see this scope and every scope under it, so registering first
	// means the whole wiring is logged.
	app.Observe(dislog.New(logger))

	app.Value(store.Config{DSN: "postgres://localhost/app"})
	app.Use(store.Module)
	app.Wire[*Repo](NewRepo)

	ctx := context.Background()
	if err := app.Start(ctx); err != nil {
		logger.Error("start", "err", err)
		os.Exit(1)
	}

	// Built after Start, so this build is logged here, between the two
	// phases, and its OnStart would run as it is handed out. It is a type in
	// main, which has no import path to lift out, so it gets no pkg.
	_ = app.Get[*Repo]()

	// Stop returns what the hooks reported as well as logging it, so a
	// caller that wants to act on a teardown failure still can.
	if err := app.Stop(ctx); err != nil {
		logger.Warn("stopped with failures", "err", err)
	}
}
INFO build service=store.Config pkg=github.com/acme/app/internal/store scope=root duration=25.667µs
INFO build service=*store.DB pkg=github.com/acme/app/internal/store scope=root module=store.Module duration=286.333µs
INFO start service=*store.DB pkg=github.com/acme/app/internal/store scope=root module=store.Module duration=792ns
INFO build service=*main.Repo scope=root duration=26.417µs
ERRO stop service=*store.DB pkg=github.com/acme/app/internal/store scope=root module=store.Module duration=4.709µs site=store.go:20 err="di: stopping *store.DB: connection reset"
WARN stopped with failures err="di: stopping *store.DB: connection reset"

A service is named the way it is written in Go, with the import path lifted out into pkg, since the path is most of the length and none of the meaning. Both come from the event -- Service and Package -- so nothing is parsed, and a key whose type is unnamed reports no package and keeps its whole name.

Observers see the scope they are registered on and every scope under it, so one on the application scope logs request scopes too. Events arrive on the goroutine that did the work, so a slow handler slows the application down. examples/guide/cmd/api wires it this way.

Performance

benchmarks/ is a separate module comparing this package with samber/do and uber-go/dig on the same four-service graph, so the library itself stays dependency-free. On an Apple M3 Pro:

Warm resolve Cold register and build
di, Provide closure 38 ns, 64 B, 2 allocs 3.7 µs, 4.5 kB, 70 allocs
di, Wire 38 ns, 64 B, 2 allocs 4.2 µs, 4.9 kB, 77 allocs
do v2.1 125 ns, 192 B, 6 allocs 6.1 µs, 11.5 kB, 120 allocs
dig v1.19 445 ns, 768 B, 24 allocs 16.4 µs, 24.3 kB, 302 allocs

di is measured twice because dig.Provide is reflective like Wire rather than like a Provide closure. The two warm figures are the same, which is what "a warm Get is the same code for both" means; the cold difference is the signature read and reflect.Call, about 120 ns per constructor here.

The dig warm figure needs a caveat. dig has no typed accessor, so the nearest thing to a resolve is Invoke with a function dig reflects over on every call, and an fx application invokes once at startup and never again. The cold comparison is the fair one, and it is the one fx cares about; read the warm number as what dig costs if used for something it does not set out to do — resolving on a request path, which is what Scoped bindings here are for.

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

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

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
	// Package is the import path of the type Service names, e.g.
	// "github.com/acme/app", so an observer can shorten or group by it
	// without parsing Service. It is empty for a shutdown, and for a key
	// whose type is unnamed -- a []byte, a map[string]int -- since reflect
	// already writes those with a short package name.
	Package  string
	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.
Package dislog logs a di.Scope's lifecycle events through log/slog.
Package dislog logs a di.Scope's lifecycle events through log/slog.

Jump to

Keyboard shortcuts

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