Resolution is how you ask goxdi for a value of type T. The entry points are Get and MustGet.
func Get[T any](from Resolver) (T, error)
func MustGet[T any](from Resolver) TAnything that implements Resolver can be passed as from: the root Container, a Scope, or the resolver handed to a factory while it runs.
Returns (T, error). Use it at boundaries where failure should become a returned error (HTTP handlers, job runners, CLI).
repo, err := goxdi.Get[*Repo](scope)
if err != nil {
return err
}Calls Get and panics if err != nil. Use it when a missing dependency is a programmer error and the process should not continue half-initialized — typically inside factories at the composition root.
_ = goxdi.AddScoped(b, func(r goxdi.Resolver) (*Repo, error) {
return &Repo{DB: goxdi.MustGet[*sql.DB](r)}, nil
})There is no TryGet. Optional dependencies are an application pattern: either register a no-op implementation, or call Get and branch on ErrNotRegistered.
When goxdi invokes a factory, it passes a Resolver tied to the current root + optional scope. The container mutex is released while the factory runs so nested Get calls do not deadlock. Dependencies resolved through that r participate correctly in:
- the active lifetime caches
- circular-dependency detection
- disposer tracking for the creating boundary
Prefer resolving through r:
func(r goxdi.Resolver) (*OrderService, error) {
repo, err := goxdi.Get[*OrderRepo](r)
if err != nil {
return nil, err
}
payments, err := goxdi.Get[Payments](r)
if err != nil {
return nil, err
}
return &OrderService{Repo: repo, Payments: payments}, nil
}Avoid resolving through a captured outer variable when constructing a graph that should use the active context:
// Avoid — skips the factory resolve context
func(r goxdi.Resolver) (*OrderService, error) {
return &OrderService{Repo: goxdi.MustGet[*OrderRepo](outerScope)}, nil
}Prefer r. Captured Container/Scope values do not carry the construction stack, so circular dependencies resolved that way may hang instead of returning ErrCircularDependency.
Factories may resolve other registered types. goxdi creates dependencies on demand (lazy), caching according to each type’s lifetime.
_ = goxdi.AddSingleton(b, func(goxdi.Resolver) (*DB, error) { return &DB{}, nil })
_ = goxdi.AddScoped(b, func(r goxdi.Resolver) (*Repo, error) {
return &Repo{DB: goxdi.MustGet[*DB](r)}, nil
})
_ = goxdi.AddTransient(b, func(r goxdi.Resolver) (*Service, error) {
return &Service{Repo: goxdi.MustGet[*Repo](r)}, nil
})
scope := root.NewScope()
defer scope.Close()
s1 := goxdi.MustGet[*Service](scope)
s2 := goxdi.MustGet[*Service](scope)
// s1 != s2 (transient)
// s1.Repo == s2.Repo (scoped, same scope)
// s1.Repo.DB == s2.Repo.DB (singleton)Build() does not walk factories to prove the graph is complete. An unregistered dependency fails at the first Get that needs it (ErrNotRegistered).
HELPFUL: At process boot, resolve your composition root once inside a throwaway scope to fail fast before serving traffic.
If A’s factory resolves B while B’s factory resolves A, goxdi returns ErrCircularDependency with a path of types.
// A → B → A
_, err := goxdi.Get[*A](root)
// errors.Is(err, goxdi.ErrCircularDependency)goxdi does not invent lazy proxies to break cycles. Fix the design:
- introduce an interface to invert one edge
- move shared state to a third type
- pass a callback/event instead of a hard constructor dependency
Resolving from a closed scope or closed root fails with ErrClosed. Create a new scope after close; do not reuse a closed one.