【青训营笔记】- Go 框架三件套详解(Web/RPC/ORM)

58 阅读9分钟

这是我参与「第五届青训营 」笔记创作活动的第5天。

前言

本次青训营中我觉得最为重要,最为基础的一节课了,大项目的实现全靠这哥仨。由于不熟悉Go语言,因此要以Java中Spring生态的框架在Go中模拟开发大项目,Gorm相当于MyBatis,Kitex差不多是SpingClond,Hertz是WEB框架,对应SpringMVC。因此该节课非常重要,认真做好笔记!

三件套介绍

Gorm

简介

Gorm 是一个已经迭代了10年+的功能强大的 ORM框架,在字节内部被广泛使用并且拥有非常丰富的开源扩展 文档介绍

基本使用

  • 定义数据库bean结构体
  • 为结构体实现一个名为TableName的返回表名字符串的函数来绑定表名
  • 连接数据库:db = gorm.Open(mysql.Open(dsn),&gorm.Config{自定义参数})
  • 添加行:db.Create(&结构体或者切片)
  • 查询单行记录:db.First(&结构体)默认查询第一条数据或者db.First(&放结果的结构体,int主键)或者db.First(&放结果的结构体,条件"code = ?",参数值),会存在问题
  • 更新数据单个字段:db.Model(&结构体)。Update(字段名,值)
  • 更新数据多个字段:
    • db.Model(&结构体)。Update(结构体)//仅更新非零值怎么办
    • db.Model(&结构体)。Update(map[string]interface{})//可以更新非零值
  • 删除行:db.Delete(&结构体,int主键)或者db.Delete(&结构体,条件"code = ?",参数值)类似查询

image.png

gorm约定

  • Gorm 使用名为 ID 的字段 作为主键
  • 没有实现TableName函数的时候使用结构体的蛇形负数作为表名
  • 字段名的蛇形作为列名
  • 使用 CreatedAt、UpdatedAt 字段作为创建更新时间

gorm支持的数据库

  • GORM 目前支持 MySQL、SQLServer、PostgreSQL、SQLite.GORM 通过驱动来连接数据库,如果需要连接其它类型的数据库,可以复用/自行开发驱动。连接到数据库的DSN
  • 连接SQLServer等使用适合的DSN

创建数据

  • 定义数据库bean结构体(没有实现TableName使用结构体名的蛇形负数)
  • go tag: gorm
    • "colomn:列名" 可以让结构体成员属性名与字段名不一致
    • "default:value"
  • gorm是链式调用,会返回结果res
    • res.Errot获取是否有err
    • p.ID获取回填自增主键

常见case

  • 出现唯一索引冲突:使用 clause.OnConflict 处理数据冲突
p := &Product{
   ID:   0,
   Code: "042",
}
db.Clauses(clause.OnConflict{DoNothing: true}).Create(&p)
  • 设置默认值:通过使用 default 标签为字段定义默认值

查询详细

  • 查询数据的注意点查询文档
    • First默认查询第一条,查不到返回ErrRecordNotFound
    • 建议使用 Find 查询多条数据,查询不到数据不会返回错误
  • Find查询:result := db.Where(query条件).Find(&切片地址)
    • result.RowsAffected查询找到的记录数,相当于len(切片)
    • result.Error获取是否有错误
  • Find复杂查询:IN/LIKE/AND/传结构体地址体条件查询(非零值)/map
  • 当使用结构作为条件查询时,GORM 只会查询非零值字段。这意味着如果您的字段值为 0、"、false 或其他 零值,该字段不会被用于构建查询条件,使用Map 来构建查询条件。

image.png

更新数据

  • 更新数据时的注意点更新文档
  • 如果Updates()不是传递结构体的话就需要使用db.Model()传递表名,有也可以用Model传递表名
  • 没有where就会直接使用Model中结构体的非零值作为条件
  • Select去选定要更新的字段名
  • 使用 Struct 更新时,只会更新非零值,如果需要更新零值可以使用 Map 更新或使用Select 选择字段

image.png

删除数据

  • 物理删除与软删除删除文档
  • 物理删除:执行的话数据库中是真的删除了
    • db.Delete(&结构体,主键)
    • db.Delete(&结构体,切片)来批量删除IN
    • db.Where("name like ?",值).Delete(&结构体获取表名)

image.png

  • 实际中建议使用软删除
    • GORM 提供了 gorm.DeletedAt 用于帮助用户实现软删
    • 拥有软删除能力的 Model 调用 Delete 时,记录不会被从数据库中真正删除。但 GORM 会将 DeletedAt 置为当前时间,并且你不能再通过正常的查询方法找到该记录。
    • 使用 Unscoped 可以查询到被软删的数据

image.png

GORM事务

  • Gorm 提供了 Begin、Commit、Rollback 方法用于使用事务事务文档
  • tx:=db.Begin()开启事务,返回db的事务对象,固化连接,不会去复用其他连接池中的连接了,因此要用这个db对象
  • tx.RollBack()
  • tx.Commit()
  • image.png
  • 但是好像都需要手动调用方法,可能会忘记导致连接泄露
  • Gorm 提供了 Tansaction 方法用于自动提交事务,避免用户漏写 Commit、Rollbcak。
    • db.Transaction(func(tx) error),当放回error时会自动回滚,nil则提交 image.png

Gorm Hook

  • GORM 在 提供了 CURD 的 Hook 能力。Hook 是在创建、查询、更新、删除等操作之前、之后自动调用的函数。hook文档
  • 如果任何 Hook 返回错误,GORM 将停止后续的操作并回滚事务。(gorm默认开启事务,能保证一致性,但是性能会降低,可以使用下面性能提高中的方法关闭) image.png

性能提高

性能文档

  • 对于写操作(创建、更新、删除》 ,为了确保数据的完整性,(GORM 会将它们封装在事务内运行。但这会降低性能,你可以使用SkipDefaultTransaction 关闭默认事务。
  • 使用 PrepareStmt 缓存预编译语句可以提高后续调用的速度,李老师机器测试提高大约 35 %左右。
dsn := "username:password@tcp(localhost:3306)/database?charset=utf-8"
db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{
   SkipDefaultTransaction: true,
   PrepareStmt: true,
})

GORM生态

GORM 拥有非常丰富的扩展生态,以下列举一部分常用扩展

GORM扩展名地址作用
GORM 代码生成工具github.com/go-gorm/gen快速生成CRUD代码确保数据安全
GORM 分片库方案github.com/go-gorm/sha…使用分片库
GORM 手动索引github.com/go-gorm/hin…提高性能手动选择索引
GORM 乐观锁github.com/go-gorm/opt…使用数据库实现乐观锁
GORM 读写分离github.com/go-gorm/dbr…读写分离提高性能
GORM OpenTelemetry 扩展github.com/go-gorm/ope…链路追踪和监控

关于更多的 GORM 用法可以查看 Gorm 的文档(gorm.cn)

Kitex

Kitex 是字节内部的Golang 微服务 RPC 架具有高性能、强可扩展的主要特点,支持多协议并且拥有丰富的开源扩展

Kitex服务注册与发现

目前 Kitex 的服务注册与发现已经对接了主流了服务注册与发现中心,如 ETCD,Nacos 等。

image.png

Kitex拓展

Kitex 拥有非常丰富的扩展生态,以下列举一部分常用扩展

拓展地址
XDS 扩展github.com/kitex-contr…
opentelemetry 扩展https://githubcom/kitex-contrib/obs-opentelemetry
ETCD 服务注册与发现扩展github.com/kitex-contr…
Nacos 服务注册与发现扩展github.com/kitex-contr…
Zookeeper 服务注册与发现扩展github.com/kitex-contr…
polaris 扩展github.com/kitex-contr…
丰富的示例代码与业务Demogithub.com/cloudwego/k…

Hertz

Hertz 是字节内部的 HTTP框架,参考了其他开源框架的优势,结合字节跳动内部的需求,具有高易用性、高性能、高扩展性特点。Hertz快速开始

基本使用

func main() {
   h := server.Default()
   h.GET("/ping", func(c context.Context, ctx *app.RequestContext) {
      ctx.JSON(consts.StatusOK, utils.H{"ping": "pong"})
   })
   h.Spin()
}
  • 为什么使用两个上下文:根据实际中出现的问题,分为两个,一个专注于传递元信息,一个专注于做请求和处理

Hertz路由

  • Hertz 提供了 GET、POSTPUT、DELETE、ANY 等方法用于注册路由,Handler自定义HTTP方法。h.GET / POST / PUT /ANY
  • Hertz 提供了路由组( Group)的能力,用于支持路由分组的功能。v1=h.Group("/prefix");v1.POST()
  • Hertz 提供了参数路由和通配路由,路由的优先级为: 静态路由 > 命名路由 > 通配路由
  • 参数路由:h.GET("/path/:id",func);c.Param("id")获取参数
  • 通配路由:h.GET("/path/:id/*action ",func);c.Param("action")。匹配的范围更大,action后可以有子路径

参数绑定

Hertz 提供了 Bind、Validate、BindAndValidate 数用于进行参数绑定和校验。将HTTP中的请求参数转到结构体中

  • 定义参数结构体时使用tag来标记这个成员要从哪里获取参数,如query/path/header/form/json见代码
  • 参数校验的话使用vd tag来进行校验(得具体了解语法)
  • BindAndValidate()需要回写,传指针

image.png

Hertz中间件

Hertz 的中间件主要分为客户端中间件与服务端中间件,如下展示一个服务端中间件

  • 通用的操作:如日志记录、计算接口耗时、元信息的设置和传递(例如springMVC中的拦截器)
  • 实现的话自定义一个无参数函数,返回app.HandleFunc并实现函数即可
  • RequestContext.Next()可以下一步执行,并以此分为前置处理和后置处理
  • 终止中间件调用链的执行:Abort()AbortWithMsg()AbortWlthStats()
  • 在server中注册调用:
    • 全局注册:h.Use(函数名())
    • 局部注册:为路由v1或者为路径绑定,v1.Use(函数名())
func main() {
   h := server.Default()
   //全局绑定中间件
   h.Use(MyMiddleware())
   h.GET("/ping", func(c context.Context, ctx *app.RequestContext) {
      ctx.JSON(consts.StatusOK, utils.H{"ping": "pong"})
   })
   //局部路由中间件绑定
   group1 := h.Group("/route")
   group1.Use(MyMiddleware2())
   {
      group1.GET("/ping")
   }
   h.Use(MyMiddleware())

   h.Spin()
}

func MyMiddleware() app.HandlerFunc {
   return func(c context.Context, ctx *app.RequestContext) {
      //pre-handle
      fmt.Println("pre-handle")
      ctx.Next(c)

      //post-handle
      fmt.Println("post-handle")

   }
}
func MyMiddleware2() app.HandlerFunc {
   return func(c context.Context, ctx *app.RequestContext) {
      //pre-handle
      fmt.Println("route pre-handle")
      ctx.Next(c)

      //post-handle
      fmt.Println("route post-handle")

   }
}

Hertz Client

  • Hertz 提供了 HTTP Client 用于帮助用户发送 HTTP 请求
  • 新建一个Client对象c
  • 添加请求参数(若有)
  • 发送http请求并接收返回:status,body, _ := c.Get(context.Background(),dst,url)
  • 具体可以通过文档中的示例代码深入了解Hertz
c, err := client.NewClient()
if err != nil {
   fmt.Println(err)
   return
}
var postArgs protocol.Args
postArgs.Set("arg", "a")
status, body, err := c.Post(context.Background(), nil/*使用复用的结构体提升性能*/, "http://example.com", &postArgs)
if err != nil {
   fmt.Println(err)
   return
}
fmt.Printf("status = %v body = %v \n", status, string(body))

Hertz代码生成

  • 可以生成对应的IDL和具体的代码

Hertz性能

  1. 网络库 NetPoll:使用字节的内部网络库,在小包场景下的性能优于标准库,但不支持TLS,需要配置切换到标准库
  2. Json 编解码 Sonic:高性能的编解码的JSON库做性能优化,默认使用Sonic
  3. 使用 sync.Pool 复用对象协议层数据解析优化

Hertz生态

Hertz 拥有非常丰富的扩展生态,以下列举一部分常用扩展

拓展地址
HTTP2 扩展github.com/hertz-contr…
opentelemetry 扩展aithub.com/hertz-contr…
国际化扩展github.com/hertz-contr…
反向代理扩展github.com/hertz-contr…
JWT 鉴权扩展qithub.com/hertz-contr…
Websocket 扩展github.com/hertz-contr…
丰富的示例代码与业务Demogithub.com/cloudwego/h…
所有扩展可以参考github.com/cloudwego/h…

项目实战

项目介绍

笔记项目是一个使用 Hertz 、 Kitex、Gorm 搭建出来的具备一定业务逻辑的后端 API 项目 image.png

项目功能

image.png

项目调用关系

image.png

总结

本次课程介绍了Go框架的三件套,web框架hertz,ORM框架Gorm以及RPC框架Kitex。在本次课程熟悉了 Gorm/Kitex/Hertz的基础用法以及常见的问题。最后通过实战案例分析将三个框架的使用串联起来,将框架与实际的一个小需求结合起来,做了一个项目架构的设计以及设计,重点讲解了重点代码。本次课程的项目实战在后续其他时间需要去熟悉具体的代码。