Go Dependency Injection in Practice: Build Modular Application Architecture with Uber Fx Using 5 Core Patterns

编程语言

Go Dependency Injection: Why Are You Still Manually Wiring Dependencies in 2026?

Still creating dozens of dependencies manually in main()? Changing one constructor requires modifying ten call sites? Replacing dependencies for testing means changing lots of code? Dependency Injection (DI) is the core pattern for solving these problems. In 2026, Uber Fx has become the most mature DI framework in the Go ecosystem, used at scale by Uber, Lyft, and others. Fx uses three core concepts — Provider + Invoker + Lifecycle — to achieve declarative dependency assembly → automatic dependency graph construction → graceful startup/shutdown management.

This article walks through 5 core patterns, covering the full pipeline from module definition → dependency injection → lifecycle management → optional dependencies → test integration.


Core Concepts

Concept Description
Provider Constructor function that provides dependencies
Invoker Function that consumes dependencies
Module Logical grouping of Providers and Invokers
Lifecycle Application startup/shutdown lifecycle hooks
Value Group Collection of multiple dependencies of the same type
Named Named dependencies to distinguish different instances of the same type
Optional Optional dependency that doesn't error when missing
Decorate Decorator to replace or wrap existing dependencies

Problem Analysis: 5 Challenges in Go Dependency Management

  1. Manual wiring: Dozens of NewXxx() calls in main(), order-sensitive
  2. Circular dependencies: A depends on B, B depends on A, compilation error
  3. Lifecycle management: Graceful shutdown of database connections and HTTP servers
  4. Testing difficulty: Replacing dependencies requires modifying lots of code
  5. Module reuse: Different services sharing the same infrastructure modules

Step-by-Step: 5 Core Uber Fx Patterns

Pattern 1: Fx Application Basics and Module Definition

// cmd/server/main.go
package main

import (
	"go.uber.org/fx"
	"github.com/example/myapp/internal/config"
	"github.com/example/myapp/internal/database"
	"github.com/example/myapp/internal/httpserver"
	"github.com/example/myapp/internal/repository"
	"github.com/example/myapp/internal/service"
)

func main() {
	app := fx.New(
		fx.Provide(
			config.NewConfig,
			database.NewPostgresDB,
			repository.NewUserRepository,
			repository.NewOrderRepository,
			service.NewUserService,
			service.NewOrderService,
		),
		fx.Invoke(
			httpserver.RegisterRoutes,
		),
	)

	app.Run()
}

Pattern 2: Provider Dependency Injection and Constructor Pattern

// internal/database/postgres.go
package database

import (
	"context"
	"fmt"
	"time"

	"github.com/example/myapp/internal/config"
	"github.com/jackc/pgx/v5/pgxpool"
	"go.uber.org/fx"
)

func NewPostgresDB(lc fx.Lifecycle, cfg *config.Config) (*pgxpool.Pool, error) {
	dsn := fmt.Sprintf(
		"postgres://%s:%s@%s:%d/%s?sslmode=%s",
		cfg.Database.User, cfg.Database.Password,
		cfg.Database.Host, cfg.Database.Port,
		cfg.Database.DBName, cfg.Database.SSLMode,
	)

	poolCfg, err := pgxpool.ParseConfig(dsn)
	if err != nil {
		return nil, fmt.Errorf("failed to parse database config: %w", err)
	}

	poolCfg.MaxConns = 25
	poolCfg.MinConns = 5
	poolCfg.MaxConnIdleTime = 5 * time.Minute

	pool, err := pgxpool.NewWithConfig(context.Background(), poolCfg)
	if err != nil {
		return nil, fmt.Errorf("failed to create connection pool: %w", err)
	}

	lc.Append(fx.Hook{
		OnStart: func(ctx context.Context) error {
			return pool.Ping(ctx)
		},
		OnStop: func(ctx context.Context) error {
			pool.Close()
			return nil
		},
	})

	return pool, nil
}

Pattern 3: Lifecycle Management and Graceful Shutdown

// internal/httpserver/server.go
package httpserver

import (
	"context"
	"fmt"
	"net/http"
	"time"

	"go.uber.org/fx"
)

func NewHTTPServer(lc fx.Lifecycle, mux *http.ServeMux, cfg *config.Config) *http.Server {
	srv := &http.Server{
		Addr:         fmt.Sprintf(":%d", cfg.Server.Port),
		Handler:      mux,
		ReadTimeout:  15 * time.Second,
		WriteTimeout: 15 * time.Second,
		IdleTimeout:  60 * time.Second,
	}

	lc.Append(fx.Hook{
		OnStart: func(ctx context.Context) error {
			go srv.ListenAndServe()
			return nil
		},
		OnStop: func(ctx context.Context) error {
			shutdownCtx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
			defer cancel()
			return srv.Shutdown(shutdownCtx)
		},
	})

	return srv
}

Pattern 4: Value Group and Named Dependencies

// internal/middleware/module.go
package middleware

import (
	"net/http"
	"go.uber.org/fx"
)

type Middleware func(http.Handler) http.Handler

var Module = fx.Module("middleware",
	fx.Provide(
		fx.Annotate(NewLoggingMiddleware, fx.ResultTags(`group:"middlewares"`)),
		fx.Annotate(NewAuthMiddleware, fx.ResultTags(`group:"middlewares"`)),
		fx.Annotate(NewCORSMiddleware, fx.ResultTags(`group:"middlewares"`)),
		fx.Annotate(NewRateLimitMiddleware, fx.ResultTags(`group:"middlewares"`)),
	),
	fx.Invoke(ChainMiddlewares),
)

func ChainMiddlewares(mux *http.ServeMux, middlewares []Middleware) {
	var handler http.Handler = mux
	for i := len(middlewares) - 1; i >= 0; i-- {
		handler = middlewares[i](handler)
	}
	mux.Handle("/", handler)
}

Pattern 5: Fx Test Integration

// internal/service/user_test.go
package service_test

import (
	"context"
	"testing"

	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/mock"
	"go.uber.org/fx/fxtest"

	"github.com/example/myapp/internal/repository"
	"github.com/example/myapp/internal/service"
)

type MockUserRepository struct {
	mock.Mock
}

func (m *MockUserRepository) GetByID(ctx context.Context, id int64) (*repository.User, error) {
	args := m.Called(ctx, id)
	if args.Get(0) == nil {
		return nil, args.Error(1)
	}
	return args.Get(0).(*repository.User), args.Error(1)
}

func TestUserService_GetUser(t *testing.T) {
	mockRepo := new(MockUserRepository)
	mockRepo.On("GetByID", mock.Anything, int64(1)).Return(&repository.User{
		ID: 1, Username: "testuser", Email: "test@example.com",
	}, nil)

	svc := service.NewUserService(mockRepo)
	user, err := svc.GetUser(context.Background(), 1)

	assert.NoError(t, err)
	assert.Equal(t, "testuser", user.Username)
}

Pitfall Guide

Pitfall 1: Circular Dependencies

// ❌ Wrong: A depends on B, B depends on A
fx.Provide(NewA, NewB)  // circular dependency

// ✅ Correct: Extract shared dependency to C
fx.Provide(NewC, NewA, NewB)  // A needs C, B needs C

Pitfall 2: Forgetting to Register Provider

// ❌ Wrong: Using unregistered dependency
fx.Invoke(func(svc *service.OrderService) { ... })

// ✅ Correct: Ensure all dependencies are registered
fx.Provide(service.NewOrderService),
fx.Invoke(func(svc *service.OrderService) { ... }),

Pitfall 3: Blocking Lifecycle Hook

// ❌ Wrong: Blocking in OnStart
lc.Append(fx.Hook{
    OnStart: func(ctx context.Context) error {
        srv.ListenAndServe()  // blocks!
        return nil
    },
})

// ✅ Correct: Start in goroutine
lc.Append(fx.Hook{
    OnStart: func(ctx context.Context) error {
        go srv.ListenAndServe()
        return nil
    },
})

Pitfall 4: Value Group Type Mismatch

// ❌ Wrong: Inconsistent group element types
fx.Annotate(NewLogMiddleware, fx.ResultTags(`group:"middlewares"`))   // Middleware
fx.Annotate(NewAuthHandler, fx.ResultTags(`group:"middlewares"`))     // http.Handler

// ✅ Correct: Ensure consistent group element types
fx.Annotate(NewLogMiddleware, fx.ResultTags(`group:"middlewares"`))   // Middleware
fx.Annotate(NewAuthMiddleware, fx.ResultTags(`group:"middlewares"`)) // Middleware

Pitfall 5: Not Using fxtest in Tests

// ❌ Wrong: Manual Fx app for testing
app := fx.New(...)

// ✅ Correct: Use fxtest for automatic verification
app := fxtest.New(t, ...)
app.RequireStart()
defer app.RequireStop()

Error Troubleshooting

# Error Message Cause Solution
1 missing type: *service.UserService Provider not registered Add fx.Provide
2 cycle detected Circular dependency Extract shared dependency
3 cannot provide function returns Constructor signature error Ensure dependency type is returned
4 OnStart hook timed out Startup timeout Check if hook is blocking
5 already providing type Duplicate Provider registration Use fx.Named to distinguish
6 no value found for group Empty Value Group Check Group tags and Providers
7 optional dependency not found Missing optional dependency Use fx.Annotate with Optional
8 invalid Annotate usage Annotation usage error Check fx.Annotate parameters
9 context cancelled Operation during shutdown Check OnStop Hook
10 failed to build dependency graph Dependency graph build failure Check all Providers and Invokes

Advanced Optimization

  1. Modular organization: Use fx.Module to group Providers and Invokers by business domain
  2. Decorator pattern: Use fx.Decorate to replace test environment dependencies
  3. Conditional injection: Use fx.Annotate with fx.OptionalTags for conditional dependencies
  4. Dependency replacement: Use fx.Replace to swap specific dependencies in tests
  5. Custom logging: Use fx.WithLogger to customize Fx log output format

Comparison

Dimension Uber Fx Wire Dig google/wire
Injection method Runtime Compile-time Runtime Compile-time
Type safety ⭐⭐⭐ ⭐⭐⭐⭐⭐ ⭐⭐⭐ ⭐⭐⭐⭐⭐
Lifecycle ⭐⭐⭐⭐⭐ ⭐⭐ ⭐⭐
Value Group ⭐⭐⭐⭐⭐ ⭐⭐ ⭐⭐⭐ ⭐⭐
Test friendliness ⭐⭐⭐⭐⭐ ⭐⭐⭐⭐ ⭐⭐⭐ ⭐⭐⭐⭐
Debug info ⭐⭐⭐⭐⭐ ⭐⭐⭐ ⭐⭐⭐⭐ ⭐⭐⭐
Learning curve Medium High Low High

Summary: Uber Fx evolves Go dependency injection from "manual wiring" to "declarative auto-injection". Provider → Invoker → Lifecycle → Value Group → Test integration — all five in one, achieving automatic dependency resolution, lifecycle management, and graceful shutdown. Core principles: constructors are Providers, dependencies are parameters, lifecycle is Hooks.


Try these browser-local tools — no sign-up required →

#Go依赖注入#Uber Fx#Go DI框架#Go应用架构#2026#编程语言