From 69a55540b474933a5c16b94a529412b5ceae1c81 Mon Sep 17 00:00:00 2001 From: dhernando Date: Sun, 13 Jul 2025 23:13:45 +0200 Subject: [PATCH 01/16] remove redunant generic hinting --- di.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/di.go b/di.go index fd363373..bb529912 100644 --- a/di.go +++ b/di.go @@ -7,7 +7,7 @@ import ( func Provide[T any](i *Injector, provider Provider[T]) { name := generateServiceName[T]() - ProvideNamed[T](i, name, provider) + ProvideNamed(i, name, provider) } func ProvideNamed[T any](i *Injector, name string, provider Provider[T]) { @@ -43,7 +43,7 @@ func ProvideNamedValue[T any](i *Injector, name string, value T) { func Override[T any](i *Injector, provider Provider[T]) { name := generateServiceName[T]() - OverrideNamed[T](i, name, provider) + OverrideNamed(i, name, provider) } func OverrideNamed[T any](i *Injector, name string, provider Provider[T]) { @@ -58,7 +58,7 @@ func OverrideNamed[T any](i *Injector, name string, provider Provider[T]) { func OverrideValue[T any](i *Injector, value T) { name := generateServiceName[T]() - OverrideNamedValue[T](i, name, value) + OverrideNamedValue(i, name, value) } func OverrideNamedValue[T any](i *Injector, name string, value T) { From 63781ea8f935d7e6fb80a4f1619ad4fb14e6bb92 Mon Sep 17 00:00:00 2001 From: dhernando Date: Sun, 13 Jul 2025 23:13:58 +0200 Subject: [PATCH 02/16] consistent makefile spacing --- Makefile | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Makefile b/Makefile index 8213998a..7c107478 100644 --- a/Makefile +++ b/Makefile @@ -16,6 +16,7 @@ watch-test: bench: go test -benchmem -count 3 -bench ./... + watch-bench: reflex -t 50ms -s -- sh -c 'go test -benchmem -count 3 -bench ./...' @@ -36,6 +37,7 @@ tools: lint: golangci-lint run --timeout 60s --max-same-issues 50 ./... + lint-fix: golangci-lint run --timeout 60s --max-same-issues 50 --fix ./... From 34c6dada46831a0dad1fcd66d72cba6ae392bf83 Mon Sep 17 00:00:00 2001 From: dhernando Date: Sun, 13 Jul 2025 23:27:11 +0200 Subject: [PATCH 03/16] use reflect instead of fmt.Sprintf to avoid comparing against a string --- service.go | 43 ++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 42 insertions(+), 1 deletion(-) diff --git a/service.go b/service.go index 53d640f6..ffbf8a08 100644 --- a/service.go +++ b/service.go @@ -2,6 +2,7 @@ package do import ( "fmt" + "reflect" ) type Service[T any] interface { @@ -21,6 +22,11 @@ type shutdownableService interface { } func generateServiceName[T any]() string { + return generateServiceNameWithReflect[T]() +} + +//nolint:unused +func generateServiceNameWithSprintf[T any]() string { var t T // struct @@ -29,10 +35,45 @@ func generateServiceName[T any]() string { return name } - // interface + //interface return fmt.Sprintf("%T", new(T)) } + +func generateServiceNameWithReflect[T any]() string { + var t T + // For non-pointer types, reflect.TypeOf(t) will never be nil. + // For pointer types, reflect.TypeOf(t) can be nil if t is nil. + typ := reflect.TypeOf(t) + if typ == nil { + return "" + } + + if name := typ.String(); name != "" { + return name + } + + return reflect.TypeOf(new(T)).String() +} + +// func generateServiceName[T any]() string { +// var t T +// // For non-pointer types, reflect.TypeOf(t) will never be nil. +// // For pointer types, reflect.TypeOf(t) can be nil if t is nil. +// typ := reflect.TypeOf(t) +// if typ == nil { +// return "" +// } +// +// name := typ.Name() +// if name != "" { +// return name +// } +// +// return reflect.TypeOf(new(T)).Name() +// } + + type Healthcheckable interface { HealthCheck() error } From 1c9c59219b81299598dd639b5ccbcd7093d7b8e8 Mon Sep 17 00:00:00 2001 From: dhernando Date: Sun, 13 Jul 2025 23:35:47 +0200 Subject: [PATCH 04/16] remove redundant generic hint --- di.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/di.go b/di.go index bb529912..e8836fd1 100644 --- a/di.go +++ b/di.go @@ -25,7 +25,7 @@ func ProvideNamed[T any](i *Injector, name string, provider Provider[T]) { func ProvideValue[T any](i *Injector, value T) { name := generateServiceName[T]() - ProvideNamedValue[T](i, name, value) + ProvideNamedValue(i, name, value) } func ProvideNamedValue[T any](i *Injector, name string, value T) { From 9e060c6f41f5c10f5ee17e703e6d32d7bccf31c4 Mon Sep 17 00:00:00 2001 From: dhernando Date: Sun, 13 Jul 2025 23:36:39 +0200 Subject: [PATCH 05/16] add unused (for now) function to generate service names with fully qualified service name (package.StructName) --- service.go | 36 +++++++++++++++++++----------------- 1 file changed, 19 insertions(+), 17 deletions(-) diff --git a/service.go b/service.go index ffbf8a08..721d0d34 100644 --- a/service.go +++ b/service.go @@ -39,7 +39,6 @@ func generateServiceNameWithSprintf[T any]() string { return fmt.Sprintf("%T", new(T)) } - func generateServiceNameWithReflect[T any]() string { var t T // For non-pointer types, reflect.TypeOf(t) will never be nil. @@ -56,23 +55,26 @@ func generateServiceNameWithReflect[T any]() string { return reflect.TypeOf(new(T)).String() } -// func generateServiceName[T any]() string { -// var t T -// // For non-pointer types, reflect.TypeOf(t) will never be nil. -// // For pointer types, reflect.TypeOf(t) can be nil if t is nil. -// typ := reflect.TypeOf(t) -// if typ == nil { -// return "" -// } -// -// name := typ.Name() -// if name != "" { -// return name -// } -// -// return reflect.TypeOf(new(T)).Name() -// } +// generateServiceNameWithFQSN generates a fully qualified service name. +// It uses the package path and type name to create a unique identifier for the service. +// This is useful for services that are defined in different packages but have the same type name. +// Example: "github.com/user/project/service.MyService" +func generateServiceNameWithFQSN[T any]() string { + var t T + // For non-pointer types, reflect.TypeOf(t) will never be nil. + // For pointer types, reflect.TypeOf(t) can be nil if t is nil. + typ := reflect.TypeOf(t) + if typ == nil { + return "" + } + + name := typ.PkgPath() + "." + typ.Name() + if name != "" { + return name + } + return reflect.TypeOf(new(T)).String() +} type Healthcheckable interface { HealthCheck() error From ac42da1ca753ee41960bfba26e79763ce0b6a092 Mon Sep 17 00:00:00 2001 From: dhernando Date: Sun, 13 Jul 2025 23:45:04 +0200 Subject: [PATCH 06/16] preallocate the map when inverting it --- utils.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/utils.go b/utils.go index 6b7c8c09..7911c49b 100644 --- a/utils.go +++ b/utils.go @@ -31,7 +31,7 @@ func mAp[T any, R any](collection []T, iteratee func(T) R) []R { } func invertMap[K comparable, V comparable](in map[K]V) map[V]K { - out := map[V]K{} + out := make(map[V]K, len(in)) for k, v := range in { out[v] = k From 36dd7007c457054dd3a58ec69fad78fded2e96fd Mon Sep 17 00:00:00 2001 From: dhernando Date: Sun, 13 Jul 2025 23:51:23 +0200 Subject: [PATCH 07/16] make helper functions consistent --- utils.go | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/utils.go b/utils.go index 7911c49b..5b3e72c8 100644 --- a/utils.go +++ b/utils.go @@ -11,23 +11,23 @@ func must(err error) { } func keys[K comparable, V any](in map[K]V) []K { - result := make([]K, 0, len(in)) + out := make([]K, 0, len(in)) for k := range in { - result = append(result, k) + out = append(out, k) } - return result + return out } -func mAp[T any, R any](collection []T, iteratee func(T) R) []R { - result := make([]R, len(collection)) +func mAp[T any, R any](in []T, f func(T) R) []R { + out := make([]R, len(in)) - for i, item := range collection { - result[i] = iteratee(item) + for i, item := range in { + out[i] = f(item) } - return result + return out } func invertMap[K comparable, V comparable](in map[K]V) map[V]K { From 815fa2017f1d339af810cf4efbda3059c1739086 Mon Sep 17 00:00:00 2001 From: dhernando Date: Mon, 14 Jul 2025 01:07:57 +0200 Subject: [PATCH 08/16] check for circular dependencies --- di_test.go | 21 +++++++++++++++++++++ service_lazy.go | 16 ++++++++++++---- 2 files changed, 33 insertions(+), 4 deletions(-) diff --git a/di_test.go b/di_test.go index a41be9ea..beb1da51 100644 --- a/di_test.go +++ b/di_test.go @@ -153,6 +153,27 @@ func TestInvoke(t *testing.T) { is.Errorf(err2, "do: service not found") } +func TestInvokeCircularDependency(t *testing.T) { + is := assert.New(t) + + type test struct{} + + i := New() + + is.Panics(func() { + Provide(i, func(i *Injector) (test, error) { + instance, err := Invoke[test](i) + if err != nil { + return test{}, nil + } + + return instance, nil + }) + + _ = MustInvoke[test](i) + }, "circular dependency was not detected (this message will only be read in here when the test never finishes because of infinite recursion)") +} + func TestInvokeNamed(t *testing.T) { is := assert.New(t) diff --git a/service_lazy.go b/service_lazy.go index f9b5ccb4..feb84845 100644 --- a/service_lazy.go +++ b/service_lazy.go @@ -2,6 +2,7 @@ package do import ( "sync" + "sync/atomic" ) type Provider[T any] func(*Injector) (T, error) @@ -14,6 +15,7 @@ type ServiceLazy[T any] struct { // lazy loading built bool provider Provider[T] + building atomic.Bool } func newServiceLazy[T any](name string, provider Provider[T]) Service[T] { @@ -25,15 +27,22 @@ func newServiceLazy[T any](name string, provider Provider[T]) Service[T] { } } -//nolint:unused func (s *ServiceLazy[T]) getName() string { return s.name } -//nolint:unused func (s *ServiceLazy[T]) getInstance(i *Injector) (T, error) { + if s.building.Load() { + panic("DI: circular dependency detected for service " + s.name) + } + s.mu.Lock() - defer s.mu.Unlock() + s.building.Store(true) + + defer func() { + s.building.Store(false) + s.mu.Unlock() + }() if !s.built { err := s.build(i) @@ -45,7 +54,6 @@ func (s *ServiceLazy[T]) getInstance(i *Injector) (T, error) { return s.instance, nil } -//nolint:unused func (s *ServiceLazy[T]) build(i *Injector) (err error) { defer func() { if r := recover(); r != nil { From 9868e99e9f005a7f739e33cd36a71fbd39cd095c Mon Sep 17 00:00:00 2001 From: dhernando Date: Mon, 14 Jul 2025 01:09:27 +0200 Subject: [PATCH 09/16] improve generateServiceNameWithFQSN implementation (name can't be empty now) --- service.go | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/service.go b/service.go index 721d0d34..2a8f867c 100644 --- a/service.go +++ b/service.go @@ -68,12 +68,14 @@ func generateServiceNameWithFQSN[T any]() string { return "" } - name := typ.PkgPath() + "." + typ.Name() - if name != "" { - return name + prefix := "" + typName := typ + if typ.Kind() == reflect.Ptr { + prefix = "*" + typName = typ.Elem() } - return reflect.TypeOf(new(T)).String() + return prefix + typName.PkgPath() + "." + typName.Name() } type Healthcheckable interface { From a8c0a67c07526a776cafeb558a35618bb75b610b Mon Sep 17 00:00:00 2001 From: dhernando Date: Mon, 14 Jul 2025 01:10:50 +0200 Subject: [PATCH 10/16] use fmt for a better panic message --- service_lazy.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/service_lazy.go b/service_lazy.go index feb84845..b0e266f7 100644 --- a/service_lazy.go +++ b/service_lazy.go @@ -1,6 +1,7 @@ package do import ( + "fmt" "sync" "sync/atomic" ) @@ -33,7 +34,7 @@ func (s *ServiceLazy[T]) getName() string { func (s *ServiceLazy[T]) getInstance(i *Injector) (T, error) { if s.building.Load() { - panic("DI: circular dependency detected for service " + s.name) + panic(fmt.Sprintf("DI: circular dependency detected for service %q", s.name)) } s.mu.Lock() From a41e0b0245c4ad700cec002361dc62bbcdd31024 Mon Sep 17 00:00:00 2001 From: dhernando Date: Mon, 14 Jul 2025 01:20:43 +0200 Subject: [PATCH 11/16] add advanced test for circular dependency --- di_test.go | 63 ++++++++++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 56 insertions(+), 7 deletions(-) diff --git a/di_test.go b/di_test.go index beb1da51..6e3a4b5b 100644 --- a/di_test.go +++ b/di_test.go @@ -154,13 +154,12 @@ func TestInvoke(t *testing.T) { } func TestInvokeCircularDependency(t *testing.T) { - is := assert.New(t) - - type test struct{} + t.Run("simple circular dependency", func(t *testing.T) { + type test struct{} - i := New() + is := assert.New(t) + i := New() - is.Panics(func() { Provide(i, func(i *Injector) (test, error) { instance, err := Invoke[test](i) if err != nil { @@ -170,8 +169,58 @@ func TestInvokeCircularDependency(t *testing.T) { return instance, nil }) - _ = MustInvoke[test](i) - }, "circular dependency was not detected (this message will only be read in here when the test never finishes because of infinite recursion)") + is.Panics(func() { + _ = MustInvoke[test](i) + }, "circular dependency was not detected (this message will only be read in here when the test never finishes because of infinite recursion)") + + }) + + t.Run("subtle circular dependency", func(t *testing.T) { + type itest any + + type test4 struct { + t itest + } + type test3 struct { + t itest + } + type test2 struct { + t itest + } + type test1 struct { + t itest + } + + is := assert.New(t) + i := New() + + // test1 -> test2 -> test3 -> test4 -- + // ^______________________________| + Provide(i, func(i *Injector) (test1, error) { + return test1{ + t: MustInvoke[test2](i), + }, nil + }) + Provide(i, func(i *Injector) (test2, error) { + return test2{ + t: MustInvoke[test3](i), + }, nil + }) + Provide(i, func(i *Injector) (test3, error) { + return test3{ + t: MustInvoke[test4](i), + }, nil + }) + Provide(i, func(i *Injector) (test4, error) { + return test4{ + t: MustInvoke[test1](i), + }, nil + }) + + is.Panics(func() { + _ = MustInvoke[test1](i) + }, "circular dependency was not detected (this message will only be read in here when the test never finishes because of infinite recursion)") + }) } func TestInvokeNamed(t *testing.T) { From c757a534c0b267ae1afdf758b6421e78e4ae7e55 Mon Sep 17 00:00:00 2001 From: dhernando Date: Mon, 14 Jul 2025 02:14:22 +0200 Subject: [PATCH 12/16] add fqsn option --- di.go | 16 ++++++------ di_test.go | 7 +----- injector.go | 3 +++ injector_test.go | 63 +++++++++++++++++++++++++++++++++++++++++++----- service.go | 31 +++++++++++++++--------- service_test.go | 51 ++++++++++++++++++++++++++++++++++++++- 6 files changed, 138 insertions(+), 33 deletions(-) diff --git a/di.go b/di.go index e8836fd1..ddfd95a0 100644 --- a/di.go +++ b/di.go @@ -5,7 +5,7 @@ import ( ) func Provide[T any](i *Injector, provider Provider[T]) { - name := generateServiceName[T]() + name := generateServiceNameFromInjector[T](i) ProvideNamed(i, name, provider) } @@ -23,7 +23,7 @@ func ProvideNamed[T any](i *Injector, name string, provider Provider[T]) { } func ProvideValue[T any](i *Injector, value T) { - name := generateServiceName[T]() + name := generateServiceNameFromInjector[T](i) ProvideNamedValue(i, name, value) } @@ -41,7 +41,7 @@ func ProvideNamedValue[T any](i *Injector, name string, value T) { } func Override[T any](i *Injector, provider Provider[T]) { - name := generateServiceName[T]() + name := generateServiceNameFromInjector[T](i) OverrideNamed(i, name, provider) } @@ -56,7 +56,7 @@ func OverrideNamed[T any](i *Injector, name string, provider Provider[T]) { } func OverrideValue[T any](i *Injector, value T) { - name := generateServiceName[T]() + name := generateServiceNameFromInjector[T](i) OverrideNamedValue(i, name, value) } @@ -71,7 +71,7 @@ func OverrideNamedValue[T any](i *Injector, name string, value T) { } func Invoke[T any](i *Injector) (T, error) { - name := generateServiceName[T]() + name := generateServiceNameFromInjector[T](i) return InvokeNamed[T](i, name) } @@ -117,7 +117,7 @@ func invokeImplem[T any](i *Injector, name string) (T, error) { } func HealthCheck[T any](i *Injector) error { - name := generateServiceName[T]() + name := generateServiceNameFromInjector[T](i) return getInjectorOrDefault(i).healthcheckImplem(name) } @@ -126,12 +126,12 @@ func HealthCheckNamed(i *Injector, name string) error { } func Shutdown[T any](i *Injector) error { - name := generateServiceName[T]() + name := generateServiceNameFromInjector[T](i) return getInjectorOrDefault(i).shutdownImplem(name) } func MustShutdown[T any](i *Injector) { - name := generateServiceName[T]() + name := generateServiceNameFromInjector[T](i) must(getInjectorOrDefault(i).shutdownImplem(name)) } diff --git a/di_test.go b/di_test.go index 6e3a4b5b..1395e8ef 100644 --- a/di_test.go +++ b/di_test.go @@ -161,12 +161,7 @@ func TestInvokeCircularDependency(t *testing.T) { i := New() Provide(i, func(i *Injector) (test, error) { - instance, err := Invoke[test](i) - if err != nil { - return test{}, nil - } - - return instance, nil + return MustInvoke[test](i), nil }) is.Panics(func() { diff --git a/injector.go b/injector.go index 5f3d4586..e41c1114 100644 --- a/injector.go +++ b/injector.go @@ -26,6 +26,7 @@ func New() *Injector { type InjectorOpts struct { HookAfterRegistration func(injector *Injector, serviceName string) HookAfterShutdown func(injector *Injector, serviceName string) + UseFQSN bool // Use Fully Qualified Service Name (FQSN) for service names Logf func(format string, args ...any) } @@ -47,6 +48,7 @@ func NewWithOpts(opts *InjectorOpts) *Injector { hookAfterRegistration: opts.HookAfterRegistration, hookAfterShutdown: opts.HookAfterShutdown, + useFQSN: opts.UseFQSN, logf: logf, } @@ -62,6 +64,7 @@ type Injector struct { hookAfterRegistration func(injector *Injector, serviceName string) hookAfterShutdown func(injector *Injector, serviceName string) + useFQSN bool // Use Fully Qualified Service Name (FQSN) for service names logf func(format string, args ...any) } diff --git a/injector_test.go b/injector_test.go index c1eac84f..17499c07 100644 --- a/injector_test.go +++ b/injector_test.go @@ -45,18 +45,43 @@ func TestInjectorNewWithOpts(t *testing.T) { } func TestInjectorListProvidedServices(t *testing.T) { + type test struct{} + is := assert.New(t) i := New() is.NotPanics(func() { - ProvideValue[int](i, 42) - ProvideValue[float64](i, 21) + ProvideValue(i, 42) + ProvideValue(i, 21.0) + ProvideValue(i, test{}) + }) + + is.NotPanics(func() { + services := i.ListProvidedServices() + is.ElementsMatch([]string{"int", "float64", "do.test"}, services) + }) +} + + +func TestInjectorListProvidedServicesWithFQSN(t *testing.T) { + type test struct{} + is := assert.New(t) + + i := NewWithOpts(&InjectorOpts{ + UseFQSN: true, + }) + + is.NotPanics(func() { + ProvideValue(i, 42) + ProvideValue(i, 21.0) + ProvideValue(i, "test") + ProvideValue(i, test{}) }) is.NotPanics(func() { services := i.ListProvidedServices() - is.ElementsMatch([]string{"int", "float64"}, services) + is.ElementsMatch([]string{"int", "float64", "string", "github.com/samber/do.test"}, services) }) } @@ -66,8 +91,8 @@ func TestInjectorListInvokedServices(t *testing.T) { i := New() is.NotPanics(func() { - ProvideValue[int](i, 42) - ProvideValue[float64](i, 21) + ProvideValue(i, 42) + ProvideValue(i, 21.0) MustInvoke[int](i) }) @@ -77,6 +102,32 @@ func TestInjectorListInvokedServices(t *testing.T) { }) } +func TestInjectorListInvokedServicesWithFQSN(t *testing.T) { + type test struct{} + is := assert.New(t) + + i := NewWithOpts(&InjectorOpts{ + UseFQSN: true, + }) + + is.NotPanics(func() { + ProvideValue(i, 42) + ProvideValue(i, 21.0) + ProvideValue(i, "test") + ProvideValue(i, test{}) + + MustInvoke[int](i) + MustInvoke[float64](i) + MustInvoke[string](i) + MustInvoke[test](i) + }) + + is.NotPanics(func() { + services := i.ListInvokedServices() + is.Equal([]string{"int", "float64", "string", "github.com/samber/do.test"}, services) + }) +} + type testHealthCheck struct { } @@ -90,7 +141,7 @@ func TestInjectorHealthCheck(t *testing.T) { i := New() is.NotPanics(func() { - ProvideValue[int](i, 42) + ProvideValue(i, 42) ProvideNamed(i, "testHealthCheck", func(i *Injector) (*testHealthCheck, error) { return &testHealthCheck{}, nil }) diff --git a/service.go b/service.go index 2a8f867c..2b281f08 100644 --- a/service.go +++ b/service.go @@ -21,6 +21,13 @@ type shutdownableService interface { shutdown() error } +func generateServiceNameFromInjector[T any](i *Injector) string { + if i != nil && i.useFQSN { + return generateServiceNameWithFQSN[T]() + } + return generateServiceName[T]() +} + func generateServiceName[T any]() string { return generateServiceNameWithReflect[T]() } @@ -41,18 +48,13 @@ func generateServiceNameWithSprintf[T any]() string { func generateServiceNameWithReflect[T any]() string { var t T - // For non-pointer types, reflect.TypeOf(t) will never be nil. - // For pointer types, reflect.TypeOf(t) can be nil if t is nil. + // reflect.TypeOf(t) will be nil when T is an interface type. typ := reflect.TypeOf(t) if typ == nil { - return "" - } - - if name := typ.String(); name != "" { - return name + typ = reflect.TypeOf(new(T)).Elem() } - return reflect.TypeOf(new(T)).String() + return typ.String() } // generateServiceNameWithFQSN generates a fully qualified service name. @@ -61,11 +63,10 @@ func generateServiceNameWithReflect[T any]() string { // Example: "github.com/user/project/service.MyService" func generateServiceNameWithFQSN[T any]() string { var t T - // For non-pointer types, reflect.TypeOf(t) will never be nil. - // For pointer types, reflect.TypeOf(t) can be nil if t is nil. + // reflect.TypeOf(t) will be nil when T is an interface type. typ := reflect.TypeOf(t) if typ == nil { - return "" + typ = reflect.TypeOf(new(T)).Elem() } prefix := "" @@ -75,7 +76,13 @@ func generateServiceNameWithFQSN[T any]() string { typName = typ.Elem() } - return prefix + typName.PkgPath() + "." + typName.Name() + name := typName.Name() + pkg := typName.PkgPath() + if name != "" && pkg != "" { + return prefix + pkg + "." + name + } + + return prefix + typName.String() } type Healthcheckable interface { diff --git a/service_test.go b/service_test.go index 37416583..59b9d7a0 100644 --- a/service_test.go +++ b/service_test.go @@ -9,7 +9,9 @@ import ( func TestGenerateServiceName(t *testing.T) { is := assert.New(t) - type test struct{} //nolint:unused + type MyFunc func(int) int + type itest interface {} + type test struct{} name := generateServiceName[test]() is.Equal("do.test", name) @@ -17,6 +19,53 @@ func TestGenerateServiceName(t *testing.T) { name = generateServiceName[*test]() is.Equal("*do.test", name) + name = generateServiceName[itest]() + is.Equal("do.itest", name) + + name = generateServiceName[*itest]() + is.Equal("*do.itest", name) + + name = generateServiceName[func(int)int]() + is.Equal("func(int) int", name) + + name = generateServiceName[MyFunc]() + is.Equal("do.MyFunc", name) + name = generateServiceName[int]() is.Equal("int", name) + + name = generateServiceName[*int]() + is.Equal("*int", name) +} + +func TestGenerateServiceNameWithFQSN(t *testing.T) { + is := assert.New(t) + + type MyFunc func(int) int + type itest interface {} + type test struct{} + + name := generateServiceNameWithFQSN[test]() + is.Equal("github.com/samber/do.test", name) + + name = generateServiceNameWithFQSN[*test]() + is.Equal("*github.com/samber/do.test", name) + + name = generateServiceNameWithFQSN[itest]() + is.Equal("github.com/samber/do.itest", name) + + name = generateServiceNameWithFQSN[*itest]() + is.Equal("*github.com/samber/do.itest", name) + + name = generateServiceNameWithFQSN[func(int)int]() + is.Equal("func(int) int", name) + + name = generateServiceNameWithFQSN[MyFunc]() + is.Equal("github.com/samber/do.MyFunc", name) + + name = generateServiceNameWithFQSN[int]() + is.Equal("int", name) + + name = generateServiceNameWithFQSN[*int]() + is.Equal("*int", name) } From b779ef4bf0ae7b4578e0db7668739b0c958e7a7c Mon Sep 17 00:00:00 2001 From: dhernando Date: Mon, 14 Jul 2025 02:26:43 +0200 Subject: [PATCH 13/16] make list ListInvokedServices return always in the invoked order --- injector.go | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/injector.go b/injector.go index e41c1114..4a415b48 100644 --- a/injector.go +++ b/injector.go @@ -81,9 +81,20 @@ func (i *Injector) ListProvidedServices() []string { func (i *Injector) ListInvokedServices() []string { i.mu.RLock() - names := keys(i.orderedInvocation) + invocations := invertMap(i.orderedInvocation) + invocationIndex := i.orderedInvocationIndex i.mu.RUnlock() + names := make([]string, 0, invocationIndex+1) + for index := 0; index <= invocationIndex; index++ { + name, ok := invocations[index] + if !ok { + continue + } + + names = append(names, name) + } + i.logf("exported list of invoked services: %v", names) return names From f2f2bcac596b80f35c86b27751e6d532604eb7fb Mon Sep 17 00:00:00 2001 From: dhernando Date: Mon, 14 Jul 2025 02:27:24 +0200 Subject: [PATCH 14/16] add slice and map types --- service_test.go | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/service_test.go b/service_test.go index 59b9d7a0..e5744b0c 100644 --- a/service_test.go +++ b/service_test.go @@ -36,6 +36,15 @@ func TestGenerateServiceName(t *testing.T) { name = generateServiceName[*int]() is.Equal("*int", name) + + name = generateServiceName[[]int]() + is.Equal("[]int", name) + + name = generateServiceName[map[string]string]() + is.Equal("map[string]string", name) + + name = generateServiceName[map[string]test]() + is.Equal("map[string]do.test", name) } func TestGenerateServiceNameWithFQSN(t *testing.T) { @@ -68,4 +77,10 @@ func TestGenerateServiceNameWithFQSN(t *testing.T) { name = generateServiceNameWithFQSN[*int]() is.Equal("*int", name) + + name = generateServiceNameWithFQSN[[]int]() + is.Equal("[]int", name) + + name = generateServiceNameWithFQSN[map[string]string]() + is.Equal("map[string]string", name) } From 9e258edf1a6177ce1e7f2b214db075b06508135f Mon Sep 17 00:00:00 2001 From: dhernando Date: Tue, 15 Jul 2025 06:59:01 +0200 Subject: [PATCH 15/16] add more test cases for service name generation --- service_test.go | 26 ++++++++++++++++++++++++-- 1 file changed, 24 insertions(+), 2 deletions(-) diff --git a/service_test.go b/service_test.go index e5744b0c..c028c261 100644 --- a/service_test.go +++ b/service_test.go @@ -10,7 +10,9 @@ func TestGenerateServiceName(t *testing.T) { is := assert.New(t) type MyFunc func(int) int - type itest interface {} + type itest interface { + Do() + } type test struct{} name := generateServiceName[test]() @@ -28,6 +30,9 @@ func TestGenerateServiceName(t *testing.T) { name = generateServiceName[func(int)int]() is.Equal("func(int) int", name) + name = generateServiceName[func(test)int]() + is.Equal("func(do.test) int", name) + name = generateServiceName[MyFunc]() is.Equal("do.MyFunc", name) @@ -40,6 +45,9 @@ func TestGenerateServiceName(t *testing.T) { name = generateServiceName[[]int]() is.Equal("[]int", name) + name = generateServiceName[[]test]() + is.Equal("[]do.test", name) + name = generateServiceName[map[string]string]() is.Equal("map[string]string", name) @@ -51,7 +59,9 @@ func TestGenerateServiceNameWithFQSN(t *testing.T) { is := assert.New(t) type MyFunc func(int) int - type itest interface {} + type itest interface { + Do() + } type test struct{} name := generateServiceNameWithFQSN[test]() @@ -69,6 +79,10 @@ func TestGenerateServiceNameWithFQSN(t *testing.T) { name = generateServiceNameWithFQSN[func(int)int]() is.Equal("func(int) int", name) + // funcs with custom types cost too much to generate FQSN (recursion), using type aliases should be recommended + name = generateServiceNameWithFQSN[func(test)int]() + is.Equal("func(do.test) int", name) + name = generateServiceNameWithFQSN[MyFunc]() is.Equal("github.com/samber/do.MyFunc", name) @@ -81,6 +95,14 @@ func TestGenerateServiceNameWithFQSN(t *testing.T) { name = generateServiceNameWithFQSN[[]int]() is.Equal("[]int", name) + // slices with custom types cost too much to generate FQSN (recursion), using type aliases should be recommended + name = generateServiceNameWithFQSN[[]test]() + is.Equal("[]do.test", name) + name = generateServiceNameWithFQSN[map[string]string]() is.Equal("map[string]string", name) + + // maps with custom types cost too much to generate FQSN (recursion), using type aliases should be recommended + name = generateServiceNameWithFQSN[map[string]test]() + is.Equal("map[string]do.test", name) } From bea46e9eb70a9bde3fbb17fa57df7931775f058f Mon Sep 17 00:00:00 2001 From: dhernando Date: Tue, 15 Jul 2025 23:43:53 +0200 Subject: [PATCH 16/16] add shutdown with context together with testing --- di.go | 19 +++++++ di_test.go | 138 +++++++++++++++++++++++++++++++++++++++++++++++ injector.go | 25 +++++++-- injector_test.go | 64 +++++++++++++++++++++- service.go | 10 ++++ service_eager.go | 13 +++++ service_lazy.go | 20 +++++++ 7 files changed, 285 insertions(+), 4 deletions(-) diff --git a/di.go b/di.go index ddfd95a0..b5a65ed7 100644 --- a/di.go +++ b/di.go @@ -1,6 +1,7 @@ package do import ( + "context" "fmt" ) @@ -142,3 +143,21 @@ func ShutdownNamed(i *Injector, name string) error { func MustShutdownNamed(i *Injector, name string) { must(getInjectorOrDefault(i).shutdownImplem(name)) } + +func ShutdownContext[T any](ctx context.Context, i *Injector) error { + name := generateServiceNameFromInjector[T](i) + return getInjectorOrDefault(i).shutdownContextImplem(ctx, name) +} + +func MustShutdownContext[T any](ctx context.Context, i *Injector) { + name := generateServiceNameFromInjector[T](i) + must(getInjectorOrDefault(i).shutdownContextImplem(ctx, name)) +} + +func ShutdownNamedContext(ctx context.Context, i *Injector, name string) error { + return getInjectorOrDefault(i).shutdownContextImplem(ctx, name) +} + +func MustShutdownNamedContext(ctx context.Context, i *Injector, name string) { + must(getInjectorOrDefault(i).shutdownContextImplem(ctx, name)) +} diff --git a/di_test.go b/di_test.go index 1395e8ef..36c3c68b 100644 --- a/di_test.go +++ b/di_test.go @@ -1,6 +1,7 @@ package do import ( + "context" "fmt" "testing" @@ -400,6 +401,143 @@ func TestMustShutdownNamed(t *testing.T) { }) } +type test struct { + waitForCtx bool +} + +func (t test) Shutdown(ctx context.Context) error { + if t.waitForCtx { + <-ctx.Done() + return ctx.Err() + } + + return nil +} + +func TestShutdownContext(t *testing.T) { + t.Run("context without cancellation returns nil", func(t *testing.T) { + is := assert.New(t) + + i := New() + + Provide(i, func(i *Injector) (test, error) { + return test{waitForCtx: false}, nil + }) + + is.NotPanics(func() { + MustInvoke[test](i) + }) + + ctx := context.Background() + err := ShutdownContext[test](ctx, i) + is.NoError(err) + + instance, err := Invoke[test](i) + is.Empty(instance) + is.Error(err) + }) + + t.Run("cancelled context returns an error", func(t *testing.T) { + is := assert.New(t) + + i := New() + + Provide(i, func(i *Injector) (test, error) { + return test{waitForCtx: true}, nil + }) + + is.NotPanics(func() { + MustInvoke[test](i) + }) + + ctx, cancel := context.WithCancel(context.Background()) + cancel() + err := ShutdownContext[test](ctx, i) + is.Error(err) + }) +} + +func TestMustShutdownContext(t *testing.T) { + t.Run("context without cancellation returns nil", func(t *testing.T) { + is := assert.New(t) + + i := New() + + Provide(i, func(i *Injector) (test, error) { + return test{waitForCtx: false}, nil + }) + + is.NotPanics(func() { + MustInvoke[test](i) + }) + + ctx := context.Background() + is.NotPanics(func() { + MustShutdownContext[test](ctx, i) + }) + }) + + t.Run("cancelled context returns an error", func(t *testing.T) { + is := assert.New(t) + + i := New() + + Provide(i, func(i *Injector) (test, error) { + return test{waitForCtx: true}, nil + }) + + is.NotPanics(func() { + MustInvoke[test](i) + }) + + ctx, cancel := context.WithCancel(context.Background()) + cancel() + is.Panics(func() { + MustShutdownContext[test](ctx, i) + }) + }) +} + +func TestShutdownContextNamed(t *testing.T) { + t.Run("cancelled context returns an error", func(t *testing.T) { + is := assert.New(t) + + i := New() + + ProvideNamedValue(i, "foobar", test{waitForCtx: true}) + + is.NotPanics(func() { + MustInvokeNamed[test](i, "foobar") + }) + + ctx, cancel := context.WithCancel(context.Background()) + cancel() + err := ShutdownNamedContext(ctx, i, "foobar") + is.Error(err) + + }) + + t.Run("context without cancellation returns nil", func(t *testing.T) { + is := assert.New(t) + + i := New() + + ProvideNamedValue(i, "foobar", test{waitForCtx: false}) + + is.NotPanics(func() { + MustInvokeNamed[test](i, "foobar") + }) + + ctx := context.Background() + err := ShutdownNamedContext(ctx, i, "foobar") + is.NoError(err) + + instance, err := InvokeNamed[test](i, "foobar") + is.Empty(instance) + is.Error(err) + }) +} + func TestDoubleInjection(t *testing.T) { is := assert.New(t) diff --git a/injector.go b/injector.go index 4a415b48..0957fb5f 100644 --- a/injector.go +++ b/injector.go @@ -1,6 +1,7 @@ package do import ( + "context" "fmt" "os" "os/signal" @@ -119,6 +120,10 @@ func (i *Injector) HealthCheck() map[string]error { } func (i *Injector) Shutdown() error { + return i.ShutdownContext(context.Background()) +} + +func (i *Injector) ShutdownContext(ctx context.Context) error { i.mu.RLock() invocations := invertMap(i.orderedInvocation) invocationIndex := i.orderedInvocationIndex @@ -132,7 +137,7 @@ func (i *Injector) Shutdown() error { continue } - err := i.shutdownImplem(name) + err := i.shutdownContextImplem(ctx, name) if err != nil { return err } @@ -193,6 +198,10 @@ func (i *Injector) healthcheckImplem(name string) error { } func (i *Injector) shutdownImplem(name string) error { + return i.shutdownContextImplem(context.Background(), name) +} + +func (i *Injector) shutdownContextImplem(ctx context.Context, name string) error { i.mu.Lock() serviceAny, ok := i.services[name] @@ -203,14 +212,24 @@ func (i *Injector) shutdownImplem(name string) error { i.mu.Unlock() - service, ok := serviceAny.(shutdownableService) + serviceWithCtx, ok := serviceAny.(shutdownableWithContextService) if ok { i.logf("requested shutdown for service %s", name) - err := service.shutdown() + err := serviceWithCtx.shutdownWithContext(ctx) if err != nil { return err } + } else { + service, ok := serviceAny.(shutdownableService) + if ok { + i.logf("requested shutdown for service %s", name) + + err := service.shutdown() + if err != nil { + return err + } + } } i.mu.Lock() diff --git a/injector_test.go b/injector_test.go index 17499c07..5d8d6577 100644 --- a/injector_test.go +++ b/injector_test.go @@ -1,6 +1,7 @@ package do import ( + "context" "fmt" "reflect" "testing" @@ -63,7 +64,6 @@ func TestInjectorListProvidedServices(t *testing.T) { }) } - func TestInjectorListProvidedServicesWithFQSN(t *testing.T) { type test struct{} is := assert.New(t) @@ -396,3 +396,65 @@ func TestInjectorCloneLazy(t *testing.T) { is.Equal(54, s2) is.Equal(3, count) } + +type testShutdownableWithCtx struct { + waitForCtx bool + CalledShutdown bool +} + +func (t *testShutdownableWithCtx) Shutdown(ctx context.Context) error { + t.CalledShutdown = true + if t.waitForCtx { + <-ctx.Done() + return ctx.Err() + } + + return nil +} + + +func TestInjectorShutdownContext(t *testing.T) { + is := assert.New(t) + + i := New() + first := &testShutdownableWithCtx{waitForCtx: true} + second := &testShutdownableWithCtx{waitForCtx: true} + ProvideNamedValue(i, "foobar", first) + ProvideNamedValue(i, "barfoo", second) + + is.NotPanics(func() { + MustInvokeNamed[*testShutdownableWithCtx](i, "foobar") + MustInvokeNamed[*testShutdownableWithCtx](i, "barfoo") + }) + + ctx, cancel := context.WithCancel(context.Background()) + cancel() + err := i.ShutdownContext(ctx) + is.Error(err) + + is.False(first.CalledShutdown) + is.True(second.CalledShutdown) +} + +func TestInjectorShutdownContextCallsEveryServicesShutdown(t *testing.T) { + is := assert.New(t) + + i := New() + first := &testShutdownableWithCtx{waitForCtx: false} + second := &testShutdownableWithCtx{waitForCtx: false} + ProvideNamedValue(i, "foobar", first) + ProvideNamedValue(i, "barfoo", second) + + is.NotPanics(func() { + MustInvokeNamed[*testShutdownableWithCtx](i, "foobar") + MustInvokeNamed[*testShutdownableWithCtx](i, "barfoo") + }) + + ctx, cancel := context.WithCancel(context.Background()) + cancel() + err := i.ShutdownContext(ctx) + is.Nil(err) + + is.True(first.CalledShutdown) + is.True(second.CalledShutdown) +} diff --git a/service.go b/service.go index 2b281f08..7fa4fac6 100644 --- a/service.go +++ b/service.go @@ -1,6 +1,7 @@ package do import ( + "context" "fmt" "reflect" ) @@ -10,6 +11,7 @@ type Service[T any] interface { getInstance(*Injector) (T, error) healthcheck() error shutdown() error + shutdownWithContext(context.Context) error clone() any } @@ -21,6 +23,10 @@ type shutdownableService interface { shutdown() error } +type shutdownableWithContextService interface { + shutdownWithContext(context.Context) error +} + func generateServiceNameFromInjector[T any](i *Injector) string { if i != nil && i.useFQSN { return generateServiceNameWithFQSN[T]() @@ -93,6 +99,10 @@ type Shutdownable interface { Shutdown() error } +type ShutdownableWithContext interface { + Shutdown(context.Context) error +} + type cloneableService interface { clone() any } diff --git a/service_eager.go b/service_eager.go index 4e27aaf6..c8c3321d 100644 --- a/service_eager.go +++ b/service_eager.go @@ -1,5 +1,7 @@ package do +import "context" + type ServiceEager[T any] struct { name string instance T @@ -40,6 +42,17 @@ func (s *ServiceEager[T]) shutdown() error { return nil } + +func (s *ServiceEager[T]) shutdownWithContext(ctx context.Context) error { + instance, ok := any(s.instance).(ShutdownableWithContext) + if ok { + return instance.Shutdown(ctx) + } + + return nil +} + + func (s *ServiceEager[T]) clone() any { return s } diff --git a/service_lazy.go b/service_lazy.go index b0e266f7..5f05310e 100644 --- a/service_lazy.go +++ b/service_lazy.go @@ -1,6 +1,7 @@ package do import ( + "context" "fmt" "sync" "sync/atomic" @@ -115,6 +116,25 @@ func (s *ServiceLazy[T]) shutdown() error { return nil } +func (s *ServiceLazy[T]) shutdownWithContext(ctx context.Context) error { + s.mu.Lock() + defer s.mu.Unlock() + + if !s.built { + return nil + } + + instance, ok := any(s.instance).(ShutdownableWithContext) + if ok { + return instance.Shutdown(ctx) + } + s.built = false + s.instance = empty[T]() + + + return nil +} + func (s *ServiceLazy[T]) clone() any { // reset `build` flag and instance return &ServiceLazy[T]{