Mastering Go Dependency Injection: Manual vs Google Wire Explained
This article examines the challenges of tangled initialization in Go's main.go, demonstrates a manual DI container for an order service, and contrasts it with Google’s compile‑time wire tool for a user service, offering practical code examples, step‑by‑step guidance, and recommendations for projects of different sizes.
When a Go project's main.go becomes a "big mud ball" filled with numerous NewService calls, the initialization order can quickly become unmanageable. This article uses the open‑source easyms.golang project to explore dependency injection (DI) concepts and presents two concrete implementations.
Problem: Over‑grown main.go
A typical Go main function often looks like spaghetti code, directly creating configuration, logger, database, repositories, services, handlers, and routing in one place. As the number of services grows, the file expands and becomes hard to maintain.
func main() {
// A typical "spaghetti" main function
cfg := config.Load()
logger := logger.New(cfg.Log)
db, err := database.New(cfg.DB)
if err != nil { /* ... */ }
userRepo := repository.NewUserRepo(db)
orderRepo := repository.NewOrderRepo(db)
authSvc := service.NewAuthService(userRepo)
orderSvc := service.NewOrderService(orderRepo, logger)
authHandler := handler.NewAuthHandler(authSvc)
orderHandler := handler.NewOrderHandler(orderSvc)
engine := gin.New()
engine.POST("/login", authHandler.Login)
engine.POST("/orders", orderHandler.Create)
engine.Run()
}Solution 1: Manual DI Container (order‑svc)
The simplest approach extracts the initialization logic into a dedicated file, creating a manual DI container.
1. Define the App struct that holds core components such as the HTTP engine, discovery service, and other services.
// internal/services/order/cmd/ordersvc/app.go
type App struct {
engine *gin.Engine
ds *discovery.Discovery
relayService *service.RelayService
// ... other fields
}2. Implement InitializeApp to load configuration, create the database, instantiate services, and assemble the App instance. The function also returns a cleanup closure that shuts down resources in reverse order.
func InitializeApp(serverName string, env string) (*App, func(), error) {
// --- 1. Load config ---
appConfig, err := provideAppConfig(...)
if err != nil { return nil, nil, err }
// --- 2. Init basic components ---
dbase, err := provideDatabase(appConfig)
// ...
// --- 3. Init services ---
orderService := service.NewOrderService(dbase)
// ...
// --- 4. Build App ---
app := &App{ /* fields */ }
// --- 5. Cleanup function ---
cleanup := func() {
relayService.Stop()
// ...
}
return app, cleanup, nil
}3. Simplify main.go by calling the initializer and deferring cleanup.
func main() {
app, cleanup, err := InitializeApp("order-svc", "dev")
if err != nil { panic(err) }
defer cleanup()
app.engine.Run(...)
}Evaluation : This manual container is clear, fully controllable, and requires no external tooling, making it ideal for small‑to‑medium projects.
Solution 2: Automatic DI with Google Wire (user‑svc)
For larger codebases, writing the initialization by hand becomes tedious. Google’s wire generates the wiring code at compile time.
1. Create a wire.go file that declares a ProviderSet describing how each component should be constructed.
// internal/services/user/cmd/usersvc/wire.go
package main
import "github.com/google/wire"
var providerSet = wire.NewSet(
config.InitAppConfigStore,
provideDiscovery,
provideAppConfig,
provideDatabase,
service.NewUserService,
handles.NewUserHandler,
NewApp,
)
func InitializeApp(inputs ConfigInputs) (*App, func(), error) {
// wire will fill this body automatically
wire.Build(providerSet)
return nil, nil, nil
}2. Run the wire command to generate wire_gen.go. The generated file contains the same initialization logic that was manually written for the order service.
3. Use the generated InitializeApp in main.go exactly as with the manual approach.
func main() {
app, cleanup, err := InitializeApp(userConfigInputs)
if err != nil { panic(err) }
defer cleanup()
app.engine.Run(...)
}Evaluation : The Wire solution offers high automation and compile‑time dependency checking. Initial setup (vendor handling, build tags) has a learning curve, but once configured it dramatically reduces boilerplate for complex projects.
Practical Recommendations
Embrace interfaces : Depend on abstractions (e.g., db.Database) rather than concrete implementations.
Use constructor functions : Provide a New... function for every struct to make dependencies explicit.
Choose the DI approach based on project size :
Small/medium projects : Manual DI container (as shown for order‑svc) – simple, no extra tooling.
Large/complex projects : Google Wire (as shown for user‑svc) – static analysis and code generation keep the wiring maintainable.
The ultimate goal of any DI strategy is to keep main.go clean and make the dependency graph obvious.
Source repository URLs (kept for reference): https://github.com/louis-xie-programmer/easyms.golang https://gitee.com/louis_xie/easyms.golang
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
Code Wrench
Focuses on code debugging, performance optimization, and real-world engineering, sharing efficient development tips and pitfall guides. We break down technical challenges in a down-to-earth style, helping you craft handy tools so every line of code becomes a problem‑solving weapon. 🔧💻
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
