Best Practices
This guide covers best practices for using godi effectively in your Go applications.
Service Design
Use Interfaces
Always define your services as interfaces:
// Good: Interface-based design
type UserService interface {
GetUser(ctx context.Context, id string) (*User, error)
CreateUser(ctx context.Context, user *User) error
UpdateUser(ctx context.Context, user *User) error
DeleteUser(ctx context.Context, id string) error
}
type userService struct {
repo UserRepository
logger Logger
cache Cache
}
func NewUserService(repo UserRepository, logger Logger, cache Cache) UserService {
return &userService{
repo: repo,
logger: logger,
cache: cache,
}
}
Benefits:
Easy testing with mocks
Clear contracts between components
Flexibility to change implementations
Better documentation
Constructor Injection
Always use constructor injection, not property injection:
// Good: Constructor injection
type OrderService struct {
userRepo UserRepository
orderRepo OrderRepository
payment PaymentGateway
logger Logger
}
func NewOrderService(
userRepo UserRepository,
orderRepo OrderRepository,
payment PaymentGateway,
logger Logger,
) *OrderService {
return &OrderService{
userRepo: userRepo,
orderRepo: orderRepo,
payment: payment,
logger: logger,
}
}
// Bad: Property injection
type BadOrderService struct {
UserRepo UserRepository // Public fields
OrderRepo OrderRepository
}
Single Responsibility
Each service should have a single, well-defined responsibility:
// Good: Focused services
type AuthService interface {
Login(username, password string) (*User, error)
Logout(token string) error
ValidateToken(token string) (*Claims, error)
}
type UserService interface {
GetUser(id string) (*User, error)
UpdateProfile(id string, profile Profile) error
}
// Bad: Mixed responsibilities
type BadService interface {
// Auth methods
Login(username, password string) (*User, error)
// User methods
GetUser(id string) (*User, error)
// Email methods
SendEmail(to, subject, body string) error
}
Lifetime Management
Choose the Right Lifetime
// Singleton: Stateless, thread-safe, shared resources
services.AddSingleton(NewLogger) // ✅ Stateless
services.AddSingleton(NewConfiguration) // ✅ Immutable
services.AddSingleton(NewMetricsCollector) // ✅ Thread-safe
// Scoped: Request-specific, holds state during request
services.AddScoped(NewUnitOfWork) // ✅ Transaction boundary
services.AddScoped(NewRequestContext) // ✅ Request metadata
services.AddScoped(NewRepository) // ✅ May use scoped transaction
// Transient: Lightweight, unique state, short-lived
services.AddTransient(NewCommand) // ✅ Single operation
services.AddTransient(NewValidator) // ✅ Stateless operation
services.AddTransient(NewEmailMessage) // ✅ Unique per use
Avoid Captive Dependencies
Never inject a service with a shorter lifetime into one with a longer lifetime:
// Bad: Scoped service in singleton
type BadSingleton struct {
scopedService ScopedService // ❌ Will capture first scope's instance
}
// Good: Use a factory or service provider
type GoodSingleton struct {
provider godi.ServiceProvider
}
func (s *GoodSingleton) DoWork(ctx context.Context) error {
scope := s.provider.CreateScope(ctx)
defer scope.Close()
scopedService, _ := godi.Resolve[ScopedService](scope.ServiceProvider())
return scopedService.Process()
}
Scope Management
Always Close Scopes
// Good: Always use defer
func HandleRequest(provider godi.ServiceProvider) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
scope := provider.CreateScope(r.Context())
defer scope.Close() // ✅ Always cleanup
// Handle request
}
}
// Bad: Manual cleanup
func BadHandler(provider godi.ServiceProvider) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
scope := provider.CreateScope(r.Context())
// Handle request
scope.Close() // ❌ May not run if panic occurs
}
}
One Scope Per Operation
// Web request = one scope
// Background job = one scope
// Test case = one scope
// Good: Scope per job
func ProcessJobs(provider godi.ServiceProvider, jobs <-chan Job) {
for job := range jobs {
func(j Job) {
scope := provider.CreateScope(context.Background())
defer scope.Close()
processor, _ := godi.Resolve[JobProcessor](scope.ServiceProvider())
processor.Process(j)
}(job)
}
}
Error Handling
Check Resolution Errors
Always handle errors from service resolution:
// Good: Check errors
service, err := godi.Resolve[UserService](provider)
if err != nil {
if godi.IsNotFound(err) {
return nil, fmt.Errorf("user service not registered")
}
return nil, fmt.Errorf("failed to resolve user service: %w", err)
}
// Bad: Ignoring errors
service, _ := godi.Resolve[UserService](provider) // ❌
Constructor Validation
Validate dependencies in constructors:
func NewPaymentService(
gateway PaymentGateway,
logger Logger,
config *PaymentConfig,
) (*PaymentService, error) {
if gateway == nil {
return nil, errors.New("gateway is required")
}
if logger == nil {
return nil, errors.New("logger is required")
}
if config == nil {
return nil, errors.New("config is required")
}
if config.APIKey == "" {
return nil, errors.New("API key is required")
}
return &PaymentService{
gateway: gateway,
logger: logger,
config: config,
}, nil
}
Testing
Use Test Containers
Create separate service collections for tests:
func TestUserService(t *testing.T) {
// Test-specific container
services := godi.NewServiceCollection()
// Register mocks
services.AddSingleton(func() UserRepository {
return &MockUserRepository{
users: map[string]*User{
"1": {ID: "1", Name: "Test User"},
},
}
})
services.AddSingleton(func() Logger {
return &TestLogger{t: t}
})
services.AddScoped(NewUserService)
provider, err := services.BuildServiceProvider()
require.NoError(t, err)
defer provider.Close()
// Test with mocks
service, err := godi.Resolve[UserService](provider)
require.NoError(t, err)
user, err := service.GetUser(context.Background(), "1")
assert.NoError(t, err)
assert.Equal(t, "Test User", user.Name)
}
Test Helpers
Create helpers for common test scenarios:
// testutil/di.go
func NewTestProvider(t *testing.T, opts ...TestOption) godi.ServiceProvider {
services := godi.NewServiceCollection()
// Default test services
services.AddSingleton(NewTestLogger)
services.AddSingleton(NewTestConfig)
// Apply options
for _, opt := range opts {
opt(services)
}
provider, err := services.BuildServiceProvider()
require.NoError(t, err)
t.Cleanup(func() {
provider.Close()
})
return provider
}
type TestOption func(godi.ServiceCollection)
func WithMockDatabase(mock Database) TestOption {
return func(s godi.ServiceCollection) {
s.AddSingleton(func() Database { return mock })
}
}
Module Organization
Module Dependencies
Define clear module dependencies:
// Core module has no dependencies
var CoreModule = godi.Module("core",
godi.AddSingleton(NewConfig),
godi.AddSingleton(NewLogger),
godi.AddSingleton(NewMetrics),
)
// Data module depends on core
var DataModule = godi.Module("data",
godi.AddModule(CoreModule), // Explicit dependency
godi.AddSingleton(NewDatabase),
godi.AddScoped(NewRepository),
)
// Business module depends on data
var BusinessModule = godi.Module("business",
godi.AddModule(DataModule), // Includes core transitively
godi.AddScoped(NewUserService),
godi.AddScoped(NewOrderService),
)
Performance
Cache Service Resolution
For hot paths, cache resolved services:
type CachedHandler struct {
provider godi.ServiceProvider
service UserService
mu sync.RWMutex
}
func (h *CachedHandler) getService() (UserService, error) {
h.mu.RLock()
if h.service != nil {
h.mu.RUnlock()
return h.service, nil
}
h.mu.RUnlock()
h.mu.Lock()
defer h.mu.Unlock()
if h.service != nil {
return h.service, nil
}
service, err := godi.Resolve[UserService](h.provider)
if err != nil {
return nil, err
}
h.service = service
return service, nil
}
Avoid Over-Injection
Don’t inject everything:
// Good: Inject services
func NewOrderService(repo OrderRepository, payment PaymentGateway) *OrderService
// Bad: Injecting simple values
func NewBadService(
repo Repository,
timeout time.Duration, // ❌ Pass in config instead
maxRetries int, // ❌ Pass in config instead
debugMode bool, // ❌ Pass in config instead
) *BadService
// Good: Group configuration
type ServiceConfig struct {
Timeout time.Duration
MaxRetries int
DebugMode bool
}
func NewGoodService(repo Repository, config ServiceConfig) *GoodService
Common Pitfalls
1. Circular Dependencies
// Bad: Circular dependency
type UserService struct {
orderService OrderService
}
type OrderService struct {
userService UserService // ❌ Circular!
}
// Good: Break the cycle
type UserService struct {
orderRepo OrderRepository // Use repository instead
}
type OrderService struct {
userRepo UserRepository // Use repository instead
}
2. Service Locator Anti-Pattern
// Bad: Service locator
type BadService struct {
provider godi.ServiceProvider
}
func (s *BadService) DoWork() error {
// Resolving services in methods
repo, _ := godi.Resolve[Repository](s.provider) // ❌
return repo.Save(data)
}
// Good: Constructor injection
type GoodService struct {
repo Repository
}
func NewGoodService(repo Repository) *GoodService {
return &GoodService{repo: repo}
}
3. Leaking Abstractions
// Bad: Exposing DI framework
func NewBadService(provider godi.ServiceProvider) *BadService // ❌
// Good: Hide DI details
func NewGoodService(dep1 Dep1, dep2 Dep2) *GoodService // ✅
Summary Checklist
✅ DO:
Use interfaces for services
Choose appropriate lifetimes
Always close scopes with defer
Handle resolution errors
Test with mock implementations
Group services in modules
Validate in constructors
❌ DON’T:
Mix service lifetimes incorrectly
Use service locator pattern
Ignore errors
Create circular dependencies
Expose DI framework details
Over-inject simple values
Forget to close scopes
Following these best practices will help you build maintainable, testable, and scalable applications with godi.