这是我参与「第五届青训营 」伴学笔记创作活动的第 6 天
本次编程学习的基本环境配置如下
OS: macOS 13.1
IDE: Goland 2022.3
Go Version: 1.18
主要内容
这节课主要介绍 如何使用 Hertz/Kitex/Gorm 完成后端开发
三件套
- Gorm: ORM框架, 在字节内部广泛使用, 有很多开源扩展
- Kitex: 是字节内部的Golang微服务RPC框架, 具有高性能、可扩展性强的主要特点, 有很多开源扩展
- Hertz: 是字节内部的HTTP框架, 具有高易用性、高性能、高扩展性等特点
Gorm的基础使用
Gorm支持的数据库
- 支持Mysql, SQLServer, PostgreSQL, SQLite等, 需要下载特定的驱动
- Gorm通过驱动来连接数据库, 如果需要连接其他类型的数据库, 可以复用/ 自行开发驱动
- 需要DSN,
protocol://{dbUser}:{dbPasswd}@{dbHost}:{dbPort}?database={dbName}
dbInfo := fmt.Sprintf("%s:%s@(%s:%d)/%s?charset=utf8&parseTime=True&loc=Local",
dbUser, dbPassword, dbIp, dbPort, dbName)
db, err := gorm.Open(
mysql.Open(dbInfo),
&gorm.Config{},
)
Gorm的约定
- Gorm使用名为
ID的字段当成主键 - 使用结构体的蛇形负数作为表名
- 字段名的蛇形作为列名
- 使用CreatedAt, UpdatedAt字段作为创建、更新时间
数据插入
注意传递的是指针, gorm可以将该条记录的自增ID写入到结构体对象中, 因此需要传递指针
// Product 定义的Gorm Model
type Product struct {
ID uint `gorm:"primarykey"`
Code string `gorm:"column: code"`
Price uint `gorm:"column: user_id"`
}
// 可以传 切片, 也可以传一个对象
db.Create(&Product{Code: "D42", Price: 100})
数据查找
- 使用First时需要注意查询不到数据会返
ErrRecordNotFound - 使用Find查询多条数据,查询不到数据不会返回错误
- 当使用结构作为条件查询时,GORM只会查询非零值字段。零值字段不会被用于构建查询条件,可使用Map 来构建查询条件
// 根据整形主键查找
db.First(&product, 1)
fmt.Printf("Result:%#v\n", product)
// 使用具体的条件
db.First(&product, "code = ?", "D42")
fmt.Printf("Result:%#v\n", product)
数据更新
- 使用结构体更新时,只会更新非零值,如果需要更新零值可以使用Map更新或使用Select选择字段
// Update更新
db.Model(&product).Update("Price", 200)
// 更新多个字段, 直接传一个结构体 UPDATE `product` SET `code`='F42',`price`=200
// 使用结构体, 只会更新非0值的字段
db.Model(&product).Updates(Product{Price: 200, Code: "F42"})
// 使用Map去更新零值
db.Model(&product).Updates(map[string]any{"Price": 200, "Code": "F42"})
数据删除
Delete()提供物理删除功能- GORM提供了
gorm.DeletedAt用于帮助用户实现软删. 拥有软删除能力的Model调用Delete时,记录不会被从数据库中真正删除。 - 但GORM会将DeletedAt置为当前时间,并且你不能再通过正常的查询方法找到该记录。使用Unscoped可以查询到被软删的数据
// 以整型主键为条件删除数据
db.Delete(&Product{}, 10)
db.Delete(&Product{}, "10")
db.Delete(&Product{}, []int{1, 2, 3})
db.Where("code LIKE ?", "D%").Delete(Product{})
db.Delete(Product{}, "email LIKE ?", "D%")
软删除 例子
type User struct {
ID int64
Name string `gorm:"default:galeone"`
Age int64 `gorm:"default:18"`
// ...
Deleted gorm.DeletedAt
}
func main() {
// ... create db...
u := User{ID: 111}
db.Delete(&u)
// 由于有DeleteAt, 所以是软删除
db.Where("age = ?", 20).Delete(&User{})
users := make([]User, 0)
// 找不到
db.Where("age=20").Find(&users)
// 能找到
db.Unscoped().Where("age=20").Find(&users)
}
链式调用
Create, Find, First等函数返回的还是一个DB对象, 可以链式调用. 返回的对象中包含了调用的错误信息
p := Product{Code: "D42"}
res := db.Create(p)
// 链式调用, res仍然是一个db对象, 其中包含了错误信息
fmt.Println(res.Error)
// 回传p的ID
fmt.Println(p.ID)
如何使用Upsert
使用clause.OnConflict处理数据冲突
db.Clauses(clause.OnConflict{DoNothing: true}).Create(&p)
GORM 事务
- Gorm提供了
Begin,Commit,Rollback方法用于使用事务
// 开始事务
tx := db.Begin()
// 在事务中执行一些DB操作
if err = tx.Create(&User{Name: "name"}).Error; err != nil {
// 回滚
tx.Rollback()
return
}
if err = tx.Create(&User{Name: "name1"}).Error; err != nil {
tx.Rollback()
return
}
// 提交事务
tx.Commit()
- Gorm提供了
Transaction方法用于自动提交事务, 避免用户漏写Commit、Rollback。 - 感觉这个功能有点像
Python中的上下文管理器
if err = db.Transaction(func(tx *gorm.DB) error {
if err = tx.Create(&User{Name: "name"}).Error; err != nil {
return err
}
if err = tx.Create(&User{Name: "name1"}).Error; err != nil {
return err
}
return nil
}); err != nil {
// 检测到错误 自动 Rollback?
return
}
Gorm Hook
- GORM 提供了 CURD 的 Hook 能力
- Hook 是在 CURD 等操作之前,之后自动调用的函数
- 如果任何Hook返回错误, Gorm将停止后续的操作并回滚事务
- 数据库的触发器?
- 只要结构体实现了对应接口, 就可以在调用对应方法的时候使用这些函数
// see also gorm/callbacks/interfaces.go
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 not save invalid data")
}
return nil
}
func (u *User) AfterCreate(tx *gorm.DB) (err error) {
return tx.Create(&Email{ID: u.ID, Email: u.Name + "@***.com"}).Error
}
Gorm性能提升
- 对于写操作, 为了确保数据的完整性, GORM会将它们封装在事务内运行。但这会降低性能, 你可以使用SkipDefaultTransaction关闭默认事务
- 使用PrepareStmt缓存预编译语句可以提高后续调用的速度
dbInfo := fmt.Sprintf("%s:%s@(%s:%d)/%s?charset=utf8&parseTime=True&loc=Local", dbUser, dbPassword, dbIp, dbPort, dbName)
db, err := gorm.Open(
mysql.Open(dbInfo),
&gorm.Config{
SkipDefaultTransaction: true, // 关闭默认事务
PrepareStmt: true, // 缓存预编译语句
},
)
Gorm拥有非常丰富的生态
Kitex的基础使用
定义IDL
- 使用IDL定义服务与接口
- 如果我们要进行RPC,就需要知道对方的接口是什么,需要传什么参数,同时也需要知道返回值是什么样的。这时候,就需要通过IDL来约定双方的协议,就像在写代码的时候需要调用某个函数,我们需要知道函数签名一样。
- Thrift:thrift.apache.org/docs/idl
- Proto3 :developers.google.com/protocol-bu…
比如定义echo.thrift如下
namespace go api
struct Request{
1: string message
}
struct Response{
1: string message
}
service Echo {
Response echo(1: Request req)
}
Kitex生成代码
- 使用
kitex -module example -service example echo.thrift命令生成代码 build.sh构建脚本kitex_gen内容相关的生成代码, 主要是基础的Server/Client代码main.go程序入口handler.go用户在该文件里实现IDL service定义的方法
其他
- 服务默认监听8888端口
- 目前Kitex的服务注册和发现已经对接了主流的服务注册和发现中心, 如
etcd,nacos等
Hertz基础使用
简单案例
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.WithHostPorts("127.0.0.1:8080"))
h.GET("/ping", func(c context.Context, ctx *app.RequestContext) {
ctx.JSON(consts.StatusOK, utils.H{"ping": "pong"})
})
h.Spin()
}
路由
- Hertz提供了GET, POST, PUT, OPTIONS, DELETE等方法的路由
- 支持分组路由、参数路由和通配路由, 静态路由>命名路由>通配路由
func registerRoute(h *server.Hertz) {
h.GET("/get", func(c context.Context, ctx *app.RequestContext) {
ctx.String(consts.StatusOK, "GET")
})
h.POST("/post", func(c context.Context, ctx *app.RequestContext) {
ctx.String(consts.StatusOK, "POST")
})
// ... 此外, 还支持 PUT, DELETE, ANY等方法注册路由
// 支持分组路由
v1 := h.Group("/v1")
{
v1.POST("login", func(c context.Context, ctx *app.RequestContext) {
panic("Login")
})
v1.POST("submit", func(c context.Context, ctx *app.RequestContext) {
panic("Submit")
})
}
// v2, v2, ...
// 参数路由
h.GET("/hertz/:version", func(c context.Context, ctx *app.RequestContext) {
ctx.String(consts.StatusOK, ctx.Param("version"))
})
// 通配路由
h.GET("/hertz/:version/*action", func(c context.Context, ctx *app.RequestContext) {
version := ctx.Param("version")
action := ctx.Param("action")
message := version + " is " + action
ctx.String(consts.StatusOK, message)
// 可以获取整条路径
fmt.Println(ctx.FullPath())
})
}
参数绑定和校验
Hertz提供了Bind, Validate, BindAndValidate函数用于参数绑定和校验
type Args struct {
Query string `query:"query"`
QuerySlice []string `query:"q"`
Path string `path:"path"`
Header string `header:"header"`
Form string `form:"form"`
Json string `json:"json"`
Vd int `query:"vd" vd:"$==0||$==1"`
}
func registerPostExample(h *server.Hertz) {
h.POST("v:path/bind", func(c context.Context, ctx *app.RequestContext) {
var arg Args
if err := ctx.BindAndValidate(&arg); err != nil {
panic(err)
}
fmt.Println(arg)
})
}
中间件
Hertz的中间件主要分为客户端中间件与服务器中间件, 如下展示一个服务器中间件.
func myMiddleWare() app.HandlerFunc {
return func(c context.Context, ctx *app.RequestContext) {
// pre-handle
fmt.Println("pre-handle")
ctx.Next(c)
fmt.Println("post-handle")
}
}
...
h.Use(MyMiddleWare())
- 可以用
c.Abort,c.AbortWithMsg,c.AbortWithStats终止中间件调用链的执行 - 用
next执行下一个中间件
Hertz客户端
package main
import (
"context"
"fmt"
"github.com/cloudwego/hertz/pkg/app/client"
"github.com/cloudwego/hertz/pkg/protocol"
)
func main() {
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))
// send http post request
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))
}
扩展
总结
今天跟着直播课了解了GORM、Hertz和Kitex三件套的基本使用