2619 字
13 分钟

这篇文章本来是两个月前的草稿。不过好像后来忘掉了,现在重新写了一遍

用 Typeless 辅助写的,受朋友推荐了下,还确实挺好用的,可惜还找不到足够好用的开源平替,以后写作都用这个了

众所周知,Golang 项目的代码结构都比较趋向按模块划分,所以也会不可避免地遇到循环依赖的问题,当然,解决循环依赖的手段实际上有很多,其中一种被认为比较优雅的处理方式就是使用依赖倒置,也就是抽出来一个接口,以及很多文章都会推荐你这么去做

但实际上,这种方式并不适用于所有场景,甚至在某些更偏向传统业务后端的项目里,大多数出现循环依赖的场景都不太适合用依赖倒置来解决。而循环依赖时到底什么场景应该用依赖倒置,什么场景不应该用呢

简单来说,不同层级的模块间的循环依赖,可以按照依赖倒置来解决,而同层级模块之间的循环依赖,并不推荐靠依赖倒置来解决。什么是同层什么是不同层呢,诸如两个业务模块user和order,这就是同层级(至少通常来说是这样的),而一个业务模块user和一个基础设施模块比如消息队列,那就是不同层级(当然通常来说消息队列可能被划分为生产者与消费者两个模块,这里只是为了方便理解就归同一个模块了)。当然层级其实要结合实际业务需求来理解并划分,只不过通常在传统业务后端里默认都是这么划分,已经成常识了,但要是放到中间件或者一些轮子项目里可能就不太一样了。以下讨论所举的例子均基于传统web业务后端来理解

同层级模块产生循环依赖用依赖倒置会发生些什么#

首先,需要提的一点是,通常来说,模块内部划分的 handler、service 和 repo 层之间,使用了依赖注入方法

从代码层面来讲,比如,我们有一个模块 user 和一个模块 order,User 里面有一个方法 U 调用了 Order 模块,然后 Order 模块里有一个方法 O 调用了 User 模块,然后产生了循环依赖。那么这个时候,用依赖倒置是在 user 模块里面定义一个接口,然后 order 模块实现这个接口,然后,代码层面的循环依赖好像就被解决了。然而哪怕仅从代码层面讲,真的解决了吗?

// user 模块
package user
type Service struct {
order *order.Service // 依赖 order 模块
}
func (s *Service) U() {
s.order.O()
}
// order 模块
package order
type Service struct {
user *user.Service // 依赖 user 模块
}
func (s *Service) O() {
s.user.U()
}

两个包互相 import,Go 直接编译不过:

import cycle not allowed
import "example/user"
import "example/order"

如果改用依赖倒置,各自定义对方需要的能力抽象:

// user 模块:定义 OrderAPI 接口
package user
type OrderAPI interface {
O()
}
type Service struct {
order OrderAPI // 只依赖抽象,不再依赖 order 包
}
func NewService(o OrderAPI) *Service {
return &Service{order: o}
}
func (s *Service) U() {
s.order.O()
}
// order 模块:定义 UserAPI 接口
package order
type UserAPI interface {
U()
}
type Service struct {
user UserAPI // 只依赖抽象,不再依赖 user 包
}
func NewService(u UserAPI) *Service {
return &Service{user: u}
}
func (s *Service) O() {
s.user.U()
}

通常来说,模块里面会采用依赖注入的方法,这也是比较普遍的。好,如果你真的是这么去做了,你就会遇到一个问题,在最上层依赖注入的时候,产生了死锁,然后就根本没法实现依赖倒置。

// 组合根 main.go:无论先创建哪个,另一个都还没就绪
func main() {
userSvc := user.NewService(orderSvc) // orderSvc 还没创建
orderSvc := order.NewService(userSvc) // userSvc 还没创建
}

这个时候你可能想去解决这个问题,于是去翻文档或者问 AI,你可能会得到一些解决方法,其中一个叫懒加载,另一个叫属性注入。确实,这两个方法确实能解决这个问题,但是实际上只是在打补丁。而且我从个人观点出发,并不推荐在 Golang 的项目里面这么写,这其实是一种非常非常 Java 的写法。

// 属性注入:不通过构造函数注入,先把对象建出来再补上依赖
func main() {
userSvc := &user.Service{} // Order 依赖先空着
orderSvc := &order.Service{User: userSvc}
userSvc.Order = orderSvc // 事后补上,绕开注入死锁
}
// 懒加载:用到的时候才去获取依赖
package user
type Service struct {
orderOnce sync.Once
order OrderAPI
}
func (s *Service) getOrder() OrderAPI {
s.orderOnce.Do(func() {
s.order = order.GetInstance()
})
return s.order
}

而且最终你发现这么做,只不过是把代码层面的显式循环依赖改成了隐式循环依赖罢了哈哈。。。

那么好,所以问题出在哪 为什么这么一个优雅的方法,用的时候竟遭如此挫折

那么,首先我们要弄明白,依赖倒置它本身到底是拿来干什么的

依赖倒置(又名依赖反转)#

以下是维基百科的解释:

在面向对象编程领域中,依赖反转原则(Dependency inversion principle, DIP)是指一种特定的解耦(传统的依赖关系建立在高层次上,而具体的策略设置则应用在低层次的模块上)形式,使得高层次的模块不依赖于低层次的模块的实现细节,依赖关系被颠倒(反转),从而使得低层次模块依赖于高层次模块的需求抽象。

该原则规定:

1.高层次的模块不应该依赖于低层次的模块,两者都应该依赖于抽象接口。 2.抽象接口不应该依赖于具体实现。而具体实现则应该依赖于抽象接口。 该原则颠倒了一部分人对于面向对象设计的认识方式。如高层次和低层次对象都应该依赖于相同的抽象接口。

也就是说,依赖倒置本来就应该被用在不同层次的模块之间用来解耦,依赖倒置不应该被当成解决pkg之间的循环依赖的一种solution

比如说我们有一个任务队列模块(这里为方便解释仍然当做一个模块)和业务里的task模块,Task 模块调用了任务队列模块的 Enqueue方法,任务作业模块调用了 Task 模块的更新任务状态方法,然后循环依赖了。那么,这个时候我们就应该用依赖倒置,因为 Task 模块本来不应该依赖于任务队列模块的具体实现。而且基础设施模块不会像那些业务模块一样采用几乎相同的依赖注入方式,所以不太会有前面的死锁问题

// task 模块
package task
type TaskQueue interface {
Enqueue(taskID string) error
}
type TaskService struct {
queue TaskQueue
}
func NewTaskService(q TaskQueue) *TaskService {
return &TaskService{queue: q}
}
func (s *TaskService) Submit(taskID string) error {
return s.queue.Enqueue(taskID)
}
func (s *TaskService) UpdateStatus(taskID string, status string) error {
// 更新任务状态
return nil
}
// taskqueue 模块
package taskqueue
type TaskUpdater interface {
UpdateStatus(taskID string, status string) error
}
type TaskQueue struct {
updater TaskUpdater
}
func NewTaskQueue(u TaskUpdater) *TaskQueue {
return &TaskQueue{updater: u}
}
func (q *TaskQueue) Enqueue(taskID string) error {
// ...投递到消息队列
return q.updater.UpdateStatus(taskID, "running")
}
// 组合根负责装配:双方的具体实现各自满足对方的抽象
func main() {
taskSvc := task.NewTaskService(nil) // 先创建,便于后续注入
queue := taskqueue.NewTaskQueue(taskSvc) // task.TaskService 实现了 TaskUpdater
taskSvc = task.NewTaskService(queue) // taskqueue.TaskQueue 实现了 TaskQueue
}

于是我们就实现了高层次模块对低层次模块的解耦,只是在 Golang 的语法与代码里表现出来,循环依赖的错误被解决了

所以循环依赖意味着什么呢?

循环依赖#

产生循环依赖,意味着你的模块设计本来就有问题,只是对于不同层次的模块出现的设计问题,恰好可以用依赖倒置来解决,而对于同层次模块出现的模块设计问题,依赖倒置则无法较好地解决。

Golang 的循环依赖错误应当被视为一种信号,而不是错误。只不过,它在 Golang 的语法里确实是错误的。

所以,最终应当归因于模块设计的问题,去更好地划分模块,而不是寻找一种解决方法去解决循环依赖的语法问题。要解决的不是循环依赖,而是要解决模块设计问题。

当然,解决模块设计问题就有很多方法了,诸如合并、抽公共层,上面讲到的依赖倒置,再到后面可以用事件驱动等等层层递进地解决

总结#

循环依赖本身并不是一个单纯的代码层面问题,它更多反映的是模块之间职责划分和依赖关系是否合理

依赖倒置确实是解决循环依赖的一种优秀手段,但它解决的前提是:两个模块之间存在明确的层级关系,高层模块不应该依赖低层模块的具体实现,而应该依赖抽象能力。在这种场景下,依赖倒置不仅解决了循环依赖,也让模块之间的边界更加清晰

但是,如果两个模块本身处于同一业务层级,例如 userorder 这种业务模块之间产生循环调用,那么强行引入接口、懒加载或者属性注入,往往只是绕过了语言层面的限制,并没有真正解决模块设计上的问题,甚至可能让原本简单的业务关系变得更加复杂

换句话说,循环依赖不是一个需要被消灭的语法错误,而是架构设计中的一个反馈信号

好的设计不是找到一种技巧让循环依赖消失,而是在出现循环依赖时,重新审视模块之间的关系,让依赖方向符合业务本身的逻辑

243 字
1 分钟
2026-07-30
无标签

7月下旬回了杭州,然后在advx期间和几位好友面基了。说到这,advx给我们几个真心要做产品的倒是全给刷了呵,当然申得晚也确实是

前天时候又从杭州回来,杭州夏天的天气实在是难受了。然后昨晚Veno说打Deadlock,然后发现拿来打游戏那台win本电源没带回来🥲,pd没撑过半小时然后歇菜了,好那估计暑假玩不了啥了,新买电源还不如来回一趟杭州划算,这一个月试试mac开vm推推旮旯得了

八月,八月接下来猜是会很忙,好像都堆到这个点上了

然后本来想计划九月初去上海今年的Kubecon China玩玩见见世面,然后偶然一看学校安排,时间刚好又和短学期的答辩重合了呵呵,好那希望到时候我能battle的过吧

1071 字
5 分钟
2026-07-23
无标签

目前的想法。 嗯,在旭哥这至少会待上比较长的一段时间,至少如果是去小厂做非核心业务的产出是肯定不如我在这边写后端学到的多,毕竟也是正儿八经的商业项目,而且对我来说很好的一点是这边后端业务也不是传统的那种枯燥的web后端业务,而是会偏infra一些的计算平台,场景也非常切合用go后端的场景。然后也都是认识的人,也有工资拿,除此以外也在平常交流里学到了很多非常重要的东西,诸如旭哥之前强调的工程师一定要在干中学,这半年来算是深有体会,我想我现在上来学任何工程领域的新东西都不会上来就去看任何课程的,哪怕课程确实很优质,比如最早碰云原生这块的时候看过一点阿里云的云原生公开课来着,但是能指望就靠这个学明白云原生学明白kubernetes吗,我想从运维上得真在集群上玩过k8s,从开发上得真在云环境里接触服务发现和治理之类的问题,再比如DDD的设计思想,如果是去知乎上找篇精美的万字长文阅读,读完之后大概率还是p都不会。总之在这边接触了真实的业务、真实的生产场景前后,很多对于后端、对于技术的认识跟之前比都是非常非常不同的,我想对我来说是一个非常难得的机遇吧,以及包括为什么要参与开源,因为确实能接触到真实的生产环境里的很多的框架级问题,尤其对于学生来说,总之真实的业务场景是非常能锻炼人的,之前想过觉得按那套所谓的后端范式去写web项目其实是没什么成长的,单体web也好,微服务也好,如果仅仅是作为个人项目去写是没有多少收获得,更多还是为了包装简历,而从提升实际的水平来考虑其实应当去多造轮子,因为轮子对个人来说是有真实的业务场景在的。写同样一个后端,假装自己有十万流量和真的有十万流量的场景还是有很大不同的

计划会是可能在明年三四月去找互联网的后端实习,嗯其实最好是能做云相关的业务。但也只是计划,我无法预料未来的我会如何思考,以及未来会发生什么不一样的事,几个月前Veno老师还打算去实习来着,结果转念一想就去尝试做创业产品了。这段时间来,校内校外有幸见识到了很多不同的人,旭哥也是从高薪工作里跳出来创业的,也有见到从大厂出来做自媒体的,诸如此类的,所以,自己也有了很多不一样的思考吧,记得很早很早以前和某位朋友闲聊说之后就是做项目、然后实习然后投大厂,然后朋友说这样生活也太无聊了吧,唉现在想来确实不无道理,很多时候我不免怀疑这仍是一种学生思维在影响自己,总想按照一种既有的路线来达成某种特定结果,那么一是既有路径是可复制的,必然会挤满了人与竞争,二是这条固定路线带来的生活,未必是自己所喜欢的。想到旭哥之前所说的,既然他之前工作的时候时薪有一千(笑),那么对他来说为什么不每天工作两小时,🤗好吧当然对我来说我很显然肯定没有这个能力,也没有在大厂工作过,没什么资格来评价这个事,只是想说见识到更多人之后,对我来说在某些生活态度上,有了更多思考吧