Go Web 服务框架实现详解

简介: 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 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框架生命周期的输出:

访问源图片: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

相关文章
|
26天前
|
开发框架 JavaScript 前端开发
如何选择合适的Web开发框架?
【9月更文挑战第1天】如何选择合适的Web开发框架?
51 1
|
2天前
|
JSON 安全 JavaScript
Web安全-JQuery框架XSS漏洞浅析
Web安全-JQuery框架XSS漏洞浅析
16 2
|
2天前
|
开发框架 JSON 缓存
震撼发布!Python Web开发框架下的RESTful API设计全攻略,让数据交互更自由!
在数字化浪潮推动下,RESTful API成为Web开发中不可或缺的部分。本文详细介绍了在Python环境下如何设计并实现高效、可扩展的RESTful API,涵盖框架选择、资源定义、HTTP方法应用及响应格式设计等内容,并提供了基于Flask的示例代码。此外,还讨论了版本控制、文档化、安全性和性能优化等最佳实践,帮助开发者实现更流畅的数据交互体验。
13 1
|
15天前
|
Go API 开发者
深入探讨:使用Go语言构建高性能RESTful API服务
在本文中,我们将探索Go语言在构建高效、可靠的RESTful API服务中的独特优势。通过实际案例分析,我们将展示Go如何通过其并发模型、简洁的语法和内置的http包,成为现代后端服务开发的有力工具。
|
20天前
|
Web App开发 前端开发 JavaScript
Web前端项目的跨平台桌面客户端打包方案之——CEF框架
Chromium Embedded Framework (CEF) 是一个基于 Google Chromium 项目的开源 Web 浏览器控件,旨在为第三方应用提供嵌入式浏览器支持。CEF 隔离了底层 Chromium 和 Blink 的复杂性,提供了稳定的产品级 API。它支持 Windows、Linux 和 Mac 平台,不仅限于 C/C++ 接口,还支持多种语言。CEF 功能强大,性能优异,广泛应用于桌面端开发,如 QQ、微信、网易云音乐等。CEF 开源且采用 BSD 授权,商业友好,装机量已超 1 亿。此外,GitHub 项目 CefDetector 可帮助检测电脑中使用 CEF
94 3
|
18天前
|
消息中间件 NoSQL Go
PHP转Go系列 | ThinkPHP与Gin框架之Redis延时消息队列技术实践
【9月更文挑战第7天】在从 PHP 的 ThinkPHP 框架迁移到 Go 的 Gin 框架时,涉及 Redis 延时消息队列的技术实践主要包括:理解延时消息队列概念,其能在特定时间处理消息,适用于定时任务等场景;在 ThinkPHP 中使用 Redis 实现延时队列;在 Gin 中结合 Go 的 Redis 客户端库实现类似功能;Go 具有更高性能和简洁性,适合处理大量消息。迁移过程中需考虑业务需求及系统稳定性。
|
27天前
|
数据库 开发者 Java
颠覆传统开发:Hibernate与Spring Boot的集成,让你的开发效率飞跃式提升!
【8月更文挑战第31天】在 Java 开发中,Spring Boot 和 Hibernate 已成为许多开发者的首选技术栈。Spring Boot 简化了配置和部署过程,而 Hibernate 则是一个强大的 ORM 框架,用于管理数据库交互。将两者结合使用,可以极大提升开发效率并构建高性能的现代 Java 应用。本文将通过代码示例展示如何在 Spring Boot 项目中集成 Hibernate,并实现基本的数据库操作,包括添加依赖、配置数据源、创建实体类和仓库接口,以及在服务层和控制器中处理 HTTP 请求。这种组合不仅简化了配置,还提供了一套强大的工具来快速开发现代 Java 应用程序。
48 0
|
27天前
|
数据库 开发者 Java
Hibernate映射注解的魔力:实体类配置的革命,让你的代码量瞬间蒸发!
【8月更文挑战第31天】Hibernate 是一款出色的对象关系映射框架,简化了 Java 应用与数据库的交互。其映射注解让实体类配置变得直观简洁。本文深入剖析核心概念与使用技巧,通过示例展示如何简化配置。
23 0
|
27天前
|
数据库 开发者 Java
数据战争:Hibernate的乐观与悲观锁之争,谁将主宰并发控制的王座?
【8月更文挑战第31天】在软件开发中,数据一致性至关重要,尤其是在多用户并发访问环境下。Hibernate 作为 Java 社区常用的 ORM 框架,提供了乐观锁和悲观锁机制来处理并发问题。乐观锁假设数据不易冲突,通过版本号字段 (`@Version`) 实现;悲观锁则假定数据易冲突,在读取时即加锁。选择哪种锁取决于具体场景:乐观锁适合读多写少的情况,减少锁开销;悲观锁适合写操作频繁的场景,避免数据冲突。正确应用这些机制可提升应用程序的健壮性和效率。
28 0
|
27天前
|
Java Maven Android开发
解锁Web开发新技能:从零开始的Struts 2之旅——让你的Java编程之路更加宽广,首个应用实例带你飞!
【8月更文挑战第31天】对于初学者,掌握 Struts 2 框架不仅能提升 Web 开发能力,还能深入了解 MVC 架构。Struts 2 是一个基于 Servlet 的 Java 框架,提供表单验证、文件上传、国际化等功能,便于快速构建易维护的 Web 应用。本文通过示例演示如何从零开始搭建环境并创建一个简单的 Struts 2 项目,包括配置 `struts.xml`、编写 Action 类及视图文件,并配置 web.xml。通过这些步骤,你将学会基本的开发流程,为进一步学习高级功能打下基础。
33 0