这是我参与「第五届青训营」笔记创作活动的第12天
入门 Hertz 框架~
前言:Hertz 项目缘起
在前面的几篇笔记中,我们了解到了 Go 的常见框架:Web 框架 Gin、OR Mapping 框架 Gorm 以及 RPC 框架 KiteX,看起来已经有了一套完整的框架体系了,为什么还需要 Hertz 呢?
最初,字节跳动内部的 HTTP 框架是对 Gin 框架的封装,而 Gin 是对 Golang 原生 net/http 进行的二次开发,在按需扩展和性能优化上受到很大局限。
因此,为了满足业务需求,更好的服务各大业务线,2020 年初,字节跳动服务框架团队开始基于自研网络库 Netpoll 开发内部框架 Hertz,让 Hertz 在面对企业级需求时,有更好的性能及稳定性表现,也能够满足业务发展和应对不断演进的技术需求。
Herzt 概览
Hertz [həːts] 是一个 Golang 微服务 HTTP 框架,在设计之初参考了其他开源框架 fasthttp、gin、echo 的优势, 并结合字节跳动内部的需求,使其具有高易用性、高性能、高扩展性等特点,目前在字节跳动内部已广泛使用。
🎈官方站点:CloudWeGo
相比于其他框架,Hertz 具有高易用性、高性能、高扩展性、多协议支持以及网络层切换能力等特性,同时 Hertz 默认集成了自研高性能网络库 Netpoll,在性能上也有不俗的表现。
Hertz 架构设计:
Hertz 采用了 4 层分层设计,保证各个层级功能内聚,同时通过层级之间的接口达到灵活扩展的目标。
- 网络层扩展:原生支持基于连接与基于流的两大类网络库;
- 协议层扩展:支持 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 数据:
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 请求, 例如记录每个请求或者启用跨域访问。
如果我们自己实现一个中间件,要注意上面图中所示的两种情况:
- 中间件可以在请求到达业务逻辑之前执行,比如执行身份认证和权限认证,当中间件只有初始化相关逻辑(pre-handler),可以省略最后的
.Next
; - 中间件也可以在执行过业务逻辑之后执行,比如记录响应时间和从异常中恢复,如果业务处理完后还有其他处理逻辑(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 支持在不同的作用范围使用中间件:
- Server 级别的中间件会对整个 server 的路由生效:
h := server.Default() h.Use(GlobalMiddleware())
- 路由组级别中间件对当前路由组下的路径生效:
h := server.Default() group := h.Group("/group") group.Use(GroupMiddleware())
- 单一路由级别中间件只对当前路径生效:
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)
})
获取 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-data 或 application/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 拥有非常丰富的扩展生态:
- JWT 鉴权扩展 - github.com/hertz-contr…
- 国际化 - github.com/hertz-contr…
- 反向代理 - github.com/hertz-contr…
- opentelemetry 扩展 - github.com/hertz-contr…
- Websocket 扩展 - github.com/hertz-contr…
🚀【参考】🚀
- 青训营直播课程:Go 语言框架三件套
- 青训营直播课程:Hertz 专场直播
- 字节跳动开源 Go HTTP 框架 Hertz 设计实践_文化 & 方法_字节跳动技术团队_InfoQ精选文章
- hertz 概览 | CloudWeGo
- 中间件概览 | CloudWeGo
- 字节开源WEB框架Hertz太香啦!_hertz框架_一堆土豆33的博客-CSDN博客