在golang中是否有更好的依赖注入模式?

62

鉴于此代码:

package main

import (
    "fmt"
)

type datstr string

type Guy interface {
   SomeDumbGuy() string
}

func (d *datstr) SomeDumbGuy() string {
  return "some guy"
}

func someConsumer(g Guy) {
  fmt.Println("Hello, " + g.SomeDumbGuy())
}

func main() {
    var d datstr
    someConsumer(&d)
}

main 中完成组件之间的布线是连接依赖关系的正确方式吗?似乎我在代码中有点过度使用它。是否有比这更好的常见模式,或者我想太多了?


4
有点基于观点的问题,但这是我个人喜欢的风格。未公开的结构体、已公开的接口,通过指针在接口中存储结构体。这使得模拟和中间件包装变得非常容易,并且比导出的结构体更符合面向对象的抽象原则。唯一的补充可能是为datastr类型添加一个工厂函数,返回一个 Guy 接口,因为这可以在编译时保证你的结构体实现了预期要实现的接口。 - Kaedys
Google的Wire看起来很有前途。有一些关于它的文章:- 使用Go Cloud的Wire进行编译时依赖注入 - 使用Wire进行Go依赖注入 - undefined
1
https://quii.gitbook.io/learn-go-with-tests/go-fundamentals/dependency-injection - user4466350
11个回答

57

最佳实践是不使用DI库。Go语言旨在成为一种简单易懂的语言。使用DI库/框架会将此过程抽象化,使得DI变得神秘


6
很遗憾我没有使用过Wire,而且也不是Golang中DI的专家。但是,我坚信不需要特定的模式,因为任何库都会使程序流程变得复杂,而Golang旨在成为一种没有任何“魔法”并且易于理解的语言。 - Zeeshan
72
如果这被归类为最佳实践,请添加一个Go doc引用以证明其有效性。考虑到Google创建了Wire作为一个避免反射并保持调用清晰度的DI系统,我认为这个答案更可能是错误的。 - Jeremy
2
没有使用 Go Doc 参考;我自己阅读了许多文章和 Effective Go。 与使用在幕后进行反射或魔法的 DI 框架相比,不使用 DI 框架更符合 Go 的习惯用法。 - Zeeshan
13
谁说的?我看到过很多使用某种依赖注入的好项目。而且,谷歌公司以其 Wire 框架也会反驳你的观点... - Dmytro Titov
3
Go语言允许你创建进行依赖注入的库。在一定程度上需要抽象化。只要遵循DRY和KISS原则即可。没有不使用XYZ库的最佳实践,除非你的团队滥用它。个人认为。 - rhzs
显示剩余7条评论

42

Wire基本上是一个编译时的工厂生成器,它处理其中的依赖关系。 - Jimbo
什么?它仍然是... - JakeRobb

12
是的,facebookgo inject库允许你获取注入的成员并为你连接图形。
代码: https://github.com/facebookgo/inject 文档: https://godoc.org/github.com/facebookgo/inject 下面是文档中的示例代码:
package main

import (
    "fmt"
    "net/http"
    "os"

    "github.com/facebookgo/inject"
)

// Our Awesome Application renders a message using two APIs in our fake
// world.
type HomePlanetRenderApp struct {
    // The tags below indicate to the inject library that these fields are
    // eligible for injection. They do not specify any options, and will
    // result in a singleton instance created for each of the APIs.

    NameAPI   *NameAPI   `inject:""`
    PlanetAPI *PlanetAPI `inject:""`
}

func (a *HomePlanetRenderApp) Render(id uint64) string {
    return fmt.Sprintf(
        "%s is from the planet %s.",
        a.NameAPI.Name(id),
        a.PlanetAPI.Planet(id),
    )
}

// Our fake Name API.
type NameAPI struct {
    // Here and below in PlanetAPI we add the tag to an interface value.
    // This value cannot automatically be created (by definition) and
    // hence must be explicitly provided to the graph.

    HTTPTransport http.RoundTripper `inject:""`
}

func (n *NameAPI) Name(id uint64) string {
    // in the real world we would use f.HTTPTransport and fetch the name
    return "Spock"
}

// Our fake Planet API.
type PlanetAPI struct {
    HTTPTransport http.RoundTripper `inject:""`
}

func (p *PlanetAPI) Planet(id uint64) string {
    // in the real world we would use f.HTTPTransport and fetch the planet
    return "Vulcan"
}

func main() {
    // Typically an application will have exactly one object graph, and
    // you will create it and use it within a main function:
    var g inject.Graph

    // We provide our graph two "seed" objects, one our empty
    // HomePlanetRenderApp instance which we're hoping to get filled out,
    // and second our DefaultTransport to satisfy our HTTPTransport
    // dependency. We have to provide the DefaultTransport because the
    // dependency is defined in terms of the http.RoundTripper interface,
    // and since it is an interface the library cannot create an instance
    // for it. Instead it will use the given DefaultTransport to satisfy
    // the dependency since it implements the interface:
    var a HomePlanetRenderApp
    err := g.Provide(
        &inject.Object{Value: &a},
        &inject.Object{Value: http.DefaultTransport},
    )
    if err != nil {
        fmt.Fprintln(os.Stderr, err)
        os.Exit(1)
    }

    // Here the Populate call is creating instances of NameAPI &
    // PlanetAPI, and setting the HTTPTransport on both to the
    // http.DefaultTransport provided above:
    if err := g.Populate(); err != nil {
        fmt.Fprintln(os.Stderr, err)
        os.Exit(1)
    }

    // There is a shorthand API for the simple case which combines the
    // three calls above is available as inject.Populate:
    //
    //   inject.Populate(&a, http.DefaultTransport)
    //
    // The above API shows the underlying API which also allows the use of
    // named instances for more complex scenarios.

    fmt.Println(a.Render(42))

}

12
很不幸,代码库 facebookgo/inject 目前已存档。 - ont.rif
4
这段代码展示了这种方法不必要的复杂性。uber/dig或google/wire是更好的方法。 - Jeremy
实际上,这个解释比Google/Wire更简洁明了。它似乎是一个完全合法的 DI 方法。编译时 DI 本来就不是真正的 DI。 - Fabio Milheiro

4
你也应该尝试一下Dargo,它是新的,但具有Facebook没有的一些功能。代码在这里
以下是一个示例:
在此示例中,名为SimpleService的服务将注入一个记录器。该记录器本身是一个绑定了创建方法的Dargo服务。该创建方法如下:
func newLogger(ioc.ServiceLocator, ioc.Descriptor) (interface{}, error) {
    return logrus.New(), nil
}

SimpleService的绑定将提供应用于实现接口的结构体。该结构体具有一个带有inject注释的字段,后跟要注入的服务的名称。这是用于实现接口的接口和结构体:

type SimpleService interface {
    // CallMe logs a message to the logger!
    CallMe()
}

// SimpleServiceData is a struct implementing SimpleService
type SimpleServiceData struct {
    Log *logrus.Logger `inject:"LoggerService_Name"`
}

// CallMe implements the SimpleService method
func (ssd *SimpleServiceData) CallMe() {
    ssd.Log.Info("This logger was injected!")
}

日志服务和SimpleService都被绑定到了ServiceLocator中。这通常在程序开始时完成:

locator, err := ioc.CreateAndBind("InjectionExampleLocator", func(binder ioc.Binder) error {
        // Binds SimpleService by providing the structure
        binder.Bind("SimpleService", SimpleServiceData{})

        // Binds the logger service by providing the creation function 
        binder.BindWithCreator("LoggerService_Name", newLogger).InScope(ioc.PerLookup)

        return nil
    })

返回的定位器可用于查找SimpleService服务。SimpleService绑定到单例范围(默认范围),这意味着它只会在第一次查找或注入时创建,以后不会再创建。另一方面,LoggerService处于PerLookup范围,这意味着每次注入或查找时都会创建一个新的实例。
这是使用查找到的服务的代码:
raw, err := locator.GetDService("SimpleService")
if err != nil {
    return err
}

ss, ok := raw.(SimpleService)
if !ok {
    return fmt.Errorf("Invalid type for simple service %v", ss)
}

ss.CallMe()

支持任何深度的注入(ServiceA可以依赖于ServiceB,后者依赖于ServiceC等等)。服务还可以依赖于尽可能多的服务(ServiceA可以依赖于服务D、E和F等等)。但是,服务不能具有循环依赖关系。

3

1
我们正在使用Uber的Dig,随着项目变得更大,它变得越来越笨重。 - kamal

1
如果你仍然对寻找一个使用非常少的反射的Go DI库感兴趣,我制作了一个叫做 axon 的库。它基于Google的 Guice,所以如果你来自Java世界(像我一样),应该很容易理解它的工作方式。 我已经在Web服务器中使用过它,并且没有出现任何问题,而且速度足够快,不会影响在CLI中使用。

1
我可能有些偏见,因为我是作者,但https://github.com/muir/nject是用于Go的最佳DI框架。它易于使用,并提供了非常丰富的功能集。
它基于类型。给定一系列消耗和产生各种类型的函数列表,它将它们连接在一起,以便调用列表中的最后一个函数,并且还会调用其他必要的函数来提供最后一个函数的参数。

1
你还可以查看Uber的fx,它是一个更通用的应用框架。正如@yndolok已经提到的那样,它在底层使用了Uber的dig。
fx的Github链接:https://github.com/uber-go/fx

0

Go 1.18增加了对泛型的支持,我认为可以通过使用这种语言特性来实现依赖注入,它现在已经内置于语言中,而不是外部依赖https://go.dev/blog/intro-generics


-1
TLDR; 不行。Golang并没有为这些模式而设计。
没有更好的依赖注入模式,因为Golang(按设计)倡导简单。例如,它没有类系统,但你可以用结构体构建类,通过将函数连接到结构体成员来实现方法,或者通过嵌入一个结构体到另一个结构体来实现继承。依赖注入是面向对象编程的概念,而Golang并不是严格的面向对象语言。
"不能把猪耳朵变成丝绸钱包。"

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接