samber/do

repository·master·Indexed 25 days ago

https://github.com/samber/do

A type-safe dependency injection toolkit for Go 1.18+ built using generics. It provides a lightweight alternative to libraries like uber/dig for managing service lifecycles, scopes, and dependency graphs. Key features include eager, lazy, and transient loading; circular dependency detection; health checks; graceful shutdown; and a scope tree for visibility control. It includes introspection tools via Explain APIs and a Web UI supporting middleware for std, Gin, Fiber, Echo, and Chi.

Tokens
28K
Snippets
102
Records
164
Agent score
84%

What's inside samber/do

  1. Overview of do features

    master

    The do library is a type-safe dependency injection toolkit for Go 1.18+ that uses generics instead of code generation. Key capabilities include:

    • Service Management: Registration by type or name, multiple service registration, and service aliasing (implicit or explicit).
    • Invocation Modes: Supports eager, lazy, and transient loading, as well as tag-based invocation and circular dependency detection.
    • Lifecycle Management: Includes health checks, graceful shutdown (dependency-aware parallel shutdown), and lifecycle hooks.
    • Container & Scopes: Provides a scope (module) tree for visibility control, container cloning, service overrides, and dependency graph visualization.
    • Debugging: Offers introspection via Explain APIs and a Web UI with HTTP middleware support for std, Gin, Fiber, Echo, and Chi.
  2. Overview of samber/do dependency injection toolkit

    master
    samber/do is a dependency injection toolkit for Go (1.18+) that utilizes generics instead of reflection to provide a type-safe API. It is designed as an alternative to packages like uber/dig and focuses on developer experience through features like circular dependency detection, service lifecycle management, and scope trees.
  3. Use implicit interface invocation with do.InvokeAs

    master

    To follow the Go proverb "Accept interfaces, return structs", you can provide a concrete struct to the injector and then invoke it using an interface. This is the preferred method for production. Use do.InvokeAs[T] or do.MustInvokeAs[T] to retrieve the service as the specified interface. The injector returns the first matching service in the scope tree.

    Warning: Avoid using very simple interface signatures (like fmt.Stringer) for implicit aliasing, as they may match multiple services in the container and lead to the wrong service being loaded.

    type Metric interface {
        Inc()
    }
    
    type RequestPerSecond struct {
        counter int
    }
    
    func (r *RequestPerSecond) Inc() {
        r.counter++
    }
    
    i := do.New()
    
    // inject the struct
    do.Provide(i, func(i do.Injector) (*RequestPerSecond, error) {
        return &RequestPerSecond{}, nil
    })
    
    // invoke using the Metric interface
    metric := do.MustInvokeAs[Metric](i)
    metric.Inc()    // <- r.counter will be incremented
  4. Mount the Web UI using standard library `http.ServeMux`

    master

    To use the debug Web UI without a web framework, install the std package and use std.Use to mount the handler to your http.ServeMux.

    go get github.com/samber/do/http/std/v2
    import "github.com/samber/do/http/std/v2"
    
    injector := startProgram()
    
    mux := http.NewServeMux()
    // Protect with your own middleware (e.g., Basic Auth) before mounting
    // the debug handler in production.
    mux.Handle("/debug/do/", std.Use("/debug/do", injector))
    
    http.ListenAndServe(":8080", mux)
  5. Migrate from Uber Dig to samber/do

    master

    This guide outlines the steps to migrate a Go project from Uber Dig to samber/do.

    Key API Changes

    • Container Creation: Replace dig.New() with do.New().
    • Service Registration: Replace container.Provide(fn) with do.Provide(injector, fn).
    • Service Invocation: Replace container.Invoke(fn) with do.Invoke[*T](injector) or do.MustInvoke[*T](injector).
    • Constructor Signatures: samber/do constructors must accept do.Injector as the first parameter and return (T, error).

    Migration Workflow

    1. Remove Dig dependencies: go mod edit -droprequire go.uber.org/dig.
    2. Replace container initialization.
    3. Update all Provide calls to use the do.Provide(injector, ...) pattern.
    4. Update all Invoke calls to use the generic do.Invoke[*T] or do.MustInvoke[*T] pattern.
    5. Refactor constructor functions to accept do.Injector and use do.MustInvoke to resolve dependencies internally.
    // After migration example
    package main
    
    import (
        "log"
        "os"
        "syscall"
        "github.com/samber/do/v2"
    )
    
    func main() {
        injector := do.New()
        
        // Register services
        do.Provide(injector, NewDatabase)
        do.Provide(injector, NewUserService)
        do.Provide(injector, NewApp)
        
        // Get the app and run it
        app, err := do.Invoke[*App](injector)
        if err != nil {
            log.Fatal(err)
        }
        
        // Optional: Graceful shutdown
        defer injector.ShutdownOnSignals(syscall.SIGTERM, os.Interrupt)
        
        app.Run()
    }
  6. Group service registrations using Package loading

    master

    You can group multiple service registrations into a single do.Package. This allows you to export a collection of services that can be loaded into an injector all at once, rather than registering each service individually.

    To create a package, use do.Package(...) and pass in service definitions using functions like do.Lazy, do.Eager, or do.EagerNamed.

    package stores
    
    import "github.com/samber/do"
    
    var Package = do.Package(
        do.Lazy(NewPostgreSQLConnectionService),
        do.Lazy(NewUserRepository),
        do.Lazy(NewArticleRepository),
    )
  7. Mount the Web UI in Gin

    master

    To use the debug Web UI with the Gin framework, install the gin package and use ginhttp.Use on a router group.

    go get github.com/samber/do/http/gin/v2
    import "github.com/samber/do/http/gin/v2"
    
    injector := startProgram()
    
    router := gin.New()
    // Attach auth middleware to the group to protect debug UI in production.
    ginhttp.Use(router.Group("/debug/do"), injector)
    
    router.Run(":8080")
  8. Load a Package into an Injector

    master

    Once you have defined a do.Package variable, you can load its services into an injector in two ways:

    1. Function call: Pass the injector to a function that accepts it and applies the package (e.g., stores.Package(injector)).
    2. Constructor argument: Pass the package variable directly to do.New(...).

    You can also create a nested scope by passing a package to injector.Scope(name, package).

    func main() {
        // Option 1: Manual registration via function call
        injector := do.New()
        stores.Package(injector)
        observability.Package(injector)
    
        // Option 2: Loading packages directly during initialization
        // injector := do.New(
        //     stores.Package,
        //     observability.Package,
        // )
    
        // Creating a nested scope with a package
        scope := injector.Scope("handlers", handlers.Package)
    }