Golang Web/RPC/ORM 框架 | 青训营

204 阅读12分钟

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 提供了 SavePointRollbackto 方法,来提供保存点以及回滚至保存点功能

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 生态

Kitex

Kitex | CloudWeGo

  • 字节内部的 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 定义服务和接口:

使用以下命令生成代码:

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

服务注册与发现

服务发现 | CloudWeGo

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 生态

Hertz

Hertz | CloudWeGo

字节内部的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 等方法用于注册路由

路由 | CloudWeGo

路由分组

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 也提供参数路由和通配路由,路由优先级为:静态路由 > 命名路由 > 通配路由

通配参数路由 | CloudWeGo

如果设置 /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 函数用于参数绑定和校验

绑定与校验 | CloudWeGo

不通过 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)

	...
})

中间件

中间件概览 | CloudWeGo

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

客户端 | CloudWeGo

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 代码生成 | CloudWeGo

代码生成工具 Hz,通过定义 IDL 文件即可生成对应的基础代码

hz new -module hertz/demo

性能

  1. 网络库 Netpoll
  2. JSON 编解码 Sonic
  3. 使用 sync.Pool 复用对象 协议层数据解析优化

Hertz 生态