Go框架三件套 | 青训营笔记

120 阅读4分钟

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

GORM

什么是ORM?

类比理解:在 Java 中,常见的 ORM 框架有 Mybatis, MyBatis-Plus, Hibernate 等。

ORM就是对象关系映射,是一种用于在关系数据库和面向对象的编程语言堆之间转换数据的编程技术。举例来说就是,我定义一个对象,那就对应着一张表,这个对象的实例,就对应着表中的一条记录。

GORM的特点

GORM是Golang语言中一款性能极好的ORM库,对开发人员相对是比较友好的。通过 GORM 技术,可以通过修改类/结构体实例的方式轻易的完成数据库增删改查(CRUD)的任务。在GORM中,与Java中最常用的Mybatis不同,它在尽量不接触 SQL 语句的情况下操作数据库。

GORM增删改查

在开始前,需要安装 GORM 及需要连接对应数据库的驱动。以下命令通过 Go Module 拉取并添加 Gorm 及 MySQL 数据库驱动:

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

在代码开头,导入GORM及MySQL的依赖:

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

随后,我们需要声明一个数据库模型。模型是标准的 struct,由 Go 的基本数据类型。数据库模型的结构将被对应到数据表中:

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

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

type Product struct {
    ID      uint    `gorm:"primarykey"`
    Code    string  `gorm:"column: code"`
    Price   uint    `gorm:"column: user_id"`
}

下面的操作就是数据库连接操作,以及对数据库的增删改查操作,列出几个例子如下:

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")
}
  
// Create
db.Create(&Product{Code: "D42", Price: 100})

// Get first matched record
db.Where("name = ?", "jinzhu").First(&user)
// SELECT * FROM users WHERE name = 'jinzhu' ORDER BY id LIMIT 1;

// Get all matched records
db.Where("name <> ?", "jinzhu").Find(&users)
// SELECT * FROM users WHERE name <> 'jinzhu';

// IN
db.Where("name IN ?", []string{"jinzhu", "jinzhu 2"}).Find(&users)
// SELECT * FROM users WHERE name IN ('jinzhu','jinzhu 2');

// LIKE
db.Where("name LIKE ?", "%jin%").Find(&users)
// SELECT * FROM users WHERE name LIKE '%jin%';

// AND
db.Where("name = ? AND age >= ?", "jinzhu", "22").Find(&users)
// SELECT * FROM users WHERE name = 'jinzhu' AND age >= 22;

// Time
db.Where("updated_at > ?", lastWeek).Find(&users)
// SELECT * FROM users WHERE updated_at > '2000-01-01 00:00:00';

// BETWEEN
db.Where("created_at BETWEEN ? AND ?", lastWeek, today).Find(&users)
// SELECT * FROM users WHERE created_at BETWEEN '2000-01-01 00:00:00' AND '2000-01-08 00:00:00';


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

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

创建 | GORM - The fantastic ORM library for Golang, aims to be developer friendly.

GORM事务操作

数据库事务(transaction) 是访问并可能操作各种数据项]的一个数据库操作序列,这些操作要么全部执行,要么全部不执行,是一个不可分割的工作单位。事务由事务开始与事务结束之间执行的全部数据库操作组成。

首先是数据库的手动手动事务操作:

db, err := gorm,Open(mysql.0pen( dsn: "username:password@tcp(localhost:9910)/database?charset=utf8")
    &gorm.Config{})
if err != nil {
    panic( v:"failed to connect database")
}
tx := db,Begin() // 开始事务
//在事务中执行一些 db 操作 (从这里开始,您应该使用tx’ 而不是db)
if err = tx.Create(&User{Name: "name"}).Error; err != nil {
    tx.RolTback(//遇到错误时回滚事务
    return
}
if err = tx.Create(&User(Name: "namel"]).Error; err != nil {
    tx.Rollback()
    return
}
//提交事务
tx.Commit()

另外还有可以类比Java中@Transactional注解的更为方便的操作方式:

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

GORM HOOK

Hook 是在创建、查询、更新、删除等操作之前、之后调用的函数。这很像 Spring Boot 遵循的 AOP(Aspect Oriented Programming,面向切面编程) ,方法被以一种约定的方式织入数据库操作逻辑中。

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
}

Hook | GORM - The fantastic ORM library for Golang, aims to be developer friendly. 

Kitex

什么是RPC?

RPC(Remote Procedure Call Protocol)远程过程调用协议。一个通俗的描述是:客户端在不知道调用细节的情况下,调用存在于远程计算机上的某个对象,就像调用本地应用程序中的对象一样。

所以说RPC是一种协议一套规范,用来远程过程调用的。也就是说两台服务器A,B,一个应用部署在A服务器上,想要调用B服务器上应用提供的函数/方法,由于不在一个内存空间,不能直接调用,需要通过网络来表达调用的语义和传达调用的数据。

IDL

Kitex 是一个 RPC 框架,既然是 RPC,底层就需要两大功能:

  1. Serialization 序列化
  2. Transport 传输

Kitex 框架及命令行工具,默认支持 ​thrift ​和 ​proto3 ​两种 IDL,对应的 Kitex 支持 ​thrift ​和 ​protobuf ​两种序列化协议。传输上 Kitex 使用扩展的 ​thrift ​作为底层的传输协议(注:thrift 既是 IDL 格式,同时也是序列化协议和传输协议)。

IDL(Interface definition language) 是一种语言的通用术语,它允许用一种语言编写的程序或对象与用未知语言编写的另一个程序进行通信。我们可以使用 IDL 来支持 RPC 的信息传输定义。

Kitex使用(服务端)

Kitex 目前对 Windows 的支持并不完善,建议使用虚拟机或 WSL2 进行测试。

要开始 Kitex 开发,首先需要安装 Kitex 代码生成工具(在此之前,请务必检查已正确设置 GOPATH 环境变量,并将 $GOPATH/bin 添加到 PATH 环境变量中):

go install github.com/cloudwego/kitex/tool/cmd/kitex@latest
​
go install github.com/cloudwego/thriftgo@latest

定义 IDL,命名为 echo.thrift

namespace go api
​
struct Request {
    1: string message
}
​
struct Resposne {
    1: string message
}
​
service Echo {
    Reponse echo(1: Request req)
}

使用以下指令为我们的回声服务生成代码:

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 内容相关的生成代码,main.go 为程序入口,handler.go 可由用户在此文件内实现 IDL service 定义的方法。

我们需要编写的服务端逻辑都在 ​handler.go​ 这个文件中,现在这个文件应该如下所示:

package main
​
import (
        "context"
        api "exmaple/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
}

这里的 ​Echo ​函数就对应了我们之前在 IDL 中定义的 ​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 ​服务就开始运行啦!

Kitex使用(服务端)

接下来,我们通过创建一个客户端来调用我们的回声服务。以下项目代码假设您已正确导入上文中生成的回声服务代码。新建项目并创建 main.go 文件,编写代码:

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

上述代码中,​echo.NewClient​ 用于创建 ​client​,其第一个参数为调用的服务名,第二个参数为 options,用于传入参数,此处的 ​client.WithHostPorts​ 用于指定服务端的地址。

接下来编写发起调用的代码:

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)

上述代码中,我们首先创建了一个请求 ​req ​, 然后通过 ​c.Echo​ 发起了调用。 其第一个参数为 ​context.Context​,通过通常用其传递信息或者控制本次调用的一些行为。 其第二个参数为本次调用的请求。 其第三个参数为本次调用的 ​options ​,Kitex 提供了一种 ​callopt ​机制,顾名思义——调用参数 ,有别于创建 client 时传入的参数,这里传入的参数仅对此次生效。 此处的 ​callopt.WithRPCTimeout​ 用于指定此次调用的超时。

在编写完一个简单的客户端后,终于可以发起调用了,使用如下命令:

$ go run main.go

就可以成功编写了一个 Kitex 的服务端和客户端,并完成了一次调用。

Hertz

HTTP协议

HTTP 协议是当今使用最为广泛的协议之一,HTTP 是前(客户)端与服务端通信的基础协议。HTTP 框架负责的就是对 HTTP 请求的解析、根据对应的路由选择对应的后端逻辑了,HTTP 在企业实际业务场景中使用广泛。 而Hertz是一个用于 Go的高性能高可用性可扩展的HTTP 框架。它旨在为开发人员简化构建微服务。

什么是路由?

路由是指导报文转发的路径信息,通过路由可以确认转发IP报文的路径。 路由设备是依据路由转发报文到目的网段的网络设备,最常见的路由设备:路由器。 路由设备维护着一张路由表,保存着路由信息。说白了就是对应关系的集合

路由中包含以下信息: 

  • 目的网络:标识目的网段 
  • 掩码:与目的地址共同标识一个网段 
  • 出接口:数据包被路由后离开本路由器的接口 
  • 下一跳:路由器转发到达目的网段的数据包所使用的下一跳地址 

这些信息标识了目的网段、明确了转发IP报文的路径。

Hertz使用

安装命令行工具 hz

go install github.com/cloudwego/hertz/cmd/hz@latest

使用 hz new 生成代码,然后使用 go mod tidy 拉取依赖。

main.go 文件,编写以下代码:

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.WithHostPoerts("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()
}

创建了一个 HTTP 服务端,服务监听 8080 端口并注册了一个 GET 方法的路由函数。

Hertz 提供了 GET,POST,PUT,DELETE,ANY 等方法用于注册对应请求方式(Reuquest Method)的路由:

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 提供了路由组( 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 提供了 HTTP Client 用于帮助用户发送 HTTP 请求:

c, err := client.NewClient()
if err != nil {
    return
}
// send http get request
status, body, _ := c.Get(context.Background(), nil, "https://www.example.com")
fmt.Printf("status=%v body=%v\n", status, string(body))
​
// send http post request
var postArgs protocol.Args
postArgs.Set("arg","a") // Set post args
status, body, _ = c.Post(context.Background(), nil, "https://www.example.com", &postArgs)
fmt.Printf("status=%v body=%v\n", status, string(body))