V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
The Go Programming Language
http://golang.org/
Go Playground
Go Projects
Revel Web Framework
chaleaochexist
V2EX  ›  Go 编程语言

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

  •  
  •   chaleaochexist · 23 天前 · 2204 次点击

    我有一个 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 不是一个类型.

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

    44 条回复    2025-08-14 12:50:05 +08:00
    sunny352787
        1
    sunny352787  
       23 天前   ❤️ 1
    参数类型为啥定义在 task ?不是应该定义在 handler 吗?
    Dorathea
        2
    Dorathea  
       23 天前   ❤️ 1
    "因为 sshclient 需要的 ip 是在 task 中的其他函数获取的"
    能否将这个依赖剥离出来, 作为 sshclient 的依赖呢? 如果 task 也需要这个, 让 task 也依赖就好了.

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

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

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

    sc := NewSshClient()

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

    task(&sc)
    chaleaochexist
        4
    chaleaochexist  
    OP
       23 天前
    @NessajCN 因为注入嘛, 我在注入的时候(也就是初始化的时候) 是不知道 ip 的.

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

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

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

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

    1. handler 没有直接管 sshclient
    2. 没有看懂
    bli22ard
        13
    bli22ard  
       23 天前   ❤️ 1
    type CommandRunner interface {
    exec(cmd string) (ret string, err error)
    }
    chaleaochexist
        14
    chaleaochexist  
    OP
       22 天前
    @everhythm
    @darksword21
    @Sendya
    @iseki
    @NessajCN
    @sthwrong
    @sunny352787
    @Dorathea
    @NessajCN

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

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

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

    如果我得代码有其他的地方的写法上的问题, 随意批评指导, 谢谢你们. 谢谢!!
    NessajCN
        15
    NessajCN  
       22 天前
    @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
        16
    chaleaochexist  
    OP
       22 天前
    @NessajCN 像你这么写, 最大的问题是如何 mock?

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

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

    #3 的例子中 client.connect(ip) 的用法和 NewSshClient(ip) 没有本质区别.
    实际上我第一版就是类似 client.connect(ip)的写法.

    "sshClient 显然是个实例,咋会是接口呢。" 在我 github 的例子中 sshClient 是接口
    https://github.com/chaleaoch/golang_demo/blob/master/v1/internal/task/a.go#L22

    "都说了接口不是这么用."
    接口实现了鸭子类型.
    然后你说我得哪个接口用的不对是指 provider 吗? 通过 provider.GetFactory 拿到了一个 sshClient 的工厂函数, 然后再实例化这个 sshclient 是不对的, 应该直接传 sshclient 然后 sshclient.connect(ip) 是这个意思吗? 我没觉得这俩有本质区别啊.

    但是我得例子中的问题不在这里, 而是引入了 handler 之后, 在哪里 定义接口的问题.
    chaleaochexist
        22
    chaleaochexist  
    OP
       22 天前
    @NessajCN 不过还是谢谢你耐心和我讨论问题... 希望你能和我继续 讨论.
    chaleaochexist
        23
    chaleaochexist  
    OP
       22 天前
    实际上我第一版 --> 在我提供的 github 仓库中没有体现, 是我之前在工作中的第一版.
    chaleaochexist
        24
    chaleaochexist  
    OP
       22 天前
    @NessajCN 别着急大佬 等我再写一个版本 和你一起讨论.
    chaleaochexist
        25
    chaleaochexist  
    OP
       22 天前
    @NessajCN 我猜 https://github.com/chaleaoch/golang_demo/tree/master/v4
    这个文件夹下的代码 比较符合你的品味吧?

    但是我得问题是, 如果我想注入 handler? 要如何做?
    答案是不是: handler 就不应该注入 而是直接调用?

    实际上我也是这么做的, 但是我需要你们 确认一下!!! 这回 我说明白了吗?
    NessajCN
        26
    NessajCN  
       22 天前
    @chaleaochexist
    「在我 github 的例子中 sshClient 是接口」
    所以我一直说你理解错 go 里 interface 的用法了呀....orz
    「但是我得例子中的问题不在这里, 而是引入了 handler 之后, 在哪里 定义接口的问题.」
    你这句话问得就错了,接口的定义仅仅是简单的
    type If interface {
    func1()
    func2()
    }
    你想表达的是这里面的 func1() func2() 具体怎么定义对吧?
    那不叫定义接口,而是给某个 struct 实现 (implement) 。
    你的 handler 里这种写法

    func Cmd1Handler(sshClient SSHClient) string {
    out, _ := sshClient.ExecuteCommand("cmd1")
    // 干点别的...100 行
    return out
    }

    func Cmd2Handler(sshClient SSHClient) string {
    out, _ := sshClient.ExecuteCommand("cmd2")
    // 干点别的...100 行
    return out
    }

    意图显然是把 sshClient 作为一个实例而不是接口参数
    所以我提到你需要把 SSHClient 定义成 struct 而不是 interface
    然后这么定义相应的 handler 方法
    func (sc *SSHClient) CmdHandler(cmd Command) string {}
    chaleaochexist
        27
    chaleaochexist  
    OP
       22 天前
    @NessajCN 要么是我没说清楚, 要么是你没仔细看我得代码.

    sshClient 是接口, 因为我现在的实现是 standardSshClient 将来可能基于第三方库去实现这个 sshclient. 所以 sshclient 需要定义成一个接口. 这是原因 1. 原因 2 如果 sshclient 是一个具体的结构体实现, 那么将来如何 mock? 如何做单元测试? 单元测试的时候, 需要脱机测试.

    [意图显然是把 sshClient 作为一个实例而不是接口参数]
    func Cmd1Handler(sshClient SSHClient) string {
    的意图就是接口, 而不是实例.

    原因是喜闻乐见的, Accept interfaces,return structs

    至于为什么 CmdHandler 是一个函数而不是 sshclient 的方法. 是因为 除了我前面说的 sshclient 是一个接口还有一个原因是, 实际上情况比 demo 要复杂一点点除了 sshclient 还有 httpclient,
    client 只负责执行, 而不考虑业务, CmdHandler 是带业务的, 就是我说的, 省略 100 行的内容.

    ==============================分隔
    我得最后一个问题, 大佬帮忙看一下 v4 是否符合逻辑呢?
    NessajCN
        28
    NessajCN  
       22 天前   ❤️ 1
    @chaleaochexist
    合逻辑呀
    虽然要我来 code review 的话 SSHClient 这些接口定义都不需要只留 StandardSSH 就行了
    不过你说除了 StandardSSH 之外还有其他 struct 需要传给 CmdHandler 那确实可以用 interface
    这样的话你需要把 Connect(username string, password string, host string) error 加到 SSHClient 的接口定义里
    chaleaochexist
        29
    chaleaochexist  
    OP
       22 天前
    @NessajCN 你平时肯定不做单元测试. 我确定.
    NessajCN
        30
    NessajCN  
       22 天前
    @chaleaochexist
    SSHClient 是 struct 就不能写脱机测试了又是咋得出的结论呀...
    chaleaochexist
        31
    chaleaochexist  
    OP
       22 天前
    @NessajCN 没办法模拟, 反正我不会...
    NessajCN
        32
    NessajCN  
       22 天前
    @chaleaochexist
    写测试更用不到 interface 了
    哪怕你代码里用 interface 作为参数定义函数
    实际测试里也肯定是传的实现了那个接口的 struct
    不理解你因为需要单元测验而必须定义 interface 的原理
    chaleaochexist
        33
    chaleaochexist  
    OP
       22 天前
    @NessajCN 你写个测试就知道了.
    譬如 repo 访问数据库吧 现在需要你 在没有数据库的情况下测试 service 层的函数

    如何 mock 假数据.
    只有接口可以做到.

    你传入一个结构体 结构体的依赖是 db.DB 他可是真的通过 TCP 去连数据库.

    但是如果是接口 我就可以做一个假的 struct 去实现这个接口, 然后返回假数据就行了.

    总之一句话 你尝试给你的 repo 层 写单元测试就明白了.
    sthwrong
        34
    sthwrong  
       22 天前
    依赖就分错了,provider 依赖 repo , 提供方法返回 clients ,handler 依赖 clients ,提供方法根据传入 clients 和 cmd 构建单个或者多个 handler ,task 依赖 handler ,提供方法执行 handler ,每个实现自己声明一个接口 。依赖清晰了,每一层都可以在 test 中声明新的 mock 实例实现 mock 方法替代调用。
    NessajCN
        35
    NessajCN  
       22 天前
    @chaleaochexist

    fakeData := DbData{}
    FunctionToBeTested(fakeData)
    这里头有接口啥事啊.....
    你的意思是 DbData{} 这个数据结构体初始化的时候必须连数据库?
    sthwrong
        36
    sthwrong  
       22 天前
    每层依赖做好的话,单测可以这样 mock 调用
    chaleaochexist
        37
    chaleaochexist  
    OP
       22 天前
    @NessajCN 啊? 我没跟上.. 啥意思?

    我得意思是
    ```

    type Repo struct {
    db *sql.DB
    }

    func (r *Repo) GetUserByID(userID int) (*User, error) {
    // ...
    }

    type UserService struct {
    repo Repo
    }

    func NewService(repo Repo) *UserService {
    return &UserService{repo: repo}
    }

    func (s *UserService) FindUserByID(userID int) (*User, error) {
    user, err := s.repo.GetUserByID(userID)
    if err != nil {
    return nil, fmt.Errorf("service error: failed to find user with ID %d: %w", userID, err)
    }
    // 在这里可以添加业务逻辑
    return user, nil
    }

    ```

    现在的要求是 1. 针对 FindUserByID 做单元测试. 2. 没有数据库 要求 mock 假数据.
    你试试吧. 不算为难你吧.


    我得问题是: 第一步 你需要实例化 NewService 你传什么参数进去? 你传一个真的, 那一定有一个真正的 db 连接, 传一个假的 编译失败.
    chaleaochexist
        38
    chaleaochexist  
    OP
       22 天前
    @sthwrong 明白你的意思 你把 repo 放 provider 里了 就迎刃而解了. 这样就不需要 动态初始化了是吧.

    逻辑上是的, 但是不符合业务逻辑吖...
    NessajCN
        39
    NessajCN  
       22 天前 via Android
    @chaleaochexist 你这么写就没打算让人能单独传假数据测啊…


    // 在这里添加业务逻辑

    这部分单独做成函数,把 user 当参数传进去,然后做 user 的假数据就行了
    sthwrong
        40
    sthwrong  
       22 天前   ❤️ 1
    @chaleaochexist #38 动态数据本身就是来自 repo 啊,生成动态结果就依赖 repo 给数据,明显的依赖关系。依赖层级对了,每层都可以决定是否需要 interface ,想 mock 的都可以,调用层依赖这个 interface ,test 的时候自己实现个 mock 实例传进去。
    sthwrong
        41
    sthwrong  
       22 天前   ❤️ 1
    @chaleaochexist #38 就算不是来自 repo ,如果来自其他模块的方法,让 provider 依赖这个模块的方法,如果需要方法中的客户端数据可以被 mock ,就给这给方法的实例定义一个接口,test 的时候传一个自定义的实现了方法的实例。我的 main 方法直接调用的原始实现,test 里面大多数都给了 mock 实现,所以打印的结果才又 mock 信息。
    chaleaochexist
        42
    chaleaochexist  
    OP
       22 天前
    @NessajCN 我没话讲了大佬, 后端 这么做不是太正常了吗?
    一个 service 有多少个 io 操作啊 这都是最简单的了.
    chaleaochexist
        43
    chaleaochexist  
    OP
       22 天前
    @sthwrong 虽然你说的有瑕疵, 但是给我提供了新思路,我明天试一下. 谢谢佬友!!!
    chaleaochexist
        44
    chaleaochexist  
    OP
       21 天前
    @sthwrong #34
    "依赖就分错了,provider 依赖 repo , 提供方法返回 clients ,handler 依赖 clients ,提供方法根据传入 clients 和 cmd 构建单个或者多个 handler ,task 依赖 handler ,提供方法执行 handler ,每个实现自己声明一个接口 。依赖清晰了,每一层都可以在 test 中声明新的 mock 实例实现 mock 方法替代调用。"

    提供方法返回 clients 中的 clients 是接口还是结构体, 接口的话定义在哪里?
    针对这个问题 我又 发了个帖子
    https://v2ex.com/t/1152334#reply0

    大佬感兴趣的话, 点拨一二..
    关于   ·   帮助文档   ·   自助推广系统   ·   博客   ·   API   ·   FAQ   ·   实用小工具   ·   947 人在线   最高记录 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 37ms · UTC 20:29 · PVG 04:29 · LAX 13:29 · JFK 16:29
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.