请教 golang 依赖注入的实际问题

2025 年 8 月 12 日
 chaleaochexist

我有一个 task 通过 ssh 运行 N 种命令, 假设 ls -lcat /etc/host吧. 然后把输出存起来.

理想中的结构是:

task 依赖 handler 依赖 sshclient

其中,

handler 是函数, 参数是 sshclient, 一个 handler 执行一种命令

sshclient 是接口, 干活的.

这个 sshclient 实例化过程只能在 task 中动态生成, 因为 sshclient 需要的 ip 是在 task 中的其他函数获取的.

我得问题:

  1. task 依赖 handler. 但是 handler 的参数的类型(也就是 sshclient) 定义在 task 中, 这不循环导入了吗?

目前的解决方案是, 我在 handler 和 task 中分别定义两个一模一样的接口, 然后通过适配的方式能让代码运行. 我不确定这样处理是否合理? 还是说我这个设计本身就有问题? 通过注入接口能实现吗?

  1. 当一个接口的实现的依赖是动态数据的时候, (譬如 sshclient 中的 ip, 端口, 认证信息), 还需要注入吗? 如何注入? 我目前采用的方案是注入一个无参的返回值是工厂函数的函数...然后再 task 中实例化 sshclient. 补充: sshclient 可能有 100 个, 不是一个固定的 client. 说白了我是通过 ssh 采集信息的. 和 *db.DB 不是一个类型.

目前有点混乱, 如果我没问清楚欢迎各位大佬提出你的疑问 我尽量补充信息.

3465 次点击
所在节点    Go 编程语言
44 条回复
sunny352787
2025 年 8 月 12 日
参数类型为啥定义在 task ?不是应该定义在 handler 吗?
Dorathea
2025 年 8 月 12 日
"因为 sshclient 需要的 ip 是在 task 中的其他函数获取的"
能否将这个依赖剥离出来, 作为 sshclient 的依赖呢? 如果 task 也需要这个, 让 task 也依赖就好了.

以及"循环导入" 的问题, 我之前接触过 nestjs, 也好奇过循环依赖如何解决
https://docs.nestjs.com/fundamentals/circular-dependency
或许可以另一种思路参考
NessajCN
2025 年 8 月 12 日
「这个 sshclient 实例化过程只能在 task 中动态生成, 因为 sshclient 需要的 ip 是在 task 中的其他函数获取的.」

没看懂这前后两句话的因果

你建一个 sshclient 的实例,然后在 task 的时候调不就行了。把 ip 当作参数传进去
譬如

sc := NewSshClient()

func task(client *SshClient) {
ip := GetIp()
client.connect(ip)
}

task(&sc)
chaleaochexist
2025 年 8 月 12 日
@NessajCN 因为注入嘛, 我在注入的时候(也就是初始化的时候) 是不知道 ip 的.

我现在的代码和你的例子差不多, 只不过不是用的 connect 而是注入了一个无参的函数, 这个函数返回一个工厂函数, 这个工厂函数返回 sshclient.

如果 sshclient 的 ip 是静态的类似 db.DB, 直接定义在 handler 和 task 的公共底层. 注入即可, 就没有这么多麻烦事了.
sthwrong
2025 年 8 月 12 日
task 没有 sshclient 的依赖,依赖是在 handler 中,task 只需要提供参数给 handler 就行了,task 依赖一个工厂函数或者工厂接口就行。
NessajCN
2025 年 8 月 12 日
@chaleaochexist 你直接发代码吧,我怀疑你提了个 xy 问题,
也许你的原始需求有更直接简单的解决法而不用注入来注入去
iseki
2025 年 8 月 12 日
要不要在这里叠加 factory 看你的倾向和你使用的 di 框架的能力啊。有的时候工厂是个很简单的解决方案,特别是你想更精确地控制生命周期时。
iseki
2025 年 8 月 12 日
我虽然不是很确信你的业务,不过如果按照我对你业务的猜测,这里使用 factory pattern 是正确的。
Sendya
2025 年 8 月 12 日
我感觉直接 interface ,然后套娃引用就行了

https://go.dev/play/p/sa9s8QpKE29
kfpenn
2025 年 8 月 12 日
我也有这种嵌套的依赖,最后是用 interface 解决的
darksword21
2025 年 8 月 12 日
op 最好能给出现在的伪代码或者哪个循环引用的伪代码
everhythm
2025 年 8 月 12 日
lz 不如画图说明下设想的流程和调用逻辑,这里 handler 和 task 名称和职责感觉不清楚

假设是 handler 生成 task ,task 调用 sshclient 干活

1. handler 没有直接管 sshclient
2. 没有看懂
bli22ard
2025 年 8 月 12 日
type CommandRunner interface {
exec(cmd string) (ret string, err error)
}
chaleaochexist
2025 年 8 月 13 日
@everhythm
@darksword21
@Sendya
@iseki
@NessajCN
@sthwrong
@sunny352787
@Dorathea
@NessajCN

谢谢你们的回复, 我写了一个 伪代码, 希望可以把问题解释清楚.

https://github.com/chaleaoch/golang_demo.git

一共是三个版本 v1, v2, v3 我把问题都放到了注释中, 可以搜索关键字 "问题" . 各个版本之间的主要差别, 可以搜索关键字 "修改"

如果我得代码有其他的地方的写法上的问题, 随意批评指导, 谢谢你们. 谢谢!!
NessajCN
2025 年 8 月 13 日
@chaleaochexist 你完全搞错 interface 的用法了,
go 的 interface 跟 jvav 的不是一回事,
你定义了一大堆完全永不上的 interface

go 的 interface 可以类比 python 的 protocol 或 rust 的 trait,
是为了方便你写「非特定类型参数」的函数,或者早期的泛型来用的。
具体的用法是你定义一个 struct 和 interface , 并给 struct 实现 interface 里的函数,
之后你定义的参数为该 interface 的函数就可以直接传这个 struct 的实例了

我的建议还是如 3# 时候讲的,你初始化就初始化 client 然后传引用进 task 函数就好,初始化时候也根本不需要知道 ip

type SSHClient struct {}
func (s *SSHClient) ExecuteCommand(user string, pass string, host string, cmd string) {
// blablabla
}

// 初始化 sshclient 放到 main 函数里, 然后传给下面的 task

func (q *DemoTask) Run(sshClient *SSHClient) {
mIps, _ := q.mIPRepo.GetByType("exampleType") // 这一条也可以大幅简化
for _, mIp := range mIps {
out, _ := sshClient.ExecuteCommand(mIp.Username, mIp.Password, mIp.Ip+":"+mIp.Port, "exampleCommand")
fmt.Println(out)
}
}
chaleaochexist
2025 年 8 月 13 日
@NessajCN 像你这么写, 最大的问题是如何 mock?

无论是 GetByType 是查询数据库的, 如果不做成接口, 如何 mock
ssh 也是, 在单元测试的时候, 我不希望真的去 ssh 执行一条命令.
NessajCN
2025 年 8 月 13 日
@chaleaochexist 就像我上一个回复内容提到的,你先要弄明白 go 的 interface 究竟是啥。
先把 jvav 思维彻底舍弃才好跟你讲下一步
chaleaochexist
2025 年 8 月 13 日
无论是 --> ~~无论是~~
chaleaochexist
2025 年 8 月 13 日
@NessajCN 好吧...其实我根本就不会 java...
golang 如果不通过 interface 没法 mock.

或者 sshClient 这个接口 可以有多个实现. 我希望注入而不是将 NewsshClient1 改成 NewsshClient2
NessajCN
2025 年 8 月 13 日
@chaleaochexist 都说了接口不是这么用.....
sshClient 显然是个实例,咋会是接口呢。
go 的 interface 是在定义函数时候作为「类泛型」传参用的,你的 sshClient 必然是要初始化然后调方法的,
概念风马牛不相及呀

这是一个专为移动设备优化的页面(即为了让你能够在 Google 搜索结果里秒开这个页面),如果你希望参与 V2EX 社区的讨论,你可以继续到 V2EX 上打开本讨论主题的完整版本。

https://v2ex.ih06.com/t/1151824

V2EX 是创意工作者们的社区,是一个分享自己正在做的有趣事物、交流想法,可以遇见新朋友甚至新机会的地方。

V2EX is a community of developers, designers and creative people.

© 2021 V2EX