go三件套 | 青训营笔记

175 阅读11分钟

这是我参与「第五届青训营 」伴学笔记创作活动的第 6 天,今天主要学习的是go三件套:gorm、kitex、hertz框架等

1 gorm

gorm 是一款用 Golang 开发的 orm 框架,目前已经成为在 Golang Web 开发中最流行的 orm 框架之一。

安装gorm和mysql驱动

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

连接 MySQL

gorm 可以连接多种数据库,只需要不同的驱动即可。官方目前仅支持 MySQL、PostgreSQL、SQlite、SQL Server 四种数据库,不过可以通过自定义的方式接入其他数据库。

连接

db, err := gorm.Open(mysql.Open("root:123456@tcp(127.0.0.1:3306)/hello"))
if err != nil {
   fmt.Println(err)
}

声明模型

每一张表都会对应一个模型(结构体)。 例如现在数据库中有一张表

image.png

就会对应如下的一个模型

type User struct {
   gorm.Model
   Name         string
   Email        *string
   Age          uint
   Birthday     time.Time
   Membernumber sql.NullString
   ActivateAt   sql.NullString
}

约定大于配置

gorm 制定了很多约定,并按照约定大于配置的思想工作。

比如会根据结构体的复数寻找表名,会使用 ID 作为主键,会根据 CreateAt、UpdateAt 和 DeletedAt 表示创建时间、更新时间和删除时间。

gorm 提供了一个 Model 结构体,可以将它嵌入到自己的结构体中,省略以上几个字段。

type Model struct {
  ID        uint           `gorm:"primaryKey"`
  CreatedAt time.Time
  UpdatedAt time.Time
  DeletedAt gorm.DeletedAt `gorm:"index"`
}

例如上表的模型中,gorm.model就代表将Model这个模型嵌入user表中

自动迁移

在数据库的表尚未初始化时,gorm 可以根据指定的结构体自动建表。

通过 db.AutoMigrate  方法根据 User 结构体,自动创建 user 表。如果表已存在,该方法不会有任何动作。

db.AutoMigrate(&User{})

建表的规则会把 user 调整为复数,并自动添加 gorm.Model 中的几个字段。

image.png

插入数据

user := User{Name: "cjh", Age: 20, Birthday: time.Now()}
result := db.Create(&user)
user.id  //返回最后插入的ID
result.RowsAffected //影响的行数
result.Error        //返回的错误

向指定的列插入数据

t, err := time.ParseInLocation("2006-01-02 ", "1999-12-20 ", time.Local)
fmt.Println(t)
user := User{Name: "cjh2", Age: 23, Birthday: t}
db.Select("Name").Create(&user)

此外还可以批量插入

users := []User{
    {UserName: "lzq", Password: "aaa"},
    {UserName: "qqq", Password: "bbb"},
    {UserName: "gry", Password: "ccc"},
}
db.Create(&users)

查询数据

gorm 提供了 First、Take、Last 方法。它们都是通过 LIMIT 1  来实现的,分别是主键升序、不排序和主键降序。

user := User{}

// 获取第一条记录(主键升序)
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;
user := User{}
res := db.First(&user)
fmt.Println(user, res.RowsAffected)

根据主键查询

在 First/Take/Last 等函数中设置第二个参数,该参数被认作是 ID。可以选择 int 或 string 类型。

db.First(&user, 10)

db.First(&user, "10")
// SELECT * FROM users WHERE id IN (1,2,3);
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、Map 和 切片时,可以实现更简便的设置条件。

db.Where(&User{UserName:"lzq", Password:"aaa"}).Find(&user)
db.Where(map[string]interface{}{"user_name": "lzq", "password": "aaa"}).Find(&user)

可以选取特定字段


db.Select("name", "age").Find(&users) // SELECT name, age FROM users; 

db.Select([]string{"name", "age"}).Find(&users) // SELECT name, age FROM users;  

db.Table("users").Select("COALESCE(age,?)", 42).Rows() // SELECT COALESCE(age,'42') FROM users;

排序

db.Order("age desc, name").Find(&users)
// SELECT * FROM users ORDER BY age desc, name;

分页

db.Limit(3).Find(&users)
db.Offset(3).Find(&users)

db.Limit(2).Offset(3).Find(&users)

分组

type result struct {
  Date  time.Time
  Total int
}

db.Model(&User{}).Select("name, sum(age) as total").Where("name LIKE ?", "group%").Group("name").First(&result)
// SELECT name, sum(age) as total FROM `users` WHERE name LIKE "group%" GROUP BY `name`


db.Model(&User{}).Select("name, sum(age) as total").Group("name").Having("name = ?", "group").Find(&result)
// SELECT name, sum(age) as total FROM `users` GROUP BY `name` HAVING name = "group"

去重

result := []string{}
db.Model(&User{}).
  Distinct("user_name").
  Find(&result)

等价于

SELECT DISTINCT
	user_name
FROM
	users

更新数据

使用 Model 和 Update 方法更新单列。

可以使用结构体作为选取条件,仅选择 ID。

user.ID = 12
db.Model(&user).Update("user_name", "lzq")

等同于以下 SQL。

UPDATE `users`
SET `user_name` = 'lzq',
`updated_at` = '2020-12-04 09:16:45.263'
WHERE
	`id` = 12

也可以在 Model 中设置空结构体,使用 Where 方法自己选取条件。

db.Model(&User{}).Where("user_name", "gry").Update("user_name", "gry2")
复制代码

等同于以下 SQL。

UPDATE `users`
SET `user_name` = 'gry2',
`updated_at` = '2020-12-04 09:21:17.043'
WHERE
	`user_name` = 'gry'

更新多个字段


// 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;
user:=User{
	Name:"",
	Age:0,
	Actived:false,
}
db.Model(&user).Updates(user)
//此时不会更新这些零值,如需更新零值使用map
userMap:=map[string]interface{}{
  "name":"",
  "age":0,
  "actived":0,
}
db.Model(&user).Updates(userMap)

删除数据 Delete

硬删除 硬删除就是传统的物理删除,直接将该记录从数据库中删除。但是是人总会犯错误,在误操作删除了重要数据后,如果想要恢复该数据,需要锁表再去访问日志文件。这样会造成大量的人力资源浪费,现在的开发不推介这种方式。

软删除 软删除又叫逻辑删除,标记删除,与我们常说的删除不同,并不是真的从数据库中将这条记录去除,而是会设置一个字段,常见的有:isDelete或者state等字段来标记删除状态。当该字段为0的时候为未删除状态,为1时则是删除状态。

在现实情况中,很多时候我们说的删除并不是真的是删除的本意,因为站在用户的角度来看,并不是一种删除的状态: 订单不是被删除的,是被“取消”的。 员工不是被删除的,是被“解雇”的(也可能是退休或者暂时离职了)。 职位不是被删除的,是被“填补”的(或者招聘申请被撤回)。 所以这些时候,我们并不能真的把记录删除,所以软删除就出现了。

删除单条

使用 Delete 方法删除单条数据。但需要指定 ID,不然会批量删除。

user.ID = 20
db.Delete(&user)

等同于以下 SQL。

UPDATE `users`
SET `deleted_at` = '2020-12-04 09:45:32.389'
WHERE
	`users`.`id` = 20
	AND `users`.`deleted_at` IS NULL

设置删除条件

使用 Where 方法进行设置条件。

db.Where("user_name", "lzq").Delete(&user)

等同于以下 SQL。

UPDATE `users`
SET `deleted_at` = '2020-12-04 09:47:30.544'
WHERE
	`user_name` = 'lzq'
	AND `users`.`deleted_at` IS NULL

根据主键删除

第二个参数可以是 int、string。使用 string 时需要注意 SQL 注入。

db.Delete(&User{}, 20)

等同于以下 SQL。

UPDATE `users`
SET `deleted_at` = '2020-12-04 09:49:05.161'
WHERE
	`users`.`id` = 20
	AND `users`.`deleted_at` IS NULL

也可以使用切片 []int、[]string 进行根据 ID 批量删除。

db.Delete(&User{}, []string{"21", "22", "23"}

等同于以下 SQL。

UPDATE `users`
SET `deleted_at` = '2020-12-04 09:50:38.46'
WHERE
	`users`.`id` IN ( '21', '22', '23' )
	AND `users`.`deleted_at` IS NULL

软删除(逻辑删除)

如果结构体包含 gorm.DeletedAt 字段,会自动获取软删除的能力。

在调用所有的 Delete 方法时,会自动变为 update 语句。

UPDATE users SET deleted_at="2020-12-04 09:40" WHERE id = 31;

在查询时会自动忽略软删除的数据。

SELECT * FROM users WHERE user_name = 'gry' AND deleted_at IS NULL;

查询软删除的数据

使用 Unscoped 方法查找被软删除的数据。

db.Unscoped().Where("user_name = gry").Find(&users)

永久删除(硬删除 物理删除)

使用 Unscoped 方法永久删除数据。

user.ID = 14
db.Unscoped().Delete(&user)

原生 SQL

除了上面的封装方法外,gorm 还提供了执行原生 SQL 的能力。

执行 SQL 并将结果映射到变量上

使用 Raw 方法配合 Scan 方法。

可以查询单条数据扫描并映射到结构体或 map 上。

db.
  Raw("SELECT id, record_id, user_name, password FROM users WHERE id = ?", 25).
  Scan(&user)

也可以映射到其他类型上。

var userCount int
db.
  Raw("SELECT count(id) FROM users").
  Scan(&userCount)

如果返回结果和传入的映射变量类型不匹配,那么变量的值不会有变化。

事务

事务保证了事务一致性,但会降低一些性能。gorm 的创建、修改和删除操作都在事务中执行。

如果不需要可以在初始化时禁用事务,可以提高 30% 左右的性能。

全局关闭事务

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

会话级别关闭事务

tx := db.Session(&Session{SkipDefaultTransaction: true})
// 继续执行 SQL 时使用 tx 对象
tx.First(&user)
复制代码

在事务中执行 SQL

假设现在需要添加一个 company 表存储公司信息,并创建一个 company_users 表用于关联用户和公司的信息。

// 创建结构体
type Company struct {
	gorm.Model
	RecordID string
	Name     string
}

type CompanyUser struct {
	gorm.Model
	RecordID  string
	UserID    string
	CompanyID string
}

// 自动迁移
db.AutoMigrate(&Company{})
db.AutoMigrate(&CompanyUser{})

// 创建一家公司
company := Company{Name: "gxt"}
company.RecordID = uuid.New().String()
db.Save(&company)

// 在事务中执行
db.Transaction(func(tx *gorm.DB) error {
    // 创建用户
    u := User{UserName: "ztg", Password: "333"}
    result := tx.Create(&u)
    if err := result.Error; err != nil {
        return err
    }
    // 查询公司信息
    company2 := Company{}
    tx.First(&company2, company.ID)
    // 关联用户和公司
    result = tx.Create(&CompanyUser{UserID: u.RecordID, CompanyID: company2.RecordID})
    if err := result.Error; err != nil {
        return err
    }
    return nil
})

image.png

2 kitex

Kitex 是一个 RPC 框架,既然是 RPC,底层就需要两大功能:

  1. Serialization 序列化
  2. Transport 传输

Kitex 框架及命令行工具,默认支持 ​thrift ​和 ​proto3 ​两种 IDL,对应的 Kitex 支持 ​thrift ​和 ​protobuf ​两种序列化协议。传输上 Kitex 使用扩展的 ​thrift ​作为底层的传输协议(注:thrift 既是 IDL 格式,同时也是序列化协议和传输协议)。IDL 全称是 Interface Definition Language,接口定义语言。

 IDL

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

Thrift IDL 语法可参考:Thrift interface description language

proto3 语法可参考:Language Guide(proto3)

Kitex 命令行工具

Kitex 自带了一个同名的命令行工具 ​kitex​,用来帮助大家很方便地生成代码,新项目的生成以及之后我们会学到的 server、client 代码的生成都是通过 kitex 工具进行。

安装

可以使用以下命令来安装或者更新 kitex:

$ go install github.com/cloudwego/kitex/tool/cmd/kitex

完成后,可以通过执行 kitex 来检测是否安装成功。

$ kitex

如果出现如下输出,则安装成功。

$ kitex

No IDL file found.

如果出现首先我们需要编写一个 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)
}

生成 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

编写 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
}

编译运行

kitex 工具已经帮我们生成好了编译和运行所需的脚本:

  • 编译:

$ sh build.sh

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

  • 运行:

$ sh output/bootstrap.sh

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

报错

image.png 解决办法

go get github.com/apache/thrift@v0.13.0

下面是建立客户端通信 在刚刚的文件夹内建立client文件夹 建立main.go 代码如下:

package main

import (
   "context"
   "example/kitex_gen/api"
   "github.com/cloudwego/kitex/client/callopt"
   "log"
   "time"
)
import "example/kitex_gen/api/echo"
import "github.com/cloudwego/kitex/client"

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

得到结果

image.png

image.png

3 hertz

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

hertz使用

package main

import (
   "context"

   "github.com/cloudwego/hertz/pkg/app"
   "github.com/cloudwego/hertz/pkg/app/server"
   "github.com/cloudwego/hertz/pkg/protocol/consts"
)

func main() {
   h := server.Default()

   h.GET("/ping", func(c context.Context, ctx *app.RequestContext) {
      ctx.String(consts.StatusOK, "get")
   })
   h.POST("/poss", func(c context.Context, ctx *app.RequestContext) {
      ctx.String(consts.StatusOK, "post")
   })

   h.Spin()
}

image.png

除此之外还支持

路由组

v1 := h.Group("/v1")
v1.GET("/get", func(c context.Context, ctx *app.RequestContext) {
   ctx.String(consts.StatusOK, "get")
})
v1.POST("/post", func(c context.Context, ctx *app.RequestContext) {
   ctx.String(consts.StatusOK, "post")
})

路由优先级

使用文档 www.cloudwego.io/zh/docs/her…

image.png

绑定与校验

先定义一个结构体

type Args struct {
   Query      string   `query:"query"`
   QuerySlice []string `query:"q"`
   Path       string   `path:"path"`
   Header     string   `header:"header"`
   Form       string   `form:"form"`//后面的名称代表请求的参数
   Json       string   `json:"json1"`
   Vd         int      `query:"vd" vd:"$==0 || $==1"`
}
h.POST("/poss", func(c context.Context, ctx *app.RequestContext) {
   var args Args
   err := ctx.BindAndValidate(&args)
   if err != nil {
      panic(err)
   }
   fmt.Println(args)
   ctx.String(consts.StatusOK, args.Form+"444")
})

hz

hertz提供了一个脚手架hz。可以帮助我们直接生成代码。 image.png image.png

image.png image.png