网站上传完成后要怎么做,wordpress怎么用畅言,网站建设 站内页面连接,温州网站制作软件一、前言
线上项目往往依赖非常多的具备特定能力的资源#xff0c;如#xff1a;DB、MQ、各种中间件#xff0c;以及随着项目业务的复杂化#xff0c;单一项目内#xff0c;业务模块也逐渐增多#xff0c;如何高效、整洁管理各种资源十分重要。
本文从“术”层面#…一、前言
线上项目往往依赖非常多的具备特定能力的资源如DB、MQ、各种中间件以及随着项目业务的复杂化单一项目内业务模块也逐渐增多如何高效、整洁管理各种资源十分重要。
本文从“术”层面讲述“依赖注入”的实现带你体会其对于整洁架构 DDD 等设计思想的落地起到的支撑作用。
涉及内容 最热门的 golang 依赖注入库GitHub 12.5khttps://github.com/google/wire GiuHub 22.5k 的 golang 微服务框架 kratos 默认使用 wire 作为依赖注入方式https://github.com/go-kratos/kratos Spring Boot 与 Golang 的依赖注入对比 依赖注入的设计哲学 B站账号白泽talk绝大部分博客内容都将会通过视频讲解不过文章一般是先于视频发布 白泽的开源 Golang 学习仓库https://github.com/BaiZe1998/go-learning用于文章归档 聚合博客代码案例 公众号【白泽talk】本期内容的 pdf 版本可以关注公众号回复【依赖注入】获得往期资源的获取都是类似的方式。 二、What 本文所涉及编写的代码已收录于 https://github.com/BaiZe1998/go-learning/di 目录
一句话概括实例 A 的创建依赖于实例 B 的创建且在实例 A 的生命周期内持有对实例 B 的访问权限。
2.1 案例分析
依赖注入Dependency Injection, DI以 Golang 为例左侧为手动完成依赖注入右侧为不使用依赖注入 不使用依赖注入风险
全局变量十分不安全存在覆写的可能资源散落在各处可能重复创建浪费内存后续维护能力极差提高循环依赖的风险全局变量的引入提高单元测试的成本 不使用依赖注入 demo
package mainvar (mysqlUrl mysql://blabla// 全局数据库实例db NewMySQLClient(mysqlUrl)
)func NewMySQLClient(url string) *MySQLClient {return MySQLClient{url: url}
}type MySQLClient struct {url string
}func (c *MySQLClient) Exec(query string, args ...interface{}) string {return data
}func NewApp() *App {return App{}
}type App struct {
}func (a *App) GetData(query string, args ...interface{}) string {data : db.Exec(query, args...)return data
}// 不使用依赖注入
func main() {app : NewApp()rest : app.GetData(select * from table where id ?, 1)println(rest)
}手动依赖注入 demo
package mainfunc NewMySQLClient(url string) *MySQLClient {return MySQLClient{url: url}
}type MySQLClient struct {url string
}func (c *MySQLClient) Exec(query string, args ...interface{}) string {return data
}func NewApp(client *MySQLClient) *App {return App{client: client}
}type App struct {// App 持有唯一的 MySQLClient 实例client *MySQLClient
}func (a *App) GetData(query string, args ...interface{}) string {data : a.client.Exec(query, args...)return data
}// 手动依赖注入
func main() {client : NewMySQLClient(mysql://blabla)app : NewApp(client)rest : app.GetData(select * from table where id ?, 1)println(rest)
}三、Why
依赖注入 (Dependency Injection缩写为 DI)可以理解为一种代码的构造模式就是写法按照这样的方式来写能够让你的代码更加容易维护。
四、How
4.1 Golang 依赖注入
以 Golang 最多的开源库 wire 为例讲解https://github.com/google/wire/blob/main/docs/guide.md
wire是由 google 开源的一个供 Go 语言使用的依赖注入代码生成工具。它能够根据你的代码生成相应的依赖注入 go 代码。
而与其它依靠反射实现的依赖注入工具不同的是wire 能在编译期准确地说是代码生成时如果依赖注入有问题在代码生成时即可报出来不会拖到运行时才报更便于 debug。
Install
go install github.com/google/wire/cmd/wirelatestprovider: a function that can produce a value
以上面手动实现依赖注入为基础wire 做的工作是帮助开发者完成如下组装过程
client : NewMySQLClient(mysql://blabla)
app : NewApp(client)而其中用到的 NewMySQLClient、NewApp 在 wire 定义为一个个的 provider是需要提前由开发者实现的。
func NewMySQLClient(url string) *MySQLClient {return MySQLClient{url: url}
}func NewApp(client *MySQLClient) *App {return App{client: client}
}假设系统中的资源很多配置很多出现了如下复杂的初始化流程人工完成依赖注入则变得复杂
a : NewA(xxx, yyy) error
b : NewB(ctx, a) error
c : NewC(zzz, a, b) error
d : NewD(www, kkk, a) error
e : NewD(ctx, b, d) errorinjector: a function that calls providers in dependency order
如下是名为 wire.go 的依赖注入配置文件是一个只会被 wire 命令行工具处理的 injector 文件用于声明依赖注入流程。
wire.go:
//go:build wireinject
// build wireinject// The build tag makes sure the stub is not built in the final build.package mainimport github.com/google/wire// wireApp init application.
func wireApp(url string) *App {wire.Build(NewMySQLClient, NewApp)return nil
}
执行 wire 命令则在当前目录下生成 wire_gen.go 文件此时的 wireApp 函数就等价于最初手动编写的依赖注入流程可以在真正需要初始化的引入。
wire_gen.go:
// Code generated by Wire. DO NOT EDIT.//go:generate go run -modmod github.com/google/wire/cmd/wire
//go:build !wireinject
// build !wireinjectpackage main// Injectors from wire.go:// wireApp init application.
func wireApp(url string) *App {mySQLClient : NewMySQLClient(url)app : NewApp(mySQLClient)return app
}4.2 针对复杂项目的依赖注入设计哲学
这里以 go-kratos 的模版项目为例讲解是一个 helloworld 服务我们着重分析其借助 wire 进行依赖注入的部分。
以下 helloworld 模板服务的 interanl 目录的内容
.
├── biz
│ ├── README.md
│ ├── biz.go
│ └── greeter.go
├── conf
│ ├── conf.pb.go
│ └── conf.proto
├── data
│ ├── README.md
│ ├── data.go
│ └── greeter.go
├── server
│ ├── grpc.go
│ ├── http.go
│ └── server.go
└── service├── README.md├── greeter.go└── service.go各个目录的关系如图 data业务数据访问包含 cache、db 等封装实现了 biz 的 repo 接口data 偏重业务的含义它所要做的是将领域对象重新拿出来。 biz业务逻辑的组装层类似 DDD 的 domain 层data 类似 DDD 的 reporepo 接口在这里定义使用依赖倒置的原则。 service实现了 api 定义的服务层类似 DDD 的 application 层处理 DTO 到 biz 领域实体的转换DTO - DO同时协同各类 biz 交互但是不应处理复杂逻辑。 server为http和grpc实例的创建和配置以及注册对应的 service 。
上图右侧部分表示了模块之间的依赖关系可以看到依赖的注入是逆向的资源往往被业务模块持有业务模块则被负责编排业务的应用持有应用则被负责对外通信的模块持有。
此时在服务启动前的实例化阶段provider 的定义和注入本质是这样一种状态
func main() {dbClient : NewDBClient()dataN : NewDataN(dbClient)dataM : NewDataM(dbClient)bizA : NewBizA(dataN)bizB : NewBizB(dataM)bizC : NewBizC(dataN, dataM)serviceX : NewService(bizA, bizB, bizC)server : NewServer(serviceX)server.httpXXX // 提供 http 服务server.grpcXXX // 提供 grpc 服务
}在 helloworld 这个 demo 当中则是这样定义 provider 的
// biz 目录
var ProviderSet wire.NewSet(NewGreeterUsecase)type GreeterUsecase struct {repo GreeterRepolog *log.Helper
}func NewGreeterUsecase(repo GreeterRepo, logger log.Logger) *GreeterUsecase {return GreeterUsecase{repo: repo, log: log.NewHelper(logger)}
}func (uc *GreeterUsecase) CreateGreeter(ctx context.Context, g *Greeter) (*Greeter, error) {uc.log.WithContext(ctx).Infof(CreateGreeter: %v, g.Hello)return uc.repo.Save(ctx, g)
}// data 目录
var ProviderSet wire.NewSet(NewData, NewGreeterRepo)type Data struct {// TODO wrapped database client
}func NewData(c *conf.Data, logger log.Logger) (*Data, func(), error) {cleanup : func() {log.NewHelper(logger).Info(closing the data resources)}return Data{}, cleanup, nil
}type greeterRepo struct {data *Datalog *log.Helper
}func NewGreeterRepo(data *Data, logger log.Logger) biz.GreeterRepo {return greeterRepo{data: data,log: log.NewHelper(logger),}
}
// service 目录
var ProviderSet wire.NewSet(NewGreeterService)type GreeterService struct {v1.UnimplementedGreeterServeruc *biz.GreeterUsecase
}func NewGreeterService(uc *biz.GreeterUsecase) *GreeterService {return GreeterService{uc: uc}
}func (s *GreeterService) SayHello(ctx context.Context, in *v1.HelloRequest) (*v1.HelloReply, error) {g, err : s.uc.CreateGreeter(ctx, biz.Greeter{Hello: in.Name})if err ! nil {return nil, err}return v1.HelloReply{Message: Hello g.Hello}, nil
}// server 目录
var ProviderSet wire.NewSet(NewGRPCServer, NewHTTPServer)func NewGRPCServer(c *conf.Server, greeter *service.GreeterService, logger log.Logger) *grpc.Server {var opts []grpc.ServerOption{grpc.Middleware(recovery.Recovery(),),}if c.Grpc.Network ! {opts append(opts, grpc.Network(c.Grpc.Network))}if c.Grpc.Addr ! {opts append(opts, grpc.Address(c.Grpc.Addr))}if c.Grpc.Timeout ! nil {opts append(opts, grpc.Timeout(c.Grpc.Timeout.AsDuration()))}srv : grpc.NewServer(opts...)v1.RegisterGreeterServer(srv, greeter)return srv
}在 helloworld 这个 demo 当中则是这样定义 injector 的
// wire.go
func wireApp(*conf.Server, *conf.Data, log.Logger) (*kratos.App, func(), error) {panic(wire.Build(server.ProviderSet, data.ProviderSet, biz.ProviderSet, service.ProviderSet, newApp))
}最后运行 wire 的到的完成注入的文件如下
// wire_gen.go
func wireApp(confServer *conf.Server, confData *conf.Data, logger log.Logger) (*kratos.App, func(), error) {dataData, cleanup, err : data.NewData(confData, logger)if err ! nil {return nil, nil, err}greeterRepo : data.NewGreeterRepo(dataData, logger)greeterUsecase : biz.NewGreeterUsecase(greeterRepo, logger)greeterService : service.NewGreeterService(greeterUsecase)grpcServer : server.NewGRPCServer(confServer, greeterService, logger)httpServer : server.NewHTTPServer(confServer, greeterService, logger)app : newApp(logger, grpcServer, httpServer)return app, func() {cleanup()}, nil
}生成代码之后则可以像使用普通的 golang 函数一样使用这个 wire_gen.go 文件内的 wireApp 函数实例化一个 helloworld 服务
func main() {flag.Parse()logger : log.With(log.NewStdLogger(os.Stdout),// ...)c : config.New(// ...)defer c.Close()// ...app, cleanup, err : wireApp(bc.Server, bc.Data, logger)if err ! nil {panic(err)}defer cleanup()// start and wait for stop signalif err : app.Run(); err ! nil {panic(err)}
}4.3 wire 的更多用法
参见 wire 的文档自己用几遍就明白了这里举几个例子
定义携带 error 返回值的 provider
func ProvideBaz(ctx context.Context, bar Bar) (Baz, error) {if bar.X 0 {return Baz{}, errors.New(cannot provide baz when bar is zero)}return Baz{X: bar.X}, nil
}provider 集合方便组织多个 provider
var SuperSet wire.NewSet(ProvideFoo, ProvideBar, ProvideBaz)接口绑定
type Fooer interface {Foo() string
}type MyFooer stringfunc (b *MyFooer) Foo() string {return string(*b)
}func provideMyFooer() *MyFooer {b : new(MyFooer)*b Hello, World!return b
}type Bar stringfunc provideBar(f Fooer) string {// f will be a *MyFooer.return f.Foo()
}var Set wire.NewSet(provideMyFooer,wire.Bind(new(Fooer), new(*MyFooer)),provideBar)五、对比 Spring Boot 的依赖注入
Spring Boot的依赖注入DI和Golang开源库Wire的依赖注入在设计思路上存在一些相同点和不同点。以下是对这些相同点和不同点的分析
相同点
降低耦合度两者都通过依赖注入的方式实现了代码的松耦合。这意味着一个对象不需要显式地创建或查找它所依赖的其他对象这些依赖项会由外部容器如Spring容器或工具如Wire自动提供。提高可测试性由于依赖关系被解耦可以更容易地替换依赖项以进行单元测试。无论是Spring Boot还是使用Wire的Golang应用都可以轻松地为组件提供模拟或存根的依赖项以进行测试。灵活性两者都允许在不修改组件代码的情况下替换依赖项。这使得应用程序在维护和扩展时更加灵活。
不同点
实现方式 Spring Boot的依赖注入是基于Java的反射机制和Spring框架的容器管理功能实现的。Spring容器负责创建和管理Bean的生命周期并在需要时自动注入依赖项核心在于运行时。Wire是一个Golang的代码生成工具它通过分析代码中的构造函数和结构体标签自动生成依赖注入的代码减少人工工作量在开发阶段已经通过工具生成好了依赖注入的代码程序编译时资源之间的依赖关系已经固定。 配置方式 Spring Boot的依赖注入通常通过配置文件如application.properties或application.yml和注解如Autowired进行配置。开发者可以在配置文件中定义Bean的属性并通过注解在需要注入的地方指明依赖关系。Wire则通过特殊的Go文件通常是wire.go文件来定义类型之间的依赖关系。这些文件包含了用于生成依赖注入代码的指令和元数据。 运行时开销 Spring Boot的依赖注入在运行时需要依赖Spring容器来管理Bean的生命周期和依赖关系。这可能会引入一些额外的运行时开销特别是在大型应用程序中。Wire在编译时生成依赖注入的代码因此它在运行时没有额外的开销。这使得使用Wire的Golang应用程序通常具有更好的性能。
六、参考资料
kratoshttps://go-kratos.dev/en/docs/getting-started/start/
wirehttps://github.com/google/wire/blob/main/_tutorial/README.md