如何組織Go代碼目錄結(jié)構(gòu)依賴注入wire使用解析
背景
對于大多數(shù) Gopher 來說,編寫 Go 程序會直接在目錄建立 main.go,xxx.go,yyy.go……
不是說不好,對于小型工程來說,簡單反而簡潔明了,我也提倡小工程沒必要整一些花里胡哨的東西。
畢竟 Go 語言作為現(xiàn)代微服務(wù)的開發(fā)新寵,各個方面都比較自由,沒有很多約束。我想,這也是它充滿活力的原因。
對于大型工程而言,或者團隊協(xié)作中,沒有明確的規(guī)范,只會使得項目越來越凌亂……
因為每個人的心中對代碼的管理、組織,對業(yè)務(wù)的理解不完全是一致的。
我參考了 非官網(wǎng)社區(qū)的規(guī)范 以及公司的規(guī)范,談?wù)勂綍r是怎么組織的,希望我的理解,對大家有所幫助。
目錄結(jié)構(gòu)示例
. ├── api 路由與服務(wù)掛接 ├── cmd 程序入口,可以有多個程序 │ └── server │ ├── inject 自動生成依賴注入代碼 │ └── main.go ├── config 配置相關(guān)文件夾 ├── internal 程序內(nèi)部邏輯 │ ├── database │ │ ├── redis.go │ │ └── mysql.go │ ├── dao 數(shù)據(jù)庫操作接口/實現(xiàn) │ │ ├── dao_impls │ │ │ └── user_impls.go │ │ └── user.go 用戶 DAO 接口 │ ├── svc_impls 服務(wù)接口實現(xiàn) │ │ ├── svc_auth │ │ └── svc_user │ └── sdks 外部 SDK 依賴 └── service 服務(wù)接口定義 ├── auth.go 認證服務(wù)定義 └── user.go 用戶服務(wù)定義
面向接口編程
正如你所看到的,我的目錄結(jié)構(gòu)將接口和實現(xiàn)分開存放了。
根據(jù)依賴倒置原則(Dependence Inversion Principle),對象應(yīng)依賴接口,而不是依賴實現(xiàn)。
依賴接口帶來的好處有很多(當然缺點就是你要多寫些代碼):
- 哪天看到某實現(xiàn)有問題,你可以更換一個實現(xiàn)(套娃大法)
- 編寫代碼的時候,你可以站在更高的視角看待問題,而不是陷入細節(jié)中
- 編碼時,因為接口已經(jīng)定義好了,你可以一直在當前的模塊寫下去,不著急寫依賴的模塊的實現(xiàn)
比如我有個 Deployment 常駐進程管理服務(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 里面,但不排除哪天,我會把 dao.DeploymentState 放到 Redis 存儲,此時只需重新實現(xiàn) CURD 四個借口即可。
因為進程的狀態(tài)是頻繁更新的,數(shù)據(jù)量大的時候,放 MySQL 不太合適。
我們再看看 ProcessManager,它也是一個 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 }
我編碼的過程中,只要先想好每個模塊的入?yún)⒑统鰠?,ProcessManager 到底要長什么樣,我到時候再寫!
本地測試時,我也可以寫個 mock 版的 ProcessManager,生產(chǎn)的時候是另一個實現(xiàn),如:
func NewProcessManager(config sdks.ProcessManagerConfig) sdks.ProcessManager { config.Default() if config.IsDevelopment() { return &ProcessManagerMock{config: config} } return &ProcessManager{config: config} }
確實是要多寫點代碼,但是你習慣了之后,你肯定會喜歡上這種方式。
如果你眼尖,你會發(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 }
也就是說,程序啟動時候,可以初始化一個應(yīng)用配置,有了應(yīng)用配置,就有了進程管理器,有了進程管理器,就有了常駐進程管理服務(wù)……
這個時候你會發(fā)現(xiàn),自己去組織這顆依賴樹是非常痛苦的,此時我們可以借助 Google 的 wire 依賴注入代碼生成器,幫我們把這些瑣事做好。
wire
我以前寫 PHP 的時候,主要是使用 Laravel 框架。
wire 和這類框架不同,它的定位是代碼生成,也就是說在編譯的時候,就已經(jīng)把程序的依賴處理好了。
Laravel 的依賴注入,在 Go 的世界里對應(yīng)的是 Uber 的 dig 和 Facebook 的 inject,都是使用 反射 機制實現(xiàn)依賴注入的。
在我看來,我更喜歡 wire,因為很多東西到了運行時,你都不知道具體是啥依賴……
基于代碼生成的 wire 對 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 是一個可以產(chǎn)生值的函數(shù)——也就是我們常說的構(gòu)造函數(shù),上面的 NewProcessManager 就是 Provider。
Injector 可以理解為,當很多個 Provider 組裝在一起的時候,可以得到一個管理對象,這個是我們定義的。
比如我有個 func NewApplicaion() *Applicaion
函數(shù),
它依賴了 A、B、C,
而 C 又依賴了我的 Service,
Service 依賴了 DAO、SDK,
wire 就會自動把 *Applicaion 需要 New 的對象都列舉出來,
先 NewDao,
然后 NewSDK,
再 NewService,
再 NewC,
最后得到 *Applicaion 返回給我們。
此時,NewApplicaion 就是 Injector,不知道這樣描述能不能聽懂!
實在沒明白的,可以看下代碼,這些不是手打的,而是 wire 自動生成的哦~
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 組合你的依賴。
可以看下面的例子,新建一個 wire.gen.go 文件,注意開啟 wireinject 標簽(wire 會識別該標簽并組裝依賴):
//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
:這個就是 Injector 了,表示我最終想要 *app.Application
,并且需要一個 func(),用于程序退出的時候釋放資源,如果中間出現(xiàn)了問題,那就返回 error 給我。
wire.Build(Sets)
:Sets 是一個依賴的集合,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
的用法看文檔就可以了,有點像 Laravel 的接口綁定實現(xiàn)。
此時我們再執(zhí)行 wire 就會生成一個 wire_gen.go 文件,它包含 !wireinject
標簽,表示會被 wire 忽略,因為是 wire 生產(chǎn)出來的!
//go:build !wireinject // +build !wireinject package inject func InitializeApplication() (*app.Application, func(), error) { // 內(nèi)容就是我上面貼的代碼! }
感謝公司的大神帶飛,好記性不如爛筆頭,學到了知識趕緊記下來!
以上就是如何組織Go代碼目錄結(jié)構(gòu)依賴注入wire使用解析的詳細內(nèi)容,更多關(guān)于Go目錄結(jié)構(gòu)依賴注入wire的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
Golang?中的?strconv?包常用函數(shù)及用法詳解
strconv是Golang中一個非常常用的包,主要用于字符串和基本數(shù)據(jù)類型之間的相互轉(zhuǎn)換,這篇文章主要介紹了Golang中的strconv包,需要的朋友可以參考下2023-06-06并發(fā)安全本地化存儲go-cache讀寫鎖實現(xiàn)多協(xié)程并發(fā)訪問
這篇文章主要介紹了并發(fā)安全本地化存儲go-cache讀寫鎖實現(xiàn)多協(xié)程并發(fā)訪問,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪2023-10-10深入學習Golang并發(fā)編程必備利器之sync.Cond類型
Go?語言的?sync?包提供了一系列同步原語,其中?sync.Cond?就是其中之一。本文將深入探討?sync.Cond?的實現(xiàn)原理和使用方法,幫助大家更好地理解和應(yīng)用?sync.Cond,需要的可以參考一下2023-05-05搭建Go語言的ORM框架Gorm的具體步驟(從Java到go)
很多朋友不知道如何使用Goland軟件,搭建一個ORM框架GORM,今天小編給大家分享一篇教程關(guān)于搭建Go語言的ORM框架Gorm的具體步驟(從Java到go),感興趣的朋友跟隨小編一起學習下吧2022-09-09使用golang引入外部包的三種方式:go get, go module, ve
這篇文章主要介紹了使用golang引入外部包的三種方式:go get, go module, vendor目錄,具有很好的參考價值,希望對大家有所幫助,如有錯誤或未考慮完全的地方,望不吝賜教2024-01-01Go語言pointer及switch?fallthrough實戰(zhàn)詳解
這篇文章主要為大家介紹了Go語言pointer及switch?fallthrough實戰(zhàn)詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪2022-06-06