go

依赖注入

go依赖注入

Posted by Liangjf on August 28, 2020

go依赖注入

依赖注入在java框架中是一个非常重要和有用的特性,特别比如spring。依赖注入可以帮助我们免除类与类间的错综复杂的依赖关系,直接帮助我们管理类的依赖,通过内部分析,直接生成“核心”类。

在go中,其实是提倡简单明了的实现的,比如go只有区区30多个关键字,简单实用就是其特点。在实际开发中,也创造出各种“优美”的go风格的编程范式。

因为没有构造函数,所以出现了可导出的Newxxx,init函数;因为没有默认参数,所以出现了函数选项模式;因为chan的特性,所以出现了pipe设计模式,因为没有析构函数,所以可借用defer特性来实现……

其实这些设计模式,都是为了在实际开发时能够更加优雅的实现需求。

在开发中,除非是个人小项目,不然系统都是由n个模块构成的,所以成百上千个struct是很正常的事。那么,在这么多的struct中,各个模块/类之间肯定是有依赖的,如果是人工的根据依赖关系来初始化,那么可能要花费很多的时间来划分和理顺这个关系。

所以依赖注入这时候就大有用处了。

依赖注入

依赖注入是实现解耦,抽象化编程,面向接口的重要实现方法。一般在依赖实现的时候,不会在内部实例化依赖的对象,而是通过参数的形式传入进去,当然传入的最好是接口,这样内部和外部就解耦了,外部实例化实现接口的任意对象,内部就可以不改变代码的情况下做出相应的改变。这样也很好的满足了对拓展开放,对修改闭合的设计原则。

在面向对象语言中,依赖注入不可或缺的。我觉得在go中,可能大多是php,c/c++转过来的,所以对这个没什么概念,包括在很多的开源项目中,依赖注入也是应用比较少的。当然,不是说依赖注入就是银弹,只是很多情况下依赖注入可以使我们的代码更加健壮,拓展性更好,高内聚松耦合。

在go中,有几个比较有名的依赖注入库:

以下四它们的star对比

google/wire facebookarchive/inject uber-go/dig
4.3k 1.3k 1.5k

虽然star数不能完全表示项目的好坏,但是有一定的参考价值。为什么google/wire比其余两者高多这么多?

As of version v0.3.0, Wire is beta and is considered feature complete. It works well for the tasks it was designed to perform, and we prefer to keep it as simple as possible.

We’ll not be accepting new features at this time, but will gladly accept bug reports and fixes.

这两句话是google/wire主页写着的。从中深深的感受到作者的自信。可以看出来wire是比其余二者更简单,更易用。

以下都是以google/wire来介绍如何在go中应用google/wire来实现自动生成代码的依赖注入。

google/wire是在编译器就自动生成代码来实现依赖注入,并且最终得到一个“核心”的Newxxx函数来作为整个依赖注入的入口。

对比facebookarchive/inject在运行时通过反射的方式来实现依赖注入,google/wire在编译器就通过自动生成代码的方式可以避免在运行时反射,避免了运行时的性能消耗,从而提高了性能。

通过 github.com/google/wire/cmd/wire 命令在编译时执行,会得到一个wire_gen.go文件,里面就是依赖注入生成的入口。

我比较喜欢这种方式,在编译器就可以得到整个依赖注入的结果,这样可以把错误放到开发中,更容易调试,更容易使用。

google/wire的使用

下面看个简单的例子,看看google/wire是如何使用的。

Foo类

package foobarbaz

type Foo struct {
    X int
}

// ProvideFoo returns a Foo.
func ProvideFoo() Foo {
    return Foo{X: 42}
}

Bar类

package foobarbaz

// ...

type Bar struct {
    X int
}

// ProvideBar returns a Bar: a negative Foo.
func ProvideBar(foo Foo) Bar {
    return Bar{X: -foo.X}
}

单独存放wire set

package foobarbaz

import (
    // ...
    "github.com/google/wire"
)

// ...

var SuperSet = wire.NewSet(ProvideFoo, ProvideBar, ProvideBaz)

wire文件用于自动生成代码

// +build wireinject
// The build tag makes sure the stub is not built in the final build.

package main

import (
    "context"

    "github.com/google/wire"
    "example.com/foobarbaz"
)

func initializeBaz(ctx context.Context) (foobarbaz.Baz, error) {
    //传入上面的SuperSet
    wire.Build(foobarbaz.MegaSet)
    return foobarbaz.Baz{}, nil
}

在wire.go文件的当前目录执行 wire 命令,安装方法 go get github.com/google/wire/cmd/wire,然后设置pkg目录到path就可以在任意目录使用wire命令了。

就会在当前目录生成wire_gen.go文件

// Code generated by Wire. DO NOT EDIT.

//go:generate wire
//+build !wireinject

package main

import (
    "example.com/foobarbaz"
)

func initializeBaz(ctx context.Context) (foobarbaz.Baz, error) {
    foo := foobarbaz.ProvideFoo()
    bar := foobarbaz.ProvideBar(foo)
    baz, err := foobarbaz.ProvideBaz(ctx, bar)
    if err != nil {
        return 0, err
    }
    return baz, nil
}

调用 initializeBaz(ctx) 就会得到 foobarbaz.Baz 的实例化对象。

对于上面的例子,要注意 initializeBaz 可以传入多个参数,但是必须是类型不一致的,比如两个string,就会报错,可以使用type来声明类型别名,但是参数变量多了,这样也麻烦,所以可以把所有的参数放到一个结构体里面,比如

type Options struct {
    ctx context.Context
    path string
    data string
    level int
}

func initializeBaz(opts Options) (foobarbaz.Baz, error) {
    //传入上面的SuperSet
    wire.Build(foobarbaz.MegaSet)
    return foobarbaz.Baz{}, nil
}

其他的类同时具有 Options 参数,就可以使用 依赖注入传入的 Options 了。

通过wire自动生成代码,实现依赖注入是不是很简单?可能这里因为类的依赖很简单,看不出效果,你可以看下我这里的mvc架构的小项目,就是使用wire来实现依赖注入的。 go-wire-mvc地址

总结

不管用不用依赖注入类的自动化框架,我们写代码都要面向接口编程,要做到高内聚松耦合,模块间解耦,做到对拓展开放,对修改闭合。争取加新功能只是新增代码,而不是要修改原来的大量代码。

当然,使用上先进的工具,会加快工作效率。