初识Go框架三件套之Hertz|青训营笔记

242 阅读6分钟

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

一、本堂课重点内容:

HTTP 框架——Hertz简介

通过阅读 www.cloudwego.io/zh/docs/her… 尝试运行 Hertz 的示例代码

安装Hertz

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

示例

使用Hertz实现,服务监听8080端口并注册了一个GET方法的路由函数。

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()  //Default会默认继承recover中间件
    h := server.Default(server.WithHostPorts("127.0.0.1:8080")) //生成Hertz对象
    h.GET("/ping", func(c context.Context, ctx *app.RequestContext) {
            ctx.JSON(consts.StatusOK, utils.H{"message": "pong"}) //使用两个上下文
    })  //注册GET路由

    h.Spin()  //开启自旋,服务未停止之前,夯在此行,不再往下执行
}

一个简单的Hertz Server代码便完成了,经过buid后便可以进行调用服务了。具体步骤参考:www.cloudwego.io/zh/docs/her…

Hertz路由

路由注册

Hertz 提供了 GETPOSTPUTDELETEANY 等方法用于注册路由。 分别用于注册 HTTP Method 为 GET 的方法 、HTTP Method 为 POST 的方法等等,Hertz.Any用于注册所有 HTTP Method 方法。详见:路由注册

func RegisterRoute(h *server.Hertz){
    h := server.Default(server.WithHostPorts("127.0.0.1:8080"))

    h.StaticFS("/", &app.FS{Root: "./", GenerateIndexPages: true})

    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")
    })
}

路由组

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

func main(){
    h := server.Default(server.WithHostPorts("127.0.0.1:8080"))
    
    v1 := h.Group("/v1")  //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")
    })
    
    v2 := h.Group("/v2")  //v2组
    v2.PUT("/put", func(ctx context.Context, c *app.RequestContext) {
            c.String(consts.StatusOK, "put")
    })
    v2.DELETE("/delete", func(ctx context.Context, c *app.RequestContext) {
            c.String(consts.StatusOK, "delete")
    })
    h.Spin()
}

路由类型

Hertz 支持丰富的路由类型用于实现复杂的功能,包括静态路由、参数路由、通配路由。当同一个请求命中多条路由时,路由的优先级:静态路由 > 命名路由 > 通配路由

静态路由

上述示例中皆为静态路由

 h.GET("/get", func(ctx context.Context, c *app.RequestContext) {
    c.String(consts.StatusOK, "get")
})

参数路由

// This handler will match: "/hertz/version", but will not match : "/hertz/" or "/hertz"
h.GET("/hertz/:version", func(ctx context.Context, c *app.RequestContext) {
    version := c.Param("version")
    c.String(consts.StatusOK, "Hello %s", version)
})

通配路由

// However, this one will match "/hertz/v1/" and "/hertz/v2/send"
h.GET("/hertz/:version/*action", func(ctx context.Context, c *app.RequestContext) {
    version := c.Param("version")
    action := c.Param("action")
    message := version + " is " + action
    c.String(consts.StatusOK, message)
})

Hertz参数绑定

参数绑定的作用:帮助我们把HTTP请求里的参数转到结构体中。

hertz 使用开源库 go-tagexpr 进行参数的绑定及验证,提供了Bind、Validate、BindAndValidate函数用于进行参数绑定和校验。 不同于其他框架,hertz参数绑定需要配合特定的go tag使用(Validate,只需要使用 "vd" tag),具体的tag说明详见:www.cloudwego.io/zh/docs/her…

image.png

注:bind后是需要回写值的,所以需要传递地址。

Hertz中间件

Hertz中间件的种类是多种多样的,简单分为两大类:

  • 服务端中间件
  • 客户端中间件

中间件使用场景:当有一些通用的逻辑,比如打印日志,计算接口耗时,一些元信息的计算和传递等等。

服务端中间件:

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

func MyMiddleware() app.HandlerFunc {
  return func(ctx context.Context, c *app.RequestContext) {
    // pre-handle    // 方式一,先处理
    // ...
    c.Next(ctx) // call the next middleware(handler) 类似于洋葱模型
    // post-handle  // 方式二,后处理
    // ...
  }
}

中间件调用链: image.png 中间件可以在请求更深入地传递到业务逻辑之前或之后执行:

  • 中间件可以在请求到达业务逻辑之前执行,比如执行身份认证和权限认证,当中间件只有初始化(pre-handle)相关逻辑,且没有和 real handler 在一个函数调用栈中的需求时,中间件中可以省略掉最后的.Next,如图中间件 B。
  • 中间件也可以在执行过业务逻辑之后执行,比如记录响应时间和从异常中恢复。如果在业务 handler 处理之后有其它处理逻辑( post-handle ),或对函数调用链(栈)有强需求,则必须显式调用.Next,如图中间件 C。

注册服务端中间件

在服务端注册中间件分类三个层次,根据其注册的层次不同,中间件生效的范围也不同。
Hertz 框架已经预置了常用的 recover 中间件,使用 server.Default() 默认可以注册该中间件。

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")
    })...)

Hertz Client

Hertz提供了HTTP Client用于帮助用户发送HTTP请求,示例:github.com/cloudwego/h…

func Get() {
	c, err := client.NewClient()
	if err != nil {
		return
	}
	status, body, _ := c.Get(context.Background(), nil, "http://www.example.com")
	fmt.Printf("status=%v body=%v\n", status, string(body))
}


func Post() {
	c, err := client.NewClient()
	if err != nil {
		return
	}

	var postArgs protocol.Args
	postArgs.Set("arg", "a") // Set post args
	status, body, _ := c.Post(context.Background(), nil, "http://www.example.com", &postArgs)
	fmt.Printf("status=%v body=%v\n", status, string(body))
}

Hertz代码生成工具

hz 是 Hertz 框架提供的一个用于生成代码的命令行工具。目前,hz 可以基于 thrift 和 protobuf 的 IDL 生成 Hertz 项目的脚手架(项目基础代码)。

demo

同样以thrift为例,在当前目录下创建 thrift idl 文件:具体步骤

// idl/hello.thrift
namespace go hello.example

struct HelloReq {
    1: string Name (api.query="name"); // 添加 api 注解为方便进行参数绑定(参数绑定下沉到IDL中)
}

struct HelloResp {
    1: string RespBody;
}


service HelloService {
    HelloResp HelloMethod(1: HelloReq request) (api.get="/hello");  //get路由
}

生成代码结构

hz 生成的代码结构都类似,以基于 thrift IDL生成的代码结构为例:文件,用户在该文件里实现 IDL service 定义的方法,update 时会查找 当前文件

.
├── biz                                // business 层,存放业务逻辑相关流程
│   ├── handler                        // 存放 handler 文件
│   │   ├── hello                      // hello/example 对应 thrift idl 中定义的 namespace;而对于 protobuf idl,则是对应 go_package 的最后一级
│   │   │   └── example
│   │   │       ├── hello_service.go   // handler 文件,用户在该文件里实现 IDL service 定义的方法,update 时会查找 当前文件已有的 handler 在尾部追加新的 handler
│   │   │       └── new_service.go     // 同上,idl 中定义的每一个 service 对应一个文件
│   │   └── ping.go                    // 默认携带的 ping handler,用于生成代码快速调试,无其他特殊含义
│   ├── model                          // IDL 内容相关的生成代码
│   │   └── hello                      // hello/example 对应 thrift idl 中定义的 namespace;而对于 protobuf idl,则是对应 go_package
│   │     └── example
│   │         └── hello.go             // thriftgo 的产物,包含 hello.thrift 定义的内容的 go 代码,update 时会重新生成
│   └── router                         // idl 中定义的路由相关生成代码
│       ├── hello                      // hello/example 对应 thrift idl 中定义的namespace;而对于 protobuf idl,则是对应 go_package 的最后一级
│       │   └── example
│       │       ├── hello.go           // hz 为 hello.thrift 中定义的路由生成的路由注册代码;每次 update 相关 idl 会重新生成该文件
│       │       └── middleware.go      // 默认中间件函数,hz 为每一个生成的路由组都默认加了一个中间件;update 时会查找当前文件已有的 middleware 在尾部追加新的 middleware
│       └── register.go                // 调用注册每一个 idl 文件中的路由定义;当有新的 idl 加入,在更新的时候会自动插入其路由注册的调用;勿动
├── go.mod                             // go.mod 文件,如不在命令行指定,则默认使用相对于GOPATH的相对路径作为 module 名
├── idl                                // 用户定义的idl,位置可任意
│   └── hello.thrift
├── main.go                            // 程序入口
├── router.go                          // 用户自定义除 idl 外的路由方法
└── router_gen.go                      // hz 生成的路由注册代码,用于调用用户自定义的路由以及 hz 生成的路由

Hertz性能

Hertz性能远高于其他框架,体现在以下三点:

1、网络库Netpoll
Hertz 默认集成了 Netpoll 和 Golang 原生网络库 两个网络库,用户可以根据自己的场景选择合适的网络库以达到最佳性能。默认Netpoll。

2、Json编解码Sonic
默认使用高性能Sonic对Json进行编解码

3、使用sync.Pool复用对象,协议层数据解析优化

生态

image.png

参考资料

Hertz参考文档