2020最佳人气项目之Go Web框架

简介: 2020最佳人气项目之Go Web框架

go-zero 是一个集成了各种工程实践的 web 和 rpc 框架,其中rest是web框架模块,基于Go语言原生的http包进行构建,是一个轻量的,高性能的,功能完整的,简单易用的web框架。获得了2020码云最有价值项目和开源中国最佳人气项目奖。

服务创建

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

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 := rest.MustNewServer(rest.RestConf{
        Port: 9090, // 侦听端口
        ServiceConf: service.ServiceConf{
            Log: logx.LogConf{Path: "./logs"}, // 日志路径
        },
    })
    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框架中我们可以通过中间件来实现诸如鉴权、限流、熔断等等功能,中间件的原理流程如下图

image.svg

rest框架中内置了非常丰富的中间件,在rest/handler路径下,通过alice工具把所有中间件链接起来,当发起请求时会依次通过每一个中间件,当满足所有条件后最终请求才会到达真正的业务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即可查看相关的指标信息

image.svg

路由原理

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

如果觉得文章不错,欢迎 github 点个 star 🤝

相关文章
|
2月前
|
Java API 数据库
构建RESTful API已经成为现代Web开发的标准做法之一。Spring Boot框架因其简洁的配置、快速的启动特性及丰富的功能集而备受开发者青睐。
【10月更文挑战第11天】本文介绍如何使用Spring Boot构建在线图书管理系统的RESTful API。通过创建Spring Boot项目,定义`Book`实体类、`BookRepository`接口和`BookService`服务类,最后实现`BookController`控制器来处理HTTP请求,展示了从基础环境搭建到API测试的完整过程。
48 4
|
2月前
|
XML JSON API
ServiceStack:不仅仅是一个高性能Web API和微服务框架,更是一站式解决方案——深入解析其多协议支持及简便开发流程,带您体验前所未有的.NET开发效率革命
【10月更文挑战第9天】ServiceStack 是一个高性能的 Web API 和微服务框架,支持 JSON、XML、CSV 等多种数据格式。它简化了 .NET 应用的开发流程,提供了直观的 RESTful 服务构建方式。ServiceStack 支持高并发请求和复杂业务逻辑,安装简单,通过 NuGet 包管理器即可快速集成。示例代码展示了如何创建一个返回当前日期的简单服务,包括定义请求和响应 DTO、实现服务逻辑、配置路由和宿主。ServiceStack 还支持 WebSocket、SignalR 等实时通信协议,具备自动验证、自动过滤器等丰富功能,适合快速搭建高性能、可扩展的服务端应用。
124 3
|
27天前
|
设计模式 前端开发 数据库
Python Web开发:Django框架下的全栈开发实战
【10月更文挑战第27天】本文介绍了Django框架在Python Web开发中的应用,涵盖了Django与Flask等框架的比较、项目结构、模型、视图、模板和URL配置等内容,并展示了实际代码示例,帮助读者快速掌握Django全栈开发的核心技术。
135 45
|
1天前
|
开发框架 Go 计算机视觉
纯Go语言开发人脸检测、瞳孔/眼睛定位与面部特征检测插件-助力GoFly快速开发框架
开发纯go插件的原因是因为目前 Go 生态系统中几乎所有现有的人脸检测解决方案都是纯粹绑定到一些 C/C++ 库,如 OpenCV 或 dlib,但通过 cgo 调用 C 程序会引入巨大的延迟,并在性能方面产生显著的权衡。此外,在许多情况下,在各种平台上安装 OpenCV 是很麻烦的。使用纯Go开发的插件不仅在开发时方便,在项目部署和项目维护也能省很多时间精力。
|
10天前
|
Go API 数据库
Go 语言中常用的 ORM 框架,如 GORM、XORM 和 BeeORM,分析了它们的特点、优势及不足,并从功能特性、性能表现、易用性和社区活跃度等方面进行了比较,旨在帮助开发者根据项目需求选择合适的 ORM 框架。
本文介绍了 Go 语言中常用的 ORM 框架,如 GORM、XORM 和 BeeORM,分析了它们的特点、优势及不足,并从功能特性、性能表现、易用性和社区活跃度等方面进行了比较,旨在帮助开发者根据项目需求选择合适的 ORM 框架。
34 4
|
10天前
|
中间件 Go API
Go语言中几种流行的Web框架,如Beego、Gin和Echo,分析了它们的特点、性能及适用场景,并讨论了如何根据项目需求、性能要求、团队经验和社区支持等因素选择最合适的框架
本文概述了Go语言中几种流行的Web框架,如Beego、Gin和Echo,分析了它们的特点、性能及适用场景,并讨论了如何根据项目需求、性能要求、团队经验和社区支持等因素选择最合适的框架。
37 1
|
24天前
|
JavaScript 前端开发 开发工具
web项目规范配置(husky、eslint、lint-staged、commit)
通过上述配置,可以确保在Web项目开发过程中自动进行代码质量检查和规范化提交。Husky、ESLint、lint-staged和Commitlint共同作用,使得每次提交代码之前都会自动检查代码风格和语法问题,防止不符合规范的代码进入代码库。这不仅提高了代码质量,还保证了团队协作中的一致性。希望这些配置指南能帮助你建立高效的开发流程。
34 5
|
23天前
|
SQL 安全 PHP
探索PHP的现代演进:从Web开发到框架创新
PHP是一种流行的服务器端脚本语言,自诞生以来在Web开发领域占据重要地位。从简单的网页脚本到支持面向对象编程的现代语言,PHP经历了多次重大更新。本文探讨PHP的现代演进历程,重点介绍其在Web开发中的应用及框架创新,如Laravel、Symfony等。这些框架不仅简化了开发流程,还提高了开发效率和安全性。
25 3
|
22天前
|
前端开发 JavaScript 开发工具
从框架到现代Web开发实践
从框架到现代Web开发实践
33 1
|
25天前
|
SQL 安全 PHP
探索PHP的现代演进:从Web开发到框架创新
PHP 自发布以来一直在 Web 开发领域占据重要地位,历经多次重大更新,从简单的脚本语言进化为支持面向对象编程的现代语言。本文探讨 PHP 的演进历程,重点介绍其在 Web 开发中的应用及框架创新。自 PHP 5.3 引入命名空间后,PHP 迈向了面向对象编程时代;PHP 7 通过优化内核大幅提升性能;PHP 8 更是带来了属性、刚性类型等新特性。
26 3