Go 工程化 -二- 項目目錄結構
序
本系列爲 Go 進階訓練營 筆記,預計 2021Q1 完成更新,訪問 博客: Go 進階訓練營 即可查看當前更新進度,部分文章篇幅較長,使用 PC 大屏瀏覽體驗更佳
工程化這一節說簡單看似簡單,無非就是目錄結構,代碼分層,依賴注入等等。但是其中很多坑如果沒踩過是不知道這裏面的痛點的。除此之外這裏面也會有很多架構的思想在裏面,這也就是爲什麼我會把架構整潔之道的閱讀筆記放在第一小節的原因。
接來下包含這一篇文章在內,我會先用幾篇文章結合參考材料以及個人的理解整理一下毛老師課上講的內容。然後恰好在這個課程前,我也在對我們之前的一些項目做重構,所以會再用一到兩篇文章大概說一些我最後選擇的方式,已經在實踐過程中的一些取捨,就工程化這個事情來說大概原理上基本都是相通的,但是每個團隊甚至每個人所面臨的一些問題都各不相同,所以最後出來的東西肯定不是完全一致的。
注意,你如果是隻是需要寫一個腳本,或者是做一些簡單的 demo 大可不必像文章接下來介紹的這樣搞的這麼麻煩,直接一個 main.go 簡單快捷方便即可,但是如果你這是一個長期維護的項目,甚至涉及到的多個人之間的合作,那麼接下來的幾篇文章就不能錯過了,可以仔細閱讀,希望可以對你有所幫助。
Standard Go Project Layout
這一部分的內容主要來自於 github 的高星項目:golang-standards/project-layout 通過這個我們可以大概的瞭解到在 Go 中一些約定俗成的目錄含義,雖然這些不是強制性的,但是如果有去看官方的源碼或者是一些知名的項目可以發現大多都是這麼命名的,所以我們最好和社區保持一致,大家保持同樣的語言。
/cmd
我們一般採用 /cmd/[appname]/main.go
的形式進行組織
-
首先 cmd 目錄下一般是項目的主幹目錄
-
這個目錄下的文件**「不應該有太多的代碼,不應該包含業務邏輯」**
-
main.go 當中主要做的事情就是負責程序的生命週期,服務所需資源的依賴注入等,其中依賴注入一般而言我們會使用一個依賴注入框架,這個主要看複雜程度,後續會有一篇文章單獨介紹這個
/internal
internal 目錄下的包,不允許被其他項目中進行導入,這是在 Go 1.4 當中引入的 feature,會在編譯時執行
-
所以我們一般會把項目文件夾放置到 internal 當中,例如
/internal/app
-
如果是可以被其他項目導入的包我們一般會放到 pkg 目錄下
-
如果是我們項目內部進行共享的包,而不期望外部共享,我們可以放到
/internal/pkg
當中 -
注意 internal 目錄的限制並不侷限於頂級目錄,在任何目錄當中都是生效的
舉個 🌰 下面的是我們當前的目錄結構,其中的代碼很簡單,在 t.go
當中導出了一個變量 I
然後在 a/cmd/a/main.go
和 b/cmd/b/main.go
當中分別導入輸出這個變量的值
❯ tree
.
├── a
│ ├── cmd
│ │ └── a
│ │ └── main.go
│ └── internal
│ └── pkg
│ └── t
│ └── t.go
└── b
└── cmd
└── b
└── main.go
我們可以發現, a
目錄下可以直接輸出 I
的值
❯ go run ./a/cmd/a/main.go
1
但是在 b
目錄下,編譯器會直接報錯說導入了 a
的私有包
❯ go run ./b/cmd/b/main.go
package command-line-arguments
b/cmd/b/main.go:3:8: use of internal package github.com/mohuishou/go-training/Week04/blog/02_project_layout/01_internal_example/a/internal/pkg/t not allowed
/pkg
一般而言,我們在 pkg 目錄下放置可以被外部程序安全導入的包,對於不應該被外部程序依賴的包我們應該放置到 internal
目錄下, internal
目錄會有編譯器進行強制驗證
-
pkg 目錄下的包一般會按照功能進行區分,例如
/pkg/cache
、/pkg/conf
等 -
如果你的目錄結構比較簡單,內容也比較少,其實也可以不使用
pkg
目錄,直接把上面的這些包放在最上層即可 -
一般而言我們應用程序 app 在最外層會包含很多文件,例如
.gitlab-ci.yml
makefile
.gitignore
等等,這種時候頂層目錄會很多並且會有點雜亂,建議還是放到/pkg
目錄比較好
Kit Project Layout
kit 庫其實也就是一些基礎庫
-
每一個公司正常來說應該**「有且僅有一個基礎庫項目」**
-
kit 庫一般會包含一些常用的公共的方法,例如緩存,配置等等,比較典型的例子就是 go-kit
-
kit 庫必須具有的特點:
-
統一
-
標準庫方式佈局
-
高度抽象
-
支持插件
-
儘量減少依賴
-
持續維護
減少依賴和持續維護是我後面補充的,這一點其實很遺憾,我們部門剛進來的時候方向是對的也建立了一套基礎庫,然後大家都使用這同一套庫,但是很遺憾,我們這一套庫一是沒人維護,二是沒有一套機制來進行迭代,到現在很多團隊和項目已經各搞各的了。這樣其實會導致做很多重複工作以及後續的一些改動很難推進,前車之鑑,如果有類似的情況一定要在小火苗出來的時候先摁住,從大的角度來講統一有時候比好用重要,不好用應該參與貢獻而不是另起爐竈。
Service Application Project Layout
在這一小節我們會先看到毛老師在課上講解的他們的應用程序目錄的迭代變化,然後說一些我最後的採用的目錄結構以及裏面的取捨,關於具體怎麼演進來的當中遇到了什麼問題,我們會在 Go 工程化這個系列的最後一篇文章詳細說明。
/api
API 定義的目錄,如果我們採用的是 grpc 那這裏面一般放的就是 proto 文件,除此之外也有可能是 openapi/swagger 定義文件,以及他們生成的文件。
下面給出一個我現在使用的 api 目錄的定義,其實和毛老師課上講的類似,後面還有一篇文章會專門講 api 的設計會講到這裏就不詳細講了
.
└── api
└── product_name // 產品名稱
└── app_name // 應用名稱
└── v1 // 版本號
└── v1.proto
/config(s)
爲什麼加個 (s) 是課上講的還有參考材料中很多都叫 configs 但是我們習慣使用 config 但是含義上都是一樣的 這裏面一般放置配置文件文件和默認模板
/test
額外的外部測試應用程序和測試數據。一般會放測試一些輔助方法和測試數據
服務類型
微服務中的 app 服務類型分爲 4 類:interface、service、job、admin。
-
interface: 對外的 BFF 服務,接受來自用戶的請求,比如暴露了 HTTP/gRPC 接口。
-
service: 對內的微服務,僅接受來自內部其他服務或者網關的請求,比如暴露了 gRPC 接口只對內服務。
-
admin:區別於 service,更多是面向運營測的服務,通常數據權限更高,隔離帶來更好的代碼級別安全。
-
job: 流式任務處理的服務,上游一般依賴 message broker。
-
task: 定時任務,類似 cronjob,部署到 task 託管平臺中。
myapp
-
myapp-api: 這個是對外暴露的 api 的服務,可以是 http, 也可以是 grpc
-
myapp-cron: 這個是定時任務
-
myapp-job: 這個用於處理來自 message 的流式任務
-
myapp-migration: 數據庫遷移任務,用於初始化數據庫 • 成都有沒有什麼風俗串了,
-
scripts/xxx: 一次性執行的腳本,有時候會有一些腳本任務
大多大同小異,主要是 BFF 層我們一般是一個獨立的應用,不會放在同一個倉庫裏面,
項目佈局 v1
-
model: 放對應 “存儲層” 的結構體,是對存儲的一一隱射。
-
dao: 數據讀寫層,數據庫和緩存全部在這層統一處理,包括 cache miss 處理。
-
service: 組合各種數據訪問來構建業務邏輯。
-
server: 依賴 proto 定義的服務作爲入參,提供快捷的啓動服務全局方法。
-
api: 定義了 API proto 文件,和生成的 stub 代碼,它生成的 interface,其實現者在 service 中。
-
service 的方法簽名因爲實現了 API 的 接口定義,DTO 直接在業務邏輯層直接使用了,更有 dao 直接使用,最簡化代碼。
-
DO(Domain Object): 領域對象,就是從現實世界中抽象出來的有形或無形的業務實體。缺乏 DTO -> DO 的對象轉換。
v1 存在的問題
-
沒有 DTO 對象,model 中的對象貫穿全局,所有層都有
-
model 層的數據不是每個接口都需要的,這個時候會有一些問題
-
在上一篇文章中其實也反覆提到了 “如果兩段看似重複的代碼,如果有不同的變更速率和原因,那麼這兩段代碼就不算是真正的重複”
-
server 層的代碼可以通過基礎庫幹掉,提供統一服務暴露方式
項目佈局 v2
-
app 目錄下有 api、cmd、configs、internal 目錄,目錄裏一般還會放置 README、CHANGELOG、OWNERS。
-
「internal:」 是爲了避免有同業務下有人跨目錄引用了內部的 biz、data、service 等內部 struct。
-
如果存在一個倉庫多個應用,那麼可以在 internal 裏面進行分層,例如
/internal/app
,/internal/job
-
「biz」: 業務邏輯的組裝層,類似 DDD 的 domain 層,data 類似 DDD 的 repo,repo 接口在這裏定義,使用依賴倒置的原則。
-
「data」: 業務數據訪問,包含 cache、db 等封裝,實現了 biz 的 repo 接口。我們可能會把 data 與 dao 混淆在一起,data 偏重業務的含義,它所要做的是將領域對象重新拿出來,我們去掉了 DDD 的 infra 層。
-
「service」: 實現了 api 定義的服務層,類似 DDD 的 application 層,處理 DTO 到 biz 領域實體的轉換 (DTO -> DO),同時協同各類 biz 交互,但是不應處理複雜邏輯。
-
PO(Persistent Object): 持久化對象,它跟持久層(通常是關係型數據庫)的數據結構形成一一對應的映射關係,如果持久層是關係型數據庫,那麼數據表中的每個字段(或若干個)就對應 PO 的一個(或若干個)屬性。
示例可以參考 kratos v2 的 example
我的項目佈局
.
├── api
├── cmd
│ └── app
├── config
├── internal
│ ├── domain
│ ├── repo
│ ├── service
│ └── usecase
└── pkg
「internal:」 是爲了避免有同業務下有人跨目錄引用了內部的對象
-
domain: 類似之前的 model 層,這裏麪包含了 DO 對象,usecase interface, repo interface 的定義
-
repo: 定於數據訪問,包含 cache, db 的封裝
-
usecase: 這裏是業務邏輯的組裝層,類似上面的 biz 層,但是區別是我們這裏不包含 DO 對象和 repo 對象的定義
-
service: 實現 api 的服務層,主要實現 DTO 和 DO 對象的轉化,參數的校驗等等
我們這裏的定義和上面 v2 最大的區別是多了一個 domain 層,這裏面有一個原因是我們對於單元測試的要求比較高,如果按照上面 v2 的代碼進行組織,service 層直接依賴 usecase 的實現,service 的代碼不太好進行單元測試。如果依賴 interface 會導致循環依賴,所以採用類似 go-clean-arch 的組織,單獨抽象一層 domain 層
應該避免的壞習慣
/src
一般而言,在 Go 項目當中不應該出現 src 目錄,Go 和 Java 不同,在 Go 中每一個目錄都是一個包,每一個包都是一等公民,我們不需要將項目代碼放到 src 當中,不要用寫其他語言的方式來寫 Go
utils,common
不要在項目中出現 utils 和 common 這種包,如果出現這種包,因爲我們並不能從包中知道你這個包的作用,長久之後這個包就會變成一個大雜燴,所有東西都往這裏面扔。有的同學這個時候會問說,那我們的工具函數應該放到哪裏?怎麼放?舉個例子,我們當前使用 gin
作爲路由框架,但是 gin
的 handler 註冊其實不是很方便,所以我們做了一層封裝,這個時候這個工具方法我們一般放在 /pkg/ginx
目錄下,表示這個是對 gin
增強的包,不直接使用 gin
作爲包名的原因是因爲我們在項目中也會引用 gin
相同的命名一個是會導致誤解,另一個是在同時導入的時候也會需要去進行重命名會比較麻煩
總結
關於項目目錄結構這種真的算是見仁見智,不同的理論有不同的方法,但是我覺得有兩件事比較重要,就服務應用而言需要靈活應用,就基礎庫而言一定要統一,做的好不好和要不要做是兩件事情,如果因爲當前做的不夠好而不做,那麼越到後面就越做不了。下一篇文章會講一講依賴注入框架 wire 的使用與最佳 (?) 實踐
參考文獻
-
Go 進階訓練營 - 極客時間
-
golang-standards/project-layout · GitHub
-
Package Oriented Design
-
Go 1.4 Release Notes - The Go Programming Language
-
I'll take pkg over internal
-
https://medium.com/@benbjohnson/standard-package-layout-7cdbc8391fc1
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/_yCG3hs3iFxZVUQ6YMhpaA