Go 工程化 -二- 項目目錄結構

本系列爲 Go 進階訓練營 筆記,預計 2021Q1 完成更新,訪問 博客: Go 進階訓練營 即可查看當前更新進度,部分文章篇幅較長,使用 PC 大屏瀏覽體驗更佳

工程化這一節說簡單看似簡單,無非就是目錄結構,代碼分層,依賴注入等等。但是其中很多坑如果沒踩過是不知道這裏面的痛點的。除此之外這裏面也會有很多架構的思想在裏面,這也就是爲什麼我會把架構整潔之道的閱讀筆記放在第一小節的原因。

接來下包含這一篇文章在內,我會先用幾篇文章結合參考材料以及個人的理解整理一下毛老師課上講的內容。然後恰好在這個課程前,我也在對我們之前的一些項目做重構,所以會再用一到兩篇文章大概說一些我最後選擇的方式,已經在實踐過程中的一些取捨,就工程化這個事情來說大概原理上基本都是相通的,但是每個團隊甚至每個人所面臨的一些問題都各不相同,所以最後出來的東西肯定不是完全一致的。

注意,你如果是隻是需要寫一個腳本,或者是做一些簡單的 demo 大可不必像文章接下來介紹的這樣搞的這麼麻煩,直接一個 main.go 簡單快捷方便即可,但是如果你這是一個長期維護的項目,甚至涉及到的多個人之間的合作,那麼接下來的幾篇文章就不能錯過了,可以仔細閱讀,希望可以對你有所幫助。

Standard Go Project Layout

這一部分的內容主要來自於 github 的高星項目:golang-standards/project-layout 通過這個我們可以大概的瞭解到在 Go 中一些約定俗成的目錄含義,雖然這些不是強制性的,但是如果有去看官方的源碼或者是一些知名的項目可以發現大多都是這麼命名的,所以我們最好和社區保持一致,大家保持同樣的語言。

/cmd

我們一般採用 /cmd/[appname]/main.go  的形式進行組織

/internal

internal 目錄下的包,不允許被其他項目中進行導入,這是在 Go 1.4 當中引入的 feature,會在編譯時執行

舉個 🌰 下面的是我們當前的目錄結構,其中的代碼很簡單,在 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  目錄會有編譯器進行強制驗證

Kit Project Layout

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。

這上面是毛老師課上講解的類型,和我們常用的做法類似,但是有點區別,同樣假設我們有一個應用叫 myapp

大多大同小異,主要是 BFF 層我們一般是一個獨立的應用,不會放在同一個倉庫裏面,

項目佈局 v1

項目的依賴路徑爲: model -> dao -> service -> api,model struct 串聯各個層,直到 api 需要做 DTO 對象轉換。

v1 存在的問題

項目佈局 v2

示例可以參考 kratos v2 的 example

我的項目佈局

.
├── api
├── cmd
│   └── app
├── config
├── internal
│   ├── domain
│   ├── repo
│   ├── service
│   └── usecase
└── pkg

「internal:」 是爲了避免有同業務下有人跨目錄引用了內部的對象

我們這裏的定義和上面 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 的使用與最佳 (?) 實踐

參考文獻

  1. Go 進階訓練營 - 極客時間

  2. golang-standards/project-layout · GitHub

  3. Package Oriented Design

  4. Go 1.4 Release Notes - The Go Programming Language

  5. I'll take pkg over internal

  6. https://medium.com/@benbjohnson/standard-package-layout-7cdbc8391fc1

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/_yCG3hs3iFxZVUQ6YMhpaA