Standard Go Project Layout

如果你嘗試學習 Go, 或者你正在為自己建立一個 PoC 或一個玩具項目, 這個項目佈局是沒啥必要的. 從一些非常簡單的事情開始(一個 main.go 文件綽綽有餘). 當有更多的人參與這個項目時, 你將需要更多的結構, 包括需要一個 toolkit 來方便生成項目的模板, 盡可能大家統一的工程目錄佈局

如果需要命名、格式與程式碼風格方面的協助, 請使用 gofmtgolint. 也請確保你閱讀過以下這些 Go 程式碼撰寫風格的指導方針與建議:

參見 Go Project Layout 了解更多的背景資訊.

更多關於套件的命名與組織方式, 以及其他程式碼結構的建議:

這裡有份中文的文件可供參考: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 應用目錄負責程序的: 啟動、關閉、配置初始化等 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).

internal

/pkg

函式庫的程式碼當然可以讓外部應用程式來使用 (例如:/pkg/mypubliclib), 其他專案會匯入這些函式庫, 並且期待它們能正常運作, 所以要把程式放在這個目錄下請多想個幾遍!:-) 注意:使用 internal 目錄可以確保私有套件不會被匯入到其他專案使用, 因為它是由 Go 的編譯器強制執行的, 所以是比較好的解決方案.使用 /pkg 目錄仍然是一種很好的方式, 它代表其他專案可以安全地使用這個目錄下的程式碼.由 Travis Jeffery 撰寫的 I'll take pkg over internal 文章提供了關於 pkginternal 目錄很好的概述, 以及使用它們的時機點.

, 這使得運行各種 Go 工具變得更加容易(正如以下這些演講中提到的那樣:來自 GopherCon EU 2018 的 Best Practices for Industrial ProgrammingGopherCon 2018: Kat Zien - How Do You Structure Your Go AppsGoLab 2018 - Massimiliano Pippi - Project layout patterns in Go).

如果你想查看哪些知名的 Go 專案使用本專案的目錄結構, 請查看 /pkg 目錄.這是一組常見的目錄結構, 但並不是所有人都接受它, 有些 Go 社群的人也不推薦使用.

如果你的應用程式專案真的很小, 或是套用這些資料夾不會對你有太大幫助(除非你真的很想用XD), 不使用本專案推薦的目錄結構是完全沒問題的.當你的專案變的越來越大, 根目錄將會會變得越來越複雜(尤其是當你有許多不是 Go 所寫的元件時), 你可以考慮參考這個專案所建議的目錄結構來組織你的程式碼.

pkg

/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

組態設定的檔案範本或預設設定.

將你的 confdconsul-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 項目必須具備的特點:

  1. 統一
  2. 標準庫方式佈局
  3. 高度抽象
  4. 支持插件

kit layout


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)

Service Application Project - v1

項目的依賴路徑為: 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做

Service Application Project - v1 2

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)

  1. 上層模塊不應該依賴於下層模塊, 他們共同依賴於一個抽象
    • High-level modules should not depend on low-level modules. Both should depend on abstractions.
  2. 抽象不能依賴於具象, 具象依賴於抽象. 抽象不應該由低階模塊定義
    • Abstractions should not depend on details.
  3. 低階模塊的實作內容應該依照抽象的定義去打造.
    • 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

Service_Application_Project_v2

Lifycycle

Lifecycle 需要考慮服務應用的對像初始化以及生命週期的管理, 所有 HTTP/gRPC 依賴的前置資源初始化,包括 data、biz、service,之後再啟動監聽服務。 我們使用 https://github.com/google/wire ,來管理所有資源的依賴注入(dependency injection, DI)。為何需要依賴注入?

依賴注入(dependency injection,縮寫為DI)是一種軟體設計模式,也是實現控制反轉的其中一種技術。 這種模式能讓一個物件接收它所依賴的其他物件。 「依賴」是指接收方所需的物件。 「注入」是指將「依賴」傳遞給接收方的過程。

核心是為了:

  1. 方便測試
  2. 單次初始化和複用
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的步驟描述如下:

  1. 確定所生成injector函數的函數簽名:func initUserStore(info ConnectionInfo) (*UserStore, error)
  2. 感知返回值第一個參數是*UserStore
  3. 檢查wire.Build列表,找到*UserStoreprovider:NewUserStore
  4. 由函數簽名func NewUserStore(cfg *Config, db *mysql.DB)得知NewUserStore依賴於*Config, 和*mysql.DB
  5. 檢查wire.Build列表,找到*Config*mysql.DBprovider:NewDefaultConfigNewDB
  6. 由函數簽名func NewDefaultConfig() *Config得知*Config沒有其他依賴了。
  7. 由函數簽名func NewDB(info *ConnectionInfo) (*mysql.DB, error)得知*mysql.DB依賴於ConnectionInfo
  8. 檢查wire.Build列表,找不到ConnectionInfoprovider,但在injector函數簽名中發現匹配的入參類型,直接使用該參數作為NewDB的入參。
  9. 感知返回值第二個參數是error
  10. 按依賴關係,按順序調用provider函數,拼裝injector函數。

Tutorial : https://github.com/google/wire/blob/main/_tutorial/README.md

創建一個小程序來模擬一個事件,迎賓員用特定的消息向客人致意

Without Wire

create three types:

  1. a message for a greeter
  2. a greeter who conveys that message
  3. 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 都是 providerwire_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接口,其中mockUserRepoUserRepository的一個實現, 由於在Go的最佳實踐中,更推薦返回具體實現而不是接口。所以mockUserRepoprovider函數返回的是*mockUserRepo這一具體類型。 wire無法自動將具體實現與接口進行關聯, 我們需要顯示聲明它們之間的關聯關係。通過wire.NewSetwire.Bind*mockUserRepoUserRepository進行綁定:

// 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函數可能會對入參做校驗,如果參數錯誤,則需要返回errorwire也考慮了這種情況,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的返回值個數和順序有所規定:

  1. 第一個參數是需要生成的依賴對象
  2. 如果返回2個返回值,第二個參數必須是func()或者error
  3. 如果返回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

example : https://github.com/go-kratos/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

Reference

© Kimi Tsai all right reserved.            Updated : 2023-07-12 09:04:53

results matching ""

    No results matching ""

    results matching ""

      No results matching ""