Hertz入门 | 青训营笔记

445 阅读5分钟

这是我参与「 第五届青训营 」伴学笔记创作活动的第 6 天 文章主要介绍Hertz的使用

前言

Hertz是一个 Golang 微服务 HTTP 框架,在设计之初参考了其他开源框架 fasthttp、gin、echo 的优势, 并结合字节跳动内部的需求,使其具有高易用性、高性能、高扩展性等特点。

除了各个业务线的同学使用外,也服务于内部很多基础组件,如:函数计算平台 FaaS、压测平台、各类网关、Service Mesh 控制面等,均收到不错的使用反馈。在如此大规模的场景下,Hertz 拥有极强的稳定性和性能,在内部实践中某些典型服务,如框架占比较高的服务、网关等服务,迁移 Hertz 后相比 Gin 框架,资源使用显著减少,CPU 使用率随流量大小降低 30%—60%,时延也有明显降低。

快速上手

Hertz需要的Go版本至少为1.15,如果低于1.15的请更新Go版本

安装

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

可使用 hz -v 查看是否安装完成

hz -v
hz version v0.5.1

使用 hz new 将默认在 GOOPATH/src 目录生成项目结构,可以通过指定 -mod 用来在当前路径下创建项目结构文件,通过指定module形式创建注意应启用GOMODULE

hz new -mod MODULENAME

项目初始化后,hertz会自动创建对应项目文件,文件结构如下

  ├── biz 
  │   ├── handler 
  │   │   └── ping.go 
  │   └── router  
  │       └── register.go 
  ├── go.mod 
  ├── main.go 
  ├── router.go 
  └── router_gen.go

运行

hertz默认监听端口为8888,可以通过 server.WithHostPorts() 来指定运行端口

func main() {
   h := server.Default(server.WithHostPorts("127.0.0.1:9999"))

   register(h)
   h.Spin()
}

运行 main.go 后控制台输出

image.png

访问 localhost:9999/ping 浏览器的响应

{"message":"pong"}

请求方式

基础路由

路由类型

Hertz 支持丰富的路由类型用于实现复杂的功能,包括静态路由、参数路由、通配路由。
路由的优先级:静态路由 > 命名路由 > 通配路由

  • 静态路由
r.GET("/getName")
r.GET("/getAge")
  • 参数路由

Hertz 支持使用 :name 这样的命名参数设置路由,并且命名参数只匹配单个路径段。

r.GET("/user/:name/:age")
路径是否匹配
/user/xiaoming/12匹配
/user/xiaoyong/1234匹配
/user/wxlll/12/other不匹配
/user/不匹配
  • 通配路由(较少使用)

Hertz 支持使用 *path 这样的通配参数设置路由,并且通配参数会匹配所有内容。如果我们设置/src/*path路由,匹配情况如下

r.GET("/src/*")
路径是否匹配
/src/匹配
/src/somefile.go匹配
/src/subdir/somefile.go匹配

Restful

Hertz 提供了 GETPOSTPUTDELETEANY 等方法用于注册路由。详情见路由

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

路由组

v1 := r.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声明使用

v1 := r.Group("/v1", middleware.Jwt())
v1.Use(middleware.Jwt())

中间件的创建方法

func Jwt() app.HandlerFunc {
    return func(ctx context.Context, c *app.RequestContext) {
        // pre-handle
        // ...
        c.Next(ctx)
    }
}

使用下列函数可终止中间件的运行

  • c.Abort
  • c.AbortWithMsg
  • c.AbortWlthStats

Hertz中间件概览

go get github.com/hertz-contrib/cors

import "github.com/cloudwego/hertz/pkg/app/middlewares/server/recovery"

go get github.com/hertz-contrib/jwt

请求参数

Query参数

func Ping(ctx context.Context, c *app.RequestContext) {
   queryName := c.Query("name")

   c.JSON(consts.StatusOK, utils.H{
      "name": queryName,
   })
}

访问

http://localhost:9999/ping?name=www

输出

{"name":"www"}

Param参数

func Ping(ctx context.Context, c *app.RequestContext) {
   name := c.Param("name")
   age := c.Param("age")

   c.JSON(consts.StatusOK, utils.H{
      "name": name,
      "age":  age,
   })

}

访问

http://localhost:9999/ping/www/123

输出

{"age":"123","name":"www"}

请求体

func Ping(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(200, utils.H{
      "person": p,
   })
}

访问
image.png
输出
image.png

文件上传下载

func PersonUpload(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)
   }
   // 将读取到的文件写入到响应
   _, err = c.Write(fileRaw)
   if err != nil {
      panic(err)
   }
}

数据绑定

Hertz提供了Bind、Validate、 BindAndValidate 函数用于进行参数绑定和校验

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

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

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

代码生成

安装 thriftgo/protoc

要使用 thrift 或 protobuf 的 IDL 生成代码,需要安装相应的编译器:thriftgo 或 protoc 。

hz 生成的代码里,一部分是底层的编译器生成的(通常是关于 IDL 里定义的结构体),另一部分是 IDL 中用户定义的路由、method 等信息。用户可直接运行该代码。

从执行流上来说,当 hz 使用 thrift IDL 生成代码时,hz 会调用 thriftgo 来生成 go 结构体代码,并将自身作为 thriftgo 的一个插件(名为 thrift-gen-hertz)来执行来生成其他代码。当用于 protobuf IDL 时亦是如此。

安装thriftgo:

go install github.com/cloudwego/thriftgo@latest

安装proto请使用Github自行下载protoc

github.com/protocolbuf…

IDL

Hertz提供了代码生成工具Hz,通过定义IDL(inteface description language )文件即可生成对应的基础服务代码。

Thrift

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

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

struct HelloResp {
    1: string RespBody;
}

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

使用 hz 创建项目目录

hz new -idl idl/hello.thrift -mod hello
hz update -idl idl/hello.thrift

Proto

// idl/hello/hello.proto
syntax = "proto3";

package hello;

option go_package = "hertz/hello";

message HelloReq {
  string Name = 1;
}

message HelloResp {
  string RespBody = 1;
}

service HelloService {
  rpc Method1(HelloReq) returns(HelloResp);
}

使用 hz 创建项目目录

hz new -I idl -idl idl/hello/hello.proto -mod helloIdlProto
hz update -I idl -idl idl/hello/hello.proto -mod helloIdlProto
如果主IDL的依赖和主IDL不在同一路径下,需要加入 -I 选项,其含义为IDL搜索路径,等同于 protoc 的 -I 命令

目录结构

  ├── biz 
  │   ├── handler 
  │   │   ├── hello
  |   |   |   └── example
  |   |   |       └── hello_service.go
  |   |   └── ping.go
  │   ├── model
  │   |  └── hello
  |   |      └── example
  |   |          └── hello.go
  |   └── router
  |       ├── hello
  |       |   └── example
  |       |       ├── hello.go
  |       |       └── middleware.go
  |       └── register.go
  ├── go.mod 
  ├── go.sum 
  ├── main.go 
  ├── router.go 
  └── router_gen.go

一个问题

作者使用的是win系统,在创建目录结构时遇到了权限不足的问题 A required privilege is not held by the client.

image.png

打开系统的开发者模式(系统设置->搜索:开发者选项)问题就可以解决。解决问题原文

image.png

参考文章

字节跳动青训营课程及其资料
字节开源WEB框架Hertz太香啦!
字节跳动开源 Go HTTP 框架 Hertz 设计实践