GO语言基础入门-web三件套详解 | 青训营笔记

221 阅读20分钟

前言

这是我参与「第五届青训营 」伴学笔记创作活动的第 4 天,其实第五届青训营已经正式开课n多天了,笔记不断更新中,都是我听完课之后的总结和平时自己的学习积累,分享出来给有需要的朋友。

本文内容

本文将涉及到Go语言框架三件套详解(Web/RPC/ORM),包括Gorm,Kitex,Hertz的基本用法与常见API的讲解。

一、Go语言基础

1.Gorm框架
(1)特性
  • 全功能 ORM
  • 关联 (Has One,Has Many,Belongs To,Many To Many,多态,单表继承)
  • Create,Save,Update,Delete,Find 中钩子方法
  • 支持 PreloadJoins 的预加载
  • 事务,嵌套事务,Save Point,Rollback To Saved Point
  • Context、预编译模式、DryRun 模式
  • 批量插入,FindInBatches,Find/Create with Map,使用 SQL 表达式、Context Valuer 进行 CRUD
  • SQL 构建器,Upsert,数据库锁,Optimizer/Index/Comment Hint,命名参数,子查询
  • 复合主键,索引,约束
  • Auto Migration
  • 自定义 Logger
  • 灵活的可扩展插件 API:Database Resolver(多数据库,读写分离)、Prometheus…
  • 每个特性都经过了测试的重重考验
  • 开发者友好
(2)Gorm的安装
go get -u gorm.io/gormgo 
(3)快速入门
get -u gorm.io/driver/sqlite # 使用sqlite,先安装
package main
​
import (
  "gorm.io/gorm"
  "gorm.io/driver/sqlite"
  "fmt"
)
​
type Product struct {
  gorm.Model
  Code  string
  Price uint
}
​
func main() {
  db, err := gorm.Open(sqlite.Open("test.db"), &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 的记录
  fmt.Println(product.Price)
  
  // 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)
}
(4)模型定义

模型是标准的 struct,由 Go 的基本数据类型、实现了 ScannerValuer 接口的自定义类型及其指针或别名组成

例如:

type User struct {
  ID           uint
  Name         string
  Email        *string
  Age          uint8
  Birthday     *time.Time
  MemberNumber sql.NullString
  ActivatedAt  sql.NullTime
  CreatedAt    time.Time
  UpdatedAt    time.Time
}
(5)约定

GORM 倾向于约定优于配置 ,默认情况下,GORM 使用 ID 作为主键,使用结构体名的 蛇形复数 作为表名,字段名的 蛇形 作为列名,并使用 CreatedAtUpdatedAt 字段追踪创建、更新时间

如果您遵循 GORM 的约定,您就可以少写的配置、代码。 如果约定不符合您的实际要求,GORM 允许你配置它们

GORM 定义一个 gorm.Model 结构体,其包括字段 IDCreatedAtUpdatedAtDeletedAt

// gorm.Model 的定义
type Model struct {
  ID        uint           `gorm:"primaryKey"`  //定义主键
  CreatedAt time.Time
  UpdatedAt time.Time
  DeletedAt gorm.DeletedAt `gorm:"index"`
}

您可以将它嵌入到您的结构体中,以包含这几个字段,详情请参考 嵌入结构体

当然你也可以自己定义表名,加上如下代码

type Product struct {
    ID unit `gorm:"primarykey"`
    Code string `gorm:"column:code"`  // 定义列名
    Name string `gorm:"defult:hmily"`  //定义默认值
    Age int64 `gorm:"default:18"`
    Price unit `gorm:"column: user_id"`  //定义列名
}
func (p Product) TableName() string{
    return "你要设置的表名"
}
func main(){
}
(6)连接数据库

GORM 官方支持的数据库类型有: MySQL, PostgreSQL, SQlite, SQL Server

# 使用前先安装
go get -u gorm.io/driver/sqlite
go get -u gorm.io/driver/mysql
go get -u gorm.io/driver/sqlserver
go get -u gorm.io/driver/postgres

1.使用MySQL

import (  
    "gorm.io/driver/mysql"
    "gorm.io/gorm"
)
​
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{})
}

注意: 想要正确的处理 time.Time ,您需要带上 parseTime 参数, (更多参数) 要支持完整的 UTF-8 编码,您需要将 charset=utf8 更改为 charset=utf8mb4 查看 此文章 获取详情。

MySQL 驱动程序提供了 一些高级配置可以在初始化过程中使用,具体请看使用文档

2.使用sqlite

package main
​
import (
  "gorm.io/gorm"
  "gorm.io/driver/sqlite"
)
func main() {
  db, err := gorm.Open(sqlite.Open("test.db"), &gorm.Config{})
  if err != nil {
    panic("failed to connect database")
  }
}

注意: 您也可以使用 file::memory:?cache=shared 替代文件路径。 这会告诉 SQLite 在系统内存中使用一个临时数据库。 (查看 SQLite 文档 获取详情)

3.使用SqlServer

import (
  "gorm.io/driver/sqlserver"
  "gorm.io/gorm"
)
​
// github.com/denisenkom/go-mssqldb
dsn := "sqlserver://gorm:LoremIpsum86@localhost:9930?database=gorm"
db, err := gorm.Open(sqlserver.Open(dsn), &gorm.Config{})

4.使用PostgreSQL

import (  "gorm.io/driver/postgres"  "gorm.io/gorm")dsn := "host=localhost user=gorm password=gorm dbname=gorm port=9920 sslmode=disable TimeZone=Asia/Shanghai"db, err := gorm.Open(postgres.Open(dsn), &gorm.Config{})

我们使用 pgx 作为 postgres 的 database/sql 驱动,默认情况下,它会启用 prepared statement 缓存,你可以这样禁用它:

// https://github.com/go-gorm/postgres
db, err := gorm.Open(postgres.New(postgres.Config{
  DSN: "user=gorm password=gorm dbname=gorm port=9920 sslmode=disable TimeZone=Asia/Shanghai",
  PreferSimpleProtocol: true, // disables implicit prepared statement usage
}), &gorm.Config{})

GORM 使用 database/sql 维护连接池

sqlDB, err := db.DB()
​
// SetMaxIdleConns 设置空闲连接池中连接的最大数量
sqlDB.SetMaxIdleConns(10)
​
// SetMaxOpenConns 设置打开数据库连接的最大数量。
sqlDB.SetMaxOpenConns(100)
​
// SetConnMaxLifetime 设置了连接可复用的最大时间。
sqlDB.SetConnMaxLifetime(time.Hour)

查看 通用接口 获取详情。

(7)创建数据
user := User{Name: "Jinzhu", Age: 18, Birthday: time.Now()}
​
result := db.Create(&user) // 通过数据的指针来创建
​
fmt.Println(user.ID)        // 返回插入数据的主键
fmt.Println(result.Error)   // 返回 error
fmt.Println(result.RowsAffected)     // 返回插入记录的条数

创建记录并更新给出的字段。

db.Select("Name", "Age", "CreatedAt").Create(&user)
// INSERT INTO `users` (`name`,`age`,`created_at`) VALUES ("jinzhu", 18, "2020-07-04 11:05:21.775")

创建一个记录且一同忽略传递给略去的字段值。

db.Omit("Name", "Age", "CreatedAt").Create(&user)
// INSERT INTO `users` (`birthday`,`updated_at`) VALUES ("2020-01-01 00:00:00.000", "2020-07-04 11:05:21.775")

批量创建

要有效地插入大量记录,请将一个 slice 传递给 Create 方法。 GORM 将生成单独一条SQL语句来插入所有数据,并回填主键的值,钩子方法也会被调用。

var users = []User{{Name: "jinzhu1"}, {Name: "jinzhu2"}, {Name: "jinzhu3"}}
db.Create(&users)
​
for _, user := range users {
  user.ID // 1,2,3
}

使用 CreateInBatches 分批创建时,你可以指定每批的数量,例如:

var users = []User{{name: "jinzhu_1"}, ...., {Name: "jinzhu_10000"}}
​
// 数量为 100
db.CreateInBatches(users, 100)

UpsertCreate With Associations 也支持批量插入

根据map创建

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

db.Model(&User{}).Create(map[string]interface{}{
  "Name": "jinzhu", "Age": 18,
})
​
// batch insert from `[]map[string]interface{}{}`
db.Model(&User{}).Create([]map[string]interface{}{
  {"Name": "jinzhu_1", "Age": 18},
  {"Name": "jinzhu_2", "Age": 20},
})

注意: 根据 map 创建记录时,association 不会被调用,且主键也不会自动填充

默认值

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

type User struct {
  ID   int64
  Name string `gorm:"default:galeone"`
  Age  int64  `gorm:"default:18"`
}

插入记录到数据库时,默认值 会被用于 填充值为 零值 的字段

创建钩子

GORM 允许用户定义的钩子有 BeforeSave, BeforeCreate, AfterSave, AfterCreate 创建记录时将调用这些钩子方法,请参考 Hooks 中关于生命周期的详细信息

func (u *User) BeforeCreate(tx *gorm.DB) (err error) {
  u.UUID = uuid.New()
​
    if u.Role == "admin" {
        return errors.New("invalid role")
    }
    return
}

如果您想跳过 钩子 方法,您可以使用 SkipHooks 会话模式,例如:

DB.Session(&gorm.Session{SkipHooks: true}).Create(&user)
​
DB.Session(&gorm.Session{SkipHooks: true}).Create(&users)
​
DB.Session(&gorm.Session{SkipHooks: true}).CreateInBatches(users, 100)
(8)查询数据

GORM 提供了 FirstTakeLast 方法,以便从数据库中检索单个对象。当查询数据库时它添加了 LIMIT 1 条件,且没有找到记录时,它会返回 ErrRecordNotFound 错误

// 获取第一条记录(主键升序)
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;
​
result := db.First(&user)
result.RowsAffected // 返回找到的记录数
result.Error        // returns error or nil
​
// 检查 ErrRecordNotFound 错误
errors.Is(result.Error, gorm.ErrRecordNotFound)

如果你想避免ErrRecordNotFound错误,你可以使用Find,比如db.Limit(1).Find(&user)Find方法可以接受struct和slice的数据。

First和Last方法将按照主键的顺序(分别)查找第一条和最后一条记录。只有当指向目标结构的指针作为参数传递给方法时,或者当使用db指定模型时,它们才起作用。使用db.Model() 。此外,如果没有为相关模型定义主键,则模型将按第一个字段排序。例如

var user User
var users []User
​
db.First(&user)
// SELECT * FROM `users` ORDER BY `users`.`id` LIMIT 1// 使用 `db.Model()`
result := map[string]interface{}{}
db.Model(&User{}).First(&result)
// SELECT * FROM `users` ORDER BY `users`.`id` LIMIT 1// 如果没有定义主键,结果将按照第一个字段进行排序 (i.e., `Code`)
type Language struct {
  Code string
  Name string
}
db.First(&Language{})
// SELECT * FROM `languages` ORDER BY `languages`.`code` LIMIT 1

根据主键检索

db.First(&user, 10)// SELECT * FROM users WHERE id = 10;
db.First(&user, "10")// SELECT * FROM users WHERE id = 10;
db.Find(&users, []int{1,2,3})// SELECT * FROM users WHERE id IN (1,2,3);

如果主键是字符串,则按照下面的写法

db.First(&user, "id = ?", "1b74413f-f3b8-409f")
// SELECT * FROM users WHERE id = "1b74413f-f3b8-409f";

当目标对象有一个主键值时,将使用主键构建查询条件,例如:

var user = User{ID: 10}
db.First(&user)
// SELECT * FROM users WHERE id = 10;
​
var result User
db.Model(User{ID: 10}).First(&result)
// SELECT * FROM users WHERE id = 10;

检索全部对象

// 获取所有记录
result := db.Find(&users)
// SELECT * FROM users;
​
result.RowsAffected // returns found records count, equals `len(users)`
result.Error        // returns error

String 条件

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

当使用结构体查询时,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;

使用struct进行搜索时,可以通过将相关的字段名或dbname传递给Where()来指定在查询条件中使用struct中的哪些特定值,例如

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

查询条件可以内联到First和Find之类的方法中,方法与Where类似。

// 字符串作为主键
First(&user, "id = ?", "string_primary_key")
// SELECT * FROM users WHERE id = 'string_primary_key';
​
// 使用find查询
db.Find(&user, "name = ?", "jinzhu")
// SELECT * FROM users WHERE name = "jinzhu";
​
db.Find(&users, "name <> ? AND age > ?", "jinzhu", 20)
// SELECT * FROM users WHERE name <> "jinzhu" AND age > 20;
​
// Struct
db.Find(&users, User{Age: 20})// SELECT * FROM users WHERE age = 20;
​
// Map
db.Find(&users, map[string]interface{}{"age": 20})
// SELECT * FROM users WHERE age = 20;

使用NOT 条件可以查询不存在表中的数据

db.Not("name = ?", "jinzhu").First(&user)
// SELECT * FROM users WHERE NOT name = "jinzhu" ORDER BY id LIMIT 1;
​
// Not In
db.Not(map[string]interface{}{"name": []string{"jinzhu", "jinzhu 2"}}).Find(&users)
// SELECT * FROM users WHERE name NOT IN ("jinzhu", "jinzhu 2");
​
// Struct
db.Not(User{Name: "jinzhu", Age: 18}).First(&user)
// SELECT * FROM users WHERE name <> "jinzhu" AND age <> 18 ORDER BY id LIMIT 1;
​
// Not In slice of primary keys
db.Not([]int64{1,2,3}).First(&user)
// SELECT * FROM users WHERE id NOT IN (1,2,3) ORDER BY id LIMIT 1;
​

使用OR条件

db.Where("role = ?", "admin").Or("role = ?", "super_admin").Find(&users)
// SELECT * FROM users WHERE role = 'admin' OR role = 'super_admin';
​
// Struct
db.Where("name = 'jinzhu'").Or(User{Name: "jinzhu 2", Age: 18}).Find(&users)
// SELECT * FROM users WHERE name = 'jinzhu' OR (name = 'jinzhu 2' AND age = 18);
​
// Map
db.Where("name = 'jinzhu'").Or(map[string]interface{}{"name": "jinzhu 2", "age": 18}).Find(&users)
// SELECT * FROM users WHERE name = 'jinzhu' OR (name = 'jinzhu 2' AND age = 18);

使用Select 允许您指定要从数据库中检索的字段。否则,GORM将默认选择所有字段

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.Order("age desc").Order("name").Find(&users)
// SELECT * FROM users ORDER BY age desc, name;

Limit & Offset

db.Limit(3).Find(&users)
// SELECT * FROM users LIMIT 3;// Cancel limit condition with -1
db.Limit(10).Find(&users1).Limit(-1).Find(&users2)
// SELECT * FROM users LIMIT 10; (users1)
// SELECT * FROM users; (users2)
​
db.Offset(3).Find(&users)
// SELECT * FROM users OFFSET 3;
​
db.Limit(10).Offset(5).Find(&users)
// SELECT * FROM users OFFSET 5 LIMIT 10;
// Cancel offset condition with -1
​
db.Offset(10).Find(&users1).Offset(-1).Find(&users2)
// SELECT * FROM users OFFSET 10; (users1)
// SELECT * FROM users; (users2)

更多其他查询操作参考Gorm查询操作高级查询

(9)更新数据

更新单个列

当使用 Update 更新单列时,需要有一些条件,否则将会引起错误 ErrMissingWhereClause ,查看 阻止全局更新 了解详情。 当使用 Model 方法,并且值中有主键值时,主键将会被用于构建条件,例如:

// 条件更新
db.Model(&User{}).Where("active = ?", true).Update("name", "hello")
// UPDATE users SET name='hello', updated_at='2013-11-17 21:34:10' WHERE active=true;
​
// User 的 ID 是 `111`
db.Model(&user).Update("name", "hello")
// UPDATE users SET name='hello', updated_at='2013-11-17 21:34:10' WHERE id=111;
​
// 根据条件和 model 的值进行更新
db.Model(&user).Where("active = ?", true).Update("name", "hello")
// UPDATE users SET name='hello', updated_at='2013-11-17 21:34:10' WHERE id=111 AND active=true;

更新多列

Updates 方法支持 structmap[string]interface{} 参数。当使用 struct 更新时,默认情况下,GORM 只会更新非零值的字段

// 根据 `struct` 更新属性,只会更新非零值的字段
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;
​
// 根据 `map` 更新属性
db.Model(&user).Updates(map[string]interface{}{"name": "hello", "age": 18, "active": false})
// UPDATE users SET name='hello', age=18, active=false, updated_at='2013-11-17 21:34:10' WHERE id=111;

注意 当使用 struct 进行更新时,GORM 只会更新非零值的字段。 你可以使用 map 更新字段,或者使用 Select 指定要更新的字段

更新选定字段

如果您想要在更新时选定、忽略某些字段,您可以使用 SelectOmit

// Select with Map
// User's ID is `111`:
db.Model(&user).Select("name").Updates(map[string]interface{}{"name": "hello", "age": 18, "active": false})
// UPDATE users SET name='hello' WHERE id=111;
​
db.Model(&user).Omit("name").Updates(map[string]interface{}{"name": "hello", "age": 18, "active": false})
// UPDATE users SET age=18, active=false, updated_at='2013-11-17 21:34:10' WHERE id=111;// Select with Struct (select zero value fields)
db.Model(&user).Select("Name", "Age").Updates(User{Name: "new_name", Age: 0})
// UPDATE users SET name='new_name', age=0 WHERE id=111;// Select all fields (select all fields include zero value fields)
db.Model(&user).Select("*").Updates(User{Name: "jinzhu", Role: "admin", Age: 0})
​
// Select all fields but omit Role (select all fields include zero value fields)
db.Model(&user).Select("*").Omit("Role").Updates(User{Name: "jinzhu", Role: "admin", Age: 0})

更新 Hook

GORM 支持的 hook 点包括:BeforeSave, BeforeUpdate, AfterSave, AfterUpdate. 更新记录时将调用这些方法,查看 Hooks 获取详细信息

func (u *User) BeforeUpdate(tx *gorm.DB) (err error) {
    if u.Role == "admin" {
        return errors.New("admin user not allowed to update")
    }
    return
}

更新的记录数

获取受更新影响的行数

// 通过 `RowsAffected` 得到更新的记录数
result := db.Model(User{}).Where("role = ?", "admin").Updates(User{Name: "hello", Age: 18})
​
// UPDATE users SET name='hello', age=18 WHERE role = 'admin';
result.RowsAffected // 更新的记录数
result.Error        // 更新的错误

更多更新操作参考更新高级选项

(10)删除数据

删除分为软删除和物理删除,当使用物理删除时,数据会真正的从数据库中删除。

接下来先介绍物理删除的相关操作

删除一条记录

删除一条记录时,删除对象需要指定主键,否则会触发 批量 Delete,例如:

// 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 允许通过主键(可以是复合主键)和内联条件来删除对象,它可以使用数字。查看 查询-内联条件(Query Inline Conditions) 了解详情。

db.Delete(&User{}, 10)
// DELETE FROM users WHERE id = 10;
​
db.Delete(&User{}, "10")
// DELETE FROM users WHERE id = 10;
​
db.Delete(&users, []int{1,2,3})
// DELETE FROM users WHERE id IN (1,2,3);

Delete Hook

对于删除操作,GORM 支持 BeforeDeleteAfterDelete Hook,在删除记录时会调用这些方法,查看 Hook 获取详情

func (u *User) BeforeDelete(tx *gorm.DB) (err error) {
    if u.Role == "admin" {
        return errors.New("admin user not allowed to delete")
    }
    return
}

批量删除

如果指定的值不包括主属性,那么 GORM 会执行批量删除,它将删除所有匹配的记录

db.Where("email LIKE ?", "%jinzhu%").Delete(&Email{})
// DELETE from emails where email LIKE "%jinzhu%";
​
db.Delete(&Email{}, "email LIKE ?", "%jinzhu%")
// DELETE from emails where email LIKE "%jinzhu%";

软删除

如果您的模型包含了一个 gorm.deletedat 字段(gorm.Model 已经包含了该字段),它将自动获得软删除的能力!

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

// user 的 ID 是 `111`
db.Delete(&user)
// UPDATE users SET deleted_at="2013-10-29 10:23" WHERE id = 111;
​
// 批量删除
db.Where("age = ?", 20).Delete(&User{})
// UPDATE users SET deleted_at="2013-10-29 10:23" WHERE age = 20;
​
// 在查询时会忽略被软删除的记录
db.Where("age = 20").Find(&user)
// SELECT * FROM users WHERE age = 20 AND deleted_at IS NULL;

如果您不想引入 gorm.Model,您也可以这样启用软删除特性:

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

查找被软删除的记录

您可以使用 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;

Delete Flag

默认情况下,gorm.Model 使用 *time.Time 作为 DeletedAt 字段的值。此外,通过 gorm.io/plugin/soft_delete 插件还支持其它数据格式。

更多详细可以参考gorm.io/zh_CN/docs/…

(11)GORM事务

1.禁用默认事务

为了确保数据一致性,GORM 会在事务里执行写入操作(创建、更新、删除)。如果没有这方面的要求,您可以在初始化时禁用它,这将获得大约 30%+ 性能提升。

// 全局禁用
db, err := gorm.Open(sqlite.Open("gorm.db"), &gorm.Config{
  SkipDefaultTransaction: true,
})
​
// 持续会话模式
tx := db.Session(&Session{SkipDefaultTransaction: true})
tx.First(&user, 1)
tx.Find(&users)
tx.Model(&user).Update("Age", 18)

2.使用事务

(1)手动事务

Gorm 支持直接调用事务控制方法(commit、rollback),例如:

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

例子:

dsn := "root:123456@tcp(127.0.0.1:3306)/golang?charset=utf8mb4&parseTime=True&loc=Local"
db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{})
if err != nil {
    panic("failed to connect database")
}
tx:= db.Begin()  //开始事务
// 在事务中执行一些db操作 
if err = tx.Create(&User{Name:"name"}).Error;err != nil {
    tx.Rollback()
    // 遇到错误时回滚事务
    return
}
if err = tx.Create(&User{Name:"name1"}).Error;err != nil {
    tx.Rollback()
    return
}
​
//提交事务
tx.Commit()

(2)使用Transaction事务

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

dsn := "root:123456@tcp(127.0.0.1:3306)/golang?charset=utf8mb4&parseTime=True&loc=Local"
db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{})
if err != nil {
    panic("failed to connect database")
}
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
})
(12)Gorm生态

Gorm拥有非常丰富的扩展生态,以下列举一部分常用扩展。

Gorm代码生成工具 : https://github.com/go-gorm/gen
Gorm分片库方案 : https://github.com/go-gorm/sharding
Gorm手动索引 : https://github.com/go-gorm/hints
Gorm乐观锁 : https://github.com/go-gorm/optimisticlock
Gorm 读写分离 : https://github.com/go-gorm/adresolver
Gorm OpenTelemetry扩展 : https://github.com/go-gorm/opentelemetry
​
Gorm官方文档 : https://gorm.cn
2.Kitex框架

Kitex[kaɪt’eks] 字节跳动内部的 Golang 微服务 RPC 框架,具有高性能强可扩展的特点,在字节内部已广泛使用。如果对微服务性能有要求,又希望定制扩展融入自己的治理体系,Kitex 会是一个不错的选择。

(1)准备环境

1.kitex 暂时没有针对 Windows 做支持,如果本地开发环境是 Windows 建议使用 WSL2

2.需要Golang开发环境,如果没有搭建的可以先去下载安装

3.推荐使用最新版本的 Golang,我们保证最新三个正式版本的兼容性(现在 >= v1.16)。

(2)代码生成工具

首先,我们需要安装使用本示例所需要的命令行代码生成工具:

  1. 确保 GOPATH 环境变量已经被正确地定义(例如 export GOPATH=~/go)并且将$GOPATH/bin添加到 PATH 环境变量之中(例如 export PATH=$GOPATH/bin:$PATH);请勿将 GOPATH 设置为当前用户没有读写权限的目录
  2. 安装 kitex:go install github.com/cloudwego/kitex/tool/cmd/kitex@latest
  3. 安装 thriftgo:go install github.com/cloudwego/thriftgo@latest

安装成功后,执行 kitex --versionthriftgo --version 应该能够看到具体版本号的输出(版本号有差异,以 x.x.x 示例):

$ kitex --version
vx.x.x
​
$ thriftgo --version
thriftgo x.x.x

确定代码放置位置

  1. 若将代码放置于 $GOPATH/src 下,需在 $GOPATH/src 下创建额外目录,进入该目录后再获取代码:
mkdir -p $(go env GOPATH)/src/github.com/cloudwego
cd $(go env GOPATH)/src/github.com/cloudwego

若将代码放置于 GOPATH 之外,可直接获取

(3)关于 Kitex

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

  1. Serialization 序列化
  2. Transport 传输

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

(4)为什么要使用 IDL*

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

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

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

(5)编写IDL

首先我们需要编写一个 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)
}
(6)生成 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

获取最新的 Kitex 框架

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

$ go get github.com/cloudwego/kitex@latest
$ go mod tidy

如果遇到类似如下报错:

github.com/apache/thrift/lib/go/thrift: ambiguous import: found package github.com/apache/thrift/lib/go/thrift in multiple modules

先执行一遍下述命令,再继续操作:

go mod edit -droprequire=github.com/apache/thrift/lib/go/thrift
go mod edit -replace=github.com/apache/thrift=github.com/apache/thrift@v0.13.0
(7)编写 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
}
(8)编译运行

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

编译:

$ sh build.sh

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

运行:

$ sh output/bootstrap.sh

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

(9)编写客户端

有了服务端后,接下来就让我们编写一个客户端用于调用刚刚运行起来的服务端。

首先,同样的,先创建一个目录用于存放我们的客户端代码:

$ mkdir client

进入目录:

$ cd client

创建一个 main.go 文件,然后就开始编写客户端代码了。

(10)创建 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 用于指定服务端的地址,更多参数可参考基本特性一节。

(11)发起调用

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

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

(12)发起调用

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

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

$ go run main.go

如果不出意外,你可以看到类似如下输出:

2021/05/20 16:51:35 Response({Message:my request})

恭喜你!至此你成功编写了一个 Kitex 的服务端和客户端,并完成了一次调用!

(13)服务与发现

Kitex 框架提供服务注册与发现的扩展,目前已经支持与业界主流注册中心对接。

Kitex 已经通过社区开发者的支持,完成了 ETCD、ZooKeeper、Eureka、Consul、Nacos、Polaris 多种服务发现模式,当然也支持 DNS 解析以及 Static IP 直连访问模式,建立起了强大且完备的社区生态,供用户按需灵活选用。

比如 DNS Resolver, 适合使用 DNS 作为服务发现的场景, 常见的用于 Kubernetes 集群。

更多服务发现组件参看扩展仓库:registry-etcdregistry-nacosregistry-zookeeperpolarisregistry-eurekaregistry-consulregistry-servicecomb

使用方式

以 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)
    }
    ...
}
(14)Kitex生态

Kitex拥有非常丰富的扩展生态,一下列举一部分常用扩展。

opentelemetry : https://github.com/kitex-contrib/xds
ETCD服务注册与发现扩展 : https://github.com/kitex-contrib/obs-opentelemetry
Nacos服务注册与发现扩展 : https://github.com/kitex-contrib/registry-etcd
Zookeeper服务注册与发现扩展 : https://github.com/kitex-contrib/registey-zookeeper
polaris扩展 : https://github.com/kitex-contrib/polaris
丰富的示例代码与业务Demo : https://github.com/cloudwego/kitex-examples
3.Hertz框架

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

(1)快速入门

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

生成/编写示例代码

  1. 在当前目录下创建 hertz_demo 文件夹,进入该目录中
  2. 生成代码 hz new
  3. 整理 & 拉取依赖(go mod tidy)

如果当前使用的是 Windows 环境,可以编写如下的示例代码:

  1. 在当前目录下创建 hertz_demo 文件夹,进入该目录中
  2. 创建 main.go 文件
  3. 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()
}
  1. 生成 go.mod 文件
$ go mod init hertz_demo
  1. 整理 & 拉取依赖
$ go mod tidy

运行示例代码

完成以上操作后,我们可以直接编译并启动 Server

$ go build -o hertz_demo && ./hertz_demo

如果成功启动,你将看到以下信息

2022/05/17 21:47:09.626332 engine.go:567: [Debug] HERTZ: Method=GET    absolutePath=/ping   --> handlerName=main.main.func1 (num=2 handlers)
2022/05/17 21:47:09.629874 transport.go:84: [Info] HERTZ: HTTP server listening on address=[::]:8888

接下来,我们可以对接口进行测试

$ curl http://127.0.0.1:8888/ping

如果不出意外,我们可以看到类似如下输出

$ {"message":"pong"}

到现在,我们已经成功启动了 Hertz Server,并完成了一次调用。更多 API 示例请参考 API 示例

总结

其实三个框架的官方文档写的都特别特别特别清楚,本文很多内容都是来源于官方文档。

写在最后

本文是我的日常学习笔记,如果哪里有写错,麻烦请指出来,感谢。这里我也推荐大家多写笔记,写笔记是一个很好的习惯,可以帮助我们更好的吸收和理解学习的新知识,新的一年大家一起加油!