day05 Go框架三件套| 青训营笔记

74 阅读6分钟

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

Gorm ORM框架

什么是ORM框架

ORM 全称Object–relational mapping,即对象关系映射,是一种用于在关系数据库和面向对象的编程语言堆之间转换数据的编程技术。通过 ORM 技术,我们可以将关系数据库中某个数据表的结构关联到某个类/结构体上,并通过修改类/结构体实例的方式轻易的完成数据库增删改查(CRUD)的任务。 ORM框架 天然阻止了sql注入

Gorm 的使用

GORM 通过驱动来连接数据库,如果需要连接其他类型的数据库,可以复用/自行开发驱动 通过以下命令 Go Module 拉取并添加 Gorm 及 MySQL 数据库驱动:

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

DB的基础操作

建表 结构体示例

type Product struct {
    Code      string
    Price     uint
    ID        uint `gorm:"primarykey"`
    CreatedAt time.Time
    UpdatedAt time.Time
    DeletedAt sql.NullTime `gorm:"index"`
}

Go的约定(默认) Gorm 使用名为ID的字段 作为主键

使用结构体的蛇形复数作为表名 即小写加上下划线加复数:my_times

字段名的蛇形作为列名

(默认创建)使用CreateAt,UpdatedAt 字段作为创建,更新时间 示例

package main

import (
  "gorm.io/gorm"
  "gorm.io/driver/mysql"
)

type Product struct {
  gorm.Model
  Code  string
  Price uint
}

func main() {
    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{})
  if err != nil {
    panic("failed to connect database")
  }

  // 迁移 schema
  db.AutoMigrate(&Product{})

  // Create
  db.Create(&Product{Code: "D42", Price: 100})

  // Read
  var product Product
  db.First(&product, 1) // 根据整型主键查找
  db.First(&product, "code = ?", "D42") // 查找 code 字段值为 D42 的记录

  // Update - 将 product 的 price 更新为 200
  db.Model(&product).Update("Price", 200)
  // Update - 更新多个字段
  db.Model(&product).Updates(Product{Price: 200, Code: "F42"}) // 仅更新非零值字段
  db.Model(&product).Updates(map[string]interface{}{"Price": 200, "Code": "F42"})

  // Delete - 删除 product
  db.Delete(&product, 1)
}

DSN

    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{})
  if err != nil {
    panic("failed to connect database")
  }

调用 gorm.Open 开启了一个数据库连接,并为可能产生的异常进行处理。 我们通过 mysql.Open 函数并传入了一个看似奇怪的字符串为 GORM 指定了数据库信息,这个字符串被称为 DSN(data source name,数据源名称),其中包含有关 ODBC(Open Database Connectivity,开放式数据库连接,一种用于访问数据库管理系统的 API) 驱动程序需要连接到的特定数据库的信息

&gorm.Config{} 为 GORM 启用默认的配置,当然,你也可以指定自己的配置,比如通过传入:

&gorm.Config{
    PrepareStmt: true
}

启用预编译语句缓存以提高性能。

  // 迁移 schema
  db.AutoMigrate(&Product{})
复制代码

为指定数据库自动迁移数据模型结构。这会为指定数据模型创建 GORM 可用的数据表结构。这一步是可选的,即使不迁移 schema,数据表也会在创建新记录的时候被创建。

Gorm Create数据

如何使用Upsert 使用clause.OnConflict 处理数据冲突

如何使用默认值 通过使用default 标签为字段定义默认值

  // Create
  db.Create(&Product{Code: "D42", Price: 100})

Gorm 查询数据

First的使用踩坑

  • 使用First时,需要注意查询不到数据会返回ErrRecordNotFound
  • 使用Find 查询多条数据,查询不到数据不会返回错误 使用结构体作为查询条件 当使用结构作为条件查询时,GORM只会查询非零值字段。这意味着如果您的字段值为0,“false或其他零值”,该字段不会被用于构建查询条件,使用Map来构建条件或者where条件子句构建
// Get first matched record
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';

可以通过结构体和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);

需要注意的是,当使用结构体作为查询条件时,只会查询结构体内的非零值字段,这意味着字段值为 0''false 或其他零值的字段不会被用于构建查询条件。

  // Update - 将 product 的 price 更新为 200
  db.Model(&product).Update("Price", 200)
  // Update - 更新多个字段
  db.Model(&product).Updates(Product{Price: 200, Code: "F42"}) // 仅更新非零值字段
  db.Model(&product).Updates(map[string]interface{}{"Price": 200, "Code": "F42"})

Gorm 更新数据

使用Struct 更新时,只会更新非零值,如果需要更新零值可以使用Map更新或使用Select 选择字段

  // Update - 将 product 的 price 更新为 200
  db.Model(&product).Update("Price", 200)
  // Update - 更新多个字段
  db.Model(&product).Updates(Product{Price: 200, Code: "F42"}) // 仅更新非零值字段
  db.Model(&product).Updates(map[string]interface{}{"Price": 200, "Code": "F42"})

Gorm 删除数据

物理删除 软删除 GORM 提供了 gom.DeletedAt 用于帮助用户实现软删 拥有软删除能力的Model 调用Delete 时,记录不会被从数据库中真正删除。但GORM会将DeletedAt 置为当前时间,并且你不能再通过正常的查询方法找到该记录 使用Unscoped 可以查询到被软删的数据

db.Unscoped().Where("age = 20").Find(&users)
// SELECT * FROM users WHERE age = 20;

GORM 事务

GORM 提供了Begin,Commit,Rollback 方法用于使用事务

// 开始事务
tx := db.Begin()
​
// 在事务中执行一些 db 操作(从这里开始,您应该使用 'tx' 而不是 'db')
tx.Create(...)
​
// ...// 遇到错误时回滚事务
tx.Rollback()
​
// 否则,提交事务
tx.Commit()

Gorm 提供了 Transaction 方法用于自动提交事务,避免用户漏写Commit,Rollback

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 Hook

Hook 是在创建,查询,更新,删除等操作之前,之后自动调用的函数 如果任何Hook返回错误,GORM将停止后续的操作并回滚事务

type User struct {
    ID      int64
    Name    string `gorm:"default:galeone"`
    Age     string `gorm:"default:18"`
}
​
type Email struct {
    ID      int64
    Name    string
    Email   string
}
​
func (u *User) BeforeCreate(tx `gorm.DB) (err error) {
    if u.Age < 0 {
        return errors.New("can't save invalid data")
    }
    return nil;
}
​
func (u *User) AfterCreate(tx `gorm.DB) (err error) {
    return tx.Create(&Email{ID: u.ID, Email: i.Name + "@***.com"}).Error
}

gorm 性能提高

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

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

经验

一般线上不用 联表查询。先把表数据拿到 再拼接 最好不要在for循环里创建数据

kitex RPC框架

什么是 RPC

RPC(Remote procedure call,远程过程调用)是指计算机程序导致过程(子例程)在不同的地址空间(通常在共享网络上的另一台计算机上)中执行,其编码就像普通(本地)过程调用一样,程序员没有显式编码远程交互的详细信息。也就是说,程序员编写的代码基本相同,无论子例程是执行程序的本地还是远程的。 简单来说,通过使用 RPC,我们可以像调用方法一样快捷的与远程服务进行交互

安装

要开始 Kitex 开发,首先需要安装 Kitex 代码生成工具, go install 命令可被用于安装 Go 二进制工具(在此之前,请务必检查已正确设置 GOPATH 环境变量,并将 $GOPATH/bin 添加到 PATH 环境变量中):

go install github.com/cloudwego/kitex/tool/cmd/kitex@latest
go install github.com/cloudwego/thriftgo@latest

使用IDL定义服务与接口

如果我们要进行RPC,就需要知道对方的接口是什么,需要传什么参数,同时也需要知道返回值是什么样的。这时候,就需要通过IDL来约定双方的协议,就像在写代码的时候需要调用某个函数,我们需要知道函数签名一样

接口描述语言(Interface definition language,IDL) 是一种语言的通用术语,它允许用一种语言编写的程序或对象与用未知语言编写的另一个程序进行通信。 我们可以使用 IDL 来支持 RPC 的信息传输定义。Kitex 默认支持 thriftproto3 两种 IDL,而在底层传输上,Kitex 使用扩展的 thrift 作为底层的传输协议。 Thrift: thrift.apache.org/docs/idl Proto3:developers.google.com/protocol-bu…

Kitex 生成代码

使用kitex -module example -service example echo.thrift 命令生成代码 build.sh : 构建脚本 kitex_gen:IDL内容相关的生成代码,主要是基础的Server/Client 代码 main.go 程序入口 handler.go 用户在该文件里实现IDL service 定义的方法

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

handler.go内容如下:

package main
​
import (
        "context"
        api "exmaple/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
}

server 端 服务默认监听 8888 端口 修改 handler.go 内的 Echo 函数为下述代码以实现我们的 Echo 服务逻辑:

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

运行 sh build.sh 以进行编译,编译结果会被生成至 output 目录.

最后,运行 sh output/bootstrap.sh 以启动服务。服务会在默认的 8888 端口上开始运行。要想修改运行端口,可打开 main.go,为 NewServer 函数指定配置参数:

 addr, _ := net.ResolveTCPAddr("tcp", "127.0.0.1:9999")
  svr := api.NewServer(new(EchoImpl), server.WithServiceAddr(addr))

重新编译

Kitex 客户端

上例中,我们使用 Kitex 创建了一个回声服务端,接下来,我们通过创建一个客户端来调用我们的回声服务。以下项目代码假设您已正确导入上文中生成的回声服务代码。新建项目并创建 main.go 文件,编写代码:

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 用于指定服务端的地址,更多参数可参考基本特性

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

在编写完一个简单的客户端后,我们终于可以发起调用了。

你可以通过下述命令来完成这一步骤:

$ go run main.go

Kitex 服务注册与发现

目前Kitex 的服务注册与发现已经对接了主流服务注册与发现中心,如ETCD,Nacos等 以下代码使用 DNS Resolver 进行服务发现:

import (
    ...
    dns "github.com/kitex-contrib/resolver-dns"
    "github.com/cloudwego/kitex/client"
    ...
)
​
func main() {
    ...
    client, err := echo.NewClient("echo", client.WithResolver(dns.NewDNSResolver()))
    if err != nil {
        log.Fatal(err)
    }
    ...
}

Hertz HTTP框架

Hertz 提供了GET,POST,PUT,DELETE,ANY等方法用于注册路由 Hertz 提供了路由组(Group)的能力,用于支持路由分组的功能 Hertz提供了参数路由和通配路由,路由的优先级为:静态路由>命名路由>通配路由 Hertz提供了Bind,Validate,BindAndValidate 函数用于进行参数绑定和校验 其中,Any 用于注册所有 HTTP Method 方法;Hertz.StaticFile/Static/StaticFS 用于注册静态文件;Handle 可用于注册自定义 HTTP Method 方法。 Hertz的中间件主要分为客户端中间件与服务端中间件,如下展示一个服务端中间件

安装

命令

go install github.com/cloudwego/hertz/cmd/hz@latest

hz 也可被用于为指定 IDL 生成服务代码。

使用 hz new 生成代码,然后使用 go mod tidy 拉取依赖

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(server.WithHostPoerts("127.0.0.1:8080"))
​
    h.GET("/ping", func(c context.Context, ctx *app.RequestContext) {
            ctx.JSON(consts.StatusOK, utils.H{"message": "pong"})
    })
​
    h.Spin()
}

创建了一个 HTTP 服务端,监听 8080 端口并注册了一个 GET 方法的路由函数(/ping)。

实战项目

笔记项目是一个使用Hertz, Kitex, Gorm搭建出来的具备一定业务逻辑的后端API项目 github.com/cloudwego/b…

引用