Go 工程化標準實踐
原文鏈接:https://www.yipwinghong.com/2021/08/04/Go_engineering-standard
目錄
-
項目結構
-
標準項目結構
-
工具包項目結構
-
服務應用項目結構
-
生命週期
-
API 設計
-
gRPC
-
目錄結構
-
兼容性
-
命名規範
-
原始字段
-
異常處理
-
設計規則
-
配置管理
-
最佳實踐
-
模塊管理
-
GOPATH
-
GO Modules
-
Proxy
-
Private
-
GOPROXY 編譯部署
-
測試
-
單元測試
-
最佳實踐
-
參考
項目結構
標準項目結構
/cmd
|-- cmd
|-- demo
|-- demo
+-- main.go
+-- demo1
|-- demo1
+-- main.go
項目的主幹,每個應用程序目錄名與可執行文件的名稱匹配。該目錄不應放置太多代碼。
/internal
|-- internal
+-- demo
|-- biz
|-- service
+-- data
私有應用程序和庫代碼。該目錄由 Go 編譯器強制執行(更多細節請參閱 Go 1.4 release notes),在項目樹的任何級別上都可以有多個 /internal 目錄。
可在 /internal 包中添加額外結構,以分隔共享和非共享的內部代碼。對於較小的項目而言不是必需,但最好有可視化線索顯示預期的包的用途。
實際應用程序代碼可放在 /internal/app 目錄下(比如 /internal/app/myapp),應用程序共享代碼可放在 /internal/pkg 目錄下(比如 /internal/pkg/myprivlib)。
相關服務(比如賬號服務內部有 rpc、job、admin 等)整合一起後需要區分 app。單一服務則可以去掉 /internal/myapp。
/pkg
|-- pkg
|-- memcache
+-- redis
|-- conf
|-- dsn
|-- env
|-- flagvar
+-- paladin
.
|-- docs
|-- example
|-- misc
|-- pkg
|-- third_party
|-- tool
外部應用程序可以使用的庫代碼。可以顯式地表示該目錄代碼對於其他人而言是安全可用的。
/pkg 目錄內可參考 Go 標準庫的組織方式,按照功能分類。/internal/pkg 一般用於項目內的跨應用公共共享代碼,但其作用域僅在單個項目工程內。
pkg 和 internal 目錄的相關描述可以參考 I’ll take pkg over internal[1]。
當根目錄包含大量非 Go 組件和目錄時,這也是一種將 Go 代碼分組到一個位置的方法,使得運行各種 Go 工具更容易組織。
工具包項目結構
|-- cache
|-- memcache
| +-- test
+-- redis
+-- test
|-- conf
|-- dsn
|-- env
|-- flagvar
+-- paladin
+-- apollo
+-- internal
+-- mockserver
|-- container
|-- group
|-- pool
+-- queue
+-- apm
|-- database
|-- hbase
|-- sql
+-- tidb
|-- ecode
+-- types
|-- log
+-- internal
|-- core
+-- filewriter
應當爲不同的微服務建立統一的 kit 工具包項目(基礎庫 / 框架)和 app 項目。
基礎庫 kit 爲獨立項目,公司級建議只有一個。由於按照功能目錄來拆分會帶來不少的管理工作,建議合併整合。
其具備以下特點:
-
統一
-
標準庫方式佈局
-
高度抽象
-
支持插件
服務應用項目結構
.
|-- README.md
|-- api
|-- cmd
|-- configs
|-- go.mod
|-- go.sum
|-- internal
+-- test
/api
API 協議定義目錄,比如 protobuf 文件和生成的 go 文件。
通常把 API 文檔直接在 proto 文件中描述。
/configs
配置文件模板或默認配置。
/test
外部測試應用程序和測試數據。可隨時根據需求構造 /test 目錄。
對於較大的項目數據子目錄是很有意義的。比如可使用 /test/data 或 /test/testdata(如果需要忽略目錄中的內容)。
Go 會忽略以 “.” 或“_”開頭的目錄或文件,因此在命名測試數據目錄方面有更大靈活性。
GitLab Project
|-- app
|-- replay
|--..
+-- member
|-- pkg
|-- database
|-- ..
+-- log
+-- ...
一個 GitLab project 中可以放置多個微服務 app(類似 monorepo),也可以按照 GitLab 的 group 裏建立多個 project,每個 project 對應一個 app。
微服務結構
|-- cmd 負責程序的:啓動、關閉、配置初始化等。
|-- myapp1-admin 面向運營側的服務,通常數據權限更高,隔離實現更好的代碼級別安全。
|-- myapp1-interface 對外的 BFF 服務,接受來自用戶的請求(HTTP、gRPC)。
|-- myapp1-job 流式任務服務,上游一般依賴 message broker。
|-- myapp1-service 對內的微服務,僅接受來自內部其他服務或網關的請求(gRPC)。
+-- myapp1-task 定時任務服務,類似 cronjob,部署到 task 託管平臺中。
以下這種目錄結構風格:
|-- service
|-- api API 定義(protobuf 等)以及對應生成的 client 代碼,基於 pb 生成的 swagger.json。
|-- cmd
|-- configs 服務配置文件,比如 database.yaml、redis.yaml、application.yaml。
|-- internal 避免有同業務下被跨目錄引用了內部的 model、dao 等內部 struct。
|-- model 對應“存儲層”的結構體,是對存儲的一一映射。
|-- dao 數據讀寫層,統一處理數據庫和緩存(cache miss 等問題)。
|-- service 組合各種數據訪問來構建業務邏輯,包括 api 中生成的接口實現。
|-- server 依賴 proto 定義的服務作爲入參,提供快捷的啓動服務全局方法。
|-- ...
app 目錄下有 api、cmd、configs、internal 目錄。一般還會放置 README、CHANGELOG、OWNERS。
項目的依賴路徑爲:model -> dao -> service -> api,model struct 串聯各個層,直到 api 做 DTO 對象轉換。
另一種結構風格是將 DDD 設計思想和工程結構做了簡化,映射到 api、service、biz、data 各層。
.
|-- CHANGELOG
|-- OWNERS
|-- README
|-- api
|-- cmd
|-- myapp1-admin
|-- myapp1-interface
|-- myapp1-job
|-- myapp1-service
+-- myapp1-task
|-- configs
|-- go.mod
|-- internal 避免有同業務下被跨目錄引用了內部的 model、dao 等內部 struct。
|-- biz 業務邏輯組裝層,類似 DDD domain(repo 接口再次定義,依賴倒置)。
|-- data 業務數據訪問,包含 cache、db 等封裝,實現 biz 的 repo 接口。
|-- pkg
+-- service 實現了 api 定義的服務層,類似 DDD application
處理 DTO 到 biz 領域實體的轉換(DTO->DO),同時協同各類 biz 交互,不處理複雜邏輯。
架構與數據模型
鬆散分層架構(Relaxed Layered System):層間關係不太嚴格,每層都可能使用它下面所有層的服務(而不僅是下一層)。每層都可能是半透明的,意味着有些服務只對上一層可見,而有些服務對上面的所有層都可見。
[ api ]
| | |
| [ service ] |
| | |
[ biz ] |
| |
[ data ]
繼承分層架構(Layering Through Inheritance):高層繼承並實現低層接口。需要調整各層順序,將基礎設施層移動到最高層。這依然是單向依賴,意味着領域層、應用層、表現層將不能依賴基礎設施層,而基礎設施層可以依賴它們。
[ data ]
| | |
| [ api ] |
| | |
[ service ] |
| |
[ biz ]
數據模型:
-
失血模型:僅包含數據定義和 getter/setter 方法,業務邏輯和應用邏輯都放到服務層中。在 Java 中稱爲 POJO,在 .NET 中稱爲 POCO。
-
貧血模型:包含一些業務邏輯,但不包含依賴持久層的業務邏輯(會放在服務層中),領域對象不依賴於持久層。
-
充血模型:包含所有業務邏輯,領域層依賴於持久層,簡單表示就是:UI 層 -> 服務層 -> 領域層 <-> 持久層。
-
脹血模型:和業務邏輯不相關的其他應用邏輯(如授權、事務等)放到領域模型中(反而是另外一種失血模型,服務層缺失、由領域層代勞)。
生命週期
考慮服務應用對象初始化和生命週期管理,所有 HTTP/gRPC 依賴的前置資源初始化(包括 data、biz、service),之後再啓動監聽服務。
資源初始化和關閉步驟繁瑣,比較容易出錯。可利用依賴注入的思路,使用 google/wire[2] 管理資源依賴注入,方便測試和實現單次初始化與複用。
svr := http.NewServer()
app := kratos.New()
app.Append(kratos.Hook{
OnStart: func(ctx context.Context) error {
return svr.Start()
},
OnStop: func(ctx context.Context) error {
return svr.Shutdown(ctx)
},
})
if err := app.Run(); err != nil {
log.Printf("app failed: %v\n", err)
return
}
另外還支持靜態生成代碼,便於診斷(而不是在運行時通過 reflection 實現)。
API 設計
爲了統一檢索和規範 API,可在內部建立統一的倉庫,整合所有對內對外 API(可參考 googleapis/googleapis[3]、envoyproxy/data-plane-api[4]、istio/api[5])。
-
API 倉庫,方便跨部門協作。
-
版本管理,基於 git 控制。
-
規範化檢查(API lint)。
-
API design review(變更 diff)。
-
權限管理,目錄 OWNERS
gRPC
gRPC[6] 是一種高性能的開源統一 RPC 框架:
-
基於 Proto 的請求響應,支持多種語言。
-
輕量級、高性能:序列化支持 Protocol Buffer 和 JSON。
-
可插拔:支持多種插件擴展。
-
IDL:基於文件定義服務,通過 proto3 生成指定語言的數據結構、服務端接口以及客戶端 Stub(所有語言都是一致的,可代表文檔)。
-
移動端基於標準 HTTP/2 設計,支持雙向流、消息頭壓縮、單 TCP 多路複用、服務端推送等特性,使得 gRPC 在移動端設備上更加省電和網絡流量(傳輸層透明,便於升級到 HTTP/3、QUIC)。
syntax = "proto3";
package rpc_package;
service HelloWorldService {
rpc SayHello (HelloRequest) returns (HelloReply) {}
}
message HelloRequest {
string name = 1;
}
message HelloReply {
string message = 1;
}
protoc --go_out=.--go_opt=paths=source_relative \
--go-grpc_out=.--go-grpc_opt=paths=source_relative \
helloworld/helloworld.proto
設計原則:
-
服務而非對象,消息而非引用:促進微服務系統間粗粒度消息交互設計理念。
-
負載無關:不同服務使用不同的消息類型和編碼,例如 protocol buffers、JSON、XML、Thrift。
-
流:Streaming API。
-
阻塞 / 非阻塞:支持異步和同步處理在客戶端和服務端間交互消息序列。
-
元數據交換:常見的橫切關注點,如認證或跟蹤,依賴數據交換。
-
標準化狀態碼:客戶端以有限方式響應 API 調用返回的錯誤(優先使用標準的 HTTP 狀態碼)。
設計時不要過早關注性能問題,先實現標準化。
目錄結構
參考:
|-- bapis
|-- api
|-- echo
|-- v1
|-- echo.proto
|-- OWNERS 權限擁有者
|-- rpc
|-- status.proto 內部狀態碼
|-- metadata 框架元信息
|-- locale
|-- network
|-- device
|-- annotations 註解定義 options
|-- third_party 第三方引用
兼容性
維護 API 需要注意總是保持向後兼容(非破壞性)的修改:
-
爲服務添加 API(從協議的角度來看始終是安全的)。
-
爲請求消息添加字段(客戶端在新版和舊版中對字段的處理保持一致,添加請求字段就是兼容的)。
-
爲響應消息添加字段(在不改變其他字段的前提下,非資源響應消息可以擴展而不必破壞客戶端的兼容性。即使會引入冗餘,先前在響應中填充的任何字段應繼續使用相同的語義填充)。
應避免破壞性的修改(一般需要修改 major 版本號):
-
刪除或重命名服務,字段,方法或枚舉值(如果客戶端代碼可引用的內容,刪除或重命名它都是不兼容的變化)。
-
修改字段的類型(即使新類型是傳輸格式兼容的,也可能導致客戶端生成代碼發生變化,對於靜態語言而言會容易引入編譯錯誤)。
-
修改現有請求的可見行爲(客戶端通常依賴於 API 行爲和語義,即使沒有被明確支持或記錄。在大多數情況下,修改 API 數據的行爲或語義將被消費者視爲是破壞性的。如果行爲沒有加密隱藏,應該假設用戶已經發現並依賴於它)。
-
給資源消息添加讀寫字段。
命名規範
包名爲應用的標識(appid),用於生成 gRPC 請求路徑或 proto 之間引用 Message。
文件中聲明的包名稱應該與產品和服務名稱一致,帶有版本的 API 的軟件包名稱必須以此版本結尾。
參考():
請求 URL:/package_name.version.service_name/method
原始字段
gRPC 默認使用 Protobuf v3 格式,去除了 required 和 optional 關鍵字(默認全部是 optional)。沒有賦值的字段默認是基礎類型字段的默認值,比如 0 或者 “”。
// proto2
message Account {
// 必須
required string name = 1;
// 可選,默認值改爲 -1.0,有 haxXxx 方法。
optional double profit_rate = 2 [default=-1.0];
}
// proto3
message Account {
// 都是可選,默認值爲 0 和 "",無 hasXxx 方法。
string name = 1
double profit_rate = 2;
}
將無法區分默認值或未賦值。因此在 Protobuf v3 中建議使用:wrappers.proto[7]。Wrapper 類型的字段即包裝一個 message,使用時變爲指針。
message DoubleValue {
double value = 1;
}
Protobuf 作爲強 schema 約束的描述文件,也方便擴展,因此也可以用於配置文件定義。
異常處理
首先由於會爲服務監控帶來麻煩,明確禁止在 HTTP Status Code 中統一設置爲 200、在 Body 中再定義 code 字段標記具體錯誤類型的做法。
使用標準錯誤配合具體錯誤:比如服務端使用一個標準 google.rpc.Code.NOT_FOUND
錯誤代碼告知客戶端無法找到特定資源(大類:404,小類:具體資源)。
-
狀態空間變小降低了文檔的複雜性。
-
在客戶端庫中提供了更好的慣用映射,降低了邏輯複雜性。
-
不限制是否包含可操作信息(
/google/rpc/error_details
)。
錯誤傳播:如果 API 服務依賴於其他服務,不應盲目地將服務錯誤傳播到客戶端。在翻譯錯誤時建議:
-
隱藏詳細信息和機密信息。
-
調整負責該錯誤的一方。比如一個服務端從其它服務接收到 INVALID ARGUMENT 錯誤,應該將 INTERNAL 傳播給自己的調用者。
全局錯誤碼 是鬆散、契約易被破壞的,應在每個服務傳播錯誤時做一次翻譯,保證每個服務 + 錯誤枚舉是唯一的,定義在 proto 中(可作爲文檔)。
設計規則
有時接口複用會帶來歧義,比如一些字段給 A 方法用、另一些給 B 方法用;如果爲不同方法定義 struct 又會造成冗餘。
service LibraryService {
rpc UpdateBook(UpdateBookRequest) returns (Book);
}
message UpdateBookRequest { Book book = 1;}
message Book {
string name = 1;
string author = 2;
string title = 3;
bool read = 4;
}
gRPC 推薦的做法是利用 FieldMask 的部分更新:客戶端可執行需要更新的字段信息,空 FieldMask 默認應用到所有字段。
service LibraryService {
rpc UpdateBook(UpdateBookRequest) returns (Book);
}
message UpdateBookRequest {
Book book = 1;
google.protobuf.FieldMask mask = 2;
}
配置管理
通常包括以下內容:
-
環境配置:Region、Zone、Cluster、Environment、Color、Discovery、AppID、Host 等之類的環境變量信息,通過在線運行時平臺打入到容器或物理機,供 kit 庫讀取使用。比如 Dev、UAT、Preprod、Prod、DR 等環境。
-
靜態配置:即資源需要初始化的配置信息,比如 HTTP/gRPC server、Redis、MySQL 等,通常不建議運行時變更(很可能會導致業務出現不可預期的事故),變更靜態配置和發佈 bianry app 沒有區別,應該走迭代發佈流程。在設計上應考慮 協議卸載:將有狀態、需要運行時變更的業務邏輯下沉,而避免安排在接入節點層(比如 TCP Server,無狀態)。
-
動態配置:應用程序可能需要比較簡單的在線開關控制業務策略,會頻繁的調整和使用,這類用於動態變更業務流的(比如 AB Test 的 flag,一般是基礎類型 int、bool 等)配置可收歸在一起,考慮結合 expvar[8] 使用,與配置中心打通。
-
全局配置:通常各類依賴組件、中間件都有大量默認配置或指定配置,在各個項目裏大量複製容易出現意外。所以使用配置模板來定製化常用組件,在特化應用進行局部替換。
配置傳參先參考 net/http 庫:
func main() {
s := &http.Server{
Addr: ":8080",
Handler: nil,
ReadTimeout: 10 * time.Second,
WriteTimeout: 10 * time.Second,
MaxHeaderBytes: 1 << 20,
}
log.Fatal(s.ListenAndServe())
}
缺點是無法獲知修改公共字段是否會有副作用,字段的含義也要自行查閱文檔。
改進是自行設計 config struct,建議使用 functional options:
-
符合編程直覺,可實現高度的可配置化,容易維護和擴展。
-
自文檔描述,代碼可讀、容易上手。
-
代碼直觀,無歧義(比如空值)。
type Server struct {
Addr string // required
Port int // required
Protocol string // not null, default TCP
Timeout time.Duration // not null, default 30
MaxConn int // not null, default 1024
TLS *tls.Config //
}
type Option func(*Server)
func Protocol(p string) Option {
return func(s *Server) {
s.Protocol = p
}
}
func Timeout(timeout time.Duration) Option {
return func(s *Server) {
s.Timeout = timeout
}
}
func MaxConn(maxConn int) Option {
return func(s *Server) {
s.MaxConn = maxConn
}
}
func TLS(tls *tls.Config) Option {
return func(s *Server) {
s.TLS = tls
}
}
func NewServerFP(addr string, port int, options ...Option) (*Server, error) {
// 有一個可變參數 options 可以傳出多個上面的函數,for-loop 設置 Server 對象。
srv := Server{
Addr: addr,
Port: port,
Protocol: "tcp",
Timeout: 30 * time.Second,
MaxConn: 1000,
TLS: nil,
}
for _, option := range options {
option(&srv)
}
//...
return &srv, nil
}
func TestFunctionalOptions(t *testing.T) {
s1, _ := NewServerFP("localhost", 1024)
s2, _ := NewServerFP("localhost", 2048, Protocol("udp"))
s3, _ := NewServerFP("0.0.0.0", 8080, Timeout(300*time.Second), MaxConn(1000))
fmt.Println(s1, s2, s3)
}
在實踐中應注意配置文件到配置數據之間映射的解耦:
-
僅保留 options API。
-
config file 和 options struct 解耦:比如利用 gRPC 的 Protobuf 的強 schema 約束定義 Config 對象,實現語義驗證、語法高亮和 lint、格式化。
[Config Web UI] <----+---------+
| ↓
[Config API] --------+--> [Config Data] ----> [System]
| ↑
[Config Language] <--+---------+
YAML:需要先轉換成 JSON,再轉成 Protobuf。Protobuf 的 Config 對象不能直接擴展方法,所以還需要加一個 Options 方法。
func ApplyYAML(s *redis.Config, yml string) error {
js, err := yaml.YAMLToJSON([]byte(yml))
if err != nil {
return err
}
return ApplyJSON(s, string(js))
}
// Options apply config to options.
func Options(c *redis.Config) []redis.Options {
return []redis.Options{
redis.DialDatabase(c.Database),
redis.DialPassword(c.Password),
redis.DialReadTimeout(c.ReadTimeout),
}
}
Protobuf:使用 wrap struct 區分是否有值。
syntax = "proto3";
import "google/protobuf/duration.proto";
package config.redis.v1;
// redis config.
message redis {
string network = 1;
string address = 2;
int32 database = 3;
string password = 4;
google.protobuf.Duration read_timeout = 5;
}
最終實現配置注入:
func main() {
// load config file from yaml.
c := new(redis.Config)
_ = ApplyYAML(c, loadConfig())
r, _ := redis.Dial(c.Network, c.Address, Options(c)...)
}
最佳實踐
實現代碼變更系統功能是冗長且複雜的過程,往往還涉及 CR、測試等流程。而更改單個配置選項也可能對功能產生重大影響,且通常情況下修改配置還容易被忽略、未經測試就上線。
配置管理的目標:
-
避免複雜:依賴的通用基礎中間件使用配置中心支持的全局配置化模板。
-
多樣的配置:配置模板通過覆蓋某些字段實現多樣化。
-
區分必選項和可選項,向簡單化努力:儘可能減少必要的配置項(最佳實踐)。
-
以基礎設施 -> 面向用戶進行轉變。
-
配置的防禦編程。
-
權限和變更跟蹤。
-
配置的版本和應用對齊。
-
安全的配置變更:逐步部署、回滾更改、自動回滾。
模塊管理
Go 依賴管理是通過 Git 倉庫模式實現,並隨着版本的更迭逐漸完善。
早期是 GOPATH 模式:GOPATH 目錄是所有工程的公共依賴包目錄,所有需要編譯的 go 工程的依賴包都放在 GOPATH 目錄下。
後續引入多版本支持的 Vendor 特性:go 1.6 之後開啓了 vendor 目錄,以支持各個工程對於不同版本的依賴包使用的需求(每個工程拷貝一份代碼)。
Go Module 管理:Go1.11 實現了依賴包的升級更新,在 Go1.13 版本後默認打開。
GOPATH
GOPATH 爲 Go 開發環境時所設置的一個環境變量。
歷史版本的 go 語言開發時,需要將代碼放在 GOPATH 目錄的 src 文件夾下。go get 命令獲取依賴,也會自動下載到 GOPATH 的 src 下。以下命令會將代碼下載到 $GOPATH/src/github.com/foo/bar
。
go get github.com/foo/bar
GOPATH 具體結構如下,必須包含三個文件夾:
GOPATH
|-- bin 二進制文件
|-- pkg 預編譯文件(加快後續編譯速度)
|-- src 源代碼
|-- github.com
GO Modules
從 Go 1.11 開始初步支持,解決了依賴版本的信息管理,並且保證安全性 。
由 go.mod 和 go.sum 組成,包括依賴模塊路徑定義,通過 checksum 保證包的安全性,並且可以在 GOPATH 外創建和編譯項目。
使用 go mod init
命令初始化項目,生成 go.mod 文件:
go mod init example.com.hello
cat go.mod
module example.com/hello
go 1.16
使用 go get github.com/sirupsen/logrus
可下載或更新依賴包:
module example.com/hello
go 1.16
require github.com/sirupsen/logrus v1.8.1
各關鍵字含義:
-
module:定義當前項目的模塊路徑。
-
go:標識當前模塊的 Go 語言版本。
-
require:依賴包及其版本。
-
exclude:在使用中排除特定的模塊版本。
-
replace:替換 require 中聲明的依賴,使用另外的依賴及其版本號。
Checksum
爲解決 Go Modules 的包被篡改的安全隱患,引入 go.sum 文件以記錄每個依賴包的哈希值,在構建時如果本地的依賴包 hash 值與 go.sum 文件中記錄的不一致,則會拒絕構建。
-
go.sum 文件中每行記錄由 module 名、版本和哈希組成,以空格分隔。
-
引入新依賴時,通常使用
go get
命令獲取,將包下載到本地緩存目錄$GOPATH/pkg/mod/cache/download
,該包後綴爲 .zip,並把哈希運算同步到 go.sum 文件中。 -
在構建應用時,從本地緩存中查找所有 go.mod 中記錄的依賴包,並計算本地包的哈希值 ,與 go.sum 中的記錄對比,如果校驗失敗,go 命令將拒絕構建。
Proxy
Go 1.13 的 GOPROXY 默認爲 https://proxy.golang.org,在國內需要配置代理才能使用。GOPROXY[9] 也可以解決公司內部的使用問題:
-
訪問內網的 git server。
-
防止公網倉庫變更導致線上編譯失敗或者緊急回退失敗。
-
滿足公司審計和安全需要。
-
防止內部開發人員配置不當造成 import path 泄露。
-
cache 熱點依賴,降低公司公網出口帶寬。
export GOPROXY=https://goproxy.io,direct
# 不走 proxy 的私有倉庫或組,以逗號分隔。
export GOPRIVATE=git.mycompany.com,github.com/my/private
Private
用於控制 go 命令把某些倉庫視作私有倉庫,可以跳過 proxy server 和 checksum 檢查,GOPRIVATE 的值同時作爲 GONOPROXY 和 GONOSUMDB 默認值:
# 以逗號分隔。
export GOPRIVATE=*.corp.example.com,github.com/org_name
推薦同時配置 GOPROXY 和 GOPRIVATE 使用,GOPRIVATE 也可以識別 Git SSH KEY 進行權限效驗。
GOPROXY 編譯部署
goproxy.io 是 Go Modules 開源代理,也可作爲公司內部代理。
# 下載編譯:
git clone https://github.com/goproxyio/goproxy.git
cd goproxy
go build
# 運行代理:
# ./goproxy -listen=0.0.0.0:8081 -cacheDir=/tmp/cache -proxy https://goproxy.io -exclude "github.com/private"
#
# -cacheDir 指定 Go 模塊的緩存目錄
# -exclude proxy 模式下指定不經過上游服務器的 path
# -listen 服務監聽端口,默認 8081
# -proxy 指定上游 proxy server,推薦 goproxy.io
訪問內網 Git 倉庫:
-
用戶本地配置
GONOSUMDB=github.com/private
-
goproxy server 配置 exclude 進行排除所代理倉庫
-
goproxy server 配置 SSH Key,並且在倉庫添加只讀權限
-
goproxy server 配置 .gitconfig 把 ssh 替換成 http 方式訪問
[url "git@github.com:"]
insteadOf = https://github.com/
[url "git@github.com:"]
insteadOf = https://gitlab.com/
測試
小型測試帶來優秀的代碼質量、良好的異常處理、優雅的錯誤報告;大中型測試會帶來整體產品質量和數據驗證。
不同類型的項目對測試的需求不同,總體上有 70/20/10 經驗法則:70% 小型測試,20% 中型測試,10% 大型測試。
如果一個項目是面向用戶的,擁有較高的集成度或用戶接口比較複雜,就應該有更多的中型和大型測試;如果是基礎平臺或者面向數據的項目(例如索引或網絡爬蟲),則最好有大量的小型測試。
單元測試
單元測試的基本要求:
-
快速
-
環境一致
-
任意順序
-
並行
基於 docker-compose 實現跨平臺跨語言環境的容器依賴管理方案,以解決運行 unittest 場景下的容器依賴問題:
-
本地安裝 Docker。
-
無侵入式的環境初始化。
-
快速重置環境。
-
隨時隨地運行 (不依賴外部服務)。
-
語義式 API 聲明資源。
-
真實外部依賴,而非 in-process 模擬。
包含測試的項目目錄結構:
|-- service
|-- api
|-- cmd
|-- configs
|-- internal
|-- test
|-- docker-compose.yaml
|-- database.sql
要滿足以下原則:
-
正確地對容器內服務進行健康檢測,避免測試啓動時資源還未準備好。
-
應該交由 app 自己初始化數據,比如 db 的 scheme,初始 sql 數據等。爲了滿足測試的一致性,在每次結束後都會銷燬容器。
-
在單元測試開始前導入封裝好的 testing 庫,方便啓動和銷燬容器。
-
對於 service 的單元測試,使用 gomock 等庫把 mock DAO 層。在設計包時,應該面向接口編程。
-
在本地啓動依賴 Docker 容器,在 CI 環境裏執行單元測試,需要考慮物理機中的容器網絡,或在容器裏再次啓動一個 Docker。
func TestMain(m *testing.M) {
flag.Set("f", "./test/docker-compose.yaml")
flag.Parse()
if err := lich.Setup(); err != nil {
panic(err)
}
defer lich.Teardown()
if ret := m.Run(); ret != 0 {
panic(ret)
}
}
最佳實踐
利用 go 官方提供的 Subtests + Gomock 完成整個單元測試。對於每層代碼:
-
/api:更適合進行集成測試,使用 API 測試框架(YApi)維護大量業務測試 case。
-
/data:使用 docker compose 模擬底層基礎設施,可以去掉 infra 的抽象層。
-
/biz:依賴 repo、rpc client,利用 gomock 模擬 interface 實現來進行業務單元測試。
-
/service:依賴 biz 實現,構建 biz 實現類傳入進行單元測試。
一般的開發測試流程:
-
基於 git branch 進行 feature 開發。
-
開發過程,在本地執行單元測試。
-
提交 gitlab merge request 進行 CI 的單元測試。
-
基於 feature branch 進行構建。
-
完成功能測試之後合併 master。
-
上線前進行集成測試。
-
上線後進行迴歸測試。
參考
-
Package Oriented Design (ardanlabs.com)[10]
-
Design Philosophy On Packaging (ardanlabs.com)[11]
-
golang-standards/project-layout: Standard Go Project Layout (github.com)[12]
-
淺析 VO、DTO、DO、PO 的概念、區別和用處 - 隨風而逝, 只是飄零 - 博客園 (cnblogs.com)[13]
-
阿里文娛技術專家戰獒: 領域驅動設計詳解之 What, Why, How?_中生代技術 - CSDN 博客
-
阿里技術專家詳解 DDD 系列 - Domain Primitive_chikuai9995 的博客 - CSDN 博客 [14]
-
阿里技術專家詳解 DDD 系列 第二彈 - 應用架構_淘系技術 - CSDN 博客
-
阿里技術專家詳解 DDD 系列 第三講 - Repository 模式_淘系技術 - CSDN 博客 [15]
-
Errors | Cloud APIs | Google Cloud[16]
-
貧血,充血模型的解釋以及一些經驗_知識庫_博客園 (cnblogs.com)[17]
-
領域驅動設計 實踐手冊 (1.Get Started) - 知乎 (zhihu.com)[18]
-
DDD 實踐手冊 (2. 實現分層架構) - 知乎 (zhihu.com)[19]
-
DDD 實踐手冊 (3. Entity, Value Object) - 知乎 (zhihu.com)[20]
-
DDD 實踐手冊 (4. Aggregate — 聚合) - 知乎 (zhihu.com)[21]
-
DDD 實踐手冊 (5. Factory 與 Repository) - 知乎 (zhihu.com)[22]
-
DDD 實踐手冊 (6. Bounded Context - 限界上下文) - 知乎 (zhihu.com)[23]
-
01、DDD 和微服務的關係 - 簡書 (jianshu.com)[24]
-
Domain Driven Design in Go – Citerus[25]
-
Domain Driven Design in Go: Part 2 – Citerus[26]
-
Domain Driven Design in Go: Part 3 – Citerus[27]
-
文章正在審覈中… - 簡書 (jianshu.com)[28]
-
領域驅動設計系列文章(1)——通過現實例子顯示領域驅動設計的威力 - Cat Qi - 博客園 (cnblogs.com)[29]
-
領域驅動設計系列文章(2)——淺析 VO、DTO、DO、PO 的概念、區別和用處 - Cat Qi - 博客園 (cnblogs.com)[30]
-
領域驅動設計系列文章(3)——有選擇性的使用領域驅動設計 - Cat Qi - 博客園 (cnblogs.com)[31]
-
區分 Protobuf 中缺失值和默認值 - 知乎 (zhihu.com)[32]
-
protobuf/wrappers.proto at master · protocolbuffers/protobuf (github.com)[33]
-
Functional options for friendly APIs – The acme of foolishness (cheney.net)[34]
-
command center: Self-referential functions and the design of options[35]
-
Creating Good API Errors in REST, GraphQL and gRPC | APIs You Won’t Hate - A community that cares about API design and development. (apisyouwonthate.com)[36]
-
Clean Coder Blog[37]
-
GopherCon 2018: Kat Zien - How Do You Structure Your Go Apps - YouTube[38]
-
zitryss/go-sample: Go Project Sample Layout (github.com)[39]
-
paper-code/packageorienteddesign.md at master · danceyoung/paper-code (github.com)[40]
-
Clean Architecture using Golang. Update | by Elton Minetto | Medium[41]
-
Standard Package Layout. Addressing one of the biggest technical… | by Ben Johnson | Medium[42]
-
410 Deleted by author — Medium[43]
-
Trying Clean Architecture on Golang | Hacker Noon[44]
-
Trying Clean Architecture on Golang — 2 | Hacker Noon[45]
-
Applying The Clean Architecture to Go applications • Manuel Kießling (kiessling.net)[46]
-
katzien/go-structure-examples: Examples for my talk on structuring go apps (github.com)[47]
-
Ashley McNamara + Brian Ketelsen. Go best practices. - YouTube[48]
-
DTO to Entity and Entity to DTO Conversion - Apps Developer Blog[49]
-
I’ll take pkg over internal (travisjeffery.com)[50]
-
wire/best-practices.md at main · google/wire (github.com)[51]
-
wire/guide.md at main · google/wire (github.com)[52]
-
Compile-time Dependency Injection With Go Cloud’s Wire - The Go Blog (golang.org)[53]
-
google/wire: Compile-time Dependency Injection for Go (github.com)[54]
-
Integration Testing in Go: Part I - Executing Tests with Docker (ardanlabs.com)[55]
-
Integration Testing in Go: Part II - Set-up and Writing Tests (ardanlabs.com)[56]
-
Testable Examples in Go - The Go Blog (golang.org)[57]
-
Using Subtests and Sub-benchmarks - The Go Blog (golang.org)[58]
-
The cover story - The Go Blog (golang.org)[59]
-
Keeping Your Modules Compatible - The Go Blog (golang.org)[60]
-
Go Modules: v2 and Beyond - The Go Blog (golang.org)[61]
-
Publishing Go Modules - The Go Blog (golang.org)[62]
-
Module Mirror and Checksum Database Launched - The Go Blog (golang.org)[63]
-
Migrating to Go Modules - The Go Blog (golang.org)[64]
-
Using Go Modules - The Go Blog (golang.org)[65]
-
Go Modules in 2019 - The Go Blog (golang.org)[66]
-
Testing with GoMock: A Tutorial - codecentric AG Blog[67]
-
gomock · pkg.go.dev[68]
-
A GoMock Quick Start Guide. An opinionated tutorial for unit… | by Che Dan | Better Programming[69]
參考資料
[1] I’ll take pkg over internal: https://travisjeffery.com/b/2019/11/i-ll-take-pkg-over-internal/
[2] google/wire: https://github.com/google/wire
[3] googleapis/googleapis: https://github.com/googleapis/googleapis
[4] envoyproxy/data-plane-api: https://github.com/envoyproxy/data-plane-api
[5] istio/api: https://github.com/istio/api
[6] gRPC: https://grpc.io/
[7] wrappers.proto: https://github.com/protocolbuffers/protobuf/blob/master/src/google/protobuf/wrappers.proto
[8] expvar: https://pkg.go.dev/expvar
[9] https://proxy.golang.org,在國內需要配置代理才能使用。GOPROXY: https://proxy.golang.xn--org%2C-ux8is5bfzj85qrnap81iff2ccld904crhfz75bsl3a8vw.goproxy/
[10] Package Oriented Design (ardanlabs.com): https://www.ardanlabs.com/blog/2017/02/package-oriented-design.html
[11] Design Philosophy On Packaging (ardanlabs.com): https://www.ardanlabs.com/blog/2017/02/design-philosophy-on-packaging.html
[12] golang-standards/project-layout: Standard Go Project Layout (github.com): https://github.com/golang-standards/project-layout
[13] 淺析 VO、DTO、DO、PO 的概念、區別和用處 - 隨風而逝, 只是飄零 - 博客園 (cnblogs.com): https://www.cnblogs.com/zxf330301/p/6534643.html
[14] 阿里技術專家詳解 DDD 系列 - Domain Primitive_chikuai9995 的博客 - CSDN 博客: https://blog.csdn.net/chikuai9995/article/details/100723540?biz_id=102&utm_term = 阿里技術專家詳解 DDD 系列 & utm_medium=distribute.pc_search_result.none-task-blog-2~all~sobaiduweb~default-0-100723540&spm=1018.2118.3001.4449
[15] 阿里技術專家詳解 DDD 系列 第三講 - Repository 模式_淘系技術 - CSDN 博客: https://blog.csdn.net/taobaojishu/article/details/106152641
[16] Errors | Cloud APIs | Google Cloud: https://cloud.google.com/apis/design/errors
[17] 貧血,充血模型的解釋以及一些經驗_知識庫_博客園 (cnblogs.com): https://kb.cnblogs.com/page/520743/
[18] 領域驅動設計 實踐手冊 (1.Get Started) - 知乎 (zhihu.com): https://zhuanlan.zhihu.com/p/105466656
[19] DDD 實踐手冊 (2. 實現分層架構) - 知乎 (zhihu.com): https://zhuanlan.zhihu.com/p/105648986
[20] DDD 實踐手冊 (3. Entity, Value Object) - 知乎 (zhihu.com): https://zhuanlan.zhihu.com/p/106634373
[21] DDD 實踐手冊 (4. Aggregate — 聚合) - 知乎 (zhihu.com): https://zhuanlan.zhihu.com/p/107347593
[22] DDD 實踐手冊 (5. Factory 與 Repository) - 知乎 (zhihu.com): https://zhuanlan.zhihu.com/p/109048532
[23] DDD 實踐手冊 (6. Bounded Context - 限界上下文) - 知乎 (zhihu.com): https://zhuanlan.zhihu.com/p/110252394
[24] 01、DDD 和微服務的關係 - 簡書 (jianshu.com): https://www.jianshu.com/p/dfa427762975
[25] Domain Driven Design in Go – Citerus: https://www.citerus.se/go-ddd/
[26] Domain Driven Design in Go: Part 2 – Citerus: https://www.citerus.se/part-2-domain-driven-design-in-go/
[27] Domain Driven Design in Go: Part 3 – Citerus: https://www.citerus.se/part-3-domain-driven-design-in-go/
[28] 文章正在審覈中… - 簡書 (jianshu.com): https://www.jianshu.com/p/5732b69bd1a1
[29] 領域驅動設計系列文章(1)——通過現實例子顯示領域驅動設計的威力 - Cat Qi - 博客園 (cnblogs.com): https://www.cnblogs.com/qixuejia/p/10789612.html
[30] 領域驅動設計系列文章(2)——淺析 VO、DTO、DO、PO 的概念、區別和用處 - Cat Qi - 博客園 (cnblogs.com): https://www.cnblogs.com/qixuejia/p/4390086.html
[31] 領域驅動設計系列文章(3)——有選擇性的使用領域驅動設計 - Cat Qi - 博客園 (cnblogs.com): https://www.cnblogs.com/qixuejia/p/10789621.html
[32] 區分 Protobuf 中缺失值和默認值 - 知乎 (zhihu.com): https://zhuanlan.zhihu.com/p/46603988
[33] protobuf/wrappers.proto at master · protocolbuffers/protobuf (github.com): https://github.com/protocolbuffers/protobuf/blob/master/src/google/protobuf/wrappers.proto
[34] Functional options for friendly APIs – The acme of foolishness (cheney.net): https://dave.cheney.net/2014/10/17/functional-options-for-friendly-apis
[35] command center: Self-referential functions and the design of options: https://commandcenter.blogspot.com/2014/01/self-referential-functions-and-design.html
[36] Creating Good API Errors in REST, GraphQL and gRPC | APIs You Won’t Hate - A community that cares about API design and development. (apisyouwonthate.com): https://apisyouwonthate.com/blog/creating-good-api-errors-in-rest-graphql-and-grpc/
[37] Clean Coder Blog: https://blog.cleancoder.com/uncle-bob/2012/08/13/the-clean-architecture.html
[38] GopherCon 2018: Kat Zien - How Do You Structure Your Go Apps - YouTube: https://www.youtube.com/watch?v=oL6JBUk6tj0
[39] zitryss/go-sample: Go Project Sample Layout (github.com): https://github.com/zitryss/go-sample
[40] paper-code/packageorienteddesign.md at master · danceyoung/paper-code (github.com): https://github.com/danceyoung/paper-code/blob/master/package-oriented-design/packageorienteddesign.md
[41] Clean Architecture using Golang. Update | by Elton Minetto | Medium: https://eminetto.medium.com/clean-architecture-using-golang-b63587aa5e3f
[42] Standard Package Layout. Addressing one of the biggest technical… | by Ben Johnson | Medium: https://medium.com/@benbjohnson/standard-package-layout-7cdbc8391fc1
[43] 410 Deleted by author — Medium: https://medium.com/wtf-dial/wtf-dial-domain-model-9655cd523182
[44] Trying Clean Architecture on Golang | Hacker Noon: https://hackernoon.com/golang-clean-archithecture-efd6d7c43047
[45] Trying Clean Architecture on Golang — 2 | Hacker Noon: https://hackernoon.com/trying-clean-architecture-on-golang-2-44d615bf8fdf
[46] Applying The Clean Architecture to Go applications • Manuel Kießling (kiessling.net): https://manuel.kiessling.net/2012/09/28/applying-the-clean-architecture-to-go-applications/
[47] katzien/go-structure-examples: Examples for my talk on structuring go apps (github.com): https://github.com/katzien/go-structure-examples
[48] Ashley McNamara + Brian Ketelsen. Go best practices. - YouTube: https://www.youtube.com/watch?v=MzTcsI6tn-0
[49] DTO to Entity and Entity to DTO Conversion - Apps Developer Blog: https://www.appsdeveloperblog.com/dto-to-entity-and-entity-to-dto-conversion/
[50] I’ll take pkg over internal (travisjeffery.com): https://travisjeffery.com/b/2019/11/i-ll-take-pkg-over-internal/
[51] wire/best-practices.md at main · google/wire (github.com): https://github.com/google/wire/blob/main/docs/best-practices.md
[52] wire/guide.md at main · google/wire (github.com): https://github.com/google/wire/blob/main/docs/guide.md
[53] Compile-time Dependency Injection With Go Cloud’s Wire - The Go Blog (golang.org): https://blog.golang.org/wire
[54] google/wire: Compile-time Dependency Injection for Go (github.com): https://github.com/google/wire
[55] Integration Testing in Go: Part I - Executing Tests with Docker (ardanlabs.com): https://www.ardanlabs.com/blog/2019/03/integration-testing-in-go-executing-tests-with-docker.html
[56] Integration Testing in Go: Part II - Set-up and Writing Tests (ardanlabs.com): https://www.ardanlabs.com/blog/2019/10/integration-testing-in-go-set-up-and-writing-tests.html
[57] Testable Examples in Go - The Go Blog (golang.org): https://blog.golang.org/examples
[58] Using Subtests and Sub-benchmarks - The Go Blog (golang.org): https://blog.golang.org/subtests
[59] The cover story - The Go Blog (golang.org): https://blog.golang.org/cover
[60] Keeping Your Modules Compatible - The Go Blog (golang.org): https://blog.golang.org/module-compatibility
[61] Go Modules: v2 and Beyond - The Go Blog (golang.org): https://blog.golang.org/v2-go-modules
[62] Publishing Go Modules - The Go Blog (golang.org): https://blog.golang.org/publishing-go-modules
[63] Module Mirror and Checksum Database Launched - The Go Blog (golang.org): https://blog.golang.org/module-mirror-launch
[64] Migrating to Go Modules - The Go Blog (golang.org): https://blog.golang.org/migrating-to-go-modules
[65] Using Go Modules - The Go Blog (golang.org): https://blog.golang.org/using-go-modules
[66] Go Modules in 2019 - The Go Blog (golang.org): https://blog.golang.org/modules2019
[67] Testing with GoMock: A Tutorial - codecentric AG Blog: https://blog.codecentric.de/en/2017/08/gomock-tutorial/
[68] gomock · pkg.go.dev: https://pkg.go.dev/github.com/golang/mock/gomock
[69] A GoMock Quick Start Guide. An opinionated tutorial for unit… | by Che Dan | Better Programming: https://betterprogramming.pub/a-gomock-quick-start-guide-71bee4b3a6f1?gi=e44758036c10
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/ezQyI4053Uc-IByFaQPfow