第29节 Go语言框架三件套(Web/RPC/GORM)
ORM、GORM
ORM全称是:Object Relational Mapping(对象关系映射),其主要作用是在编程中,把面向对象的概念跟数据库中表的概念对应起来。
举例来说就是,我定义一个对象,那就对应着一张表,这个对象的实例,就对应着表中的一条记录。
GORM:是Golang语言中一款性能极好的ORM库,对开发人员相对是比较友好的。
GROM
GORM特性
PreloadJoins驱动方式链接
GORM 是通过驱动的方式来链接数据库的,目前支持 Mysql、SQLSever、PostgreSQL、SQLite。如果需要链接其他类型的数据库,可以 复用/自行 开发驱动。
- 驱动,是指软件驱动程序。是一种中间件,它能够将应用程序与硬件或其他系统之间的信息进行转换。在数据库领域中,驱动程序就是一种软件,它能够将应用程序与数据库之间的信息进行转换。
- 驱动程序通常提供一组标准的数据库连接接口。这些接口包括连接数据库、执行 SQL 语句、获取结果集等。应用程序可以使用这些接口与数据库进行交互。
在 GORM 中,驱动程序是将 Go 语言与数据库之间的信息进行转换的软件。 GORM 库使用驱动程序来连接数据库,并使用驱动程序提供的接口来进行数据库操作。这样,GORM 库就不用关心数据库是哪种数据库,只要有驱动程序就可以连接数据库。
驱动是一种软件,它能够将 Go 语言与数据库之间的信息进行转换。驱动通过一组标准的数据库连接接口来提供连接数据库的功能。
GORM 的设计原则是对数据库访问进行封装,而不是对 SQL 语句进行封装。这样可以让开发人员专注于业务逻辑,而不用关心数据库的细节。
以驱动的方式链接数据库的好处是:
- 使用驱动可以支持多种数据库,而不用对 GORM 进行修改。
- 使用驱动可以更好地支持数据库的新特性。
- 使用驱动可以更好地进行数据库性能优化。
DSN
[[DSN]] 是 Data Source Name 的缩写,是一种数据库连接字符串,它提供了连接数据库所需的所有信息。
DSN 的格式通常包括数据库类型、数据库名称、用户名、密码和连接地址等信息。
例如对于 mysql 数据库来说,DSN 格式如下:
其中:
userpasswordhostportdbnameDSN 字符串的格式可能因数据库类型而异。
使用DSN字符串连接数据库是一种常用的方式,因为它简化了连接数据库的过程,并且使用DSN字符串连接的数据库驱动程序通常支持多种数据库类型,这样就不用写多个连接数据库的函数了。
安装和简单实用
GORM安装:
Quick Start:
Mysql 的 简单的一个案例如下:
对上面的解释:
- 在 main 函数中,我们通过调用 gorm.Open("mysql", "root:password@/dbname?charset=utf8&parseTime=True&loc=Local") 来建立数据库连接。
- 我们使用 db.AutoMigrate(&Product{}) 来自动迁移数据库结构,即自动根据 Product 结构体创建数据表。
- 我们使用 db.Create(&Product{Name: "面包", Price: 3}) 来创建一条数据。
- 使用 db.Find(&products) 来查询所有数据并将其存入 products 变量中。
- 我们使用 db.Model(&Product{}).Where("name = ?", "面包").Update("price", 4) 来更新名称为 "面包" 的产品的价格。
- 我们使用 db.Where("name = ?", "面包").Delete(&Product{}) 来删除名称为 "面包" 的产品。
- 最后,通过 defer db.Close() 关闭数据库连接。
可以使用gorm.Open()函数打开数据库连接。 例如:
这里是一些基本的GORM用法,尝试使用它来 创建、读取、更新和删除 数据库记录。
解决数据冲突
在 GORM 中,使用 OnConflict 方法可以解决数据冲突问题。这个方法可以让你指定在冲突时应该采取的操作。
一般来说,在执行插入操作时可能会因为主键重复而导致数据冲突。使用 OnConflict 方法可以解决这个问题。
简单的一个案例如下:
这行代码表示 如果 name 列重复时,将执行更新操作来更新 price 列。
OnConflict("(name) DO NOTHING")OnConflict 方法非常有用,因为它可以帮助你解决数据冲突问题,并让你更好地控制你的数据库。
使用默认值
GORM 支持在数据库中设置默认值。这可以通过在结构体中使用 "default" 标签来实现。
这样的话,在创建或更新数据时,如果没有指定 Price 列的值,那么会自动将其设置为 0。
默认值还可以使用SQL表达式和函数来进行设置。
created_at设置默认值是非常有用的,它可以帮助你简化代码并减少错误。
First 使用详细
FirstFirst例如,如果你想查询第一条名称为 "Apple" 的产品数据,你可以这样写:
这里的参数是一个模型结构体的指针,第二个参数是查询条件,第三个参数是条件的值。
也可以使用链式查询来实现这个操作,如下:
⚠️ 在链式调用的时候,需要注意~!
FirstFirstOrInitFirstOrCreate在使用 First 方法时, 如果传入的结构体中有组合索引,但没有使用 Where 函数来限制组合索引的值会触发。
FirstFirstecordNotFound使用时需要注意,如果传递的参数是结构体指针,gorm会将查询结果赋值到结构体上,而传递结构体则不会有赋值操作。
FirstRecordNotFoundFind
FindusersUserWhereFindFind最后我们打印出查询到的用户信息。
FindFirst 和 Find
FirstFindFirstRecordNotFoundFindFirstFindFirstFind更新数据
SaveUpdatesUpdateSaveUpdatesUpdateusersUserFirstSaveUpdatesGorm 更新数据有两种方式:
- 使用 Update 方法:
这种方式可以对指定字段进行更新,而不会影响其他字段的值。
- 使用 Save 方法:
这种方式会更新整个结构体。
FirstOrCreate注意,在使用 Save 方法时,如果结构体中有没有被赋值的字段,那么这些字段可能会被赋上空值,所以需要特别小心。
updated_at删除
Gorm 提供了 Delete 方法来删除数据。
使用 Delete 方法可以删除一条记录,方法的参数是要删除的记录的指针。
这个方法会将这条记录从数据库中删除,删除之后该记录就不再是有效的。
如果要删除多条记录,可以使用 Where 方法限制删除范围。
这个方法会删除年龄大于20岁的所有用户。
注意,在删除数据时,Gorm 不会自动跟踪删除时间,如果需要记录删除时间,需要手动设置。
物理删除和软删除
物理删除和软删除是 Gorm 提供的两种不同的删除方式。
- 物理删除就是直接从数据库中删除记录,不管有没有其他业务逻辑的依赖,这种方式删除的数据不可恢复,需要谨慎使用。
- 软删除是通过在数据表中添加一个删除标记来标记记录被删除,这种方式删除的数据可以恢复,适用于不需要永久删除数据的场景。
Gorm 提供了一些辅助方法来支持软删除,比如 DeletedAt 字段,在使用 Delete 方法删除数据时会自动设置这个字段,可以通过 Unscoped 方法来忽略这个字段来进行物理删除。
Gorm默认不支持软删除,需要手动支持。Gorm 提供了 gorm.DeletedAt 用于帮助用户实现软删除
事务
db.Begin()tx.Commit()tx.Rollback()Begin()TXCommit()Rollback()db.Transaction(func(tx *gorm.DB) error{})db.Transaction(func(tx *gorm.DB) error{})tansaction注意:如果返回值为nil,事务会提交,如果返回值非nil,事务会回滚。
GORM Hook
GORM Hook是GORM中的一种事件钩子机制,它允许在对数据库进行操作时,在特定的时间点执行额外的代码。
GORM Hook提供了多种事件类型,如创建、更新、查询和删除等。
db.Callback()gorm:before_create可以使用如下的事件类型:
Before/After("gorm:create")Before/After("gorm:query")Before/After("gorm:update")Before/After("gorm:delete")通过这些事件类型,可以在对数据库进行操作时,对数据进行预处理或后处理,也可以在数据库操作失败时进行错误处理。
Gorm 性能提高
GORM是一个ORM库,它会在运行时执行很多操作,因此其运行性能可能不如直接使用原生SQL。但是,还是有一些方法可以提高GORM的性能。
- 使用缓存: GORM支持对数据库的缓存,如果你的应用程序需要频繁读取数据库中的同一数据,可以考虑使用缓存来提高性能。
- 尽量减少查询次数: GORM在查询数据库时会执行很多操作,如果你可以通过减少查询次数来提高性能,就应该尽量这样做。
- 尽量减少使用高级功能: GORM支持很多高级功能,如果你不需要这些功能,可以考虑不使用它们,以提高性能。
- 尽量使用批量操作: GORM支持对多个对象批量进行操作,如果你需要对多个对象进行相同的操作,可以考虑使用批量操作来提高性能。
- 数据库连接池: GORM默认使用数据库连接池来管理数据库连接,但是默认的连接池大小可能不够,可以通过调整连接池大小来提高性能。
缓存预编译
预编译是一种将 SQL 语句编译为数据库可以识别的语句的过程。这种编译过程可以提高查询性能,因为数据库不需要在每次执行查询时都重新编译 SQL 语句。
GORM 中可以使用预编译语句来提高查询性能。使用预编译语句的方法是将 SQL 语句传递给 gorm.DB.Exec 或 gorm.DB.Query,并将需要传递给 SQL 语句的参数作为第二个参数传递。
简单的一个案例如下:
其中,第一个参数是预编译的 SQL 语句,第二个参数是要传递给 SQL 语句的参数。
缓存预编译是一种优化查询性能的方法,它可以将预编译过的 SQL 语句存储在缓存中,以便在需要执行相同的查询时可以重用已编译的语句。这可以减少数据库编译 SQL 的开销,提高性能。
strict=truecache=true使用缓存预编译语句可以显著提高查询性能,特别是对于执行相同的查询的情况。
关闭默认事务
GORM 默认在每次数据库操作时都会启用事务,但是有时候我们不需要这样。
在 GORM 中关闭默认事务可以使用 gorm.Open 方法打开数据库连接时的参数。
autocommit=true注意:关闭事务后需要自己手动管理事务,如果需要事务支持,请不要关闭默认事务。
GORM生态
| 扩展名称 | 描述 | GitHub地址 |
|---|---|---|
| GORM 代码生成工具 | 可以根据数据库表结构自动生成 Go 代码,帮助使用 GORM 的用户更快速地开发 | |
| GORM 分片库方案 | 对 GORM 进行的分片库扩展,可以帮助用户管理分片数据库 | |
| GORM 手动索引 | 帮助用户在 GORM 中手动创建索引 | |
| GORM 乐观锁 | 对 GORM 进行的乐观锁扩展,可以帮助用户处理并发冲突 | |
| GORM 读写分离 | 对 GORM 进行的读写分离扩展,可以帮助用户优化数据库性能 | |
| GORM OpenTelemetry 扩展 | 对 GORM 进行的 OpenTelemetry 扩展,可以帮助用户对 GORM 的性能进行监控和追踪 |
更多用法:https://gorm.cn
Kitex
Kitex是字节内部的Golang微服务PRC框架。
Kitex是一个高性能的开源网络库,它具有高性能、低延迟和高可靠性特点。它支持多种协议,如TCP、UDP、HTTP和RPC等,可以用于构建分布式系统、微服务和云计算等应用。 Kitex的主要用途是在应用程序中提供高性能的网络通信支持,并帮助开发人员更轻松地实现分布式应用程序和微服务。
Kitex 目前对 Windows 的支持不完善,如果本地开发环境是 windows 的话需要用到 虚拟机或者是 WSL2。
安装代码生成工具:
验证:
定义 IDL
IDL (Interface Definition Language) 是指接口定义语言,是用于描述远程过程调用(RPC)或对象请求代理(ORP)接口的语言。 IDL 中定义了远程对象的接口,其中包括方法名称、参数类型和返回值类型等信息。这些信息可以用于生成代码来实现远程调用。常见的 IDL 标准有 CORBA 和 Microsoft COM/DCOM。
生成代码
使用下面的命令生成代码:
RPC(Remote Procedure Call Protocol)远程过程调用协议。一个通俗的描述是:客户端在不知道调用细节的情况下,调用存在于远程计算机上的某个对象,就像调用本地应用程序中的对象一样。比较正式的描述是:一种通过网络从远程计算机程序上请求服务,而不需要了解底层网络技术的协议。
文件说明:
- build.sh:构建脚本
- kitex_gen:IDL内容相关的生成代码,主要是基础的Server/Client代码
- main.go:程序入口
- handler.go:用户在该文件里实现IDLservice定义的方法
服务默认监听8888端口;
Kitex Client发起请求:
- 创建Client
- 发起请求
Kitex服务注册与发现:
目前Kitex的服务注册与发现已经对接了主流的服务注册与发现中心,如ETCD,Nacos等;
Kitex生态:
https://www.cloudwego.io/zh/docs/kitex/发起请求:
Kitex 服务注册与发现
目前的 Kitex 的服务注册与发现已经对接了主流的服务注册与发现中心,如 ETCD 、Nacos
生态
Hertz
Hertz 是字节内部的 HTTP 框架。
HertzHTTPFaaSService MeshHertzHertz30%-60%Hertz 坚持内外维护一套代码,为开源使用提供了强有力的保障。通过开源, Hertz 也将丰富云原生的 Golang 中间件体系,完善 CloudWeGo 生态矩阵,为更多开发者和企业搭建云原生化的大规模分布式系统,提供一种现代的、资源高效的的技术方案。
项目源码
最初,字节跳动内部的 HTTP 框架是对 Gin 框架的封装,具备不错的易用性、生态完善等优点。随着内部业务的不断发展,高性能、多场景的需求日渐强烈。而 Gin 是对 Golang 原生 net/http 进行的二次开发,在按需扩展和性能优化上受到很大局限。因此,为了满足业务需求,更好的服务各大业务线,2020 年初,字节跳动服务框架团队经过内部使用场景和外部主流开源 HTTP 框架 Fasthttp、Gin、Echo 的调研后,开始基于自研网络库 Netpoll 开发内部框架 Hertz,让 Hertz 在面对企业级需求时,有更好的性能及稳定性表现,也能够满足业务发展和应对不断演进的技术需求。
架构设计
Hertz 设计之初调研了大量业界优秀的 HTTP 框架,同时参考了近年来内部实践中积累的经验。为了保证框架整体上满足:1. 极致性能优化的可能性;2. 面对未来不可控需求的扩展能力, Hertz 采用了 4 层分层设计,保证各个层级功能内聚,同时通过层级之间的接口达到灵活扩展的目标。整体架构图如图 1 所示。
Hertz 从上到下分为:应用层、路由层、协议层和传输层,每一层各司其职,同时公共能力被统一抽象到公共层(common),做到跨层级复用。另外,同主库一同发布的还有作为子模块的 Hz 脚手架,它能够协助使用者快速搭建出项目核心骨架以及提供实用的构建工具链。
应用层
应用层是和用户直接交互的一层,提供丰富易用的 API,主要包括 Server、Client 和一些其他通用抽象。Server 提供了注册 HandlerFunc、Binding、Rendering 等能力;Client 提供了调用下游和服务发现等能力;以及抽象一个 HTTP 请求所必须涉及到的请求(Request)、响应(Response)、上下文(RequestContext)、中间件(Middleware)等等。Hertz 的 Server 和 Client 都能够提供中间件这样的扩展能力。
context.Context路由层
路由层负责根据 URI 匹配对应的处理函数。
起初,Hertz 的路由基于 httprouter 开发,但随着使用的用户越来越多,httprouter 渐渐不能够满足需求,主要体现在 httprouter 不能够同时注册静态路由和参数路由,即 /a/b,/:c/d 这两个路由不能够同时注册;甚至有一些更特殊的需求,如/a/b、/:c/b ,当匹配 /a/b 路由时,两个路由都能够匹配上。
Hertz 为满足这些需求重新构造了路由树,用户在注册路由时拥有很高的自由度:支持静态路由、参数路由的注册;支持按优先级匹配,如上述例子会优先匹配静态路由 /a/b ;支持路由回溯,如注册 /a/b、/:c/d,当匹配 /a/d 时仍然能够匹配上;支持尾斜线重定向,如注册 /a/b,当匹配 /a/b/ 时能够重定向到 /a/b 上。Hertz 提供了丰富的路由能力来满足用户的需求,更多的功能可以参考 Hertz 配置文档。
协议层
协议层负责不同协议的实现和扩展。
HTTP1HTTP2传输层
传输层负责底层的网络库的抽象和实现。
Hertz 支持底层网络库的扩展。Hertz 原生完美适配 Netpoll,在时延方面有很多深度的优化,非常适合时延敏感的业务接入。Netpoll 对 TLS 能力的支持有待完善,而 TLS 能力又是 HTTP 框架必备能力,为此 Hertz 底层同时支持基于 Golang 标准网络库的实现适配,支持网络库的一键切换,用户可根据自己的需求选择合适的网络库进行替换。如果用户有更加高效的网络库或其他网络库需求,也完全可以根据需求自行扩展。
Hz 脚手架
与 Hertz 一并开源的还有一个易用的命令行工具 Hz,用户只需提供一个 IDL,根据定义好的接口信息,Hz 便可以一键生成项目脚手架,让 Hertz 达到开箱即用的状态;Hz 也支持基于 IDL 的更新能力,能够基于 IDL 变动智能地更新项目代码。目前 Hz 支持了 Thrift 和 Protobuf 两种 IDL 定义。命令行工具内置丰富的选项,可以根据自己的需求使用。同时它底层依赖 Protobuf 官方的编译器和自研的 Thriftgo 的编译器,两者都支持自定义的生成代码插件。如果默认模板不能够满足需求,完全能够按需定义。
未来,我们将继续迭代 Hz,持续集成各种常用的中间件,提供更高层面的模块化构建能力。给 Hertz 的用户提供按需调整的能力,通过灵活的自定义配置打造一套满足自身开发需求的脚手架。
Common 组件
Common 组件主要存放一些公共的能力,比如错误处理、单元测试能力、可观测性相关能力(Log、Trace、Metrics 等)。对于服务可观测性的能力,Hertz 提供了默认的实现,用户可以按需装配;如果用户有特殊的需求,也可以通过 Hertz 提供的接口注入。比如对于 Trace 能力,Hertz 提供了默认的实现,也提供了将 Hertz 和 Kitex 串起来的 Example。如果想注入自己的实现,也可以实现下面的接口:
复制
Hertz 中间件
Hertz 除了提供 Server 的中间件能力,还提供了 Client 中间件能力。用户可以使用中间件能力将通用逻辑(如:日志记录、性能统计、异常处理、鉴权逻辑等等)和业务逻辑区分开,让用户更加专注于业务代码。Server 和 Client 中间件使用方式相同,使用 Use 方法注册中间件,中间件执行顺序和注册顺序相同,同时支持预处理和后处理逻辑。
Server 和 Client 的中间件实现方式并不相同。对于 Server 来说,我们希望减少栈的深度,同时也希望中间件能够默认的执行下一个,用户需要手动终止中间件的执行。因此,我们将 Server 的中间件分成了两种类型,即不在同一个函数调用栈(该中间件调用完后返回,由上一个中间件调用下一个中间件,如图 2 中 B 和 C)和在同一个函数调用栈的中间件(该中间件调用完后由该中间件继续调用下一个中间件,如图 2 中 C 和 Business Handler)。
其核心是需要一个地方存下当前的调用位置 index,并始终保持其递增。恰好 RequestContext 就是一个存储 index 合适的位置。但是对于 Client,由于没有合适的地方存储 index,我们只能退而求其次,抛弃 index 的实现,将所有的中间件构造在同一调用链上,需要用户手动调用下一个中间件。
Hertz代码生成工具
Hertz 性能
hertz 的性能非常不错
Hertz 使用字节跳动自研高性能网络库 Netpoll,在提高网络库效率方面有诸多实践,参考已发布文章字节跳动在 Go 网络库上的实践。除此之外,Netpoll 还针对 HTTP 场景进行优化,通过减少拷贝和系统调用次数提高吞吐以及降低时延。为了衡量 Hertz 性能指标,我们选取了社区中有代表性的框架 Gin(net/http)和 Fasthttp 作为对比,可以看到,Hertz 的极限吞吐、TP99 等指标均处于业界领先水平。未来,Hertz 还将继续和 Netpoll 深度配合,探索 HTTP 框架性能的极限。
笔记项目
| Service Name | Usage | Framework | protocol | Path | IDL |
|---|---|---|---|---|---|
| demoapi | http interface | kitex/hertz | http | bizdemo/easy_note/cmd/api | |
| demouser | user data management | kitex/gorm | protobuf | bizdemo/easy_note/cmd/user | bizdemo/easy_note/idl/user.proto |
| demonote | note data management | kitex/gorm | thrift | bizdemo/easy_note/cmd/note | bizdemo/easy_note/idl/note.thrift |
项目模板:
项目调用关系:
API 并不是和数据库打交道的,这样好处是可以复用接口,可扩展性强。
技术栈: