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(&req); err != nil {
            // ...
            return
        }

        // ⚠️ 隐式依赖:直接调用全局单例
        svc := service.GetUserSrv()
        resp, err := svc.UserRegister(ctx.Request.Context(), &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(&req); err != nil {
            // ...
            return
        }

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

// 生产环境注册
router.POST("/register", MakeUserRegisterHandler(service.NewUserSrv(db)))

这样修改后,对其编写单元测试,不依赖后续 service 层提供的函数,不需要启动实际的数据库,这种设计不仅提升了代码的可测试性,更增强了系统的弹性。

3.0 常见隐式依赖陷阱及规避策略

隐式依赖形式风险显式变化方案
全局变量(var db *sql.DB)测试污染、并发问题通过构造函数注入
单例函数(service.GetInstance())无法 mock改为接口 + 工厂函数
time.Now()时间不可控注入func() time.Time
os.Getenv("DB_URL")配置硬编码抽象为Config接口
http.Get(...)网络不可控封装为 HTTPClient 接口
黄金法则:任何外部依赖都应通过接口抽象,并由上层注入。