前言
此系列文章要求读者有一定的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服务之代码讲解