9 GO框架三件套 | 青训营

81 阅读11分钟

GO框架三件套

1 三件套介绍

  • Gorm gorm是Golang语言中一个已经迭代数十年且功能强大、性能极好的ORM框架。

    ORM: Object–relational mapping,即对象关系映射,是一种用于在关系数据库和面向对象的编程语言堆之间转换数据的编程技术。通过 ORM 技术,我们可以将关系数据库中某个数据表的结构关联到某个类/结构体上,并通过修改类/结构体实例的方式轻易的完成数据库增删改查(CRUD)的任务。通过 ORM 技术,我们得以用一种更加友好且高效的方式,在尽量不接触 SQL 语句的情况下操作数据库。

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

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

gorm.io/zh_CN/docs/…

2 GORM

设计简洁、功能强大、自由扩展的全功能ORM

  • 设计原则: API精简、测试优先、最小惊讶、灵活扩展、无依赖可信赖
  • 功能完善:
    • 关联:一对一、一对多、单表自关联、多态;Preload、 Joins 预加载、级联删除;关联模式;自定义关联表
    • 事务:事务代码块、嵌套事务、Save Point
    • 多数据库、读写分离、命名参数、Map、子查询、分组条件、代码共享、SQL表达式(查询、创建、更新)、自动选字段、查询优化器
    • 字段权限、软删除、批量数据处理、Prepared Stmt、自定义类型、命名策略、虚拟字段、自动track时间、SQL Builder、Logger
    • 代码生成、复合主键、Constraint、 Prometheus、 Auto Migration、真 跨数据库兼容...
    • 多模式灵活自由扩展
    • Developer Friendly

2.1 GORM基本使用

GORM 并不属于 Go 标准库,使用Gorm之前需要先安装 GORM 及需要连接对应数据库的驱动(支持MySQL, PostgreSQL, SQlite, SQL Server)。这里选择MySQL。

go get -u gorm.io/gorm
go get -u gorm.io/driver/mysql

GORM 示例代码:

package main

import ( //导入 GORM 和 MySQL 数据驱动:
  "gorm.io/gorm"
  "gorm.io/driver/sqlite"
)

type Product struct { //定义gorm model
  gorm.Model
  Code  string
  Price uint
}

func (p product) TableName() string { //定义一个表名的接口
    return "product"
}

func main() {
  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")
  }

  // Migrate the schema 迁移
  db.AutoMigrate(&Product{})

  // Create 创建数据 可以传入切片,创建多条数据
  db.Create(&Product{Code: "D42", Price: 100})

  // Read 查询数据 First 单个查询
  var product Product
  db.First(&product, 1) // find product with integer primary key 根据主键
  db.First(&product, "code = ?", "D42") // find product with code D42 

  // Update - update product's price to 200 更新数据
  db.Model(&product).Update("Price", 200)
  // Update - update multiple fields 单个
  db.Model(&product).Updates(Product{Price: 200, Code: "F42"}) // non-zero fields 仅更新非零值字段
  db.Model(&product).Updates(map[string]interface{}{"Price": 200, "Code": "F42"}) //更新零值字段

  // Delete - delete product 删除数据
  db.Delete(&product, 1)
}

代码中,首先导入了 GORM 和 MySQL 数据驱动,随后,声明了一个数据库模型 gorm model,数据库模型的结构将被对应到数据表中。

按约定编程的 GORM: 遵循 GORM 的约定,可以少写的配置、代码。

  • 表名为struct name的snake_ cases复数格式
  • 字段名为field name的snake_ case单数格式
  • ID/ ld字段为主键,如果为数字,则为自增主键
  • CreatedAt字段,创建时,保存当前时间
  • UpdatedAt字段,创建、更新时,保存当前时间
  • gorm.DeletedAt字段,默认开启soft delete模式

默认情况下,GORM 使用 ID 作为主键,使用结构体名的蛇形复数snake_ cases作为表名,字段名的蛇形snake_ case作为列名,并使用CreatedAtUpdatedAt 字段追踪创建、更新时间。

type Product struct {
  gorm.Model
  Code  string
  Price uint
}
// 等效于
type Product struct {
    Code      string
    Price     uint
    ID        uint `gorm:"primarykey"`
    CreatedAt time.Time
    UpdatedAt time.Time
    DeletedAt sql.NullTime `gorm:"index"`
}

DSN (Data Source Name):数据源名称,用来描述数据库连接信息。一般都包含数据库连接地址,账号,密码之类的信息。此处的 DSN 为 GORM 提供了以下信息:通过 tcp 协议连接 127.0.0.1:3306 地址(MySQL 数据库连接地址)数据库的 dbname 数据库,并使用 user 作为用户名,pass 作为密码进行认证;指定此连接使用 utf8mb4 作为文字编码集(MySQL 使用 utf8,也作 utf8mb3 作为默认的文字编码集,此文件编码集并不是真正的 UTF-8 编码,对于 emoji 一类高字符平面的 Unicode 文字无法正确存储,而 utf8mb4 才是真正且完整支持 Unicode 编码的 UTF-8 编码),启用 parseTime 以将时间信息正确映射到 Go 的 time.Time 结构体,设置时区 loc 为本地时区。

&gorm.Config{} 为 GORM 启用默认的配置

db.AutoMigrate(&Product{})为指定数据库自动迁移数据模型结构。这会为指定数据模型创建 GORM 可用的数据表结构。这一步是可选的,即使不迁移 schema,数据表也会在创建新记录的时候被创建。

创建

2023-08-07-17-36-14.png clause.OnConflict default

查找

2023-08-07-17-37-07.png First 方法返回符合指定条件的首个记录值;使用 First方法进行查询时,如果查找不到数据会返回 ErrRecodeNotFound 错误。可以使用Find查询多条记录,而 Find 方法在查询不到数据的时候并不会返回错误。 还可使用 Where 子句查询:

// Get first matched record
db.Where("name = ?", "jinzhu").First(&user)
// SELECT * FROM users WHERE name = 'jinzhu' ORDER BY id LIMIT 1;

// Get all matched records
db.Where("name <> ?", "jinzhu").Find(&users)
// SELECT * FROM users WHERE name <> 'jinzhu';

// IN
db.Where("name IN ?", []string{"jinzhu", "jinzhu 2"}).Find(&users)
// SELECT * FROM users WHERE name IN ('jinzhu','jinzhu 2');

// LIKE
db.Where("name LIKE ?", "%jin%").Find(&users)
// SELECT * FROM users WHERE name LIKE '%jin%';

// AND
db.Where("name = ? AND age >= ?", "jinzhu", "22").Find(&users)
// SELECT * FROM users WHERE name = 'jinzhu' AND age >= 22;

// Time
db.Where("updated_at > ?", lastWeek).Find(&users)
// SELECT * FROM users WHERE updated_at > '2000-01-01 00:00:00';

// BETWEEN
db.Where("created_at BETWEEN ? AND ?", lastWeek, today).Find(&users)
// SELECT * FROM users WHERE created_at BETWEEN '2000-01-01 00:00:00' AND '2000-01-08 00:00:00';

// Struct
db.Where(&User{Name: "jinzhu", Age: 20}).First(&user)
// SELECT * FROM users WHERE name = "jinzhu" AND age = 20 ORDER BY id LIMIT 1;

// Map
db.Where(map[string]interface{}{"name": "jinzhu", "age": 20}).Find(&users)
// SELECT * FROM users WHERE name = "jinzhu" AND age = 20;

// Slice of primary keys
db.Where([]int64{20, 21, 22}).Find(&users)
// SELECT * FROM users WHERE id IN (20, 21, 22);

当使用结构体作为查询条件时,只会查询结构体内的非零值字段,这意味着字段值为 0, '', false 或其他零值的字段不会被用于构建查询条件。相对的,可以使用 Map 来构建查找条件。GORM 支持根据map[string]interface{}[]map[string]interface{}{}创建记录

更新:使用结构体更新时,只会更新非零值 2023-08-07-17-58-02.png

删除2023-08-07-18-09-49.png

// Email 的 ID 是 `10`
db.Delete(&email)
// DELETE from emails where id = 10;

// 带额外条件的删除
db.Where("name = ?", "jinzhu").Delete(&email)
// DELETE from emails where id = 10 AND name = "jinzhu";

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%";

需要注意的是,如果我们指定了 gorm.deletedat 字段(gorm.Model 包含该字段),将启用软删除模式:这意味着,改数据模型调用 Delete 方法时,并不会被真正从数据表中删除,而是会设置 DeletedAt 字段为当前时间,此后,你不能再通过普通的查询方法找到该记录。使用 Unscoped 可绕过该机制,找到被软删除的记录。

// user 的 ID 是 `111`
db.Delete(&user)
// UPDATE users SET deleted_at="2013-10-29 10:23" WHERE id = 111;
// 批量删除
db.Where("age = ?", 20).Delete(&User{})
// UPDATE users SET deleted_at="2013-10-29 10:23" WHERE age = 20;
// 在查询时会忽略被软删除的记录
db.Where("age = 20").Find(&user)
// SELECT * FROM users WHERE age = 20 AND deleted_at IS NULL;
db.Unscoped().Where("age = 20").Find(&users)
// SELECT * FROM users WHERE age = 20;
db.Unscoped().Delete(&order)
// DELETE FROM orders WHERE id=10;

2.2 GORM 事务

数据库事务(transaction) 是访问并可能操作各种数据项的一个数据库操作序列,这些操作要么全部执行,要么全部不执行,是一个不可分割的工作单位。事务由事务开始与事务结束之间执行的全部数据库操作组成。 当其中一个操作出现错误,其他操作便会被自动回滚。

2023-08-07-18-16-07.png 2023-08-07-18-20-59.png

db.Transaction(func(tx *gorm.DB) error {
  // 在事务中执行一些 db 操作(从这里开始,您应该使用 'tx' 而不是 'db')
  if err := tx.Create(&Animal{Name: "Giraffe"}).Error; err != nil {
    // 返回任何错误都会回滚事务
    return err
  }

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

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

2.3 GORM Hook

Hook 是在创建、查询、更新、删除等操作之前、之后调用的函数。

如果您已经为模型定义了指定的方法,它会在创建、更新、查询、删除时自动被调用。如果任何回调返回错误,GORM 将停止后续的操作并回滚事务。 2023-08-07-18-29-39.png

2.3 性能

2023-08-07-18-34-03.png

3 kitex

RPC(Remote procedure call,远程过程调用) IDL接口描述语言(Interface definition language)是一种语言的通用术语,它允许用一种语言编写的程序或对象与用未知语言编写的另一个程序进行通信。我们可以使用 IDL 来支持 RPC 的信息传输定义。Kitex 默认支持 thrift 和 proto3 两种 IDL,而在底层传输上,Kitex 使用扩展的 thrift 作为底层的传输协议。

www.cloudwego.io/zh/docs/kit…

Kitex(服务端)

安装 Kitex 代码生成工具

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

定义 IDL,命名为 echo.thrift:

namespace go api // namespace python api 
struct Request {
    1: string message
}
struct Resposne {
    1: string message
}
service Echo {
    Reponse echo(1: Request req)
}

为回声服务生成代码: kitex -module exmaple -service example echo.thrift

2023-08-07-21-00-02.png

handler.go 文件内容如下:

package main
import (
        "context"
        api "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
}

修改 handler.go 内的 Echo 函数为下述代码以实现我们的 Echo 服务逻辑:

func (s *EchoImpl) Echo(ctx context.Context, req *api.Request) (resp *api.Response, err error) {
  return &api.Response{Message: req.Message}, nil
}

运行 sh build.sh 以进行编译,编译结果会被生成至 output 目录.

最后,运行 sh output/bootstrap.sh 以启动服务。服务会在默认的 8888 端口上开始运行。要想修改运行端口,可打开 main.go,为 NewServer 函数指定配置参数,重新编译:

 addr, _ := net.ResolveTCPAddr("tcp", "127.0.0.1:9999")
  svr := api.NewServer(new(EchoImpl), server.WithServiceAddr(addr))

Kitex(客户端):

创建一个客户端来调用回声服务。新建项目并创建 main.go 文件,编写代码:

// 创建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)
}

上述代码中,echo.NewClient 用于创建 client,其第一个参数为调用的 服务名,第二个参数为 options,用于传入参数, 此处的 client.WithHostPorts 用于指定服务端的地址,更多参数可参考基本特性。

// 发起请求
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)

首先创建了一个请求 req , 然后通过 c.Echo 发起了调用。

其第一个参数为 context.Context,通过通常用其传递信息或者控制本次调用的一些行为,你可以在后续章节中找到如何使用它。

其第二个参数为本次调用的请求。

其第三个参数为本次调用的 options ,Kitex 提供了一种 callopt 机制,顾名思义——调用参数 ,有别于创建 client 时传入的参数,这里传入的参数仅对此次生效。 此处的 callopt.WithRPCTimeout 用于指定此次调用的超时(通常不需要指定,此处仅作演示之用)

运行,发起调用:go run main.go

2021/05/20 16:51:35 Response({Message:my request})

Kitex 服务注册与发现: 注册 2023-08-07-21-19-20.png 发现 2023-08-07-21-27-55.png github.com/kitex-contr…

使用 DNS Resolver 进行服务发现: www.cloudwego.io/zh/docs/kit…

import (
    ...
    dns "github.com/kitex-contrib/resolver-dns"
    "github.com/cloudwego/kitex/client"
    ...
)
func main() {
    ...
    client, err := echo.NewClient("echo", client.WithResolver(dns.NewDNSResolver()))
    if err != nil {
        log.Fatal(err)
    }
    ...
}

4 Hertz

Hertz[həːts] 是一个 Golang 微服务 HTTP 框架,在设计之初参考了其他开源框架 fasthttp、gin、echo 的优势,并结合字节跳动内部的需求,使其具有高易用性、高性能、高扩展性等特点,目前在字节跳动内部已广泛使用。如今越来越多的微服务选择使用 Golang,如果对微服务性能有要求,又希望框架能够充分满足内部的可定制化需求,Hertz 会是一个不错的选择。 Hertz

Hertz(服务端): 安装命令行工具 hz(依然,在此之前,请务必检查已正确设置 GOPATH 环境变量,并将$GOPATH/bin添加到 PATH 环境变量中): go install github.com/cloudwego/hertz/cmd/hz@latest

2023-08-07-21-54-32.png 2023-08-07-21-58-24.png route 2023-08-07-21-59-23.png 2023-08-07-21-59-34.png 2023-08-07-22-00-14.png 2023-08-07-22-02-38.png

Hertz(客户端): 2023-08-07-22-04-38.png

Hertz代码生产工具: IDL 代码生成

5 笔记项目

项目地址:EasyNote BizEasyNote

项目模块介绍:

服务名称模块介绍技术框架传输协议注册中心链路追踪
demoapiAPI服务Kitex Ginhttpetcdopentracing
demouser用户数据管理Gorm Kitex protobufetcdopentracing
demonote笔记数据管理Gorm Kitex thriftetcdopentracing

项目服务调用关系:

项目模块功能介绍:

项目技术栈:

IDL 2023-08-07-22-43-51.png 2023-08-07-22-44-33.png

Kitex client 关键代码 github.com/cloudwego/k…

Kitex server 关键代码 github.com/cloudwego/k…

GORM 关键代码 github.com/cloudwego/k…