ioc

package module
v0.0.0-...-c4b4ffb Latest Latest
Warning

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

Go to latest
Published: Nov 6, 2025 License: MIT Imports: 6 Imported by: 0

README

IoC Container for Go

中文

Go Version License

A powerful and lightweight Inversion of Control (IoC) container for Go applications that provides comprehensive dependency injection capabilities.

Features

  • Type-safe generics for Go 1.21+
  • Hierarchical containers with parent-child relationships
  • Factory pattern for lazy initialization
  • Named bindings for multiple implementations
  • Tag-based injection using struct field tags
  • Context integration for request-scoped dependencies
  • Interface compatibility checking and conversion
  • Circular dependency detection
  • Singleton and instance lifetime management

Installation

go get zestack.dev/ioc

Quick Start

Basic Usage
package main

import (
    "context"
    "fmt"
    "zestack.dev/ioc"
)

type Database interface {
    Query() string
}

type MySQLDatabase struct{}

func (db *MySQLDatabase) Query() string {
    return "MySQL query result"
}

type UserService struct {
    DB Database `ioc:""`
}

func main() {
    // Bind database implementation
    ioc.Bind(&MySQLDatabase{})

    // Get service with dependency injection
    ctx := ioc.NewContext(context.Background())
    service, err := ioc.Get[*UserService](ctx)
    if err != nil {
        panic(err)
    }

    fmt.Println(service.DB.Query()) // Output: MySQL query result
}
Named Bindings
type Database interface {
    Query() string
}

type MySQLDatabase struct{}
func (db *MySQLDatabase) Query() string { return "MySQL" }

type PostgreSQLDatabase struct{}
func (db *PostgreSQLDatabase) Query() string { return "PostgreSQL" }

type Service struct {
    PrimaryDB Database `ioc:"primary"`
    CacheDB   Database `ioc:"cache"`
}

func main() {
    ioc.NamedBind("primary", &MySQLDatabase{})
    ioc.NamedBind("cache", &PostgreSQLDatabase{})

    service := &Service{}
    if err := ioc.Resolve(service); err != nil {
        panic(err)
    }

    fmt.Println(service.PrimaryDB.Query()) // MySQL
    fmt.Println(service.CacheDB.Query())   // PostgreSQL
}
Factory Pattern
type DatabaseConfig struct {
    Host string
    Port int
}

type Database struct {
    config DatabaseConfig
}

func main() {
    // Bind factory function
    err := ioc.Factory(func() *Database {
        return &Database{
            config: DatabaseConfig{
                Host: "localhost",
                Port: 3306,
            },
        }
    })
    if err != nil {
        panic(err)
    }

    // Get instance (factory will be called)
    db, err := ioc.Get[*Database](context.Background())
    if err != nil {
        panic(err)
    }

    fmt.Printf("Database config: %+v\n", db.config)
}
Shared (Singleton) Factory
type ConnectionPool struct {
    connections int
}

func main() {
    // Bind as shared (singleton)
    err := ioc.Factory(func() *ConnectionPool {
        return &ConnectionPool{connections: 10}
    }, true) // shared = true
    if err != nil {
        panic(err)
    }

    // All requests get the same instance
    pool1, err := ioc.Get[*ConnectionPool](context.Background())
    if err != nil {
        panic(err)
    }
    pool2, err := ioc.Get[*ConnectionPool](context.Background())
    if err != nil {
        panic(err)
    }

    fmt.Printf("Same instance: %v\n", pool1 == pool2) // true
}
Hierarchical Containers
func main() {
    // Create child container
    child := ioc.NewChild()

    // Bind different implementations
    ioc.Bind(&MySQLDatabase{})
    child.Bind(&PostgreSQLDatabase{})

    // Context with child container
    ctx := child.NewContext(context.Background())

    // Gets from child container
    db, err := ioc.Get[*Database](ctx)
    if err != nil {
        panic(err)
    }

    fmt.Println(db.Query()) // PostgreSQL
}
Function Invocation

The IoC container provides three ways to invoke functions with dependency injection:

func GetUserName(service *UserService, db *Database) string {
    // Returns single value
    return service.GetName()
}

func ProcessUser(service *UserService, db *Database) (*User, error) {
    // Returns value with error (common pattern)
    return service.Process()
}

func main() {
    ioc.Bind(&UserService{})
    ioc.Bind(&Database{})
    ctx := context.Background()

    // 1. Call[T] - for single return value
    name, err := ioc.Call[string](ctx, GetUserName)
    if err != nil {
        panic(err)
    }
    fmt.Println(name)

    // 2. Call2[T] - for (result, error) pattern
    user, err := ioc.Call2[*User](ctx, ProcessUser)
    if err != nil {
        panic(err)
    }
    fmt.Println(user)

    // 3. Invoke - for complex cases with multiple return values
    results, err := ioc.Invoke(ctx, func(s *UserService) (int, string, bool) {
        return 1, "test", true
    })
    if err != nil {
        panic(err)
    }
    // Handle results...
}

API Reference

Global Functions
  • Bind(instance any) - Bind a concrete instance
  • NamedBind(name string, instance any) - Bind a named instance
  • Factory(factory any, shared ...bool) error - Bind factory function
  • NamedFactory(name string, factory any, shared ...bool) error - Bind named factory
  • Get[T any](ctx context.Context) (*T, error) - Get instance by type
  • NamedGet[T any](ctx context.Context, name string) (*T, error) - Get named instance
  • Resolve(i any) error - Resolve dependencies in struct
  • Invoke(ctx context.Context, f any) ([]reflect.Value, error) - Invoke function with injection
  • Call[T](ctx context.Context, f any) (T, error) - Invoke function and return single typed result
  • Call2[T](ctx context.Context, f any) (T, error) - Invoke function with (result, error) pattern
  • NewContext(parentCtx context.Context) context.Context - Create context with container
  • FromContext(ctx context.Context) (*Container, bool) - Get container from context
Container Methods

The container implements all the above methods with receiver syntax:

c := ioc.New()
c.Bind(&Service{})
err := c.Factory(func() *Database { return &Database{} })
service, err := c.Get(reflect.TypeOf(&Service{}))
Tag Options

Use struct field tags for dependency injection:

type Service struct {
    // Named dependency
    DB Database `ioc:"primary"`

    // Optional dependency (won't fail if not found)
    Cache Cache `ioc:"cache,optional"`

    // Default binding (empty name)
    Logger Logger `ioc:""`
}

Advanced Features

Interface Compatibility

The container automatically handles interface implementations:

type Writer interface {
    Write(string) error
}

type FileWriter struct{}

func (fw *FileWriter) Write(s string) error {
    return nil
}

// Both will work for Writer interface
ioc.Bind(&FileWriter{})           // Direct binding
ioc.Bind[Writer](&FileWriter{})   // Interface binding
Context Integration

Use contexts for request-scoped containers:

func HandleRequest(ctx context.Context) {
    // Get container from context
    container, ok := ioc.FromContext(ctx)
    if !ok {
        // Handle case where container is not in context
        return
    }

    // Or use generic functions
    service, err := ioc.Get[*Service](ctx)
    if err != nil {
        // Handle error appropriately
        return
    }
}
Error Handling

The package emphasizes proper error handling with explicit error returns:

// Proper error handling
service, err := ioc.Get[*Service](ctx)
if err != nil {
    // Handle error appropriately: log, return, use default value, etc.
    return fmt.Errorf("failed to get service: %w", err)
}

Best Practices

  1. Use interfaces for better testability and flexibility
  2. Prefer named bindings when multiple implementations exist
  3. Use shared factories for expensive resources
  4. Leverage hierarchical containers for different environments
  5. Handle optional dependencies with optional tag

Performance

The IoC container is designed for performance:

  • Minimal reflection overhead after initial binding
  • Efficient type lookup using hash maps
  • Lazy initialization with factory pattern
  • Shared instances to reduce object creation

License

This project is licensed under the MIT License - see the LICENSE file for details.

Contributing

Contributions are welcome! Please feel free to submit a Pull Request.

Acknowledgments

Inspired by modern dependency injection patterns and designed specifically for Go applications.

Documentation

Overview

Package ioc provides a powerful and lightweight Inversion of Control (IoC) container for Go applications with comprehensive dependency injection capabilities.

Package ioc provides a powerful and lightweight Inversion of Control (IoC) container for Go applications with comprehensive dependency injection capabilities.

Package ioc provides a powerful and lightweight Inversion of Control (IoC) container for Go applications with comprehensive dependency injection capabilities.

This file defines error types and error formatting functions used throughout the IoC container system to provide consistent and detailed error messages.

Package ioc provides a powerful and lightweight Inversion of Control (IoC) container for Go applications with comprehensive dependency injection capabilities.

Package ioc provides a powerful and lightweight Inversion of Control (IoC) container for Go applications with comprehensive dependency injection capabilities.

Example

Example demonstrates basic usage of the IoC container

package main

import (
	"context"
	"fmt"

	"go-slim.dev/ioc"
)

func main() {
	// Create services
	type Database struct {
		Name string
	}

	type UserService struct {
		DB *Database
	}

	// Bind instances to the container
	ioc.Bind(&Database{Name: "postgres"})
	ioc.Bind(&UserService{})

	// Retrieve instances
	ctx := context.Background()
	db, _ := ioc.Get[*Database](ctx)
	fmt.Println((*db).Name)

}
Output:
postgres

Index

Examples

Constants

This section is empty.

Variables

View Source
var (
	// ErrValueNotFound is returned when a requested value cannot be found in the container
	ErrValueNotFound = errors.New("ioc: value not found")

	// ErrFactoryFailed is returned when a factory function fails to create an instance
	ErrFactoryFailed = errors.New("ioc: factory function failed")

	// ErrInvalidType is returned when an invalid type is requested
	ErrInvalidType = errors.New("ioc: invalid type")

	// ErrDependencyResolution is returned when dependency resolution fails
	ErrDependencyResolution = errors.New("ioc: dependency resolution failed")
)

Functions

func Bind

func Bind(instance any)

Bind binds a value to the container. Valid types include:

  • Concrete implementation values of interfaces
  • Struct instances
  • Type values (avoid using primitive types, prefer using element type variants)

func Call

func Call[T any](ctx context.Context, fn any) (T, error)

Call executes the given function with automatic dependency injection and returns a single typed result. The function must return exactly one value of type T. This is a convenience wrapper around Invoke for functions with a single return value.

Example:

result, err := ioc.Call[string](ctx, func(db *Database) string {
    return db.Query()
})
Example

ExampleCall demonstrates function invocation with dependency injection

package main

import (
	"context"
	"fmt"

	"go-slim.dev/ioc"
)

func main() {
	type Greeter struct {
		Prefix string
	}

	ioc.Bind(&Greeter{Prefix: "Hello"})

	// Call function with automatic dependency injection
	greeting, _ := ioc.Call[string](context.Background(), func(g *Greeter) string {
		return g.Prefix + ", World!"
	})

	fmt.Println(greeting)

}
Output:
Hello, World!

func Call2

func Call2[T any](ctx context.Context, fn any) (T, error)

Call2 executes the given function with automatic dependency injection and returns a typed result with error. The function must return exactly two values: a result of type T and an error. This is a convenience wrapper around Invoke for functions following the common (T, error) pattern.

Example:

user, err := ioc.Call2[*User](ctx, func(repo *UserRepository) (*User, error) {
    return repo.FindByID(123)
})
if err != nil {
    return err
}
Example

ExampleCall2 demonstrates function invocation with error handling

package main

import (
	"context"
	"fmt"

	"go-slim.dev/ioc"
)

func main() {
	type Validator struct {
		Enabled bool
	}

	ioc.Bind(&Validator{Enabled: true})

	// Call function that returns (result, error)
	result, err := ioc.Call2[string](context.Background(), func(v *Validator) (string, error) {
		if v.Enabled {
			return "Valid", nil
		}
		return "", fmt.Errorf("validation disabled")
	})

	if err != nil {
		fmt.Println("Error:", err)
		return
	}

	fmt.Println(result)

}
Output:
Valid

func Factory

func Factory(factory any, shared ...bool) error

Factory binds a factory function to the container. Factory functions will be called lazily when the type is requested. The shared parameter determines if the factory result should be cached (singleton).

Example

ExampleFactory demonstrates how to use factory functions

package main

import (
	"context"
	"fmt"

	"go-slim.dev/ioc"
)

func main() {
	type Config struct {
		Port int
	}

	// Register a factory function
	ioc.Factory(func() *Config {
		return &Config{Port: 8080}
	})

	// Retrieve the instance
	ctx := context.Background()
	config, _ := ioc.Get[*Config](ctx)
	fmt.Println((*config).Port)

}
Output:
8080
Example (Shared)

ExampleFactory_shared demonstrates shared (singleton) factory

package main

import (
	"context"
	"fmt"

	"go-slim.dev/ioc"
)

func main() {
	type Counter struct {
		Value int
	}

	counter := &Counter{Value: 0}

	// Shared factory returns the same instance every time
	ioc.Factory(func() *Counter {
		counter.Value++
		return counter
	}, true) // shared = true

	ctx := context.Background()

	// Get the instance twice
	c1, _ := ioc.Get[*Counter](ctx)
	c2, _ := ioc.Get[*Counter](ctx)

	// Both return the same instance (Value was incremented only once)
	fmt.Println((*c1).Value)
	fmt.Println((*c1) == (*c2)) // Same instance

}
Output:
1
true

func Get

func Get[T any](ctx context.Context) (*T, error)

Get retrieves a value of the specified type from the container. Generic type T should be a struct pointer. For interface instances, use the Instance function to get a Container instance, then use that container to get the concrete implementation of the interface.

func GetFrom

func GetFrom[T any](c *Container) (*T, error)

GetFrom retrieves a value of the specified type from the given container. Generic type T should be a struct pointer. For interface instances, use the Container methods directly to get the concrete implementation. This function provides direct access to a specific container without context.

func Invoke

func Invoke(ctx context.Context, fn any) ([]reflect.Value, error)

Invoke executes the given function with automatic dependency injection. Function parameters are resolved from the container and injected automatically. It first checks the context for a container, then falls back to the default container.

func NamedBind

func NamedBind(name string, instance any)

NamedBind binds a named value to the container. Named bindings allow multiple implementations of the same type with different names.

Example

ExampleNamedBind demonstrates multiple implementations with names

package main

import (
	"context"
	"fmt"

	"go-slim.dev/ioc"
)

func main() {
	type Logger struct {
		Level string
	}

	// Bind multiple loggers with different names
	ioc.NamedBind("debug", &Logger{Level: "DEBUG"})
	ioc.NamedBind("info", &Logger{Level: "INFO"})

	ctx := context.Background()

	// Get specific logger by name
	debugLogger, _ := ioc.NamedGet[*Logger](ctx, "debug")
	infoLogger, _ := ioc.NamedGet[*Logger](ctx, "info")

	fmt.Println((*debugLogger).Level)
	fmt.Println((*infoLogger).Level)

}
Output:
DEBUG
INFO

func NamedFactory

func NamedFactory(name string, factory any, shared ...bool) error

NamedFactory binds a named factory function to the container. Named factories allow multiple factory functions for the same type with different names.

func NamedGet

func NamedGet[T any](ctx context.Context, name string) (*T, error)

NamedGet retrieves a value of the specified type with the given name from the container. It first checks the context for a container, then falls back to the defaultContainer container.

func NamedGetFrom

func NamedGetFrom[T any](c *Container, name string) (*T, error)

NamedGetFrom retrieves a value of the specified type with the given name from the given container. This function provides direct access to a specific container without context. The container parameter specifies which container to retrieve the value from.

func NewContext

func NewContext(parentCtx context.Context) context.Context

NewContext creates a new context with the defaultContainer container embedded. The returned context can be used to retrieve the container via FromContext. The parentCtx must not be nil. Use context.Background() or context.TODO() if you don't have a context.

Example

ExampleNewContext demonstrates passing container through context

package main

import (
	"context"
	"fmt"

	"go-slim.dev/ioc"
)

func main() {
	type RequestID struct {
		ID string
	}

	// Create a request-specific container
	requestContainer := ioc.New()
	requestContainer.Bind(&RequestID{ID: "req-123"})

	// Create context with container
	ctx := requestContainer.NewContext(context.Background())

	// Retrieve from context
	reqID, _ := ioc.Get[*RequestID](ctx)
	fmt.Println((*reqID).ID)

}
Output:
req-123

func Resolve

func Resolve(i any) error

Resolve performs dependency injection on the given value. The value must be a pointer to a struct. Struct fields can be annotated with the "ioc" tag to specify which dependencies should be injected.

Example

ExampleResolve demonstrates struct field injection with tags

package main

import (
	"fmt"

	"go-slim.dev/ioc"
)

func main() {
	type Logger struct {
		Name string
	}

	type Service struct {
		Logger *Logger `ioc:"myLogger"`
	}

	// Bind with name
	ioc.NamedBind("myLogger", &Logger{Name: "app-logger"})

	// Create struct and resolve dependencies
	svc := &Service{}
	ioc.Resolve(svc)

	fmt.Println(svc.Logger.Name)

}
Output:
app-logger

Types

type Container

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

Container represents a service container that manages dependencies and their lifecycle. Thread-safe implementation using separate RWMutex for different data structures.

func Default

func Default() *Container

Default returns the default container instance. This is useful for testing or when you need direct access to the default container.

func FromContext

func FromContext(ctx context.Context) (*Container, bool)

FromContext retrieves the container from the given context. Returns the container and true if found in context, or nil and false if not found. Note: Passing nil context will cause a panic; use context.TODO() if unsure.

func New

func New() *Container

New creates a new service container instance.

func NewChild

func NewChild() *Container

NewChild creates a child container from the default container. The child container starts empty but can access parent's services. Services registered in the child won't affect the default parent container.

Example

ExampleNewChild demonstrates hierarchical containers

package main

import (
	"context"
	"fmt"

	"go-slim.dev/ioc"
)

func main() {
	type Config struct {
		Env string
	}

	// Bind to default container
	ioc.Bind(&Config{Env: "production"})

	// Create child container with different config
	child := ioc.NewChild()
	child.Bind(&Config{Env: "testing"})

	ctx := context.Background()

	// Default container returns production config
	prodConfig, _ := ioc.Get[*Config](ctx)
	fmt.Println((*prodConfig).Env)

	// Child container returns testing config
	testConfig, _ := ioc.GetFrom[*Config](child)
	fmt.Println((*testConfig).Env)

}
Output:
production
testing

func (*Container) Bind

func (c *Container) Bind(value any)

Bind binds a concrete implementation (instance or primitive value) to the container. Note: Since the internal mapping is established directly between type and concrete implementation, each type will have at most one concrete implementation.

func (*Container) Factory

func (c *Container) Factory(factory any, shared ...bool) error

Factory binds a factory function to the container. The factory function must return a concrete implementation and can optionally return an error to indicate construction failure. Similar to the Bind method, each type will have at most one factory function.

func (*Container) Get

func (c *Container) Get(t reflect.Type) (reflect.Value, error)

Get retrieves a concrete implementation value of the specified type from the container. Retrieval steps:

  1. Check cached instances with exact type match
  2. Check factory functions with exact type match
  3. Check type-compatible instances (implements interface or assignable)
  4. Check type-compatible factories (implements interface or assignable)
  5. Check parent container if available
  6. If all above fail and type is a struct or struct pointer, auto-create an instance

func (*Container) Invoke

func (c *Container) Invoke(fn any) ([]reflect.Value, error)

Invoke executes the specified function with automatic dependency injection from the service container.

func (*Container) NamedBind

func (c *Container) NamedBind(name string, value any)

NamedBind binds a named concrete implementation (instance or primitive value) to the container. Since this concrete implementation has a name, it won't override the concrete implementation bound through the Bind method. This allows specifying different concrete implementations for the same type in different scenarios and use cases. Structs can select the bound concrete implementation through the `ioc` tag during dependency injection.

func (*Container) NamedFactory

func (c *Container) NamedFactory(name string, factory any, shared ...bool) error

NamedFactory binds a named factory function to the container. The implementation approach is similar to the NamedBind method.

func (*Container) NamedGet

func (c *Container) NamedGet(name string, t reflect.Type) (reflect.Value, error)

NamedGet retrieves a named concrete implementation value of the specified type from the container. Works exactly like Get but matches bindings by both name and type. The retrieval follows the same priority order as Get, checking instances, factories, parent container, and auto-creation for struct types.

func (*Container) NewChild

func (c *Container) NewChild() *Container

NewChild creates a child container with this container as its parent. The child container:

  • Starts empty (no bindings copied)
  • Can access parent's services through lookup chain
  • Can override parent's services without affecting parent
  • Useful for creating scoped containers (e.g., per-request)

func (*Container) NewContext

func (c *Container) NewContext(parentCtx context.Context) context.Context

NewContext creates a new context with the container embedded. The returned context can be used to retrieve the container via FromContext. The parentCtx must not be nil. Use context.Background() or context.TODO() if you don't have a context.

func (*Container) Resolve

func (c *Container) Resolve(i any) error

Resolve performs dependency injection on the given value. In structs, you can specify the name of the concrete implementation to use through the `ioc` tag to complete the injection.

Jump to

Keyboard shortcuts

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