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

132 阅读5分钟

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

对于Go开发而言,使用适当的框架可以大大减少开发成本,本文介绍Go在Web/RPC/ORM的三种常用框架。

Gorm

Gorm是一个成熟的ORM框架,拥有丰富的开源扩展。
ORM,即对象关系映射模式,是一种为了解决面向对象与关系数据库存在的互不匹配的现象的技术。

文档

  1. 连接
import (
  "gorm.io/driver/mysql"
  "gorm.io/gorm"
)

func main() {
  // 参考 https://github.com/go-sql-driver/mysql#dsn-data-source-name 获取详情
  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{})
}

db, err := gorm.0pen(mysgl.Open("username;password@tcp(localhost:9910)/database?charset=utf8"), &gorm.Config{})
if err != nil {
	panic("failed to connect database")
}
  1. 新增
user := User{Name: "Jinzhu", Age: 18, Birthday: time.Now()}

result := db.Create(&user) // 通过数据的指针来创建

user.ID             // 返回插入数据的主键
result.Error        // 返回 error
result.RowsAffected // 返回插入记录的条数

GORM 支持根据 map[string]interface{} 和 []map[string]interface{}{} 创建记录,例如:

DB.Model(&User{}).Create(map[string]interface{}{
  "Name": "jinzhu", "Age": 18,
})

// 根据 `[]map[string]interface{}{}` 批量插入
DB.Model(&User{}).Create([]map[string]interface{}{
  {"Name": "jinzhu_1", "Age": 18},
  {"Name": "jinzhu_2", "Age": 20},
})

可以通过标签 default 为字段定义默认值,如:

type User struct {
  ID         int64
  Name       string `gorm:"default:galeone"`
  Age        int64  `gorm:"default:18"`
    uuid.UUID  UUID   `gorm:"type:uuid;default:gen_random_uuid()"` // 数据库函数
}

0、''、false 之类零值,这些字段定义的默认值不会被保存到数据库,您需要使用指针类型或 Scanner/Valuer 来避免这个问题

type User struct {
  gorm.Model
  Name string
  Age  *int           `gorm:"default:18"`
  Active sql.NullBool `gorm:"default:true"`
}
  1. 查询

常用的是where和find组合查询。
First查询失败时返回error,Find查询失败时返回null。

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

当使用结构作为条件查询时,GORM 只会查询非零值字段。这意味着如果字段值为 0、''、false 或其他 零值,该字段不会被用于构建查询条件。

db.Where(&User{Name: "jinzhu", Age: 0}).Find(&users)
// SELECT * FROM users WHERE name = "jinzhu";

可以使用 map 来构建查询条件,例如:

db.Where(map[string]interface{}{"Name": "jinzhu", "Age": 0}).Find(&users)
// SELECT * FROM users WHERE name = "jinzhu" AND age = 0;
  1. 更新
db.Model(&user).Updates(map[string]interface{}{"name": "hello", "age": 18, "actived": false})
// UPDATE users SET name='hello', age=18, actived=false, updated_at='2013-11-17 21:34:10' WHERE id=111;

可以使用SQL表达式更新

// product 的 ID 是 `3`
DB.Model(&product).Update("price", gorm.Expr("price * ? + ?", 2, 100))
// UPDATE "products" SET "price" = price * 2 + 100, "updated_at" = '2013-11-17 21:34:10' WHERE "id" = 3;
  1. 删除

物理删除(删了之后就真的没有了)

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

软删除(工程常用)
gorm.Model包含gorm.deletedat 字段,它将自动获得软删除的能力。
如果您不想引入 gorm.Model,您也可以这样启用软删除特性:

type User struct {
  ID      int
  Deleted gorm.DeletedAt
  Name    string
}

拥有软删除能力的模型调用 Delete 时,记录不会被从数据库中真正删除。但 GORM 会将 DeletedAt 置为当前时间, 并且你不能再通过正常的查询方法找到该记录。可以使用 Unscoped 找到被软删除的记录。

db.Unscoped().Where("age = 20").Find(&users)
// SELECT * FROM users WHERE age = 20;
可以使用 Unscoped 永久删除匹配的记录。
db.Unscoped().Delete(&order)
// DELETE FROM orders WHERE id=10;
  1. 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
}

Gorm 提供了 Tansaction 方法用于自动提交事务,避免用户漏写 Commit,Rolbcak(推荐)。

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

使用SkipDefaultTransaction 关闭默认事务可以提高性能。

  1. Hook

在增create删delete改update之前或者之后进行的行为。

func (u *User) BeforeCreate(tx *gorm.DB) (err error) {
  u.UUID = uuid.New()

  if !u.IsValid() {
    err = errors.New("can't save invalid data")
  }
  return
}

func (u *User) AfterCreate(tx *gorm.DB) (err error) {
  if u.ID == 1 {
    tx.Model(u).Update("role", "admin")
  }
  return
}
  1. 性能优化

使用SkipDefaultTransaction 关闭默认事务可以提高性能。 使用PrepareStmt缓存预编译语句可以提高后续调用的速度,本机测试提高大约 35 %左右。

  1. Gorm生态
    文档

Gorm生态.png

Kitex

字节内部的Golang微服务RPC框架。
文档

  1. 安装Kitex

Kitex 目前对 Windows 的支持不完善,如果本地开发环境是 Windows 的同学建议使用虚拟机或 WSL2。
安装WSL:learn.microsoft.com/zh-cn/windo…
在首次启动新安装的 Linux 发行版Ubuntu时出现问题:
WslRegisterDistribution failed with error: 0x80370102
解决方案:进入BIOS开启虚拟化技术,启动或关闭Windows功能,勾选Hyper-V、适用于Linux的Windows子系统、虚拟机平台,然后重启电脑即可。

安装 kitex和thriftgo,命令行输入:

PS D:\Go> go install github.com/cloudwego/kitex/tool/cmd/kitex@latest
PS D:\Go> go install github.com/cloudwego/thriftgo@latest
PS D:\Go> kitex --version                               
v0.4.4
PS D:\desktop\Go\go-pprof-practice-master> thriftgo --version
thriftgo 0.2.5
  1. 编写 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)
}
  1. 生成 echo 服务代码

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

$ 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
  1. 获取最新的 Kitex 框架

由于 kitex 要求使用 go mod 进行依赖管理,所以我们要升级 kitex 框架会很容易,只需要执行以下命令即可:

$ go get github.com/cloudwego/kitex@latest
$ go mod tidy
  1. 编写 echo 服务逻辑

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

package main

import (
  "context"
  "example/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
}

这里的 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
}
  1. 创建 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)
}

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

  1. 发起请求

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

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

  1. Kitex 服务注册与发现

目前 Kitex 的服务注册与发现已经对接了主流了服务注册与发现中心,如 ETCD,Nacos 等。

  1. Kitex生态

XDS扩展 github.com/kitex-contr…
opentelemetry扩展 github.com/kitex-contr…
ETCD服务注册与发现扩展 github.com/kitex-contr…
Nacos服务注册与发现扩展 github.com/kitex-contr…
Zookeeper服务注册与发现扩展 github.com/kitex-contr…
polaris扩展 github.com/kitex-contr…
丰富的示例代码与业务Demo github.com/cloudwego/k…

Hertz

字节内部HTTP框架。
文档

  1. 在Windows 环境,可以编写如下的示例代码:

在当前目录下创建 hertz_demo 文件夹,进入该目录中 创建 main.go 文件 在 main.go 文件中添加以下代码

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

    h.GET("/ping", func(c context.Context, ctx *app.RequestContext) {
            ctx.JSON(consts.StatusOK, utils.H{"message": "pong"})
    })

    h.Spin()
}

生成 go.mod 文件

$ go mod init hertz_demo

整理 & 拉取依赖

$ go mod tidy
  1. 运行

命令行输入

PS D:\hertz_demo> go run main.go
2023/01/29 20:30:06.195660 engine.go:617: [Debug] HERTZ: Method=GET    absolutePath=/ping                     --> handlerName=main.main.func1 (num=2 handlers)
2023/01/29 20:30:06.263867 engine.go:389: [Info] HERTZ: Using network library=standard
2023/01/29 20:30:06.267262 transport.go:65: [Info] HERTZ: HERTZ: HTTP server listening on address=[::]:8888

浏览器访问 http://127.0.0.1:8888/ping ,显示{"message":"pong"},就算成功运行了。

  1. Hertz路由

Hertz 提供了 GET、POST、PUT、DELETE、ANY 等方法用于注册路由。
Hertz 提供了路由组( Group)的能力,用于支持路由分组的功能
Hertz 提供了参数路由和通配路由,路由的优先级为: 静态路由 >命名路由 >通配路由

  1. Hertz参数绑定

  2. Hertz中间件

  3. Hertz生态

HTTP2扩展 github.com/hertz-contr…
opentelemetry扩展 github.com/hertz-contr…
国际化扩展 github.com/hertz-contr…
反向代理扩展 github.com/hertz-contr…
JWT鉴权扩展 github.com/hertz-contr…
Websocket扩展 github.com/hertz-contr…
丰富的示例代码与业Demo github.com/cloudwego/h…

实战

项目地址