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

71 阅读9分钟

资料链接

快速开始 | CloudWeGo GORM - The fantastic ORM library for Golang, aims to be developer friendly.

1. GORM

1.1 GORM创建数据

创建单条记录

user := User{Name: "Jinzhu", Age: 18, Birthday: time.Now()}
​
result := db.Create(&user) // 通过数据的指针来创建
​
user.ID             // 返回插入数据的主键
result.Error        // 返回 error
result.RowsAffected // 返回插入记录的条数

创建多条记录

users := []*User{
    User{Name: "Jinzhu", Age: 18, Birthday: time.Now()},
    User{Name: "Jackson", Age: 19, Birthday: time.Now()},
}
​
result := db.Create(users) // pass a slice to insert multiple row
​
result.Error        // returns error
result.RowsAffected // returns inserted records count

1.2 GORM查询数据

查询单挑数据

GORM 提供了 FirstTakeLast 方法,以便从数据库中检索单个对象。当查询数据库时它添加了 LIMIT 1 条件,且没有找到记录时,它会返回 ErrRecordNotFound 错误

// 获取第一条记录(主键升序)
db.First(&user)
// SELECT * FROM users ORDER BY id LIMIT 1;
​
// 获取一条记录,没有指定排序字段
db.Take(&user)
// SELECT * FROM users LIMIT 1;
​
// 获取最后一条记录(主键降序)
db.Last(&user)
// SELECT * FROM users ORDER BY id DESC LIMIT 1;
​
result := db.First(&user)
result.RowsAffected // 返回找到的记录数
result.Error        // returns error or nil
​
// 检查 ErrRecordNotFound 错误
errors.Is(result.Error, gorm.ErrRecordNotFound)
​
  1. 使用First的时候,查询不到会返回ErrRecordNotFound
  2. 使用Find查询多条数据的时候,查询不到数据不会返回错误

查询全部

// Get all records
result := db.Find(&users)
// SELECT * FROM users;
​
result.RowsAffected // returns found records count, equals `len(users)`
result.Error        // returns error

使用Struct或Map条件查询

// 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);

当结构体作为查询条件时,仅会比较非零字段,如果要比较所有的字段,需要使用Map来构建查询条件

1.3 更新数据

更新所有字段

Save 会保存所有的字段,即使字段是零值

db.Save(&User{Name: "jinzhu", Age: 100})
// INSERT INTO `users` (`name`,`age`,`birthday`,`update_at`) VALUES ("jinzhu",100,"0000-00-00 00:00:00","0000-00-00 00:00:00")
​
db.Save(&User{ID: 1, Name: "jinzhu", Age: 100})
// UPDATE `users` SET `name`="jinzhu",`age`=100,`birthday`="0000-00-00 00:00:00",`upda

save会保存所有的字段,在有prime key的情况下会执行Update,否则直接执行Create

更新单个列

当使用Update更新单个列时,它需要有条件限制,否则会引发错误ErrMissingWhereClause。当使用Model方法并且它的值有一个主值时,主键将被用来构建条件

// Update with conditions
db.Model(&User{}).Where("active = ?", true).Update("name", "hello")
// UPDATE users SET name='hello', updated_at='2013-11-17 21:34:10' WHERE active=true;
​
// User's ID is `111`:
db.Model(&user).Update("name", "hello")
// UPDATE users SET name='hello', updated_at='2013-11-17 21:34:10' WHERE id=111;
​
// Update with conditions and model value
db.Model(&user).Where("active = ?", true).Update("name", "hello")
// UPDATE users SET name='hello', updated_at='2013-11-17 21:34:10' WHERE id=111 AND active=true;

更新多个列

可以使用struct或者Map进行更新,其中前者只能更新非零字段,后者则是都可以

// Update attributes with `struct`, will only update non-zero fields
db.Model(&user).Updates(User{Name: "hello", Age: 18, Active: false})
// UPDATE users SET name='hello', age=18, updated_at = '2013-11-17 21:34:10' WHERE id = 111;
​
// Update attributes with `map`
db.Model(&user).Updates(map[string]interface{}{"name": "hello", "age": 18, "active": false})
// UPDATE users SET name='hello', age=18, active=false, updated_at='2013-11-17 21:34:10' WHERE id=111;
​

更新选定的字段

可以使用select或者omit选中需要进行更新的字段

// Select with Map
// User's ID is `111`:
db.Model(&user).Select("name").Updates(map[string]interface{}{"name": "hello", "age": 18, "active": false})
// UPDATE users SET name='hello' WHERE id=111;
​
db.Model(&user).Omit("name").Updates(map[string]interface{}{"name": "hello", "age": 18, "active": false})
// UPDATE users SET age=18, active=false, updated_at='2013-11-17 21:34:10' WHERE id=111;// Select with Struct (select zero value fields)
db.Model(&user).Select("Name", "Age").Updates(User{Name: "new_name", Age: 0})
// UPDATE users SET name='new_name', age=0 WHERE id=111;// Select all fields (select all fields include zero value fields)
db.Model(&user).Select("*").Updates(User{Name: "jinzhu", Role: "admin", Age: 0})
​
// Select all fields but omit Role (select all fields include zero value fields)
db.Model(&user).Select("*").Omit("Role").Updates(User{Name: "jinzhu", Role: "admin", Age: 0})

GORM删除

物理删除

// 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);

软删除

当数据具有DeletedAt字段的时候 gorm通过gorm.DeletedAt进行软删除,将数据的DeletedAt设置为当前的时间,使得不会在数据库中通过一般查询查询到该数据,但是可以通过unscoped方法查询到被软删除的数据

// user's ID is `111`
db.Delete(&user)
// UPDATE users SET deleted_at="2013-10-29 10:23" WHERE id = 111;
​
// Batch Delete
db.Where("age = ?", 20).Delete(&User{})
// UPDATE users SET deleted_at="2013-10-29 10:23" WHERE age = 20;
​
// Soft deleted records will be ignored when querying
db.Where("age = 20").Find(&user)
// SELECT * FROM users WHERE age = 20 AND deleted_at IS NULL;
​

GORM事务

gorm使用Begin,Commit,RollBack等方法完成事务的编写

func CreateAnimals(db *gorm.DB) error {
  // 再唠叨一下,事务一旦开始,你就应该使用 tx 处理数据
  tx := db.Begin()
  defer func() {
    if r := recover(); r != nil {
      tx.Rollback()
    }
  }()
​
  if err := tx.Error; err != nil {
    return err
  }
​
  if err := tx.Create(&Animal{Name: "Giraffe"}).Error; err != nil {
     tx.Rollback()
     return err
  }
​
  if err := tx.Create(&Animal{Name: "Lion"}).Error; err != nil {
     tx.Rollback()
     return err
  }
​
  return tx.Commit().Error
}

也可以直接使用Transaction方法

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

GORM性能优化

  • 对于写操作(创建、更新、删除),为了确保数据的完整性,GORM会将它们封装在事务内运行。但这会降低性能,可以使用SkipDefaultTransaction关闭默认事务。
  • 使用PrepareStmt缓存预编译语句可以提高后续调用的速度,本机测试提高大约35%左右。

GORM生态

image-20230826232040862.png

2. Kitex

2.1 Kitex简介

Kitex字节跳动内部的 Golang 微服务 RPC 框架,具有高性能强可扩展的特点,在字节内部已广泛使用。如果对微服务性能有要求,又希望定制扩展融入自己的治理体系,Kitex 会是一个不错的选择。

框架特点

  • 高性能

    使用自研的高性能网络库 Netpoll,性能相较 go net 具有显著优势。

  • 扩展性

    提供了较多的扩展接口以及默认扩展实现,使用者也可以根据需要自行定制扩展,具体见下面的框架扩展。

  • 多消息协议

    RPC 消息协议默认支持 ThriftKitex ProtobufgRPC。Thrift 支持 Buffered 和 Framed 二进制协议;Kitex Protobuf 是 Kitex 自定义的 Protobuf 消息协议,协议格式类似 Thrift;gRPC 是对 gRPC 消息协议的支持,可以与 gRPC 互通。除此之外,使用者也可以扩展自己的消息协议。

  • 多传输协议

    传输协议封装消息协议进行 RPC 互通,传输协议可以额外透传元信息,用于服务治理,Kitex 支持的传输协议有 TTHeaderHTTP2。TTHeader 可以和 Thrift、Kitex Protobuf 结合使用;HTTP2 目前主要是结合 gRPC 协议使用,后续也会支持 Thrift。

  • 多种消息类型

    支持 PingPongOneway双向 Streaming。其中 Oneway 目前只对 Thrift 协议支持,双向 Streaming 只对 gRPC 支持,后续会考虑支持 Thrift 的双向 Streaming。

  • 服务治理

    支持服务注册/发现、负载均衡、熔断、限流、重试、监控、链路跟踪、日志、诊断等服务治理模块,大部分均已提供默认扩展,使用者可选择集成。

  • 代码生成

    Kitex 内置代码生成工具,可支持生成 ThriftProtobuf 以及脚手架代码。

2.2 定义IDL

如果我们要进行 RPC,就需要知道对方的接口是什么,需要传什么参数,同时也需要知道返回值是什么样的,就好比两个人之间交流,需要保证在说的是同一个语言、同一件事。 这时候,就需要通过 IDL 来约定双方的协议,就像在写代码的时候需要调用某个函数,我们需要知道函数签名一样。

首先我们需要编写一个 IDL,这里以 thrift IDL 为例。

首先创建一个名为 echo.thrift 的 thrift IDL 文件。

然后在里面定义我们的服务

namespace go api
​
struct Request {
  1: string message
}
​
struct Response {
  1: string message
}
​
service Echo {
    Response echo(1: Request req)
}

2.3 生成服务代码

有了 IDL 以后我们便可以通过 kitex 工具生成项目代码了,执行如下命令:

$ kitex -module example -service example-server echo.thrift

上述命令中:

  • -module 表示生成的该项目的 go module 名;建议使用完整包名,例如 github.com/Yourname/exampleserver

  • -service 表明我们要生成一个服务端项目,后面紧跟的 example-server 为该服务的名字

  • 最后一个参数则为该服务的 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
    

2.4 编写服务逻辑

我们需要编写的服务端逻辑都在 handler.go 这个文件中,现在这个文件应该如下所示:

package main
​
import (
  "context"
  "example/kitex_gen/api" //如果修改了 -module 参数的值,这里的 'example' 也要相应替换
)
​
// 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
}
​
​

这里的 Echo 函数就对应了我们之前在 IDL 中定义的 echo 方法。

现在让我们修改一下服务端逻辑,让 Echo 服务名副其实。

修改 Echo 函数为下述代码:

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

2.5 编译运行

编译:

$ sh build.sh

执行上述命令后,会生成一个 output 目录,里面含有我们的编译产物。

运行:

$ sh output/bootstrap.sh

执行上述命令后,Echo 服务就开始运行啦!

2.6 编写客户端

有了服务端后,接下来就让我们编写一个客户端用于调用刚刚运行起来的服务端。

首先,同样的,先创建一个目录用于存放我们的客户端代码:

$ mkdir client

进入目录:

$ cd client

然后用 kitex 命令生成客户端所需的相关代码(如果该目录放在前述 example-server 目录下,则可省略此步,因为 server 代码中包含了 client 所需代码):

$ kitex -module example echo.thrift

注:

  1. 客户端代码不需要指定 -service 参数,生成的代码在 kitex_gen 目录下;
  2. -module 参数 建议使用完整的包名,例如 github.com/Yourname/exampleclient

然后创建一个 main.go 文件,就开始编写客户端代码了

2.7 创建client

首先让我们创建一个调用所需的 client

import "example/kitex_gen/api/echo" //如果修改了 -module 参数,这里要将 'example' 相应替换成相应的包名
import "github.com/cloudwego/kitex/client"
...
c, err := echo.NewClient("example-server", client.WithHostPorts("0.0.0.0:8888"))
if err != nil {
  log.Fatal(err)
}

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

2.8 发起调用

接下来让我们编写用于发起调用的代码:

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 用于指定此次调用的超时(通常不需要指定,此处仅作演示之用)同样的,你可以在基本特性一节中找到更多的参数。