# do - Dependency Injection for Go This is the official documentation for the "do" library, a modern, type-safe dependency injection framework for Go applications. The `do` library represents a paradigm shift in how Go developers approach dependency management, offering a comprehensive solution that combines simplicity with powerful features. ## About the Library do is a lightweight, fast, and type-safe dependency injection library for Go that has been carefully designed to address the common pain points developers face when building complex applications. It provides a simple and intuitive API for managing dependencies in Go applications with comprehensive support for scopes, lifecycle management, health checks, and graceful shutdown capabilities. The library was built with modern Go development practices in mind, leveraging the power of Go generics to provide compile-time type safety while maintaining excellent runtime performance. Unlike traditional reflection-based DI containers, `do` eliminates the potential runtime errors associated with reflection by using Go's type system to its fullest advantage. **Key Features:** - **๐Ÿ“’ Service registration** - Register by type - Register by name - Register multiple services from a package at once - **๐Ÿชƒ Service invocation** - Eager loading - Lazy loading - Transient loading - Tag-based invocation - Circular dependency detection - **๐Ÿง™โ€โ™‚๏ธ Service aliasing** - Implicit (provide struct, invoke interface) - Explicit (provide struct, bind interface, invoke interface) - **๐Ÿ” Service lifecycle** - Health check - Graceful unload (shutdown) - Dependency-aware parallel shutdown - Lifecycle hooks - **๐Ÿ“ฆ Scope (a.k.a module) tree** - Visibility control - Dependency grouping - **๐Ÿ“ค Container** - Dependency graph resolution and visualization - Default container - Container cloning - Service override - **๐Ÿงช Debugging & introspection** - Explain APIs: scope tree and service dependencies - Web UI & HTTP middleware (std, Gin, Fiber, Echo, Chi) - **๐ŸŒˆ Lightweight, no dependencies** - **๐Ÿ”… No code generation** - **๐Ÿ˜ท Typeโ€‘safe API** ## Why Choose do? The decision to choose a dependency injection framework is crucial for the long-term maintainability and performance of your Go applications. do stands out among the available options for several compelling reasons that address both immediate development needs and long-term architectural concerns. ### Performance Benefits - **Minimal Overhead**: The library is optimized for high-performance applications with minimal runtime cost. Benchmarks show that do introduces negligible overhead compared to manual dependency management. - **Memory Efficient**: Smart caching and lifecycle management reduce memory footprint. Services are cached appropriately based on their lifecycle, and unused scopes can be garbage collected efficiently. - **Fast Startup**: Lazy loading and efficient dependency resolution ensure that your application starts quickly, even with complex dependency graphs. - **Optimized Dependency Resolution**: The dependency resolution algorithm is highly optimized to minimize the time spent resolving dependencies at runtime. - **Concurrent Access Optimization**: Thread-safe operations are optimized to minimize lock contention in high-concurrency scenarios. ### Developer Experience - **Type Safety**: The framework uses reflection internally, but the developer only use type-safe API. Runtime depepdency resolution errors are handled gracefully - **Intuitive API**: Simple, fluent interface that feels natural to Go developers. The API follows Go conventions and idioms, making it easy to learn and use effectively. - **Rich Tooling**: Built-in debugging, health checks, and observability features provide comprehensive insights into your application's dependency graph and runtime behavior. - **Comprehensive Testing**: Easy mocking and testing with container cloning. The testing utilities make it simple to create isolated test environments with mocked dependencies. - **Excellent IDE Support**: Full IDE support with autocomplete, go-to-definition, and refactoring capabilities thanks to the use of Go generics. - **Clear Error Messages**: When things go wrong, do provides clear, actionable error messages that help you quickly identify and fix issues. ### Production Ready - **Graceful Shutdown**: Proper cleanup and resource management ensure that your application shuts down cleanly, preventing resource leaks and data corruption. - **Health Monitoring**: Built-in health check system for microservices allows you to monitor the health of your services and their dependencies in real-time. - **Error Handling**: Robust error handling with detailed diagnostics provides comprehensive information about what went wrong and why. - **Observability**: Debug web UI and comprehensive logging give you visibility into your application's dependency graph and runtime behavior. - **Production Hardened**: The library has been tested in production environments with high-traffic applications, ensuring reliability and stability. - **Backward Compatibility**: Careful attention to backward compatibility ensures that upgrades don't break existing applications. ## Documentation Structure The `do` library documentation is organized to provide a comprehensive learning path for developers at all levels, from beginners to advanced users. Each section builds upon the previous ones, ensuring a solid understanding of the library's capabilities and best practices. ### Getting Started - **Getting Started Guide** (`/docs/getting-started`) - Quick start guide for new users that walks through the basic concepts and provides a hands-on introduction to the library - **About** (`/docs/about`) - Comprehensive overview of the library and its design principles, including the philosophy behind the library's architecture - **Glossary** (`/docs/glossary`) - Detailed definitions of key terms and concepts used throughout the documentation, ensuring consistent understanding ### Core Concepts The core concepts section covers the fundamental building blocks of the `do` library, providing deep insights into how the library works and how to use it effectively. #### Service Registration - **Lazy Loading** (`/docs/service-registration/lazy-loading`) - Comprehensive guide to lazy loading services, including when to use it, performance implications, and best practices for implementation - **Eager Loading** (`/docs/service-registration/eager-loading`) - Detailed explanation of eager loading services, covering startup performance considerations and use cases where immediate initialization is beneficial - **Transient Loading** (`/docs/service-registration/transient-loading`) - In-depth coverage of transient services, including memory management, performance characteristics, and scenarios where new instances are preferred - **Package Loading** (`/docs/service-registration/package-loading`) - Advanced guide to loading services from packages, including modular architecture patterns and package organization strategies #### Service Invocation - **Service Invocation** (`/docs/service-invocation/service-invocation`) - Comprehensive guide on how to retrieve and use services, including error handling, performance optimization, and common patterns - **Accept Interfaces, Return Structs** (`/docs/service-invocation/accept-interfaces-return-structs`) - Detailed best practices for service design, including interface design principles, dependency inversion, and maintainable architecture patterns #### Container Management - **Scope** (`/docs/container/scope`) - Deep dive into understanding scope hierarchy and isolation, including lifecycle management, memory optimization, and architectural considerations - **Options** (`/docs/container/options`) - Comprehensive guide to configuration options for the DI container, including performance tuning, debugging options, and customization capabilities - **Clone** (`/docs/container/clone`) - Detailed explanation of creating copies of containers for testing, including isolation strategies, mock injection, and test environment setup #### Service Lifecycle - **Health Checker** (`/docs/service-lifecycle/healthchecker`) - Comprehensive guide to implementing health checks for services, including monitoring strategies, failure detection, and integration with monitoring systems - **Shutdowner** (`/docs/service-lifecycle/shutdowner`) - In-depth coverage of graceful shutdown of services, including resource cleanup, timeout management, and shutdown coordination strategies ### Advanced Topics The advanced topics section covers complex scenarios, troubleshooting, and migration strategies for experienced users who need to handle sophisticated use cases and maintain existing applications. #### Troubleshooting - **Service Dependencies** (`/docs/troubleshooting/service-dependencies`) - Comprehensive guide to common dependency issues, including circular dependencies, missing dependencies, and dependency resolution failures with detailed solutions and prevention strategies - **Service Registration** (`/docs/troubleshooting/service-registration`) - In-depth coverage of registration problems and solutions, including duplicate registrations, registration order issues, and scope-related registration problems - **Scope Tree** (`/docs/troubleshooting/scope-tree`) - Detailed explanation of understanding scope relationships, including scope hierarchy visualization, scope isolation issues, and cross-scope dependency problems - **Web UI** (`/docs/troubleshooting/web-ui`) - Comprehensive guide to using the debugging web interface, including real-time dependency graph visualization, service state monitoring, and performance analysis tools #### Migration - **Upgrading from v1.x to v2** (`/docs/upgrading/from-v1-x-to-v2`) - Detailed migration guide for existing users, including breaking changes, compatibility considerations, and step-by-step migration procedures with code examples and testing strategies ## API Reference The API reference provides comprehensive documentation of all public functions and types available in the `do` library. Each function is documented with its purpose, parameters, return values, and usage examples. ### Core Functions - **`do.New()`** - Create a new DI container with default configuration options. This is the entry point for creating a dependency injection container. - **`do.Provide()`** - Register a service provider function that will be called to create instances of a service. Supports lazy loading by default. - **`do.ProvideValue()`** - Register a pre-created value directly in the container. Useful for configuration objects, constants, or already-instantiated services. - **`do.ProvideTransient()`** - Register a transient service provider that creates a new instance every time the service is requested. Ideal for stateless services. - **`do.Invoke()`** - Retrieve and instantiate a service from the container. Returns both the service and any error that occurred during instantiation. - **`do.MustInvoke()`** - Retrieve a service from the container, panicking if an error occurs. Use this when you're confident the service exists and can be instantiated. - **`do.As()`** - Create service aliases, allowing a single service to be registered under multiple interfaces or types. ### Scope Management - **`injector.Scope()`** - Create child scopes that inherit from a parent scope. Child scopes can access services from parent scopes but provide isolation for their own services. - **`scope.HealthCheck()`** - Check the health of services that implement the HealthChecker interface. Returns detailed health status information. - **`scope.Shutdown()`** - Gracefully shutdown services that implement the Shutdowner interface, ensuring proper resource cleanup and preventing data corruption. ### Advanced Functions - **`do.Clone()`** - Create a copy of an existing container for testing purposes. The cloned container maintains the same service registrations but provides isolation for testing. - **`do.ListServices()`** - List all registered services in a container, including their types, scopes, and lifecycle information. - **`do.ListScopes()`** - List all scopes in the container hierarchy, showing the relationship between parent and child scopes. - **`do.HealthCheckAll()`** - Perform health checks on all services that implement the HealthChecker interface in the container and all its scopes. - **`do.ShutdownAll()`** - Gracefully shutdown all services that implement the Shutdowner interface in the container and all its scopes. ## AI Agent Skill: ```bash npx skills add https://github.com/samber/cc-skills-golang --skill golang-samber-do ``` ## Quick Start Example This comprehensive example demonstrates the fundamental concepts of the `do` library, showing how to create services, manage dependencies, and use the dependency injection container effectively. The example builds a simple user management system with email notification capabilities. ```go package main import ( "fmt" "github.com/samber/do/v2" ) // Service interfaces define the contracts that our services must implement. // This follows the "Accept Interfaces, Return Structs" principle for better // testability and flexibility. type UserService interface { GetUser(id string) string CreateUser(id, name string) error } type EmailService interface { SendEmail(to, subject, body string) error SendWelcomeEmail(to, name string) error } // Service implementations provide the concrete functionality. // The userService depends on the emailService, demonstrating dependency injection. type userService struct { emailService EmailService } // GetUser retrieves user information and demonstrates basic service functionality func (s *userService) GetUser(id string) string { return fmt.Sprintf("User %s", id) } // CreateUser creates a new user and sends a welcome email, showing how services // can collaborate through dependency injection func (s *userService) CreateUser(id, name string) error { // Simulate user creation fmt.Printf("Creating user %s with ID %s\n", name, id) // Use the injected email service to send a welcome email return s.emailService.SendWelcomeEmail(id, name) } // emailService provides email functionality and demonstrates a simple service // that doesn't have dependencies of its own type emailService struct{} // SendEmail handles general email sending with detailed logging func (s *emailService) SendEmail(to, subject, body string) error { fmt.Printf("Sending email to %s: %s - %s\n", to, subject, body) return nil } // SendWelcomeEmail is a specialized method for sending welcome emails func (s *emailService) SendWelcomeEmail(to, name string) error { subject := "Welcome to our platform!" body := fmt.Sprintf("Hello %s, welcome to our amazing platform!", name) return s.SendEmail(to, subject, body) } func main() { // Create a new dependency injection container with default configuration // This is the entry point for all dependency management injector := do.New() // Register services in the container. The order of registration doesn't matter // as long as all dependencies are registered before they're needed. // Register the email service first (it has no dependencies) do.Provide(injector, func(i do.Injector) (EmailService, error) { // This provider function creates a new emailService instance // The function signature must match the expected return type return &emailService{}, nil }) // Register the user service, which depends on the email service do.Provide(injector, func(i do.Injector) (UserService, error) { // Retrieve the email service from the container using MustInvoke // This demonstrates how services can depend on other services emailService := do.MustInvoke[EmailService](i) // Create and return the user service with its dependency injected return &userService{emailService: emailService}, nil }) // Use the services by retrieving them from the container // MustInvoke will panic if the service cannot be created, so use it // when you're confident the service exists and can be instantiated userService := do.MustInvoke[UserService](injector) // Demonstrate basic service usage fmt.Println(userService.GetUser("123")) // Demonstrate service collaboration through dependency injection if err := userService.CreateUser("456", "John Doe"); err != nil { fmt.Printf("Error creating user: %v\n", err) } } ``` This example demonstrates several key concepts: 1. **Interface-based Design**: Services are defined by interfaces, making them easy to test and mock 2. **Dependency Injection**: The userService automatically receives its emailService dependency 3. **Service Registration**: Services are registered with provider functions that describe how to create them 4. **Service Retrieval**: Services are retrieved from the container when needed 5. **Service Collaboration**: Services can work together through their injected dependencies The example shows how the `do` library makes dependency management simple and intuitive while providing type safety and excellent performance. ## Advanced Usage Patterns The `do` library provides several advanced patterns that enable sophisticated dependency management scenarios. These patterns are essential for building complex applications with proper separation of concerns and resource management. ### Scope Hierarchy Scope hierarchy is one of the most powerful features of the `do` library, allowing you to organize services in logical groups and manage their lifecycle independently. This pattern is particularly useful for web applications, microservices, and any system that needs to manage resources at different levels. ```go package main import ( "fmt" "github.com/samber/do/v2" ) // Database represents a shared resource that should be available across all scopes type Database interface { Connect() error Close() error } // UserRepository represents a service that should be scoped to user sessions type UserRepository interface { FindByID(id string) (*User, error) Save(user *User) error } // SessionManager represents a service that should be scoped to individual sessions type SessionManager interface { CreateSession(userID string) (string, error) ValidateSession(sessionID string) (string, error) } // Concrete implementations type database struct { connectionString string } func (d *database) Connect() error { fmt.Printf("Connecting to database: %s\n", d.connectionString) return nil } func (d *database) Close() error { fmt.Println("Closing database connection") return nil } type userRepository struct { db Database } func (r *userRepository) FindByID(id string) (*User, error) { fmt.Printf("Finding user %s in database\n", id) return &User{ID: id, Name: "User " + id}, nil } func (r *userRepository) Save(user *User) error { fmt.Printf("Saving user %s to database\n", user.ID) return nil } type sessionManager struct { userRepo UserRepository } func (s *sessionManager) CreateSession(userID string) (string, error) { fmt.Printf("Creating session for user %s\n", userID) return "session-" + userID, nil } func (s *sessionManager) ValidateSession(sessionID string) (string, error) { fmt.Printf("Validating session %s\n", sessionID) return "user-123", nil } type User struct { ID string Name string } func main() { // Create root scope - this is the top-level container // Services in the root scope are available to all child scopes root := do.New() // Register shared services in the root scope // These services will be available to all child scopes do.Provide(root, func(i do.Injector) (Database, error) { db := &database{connectionString: "postgres://localhost:5432/mydb"} if err := db.Connect(); err != nil { return nil, err } return db, nil }) // Create user scope - this represents a user session // Services in this scope are isolated to the user but can access root services userScope := do.Scope(root, "user") // Register user-specific services in the user scope do.Provide(userScope, func(i do.Injector) (UserRepository, error) { // This service can access the Database from the root scope db := do.MustInvoke[Database](i) return &userRepository{db: db}, nil }) // Create session scope - this represents an individual session // Services in this scope are isolated to the session but can access user and root services sessionScope := do.Scope(userScope, "session") // Register session-specific services in the session scope do.Provide(sessionScope, func(i do.Injector) (SessionManager, error) { // This service can access UserRepository from the user scope userRepo := do.MustInvoke[UserRepository](i) return &sessionManager{userRepo: userRepo}, nil }) // Demonstrate scope hierarchy usage fmt.Println("=== Using services from different scopes ===") // Access services from the session scope sessionManager := do.MustInvoke[SessionManager](sessionScope) sessionID, _ := sessionManager.CreateSession("user123") fmt.Printf("Created session: %s\n", sessionID) // The session manager can access user repository from user scope userRepo := do.MustInvoke[UserRepository](sessionScope) user, _ := userRepo.FindByID("user123") fmt.Printf("Found user: %s\n", user.Name) // All scopes can access the database from root scope db := do.MustInvoke[Database](sessionScope) fmt.Println("Database is accessible from session scope") // Demonstrate scope isolation fmt.Println("\n=== Scope Isolation ===") // Create another user scope - this is completely isolated from the first one userScope2 := do.Scope(root, "user2") do.Provide(userScope2, func(i do.Injector) (UserRepository, error) { db := do.MustInvoke[Database](i) return &userRepository{db: db}, nil }) // Services in userScope2 are isolated from userScope userRepo2 := do.MustInvoke[UserRepository](userScope2) user2, _ := userRepo2.FindByID("user456") fmt.Printf("Found user in second scope: %s\n", user2.Name) } ``` This scope hierarchy example demonstrates several important concepts: 1. **Shared Resources**: The Database service is registered in the root scope and shared across all child scopes 2. **Scope Isolation**: Each scope can have its own services that are isolated from other scopes at the same level 3. **Dependency Inheritance**: Child scopes can access services from parent scopes, but not from sibling scopes 4. **Resource Management**: Different types of resources can be managed at appropriate scope levels 5. **Lifecycle Management**: Each scope can be managed independently, allowing for efficient resource cleanup ### Health Checks Health checks are essential for production applications, allowing you to monitor the health of your services and their dependencies. The `do` library provides built-in support for health checks through the `HealthChecker` interface, making it easy to implement comprehensive health monitoring. ```go package main import ( "context" "errors" "fmt" "log" "net/http" "time" "github.com/samber/do/v2" ) // HealthChecker interface defines the contract for services that can report their health // This interface is built into the `do` library type HealthChecker interface { HealthCheck() error } // Database represents a service that implements health checking type Database interface { Connect() error Close() error Ping() error } // Cache represents another service with health checking capabilities type Cache interface { Get(key string) (interface{}, error) Set(key string, value interface{}) error Ping() error } // Concrete implementations with health check capabilities type database struct { connected bool host string port int } func (d *database) Connect() error { fmt.Printf("Connecting to database at %s:%d\n", d.host, d.port) d.connected = true return nil } func (d *database) Close() error { fmt.Println("Closing database connection") d.connected = false return nil } func (d *database) Ping() error { if !d.connected { return errors.New("database not connected") } fmt.Println("Database ping successful") return nil } // HealthCheck implements the HealthChecker interface func (d *database) HealthCheck() error { if !d.connected { return errors.New("database is not connected") } // Perform a more thorough health check if err := d.Ping(); err != nil { return fmt.Errorf("database ping failed: %w", err) } return nil } type cache struct { connected bool endpoint string } func (c *cache) Get(key string) (interface{}, error) { if !c.connected { return nil, errors.New("cache not connected") } fmt.Printf("Getting key %s from cache\n", key) return "cached_value", nil } func (c *cache) Set(key string, value interface{}) error { if !c.connected { return errors.New("cache not connected") } fmt.Printf("Setting key %s in cache\n", key) return nil } func (c *cache) Ping() error { if !c.connected { return errors.New("cache not connected") } fmt.Println("Cache ping successful") return nil } // HealthCheck implements the HealthChecker interface func (c *cache) HealthCheck() error { if !c.connected { return errors.New("cache is not connected") } // Perform a more thorough health check if err := c.Ping(); err != nil { return fmt.Errorf("cache ping failed: %w", err) } return nil } // Application service that depends on other services type ApplicationService interface { ProcessRequest(data string) (string, error) } type applicationService struct { db Database cache Cache } func (a *applicationService) ProcessRequest(data string) (string, error) { // Try to get from cache first if cached, err := a.cache.Get(data); err == nil { return fmt.Sprintf("Cached result: %v", cached), nil } // If not in cache, process and store result := fmt.Sprintf("Processed: %s", data) a.cache.Set(data, result) return result, nil } // HealthCheck implements the HealthChecker interface for the application service func (a *applicationService) HealthCheck() error { // Check database health if err := a.db.HealthCheck(); err != nil { return fmt.Errorf("database health check failed: %w", err) } // Check cache health if err := a.cache.HealthCheck(); err != nil { return fmt.Errorf("cache health check failed: %w", err) } return nil } func main() { // Create the dependency injection container injector := do.New() // Register services with health check capabilities do.Provide(injector, func(i do.Injector) (Database, error) { db := &database{ host: "localhost", port: 5432, } if err := db.Connect(); err != nil { return nil, err } return db, nil }) do.Provide(injector, func(i do.Injector) (Cache, error) { cache := &cache{ endpoint: "localhost:6379", } cache.connected = true // Simulate successful connection return cache, nil }) do.Provide(injector, func(i do.Injector) (ApplicationService, error) { db := do.MustInvoke[Database](i) cache := do.MustInvoke[Cache](i) return &applicationService{db: db, cache: cache}, nil }) // Demonstrate individual health checks fmt.Println("=== Individual Health Checks ===") if err := do.HealthCheck[Database](injector); err != nil { log.Printf("Database health check failed: %v", err) } else { fmt.Println("Database is healthy") } if err := do.HealthCheck[Cache](injector); err != nil { log.Printf("Cache health check failed: %v", err) } else { fmt.Println("Cache is healthy") } if err := do.HealthCheck[ApplicationService](injector); err != nil { log.Printf("Application service health check failed: %v", err) } else { fmt.Println("Application service is healthy") } // Demonstrate comprehensive health checking fmt.Println("\n=== Comprehensive Health Check ===") // Check health of all services that implement HealthChecker if err := do.HealthCheckAll(injector); err != nil { log.Printf("Comprehensive health check failed: %v", err) } else { fmt.Println("All services are healthy") } // Demonstrate health check in a web server context fmt.Println("\n=== Health Check Endpoint Simulation ===") // Simulate a health check endpoint http.HandleFunc("/health", func(w http.ResponseWriter, r *http.Request) { if err := do.HealthCheckAll(injector); err != nil { w.WriteHeader(http.StatusServiceUnavailable) fmt.Fprintf(w, "Health check failed: %v", err) } else { w.WriteHeader(http.StatusOK) fmt.Fprintf(w, "All services are healthy") } }) fmt.Println("Health check endpoint would be available at /health") // Demonstrate health check with timeout fmt.Println("\n=== Health Check with Timeout ===") ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer cancel() // In a real application, you might want to check health with a timeout healthChan := make(chan error, 1) go func() { healthChan <- do.HealthCheckAll(injector) }() select { case err := <-healthChan: if err != nil { log.Printf("Health check failed: %v", err) } else { fmt.Println("Health check completed successfully") } case <-ctx.Done(): log.Printf("Health check timed out: %v", ctx.Err()) } } ``` This health check example demonstrates several important concepts: 1. **HealthChecker Interface**: Services implement the `HealthChecker` interface to provide health status 2. **Individual Health Checks**: You can check the health of specific services using `do.HealthCheck[T]` 3. **Comprehensive Health Checks**: Use `do.HealthCheckAll()` to check all services that implement `HealthChecker` 4. **Dependency Health**: Services can check the health of their dependencies as part of their own health check 5. **Production Integration**: Health checks can be easily integrated into web servers and monitoring systems 6. **Timeout Handling**: Health checks can be performed with timeouts to prevent hanging 7. **Error Propagation**: Health check errors provide detailed information about what went wrong ### Graceful Shutdown Graceful shutdown is crucial for production applications to ensure that resources are properly cleaned up and data integrity is maintained. The `do` library provides built-in support for graceful shutdown through the `Shutdowner` interface, allowing services to clean up resources in the correct order. ```go package main import ( "context" "errors" "fmt" "log" "net" "net/http" "os" "os/signal" "sync" "syscall" "time" "github.com/samber/do/v2" ) // Shutdowner interface defines the contract for services that need graceful shutdown // This interface is built into the `do` library type Shutdowner interface { Shutdown(ctx context.Context) error } // Server represents an HTTP server that needs graceful shutdown type Server interface { Start() error Stop() error } // Database represents a database connection that needs proper cleanup type Database interface { Connect() error Close() error IsConnected() bool } // Cache represents a cache service that needs cleanup type Cache interface { Connect() error Close() error Flush() error } // Concrete implementations with shutdown capabilities type server struct { listener net.Listener server *http.Server mu sync.Mutex running bool } func (s *server) Start() error { s.mu.Lock() defer s.mu.Unlock() if s.running { return errors.New("server is already running") } // Create listener listener, err := net.Listen("tcp", ":8080") if err != nil { return fmt.Errorf("failed to create listener: %w", err) } s.listener = listener // Create HTTP server s.server = &http.Server{ Addr: ":8080", Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { fmt.Fprintf(w, "Hello from server!") }), } s.running = true fmt.Println("Server started on :8080") // Start server in goroutine go func() { if err := s.server.Serve(s.listener); err != nil && err != http.ErrServerClosed { log.Printf("Server error: %v", err) } }() return nil } func (s *server) Stop() error { s.mu.Lock() defer s.mu.Unlock() if !s.running { return nil } s.running = false return s.server.Close() } // Shutdown implements the Shutdowner interface func (s *server) Shutdown(ctx context.Context) error { fmt.Println("Shutting down HTTP server...") // Stop accepting new connections if err := s.Stop(); err != nil { return fmt.Errorf("failed to stop server: %w", err) } // Wait for existing connections to finish if err := s.server.Shutdown(ctx); err != nil { return fmt.Errorf("failed to shutdown server gracefully: %w", err) } fmt.Println("HTTP server shutdown completed") return nil } type database struct { connected bool mu sync.Mutex } func (d *database) Connect() error { d.mu.Lock() defer d.mu.Unlock() if d.connected { return errors.New("database already connected") } // Simulate database connection time.Sleep(100 * time.Millisecond) d.connected = true fmt.Println("Database connected") return nil } func (d *database) Close() error { d.mu.Lock() defer d.mu.Unlock() if !d.connected { return nil } // Simulate database disconnection time.Sleep(100 * time.Millisecond) d.connected = false fmt.Println("Database disconnected") return nil } func (d *database) IsConnected() bool { d.mu.Lock() defer d.mu.Unlock() return d.connected } // Shutdown implements the Shutdowner interface func (d *database) Shutdown(ctx context.Context) error { fmt.Println("Shutting down database...") // Perform any cleanup operations if err := d.Close(); err != nil { return fmt.Errorf("failed to close database: %w", err) } fmt.Println("Database shutdown completed") return nil } type cache struct { connected bool mu sync.Mutex } func (c *cache) Connect() error { c.mu.Lock() defer c.mu.Unlock() if c.connected { return errors.New("cache already connected") } // Simulate cache connection time.Sleep(50 * time.Millisecond) c.connected = true fmt.Println("Cache connected") return nil } func (c *cache) Close() error { c.mu.Lock() defer c.mu.Unlock() if !c.connected { return nil } // Simulate cache disconnection time.Sleep(50 * time.Millisecond) c.connected = false fmt.Println("Cache disconnected") return nil } func (c *cache) Flush() error { c.mu.Lock() defer c.mu.Unlock() if !c.connected { return errors.New("cache not connected") } fmt.Println("Cache flushed") return nil } // Shutdown implements the Shutdowner interface func (c *cache) Shutdown(ctx context.Context) error { fmt.Println("Shutting down cache...") // Flush cache before closing if err := c.Flush(); err != nil { return fmt.Errorf("failed to flush cache: %w", err) } // Close cache connection if err := c.Close(); err != nil { return fmt.Errorf("failed to close cache: %w", err) } fmt.Println("Cache shutdown completed") return nil } // Application service that coordinates shutdown type ApplicationService interface { Start() error Stop() error } type applicationService struct { server Server db Database cache Cache } func (a *applicationService) Start() error { fmt.Println("Starting application services...") // Start services in order if err := a.db.Connect(); err != nil { return fmt.Errorf("failed to connect database: %w", err) } if err := a.cache.Connect(); err != nil { return fmt.Errorf("failed to connect cache: %w", err) } if err := a.server.Start(); err != nil { return fmt.Errorf("failed to start server: %w", err) } fmt.Println("All application services started") return nil } func (a *applicationService) Stop() error { fmt.Println("Stopping application services...") // Stop services in reverse order if err := a.server.Stop(); err != nil { return fmt.Errorf("failed to stop server: %w", err) } if err := a.cache.Close(); err != nil { return fmt.Errorf("failed to close cache: %w", err) } if err := a.db.Close(); err != nil { return fmt.Errorf("failed to close database: %w", err) } fmt.Println("All application services stopped") return nil } // Shutdown implements the Shutdowner interface func (a *applicationService) Shutdown(ctx context.Context) error { fmt.Println("Shutting down application...") // Shutdown services in reverse dependency order if err := a.server.Shutdown(ctx); err != nil { return fmt.Errorf("server shutdown failed: %w", err) } if err := a.cache.Shutdown(ctx); err != nil { return fmt.Errorf("cache shutdown failed: %w", err) } if err := a.db.Shutdown(ctx); err != nil { return fmt.Errorf("database shutdown failed: %w", err) } fmt.Println("Application shutdown completed") return nil } func main() { // Create the dependency injection container injector := do.New() // Register services with shutdown capabilities do.Provide(injector, func(i do.Injector) (Database, error) { return &database{}, nil }) do.Provide(injector, func(i do.Injector) (Cache, error) { return &cache{}, nil }) do.Provide(injector, func(i do.Injector) (Server, error) { return &server{}, nil }) do.Provide(injector, func(i do.Injector) (ApplicationService, error) { server := do.MustInvoke[Server](i) db := do.MustInvoke[Database](i) cache := do.MustInvoke[Cache](i) return &applicationService{server: server, db: db, cache: cache}, nil }) // Start the application app := do.MustInvoke[ApplicationService](injector) if err := app.Start(); err != nil { log.Fatalf("Failed to start application: %v", err) } // Set up signal handling for graceful shutdown sigChan := make(chan os.Signal, 1) signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM) fmt.Println("Application running. Press Ctrl+C to shutdown gracefully...") // Wait for shutdown signal <-sigChan fmt.Println("\nReceived shutdown signal. Starting graceful shutdown...") // Perform graceful shutdown with timeout ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) defer cancel() // Shutdown all services that implement Shutdowner if err := do.ShutdownAll(injector, ctx); err != nil { log.Printf("Graceful shutdown failed: %v", err) os.Exit(1) } fmt.Println("Graceful shutdown completed successfully") } ``` This graceful shutdown example demonstrates several important concepts: 1. **Shutdowner Interface**: Services implement the `Shutdowner` interface to provide graceful shutdown capabilities 2. **Dependency Order**: Services are shut down in reverse dependency order to prevent issues 3. **Timeout Handling**: Shutdown operations can be performed with timeouts to prevent hanging 4. **Resource Cleanup**: Each service properly cleans up its resources during shutdown 5. **Signal Handling**: The application responds to system signals for graceful shutdown 6. **Error Propagation**: Shutdown errors provide detailed information about what went wrong 7. **Comprehensive Shutdown**: Use `do.ShutdownAll()` to shutdown all services that implement `Shutdowner` 8. **Individual Shutdown**: Use `do.Shutdown[T]()` to shutdown specific services ## Performance Considerations Performance is a critical aspect of any dependency injection framework, especially in high-throughput applications. The `do` library has been designed with performance in mind, providing excellent runtime characteristics while maintaining type safety and ease of use. ### Memory Management Efficient memory management is essential for long-running applications and systems with limited resources. The `do` library provides several mechanisms to optimize memory usage. - **Lazy Loading**: Services are created only when needed, reducing initial memory usage. This is particularly beneficial for applications with many services, as only the services that are actually used consume memory. - **Singleton Pattern**: Services are cached after first creation, ensuring that only one instance exists in memory. This reduces memory footprint and improves performance for frequently used services. - **Scope Isolation**: Child scopes can be garbage collected independently when they are no longer needed. This allows for efficient cleanup of temporary resources and prevents memory leaks. - **Transient Services**: Use for stateless services that don't need caching. Transient services are created fresh each time they're requested, which can be more memory efficient for services that are used infrequently or have short lifespans. - **Smart Caching**: The library uses intelligent caching strategies to minimize memory usage while maximizing performance. Services are cached based on their lifecycle and usage patterns. - **Memory Pooling**: For frequently created and destroyed services, consider implementing memory pooling to reduce allocation overhead. ### Startup Performance Fast startup times are crucial for applications that need to be responsive quickly, such as microservices and command-line tools. - **Eager Loading**: Use for critical services that must be available immediately. Eager loading ensures that essential services are ready when the application starts, reducing latency for the first request. - **Parallel Initialization**: Services can be initialized concurrently when they don't have dependencies on each other. This can significantly reduce startup time for applications with many independent services. - **Dependency Optimization**: Minimize dependency depth for faster resolution. Deep dependency chains can slow down service resolution, so it's important to keep the dependency graph as shallow as possible. - **Lazy Initialization**: For non-critical services, use lazy loading to defer initialization until the service is actually needed. This can dramatically improve startup times for applications with many optional services. - **Conditional Registration**: Only register services that are actually needed based on configuration or environment. This reduces the overhead of managing unused services. ### Runtime Performance Runtime performance is critical for applications that handle high request volumes or need to respond quickly to user interactions. - **Direct Function Calls**: Service resolution uses direct function calls rather than reflection or dynamic dispatch. This provides near-native performance for service instantiation and method calls. - **Minimal Allocations**: Optimized to reduce garbage collection pressure. The library is designed to minimize memory allocations during service resolution and method calls. - **Efficient Dependency Resolution**: The dependency resolution algorithm is highly optimized to minimize the time spent resolving dependencies at runtime. The algorithm uses efficient data structures and caching to ensure fast lookups. - **Concurrent Access Optimization**: Thread-safe operations are optimized to minimize lock contention in high-concurrency scenarios. The library uses fine-grained locking and lock-free data structures where possible. - **Zero-Copy Operations**: Where possible, the library avoids unnecessary copying of data, reducing memory usage and improving performance. ### Performance Best Practices To achieve optimal performance with the `do` library, consider the following best practices: 1. **Profile Your Application**: Use Go's built-in profiling tools to identify performance bottlenecks in your dependency injection usage. 2. **Monitor Memory Usage**: Keep track of memory usage patterns to ensure that services are being created and destroyed efficiently. 3. **Use Appropriate Lifecycles**: Choose the right service lifecycle (lazy, eager, or transient) based on your application's needs and performance requirements. 4. **Optimize Dependency Graphs**: Keep dependency graphs shallow and avoid circular dependencies to minimize resolution time. 5. **Consider Service Granularity**: Balance between having too many small services (increased overhead) and too few large services (reduced flexibility). 6. **Use Scope Appropriately**: Use scopes to isolate services that don't need to be shared globally, reducing memory usage and improving performance. 7. **Benchmark Critical Paths**: Benchmark your application's critical paths to ensure that dependency injection isn't becoming a bottleneck. 8. **Monitor Garbage Collection**: Keep an eye on garbage collection patterns to ensure that the library isn't causing excessive GC pressure. ### Performance Monitoring The `do` library provides several tools for monitoring performance: - **Service Resolution Metrics**: Track how long it takes to resolve different services - **Memory Usage Tracking**: Monitor memory usage patterns for different service types - **Dependency Graph Analysis**: Analyze the complexity of your dependency graph - **Scope Performance**: Monitor the performance impact of different scope configurations By following these performance considerations and best practices, you can ensure that your application achieves optimal performance while benefiting from the type safety and ease of use provided by the `do` library. ## Testing with do Testing is a crucial aspect of software development, and the `do` library provides excellent support for writing comprehensive tests. The library's design makes it easy to create isolated test environments, mock dependencies, and verify service behavior. ### Container Cloning Container cloning is one of the most powerful features for testing with the `do` library. It allows you to create isolated test environments that inherit all the service registrations from the original container while allowing you to override specific services with mocks or test implementations. ```go package main import ( "fmt" "testing" "github.com/samber/do/v2" "github.com/stretchr/testify/assert" ) // Service interfaces for testing type UserService interface { GetUser(id string) (*User, error) CreateUser(name, email string) (*User, error) } type EmailService interface { SendEmail(to, subject, body string) error SendWelcomeEmail(user *User) error } type Database interface { SaveUser(user *User) error FindUser(id string) (*User, error) } // Concrete implementations type User struct { ID string Name string Email string } type userService struct { db Database email EmailService } func (s *userService) GetUser(id string) (*User, error) { return s.db.FindUser(id) } func (s *userService) CreateUser(name, email string) (*User, error) { user := &User{ ID: generateID(), Name: name, Email: email, } if err := s.db.SaveUser(user); err != nil { return nil, err } if err := s.email.SendWelcomeEmail(user); err != nil { return nil, err } return user, nil } func generateID() string { return fmt.Sprintf("user_%d", time.Now().UnixNano()) } // Test setup function func setupTestContainer() do.Injector { injector := do.New() // Register real services do.Provide(injector, func(i do.Injector) (Database, error) { return &realDatabase{}, nil }) do.Provide(injector, func(i do.Injector) (EmailService, error) { return &realEmailService{}, nil }) do.Provide(injector, func(i do.Injector) (UserService, error) { db := do.MustInvoke[Database](i) email := do.MustInvoke[EmailService](i) return &userService{db: db, email: email}, nil }) return injector } // Mock implementations for testing type mockDatabase struct { users map[string]*User saved []*User } func (m *mockDatabase) SaveUser(user *User) error { if m.users == nil { m.users = make(map[string]*User) } m.users[user.ID] = user m.saved = append(m.saved, user) return nil } func (m *mockDatabase) FindUser(id string) (*User, error) { if user, exists := m.users[id]; exists { return user, nil } return nil, fmt.Errorf("user not found: %s", id) } type mockEmailService struct { sentEmails []string welcomeEmails []*User } func (m *mockEmailService) SendEmail(to, subject, body string) error { m.sentEmails = append(m.sentEmails, fmt.Sprintf("%s:%s:%s", to, subject, body)) return nil } func (m *mockEmailService) SendWelcomeEmail(user *User) error { m.welcomeEmails = append(m.welcomeEmails, user) return m.SendEmail(user.Email, "Welcome!", "Welcome to our platform!") } // Test functions func TestUserService_GetUser(t *testing.T) { // Create test container by cloning the main container testInjector := do.Clone(setupTestContainer()) // Override database with mock mockDB := &mockDatabase{ users: map[string]*User{ "user1": {ID: "user1", Name: "John Doe", Email: "john@example.com"}, }, } do.ProvideValue(testInjector, mockDB) // Test with mocked dependencies userService := do.MustInvoke[UserService](testInjector) // Test successful user retrieval user, err := userService.GetUser("user1") assert.NoError(t, err) assert.Equal(t, "John Doe", user.Name) assert.Equal(t, "john@example.com", user.Email) // Test user not found _, err = userService.GetUser("nonexistent") assert.Error(t, err) assert.Contains(t, err.Error(), "user not found") } func TestUserService_CreateUser(t *testing.T) { // Create test container testInjector := do.Clone(setupTestContainer()) // Override both database and email service with mocks mockDB := &mockDatabase{users: make(map[string]*User)} mockEmail := &mockEmailService{} do.ProvideValue(testInjector, mockDB) do.ProvideValue(testInjector, mockEmail) // Test with mocked dependencies userService := do.MustInvoke[UserService](testInjector) // Test successful user creation user, err := userService.CreateUser("Jane Doe", "jane@example.com") assert.NoError(t, err) assert.NotEmpty(t, user.ID) assert.Equal(t, "Jane Doe", user.Name) assert.Equal(t, "jane@example.com", user.Email) // Verify database was called assert.Len(t, mockDB.saved, 1) assert.Equal(t, user.ID, mockDB.saved[0].ID) // Verify welcome email was sent assert.Len(t, mockEmail.welcomeEmails, 1) assert.Equal(t, user.ID, mockEmail.welcomeEmails[0].ID) assert.Len(t, mockEmail.sentEmails, 1) assert.Contains(t, mockEmail.sentEmails[0], "jane@example.com") } func TestUserService_Integration(t *testing.T) { // Test with real services (integration test) injector := setupTestContainer() userService := do.MustInvoke[UserService](injector) // This would test the actual integration between services // In a real test, you might use a test database user, err := userService.CreateUser("Integration Test", "integration@example.com") assert.NoError(t, err) assert.NotEmpty(t, user.ID) } // Real implementations (for integration tests) type realDatabase struct { // Real database implementation } func (r *realDatabase) SaveUser(user *User) error { // Real database save implementation return nil } func (r *realDatabase) FindUser(id string) (*User, error) { // Real database find implementation return nil, fmt.Errorf("not implemented") } type realEmailService struct { // Real email service implementation } func (r *realEmailService) SendEmail(to, subject, body string) error { // Real email sending implementation return nil } func (r *realEmailService) SendWelcomeEmail(user *User) error { return r.SendEmail(user.Email, "Welcome!", "Welcome to our platform!") } ``` ### Advanced Testing Patterns The `do` library supports several advanced testing patterns that make it easy to write comprehensive tests: #### Test Utilities ```go // Test utilities for common testing scenarios type TestContainer struct { injector do.Injector mocks map[string]interface{} } func NewTestContainer() *TestContainer { return &TestContainer{ injector: do.New(), mocks: make(map[string]interface{}), } } func (tc *TestContainer) MockService[T any](mock T) *TestContainer { do.ProvideValue(tc.injector, mock) return tc } func (tc *TestContainer) GetService[T any]() T { return do.MustInvoke[T](tc.injector) } func (tc *TestContainer) Clone() *TestContainer { return &TestContainer{ injector: do.Clone(tc.injector), mocks: tc.mocks, } } // Usage example func TestWithTestContainer(t *testing.T) { tc := NewTestContainer(). MockService(&mockDatabase{}). MockService(&mockEmailService{}) userService := tc.GetService[UserService]() // Test with mocked services } ``` #### Behavior Verification ```go // Mock with behavior verification type VerifiableMockEmailService struct { mockEmailService callCount int lastCall *EmailCall } type EmailCall struct { To string Subject string Body string } func (m *VerifiableMockEmailService) SendEmail(to, subject, body string) error { m.callCount++ m.lastCall = &EmailCall{To: to, Subject: subject, Body: body} return m.mockEmailService.SendEmail(to, subject, body) } func (m *VerifiableMockEmailService) VerifyCallCount(expected int) error { if m.callCount != expected { return fmt.Errorf("expected %d calls, got %d", expected, m.callCount) } return nil } func (m *VerifiableMockEmailService) VerifyLastCall(to, subject string) error { if m.lastCall == nil { return fmt.Errorf("no calls were made") } if m.lastCall.To != to || m.lastCall.Subject != subject { return fmt.Errorf("expected call to %s with subject %s, got %s with subject %s", to, subject, m.lastCall.To, m.lastCall.Subject) } return nil } ``` ### Testing Best Practices 1. **Isolate Tests**: Use container cloning to ensure each test has an isolated environment 2. **Mock Dependencies**: Mock external dependencies to make tests fast and reliable 3. **Test Service Contracts**: Test that services implement their interfaces correctly 4. **Verify Behavior**: Use mocks to verify that services interact correctly with their dependencies 5. **Integration Tests**: Include integration tests with real services for critical paths 6. **Test Error Scenarios**: Test error handling and edge cases 7. **Use Test Utilities**: Create reusable test utilities for common testing patterns 8. **Benchmark Critical Paths**: Use benchmarks to ensure performance doesn't regress The `do` library's testing support makes it easy to write comprehensive, maintainable tests that verify both individual service behavior and integration between services. ## HTTP Framework Integration The `do` library provides debugging integrations with popular Go HTTP frameworks. These integrations are primarily designed for debugging and introspection purposes, allowing you to visualize your dependency graph and service relationships through a web interface. While they can be used for service injection in handlers, their main purpose is to provide debugging capabilities. ### Chi Integration Chi is a lightweight, fast HTTP router for Go. The `do` library provides debugging middleware that allows you to visualize your dependency graph and access services for debugging purposes. ```go package main import ( "encoding/json" "fmt" "net/http" "strconv" "github.com/go-chi/chi/v5" "github.com/go-chi/chi/v5/middleware" "github.com/samber/do/v2" "github.com/samber/do/v2/http/chi" ) // Service interfaces type UserService interface { GetUser(id int) (*User, error) CreateUser(user *User) error UpdateUser(id int, user *User) error DeleteUser(id int) error } type EmailService interface { SendEmail(to, subject, body string) error } // Data models type User struct { ID int `json:"id"` Name string `json:"name"` Email string `json:"email"` } // Service implementations type userService struct { emailService EmailService users map[int]*User nextID int } func (s *userService) GetUser(id int) (*User, error) { if user, exists := s.users[id]; exists { return user, nil } return nil, fmt.Errorf("user not found: %d", id) } func (s *userService) CreateUser(user *User) error { user.ID = s.nextID s.nextID++ s.users[user.ID] = user // Send welcome email return s.emailService.SendEmail(user.Email, "Welcome!", "Welcome to our platform!") } func (s *userService) UpdateUser(id int, user *User) error { if _, exists := s.users[id]; !exists { return fmt.Errorf("user not found: %d", id) } user.ID = id s.users[id] = user return nil } func (s *userService) DeleteUser(id int) error { if _, exists := s.users[id]; !exists { return fmt.Errorf("user not found: %d", id) } delete(s.users, id) return nil } type emailService struct{} func (s *emailService) SendEmail(to, subject, body string) error { fmt.Printf("Sending email to %s: %s - %s\n", to, subject, body) return nil } // HTTP handlers (for debugging purposes) func getUserHandler(w http.ResponseWriter, r *http.Request) { // Get the injector from the request context (for debugging) injector := chi.GetInjector(r) // Get the user service (for debugging/inspection) userService := do.MustInvoke[UserService](injector) // Parse user ID from URL userIDStr := chi.URLParam(r, "id") userID, err := strconv.Atoi(userIDStr) if err != nil { http.Error(w, "Invalid user ID", http.StatusBadRequest) return } // Get user user, err := userService.GetUser(userID) if err != nil { http.Error(w, err.Error(), http.StatusNotFound) return } // Return JSON response w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(user) } func createUserHandler(w http.ResponseWriter, r *http.Request) { injector := chi.GetInjector(r) userService := do.MustInvoke[UserService](injector) // Parse request body var user User if err := json.NewDecoder(r.Body).Decode(&user); err != nil { http.Error(w, "Invalid request body", http.StatusBadRequest) return } // Create user if err := userService.CreateUser(&user); err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } // Return created user w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusCreated) json.NewEncoder(w).Encode(user) } func updateUserHandler(w http.ResponseWriter, r *http.Request) { injector := chi.GetInjector(r) userService := do.MustInvoke[UserService](injector) // Parse user ID userIDStr := chi.URLParam(r, "id") userID, err := strconv.Atoi(userIDStr) if err != nil { http.Error(w, "Invalid user ID", http.StatusBadRequest) return } // Parse request body var user User if err := json.NewDecoder(r.Body).Decode(&user); err != nil { http.Error(w, "Invalid request body", http.StatusBadRequest) return } // Update user if err := userService.UpdateUser(userID, &user); err != nil { http.Error(w, err.Error(), http.StatusNotFound) return } // Return updated user w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(user) } func deleteUserHandler(w http.ResponseWriter, r *http.Request) { injector := chi.GetInjector(r) userService := do.MustInvoke[UserService](injector) // Parse user ID userIDStr := chi.URLParam(r, "id") userID, err := strconv.Atoi(userIDStr) if err != nil { http.Error(w, "Invalid user ID", http.StatusBadRequest) return } // Delete user if err := userService.DeleteUser(userID); err != nil { http.Error(w, err.Error(), http.StatusNotFound) return } w.WriteHeader(http.StatusNoContent) } func main() { // Create dependency injection container injector := do.New() // Register services do.Provide(injector, func(i do.Injector) (EmailService, error) { return &emailService{}, nil }) do.Provide(injector, func(i do.Injector) (UserService, error) { emailService := do.MustInvoke[EmailService](i) return &userService{ emailService: emailService, users: make(map[int]*User), nextID: 1, }, nil }) // Create Chi router r := chi.NewRouter() // Add middleware r.Use(middleware.Logger) r.Use(middleware.Recoverer) // Add do injector middleware (for debugging) chi.UseInjector(r, injector) // Define routes r.Route("/api/users", func(r chi.Router) { r.Get("/{id}", getUserHandler) r.Post("/", createUserHandler) r.Put("/{id}", updateUserHandler) r.Delete("/{id}", deleteUserHandler) }) // Start server fmt.Println("Server starting on :8080") fmt.Println("Debug UI available at http://localhost:8080/debug") http.ListenAndServe(":8080", r) } ``` ### Echo Integration Echo is a high-performance, extensible, minimalist Go web framework. The `do` library provides debugging middleware for Echo that allows you to visualize your dependency graph and access services for debugging purposes. ```go package main import ( "net/http" "strconv" "github.com/labstack/echo/v4" "github.com/labstack/echo/v4/middleware" "github.com/samber/do/v2" "github.com/samber/do/v2/http/echo" ) // Service interfaces (same as above) type UserService interface { GetUser(id int) (*User, error) CreateUser(user *User) error UpdateUser(id int, user *User) error DeleteUser(id int) error } type EmailService interface { SendEmail(to, subject, body string) error } // Data models type User struct { ID int `json:"id"` Name string `json:"name"` Email string `json:"email"` } // Service implementations (same as above) type userService struct { emailService EmailService users map[int]*User nextID int } func (s *userService) GetUser(id int) (*User, error) { if user, exists := s.users[id]; exists { return user, nil } return nil, fmt.Errorf("user not found: %d", id) } func (s *userService) CreateUser(user *User) error { user.ID = s.nextID s.nextID++ s.users[user.ID] = user // Send welcome email return s.emailService.SendEmail(user.Email, "Welcome!", "Welcome to our platform!") } func (s *userService) UpdateUser(id int, user *User) error { if _, exists := s.users[id]; !exists { return fmt.Errorf("user not found: %d", id) } user.ID = id s.users[id] = user return nil } func (s *userService) DeleteUser(id int) error { if _, exists := s.users[id]; !exists { return fmt.Errorf("user not found: %d", id) } delete(s.users, id) return nil } type emailService struct{} func (s *emailService) SendEmail(to, subject, body string) error { fmt.Printf("Sending email to %s: %s - %s\n", to, subject, body) return nil } // HTTP handlers for Echo (for debugging purposes) func getUserHandler(c echo.Context) error { // Get the injector from the context (for debugging) injector := echo.GetInjector(c) // Get the user service (for debugging/inspection) userService := do.MustInvoke[UserService](injector) // Parse user ID from URL userIDStr := c.Param("id") userID, err := strconv.Atoi(userIDStr) if err != nil { return echo.NewHTTPError(http.StatusBadRequest, "Invalid user ID") } // Get user user, err := userService.GetUser(userID) if err != nil { return echo.NewHTTPError(http.StatusNotFound, err.Error()) } // Return JSON response return c.JSON(http.StatusOK, user) } func createUserHandler(c echo.Context) error { injector := echo.GetInjector(c) userService := do.MustInvoke[UserService](injector) // Parse request body var user User if err := c.Bind(&user); err != nil { return echo.NewHTTPError(http.StatusBadRequest, "Invalid request body") } // Create user if err := userService.CreateUser(&user); err != nil { return echo.NewHTTPError(http.StatusInternalServerError, err.Error()) } // Return created user return c.JSON(http.StatusCreated, user) } func updateUserHandler(c echo.Context) error { injector := echo.GetInjector(c) userService := do.MustInvoke[UserService](injector) // Parse user ID userIDStr := c.Param("id") userID, err := strconv.Atoi(userIDStr) if err != nil { return echo.NewHTTPError(http.StatusBadRequest, "Invalid user ID") } // Parse request body var user User if err := c.Bind(&user); err != nil { return echo.NewHTTPError(http.StatusBadRequest, "Invalid request body") } // Update user if err := userService.UpdateUser(userID, &user); err != nil { return echo.NewHTTPError(http.StatusNotFound, err.Error()) } // Return updated user return c.JSON(http.StatusOK, user) } func deleteUserHandler(c echo.Context) error { injector := echo.GetInjector(c) userService := do.MustInvoke[UserService](injector) // Parse user ID userIDStr := c.Param("id") userID, err := strconv.Atoi(userIDStr) if err != nil { return echo.NewHTTPError(http.StatusBadRequest, "Invalid user ID") } // Delete user if err := userService.DeleteUser(userID); err != nil { return echo.NewHTTPError(http.StatusNotFound, err.Error()) } return c.NoContent(http.StatusNoContent) } func main() { // Create dependency injection container injector := do.New() // Register services do.Provide(injector, func(i do.Injector) (EmailService, error) { return &emailService{}, nil }) do.Provide(injector, func(i do.Injector) (UserService, error) { emailService := do.MustInvoke[EmailService](i) return &userService{ emailService: emailService, users: make(map[int]*User), nextID: 1, }, nil }) // Create Echo instance e := echo.New() // Add middleware e.Use(middleware.Logger()) e.Use(middleware.Recover()) // Add do injector middleware (for debugging) echo.UseInjector(e, injector) // Define routes api := e.Group("/api") users := api.Group("/users") users.GET("/:id", getUserHandler) users.POST("/", createUserHandler) users.PUT("/:id", updateUserHandler) users.DELETE("/:id", deleteUserHandler) // Start server fmt.Println("Server starting on :8080") fmt.Println("Debug UI available at http://localhost:8080/debug") e.Start(":8080") } ``` ### Other Framework Integrations The `do` library also provides debugging integrations for other popular Go HTTP frameworks: #### Fiber Integration ```go import "github.com/samber/do/v2/http/fiber" func main() { injector := do.New() // ... register services app := fiber.New() fiber.UseInjector(app, injector) // For debugging app.Get("/users/:id", func(c *fiber.Ctx) error { userService := do.MustInvoke[UserService](injector) // For debugging // ... handle request return nil }) } ``` #### Gin Integration ```go import "github.com/samber/do/v2/http/gin" func main() { injector := do.New() // ... register services r := gin.Default() gin.UseInjector(r, injector) // For debugging r.GET("/users/:id", func(c *gin.Context) { userService := do.MustInvoke[UserService](injector) // For debugging // ... handle request }) } ``` #### Standard Library Integration ```go import "github.com/samber/do/v2/http/std" func main() { injector := do.New() // ... register services http.HandleFunc("/users", func(w http.ResponseWriter, r *http.Request) { userService := do.MustInvoke[UserService](injector) // For debugging // ... handle request }) } ``` ### Integration Benefits The HTTP framework debugging integrations provide several benefits: 1. **Dependency Visualization**: Visualize your dependency graph through a web interface 2. **Service Introspection**: Inspect service relationships and dependencies in real-time 3. **Debugging Support**: Access services from handlers for debugging purposes 4. **Development Tools**: Provide development-time tools for understanding your application structure 5. **Middleware Support**: Integrations work seamlessly with existing framework middleware These integrations are primarily designed for development and debugging, helping you understand and troubleshoot your dependency injection setup. ## Best Practices Following best practices is essential for building maintainable, scalable, and robust applications with the `do` library. These practices have been developed through real-world experience and help ensure that your dependency injection usage is effective and sustainable. ### Service Design Good service design is the foundation of a well-architected application. The `do` library encourages clean, maintainable service design through its type-safe approach. 1. **Accept Interfaces, Return Structs**: Design services to accept interfaces and return concrete types. This principle promotes loose coupling and makes services easier to test and mock. ```go // Good: Accept interfaces, return structs type UserService interface { GetUser(id string) (*User, error) CreateUser(user *User) error } type userService struct { db Database cache Cache } func NewUserService(db Database, cache Cache) *userService { return &userService{db: db, cache: cache} } // Bad: Accepting concrete types type userService struct { db *PostgreSQLDatabase cache *RedisCache } ``` 2. **Single Responsibility**: Each service should have a single, well-defined responsibility. This makes services easier to understand, test, and maintain. ```go // Good: Single responsibility type UserService interface { GetUser(id string) (*User, error) CreateUser(user *User) error } type EmailService interface { SendEmail(to, subject, body string) error } // Bad: Multiple responsibilities type UserService interface { GetUser(id string) (*User, error) CreateUser(user *User) error SendEmail(to, subject, body string) error ProcessPayment(amount float64) error } ``` 3. **Dependency Inversion**: Depend on abstractions, not concrete implementations. This allows for easy substitution of implementations and better testing. ```go // Good: Depend on abstractions type Database interface { SaveUser(user *User) error FindUser(id string) (*User, error) } type userService struct { db Database // Interface, not concrete type } // Bad: Depend on concrete implementations type userService struct { db *PostgreSQLDatabase // Concrete type } ``` 4. **Interface Segregation**: Keep interfaces small and focused. Large interfaces are harder to implement and test. ```go // Good: Small, focused interfaces type UserReader interface { GetUser(id string) (*User, error) } type UserWriter interface { CreateUser(user *User) error UpdateUser(user *User) error } type UserService interface { UserReader UserWriter } // Bad: Large, monolithic interface type UserService interface { GetUser(id string) (*User, error) CreateUser(user *User) error UpdateUser(user *User) error DeleteUser(id string) error ListUsers() ([]*User, error) SearchUsers(query string) ([]*User, error) ValidateUser(user *User) error // ... many more methods } ``` ### Error Handling Proper error handling is crucial for building reliable applications. The `do` library provides several mechanisms for handling errors effectively. 1. **Provider Errors**: Always handle errors in service providers. Service providers can fail during initialization, and these errors should be handled appropriately. ```go // Good: Handle provider errors do.Provide(injector, func(i do.Injector) (Database, error) { db, err := NewDatabase(config) if err != nil { return nil, fmt.Errorf("failed to create database: %w", err) } return db, nil }) // Bad: Ignore provider errors do.Provide(injector, func(i do.Injector) (Database, error) { db, _ := NewDatabase(config) // Ignoring error return db, nil }) ``` 2. **Graceful Degradation**: Design services to handle dependency failures gracefully. Services should be able to continue operating even when some dependencies are unavailable. ```go // Good: Graceful degradation type userService struct { db Database cache Cache } func (s *userService) GetUser(id string) (*User, error) { // Try cache first if user, err := s.cache.GetUser(id); err == nil { return user, nil } // Fall back to database user, err := s.db.GetUser(id) if err != nil { return nil, err } // Try to cache the result (don't fail if cache is down) s.cache.SetUser(id, user) return user, nil } ``` 3. **Health Checks**: Implement health checks for critical services. This allows you to monitor the health of your application and its dependencies. ```go // Good: Implement health checks type database struct { connected bool } func (d *database) HealthCheck() error { if !d.connected { return errors.New("database not connected") } // Perform actual health check if err := d.ping(); err != nil { return fmt.Errorf("database ping failed: %w", err) } return nil } ``` 4. **Logging**: Use structured logging for debugging and monitoring. Log important events and errors with appropriate context. ```go // Good: Structured logging func (s *userService) CreateUser(user *User) error { logger := s.logger.With("user_id", user.ID, "email", user.Email) logger.Info("creating user") if err := s.db.SaveUser(user); err != nil { logger.Error("failed to save user", "error", err) return fmt.Errorf("failed to save user: %w", err) } logger.Info("user created successfully") return nil } ``` ### Performance Optimization Performance optimization is important for applications that need to handle high loads or respond quickly to user requests. 1. **Lazy Loading**: Use lazy loading for expensive services. This reduces startup time and memory usage. ```go // Good: Lazy loading for expensive services do.Provide(injector, func(i do.Injector) (ExpensiveService, error) { // Service is only created when first requested return NewExpensiveService(), nil }) // Bad: Eager loading for expensive services do.Provide(injector, func(i do.Injector) (ExpensiveService, error) { // Service is created immediately, even if not needed return NewExpensiveService(), nil }) ``` 2. **Scope Management**: Organize services in appropriate scopes. Use scopes to isolate services that don't need to be shared globally. ```go // Good: Appropriate scope usage // Global services in root scope do.Provide(rootScope, func(i do.Injector) (Database, error) { return NewDatabase(), nil }) // User-specific services in user scope do.Provide(userScope, func(i do.Injector) (UserSession, error) { db := do.MustInvoke[Database](i) return NewUserSession(db), nil }) ``` 3. **Dependency Minimization**: Keep dependency graphs shallow. Deep dependency chains can slow down service resolution and make the system harder to understand. ```go // Good: Shallow dependency graph type userService struct { db Database // Direct dependency } // Bad: Deep dependency graph type userService struct { db Database } type database struct { config Config } type config struct { env Environment } type environment struct { // ... many more levels } ``` 4. **Resource Cleanup**: Implement proper shutdown handlers. This ensures that resources are cleaned up properly when the application shuts down. ```go // Good: Proper resource cleanup type database struct { connection *sql.DB } func (d *database) Shutdown(ctx context.Context) error { if d.connection != nil { return d.connection.Close() } return nil } ``` ### Testing Strategy A comprehensive testing strategy is essential for maintaining code quality and ensuring that changes don't break existing functionality. 1. **Unit Testing**: Test services in isolation with mocked dependencies. This ensures that each service works correctly in isolation. ```go // Good: Unit testing with mocks func TestUserService_CreateUser(t *testing.T) { mockDB := &MockDatabase{} mockEmail := &MockEmailService{} userService := &userService{ db: mockDB, email: mockEmail, } user := &User{Name: "John", Email: "john@example.com"} mockDB.On("SaveUser", user).Return(nil) mockEmail.On("SendWelcomeEmail", user).Return(nil) err := userService.CreateUser(user) assert.NoError(t, err) mockDB.AssertExpectations(t) mockEmail.AssertExpectations(t) } ``` 2. **Integration Testing**: Test service interactions with real dependencies. This ensures that services work correctly together. ```go // Good: Integration testing func TestUserService_Integration(t *testing.T) { injector := do.New() // Use real database for integration test do.Provide(injector, func(i do.Injector) (Database, error) { return NewTestDatabase(), nil }) do.Provide(injector, func(i do.Injector) (UserService, error) { db := do.MustInvoke[Database](i) return NewUserService(db), nil }) userService := do.MustInvoke[UserService](injector) user := &User{Name: "John", Email: "john@example.com"} err := userService.CreateUser(user) assert.NoError(t, err) // Verify user was actually saved savedUser, err := userService.GetUser(user.ID) assert.NoError(t, err) assert.Equal(t, user.Name, savedUser.Name) } ``` 3. **Container Cloning**: Use container cloning for test isolation. This ensures that tests don't interfere with each other. ```go // Good: Test isolation with container cloning func TestUserService_Isolated(t *testing.T) { // Create test container by cloning the main container testInjector := do.Clone(mainInjector) // Override with test-specific mocks do.ProvideValue(testInjector, &MockDatabase{}) userService := do.MustInvoke[UserService](testInjector) // Test with isolated environment } ``` 4. **Mock Services**: Create mock implementations for external dependencies. This makes tests fast and reliable. ```go // Good: Mock implementations type MockDatabase struct { users map[string]*User } func (m *MockDatabase) SaveUser(user *User) error { if m.users == nil { m.users = make(map[string]*User) } m.users[user.ID] = user return nil } func (m *MockDatabase) GetUser(id string) (*User, error) { if user, exists := m.users[id]; exists { return user, nil } return nil, errors.New("user not found") } ``` ### Architecture Patterns The `do` library supports several architectural patterns that can help you build better applications. 1. **Repository Pattern**: Use repositories to abstract data access logic. ```go // Good: Repository pattern type UserRepository interface { FindByID(id string) (*User, error) Save(user *User) error Delete(id string) error } type userRepository struct { db Database } func (r *userRepository) FindByID(id string) (*User, error) { return r.db.GetUser(id) } func (r *userRepository) Save(user *User) error { return r.db.SaveUser(user) } ``` 2. **Factory Pattern**: Use factories to create complex objects. ```go // Good: Factory pattern type ServiceFactory interface { CreateUserService() (UserService, error) CreateEmailService() (EmailService, error) } type serviceFactory struct { config Config } func (f *serviceFactory) CreateUserService() (UserService, error) { db, err := f.createDatabase() if err != nil { return nil, err } return NewUserService(db), nil } ``` 3. **Configuration Pattern**: Use configuration objects to manage application settings. ```go // Good: Configuration pattern type Config struct { DatabaseURL string Port int Environment string } func LoadConfig() (*Config, error) { return &Config{ DatabaseURL: os.Getenv("DATABASE_URL"), Port: 8080, Environment: os.Getenv("ENV"), }, nil } ``` By following these best practices, you can build maintainable, scalable, and robust applications with the `do` library. These practices help ensure that your code is easy to understand, test, and maintain over time. ## Common Patterns The `do` library supports several common architectural patterns that are widely used in software development. These patterns help you build clean, maintainable, and scalable applications. ### Configuration Management Configuration management is essential for applications that need to be configurable across different environments. The `do` library makes it easy to manage configuration through dependency injection. ```go package main import ( "fmt" "os" "strconv" "time" "github.com/samber/do/v2" ) // Configuration interfaces and structs type Config interface { GetDatabaseURL() string GetPort() int GetEnvironment() string GetLogLevel() string GetTimeout() time.Duration } type AppConfig struct { DatabaseURL string Port int Environment string LogLevel string Timeout time.Duration } func (c *AppConfig) GetDatabaseURL() string { return c.DatabaseURL } func (c *AppConfig) GetPort() int { return c.Port } func (c *AppConfig) GetEnvironment() string { return c.Environment } func (c *AppConfig) GetLogLevel() string { return c.LogLevel } func (c *AppConfig) GetTimeout() time.Duration { return c.Timeout } // Configuration loader type ConfigLoader interface { Load() (*AppConfig, error) } type envConfigLoader struct{} func (l *envConfigLoader) Load() (*AppConfig, error) { port, err := strconv.Atoi(getEnvOrDefault("PORT", "8080")) if err != nil { return nil, fmt.Errorf("invalid port: %w", err) } timeout, err := time.ParseDuration(getEnvOrDefault("TIMEOUT", "30s")) if err != nil { return nil, fmt.Errorf("invalid timeout: %w", err) } return &AppConfig{ DatabaseURL: getEnvOrDefault("DATABASE_URL", "postgres://localhost:5432/mydb"), Port: port, Environment: getEnvOrDefault("ENV", "development"), LogLevel: getEnvOrDefault("LOG_LEVEL", "info"), Timeout: timeout, }, nil } func getEnvOrDefault(key, defaultValue string) string { if value := os.Getenv(key); value != "" { return value } return defaultValue } // Service that uses configuration type DatabaseService interface { Connect() error IsConnected() bool } type databaseService struct { config Config connected bool } func (d *databaseService) Connect() error { fmt.Printf("Connecting to database: %s\n", d.config.GetDatabaseURL()) d.connected = true return nil } func (d *databaseService) IsConnected() bool { return d.connected } func main() { injector := do.New() // Register configuration do.Provide(injector, func(i do.Injector) (ConfigLoader, error) { return &envConfigLoader{}, nil }) do.Provide(injector, func(i do.Injector) (Config, error) { loader := do.MustInvoke[ConfigLoader](i) return loader.Load() }) // Register services that depend on configuration do.Provide(injector, func(i do.Injector) (DatabaseService, error) { config := do.MustInvoke[Config](i) return &databaseService{config: config}, nil }) // Use the services dbService := do.MustInvoke[DatabaseService](injector) if err := dbService.Connect(); err != nil { panic(err) } fmt.Printf("Database connected: %v\n", dbService.IsConnected()) } ``` ### Factory Pattern The factory pattern is useful for creating complex objects or when you need to create objects based on configuration or other runtime conditions. ```go package main import ( "fmt" "github.com/samber/do/v2" ) // Service interfaces type Logger interface { Log(level, message string) } type Database interface { Connect() error Query(sql string) ([]string, error) } type Cache interface { Get(key string) (interface{}, error) Set(key string, value interface{}) error } // Concrete implementations type fileLogger struct { filePath string } func (l *fileLogger) Log(level, message string) { fmt.Printf("[%s] %s: %s\n", level, l.filePath, message) } type consoleLogger struct{} func (l *consoleLogger) Log(level, message string) { fmt.Printf("[%s] %s\n", level, message) } type postgresDatabase struct { connectionString string } func (d *postgresDatabase) Connect() error { fmt.Printf("Connecting to PostgreSQL: %s\n", d.connectionString) return nil } func (d *postgresDatabase) Query(sql string) ([]string, error) { fmt.Printf("Executing query: %s\n", sql) return []string{"result1", "result2"}, nil } type redisCache struct { endpoint string } func (c *redisCache) Get(key string) (interface{}, error) { fmt.Printf("Getting from Redis (%s): %s\n", c.endpoint, key) return "cached_value", nil } func (c *redisCache) Set(key string, value interface{}) error { fmt.Printf("Setting in Redis (%s): %s = %v\n", c.endpoint, key, value) return nil } // Factory interfaces type LoggerFactory interface { CreateLogger(logType string) (Logger, error) } type DatabaseFactory interface { CreateDatabase(dbType string) (Database, error) } type CacheFactory interface { CreateCache(cacheType string) (Cache, error) } // Factory implementations type loggerFactory struct { config Config } func (f *loggerFactory) CreateLogger(logType string) (Logger, error) { switch logType { case "file": return &fileLogger{filePath: f.config.GetLogPath()}, nil case "console": return &consoleLogger{}, nil default: return nil, fmt.Errorf("unknown logger type: %s", logType) } } type databaseFactory struct { config Config } func (f *databaseFactory) CreateDatabase(dbType string) (Database, error) { switch dbType { case "postgres": return &postgresDatabase{connectionString: f.config.GetDatabaseURL()}, nil default: return nil, fmt.Errorf("unknown database type: %s", dbType) } } type cacheFactory struct { config Config } func (f *cacheFactory) CreateCache(cacheType string) (Cache, error) { switch cacheType { case "redis": return &redisCache{endpoint: f.config.GetCacheEndpoint()}, nil default: return nil, fmt.Errorf("unknown cache type: %s", cacheType) } } // Application service that uses factories type ApplicationService interface { Start() error } type applicationService struct { logger Logger database Database cache Cache } func (a *applicationService) Start() error { a.logger.Log("info", "Starting application") if err := a.database.Connect(); err != nil { return err } a.logger.Log("info", "Application started successfully") return nil } func main() { injector := do.New() // Register configuration do.Provide(injector, func(i do.Injector) (Config, error) { return &AppConfig{ DatabaseURL: "postgres://localhost:5432/mydb", LogPath: "/var/log/app.log", CacheEndpoint: "localhost:6379", }, nil }) // Register factories do.Provide(injector, func(i do.Injector) (LoggerFactory, error) { config := do.MustInvoke[Config](i) return &loggerFactory{config: config}, nil }) do.Provide(injector, func(i do.Injector) (DatabaseFactory, error) { config := do.MustInvoke[Config](i) return &databaseFactory{config: config}, nil }) do.Provide(injector, func(i do.Injector) (CacheFactory, error) { config := do.MustInvoke[Config](i) return &cacheFactory{config: config}, nil }) // Register services using factories do.Provide(injector, func(i do.Injector) (Logger, error) { factory := do.MustInvoke[LoggerFactory](i) return factory.CreateLogger("file") }) do.Provide(injector, func(i do.Injector) (Database, error) { factory := do.MustInvoke[DatabaseFactory](i) return factory.CreateDatabase("postgres") }) do.Provide(injector, func(i do.Injector) (Cache, error) { factory := do.MustInvoke[CacheFactory](i) return factory.CreateCache("redis") }) do.Provide(injector, func(i do.Injector) (ApplicationService, error) { logger := do.MustInvoke[Logger](i) database := do.MustInvoke[Database](i) cache := do.MustInvoke[Cache](i) return &applicationService{ logger: logger, database: database, cache: cache, }, nil }) // Use the application service app := do.MustInvoke[ApplicationService](injector) if err := app.Start(); err != nil { panic(err) } } ``` ### Repository Pattern The repository pattern abstracts data access logic and provides a clean interface for data operations. This pattern is particularly useful for applications that need to work with different data sources. ```go package main import ( "fmt" "time" "github.com/samber/do/v2" ) // Domain models type User struct { ID string `json:"id"` Name string `json:"name"` Email string `json:"email"` CreatedAt time.Time `json:"created_at"` UpdatedAt time.Time `json:"updated_at"` } type Product struct { ID string `json:"id"` Name string `json:"name"` Description string `json:"description"` Price float64 `json:"price"` Stock int `json:"stock"` } // Repository interfaces type UserRepository interface { FindByID(id string) (*User, error) FindByEmail(email string) (*User, error) Save(user *User) error Update(user *User) error Delete(id string) error List(limit, offset int) ([]*User, error) } type ProductRepository interface { FindByID(id string) (*Product, error) Save(product *Product) error Update(product *Product) error Delete(id string) error List(limit, offset int) ([]*Product, error) Search(query string) ([]*Product, error) } // Database interface type Database interface { Query(sql string, args ...interface{}) ([]map[string]interface{}, error) Execute(sql string, args ...interface{}) error BeginTransaction() (Transaction, error) } type Transaction interface { Commit() error Rollback() error Query(sql string, args ...interface{}) ([]map[string]interface{}, error) Execute(sql string, args ...interface{}) error } // Repository implementations type userRepository struct { db Database } func (r *userRepository) FindByID(id string) (*User, error) { results, err := r.db.Query("SELECT id, name, email, created_at, updated_at FROM users WHERE id = ?", id) if err != nil { return nil, fmt.Errorf("failed to find user by ID: %w", err) } if len(results) == 0 { return nil, fmt.Errorf("user not found: %s", id) } return r.mapToUser(results[0]), nil } func (r *userRepository) FindByEmail(email string) (*User, error) { results, err := r.db.Query("SELECT id, name, email, created_at, updated_at FROM users WHERE email = ?", email) if err != nil { return nil, fmt.Errorf("failed to find user by email: %w", err) } if len(results) == 0 { return nil, fmt.Errorf("user not found: %s", email) } return r.mapToUser(results[0]), nil } func (r *userRepository) Save(user *User) error { user.CreatedAt = time.Now() user.UpdatedAt = time.Now() err := r.db.Execute( "INSERT INTO users (id, name, email, created_at, updated_at) VALUES (?, ?, ?, ?, ?)", user.ID, user.Name, user.Email, user.CreatedAt, user.UpdatedAt, ) if err != nil { return fmt.Errorf("failed to save user: %w", err) } return nil } func (r *userRepository) Update(user *User) error { user.UpdatedAt = time.Now() err := r.db.Execute( "UPDATE users SET name = ?, email = ?, updated_at = ? WHERE id = ?", user.Name, user.Email, user.UpdatedAt, user.ID, ) if err != nil { return fmt.Errorf("failed to update user: %w", err) } return nil } func (r *userRepository) Delete(id string) error { err := r.db.Execute("DELETE FROM users WHERE id = ?", id) if err != nil { return fmt.Errorf("failed to delete user: %w", err) } return nil } func (r *userRepository) List(limit, offset int) ([]*User, error) { results, err := r.db.Query( "SELECT id, name, email, created_at, updated_at FROM users ORDER BY created_at DESC LIMIT ? OFFSET ?", limit, offset, ) if err != nil { return nil, fmt.Errorf("failed to list users: %w", err) } users := make([]*User, len(results)) for i, result := range results { users[i] = r.mapToUser(result) } return users, nil } func (r *userRepository) mapToUser(data map[string]interface{}) *User { return &User{ ID: data["id"].(string), Name: data["name"].(string), Email: data["email"].(string), CreatedAt: data["created_at"].(time.Time), UpdatedAt: data["updated_at"].(time.Time), } } type productRepository struct { db Database } func (r *productRepository) FindByID(id string) (*Product, error) { results, err := r.db.Query("SELECT id, name, description, price, stock FROM products WHERE id = ?", id) if err != nil { return nil, fmt.Errorf("failed to find product by ID: %w", err) } if len(results) == 0 { return nil, fmt.Errorf("product not found: %s", id) } return r.mapToProduct(results[0]), nil } func (r *productRepository) Save(product *Product) error { err := r.db.Execute( "INSERT INTO products (id, name, description, price, stock) VALUES (?, ?, ?, ?, ?)", product.ID, product.Name, product.Description, product.Price, product.Stock, ) if err != nil { return fmt.Errorf("failed to save product: %w", err) } return nil } func (r *productRepository) Update(product *Product) error { err := r.db.Execute( "UPDATE products SET name = ?, description = ?, price = ?, stock = ? WHERE id = ?", product.Name, product.Description, product.Price, product.Stock, product.ID, ) if err != nil { return fmt.Errorf("failed to update product: %w", err) } return nil } func (r *productRepository) Delete(id string) error { err := r.db.Execute("DELETE FROM products WHERE id = ?", id) if err != nil { return fmt.Errorf("failed to delete product: %w", err) } return nil } func (r *productRepository) List(limit, offset int) ([]*Product, error) { results, err := r.db.Query( "SELECT id, name, description, price, stock FROM products ORDER BY name LIMIT ? OFFSET ?", limit, offset, ) if err != nil { return nil, fmt.Errorf("failed to list products: %w", err) } products := make([]*Product, len(results)) for i, result := range results { products[i] = r.mapToProduct(result) } return products, nil } func (r *productRepository) Search(query string) ([]*Product, error) { results, err := r.db.Query( "SELECT id, name, description, price, stock FROM products WHERE name LIKE ? OR description LIKE ?", "%"+query+"%", "%"+query+"%", ) if err != nil { return nil, fmt.Errorf("failed to search products: %w", err) } products := make([]*Product, len(results)) for i, result := range results { products[i] = r.mapToProduct(result) } return products, nil } func (r *productRepository) mapToProduct(data map[string]interface{}) *Product { return &Product{ ID: data["id"].(string), Name: data["name"].(string), Description: data["description"].(string), Price: data["price"].(float64), Stock: data["stock"].(int), } } // Service layer that uses repositories type UserService interface { GetUser(id string) (*User, error) CreateUser(name, email string) (*User, error) UpdateUser(id, name, email string) (*User, error) DeleteUser(id string) error ListUsers(limit, offset int) ([]*User, error) } type userService struct { userRepo UserRepository } func (s *userService) GetUser(id string) (*User, error) { return s.userRepo.FindByID(id) } func (s *userService) CreateUser(name, email string) (*User, error) { // Check if user already exists if _, err := s.userRepo.FindByEmail(email); err == nil { return nil, fmt.Errorf("user with email %s already exists", email) } user := &User{ ID: generateID(), Name: name, Email: email, } if err := s.userRepo.Save(user); err != nil { return nil, err } return user, nil } func (s *userService) UpdateUser(id, name, email string) (*User, error) { user, err := s.userRepo.FindByID(id) if err != nil { return nil, err } user.Name = name user.Email = email if err := s.userRepo.Update(user); err != nil { return nil, err } return user, nil } func (s *userService) DeleteUser(id string) error { return s.userRepo.Delete(id) } func (s *userService) ListUsers(limit, offset int) ([]*User, error) { return s.userRepo.List(limit, offset) } func generateID() string { return fmt.Sprintf("user_%d", time.Now().UnixNano()) } func main() { injector := do.New() // Register database do.Provide(injector, func(i do.Injector) (Database, error) { return &mockDatabase{}, nil }) // Register repositories do.Provide(injector, func(i do.Injector) (UserRepository, error) { db := do.MustInvoke[Database](i) return &userRepository{db: db}, nil }) do.Provide(injector, func(i do.Injector) (ProductRepository, error) { db := do.MustInvoke[Database](i) return &productRepository{db: db}, nil }) // Register services do.Provide(injector, func(i do.Injector) (UserService, error) { userRepo := do.MustInvoke[UserRepository](i) return &userService{userRepo: userRepo}, nil }) // Use the service userService := do.MustInvoke[UserService](injector) user, err := userService.CreateUser("John Doe", "john@example.com") if err != nil { panic(err) } fmt.Printf("Created user: %+v\n", user) } // Mock database for demonstration type mockDatabase struct{} func (m *mockDatabase) Query(sql string, args ...interface{}) ([]map[string]interface{}, error) { fmt.Printf("Mock query: %s with args %v\n", sql, args) return []map[string]interface{}{ { "id": "user_123", "name": "John Doe", "email": "john@example.com", "created_at": time.Now(), "updated_at": time.Now(), }, }, nil } func (m *mockDatabase) Execute(sql string, args ...interface{}) error { fmt.Printf("Mock execute: %s with args %v\n", sql, args) return nil } func (m *mockDatabase) BeginTransaction() (Transaction, error) { return &mockTransaction{}, nil } type mockTransaction struct{} func (m *mockTransaction) Commit() error { fmt.Println("Mock transaction commit") return nil } func (m *mockTransaction) Rollback() error { fmt.Println("Mock transaction rollback") return nil } func (m *mockTransaction) Query(sql string, args ...interface{}) ([]map[string]interface{}, error) { return nil, nil } func (m *mockTransaction) Execute(sql string, args ...interface{}) error { return nil } ``` These common patterns demonstrate how the `do` library can be used to implement clean, maintainable architectures. Each pattern provides specific benefits: - **Configuration Management**: Centralizes configuration and makes it easy to manage different environments - **Factory Pattern**: Provides flexibility in object creation and supports runtime configuration - **Repository Pattern**: Abstracts data access logic and makes the application more testable and maintainable By using these patterns with the `do` library, you can build applications that are easy to understand, test, and maintain. ## Troubleshooting Guide The `do` library is designed to be robust and provide clear error messages, but you may encounter issues as you build complex applications. This troubleshooting guide covers the most common problems and their solutions. ### Common Issues #### Circular Dependencies Circular dependencies occur when two or more services depend on each other, either directly or indirectly. The `do` library detects circular dependencies and provides clear error messages. **Problem**: Service A depends on Service B, which depends on Service A. ```go // Problem: Circular dependency type UserService interface { GetUser(id string) (*User, error) } type EmailService interface { SendEmail(to, subject, body string) error } type userService struct { emailService EmailService } type emailService struct { userService UserService // This creates a circular dependency } // Registration that will fail do.Provide(injector, func(i do.Injector) (UserService, error) { emailService := do.MustInvoke[EmailService](i) // Depends on EmailService return &userService{emailService: emailService}, nil }) do.Provide(injector, func(i do.Injector) (EmailService, error) { userService := do.MustInvoke[UserService](i) // Depends on UserService - CIRCULAR! return &emailService{userService: userService}, nil }) ``` **Solutions**: 1. **Use Interfaces**: Break the circular dependency by using interfaces. ```go // Solution 1: Use interfaces to break circular dependencies type UserService interface { GetUser(id string) (*User, error) } type EmailService interface { SendEmail(to, subject, body string) error } type userService struct { emailService EmailService } type emailService struct { // Remove direct dependency on UserService } // Registration that works do.Provide(injector, func(i do.Injector) (UserService, error) { emailService := do.MustInvoke[EmailService](i) return &userService{emailService: emailService}, nil }) do.Provide(injector, func(i do.Injector) (EmailService, error) { return &emailService{}, nil // No dependency on UserService }) ``` 2. **Restructure Dependencies**: Reorganize your services to eliminate circular dependencies. ```go // Solution 2: Restructure dependencies type UserService interface { GetUser(id string) (*User, error) } type EmailService interface { SendEmail(to, subject, body string) error } type NotificationService interface { SendWelcomeEmail(userID string) error } type userService struct { // No dependency on EmailService } type emailService struct { // No dependency on UserService } type notificationService struct { userService UserService emailService EmailService } // Registration that works do.Provide(injector, func(i do.Injector) (UserService, error) { return &userService{}, nil }) do.Provide(injector, func(i do.Injector) (EmailService, error) { return &emailService{}, nil }) do.Provide(injector, func(i do.Injector) (NotificationService, error) { userService := do.MustInvoke[UserService](i) emailService := do.MustInvoke[EmailService](i) return ¬ificationService{userService: userService, emailService: emailService}, nil }) ``` 3. **Use Lazy Loading**: Defer the resolution of dependencies until they're actually needed. ```go // Solution 3: Use lazy loading type userService struct { injector do.Injector // Store injector for lazy loading } func (s *userService) GetUser(id string) (*User, error) { // Lazy load email service only when needed emailService := do.MustInvoke[EmailService](s.injector) // Use email service... return &User{}, nil } // Registration do.Provide(injector, func(i do.Injector) (UserService, error) { return &userService{injector: i}, nil }) ``` #### Missing Dependencies Missing dependencies occur when a service tries to use another service that hasn't been registered in the container. **Problem**: Service not registered or registered in wrong scope. ```go // Problem: Missing dependency do.Provide(injector, func(i do.Injector) (UserService, error) { db := do.MustInvoke[Database](i) // Database not registered - will panic return &userService{db: db}, nil }) ``` **Solutions**: 1. **Check Registration Order**: Ensure all dependencies are registered before they're needed. ```go // Solution: Register dependencies first do.Provide(injector, func(i do.Injector) (Database, error) { return &database{}, nil }) do.Provide(injector, func(i do.Injector) (UserService, error) { db := do.MustInvoke[Database](i) // This will work now return &userService{db: db}, nil }) ``` 2. **Use Error Handling**: Use `do.Invoke` instead of `do.MustInvoke` to handle missing dependencies gracefully. ```go // Solution: Handle missing dependencies gracefully do.Provide(injector, func(i do.Injector) (UserService, error) { db, err := do.Invoke[Database](i) if err != nil { return nil, fmt.Errorf("database not available: %w", err) } return &userService{db: db}, nil }) ``` 3. **Check Scope**: Ensure services are registered in the correct scope. ```go // Solution: Register in correct scope rootScope := do.New() childScope := do.Scope(rootScope, "child") // Register in parent scope do.Provide(rootScope, func(i do.Injector) (Database, error) { return &database{}, nil }) // Use in child scope do.Provide(childScope, func(i do.Injector) (UserService, error) { db := do.MustInvoke[Database](i) // Will find Database in parent scope return &userService{db: db}, nil }) ``` #### Scope Issues Scope issues occur when services are not available in the expected scope or when there are conflicts between scopes. **Problem**: Service not found in scope or scope isolation issues. ```go // Problem: Service not found in scope rootScope := do.New() childScope := do.Scope(rootScope, "child") // Service registered in child scope do.Provide(childScope, func(i do.Injector) (Database, error) { return &database{}, nil }) // Trying to use in parent scope db := do.MustInvoke[Database](rootScope) // Will fail - Database not in root scope ``` **Solutions**: 1. **Register in Correct Scope**: Ensure services are registered in the appropriate scope. ```go // Solution: Register in correct scope rootScope := do.New() childScope := do.Scope(rootScope, "child") // Register shared services in parent scope do.Provide(rootScope, func(i do.Injector) (Database, error) { return &database{}, nil }) // Use in child scope userService := do.MustInvoke[UserService](childScope) // Will find Database in parent ``` 2. **Use Scope Hierarchy**: Understand how scope inheritance works. ```go // Solution: Use scope hierarchy correctly rootScope := do.New() userScope := do.Scope(rootScope, "user") sessionScope := do.Scope(userScope, "session") // Shared services in root scope do.Provide(rootScope, func(i do.Injector) (Database, error) { return &database{}, nil }) // User-specific services in user scope do.Provide(userScope, func(i do.Injector) (UserRepository, error) { db := do.MustInvoke[Database](i) // Can access Database from root return &userRepository{db: db}, nil }) // Session-specific services in session scope do.Provide(sessionScope, func(i do.Injector) (SessionManager, error) { userRepo := do.MustInvoke[UserRepository](i) // Can access UserRepository from user scope return &sessionManager{userRepo: userRepo}, nil }) ``` 3. **Check Scope Isolation**: Be aware that sibling scopes are isolated from each other. ```go // Solution: Understand scope isolation rootScope := do.New() scope1 := do.Scope(rootScope, "scope1") scope2 := do.Scope(rootScope, "scope2") // Service in scope1 do.Provide(scope1, func(i do.Injector) (Service, error) { return &service{}, nil }) // Cannot access Service from scope2 // service := do.MustInvoke[Service](scope2) // This will fail // But both can access services from root scope do.Provide(rootScope, func(i do.Injector) (SharedService, error) { return &sharedService{}, nil }) shared1 := do.MustInvoke[SharedService](scope1) // Works shared2 := do.MustInvoke[SharedService](scope2) // Works ``` #### Provider Errors Provider errors occur when service providers fail during initialization. **Problem**: Service provider returns an error. ```go // Problem: Provider error do.Provide(injector, func(i do.Injector) (Database, error) { db, err := NewDatabase(config) if err != nil { return nil, err // This error will be returned when service is requested } return db, nil }) ``` **Solutions**: 1. **Handle Provider Errors**: Always check for provider errors when invoking services. ```go // Solution: Handle provider errors db, err := do.Invoke[Database](injector) if err != nil { log.Printf("Failed to get database: %v", err) // Handle error appropriately return } ``` 2. **Validate Configuration**: Ensure configuration is valid before creating services. ```go // Solution: Validate configuration do.Provide(injector, func(i do.Injector) (Database, error) { config := do.MustInvoke[Config](i) if config.GetDatabaseURL() == "" { return nil, errors.New("database URL not configured") } db, err := NewDatabase(config) if err != nil { return nil, fmt.Errorf("failed to create database: %w", err) } return db, nil }) ``` 3. **Use Health Checks**: Implement health checks to detect service issues. ```go // Solution: Use health checks type database struct { connected bool } func (d *database) HealthCheck() error { if !d.connected { return errors.New("database not connected") } return nil } // Check health before using service if err := do.HealthCheck[Database](injector); err != nil { log.Printf("Database health check failed: %v", err) // Handle health check failure } ``` #### Performance Issues Performance issues can occur when dependency resolution is slow or when there are too many dependencies. **Problem**: Slow service resolution or high memory usage. **Solutions**: 1. **Use Lazy Loading**: Defer service creation until needed. ```go // Solution: Use lazy loading for expensive services do.Provide(injector, func(i do.Injector) (ExpensiveService, error) { // Service is only created when first requested return NewExpensiveService(), nil }) ``` 2. **Optimize Dependency Graph**: Keep dependency graphs shallow. ```go // Solution: Optimize dependency graph // Bad: Deep dependency chain type serviceA struct { serviceB ServiceB } type serviceB struct { serviceC ServiceC } type serviceC struct { serviceD ServiceD } // Good: Shallow dependency chain type serviceA struct { serviceB ServiceB serviceC ServiceC serviceD ServiceD } ``` 3. **Use Appropriate Scopes**: Use scopes to isolate services that don't need to be shared. ```go // Solution: Use appropriate scopes // Global services in root scope do.Provide(rootScope, func(i do.Injector) (Database, error) { return NewDatabase(), nil }) // Request-specific services in request scope do.Provide(requestScope, func(i do.Injector) (RequestContext, error) { return NewRequestContext(), nil }) ``` ### Debugging Tools The `do` library provides several tools to help you debug dependency injection issues: 1. **List Services**: See all registered services. ```go // List all services in container services := do.ListServices(injector) for _, service := range services { fmt.Printf("Service: %s, Type: %s\n", service.Name, service.Type) } ``` 2. **List Scopes**: See the scope hierarchy. ```go // List all scopes scopes := do.ListScopes(injector) for _, scope := range scopes { fmt.Printf("Scope: %s, Parent: %s\n", scope.Name, scope.Parent) } ``` 3. **Web UI**: Use the debugging web interface for visual inspection. ```go // Enable web UI for debugging do.Provide(injector, func(i do.Injector) (*do.Options, error) { return &do.Options{ EnableWebUI: true, WebUIPort: 8081, }, nil }) ``` ### Common Error Messages Here are some common error messages and their meanings: - **"circular dependency detected"**: Two or more services depend on each other - **"service not found"**: A service hasn't been registered in the container - **"service not found in scope"**: A service is registered in a different scope - **"provider error"**: A service provider returned an error during initialization - **"invalid service type"**: The service type doesn't match the registered provider By understanding these common issues and their solutions, you can quickly resolve problems and build robust applications with the `do` library. ## Migration from Other DI Libraries If you're currently using another dependency injection library in your Go application, migrating to the `do` library can provide significant benefits in terms of type safety, performance, and developer experience. This section provides detailed migration guides for popular DI libraries. ### Migration from wire Google's wire is a compile-time dependency injection library that uses code generation. While wire is powerful, it has some limitations that do addresses. **Key Differences**: - wire uses code generation, do uses runtime resolution - wire requires provider functions to be named, do uses anonymous functions - wire has limited support for interfaces, do provides full interface support - wire doesn't support scopes, do provides hierarchical scope management **Migration Steps**: 1. **Replace wire.go files with do registration**: ```go // Before: wire.go //go:build wireinject // +build wireinject package main import "github.com/google/wire" func InitializeAPI(db *Database) *API { return &API{db: db} } // After: main.go package main import "github.com/samber/do/v2" func main() { injector := do.New() do.Provide(injector, func(i do.Injector) (*Database, error) { return NewDatabase(), nil }) do.Provide(injector, func(i do.Injector) (*API, error) { db := do.MustInvoke[*Database](i) return &API{db: db}, nil }) api := do.MustInvoke[*API](injector) // Use API... } ``` 2. **Convert provider functions**: ```go // Before: wire providers func NewDatabase() *Database { return &Database{} } func NewAPI(db *Database) *API { return &API{db: db} } // After: do providers do.Provide(injector, func(i do.Injector) (*Database, error) { return &Database{}, nil }) do.Provide(injector, func(i do.Injector) (*API, error) { db := do.MustInvoke[*Database](i) return &API{db: db}, nil }) ``` 3. **Handle interface dependencies**: ```go // Before: wire with interfaces (limited support) type Database interface { Query(sql string) ([]string, error) } type PostgreSQLDatabase struct{} func NewPostgreSQLDatabase() *PostgreSQLDatabase { return &PostgreSQLDatabase{} } // Wire requires concrete types func InitializeAPI(db *PostgreSQLDatabase) *API { return &API{db: db} } // After: do with full interface support do.Provide(injector, func(i do.Injector) (Database, error) { return &PostgreSQLDatabase{}, nil }) do.Provide(injector, func(i do.Injector) (*API, error) { db := do.MustInvoke[Database](i) // Interface injection return &API{db: db}, nil }) ``` 4. **Add error handling**: ```go // Before: wire (no error handling in providers) func NewDatabase() *Database { return &Database{} } // After: do with error handling do.Provide(injector, func(i do.Injector) (*Database, error) { db, err := NewDatabase() if err != nil { return nil, fmt.Errorf("failed to create database: %w", err) } return db, nil }) ``` ### Migration from dig Uber's dig is a reflection-based dependency injection library. While dig is feature-rich, it has performance overhead due to reflection. **Key Differences**: - dig uses reflection, do uses generics for type safety - dig has runtime type checking, do provides compile-time type safety - dig supports constructor functions, do uses provider functions - dig has limited scope support, do provides hierarchical scopes **Migration Steps**: 1. **Replace dig container with do injector**: ```go // Before: dig package main import "go.uber.org/dig" func main() { container := dig.New() container.Provide(NewDatabase) container.Provide(NewAPI) var api *API container.Invoke(func(a *API) { api = a }) } // After: do package main import "github.com/samber/do/v2" func main() { injector := do.New() do.Provide(injector, func(i do.Injector) (*Database, error) { return NewDatabase(), nil }) do.Provide(injector, func(i do.Injector) (*API, error) { db := do.MustInvoke[*Database](i) return NewAPI(db), nil }) api := do.MustInvoke[*API](injector) // Use API... } ``` 2. **Convert constructor functions**: ```go // Before: dig constructors func NewDatabase() *Database { return &Database{} } func NewAPI(db *Database) *API { return &API{db: db} } // After: do providers do.Provide(injector, func(i do.Injector) (*Database, error) { return &Database{}, nil }) do.Provide(injector, func(i do.Injector) (*API, error) { db := do.MustInvoke[*Database](i) return &API{db: db}, nil }) ``` 3. **Handle named dependencies**: ```go // Before: dig with named dependencies type Config struct { DatabaseURL string `name:"database_url"` Port int `name:"port"` } func NewDatabase(config *Config) *Database { return &Database{url: config.DatabaseURL} } // After: do with configuration type Config struct { DatabaseURL string Port int } do.Provide(injector, func(i do.Injector) (*Config, error) { return &Config{ DatabaseURL: os.Getenv("DATABASE_URL"), Port: 8080, }, nil }) do.Provide(injector, func(i do.Injector) (*Database, error) { config := do.MustInvoke[*Config](i) return &Database{url: config.DatabaseURL}, nil }) ``` 4. **Add error handling**: ```go // Before: dig (errors in constructors) func NewDatabase() (*Database, error) { db, err := sql.Open("postgres", "connection_string") if err != nil { return nil, err } return &Database{db: db}, nil } // After: do (errors in providers) do.Provide(injector, func(i do.Injector) (*Database, error) { db, err := sql.Open("postgres", "connection_string") if err != nil { return nil, fmt.Errorf("failed to open database: %w", err) } return &Database{db: db}, nil }) ``` ### Migration from fx Uber's fx is a framework for building applications with dependency injection. It's built on top of dig and provides lifecycle management. **Key Differences**: - fx uses dig under the hood, do is standalone - fx has built-in lifecycle management, do provides HealthChecker and Shutdowner interfaces - fx uses reflection, do uses generics - fx has limited scope support, do provides hierarchical scopes **Migration Steps**: 1. **Replace fx.App with do injector**: ```go // Before: fx package main import "go.uber.org/fx" func main() { app := fx.New( fx.Provide(NewDatabase), fx.Provide(NewAPI), fx.Invoke(func(api *API) { // Use API... }), ) app.Run() } // After: do package main import "github.com/samber/do/v2" func main() { injector := do.New() do.Provide(injector, func(i do.Injector) (*Database, error) { return NewDatabase(), nil }) do.Provide(injector, func(i do.Injector) (*API, error) { db := do.MustInvoke[*Database](i) return NewAPI(db), nil }) api := do.MustInvoke[*API](injector) // Use API... } ``` 2. **Convert lifecycle hooks**: ```go // Before: fx lifecycle type Database struct { db *sql.DB } func NewDatabase(lifecycle fx.Lifecycle) *Database { db, _ := sql.Open("postgres", "connection_string") lifecycle.Append(fx.Hook{ OnStart: func(context.Context) error { return db.Ping() }, OnStop: func(context.Context) error { return db.Close() }, }) return &Database{db: db} } // After: do lifecycle type Database struct { db *sql.DB } func NewDatabase() (*Database, error) { db, err := sql.Open("postgres", "connection_string") if err != nil { return nil, err } return &Database{db: db}, nil } func (d *Database) HealthCheck() error { return d.db.Ping() } func (d *Database) Shutdown(ctx context.Context) error { return d.db.Close() } ``` 3. **Handle startup and shutdown**: ```go // Before: fx with startup/shutdown func main() { app := fx.New( fx.Provide(NewDatabase), fx.Invoke(func(db *Database) { // Startup logic }), ) app.Run() } // After: do with startup/shutdown func main() { injector := do.New() do.Provide(injector, func(i do.Injector) (*Database, error) { return NewDatabase() }) // Startup db := do.MustInvoke[*Database](injector) // Shutdown defer func() { ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) defer cancel() do.ShutdownAll(injector, ctx) }() } ``` ### Migration from manual dependency injection If you're currently using manual dependency injection, migrating to do can significantly improve your code organization and maintainability. **Before: Manual dependency injection**: ```go func main() { // Manual dependency creation config := &Config{ DatabaseURL: os.Getenv("DATABASE_URL"), Port: 8080, } db, err := NewDatabase(config) if err != nil { log.Fatal(err) } userRepo := NewUserRepository(db) emailService := NewEmailService(config) userService := NewUserService(userRepo, emailService) api := NewAPI(userService) // Use API... } ``` **After: do dependency injection**: ```go func main() { injector := do.New() // Register all dependencies do.Provide(injector, func(i do.Injector) (*Config, error) { return &Config{ DatabaseURL: os.Getenv("DATABASE_URL"), Port: 8080, }, nil }) do.Provide(injector, func(i do.Injector) (*Database, error) { config := do.MustInvoke[*Config](i) return NewDatabase(config) }) do.Provide(injector, func(i do.Injector) (UserRepository, error) { db := do.MustInvoke[*Database](i) return NewUserRepository(db), nil }) do.Provide(injector, func(i do.Injector) (EmailService, error) { config := do.MustInvoke[*Config](i) return NewEmailService(config), nil }) do.Provide(injector, func(i do.Injector) (UserService, error) { userRepo := do.MustInvoke[UserRepository](i) emailService := do.MustInvoke[EmailService](i) return NewUserService(userRepo, emailService), nil }) do.Provide(injector, func(i do.Injector) (*API, error) { userService := do.MustInvoke[UserService](i) return NewAPI(userService), nil }) api := do.MustInvoke[*API](injector) // Use API... } ``` ### Migration Benefits Migrating to the `do` library provides several benefits: 1. **Type-safe APIs**: Compile-time type checking eliminates runtime errors 2. **Performance**: Low reflection overhead, faster service resolution 3. **Developer Experience**: Better IDE support, clearer error messages 4. **Scope Management**: Hierarchical scopes for better resource management 5. **Testing**: Easy mocking and testing with container cloning 6. **Lifecycle Management**: Built-in health checks and graceful shutdown 7. **HTTP Integration**: Seamless integration with popular web frameworks ### Migration Checklist When migrating from another DI library to do, follow this checklist: - [ ] Identify all service dependencies - [ ] Convert provider/constructor functions to do providers - [ ] Add error handling to all providers - [ ] Implement HealthChecker and Shutdowner interfaces where appropriate - [ ] Set up appropriate scopes for service organization - [ ] Update tests to use container cloning - [ ] Configure HTTP framework integration if applicable - [ ] Test all service interactions - [ ] Monitor performance and memory usage - [ ] Update documentation and examples By following these migration guides, you can successfully transition to the `do` library and take advantage of its type safety, performance, and developer experience benefits. ## Community and Support The `do` library has a vibrant and growing community of developers who contribute to its development, share knowledge, and help each other solve problems. This section provides information about how to get involved and where to find help. ### Getting Started - **GitHub Repository**: https://github.com/samber/do - The main repository containing the source code, issues, and discussions - **Go Documentation**: https://pkg.go.dev/github.com/samber/do/v2 - Official Go documentation with API reference - **Examples**: Available in the `/examples` directory of the repository, covering various use cases and patterns - **Getting Started Guide**: Comprehensive guide for new users at `/docs/getting-started` ### Getting Help When you encounter issues or have questions about the `do` library, there are several ways to get help: #### GitHub Issues GitHub Issues is the primary place to report bugs, request features, and ask questions: - **Bug Reports**: If you find a bug, please create an issue with a detailed description of the problem, including: - Steps to reproduce the issue - Expected behavior - Actual behavior - Code examples - Environment information (Go version, operating system, etc.) - **Feature Requests**: If you have ideas for new features or improvements, create an issue describing: - The problem you're trying to solve - Proposed solution - Use cases and examples - Benefits of the feature - **Questions**: For general questions about usage, create an issue with: - Clear description of what you're trying to achieve - Code examples - What you've tried so far - Any error messages #### GitHub Discussions GitHub Discussions is a great place for: - **General Questions**: Ask questions about best practices, patterns, and usage - **Show and Tell**: Share your projects and how you're using the `do` library - **Community Help**: Help other developers with their questions - **Announcements**: Stay updated on new releases and features #### Documentation The documentation is comprehensive and covers: - **Getting Started**: Quick start guide for new users - **Core Concepts**: Detailed explanations of key features - **Advanced Topics**: Complex scenarios and troubleshooting - **API Reference**: Complete API documentation - **Examples**: Real-world examples and patterns - **Migration Guides**: Help migrating from other DI libraries ### Contributing The `do` library welcomes contributions from the community. Here are ways you can contribute: #### Code Contributions 1. **Fork the Repository**: Start by forking the repository on GitHub 2. **Create a Branch**: Create a feature branch for your changes 3. **Make Changes**: Implement your changes with proper tests 4. **Test Your Changes**: Ensure all tests pass and add new tests for new features 5. **Submit a Pull Request**: Create a pull request with a clear description of your changes #### Contribution Guidelines - **Code Style**: Follow Go conventions and the existing code style - **Testing**: Add tests for new features and ensure existing tests pass - **Documentation**: Update documentation for new features or changes - **Commit Messages**: Use clear, descriptive commit messages - **Pull Request Description**: Provide a clear description of what the PR does and why #### Areas for Contribution - **Bug Fixes**: Help fix reported bugs - **Feature Development**: Implement requested features - **Documentation**: Improve documentation and add examples - **Testing**: Add more test cases and improve test coverage - **Performance**: Optimize performance and reduce memory usage - **HTTP Framework Integration**: Add support for more web frameworks #### Development Setup To set up the development environment: ```bash # Clone the repository git clone https://github.com/samber/do.git cd do # Install dependencies go mod download # Run tests go test ./... # Run tests with race detection go test -race ./... # Run benchmarks go test -bench=. ./... # Build the library go build ./... ``` ### Community Guidelines The `do` library community follows these guidelines: - **Be Respectful**: Treat all community members with respect and kindness - **Be Helpful**: Help others learn and solve problems - **Be Patient**: Remember that everyone is at different skill levels - **Be Constructive**: Provide constructive feedback and suggestions - **Follow the Code of Conduct**: Adhere to the project's code of conduct ### Staying Updated To stay updated with the latest developments: - **Watch the Repository**: Watch the GitHub repository for updates - **Follow Releases**: Check the releases page for new versions - **Join Discussions**: Participate in GitHub discussions - **Follow the Author**: Follow the maintainer on GitHub and social media - **Newsletter**: Subscribe to the project newsletter if available ### Examples and Showcases The community has created many examples and showcases: - **Official Examples**: Comprehensive examples in the `/examples` directory - **Community Examples**: Examples shared by community members - **Real-world Projects**: Projects using the `do` library in production - **Blog Posts**: Articles and tutorials about using the `do` library - **Videos**: Video tutorials and presentations ### Support Channels - **GitHub Issues**: https://github.com/samber/do/issues - **GitHub Discussions**: https://github.com/samber/do/discussions - **Go Documentation**: https://pkg.go.dev/github.com/samber/do/v2 - **Stack Overflow**: Tag questions with `go` and `dependency-injection` - **Reddit**: r/golang community for general Go questions ## Technical Details The `do` library is built with modern Go practices and designed for performance, type safety, and developer experience. ### System Requirements - **Go Version**: Requires Go 1.18+ (for generics support) - **Operating Systems**: Supports all platforms that Go supports (Linux, macOS, Windows, etc.) - **Architectures**: Supports all architectures that Go supports (x86, x64, ARM, etc.) - **Memory**: Minimal memory footprint, typically under 1MB for the library itself ### Performance Characteristics - **Runtime Overhead**: Minimal overhead - **Memory Usage**: Efficient memory management with smart caching - **Startup Time**: Fast initialization with lazy loading support - **Service Resolution**: Optimized dependency resolution algorithm - **Concurrent Access**: Full thread safety for concurrent access - **Garbage Collection**: Optimized to reduce GC pressure ### Dependencies - **External Dependencies**: Minimal external dependencies - **Standard Library**: Primarily uses Go standard library - **Optional Dependencies**: HTTP framework integrations are optional - **Testing Dependencies**: Testing utilities and examples may have additional dependencies ### License and Legal - **License**: MIT License - Very permissive, allows commercial use - **Copyright**: Copyright (c) 2023 Samuel Berthe - **Contributions**: Contributions are licensed under the same MIT license - **Third-party Code**: Any third-party code is properly attributed and licensed ### Security - **No Reflection**: Eliminates reflection-based security concerns for developers - **Type Safety**: Compile-time type checking prevents runtime errors - **Memory Safety**: Go's memory safety features apply - **No Code Generation**: No generated code that could introduce security issues - **Audit Trail**: All changes are tracked in version control ### Compatibility - **Go Versions**: Compatible with Go 1.18 and later - **Backward Compatibility**: Maintains backward compatibility within major versions - **API Stability**: Stable API with clear deprecation policies - **Migration Path**: Clear migration paths for breaking changes ### Testing and Quality - **Test Coverage**: High test coverage for all core functionality - **Benchmarks**: Comprehensive benchmarks for performance monitoring - **Integration Tests**: Tests for HTTP framework integrations - **Race Detection**: Tests run with race detection enabled - **Cross-platform Testing**: Tests run on multiple platforms and architectures ### Release Process - **Versioning**: Follows semantic versioning (SemVer) - **Release Notes**: Detailed release notes for each version - **Breaking Changes**: Clearly documented breaking changes - **Migration Guides**: Migration guides for major version changes - **Release Schedule**: Regular releases with bug fixes and features This comprehensive documentation and community support system ensures that developers can effectively use the `do` library and get help when needed. The library emphasizes simplicity, type safety, and performance while providing powerful features for complex application architectures. Whether you're building microservices, web applications, or command-line tools, do provides the tools you need for clean, maintainable, and testable code. The `do` library represents a modern approach to dependency injection in Go, leveraging the power of generics to provide compile-time type safety while maintaining excellent runtime performance. With its comprehensive feature set, excellent documentation, and active community, do is an excellent choice for any Go project that needs robust dependency injection capabilities.