这是我参与「第五届青训营」伴学笔记创作活动的第5天
Go框架三件套(Web/RPC/ORM)
Gorm 基础使用
-
Gorm 的约定(默认)
- Gorm 使用名为 ID 的字段作为主键
- 没有定义 TableName 的时候,默认使用结构体的蛇形负数作为表名
- 字段名的蛇形作为列名
- 使用 CreatedAt 、 UpdateAt 字段作为创建、更新时间
-
定义
gorm model,对应数据库里一张表,它的字段对应表中的每一个字段type Product struct { Code string Price uint }gorm:"primarykey"来将某个字段设为主键gorm:"column: ?"来为某个字段指定列名gorm:"default: ?"来为某个字段定义默认值
-
为 model 定义表名, Gorm 提供了一个
TableName接口,实现接口返回表名func (p Product) TableName() string { return "product" } -
通过使用
gorm.Open初始化数据库连接,第一个参数是数据库的连接,第二个参数是 Gorm 的 Config ,用来传递一些自定义配置。dsn = "user:pass@tcp(127.0.0.1:3306)/dbname?charset=utf8mb4&parseTime=True&loc=Local" db, err := gorm.Open( mysql.Open(dsn), &gorm.Config{}) if err != nil { panic("failed to connect database") } -
创建数据
Create支持创建一条或多条数据,创建多条使用list结构体- 会返回一个 gorm 对象,可以使用对象的Error获取 error
- gorm 主键会进行回写
- 使用 Upsert 处理数据冲突,在 Create 前加上 Clauses(clause.OnConflict{DoNothing: true}) 子句
-
读数据
-
First获取第一条数据(主键升序)查询不到会返回 ErrRecordNotFound
-
Find查询一组数据使用
Find查询多条数据,查询不到不会返回错误返回对象可以获取 找到的记录数 和 错误
-
Where还可以传入 结构体 和 map -
使用结构体作为查询条件时,只会查询非零值字段,意味着字段值为
0,'',false或其他零值的字段不会被用于构建查询条件// IN SELECT * FROM users WHERE name IN ('jinzhu','jinzhu 2'); db.Where("name IN ?", []string{"jinzhu", "jinzhu 2"}).Find(&users) // LIKE SELECT * FROM users WHERE name LIKE '%jin%'; db.Where("name LIKE ?", "%jin%").Find(&users) // AND SELECT * FROM users WHERE name = 'jinzhu' AND age >= 22; db.Where("name = ? AND age >= ?", "jinzhu", "22").Find(&users) 零值问题 // SELECT * FROM users WHERE name = "jinzhu" ORDER BY id LIMIT 1; db.Where(&User{Name: "jinzhu", Age: 0}).First(&user) // SELECT * FROM users WHERE name = "jinzhu" AND age = 0; db.Where(map[string]interface{}{"name": "jinzhu", "age": 0}).Find(&users)
-
-
更新数据
-
Update用来更改单个字段,Updates用来更改多个字段,参数是结构体或者map -
传递
Model意味着设置一个表名,实现TableName接口就用返回的表名,否则选择结构体的蛇形负数另一个方式是使用
Table,参数是表名字符串 -
使用结构体仅会更新非零值字段
-
用
Select可以选定字段更新 -
Gorm 表达式
gorm.Expr(expr, args...)
-
-
删除数据
-
物理删除
db.Delete(&User{}, 10) // DELETE FROM users WHERE id = 10; db.Delete(&User{}, "10") // DELETE FROM users WHERE id = 10; db.Delete(&users, []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%"; -
软删除
需要额外定义一个
Deleted字段,使用gorm.DeletedAt类型拥有软删除能力的
Model调用Delete时,记录不会从数据表中真正删除,而是将记录的DeletedAt字段设置为当前时间,并且不能再通过正常的查询方法找到该记录在执行前调用
Unscoped可以查询到被软删除的数据
-
-
事务
Gorm 提供了 Begin、Commit、Rollback 方法用于事务
使用 Begin 开启事务后,应该要使用方法返回的 gorm 对象而不是原来的
Gorm 提供了
Transaction方法用于自动提交事务,避免用户漏写 Commit、Rollback (推荐使用)- 出现 error 或者 panic 的时候,这个方法帮我们自动回滚
- 返回 nil ,自动提交事务
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 { return } -
Hook
Gorm 提供了 CURD 的 Hook 能力
Hook 是在创建、查询、更新、删除等操作之前、之后调用的函数
如果任何 Hook 返回错误,Gorm 将停止后续的操作并回滚事务
type User struct { ID int64 Name string `gorm:"default:galeone"` Age string `gorm:"default:18"` } 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'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 } -
性能提升
-
对于写操作(创建、更新、删除),没有使用关联创建,也没有使用 Hook 的时候,没有必要使用默认事务
默认事务会降低性能,使用 SkipDefaultTransaction 关闭默认事务
-
使用 PrepareStmt 缓存预编译语句可以提高后续调用的速度,提高大约35%左右
db, err := gorm.Open( mysql.Open("root:gh123456@tcp(127.0.0.1:3306)/product?charset=utf8mb4&parseTime=True&loc=Local"), &gorm.Config{ SkipDefaultTransaction: true // 关闭默认事务 PrepareStmt : true // 缓存预编译语句 }) if err != nil { panic("failed to connect database") } -
Kitex
Kitex 目前对 Windows 的支持不完善,建议使用虚拟机或者 WSL2
-
安装Kitex
go install github.com/cloudwego/kitex/tool/cmd/kitex@latest
go install github.com/cloudwego/thrift@latest
-
使用 IDL 定义服务与接口
命名为
echo.thriftnamespace go api struct Request { 1: string message } struct Resposne { 1: string message } service Echo { Reponse echo(1: Request req) }如果我们要使用 RPC,就需要知道对方的接口是什么,需要传什么参数,同时也需要知道返回值是什么样的
使用 IDL 来约定双方的协议,就像在写代码时需要调用某个函数,我们需要知道函数签名一样
-
Kitex 生成代码
使用
kitex -module exmaple -service example echo.thrift命令生成代码. |-- build.sh |-- echo.thrift |-- handler.go |-- kitex_gen | `-- api | |-- echo | | |-- client.go | | |-- echo.go | | |-- invoker.go | | `-- server.go | |-- echo.go | `-- k-echo.go |-- main.go `-- script |-- bootstrap.sh `-- settings.pybuild.sh:构建脚本kitex_gen:IDL 内容相关的生成代码,主要是基础的 Server / Client 代码main.go:程序入口handler.go:用户该文件里实现 IDL service 定义的方法。
-
Kitex 基本使用
下面为
handler.go的内容服务默认监听 8888 端口
package main import ( "context" "exmaple/kitex_gen/api" ) // EchoImpl implements the last service interface defined in the IDL. type EchoImpl struct{} // Echo implements the EchoImpl interface. func (s *EchoImpl) Echo(ctx context.Context, req *api.Request) (resp *api.Response, err error) { // TODO: Your code here... return }
-
Kitex Client 发起请求
创建Client
import "example/kitex_gen/api/echo" import "github.com/cloudwego/kitex/client" ... c, err := echo.NewClient("example", client.WithHostPorts("0.0.0.0:8888")) if err != nil { log.Fatal(err) }使用
NewClient初始化Client,第一个参数为目标服务名,第二个参数指定对端服务的IP PORT发起请求
import "example/kitex_gen/api" ... 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 等
Hertz 基本使用
-
安装 Hertz
go install github.com/cloudwego/hertz/cmd/hz@latest
-
使用 Hertz 实现,服务监听 8080 端口并注册了一个 GET 方法的路由函数
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.WithHostPoerts("127.0.0.1:8080")) h.GET("/ping", func(c context.Context, ctx *app.RequestContext) { ctx.JSON(consts.StatusOK, utils.H{"message": "pong"}) }) h.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") })Hertz 提供了路由组(Group)的能力,用于支持路由分组的功能,同时中间件也可以注册到路由组上
Hertz提供了参数路由和通配路由,Hertz 支持丰富的路由类型用于实现复杂的功能,包括静态路由、参数路由、通配路由
路由的优先级:
静态路由>命名路由>通配路由-
参数路由
h.GET("/hertz/:version", func(ctx context.Context, c *app.RequestContext) { version := c.Param("version") c.String(consts.StatusOK, "Hello %s", version) }) -
通配路由
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 参数绑定
Hertz 提供了 Bind,Validate,BindAndValidate 函数用于进行参数绑定和校验
-
Hertz 中间件
Hertz 的中间件主要分为客户端中间件与服务端中间件。如下展示一个服务端中间件
func MyMiddleware() app.HandlerFunc { return func(ctx context.Context, c *app.RequestContext) { // pre-handle fmt.Println("pre-handle") c.Next(ctx) // call the next middleware(handler) // post-handle fmt.Println("post-handle") } } func main() { h := server.Default(server.WithHostPort("127.0.0.1:8080")) h.Use(MyMiddleware()) h.Get("/middleware",func(ctx context.Context, c *app.RequestContext) { c.String(consts.StatusOK, "Hello hertz!") }) h.Spin() }可使用
Abort,AbortWithMsg,AbortWithStatus终止中间件调用链的执行
-
Hertz Client
Hertz 提供了 HTTP Client 用于帮助用户发送 HTTP 请求
c, err := client.NewClient() if err != nil { return } // send http get request status, body, _ := c.Get(context.Background(), nil, "https://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") // Set post args status, body, _ = c.Post(context.Background(), nil, "https://www.example.com", &postArgs) fmt.Printf("status=%v body=%v\n", status, string(body))