Inversion of Control and Dependency Injection
Inversion of Control (IoC) is a design principle in object-oriented programming that helps reduce coupling between components. The most common implementation of IoC is Dependancy Injection (DI). DI enables flexible and loosely coupled code by explicitly providing components with all their dependencies.
In Go, dependency injection is typically achieved by passing dependencies as parameters to constructor functions:
// Constructor for UserRepository
func NewUserRepository(database *sql.DB) *UserRepository {
return &UserRepository{db: database}
}
This approach decouples the UserRepository from database creation logic—the caller handles dependency construction while the constructor focuses on object initialization.
Without DI, constructors that create their own dependencies lead to tightly coupled code that's difficult to maintain and test:
// Anti-pattern: Self-contained constructor
func NewUserRepository() *UserRepository {
db, _ := sql.Open("postgres", "connection_string")
return &UserRepository{db: db}
}
Why Dependency Injection Tools Matter
While manual DI works for small projects, large applications with numerous dependencies become tedious to manage manually.
Consider a typical layered HTTP service structure:
├── internal
│ ├── config
│ │ └── config.go
│ ├── repository
│ │ └── user.go
│ ├── handler
│ │ └── user.go
│ └── service
│ └── user.go
└── main.go
The configuration layer provides application settings:
// config/config.go
func DefaultConfig() *Config {
return &Config{
Mode: "release",
Database: "app.db",
Port: 8080,
}
}
The repository layer handles database operations, requiring database connection:
// repository/user.go
func NewDatabase(cfg *config.Config) (*sql.DB, error) {
db, err := sql.Open("sqlite3", cfg.Database)
if err != nil {
return nil, err
}
return db, nil
}
type UserRepository struct {
db *sql.DB
}
func NewUserRepository(db *sql.DB) *UserRepository {
return &UserRepository{db: db}
}
The service layer orchestrates business logic, depending on configuration and repository:
// service/user.go
type UserService struct {
cfg *config.Config
repo *repository.UserRepository
}
func NewUserService(cfg *config.Config, repo *repository.UserRepository) *UserService {
return &UserService{cfg: cfg, repo: repo}
}
The handler layer processes HTTP requests:
// handler/user.go
type UserHandler struct {
service *service.UserService
}
func NewUserHandler(srv *service.UserService) *UserHandler {
return &UserHandler{service: srv}
}
Finally, the application entry point wires everything together:
// main.go
type Application struct {
handler *handler.UserHandler
}
func NewApplication(h *handler.UserHandler) *Application {
return &Application{handler: h}
}
Manual wiring in main() becomes cumbersome:
// main.go
func main() {
cfg := config.DefaultConfig()
db, _ := repository.NewDatabase(cfg)
userRepo := repository.NewUserRepository(db)
userSvc := service.NewUserService(cfg, userRepo)
userHandler := handler.NewUserHandler(userSvc)
app := NewApplication(userHandler)
// app.Run()
}
As dependencies grow, manually tracking creation order and injection points becomes error-prone.
Introducing Wire
Wire is a code generation tool designed specifically for dependency injection in Go. It automatically generates initialization code by analyzing dependency relationships between constructors.
Installation
Install Wire via the Go toolchain:
$ go install github.com/google/wire/cmd/wire@latest
Ensure $GOPATH/bin is in your $PATH for the command to be accessible.
Basic Usage
Setting Up the Project
Add Wire as a dependency in your project:
$ go get github.com/google/wire@latest
Consider a blog application with article-related functionality:
.
├── article
│ └── handler.go
├── article
│ └── service.go
├── engine
│ └── setup.go
├── wire_gen.go
└── main.go
The handler depends on a service interface:
// article/handler.go
type ArticleHandler struct {
articleService ArticleService
}
func (h *ArticleHandler) RegisterRoutes(router *gin.Engine) {
router.GET("/articles/:id", h.FetchArticle)
}
func (h *ArticleHandler) FetchArticle(c *gin.Context) {
id := c.Param("id")
content := h.articleService.GetArticle(c, id)
c.String(http.StatusOK, content)
}
func NewArticleHandler(svc ArticleService) *ArticleHandler {
return &ArticleHandler{articleService: svc}
}
The service interface and implementation:
// article/service.go
type ArticleService interface {
GetArticle(ctx context.Context, id string) string
}
var _ ArticleService = (*ArticleServiceImpl)(nil)
type ArticleServiceImpl struct {
}
func (s *ArticleServiceImpl) GetArticle(ctx context.Context, id string) string {
return "Article content for ID: " + id
}
func NewArticleService() ArticleService {
return &ArticleServiceImpl{}
}
The HTTP engine initialization:
// engine/setup.go
func SetupRouter(articleHandler *handler.ArticleHandler) *gin.Engine {
router := gin.Default()
articleHandler.RegisterRoutes(router)
return router
}
Creating the Wire Configuration
Create wire.go to define the injector function:
//go:build wireinject
package main
func InitializeAPI() *gin.Engine {
wire.Build(
handler.NewArticleHandler,
service.NewArticleService,
engine.SetupRouter,
)
return &gin.Engine{}
}
The wire.Build call declares all dependencies. The comment //go:build wireinject ensures this file is only compiled when running Wire.
Run the wire command to generate code:
$ wire
This produces wire_gen.go:
// Code generated by Wire. DO NOT EDIT.
//go:build !wireinject
package main
import (
"github.com/gin-gonic/gin"
"golang-example/wire/blog/engine"
"golang-example/wire/blog/handler"
"golang-example/wire/blog/service"
)
// Injectors from wire.go:
func InitializeAPI() *gin.Engine {
articleService := service.NewArticleService()
articleHandler := handler.NewArticleHandler(articleService)
router := engine.SetupRouter(articleHandler)
return router
}
The generated code correctly resolves dependency order and injects instances.
Wire Core Concepts
Providers
A provider is any function that produces a value—essentially any function with a return value. For example, NewArticleHandler is a provider:
func NewArticleHandler(svc ArticleService) *ArticleHandler {
return &ArticleHandler{articleService: svc}
}
Providers can return multiple values, including error for failure cases.
When multiple providers exist, group them using wire.NewSet:
package handler
var HandlerSet = wire.NewSet(NewArticleHandler, NewUserHandler)
Grouped sets can be combined:
var AllHandlers = wire.NewSet(HandlerSet, AdminHandlerSet)
Refactoring the injector:
func InitializeAPIV2() *gin.Engine {
wire.Build(
handler.HandlerSet,
service.NewArticleService,
engine.SetupRouter,
)
return &gin.Engine{}
}
Injectors
An injector connects providers together and returns the final result. In the previous example, InitializeAPI is an injector—it calls wire.Build to declare dependencies and returns a value that satisfies the compiler while Wire extracts the actual result from the underlying provider call.
Advanced Wire Features
Interface Binding
When a provider accepts an interface but the constructor returns a concrete type, use wire.Bind to establish the relationship:
// service.go - returns concrete type
func NewArticleService() *ArticleServiceImpl {
return &ArticleServiceImpl{}
}
// wire.go - binding interface to implementation
func InitializeAPI() *gin.Engine {
wire.Build(
handler.NewArticleHandler,
service.NewArticleService,
engine.SetupRouter,
wire.Bind(new(ArticleService), new(*ArticleServiceImpl)),
)
return &gin.Engine{}
}
wire.Bind takes two arguments: a pointer to the interface type and a pointer to the implementing type.
Struct Providers
Use wire.Struct to construct structs from field providers:
type Author string
func NewAuthor() Author {
return "John Doe"
}
type Category string
func NewCategory() Category {
return "Technology"
}
type Article struct {
Author Author
Category Category
}
func CreateArticle() *Article {
wire.Build(
NewAuthor,
NewCategory,
wire.Struct(new(Article), "Author", "Category"),
)
return &Article{}
}
Generated code:
func CreateArticle() *Article {
author := NewAuthor()
category := NewCategory()
article := &Article{
Author: author,
Category: category,
}
return article
}
Value Binding
Inject specific values rather than relying on providers:
func CreateArticleWithValue() Article {
wire.Build(wire.Value(Article{
Author: "Jane Smith",
Category: "Science",
}))
return Article{}
}
Generated code:
func CreateArticleWithValue() Article {
return _wireArticleValue
}
var (
_wireArticleValue = Article{
Author: "Jane Smith",
Category: "Science",
}
)
For interfaces, use InterfaceValue:
func ProvideMockService() ArticleService {
wire.Build(wire.InterfaceValue(new(ArticleService), &MockService{}))
return nil
}
Field Extraction
Extract struct fields as standalone providers using wire.FieldsOf:
func GetAuthorName() Author {
wire.Build(
CreateArticle,
wire.FieldsOf(new(Article), "Author"),
)
return ""
}
Generated code:
func GetAuthorName() Author {
article := CreateArticle()
author := article.Author
return author
}
Cleanup Functions
Providers can return cleanup functions for resource management. Wire aggregates these into a single cleanup function returned alongside the main value.
Provider signature requirements:
- First return value: the produced object
- Second return value:
func()orerror - Third return value:
func()if second isfunc(), otherwiseerror
// database/database.go
func EstablishConnection(dsn string) (*sql.DB, func(), error) {
db, err := sql.Open("postgres", dsn)
if err != nil {
return nil, nil, err
}
cleanup := func() {
db.Close()
}
return db, cleanup, nil
}
// wire.go
func BuildApplication() (*Application, func(), error) {
wire.Build(
database.EstablishConnection,
repository.NewUserRepository,
service.NewUserService,
NewApplication,
)
return nil, nil, nil
}
Generated cleanup aggregation:
func BuildApplication() (*Application, func(), error) {
db, cleanup1, err := database.EstablishConnection("postgres://...")
if err != nil {
return nil, nil, err
}
// ... other dependency creation ...
aggregatedCleanup := func() {
cleanup1()
// other cleanup functions from providers
}
return app, aggregatedCleanup, nil
}
Usage in main:
app, cleanup, err := BuildApplication()
if err != nil {
log.Fatal(err)
}
defer cleanup()
Alternative Injector Syntax
Replace the return statement with panic for cleaner injector definitions:
func InitializeGin() *gin.Engine {
panic(wire.Build(
handler.HandlerSet,
service.NewArticleService,
engine.SetupRouter,
))
}
This eliminates the dummy return value while satisfying the compiler.