这是我参与「第五届青训营 」伴学笔记创作活动的第 5 天,我学习了Gorm、Kitex、Hertz三个在Go开发里非常重要的框架,下面是我此次学习的收获
Go框架三件套(Web/RPC/ORM)
1.1 Gorm
1.1.1 介绍
Gorm是一个已经迭代十年以上的功能强大的ORM框架,在字节内部被广泛使用且拥有非常丰富的开源拓展
1.1.2 基本使用
- 定义model也就是一个表
type Product struct {
ID int
Name string
Price float64
}
- 给表命名
func (p *Product) TableName() string {
return "product"
}
- 连接数据库
db, err := gorm.Open(mysql.Open("user:password@tcp(127.0.0.1:3306)/demo?charset=utf8mb4&parseTime=True&loc=Local"), &gorm.Config{})
if err != nil {
panic("failed to connect because")
}
- 创建数据
db.Create(&Product{ID: 1, Name: "apple", Price: 5.5})
- 查询数据
db.First(&product, 1)//按照主键获取第一条记录
db.First(&product, "id = ?", 1)//根据表达式查询
- 更新数据
//update-单条数据更新
db.Model(&product).Update("Price", 7.5)
//updates-多条数据更新
db.Model(&product).Updates(Product{ID: 2, Price: 7.5})//仅支持非零字段
db.Model(&product).Updates(map[string]interface{}{"ID": 2, "Price": 7.5})//支持所有字段
- 删除数据
db.Delete(&product, 1)
这些是基本的使用,还存在很多填充的地方,比如update还没加where条件,下面一一展示具体的实现
1.1.3 创建数据
创建数据下面我由一段代码简单介绍
type Product struct {
ID int `gorm:"primarykey"`
Name string `gorm:"column:name"`
Price float64 `gorm:"column:price"`
}
func (p *Product) TableName() string {
return "product"
}
func main() {
db, err := gorm.Open(mysql.Open("rainyday:123456@tcp(127.0.0.1:3306)/demo?charset=utf8mb4&parseTime=True&loc=Local"), &gorm.Config{})
if err != nil {
panic("failed to connect")
}
//创建一条数据
p := &Product{Name: "pear"}
res := db.Create(p)
fmt.Println(res.Error)
fmt.Println(p.ID)
//创建多条数据
products := []*Product{{Name: "orange"}, {Name: "banana"}}
res = db.Create(products)
fmt.Println(res.Error)
for _, p := range products {
fmt.Println(p.ID)
}
}
上述代码介绍了创建单条和多条数据的方法,比较简单,但是需要注意怎么处理数据冲突,可以用clause.OnConflict处理,代码如下
p := &Product{Name: "pitaya", ID: 1}
db.Clauses(clause.OnConflict{DoNothing: true}).Create(&p)
1.1.4 查询数据
下面用代码介绍
//查询第一条数据,按照主键升序,查询不到返回ErrRecordNotFound
user := &User{}
db.First(user)
//查询多条数据
users := make([]*User, 0)
res := db.Where("age > 20").Find(&users)
fmt.Println(res.RowsAffected)
fmt.Println(res.Error)
//IN操作
db.Where("username IN ?", []string{"张三", "张四"}).Find(&users)
//LIKE操作
db.Where("username LIKE ?", "张%").Find(&user)
//AND操作
db.Where("username = ? AND age >= ?", "张三", "19").Find(&users)
//结构体查询方式
db.Where(&User{Username: "张三", Age: 20}).Find(&users)
//map查询方式
db.Where(map[string]interface{}{"Username": "张三", "Age": 20}).Find(&users)
First和Find的区别在于,前者查不到数据会返回ErrRecordNotFound,后者查询多条时不会返回错误;还有就是结构体查询方式和map查询方式的区别是,前者只支持查询非零值字段,后者都能查询
1.1.5 删除数据
删除数据分为物理删除和软删除,代码如下
物理删除
db.Delete(&User{}, 3)
db.Delete(&User{}, "3")
db.Delete(&User{}, []int{1, 2, 3})
db.Where("username LIKE ?", "张%").Delete(&User{})
db.Delete(User{}, "username LIKE ?", "李%")
软删
//删除一条
u := User{ID: 3}
db.Delete(&u)
//批量删除
db.Where("age = ?", "19").Delete(&User{})//删除一条
u := User{ID: 3}
db.Delete(&u)
//批量删除
db.Where("age = ?", "19").Delete(&User{})
值得注意的是拥有软删能力的Model删除数据时,记录不会从数据库中真正的删除,gorm会将deleted设置为当前时间,可以通过Unscoped函数查询到被软删的数据
//查询时会忽略被软删的记录
user := make([]*User, 0)
db.Where("age = 19").Find(&user)
//查询时间不忽略软删的记录
db.Unscoped().Where("age = 19").Find(&user)
1.1.6 事务
下面介绍Gorm的事务的实现方法
//开始事务
tx := db.Begin()
/*
在事务中执行的db操作,这里使用tx而不是db
*/
//回滚事务
if err = tx.Create(&User{Username: "张三"}).Error; err != nil {
tx.Rollback()
return
}
//提交事务
tx.Commit()
但是Gorm还贴心的提供了Tansaction方法用于自动提交事务,避免用户漏写Commit和Rollback
if err = db.Transaction(func(tx *gorm.DB) error {
if err = tx.Create(&User{Username: "张三"}).Error;
err != nil {
return err
}
return nil
}); err != nil {
return
}
当返回err的时候自动回滚,返回nil则自动提交
1.1.7 Hook
Gorm也提供了curd的hook能力,hook是在创建、查询、更新、删除等操作之前和之后调用的函数,一旦hook返回错误,Gorm将停止后续的操作并回滚事务。具体代码如下
func (u *User) BeforeCreate(tx *gorm.DB) (err error) {
if u.Age < 0 {
return errors.New("can't save invalid data")
}
return
}
func (u *User) AfterCreate(tx *gorm.DB) (err error) {
return tx.Create(&Email{ID: u.ID, Email: u.Username + "@qq.com"}).Error
}
1.1.8 性能提高
对于写操作,为了确保数据的完整性,Gorm会将它们封装在事务内运行,但是这hi降低性能,可以使用SkipDefaultTransaction关闭默认事务,此外使用PrepareStmt缓存预编译语句也可以提高后续调用的速度,大约提高35%左右
db, err := gorm.Open(mysql.Open("rainyday:123456@tcp(127.0.0.1:3306)/demo?charset=utf8mb4&parseTime=True&loc=Local"), &gorm.Config{
SkipDefaultTransaction: true,
PrepareStmt: true,
})
if err != nil {
panic("failed to connect")
}
1.2 Kitex
1.2.1 介绍
Kitex是一个字节开源的高性能RPC框架,在字节内部广泛使用
1.2.2 定义IDL
使用IDL定义服务与接口,因为我们要进行RPC就需要知道对方的接口是什么,也需要知道传递的参数和返回值是什么样子的,这就需要通过IDL来约定双方的协议,就像写代码时调用某个函数我们需要知道函数签名一样
namespace go api
struct Request {
1:string message
}
struct Response {
1:string message
}
service Echo {
Response echo(1:Request req)
}
6.2.3 目录结构
主要的目录结构内容如下:
build.sh:构建脚本
kitex_gen:IDL内容相关的生成代码,主要是基础的Server/Client代码
main.go:程序入口
handler.go:用户在该文件里实现IDL service定义的方法
1.2.4 Kitex基本使用
服务是默认监听8888端口
下面是handle.go自动生成的代码,根据参数和返回类型即可基本的使用Kitex
//EchoImpl implements the last service interface defined in the IDL
type EchoImpl structs {
}
//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
}
1.2.5 Kitex Client发起请求
创建Client
c, err := echo.NewClient("example", client.WithHostPorts("0.0.0.0:8888"))
if err != nil {
log.Fatal(err)
}
example是目标服务名,在这种使用方式的时候是可有可无的
发起请求
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)
根据前面的Echo函数传递参数即可
1.2.6 服务注册与发现
Kitex的服务注册与发现已经对接了主流的服务注册于发现中心,例如ETCD, Nacos等,服务注册与发现相当于IP直连,比走代理的方式要好很多
服务注册
type HelloImpl struct{}
func (h *HelloImpl) Echo(ctx context.Context,req*api.Request)(resp *api .Response,err error) {
resp = &api.Response{
Message: req.Message,
}
return
}
func main() {
r, err := etcd.NewEtcdRegistry([]string{"127.0.0.1:2379"})
if err != nil {
log.Fatal(err)
}
server := hello.NewServer(new(HelloImpl), server.WithRegistry(r),
server.WithServerBasicInfo(&pcinfo.EndpointBasicInfo{
ServiceName: "Hello",
}))
err = server.Run()
if err != nil {
log.Fatal(err)
}
}
服务发现
func main() {
r, err := etcd.NewEtcdResolver([]string{"127.0.0.1:2379"})
if err != nil {
log.Fatal(err)
}
client := hello.MustNewClient("Hello", client.WithResolver(r))
resp, err := client.Echo(ctx, &api.Request{Message: "Hello"})
cancel()
if err != nil {
log.Fatal(err)
}
log.Println(resp)
time.Sleep(time.Second)
}
服务发现的目标服务名是必须指定的,因为存在多个注册对象,此外kitex会个etcd加一道缓存,在请求过一次后会设置缓存,并且存在异步的更新和删除对象的逻辑,会定时删除和更新,所以损耗会很小
1.3 Hertz
1.3.1 介绍
hertz是一个字节开源的web框架,具有高性能、高易用性和高拓展性的优势,在字节内部已广泛使用
1.3.2 基本使用
由一段代码说明
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{"ping": "pong"})
})
h.Spin()
}
该代码服务监听8080端口并注册了一个GET方法的路由函数
1.3.3 路由
Hertz提供了GET、POST、PUT、DELETE、ANY等方法用于注册路由
func RegisterRoute(h *server.Hertz) {
h.GET("/get", func(c context.Context, ctx *app.RequestContext) {
ctx.String(consts.StatusOK, "get")
})
h.POST("/post", func(c context.Context, ctx *app.RequestContext) {
ctx.String(consts.StatusOK, "post")
})
h.PUT("/put", func(c context.Context, ctx *app.RequestContext) {
ctx.String(consts.StatusOK, "put")
})
h.DELETE("/delete", func(c context.Context, ctx *app.RequestContext) {
ctx.String(consts.StatusOK, "delete")
})
h.PATCH("/patch", func(c context.Context, ctx *app.RequestContext) {
ctx.String(consts.StatusOK, "patch")
})
h.HEAD("/head", func(c context.Context, ctx *app.RequestContext) {
ctx.String(consts.StatusOK, "head")
})
h.OPTIONS("/options", func(c context.Context, ctx *app.RequestContext) {
ctx.String(consts.StatusOK, "options")
})
}
此外还提供了路由组的能力,用于支持路由分组的功能
v1 := h.Group("/v1")
{
//loginEndpoint是一个handler函数
v1.POST("/login", loginEndpoint)
v1.POST("/submit", submitEndpoint)
v1.POST("/streaming_read", readEndpoint)
}
v2 := h.Group("/v2")
{
v2.POST("/login", loginEndpoint)
v2.POST("/submit", submitEndpoint)
v2.POST("/streaming_read", readEndpoint)
}
Hertz还提供了参数路由和通配路由,路由的优先级为:静态路由>命名路由>通配路由
参数路由
// This handler will match: "/hertz/version", but will not match : "/hertz/" or "/hertz"
h.GET("/hertz/:version", func(ctx context.Context, c *app.RequestContext) {
version := c.Param("version")
c.String(consts.StatusOK, "Hello %s", version)
})
通配路由
// However, this one will match "/hertz/v1/" and "/hertz/v2/send"
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)
})
1.3.4 参数绑定
hertz提供了Bind、Validate、BindAndValidate函数用于进行参数绑定和校验
type Args struct {
Query string `query:"query"`
QuerySlice []string `query:"q"`
Path string `path:"path"`
Header string `header:"header"`
Form string `form:"form"`
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()
}
1.3.5 中间件
hertz的中间件分为客户端中间件和服务端中间件,下面由一段代码展示服务端中间件
func MyMiddleware() app.HandlerFunc {
return func(c context.Context, ctx *app.RequestContext) {
//pre-handle,每次执行前打印
fmt.Println("pre-handle")
//相当于洋葱模型,向下执行handler或者中间件,如果不需要可以不写
ctx.Next(c)
//post-handler,每次执行完打印
fmt.Println("post-handle")
}
}
func main() {
h := server.Default(server.WithHostPorts("127.0.0.1:8080"))
h.Use(MyMiddleware())
h.GET("/middleware", func(c context.Context, ctx *app.RequestContext) {
ctx.String(consts.StatusOK, "Hello hertz!")
})
h.Spin()
}
终止中间件调用链的执行的方法是:
ctx.Abort、c.AbortWithMsg、c.AbortWithStats
1.3.6 Client
Hertz提供了HTTP Client用于帮助用户发送HTTP请求
func main() {
c, err := client.NewClient()
if err != nil {
return
}
//send ttp get request
status, body, err := c.Get(context.Background(), nil, "http:///www.example.com")
if err != nil {
return
}
fmt.Printf("status = %v body = %v\n", status, string(body))
//send http post request
var postArgs protocol.Args
//set post args
postArgs.Set("arg", "rainyday")
status, body, err = c.Get(context.Background(), nil, "http:///www.example.com")
if err != nil {
return
}
fmt.Printf("status = %v body = %v\n", status, string(body))
}
1.3.7 代码生成工具
Hertz提供了代码生成工具Hz,通过定义kitex提到的IDL文件即可生成对应的基础服务代码
namespace go hello.example
struct HelloReq {
1:string Name (api.query="name);
}
struct HelloResp {
1:string RespBody;
}
service HelloService {
HelloResp HelloMethod(1:HelloReq request) (api.get="/hellp")
}