Standard Go Project Layout
如果你嘗試學習 Go, 或者你正在為自己建立一個 PoC 或一個玩具項目, 這個項目佈局是沒啥必要的. 從一些非常簡單的事情開始(一個 main.go 文件綽綽有餘). 當有更多的人參與這個項目時, 你將需要更多的結構, 包括需要一個 toolkit 來方便生成項目的模板, 盡可能大家統一的工程目錄佈局
如果需要命名、格式與程式碼風格方面的協助, 請使用 gofmt 和 golint.
也請確保你閱讀過以下這些 Go 程式碼撰寫風格的指導方針與建議:
- https://talks.golang.org/2014/names.slide
- https://golang.org/doc/effective_go.html#names
- https://blog.golang.org/package-names
- https://github.com/golang/go/wiki/CodeReviewComments
參見 Go Project Layout 了解更多的背景資訊.
更多關於套件的命名與組織方式, 以及其他程式碼結構的建議:
- GopherCon EU 2018: Peter Bourgon - Best Practices for Industrial Programming
- GopherCon Russia 2018: Ashley McNamara + Brian Ketelsen - Go best practices.
- GopherCon 2017: Edward Muller - Go Anti-Patterns
- GopherCon 2018: Kat Zien - How Do You Structure Your Go Apps
這裡有份中文的文件可供參考:Go 面向包的设计和架构分层
Source from: https://github.com/golang-standards/project-layout/blob/master/README_zh-TW.md#pkg
.
├── LICENSE.md
├── Makefile
├── README.md
├── README_es.md
├── README_fr.md
├── README_it.md
├── README_ja.md
├── README_ko.md
├── README_ptBR.md
├── README_ro.md
├── README_ru.md
├── README_tr.md
├── README_zh-CN.md
├── README_zh-TW.md
├── README_zh.md
├── api
│ └── README.md
├── assets
│ └── README.md
├── build
│ ├── README.md
│ ├── ci
│ └── package
├── cmd
│ ├── README.md
│ └── _your_app_
├── configs
│ └── README.md
├── deployments
│ └── README.md
├── docs
│ └── README.md
├── examples
│ └── README.md
├── githooks
│ └── README.md
├── go.mod
├── init
│ └── README.md
├── internal
│ ├── README.md
│ ├── app
│ │ └── _your_app_
│ └── pkg
│ └── _your_private_lib_
├── pkg
│ ├── README.md
│ └── _your_public_lib_
├── scripts
│ └── README.md
├── test
│ └── README.md
├── third_party
│ └── README.md
├── tools
│ └── README.md
├── vendor
│ └── README.md
├── web
│ ├── README.md
│ ├── app
│ ├── static
│ └── template
└── website
└── README.md
Go 目錄結構
/cmd
本專案的主要應用程式.
每個應用程式的目錄名應該與你的執行檔案名稱一致 (例如:/cmd/myapp).
不要在這個目錄下放置太多程式碼.如果你認為某些程式碼也可以從其他應用程式或專案中匯入使用, 那麼這些程式碼應該位於 /pkg 目錄中.如果程式碼不是可重複利用的, 或者你不希望其他人使用它, 請將該程式碼放到 /internal 目錄下.你未來將會驚訝的發現別人是怎麼使用你的程式碼, 所以請現在就明確的表達你的意圖!
通常主要應用程式只會有一個小小的 main 函式, 然後大部分的程式都是從 /internal 和 /pkg 匯入呼叫並執行, 除此之外應該什麼都沒有!
請查看 /cmd 目錄獲得更多範例.
微服務中的 app 服務類型分為4類:interface、service、job、admin.
- interface: 對外的 BFF 服務, 接受來自用戶的請求, 比如暴露了 HTTP/gRPC 接口.
- service: 對內的微服務, 僅接受來自內部其他服務或者網關的請求, 比如暴露了gRPC 接口只對內服務.
- admin:區別於 service, 更多是面向運營側的服務, 通常數據權限更高, 隔離帶來更好的代碼級別安全.
- job: 流式任務處理的服務, 上游一般依賴 message broker , 常駐的.
- task: 定時任務, 類似 cronjob, 部署到 task 託管平台中.
cmd 應用目錄負責程序的: 啟動、關閉、配置初始化等

/internal
私有應用程式和函式庫的程式碼, 是你不希望其他人在其應用程式或函式庫中匯入的程式碼.請注意:這個目錄結構是由 Go 編譯器本身所要求的.有關更多細節, 請參閱 Go 1.4 的 release notes.注意:這個目錄並不侷限於放在專案最上層的 internal 目錄.事實上, 你在專案目錄下的任何子目錄都可以包含 internal 目錄.
你可以選擇性的加入一些額外的目錄結構到你的內部套件(internal package)中, 用來區分你想「共用」與「非共用」的內部程式碼(internal code).這不是必要的(尤其是對小型專案來說), 但有視覺上的線索來表達套件的共用意圖來說, 肯定會更好(nice to have).你的應用程式程式碼可以放在 /internal/app 目錄下 (例如:/internal/app/myapp), 而這些應用程式共享的程式碼就可以放在 /internal/pkg 目錄下 (例如:/internal/pkg/myprivlib).

/pkg
函式庫的程式碼當然可以讓外部應用程式來使用 (例如:/pkg/mypubliclib), 其他專案會匯入這些函式庫, 並且期待它們能正常運作, 所以要把程式放在這個目錄下請多想個幾遍!:-) 注意:使用 internal 目錄可以確保私有套件不會被匯入到其他專案使用, 因為它是由 Go 的編譯器強制執行的, 所以是比較好的解決方案.使用 /pkg 目錄仍然是一種很好的方式, 它代表其他專案可以安全地使用這個目錄下的程式碼.由 Travis Jeffery 撰寫的 I'll take pkg over internal 文章提供了關於 pkg 和 internal 目錄很好的概述, 以及使用它們的時機點.
, 這使得運行各種 Go 工具變得更加容易(正如以下這些演講中提到的那樣:來自 GopherCon EU 2018 的 Best Practices for Industrial Programming、GopherCon 2018: Kat Zien - How Do You Structure Your Go Apps 和 GoLab 2018 - Massimiliano Pippi - Project layout patterns in Go).
如果你想查看哪些知名的 Go 專案使用本專案的目錄結構, 請查看 /pkg 目錄.這是一組常見的目錄結構, 但並不是所有人都接受它, 有些 Go 社群的人也不推薦使用.
如果你的應用程式專案真的很小, 或是套用這些資料夾不會對你有太大幫助(除非你真的很想用XD), 不使用本專案推薦的目錄結構是完全沒問題的.當你的專案變的越來越大, 根目錄將會會變得越來越複雜(尤其是當你有許多不是 Go 所寫的元件時), 你可以考慮參考這個專案所建議的目錄結構來組織你的程式碼.

/vendor
應用程式的相依套件可透過手動管理, 或使用你喜歡的相依性套件管理工具, 例如內建的 Go Modules 特性.使用 go mod vendor 命令可以幫你建立一個 /vendor 目錄.請注意:如果你不是用 Go 1.14+ 版本的話, 你可能需要在執行 go build 的時候增加 -mod=vendor 命令列參數.從 Go 1.14 開始, 這個參數預設就是啟用的.
如果你正在建立一個函式庫套件, 那麼請不要將你應用程式的相依套件加入版控!
注意:從 Go 1.13 開始, Go 預設啟用了模組的代理伺服器 (module proxy) 功能 (預設使用 https://proxy.golang.org 作為模組的代理伺服器).你可以從這裡查看這功能是否符合你的需求與限制.如果你可以使用 module proxy 的話, 那麼你根本不需要 vendor 目錄.
服務應用程式目錄 (Service Application Directories)
/api
OpenAPI/Swagger 規格、JSON schema 檔案、各種協定定義檔.
請查看 /api 目錄獲得更多範例.
API 協議定義目錄, xxapi.proto protobuf 文件, 以及生成的 go 文件.我們通常把 api 文檔直接在 proto 文件中描述.
Web 應用程式目錄 (Web Application Directories)
/web
Web 應用程式相關的元件:靜態 Web 檔案、伺服器端範本與 SPAs 相關檔案.
通用應用程式目錄 (Common Application Directories)
/configs
組態設定的檔案範本或預設設定.
將你的 confd 或 consul-template 範本檔案放在這裡.
/init
放置 System init (systemd, upstart, sysv) 與 Process manager/supervisor (runit, supervisor) 相關設定.
/scripts
放置要執行各種建置、安裝、分析等操作的命令腳本!
這些腳本可以讓你在根目錄的 Makefile 更小、更簡單(例如:https://github.com/hashicorp/terraform/blob/master/Makefile).
請查看 /scripts 目錄獲得更多範例.
/build
封裝套件與持續整合(CI).
將你的雲端 (AMI)、容器 (Docker)、OS (deb, rpm, pkg) 套件的組態設定與腳本放在 /build/package 目錄下.
將你的 CI (Travis CI, CircleCI, Drone CI) 的組態設定與腳本放在 /build/ci 目錄中.請注意:有些 CI 工具 (例如 Travis CI 等), 它們對這些組態設定檔案的位置非常挑剔.如果可能的話, 請嘗試將檔案放在 /build/ci 目錄中, 並連結 (linking) 這些檔案到 CI 工具期望它們出現的位置.
/deployments
IaaS、PaaS、系統和容器編配部署的組態設定與範本 (docker-compose、kubernetes/helm、mesos、terraform、bosh).注意:在某些儲存庫中(特別是那些部署在 kubernetes 的應用程式), 這個目錄會被命名為 /deploy.
/test
額外的外部測試應用程式和測試資料.你可以自在的調整你在 /test 目錄中的結構.對於較大的專案來說, 通常會有一個 data 資料夾也是蠻正常的.例如:如果你需要 Go 忽略這些目錄下的檔案, 你可以使用 /test/data 或 /test/testdata 當作你的目錄名稱.請注意:Go 還會忽略以 . 或 _ 開頭的目錄或檔案, 所以你在測試資料的目錄命名上, 將擁有更大的彈性.
請查看 /test 目錄獲得更多範例.
其他目錄
/docs
設計和使用者文件 (除了 godoc 自動產生的文件之外).
請查看 /docs 目錄獲得更多範例.
/tools
這個專案的支援工具.注意:這些工具可以從 /pkg 和 /internal 目錄匯入程式碼.
請查看 /tools 目錄獲得更多範例.
/examples
放置關於你的應用程式或公用函式庫的使用範例.
請查看 /examples 目錄獲得更多範例.
/third_party
外部輔助工具、Forked 程式碼, 以及其他第三方工具 (例如:Swagger UI).
/githooks
Git hooks.
/assets
其他要一併放入儲存庫的相關檔案 (例如圖片、Logo、... 等等).
/website
如果你不使用 GitHub Pages 的話, 這裡可以放置專案的網站相關資料.
請查看 /website 目錄獲得更多範例.
你不應該擁有的目錄
/src
不應該包含:/src
有些 Go 專案確實擁有一個 src 資料夾, 但這通常發生在開發人員有 Java 背景, 這在 Java 的世界很常見.如果你可以嘗試不要採用這種 Java 常見的資料夾的話, 你應該不希望你的 Go 程式碼或 Go 專案看起來像 Java 吧! :-)
不要將專案層級的 /src 目錄與 How to Write Go Code 所描述的 /src 混為一談.$GOPATH 環境變數指向你目前的工作區 (workspace)(非 Windows 的作業環境預設指向 $HOME/go), 這個工作區包含了最上層的 /pkg、/bin 和 /src 目錄.你的實際專案最終其實是放在 /src 下的一個子目錄, 所以你的專案路徑大概會長這樣:/some/path/to/workspace/src/your_project/src/your_code.go.注意:雖然 Go 1.11 可以將專案放在 GOPATH 之外, 但這並不意味著使用這種目錄結構模式是一個好主意!
Kit Project Layout
每個公司都應當為不同的微服務建立一個統一的 kit 工具包項目(基礎庫/框架) 和 app 項目. 基礎庫 kit 為獨立項目, 公司級建議只有一個, 按照功能目錄來拆分會帶來不少的管理工作, 因此建議合併整合.
Package Oriented Design : https://www.ardanlabs.com/blog/2017/02/package-oriented-design.html
The Kit project is not allowed to have a vendor folder. If any of packages are dependent on 3rd party packages, they must always build against the latest version of those dependences.
kit 項目必須具備的特點:
- 統一
- 標準庫方式佈局
- 高度抽象
- 支持插件

Server Application Project v1 (Old)
我們老的佈局, app 目錄下有 api、cmd、configs、internal 目錄, 目錄裡一般還會放置 README、CHANGELOG、OWNERS.
- api: 放置 API 定義(protobuf), 以及對應的生成的 client 代碼, 基於 pb 生成的 swagger.json.
- configs: 放服務所需要的配置文件, 比如database.yaml、redis.yaml、application.yaml.
- internal: 是為了避免有同業務下有人跨目錄引用了內部的 model、dao 等內部 struct.
- server: 放置 HTTP/gRPC 的路由代碼, 以及 DTO 轉換的代碼.
DTO(Data Transfer Object):數據傳輸對象, 這個概念來源於J2EE 的設計模式.但在這裡, 泛指用於展示層/API 層與服務層(業務邏輯層)之間的數據傳輸對象. (api -> server -> service)

項目的依賴路徑為: model -> dao -> service -> api, model struct 串聯各個層,直到 api 需要做 DTO 對象轉換。
- model: 放對應"存儲層"的結構體,是對存儲的一一影射。
- dao(data access object): 數據讀寫層,數據庫和緩存全部在這層統一處理,包括 cache miss 處理。
- service: 組合各種數據訪問來構建業務邏輯。
- server: 依賴 proto 定義的服務作為入參,提供快捷的啟動服務全局方法。
- api: 定義了 API proto 文件,和生成的 stub 代碼,它生成的 interface,其實現者在 service 中。
service 的方法簽名因為實現了 API 的 接口定義,DTO 直接在業務邏輯層直接使用了,更有 dao 直接使用,最簡化代碼。 DO(Domain Object): 領域對象,就是從現實世界中抽像出來的有形或無形的業務實體。 缺乏 DTO -> DO 的對象轉換, 這邏輯在API->Service做

example:
// mysql table
type Staff struct {
Name string
Age int
}
type (s *Staff) Retire() bool{
return s.Age >= 65
}
type Service struct {
dao Dao
}
func NewService(dao Dao) *Service {
return &Service{dao: dao}
}
type Dao interface {
Query(id int) (Staff, error)
}
func NewDao(db *sql.DB) Dao {
return &dao{db: db}
}
type dao struct{
db *sql.DB
}
func(d *Dao) Query(id int)(Staff, error){
// SELECT name from Staff WHERE id = @id
}
type testDao struct{}
func (dao *testDao) Query(id int)(Staff, error){
return Staff{Name:"kk",age:"18"}, nil
}
此範例實現了 依賴反轉原則 (Dependency inversion principle,DIP)
- 上層模塊不應該依賴於下層模塊, 他們共同依賴於一個抽象
- High-level modules should not depend on low-level modules. Both should depend on abstractions.
- 抽象不能依賴於具象, 具象依賴於抽象. 抽象不應該由低階模塊定義
- Abstractions should not depend on details.
- 低階模塊的實作內容應該依照抽象的定義去打造.
- Details should depend on abstractions. 高階與低階,是相對關係,其實也就是 呼叫者 (Caller) 與 被呼叫者 (Callee)。 越高階的模組接近商業邏輯(電子商務買賣交易流程等),越低階的模組越接近實作邏輯(讀寫資料庫的、計算金額邏輯等)
dao 依賴(call) model, service 依賴 dao, server 依賴 service dao : Caller model : Callee
DIP example : https://github.com/fkcs/Go-Design-Pattern/blob/main/DesighPrinciple/03%20DependencyInversion.go https://go.dev/play/p/TVgYw5h4jrw
package main
import "fmt"
// 依賴反轉原則
// 交通工具接口
type Drive interface {
drive()
}
type Bike struct{}
func (b *Bike) drive() {
fmt.Println("drive by Bike!")
}
type Car struct{}
func (c *Car) drive() {
fmt.Println("drive by Car!")
}
// Person 類
type Person struct {
drive Drive
}
func NewPerson() *Person {
return &Person{
drive: new(Bike),
}
}
func (p *Person) DriveTool() {
p.drive.drive()
}
/*
Person中使用接口Drive來定義交通工具,出行依賴的是交通工具接口,而不是具體的交通工具。
這樣,可以自由選擇交通工具,只要交通工具實現該接口,並將其傳遞給Person類中的Drive接口。
好處:可擴展性好,以後添加其他交通工具不影響代碼實現,即抽像不依賴於細節。
*/
/*
控制反轉
反轉:在沒有使用框架之前,程序員自己控制整個程序的執行,當使用框架之後,整個程序的執行可以通過框架來控制;
框架提供了可擴展的代碼骨架,用來組裝對象/管理整個執行流程,程序只需關注擴展點,就可以利用框架來驅動整個程序流程的執行。
*/
func NewPersonx(drive Drive) *Person {
return &Person{
drive: drive,
}
}
func main() {
p := NewPerson()
p.DriveTool() // drive by Bike!
p2 := NewPersonx(new(Car))
p2.DriveTool() // drive by Car!
}
Server Application Project v2
app 目錄下有 api、cmd、configs、internal 目錄, 目錄裡一般還會放置 README、CHANGELOG、OWNERS。
- internal: 是為了避免有同業務下有人跨目錄引用了內部的 biz、data、service 等內部 struct。
- biz: 業務邏輯的組裝層,類似 DDD 的 domain 層,data 類似 DDD 的 repo,repo 接口在這裡定義,使用依賴反轉的原則。
- data: 業務數據訪問,包含 cache、db 等封裝,實現了 biz 的 repo 接口。我們可能會把 data 與 dao 混淆在一起,data 偏重業務的含義,它所要做的是將領域對象重新拿出來,我們去掉了 DDD 的 infra層。
- service: 實現了 api 定義的服務層,類似 DDD 的 application 層,處理 DTO(Data Transfer Object) 到 biz 領域實體的轉換(DTO -> DO),同時協同各類 biz 交互,但是不應處理複雜邏輯。
PO(Persistent Object): 持久化對象,它跟持久層(通常是關係型數據庫)的數據結構形成一一對應的映射關係,如果持久層是關係型數據庫,那麼數據表中的每個字段(或若干個)就對應 PO 的一個(或若干個)屬性。 https://github.com/facebook/ent

Lifycycle
Lifecycle 需要考慮服務應用的對像初始化以及生命週期的管理, 所有 HTTP/gRPC 依賴的前置資源初始化,包括 data、biz、service,之後再啟動監聽服務。 我們使用 https://github.com/google/wire ,來管理所有資源的依賴注入(dependency injection, DI)。為何需要依賴注入?
依賴注入(dependency injection,縮寫為DI)是一種軟體設計模式,也是實現控制反轉的其中一種技術。 這種模式能讓一個物件接收它所依賴的其他物件。 「依賴」是指接收方所需的物件。 「注入」是指將「依賴」傳遞給接收方的過程。
核心是為了:
- 方便測試
- 單次初始化和複用
class RedisList:
def __init__(self, host, port, password):
self._client = redis.Redis(host, port, password)
def push(self, key, val):
self._client.lpush(key, val)
l = RedisList(host, port, password)
依賴反轉之後變成
class RedisList:
def __init__(self, redis_client):
self._client = redis_client
def push(self, key, val):
self._client.lpush(key, val)
redis_client = get_redis_client(...)
l = RedisList(redis_client)
Wire
wire是google開源的依賴注入框架。或者引用官方的話來說: "Wire is a code generation tool that automates connecting components using dependency injection."
Installing
go install github.com/google/wire/cmd/wire@latest
手擼資源的初始化和關閉是非常繁瑣,容易出錯的。 上面提到我們使用依賴注入的思路 DI,結合 google wire, wire 採用生成代碼的的方式来達到編譯時依賴注入(compile-time dependency injection) 靜態的 go generate 生成靜態的代碼,可以在很方便診斷和查看,不是在運行時利用 reflection 實現。
Why wire?
除了wire,Go的依賴注入框架還有Uber的dig和Facebook的inject,它們都是使用反射機制來實現運行時依賴注入(runtime dependency injection), 而wire則是採用代碼生成的方式來達到編譯時依賴注入(compile-time dependency injection)。 使用反射帶來的性能損失倒是其次,更重要的是反射使得代碼難以追踪和調試(反射會令Ctrl+左鍵失效…)。 而wire生成的代碼是符合程序員常規使用習慣的代碼,十分容易理解和調試。 關於wire的優點,在官方博文上有更詳細的的介紹:blog.golang.org/wire
Provider & Injector
provider和injector是wire的兩個核心概念。
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函數,讓wire知道如何產生這些依賴對象。 wire根據我們定義的injector函數簽名,生成完整的injector函數,injector函數是最終我們需要的函數,它將按依賴順序調用provider。
provider
provider就是普通的Go函數,可以把它看作是某對象的構造函數,我們通過provider告訴wire該對象的依賴情況:
// NewUserStore是*UserStore的provider,表明*UserStore依賴於*Config和 *mysql.DB.
func NewUserStore(cfg *Config, db *mysql.DB) (*UserStore, error) {...}
// NewDefaultConfig是*Config的provider,沒有依賴
func NewDefaultConfig() *Config {...}
// NewDB是*mysql.DB的provider,依賴於ConnectionInfo
func NewDB(info ConnectionInfo) (*mysql.DB, error) {...}
// UserStoreSet 可選項,可以使用wire.NewSet將通常會一起使用的依賴組合起來。
var UserStoreSet = wire.NewSet(NewUserStore, NewDefaultConfig)
injector
injector是wire生成的函數,我們通過調用injector來獲取我們所需的對像或值,injector會按照依賴關係,按順序調用provider函數:
// File: wire_gen.go
// Code generated by Wire. DO NOT EDIT.
//go:generate wire
//+build !wireinject
// initUserStore是由wire生成的injector
func initUserStore(info ConnectionInfo) (*UserStore, error) {
// *Config的provider函數
defaultConfig := NewDefaultConfig()
// *mysql.DB的provider函數
db, err := NewDB(info)
if err != nil {
return nil, err
}
// *UserStore的provider函數
userStore, err := NewUserStore(defaultConfig, db)
if err != nil {
return nil, err
}
return userStore, nil
}
injector 幫我們把按順序初始化依賴的步驟給做了,我們在main.go中只需要調用initUserStore方法就能得到我們想要的對象了。
那麼wire是怎麼知道如何生成injector的呢?我們需要寫一個函數來告訴它:
- 定義
injector的函數簽名 - 在函數中使用
wire.Build方法列舉生成injector所需的provider
例如:
//go:build wireinject
// +build wireinject
// initUserStore用於聲明injector的函數簽名
func initUserStore(info ConnectionInfo) (*UserStore, error) {
// wire.Build聲明要獲取一個UserStore需要調用到哪些provider函數
wire.Build(UserStoreSet, NewDB)
return nil, nil // 這些返回值wire並不關心。
}
有了上面的函數,wire就可以得知如何生成injector了。 wire生成injector的步驟描述如下:
- 確定所生成
injector函數的函數簽名:func initUserStore(info ConnectionInfo) (*UserStore, error) - 感知返回值第一個參數是
*UserStore - 檢查wire.Build列表,找到
*UserStore的provider:NewUserStore - 由函數簽名
func NewUserStore(cfg *Config, db *mysql.DB)得知NewUserStore依賴於*Config, 和*mysql.DB - 檢查
wire.Build列表,找到*Config和*mysql.DB的provider:NewDefaultConfig和NewDB - 由函數簽名
func NewDefaultConfig() *Config得知*Config沒有其他依賴了。 - 由函數簽名
func NewDB(info *ConnectionInfo) (*mysql.DB, error)得知*mysql.DB依賴於ConnectionInfo。 - 檢查
wire.Build列表,找不到ConnectionInfo的provider,但在injector函數簽名中發現匹配的入參類型,直接使用該參數作為NewDB的入參。 - 感知返回值第二個參數是error
- …
- 按依賴關係,按順序調用
provider函數,拼裝injector函數。
Tutorial : https://github.com/google/wire/blob/main/_tutorial/README.md
創建一個小程序來模擬一個事件,迎賓員用特定的消息向客人致意
Without Wire
create three types:
- a message for a greeter
- a greeter who conveys that message
- an event that starts with the greeter greeting guests
type Message string
type Greeter struct {
// ... TBD
}
type Event struct {
// ... TBD
}
func NewMessage() Message {
return Message("Hi there!")
}
我們Greeter將需要參考Message. 所以讓我們也為我們創建一個初始化器Greeter。
func NewGreeter(m Message) Greeter {
return Greeter{Message: m}
}
type Greeter struct {
Message Message // <- adding a Message field
}
func (g Greeter) Greet() Message {
return g.Message
}
Event 需要有一個 Greeter
func NewEvent(g Greeter) Event {
return Event{Greeter: g}
}
type Event struct {
Greeter Greeter // <- adding a Greeter field
}
func (e Event) Start() {
msg := e.Greeter.Greet()
fmt.Println(msg)
}
func main() {
message := NewMessage()
greeter := NewGreeter(message)
event := NewEvent(greeter)
event.Start()
}
With Wire
func main() {
e := InitializeEvent()
e.Start()
}
// wire.go
func InitializeEvent() Event {
wire.Build(NewEvent, NewGreeter, NewMessage)
return Event{}
}
調用wire命令生成依賴文件:
// wire_gen.go
func InitializeEvent() Event {
message := NewMessage()
greeter := NewGreeter(message)
event := NewEvent(greeter)
return event
}
完整程式
main.go
// Copyright 2018 The Wire Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// The greeter binary simulates an event with greeters greeting guests.
package main
import (
"errors"
"fmt"
"os"
"time"
)
// Message is what greeters will use to greet guests.
type Message string
// NewMessage creates a default Message.
func NewMessage(phrase string) Message {
return Message(phrase)
}
// NewGreeter initializes a Greeter. If the current epoch time is an even
// number, NewGreeter will create a grumpy Greeter.
func NewGreeter(m Message) Greeter {
var grumpy bool
if time.Now().Unix()%2 == 0 {
grumpy = true
}
return Greeter{Message: m, Grumpy: grumpy}
}
// Greeter is the type charged with greeting guests.
type Greeter struct {
Grumpy bool
Message Message
}
// Greet produces a greeting for guests.
func (g Greeter) Greet() Message {
if g.Grumpy {
return Message("Go away!")
}
return g.Message
}
// NewEvent creates an event with the specified greeter.
func NewEvent(g Greeter) (Event, error) {
if g.Grumpy {
return Event{}, errors.New("could not create event: event greeter is grumpy")
}
return Event{Greeter: g}, nil
}
// Event is a gathering with greeters.
type Event struct {
Greeter Greeter
}
// Start ensures the event starts with greeting all guests.
func (e Event) Start() {
msg := e.Greeter.Greet()
fmt.Println(msg)
}
func main() {
e, err := InitializeEvent("hi there!")
if err != nil {
fmt.Printf("failed to create event: %s\n", err)
os.Exit(2)
}
e.Start()
}
wire.go
// Copyright 2018 The Wire Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//go:build wireinject
// +build wireinject
// The build tag makes sure the stub is not built in the final build.
package main
import "github.com/google/wire"
// InitializeEvent creates an Event. It will error if the Event is staffed with
// a grumpy greeter.
func InitializeEvent(phrase string) (Event, error) {
wire.Build(NewEvent, NewGreeter, NewMessage)
return Event{}, nil
}
wire_gen.go
// Code generated by Wire. DO NOT EDIT.
//go:generate go run -mod=mod github.com/google/wire/cmd/wire
//go:build !wireinject
// +build !wireinject
package main
// Injectors from wire.go:
func InitializeEvent(phrase string) (Event, error) {
message := NewMessage(phrase)
greeter := NewGreeter(message)
event, err := NewEvent(greeter)
if err != nil {
return Event{}, err
}
return event, nil
}
使用wire前 vs 後
// 使用wire前
func main() {
message := NewMessage()
greeter := NewGreeter(message)
event := NewEvent(greeter)
event.Start()
}
// 使用wire後
func main() {
e, err := InitializeEvent("hi there!")
if err != nil {
fmt.Printf("failed to create event: %s\n", err)
os.Exit(2)
}
e.Start()
}
NewMessage,NewGreeter,NewEvent 都是 provider ,wire_gen.go中的 InitializeEvent 函数是 injector,可以看到 injector 通過按依賴順序調用provider 来生成我們需要的對象 Event。
上述範例在wire.go中定義了injector的函數簽名, 注意要在文件第一行加上:
// +build wireinject
...
用於告訴編譯器無需編譯該文件。在injector的簽名定義函數中,通過調用wire.Build方法,指定用於生成依賴的provider:
// InitializeEvent 聲明injector的函數簽名
func InitializeEvent(msg string) Event{
wire.Build(NewEvent, NewGreeter, NewMessage) // <--- 傳入provider函數
return Event{} //返回值沒有實際意義,只需符合函數簽名即可
}
高級特性
1. binding interfaces
根據依賴倒置原則(Dependence Inversion Principle, DIP),對象應當依賴於接口,而不是直接依賴於具體實現。
抽象成接口依賴更有助於單元測試哦! 搞定Go單元測試(一)——基礎原理 搞定Go單元測試(二)—— mock框架(gomock)
現在我們來看看在wire中如何處理接口依賴:
// UserService
type UserService struct {
userRepo UserRepository // <-- UserService依賴UserRepository接口
}
// UserRepository 存放User對象的數據倉庫接口,比如可以是mysql,restful api ....
type UserRepository interface {
// GetUserByID 根據ID獲取User, 如果找不到User返回對應錯誤信息
GetUserByID(id int) (*User, error)
}
// NewUserService *UserService構造函數
func NewUserService(userRepo UserRepository) *UserService {
return &UserService{
userRepo:userRepo,
}
}
// mockUserRepo 模擬一個UserRepository實現
type mockUserRepo struct {
foo string
bar int
}
// GetUserByID UserRepository接口實現
func (u *mockUserRepo) GetUserByID(id int) (*User,error){
return &User{}, nil
}
// NewMockUserRepo *mockUserRepo構造函數
func NewMockUserRepo(foo string,bar int) *mockUserRepo {
return &mockUserRepo{
foo:foo,
bar:bar,
}
}
// MockUserRepoSet 將 *mockUserRepo與UserRepository綁定
var MockUserRepoSet = wire.NewSet(NewMockUserRepo,wire.Bind(new(UserRepository), new(*mockUserRepo)))
在這個例子中,UserService依賴UserRepository接口,其中mockUserRepo是UserRepository的一個實現,
由於在Go的最佳實踐中,更推薦返回具體實現而不是接口。所以mockUserRepo的provider函數返回的是*mockUserRepo這一具體類型。 wire無法自動將具體實現與接口進行關聯,
我們需要顯示聲明它們之間的關聯關係。通過wire.NewSet和wire.Bind將*mockUserRepo與UserRepository進行綁定:
// MockUserRepoSet 將 *mockUserRepo與UserRepository綁定
var MockUserRepoSet = wire.NewSet(NewMockUserRepo,wire.Bind(new(UserRepository), new(*mockUserRepo)))
定義injector函數簽名:
// ...
func InitializeUserService(foo string, bar int) *UserService{
wire.Build(NewUserService,MockUserRepoSet) // 使用MockUserRepoSet
return nil
}
// ...
範例 : https://github.com/DrmagicE/wire-examples/tree/master/advance-features/binding-interfaces
2. return error
在前面的例子中,我們的provider函數均只有一個返回值,但在某些情況下,provider函數可能會對入參做校驗,如果參數錯誤,則需要返回error。 wire也考慮了這種情況,provider函數可以將返回值的第二個參數設置成error:
// Config 配置
type Config struct {
// RemoteAddr 連接的遠程地址
RemoteAddr string
}
// APIClient API客戶端
type APIClient struct {
c Config
}
// NewAPIClient APIClient構造函數,如果入參校驗失敗,返回錯誤原因
func NewAPIClient(c Config) (*APIClient,error) { // <-- 第二個參數設置成error
if c.RemoteAddr == "" {
return nil, errors.New("沒有設置遠程地址")
}
return &APIClient{
c:c,
},nil
}
// Service
type Service struct {
client *APIClient
}
// NewService Service構造函數
func NewService(client *APIClient) *Service{
return &Service{
client:client,
}
}
類似的,injector函數定義的時候也需要將第二個返回值設置成error:
// ...
func InitializeClient(config Config) (*Service, error) { // <-- 第二個參數設置成error
wire.Build(NewService,NewAPIClient)
return nil,nil
}
// ...
觀察一下wire生成的injector:
func InitializeClient(config Config) (*Service, error) {
apiClient, err := NewAPIClient(config)
if err != nil { // <-- 在構造依賴的順序中如果發生錯誤,則會返回對應的"零值"和相應錯誤
return nil, err
}
service := NewService(apiClient)
return service, nil
}
在構造依賴的順序中如果發生錯誤,則會返回對應的"零值"和相應錯誤。
範例: https://github.com/DrmagicE/wire-examples/tree/master/advance-features/return-error
3. Cleanup functions
當provider生成的對象需要一些cleanup處理,比如關閉文件,關閉數據庫連接等操作時,依然可以通過設置provider的返回值來達到這樣的效果:
// FileReader
type FileReader struct {
f *os.File
}
// NewFileReader *FileReader 構造函數,第二個參數是cleanup function
func NewFileReader(filePath string) (*FileReader, func(), error){
f, err := os.Open(filePath)
if err != nil {
return nil,nil,err
}
fr := &FileReader{
f:f,
}
fn := func() {
log.Println("cleanup")
fr.f.Close()
}
return fr,fn,nil
}
跟返回錯誤類似,將provider的第二個返回參數設置成func()用於返回cleanup function,上述例子中在第三個參數中返回了error,但這是可選的:
wire對provider的返回值個數和順序有所規定:
- 第一個參數是需要生成的依賴對象
- 如果返回2個返回值,第二個參數必須是func()或者error
- 如果返回3個返回值,第二個參數必須是func(),第三個參數則必須是error
範例: https://github.com/DrmagicE/wire-examples/tree/master/advance-features/cleanup-functions
4. Provider set
當一些provider通常是一起使用的時候,可以使用provider set將它們組織起來,以quickstart示例為模板稍作修改:
// NewMessage Message的構造函數
func NewMessage(msg string) Message {
return Message{
msg:msg,
}
}
// NewGreeter Greeter構造函數
func NewGreeter(m Message) Greeter {
return Greeter{Message: m}
}
// NewEvent Event構造函數
func NewEvent(g Greeter) Event {
return Event{Greeter: g}
}
func (e Event) Start() {
msg := e.Greeter.Greet()
fmt.Println(msg)
}
// EventSet Event通常是一起使用的一個集合,使用wire.NewSet進行組合
var EventSet = wire.NewSet(NewEvent, NewMessage, NewGreeter) // <--
上述例子中將Event和它的依賴通過wire.NewSet組合起來,作為一個整體在injector函數簽名定義中使用:
func InitializeEvent(msg string) Event{
//wire.Build(NewEvent, NewGreeter, NewMessage)
wire.Build(EventSet)
return Event{}
}
這時只需將EventSet傳入wire.Build即可。
範例: https://github.com/DrmagicE/wire-examples/tree/master/advance-features/provider-set
5. struct provider
除了函數外,結構體也可以充當provider的角色,類似於setter注入:
type Foo int
type Bar int
func ProvideFoo() Foo {
return 1
}
func ProvideBar() Bar {
return 2
}
type FooBar struct {
MyFoo Foo
MyBar Bar
}
var Set = wire.NewSet(
ProvideFoo,
ProvideBar,
wire.Struct(new(FooBar), "MyFoo", "MyBar"))
通過wire.Struct來指定那些字段要被注入到結構體中,如果是全部字段,也可以簡寫成:
var Set = wire.NewSet(
ProvideFoo,
ProvideBar,
wire.Struct(new(FooBar), "*")) // * 表示注入全部字段
生成的injector函數:
func InitializeFooBar() FooBar {
foo := ProvideFoo()
bar := ProvideBar()
fooBar := FooBar{
MyFoo: foo,
MyBar: bar,
}
return fooBar
}
範例 : https://github.com/DrmagicE/wire-examples/tree/master/advance-features/struct-provider
Best Practices
distinguishing-types 區分類型
由於injector的函數中,不允許出現重複的參數類型,否則wire將無法區分這些相同的參數類型,比如:
type FooBar struct {
foo string
bar string
}
func NewFooBar(foo string, bar string) FooBar {
return FooBar{
foo: foo,
bar: bar,
}
}
injector函數簽名定義:
// wire無法得知入參a,b跟FooBar.foo,FooBar.bar的對應關係
func InitializeFooBar(a string, b string) FooBar {
wire.Build(NewFooBar)
return FooBar{}
}
如果使用上面的provider來生成injector,wire會報如下錯誤:
provider has multiple parameters of type string
因為入參均是字符串類型,wire無法得知入參a,b跟FooBar.foo,FooBar.bar的對應關係。 所以我們使用不同的類型來避免衝突:
type Foo string
type Bar string
type FooBar struct {
foo Foo
bar Bar
}
func NewFooBar(foo Foo, bar Bar) FooBar {
return FooBar{
foo: foo,
bar: bar,
}
}
injector函數簽名定義:
func InitializeFooBar(a Foo, b Bar) FooBar {
wire.Build(NewFooBar)
return FooBar{}
}
其中基礎類型和通用接口類型是最容易發生衝突的類型,如果它們在provider函數中出現,最好統一新建一個別名來代替它(儘管還未發生衝突),例如:
type MySQLConnectionString string
type FileReader io.Reader
範例 : https://github.com/DrmagicE/wire-examples/tree/master/best-practice/distinguishing-types
Options Structs
如果一個provider方法包含了許多依賴,可以將這些依賴放在一個options結構體中,從而避免構造函數的參數太多:
type Message string
// Options
type Options struct {
Messages []Message
Writer io.Writer
Reader io.Reader
}
type Greeter struct {
}
// NewGreeter Greeter的provider方法使用Options以避免構造函數過長
func NewGreeter(ctx context.Context, opts *Options) (*Greeter, error) {
return nil, nil
}
// GreeterSet 使用wire.Struct設置Options為provider
var GreeterSet = wire.NewSet(wire.Struct(new(Options), "*"), NewGreeter)
injector函數簽名:
func InitializeGreeter(ctx context.Context, msg []Message, w io.Writer, r io.Reader) (*Greeter, error) {
wire.Build(GreeterSet)
return nil, nil
}
範例 : https://github.com/DrmagicE/wire-examples/tree/master/best-practice/options-structs
一些缺點和限制
額外的類型定義
由於wire自身的限制,injector中的變量類型不能重複,需要定義許多額外的基礎類型別名。
mock支持暫時不夠友好
目前wire命令還不能識別_test.go結尾文件中的provider函數,這樣就意味著如果需要在測試中也使用wire來注入我們的mock對象,我們需要在常規代碼中嵌入mock對象的provider,這對常規代碼有侵入性,不過官方似乎也已經註意到了這個問題,感興趣的小伙伴可以關註一下這條issue:https://github.com/google/wire/issues/48
更多參考
Wire 官方README.md Wire 官方guide.md Wire 官方best-practices.md
作者:水立方 鏈接:https://juejin.cn/post/6844903901469097998 來源:稀土掘金 著作權歸作者所有。商業轉載請聯繫作者獲得授權,非商業轉載請註明出處。
Kratos Layout
.
├── Dockerfile
├── LICENSE
├── Makefile
├── README.md
├── api // 下面維護了微服務使用的proto文件以及根據它們所生成的go文件
│ └── helloworld
│ └── v1
│ ├── error_reason.pb.go
│ ├── error_reason.proto
│ ├── error_reason.swagger.json
│ ├── greeter.pb.go
│ ├── greeter.proto
│ ├── greeter.swagger.json
│ ├── greeter_grpc.pb.go
│ └── greeter_http.pb.go
├── cmd // 整個項目啟動的入口文件
│ └── server
│ ├── main.go
│ ├── wire.go // 我們使用wire來維護依賴注入
│ └── wire_gen.go
├── configs // 這里通常維護一些本地調試用的樣例配置文件
│ └── config.yaml
├── generate.go
├── go.mod
├── go.sum
├── internal // 該服務所有不對外暴露的代碼,通常的業務邏輯都在這下面,使用internal避免錯誤引用
│ ├── biz // 業務邏輯的組裝層,類似 DDD 的 domain 層,data 類似 DDD 的 repo,而 repo 接口在這裡定義,使用依賴倒置的原則。
│ │ ├── README.md
│ │ ├── biz.go
│ │ └── greeter.go
│ ├── conf // 內部使用的config的結構定義,使用proto格式生成
│ │ ├── conf.pb.go
│ │ └── conf.proto
│ ├── data // 業務數據訪問,包含 cache、db 等封裝,實現了 biz 的 repo 接口。我們可能會把 data 與 dao 混淆在一起,data 偏重業務的含義,它所要做的是將領域對象重新拿出來,我們去掉了 DDD 的 infra層。
│ │ ├── README.md
│ │ ├── data.go
│ │ └── greeter.go
│ ├── server // http和grpc實例的創建和配置
│ │ ├── grpc.go
│ │ ├── http.go
│ │ └── server.go
│ └── service // 實現了 api 定義的服務層,類似 DDD 的 application 層,處理 DTO 到 biz 領域實體的轉換(DTO -> DO),同時協同各類 biz 交互,但是不應處理複雜邏輯
│ ├── README.md
│ ├── greeter.go
│ └── service.go
└── third_party // api 依賴的第三方proto
├── README.md
├── google
│ └── api
│ ├── annotations.proto
│ ├── http.proto
│ └── httpbody.proto
└── validate
├── README.md
└── validate.proto