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

133 阅读5分钟

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

前言

本文主要介绍3个主流的Go框架Gorm、Kitex、Hertz的基本使用,覆盖了ORM、RPC、HTTP这三方面。帮助我们快速上手学习并使用它们。

Gorm

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

什么是ORM

ORM 是 Object Relational Mapping 的缩写,译为“对象关系映射”,它解决了对象和关系型数据库之间的数据交互问题。

使用面向对象编程时,数据很多时候都存储在对象里面,具体来说是存储在对象的各个属性(也称成员变量)中。例如有一个 User 类,它的 id、username、password、email 属性都可以用来记录用户信息。当我们需要把对象中的数据存储到数据库时,按照传统思路,就得手动编写 SQL 语句,将对象的属性值提取到 SQL 语句中,然后再调用相关方法执行 SQL 语句。

而有了 ORM 技术以后,只要提前配置好对象和数据库之间的映射关系,ORM 就可以自动生成 SQL 语句,并将对象中的数据自动存储到数据库中,整个过程不需要人工干预。在 Java 中,ORM 一般使用 XML 或者注解来配置对象和数据库之间的映射关系。

Gorm基本使用

Gorm官方支持的数据库类型有:MySQL、SQLServer、PostgreSQL、SQLite。 首先,安装Gorm,连接数据库。

go get -u gorm.io/gorm
go get -u gorm.io/driver/mysql

快速入门代码如下,以MySQL为例:

package main

import (
  "gorm.io/gorm"
  "gorm.io/driver/mysql"
)

type Product struct {
  gorm.Model
  Code  string
  Price uint
}

func main() {
    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{})
  if err != nil {
    panic("failed to connect database")
  }

  // 迁移 schema
  db.AutoMigrate(&Product{})

  // 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增删改查

创建数据:

image.png

查询数据:

image.png 更新数据:

image.png

删除数据:

// 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";

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);

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%";

需要注意的是,如果我们指定了 gorm.deletedat 字段(gorm.Model 包含该字段),将启用软删除模式:这意味着,改数据模型调用 Delete 方法时,并不会被真正从数据表中删除,而是会设置 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;

也可以使用Unscoped永久删除匹配的记录:

db.Unscoped().Delete(&order)
// DELETE FROM orders WHERE id=10;

Gorm 事务

为了确保数据一致性,GORM 会在事务里执行写入操作(创建、更新、删除)。如果没有这方面的要求,您可以在初始化时禁用它,这将获得大约 30%+ 性能提升。

// 全局禁用 
db, err := gorm.Open(sqlite.Open("gorm.db"), &gorm.Config{   
SkipDefaultTransaction: true, 
})

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

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
})

手动事务:

// 开始事务
tx := db.Begin()

// 在事务中执行一些 db 操作(从这里开始,您应该使用 'tx' 而不是 'db')
tx.Create(...)

// ...

// 遇到错误时回滚事务
tx.Rollback()

// 否则,提交事务
tx.Commit()

Gorm Hook

Hook 是在创建、查询、更新、删除等操作之前、之后调用的函数。

如果您已经为模型定义了指定的方法,它会在创建、更新、查询、删除时自动被调用。如果任何回调返回错误,GORM 将停止后续的操作并回滚事务。

钩子方法的函数签名应该是 func(*gorm.DB) erro

type User struct {
    ID      int64
    Name    string `gorm:"default:galeone"`
    Age     string `gorm:"default:18"`
}

type Email struct {
    ID      int64
    Name    string
    Email   string
}

func (u *User) BeforeCreate(tx `gorm.DB) (err error) {
    if u.Age < 0 {
        return errors.New("can't save invalid data")
    }
    return nil;
}

func (u *User) AfterCreate(tx `gorm.DB) (err error) {
    return tx.Create(&Email{ID: u.ID, Email: i.Name + "@***.com"}).Error
}

Kitex

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

什么是RPC

RPC 全称是 Remote Procedure Call ,即远程过程调用,其对应的是我们的本地调用。远程其实指的就是需要网络通信,可以理解为调用远程机器上的方法。

Kitex基本使用

Kitex暂时没有针对Windows做支持,如果本地开发环境是Windows建议使用WSL2。 首先,我们 需要安装Kitex代码生成工具

  1. 确保 GOPATH 环境变量已经被正确地定义(例如 export GOPATH=~/go)并且将$GOPATH/bin添加到 PATH 环境变量之中(例如 export PATH=$GOPATH/bin:$PATH);请勿将 GOPATH 设置为当前用户没有读写权限的目录 2.安装kitex和thriftgo。
go install github.com/cloudwego/kitex/tool/cmd/kitex@latest

go install github.com/cloudwego/thriftgo@latest

安装成功后,执行 kitex --version 和 thriftgo --version 应该能够看到具体版本号的输出:

$ kitex --version
vx.x.x

$ thriftgo --version
thriftgo x.x.x

接着,定义IDL,并命名为echo.thirift.

namespace go api

struct Request {
    1: string message
}

struct Resposne {
    1: string message
}

service Echo {
    Reponse echo(1: Request req)
}

Thrift和proto3语法参考下列引用。

然后,通过上面定义的echo服务,使用下面指令生成代码:

kitex -module exmaple -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

其中 build.sh 为构建脚本;kitex_gen 为 IDL 内容相关的生成代码,主要是基础的Server/Client代码;main.go 为程序入口,handler.go 为用户在该文件实现 IDL service 定义的方法。

我们需要编写的服务端逻辑都在 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
}

修改 handler.go 内的 Echo 函数为下述代码来实现我们的 Echo 服务逻辑:

func (s *EchoImpl) Echo(ctx context.Context, req *api.Request) (resp *api.Response, err error) {
  return &api.Response{Message: req.Message}, nil
}

运行$ sh build.sh进行编译,执行上述命令后,会生成一个 output 目录,里面含有我们的编译产物。 最后运行$ sh output/bootstrap.sh执行上述命令后,Echo 服务就开始运行啦。服务会在默认的8888端口上运行。 要想修改端口,需打开main.go,在NewServer函数中修改端口参数:

 addr, _ := net.ResolveTCPAddr("tcp", "127.0.0.1:7777")
  svr := api.NewServer(new(EchoImpl), server.WithServiceAddr(addr))

Kitex 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)
}

然后发起调用,代码如下:

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)

在编写完一个简单的客户端后,我们终于可以发起调用了。

你可以通过下述命令来完成这一步骤:

$ go run main.go

如果不出意外,你可以看到类似如下输出:

2021/05/20 16:51:35 Response({Message:my request})

Hertz

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

Hertz基本使用

安装命令行工具 hz:

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

3.通过hz new生成代码,

4.创建main.go文件,并添加以下代码 使用Hertz实现,服务监听8080端口并注册了一个GGET方法的路由函数。

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()
}

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 main(){
    h := server.Default(server.WithHostPorts("127.0.0.1:8080"))

    h.StaticFS("/", &app.FS{Root: "./", GenerateIndexPages: true})

    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")
    })
    h.Any("/ping_any", func(ctx context.Context, c *app.RequestContext) {
        c.String(consts.StatusOK, "any")
    })
    h.Handle("LOAD","/load", func(ctx context.Context, c *app.RequestContext) {
        c.String(consts.StatusOK, "load")
    })
    h.Spin()
}

Hertz路由组

Hertz 提供了路由组( Group )的能力,用于支持路由分组的功能,同时中间件也可以注册到路由组上。

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 main(){
	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, "get")
	})
	v1.POST("/post", func(ctx context.Context, c *app.RequestContext) {
		c.String(consts.StatusOK, "post")
	})
	v2 := h.Group("/v2")
	v2.PUT("/put", func(ctx context.Context, c *app.RequestContext) {
		c.String(consts.StatusOK, "put")
	})
	v2.DELETE("/delete", func(ctx context.Context, c *app.RequestContext) {
		c.String(consts.StatusOK, "delete")
	})
	h.Spin()
}

Hertz路由类型

Hertz提供了参数路由和通配路由,路由的优先级为:静态路由(如上)>命名路由>通配路由

参数路由

Hertz 支持使用 :name 这样的命名参数设置路由,并且命名参数只匹配单个路径段。

如果我们设置/user/:name路由,匹配情况如下

路径是否匹配
/user/gordon匹配
/user/you匹配
/user/gordon/profile不匹配
/user/不匹配

通过使用 RequestContext.Param 方法,我们可以获取路由中携带的参数。

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 main(){
	h := server.Default(server.WithHostPorts("127.0.0.1:8080"))
	// 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)
	})
	h.Spin()
}

通配路由

Hertz 支持使用 *path 这样的通配参数设置路由,并且通配参数会匹配所有内容。

如果我们设置/src/*path路由,匹配情况如下

路径是否匹配
/src/匹配
/src/somefile.go匹配
/src/subdir/somefile.go匹配

通过使用 RequestContext.Param 方法,我们可以获取路由中携带的参数。

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 main(){
	h := server.Default(server.WithHostPorts("127.0.0.1:8080"))
	// 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)
	})
	h.Spin()
}

Hertz参数绑定

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

func main() {
	r := server.New()

    r.GET("/hello", func(c context.Context, ctx *app.RequestContext) {
        // 参数绑定需要配合特定的go tag使用
		type Test struct {
            A string `query:"a" vd:"$!='Hertz'"`
        }

        // BindAndValidate
        var req Test
        err := ctx.BindAndValidate(&req)

        ...

	    // Bind
        req = Test{}
        err = ctx.Bind(&req)

        ...

        // Validate,需要使用 "vd" tag
        err = ctx.Validate(&req)

        ...
    })
...
}

Hertz中间件

Hertz的中间件主要分为客户端中间件与服务端中间件。

func MyMiddleware() app.HandlerFunc {
  return func(ctx context.Context, c *app.RequestContext) {
    // pre-handle
    // ...
    c.Next(ctx) // call the next middleware(handler)
    // post-handle
    // ...
  }
}

func main() {
    h := server.Default(server.WithHostPort("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请求。

Hertz代码生成工具

Hertz提供了代码生成工具hz,通过定义IDL(interface description language)文件即可生成对应的基础服务代码。

// idl/hello.thrift
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");
}

目录结构:

image.png

案例实战

笔记项目是一个使用Hertz、Kitex、Gorm搭建出来的具备一定业务逻辑的后端API项目。

服务名称服务介绍传输协议主要技术栈
demoapiAPI服务HTTPGorm/Kitex/Hertz
demouser用户数据管理ProtobufGorm/Kitex
demonote笔记数据管理ThriftGorm/Kitex

项目功能介绍

image.png

引用