如何組織Go代碼目錄結(jié)構(gòu)依賴注入wire使用解析
背景
對(duì)于大多數(shù) Gopher 來說,編寫 Go 程序會(huì)直接在目錄建立 main.go,xxx.go,yyy.go……
不是說不好,對(duì)于小型工程來說,簡(jiǎn)單反而簡(jiǎn)潔明了,我也提倡小工程沒必要整一些花里胡哨的東西。
畢竟 Go 語(yǔ)言作為現(xiàn)代微服務(wù)的開發(fā)新寵,各個(gè)方面都比較自由,沒有很多約束。我想,這也是它充滿活力的原因。
對(duì)于大型工程而言,或者團(tuán)隊(duì)協(xié)作中,沒有明確的規(guī)范,只會(huì)使得項(xiàng)目越來越凌亂……
因?yàn)槊總€(gè)人的心中對(duì)代碼的管理、組織,對(duì)業(yè)務(wù)的理解不完全是一致的。
我參考了 非官網(wǎng)社區(qū)的規(guī)范 以及公司的規(guī)范,談?wù)勂綍r(shí)是怎么組織的,希望我的理解,對(duì)大家有所幫助。
目錄結(jié)構(gòu)示例
.
├── api 路由與服務(wù)掛接
├── cmd 程序入口,可以有多個(gè)程序
│ └── server
│ ├── inject 自動(dòng)生成依賴注入代碼
│ └── main.go
├── config 配置相關(guān)文件夾
├── internal 程序內(nèi)部邏輯
│ ├── database
│ │ ├── redis.go
│ │ └── mysql.go
│ ├── dao 數(shù)據(jù)庫(kù)操作接口/實(shí)現(xiàn)
│ │ ├── dao_impls
│ │ │ └── user_impls.go
│ │ └── user.go 用戶 DAO 接口
│ ├── svc_impls 服務(wù)接口實(shí)現(xiàn)
│ │ ├── svc_auth
│ │ └── svc_user
│ └── sdks 外部 SDK 依賴
└── service 服務(wù)接口定義
├── auth.go 認(rèn)證服務(wù)定義
└── user.go 用戶服務(wù)定義面向接口編程
正如你所看到的,我的目錄結(jié)構(gòu)將接口和實(shí)現(xiàn)分開存放了。
根據(jù)依賴倒置原則(Dependence Inversion Principle),對(duì)象應(yīng)依賴接口,而不是依賴實(shí)現(xiàn)。
依賴接口帶來的好處有很多(當(dāng)然缺點(diǎn)就是你要多寫些代碼):
- 哪天看到某實(shí)現(xiàn)有問題,你可以更換一個(gè)實(shí)現(xiàn)(套娃大法)
- 編寫代碼的時(shí)候,你可以站在更高的視角看待問題,而不是陷入細(xì)節(jié)中
- 編碼時(shí),因?yàn)榻涌谝呀?jīng)定義好了,你可以一直在當(dāng)前的模塊寫下去,不著急寫依賴的模塊的實(shí)現(xiàn)
比如我有個(gè) Deployment 常駐進(jìn)程管理服務(wù),我是這樣定義的:
type Service struct {
DB isql.GormSQL
DaoGroup dao.Group
DaoDeployment dao.Deployment
DaoDeploymentStates dao.DeploymentState
ProcessManager sdks.ProcessManager
ServerManager sdks.ServerManager
ServerSelector sdks.ServerSelector
}該 struct 的成員都是接口。
目前 dao.* 都是在 MySQL 里面,但不排除哪天,我會(huì)把 dao.DeploymentState 放到 Redis 存儲(chǔ),此時(shí)只需重新實(shí)現(xiàn) CURD 四個(gè)借口即可。
因?yàn)檫M(jìn)程的狀態(tài)是頻繁更新的,數(shù)據(jù)量大的時(shí)候,放 MySQL 不太合適。
我們?cè)倏纯?ProcessManager,它也是一個(gè) interface:
type ProcessManager interface {
StartProcess(ctx context.Context, serverIP string, params ProcessCmdArgs) (code, pid int, err error)
CheckProcess(ctx context.Context, serverIP string, pid int) (err error)
InfoProcess(ctx context.Context, serverIP string, pid int) (info jobExecutor.ProcessInfoResponse, err error)
KillProcess(ctx context.Context, serverIP string, pid int) (err error)
IsProcessNotRunningError(err error) bool
}我編碼的過程中,只要先想好每個(gè)模塊的入?yún)⒑统鰠?,ProcessManager 到底要長(zhǎng)什么樣,我到時(shí)候再寫!
本地測(cè)試時(shí),我也可以寫個(gè) mock 版的 ProcessManager,生產(chǎn)的時(shí)候是另一個(gè)實(shí)現(xiàn),如:
func NewProcessManager(config sdks.ProcessManagerConfig) sdks.ProcessManager {
config.Default()
if config.IsDevelopment() {
return &ProcessManagerMock{config: config}
}
return &ProcessManager{config: config}
}確實(shí)是要多寫點(diǎn)代碼,但是你習(xí)慣了之后,你肯定會(huì)喜歡上這種方式。
如果你眼尖,你會(huì)發(fā)現(xiàn) NewProcessManager 也是依賴倒置的!它依賴 sdks.ProcessManagerConfig 配置:
func GetProcessManagerConfig() sdks.ProcessManagerConfig {
return GetAcmConfig().ProcessManagerConfig
}而 GetProcessManagerConfig 又依賴 AcmConfig 配置:
func GetAcmConfig() AcmConfig {
once.Do(func() {
err := cfgLoader.Load(&acmCfg, ...)
if err != nil {
panic(err)
}
})
return acmCfg
}也就是說,程序啟動(dòng)時(shí)候,可以初始化一個(gè)應(yīng)用配置,有了應(yīng)用配置,就有了進(jìn)程管理器,有了進(jìn)程管理器,就有了常駐進(jìn)程管理服務(wù)……
這個(gè)時(shí)候你會(huì)發(fā)現(xiàn),自己去組織這顆依賴樹是非常痛苦的,此時(shí)我們可以借助 Google 的 wire 依賴注入代碼生成器,幫我們把這些瑣事做好。
wire
我以前寫 PHP 的時(shí)候,主要是使用 Laravel 框架。
wire 和這類框架不同,它的定位是代碼生成,也就是說在編譯的時(shí)候,就已經(jīng)把程序的依賴處理好了。
Laravel 的依賴注入,在 Go 的世界里對(duì)應(yīng)的是 Uber 的 dig 和 Facebook 的 inject,都是使用 反射 機(jī)制實(shí)現(xiàn)依賴注入的。
在我看來,我更喜歡 wire,因?yàn)楹芏鄸|西到了運(yùn)行時(shí),你都不知道具體是啥依賴……
基于代碼生成的 wire 對(duì) IDE 十分友好,容易調(diào)試。
要想使用 wire,得先理解 Provider 和 Injector:
Provider: a function that can produce a value. These functions are ordinary Go code.
Injector: a function that calls providers in dependency order. With Wire, you write the injector’s signature, then Wire generates the function’s body.
Provider 是一個(gè)可以產(chǎn)生值的函數(shù)——也就是我們常說的構(gòu)造函數(shù),上面的 NewProcessManager 就是 Provider。
Injector 可以理解為,當(dāng)很多個(gè) Provider 組裝在一起的時(shí)候,可以得到一個(gè)管理對(duì)象,這個(gè)是我們定義的。
比如我有個(gè) func NewApplicaion() *Applicaion 函數(shù),
它依賴了 A、B、C,
而 C 又依賴了我的 Service,
Service 依賴了 DAO、SDK,
wire 就會(huì)自動(dòng)把 *Applicaion 需要 New 的對(duì)象都列舉出來,
先 NewDao,
然后 NewSDK,
再 NewService,
再 NewC,
最后得到 *Applicaion 返回給我們。
此時(shí),NewApplicaion 就是 Injector,不知道這樣描述能不能聽懂!
實(shí)在沒明白的,可以看下代碼,這些不是手打的,而是 wire 自動(dòng)生成的哦~
func InitializeApplication() (*app.Application, func(), error) {
extend := app.Extend{}
engine := app.InitGinServer()
wrsqlConfig := config.GetMysqlConfig()
gormSQL, cleanup, err := database.InitSql(wrsqlConfig)
if err != nil {
return nil, nil, err
}
daoImpl := &dao_group.DaoImpl{}
cmdbConfig := config.GetCmdbConfig()
rawClient, cleanup2 := http_raw_client_impls.NewHttpRawClient()
cmdbClient, err := cmdb_client_impls.NewCmdbCli(cmdbConfig, rawClient)
if err != nil {
cleanup2()
cleanup()
return nil, nil, err
}
serverManagerConfig := config.GetServerManagerConfig()
jobExecutorClientFactoryServer := job_executor_client_factory_server_impls.NewJobExecutorClientFactoryServer(serverManagerConfig)
serverManager := server_manager_impls.NewServerManager(gormSQL, daoImpl, cmdbClient, serverManagerConfig, jobExecutorClientFactoryServer)
service := &svc_cmdb.Service{
ServerManager: serverManager,
}
svc_groupService := &svc_group.Service{
DB: gormSQL,
DaoGroup: daoImpl,
ServerManager: serverManager,
}
dao_deploymentDaoImpl := &dao_deployment.DaoImpl{}
dao_deployment_stateDaoImpl := &dao_deployment_state.DaoImpl{}
processManagerConfig := config.GetProcessManagerConfig()
jobExecutorClientFactoryProcess := job_executor_client_factory_process_impls.NewJobExecutorClientFactoryProcess(serverManagerConfig)
jobExecutorClientFactoryJob := job_executor_client_factory_job_impls.NewJobExecutorClientFactoryJob(serverManagerConfig)
processManager := process_manager_impls.NewProcessManager(processManagerConfig, jobExecutorClientFactoryProcess, jobExecutorClientFactoryJob)
serverSelector := server_selector_impls.NewMultiZonesSelector()
svc_deploymentService := &svc_deployment.Service{
DB: gormSQL,
DaoGroup: daoImpl,
DaoDeployment: dao_deploymentDaoImpl,
DaoDeploymentStates: dao_deployment_stateDaoImpl,
ProcessManager: processManager,
ServerManager: serverManager,
ServerSelector: serverSelector,
}
svc_deployment_stateService := &svc_deployment_state.Service{
DB: gormSQL,
ProcessManager: processManager,
DaoDeployment: dao_deploymentDaoImpl,
DaoDeploymentState: dao_deployment_stateDaoImpl,
JobExecutorClientFactoryProcess: jobExecutorClientFactoryProcess,
}
authAdminClientConfig := config.GetAuthAdminConfig()
authAdminClient := auth_admin_client_impls.NewAuthAdminClient(authAdminClientConfig, rawClient)
redisConfig := config.GetRedisConfig()
redis, cleanup3, err := database.InitRedis(redisConfig)
if err != nil {
cleanup2()
cleanup()
return nil, nil, err
}
svc_authService := &svc_auth.Service{
AuthAdminClient: authAdminClient,
Redis: redis,
}
dao_managersDaoImpl := &dao_managers.DaoImpl{}
kserverConfig := config.GetServerConfig()
svc_heartbeatService := &svc_heartbeat.Service{
DB: gormSQL,
DaoManagers: dao_managersDaoImpl,
ServerConfig: kserverConfig,
JobExecutorClientFactoryServer: jobExecutorClientFactoryServer,
}
portalClientConfig := config.GetPortalClientConfig()
portalClient := portal_client_impls.NewPortalClient(portalClientConfig, rawClient)
authConfig := config.GetAuthConfig()
svc_portalService := &svc_portal.Service{
PortalClient: portalClient,
AuthConfig: authConfig,
Auth: svc_authService,
}
apiService := &api.Service{
CMDB: service,
Group: svc_groupService,
Deployment: svc_deploymentService,
DeploymentState: svc_deployment_stateService,
Auth: svc_authService,
Heartbeat: svc_heartbeatService,
Portal: svc_portalService,
}
ginSvcHandler := app.InitSvcHandler()
grpcReportTracerConfig := config.GetTracerConfig()
configuration := config.GetJaegerTracerConfig()
tracer, cleanup4, err := pkgs.InitTracer(grpcReportTracerConfig, configuration)
if err != nil {
cleanup3()
cleanup2()
cleanup()
return nil, nil, err
}
gatewayConfig := config.GetMetricsGatewayConfig()
gatewayDaemon, cleanup5 := pkgs.InitGateway(gatewayConfig)
application := app.NewApplication(extend, engine, apiService, ginSvcHandler, kserverConfig, tracer, gatewayDaemon)
return application, func() {
cleanup5()
cleanup4()
cleanup3()
cleanup2()
cleanup()
}, nil
}wire 怎么用倒是不難,推薦大家使用 Provider Set 組合你的依賴。
可以看下面的例子,新建一個(gè) wire.gen.go 文件,注意開啟 wireinject 標(biāo)簽(wire 會(huì)識(shí)別該標(biāo)簽并組裝依賴):
//go:build wireinject
// +build wireinject
package inject
import (
"github.com/google/wire"
)
func InitializeApplication() (*app.Application, func(), error) {
panic(wire.Build(Sets))
}
func InitializeWorker() (*worker.Worker, func(), error) {
panic(wire.Build(Sets))
}InitializeApplication:這個(gè)就是 Injector 了,表示我最終想要 *app.Application,并且需要一個(gè) func(),用于程序退出的時(shí)候釋放資源,如果中間出現(xiàn)了問題,那就返回 error 給我。
wire.Build(Sets) :Sets 是一個(gè)依賴的集合,Sets 里面可以套 Sets:
var Sets = wire.NewSet(
ConfigSet,
DaoSet,
SdksSet,
ServiceSet,
)
var ServiceSet = wire.NewSet(
// ...
wire.Struct(new(svc_deployment.Service), "*"),
wire.Bind(new(service.Deployment), new(*svc_deployment.Service)),
wire.Struct(new(svc_group.Service), "*"),
wire.Bind(new(service.Group), new(*svc_group.Service)),
)注:wire.Struct 和 wire.Bind 的用法看文檔就可以了,有點(diǎn)像 Laravel 的接口綁定實(shí)現(xiàn)。
此時(shí)我們?cè)賵?zhí)行 wire 就會(huì)生成一個(gè) wire_gen.go 文件,它包含 !wireinject 標(biāo)簽,表示會(huì)被 wire 忽略,因?yàn)槭?wire 生產(chǎn)出來的!
//go:build !wireinject
// +build !wireinject
package inject
func InitializeApplication() (*app.Application, func(), error) {
// 內(nèi)容就是我上面貼的代碼!
}感謝公司的大神帶飛,好記性不如爛筆頭,學(xué)到了知識(shí)趕緊記下來!
以上就是如何組織Go代碼目錄結(jié)構(gòu)依賴注入wire使用解析的詳細(xì)內(nèi)容,更多關(guān)于Go目錄結(jié)構(gòu)依賴注入wire的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
golang的匿名函數(shù)和普通函數(shù)的區(qū)別解析
匿名函數(shù)是不具名的函數(shù),可以在不定義函數(shù)名的情況下直接使用,通常用于函數(shù)內(nèi)部的局部作用域中,這篇文章主要介紹了golang的匿名函數(shù)和普通函數(shù)的區(qū)別,需要的朋友可以參考下2023-03-03
Go語(yǔ)言實(shí)現(xiàn)動(dòng)態(tài)解析JSON數(shù)據(jù)的多種方式
本文主要介紹了Go語(yǔ)言實(shí)現(xiàn)動(dòng)態(tài)解析JSON數(shù)據(jù)的多種方式,包括map[string]interface{}、interface{}、json.RawMessage及第三方庫(kù)gjson/mapstructure,具有一定的參考價(jià)值,感興趣的可以了解一下2025-05-05
詳解Golang如何優(yōu)雅的終止一個(gè)服務(wù)
后端服務(wù)通常會(huì)需要?jiǎng)?chuàng)建子協(xié)程來進(jìn)行相應(yīng)的作業(yè),但進(jìn)程接受到終止信號(hào)或正常結(jié)束時(shí),并沒有判斷或等待子協(xié)程執(zhí)行結(jié)束,下面這篇文章主要給大家介紹了關(guān)于Golang如何優(yōu)雅的終止一個(gè)服務(wù)的相關(guān)資料,需要的朋友可以參考下2022-03-03
go編程中g(shù)o-sql-driver的離奇bug解決記錄分析
這篇文章主要為大家介紹了go編程中g(shù)o-sql-driver的離奇bug解決記錄分析,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-05-05
Golang使用archive/zip包實(shí)現(xiàn)ZIP壓縮與解壓
Golang?中的?archive/zip?包用于處理?ZIP?格式的壓縮文件,提供了一系列用于創(chuàng)建、讀取和解壓縮?ZIP?格式文件的函數(shù)和類型,使用起來非常方便,下面就跟隨小編一起了解一下具體使用方法吧2023-08-08
聊聊go xorm生成mysql的結(jié)構(gòu)體問題
這篇文章主要介紹了go xorm生成mysql的結(jié)構(gòu)體問題,本文給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友參考下吧2022-03-03
Go語(yǔ)言實(shí)現(xiàn)牛頓法求平方根函數(shù)的案例
這篇文章主要介紹了Go語(yǔ)言實(shí)現(xiàn)牛頓法求平方根函數(shù)的案例,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過來看看吧2020-12-12

