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。可以帮助我们直接生成代码。