Core Concepts

Understanding these core concepts will help you use godi effectively.

Dependency Injection

Dependency injection (DI) is a pattern where objects receive their dependencies rather than creating them. godi automates this process:

// Without DI - tight coupling
type Service struct {
    logger *Logger
}

func NewService() *Service {
    return &Service{
        logger: NewLogger(), // Creates its own dependency
    }
}

// With DI - loose coupling
type Service struct {
    logger Logger
}

func NewService(logger Logger) *Service {
    return &Service{
        logger: logger, // Receives dependency
    }
}

Services and Constructors

What is a Service?

A service is any type that provides functionality in your application:

type Logger interface { Log(string) }
type Database interface { Query(string) Result }
type EmailSender interface { Send(Email) error }

What is a Constructor?

A constructor is a function that creates a service. godi calls these automatically:

// Simple constructor
func NewLogger() Logger {
    return &logger{}
}

// Constructor with dependencies
func NewEmailService(logger Logger, smtp SMTPClient) EmailService {
    return &emailService{
        logger: logger,
        smtp:   smtp,
    }
}

The Service Collection

The collection is where you register all your services:

services := godi.NewCollection()

// Register services
services.AddSingleton(NewLogger)
services.AddSingleton(NewDatabase)
services.AddScoped(NewEmailService)

The Provider

The provider is built from the collection and manages service creation:

// Build provider from collection
provider, err := services.Build()
if err != nil {
    // Handle build errors (circular dependencies, etc.)
}
defer provider.Close()

// Use provider to resolve services
logger := godi.MustResolve[Logger](provider)

Service Lifetimes

Every service has a lifetime that determines when it’s created:

Singleton

Created once, shared everywhere:

services.AddSingleton(NewDatabaseConnection)
// Same instance returned every time

Scoped

Created once per scope (e.g., per HTTP request):

services.AddScoped(NewRequestContext)
// Different instance for each scope

Transient

Created fresh every time:

services.AddTransient(NewTempFileHandler)
// New instance on every resolution

Dependency Resolution

godi automatically builds a dependency graph and creates services in the correct order:

// godi sees this dependency chain:
// UserService → Database → Logger

func NewLogger() Logger { }
func NewDatabase(logger Logger) Database { }
func NewUserService(db Database) UserService { }

services.AddSingleton(NewLogger)
services.AddSingleton(NewDatabase)
services.AddSingleton(NewUserService)

// godi creates them in order:
// 1. Logger (no dependencies)
// 2. Database (needs Logger)
// 3. UserService (needs Database)

Scopes

Scopes provide isolation between different contexts (like HTTP requests):

// In an HTTP handler
func Handler(provider godi.Provider) http.HandlerFunc {
    return func(w http.ResponseWriter, r *http.Request) {
        // Create isolated scope for this request
        scope, _ := provider.CreateScope(r.Context())
        defer scope.Close()

        // Services resolved in this scope are isolated
        service := godi.MustResolve[MyService](scope)
    }
}

Type Safety with Generics

godi uses Go generics for compile-time type safety:

// Type-safe resolution
logger := godi.MustResolve[Logger](provider)
db := godi.MustResolve[Database](provider)

// Compile error if type doesn't match
// user := godi.MustResolve[string](provider) // Error!

Resource Cleanup

Services implementing Disposable are automatically cleaned up:

type FileHandler struct {
    file *os.File
}

func (f *FileHandler) Close() error {
    return f.file.Close()
}

// Automatically closed when scope/provider closes

Complete Example

Putting it all together:

package main

import (
    "fmt"
    "github.com/junioryono/godi/v4"
)

// Define services
type Config interface { GetDBURL() string }
type Logger interface { Log(string) }
type Database interface { Connect() error }
type UserService interface { GetUser(int) string }

// Constructors
func NewConfig() Config { return &config{url: "localhost:5432"} }
func NewLogger() Logger { return &logger{} }
func NewDatabase(cfg Config, log Logger) Database {
    return &database{url: cfg.GetDBURL(), logger: log}
}
func NewUserService(db Database, log Logger) UserService {
    return &userService{db: db, logger: log}
}

func main() {
    // Setup DI
    services := godi.NewCollection()
    services.AddSingleton(NewConfig)
    services.AddSingleton(NewLogger)
    services.AddSingleton(NewDatabase)
    services.AddScoped(NewUserService)

    // Build and use
    provider, _ := services.Build()
    defer provider.Close()

    userService := godi.MustResolve[UserService](provider)
    fmt.Println(userService.GetUser(1))
}

Next Steps

Now that you understand the core concepts: