Convert Figma logo to code with AI

samber logodo

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

1,801
73
1,801
29

Top Related Projects

5,648

A dependency injection based application framework for Go.

12,864

Compile-time Dependency Injection for Go

1,406

Package inject provides a reflect based injector.

Quick Overview

samber/do is a dependency injection toolkit for Go, designed to simplify the management of dependencies in Go applications. It provides a lightweight and flexible approach to dependency injection, allowing developers to write more modular and testable code.

Pros

  • Lightweight and easy to use, with minimal boilerplate code
  • Supports both singleton and transient dependencies
  • Allows for easy mocking and testing of components
  • Type-safe dependency resolution at compile-time

Cons

  • Requires some initial setup and understanding of dependency injection concepts
  • May introduce a small performance overhead compared to manual dependency management
  • Limited documentation and community support compared to more established DI frameworks
  • Potential for circular dependencies if not carefully managed

Code Examples

  1. Defining and resolving a dependency:
type Database struct{}

func NewDatabase() *Database {
    return &Database{}
}

container := do.New()
container.AddSingleton(NewDatabase)

db := do.MustInvoke[*Database](container)
  1. Using interfaces for dependency injection:
type Logger interface {
    Log(message string)
}

type ConsoleLogger struct{}

func (l *ConsoleLogger) Log(message string) {
    fmt.Println(message)
}

container.AddSingleton(func() Logger {
    return &ConsoleLogger{}
})

logger := do.MustInvoke[Logger](container)
logger.Log("Hello, World!")
  1. Injecting dependencies into a struct:
type Service struct {
    DB     *Database `inject:""`
    Logger Logger    `inject:""`
}

container.AddSingleton(func(db *Database, logger Logger) *Service {
    return &Service{DB: db, Logger: logger}
})

service := do.MustInvoke[*Service](container)

Getting Started

To start using samber/do in your Go project:

  1. Install the package:

    go get github.com/samber/do
    
  2. Import the package in your code:

    import "github.com/samber/do"
    
  3. Create a new container and start adding dependencies:

    container := do.New()
    container.AddSingleton(NewDatabase)
    container.AddSingleton(func() Logger { return &ConsoleLogger{} })
    
  4. Resolve dependencies when needed:

    db := do.MustInvoke[*Database](container)
    logger := do.MustInvoke[Logger](container)
    

Competitor Comparisons

5,648

A dependency injection based application framework for Go.

Pros of fx

  • More mature and widely adopted project with extensive documentation
  • Provides a complete dependency injection framework with lifecycle management
  • Offers advanced features like parameter objects and optional dependencies

Cons of fx

  • Steeper learning curve due to more complex API and concepts
  • Requires more boilerplate code for setup and configuration
  • Heavier runtime overhead compared to simpler alternatives

Code Comparison

fx:

func NewHandler(lc fx.Lifecycle, logger *zap.Logger) *Handler {
    handler := &Handler{logger: logger}
    lc.Append(fx.Hook{
        OnStart: func(context.Context) error {
            // Initialization logic
            return nil
        },
    })
    return handler
}

do:

func NewHandler(logger *zap.Logger) *Handler {
    return &Handler{logger: logger}
}

// Usage
handler := do.MustInvoke[*Handler](container)

Summary

fx is a more comprehensive dependency injection framework with advanced features and lifecycle management, suitable for larger projects. do offers a simpler, lightweight alternative with less overhead and easier setup, making it ideal for smaller applications or those preferring a minimalist approach. The choice between the two depends on project complexity, team familiarity, and specific requirements for dependency management and application structure.

12,864

Compile-time Dependency Injection for Go

Pros of Wire

  • Generates code at compile-time, reducing runtime overhead
  • Backed by Google, potentially offering more long-term support
  • Provides clear error messages for dependency issues

Cons of Wire

  • Requires a separate code generation step
  • Less flexible for runtime configuration changes
  • Steeper learning curve for newcomers to dependency injection

Code Comparison

Wire:

func InitializeEvent(w http.ResponseWriter, r *http.Request) (*Event, error) {
    wire.Build(NewEvent, NewGreeter, NewMessage)
    return nil, nil
}

Do:

container := do.New()
container.Provide(NewEvent)
container.Provide(NewGreeter)
container.Provide(NewMessage)
event := container.Get((*Event)(nil))

Summary

Wire focuses on compile-time dependency injection, offering performance benefits but less runtime flexibility. Do provides a more traditional runtime dependency injection approach, allowing for easier dynamic configuration but with potential runtime overhead. Wire's code generation may be preferable for larger projects with stable dependencies, while Do's simplicity might be more suitable for smaller projects or those requiring frequent runtime changes. The choice between the two depends on specific project requirements and developer preferences.

1,406

Package inject provides a reflect based injector.

Pros of inject

  • More mature and battle-tested, having been used in production at Facebook
  • Supports more advanced features like optional dependencies and provider overrides
  • Has a larger community and ecosystem of extensions and integrations

Cons of inject

  • No longer actively maintained (archived repository)
  • More complex API and steeper learning curve
  • Heavier and potentially slower due to more features

Code Comparison

inject:

type Server struct {
    Port int `inject:""`
    DB   *sql.DB `inject:""`
}

var server Server
injector.Inject(&server)

do:

type Server struct {
    Port int
    DB   *sql.DB
}

server := do.MustInvoke[Server](container)

Summary

inject is a more feature-rich and mature dependency injection framework, but it's no longer maintained. do offers a simpler, more modern approach with a focus on type safety and ease of use. While inject may be better suited for complex, large-scale applications, do is likely a better choice for new projects due to its active development and straightforward API.

Convert Figma logo designs to code with AI

Visual Copilot

Introducing Visual Copilot: A new AI model to turn Figma designs to high quality code using your components.

Try Visual Copilot

README

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/v2@v2.0.0-beta.7

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.