这是我参与「第五届青训营 」伴学笔记创作活动的第6天
青训营小帖士 @所有人 Hertz 新手任务地址: github.com/cloudwego/h…
Go 框架三件套详解(Web/RPC/ORM)实战环节-笔记服务项目地址:
Hertz 代码设计实践: www.cloudwego.io/zh/blog/202…
1 课程介绍
将前面几节课所学到的知识应用到项目中
掌握Hertz/Kitex/Gorm的基本用法
通过学习实战案例, 可以使用Hertz/Kitex/Gorm完成日常后端开发任务
2 三件套的使用
简介
Gorm 是功能强大的ORM框架, 被字节广泛使用, 拥有非常丰富的开源扩展
Kitex 是字节内部的Golang微服务RPC框架, 具有高性能, 强可扩展性的主要特点, 支持多协议且拥有丰富的开源扩展
Hertz 是字节内部的HTTP框架, 参考了其他开源框架的优势, 结合字节跳动内部的需求, 具有高易用性, 高性能, 高扩展性特点
使用
Gorm
go get -u gorm.io/gorm
(-u 表示update, 即如果包已有则更新为最新版本, 如果没用则下载)
go get -u gorm.io/driver/sqlite
这里我们要用mysql而不是sqlite
go get -u gorm.io/driver/mysql
package main
import (
"gorm.io/driver/mysql"
"gorm.io/gorm"
)
// Product 定义gorm model
type Product struct {
Code string
Price uint
}
// TableName 定义表名
func (p Product) TableName() string {
return "product"
}
func main() {
// 连接数据库
db, err := gorm.Open(
// 后续会讲什么是dsn
mysql.Open("user:pass@tcp(127.0.0.1:3306)/dbname?charset=utf8mb4&parseTime=True&loc=Local"),
&gorm.Config{})
if err != nil {
panic("failed to connect database")
}
// Create 创建数据
// 支持创建单个或多个
// 创建单个时传对象指针, 如&Product
// 创建多个时传切片, 如[]Product
db.Create(&Product{
Code: "042",
Price: 100,
})
// Read 查询数据
// First: 查单条, Find: 查多条
var product Product
// 注意传的是指针
db.First(&product, 1) // 根据整型主键查找 (不懂)
db.First(&product, "code = ?", "042") // 查找 cod 字段值为 042 的记录
// Update - 更新单个字段
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 删除数据
db.Delete(&product, 1)
}
约定 (默认)
Gorm使用名为ID的字段作为主键
当没有定义TableName方法时, Gorm使用结构体的蛇形复数作为表面
使用CreatedAt, UpdatedAt字段作为创建, 更新时间
数据库
Gorm通过驱动来连接数据库
支持MySQL, SQLServer, PostgreSQL, SQLite
DSN: Data Source Name
[username[:password]@][protocol[(address)]]/dbname[?param1=value1&...¶mN=valueN] 如username:password@protocol(address)/dbname?param=value
一些经验
//// 创建数据
用clause.OnConfict处理数据冲突
// 例如: create冲突时不进行处理
db.Clauses(clause.OnConflict{DoNothing: true}).Create(&p)
用default标签为字段定义默认值
type User struct {
ID int64
Name string `gorm:"default:galeone"`
Age int64 `gorm:"default:18"`
}
//// 查询数据
First查询不到数据时, 会返回ErrRecordNotFound
而用Find查询不到数据时, 不会返回错误
⭐一般都用Find, 然后自己判断是否查询到数据
当使用结构体作为查询条件时, Gorm只会查询非零值字段
db.Where(&User{Name: "jinzhu", Age: 0}).Find(&users) // Age是零值字段, 会被忽略
想查询零值字段, 要用Map来构建查询条件
db.Where(map[string]interface{}{"Name": "jinzhu", "Age": 0}).Find(&users)
//// 更新数据
使用Struct更新时, 只会更新非零值, 如果需要更新零值, 可以使用Map更新或者使用Select选择字段
//// 删除数据
// 物理删除
传一个值, 表示主键值, 如id=10
db.Delete(&User{}, 10)
传切片表示, 主键值范围, 即id IN (1, 2, 3)
db.Delete(&User{}, []int{1, 2, 3})
筛选后删除
db.Where("xxx").Delete(User{})
等价于
db.Where(User{}, "xxx")
// 软删除: 记录不会从数据库真正删除, 但不能通过正常的查询方法找到该记录. 使用Unscoped可以查询到被软删的数据
要在struct里额外定义一个字段
type User struct {
Deleted gorm.DeleteAt // 存储的是删除时间
}
要用Unscoped才能查询被软删的数据, 如下
db.Unscoped().Where("age = 20").Find(&users)
//// 事物
对数据一致性强的场景时, 要使用事物
tx := db.Begin() // 开启事物, 之后应该用tx, 而不是用db
if err = tx.Create(&User{Name: "name"}).Error; err != nil {
tx.Rollback()
// 遇到错误时回滚事物
return
}
// 提交事务
tx.Commit()
// 手动Rollback和Commit可能会忘记写, 导致链接泄露
// Gorm提供Transaction方法用于自动提交事物, 避免用户漏写Commit, Rollback
⭐最好用Transaction
if err = db.Transaction(func(tx *gorm) error {
if err = tx.Create(&User{Name: "name"}).Error; err != nil {
return err // 返回nil自动Commit, 否则自动Rollback
}
})
//// Hook
// 在创建, 查询, 更新, 删除等操作之前/之后, 自动调用的函数
// Hook自带默认事务, 如果任何Hook返回错误, Gorm将停止后续操作, 并回滚事务
如
func (u *User) BeforeCreate(tx *gorm.DB) (err error) {
if u.Age < 0 {
return erros.New("can't save invalid data")
}
return
}
func (u *User) AfterCreate(tx *gorm.DB) (err error) {
return tx.Create(&Email{ID: u.ID, Email: u.Name + "@***.com"}).Error
}
//// 性能优化
mysql.Open("user:pass@tcp(127.0.0.1:3306)/dbname?charset=utf8mb4&parseTime=True&loc=Local"),
&gorm.Config{
SkipDefaultTransaction: true, // 关闭默认事务
PrepareStmt: true, // 缓存预编译语句
})
1 对于写操作(创建, 更新), 为了保证数的完整性, Gorm会将它们封装在事务内运行, 但这会降低性能, 可以使用SkipDefaultTransaction关闭默认事务
2 使用PrepareStmt缓存预编译语句可以提高后续调用的速度, 本机测试提高大约35%左右
Gorm生态
Gorm 代码生成工具 github.com/go-gorm/gen
Gorm 分片库方案 github.com/go-gorm/sharding
Gorm 手动索引 github.com/go-gorm/hints
Gorm 乐观锁 github.com/go-gorm/optimisticlock
Gorm 读写分离 github.com/go-gorm/dbresolver
Gorm OpenTelemetry扩展 github.com/go-gorm/opentelemetry
Q&A
1 db.Model(&product)的product是用来做什么的?
可以用来做条件判断, 如db.Mode(&User{111})
但更多的意义是传递表名
2 主键冲突时, 不处理冲突, 数据怎么进库?
会报错, 进不了库的
3 为什么有零值的问题?
因为Go语言零值是默认值, 不知道零值是真的零值, 还是没有值
4 用不用.Model就看后续操作是否需要表名?
是
Kitex
示例文档 www.cloudwego.io/zh/docs/kit…
了解 IDL: zh.m.wikipedia.org/zh-hans/%E6…
Thrift IDLy语法: thrift.apache.org/docs/idl
Proto3 IDL语法: developers.google.com/protocol-bu…
RPC框架
(RPC: Remote Produce Call)
Kitex对Windows支持不完善, 建议使用虚拟机或WSL2
正在改进
上手前可先稍微了解:
> 什么IDL以及IDL的语法
如果我们要进行RPC, 就需要知道对方的接口是什么, 需要传什么参数, 同时也需要知道返回值是什么样的. 这时候, 就需要通过IDL来约定双方的协议, 就像写代码的时候需要调用某个函数, 我们需要知道函数签名一样
如
namespace go api
struct Request {
1: string message
}
struct Response {
1: string message
}
service Echo {
Response echo(1: Request req)
}
> 什么是opentracing以及etcd
go install github.com/cloudwego/kitex/tool/cmd/kitex@latest
go install github.com/cloudwego/thriftgo@latest
使用Kitex生成代码
使用Kitex -module example -service example echo.thrift命令生成代码
build.sh // 构建脚本
echo.thrift
handler.go // 用户在该文件里实现IDL service定义的方法
kitex_gen // IDL内容和相关生成代码, 主要是基础的Server/Client代码
api
echo
client.go
echo.go
invoker.go
server.go
echo.go
k-echo.go
main.go // 程序入口
script
bootstrap.sh
settings.py
Kitex基本使用
服务默认监听8888端口
Kitex Client发起请求
目前Kitex的服务注册与发现已经对接了主流了服务注册与发现中心, 如ETCD, Nacos
Kitex不是很懂, 之后仔细学了, 再看PPT
Hertz
Hertx: go的http框架
Hertz提供了代码生成工具Hz, 通过定义IDL(interface description language)文件即可生成对应的基础服务代码
www.cloudwego.io/zh/docs/her…
默认使用的网络库的Netpoll, 性能优于标准库
(当使用TLS时, netpoll不支持, 配置使用标准库)
Json编解码使用Sonic
使用sync.Pool复用对象, 协议层数据解析优化
3 实战案例介绍
项目介绍
笔记项目, 一个使用Hertz, Kitex, Gorm搭建出来具备一定业务逻辑的后端API项目
demoapi API服务
demouser 用户数据管理
demonote 笔记数据管理
更多见PPT, 如项目调用关系等
普通版: github.com/cloudwego/k… ⭐先看普通版
Hertz 代码设计实践: [www.cloudwego.io/zh/blog/202…](
4 课程总结
了解Gorm/Kitex/Hertz是什么
熟悉Gorm/Kitex/Hertz的基础用法
通过实战案例分析将三个框架的使用串联起来