Giter Site home page Giter Site logo

samber / do Goto Github PK

View Code? Open in Web Editor NEW
1.7K 22.0 65.0 1.84 MB

⚙️ A dependency injection toolkit based on Go 1.18+ Generics.

Home Page: https://pkg.go.dev/github.com/samber/do

License: MIT License

Makefile 3.19% Go 96.81%
di go golang container dependency dependency-graph generics graceful-shutdown healthcheck injection

do's Introduction

do - Dependency Injection

tag Go Version GoDoc Build Status Go report Coverage License

⚙️ A dependency injection toolkit based on Go 1.18+ Generics.

This library implements the Dependency Injection design pattern. It may replace the uber/dig fantastic package in simple Go projects. samber/do uses Go 1.18+ generics and therefore offers a typesafe API.

See also:

  • samber/lo: A Lodash-style Go library based on Go 1.18+ Generics
  • samber/mo: Monads based on Go 1.18+ Generics (Option, Result, Either...)

Why this name?

I love short name for such a utility library. This name is the sum of DI and Go and no Go package currently uses this name.

⭕⭕⭕⭕⭕⭕ About v2 ⭕⭕⭕⭕⭕⭕

Check out the beta now!

go get -u github.com/samber/do/[email protected]

Documentation: https://do.samber.dev/

Please report bugs here: #45.

💡 Features

  • Service registration
  • Service invocation
  • Service health check
  • Service shutdown
  • Service lifecycle hooks
  • Named or anonymous services
  • Eagerly or lazily loaded services
  • Dependency graph resolution
  • Default injector
  • Injector cloning
  • Service override
  • Lightweight, no dependencies
  • No code generation

🚀 Services are loaded in invocation order.

🕵️ Service health can be checked individually or globally. Services implementing do.Healthcheckable interface will be called via do.HealthCheck[type]() or injector.HealthCheck().

🛑 Services can be shutdowned properly, in back-initialization order. Services implementing do.Shutdownable interface will be called via do.Shutdown[type]() or injector.Shutdown().

🚀 Install

go get github.com/samber/do@v1

This library is v1 and follows SemVer strictly.

No breaking changes will be made to exported APIs before v2.0.0.

This library has no dependencies except the Go std lib.

💡 Quick start

You can import do using:

import (
    "github.com/samber/do"
)

Then instantiate services:

func main() {
    injector := do.New()

    // provides CarService
    do.Provide(injector, NewCarService)

    // provides EngineService
    do.Provide(injector, NewEngineService)

    car := do.MustInvoke[*CarService](injector)
    car.Start()
    // prints "car starting"

    do.HealthCheck[EngineService](injector)
    // returns "engine broken"

    // injector.ShutdownOnSIGTERM()    // will block until receiving sigterm signal
    injector.Shutdown()
    // prints "car stopped"
}

Services:

type EngineService interface{}

func NewEngineService(i *do.Injector) (EngineService, error) {
    return &engineServiceImplem{}, nil
}

type engineServiceImplem struct {}

// [Optional] Implements do.Healthcheckable.
func (c *engineServiceImplem) HealthCheck() error {
	return fmt.Errorf("engine broken")
}
func NewCarService(i *do.Injector) (*CarService, error) {
    engine := do.MustInvoke[EngineService](i)
    car := CarService{Engine: engine}
    return &car, nil
}

type CarService struct {
	Engine EngineService
}

func (c *CarService) Start() {
	println("car starting")
}

// [Optional] Implements do.Shutdownable.
func (c *CarService) Shutdown() error {
	println("car stopped")
	return nil
}

🤠 Spec

GoDoc: https://godoc.org/github.com/samber/do

Injector:

Service registration:

Service invocation:

Service override:

Injector (DI container)

Build a container for your components. Injector is responsible for building services in the right order, and managing service lifecycle.

injector := do.New()

Or use nil as the default injector:

do.Provide(nil, func (i *Injector) (int, error) {
    return 42, nil
})

service := do.MustInvoke[int](nil)

You can check health of services implementing func HealthCheck() error.

type DBService struct {
    db *sql.DB
}

func (s *DBService) HealthCheck() error {
    return s.db.Ping()
}

injector := do.New()
do.Provide(injector, ...)
do.Invoke(injector, ...)

statuses := injector.HealthCheck()
// map[string]error{
//   "*DBService": nil,
// }

De-initialize all compoments properly. Services implementing func Shutdown() error will be called synchronously in back-initialization order.

type DBService struct {
    db *sql.DB
}

func (s *DBService) Shutdown() error {
    return s.db.Close()
}

injector := do.New()
do.Provide(injector, ...)
do.Invoke(injector, ...)

// shutdown all services in reverse order
injector.Shutdown()

List services:

type DBService struct {
    db *sql.DB
}

injector := do.New()

do.Provide(injector, ...)
println(do.ListProvidedServices())
// output: []string{"*DBService"}

do.Invoke(injector, ...)
println(do.ListInvokedServices())
// output: []string{"*DBService"}

Service registration

Services can be registered in multiple way:

  • with implicit name (struct or interface name)
  • with explicit name
  • eagerly
  • lazily

Anonymous service, loaded lazily:

type DBService struct {
    db *sql.DB
}

do.Provide[DBService](injector, func(i *Injector) (*DBService, error) {
    db, err := sql.Open(...)
    if err != nil {
        return nil, err
    }

    return &DBService{db: db}, nil
})

Named service, loaded lazily:

type DBService struct {
    db *sql.DB
}

do.ProvideNamed(injector, "dbconn", func(i *Injector) (*DBService, error) {
    db, err := sql.Open(...)
    if err != nil {
        return nil, err
    }

    return &DBService{db: db}, nil
})

Anonymous service, loaded eagerly:

type Config struct {
    uri string
}

do.ProvideValue[Config](injector, Config{uri: "postgres://user:pass@host:5432/db"})

Named service, loaded eagerly:

type Config struct {
    uri string
}

do.ProvideNamedValue(injector, "configuration", Config{uri: "postgres://user:pass@host:5432/db"})

Service invocation

Loads anonymous service:

type DBService struct {
    db *sql.DB
}

dbService, err := do.Invoke[DBService](injector)

Loads anonymous service or panics if service was not registered:

type DBService struct {
    db *sql.DB
}

dbService := do.MustInvoke[DBService](injector)

Loads named service:

config, err := do.InvokeNamed[Config](injector, "configuration")

Loads named service or panics if service was not registered:

config := do.MustInvokeNamed[Config](injector, "configuration")

Individual service healthcheck

Check health of anonymous service:

type DBService struct {
    db *sql.DB
}

dbService, err := do.Invoke[DBService](injector)
err = do.HealthCheck[DBService](injector)

Check health of named service:

config, err := do.InvokeNamed[Config](injector, "configuration")
err = do.HealthCheckNamed(injector, "configuration")

Individual service shutdown

Unloads anonymous service:

type DBService struct {
    db *sql.DB
}

dbService, err := do.Invoke[DBService](injector)
err = do.Shutdown[DBService](injector)

Unloads anonymous service or panics if service was not registered:

type DBService struct {
    db *sql.DB
}

dbService := do.MustInvoke[DBService](injector)
do.MustShutdown[DBService](injector)

Unloads named service:

config, err := do.InvokeNamed[Config](injector, "configuration")
err = do.ShutdownNamed(injector, "configuration")

Unloads named service or panics if service was not registered:

config := do.MustInvokeNamed[Config](injector, "configuration")
do.MustShutdownNamed(injector, "configuration")

Service override

By default, providing a service twice will panic. Service can be replaced at runtime using do.Override helper.

do.Provide[Vehicle](injector, func (i *do.Injector) (Vehicle, error) {
    return &CarImplem{}, nil
})

do.Override[Vehicle](injector, func (i *do.Injector) (Vehicle, error) {
    return &BusImplem{}, nil
})

Hooks

2 lifecycle hooks are available in Injectors:

  • After registration
  • After shutdown
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...)
    },
})

Cloning injector

Cloned injector have same service registrations as it's parent, but it doesn't share invoked service state.

Clones are useful for unit testing by replacing some services to mocks.

var injector *do.Injector;

func init() {
    do.Provide[Service](injector, func (i *do.Injector) (Service, error) {
        return &RealService{}, nil
    })
    do.Provide[*App](injector, func (i *do.Injector) (*App, error) {
        return &App{i.MustInvoke[Service](i)}, nil
    })
}

func TestService(t *testing.T) {
    i := injector.Clone()
    defer i.Shutdown()

    // replace Service to MockService
    do.Override[Service](i, func (i *do.Injector) (Service, error) {
        return &MockService{}, nil
    }))

    app := do.Invoke[*App](i)
    // do unit testing with mocked service
}

🛩 Benchmark

// @TODO

🤝 Contributing

Don't hesitate ;)

With Docker

docker-compose run --rm dev

Without Docker

# Install some dev dependencies
make tools

# Run tests
make test
# or
make watch-test

👤 Contributors

Contributors

💫 Show your support

Give a ⭐️ if this project helped you!

GitHub Sponsors

📝 License

Copyright © 2022 Samuel Berthe.

This project is MIT licensed.

do's People

Contributors

aflybird0 avatar dezren39 avatar graveland avatar greyxor avatar hesining avatar ilharp avatar maddiesch avatar rainydew avatar samber avatar suzuki-safie avatar syuparn avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

do's Issues

Feature request: service lookup & Singleton

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.

V2: Bug in parallel shutdown

I'm opening an issue to investigate a bug from @GreyXor, shared in #45.

I have two services, Config and Event. Where Event is a dependency of Config.

type ConfigService struct {
	Event *event.Service
}

In my Config's shutdown method, I calling anEvent's method.

With v2.0.0-beta.3, it's working as expected because the actual reverse order is respected.
With v2.0.0-beta.5, it's randomly not working because Event is randomly Shutdown before Config. (because of the async)

@GreyXor I was not able to replicate this bug. Can you write a demo?

FYI, I just added a commit to v2 for supporting shutdown on circular dependencies.

Service register best practices

I have been using do for a while. But I have a confused about service register

a typical service:

type Service struct {
	meegoMaterial *material.MeegoMaterial[material.MeegoMeta] `do:""`
	docMaterial   *material.DocMaterial[material.DocMeta]     `do:""`
	anyMaterial   *material.AnyMaterial[any]                  `do:""`
}

func NewService() (*Service, error) {
	return di.InvokeStruct[Service]()
}

And I add service.Register() function in main.go.

func Register() {
	di.Provide(material.NewMeegoMaterial)
	di.Provide(material.NewAnyMaterial)
	di.Provide(material.NewDocMaterial)

        di.Provide(report.NewService)
}

It works, But when I testing report:

func init() {
	service.Register()
}

func TestService_Report(t *testing.T) {

}

Circular dependencies error:

# ***/biz/service/report
package ***/biz/service/report
	imports ***/biz/service
	imports ***/biz/service/report: import cycle not allowed in test

I register service in test init func without report now.

func init() {
	di.Provide(material.NewMeegoMaterial)
	di.Provide(material.NewDocMaterial)
	di.Provide(objectstorage.NewService)
	di.Provide(material.NewAnyMaterial)
}

But I think it is ugly, Is there a better way? all in one service.Register() maybe no need to exist?

Of course, this is not do is problem, It should be my problem with using and understanding.

Warm up the container

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?

shutdown all services even on error

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.

feature request: auto injection with struct tags

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()

[Feature Request] Method aliases skipping "injector" argument (default injector)

  • 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?

How can I InvokeNamed T to any

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

image

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

Can we do auto inject?

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,
	}
}

Proposal: way to automate invoke deps in do.Provider for any type constructor

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))

Invoke returns panic because of nil instance

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

Is there an equivalent to dig.As() ?

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().

Middleware

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.

Proposal: allow service providers registered in the root scope to be invoked with values and other providers in the scoped injector

Thanks for the framework, which provides perfect experience on dependency injection in my application.

However when I'm using the framework, I find that the providers registered in the root/parent scope will only be invoked in the registered scope. For example the test case blow:

type SomeService struct {
	someRuntimeVal int
}

func ProvideSomeService(injector do.Injector) (*SomeService, error) {
	val := do.MustInvokeNamed[int](injector, "someVal")
	return &SomeService{
		someRuntimeVal: val,
	}, nil
}

func TestRuntimeInvoke(t *testing.T) {
	rootInjector := do.DefaultRootScope
	do.Provide(rootInjector, ProvideSomeService)

	runtimeInjector := rootInjector.Clone().Scope("runtime")
	do.ProvideNamedValue(runtimeInjector, "someVal", 100)
	service, err := do.Invoke[*SomeService](runtimeInjector)
	if err != nil {
		t.Errorf("Failed to invoke service: %v", err)
	} else if service.someRuntimeVal != 100 {
		t.Errorf("Wrong instance initialization: expected: 100, actual: %v", service.someRuntimeVal)
	}
}

I would expect the service provider can be injected in the root scope, and can be invoked correctly in each forked scope, with correct runtime values. In current framework, the test case will fail when invoking *SomeService, with error message:

Failed to invoke service: DI: could not find service someVal, available services: *SomeService, path: *SomeService -> someVal

I read the code and find that when creating scopes, the services are not deep copied to the scoped injector, so the child scopes are using the same service instance in the parent scope. It's the same with Injector.Clone() method since it also only shadow copies the services. It would be best if each injector can have its own provider management namespace and invoke the owning and inheriting providers independently.

Add telemetry, OTEL, logger...

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

Improvements on dependency graph

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:

  • circular DI detection (at provide() time)
  • print dependency graph
  • shutdown a single graph branch
  • provide then invoke a group of services needed for a larger module

Add support of Value Groups

@matdurand :

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.

https://uber-go.github.io/fx/value-groups/

proposal: Add factory provide mode

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.

Not all services shutting down

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

Transform hooks into slices of hooks

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

Add hook at the scope level

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?

Add TypeMismatch instead NotFound invoke error

package main

import (
	"fmt"

	"github.com/samber/do/v2" // v2.0.0-beta.6
)

func main() {
	i := do.New()

	do.ProvideNamedValue(i, "NAME", 1)
	fmt.Println(do.InvokeNamed[any](i, "NAME"))
	// DI: could not find service `NAME`, available services: `NAME`
}

do/invoke.go

Lines 84 to 87 in 6a1325b

service, ok := serviceAny.(Service[T])
if !ok {
return empty[T](), serviceNotFound(injector, ErrServiceNotFound, invokerChain)
}

Could not find service `main-router`, available services: `main-router`

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.

Is it true that this library doesn't use the reflection package? You lied.

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))
}

Data race: unsynchronized read of `Injector.services` in `serviceNotFound`

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

Support `interface` in `As`/`InvokeAs`

I suggest using something like:

func canCast[FROM, TO any](soft bool) bool {
	var from FROM
	anyFrom := any(from)

	if anyFrom == nil { // check for interface by reflect
		typeFrom := reflect.TypeOf(&from).Elem()
		typeTo := reflect.TypeOf((*TO)(nil)).Elem()

		toInterface := typeTo.Kind() == reflect.Interface

		return toInterface && typeFrom.Implements(typeTo) ||

			// interface -> not interface - maybe false positive when real FROM not TO
			soft && !toInterface && typeTo.Implements(typeFrom)
	}

	_, ok := anyFrom.(TO)

	return ok
}

instead simply:

var from FROM
_, ok := any(from).(TO)

For As just call canCast[Initial, Alias]

do/di_alias.go

Lines 25 to 26 in 1998a7a

_, ok := any(empty[Initial]()).(Alias)
if !ok {

For InvokeAs apparently need getInstanceType() reflect.Type method

do/service.go

Lines 131 to 133 in 1998a7a

if svc, ok := service.(serviceGetEmptyInstance); ok {
// we need an empty instance here, because we don't want to instantiate the service when not needed
if _, ok = svc.getEmptyInstance().(T); ok {

Anonymous Invocation

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:

  • Providers return structs
  • Invocation with [any] is possible
  • Casting results (any) to wanted interfaces.
    Thank you.

Data race when invoking services after shutdown

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?

`InvokeAs` problems

  1. Can't invoke 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`
  1. Random invoked, other not checked (#45 (comment))
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

ServiceLazy shutdowm method

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?

V2 Feature Request: add lifecycle interface support

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.

feat: InjectorOpts.HookBeforePanic

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.

Proposal: do.Shutdown returning shutdown time ?

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 ?

Proposal: allow multiple implementation for the same interface and being able to return all them

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.

v2: Feature Request - expose a method for add lifecycle hook

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!

Need better handling for arrays

package main

import (
	"fmt"

	"github.com/samber/do/v2"
	typeStr "github.com/samber/go-type-to-string" // v1.3.0
)

func main() {
	i := do.New()

	fmt.Println(typeStr.GetType[[1]int]()) // []int
	fmt.Println(do.NameOf[[1]int]())       // []int

	// but
	fmt.Println(reflect.TypeOf([1]int{1}).String()) // [1]int

	do.ProvideValue(i, [1]int{})
	fmt.Println(do.InvokeAs[[3]int](i)) // could not find service satisfying interface `[]int`, available services: `[]int`
	fmt.Println(do.InvokeAs[[]int](i))  // could not find service satisfying interface `[]int`, available services: `[]int`
	fmt.Println(do.InvokeAs[[]int](i))  // could not find service satisfying interface `[]int`, available services: `[]int`

	do.ProvideValue(i, []int{}) // panic: DI: service `[]int` has already been declared
}

More hooks

Currently, we listen to the following events:

  • AfterRegistration
  • AfterShutdown

I would add:

  • BeforeRegistration
  • BeforeShutdown
  • BeforeClone
  • AfterClone
  • BeforeInvocation
  • AfterInvocation

Performance Question about service instnace : getInstance(i Injector) (T, error)

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?

Possible use of closed services in main() on shutdown

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.

  • Force users to don't start background goroutines from 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.
  • Implement API that allows to define Shutdown function for the main() code, to allow the main() stop working with the services before they starts shutdown.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.