inject

package module
v1.2.2 Latest Latest
Warning

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

Go to latest
Published: Jan 3, 2026 License: MIT Imports: 11 Imported by: 2

README

inject

The inject package provides a powerful and intuitive dependency injection framework for Go applications. It features automatic dependency resolution, lifecycle management, and thread-safe operations, making it ideal for building scalable applications with clean architecture.

Features

  • Provide: Used to provide dependencies through a function.
  • Invoke: Used to resolve dependencies and invoke a function with them.
  • Resolve: Used to resolve a dependency by its type.
  • Apply: Used to apply dependencies to a struct.
  • Build: Used to eagerly build all provided dependencies.
  • SetParent: Used to set the parent injector.
  • Element[T]: Used to provide multiple values of the same type, resolved as Slice[T].
  • Thread safety ensured
  • Supports injection through the inject tag of struct fields

Usage

For complete usage examples of the basic inject functionality, see example_test.go.

Lifecycle Management

The lifecycle subpackage provides a complete lifecycle management system that combines dependency injection with coordinated startup, monitoring, and shutdown of services and actors.

Key Concepts
  • Actor: Simple components with start/stop operations (databases, configurations, migrations)
  • Service: Long-running components that can signal completion (HTTP servers, background workers)
  • Lifecycle: Orchestrates multiple actors and services with dependency injection
Core Features
  • Dependency Injection: Built-in injector for managing dependencies
  • Coordinated Startup: Actors start in registration order
  • Graceful Shutdown: Actors stop in reverse order with timeout control
  • Service Monitoring: Automatic monitoring of long-running services
  • Signal Handling: Built-in OS signal handling for graceful shutdown
  • Readiness Probes: Optional blocking until services are ready to serve traffic
  • Error Handling: Comprehensive error handling and logging
Basic Example
package main

import (
    "context"
    "fmt"
    "log"
    "net/http"

    "github.com/pkg/errors"
    "github.com/theplant/inject/lifecycle"
)

func main() {
    if err := lifecycle.Serve(context.Background(),
        lifecycle.SetupSignal,

        func() *Config {
            return &Config{
                HTTPServerPort: 8080,
                DatabaseURL:    "postgres://localhost:5432/myapp",
                RPCServerURL:   "127.0.0.1:1088",
            }
        },

        CreateRPCClient,

        func(lc *lifecycle.Lifecycle, conf *Config) *Database {
            db := &Database{}
            lc.Add(lifecycle.NewFuncActor(
                func(_ context.Context) error {
                    log.Printf("Connecting to database: %s", conf.DatabaseURL)
                    return db.Connect()
                },
                func(_ context.Context) error {
                    return db.Close()
                },
            ).WithName("database"))
            return db
        },

        func(lc *lifecycle.Lifecycle, conf *Config, db *Database, rpcClient *RPCClient) *http.Server {
            mux := http.NewServeMux()
            mux.HandleFunc("/health", func(w http.ResponseWriter, r *http.Request) {
                fmt.Fprintf(w, "OK - DB Connected: %t, RPCClient Connected: %t", db.connected, rpcClient.connected)
            })

            addr := fmt.Sprintf(":%d", conf.HTTPServerPort)
            server := &http.Server{
                Addr:    addr,
                Handler: mux,
            }

            lc.Add(lifecycle.NewFuncService(func(ctx context.Context) error {
                log.Printf("Starting HTTP server on %s", addr)
                if err := server.ListenAndServe(); err != http.ErrServerClosed {
                    return err
                }
                return nil
            }).WithStop(server.Shutdown).WithName("http-server"))

            return server
        },
    ); err != nil {
        log.Fatal(err)
    }
}

type Config struct {
    HTTPServerPort int
    DatabaseURL    string
    RPCServerURL   string
}

type Database struct {
    connected bool
}

func (db *Database) Connect() error {
    db.connected = true
    log.Println("Database connected")
    return nil
}

func (db *Database) Close() error {
    db.connected = false
    log.Println("Database closed")
    return nil
}

type RPCClient struct {
    connected bool
}

func (c *RPCClient) Close() error {
    c.connected = false
    log.Println("RPC client disconnected")
    return nil
}

func DialContext(ctx context.Context, serverURL string) (*RPCClient, error) {
    if serverURL == "" {
        return nil, errors.New("server URL cannot be empty")
    }

    client := &RPCClient{
        connected: true,
    }

    log.Printf("RPC client connected to %s", serverURL)
    return client, nil
}

func CreateRPCClient(ctx context.Context, lc *lifecycle.Lifecycle, conf *Config) (*RPCClient, error) {
    client, err := DialContext(ctx, conf.RPCServerURL)
    if err != nil {
        return nil, err
    }

    lc.Add(lifecycle.NewFuncActor(
        nil,
        func(_ context.Context) error {
            return client.Close()
        },
    ).WithName("rpc-client"))

    return client, nil
}
Advanced Usage
Manual Lifecycle Management
type Config struct {
    Port        int
    DatabaseURL string
}

lc := lifecycle.New()

// Provide dependencies
err := lc.Provide(
    func() *Config {
        return &Config{
            Port:        8080,
            DatabaseURL: "postgres://localhost:5432/myapp",
        }
    },
    func(lc *lifecycle.Lifecycle, conf *Config) *Database {
        db := &Database{}
        lc.Add(lifecycle.NewFuncActor(
            func(_ context.Context) error {
                log.Printf("Connecting to database: %s", conf.DatabaseURL)
                return db.Connect()
            },
            func(_ context.Context) error {
                return db.Close()
            },
        ).WithName("database"))
        return db
    },
    setupHTTPServer,
    lifecycle.SetupSignal,
)

// Start all services
if err := lc.Serve(context.Background()); err != nil {
    log.Fatal(err)
}
Service Collections

You can group related setup functions:

var setupHTTPServer = []any{
    func() *HTTPConfig { return &HTTPConfig{Port: 8080} },
    func(lc *lifecycle.Lifecycle, conf *HTTPConfig) *http.Server {
        server := &http.Server{Addr: fmt.Sprintf(":%d", conf.Port)}
        lc.Add(lifecycle.NewFuncService(func(ctx context.Context) error {
            return server.ListenAndServe()
        }).WithStop(server.Shutdown).WithName("http"))
        return server
    },
}

// Use in lifecycle
lc.Provide(setupHTTPServer)
Signal Handling
import "syscall"

// Custom signals
lc.Provide(lifecycle.SetupSignalWith(syscall.SIGUSR1, syscall.SIGUSR2))

// Default signals (SIGINT, SIGTERM)
lc.Provide(lifecycle.SetupSignal)
Readiness Probe

The lifecycle supports optional readiness probes to block startup until services are ready. Use FuncActor.WithReadiness() to enable automatic readiness signaling - the probe is signaled when Start() completes:

func SetupHTTPReadinessProbe(lc *lifecycle.Lifecycle, listener net.Listener) {
    addr := fmt.Sprintf("http://%s/health", listener.Addr().String())

    lc.Add(lifecycle.NewFuncActor(func(ctx context.Context) error {
        return WaitForReady(ctx, addr)
    }, nil).WithName("http-readiness").WithReadiness())
}

When using lifecycle.Start(), the lifecycle will block until all probes signal ready:

lc, err := lifecycle.Start(context.Background(),
    SetupHTTPListener,
    SetupHTTPServer,
    SetupHTTPReadinessProbe,
)

If the Actor.Start() function returns an error, the probe signals failure and lifecycle.Start() returns that error.

Nested Lifecycle

Since *lifecycle.Lifecycle itself implements the Service interface, you can nest lifecycles to create modular subsystems:

// Create a subsystem lifecycle for database-related services
func SetupDatabaseSubsystem(parent *lifecycle.Lifecycle, conf *DatabaseConfig) (*DatabaseSubsystem, error) {
    // Create a nested lifecycle
    sub, err := lifecycle.Provide(
        func() *DatabaseConfig { return conf },
        func(lc *lifecycle.Lifecycle, conf *DatabaseConfig) *Database {
            db := &Database{}
            lc.Add(lifecycle.NewFuncActor(
                func(_ context.Context) error { return db.Connect(conf.DatabaseURL) },
                func(_ context.Context) error { return db.Close() },
            ).WithName("database"))
            return db
        },
        func(lc *lifecycle.Lifecycle, db *Database) *MigrationRunner {
            runner := &MigrationRunner{db: db}
            lc.Add(lifecycle.NewFuncActor(
                func(_ context.Context) error { return runner.Run() },
                nil,
            ).WithName("migrations"))
            return runner
        },
    )
    if err != nil {
        return nil, err
    }

    // Add the nested lifecycle as a service to the parent
    parent.Add(sub.WithName("database-subsystem"))

    return &DatabaseSubsystem{lifecycle: sub}, nil
}

// Main application
func main() {
    if err := lifecycle.Serve(context.Background(),
        lifecycle.SetupSignal,
        func() *Config { return loadConfig() },
        func(conf *Config) *DatabaseConfig { return conf.DatabaseConfig },
        SetupDatabaseSubsystem,  // Nested lifecycle
        SetupHTTPServer,
    ); err != nil {
        log.Fatal(err)
    }
}

Benefits of nested lifecycles:

  • Modularity: Group related services into self-contained subsystems
  • Isolation: Each subsystem has its own dependency injection scope
  • Coordinated shutdown: Parent lifecycle manages shutdown of all nested lifecycles
Configuration
Timeouts
import "time"

lc := lifecycle.New().
    WithStopTimeout(60 * time.Second).     // Total shutdown timeout
    WithStopEachTimeout(10 * time.Second)  // Per-actor shutdown timeout
Custom Logging
import (
    "log/slog"
    "os"
)

logger := slog.New(slog.NewJSONHandler(os.Stdout, nil))
lc := lifecycle.New().WithLogger(logger)
Error Handling and Monitoring

The lifecycle system provides comprehensive monitoring:

  • Startup Errors: Any actor failing to start stops the entire lifecycle
  • Service Monitoring: Long-running services are monitored for completion or errors
  • Graceful Shutdown: When any service completes or signal is received, all actors are stopped in reverse order
  • Timeout Handling: Configurable timeouts for shutdown operations
  • Stop Cause: Context includes information about why shutdown was initiated
// Access stop cause in actor shutdown
func(ctx context.Context) error {
    cause := lifecycle.GetStopCause(ctx)
    if cause != nil {
        log.Printf("Shutting down due to: %v", cause)
    }
    return cleanup()
}

Element - Multiple Values of Same Type

The Element[T] type allows you to provide multiple values of the same type T. When resolving Slice[T], all *Element[T] values are automatically collected. Use NewElement() to create elements conveniently.

inj := inject.New()

err := inj.Provide(
    func() *Config { return &Config{Prefix: "api-"} },
    // Multiple *Element[T] providers with dependencies
    func(cfg *Config) *inject.Element[string] {
        return inject.NewElement(cfg.Prefix + "route1")
    },
    func(cfg *Config) *inject.Element[string] {
        return inject.NewElement(cfg.Prefix + "route2")
    },
)

// Resolve as Slice[T] (which is []T)
var routes inject.Slice[string]
inj.Resolve(&routes) // routes = []string{"api-route1", "api-route2"}

Note: *Element[T] must be a pointer type. Slice[T] is a slice type alias (type Slice[T any] []T), so you can use it directly as a slice.

Nil Element Handling

When resolving Slice[T], the following behaviors apply:

  • nil *Element[T]: If a provider returns nil (i.e., return nil instead of return NewElement(...)), the nil element is skipped and not included in the resulting slice.
  • *Element[T] with nil Value: If a provider returns a valid *Element[T] but with a nil Value (e.g., return NewElement[*Service](nil)), the nil value is included in the resulting slice.
// Example: nil *Element[T] is skipped
inj.Provide(
    func() *inject.Element[string] { return inject.NewElement("first") },
    func() *inject.Element[string] { return nil },  // Skipped
    func() *inject.Element[string] { return inject.NewElement("third") },
)
var strs inject.Slice[string]
inj.Resolve(&strs) // strs = []string{"first", "third"}

// Example: *Element[T] with nil Value is included
inj.Provide(
    func() *inject.Element[*Service] { return inject.NewElement(&Service{Name: "valid"}) },
    func() *inject.Element[*Service] { return inject.NewElement[*Service](nil) },  // Included as nil
    func() *inject.Element[*Service] { return inject.NewElement(&Service{Name: "another"}) },
)
var services inject.Slice[*Service]
inj.Resolve(&services) // services = []*Service{&Service{...}, nil, &Service{...}}

Void Constructors

The Void type allows you to register constructors that don't return any value (or only return error). These are useful for "side-effect only" operations like modifying configuration or performing initialization tasks.

When a constructor has no return type (or only returns error), it is automatically treated as returning *Element[*Void].

inj := inject.New()

// Provide a config
err := inj.Provide(func() *Config {
    return &Config{Debug: false}
})

// Void constructor - modifies config as side effect
err = inj.Provide(func(cfg *Config) {
    cfg.Debug = true  // Modify config
})

// Execute all constructors via Build
err = inj.Build()

// Or resolve explicitly to trigger void constructors
var voids inject.Slice[*inject.Void]
inj.Resolve(&voids)

Key behaviors:

  • Lazy execution: Void constructors are not executed until Build() is called or Slice[*Void] is resolved
  • Single execution: Each void constructor is executed only once (like all other constructors)
  • Dependency injection: Void constructors can have dependencies injected as parameters
  • Error handling: Void constructors can return error as their only return type
// Void constructor with error handling
inj.Provide(func(cfg *Config) error {
    if cfg.DatabaseURL == "" {
        return errors.New("database URL is required")
    }
    return nil
})

Important Notes

Special Parameter Types

The inject package handles certain parameter types in special ways:

  • context.Context: Not managed by the injector. Cannot be used as a return type. This is the context passed to dependency resolution methods (like InvokeContext, ResolveContext) and will be automatically passed to constructor functions that declare it as a parameter.

  • error: Not managed by the injector. Can only be used as a return type, and must be the last return type if present. If a constructor returns an error, the injection will fail and propagate the error.

Constructor Function Rules
  • Constructor functions can have multiple return values, but error must be the last one if present
  • All non-error return values will be registered as available dependencies
  • Constructor functions are called lazily when their return types are needed
  • Use Build() or BuildContext() to eagerly instantiate all dependencies
Error Handling
// Constructor with error handling
func NewDatabase(conf *Config) (*Database, error) {
    db, err := sql.Open("postgres", conf.DatabaseURL)
    if err != nil {
        return nil, errors.Wrap(err, "failed to open database")
    }
    return &Database{db: db}, nil
}

// context.Context comes from the calling context (e.g., injector.InvokeContext(ctx, ...))
func NewRPCClient(ctx context.Context, conf *Config) (*RPCClient, error) {
    client, err := rpc.DialContext(ctx, conf.ServerURL)
    if err != nil {
        return nil, errors.Wrap(err, "failed to dial RPC server")
    }
    return client, nil
}

Documentation

Index

Examples

Constants

This section is empty.

Variables

View Source
var (
	ErrTypeNotProvided     = errors.New("type not provided")
	ErrTypeAlreadyProvided = errors.New("type already provided")
	ErrParentAlreadySet    = errors.New("parent already set")
	ErrTypeNotAllowed      = errors.New("type not allowed")
	ErrCircularDependency  = errors.New("circular dependency detected")
	ErrErrorTypeMustBeLast = errors.New("error type must be the last return value")
	ErrInvalidProvider     = errors.New("provide only accepts a function that returns at least one type except error")
	ErrInvalidInvokeTarget = errors.New("invoke only accepts a function")
	ErrInvalidApplyTarget  = errors.New("apply only accepts a struct")
)
View Source
var (
	TagName          = "inject"
	TagValueOptional = "optional"
)

Functions

func IsInputTypeAllowed added in v1.1.2

func IsInputTypeAllowed(typ reflect.Type) bool

IsInputTypeAllowed checks if a type is allowed as a function input parameter. Element[T] types are not allowed as input - use Slice[T] instead.

func IsOutputTypeAllowed added in v1.1.2

func IsOutputTypeAllowed(typ reflect.Type) bool

IsOutputTypeAllowed checks if a type is allowed as a function output. Slice[T] types are not allowed as output - use Element[T] instead.

func IsTypeAllowed added in v1.0.0

func IsTypeAllowed(typ reflect.Type) bool

IsTypeAllowed checks if a type is allowed as a function input or output parameter.

func MustResolve added in v1.0.0

func MustResolve[T any](inj interface{ Resolve(...any) error }) T

MustResolve resolves a dependency from the injector.

func MustResolveContext added in v1.0.0

func MustResolveContext[T any](
	ctx context.Context,
	inj interface {
		ResolveContext(context.Context, ...any) error
	},
) T

func Resolve added in v1.0.0

func Resolve[T any](inj interface{ Resolve(...any) error }) (T, error)

Resolve resolves a dependency from the injector.

func ResolveContext added in v1.0.0

func ResolveContext[T any](
	ctx context.Context,
	inj interface {
		ResolveContext(context.Context, ...any) error
	},
) (T, error)

Types

type Element added in v1.1.2

type Element[T any] struct {
	Value T
}

Element is a wrapper type that allows multiple values of the same type T to be provided to the injector. When resolving Slice[T], all *Element[T] values will be collected and returned.

Usage:

inj.Provide(func() *Element[http.Handler] { return NewElement(handler1) })
inj.Provide(func() *Element[http.Handler] { return NewElement(handler2) })

// Resolve as Slice[T]
var handlers Slice[http.Handler]
inj.Resolve(&handlers) // handlers = []http.Handler{handler1, handler2}

func NewElement added in v1.1.2

func NewElement[T any](value T) *Element[T]

NewElement creates a new *Element[T] with the given value.

func NewVoidElement added in v1.2.0

func NewVoidElement() *Element[*Void]

NewVoidElement creates a new *Element[*Void].

type Injector

type Injector struct {
	// contains filtered or unexported fields
}
Example
package main

import (
	"fmt"

	"github.com/theplant/inject"
)

// Define interfaces and implementations
type Printer interface {
	Print() string
}

type SimplePrinter struct{}

func (p *SimplePrinter) Print() string {
	return "Printing document"
}

// New type definition
type DocumentDescription string

type Document struct {
	Injector *inject.Injector `inject:""`

	ID          string
	Description DocumentDescription `inject:""`
	Printer     Printer             `inject:""`
	Size        int64               `inject:"optional"`
	page        int                 `inject:""`
	name        string              `inject:"optional"`
	ReadCount   int32               `inject:""`
}

func main() {
	inj := inject.New()

	// Provide dependencies
	if err := inj.Provide(
		func() Printer {
			return &SimplePrinter{}
		},
		func() string {
			return "A simple string"
		},
		func() DocumentDescription {
			return "A document description"
		},
		func() (int, int32) {
			return 42, 32
		},
	); err != nil {
		panic(err)
	}

	{
		// Resolve dependencies
		var printer Printer
		if err := inj.Resolve(&printer); err != nil {
			panic(err)
		}
		fmt.Println("Resolved printer:", printer.Print())
	}

	printDoc := func(doc *Document) {
		fmt.Printf("Document id: %q\n", doc.ID)
		fmt.Printf("Document description: %q\n", doc.Description)
		fmt.Printf("Document printer: %q\n", doc.Printer.Print())
		fmt.Printf("Document size: %d\n", doc.Size)
		fmt.Printf("Document page: %d\n", doc.page)
		fmt.Printf("Document name: %q\n", doc.name)
		fmt.Printf("Document read count: %d\n", doc.ReadCount)
	}

	fmt.Println("-------")

	{
		// Invoke a function
		results, err := inj.Invoke(func(printer Printer) *Document {
			return &Document{
				// This value will be retained as it is not tagged with `inject`, despite string being provided
				ID: "idInvoked",
				// This value will be overridden since it is tagged with `inject` and DocumentDescription is provided
				Description: "DescriptionInvoked",
				// This value will be overridden with the same value since it is tagged with `inject` and Printer is provided
				Printer: printer,
				// This value will be retained since it is tagged with `inject:"optional"` and int64 is not provided
				Size: 100,
			}
		})
		if err != nil {
			panic(err)
		}

		printDoc(results[0].(*Document))
	}

	fmt.Println("-------")

	{
		// Apply dependencies to a struct instance
		doc := &Document{}
		if err := inj.Apply(doc); err != nil {
			panic(err)
		}

		printDoc(doc)
	}

	fmt.Println("-------")

	{
		// Create a child injector and then apply dependencies to a struct instance
		child := inject.New()
		_ = child.SetParent(inj)

		doc := &Document{}
		if err := child.Apply(doc); err != nil {
			panic(err)
		}

		printDoc(doc)
	}

}
Output:

Resolved printer: Printing document
-------
Document id: "idInvoked"
Document description: "A document description"
Document printer: "Printing document"
Document size: 100
Document page: 42
Document name: "A simple string"
Document read count: 32
-------
Document id: ""
Document description: "A document description"
Document printer: "Printing document"
Document size: 0
Document page: 42
Document name: "A simple string"
Document read count: 32
-------
Document id: ""
Document description: "A document description"
Document printer: "Printing document"
Document size: 0
Document page: 42
Document name: "A simple string"
Document read count: 32

func New

func New() *Injector

func (*Injector) Apply

func (inj *Injector) Apply(val any) error

func (*Injector) ApplyContext added in v1.0.0

func (inj *Injector) ApplyContext(ctx context.Context, val any) error

func (*Injector) Build added in v1.0.0

func (inj *Injector) Build(ctors ...any) error

Build automatically resolves all provided types using background context. This will trigger the creation of all registered constructors, ensuring that all dependencies are properly instantiated.

Example

ExampleInjector_Build demonstrates eager dependency building. The Build and BuildContext methods allow you to eagerly instantiate all provided dependencies at once. This is useful for: - Application startup initialization - Validating that all dependencies can be created successfully - Pre-warming expensive dependencies - Ensuring deterministic dependency creation order

package main

import (
	"fmt"

	"github.com/theplant/inject"
)

// Define interfaces and implementations
type Printer interface {
	Print() string
}

type SimplePrinter struct{}

func (p *SimplePrinter) Print() string {
	return "Printing document"
}

func main() {
	inj := inject.New()

	// Provide dependencies
	if err := inj.Provide(
		func() string { return "config-value" },
		func() int { return 42 },
		func(s string) Printer {
			fmt.Printf("Creating printer with config: %s\n", s)
			return &SimplePrinter{}
		},
	); err != nil {
		panic(err)
	}

	// Build all dependencies eagerly
	if err := inj.Build(); err != nil {
		panic(err)
	}

	fmt.Println("All dependencies built successfully!")

	// All dependencies are now instantiated and ready to use
	var printer Printer
	if err := inj.Resolve(&printer); err != nil {
		panic(err)
	}

	fmt.Println("Resolved printer:", printer.Print())

}
Output:

Creating printer with config: config-value
All dependencies built successfully!
Resolved printer: Printing document

func (*Injector) BuildContext added in v1.0.0

func (inj *Injector) BuildContext(ctx context.Context, ctors ...any) error

BuildContext automatically resolves all provided types. This will trigger the creation of all registered constructors, ensuring that all dependencies are properly instantiated.

func (*Injector) Invoke

func (inj *Injector) Invoke(f any) ([]any, error)

func (*Injector) InvokeContext added in v1.0.0

func (inj *Injector) InvokeContext(ctx context.Context, f any) ([]any, error)

func (*Injector) Provide

func (inj *Injector) Provide(ctors ...any) (xerr error)

func (*Injector) Resolve

func (inj *Injector) Resolve(refs ...any) error

func (*Injector) ResolveContext added in v1.0.0

func (inj *Injector) ResolveContext(ctx context.Context, refs ...any) error

func (*Injector) SetParent

func (inj *Injector) SetParent(parent *Injector) error

type Resolver added in v1.2.0

type Resolver interface {
	Resolve(...any) error
	ResolveContext(context.Context, ...any) error
}

Resolver is an interface for resolving dependencies from an injector.

type Slice added in v1.1.2

type Slice[T any] []T

Slice is a container type that collects all *Element[T] values into a slice of T. Use Slice[T] to resolve multiple *Element[T] providers at once.

type Void added in v1.2.0

type Void struct{}

Void is a marker type for constructors that don't return any value. When a constructor has no return type (or only returns error), it is automatically treated as returning *Element[*Void].

This allows "side-effect only" constructors (e.g., modifying config) to be registered and executed through the injection system.

Usage:

// Void constructor - automatically returns *Element[*Void]
inj.Provide(func(cfg *Config) {
    cfg.Debug = true  // Modify config as side effect
})

// Execute all void constructors via Build
inj.Build()

// Or resolve explicitly
var voids Slice[*Void]
inj.Resolve(&voids)

Directories

Path Synopsis

Jump to

Keyboard shortcuts

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