Go三件套详解 | 青训营

168 阅读17分钟

Gorm

GormGo语言目前比较热门的数据库ORM操作库,对开发者也比较友好,使用非常简单,使用上主要就是把struct类型和数据库表记录进行映射,操作数据库的时候不需要直接手写SQL代码。

迄今为止,Gorm是一个已经迭代了10年+的功能强大的ORM框架,在字节内部被广泛使用并且拥有非常丰富的开源扩展。

Gorm目前支持数据库有MySQLSQLServerPostgreSQLSQLite

VScode报错:gopls was not able to find modules in your workspace

原因: go语言从1.18开始,能够支持在worksapce中使用多个module,但是必须生成一个go.work文件。回到worksapce使用命令go work init ./ethdemo ./gotask,其中ethdemo和gotask是我的两个模块。

1. CRUD

GORM 并不包含在 Go 标准库中,我们需要先安装 GORM 及需要连接对应数据库的驱动。

go get -u gorm.io/driver/mysql
go get -u gorm.io/gorm

Gorm 官方支持的数据库类型有:MySQL, PostgreSQL, SQlite, SQL Server。

Gorm在使用时有以下约定:

  • Gorm使用名为ID的字段作为主键
  • 如果没有TableName函数,使用结构体的蛇形复数作为表名。蛇形复数:ProductChenJiaXin -> product_chen_jia_xins
  • 字段名的蛇形作为列表
  • 使用CreatedAtUpdatedAt字段作为创建更新时间

1.1 创建模型

创建一个Product类型的结构体,结构体里的字段和MySQLproduct表字段一一对应。

type Product struct {
    gorm.Model
    Code  string
    Price uint
}

这个结构体和数据库字段对应:

CREATE TABLE IF NOT EXISTS `products` (
 `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
 `created_at` datetime(3) DEFAULT NULL,
 `updated_at` datetime(3) DEFAULT NULL,
 `deleted_at` datetime(3) DEFAULT NULL,
 `code` longtext DEFAULT NULL,
 `price` bigint(20) unsigned DEFAULT NULL,
 PRIMARY KEY (`id`),
 KEY `idx_products_deleted_at` (`deleted_at`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

以下结构体和上述结构体效果完全相同:

type Product struct {
    Code      string
    Price     uint
    ID        uint `gorm:"primarykey"`
    CreatedAt time.Time
    UpdatedAt time.Time
    DeletedAt sql.NullTime `gorm:"index"`
}

可以通过如下方法为字段指定默认值:

type User struct {
    ID      int64
    Name    string `gorm:"default:galeone"`
    Age     int64  `gorm:"default:18"`
}

或是为字段手动指定列名和主键:

type Product struct {
    ID      uint    `gorm:"primarykey"`
    Code    string  `gorm:"column: code"`
    Price   uint    `gorm:"column: user_id"`
}

创建表名对应关系,刚才创建的结构体ProductMySQL里的表名并不一致,所以要创建一个对应关系,创建方式为为Product结构体增加一个TableName方法

func (p Product) TableName() string {
    return "product"
}

1.2 连接数据库Open

打开数据库连接,通过mysql.Open函数打开连接,该函数需要传递一个dsndsn是连接数据库的一个连接串,相当于Java中的JDBC连接串。

dsn的格式为:{username}:{password}@tcp({host}:{port})/{dbname}?charset=utf8&parseTime=True&loc=Local

具体创建数据库连接的代码为:

dsn := fmt.Sprintf("%s:%s@tcp(%s:%d)/%s?charset=utf8&parseTime=True&loc=Local", username, password, host, port, dbname)
db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{
    Logger: logger.Default.LogMode(logger.Info),  // 设置日志级别,看见sql
})
if err != nil {
    panic("failed to connect database, error=" + err.Error())
}

1.3 插入数据Create

通过Create函数创建数据,先初始化一个结构体,然后调用Create函数。

p := Product{Code: "D42", Price: 100}
if err := db.Create(&p).Error; err != nil {
    fmt.Println("插入失败", err)
}

Create函数还支持创建多条数据,如果是多条的话,需要传一个数组参数。

products := []*Product{{Code: "D42", Price: 100},{Code: "D43", Price: 200}}
if err := db.Create(&products).Error; err != nil {
    fmt.Println("插入失败", err)
}

如果插入数据的时候,出现了主键冲突怎么办?可以使用OnConflict处理冲突。

p := Product{Code: "D42", ID: 1}
if err := db.Clauses(clause.OnConflict{DoNothing: true}).Create(&p).Error; err != nil {
    fmt.Println("插入失败", err)
}

DoNothingtrue代表如果冲突的话,什么也不做。

1.4 查询数据First/Find

Gorm通过First函数来进行数据的查询,如果First函数的第二个参数是个整形的话,会默认按主键进行查询,如果是字符串,则会直接拼接在where后面。

var product Product
db.First(&product, 1)

这段代码生成的SQL如下:

SELECT * FROM `products` WHERE `products`.`id` = 1 ORDER BY `products`.`id` LIMIT 1

如果要按照指定的条件查询的话,则需要传条件参数:

db.First(&product, "Code = ?", "D42")

或者使用Where函数:

db.Where("Code = ?", "D42").First(&product)

这两种方法的功能是一样的,生成的SQL如下:

SELECT * FROM `products` WHERE Code = 'D42' AND `products`.`deleted_at` IS NULL ORDER BY `products`.`id` LIMIT 1

First函数只能查询一条记录,如果要查多条的话,需要使用Find函数。

products := make([]Product, 0)
db.Find(&products, "price = ?", 100)

使用IN查询:

db.Where("price IN ?", []uint{100, 200}).Find(&products)

使用like查询:

db.Where("code like ?", "%D%").Find(&products)

使用结构体查询:

db.Where(&Product{Code: "D43", Price: 0}).Find(&products)

使用结构体作为查询条件时,Gorm只会查询非零值字段,也就是0''false或其他零值字段将被忽略,可以使用MapSelect来替换

生成的SQL为:

SELECT * FROM `products` WHERE `products`.`code` = 'D43' AND `products`.`deleted_at` IS NULL

注意

  • 使用First查询时,如果查询不到数据会返回ErrRecordNotFound
  • 使用Find查询时,查询不到数据不会返回错误

1.5 更新数据Update

Gorm更新数据是通过Update函数操作的,Update函数需要传入要更新的字段和对应的值。

需要通过Model函数来传入要更新的模型,主要是用来确定表名,也可以使用Table函数来确定表名。

db.Model(&product).Where("1=1").Update("Price", 200)
// 这个版本的Update必须写上Where

如果要更新多个字段的话,可以使用Updates函数,该函数需要传入一个结构体或map

db.Model(&product).Where("1=1").Updates(Product{Code: "D43", Price: 1})

如果使用结构体作为参数的话,可以省略Model这一部分,因为Gorm也会从参数的结构体中查询相关表名。

注意在使用结构体时,不会更新零值,如果要更新的话,需要使用map

db.Model(&Product{}).Updates(map[string]interface{}{"Price": 200, "Code": "F42"})

更新选定字段,只会更新Select函数里的字段,其他字段会被忽略:

db.Model(&Product{}).Select("price").Where("1=1").Updates(map[string]interface{}{"Price": 20, "Code": "F44"})

表达式更新,如果要实现update product set price = price * 2 * 100的话,实现方式如下:

db.Model(&Product{}).Where("1=1").Update("price", gorm.Expr("price * ? * ?", 2, 100))

1.6 删除数据Delete

删除数据又分为物理删除和逻辑删除。

删除使用的是Delete函数,Delete函数需要传入一个结构体以及参数,如果参数是整形的话,会按主键进行删除。

db.Delete(&Product{}, 1)
db.Delete(&Product{}, "Code = ?", "F42")
db.Delete(&Product{}, []int{1,2,3})

如果想按条件删除的话,需要使用Where函数:

db.Where("Code = ?", "F42").Delete(&Product{})

软删除需要在结构体中定义一个DeletedAt字段,此时再调用Delete删除函数,则会生成update语句,并将deleted字段赋值为当前删除时间。

UPDATE `products` SET `deleted_at`='2023-08-08 20:32:29.423' WHERE `products`.`id` = 1 AND `products`.`deleted_at` IS NULL

查询被软删除的操作,要使用Unscoped函数:

db.Unscoped().First(&product, 1)

有了DeletedAt字段后,删除操作已经变成了更新操作,那么想要物理删除怎么办?也是使用Unscoped函数:

db.Unscoped().Delete(&Product{}, 1)

2. 事务

Gorm提供了BeginCommitRollback方法用于使用事务。

tx := db.Begin()  // 开启事务
if err := tx.Create(&Product{Code: "D32", Price: 100}).Error; err != nil {
    tx.Rollback()  // 出现错误回滚
    return
}
if err := tx.Create(&Product{Code: "D33", Price: 100}).Error; err != nil {
    tx.Rollback()  // 出现错误回滚
    return
}
tx.Commit()  // 提交事务

在开启事务后,调用增删改操作是应该使用开启事务返回的tx而不是db

Gorm还提供了Transaction函数用于自定提交事务,避免用户漏写CommitRollback

if err = db.Transaction(func(tx *gorm.DB) error {
    if err := db.Create(&Product{Code: "D55", Price: 100}).Error; err != nil {
        return err
    }
    if err := db.Create(&Product{Code: "D56", Price: 100}).Error; err != nil {
        return err
    }
    return nil
}); err != nil {
    return
}

这种写法,当出现错误时会自动进行Rollback,当正常执行时会自动Commit

3. Hook

当我们想在执行增删改查操作前后做一些额外的操作时,可以使用Gorm提供的Hook能力。

Hook是在创建、查询、更新、删除等操作之前、之后自动调用的函数,如果任何Hook返回错误,Gorm将停止后续的操作并回滚事务。

Hook会开启默认事务,所以会带来了一些性能损失。

这很像 Spring Boot 遵循的 AOP(Aspect Oriented Programming,面向切面编程) ,方法被以一种约定的方式织入数据库操作逻辑中。

钩子方法的函数签名应该是 func(*gorm.DB) error

func (p *Product) BeforeCreate(tx *gorm.DB) (err error) {
	if p.Code == "CC" {
		return errors.New("can't save invalid data")
	}
	return nil
}

4. 提高性能

对于写操作(创建、更新、删除),为了确保数据完整性,Gorm会封装在事务内运行,这样会降低性能,可以使用SkipDefaultTransaction关闭默认事务。

使用PrepareStmt缓存预编译语句可以提高后续调用的速度。

db, err := gorm.Open(sqlite.Open("gorm.db"), &gorm.Config{
  SkipDefaultTransaction: true,
  PrepareStmt: true,
})

5. Gorm生态

Gorm拥有非常丰富的扩展生态,下面列举一些常用的扩展。

Kitex

Kitex是字节内部的Go语言微服务RPC框架,拥有高性能、强可扩展的主要特定,支持多协议并且拥有吩咐的开源扩展。

go install github.com/cloudwego/kitex/tool/cmd/kitex@latest
go install github.com/cloudwego/thriftgo@latest

1. IDL

接口描述语言(Interface definition language,IDL)是一种语言的通用术语,它允许用一种语言编写的程序或对象与用未知语言编写的另一个程序进行通信。我们可以使用 IDL 来支持 RPC 的信息传输定义。Kitex 默认支持 thriftproto3 两种 IDL,而在底层传输上,Kitex 使用扩展的 thrift 作为底层的传输协议。

使用thrift定义IDL需要创建一个文件以.thrift结尾,例如hello.thrift定义了一个Echo服务,接收一个具有单个字符串的消息,返回同样的消息:

namespace go api

struct Request {
    1: string message
}
struct Response {
    1: string message
}
service Hello {
    Response echo(1: Request req)
}
  • amespace代表命名空间,上面示例的命名空间为apigo代表go语言,如果用python语言来使用的话,就写python

  • service就代表一个服务,例如示例中的Hello服务,这个Hello服务提供了一个echo方法。

  • struct结构体用来定义参数或响应类型。

2. 服务端

定义好了IDL后,就可以使用kitex生成代码了,执行以下命令来生成项目代码:

kitex -module learning -service hello hello.thrift
go mod tidy

-module 模块名或者说是项目名,-service是服务名。

执行完成之后,生成的代码目录如下:

├── build.sh
├── go.mod
├── go.sum
├── handler.go
├── hello.thrift
├── kitex_gen
│   └── api
│       ├── hello
│       │   ├── client.go
│       │   ├── hello.go
│       │   ├── invoker.go
│       │   └── server.go
│       ├── hello.go
│       ├── k-consts.go
│       └── k-hello.go
├── kitex_info.yaml
├── main.go
└── script
    └── bootstrap.sh

生成的文件说明如下:

  • build.sh:构建脚本,可以把代码变成可执行文件
  • kitex_gen:IDL内容相关的生成代码,主要是基础的Server/Client代码
  • main.go:程序入口
  • handler.go:用户在该文件里实现IDLservice定义的方法

需要在handler.go实现Hello服务的echo方法,kitex默认监听8888端口。

Hello服务的代码实现,只需要在文件中的echo方法加入业务代码就可以了,复杂的代码逻辑的话可以按照MVC模式进行代码分层。

// HelloImpl implements the last service interface defined in the IDL.
type HelloImpl struct{}

// Echo implements the HelloImpl interface.
func (s *HelloImpl) Echo(ctx context.Context, req *api.Request) (resp *api.Response, err error) {
	// TODO: Your code here...
	return
}

修改 handler.go 内的 Echo 函数为下述代码以实现我们的 Echo 服务逻辑:

func (s *HelloImpl) Echo(ctx context.Context, req *api.Request) (resp *api.Response, err error) {
	// TODO: Your code here...
	return &api.Response{Message: req.Message}, nil
}

运行 sh build.sh 以进行编译,编译结果会被生成至 output 目录。

这里在windows报了个错,升级到kitexv0.6.1后编译失败,解决方案是升级netpoll:

go get -u github.com/cloudwego/netpoll

运行 sh output/bootstrap.sh 以启动服务,服务会默认在 8888 端口运行,要想修改运行端口,可打开 main.goNewServer 函数指定配置参数:

addr, _ := net.ResolveTCPAddr("tcp", ":9999")
svr := api.NewServer(new(HelloImpl), server.WithServiceAddr(addr))

3. 客户端

然后用 kitex 命令生成客户端所需的相关代码(如果该目录放在前述 server 目录下,则可省略此步,因为 server 代码中包含了 client 所需代码):

kitex -module learning hello.thrift

客户端代码不需要指定 -service 参数,生成的代码在 kitex_gen 目录下。

创建客户端:

package main

import (
	"context"
	"github.com/cloudwego/kitex/client"
	"github.com/cloudwego/kitex/client/callopt"
	"learning/kitex_gen/api"
	"learning/kitex_gen/api/hello"
	"log"
	"time"
)

func main() {
    // 创建客户端,其第一个参数为调用的服务名,第二个参数为 options,用于传入参数, 此处的 `client.WithHostPorts` 用于指定服务端的地址。
	c, err := hello.NewClient("hello", client.WithHostPorts("0.0.0.0:8888"))
	if err != nil {
		log.Fatal(err)
	}
    
    // 发起调用
	req := &api.Request{Message: "chenjiaxin request"}
    // 第一个参数为 context.Context,通过通常用其传递信息或者控制本次调用的一些行为
    // 第二个参数为本次调用的请求
    // 第三个参数为本次调用的options,Kitex 提供了一种 callopt 机制。有别于创建 client 时传入的参数,这里传入的参数仅对此次生效。
    // 此处的 callopt.WithRPCTimeout 用于指定此次调用的超时(通常不需要指定,此处仅作演示之用)
	resp, err := c.Echo(context.Background(), req, callopt.WithRPCTimeout(3*time.Second))
	if err != nil {
		log.Fatal(err)
	}
	log.Println(resp)
}

在编写完一个简单的客户端后,我们终于可以发起调用了。

你可以通过下述命令来完成这一步骤:

go run main.go

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

2023/08/09 21:11:47 Response({Message:chenjiaxin request_chenjx529})

恭喜你!至此你成功编写了一个 Kitex 的服务端和客户端,并完成了一次调用!

4. 增加新的方法

在IDL中增加add方法:

namespace go api

struct Request {
    1: string message
}

struct Response {
    1: string message
}

struct AddRequest {
    1: i64 first
    2: i64 second
}

struct AddResponse {
    1: i64 sum
}

service Hello {
    Response echo(1: Request req)
    AddResponse add(1: AddRequest req)
}

重新生成代码:

kitex -module learning -service hello hello.thrift

执行完上述命令后,kitex 工具将更新下述文件

  • 更新 ./handler.go,在里面增加一个 Add 方法的基本实现

  • 更新 ./kitex_gen,里面有框架运行所必须的代码文件

// HelloImpl implements the last service interface defined in the IDL.
type HelloImpl struct{}

// Echo implements the HelloImpl interface.
func (s *HelloImpl) Echo(ctx context.Context, req *api.Request) (resp *api.Response, err error) {
	// TODO: Your code here...
	return &api.Response{Message: req.Message + "_chenjx529"}, nil
}

// Add implements the HelloImpl interface.
func (s *HelloImpl) Add(ctx context.Context, req *api.AddRequest) (resp *api.AddResponse, err error) {
	// TODO: Your code here...
	return
}

增加add方法的逻辑:

// Add implements the HelloImpl interface.
func (s *HelloImpl) Add(ctx context.Context, req *api.AddRequest) (resp *api.AddResponse, err error) {
	// TODO: Your code here...
	return &api.AddResponse{Sum: req.First + req.Second}, nil
}

修改client进行调用:

func main() {
    // 创建client
	client, _ := hello.NewClient("hello", client.WithHostPorts("0.0.0.0:8888"))
    
	for {
        // 调用Echo方法
		EchoReq := &api.Request{Message: "my request"}
		resp, err := client.Echo(context.Background(), EchoReq)
		if err != nil {
			log.Fatal(err)
		}
		log.Println(resp)
		time.Sleep(time.Second)
		
        // 调用Add方法
		addReq := &api.AddRequest{First: 512, Second: 512}
		addResp, err := client.Add(context.Background(), addReq)
		if err != nil {
			log.Fatal(err)
		}
		log.Println(addResp)
		time.Sleep(time.Second)
	}
}

运行 server:go run . 这里有个疑问,go run main.go会报错,但是go run .就可以。

运行 client:另起一个终端后,go run ./client

5. 注册与发现

Kitex 已经通过社区开发者的支持,完成了 ETCD、ZooKeeper、Eureka、Consul、Nacos、Polaris 多种服务发现模式,当然也支持 DNS 解析以及 Static IP 直连访问模式,建立起了强大且完备的社区生态,供用户按需灵活选用。

这里使用Nacos作为演示,先启动一个Nacos服务:

image-20230810201859962.png

对nacos做统一配置:

package cust_nacos

import (
	"github.com/nacos-group/nacos-sdk-go/clients"
	"github.com/nacos-group/nacos-sdk-go/clients/naming_client"
	"github.com/nacos-group/nacos-sdk-go/common/constant"
	"github.com/nacos-group/nacos-sdk-go/vo"
)

func KitexNacosRegistry() (iClient naming_client.INamingClient) {
	sc := []constant.ServerConfig{
		*constant.NewServerConfig("127.0.0.1", 8848),
	}
    // 客户端配置 比如log
	cc := constant.ClientConfig{
		NamespaceId:         "",
		TimeoutMs:           5000,
		NotLoadCacheAtStart: true,
		LogLevel:            "info",
	}

	icl, err := clients.NewNamingClient(
		vo.NacosClientParam{
			ClientConfig:  &cc,
			ServerConfigs: sc,
		},
	)

	if err != nil {
		panic(err)
	}

	return icl
}

服务端:

func main() {
    cli := cust_nacos.KitexNacosRegistry()  // 连接nacos
    svr := api.NewServer(
        new(HelloImpl),
        server.WithServerBasicInfo(&rpcinfo.EndpointBasicInfo{ServiceName: "Hello"}),  // 服务名称
        server.WithRegistry(registry.NewNacosRegistry(cli)),
        server.WithServiceAddr(&net.TCPAddr{IP: net.IPv4(127, 0, 0, 1), Port: 8081}),  // 服务地址
    )

    if err := svr.Run(); err != nil {
        log.Println("server stopped with error:", err)
    } else {
        log.Println("server stopped")
    }
}

启动后可以看见服务:

image-20230810202312135.png

客户端:

func main() {
    cli := cust_nacos.KitexNacosRegistry()

    newClient, err := hello.NewClient("Hello", client.WithResolver(resolver.NewNacosResolver(cli)))  // 服务区分大小写
    if err != nil {
        log.Fatal(err)
    }

    for {
        EchoReq := &api.Request{Message: "my request"}
        resp, err := newClient.Echo(context.Background(), EchoReq)
        if err != nil {
            log.Fatal(err)
        }
        log.Println(resp)
        time.Sleep(time.Second)

        addReq := &api.AddRequest{First: 512, Second: 512}
        addResp, err := newClient.Add(context.Background(), addReq)
        if err != nil {
            log.Fatal(err)
        }
        log.Println(addResp)
        time.Sleep(time.Second)
    }
}

可以看见订阅者:

image-20230810202411628.png

6. Kitex生态

Kitex有非常丰富的扩展生态,如:

  • XDS扩展
  • opentelemetry扩展
  • ETCD服务注册与发现扩展
  • Nacos服务注册与发现扩展
  • Zookeeper服务注册与发现扩展
  • Polaris扩展
  • 丰富的实例代码与业务Demo

详细扩展代码请查看GitHub

Hertz

Hertz是字节内部开源的http框架,参考了其他开源框架的优势,结合字节跳动内部的需求,具有高易用性、高性能、高扩展性等特点。

  • 使用高性能网络库Netpoll
  • Json编解码SonicSonic是一个高性能Json编解码库
  • 使用sync.Pool复用对象协议层数据解析优化

安装Hertz需要确保电脑已经正确安装了Go,并且Go的版本要>=1.15

执行以下命令进行安装Hertz

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

安装后进行验证,输入以下命令:

hz -v

可以使用hz命令直接生成示例代码,步骤如下:

mkdir hertz_demo
hz new -module example
go mod tidy

1. 路由

Hertz提供了多种路由规则,路由的优先级为:静态路由 > 参数路由 > 通配路由。

1.1 静态路由

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

Any 用于注册所有 HTTP Method 方法;

Hertz.StaticFile/Static/StaticFS 用于注册静态文件;

Handle 可用于注册自定义 HTTP Method 方法。

func main(){
    h := server.Default(server.WithHostPorts("127.0.0.1:8080"))
    
    h.StaticFS("/", &app.FS{Root: "D:/OneDrive/图片/美图/", 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")
	})
	h.PATCH("/patch", func(ctx context.Context, c *app.RequestContext) {
		c.String(consts.StatusOK, "patch")
	})
	h.HEAD("/head", func(ctx context.Context, c *app.RequestContext) {
		c.String(consts.StatusOK, "head")
	})
	h.OPTIONS("/options", func(ctx context.Context, c *app.RequestContext) {
		c.String(consts.StatusOK, "options")
	})
	h.Any("/ping_any", func(ctx context.Context, c *app.RequestContext) {
		c.String(consts.StatusOK, "any")
	})
	h.Handle("LOAD", "/load", func(ctx context.Context, c *app.RequestContext) {
		c.String(consts.StatusOK, "load")
	})
    h.Spin()
}

1.2 路由组

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

package main

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

func main(){
    h := server.Default(server.WithHostPorts("127.0.0.1:8080"))
	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")
	})
	v2 := h.Group("/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()
}

1.3 参数路由

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

如果我们设置/hertz/:version路由,匹配情况如下:

路径是否匹配
/hertz/v1匹配
/hertz/v2匹配
/hertz/v1/detail不匹配
/hertz/不匹配

通过使用 RequestContext.Param 方法,我们可以获取路由中携带的参数。

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

1.4 通配路由

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

如果我们设置/hertz/*path路由,匹配情况如下

路径是否匹配
/hertz/v1匹配
/hertz/v1/detail匹配
/hertz/匹配

通过使用 RequestContext.Param 方法,我们可以获取路由中携带的参数。

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

2. 参数绑定

Hertz提供了 BindValidateBindAndValidate 函数用于进行参数绑定校验。

绑定的作用是把http请求的参数封装到定义的结构体里面方便开发使用,Hertz使用的是tag的方式进行绑定的。

Tag说明
path绑定 url 上的路径参数,相当于 hertz 路由 :param*param 中拿到的参数
form绑定请求的 body 内容
query绑定请求的 query 参数
header绑定请求的 header 参数
json绑定 json 参数
vd参数校验

绑定使用的是BindAndValidate方法,使用方法如下:

func main() {
	h := server.Default()

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

		// BindAndValidate
		var req Test
		err := c.BindAndValidate(&req)
		if err != nil {
			log.Fatal(err)
		}

		// Bind
		req = Test{}
		err = c.Bind(&req)
		if err != nil {
			log.Fatal(err)
		}

		// Validate,需要使用 "vd" tag
		err = c.Validate(&req)
		if err != nil {
			log.Fatal(err)
		}

		c.String(consts.StatusOK, "OK")
	})
	register(h)
	h.Spin()
}

3. 中间件

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

中间件可以在请求更深入地传递到业务逻辑之前或之后执行。

func MyMiddleware() app.HandlerFunc {
    return func(ctx context.Context, c *app.RequestContext) {
        hlog.Info("pre-handle")
        c.Next(ctx) 
        hlog.Info("post-handle")

    }
}
func main() {
    h := server.Default(server.WithHostPorts(":8080"))
    h.Use(MyMiddleware())
    h.GET("/mid", func(ctx context.Context, c *app.RequestContext) {
        c.String(consts.StatusOK, "Hello hertz!")
    })
    register(h)
    h.Spin()
}

可使用 Abort(), AbortWithMsg(msg string, statusCode int), AbortWithStatus(code int) 终止后续调用。

4. Hertz Client

Hertz提供了 Http Client 用于帮助用户发送 Http 请求,可以在代码的逻辑中请求第三方服务。

例如,使用GET发送一个Http请求:

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

使用POST发送一个Http请求:

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

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

5. 代码生成

hzHertz 框架提供的一个用于生成代码的命令行工具,hz 可以基于 thriftprotobufIDL 生成 Hertz 项目的脚手架。

6. Hertz生态

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

  • Http2扩展
  • opentelemetry扩展
  • 国际化扩展
  • 反向代理扩展
  • JWT鉴权扩展
  • Websocket扩展
  • 丰富的代码示例