⚙️ 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 instead of reflection and therefore is typesafe.
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 utility library. This name is the sum of DI
and Go
and no Go package currently uses this name.
- 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
🚀 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()
.
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.
You can import do
using:
import (
"github.com/samber/do"
)
Then instanciate 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
}
GoDoc: https://godoc.org/github.com/samber/do
Injector:
- do.New
- do.NewWithOpts
- do.HealthCheck
- do.HealthCheckNamed
- do.Shutdown
- do.ShutdownNamed
- do.MustShutdown
- do.MustShutdownNamed
Service registration:
Service invocation:
Service override:
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"}
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"})
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")
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")
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")
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
})
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...)
},
})
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
}
// @TODO
This library does not use reflect
package. We don't expect overhead.
- Ping me on twitter @samuelberthe (DMs, mentions, whatever :))
- Fork the project
- Fix open issues or request new features
Don't hesitate ;)
docker-compose run --rm dev
# Install some dev dependencies
make tools
# Run tests
make test
# or
make watch-test
Give a ⭐️ if this project helped you!
Copyright © 2022 Samuel Berthe.
This project is MIT licensed.