这是我参与「第五届青训营」伴学笔记创作活动的第 9 天
入门GO语言-Go 框架三件套详解(Web/RPC/ORM)
三件套介绍
Gorm
Gorm是一个已经迭代了10年+的功能强大的 ORM框架,在字节内部被广泛使用并且拥有非常丰富的开源扩展。
Kitex
Kitex是字节内部的Golang微服务RPC框架,具有高性能、强可扩展的主要特点,支持多协议并且拥有丰富的开源扩展。
Hertz
Hertz是字节内部的HTTP框架,参考了其他开源框架的优势,结合字节跳动内部的需求,具有高易用性、高性能、高扩展性特点。
三件套的使用
Gorm 的基础使用
安装
go get -u gorm.io/gormgo get -u gorm.io/driver/sqlite
基本使用
package main
import (
"gorm.io/driver/mysql"
"gorm.io/gorm"
)
// 定义gorm model
type Product struct {
Code string
Price uint
}
// 为modle定义表名
func (p Product) TableName() string {
return "product"
}
func main() {
//链接数据库
db, err := gorm.Open(
mysql.Open("root:123456@tcp(127.0.0.1:3306)/test?charset=utf8mb4&parseTime=True&loc=Local"),
&gorm.Config{})
if err != nil {
panic("failed to connect database")
}
// 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)
}
- Gorm的约定(默认)
- Gorm使用名为ID的字段作为主键
- 使用结构体的蛇形负数作为表名
- 字段名的蛇形作为列名
- 使用CreatedAt、UpdatedAt字段作为创建、更新时间
GORM支持的数据库
- GORM目前支持MySQL、SQLServer、PostgreSQL、SQLite
- 连接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{)
- GORM通过驱动来连接数据库,如果需要连接其它类型的数据库,可以复用/自行开发驱动
GORM创建数据库
package main
import (
"fmt"
"gorm.io/driver/mysql"
"gorm.io/gorm"
)
type Product struct {
ID uint `gorm:"primarykey"`
Code string `gorm:"column:code"`
Price uint `gorm:"column:price"`
}
func main() {
db, err := gorm.Open(
mysql.Open("root:123456@tcp(127.0.0.1:3306)/test?charset=utf8mb4&parseTime=True&loc=Local"),
&gorm.Config{})
if err != nil {
panic("failed to connect database")
}
//创建一条
p := &Product{Code: "F23"}
res := db.Create(p)
fmt.Println(res.Error) //获取err
fmt.Println(p.ID) //返回插入数据的主键
//创建多条
products := []*Product{{Code: "E51"}, {Code: "W12"}, {Code: "K01"}}
res = db.Create(products)
fmt.Println(res.Error)
for _, p := range products {
fmt.Println(p.ID)
}
}
如何使用 Upsert
- 使用clause.OnConflict处理数据冲突
//以不处理冲突为例,创建一条数据
p: = &Product{Code : "D42",ID:1}
db.Clauses(clause.OnConflict{DoNothing: true}).Create(&p)
如何使用默认值
- 通过使用default标签为字段定义默认值
type Product struct {
ID int64
Name string `gorm:"default:galeone"`
Age int64 `gorm:"default:18"`
}
GORM查询数据
import (
"fmt"
"gorm.io/driver/mysql"
"gorm.io/gorm"
)
type SelectSql struct {
Name string `gorm:"column:name"`
Age int `gorm:"column:age"`
}
func (s SelectSql) TableName() string {
return "selectsql"
}
func main() {
db, err := gorm.Open(
mysql.Open("root:123456@tcp(127.0.0.1:3306)/test?charset=utf8mb4&parseTime=True&loc=Local"),
&gorm.Config{})
if err != nil {
panic("failed to connect database")
}
//获取第一条记录(主键升序),查询不到数据返回ErrRecordNotFound
u := &SelectSql{}
db.First(u) //select * from selectsql order by Name limit 1;
//查询多条
users := make([]*SelectSql, 8)
result := db.Debug().Where("age>10").Find(&users) //SELECT * FROM `selectsql` WHERE age>10
fmt.Println(result.RowsAffected) //返回找到的数据记录相当于len(users)
fmt.Println(result.Error) //returns error
//SELECT * FROM `selectsql` WHERE name In ('jinzhu','jinzhu 2')
db.Debug().Where("name IN ?", []string{"jinzhu", "jinzhu 2"}).Find(&users)
//SELECT * FROM `selectsql` WHERE name Like '%jin%'
db.Debug().Where("name LIKE ?", "%jin%").Find(&users)
//SELECT * FROM `selectsql` WHERE name = 'lipeiqi' AND age >= 20
db.Debug().Where("name = ? AND age >= ?", "lipeiqi", 20).Find(&users)
//SELECT * FROM `selectsql` WHERE `selectsql`.`name` = 'lipeiqi' AND `selectsql`.`age` = 22
db.Debug().Where(&SelectSql{Name: "lipeiqi", Age: 22}).Find(&users)
//SELECT * FROM `selectsql` WHERE `Age` = 2 AND `Name` = 'jinzhu'
db.Debug().Where(map[string]interface{}{"Name": "jinzhu", "Age": 2}).Find(&users)
}
- First的使用踩坑
- 使用First时,需要注意查询不到数据会返回ErrRecordNotFound。
- 使用 Find查询多条数据,查询不到数据不会返回错误。
- 使用结构体作为查询条件
- 当使用结构作为条件查询时,GORM只会查询非零值字段。这意味着如果您的字段值为0、"、 false或其他零值,该字段不会被用于构建查询条件,使用Map来构建查询条件。
Gorm更新数据
package main
import (
"gorm.io/driver/mysql"
"gorm.io/gorm"
)
type UpDataSql struct {
Name string `gorm:colum:name`
Age int `grom:colum:age`
}
func (u UpDataSql) TableName() string {
return "updatasql"
}
func main() {
db, err := gorm.Open(
mysql.Open("root:123456@tcp(127.0.0.1:3306)/test?charset=utf8mb4&parseTime=True&loc=Local"),
&gorm.Config{})
if err != nil {
panic("failed to connect database")
}
//条件更新单个列
var updatesql UpDataSql
//UPDATE `updatasql` SET `age`=18 WHERE age > 22
db.Model(&updatesql).Where("age > ?", 22).Update("age", 18)
//更新多个列
//根据‘struct”更新属性,只会更新非零值的字段
//UPDATE `updatasql` SET `name`='yyk',`age`=21 WHERE name = 'lpq'
db.Model(&updatesql).Where("name = ?", "yyk").Updates(UpDataSql{Name: "yyk", Age: 21})
//根据Map更新属性
//UPDATE `updatasql` SET `age`=18,`name`='hello' WHERE name = 'yyk'
db.Model(&updatesql).Where("name = ?", "yyk").Updates(map[string]interface{}{"name": "hello", "age": 18})
//更新选定字段
// UPDATE `updatasql` SET `age`=21 WHERE name = 'hello'
db.Model(&updatesql).Where("name = ?", "hello").Select("age").Updates(UpDataSql{Name: "yyk", Age: 21})
//sql表达式
// UPDATE `updatasql` SET `age`=age * 2 +100 WHERE name = 'hello'
db.Model(&updatesql).Where("name = ?", "hello").Update("age", gorm.Expr("age * ? +?", 2, 100))
}
- 使用 Struct 更新时,只会更新非零值,如果需要更新零值可以使用 Map更新或使用Select 选择字段。
GORM删除数据
- 物理删除
db.Delete(&updatesql,10)// DELETE FROM updatesql WHERE id = 10;
db.Delete(&updatesql,10)// DELETE FROM updatesql WHERE id = "10";
db.Delete(&updatesql,[]int{1,2,3})// DELETE FROM updatesql WHERE id IN (1,2,3);
db.Where("name LIKE ?","%jinzhu").Delete(UpDataSql{})// DELETE FROM updatesql where name LIKE “%jinzhu%";
db.Delete(UpDataSql{},"email LIKE ?","%jinzhu%")// DELETE from updatesql where name LIKE“%jinzhu%";
- 软删除
package main
import (
"gorm.io/driver/mysql"
"gorm.io/gorm"
)
type DelectSql struct {
Name string
Age int `gorm:"default:18"`
Deleted gorm.DeletedAt
}
func (d DelectSql) TableName() string {
return "delectsql"
}
func main() {
db, err := gorm.Open(
mysql.Open("root:123456@tcp(127.0.0.1:3306)/test?charset=utf8mb4&parseTime=True&loc=Local"),
&gorm.Config{})
if err != nil {
panic("failed to connect database")
}
// INSERT INTO `delectsql` (`name`,`age`,`deleted`) VALUES ('E51',0,NULL),('W12',0,NULL),('K01',0,NULL)
db.Debug().Create([]*DelectSql{{Name: "E51"}, {Name: "W12"}, {Name: "K01"}})
//UPDATE `delectsql` SET `deleted`='2023-01-29 19:16:07.686' WHERE age =18 AND `delectsql`.`deleted` IS NULL
db.Debug().Where("age =?", 18).Delete(&DelectSql{})
//找不到删除记录
//SELECT * FROM `delectsql` WHERE age =18 AND `delectsql`.`deleted` IS NULL
db.Debug().Where("age =?", 18).Find(&DelectSql{})
//SELECT * FROM `delectsql` WHERE age =18
////可以找到删除记录
db.Debug().Unscoped().Where("age =?", 18).Find(&DelectSql{})
}
- GORM提供了gorm.DeletedAt 用于帮助用户实现软删
- 拥有软删除能力的 Model调用Delete时,记录不会被从数据库中真正删除。但 GORM会将DeletedAt置为当前时间,并且你不能再通过正常的查询方法找到该记录。
- 使用Unscoped可以查询到被软删的数据
GORM事务
- Gorm提供了Begin、Commit、Rollback方法用于使用事务
package main
import (
"gorm.io/driver/mysql"
"gorm.io/gorm"
)
type SelectSql struct {
Name string
Age int
}
func (s SelectSql) TableName() string {
return "selectsql"
}
func main() {
db, err := gorm.Open(
mysql.Open("root:123456@tcp(127.0.0.1:3306)/test?charset=utf8mb4&parseTime=True&loc=Local"),
&gorm.Config{})
if err != nil {
panic("failed to connect database")
}
//开始事务
tx := db.Begin()
//在事务中执行一些db操作(从这里开始,您应该使用‘tx′而不是‘db ' )
if err = tx.Create(&SelectSql{Name: "yyk"}).Error; err != nil {
//遇到错误回滚事务
tx.Rollback()
return
}
if err = tx.Create(&SelectSql{Name: "yyk1"}).Error; err != nil {
//遇到错误回滚事务
tx.Rollback()
return
}
//提交事务
tx.Commit()
}
- Gorm提供了Tansaction方法用于自动提交事务,避免用户漏写Commit、Rollbcak.
package main
import (
"gorm.io/driver/mysql"
"gorm.io/gorm"
)
type SelectSql struct {
Name string
Age int
}
func (s SelectSql) TableName() string {
return "selectsql"
}
func main() {
db, err := gorm.Open(
mysql.Open("root:123456@tcp(127.0.0.1:3306)/test?charset=utf8mb4&parseTime=True&loc=Local"),
&gorm.Config{})
if err != nil {
panic("failed to connect database")
}
//Gorm提供了Tansaction方法用于自动提交事务,避免用户漏写Commit、Rollbcak.
if err=db.Transaction(func(tx *gorm.DB) error {
if err=tx.Create(&SelectSql{Name:"name"}).Error;err!=nil{
return err
}
if err=tx.Create(&SelectSql{Name:"name1"}).Error;err!=nil{
return err
}
return nil
});err!=nil{
return
}
}
GORM Hook
- GORM在提供了CURD的Hook 能力。
- Hook是在创建、查询、更新、删除等操作之前、之后自动调用的函数。
- 如果任何Hook返回错误,GORM将停止后续的操作并回滚事务。
package main
import (
"errors"
"gorm.io/gorm"
)
type User struct {
ID int64
Name string `gorm:"default:galeone"`
Age int64 `gorm:"default:18"`
}
type Email struct {
ID int64
Name string
Email string
}
//创建前hook比如可以做参数校验
func (u *User) BeforeCreate(tx *gorm.DB) (err error) {
if u.Age < 0 {
return errors.New("can't save invalid data" )
}
return
}
//创建后hook比如可以给用户添加邮箱
func (u *User) AfterCreate(tx *gorm.DB) (err error) {
return tx.Create(&Email{ID: u.ID,Email: u.Name + "@*** ,com"}).Error
}
Gorm性能提高
- 对于写操作(创建、更新、删除),为了确保数据的完整性,GORM 会将它们封装在事务内运行。但这会降低性能,你可以使用SkipDefaultTransaction关闭默认事务。
- 使用 PrepareStmt缓存预编译语句可以提高后续调用的速度,本机测试提高大约35%左右。
db, err := gorm.Open(
mysql.Open("root:123456@tcp(127.0.0.1:3306)/test?charset=utf8mb4&parseTime=True&loc=Local"),
&gorm.Config{
SkipDefaultTransaction: true, //关闭默认事务
PrepareStmt: true, //缓存预编译语句
})
if err != nil {
panic("failed to connect database")
}
GORM生态
- GORM 拥有非常丰富的扩展生态,以下列举一部分常用扩展。
| GORM代码生成工具 | github.com/go-gorm/gen |
|---|---|
| GORM分片库方案 | github.com/go-gorm/sha… |
| GORM手动索引 | github.com/go-gorm/hin… |
| GORM乐观锁 | github.comlgo-gorm/optimistICI… |
| GORM读写分离 | github.com/go-gorm/dbr… |
| GORM OpenTelemetry扩展 | github.com/go-gorm/ope… |
Kitex的基础使用
安装Kitex代码生成工具
- Kitex目前对Windows 的支持不完善,如果本地开发环境是Windows 的同学建议使用虚拟机或WSL2。
- 安装代码生成工具
go install github.com/cloudwego/kitex/tool/cmd/kitex@latestgo install github.com/cloudwego/thriftgo@latest
定义IDL
- 使用IDL定义服务与接口
- 如果我们要进行RPC,就需要知道对方的接口是什么,需要传什么参数,同时也需要知道返回值是什么样的。这时候,就需要通过IDL来约定双方的协议,就像在写代码的时候需要调用某个函数,我们需要知道函数签名一样。
- Thrift
- Proto3
namespace go api
struct Request {
1: string message
}
struct Response {
1: string message
}
service Echo {
Response echo(1: Request req)
}
Kitex生成代码
- 使用
kitex -module example -service example echo.thrift命令生成代码 - build.sh :构建脚本
- kitex_gen :lDL内容相关的生成代码,主要是基础的Server/Client代码。main.go程序入口
- handler.go 用户在该文件里实现IDL service定义的方法
Kitex基本使用
- 服务默认监听8888端口
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) {
return
}
Kitex Client 发起请求
- 创建Client
import "example/kitexgen/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)
}
- 发起请求
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)
Kitex服务注册与发现
- 目前 Kitex的服务注册与发现已经对接了主流了服务注册与发现中心,如ETCD,Nacos等。
Kitex服务注册
package main
import (
"context"
"log"
)
type HelloImpl struct{}
func (h *HelloImpl) Echo(ctx context.Context,req *api.Request) (resp *api.Response,err error) {
resp = &api.Responsei{
Message: req.Message,
}
return
}
func main() {
r, err := etcd.NewEtcdRegistry([]string{"127.0.0.1:2379"})
if err != nil {
log.Fatal(err)
}
//初始化server
server:=hello.NewServer(new(HelloImpl),server.WithRegistry(r),server.withServerBasicInfo(&rpcinfo.EndpointBasicInfo{
ServiceName : "Hello",
}))
err = server.Run()
if err != nil {
log.Fatal(err)
}
}
Kitex服务发现
func main() {
r, err := etcd.NewEtcdRegistry([]string{"127.0.0.1:2379"})
if err != nil {
log.Fatal(err)
}
client:=hello.MustNewClient("Hello",client.WithResolver(r))
for{
ctx,cancel:=context.WithTimeou(context.Backgrround(),time.Second*3)
resp,err:=client.Echo(ctx,&api.Reauest{Message:"Helloo"})
cancel()
if err!=nil{
log.Fatal(err)
}
log.Println(resp)
time.Sleep(time.Second)
}
}
Kitex生态
- Kitex拥有非常丰富的扩展生态,以下列举一部分常用扩展。 | XDS扩展 | github.com/kitex-contr… | | --- | --- | | opentelemetry扩展 | github.com/kitex-contr… | | ETCD服务注册与发现扩展 | https:/lgithub.com/kitex-contrib/registry-etcd | | ETCD服务注册与发现扩展 | github.com/kitex-contr… | | Zookeeper 服务注册与发现扩展 | github.com/kitex-contr… | | polaris扩展念 | github.com/kitex-contr… | | 丰富的示例代码与业务Demo | github.com/cloudwego/k… |
Hertz的基础使用
Hertz的安装
- 确保
GOPATH环境变量已经被正确地定义(例如export GOPATH=~/go)并且将$GOPATH/bin添加到PATH环境变量之中(例如export PATH=$GOPATH/bin:$PATH);请勿将GOPATH设置为当前用户没有读写权限的目录 - 安装 hz:
go install github.com/cloudwego/hertz/cmd/hz@latest
Hertz的使用
- 使用Hertz实现,服务监听8080端口并注册了一个 GET方法的路由函数。
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.WithHostPorts("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()
}
Hertz路由
- Hertz提供了GET、POST、PUT、DELETE、ANY 等方法用于注册路
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 RegisterRoute(h *server.Hertz) {
h.GET("/get", func(ctx context.Context, c *app.RequestContext) {
c.String(consts.StatusOK, "get")
})
h.POST("/post", func(ctx context.Context, c *app.RequestContext) {
c.String(consts.StatusOK, "post")
})
h.PUT("/put", func(ctx context.Context, c *app.RequestContext) {
c.String(consts.StatusOK, "put")
})
h.DELETE("/delete", func(ctx context.Context, c *app.RequestContext) {
c.String(consts.StatusOK, "delete")
})
h.PATCH("/patch", func(ctx context.Context, c *app.RequestContext) {
c.String(consts.StatusOK, "patch")
})
h.HEAD("/head", func(ctx context.Context, c *app.RequestContext) {
c.String(consts.StatusOK, "head")
})
h.OPTIONS("/options", func(ctx context.Context, c *app.RequestContext) {
c.String(consts.StatusOK, "options")
})
}
func main() {
h := server.Default(server.WithHostPorts("127.0.0.1:8080"))
RegisterRoute(h)
h.Spin()
}
- Hertz提供了路由组(Group )的能力,用于支持路由分组的功能
h := server.Default(server.WithHostPorts("127.0.0.1:8080"))
v1 := h.Group("/v1")
{
v1.GET("/get", func(ctx context.Context, c *app.RequestContext) {
c.String(consts.StatusOK, "v1get")
})
v1.POST("/post", func(ctx context.Context, c *app.RequestContext) {
c.String(consts.StatusOK, "v1post")
})
}
v2 := h.Group("/v2")
{
v2.GET("/get", func(ctx context.Context, c *app.RequestContext) {
c.String(consts.StatusOK, "v2get")
})
v2.POST("/post", func(ctx context.Context, c *app.RequestContext) {
c.String(consts.StatusOK, "v2post")
})
}
h.Spin()
- Hertz提供了参数路由和通配路由,路由的优先级为:静态路由>命名路由>通配路由
h := server.Default(server.WithHostPorts("127.0.0.1:8080"))
h.GET("/hertz/:version", func(ctx context.Context, c *app.RequestContext) {
version := c.Param("version")
c.String(consts.StatusOK, "Hello %s", version)
})
h.GET("/hertz/:version/*action", func(ctx context.Context, c *app.RequestContext) {
version := c.Param("version")
action:=c.Param("action")
message:=version+"is"+action
c.String(consts.StatusOK, message)
})
h.POST("/hertz/:version/*action", func(ctx context.Context, c *app.RequestContext) {
c.String(consts.StatusOK, c.FullPath())
})
h.Spin()
Hertz参数绑定
- Hertz提供了Bind、Validate、BindAndValidate 函数用于进行参数绑定和校验
package main
import (
"context"
"fmt"
"github.com/cloudwego/hertz/pkg/app"
"github.com/cloudwego/hertz/pkg/app/server"
"github.com/cloudwego/hertz/pkg/protocol/consts"
)
type Args struct {
Query string `query:"query"`
QuerySlice []string `query:"q"`
Path string `path:"path"`
Header string `header:"header"`
From string `from:"from"`
Json string `json:"json"`
Vd int `query:"vd" vd:"$==0||$==1"`
}
func main() {
h := server.Default(server.WithHostPorts("127.0.0.1:8080"))
h.POST("v:path/bind", func(c context.Context, ctx *app.RequestContext) {
var arg Args
err := ctx.BindAndValidate(&arg)
if err != nil {
panic(err)
}
fmt.Println(arg)
})
h.Spin()
}
Hertz中间件
- Hertz的中间件主要分为客户端中间件与服务端中间件,如下展示一个服务端中间件。
- 如何终止中间件调用链的执行
- c.Abort
- c.AbortWithMsg
- c.AbortWIthStats
func MyMiddleware() app.HandlerFunc {
return func(ctx context.Context, c *app.RequestContext) {
fmt.Println("pre-handle")
c.Next(ctx)//调用下一个中间件(处理程序)
fmt.Println("post-handle")
}
}
func main() {
h := server.Default(server.WithHostPorts("127.0.0.1:8080"))
//中间件
h.Use(MyMiddleware())
h.GET("/middleware", func(ctx context.Context, c *app.RequestContext) {
c.String(consts.StatusOK, "Hello hertz!")
})
h.Spin()
}
Hertz Client
- Hertz提供了HTTP Client用于帮助用户发送HTTP请求
c, err := client.NewClient()
if err != nil {
return
}
// 发送http get请求;
status, body, _ := c.Get(context.Background(), nil, "http://www.example.com")
fmt.Printf("status=%v body=%v\n", status, string(body))
// 发送http post请求
var postArgs protocol.Args
postArgs.Set("arg", "a") //post参数设置
status, body, _ = c.Post(context.Background(), nil, "http://www.example.com", &postArgs)
fmt.Printf("status=%v body=%v\n", status, string(body))
Hertz代码生成工具
- Hertz提供了代码生成工具Hz,通过定义IDL(inteface description langu
- lage)文件即可生成对应的基础服务代码。
namespace go hello.example
struct HelloReq {
1: string Name (api.query="name"); // 添加 api 注解为方便进行参数绑定
}
struct HelloResp {
1: string RespBody;
}
service HelloService {
HelloResp HelloMethod(1: HelloReq request) (api.get="/hello");
}
- 目录结构
.
├── biz
│ ├── handler
│ │ └── ping.go
│ │ └── ****.go // 按照服务划分的 handler 集合,位置可根据 handler_dir 改变
│ ├── model
│ │ └── model.go // idl 生成的 struct,位置可根据 model_dir 改变
│ └── router // 未开发自定义 dir
│ └── register.go // 路由注册,用来调用具体的路由注册
│ └── route.go // 具体路由注册位置
│ └── middleware.go // 默认 middleware 生成位置
├── .hz // hz 创建代码标志
├── go.mod
├── main.go // 启动入口
├── router.go // 用户自定义路由写入位置
└── router_gen.go // hz 生成的路由注册调用
- 生成文件
// handler path: biz/handler/hello/example/hello_service.go
// 其中 "hello/example" 是 thrift idl 的 namespace
// "hello_service.go" 是 thrift idl 中 service 的名字,所有 service 定义的方法都会生成在这个文件中
// HelloMethod .
// @router /hello [GET]
func HelloMethod(ctx context.Context, c *app.RequestContext) {
var err error
var req example.HelloReq
err = c.BindAndValidate(&req)
if err != nil {
c.String(400, err.Error())
return
}
resp := new(example.HelloResp)
// 你可以修改整个函数的逻辑,而不仅仅局限于当前模板
resp.RespBody = "hello," + req.Name // 添加的逻辑
c.JSON(200, resp)
}
Hertz 性能
- 网络库Netpoll
- Json编解码Sonic
- 使用sync.Pool复用对象协议层数据解析优化
Hertz生态
- Hertz拥有非常丰富的扩展生态,以下列举一部分常用扩展。
| HTTP2扩展 | github.com/hertz-contr… |
|---|---|
| opentelemetry扩展 | github.com/hertz-contr… |
| 国际化扩展 | github.com/hertz-contr… |
| 反向代理扩展 | github.com/hertz-contr… |
| JWT鉴权扩展 | github.com/hertz-contr… |
| Websocket扩展 | github.com/hertz-contr… |
| 丰富的示例代码与业务Demo | github.com/cloudwego/h… |
实战案例介绍
项目介绍
- 笔记项目是一个使用Hertz 、Kitex、Gorm搭建出来的具备一定业务逻辑的后端API项目
| 服务名称 | 服务介绍 | 传输协议 | 主要技术栈 |
|---|---|---|---|
| demoapi | API服务 | HTTP | rpc/Hertz |
| demouser | 用户数据管理 | Protobuf | Gorm/Kitex |
| demonote | 笔记数据管理 | Thrift | Gorm/Kitex |
项目功能介绍
graph LR
笔记项目 --> demoapi --> 用户登陆
demoapi --> 用户注册
demoapi --> 用户创建笔记
demoapi --> 用户更新笔记
demoapi --> 用户删除笔记
demoapi --> 用户查询笔记
笔记项目 --> demouser
demouser --> 创建用户
demouser --> 查询用户
demouser --> 校验用户
笔记项目 --> demonote
demonote --> 创建笔记
demonote --> 更新笔记
demonote --> 删除笔记
demonote --> 查询笔记
项目调用关系
graph TD
调用方 --HTTP--> demoapi --服务发现--> ETCD
demoapi --操作用户数据proto--> demouser --服务注册--> ETCD
demoapi --操作用户笔记数据thrift--> demonote --服务注册--> ETCD
demouser --> MySql
demonote --> MySql
IDL介绍
note.thrift
namespace go notedemo
struct BaseResp {
1:i64 status_code
2:string status_message
3:i64 service_time
}
struct Note {
1:i64 note_id
2:i64 user_id
3:string user_name
4:string user_avatar
5:string title
6:string content
7:i64 create_time
}
struct CreateNoteRequest {
1:string title
2:string content
3:i64 user_id
}
struct CreateNoteResponse {
1:BaseResp base_resp
}
struct DeleteNoteRequest {
1:i64 note_id
2:i64 user_id
}
struct DeleteNoteResponse {
1:BaseResp base_resp
}
struct UpdateNoteRequest {
1:i64 note_id
2:i64 user_id
3:optional string title
4:optional string content
}
struct UpdateNoteResponse {
1:BaseResp base_resp
}
struct MGetNoteRequest {
1:list<i64> note_ids
}
struct MGetNoteResponse {
1:list<Note> notes
2:BaseResp base_resp
}
struct QueryNoteRequest {
1:i64 user_id
2:optional string search_key
3:i64 offset
4:i64 limit
}
struct QueryNoteResponse {
1:list<Note> notes
2:i64 total
3:BaseResp base_resp
}
service NoteService {
CreateNoteResponse CreateNote(1:CreateNoteRequest req)
MGetNoteResponse MGetNote(1:MGetNoteRequest req)
DeleteNoteResponse DeleteNote(1:DeleteNoteRequest req)
QueryNoteResponse QueryNote(1:QueryNoteRequest req)
UpdateNoteResponse UpdateNote(1:UpdateNoteRequest req)
}
user.proto
syntax = "proto3";
package user;
option go_package = "userdemo";
message BaseResp {
int64 status_code = 1;
string status_message = 2;
int64 service_time = 3;
}
message User {
int64 user_id = 1;
string user_name = 2;
string avatar = 3;
}
message CreateUserRequest {
string user_name = 1;
string password = 2;
}
message CreateUserResponse {
BaseResp base_resp = 1;
}
message MGetUserRequest {
repeated int64 user_ids = 1;
}
message MGetUserResponse {
repeated User users = 1;
BaseResp base_resp = 2;
}
message CheckUserRequest{
string user_name = 1;
string password = 2;
}
message CheckUserResponse{
int64 user_id = 1;
BaseResp base_resp = 2;
}
service UserService {
rpc CreateUser (CreateUserRequest) returns (CreateUserResponse) {}
rpc MGetUser (MGetUserRequest) returns (MGetUserResponse) {}
rpc CheckUser (CheckUserRequest) returns (CheckUserResponse) {}
}
项目技术栈介绍
graph LR
技术框架 --> 语言 --> go
技术框架 --> 底层存储 --> MySQL
技术框架 --> 服务注册 --> Etcd
技术框架 --> RPC框架
RPC框架 --> Kitex
RPC框架 --> Kitex扩展
Kitex扩展 --> registry-etcd
Kitex扩展 --> tracer-opentracing
技术框架 --> ORM框架
ORM框架 --> ORM
ORM框架 --> ORM扩展
ORM扩展 --> gorm-mysql
ORM扩展 --> gorm-opentracing
技术框架 --> HTTP框架
HTTP框架 --> Hertz
HTTP框架 --> Hertz扩展--> Hertz-Jwt
技术框架 --> 链路追踪
链路追踪 --> Jeager
链路追踪 --> opentracing
Hertz关键代码讲解
create_note.go
// Copyright 2021 CloudWeGo Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
package handlers
import (
"context"
"github.com/cloudwego/hertz/pkg/app"
"github.com/cloudwego/kitex-examples/bizdemo/easy_note/cmd/api/rpc"
"github.com/cloudwego/kitex-examples/bizdemo/easy_note/kitex_gen/notedemo"
"github.com/cloudwego/kitex-examples/bizdemo/easy_note/pkg/constants"
"github.com/cloudwego/kitex-examples/bizdemo/easy_note/pkg/errno"
"github.com/hertz-contrib/jwt"
)
// CreateNote create note info
func CreateNote(ctx context.Context, c *app.RequestContext) {
var noteVar NoteParam
if err := c.Bind(¬eVar); err != nil {
SendResponse(c, errno.ConvertErr(err), nil)
return
}
if len(noteVar.Title) == 0 || len(noteVar.Content) == 0 {
SendResponse(c, errno.ParamErr, nil)
return
}
claims := jwt.ExtractClaims(ctx, c)
userID := int64(claims[constants.IdentityKey].(float64))
err := rpc.CreateNote(context.Background(), ¬edemo.CreateNoteRequest{
UserId: userID,
Content: noteVar.Content, Title: noteVar.Title,
})
if err != nil {
SendResponse(c, errno.ConvertErr(err), nil)
return
}
SendResponse(c, errno.Success, nil)
}
Kitex Client关键代码讲解
note.go
// Copyright 2021 CloudWeGo Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
package rpc
import (
"context"
"time"
"github.com/cloudwego/kitex-examples/bizdemo/easy_note/kitex_gen/notedemo"
"github.com/cloudwego/kitex-examples/bizdemo/easy_note/kitex_gen/notedemo/noteservice"
"github.com/cloudwego/kitex-examples/bizdemo/easy_note/pkg/constants"
"github.com/cloudwego/kitex-examples/bizdemo/easy_note/pkg/errno"
"github.com/cloudwego/kitex-examples/bizdemo/easy_note/pkg/middleware"
"github.com/cloudwego/kitex/client"
"github.com/cloudwego/kitex/pkg/retry"
etcd "github.com/kitex-contrib/registry-etcd"
trace "github.com/kitex-contrib/tracer-opentracing"
)
var noteClient noteservice.Client
func initNoteRpc() {
r, err := etcd.NewEtcdResolver([]string{constants.EtcdAddress})
if err != nil {
panic(err)
}
c, err := noteservice.NewClient(
constants.NoteServiceName,
client.WithMiddleware(middleware.CommonMiddleware),
client.WithInstanceMW(middleware.ClientMiddleware),
client.WithMuxConnection(1), // mux
client.WithRPCTimeout(3*time.Second), // rpc timeout
client.WithConnectTimeout(50*time.Millisecond), // conn timeout
client.WithFailureRetry(retry.NewFailurePolicy()), // retry
client.WithSuite(trace.NewDefaultClientSuite()), // tracer
client.WithResolver(r), // resolver
)
if err != nil {
panic(err)
}
noteClient = c
}
// CreateNote create note info
func CreateNote(ctx context.Context, req *notedemo.CreateNoteRequest) error {
resp, err := noteClient.CreateNote(ctx, req)
if err != nil {
return err
}
if resp.BaseResp.StatusCode != 0 {
return errno.NewErrNo(resp.BaseResp.StatusCode, resp.BaseResp.StatusMessage)
}
return nil
}
// QueryNotes query list of note info
func QueryNotes(ctx context.Context, req *notedemo.QueryNoteRequest) ([]*notedemo.Note, int64, error) {
resp, err := noteClient.QueryNote(ctx, req)
if err != nil {
return nil, 0, err
}
if resp.BaseResp.StatusCode != 0 {
return nil, 0, errno.NewErrNo(resp.BaseResp.StatusCode, resp.BaseResp.StatusMessage)
}
return resp.Notes, resp.Total, nil
}
// UpdateNote update note info
func UpdateNote(ctx context.Context, req *notedemo.UpdateNoteRequest) error {
resp, err := noteClient.UpdateNote(ctx, req)
if err != nil {
return err
}
if resp.BaseResp.StatusCode != 0 {
return errno.NewErrNo(resp.BaseResp.StatusCode, resp.BaseResp.StatusMessage)
}
return nil
}
// DeleteNote delete note info
func DeleteNote(ctx context.Context, req *notedemo.DeleteNoteRequest) error {
resp, err := noteClient.DeleteNote(ctx, req)
if err != nil {
return err
}
if resp.BaseResp.StatusCode != 0 {
return errno.NewErrNo(resp.BaseResp.StatusCode, resp.BaseResp.StatusMessage)
}
return nil
}
Kitex Server关键代码讲解
create_note.go
// Copyright 2021 CloudWeGo Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
package service
import (
"context"
"github.com/cloudwego/kitex-examples/bizdemo/easy_note/kitex_gen/notedemo"
"github.com/cloudwego/kitex-examples/bizdemo/easy_note/cmd/note/dal/db"
)
type CreateNoteService struct {
ctx context.Context
}
// NewCreateNoteService new CreateNoteService
func NewCreateNoteService(ctx context.Context) *CreateNoteService {
return &CreateNoteService{ctx: ctx}
}
// CreateNote create note info
func (s *CreateNoteService) CreateNote(req *notedemo.CreateNoteRequest) error {
noteModel := &db.Note{
UserID: req.UserId,
Title: req.Title,
Content: req.Content,
}
return db.CreateNote(s.ctx, []*db.Note{noteModel})
}
Gorm关键代码讲解
note.go
// Copyright 2021 CloudWeGo Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
package db
import (
"context"
"github.com/cloudwego/kitex-examples/bizdemo/easy_note/pkg/constants"
"gorm.io/gorm"
)
type Note struct {
gorm.Model
UserID int64 `json:"user_id"`
Title string `json:"title"`
Content string `json:"content"`
}
func (n *Note) TableName() string {
return constants.NoteTableName
}
// CreateNote create note info
func CreateNote(ctx context.Context, notes []*Note) error {
if err := DB.WithContext(ctx).Create(notes).Error; err != nil {
return err
}
return nil
}
// MGetNotes multiple get list of note info
func MGetNotes(ctx context.Context, noteIDs []int64) ([]*Note, error) {
var res []*Note
if len(noteIDs) == 0 {
return res, nil
}
if err := DB.WithContext(ctx).Where("id in ?", noteIDs).Find(&res).Error; err != nil {
return res, err
}
return res, nil
}
// UpdateNote update note info
func UpdateNote(ctx context.Context, noteID, userID int64, title, content *string) error {
params := map[string]interface{}{}
if title != nil {
params["title"] = *title
}
if content != nil {
params["content"] = *content
}
return DB.WithContext(ctx).Model(&Note{}).Where("id = ? and user_id = ?", noteID, userID).
Updates(params).Error
}
// DeleteNote delete note info
func DeleteNote(ctx context.Context, noteID, userID int64) error {
return DB.WithContext(ctx).Where("id = ? and user_id = ? ", noteID, userID).Delete(&Note{}).Error
}
// QueryNote query list of note info
func QueryNote(ctx context.Context, userID int64, searchKey *string, limit, offset int) ([]*Note, int64, error) {
var total int64
var res []*Note
conn := DB.WithContext(ctx).Model(&Note{}).Where("user_id = ?", userID)
if searchKey != nil {
conn = conn.Where("title like ?", "%"+*searchKey+"%")
}
if err := conn.Count(&total).Error; err != nil {
return res, total, err
}
if err := conn.Limit(limit).Offset(offset).Find(&res).Error; err != nil {
return res, total, err
}
return res, total, nil
}