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

165 阅读12分钟

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

框架三件套详解(Web/RPC/ORM)

什么是 ORM 框架

对象关系映射(Object Relational Mapping,简称 ORM)模式是一种为了解决面向对象与关系数据库存在的互不匹配的现象的技术。ORM框架是连接数据库的桥梁,只要提供了持久化类与表的映射关系,ORM框架在运行时就能参照映射文件的信息,把对象持久化到数据库中

什么是 RPC 框架

RPC 是远程过程调用(Remote Procedure Call)。RPC 的主要功能目标是让构建分布式计算(应用)更容易,在提供强大的远程调用能力时不损失本地调用的语义简洁性。为实现该目标,RPC 框架需提供一种透明调用机制,让使用者不必显式的区分本地调用和远程调用。

什么是 Web 框架(这里的 Web 框架在后面用 HTTP 框架来称呼)

Web 框架是用于进行 Web 开发的一套软件架构。主要是为开发者封装好了一系列与业务逻辑无关的代码实现,方便开发者专注于业务逻辑代码的编写。

1.三件套介绍

  • ORM 框架:GORM

    GORM 是一个已经迭代了十多年的功能强大的 ORM 框架,拥有非常丰富的开源扩展。

  • RPC 框架:Kitex

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

  • HTTP 框架:Hertz

    Hertz 由字节跳动开发,是 HTTP 框架,参考了其他开源框架的优势,具有高易用性、高性能、高扩展性特点。

2. 三件套的使用

GORM 的基本使用

GORM 的约定(默认)
  • GORM 使用名为 ID 的字段作为主键
  • 使用结构体的蛇形负数作为表名
  • 字段名的蛇形作为列名
  • 使用 CreatedAt、UpdatedAt 字段作为创建、更新时间

示例

// 定义 model
type Product struct {
    Code  string
    Price uint
}
// 为 model 定义表名
func (p Product) TableName() string {
    return "product"
}

func main() {
    // 连接数据库
    // 创建数据
    // 查询数据
    // 更新数据
    // 删除数据
}

参考模型定义 | GORM

GORM 支持的数据库

GORM 目前支持 MySQL、SQLServer、PostgreSQL、SQLite。

GORM连接数据库需要提供 DSN 参数。DSN(DataSource Name)数据源名称,用来描述数据库连接信息。参考github.com/go-sql-driv…

GORM 通过驱动来连接数据库,如果需要连接其它类型的数据库,可以复用/自行开发驱动。

示例

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{})

参考连接到数据库 | GORM

GORM 创建数据

通过 Create 方法创建数据

  • 创建一条数据

    p := &Product{Code: "D42"}
    res := db.Create(p)
    
  • 创建多条数据

    products := []*Product{{Code: "D41"}, {Code: "D42"}, {Code: "D43"}}
    res, err := db.Create(products)
    

特殊的操作 Upsert ,用于创建一条唯一的数据,若该数据已存在则执行更新操作,若该数据不存在则执行插入操作。(这里的Upsert不是一个方法,而是一种思路逻辑)

Upsert 数据冲突问题

  • 使用 OnConflict 方法应对

    // 发生冲突时, 不对冲突做处理, 数据也不会改变
    p := &Product{Code: "D42", ID: 1}
    db.Clauses(clause.OnConflict{DoNothing: true}).Create(&p)
    
  • 使用默认值,在结构体中使用 default 标签为字段定义默认值

    type User struct {
        ID   int64
        Code string `gorm:"default:404"`
    }
    

参考创建 | GORM

GORM 查询数据

使用 First 方法查询第一条数据(默认主键升序),查询不到数据则返回 ErrRecordNotFound 错误

u := &User{}
db.First(u) // SELECT * FROM users ORDER BY id LIMIT 1;

其它类似的有 Take 方法获取一条数据(没有指定排序字段)和 Last 方法获取最后一条数据(默认主键降序)。

使用 Where 方法和 Find 方法查询多条数据,需要注意使用 Find 查询多条数据时,查询不到数据也不会报错。

users := make([]*User, 0)
result := db.Where("age > 10").Find(&users) // SELECT * FROM users where age > 10;
fmt.Println(result.RowsAffected) // 返回查询到的记录数
// IN: SELECT * FROM users WHERE name IN ('abcd', 'defg');
db.Where("name IN ?", []string{"abcd", "defg"}).Find(&users)
// LIKE: SELECT * FROM users WHERE name LIKE '%d%';
db.Where("name LIKE ?", "%d%").Find(&users)
// AND: SELECT * FROM users WHERE name = 'abcd' AND age >= 22;
db.Where("name = ? AND age >= ?", "abcd", "22").Find(&users)

使用结构体作为查询条件

当使用结构体作为条件查询时,GORM 只会查询非零值字段。这意味着字段值为 0、''、false或其他零值的字段无法用于构建查询条件,可以使用 Map 来构建查询条件。

// SELECT * FROM users WHERE name = "abc";
db.Where(&User{Name: "abc", Age: 0}).Find(&users) //Age为零值,不会被构建
// SELECT * FROM users WHERE name = "abc" AND age = 0;
db.Where(map[string]interface{}{"Name": "abc", "Age": 0}).Find(&users)

参考查询 | GORM

GORM 更新数据

使用 Model 方法和 Update 以及 Updates 方法更新数据

  • 条件更新单个列

    // UPDATE users SET name='hello' WHERE age > 18;
    db.Model(&User{}).Where("age > ?", 18).Update("name", "hello")
    
  • 条件更新多个列

    • 根据 struct 更新属性,使用 Struct 更新时,只会更新非零值字段,如果需要更新零值字段可以使用 Map 更新或使用 Select 选择字段。

      // 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 "products" SET "price" = price * 2 + 100 WHERE "id" = 3;
    db.Model(&Product{ID: 3}).Update("price", gorm.Expr("price * ? + ?", 2, 100))
    

参考更新 | GORM

GORM 删除数据

物理删除,硬删除,数据会被直接删除,执行的是删除操作。

// 根据主键删除
db.Delete(&User{}, 10) // DELETE FROM users WHERE id = 10;
db.Delete(&User{}, "10") // DELETE FROM users WHERE id = 10;
db.Delete(&User{}, []int{1, 2, 3}) // DELETE FROM users WHERE id IN (1,2,3);

// 批量删除
db.Where("name LIKE ?", "%d%").Delete(User{}) // DELETE FROM users WHERE name LIKE "%d%";
db.Delete(User{}, "name LIKE ?", "%d%") // DELETE FROM users WHERE name LIKE "%d%";

逻辑删除,软删除,数据实际上没有被删除,执行的实际上是更新操作。

GORM 提供了 gorm.DeletedAt 字段用于实现软删除,只需在 Model 结构体中添加这个字段即可。

type User struct {
    ID      int64
    Name    string `gorm:"default:404"`
    Age     int64  `gorm:"default:18"`
    Deleted gorm.DeletedAt
}

拥有该字段的 Model 调用 Delete 方法时能够进行软删除,记录不会从数据库中真正删除。但 GORM 会将 DeletedAt 置为当前时间,并且无法通过正常的查询方法找到该记录。可以使用 Unscoped 方法查询被软删除的数据,也可以通过 Unscoped 方法来实现永久删除。

db.Unscoped().Where("age = 20").Find(&users)

db.Unscoped().Where("age = 20").Delete(&users) // 数据被永久删除

参考删除 | GORM

GORM 事务

GORM 提供了 Begin、Commit、Rollback 方法用于使用事务

开始事务

tx := db.Begin()

事务操作,注意这里应使用 tx 而不是 db

err := tx.Create(&User{Name: "name"}).Error

事务回滚,遇到错误时放弃本次事务操作

if err != nil {
    tx.Rollback()
    return
}

提交事务,应用本次事务操作带来的修改

tx.Commit()

GORM 提供了 Transaction 方法用于自动提交事务,可以不用写Commit 和 Rollback 方法就能实现事务的自动管理。

db.Transaction(func(tx *gorm.DB) error {
  if err := tx.Create(&User{Name: "404"}).Error; err != nil {
    // 返回任何错误都会回滚事务
    return err
  }

  if err := tx.Create(&User{Name: "504"}).Error; err != nil {
    return err
  }

  // 返回 nil 提交事务
  return nil
})

参考事务 | GORM

GORM Hook

GORM 提供了 CURD 的 Hook 能力。

Hook 是在创建、查询、更新、删除等操作执行之前、之后自动调用的函数。如果 Hook 返回错误,GORM 将停止后续的操作并回滚事务。

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(&User{Name: 'admin'}).Error
}

参考Hook | GORM

GORM 性能提高

对于写操作(创建、更新、删除),为了确保数据的完整性,GORM 会将它们封装在事务内运行,但这会降低性能。可以使用 SkipDefaultTransaction 关闭默认事务。

db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{
  SkipDefaultTransaction: true,
})

使用 PrepareStmt 缓存预编译语句可以提高后续调用的速度

PrepareStmt 可以在连接数据库进行全局配置,也可以在事务操作中临时配置,详细可查阅 GORM 的文档

参考性能 | GORM

GORM 生态

GORM 拥有非常丰富的生态,以下是一些常用的扩展

Kitex 的基本使用

安装 Kitex 代码生成工具

Kitex 目前对 Windows 的支持不完善,建议使用 linux 系统或者安装 WSL | Microsoft Learn

安装代码生成工具

$ go install github.com/cloudwego/kitex/tool/cmd/kitex@latest
$ go install github.com/cloudwego/thriftgo@latest

参考快速开始 | CloudWeGo

定义 IDL

IDL(Interface description language),即接口描述语言。是跨平台开发的基础。

对于RPC框架而言,IDL又不仅仅是一个接口描述语言。对于市面上绝大多数的RPC框架而言,IDL还是一个工具和一种使用过程,专指根据 IDL 描述文件,用指定的开发语言,生成对应的服务端接口模块,和客户端程序。这样的好处是,便于开发者快速开发。

使用 IDL 定义服务与接口

要进行 RPC 远程过程调用,就需要知道对方的接口是什么,需要什么参数,需要知道返回值是什么样的。这就需要通过 IDL 来约定双方的协议,类似于函数签名,然后通过函数名调用函数。

关于 IDL 的具体编写有多种语法,可以参考以下资料

使用 Thrift 语法Apache Thrift - Interface Description Language (IDL)

使用 proto3 语法语言指南 (proto3) | Protocol Buffers | Google Developers或者Language Guide (proto 3) | Protocol Buffers Documentation (protobuf.dev)

这里以 thrift 语法为例,创建一个 echo.thrift

namespace go api

struct Request {
    1: string message
}

struct Response {
	1: string message
}

service Echo {
	Response echo(1: Request req)
}
Kitex 生成代码

在 IDL 的基础上通过运行以下命令生成代码

$ kitex -module example -service example echo.thrift

上述命令中,-module 表示生成的该项目的 go module 名,-service 表明我们要生成一个服务端项目,后面紧跟的 example 为该服务的名字。最后一个参数则为该服务的 IDL 文件。

生成后的项目结构如下:

.
|-- 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 基本使用

服务默认监听 8888 端口。

在 handler.go 中实现方法

type EchoImpl struct{}

func (s *EchoImpl) Echo(ctx context.Context, req *api.Request) (resp *api.Response. err error) {
    // TODO: ...
    return
}
Kitex Client 发起请求

新建一个 client 目录,创建 main.go,在 main.go 中实现方法

  • 创建 Client

    c, err := echo.NewClient("example", client.WithHostPorts("0.0.0.0:8888"))
    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 等。详细信息参考服务发现 | CloudWeGo

Kitex 生态

Kitex 拥有非常丰富的扩展生态,以下是一些常用的扩展

更多信息参考www.cloudwego.io/zh/docs/kit…

Hertz 的基本使用

Hertz 基本使用

使用 Hertz 实现,服务监听 8080 端口并注册了一个 GET 方法的路由函数。

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
}

参考快速开始 | CloudWeGo

Hertz 路由

  • Hertz 提供了 GET、POST、PUT、DELETE、ANY 等方法用于注册路由。

  • Hertz 提供了路由组(Group)的能力,用于支持路由分组的功能。

  • Hertz 提供了参数路由和通配路由,路由的优先级为:静态路由 > 命名路由 > 通配路由

更多信息以及使用方法参考路由 | CloudWeGo

Hertz 参数绑定

Hertz 提供了 Bind、Validate、BindAndValidate 函数用于进行参数绑定和校验

更多信息以及使用方法参考绑定与校验 | CloudWeGo

Hertz 中间件

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

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

func main() {
    h := server.Default(server.WithHostPorts("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()
}

终止中间件调用链的执行,可以使用 c.Abort、c.AbortWithMsg、c.AbortWithStatus 方法。

参考中间件概览 | CloudWeGo

Hertz Client

Hertz 提供了 HTTP Client 用于发送 HTTP 请求。

示例

c, err := client.NewClient()
if err != nil {
    return
}
// get请求
status, body, _ := c.Get(context.Background(), nil, "http://example.com")
fmt.Printf("status=%v body=%v\n", status, string(body))
// post请求
var postArgs protocol.Args
postArgs.Set("arg", "a")
status, body, _ = c.Post(context.Background(), nil, "http://example.com", &postArgs)
fmt.Printf("status=%v body=%v\n", status, string(body))

参考cloudwego/hertz-examples: Examples for Hertz.

Hertz 代码生成工具

Hertz 提供了代码生成工具 Hz ,通过定义 IDL 文件即可生成对应的基础服务代码。

参考hz 命令行工具使用 | CloudWeGo

Hertz 性能
  • 网络库
  • Json 编解码 Sonic
  • 使用 sync.Pool 复用对象协议层数据解析优化
Hertz 生态

Hertz 拥有非常丰富的扩展生态,以下是一些常用的扩展

更多信息参考github.com/cloudwego/h…

3.实战案例介绍

项目介绍

项目地址cloudwego/kitex-examples

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

服务名称服务介绍传输协议主要技术栈
demoapiAPI 服务HTTPGorm/Kitex/Hertz
demouser用户数据管理ProtobufGorm/Kitex
demonote笔记数据管理ThriftGorm/Kitex

功能介绍

项目模块

  • demoapi
    • 用户登录
    • 用户注册
    • 用户创建笔记
    • 用户更新笔记
    • 用户删除笔记
    • 用户查询笔记
  • demouser
    • 创建用户
    • 查询用户
    • 校验用户
  • demonote
    • 创建笔记
    • 更新笔记
    • 删除笔记
    • 查询笔记

项目调用关系

  • demouser 和 demonote 的数据都存储在 MySQL 数据库中,从 MySQL 中获取数据。
  • demouser 和 demonote 使用 ETCD 服务注册。
  • demoapi 使用 proto 协议调用 demouser 操作用户数据,使用 thrift 协议调用 demonote 操作笔记数据。
  • demoapi 使用 ETCD 服务发现。
  • 调用方通过 HTTP 请求调用demoapi。

IDL 介绍

参考项目文件kitex-examples/bizdemo/easy_note/idl

技术栈介绍

技术框架

  • 语言:GO
  • 底层存储:MySQL
  • 服务注册:ETCD
  • RPC 框架
    • Kitex:registry-etcd 和 tracer-opentracing
    • Kitex 扩展
  • ORM 框架
    • GORM
    • GORM 扩展:gorm-mysql 和 gorm-opentracing
  • HTTP 框架
    • Hertz
    • Hertz 扩展:Hertz-Jwt
  • 链路追踪
    • Jeager
    • opentracing

创建笔记功能实现关键代码

Hertz 关键代码

kitex-examples/create_note.go

Kitex Client 关键代码

kitex-examples/note.go

Kitex Server 关键代码

kitex-examples/create_note.go

Gorm 关键代码

kitex-examples/note.go

4.总结与展望

总结

  • 了解 Gorm/Kitex/Hertz 的基本概念
  • 熟悉 Gorm/Kitex/Hertz 的基础用法
  • 通过实战案例分析将三件套的使用联系起来

展望

  • 进一步熟悉 Gorm/Kitex/Hertz 的使用,通过阅读官方文档熟悉框架更多的用法。
  • 阅读笔记项目源码并在本地环境运行,使用 Gorm/Kitex/Hertz 进行项目开发。
  • 参与 Gorm/Kitex/Hertz 的开源贡献,例如 Hertz 的 Windows 平台支持。
  • 官方 issues | 问题 ·Cloudwego/Hertz

参考资料

什么是RPC?原理是什么?如何实现一个 RPC 框架?

RPC框架的IDL与IDL-less

GORM 指南 | GORM

Mysql 的 upsert 操作 | Seeker (mazhuang.vip)

Kitex 快速开始 | CloudWeGo

一文带你快速入门GolangHTTP框架Hertz - 掘金 (juejin.cn)