这是我参与「第五届青训营 」伴学笔记创作活动的第 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)
}
声明模型
每一张表都会对应一个模型(结构体)。 例如现在数据库中有一张表
就会对应如下的一个模型
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 中的几个字段。
插入数据
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
})
2 kitex
Kitex 是一个 RPC 框架,既然是 RPC,底层就需要两大功能:
- Serialization 序列化
- 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 服务就开始运行啦!
报错
解决办法
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)
}
得到结果
3 hertz
Hertz[həːts] 是一个 Golang 微服务 HTTP 框架,在设计之初参考了其他开源框架 fasthttp、gin、echo 的优势, 并结合字节跳动内部的需求,使其具有高易用性、高性能、高扩展性等特点,目前在字节跳动内部已广泛使用。 如今越来越多的微服务选择使用 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()
}
除此之外还支持
路由组
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…
绑定与校验
先定义一个结构体
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。可以帮助我们直接生成代码。