Resource Management
Learn how godi manages resources and ensures proper cleanup.
The Disposable Interface
Implementing Disposable
Any service that needs cleanup should implement the Disposable interface:
type DatabaseConnection interface {
godi.Disposable
Query(query string) Result
}
// Example: Database connection
type databaseConnection struct {
conn *sql.DB
}
func NewDatabaseConnection(config *Config) DatabaseConnection {
conn, _ := sql.Open("postgres", config.DatabaseURL)
return &databaseConnection{conn: conn}
}
func (d *databaseConnection) Query(query string) Result {
// Execute query...
return Result{}
}
func (d *databaseConnection) Close() error {
return d.conn.Close()
}
// Automatically closed when provider/scope closes
services.AddSingleton(NewDatabaseConnection)
Common Disposable Resources
// File handles
type FileLogger struct {
file *os.File
}
func (f *FileLogger) Close() error {
return f.file.Close()
}
// Network connections
type APIClient struct {
client *http.Client
conn net.Conn
}
func (a *APIClient) Close() error {
return a.conn.Close()
}
// Background workers
type Worker struct {
done chan struct{}
}
func (w *Worker) Close() error {
close(w.done)
return nil
}
// Temporary resources
type TempDirectory struct {
path string
}
func (t *TempDirectory) Close() error {
return os.RemoveAll(t.path)
}
Cleanup Lifecycle
Disposal Order
Resources are disposed in reverse order of creation:
// Services created in order: Logger -> Database -> Cache -> Service
// Disposal happens in reverse: Service -> Cache -> Database -> Logger
type Service struct {
db *Database
cache *Cache
}
func (s *Service) Close() error {
fmt.Println("Closing Service")
return nil
}
type Cache struct {
conn *redis.Client
}
func (c *Cache) Close() error {
fmt.Println("Closing Cache")
return c.conn.Close()
}
type Database struct {
conn *sql.DB
}
func (d *Database) Close() error {
fmt.Println("Closing Database")
return d.conn.Close()
}
// When provider closes:
// Output:
// Closing Service
// Closing Cache
// Closing Database
Lifetime-Based Cleanup
Different lifetimes are cleaned up at different times:
// Singleton - cleaned up when provider closes
services.AddSingleton(NewDatabasePool)
provider.Close() // Singletons disposed here
// Scoped - cleaned up when scope closes
services.AddScoped(NewTransaction)
scope.Close() // Scoped services disposed here
// Transient - cleaned up when scope closes (if tracked)
services.AddTransient(NewTempFile)
scope.Close() // Transients created in scope disposed here
Resource Pooling
Connection Pool Pattern
type ConnectionPool struct {
connections chan *Connection
maxSize int
}
func NewConnectionPool(config *Config) *ConnectionPool {
pool := &ConnectionPool{
connections: make(chan *Connection, config.MaxConnections),
maxSize: config.MaxConnections,
}
// Pre-create connections
for i := 0; i < config.MinConnections; i++ {
conn := createConnection(config)
pool.connections <- conn
}
return pool
}
func (p *ConnectionPool) Get() *Connection {
select {
case conn := <-p.connections:
return conn
default:
return createConnection(p.config)
}
}
func (p *ConnectionPool) Put(conn *Connection) {
select {
case p.connections <- conn:
// Returned to pool
default:
// Pool full, close connection
conn.Close()
}
}
func (p *ConnectionPool) Close() error {
close(p.connections)
for conn := range p.connections {
conn.Close()
}
return nil
}
// Register as singleton for app-wide pooling
services.AddSingleton(NewConnectionPool)
Graceful Shutdown
Provider Shutdown
func main() {
services := godi.NewCollection()
services.AddSingleton(NewLogger)
services.AddSingleton(NewDatabase)
services.AddSingleton(NewWorker)
provider, _ := services.Build()
// Setup graceful shutdown
sigChan := make(chan os.Signal, 1)
signal.Notify(sigChan, os.Interrupt, syscall.SIGTERM)
// Run application
go runApp(provider)
// Wait for shutdown signal
<-sigChan
fmt.Println("Shutting down gracefully...")
// Close provider - all resources cleaned up
if err := provider.Close(); err != nil {
log.Printf("Error during shutdown: %v", err)
}
fmt.Println("Shutdown complete")
}
Background Worker Management
type BackgroundWorker struct {
done chan struct{}
wg sync.WaitGroup
ticker *time.Ticker
}
func NewBackgroundWorker(logger Logger) *BackgroundWorker {
w := &BackgroundWorker{
done: make(chan struct{}),
ticker: time.NewTicker(5 * time.Second),
}
w.wg.Add(1)
go w.run(logger)
return w
}
func (w *BackgroundWorker) run(logger Logger) {
defer w.wg.Done()
for {
select {
case <-w.done:
logger.Info("Worker shutting down")
return
case <-w.ticker.C:
// Do periodic work
w.doWork()
}
}
}
func (w *BackgroundWorker) Close() error {
// Signal shutdown
close(w.done)
// Stop ticker
w.ticker.Stop()
// Wait for worker to finish
w.wg.Wait()
return nil
}
// Worker automatically stopped when provider closes
services.AddSingleton(NewBackgroundWorker)
Transaction Management
Auto-Rollback Pattern
type Transaction struct {
tx *sql.Tx
committed bool
}
func NewTransaction(db *Database) (*Transaction, error) {
tx, err := db.Begin()
if err != nil {
return nil, err
}
return &Transaction{
tx: tx,
committed: false,
}, nil
}
func (t *Transaction) Commit() error {
err := t.tx.Commit()
if err == nil {
t.committed = true
}
return err
}
func (t *Transaction) Rollback() error {
return t.tx.Rollback()
}
func (t *Transaction) Close() error {
if !t.committed {
// Auto-rollback if not committed
return t.tx.Rollback()
}
return nil
}
// Transaction automatically rolled back if not committed
services.AddScoped(NewTransaction)
func ProcessOrder(scope godi.Scope) error {
tx := godi.MustResolve[*Transaction](scope)
// Do work...
if err := validateOrder(); err != nil {
// No need to call Rollback - auto cleanup
return err
}
return tx.Commit()
// If we don't reach Commit, transaction rolls back automatically
}
Memory Management
Preventing Memory Leaks
// ❌ Potential memory leak
type LeakyCache struct {
data map[string]*BigObject
}
func NewLeakyCache() *LeakyCache {
return &LeakyCache{
data: make(map[string]*BigObject),
}
}
// ✅ Proper cleanup
type SafeCache struct {
data map[string]*BigObject
mu sync.RWMutex
}
func NewSafeCache() *SafeCache {
return &SafeCache{
data: make(map[string]*BigObject),
}
}
func (c *SafeCache) Close() error {
c.mu.Lock()
defer c.mu.Unlock()
// Clear references
for k := range c.data {
delete(c.data, k)
}
return nil
}
Resource Limits
type RateLimiter struct {
semaphore chan struct{}
wg sync.WaitGroup
}
func NewRateLimiter(maxConcurrent int) *RateLimiter {
return &RateLimiter{
semaphore: make(chan struct{}, maxConcurrent),
}
}
func (r *RateLimiter) Execute(fn func()) {
r.wg.Add(1)
r.semaphore <- struct{}{} // Acquire
go func() {
defer r.wg.Done()
defer func() { <-r.semaphore }() // Release
fn()
}()
}
func (r *RateLimiter) Close() error {
// Wait for all operations to complete
r.wg.Wait()
close(r.semaphore)
return nil
}
Error Handling in Cleanup
Aggregating Disposal Errors
type ServiceManager struct {
services []Disposable
}
func (m *ServiceManager) AddService(service Disposable) {
m.services = append(m.services, service)
}
func (m *ServiceManager) Close() error {
var errors []error
// Close in reverse order
for i := len(m.services) - 1; i >= 0; i-- {
if err := m.services[i].Close(); err != nil {
errors = append(errors, fmt.Errorf("service %d: %w", i, err))
}
}
if len(errors) > 0 {
return fmt.Errorf("disposal errors: %v", errors)
}
return nil
}
Critical vs Non-Critical Cleanup
type ComplexService struct {
db *sql.DB // Critical
cache *Cache // Non-critical
metrics *MetricsClient // Non-critical
}
func (s *ComplexService) Close() error {
var criticalError error
// Non-critical cleanup (log but don't fail)
if err := s.metrics.Close(); err != nil {
log.Printf("Warning: failed to close metrics: %v", err)
}
if err := s.cache.Close(); err != nil {
log.Printf("Warning: failed to close cache: %v", err)
}
// Critical cleanup (return error)
if err := s.db.Close(); err != nil {
criticalError = fmt.Errorf("failed to close database: %w", err)
}
return criticalError
}
Testing Resource Management
Verify Cleanup
func TestResourceCleanup(t *testing.T) {
cleaned := false
services := godi.NewCollection()
services.AddScoped(func() *TestResource {
return &TestResource{
onClose: func() { cleaned = true },
}
})
provider, _ := services.Build()
scope, _ := provider.CreateScope(context.Background())
resource := godi.MustResolve[*TestResource](scope)
resource.DoWork()
// Verify cleanup happens
scope.Close()
assert.True(t, cleaned, "Resource should be cleaned up")
}
type TestResource struct {
onClose func()
}
func (r *TestResource) Close() error {
if r.onClose != nil {
r.onClose()
}
return nil
}
Mock Disposables
type MockConnection struct {
closed bool
mu sync.Mutex
}
func (m *MockConnection) IsClosed() bool {
m.mu.Lock()
defer m.mu.Unlock()
return m.closed
}
func (m *MockConnection) Close() error {
m.mu.Lock()
defer m.mu.Unlock()
if m.closed {
return errors.New("already closed")
}
m.closed = true
return nil
}
func TestConnectionLifecycle(t *testing.T) {
conn := &MockConnection{}
services := godi.NewCollection()
services.AddSingleton(func() *MockConnection { return conn })
provider, _ := services.Build()
// Use connection
c := godi.MustResolve[*MockConnection](provider)
assert.False(t, c.IsClosed())
// Close provider
provider.Close()
assert.True(t, conn.IsClosed())
}
Best Practices
Always implement Close() for resources needing cleanup
Use defer for cleanup in goroutines
Handle errors in Close() but don’t panic
Clean up in reverse order of creation
Test cleanup paths explicitly
Use context for cancellation in long-running operations
Log cleanup failures for debugging
Common Patterns
Resource Wrapper
type ResourceWrapper[T any] struct {
resource T
cleanup func() error
}
func NewResourceWrapper[T any](
resource T,
cleanup func() error,
) *ResourceWrapper[T] {
return &ResourceWrapper[T]{
resource: resource,
cleanup: cleanup,
}
}
func (w *ResourceWrapper[T]) Get() T {
return w.resource
}
func (w *ResourceWrapper[T]) Close() error {
if w.cleanup != nil {
return w.cleanup()
}
return nil
}
// Usage
services.AddSingleton(func() *ResourceWrapper[*os.File] {
file, _ := os.Create("app.log")
return NewResourceWrapper(file, file.Close)
})
Cleanup Chain
type CleanupChain struct {
cleanups []func() error
}
func (c *CleanupChain) Add(cleanup func() error) {
c.cleanups = append(c.cleanups, cleanup)
}
func (c *CleanupChain) Close() error {
var errs []error
// Execute in reverse order
for i := len(c.cleanups) - 1; i >= 0; i-- {
if err := c.cleanups[i](); err != nil {
errs = append(errs, err)
}
}
if len(errs) > 0 {
return fmt.Errorf("cleanup errors: %v", errs)
}
return nil
}
Next Steps
Learn about Modules
Explore Keyed Services
Understand Service Groups