Go Web 服務框架實現詳解

kevinwan發表於2022-05-31

前言

此係列文章要求讀者有一定的golang基礎。
go-zero 是一個整合了各種工程實踐的 web 和 rpc 框架。通過彈性設計保障了大併發服務端的穩定性,經受了充分的實戰檢驗。
go-zero 包含極簡的 API 定義和生成工具 goctl,可以根據定義的 api 檔案一鍵生成 Go, iOS, Android, Kotlin, Dart, TypeScript, JavaScript 程式碼,並可直接執行。

如何解讀一個Web框架
毫無疑問讀go的Web框架和PHP框架也是一樣的:
  1. 配置載入:如何載入配置檔案。
  2. 路由:分析框架如何通過URL執行對應業務的。
  3. 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這個介面。當請求來了會執行這個HandlerServeHTTP方法,如下:

// 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框架生命週期的輸出:
 title=

訪問源圖片: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)
}

結語

最後我們再簡單的回顧下上面的流程,從下圖來看,相對還是很容易理解的。
 title=


參考

https://www.bilibili.com/vide... Mikael大佬的api服務之程式碼講解

專案地址

https://github.com/zeromicro/go-zero

歡迎使用 go-zerostar 支援我們!

微信交流群

關注『微服務實踐』公眾號並點選 交流群 獲取社群群二維碼。

相關文章