samber / do Goto Github PK
View Code? Open in Web Editor NEW⚙️ A dependency injection toolkit based on Go 1.18+ Generics.
Home Page: https://pkg.go.dev/github.com/samber/do
License: MIT License
⚙️ A dependency injection toolkit based on Go 1.18+ Generics.
Home Page: https://pkg.go.dev/github.com/samber/do
License: MIT License
I used some other framework that could auto inject with struct tags.
Something like this:
type Bar interface {
DoSomething()
}
type Foo struct {
Bar `inject:""` // or we can use named value here `inject:"name"`
}
do.Provide(injector, func() ( Bar, err){...})
f, err := do.Invoke[Foo]() // f.Bar should be injected automatically
f.Bar.DoSomething()
Service register
func ProvideNamedValue[T any](i *Injector, name string, value T) {
func Provide[T any](i *Injector, provider Provider[T]) {
Service lookup
func InvokeNamed[T any](i *Injector, name string) (T, error) {
for these two group method, it can accept a nil Injector, in this case then it will use system default Injector.
can we supply another group of API without the ** Injector* parameter to make a simpler API?
Hi. Your example code shows us, that it is OK to define services by specifying factories and next get it from the container with the function do.MustInvoke[...](x)
.
Lets imagine we have following code (based on readme example)
func main() {
injector := do.New()
// provides CarService
do.Provide(injector, NewCarService)
// provides EngineService
do.Provide(injector, NewEngineService)
car := do.MustInvoke[*CarService](injector)
ch := make(chan struct{})
// run background goroutine with stop chan
go (func(){
for {
select {
case <-ch:
return
default:
car.Beep() // service may be already closed
time.Sleep(1s)
}
}
})()
injector.ShutdownOnSIGTERM() // will block until receiving sigterm signal
close(ch) // stop the goroutine, but only after container shutdown
}
Here we have no API to let the main()
code know that container is wanting to shutdown and await the reaction from main()
for this event BEFORE starting the shutdown. This may trigger misuse of the container by users.
Solutions.
main()
code which uses services spawned by the container. Instead, users should implement services with right Shutdown()
implementation to allow the container manage services life times. I.e. users should implement glue code services
.Shutdown
function for the main()
code, to allow the main()
stop working with the services before they starts shutdown.Debugging IoC is a pain.
An OTEL plugin listening to lifecycle events would be very nice.
Requires #73.
I imagine an API similar to this:
import (
"github.com/samber/do/v2"
"github.com/samber/do/observability/otel/v2"
)
injector := do.New()
otel.Listen(injector, ...) // <-- handling every available hooks
Hey! I have a problem. I have a config struct and want to provide some instances for my application.
main.go:
di := do.New()
do.Provide[*redis.Client](di, func(i *do.Injector) (*redis.Client, error) {
db, err := c.ConnectRedis()
if err != nil {
return nil, err
}
return db, nil
})
do.Provide[*bun.DB](di, func(i *do.Injector) (*bun.DB, error) {
db, err := c.ConnectPostgres()
if err != nil {
return nil, err
}
return db, nil
})
And the strange thing is that the Redis client was created successfully and I can invoke it, but the Postgres client instance is nil (found with debugger). What is the logic of that? Because I didn't understand and can't do the job, because some of my clients weren't built.
Example of invocation
redisClient := do.MustInvoke[*redis.Client](i) // ok, I can use it
postgresClient := do.MustInvoke[*bun.DB](i) // panic here and in debugger: built = false, instance = nil
Add such method:
injector.AddAfterRegistrationListener(...)
injector.RemoveAfterRegistrationListener(...)
These methods should support variadic parameters.
interface
(can't cast nil
)package main
import "github.com/samber/do/v2"
func main() {
type I interface{}
i := do.New()
do.ProvideValue(i, I("some"))
do.MustInvokeAs[I](i)
}
// panic: DI: could not find service `*main.I`, available services: `*main.I`
package main
import (
"fmt"
"github.com/samber/do/v2"
)
type I interface{ Name() string }
type i struct{}
func (i) Name() string { return "EXPECTED" }
type other struct{}
func (other) Other() {}
func (other) Name() string { return "OTHER" }
// type
func main() {
scope := do.New()
do.ProvideValue(scope, other{})
do.ProvideValue(scope, i{})
for range 10 {
fmt.Println(do.MustInvokeAs[I](scope).Name())
}
// output: something like
// OTHER
// OTHER
// OTHER
// EXPECTED
// OTHER
// OTHER
// OTHER
// EXPECTED
// OTHER
// OTHER
}
I create a function that would temporarily delete InvokedAs result by OverrideNamedValue and tried to check the next not exist.
But I came across both 1 problem and samber/go-type-to-string#2
My monolith application relies a lot on do.Shutdownable
interface. Some tasks call database or third-parties, and flush buffer.
I would like to measure the time of the do.Shutdown
processing.
API proposal: duration, err := do.Shutdown()
duration
would be either a time.Duration
or:
type ShutdownDuration struct {
All time.Duration
Services map[string]time.Duration
}
WDYT ?
Today we only keep an ordered list of service inception.
It would be awesome to keep track of a real dependency graph in a doubly-linked list or anything similar.
Other improvements:
provide()
time)How to pass structs as inputs for those services.
We only have ProvideNamedValues which allows only strings
Hello, thanks for your great works.
here the specs :
github.com/samber/do v1.2.0
go 1.18
Maybe I don't understand something ?
I tried to generate gin router and retrieved it to run the server.
But it doesn't work :'(
Here what I tried :
package main
import (
"github.com/gin-gonic/gin"
glog "github.com/magicsong/color-glog"
"github.com/samber/do"
)
func generateContainer() *do.Injector {
injector := do.New()
do.ProvideNamedValue(injector, "main-router", gin.Default())
return injector
}
func main() {
container := generateContainer()
router := do.MustInvokeNamed[gin.Engine](container, "main-router")
if err := router.Run(); err != nil {
glog.Fatal(err)
}
}
And the error :
panic: DI: could not find service `main-router`, available services: `main-router`
Is it possible to have some help.
Demo(use v2):
func TestInvokeAny(t *testing.T) {
do.ProvideNamed(nil, "test", func(i do.Injector) (int, error) {
return 1, nil
})
int1, err := do.InvokeNamed[int](nil, "test")
if err != nil {
t.Errorf("invoke error: %v", err)
return
}
if int1 != 1 {
t.Errorf("invoke response error: %v", int1)
return
}
int2, err := do.InvokeNamed[any](nil, "test")
if err != nil {
t.Errorf("invoke error: %v", err)
return
}
t.Log(int2)
}
do.InvokeNamed[any](nil, "test")
not work
Why do I have to do this?
I want to generic call my local service, I just known service name
localService, err := do.InvokeNamed[any](nil, "ServiceName")
if err != nil {
return nil, err
}
methodValue := reflect.ValueOf(localService).MethodByName("FuncName")
invokeAnyByName
Is what I want, can you Export It ?
thank you
func OverrideTransient[T any](i Injector, provider Provider[T]) {
name := inferServiceName[T]()
OverrideNamed[T](i, name, provider)
}
Then the instances when calling Invoke
multiple times are always identical.
We should be able to add multiple callbacks for each hook:
type InjectorOpts struct {
HookAfterRegistration []func(scope *Scope, serviceName string)
HookAfterShutdown []func(scope *Scope, serviceName string)
Logf []func(format string, args ...any)
}
instead of
type InjectorOpts struct {
HookAfterRegistration func(scope *Scope, serviceName string)
HookAfterShutdown func(scope *Scope, serviceName string)
Logf func(format string, args ...any)
}
Requirement for #70
1: Does this framework ensure singleton instance?
2: Support service lookup:
There is scenario that we just want to get a service when in a function call but I dont want to new a new instance of the target service.
in java ecosystem the famous springboot support above features.
I am choosing a DI framework for my project and find I need above two features. the case is
** source code**
interface A {
M1()
M2()
}
var _ A = (*ImplA)(nil)
struct ImplA {
}
func NewImplaA() {
}
....
but right now I want to test another function which depends on the interface(ImplA) with Mock. if this framework can supply above features than I can inject a mocked implementation and then the tested method can looup the mock object.
I want to know if we has a construct function, how to make it be auto injected?
Should i always code like this bellow?
do.Provide(builder, func(i *do.Injector) (*appcmd.CmdUser, error) {
db := do.MustInvoke[trans.ItfDB](i)
log := do.MustInvoke[logger.ItfLogger](i)
userRepo := do.MustInvoke[reposititf.ItfRepoUser](i)
accountRepo := do.MustInvoke[reposititf.ItfRepoAccount](i)
publisher := do.MustInvoke[publisheritf.ItfPublisher](i)
productRemote := do.MustInvokeNamed[clientitf.ItfProxyProduct](i, "productRemote")
productLocal := do.MustInvokeNamed[clientitf.ItfProxyProduct](i, "productLocal")
return appcmd.NewCmdUser(db, log, userRepo, accountRepo, publisher, productRemote, productLocal), nil
})
func NewCmdUser(
db trans.ItfDB,
log logger.ItfLogger,
userRepo reposititf.ItfRepoUser,
accoutRepo reposititf.ItfRepoAccount,
pub publisheritf.ItfPublisher,
productRemote clientitf.ItfProxyProduct,
productLocal clientitf.ItfProxyProduct) *CmdUser {
return &CmdUser{
db: db,
userRepo: userRepo,
accountRepo: accoutRepo,
log: log,
publisher: pub,
productRemoteProxy: productRemote,
productLocalProxy: productLocal,
}
}
func (s *serviceEager[T]) getInstance(i Injector) (T, error) {
frame, ok := stacktrace.NewFrameFromCaller()
if ok {
s.mu.Lock()
s.invokationFrames = append(s.invokationFrames, frame) // @TODO: potential memory leak
s.mu.Unlock()
}
return s.instance, nil
}
above code is from https://github.com/samber/do/blob/v2-%F0%9F%9A%80/service_eager.go,
right now I am using do by generate a global singleton scope and get the service on demand. but there is a call to get call
stack everytime I try to get the instnace
frame, ok := stacktrace.NewFrameFromCaller(), in fact this methos is not exported just for internal usagae. It will decrease the system performance. is there way to optimize the design?
Currently, we listen to the following events:
I would add:
I have a constructor that returns a concrete type which implements more than one interface and I would like to register the same instance as providing multiple interfaces.
In dig, it is possible using dig.As().
Would you be interested in a PR that adds standard http
based middleware? The ideal is that it provides middleware that adds the injector
to the request context and then, in each request, you can quickly fetch not just the injector
instance itself but also request specific services out of the injector while handling requests. Seems like it would be generally useful for anybody doing web development using do
and it doesn't add any dependencies since it is based on the standard library.
I've written such a thing and it would be useful to include here since otherwise I'd have to make a tiny package and publish it just to reuse it across projects.
Hey, great work on the lib!
Any chance you have plans to support a concept similar to groups in Dig? (aka, registering a bunch of things of the same type, and asking for the list later). Pretty useful to do something like register a bunch of Routes associated with controllers in a DI container and then asking for all the routes to register them. That is super useful to decouple route creation, controller creation and servers for example.
Hi! I am doing some research on DI for Golang and I find this library to be the perfect middle ground of simplicity, efficiency and static stability. There's one missing feature that at the moment doesn't seem to be possible (correct me please if I'm wrong).
There's no way right now to register multiple implementation of the same interface and then being able to retrieve all of them.
I come from Java, where with Springboot, you can inject a Collection<IService>
.
Would that make sense to do it here too?
If you wondering "ok, but how do you handle the case of having 2 implementation of the same interface but your struct only needs one?"
In Springboot in this case you would have an error, because the framework wouldn't know which one to inject.. And we could have the same here too.
package main
import "github.com/samber/do/v2"
type Type struct {
int
uint64
string
}
func NewType(i int, u uint64, s string) Type {
return Type{i, u, s}
}
func main() {
scope := do.New()
do.ProvideValue(scope, int(1))
do.ProvideValue(scope, uint64(2))
do.ProvideValue(scope, string("str"))
// instead
do.Provide(scope, func(i do.Injector) (Type, error) {
return NewType(
do.MustInvoke[int](i),
do.MustInvoke[uint64](i),
do.MustInvoke[string](i),
), nil
})
// something like
do.ProvideAny[Type](scope, NewType)
_ = do.MustInvoke[Type](scope)
}
Where
func ProvideAny[T any](i do.Injector, fun any) {
fn := reflect.ValueOf(fun)
// check that fn is func(...) T or func(...) (T, error)
do.Provide[T](i, func(i do.Injector) (res T, err error) {
inputTypes := []reflect.Value{}
// inputTypes - invoked input by type name
out := fn.Call(inputTypes)
// res = out[0]
// err = out[1]
return
})
}
Also the function can have variadic names ... string
With a match for each function parameter, to use it instead of the type name.
Analog do:"name"
in struct
If interested I can send pull request.
We can discuss the interface.
Now it just
func ToProvider[T any](fun any) do.Provider[T]
do.Provide(scope, ToProvider[Type](NewType))
First, this is a amazing project.
But now, both Provide and ProvideValue only support singleton
likely mode, for Provide
which only create value when first invoked. In sometimes, it's useful to create a new instance whenever it invoked.
I have Java background , in springboot there is a interface named InitializingBean this interface give an opportunity to do some one-time initialization. We can introduce similar interface, service implement this interface will call the method automatically once after the service creation.
Hello. Thank you for this package. I've tried it on my project with grpc client and i wanted to close client connection after injector shutting down. The key problem is that grpc client doesn't implements do.Shutdownable
by default, so i needed to wrap it in my struct. But. I found that this connection not shutting down.
I wrote some simple code, which visualises that service A
will never shutdown.
package main
import (
"log"
"github.com/samber/do"
)
type ServiceA struct{}
// doesn't have Shutdown method, so, we need make it shutdownable
func (*ServiceA) Close() error {
return nil
}
type ServiceB struct{}
func (*ServiceB) Shutdown() error {
log.Println("Shutted down B")
return nil
}
type ServiceC struct{}
func (*ServiceC) Shutdown() error {
log.Println("Shutted down C")
return nil
}
// Simple, ok, because it shutdownable
func NewServiceC(i *do.Injector) (*ServiceC, error) {
return &ServiceC{}, nil
}
func NewServiceB(i *do.Injector) (*ServiceB, error) {
return &ServiceB{}, nil
}
// Make shutdownable service A
type ShutdownableServiceA struct {
a *ServiceA
}
// implemenint do.Shutdownable
func (v *ShutdownableServiceA) Shutdown() error {
return v.a.Close()
}
// Custruct shutdownable
func MakeShutdownableServiceA(a *ServiceA) do.Provider[*ShutdownableServiceA] {
return func(i *do.Injector) (*ShutdownableServiceA, error) {
return &ShutdownableServiceA{
a: a,
}, nil
}
}
func main() {
injector := do.New()
do.Provide(injector, NewServiceC)
do.Provide(injector, MakeShutdownableServiceA(&ServiceA{}))
do.Provide(injector, NewServiceB)
do.MustInvoke[*ServiceC](injector)
do.MustInvoke[*ShutdownableServiceA](injector)
do.MustInvoke[*ServiceB](injector)
injector.Shutdown()
}
Result:
2022/05/28 16:01:05 Shutted down B
2022/05/28 16:01:05 Shutted down C
I am not sure if this is a problem or a feature, but invoking dependencies after shutdown creates a race condition (runnable code at https://goplay.tools/snippet/vIQ3Pwu4lw1):
package main
import (
"log"
"sync"
"time"
"github.com/samber/do"
)
type Service struct{}
func main() {
var wg sync.WaitGroup
i := do.New()
do.Provide(i, func(injector *do.Injector) (*Service, error) {
return &Service{}, nil
})
// uncommenting the line below gives an error: `Invoking after shutdown error: DI: could not find service `*main.Service`, available services:`
//_ = do.MustInvoke[*Service](i)
wg.Add(1)
go func() {
defer wg.Done()
time.Sleep(time.Second * 1)
_, err := do.Invoke[*Service](i)
if err != nil {
log.Printf("Invoking after shutdown error: %v\n", err)
return
}
}()
err := i.Shutdown()
if err != nil {
log.Printf("Shutdown error: %v\n", err)
}
wg.Wait()
log.Println("Finished")
}
When running with go run -race .
it detects a race condition:
==================
WARNING: DATA RACE
Write at 0x00c0000b60c8 by goroutine 6:
github.com/samber/do.(*Injector).onServiceInvoke()
/Users/ivand/go/pkg/mod/github.com/samber/[email protected]/injector.go:278 +0x120
github.com/samber/do.InvokeNamed[...]()
/Users/ivand/go/pkg/mod/github.com/samber/[email protected]/di.go:128 +0xc8
github.com/samber/do.Invoke[...]()
/Users/ivand/go/pkg/mod/github.com/samber/[email protected]/di.go:101 +0x6c
main.main.func2()
/Users/ivand/IdeaProjects/untitled1/main.go:32 +0x7c
Previous read at 0x00c0000b60c8 by main goroutine:
github.com/samber/do.(*Injector).Shutdown()
/Users/ivand/go/pkg/mod/github.com/samber/[email protected]/injector.go:124 +0x1b0
main.main()
/Users/ivand/IdeaProjects/untitled1/main.go:39 +0x130
Goroutine 6 (running) created at:
main.main()
/Users/ivand/IdeaProjects/untitled1/main.go:27 +0x128
==================
2023/04/28 12:55:27 Finished
Found 1 data race(s)
exit status 66
Is this the intended way it supposed to work? Is the shutdown procedure expected to be a final call with no invoking of services after it?
I haven't dug deeply in the source code, but apparently the shutdown procedure does not operate under the write lock. Is there a particular reason for that?
Currently when Injector.Shutdown
receives an error while calling Shutdown
on a service, it aborts and never calls Shutdown
on the following services. It would be nice to have it behave like Injector.HealthCheck
were all services are called regardless of their returned errors.
Hello, I would like to test that all services are wired correctly inside my DIC and also warm up when booting the app.
I would like to do something like this
func TestAllServicesInDI(t *testing.T) {
injector := do.New()
do.ProvideNamedValue(injector, "first", 1)
do.ProvideNamedValue(injector, "second", 2)
do.ProvideNamedValue(injector, "third", 3)
do.ProvideNamedValue(injector, "string", "Hello World!")
for _, v := range injector.ListProvidedServices() {
_, err := do.InvokeNamed[any](injector, v)
assert.NilError(t, err)
}
}
It will fail with assertion failed: error is not nil: DI: could not find service "third", available services: "third", "string", "first", "second"
The easiest way how to accomplish this would be to add InvokeNamedUntyped(i, name) or something that would return any but invoke the service. WDYT?
I thought there was some new hack to implement dependency injection without reflection and only through generics. A quick look at the code revealed that this was a lie.
// service.go:27
func generateServiceName[T any]() string {
var t T
// struct
name := fmt.Sprintf("%T", t) // <-- Here you use reflection !!!
if name != "<nil>" {
return name
}
// interface
return fmt.Sprintf("%T", new(T))
}
Is it possible to allow a func before panic from DI functions such as Must*?
Basically, to log out the service name that caused panic.
3 repositories:
Services:
Use existing template:
right now life cycle hook can only be set when build a container as below
`injector := do.NewWithOpts(&do.InjectorOpts{
HookAfterRegistration: func(injector *do.Injector, serviceName string) {
fmt.Printf("Service registered: %s\n", serviceName)
},
HookAfterShutdown: func(injector *do.Injector, serviceName string) {
fmt.Printf("Service stopped: %s\n", serviceName)
},
Logf: func(format string, args ...any) {
log.Printf(format, args...)
},
})`
my case is that I am using do as DI container build some basic infra in the group for other projects. it's obvious that other teams(projects) also need hooks for project specific design. I export the DI but they can not register hook.
proposal:
if HookAfterRegistration and HookAfterShutdown are slcie of functions and di expose a new method to add a new hook method then it will work in my case
of course there is gotcha: this new added method which for adding hook will throw error when the DI is not emtpy
thank you!
AfterRegistration
and AfterShutdown
are currently defined globally.
It would be nice to listen events at the scope level.
In a big project, having tons of libs and modules, a developer should be able to create listeners locally, in its own scope.
Question: do we need to listen to events coming from the current scope or events coming from children as well?
Lazy services keep track of build time.
It would be very nice to be able to check what service takes the most time to load.
When a service is not found in Invoke/MustInvoke/InvokeNamed
, serviceNotFound
is called
func (i *Injector) serviceNotFound(name string) error {
// @TODO: use the Keys+Map functions from `golang.org/x/exp/maps` as
// soon as it is released in stdlib.
servicesNames := keys(i.services)
servicesNames = mAp(servicesNames, func(name string) string {
return fmt.Sprintf("`%s`", name)
})
return fmt.Errorf("DI: could not find service `%s`, available services: %s", name, strings.Join(servicesNames, ", "))
}
which calls keys(i.services)
. keys
performs a read on i.services
but serviceNotFound
hasn't acquired the mutex for reading. This trips the race detector in some of my tests.
Solution is probably to just acquire the mutex for reading at the beginning of either serviceNotFound
or InvokeNamed
.
data race:
WARNING: DATA RACE
Write at 0x00c000f51440 by goroutine 20:
runtime.mapaccess2_faststr()
/opt/homebrew/Cellar/go/1.21.1/libexec/src/runtime/map_faststr.go:108 +0x42c
github.com/samber/do.(*Injector).set()
/Users/devin/go/pkg/mod/github.com/samber/[email protected]/injector.go:239 +0xa4
github.com/samber/do.ProvideValue[go.shape.interface { Sleep(time.Duration) }]()
/Users/devin/go/pkg/mod/github.com/samber/[email protected]/di.go:42 +0x188
# proprietary code which calls ProvideValue
Previous read at 0x00c000f51440 by goroutine 21:
github.com/samber/do.keys[go.shape.string,go.shape.interface {}]()
/Users/devin/go/pkg/mod/github.com/samber/[email protected]/utils.go:14 +0x54
github.com/samber/do.(*Injector).serviceNotFound()
/Users/devin/go/pkg/mod/github.com/samber/[email protected]/injector.go:264 +0x34
github.com/samber/do.InvokeNamed[go.shape.interface { Sleep(time.Duration) }]()
/Users/devin/go/pkg/mod/github.com/samber/[email protected]/di.go:115 +0x20c
github.com/samber/do.Invoke[go.shape.interface { Sleep(time.Duration) }]()
/Users/devin/go/pkg/mod/github.com/samber/[email protected]/di.go:101 +0x6c
# proprietary code which calls Invoke
Is it possible to do an Invoke of the providers corresponding to the interface and get them as a slice? And it would be great if they were initialized at the moment of such Invoke.
func (s *ServiceLazy[T]) shutdown() error {
instance, ok := any(s.instance).(Shutdownable)
if ok {
return instance.Shutdown()
}
s.built = false
s.instance = empty[T]()
return nil
}
Is it necessary for ‘shutdown’ not return, continue to set the 'build' and 'instance' field?
Hi, thanks for the great library.
I'm wondering if anonymous invocation is possible because I'm following Go proverbs with accept interfaces, return structs
.
What I am expecting is that:
A declarative, efficient, and flexible JavaScript library for building user interfaces.
🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
An Open Source Machine Learning Framework for Everyone
The Web framework for perfectionists with deadlines.
A PHP framework for web artisans
Bring data to life with SVG, Canvas and HTML. 📊📈🎉
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
Some thing interesting about web. New door for the world.
A server is a program made to process requests and deliver data to clients.
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
Some thing interesting about visualization, use data art
Some thing interesting about game, make everyone happy.
We are working to build community through open source technology. NB: members must have two-factor auth.
Open source projects and samples from Microsoft.
Google ❤️ Open Source for everyone.
Alibaba Open Source for everyone
Data-Driven Documents codes.
China tencent open source team.