这是我参与「第五届青训营 」伴学笔记创作活动的第 6 天
三个 Go 语言的主流开发框架
- GORM in ORM
- Kitex in RPC
- Hertz in HTTP
1. GROM
1.1 ORM
Object–relational mapping,即对象关系映射,作用是在编程中,把面向对象的概念跟数据库中表的概念对应起来,可以将关系数据库中某个数据表的结构关联到某个类/结构体上,并通过修改类/结构体实例的方式完成数据库增删改查(CRUD)的任务。通过 ORM 技术,我们得以以一种更加友好且高效的方式,在尽量不接触 SQL 语句的情况下操作数据库。
在 Java 中,常见的 ORM 框架有 Mybatis, MyBatis-Plus, Hibernate 等。
ORM框架是连接数据库的桥梁,只要提供了持久化类与表的映射关系,ORM框架在运行时就能参照映射文件的信息,把对象持久化到数据库中。 Gorm 是 Go 语言中实现对象和数据库映射的框架,可以有效地提高开发数据库应用的效率。 Gorm 主要用途是把 struct类型 和 数据库表 进行映射,使用简单方便。 因此,使用 Gorm 操作数据库的时候一般不需要直接手写 SQL 语句代码。
举例来说就是,我定义一个对象,那就对应着一张表,这个对象的实例,就对应着表中的一条记录。
GORM:是Golang语言中一款性能极好的ORM库,对开发人员相对是比较友好的。
1.2 GORM的基本使用——以连接到MySQL为例
参考官方文档 GORM指南
依赖管理
import (
"gorm.io/driver/mysql"
"gorm.io/gorm"
)
定义gorm model
Gorm 操作数据库,需要定义一个 struct 类型和 Mysql 表进行绑定或者叫映射,struct 中的字段 和 Mysql 表的字段一一对应,支持的数据类型包括Go的基本数据类型和自定义的数据类型(必须实现 Scanner 和 Valuer 接口)。在结构体中通过在字段后面的标签说明,定义golang字段和表字段的关系。
type User struct {
ID int64 `gorm:"primaryKey"`
Username string `gorm:"column:username"`
Password string `gorm:"column:password"`
CreateTime time.Time
//UpdatedAt time.Time
//DeletedAt gorm.DeletedAt `gorm:"index"`
}
// 设置表名,可以通过给 struct 类型定义 TableName 函数,返回当前 struct 绑定的 Mysql 表名
func (u User) TableName() string {
return "users" // 绑定 Mysql 表名为 users
}
gorm.Model可以嵌入在已有的结构体中,还可以进行权限控制
约定:GORM 倾向于约定优于配置 默认情况下,使用 ID 作为主键,若为数字 是自增主键;使用结构体名的 蛇形复数 作为表名,字段名的 蛇形 作为列名,并使用 CreatedAt、UpdatedAt 字段追踪创建、更新时间,DeleteAt字段默认开启soft delete模式
连接到数据库
具体操作和参数可参考 github.com/go-sql-driv…
func main() {
// 配置 MySQL 连接参数
username := "root" //账号
password := "123456" //密码
host := "127.0.0.1" //数据库地址,可以是Ip或者域名
port := 3306 //数据库端口
Dbname := "codeXYY" //数据库名
// 拼接 Mysql DSN,即数据库连接串(数据源名称)
// Mysql dsn格式: {username}:{password}@tcp({host}:{port})/{Dbname}?charset=utf8&parseTime=True&loc=Local
dsn := fmt.Sprintf("%s:%s@tcp(%s:%d)/%s?charset=utf8&parseTime=True&loc=Local", username, password, host, port, Dbname)
// 连接 Mysql
db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{})
if err != nil {
panic("failed to connect database, error=" + err.Error())
}
//dsn通过mysql配置
/* dsn := mysql.New(mysql.Config{
DSN: "gorm:gorm@tcp(127.0.0.1:3306)/gorm?charset=utf8&parseTime=True&loc=Local", // DSN data source name
DefaultStringSize: 256, // string 类型字段的默认长度
DisableDatetimePrecision: true, // 禁用 datetime 精度,MySQL 5.6 之前的数据库不支持
DontSupportRenameIndex: true, // 重命名索引时采用删除并新建的方式,MySQL 5.7 之前的数据库和 MariaDB 不支持重命名索引
DontSupportRenameColumn: true, // 用 `change` 重命名列,MySQL 8 之前的数据库和 MariaDB 不支持重命名列
SkipInitializeWithVersion: false, // 根据当前 MySQL 版本自动配置
}) */
GORM使用Database / SQL来维护连接池;
sqlDB, err := db.DB()
sqlDB.SetMaxIdleConns(10) //设置空闲连接池中连接的最大数量
sqlDB.SetMaxOpenConns(100) //设置打开数据库连接的最大数量
sqlDB.SetConnMaxLifetime(time.Hour) //设置连接可复用的最大时间
CURD 增删改查
// 定义一个用户,并初始化数据
u := User{
Username:"codeXYY",
Password:"123456",
CreateTime:time.Now().Unix(),
}
// 插入,自动生成SQL语句:INSERT INTO `users` (`username`,`password`,`createtime`) VALUES ('codeXYY','123456','……')
//处理数据冲突:Clauses(Clause.OnConflicts{DoNothing:true})
//在结构体中使用default标签为字段定义默认值
if err := db.Create(&u).Error; err != nil {
fmt.Println("fail to insert data, error =", err)
return
}
// 查询并返回第一条数据(主键升序):First(查询不到返回ErrRecordNotFind)
u = User{}
db.First(u)// SELECT * FROM `users` ORDER BY ID LIMIT 1
// 查询多条数据:Find(方法RowsAffected和Error)
// 注意使用结构体作为查询条件时不能查询零值,可使用map构建查询条件以代替
users := make([]*User, 0)
result := db.Where("username = ?", "codeXYY").Find(&users)// SELECT * FROM `users` WHERE (username = 'codeXYY')
// 更新数据库记录(还可以更新多列/指定字段/配合map和select语句使用)
db.Model(&User{}).Where("username = ?", "codeXYY").Update("password", "654321")//UPDATE `users` SET `password` = '654321' WHERE (username = 'codeXYY')
// 删除
//在结构体里添加字段DeletedAt可以实现软删除,可通过Unscoped查询这部分数据
db.Where("username = ?", "codeXYY").Delete(&User{})//DELETE FROM `users` WHERE (username = 'codeXYY')
}
1.3 GROM事务
数据库事务(transaction) ——是访问并可能操作各种数据项的一个数据库操作序列,事务由事务开始与事务结束之间执行的全部数据库操作组成,视作一个整体,当其中一个操作出现错误,其他操作会被自动回滚。
- 自动事务处理——Transaction方法
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()// 开始事务 tx.Create(...) tx.Rollback()// 遇到错误时回滚事务 tx.Commit()//提交事务
1.4 其他
hook——在CRUD之前之后自动调用的函数
如果任何hook返回错误,GROM将停止后续操作并回滚事务
性能提高
config{
SkipDefaultTransaction:true//关闭封装CRUD的默认事务
PrepareStmt:true//缓存预编译语句,可提高后续调用速度
}
扩展生态
代码生成工具/分片库方案/手动索引/乐观索/读写分离/OpenTelemetry扩展
2. Kitex
Kitex是字节跳动内部的 Golang 微服务 RPC 框架,具有高性能、强可扩展的特点,如果对微服务性能有要求,又希望定制扩展融入自己的治理体系,Kitex 会是一个不错的选择。
参考官方文档 www.cloudwego.io/zh/docs/kit…
2.1 RPC
RPC,即Remote Procedure Call Protocol 远程过程调用协议,是一种通过网络从远程计算机程序上请求服务,而不需要了解底层网络技术的协议。可以实现让客户端在不知道调用细节的情况下,调用存在于远程计算机上的某个对象,就像调用本地应用程序中的对象一样,可以像调用方法一样快捷的与远程服务进行交互。
适用于服务化 、微服务和分布式系统架构的基础场景。
需要底层实现两大功能:Serialization 序列化和Transport 传输
常用的的RPC框架有很多,如gRPC,Spring cloud等。
2.2 使用IDL定义服务与接口
IDL是Interface description language的缩写,指接口描述语言,是规范的一部分,是跨平台开发的基础,用于约定双方的协议。在进行RPC时需要知道对方的接口、传参和返回值。
Kitex 框架及命令行工具,默认支持 thrift 和 proto3两种 IDL,对应的 Kitex 支持 thrift 和 protobuf两种序列化协议。传输上 Kitex 使用扩展的 thrift 作为底层的传输协议。
语法参考 Thrift: thrift.apache.org/docs/idl
Proto3: developers.google.com/protocol-bu…
2.3 Kitex 服务端使用
定义 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 example -service example echo.thrift
//`-module` 表示生成的该项目的 go module 名
//`-service` 表明我们要生成一个服务端项目
//`example` 为该服务的名字
//最后一个参数则为该服务的 IDL 文件。
生成后的项目结构如下
.
|-- build.sh //构建脚本
|-- echo.thrift
|-- handler.go //此处实现IDL sevice定义的方法
|-- kitex_gen //IDL内容相关的生成代码,主要是基础的server/client代码
| `-- api
| |-- echo
| | |-- client.go
| | |-- echo.go
| | |-- invoker.go
| | `-- server.go
| |-- echo.go
| `-- k-echo.go
|-- main.go //程序入口
`-- script
|-- bootstrap.sh
`-- settings.py
handler.go文件的内容如下,服务默认监听8888端口,并修改Echo 函数为下述代码以实现 Echo 服务逻辑
package main
import (
"context"
"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) {
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))
2.4 Kitex客户端使用
创建client——在新建项目的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"} //创建一个请求 req
resp, err := c.Echo(context.Background(), req, callopt.WithRPCTimeout(3*time.Second)) //通过 c.Echo 发起调用
if err != nil {
log.Fatal(err)
}
log.Println(resp)
发起调用的部分参数解释如下
- 第一个参数为 context.Context,通过通常用其传递信息或者控制本次调用的一些行为
- 第二个参数为本次调用的请求
- 第三个参数为本次调用的 options ,Kitex 提供了一种 callopt 机制,顾名思义——调用参数 ,有别于创建 client 时传入的参数,这里传入的参数仅对此次生效。 此处的 callopt.WithRPCTimeout 用于指定此次调用的超时(通常不需要指定,此处仅作演示之用)
go run main.go //运行客户端完成调用
//可以看到类似输出:2021/05/20 16:51:35 Response({Message:my request})
2.5 Kitex服务注册与发现
目前Kitex 已经通过社区开发者的支持,完成了 ETCD、ZooKeeper、Eureka、Consul、Nacos、Polaris 多种服务发现模式,当然也支持 DNS 解析以及 Static IP 直连访问模式,建立起了强大且完备的社区生态。
3. Hertz
HTTP 协议是当今使用最为广泛的协议之一,HTTP 是前(客户)端与服务端通信的基础协议。HTTP 框架负责的就是对 HTTP 请求的解析、根据对应的路由选择对应的后端逻辑了,HTTP 在企业实际业务场景中使用广泛。Hertz是一个用于 Go的高性能、高可用性、可扩展的微服务HTTP 框架。
参考官方文档Hertz 代码设计实践
命令行工具hz安装【也可用于为指定 IDL 生成服务代码(使用 hz new 生成代码,然后使用 go mod tidy 拉取依赖)】
go install github.com/cloudwego/hertz/cmd/hz@latest
3.1 Hertz服务端使用
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"))//服务监听8080端口
h.GET("/ping", func(c context.Context, ctx *app.RequestContext) {
ctx.JSON(consts.StatusOK, utils.H{"message": "pong"})
})//注册一个GET方法的路由函数
h.Spin()
}
3.1.1 Hertz路由
Hertz 提供了 GET,POST,PUT,DELETE,ANY 等方法用于注册对应请求方式(Reuquest Method)的路由。 路由的理解可参考 笔记
func main(){
h.StaticFS("/", &app.FS{Root: "./", GenerateIndexPages: true})//Hertz.StaticFile/Static/StaticFS 用于注册静态文件
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")
})//Any 用于注册所有 HTTP Method 方法
h.Handle("LOAD","/load", func(ctx context.Context, c *app.RequestContext) {
c.String(consts.StatusOK, "load")
})//Handle 可用于注册自定义 HTTP Method 方法
h.Spin()
}
路由组
Hertz 提供了路由组( Group )的能力,用于支持路由分组的功能,同时中间件也可以注册到路由组上.
参数路由和通配路由
Hertz 支持丰富的路由类型用于实现复杂的功能,包括静态路由(见上)、参数路由、通配路由。
路由的优先级:静态路由 > 命名路由 > 通配路由
参数路由
Hertz 支持使用 :name 这样的命名参数设置路由,并且命名参数只匹配单个路径段。
对于 /user/:name 路由,/user/gordon 和 /user/you 路径会得到匹配,而 /user/gordon/profile 和 /user/ 则不会。
通过使用 RequestContext.Param 方法,我们可以获取路由中携带的参数:
// 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)
})
通配路由
Hertz 支持使用 *path 这样的通配参数设置路由,并且通配参数会匹配所有内容。
对于 /src/*path 路由,/src/, /src/somefile.go, /src/subdir/somefile.go 均会得到匹配。
// 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)
})
3.1.2 Hertz参数绑定
Hertz 提供了 Bind,Validate,BindAndValidate 函数用于进行参数绑定和校验:
3.1.3 Hertz 中间件
Hertz 服务端中间件是 HTTP 请求-响应周期中的一个函数,提供了一种方便的机制来检查和过滤进入应用程序的 HTTP 请求, 例如记录每个请求或者启用CORS。中间件可以在请求更深入地传递到业务逻辑之前或之后执行。
可使用 Abort(), AbortWithMsg(msg string, statusCode int), AbortWithStatus(code int) 终止中间件调用链的执行。
这里展示一个服务端中间件:
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()
}
3.2 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))
3.3 Hertz项目快速生成
类似于2.3
- 定义IDL,使用hz生成代码,并整理和拉取依赖
- 填充业务逻辑
- 编译并运行项目
- curl测试
4.实战——笔记项目
待补充