Service Disposal

Service disposal ensures proper cleanup of resources when they’re no longer needed. godi automatically manages disposal based on service lifetimes and scope boundaries.

Disposable Interface

Services that need cleanup should implement the Disposable interface:

type Disposable interface {
    Close() error
}

Or for context-aware cleanup:

type DisposableWithContext interface {
    Close(ctx context.Context) error
}

Basic Disposal

Simple Disposable Service

type DatabaseConnection struct {
    db     *sql.DB
    logger Logger
}

func NewDatabaseConnection(config *Config, logger Logger) (*DatabaseConnection, error) {
    db, err := sql.Open("postgres", config.DatabaseURL)
    if err != nil {
        return nil, err
    }

    if err := db.Ping(); err != nil {
        db.Close()
        return nil, err
    }

    return &DatabaseConnection{
        db:     db,
        logger: logger,
    }, nil
}

// Implement Disposable
func (c *DatabaseConnection) Close() error {
    c.logger.Info("Closing database connection")
    return c.db.Close()
}

// Register as singleton - disposed when provider closes
collection.AddSingleton(NewDatabaseConnection)

Context-Aware Disposal

type MessageQueue struct {
    conn     *amqp.Connection
    channel  *amqp.Channel
    logger   Logger
}

// Implement DisposableWithContext for graceful shutdown
func (mq *MessageQueue) Close(ctx context.Context) error {
    mq.logger.Info("Closing message queue connection")

    // Close channel first
    if err := mq.closeChannel(ctx); err != nil {
        return err
    }

    // Then close connection
    return mq.closeConnection(ctx)
}

func (mq *MessageQueue) closeChannel(ctx context.Context) error {
    done := make(chan error, 1)

    go func() {
        done <- mq.channel.Close()
    }()

    select {
    case err := <-done:
        return err
    case <-ctx.Done():
        return ctx.Err()
    }
}

Disposal Order

Services are disposed in reverse order of creation (LIFO):

// Creation order:
// 1. Config (singleton)
// 2. Logger (singleton)
// 3. Database (singleton)
// 4. Cache (singleton)
// 5. Repository (scoped)
// 6. Service (scoped)

// Disposal order when scope closes:
// 1. Service (scoped)
// 2. Repository (scoped)

// Disposal order when provider closes:
// 1. Cache (singleton)
// 2. Database (singleton)
// 3. Logger (singleton)
// 4. Config (singleton)

Lifetime-Based Disposal

Singleton Disposal

Singletons are disposed when the root provider is closed:

func main() {
    collection := godi.NewServiceCollection()
    collection.AddSingleton(NewDatabaseConnection)
    collection.AddSingleton(NewCacheClient)

    provider, err := collection.BuildServiceProvider()
    if err != nil {
        log.Fatal(err)
    }

    // Ensure cleanup on exit
    defer provider.Close() // Disposes all singletons

    // Use the application...
}

Scoped Disposal

Scoped services are disposed when their scope is closed:

func HandleRequest(provider godi.ServiceProvider) http.HandlerFunc {
    return func(w http.ResponseWriter, r *http.Request) {
        // Create scope for this request
        scope := provider.CreateScope(r.Context())
        defer scope.Close() // Disposes all scoped services

        // Handle request...
    }
}

Transient Disposal

Transient services are disposed with their containing scope:

type TempFile struct {
    file *os.File
    path string
}

func NewTempFile() (*TempFile, error) {
    file, err := os.CreateTemp("", "upload-*")
    if err != nil {
        return nil, err
    }

    return &TempFile{
        file: file,
        path: file.Name(),
    }, nil
}

func (t *TempFile) Close() error {
    t.file.Close()
    return os.Remove(t.path)
}

// Register as transient
collection.AddTransient(NewTempFile)

// Each resolution creates a new instance
// All instances disposed when scope closes

Disposal Patterns

Resource Pool

type ConnectionPool struct {
    connections []*Connection
    mu          sync.Mutex
}

func NewConnectionPool(size int) *ConnectionPool {
    pool := &ConnectionPool{
        connections: make([]*Connection, 0, size),
    }

    // Pre-create connections
    for i := 0; i < size; i++ {
        conn := createConnection()
        pool.connections = append(pool.connections, conn)
    }

    return pool
}

func (p *ConnectionPool) Close() error {
    p.mu.Lock()
    defer p.mu.Unlock()

    var errs []error

    // Close all connections
    for _, conn := range p.connections {
        if err := conn.Close(); err != nil {
            errs = append(errs, err)
        }
    }

    p.connections = nil

    if len(errs) > 0 {
        return fmt.Errorf("failed to close %d connections", len(errs))
    }

    return nil
}

Composite Disposal

type Application struct {
    server   *http.Server
    db       *sql.DB
    cache    Cache
    logger   Logger
    metrics  *MetricsCollector
    shutdown []func() error
}

func (app *Application) Close() error {
    app.logger.Info("Shutting down application")

    // Shutdown HTTP server
    ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
    defer cancel()

    if err := app.server.Shutdown(ctx); err != nil {
        app.logger.Error("Server shutdown error", err)
    }

    // Run custom shutdown hooks
    for _, fn := range app.shutdown {
        if err := fn(); err != nil {
            app.logger.Error("Shutdown hook error", err)
        }
    }

    // Flush metrics
    if err := app.metrics.Flush(); err != nil {
        app.logger.Error("Metrics flush error", err)
    }

    // Note: db and cache are managed by DI container
    // They will be disposed automatically

    app.logger.Info("Application shutdown complete")
    return nil
}

Graceful Shutdown

type Worker struct {
    tasks    chan Task
    done     chan struct{}
    wg       sync.WaitGroup
    logger   Logger
}

func NewWorker(logger Logger) *Worker {
    w := &Worker{
        tasks:  make(chan Task, 100),
        done:   make(chan struct{}),
        logger: logger,
    }

    // Start worker goroutines
    for i := 0; i < 5; i++ {
        w.wg.Add(1)
        go w.process()
    }

    return w
}

func (w *Worker) Close() error {
    w.logger.Info("Shutting down worker")

    // Signal workers to stop
    close(w.done)

    // Wait for workers to finish current tasks
    done := make(chan struct{})
    go func() {
        w.wg.Wait()
        close(done)
    }()

    // Wait with timeout
    select {
    case <-done:
        w.logger.Info("All workers stopped gracefully")
        return nil
    case <-time.After(30 * time.Second):
        return errors.New("worker shutdown timeout")
    }
}

func (w *Worker) process() {
    defer w.wg.Done()

    for {
        select {
        case task := <-w.tasks:
            // Process task
            task.Execute()
        case <-w.done:
            return
        }
    }
}

Error Handling

Multiple Disposal Errors

type MultiResource struct {
    resources []io.Closer
}

func (m *MultiResource) Close() error {
    var errs []error

    // Close all resources, collecting errors
    for _, resource := range m.resources {
        if err := resource.Close(); err != nil {
            errs = append(errs, fmt.Errorf("resource close: %w", err))
        }
    }

    // Return combined errors
    if len(errs) > 0 {
        return errors.Join(errs...)
    }

    return nil
}

Panic Recovery

type SafeDisposable struct {
    resource io.Closer
    logger   Logger
}

func (s *SafeDisposable) Close() error {
    defer func() {
        if r := recover(); r != nil {
            s.logger.Error("Panic during disposal", "panic", r)
        }
    }()

    return s.resource.Close()
}

Testing Disposal

Verify Disposal

type MockResource struct {
    closed bool
    mu     sync.Mutex
}

func (m *MockResource) Close() error {
    m.mu.Lock()
    defer m.mu.Unlock()

    if m.closed {
        return errors.New("already closed")
    }

    m.closed = true
    return nil
}

func (m *MockResource) IsClosed() bool {
    m.mu.Lock()
    defer m.mu.Unlock()
    return m.closed
}

func TestDisposal(t *testing.T) {
    resource := &MockResource{}

    collection := godi.NewServiceCollection()
    collection.AddSingleton(func() *MockResource { return resource })

    provider, _ := collection.BuildServiceProvider()

    // Use the resource
    r, _ := godi.Resolve[*MockResource](provider)
    assert.False(t, r.IsClosed())

    // Close provider
    provider.Close()

    // Verify disposal
    assert.True(t, resource.IsClosed())
}

Test Disposal Order

func TestDisposalOrder(t *testing.T) {
    var order []string

    createResource := func(name string) func() Disposable {
        return func() Disposable {
            return &TestDisposable{
                name: name,
                onClose: func() {
                    order = append(order, name)
                },
            }
        }
    }

    collection := godi.NewServiceCollection()
    collection.AddSingleton(createResource("first"))
    collection.AddSingleton(createResource("second"))
    collection.AddSingleton(createResource("third"))

    provider, _ := collection.BuildServiceProvider()

    // Force creation in order
    godi.Resolve[Disposable](provider) // first
    godi.Resolve[Disposable](provider) // second
    godi.Resolve[Disposable](provider) // third

    // Close and verify LIFO order
    provider.Close()

    assert.Equal(t, []string{"third", "second", "first"}, order)
}

Best Practices

1. Always Implement Disposal for Resources

// ✅ Good - implements disposal
type FileWriter struct {
    file *os.File
}

func (w *FileWriter) Close() error {
    return w.file.Close()
}

// ❌ Bad - leaks file handle
type BadFileWriter struct {
    file *os.File
    // No Close method!
}

2. Use Defer for Scopes

// ✅ Good - always cleans up
func ProcessRequest(provider godi.ServiceProvider) {
    scope := provider.CreateScope(context.Background())
    defer scope.Close()

    // Process...
}

// ❌ Bad - might leak on error
func BadProcessRequest(provider godi.ServiceProvider) {
    scope := provider.CreateScope(context.Background())

    // Process...

    scope.Close() // Might not be called
}

3. Handle Disposal Errors

// ✅ Good - logs disposal errors
func (s *Service) Close() error {
    if err := s.db.Close(); err != nil {
        s.logger.Error("Failed to close database", err)
        return err
    }

    if err := s.cache.Close(); err != nil {
        s.logger.Error("Failed to close cache", err)
        return err
    }

    return nil
}

4. Make Disposal Idempotent

// ✅ Good - safe to call multiple times
type IdempotentResource struct {
    resource io.Closer
    closed   int32
}

func (r *IdempotentResource) Close() error {
    if !atomic.CompareAndSwapInt32(&r.closed, 0, 1) {
        return nil // Already closed
    }

    return r.resource.Close()
}

5. Document Disposal Behavior

// NewTempProcessor creates a processor that manages temporary files.
// The processor and all temporary files are automatically cleaned up
// when the containing scope is disposed.
//
// Disposal behavior:
// - Flushes any pending data
// - Deletes all temporary files
// - Closes the underlying writer
func NewTempProcessor(writer io.Writer) *TempProcessor {
    // ...
}

Summary

Proper disposal is crucial for:

  • Preventing resource leaks

  • Graceful shutdown

  • Clean test isolation

  • Production reliability

godi’s automatic disposal management based on service lifetimes ensures resources are cleaned up properly without manual intervention.