Releases: floatdrop/di
Release list
v0.14.0
A module dependency report, derived from what registrations already carry.
go doc -all against 0.13.1 adds Scope.Modules and changes no signature;
an upgrade cannot break a caller.
Added
Scope.Modules()renders the modules registered into a 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
known only when they run. A need only a resolving scope can provide is
reported as owed, asValidatereports it, and a module's dependency on its
own services is left out. It is derived from what registrations already
carry, the module label and the parametersWiredeclares, so there is
nothing to declare twice. Keys are named by their package rather than their
import path, to read beside the module labels.
v0.13.1
Three defects reported in #35, each with a reproduction, each real. go doc -all against 0.13.0 is unchanged in di and dihttp; an upgrade cannot
break a caller, and every fix turns a panic, a false rejection or a lost
error into the behaviour the documentation already promised.
Fixed
WireandWrapaccepted a result merely assignable to the key, such as a
chan intfor a<-chan intkey or a[]bytefor a named slice type, and
then stored it as the constructor's own type, soGetpanicked asserting
it. The value is stored as the key's type now (#35).Validatereported a cycle where a valid graph visited oneScoped
binding in two scopes. A node on its path is now a binding in a holder, as
it is on a resolution path at run time, so the two instances are told
apart (#35).- A
Workerthat returned its own failure joined with the cancellation, as
errors.Join(ctx.Err(), err), had the failure dropped with the
cancellation, andStopreturned nil. Only an error that says nothing
beyond the cancellation is dropped now (#35).
v0.13.0
One route, one line. go doc -all against 0.12.0 leaves di untouched and
adds dihttp.Handle; nothing changes shape or behaviour, and an upgrade
cannot break a caller.
Added
dihttp.Handle(method)makes anhttp.Handlerthat resolves a handler
type from the request's scope and calls one of its methods, named by a
method expression:mux.Handle("GET /users/{id}", dihttp.Handle((*Users).Show)).
One type per resource with a method per route replaces a type, a
registration and a closure per route. The type isScopedwhen it needs
the request and a plain singleton when it does not;Handlefollows
either. Nothing existing changes; an upgrade cannot break a caller.
v0.12.0
The request-scope middleware becomes a dependency a server's constructor can
take, so an application needs no Provide closure to serve HTTP. go doc -all against 0.11.0 leaves di untouched and changes dihttp: Middleware
is now the type, NewMiddleware makes one, Module registers one. The rename
is the breaking change, and the reason for the minor bump.
Changed
dihttp.Middlewareis now the type,func(http.Handler) http.Handler, so
a server's constructor can take one as a parameter; the function that
makes one isdihttp.NewMiddleware. A calldihttp.Middleware(app)no
longer compiles: rename it, or take the middleware as a dependency.
Added
dihttp.Moduleregisters aMiddlewareover the scope it is applied to.
With it the guide's server is a wired constructor with its dependencies
declared, and the guide application uses noProvideclosure at all.
v0.11.0
Validate no longer needs an adapter to check request scopes: the caller
says what such a scope will hold. go doc -all against 0.10.0 adds Stub
and Provided, gives Scope.Validate a variadic parameter, which every
existing call satisfies, and removes dihttp.Validate, the one breaking
change.
Added
di.Provided[T]()makes aStub, a key the scope resolving aScoped
binding will provide.s.Validate(stubs...)then makes the check that scope
would make: what the stubs cover is satisfied, and what neither the scope
nor the stubs provide is an error rather thanOwed. Stubs apply to the
Scopedpath only; a singleton that would build aScopedservice in its
own scope still fails there, whatever a request scope holds.
Removed
dihttp.Validate, which opened a throwaway request scope to do what
app.Validate(di.Provided[*http.Request]())now does from the application
scope, without an HTTP-shaped helper in the way. Replace the call with that
expression and.Err().
v0.10.0
A service can be wrapped without being replaced (#1), which was the last
open issue and the gap most often felt next to uber/fx. go doc -all
against 0.9.1 adds Scope.Wrap and changes no signature; an upgrade cannot
break a caller, since every new rejection involves a wrapper.
Added
Scope.Wrap[T](fn)composes over whatever servesTwhen it is called,
the latest registration in this scope or the one an ancestor provides
(#1).fntakes the wrapped value first and its other dependencies after
it, read with reflection asWirereads a constructor, and returnsTor
(T, error). What is wrapped keeps its registration, hooks and lifetime, is
built first and stopped after the wrapper; wrappers chain in registration
order; a wrapper takes the wrapped lifetime, andScoped()on it makes one
per resolving scope over a shared inner. A wrapper in a child scope applies
to that scope and its descendants only, which is what uber/fx calls
Decorate. Nothing to wrap, a group, and theGroup()orOverride()
markers on a wrapper are rejected. A registration some wrapper composes
over can no longer be overridden, in its scope or a descendant's, until an
Override()replaces the wrapper itself.Explainnames a wrapper and
draws what it wraps beneath it;Validatewalks the chain.
v0.9.1
Explain shows the graph Wire declared before it is built. go doc -all
against 0.9.0 adds no symbol and changes no signature, and an upgrade cannot
break a caller; only the rendering of an unbuilt Wire service gains lines.
Changed
Explaindraws the dependencies aWirebinding declares when it has not
been built: dashed edges under the node, each continuing as the recorded
tree where the dependency has been built and as a declared one where it has
not, ending at a closure or at a key nothing provides. Adeclared by:line
names the unbuilt services that declare a key, besideneeded by:for the
built ones. The output for a built service is unchanged, andGraphstill
renders only what was built.
v0.9.0
Constructors can be handed over as they are written, and the graph they
declare can be checked before anything is built (#3). Nothing existing
changes shape or behaviour; an upgrade cannot break a caller.
go doc -all against 0.8.0 adds Scope.Wire, Scope.Validate, Validation
with its Err method, and dihttp.Validate, and changes no signature. The
README no longer promises no reflection at all: Wire reads a constructor's
signature once at registration and calls it through reflect.Call; Provide
is as it was.
Added
Scope.Wire[T](ctor)registers a plain constructor,func(A, B) Tor
func(A, B) (T, error), whose parameters are its dependencies. Each is
resolved as aProvideclosure would resolve it, from the same scope, so
lifetimes, hooks, cycles and error paths are unchanged. The signature is
read with reflection once at registration, where a constructor of the wrong
shape is rejected like any other configuration error; a result merely
assignable toTis accepted, so a concrete constructor can serve an
interface key. The build calls the constructor throughreflect.Call, about
150ns and two allocations over a closure.Provideis untouched.Scope.Validate()walks the declared graph without building and returns a
Validation:Errors(joined byErr()) for a dependency nothing provides,
a cycle amongWireconstructors, or a singleton that would build aScoped
service in a scope that cannot satisfy it;Owedfor what aScoped
binding needs that the validating scope does not provide, since the scope
resolving it may;Uncheckedfor theProvideclosures.dihttp.Validate(app)validates from a throwaway request scope holding an
*http.Request, so what a request scope would still miss is an error.
v0.8.0
v0.8.0
Two features and one rule. Explain and Graph render the dependency graph as it
was actually built. Modules compose by Use, with every registration attributed
to the module that made it. And the rule that makes modules safe to compose: a
second registration of a key within one scope must say so with Override(), or
it is rejected naming both sites. That last one is breaking, and the reason for
the minor bump.
go doc -all against v0.7.0 adds Binding.Override, Module, Scope.Use,
Scope.Explain and Scope.Graph, and changes one signature: Test takes
...Module.
Behaviour that can break an upgrade:
-
A duplicate registration of a key within one scope no longer wins silently.
It is rejected at the next resolution, naming both registrations and, when
they came from modules, both modules:di: *app.DB is provided at app.Storage (wire.go:12) and again at
app.Caching (cache.go:8): a second registration of a key must be
marked Override() to replace the firstMark the registration that means to replace the earlier one: for the test
seam, s.Value(&fake).Override(). An Override() with nothing to override in
its scope is rejected too, since a fake for a service that has since been
renamed would otherwise be a registration nobody resolves. A child scope
still shadows its parent without any marker. A key that has served a value
still cannot be replaced at all. -
Test takes ...Module. Function literals and named functions are assignable,
so those calls compile unchanged; a call that spreads a []func(*Scope) with
... needs the slice typed []Module.
Added:
-
Scope.Explain[T] prints the dependency tree of a service as it was built,
with each node's lifetime, scope, lifecycle state and registration site, and
the instances that needed it. Scope.Graph exports everything built as
Graphviz DOT, one cluster per scope. Neither builds anything. The recording
behind them costs nothing on the warm path. -
Module is a named func(*Scope); Scope.Use applies modules in order and
attributes every registration they make -- directly, from a child the module
opens, or later from a constructor the module registered -- to the module's
function name. Event.Module carries the same name to observers.
Fixed:
- A panicking OnDrain or OnStop hook is reported as that hook's failure rather
than propagating out of Stop. The start step was always recovered this way;
the other two were not, so a panic in either abandoned the teardown halfway:
stopOnce claimed and never settled, every later Stop waiting on it until its
context ran out, every instance behind it never released. Found by the
concurrent fuzzer the moment it could build a scope with a permanently
rejected registration -- a drain hook resolving through one meets the
rejection as a panic.
Internal: the machinery the 0.7.0 API cuts left behind is gone. A resolution
path node is 32 bytes rather than 48, so a warm resolve allocates 64 B rather
than 96 B and runs about 8% faster.
Testing: the generators register through Use and carry an override bit, the
property model gained the override rule and was mutation-tested clause by
clause, and generator coverage went from 88.5% to 92.1% with the CI floor
raised to 90.
v0.7.0
The API surface cut down ahead of the graph-validation work of #3, so that it lands on a smaller and more regular API, plus the four defects of the fourth September 2026 review.
This release breaks callers. Nine identifiers are gone and one is renamed; go doc -all diffed against 0.6.0 lists exactly those and one addition, Binding.Group. Every one has a one-line migration:
| 0.6.0 | 0.7.0 |
|---|---|
.Named("x"), s.Lookup(di.Named[T]("x")), Key, di.Named |
a distinct type: type ReplicaDB struct{ *DB } |
Binding.Run(f) |
Binding.Worker(f) |
s.Add(ctor) |
s.Provide(ctor).Group() |
s.Bind[Reader, *Repo]() |
s.Provide(func(s *di.Scope) Reader { return s.Get[*Repo]() }), .Scoped() too when the target is |
.Transient() |
a factory closure, or call the constructor |
.Health(f), HealthCheck, ErrUnhealthy, EventHealth |
a Group of checkers; the README's Groups section is the recipe |
app.Middleware(mux) |
dihttp.Middleware(app)(mux), from the new dihttp package |
di.Signals(...) |
none; Run exits on os.Interrupt and SIGTERM |
di.SlogObserver(l) |
four lines in user code |
Why these: Named was the one stringly-typed corner, and a newtype is checked at every reference where a misspelt name was a missing key at runtime. Bind carried the most machinery per use in the library, and a constructor that returns the implementation is checked by the compiler where Bind checked Implements at registration. Transient had no hooks and no tracking, a lifetime in name only, and every teardown oracle carried an exemption for it; the singleton-stability invariant now has none. Health checks are an application concern that a group expresses in three lines. The core package no longer imports net/http or log/slog, and registers nothing on a caller's behalf.
The rest is correctness, from the fourth review. Scope.Run reports a cause published through Shutdown while a failed Start was rolling back, not only during an ordinary stop. A Stop reports the failure of a drain hook of its own scope even when an ancestor's Stop ran the hook, which is the request-scope-during-shutdown case. And a key can no longer be overridden while a resolution of it is in flight, which closed a path to two live values for one key from a single goroutine.
Testing: drain hooks can fail in the drivers, the machines drive Scope.Run, and two registration shapes have constructors that register. Generator coverage went from 82.5% to 90.2%, and CI now fails below 85%. The coverage-gap script itself turned out to be wrong, merging blocks that shared a line, and CI now checks its arithmetic against go tool cover.
Full notes with the reasoning behind each cut are in CHANGELOG.md.