这是我参与「第五届青训营 」伴学笔记创作活动的第 1 天
Web/ RPC / ORM
课程目标: 学习并掌握Hertz/Kitex/Gorm基本用法
Gorm
Gorm 是一个迭代了 10年 + 的功能强大的 ORM 框架,有丰富的开源拓展。
// 定义 gorm model
type Product struct {
Code string
Price uint
}
// 为 model 定义表名
func (p product) TableName() string {
return "product"
}
func main() {
// 链接 MySQL 数据库
db, err = gorm.Open(
mysql.Open("user:password@tcp(127.0.0.1:3306)/dbname?charset=utf8mb4&parseTime=True&loc=Local"),
&gorm.Config{})
if err != nil {
panic("failed to connect database")
}
// 创建一条数据(如何创建多条?
db.Create(&Product{Code : "D42", Price : 100})
}
行中,更新方法使用了 Model 方法,而在 行中,我们设置了 Model 方法的表名,这样就可以知道我们更新的数据在哪一个表了。除了用这种方法,还可以使用 Table 方法设置表名 db.Table()
Gorm 默认约定
- Grom 默认
ID字段作为主键 - 当没有定义表名方法的时候,Grom 使用结构体的蛇形负数作为表名(即小写+下划线+复数)
- 字段名的蛇形作为列名
- 使用
CreatedAt、UpdatedAt字段作为创建、更新时间
gorm 支持的数据库
Grom 目前支持 Mysql, SQLServer, PostgreSQL, SQLite
Grom 创建数据
package main
import (
"fmt"
"gorm.io/driver/mysql"
"gorm.io/gorm"
)
type Product struct {
Id uint `grom:"primarykey"` // 定义主键
Code string `grom:"column: code"` // 定义列名
Price uint `grom:"column: user_id"`// 定义不一样的列名
}
func main() {
db, err := gorm.Open(mysql.Open("username:password@tcp(localhost:9910)/database?charseet=utf8"),
&grom.Config{})
if err != nil : "failed to connect database"
// 创建一条数据
p := &Product{Code: "D42"}
res := db.Create(p)
fmt.Println(res.Error) // 获取错误信息
fmt.Println(p.ID) // 返回插入数据的主键
// 创建多条数据
products := []*Product{{Code: "D41"}, {Code: "D42"}, {Code: "D43"}}
res = db.Create(products)
fmt.Println(res.Error)
for _, p := range products {
fmt.Println(p.ID)
}
}
问题一:当创建数据的时候出现唯一索引冲突
使用 Upsert 的 clause.OnConflict 处理数据冲突
// 以不处理冲突为例,创建一条数据
p := &Product{Code: " D42", ID: 1}
db.Clauses(clause.OnConflict{DoNothing: true}).Create(&p)
问题二:设置默认值
通过使用 default 标签定义默认值
type User struct {
ID int64
Name string `gorm:"default:galeone"`
Age int64 `gorm:"default:18"`
}
Grom 查询数据
Grom 查询数据大体有两种方式,一种是通过 First 一种是通过 Find
u := &User{}
// 查询单条数据(按主键升序方式)
db.First(u)
// 查询多条数据使用Find
users := make([] *User, 0)
result := db.Where("age > 10").Find(&users) // select * from users where age > 10
fmt.Println(result.RowsAffected) // 返回记录数, 相当于 `len(users)`
fmt.Println(result.Error) // 返回错误信息
// 条件查询
// select * from users where name in('jinzhu', 'jinzhu 2');
db.Where("name IN ?", []string{"jinzhu", "jinzhu2"}).Find(&users)
// select * from users where name like '%jin%';
db.Where("name LIKE ?", "%jin%").Find(&users)
// select * from users where name = 'jinzhu' and age >= 22;
db.Where("name = ? And age >= ?", "jinzhu", "22").Find(&users)
// 一个小坑
// select * form users where name = "jinzhu";
db.Where(&User{name: "jinzhu", Age: 0}).Find(&users)
// select * from users where name = "jinzhu" AND age = 0
db.Where(map[string]interface{}{"Name": "jinzhu", "Age": 0}).Find(&users)
注意:
- 使用
First方法的时候,需要注意当查询不到数据的时候,会返回ErrRecordNotFound - 使用
Find方法的时候,查询多条数据,查询不到数据不会返回错误,而是返回一个空列表 - 第 行代码告诉我们,使用
Where方法的时候,如果出现 0值字段(或者 false),就不会构建到 SQL 语句中。想要查询0值字段,需要往里面传一个map结构体。或者使用select的 api
Grom 更新数据
// 更新单条数据
// UPDATE users SET name = "hello", update_at='2023-1-20 12:26:23' WHERE age > 18;
db.Model(&User{ID: 111}).Where("age > ?", 18).Upate("name", "hello")
// 更新多条数据
// UPDATE users SET name = "hello", age = 18 WHERE id = 111;
db.Model(&User{ID: 111}).Updates(User{Name: "hello", Age: 18})
// 根据 map 更新
// UPDATE users SET name="hello", age=18, actived=false WHERE id = 111;
db.Model(&User{ID: 111}).Updates(map[string] interface{}{"name": "hello", "age": 18, "actived": false})
// 只更新选定字段
// update users set name = "hello" where id = 111;
db.Model(&User{ID : 111}).Select("name").Updates(map[string] interface{}{"name" :"hello",
"age":18, "actived": false})
// SQL 表达式更新
// update "prodects" set "price" = price * 2 + 100, where "id = 3"
db.Model(&User{ID: 111}).Update("age", gorm.Expr("age * ? + ?", 2, 100))
注意: 使用 Struct 更新的时候,只会更新非零值,如果需要更新零值,需要使用 Map 或者 select
Grom 删除数据
物理删除
// DELETE FROM users WHERE id = 10;
db.Delete(&User{}, 10)
db.Delete(&User{}, "10")
db.Delete(&User{}, []int{1, 2, 3}) // delete from users where id in (1, 2, 3);
// delete from users where name like "%jinzhu%";
db.Where("name LIKE ?", "%jinzhu%").Delete(User{})
db.Delete(User{}, "email like ?", "%jinzhu%")
软删除
Grom 提供了 grom.DeletedAt 用于帮助用户实现软删除。
拥有软删除能力的 Model 调用 Delete 方法的时候,记录不会从数据库中真正删除,但是 Grom 会将 DeletedAt 设置为当前时间,并且你不能通过正常的查询找到该记录。
可以通过 Unscoped 查询到被软删除的数据阿
...
type User struct {
ID int64
Name string `grom:"default:galcone"`
Age string `gorm:"defalut:18"`
Deleted grom.DeletedAt
}
...
func main() {
...
// 删除一条数据
u := User{ID: 111}
db.Delete(&u) // update users set deleted_at='现在的时间' where id = 111
// 删除多条数据
db.Where("age = ?", 20).Delete(&User{})
users := make([]*User, 0)
// 查询被软删除的数据
db.Unscopeed.Where().Find()
}
Grom 事务
Gorm 提供了 Begin,Commit,Rollback 方法用于使用事务。
tx := db.Begin() // 这里开启一个事务
// 当事务开始时,要执行一些 db 操作的时候,应该使用 'tx' 而不是 'db'
if err = tx.Create(&User{Name: "name"}).Error; err != nil {
tx.Rollback()
// 遇到错误回滚事务
return
}
tx.Commit() // 提交事务
Gorm 提供了 Tansaction 方法用于自动提交事务,避免用户漏写 Commit 和 Rollback
if err = db.Transaction(func(tx * gorm.DB) error {
if err = tx.Create(&User{Name: "name"}).Error; err != {
return err
}
return nil
}); err != nil {
return
}
-
Tansaction返回err的时候会自动Rollback -
Tansaction返回nil的时候会自动Commit
Gorm 性能提高
对于写操作(创建, 更新, 删除), 为了确保数据的完整性,GROM 会将他们封装在事务内运行。这样会造成性能的降低,可以使用 SkipDefaultTransaction 关闭默认事务
使用 Preparestmt 缓存预编译语句可以提高后续的调用的速度
db, err := gorm.Open(mysql.Open("username:password@tcp(localhost:9910)/database?charset=utf8"),
&gorm.Config{
SkipDefaultTransaction: true,
PrepareStmt: true},
)
if err != nil {
panic("failed to connect database")
}
Grom 生态
| Grom 代码生成工具 | github.com/go-gorm/gen |
| Grom 分片库方案 | github.com/go-gorm/sha… |
| Grom 手动索引 | github.com/go-gorm/hin… |
| Grom 乐观锁 | github.com/go-gorm/opt… |
| Grom 读写分离 | github.com/go-gorm/dbr… |
| Grom OpenTelemetry 拓展 | gothub.com/go-gorm/ope… |
Kitex
kitex 是字节内部的 Golang 微服务 RPC 框架,有着高性能,强可拓展性的特点,支持多协议。
Kitex 目前对 Windows 支持不完善,如果本地开发环境是 Windows 建议使用虚拟机或者 WSL2
流程:
- 安装
wsl2: 最好安装在其他盘(非C盘) - 安装
Ubuntu - 在
wsl2上安装go语言环境,安装docker环境 - 每次打开终端
source /etc/profile就可以进入go可编辑环境
想要进入其他盘: cd /mnt/d或者e或者c
安装代码生成工具
go install github.com/cloudwego/kitex/tool/cmd/kitex@latest
go install github.com/cloud
定义IDL
如果我们要进行 RPC , 就需要知道对方的接口是什么,需要传什么参数,同时也需要知道返回值长什么样子。这时候,就需要通过 IDL 来约定双方的协议。
namespace go api
struct Request {
1: string message
}
stuct Response {
1: string message
}
service Echo {
Response echo(1: Request req)
}
Kitex 生成代码
server 服务端
使用以下代码生成代码
kitex -module example -service example echo.thrift
下图中 client 文件是运行起来后被另外一个端口生成的 client 端,初次生成代码是不存在的。
- 服务默认监听
Client 端
创建 Clinet
c, err := echo.NewClient("example", client.WithHostPorts("0.0.0.0:8888")) // 初始化Client
if err != nil {
log.Fatal(err)
}
发起请求
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)
Kitex 服务注册与发现
Kitex 的服务注册与发现已经对接了主流的服务注册与发现中心,如 ETCD, Nacos 等
注册服务
type HelloImpl struct {}
fun (h *HellpImpl) Echo(ctx context.Context, req *api.Request) (resp *api Response, err error) {
resp = &api.Response {
Message: req.Message,
}
return
}
func main() {
r, err := etcd.NewEtcdRegistry([]string{"127.0.0.1:2379"})
if err != nil {
log.Fatal(err)
}
server := hello.NewServer(new(HelloImpl), server.WithRegistry(r), server.WithServerBasicInfo(&rpcinfo.EndpointBasicInfo {
ServerName: "Hello",
}))
err = server.Run()
if err != nil {
log.Fatal(err)
}
}
第 行指定了 ETCD 的集群地址
第 行初始化 server ,先新建一个 server 对象,将 server 的实现( helloImpl )注入,通过 WithReqistry 传递 ETCD 的对象,使用 WithServerBasicInfo 设置服务名 ServerName,使用 WithRegistryInfo 设置其他属性。
这样我们的 server 信息就注册到 ETCD 中了,接下来就是要让 client 能在 ETCD 中获取
发现服务
func main() {
r, err := etcd.NewEtcdResolver([] string{"127.0.0.1:2379"})
if err != nil {
log.Fatal(err)
}
client := hello.MustNewClient("Hello", client.WithResolver(r))
for {
ctx, cancel :=context.WithTimeout(context.Background(), time.Second * 3)
resp, err := client.Echo(ctx, &api.Request{Message: "Hello"})
cancel()
if err != nil {
log.Fatal(err)
}
log.Println(resp)
time.Sleep(time.Second)
}
}
第二行使用 NewEtcdResolver 传递 ETCD 对象的地址,就可以初始化发现对象 r 。这里一定要把 error 处理好。
使用 MustNewClient 传递并发现服务名,达到服务过滤的效果。再将发现的对象 r 传递进来。然后就可以初始化 client
Kitex 生态
XDS 扩展 : 做多泳道的工具,多环境治理,流量路由
opentelemetry 拓展 : 可监控,可链路追踪,且Gron, kitex, hertz 都有这个扩展插件
ETCD服务注册与发现拓展 Nacos服务注册与发现拓展 polaris
Zookeeper 服务注册与发现拓展 丰富的示例代码与业务demo
Hertz
Hertz 是字节内部的 HTTP 框架。
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")) // 会自带一个 recover 中间件
h.GET("/ping", func(c context.Context, ctx *app.RequestContext)) {
ctx.JSON(consts.StatusOK, utils.H{"ping": "pong"})
})
h.Spin()
}
第 行使用 server.Default 会自带一个 recover 中间件,如果不需要或者想自定义中间件的话,可以使用 server.New
第 行注册一个 GET 路由,地址是 \ping,传递一个 function
第 行调用 Spin(),开启自旋,服务没有停止之前,他就会停留在这,不执行后面的代码。
Hertz 路由
Hertz 提供了 GET/POST/PUT/DELETE/ANY 等方法注册路由
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")
})
分组功能
v1 := h.Group("/v1")
{
v1.Post("/login", loginEndpoint) // 路由为 /v1/login
v1.Post("/submit", submitEndpoint)
}
v2 := h.Group("/v2")
{
v2.POST("/login", loginEndpoint)
v2.POST("/submit", submitEndpoint)
}
参数路由
h.GET("/hertz/:version", func(ctx context.Context, c *app.RequestContext) {
version := c.Param("version")
c.String(consts.StatusOK, "hello %s", version)
})
可以匹配到 /hertz/version 但不能匹配到 /hertz
通配路由
h.POST("/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/v1/ 和 /hertz/v2/send
注意: 路由的优先级: 静态路由 > 命名路由 > 通配路由
Hertz 参数绑定
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"`
Vd int `query: "vd"` vd: "$ == 0 || $ == 1"
}
func main() {
h := server.Default(server.WithHostPorts("127.0.0.1:8080"))
h.Post("v:path/bind", func(ctx context.Context, c *app.RequestContext) {
var arg Args // 声明结构体
err := c.BindAndValidate(&arg) // 回写传递指针
if err != nil {
panic(err)
}
fmt.Println(arg)
})
h.Spin()
}
Hertz 中间件
Hertz 中间件主要分为客户端中间件和服务端中间件,当有一些通用的逻辑(比如打印日志)的时候就可以使用中间件(参考洋葱模型)
func MyMiddleware() app.HandlerFunc {
return func(ctx context.Context, c *app.RequestContext) {
fmt.Println("pre-handle")
c.Next(ctx) // 向下执行,调用下一个中间件(handler)
fmt.Println("post-handle")
}
}
func main() {
h := server.Default(server.WithHostPorts("127.0.0.1:8080")) // h注册为全局环境
h.Use(MyMiddleware()) // 全局注册中间件,每个路由调用前都会执行中间件方法(也可以注册在路由组上)
}
第 行全局注册中间件后,每一个路由执行前都会打印 pre-handle ,执行后都会打印 post-handle
Hertz Client
Hertz 提供里 HTTP Client 用于帮助用户发送 HTTP 请求
c, err := client.NewClient()
if err != nil {
return
}
// send http get request
status, body, err := c.Get(context.Background(), nil, "http://www.example.com") // 状态码,主体,错误
fmt.Println("status = %v body = %v\n" status, string(body))
// send http post request
var postArgs protocol.Args
postArgs.Set("arg", "a") // Set post args
sstatus, body, err = c.Post(context.Background(), nil, "http://www.example.com", &postArgs)
fmt.Printf("status = %v body = %v\n", status, string(body))
Hertz 代码生成工具
Hertz 提供了代码生成工具 Hz,通过 IDL(inteface description language) 文件即可生成对应的基础服务代码
namespace go hello.example
struct HelloReq {
1: string Name (api.query = "name")
}
struct HelloResp {
1: string RespBody;
}
service HelloService {
HelloResp HelloMethod(1: HelloReq request) (api.get="/hello")
}
第 行代码中 api.query 就是 Hertz 参数绑定中,通过名字绑定的参数
第 行代码中 api.get 定义了 Hertz 路由中的 get 方法,路径为 /hello
Hertz 性能
- 网络库 Netpoll
- Josn 编解码 Sonic
- 使用
sync.Pool复用对象协议层数据解析优化
Hertz 生态
| 拓展插件 | 网址 |
|---|---|
| opentelemetry 拓展 | github.com/hertz-contr… |
| 国际化拓展 | github.com/hertz-contr… |
| 反向代理拓展 | github.com/hertz-contr… |
| JWT 拓展 | github.com/hertz-contr… |
| Websocket 拓展 | github.com/hertz-contr… |
| 示例代码与业务 Demo | github.com/cloudwego/h… |
| HTTP2 拓展 | github.com/hertz-contr… |