#6 Go语言的后端开发 | 青训营笔记

126 阅读5分钟

这是我参与「第五届青训营 」伴学笔记创作活动的第 6 天
本次编程学习的基本环境配置如下
OS: macOS 13.1
IDE: Goland 2022.3
Go Version: 1.18

主要内容

这节课主要介绍 如何使用 Hertz/Kitex/Gorm 完成后端开发

三件套

  1. Gorm: ORM框架, 在字节内部广泛使用, 有很多开源扩展
  2. Kitex: 是字节内部的Golang微服务RPC框架, 具有高性能、可扩展性强的主要特点, 有很多开源扩展
  3. Hertz: 是字节内部的HTTP框架, 具有高易用性、高性能、高扩展性等特点

Gorm的基础使用

Gorm支持的数据库

  1. 支持Mysql, SQLServer, PostgreSQL, SQLite等, 需要下载特定的驱动
  2. Gorm通过驱动来连接数据库, 如果需要连接其他类型的数据库, 可以复用/ 自行开发驱动
  3. 需要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的约定

  1. Gorm使用名为ID的字段当成主键
  2. 使用结构体的蛇形负数作为表名
  3. 字段名的蛇形作为列名
  4. 使用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})

数据查找

  1. 使用First时需要注意查询不到数据会返ErrRecordNotFound
  2. 使用Find查询多条数据,查询不到数据不会返回错误
  3. 当使用结构作为条件查询时,GORM只会查询非零值字段。零值字段不会被用于构建查询条件,可使用Map 来构建查询条件
// 根据整形主键查找
db.First(&product, 1)
fmt.Printf("Result:%#v\n", product)

// 使用具体的条件
db.First(&product, "code = ?", "D42")
fmt.Printf("Result:%#v\n", product)

数据更新

  1. 使用结构体更新时,只会更新非零值,如果需要更新零值可以使用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"})

数据删除

  1. Delete()提供物理删除功能
  2. GORM提供了gorm.DeletedAt用于帮助用户实现软删. 拥有软删除能力的Model调用Delete时,记录不会被从数据库中真正删除。
  3. 但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 事务

  1. 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()
  1. Gorm提供了 Transaction方法用于自动提交事务, 避免用户漏写Commit、Rollback。
  2. 感觉这个功能有点像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

  1. GORM 提供了 CURD 的 Hook 能力
  2. Hook 是在 CURD 等操作之前,之后自动调用的函数
  3. 如果任何Hook返回错误, Gorm将停止后续的操作并回滚事务
  4. 数据库的触发器?
  5. 只要结构体实现了对应接口, 就可以在调用对应方法的时候使用这些函数
// 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性能提升

  1. 对于写操作, 为了确保数据的完整性, GORM会将它们封装在事务内运行。但这会降低性能, 你可以使用SkipDefaultTransaction关闭默认事务
  2. 使用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拥有非常丰富的生态

image.png

Kitex的基础使用

定义IDL

  1. 使用IDL定义服务与接口
  2. 如果我们要进行RPC,就需要知道对方的接口是什么,需要传什么参数,同时也需要知道返回值是什么样的。这时候,就需要通过IDL来约定双方的协议,就像在写代码的时候需要调用某个函数,我们需要知道函数签名一样。
  3. Thrift:thrift.apache.org/docs/idl
  4. 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生成代码

  1. 使用kitex -module example -service example echo.thrift命令生成代码 image.png
  2. build.sh构建脚本
  3. kitex_gen 内容相关的生成代码, 主要是基础的Server/Client代码
  4. main.go 程序入口
  5. handler.go 用户在该文件里实现IDL service定义的方法

其他

  1. 服务默认监听8888端口
  2. 目前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()
}

image.png

路由

  1. Hertz提供了GET, POST, PUT, OPTIONS, DELETE等方法的路由
  2. 支持分组路由、参数路由和通配路由, 静态路由>命名路由>通配路由
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())
  1. 可以用c.Abort, c.AbortWithMsg, c.AbortWithStats终止中间件调用链的执行
  2. 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))

}

扩展

image.png

总结

今天跟着直播课了解了GORMHertzKitex三件套的基本使用