Java学渣学习Go 的三个主流开发框架| 青训营笔记

262 阅读14分钟

Java学渣学习Go 的三个主流开发框架| 青训营笔记

前言

本文介绍了三个 Go 主流开发框架 GORM,Kitex,Hertz 的基本使用方法,包含了 ORM,RPC,HTTP 三个领域,帮助读者快速入门 Go 工程开发。

GORM

GORM是一个专为Go语言开发者设计的优秀ORM库,旨在提供开发者友好的使用体验。人们对它的评价是“梦幻般的”。

了解ORM

在学习Gorm之前,建议先了解什么是ORM(对象关系映射)。ORM是一种编程技术,用于在关系数据库和面向对象的编程语言之间转换数据。它不仅限于面向对象语言,在Go语言中也有广泛的应用。通过使用ORM技术,我们可以将关系数据库的表结构映射到类或结构体上,然后通过对类或结构体实例进行修改,轻松地完成数据库的增删改查(CRUD)操作。ORM技术使得以一种更友好且高效的方式操作数据库成为可能,而无需直接编写SQL语句。

在Java中,一些常见的ORM框架包括Mybatis、MyBatis-Plus和Hibernate等。

GORM的使用

增删改查

由于GORM并不是Go标准库的一部分,因此,在使用GORM之前,我们需要先进行安装并连接相应的数据库驱动程序。GORM官方支持多种数据库类型,包括MySQL、PostgreSQL、SQLite和SQL Server。下面的命令可以通过Go Module来获取并添加GORM以及MySQL数据库驱动。

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

这样,我们就可以借助GORM连接并操作MySQL数据库了

我们定义一个数据库模型。该模型是一个结构体,由Go语言的基本数据类型,实现了扫描器(Scanner)和值处理器(Valuer)接口的自定义类型,以及它们的指针或别名组成。数据库模型的结构将与数据表进行相应的映射。

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

或许你会留意到在声明数据表结构时,似乎多了一些内容。同时,这一切似乎进展得比较迅速。

按约定编程的 GORM

如果你是一位Java开发者,并且有使用过Spring Boot的经验,你一定会惊讶地发现开发Spring Boot应用程序是多么简单。它不仅仅是开箱即用,所有的一切都已经由Spring Boot帮你进行了配置......实际上,这就是约定优于配置(Convention over Configuration)的原则,这是一种软件设计范例,它旨在减少使用框架的开发者需要做出的决策的数量,同时保持灵活性。GORM也采用了这种设计原则,这意味着:

默认情况下,GORM使用ID作为主键,使用结构体名的蛇形复数作为表名,字段名的蛇形作为列名,并使用CreatedAt和UpdatedAt字段来跟踪创建和更新时间。

因此,当我们在数据模型中指定gorm.Model时,ID、CreatedAt、UpdatedAt和DeletedAt字段会被自动创建,并按照其名称的含义进行工作。例如,CreatedAt字段会在我们创建一条记录时自动填充创建时间。当然,你也可以尝试手动将这些字段添加到你的数据模型中,以下的结构体与前述的结构体具有相同的效果:

type Product struct {
    Code      string
    Price     uint
    ID        uint `gorm:"primarykey"`
    CreatedAt time.Time
    UpdatedAt time.Time
    DeletedAt sql.NullTime `gorm:"index"`
}

当然,为了保持简洁,我们仍然建议您采用上述的写法。

遵循GORM的约定将减少您需要编写的配置和代码量。然而,如果这些约定不符合您的实际需求,GORM也允许您进行自定义配置。例如,您可以使用以下方法为字段指定默认值:

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"`
}

至此,我们返回来看上文代码,在 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")
  }

通过调用 gorm.Open 方法,我们可以打开一个数据库连接,并对可能发生的异常进行处理。

在这里,我们使用 mysql.Open 函数,并传入了一个看起来有点奇怪的字符串,该字符串被称为 DSN(数据源名称),它包含了关于ODBC(开放式数据库连接)驱动程序连接到特定数据库的信息。

在这个DSN中,我们为GORM提供了以下信息:使用tcp协议连接到地址127.0.0.1:3306(MySQL数据库的连接地址),并指定数据库的名称为dbname,使用user作为用户名,pass作为密码进行身份验证;我们还指定了该连接使用utf8mb4作为字符编码集(MySQL默认使用utf8或utf8mb3作为字符编码集,但这些编码集不支持存储包含高级字符平面的Unicode字符,而utf8mb4是真正支持Unicode编码的完整UTF-8编码),同时启用parseTime以将时间信息正确映射到Go的time.Time结构体,并将时区设置为本地时区(loc)。

通过使用&gorm.Config{},我们启用了GORM的默认配置,当然,您也可以指定自己的配置,例如通过传入:

&gorm.Config{
    PrepareStmt: true
}

启用预编译语句缓存以提高性能。

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

可以使用自动迁移功能来为指定的数据库迁移数据模型的结构。这将创建与指定数据模型相对应的适用于GORM的数据表结构。需要注意的是,这一步是可选的,即使不进行迁移(schema),在创建新记录时,数据表也会被自动创建。

  // Create
  db.Create(&Product{Code: "D42", Price: 100})

在增删改查操作中的“增”操作,意味着在指定的数据库数据表中创建一个新的记录。值得注意的是,你不需要手动指定ID、CreatedAt等字段的值,这些字段的值将在特定时间点自动填充。

  // Read
  var product Product
  db.First(&product, 1) // 根据整型主键查找
  db.First(&product, "code = ?", "D42") // 查找 code 字段值为 D42 的记录

在增删改查操作中的“查”操作中,使用First方法可返回满足指定条件的第一条数据记录。需要注意的是,如果使用First方法查询时找不到数据,将会返回错误ErrRecordNotFound。另外,还可以使用Find方法查询多条数据记录,而当查询不到数据时,Find方法不会返回错误。

除了上述查询方法,还可以使用Where子句进行查询。需要留意的是,如果我们使用结构体作为查询条件,那么只有结构体中非零值的字段会被用于构建查询条件,而零值字段(如:0, '', false 或其他零值)将不会被考虑在内。换句话说,只有具有实际值的字段才会被纳入查询条件中。

  // 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.DeleteAt字段(既在gorm.Model中包括该字段),将会启用软删除模式。这意味着,当我们调用Delete方法删除数据时,该记录并不会真正从数据库表中删除,而是将DeletedAt字段设置为当前时间。此后,我们将无法通过常规的查询方法再次找到该记录。

使用 Unscoped 可绕过该机制,找到被软删除的记录:

db.Unscoped().Where("age = 20").Find(&users)
// SELECT * FROM users WHERE age = 20;

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

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

也可以使用手动方式创建,提交和回滚事务:

// 开始事务
tx := db.Begin()
// 在事务中执行一些 db 操作(从这里开始,您应该使用 'tx' 而不是 'db')
tx.Create(...)
// 遇到错误时回滚事务
tx.Rollback() 
// 否则,提交事务
tx.Commit()

GORM Hook

在执行创建、查询、更新和删除等操作之前或之后,会调用钩子函数(Hook)。

如果您在模型中定义了指定的方法,GORM会自动在执行创建、更新、查询和删除操作时调用这些方法。如果任何一个钩子函数返回错误,GORM将停止后续操作并回滚事务。

这个机制类似于Spring Boot中的AOP(Aspect Oriented Programming,面向切面编程),通过一种约定的方式将方法织入到数据库操作逻辑中。

钩子函数应该具有以下函数签名:func(*gorm.DB) error。

Kitex

Kitex [kaɪt'eks] 是字节跳动内部使用的一种 Golang 微服务 RPC 框架。它具有高性能和可扩展性,并且在字节跳动内部得到广泛应用。随着越来越多的微服务采用Golang语言开发,如果您对微服务的性能要求较高,并且希望能够自定义扩展并融入您自己的治理体系,那么选择Kitex将会是一个不错的决策。

什么是 RPC

RPC(Remote procedure call,远程过程调用)是一种计算机程序设计模式,用于使分布在不同地址空间(通常是在共享网络上的其他计算机)中的子例程在执行时就像本地过程调用一样进行编码,程序员无需显式编写详细的远程交互代码。也就是说,程序员可以以相同的方式编写代码,无论子例程是在本地还是远程执行。简而言之,使用RPC,我们可以像调用本地方法一样方便地与远程服务进行交互。

使用 Kitex(服务端)

目前,Kitex对于Windows的支持还不够完善。建议在Windows上使用虚拟机或者WSL2(Windows Subsystem for Linux 2)进行测试。

安装Kitex的代码生成工具是开始Kitex开发的第一步。您可以使用"go install"命令来安装Go的二进制工具。在执行此命令之前,请确保您已正确设置了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 Response {
    1: string message
}
 
service Echo {
    Response echo(1: Request req)
}

接口描述语言(Interface definition language,IDL)是一种通用术语,用于描述允许使用一种语言编写的程序或对象与使用另一种未知语言编写的程序进行通信的方式。我们可以通过使用IDL来定义支持RPC的信息传输。在Kitex中,默认支持thrift和proto3这两种IDL。而在底层传输方面,Kitex使用扩展的thrift作为传输协议。

如果您需要参考Thrift IDL语法,请查看"Thrift interface description language"。

如果您需要参考proto3语法,请查看"Language Guide(proto3)"。

上述文件定义了一个回声(Echo)服务,它接收一个包含单个字符串消息的请求,并返回相同的消息。

接下来,我们可以使用以下命令为我们的回声服务生成代码:

kitex -module exmaple -service example echo.thrift

在上述命令中,使用"-module"选项来指定生成的项目的Go模块名,使用"-service"选项指定我们要生成一个服务器端项目,后面的"example"表示该服务的名称。最后一个参数是该服务的IDL文件。

在此项目中,build.sh是用于构建的脚本,kitex_gen是生成与IDL内容相关的代码,main.go是程序的入口文件,而handler.go则是用户可以在其中实现IDL服务定义的方法。

修改 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命令可以启动服务。服务将在默认的8888端口上运行。如果要修改运行端口,可以打开main.go文件,并为NewServer函数指定配置参数。

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

重新执行编译步骤即可。

使用 Kitex(客户端)

在上面的示例中,我们使用Kitex创建了一个回声服务端。现在,我们将创建一个客户端代码来调用我们的回声服务。请确保您已经正确导入之前生成的回声服务代码,并新建一个项目并创建main.go文件,并编写代码创建一个客户端实例 c,并指定了服务端的地址。然后,我们创建了一个请求 req,并使用 c.Echo 发起了调用。最后,我们打印了响应 resp。

需要注意的是,我们使用了context.Context作为第一个参数,用于传递一些额外的信息或控制本次调用的行为。第二个参数是我们的请求对象,而第三个参数是一些调用参数,这些参数仅对当前调用有效。在示例中,我们使用了callopt.WithRPCTimeout来指定调用的超时时间。

在编写完这个简单的客户端后,您可以通过以下命令运行它:

$ go run main.go

如果一切正常,您将会看到类似以下的输出:

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

至此,已成功编写了一个Kitex的服务端和客户端,并完成了一次调用!

Kitex 服务注册与发现

Kitex拥有一个强大而完善的社区生态系统,得益于社区开发者的支持,已经实现了多种服务发现模式,包括ETCD、ZooKeeper、Eureka、Consul、Nacos和Polaris。此外,Kitex还支持DNS解析和直连访问(Static IP)模式,为用户提供了灵活多样的选择。这意味着用户可以根据自己的需求,选择适合的服务发现方式来构建和部署他们的应用。

Hertz

Hertz [həːts] 是一个Golang微服务HTTP框架,它借鉴了其他开源框架fasthttp、gin和echo的优点,并结合了字节跳动内部的需求。因此,Hertz具有高易用性、高性能和高扩展性等特点。在字节跳动内部,Hertz已经被广泛应用。

现在,越来越多的微服务选择使用Golang作为开发语言。如果你对微服务的性能有较高的要求,并且希望框架能够满足内部的定制需求,那么Hertz将是一个很好的选择。它能够提供所需的灵活性和性能优势,能够满足各种需求。

使用 Hertz(服务端)

安装命令行工具 hz(依然,在此之前,请务必检查已正确设置 GOPATH 环境变量,并将 $GOPATH/bin 添加到 PATH 环境变量中)

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

hz 也可被用于为指定 IDL 生成服务代码。

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

在 main.go 文件,编写创建一个 HTTP 服务端,监听 8080 端口并注册了一个 GET 方法的路由函数(/ping)。

Hertz 路由

Hertz 提供了 GET,POST,PUT,DELETE,ANY 等方法用于注册对应请求方式,其中,Any 用于注册所有 HTTP Method 方法;Hertz.StaticFile/Static/StaticFS 用于注册静态文件;Handle 可用于注册自定义 HTTP Method 方法。

路由组

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

参数路由和通配路由

Hertz提供了多种路由类型,用于实现各种复杂功能,包括静态路由(如前所述)、参数路由和通配路由。

在路由的优先级方面,Hertz遵循以下规则:静态路由优先于命名路由,而命名路由优先于通配路由。这意味着当使用静态路由和命名路由时,它们将具有更高的优先级,而通配路由将处于较低的优先级。

参数路由

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

例如,对于/user/:name路由,路径/user/gordon和/user/you会匹配成功,而路径/user/gordon/profile和/user/则不会匹配成功。

通过使用RequestContext.Param方法,我们可以获取在路由中传递的参数值:

通配路由

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

举例来说,对于/src/*path路由,路径/src/、/src/somefile.go和/src/subdir/somefile.go都会匹配成功。

我们可以通过使用RequestContext.Param方法来获取在路由中传递的参数值:

Hertz 参数绑定

Hertz提供了Bind、Validate和BindAndValidate函数,用于参数的绑定和验证处理。

Hertz 中间件

Hertz的服务端中间件是在HTTP请求-响应周期中的一个函数,它提供了一种方便的机制,允许我们检查和过滤进入应用程序的HTTP请求。例如,我们可以使用中间件记录每个请求的信息,或者启用CORS功能。

中间件可以在请求到达业务逻辑之前或之后执行,以便进行更深入的处理。 如果需要终止后续调用,可以使用Abort()、AbortWithMsg(msg string, statusCode int)或AbortWithStatus(code int)这些函数。

使用 Hertz(客户端)

Hertz提供了HTTP Client来帮助用户发送HTTP请求。这个HTTP Client提供了简单而方便的方法来发送GET、POST、PUT、DELETE等HTTP请求。用户可以使用这个HTTP Client在他们的应用程序中与其他HTTP服务器进行通信。通过Hertz的HTTP Client,用户可以发送数据、设置请求头、处理响应等。这使得与其他服务进行API交互变得更加容易。

引用

该笔记中的资料主要来源于: 后端 - 字节内部课 (juejin.cn)