轻识Logo
目录

    好未来开源框架 go-zero:如何用它进行 rest 开发?

    go-zero 是一个集成了各种工程实践的 web 和 rpc 框架,其中 rest 是 web 框架模块,基于 Go 语言原生的 http 包进行构建,是一个轻量的,高性能的,功能完整的,简单易用的 web 框架。

    服务创建

    go-zero 中创建 http 服务非常简单,官方推荐使用goctl[1]工具来生成。为了方便演示,这里通过手动创建服务,代码如下

    package main

    import (
     "log"
     "net/http"

     "github.com/tal-tech/go-zero/core/logx"
     "github.com/tal-tech/go-zero/core/service"
     "github.com/tal-tech/go-zero/rest"
     "github.com/tal-tech/go-zero/rest/httpx"
    )

    func main() {
     srv, err := rest.NewServer(rest.RestConf{
      Port: 9090// 侦听端口
      ServiceConf: service.ServiceConf{
       Log: logx.LogConf{Path: "./logs"}, // 日志路径
      },
     })
     if err != nil {
      log.Fatal(err)
     }
     defer srv.Stop()
     // 注册路由
     srv.AddRoutes([]rest.Route{
      {
       Method:  http.MethodGet,
       Path:    "/user/info",
       Handler: userInfo,
      },
     })

     srv.Start() // 启动服务
    }

    type User struct {
     Name  string `json:"name"`
     Addr  string `json:"addr"`
     Level int    `json:"level"`
    }

    func userInfo(w http.ResponseWriter, r *http.Request) {
     var req struct {
      UserId int64 `form:"user_id"` // 定义参数
     }
     if err := httpx.Parse(r, &req); err != nil { // 解析参数
      httpx.Error(w, err)
      return
     }
     users := map[int64]*User{
      1: &User{"go-zero""shanghai"1},
      2: &User{"go-queue""beijing"2},
     }
     httpx.WriteJson(w, http.StatusOK, users[req.UserId]) // 返回结果
    }

    通过 rest.NewServer 创建服务,示例配置了端口号和日志路径,服务启动后侦听在 9090 端口,并在当前目录下创建 logs 目录同时创建各等级日志文件

    然后通过 srv.AddRoutes 注册路由,每个路由需要定义该路由的方法、Path 和 Handler,其中 Handler 类型为 http.HandlerFunc

    最后通过 srv.Start 启动服务,启动服务后通过访问http://localhost:9090/user/info?user_id=1可以看到返回结果

    {
     name: "go-zero",
     addr: "shanghai",
     level: 1
    }

    到此一个简单的 http 服务就创建完成了,可见使用 rest 创建 http 服务非常简单,主要分为三个步骤:创建 Server、注册路由、启动服务

    JWT 鉴权

    鉴权几乎是每个应用必备的能力,鉴权的方式很多,而 jwt 是其中比较简单和可靠的一种方式,在 rest 框架中内置了 jwt 鉴权功能,jwt 的原理流程如下图

    rest 框架中通过 rest.WithJwt(secret)启用 jwt 鉴权,其中 secret 为服务器秘钥是不能泄露的,因为需要使用 secret 来算签名验证 payload 是否被篡改,如果 secret 泄露客户端就可以自行签发 token,黑客就能肆意篡改 token 了。我们基于上面的例子进行改造来验证在 rest 中如何使用 jwt 鉴权

    获取 jwt

    第一步客户端需要先获取 jwt,在登录接口中实现 jwt 生成逻辑

    srv.AddRoute(rest.Route{
      Method:  http.MethodPost,
      Path:    "/user/login",
      Handler: userLogin,
    })

    为了演示方便,userLogin 的逻辑非常简单,主要是获取信息然后生成 jwt,获取到的信息存入 jwt payload 中,然后返回 jwt

    func userLogin(w http.ResponseWriter, r *http.Request) {
     var req struct {
      UserName string `json:"user_name"`
      UserId   int    `json:"user_id"`
     }
     if err := httpx.Parse(r, &req); err != nil {
      httpx.Error(w, err)
      return
     }
     token, _ := genToken(accessSecret, map[string]interface{}{
      "user_id":   req.UserId,
      "user_name": req.UserName,
     }, accessExpire)

     httpx.WriteJson(w, http.StatusOK, struct {
      UserId   int    `json:"user_id"`
      UserName string `json:"user_name"`
      Token    string `json:"token"`
     }{
      UserId:   req.UserId,
      UserName: req.UserName,
      Token:    token,
     })
    }

    生成 jwt 的方法如下

    func genToken(secret string, payload map[string]interface{}, expire int64) (string, error) {
     now := time.Now().Unix()
     claims := make(jwt.MapClaims)
     claims["exp"] = now + expire
     claims["iat"] = now
     for k, v := range payload {
      claims[k] = v
     }
     token := jwt.New(jwt.SigningMethodHS256)
     token.Claims = claims
     return token.SignedString([]byte(secret))
    }

    启动服务后通过 cURL 访问

    curl -X "POST" "http://localhost:9090/user/login" \
         -H 'Content-Type: application/json; charset=utf-8' \
         -d $'{
      "user_name": "gozero",
      "user_id": 666
    }'

    会得到如下返回结果

    {
      "user_id"666,
      "user_name""gozero",
      "token""eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE2MDYxMDgwNDcsImlhdCI6MTYwNTUwMzI0NywidXNlcl9pZCI6NjY2LCJ1c2VyX25hbWUiOiJnb3plcm8ifQ.hhMd5gc3F9xZwCUoiuFqAWH48xptqnNGph0AKVkTmqM"
    }

    添加 Header

    通过 rest.WithJwt(accessSecret)启用 jwt 鉴权

    srv.AddRoute(rest.Route{
      Method:  http.MethodGet,
      Path:    "/user/data",
      Handler: userData,
    }, rest.WithJwt(accessSecret))

    访问/user/data 接口返回 401 Unauthorized 鉴权不通过,添加 Authorization Header,即能正常访问

    curl "http://localhost:9090/user/data?user_id=1" \
          -H 'Authorization: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE2MDYxMDgwNDcsImlhdCI6MTYwNTUwMzI0NywidXNlcl9pZCI6NjY2LCJ1c2VyX25hbWUiOiJnb3plcm8ifQ.hhMd5gc3F9xZwCUoiuFqAWH48xptqnNGph0AKVkTmqM'

    获取信息

    一般会将用户的信息比如用户 id 或者用户名存入 jwt 的 payload 中,然后从 jwt 的 payload 中解析出我们预存的信息,即可知道本次请求时哪个用户发起的

    func userData(w http.ResponseWriter, r *http.Request) {
     var jwt struct {
      UserId   int    `ctx:"user_id"`
      UserName string `ctx:"user_name"`
     }
     err := contextx.For(r.Context(), &jwt)
     if err != nil {
      httpx.Error(w, err)
     }
     httpx.WriteJson(w, http.StatusOK, struct {
      UserId   int    `json:"user_id"`
      UserName string `json:"user_name"`
     }{
      UserId:   jwt.UserId,
      UserName: jwt.UserName,
     })
    }

    实现原理

    jwt 鉴权的实现在 authhandler.go 中,实现原理也比较简单,先根据 secret 解析 jwt token,验证 token 是否有效,无效或者验证出错则返回 401 Unauthorized

    func unauthorized(w http.ResponseWriter, r *http.Request, err error, callback UnauthorizedCallback) {
     writer := newGuardedResponseWriter(w)

     if err != nil {
      detailAuthLog(r, err.Error())
     } else {
      detailAuthLog(r, noDetailReason)
     }
     if callback != nil {
      callback(writer, r, err)
     }

     writer.WriteHeader(http.StatusUnauthorized)
    }

    验证通过后把 payload 中的信息存入 http request 的 context 中

    ctx := r.Context()
    for k, v := range claims {
      switch k {
        case jwtAudience, jwtExpire, jwtId, jwtIssueAt, jwtIssuer, jwtNotBefore, jwtSubject:
        // ignore the standard claims
        default:
        ctx = context.WithValue(ctx, k, v)
      }
    }

    next.ServeHTTP(w, r.WithContext(ctx))

    中间件

    web 框架中的中间件是实现业务和非业务功能解耦的一种方式,在 web 框架中我们可以通过中间件来实现诸如鉴权、限流、熔断等等功能,中间件的原理流程如下图

    handler

    rest 框架中内置了非常丰富的中间件,在 rest/handler 路径下,通过alice[2]工具把所有中间件链接起来,当发起请求时会依次通过每一个中间件,当满足所有条件后最终请求才会到达真正的业务 Handler 执行业务逻辑,上面介绍的 jwt 鉴权就是通过 authHandler 来实现的。由于内置中间件比较多篇幅有限不能一一介绍,感兴趣的伙伴可以自行学习,这里我们介绍一下 prometheus 指标收集的中间件 PromethousHandler,代码如下

    func PromethousHandler(path string) func(http.Handler) http.Handler {
     return func(next http.Handler) http.Handler {
      return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
       startTime := timex.Now() // 起始时间
       cw := &security.WithCodeResponseWriter{Writer: w}
       defer func() {
            // 耗时
        metricServerReqDur.Observe(int64(timex.Since(startTime)/time.Millisecond), path)
            // code码
        metricServerReqCodeTotal.Inc(path, strconv.Itoa(cw.Code))
       }()

       next.ServeHTTP(cw, r)
      })
     }
    }

    在该中间件中,在请求开始时记录了起始时间,在请求结束后在 defer 中通过 prometheus 的 Histogram 和 Counter 数据类型分别记录了当前请求 path 的耗时和返回的 code 码,此时我们通过访问http://127.0.0.1:9101/metrics即可查看相关的指标信息

    rest_metric

    路由原理

    rest 框架中通过 AddRoutes 方法来注册路由,每一个 Route 有 Method、Path 和 Handler 三个属性,Handler 类型为 http.HandlerFunc,添加的路由会被换成 featuredRoutes 定义如下

    featuredRoutes struct {
      priority  bool // 是否优先级
      jwt       jwtSetting  // jwt配置
      signature signatureSetting // 验签配置
      routes    []Route  // 通过AddRoutes添加的路由
     }

    featuredRoutes 通过 engine 的 AddRoutes 添加到 engine 的 routes 属性中

    func (s *engine) AddRoutes(r featuredRoutes) {
     s.routes = append(s.routes, r)
    }

    调用 Start 方法启动服务后会调用 engine 的 Start 方法,然后会调用 StartWithRouter 方法,该方法内通过 bindRoutes 绑定路由

    func (s *engine) bindRoutes(router httpx.Router) error {
     metrics := s.createMetrics()

     for _, fr := range s.routes {
      if err := s.bindFeaturedRoutes(router, fr, metrics); err != nil { // 绑定路由
       return err
      }
     }

     return nil
    }

    最终会调用 patRouter 的 Handle 方法进行绑定,patRouter 实现了 Router 接口

    type Router interface {
     http.Handler
     Handle(method string, path string, handler http.Handler) error
     SetNotFoundHandler(handler http.Handler)
     SetNotAllowedHandler(handler http.Handler)
    }

    patRouter 中每一种请求方法都对应一个树形结构,每个树节点有两个属性 item 为 path 对应的 handler,而 children 为带路径参数和不带路径参数对应的树节点, 定义如下:

    node struct {
      item     interface{}
      children [2]map[string]*node
    }

    Tree struct {
      root *node
    }

    通过 Tree 的 Add 方法把不同 path 与对应的 handler 注册到该树上我们通过一个图来展示下该树的存储结构,比如我们定义路由如下

    {
      Method:  http.MethodGet,
      Path:    "/user",
      Handler: userHander,
    },
    {
      Method:  http.MethodGet,
      Path:    "/user/infos",
      Handler: infosHandler,
    },
    {
      Method:  http.MethodGet,
      Path:    "/user/info/:id",
      Handler: infoHandler,
    },

    路由存储的树形结构如下图

    当请求来的时候会调用 patRouter 的 ServeHTTP 方法,在该方法中通过 tree.Search 方法找到对应的 handler 进行执行,否则会执行 notFound 或者 notAllow 的逻辑

    func (pr *patRouter) ServeHTTP(w http.ResponseWriter, r *http.Request) {
     reqPath := path.Clean(r.URL.Path)
     if tree, ok := pr.trees[r.Method]; ok {
      if result, ok := tree.Search(reqPath); ok { // 在树中搜索对应的handler
       if len(result.Params) > 0 {
        r = context.WithPathVars(r, result.Params)
       }
       result.Item.(http.Handler).ServeHTTP(w, r)
       return
      }
     }

     allow, ok := pr.methodNotAllowed(r.Method, reqPath)
     if !ok {
      pr.handleNotFound(w, r)
      return
     }

     if pr.notAllowed != nil {
      pr.notAllowed.ServeHTTP(w, r)
     } else {
      w.Header().Set(allowHeader, allow)
      w.WriteHeader(http.StatusMethodNotAllowed)
     }
    }

    总结

    本文从整体上介绍了 rest,通过该篇文章能够基本了解 rest 的设计和主要功能,其中中间件部分是重点,里面集成了各种服务治理相关的功能,并且是自动集成的不需要我们做任何配置,其他功能比如参数自动效验等功能由于篇幅有限在这里就不做介绍了,感兴趣的朋友可以自行查看官方文档进行学习。go-zero 中不光有 http 协议还提供了 rpc 协议和各种提高性能和开发效率的工具,是一款值得我们深入学习和研究的框架。

    项目地址

    https://github.com/tal-tech/go-zero[3]

    如果觉得文章不错,欢迎 github[4] 点个 star ?

    参考资料

    [1]

    goctl: https://github.com/tal-tech/go-zero/tree/master/tools/goctl

    [2]

    alice: https://github.com/justinas/alice

    [3]

    https://github.com/tal-tech/go-zero: https://github.com/tal-tech/go-zero

    [4]

    github: https://github.com/tal-tech/go-zero



    推荐阅读

    • Go 基准测试还可以这么搞?高级基准测试


    福利

    我为大家整理了一份从入门到进阶的Go学习资料礼包,包含学习建议:入门看什么,进阶看什么。关注公众号 「polarisxu」,回复 ebook 获取;还可以回复「进群」,和数万 Gopher 交流学习。


    浏览 56
    点赞
    评论
    收藏
    分享

    手机扫一扫分享

    举报
    libCVDC++多媒体处理库
    libCVD 是一个高可移植和具备高性能的用于处理计算机视觉、图像、视频的C++库。其重点是提供简单
    libCVDC++多媒体处理库
    0
    libCVDC++多媒体处理库
    libCVD是一个高可移植和具备高性能的用于处理计算机视觉、图像、视频的C++库。其重点是提供简单高效的高质量的图形和视频处理方法。示例代码:  CVD; main(){  Image
    libCVDC++多媒体处理库
    0
    JffmpegJava多媒体处理工具包
    Jffmpeg是一个Java的多媒体框架插件,可以用来播放大部分格式的音频和视频格式的文件,基于ffmpeg项目开发,支持多种纯Java的编码器,使用JNI方式来调用ffmpeg进行多媒体格式文件处理
    JffmpegJava多媒体处理工具包
    0
    pipewire多媒体处理工具
    PipeWire 是用于处理多媒体管道的服务器和用户空间 API 的多媒体处理工具,包括:提供视频源
    pipewire多媒体处理工具
    0
    pipewire多媒体处理工具
    PipeWire是用于处理多媒体管道的服务器和用户空间API的多媒体处理工具,包括:提供视频源(例如来自捕获设备或应用程序提供的流),并将其与客户端复用。访问视频源进行消费。生成用于音频和视频处理的图
    pipewire多媒体处理工具
    0
    JffmpegJava多媒体处理工具包
    Jffmpeg 是一个 Java 的多媒体框架插件,可以用来播放大部分格式的音频和视频格式的文件,基
    JffmpegJava多媒体处理工具包
    0
    FFmpeg多媒体处理工具
    Fmpeg是领先的多媒体框架,能够解码、编码、转码、混合、解密、流媒体、过滤和播放人类和机器创造的几乎所有东西。它支持最晦涩的古老格式,直到最尖端的格式。无论它们是由某个标准委员会、社区还是公司设计的
    FFmpeg多媒体处理工具
    0
    Git mediaGit 多媒体处理组件
    Git media 可能是可供选择的最古老的多媒体处理方案。 Git media使用类似过滤器,并支
    Git mediaGit 多媒体处理组件
    0
    svensonJSON处理包
    svenson 是一个Java语言用来生成和解析 JSON 数据的Java类库,支持各种类型数据和集
    svensonJSON处理包
    0
    点赞
    评论
    收藏
    分享

    手机扫一扫分享

    举报

    聚圣源汪姓鼠佳宝宝起名字医妃倾天下女孩用爱字起名字起一个企业名字秦放的真实身份洛阳宝宝起名哪家好长征小故事起名大全姓梁女孩穿越火线烟雾头盔怎么调西安区号魔石咕噜鱼加盟怎么起英文名字tubecao进口商品商标注册起名我本刀仙康熙字典三画的起名吉利字免费婴儿起名宝典给姓彭什么宝宝起名字科技公司起名字水果商标起名米读小说赛百味男人起名肖字取名起名大全手机归属photoshopcs4请示范文金鼠宝宝起乳名超级电力强国姓段的怎么起名字淀粉肠小王子日销售额涨超10倍罗斯否认插足凯特王妃婚姻让美丽中国“从细节出发”清明节放假3天调休1天男孩疑遭霸凌 家长讨说法被踢出群国产伟哥去年销售近13亿网友建议重庆地铁不准乘客携带菜筐雅江山火三名扑火人员牺牲系谣言代拍被何赛飞拿着魔杖追着打月嫂回应掌掴婴儿是在赶虫子山西高速一大巴发生事故 已致13死高中生被打伤下体休学 邯郸通报李梦为奥运任务婉拒WNBA邀请19岁小伙救下5人后溺亡 多方发声王树国3次鞠躬告别西交大师生单亲妈妈陷入热恋 14岁儿子报警315晚会后胖东来又人满为患了倪萍分享减重40斤方法王楚钦登顶三项第一今日春分两大学生合买彩票中奖一人不认账张家界的山上“长”满了韩国人?周杰伦一审败诉网易房客欠租失踪 房东直发愁男子持台球杆殴打2名女店员被抓男子被猫抓伤后确诊“猫抓病”“重生之我在北大当嫡校长”槽头肉企业被曝光前生意红火男孩8年未见母亲被告知被遗忘恒大被罚41.75亿到底怎么缴网友洛杉矶偶遇贾玲杨倩无缘巴黎奥运张立群任西安交通大学校长黑马情侣提车了西双版纳热带植物园回应蜉蝣大爆发妈妈回应孩子在校撞护栏坠楼考生莫言也上北大硕士复试名单了韩国首次吊销离岗医生执照奥巴马现身唐宁街 黑色着装引猜测沈阳一轿车冲入人行道致3死2伤阿根廷将发行1万与2万面值的纸币外国人感慨凌晨的中国很安全男子被流浪猫绊倒 投喂者赔24万手机成瘾是影响睡眠质量重要因素春分“立蛋”成功率更高?胖东来员工每周单休无小长假“开封王婆”爆火:促成四五十对专家建议不必谈骨泥色变浙江一高校内汽车冲撞行人 多人受伤许家印被限制高消费

    聚圣源 XML地图 TXT地图 虚拟主机 SEO 网站制作 网站优化