Dependency Injection

Goose includes a powerful IoC (Inversion of Control) container for dependency injection, making it easy to manage service dependencies.

How It Works

Dependencies are injected automatically using struct tags:

type UserController struct {
    service *UserService `inject:""`  // Injected automatically
    log     types.Log    `inject:""`  // Framework service
    cache   *cache.Cache `inject:""`  // Module service
}

Basic Injection

Service Injection

// Define a service
type UserService struct {
    db *sql.Db `inject:""`  // Database injected
}

// Use the service
func (s *UserService) GetUser(id string) (*User, error) {
    var user User
    err := s.db.First(&user, "id = ?", id).Error
    return &user, err
}

Controller Injection

type UserController struct {
    service *UserService `inject:""`
}

func (c *UserController) Show(ctx types.Context) any {
    id := ctx.Param("id")
    return c.service.GetUser(id)
}

Injection Tags

Default Injection

The empty inject:"" tag resolves by type:

type MyService struct {
    db *sql.Db `inject:""` // Resolves *sql.Db type
}

Named Injection

Use inject:"name" for named services:

type MyService struct {
    primaryDB   *sql.Db `inject:"primary"`
    secondaryDB *sql.Db `inject:"secondary"`
}

Type Injection

Use inject:"type" to explicitly resolve by type:

type MyService struct {
    logger types.Log `inject:"type"`
}

Registering Services

In Module Declarations

Most services are registered through module declarations:

func (m *AppModule) Declarations() []any {
    return []any{
        &UserService{},      // Registered automatically
        &UserController{},
        &OrderService{},
    }
}

Using Initializers

For custom registration, use initializers:

func main() {
    initializers := []func(types.Container) error{
        func(c types.Container) error {
            // Register with factory function
            c.Register(func() *MyCustomService {
                return &MyCustomService{
                    apiKey: os.Getenv("API_KEY"),
                }
            }, "", true)
            return nil
        },
    }

    stop, err := goose.Start(goose.API(platform, module, initializers))
}

Factory Functions

Register services using factory functions:

// Register a singleton
container.Register(func(db *sql.Db) *UserRepository {
    return &UserRepository{db: db}
}, "", true)  // true = singleton

// Register a transient (new instance each time)
container.Register(func() *RequestContext {
    return &RequestContext{}
}, "", false)  // false = transient

Resolving Dependencies

Automatic Resolution

Dependencies are resolved automatically when needed:

type OrderService struct {
    userService    *UserService    `inject:""`  // Auto-resolved
    productService *ProductService `inject:""`  // Auto-resolved
    db             *sql.Db         `inject:""`  // Auto-resolved
}

// When OrderService is created, all dependencies are injected

Manual Resolution

You can also resolve manually:

func customInitializer(container types.Container) error {
    // Resolve by type
    userService := container.Resolve(&UserService{})

    // Use the service
    return nil
}

Service Lifetime

Singleton

One instance shared across the application:

// Registered as singleton (default)
container.Register(func() *DatabaseConnection {
    return &DatabaseConnection{}
}, "", true)  // true = singleton

Transient

New instance created each time:

container.Register(func() *RequestScope {
    return &RequestScope{}
}, "", false)  // false = transient

Dependency Graph

Goose automatically resolves the dependency graph:

// These dependencies are resolved in correct order
type OrderController struct {
    service *OrderService `inject:""`
}

type OrderService struct {
    repo    *OrderRepository `inject:""`
    events  *EventDispatcher `inject:""`
}

type OrderRepository struct {
    db *sql.Db `inject:""`
}

// Resolution order: sql.Db โ†’ OrderRepository โ†’ EventDispatcher โ†’ OrderService โ†’ OrderController

Interface Binding

Bind interfaces to concrete implementations:

// Define interface
type UserRepositoryInterface interface {
    FindByID(id string) (*User, error)
}

// Implement interface
type SqlUserRepository struct {
    db *sql.Db `inject:""`
}

func (r *SqlUserRepository) FindByID(id string) (*User, error) {
    // Implementation
}

// Register binding
container.Register(func(db *sql.Db) UserRepositoryInterface {
    return &SqlUserRepository{db: db}
}, "", true)

Circular Dependencies

Goose detects and prevents circular dependencies:

// โŒ This will cause an error
type ServiceA struct {
    serviceB *ServiceB `inject:""`
}

type ServiceB struct {
    serviceA *ServiceA `inject:""`
}

// โœ… Solution: Use interfaces or refactor
type ServiceA struct {
    sharedService *SharedService `inject:""`
}

type ServiceB struct {
    sharedService *SharedService `inject:""`
}

Testing with DI

The DI system makes testing easy:

func TestUserController(t *testing.T) {
    // Create mock service
    mockService := &MockUserService{
        users: map[string]*User{
            "1": {ID: "1", Name: "Test User"},
        },
    }

    // Create controller with mock
    controller := &UserController{
        service: mockService,
    }

    // Test the controller
    result := controller.Show(mockContext("1"))
    // Assert...
}

Best Practices

1. Constructor Functions

Use constructor functions for complex initialization:

func NewUserService(db *sql.Db, cache *cache.Cache) *UserService {
    return &UserService{
        db:    db,
        cache: cache,
    }
}

// Register with constructor
container.Register(NewUserService, "", true)

2. Interface Segregation

Depend on interfaces, not concrete types:

// โœ… Good: Depends on interface
type OrderService struct {
    repo OrderRepositoryInterface `inject:""`
}

// โŒ Bad: Depends on concrete type
type OrderService struct {
    repo *SqlOrderRepository `inject:""`
}

3. Minimal Dependencies

Keep dependencies minimal:

// โœ… Good: Only needed dependencies
type UserService struct {
    db *sql.Db `inject:""`
}

// โŒ Bad: Too many dependencies
type UserService struct {
    db      *sql.Db      `inject:""`
    cache   *cache.Cache `inject:""`
    queue   *Queue       `inject:""`
    mailer  *Mailer      `inject:""`
    // ... more
}

Next Steps