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