← 返回文章列表

控制反转和依赖注入

理解控制反转与依赖注入,以及它们在 Go 项目中的作用。

go

控制反转(Inversion of Control, IoC)是面向对象编程中的一种设计原则,用来降低计算机代码之间的耦合程度,控制反转最常见的方式就是依赖注入(Dependency Injection, DI),Wire 就是一个常见的依赖注入工具。

基本概念

控制反转本质是一种把控制权交出去的设计思想:以前是你在代码里自己 new 一个对象、自己决定它什么时候创建、用谁当依赖、怎么组装调用链;IoC 之后,这些“创建 + 组装 + 调度”的控制权交给外部(框架/容器/上层),你的业务代码只描述“我需要什么、我提供什么”,不负责把一切拼起来,能大大降低代码的耦合程度。

依赖注入是 IoC 最常见的方式之一,当 A 需要 B 的时候,不需要A 自己创建,直接通过外部创建好,通过参数传递进来,通过方法字段进行注入,DI 将依赖关系表现得更加显式。

Wire 是 Go 里的编译期依赖注入代码生成工具。它的用途是把你声明的 provider(也就是构造函数)和 injector(也就是装配入口函数)组合起来,自动生成一段等价于你手写 new 然后逐层传参组装的初始化代码。你先定义一个 ProviderSet,比如 ProviderSet = wire.NewSet(NewData, NewRepo, NewUsecase, NewService 等),再在 wireApp 这种 injector 函数里调用 wire.Build 来表达你最终想得到的对象,比如我要一个 kratos.App。Wire 在生成阶段会根据这些构造函数的类型签名推导依赖关系,计算出正确的创建顺序,检查依赖是否缺失以及是否存在循环依赖,最后生成 wire_gen.go,在里面给出真正可运行的 wireApp 实现,同时把需要释放资源的 cleanup 函数也拼出来。运行时不需要容器,也不需要反射,本质上就是把装配代码自动写出来,让初始化过程更清晰、更安全、更容易维护。

为什么要降低耦合呢?

增加可维护性,防止牵一发动全身

增加可测试性,测试位置更加精准,只跑逻辑本身,更加符合现在设计的理念

增加可替换性,降低代码迭代成本,可能只需要替换一个参数就能实现数据库的替换,不需要进行大改

降低耦合也能提升团队的协作效率,每个人只需要关心自己的一部分。

增加项目可靠性,这样某个部分故障,不容易发生扩散,更容易找出问题。

控制反转

控制反转的常见方法

  • 构造器注入:把依赖作为构造函数参数传入,依赖关系最显式,Go 最推荐。

  • 方法/Setter 注入:通过 setter 或初始化方法注入,适合可选依赖或运行时替换。

  • 字段/属性注入:框架通过反射/标签把依赖塞进结构体字段,写起来省事但依赖隐藏、可测试性差。

  • 接口 + 工厂(Factory):通过接口隔离具体实现,再用工厂负责创建实现对象,常用于“按配置选择实现”。

  • 抽象工厂/构建器(Abstract Factory / Builder):当对象组装很复杂、需要分步骤或多套组合方案时用。

  • 服务定位器(Service Locator):对象自己从“容器”里按 key/类型取依赖(也是 IoC),但依赖不透明,一般不推荐做主方案。

  • 插件化/注册表(Plugin / Registry):实现通过注册机制挂到 registry,运行时按名称/能力发现与加载。

  • 函数式 Options(Functional Options):Go 常见,用 WithXxx() 注入可选依赖或配置,适合参数很多的构造。

  • Context/请求级注入(Request-scoped Injection):每个请求携带依赖(如 trace/span、用户信息、事务对象),常见于 web 框架中间件链。

  • 编译期装配(Codegen Wiring):依赖在编译期被解析并生成装配代码,运行时无容器/无反射(Wire、Dagger 这类)。

依赖注入

依赖注入常见工具

Go

  • 编译期:Google Wire

  • 运行时容器:Uber dig、sarulabs/di、(较老) facebookgo/inject

  • 应用框架化生命周期:Uber fx(基于 dig,带 OnStart/OnStop)

  • 其他思路:手写 main 装配、Functional Options(不算工具但很常用)

Java / JVM

  • Spring / Spring Boot(运行时容器,注解驱动)

  • Guice(运行时容器)

  • Dagger(编译期为主,Android 常见)

  • Micronaut(偏编译期/减少反射)

.NET

  • Microsoft.Extensions.DependencyInjection(官方内置 DI)

  • Autofac、Castle Windsor(第三方容器)

Node.js / TypeScript

  • NestJS(框架自带 DI)

  • InversifyJS、tsyringe、typedi(容器/注入库)

Python

  • dependency-injector(容器)

  • FastAPI Depends(更偏“依赖解析/函数注入”,request-scoped 很强)

  • Injector(Guice 风格)

Rust / C++(更少“容器化”,更多手动/编译期)

  • Rust:更常见是手动装配 + trait 抽象(DI 工具相对少)

  • C++:Boost.DI(编译期)

Wire

Go社区中有很多依赖注入框架。比如:Uber的dig和Facebook的inject都使用反射来做运行时依赖注入。

wire是一个的 Google 开源的依赖注入工具,通过自动生成代码的方式在编译期完成依赖注入。

wire 中有两个核心概念:提供者(provider)和注入器(injector)。

Provider

provider 通常是一个 New 开头的构造函数,表示实现了某一个结构体,也就是创建了一个对象

package demo

import (
    "context"
    "errors"
)

type A struct {
    Value int
}

func NewA(ctx context.Context, b B) (A, error) {
        if b.Value == 0 {
                return A{}, errors.New("cannot provide a when value is zero")
        }
        return A{Value: b.Value + 2}, nil
}

提供者函数可以分组为提供者函数集(provider set)。使用wire.NewSet 函数可以将多个提供者函数添加到一个集合中。如果经常同时使用多个提供者函数,这非常有用。

package demo

import (
    // ...
    "github.com/google/wire"
)

var ProviderSet = wire.NewSet(NewData, NewRepo, NewUsecase, NewService)

同时,还可以将其他提供者函数集添加到提供者函数集中。

Injector

应用程序中是用一个注入器来连接提供者,注入器就是一个按照依赖顺序调用提供者。

要声明一个注入器函数只需要在函数体中调用wire.Build。这个函数的返回值也无关紧要,只要它们的类型正确即可。这些值在生成的代码中将被忽略。

func wireApp(*conf.Server, *conf.Data, log.Helper) (*kratos.App, func(), error) {
    panic(wire.Build(server.ProviderSet, data.ProviderSet, biz.ProviderSet, service.ProviderSet, newApp))
}

wire的安装和使用

在 golang 中使用 install 安装依赖包,通过在注射器同级目录下使用 wire 指令即可完成自动依赖注入

go install github.com/google/wire/cmd/wire@latest

cd cmd/golist

wire

通过上面例子会生成一个 wire_gen.go文件,此时依赖注入完成。

// Code generated by Wire. DO NOT EDIT.

//go:generate go run -mod=mod github.com/google/wire/cmd/wire
//go:build !wireinject
// +build !wireinject

package main

import (
    "golist/internal/biz"
    "golist/internal/conf"
    "golist/internal/data"
    "golist/internal/server"
    "golist/internal/service"

    "github.com/go-kratos/kratos/v2"
    "github.com/go-kratos/kratos/v2/log"

    _ "go.uber.org/automaxprocs"
)

// Injectors from wire.go:

// wireApp init kratos application.
func wireApp(confServer *conf.Server, confData *conf.Data, logger log.Logger) (*kratos.App, func(), error) {
    db, err := data.NewDB(confData)
    if err != nil {
        return nil, nil, err
    }
    dataData, cleanup, err := data.NewData(db, logger)
    if err != nil {
        return nil, nil, err
    }
    todoRepo := data.NewTodoRepo(dataData, logger)
    todoUsecase := biz.NewTodoUsecase(todoRepo, logger)
    todoService := service.NewTodoService(todoUsecase)
    grpcServer := server.NewGRPCServer(confServer, todoService, logger)
    httpServer := server.NewHTTPServer(confServer, todoService, logger)
    app := newApp(logger, grpcServer, httpServer)
    return app, func() {
        cleanup()
    }, nil
}

参考资料

依赖注入工具-wire

Wire 依赖注入

ChatGPT老师

关于这篇文章的日期

旧日期 2025-01-16 与文章发布记录不符,现按旧站可确认的首次发布日 2026-02-13 整理。实际创作日期尚无法确认。