520 lines
12 KiB
Markdown
520 lines
12 KiB
Markdown
|
|
# do - Dependency Injection
|
|
|
|
[](https://github.com/samber/do/releases)
|
|

|
|
[](https://pkg.go.dev/github.com/samber/do)
|
|

|
|
[](https://goreportcard.com/report/github.com/samber/do)
|
|
[](https://codecov.io/gh/samber/do)
|
|
[](./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 instead of reflection and therefore is typesafe.
|
|
|
|
**See also:**
|
|
|
|
- [samber/lo](https://github.com/samber/lo): A Lodash-style Go library based on Go 1.18+ Generics
|
|
- [samber/mo](https://github.com/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.
|
|
|
|
## 💡 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
|
|
|
|
🚀 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
|
|
|
|
```sh
|
|
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 outside the Go standard library.
|
|
|
|
## 💡 Quick start
|
|
|
|
You can import `do` using:
|
|
|
|
```go
|
|
import (
|
|
"github.com/samber/do"
|
|
)
|
|
```
|
|
|
|
Then instanciate services:
|
|
|
|
```go
|
|
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:
|
|
|
|
```go
|
|
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")
|
|
}
|
|
```
|
|
|
|
```go
|
|
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](https://godoc.org/github.com/samber/do)
|
|
|
|
Injector:
|
|
|
|
- [do.New](https://pkg.go.dev/github.com/samber/do#New)
|
|
- [do.NewWithOpts](https://pkg.go.dev/github.com/samber/do#NewWithOpts)
|
|
- [injector.Clone](https://pkg.go.dev/github.com/samber/do#injector.Clone)
|
|
- [injector.CloneWithOpts](https://pkg.go.dev/github.com/samber/do#injector.CloneWithOpts)
|
|
- [injector.HealthCheck](https://pkg.go.dev/github.com/samber/do#injector.HealthCheck)
|
|
- [injector.Shutdown](https://pkg.go.dev/github.com/samber/do#injector.Shutdown)
|
|
- [injector.ShutdownOnSIGTERM](https://pkg.go.dev/github.com/samber/do#injector.ShutdownOnSIGTERM)
|
|
- [injector.ListProvidedServices](https://pkg.go.dev/github.com/samber/do#injector.ListProvidedServices)
|
|
- [injector.ListInvokedServices](https://pkg.go.dev/github.com/samber/do#injector.ListInvokedServices)
|
|
- [do.HealthCheck](https://pkg.go.dev/github.com/samber/do#HealthCheck)
|
|
- [do.HealthCheckNamed](https://pkg.go.dev/github.com/samber/do#HealthCheckNamed)
|
|
- [do.Shutdown](https://pkg.go.dev/github.com/samber/do#Shutdown)
|
|
- [do.ShutdownNamed](https://pkg.go.dev/github.com/samber/do#ShutdownNamed)
|
|
- [do.MustShutdown](https://pkg.go.dev/github.com/samber/do#MustShutdown)
|
|
- [do.MustShutdownNamed](https://pkg.go.dev/github.com/samber/do#MustShutdownNamed)
|
|
|
|
Service registration:
|
|
|
|
- [do.Provide](https://pkg.go.dev/github.com/samber/do#Provide)
|
|
- [do.ProvideNamed](https://pkg.go.dev/github.com/samber/do#ProvideNamed)
|
|
- [do.ProvideNamedValue](https://pkg.go.dev/github.com/samber/do#ProvideNamedValue)
|
|
- [do.ProvideValue](https://pkg.go.dev/github.com/samber/do#ProvideValue)
|
|
|
|
Service invocation:
|
|
|
|
- [do.Invoke](https://pkg.go.dev/github.com/samber/do#Invoke)
|
|
- [do.MustInvoke](https://pkg.go.dev/github.com/samber/do#MustInvoke)
|
|
- [do.InvokeNamed](https://pkg.go.dev/github.com/samber/do#InvokeNamed)
|
|
- [do.MustInvokeNamed](https://pkg.go.dev/github.com/samber/do#MustInvokeNamed)
|
|
|
|
Service override:
|
|
|
|
- [do.Override](https://pkg.go.dev/github.com/samber/do#Override)
|
|
- [do.OverrideNamed](https://pkg.go.dev/github.com/samber/do#OverrideNamed)
|
|
- [do.OverrideNamedValue](https://pkg.go.dev/github.com/samber/do#OverrideNamedValue)
|
|
- [do.OverrideValue](https://pkg.go.dev/github.com/samber/do#OverrideValue)
|
|
|
|
### Injector (DI container)
|
|
|
|
Build a container for your components. `Injector` is responsible for building services in the right order, and managing service lifecycle.
|
|
|
|
```go
|
|
injector := do.New()
|
|
```
|
|
|
|
Or use `nil` as the default injector:
|
|
|
|
```go
|
|
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`.
|
|
|
|
```go
|
|
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.
|
|
|
|
```go
|
|
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:
|
|
|
|
```go
|
|
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:
|
|
|
|
```go
|
|
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:
|
|
|
|
```go
|
|
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:
|
|
|
|
```go
|
|
type Config struct {
|
|
uri string
|
|
}
|
|
|
|
do.ProvideValue[Config](injector, Config{uri: "postgres://user:pass@host:5432/db"})
|
|
```
|
|
|
|
Named service, loaded eagerly:
|
|
|
|
```go
|
|
type Config struct {
|
|
uri string
|
|
}
|
|
|
|
do.ProvideNamedValue(injector, "configuration", Config{uri: "postgres://user:pass@host:5432/db"})
|
|
```
|
|
|
|
### Service invocation
|
|
|
|
Loads anonymous service:
|
|
|
|
```go
|
|
type DBService struct {
|
|
db *sql.DB
|
|
}
|
|
|
|
dbService, err := do.Invoke[DBService](injector)
|
|
```
|
|
|
|
Loads anonymous service or panics if service was not registered:
|
|
|
|
```go
|
|
type DBService struct {
|
|
db *sql.DB
|
|
}
|
|
|
|
dbService := do.MustInvoke[DBService](injector)
|
|
```
|
|
|
|
Loads named service:
|
|
|
|
```go
|
|
config, err := do.InvokeNamed[Config](injector, "configuration")
|
|
```
|
|
|
|
Loads named service or panics if service was not registered:
|
|
|
|
```go
|
|
config := do.MustInvokeNamed[Config](injector, "configuration")
|
|
```
|
|
|
|
### Individual service healthcheck
|
|
|
|
Check health of anonymous service:
|
|
|
|
```go
|
|
type DBService struct {
|
|
db *sql.DB
|
|
}
|
|
|
|
dbService, err := do.Invoke[DBService](injector)
|
|
err = do.HealthCheck[DBService](injector)
|
|
```
|
|
|
|
Check health of named service:
|
|
|
|
```go
|
|
config, err := do.InvokeNamed[Config](injector, "configuration")
|
|
err = do.HealthCheckNamed(injector, "configuration")
|
|
```
|
|
|
|
### Individual service shutdown
|
|
|
|
Unloads anonymous service:
|
|
|
|
```go
|
|
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:
|
|
|
|
```go
|
|
type DBService struct {
|
|
db *sql.DB
|
|
}
|
|
|
|
dbService := do.MustInvoke[DBService](injector)
|
|
do.MustShutdown[DBService](injector)
|
|
```
|
|
|
|
Unloads named service:
|
|
|
|
```go
|
|
config, err := do.InvokeNamed[Config](injector, "configuration")
|
|
err = do.ShutdownNamed(injector, "configuration")
|
|
```
|
|
|
|
Unloads named service or panics if service was not registered:
|
|
|
|
```go
|
|
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.
|
|
|
|
```go
|
|
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
|
|
|
|
```go
|
|
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.
|
|
|
|
```go
|
|
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
|
|
|
|
This library does not use `reflect` package. We don't expect overhead.
|
|
|
|
## 🤝 Contributing
|
|
|
|
- Ping me on twitter [@samuelberthe](https://twitter.com/samuelberthe) (DMs, mentions, whatever :))
|
|
- Fork the [project](https://github.com/samber/do)
|
|
- Fix [open issues](https://github.com/samber/do/issues) or request new features
|
|
|
|
Don't hesitate ;)
|
|
|
|
### With Docker
|
|
|
|
```bash
|
|
docker-compose run --rm dev
|
|
```
|
|
|
|
### Without Docker
|
|
|
|
```bash
|
|
# Install some dev dependencies
|
|
make tools
|
|
|
|
# Run tests
|
|
make test
|
|
# or
|
|
make watch-test
|
|
```
|
|
|
|
## 👤 Contributors
|
|
|
|

|
|
|
|
## 💫 Show your support
|
|
|
|
Give a ⭐️ if this project helped you!
|
|
|
|
[](https://www.patreon.com/samber)
|
|
|
|
## 📝 License
|
|
|
|
Copyright © 2022 [Samuel Berthe](https://github.com/samber).
|
|
|
|
This project is [MIT](./LICENSE) licensed.
|