Go框架三件套(Web/RPC/ORM) | 青训营

88 阅读4分钟

Go框架三件套详解(Web/RPC/ORM)

Gorm 的基本操作

type Product struct {
    Code string
    Price uint
}
​
func (p Product) TableName() string {
    return "product"
}
​
func main() {
    db, err := gorm.Open(
        mysql.Open("user:pass@tcp(127.0.0.1:3306)/dbname?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"})//仅更新非0字段
    db.Model(&product).Updates(map[string]interfase{}{"Price":200,"Code":"F42"})
    //Delete - 删除 product
    db.Delete(&Product,1)                                                   //删除数据
    
}

Gorm的约定(默认)

  • Gorm使用名为ID的字段作为主键
  • 使用结构体的蛇形复数作为表名
  • 字段名的蛇形作为列名
  • 使用CreatedAt,UpdatedAt字段作为创建、更新时间
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:"user_id'
}
​
func main() {
    db, err := gorm.Open(mysql.Open("username:password@tcp(localhost:9910)/database?charset=utf8"),
                         &gorm.Config{})
    if err != nil {
        panic("failed to connect database")
    }
    
    //创建一条
    p := &Product{Code: "D42"}
    res := db.Create(p)
    fmt.Println(res.Error)  //获取err
    fmt.Println(p.ID)
    
    //创建多条
    products := []*Product{{Code: "D41"},{Code: "D42"},{Code: "D43"}}
    res = db.Create(products)
    fmt.Println(res.Error)  //获取err
    for _, p := range products {
        fmt.Println(p.ID)
    }
    
}

如果涉及到唯一ID冲突,可以使用clause.OnConflict处理数据冲突

//以不处理冲突为例,创建一条数据
p := &Product{Code: "D42",ID: 1}
db.Clauses(clause.OnConflict{DoNothing: true}).Create(&p)
Gorm查询数据
db, err := gorm.Open(
        mysql.Open("user:pass@tcp(127.0.0.1:3306)/dbname?charset=utf8mb4&parseTime=True&loc=Local"),
        &gorm.Config{})                                                     //连接数据库
    if err != nil {
        panic("failed to connect database")                                 
    }
u := &User{}
//获取第一条记录(主键升序),查询不到数据则返回 ErrRecordNotFound
db.First(u)
//查询多条数据
Users := make([]*User,8)
result := db.Where("age > 10").Find(&Users)
fmt.Println(result.RowsAffected)
fmt.Println(result.Error)
​
​
  • 使用First时,需要注意查询不到数据会返回ErrRecordNotFound
  • 使用Find查询多条数据,查询不到数据不会返回错误
  • 当使用结构作为条件查询的时候,GORM只会查询非零字段。这意味着如果您的字段值为0,false或其他零值,该字段不会被用于构建插叙条件,使用Mao来构建查询条件。
GORM更新
//条件更新单列
db.Model(&User{}).Where("active = ?", true).Update("name", "hello")
​
//更新多个列
//根据'struct'更新属性,只会更新非零值的字段
db.Model(&User{ID:111}).Updates(User{Name:"hello",Age: 18})
​
//根据'map'更新属性
db.Model(&User{ID: 111}).Updates(map[string]intefase{}{"name":"hello","age": 18,"actived":false})
​
//根据选定字段
db.Model(&User{ID: 111}).Select("name").Updates.(map[string]interfase{}{"name":"hello",
                                                                        "age": 18,"actived":false})
//SQL 表达式更新
db.Model(&User{ID: 111}).Update("age", gorm.Expr("age * ? + ?",2,100))
  • 使用Struct更新时,只会更新非零值,如果需要更新零值可以使用Map更新或使用Select选择字段
GORM删除数据
物理删除
db.Delete(&User{},10)
软删除
type User struct {
    ID      int64
    Name    string  'gorm:"defalult:golcone"'
    Age     int64   'gorm:"datault:18"'
    Ddleted gorm.DeletedAt
}
​
//软删
db.Where("age = ?", 20).Delete(&User{})
​
//查询被软删的数据
db.Unscoped().Where("age = 20").Find(&users)
​
//永久删除
db.Unscoped().Delete(&order)
  • GORM提供了 gorm.DeletedAt用于帮助用户实现软删
  • 用于软删除能力的Model调用 Delete 时,记录不会被从数据库真正的删除。但是GORM会将DeletedAt置为当前时间,并且你不能再通过正常的查询找到该记录。
  • 使用Unscoped可以查询到被软删的数据

GORM事务

db, err := gorm.Open(
        mysql.Open("user:pass@tcp(127.0.0.1:3306)/dbname?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(&User{Name: "name"}).Error; err != nil {
    tx.Rollback()
    //遇到是错误时回滚事务
    return
}
​
if err = tx.Create(&User{Name: "name1"}).Error; err != nil {
    tx.Rollback()
    return
}
​
//提交事务
tx.Commit()

GORM Hook

func BeforeCreate(tx *gorm.DB) (err error) {
    if u.Age < 0 {
        return errors.New("can't save invalid data")
    }
    return
}
​
func AfterCreate(tx *gorm.DB) (err error) {
    return tx.Create(&Email{ID: u.ID, Email: u.Name + "@***.com"}).Error
}
  • GORM在提供了curd的Hook能力
  • Hook是在创建、查询、更新、删除等操作之前、之后自动调用的函数
  • 如果任何Hook返回错误,GORM将停止后续的操作并回滚事务

Gorm性能提高

对于写操作(创建、更新、删除),为了确保数据的完整性,GORM会将他们封装在事务内运行。但这会降低性能,你可以使用SkipDefaultTransaction关闭默认事务。

使用PrepareStmt缓存预编译语句可以提高后续调用的速度,本机测试提高大约35%左右。

db, err := gorm.Open(
        mysql.Open("user:pass@tcp(127.0.0.1:3306)/dbname?charset=utf8mb4&parseTime=True&loc=Local"),
        &gorm.Config{
            SkipDefaultTransaction: true,//关闭默认事务
            PrepareStmt:true,
        })                                                      //连接数据库
    if err != nil {
        panic("failed to connect database")                                 
    }

GORM生态

image-20230823160950563.png

Kitex生成代码

image-20230823165607839.png

  • build.sh:构建脚本
  • Kitex_gen:IDL内容相关的生成代码,主要是基础的Server/Clinet代码
  • mian.go 程序入口
  • handler.go用户在该文件里实现IDL service定义的方法

Kitex基本使用

服务默认监听8888端口

package main
import (
    "context"
    "example/kitex_gen/api"
)
​
type EchoImpl struct{}
​
func (s *EchoImpl) Echo(ctx context.Context,req *api.Request) (resp *api.Response,err error) {
    return
}
​

Kitex Client 发起请求

创建Client
import (
    "example/kitex_gen/api/echo"
    "github.com/cloudwego/kitex/client"
)
​
c, err := echo.NewClient("example",client.WitchHostPorts("0.0.0.0:8888"))
if err != nil {
    log.Fatal(err)
}
发起请求
import "example/kitex_gen/api"
...
req := &api.Request{Message: "my request"}
req ,err := c.Echo(context.Background(), req, callopt.WithRPCTimeout(3*time.Second))
if err != nil {
    log.Fatal(err)
}
log.Println(resp)

Kitex服务注册与发现

目前Kitex的服务注册与发现已经对接了主流的服务注册与发现中心,如ETCD,Nacos等

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(&rpcinfo.EndpointBasicInfo{
        ServiceName:"Hello",
    }))
    err = server.Run()
    if err != nil {
        log.Fatal(err)
    }
}

Kitex生态

image-20230823174110334.png

Hertz基本使用

image-20230823175135610.png

使用Hertz实现,服务监听8080端口并注册了一个GET方法的路由函数

Hertz路由

  1. Hertz提供了GET、POST、PUT、DELETE、ANY等方法用于注册路由

image-20230823175313607.png

  1. Hertz提供路由组(Group)的能力,用于支持路由分组的功能

image-20230823175440820.png

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

image-20230823175704377.png

Hertz参数绑定

Hertz提供了Bind、Validate、BindAndValidate函数用于进行参数绑定和校验

image-20230823180500952.png

Hertz中间件

Hertz的中间件主要分为客户端中间件和服务端中间件,如下展示一个服务端中间件。

func MyMiddleware() app.HandlerFunc {
    return func(ctx context.Context,c *app.RequestContext) {
        //pre-handle
        fmt.Println("pre-handle")
        c.Next(ctx)//调用下一个中间件
        //pro-handle
        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, boby, _ := c.Get(context.Background(),nil,"http://www.example.com")//这里的_没有处理err,实际开发中需要处理
fmt.Println("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.Println("status=%v body=%v\n",status,string(body))

Hertz代码生成工具

hertz提供代码生成工具Hz,通过定义IDL文件即可生成对应的基础服务代码。

image-20230823205020028.png

目录结构

image-20230823205253126.png

生成文件

image-20230823205313180.png

Hertz性能

网络库Netpoll
json编解码Sonic
使用sync.Pool 复用对象协议层数据解析优化

Hertz生态

image-20230823205621369.png