從用了近十年的 C# 轉到 Go 是一個有趣的旅程。有時,我陶醉于 Go 的簡潔[1];也有些時候,當熟悉的 OOP (面向對象編程)模式[2]無法在 Go 代碼中使用的時候會感到沮喪。幸運的是,我已經摸索出了一些寫 HTTP 服務的模式,在我的團隊中應用地很好。
當在公司項目上工作時,我傾向把可發現性放在最高的優先級上。這些應用會在接下來的 20 年運行在生產環境中,必須有眾多的開發人員和網站可靠性工程師(可能是指運維)來進行熱補丁,維護和調整工作。因此,我不指望這些模式能適合所有人。
Mat Ryer 的文章[3]是我使用 Go 試驗 HTTP 服務的起點之一,也是這篇文章的靈感來源。
代碼組成
Broker
一個 Broker 結構是將不同的 service 包綁定到 HTTP 邏輯的膠合結構。沒有包作用域結級別的變量被使用。依賴的接口得益于了 Go 的組合[4]的特點被嵌入了進來。
type Broker struct {
auth.Client // 從外部倉庫導入的身份驗證依賴(接口)
service.Service // 倉庫的業務邏輯包(接口)
cfg Config // 該 API 服務的配置
router *mux.Router // 該 API 服務的路由集
}
broker 可以使用阻塞[5]函數 New() 來初始化,該函數校驗配置,并且運行所有需要的前置檢查。
func New(cfg Config, port int) (*Broker, error) {
r := &Broker{
cfg: cfg,
}
...
r.auth.Client, err = auth.New(cfg.AuthConfig)
if err != nil {
return nil, fmt.Errorf("Unable to create new API broker: %w", err)
}
...
return r, nil
}
初始化后的 Broker 滿足了暴露在外的 Server 接口,這些接口定義了所有的,被 route 和 中間件(middleware)使用的功能。service 包接口被嵌入,這些接口與 Broker 上嵌入的接口相匹配。
type Server interface {
PingDependencies(bool) error
ValidateJWT(string) error
service.Service
}
web 服務通過調用 Start() 函數來啟動。路由綁定通過一個閉包函數[6]進行綁定,這種方式保證循環依賴不會破壞導入周期規則。
func (bkr *Broker) Start(binder func(s Server, r *mux.Router)) {
...
bkr.router = mux.NewRouter().StrictSlash(true)
binder(bkr, bkr.router)
...
if err := http.Serve(l, bkr.router); errors.Is(err, http.ErrServerClosed) {
log.Warn().Err(err).Msg("Web server has shut down")
} else {
log.Fatal().Err(err).Msg("Web server has shut down unexpectedly")
}
}
那些對故障排除(比如,Kubernetes 探針[7])或者災難恢復方案方面有用的函數,掛在 Broker 上。如果被 routes/middleware 使用的話,這些僅僅被添加到 webserver.Server 接口上。
func (bkr *Broker) SetupDatabase() { ... }
func (bkr *Broker) PingDependencies(failFast bool)) { ... }
啟動引導
整個應用的入口是一個 main 包。默認會啟動 Web 服務。我們可以通過傳入一些命令行參數來調用之前提到的故障排查功能,方便使用傳入 New() 函數的,經過驗證的配置來測試代理權限以及其他網絡問題。我們所要做的只是登入運行著的 pod 然后像使用其他命令行工具一樣使用它們。
func main() {
subCommand := flag.String("start", "", "start the webserver")
...
srv := webserver.New(cfg, 80)
switch strings.ToLower(subCommand) {
case "ping":
srv.PingDependencies(false)
case "start":
srv.Start(BindRoutes)
default:
fmt.Printf("Unrecognized command %q, exiting.", subCommand)
os.Exit(1)
}
}
HTTP 管道設置在 BindRoutes() 函數中完成,該函數通過 ser.Start() 注入到服務(server)中。
func BindRoutes(srv webserver.Server, r *mux.Router) {
r.Use(middleware.Metrics(), middleware.Authentication(srv))
r.HandleFunc("/ping", routes.Ping()).Methods(http.MethodGet)
...
r.HandleFunc("/makes/{makeID}/models/{modelID}", model.get(srv)).Methods(http.MethodGet)
}
中間件
中間件(Middleware)返回一個帶有 handler 的函數,handler 用來構建需要的 http.HandlerFunc。這使得 webserver.Server 接口被注入,同時所有的安靜檢查只在啟動時執行,而不是在所有路由調用的時候。
func Authentication(srv webserver.Server) func(h http.Handler) http.Handler {
if srv == nil || !srv.Client.IsValid() {
log.Fatal().Msg("a nil dependency was passed to authentication middleware")
}
// additional setup logic
...
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
token := strings.TrimSpace(r.Header.Get("Authorization"))
if err := srv.ValidateJWT(token); err != nil {
...
w.WriteHeader(401)
w.Write([]byte("Access Denied"))
return
}
next.ServeHTTP(w, r)
}
}
}
路由
路由有著與中間件有著類似的套路——簡單的設置,但是有著同樣的收益。
func GetLatest(srv webserver.Server) http.HandlerFunc {
if srv == nil {
log.Fatal().Msg("a nil dependency was passed to the `/makes/{makeID}/models/{modelID}` route")
}
// additional setup logic
...
return func(w http.ResponseWriter, r *http.Request) {
...
makeDTO, err := srv.Get
}
}
目錄結構
代碼的目錄結構對可發現性進行了高度優化。
├── App/
| └── service-api/**
├── cmd/
| └── service-tool-x/
├── internal/
| └── service/
| └── mock/
├── pkg/
| ├── client/
| └── dtos/
├── (.editorconfig, .gitattributes, .gitignore)
└── go.mod
- app/ 用于項目應用——這是新來的人了解代碼傾向的切入點。dd
- ./service-api/ 是該倉庫的微服務 API;所有的 HTTP 實現細節都在這里。
- cmd/ 是存放命令行應用的地方。
- internal/ 是不可以被該倉庫以外的項目引入的一個特殊目錄[8]。
- ./service/ 是所有領域邏輯(domain logic)所在的地方;可以被 service-api,service-tool-x,以及任何未來直接訪問這個目錄可以帶來收益的應用或者包所引入。
- pkg/ 用于存放鼓勵被倉庫以外的項目所引入的包。
- ./client/ 是用于訪問 service-api 的 client 庫。其他團隊可以使用而不是自己寫一個 client,并且我們可以借助我們在 cmd/ 里面的 CI/CD 工具來 “dogfood it[9]” (使用自己產品的意思)。
- ./dtos/ 是存放項目的數據傳輸對象,不同包之間共享的數據且以 json 形式在線路上編碼或傳輸的結構體定義。沒有從其他倉庫包導出的模塊化的結構體。/internal/service 負責 這些 DTO (數據傳輸對象)和自己內部模型的相互映射,避免實現細節的遺漏(如,數據庫注釋)并且該模型的改變不破壞下游客戶端消費這些 DTO。
- .editorconfig,.gitattributes,.gitignore 因為所有的倉庫必須使用 .editorconfig,.gitattributes,.gitignore[10]!
- go.mod 甚至可以在有限制的且官僚的公司環境[11]工作。
最重要的:每個包只負責意見事情,一件事情!
HTTP 服務結構
└── service-api/
├── cfg/
├── middleware/
├── routes/
| ├── makes/
| | └── models/**
| ├── create.go
| ├── create_test.go
| ├── get.go
| └── get_test.go
├── webserver/
├── main.go
└── routebinds.go
- ./cfg/ 用于存放配置文件,通常是以 JSON 或者 YAML 形式保存的純文本文件,它們也應該被檢入到 Git 里面(除了密碼,秘鑰等)。
- ./middleware 用于所有的中間件。
- ./routes 采用類似應用的類 RESTFul 形式的目錄對路由代碼進行分組和嵌套。
- ./webserver 保存所有共享的 HTTP 結構和接口(Broker,配置,Server等等)。
- main.go 啟動應用程序的地方(New(),Start())。
- routebinds.go BindRoutes() 函數存放的地方。
你覺得呢?
如果你最終采用了這種模式,或者有其他的想法我們可以討論,我樂意聽到這些想法!
via: https://www.dudley.codes/posts/2020.05.19-golang-structure-web-servers/
作者:James Dudley[12]譯者:dust347[13]校對:unknwon[14]
本文由 GCTT[15] 原創編譯,Go 中文網[16] 榮譽推出
參考資料
[1]
簡潔:
https://www.youtube.com/watch?v=rFejpH_tAHM
[2]
模式:
https://en.wikipedia.org/wiki/Software_design_pattern
[3]
Mat Ryer 的文章:
https://pace.dev/blog/2018/05/09/how-I-write-http-services-after-eight-years.html
[4]
Go 的組合:
https://www.ardanlabs.com/blog/2015/09/composition-with-go.html
[5]
阻塞:
https://stackoverflow.com/questions/2407589/what-does-the-term-blocking-mean-in-programming
[6]
閉包函數:
https://gobyexample.com/closures
[7]
Kubernetes 探針:
https://kubernetes.io/docs/tasks/configure-pod-container/configure-liveness-readiness-startup-probes/)0
[8]
特殊目錄:
https://dave.cheney.net/2019/10/06/use-internal-packages-to-reduce-your-public-api-surface
[9]
dogfood it: https://en.wikipedia.org/wiki/Eating_your_own_dog_food
[10]
所有的倉庫必須使用 .editorconfig,.gitattributes,.gitignore:
https://www.dudley.codes/posts/2020.02.16-git-lost-in-translation/
[11]
有限制的且官僚的公司環境:
https://www.dudley.codes/posts/2020.04.02-golang-behind-corporate-firewall/
[12]
James Dudley: https://www.dudley.codes/
[13]
dust347: https://github.com/dust347
[14]
unknwon: https://github.com/unknwon
[15]
GCTT: https://github.com/studygolang/GCTT
[16]
Go 中文網: https://studygolang.com/