字节开源 Go HTTP 框架 - Hertz 入门实践 | 青训营笔记

1,151 阅读8分钟

这是我参与「第五届青训营」笔记创作活动的第12天

入门 Hertz 框架~

前言:Hertz 项目缘起

在前面的几篇笔记中,我们了解到了 Go 的常见框架:Web 框架 GinOR Mapping 框架 Gorm 以及 RPC 框架 KiteX,看起来已经有了一套完整的框架体系了,为什么还需要 Hertz 呢?

最初,字节跳动内部的 HTTP 框架是对 Gin 框架的封装,而 Gin 是对 Golang 原生 net/http 进行的二次开发,在按需扩展和性能优化上受到很大局限。

因此,为了满足业务需求,更好的服务各大业务线,2020 年初,字节跳动服务框架团队开始基于自研网络库 Netpoll 开发内部框架 Hertz,让 Hertz 在面对企业级需求时,有更好的性能及稳定性表现,也能够满足业务发展和应对不断演进的技术需求。


Herzt 概览

Hertz [həːts] 是一个 Golang 微服务 HTTP 框架,在设计之初参考了其他开源框架 fasthttpginecho 的优势, 并结合字节跳动内部的需求,使其具有高易用性、高性能、高扩展性等特点,目前在字节跳动内部已广泛使用。

image.png

🎈官方站点:CloudWeGo

相比于其他框架,Hertz 具有高易用性、高性能、高扩展性、多协议支持以及网络层切换能力等特性,同时 Hertz 默认集成了自研高性能网络库 Netpoll,在性能上也有不俗的表现。

Hertz 架构设计:

Hertz 采用了 4 层分层设计,保证各个层级功能内聚,同时通过层级之间的接口达到灵活扩展的目标。

image.png
  • 网络层扩展:原生支持基于连接与基于流的两大类网络库;
  • 协议层扩展:支持 HTTP/1.1、HTTP/2、HTTP/3、Websocket以及自定义协议;
  • 应用层扩展:支持 pprof、gzip、i18n、csrf、反向代理等常用中间件扩展。

Hertz 还提供了脚手架工具 Hz,根据接口定义(IDL)自动生成项目骨架,帮助业务聚焦核心逻辑。


第一个 Hertz 程序

接下来让我们参照官方文档,尝试使用 Hertz 框架搭建一个最简单的 Web 服务 - ping pong

1. 环境准备:

目前 Hertz 支持 Linux、macOS、Windows 系统,需要 Golang 版本在 v1.15 以上(其他版本不保证稳定性和兼容性),并且确保打开 Go mod 支持。

💻我的开发环境:

  • Go IDE:GoLand v2021.1.3
  • 系统环境:Win10 + WSL - Ubuntu
  • Go版本:Golang v1.17

2. 安装 hz:

hz 是 Hertz 的命令行工具,安装之前需要确保 GOPATH 环境变量已经被正确定义。

PS: 下载 Go 模块前建议先配置代理:七牛云 - Goproxy.cn

在 GoLand 中创建项目 hertz_demo,打开 Terminal 终端,执行安装命令:

$ go install github.com/cloudwego/hertz/cmd/hz@latest

3. 获取/编写代码:

如果是 Linux 平台,直接使用如下命令拉取示例代码:

$ hz new

PS: 报错 “模块名在 go.mod 中没有定义”,可以使用 go mod tidy 命令自动整理 go.mod 文件。

如果是 Windows 环境,可以创建 main.go 文件手动编写示例代码:

package main

import (
    "context"
    "github.com/cloudwego/hertz/pkg/app"
    "github.com/cloudwego/hertz/pkg/app/server"
    "github.com/cloudwego/hertz/pkg/common/utils"
    "github.com/cloudwego/hertz/pkg/protocol/consts"
)

func main() {
    h := server.Default()
    h.GET("/ping", func(c context.Context, ctx *app.RequestContext) {
            ctx.JSON(consts.StatusOK, utils.H{"message": "pong"})
    })
    h.Spin()
}

使用 go mod 整理 & 下载依赖包:

$ go mod tidy

PS:

  • server.Default() 默认监听 8888 端口,当然也可以手动指定端口号,只需传入一个参数:
    server.Default(server.WithHostPorts("127.0.0.1:8080"))
    
  • GET 是 Hertz 的路由方法,用于 Get 方式的请求。
  • 最后的 Spin() 方法用于启动服务器。

4. 运行示例代码:

完成上述操作后直接右键运行 main.go 文件或者使用指令运行:

$ go build -o hertz_demo && ./hertz_demo

使用 curl 命令请求 Web 服务器,测试我们刚刚创建的接口:

$ curl http://127.0.0.1:8888/ping

如果不出意外,我们可以看到类似如下输出:

$ {"message":"pong"}

或者直接使用浏览器访问 http://localhost:8888/ping,可以看到浏览器显示的 json 数据:

image.png

Hertz 基本使用

路由 & 分组

Hertz 提供了 GET、POST、PUT、DELETE 等方法用于注册路由,并且支持静态路由、参数路由和通配路由三种类型。

静态路由示例:

h := server.Default()

h.GET("/get", func(ctx context.Context, c *app.RequestContext) {
   c.String(consts.StatusOK, "get")
})
h.POST("/post", func(ctx context.Context, c *app.RequestContext) {
   c.String(consts.StatusOK, "post")
})
h.PUT("/put", func(ctx context.Context, c *app.RequestContext) {
   c.String(consts.StatusOK, "put")
})
h.DELETE("/delete", func(ctx context.Context, c *app.RequestContext) {
   c.String(consts.StatusOK, "delete")
})

// ...

其中 consts 是 Hertz 自带的状态码常量包,用来代替 Go 原生的 http 包。

参数路由就是我们常常听到的 Restful 风格,使用 :name 这样的命名参数设置路由(命名参数只能匹配单个字段)。

h := server.Default()

// url:http://localhost:8888/user/jack
h.GET("/user/:name", func(ctx context.Context, c *app.RequestContext) {
    name := c.Param("name")
    c.String(consts.StatusOK, "Hello %s", name)
})

// ...

通配路由使用 *parm 这样的通配参数设置路由,并且通配参数会匹配所有内容。

Hertz 提供了路由组 Group 的能力,用于支持路由分组的功能,同时中间件也可以注册到路由组上。

h := server.Default()

v1 := h.Group("/v1")
v1.GET("/get", func(ctx context.Context, c *app.RequestContext) {
    c.String(consts.StatusOK, "get")
})
v1.POST("/post", func(ctx context.Context, c *app.RequestContext) {
    c.String(consts.StatusOK, "post")
})

// use middleware
v2 := h.Group("/v2", basic_auth.BasicAuth(map[string]string{"test": "test"}))

// ...

中间件

Hertz 中间件的种类多种多样,可以简单分为两大类:服务端中间件、客户端中间件。

服务端中间件

Hertz 服务端中间件是 HTTP 请求-响应周期中的一个函数,提供了一种方便的机制来检查和过滤进入应用程序的 HTTP 请求, 例如记录每个请求或者启用跨域访问。

image.png

如果我们自己实现一个中间件,要注意上面图中所示的两种情况:

  1. 中间件可以在请求到达业务逻辑之前执行,比如执行身份认证和权限认证,当中间件只有初始化相关逻辑(pre-handler),可以省略最后的 .Next
  2. 中间件也可以在执行过业务逻辑之后执行,比如记录响应时间和从异常中恢复,如果业务处理完后还有其他处理逻辑(post-handler),或者或对函数调用链有强需求,则必须显式调用 .Next
// 情况一
func MyMiddleware() app.HandlerFunc {
  return func(ctx context.Context, c *app.RequestContext) {
    // pre-handle
    // ...
    c.Next(ctx)
  }
}

// 情况二
func MyMiddleware() app.HandlerFunc {
  return func(ctx context.Context, c *app.RequestContext) {
    c.Next(ctx) // call the next middleware(handler)
    // post-handle
    // ...
  }
}

中间件会按定义的先后顺序依次执行,如果想快速终止中间件调用,可以使用 Abort() 方法。

Hertz 支持在不同的作用范围使用中间件:

  1. Server 级别的中间件会对整个 server 的路由生效:
    h := server.Default()
    
    h.Use(GlobalMiddleware())
    
  2. 路由组级别中间件对当前路由组下的路径生效:
    h := server.Default()
    
    group := h.Group("/group")
    group.Use(GroupMiddleware())
    
  3. 单一路由级别中间件只对当前路径生效:
    h := server.Default(server.WithHostPorts("127.0.0.1:8888"))
    
    h.GET("/path", append(PathMiddleware(),
    func(ctx context.Context, c *app.RequestContext) {
            c.String(http.StatusOK, "path")
    })...)
    
客户端中间件

客户端中间件实现和服务端中间件不同。Client 侧无法拿到中间件 index 实现递增,因此 Client 中间件采用提前构建嵌套函数的形式实现,在实现一个中间件时,可以参考下面的代码。

func MyMiddleware(next client.Endpoint) client.Endpoint {
  return func(ctx context.Context, req *protocol.Request, resp *protocol.Response) (err error) {
    // pre-handle
    // ...
    err = next(ctx, req, resp)
    if err != nil {
      return
    }
    // post-handle
    // ...
  }
}

Hertz 提供了常用的 BasicAuth、CORS、JWT 等中间件,更多实现可以在 hertz-contrib 查找。

参数获取

Query 函数可以获取到路径参数,主要用于 Get 请求。

// url:http://localhost:8888/user?name=jack&pwd=123
h.GET("/user", func(ctx context.Context, c *app.RequestContext) {
    name, pwd := c.Query("name"), c.Query("pwd")
    c.String(consts.StatusOK, "name: %s pwd: %s", name, pwd)
})
image.png

获取 Restful 风格的路径参数,可以使用 Param 方法:

// url:http://localhost:8888/user/jack/123
h.GET("/user/:name/:pwd", func(ctx context.Context, c *app.RequestContext) {
    name, pwd := c.Param("name"), c.Param("pwd")
    c.JSON(consts.StatusOK, utils.H{
       "name": name,
       "password": pwd,
    })
})

获取表单参数可以使用 PostForm 或者 GetPostForm 方法,区别在于 GetPostForm 除了返回参数以外还会返回一个布尔值,用来判断指定的参数是否存在。

h.POST("/form", func(ctx context.Context, c *app.RequestContext) {
    name := c.PostForm("name")
    c.String(consts.StatusOK, name)
})
h.POST("/form", func(ctx context.Context, c *app.RequestContext) {
    if name, ok := c.GetPostForm("name"); ok {
       c.String(consts.StatusOK, name)
    }
})

使用 Body 方法可以获取请求 body:

h.GET("/getBody", func(ctx context.Context, c *app.RequestContext) {
    type Person struct {
       Age int `json:"age"`
       Name string `json:"name"`
    }
    body, err := c.Body()
    if err != nil {
       panic(err)
    }
    var p Person
    if err := json.Unmarshal(body, &p); err != nil {
       panic(err)
    }
    c.JSON(consts.StatusOK, utils.H{"person": p})
})

参数绑定

Hertz 提供了参数绑定,可以非常优雅的完成请求参数映射到结构体与请求参数的验证。

func main() {
    r := server.New()

    r.GET("/hello", func(c context.Context, ctx *app.RequestContext) {
        // 参数绑定需要配合特定的 go tag 使用
	type Test struct {
            A string `query:"a" vd:"$!='Hertz'"`
            B string `json:"b"`
        }

        // BindAndValidate
        var req Test
        err := ctx.BindAndValidate(&req)

        // ...

	// Bind
        req = Test{}
        err = ctx.Bind(&req)

        ...

        // Validate,需要使用 "vd" tag
        err = ctx.Validate(&req)

        // ...
    })
// ...
}

支持的 go tag 如下:

go tag说明
path绑定 url 上的路径参数,相当于 hertz 路由{:param}或{*param}中拿到的参数。例如:如果定义的路由为: /v:version/example,可以把 path 的参数指定为路由参数:path:"version",此时,url: http://127.0.0.1:8888/v1/example,可以绑定path参数"1"
form绑定请求的 body 内容。content-type -> multipart/form-dataapplication/x-www-form-urlencoded,绑定 form 的 key-value
query绑定请求的 query 参数
header绑定请求的 header 参数
json绑定请求的 body 内容 content-type -> application/json,绑定 json 参数
raw_body绑定请求的原始 body(bytes),绑定的字段名不指定,也能绑定参数。(注:raw_body 绑定优先级最低,当指定多个 tag 时,一旦其他 tag 成功绑定参数,则不会绑定 body 内容。)
vd参数校验,校验语法

文件上传下载 & 流式处理

Hertz 提供了简便的文件上传与下载方法,参考代码如下:

h.POST("/upload", func(ctx context.Context, c *app.RequestContext) {
    // 获取文件
    fileHeader, err := c.FormFile("file")
    if err != nil {
       panic(err)
    }
    open, err := fileHeader.Open()
    if err != nil {
       panic(err)
    }
    // 读取文件到字节数组
    fileRaw, err := ioutil.ReadAll(open)
    if err != nil {
       panic(err)
    }
    // 将读取到的文件写入到响应
    if _, err = c.Write(fileRaw); err != nil {
       panic(err)
    }
})

HTTP 的文件场景是十分常见的场景,除了 Server 侧的上传场景之外,Client 的下载场景也十分常见。为此,Hertz 支持了 Server 和 Client 的流式处理,开启方式只需要添加上全局配置即可。

Server 开启流式:

import (
    "github.com/cloudwego/hertz/pkg/app/server"
    "github.com/cloudwego/hertz/pkg/network/standard"
    )

func main() {
    h := server.New(
        server.WithStreamBody(),
        server.WithTransport(standard.NewTransporter),
    )
    // ...
}

Client 开启流式:

c, err := client.NewClient(client.WithResponseBodyStream(true))

PS: 参考文档可以了解更多 Hertz 高级用法 - Hertz | CloudWeGo


Hertz 生态

Hertz 拥有非常丰富的扩展生态:


🚀【参考】🚀

  1. 青训营直播课程:Go 语言框架三件套
  2. 青训营直播课程:Hertz 专场直播
  3. 字节跳动开源 Go HTTP 框架 Hertz 设计实践_文化 & 方法_字节跳动技术团队_InfoQ精选文章
  4. hertz 概览 | CloudWeGo
  5. 中间件概览 | CloudWeGo
  6. 字节开源WEB框架Hertz太香啦!_hertz框架_一堆土豆33的博客-CSDN博客