如何組織Go代碼目錄結(jié)構(gòu)依賴注入wire使用解析
背景
對(duì)于大多數(shù) Gopher 來(lái)說(shuō),編寫 Go 程序會(huì)直接在目錄建立 main.go,xxx.go,yyy.go……
不是說(shuō)不好,對(duì)于小型工程來(lái)說(shuō),簡(jiǎn)單反而簡(jiǎn)潔明了,我也提倡小工程沒(méi)必要整一些花里胡哨的東西。
畢竟 Go 語(yǔ)言作為現(xiàn)代微服務(wù)的開(kāi)發(fā)新寵,各個(gè)方面都比較自由,沒(méi)有很多約束。我想,這也是它充滿活力的原因。
對(duì)于大型工程而言,或者團(tuán)隊(duì)協(xié)作中,沒(méi)有明確的規(guī)范,只會(huì)使得項(xiàng)目越來(lái)越凌亂……
因?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)分開(kāi)存放了。
根據(jù)依賴倒置原則(Dependence Inversion Principle),對(duì)象應(yīng)依賴接口,而不是依賴實(shí)現(xiàn)。
依賴接口帶來(lái)的好處有很多(當(dāng)然缺點(diǎn)就是你要多寫些代碼):
- 哪天看到某實(shí)現(xiàn)有問(wèn)題,你可以更換一個(gè)實(shí)現(xiàn)(套娃大法)
- 編寫代碼的時(shí)候,你可以站在更高的視角看待問(wèn)題,而不是陷入細(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 }
我編碼的過(guò)程中,只要先想好每個(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 }
也就是說(shuō),程序啟動(dòng)時(shí)候,可以初始化一個(gè)應(yīng)用配置,有了應(yīng)用配置,就有了進(jìn)程管理器,有了進(jìn)程管理器,就有了常駐進(jìn)程管理服務(wù)……
這個(gè)時(shí)候你會(huì)發(fā)現(xiàn),自己去組織這顆依賴樹(shù)是非常痛苦的,此時(shí)我們可以借助 Google 的 wire 依賴注入代碼生成器,幫我們把這些瑣事做好。
wire
我以前寫 PHP 的時(shí)候,主要是使用 Laravel 框架。
wire 和這類框架不同,它的定位是代碼生成,也就是說(shuō)在編譯的時(shí)候,就已經(jīng)把程序的依賴處理好了。
Laravel 的依賴注入,在 Go 的世界里對(duì)應(yīng)的是 Uber 的 dig 和 Facebook 的 inject,都是使用 反射 機(jī)制實(shí)現(xiàn)依賴注入的。
在我看來(lái),我更喜歡 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ù)——也就是我們常說(shuō)的構(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ì)象都列舉出來(lái),
先 NewDao,
然后 NewSDK,
再 NewService,
再 NewC,
最后得到 *Applicaion 返回給我們。
此時(shí),NewApplicaion 就是 Injector,不知道這樣描述能不能聽(tīng)懂!
實(shí)在沒(méi)明白的,可以看下代碼,這些不是手打的,而是 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 文件,注意開(kāi)啟 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)了問(wè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)出來(lái)的!
//go:build !wireinject // +build !wireinject package inject func InitializeApplication() (*app.Application, func(), error) { // 內(nèi)容就是我上面貼的代碼! }
感謝公司的大神帶飛,好記性不如爛筆頭,學(xué)到了知識(shí)趕緊記下來(lái)!
以上就是如何組織Go代碼目錄結(jié)構(gòu)依賴注入wire使用解析的詳細(xì)內(nèi)容,更多關(guān)于Go目錄結(jié)構(gòu)依賴注入wire的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
Golang?中的?strconv?包常用函數(shù)及用法詳解
strconv是Golang中一個(gè)非常常用的包,主要用于字符串和基本數(shù)據(jù)類型之間的相互轉(zhuǎn)換,這篇文章主要介紹了Golang中的strconv包,需要的朋友可以參考下2023-06-06并發(fā)安全本地化存儲(chǔ)go-cache讀寫鎖實(shí)現(xiàn)多協(xié)程并發(fā)訪問(wèn)
這篇文章主要介紹了并發(fā)安全本地化存儲(chǔ)go-cache讀寫鎖實(shí)現(xiàn)多協(xié)程并發(fā)訪問(wèn),有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-10-10深入學(xué)習(xí)Golang并發(fā)編程必備利器之sync.Cond類型
Go?語(yǔ)言的?sync?包提供了一系列同步原語(yǔ),其中?sync.Cond?就是其中之一。本文將深入探討?sync.Cond?的實(shí)現(xiàn)原理和使用方法,幫助大家更好地理解和應(yīng)用?sync.Cond,需要的可以參考一下2023-05-05Go?http請(qǐng)求排隊(duì)處理實(shí)戰(zhàn)示例
這篇文章主要為大家介紹了Go?http請(qǐng)求排隊(duì)處理實(shí)戰(zhàn)實(shí)例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-07-07搭建Go語(yǔ)言的ORM框架Gorm的具體步驟(從Java到go)
很多朋友不知道如何使用Goland軟件,搭建一個(gè)ORM框架GORM,今天小編給大家分享一篇教程關(guān)于搭建Go語(yǔ)言的ORM框架Gorm的具體步驟(從Java到go),感興趣的朋友跟隨小編一起學(xué)習(xí)下吧2022-09-09Golang中map的三種聲明定義方式實(shí)現(xiàn)
本文主要介紹了Golang中map的三種聲明定義方式實(shí)現(xiàn),文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2023-02-02使用golang引入外部包的三種方式:go get, go module, ve
這篇文章主要介紹了使用golang引入外部包的三種方式:go get, go module, vendor目錄,具有很好的參考價(jià)值,希望對(duì)大家有所幫助,如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2024-01-01Go語(yǔ)言pointer及switch?fallthrough實(shí)戰(zhàn)詳解
這篇文章主要為大家介紹了Go語(yǔ)言pointer及switch?fallthrough實(shí)戰(zhàn)詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-06-06