Using Wire for Dependency Injection in Go Web Applications: A Practical Guide

This article demonstrates how to apply Google’s Wire tool in a production‑grade Go web service, covering project layout, a four‑layer architecture, code generation for constructors, comparisons with dig and inject, and practical CLI usage to streamline dependency injection and improve development efficiency.

Go Programming World
Go Programming World
Go Programming World
Using Wire for Dependency Injection in Go Web Applications: A Practical Guide

Building on the introductory article about Wire, this piece walks through a real‑world Go project named user that implements a simple CRUD web service and shows how Wire can be used to wire the components together in production.

The project follows a clean directory structure:

$ tree user
user
├── assets
│   ├── curl.sh
│   └── schema.sql
├── cmd
│   └── main.go
├── go.mod
├── go.sum
├── internal
│   ├── biz
│   │   └── user.go
│   ├── config
│   │   └── config.go
│   ├── controller
│   │   └── user.go
│   ├── model
│   │   └── user.go
│   ├── router.go
│   ├── store
│   │   └── user.go
│   ├── user.go
│   ├── wire.go
│   └── wire_gen.go
└── pkg
    ├── api
    │   └── user.go
    └── db
        └── db.go

12 directories, 16 files

Each folder has a clear purpose: assets stores resources such as schema.sql (the table‑creation script) and curl.sh (a curl command for testing); cmd holds the program entry point; internal contains the business logic split into four layers—controller, biz, store, and model; pkg provides reusable libraries like API request structs and a database helper.

The four‑layer dependency graph is straightforward: the controller depends on the biz layer, which depends on the store layer, which in turn depends on the model layer and the GORM *gorm.DB instance created by pkg/db. All three layers also share the model definitions.

The entry point cmd/main.go creates a configuration object, builds the application via user.NewApp, defers cleanup, and runs the server:

package main

import (
    user "github.com/jianghushinian/blog-go-example/wire/user/internal"
    "github.com/jianghushinian/blog-go-example/wire/user/internal/config"
    "github.com/jianghushinian/blog-go-example/wire/user/pkg/db"
)

func main() {
    cfg := &config.Config{MySQL: db.MySQLOptions{Address: "127.0.0.1:3306", Database: "user", Username: "root", Password: "123456"}}
    app, cleanup, err := user.NewApp(cfg)
    if err != nil { panic(err) }
    defer cleanup()
    app.Run()
}

The configuration struct and the App definition illustrate how the web framework (Gin) and the business components are embedded:

type Config struct { MySQL db.MySQLOptions `json:"mysql" yaml:"mysql"` }

type MySQLOptions struct { Address, Database, Username, Password string }

// App represents the web application
type App struct {
    *config.Config
    g  *gin.Engine
    uc *controller.UserController
}

func NewApp(cfg *config.Config) (*App, func(), error) {
    gormDB, cleanup, err := db.NewMySQL(&cfg.MySQL)
    if err != nil { return nil, nil, err }
    userStore := store.New(gormDB)
    userBiz := biz.New(userStore)
    userController := controller.New(userBiz)
    engine := gin.Default()
    app := &App{Config: cfg, g: engine, uc: userController}
    return app, cleanup, nil
}

The database helper creates a GORM instance:

// NewMySQL constructs *gorm.DB from options
func NewMySQL(opts *MySQLOptions) (*gorm.DB, func(), error) {
    cleanFunc := func() {}
    db, err := gorm.Open(mysql.Open(opts.DSN()), &gorm.Config{Logger: logger.Default.LogMode(logger.Silent)})
    return db, cleanFunc, err
}

The store layer provides a provider set that binds the UserStore interface to its concrete implementation:

var ProviderSet = wire.NewSet(New, wire.Bind(new(UserStore), new(*userStore))

type UserStore interface { Create(ctx context.Context, user *model.UserM) error }

type userStore struct { db *gorm.DB }

func New(db *gorm.DB) *userStore { return &userStore{db} }

func (u *userStore) Create(ctx context.Context, user *model.UserM) error { return u.db.Create(&user).Error }

The biz layer follows the same pattern, exposing a UserBiz interface and its implementation:

var ProviderSet = wire.NewSet(New, wire.Bind(new(UserBiz), new(*userBiz))

type UserBiz interface { Create(ctx context.Context, r *api.CreateUserRequest) error }

type userBiz struct { s store.UserStore }

func New(s store.UserStore) *userBiz { return &userBiz{s: s} }

func (b *userBiz) Create(ctx context.Context, r *api.CreateUserRequest) error {
    var userM model.UserM
    _ = copier.Copy(&userM, r)
    return b.s.Create(ctx, &userM)
}

The controller receives the business interface and handles HTTP requests:

type UserController struct { b biz.UserBiz }

func New(b biz.UserBiz) *UserController { return &UserController{b: b} }

func (ctrl *UserController) Create(c *gin.Context) {
    var r api.CreateUserRequest
    if err := c.ShouldBindJSON(&r); err != nil { c.JSON(http.StatusBadRequest, gin.H{"err": err.Error()}); return }
    if err := ctrl.b.Create(c, &r); err != nil { c.JSON(http.StatusInternalServerError, gin.H{"err": err.Error()}); return }
    c.JSON(http.StatusOK, gin.H{})
}

Routes are registered in InitRouter and the service can be exercised with the curl command provided in assets/curl.sh:

curl --location --request POST 'http://127.0.0.1:8000/users' \
--header 'Content-Type: application/json' \
--data-raw '{
    "email": "[email protected]",
    "nickname": "江湖十年",
    "username": "jianghushinian",
    "password": "pass"
}'

Wire is chosen over alternatives such as Uber’s dig and Facebook’s inject because it generates compile‑time code instead of relying on runtime reflection, resulting in better performance and a codebase that is easier to review.

The core Wire injector for this project is defined as:

func wireApp(engine *gin.Engine, cfg *config.Config, mysqlOptions *db.MySQLOptions) (*App, func(), error) {
    wire.Build(
        db.NewMySQL,
        store.ProviderSet,
        biz.ProviderSet,
        controller.New,
        wire.Struct(new(App), "*"),
    )
    return nil, nil, nil
}

Running wire generates wire_gen.go with the concrete constructor code. The CLI offers several sub‑commands: wire gen – generates code (default when no sub‑command is given). wire check – validates provider sets and reports missing dependencies. wire diff – shows a diff between the existing generated file and what would be generated. wire show – lists injectors in the package. wire commands and wire flags – list available commands and their flags.

Examples from the article illustrate the output of wire --help , wire check (showing a missing provider), and wire diff (displaying a change in the generated signature). In summary, the article provides a complete walkthrough of structuring a Go web service, defining clear interfaces, using Wire to automate dependency wiring, and leveraging the Wire CLI to maintain the generated code, thereby improving developer productivity and code quality.

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

backendarchitectureGoWeb developmentdependency-injectionWire
Go Programming World
Written by

Go Programming World

Mobile version of tech blog https://jianghushinian.cn/, covering Golang, Docker, Kubernetes and beyond.

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.