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框架也是一样的:
- 配置加载:如何加载配置文件。
- 路由:分析框架如何通过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 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
这个接口。当请求来了会执行这个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 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框架生命周期的输出:
文章图片
访问源图片: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)
}
结语
最后我们再简单的回顾下上面的流程,从下图来看,相对还是很容易理解的。
文章图片
参考
https://www.bilibili.com/vide... Mikael大佬的api服务之代码讲解
项目地址 https://github.com/zeromicro/go-zero
欢迎使用
go-zero
并 star 支持我们!微信交流群 关注『微服务实践』公众号并点击 交流群 获取社区群二维码。
推荐阅读
- 原力计划|云原生之 Docker Swarm服务编排介绍及使用入门
- netty系列之:自建客户端和HTTP服务器交互
- rsync实现服务器之间同步目录文件
- 如何重启数据库服务(包含单实例/流复制/集群)
- IOS技术分享| 在iOS WebRTC 中添加美颜滤镜
- Alibaba微服务技术系列「Dubbo3.0技术专题」回顾Dubbo2.x的技术原理和功能实现
- Java技术指南「TestNG专题」单元测试框架之TestNG使用教程指南(上)
- python实现两台不同主机之间进行通信(客户端和服务端)——Socket
- 使用 Jenkins 创建微服务应用的持续集成
- netty系列之:轻轻松松搭个支持中文的服务器