Web/RPC/ORM框架
Gorm、Kitex和Hertz是Go语言生态系统中几个重要的框架:
Gorm,作为一款强大的ORM(对象关系映射)框架,为开发人员提供了在Go应用程序中与数据库交互的便捷方法。通过Gorm,我们能够以面向对象的方式操作数据库,从而降低了数据库操作的复杂性,提升了开发效率。在本文中,我们将探索Gorm的核心特性、基本使用以及在项目中运用的一些方法技巧。
Kitex,则是一个专注于构建高性能RPC(远程过程调用)框架的工具。在分布式系统中,RPC是不可或缺的,而利用Kitex可以使得跨网络的函数调用变得更加高效。我们将了解Kitex的设计、架构以及如何使用它来构建快速响应的分布式应用程序。
Hertz,是一个相对较新的Web框架,专注于简化Web应用程序的开发。借助Hertz,开发人员可以更轻松地构建出色的Web界面和后端逻辑。本文中,我们将看到Hertz框架为Web开发带来的创新,了解其主要特点,并演示如何使用Hertz框架快速打造现代化的Web应用程序。
Gorm
Gorm是一个已迭代10年以上的功能强大的ORM框架,拥有丰富的开源扩展
GORM 指南 | GORM - The fantastic ORM library for Golang, aims to be developer friendly.
- Gorm 通过驱动连接数据库,目前支持 MySQL、SQLServer、PostgreSQL、SQLite;如果需要连接其他类型数据库,可以复用或自行开发驱动
- Data Source Name (DSN)
约定
- 默认使用名为 ID 的字段为主键
- 若未定义 TableName() 方法,默认使用结构体的 蛇形复数(Product -> products) 作为表名
- 使用字段名的蛇形作为列名 (蛇形命名法(snake_case)是指每个空格皆以下划线取代的书写风格,且每个单字的第一个字母皆为小写)
- 使用 CreatedAt、UpdatedAt 字段作为创建、更新时间
基础使用
- 定义 Gorm Model
type Product struct {
Code string
Price uint
}
- 为 Model 定义表名
func (p Product) TableName() string {
return "product"
}
- 连接数据库、创建数据、查询数据、更新数据、删除数据
import (
"gorm.io/driver/mysql"
"gorm.io/driver/sqlite"
"gorm.io/gorm"
)
//db, err := gorm.Open(sqlite.Open("test.db"), &gorm.Config{})
db, err := gorm.Open(mysql.Open("user:pass@tcp(127.0.0.1:3306)/test?charset=utf8mb4&parseTime=True&loc=Local"), &gorm.Config{})
if err != nil {
panic("failed to connect database")
}
//创建数据
p := Product{Code: "D42", Price: 100}
result := db.Create(&p)
p.ID // 返回插入数据的主键
result.Error // 返回 error
result.RowsAffected // 返回插入记录的条数
//创建多条
ps := []*Product{{Code: "D41"}, {Code: "D42"}, {Code: "D43"}}
res = db.Create(ps)
//查询数据
var product Product
db.First(&product, 1) // 根据整形主键查找
db.First(&product, "code = ?", "D42") // 查找 code 字段值为 D42 的记录
//更新数据
db.Model(&product).Update("Price", 200)
db.Model(&product).Updates(Product{Price: 200, Code: "F42"}) // 仅更新非零值字段
db.Model(&product).Updates(map[string]interface{}{"Price": 200, "Code": "F42"})
//删除数据
db.Delete(&product, 1)
- 通过 Tag 定义默认值
type User struct {
ID int64
Name string `gorm:"default:galeone"`
Age int64 `gorm:"default:18"`
}
Upsert
- 不存在则插入,存在则更新
//冲突时什么也不做
db.Clauses(clause.OnConflict{DoNothing: true}).Create(&p)
//冲突时将所有列更新为新值,但主键和具有 sql func 默认值的列除外
db.Clauses(clause.OnConflict{
UpdateAll: true,
}).Create(&p)
db.Clauses(clause.OnConflict{
Columns: []clause.Column{{Name: "id"}},
DoUpdates: clause.Assignments(map[string]interface{}{"count": gorm.Expr("GREATEST(count, VALUES(count))")}),
}).Create(&p)
查询数据
- 使用 First() 查询不到数据会返回 ErrRecordNotFound
- 使用 Find() 查询多条数据,查询不到不会返回错误
- 使用结构体作为查询条件时,只会查询非零值字段,可以使用 Map 来构建查询条件
// 获取第一条记录(主键升序)
db.First(&user)
// SELECT * FROM users ORDER BY id LIMIT 1;
// 获取一条记录,没有指定排序字段
db.Take(&user)
// SELECT * FROM users LIMIT 1;
// 获取最后一条记录(主键降序)
db.Last(&user)
// SELECT * FROM users ORDER BY id DESC LIMIT 1;
result := db.First(&user)
result.RowsAffected // 返回找到的记录数
result.Error // returns error or nil
// 检查 ErrRecordNotFound 错误
errors.Is(result.Error, gorm.ErrRecordNotFound)
var users []User
// Get all records
result = db.Find(&users)
// SELECT * FROM users;
result.RowsAffected // returns found records count, equals `len(users)`
result.Error // returns error
// 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;
// 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);
更新数据
// Update with conditions
db.Model(&User{}).Where("active = ?", true).Update("name", "hello")
// UPDATE users SET name='hello', updated_at='2013-11-17 21:34:10' WHERE active=true;
db.Model(&User{ID:111}).Update("name", "hello")
// UPDATE users SET name='hello', updated_at='2013-11-17 21:34:10' WHERE id=111;
// Update with conditions and model value
db.Model(&User{ID:111}).Where("active = ?", true).Update("name", "hello")
// UPDATE users SET name='hello', updated_at='2013-11-17 21:34:10' WHERE id=111 AND active=true;
// Update attributes with `struct`, will only update non-zero fields
db.Model(&User{ID:111}).Updates(User{Name: "hello", Age: 18, Active: false})
// UPDATE users SET name='hello', age=18, updated_at = '2013-11-17 21:34:10' WHERE id = 111;
// Update attributes with `map`
db.Model(&User{ID:111}).Updates(map[string]interface{}{"name": "hello", "age": 18, "active": false})
// UPDATE users SET name='hello', age=18, active=false, updated_at='2013-11-17 21:34:10' WHERE id=111;
db.Model(&User{ID:111}).Select("name").Updates(map[string]interface{}{"name": "hello", "age": 18, "active": false})
// UPDATE users SET name='hello' WHERE id=111;
// product's ID is `3`
db.Model(&product).Update("price", gorm.Expr("price * ? + ?", 2, 100))
// UPDATE "products" SET "price" = price * 2 + 100, "updated_at" = '2013-11-17 21:34:10' WHERE "id" = 3;
删除数据
物理删除
db.Delete(&User{}, 10)
// DELETE FROM users WHERE id = 10;
db.Delete(&User{}, "10")
// DELETE FROM users WHERE id = 10;
db.Delete(&User{}, []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 时,GORM 并不会从数据库中删除该记录,而是将该记录的 DeleteAt 设置为当前时间,而后的一般查询方法将无法查找到此条记录
// user's ID is `111`
db.Delete(&user)
// UPDATE users SET deleted_at="2013-10-29 10:23" WHERE id = 111;
// Batch Delete
db.Where("age = ?", 20).Delete(&User{})
// UPDATE users SET deleted_at="2013-10-29 10:23" WHERE age = 20;
// Soft deleted records will be ignored when querying
db.Where("age = 20").Find(&user)
// SELECT * FROM users WHERE age = 20 AND deleted_at IS NULL;
以使用 Unscoped 来查询到被软删除的记录
db.Unscoped().Where("age = 20").Find(&users)
// SELECT * FROM users WHERE age = 20;
也可以使用 Unscoped 来永久删除匹配的记录
db.Unscoped().Delete(&order)
// DELETE FROM orders WHERE id=10;
Gorm 事务
Gorm 提供了 Begin、Commit、Rollback 方法用于使用事务;执行 db.Begin() 后,将固化一个连接(底层对数据库操作使用的是连接池)
tx := db.Begin()
// 在事务中执行一些 db 操作(从这里开始,应该使用 'tx' 而不是 'db')
tx.Create(...)
// ...
// 遇到错误时回滚事务
tx.Rollback()
// 否则,提交事务
tx.Commit()
Gorm 提供了 SavePoint、Rollbackto 方法,来提供保存点以及回滚至保存点功能
tx := db.Begin()
tx.Create(&user1)
tx.SavePoint("sp1")
tx.Create(&user2)
tx.RollbackTo("sp1") // Rollback user2
tx.Commit() // Commit user1
【推荐】Gorm 提供 Transaction 方法用于自动提交事务,避免用户漏写 Commit、Rollback 造成数据库连接泄露
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
})
Gorm Hook
Hook 是在创建、查询、更新、删除等操作之前、之后自动调用的函数;如果任何 Hook 返回错误,Gorm 将停止后续的操作并回滚事务
Hook 方法的函数签名应该是 func(*gorm.DB) error
// 开始事务
BeforeSave
BeforeCreate
// 关联前的 save
// 插入记录至 db
// 关联后的 save
AfterCreate
AfterSave
// 提交或回滚事务
func (u *User) BeforeCreate(tx *gorm.DB) (err error) {
u.UUID = uuid.New()
if !u.IsValid() {
err = errors.New("can't save invalid data")
}
return
}
func (u *User) AfterCreate(tx *gorm.DB) (err error) {
if u.ID == 1 {
tx.Model(u).Update("role", "admin")
}
return
}
性能优化
为了确保数据一致性,Gorm 会在事务里执行写入操作(创建、更新、删除) 如果没有这方面的要求,可以在初始化时禁用它,这将获得约 30%+ 性能提升
使用 PrepareStmt 缓存预编译语句可以提高后续调用的速度,本机测试提高约 35%
// 全局禁用
db, err := gorm.Open(sqlite.Open("gorm.db"),
&gorm.Config{
SkipDefaultTransaction: true, // 关闭默认事务
PrepareStmt: true, // 缓存预编译语句
})
// 持续会话模式
tx := db.Session(&Session{SkipDefaultTransaction: true})
tx.First(&user, 1)
tx.Find(&users)
tx.Model(&user).Update("Age", 18)
Gorm 生态
- 代码生成工具:GitHub - go-gorm/gen: Gen: Friendly & Safer GORM powered by Code Generation
- 分片库方案:GitHub - go-gorm/sharding: High performance table sharding plugin for Gorm.
- 手动索引:GitHub - go-gorm/hints: Optimizer/Index/Comment Hints for GORM
- 乐观锁:GitHub - go-gorm/optimisticlock: optimistic lock plugin for gorm
- 读写分离:GitHub - go-gorm/dbresolver: Multiple databases, read-write splitting FOR GORM
- OpenTelemetry 扩展:GitHub - go-gorm/opentelemetry: opentelemetry for gorm
Kitex
- 字节内部的 Golang 微服务 RPC 框架,具有高性能、强可扩展的主要特点,支持多协议且拥有丰富的开源扩展
- 目前对 Windows 的支持不完善
- 服务默认监听 8888 端口
# 需要先安装代码生成工具
go install github.com/cloudwego/kitex/tool/cmd/kitex@latest
go install github.com/cloudwego/thriftgo@latest
kitex --version
thriftgo --version
使用 IDL 定义服务和接口:
- Thrift: Apache Thrift - Interface Description Language (IDL)
- Proto3: Language Guide (proto 3) | Protocol Buffers Documentation
使用以下命令生成代码:
kitex -module example -service example echo.thrift
Server 端将业务逻辑写在生成的 handler.go;Client 端需要先创建 Client,再发起请求
import "example/kitex_gen/api/echo"
import "github.com/cloudwego/kitex/client"
// 使用其他协议一致的客户端也可以,也可以用这个client调用其他同协议的服务端
// 第一个参数可作为服务发现/注册时的服务名
c, err := echo.NewClient("example", client.WithHostPorts("0.0.0.0:8888"))
import "example/kitex_gen/api"
req := api.Request{Message: "my request"}
resp, err := c.Echo(context.Background(), &req, callopt.WithRPCTimeout(3*time.Second))
服务注册与发现
Kitex 的服务注册与发现已经对接了主流的服务注册与发现中心,如 ETCD、Nacos 等
在服务端的 main 函数中注册
func main() {
r, err := etcd.NewEtcdRegistry([]string{"127.0.0.1:2379"})
...
server := hello.NewServer(new(HelloImpl), server.WithRegistry(r), server.WithServerBasicInfo(&rpcinfo.EndpointBasicInfo{
ServiceName: "Hello",
}))
err = server.Run()
...
}
客户端发现
func main() {
r, err := etcd.NewEtcdResolver([]string{"127.0.0.1:2379"})
...
client := echo.MustNewClient("Hello", client.WithResolver(r))
...
}
Kitex 生态
- XDS 扩展:GitHub - kitex-contrib/xds
- OpenTelemetry 扩展:GitHub - kitex-contrib/obs-opentelemetry
- ETCD 服务注册与发现扩展:GitHub - kitex-contrib/registry-etcd
- Nacos 服务注册与发现扩展:GitHub - kitex-contrib/registry-nacos: Nacos as service registry for Kitex.
- Zookeeper 服务注册与发现扩展:GitHub - kitex-contrib/registry-zookeeper
- polaris 扩展:GitHub - kitex-contrib/polaris
- 示例代码与业务 Demo:GitHub - cloudwego/kitex-examples: Examples for Kitex.
Hertz
字节内部的HTTP框架,参考了其他开源框架的优势,具有高易用性、高性能、高扩展性特点
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.WithHostPorts(":8080"))
h.GET("/hello", func(c context.Context, ctx *app.RequestContext) {
ctx.JSON(consts.StatusOK, utils.H{"message": "hello world"})
}) // 注册路由
h.Spin()
}
server.Default 默认继承一个 Recovery 中间件,server.New 则没有
路由
Hertz 提供了 GET、POST、PUT、DELETE、PATCH、HEAD、OPTIONS、ANY 等方法用于注册路由
路由分组
Hertz 也提供了路由组(Group)的能力,用于支持路由分组的功能
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")
})
}
通配参数路由
Hertz 也提供参数路由和通配路由,路由优先级为:静态路由 > 命名路由 > 通配路由
如果设置 /src/*path 通配路由,匹配情况如下;通配参数会匹配所有内容;使用 RequestContext.Param 方法,可以获取路由中携带的参数
| 路径 | 是否匹配 |
|---|---|
| /src/ | 匹配 |
| /src/somefile.go | 匹配 |
| /src/subdir/somefile.go | 匹配 |
// 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 提供了 Bind、Validate、BindAndValidate 函数用于参数绑定和校验
不通过 IDL 生成代码时若字段不添加任何 tag 则会遍历所有 tag 并按照优先级绑定参数,添加 tag 则会根据对应的 tag 按照优先级去绑定参数
参数绑定优先级:path > form > query > cookie > header > json > raw_body
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 的中间件主要分为客户端中间件与服务端中间件,下面是一个服务端中间件
func MyMiddleware() app.HandlerFunc {
return func(ctx context.Context, c *app.RequestContext) {
// pre-handle
// ...
c.Next(ctx) // call the next middleware(handler)
// post-handle
// ...
}
}
中间件会按定义的先后顺序依次执行,如果想快速终止中间件调用,可以使用以下方法,注意当前中间件仍将执行
Abort():终止后续调用AbortWithMsg(msg string, statusCode int):终止后续调用,并设置 response中body,和状态码AbortWithStatus(code int):终止后续调用,并设置状态码
全局中间件:Server 级别中间件会对整个 server 的路由生效
h := server.Default()
h.Use(GlobalMiddleware())
路由组级别中间件:对当前路由组下的路径生效
h := server.Default()
group := h.Group("/group")
group.Use(GroupMiddleware())
HTTP Client
Hertz 也提供 HTTP Client 用于帮助发送 HTTP 请求
c, _ := client.NewClient()
status, body, _ := c.Get(context.Background(), nil, "http://example.com")
var postArgs protocol.Args
postArgs.set("arg", "a") // 设置 Post Args
status, body, _ = c.POST(context.Background(), nil. "http://example.com", &postArgs)
func performRequest() {
c, _ := client.NewClient()
req, resp := protocol.AcquireRequest(), protocol.AcquireResponse()
req.SetRequestURI("http://localhost:8080/hello")
req.SetMethod("GET")
_ = c.Do(context.Background(), req, resp)
fmt.Printf("get response: %s\n", resp.Body()) // status == 200 resp.Body() == []byte("hello hertz")
}
func main() {
h := server.New(server.WithHostPorts(":8080"))
h.GET("/hello", func(c context.Context, ctx *app.RequestContext) {
ctx.JSON(consts.StatusOK, "hello hertz")
})
go performRequest()
h.Spin()
}
代码生成工具
代码生成工具 Hz,通过定义 IDL 文件即可生成对应的基础代码
hz new -module hertz/demo
性能
- 网络库 Netpoll
- JSON 编解码 Sonic
- 使用 sync.Pool 复用对象 协议层数据解析优化
Hertz 生态
- HTTP2 扩展:GitHub - hertz-contrib/http2: HTTP2 support for Hertz
- OpenTelemetry 扩展:GitHub - hertz-contrib/obs-opentelemetry: Opentelemetry for Hertz
- 国际化扩展:GitHub - hertz-contrib/i18n: i18n for Hertz
- 反向代理扩展:GitHub - hertz-contrib/reverseproxy: reverseproxy for Hertz
- JWT 鉴权扩展:GitHub - hertz-contrib/jwt: JWT middleware for Hertz
- Websocket 扩展:GitHub - hertz-contrib/websocket: Websocket for Hertz
- 示例代码和业务逻辑 Demo:GitHub - cloudwego/hertz-examples: Examples for Hertz.