do

package module
Version: v1.2.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jun 25, 2022 License: MIT Imports: 3 Imported by: 5

README

do - Dependency Injection

tag GoDoc Build Status Go report codecov

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

💡 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

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.

💡 Quick start

You can import do using:

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

Then instanciate 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
}
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.Shutdown()
    // prints "car stopped"
}

🤠 Spec

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

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

Anonymous 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.Replace**** helpers.

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

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

This library does not use reflect package. We don't expect overhead.

🤝 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

👤 Authors

  • Samuel Berthe

💫 Show your support

Give a ⭐️ if this project helped you!

support us

📝 License

Copyright © 2022 Samuel Berthe.

This project is MIT licensed.

Documentation

Index

Constants

This section is empty.

Variables

View Source
var DefaultInjector = New()

Functions

func HealthCheck

func HealthCheck[T any](i *Injector) error

func HealthCheckNamed

func HealthCheckNamed(i *Injector, name string) error

func Invoke

func Invoke[T any](i *Injector) (T, error)

func InvokeNamed

func InvokeNamed[T any](i *Injector, name string) (T, error)

func MustInvoke

func MustInvoke[T any](i *Injector) T

func MustInvokeNamed

func MustInvokeNamed[T any](i *Injector, name string) T

func MustShutdown

func MustShutdown[T any](i *Injector)

func MustShutdownNamed

func MustShutdownNamed(i *Injector, name string)

func Override added in v1.2.0

func Override[T any](i *Injector, provider Provider[T])

func OverrideNamed added in v1.2.0

func OverrideNamed[T any](i *Injector, name string, provider Provider[T])

func OverrideNamedValue added in v1.2.0

func OverrideNamedValue[T any](i *Injector, name string, value T)

func OverrideValue added in v1.2.0

func OverrideValue[T any](i *Injector, value T)

func Provide

func Provide[T any](i *Injector, provider Provider[T])

func ProvideNamed

func ProvideNamed[T any](i *Injector, name string, provider Provider[T])

func ProvideNamedValue

func ProvideNamedValue[T any](i *Injector, name string, value T)

func ProvideValue

func ProvideValue[T any](i *Injector, value T)

func Shutdown

func Shutdown[T any](i *Injector) error

func ShutdownNamed

func ShutdownNamed(i *Injector, name string) error

Types

type Healthcheckable

type Healthcheckable interface {
	HealthCheck() error
}

type Injector

type Injector struct {
	// contains filtered or unexported fields
}

func New

func New() *Injector

func NewWithOpts added in v1.1.0

func NewWithOpts(opts *InjectorOpts) *Injector

func (*Injector) Clone added in v1.2.0

func (i *Injector) Clone() *Injector

Clone clones injector with provided services but not with invoked instances.

func (*Injector) CloneWithOpts added in v1.2.0

func (i *Injector) CloneWithOpts(opts *InjectorOpts) *Injector

CloneWithOpts clones injector with provided services but not with invoked instances, with options.

func (*Injector) HealthCheck

func (i *Injector) HealthCheck() map[string]error

func (*Injector) ListInvokedServices added in v1.1.0

func (i *Injector) ListInvokedServices() []string

func (*Injector) ListProvidedServices added in v1.1.0

func (i *Injector) ListProvidedServices() []string

func (*Injector) Shutdown

func (i *Injector) Shutdown() error

type InjectorOpts added in v1.1.0

type InjectorOpts struct {
	HookAfterRegistration func(injector *Injector, serviceName string)
	HookAfterShutdown     func(injector *Injector, serviceName string)
}

type Provider

type Provider[T any] func(*Injector) (T, error)

type Service

type Service[T any] interface {
	// contains filtered or unexported methods
}

type ServiceEager

type ServiceEager[T any] struct {
	// contains filtered or unexported fields
}

type ServiceLazy

type ServiceLazy[T any] struct {
	// contains filtered or unexported fields
}

type Shutdownable

type Shutdownable interface {
	Shutdown() error
}

Directories

Path Synopsis

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL