<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0"
xmlns:content="http://purl.org/rss/1.0/modules/content/"
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:slash="http://purl.org/rss/1.0/modules/slash/"
xmlns:atom="http://www.w3.org/2005/Atom"
xmlns:wfw="http://wellformedweb.org/CommentAPI/">
<channel>
<title>JRNitre&#039;s Blog - Go</title>
<link>https://blog.atoery.cn/index.php/tag/go/</link>
<atom:link href="https://blog.atoery.cn/index.php/feed/tag/go/" rel="self" type="application/rss+xml" />
<language>zh-CN</language>
<description></description>
<lastBuildDate>Tue, 07 Apr 2026 11:28:18 +0800</lastBuildDate>
<pubDate>Tue, 07 Apr 2026 11:28:18 +0800</pubDate>
<item>
<title>[编程范式] DI 依赖管理与手动依赖注入</title>
<link>https://blog.atoery.cn/index.php/2026/04/07/174.html</link>
<guid>https://blog.atoery.cn/index.php/2026/04/07/174.html</guid>
<pubDate>Tue, 07 Apr 2026 11:28:18 +0800</pubDate>
<dc:creator>JRNitre</dc:creator>
<description><![CDATA[为什么会写这篇文章在刚接触 DI 时，最容易混淆的不是语法，而是对象到底在什么时候被创建、依赖到底由谁来传。很多人之前写项目时，已经在使用“依赖注入”，只是方式是手动的。例如：在 main 中先...]]></description>
<content:encoded xml:lang="zh-CN"><![CDATA[
<h2>为什么会写这篇文章</h2><p>在刚接触 DI 时，最容易混淆的不是语法，而是对象到底在什么时候被创建、依赖到底由谁来传。</p><p>很多人之前写项目时，已经在使用“依赖注入”，只是方式是手动的。例如：</p><ul><li>在 <code>main</code> 中先创建配置对象</li><li>再创建 logger</li><li>再创建 service</li><li>最后通过 <code>NewUserSrv(...)</code>、<code>NewUserAPI(...)</code> 这类函数一层层传下去</li></ul><p>这当然也是依赖注入，但它是人工装配。</p><p>而使用 DI 容器之后，依赖的装配关系不再由 <code>main</code> 手动维护，而是交给容器在运行时完成。本文会围绕这个核心差异展开说明。</p><h2>一句话区分两种方式</h2><p>手动依赖注入：</p><ul><li>你自己创建对象</li><li>你自己把依赖传进去</li><li>你自己维护对象之间的装配顺序</li></ul><p>DI 容器管理依赖：</p><ul><li>你声明对象需要什么依赖</li><li>你把对象注册给容器</li><li>容器在运行时创建对象并填充依赖</li><li>你只需要从容器中取出装配完成的对象</li></ul><p>可以把它概括为一句话：</p><p>以前你是自己搬运依赖；现在你是声明依赖，由容器配送。</p><h2>手动依赖注入是什么</h2><p>先看最熟悉的方式。</p><p>假设有一个删除用户的功能，依赖：</p><ul><li>配置 <code>Config</code></li><li>日志 <code>Logger</code></li><li>鉴权模块 <code>AuthHandler</code></li></ul><p>手动注入的常见写法如下：</p><pre><code class="lang-go">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,
    }
}</code></pre><p>然后在 <code>main</code> 中手动组装：</p><pre><code class="lang-go">cfg := loadConfig()
logger := newLogger(cfg)
authHandler := NewAuthHandler(cfg, logger)
userController := NewUserController(cfg, logger, authHandler)

engine.DELETE(&quot;/api/user/delete&quot;, userController.Delete)</code></pre><p>这就是手动依赖注入。</p><h3>它的问题在哪里</h3><p>这种方式在小项目里完全可行，但随着模块变多，会有几个问题：</p><ul><li><code>main</code> 必须知道所有对象的内部依赖关系</li><li>创建顺序必须手动维护</li><li>任何对象新增依赖，启动层都要跟着改</li><li><code>NewXxx(...)</code> 和参数传递链会越来越长</li></ul><p>这时启动层就不再只是“启动程序”，而变成了“手工装配整个系统”。</p><h2>DI 容器管理依赖是什么</h2><p>使用 DI 容器时，思路会变成：</p><ol><li>定义一个对象</li><li>在对象上声明它需要哪些依赖</li><li>把这个对象注册到容器</li><li>容器在运行时创建并注入依赖</li><li>使用方只负责获取这个对象</li></ol><p>例如：</p><pre><code class="lang-go">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;)
}</code></pre><p>这里最关键的变化是：</p><ul><li><code>UserController</code> 不再通过 <code>NewUserController(...)</code> 接收依赖</li><li>它只声明自己需要什么</li><li>具体依赖由容器负责填充</li></ul><p>然后在容器里注册：</p><pre><code class="lang-go">func registerControllerBeans() error {
    if _, err := di.RegisterBean(
        &quot;userController&quot;,
        reflect.TypeOf((*controller.UserController)(nil)),
    ); err != nil {
        return err
    }

    return nil
}</code></pre><p>如果 <code>authHandler</code>、<code>appConfig</code>、<code>appLogger</code> 也都已注册，那么当容器初始化时，<code>UserController</code> 就能被完整装配。</p><p>最后在使用处获取：</p><pre><code class="lang-go">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)</code></pre><p>这里没有手动创建 <code>Config</code>、<code>Logger</code>、<code>Auth</code> 并塞进 <code>UserController</code>。容器已经做完了这件事。</p><h2>最容易误解的地方</h2><p>很多人第一次接触 DI 时，会下意识地觉得：</p><ul><li>既然函数要用到依赖</li><li>那么总得先创建一个 <code>deps</code> 实例</li><li>然后把它作为参数传给下一层</li></ul><p>这个想法来自手动依赖注入的经验，本身没有错，但它不适用于容器式 DI 的核心模型。</p><p>在 DI 中，你写下来的 struct 更像是一个“对象模板”或者“依赖声明模板”：</p><pre><code class="lang-go">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;`
}</code></pre><p>你在写代码时，并没有真的创建这个对象。</p><p>真正的对象创建发生在程序运行时：</p><ul><li>容器读取注册信息</li><li>容器创建 <code>UserController</code> 实例</li><li>容器按 <code>di.inject</code> 标签填充字段</li><li>最后把这个完整对象交给你</li></ul><p>所以真正的关键不是“我该怎么手动造一个 <code>deps</code>”，而是：</p><pre><code class="lang-go">userControllerInstance, err := di.GetInstanceSafe(&quot;userController&quot;)
userController := userControllerInstance.(*controller.UserController)</code></pre><p>这两行的意义是：</p><ul><li>从容器中取出已经装配完成的 <code>userController</code></li><li>将返回的 <code>interface{}</code> 断言成具体类型</li><li>后续直接调用这个对象的方法</li></ul><p>这里不是类型断言在“智能填充依赖”，而是容器在此之前已经完成了填充；类型断言只是为了让 Go 知道这个对象的具体类型。</p><h2>为什么说依赖像对象的成员变量</h2><p>从 OOP 的角度看，DI 注入后的依赖非常像一个对象的成员变量。</p><p>例如：</p><pre><code class="lang-go">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;)
}</code></pre><p>在方法内部，<code>Config</code>、<code>Logger</code>、<code>Auth</code> 的使用体验和普通成员变量完全一样。</p><p>差别只在于：</p><ul><li>普通对象的成员变量通常由你手动赋值</li><li>DI 对象的成员变量由容器在运行时自动填充</li></ul><p>因此可以这样理解：</p><ul><li>方法只依赖自己的成员变量</li><li>DI 容器负责提前把这些成员变量填好</li><li>你拿到对象后，直接调用它的方法即可</li></ul><h2><code>main</code> 中的 <code>runtimeDeps</code> 和控制器 Bean 有什么区别</h2><p>这是另一个很容易混淆的问题。</p><p>在当前项目里，<code>main</code> 中有这样一层：</p><pre><code class="lang-go">type runtimeDeps struct {
    logger         *slog.Logger
    logHandler     *logx.Handler
    webHandler     *web.Handler
    testController *controller.TestController
}</code></pre><p>然后通过类似这样的函数集中获取：</p><pre><code class="lang-go">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
}</code></pre><p>这和 <code>userController := di.GetInstanceSafe(&quot;userController&quot;)</code> 看起来很像，但角色不同。</p><h3><code>runtimeDeps</code> 是启动层的手动聚合</h3><p>它的作用是：</p><ul><li>把 <code>main</code> 启动流程直接要用到的几个顶层 Bean 聚合起来</li><li>让 <code>run()</code> 只面对一个统一的依赖入口</li></ul><p>这是一种启动层便利封装，不是 DI 自动注入 <code>run()</code>。</p><p>换句话说：</p><ul><li>DI 负责把 <code>logger</code>、<code>webHandler</code>、<code>testController</code> 都准备好</li><li><code>loadRuntimeDeps()</code> 再把这些“已经注入完成的对象”聚合成一个 struct</li></ul><h3><code>userController</code> 是容器直接交付的业务对象</h3><p>而 <code>userController</code> 不一样。</p><p>它是一个真正的消费者 Bean。容器会直接为它完成内部依赖注入。你拿到它时，它已经是完整对象：</p><pre><code class="lang-go">userControllerInstance, err := di.GetInstanceSafe(&quot;userController&quot;)
userController := userControllerInstance.(*controller.UserController)</code></pre><p>所以这两者的本质区别是：</p><ul><li><code>runtimeDeps</code>：启动层手动聚合多个已注入对象</li><li><code>userController</code>：容器直接交付的已注入对象</li></ul><h2>是否还需要单独定义 <code>UserDeps</code></h2><p>很多人会想写成这样：</p><pre><code class="lang-go">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) {
}</code></pre><p>这不是完全不行，但通常不推荐。</p><p>更推荐的写法是：</p><pre><code class="lang-go">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) {
}</code></pre><p>原因很简单：</p><ul><li>路由最终绑定的是一个对象的方法</li><li>对象本身就是依赖载体</li><li>没必要再人为拆一个 <code>UserDeps</code></li></ul><p>因此在容器式 DI 中，更自然的组织方式是：</p><ul><li>用对象表达业务角色，例如 <code>UserController</code></li><li>用对象字段表达它的依赖</li><li>用对象方法表达它的行为</li></ul><h2>一个完整的运作流程</h2><p>假设现在有接口 <code>/api/user/delete</code>，它需要：</p><ul><li>配置</li><li>日志</li><li>鉴权模块</li></ul><p>推荐的实现路径如下。</p><h3>第一步：定义依赖消费者</h3><p>文件：<code>internal/controller/user.go</code></p><pre><code class="lang-go">package 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;,
    })
}</code></pre><h3>第二步：注册 Bean</h3><p>文件：<code>internal/container/controller.go</code></p><pre><code class="lang-go">func registerControllerBeans() error {
    if _, err := di.RegisterBean(
        &quot;userController&quot;,
        reflect.TypeOf((*controller.UserController)(nil)),
    ); err != nil {
        return err
    }

    return nil
}</code></pre><p>文件：<code>internal/container/auth.go</code></p><pre><code class="lang-go">func registerAuthBeans() error {
    if _, err := di.RegisterBean(
        &quot;authHandler&quot;,
        reflect.TypeOf((*auth.Handler)(nil)),
    ); err != nil {
        return err
    }

    return nil
}</code></pre><h3>第三步：初始化容器</h3><p>文件：<code>cmd/main.go</code></p><pre><code class="lang-go">if err := container.Init(); err != nil {
    return err
}</code></pre><h3>第四步：获取顶层对象</h3><pre><code class="lang-go">userControllerInstance, err := di.GetInstanceSafe(&quot;userController&quot;)
if err != nil {
    return err
}

userController := userControllerInstance.(*controller.UserController)</code></pre><h3>第五步：绑定到 API</h3><pre><code class="lang-go">engine.DELETE(&quot;/api/user/delete&quot;, userController.Delete)</code></pre><p>到这里，依赖链就已经打通了。</p><p>注意：<code>main</code> 不需要手动这样做：</p><pre><code class="lang-go">cfg := ...
logger := ...
authHandler := ...
userController := ...</code></pre><p>这些装配工作已经交给 DI 容器处理。</p><h2>应该由谁获取依赖</h2><p>这是实践中最重要的边界之一。</p><h3><code>main</code> 应该获取什么</h3><p><code>main</code> 只应该获取“启动流程要直接使用的顶层对象”，例如：</p><ul><li><code>webHandler</code></li><li><code>testController</code></li><li><code>userController</code></li><li><code>appLogger</code></li></ul><h3><code>main</code> 不应该获取什么</h3><p><code>main</code> 不应该为了组装某个控制器，再单独去拿它的内部依赖，例如：</p><ul><li><code>appConfig</code></li><li><code>authHandler</code></li><li><code>dbService</code></li></ul><p>然后自己拼成一个 <code>UserController</code>。</p><p>如果 <code>main</code> 开始知道 <code>UserController</code> 依赖什么、怎么创建，那就说明启动层又回到了手动装配模式。</p><h2>DI 与手动注入的关系</h2><p>DI 不是对手动注入的否定，而是对手动注入的进一步抽象。</p><p>两者的关系可以这样理解：</p><ul><li>手动依赖注入也是依赖注入</li><li>DI 容器管理依赖是“由容器执行的依赖注入”</li><li>两者目标相同，都是让对象依赖外部提供，而不是自己在内部硬编码创建</li><li>两者差别在于：由谁来负责装配</li></ul><p>所以如果非要用一句最严谨的话概括：</p><p>DI 容器并没有消灭依赖注入，它只是把“注入动作”从开发者手里接管过去。</p><h2>最后总结</h2><p>如果只保留最核心的结论，那么有下面几条：</p><ol><li>手动依赖注入与 DI 容器管理依赖的本质差别，不在于“有没有注入”，而在于“谁来装配”。</li><li>在 DI 模式中，你写下的 struct 更像是对象模板；真正的实例是在程序运行时由容器创建的。</li><li>依赖字段可以理解为对象的成员变量，方法只依赖自己的成员变量工作。</li><li><code>di.GetInstanceSafe(&quot;userController&quot;)</code> 的意义是“取出已装配完成的对象”，不是“现场组装对象”。</li><li><code>main</code> 只获取顶层对象，不手动拼装它们的内部依赖。</li><li><code>runtimeDeps</code> 是启动层的手动聚合；<code>userController</code> 是容器直接交付的业务对象，这两者不要混淆。</li></ol><p>当你真正接受“我写的是依赖声明，容器负责运行时装配”这个前提之后，DI 的理解就会突然变得清晰很多。</p>
]]></content:encoded>
<slash:comments>0</slash:comments>
<comments>https://blog.atoery.cn/index.php/2026/04/07/174.html#comments</comments>
<wfw:commentRss>https://blog.atoery.cn/index.php/feed/tag/go/</wfw:commentRss>
</item>
<item>
<title>软件工程中的显式依赖与隐式依赖</title>
<link>https://blog.atoery.cn/index.php/2025/11/26/171.html</link>
<guid>https://blog.atoery.cn/index.php/2025/11/26/171.html</guid>
<pubDate>Wed, 26 Nov 2025 16:58:18 +0800</pubDate>
<dc:creator>JRNitre</dc:creator>
<description><![CDATA[0.0 前言许多项目在开发初期，为了快速实现功能、看到效果从而在编码的过程中加入了大量的隐式依赖；如果不对其处理在编码工作进行到后期时可能遇到：难以测试、调试、代码架构升级等，从而造出一大坨屎山...]]></description>
<content:encoded xml:lang="zh-CN"><![CDATA[
<h1>0.0 前言</h1><p>许多项目在开发初期，为了快速实现功能、看到效果从而在编码的过程中加入了大量的<strong>隐式依赖</strong>；如果不对其处理在编码工作进行到后期时可能遇到：难以测试、调试、代码架构升级等，从而造出一大坨屎山。</p><p>大量隐式依赖的存在导致后期对代码结构进行修改时极其困难，比如你请了一位大厨来到家里为你做饭，来之前大厨用电话跟你沟通，说是需要你准备面粉，鸡蛋。。。等等一大堆“依赖”。结果厨师到你家开始做饭了，做一半突然告诉你：哎呀！我家祖传的大铁锅没拿啊！没这口锅我不会做饭了！没办法还得让人家跑回家拿锅；这时想到为什么你不打电话的时候就告诉我你有“铁锅”这一依赖啊？总不能明天我让你给我做炒菜的时候再告诉我祖传的锅铲没拿吧。</p><p>所以隐式依赖对项目开发过程中可能导致没法编写单元测试，耦合度高等问题所在。</p><h1>1.0 隐式依赖</h1><p>我们给出一段 Go 的业务代码，显而易见的标出了隐式依赖的位置：</p><pre><code>// 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)
        // ...
    }
}</code></pre><p>这样的代码存在很多问题：</p><ul><li>不可测试</li></ul><p>单元测试时无法替换 GetUserSrv()，必须启动真实数据库，导致测试慢、不稳定。</p><ul><li>行为不可控</li></ul><p>测试无法模拟“用户已存在”“网络超时”等异常场景。</p><ul><li>耦合度高</li></ul><p>Handler 与具体 Service 实现强绑定，违反“依赖倒置原则”。</p><ul><li>文档缺失</li></ul><p>从函数签名无法得知其真实依赖，增加理解成本。</p><blockquote>这类代码常被称为“黑盒”——你知道输入，但不知道它背后偷偷用了什么。</blockquote><h1>2.0 显式依赖</h1><p>现在我们对上述有问题的代码进行改进：</p><pre><code>// 定义接口契约
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)))</code></pre><p>这样修改后，对其编写单元测试，不依赖后续 service 层提供的函数，不需要启动实际的数据库，这种设计不仅提升了代码的可测试性，更增强了系统的弹性。</p><h1>3.0 常见隐式依赖陷阱及规避策略</h1><table><thead><tr><th align="center">隐式依赖形式</th><th align="center">风险</th><th align="center">显式变化方案</th></tr></thead><tbody><tr><td align="center">全局变量(<code>var db *sql.DB</code>)</td><td align="center">测试污染、并发问题</td><td align="center">通过构造函数注入</td></tr><tr><td align="center">单例函数(<code>service.GetInstance()</code>)</td><td align="center">无法 mock</td><td align="center">改为接口 + 工厂函数</td></tr><tr><td align="center"><code>time.Now()</code></td><td align="center">时间不可控</td><td align="center">注入<code>func() time.Time</code></td></tr><tr><td align="center"><code>os.Getenv(&quot;DB_URL&quot;)</code></td><td align="center">配置硬编码</td><td align="center">抽象为<code>Config</code>接口</td></tr><tr><td align="center">http.Get(...)</td><td align="center">网络不可控</td><td align="center">封装为 <code>HTTPClient</code> 接口</td></tr></tbody></table><blockquote>黄金法则：任何外部依赖都应通过接口抽象，并由上层注入。</blockquote>
]]></content:encoded>
<slash:comments>0</slash:comments>
<comments>https://blog.atoery.cn/index.php/2025/11/26/171.html#comments</comments>
<wfw:commentRss>https://blog.atoery.cn/index.php/feed/tag/go/</wfw:commentRss>
</item>
</channel>
</rss>