Go Web 服務框架實現詳解
前言
此係列文章要求讀者有一定的 golang 基礎。go-zero 是一個集成了各種工程實踐的 web 和 rpc 框架。通過彈性設計保障了大併發服務端的穩定性,經受了充分的實戰檢驗。go-zero 包含極簡的 API 定義和生成工具 goctl,可以根據定義的 api 文件一鍵生成 Go, iOS, Android, Kotlin, Dart, TypeScript, JavaScript 代碼,並可直接運行。
如何解讀一個 Web 框架 毫無疑問讀 go 的 Web 框架和 PHP 框架也是一樣的:
-
配置加載:如何加載配置文件。
-
路由:分析框架如何通過 URL 執行對應業務的。
-
ORM:ORM 如何實現。其中 1、3 無非是加載解析配置文件和 sql 解析器的實現,我就忽略了,由於業內大多數都是性能分析的比較多,我可能會更側重於以下維度:
-
框架設計
-
路由算法
首先我們主要把重點放在框架設計上面。
安裝
開發 golang 程序,必然少不了對其環境的安裝,我們這裏選擇以 1.16.13 爲例。並且使用 Go Module 作爲管理依賴的方式,與 PHP 中 composer 管理依賴類似。首先安裝 goctl(go control)工具:
goctl 是 go-zero 微服務框架下的代碼生成工具。使用 goctl 可顯著提升開發效率,讓開發人員將時間重點放在業務開發上,其功能有:
-
api 服務生成
-
rpc 服務生成
-
model 代碼生成
-
模板管理
# Go 1.16 及以後版本
GOPROXY=https://goproxy.cn/,direct go install github.com/zeromicro/go-zero/tools/goctl@latest
通過此命令可以將 goctl 工具安裝到 $GOPATH/bin 目錄下。我們以 api 服務爲例進行操作,使用 go mod 安裝:
// 創建項目目錄
mkdir zero-demo
cd zero-demo
// 初始化go.mod文件
go mod init zero-demo
// 快捷創建api服務
goctl api new greet
// 安裝依賴
go mod tidy
// 複製依賴到vender目錄
go mod vendor
到此一個簡單的 api 服務就初始化完成了。啓動服務:
// 默認開啓8888端口
go run greet/greet.go -f greet/etc/greet-api.yaml
代碼分析
HTTP SERVER
go 有自己實現的 http 包,大多 go 框架也是基於這個 http 包,所以看 go-zero 之前我們先補充或者複習下這個知識點。如下:GO 如何啓動一個HTTP SERVER
// main.go
package main
import (
// 導入net/http包
"net/http"
)
func main() {
// ------------------ 使用http包啓動一個http服務 方式一 ------------------
// *http.Request http請求內容實例的指針
// http.ResponseWriter 寫http響應內容的實例
http.HandleFunc("/v1/demo", func(w http.ResponseWriter, r *http.Request) {
// 寫入響應內容
w.Write([]byte("Hello World !\n"))
})
// 啓動一個http服務並監聽8888端口 這裏第二個參數可以指定handler
http.ListenAndServe(":8888", nil)
}
// 測試我們的服務
// --------------------
// 啓動:go run main.go
// 訪問:curl "http://127.0.0.1:8888/v1/demo"
// 響應結果:Hello World !
ListenAndServe
是對http.Server
的進一步封裝,除了上面的方式,還可以使用http.Server
直接啓服務,這個需要設置Handler
,這個Handler
要實現Server.Handler
這個接口。當請求來了會執行這個Handler
的ServeHTTP
方法,如下:
// main.go
package main
// 導入net/http包
import (
"net/http"
)
// DemoHandle server handle示例
type DemoHandle struct {
}
// ServeHTTP 匹配到路由後執行的方法
func (DemoHandle) ServeHTTP(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("Hello World !\n"))
}
func main() {
// ------------------ 使用http包的Server啓動一個http服務 方式二 ------------------
// 初始化一個http.Server
server := &http.Server{}
// 初始化handler並賦值給server.Handler
server.Handler = DemoHandle{}
// 綁定地址
server.Addr = ":8888"
// 啓動一個http服務
server.ListenAndServe()
}
// 測試我們的服務
// --------------------
// 啓動:go run main.go
// 訪問:curl "http://127.0.0.1:8888/v1/demo"
// 響應結果:Hello World !
至此我們就明白了基本 sever 服務基礎,下面讓我們一起來看一下 go-zero 是如何使用的。
目錄結構
// 命令行
tree greet
greet
├── etc // 配置
│ └── greet-api.yaml // 配置文件
├── greet.api // 描述文件用於快速生成代碼
├── greet.go // 入口文件
└── internal // 主要操作文件夾,包括路由、業務等
├── config // 配置
│ └── config.go // 配置解析映射結構體
├── handler // 路由
│ ├── greethandler.go // 路由對應方法
│ └── routes.go // 路由文件
├── logic // 業務
│ └── greetlogic.go
├── svc
│ └── servicecontext.go // 類似於IOC容器,綁定主要操作依賴
└── types
└── types.go // 請求及響應結構體
我們先從入口文件入手:
package main
import (
"flag"
"fmt"
"zero-demo/greet/internal/config"
"zero-demo/greet/internal/handler"
"zero-demo/greet/internal/svc"
"github.com/zeromicro/go-zero/core/conf"
"github.com/zeromicro/go-zero/rest"
)
var configFile = flag.String("f", "etc/greet-api.yaml", "the config file")
func main() {
// 解析命令
flag.Parse()
// 讀取並映射配置文件到config結構體
var c config.Config
conf.MustLoad(*configFile, &c)
// 初始化上下文
ctx := svc.NewServiceContext(c)
// 初始化服務
server := rest.MustNewServer(c.RestConf)
defer server.Stop()
// 初始化路由及綁定上下文
handler.RegisterHandlers(server, ctx)
// 啓動服務
fmt.Printf("Starting server at %s:%d...\n", c.Host, c.Port)
server.Start()
}
go-zero 的生命週期
下圖就是我對整個 go-zero 框架生命週期的輸出:
訪問源圖片:https://franktrue.oss-cn-shanghai.aliyuncs.com/images/go-zero%27s%20life%20cycle-small.png
關鍵代碼解析
⬇️step1
// 獲取一個server實例
server := rest.MustNewServer(c.RestConf)
⬇️step2
// 具體的rest.MustNewServer方法
// ----------------------MustNewServer---------------------------
func MustNewServer(c RestConf, opts ...RunOption) *Server {
server, err := NewServer(c, opts...)
if err != nil {
log.Fatal(err)
}
return server
}
⬇️step3
// 創建一個server實例的具體方法
// ---------------------NewServer------------------------------------
func NewServer(c RestConf, opts ...RunOption) (*Server, error) {
if err := c.SetUp(); err != nil {
return nil, err
}
server := &Server{
ngin: newEngine(c),
router: router.NewRouter(),
}
// opts主要是一些對server的自定義操作函數
opts = append([]RunOption{WithNotFoundHandler(nil)}, opts...)
for _, opt := range opts {
opt(server)
}
return server, nil
}
⬇️step4
// 上面是一個server實例初始化的關鍵代碼,下面我們分別看下server.ngin和server.router
// -----------------------------engine----------------------------------------
// 創建一個engine
func newEngine(c RestConf) *engine {
srv := &engine{
conf: c,
}
// Omit the code
return srv
}
type engine struct {
conf RestConf // 配置信息
routes []featuredRoutes // 初始路由組信息
unauthorizedCallback handler.UnauthorizedCallback // 認證
unsignedCallback handler.UnsignedCallback // 簽名
middlewares []Middleware // 中間件
shedder load.Shedder
priorityShedder load.Shedder
tlsConfig *tls.Config
}
⬇️step5
// -----------------------------router-------------------------------------------
// 接下來我們看路由註冊部分
// 創建一個router
func NewRouter() httpx.Router {
return &patRouter{
trees: make(map[string]*search.Tree),
}
}
// 這裏返回了一個實現httpx.Router接口的實例,實現了ServeHttp方法
// ---------------------------Router interface-----------------------------------
type Router interface {
http.Handler
Handle(method, path string, handler http.Handler) error
SetNotFoundHandler(handler http.Handler)
SetNotAllowedHandler(handler http.Handler)
}
⬇️step6
// 註冊請求路由
// 這個方法就是將server.ngin.routes即featuredRoutes映射到路由樹trees上
func (pr *patRouter) Handle(method, reqPath string, handler http.Handler) error {
if !validMethod(method) {
return ErrInvalidMethod
}
if len(reqPath) == 0 || reqPath[0] != '/' {
return ErrInvalidPath
}
cleanPath := path.Clean(reqPath)
tree, ok := pr.trees[method]
if ok {
return tree.Add(cleanPath, handler)
}
tree = search.NewTree()
pr.trees[method] = tree
return tree.Add(cleanPath, handler)
}
⬇️step7
// 路由樹節點
Tree struct {
root *node
}
node struct {
item interface{}
children [2]map[string]*node
}
// 上面我們基本看完了server.ngin和server.router的實例化
// ----------------------------------http server------------------------------------
// 接下來我們看下go-zero如何啓動http server的
⬇️step8
server.Start()
⬇️step9
func (s *Server) Start() {
handleError(s.ngin.start(s.router))
}
⬇️step10
func (ng *engine) start(router httpx.Router) error {
// 綁定路由,將server.ngin.routes即featuredRoutes映射到路由樹trees上
if err := ng.bindRoutes(router); err != nil {
return err
}
if len(ng.conf.CertFile) == 0 && len(ng.conf.KeyFile) == 0 {
// 無加密證書,則直接通過http啓動
return internal.StartHttp(ng.conf.Host, ng.conf.Port, router)
}
// 這裏是針對https形式的訪問,我們主要看上面的http形式
return internal.StartHttps(ng.conf.Host, ng.conf.Port, ng.conf.CertFile,
ng.conf.KeyFile, router, func(srv *http.Server) {
if ng.tlsConfig != nil {
srv.TLSConfig = ng.tlsConfig
}
})
}
⬇️step11
// 綁定路由
ng.bindRoutes(router)
⬇️step12
// 將server.ngin.routes即featuredRoutes映射到路由樹trees上
func (ng *engine) bindRoutes(router httpx.Router) error {
metrics := ng.createMetrics()
for _, fr := range ng.routes {
if err := ng.bindFeaturedRoutes(router, fr, metrics); err != nil {
return err
}
}
return nil
}
// 映射的同時對每個路由執行中間件操作
func (ng *engine) bindRoute(fr featuredRoutes, router httpx.Router, metrics *stat.Metrics,
route Route, verifier func(chain alice.Chain) alice.Chain) error {
// go-zero框架默認中間件
// ---------------------------------Alice--------------------------------------------
// Alice提供了一種方便的方法來鏈接您的HTTP中間件函數和應用程序處理程序。
//In short, it transforms
// Middleware1(Middleware2(Middleware3(App)))
// to
// alice.New(Middleware1, Middleware2, Middleware3).Then(App)
// --------------------------------Alice--------------------------------------------
chain := alice.New(
handler.TracingHandler(ng.conf.Name, route.Path),
ng.getLogHandler(),
handler.PrometheusHandler(route.Path),
handler.MaxConns(ng.conf.MaxConns),
handler.BreakerHandler(route.Method, route.Path, metrics),
handler.SheddingHandler(ng.getShedder(fr.priority), metrics),
handler.TimeoutHandler(ng.checkedTimeout(fr.timeout)),
handler.RecoverHandler,
handler.MetricHandler(metrics),
handler.MaxBytesHandler(ng.conf.MaxBytes),
handler.GunzipHandler,
)
chain = ng.appendAuthHandler(fr, chain, verifier)
// 自定義的全局中間件
for _, middleware := range ng.middlewares {
chain = chain.Append(convertMiddleware(middleware))
}
handle := chain.ThenFunc(route.Handler)
return router.Handle(route.Method, route.Path, handle)
}
⬇️step13
internal.StartHttp(ng.conf.Host, ng.conf.Port, router)
⬇️step14
func StartHttp(host string, port int, handler http.Handler, opts ...StartOption) error {
return start(host, port, handler, func(srv *http.Server) error {
return srv.ListenAndServe()
}, opts...)
}
⬇️step15
func start(host string, port int, handler http.Handler, run func(srv *http.Server) error,
opts ...StartOption) (err error) {
server := &http.Server{
Addr: fmt.Sprintf("%s:%d", host, port),
Handler: handler,
}
for _, opt := range opts {
opt(server)
}
waitForCalled := proc.AddWrapUpListener(func() {
if e := server.Shutdown(context.Background()); err != nil {
logx.Error(e)
}
})
defer func() {
if err == http.ErrServerClosed {
waitForCalled()
}
}()
// run即上一步中的srv.ListenAndServe()操作,因爲server實現了ServeHttp方法
// 最終走到了http包的Server啓動一個http服務(上文中http原理中的方式二)
return run(server)
}
結語
最後我們再簡單的回顧下上面的流程,從下圖來看,相對還是很容易理解的。
參考
https://www.bilibili.com/video/BV1d34y1t7P9 Mikael 大佬的 api 服務之代碼講解
項目地址
https://github.com/zeromicro/go-zero
歡迎使用 go-zero
並 star 支持我們!
微信交流羣
關注『微服務實踐』公衆號並點擊 交流羣 獲取社區羣二維碼。
微服務實踐 分享微服務的原理和最佳實踐,講透服務治理的底層原理,帶你細讀 go-zero 源碼。go-zero 是一個集成了各種工程實踐的 web 和 rpc 框架,旨在縮短從需求到上線的距離。公衆號文章勘誤在知乎號:萬俊峯 Kevin
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/pNH7TCL5GD75xfwt9N-G1w