清晰架構(Clean Architecture)的 Go 微服務: 程序結構

我使用 Go 和 gRPC 創建了一個微服務,並試圖找出最佳的程序結構,它可以用作我未來程序的模板。 我有 Java 背景,並發現自己在 Java 和 Go 之間掙扎,它們之間的編程理念完全不同。我寫了一系列關於在項目工作中做出的設計決策和取捨的文章。 這是其中的第一篇, 是關於程序結構的。

程序結構的資源

Go 的標準程序結構的最佳資源可能是 Github 上的標準 Go 程序結構 ¹,但它不適合我的項目。在閱讀了 Sylvain Wallez 的文章 ² 之後,我終於得到了一些關於其背後原因的信息。 Go 起初是專爲 API 和網絡服務器而設計,Github 上的大多數 Go 項目都是以庫的形式編寫的,因此 “標準 Go 程序結構” 可能非常適合。 商業微服務項目是一種完全不同的動物,需要不同的程序結構。但我還是採用了 “標準 Go 程序結構” 中適用的一些建議,這些建議約佔 30%。

一般來說,創建應用程序結構有兩種不同的方法:基於業務功能或基於技術結構。大家的共識 ³ 是基於業務功能的更好,對於單體項目(monolithic project)來說也確實如此。在微服務架構中,情況發生了變化,因爲每個服務都有自己的源碼庫,這相當於已經把應用程序按業務功能查分成了一個個的微服務。因此,在每個微服務中,基於技術結構創建項目結構實際上是可行的。

我還找到了 Ben Johnson 關於應用程序結構⁴和包結構⁵的一些好建議。但它沒有爲我的項目提供完整的解決方案,所以我決定創建自己的程序結構。程序結構取決於項目要求,以下是需求。

項目需求:
  1. 在數據持久層上支持不同的數據庫(Sql 和 NoSql 數據庫)

  2. 使用不同的協議(如 gRPC 或 REST)支持來自其他微服務的數據

  3. 鬆散耦合和高度內聚

  4. 支持簡單一致的日誌記錄,並能夠更改它(例如,日誌記錄級別和日誌記錄庫),而無需修改程序中的日誌記錄語句。

  5. 支持業務級別的事物交易。

程序結構也受到程序設計的影響。 我採用了 Bob Martin 的清晰架構(Clean Architecture)⁶ 和 Go 的 簡潔⁷ 設計風格.

在業務邏輯方面,有三層:“模型(model)”,即域模型; “數據服務(dataservice)”,它是數據持久性(數據庫)層; “用例(usecase)”,這是業務邏輯層。

頂層程序結構:

adapter: 這是應用程序和外部數據服務之間的接口,例如另一個 gRPC 服務。 所有數據轉換都發生在這裏,這樣你的業務邏輯代碼不需要了解外部服務的具體實現(無論是 gRPC 還是 REST)。

cmd: 命令。 所有不同類型的 “main.go” 都在這裏,你可以有多個。 這是應用程序的起點。

config: 設置程序和配置數據,包括配置文件。

container: 應用程序依賴注入容器,負責創建具體類型並將它們注入每個函數。

dataservice: 持久層,負責檢索和修改域模型的數據。 它只依賴(depend)於模型(model)層。 數據服務可以通過 RPC 或 RESTFul 調用從數據庫或其他微服務獲取數據。

model: 域模型層,具有域結構(model struct)。 所有其他層依賴於此層,而此層不依賴於任何其他層。

script: 與設置應用程序相關的腳本,例如,數據庫腳本。

tool: 第三方工具。

usecase : 這是一個重要的層並且是業務邏輯的切入點。 每個業務功能都由用例實現。 它是程序頂層,因此沒有其他層依賴於它(“cmd” 除外),但它依賴於其他層。

用例和數據服務層功能全部由接口調用,因此可以輕鬆更改實現。

頂級包下的子文件包:

adapter:

當程序需要與微服務或其他外部服務進行交互時,你需要創建接口以減少依賴性。例如,本程序中的 “緩存服務” 是一個 gRPC 微服務。每個外部服務都有自己的子包和文件。例如,緩存服務具有 “cacheclient” 包和 “cacheClient.go” 文件,該文件定義了與外部 “緩存” 微服務交互的類型和方法。

在我們的示例中,與其他數據服務不同,“cacheClient.go”文件沒有定義緩存服務的接口。實際上它有一個,但是 interface 是在 “dataservice” 包中定義的,因爲 “緩存服務” 也是一個數據服務。更明確的方法可能是在兩個包中各自創建一個接口,這將保持包結構的統一。但是這兩個接口將是相同且冗餘的,所以我刪除了適配器中的接口。

由於我們還需要將應用程序本身發佈爲 gRPC 服務,因此需要在此處創建 “userclient” 子包。 “generatedclient”子包是爲 gRPC 和 Protobuf 生成的代碼。“userGrpc.go”用來處理域模型結構和 gRPC 結構之間的格式轉換。

cmd:

應用程序的命令,是整個程序的起點。 你可以將應用程序作爲本地應用程序運行,也可以將其作爲微服務應用程序運行,在這種情況下,你同時擁有客戶端(grpcClientMain.go)和服務器端(grpcServerMain.go)主文件。 所有未來的主文件 (main.go) 也將在此處,例如,Web 應用程序服務器主文件。

config:

在此保存所有應用配置文件。 “appConfig.go”負責從配置文件中讀取並數據將它們加載到應用程序配置結構中。 你可以爲不同的環境提供不同的配置文件(YAML 文件),例如 “Dev” 和“Prod”。

container:

本程序中最複雜的包,它爲每個接口創建具體結構並將它們注入其他層。此包中的子包結構類似於應用邏輯分層,它還具有 “用例(usecase)”,“數據服務(dataservice)” 和“數據存儲(datastore)”層。

在本包頂層,“container.go”定義容器接口,它的實現文件 “serviceContainer.go” 在“servicecontainer”子包中。它是此包的入口點,它將此包中的其他代碼粘合在一起。 “usecasefactory”子包中的 “registrationFactory.go” 和其他工廠(factory)使用工廠方法模式(factory method pattern)創建具體結構(struct)。 日誌⁸不屬於任何應用層,因此我爲它創建了一個單獨的子包 “loggerfactory”。它還有一個“logger” 子包來定義日誌記錄接口。我在文章程序容器 9 中解釋了這個包中的所有內容。

dataservice:

域模型的持久層。 頂層只有一個文件 - “dataService.go”,它具有數據服務的所有接口,包括其他微服務提供的數據服務(例如 gRPC)。 其他包只需要依賴於頂級數據服務接口,而不需要了解特定數據庫的實現細節。

對於域模型中的每種類型,都有相應的數據服務。 例如,對於模型 “User”,有一個“userdata” 子包,它包含用戶持久性服務的所有實現,包括 sqldb(MySql)和 CouchDB。

model:

該層不需要接口,模型都是具體結構。

Script:

目前它只有 MySql 的數據庫腳本。

tool:

此程序包適用於第三方工具。 如果你不想直接依賴第三方庫,或者需要增強這些第三方庫,請在此處進行封裝。 不要與 “adapter” 包混淆,後者也處理第三方庫,但只適用於應用程序級數據服務。 “tool”包更適用於較低級別的庫。

useCase:

頂級包下只有一個文件 “useCase.go”,它包含所有用例接口。 目前,有三個用例:“RegistrationUseCase”,“ListUserUseCase” 和“ListCourse”。

每個用例都有一個子包。 該子包中的文件定義了實現用例接口的具體結構。 所有其他包僅依賴於頂級的用例接口,不需要了解每個用例的實現細節。

可能的問題:

這個程序結構最終會產生很多小的子包,每個子包只有一個或幾個文件。 這與 Go 習慣用法 “考慮更少,更大的包 ¹⁰相矛盾. 以下才是習慣用法應創建的包:

如果你爲其他人編寫一個外部庫,那麼將代碼放入一個大包中是一個很好的規則,因爲人們不需要多個 import 語句來使用你的庫。 但是在你自己的應用程序中,擁有小包是可以的,特別是當你只將接口暴露給其他層時。

本程序爲什要用小包呢? 首先 “useCase.go” 只定義接口,而其他包(容器除外)僅依賴於接口,因此 “useCase.go” 需要一個獨立的包。 其次,用文件夾分隔每個用例使程序更清晰易讀。

結論:

對於 gRPC 微服務項目,“標準 Go 程序結構” 可能不太適合。 基於業務邏輯而不是技術結構創建應用程序結構對單體項目是一個很好的建議,不一定適合微服務項目。 本文使用一個真實的應用程序作爲示例來展示什麼是一個很好的微服務應用程序結構及其背後的原因。

源程序:

完整的源程序鏈接 github: https://github.com/jfeng45/servicetmpl

索引:

[1][golang-standards/project-layout]
(https://github.com/golang-standards/project-layout)

[2][Go: the Good, the Bad and the Ugly]
(https://bluxte.net/musings/2018/04/10/go-good-bad-ugly/)

[3][Package by feature, not layer]
(http://www.javapractices.com/topic/TopicAction.do?Id=205)

[4][Structuring Applications in Go]
(https://medium.com/@benbjohnson/structuring-applications-in-go-3b04be4ff091)

[5][Standard Package Layout]
(https://medium.com/@benbjohnson/standard-package-layout-7cdbc8391fc1)

[6][The Clean Code Blog]
(https://blog.cleancoder.com/uncle-bob/2012/08/13/the-clean-architecture.html)

[7][Go at Google: Language Design in the Service of Software Engineering]
(https://talks.golang.org/2012/splash.article)

[8][Go Microservice with Clean Architecture: Application Logging]
(https://jfeng45.github.io/posts/go_logging_and_error_handling/)

[9][Go Microservice with Clean Architecture: Application Container]
(https://jfeng45.github.io/posts/application_container/)

[10][Practical Go: Real world advice for writing maintainable Go programs]
(https://dave.cheney.net/practical-go/presentations/qcon-china.html)

不堆砌術語,不羅列架構,不迷信權威,不盲從流行,堅持獨立思考

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://blog.csdn.net/weixin_38748858/article/details/103629874