di
packageAPI reference for the di
package.
Imports
(10)testDB
type testDB struct
Fields
| Name | Type | Description |
|---|---|---|
| Name | string |
testService
type testService struct
Fields
| Name | Type | Description |
|---|---|---|
| DB | *testDB | inject:"db" |
| Logger | string | inject:"logger" |
TestContainer_ProvideAndGet
Parameters
func TestContainer_ProvideAndGet(t *testing.T)
{
c := New()
db := &testDB{Name: "test"}
c.Provide("db", db)
got, ok := c.Get("db")
if !ok {
t.Fatal("expected to find 'db'")
}
gotDB, ok := got.(*testDB)
if !ok {
t.Fatal("expected *testDB type")
}
if gotDB.Name != "test" {
t.Errorf("got %q, want %q", gotDB.Name, "test")
}
}
TestContainer_Has
Parameters
func TestContainer_Has(t *testing.T)
{
c := New()
c.Provide("exists", "value")
if !c.Has("exists") {
t.Error("Has should return true for 'exists'")
}
if c.Has("missing") {
t.Error("Has should return false for 'missing'")
}
}
TestContainer_Inject
Parameters
func TestContainer_Inject(t *testing.T)
{
c := New()
db := &testDB{Name: "injected"}
c.Provide("db", db)
c.Provide("logger", "stdout")
svc := &testService{}
if err := c.Inject(svc); err != nil {
t.Fatalf("Inject() error = %v", err)
}
if svc.DB != db {
t.Errorf("DB not injected correctly")
}
if svc.Logger != "stdout" {
t.Errorf("Logger: got %q, want %q", svc.Logger, "stdout")
}
}
TestContainer_InjectRejectsMissingDependency
Parameters
func TestContainer_InjectRejectsMissingDependency(t *testing.T)
{
c := New()
c.Provide("db", &testDB{})
err := c.Inject(&testService{})
if err == nil {
t.Fatal("Inject() accepted a missing dependency")
}
}
TestContainer_InjectRejectsWrongType
Parameters
func TestContainer_InjectRejectsWrongType(t *testing.T)
{
c := New()
c.Provide("db", &testDB{})
c.Provide("logger", 42)
err := c.Inject(&testService{})
if err == nil {
t.Fatal("Inject() accepted a dependency with the wrong type")
}
}
TestContainer_InjectRejectsUnexportedField
Parameters
func TestContainer_InjectRejectsUnexportedField(t *testing.T)
{
target := struct {
value string `inject:"value"`
}{}
container := NewBuilder()
container.Provide("value", "ok")
if err := container.MustBuild().Inject(&target); err == nil {
t.Fatal("Inject() accepted an unexported field")
}
}
TestContainer_Clone
Parameters
func TestContainer_Clone(t *testing.T)
{
c := New()
c.Provide("key", "value")
clone := c.Clone()
clone.Provide("new", "added")
if !clone.Has("key") {
t.Error("clone should have 'key'")
}
if !clone.Has("new") {
t.Error("clone should have 'new'")
}
if c.Has("new") {
t.Error("original should not have 'new'")
}
}
TestContainer_Keys
Parameters
func TestContainer_Keys(t *testing.T)
{
c := New()
c.Provide("a", 1)
c.Provide("b", 2)
keys := c.Keys()
if len(keys) != 2 {
t.Errorf("got %d keys, want 2", len(keys))
}
}
TestContainer_MustGet_Panic
Parameters
func TestContainer_MustGet_Panic(t *testing.T)
{
c := New()
defer func() {
if r := recover(); r == nil {
t.Error("MustGet should panic for missing key")
}
}()
c.MustGet("missing")
}
TestResolve
Parameters
func TestResolve(t *testing.T)
{
c := New()
c.Provide("num", 42)
got, ok := Resolve[int](c, "num")
if !ok {
t.Fatal("expected to resolve 'num'")
}
if got != 42 {
t.Errorf("got %d, want 42", got)
}
}
TestResolve_TypeMismatch
Parameters
func TestResolve_TypeMismatch(t *testing.T)
{
c := New()
c.Provide("num", 42)
_, ok := Resolve[string](c, "num")
if ok {
t.Error("should return false for type mismatch")
}
}
TestMustResolve_Panic
Parameters
func TestMustResolve_Panic(t *testing.T)
{
c := New()
defer func() {
if r := recover(); r == nil {
t.Error("MustResolve should panic for missing key")
}
}()
MustResolve[int](c, "missing")
}
TestBuilder_RegisterAndResolveType
Parameters
func TestBuilder_RegisterAndResolveType(t *testing.T)
{
b := NewBuilder()
Register(b, func() *testDB { return &testDB{Name: "built"} })
c := b.MustBuild()
db := ResolveType[*testDB](c)
if db.Name != "built" {
t.Errorf("got %q, want %q", db.Name, "built")
}
}
TestBuilder_RegisterAs
Parameters
func TestBuilder_RegisterAs(t *testing.T)
{
b := NewBuilder()
RegisterAs[Worker](b, func() *GoodWorker { return &GoodWorker{} })
c := b.MustBuild()
if got := ResolveType[Worker](c).Work(); got != "working hard" {
t.Fatalf("ResolveType[Worker]() = %q", got)
}
}
TestBuilder_RegisterImplRejectsInvalidContract
Parameters
func TestBuilder_RegisterImplRejectsInvalidContract(t *testing.T)
{
b := NewBuilder()
RegisterImpl[Worker, *BrokenWorker](b)
if _, err := b.Build(); err == nil {
t.Fatal("Build() accepted an invalid implementation")
}
}
TestBuilder_RegisterImplIgnoresContractMarkerForInjection
Parameters
func TestBuilder_RegisterImplIgnoresContractMarkerForInjection(t *testing.T)
{
b := NewBuilder()
RegisterImpl[Worker, *GoodWorker](b)
worker, err := TryResolveType[Worker](b.MustBuild())
if err != nil {
t.Fatal(err)
}
if worker.Work() != "working hard" {
t.Fatalf("Work() = %q", worker.Work())
}
}
TestBuilder_RegisterImplValidatesPointerFields
Parameters
func TestBuilder_RegisterImplValidatesPointerFields(t *testing.T)
{
b := NewBuilder()
RegisterImpl[Worker, *configuredWorker](b)
if _, err := b.Build(); err == nil {
t.Fatal("Build() accepted a pointer implementation with a missing dependency")
}
}
TestBuilder_RegisterImplRejectsUnexportedInjectionField
Parameters
func TestBuilder_RegisterImplRejectsUnexportedInjectionField(t *testing.T)
{
b := NewBuilder()
Register(b, func() string { return "configured" })
RegisterImpl[Worker, *hiddenDependencyWorker](b)
if _, err := b.Build(); err == nil {
t.Fatal("Build() accepted an unexported injection field")
}
}
TestBuilder_TransientLifetime
Parameters
func TestBuilder_TransientLifetime(t *testing.T)
{
b := NewBuilder()
Register(b, func() *testDB { return &testDB{Name: "fresh"} }, Transient)
c := b.MustBuild()
a := ResolveType[*testDB](c)
b2 := ResolveType[*testDB](c)
if a == b2 {
t.Error("Transient should return new instances")
}
}
TestBuilder_SingletonLifetime
Parameters
func TestBuilder_SingletonLifetime(t *testing.T)
{
b := NewBuilder()
Register(b, func() *testDB { return &testDB{Name: "singleton"} }, Singleton)
c := b.MustBuild()
a := ResolveType[*testDB](c)
b2 := ResolveType[*testDB](c)
if a != b2 {
t.Error("Singleton should return same instance")
}
}
TestBuilder_ConcurrentSingletonResolution
Parameters
func TestBuilder_ConcurrentSingletonResolution(t *testing.T)
{
b := NewBuilder()
started := make(chan struct{})
release := make(chan struct{})
var calls atomic.Int32
Register(b, func() *testDB {
if calls.Add(1) == 1 {
close(started)
}
<-release
return &testDB{Name: "singleton"}
})
container := b.MustBuild()
results := make(chan *testDB, 2)
go func() {
results <- ResolveType[*testDB](container)
}()
<-started
go func() {
results <- ResolveType[*testDB](container)
}()
close(release)
first := <-results
second := <-results
if first != second {
t.Fatal("concurrent singleton resolutions returned different instances")
}
if got := calls.Load(); got != 1 {
t.Fatalf("singleton factory called %d times", got)
}
}
TestResolveType_PanicOnMissing
Parameters
func TestResolveType_PanicOnMissing(t *testing.T)
{
b := NewBuilder()
c := b.MustBuild()
defer func() {
if r := recover(); r == nil {
t.Error("ResolveType should panic on missing type")
}
}()
ResolveType[*testDB](c)
}
TestBuilder_ProvideNamed
Parameters
func TestBuilder_ProvideNamed(t *testing.T)
{
b := NewBuilder()
b.Provide("db", &testDB{Name: "named"})
c := b.MustBuild()
got, ok := c.Get("db")
if !ok {
t.Fatal("expected named dep 'db'")
}
db := got.(*testDB)
if db.Name != "named" {
t.Errorf("got %q, want %q", db.Name, "named")
}
}
TestBuilder_RejectsDuplicateNamedDependency
Parameters
func TestBuilder_RejectsDuplicateNamedDependency(t *testing.T)
{
b := NewBuilder()
b.Provide("db", &testDB{})
b.Provide("db", &testDB{})
if _, err := b.Build(); err == nil {
t.Fatal("Build() accepted a duplicate named dependency")
}
}
TestBuilder_RejectsNilInstances
Parameters
func TestBuilder_RejectsNilInstances(t *testing.T)
{
b := NewBuilder()
var database *testDB
RegisterInstance(b, database)
b.Provide("database", database)
if _, err := b.Build(); err == nil {
t.Fatal("Build() accepted nil instances")
}
}
TestBuilder_RejectsNilFactories
Parameters
func TestBuilder_RejectsNilFactories(t *testing.T)
{
b := NewBuilder()
var factory func() *testDB
Register(b, factory)
if _, err := b.Build(); err == nil {
t.Fatal("Build() accepted a nil factory")
}
}
TestBuilder_RejectsNilFactoryResults
Parameters
func TestBuilder_RejectsNilFactoryResults(t *testing.T)
{
t.Run("register", func(t *testing.T) {
b := NewBuilder()
Register(b, func() *testDB { return nil })
if _, err := TryResolveType[*testDB](b.MustBuild()); err == nil {
t.Fatal("TryResolveType() accepted a nil factory result")
}
})
t.Run("register as", func(t *testing.T) {
b := NewBuilder()
RegisterAs[Worker](b, func() *GoodWorker { return nil })
if _, err := TryResolveType[Worker](b.MustBuild()); err == nil {
t.Fatal("TryResolveType() accepted a nil implementation")
}
})
t.Run("constructor", func(t *testing.T) {
b := NewBuilder()
RegisterFromFunc[*testDB](b, func() *testDB { return nil })
if _, err := TryResolveType[*testDB](b.MustBuild()); err == nil {
t.Fatal("TryResolveType() accepted a nil constructor result")
}
})
}
valueError
type valueError struct
Methods
TestRegisterFromFuncRejectsConcreteErrorResult
Parameters
func TestRegisterFromFuncRejectsConcreteErrorResult(t *testing.T)
{
defer func() {
if recover() == nil {
t.Fatal("RegisterFromFunc() accepted a concrete error result")
}
}()
RegisterFromFunc[*testDB](NewBuilder(), func() (*testDB, valueError) {
return &testDB{}, valueError{}
})
}
Worker
type Worker interface
Methods
BrokenWorker
type BrokenWorker struct
TestContainer_ProvideWithContracts
Parameters
func TestContainer_ProvideWithContracts(t *testing.T)
{
c := New()
t.Run("Valid implementation", func(t *testing.T) {
defer func() {
if r := recover(); r != nil {
t.Errorf("Provide should not panic for valid worker: %v", r)
}
}()
c.Provide("good", &GoodWorker{})
})
t.Run("Invalid implementation panics", func(t *testing.T) {
defer func() {
if r := recover(); r == nil {
t.Error("Provide should panic for broken worker")
}
}()
c.Provide("broken", &BrokenWorker{})
})
}
TestResolveAll
Parameters
func TestResolveAll(t *testing.T)
{
c := New()
c.Provide("good", &GoodWorker{})
c.Provide("lazy", &LazyWorker{})
c.Provide("other", "not a worker")
workers := ResolveAll[Worker](c)
if len(workers) != 2 {
t.Fatalf("expected 2 workers, got %d", len(workers))
}
foundGood := false
foundLazy := false
for _, w := range workers {
switch w.Work() {
case "working hard":
foundGood = true
case "working smart":
foundLazy = true
}
}
if !foundGood || !foundLazy {
t.Error("ResolveAll did not find all expected workers")
}
}
TestContainer_Scope
Parameters
func TestContainer_Scope(t *testing.T)
{
c := New()
c.Provide("shared", "value")
child := c.Scope()
if !child.Has("shared") {
t.Error("child should inherit named deps from parent")
}
}
TestContainer_ScopeDoesNotOwnSingleton
Parameters
func TestContainer_ScopeDoesNotOwnSingleton(t *testing.T)
{
b := NewBuilder()
Register(b, func() *closableService { return &closableService{} }, Singleton)
container := b.MustBuild()
scope := container.Scope()
service := ResolveType[*closableService](scope)
if err := scope.Close(); err != nil {
t.Fatalf("scope.Close(): %v", err)
}
if service.closed {
t.Fatal("scope closed a singleton owned by the root container")
}
if err := container.Close(); err != nil {
t.Fatalf("container.Close(): %v", err)
}
if !service.closed {
t.Fatal("root container did not close its singleton")
}
}
TestContainer_ProvideLazy
Parameters
func TestContainer_ProvideLazy(t *testing.T)
{
c := New()
called := 0
c.ProvideLazy("lazy", func() any {
called++
return "computed"
})
if called != 0 {
t.Error("lazy factory should not be called on registration")
}
v, ok := c.Get("lazy")
if !ok {
t.Fatal("expected to find 'lazy'")
}
if v != "computed" {
t.Errorf("got %v, want %q", v, "computed")
}
if called != 1 {
t.Error("lazy factory should be called once on first access")
}
v2, _ := c.Get("lazy")
if v2 != "computed" {
t.Error("lazy factory should return cached value")
}
if called != 1 {
t.Error("lazy factory should only be called once")
}
}
TestContainer_ResolveAllAllowsReentrantLazyFactory
Parameters
func TestContainer_ResolveAllAllowsReentrantLazyFactory(t *testing.T)
{
c := New()
c.ProvideLazy("worker", func() any {
c.Provide("side-effect", "registered")
return &GoodWorker{}
})
done := make(chan []Worker, 1)
go func() {
done <- ResolveAll[Worker](c)
}()
select {
case workers := <-done:
if len(workers) != 1 {
t.Fatalf("ResolveAll() returned %d workers", len(workers))
}
case <-time.After(time.Second):
t.Fatal("ResolveAll() deadlocked in a reentrant lazy factory")
}
}
TestContainer_ProvideLazyRejectsNilFactory
Parameters
func TestContainer_ProvideLazyRejectsNilFactory(t *testing.T)
{
defer func() {
if recover() == nil {
t.Fatal("ProvideLazy() accepted a nil factory")
}
}()
New().ProvideLazy("nil", nil)
}
TestContainer_ProvideLazyRejectsNilResult
Parameters
func TestContainer_ProvideLazyRejectsNilResult(t *testing.T)
{
container := New()
container.ProvideLazy("nil", func() any {
return (*testDB)(nil)
})
if value, ok := container.Get("nil"); ok || value != nil {
t.Fatalf("Get() = (%v, %v), want (nil, false)", value, ok)
}
}
TestContainer_NamedCloserIsClosed
Parameters
func TestContainer_NamedCloserIsClosed(t *testing.T)
{
service := &closableService{}
builder := NewBuilder()
builder.Provide("service", service)
container := builder.MustBuild()
if err := container.Close(); err != nil {
t.Fatal(err)
}
if !service.closed {
t.Fatal("Close() did not close a named dependency")
}
}
TestContainer_LazyCloserBelongsToRoot
Parameters
func TestContainer_LazyCloserBelongsToRoot(t *testing.T)
{
service := &closableService{}
container := New()
container.ProvideLazy("service", func() any { return service })
scope := container.Scope()
if _, ok := scope.Get("service"); !ok {
t.Fatal("scope did not resolve the lazy dependency")
}
if err := scope.Close(); err != nil {
t.Fatal(err)
}
if service.closed {
t.Fatal("scope closed a lazy dependency owned by the root")
}
if err := container.Close(); err != nil {
t.Fatal(err)
}
if !service.closed {
t.Fatal("root did not close its lazy dependency")
}
}
TestContainerRejectsUseAfterClose
Parameters
func TestContainerRejectsUseAfterClose(t *testing.T)
{
container := New()
container.Provide("value", 1)
if err := container.Close(); err != nil {
t.Fatal(err)
}
if _, ok := container.Get("value"); ok {
t.Fatal("Get() returned a value after Close()")
}
if _, err := TryResolveType[*testDB](container); !errors.Is(err, ErrContainerClosed) {
t.Fatalf("TryResolveType() error = %v, want ErrContainerClosed", err)
}
defer func() {
if recover() == nil {
t.Fatal("Provide() succeeded after Close()")
}
}()
container.Provide("late", 2)
}
TestResolveAllRejectsUseAfterClose
Parameters
func TestResolveAllRejectsUseAfterClose(t *testing.T)
{
container := New()
var called atomic.Bool
container.ProvideLazy("worker", func() any {
called.Store(true)
return &GoodWorker{}
})
if err := container.Close(); err != nil {
t.Fatal(err)
}
if workers := ResolveAll[Worker](container); len(workers) != 0 {
t.Fatalf("ResolveAll() returned %d workers after Close()", len(workers))
}
if called.Load() {
t.Fatal("ResolveAll() realized a lazy dependency after Close()")
}
}
TestContainerProvideLinearizesWithClose
Parameters
func TestContainerProvideLinearizesWithClose(t *testing.T)
{
for iteration := 0; iteration < 100; iteration++ {
container := New()
service := &closableService{}
start := make(chan struct{})
var wg sync.WaitGroup
var provided atomic.Bool
wg.Add(2)
go func() {
defer wg.Done()
<-start
_ = container.Close()
}()
go func() {
defer wg.Done()
<-start
defer func() {
_ = recover()
}()
container.Provide("service", service)
provided.Store(true)
}()
close(start)
wg.Wait()
if container.Has("service") {
t.Fatal("closed container reported a registered service")
}
if provided.Load() && !service.closed {
t.Fatal("service admitted before Close() was not closed")
}
}
}
TestContainerClosesLazyResourceRacingClose
Parameters
func TestContainerClosesLazyResourceRacingClose(t *testing.T)
{
container := New()
started := make(chan struct{})
release := make(chan struct{})
service := &closableService{}
container.ProvideLazy("service", func() any {
close(started)
<-release
return service
})
resolved := make(chan bool, 1)
go func() {
_, ok := container.Get("service")
resolved <- ok
}()
<-started
if err := container.Close(); err != nil {
t.Fatal(err)
}
close(release)
if <-resolved {
t.Fatal("Get() succeeded after concurrent Close()")
}
if !service.closed {
t.Fatal("late lazy resource was not closed")
}
}
Config
type Config struct
Fields
| Name | Type | Description |
|---|---|---|
| DSN | string |
NewUserService
Returns
func NewUserService(db *testDB, cfg *Config) UserService
{
return UserService{DB: db, Cfg: cfg}
}
Uses
NewFailingUserService
Returns
func NewFailingUserService(db *testDB, cfg *Config) (UserService, error)
{
return UserService{}, errors.New("constructor failed")
}
Uses
TestRegisterFromFunc
Parameters
func TestRegisterFromFunc(t *testing.T)
{
b := NewBuilder()
Register(b, func() *testDB { return &testDB{Name: "pg"} })
Register(b, func() *Config { return &Config{DSN: "host=localhost"} })
RegisterFromFunc[UserService](b, NewUserService, Scoped)
c := b.MustBuild()
svc := ResolveType[UserService](c)
if svc.DB.Name != "pg" {
t.Errorf("DB.Name = %q, want %q", svc.DB.Name, "pg")
}
if svc.Cfg.DSN != "host=localhost" {
t.Errorf("Cfg.DSN = %q, want %q", svc.Cfg.DSN, "host=localhost")
}
}
TestRegisterFromFunc_MissingDep
Parameters
func TestRegisterFromFunc_MissingDep(t *testing.T)
{
b := NewBuilder()
Register(b, func() *testDB { return &testDB{Name: "pg"} })
RegisterFromFunc[UserService](b, NewUserService, Scoped)
_, err := b.Build()
if err == nil {
t.Fatal("expected build error for missing Config dependency")
}
}
TestRegisterFromFunc_ReturnsConstructorError
Parameters
func TestRegisterFromFunc_ReturnsConstructorError(t *testing.T)
{
b := NewBuilder()
Register(b, func() *testDB { return &testDB{Name: "pg"} })
Register(b, func() *Config { return &Config{DSN: "host=localhost"} })
RegisterFromFunc[UserService](b, NewFailingUserService)
c := b.MustBuild()
if _, err := TryResolveType[UserService](c); err == nil {
t.Fatal("TryResolveType() ignored the constructor error")
}
}
TestRegisterFromFunc_ZeroArgumentConstructor
Parameters
func TestRegisterFromFunc_ZeroArgumentConstructor(t *testing.T)
{
builder := NewBuilder()
RegisterFromFunc[*testDB](builder, func() *testDB {
return &testDB{Name: "zero"}
})
if got := ResolveType[*testDB](builder.MustBuild()).Name; got != "zero" {
t.Fatalf("resolved name = %q, want zero", got)
}
}
failingInitializer
type failingInitializer interface
Methods
func Ready(...)
initFailure
type initFailure struct
TestRegisterImpl_PropagatesInitError
Parameters
func TestRegisterImpl_PropagatesInitError(t *testing.T)
{
builder := NewBuilder()
RegisterImpl[failingInitializer, *initFailure](builder)
container := builder.MustBuild()
if _, err := TryResolveType[failingInitializer](container); err == nil {
t.Fatal("TryResolveType() ignored Init error")
}
}
cyclicA
type cyclicA struct
cyclicB
type cyclicB struct
TestRegisterFromFunc_RejectsCircularDependency
Parameters
func TestRegisterFromFunc_RejectsCircularDependency(t *testing.T)
{
b := NewBuilder()
RegisterFromFunc[*cyclicA](b, newCyclicA)
RegisterFromFunc[*cyclicB](b, newCyclicB)
if _, err := b.Build(); err == nil {
t.Fatal("Build() accepted a circular constructor dependency")
}
}
serviceWithDB
type serviceWithDB struct
Fields
| Name | Type | Description |
|---|---|---|
| DB | *testDB |
TestRegisterFromFunc_UsesRegisteredSingleton
Parameters
func TestRegisterFromFunc_UsesRegisteredSingleton(t *testing.T)
{
b := NewBuilder()
created := 0
Register(b, func() *testDB {
created++
return &testDB{Name: "singleton"}
})
RegisterFromFunc[*serviceWithDB](b, func(db *testDB) *serviceWithDB {
return &serviceWithDB{DB: db}
})
container := b.MustBuild()
db := ResolveType[*testDB](container)
service := ResolveType[*serviceWithDB](container)
if service.DB != db {
t.Fatal("constructor dependency did not use the registered singleton")
}
if created != 1 {
t.Fatalf("singleton factory called %d times", created)
}
}
closableService
type closableService struct
Methods
Returns
func (*closableService) Close() error
{
c.closed = true
if c.order != nil {
*c.order = append(*c.order, c.name)
}
return nil
}
Fields
| Name | Type | Description |
|---|---|---|
| closed | bool | |
| order | *[]string | |
| name | string |
TestScopedContainer_Close
Parameters
func TestScopedContainer_Close(t *testing.T)
{
b := NewBuilder()
Register(b, func() *closableService { return &closableService{} }, Scoped)
c := b.MustBuild()
scope := c.Scope()
svc := ResolveType[*closableService](scope)
if svc.closed {
t.Error("service should not be closed yet")
}
if err := scope.Close(); err != nil {
t.Fatalf("Close: %v", err)
}
if !svc.closed {
t.Error("service should be closed after scope.Close()")
}
}
TestContainer_Close_Singleton
Parameters
func TestContainer_Close_Singleton(t *testing.T)
{
b := NewBuilder()
Register(b, func() *closableService { return &closableService{} }, Singleton)
c := b.MustBuild()
svc := ResolveType[*closableService](c)
if svc.closed {
t.Error("service should not be closed yet")
}
if err := c.Close(); err != nil {
t.Fatalf("Close: %v", err)
}
if !svc.closed {
t.Error("singleton implementing io.Closer should be closed on container close")
}
}
TestContainer_CloseRegisteredInstance
Parameters
func TestContainer_CloseRegisteredInstance(t *testing.T)
{
service := &closableService{}
b := NewBuilder()
RegisterInstance(b, service)
container := b.MustBuild()
if err := container.Close(); err != nil {
t.Fatal(err)
}
if !service.closed {
t.Fatal("Close() did not close a registered instance")
}
}
TestContainer_Close_NonCloser
Parameters
func TestContainer_Close_NonCloser(t *testing.T)
{
b := NewBuilder()
Register(b, func() *testDB { return &testDB{Name: "ok"} })
c := b.MustBuild()
ResolveType[*testDB](c)
if err := c.Close(); err != nil {
t.Fatalf("Close on non-closer should not error: %v", err)
}
}
dependentCloser
type dependentCloser struct
Methods
Returns
func (*dependentCloser) Close() error
{
*c.order = append(*c.order, "dependent")
return nil
}
Fields
| Name | Type | Description |
|---|---|---|
| service | *closableService | |
| order | *[]string |
TestContainer_CloseUsesReverseCreationOrder
Parameters
func TestContainer_CloseUsesReverseCreationOrder(t *testing.T)
{
var order []string
b := NewBuilder()
Register(b, func() *closableService {
return &closableService{name: "dependency", order: &order}
})
RegisterFromFunc[*dependentCloser](b, func(service *closableService) *dependentCloser {
return &dependentCloser{service: service, order: &order}
})
container := b.MustBuild()
ResolveType[*dependentCloser](container)
if err := container.Close(); err != nil {
t.Fatal(err)
}
if len(order) != 2 || order[0] != "dependent" || order[1] != "dependency" {
t.Fatalf("close order = %v", order)
}
}
Key
Key is a typed name for a dependency.
type Key struct
Fields
| Name | Type | Description |
|---|---|---|
| name | string |
NewKey
NewKey creates a typed dependency key.
Parameters
Returns
func NewKey[T any](name string) Key[T]
{
if name == "" {
panic("di: dependency name cannot be empty")
}
return Key[T]{name: name}
}
ProvideKey
ProvideKey registers a typed named dependency.
Parameters
func ProvideKey[T any](b *Builder, key Key[T], instance T)
{
b.Provide(key.name, instance)
}
ResolveKey
ResolveKey retrieves a dependency through a typed key.
Parameters
Returns
func ResolveKey[T any](c *Container, key Key[T]) (T, bool)
{
return Resolve[T](c, key.name)
}
MustResolveKey
MustResolveKey retrieves a dependency through a typed key and panics when missing.
Parameters
Returns
func MustResolveKey[T any](c *Container, key Key[T]) T
{
return MustResolve[T](c, key.name)
}
ProvideLazyKey
ProvideLazyKey registers a typed lazy dependency.
Parameters
func ProvideLazyKey[T any](c *Container, key Key[T], factory func() T)
{
if factory == nil {
panic("di: lazy dependency factory cannot be nil")
}
c.ProvideLazy(key.name, func() any {
return factory()
}, reflect.TypeOf((*T)(nil)).Elem())
}
TestKey
Parameters
func TestKey(t *testing.T)
{
key := NewKey[int]("answer")
b := NewBuilder()
ProvideKey(b, key, 42)
c := b.MustBuild()
got, ok := ResolveKey(c, key)
if !ok || got != 42 {
t.Fatalf("ResolveKey() = %v, %v", got, ok)
}
}
TestKeyRejectsWrongType
Parameters
func TestKeyRejectsWrongType(t *testing.T)
{
key := NewKey[int]("answer")
c := New()
c.Provide(key.Name(), "wrong")
if _, ok := ResolveKey(c, key); ok {
t.Fatal("ResolveKey() accepted the wrong type")
}
}
TestProvideLazyKeyRejectsNilFactory
Parameters
func TestProvideLazyKeyRejectsNilFactory(t *testing.T)
{
defer func() {
if recover() == nil {
t.Fatal("ProvideLazyKey() did not panic")
}
}()
ProvideLazyKey(New(), NewKey[int]("answer"), nil)
}
TestProvideLazyKeyCanBeValidatedWithoutInstantiation
Parameters
func TestProvideLazyKeyCanBeValidatedWithoutInstantiation(t *testing.T)
{
type target struct {
Answer int `inject:"answer"`
}
container := New()
called := false
ProvideLazyKey(container, NewKey[int]("answer"), func() int {
called = true
return 42
})
if err := container.ValidateTarget(&target{}); err != nil {
t.Fatal(err)
}
if called {
t.Fatal("ValidateTarget() instantiated a lazy dependency")
}
}
Resolve
Resolve retrieves a typed dependency from the container by name.
Parameters
Returns
func Resolve[T any](c *Container, name string) (T, bool)
{
var zero T
v, ok := c.Get(name)
if !ok {
return zero, false
}
typed, ok := v.(T)
if !ok {
return zero, false
}
return typed, true
}
MustResolve
MustResolve retrieves a typed dependency by name and panics if not found.
Parameters
Returns
func MustResolve[T any](c *Container, name string) T
{
v, ok := Resolve[T](c, name)
if !ok {
panic("di: cannot resolve " + name)
}
return v
}
ResolveAll
ResolveAll finds all named dependencies that implement the given interface T.
Parameters
Returns
func ResolveAll[T any](c *Container) []T
{
if c == nil || c.ensureOpen() != nil {
return nil
}
c.mu.RLock()
values := make([]any, 0, len(c.named))
for _, value := range c.named {
values = append(values, value)
}
c.mu.RUnlock()
var result []T
for _, v := range values {
if lp, ok := v.(*lazyProvider); ok {
var err error
v, err = lp.get()
if err != nil {
continue
}
if lp.owner.trackCloser(v) != nil {
continue
}
}
if t, ok := v.(T); ok {
result = append(result, t)
}
}
return result
}
Lifetime
Lifetime controls how services are instantiated.
type Lifetime int
serviceEntry
type serviceEntry struct
Fields
| Name | Type | Description |
|---|---|---|
| lifetime | Lifetime | |
| factory | any | |
| constructor | any | |
| instance | any | |
| built | bool | |
| buildMu | sync.Mutex | |
| concreteType | reflect.Type | |
| paramTypes | []reflect.Type |
Uses
ResolveError
ResolveError is returned when a type cannot be resolved.
type ResolveError struct
Methods
Returns
func (*ResolveError) Error() string
{
return fmt.Sprintf("di: cannot resolve type %v", e.Type)
}
Fields
| Name | Type | Description |
|---|---|---|
| Type | reflect.Type |
Container
Container is the dependency injection container.
type Container struct
Methods
Returns
func (*Container) validate() []error
{
var errs []error
states := make(map[reflect.Type]uint8)
for typ := range c.services {
if err := c.validateType(typ, states); err != nil {
errs = append(errs, err)
}
}
return errs
}
Parameters
Returns
func (*Container) validateType(typ reflect.Type, states map[reflect.Type]uint8) error
{
switch states[typ] {
case 1:
return fmt.Errorf("di: circular dependency detected for type %v", typ)
case 2:
return nil
}
states[typ] = 1
defer func() {
if states[typ] == 1 {
delete(states, typ)
}
}()
entry, ok := c.services[typ]
if !ok {
return &ResolveError{Type: typ}
}
if entry.constructor != nil {
for _, pt := range entry.paramTypes {
if err := c.validateType(pt, states); err != nil {
return fmt.Errorf("di: type %v requires %v: %w", typ, pt, err)
}
}
}
if entry.factory == nil && entry.constructor == nil {
if err := c.validateConcreteType(entry.concreteType, states); err != nil {
return err
}
}
states[typ] = 2
return nil
}
Parameters
Returns
func (*Container) validateConcreteType(typ reflect.Type, states map[reflect.Type]uint8) error
{
if typ.Kind() == reflect.Ptr {
typ = typ.Elem()
}
if typ.Kind() != reflect.Struct {
return fmt.Errorf("di: auto-construction requires struct type, got %v", typ)
}
if initMethod, ok := reflect.PointerTo(typ).MethodByName("Init"); ok {
errorType := reflect.TypeOf((*error)(nil)).Elem()
if initMethod.Type.NumOut() > 1 ||
initMethod.Type.NumOut() == 1 && initMethod.Type.Out(0) != errorType {
return fmt.Errorf("di: type %v Init must return nothing or error", typ)
}
for i := 1; i < initMethod.Type.NumIn(); i++ {
paramType := initMethod.Type.In(i)
if err := c.validateType(paramType, states); err != nil {
return fmt.Errorf("di: type %v Init requires %v: %w", typ, paramType, err)
}
}
return nil
}
for i := 0; i < typ.NumField(); i++ {
field := typ.Field(i)
tag := field.Tag.Get("inject")
if tag == "" {
continue
}
if !field.IsExported() {
return fmt.Errorf("di: struct %v injected field %s must be exported", typ, field.Name)
}
if err := c.validateType(field.Type, states); err != nil {
return fmt.Errorf(
"di: struct %v field %s requires %v: %w",
typ,
field.Name,
field.Type,
err,
)
}
}
return nil
}
Parameters
Returns
func (*Container) trackCloser(instance any) error
{
if closer, ok := instance.(io.Closer); ok {
c.closeMu.Lock()
if c.closed {
c.closeMu.Unlock()
_ = closer.Close()
return ErrContainerClosed
}
c.trackCloserLocked(closer)
c.closeMu.Unlock()
}
return nil
}
Parameters
func (*Container) trackCloserLocked(closer io.Closer)
{
for _, existing := range c.closers {
if sameCloser(existing, closer) {
return
}
}
c.closers = append(c.closers, closer)
}
Returns
func (*Container) ensureOpen() error
{
c.closeMu.Lock()
defer c.closeMu.Unlock()
if c.closed {
return ErrContainerClosed
}
return nil
}
Close calls Close() on all resolved services that implement io.Closer. Call this at the end of a scope (e.g. at end of HTTP request).
Returns
func (*Container) Close() error
{
c.closeMu.Lock()
if c.closed {
c.closeMu.Unlock()
return nil
}
c.closed = true
closers := make([]io.Closer, len(c.closers))
copy(closers, c.closers)
c.closers = nil
c.closeMu.Unlock()
var errs []error
for index := len(closers) - 1; index >= 0; index-- {
if err := closers[index].Close(); err != nil {
errs = append(errs, err)
}
}
if len(errs) > 0 {
return fmt.Errorf("di: close errors: %v", errs)
}
return nil
}
Parameters
func (*Container) Provide(name string, instance any)
{
if isNilInstance(instance) {
panic(fmt.Sprintf("di: dependency %q cannot be nil", name))
}
contracts.MustVerify(instance)
if name == "" {
panic("di: dependency name cannot be empty")
}
c.closeMu.Lock()
if c.closed {
c.closeMu.Unlock()
panic(ErrContainerClosed)
}
c.mu.Lock()
if _, exists := c.named[name]; exists {
c.mu.Unlock()
c.closeMu.Unlock()
panic(fmt.Sprintf("di: dependency %q is already registered", name))
}
c.named[name] = instance
c.mu.Unlock()
if closer, ok := instance.(io.Closer); ok {
c.trackCloserLocked(closer)
}
c.closeMu.Unlock()
}
Parameters
Returns
func (*Container) Get(name string) (any, bool)
{
if c.ensureOpen() != nil {
return nil, false
}
c.mu.RLock()
v, ok := c.named[name]
c.mu.RUnlock()
if !ok {
return nil, false
}
if lp, ok := v.(*lazyProvider); ok {
value, err := lp.get()
if err != nil {
return nil, false
}
if lp.owner.trackCloser(value) != nil {
return nil, false
}
return value, true
}
return v, true
}
Parameters
Returns
func (*Container) MustGet(name string) any
{
v, ok := c.Get(name)
if !ok {
panic("di: dependency not found: " + name)
}
return v
}
Parameters
Returns
func (*Container) Has(name string) bool
{
if c.ensureOpen() != nil {
return false
}
c.mu.RLock()
_, ok := c.named[name]
c.mu.RUnlock()
return ok
}
ValidateTarget checks named injection fields without changing target.
Parameters
Returns
func (*Container) ValidateTarget(target any) error
{
if err := c.ensureOpen(); err != nil {
return err
}
val := reflect.ValueOf(target)
if val.Kind() != reflect.Ptr || val.Elem().Kind() != reflect.Struct {
return fmt.Errorf("di: injection target must be a pointer to a struct")
}
fields := injectParser.ParseStruct(target)
for _, meta := range fields {
field := val.Elem().Field(meta.Index)
if !field.CanSet() {
return fmt.Errorf("di: injected field %s must be exported and settable", meta.Name)
}
name := meta.RawTag
if name == "" {
name = meta.Name
}
c.mu.RLock()
dep, ok := c.named[name]
c.mu.RUnlock()
if !ok {
return fmt.Errorf("di: dependency %q required by field %s is not registered", name, meta.Name)
}
depType := reflect.TypeOf(dep)
if lazy, isLazy := dep.(*lazyProvider); isLazy {
depType = lazy.resultType
if depType == nil {
return fmt.Errorf(
"di: lazy dependency %q has no type metadata; use a typed key or provide its result type",
name,
)
}
}
if depType == nil || !depType.AssignableTo(field.Type()) {
return fmt.Errorf("di: dependency %q has type %v, field %s requires %v", name, depType, meta.Name, field.Type())
}
}
return nil
}
Inject validates and populates named injection fields.
Parameters
Returns
func (*Container) Inject(target any) error
{
if err := c.ValidateTarget(target); err != nil {
return err
}
elem := reflect.ValueOf(target).Elem()
fields := injectParser.ParseStruct(target)
for _, meta := range fields {
field := elem.Field(meta.Index)
if !field.CanSet() {
return fmt.Errorf("di: injected field %s must be exported and settable", meta.Name)
}
name := meta.RawTag
if name == "" {
name = meta.Name
}
dep, _ := c.Get(name)
field.Set(reflect.ValueOf(dep))
}
return nil
}
Scope creates a child container with fresh scoped instances. When the scope ends, call Close() to dispose scoped services.
Returns
func (*Container) Scope() *Container
{
if err := c.ensureOpen(); err != nil {
panic(err)
}
c.mu.RLock()
defer c.mu.RUnlock()
child := New()
child.parent = c
for k, v := range c.services {
if v.lifetime == Scoped {
child.services[k] = &serviceEntry{
lifetime: v.lifetime,
factory: v.factory,
concreteType: v.concreteType,
constructor: v.constructor,
paramTypes: v.paramTypes,
}
} else {
child.services[k] = v
}
}
for k, v := range c.named {
child.named[k] = v
}
return child
}
Parameters
func (*Container) ProvideLazy(name string, factory func() any, resultType ...reflect.Type)
{
if name == "" {
panic("di: dependency name cannot be empty")
}
if factory == nil {
panic("di: lazy dependency factory cannot be nil")
}
c.closeMu.Lock()
if c.closed {
c.closeMu.Unlock()
panic(ErrContainerClosed)
}
c.mu.Lock()
if _, exists := c.named[name]; exists {
c.mu.Unlock()
c.closeMu.Unlock()
panic(fmt.Sprintf("di: dependency %q is already registered", name))
}
var typ reflect.Type
if len(resultType) > 0 {
typ = resultType[0]
}
c.named[name] = &lazyProvider{factory: factory, owner: c, resultType: typ}
c.mu.Unlock()
c.closeMu.Unlock()
}
Returns
func (*Container) Clone() *Container
{
if err := c.ensureOpen(); err != nil {
panic(err)
}
c.mu.RLock()
defer c.mu.RUnlock()
clone := New()
clone.parent = c
for k, v := range c.services {
clone.services[k] = v
}
for k, v := range c.named {
clone.named[k] = v
}
return clone
}
Returns
func (*Container) Keys() []string
{
if c.ensureOpen() != nil {
return nil
}
c.mu.RLock()
defer c.mu.RUnlock()
keys := make([]string, 0, len(c.named))
for k := range c.named {
keys = append(keys, k)
}
return keys
}
Parameters
Returns
func (*Container) ResolveAllTyped(iface reflect.Type) []any
{
if c.ensureOpen() != nil {
return nil
}
c.mu.RLock()
values := make([]any, 0, len(c.named))
for _, value := range c.named {
values = append(values, value)
}
c.mu.RUnlock()
var result []any
for _, v := range values {
if lp, ok := v.(*lazyProvider); ok {
var err error
v, err = lp.get()
if err != nil {
continue
}
if lp.owner.trackCloser(v) != nil {
continue
}
}
if !isNilInstance(v) && reflect.TypeOf(v).Implements(iface) {
result = append(result, v)
}
}
return result
}
Fields
| Name | Type | Description |
|---|---|---|
| services | map[reflect.Type]*serviceEntry | |
| named | map[string]any | |
| mu | sync.RWMutex | |
| parent | *Container | |
| closers | []io.Closer | |
| closeMu | sync.Mutex | |
| closed | bool |
New
New creates a new empty Container.
Returns
func New() *Container
{
return &Container{
services: make(map[reflect.Type]*serviceEntry),
named: make(map[string]any),
}
}
Builder
Builder constructs a Container with validation.
type Builder struct
Methods
Build creates a Container and validates all registrations. Returns an error if any registered type has unresolvable dependencies.
Returns
func (*Builder) Build() (*Container, error)
{
c := New()
b.mu.RLock()
buildErrors := append([]error(nil), b.buildErrors...)
for k, v := range b.services {
c.services[k] = cloneServiceEntry(v)
}
for k, v := range b.named {
c.named[k] = v
}
b.mu.RUnlock()
if len(buildErrors) > 0 {
return c, fmt.Errorf("di: build validation failed: %v", buildErrors)
}
if errs := c.validate(); len(errs) > 0 {
return c, fmt.Errorf("di: build validation failed: %v", errs)
}
for _, entry := range c.services {
if entry.built {
_ = c.trackCloser(entry.instance)
}
}
for _, instance := range c.named {
if _, lazy := instance.(*lazyProvider); !lazy {
_ = c.trackCloser(instance)
}
}
return c, nil
}
MustBuild is like Build but panics on validation errors.
Returns
func (*Builder) MustBuild() *Container
{
c, err := b.Build()
if err != nil {
panic(err.Error())
}
return c
}
Parameters
func (*Builder) Provide(name string, instance any)
{
if isNilInstance(instance) {
b.mu.Lock()
b.buildErrors = append(b.buildErrors, fmt.Errorf("di: dependency %q cannot be nil", name))
b.mu.Unlock()
return
}
contracts.MustVerify(instance)
b.mu.Lock()
if name == "" {
b.buildErrors = append(b.buildErrors, fmt.Errorf("di: dependency name cannot be empty"))
b.mu.Unlock()
return
}
if _, exists := b.named[name]; exists {
b.buildErrors = append(b.buildErrors, fmt.Errorf("di: dependency %q is already registered", name))
b.mu.Unlock()
return
}
b.named[name] = instance
b.mu.Unlock()
}
Fields
| Name | Type | Description |
|---|---|---|
| services | map[reflect.Type]*serviceEntry | |
| named | map[string]any | |
| mu | sync.RWMutex | |
| validated | bool | |
| buildErrors | []error |
NewBuilder
NewBuilder creates a new Builder.
Returns
func NewBuilder() *Builder
{
return &Builder{
services: make(map[reflect.Type]*serviceEntry),
named: make(map[string]any),
}
}
Register
Register adds a service factory to the builder.
Parameters
func Register[T any](b *Builder, factory func() T, lifetime ...Lifetime)
{
lt := Singleton
if len(lifetime) > 0 {
lt = lifetime[0]
}
typ := reflect.TypeOf((*T)(nil)).Elem()
b.mu.Lock()
if factory == nil {
b.buildErrors = append(b.buildErrors, fmt.Errorf("di: factory for %v cannot be nil", typ))
b.mu.Unlock()
return
}
if _, exists := b.services[typ]; exists {
b.buildErrors = append(b.buildErrors, fmt.Errorf("di: type %v is already registered", typ))
b.mu.Unlock()
return
}
b.services[typ] = &serviceEntry{lifetime: lt, factory: factory, concreteType: typ}
b.mu.Unlock()
}
RegisterAs
RegisterAs registers a service factory resolving to a different concrete type.
Parameters
func RegisterAs[I, T any](b *Builder, factory func() T, lifetime ...Lifetime)
{
lt := Singleton
if len(lifetime) > 0 {
lt = lifetime[0]
}
typ := reflect.TypeOf((*I)(nil)).Elem()
concreteType := reflect.TypeOf((*T)(nil)).Elem()
b.mu.Lock()
if factory == nil {
b.buildErrors = append(b.buildErrors, fmt.Errorf("di: factory for %v cannot be nil", typ))
b.mu.Unlock()
return
}
if !concreteType.Implements(typ) {
b.buildErrors = append(b.buildErrors, fmt.Errorf("di: type %v does not implement %v", concreteType, typ))
b.mu.Unlock()
return
}
if _, exists := b.services[typ]; exists {
b.buildErrors = append(b.buildErrors, fmt.Errorf("di: type %v is already registered", typ))
b.mu.Unlock()
return
}
b.services[typ] = &serviceEntry{lifetime: lt, factory: factory, concreteType: concreteType}
b.mu.Unlock()
}
RegisterInstance
RegisterInstance registers a pre-created instance as a singleton.
Parameters
func RegisterInstance[T any](b *Builder, instance T)
{
typ := reflect.TypeOf((*T)(nil)).Elem()
b.mu.Lock()
if isNilInstance(instance) {
b.buildErrors = append(b.buildErrors, fmt.Errorf("di: instance for %v cannot be nil", typ))
b.mu.Unlock()
return
}
if _, exists := b.services[typ]; exists {
b.buildErrors = append(b.buildErrors, fmt.Errorf("di: type %v is already registered", typ))
b.mu.Unlock()
return
}
b.services[typ] = &serviceEntry{
lifetime: Singleton,
concreteType: typ,
factory: func() T { return instance },
built: true,
instance: instance,
}
b.mu.Unlock()
}
RegisterImpl
RegisterImpl registers a concrete type T that satisfies interface I.
Parameters
func RegisterImpl[I, T any](b *Builder, lifetime ...Lifetime)
{
lt := Singleton
if len(lifetime) > 0 {
lt = lifetime[0]
}
iTyp := reflect.TypeOf((*I)(nil)).Elem()
tTyp := reflect.TypeOf((*T)(nil)).Elem()
b.mu.Lock()
if !tTyp.Implements(iTyp) {
b.buildErrors = append(b.buildErrors, fmt.Errorf("di: type %v does not implement %v", tTyp, iTyp))
b.mu.Unlock()
return
}
if _, exists := b.services[iTyp]; exists {
b.buildErrors = append(b.buildErrors, fmt.Errorf("di: type %v is already registered", iTyp))
b.mu.Unlock()
return
}
b.services[iTyp] = &serviceEntry{lifetime: lt, factory: nil, concreteType: tTyp}
b.mu.Unlock()
}
RegisterFromFunc
RegisterFromFunc registers a service by constructor function with dependency injection.
Parameters
func RegisterFromFunc[T any](b *Builder, constructor any, lifetime ...Lifetime)
{
lt := Singleton
if len(lifetime) > 0 {
lt = lifetime[0]
}
typ := reflect.TypeOf((*T)(nil)).Elem()
ctorType := reflect.TypeOf(constructor)
if ctorType == nil || ctorType.Kind() != reflect.Func {
panic("di: RegisterFromFunc requires a function")
}
if ctorType.NumOut() < 1 || ctorType.NumOut() > 2 {
panic("di: RegisterFromFunc constructor must return a value and optional error")
}
if !ctorType.Out(0).AssignableTo(typ) {
panic(fmt.Sprintf("di: RegisterFromFunc constructor returns %v, want %v", ctorType.Out(0), typ))
}
errorType := reflect.TypeOf((*error)(nil)).Elem()
if ctorType.NumOut() == 2 && ctorType.Out(1) != errorType {
panic("di: RegisterFromFunc second result must be error")
}
paramTypes := make([]reflect.Type, ctorType.NumIn())
for i := 0; i < ctorType.NumIn(); i++ {
paramTypes[i] = ctorType.In(i)
}
b.mu.Lock()
if _, exists := b.services[typ]; exists {
b.buildErrors = append(b.buildErrors, fmt.Errorf("di: type %v is already registered", typ))
b.mu.Unlock()
return
}
b.services[typ] = &serviceEntry{
lifetime: lt, factory: nil, concreteType: typ,
constructor: constructor, paramTypes: paramTypes,
}
b.mu.Unlock()
}
cloneServiceEntry
Parameters
Returns
func cloneServiceEntry(entry *serviceEntry) *serviceEntry
{
return &serviceEntry{
lifetime: entry.lifetime,
factory: entry.factory,
constructor: entry.constructor,
instance: entry.instance,
built: entry.built,
concreteType: entry.concreteType,
paramTypes: append([]reflect.Type(nil), entry.paramTypes...),
}
}
ResolveType
ResolveType resolves a service from the container, panicking on failure.
Parameters
Returns
func ResolveType[T any](c *Container) T
{
result, err := tryResolveType(c, reflect.TypeOf((*T)(nil)).Elem())
if err != nil {
panic(err.Error())
}
return result.Interface().(T)
}
TryResolveType
TryResolveType resolves a service from the container, returning an error on failure.
Parameters
Returns
func TryResolveType[T any](c *Container) (T, error)
{
var zero T
result, err := tryResolveType(c, reflect.TypeOf((*T)(nil)).Elem())
if err != nil {
return zero, err
}
return result.Interface().(T), nil
}
tryResolveType
Parameters
Returns
func tryResolveType(c *Container, typ reflect.Type) (reflect.Value, error)
{
return resolveByType(c, typ, make(map[reflect.Type]struct{}))
}
resolveByType
Parameters
Returns
func resolveByType(c *Container, typ reflect.Type, stack map[reflect.Type]struct{}) (reflect.Value, error)
{
if err := c.ensureOpen(); err != nil {
return reflect.Value{}, err
}
c.mu.RLock()
entry, ok := c.services[typ]
c.mu.RUnlock()
if !ok {
if c.parent != nil {
return resolveByType(c.parent, typ, stack)
}
return reflect.Value{}, &ResolveError{Type: typ}
}
if entry.lifetime == Singleton && c.parent != nil {
return resolveByType(c.parent, typ, stack)
}
if _, resolving := stack[typ]; resolving {
return reflect.Value{}, fmt.Errorf("di: circular dependency detected for type %v", typ)
}
stack[typ] = struct{}{}
defer func() {
delete(stack, typ)
}()
switch entry.lifetime {
case Singleton, Scoped:
entry.buildMu.Lock()
defer entry.buildMu.Unlock()
if entry.built {
return reflect.ValueOf(entry.instance), nil
}
result, err := invokeEntry(c, entry, stack)
if err != nil {
return reflect.Value{}, err
}
if isNilReflectValue(result) {
return reflect.Value{}, fmt.Errorf("di: factory for %v returned nil", typ)
}
entry.instance = result.Interface()
entry.built = true
if err := c.trackCloser(entry.instance); err != nil {
return reflect.Value{}, err
}
return result, nil
case Transient:
result, err := invokeEntry(c, entry, stack)
if err != nil {
return reflect.Value{}, err
}
if isNilReflectValue(result) {
return reflect.Value{}, fmt.Errorf("di: factory for %v returned nil", typ)
}
if err := c.trackCloser(result.Interface()); err != nil {
return reflect.Value{}, err
}
return result, nil
default:
return reflect.Value{}, &ResolveError{Type: typ}
}
}
sameCloser
func sameCloser(left, right io.Closer) bool
{
leftType := reflect.TypeOf(left)
rightType := reflect.TypeOf(right)
if leftType != rightType || leftType == nil || !leftType.Comparable() {
return false
}
return left == right
}
invokeEntry
Parameters
Returns
func invokeEntry(c *Container, entry *serviceEntry, stack map[reflect.Type]struct{}) (reflect.Value, error)
{
if entry.constructor != nil {
return invokeConstructor(c, entry, stack)
}
if entry.factory != nil {
results := reflect.ValueOf(entry.factory).Call(nil)
return results[0], nil
}
return constructValue(c, entry.concreteType, stack)
}
invokeConstructor
Parameters
Returns
func invokeConstructor(c *Container, entry *serviceEntry, stack map[reflect.Type]struct{}) (reflect.Value, error)
{
ctorVal := reflect.ValueOf(entry.constructor)
args := make([]reflect.Value, len(entry.paramTypes))
for i, pt := range entry.paramTypes {
resolved, err := resolveByType(c, pt, stack)
if err != nil {
return reflect.Value{}, fmt.Errorf("di: cannot resolve param %d (%v) of constructor for %v: %w", i, pt, entry.concreteType, err)
}
args[i] = resolved
}
results := ctorVal.Call(args)
if len(results) == 2 && !results[1].IsNil() {
return reflect.Value{}, results[1].Interface().(error)
}
return results[0], nil
}
constructValue
Parameters
Returns
func constructValue(c *Container, typ reflect.Type, stack map[reflect.Type]struct{}) (reflect.Value, error)
{
if typ.Kind() == reflect.Ptr {
typ = typ.Elem()
}
if typ.Kind() != reflect.Struct {
return reflect.Value{}, fmt.Errorf("di: auto-construction requires struct type, got %v", typ)
}
val := reflect.New(typ)
ctorVal := val.MethodByName("Init")
if ctorVal.IsValid() {
ctorType := ctorVal.Type()
args := make([]reflect.Value, ctorType.NumIn())
for i := 0; i < ctorType.NumIn(); i++ {
resolved, err := resolveByType(c, ctorType.In(i), stack)
if err != nil {
return reflect.Value{}, fmt.Errorf("di: cannot resolve param %d of Init: %w", i, err)
}
args[i] = resolved
}
results := ctorVal.Call(args)
if len(results) == 1 && !results[0].IsNil() {
return reflect.Value{}, results[0].Interface().(error)
}
} else {
elem := val.Elem()
for i := 0; i < typ.NumField(); i++ {
field := typ.Field(i)
tag := field.Tag.Get("inject")
if tag == "" {
continue
}
fieldVal := elem.Field(i)
if !fieldVal.CanSet() || !field.IsExported() {
return reflect.Value{}, fmt.Errorf("di: injected field %s must be exported and settable", field.Name)
}
resolved, err := resolveByType(c, fieldVal.Type(), stack)
if err != nil {
return reflect.Value{}, fmt.Errorf("di: cannot resolve field %s: %w", field.Name, err)
}
fieldVal.Set(resolved)
}
}
return val, nil
}
isNilInstance
Parameters
Returns
func isNilInstance(instance any) bool
{
if instance == nil {
return true
}
value := reflect.ValueOf(instance)
switch value.Kind() {
case reflect.Chan, reflect.Func, reflect.Interface, reflect.Map,
reflect.Ptr, reflect.Slice:
return value.IsNil()
default:
return false
}
}
isNilReflectValue
Parameters
Returns
func isNilReflectValue(value reflect.Value) bool
{
if !value.IsValid() {
return true
}
switch value.Kind() {
case reflect.Chan, reflect.Func, reflect.Interface, reflect.Map,
reflect.Ptr, reflect.Slice:
return value.IsNil()
default:
return false
}
}
lazyProvider
type lazyProvider struct
Methods
Returns
func (*lazyProvider) get() (any, error)
{
l.once.Do(func() {
l.value = l.factory()
if isNilInstance(l.value) {
l.err = errors.New("di: lazy dependency factory returned nil")
}
})
return l.value, l.err
}
Fields
| Name | Type | Description |
|---|---|---|
| once | sync.Once | |
| value | any | |
| err | error | |
| factory | func() any | |
| owner | *Container | |
| resultType | reflect.Type |