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

前语

记载加入青训营的每一天的笔记

今日这次的笔记是对于Go结构(Gorm、Kitex、Hertz)三件套的内容

三件套介绍

Gorm、Kitex、Hertz的基本用法与常见的API讲解

Gorm

gorm是Golang语言中一个现已迭代数十年且功能强大、功能极好的ORM结构

ORM:Object Relational Mapping(目标关系映射),其主要作用是在编程中,把面向目标的概念跟数据库中表的概念对应起来,

简略来说,在golang中,自界说的一个结构体对应着一张表,结构体的实例则对应着表中的一条记载。

Kitex

Kitex是字节内部Golang微服务RPC结构 具有高功能、强可扩展的主要特点 支撑多协议而且拥有丰厚的开源扩展

Hertz

Hertz是字节内部的Http结构 参阅了其他开源结构的优势 结合字节跳动内部的需求 具有高可用、高功能、高扩展性的特点

三件套运用

Gorm

该部分笔记主要参阅:gorm.io/zh_CN/docs

声明模型

模型界说

模型是标准的 struct,由 Go 的基本数据类型、完结了 Scanner 和 Valuer 接口的自界说类型及其指针或别号组成

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
}
约好

GORM 倾向于约好优于配置

默许情况下,GORM 运用 ID 作为主键,运用结构体名的 蛇形复数 作为表名,字段名的 蛇形 作为列名,并运用 CreatedAtUpdatedAt 字段追寻创立、更新时刻

gorm.Model

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 User struct {
 gorm.Model
 Name string
}
// 等效于
type User struct {
 ID    uint      `gorm:"primaryKey"`
 CreatedAt time.Time
 UpdatedAt time.Time
 DeletedAt gorm.DeletedAt `gorm:"index"`
 Name string
}

连接到数据库

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

MySQL
import (
 "gorm.io/driver/mysql"
 "gorm.io/gorm"
)
​
func main() {
 // 参阅 https://github.com/go-sql-driver/mysql#dsn-data-source-name 获取概况
 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

运用现有数据库连接

GORM 答应经过一个现有的数据库连接来初始化 *gorm.DB

import (
 "database/sql"
 "gorm.io/driver/mysql"
 "gorm.io/gorm"
)
​
sqlDB, err := sql.Open("mysql", "mydb_dsn")
gormDB, err := gorm.Open(mysql.New(mysql.Config{
 Conn: sqlDB,
}), &gorm.Config{})

CRUD接口

创立
创立记载
user := User{Name: "Jinzhu", Age: 18, Birthday: time.Now()}
​
result := db.Create(&user) // 经过数据的指针来创立
​
user.ID       // 回来插入数据的主键
result.Error    // 回来 error
result.RowsAffected // 回来插入记载的条数
批量插入

将切片数据传递给 Create 办法,GORM 将生成一个单一的 SQL 句子来插入一切数据,并回填主键的值,钩子办法也会被调用。

var users = []User{{Name: "jinzhu1"}, {Name: "jinzhu2"}, {Name: "jinzhu3"}}
DB.Create(&users)
​
for _, user := range users {
 user.ID // 1,2,3
}
经过Map数据类型创立记载

GORM 支撑依据 map[string]interface{}[]map[string]interface{}{} 创立记载

DB.Model(&User{}).Create(map[string]interface{}{
 "Name": "jinzhu", "Age": 18,
})
​
// 依据 `[]map[string]interface{}{}` 批量插入
DB.Model(&User{}).Create([]map[string]interface{}{
  {"Name": "jinzhu_1", "Age": 18},
  {"Name": "jinzhu_2", "Age": 20},
})
查询
检索单个目标

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
​
// 查看 ErrRecordNotFound 过错
errors.Is(result.Error, gorm.ErrRecordNotFound)
​

FirstLast 办法会依据主键查找到第一个、最终一个记载, 它仅在经过 struct 或供给 model 值进行查询时才起作用。假如 model 类型没有界说主键,则按第一个字段排序

var user User
​
// 能够
DB.First(&user)
// SELECT * FROM `users` ORDER BY `users`.`id` LIMIT 1// 能够
result := map[string]interface{}{}
DB.Model(&User{}).First(&result)
// SELECT * FROM `users` ORDER BY `users`.`id` LIMIT 1// 不可
result := map[string]interface{}{}
DB.Table("users").First(&result)
​
// 但能够合作 Take 运用
result := map[string]interface{}{}
DB.Table("users").Take(&result)
​
// 依据第一个字段排序
type Language struct {
 Code string
 Name string
}
DB.First(&Language{})
// SELECT * FROM `languages` ORDER BY `languages`.`code` LIMIT 1
检索目标
// 获取悉数记载
result := db.Find(&users)
// SELECT * FROM users;
​
result.RowsAffected // 回来找到的记载数,相当于 `len(users)`
result.Error    // returns error
条件查询

String条件

// 获取第一条匹配的记载
db.Where("name = ?", "jinzhu").First(&user)
// SELECT * FROM users WHERE name = 'jinzhu' ORDER BY id LIMIT 1;
​
// 获取悉数匹配的记载
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 条件

// 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;
​
// 主键切片条件
db.Where([]int64{20, 21, 22}).Find(&users)
// SELECT * FROM users WHERE id IN (20, 21, 22);

留意 当运用结构作为条件查询时,GORM 只会查询非零值字段。这意味着假如您的字段值为 0''false 或其他 零值,该字段不会被用于构建查询条件

Not 条件

构建NOT条件,用法与 Where 相似

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;
​
// 不在主键切片中的记载
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);
挑选特定字段

挑选想从数据库中检索的字段,默许情况下会挑选悉数字段

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;
Order查询

指定从数据库检索记载时的排序办法

db.Order("age desc, name").Find(&users)
// SELECT * FROM users ORDER BY age desc, name;
​
// Multiple orders
db.Order("age desc").Order("name").Find(&users)
// SELECT * FROM users ORDER BY age desc, name;
Limit & Offset查询

Limit 指定获取记载的最大数量 Offset 指定在开端回来记载之前要越过的记载数量

db.Limit(3).Find(&users)
// SELECT * FROM users LIMIT 3;// 经过 -1 消除 Limit 条件
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;// 经过 -1 消除 Offset 条件
db.Offset(10).Find(&users1).Offset(-1).Find(&users2)
// SELECT * FROM users OFFSET 10; (users1)
// SELECT * FROM users; (users2)
Group & Having查询
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"
​
rows, err := db.Table("orders").Select("date(created_at) as date, sum(amount) as total").Group("date(created_at)").Rows()
for rows.Next() {
 ...
}
​
rows, err := db.Table("orders").Select("date(created_at) as date, sum(amount) as total").Group("date(created_at)").Having("sum(amount) > ?", 100).Rows()
for rows.Next() {
 ...
}
​
type Result struct {
 Date time.Time
 Total int64
}
db.Table("orders").Select("date(created_at) as date, sum(amount) as total").Group("date(created_at)").Having("sum(amount) > ?", 100).Scan(&results)
Distinct查询

从模型中挑选不相同的值

db.Distinct("name", "age").Order("name, age desc").Find(&results)
高档查询
智能挑选字段

GORM 答应经过 Select 办法挑选特定的字段,假如您在应用程序中经常运用此功能,你也能够界说一个较小的结构体,以完结调用 API 时主动挑选特定的字段

type User struct {
 ID   uint
 Name  string
 Age  int
 Gender string
 // 假设后边还有几百个字段...
}
​
type APIUser struct {
 ID  uint
 Name string
}
​
// 查询时会主动挑选 `id`, `name` 字段
db.Model(&User{}).Limit(10).Find(&APIUser{})
// SELECT `id`, `name` FROM `users` LIMIT 10
子查询

子查询能够嵌套在查询中,GORM 答应在运用 *gorm.DB 目标作为参数时生成子查询

db.Where("amount > (?)", db.Table("orders").Select("AVG(amount)")).Find(&orders)
// SELECT * FROM "orders" WHERE amount > (SELECT AVG(amount) FROM "orders");
​
subQuery := db.Select("AVG(age)").Where("name LIKE ?", "name%").Table("users")
db.Select("AVG(age) as avgage").Group("name").Having("AVG(age) > (?)", subQuery).Find(&results)
// SELECT AVG(age) as avgage FROM `users` GROUP BY `name` HAVING AVG(age) > (SELECT AVG(age) FROM `users` WHERE name LIKE "name%")
From子查询

GORM 答应您在 Table 办法中经过 FROM 子句运用子查询

db.Table("(?) as u", db.Model(&User{}).Select("name", "age")).Where("age = ?", 18).Find(&User{})
// SELECT * FROM (SELECT `name`,`age` FROM `users`) as u WHERE `age` = 18
​
subQuery1 := db.Model(&User{}).Select("name")
subQuery2 := db.Model(&Pet{}).Select("name")
db.Table("(?) as u, (?) as p", subQuery1, subQuery2).Find(&User{})
// SELECT * FROM (SELECT `name` FROM `users`) as u, (SELECT `name` FROM `pets`) as p
Group条件

运用 Group 条件能够更轻松的编写复杂 SQL

db.Where(
  db.Where("pizza = ?", "pepperoni").Where(db.Where("size = ?", "small").Or("size = ?", "medium")),
).Or(
  db.Where("pizza = ?", "hawaiian").Where("size = ?", "xlarge"),
).Find(&Pizza{}).Statement
​
// SELECT * FROM `pizzas` WHERE (pizza = "pepperoni" AND (size = "small" OR size = "medium")) OR (pizza = "hawaiian" AND size = "xlarge")
更新
保存一切字段

Save 会保存一切的字段,即便字段是零值

db.First(&user)
​
user.Name = "jinzhu 2"
user.Age = 100
db.Save(&user)
// UPDATE users SET name='jinzhu 2', age=100, birthday='2016-01-01', updated_at = '2013-11-17 21:34:10' WHERE id=111;
更新单个列

当运用 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;
更新选定字段

假如想要在更新时选定、疏忽某些字段,您能够运用 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})
批量更新

假如尚未经过 Model 指定记载的主键,则 GORM 会履行批量更新

// 依据 struct 更新
db.Model(User{}).Where("role = ?", "admin").Updates(User{Name: "hello", Age: 18})
// UPDATE users SET name='hello', age=18 WHERE role = 'admin';
​
// 依据 map 更新
db.Table("users").Where("id IN ?", []int{10, 11}).Updates(map[string]interface{}{"name": "hello", "age": 18})
// UPDATE users SET name='hello', age=18 WHERE id IN (10, 11);
获取更新的记载数
// 经过 `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    // 更新的过错
删去
删去一条记载

删去一条记载时,删去目标需求指定主键,不然会触发 批量 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 答应经过主键(能够是复合主键)和内联条件来删去目标,它能够运用数字(如以下例子。也能够运用字符串——译者注)

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);
​
批量删去

假如指定的值不包含主特点,那么 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%";
回来删去行的数据
// 回来一切列
var users []User
DB.Clauses(clause.Returning{}).Where("role = ?", "admin").Delete(&users)
// DELETE FROM `users` WHERE role = "admin" RETURNING *
// users => []User{{ID: 1, Name: "jinzhu", Role: "admin", Salary: 100}, {ID: 2, Name: "jinzhu.2", Role: "admin", Salary: 1000}}
​
// 回来指定的列
DB.Clauses(clause.Returning{Columns: []clause.Column{{Name: "name"}, {Name: "salary"}}}).Where("role = ?", "admin").Delete(&users)
// DELETE FROM `users` WHERE role = "admin" RETURNING `name`, `salary`
// users => []User{{ID: 0, Name: "jinzhu", Role: "", Salary: 100}, {ID: 0, Name: "jinzhu.2", Role: "", Salary: 1000}}
软删去

假如模型包含了一个 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;

查询被软删去的记载

能够运用 Unscoped 找到被软删去的记载

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

事务

禁用默许事务

为了保证数据一致性,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)
事务敞开

要在事务中履行一系列操作,一般流程如下

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 支撑嵌套事务,您能够回滚较大事务内履行的一部分操作

db.Transaction(func(tx *gorm.DB) error {
 tx.Create(&user1)
​
 tx.Transaction(func(tx2 *gorm.DB) error {
  tx2.Create(&user2)
  return errors.New("rollback user2") // Rollback user2
  })
​
 tx.Transaction(func(tx2 *gorm.DB) error {
  tx2.Create(&user3)
  return nil
  })
​
 return nil
})
手动事务

Gorm 支撑直接调用事务控制办法(commit、rollback)

// 开端事务
tx := db.Begin()
​
// 在事务中履行一些 db 操作(从这儿开端,您应该运用 'tx' 而不是 'db')
tx.Create(...)
​
// ...
​
// 遇到过错时回滚事务
tx.Rollback()
​
// 不然,提交事务
tx.Commit()

Gorm功能进步

运用PrepareStmt缓存预编译句子能够进步后续调用的速度,进步大约35%左右。

db , err := gorm.Open(mysql.Open("username:password@tcp(localhost:9910)/database?charset=utf8"),&gorm.Config{
             PrepareStmt: true}

Kitex

装置

Kitex目前对Windows的支撑不完善,主张运用虚拟机或WSL2

装置代码生成东西

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

运用

kitex 是 Kitex 结构供给的用于生成代码的一个指令行东西。目前,kitex 支撑 thrift 和 protobuf 的 IDL,并支撑生成一个服务端项意图骨架。

编写IDL

IDL是什么:IDL 全称是 Interface Definition Language,接口界说语言

为什么运用IDL:要进行 RPC,就需求知道对方的接口是什么,需求传什么参数,同时也需求知道回来值是什么样的,就比方两个人之间交流,需求保证在说的是同一个语言、同一件事。 这时分,就需求经过 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)
}
生成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 服务起到作用。

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 服务就开端运转了。

编写客户端

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

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

$ mkdir client

进入目录:

$ cd client

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

首要让咱们创立一个调用所需的 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 用于指定服务端的地址。

建议调用
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

假如不出意外,能够看到相似如下输出:

2023/01/26 07:23:35 Response({Message:my request})

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

Hertz

装置指令行东西hz

首要,咱们需求装置运用demo所需求的指令行东西 hz:

  • 保证 GOPATH 环境变量现已被正确地界说(例如 export GOPATH=~/go)而且将$GOPATH/bin添加到 PATH 环境变量之中(例如 export PATH=$GOPATH/bin:$PATH);请勿将 GOPATH 设置为当时用户没有读写权限的目录
  • 装置 hz:go install github.com/cloudwego/hertz/cmd/hz@latest

确认代码放置位置

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

编写示例代码

  • 在当时目录下创立 hertz_demo 文件夹,进入该目录中
  • 创立 main.go 文件
  • 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()
}
  • 生成go.mod 文件

    $ go mod init hertz_demo
    
  • 整理 & 拉取依靠

    $ go mod tidy
    

运转示例代码

完结以上操作后,咱们能够直接编译并发动 Server

$ go build -o hertz_demo && ./hertz_demo

假如成功发动,将看到以下信息

2023/01/28 21:47:09.626332 engine.go:567: [Debug] HERTZ: Method=GET   absolutePath=/ping  --> handlerName=main.main.func1 (num=2 handlers)
2023/01/28 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,并完结了一次调用!

Hertz路由优先级

Hertz供给了参数路由和通配路由,路由的优先级为:静态路由>命名路由>通配路由

Hertz中间件

Hertz中间件的品种是多种多样的,简略分为两大类:

  • 服务端中间件
  • 客户端中间件
服务端中间件

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

中间件能够在恳求更深化地传递到事务逻辑之前或之后履行:

  • 中间件能够在恳求到达事务逻辑之前履行,比方履行身份认证和权限认证,当中间件只要初始化(pre-handle)相关逻辑,且没有和 real handler 在一个函数调用栈中的需求时,中间件中能够省略掉最终的.Next,如图中的中间件 B。
  • 中间件也能够在履行过事务逻辑之后履行,比方记载呼应时刻和从异常中康复。假如在事务 handler 处理之后有其它处理逻辑( post-handle ),或对函数调用链(栈)有强需求,则必须显式调用.Next,如图中的中间件 C。
完结一个中间件
// 办法一
func MyMiddleware() app.HandlerFunc {
 return func(ctx context.Context, c *app.RequestContext) {
  // pre-handle
  // ...
  c.Next(ctx)
  }
}
​
// 办法二
func MyMiddleware() app.HandlerFunc {
 return func(ctx context.Context, c *app.RequestContext) {
  c.Next(ctx) // call the next middleware(handler)
  // post-handle
  // ...
  }
}

中间件会按界说的先后顺序顺次履行,假如想快速停止中间件调用,能够运用以下办法,留意当时中间件仍将履行

  • Abort():停止后续调用
  • AbortWithMsg(msg string, statusCode int):停止后续调用,并设置 response中body,和状态码
  • AbortWithStatus(code int):停止后续调用,并设置状态码
Server等级中间件

Server等级中间件会对整个server的路由收效

h := server.Default()
h.Use(GlobalMiddleware())
路由组等级中间件

路由组等级中间件对当时路由组下的途径收效

h := server.Default()
group := h.Group("/group")
group.Use(GroupMiddleware())

或者

package main
​
import (
    "context"
    "fmt""github.com/cloudwego/hertz/pkg/app"
    "github.com/cloudwego/hertz/pkg/app/server"
)
​
func GroupMiddleware() []app.HandlerFunc {
    return []app.HandlerFunc{func(ctx context.Context, c *app.RequestContext) {
        fmt.Println("group middleware")
        c.Next(ctx)
    }}
}
​
func main() {
    h := server.Default(server.WithHostPorts("127.0.0.1:8888"))
​
    group := h.Group("/group", append(GroupMiddleware(),
    func(ctx context.Context, c *app.RequestContext) {
      fmt.Println("group middleware 2")
      c.Next(ctx)
     })...)
    // ...
    h.Spin()
}
运用默许中间件

Hertz 结构现已预置了常用的 recover 中间件,运用 server.Default() 默许能够注册该中间件

客户端中间件

客户端中间件能够在恳求宣布之前或获取呼应之后履行:

  • 中间件能够在恳求宣布之前履行,比方一致为恳求添加签名或其他字段。
  • 中间件也能够在收到呼应之后履行,比方一致修正呼应结果适配事务逻辑。
完结一个中间件

客户端中间件完结和服务端中间件不同。Client 侧无法拿到中间件 index 完结递加,因此 Client 中间件选用提前构建嵌套函数的方式完结,在完结一个中间件时,能够参阅下面的代码。

func MyMiddleware(next client.Endpoint) client.Endpoint {
 return func(ctx context.Context, req *protocol.Request, resp *protocol.Response) (err error) {
  // pre-handle
  // ...
  err = next(ctx, req, resp)
  if err != nil {
   return
   }
  // post-handle
  // ...
  }
}
注册一个中间件
package main
​
import (
    "context"
    "fmt""github.com/cloudwego/hertz/pkg/app/client"
    "github.com/cloudwego/hertz/pkg/protocol"
)
​
func MyMiddleware(next client.Endpoint) client.Endpoint {
    return func(ctx context.Context, req *protocol.Request, resp *protocol.Response) (err error) {
        // pre-handle
        // ...
        fmt.Println("before request")
​
        req.AppendBodyString("k1=v1&")
​
        err = next(ctx, req, resp)
        if err != nil {
            return
        }
        // post-handle
        // ...
        fmt.Println("after request")
​
        return nil
    }
}
​
func main() {
    client, _ := client.NewClient()
    client.Use(MyMiddleware)
    statusCode, body, err := client.Post(context.Background(),
        []byte{},
        "http://httpbin.org/redirect-to?url=http%3A%2F%2Fhttpbin.org%2Fpost&status_code=302",
        &protocol.Args{})
    fmt.Printf("%d, %s, %s", statusCode, body, err)
}

总结

  • 了解Gorm/Kitex/Hertz是什么
  • 熟悉Gorm/Kitex/Hertz的基础运用
  • 经过实战事例分析将三个结构的运用串联起来