一文学会 Go 的三个主流开发框架| 青训营笔记

2023-03-06 18:44:14 浏览数 (1)

一文学会 Go 的三个主流开发框架| 青训营笔记

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

前言

本文介绍了三个 Go 主流开发框架 GORM,Kitex,Hertz 的基本使用方法,覆盖了 ORM,RPC,HTTP 三个领域。帮助读者快速入门 Go 工程开发。

GORM

GORM 是一款面向 Go 开发的,对开发人员友好的,“梦幻般的” ORM 库。(The fantastic ORM library for Golang aims to be developer friendly.

什么是 ORM

在学习 Gorm 前,你应该先了解什么是 ORM。ORM 全称Object–relational mapping,即对象关系映射,是一种用于在关系数据库和面向对象的编程语言堆之间转换数据的编程技术(Wikipedia,并非只有面向对象语言才支持 ORM,事实上,Go 就不是一个面向对象语言)。通过 ORM 技术,我们可以将关系数据库中某个数据表的结构关联到某个类/结构体上,并通过修改类/结构体实例的方式轻易的完成数据库增删改查(CRUD)的任务。通过 ORM 技术,我们得以以一种更加友好且高效的方式,在尽量不接触 SQL 语句的情况下操作数据库。

在 Java 中,常见的 ORM 框架有 Mybatis, MyBatis-Plus, Hibernate 等。

使用 GORM

GORM 增删改查

GORM 并不包含在 Go 标准库中,因此,我们需要先安装 GORM 及需要连接对应数据库的驱动。Gorm 官方支持的数据库类型有:MySQL, PostgreSQL, SQlite, SQL Server。以下命令通过 Go Module 拉取并添加 Gorm 及 MySQL 数据库驱动:

代码语言:javascript复制
go get -u gorm.io/gorm
go get -u gorm.io/driver/mysql

完美!如此一来,我们便可通过 GORM 连接 MySQL 数据库并进行数据库编程了。以下代码展示了一段用于快速入门的 GORM 示例,我们将在下文详细解释它们:

代码语言:javascript复制
package main

import (
  "gorm.io/gorm"
  "gorm.io/driver/mysql"
)

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

func main() {
    dsn := "user:pass@tcp(127.0.0.1:3306)/dbname?charset=utf8mb4&parseTime=True&loc=Local"
    db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{})
  if err != nil {
    panic("failed to connect database")
  }

  // 迁移 schema
  db.AutoMigrate(&Product{})

  // Create
  db.Create(&Product{Code: "D42", Price: 100})

  // Read
  var product Product
  db.First(&product, 1) // 根据整型主键查找
  db.First(&product, "code = ?", "D42") // 查找 code 字段值为 D42 的记录

  // Update - 将 product 的 price 更新为 200
  db.Model(&product).Update("Price", 200)
  // Update - 更新多个字段
  db.Model(&product).Updates(Product{Price: 200, Code: "F42"}) // 仅更新非零值字段
  db.Model(&product).Updates(map[string]interface{}{"Price": 200, "Code": "F42"})

  // Delete - 删除 product
  db.Delete(&product, 1)
}

在代码开始,我们首先导入了 GORM 和 MySQL 数据驱动:

代码语言:javascript复制
import (
  "gorm.io/gorm"
  "gorm.io/driver/mysql"
)

随后,我们声明了一个数据库模型。模型是标准的 struct,由 Go 的基本数据类型、实现了 Scanner 和 Valuer 接口的自定义类型及其指针或别名组成。数据库模型的结构将被对应到数据表中:

代码语言:javascript复制
type Product struct {
  gorm.Model
  Code  string
  Price uint
}

声明了相当于使用以下(包含 MySQL 方言的)SQL 语句创建的数据表结构:

代码语言:javascript复制
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=utf8mb3;

你可能会注意到声明的数据表结构中似乎多了什么东西,同时,一切似乎是不是又来的太快了?

按约定编程的 GORM

如果你是一位 Java 开发者,并且使用过 Spring Boot 的话,你一定会惊异的发现开发一个 Spring Boot 应用是如此的简单,不仅开箱即用,一切的一切都已经由 Spring Boot 帮你配置好了…… 其实,这就是按约定编程(Convention over configuration),这是一种软件设计范例,旨在在减少使用框架的开发人员需要做出的决策数量的同时不失去灵活性。GORM 也采用了这种设计范例,这意味着:

默认情况下,GORM 使用 ID 作为主键,使用结构体名的 蛇形复数 作为表名,字段名的 蛇形 作为列名,并使用 CreatedAtUpdatedAt 字段追踪创建、更新时间。

因此,当我们在数据模型中指定 gorm.Model 时,ID, CreatedAtUpdatedAt , DeletedAt 会被自动创建,并按其名字那样工作,例如 CreatedAt 字段会在我们创建一个记录时自动填充创建时间。当然,你也可以尝试手动添加这些字段到你的数据模型中,以下结构体和上述结构体效果完全相同:

代码语言:javascript复制
type Product struct {
    Code      string
    Price     uint
    ID        uint `gorm:"primarykey"`
    CreatedAt time.Time
    UpdatedAt time.Time
    DeletedAt sql.NullTime `gorm:"index"`
}

当然,简洁起见,我们还是建议您使用上面那种写法。

如果您遵循 GORM 的约定,您就可以少写的配置、代码。 如果约定不符合您的实际要求,GORM 允许你配置它们例如,你可以通过如下方法为字段指定默认值:

代码语言:javascript复制
type User struct {
    ID      int64
    Name    string `gorm:"default:galeone"`
    Age     int64  `gorm:"default:18"`
}

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

代码语言:javascript复制
type Product struct {
    ID      uint    `gorm:"primarykey"`
    Code    string  `gorm:"column: code"`
    Price   uint    `gorm:"column: user_id"`
}

说完按约定编程,回过头来接着看上文代码,在 main 函数中:

代码语言:javascript复制
    dsn := "user:pass@tcp(127.0.0.1:3306)/dbname?charset=utf8mb4&parseTime=True&loc=Local"
    db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{})
  if err != nil {
    panic("failed to connect database")
  }

调用 gorm.Open 开启了一个数据库连接,并为可能产生的异常进行处理。

我们通过 mysql.Open 函数并传入了一个看似奇怪的字符串为 GORM 指定了数据库信息,这个字符串被称为 DSN(data source name,数据源名称),其中包含有关 ODBC(Open Database Connectivity,开放式数据库连接,一种用于访问数据库管理系统的 API) 驱动程序需要连接到的特定数据库的信息。

此处的 DSN 为 GORM 提供了以下信息:通过 tcp 协议连接 127.0.0.1:3306 地址(MySQL 数据库连接地址)数据库的 dbname 数据库,并使用 user 作为用户名,pass 作为密码进行认证;指定此连接使用 utf8mb4 作为文字编码集(MySQL 使用 utf8,也作 utf8mb3 作为默认的文字编码集,此文件编码集并不是真正的 UTF-8 编码,对于 emoji 一类高字符平面的 Unicode 文字无法正确存储,而 utf8mb4 才是真正且完整支持 Unicode 编码的 UTF-8 编码),启用 parseTime 以将时间信息正确映射到 Go 的 time.Time 结构体,设置时区 loc 为本地时区。

&gorm.Config{} 为 GORM 启用默认的配置,当然,你也可以指定自己的配置,比如通过传入:

代码语言:javascript复制
&gorm.Config{
    PrepareStmt: true
}

启用预编译语句缓存以提高性能。

代码语言:javascript复制
  // 迁移 schema
  db.AutoMigrate(&Product{})

为指定数据库自动迁移数据模型结构。这会为指定数据模型创建 GORM 可用的数据表结构。这一步是可选的,即使不迁移 schema,数据表也会在创建新记录的时候被创建。

代码语言:javascript复制
  // Create
  db.Create(&Product{Code: "D42", Price: 100})

对应增删改查中的“增”,以指定数据值在指定数据库的数据表中创建一条记录,额外的,你可以注意到你无须为 ID, CreatedAt 等字段指定数据值,而这些数据值将会在特定时刻被自动填入。

代码语言:javascript复制
  // Read
  var product Product
  db.First(&product, 1) // 根据整型主键查找
  db.First(&product, "code = ?", "D42") // 查找 code 字段值为 D42 的记录

对应增删改查中的“查”。First 方法返回符合指定条件的首个记录值;值得注意的是,使用 First 方法进行查询时,如果查找不到数据会返回 ErrRecodeNotFound 错误。可以使用 Find 查询多条记录,而 Find 方法在查询不到数据的时候并不会返回错误。

除了此种查询方式外,还可使用 Where 子句查询:

代码语言:javascript复制
// Get first matched record
db.Where("name = ?", "jinzhu").First(&user)
// SELECT * FROM users WHERE name = 'jinzhu' ORDER BY id LIMIT 1;

// Get all matched records
db.Where("name <> ?", "jinzhu").Find(&users)
// SELECT * FROM users WHERE name <> 'jinzhu';

// IN
db.Where("name IN ?", []string{"jinzhu", "jinzhu 2"}).Find(&users)
// SELECT * FROM users WHERE name IN ('jinzhu','jinzhu 2');

// LIKE
db.Where("name LIKE ?", "%jin%").Find(&users)
// SELECT * FROM users WHERE name LIKE '%jin%';

// AND
db.Where("name = ? AND age >= ?", "jinzhu", "22").Find(&users)
// SELECT * FROM users WHERE name = 'jinzhu' AND age >= 22;

// Time
db.Where("updated_at > ?", lastWeek).Find(&users)
// SELECT * FROM users WHERE updated_at > '2000-01-01 00:00:00';

// BETWEEN
db.Where("created_at BETWEEN ? AND ?", lastWeek, today).Find(&users)
// SELECT * FROM users WHERE created_at BETWEEN '2000-01-01 00:00:00' AND '2000-01-08 00:00:00';

可通过结构体和 Map 传入条件:

代码语言:javascript复制
// Struct
db.Where(&User{Name: "jinzhu", Age: 20}).First(&user)
// SELECT * FROM users WHERE name = "jinzhu" AND age = 20 ORDER BY id LIMIT 1;

// Map
db.Where(map[string]interface{}{"name": "jinzhu", "age": 20}).Find(&users)
// SELECT * FROM users WHERE name = "jinzhu" AND age = 20;

// Slice of primary keys
db.Where([]int64{20, 21, 22}).Find(&users)
// SELECT * FROM users WHERE id IN (20, 21, 22);

需要注意的是,当使用结构体作为查询条件时,只会查询结构体内的非零值字段,这意味着字段值为 0, '', false 或其他零值的字段不会被用于构建查询条件。

代码语言:javascript复制
  // Update - 将 product 的 price 更新为 200
  db.Model(&product).Update("Price", 200)
  // Update - 更新多个字段
  db.Model(&product).Updates(Product{Price: 200, Code: "F42"}) // 仅更新非零值字段
  db.Model(&product).Updates(map[string]interface{}{"Price": 200, "Code": "F42"})

对应增删改查中的“改”。同样,使用结构体更新时,只会更新非零值。

代码语言:javascript复制
  // Delete - 删除 product
  db.Delete(&product, 1)

对应增删改查中的“删”。同样,也有多种其他方式可选:

代码语言:javascript复制
// Email 的 ID 是 `10`
db.Delete(&email)
// DELETE from emails where id = 10;

// 带额外条件的删除
db.Where("name = ?", "jinzhu").Delete(&email)
// DELETE from emails where id = 10 AND name = "jinzhu";

db.Delete(&User{}, 10)
// DELETE FROM users WHERE id = 10;

db.Delete(&User{}, "10")
// DELETE FROM users WHERE id = 10;

db.Delete(&users, []int{1,2,3})
// DELETE FROM users WHERE id IN (1,2,3);

db.Where("email LIKE ?", "%jinzhu%").Delete(&Email{})
// DELETE from emails where email LIKE "%jinzhu%";

db.Delete(&Email{}, "email LIKE ?", "%jinzhu%")
// DELETE from emails where email LIKE "%jinzhu%";

需要注意的是,如果我们指定了 gorm.deletedat 字段(gorm.Model 包含该字段),将启用软删除模式:这意味着,改数据模型调用 Delete 方法时,并不会被真正从数据表中删除,而是会设置 DeletedAt 字段为当前时间,此后,你不能再通过普通的查询方法找到该记录:

代码语言:javascript复制
// user 的 ID 是 `111`
db.Delete(&user)
// UPDATE users SET deleted_at="2013-10-29 10:23" WHERE id = 111;

// 批量删除
db.Where("age = ?", 20).Delete(&User{})
// UPDATE users SET deleted_at="2013-10-29 10:23" WHERE age = 20;

// 在查询时会忽略被软删除的记录
db.Where("age = 20").Find(&user)
// SELECT * FROM users WHERE age = 20 AND deleted_at IS NULL;

使用 Unscoped 可绕过该机制,找到被软删除的记录:

代码语言:javascript复制
db.Unscoped().Where("age = 20").Find(&users)
// SELECT * FROM users WHERE age = 20;

同样,也可以用来永久删除匹配的记录:

代码语言:javascript复制
db.Unscoped().Delete(&order)
// DELETE FROM orders WHERE id=10;
GORM 事务

数据库事务(transaction) 是访问并可能操作各种数据项]的一个数据库操作序列,这些操作要么全部执行,要么全部不执行,是一个不可分割的工作单位。事务由事务开始与事务结束之间执行的全部数据库操作组成。举个例子,假设有一个电商网站数据库,涉及创建订单和配置库存两个操作,如果创建订单完成后配置库存失败了,那么理论上创建订单也应该被自动回滚以避免数据不一致,通过事务系统,我们可以将这两个操作划入一个事务,这样,当其中一个操作出现错误,其他操作便会被自动回滚。

值得一提的是,为了保证数据一致性,GORM 会在事务里执行写入操作(创建、更新、删除)。如果没有这方面的要求,您可以在初始化时禁用它,这将获得大约 30% 性能提升

代码语言:javascript复制
db, err := gorm.Open(sqlite.Open("gorm.db"), &gorm.Config{
  SkipDefaultTransaction: true,
})

使用事务的一般流程如下:

代码语言:javascript复制
db.Transaction(func(tx *gorm.DB) error {
  // 在事务中执行一些 db 操作(从这里开始,您应该使用 'tx' 而不是 'db')
  if err := tx.Create(&Animal{Name: "Giraffe"}).Error; err != nil {
    // 返回任何错误都会回滚事务
    return err
  }

  if err := tx.Create(&Animal{Name: "Lion"}).Error; err != nil {
    return err
  }

  // 返回 nil 提交事务
  return nil
})

当然,您也可以使用手动方式创建,提交和回滚事务:

代码语言:javascript复制
// 开始事务
tx := db.Begin()

// 在事务中执行一些 db 操作(从这里开始,您应该使用 'tx' 而不是 'db')
tx.Create(...)

// ...

// 遇到错误时回滚事务
tx.Rollback()

// 否则,提交事务
tx.Commit()
GORM Hook

Hook 是在创建、查询、更新、删除等操作之前、之后调用的函数。

如果您已经为模型定义了指定的方法,它会在创建、更新、查询、删除时自动被调用。如果任何回调返回错误,GORM 将停止后续的操作并回滚事务。

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

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

代码语言:javascript复制
type User struct {
    ID      int64
    Name    string `gorm:"default:galeone"`
    Age     string `gorm:"default:18"`
}

type Email struct {
    ID      int64
    Name    string
    Email   string
}

func (u *User) BeforeCreate(tx `gorm.DB) (err error) {
    if u.Age < 0 {
        return errors.New("can't save invalid data")
    }
    return nil;
}

func (u *User) AfterCreate(tx `gorm.DB) (err error) {
    return tx.Create(&Email{ID: u.ID, Email: i.Name   "@***.com"}).Error
}

查阅 Hook | GORM – The fantastic ORM library for Golang, aims to be developer friendly. 以获得更多有关 Hook 的信息。

Kitex

Kitex[kaɪt’eks] 是字节跳动内部的 Golang 微服务 RPC 框架,具有高性能强可扩展的特点,在字节内部已广泛使用。如今越来越多的微服务选择使用 Golang,如果对微服务性能有要求,又希望定制扩展融入自己的治理体系,Kitex 会是一个不错的选择。

什么是 RPC

RPC(Remote procedure call,远程过程调用)是指计算机程序导致过程(子例程)在不同的地址空间(通常在共享网络上的另一台计算机上)中执行,其编码就像普通(本地)过程调用一样,程序员没有显式编码远程交互的详细信息。也就是说,程序员编写的代码基本相同,无论子例程是执行程序的本地还是远程的。简单来说,通过使用 RPC,我们可以像调用方法一样快捷的与远程服务进行交互。

使用 Kitex(服务端)

Kitex 目前对 Windows 的支持并不完善,建议使用虚拟机或 WSL2 进行测试。

要开始 Kitex 开发,首先需要安装 Kitex 代码生成工具, go install 命令可被用于安装 Go 二进制工具(在此之前,请务必检查已正确设置 GOPATH 环境变量,并将 $GOPATH/bin 添加到 PATH 环境变量中):

代码语言:javascript复制
go install github.com/cloudwego/kitex/tool/cmd/kitex@latest

go install github.com/cloudwego/thriftgo@latest

接下来,定义 IDL,命名为 echo.thrift

代码语言:javascript复制
namespace go api

struct Request {
    1: string message
}

struct Resposne {
    1: string message
}

service Echo {
    Reponse echo(1: Request req)
}

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

Thrift IDL 语法可参考:Thrift interface description language。

proto3 语法可参考:Language Guide(proto3)。

以上文件定义了一个回声(Echo)服务,接收一个具有单个字符串的消息,返回同样的消息。

接下来,使用以下指令为我们的回声服务生成代码:

代码语言:javascript复制
kitex -module exmaple -service example echo.thrift

上述命令中,-module 表示生成的该项目的 go module 名,-service 表明我们要生成一个服务端项目,后面紧跟的 example 为该服务的名字。最后一个参数则为该服务的 IDL 文件。生成后的项目结构如下:

代码语言:javascript复制
.
|-- build.sh
|-- echo.thrift
|-- handler.go
|-- kitex_gen
|   `-- api
|       |-- echo
|       |   |-- client.go
|       |   |-- echo.go
|       |   |-- invoker.go
|       |   `-- server.go
|       |-- echo.go
|       `-- k-echo.go
|-- main.go
`-- script
    |-- bootstrap.sh
    `-- settings.py

其中 build.sh 为构建脚本,kitex_gen 为 IDL 内容相关的生成代码,main.go 为程序入口,handler.go 可由用户在此文件内实现 IDL service 定义的方法。

handler.go 文件内容如下:

代码语言:javascript复制
package main

import (
        "context"
        api "exmaple/kitex_gen/api"
)

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

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

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

代码语言:javascript复制
func (s *EchoImpl) Echo(ctx context.Context, req *api.Request) (resp *api.Response, err error) {
  return &api.Response{Message: req.Message}, nil
}

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

最后,运行 sh output/bootstrap.sh 以启动服务。服务会在默认的 8888 端口上开始运行。要想修改运行端口,可打开 main.go,为 NewServer 函数指定配置参数:

代码语言:javascript复制
 addr, _ := net.ResolveTCPAddr("tcp", "127.0.0.1:9999")
  svr := api.NewServer(new(EchoImpl), server.WithServiceAddr(addr))

然后重新执行编译步骤即可。

使用 Kitex(客户端)

上例中,我们使用 Kitex 创建了一个回声服务端,接下来,我们通过创建一个客户端来调用我们的回声服务。以下项目代码假设您已正确导入上文中生成的回声服务代码。新建项目并创建 main.go 文件,编写代码:

代码语言:javascript复制
import "example/kitex_gen/api/echo"
import "github.com/cloudwego/kitex/client"
...
c, err := echo.NewClient("example", client.WithHostPorts("0.0.0.0:8888"))
if err != nil {
  log.Fatal(err)
}

上述代码中,echo.NewClient 用于创建 client,其第一个参数为调用的 服务名,第二个参数为 options,用于传入参数, 此处的 client.WithHostPorts 用于指定服务端的地址,更多参数可参考基本特性。

代码语言:javascript复制
import "example/kitex_gen/api"
...
req := &api.Request{Message: "my request"}
resp, err := c.Echo(context.Background(), req, callopt.WithRPCTimeout(3*time.Second))
if err != nil {
  log.Fatal(err)
}
log.Println(resp)

上述代码中,我们首先创建了一个请求 req , 然后通过 c.Echo 发起了调用。

其第一个参数为 context.Context,通过通常用其传递信息或者控制本次调用的一些行为,你可以在后续章节中找到如何使用它。

其第二个参数为本次调用的请求。

其第三个参数为本次调用的 options ,Kitex 提供了一种 callopt 机制,顾名思义——调用参数 ,有别于创建 client 时传入的参数,这里传入的参数仅对此次生效。 此处的 callopt.WithRPCTimeout 用于指定此次调用的超时(通常不需要指定,此处仅作演示之用)同样的,你可以在基本特性一节中找到更多的参数。

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

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

代码语言:javascript复制
$ go run main.go

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

代码语言:javascript复制
2021/05/20 16:51:35 Response({Message:my request})

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

Kitex 服务注册与发现

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

代码语言:javascript复制
import (
    ...
    dns "github.com/kitex-contrib/resolver-dns"
    "github.com/cloudwego/kitex/client"
    ...
)

func main() {
    ...
    client, err := echo.NewClient("echo", client.WithResolver(dns.NewDNSResolver()))
    if err != nil {
        log.Fatal(err)
    }
    ...
}

Hertz

Hertz[həːts] 是一个 Golang 微服务 HTTP 框架,在设计之初参考了其他开源框架 fasthttpginecho 的优势,并结合字节跳动内部的需求,使其具有高易用性、高性能、高扩展性等特点,目前在字节跳动内部已广泛使用。如今越来越多的微服务选择使用 Golang,如果对微服务性能有要求,又希望框架能够充分满足内部的可定制化需求,Hertz 会是一个不错的选择。

使用 Hertz(服务端)

安装命令行工具 hz(依然,在此之前,请务必检查已正确设置 GOPATH 环境变量,并将 $GOPATH/bin 添加到 PATH 环境变量中):

代码语言:javascript复制
go install github.com/cloudwego/hertz/cmd/hz@latest

hz 也可被用于为指定 IDL 生成服务代码。

使用 hz new 生成代码,然后使用 go mod tidy 拉取依赖。

main.go 文件,编写以下代码:

代码语言:javascript复制
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(server.WithHostPoerts("127.0.0.1:8080"))

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

    h.Spin()
}

创建了一个 HTTP 服务端,监听 8080 端口并注册了一个 GET 方法的路由函数(/ping)。

Hertz 路由

Hertz 提供了 GET,POST,PUT,DELETE,ANY 等方法用于注册对应请求方式(Reuquest Method)的路由:

代码语言:javascript复制
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"))

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

其中,Any 用于注册所有 HTTP Method 方法;Hertz.StaticFile/Static/StaticFS 用于注册静态文件;Handle 可用于注册自定义 HTTP Method 方法。

路由组

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

代码语言:javascript复制
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()
}

创建了如下路由:/v1/get/v1/post/v2/put/v2/delete

参数路由和通配路由

Hertz 支持丰富的路由类型用于实现复杂的功能,包括静态路由(见上)、参数路由、通配路由。

路由的优先级:静态路由 > 命名路由 > 通配路由

参数路由

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

对于 /user/:name 路由,/user/gordon/user/you 路径会得到匹配,而 /user/gordon/profile/user/ 则不会。

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

代码语言:javascript复制
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"))
    // 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)
    })
    h.Spin()
}
通配路由

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

对于 /src/*path 路由,/src/, /src/somefile.go, /src/subdir/somefile.go 均会得到匹配。

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

代码语言:javascript复制
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"))
    // 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)
    })
    h.Spin()
}
Hertz 参数绑定

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

代码语言:javascript复制
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'"`
        }

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

        ...

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

        ...

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

        ...
    })
...
}
Hertz 中间件

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

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

代码语言:javascript复制
func MyMiddleware() app.HandlerFunc {
  return func(ctx context.Context, c *app.RequestContext) {
    // pre-handle
    // ...
    c.Next(ctx) // call the next middleware(handler)
    // post-handle
    // ...
  }
}

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

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

使用 Hertz(客户端)

Hertz 提供了 HTTP Client 用于帮助用户发送 HTTP 请求:

代码语言:javascript复制
c, err := client.NewClient()
if err != nil {
    return
}
// send http get request
status, body, _ := c.Get(context.Background(), nil, "https://www.example.com")
fmt.Printf("status=%v body=%vn", status, string(body))

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

引用

该文章部分内容来自于以下课程或网页:

  • 字节内部课:Go 框架三件套详解(Web/RPC/ORM)
  • GORM 指南 | GORM – The fantastic ORM library for Golang, aims to be developer friendly.
  • 模型定义 | GORM – The fantastic ORM library for Golang, aims to be developer friendly.
  • 查询 | GORM – The fantastic ORM library for Golang, aims to be developer friendly.
  • 删除 | GORM – The fantastic ORM library for Golang, aims to be developer friendly.
  • 事务 | GORM – The fantastic ORM library for Golang, aims to be developer friendly.
  • Hook | GORM – The fantastic ORM library for Golang, aims to be developer friendly.
  • Object–relational mapping – Wikipedia
  • What is a data source name (DSN)? – TechTarget Definition
  • Convention over configuration – Wikipedia
  • Open Database Connectivity – Wikipedia
  • 数据库事务_百度百科 (baidu.com)
  • kitex/README_cn.md at develop · cloudwego/kitex (github.com)
  • 快速开始 | CloudWeGo
  • Remote procedure call – Wikipedia
  • [Interface description language – Wikipedia](https://en.wikipedia.org/wiki/Interface_description_language#:~:text=An interface description language or,written in an unknown language.)
  • Server Option | CloudWeGo
  • 服务发现 | CloudWeGo
  • hertz/README_cn.md at develop · cloudwego/hertz (github.com)
  • 快速开始 | CloudWeGo
  • 路由 | CloudWeGo
  • 绑定与校验 | CloudWeGo
  • 中间件概览 | CloudWeGo

分发

This work is licensed under CC BY-SA 4.0

0 人点赞