<?xml version="1.0" encoding="UTF-8"?>
<rdf:RDF
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns="http://purl.org/rss/1.0/"
xmlns:dc="http://purl.org/dc/elements/1.1/">
<channel rdf:about="https://blog.atoery.cn/index.php/feed/rss/tag/go/">
<title>JRNitre&#039;s Blog - Go</title>
<link>https://blog.atoery.cn/index.php/tag/go/</link>
<description></description>
<items>
<rdf:Seq>
<rdf:li resource="https://blog.atoery.cn/index.php/2026/04/07/174.html"/>
<rdf:li resource="https://blog.atoery.cn/index.php/2025/11/26/171.html"/>
</rdf:Seq>
</items>
</channel>
<item rdf:about="https://blog.atoery.cn/index.php/2026/04/07/174.html">
<title>[编程范式] DI 依赖管理与手动依赖注入</title>
<link>https://blog.atoery.cn/index.php/2026/04/07/174.html</link>
<dc:date>2026-04-07T11:28:18+08:00</dc:date>
<description>为什么会写这篇文章在刚接触 DI 时，最容易混淆的不是语法，而是对象到底在什么时候被创建、依赖到底由谁来传。很多人之前写项目时，已经在使用“依赖注入”，只是方式是手动的。例如：在 main 中先创建配置对象再创建 logger再创建 service最后通过 NewUserSrv(...)、NewUserAPI(...) 这类函数一层层传下去这当然也是依赖注入，但它是人工装配。而使用 DI 容器之后，依赖的装配关系不再由 main 手动维护，而是交给容器在运行时完成。本文会围绕这个核心差异展开说明。一句话区分两种方式手动依赖注入：你自己创建对象你自己把依赖传进去你自己维护对象之间的装配顺序DI 容器管理依赖：你声明对象需要什么依赖你把对象注册给容器容器在运行时创建对象并填充依赖你只需要从容器中取出装配完成的对象可以把它概括为一句话：以前你是自己搬运依赖；现在你是声明依赖，由容器配送。手动依赖注入是什么先看最熟悉的方式。假设有一个删除用户的功能，依赖：配置 Config日志 Logger鉴权模块 AuthHandler手动注入的常见写法如下：type AuthHandler struct {
    Config *modelconfig.Config
    Logger *slog.Logger
}

func NewAuthHandler(cfg *modelconfig.Config, logger *slog.Logger) *AuthHandler {
    return &amp;AuthHandler{
        Config: cfg,
        Logger: logger,
    }
}

type UserController struct {
    Config *modelconfig.Config
    Logger *slog.Logger
    Auth   *AuthHandler
}

func NewUserController(
    cfg *modelconfig.Config,
    logger *slog.Logger,
    auth *AuthHandler,
) *UserController {
    return &amp;UserController{
        Config: cfg,
        Logger: logger,
        Auth:   auth,
    }
}然后在 main 中手动组装：cfg := loadConfig()
logger := newLogger(cfg)
authHandler := NewAuthHandler(cfg, logger)
userController := NewUserController(cfg, logger, authHandler)

engine.DELETE(&quot;/api/user/delete&quot;, userController.Delete)这就是手动依赖注入。它的问题在哪里这种方式在小项目里完全可行，但随着模块变多，会有几个问题：main 必须知道所有对象的内部依赖关系创建顺序必须手动维护任何对象新增依赖，启动层都要跟着改NewXxx(...) 和参数传递链会越来越长这时启动层就不再只是“启动程序”，而变成了“手工装配整个系统”。DI 容器管理依赖是什么使用 DI 容器时，思路会变成：定义一个对象在对象上声明它需要哪些依赖把这个对象注册到容器容器在运行时创建并注入依赖使用方只负责获取这个对象例如：type UserController struct {
    Config *modelconfig.Config `di.inject:&quot;appConfig&quot;`
    Logger *slog.Logger        `di.inject:&quot;appLogger&quot;`
    Auth   *auth.Handler       `di.inject:&quot;authHandler&quot;`
}

func (c *UserController) Delete(ctx *gin.Context) {
    c.Logger.Info(&quot;delete user request received&quot;)
}这里最关键的变化是：UserController 不再通过 NewUserController(...) 接收依赖它只声明自己需要什么具体依赖由容器负责填充然后在容器里注册：func registerControllerBeans() error {
    if _, err := di.RegisterBean(
        &quot;userController&quot;,
        reflect.TypeOf((*controller.UserController)(nil)),
    ); err != nil {
        return err
    }

    return nil
}如果 authHandler、appConfig、appLogger 也都已注册，那么当容器初始化时，UserController 就能被完整装配。最后在使用处获取：userControllerInstance, err := di.GetInstanceSafe(&quot;userController&quot;)
if err != nil {
    return err
}

userController := userControllerInstance.(*controller.UserController)
engine.DELETE(&quot;/api/user/delete&quot;, userController.Delete)这里没有手动创建 Config、Logger、Auth 并塞进 UserController。容器已经做完了这件事。最容易误解的地方很多人第一次接触 DI 时，会下意识地觉得：既然函数要用到依赖那么总得先创建一个 deps 实例然后把它作为参数传给下一层这个想法来自手动依赖注入的经验，本身没有错，但它不适用于容器式 DI 的核心模型。在 DI 中，你写下来的 struct 更像是一个“对象模板”或者“依赖声明模板”：type UserController struct {
    Config *modelconfig.Config `di.inject:&quot;appConfig&quot;`
    Logger *slog.Logger        `di.inject:&quot;appLogger&quot;`
    Auth   *auth.Handler       `di.inject:&quot;authHandler&quot;`
}你在写代码时，并没有真的创建这个对象。真正的对象创建发生在程序运行时：容器读取注册信息容器创建 UserController 实例容器按 di.inject 标签填充字段最后把这个完整对象交给你所以真正的关键不是“我该怎么手动造一个 deps”，而是：userControllerInstance, err := di.GetInstanceSafe(&quot;userController&quot;)
userController := userControllerInstance.(*controller.UserController)这两行的意义是：从容器中取出已经装配完成的 userController将返回的 interface{} 断言成具体类型后续直接调用这个对象的方法这里不是类型断言在“智能填充依赖”，而是容器在此之前已经完成了填充；类型断言只是为了让 Go 知道这个对象的具体类型。为什么说依赖像对象的成员变量从 OOP 的角度看，DI 注入后的依赖非常像一个对象的成员变量。例如：type UserController struct {
    Config *modelconfig.Config `di.inject:&quot;appConfig&quot;`
    Logger *slog.Logger        `di.inject:&quot;appLogger&quot;`
    Auth   *auth.Handler       `di.inject:&quot;authHandler&quot;`
}

func (c *UserController) Delete(ctx *gin.Context) {
    c.Logger.Info(&quot;start delete user&quot;)
}在方法内部，Config、Logger、Auth 的使用体验和普通成员变量完全一样。差别只在于：普通对象的成员变量通常由你手动赋值DI 对象的成员变量由容器在运行时自动填充因此可以这样理解：方法只依赖自己的成员变量DI 容器负责提前把这些成员变量填好你拿到对象后，直接调用它的方法即可main 中的 runtimeDeps 和控制器 Bean 有什么区别这是另一个很容易混淆的问题。在当前项目里，main 中有这样一层：type runtimeDeps struct {
    logger         *slog.Logger
    logHandler     *logx.Handler
    webHandler     *web.Handler
    testController *controller.TestController
}然后通过类似这样的函数集中获取：func loadRuntimeDeps() (*runtimeDeps, error) {
    loggerInstance, err := di.GetInstanceSafe(&quot;appLogger&quot;)
    if err != nil {
        return nil, err
    }

    webHandlerInstance, err := di.GetInstanceSafe(&quot;webHandler&quot;)
    if err != nil {
        return nil, err
    }

    return &amp;runtimeDeps{
        logger:     loggerInstance.(*slog.Logger),
        webHandler: webHandlerInstance.(*web.Handler),
    }, nil
}这和 userController := di.GetInstanceSafe(&quot;userController&quot;) 看起来很像，但角色不同。runtimeDeps 是启动层的手动聚合它的作用是：把 main 启动流程直接要用到的几个顶层 Bean 聚合起来让 run() 只面对一个统一的依赖入口这是一种启动层便利封装，不是 DI 自动注入 run()。换句话说：DI 负责把 logger、webHandler、testController 都准备好loadRuntimeDeps() 再把这些“已经注入完成的对象”聚合成一个 structuserController 是容器直接交付的业务对象而 userController 不一样。它是一个真正的消费者 Bean。容器会直接为它完成内部依赖注入。你拿到它时，它已经是完整对象：userControllerInstance, err := di.GetInstanceSafe(&quot;userController&quot;)
userController := userControllerInstance.(*controller.UserController)所以这两者的本质区别是：runtimeDeps：启动层手动聚合多个已注入对象userController：容器直接交付的已注入对象是否还需要单独定义 UserDeps很多人会想写成这样：type UserDeps struct {
    Config *modelconfig.Config `di.inject:&quot;appConfig&quot;`
    Logger *slog.Logger        `di.inject:&quot;appLogger&quot;`
    Auth   *auth.Handler       `di.inject:&quot;authHandler&quot;`
}

func Delete(deps UserDeps) {
}这不是完全不行，但通常不推荐。更推荐的写法是：type UserController struct {
    Config *modelconfig.Config `di.inject:&quot;appConfig&quot;`
    Logger *slog.Logger        `di.inject:&quot;appLogger&quot;`
    Auth   *auth.Handler       `di.inject:&quot;authHandler&quot;`
}

func (c *UserController) Delete(ctx *gin.Context) {
}原因很简单：路由最终绑定的是一个对象的方法对象本身就是依赖载体没必要再人为拆一个 UserDeps因此在容器式 DI 中，更自然的组织方式是：用对象表达业务角色，例如 UserController用对象字段表达它的依赖用对象方法表达它的行为一个完整的运作流程假设现在有接口 /api/user/delete，它需要：配置日志鉴权模块推荐的实现路径如下。第一步：定义依赖消费者文件：internal/controller/user.gopackage controller

import (
    &quot;log/slog&quot;

    &quot;github.com/gin-gonic/gin&quot;

    &quot;github.com/jrnitre/blackshores/internal/auth&quot;
    modelconfig &quot;github.com/jrnitre/blackshores/model/config&quot;
)

type UserController struct {
    Config *modelconfig.Config `di.inject:&quot;appConfig&quot;`
    Logger *slog.Logger        `di.inject:&quot;appLogger&quot;`
    Auth   *auth.Handler       `di.inject:&quot;authHandler&quot;`
}

func (c *UserController) Delete(ctx *gin.Context) {
    c.Logger.Info(&quot;delete user request received&quot;)
    ctx.JSON(200, gin.H{
        &quot;message&quot;: &quot;ok&quot;,
    })
}第二步：注册 Bean文件：internal/container/controller.gofunc registerControllerBeans() error {
    if _, err := di.RegisterBean(
        &quot;userController&quot;,
        reflect.TypeOf((*controller.UserController)(nil)),
    ); err != nil {
        return err
    }

    return nil
}文件：internal/container/auth.gofunc registerAuthBeans() error {
    if _, err := di.RegisterBean(
        &quot;authHandler&quot;,
        reflect.TypeOf((*auth.Handler)(nil)),
    ); err != nil {
        return err
    }

    return nil
}第三步：初始化容器文件：cmd/main.goif err := container.Init(); err != nil {
    return err
}第四步：获取顶层对象userControllerInstance, err := di.GetInstanceSafe(&quot;userController&quot;)
if err != nil {
    return err
}

userController := userControllerInstance.(*controller.UserController)第五步：绑定到 APIengine.DELETE(&quot;/api/user/delete&quot;, userController.Delete)到这里，依赖链就已经打通了。注意：main 不需要手动这样做：cfg := ...
logger := ...
authHandler := ...
userController := ...这些装配工作已经交给 DI 容器处理。应该由谁获取依赖这是实践中最重要的边界之一。main 应该获取什么main 只应该获取“启动流程要直接使用的顶层对象”，例如：webHandlertestControlleruserControllerappLoggermain 不应该获取什么main 不应该为了组装某个控制器，再单独去拿它的内部依赖，例如：appConfigauthHandlerdbService然后自己拼成一个 UserController。如果 main 开始知道 UserController 依赖什么、怎么创建，那就说明启动层又回到了手动装配模式。DI 与手动注入的关系DI 不是对手动注入的否定，而是对手动注入的进一步抽象。两者的关系可以这样理解：手动依赖注入也是依赖注入DI 容器管理依赖是“由容器执行的依赖注入”两者目标相同，都是让对象依赖外部提供，而不是自己在内部硬编码创建两者差别在于：由谁来负责装配所以如果非要用一句最严谨的话概括：DI 容器并没有消灭依赖注入，它只是把“注入动作”从开发者手里接管过去。最后总结如果只保留最核心的结论，那么有下面几条：手动依赖注入与 DI 容器管理依赖的本质差别，不在于“有没有注入”，而在于“谁来装配”。在 DI 模式中，你写下的 struct 更像是对象模板；真正的实例是在程序运行时由容器创建的。依赖字段可以理解为对象的成员变量，方法只依赖自己的成员变量工作。di.GetInstanceSafe(&quot;userController&quot;) 的意义是“取出已装配完成的对象”，不是“现场组装对象”。main 只获取顶层对象，不手动拼装它们的内部依赖。runtimeDeps 是启动层的手动聚合；userController 是容器直接交付的业务对象，这两者不要混淆。当你真正接受“我写的是依赖声明，容器负责运行时装配”这个前提之后，DI 的理解就会突然变得清晰很多。</description>
</item>
<item rdf:about="https://blog.atoery.cn/index.php/2025/11/26/171.html">
<title>软件工程中的显式依赖与隐式依赖</title>
<link>https://blog.atoery.cn/index.php/2025/11/26/171.html</link>
<dc:date>2025-11-26T16:58:18+08:00</dc:date>
<description>0.0 前言许多项目在开发初期，为了快速实现功能、看到效果从而在编码的过程中加入了大量的隐式依赖；如果不对其处理在编码工作进行到后期时可能遇到：难以测试、调试、代码架构升级等，从而造出一大坨屎山。大量隐式依赖的存在导致后期对代码结构进行修改时极其困难，比如你请了一位大厨来到家里为你做饭，来之前大厨用电话跟你沟通，说是需要你准备面粉，鸡蛋。。。等等一大堆“依赖”。结果厨师到你家开始做饭了，做一半突然告诉你：哎呀！我家祖传的大铁锅没拿啊！没这口锅我不会做饭了！没办法还得让人家跑回家拿锅；这时想到为什么你不打电话的时候就告诉我你有“铁锅”这一依赖啊？总不能明天我让你给我做炒菜的时候再告诉我祖传的锅铲没拿吧。所以隐式依赖对项目开发过程中可能导致没法编写单元测试，耦合度高等问题所在。1.0 隐式依赖我们给出一段 Go 的业务代码，显而易见的标出了隐式依赖的位置：// api/user.go
func UserRegister() gin.HandlerFunc {
    return func(ctx *gin.Context) {
        var req types.UserRegisterReq
        if err := ctx.ShouldBindJSON(&amp;req); err != nil {
            // ...
            return
        }

        // ⚠️ 隐式依赖：直接调用全局单例
        svc := service.GetUserSrv()
        resp, err := svc.UserRegister(ctx.Request.Context(), &amp;req)
        // ...
    }
}这样的代码存在很多问题：不可测试单元测试时无法替换 GetUserSrv()，必须启动真实数据库，导致测试慢、不稳定。行为不可控测试无法模拟“用户已存在”“网络超时”等异常场景。耦合度高Handler 与具体 Service 实现强绑定，违反“依赖倒置原则”。文档缺失从函数签名无法得知其真实依赖，增加理解成本。这类代码常被称为“黑盒”——你知道输入，但不知道它背后偷偷用了什么。2.0 显式依赖现在我们对上述有问题的代码进行改进：// 定义接口契约
type UserRegisterService interface {
    UserRegister(context.Context, *types.UserRegisterReq) (*UserResponse, error)
}

// Handler 工厂函数：依赖通过参数注入
func MakeUserRegisterHandler(svc UserRegisterService) gin.HandlerFunc {
    return func(ctx *gin.Context) {
        var req types.UserRegisterReq
        if err := ctx.ShouldBindJSON(&amp;req); err != nil {
            // ...
            return
        }

        // ✅ 显式使用注入的依赖
        resp, err := svc.UserRegister(ctx.Request.Context(), &amp;req)
        // ...
    }
}

// 生产环境注册
router.POST(&quot;/register&quot;, MakeUserRegisterHandler(service.NewUserSrv(db)))这样修改后，对其编写单元测试，不依赖后续 service 层提供的函数，不需要启动实际的数据库，这种设计不仅提升了代码的可测试性，更增强了系统的弹性。3.0 常见隐式依赖陷阱及规避策略隐式依赖形式风险显式变化方案全局变量(var db *sql.DB)测试污染、并发问题通过构造函数注入单例函数(service.GetInstance())无法 mock改为接口 + 工厂函数time.Now()时间不可控注入func() time.Timeos.Getenv(&quot;DB_URL&quot;)配置硬编码抽象为Config接口http.Get(...)网络不可控封装为 HTTPClient 接口黄金法则：任何外部依赖都应通过接口抽象，并由上层注入。</description>
</item>
</rdf:RDF>