Go框架三件套(Web/RPC/ORM) | 青训营笔记

95 阅读6分钟

这是我参与「第五届青训营」伴学笔记创作活动的第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
    }
    
  • 性能提升

    1. 对于写操作(创建、更新、删除),没有使用关联创建,也没有使用 Hook 的时候,没有必要使用默认事务

      默认事务会降低性能,使用 SkipDefaultTransaction 关闭默认事务

    2. 使用 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.thrift

    namespace 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.py
    

    build.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()
    }
    

    可使用 AbortAbortWithMsgAbortWithStatus 终止中间件调用链的执行

  • 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))