Go|Go Web 服务框架实现详解

前言 【Go|Go Web 服务框架实现详解】此系列文章要求读者有一定的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 mainimport ( // 导入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 greetgreet ├── 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 mainimport ( "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框架生命周期的输出:
Go|Go Web 服务框架实现详解
文章图片

访问源图片: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--------------------------- funcMustNewServer(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 { confRestConf// 配置信息 routes[]featuredRoutes// 初始路由组信息 unauthorizedCallback handler.UnauthorizedCallback// 认证 unsignedCallbackhandler.UnsignedCallback// 签名 middlewares[]Middleware// 中间件 shedderload.Shedder priorityShedderload.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 { iteminterface{} 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) }

结语
最后我们再简单的回顾下上面的流程,从下图来看,相对还是很容易理解的。
Go|Go Web 服务框架实现详解
文章图片

参考
https://www.bilibili.com/vide... Mikael大佬的api服务之代码讲解
项目地址 https://github.com/zeromicro/go-zero
欢迎使用 go-zero 并 star 支持我们!
微信交流群 关注『微服务实践』公众号并点击 交流群 获取社区群二维码。

    推荐阅读