入门GO语言-Go 框架三件套详解(Web/RPC/ORM) | 青训笔记

166 阅读10分钟

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

入门GO语言-Go 框架三件套详解(Web/RPC/ORM)

三件套介绍

Gorm

Gorm是一个已经迭代了10年+的功能强大的 ORM框架,在字节内部被广泛使用并且拥有非常丰富的开源扩展。

Kitex

Kitex是字节内部的Golang微服务RPC框架,具有高性能、强可扩展的主要特点,支持多协议并且拥有丰富的开源扩展。

Hertz

Hertz是字节内部的HTTP框架,参考了其他开源框架的优势,结合字节跳动内部的需求,具有高易用性、高性能、高扩展性特点。

三件套的使用

Gorm 的基础使用

安装

  • go get -u gorm.io/gorm
  • go 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@latest
  • go 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定义的方法 image.png

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

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

  1. 网络库Netpoll
  2. Json编解码Sonic
  3. 使用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…
丰富的示例代码与业务Demogithub.com/cloudwego/h…

实战案例介绍

项目介绍

  • 笔记项目是一个使用Hertz 、Kitex、Gorm搭建出来的具备一定业务逻辑的后端API项目
服务名称服务介绍传输协议主要技术栈
demoapiAPI服务HTTPrpc/Hertz
demouser用户数据管理ProtobufGorm/Kitex
demonote笔记数据管理ThriftGorm/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(&noteVar); 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(), &notedemo.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
}