# 框架概览 GoFrame 是 Go 语言的模块化应用开发框架,模块路径为 `github.com/gogf/gf/v2`,当前版本 2.10.4,采用 MIT 许可。框架既可以作为完整项目框架使用,也可以只引入需要的组件。 ## 运行要求 - Go 1.23 及以上 - Linux、macOS、Windows - 官方 CI 覆盖 amd64 与 386 架构 ## 安装 在已有 Go 模块中添加框架: ```bash go get -u github.com/gogf/gf/v2 ``` 安装 `gf` 命令行工具: ```bash go install github.com/gogf/gf/cmd/gf/v2@latest ``` 命令行工具提供项目创建、代码生成、编译运行、资源打包等能力,安装与其他命令见[工具安装](../docs/cli/install.md)。 ## 最小 HTTP 服务 ```go package main import ( "github.com/gogf/gf/v2/frame/g" "github.com/gogf/gf/v2/net/ghttp" ) func main() { s := g.Server() s.BindHandler("/", func(r *ghttp.Request) { r.Response.Write("Hello World!") }) s.SetPort(8000) s.Run() } ``` 将代码保存为 `main.go`,执行 `go mod tidy` 与 `go run main.go`,然后访问 `http://127.0.0.1:8000`。 使用命令行工具创建完整项目脚手架: ```bash gf init hello cd hello && gf run main.go ``` ## 阅读建议 - 首次使用:从[下载与使用](../quick/install.md)开始,按基础教程顺序阅读 - 了解模块划分:阅读[源码结构](source-map.md) - 使用 AI 生成代码:先读 [AI 开发约定](ai-conventions.md) - 按组件查找页面:查看[完整索引](../llms.txt) 本项目的文档按官方文档的章节组织,代码示例与接口说明均来自对应页面。框架设计、组件用法和常见问题集中在 `docs` 目录,示例项目集中在 `examples` 目录。 ## 许可与来源 框架代码采用 MIT 许可。文档内容来自 GoFrame 官方文档仓库 `gogf/gf-site`,版权归 GoFrame 项目所有。来源与构建方式见项目根目录的 `README.md`。 # 源码结构 GoFrame 主仓库采用多模块结构。根模块提供核心框架,`cmd/gf` 与 `contrib` 下的每个目录都是独立 Go 模块,使用时按需引入,避免为单个功能引入过多依赖。 | 模块 | 导入路径 | 说明 | | --- | --- | --- | | 根模块 | `github.com/gogf/gf/v2` | 核心框架与基础组件 | | 命令行工具 | `github.com/gogf/gf/cmd/gf/v2` | `gf` 命令 | | 扩展模块 | `github.com/gogf/gf/contrib/*/v2` | 数据库驱动、注册中心、配置适配器、链路导出器等 | ## 顶层包 | 路径 | 职责 | | --- | --- | | `frame/g`、`frame/gins` | 单例入口与类型门面,提供 `g.Server()`、`g.DB()`、`g.Cfg()` 等访问方法 | | `net/` | HTTP 服务、TCP 与 UDP、服务注册发现、负载均衡、链路跟踪、OpenAPI | | `os/` | 配置、命令行、定时任务、日志、文件、缓存、Session 等系统能力 | | `database/` | ORM 核心 `gdb` 与 Redis 客户端核心 `gredis` | | `container/` | 并发安全数组、字典、集合、链表、队列与泛型类型 | | `encoding/` | JSON、XML、YAML、TOML、INI 等编解码 | | `text/` | 字符串处理 `gstr` 与正则表达式 `gregex` | | `util/` | 类型转换 `gconv`、数据校验 `gvalid` 与常用工具 | | `crypto/` | AES、DES、RSA、MD5、SHA、CRC32 | | `errors/` | 带堆栈的错误对象与错误码 | | `internal/` | 私有实现,不支持在根模块之外导入 | | `test/gtest` | 框架测试辅助工具 | ## 扩展模块 `contrib` 下的模块独立发布,导入路径以 `/v2` 结尾。 | 分类 | 模块 | | --- | --- | | 配置 | `contrib/config/apollo`、`consul`、`kubecm`、`nacos`、`polaris` | | 数据库驱动 | `contrib/drivers/clickhouse`、`dm`、`gaussdb`、`mariadb`、`mssql`、`mysql`、`oceanbase`、`oracle`、`pgsql`、`sqlite`、`sqlitecgo`、`tidb` | | 指标 | `contrib/metric/otelmetric` | | NoSQL | `contrib/nosql/redis` | | 注册中心 | `contrib/registry/consul`、`etcd`、`file`、`nacos`、`polaris`、`zookeeper` | | RPC | `contrib/rpc/grpcx` | | SDK | `contrib/sdk/httpclient` | | 链路导出 | `contrib/trace/otlpgrpc`、`otlphttp` | 数据库驱动与 Redis 适配器通过导入触发注册,核心包只定义接口。例如使用 MySQL 时同时导入驱动: ```go import ( "github.com/gogf/gf/contrib/drivers/mysql/v2" "github.com/gogf/gf/v2/frame/g" ) ``` ## 关键约定 - `frame/g` 是门面,不是独立实现。框架内部代码通常直接导入底层包,业务代码可以使用 `g.*` 简化调用。 - `util/gconv` 是类型转换的基础。多数公开接口接收 `any` 并通过 `gconv` 完成转换,修改参数处理逻辑时需要同时检查转换行为。 - `internal/` 属于私有包,不应在根模块之外导入,也不应在导出签名中暴露其类型。 - 测试统一使用 `test/gtest` 的 `gtest.C` 写法与 `t.Assert*` 断言,不使用标准库 `testing` 直接编写断言。 - 工程分层与目录规范见[工程目录设计](../docs/design/project-structure.md)与[代码分层设计](../docs/design/project-layer.md)。 ## 相关页面 - [框架概览](overview.md) - [数据库 ORM](../docs/core/gdb.md) - [开发工具](../docs/cli/install.md) - [微服务开发](../docs/micro-service.md) # AI 开发约定 本页汇总使用 AI 生成 GoFrame 代码时需要遵守的项目约定。约定来自 GoFrame Skills 技能定义与框架文档,适用于 GoFrame v2 项目。 仅在目标项目使用 GoFrame v2 时适用:`go.mod` 依赖 `github.com/gogf/gf/v2`,或代码导入了该模块及其子包。 ## 项目与代码组织 - 完整项目使用 `gf init` 创建脚手架,命令见[项目创建](../docs/cli/init.md)。 - `dao`、`do`、`entity` 由生成工具维护,不手工创建或修改。生成命令见[数据规范-gen dao](../docs/cli/gen-dao.md)。 - 业务逻辑直接放在 `service` 目录。除非明确要求,不新增 `logic` 目录。 - 新增方法或变量前先检索现有实现,优先复用。 ## 错误处理 所有可能出错的调用都要显式处理,并使用 `gerror` 包装,保留完整堆栈。 ```go if err != nil { return gerror.Wrap(err, "创建用户失败") } ``` ## 数据库操作 - 写操作使用 DO 对象,位于 `internal/model/do`。不使用 `g.Map` 或 `map[string]any` 构造更新数据。 - DO 字段类型为 `interface{}`,未赋值字段保持 `nil`,ORM 会自动忽略。 - 需要显式写入 `NULL` 时使用 `gdb.Raw("NULL")`。 ```go data := do.User{} if password != "" { data.PasswordHash = hash } if isAdmin != nil { data.IsAdmin = *isAdmin } dao.Users.Ctx(ctx).Where(cols.Id, id).Data(data).Update() ``` ```go dao.Instances.Ctx(ctx).Where(cols.Id, id).Data(do.Instance{IdleSince: gdb.Raw("NULL")}).Update() ``` ## 时间字段与软删除 表包含 `created_at`、`updated_at`、`deleted_at` 时,ORM 自动维护对应字段并自动过滤软删除数据。 - 不手工设置 `created_at` 与 `updated_at`。 - 不手工追加 `deleted_at IS NULL` 查询条件。 - 删除使用 `Delete()`,框架会转换为软删除更新。 ```go dao.User.Ctx(ctx).Where(do.User{Id: id}).Delete() ``` 时间字段名称与行为可以通过配置调整,见[时间维护](../docs/core/gdb-chaining-soft-time.md)。 ## 变量声明 同一作用域内三个及以上相关变量使用 `var` 块,便于对齐和阅读。 ```go var ( authSvc *auth.Service bizCtxSvc *bizctx.Service middlewareSvc *middleware.Service ) ``` ## 测试 测试使用 `test/gtest`,采用 `gtest.C` 与断言方法,保持与框架其他测试一致。 ```go func TestUserCreate(t *testing.T) { gtest.C(t, func(t *gtest.T) { user, err := service.User.Create(ctx, input) t.AssertNil(err) t.AssertGT(user.Id, 0) }) } ``` ## 相关页面 - [GoFrame Skills 安装与使用](../ai/goframe-skills.md) - [HTTP 项目示例](../examples/practices/user-http-service.md) - [gRPC 项目示例](../examples/practices/user-grpc-service.md) - [工程目录设计](../docs/design/project-structure.md) - [代码分层设计](../docs/design/project-layer.md) # 下载与使用 大家好,欢迎访问 `GoFrame` 框架的快速开始章节。 由于 `GoFrame` 是一款模块化、低耦合设计的开发框架,包含了常用的基础组件和开发工具,既可以作为完整的业务项目框架使用也可以作为独立的组件库使用。 我们本章节从框架的下载安装使用开始,到指导大家完成一个较完善的`Web Server`接口开发。 **说明** 如果还未准备好`Go`开发环境,建议在进一步开始之前,先查阅环境安装文档:[环境安装](../docs/install-go.md) 了解一些基础的`Go`语言知识再进行下一步。 如果在快速开始的任何章节中遇到任何问题,欢迎在评论区留言,我们将尽可能及时提供解答。 ## 限制 为保证框架的稳定性和安全性,`GoFrame`框架要求的最低的基础`Go`语言版本通常会比最新的`Go`语言版本低`1~3`个版本。 当前最新框架版本要求的最低`Go`语言版本: ```bash golang版本 >= 1.23 ``` ## 安装 初始化`go.mod`: ```bash go mod init hello ``` 下载框架最新版本: ```bash go get -u -v github.com/gogf/gf/v2 ``` ## 运行 我们尝试运行以下代码: ```go title="main.go" package main import ( "fmt" "github.com/gogf/gf/v2" ) func main() { fmt.Println("Hello GoFrame:", gf.VERSION) } ``` 这段简单的代码示例是打印当前引入的`GoFrame`框架版本。通过以下命令执行: ```bash go run main.go ``` 将会在终端看到: ```bash Hello GoFrame: v2.8.3 ``` 完成。这表示已经成功引用了`GoFrame`到当前项目中。 下一步,我们将使用`GoFrame`开发一个简单的`HTTP Server`服务。 # Hello World `GoFrame` 是一款基础设施建设比较完善的模块化框架, `Web Server` 模块是其中比较核心的模块,我们这里将 `Web` 服务开发作为框架入门的选择,便于大家更容易学习和理解。 ## Hello World 我们先来开发一个简单的`Web Server`程序。 - 新建`main.go`文件 ```go title="main.go" package main import ( "github.com/gogf/gf/v2/frame/g" "github.com/gogf/gf/v2/net/ghttp" ) func main() { s := g.Server() s.BindHandler("/", func(r *ghttp.Request) { r.Response.Write("Hello World!") }) s.SetPort(8000) s.Run() } ``` - 配置`go mod`并安装依赖 ```shell go mod init main go mod tidy ``` 我们来看看这段代码: - 任何时候,都可以通过 `g.Server()` 方法获得一个默认的 `Server` 对象,该方法采用**单例模式**设计, 也就是说,多次调用该方法,返回的是同一个 `Server` 对象。其中的`g`组件是框架提供的一个耦合组件,封装和初始化一些常用的组件对象,为业务项目提供便捷化的使用方式。 - 通过`Server`对象的`BindHandler`方法绑定路由以及路由函数。在本示例中,我们绑定了`/`路由,并指定路由函数返回`Hello World`。 - 在路由函数中,输入参数为当前请求对象`r *ghttp.Request`,该对象包含当前请求的上下文信息。在本示例中,我们通过`r.Response`返回对象直接`Write`返回结果信息。 - 通过`SetPort`方法设置当前`Server`监听端口。在本示例中,我们监听`8000`端口,如果在没有设置端口的情况下,它默认会监听一个随机的端口。 - 通过 `Run()` 方法阻塞执行 `Server` 的监听运行。 ## 执行结果 运行该程序,将在终端看到类似以下日志信息: ```html $ go run main.go 2024-10-27 21:30:39.412 [INFO] pid[58889]: http server started listening on [:8000] 2024-10-27 21:30:39.412 [INFO] {08a0b0086e5202184111100658330800} openapi specification is disabled ADDRESS | METHOD | ROUTE | HANDLER | MIDDLEWARE ----------|--------|-------|-----------------|------------- :8000 | ALL | / | main.main.func1 | ----------|--------|-------|-----------------|------------- ``` 在默认的日志打印中包含以下信息: - 当前进程号`58889`,以及监听的地址`:8000`(表示监听本机所有IP地址的`8000`端口)。 - 由于框架带有自动接口文档生成功能,本示例中未启用,因此提示`openapi specification is disabled`。 关于接口文档的自动生成,在开发手册中对应章节会详细讲解,本示例不作介绍。 - 最后会打印当前`Server`的路由列表。由于我们只监听了`/`路由,那么这里只打印了一个路由信息。在路由信息表中: | 路由字段 | 字段描述 | |----------|----------| | `ADDRESS` | 表示该路由的监听地址,同一个进程可以同时运行多个`Server`,不同的`Server`可以监听不同的地址。 | | `METHOD` | 表示路由监听的`HTTP Method`信息,比如`GET/POST/PUT/DELETE`等。这里的`ALL`标识监听所有的`HTTP Method`。 | | `ROUTE` | 表示监听的具体路由地址信息。 | | `HANDLER` | 表示路由函数的名称。由于本示例使用的是闭包函数,因此看到的是一个临时函数名称`main.main.func1`。 | | `MIDDLEWARE` | 表示绑定到当前路由的中间件函数名称,中间件是`Server`中一种经典的拦截器,后续章节中会有详细讲解,这里暂不做介绍。 | 运行后,我们尝试访问 http://127.0.0.1:8000/ 将在页面中看到输出 ## 学习小结 太棒了。使用`GoFrame`框架开发第一个`Web Server`程序。 下一步,我们将会学习如何在`Web Server`中获取客户端提交的参数数据。 # 获取请求参数 在前面章节中,我们学会了如何开发一个简单的`Web Server`程序。 在本章节中,我们将会尝试着在`Web Server`中获取客户端提交的参数信息。 为简化示例,我们使用`Query String`的方式来传递请求参数。 ## 参数示例 我们将之前的`Hello World`示例程序进行简单的改造: ```go title="main.go" package main import ( "github.com/gogf/gf/v2/frame/g" "github.com/gogf/gf/v2/net/ghttp" ) func main() { s := g.Server() s.BindHandler("/", func(r *ghttp.Request) { r.Response.Writef( "Hello %s! Your Age is %d", r.Get("name", "unknown").String(), r.Get("age").Int(), ) }) s.SetPort(8000) s.Run() } ``` 在`GoFrame`框架中,获取参数非常便捷。在本示例中,我们通过`r.Get`方法获取客户端提交的参数,该方法能够获取所有`HTTP Method`提交的参数, 比如`Query String/Form/Body`等,其内部将会根据客户端提交的类型自动识别解析,比如支持自动识别参数格式例如`json/xml`等。该方法的定义如下: ```go func (r *Request) Get(key string, def ...interface{}) *gvar.Var ``` 可以看到,`Get`方法接受两个参数,第一个为参数名称,第二个参数为非必须参数,表示默认值。返回结果为一个`*gvar.Var`对象,该对象为`GoFrame`框架 提供的运行时泛型对象,开发者可以根据业务场景需要将参数转换为各种类型。 ## 执行结果 运行后,我们访问 http://127.0.0.1:8000/?name=john&age=18 可以看到,页面输出结果符合预期。 我们访问 http://127.0.0.1:8000/ 可以看到,页面输出结果同样符合预期。当未传递`name`参数时,程序将会使用默认值`unknown`,并且`age`参数会使用`int`类型的默认值`0`。 ## 学习小结 在本章节中,我们学会了如何在HTTP路由函数中获取客户端提交的参数。 但大家可以看到,在本示例的代码中,存在一些显而易见的问题: - 参数的接收使用了**硬编码的参数名称**,如果在真实的项目开发中,很容易由于名称拼写错误引发异常。 - 硬编码的参数名称不能确定业务含义和数据类型,很难进行长期管理维护,例如难以对参数进行描述说明、类型校验等基本操作。 在下一章节中,我们尝试通过结构化的参数对象来规避这一问题。 # 请求数据结构 在本示例,我们尝试通过数据结构化的方式,解决上一章节的**参数名称硬编码**问题。 ## 请求对象 我们定义一个请求的数据结构来接收客户端提交的参数信息: ```go type HelloReq struct { Name string // 名称 Age int // 年龄 } ``` 太棒了,看起来我们既可以对参数进行注释描述,也能确定参数的类型,不再需要参数名称硬编码。 ## 示例代码 我们调整我们前面的`Web Server`程序如下: ```go title="main.go" package main import ( "github.com/gogf/gf/v2/frame/g" "github.com/gogf/gf/v2/net/ghttp" ) type HelloReq struct { Name string // 名称 Age int // 年龄 } func main() { s := g.Server() s.BindHandler("/", func(r *ghttp.Request) { var req HelloReq if err := r.Parse(&req); err != nil { r.Response.Write(err.Error()) return } if req.Name == "" { r.Response.Write("name should not be empty") return } if req.Age <= 0 { r.Response.Write("invalid age value") return } r.Response.Writef( "Hello %s! Your Age is %d", req.Name, req.Age, ) }) s.SetPort(8000) s.Run() } ``` 在本示例中: - 我们通过`r.Parse`方法将请求参数映射到请求对象上,随后可以通过对象的方式来使用参数。 `r.Parse`方法支持自动解析客户端提交参数,并赋值到指定对象上。 内部有固定的名称映射逻辑,将在开发手册的类型转换组件中详细了解到,这里不作过多介绍。 - 同时,我们在本示例中增加了校验逻辑,`Name`及`Age`参数不能为空。 ## 执行结果 运行后,我们访问 http://127.0.0.1:8000/?name=john&age=18 可以看到,页面输出结果符合预期。 我们尝试一下错误的参数请求 http://127.0.0.1:8000/ 可以看到,参数被按照预期校验,页面输出结果同样符合预期 ## 学习小结 在本章节中,我们学会了通过结构化请求对象的方式来规避参数名称硬编码的问题, 也能很好地维护参数的名称、描述和类型定义。 同时,我们也对参数增加了必要的校验逻辑,以保障参数的完整性。 但在该示例代码中,同样有值得改进的地方: - 其中的`r.Parse`是属于业务无关的操作,理应当独立于业务逻辑之外处理。 - 如果接口比较多,所有的接口中都要重复进行`r.Parse`操作,操作比较冗余。 - 针对数据校验,如果接口的参数比较多,进行大量的`if`数据校验操作太过于繁琐。 是否有更好的方式来简化并解决这些问题呢?答案是肯定的,我们下一个章节解决这一问题。 # 使用规范路由 为了简化路由注册方式,避免一些繁琐的参数处理细节, 让开发者将精力聚焦于业务逻辑本身,`GoFrame`框架提供了规范化的路由注册方式。 规范化的路由注册方式,我们为了见名知意,便命名为了**规范路由**。 ## 数据结构定义 在规范路由中,我们同样定义一个请求的数据结构来接收客户端提交的参数信息,但同时需要定义一个返回对象。 目的是为了未来返回参数扩展的需要,以及未来标准化接口文档生成的需要。 ```go type HelloReq struct { g.Meta `path:"/" method:"get"` Name string `v:"required" dc:"姓名"` Age int `v:"required" dc:"年龄"` } type HelloRes struct {} ``` 简要介绍: - 在请求对象中,我们多了一个`g.Meta`对象的引用,并给定了一些结构体标签。该对象为**元数据对象**,用于给结构体嵌入 一些定义的标签信息。例如在本示例中: - `path`:表示注册的路由地址。 - `method`:表示注册绑定的`HTTP Method`。 - 在属性中同样出现两个新的标签名称: - `v`:表示校验规则,为`valid`的缩写,用于自动校验该参数。这里使用`v:"required"`表示该参数为必需参数,如果客户端未传递该参数时,服务端将会校验失败。 - `dc`:表示参数描述信息,为`description`的缩写,用于描述该参数的含义。 **说明** 在开发手册的对应章节中,有关于全部标签信息以及校验组件的详细讲解,这里只需要了解其作用即可,不做过多介绍。 ## 路由对象管理 为了更好地管理路由注册,特别是接口比较多的场景下,如果手动一一去配置路由与路由函数关系太过于繁琐。 我们通过对象化的形式来封装路由函数,通过对象化封装的方式来简化我们的路由管理。 我们定义一个路由对象如下: ```go type Hello struct{} func (Hello) Say(ctx context.Context, req *HelloReq) (res *HelloRes, err error) { r := g.RequestFromCtx(ctx) r.Response.Writef( "Hello %s! Your Age is %d", req.Name, req.Age, ) return } ``` - 我们定义了一个`Hello`对象,该对象用于封装路由函数,其所有定义的公开方法都将被作为路由函数进行注册。 - 可以看到该路由对象的`Say`方法的路由函数的定义方式,相比较于`func(*ghttp.Request)`的路由函数定义方式,更符合业务逻辑函数的定义风格。 - 在路由回调方法`Say`中,我们通过`g.RequestFromCtx`方法从`ctx`获取原始的`*ghttp.Request`请求对象,用于自定义返回内容数据。 ## 完整示例代码 我们调整我们前面的`Web Server`程序如下: ```go title="main.go" package main import ( "context" "github.com/gogf/gf/v2/frame/g" "github.com/gogf/gf/v2/net/ghttp" ) type HelloReq struct { g.Meta `path:"/" method:"get"` Name string `v:"required" dc:"姓名"` Age int `v:"required" dc:"年龄"` } type HelloRes struct{} type Hello struct{} func (Hello) Say(ctx context.Context, req *HelloReq) (res *HelloRes, err error) { r := g.RequestFromCtx(ctx) r.Response.Writef( "Hello %s! Your Age is %d", req.Name, req.Age, ) return } func main() { s := g.Server() s.Group("/", func(group *ghttp.RouterGroup) { group.Bind( new(Hello), ) }) s.SetPort(8000) s.Run() } ``` 在本示例中: - 通过`s.Group`的分组路由方式定义一组路由注册,在其回调方法中注册的所有路由,都会带有其定义的分组路由前缀`/`。 - 通过`group.Bind`方法注册路由对象,该方法将会遍历路由对象的所有公开方法,读取方法的输入输出结构体定义,并对其执行路由注册。 ## 执行结果 运行后,我们访问 http://127.0.0.1:8000/?name=john&age=18 可以看到,页面输出结果符合预期。 我们尝试一下错误的参数请求 http://127.0.0.1:8000/ 但我们发现,页面没有输出任何的结果? **这是由于参数校验失败,并未进入到我们的路由函数中,而是被`Server`直接返回了。** ## 学习小结 在本章节我们学会了规范的路由注册方式,但是还缺少对返回结果,特别是产生错误之后的统一处理控制。 那么,我们应该如何捕获校验失败错误并自定义返回数据呢?我们将在下一章节更进一步介绍。 # 中间件初试用 为了解决上一章节遗留的疑问:如何捕获返回的错误对象并作自定义的错误处理。 在本章节中,我们将会先简单介绍`Web Server`的中间件特性,再回答这个疑问。 ## 中间件介绍 中间件是一种拦截器设计,在`Web Server`中可以拦截请求和返回结果,并在其前后进行自定义处理逻辑。 中间件的定义和普通的路由函数一样,但是可以在 `Request` 参数中使用 `Middleware` 属性对象来控制请求流程。 中间件的类型分为两种:**前置中间件**和**后置中间件**。前置即在路由服务函数调用之前调用,后置即在其后调用。 ```go func Middleware(r *ghttp.Request) { // 前置中间件处理逻辑 r.Middleware.Next() // 后置中间件处理逻辑 } ``` 在中间件中执行完成处理逻辑后,使用 `r.Middleware.Next()` 方法进一步执行下一个流程; 如果这个时候直接退出不调用 `r.Middleware.Next()` 方法的话,将会退出后续的执行流程(例如可以用于请求的鉴权处理)。 ## 使用中间件 我们使用中间件对上一章节的程序进行简单的改造,如下: ```go title="main.go" package main import ( "context" "github.com/gogf/gf/v2/frame/g" "github.com/gogf/gf/v2/net/ghttp" ) type HelloReq struct { g.Meta `path:"/" method:"get"` Name string `v:"required" dc:"姓名"` Age int `v:"required" dc:"年龄"` } type HelloRes struct{} type Hello struct{} func (Hello) Say(ctx context.Context, req *HelloReq) (res *HelloRes, err error) { r := g.RequestFromCtx(ctx) r.Response.Writef( "Hello %s! Your Age is %d", req.Name, req.Age, ) return } func ErrorHandler(r *ghttp.Request) { // 执行路由函数 r.Middleware.Next() // 判断是否产生错误 if err := r.GetError(); err != nil { r.Response.Write("error occurs: ", err.Error()) return } } func main() { s := g.Server() s.Group("/", func(group *ghttp.RouterGroup) { group.Middleware(ErrorHandler) group.Bind( new(Hello), ) }) s.SetPort(8000) s.Run() } ``` - 我们定义了一个错误处理的中间件`ErrorHandler`,在该中间件中我们先通过`r.Middleware.Next()`执行路由函数流程, 随后通过`r.GetError()`获取路由函数是否有错误产生。如果产生错误,那么直接输出错误信息。 - 在路由注册中,我们通过`group.Middleware(ErrorHandler)`给该分组路由下的所有注册的路由,都绑定了错误处理的中间件。 ## 执行结果 运行后,终端输出: ```text 2024-11-06 22:30:06.927 [INFO] pid[35434]: http server started listening on [:8000] 2024-11-06 22:30:06.927 [INFO] {905637567a67051830833b2189796dda} openapi specification is disabled ADDRESS | METHOD | ROUTE | HANDLER | MIDDLEWARE ----------|--------|-------|-------------------|-------------------- :8000 | GET | / | main.(*Hello).Say | main.ErrorHandler ----------|--------|-------|-------------------|-------------------- ``` 这里的`MIDDLEWARE`部分多了一个`main.ErrorHandler`方法,表示该路由绑定的中间件名称。 我们访问 http://127.0.0.1:8000/?name=john&age=18 可以看到,页面输出结果符合预期。 我们尝试一下错误的参数请求 http://127.0.0.1:8000/ 可以看到,页面也输出了错误的提示信息。 ## 学习小结 我们使用中间件对请求错误进行自定义的处理,并捕获校验错误返回自定义的错误信息。 可以看到,中间件的功能非常灵活且强大,当然不仅仅局限于处理校验错误这种小场景。 试想一下,假如我们的项目中有很多接口,通常这些接口的输出格式都是固定的,例如都是`json`格式。 那么能否直接通过中间件的方式,统一错误处理以及返回的数据结构呢?答案当然是肯定的,我们将在下一章节介绍。 # 统一返回结构 本章节我们将使用固定的数据格式返回结果,无论路由函数执行成功还是失败,我们以`json`格式返回结果给调用端。 ## 返回格式定义 我们定义一个统一的数据结构: ```go type Response struct { Message string `json:"message" dc:"消息提示"` Data interface{} `json:"data" dc:"执行结果"` } ``` 由于我们需要返回`json`数据格式,因此我们给每个字段都加上`json`标签。 ## 接口数据结构 ```go type HelloReq struct { g.Meta `path:"/" method:"get"` Name string `v:"required" json:"name" dc:"姓名"` Age int `v:"required" json:"age" dc:"年龄"` } type HelloRes struct { Content string `json:"content" dc:"返回结果"` } ``` - 我们给`HelloRes`增加了一个`Content`属性,用于路由函数返回具体的数据。 - 由于需要通过`json`数据格式返回数据,因此所有的属性字段均增加`json`标签。 ## 路由函数 ```go type Hello struct{} func (Hello) Say(ctx context.Context, req *HelloReq) (res *HelloRes, err error) { res = &HelloRes{ Content: fmt.Sprintf( "Hello %s! Your Age is %d", req.Name, req.Age, ), } return } ``` 我们这里通过`HelloRes`返回数据结构返回执行结果,而不是像前面的示例那样通过`g.RequestFromCtx(ctx)`获取原始的请求对象直接`Write`结果。 ## 中间件定义 ```go func Middleware(r *ghttp.Request) { r.Middleware.Next() var ( msg string res = r.GetHandlerResponse() err = r.GetError() ) if err != nil { msg = err.Error() } else { msg = "OK" } r.Response.WriteJson(Response{ Message: msg, Data: res, }) } ``` 在该中间件中: - 通过`r.GetHandlerResponse()`方法获取路由函数的执行结果,即路由函数返回的第一个结果参数`*HelloRes`。 - 通过`r.GetError()`获取路由函数的执行状态,即路由函数返回的第二个结果参数`error`,如果该结果不为`nil`,表示该路由函数执行产生了错误。 - 通过`r.Response.WriteJson`将结果整合到统一的返回数据结构`Response`,并编码为`json`格式返回给调用端。 ## 完整示例代码 ```go title="main.go" package main import ( "context" "fmt" "github.com/gogf/gf/v2/frame/g" "github.com/gogf/gf/v2/net/ghttp" ) type Response struct { Message string `json:"message" dc:"消息提示"` Data interface{} `json:"data" dc:"执行结果"` } type HelloReq struct { g.Meta `path:"/" method:"get"` Name string `v:"required" json:"name" dc:"姓名"` Age int `v:"required" json:"age" dc:"年龄"` } type HelloRes struct { Content string `json:"content" dc:"返回结果"` } type Hello struct{} func (Hello) Say(ctx context.Context, req *HelloReq) (res *HelloRes, err error) { res = &HelloRes{ Content: fmt.Sprintf( "Hello %s! Your Age is %d", req.Name, req.Age, ), } return } func ResponseMiddleware(r *ghttp.Request) { r.Middleware.Next() var ( msg string res = r.GetHandlerResponse() err = r.GetError() ) if err != nil { msg = err.Error() } else { msg = "OK" } r.Response.WriteJson(Response{ Message: msg, Data: res, }) } func main() { s := g.Server() s.Group("/", func(group *ghttp.RouterGroup) { group.Middleware(ResponseMiddleware) group.Bind( new(Hello), ) }) s.SetPort(8000) s.Run() } ``` 该示例中的所有代码我们已经做过了介绍,我们直接执行即可。 ## 执行结果 运行后,我们访问 http://127.0.0.1:8000/?name=john&age=18 可以看到,页面输出结果符合预期。 我们尝试一下错误的参数请求 http://127.0.0.1:8000/ 可以看到,页面输出了错误的结果信息,返回数据类型也是`json`格式。 ## 学习小结 在本章节中,我们学会了使用常用的`json`格式对接口进行统一的数据格式封装,这在有着成百上千接口的业务项目中是非常有必要的。 可以看到,我们已经将参数完整地进行了结构化,结构中带有详细的输入输出参数描述、类型定义甚至数据校验规则, 那么我们能否基于这些信息自动化地生成接口文档呢?答案也是肯定的,我们将在下一章节介绍。 # 生成接口文档 使用`GoFrame`框架自动化生成接口文档非常简单。 我们首先对前面章节的接口数据结构进行简单的完善,以便生成的接口文档更加优雅。 ## 接口定义完善 ```go type HelloReq struct { g.Meta `path:"/" method:"get" tags:"Test" summary:"Hello world test case"` Name string `v:"required" json:"name" dc:"姓名"` Age int `v:"required" json:"age" dc:"年龄"` } type HelloRes struct { Content string `json:"content" dc:"返回结果"` } ``` 可以看到,基于前面章节的接口数据结构定义,我们在其`g.Meta`中增加了两个标签: - `tags`: 该接口属于哪个分类,或者接口模块。 - `summary`: 接口描述。 **说明** 这些标签都是`OpenAPIv3`标准接口协议的规范字段,在开发手册对应章节有关于接口文档生成以及标签的详细讲解,我们这里不作过多介绍。 ## 完整代码示例 ```go title="main.go" package main import ( "context" "fmt" "github.com/gogf/gf/v2/frame/g" "github.com/gogf/gf/v2/net/ghttp" ) type Response struct { Message string `json:"message" dc:"消息提示"` Data interface{} `json:"data" dc:"执行结果"` } type HelloReq struct { g.Meta `path:"/" method:"get" tags:"Test" summary:"Hello world test case"` Name string `v:"required" json:"name" dc:"姓名"` Age int `v:"required" json:"age" dc:"年龄"` } type HelloRes struct { Content string `json:"content" dc:"返回结果"` } type Hello struct{} func (Hello) Say(ctx context.Context, req *HelloReq) (res *HelloRes, err error) { res = &HelloRes{ Content: fmt.Sprintf( "Hello %s! Your Age is %d", req.Name, req.Age, ), } return } func ResponseMiddleware(r *ghttp.Request) { r.Middleware.Next() var ( msg string res = r.GetHandlerResponse() err = r.GetError() ) if err != nil { msg = err.Error() } else { msg = "OK" } r.Response.WriteJson(Response{ Message: msg, Data: res, }) } func main() { s := g.Server() s.Group("/", func(group *ghttp.RouterGroup) { group.Middleware(ResponseMiddleware) group.Bind( new(Hello), ) }) s.SetOpenApiPath("/api.json") s.SetSwaggerPath("/swagger") s.SetPort(8000) s.Run() } ``` 在本示例中: - 通过`s.SetOpenApiPath("/api.json")`启用`OpenAPIv3`的接口文档生成,并指定生成的文件路径`/api.json`。 - 通过`s.SetSwaggerPath("/swagger")`启用内置的`Swagger`接口文档UI,并指定客访问的UI地址为`/swagger`。内置的`Swagger UI`可自定义修改,具体可参考开发手册相应章节。 ### 关于`OpenAPIv3` `OpenAPIv3`是目前业内的接口文档标准协议,用于接口文档的定义,通常使用`json`格式生成。该接口文档`json`文件可以用许多接口管理工具打开,例如`Swagger UI/PostMan/APIFox`等等。 ### 关于`Swagger` `Swagger`是常用的接口文档UI展示工具,支持多种接口文档格式,最常用的接口文档格式当然是`OpenAPIv3`。 在本示例中,如果需要使用内置的`Swagger UI`查看接口文档,需要同时启用`OpenAPIv3`和`Swagger UI`。即调用`SetOpenApiPath`和`SetSwaggerPath`方法设置对应的访问路径。 ## 执行结果 ### 终端输出 执行后,终端输出: ```text 2024-10-28 21:56:42.747 [INFO] pid[87280]: http server started listening on [:8000] 2024-10-28 21:56:42.747 [INFO] {d84db2976ea202187ba40f5d0657e4d7} swagger ui is serving at address: http://127.0.0.1:8000/swagger/ 2024-10-28 21:56:42.747 [INFO] {d84db2976ea202187ba40f5d0657e4d7} openapi specification is serving at address: http://127.0.0.1:8000/api.json ADDRESS | METHOD | ROUTE | HANDLER | MIDDLEWARE ----------|--------|------------|-------------------------------------------------------|-------------------- :8000 | GET | / | main.(*Hello).Say | main.Middleware ----------|--------|------------|-------------------------------------------------------|-------------------- :8000 | ALL | /api.json | github.com/gogf/gf/v2/net/ghttp.(*Server).openapiSpec | ----------|--------|------------|-------------------------------------------------------|-------------------- :8000 | ALL | /swagger/* | github.com/gogf/gf/v2/net/ghttp.(*Server).swaggerUI | HOOK_BEFORE_SERVE ----------|--------|------------|-------------------------------------------------------|-------------------- ``` 我们可以看到,终端输出的提示信息发生了一些变化: - 终端输出了`swagger`的地址`http://127.0.0.1:8000/swagger/`点击该地址可以直接打开`Swagger UI`界面。 - 终端输出了`openapi`的接口文档地址`http://127.0.0.1:8000/api.json`,该地址文件可以使用很多接口文档UI查看工具打开。 - 路由信息也多了两行地址,对应的是`swagger`和`openapi`的地址。其中`swagger`的路由地址使用了`Server`的`HOOK`钩子特性,`HOOK`是一种比中间件更加自由的请求拦截方式,可以同时拦截动态接口和静态资源请求。有固定的几种埋点`HOOK`,这里使用的埋点是`HOOK_BEFORE_SERVE`,具体的细节请查看开发手册中`Web Server`的相关章节,这里不展开介绍。 ### `Swagger UI` 我们点击`swagger`地址 http://127.0.0.1:8000/swagger/ 查看: - 由于我们只定义了一个接口,所以`Swagger`页面只看一个接口定义,在`Test`分类下展示的具体的接口名称`Hello world test case`。 - 在接口详情中,展示了接口的输入与输出数据结构。甚至对于输入的数据结构,`GoFrame`生成的接口文档能够自动识别其校验规则,比如是否必需参数。并展示了各个参数的数据类型、参数描述。 ## 学习小结 通过本章节的学习,我们学习到了以下几点知识: - 通过对路由接口进行规范化的输入输出数据结构定义,可以很好地维护接口,特别是业务较复杂、接口比较多的项目。 - 通过`GoFrame`框架自动化接口文档生成特性,可以很方便地生成基于`OpenAPIv3`标准化的接口文档、`Swagger UI`展示页面,极大降低接口维护成本、提高服务间协作效率。 # What's Next ## 学习小结 通过快速章节,应当了解到了关于`Web Server`接口的完整开发流程,以及一些实用特性的掌握。在这些章节中,我们只是点到即止,更详细的讲解以及功能特性介绍请查看对应开发手册章节。当对某一模块的使用感到疑惑不解时,建议仔细阅读对应的开发手册章节,里面对各个组件重要的功能特性、使用方式、常见问题做了详细的讲解。 **说明** 如果开始尝试翻阅开发手册,需要注意的是,由于`GoFrame`是低耦合、模块化设计的,每一个模块都相对独立,因此开发手册中的文档也是基于模块化编写的,每个章节只会介绍对应的组件本身的使用。 接下来我们应该做什么呢? ## 项目脚手架 在接下来的章节中,我们将尝试使用`GoFrame`框架提供的项目脚手架,开发一个简单的、较完整的业务项目。 # 基本介绍 从本章节开始,我们主要主要以完整的业务项目为主,介绍如何使用框架提供的脚手架完成一个简单的接口项目。 **注意** **在进一步开始之前请注意**,由于 `GoFrame` 算得上真正意义的 `Golang` 企业级、工程化的开发框架,她设计严谨、易于使用、文档丰富且社区活跃, **一旦学习并掌握了框架的使用,将难以回到过去刀耕火种的开发年代**。 如果在快速开始的任何章节中遇到任何问题,欢迎在评论区留言,我们将尽可能及时提供解答。 ## 安装框架工具 框架提供的开发工具为开发者提供了便捷的开发指令简化开发工作,提供了例如工程脚手架、代码自动生成、工具及框架更新等实用命令。 ### 方式一:源码安装 通过下载源码到本地编译生成二进制后安装到系统目录,使用的是`go install`命令,这是万能的安装方式: ```bash go install github.com/gogf/gf/cmd/gf/v2@latest ``` ### 方式二:预编译安装 预编译安装是开源社区预编译好的二进制文件,供开发者直接使用,二进制文件托管存放到 `github` 仓库中。工具下载地址: [https://github.com/gogf/gf/releases](https://github.com/gogf/gf/releases) #### `Mac` 环境 ```bash wget -O gf "https://github.com/gogf/gf/releases/latest/download/gf_$(go env GOOS)_$(go env GOARCH)" && chmod +x gf && ./gf install -y && rm ./gf ``` 可能会遇到以下常见问题: - 如果当前系统没有安装 `wget` 命令,那么请使用 `brew install wget` 安装后继续执行该命令。 - 如果使用的是 `zsh` 终端,可能会存在 `gf` 别名冲突( `git fetch` 快捷指令),那么安装后(至少执行一次)请 **重启终端软件** 来继续使用。 - 的`Golang` 开发环境未准备好,缺少`GOOS`或`GOARCH`环境变量,建议先查阅环境安装文档:[环境安装](../docs/install-go.md)。 - 若无法解决的其他问题,那么可以使用后续的【源码编译安装】方式。 #### `Linux` 环境 ```bash wget -O gf "https://github.com/gogf/gf/releases/latest/download/gf_$(go env GOOS)_$(go env GOARCH)" && chmod +x gf && ./gf install -y && rm ./gf ``` 如果系统没有安装 `wget` 命令: - `Ubuntu/Debian` 系统请使用 `apt-get install wget -y` - `CentOS/RedHat` 系统请使用 `yum install wget -y` #### `Windows` 环境 手动下载对应的二进制文件,双击按照终端指令安装即可。如果双击安装失败,请采用最后万能的【源码编译安装】方式。 ### 验证安装 执行 `gf -v` 指令如果能打印出例如以下信息,表示已成功安装好了框架脚手架工具 ```html $ gf -v v2.7.4 Welcome to GoFrame! Env Detail: Go Version: go1.23.1 darwin/arm64 GF Version(go.mod): cannot find go.mod CLI Detail: Installed At: /Users/john/go/bin/gf Built Go Version: go1.23.1 Built GF Version: v2.7.4 Others Detail: Docs: https://goframe.org Now : 2024-10-29T13:30:30+08:00 ``` 注意其中的 `Go/GF Version` 是当前编译的二进制文件使用的 `Golang` 及 `GoFrame` 框架版本,其中的 `GoFrame Version` 是当前项目使用的 `GoFrame` 框架版本(自动检测当前目录下的 `go.mod`)。 **说明** 常见问题注意事项:如果使用的是 `zsh` 终端,可能会存在 `gf` 别名冲突( `git fetch` 快捷指令),那么安装后(至少执行一次)请 **重启终端软件** 来继续使用。 ## 创建项目模板 ```bash gf init demo -u ``` 该命令创建一个工程脚手架目录,项目名称是 `demo`,其中的 `-u` 参数用户指定是否更新项目中使用的 `goframe` 框架为最新版本。 `GoFrame`框架有独特的项目工程结构,工程目录结构介绍具体请参考: [工程目录设计](../docs/design/project-structure.md)。 框架的脚手架目录是按照通用性设计的,可以满足WEB、终端、微服务等业务开发场景。默认会生成一个 `HTTP Web Server` 的模板项目。在理解完成目录含义后,如果其中有不需要的目录,可以自行删除。 ## 运行项目模板 项目模板可以执行以下命令运行: ```bash cd demo && gf run main.go ``` 其中的 `gf run` 是框架开发工具的动态编译命令,也可以替换为 `go run` 命令。 执行后,终端输出: ```text $ cd demo && gf run main.go build: main.go go build -o ./main main.go ./main build running pid: 76159 2022-08-22 12:20:59.058 [INFO] swagger ui is serving at address: http://127.0.0.1:8000/swagger/ 2022-08-22 12:20:59.058 [INFO] openapi specification is serving at address: http://127.0.0.1:8000/api.json 2022-08-22 12:20:59.059 [INFO] pid[76159]: http server started listening on [:8000] ADDRESS | METHOD | ROUTE | HANDLER | MIDDLEWARE ----------|--------|------------|-----------------------------------------------------------------|---------------------------------- :8000 | ALL | /* | github.com/gogf/gf/v2/net/ghttp.internalMiddlewareServerTracing | GLOBAL MIDDLEWARE ----------|--------|------------|-----------------------------------------------------------------|---------------------------------- :8000 | ALL | /api.json | github.com/gogf/gf/v2/net/ghttp.(*Server).openapiSpec | ----------|--------|------------|-----------------------------------------------------------------|---------------------------------- :8000 | GET | /hello | demo/internal/controller.(*cHello).Hello | ghttp.MiddlewareHandlerResponse ----------|--------|------------|-----------------------------------------------------------------|---------------------------------- :8000 | ALL | /swagger/* | github.com/gogf/gf/v2/net/ghttp.(*Server).swaggerUI | HOOK_BEFORE_SERVE ----------|--------|------------|-----------------------------------------------------------------|---------------------------------- ``` 默认情况下项目会设置Web服务端口为 `8000`、开启 `OpenAPI` 接口文档、展示 `Swagger` 接口文档页面,这些关键信息都会展示到终端。默认情况下,会打印所有的路由信息到终端,该项目模板只会添加一个仅供演示的路由: `/hello`。 默认路由: [http://127.0.0.1:8000/hello](http://127.0.0.1:8000/hello) `Swagger` 接口文档页面: ## 升级框架版本 随时可以在项目根目录下(目录下有 `go.mod`),执行以下命令更新使用最新的框架版本: ```bash gf up -a ``` ## 学习小结 通过本章节的学习,我们知道如何下载和安装脚手架的`cli`工具,并通过该工具创建一个脚手架项目模板。 在下一章节,我们简单介绍一下这个脚手架项目模板的项目启动流程。 # 项目启动 当开始第一个项目的时候,可能对工程下面有这么多目录感觉困惑,没关系,可以通过这个章节 [工程目录设计](../docs/design/project-structure.md) 先了解一下各个目录的作用。接下来我们会介绍一下项目是如何启动的,一个程序的启动串联了哪些目录,让大家对程序整体启动的经脉有所了解。 ## main.go 所有的程序入口都是由 `main.go` 进入,该文件主要是调用 `internal/cmd` 包的对应命令引导程序启动。在项目模板中,默认会执行 `internal/cmd` 包的 `Main` 对象 `Run` 命令引导程序启动。 项目的所有的核心业务逻辑都是放到了`internal`目录下,该目录是`Golang`的特性,用于对外隐藏可见性。`internal`目录下面的代码内容将无法被外部通过`import`的方式引用,提高项目的安全性和简洁性。 **提示** 框架的核心组件均需要传递 `context` 上下文参数,这里使用 `gctx.GetInitCtx` 表示承接父进程的链路跟踪信息,如果没有父进程那么会创建一个带链路跟踪特性的 `context` 上下文对象给下游链路。 ## 引导启动 `Main` 对象的 `Run` 命令的主要作用是做引导启动,将一些动态初始化的逻辑放到 `Main` 的 `Run` 方法中。在项目模板中,默认创建一个 `HTTP Server`,然后通过分组路由的方式注册路由,并启动 `HTTP Server`。随后 `HTTP Server` 将会阻塞运行,它同时也会异步监听系统信号,直至收到退出信号后,它会优雅关闭连接随后退出进程。 **提示** 框架的命令行管理默认采用了结构化的对象管理方式,详细介绍感兴趣请参考章节:[命令管理](../docs/core/gcmd.md) ## 路由注册 在项目模板中使用了 `Group` 方法创建了分组路由,框架的 `HTTP Server` **支持多种路由注册方式**,而分组路由也是最常见的路由注册方式。 ```go s := g.Server() s.Group("/", func(group *ghttp.RouterGroup) { group.Middleware(ghttp.MiddlewareHandlerResponse) group.Bind( hello.NewV1(), ) }) ``` - 在分组路由的闭包方法内部,通过 `Middleware` 方法注册了一个中间件,该中间件是 `HTTP Server` 组件用于规范化路由的数据返回。 - 随后通过 `Bind` 方法的规范化路由方式绑定一个 `hello.NewV1()` 返回的路由对象,该路由对象下的所有公开方法均会被自动注册到路由。我们的项目脚手架支持接口的版本管理,默认情况下我们的路由对象都是`v1`版本,并且通过`NewV1`的方式创建。 **提示** 详细的HTTP Server路由介绍请参考章节: [路由管理](../docs/web/router.md) ## 路由对象 ### 对象创建 路由对象由`hello.NewV1()`方法返回,其定义如下: 可以看到`NewV1`方法其实返回的是一个接口,而不是具体的对象。该接口的定义如下: 为什么这里不直接返回`ControllerV1`对象,而是返回`hello.IHelloV1`接口呢? 试想一下,假如咱们定义了很多`api`接口,但是具体实现的`controller`只实现了其中一部分,而在运行时才能发现有缺失。那么我们如何能提前发现这个问题呢?对的,就是依赖这个接口实现。假如`controller`只实现了部分接口,那么在编译阶段就会展示错误,并且现代的编辑器中也会提前发现告警。 **提示** - 首先,并不是必需这么做,脚手架模板提供的是比较严谨的代码编写方式。 - 其次,这里的绝大部分代码,是通过`cli`工具的`gf gen ctrl`命令自动解析`api`目录的接口定义自动生成的。 ### 函数定义 对应的路由函数定义如下: `Hello` 方法对应的路由信息是定义到 `HelloReq` 输入参数对象中的,该对象的数据结构定义如下: **提示** 这种通过统一的中间件返回统一的数据结构,统一路由对象的方法管理路由的方式,叫做**规范路由**。 更详细的介绍请参考章节:[路由注册-规范路由](../docs/web/router-registering-strict-router.md)。 ## 运行服务 ### 阻塞运行 通过 `HTTP Server` 的 `Run` 方法启动 `HTTP Server`,随后 `HTTP Server` 将会阻塞运行接收客户端请求,并监听进程信号,用于 `HTTP Server` 重启/关闭。 运行后,终端输出如下: 可以看到,我们启用了`API`接口文档以及`Swagger UI`。 ### 查看效果 我们访问 http://127.0.0.1:8000/hello 查看接口效果: 我们访问 http://127.0.0.1:8000/swagger 查看`Swagger UI`: ## 学习小结 通过本章节的学习,我们了解了脚手架项目模板程序的执行流程。 这里其实有比较多的细节没有详细讲解,感兴趣可以了解下对应栏目下的关联文档资料。 在下一章节,我们将尝试着通过脚手架项目模板编写简单的`CRUD`接口,实现对数据库表的增删查改。 # What's Next ## 简单总结 好的,当已经学习完前面的基础章节后,应该学习到以下的内容: 1. 业务项目搭建 2. 项目启动逻辑 3. 了解框架设计的整体思路 4. 编写一个简单的 `api` 项目 ## 接下来的学习 那么接下来,我们应该怎么进一步学习框架内容呢? ### 社区教程 `GoFrame`社区的老师们为大家分享了一些不错的学习资料,感兴趣的同学可以进一步了解:[社区教程](https://goframe.org/course/社区教程.md) ### 服务组件的进一步学习 大部分的同学学习框架是为了更好地开发自己的业务项目,大部分的业务项目基于两种类型:Web项目或微服务。 #### Web项目学习 如果是 `HTTP WEB` 项目,那么请进入该章节学习: [WEB服务开发](../docs/web.md) 从该章节开始,会循序渐进地介绍WEB服务的开发以及相关组件的使用。 #### 微服务学习 如果是微服务项目,那么请进入该章节学习: [微服务开发](../docs/micro-service.md) 该章节主要是介绍如何使用框架来实现微服务开发。 ### 框架组件的进一步学习 如果在开发和学习的过程中遇到某个组件不太会使用,那么可以先阅读源码,或者查看官网中相应的组件介绍章节。此外,另外一部分同学是将框架作为基础组件库来使用,那么也可以单独去查看对应组件的介绍使用章节。 #### 核心组件 核心组件是构成框架不可或缺的部分,业务项目基本都会用得到,也是框架学习的重点,章节地址: [核心组件(重点)](../docs/core.md) #### 组件列表 组件列表是展示了框架所有组件的汇总章节,章节地址: [组件列表](../docs/components.md) ## 重要的Tips 基于示例来学习框架使用比较事半功倍,框架提供了丰富的示例项目: - `Web API` 开发: [https://github.com/gogf/gf-demo-user](https://github.com/gogf/gf-demo-user) - `Web MVC` 开发: [https://github.com/gogf/gf-demo-chat](https://github.com/gogf/gf-demo-chat) - 微服务开发: [https://github.com/gogf/gf-demo-grpc](https://github.com/gogf/gf-demo-grpc) - 更多示例项目: [https://github.com/gogf/awesome-gf](https://github.com/gogf/awesome-gf) # 接口开发 在本章节,我们将学会`Golang`工程师的基本讨饭技能,开发`CRUD`接口。使用`GoFrame`框架提供的脚手架工具开发`API`接口非常简单,我们一起来吧,`Let's Go!` **提示** 该章节的完整示例源码可以在这里找到:https://github.com/gogf/quick-demo # Step1 - 设计数据表 ## 设计数据表 我们先定义一个数据表,以下是本章节示例会用到的数据表`SQL`文件: ```sql CREATE TABLE `user` ( `id` int(10) unsigned NOT NULL AUTO_INCREMENT COMMENT 'user id', `name` varchar(45) DEFAULT NULL COMMENT 'user name', `status` tinyint DEFAULT NULL COMMENT 'user status', `age` tinyint unsigned DEFAULT NULL COMMENT 'user age', PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8; ``` ## 应用数据表 我们需要把这个数据表应用到`mysql`数据库中,便于后续的使用。如果本地没有`mysql`数据库服务,那么这里使用`docker`运行一个吧: ```bash docker run -d --name mysql \ -p 3306:3306 \ -e MYSQL_DATABASE=test \ -e MYSQL_ROOT_PASSWORD=12345678 \ loads/mysql:5.7 ``` 启动后,连接数据库,将数据表创建`sql`语句应用进去: ```text $ mysql -h 127.0.0.1 -P 3306 -u root -p mysql: [Warning] Using a password on the command line interface can be insecure. Enter password: Welcome to the MySQL monitor. Commands end with ; or \g. Your MySQL connection id is 57 Server version: 9.0.1 Homebrew Copyright (c) 2000, 2024, Oracle and/or its affiliates. Oracle is a registered trademark of Oracle Corporation and/or its affiliates. Other names may be trademarks of their respective owners. Type 'help;' or '\h' for help. Type '\c' to clear the current input statement. mysql> use test; Database changed mysql> CREATE TABLE `user` ( -> `id` int(10) unsigned NOT NULL AUTO_INCREMENT COMMENT 'user id', -> `name` varchar(45) DEFAULT NULL COMMENT 'user name', -> `status` tinyint DEFAULT NULL COMMENT 'user status', -> `age` tinyint unsigned DEFAULT NULL COMMENT 'user age', -> PRIMARY KEY (`id`) -> ) ENGINE=InnoDB DEFAULT CHARSET=utf8; Query OK, 0 rows affected, 2 warnings (0.02 sec) mysql> ``` ## 学习小结 在接口开发之前先设计数据库表是比较好的开发习惯。这里我们使用的是`mysql`数据库,是需要先搭建/运行数据库服务。 在设计完成数据库表后,我们下一步可以使用脚手架工具自动去生成对应的数据库操作相关文件。 # Step2 - 生成/dao/do/entity ## 检查工具配置 在使用脚手架工具之前,请检查本地的`cli`工具配置是否正确。默认的配置如下: ```yaml title="hack/config.yaml" # CLI tool, only in development environment. # https://goframe.org/docs/cli gfcli: gen: dao: - link: "mysql:root:12345678@tcp(127.0.0.1:3306)/test" descriptionTag: true docker: build: "-a amd64 -s linux -p temp -ew" tagPrefixes: - my.image.pub/my-app ``` - 其中的`dao`部分配置即本次将要执行的命令的配置,其中的`link`为需要连接的数据库配置。`descriptionTag`表示为生成的`entity`代码文件增加字段描述到`description`标签中,如果数据表的`entity`对象被用到了接口`api`定义中,该标签则可作为参数描述。我们需要将此`link`配置改为我们的数据库连接地址。关于`link`配置项的详细介绍请参考章节 [ORM使用配置-配置文件](../docs/core/gdb-config-file.md)。 - 其中`docker`配置项是模板默认提供的配置,用于镜像编译。这里不做详解,感兴趣可以参考开发手册中开发工具相关章节。 ## 执行代码生成 将数据表创建好后,我们在项目根目录下执行`make dao`自动生成对应的`dao/do/entity`文件。 ```text $ make dao generated: /Users/john/Temp/demo/internal/dao/user.go generated: /Users/john/Temp/demo/internal/dao/internal/user.go generated: /Users/john/Temp/demo/internal/model/do/user.go generated: /Users/john/Temp/demo/internal/model/entity/user.go done! ``` **提示** `make`命令是在`*nix`系统下默认提供的指令,包括`Linux/MacOS`系统。如果是其他系统,例如`Windows`,默认不支持`make`命令,可以使用`gf gen dao`命令来替换。 每张表将会生成三类`Go`文件: - `dao`:通过对象方式访问底层数据源,底层基于`ORM`组件实现。 - `do`:数据转换模型,用于业务模型到数据模型的转换,由工具维护,用户不能修改。工具每次生成代码文件将会覆盖该目录。 - `entity`:数据模型,由工具维护,用户不能修改。工具每次生成代码文件将会覆盖该目录。 更详细的介绍请参考章节 [数据规范-gen dao](../docs/cli/gen-dao.md) **说明** 由脚手架工具生成的代码中,都会带有顶部文件注释。如果文件注释中带有`Code generated and maintained by GoFrame CLI tool. DO NOT EDIT.`的注释描述,那么表示该文件是由脚手架工具维护,每一次代码生成都会覆盖它。 ### dao 生成的`dao`文件有两个: - `internal/dao/internal/user.go`用于封装对数据表`user`的访问。该文件自动生成了一些数据结构和方法,简化对数据表的`CRUD`操作。该文件每次生成都会覆盖,由开发工具自动维护,开发者无需关心。 - `internal/dao/user.go`其实是对`internal/dao/internal/user.go`的进一步封装,用于供其他模块直接调用访问。该文件开发者可以随意修改,或者扩展`dao`的能力。 由于生成的`internal/dao/internal/user.go`文件完全由开发工具维护,那么我们只需关心`internal/dao/user.go`这个生成的源码文件,后续如果有需要可以在这个文件上做功能扩展。 示例源码:https://github.com/gogf/quick-demo/blob/main/internal/dao/user.go ```go title="internal/dao/user.go" // ================================================================================= // This is auto-generated by GoFrame CLI tool only once. Fill this file as you wish. // ================================================================================= package dao import ( "demo/internal/dao/internal" ) // internalUserDao is internal type for wrapping internal DAO implements. type internalUserDao = *internal.UserDao // userDao is the data access object for table user. // You can define custom methods on it to extend its functionality as you wish. type userDao struct { internalUserDao } var ( // User is globally public accessible object for table user operations. User = userDao{ internal.NewUserDao(), } ) // Fill with you ideas below. ``` ### do 示例源码:https://github.com/gogf/quick-demo/blob/main/internal/model/do/user.go ```go title="internal/model/do/user.go" // ================================================================================= // Code generated and maintained by GoFrame CLI tool. DO NOT EDIT. // ================================================================================= package do import ( "github.com/gogf/gf/v2/frame/g" ) // User is the golang structure of table user for DAO operations like Where/Data. type User struct { g.Meta `orm:"table:user, do:true"` Id interface{} // user id Name interface{} // user name Status interface{} // user status Age interface{} // user age } ``` 关于`do`数据转换模型的使用,我们将在后续代码逻辑中做展示。 ### entity 示例源码:https://github.com/gogf/quick-demo/blob/main/internal/model/entity/user.go ```go title="internal/model/entity/user.go" // ================================================================================= // Code generated and maintained by GoFrame CLI tool. DO NOT EDIT. // ================================================================================= package entity // User is the golang structure for table user. type User struct { Id uint `json:"id" orm:"id" description:"user id"` // user id Name string `json:"name" orm:"name" description:"user name"` // user name Status int `json:"status" orm:"status" description:"user status"` // user status Age uint `json:"age" orm:"age" description:"user age"` // user age } ``` 可以看到该`entity`数据结构定义与数据表字段一一对应。 ## 学习小结 可以感受到,使用`GoFrame`框架便捷的脚手架工具,我们从一些重复性的代码劳动中解放了出来,极大地提高了生产效率。针对数据库的操作将会变得非常简单。 在下一步,我们将设计`CRUD`接口,一起看看在`GoFrame`框架中是如何快速定义接口的吧。 # Step3 - 编写api接口定义 在项目的`api`目录下,我们开始定义我们的`CRUD`接口。 - 接口我们使用`RESTful`风格设计,充分使用`GET/POST/PUT/DELETE`的`HTTP Method`,这样规范设计的接口会非常优雅。 - 同样的,我们默认开始使用`v1`版本。使用版本号做为良好的开发习惯,有利于未来接口的兼容性维护。 ## 创建接口 ```go title="api/user/v1/user.go" type CreateReq struct { g.Meta `path:"/user" method:"post" tags:"User" summary:"Create user"` Name string `v:"required|length:3,10" dc:"user name"` Age uint `v:"required|between:18,200" dc:"user age"` } type CreateRes struct { Id int64 `json:"id" dc:"user id"` } ``` 简要介绍: - 接口定义中,使用`g.Meta`来管理接口的元数据信息,这些元数据信息通过标签的形式定义在`g.Meta`属性上。这里的元数据信息包括:`path`路由地址、`method`请求方式、`tags`接口分组(用于生成接口文档)、`summary`接口描述。这些元数据信息都是`OpenAPIv3`里面的东西,我们这里不做详细介绍,大家了解即可,感兴趣可以参考章节:[接口文档-OpenAPIv3](../docs/web/api-document-openapiv3.md)。 - 这里的`Name`和`Age`属性即是咱们接口的参数定义。其中`dc`标签是`description`的缩写,表示参数的含义;`v`标签是`valid`得缩写,表示参数的校验规则。我们这里使用到了`3`条内置的校验规则: - `required`:该参数是必需参数。 - `length`:参数的长度校验。 - `between`:参数的大小校验。 - 这里仅做了解即可,更多的校验规则请参考章节 [数据校验-校验规则](../docs/core/gvalid-rules.md)。 - 请求的参数结构体`CreateReq`中,我们并没有定义参数的接收方式,因为`GoFrame`框架支持非常强大灵活的参数接收方式,能够自动识别`Query String/Form/Json/Xml`等提交方式,并将提交参数自动映射到请求参数接收对象上。 - 只有返回的参数结构体中带有`json`标签,因为返回的数据往往需要转换为`json`格式给前端使用,通过`snake`的参数命名的方式更符合前端命名习惯。 **提示** 在`RESTful`风格的接口设计中,我们通常使用`HTTP Method`中的`POST`来表示写入操作,而使用`PUT`来表示更新操作。 ## 删除接口 ```go title="api/user/v1/user.go" type DeleteReq struct { g.Meta `path:"/user/{id}" method:"delete" tags:"User" summary:"Delete user"` Id int64 `v:"required" dc:"user id"` } type DeleteRes struct{} ``` 这里的路由标签`path`使用的`/user/{id}`,其中的`{id}`表示一个字段匹配路由,该参数通过`URL Path`的方式传递,参数名称为`id`。可以看到,我们在请求参数对象中正好定义了一个`Id`参数,是的,从路由中匹配到的`id`参数会**不区分参数字母大小写**直接映射到该`Id`上。 举个例子:路由`/user/1`中,`id`参数的值便是`1`;在路由`/user/100`中,`id`参数的值便是`100`。 ## 更新接口 ```go title="api/user/v1/user.go" // Status marks user status. type Status int const ( StatusOK Status = 0 // User is OK. StatusDisabled Status = 1 // User is disabled. ) type UpdateReq struct { g.Meta `path:"/user/{id}" method:"put" tags:"User" summary:"Update user"` Id int64 `v:"required" dc:"user id"` Name *string `v:"length:3,10" dc:"user name"` Age *uint `v:"between:18,200" dc:"user age"` Status *Status `v:"in:0,1" dc:"user status"` } type UpdateRes struct{} ``` 在这里: - 我们这里定义了一个用户状态类型`Status`,采用的是`Golang`里面约定俗成的`enums`定义方式。这里大家了解即可。 - 对`Status`参数的校验使用了`in:0,1`校验规则,该规则将会校验传递的`Status`的值必需是我们定义的常量的两个值`StatusOK/StatusDisabled`,即`0/1`。 - 接口参数我们使用了指针来接收,目的是避免类型默认值对我们修改接口的影响。举个例子,假如`Status`不定义为指针,那么它就会有默认值`0`的影响,那么在处理逻辑中,很难判断到底调用端有没有传递该参数,是否要真正修改数值为`0`。但我们使用指针后,当用户没有传递该参数时,该参数的默认值就是`nil`,处理逻辑便很好做判断。 ## 查询接口(单个) ```go title="api/user/v1/user.go" type GetOneReq struct { g.Meta `path:"/user/{id}" method:"get" tags:"User" summary:"Get one user"` Id int64 `v:"required" dc:"user id"` } type GetOneRes struct { *entity.User `dc:"user"` } ``` 这里的返回结果我们使用了`*entity.User`结构体,该结构是前面我们通过`make dao`命令生成的`entity`,该数据结构与数据表字段一一对应。 ## 查询接口(列表) ```go title="api/user/v1/user.go" type GetListReq struct { g.Meta `path:"/user" method:"get" tags:"User" summary:"Get users"` Age *uint `v:"between:18,200" dc:"user age"` Status *Status `v:"in:0,1" dc:"user status"` } type GetListRes struct { List []*entity.User `json:"list" dc:"user list"` } ``` 该接口可以根据`Age`和`Status`进行查询,返回的是多条记录`List []*entity.User`。 ## 学习小结 本章节的示例源码:https://github.com/gogf/quick-demo/blob/main/api/user/v1/user.go 可以看到,在`GoFrame`框架的脚手架项目定义`api`接口相当优雅,并且支持自动的数据校验、元数据注入、灵活的路由配置等实用特性。这种接口定义方式,可以自动化地生成接口文档,代码即文档,我们也可以保证代码和文档的一致性。 并且,这还并不是`GoFrame`魅力的全部,只是玫瑰上的一片花瓣。下一步,我们将使用脚手架工具,自动化地帮我们生成对应的`controller`控制代码。 # Step4 - 生成controller代码 ## 根据`api`生成代码 当`api`定义完成后,我们通过`make ctrl`命令(或者`gf gen ctrl`)生成控制器代码。 ```text $ make ctrl generated: /Users/john/Temp/demo/api/user/user.go generated: /Users/john/Temp/demo/internal/controller/user/user.go generated: /Users/john/Temp/demo/internal/controller/user/user_new.go generated: /Users/john/Temp/demo/internal/controller/user/user_v1_create.go generated: /Users/john/Temp/demo/internal/controller/user/user_v1_update.go generated: /Users/john/Temp/demo/internal/controller/user/user_v1_delete.go generated: /Users/john/Temp/demo/internal/controller/user/user_v1_get_one.go generated: /Users/john/Temp/demo/internal/controller/user/user_v1_get_list.go done! ``` 生成的代码主要包含`3`类文件。 ## `api`接口抽象文件 定义了`api interface`,用于保证控制器实现的接口完整性,避免`controller`缺失部分接口实现的问题。由于`GoFrame`是一款严谨的开发框架,这种细节控制得比较好,至于该特性开发者使用与否,可以根据自身场景和需要来选择。 ```text /Users/john/Temp/demo/api/user/user.go ``` 该文件由开发工具维护,开发者无需关心。 内容如下: ```go title="api/user/user.go" // ================================================================================= // Code generated and maintained by GoFrame CLI tool. DO NOT EDIT. // ================================================================================= package user import ( "context" "demo/api/user/v1" ) type IUserV1 interface { Create(ctx context.Context, req *v1.CreateReq) (res *v1.CreateRes, err error) Update(ctx context.Context, req *v1.UpdateReq) (res *v1.UpdateRes, err error) Delete(ctx context.Context, req *v1.DeleteReq) (res *v1.DeleteRes, err error) GetOne(ctx context.Context, req *v1.GetOneReq) (res *v1.GetOneRes, err error) GetList(ctx context.Context, req *v1.GetListReq) (res *v1.GetListRes, err error) } ``` ## `controller`路由对象管理 用于管理控制器的初始化,以及一些控制内部使用的数据结构、常量定义。 ```text generated: /Users/john/Temp/demo/internal/controller/user/user.go generated: /Users/john/Temp/demo/internal/controller/user/user_new.go ``` 其中`internal/controller/user/user.go`是一个空的源码文件,可用于定义一些控制器内部使用的数据结构、常量等内容。 ```go title="internal/controller/user/user.go" // ================================================================================= // This is auto-generated by GoFrame CLI tool only once. Fill this file as you wish. // ================================================================================= package user ``` 另一个`internal/controller/user/user_new.go`文件是自动生成的路由对象创建文件。 ```go title="internal/controller/user/user_new.go" // ================================================================================= // This is auto-generated by GoFrame CLI tool only once. Fill this file as you wish. // ================================================================================= package user import ( "demo/api/user" ) type ControllerV1 struct{} func NewV1() user.IUserV1 { return &ControllerV1{} } ``` 这两个文件都只会生成一次,随后开发者可以随意修改、扩展。 **提示** 如果后续我们需要定义`v2`接口,`make ctrl`命令会类似生成`type ControllerV2 struct{}`结构体定义,以及`func NewV2() user.IUserV2`初始化方法。 ## `controller`路由实现代码 用于具体的`api`接口实现的代码文件。默认情况下,会按照一个`api`接口一个源码文件的形式生成代码。当然,也可以控制按照`api`文件定义的接口聚合生成到对应的一个源码文件中。具体的命令介绍及配置请参考章节 [接口规范-gen ctrl](../docs/cli/gen-ctrl.md)。 ```text generated: /Users/john/Temp/demo/internal/controller/user/user_v1_create.go generated: /Users/john/Temp/demo/internal/controller/user/user_v1_update.go generated: /Users/john/Temp/demo/internal/controller/user/user_v1_delete.go generated: /Users/john/Temp/demo/internal/controller/user/user_v1_get_one.go generated: /Users/john/Temp/demo/internal/controller/user/user_v1_get_list.go ``` 我们打开一个文件查看生成的代码模板: ```go title="internal/controller/user/user_v1_create.go" package user import ( "context" "github.com/gogf/gf/v2/errors/gcode" "github.com/gogf/gf/v2/errors/gerror" "demo/api/user/v1" ) func (c *ControllerV1) Create(ctx context.Context, req *v1.CreateReq) (res *v1.CreateRes, err error) { return nil, gerror.NewCode(gcode.CodeNotImplemented) } ``` 可以看到,这只是我们定义的创建接口的实现模板,我们完善这个路由函数的具体业务逻辑即可。 ## 学习小结 本章节的示例源码:https://github.com/gogf/quick-demo/tree/main/internal/controller/user `GoFrame`脚手架工具,帮助我们将一切与业务逻辑无关的代码都生成好了,我们只需要关注业务逻辑实现即可。并且,这些自动生成的代码文件,除了开发者可以扩展的个别代码文件,大部分代码文件都完全由工具自动维护,我们也无需关心其未来的维护。 是的,`GoFrame`脚手架工具的目标,就是让开发者可以将精力聚焦于业务逻辑本身,而业务逻辑以外的工作,都交给开发框架和脚手架工具来完成。 这样的开发方式简直太方便了,不要太舒爽。以为这就是全部了吗?当然不是,在快速开始章节我们只会介绍部分入门级功能。当深入地接触她,会发现她更多的好、她的善解人意。 下一步,我们将完成接口的业务逻辑实现,感受下`GoFrame`数据库`ORM`组件的魅力。 # Step5 - 完成接口逻辑实现 可以看到,通过项目脚手架工具,很多与项目业务逻辑无关的代码都已经预先生成好,我们只需要关注业务逻辑实现即可。我们接下来看看如何实现`CRUD`具体逻辑吧。 ## 创建接口 ### 创建逻辑实现 ```go title="internal/controller/user/user_v1_create.go" package user import ( "context" "demo/api/user/v1" "demo/internal/dao" "demo/internal/model/do" ) func (c *ControllerV1) Create(ctx context.Context, req *v1.CreateReq) (res *v1.CreateRes, err error) { insertId, err := dao.User.Ctx(ctx).Data(do.User{ Name: req.Name, Status: v1.StatusOK, Age: req.Age, }).InsertAndGetId() if err != nil { return nil, err } res = &v1.CreateRes{ Id: insertId, } return } ``` 在`Create`实现方法中: - 我们通过`dao.User`通过`dao`组件操作`user`表。 - 每个`dao`操作都需要传递`ctx`参数,因此我们通过`Ctx(ctx)`方法创建一个`gdb.Model`对象,该对象是框架的模型对象,用于操作特定的数据表。 - 通过`Data`传递需要写入数据表的数据,我们这里使用`do`转换模型对象输入我们的数据。`do`转换模型会自动过滤`nil`数据,并在底层自动转换为对应的数据表字段类型。在绝大部分时候,我们都使用`do`转换模型来给数据库操作对象传递写入/更新参数、查询条件等数据。 - 通过`InsertAndGetId`方法将`Data`的参数写入数据库,并返回新创建的记录主键`id`。 ### 参数校验实现 等等,大家可能会问,为什么这里没有校验逻辑呢?因为校验逻辑都已经配置到请求参数对象`CreateReq`上了。还记得前面介绍的`v`标签吗?我们再来看看这个请求参数对象: ```go title="api/user/v1/user.go" type CreateReq struct { g.Meta `path:"/user" method:"put" tags:"User" summary:"Create user"` Name string `v:"required|length:3,10" dc:"user name"` Age uint `v:"required|between:18,200" dc:"user age"` } type CreateRes struct { Id int64 `json:"id" dc:"user id"` } ``` 这里的`required/length/between`校验规则在调用路由函数`Create`之前就已经由`GoFrame`框架的`Server`自动执行了。 如果请求参数校验失败,会立即返回错误,不会进入到路由函数。`GoFrame`框架的这种机制极大地简便了开发流程, 开发者在这个路由函数中,仅需要关注业务逻辑实现即可。 **说明** 当然,如果有一些额外的、定制化的业务逻辑校验,是需要在路由函数中自行实现的哟。 ## 删除接口 ```go title="internal/controller/user/user_v1_delete.go" package user import ( "context" "demo/api/user/v1" "demo/internal/dao" ) func (c *ControllerV1) Delete(ctx context.Context, req *v1.DeleteReq) (res *v1.DeleteRes, err error) { _, err = dao.User.Ctx(ctx).WherePri(req.Id).Delete() return } ``` 删除逻辑比较简单,我们这里用到一个`WherePri`方法,该方法会将给定的参数`req.Id`作为主键进行`Where`条件限制。 ## 更新接口 ```go title="internal/controller/user/user_v1_update.go" package user import ( "context" "demo/api/user/v1" "demo/internal/dao" "demo/internal/model/do" ) func (c *ControllerV1) Update(ctx context.Context, req *v1.UpdateReq) (res *v1.UpdateRes, err error) { _, err = dao.User.Ctx(ctx).Data(do.User{ Name: req.Name, Status: req.Status, Age: req.Age, }).WherePri(req.Id).Update() return } ``` 更新接口也比较简单,除了已经介绍过的`WherePri`方法,在更新数据时也需要通过`Data`方法传递更新的数据。 ## 查询接口(单个) ```go title="internal/controller/user/user_v1_get_one.go" package user import ( "context" "demo/api/user/v1" "demo/internal/dao" ) func (c *ControllerV1) GetOne(ctx context.Context, req *v1.GetOneReq) (res *v1.GetOneRes, err error) { res = &v1.GetOneRes{} err = dao.User.Ctx(ctx).WherePri(req.Id).Scan(&res.User) return } ``` 数据查询接口中,我们使用了`Scan`方法,该方法可以将查询到的单条数据表记录智能地映射到结构体对象上。大家需要注意这里的`&res.User`中的`User`属性对象其实是没有初始化的,其值为`nil`。如果查询到了数据,`Scan`方法会对其做初始化并赋值,如果查询不到数据,那么`Scan`方法什么都不会做,其值还是`nil`。 ## 查询接口(列表) ```go title="internal/controller/user/user_v1_get_list.go" package user import ( "context" "demo/api/user/v1" "demo/internal/dao" "demo/internal/model/do" ) func (c *ControllerV1) GetList(ctx context.Context, req *v1.GetListReq) (res *v1.GetListRes, err error) { res = &v1.GetListRes{} err = dao.User.Ctx(ctx).Where(do.User{ Age: req.Age, Status: req.Status, }).Scan(&res.List) return } ``` 查询列表数据我们同样使用到了`Scan`方法,这个方法是非常强大的。同查询单条数据的逻辑一样,它仅会在查询的数据时才会初始化这里的`&res.List`。 ## 学习小结 本章节的示例源码:https://github.com/gogf/quick-demo/tree/main/internal/controller/user 可以看到,使用`GoFrame`数据库`ORM`组件可以非常快速、高效地完成接口开发工作。整个`CRUD`接口开发下来,开发者需要实现的业务逻辑仅需要几行代码。 开发效率的提升,除了归功于脚手架工具自动生成的`dao`和`controller`代码之外,强大的数据库`ORM`组件也是功不可没。可以看到,我们在对数据库表进行操作时,代码量非常简洁优雅,但在数据库`ORM`组件的内部设计中,涉及很多精细的设计、严格的代码测试、年复一年的功能迭代的沉淀结果。 接口逻辑开发完了,在下一步,我们需要做一些数据库配置和路由注册的操作,同样也是非常简便,一起看看吧。 # Step6 - 配置与路由 ## 引入数据库驱动 `GoFrame`的数据库组件使用了接口化设计,接口与实现是分离的,这样能提供更好的抽象和扩展性。我们这里使用了`mysql`数据库,那么需要引入具体的`mysql`驱动实现。我们在`main.go`中加上`_ "github.com/gogf/gf/contrib/drivers/mysql/v2"`即可。 示例源码:https://github.com/gogf/quick-demo/blob/main/main.go ```go title="main.go" package main import ( _ "demo/internal/packed" _ "github.com/gogf/gf/contrib/drivers/mysql/v2" "github.com/gogf/gf/v2/os/gctx" "demo/internal/cmd" ) func main() { cmd.Main.Run(gctx.GetInitCtx()) } ``` 数据库的驱动,项目只需要引入这一次即可,后续都不需要做调整。 更多关于数据库驱动的支持以及详细介绍,请参考章节 [数据库ORM](../docs/core/gdb.md)。 如果在没有引入数据库驱动的情况下执行数据库操作,数据库ORM组件会报如下的错误提示: ```text cannot find database driver for specified database type "mysql", did you misspell type name "mysql" or forget importing the database driver? possible reference: https://github.com/gogf/gf/tree/master/contrib/drivers ``` **提示** 在脚手架项目模板`main.go`的`import`中有一段`_ "demo/internal/packed"`,表示`GoFrame`框架的资源管理,这是一个高级特性。该特性可以将任何资源打包进二进制文件,这样我们在发布的时候,仅需要发布一个二进制文件即可。我们这里没有用到该特性,因此大家了解即可,感兴趣可以后续查阅开发手册相关章节。 ## 添加数据库配置 在脚手架项目模板中主要有两个配置文件。 ### 工具配置 `hack/config.yaml` 在前面的章节我们已经有过介绍。这个配置文件主要是本地开发时候使用,当`cli`脚手架工具执行时会自动读取其中的配置内容。 示例源码:https://github.com/gogf/quick-demo/blob/main/hack/config.yaml ### 业务配置 `manifest/config/config.yaml` 主要维护业务项目的组件配置信息、业务模块配置,完全由开发者自行维护。在程序启动时会读取该配置文件。该业务 默认的脚手架项目模板提供的业务配置如下: ```yaml title="manifest/config/config.yaml" # https://goframe.org/docs/web/server-config-file-template server: address: ":8000" openapiPath: "/api.json" swaggerPath: "/swagger" # https://goframe.org/docs/core/glog-config logger: level : "all" stdout: true # https://goframe.org/docs/core/gdb-config-file database: default: link: "mysql:root:12345678@tcp(127.0.0.1:3306)/test" ``` 默认提供了`3`项组件的配置,分别为: - `server`:`Web Server`的配置。这里默认配置的监听地址为`:8000`,并启用了接口文档特性。 - `logger`:默认日志组件的配置。这里的日志级别是所有日志都会打印,并且都会输出到标准输出。 - `database`:数据库组件的配置。这只是一个模板,需要我们根据实际情况修改链接地址。 每一项组件配置的注释上提供了官网文档的配置参考链接。我们这里需要修改数据库配置中的链接信息,为我们真实可用的链接信息。关于数据库的配置详细介绍,感兴趣请参考章节:[ORM使用配置-配置文件](../docs/core/gdb-config-file.md) 示例源码:https://github.com/gogf/quick-demo/blob/main/manifest/config/config.yaml ## 添加路由注册 添加我们新填写的`api`到路由非常简单,如下: 在分组路由的`group.Bind`方法中,通过`user.NewV1()`添加我们的路由对象即可。 示例源码:https://github.com/gogf/quick-demo/blob/main/internal/cmd/cmd.go 到目前为止,我们的接口已经完全开发完了,下一步,我们将启动服务,并做一些接口测试,查看效果。 ## 学习小结 当我们在使用数据库功能的时候,需要引入特定的数据库驱动。在`GoFrame`官方仓库中,通过社区组件的形式提供了常用数据库的驱动实现。我们的程序主要使用的是业务配置,并且需要将其中的数据库连接地址修改为我们搭建的数据库地址。 路由注册就太简单了,添加一个`controller`对象到分组路由注册中`group.Bind`即可。 目前为止,我们已经将`CRUD`接口开发完成啦。可以看到,我们做的事情主要是这几项: - 数据库表设计 - `api`接口定义 - 接口的业务逻辑实现 - 简单的配置和路由注册 下一步,我们启动程序看看效果吧。 # Step7 - 启动与测试 ## 启动服务 我们可以使用命令行或者`IDE`自带的启动工具来启动服务,为了简化示例,我们这里直接在命令行中使用`go run main.go`来启动我们的服务: ```text $ go run main.go 2024-11-16 16:40:07.394 [INFO] pid[39511]: http server started listening on [:8000] 2024-11-16 16:40:07.394 [INFO] {18594fad2e66081870e88c6e1440060b} swagger ui is serving at address: http://127.0.0.1:8000/swagger/ 2024-11-16 16:40:07.394 [INFO] {18594fad2e66081870e88c6e1440060b} openapi specification is serving at address: http://127.0.0.1:8000/api.json ADDRESS | METHOD | ROUTE | HANDLER | MIDDLEWARE ----------|--------|------------|-------------------------------------------------------|---------------------------------- :8000 | ALL | /api.json | github.com/gogf/gf/v2/net/ghttp.(*Server).openapiSpec | ----------|--------|------------|-------------------------------------------------------|---------------------------------- :8000 | GET | /hello | demo/internal/controller/hello.(*ControllerV1).Hello | ghttp.MiddlewareHandlerResponse ----------|--------|------------|-------------------------------------------------------|---------------------------------- :8000 | ALL | /swagger/* | github.com/gogf/gf/v2/net/ghttp.(*Server).swaggerUI | HOOK_BEFORE_SERVE ----------|--------|------------|-------------------------------------------------------|---------------------------------- :8000 | GET | /user | demo/internal/controller/user.(*ControllerV1).GetList | ghttp.MiddlewareHandlerResponse ----------|--------|------------|-------------------------------------------------------|---------------------------------- :8000 | POST | /user | demo/internal/controller/user.(*ControllerV1).Create | ghttp.MiddlewareHandlerResponse ----------|--------|------------|-------------------------------------------------------|---------------------------------- :8000 | DELETE | /user/{id} | demo/internal/controller/user.(*ControllerV1).Delete | ghttp.MiddlewareHandlerResponse ----------|--------|------------|-------------------------------------------------------|---------------------------------- :8000 | GET | /user/{id} | demo/internal/controller/user.(*ControllerV1).GetOne | ghttp.MiddlewareHandlerResponse ----------|--------|------------|-------------------------------------------------------|---------------------------------- :8000 | PUT | /user/{id} | demo/internal/controller/user.(*ControllerV1).Update | ghttp.MiddlewareHandlerResponse ----------|--------|------------|-------------------------------------------------------|---------------------------------- ``` 可以看到,我们开发的`CRUD`接口已经成功注册到`Web Server`上,并正确显示到了终端。同时,我们启用了接口文档特性,我们看看自动生成的接口文档。 ## 接口文档 根据终端打印的地址,我们访问:http://127.0.0.1:8000/swagger/ **提示** 自动化的接口文档生成也是`GoFrame`框架提供的非常强大的特性之一,我们这里不做详细的介绍,感兴趣的朋友可以参考章节:[接口文档](../docs/web/api-document.md) ## 接口测试 为了简化测试操作,我们使用`curl`命令来做测试,并使用`json`格式提交和接受返回参数。 **提示** 以下执行命令中前面的`$`符号,表示终端命令行工具的提示符号,并不是我们输入命令的一部分,不同的终端命令行提示符号不同。 ### 创建接口 创建请求需要使用`POST`方式提交。 ```bash $ curl -X POST 'http://127.0.0.1:8000/user' -d '{"name":"john","age":20}' {"code":0,"message":"","data":{"id":1}} $ curl -X POST 'http://127.0.0.1:8000/user' -d '{"name":"alice","age":18}' {"code":0,"message":"","data":{"id":2}} ``` 这里返回的`code`为`0`表示执行成功。 我们构造不符合校验规则的请求,看看效果: ```bash $ curl -X POST 'http://127.0.0.1:8000/user' -d '{"name":"smith","age":16}' {"code":51,"message":"The Age value `16` must be between 18 and 200","data":null} $ curl -X POST 'http://127.0.0.1:8000/user' -d '{"name":"sm","age":18}' {"code":51,"message":"The Name value `sm` length must be between 3 and 10","data":null} ``` 可以看到,校验规则起了作用,返回了具体的校验错误信息,错误码`code`为`51`,这个是框架内置的错误码,表示校验错误,开发者也可以自定义错误码。更多的错误码介绍请查看开发手册相关章节。 ### 查询接口 #### 查询单条数据 ```bash $ curl -X GET 'http://127.0.0.1:8000/user/1' {"code":0,"message":"","data":{"id":1,"name":"john","status":0,"age":20}} $ curl -X GET 'http://127.0.0.1:8000/user/2' {"code":0,"message":"","data":{"id":2,"name":"alice","status":0,"age":18}} ``` #### 查询数据列表 ```bash $ curl -X GET 'http://127.0.0.1:8000/user' {"code":0,"message":"","data":{"list":[{"id":1,"name":"john","status":0,"age":20},{"id":2,"name":"alice","status":0,"age":18}]}} $ curl -X GET 'http://127.0.0.1:8000/user?age=18' {"code":0,"message":"","data":{"list":[{"id":2,"name":"alice","status":0,"age":18}]}} ``` ### 修改接口 创建请求需要使用`PUT`方式提交。 ```bash $ curl -X PUT 'http://127.0.0.1:8000/user/1' -d '{"age":26}' {"code":0,"message":"","data":null} ``` 执行成功后,我们再查询对应的数据,看看是否已经修改成功: ```bash $ curl -X GET 'http://127.0.0.1:8000/user/1' {"code":0,"message":"","data":{"id":1,"name":"john","status":0,"age":26}} ``` ### 删除接口 创建请求需要使用`DELETE`方式提交。 ```bash $ curl -X DELETE 'http://127.0.0.1:8000/user/1' {"code":0,"message":"","data":null} ``` 执行成功后,我们再查询所有的数据列表,看看是否已经被删除: ```bash $ curl -X GET 'http://127.0.0.1:8000/user' {"code":0,"message":"","data":{"list":[{"id":2,"name":"alice","status":0,"age":18}]}} ``` 可以看到,数据已经被成功删除了。 ## 学习小结 可以看到,通过`GoFrame`框架的脚手架工具生成的项目模板,我们可以很高效、快速地完成接口开发工作,并且能自动生成接口文档,方便前后端协作。 到此为止,一个使用`GoFrame`框架的`CRUD`接口项目便快速完成了。但`GoFrame`框架的优秀之处,还远不止于此,她的更多特性,等待进一步探索。 # 对象管理 `GoFrame` 框架封装了一些常用的数据类型以及对象获取方法,通过 `g.*` 方法获取。 **提示** `g` 是一个强耦合的模块,目的是为开发者在对频繁使用的类型/对象调用时提供便利。 **使用方式**: ```go import "github.com/gogf/gf/v2/frame/g" ``` ## 数据类型 常用数据类型别名。 ```go type ( Var = gvar.Var // Var is a universal variable interface, like generics. Ctx = context.Context // Ctx is alias of frequently-used context.Context. ) type ( Map = map[string]interface{} // Map is alias of frequently-used map type map[string]interface{}. MapAnyAny = map[interface{}]interface{} // MapAnyAny is alias of frequently-used map type map[interface{}]interface{}. MapAnyStr = map[interface{}]string // MapAnyStr is alias of frequently-used map type map[interface{}]string. MapAnyInt = map[interface{}]int // MapAnyInt is alias of frequently-used map type map[interface{}]int. MapStrAny = map[string]interface{} // MapStrAny is alias of frequently-used map type map[string]interface{}. MapStrStr = map[string]string // MapStrStr is alias of frequently-used map type map[string]string. MapStrInt = map[string]int // MapStrInt is alias of frequently-used map type map[string]int. MapIntAny = map[int]interface{} // MapIntAny is alias of frequently-used map type map[int]interface{}. MapIntStr = map[int]string // MapIntStr is alias of frequently-used map type map[int]string. MapIntInt = map[int]int // MapIntInt is alias of frequently-used map type map[int]int. MapAnyBool = map[interface{}]bool // MapAnyBool is alias of frequently-used map type map[interface{}]bool. MapStrBool = map[string]bool // MapStrBool is alias of frequently-used map type map[string]bool. MapIntBool = map[int]bool // MapIntBool is alias of frequently-used map type map[int]bool. ) type ( List = []Map // List is alias of frequently-used slice type []Map. ListAnyAny = []MapAnyAny // ListAnyAny is alias of frequently-used slice type []MapAnyAny. ListAnyStr = []MapAnyStr // ListAnyStr is alias of frequently-used slice type []MapAnyStr. ListAnyInt = []MapAnyInt // ListAnyInt is alias of frequently-used slice type []MapAnyInt. ListStrAny = []MapStrAny // ListStrAny is alias of frequently-used slice type []MapStrAny. ListStrStr = []MapStrStr // ListStrStr is alias of frequently-used slice type []MapStrStr. ListStrInt = []MapStrInt // ListStrInt is alias of frequently-used slice type []MapStrInt. ListIntAny = []MapIntAny // ListIntAny is alias of frequently-used slice type []MapIntAny. ListIntStr = []MapIntStr // ListIntStr is alias of frequently-used slice type []MapIntStr. ListIntInt = []MapIntInt // ListIntInt is alias of frequently-used slice type []MapIntInt. ListAnyBool = []MapAnyBool // ListAnyBool is alias of frequently-used slice type []MapAnyBool. ListStrBool = []MapStrBool // ListStrBool is alias of frequently-used slice type []MapStrBool. ListIntBool = []MapIntBool // ListIntBool is alias of frequently-used slice type []MapIntBool. ) type ( Slice = []interface{} // Slice is alias of frequently-used slice type []interface{}. SliceAny = []interface{} // SliceAny is alias of frequently-used slice type []interface{}. SliceStr = []string // SliceStr is alias of frequently-used slice type []string. SliceInt = []int // SliceInt is alias of frequently-used slice type []int. ) type ( Array = []interface{} // Array is alias of frequently-used slice type []interface{}. ArrayAny = []interface{} // ArrayAny is alias of frequently-used slice type []interface{}. ArrayStr = []string // ArrayStr is alias of frequently-used slice type []string. ArrayInt = []int // ArrayInt is alias of frequently-used slice type []int. ) ``` ## 常用对象 常用对象往往通过 `单例模式` 进行管理,可以根据不同的单例名称获取对应的对象实例,并在对象初始化时会自动检索获取配置文件中的对应配置项,具体配置项请查看对应对象的章节介绍。 **说明** 注意事项:在运行时阶段,每一次通过 `g` 模块获取单例对象时都会有内部全局锁机制来保证操作和数据的并发安全性,原理性上来讲在并发量大的场景下会存在锁竞争的情况,但绝大部分的业务场景下开发者均不需要太在意锁竞争带来的性能损耗。此外,开发者也可以通过将获取到的单例对象保存到特定的模块下的内部变量重复使用,以此避免运行时锁竞争情况。 ### `HTTP` 客户端对象 ```go func Client() *ghttp.Client ``` 创建一个新的 `HTTP` 客户端对象。 ### `Validator` 校验对象 ```go func Validator() *gvalid.Validator ``` 创建一个新的数据校验对象。 ### (单例) 配置管理对象 ```go func Cfg(name ...string) *gcfg.Config ``` 该单例对象将会自动按照文件后缀 `toml/yaml/yml/json/ini/xml/properties` 文自动检索配置文件。默认情况下会自动检索以下配置文件: - `config` - `config.toml` - `config.yaml` - `config.yml` - `config.json` - `config.ini` - `config.xml` - `config.properties` 并缓存,配置文件在外部被修改时将会自动刷新缓存。 为方便多文件场景下的配置文件调用,简便使用并提高开发效率,单例对象在创建时将会自动使用单例名称进行文件检索。例如: `g.Cfg("redis")` 获取到的单例对象将默认会自动检索以下文件: - `redis` - `redis.toml` - `redis.yaml` - `redis.yml` - `redis.json` - `redis.ini` - `redis.xml` - `redis.properties` 如果检索成功那么将该文件加载到内存缓存中,下一次将会直接从内存中读取;当该文件不存在时,则使用默认的配置文件( `config.toml`)。 ### (单例) 日志管理对象 ```go func Log(name ...string) *glog.Logger ``` 该单例对象将会自动读取默认配置文件中的 `logger` 配置项,并只会初始化一次日志对象。 ### (单例) 模板引擎对象 ```go func View(name ...string) *gview.View ``` 该单例对象将会自动读取默认配置文件中的 `viewer` 配置项,并只会初始化一次模板引擎对象。内部采用了 `懒初始化` 设计,获取模板引擎对象时只是创建了一个轻量的模板管理对象,只有当解析模板文件时才会真正初始化。 ### (单例) `WEB Server` ```go func Server(name ...interface{}) *ghttp.Server ``` 该单例对象将会自动读取默认配置文件中的 `server` 配置项,并只会初始化一次 `Server` 对象。 ### (单例) `TCP Server` ```go func TcpServer(name ...interface{}) *gtcp.Server ``` ### (单例) `UDP Server` ```go func UdpServer(name ...interface{}) *gudp.Server ``` ### (单例) 数据库 `ORM` 对象 ```go func DB(name ...string) *gdb.Db ``` 该单例对象将会自动读取默认配置文件中的 `database` 配置项,并只会初始化一次 `DB` 对象。 此外,可以通过以下方法在默认数据库上创建一个 `Model` 对象: ```go func Model(tables string, db ...string) *gdb.Model ``` ### (单例) `Redis` 客户端对象 ```go func Redis(name ...string) *gredis.Redis ``` 该单例对象将会自动读取默认配置文件中的 `redis` 配置项,并只会初始化一次 `Redis` 对象。 ### (单例) 资源管理对象 ```go func Res(name ...string) *gres.Resource ``` ### (单例) 国际化管理对象 ```go func I18n(name ...string) *gi18n.Manager ``` # 调试模式 ## 基本介绍 `GoFrame` 框架的各个组件在一些关键的功能节点会打印出一些调试信息,原本仅供框架内部开发者在开发阶段使用。由于功能强大,从 `GoFrame` 框架 `v1.10.0` 版本开始,全面开放给所有的框架使用者。 框架调试模式下打印的调试信息将会以 `[INTE]` 级别的日志前缀输出到终端标准输出,并且会打印出所在源文件的名称以及代码行号,例如: ```html 2021-04-14 15:24:52.954 [INTE] gdb_driver_mysql.go:49 Open: root:12345678@tcp(127.0.0.1:3306)/test 2021-04-14 15:24:52.954 [INTE] gdb.go:492 open new connection success, master:false, config:&gdb.ConfigNode{Host:"", Port:"", User:"", Pass:"", Name:"", Type:"mysql", Role:"", Debug:false, Prefix:"", DryRun:false, Weight:0, Charset:"", LinkInfo:"root:12345678@tcp(127.0.0.1:3306)/test", MaxIdleConnCount:0, MaxOpenConnCount:0, MaxConnLifeTime:0, QueryTimeout:0, ExecTimeout:0, TranTimeout:0, PrepareTimeout:0, CreatedAt:"", UpdatedAt:"", DeletedAt:"", TimeMaintainDisabled:false}, node:&gdb.ConfigNode{Host:"", Port:"", User:"", Pass:"", Name:"", Type:"mysql", Role:"", Debug:false, Prefix:"", DryRun:false, Weight:0, Charset:"utf8", LinkInfo:"root:12345678@tcp(127.0.0.1:3306)/test", MaxIdleConnCount:0, MaxOpenConnCount:0, MaxConnLifeTime:0, QueryTimeout:0, ExecTimeout:0, TranTimeout:0, PrepareTimeout:0, CreatedAt:"", UpdatedAt:"", DeletedAt:"", TimeMaintainDisabled:false} ``` ## 特性开启 这些调试信息默认情况下是关闭的,不会影响框架性能,框架的开发者和使用者可以通过以下方式打开: 1. 命令行启动参数 - `gf.debug=true`。 2. 指定的环境变量 - `GF_DEBUG=true`。 3. 在 GoFrame `v1.14.0` 版本以后,在程序启动 `boot` 包中使用 `g.SetDebug` 方法手动打开或关闭。该方法是非并发安全的,意味着不能在运行时异步多协程去调用该方法动态设置调试模式。 **提示** 可以发现 `GoFrame` 框架的许多功能模块特性也是按照 **命令行启动参数+环境变量** 的形式按照一定规则进行配置。 **说明** 需要注意的是,框架的各个模块关键调试信息仅会输出到 **终端标准输出**,不支持输出到日志文件中。 ## 使用示例 ### 通过环境变量启用调试模式 我们以 Goland IDE 为例,在运行模板中添加 `GF_DEBUG` 环境变量即可。 ### 通过命令行参数启用调试模式 启动程序的时候带上 `--gf.debug=true` 即可,例如: ```bash $ ./app --gf.debug=true ``` ```bash $ ./app --gf.debug true ``` 或者 ```bash $ ./app --gf.debug=1 ``` ```bash $ ./app --gf.debug 1 ``` # 命令管理 ## 基本介绍 程序需要通过命令行来管理程序启动入口,因此命令行管理组件也是框架的核心组件之一。GoFrame 框架提供了强大的命令行管理模块,由 `gcmd` 组件实现。 **使用方式**: ```go import "github.com/gogf/gf/v2/os/gcmd" ``` **接口文档**: [https://pkg.go.dev/github.com/gogf/gf/v2/os/gcmd](https://pkg.go.dev/github.com/gogf/gf/v2/os/gcmd) ## 组件特性 `gcmd` 组件具有以下显著特性: - 使用简便、功能强大 - 命令行参数管理灵活 - 支持灵活的 `Parser` 命令行自定义解析 - 支持多层级的命令行管理、更丰富的命令行信息 - 支持对象模式结构化输入/输出管理大批量命令行 - 支持参数结构化自动类型转换、自动校验 - 支持参数结构化从配置组件读取数据 - 支持自动生成命令行帮助信息 - 支持终端录入功能 ## 与 `Cobra` 比较 `Cobra` 是 Golang 中使用比较广泛的命令行管理库,开源项目地址:[https://github.com/spf13/cobra](https://github.com/spf13/cobra) GoFrame 框架的 `gcmd` 命令行组件与 `Cobra` 比较,基础功能比较相似,但差别比较大的在于参数管理方式以及可观测性支持方面: - `gcmd` 组件支持结构化的参数管理,支持层级对象的命令行管理、方法自动生成命令,无需开发者手动定义手动解析参数变量。 - `gcmd` 组件支持自动化的参数类型转换,支持基础类型以及复杂类型。 - `gcmd` 组件支持可配置的常用参数校验能力,提高参数维护效率。 - `gcmd` 组件支持没有终端传参时通过配置组件读取参数方式。 - `gcmd` 组件支持链路跟踪,便于父子进程的链路信息传递。 # 命令管理-基本概念 ## 基本概念 ### 参数(`Argument`) 程序命令行按照顺序进行传递、没有名称标识的数据叫做参数(`Argument`),参数的输入具有顺序性。 ### 选项(`Option`) 控制程序逻辑的附加输入、并且具有名称标识的数据叫做选项(`Options`),选项名称以`-`或者`--`字符串作为前缀,选项是无序的,可以放置于命令行中任意位置。选项可以带有数据,也可以不带数据。在其他类似的第三方功能组件里面,选项的功能类似于**标识**(`Flag`)。 此外,根据传统的命令行管理习惯,选项可以设置缩写别名(`Short`),用于简化命令行参数输入。缩写别名往往设定为一个单字母。 ## 选项位置与 `=` 号 `gcmd` 组件支持选项的位置在命令行中是任意的,也就是说,以下命令行选项输入其实意义是一样的: ```bash gf build main.go -a amd64 -o linux -n app -yes gf -a amd64 -o linux build main.go -yes -n app gf -yes -n app build -o linux -a amd64 main.go ``` 其中, - `gf`/`build`/`main.go` 是参数,索引分别为 0、1、2;因为参数是有序性的,因此无论命令行怎么修改,这三者的顺序却无法改变 - `a`/`o`/`n` 是带有数据的选项,由于是顺序无关的,通过选项名称获取数据,因此可以随意放置位置 - `yes` 是不带数据的选项,也可以随意放置位置 命令行的选项与数据之间可以通过空格,也可以通过 `=` 符号进行连接,如: ```bash gf build main.go -a=amd64 -o=linux -n=app -yes ``` ## 默认解析规则 由于 `gcmd` 模块提供了一些包方法用以获取默认的命令行解析规则。在默认规则下,将会自动识别参数与选项。 ### 命令行中带有 `=` 符号的场景下 ```bash gf build main.go -a=amd64 -o=linux -n=app -yes ``` 在默认规则下, - `gf`/ `build`/ `main.go` 是参数,索引分别为 `0`, `1`, `2。` - `a`/ `o`/ `n`/ `yes` 将会被解析为选项,并且 `yes` 为无数据选项。 ### 不使用 `=` 符号来连接选项参数 ```bash gf build main.go -a amd64 -o linux -n app -yes ``` 在默认规则下, - `gf`/ `build`/ `main.go` 是参数,索引分别为 `0`, `1`, `2。` - `a`/ `o`/ `n`/ `yes` 将会被解析为选项,并且 `yes` 为无数据选项。 # 命令管理-基础方法 `gcmd` 组件提供了常用的基础包方法,可以按照默认的解析规则,直接获取命令行参数及选项。 ## 常用方法 更多组件方法请参考接口文档: [https://pkg.go.dev/github.com/gogf/gf/v2/os/gcmd](https://pkg.go.dev/github.com/gogf/gf/v2/os/gcmd) ```go func Init(args ...string) func GetArg(index int, def ...string) *gvar.Var func GetArgAll() []string func GetOpt(name string, def ...string) *gvar.Var func GetOptAll() map[string]string ``` ## Init 自定义命令行 默认情况下,`gcmd` 组件会自动从 `os.Args` 解析获取参数及数据。我们可以通过 `Init` 方法自定义命令行数据。使用示例: ```go func ExampleInit() { gcmd.Init("gf", "build", "main.go", "-o=gf.exe", "-y") fmt.Printf(`%#v`, gcmd.GetArgAll()) // Output: // []string{"gf", "build", "main.go"} } ``` ## GetArg 参数获取 参数获取可以通过以下两个方法: 1. `GetArg` 方法用以获取默认解析的命令行参数,参数通过输入索引位置获取,索引位置从 0 开始,但往往我们需要获取的参数是从 1 开始,因为索引 0 的参数是程序名称。 2. `GetArgAll` 方法用于获取所有的命令行参数。 使用示例: ```go func ExampleGetArg() { gcmd.Init("gf", "build", "main.go", "-o=gf.exe", "-y") fmt.Printf( `Arg[0]: "%v", Arg[1]: "%v", Arg[2]: "%v", Arg[3]: "%v"`, gcmd.GetArg(0), gcmd.GetArg(1), gcmd.GetArg(2), gcmd.GetArg(3), ) // Output: // Arg[0]: "gf", Arg[1]: "build", Arg[2]: "main.go", Arg[3]: "" } func ExampleGetArgAll() { gcmd.Init("gf", "build", "main.go", "-o=gf.exe", "-y") fmt.Printf(`%#v`, gcmd.GetArgAll()) // Output: // []string{"gf", "build", "main.go"} } ``` ## GetOpt 选项获取 选项获取可以通过以下两个方法: 1. `GetOpt` 方法用以获取默认解析的命令行选项,选项通过名称获取,并且选项的输入没有顺序性,可以输入到任意的命令行位置。当给定名称的选项数据不存在时,返回 `nil`。注意判断不带数据的选项是否存在时,可以通过 `GetOpt(name) != nil` 方式。 2. `GetOptAll` 方法用于获取所有的选项。 使用示例: ```go func ExampleGetOpt() { gcmd.Init("gf", "build", "main.go", "-o=gf.exe", "-y") fmt.Printf( `Opt["o"]: "%v", Opt["y"]: "%v", Opt["d"]: "%v"`, gcmd.GetOpt("o"), gcmd.GetOpt("y"), gcmd.GetOpt("d", "default value"), ) // Output: // Opt["o"]: "gf.exe", Opt["y"]: "", Opt["d"]: "default value" } func ExampleGetOptAll() { gcmd.Init("gf", "build", "main.go", "-o=gf.exe", "-y") fmt.Printf(`%#v`, gcmd.GetOptAll()) // May Output: // map[string]string{"o":"gf.exe", "y":""} } ``` ## GetOptWithEnv 特性 ```go func GetOptWithEnv(key string, def ...interface{}) *gvar.Var ``` 该方法用于获取命令行中指定的选项数值,如果该选项不存在时,则从环境变量中读取。但是两者的名称规则会不一样。例如: `gcmd.GetOptWithEnv("gf.debug")` 将会优先去读取命令行中的 `gf.debug` 选项,当不存在时,则会去读取 `GF_DEBUG` 环境变量。 需要注意的是参数命名转换规则: - 环境变量会将名称转换为大写,名称中的 `.` 字符转换为 `_` 字符。 - 命令行中会将名称转换为小写,名称中的 `_` 字符转换为 `.` 字符。 使用示例: ```go func ExampleGetOptWithEnv() { fmt.Printf("Opt[gf.test]:%s\n", gcmd.GetOptWithEnv("gf.test")) _ = genv.Set("GF_TEST", "YES") fmt.Printf("Opt[gf.test]:%s\n", gcmd.GetOptWithEnv("gf.test")) // Output: // Opt[gf.test]: // Opt[gf.test]:YES } ``` # 命令管理-Parser解析 ## 基本介绍 命令行解析最主要的是针对于选项的解析, `gcmd` 组件提供了 `Parse` 方法,用于自定义解析选项,包括有哪些选项名称,每个选项是否带有数值。根据这一配置便可将所有的参数和选项进行解析归类。 **提示** 大部分场景下,我们并不需要显式创建 `Parser` 对象,因为我们有层级管理以及对象管理方式来管理多命令。但底层仍然是采用 `Parser` 方式实现,因此本章节大家了解原理即可。 相关方法: 更多 `Parser` 方法请参考接口文档: [https://pkg.go.dev/github.com/gogf/gf/v2/os/gcmd#Parser](https://pkg.go.dev/github.com/gogf/gf/v2/os/gcmd#Parser) ```go func Parse(supportedOptions map[string]bool, option ...ParserOption) (*Parser, error) func ParseWithArgs(args []string, supportedOptions map[string]bool, option ...ParserOption) (*Parser, error) func ParserFromCtx(ctx context.Context) *Parser func (p *Parser) GetArg(index int, def ...string) *gvar.Var func (p *Parser) GetArgAll() []string func (p *Parser) GetOpt(name string, def ...interface{}) *gvar.Var func (p *Parser) GetOptAll() map[string]string ``` 其中, `ParserOption` 如下: ```go // ParserOption manages the parsing options. type ParserOption struct { CaseSensitive bool // Marks options parsing in case-sensitive way. Strict bool // Whether stops parsing and returns error if invalid option passed. } ``` 解析示例: ```go parser, err := gcmd.Parse(g.MapStrBool{ "n,name": true, "v,version": true, "a,arch": true, "o,os": true, "p,path": true, }) ``` 可以看到,选项输入参数其实是一个 `map` 类型。其中键值为选项名称,同一个选项的不同名称可以通过 `,` 符号进行分隔。比如,该示例中 `n` 和 `name` 选项是同一个选项,当用户输入 `-n john` 的时候, `n` 和 `name` 选项都会获得到数据 `john`。 而键值是一个布尔类型,标识该选项是否需要解析参数。这一选项配置是非常重要的,因为有的选项是不需要获得数据的,仅仅作为一个标识。例如, `-f force` 这个输入,在需要解析数据的情况下,选项 `f` 的值为 `force`;而在不需要解析选项数据的情况下,其中的 `force` 便是命令行的一个参数,而不是选项。 ## 使用示例 ```go func ExampleParse() { os.Args = []string{"gf", "build", "main.go", "-o=gf.exe", "-y"} p, err := gcmd.Parse(g.MapStrBool{ "o,output": true, "y,yes": false, }) if err != nil { panic(err) } fmt.Println(p.GetOpt("o")) fmt.Println(p.GetOpt("output")) fmt.Println(p.GetOpt("y") != nil) fmt.Println(p.GetOpt("yes") != nil) fmt.Println(p.GetOpt("none") != nil) // Output: // gf.exe // gf.exe // true // true // false } ``` # 命令管理-命令行对象 ## 基本介绍 大部分场景下,我们通过`Command`命令行对象来管理单个或多个命令,并且使用默认的命令行解析规则(不用显式使用`Parser`解析器)即可。`Command`对象定义如下: 详细请参考接口文档: [https://pkg.go.dev/github.com/gogf/gf/v2/os/gcmd@master#Command](https://pkg.go.dev/github.com/gogf/gf/v2/os/gcmd@master#Command) ```go // Command holds the info about an argument that can handle custom logic. type Command struct { Name string // Command name(case-sensitive). Usage string // A brief line description about its usage, eg: gf build main.go [OPTION] Brief string // A brief info that describes what this command will do. Description string // A detailed description. Arguments []Argument // Argument array, configuring how this command act. Func Function // Custom function. FuncWithValue FuncWithValue // Custom function with output parameters that can interact with command caller. HelpFunc Function // Custom help function Examples string // Usage examples. Additional string // Additional info about this command, which will be appended to the end of help info. Strict bool // Strict parsing options, which means it returns error if invalid option given. Config string // Config node name, which also retrieves the values from config component along with command line. parent *Command // Parent command for internal usage. commands []*Command // Sub commands of this command. } ``` 由于对象均有详细的注释,这里不再赘述。 ## 回调方法 `Command`对象支持3个回调方法: - `Func` 我们一般需要自定义这个回调方法,用于实现当前命令执行的操作。 - `FuncWithValue` 方法同`Func`,只不过支持返回值,往往用于命令行相互调用的场景,一般项目用不到。 - `HelpFunc` 自定义帮助信息,一般来说没有太大必要,因为`Command`对象能够自动生成帮助信息。 我们主要关注`Func`回调方法即可,其他方法大家感兴趣可以自行研究。 ## Func 回调 方法定义: ```go // Function is a custom command callback function that is bound to a certain argument. type Function func(ctx context.Context, parser *Parser) (err error) ``` 可以看到,在回调方法内部,我们通过 `parser` 对象获取解析参数和选项,并通过返回 `error` 来告诉上层调用方法是否执行成功。 使用示例: ```go package main import ( "context" "github.com/gogf/gf/v2/frame/g" "github.com/gogf/gf/v2/net/ghttp" "github.com/gogf/gf/v2/os/gcmd" "github.com/gogf/gf/v2/os/gctx" ) var ( Main = &gcmd.Command{ Name: "main", Brief: "start http server", Description: "this is the command entry for starting your http server", Func: func(ctx context.Context, parser *gcmd.Parser) (err error) { s := g.Server() s.BindHandler("/", func(r *ghttp.Request) { r.Response.Write("Hello world") }) s.SetPort(8199) s.Run() return }, } ) func main() { Main.Run(gctx.New()) } ``` 这也是大部分项目的启动命令行对象的样子,大部分项目只有一个启动入口,并且只会有一个回调方法实现。 ## 帮助信息生成 `Command`对象虽然可以自定义`HelpFunc`帮助回调方法,但`Command`对象可以自动生成`Help`使用帮助信息,大部分场景下无需自定义。并且`gcmd`组件默认内置了支持了`h`/`help`选项,因此使用`gcmd`组件的程序可以通过这两个选项自动生成`Help`帮助信息。 我们来看一个例子,我们先通过 `go build main.go` 把上面的例子编译为二进制 `main` 文件,然后来简单看一下只有一个命令场景下自动生成的帮助信息: ```bash $ ./main -h USAGE main [OPTION] DESCRIPTION this is the command entry for starting your http server ``` ## 层级命令管理 ### 父命令与子命令 一个`Command`命令可以添加子级命令,当`Command`存在子级命令时,自己便成为了父级命令。子级命令也可以添加自己的子级命令,以此类推,形成层级命令关系。父级命令和子级命令都可以有自己的回调方法,不过大部分场景下,一旦`Command`成为了父级命令,回调方法往往都没有太大存在的必要。我们通常通过`AddCommand`方法为`Command`添加子级命令: ```go // AddCommand adds one or more sub-commands to current command. func (c *Command) AddCommand(commands ...*Command) error ``` ### 层级命令使用示例 我们来演示一个多命令管理的示例。我们将上面的例子改进一下,增加两个子级命令。 ```go package main import ( "context" "fmt" "github.com/gogf/gf/v2/os/gcmd" "github.com/gogf/gf/v2/os/gctx" ) var ( Main = &gcmd.Command{ Name: "main", Brief: "start http server", Description: "this is the command entry for starting your process", } Http = &gcmd.Command{ Name: "http", Brief: "start http server", Description: "this is the command entry for starting your http server", Func: func(ctx context.Context, parser *gcmd.Parser) (err error) { fmt.Println("start http server") return }, } Grpc = &gcmd.Command{ Name: "grpc", Brief: "start grpc server", Description: "this is the command entry for starting your grpc server", Func: func(ctx context.Context, parser *gcmd.Parser) (err error) { fmt.Println("start grpc server") return }, } ) func main() { err := Main.AddCommand(Http, Grpc) if err != nil { panic(err) } Main.Run(gctx.New()) } ``` 可以看到,我们通过 `AddCommand` 命令为主命令增加了两个子级命令 `http`/`grpc`,分别用于开启 `HTTP`/`gRPC` 服务。当存在子级命令式,父命令往往便没有 `Func` 回调定义的必要了,因此我们这里去掉了 `main` 命令的 `Func` 定义。 我们编译后来执行一下看看效果: ```bash $ main USAGE main COMMAND [OPTION] COMMAND http start http server grpc start grpc server DESCRIPTION this is the command entry for starting your process ``` 使用 `http` 命令: ```bash $ main http start http server ``` 使用 `grpc` 命令: ```bash $ main grpc start grpc server ``` # 命令管理-结构化参数 ## 命令行管理痛点 前面我们介绍的命令行管理,都是通过回调函数的 `parser` 对象获取解析的参数及选项数据,在使用的时候存在以下痛点: - 需要手动传入硬编码的参数索引或者选项名称信息来获取数据 - 难以定义参数/选项的说明介绍 - 难以定义参数/选项的数据类型 - 难以对参数/选项进行通用性的数据校验 - 对于需要管理大量命令行的项目是个灾难 ## 对象化管理命令 我们来一个最简单的结构化管理参数示例。我们将前面介绍过的 `Command` 示例改造为结构化管理: ```go package main import ( "context" "fmt" "github.com/gogf/gf/v2/frame/g" "github.com/gogf/gf/v2/os/gcmd" "github.com/gogf/gf/v2/os/gctx" ) type cMain struct { g.Meta `name:"main"` } type cMainHttpInput struct { g.Meta `name:"http" brief:"start http server"` } type cMainHttpOutput struct{} type cMainGrpcInput struct { g.Meta `name:"grpc" brief:"start grpc server"` } type cMainGrpcOutput struct{} func (c *cMain) Http(ctx context.Context, in cMainHttpInput) (out *cMainHttpOutput, err error) { fmt.Println("start http server") return } func (c *cMain) Grpc(ctx context.Context, in cMainGrpcInput) (out *cMainGrpcOutput, err error) { fmt.Println("start grpc server") return } func main() { cmd, err := gcmd.NewFromObject(cMain{}) if err != nil { panic(err) } cmd.Run(gctx.New()) } ``` 可以看到,我们通过对象的形式来管理父级命令,通过方法的形式来管理其下一层级的子级命令,并通过规范化的 `Input` 输入参数对象来定义子级命令的描述/参数/选项。大部分场景下,大家可以忽略 `Output` 返回对象的使用,但为规范化及扩展性需要保留,如果未用到,该返回参数直接返回 `nil` 即可。关于其中的结构体标签,后续会有介绍。 我们将示例代码编译后,执行查看效果: ```bash $ main USAGE main COMMAND [OPTION] COMMAND http start http server grpc start grpc server DESCRIPTION this is the command entry for starting your process ``` 使用 `HTTP` 命令: ```bash $ main http start http server ``` 使用 `grpc` 命令: ```bash $ main grpc start grpc server ``` 效果和前面介绍的示例一致。 ## 结构化管理入参 既然命令行通过对象化管理,我们仔细看看参数/选项是如何通过结构化管理。 我们将上面的实例简化一下,来个简单的例子,实现通过 `http` 命令开启 `HTTP` 服务: ```go package main import ( "context" "github.com/gogf/gf/v2/frame/g" "github.com/gogf/gf/v2/net/ghttp" "github.com/gogf/gf/v2/os/gcmd" "github.com/gogf/gf/v2/os/gctx" ) type cMain struct { g.Meta `name:"main" brief:"start http server"` } type cMainHttpInput struct { g.Meta `name:"http" brief:"start http server"` Name string `v:"required" name:"NAME" arg:"true" brief:"server name"` Port int `v:"required" short:"p" name:"port" brief:"port of http server"` } type cMainHttpOutput struct{} func (c *cMain) Http(ctx context.Context, in cMainHttpInput) (out *cMainHttpOutput, err error) { s := g.Server(in.Name) s.BindHandler("/", func(r *ghttp.Request) { r.Response.Write("Hello world") }) s.SetPort(in.Port) s.Run() return } func main() { cmd, err := gcmd.NewFromObject(cMain{}) if err != nil { panic(err) } cmd.Run(gctx.New()) } ``` 我们为 `http` 命令定义了两个输入参数: - `NAME` 服务的名称,通过参数输入。这里使用了大写形式,方便展示在自动生成的帮助信息中 - `port` 服务的端口,通过 `p/port` 选项输入 并且我们通过`v:"required"`校验标签为这两个参数都绑定的必需的校验规则。是的,在`GoFrame`框架中,只要涉及到校验的地方都使用了统一的校验组件,具体请参考章节:[数据校验](./gvalid.md) 我们编译后执行看看效果: ```bash $ main http arguments validation failed for command "http": The Name field is required 1. arguments validation failed for command "http" 1). github.com/gogf/gf/v2/os/gcmd.newCommandFromMethod.func1 /Users/john/Workspace/Go/GOPATH/src/github.com/gogf/gf/v2/os/gcmd/gcmd_command_object.go:290 2). github.com/gogf/gf/v2/os/gcmd.(*Command).doRun /Users/john/Workspace/Go/GOPATH/src/github.com/gogf/gf/v2/os/gcmd/gcmd_command_run.go:120 3). github.com/gogf/gf/v2/os/gcmd.(*Command).RunWithValueError /Users/john/Workspace/Go/GOPATH/src/github.com/gogf/gf/v2/os/gcmd/gcmd_command_run.go:77 4). github.com/gogf/gf/v2/os/gcmd.(*Command).RunWithValue /Users/john/Workspace/Go/GOPATH/src/github.com/gogf/gf/v2/os/gcmd/gcmd_command_run.go:32 5). github.com/gogf/gf/v2/os/gcmd.(*Command).Run /Users/john/Workspace/Go/GOPATH/src/github.com/gogf/gf/v2/os/gcmd/gcmd_command_run.go:26 6). main.main /Users/john/Workspace/Go/GOPATH/src/github.com/gogf/gf/.test/test.go:38 2. The Name field is required ``` 执行后,报错了,这个错误来自于数据校验,表示必须参数( `Name/Port`)必须传递。 **提示** 这里的报错打印了堆栈信息,因为 `GoFrame` 框架采用了全错误堆栈设计,所有组件错误都会带有自底向上的错误堆栈,以方便错误快速定位。当然我们可以通过 `RunWithError` 方法获取返回的错误对象关闭堆栈信息。 我增加参数输入再试试: ```bash $ main http my-http-server -p 8199 2022-01-19 22:52:45.808 [DEBU] openapi specification is disabled SERVER | DOMAIN | ADDRESS | METHOD | ROUTE | HANDLER | MIDDLEWARE -----------------|---------|---------|--------|-------|-----------------------------------------------------------------|-------------------- my-http-server | default | :8199 | ALL | / | main.(*cMain).Http.func1 | -----------------|---------|---------|--------|-------|-----------------------------------------------------------------|-------------------- my-http-server | default | :8199 | ALL | /* | github.com/gogf/gf/v2/net/ghttp.internalMiddlewareServerTracing | GLOBAL MIDDLEWARE -----------------|---------|---------|--------|-------|-----------------------------------------------------------------|-------------------- 2022-01-19 22:52:45.810 66292: http server started listening on [:8199] ``` 是的,这就对了。 ## 完整使用案例 GoFrame 框架的开发工具广泛使用了对象化、结构化的命令行管理,大家感兴趣可以更进一步查看源码了解:[https://github.com/gogf/gf/tree/master/cmd/gf](https://github.com/gogf/gf/tree/master/cmd/gf) ## 预定义的标签 在结构化设计中,我们使用了一些结构体标签,大部分来源于 `Command` 命令的属性,这里我们来介绍一下: | 标签 | 缩写 | 说明 | 注意事项 | | --- | --- | --- | --- | | `name` | - | 命名名称 | 如果是输入参数结构体,在未指定 `name` 时将会自动读取 **方法名称** 作为 `name` | | `short` | - | 命令缩写 | | | `usage` | - | 命令使用 | | | `brief` | - | 命令描述 | | | `arg` | - | 表示该输入参数来源于参数而不是选项 | 仅用于属性标签 | | `orphan` | - | 表示该选项不带参数 | 属性通常为 `bool` 类型 | | `description` | `dc` | 命令的详细介绍 | | | `additional` | `ad` | 命令的额外描述信息 | | | `examples` | `eg` | 命令的使用示例 | | | `root` | - | 指定子级命令名称是父级命令,其他方法是它的子级命令 | 仅用于 **主命令** 对象结构体 `Meta` 标签 | | `strict` | - | 表示该命令严格解析参数/选项,当输入不支持的参数/选项时,返回错误 | 仅用于对象结构体 `Meta` 标签 | | `config` | - | 表示该命令的选项数据支持从指定的配置读取,配置来源于默认的全局单例配置对象 | 仅用于方法输入结构体 `Meta` 标签 | | `default` | `d` | 表示该输入参数不传递时使用默认值 | 仅用于属性标签 | ## 高级特性 ### 自动数据转换 结构化的参数输入支持自动的数据类型转换,只需要定义好数据类型,其他的事情交给框架组件即可。自动数据类型转换出现在框架的很多组件中,特别是 `HTTP`/`gRPC` 服务的参数输入中。底层数据转换组件使用的是:[类型转换](./gconv.md) **提示** 命令行参数的数据转换采用 **不区分大小写、且忽略特殊字符** 的规则来匹配属性字段。例如,如果入参结构体中存在 `Name` 字段属性,无论命令行输入 `name` 还是 `NAME` 的命名参数,都将能被 `Name` 字段属性接收。 ### 自动数据校验 同样的,数据校验组件也是使用的统一的组件,具体请参考章节: [数据校验](./gvalid.md) ### 从配置读取数据 当命令行中没有传递对应的数据时,输入参数的结构体数据支持从配置组件中自动获取,只需要在 `Meta` 中设置 `config` 标签即可,配置来源于默认的全局单例配置对象。具体示例可以参考 GoFrame 框架开发工具源码:[https://github.com/gogf/gf/tree/master/cmd/gf](https://github.com/gogf/gf/tree/master/cmd/gf) # 命令管理-终端交互 ## 基本介绍 `gcmd` 组件支持从终端读取用户输入数据,常用于终端交互场景。 相关方法: ```go func Scan(info ...interface{}) string func Scanf(format string, info ...interface{}) string ``` 这两个方法都是向终端展示给定的信息,自动读取终端用户输入的信息返回,通过回车符号返回。 ## `Scan` 使用 ```go package main import ( "fmt" "github.com/gogf/gf/v2/os/gcmd" ) func main() { name := gcmd.Scan("What's your name?\n") fmt.Println("Your name is:", name) } ``` 执行后,交互示例: ``` > What's your name? john > Your name is: john ``` ## `Scanf` 使用 ```go package main import ( "fmt" "github.com/gogf/gf/v2/os/gcmd" ) func main() { name := gcmd.Scan("> What's your name?\n") age := gcmd.Scanf("> Hello %s, how old are you?\n", name) fmt.Printf("> %s's age is: %s", name, age) } ``` 执行后,交互示例: ``` > What's your name? john > Hello john, how old are you? 18 > john's age is: 18 ``` # 命令管理-链路跟踪 ## 基本介绍 `GoFrame`命令管理组件也支持跨进程的链路跟踪特性,特别是对于一些临时运行的进程特别有用。 框架整体的链路跟踪都是采用的`OpenTelemetry`规范。 ## 使用示例 ### 主进程 `main.go` ```go package main import ( "context" "github.com/gogf/gf/v2/frame/g" "github.com/gogf/gf/v2/os/gcmd" "github.com/gogf/gf/v2/os/gctx" "github.com/gogf/gf/v2/os/gproc" ) var ( Main = &gcmd.Command{ Name: "main", Brief: "main process", Func: func(ctx context.Context, parser *gcmd.Parser) (err error) { g.Log().Debug(ctx, `this is main process`) return gproc.ShellRun(ctx, `go run sub.go`) }, } ) func main() { Main.Run(gctx.GetInitCtx()) } ``` ### 子进程 `sub.go` ```go package main import ( "context" "github.com/gogf/gf/v2/frame/g" "github.com/gogf/gf/v2/os/gcmd" "github.com/gogf/gf/v2/os/gctx" ) var ( Sub = &gcmd.Command{ Name: "sub", Brief: "sub process", Func: func(ctx context.Context, parser *gcmd.Parser) (err error) { g.Log().Debug(ctx, `this is sub process`) return nil }, } ) func main() { Sub.Run(gctx.GetInitCtx()) } ``` ### 执行结果 执行后,终端输出如下: ```bash $ go run main.go 2022-06-21 20:35:06.196 [DEBU] {00698a61e2a2fa1661da5d7993d72e8c} this is main process 2022-06-21 20:35:07.482 [DEBU] {00698a61e2a2fa1661da5d7993d72e8c} this is sub process ``` 可以看到,链路信息已经自动传递给了子进程。 # 配置管理 ## 基本介绍 `GoFrame` 的配置管理由 `gcfg` 组件实现, `gcfg` 组件的所有方法是并发安全的。 `gcfg` 组件采用接口化设计,默认提供的是基于文件系统的接口实现。 **使用方式**: ```go import "github.com/gogf/gf/v2/os/gcfg" ``` **接口文档**: [https://pkg.go.dev/github.com/gogf/gf/v2/os/gcfg](https://pkg.go.dev/github.com/gogf/gf/v2/os/gcfg) ## 组件特性 `gcfg` 组件具有以下显著特性: - 接口化设计,很高的灵活性及扩展性,默认提供文件系统接口实现 - 支持多种常见配置文件格式: `yaml/toml/json/xml/ini/properties` - 支持配置项不存在时读取指定环境变量或命令行参数 - 支持检索读取资源管理组件中的配置文件 - 支持配置文件自动检测热更新特性 - 支持层级访问配置项 - 支持单例管理模式 ## 注意事项 框架配置组件支持多种常用的数据格式,但在后续的示例代码中均使用 `yaml` 数据格式来做演示说明。在使用中,请随意使用习惯的数据格式 **不用局限于官网示例使用的 `yaml` 数据格式**。例如,在业务项目模板中提供的是 `config.yaml` 配置文件模板(因为默认模板只能提供一种啊),也可以直接修改为 `config.toml` 或者 `config.ini` 等支持的数据格式, **配置组件也能自动根据文件名后缀识别读取**。 # 配置管理-配置对象 我们推荐使用单例模式获取配置管理对象。我们可以方便地通过`g.Cfg()`获取默认的全局配置管理对象。同时,我们也可以通过`gcfg.Instance`包方法获取配置管理对象单例。 ### 使用`g.Cfg` 我们来看一个示例,演示如何读取全局配置的信息。需要注意的是,全局配置是与框架相关的,因此统一使用`g.Cfg()`进行获取。以下是一个默认的全局配置文件,包含了模板引擎的目录配置以及`MySQL`数据库集群(两台`master`)的配置。 示例配置: ```yaml viewpath: "/home/www/templates/" database: default: - link: "mysql:root:12345678@tcp(127.0.0.1:3306)/test" role: "master" - link: "mysql:root:12345678@tcp(127.0.0.1:3306)/test" role: "slave" ``` 示例代码: ```go package main import ( "fmt" "github.com/gogf/gf/v2/frame/g" "github.com/gogf/gf/v2/os/gctx" ) func main() { var ctx = gctx.New() fmt.Println(g.Cfg().Get(ctx, "viewpath")) fmt.Println(g.Cfg().Get(ctx, "database.default.0.role")) } ``` 以上示例为读取数据库的第一个配置的 `role` 信息。运行后输出: ``` /home/www/templates/ master ``` 可以看到,我们可以通过 `g.Cfg()` 方法获取一个全局的配置管理器单例对象。配置文件内容可以通过英文「 `.`」号进行层级访问(数组默认从 `0` 开始), `pattern` 参数 `database.default.0.role` 表示读取 `database` 配置项中 `default` 数据库集群中的第 `0` 项数据库服务器的 `role` 数据。 ### 使用 `gcfg.Instance` 当然也可以独立使用 `gcfg` 包,通过 `Instance` 方法获取单例对象。 ```go package main import ( "fmt" "github.com/gogf/gf/v2/os/gcfg" "github.com/gogf/gf/v2/os/gctx" ) func main() { var ctx = gctx.New() fmt.Println(gcfg.Instance().Get(ctx, "viewpath")) fmt.Println(gcfg.Instance().Get(ctx, "database.default.0.role")) } ``` ### 自动检索特性 单例对象在创建时会按照文件后缀 `toml/yaml/yml/json/ini/xml/properties` 自动检索配置文件。默认情况下会自动检索配置文件 `config.toml/yaml/yml/json/ini/xml/properties` 并缓存,配置文件在外部被修改时将会自动刷新缓存。 为方便多文件场景下的配置文件调用,简便使用并提高开发效率,单例对象在创建时将会自动使用 **单例名称** 进行文件检索。例如: `g.Cfg("redis")` 获取到的单例对象将默认会自动检索 `redis.toml/yaml/yml/json/ini/xml/properties`,如果检索成功那么将该文件加载到内存缓存中,下一次将会直接从内存中读取;当该文件不存在时,则使用默认的配置文件( `config.toml`)。 # 配置管理-文件配置 `gcfg`组件采用接口化设计,默认提供的是基于文件系统的接口实现。支持的数据文件格式包括:`JSON/XML/YAML(YML)/TOML/INI/PROPERTIES`,项目中开发者可以灵活地选择自己熟悉的配置文件格式来进行配置管理。 ## 配置文件 ### 默认配置文件 配置对象我们推荐使用单例方式获取,单例对象将会按照文件后缀`toml/yaml/yml/json/ini/xml/properties`文自动检索配置文件。默认情况下会自动检索配置文件`config.toml/yaml/yml/json/ini/xml/properties`并缓存,配置文件在外部被修改时将会自动刷新缓存。 如果想要自定义文件格式,可以通过`SetFileName`方法修改默认读取的配置文件名称(如:`default.yaml`、`default.json`、`default.xml`等等)。例如,我们可以通过以下方式读取`default.yaml`配置文件中的数据库`database`配置项。 ```go // 设置默认配置文件,默认读取的配置文件设置为 default.yaml g.Cfg().GetAdapter().(*gcfg.AdapterFile).SetFileName("default.yaml") // 后续读取时将会读取到 default.yaml 配置文件内容 g.Cfg().Get(ctx, "database") ``` ### 默认文件修改 **提示** 文件可以是一个具体的文件名称或者完整的文件绝对路径。 我们可以通过多种方式修改默认文件名称: 1. 通过配置管理方法 `SetFileName` 修改。 2. 修改命令行启动参数 - `gf.gcfg.file`。 3. 修改指定的环境变量 - `GF_GCFG_FILE`。 假如我们的执行程序文件为 `main`,那么可以通过以下方式修改配置管理器的配置文件目录( `Linux` 下): 1. **通过单例模式** ```go g.Cfg().GetAdapter().(*gcfg.AdapterFile).SetFileName("default.yaml") ``` 2. **通过命令行启动参数** ```shell ./main --gf.gcfg.file=config.prod.toml ``` 3. **通过环境变量(常用在容器中)** - 启动时修改环境变量: ```shell GF_GCFG_FILE=config.prod.toml; ./main ``` - 使用 `genv` 模块来修改环境变量: ```go genv.Set("GF_GCFG_FILE", "config.prod.toml") ``` ## 配置目录 ### 目录配置方法 `gcfg` 配置管理器支持非常灵活的多目录自动搜索功能,通过 `SetPath` 可以修改目录管理目录为 **唯一** 的目录地址,同时,我们推荐通过 `AddPath` 方法添加多个搜索目录,配置管理器底层将会按照添加目录的顺序作为优先级进行自动检索。直到检索到一个匹配的文件路径为止,如果在所有搜索目录下查找不到配置文件,那么会返回失败。 ### 默认目录配置 `gcfg` 配置管理对象初始化时,默认会自动添加以下配置文件搜索目录: 1. **当前工作目录及其下的 `config`、 `manifest/config` 目录**:例如当前的工作目录为 `/home/www` 时,将会添加: - `/home/www` - `/home/www/config` - `/home/www/manifest/config` 2. **当前可执行文件所在目录及其下的 `config`、 `manifest/config` 目录**:例如二进制文件所在目录为 `/tmp` 时,将会添加: - `/tmp` - `/tmp/config` - `/tmp/manifest/config` 3. **当前 `main` 源代码包所在目录及其下的 `config`、 `manifest/config` 目录**(仅对源码开发环境有效):例如 `main` 包所在目录为 `/home/john/workspace/gf-app` 时,将会添加: - `/home/john/workspace/gf-app` - `/home/john/workspace/gf-app/config` - `/home/john/workspace/gf-app/manifest/config` ### 默认目录修改 **注意** 注意这里修改的参数必须是一个目录,不能是文件路径。 我们可以通过以下方式修改配置管理器的配置文件搜索目录,配置管理对象将会只在该指定目录执行配置文件检索: 1. 通过配置管理器的 `SetPath` 方法手动修改; 2. 修改命令行启动参数 - `gf.gcfg.path`; 3. 修改指定的环境变量 - `GF_GCFG_PATH`; 假如我们的执行程序文件为 `main`,那么可以通过以下方式修改配置管理器的配置文件目录(Linux下): 1. **通过单例模式** ```go g.Cfg().GetAdapter().(*gcfg.AdapterFile).SetPath("/opt/config") ``` 2. **通过命令行启动参数** ```shell ./main --gf.gcfg.path=/opt/config/ ``` 3. **通过环境变量(常用在容器中)** - 启动时修改环境变量: ```shell GF_GCFG_PATH=/opt/config/; ./main ``` - 使用 `genv` 模块来修改环境变量: ```go genv.Set("GF_GCFG_PATH", "/opt/config") ``` ## 内容配置 `gcfg` 包也支持直接内容配置,而不是读取配置文件,常用于程序内部动态修改配置内容。通过以下包配置方法实现全局的配置: ```go func (c *AdapterFile) SetContent(content string, file ...string) func (c *AdapterFile) GetContent(file ...string) string func (c *AdapterFile) RemoveContent(file ...string) func (c *AdapterFile) ClearContent() ``` 需要注意的是该配置是全局生效的,并且优先级会高于读取配置文件。因此,假如我们通过 `SetContent("v = 1", "config.toml")` 配置了 `config.toml` 的配置内容,并且也同时存在 `config.toml` 配置文件,那么只会使用到 `SetContent` 的配置内容,而配置文件内容将会被忽略。 ## 层级访问 在默认提供的文件系统接口实现下, `gcfg` 组件支持按层级获取配置数据,层级访问默认通过英文 `.` 号指定,其中 `pattern` 参数和 [通用编解码-gjson](../components/encoding-gjson.md) 的 `pattern` 参数一致。例如以下配置( `config.yaml`): ```yaml server: address: ":8199" serverRoot: "resource/public" database: default: link: "mysql:root:12345678@tcp(127.0.0.1:3306)/focus" debug: true ``` 例如针对以上配置文件内容的层级读取: ```go // :8199 g.Cfg().Get(ctx, "server.address") // true g.Cfg().Get(ctx, "database.default.debug") ``` ## 注意事项 大家都知道,在 `Golang` 里面, `map/slice` 类型其实是一个」引用类型」(也叫」指针类型」),因此当对这种类型的变量 键值对/索引项 进行修改时,会同时修改到其对应的底层数据。从效率上考虑, `gcfg` 包某些获取方法返回的数据类型为 `map/slice` 时,没有对其做值拷贝,因此当对返回的数据进行修改时,会同时修改 `gcfg` 对应的底层数据。 例如: 配置文件: ```go // config.json: `{"map":{"key":"value"}, "slice":[59,90]}` ``` 示例代码: ```go var ctx = gctx.New() m := g.Cfg().MustGet(ctx, "map").Map() fmt.Println(m) // Change the key-value pair. m["key"] = "john" // It changes the underlying key-value pair. fmt.Println(g.Cfg().MustGet(ctx, "map").Map()) s := g.Cfg().MustGet(ctx, "slice").Slice() fmt.Println(s) // Change the value of specified index. s[0] = 100 // It changes the underlying slice. fmt.Println(g.Cfg().MustGet(ctx, "slice").Slice()) // output: // map[key:value] // map[key:john] // [59 90] // [100 90] ``` ## 检测更新 配置管理器使用了 **缓存机制**,当配置文件第一次被读取后会被缓存到内存中,下一次读取时将会直接从缓存中获取,以提高性能。同时,配置管理器提供了对配置文件的 **自动检测更新机制**,当配置文件在外部被修改后,配置管理器能够即时地刷新配置文件的缓存内容。 # 配置管理-常用方法 **提示** 以下常用方法列表,文档更新可能滞后于代码新特性,更多的方法及示例请参考代码文档: [https://pkg.go.dev/github.com/gogf/gf/v2/os/gcfg](https://pkg.go.dev/github.com/gogf/gf/v2/os/gcfg) ## `GetWithEnv` - 说明 - `GetWithEnv`方法会先从默认的配置文件中获取配置数据,获取为空的时候,将会去当前的环境变量中进行获取。需要注意的是名称命名转换规则: - 环境变量会将名称转换为大写,名称中的`.`字符转换为`_`字符。 - 参数名称中会将名称转换为小写,名称中的`_`字符转换为`.`字符。 - 格式: ```go GetWithEnv(ctx context.Context, pattern string, def ...interface{}) (*gvar.Var, error) ``` - 示例: ```go func ExampleConfig_GetWithEnv() { var ( key = `env.test` ctx = gctx.New() ) v, err := g.Cfg().GetWithEnv(ctx, key) if err != nil { panic(err) } fmt.Printf("env:%s\n", v) if err = genv.Set(`ENV_TEST`, "gf"); err != nil { panic(err) } v, err = g.Cfg().GetWithEnv(ctx, key) if err != nil { panic(err) } fmt.Printf("env:%s", v) // Output: // env: // env:gf } ``` ## `GetWithCmd` - 说明:`GetWithCmd`方法与`GetWithEnv`方法类似,也是先从默认的配置对象中获取配置数据,但是获取为空的时候,是去命令行中获取配置信息。 - 格式: ```go GetWithCmd(ctx context.Context, pattern string, def ...interface{}) (*gvar.Var, error) ``` - 示例: ```go func ExampleConfig_GetWithCmd() { var ( key = `cmd.test` ctx = gctx.New() ) v, err := g.Cfg().GetWithCmd(ctx, key) if err != nil { panic(err) } fmt.Printf("cmd:%s\n", v) // Re-Initialize custom command arguments. os.Args = append(os.Args, fmt.Sprintf(`--%s=yes`, key)) gcmd.Init(os.Args...) // Retrieve the configuration and command option again. v, err = g.Cfg().GetWithCmd(ctx, key) if err != nil { panic(err) } fmt.Printf("cmd:%s", v) // Output: // cmd: // cmd:yes } ``` ## `MustGetWithCmd` - 说明:`MustGetWithCmd`方法与`GetWithCmd`方法类似,该方法只会返回配置内容,一旦内部发生任何错误,将会有`panic`。 - 格式: ```go MustGetWithCmd(ctx context.Context, pattern string, def ...interface{}) *gvar.Var ``` - 示例: ```go func ExampleConfig_MustGetWithCmd() { var ( key = `cmd.test` ctx = gctx.New() ) v := g.Cfg().MustGetWithCmd(ctx, key) fmt.Printf("cmd:%s\n", v) // Re-Initialize custom command arguments. os.Args = append(os.Args, fmt.Sprintf(`--%s=yes`, key)) gcmd.Init(os.Args...) // Retrieve the configuration and command option again. v = g.Cfg().MustGetWithCmd(ctx, key) fmt.Printf("cmd:%s", v) // Output: // cmd: // cmd:yes } ``` ## `MustGetWithEnv` - 说明: `MustGetWithEnv` 方法与 `GetWithEnv` 方法类似,该方法只会返回配置内容,一旦内部发生任何错误,将会有 `panic。` - 格式: ```go MustGetWithEnv(ctx context.Context, pattern string, def ...interface{}) *gvar.Var ``` - 示例: ```go func ExampleConfig_MustGetWithEnv() { var ( key = `env.test` ctx = gctx.New() ) v := g.Cfg().MustGetWithEnv(ctx, key) fmt.Printf("env:%s\n", v) if err := genv.Set(`ENV_TEST`, "gf"); err != nil { panic(err) } v = g.Cfg().MustGetWithEnv(ctx, key) fmt.Printf("env:%s", v) // Output: // env: // env:gf } ``` ## `Data` - 说明: `Data` 方法从配置对象中获取配置数据,组装成 `map[string]interface{}` 类型。 - 格式: ```go Data(ctx context.Context) (data map[string]interface{}, err error) ``` - 示例: ```go func ExampleConfig_Data() { ctx := gctx.New() content := ` v1 = 1 v2 = "true" v3 = "off" v4 = "1.23" array = [1,2,3] [redis] disk = "127.0.0.1:6379,0" cache = "127.0.0.1:6379,1" ` c, err := gcfg.New() if err != nil{ panic(err) } c.GetAdapter().(*gcfg.AdapterFile).SetContent(content) data, err := c.Data(ctx) if err != nil{ panic(err) } fmt.Println(data) // Output: // map[array:[1 2 3] redis:map[cache:127.0.0.1:6379,1 disk:127.0.0.1:6379,0] v1:1 v2:true v3:off v4:1.23] } ``` ## `MustData` - 说明: `MustData` 方法从配置对象中获取配置数据,组装成 `map[string]interface{}` 类型。当该方法内部产生错误时不会返回错误,而是直接 `panic。` - 格式: ```go MustData(ctx context.Context) map[string]interface{} ``` - 示例: ```go func ExampleConfig_MustData() { ctx := gctx.New() content := ` v1 = 1 v2 = "true" v3 = "off" v4 = "1.23" array = [1,2,3] [redis] disk = "127.0.0.1:6379,0" cache = "127.0.0.1:6379,1" ` c, err := gcfg.New() if err != nil{ panic(err) } c.GetAdapter().(*gcfg.AdapterFile).SetContent(content) data := c.MustData(ctx) fmt.Println(data) // Output: // map[array:[1 2 3] redis:map[cache:127.0.0.1:6379,1 disk:127.0.0.1:6379,0] v1:1 v2:true v3:off v4:1.23] } ``` ## `Get` - 说明: `Get` 方法从配置对象中获取配置数据,返回 `gvar` 泛型对象。 - 格式: ```go Get(ctx context.Context, pattern string, def ...interface{}) (*gvar.Var, error) ``` - 示例: ```go func ExampleConfig_Get() { ctx := gctx.New() content := ` v1 = 1 v2 = "true" v3 = "off" v4 = "1.23" array = [1,2,3] [redis] disk = "127.0.0.1:6379,0" cache = "127.0.0.1:6379,1" ` c, err := gcfg.New() if err != nil{ panic(err) } c.GetAdapter().(*gcfg.AdapterFile).SetContent(content) data,err := c.Get(ctx,"redis") if err != nil { panic(err) } fmt.Println(data) // Output: // {"cache":"127.0.0.1:6379,1","disk":"127.0.0.1:6379,0"} } ``` ## `MustGet` - 说明: `MustGet` 方法与 `Get` 类似,也是配置对象中获取配置数据,组装成 `gvar` 结构,但是返回参数只有一个: `*gvar.Var` - 注意:当配置文件不存在或者是其他 `error` 的情况下,会直接 `panic`,需要做好异常捕获措施。 - 格式: ```go MustGet(ctx context.Context, pattern string, def ...interface{}) *gvar.Var ``` - 示例: ```go func ExampleConfig_MustGet() { ctx := gctx.New() content := ` v1 = 1 v2 = "true" v3 = "off" v4 = "1.23" array = [1,2,3] [redis] disk = "127.0.0.1:6379,0" cache = "127.0.0.1:6379,1" ` c, err := gcfg.New() if err != nil{ panic(err) } c.GetAdapter().(*gcfg.AdapterFile).SetContent(content) data := c.MustGet(ctx,"redis") fmt.Println(data) // Output: // {"cache":"127.0.0.1:6379,1","disk":"127.0.0.1:6379,0"} } ``` ## `GetAdapter` - 说明: `GetAdapter` 方法是获取当前运行的 `gcfg` 适配器信息,关于适配器相关,可以请点击 [配置管理-接口化设计](./gcfg-interface.md) - 格式: ```go GetAdapter() Adapter ``` - 示例: ```go func ExampleConfig_GetAdapter() { c, err := gcfg.New() if err != nil{ panic(err) } adapter := c.GetAdapter() fmt.Println(adapter) // Output: // &{config.toml 0xc00014d720 0xc000371880 false} } ``` ## `SetAdapter` - 说明: `SetAdapter` 方法是设置当前运行的 `gcfg` 适配器信息,关于适配器相关,可以请点击 [配置管理-接口化设计](./gcfg-interface.md) - 格式: ```go SetAdapter(adapter Adapter) ``` - 示例: ```go func ExampleConfig_SetAdapter() { c, err := gcfg.New() if err != nil{ panic(err) } adapter := c.GetAdapter() c.SetAdapter(adapter) fmt.Println(adapter) // Output: // &{config.toml 0xc00014d720 0xc000371880 false} } ``` # 配置管理-YAML格式 ### 一、简介 YAML 语言(发音 /ˈjæməl/ )的设计目标,就是方便人类读写。它实质上是一种通用的数据串行化格式。 它的基本语法规则如下。 > - 大小写敏感 > - 使用缩进表示层级关系 > - 缩进时不允许使用Tab键,只允许使用空格。 > - 缩进的空格数目不重要,只要相同层级的元素左侧对齐即可 `#` 表示注释,从这个字符一直到行尾,都会被解析器忽略。 YAML 支持的数据结构有三种。 > - 对象:键值对的集合,又称为映射(mapping)/ 哈希(hashes) / 字典(dictionary) > - 数组:一组按次序排列的值,又称为序列(sequence) / 列表(list) > - 纯量(scalars):单个的、不可再分的值 以下分别介绍这三种数据结构。 ### 二、对象 对象的一组键值对,使用冒号结构表示。 > ```javascript > animal: pets > ``` 转为 JavaScript 如下。 > ```javascript > { animal: 'pets' } > ``` Yaml 也允许另一种写法,将所有键值对写成一个行内对象。 > ```javascript > hash: { name: Steve, foo: bar } > ``` 转为 JavaScript 如下。 > ```javascript > { hash: { name: 'Steve', foo: 'bar' } } > ``` ### 三、数组 一组连词线开头的行,构成一个数组。 > ```javascript > - Cat > - Dog > - Goldfish > ``` 转为 JavaScript 如下。 > ```javascript > [ 'Cat', 'Dog', 'Goldfish' ] > ``` 数据结构的子成员是一个数组,则可以在该项下面缩进一个空格。 > ```javascript > - > - Cat > - Dog > - Goldfish > ``` 转为 JavaScript 如下。 > ```javascript > [ [ 'Cat', 'Dog', 'Goldfish' ] ] > ``` 数组也可以采用行内表示法。 > ```javascript > animal: [Cat, Dog] > ``` 转为 JavaScript 如下。 > ```javascript > { animal: [ 'Cat', 'Dog' ] } > ``` ### 四、复合结构 对象和数组可以结合使用,形成复合结构。 > ```javascript > languages: > - Ruby > - Perl > - Python > websites: > YAML: yaml.org > Ruby: ruby-lang.org > Python: python.org > Perl: use.perl.org > ``` 转为 JavaScript 如下。 > ```javascript > { languages: [ 'Ruby', 'Perl', 'Python' ], > websites: > { YAML: 'yaml.org', > Ruby: 'ruby-lang.org', > Python: 'python.org', > Perl: 'use.perl.org' } } > ``` ### 五、纯量 纯量是最基本的、不可再分的值。以下数据类型都属于 JavaScript 的纯量。 > - 字符串 > - 布尔值 > - 整数 > - 浮点数 > - Null > - 时间 > - 日期 数值直接以字面量的形式表示。 > ```javascript > number: 12.30 > ``` 转为 JavaScript 如下。 > ```javascript > { number: 12.30 } > ``` 布尔值用 `true` 和 `false` 表示。 > ```javascript > isSet: true > ``` 转为 JavaScript 如下。 > ```javascript > { isSet: true } > ``` `null` 用 `~` 表示。 > ```javascript > parent: ~ > ``` 转为 JavaScript 如下。 > ```javascript > { parent: null } > ``` 时间采用 ISO8601 格式。 > ```javascript > iso8601: 2001-12-14t21:59:43.10-05:00 > ``` 转为 JavaScript 如下。 > ```javascript > { iso8601: new Date('2001-12-14t21:59:43.10-05:00') } > ``` 日期采用复合 iso8601 格式的年、月、日表示。 > ```javascript > date: 1976-07-31 > ``` 转为 JavaScript 如下。 > ```javascript > { date: new Date('1976-07-31') } > ``` YAML 允许使用两个感叹号,强制转换数据类型。 > ```javascript > e: !!str 123 > f: !!str true > ``` 转为 JavaScript 如下。 > ```javascript > { e: '123', f: 'true' } > ``` ### 六、字符串 字符串是最常见,也是最复杂的一种数据类型。 字符串默认不使用引号表示。 > ```javascript > str: 这是一行字符串 > ``` 转为 JavaScript 如下。 > ```javascript > { str: '这是一行字符串' } > ``` 如果字符串之中包含空格或特殊字符,需要放在引号之中。 > ```javascript > str: '内容: 字符串' > ``` 转为 JavaScript 如下。 > ```javascript > { str: '内容: 字符串' } > ``` 单引号和双引号都可以使用,双引号不会对特殊字符转义。 > ```javascript > s1: '内容\n字符串' > s2: "内容\n字符串" > ``` 转为 JavaScript 如下。 > ```javascript > { s1: '内容\\n字符串', s2: '内容\n字符串' } > ``` 单引号之中如果还有单引号,必须连续使用两个单引号转义。 > ```javascript > str: 'labor''s day' > ``` 转为 JavaScript 如下。 > ```javascript > { str: 'labor\'s day' } > ``` 字符串可以写成多行,从第二行开始,必须有一个单空格缩进。换行符会被转为空格。 > ```javascript > str: 这是一段 > 多行 > 字符串 > ``` 转为 JavaScript 如下。 > ```javascript > { str: '这是一段 多行 字符串' } > ``` 多行字符串可以使用 `|` 保留换行符,也可以使用 `>` 折叠换行。 > ```javascript > this: | > Foo > Bar > that: > Foo > Bar > ``` 转为 JavaScript 代码如下。 > ```javascript > { this: 'Foo\nBar\n', that: 'Foo Bar\n' } > ``` `+` 表示保留文字块末尾的换行, `-` 表示删除字符串末尾的换行。 > ```javascript > s1: | > Foo > s2: |+ > Foo > s3: |- > Foo > ``` 转为 JavaScript 代码如下。 > ```javascript > { s1: 'Foo\n', s2: 'Foo\n\n\n', s3: 'Foo' } > ``` 字符串之中可以插入 HTML 标记。 > ```javascript > message: | >

段落 >

``` 转为 JavaScript 如下。 > ```javascript > { message: '\n

\n 段落\n

\n' } > ``` ### 七、引用 锚点 `&` 和别名 `*`,可以用来引用。 > ```javascript > defaults: &defaults > adapter: postgres > host: localhost > development: > database: myapp_development > <<: *defaults > test: > database: myapp_test > <<: *defaults > ``` 等同于下面的代码。 > ```javascript > defaults: > adapter: postgres > host: localhost > development: > database: myapp_development > adapter: postgres > host: localhost > test: > database: myapp_test > adapter: postgres > host: localhost > ``` `&` 用来建立锚点( `defaults`), `<<` 表示合并到当前数据, `*` 用来引用锚点。 下面是另一个例子。 > ```javascript > - &showell Steve > - Clark > - Brian > - Oren > - *showell > ``` 转为 JavaScript 代码如下。 > ```javascript > [ 'Steve', 'Clark', 'Brian', 'Oren', 'Steve' ] > ``` # 配置管理-TOML格式 ## 基本介绍 `Toml` 是一种易读、 `mini` 语言,由 `github` 前CEO `Tom` 创建。 `Tom's Obvious, Minimal Language`。 `TOML` 致力于配置文件的小型化和易读性。 1. WIKI介绍: [https://github.com/toml-lang/toml/wiki](https://github.com/toml-lang/toml/wiki) 2. 官方地址: [https://github.com/toml-lang/toml](https://github.com/toml-lang/toml) 3. 汉化版: [https://github.com/LongTengDao/TOML/blob/%E9%BE%99%E8%85%BE%E9%81%93-%E8%AF%91/toml-v1.0.0.md](https://github.com/LongTengDao/TOML/blob/%E9%BE%99%E8%85%BE%E9%81%93-%E8%AF%91/toml-v1.0.0.md) ## 与其他格式比较 `TOML` 与用于应用程序配置和数据序列化的其他文件格式(如 `YAML` 和 `JSON`)具有相同的特性。 `TOML` 和 `JSON` 都很简单,并且使用普遍存在的数据类型,这使得它们易于编写代码或使用机器进行解析。 `TOML` 和 `YAML` 都强调人的可读性,比如注释,它使理解给定行的目的变得更容易。 `TOML` 的不同之处在于,它支持注释(不像 `JSON`),但保持了简单性(不像 `YAML`)。 由于 `TOML` 被显式地设计为一种配置文件格式,所以解析它很容易,但并不打算序列化任意的数据结构。 `TOML` 的文件顶层是一个哈希表,它很容易在键中嵌套数据,但是它不允许顶级数组或浮点数,所以它不能直接序列化一些数据。也没有标准来标识 `TOML` 文件的开始或结束,这会使通过流发送文件变得复杂。这些细节必须在应用层进行协商。 `INI` 文件经常与 `TOML` 进行比较,因为它们在语法和用作配置文件方面具有相似性。然而, `INI` 没有标准化的格式,它们不能优雅地处理超过一两个层次的嵌套。 ## 基础语法 ```toml title = "TOML 例子" [owner] name = "Tom Preston-Werner" organization = "GitHub" bio = "GitHub Cofounder & CEO\nLikes tater tots and beer." dob = 1979-05-27T07:32:00Z # 日期时间是一等公民。为什么不呢? [database] server = "192.168.1.1" ports = [ 8001, 8001, 8002 ] connection_max = 5000 enabled = true [servers] # 你可以依照你的意愿缩进。使用空格或Tab。TOML不会在意。 [servers.alpha] ip = "10.0.0.1" dc = "eqdc10" [servers.beta] ip = "10.0.0.2" dc = "eqdc10" [clients] data = [ ["gamma", "delta"], [1, 2] ] #在数组里换行没有关系。 hosts = [ "alpha", "omega" ] ``` 特点: - 大小写敏感,必须是 `UTF-8` 编码 - 注释: `#` - 空白符: `tab(0x09)` 或 `space(0x20)` - 换行符: `LF(0x0A)` 或 `CRLF(0x0D 0x0A)` - 键值对:同一行,无值的键不可用,每行只能保存一个键值对 `TOML` 主要结构是键值对,与 `JSON` 类似。值必须是如下类型: `String`, `Integer`, `Float`, `Boolean`, `Datetime`, `Array`, `Table` ## 注释 使用 `#` 表示注释: ```toml # I am a comment. Hear me roar. Roar. key = "value" # Yeah, you can do this. ``` ## 字符串 `TOML` 中有4种字符串表示方法:基本、多行-基本、字面量、多行-字面量 ### 1\. 基本字符串 由双引号包裹,所有 `Unicode` 字符均可出现,除了双引号、反斜线、控制字符( `U+0000` to `U+001F`)需要转义。 ### 2\. 多行-基本字符串 由三个双引号包裹,除了分隔符开始的换行外,字符串内的换行将被保留: ```toml str1 = """ Roses are red Violets are blue""" ``` ### 3\. 字面量字符串 由单引号包裹,其内不允许转义,因此可以方便的表示基本字符串中需要转义的内容: ```toml winpath = 'C:\Users\nodejs\templates' ``` ### 4\. 多行-字面量字符串 与多行-基本字符串相似: ```toml str1 = ''' Roses are red Violets are blue''' ``` ## 数值与BOOL值 ```toml int1 = +99 flt3 = -0.01 bool1 = true ``` ## 日期时间 ```toml date = 1979-05-27T07:32:00Z ``` ## 数组 数组使用方括号包裹。空格会被忽略。元素使用逗号分隔。 注意,同一个数组下不允许混用数据类型。 ```toml array1 = [ 1, 2, 3 ] array2 = [ "red", "yellow", "green" ] array3 = [ [ 1, 2 ], [3, 4, 5] ] array4 = [ [ 1, 2 ], ["a", "b", "c"] ] # 这是可以的。 array5 = [ 1, 2.0 ] # 注意:这是不行的。 ``` ## 表格 表格(也叫哈希表或字典)是键值对的集合。它们在方括号内,自成一行。注意和数组相区分,数组只有值。 ```toml [table] ``` 在此之下,直到下一个 `table` 或 `EOF` 之前,是这个表格的键值对。键在左,值在右,等号在中间。键以非空字符开始,以等号前的非空字符为结尾。键值对是无序的。 ```toml [table] key = "value" ``` 可以随意缩进,使用 `Tab` 或 `空格`。为什么要缩进呢?因可以嵌套表格。 嵌套表格的表格名称中使用 `.` 符号。可以任意命名的表格,只是不要用点,点是保留的。 ```toml [dog.tater] type = "pug" ``` 以上等价于如下的 `JSON` 结构: ``` { "dog": { "tater": { "type": "pug" } } } ``` 如果不想的话,不用声明所有的父表。TOML 知道该如何处理。 ```toml # [x] 你 # [x.y] 不需要 # [x.y.z] 这些 [x.y.z.w] # 可以直接写 ``` 空表是允许的,其中没有键值对。 只要父表没有被直接定义,而且没有定义一个特定的键,可以继续写入: ```toml [a.b] c = 1 [a] d = 2 ``` 然而不能多次定义键和表格。这么做是不合法的。 ```toml # 别这么干! [a] b = 1 [a] c = 2 # 也别这个干 [a] b = 1 [a.b] c = 2 ``` ## 表格数组 最后要介绍的类型是表格数组。表格数组可以通过包裹在双方括号内的表格名来表达。使用相同的双方括号名称的表格是同一个数组的元素。表格按照书写的顺序插入。双方括号表格如果没有键值对,会被当成空表。 ```toml [[products]] name = "Hammer" sku = 738594937 [[products]] [[products]] name = "Nail" sku = 284758393 color = "gray" ``` 等价于以下的 `JSON` 结构: ``` { "products": [ { "name": "Hammer", "sku": 738594937 }, { }, { "name": "Nail", "sku": 284758393, "color": "gray" } ] } ``` 表格数组同样可以嵌套。只需在子表格上使用相同的双方括号语法。每一个双方括号子表格从属于最近定义的上层表格元素。 ```toml [[fruit]] name = "apple" [fruit.physical] color = "red" shape = "round" [[fruit.variety]] name = "red delicious" [[fruit.variety]] name = "granny smith" [[fruit]] name = "banana" [[fruit.variety]] name = "plantain" ``` 等价于如下的 `JSON` 结构: ``` { "fruit": [ { "name": "apple", "physical": { "color": "red", "shape": "round" }, "variety": [ { "name": "red delicious" }, { "name": "granny smith" } ] }, { "name": "banana", "variety": [ { "name": "plantain" } ] } ] } ``` 尝试定义一个普通的表格,使用已经定义的数组的名称,将抛出一个解析错误: ```toml # 不合法的 TOML [[fruit]] name = "apple" [[fruit.variety]] name = "red delicious" # 和上面冲突了 [fruit.variety] name = "granny smith" ``` # 配置管理-配置监听回调 ## 基本介绍 从`v2.10.0`版本开始,`gcfg`组件新增`WatcherAdapter`接口,`WatcherAdapter`是`gcfg`包中的一个接口,用于实现配置文件变化监听功能。当配置文件被修改、创建、删除或权限发生变化时,注册的观察者会收到通知。 `GoFrame`的配置监听功能为应用程序提供了动态响应配置变化的能力,使系统能够实时调整行为而无需重启。通过灵活的监听器机制和丰富的`context`信息,开发者可以轻松构建自适应的应用程序。 ## 接口定义 [https://github.com/gogf/gf/blob/master/os/gcfg/gcfg\_adaper.go](https://github.com/gogf/gf/blob/master/os/gcfg/gcfg_adaper.go) ```go // WatcherAdapter is the interface for configuration watcher. type WatcherAdapter interface { // AddWatcher adds a watcher function for specified `pattern` and `resource`. AddWatcher(name string, fn func(ctx context.Context)) // RemoveWatcher removes the watcher function for specified `pattern` and `resource`. RemoveWatcher(name string) // GetWatcherNames returns all watcher names. GetWatcherNames() []string } ``` ### 适配器适配情况: | 适配器名称 | 是否支持监听回调 | |------------------|----------| | `AdapterFile` | 支持 | | `AdapterContent` | 支持 | | `polaris` | 支持 | | `nacos` | 支持 | | `apollo` | 支持 | | `consul` | 支持 | | `kubecm` | 支持 | ### 核心概念 1. `WatcherAdapter` 接口:定义了添加、移除和获取观察者的方法 2. `WatcherRegistry`:观察者注册管理器,提供了统一的观察者管理实现 3. `context`上下文传递:通过 `context` 传递配置变更的详细信息 ### 主要功能 1. `文件监控`:自动监控配置文件的变化(写入、创建、删除、重命名、权限更改) 2. `手动触发`:支持手动触发观察者回调(如 `Set` 操作) 3. `并发安全`:所有操作都是并发安全的 4. `panic 防护`:每个观察者的执行都在独立的 `goroutine` 中,并有 `panic` 恢复机制 ## 使用方法 让我通过示例代码展示如何使用 `WatcherAdapter` 功能: ### 1. 文件适配器的观察者使用 ```go // 创建配置适配器 c, err := gcfg.NewAdapterFile("config.toml") if err != nil { panic(err) } // 添加观察者 c.AddWatcher("my-watcher", func(ctx context.Context) { // 获取文件操作上下文信息 fileCtx := gcfg.GetAdapterFileCtx(ctx) fmt.Printf("操作类型: %s\n", fileCtx.GetOperation()) fmt.Printf("文件名: %s\n", fileCtx.GetFileName()) fmt.Printf("文件路径: %s\n", fileCtx.GetFilePath()) // 根据不同操作类型处理 switch fileCtx.GetOperation() { case gcfg.OperationWrite: fmt.Println("配置文件被修改了") case gcfg.OperationSet: fmt.Printf("配置项 %s 被设置为 %v\n", fileCtx.GetKey(), fileCtx.GetValue()) } }) // 修改配置会触发观察者 c.Set("key", "new-value") ``` ### 2. 内容适配器的观察者使用 ```go // 创建内容适配器 content := `{"key": "value"}` adapter, err := gcfg.NewAdapterContent(content) if err != nil { panic(err) } // 添加观察者 adapter.AddWatcher("content-watcher", func(ctx context.Context) { contentCtx := gcfg.GetAdapterContentCtx(ctx) fmt.Printf("内容操作: %s\n", contentCtx.GetOperation()) }) // 设置新内容会触发观察者 adapter.SetContent(`{"key": "new-value"}`) ``` ### 3. 与 gcfg.Config 实例一起使用 ```go // 创建配置实例 config := g.Cfg() // 添加观察者到配置实例的适配器 if adapter, ok := config.GetAdapter().(gcfg.WatcherAdapter); ok { adapter.AddWatcher("config-watcher", func(ctx context.Context) { fmt.Println("配置发生了变化") }) } ``` ## 观察者上下文信息 根据不同适配器类型,可以通过相应的上下文获取不同的信息,具体可以参考不同类型适配器的`xxxAdapterCtx`实现。 例如`nacos`可以通过`adapterCtx := nacos.GetAdapterCtx(ctx)`获取`*NacosAdapterCtx` 位于: `contrib/config/nacos/nacos_adapter_ctx.go` 上下文键常量: - `ContextKeyNamespace`: 命名空间 - `ContextKeyGroup`: 组 - `ContextKeyDataId`: 数据ID - `gcfg.ContextKeyOperation`: 操作类型(继承自gcfg) - `gcfg.ContextKeyContent`: 设置的内容(继承自gcfg) 方法: - `WithOperation()`: 设置操作类型 - `WithNamespace()`: 设置命名空间 - `WithGroup()`: 设置组 - `WithDataId()`: 设置数据ID - `WithContent()`: 设置内容 - `GetNamespace()`: 获取命名空间 - `GetGroup()`: 获取组 - `GetDataId()`: 获取数据ID - `GetContent()`: 获取内容 - `GetOperation()`: 获取操作类型 ## 实际应用场景 1. `动态配置更新`:当配置文件被修改时,应用程序可以自动重新加载配置 2. `日志记录`:记录配置变更历史 3. `通知机制`:配置变更时通知其他组件 4. `缓存清理`:配置变更时清理相关缓存 # 配置管理-Loader加载器 `Loader` 是 `gcfg` 组件提供的通用配置管理器,从 `v2.10.0` 版本开始提供。它提供了类似于 `Spring Boot`的 `@ConfigurationProperties` 的配置加载、监控、更新和管理功能。 ## 功能特性 - **泛型支持**:使用`Go`泛型,提供类型安全的配置绑定 - **配置加载**:从配置源加载数据并自动绑定到结构体 - **配置监控**:自动监控配置变化并实时更新 - **自定义转换器**:支持自定义数据转换函数 - **回调处理**:支持配置变更时的回调函数 - **错误处理**:提供灵活的错误处理机制 ## 基本使用 ### 方式一:使用全局配置对象 ```go package main import ( "fmt" "github.com/gogf/gf/v2/frame/g" "github.com/gogf/gf/v2/os/gcfg" "github.com/gogf/gf/v2/os/gctx" ) type AppConfig struct { Name string `json:"name"` Version string `json:"version"` Server ServerConfig `json:"server"` Database DBConfig `json:"database"` } type ServerConfig struct { Host string `json:"host"` Port int `json:"port"` } type DBConfig struct { Host string `json:"host"` Port int `json:"port"` Username string `json:"username"` Password string `json:"password"` } func main() { ctx := gctx.New() // 创建 Loader 实例,使用全局配置 loader := gcfg.NewLoader[AppConfig](g.Cfg(), "") // 加载并监听配置 loader.MustLoadAndWatch(ctx, "app-config-watcher") // 获取配置 config := loader.Get() fmt.Printf("应用名称: %s\n", config.Name) fmt.Printf("服务地址: %s:%d\n", config.Server.Host, config.Server.Port) } ``` ### 方式二:使用独立的配置适配器 ```go package main import ( "fmt" "github.com/gogf/gf/v2/os/gcfg" "github.com/gogf/gf/v2/os/gctx" ) type AppConfig struct { Name string `json:"name"` Version string `json:"version"` Server ServerConfig `json:"server"` } type ServerConfig struct { Host string `json:"host"` Port int `json:"port"` } func main() { ctx := gctx.New() // 创建独立的配置文件适配器 adapter, err := gcfg.NewAdapterFile("config.yaml") if err != nil { panic(err) } // 使用适配器创建 Loader loader := gcfg.NewLoaderWithAdapter[AppConfig](adapter, "") // 加载并监听配置 loader.MustLoadAndWatch(ctx, "app-watcher") // 获取配置 config := loader.Get() fmt.Printf("应用: %s v%s\n", config.Name, config.Version) } ``` ## 配置文件示例 以下是配置文件 `config.yaml` 的示例: ```yaml name: "MyApp" version: "1.0.0" server: host: "127.0.0.1" port: 8080 database: host: "localhost" port: 3306 username: "root" password: "123456" ``` ## 高级特性 ### 监控特定配置键 如果只想监控配置文件中的特定部分,可以指定 `propertyKey` 参数: ```go // 只监控并加载 server 配置部分 loader := gcfg.NewLoader[ServerConfig](g.Cfg(), "server") loader.MustLoadAndWatch(ctx, "server-watcher") config := loader.Get() fmt.Printf("服务器配置: %s:%d\n", config.Host, config.Port) ``` 对应的配置文件: ```yaml server: host: "127.0.0.1" port: 8080 database: host: "localhost" port: 3306 ``` ### 配置变更回调 当配置文件发生变化时,可以通过回调函数执行自定义逻辑: ```go loader := gcfg.NewLoader[AppConfig](g.Cfg(), "") // 设置配置变更回调 loader.OnChange(func(updated AppConfig) error { fmt.Printf("配置已更新,新的应用名称: %s\n", updated.Name) // 在这里可以执行其他逻辑,例如: // - 重新连接数据库 // - 更新缓存 // - 通知其他服务 return nil }) // 加载并监听 loader.MustLoadAndWatch(ctx, "my-watcher") ``` ### 自定义转换器 如果需要对配置数据进行自定义处理,可以设置转换器: ```go loader := gcfg.NewLoader[AppConfig](g.Cfg(), "") // 设置自定义转换器 loader.SetConverter(func(data any, target *AppConfig) error { // 自定义转换逻辑 // 例如:解密加密的配置项、格式化特殊字段等 // 使用默认转换 if err := gconv.Scan(data, target); err != nil { return err } // 额外的处理 if target.Server.Port == 0 { target.Server.Port = 8080 // 设置默认端口 } return nil }) loader.MustLoadAndWatch(ctx, "custom-converter-watcher") ``` ### 错误处理 在监控过程中,如果配置加载失败,可以通过错误处理器来处理: ```go loader := gcfg.NewLoader[AppConfig](g.Cfg(), "") // 设置错误处理器 loader.SetWatchErrorHandler(func(ctx context.Context, err error) { g.Log().Errorf(ctx, "配置加载失败: %v", err) // 可以发送告警通知等 }) loader.MustLoadAndWatch(ctx, "error-handler-watcher") ``` ### 使用默认值 可以在创建`Loader`时提供一个带默认值的结构体: ```go // 创建带默认值的配置结构体 defaultConfig := &AppConfig{ Name: "DefaultApp", Version: "0.0.1", Server: ServerConfig{ Host: "0.0.0.0", Port: 8080, }, } // 使用默认配置创建 Loader // SetReuseTargetStruct(true):在默认值基础上覆盖,而不是从零值重建 // 注意:yaml 中请勿写空节点(如无内容的 "appConfig:\n server:"),空节点会使默认值被清零 loader := gcfg.NewLoader(g.Cfg(), "appConfig", defaultConfig).SetReuseTargetStruct(true) loader.MustLoad(ctx) // 如果配置文件中没有某些字段,将使用默认值 config := loader.Get() ``` > 注意:`SetReuseTargetStruct` 在 v2.10.3 中默认为`false` (源码注释为避免数据竞争),必须显式设置为`true` 默认值才会生效。 ## API 参考 ### NewLoader 创建一个新的`Loader`实例。 ```go func NewLoader[T any](config *Config, propertyKey string, targetStruct ...*T) *Loader[T] ``` **参数:** - `config`: 配置实例 - `propertyKey`: 监控的配置键(使用 `""` 或 `"."` 监控所有配置) - `targetStruct`: 可选的目标结构体指针(用于设置默认值) ### NewLoaderWithAdapter 使用适配器创建`Loader`实例。 ```go func NewLoaderWithAdapter[T any](adapter Adapter, propertyKey string, targetStruct ...*T) *Loader[T] ``` ### Load 从配置源加载数据并绑定到结构体。 ```go func (l *Loader[T]) Load(ctx context.Context) error ``` ### MustLoad 与 `Load` 类似,但出错时会 panic。 ```go func (l *Loader[T]) MustLoad(ctx context.Context) ``` ### Watch 开始监控配置变化并自动更新结构体。 ```go func (l *Loader[T]) Watch(ctx context.Context, name string) error ``` ### MustWatch 与 `Watch` 类似,但出错时会 panic。 ```go func (l *Loader[T]) MustWatch(ctx context.Context, name string) ``` ### MustLoadAndWatch 便捷方法,同时执行 `MustLoad` 和 `MustWatch`。 ```go func (l *Loader[T]) MustLoadAndWatch(ctx context.Context, name string) ``` ### Get 返回当前的配置结构体(值副本)。 ```go func (l *Loader[T]) Get() T ``` ### GetPointer 返回指向当前配置结构体的指针。 ```go func (l *Loader[T]) GetPointer() *T ``` ### OnChange 设置配置变化时的回调函数。 ```go func (l *Loader[T]) OnChange(fn func(updated T) error) ``` ### SetConverter 设置自定义转换函数。 ```go func (l *Loader[T]) SetConverter(converter func(data any, target *T) error) ``` ### SetWatchErrorHandler 设置监控过程中的错误处理器。 ```go func (l *Loader[T]) SetWatchErrorHandler(errorFunc func(ctx context.Context, err error)) ``` ### SetReuseTargetStruct 设置是否重用目标结构体(默认为 false)。 ```go func (l *Loader[T]) SetReuseTargetStruct(reuse bool) ``` ### StopWatch 停止监控配置变化。 ```go func (l *Loader[T]) StopWatch(ctx context.Context) (bool, error) ``` ### IsWatching 返回是否正在监控配置变化。 ```go func (l *Loader[T]) IsWatching() bool ``` ## 完整示例 以下是一个完整的应用示例,展示如何使用`Loader`管理应用配置: ```go package main import ( "context" "fmt" "time" "github.com/gogf/gf/v2/frame/g" "github.com/gogf/gf/v2/os/gcfg" "github.com/gogf/gf/v2/os/gctx" ) // AppConfig 应用配置 type AppConfig struct { App AppInfo `json:"app"` Server ServerConfig `json:"server"` Database DBConfig `json:"database"` Redis RedisConfig `json:"redis"` } type AppInfo struct { Name string `json:"name"` Version string `json:"version"` Debug bool `json:"debug"` } type ServerConfig struct { Host string `json:"host"` Port int `json:"port"` } type DBConfig struct { Host string `json:"host"` Port int `json:"port"` Database string `json:"database"` Username string `json:"username"` Password string `json:"password"` } type RedisConfig struct { Host string `json:"host"` Port int `json:"port"` Database int `json:"database"` } func main() { ctx := gctx.New() // 创建 Loader loader := gcfg.NewLoader[AppConfig](g.Cfg(), "") // 设置配置变更回调 loader.OnChange(func(updated AppConfig) error { g.Log().Infof(ctx, "配置已更新: %s v%s", updated.App.Name, updated.App.Version) return nil }) // 设置错误处理器 loader.SetWatchErrorHandler(func(ctx context.Context, err error) { g.Log().Errorf(ctx, "配置加载失败: %v", err) }) // 加载并监听配置 loader.MustLoadAndWatch(ctx, "app-config") // 获取配置并使用 config := loader.Get() fmt.Printf("应用信息:\n") fmt.Printf(" 名称: %s\n", config.App.Name) fmt.Printf(" 版本: %s\n", config.App.Version) fmt.Printf(" 调试模式: %v\n", config.App.Debug) fmt.Printf("\n服务器配置:\n") fmt.Printf(" 地址: %s:%d\n", config.Server.Host, config.Server.Port) fmt.Printf("\n数据库配置:\n") fmt.Printf(" 地址: %s:%d\n", config.Database.Host, config.Database.Port) fmt.Printf(" 数据库: %s\n", config.Database.Database) // 保持程序运行以监控配置变化 select {} } ``` 对应的配置文件 `config.yaml`: ```yaml app: name: "MyApplication" version: "1.0.0" debug: true server: host: "0.0.0.0" port: 8080 database: host: "localhost" port: 3306 database: "mydb" username: "root" password: "123456" redis: host: "localhost" port: 6379 database: 0 ``` ## 使用建议 1. **类型安全**:使用`Loader`可以获得编译时类型检查,避免运行时类型转换错误。 2. **配置监听**:在生产环境中使用配置监听功能可以实现配置热更新,无需重启应用。 3. **错误处理**:始终设置错误处理器,确保配置加载失败时能够及时发现并处理。 4. **配置分离**:对于复杂的应用,建议将不同模块的配置分离到不同的键下,使用 `propertyKey` 参数单独加载。 5. **默认值**:为关键配置项设置合理的默认值,提高应用的健壮性。 6. **回调函数**:在配置变更回调中执行必要的资源更新操作,如重建连接池、清理缓存等。 # 配置管理-接口化设计 `gcfg`组件采用了接口化设计,以实现高扩展性。通过接口化设计的方式,使用者可以自定义对接的配置获取方式,例如`etcd`、`zookeeper`、`consul`、`kubernetes configmap`、`apollo`等等。 ## 接口定义 [https://github.com/gogf/gf/blob/master/os/gcfg/gcfg\_adaper.go](https://github.com/gogf/gf/blob/master/os/gcfg/gcfg_adaper.go) ```go // Adapter is the interface for configuration retrieving. type Adapter interface { // Available checks and returns the configuration service is available. // The optional parameter `resource` specifies certain configuration resource. // // It returns true if configuration file is present in default AdapterFile, or else false. // Note that this function does not return error as it just does simply check for backend configuration service. Available(ctx context.Context, resource ...string) (ok bool) // Get retrieves and returns value by specified `pattern`. Get(ctx context.Context, pattern string) (value interface{}, err error) // Data retrieves and returns all configuration data as map type. // Note that this function may lead lots of memory usage if configuration data is too large, // you can implement this function if necessary. Data(ctx context.Context) (data map[string]interface{}, err error) } ``` ## 设置接口实现 配置对象可以通过 `SetAdapter` 方法设置当前使用的接口实现。 ```go // SetAdapter sets the adapter of current Config object. func (c *Config) SetAdapter(adapter Adapter) ``` ## 获取接口实现 配置对象可以通过 `GetAdapter` 方法获取当前使用的接口实现。 ```go // GetAdapter returns the adapter of current Config object. func (c *Config) GetAdapter() Adapter ``` # 配置管理-AdapterFile ## `AdapterFile` `AdapterFile` 是框架默认的配置管理实现方式,基于文件的配置加载和读取。 ## 通过 `g.Cfg` 单例对象使用 大部分场景下,我们可以通过框架已经封装好的g.Cfg单例对象来便捷使用基于文件的配置管理实现。例如: `config.yaml` ```yaml server: address: ":8888" openapiPath: "/api.json" swaggerPath: "/swagger" dumpRouterMap: false database: default: link: "mysql:root:12345678@tcp(127.0.0.1:3306)/test" debug: true ``` `main.go` ```go package main import ( "fmt" "github.com/gogf/gf/v2/frame/g" "github.com/gogf/gf/v2/os/gctx" ) func main() { var ctx = gctx.New() fmt.Println(g.Cfg().MustGet(ctx, "server.address").String()) fmt.Println(g.Cfg().MustGet(ctx, "database.default").Map()) } ``` 运行后,终端输出: ```html :8888 map[debug:true link:mysql:root:12345678@tcp(127.0.0.1:3306)/test] ``` ## 通过 `gcfg.NewWithAdapter` 使用 我们也可以通过配置组件的 `NewWithAdapter` 方法来创建一个基于给定 `Adapter` 的配置管理对象,当然,在这里我们给一个 `AdapterFile` 接口对象。 `config.yaml` ```yaml server: address: ":8888" openapiPath: "/api.json" swaggerPath: "/swagger" dumpRouterMap: false database: default: link: "mysql:root:12345678@tcp(127.0.0.1:3306)/test" debug: true ``` `main.go` ```go package main import ( "fmt" "github.com/gogf/gf/v2/os/gcfg" "github.com/gogf/gf/v2/os/gctx" ) func main() { var ctx = gctx.New() adapter, err := gcfg.NewAdapterFile("config") if err != nil { panic(err) } config := gcfg.NewWithAdapter(adapter) fmt.Println(config.MustGet(ctx, "server.address").String()) fmt.Println(config.MustGet(ctx, "database.default").Map()) } ``` 运行后,终端输出: ```html :8888 map[debug:true link:mysql:root:12345678@tcp(127.0.0.1:3306)/test] ``` # 配置管理-AdapterContent ## `AdapterContent` `AdapterContent` 是基于配置内容的实现,用户可以给定具体的配置内容,生成 `Adapter` 接口对象。配置内容支持多种格式,格式列表同配置管理组件。 ## 使用示例 大部分场景下,我们可以通过框架已经封装好的g.Cfg单例对象来便捷使用基于文件的配置管理实现。例如: ```go package main import ( "fmt" "github.com/gogf/gf/v2/os/gcfg" "github.com/gogf/gf/v2/os/gctx" ) const content = ` server: address: ":8888" openapiPath: "/api.json" swaggerPath: "/swagger" dumpRouterMap: false database: default: link: "mysql:root:12345678@tcp(127.0.0.1:3306)/test" debug: true ` func main() { var ctx = gctx.New() adapter, err := gcfg.NewAdapterContent(content) if err != nil { panic(err) } config := gcfg.NewWithAdapter(adapter) fmt.Println(config.MustGet(ctx, "server.address").String()) fmt.Println(config.MustGet(ctx, "database.default").Map()) } ``` 运行后,终端输出: ```html :8888 map[debug:true link:mysql:root:12345678@tcp(127.0.0.1:3306)/test] ``` # 日志组件 `glog` 是通用的高性能日志管理模块,实现了强大易用的日志管理功能,是 `GoFrame` 开发框架的核心组件之一。 ## 基本介绍 **使用方式**: ```go import "github.com/gogf/gf/v2/os/glog" ``` **接口文档**: [https://pkg.go.dev/github.com/gogf/gf/v2/os/glog](https://pkg.go.dev/github.com/gogf/gf/v2/os/glog) **简要说明:** 1. `glog` 模块固定日志文件名称格式为 `*.log`,即固定使用 `.log` 作为日志文件名后缀。 2. `glog` 支持文件输出、日志级别、日志分类、调试管理、调用跟踪、链式操作、滚动切分等等丰富特性。 3. 可以使用 `glog.New` 方法创建 `glog.Logger` 对象用于自定义日志打印,也可以并推荐使用 `glog` 默认提供的包方法来打印日志。 4. 当使用包方法修改模块配置时,注意任何的 `glog.Set*` 设置方法都将会**全局生效**。 5. 日志内容默认时间格式为 `时间 [级别] 内容 换行`,其中 `时间` 精确到毫秒级别,`级别` 为可选输出,`内容` 为调用端的参数输入,`换行` 为可选输出(部分方法自动为日志内容添加换行符号),日志内容示例:`2018-10-10 12:00:01.568 [ERRO] 产生错误`。 6. `Print*`/`Debug*`/`Info*` 方法输出日志内容到标准输出(`stdout`),为防止日志的错乱,`Notice*`/`Warning*`/`Error*`/`Critical*`/`Panic*`/`Fatal*` 方法也是将日志内容输出到标准输出(`stdout`)。 7. `Panic*` 方法在输出日志信息后会引发 `panic` 错误方法。 8. `Fatal*` 方法在输出日志信息之后会停止进程运行,并返回进程状态码值为 `1`(正常程序退出状态码为 `0`)。 ## 组件特性 `glog` 组件具有以下显著特性: - 使用简便,功能强大 - 支持配置管理,使用统一的配置组件 - 支持日志级别 - 支持颜色打印 - 支持链式操作 - 支持指定输出日志文件或目录 - 支持链路跟踪 - 支持异步输出 - 支持堆栈打印 - 支持调试信息开关 - 支持自定义 `Writer` 输出接口 - 支持自定义日志 `Handler` 处理 - 支持自定义日志 `CtxKeys` 键值 - 支持 `JSON` 格式打印 - 支持 `Flags` 特性 - 支持 `Rotate` 滚动切分特性 ## 单例对象 日志组件支持单例模式,使用 `g.Log(单例名称)` 获取不同的单例日志管理对象。提供单例对象的目的在于针对不同业务场景可以使用不同配置的日志管理对象。我们推荐使用 `g.Log()` 方法获取单例对象来进行日志操作,该方法内部会自动读取配置文件并初始化单例对象,该初始化操作仅会执行一次。 ## `glog.Print` 和 `g.Log().Print` 区别 - `glog` 是日志组件的包名, `g.Log()` 是一个日志组件单例对象。 - `g.Log()` 单例对象通过对象管理组件 `g` 包来维护,对象初始化时会自动读取日志配置,使用简便,大多数场景下推荐使用这种方式使用日志组件。 - `glog` 通过独立组件的形式使用,默认情况下会直接输出日志到终端。也可以通过配置管理方法设置全局配置,或者通过 `New` 创建独立的日志对象使用。 **提示** **网友**:为什么会有两种日志打印方式?我应该用哪种方式呢? **回答**: 由于框架的每个组件都是 **解耦化设计** 的,框架是 **可以作为独立的工具库使用** 的。比如项目只需要使用日志组件,那么可以直接使用 `glog` 包的方法即可,不会引入其他的组件。但在项目工程化使用中,工具库的使用方式会较为繁琐,往往会涉及到配置和组件初始化(大部分场景会引起二次封装),因此框架也提供了一个 **耦合包** 叫做 `g` 包,这个包下面默认加载了一些常用的组件。 `g.Log()` 就是其中的日志组件,它会自动按照工程规范读取并初始化配置对象,快速初始化日志对象并实现单例管理,极大简化工程化下的日志使用。更多的介绍请参考: [对象管理](./g.md) # 日志组件-配置管理 日志组件是`GoFrame`框架核心的组件之一,支持非常方便的配置管理能力。 ## 配置文件(推荐) **提示** 日志的配置使用的是框架统一的配置组件,支持多种文件格式,也支持配置中心、接口化扩展等特性,更多细节请参考章节: [配置管理](./gcfg.md) 日志组件支持配置文件,当使用 `g.Log(单例名称)` 获取 `Logger` 单例对象时,将会自动通过默认的配置管理对象获取对应的 `Logger` 配置。默认情况下会读取 `logger.单例名称` 配置项,当该配置项不存在时,将会读取默认的 `logger` 配置项。配置项请参考配置对象结构定义: [https://pkg.go.dev/github.com/gogf/gf/v2/os/glog#Config](https://pkg.go.dev/github.com/gogf/gf/v2/os/glog#Config) 完整配置文件配置项及说明如下,其中配置项名称不区分大小写: ```yaml logger: path: "/var/log/" # 日志文件路径。默认为空,表示关闭,仅输出到终端 file: "{Y-m-d}.log" # 日志文件格式。默认为"{Y-m-d}.log" prefix: "" # 日志内容输出前缀。默认为空 level: "all" # 日志输出级别 timeFormat: "2006-01-02T15:04:05" # 自定义日志输出的时间格式,使用Golang标准的时间格式配置 ctxKeys: [] # 自定义Context上下文变量名称,自动打印Context的变量到日志中。默认为空 header: true # 是否打印日志的头信息。默认true stdout: true # 日志是否同时输出到终端。默认true rotateSize: 0 # 按照日志文件大小对文件进行滚动切分。默认为0,表示关闭滚动切分特性 rotateExpire: 0 # 按照日志文件时间间隔对文件滚动切分。默认为0,表示关闭滚动切分特性 rotateBackupLimit: 0 # 按照切分的文件数量清理切分文件,当滚动切分特性开启时有效。默认为0,表示不备份,切分则删除 rotateBackupExpire: 0 # 按照切分的文件有效期清理切分文件,当滚动切分特性开启时有效。默认为0,表示不备份,切分则删除 rotateBackupCompress: 0 # 滚动切分文件的压缩比(0-9)。默认为0,表示不压缩 rotateCheckInterval: "1h" # 滚动切分的时间检测间隔,一般不需要设置。默认为1小时 stdoutColorDisabled: false # 关闭终端的颜色打印。默认开启 writerColorEnable: false # 日志文件是否带上颜色。默认false,表示不带颜色 ``` 其中,`level`配置项使用字符串配置,按照日志级别支持以下配置:`DEBU` < `INFO` < `NOTI` < `WARN` < `ERRO` < `CRIT`,也支持`ALL`、`DEV`、`PROD`常见部署模式配置名称。`level`配置项字符串不区分大小写。关于日志级别的详细介绍请查看[日志组件-日志级别](./glog-level.md)章节。 ### 示例1,默认配置项 ```yaml logger: path: "/var/log" level: "all" stdout: false ``` 随后可以使用`g.Log()`获取默认的单例对象时自动获取并设置该配置。 ### 示例2,多个配置项 多个`Logger`的配置示例: ```yaml logger: path: "/var/log" level: "all" stdout: false logger1: path: "/var/log/logger1" level: "dev" stdout: false logger2: path: "/var/log/logger2" level: "prod" stdout: true ``` 我们可以通过单例对象名称获取对应配置的`Logger`单例对象: ```go // 对应 logger.logger1 配置项 l1 := g.Log("logger1") // 对应 logger.logger2 配置项 l2 := g.Log("logger2") // 对应默认配置项 logger l3 := g.Log("none") // 对应默认配置项 logger l4 := g.Log() ``` ## 配置方法(高级) 配置方法用于模块化使用`glog`时由开发者自己进行配置管理。 方法列表: 简要说明: 1. 可以通过`SetConfig`及`SetConfigWithMap`来设置。 2. 也可以使用`Logger`对象的`Set*`方法进行特定配置的设置。 3. 主要注意的是,配置项在`Logger`对象执行日志输出之前设置,避免并发安全问题。 我们可以使用`SetConfigWithMap`方法通过`Key-Value`键值对来设置/修改`Logger`的特定配置,其余的配置使用默认配置即可。其中`Key`的名称即是`Config`这个`struct`中的属性名称,并且不区分大小写,单词间也支持使用`-`/`_`/`空格`符号连接,具体可参考[类型转换-Struct转换](./gconv-struct.md)章节的转换规则。 简单示例: ```go logger := glog.New() logger.SetConfigWithMap(g.Map{ "path": "/var/log", "level": "all", "stdout": false, "StStatus": 0, }) logger.Print("test") ``` 其中`StStatus`表示是否开启堆栈打印,设置为`0`表示关闭。键名也可以使用`stStatus`、`st-status`、`st_status`、`St Status`,其他配置属性以此类推。 ## 注意事项 常见问题:如日志组件的配置为何没有对`HTTP Server`、`GRPC Server`、`ORM`组件打印的日志生效。 `GoFrame`框架采用了模块化设计,日志组件是框架独立的组件,本章节介绍的配置均只对独立使用日志组件的方式生效,例如使用`g.Log()`或者`glog.New()`方法创建的日志组件。其他组件的日志配置有专门的配置项,或者日志对象设置方法来实现日志的配置,请具体查看对应的组件文档和`API`。 # 日志组件-日志级别 ## 日志级别 日志级别用于管理日志的输出,我们可以通过设定特定的日志级别来关闭/开启特定的日志内容。日志级别的设置可以通过三个方法实现: ```go func (l *Logger) SetLevel(level int) func (l *Logger) SetLevelStr(levelStr string) error func (l *Logger) SetLevelPrint(enabled bool) ``` ### `SetLevel`方法 通过`SetLevel`方法可以设置日志级别,`glog`模块支持以下几种日志级别常量设定: ```html LEVEL_ALL LEVEL_DEV LEVEL_PROD LEVEL_DEBU LEVEL_INFO LEVEL_NOTI LEVEL_WARN LEVEL_ERRO ``` 我们可以通过`位操作`组合使用这几种级别,例如其中`LEVEL_ALL`等价于`LEVEL_DEBU | LEVEL_INFO | LEVEL_NOTI | LEVEL_WARN | LEVEL_ERRO | LEVEL_CRIT`。我们还可以通过`LEVEL_ALL & ^LEVEL_DEBU & ^LEVEL_INFO & ^LEVEL_NOTI`来过滤掉`LEVEL_DEBU`/`LEVEL_INFO`/`LEVEL_NOTI`日志内容。 **注意** 当然日志模块还有其他的一些级别,如 `CRIT/PANI/FATA`,但是这几个级别是非常严重的错误,无法在日志级别中由开发者自定义屏蔽。例如产生严重错误的时候, `PANI/FATA` 错误界别将会产生一些额外的系统动作: `panic`/ `exit`。 使用示例: ```go package main import ( "context" "github.com/gogf/gf/v2/os/glog" ) func main() { ctx := context.TODO() l := glog.New() l.Info(ctx, "info1") l.SetLevel(glog.LEVEL_ALL ^ glog.LEVEL_INFO) l.Info(ctx, "info2") } ``` 执行后,输出结果为: ```html 2021-12-31 11:16:57.272 [INFO] info1 ``` ### `SetLevelStr`方法 大部分场景下我们可以通过`SetLevelStr`方法来通过字符串设置日志级别,配置文件中的`level`配置项也是通过字符串来配置日志级别。支持的日志级别字符串如下,不区分大小写: ```html "ALL": LEVEL_DEBU | LEVEL_INFO | LEVEL_NOTI | LEVEL_WARN | LEVEL_ERRO | LEVEL_CRIT, "DEV": LEVEL_DEBU | LEVEL_INFO | LEVEL_NOTI | LEVEL_WARN | LEVEL_ERRO | LEVEL_CRIT, "DEVELOP": LEVEL_DEBU | LEVEL_INFO | LEVEL_NOTI | LEVEL_WARN | LEVEL_ERRO | LEVEL_CRIT, "PROD": LEVEL_WARN | LEVEL_ERRO | LEVEL_CRIT, "PRODUCT": LEVEL_WARN | LEVEL_ERRO | LEVEL_CRIT, "DEBU": LEVEL_DEBU | LEVEL_INFO | LEVEL_NOTI | LEVEL_WARN | LEVEL_ERRO | LEVEL_CRIT, "DEBUG": LEVEL_DEBU | LEVEL_INFO | LEVEL_NOTI | LEVEL_WARN | LEVEL_ERRO | LEVEL_CRIT, "INFO": LEVEL_INFO | LEVEL_NOTI | LEVEL_WARN | LEVEL_ERRO | LEVEL_CRIT, "NOTI": LEVEL_NOTI | LEVEL_WARN | LEVEL_ERRO | LEVEL_CRIT, "NOTICE": LEVEL_NOTI | LEVEL_WARN | LEVEL_ERRO | LEVEL_CRIT, "WARN": LEVEL_WARN | LEVEL_ERRO | LEVEL_CRIT, "WARNING": LEVEL_WARN | LEVEL_ERRO | LEVEL_CRIT, "ERRO": LEVEL_ERRO | LEVEL_CRIT, "ERROR": LEVEL_ERRO | LEVEL_CRIT, "CRIT": LEVEL_CRIT, "CRITICAL": LEVEL_CRIT, ``` 可以看到,通过级别名称设置的日志级别是按照日志级别的高低来进行过滤的:`DEBU < INFO < NOTI < WARN < ERRO < CRIT`,也支持`ALL`、`DEV`、`PROD`常见部署模式配置名称。 使用示例: ```go package main import ( "context" "github.com/gogf/gf/v2/os/glog" ) func main() { ctx := context.TODO() l := glog.New() l.Info(ctx, "info1") l.SetLevelStr("notice") l.Info(ctx, "info2") } ``` 执行后,输出结果为: ```html 2021-12-31 11:20:15.019 [INFO] info1 ``` ### `SetLevelPrint`方法 控制默认日志输出是否打印日志级别标识,默认会打印日志级别标识。 使用示例: ```go package main import ( "context" "github.com/gogf/gf/v2/os/glog" ) func main() { ctx := context.TODO() l := glog.New() l.Info(ctx, "info1") l.SetLevelPrint(false) l.Info(ctx, "info2") } ``` 执行后,输出结果为: ```html 2023-03-14 10:28:18.598 [INFO] info1 2023-03-14 10:28:18.631 info1 ``` ## 级别名称 在日志中我们会看到不同级别的打印内容,会在内容前面带有不同的日志级别名称。默认的日志级别名称如下: ```html LEVEL_DEBU: "DEBU", LEVEL_INFO: "INFO", LEVEL_NOTI: "NOTI", LEVEL_WARN: "WARN", LEVEL_ERRO: "ERRO", LEVEL_CRIT: "CRIT", LEVEL_PANI: "PANI", LEVEL_FATA: "FATA", ``` 为方便统一日志格式,保证比较优雅的排版风格,因此日志级别的名称都使用了级别英文单词的前四个字符。若有特殊需求需要修改日志级别名称的,可以通过以下方法进行设置: ```go func (l *Logger) SetLevelPrefix(level int, prefix string) func (l *Logger) SetLevelPrefixes(prefixes map[int]string) ``` 使用示例: ```go package main import ( "context" "github.com/gogf/gf/v2/os/glog" ) func main() { ctx := context.TODO() l := glog.New() l.SetLevelPrefix(glog.LEVEL_DEBU, "debug") l.Debug(ctx, "test") } ``` 执行后,终端输出: ```html 2021-12-31 11:21:45.754 [debug] test ``` # 日志组件-文件目录 ## 日志文件 默认情况下,日志文件名称以当前时间日期命名,格式为`YYYY-MM-DD.log`,我们可以使用`SetFile`方法来设置文件名称的格式,并且文件名称格式支持[时间管理-gtime](../components/os-gtime.md)时间格式。简单示例: ```go package main import ( "context" "github.com/gogf/gf/v2/frame/g" "github.com/gogf/gf/v2/os/gfile" "github.com/gogf/gf/v2/os/glog" ) // 设置日志等级 func main() { ctx := context.TODO() path := "./glog" glog.SetPath(path) glog.SetStdoutPrint(false) // 使用默认文件名称格式 glog.Print(ctx, "标准文件名称格式,使用当前时间时期") // 通过SetFile设置文件名称格式 glog.SetFile("stdout.log") glog.Print(ctx, "设置日志输出文件名称格式为同一个文件") // 链式操作设置文件名称格式 glog.File("stderr.log").Print(ctx, "支持链式操作") glog.File("error-{Ymd}.log").Print(ctx, "文件名称支持带gtime日期格式") glog.File("access-{Ymd}.log").Print(ctx, "文件名称支持带gtime日期格式") list, err := gfile.ScanDir(path, "*") g.Dump(err) g.Dump(list) } ``` 执行后,输出结果为: ```html [ "C:\hailaz\test\glog\2021-12-31.log", "C:\hailaz\test\glog\access-20211231.log", "C:\hailaz\test\glog\error-20211231.log", "C:\hailaz\test\glog\stderr.log", "C:\hailaz\test\glog\stdout.log", ] ``` 可以看到,文件名称格式中如果需要使用`gtime`时间格式,格式内容需要使用`{xxx}`包含起来。该示例中也使用到了`链式操作`的特性,具体请看后续说明。 ## 日志目录 默认情况下,`glog`将会输出日志内容到标准输出,我们可以通过`SetPath`方法设置日志输出的目录路径,这样日志内容将会写入到日志文件中,并且由于其内部使用了`gfpool`文件指针池,文件写入的效率相当优秀。简单示例: ```go package main import ( "context" "github.com/gogf/gf/v2/frame/g" "github.com/gogf/gf/v2/os/gfile" "github.com/gogf/gf/v2/os/glog" ) // 设置日志等级 func main() { ctx := context.TODO() path := "./glog" glog.SetPath(path) glog.Print(ctx, "日志内容") list, err := gfile.ScanDir(path, "*") g.Dump(err) g.Dump(list) } ``` 执行后,输出内容为: ```html 2021-12-31 11:32:16.742 日志内容 [ "C:\hailaz\test\glog\2021-12-31.log", ] ``` 当通过`SetPath`设置日志的输出目录,如果目录不存在时,将会递归创建该目录路径。可以看到,执行`Println`之后,在`/tmp`下创建了日志目录`glog`,并且在其下生成了日志文件。同时,我们也可以看见日志内容不仅输出到了文件,默认情况下也输出到了终端,我们可以通过`SetStdoutPrint(false)`方法来关闭终端的日志输出,这样日志内容仅会输出到日志文件中。 # 日志组件-链式操作 完整的方法列表可参考接口文档:[https://pkg.go.dev/github.com/gogf/gf/v2/os/glog](https://pkg.go.dev/github.com/gogf/gf/v2/os/glog) `glog`模块支持非常简便的`链式操作`方式,主要的链式操作方法如下: ```go // 重定向日志输出接口 func To(writer io.Writer) *Logger // 日志内容输出到目录 func Path(path string) *Logger // 设置日志文件分类 func Cat(category string) *Logger // 设置日志文件格式 func File(file string) *Logger // 设置日志打印级别 func Level(level int) *Logger // 设置日志打印级别(字符串) func LevelStr(levelStr string) *Logger // 设置文件回溯值 func Skip(skip int) *Logger // 是否开启trace打印 func Stack(enabled bool) *Logger // 开启trace打印并设定过滤trace的字符串 func StackWithFilter(filter string) *Logger // 是否开启终端输出 func Stdout(enabled...bool) *Logger // 是否输出日志头信息 func Header(enabled...bool) *Logger // 输出日志调用行号信息 func Line(long...bool) *Logger // 异步输出日志 func Async(enabled...bool) *Logger ``` ## 示例1, 基本使用 ```go package main import ( "context" "github.com/gogf/gf/v2/frame/g" "github.com/gogf/gf/v2/os/gfile" ) func main() { ctx := context.TODO() path := "/tmp/glog-cat" g.Log().SetPath(path) g.Log().Stdout(false).Cat("cat1").Cat("cat2").Print(ctx, "test") list, err := gfile.ScanDir(path, "*", true) g.Dump(err) g.Dump(list) } ``` 执行后,输出结果为: ```javascriprt [ "/tmp/glog-cat/cat1", "/tmp/glog-cat/cat1/cat2", "/tmp/glog-cat/cat1/cat2/2018-10-10.log", ] ``` ## 示例2, 打印调用行号 ```go package main import ( "context" "github.com/gogf/gf/v2/frame/g" ) func main() { ctx := context.TODO() g.Log().Line().Print(ctx, "this is the short file name with its line number") g.Log().Line(true).Print(ctx, "lone file name with line number") } ``` 执行后,终端输出结果为: ```html 2019-05-23 09:22:58.141 glog_line.go:8: this is the short file name with its line number 2019-05-23 09:22:58.142 /Users/john/Workspace/Go/GOPATH/src/github.com/gogf/gf/.example/os/glog/glog_line.go:9: lone file name with line number ``` ## 示例3, 文件回溯 `Skip` 有时我们通过一些模块封装了 `glog` 模块来打印日志,例如封装了一个 `logger` 包通过 `logger.Print` 来打印日志,这个时候打印出来的调用文件行号总是同一个位置,因为对于 `glog` 来讲,它的调用方即总是 `logger.Print` 方法。这个时候,我们可以通过设置回溯值来跳过回溯的文件数,使用 `SetStackSkip` 或者链式方法 `Skip` 实现。 > 文件回溯值的设置同样也会影响 `Stack` 调用回溯打印结果。 ```go package main import ( "context" "github.com/gogf/gf/v2/frame/g" ) func PrintLog(ctx context.Context, content string) { g.Log().Skip(1).Line().Print(ctx, "line number with skip:", content) g.Log().Line().Print(ctx, "line number without skip:", content) } func main() { ctx := context.TODO() PrintLog(ctx, "just test") } ``` 执行后,终端输出结果为: ```html 2019-05-23 19:30:10.984 glog_line2.go:13: line number with skip: just test 2019-05-23 19:30:10.984 glog_line2.go:9: line number without skip: just test ``` # 日志组件-颜色打印 ## 颜色打印 可以增加日志的可查看性,打印日志时,会将错误等级文字通过添加字体颜色的方式突出显示。 ## 效果示例 ```go package main import ( "context" "github.com/gogf/gf/v2/frame/g" ) func main() { ctx := context.TODO() g.Log().Debug(ctx, "Debug") g.Log().Info(ctx, "Info") g.Log().Notice(ctx, "Notice") g.Log().Warning(ctx, "Warning") g.Log().Error(ctx, "Error") } ``` ## 使用配置 控制台是必然会自带颜色输出的,文件日志默认不带颜色 若需要在文件中的日志也带上颜色可以在配置文件中添加配置 ```yaml logger: stdoutColorDisabled: false # 是否关闭终端的颜色打印。默认否,表示终端的颜色输出。 writerColorEnable: false # 是否开启Writer的颜色打印。默认否,表示不输出颜色到自定义的Writer或者文件。 ``` 也可以在代码中添加 ```go g.Log().SetWriterColorEnable(true) ``` 得到效果如下(红框部分是添加了文件日志颜色后的效果,另外部分为未开启时的效果) ## 默认颜色对应表 默认情况下,不同日志等级对应的颜色如下: ```go // \v2\os\glog\glog_logger_color.go var defaultLevelColor = map[int]int{ LEVEL_DEBU: COLOR_YELLOW, LEVEL_INFO: COLOR_GREEN, LEVEL_NOTI: COLOR_CYAN, LEVEL_WARN: COLOR_MAGENTA, LEVEL_ERRO: COLOR_RED, LEVEL_CRIT: COLOR_HI_RED, LEVEL_PANI: COLOR_HI_RED, LEVEL_FATA: COLOR_HI_RED, } ``` # 日志组件-Context 从`v2`版本开始,`glog`组件将`ctx`上下文变量作为日志打印的必需参数。 ## 自定义`CtxKeys` 日志组件支持自定义的键值打印,通过`ctx`上下文变量中读取。 ### 使用配置 ```yaml # 日志组件配置 logger: Level: "all" Stdout: true CtxKeys: ["RequestId", "UserId"] ``` 其中`CtxKeys`用于配置需要从`context.Context`接口对象中读取并输出的键名。 ### 日志输出 使用上述配置,然后在输出日志的时候,通过 `Ctx` 链式操作方法指定输出的 `context.Context` 接口对象,请注意 **不要使用自定义类型作为Key**,否则无法输出到日志文件中,例如: ```go package main import ( "context" "github.com/gogf/gf/v2/frame/g" ) func main() { var ctx = context.Background() // 可以直接使用String作为Key ctx = context.WithValue(ctx, "RequestId", "123456789") // 如需将Key提取为公共变量,可以使用gctx.StrKey类型,或直接使用string类型 const userIdKey gctx.StrKey = "UserId" // or const userIdKey = "UserId" ctx = context.WithValue(ctx, userIdKey, "10000") // 不能自定义类型 type notPrintKeyType string const notPrintKey notPrintKeyType = "NotPrintKey" ctx = context.WithValue(ctx, notPrintKey, "notPrintValue") // 不会打印 notPrintValue g.Log().Error(ctx, "runtime error") } ``` 执行后,终端输出: ```html 2024-09-26 11:45:33.790 [ERRO] {123456789, 10000} runtime error Stack: 1. main.main /Users/teemo/GolandProjects/gogf/demo/main.go:24 ``` ### 日志示例 ## 传递给 `Handler` 如果开发者自定义了日志对象的 `Handler`,那么每个日志打印传递的 `ctx` 上下文变量将会传递给 `Handler` 中。关于日志 `Handler` 的介绍请参考章节: [日志组件-Handler](./glog-handler.md) ## 链路跟踪支持 `glog` 组件支持 `OpenTelemetry` 标准的链路跟踪特性,该支持是内置的,无需开发者做任何设置,具体请参考章节: [服务链路跟踪](../obs/tracing.md) # 日志组件-Handler 从 `v2.0` 版本开始, `glog` 组件提供了超级强大的、可自定义日志处理的 `Handler` 特性。 `Handler` 采用了中间件设计方式,开发者可以为日志对象注册多个处理 `Handler`,也可以在 `Handler` 中覆盖默认的日志组件处理逻辑。 ## 相关定义 ### `Handler` 方法定义 ```go // Handler is function handler for custom logging content outputs. type Handler func(ctx context.Context, in *HandlerInput) ``` 可以看到第二个参数为日志处理的日志信息,并且为指针类型,意味着在 `Handler` 中可以修改该参数的任意属性信息,并且修改后的内容将会传递给下一个 `Handler`。 ### `Handler` 参数定义 ```go // HandlerInput is the input parameter struct for logging Handler. type HandlerInput struct { Logger *Logger // Current Logger object. Buffer *bytes.Buffer // Buffer for logging content outputs. Time time.Time // Logging time, which is the time that logging triggers. TimeFormat string // Formatted time string, like "2016-01-09 12:00:00". Color int // Using color, like COLOR_RED, COLOR_BLUE, etc. Eg: 34 Level int // Using level, like LEVEL_INFO, LEVEL_ERRO, etc. Eg: 256 LevelFormat string // Formatted level string, like "DEBU", "ERRO", etc. Eg: ERRO CallerFunc string // The source function name that calls logging, only available if F_CALLER_FN set. CallerPath string // The source file path and its line number that calls logging, only available if F_FILE_SHORT or F_FILE_LONG set. CtxStr string // The retrieved context value string from context, only available if Config.CtxKeys configured. TraceId string // Trace id, only available if OpenTelemetry is enabled. Prefix string // Custom prefix string for logging content. Content string // Content is the main logging content without error stack string produced by logger. Values []any // The passed un-formatted values array to logger. Stack string // Stack string produced by logger, only available if Config.StStatus configured. IsAsync bool // IsAsync marks it is in asynchronous logging. } ``` 开发者有 **两种方式** 通过 `Handler` 自定义日志输出内容: - 一种是直接修改 `HandlerInput` 中的属性信息,然后继续执行 `in.Next(ctx)`,默认的日志输出逻辑会将 `HandlerInput` 中的属性打印为字符串输出。 - 另一种是将日志内容写入到 `Buffer` 缓冲对象中即可,默认的日志输出逻辑如果发现 `Buffer` 已经存在日志内容将会忽略默认日志输出逻辑。 ### `Handler` 注册到 `Logger` 方法 ```go // SetHandlers sets the logging handlers for current logger. func (l *Logger) SetHandlers(handlers ...Handler) ``` ## 使用示例 我们来看两个示例便于更快速了解 `Handler` 的使用。 ### 示例1\. 将日志输出转换为 `Json` 格式输出 在本示例中,我们采用了前置中间件的设计,通过自定义 `Handler` 将日志内容输出格式修改为了 `JSON` 格式。 ```go package main import ( "context" "encoding/json" "os" "github.com/gogf/gf/v2/frame/g" "github.com/gogf/gf/v2/os/glog" "github.com/gogf/gf/v2/text/gstr" ) // JsonOutputsForLogger is for JSON marshaling in sequence. type JsonOutputsForLogger struct { Time string `json:"time"` Level string `json:"level"` Content string `json:"content"` } // LoggingJsonHandler is a example handler for logging JSON format content. var LoggingJsonHandler glog.Handler = func(ctx context.Context, in *glog.HandlerInput) { jsonForLogger := JsonOutputsForLogger{ Time: in.TimeFormat, Level: gstr.Trim(in.LevelFormat, "[]"), Content: gstr.Trim(in.Content), // 2.7以上版本用in.ValuesContent() } jsonBytes, err := json.Marshal(jsonForLogger) if err != nil { _, _ = os.Stderr.WriteString(err.Error()) return } in.Buffer.Write(jsonBytes) in.Buffer.WriteString("\n") in.Next(ctx) } func main() { g.Log().SetHandlers(LoggingJsonHandler) ctx := context.TODO() g.Log().Debug(ctx, "Debugging...") g.Log().Warning(ctx, "It is warning info") g.Log().Error(ctx, "Error occurs, please have a check") } ``` 可以看到,我们可以在 `Handler` 中通过 `Buffer` 属性操作来控制输出的日志内容。如果在所有的前置中间件 `Handler` 处理后 `Buffer` 内容为空,那么继续 `Next` 执行后将会执行日志中间件默认的 `Handler` 逻辑。执行本示例的代码后,终端输出: ```html {"time":"2021-12-31 11:03:25.438","level":"DEBU","content":"Debugging..."} {"time":"2021-12-31 11:03:25.438","level":"WARN","content":"It is warning info"} {"time":"2021-12-31 11:03:25.438","level":"ERRO","content":"Error occurs, please have a check \nStack:\n1. main.main\n C:/hailaz/test/main.go:42"} ``` ### 示例2\. 将内容输出到第三方日志搜集服务中 在本示例中,我们采用了后置中间件的设计,通过自定义 `Handler` 将日志内容输出一份到第三方 `graylog` 日志搜集服务中,并且不影响原有的日志输出处理。 > `Graylog` 是与 `ELK` 可以相提并论的一款集中式日志管理方案,支持数据收集、检索、可视化 `Dashboard`。在本示例中使用到了一个简单的第三方 `graylog` 客户端组件。 ```go package main import ( "context" "github.com/gogf/gf/v2/frame/g" "github.com/gogf/gf/v2/os/glog" gelf "github.com/robertkowalski/graylog-golang" ) var grayLogClient = gelf.New(gelf.Config{ GraylogPort: 80, GraylogHostname: "graylog-host.com", Connection: "wan", MaxChunkSizeWan: 42, MaxChunkSizeLan: 1337, }) // LoggingGrayLogHandler is an example handler for logging content to remote GrayLog service. var LoggingGrayLogHandler glog.Handler = func(ctx context.Context, in *glog.HandlerInput) { in.Next(ctx) grayLogClient.Log(in.Buffer.String()) } func main() { g.Log().SetHandlers(LoggingGrayLogHandler) ctx := context.TODO() g.Log().Debug(ctx, "Debugging...") g.Log().Warning(ctx, "It is warning info") g.Log().Error(ctx, "Error occurs, please have a check") glog.Print(ctx, "test log") } ``` ## 全局默认 `Handler` 日志对象默认是没有设置任何的 `Handler`,从 `v2.1` 版本开始,框架提供了可以设置全局默认 `Handler` 的功能特性。全局默认 `Handler` 将对所有的使用该日志组件,并且没有自定义 `Handler` 的日志打印功能生效。同时,全局默认 `Handler` 将会影响日志包方法的日志打印行为。 开发者可以通过以下两个方法来设置和获取全局默认的 `Handler`。 ```go // SetDefaultHandler sets default handler for package. func SetDefaultHandler(handler Handler) // GetDefaultHandler returns the default handler of package. func GetDefaultHandler() Handler ``` 需要注意,这种全局包配置的方法不是并发安全的,并且往往需要在项目启动逻辑最顶部执行。 使用示例,我们将项目所有的日志输出均采用 `JSON` 格式输出,以保证日志内容结构化并且每次日志输出都是单行,方便日志采集期采集日志: ```go package main import ( "context" "encoding/json" "os" "github.com/gogf/gf/v2/frame/g" "github.com/gogf/gf/v2/os/glog" "github.com/gogf/gf/v2/text/gstr" ) // JsonOutputsForLogger is for JSON marshaling in sequence. type JsonOutputsForLogger struct { Time string `json:"time"` Level string `json:"level"` Content string `json:"content"` } // LoggingJsonHandler is a example handler for logging JSON format content. var LoggingJsonHandler glog.Handler = func(ctx context.Context, in *glog.HandlerInput) { jsonForLogger := JsonOutputsForLogger{ Time: in.TimeFormat, Level: gstr.Trim(in.LevelFormat, "[]"), Content: gstr.Trim(in.Content), // 2.7以上版本用in.ValuesContent() } jsonBytes, err := json.Marshal(jsonForLogger) if err != nil { _, _ = os.Stderr.WriteString(err.Error()) return } in.Buffer.Write(jsonBytes) in.Buffer.WriteString("\n") in.Next(ctx) } func main() { ctx := context.TODO() glog.SetDefaultHandler(LoggingJsonHandler) g.Log().Debug(ctx, "Debugging...") glog.Warning(ctx, "It is warning info") glog.Error(ctx, "Error occurs, please have a check") } ``` 执行后,终端输出: ```html {"time":"2022-06-20 10:51:50.235","level":"DEBU","content":"Debugging..."} {"time":"2022-06-20 10:51:50.235","level":"WARN","content":"It is warning info"} {"time":"2022-06-20 10:51:50.235","level":"ERRO","content":"Error occurs, please have a check"} ``` ## 组件通用 `Handler` 组件提供了一些通用性的日志 `Handler`,方便开发者使用,提高开发效率。 ### `HandlerJson` 该 `Handler` 可以将日志内容转换为 `Json` 格式打印。使用示例: ```go package main import ( "context" "github.com/gogf/gf/v2/frame/g" "github.com/gogf/gf/v2/os/glog" ) func main() { ctx := context.TODO() glog.SetDefaultHandler(glog.HandlerJson) g.Log().Debug(ctx, "Debugging...") glog.Warning(ctx, "It is warning info") glog.Error(ctx, "Error occurs, please have a check") } ``` 执行后,终端输出: ```html {"Time":"2022-06-20 20:04:04.725","Level":"DEBU","Content":"Debugging..."} {"Time":"2022-06-20 20:04:04.725","Level":"WARN","Content":"It is warning info"} {"Time":"2022-06-20 20:04:04.725","Level":"ERRO","Content":"Error occurs, please have a check","Stack":"1. main.main\n /Users/john/Workspace/Go/GOPATH/src/github.com/gogf/gf/.test/main.go:16\n"} ``` ### `HandlerStructure` 该 `Handler` 可以将日志内容转换为结构化格式打印,主要是为了和 `Golang` 新版本的 `slog` 日志输出内容保持一致。需要注意,日志结构化打印的特性需要保证所有日志记录均采用结构化输出才更具意义。使用示例: ```go package main import ( "context" "net" "github.com/gogf/gf/v2/frame/g" "github.com/gogf/gf/v2/os/glog" ) func main() { ctx := context.TODO() glog.SetDefaultHandler(glog.HandlerStructure) g.Log().Info(ctx, "caution", "name", "admin") glog.Error(ctx, "oops", net.ErrClosed, "status", 500) } ``` 执行后,终端输出: ```html Time="2023-11-23 21:00:08.671" Level=INFO Content=caution name=admin Time="2023-11-23 21:00:08.671" Level=ERRO oops="use of closed network connection" status=500 Stack="1. main.main\n /Users/txqiangguo/Workspace/gogf/gf/example/.test/main.go:16\n" ``` # 日志组件-JSON格式 `glog` 对日志分析工具非常友好,支持输出 `JSON` 格式的日志内容,以便于后期对日志内容进行解析分析。 ## 使用 `map/struct` 参数 想要支持 `JSON` 数据格式的日志输出非常简单,给打印方法提供 `map`/ `struct` 类型参数即可。 使用示例: ```go package main import ( "context" "github.com/gogf/gf/v2/frame/g" ) func main() { ctx := context.TODO() g.Log().Debug(ctx, g.Map{"uid": 100, "name": "john"}) type User struct { Uid int `json:"uid"` Name string `json:"name"` } g.Log().Debug(ctx, User{100, "john"}) } ``` 执行后,终端输出结果: ```html 2019-06-02 15:28:52.653 [DEBU] {"name":"john","uid":100} 2019-06-02 15:28:52.653 [DEBU] {"uid":100,"name":"john"} ``` ## 结合 `gjson.MustEncode` 此外,也可以结合 `gjson.MustEncode` 来实现 `Json` 内容输出,例如: ```go package main import ( "context" "github.com/gogf/gf/v2/encoding/gjson" "github.com/gogf/gf/v2/frame/g" ) func main() { ctx := context.TODO() type User struct { Uid int `json:"uid"` Name string `json:"name"` } g.Log().Debugf(ctx, `user json: %s`, gjson.MustEncode(User{100, "john"})) } ``` 执行后,终端输出结果: ```html 2022-04-25 18:09:45.029 [DEBU] user json: {"uid":100,"name":"john"} ``` # 日志组件-异步输出 对于日志输出即时性要求不高的内容,可以通过异步的方式输出日志,异步输出使得日志打印调用可立即返回,因此效率较高。`glog`当然支持异步输出特性,并且内部使用了`goroutine`池来管理异步日志打印任务,可以充分的降低对资源的占用率。 异步输出可以通过日志对象的`SetAsync`/`SetFlags`方法,或者通过链式操作`Async`方法实现。但是需要注意的是,如果通过对象设置方法设置异步输出,那么后续所有的日志输出都将是异步的;如果是通过链式操作输出,那么仅对当前日志输出为异步。 **注意** 如果对于同一个文件日志输出既采用了同步打印,也采用了异步打印,注意日志文件的内容可能会出现乱序问题,这种情况应当尽量避免。 ### `SetAsync` 我们来看一个使用 `SetAsync` 方法实现异步打印的示例。 ```go package main import ( "context" "time" "github.com/gogf/gf/v2/frame/g" ) func main() { ctx := context.TODO() g.Log().SetAsync(true) for i := 0; i < 10; i++ { g.Log().Print(ctx, "async log", i) } } ``` 执行后,可以发现终端什么内容也没有输出,因为日志输出的异步的,该示例在日志内容还没有输出之前就退出了。因此,我们可以稍做改进如下: ```go package main import ( "context" "time" "github.com/gogf/gf/v2/frame/g" ) func main() { ctx := context.TODO() g.Log().SetAsync(true) for i := 0; i < 10; i++ { g.Log().Print(ctx, "async log", i) } time.Sleep(time.Second) } ``` 执行后,终端输出结果为: ```html 2019-06-02 15:44:21.399 async log 0 2019-06-02 15:44:21.399 async log 1 2019-06-02 15:44:21.399 async log 2 2019-06-02 15:44:21.399 async log 3 2019-06-02 15:44:21.399 async log 4 2019-06-02 15:44:21.399 async log 5 2019-06-02 15:44:21.399 async log 6 2019-06-02 15:44:21.399 async log 7 2019-06-02 15:44:21.399 async log 8 2019-06-02 15:44:21.399 async log 9 ``` ### `Async` 链式操作 使用链式操作比较简单。 ```go package main import ( "context" "time" "github.com/gogf/gf/v2/frame/g" ) func main() { ctx := context.TODO() for i := 0; i < 10; i++ { g.Log().Async().Print(ctx, "async log", i) } g.Log().Print(ctx, "normal log") g.Log().Print(ctx, "normal log") g.Log().Print(ctx, "normal log") time.Sleep(time.Second) } ``` 执行后,终端输出结果为: ```html 2022-01-05 15:00:44.101 normal log 2022-01-05 15:00:44.101 async log 0 2022-01-05 15:00:44.101 async log 1 2022-01-05 15:00:44.101 async log 2 2022-01-05 15:00:44.101 async log 3 2022-01-05 15:00:44.101 async log 4 2022-01-05 15:00:44.101 async log 5 2022-01-05 15:00:44.101 async log 6 2022-01-05 15:00:44.101 async log 7 2022-01-05 15:00:44.101 async log 8 2022-01-05 15:00:44.101 async log 9 2022-01-05 15:00:44.101 normal log 2022-01-05 15:00:44.103 normal log ``` # 日志组件-堆栈打印 错误日志信息支持`Stack`特性,该特性可以自动打印出当前调用日志组件方法的堆栈信息,该堆栈信息可以通过`Notice*`/`Warning*`/`Error*`/`Critical*`/`Panic*`/`Fatal*`等错误日志输出方法触发,也可以通过`GetStack`/`PrintStack`获取/打印。错误信息的`stack`信息对于调试来说相当有用。 ### 示例1,通过`Error`方法触发 ```go package main import ( "context" "github.com/gogf/gf/v2/os/glog" ) func Test(ctx context.Context) { glog.Error(ctx, "This is error!") } func main() { ctx := context.TODO() Test(ctx) } ``` 打印出的结果如下: ```html 2022-01-05 15:08:54.998 [ERRO] This is error! Stack: 1. main.Test C:/hailaz/test/main.go:10 2. main.main C:/hailaz/test/main.go:15 ``` ### 示例2,通过`Stack`方法打印 ```go package main import ( "context" "fmt" "github.com/gogf/gf/v2/os/glog" ) func main() { ctx := context.TODO() glog.PrintStack(ctx) glog.New().PrintStack(ctx) fmt.Println(glog.GetStack()) fmt.Println(glog.New().GetStack()) } ``` 执行后,输出结果为: ```html 2019-07-12 22:20:28.070 Stack: 1. main.main /home/john/Workspace/Go/GOPATH/src/github.com/gogf/gf/v2/geg/os/glog/glog_stack.go:11 2019-07-12 22:20:28.070 Stack: 1. main.main /home/john/Workspace/Go/GOPATH/src/github.com/gogf/gf/v2/geg/os/glog/glog_stack.go:12 1. main.main /home/john/Workspace/Go/GOPATH/src/github.com/gogf/gf/v2/geg/os/glog/glog_stack.go:14 1. main.main /home/john/Workspace/Go/GOPATH/src/github.com/gogf/gf/v2/geg/os/glog/glog_stack.go:15 ``` ### 示例3,打印`gerror.Error` `glog`日志模块支持对标准错误以及`gerror`错误的堆栈打印支持。 ```go package main import ( "context" "errors" "github.com/gogf/gf/v2/errors/gerror" "github.com/gogf/gf/v2/os/glog" ) func MakeError() error { return errors.New("connection closed with normal error") } func MakeGError() error { return gerror.New("connection closed with gerror") } func TestGError(ctx context.Context) { err1 := MakeError() err2 := MakeGError() glog.Error(ctx, err1) glog.Errorf(ctx, "%+v", err2) } func main() { ctx := context.TODO() TestGError(ctx) } ``` 执行后,终端输出: ```html 2019-07-12 22:23:11.467 [ERRO] connection closed with normal error Stack: 1. main.TestGError /home/john/Workspace/Go/GOPATH/src/github.com/gogf/gf/v2/geg/os/glog/glog_gerror.go:20 2. main.main /home/john/Workspace/Go/GOPATH/src/github.com/gogf/gf/v2/geg/os/glog/glog_gerror.go:25 2019-07-12 22:23:11.467 [ERRO] connection closed with gerror 1. connection closed with gerror 1). main.MakeGError /home/john/Workspace/Go/GOPATH/src/github.com/gogf/gf/v2/geg/os/glog/glog_gerror.go:14 2). main.TestGError /home/john/Workspace/Go/GOPATH/src/github.com/gogf/gf/v2/geg/os/glog/glog_gerror.go:19 3). main.main /home/john/Workspace/Go/GOPATH/src/github.com/gogf/gf/v2/geg/os/glog/glog_gerror.go:25 Stack: 1. main.TestGError /home/john/Workspace/Go/GOPATH/src/github.com/gogf/gf/v2/geg/os/glog/glog_gerror.go:21 2. main.main /home/john/Workspace/Go/GOPATH/src/github.com/gogf/gf/v2/geg/os/glog/glog_gerror.go:25 ``` # 日志组件-调试信息 `Debug`/`Debugf`是非常有用的几个方法,用于调试信息的记录,常用于开发/测试环境中,当应用上线之后可以方便地使用`SetDebug`或者配置文件进行开启/关闭。 ```go package main import ( "context" "time" "github.com/gogf/gf/v2/frame/g" "github.com/gogf/gf/v2/os/gtime" "github.com/gogf/gf/v2/os/gtimer" ) func main() { ctx := context.TODO() gtimer.SetTimeout(ctx, 3*time.Second, func(ctx context.Context) { g.Log().SetDebug(false) }) for { g.Log().Debug(ctx, gtime.Datetime()) g.Log().Info(ctx, gtime.Datetime()) time.Sleep(time.Second) } } ``` 该示例中使用`glog.Debug`方法输出调试信息,3秒后关闭调试信息的输出。执行后,输出结果如下,可以看到只输出了3条日志信息,后续的调试日志信息由于通过`SetDebug`方法关闭后,便不再输出。 ```html 2022-01-05 15:59:05.674 [DEBU] 2022-01-05 15:59:05 2022-01-05 15:59:05.675 [INFO] 2022-01-05 15:59:05 2022-01-05 15:59:06.684 [DEBU] 2022-01-05 15:59:06 2022-01-05 15:59:06.684 [INFO] 2022-01-05 15:59:06 2022-01-05 15:59:07.692 [DEBU] 2022-01-05 15:59:07 2022-01-05 15:59:07.692 [INFO] 2022-01-05 15:59:07 2022-01-05 15:59:08.708 [INFO] 2022-01-05 15:59:08 2022-01-05 15:59:09.717 [INFO] 2022-01-05 15:59:09 2022-01-05 15:59:10.728 [INFO] 2022-01-05 15:59:10 2022-01-05 15:59:11.733 [INFO] 2022-01-05 15:59:11 ``` > 我们还可以通过命令行参数或者系统环境变量参数的方式关闭掉调试信息。 > > 1. 修改命令行启动参数 - `gf.glog.debug=false`; > 2. 修改指定的环境变量 - `GF_GLOG_DEBUG=false`; # 日志组件-Writer接口 **提示** `Writer` 接口是最底层的 `IO` 写入接口,如果业务需要自定义日志内容打印,建议使用 `Handler` 特性,参考章节: [日志组件-Handler](./glog-handler.md) ## 自定义 `Writer` 接口 `glog` 模块实现了标准输出以及文件输出的日志内容打印。当然,开发者也可以通过自定义 `io.Writer` 接口实现自定义的日志内容输出。 `io.Writer` 是标准库提供的内容输出接口,其定义如下: ```go type Writer interface { Write(p []byte) (n int, err error) } ``` 我们可以通过 `SetWriter` 方法或者链式方法 `To` 来实现自定义 `Writer` 输出,开发者可以在该 `Writer` 中实现定义的操作,也可以在其中整合其他的模块功能。 此外, `glog.Logger` 对象已经实现了 `io.Writer` 接口,因此开发者可以非常方便地将 `glog` 整合使用到其他的模块中。 ## 示例1,实现日志 `HOOK` 在该示例中,我们实现了一个自定义的 `Writer` 对象 `MyWriter`,在该对象实现的 `Writer` 接口中我们对日志内容进行判断,如果出现了 `PANI` 或者 `FATA` 错误,那么表示是非常严重的错误,该接口将会第一时间通过 `HTTP` 接口告知 `Monitor` 监控服务。随后再将日志内容通过 `glog` 模块按照配置写入到文件和标准输出。 ```go package main import ( "context" "fmt" "github.com/gogf/gf/v2/frame/g" "github.com/gogf/gf/v2/net/ghttp" "github.com/gogf/gf/v2/os/glog" "github.com/gogf/gf/v2/text/gregex" ) type MyWriter struct { logger *glog.Logger } func (w *MyWriter) Write(p []byte) (n int, err error) { var ( s = string(p) ctx = context.Background() ) if gregex.IsMatchString(`PANI|FATA`, s) { fmt.Println("SERIOUS ISSUE OCCURRED!! I'd better tell monitor in first time!") g.Client().PostContent(ctx, "http://monitor.mydomain.com", s) } return w.logger.Write(p) } func main() { var ctx = context.Background() glog.SetWriter(&MyWriter{ logger: glog.New(), }) glog.Fatal(ctx, "FATAL ERROR") } ``` 执行后,输出结果为: ```html SERIOUS ISSUE OCCURRED!! I'd better tell monitor in first time! 2019-05-23 20:14:49.374 [FATA] FATAL ERROR Stack: 1. /Users/john/Workspace/Go/GOPATH/src/github.com/gogf/gf/v2/geg/os/glog/glog_writer_hook.go:27 ``` ## 示例2,整合 `graylog` 假如我们需要输出日志到文件及标准输出,并且同时也需要输出日志到 `Graylog`,很明显这个也是需要自定义 `Writer` 才能实现。当然同理,我们也可以自定义输出到其他的日志收集组件或者数据库中。 > `Graylog` 是与 `ELK` 可以相提并论的一款集中式日志管理方案,支持数据收集、检索、可视化 `Dashboard`。 示例代码: ```go package main import ( "context" "github.com/gogf/gf/v2/os/glog" "github.com/robertkowalski/graylog-golang" ) type MyGrayLogWriter struct { gelf *gelf.Gelf logger *glog.Logger } func (w *MyGrayLogWriter) Write(p []byte) (n int, err error) { w.gelf.Send(p) return w.logger.Write(p) } func main() { var ctx = context.Background() glog.SetWriter(&MyGrayLogWriter{ logger : glog.New(), gelf : gelf.New(gelf.Config{ GraylogPort : 80, GraylogHostname : "graylog-host.com", Connection : "wan", MaxChunkSizeWan : 42, MaxChunkSizeLan : 1337, }), }) glog.Println(ctx, "test log") } ``` # 日志组件-Flags特性 `flags`用于控制日志组件的额外特性开关,这些属性使用常量进行组合控制,包括: ```go F_ASYNC = 1 << iota // 开启日志异步输出 F_FILE_LONG // 打印调用行号信息,完整绝对路径,例如:/a/b/c/d.go:23 F_FILE_SHORT // 打印调用行号信息,仅打印文件名,例如:d.go:23,覆盖 F_FILE_LONG. F_TIME_DATE // 打印当前日期,如:2009-01-23 F_TIME_TIME // 打印当前时间,如:01:23:23 F_TIME_MILLI // 打印当前时间+毫秒,如:01:23:23.675 F_TIME_STD = F_TIME_DATE | F_TIME_MILLI // (默认)打印当前日期+时间+毫秒,如:2009-01-23 01:23:23.675 ``` 使用示例: ```go package main import ( "context" "github.com/gogf/gf/v2/os/glog" ) func main() { ctx := context.TODO() l := glog.New() l.SetFlags(glog.F_TIME_TIME | glog.F_FILE_SHORT) l.Print(ctx, "time and short line number") l.SetFlags(glog.F_TIME_MILLI | glog.F_FILE_LONG) l.Print(ctx, "time with millisecond and long line number") l.SetFlags(glog.F_TIME_STD | glog.F_FILE_LONG) l.Print(ctx, "standard time format and long line number") } ``` 执行后,终端输出结果为: ```html PS C:\hailaz\test> go run .\main.go 16:05:35 main.go:13: time and short line number 16:05:35.108 C:/hailaz/test/main.go:15: time with millisecond and long line number 2022-01-05 16:05:35.109 C:/hailaz/test/main.go:17: standard time format and long line number ``` # 日志组件-Rotate特性 **注意** 滚动切分目前属于实验性特性,如有问题请随时反馈。 之前的章节中我们知道, `glog` 组件支持通过设置日志文件名称的方式,使得日志文件按照日期进行输出。从 `GoFrame v1.12` 版本开始, `glog` 组件也支持对日志文件进行滚动切分的特性,该特性涉及到日志对象配置属性中的以下几个配置项: ```go RotateSize int64 // Rotate the logging file if its size > 0 in bytes. RotateExpire time.Duration // Rotate the logging file if its mtime exceeds this duration. RotateBackupLimit int // Max backup for rotated files, default is 0, means no backups. RotateBackupExpire time.Duration // Max expire for rotated files, which is 0 in default, means no expiration. RotateBackupCompress int // Compress level for rotated files using gzip algorithm. It's 0 in default, means no compression. RotateCheckInterval time.Duration // Asynchronizely checks the backups and expiration at intervals. It's 1 hour in default. ``` 简要说明: 1. `RotateSize` 用于设置滚动切分时按照文件大小进行切分,该属性的单位为字节。只有当该属性值大于0时才会开启滚动切分的特性。 2. `RotateExpire` 用于设置滚动切分时按照文件超过一定时间没有修改时进行切分。只有当该属性值大于0时才会开启滚动切分的特性。 3. `RotateBackupLimit` 用于设置滚动切分的保留文件数,默认为0表示不保留,往往该值需要设置大于0。超过该保留文件数的切分文件将会按照从旧到新进行删除。 4. `RotateBackupExpire` 用于设置按照过期时间进行清理。当切分文件超过指定的时间时将会被删除。 5. `RotateBackupCompress` 用于设置切分文件的压缩级别,默认为0表示不压缩。该压缩级别的取值范围为 `1-9`,其中 `9` 为最大压缩级别。 6. `RotateCheckInterval` 用于设置定时器的定时检测间隔,默认为1小时,往往不需要设置。 ### 功能开启 从以上滚动切分的配置项可以看到,仅有当 `RotateSize` 或者 `RotateExpire` 配置项被设置时才会生效,默认情况下是关闭的。 ### 文件格式 `glog` 组件的日志输出文件固定格式为 `*.log`,即使用 `.log` 作为日志文件名后缀。为方便统一规范管理,切分文件的格式也是固定的,开发者无法自定义。当日志文件被滚动切分时,当前被切分的日志文件将会按照」 `*.切分时间.log`「的格式进行重命名。其中 `切分时间` 的格式为: `年月日时分秒毫秒微秒`,例如: ```html access.log -> access.20200326101301899002.log access.20200326.log -> access.20200326.20200326101301899002.log ``` ### 配置示例 1. 示例1,按照日志文件大小进行滚动切分: ```yaml logger: Path: "/var/log" Level: "all" Stdout: false RotateSize: "100M" RotateBackupLimit: 2 RotateBackupExpire: "7d" RotateBackupCompress: 9 ``` 简要说明: - 可以看到 `RotateSize` 配置项在配置文件中支持使用字符串的形式进行设置,例如: `100M`, `200MB`, `500KB`, `1Gib` 等等,在该示例中,我们设置日志文件大小超过 `100M` 时进行切分。 - 同时, `RotateBackupExpire` 的配置项也支持字符串配置,例如: `1h`, `30m`, `1d6h`, `7d` 等等,在该示例中,我们设置切分文件的过期时间为 `7d`,即七天后会自动删除该切分文件。 - 这里通过设置 `RotateBackupCompress` 为 `9` 表示使用最大的压缩级别,使得切分后的文件最小化存储。但是需要注意,切分和压缩是两个不同的操作,文件压缩是一个异步操作,因此当文件被自动切分后,定时器通过一定的时间间隔定期检查后再自动将其压缩为 `*.gz` 文件。压缩级别设置得越高,会更多使用 `CPU` 资源。 2. 示例2,按照日志文件修改过期进行滚动切分: ```yaml logger: Path: "/var/log" Level: "all" Stdout: false RotateExpire: "1d" RotateBackupLimit: 1 RotateBackupExpire: "7d" RotateBackupCompress: 9 ``` 在这里,我们设置 `RotateExpire` 为 `1d` 表示当某个日志文件超过一天都没有任何修改/写入时, `glog` 模块将会自动将其进行滚动切分。同时进行压缩存储。 ### 注意事项 由于不同的日志对象可能有不同的滚动切分配置,假如多个日志对象的日志目录为同一个,并且都开启了滚动切分特性,那么多个日志对象的滚动切分配置项会冲突,会有意想不到的结果。因此,我们建议两个选择: 1. 全局使用同一个默认的日志单例对象( `g.Log()`),通过 `Cat` 或 `File` 方法设置输出日志文件到不同的目录或文件名。 2. 将不同日志对象( `g.Log(名称)`)的输出目录( `Path` 配置项)设置为不同的文件目录,并且多个日志对象的日志目录不存在相互的层级关系。 例如: 我们有两类业务日志文件 `order` 和 `promo`,分别对应订单业务和促销业务,我们先假定他们属于同一个服务程序中。 假如日志路径为 `/var/log`,我们可以: 1. 通过 `g.Log().Cat("order").Print(xxx)` 输出订单日志。生成的日志文件例如: `/var/log/order/2020-03-26.log`。 2. 通过 `g.Log().Cat("promo").Print(xxx)` 输出促销日志。生成的日志文件例如: `/var/log/promo/2020-03-26.log`。 也可以通过: 1. 通过 `g.Log("order").Print(xxx)` 输出订单日志。生成的日志文件例如: `/var/log/order.log`。 2. 通过 `g.Log("promo").Print(xxx)` 输出促销日志。生成的日志文件例如: `/var/log/promo.log`。 # 日志组件-常见问题 ## 如何打印`error`变量,打印出`error`对应的堆栈而不是日志方法调用时的堆栈 通过以下方法只会打印`error`的字符串描述信息,并且堆栈只是日志方法`Error`调用时的堆栈: ```go g.Log().Error(ctx, err) ``` 如果想要打印`error`变量的堆栈,并不想打印`Error`方法调用时的堆栈,可以通过以下方式: ```go g.Log().Printf(ctx, "%+v", err) ``` 参考连接: - [https://github.com/gogf/gf/issues/1640](https://github.com/gogf/gf/issues/1640) - [错误处理-其他特性](./gerror-other.md) # 错误处理 ## 基本介绍 `GoFrame` 框架提供了强大、丰富、统一的错误处理能力,由 `gerror` 组件实现。该组件也是框架统一的错误处理组件,框架所有组件如果存在错误返回时,均带有堆栈信息,方便开发者快速定位问题。 **使用方式**: ```go import "github.com/gogf/gf/v2/errors/gerror" ``` **接口文档**: [https://pkg.go.dev/github.com/gogf/gf/v2/errors/gerror](https://pkg.go.dev/github.com/gogf/gf/v2/errors/gerror) # 错误处理-常用方法 本章节仅介绍一些常用方法,完整的错误方法请参考接口文档: [https://pkg.go.dev/github.com/gogf/gf/v2/errors/gerror](https://pkg.go.dev/github.com/gogf/gf/v2/errors/gerror) ## `New/Newf` - 说明:用于创建一个自定义错误信息的`error`对象,并包含堆栈信息。 - 格式: ```go // New creates and returns an error which is formatted from given text. New(text string) error // Newf returns an error that formats as the given format and args. Newf(format string, args ...interface{}) error ``` ## `Wrap/Wrapf` - 说明:用于包裹其他错误 `error` 对象,构造成多级的错误信息,包含堆栈信息。 - 格式: ```go // Wrap wraps error with text. It returns nil if given err is nil. // Note that it does not lose the error code of wrapped error, as it inherits the error code from it. func Wrap(err error, text string) error // Wrapf returns an error annotating err with a stack trace at the point Wrapf is called, and the format specifier. // It returns nil if given `err` is nil. // Note that it does not lose the error code of wrapped error, as it inherits the error code from it. func Wrapf(err error, format string, args ...interface{}) error ``` ## `NewSkip/NewSkipf` - 说明:用于创建一个自定义错误信息的 `error` 对象,并且忽略部分堆栈信息(按照当前调用方法位置往上忽略)。高级功能,一般开发者很少用得到。 - 格式: ```go // NewSkip creates and returns an error which is formatted from given text. // The parameter `skip` specifies the stack callers skipped amount. func NewSkip(skip int, text string) error // NewSkipf returns an error that formats as the given format and args. // The parameter `skip` specifies the stack callers skipped amount. func NewSkipf(skip int, format string, args ...interface{}) error ``` ## `WrapSkip/WrapSkipf` - 说明:类似于`Warp/Wrapf`方法,但会忽略部分堆栈信息。 - 格式: ```go // WrapSkip wraps error with text. It returns nil if given err is nil. // The parameter `skip` specifies the stack callers skipped amount. // Note that it does not lose the error code of wrapped error, as it inherits the error code from it. func WrapSkip(skip int, err error, text string) error // WrapSkipf wraps error with text that is formatted with given format and args. It returns nil if given err is nil. // The parameter `skip` specifies the stack callers skipped amount. // Note that it does not lose the error code of wrapped error, as it inherits the error code from it. func WrapSkipf(skip int, err error, format string, args ...interface{}) error ``` # 错误处理-堆栈特性 ## 错误堆栈 标准库的`error`错误实现比较简单,无法进行堆栈追溯,对于产生错误时的上层调用者来讲不是很友好,无法获得错误的调用链详细信息。`gerror`支持错误堆栈记录,通过`New/Newf`、`Wrap/Wrapf`、`WrapSkip/WrapSkipf`等方法均会自动记录当前错误产生时的堆栈信息。 示例: ```go package main import ( "fmt" "github.com/gogf/gf/v2/errors/gerror" ) func OpenFile() error { return gerror.New("permission denied") } func OpenConfig() error { return gerror.Wrap(OpenFile(), "configuration file opening failed") } func ReadConfig() error { return gerror.Wrap(OpenConfig(), "reading configuration failed") } func main() { fmt.Printf("%+v", ReadConfig()) } ``` 执行后,输出示例: ```text reading configuration failed: configuration file opening failed: permission denied 1. reading configuration failed 1). main.ReadConfig /Users/john/Workspace/Go/GOPATH/src/github.com/gogf/gf/v2/geg/errors/gerror/gerror2.go:18 2). main.main /Users/john/Workspace/Go/GOPATH/src/github.com/gogf/gf/v2/geg/errors/gerror/gerror2.go:25 2. configuration file opening failed 1). main.OpenConfig /Users/john/Workspace/Go/GOPATH/src/github.com/gogf/gf/v2/geg/errors/gerror/gerror2.go:14 2). main.ReadConfig /Users/john/Workspace/Go/GOPATH/src/github.com/gogf/gf/v2/geg/errors/gerror/gerror2.go:18 3). main.main /Users/john/Workspace/Go/GOPATH/src/github.com/gogf/gf/v2/geg/errors/gerror/gerror2.go:25 3. permission denied 1). main.OpenFile /Users/john/Workspace/Go/GOPATH/src/github.com/gogf/gf/v2/geg/errors/gerror/gerror2.go:10 2). main.OpenConfig /Users/john/Workspace/Go/GOPATH/src/github.com/gogf/gf/v2/geg/errors/gerror/gerror2.go:14 3). main.ReadConfig /Users/john/Workspace/Go/GOPATH/src/github.com/gogf/gf/v2/geg/errors/gerror/gerror2.go:18 4). main.main /Users/john/Workspace/Go/GOPATH/src/github.com/gogf/gf/v2/geg/errors/gerror/gerror2.go:25 ``` **提示** 其中打印的错误堆栈路径为笔者本地执行的代码路径。 可以看到,调用端可以通过 `Wrap` 方法将底层的错误信息进行层级叠加,并且包含完整的错误堆栈信息。 ## `HasStack` 判断错误是否带堆栈 - 说明:通过 `HasStack` 方法我们可以判断给定的 `error` 接口对象是否实现(包含)了堆栈信息。 - 格式: ```go // HasStack checks and reports whether `err` implemented interface `gerror.IStack`. HasStack(err error) bool ``` - 示例: ```go func ExampleHasStack() { err1 := errors.New("sql error") err2 := gerror.New("write error") fmt.Println(gerror.HasStack(err1)) fmt.Println(gerror.HasStack(err2)) // Output: // false // true } ``` **注意** 目前该方法是通过判断错误对象是否实现`gerror.IStack`接口来实现的,如果错误对象未实现该接口那么将会无法识别其是否带有堆栈信息。 ## `Stack` 获取堆栈信息 - 说明:通过 `Stack` 方法我们可以获得 `error` 对象的完整堆栈信息,返回堆栈列表字符串。 - 格式: ```go // Stack returns the stack callers as string. // It returns the error string directly if the `err` does not support stacks. Stack(err error) string ``` - 示例: ```go func ExampleStack() { var err error err = errors.New("sql error") err = gerror.Wrap(err, "adding failed") err = gerror.Wrap(err, "api calling failed") fmt.Println(gerror.Stack(err)) } ``` 执行后,输出示例信息: ```go 1. api calling failed 1). main.main /Users/john/Workspace/Go/GOPATH/src/github.com/gogf/gf/.example/other/test.go:14 2. adding failed 1). main.main /Users/john/Workspace/Go/GOPATH/src/github.com/gogf/gf/.example/other/test.go:13 3. sql error ``` **注意** 目前该方法是通过获取错误对象实现的`gerror.IStack`接口来实现的,如果错误对象未实现该接口那么该方法将会返回空字符串。 ## `Current` 获取当前 `error` - 说明: `Current` 方法用于获取当前层级的错误信息,通过 `error` 接口对象返回。 - 格式: ```go // Current creates and returns the current level error. // It returns nil if current level error is nil. Current(err error) error ``` - 示例: ```go func ExampleCurrent() { var err error err = errors.New("sql error") err = gerror.Wrap(err, "adding failed") err = gerror.Wrap(err, "api calling failed") fmt.Println(err) fmt.Println(gerror.Current(err)) // Output: // api calling failed: adding failed: sql error // api calling failed } ``` ## `Unwrap` 获取下一层 `error` - 说明: `Unwrap` 方法用于获取层级错误的下一级错误 `error` 接口对象。当下一层级不存在时,返回 `nil`。 - 格式: ```go // Unwrap returns the next level error. // It returns nil if current level error or the next level error is nil. Unwrap(err error) error ``` - 示例1:简单的错误层级访问示例。 ```go func ExampleUnwrap() { var err error err = errors.New("sql error") err = gerror.Wrap(err, "adding failed") err = gerror.Wrap(err, "api calling failed") fmt.Println(err) err = gerror.Unwrap(err) fmt.Println(err) err = gerror.Unwrap(err) fmt.Println(err) // Output: // api calling failed: adding failed: sql error // adding failed: sql error // sql error } ``` - 示例2:常见遍历逻辑代码示例。 ```go func IsGrpcErrorNotFound(err error) bool { if err != nil { for e := err; e != nil; e = gerror.Unwrap(e) { if s, ok := status.FromError(e); ok && s != nil && s.Code() == codes.NotFound { return true } } } return false } ``` ## `Cause` 获取根错误 `error` - 说明:通过 `Cause` 方法我们可以获得 `error` 对象的根错误信息(原始错误)。 - 格式: ```go // Cause returns the root cause error of `err`. Cause(err error) error ``` - 示例: ```go package main import ( "fmt" "github.com/gogf/gf/v2/errors/gerror" ) func OpenFile() error { return gerror.New("permission denied") } func OpenConfig() error { return gerror.Wrap(OpenFile(), "configuration file opening failed") } func ReadConfig() error { return gerror.Wrap(OpenConfig(), "reading configuration failed") } func main() { fmt.Println(gerror.Cause(ReadConfig())) } // Output: // permission denied ``` # 错误处理-错误比较 ## `Equal`比较方法 错误对象支持比较,`Equal`方法用于完整判断两个`error`是否相同。主要通过以下方法: ```go // Equal reports whether current error `err` equals to error `target`. // Please note that, in default comparison for `Error`, // the errors are considered the same if both the `code` and `text` of them are the same. func Equal(err, target error) bool ``` ### 接口定义 如果自定义的错误数据结构需要支持比较,需要自定义的错误结构实现以下接口: ```go // IEqual is the interface for Equal feature. type IEqual interface { Error() string Equal(target error) bool } ``` 通过`GoFrame`框架错误组件创建的错误对象已经实现了该接口,组件默认比较逻辑判断错误码和错误信息。 **说明** 需要注意,如果两个错误均不带错误码,并且错误信息相等,那么组件认为两个错误相等。 ### 使用示例 ```go func ExampleEqual() { err1 := errors.New("permission denied") err2 := gerror.New("permission denied") err3 := gerror.NewCode(gcode.CodeNotAuthorized, "permission denied") fmt.Println(gerror.Equal(err1, err2)) fmt.Println(gerror.Equal(err2, err3)) // Output: // true // false } ``` ## `Is`判断错误链中是否包含目标错误 - 说明:`Is`方法用于判断错误链中是否包含特定的目标错误。该方法是对`Go`标准库`errors.Is`的封装,保证了与标准库相同的性能。 - 格式: ```go Is(err, target error) bool ``` - 示例: ```go func ExampleIs() { // 创建一个特定的错误作为目标 targetErr := gerror.New("target error") // 包装错误 err1 := gerror.Wrap(targetErr, "wrap error 1") err2 := gerror.Wrap(err1, "wrap error 2") // 使用Is判断是否包含目标错误 fmt.Println(gerror.Is(err2, targetErr)) // 与不相关的错误进行判断 otherErr := gerror.New("other error") fmt.Println(gerror.Is(err2, otherErr)) // Output: // true // false } ``` ## `As`将错误转换为特定类型 - 说明:`As`方法用于将错误链中的错误转换为特定类型。它会在错误链中查找第一个与目标**类型匹配**的错误,如果找到,则将其转换为目标类型并返回`true`。该方法是对`Go`标准库`errors.As`的封装,保证了与标准库相同的性能。 - 格式: ```go As(err error, target any) bool ``` - 示例: ```go // 自定义错误类型 type MyError struct { Message string Code int } // 实现error接口 func (e *MyError) Error() string { return fmt.Sprintf("code: %d, message: %s", e.Code, e.Message) } func ExampleAs() { // 创建一个自定义错误 originalErr := &MyError{Message: "database connection failed", Code: 500} // 包装错误 err := gerror.Wrap(originalErr, "operation failed") // 尝试将错误转换为MyError类型 var myErr *MyError if gerror.As(err, &myErr) { fmt.Println("Error code:", myErr.Code) fmt.Println("Error message:", myErr.Message) } // 尝试转换为不匹配的类型 type OtherError struct{} var otherErr *OtherError fmt.Println(gerror.As(err, &otherErr)) // Output: // Error code: 500 // Error message: database connection failed // false } ``` `As` 方法在以下场景中特别有用: 1. **类型断言**:当需要从错误链中提取特定类型的错误信息时。 2. **错误处理**:根据错误类型执行不同的处理逻辑。 3. **自定义错误恢复**:当需要从错误中提取特定信息来实现错误恢复机制时。 下面是一个实际应用的例子: ```go package main import ( "fmt" "github.com/gogf/gf/v2/errors/gerror" ) // 数据库错误 type DBError struct { Table string Message string } func (e *DBError) Error() string { return fmt.Sprintf("database error on table '%s': %s", e.Table, e.Message) } // 认证错误 type AuthError struct { Username string Message string } func (e *AuthError) Error() string { return fmt.Sprintf("authentication error for user '%s': %s", e.Username, e.Message) } // 模拟数据库操作 func queryDatabase() error { return gerror.Wrap(&DBError{ Table: "users", Message: "connection timeout", }, "database query failed") } // 模拟用户认证 func authenticateUser() error { return gerror.Wrap(&AuthError{ Username: "john_doe", Message: "invalid password", }, "authentication failed") } // 处理不同类型的错误 func handleError(err error) { // 尝试将错误转换为数据库错误 var dbErr *DBError if gerror.As(err, &dbErr) { fmt.Println("Database error detected:") fmt.Println(" Table:", dbErr.Table) fmt.Println(" Message:", dbErr.Message) fmt.Println(" Recommended action: Check database connection and retry") return } // 尝试将错误转换为认证错误 var authErr *AuthError if gerror.As(err, &authErr) { fmt.Println("Authentication error detected:") fmt.Println(" Username:", authErr.Username) fmt.Println(" Message:", authErr.Message) fmt.Println(" Recommended action: Verify credentials and try again") return } // 其他错误类型 fmt.Println("Unknown error:", err.Error()) } func main() { // 模拟数据库错误 err1 := queryDatabase() handleError(err1) fmt.Println() // 模拟认证错误 err2 := authenticateUser() handleError(err2) } ``` 运行结果: ```text Database error detected: Table: users Message: connection timeout Recommended action: Check database connection and retry Authentication error detected: Username: john_doe Message: invalid password Recommended action: Verify credentials and try again ``` # 错误处理-国际化支持 ## 基本介绍 **提示**:版本支持 该特性从`v2.10.0`版本开始支持。 在开发多语言应用时,错误消息的国际化是一个常见需求。`GoFrame`框架的`gerror`组件从`v2.10.0`版本开始,提供了`ITextArgs`接口来支持错误信息的国际化场景。 该接口允许错误对象分别暴露**错误消息模板**和**格式化参数**,使得开发者可以: 1. 获取原始的错误消息模板(未格式化) 2. 获取模板中的格式化参数 3. 根据不同语言环境翻译模板 4. 使用参数重新格式化翻译后的消息 这种设计避免了错误消息在创建时就被格式化,确保了国际化场景下的灵活性。 ## 接口定义 ### ITextArgs 接口 ```go // ITextArgs is the interface for Text and Args features. // This interface is mainly used for i18n features, that needs text and args separately. type ITextArgs interface { error Text() string Args() []any } ``` ### 核心方法 `gerror.Error` 对象实现了 `ITextArgs` 接口,提供以下方法: | 方法 | 返回类型 | 说明 | | --- | --- | --- | | `Text()` | `string` | 返回错误消息的格式化模板字符串(未进行参数替换)。例如:`"user %s not found with id: %d"` | | `Args()` | `[]any` | 返回用于格式化的参数列表。例如:`["john", 123]` | | `TextWithArgs()` | `string` | 返回完整的格式化后的错误消息,等同于 `Error()` 方法。例如:`"user john not found with id: 123"` | ## 使用场景 ### 1. 国际化(i18n) 在多语言应用中,可以获取错误消息模板,在不同语言环境下翻译后再进行格式化: ```go package main import ( "context" "errors" "fmt" "github.com/gogf/gf/v2/errors/gerror" "github.com/gogf/gf/v2/i18n/gi18n" ) func main() { // 创建一个带参数的错误 err := gerror.Newf("user %s not found with id: %d", "john", 123) // 获取错误消息模板和参数 var e gerror.ITextArgs if errors.As(err, &e) { var ( format = e.Text() // "user %s not found with id: %d" args = e.Args() // ["john", 123] ctx = context.Background() i18n = gi18n.New(gi18n.Options{ Language: "zh-CN", }) ) // 在国际化场景中,可以先翻译模板,再进行格式化 translated := i18n.Tf(ctx, format, args...) fmt.Println(translated) } } ``` ### 2. 结构化错误处理 在日志系统或监控系统中,可以单独记录错误模板和参数,便于后续分析: ```go package main import ( "context" "errors" "github.com/gogf/gf/v2/errors/gerror" "github.com/gogf/gf/v2/frame/g" ) func main() { var ( ctx = context.Background() err = gerror.Newf("failed to connect to database %s:%d", "localhost", 3306) e gerror.ITextArgs ) if errors.As(err, &e) { // 记录结构化日志 g.Log().Error(ctx, g.Map{ "error_template": e.Text(), "error_args": e.Args(), "error_message": e.Error(), }) } } ``` ### 3. 错误分析与聚合 在错误收集系统中,可以按错误模板进行聚合统计,而不是按完整错误消息: ```go package main import ( "github.com/gogf/gf/v2/errors/gerror" ) // 错误统计器 type ErrorStats struct { counts map[string]int } func (s *ErrorStats) Record(err error) { if e, ok := err.(gerror.ITextArgs); ok { // 按模板统计,而不是按完整消息 template := e.Text() s.counts[template]++ } } ``` ## 完整示例 ### 基本使用 ```go package main import ( "errors" "fmt" "github.com/gogf/gf/v2/errors/gerror" ) func main() { // 创建一个带参数的错误 err := gerror.Newf("user %s not found with id: %d", "john", 123) // 获取完整的错误消息 fmt.Println("Error:", err.Error()) // 输出: Error: user john not found with id: 123 // 获取错误消息模板和参数 var e gerror.ITextArgs if errors.As(err, &e) { fmt.Println("Template:", e.Text()) // 输出: Template: user %s not found with id: %d fmt.Println("Args:", e.Args()) // 输出: Args: [john 123] } } ``` ### 国际化翻译示例 以下示例展示了如何结合`ITextArgs`接口实现错误消息的多语言翻译: ```go package main import ( "errors" "fmt" "github.com/gogf/gf/v2/errors/gcode" "github.com/gogf/gf/v2/errors/gerror" ) // 简单的翻译映射 var translations = map[string]map[string]string{ "en": { "user %s not found with id: %d": "user %s not found with id: %d", "failed to connect to database %s:%d": "failed to connect to database %s:%d", "invalid parameter: %s must be %s": "invalid parameter: %s must be %s", "operation timeout after %d seconds": "operation timeout after %d seconds", }, "zh": { "user %s not found with id: %d": "用户 %s 未找到,ID: %d", "failed to connect to database %s:%d": "无法连接到数据库 %s:%d", "invalid parameter: %s must be %s": "无效参数:%s 必须是 %s", "operation timeout after %d seconds": "操作超时,已等待 %d 秒", }, "ja": { "user %s not found with id: %d": "ユーザー %s が見つかりません、ID: %d", "failed to connect to database %s:%d": "データベース %s:%d への接続に失敗しました", "invalid parameter: %s must be %s": "無効なパラメータ: %s は %s である必要があります", "operation timeout after %d seconds": "%d 秒後に操作がタイムアウトしました", }, } // 翻译函数 func translate(template string, lang string) string { if langMap, ok := translations[lang]; ok { if trans, ok := langMap[template]; ok { return trans } } // 如果没有找到翻译,返回原模板 return template } // TranslateError 将错误翻译为指定语言 func TranslateError(err error, lang string) string { var e gerror.ITextArgs if errors.As(err, &e) { // 获取模板和参数 template := e.Text() args := e.Args() // 翻译模板 translatedTemplate := translate(template, lang) // 使用参数格式化翻译后的模板 return fmt.Sprintf(translatedTemplate, args...) } // 如果不支持 ITextArgs 接口,直接返回错误消息 return err.Error() } func main() { // 创建不同类型的错误 allErrors := []error{ gerror.Newf("user %s not found with id: %d", "john", 123), gerror.NewCodef(gcode.CodeInvalidParameter, "invalid parameter: %s must be %s", "age", "positive"), gerror.Newf("operation timeout after %d seconds", 30), } // 测试不同语言的翻译 languages := []string{"en", "zh", "ja"} for _, err := range allErrors { fmt.Println("\n原始错误:", err.Error()) for _, lang := range languages { translated := TranslateError(err, lang) fmt.Printf(" %s: %s\n", lang, translated) } } } ``` 输出示例: ```text 原始错误: user john not found with id: 123 en: user john not found with id: 123 zh: 用户 john 未找到,ID: 123 ja: ユーザー john が見つかりません、ID: 123 原始错误: invalid parameter: age must be positive en: invalid parameter: age must be positive zh: 无效参数:age 必须是 positive ja: 無効なパラメータ: age は positive である必要があります 原始错误: operation timeout after 30 seconds en: operation timeout after 30 seconds zh: 操作超时,已等待 30 秒 ja: 30 秒後に操作がタイムアウトしました ``` ### 与 gi18n 组件结合 在实际项目中,可以结合 `GoFrame` 的 `gi18n` 国际化组件使用: ```go package main import ( "context" "errors" "fmt" "github.com/gogf/gf/v2/errors/gerror" "github.com/gogf/gf/v2/i18n/gi18n" "github.com/gogf/gf/v2/os/gctx" ) // TranslateErrorWithI18n 使用 gi18n 翻译错误 func TranslateErrorWithI18n(ctx context.Context, err error) string { var e gerror.ITextArgs if errors.As(err, &e) { // 获取模板和参数 var ( format = e.Text() args = e.Args() ) // 使用参数格式化翻译后的模板 return gi18n.Tf(ctx, format, args...) } return err.Error() } func main() { var ctx = gctx.New() // 设置语言 ctx = gi18n.WithLanguage(ctx, "zh-CN") // 创建错误 err := gerror.Newf("user %s not found with id: %d", "john", 123) // 翻译错误 translated := TranslateErrorWithI18n(ctx, err) fmt.Println(translated) } ``` # 错误处理-其他特性 ## `NewWithOption`自定义错误创建 - 说明:用于自定义配置的错误对象创建。 - 格式: ```go // NewWithOption creates and returns a custom error with Option. // It is the senior usage for creating error, which is often used internally in framework. NewWithOption(option Option) error ``` - 示例: ```go func ExampleNewWithOption() { err := gerror.NewWithOption(gerror.Option{ Text: "this feature is disabled in this storage", Code: gcode.CodeNotSupported, }) } ``` 其中,`Option`的定义如下: ```go // Option is option for creating error. type Option struct { Error error // Wrapped error if any. Stack bool // Whether recording stack information into error. Text string // Error text, which is created by New* functions. Code gcode.Code // Error code if necessary. } ``` ## `fmt`格式化 通过前面示例我们可以看到,通过`%+v`的打印格式可以打印出完整的堆栈信息,当然`gerror.Error`对象支持多种`fmt`格式: | 格式符 | 输出内容 | | --- | --- | | `%v`, `%s` | 打印所有的层级错误信息,构成完成的字符串返回,多个层级使用 `:` 拼接。 | | `%-v`, `%-s` | 打印当前层级的错误信息,返回字符串。 | | `%+s` | 打印完整的堆栈信息列表。 | | `%+v` | 打印所有的层级错误信息字符串,以及完整的堆栈信息,等同于 `%s\n%+s`。 | 使用示例: ```go package main import ( "errors" "fmt" "github.com/gogf/gf/v2/errors/gerror" ) func main() { var err error err = errors.New("sql error") err = gerror.Wrap(err, "adding failed") err = gerror.Wrap(err, "api calling failed") fmt.Printf(" %%s: %s\n", err) fmt.Printf("%%-s: %-s\n", err) fmt.Println("%+s: ") fmt.Printf("%+s\n", err) } ``` 执行后,输出示例: ```text %s: api calling failed: adding failed: sql error %-s: api calling failed %+s: 1. api calling failed 1). main.main /Users/john/Workspace/Go/GOPATH/src/github.com/gogf/gf/.example/other/test.go:14 2. adding failed 1). main.main /Users/john/Workspace/Go/GOPATH/src/github.com/gogf/gf/.example/other/test.go:13 3. sql error ``` ## 日志组件支持 `glog`日志组件天然支持对`gerror`错误堆栈打印支持,这种支持不是强耦合性的,而是通过`fmt`格式化打印接口支持的。 使用示例: ```go package main import ( "errors" "github.com/gogf/gf/v2/frame/g" "github.com/gogf/gf/v2/errors/gerror" ) func main() { var err error err = errors.New("sql error") err = gerror.Wrap(err, "adding failed") err = gerror.Wrap(err, "api calling failed") g.Log().Printf("%+v", err) } ``` 执行后,输出示例: ```text 2020-10-17 15:22:26.793 api calling failed: adding failed: sql error 1. api calling failed 1). main.main /Users/john/Workspace/Go/GOPATH/src/github.com/gogf/gf/.example/other/test.go:14 2. adding failed 1). main.main /Users/john/Workspace/Go/GOPATH/src/github.com/gogf/gf/.example/other/test.go:13 3. sql error ``` ## 堆栈打印模式 该特性从框架`v2.6.0`版本开始提供。 错误组件在打印堆栈信息时,支持使用者通过环境变量(`GF_GERROR_STACK_MODE`)或者命令行启动参数(`gf.gerror.stack.mode`)指定堆栈打印信息模式: | 堆栈模式 | 是否默认 | 说明 | | --- | --- | --- | | `brief` | 是 | **简略模式**。错误堆栈打印时,不会打印框架相关的堆栈。 | | `detail` | | **详情模式**。错误堆栈打印时,会打印完整的框架组件代码调用链路。 | **在详情模式( `detail`)下,将会打印错误对象中完整的框架堆栈信息。** 比如,在以下错误堆栈示例中,可以看到,大部分框架堆栈信息对于错误定位来说,其实是没有太大意义的,作为开发者,更多关心的自身代码层面的堆栈信息。 ```text 2022-10-08 21:07:00.751 [ERRO] {328d1204e2191c179a09086890c857b8} request done, cost: 3 ms, code: -1, message: "", detail: , error: GetParams failed: {ResourceId:tdxxxx-a2c378bd Component: Version:0}: rpc error: code = NotFound desc = cluster.khaos.tencent.com "tdxxxx-a2c378bd" not found 1. GetParams failed: {ResourceId:tdxxxx-a2c378bd Component: Version:0} 1). github.com/khaos/eros/app/khaos-oss/internal/logic/params.(*sParams).doGetParamsJson /root/workspace/khaos/eros/app/khaos-oss/internal/logic/params/params.go:66 2). github.com/khaos/eros/app/khaos-oss/internal/logic/params.(*sParams).GetParams /root/workspace/khaos/eros/app/khaos-oss/internal/logic/params/params.go:36 3). github.com/khaos/eros/app/khaos-oss/internal/controller.(*cParams).GetOne /root/workspace/khaos/eros/app/khaos-oss/internal/controller/params.go:21 4). github.com/gogf/gf/v2/net/ghttp.(*middleware).callHandlerFunc.func1 /root/go/pkg/mod/github.com/gogf/gf/v2@v2.1.4/net/ghttp/ghttp_request_middleware.go:152 5). github.com/gogf/gf/v2/net/ghttp.niceCallFunc /root/go/pkg/mod/github.com/gogf/gf/v2@v2.1.4/net/ghttp/ghttp_func.go:55 6). github.com/gogf/gf/v2/net/ghttp.(*middleware).callHandlerFunc /root/go/pkg/mod/github.com/gogf/gf/v2@v2.1.4/net/ghttp/ghttp_request_middleware.go:129 7). github.com/gogf/gf/v2/net/ghttp.(*middleware).Next.func1 /root/go/pkg/mod/github.com/gogf/gf/v2@v2.1.4/net/ghttp/ghttp_request_middleware.go:75 8). github.com/gogf/gf/v2/util/gutil.TryCatch /root/go/pkg/mod/github.com/gogf/gf/v2@v2.1.4/util/gutil/gutil.go:56 9). github.com/gogf/gf/v2/net/ghttp.(*middleware).Next /root/go/pkg/mod/github.com/gogf/gf/v2@v2.1.4/net/ghttp/ghttp_request_middleware.go:49 10). github.com/khaos/eros/app/khaos-oss/internal/logic/middleware.(*sMiddleware).CheckLimit /root/workspace/khaos/eros/app/khaos-oss/internal/logic/middleware/middleware.go:27 11). github.com/gogf/gf/v2/net/ghttp.(*middleware).Next.func1.5 /root/go/pkg/mod/github.com/gogf/gf/v2@v2.1.4/net/ghttp/ghttp_request_middleware.go:96 12). github.com/gogf/gf/v2/net/ghttp.niceCallFunc /root/go/pkg/mod/github.com/gogf/gf/v2@v2.1.4/net/ghttp/ghttp_func.go:55 13). github.com/gogf/gf/v2/net/ghttp.(*middleware).Next.func1 /root/go/pkg/mod/github.com/gogf/gf/v2@v2.1.4/net/ghttp/ghttp_request_middleware.go:95 14). github.com/gogf/gf/v2/util/gutil.TryCatch /root/go/pkg/mod/github.com/gogf/gf/v2@v2.1.4/util/gutil/gutil.go:56 15). github.com/gogf/gf/v2/net/ghttp.(*middleware).Next /root/go/pkg/mod/github.com/gogf/gf/v2@v2.1.4/net/ghttp/ghttp_request_middleware.go:49 16). github.com/khaos/eros/utility/server.MiddlewareCommonResponse /root/workspace/khaos/eros/utility/server/server_common.go:14 17). github.com/gogf/gf/v2/net/ghttp.(*middleware).Next.func1.5 /root/go/pkg/mod/github.com/gogf/gf/v2@v2.1.4/net/ghttp/ghttp_request_middleware.go:96 18). github.com/gogf/gf/v2/net/ghttp.niceCallFunc /root/go/pkg/mod/github.com/gogf/gf/v2@v2.1.4/net/ghttp/ghttp_func.go:55 19). github.com/gogf/gf/v2/net/ghttp.(*middleware).Next.func1 /root/go/pkg/mod/github.com/gogf/gf/v2@v2.1.4/net/ghttp/ghttp_request_middleware.go:95 20). github.com/gogf/gf/v2/util/gutil.TryCatch /root/go/pkg/mod/github.com/gogf/gf/v2@v2.1.4/util/gutil/gutil.go:56 21). github.com/gogf/gf/v2/net/ghttp.(*middleware).Next /root/go/pkg/mod/github.com/gogf/gf/v2@v2.1.4/net/ghttp/ghttp_request_middleware.go:49 22). github.com/khaos/eros/utility/server.MiddlewareLogging /root/workspace/khaos/eros/utility/server/server.go:46 23). github.com/gogf/gf/v2/net/ghttp.(*middleware).Next.func1.5 /root/go/pkg/mod/github.com/gogf/gf/v2@v2.1.4/net/ghttp/ghttp_request_middleware.go:96 24). github.com/gogf/gf/v2/net/ghttp.niceCallFunc /root/go/pkg/mod/github.com/gogf/gf/v2@v2.1.4/net/ghttp/ghttp_func.go:55 25). github.com/gogf/gf/v2/net/ghttp.(*middleware).Next.func1 /root/go/pkg/mod/github.com/gogf/gf/v2@v2.1.4/net/ghttp/ghttp_request_middleware.go:95 26). github.com/gogf/gf/v2/util/gutil.TryCatch /root/go/pkg/mod/github.com/gogf/gf/v2@v2.1.4/util/gutil/gutil.go:56 27). github.com/gogf/gf/v2/net/ghttp.(*middleware).Next /root/go/pkg/mod/github.com/gogf/gf/v2@v2.1.4/net/ghttp/ghttp_request_middleware.go:49 28). github.com/gogf/gf/v2/net/ghttp.MiddlewareCORS /root/go/pkg/mod/github.com/gogf/gf/v2@v2.1.4/net/ghttp/ghttp_middleware_cors.go:12 29). github.com/gogf/gf/v2/net/ghttp.(*middleware).Next.func1.5 /root/go/pkg/mod/github.com/gogf/gf/v2@v2.1.4/net/ghttp/ghttp_request_middleware.go:96 30). github.com/gogf/gf/v2/net/ghttp.niceCallFunc /root/go/pkg/mod/github.com/gogf/gf/v2@v2.1.4/net/ghttp/ghttp_func.go:55 2. rpc error: code = NotFound desc = cluster.khaos.tencent.com "tdxxxx-a2c378bd" not found ``` 而**在简略模式( `brief`)下,堆栈信息中将不会打印框架的堆栈信息**。例如: ```text 2022-10-08 21:07:00.751 [ERRO] {328d1204e2191c179a09086890c857b8} request done, cost: 3 ms, code: -1, message: "", detail: , error: GetParams failed: {ResourceId:tdxxxx-a2c378bd Component: Version:0}: rpc error: code = NotFound desc = cluster.khaos.tencent.com "tdxxxx-a2c378bd" not found 1. GetParams failed: {ResourceId:tdxxxx-a2c378bd Component: Version:0} 1). github.com/khaos/eros/app/khaos-oss/internal/logic/params.(*sParams).doGetParamsJson /root/workspace/khaos/eros/app/khaos-oss/internal/logic/params/params.go:66 2). github.com/khaos/eros/app/khaos-oss/internal/logic/params.(*sParams).GetParams /root/workspace/khaos/eros/app/khaos-oss/internal/logic/params/params.go:36 3). github.com/khaos/eros/app/khaos-oss/internal/controller.(*cParams).GetOne /root/workspace/khaos/eros/app/khaos-oss/internal/controller/params.go:21 2. rpc error: code = NotFound desc = cluster.khaos.tencent.com "tdxxxx-a2c378bd" not found ``` # 错误处理-最佳实践 ## 打印错误对象中的堆栈信息 通过`fmt/glog`或者其他包打印错误对象时,默认情况下不会输出错误堆栈信息。例如: ```go package main import ( "fmt" "github.com/gogf/gf/v2/encoding/gjson" ) func main() { _, err := gjson.Encode(func() {}) fmt.Printf("err: %v", err) } ``` 执行后,终端输出: ```html err: json.Marshal failed: json: unsupported type: func() ``` 打印错误对象中的错误堆栈信息,应当使用`%+v`的格式化方式。例如: ```go package main import ( "fmt" "github.com/gogf/gf/v2/encoding/gjson" ) func main() { _, err := gjson.Encode(func() {}) fmt.Printf("err: %+v", err) } ``` 执行后,终端输出: ```text err: json.Marshal failed: json: unsupported type: func() 1. json.Marshal failed 1). github.com/gogf/gf/v2/internal/json.Marshal /Users/john/Workspace/Go/GOPATH/src/github.com/gogf/gf/internal/json/json.go:30 2). github.com/gogf/gf/v2/encoding/gjson.Encode /Users/john/Workspace/Go/GOPATH/src/github.com/gogf/gf/encoding/gjson/gjson_stdlib_json_util.go:41 3). main.main /Users/john/Workspace/Go/GOPATH/src/github.com/gogf/gf/.test/main.go:10 2. json: unsupported type: func() ``` ## 使用Wrap方法时,避免重复的错误信息打印 当对错误对象进行 `Wrap` 时,不要将错误对象打印到 `Wrap` 的错误信息中,因为 `Wrap` 时本身会将目标错误对象包裹到创建的新的错误对象内部。如果将错误信息再打印包含在错误字符串中,那么在打印错误堆栈的时候会出现错误信息重复。例如(为简化示例,这里没有打印堆栈信息): ```go package main import ( "fmt" "github.com/gogf/gf/v2/encoding/gjson" "github.com/gogf/gf/v2/errors/gerror" ) func main() { _, err1 := gjson.Encode(func() {}) err2 := gerror.Wrapf(err1, `error occurred: %v`, err1) fmt.Printf("err: %v", err2) } ``` 执行后,终端输出,可以看到,打印的错误信息出现了重复: ```text err: error occurred: json.Marshal failed: json: unsupported type: func(): json.Marshal failed: json: unsupported type: func() ``` 我们将上面的示例代码修复一下,如下: ```go package main import ( "fmt" "github.com/gogf/gf/v2/encoding/gjson" "github.com/gogf/gf/v2/errors/gerror" ) func main() { _, err1 := gjson.Encode(func() {}) err2 := gerror.Wrap(err1, `error occurred`) fmt.Printf("err: %v", err2) } ``` 执行后,终端输出: ```text err: error occurred: json.Marshal failed: json: unsupported type: func() ``` # 错误处理-性能测试 常用方法的基准性能测试:[https://github.com/gogf/gf/blob/master/errors/gerror/gerror_z_bench_test.go](https://github.com/gogf/gf/blob/master/errors/gerror/gerror_z_bench_test.go) ```bash $ go test *.go -bench=".*" -benchmem goos: linux goarch: amd64 Benchmark_New-4 1890454 589 ns/op 256 B/op 1 allocs/op Benchmark_Newf-4 1569258 762 ns/op 324 B/op 3 allocs/op Benchmark_Wrap-4 2012910 600 ns/op 256 B/op 1 allocs/op Benchmark_Wrapf-4 1600197 749 ns/op 324 B/op 3 allocs/op Benchmark_NewSkip-4 2009928 579 ns/op 256 B/op 1 allocs/op Benchmark_NewSkipf-4 1578370 752 ns/op 324 B/op 3 allocs/op Benchmark_NewCode-4 2060835 591 ns/op 256 B/op 1 allocs/op Benchmark_NewCodef-4 1603306 752 ns/op 324 B/op 3 allocs/op Benchmark_NewCodeSkip-4 2047794 584 ns/op 256 B/op 1 allocs/op Benchmark_NewCodeSkipf-4 1524495 779 ns/op 324 B/op 3 allocs/op Benchmark_WrapCode-4 1972147 603 ns/op 256 B/op 1 allocs/op Benchmark_WrapCodef-4 1651316 735 ns/op 324 B/op 3 allocs/op ``` # 错误处理-错误码特性 ## 延伸阅读 - [Go开发中如何优雅地使用错误码](https://goframe.org/articles/use-error-code-gracefully-in-golang) # 错误处理-错误码接口 ## 基本介绍 我们前面讲到的主要是`gerror`组件的使用,该组件主要用于错误管理。在后续章节中,我们主要讲解`gcode`组件的使用,该组件主要用于扩展`gerror`组件的能力,提供错误码的特性。`gcode`错误码组件使用了接口化设计,以提供高扩展性。 **使用方式**: ```go import "github.com/gogf/gf/v2/errors/gcode" ``` **接口文档**: [https://pkg.go.dev/github.com/gogf/gf/v2/errors/gcode](https://pkg.go.dev/github.com/gogf/gf/v2/errors/gcode) ## 接口定义 ```go // Code is universal error code interface definition. type Code interface { // Code returns the integer number of current error code. Code() int // Message returns the brief message for current error code. Message() string // Detail returns the detailed information of current error code, // which is mainly designed as an extension field for error code. Detail() interface{} } ``` ## 默认实现 框架提供了默认实现 `gcode.Code` 的结构体,开发者可以直接`gcode`组件的`New/WithCode`方法创建错误码: - 格式: ```go // New creates and returns an error code. // Note that it returns an interface object of Code. func New(code int, message string, detail interface{}) Code ``` - 示例: ```go func ExampleNew() { c := gcode.New(1, "custom error", "detailed description") fmt.Println(c.Code()) fmt.Println(c.Message()) fmt.Println(c.Detail()) // Output: // 1 // custom error // detailed description } ``` 如果开发者觉得框架默认实现 `gcode.Code` 的结构体不满足需求,可以自行定义,只需实现 `gcode.Code` 即可。 # 错误处理-错误码使用 ## 创建带错误码的`error` ### `NewCode/NewCodef` - 说明:功能同`New/Newf`方法,用于创建一个自定义错误信息的`error`对象,并包含堆栈信息,并增加错误码对象的输入。 - 格式: ```go // NewCode creates and returns an error that has error code and given text. NewCode(code gcode.Code, text ...string) error // NewCodef returns an error that has error code and formats as the given format and args. NewCodef(code gcode.Code, format string, args ...interface{}) error ``` - 实例: ```go func ExampleNewCode() { err := gerror.NewCode(gcode.New(10000, "", nil), "My Error") fmt.Println(err.Error()) fmt.Println(gerror.Code(err)) // Output: // My Error // 10000 } func ExampleNewCodef() { err := gerror.NewCodef(gcode.New(10000, "", nil), "It's %s", "My Error") fmt.Println(err.Error()) fmt.Println(gerror.Code(err).Code()) // Output: // It's My Error // 10000 } ``` ### `WrapCode/WrapCodef` - 说明:功能同`Wrap/Wrapf`方法,用于包裹其他错误`error`对象,构造成多级的错误信息,包含堆栈信息,并增加错误码参数的输入。 - 格式: ```go // WrapCode wraps error with code and text. // It returns nil if given err is nil. WrapCode(code gcode.Code, err error, text ...string) error // WrapCodef wraps error with code and format specifier. // It returns nil if given `err` is nil. WrapCodef(code gcode.Code, err error, format string, args ...interface{}) error ``` - 示例: ```go func ExampleWrapCode() { err1 := errors.New("permission denied") err2 := gerror.WrapCode(gcode.New(10000, "", nil), err1, "Custom Error") fmt.Println(err2.Error()) fmt.Println(gerror.Code(err2).Code()) // Output: // Custom Error: permission denied // 10000 } func ExampleWrapCodef() { err1 := errors.New("permission denied") err2 := gerror.WrapCodef(gcode.New(10000, "", nil), err1, "It's %s", "Custom Error") fmt.Println(err2.Error()) fmt.Println(gerror.Code(err2).Code()) // Output: // It's Custom Error: permission denied // 10000 } ``` ### `NewCodeSkip/NewCodeSkipf` - 说明:功能同`NewCode/NewCodef`,用于创建一个带错误码的`error`对象,但忽略部分堆栈信息(按照当前调用方法位置往上忽略)。 - 格式: ```go // NewCodeSkip creates and returns an error which has error code and is formatted from given text. // The parameter `skip` specifies the stack callers skipped amount. func NewCodeSkip(code, skip int, text string) error // NewCodeSkipf returns an error that has error code and formats as the given format and args. // The parameter `skip` specifies the stack callers skipped amount. func NewCodeSkipf(code, skip int, format string, args ...interface{}) error ``` ### `WrapCodeSkip/WrapCodeSkipf` - 说明:功能同 `WrapCodeSkip/WrapCodeSkipf`,用于包裹一个带错误码的 `error` 对象,但忽略部分堆栈信息(按照当前调用方法位置往上忽略)。 - 格式: ```go // WrapCodeSkip wraps error with code and text. // It returns nil if given err is nil. // The parameter `skip` specifies the stack callers skipped amount. func WrapCodeSkip(code gcode.Code, skip int, err error, text ...string) error // WrapCodeSkipf wraps error with code and text that is formatted with given format and args. // It returns nil if given err is nil. // The parameter `skip` specifies the stack callers skipped amount. func WrapCodeSkipf(code gcode.Code, skip int, err error, format string, args ...interface{}) error ``` ## `Code` - 说明:获取错误对象中的错误码。该方法会递归获取,如果当前错误对象不存在错误码时,该方法会追溯查找其上一级错误对象的错误码,以此类推。当给定的`error`参数不带有错误码信息时,该方法返回预定义的错误码`gcode.CodeNil`。 - 格式: ```go // Code returns the error code of `current error`. // It returns `CodeNil` if it has no error code neither it does not implement interface Code. func Code(err error) gcode.Code ``` - 示例: ```go func ExampleCode() { err1 := gerror.NewCode(gcode.CodeInternalError, "permission denied") err2 := gerror.Wrap(err1, "operation failed") fmt.Println(gerror.Code(err1)) fmt.Println(gerror.Code(err2)) // Output: // 50:Internal Error // 50:Internal Error } ``` ## `HasCode` - 说明:该方法判断给定的错误对象中是否包含指定的错误码。与`Code`方法类似,该方法也会追溯查找上一级错误的错误码。 - 格式: ```go // HasCode checks and reports whether `err` has `code` in its chaining errors. func HasCode(err error, code gcode.Code) bool ``` - 示例: ```go func ExampleHasCode() { err1 := gerror.NewCode(gcode.CodeInternalError, "permission denied") err2 := gerror.Wrap(err1, "operation failed") fmt.Println(gerror.HasCode(err1, gcode.CodeOK)) fmt.Println(gerror.HasCode(err2, gcode.CodeInternalError)) // Output: // false // true } ``` # 错误处理-错误码扩展 当业务需要复杂的错误码定义时,我们推荐灵活使用错误码的`Detail`参数来扩展错误码功能。 我们来看个示例,该示例主要实现以下几个功能: - 使用一个中间件识别路由函数的执行结果,并获取路由函数返回的错误码信息,将该错误码信息作为返回结果的一部分返回给调用端。 - 如果产生了错误,会记录产生该错误的当前用户信息到日志中。该功能使用`Detail`扩展参数实现。 ## 业务错误码 ### 错误码定义 ```go type BizCode struct { User User // ... } type User struct { Id int Name string // ... } ``` ### 错误码使用 扩展错误码大多数场景下需要使用 `gcode.WithCode` 方法: ```go // WithCode creates and returns a new error code based on given Code. // The code and message is from given `code`, but the detail if from given `detail`. func WithCode(code Code, detail interface{}) Code ``` 因此上面我们的自定义扩展可以这么使用: ```go code := gcode.WithCode(gcode.CodeNotFound, BizCode{ User: User{ Id: 1, Name: "John", }, }) fmt.Println(code) ``` 即在错误码中我们可以根据业务场景注入一些自定义的错误码扩展数据,以方便上层获取错误码后做进一步处理。 ## 改写中间件 我们将上面自定义的错误码应用到请求返回中间件中,顶层业务逻辑也可以获取到错误码对应的详情再进一步做相关的业务处理。中间件返回的数据结构也可以自定义重写,例如其中返回的 `code` 字段也不一定是整形数值,可以完全自定义。 ```go func ResponseHandler(r *ghttp.Request) { r.Middleware.Next() // There's custom buffer content, it then exits current handler. if r.Response.BufferLength() > 0 { return } var ( err = r.GetError() res = r.GetHandlerResponse() code = gerror.Code(err) ) if code == gcode.CodeNil && err != nil { code = gcode.CodeInternalError } if detail, ok := code.Detail().(BizCode); ok { g.Log().Errorf(r.Context(), `error caused by user "%+v"`, detail.User) } r.Response.WriteJson(ghttp.DefaultHandlerResponse{ Code: gcode.CodeOK.Code(), Message: gcode.CodeOK.Message(), Data: res, }) } ``` **提示** 在框架`Server`默认的日志中会自动打印`Detail`数据。 # 错误处理-错误码实现 当业务需要更复杂的错误码定义时,我们可以自定义实现业务的错误码,只需要实现`gcode.Code`相关的接口即可。 我们来看个例子。 ## 自定义错误码 定义基础的业务错误码结构体,实现`gcode.Code`接口。 ```go type BizCode struct { code int message string detail BizCodeDetail } type BizCodeDetail struct { Code string HttpCode int } func (c BizCode) BizDetail() BizCodeDetail { return c.detail } func (c BizCode) Code() int { return c.code } func (c BizCode) Message() string { return c.message } func (c BizCode) Detail() interface{} { return c.detail } func New(httpCode int, code string, message string) gcode.Code { return BizCode{ code: 0, message: message, detail: BizCodeDetail{ Code: code, HttpCode: httpCode, }, } } ``` 定义业务错误码 ```go var ( CodeNil = New(200, "OK", "") CodeNotFound = New(404, "Not Found", "Resource does not exist") CodeInternal = New(500, "Internal Error", "An error occurred internally") // ... ) ``` ## 使用到中间件 ```go func ResponseHandler(r *ghttp.Request) { r.Middleware.Next() // There's custom buffer content, it then exits current handler. if r.Response.BufferLength() > 0 { return } var ( err = r.GetError() res = r.GetHandlerResponse() code = gerror.Code(err) ) if code == gcode.CodeNil && err != nil { code = CodeInternal } else { code = CodeNil } if bizCode, ok := code.(BizCode); ok { r.Response.WriteStatus(bizCode.BizDetail().HttpCode) } r.Response.WriteJson(g.Map{ `code`: gcode.CodeOK.Code(), `message`: gcode.CodeOK.Message(), `data`: res, }) } ``` # 错误处理-内置错误码 框架预定义了一些常见的错误码,开发者可直接使用: [https://github.com/gogf/gf/blob/master/errors/gcode/gcode.go](https://github.com/gogf/gf/blob/master/errors/gcode/gcode.go) **注意** 需要注意,如果业务使用整型错误码,`<1000`的错误码都被框架预留,业务错误码请定义`>1000`。 # 数据校验 ## 基本介绍 GoFrame 框架提供了功能强大、使用便捷、灵活易扩展的数据和表单校验组件,由 `gvalid` 组件实现。`gvalid` 组件实现了非常强大的数据校验功能,内置了数十种常用的校验规则,支持单数据多规则校验、多数据多规则批量校验、自定义错误信息、自定义正则校验、自定义校验规则注册、支持 `i18n` 国际化处理、支持 `struct tag` 规则及提示信息绑定等等特性,是目前功能最强大的 Go 数据校验模块。 **提示** 数据校验设计的灵感来源于经典的 `PHP Laravel` 框架 [https://laravel.com/docs/8.x/validation](https://laravel.com/docs/8.x/validation) 感谢 `Laravel` **使用方式**: ```go import "github.com/gogf/gf/v2/util/gvalid" ``` **接口文档**: [https://pkg.go.dev/github.com/gogf/gf/v2/util/gvalid](https://pkg.go.dev/github.com/gogf/gf/v2/util/gvalid) ## 组件特性 `gvalid` 组件具有以下显著特性: - 内置数十种常见数据校验规则,支持大部分业务场景 - 支持 `Server` 层及命令行组件自动化校验 - 支持基础类型及复杂对象类型参数校验 - 支持顺序校验、灵活的校验结果处理 - 支持自定义校验错误提示 - 支持结构体属性递归校验 - 支持自定义校验规则 - 支持 `I18n` 国际化特性 # 数据校验-校验规则 `GoFrame` 框架校验组件内置了数十项常用的校验规则,校验组件是开发者最频繁使用的框架核心组件之一。 **说明** 校验规则涉及到联合校验的场景时,规则中关联的 **参数名称** 会自动按照不区分大小写且忽略特殊字符的形式进行智能匹配。以下示例代码中我们均采用常用的结构体校验的方式,结构体属性中的 `v` 标签标识 `validation` 的缩写,用于设置该属性的校验规格。 ## 修饰规则 修饰规则本身没有任何的校验逻辑,而是修改后续功能规则的实现逻辑。 ### `ci` - 格式:`ci` - 说明:字段值比较在默认情况下为 **区分大小写** 严格匹配比较,通过 `ci(Case Insensitive)` 修饰规则,可以设置后续需要比较值的规则字段为 **不区分大小写**。如: `same`, `different`, `in`, `not-in` 等。 - 示例: ```go func Example_Rule_CaseInsensitive() { type BizReq struct { Account string `v:"required"` Password string `v:"required|ci|same:Password2"` Password2 string `v:"required"` } var ( ctx = context.Background() req = BizReq{ Account: "gf", Password: "Goframe.org", // Diff from Password2, but because of "ci", rule check passed Password2: "goframe.org", } ) if err := g.Validator().Data(req).Run(ctx); err != nil { fmt.Println(err) } // output: } ``` ### `bail` 需要特别注意:如果参数校验存在多条校验规则,并且规则中存在 `required*` 规则和 `bail` 修饰规则一起使用时,那么建议将 `required*` 校验规则放置于所有规则之前,否则 `bail` 校验规则的特性(校验失败即停止后续校验)有可能会引起后续规则的 `required*` 规则不生效。 - 格式:`bail` - 说明:只要后续的多个校验中有一个规则校验失败则停止校验并立即返回校验结果。 - 注意:在框架的 `HTTP Server` 组件中,如果采用规范路由注册方式,在其自动校验特性中将会自动开启 `bail` 修饰规则,开发者无需在 `Req` 对象中显式设置 `bail`。 - 示例: ```go func Example_Rule_Bail() { type BizReq struct { Account string `v:"bail|required|length:6,16|same:QQ"` QQ string Password string `v:"required|same:Password2"` Password2 string `v:"required"` } var ( ctx = context.Background() req = BizReq{ Account: "gf", QQ: "123456", Password: "goframe.org", Password2: "goframe.org", } ) if err := g.Validator().Data(req).Run(ctx); err != nil { fmt.Println(err) } // output: // The Account value `gf` length must be between 6 and 16 } ``` ### `foreach` - 格式:`foreach` - 说明:**用于数组参数**,将待检验的参数作为数组遍历,并将后一个校验规则应用于数组中的每一项。 - 版本:框架版本 `>=v2.2.0` - 示例: ```go func Example_Rule_Foreach() { type BizReq struct { Value1 []int `v:"foreach|in:1,2,3"` Value2 []int `v:"foreach|in:1,2,3"` } var ( ctx = context.Background() req = BizReq{ Value1: []int{1, 2, 3}, Value2: []int{3, 4, 5}, } ) if err := g.Validator().Bail().Data(req).Run(ctx); err != nil { fmt.Println(err.String()) } // Output: // The Value2 value `4` is not in acceptable range: 1,2,3 } ``` ## 功能规则 功能规则实现特定的校验逻辑,框架内置了非常丰富强大的内置校验规则。 ### `required` - 格式:`required` - 说明:必需参数,除了支持常见的字符串,也支持 `Slice/Map` 类型。 - 示例:姓名字段 `Name` 为必需参数必需不能为空。 ```go func Example_Rule_Required() { type BizReq struct { ID uint `v:"required"` Name string `v:"required"` } var ( ctx = context.Background() req = BizReq{ ID: 1, } ) if err := g.Validator().Data(req).Run(ctx); err != nil { fmt.Print(err) } // Output: // The Name field is required } ``` ### `required-if` - 格式:`required-if:field,value,...` - 说明:必需参数(当任意所给定字段值与所给值相等时,即:当 `field` 字段的值为 `value` 时,当前验证字段为必须参数)。多个字段以 `,` 号分隔。 - 示例:当 `Gender` 字段为 `1` 时 `WifeName` 字段必须不为空, 当 `Gender` 字段为 `2` 时 `HusbandName` 字段必须不为空。 ```go func Example_Rule_RequiredIf() { type BizReq struct { ID uint `v:"required" dc:"Your ID"` Name string `v:"required" dc:"Your name"` Gender uint `v:"in:0,1,2" dc:"0:Secret;1:Male;2:Female"` WifeName string `v:"required-if:gender,1"` HusbandName string `v:"required-if:gender,2"` } var ( ctx = context.Background() req = BizReq{ ID: 1, Name: "test", Gender: 1, } ) if err := g.Validator().Data(req).Run(ctx); err != nil { fmt.Println(err) } // Output: // The WifeName field is required } ``` ### `required-if-all` - 格式:`required-if-all:field1,value1,field2,value2,...` - 说明:必需参数(当所有所给定字段值与所给值相等时,即:当`field1`字段的值为`value1`,同时`field2`字段的值为`value2`,以此类推,当前验证字段为必须参数)。多个字段以 `,` 号分隔。 - 示例:当 `ID`字段为`1`,并且`Age`字段为`18`时,`MoreInfo`字段不能为空。 ```go func ExampleRule_RequiredIfAll() { type BizReq struct { ID uint `v:"required" dc:"Your ID"` Name string `v:"required" dc:"Your name"` Age int `v:"required" dc:"Your age"` MoreInfo string `v:"required-if-all:id,1,age,18" dc:"Your more info"` } var ( ctx = context.Background() req = BizReq{ ID: 1, Name: "test", Age: 18, } ) if err := g.Validator().Data(req).Run(ctx); err != nil { fmt.Println(err) } // Output: // The MoreInfo field is required } ``` ### `required-unless` - 格式:`required-unless:field,value,...` - 说明:必需参数(当所给定字段值与所给值都不相等时,即:当 `field` 字段的值不为 `value` 时,当前验证字段为必须参数)。多个字段以 `,` 号分隔。 - 示例:当 `Gender` 不等于 `0` 且 `Gender` 不等于 `2` 时, `WifeName` 必须不为空;当 `Id` 不等于 `0` 且 `Gender` 不等于 `2` 时, `HusbandName` 必须不为空。 ```go func Example_Rule_RequiredUnless() { type BizReq struct { ID uint `v:"required" dc:"Your ID"` Name string `v:"required" dc:"Your name"` Gender uint `v:"in:0,1,2" dc:"0:Secret;1:Male;2:Female"` WifeName string `v:"required-unless:gender,0,gender,2"` HusbandName string `v:"required-unless:id,0,gender,2"` } var ( ctx = context.Background() req = BizReq{ ID: 1, Name: "test", Gender: 1, } ) if err := g.Validator().Data(req).Run(ctx); err != nil { fmt.Println(err) } // Output: // The WifeName field is required; The HusbandName field is required } ``` ### `required-with` - 格式:`required-with:field1,field2,...` - 说明:必需参数(当所给定任意字段值其中之一不为空时)。 - 示例:当 `WifeName` 不为空时, `HusbandName` 必须不为空。 ```go func Example_Rule_RequiredWith() { type BizReq struct { ID uint `v:"required" dc:"Your ID"` Name string `v:"required" dc:"Your name"` Gender uint `v:"in:0,1,2" dc:"0:Secret;1:Male;2:Female"` WifeName string HusbandName string `v:"required-with:WifeName"` } var ( ctx = context.Background() req = BizReq{ ID: 1, Name: "test", Gender: 1, WifeName: "Ann", } ) if err := g.Validator().Data(req).Run(ctx); err != nil { fmt.Println(err) } // Output: // The HusbandName field is required } ``` ### `required-with-all` - 格式:`required-with-all:field1,field2,...` - 说明:必须参数(当所给定所有字段值全部都不为空时)。 - 示例:当 `Id,Name,Gender,WifeName` 全部不为空时, `HusbandName` 必须不为空。 ```go func Example_Rule_RequiredWithAll() { type BizReq struct { ID uint `v:"required" dc:"Your ID"` Name string `v:"required" dc:"Your name"` Gender uint `v:"in:0,1,2" dc:"0:Secret;1:Male;2:Female"` WifeName string HusbandName string `v:"required-with-all:Id,Name,Gender,WifeName"` } var ( ctx = context.Background() req = BizReq{ ID: 1, Name: "test", Gender: 1, WifeName: "Ann", } ) if err := g.Validator().Data(req).Run(ctx); err != nil { fmt.Println(err) } // Output: // The HusbandName field is required } ``` ### `required-without` - 格式:`required-without:field1,field2,...` - 说明:必需参数(当所给定任意字段值其中之一为空时)。 - 示例:当 `Id` 或 `WifeName` 为空时, `HusbandName` 必须不为空。 ```go func Example_Rule_RequiredWithout() { type BizReq struct { ID uint `v:"required" dc:"Your ID"` Name string `v:"required" dc:"Your name"` Gender uint `v:"in:0,1,2" dc:"0:Secret;1:Male;2:Female"` WifeName string HusbandName string `v:"required-without:Id,WifeName"` } var ( ctx = context.Background() req = BizReq{ ID: 1, Name: "test", Gender: 1, } ) if err := g.Validator().Data(req).Run(ctx); err != nil { fmt.Println(err) } // Output: // The HusbandName field is required } ``` ### `required-without-all` - 格式:`required-without-all:field1,field2,...` - 说明:必须参数(当所给定所有字段值全部都为空时)。 - 示例:当 `Id` 和 `WifeName` 都为空时, `HusbandName` 必须不为空。 ```go func Example_Rule_RequiredWithoutAll() { type BizReq struct { ID uint `v:"required" dc:"Your ID"` Name string `v:"required" dc:"Your name"` Gender uint `v:"in:0,1,2" dc:"0:Secret;1:Male;2:Female"` WifeName string HusbandName string `v:"required-without-all:Id,WifeName"` } var ( ctx = context.Background() req = BizReq{ Name: "test", Gender: 1, } ) if err := g.Validator().Data(req).Run(ctx); err != nil { fmt.Println(err) } // Output: // The HusbandName field is required } ``` ### `date` - 格式:`date` - 说明:参数为常用日期类型,日期之间支持的连接符号 `-` 或 `/` 或 `.`,也支持不带连接符号的 `8` 位长度日期,格式如: `2006-01-02`, `2006/01/02`, `2006.01.02`, `20060102` - 示例: ```go func Example_Rule_Date() { type BizReq struct { Date1 string `v:"date"` Date2 string `v:"date"` Date3 string `v:"date"` Date4 string `v:"date"` Date5 string `v:"date"` } var ( ctx = context.Background() req = BizReq{ Date1: "2021-10-31", Date2: "2021.10.31", Date3: "2021-Oct-31", Date4: "2021 Octa 31", Date5: "2021/Oct/31", } ) if err := g.Validator().Data(req).Run(ctx); err != nil { fmt.Print(gstr.Join(err.Strings(), "\n")) } // Output: // The Date3 value `2021-Oct-31` is not a valid date // The Date4 value `2021 Octa 31` is not a valid date // The Date5 value `2021/Oct/31` is not a valid date } ``` ### `datetime` - 格式:`datetime` - 说明:参数为常用日期时间类型,其中日期之间支持的连接符号只支持 `-`,格式如: `2006-01-02 12:00:00` - 示例: ```go func Example_Rule_Datetime() { type BizReq struct { Date1 string `v:"datetime"` Date2 string `v:"datetime"` Date3 string `v:"datetime"` Date4 string `v:"datetime"` } var ( ctx = context.Background() req = BizReq{ Date1: "2021-11-01 23:00:00", Date2: "2021-11-01 23:00", // error Date3: "2021/11/01 23:00:00", // error Date4: "2021/Dec/01 23:00:00", // error } ) if err := g.Validator().Data(req).Run(ctx); err != nil { fmt.Print(gstr.Join(err.Strings(), "\n")) } // Output: // The Date2 value `2021-11-01 23:00` is not a valid datetime // The Date3 value `2021/11/01 23:00:00` is not a valid datetime // The Date4 value `2021/Dec/01 23:00:00` is not a valid datetime } ``` ### `date-format` - 格式:`date-format:format` - 说明:判断日期是否为指定的日期/时间格式, `format` 参数格式为 `gtime` 日期格式(可以包含日期及时间),格式说明参考章节: [gtime模块](../components/os-gtime.md) - 示例: `date-format:Y-m-d H:i:s` ```go func Example_Rule_DateFormat() { type BizReq struct { Date1 string `v:"date-format:Y-m-d"` Date2 string `v:"date-format:Y-m-d"` Date3 string `v:"date-format:Y-m-d H:i:s"` Date4 string `v:"date-format:Y-m-d H:i:s"` } var ( ctx = context.Background() req = BizReq{ Date1: "2021-11-01", Date2: "2021-11-01 23:00", // error Date3: "2021-11-01 23:00:00", Date4: "2021-11-01 23:00", // error } ) if err := g.Validator().Data(req).Run(ctx); err != nil { fmt.Print(gstr.Join(err.Strings(), "\n")) } // Output: // The Date2 value `2021-11-01 23:00` does not match the format: Y-m-d // The Date4 value `2021-11-01 23:00` does not match the format: Y-m-d H:i:s } ``` ### `before` - 格式:`before:field` - 说明:判断给定的日期/时间是否在指定字段的日期/时间之前。 - 版本:框架版本 `>=v2.2.0` ```go func Example_Rule_Before() { type BizReq struct { Time1 string `v:"before:Time3"` Time2 string `v:"before:Time3"` Time3 string } var ( ctx = context.Background() req = BizReq{ Time1: "2022-09-02", Time2: "2022-09-03", Time3: "2022-09-03", } ) if err := g.Validator().Data(req).Run(ctx); err != nil { fmt.Println(err.String()) } // Output: // The Time2 value `2022-09-03` must be before field Time3 value `2022-09-03` } ``` ### `before-equal` - 格式:`before-equal:field` - 说明:判断给定的日期/时间是否在指定字段的日期/时间之前,或者与指定字段的日期/时间相等。 - 版本:框架版本 `>=v2.2.0` ```go func Example_Rule_BeforeEqual() { type BizReq struct { Time1 string `v:"before-equal:Time3"` Time2 string `v:"before-equal:Time3"` Time3 string } var ( ctx = context.Background() req = BizReq{ Time1: "2022-09-02", Time2: "2022-09-01", Time3: "2022-09-01", } ) if err := g.Validator().Data(req).Run(ctx); err != nil { fmt.Print(gstr.Join(err.Strings(), "\n")) } // Output: // The Time1 value `2022-09-02` must be before or equal to field Time3 } ``` ### `after` - 格式:`after:field` - 说明:判断给定的日期/时间是否在指定字段的日期/时间之后。 - 版本:框架版本 `>=v2.2.0` ```go func Example_Rule_After() { type BizReq struct { Time1 string Time2 string `v:"after:Time1"` Time3 string `v:"after:Time1"` } var ( ctx = context.Background() req = BizReq{ Time1: "2022-09-01", Time2: "2022-09-01", Time3: "2022-09-02", } ) if err := g.Validator().Data(req).Run(ctx); err != nil { fmt.Println(err.String()) } // Output: // The Time2 value `2022-09-01` must be after field Time1 value `2022-09-01` } ``` ### `after-equal` - 格式:`after-equal:field` - 说明:判断给定的日期/时间是否在指定字段的日期/时间之后,或者与指定字段的日期/时间相等。 - 版本:框架版本 `>=v2.2.0` ```go func Example_Rule_AfterEqual() { type BizReq struct { Time1 string Time2 string `v:"after-equal:Time1"` Time3 string `v:"after-equal:Time1"` } var ( ctx = context.Background() req = BizReq{ Time1: "2022-09-02", Time2: "2022-09-01", Time3: "2022-09-02", } ) if err := g.Validator().Data(req).Run(ctx); err != nil { fmt.Print(gstr.Join(err.Strings(), "\n")) } // Output: // The Time2 value `2022-09-01` must be after or equal to field Time1 value `2022-09-02` } ``` ### `array` - 格式:`array` - 说明:判断给定的参数是否数组格式。如果给定的参数为 `JSON` 数组字符串,也将检验通过。 - 版本:框架版本 `>=v2.2.0` ```go func Example_Rule_Array() { type BizReq struct { Value1 string `v:"array"` Value2 string `v:"array"` Value3 string `v:"array"` Value4 []string `v:"array"` } var ( ctx = context.Background() req = BizReq{ Value1: "1,2,3", Value2: "[]", Value3: "[1,2,3]", Value4: []string{}, } ) if err := g.Validator().Data(req).Run(ctx); err != nil { fmt.Print(gstr.Join(err.Strings(), "\n")) } // Output: // The Value1 value `1,2,3` is not of valid array type } ``` ### `enums` - 格式:`enums` - 说明:校验提交的参数是否在字段类型的枚举值中。该规则需要结合 `gf gen enums` 命令一起使用,详情请参考: [枚举维护-gen enums](../cli/gen-enums.md) ```go func ExampleRule_Enums() { type Status string const ( StatusRunning Status = "Running" StatusOffline Status = "Offline" ) type BizReq struct { Id int `v:"required"` Name string `v:"required"` Status Status `v:"enums"` } var ( ctx = context.Background() req = BizReq{ Id: 1, Name: "john", Status: Status("Pending"), } ) if err := g.Validator().Data(req).Run(ctx); err != nil { fmt.Print(gstr.Join(err.Strings(), "\n")) } // May Output: // The Status value `Pending` should be in enums of: ["Running","Offline"] } ``` ### `email` - 格式:`email` - 说明:`EMAIL` 邮箱地址格式。 ```go func Example_Rule_Email() { type BizReq struct { MailAddr1 string `v:"email"` MailAddr2 string `v:"email"` MailAddr3 string `v:"email"` MailAddr4 string `v:"email"` } var ( ctx = context.Background() req = BizReq{ MailAddr1: "gf@goframe.org", MailAddr2: "gf@goframe", // error MailAddr3: "gf@goframe.org.cn", MailAddr4: "gf#goframe.org", // error } ) if err := g.Validator().Data(req).Run(ctx); err != nil { fmt.Print(gstr.Join(err.Strings(), "\n")) } // Output: // The MailAddr2 value `gf@goframe` is not a valid email address // The MailAddr4 value `gf#goframe.org` is not a valid email address } ``` ### `phone` - 格式:`phone` - 说明:大中国区手机号格式。 ```go func Example_Rule_Phone() { type BizReq struct { PhoneNumber1 string `v:"phone"` PhoneNumber2 string `v:"phone"` PhoneNumber3 string `v:"phone"` PhoneNumber4 string `v:"phone"` } var ( ctx = context.Background() req = BizReq{ PhoneNumber1: "13578912345", PhoneNumber2: "11578912345", // error 11x not exist PhoneNumber3: "11178912345", // error 171 not exit PhoneNumber4: "1357891234", // error len must be 11 } ) if err := g.Validator().Data(req).Run(ctx); err != nil { fmt.Print(gstr.Join(err.Strings(), "\n")) } // Output: // The PhoneNumber2 value `11578912345` is not a valid phone number // The PhoneNumber3 value `11178912345` is not a valid phone number // The PhoneNumber4 value `1357891234` is not a valid phone number } ``` ### `phone-loose` - 格式:`phone` - 说明:宽松的手机号验证,只要满足 `13、14、15、16、17、18、19` 开头的11位数字都可以通过验证。可用于非严格的业务场景。 ```go func Example_Rule_PhoneLoose() { type BizReq struct { PhoneNumber1 string `v:"phone-loose"` PhoneNumber2 string `v:"phone-loose"` PhoneNumber3 string `v:"phone-loose"` PhoneNumber4 string `v:"phone-loose"` } var ( ctx = context.Background() req = BizReq{ PhoneNumber1: "13578912345", PhoneNumber2: "11578912345", // error 11x not exist PhoneNumber3: "17178912345", PhoneNumber4: "1357891234", // error len must be 11 } ) if err := g.Validator().Data(req).Run(ctx); err != nil { fmt.Print(gstr.Join(err.Strings(), "\n")) } // Output: // The PhoneNumber2 value `11578912345` is invalid // The PhoneNumber4 value `1357891234` is invalid } ``` ### `telephone` - 格式:`telephone` - 说明:大中国区座机电话号码,」XXXX-XXXXXXX」、」XXXX-XXXXXXXX」、」XXX-XXXXXXX」、」XXX-XXXXXXXX」、」XXXXXXX」、」XXXXXXXX」。 ```go func Example_Rule_Telephone() { type BizReq struct { Telephone1 string `v:"telephone"` Telephone2 string `v:"telephone"` Telephone3 string `v:"telephone"` Telephone4 string `v:"telephone"` } var ( ctx = context.Background() req = BizReq{ Telephone1: "010-77542145", Telephone2: "0571-77542145", Telephone3: "20-77542145", // error Telephone4: "775421451", // error len must be 7 or 8 } ) if err := g.Validator().Data(req).Run(ctx); err != nil { fmt.Print(gstr.Join(err.Strings(), "\n")) } // Output: // The Telephone3 value `20-77542145` is not a valid telephone number // The Telephone4 value `775421451` is not a valid telephone number } ``` ### `passport` - 格式:`passport` - 说明:通用帐号规则( **字母开头,只能包含字母、数字和下划线,长度在6~18之间**)。 ```go func Example_Rule_Passport() { type BizReq struct { Passport1 string `v:"passport"` Passport2 string `v:"passport"` Passport3 string `v:"passport"` Passport4 string `v:"passport"` } var ( ctx = context.Background() req = BizReq{ Passport1: "goframe", Passport2: "1356666", // error starting with letter Passport3: "goframe#", // error containing only numbers or underscores Passport4: "gf", // error length between 6 and 18 } ) if err := g.Validator().Data(req).Run(ctx); err != nil { fmt.Print(gstr.Join(err.Strings(), "\n")) } // Output: // The Passport2 value `1356666` is not a valid passport format // The Passport3 value `goframe#` is not a valid passport format // The Passport4 value `gf` is not a valid passport format } ``` ### `password` - 格式:`password` - 说明:通用密码规则( **任意可见字符,长度在6~18之间**)。 ```go func Example_Rule_Password() { type BizReq struct { Password1 string `v:"password"` Password2 string `v:"password"` } var ( ctx = context.Background() req = BizReq{ Password1: "goframe", Password2: "gofra", // error length between 6 and 18 } ) if err := g.Validator().Data(req).Run(ctx); err != nil { fmt.Print(err) } // Output: // The Password2 value `gofra` is not a valid password format } ``` ### `password2` - 格式:`password2` - 说明:中等强度密码( **在通用密码规则的基础上,要求密码必须包含大小写字母和数字**)。 ```go func Example_Rule_Password2() { type BizReq struct { Password1 string `v:"password2"` Password2 string `v:"password2"` Password3 string `v:"password2"` Password4 string `v:"password2"` } var ( ctx = context.Background() req = BizReq{ Password1: "Goframe123", Password2: "gofra", // error length between 6 and 18 Password3: "Goframe", // error must contain lower and upper letters and numbers. Password4: "goframe123", // error must contain lower and upper letters and numbers. } ) if err := g.Validator().Data(req).Run(ctx); err != nil { fmt.Print(gstr.Join(err.Strings(), "\n")) } // Output: // The Password2 value `gofra` is not a valid password format // The Password3 value `Goframe` is not a valid password format // The Password4 value `goframe123` is not a valid password format } ``` ### `password3` - 格式:`password3` - 说明:强等强度密码( **在通用密码规则的基础上,必须包含大小写字母、数字和特殊字符**)。 ```go func Example_Rule_Password3() { type BizReq struct { Password1 string `v:"password3"` Password2 string `v:"password3"` Password3 string `v:"password3"` } var ( ctx = context.Background() req = BizReq{ Password1: "Goframe123#", Password2: "gofra", // error length between 6 and 18 Password3: "Goframe123", // error must contain lower and upper letters, numbers and special chars. } ) if err := g.Validator().Data(req).Run(ctx); err != nil { fmt.Print(gstr.Join(err.Strings(), "\n")) } // Output: // The Password2 value `gofra` is not a valid password format // The Password3 value `Goframe123` is not a valid password format } ``` ### `postcode` - 格式:`postcode` - 说明:大中国区邮政编码规则。 ```go func Example_Rule_Postcode() { type BizReq struct { Postcode1 string `v:"postcode"` Postcode2 string `v:"postcode"` Postcode3 string `v:"postcode"` } var ( ctx = context.Background() req = BizReq{ Postcode1: "100000", Postcode2: "10000", // error length must be 6 Postcode3: "1000000", // error length must be 6 } ) if err := g.Validator().Data(req).Run(ctx); err != nil { fmt.Print(gstr.Join(err.Strings(), "\n")) } // Output: // The Postcode2 value `10000` is not a valid postcode format // The Postcode3 value `1000000` is not a valid postcode format } ``` ### `resident-id` - 格式:  resident-id - 说明:公民身份证号码。 ```go func Example_Rule_ResidentId() { type BizReq struct { ResidentID1 string `v:"resident-id"` } var ( ctx = context.Background() req = BizReq{ ResidentID1: "320107199506285482", } ) if err := g.Validator().Data(req).Run(ctx); err != nil { fmt.Print(err) } // Output: // The ResidentID1 value `320107199506285482` is not a valid resident id number } ``` ### `bank-card` - 格式:`bank-card` - 说明:大中国区银行卡号校验。 ```go func Example_Rule_BankCard() { type BizReq struct { BankCard1 string `v:"bank-card"` } var ( ctx = context.Background() req = BizReq{ BankCard1: "6225760079930218", } ) if err := g.Validator().Data(req).Run(ctx); err != nil { fmt.Print(err) } // Output: // The BankCard1 value `6225760079930218` is not a valid bank card number } ``` ### `qq` - 格式:`qq` - 说明:腾讯QQ号码规则。 ```go func Example_Rule_QQ() { type BizReq struct { QQ1 string `v:"qq"` QQ2 string `v:"qq"` QQ3 string `v:"qq"` } var ( ctx = context.Background() req = BizReq{ QQ1: "389961817", QQ2: "9999", // error >= 10000 QQ3: "514258412a", // error all number } ) if err := g.Validator().Data(req).Run(ctx); err != nil { fmt.Print(gstr.Join(err.Strings(), "\n")) } // Output: // The QQ2 value `9999` is not a valid QQ number // The QQ3 value `514258412a` is not a valid QQ number } ``` ### `ip` - 格式:`ip` - 说明:`IPv4/IPv6` 地址。 ```go func Example_Rule_IP() { type BizReq struct { IP1 string `v:"ip"` IP2 string `v:"ip"` IP3 string `v:"ip"` IP4 string `v:"ip"` } var ( ctx = context.Background() req = BizReq{ IP1: "127.0.0.1", IP2: "fe80::812b:1158:1f43:f0d1", IP3: "520.255.255.255", // error >= 10000 IP4: "ze80::812b:1158:1f43:f0d1", } ) if err := g.Validator().Data(req).Run(ctx); err != nil { fmt.Print(gstr.Join(err.Strings(), "\n")) } // Output: // The IP3 value `520.255.255.255` is not a valid IP address // The IP4 value `ze80::812b:1158:1f43:f0d1` is not a valid IP address } ``` ### `ipv4` - 格式:`ipv4` - 说明:`IPv4` 地址。 ```go func Example_Rule_IPV4() { type BizReq struct { IP1 string `v:"ipv4"` IP2 string `v:"ipv4"` } var ( ctx = context.Background() req = BizReq{ IP1: "127.0.0.1", IP2: "520.255.255.255", } ) if err := g.Validator().Data(req).Run(ctx); err != nil { fmt.Print(err) } // Output: // The IP2 value `520.255.255.255` is not a valid IPv4 address } ``` ### `ipv6` - 格式:`ipv6` - 说明:`IPv6` 地址。 ```go func Example_Rule_IPV6() { type BizReq struct { IP1 string `v:"ipv6"` IP2 string `v:"ipv6"` } var ( ctx = context.Background() req = BizReq{ IP1: "fe80::812b:1158:1f43:f0d1", IP2: "ze80::812b:1158:1f43:f0d1", } ) if err := g.Validator().Data(req).Run(ctx); err != nil { fmt.Print(err) } // Output: // The IP2 value `ze80::812b:1158:1f43:f0d1` is not a valid IPv6 address } ``` ### `mac` - 格式:`mac` - 说明:`MAC` 地址。 ```go func Example_Rule_Mac() { type BizReq struct { Mac1 string `v:"mac"` Mac2 string `v:"mac"` } var ( ctx = context.Background() req = BizReq{ Mac1: "4C-CC-6A-D6-B1-1A", Mac2: "Z0-CC-6A-D6-B1-1A", } ) if err := g.Validator().Data(req).Run(ctx); err != nil { fmt.Print(err) } // Output: // The Mac2 value `Z0-CC-6A-D6-B1-1A` is not a valid MAC address } ``` ### `url` - 格式:`url` - 说明:URL - 示例:支持以 `http,https,ftp,file` 开头的地址。 ```go func Example_Rule_Url() { type BizReq struct { URL1 string `v:"url"` URL2 string `v:"url"` URL3 string `v:"url"` } var ( ctx = context.Background() req = BizReq{ URL1: "http://goframe.org", URL2: "ftp://goframe.org", URL3: "ws://goframe.org", } ) if err := g.Validator().Data(req).Run(ctx); err != nil { fmt.Print(err) } // Output: // The URL3 value `ws://goframe.org` is not a valid URL address } ``` ### `domain` - 格式:`domain` - 说明:域名 - 示例:域名规则。 `xxx.yyy`(首位必须为字母)。 ```go func Example_Rule_Domain() { type BizReq struct { Domain1 string `v:"domain"` Domain2 string `v:"domain"` Domain3 string `v:"domain"` Domain4 string `v:"domain"` } var ( ctx = context.Background() req = BizReq{ Domain1: "goframe.org", Domain2: "a.b", Domain3: "goframe#org", Domain4: "1a.2b", } ) if err := g.Validator().Data(req).Run(ctx); err != nil { fmt.Print(gstr.Join(err.Strings(), "\n")) } // Output: // The Domain3 value `goframe#org` is not a valid domain format // The Domain4 value `1a.2b` is not a valid domain format } ``` ### `size` - 格式:`size:size` - 说明:参数 **长度** 为 `size`(长度参数为整形),注意底层使用 `Unicode` 计算长度,因此中文一个汉字占 `1` 个长度单位。 ```go func Example_Rule_Size() { type BizReq struct { Size1 string `v:"size:10"` Size2 string `v:"size:5"` } var ( ctx = context.Background() req = BizReq{ Size1: "goframe欢迎你", Size2: "goframe", } ) if err := g.Validator().Data(req).Run(ctx); err != nil { fmt.Print(err) } // Output: // The Size2 value `goframe` length must be 5 } ``` ### `length` - 格式:`length:min,max` - 说明:参数 **长度** 为 `min` 到 `max`(长度参数为整形),注意底层使用 `Unicode` 计算长度,因此中文一个汉字占 `1` 个长度单位。 ```go func Example_Rule_Length() { type BizReq struct { Length1 string `v:"length:5,10"` Length2 string `v:"length:10,15"` } var ( ctx = context.Background() req = BizReq{ Length1: "goframe欢迎你", Length2: "goframe", } ) if err := g.Validator().Data(req).Run(ctx); err != nil { fmt.Print(err) } // Output: // The Length2 value `goframe` length must be between 10 and 15 } ``` ### `min-length` - 格式:`min-length:min` - 说明:参数 **长度** 最小为 `min`(长度参数为整形),注意底层使用 `Unicode` 计算长度,因此中文一个汉字占 `1` 个长度单位。 ```go func Example_Rule_MinLength() { type BizReq struct { MinLength1 string `v:"min-length:10"` MinLength2 string `v:"min-length:8"` } var ( ctx = context.Background() req = BizReq{ MinLength1: "goframe欢迎你", MinLength2: "goframe", } ) if err := g.Validator().Data(req).Run(ctx); err != nil { fmt.Print(err) } // Output: // The MinLength2 value `goframe` length must be equal or greater than 8 } ``` ### `max-length` - 格式:`max-length:max` - 说明:参数 **长度** 最大为 `max`(长度参数为整形),注意底层使用 `Unicode` 计算长度,因此中文一个汉字占 `1` 个长度单位。 ```go func Example_Rule_MaxLength() { type BizReq struct { MaxLength1 string `v:"max-length:10"` MaxLength2 string `v:"max-length:5"` } var ( ctx = context.Background() req = BizReq{ MaxLength1: "goframe欢迎你", MaxLength2: "goframe", } ) if err := g.Validator().Data(req).Run(ctx); err != nil { fmt.Print(err) } // Output: // The MaxLength2 value `goframe` length must be equal or lesser than 5 } ``` ### `between` - 格式:`between:min,max` - 说明:参数 **大小** 为 `min` 到 `max`(支持整形和浮点类型参数)。 ```go func Example_Rule_Between() { type BizReq struct { Age1 int `v:"between:1,100"` Age2 int `v:"between:1,100"` Score1 float32 `v:"between:0,10"` Score2 float32 `v:"between:0,10"` } var ( ctx = context.Background() req = BizReq{ Age1: 50, Age2: 101, Score1: 9.8, Score2: -0.5, } ) if err := g.Validator().Data(req).Run(ctx); err != nil { fmt.Print(gstr.Join(err.Strings(), "\n")) } // Output: // The Age2 value `101` must be between 1 and 100 // The Score2 value `-0.5` must be between 0 and 10 } ``` ### `min` - 格式:`min:min` - 说明:参数 **大小** 最小为 `min`(支持整形和浮点类型参数)。 ```go func Example_Rule_Min() { type BizReq struct { Age1 int `v:"min:100"` Age2 int `v:"min:100"` Score1 float32 `v:"min:10"` Score2 float32 `v:"min:10"` } var ( ctx = context.Background() req = BizReq{ Age1: 50, Age2: 101, Score1: 9.8, Score2: 10.1, } ) if err := g.Validator().Data(req).Run(ctx); err != nil { fmt.Print(gstr.Join(err.Strings(), "\n")) } // Output: // The Age1 value `50` must be equal or greater than 100 // The Score1 value `9.8` must be equal or greater than 10 } ``` ### `max` - 格式:`max:max` - 说明:参数 **大小** 最大为 `max`(支持整形和浮点类型参数)。 ```go func Example_Rule_Max() { type BizReq struct { Age1 int `v:"max:100"` Age2 int `v:"max:100"` Score1 float32 `v:"max:10"` Score2 float32 `v:"max:10"` } var ( ctx = context.Background() req = BizReq{ Age1: 99, Age2: 101, Score1: 9.9, Score2: 10.1, } ) if err := g.Validator().Data(req).Run(ctx); err != nil { fmt.Print(gstr.Join(err.Strings(), "\n")) } // Output: // The Age2 value `101` must be equal or lesser than 100 // The Score2 value `10.1` must be equal or lesser than 10 } ``` ### `json` - 格式:`json` - 说明:判断数据格式为 `JSON`。 ```go func Example_Rule_Json() { type BizReq struct { JSON1 string `v:"json"` JSON2 string `v:"json"` } var ( ctx = context.Background() req = BizReq{ JSON1: "{\"name\":\"goframe\",\"author\":\"郭强\"}", JSON2: "{\"name\":\"goframe\",\"author\":\"郭强\",\"test\"}", } ) if err := g.Validator().Data(req).Run(ctx); err != nil { fmt.Print(err) } // Output: // The JSON2 value `{"name":"goframe","author":"郭强","test"}` is not a valid JSON string } ``` ### `integer` - 格式:`integer` - 说明:整数(正整数或者负整数)。 ```go func Example_Rule_Integer() { type BizReq struct { Integer string `v:"integer"` Float string `v:"integer"` Str string `v:"integer"` } var ( ctx = context.Background() req = BizReq{ Integer: "100", Float: "10.0", Str: "goframe", } ) if err := g.Validator().Data(req).Run(ctx); err != nil { fmt.Print(gstr.Join(err.Strings(), "\n")) } // Output: // The Float value `10.0` is not an integer // The Str value `goframe` is not an integer } ``` ### `float` - 格式:`float` - 说明:浮点数。 ```go func Example_Rule_Float() { type BizReq struct { Integer string `v:"float"` Float string `v:"float"` Str string `v:"float"` } var ( ctx = context.Background() req = BizReq{ Integer: "100", Float: "10.0", Str: "goframe", } ) if err := g.Validator().Data(req).Run(ctx); err != nil { fmt.Print(err) } // Output: // The Str value `goframe` is invalid } ``` ### `boolean` - 格式:`boolean` - 说明:布尔值( `1`, `true`, `on`, `yes` 为 `true` \| `0`, `false`, `off`, `no`, `""` 为 `false`)。 ```go func Example_Rule_Boolean() { type BizReq struct { Boolean bool `v:"boolean"` Integer int `v:"boolean"` Float float32 `v:"boolean"` Str1 string `v:"boolean"` Str2 string `v:"boolean"` Str3 string `v:"boolean"` } var ( ctx = context.Background() req = BizReq{ Boolean: true, Integer: 1, Float: 10.0, Str1: "on", Str2: "", Str3: "goframe", } ) if err := g.Validator().Data(req).Run(ctx); err != nil { fmt.Print(gstr.Join(err.Strings(), "\n")) } // Output: // The Float value `10` field must be true or false // The Str3 value `goframe` field must be true or false } ``` ### `same` - 格式:`same:field` - 说明:参数值必需与 `field` 字段参数的值相同。 - 示例:在用户注册时,提交密码 `Password` 和确认密码 `Password2` 必须相等(服务端校验)。 ```go func Example_Rule_Same() { type BizReq struct { Name string `v:"required"` Password string `v:"required|same:Password2"` Password2 string `v:"required"` } var ( ctx = context.Background() req = BizReq{ Name: "gf", Password: "goframe.org", Password2: "goframe.net", } ) if err := g.Validator().Data(req).Run(ctx); err != nil { fmt.Println(err) } // Output: // The Password value `goframe.org` must be the same as field Password2 } ``` ### `different` - 格式:`different:field` - 说明:参数值不能与 `field` 字段参数的值相同。 - 示例:备用邮箱 `OtherMailAddr` 和邮箱地址 `MailAddr` 必须不相同。 ```go func Example_Rule_Different() { type BizReq struct { Name string `v:"required"` MailAddr string `v:"required"` ConfirmMailAddr string `v:"required|different:MailAddr"` } var ( ctx = context.Background() req = BizReq{ Name: "gf", MailAddr: "gf@goframe.org", ConfirmMailAddr: "gf@goframe.org", } ) if err := g.Validator().Data(req).Run(ctx); err != nil { fmt.Println(err) } // Output: // The ConfirmMailAddr value `gf@goframe.org` must be different from field MailAddr } ``` ### `eq` - 格式:`eq:field` - 说明:参数值必需与 `field` 字段参数的值相同。 `same` 规则的别名,功能同 `same` 规则。 - 版本:框架版本 `>=v2.2.0` ```go func Example_Rule_EQ() { type BizReq struct { Name string `v:"required"` Password string `v:"required|eq:Password2"` Password2 string `v:"required"` } var ( ctx = context.Background() req = BizReq{ Name: "gf", Password: "goframe.org", Password2: "goframe.net", } ) if err := g.Validator().Data(req).Run(ctx); err != nil { fmt.Println(err) } // Output: // The Password value `goframe.org` must be equal to field Password2 value `goframe.net` } ``` ### `not-eq` - 格式:`not-eq:field` - 说明:参数值必需与 `field` 字段参数的值不相同。 `different` 规则的别名,功能同 `different` 规则。 - 版本:框架版本 `>=v2.2.0` ```go func Example_Rule_NotEQ() { type BizReq struct { Name string `v:"required"` MailAddr string `v:"required"` OtherMailAddr string `v:"required|not-eq:MailAddr"` } var ( ctx = context.Background() req = BizReq{ Name: "gf", MailAddr: "gf@goframe.org", OtherMailAddr: "gf@goframe.org", } ) if err := g.Validator().Data(req).Run(ctx); err != nil { fmt.Println(err) } // Output: // The OtherMailAddr value `gf@goframe.org` must not be equal to field MailAddr value `gf@goframe.org` } ``` ### `gt` - 格式:`gt:field` - 说明:参数值必需大于给定字段对应的值。 - 版本:框架版本 `>=v2.2.0` ```go func Example_Rule_GT() { type BizReq struct { Value1 int Value2 int `v:"gt:Value1"` Value3 int `v:"gt:Value1"` } var ( ctx = context.Background() req = BizReq{ Value1: 1, Value2: 1, Value3: 2, } ) if err := g.Validator().Data(req).Run(ctx); err != nil { fmt.Println(err.String()) } // Output: // The Value2 value `1` must be greater than field Value1 value `1` } ``` ### `gte` - 格式:`gte:field` - 说明:参数值必需大于或等于给定字段对应的值。 - 版本:框架版本 `>=v2.2.0` ```go func Example_Rule_GTE() { type BizReq struct { Value1 int Value2 int `v:"gte:Value1"` Value3 int `v:"gte:Value1"` } var ( ctx = context.Background() req = BizReq{ Value1: 2, Value2: 1, Value3: 2, } ) if err := g.Validator().Data(req).Run(ctx); err != nil { fmt.Println(err.String()) } // Output: // The Value2 value `1` must be greater than or equal to field Value1 value `2` } ``` ### `lt` - 格式:`lt:field` - 说明:参数值必需小于给定字段对应的值。 - 版本:框架版本 `>=v2.2.0` ```go func Example_Rule_LT() { type BizReq struct { Value1 int Value2 int `v:"lt:Value1"` Value3 int `v:"lt:Value1"` } var ( ctx = context.Background() req = BizReq{ Value1: 2, Value2: 1, Value3: 2, } ) if err := g.Validator().Data(req).Run(ctx); err != nil { fmt.Println(err.String()) } // Output: // The Value3 value `2` must be lesser than field Value1 value `2` } ``` ### `lte` - 格式:`lte:field` - 说明:参数值必需小于或等于给定字段对应的值。 - 版本:框架版本 `>=v2.2.0` ```go func Example_Rule_LTE() { type BizReq struct { Value1 int Value2 int `v:"lte:Value1"` Value3 int `v:"lte:Value1"` } var ( ctx = context.Background() req = BizReq{ Value1: 1, Value2: 1, Value3: 2, } ) if err := g.Validator().Data(req).Run(ctx); err != nil { fmt.Println(err.String()) } // Output: // The Value3 value `2` must be lesser than or equal to field Value1 value `1` } ``` ### `in` - 格式:`in:value1,value2,...` - 说明:参数值应该在 `value1,value2,...` 中(字符串匹配)。 - 示例:性别字段 `Gender` 的值必须在 `0/1/2` 中。 ```go func Example_Rule_In() { type BizReq struct { ID uint `v:"required" dc:"Your Id"` Name string `v:"required" dc:"Your name"` Gender uint `v:"in:0,1,2" dc:"0:Secret;1:Male;2:Female"` } var ( ctx = context.Background() req = BizReq{ ID: 1, Name: "test", Gender: 3, } ) if err := g.Validator().Data(req).Run(ctx); err != nil { fmt.Println(err) } // Output: // The Gender value `3` is not in acceptable range: 0,1,2 } ``` ### `not-in` - 格式:`not-in:value1,value2,...` - 说明:参数值不应该在 `value1,value2,...` 中(字符串匹配)。 - 示例:无效索引 `InvalidIndex` 的值必须不在 `-1/0/1` 中。 ```go func Example_Rule_NotIn() { type BizReq struct { ID uint `v:"required" dc:"Your Id"` Name string `v:"required" dc:"Your name"` InvalidIndex uint `v:"not-in:-1,0,1"` } var ( ctx = context.Background() req = BizReq{ ID: 1, Name: "test", InvalidIndex: 1, } ) if err := g.Validator().Data(req).Run(ctx); err != nil { fmt.Println(err) } // Output: // The InvalidIndex value `1` must not be in range: -1,0,1 } ``` ### `alpha` - 格式:`alpha` - 说明:参数值应当仅包含字母(a-z, A-Z)。 - 版本:框架版本 `>=v2.10.0` - 示例: ```go func Example_Rule_Alpha() { type BizReq struct { Alpha1 string `v:"alpha"` Alpha2 string `v:"alpha"` } var ( ctx = context.Background() req = BizReq{ Alpha1: "abcABC", Alpha2: "abc123", } ) if err := g.Validator().Data(req).Run(ctx); err != nil { fmt.Println(err) } // Output: // The Alpha2 value `abc123` should be alpha } ``` ### `alpha-dash` - 格式:`alpha-dash` - 说明:参数值应当仅包含字母、数字、下划线和破折号(a-z, A-Z, 0-9, _, -)。 - 版本:框架版本 `>=v2.10.0` - 示例: ```go func Example_Rule_AlphaDash() { type BizReq struct { Value1 string `v:"alpha-dash"` Value2 string `v:"alpha-dash"` } var ( ctx = context.Background() req = BizReq{ Value1: "abc-123_DEF", Value2: "abc@123", } ) if err := g.Validator().Data(req).Run(ctx); err != nil { fmt.Println(err) } // Output: // The Value2 value `abc@123` should be alpha-dash } ``` ### `alpha-num` - 格式:`alpha-num` - 说明:参数值应当仅包含字母和数字(a-z, A-Z, 0-9)。 - 版本:框架版本 `>=v2.10.0` - 示例: ```go func Example_Rule_AlphaNum() { type BizReq struct { Value1 string `v:"alpha-num"` Value2 string `v:"alpha-num"` } var ( ctx = context.Background() req = BizReq{ Value1: "abc123", Value2: "abc-123", } ) if err := g.Validator().Data(req).Run(ctx); err != nil { fmt.Println(err) } // Output: // The Value2 value `abc-123` should be alpha-num } ``` ### `lowercase` - 格式:`lowercase` - 说明:参数值应当仅包含小写字母(a-z)。 - 版本:框架版本 `>=v2.10.0` - 示例: ```go func Example_Rule_Lowercase() { type BizReq struct { Value1 string `v:"lowercase"` Value2 string `v:"lowercase"` } var ( ctx = context.Background() req = BizReq{ Value1: "abc", Value2: "Abc", } ) if err := g.Validator().Data(req).Run(ctx); err != nil { fmt.Println(err) } // Output: // The Value2 value `Abc` should be lowercase } ``` ### `uppercase` - 格式:`uppercase` - 说明:参数值应当仅包含大写字母(A-Z)。 - 版本:框架版本 `>=v2.10.0` - 示例: ```go func Example_Rule_Uppercase() { type BizReq struct { Value1 string `v:"uppercase"` Value2 string `v:"uppercase"` } var ( ctx = context.Background() req = BizReq{ Value1: "ABC", Value2: "Abc", } ) if err := g.Validator().Data(req).Run(ctx); err != nil { fmt.Println(err) } // Output: // The Value2 value `Abc` should be uppercase } ``` ### `numeric` - 格式:`numeric` - 说明:参数值应当为纯数字字符串。 - 版本:框架版本 `>=v2.10.0` - 示例: ```go func Example_Rule_Numeric() { type BizReq struct { Value1 string `v:"numeric"` Value2 string `v:"numeric"` } var ( ctx = context.Background() req = BizReq{ Value1: "123456", Value2: "12.34", } ) if err := g.Validator().Data(req).Run(ctx); err != nil { fmt.Println(err) } // Output: // The Value2 value `12.34` should be numeric } ``` ### `regex` - 格式:`regex:pattern` - 说明:参数值应当满足正则匹配规则 `pattern` ```go func Example_Rule_Regex() { type BizReq struct { Regex1 string `v:"regex:[1-9][0-9]{4,14}"` Regex2 string `v:"regex:[1-9][0-9]{4,14}"` Regex3 string `v:"regex:[1-9][0-9]{4,14}"` } var ( ctx = context.Background() req = BizReq{ Regex1: "1234", Regex2: "01234", Regex3: "10000", } ) if err := g.Validator().Data(req).Run(ctx); err != nil { fmt.Print(gstr.Join(err.Strings(), "\n")) } // Output: // The Regex1 value `1234` must be in regex of: [1-9][0-9]{4,14} // The Regex2 value `01234` must be in regex of: [1-9][0-9]{4,14} } ``` ### `not-regex` - 格式:`not-regex:pattern` - 说明:参数值不应当满足正则匹配规则 `pattern`。 - 版本:框架版本 `>=v2.2.0` ```go func Example_Rule_NotRegex() { type BizReq struct { Regex1 string `v:"regex:\\d{4}"` Regex2 string `v:"not-regex:\\d{4}"` } var ( ctx = context.Background() req = BizReq{ Regex1: "1234", Regex2: "1234", } ) if err := g.Validator().Data(req).Run(ctx); err != nil { fmt.Print(gstr.Join(err.Strings(), "\n")) } // Output: // The Regex2 value `1234` should not be in regex of: \d{4} } ``` # 数据校验-校验对象 ## 校验对象 数据校验组件提供了数据校验对象,用于数据校验的统一的配置管理、便捷的链式操作。 **接口文档**: [https://pkg.go.dev/github.com/gogf/gf/v2/util/gvalid](https://pkg.go.dev/github.com/gogf/gf/v2/util/gvalid) ```go type Validator func New() *Validator func (v *Validator) Assoc(assoc interface{}) *Validator func (v *Validator) Bail() *Validator func (v *Validator) Ci() *Validator func (v *Validator) Clone() *Validator func (v *Validator) Data(data interface{}) *Validator func (v *Validator) I18n(i18nManager *gi18n.Manager) *Validator func (v *Validator) Messages(messages interface{}) *Validator func (v *Validator) RuleFunc(rule string, f RuleFunc) *Validator func (v *Validator) RuleFuncMap(m map[string]RuleFunc) *Validator func (v *Validator) Rules(rules interface{}) *Validator func (v *Validator) Run(ctx context.Context) Error ``` **简要说明:** 1. `New`方法用于创建一个新的校验对象。 2. `Assoc`用于关联数据校验,具体请查看后续章节介绍。 3. `Bail`方法用于设定只要后续的多个校验中有一个规则校验失败则停止校验立即返回错误结果。 4. `Ci`方法用于设置需要比较数值的规则时,不区分字段的大小写。 5. `Run`方法对给定规则和信息的数据进行校验操作。 6. `I18n`方法用于设置当前校验对象的`I18N`国际化组件。默认情况下,校验组件使用的是框架全局默认的`i18n`组件对象。 7. `Data`方法用于传递需要联合校验的数据集合,往往传递的是`map`类型或者`struct`类型。 8. `Rules`方法用于传递当前链式操作校验的自定义校验规则,往往使用`[]string`类型或者`map`类型。 9. `Messages`方法用于传递当前链式操作校验的自定义错误提示信息,往往使用`map`类型传递,具体看后续代码示例。 **提示** 由于数据校验对象也是一个非常常用的对象,因此`g`模块中定义了`Validator`方法来快捷创建校验对象。大部分场景下我们推荐使用`g`模块的`g.Validator()`方式来快捷创建一个校验对象,关于`g`模块的介绍请参考章节:[对象管理](./g.md) ## 使用示例 ### 单数据校验 ```go var ( err error ctx = gctx.New() ) err = g.Validator(). Rules("min:18"). Data(16). Messages("未成年人不允许注册哟"). Run(ctx) fmt.Println(err.Error()) // Output: // 未成年人不允许注册哟 ``` ```go var ( err error ctx = gctx.New() data = g.Map{ "password": "123", } ) err = g.Validator().Data("").Assoc(data). Rules("required-with:password"). Messages("请输入确认密码"). Run(ctx) fmt.Println(err.Error()) ``` ### `Struct` 数据校验 ```go type User struct { Name string `v:"required#请输入用户姓名"` Type int `v:"required#请选择用户类型"` } var ( err error ctx = gctx.New() user = User{} data = g.Map{ "name": "john", } ) if err = gconv.Scan(data, &user); err != nil { panic(err) } err = g.Validator().Assoc(data).Data(user).Run(ctx) if err != nil { fmt.Println(err.(gvalid.Error).Items()) } // Output: // [map[Type:map[required:请选择用户类型]]] ``` ### `Map` 数据校验 ```go params := map[string]interface{}{ "passport": "", "password": "123456", "password2": "1234567", } rules := map[string]string{ "passport": "required|length:6,16", "password": "required|length:6,16|same:password2", "password2": "required|length:6,16", } messages := map[string]interface{}{ "passport": "账号不能为空|账号长度应当在{min}到{max}之间", "password": map[string]string{ "required": "密码不能为空", "same": "两次密码输入不相等", }, } err := g.Validator().Messages(messages).Rules(rules).Data(params).Run(gctx.New()) if err != nil { g.Dump(err.Maps()) } ``` 执行后,终端输出: ``` { "passport": { "length": "账号长度应当在6到16之间", "required": "账号不能为空" }, "password": { "same": "两次密码输入不相等" } } ``` # 数据校验-校验结果 ## 基本介绍 校验结果为一个`error`错误对象,内部使用`gvalid.Error`对象实现。当数据规则校验成功时,校验方法返回的结果为`nil`。当数据规则校验失败时,返回的该对象是包含结构化的层级`map`,包含多个字段及其规则及对应错误信息,以便于接收端能够准确定位错误规则。相关数据结构及方法如下: [https://pkg.go.dev/github.com/gogf/gf/v2/util/gvalid](https://pkg.go.dev/github.com/gogf/gf/v2/util/gvalid) ```go type Error interface { Code() gcode.Code Current() error Error() string FirstItem() (key string, messages map[string]error) FirstRule() (rule string, err error) FirstError() (err error) Items() (items []map[string]map[string]error) Map() map[string]error Maps() map[string]map[string]error String() string Strings() (errs []string) } ``` 可以结合后续的示例理解这个数据结构。我们可以通过`Maps()`方法获得该原始错误信息数据结构`map`。但在大多数时候我们可以通过`Error`接口的其他方法来方便地获取特定的错误信息。 **提示** 大多数情况下,我们不关心具体出错的校验规则,可以使用`Error/String`方法直接返回所有的错误信息即可。大部分的方法获取错误信息时根据校验规则的顺序性与否,返回的结果顺序会不太一样。 **简要说明:** 获取校验结果的值可以通过多个校验结果方法获取,为让各位开发者有充分的理解,详细说明以下: | 方法 | 说明 | | --- | --- | | `Code` | 常用方法。实现 `gerror` 的 `Code` 接口,在校验组件中,该方法固定返回错误码 `gcode.CodeValidationFailed`。 | | `Error` | 常用方法。实现标准库的 `error.Error` 接口,获取返回所有校验错误组成的错误字符串。内部逻辑同 `String` 方法。 | | `Current` | 常用方法。实现了 `gerror` 的 `Current` 接口,用于获取校验错误中的第一条错误对象。 | | `Items` | 在顺序性校验中将会按照校验规则顺序返回校验错误数组。其顺序性只有在顺序校验时有效,否则返回的结果是随机的。 | | `Map` | 返回 `FirstItem` 中得出错自规则及对应错误信息 `map`。 | | `Maps` | 返回所有的出错键名及对应的出错规则及对应的错误信息( `map[string]map[string]error`)。 | | `String` | 返回所有的错误信息,构成一条字符串返回,多个规则错误信息之间以 `;` 符号连接。其顺序性只有使用顺序校验规则时有效,否则返回的结果是随机的。 | | `Strings` | 返回所有的错误信息,构成 `[]string` 类型返回。其顺序性只有使用顺序校验规则时有效,否则返回的结果是随机的。 | | `FirstItem` | 在有多个键名/属性校验错误的时候,用以获取出错的第一个键名,以及其对应的出错规则和错误信息。其顺序性只有使用顺序校验规则时有效,否则返回的结果是随机的。 | | `FirstRule` | 返回 `FirstItem` 中得第一条出错的规则及错误信息。其顺序性只有使用顺序校验规则时有效,否则返回的结果是随机的。 | | `FirstString` | 返回 `FirstRule` 中得第一条规则错误信息。其顺序性只有使用顺序校验规则时有效,否则返回的结果是随机的。 | ## `gerror.Current` 支持 我们可以看到, `gvalid.Error` 实现了 `Current() error` 接口,因此可以通过 `gerror.Current` 方法获取它的第一条错误信息,这在接口校验失败时返回错误信息非常方便。我们来看一个示例: ```go package main import ( "github.com/gogf/gf/v2/errors/gerror" "github.com/gogf/gf/v2/frame/g" "github.com/gogf/gf/v2/os/gctx" "github.com/gogf/gf/v2/util/gvalid" ) func main() { type User struct { Name string `v:"required#请输入用户姓名"` Type int `v:"required|min:1#|请选择用户类型"` } var ( err error ctx = gctx.New() user = User{} ) if err = g.Validator().Data(user).Run(ctx); err != nil { g.Dump(err.(gvalid.Error).Maps()) g.Dump(gerror.Current(err)) } } ``` 这里使用了 `gerror.Current(err)` 来获取校第一条验错误信息。执行后,终端输出: ``` { "Name": { "required": "请输入用户姓名", }, "Type": { "min": "请选择用户类型", }, } "请输入用户姓名" ``` **注意** 需要注意的是,数据校验时存在 **顺序性校验** 和 **非顺序性校验**,这会对获取第一条错误信息的结果产生影响。关于顺序与非顺序性校验,具体可以参考后续章节介绍。 # 数据校验-可选校验 ## 可选校验规则 当给定的数据校验规则中不包含`required*`规则时,表示该规则不是一个必须规则。如果当给定的值为`nil`或者`空字符串`时,将会忽略其校验。 ## 示例1,空字符串 ```go package main import ( "fmt" "github.com/gogf/gf/v2/frame/g" "github.com/gogf/gf/v2/os/gctx" ) func main() { type Params struct { Page int `v:"required|min:1 # page is required"` Size int `v:"required|between:1,100 # size is required"` ProjectId string `v:"between:1,10000 # project id must between {min}, {max}"` } var ( ctx = gctx.New() obj = &Params{ Page: 1, Size: 10, } ) err := g.Validator().Data(obj).Run(ctx) fmt.Println(err) // Output: // } ``` ## 示例2,空指针属性 ```go package main import ( "fmt" "github.com/gogf/gf/v2/container/gvar" "github.com/gogf/gf/v2/frame/g" "github.com/gogf/gf/v2/os/gctx" ) func main() { type Params struct { Page int `v:"required|min:1 # page is required"` Size int `v:"required|between:1,100 # size is required"` ProjectId *gvar.Var `v:"between:1,10000 # project id must between {min}, {max}"` } var ( ctx = gctx.New() obj = &Params{ Page: 1, Size: 10, } ) err := g.Validator().Data(obj).Run(ctx) fmt.Println(err) // Output: // } ``` ## 示例3,空整型属性 **需要注意的是,如果键值为 `0` 或者 `false`,参数值将仍然会被校验。** ```go package main import ( "fmt" "github.com/gogf/gf/v2/frame/g" "github.com/gogf/gf/v2/os/gctx" ) func main() { type Params struct { Page int `v:"required|min:1 # page is required"` Size int `v:"required|between:1,100 # size is required"` ProjectId int `v:"between:1,10000 # project id must between {min}, {max}"` } var ( ctx = gctx.New() obj = &Params{ Page: 1, Size: 10, } ) err := g.Validator().Data(obj).Run(ctx) fmt.Println(err) // Output: // } ``` 执行后,终端输出: ``` project id must between 1, 10000 ``` ## 示例4,通过 `map` 传参 ```go package main import ( "github.com/gogf/gf/v2/frame/g" "github.com/gogf/gf/v2/os/gctx" ) func main() { var ( ctx = gctx.New() params = map[string]interface{}{ "passport": "", "password": "123456", "password2": "1234567", } rules = []string{ "passport@length:6,16", "password@required|length:6,16|same:password2", "password2@required|length:6,16", } ) err := g.Validator().Rules(rules).Data(params).Run(ctx) if err != nil { g.Dump(err.Maps()) } } ``` 需要注意,其中的 `passport` 键名并没有 `required` 规则,因此即便给定的 `passport` 参数为空字符串,不满足规则时,也并没有报错,因为校验组件将其看做可选校验规则。 执行后,终端输出: ``` { "password": { "same": "The password value `123456` must be the same as field password2", }, } ``` # 数据校验-递归校验 校验组件支持强大的递归校验(嵌套校验)特性。如果给定的校验数据中的属性或者键值为`struct`/`map`/`slice`类型时,将会被自动执行递归校验。我们来看几个示例: ## 示例1,递归校验:`struct` ```go package main import ( "fmt" "github.com/gogf/gf/v2/frame/g" "github.com/gogf/gf/v2/os/gctx" ) type SearchReq struct { Key string `v:"required"` Option SearchOption } type SearchOption struct { Page int `v:"min:1"` Size int `v:"max:100"` } func main() { var ( ctx = gctx.New() req = SearchReq{ Key: "GoFrame", Option: SearchOption{ Page: 1, Size: 10000, }, } ) err := g.Validator().Data(req).Run(ctx) fmt.Println(err) } ``` 执行后,终端输出: ``` The Size value `10000` must be equal or lesser than 100 ``` ## 示例2,递归校验:`slice` ```go package main import ( "fmt" "github.com/gogf/gf/v2/frame/g" "github.com/gogf/gf/v2/os/gctx" ) func main() { type Student struct { Name string `v:"required#Student Name is required"` Age int } type Teacher struct { Name string Students []Student } var ( ctx = gctx.New() teacher = Teacher{} data = g.Map{ "name": "john", "students": `[{"age":2},{"name":"jack", "age":4}]`, } ) err := g.Validator().Assoc(data).Data(teacher).Run(ctx) fmt.Println(err) } ``` 执行后,终端输出: ``` Student Name is required ``` ## 示例3,递归校验:`map` ```go package main import ( "fmt" "github.com/gogf/gf/v2/frame/g" "github.com/gogf/gf/v2/os/gctx" ) func main() { type Student struct { Name string `v:"required#Student Name is required"` Age int } type Teacher struct { Name string Students map[string]Student } var ( ctx = gctx.New() teacher = Teacher{ Name: "Smith", Students: map[string]Student{ "john": {Name: "", Age: 18}, }, } ) err := g.Validator().Data(teacher).Run(ctx) fmt.Println(err) } ``` 执行后,终端输出: ``` Student Name is required ``` ## 注意事项:空对象对递归校验的影响 ```go package main import ( "fmt" "github.com/gogf/gf/v2/frame/g" "github.com/gogf/gf/v2/os/gctx" ) func main() { type Student struct { Name string `v:"required"` } type Teacher struct { Students Student } var ( ctx = gctx.New() teacher = Teacher{} data = g.Map{ "students": nil, } ) err := g.Validator().Assoc(data).Data(teacher).Run(ctx) fmt.Println(err) } ``` 执行后,终端输出: ``` Student Name is required ``` 有同学可能会觉得奇怪,明明我都没有传递`Student`字段值,为什么还会递归校验`Student`结构体里面的`Name`字段?这是因为这里的`Student`属性是个空结构体,是带有默认值的(`Name`默认值为空字符串)。递归校验里面,虽然`Student`不是必须参数,这个意思是可以不传递,但是只要传递了就会按照里面属性的校验规则执行校验(带有默认值的空对象也算是有值)。可以对比和以下代码的差别: ```go package main import ( "fmt" "github.com/gogf/gf/v2/frame/g" "github.com/gogf/gf/v2/os/gctx" ) func main() { type Student struct { Name string `v:"required"` } type Teacher struct { Students *Student } var ( ctx = gctx.New() teacher = Teacher{} data = g.Map{ "students": nil, } ) err := g.Validator().Assoc(data).Data(teacher).Run(ctx) fmt.Println(err) } ``` 和前一示例的唯一差异在于`Student`属性从结构体改为了结构体指针`*Student`,这样该属性不是空对象便没有默认值了。执行后,终端输出为空,表示校验通过。 # 数据校验-自定义错误 数据校验组件支持`i18n`特性,内部使用了`GoFrame`框架统一的`i18n`组件实现。默认使用默认的`i18n`单例对象,即`g.I18n()`对象。 在进一步使用之前,关于`i18n`国际化功能配置及使用请参考章节:[I18N国际化](./gi18n.md) ## 配置示例 ### 默认`i18n`错误提示 默认的英文国际化语言配置文件参考:[https://github.com/gogf/gf/tree/master/util/gvalid/i18n/en](https://github.com/gogf/gf/tree/master/util/gvalid/i18n/en) ### 中文错误提示 我们提供了建议的中文`i18n`国际化语言配置文件:[https://github.com/gogf/gf/tree/master/util/gvalid/i18n/cn](https://github.com/gogf/gf/tree/master/util/gvalid/i18n/cn) ### 默认错误提示 当在`i18n`中找不到对应规则的错误提示时,将会使用`__default__`配置的错误提示信息。往往使用在自定义规则中。 ## 开发示例 我们通过中间件统一设置请求的错误提示`i18n`语言。 ### 目录结构 **注意** 注意工程目录结构,以便于默认的`g.i18n()`对象能自动读取配置。有相当一部分同学倒在了这里。 ``` ├── main.go └── i18n ├── en.toml └── zh-CN.toml ``` ### `i18n`文件 `en.toml` ``` "ReuiredUserName" = "Please input user name" "ReuiredUserType" = "Please select user type" ``` `zh-CN.toml` ``` "ReuiredUserName" = "请输入用户名称" "ReuiredUserType" = "请选择用户类型" ``` ### 示例代码 ```go package main import ( "github.com/gogf/gf/v2/frame/g" "github.com/gogf/gf/v2/i18n/gi18n" "github.com/gogf/gf/v2/os/gctx" "github.com/gogf/gf/v2/util/gconv" ) func main() { type User struct { Name string `v:"required#ReuiredUserName"` Type int `v:"required#ReuiredUserType"` } var ( ctx = gctx.New() data = g.Map{ "name": "john", } user = User{} ctxEn = gi18n.WithLanguage(ctx, "en") ctxCh = gi18n.WithLanguage(ctx, "zh-CN") ) if err := gconv.Scan(data, &user); err != nil { panic(err) } // 英文 if err := g.Validator().Assoc(data).Data(user).Run(ctxEn); err != nil { g.Dump(err.String()) } // 中文 if err := g.Validator().Assoc(data).Data(user).Run(ctxCh); err != nil { g.Dump(err.String()) } } ``` 执行后,终端输出: ``` Please select user type 请选择用户类型 ``` # 数据校验-方法介绍 **提示** 以下常用方法列表,文档更新可能滞后于代码新特性,更多的方法及示例请参考代码文档: [https://pkg.go.dev/github.com/gogf/gf/v2/util/gvalid](https://pkg.go.dev/github.com/gogf/gf/v2/util/gvalid) ## `New` - 说明:`New`创建并返回一个`Validator`的新对象。 - 格式: ```go New() *Validator ``` - 示例: ```go func ExampleNew() { validator := gvalid.New() if err := validator.Data(16).Rules("min:18").Run(context.Background()); err != nil { fmt.Print(err) } // Output: // The value `16` must be equal or greater than 18 } ``` ## `Run` - 说明:`Run`对给定规则和信息的数据进行校验操作。 - 格式: ```go Run(ctx context.Context) Error ``` - 示例: ```go func ExampleValidator_Run() { // check value mode if err := g.Validator().Data(16).Rules("min:18").Run(context.Background()); err != nil { fmt.Println("check value err:", err) } // check map mode data := map[string]interface{}{ "passport": "", "password": "123456", "password2": "1234567", } rules := map[string]string{ "passport": "required|length:6,16", "password": "required|length:6,16|same:password2", "password2": "required|length:6,16", } if err := g.Validator().Data(data).Rules(rules).Run(context.Background()); err != nil { fmt.Println("check map err:", err) } // check struct mode type Params struct { Page int `v:"required|min:1"` Size int `v:"required|between:1,100"` ProjectId string `v:"between:1,10000"` } rules = map[string]string{ "Page": "required|min:1", "Size": "required|between:1,100", "ProjectId": "between:1,10000", } obj := &Params{ Page: 0, Size: 101, } if err := g.Validator().Data(obj).Run(context.Background()); err != nil { fmt.Println("check struct err:", err) } // May Output: // check value err: The value `16` must be equal or greater than 18 // check map err: The passport field is required; The passport value `` length must be between 6 and 16; The password value `123456` must be the same as field password2 // check struct err: The Page value `0` must be equal or greater than 1; The Size value `101` must be between 1 and 100 } ``` ## `Clone` - 说明:Clone创建并返回一个当前Validator的值拷贝对象。 - 格式: ``` (v *Validator) Clone() *Validator ``` - 示例: ```go func ExampleValidator_Clone() { if err := g.Validator().Data(16).Rules("min:18").Run(context.Background()); err != nil { fmt.Println(err) } if err := g.Validator().Clone().Data(20).Run(context.Background()); err != nil { fmt.Println(err) } else { fmt.Println("Check Success!") } // Output: // The value `16` must be equal or greater than 18 // Check Success! } ``` ## I18n - 说明: `I18n` 方法用于设置当前校验对象的 `I18N` 国际化组件。默认情况下,校验组件使用的是框架全局默认的 `i18n` 组件对象。 - 格式: ```go I18n(i18nManager *gi18n.Manager) *Validator ``` - 示例: ```go func ExampleValidator_I18n() { var ( i18nManager = gi18n.New() ctxCn = gi18n.WithLanguage(context.Background(), "cn") validator = gvalid.New() ) validator = validator.Data(16).Rules("min:18") if err := validator.Run(context.Background()); err != nil { fmt.Println(err) } if err := validator.I18n(i18nManager).Run(ctxCn); err != nil { fmt.Println(err) } // Output: // The value `16` must be equal or greater than 18 // 字段值`16`字段最小值应当为18 } ``` ## Bail - 说明: `Bail` 方法用于设定只要后续的多个校验中有一个规则校验失败则停止校验立即返回错误结果。 - 格式: ```go Bail() *Validator ``` - 示例: ```go func ExampleValidator_Bail() { type BizReq struct { Account string `v:"required|length:6,16|same:QQ"` QQ string Password string `v:"required|same:Password2"` Password2 string `v:"required"` } var ( ctx = context.Background() req = BizReq{ Account: "gf", QQ: "123456", Password: "goframe.org", Password2: "goframe.org", } ) if err := g.Validator().Bail().Data(req).Run(ctx); err != nil { fmt.Println("Use Bail Error:", err) } if err := g.Validator().Data(req).Run(ctx); err != nil { fmt.Println("Not Use Bail Error:", err) } // output: // Use Bail Error: The Account value `gf` length must be between 6 and 16 // Not Use Bail Error: The Account value `gf` length must be between 6 and 16; The Account value `gf` must be the same as field QQ } ``` ## `Ci` - 说明: `Ci` 方法用于设置需要比较数值的规则时,不区分字段的大小写。 - 格式: ```go Ci() *Validator ``` - 示例: ```go func ExampleValidator_Ci() { type BizReq struct { Account string `v:"required"` Password string `v:"required|same:Password2"` Password2 string `v:"required"` } var ( ctx = context.Background() req = BizReq{ Account: "gf", Password: "Goframe.org", // Diff from Password2, but because of "ci", rule check passed Password2: "goframe.org", } ) if err := g.Validator().Data(req).Run(ctx); err != nil { fmt.Println("Not Use CI Error:", err) } if err := g.Validator().Ci().Data(req).Run(ctx); err == nil { fmt.Println("Use CI Passed!") } // output: // Not Use CI Error: The Password value `Goframe.org` must be the same as field Password2 // Use CI Passed! } ``` ## Data - 说明: `Data` 方法用于传递需要联合校验的数据。 - 格式: ```go Data(data interface{}) *Validator ``` - 示例: ```go func ExampleValidator_Data() { type BizReq struct { Password1 string `v:"password"` Password2 string `v:"password"` } var ( ctx = context.Background() req = BizReq{ Password1: "goframe", Password2: "gofra", // error length between 6 and 18 } ) if err := g.Validator().Data(req).Run(ctx); err != nil { fmt.Print(err) } // Output: // The Password2 value `gofra` is not a valid password format } ``` ## Assoc - 说明:`Assoc` 是一个链式操作函数,为当前 `Validator` 设置验证的数据。参数 `assoc` 的类型通常是 `map`,指定了 `union validator` 中的 `map` 的值。 - 注意:使用非 `nil` 的 `assoc` 参数,会将 `useDataInsteadOfObjectAttributes` 属性设置为 `true`。 - 格式: ```go Assoc(assoc interface{}) *Validator ``` - 示例: ```go func ExampleValidator_Assoc() { type User struct { Name string `v:"required"` Type int `v:"required"` } data := g.Map{ "name": "john", } user := User{} if err := gconv.Scan(data, &user); err != nil { panic(err) } if err := g.Validator().Data(user).Assoc(data).Run(context.Background()); err != nil { fmt.Print(err) } // Output: // The Type field is required } ``` ## Rules - 说明: `Rules` 方法用于传递当前链式操作校验的自定义校验规则。 - 格式: ```go Rules(rules interface{}) *Validator ``` - 示例: ```go func ExampleValidator_Rules() { if err := g.Validator().Data(16).Rules("min:18").Run(context.Background()); err != nil { fmt.Println(err) } // Output: // The value `16` must be equal or greater than 18 } ``` ## `Message` - 说明: `Messages` 方法用于传递当前链式操作校验的自定义错误提示信息。 - 格式: ```go Messages(messages interface{}) *Validator ``` - 示例: ```go func ExampleValidator_Messages() { if err := g.Validator().Data(16).Rules("min:18").Messages("Can not regist, Age is less then 18!").Run(context.Background()); err != nil { fmt.Println(err) } // Output: // Can not regist, Age is less then 18! } ``` ## RuleFunc - 说明:`RuleFunc` 向当前的 `Validator` 注册一个自定义校验规则的函数。 - 格式: ```go RuleFunc(rule string, f RuleFunc) *Validator ``` - 示例: ```go func ExampleValidator_RuleFunc() { var ( ctx = context.Background() lenErrRuleName = "LenErr" passErrRuleName = "PassErr" lenErrRuleFunc = func(ctx context.Context, in gvalid.RuleFuncInput) error { pass := in.Value.String() if len(pass) != 6 { return errors.New(in.Message) } return nil } passErrRuleFunc = func(ctx context.Context, in gvalid.RuleFuncInput) error { pass := in.Value.String() if m := in.Data.Map(); m["data"] != pass { return errors.New(in.Message) } return nil } ) type LenErrStruct struct { Value string `v:"uid@LenErr#Value Length Error!"` Data string `p:"data"` } st := &LenErrStruct{ Value: "123", Data: "123456", } // single error sample if err := g.Validator().RuleFunc(lenErrRuleName, lenErrRuleFunc).Data(st).Run(ctx); err != nil { fmt.Println(err) } type MultiErrorStruct struct { Value string `v:"uid@LenErr|PassErr#Value Length Error!|Pass is not Same!"` Data string `p:"data"` } multi := &MultiErrorStruct{ Value: "123", Data: "123456", } // multi error sample if err := g.Validator().RuleFunc(lenErrRuleName, lenErrRuleFunc).RuleFunc(passErrRuleName, passErrRuleFunc).Data(multi).Run(ctx); err != nil { fmt.Println(err) } // Output: // Value Length Error! // Value Length Error!; Pass is not Same! } ``` ## RuleFuncMap - 说明:`RuleFuncMap` 向当前的 `Validator` 注册多个自定义校验规则的函数。 - 格式: ```go RuleFuncMap(m map[string]RuleFunc) *Validator ``` - 示例: ```go func ExampleValidator_RuleFuncMap() { var ( ctx = context.Background() lenErrRuleName = "LenErr" passErrRuleName = "PassErr" lenErrRuleFunc = func(ctx context.Context, in gvalid.RuleFuncInput) error { pass := in.Value.String() if len(pass) != 6 { return errors.New(in.Message) } return nil } passErrRuleFunc = func(ctx context.Context, in gvalid.RuleFuncInput) error { pass := in.Value.String() if m := in.Data.Map(); m["data"] != pass { return errors.New(in.Message) } return nil } ruleMap = map[string]gvalid.RuleFunc{ lenErrRuleName: lenErrRuleFunc, passErrRuleName: passErrRuleFunc, } ) type MultiErrorStruct struct { Value string `v:"uid@LenErr|PassErr#Value Length Error!|Pass is not Same!"` Data string `p:"data"` } multi := &MultiErrorStruct{ Value: "123", Data: "123456", } if err := g.Validator().RuleFuncMap(ruleMap).Data(multi).Run(ctx); err != nil { fmt.Println(err) } // Output: // Value Length Error!; Pass is not Same! } ``` # 数据校验-常见问题 ## `Struct`默认值对`required`规则的影响 `Struct`的属性会有`默认值`,在某些情况下会引起`required`规则的失效。例如: ```go type User struct { Name string `v:"required"` Age uint `v:"required"` } ``` 在该结构体校验中,`Age`属性的`required`校验将会失效,因为`Age`即便没有输入也会有默认值`0`。 这里有**三种**解决方案: 1. 将属性改为指针类型,例如`*int`、`*float64`、`*g.Var`等,通过指针类型默认值为`nil`的特点绕过了这个问题。 2. 使用组合校验规则来弥补默认值对`required`规则的影响,例如以上示例中将`Age`属性的校验规则修改为`required|min:1`将能达到业务校验的效果。 3. 使用`Struct`校验的`Assoc`联合校验方法设置联合校验参数,在校验`Struct`类型参数时,参数值将以`Assoc`方法中给定的参数为准执行校验。如果使用框架的`Server`,采用结构化的`API`输入输出(`XxxReq/XxxRes`),那么`Server`将会自动调用`Assoc`执行校验,开发者无需担心默认值的影响。文档链接:[Struct校验-Assoc关联](./gvalid-parameter-type-struct-assoc.md) # 数据校验-单数据校验 我们可以将给定的变量当做一个完整的参数进行校验,即单数据校验。如果变量是`Struct/Map`复杂类型,我们需要校验其内部的属性/键值对的场景,将会在后续章节介绍。单数据校验必须通过`Data`方法给定被校验数据,`Rules`方法给定校验规则。单数据校验比较简单,我们来看几个示例。 ## 校验数据长度,使用默认的错误提示 ```go package main import ( "fmt" "github.com/gogf/gf/v2/frame/g" "github.com/gogf/gf/v2/os/gctx" ) func main() { var ( ctx = gctx.New() rule = "length:6,16" ) if err := g.Validator().Rules(rule).Data("123456").Run(ctx); err != nil { fmt.Println(err.String()) } if err := g.Validator().Rules(rule).Data("12345").Run(ctx); err != nil { fmt.Println(err.String()) } } ``` 执行后,终端输出: ``` The value `12345` length must be between 6 and 16 ``` ## 校验数据类型及大小,并且使用自定义的错误提示 ```go package main import ( "github.com/gogf/gf/v2/frame/g" "github.com/gogf/gf/v2/os/gctx" ) func main() { var ( ctx = gctx.New() rule = "integer|between:6,16" messages = "请输入一个整数|参数大小不对啊老铁" value = 5.66 ) if err := g.Validator().Rules(rule).Messages(messages).Data(value).Run(ctx); err != nil { g.Dump(err.Map()) } } ``` 执行后,终端输出: ``` { "integer": "请输入一个整数", "between": "参数大小不对啊老铁", } ``` 可以看到,多个规则以及多个自定义错误提示之间使用英文 `|` 号进行分割,注意自定义错误提示的顺序和多规则的顺序一一对应。 `messages` 参数除了支持 `string` 类型以外,还支持 `map[string]string` 类型,请看以下例子: ```go package main import ( "github.com/gogf/gf/v2/frame/g" "github.com/gogf/gf/v2/os/gctx" ) func main() { var ( ctx = gctx.New() rule = "url|min-length:11" value = "goframe.org" messages = map[string]string{ "url": "请输入正确的URL地址", "min-length": "地址长度至少为{min}位", } ) if err := g.Validator().Rules(rule).Messages(messages).Data(value).Run(ctx); err != nil { g.Dump(err.Map()) } } ``` 执行后,终端输出: ``` { "url": "请输入正确的URL地址", } ``` ## 使用自定义正则校验数据格式,使用默认错误提示 ```go package main import ( "fmt" "github.com/gogf/gf/v2/frame/g" "github.com/gogf/gf/v2/os/gctx" ) func main() { var ( ctx = gctx.New() rule = `regex:\d{6,}|\D{6,}|max-length:16` ) if err := g.Validator().Rules(rule).Data(`123456`).Run(ctx); err != nil { fmt.Println(err) } if err := g.Validator().Rules(rule).Data(`abcde6`).Run(ctx); err != nil { fmt.Println(err) } } ``` 执行后,终端输出: ``` The value `abcde6` must be in regex of: \d{6,}|\D{6,} ``` # Map校验-基本使用 ## 默认错误提示 ```go package main import ( "github.com/gogf/gf/v2/frame/g" "github.com/gogf/gf/v2/os/gctx" ) func main() { var ( ctx = gctx.New() params = map[string]interface{}{ "passport": "", "password": "123456", "password2": "1234567", } rules = map[string]string{ "passport": "required|length:6,16", "password": "required|length:6,16|same:password2", "password2": "required|length:6,16", } ) err := g.Validator().Rules(rules).Data(params).Run(ctx) if err != nil { g.Dump(err.Maps()) } } ``` 执行后,终端输出: ```javascript { "passport": { "required": "The passport field is required", "length": "The passport value `` length must be between 6 and 16", }, "password": { "same": "The password value `123456` must be the same as field password2", }, } ``` ## 自定义错误提示 ```go package main import ( "github.com/gogf/gf/v2/frame/g" "github.com/gogf/gf/v2/os/gctx" ) func main() { var ( ctx = gctx.New() params = map[string]interface{}{ "passport": "", "password": "123456", "password2": "1234567", } rules = map[string]string{ "passport": "required|length:6,16", "password": "required|length:6,16|same:password2", "password2": "required|length:6,16", } messages = map[string]interface{}{ "passport": "账号不能为空|账号长度应当在{min}到{max}之间", "password": map[string]string{ "required": "密码不能为空", "same": "两次密码输入不相等", }, } ) err := g.Validator().Messages(messages).Rules(rules).Data(params).Run(ctx) if err != nil { g.Dump(err.Maps()) } } ``` 该示例同时也展示了`messages`自定义错误信息传递的两种数据类型,`string`或者`map[string]string`。其中`map[string]string`类型参数需要指定对应字段、对应规则的错误提示信息,是一个二维的「关联数组」。该示例执行后,终端输出: ```javascript { "passport": { "length": "账号长度应当在6到16之间", "required": "账号不能为空" }, "password": { "same": "两次密码输入不相等" } } ``` # Map校验-校验顺序性 ## 基本介绍 如果将之前的示例代码多执行几次之后会发现,返回的结果是没有排序的,而且字段及规则输出的先后顺序完全是随机的。即使我们使用`FirstItem`、`FirstString()`等其他方法获取校验结果也是一样,返回的校验结果不固定。那是因为校验的规则我们传递的是`map`类型,而`golang`的`map`类型并不具有有序性,因此校验的结果和规则一样是随机的,同一个校验结果的同一个校验方法多次获取结果值返回的可能也不一样了。 ## 顺序校验 我们来改进一下以上的示例:校验结果中如果不满足 `required` 那么返回对应的错误信息,否则才是后续的校验错误信息;也就是说,返回的错误信息应当和我设定规则时的顺序一致。如下: ```go package main import ( "fmt" "github.com/gogf/gf/v2/frame/g" "github.com/gogf/gf/v2/os/gctx" ) func main() { var ( ctx = gctx.New() params = map[string]interface{}{ "passport": "", "password": "123456", "password2": "1234567", } rules = []string{ "passport@required|length:6,16#账号不能为空|账号长度应当在{min}到{max}之间", "password@required|length:6,16|same:password2#密码不能为空|密码长度应当在{min}到{max}之间|两次密码输入不相等", "password2@required|length:6,16#", } ) err := g.Validator().Rules(rules).Data(params).Run(ctx) if err != nil { fmt.Println(err.Map()) fmt.Println(err.FirstItem()) fmt.Println(err.FirstError()) } } ``` 执行后,终端输出: ``` map[length:账号长度应当在6到16之间 required:账号不能为空] passport map[length:账号长度应当在6到16之间 required:账号不能为空] 账号不能为空 ``` 可以看到,我们想要校验结果满足顺序性,只需要将`rules`参数的类型修改为`[]string`,按照一定的规则设定即可,并且`messages`参数既可以定义到`rules`参数中,也可以分开传入(使用第三个参数)。`rules`的中的校验规则编写请参考章节[Struct校验-基本使用](./gvalid-parameter-type-struct-example.md)。 # Struct校验-基本使用 `Struct`校验常使用以下链式操作方式: ```go g.Validator().Data(object).Run(ctx) ``` ## 校验 `tag` 规则介绍 在开始介绍 `Struct` 参数类型校验之前,我们来介绍一下常用的校验 `tag` 规则。规则如下: ``` [属性别名@]校验规则[#错误提示] ``` 其中: - `属性别名` 和 `错误提示` 为 **非必需字段**, `校验规则` 是 **必需字段。** - `属性别名` 非必需字段,指定在校验中使用的对应 `struct` 属性的别名,同时校验后返回的 `error` 对象中的也将使用该别名返回。例如在处理请求表单时比较有用,因为表单的字段名称往往和 `struct` 的属性名称不一致。大部分场景下不需要设置属性别名,默认直接使用属性名称即可。 - `校验规则` 则为当前属性的校验规则,多个校验规则请使用 `|` 符号组合,例如: `required|between:1,100`。 - `错误提示` 非必需字段,表示自定义的错误提示信息,当规则校验时对默认的错误提示信息进行覆盖。 ## 校验 `tag` 使用示例 ```go package main import ( "github.com/gogf/gf/v2/frame/g" "github.com/gogf/gf/v2/os/gctx" ) type User struct { Uid int `v:"uid @integer|min:1#|请输入用户ID"` Name string `v:"name @required|length:6,30#请输入用户名称|用户名称长度非法"` Pass1 string `v:"password1@required|password3"` Pass2 string `v:"password2@required|password3|same:Pass1#|密码格式不合法|两次密码不一致,请重新输入"` } func main() { var ( ctx = gctx.New() user = &User{ Name: "john", Pass1: "Abc123!@#", Pass2: "123", } ) err := g.Validator().Data(user).Run(ctx) if err != nil { g.Dump(err.Items()) } } ``` 可以看到,我们可以对在 `struct` 定义时使用了 `gvalid` 的 `gvalid tag` 来绑定校验的规则及错误提示信息。在此示例代码中, `same:password1` 规则同使用 `same:Pass1` 规则是一样的效果。 **也就是说,在数据校验中,可以同时使用原有的 `struct` 属性名称,也可以使用别名。但是,返回的结果中只会使用别名返回,这也是别名最大的用途。** 此外,在对 `struct` 对象进行校验时,也可以传递校验或者和错误提示参数,这个时候会覆盖 `struct` 在定义时绑定的对应参数。 以上示例执行后,输出结果为: ``` [ { "uid": { "min": "请输入用户ID", }, }, { "name": { "length": "用户名称长度非法", }, }, { "password2": { "password3": "密码格式不合法", }, }, ] ``` ## 使用 `map` 指定校验规则 ```go package main import ( "github.com/gogf/gf/v2/frame/g" "github.com/gogf/gf/v2/os/gctx" ) func main() { type User struct { Age int Name string } var ( ctx = gctx.New() user = User{Name: "john"} rules = map[string]string{ "Name": "required|length:6,16", "Age": "between:18,30", } messages = map[string]interface{}{ "Name": map[string]string{ "required": "名称不能为空", "length": "名称长度为{min}到{max}个字符", }, "Age": "年龄为18到30周岁", } ) err := g.Validator().Rules(rules).Messages(messages).Data(user).Run(ctx) if err != nil { g.Dump(err.Maps()) } } ``` 在以上示例中, `Age` 属性由于默认值 `0` 的存在,因此会引起 `required` 规则的失效,因此这里没有使用 `required` 规则而是使用 `between` 规则来进行校验。示例代码执行后,终端输出: ``` { "Age": { "between": "年龄为18到30周岁" }, "Name": { "length": "名称长度为6到16个字符" } } ``` ## 结构体递归校验(嵌套校验) 支持递归的结构体校验(嵌套校验),即如果属性也是结构体(也支持嵌套结构体( `embedded`)),那么将会自动将该属性执行递归校验。使用示例: ```go package main import ( "github.com/gogf/gf/v2/frame/g" "github.com/gogf/gf/v2/os/gctx" ) func main() { type Pass struct { Pass1 string `v:"password1@required|same:password2#请输入您的密码|您两次输入的密码不一致"` Pass2 string `v:"password2@required|same:password1#请再次输入您的密码|您两次输入的密码不一致"` } type User struct { Pass Id int Name string `valid:"name@required#请输入您的姓名"` } var ( ctx = gctx.New() user = &User{ Name: "john", Pass: Pass{ Pass1: "1", Pass2: "2", }, } ) err := g.Validator().Data(user).Run(ctx) g.Dump(err.Maps()) } ``` 或者属性为嵌套结构体( `embedded`)的场景: ```go package main import ( "github.com/gogf/gf/v2/frame/g" "github.com/gogf/gf/v2/os/gctx" ) func main() { type Pass struct { Pass1 string `v:"password1@required|same:password2#请输入您的密码|您两次输入的密码不一致"` Pass2 string `v:"password2@required|same:password1#请再次输入您的密码|您两次输入的密码不一致"` } type User struct { Id int Name string `valid:"name@required#请输入您的姓名"` Pass Pass } var ( ctx = gctx.New() user = &User{ Name: "john", Pass: Pass{ Pass1: "1", Pass2: "2", }, } ) err := g.Validator().Data(user).Run(ctx) g.Dump(err.Maps()) } ``` 执行后,终端输出: ``` { "password1": { "same": "您两次输入的密码不一致", }, "password2": { "same": "您两次输入的密码不一致", }, } ``` 更多递归校验的介绍,请参考章节: [数据校验-递归校验](./gvalid-recursive-validating.md) # Struct校验-Assoc关联 为了避免结构体默认值带来的困惑,从`goframe v2.0`版本开始,我们增加了一个`Assoc`方法,用于结构体校验时严格按照给定的参数而不是按照结构体的属性值(避免结构体属性默认值的影响),而校验规则同样会自动读取结构体中的`gvalid tag`。 **提示** 该特定对接收客户端请求参数校验的场景特别有用。 ## 使用示例 ```go package main import ( "github.com/gogf/gf/v2/frame/g" "github.com/gogf/gf/v2/os/gctx" ) func main() { type User struct { Name string `v:"required#请输入用户姓名"` Type int `v:"required#请选择用户类型"` } var ( ctx = gctx.New() user = User{} data = g.Map{ "name": "john", } ) err := g.Validator().Assoc(data).Data(user).Run(ctx) if err != nil { g.Dump(err.Items()) } } ``` 执行后,终端输出: ``` [ { "Type": { "required": "请选择用户类型" } } ] ``` 可以看到,结构体中的属性`Type`校验规则`required`并没有受到默认值的影响,仍旧被执行了预期的校验检查。 # 数据校验-自定义规则 虽然 `gvalid` 已经内置了常见的数十种校验规则,但是在部分业务场景下我们需要自定义校验规则,特别是一些可以重复使用的业务相关的校验规则。当然, `gvalid` 如此的强大和贴心,她已经考虑得如此周全。自定义校验规则可以实现灵活性强,可复用性高的校验特性。 # 自定义规则-规则注册 ## 相关数据结构 自定义规则方法定义,以及对应的输入参数数据结构。 ```go // RuleFuncInput holds the input parameters that passed to custom rule function RuleFunc. type RuleFuncInput struct { // Rule specifies the validation rule string, like "required", "between:1,100", etc. Rule string // Message specifies the custom error message or configured i18n message for this rule. Message string // Value specifies the value for this rule to validate. Value *gvar.Var // Data specifies the `data` which is passed to the Validator. It might be a type of map/struct or a nil value. // You can ignore the parameter `Data` if you do not really need it in your custom validation rule. Data *gvar.Var } // RuleFunc is the custom function for data validation. type RuleFunc func(ctx context.Context, in RuleFuncInput) error ``` 方法参数简要说明: 1. 上下文参数`ctx`是必须的。 2. `RuleFuncInput`数据结构说明: - `Rule`表示当前的校验规则,包含规则的参数,例如:`required`、`between:1,100`、`length:6`等等。 - `Message`参数表示在校验失败后返回的校验错误提示信息。 - `Value`参数表示被校验的数据值,注意类型是一个`*gvar.Var`泛型,因此可以传递任意类型的参数。 - `Data`参数表示校验时传递的参数,例如校验的是一个`map`或者`struct`时,往往在联合校验时有用。需要注意的是,这个值是运行时输入的,值可能是`nil`。 **提示** 自定义错误默认情况下已支持`i18n`特性,因此只需要按照`gf.gvalid.rule.自定义规则名称`配置`i18n`转译信息即可,该信息在校验失败时会自动从`i18n`管理器获取后,通过`Message`参数传入给注册的自定义校验方法中。 ## 全局校验规则注册 自定义规则分为两种:全局规则注册和局部规则注册。 全局规则是全局生效的规则,注册之后无论是使用方法还是对象来执行数据校验都可以使用自定义的规则。 注册校验方法: ```go // RegisterRule registers custom validation rule and function for package. func RegisterRule(rule string, f RuleFunc) { customRuleFuncMap[rule] = f } // RegisterRuleByMap registers custom validation rules using map for package. func RegisterRuleByMap(m map[string]RuleFunc) { for k, v := range m { customRuleFuncMap[k] = v } } ``` 需要按照`RuleFunc`类型的方法定义,实现一个需要的校验方法,随后使用`RegisterRule`注册到`gvalid`模块中全局管理。该注册逻辑往往是在程序初始化时执行。该方法在对数据进行校验时将会被自动调用,方法返回`nil`表示校验通过,否则应当返回一个非空的`error`类型值。 **注意** 注意事项:自定义规则的注册方法不支持并发调用,需要在程序启动时进行注册(例如在`boot`包中处理),无法在运行时动态注册,否则会产生并发安全问题。 ### 示例1,订单ID存在校验 在电商业务中,当我们对订单进行操作时,可以通过自定义规则校验给定的订单ID是否存在,因此我们可以注册一个 `order-exist` 的全局规则来实现。 ```go package main import ( "context" "fmt" "github.com/gogf/gf/v2/database/gdb" "github.com/gogf/gf/v2/errors/gerror" "github.com/gogf/gf/v2/frame/g" "github.com/gogf/gf/v2/os/gctx" "github.com/gogf/gf/v2/util/gvalid" "time" ) type Request struct { OrderId int64 `v:"required|order-exist"` ProductName string Amount int64 // ... } func init() { rule := "order-exist" gvalid.RegisterRule(rule, RuleOrderExist) } func RuleOrderExist(ctx context.Context, in gvalid.RuleFuncInput) error { // SELECT COUNT(*) FROM `order` WHERE `id` = xxx count, err := g.Model("order"). Ctx(ctx). Cache(gdb.CacheOption{ Duration: time.Hour, Name: "", Force: false, }). WhereNot("id", in.Value.Int64()). Count() if err != nil { return err } if count == 0 { return gerror.Newf(`invalid order id "%d"`, in.Value.Int64()) } return nil } func main() { var ( ctx = gctx.New() req = &Request{ OrderId: 65535, ProductName: "HikingShoe", Amount: 10000, } ) err := g.Validator().CheckStruct(ctx, req) fmt.Println(err) } ``` ### 示例2,用户唯一性规则 在用户注册时,我们往往需要校验当前用户提交的名称/账号是否唯一,因此我们可以注册一个 `unique-name` 的全局规则来实现。 ```go package main import ( "context" "fmt" "github.com/gogf/gf/v2/database/gdb" "github.com/gogf/gf/v2/errors/gerror" "github.com/gogf/gf/v2/frame/g" "github.com/gogf/gf/v2/os/gctx" "github.com/gogf/gf/v2/util/gvalid" "time" ) type User struct { Id int Name string `v:"required|unique-name#请输入用户名称|用户名称已被占用"` Pass string `v:"required|length:6,18"` } func init() { rule := "unique-name" gvalid.RegisterRule(rule, RuleUniqueName) } func RuleUniqueName(ctx context.Context, in gvalid.RuleFuncInput) error { var user *User if err := in.Data.Scan(&user); err != nil { return gerror.Wrap(err, `Scan data to user failed`) } // SELECT COUNT(*) FROM `user` WHERE `id` != xxx AND `name` != xxx count, err := g.Model("user"). Ctx(ctx). Cache(gdb.CacheOption{ Duration: time.Hour, Name: "", Force: false, }). WhereNot("id", user.Id). WhereNot("name", user.Name). Count() if err != nil { return err } if count > 0 { if in.Message != "" { return gerror.New(in.Message) } return gerror.Newf(`user name "%s" is already token by others`, user.Name) } return nil } func main() { var ( ctx = gctx.New() user = &User{ Id: 1, Name: "john", Pass: "123456", } ) err := g.Validator().CheckStruct(ctx, user) fmt.Println(err) } ``` ## 局部校验规则注册 局部规则是仅在当前校验对象下生效规则,校验规则是注册到当前使用的链式操作流程中而不是全局中。 注册方法: ```go // RuleFunc registers one custom rule function to current Validator. func (v *Validator) RuleFunc(rule string, f RuleFunc) *Validator // RuleFuncMap registers multiple custom rule functions to current Validator. func (v *Validator) RuleFuncMap(m map[string]RuleFunc) *Validator ``` 简要介绍: - `RuleFunc` 方法用于注册单个自定义校验规则到当前对象。 - `RuleFuncMap` 方法用于注册多个自定义校验规则到当前对象。 使用示例: 我们将上面其中一个例子改为局部校验规则注册。 ```go package main import ( "context" "fmt" "github.com/gogf/gf/v2/database/gdb" "github.com/gogf/gf/v2/errors/gerror" "github.com/gogf/gf/v2/frame/g" "github.com/gogf/gf/v2/os/gctx" "github.com/gogf/gf/v2/util/gvalid" "time" ) type Request struct { OrderId int64 ProductName string Amount int64 // ... } func RuleOrderExist(ctx context.Context, in gvalid.RuleFuncInput) error { // SELECT COUNT(*) FROM `order` WHERE `id` = xxx count, err := g.Model("order"). Ctx(ctx). Cache(gdb.CacheOption{ Duration: time.Hour, Name: "", Force: false, }). WhereNot("id", in.Value.Int64()). Count() if err != nil { return err } if count == 0 { return gerror.Newf(`invalid order id "%d"`, in.Value.Int64()) } return nil } func main() { var ( ctx = gctx.New() req = &Request{ OrderId: 65535, ProductName: "HikingShoe", Amount: 10000, } ) err := g.Validator().RuleFunc("order-exist", RuleOrderExist).Data(req).Run(ctx) fmt.Println(err) } ``` # 自定义规则-完整数据校验 ## 基本介绍 大家也许已经注意到,当我们给定一个`struct`时,我们的规则只能对其中的键值或者属性进行校验,如果我们想要通过规则完整校验`struct`这个对象时,居然无法注册校验组件的自定义校验规则。当然,我们的校验组件也支持直接校验当前的`struct`对象。我们来看一个例子,在这个例子中,我们需要对创建的用户请求进行完整的自定义校验,并注册一个`UserCreateReq`的校验规则来实现。 ## 使用示例 ```go package main import ( "context" "fmt" "github.com/gogf/gf/v2/database/gdb" "github.com/gogf/gf/v2/errors/gerror" "github.com/gogf/gf/v2/frame/g" "github.com/gogf/gf/v2/os/gctx" "github.com/gogf/gf/v2/util/gvalid" "time" ) type UserCreateReq struct { g.Meta `v:"UserCreateReq"` Name string Pass string } func RuleUserCreateReq(ctx context.Context, in gvalid.RuleFuncInput) error { var req *UserCreateReq if err := in.Data.Scan(&req); err != nil { return gerror.Wrap(err, `Scan data to UserCreateReq failed`) } // SELECT COUNT(*) FROM `user` WHERE `name` = xxx count, err := g.Model("user").Ctx(ctx).Cache(gdb.CacheOption{ Duration: time.Hour, Name: "", Force: false, }).Where("name", req.Name).Count() if err != nil { return err } if count > 0 { return gerror.Newf(`The name "%s" is already token by others`, req.Name) } return nil } func main() { var ( ctx = gctx.New() user = &UserCreateReq{ Name: "john", Pass: "123456", } ) err := g.Validator().RuleFunc("UserCreateReq", RuleUserCreateReq).Data(user).Run(ctx) fmt.Println(err) } ``` 可以看到,我们通过给结构体一个`g.Meta`嵌入的元数据,并绑定`UserCreateReq`的自定义规则,`g.Meta`作为结构体的一部分,当我们通过`CheckStruct`校验该结构体对象时,便可以通过`UserCreateReq`来实现校验。 上面的例子,我们执行后,终端输出: ``` The name "john" is already token ``` # 类型转换 ## 基本介绍 `GoFrame` 框架提供了非常强大易用的类型转换包 `gconv`,可以实现将常用数据类型转换为指定的数据类型,对常用基本数据类型之间的无缝转换,同时也支持任意类型到 `struct` 对象的转换。由于 `gconv` 模块内部大量优先使用了断言而非反射,因此执行的效率非常高。 **注意事项:** `gconv` 包的主要目标是提供简便且高效的类型转换功能,开发者应当注意转换的输入参数以及当前使用的业务场景,部分方法如果转换失败,那么方法并不会返回错误原因,也不会产生 `panic`,而是直接以转换失败后的数值返回。因此,开发者往往需要结合返回值以及当前使用的业务场景来综合判断结果的正确性。 **使用方式:** ```go import "github.com/gogf/gf/v2/util/gconv" ``` **接口文档**: [https://pkg.go.dev/github.com/gogf/gf/v2/util/gconv](https://pkg.go.dev/github.com/gogf/gf/v2/util/gconv) **方法列表:** 方法列表可能滞后于代码,详情请查看接口文档。 # 类型转换-基本类型 常用基本类型的转换方法比较简单,我们这里使用一个例子来演示转换方法的使用及效果。 ## 基本示例 **提示** 更多的类型转换方法请参考接口文档: [https://pkg.go.dev/github.com/gogf/gf/v2/util/gconv](https://pkg.go.dev/github.com/gogf/gf/v2/util/gconv) ```go package main import ( "fmt" "github.com/gogf/gf/v2/util/gconv" ) func main() { i := 123.456 fmt.Printf("%10s %v\n", "Int:", gconv.Int(i)) fmt.Printf("%10s %v\n", "Int8:", gconv.Int8(i)) fmt.Printf("%10s %v\n", "Int16:", gconv.Int16(i)) fmt.Printf("%10s %v\n", "Int32:", gconv.Int32(i)) fmt.Printf("%10s %v\n", "Int64:", gconv.Int64(i)) fmt.Printf("%10s %v\n", "Uint:", gconv.Uint(i)) fmt.Printf("%10s %v\n", "Uint8:", gconv.Uint8(i)) fmt.Printf("%10s %v\n", "Uint16:", gconv.Uint16(i)) fmt.Printf("%10s %v\n", "Uint32:", gconv.Uint32(i)) fmt.Printf("%10s %v\n", "Uint64:", gconv.Uint64(i)) fmt.Printf("%10s %v\n", "Float32:", gconv.Float32(i)) fmt.Printf("%10s %v\n", "Float64:", gconv.Float64(i)) fmt.Printf("%10s %v\n", "Bool:", gconv.Bool(i)) fmt.Printf("%10s %v\n", "String:", gconv.String(i)) fmt.Printf("%10s %v\n", "Bytes:", gconv.Bytes(i)) fmt.Printf("%10s %v\n", "Strings:", gconv.Strings(i)) fmt.Printf("%10s %v\n", "Ints:", gconv.Ints(i)) fmt.Printf("%10s %v\n", "Floats:", gconv.Floats(i)) fmt.Printf("%10s %v\n", "Interfaces:", gconv.Interfaces(i)) } ``` 执行后,输出结果为: ``` Int: 123 Int8: 123 Int16: 123 Int32: 123 Int64: 123 Uint: 123 Uint8: 123 Uint16: 123 Uint32: 123 Uint64: 123 Float32: 123.456 Float64: 123.456 Bool: true String: 123.456 Bytes: [119 190 159 26 47 221 94 64] Strings: [123.456] Ints: [123] Floats: [123.456] Interfaces: [123.456] ``` ## 注意事项 数字转换方法例如`gconv.Int`/`Uint`等等,当给定的转换参数为字符串时,会自动识别十六进制、八进制。 ### 十六进制转换 `gconv`将`0x`开头的数字字符串当做十六进制转换。例如,`gconv.Int("0xff")`将会返回`255`。 # 类型转换-Map转换 `gconv.Map` 支持将任意的 `map` 或 `struct`/ `*struct` 类型转换为常用的 `map[string]interface{}` 类型。当转换参数为 `struct`/ `*struct` 类型时,支持自动识别 `struct` 的 `c/gconv/json` 标签,并且可以通过 `Map` 方法的第二个参数 `tags` 指定自定义的转换标签,以及多个标签解析的优先级。如果转换失败,返回 `nil`。 **提示** 属性标签:当转换 `struct`/ `*struct` 类型时,支持 `c/gconv/json` 属性标签,也支持 `-` 及 `omitempty` 标签属性。当使用 `-` 标签属性时,表示该属性不执行转换;当使用 `omitempty` 标签属性时,表示当属性为空时(空指针 `nil`, 数字 `0`, 字符串 `""`, 空数组 `[]` 等)不执行转换。具体请查看随后示例。 常用转换方法: ```go func Map(value interface{}, tags ...string) map[string]interface{} func MapDeep(value interface{}, tags ...string) map[string]interface{} ``` 其中, `MapDeep` 支持递归转换,即会递归转换属性中的 `struct`/ `*struct` 对象。 **提示** 更多的 `map` 相关转换方法请参考接口文档: [https://pkg.go.dev/github.com/gogf/gf/v2/util/gconv](https://pkg.go.dev/github.com/gogf/gf/v2/util/gconv) ## 基本示例 ```go package main import ( "github.com/gogf/gf/v2/frame/g" "github.com/gogf/gf/v2/util/gconv" ) func main() { type User struct { Uid int `c:"uid"` Name string `c:"name"` } // 对象 g.Dump(gconv.Map(User{ Uid: 1, Name: "john", })) // 对象指针 g.Dump(gconv.Map(&User{ Uid: 1, Name: "john", })) // 任意map类型 g.Dump(gconv.Map(map[int]int{ 100: 10000, })) } ``` 执行后,终端输出: ``` { "name": "john", "uid": 1, } { "name": "john", "uid": 1, } { "100": 10000, } ``` ## 属性标签 我们可以通过 `c/gconv/json` 标签来自定义转换后的 `map` 键名,当多个标签存在时,按照 `gconv/c/json` 的标签顺序进行优先级识别。 ```go package main import ( "github.com/gogf/gf/v2/frame/g" "github.com/gogf/gf/v2/util/gconv" ) func main() { type User struct { Uid int Name string `c:"-"` NickName string `c:"nickname, omitempty"` Pass1 string `c:"password1"` Pass2 string `c:"password2"` } user := User{ Uid: 100, Name: "john", Pass1: "123", Pass2: "456", } g.Dump(gconv.Map(user)) } ``` 执行后,终端输出: ``` { "Uid": 100, "password1": "123", "password2": "456", "nickname": "", } ``` ## 自定义标签 此外,我们也可以给 `struct` 的属性自定义自己的标签名称,并在 `map` 转换时通过第二个参数指定标签优先级。 ```go package main import ( "github.com/gogf/gf/v2/frame/g" "github.com/gogf/gf/v2/util/gconv" ) func main() { type User struct { Id int `c:"uid"` Name string `my-tag:"nick-name" c:"name"` } user := &User{ Id: 1, Name: "john", } g.Dump(gconv.Map(user, "my-tag")) } ``` 执行后,输出结果为: ``` { "nick-name": "john", "uid": 1, } ``` ## 递归转换 当参数为 `map`/ `struct`/ `*struct` 类型时,如果键值/属性为一个对象(或者对象指针)时,并且不是 `embedded` 结构体且没有任何的别名标签绑定, `Map` 方法将会将对象转换为结果的一个键值。我们可以使用 `MapDeep` 方法递归转换参数的子对象,即把属性也转换为 `map` 类型。我们来看个例子。 ```go package main import ( "fmt" "github.com/gogf/gf/v2/frame/g" "github.com/gogf/gf/v2/util/gconv" "reflect" ) func main() { type Base struct { Id int `c:"id"` Date string `c:"date"` } type User struct { UserBase Base `c:"base"` Passport string `c:"passport"` Password string `c:"password"` Nickname string `c:"nickname"` } user := &User{ UserBase: Base{ Id: 1, Date: "2019-10-01", }, Passport: "john", Password: "123456", Nickname: "JohnGuo", } m1 := gconv.Map(user) m2 := gconv.MapDeep(user) g.Dump(m1, m2) fmt.Println(reflect.TypeOf(m1["base"])) fmt.Println(reflect.TypeOf(m2["base"])) } ``` 执行后,终端输出结果为: ``` { "base": { Id: 1, Date: "2019-10-01", }, "passport": "john", "password": "123456", "nickname": "JohnGuo", } { "base": { "id": 1, "date": "2019-10-01", }, "passport": "john", "password": "123456", "nickname": "JohnGuo", } main.Base map[string]interface {} ``` 看出来差别了吗? # 类型转换-Struct转换 项目中我们经常会遇到大量 `struct` 的使用,以及各种数据类型到 `struct` 的转换/赋值(特别是 `json`/ `xml`/各种协议编码转换)。为提高编码及项目维护效率, `gconv` 模块为各位开发者带来了极大的福利,为数据解析提供了更高的灵活度。 `gconv` 模块通过 `Struct` 转换方法执行 `struct` 类型转换,其定义如下: ```go // Struct maps the params key-value pairs to the corresponding struct object's attributes. // The third parameter `mapping` is unnecessary, indicating the mapping rules between the // custom key name and the attribute name(case sensitive). // // Note: // 1. The `params` can be any type of map/struct, usually a map. // 2. The `pointer` should be type of *struct/**struct, which is a pointer to struct object // or struct pointer. // 3. Only the public attributes of struct object can be mapped. // 4. If `params` is a map, the key of the map `params` can be lowercase. // It will automatically convert the first letter of the key to uppercase // in mapping procedure to do the matching. // It ignores the map key, if it does not match. func Struct(params interface{}, pointer interface{}, mapping ...map[string]string) (err error) ``` 其中: 1. `params` 为需要转换到 `struct` 的变量参数,可以为任意数据类型,常见的数据类型为 `map`。 2. `pointer` 为需要执行转的目标 `struct` 对象,这个参数必须为该 `struct` 的对象指针,转换成功后该对象的属性将会更新。 3. `mapping` 为自定义的 `map键名` 到 `strcut属性` 之间的映射关系,此时 `params` 参数必须为 `map` 类型,否则该参数无意义。大部分场景下使用可以不用提供该参数,直接使用默认的转换规则即可。 **提示** 更多的 `struct` 相关转换方法请参考接口文档: [https://pkg.go.dev/github.com/gogf/gf/v2/util/gconv](https://pkg.go.dev/github.com/gogf/gf/v2/util/gconv) ## 转换规则 `gconv` 模块的 `struct` 转换特性非常强大,支持任意数据类型到 `struct` 属性的映射转换。在没有提供自定义 `mapping` 转换规则的情况下,默认的转换规则如下: 1. `struct` 中需要匹配的属性必须为 **公开属性**(首字母大写)。 2. 根据 `params` 类型的不同,逻辑会有不同: - `params` 参数类型为 `map`:键名会自动按照 **不区分大小写** 且 **忽略特殊字符** 的形式与struct属性进行匹配。 - `params` 参数为其他类型:将会把该变量值与 `struct` 的第一个属性进行匹配。 -  此外,如果 `struct` 的属性为复杂数据类型如 `slice`, `map`, `strcut` 那么会进行递归匹配赋值。 3. 如果匹配成功,那么将键值赋值给属性,如果无法匹配,那么忽略该键值。 ## 匹配规则优先级说明(只针对于map到struct的转换) 1.如果 `mapping` 参数不为空,将会按照 `mapping` 的 `key ` 到 `strcut字段名` 之间的映射关系。 2.如果设置了字段的 `tag`,会使用 `tag` 来匹配 `params` 参数的  `key`。        如果没有设置 `tag`,gconv将会依次按照 `gconv, param, c, p, json` 这个顺序来查找字段是否有对应的 `tag` 3.按照 `字段名` 匹配。 4.如果以上都没有匹配到,gconv将会遍历 `params` 参数所有的 `key`,按照 以下规则来匹配 `字段名`:忽略大小写和下划线 `key`: 忽略大小写和下划线和特殊字符 提示 **注意** 没有特殊情况,请尽量满足前三条规则,第四条规则性能较差 以下是几个 `map` 键名与 `struct` 属性名称的示例: ``` map键名 struct属性 是否匹配 name Name match Email Email match nickname NickName match NICKNAME NickName match Nick-Name NickName match nick_name NickName match nick name NickName match NickName Nick_Name match Nick-name Nick_Name match nick_name Nick_Name match nick name Nick_Name match ``` ## 自动创建对象 当给定的 `pointer` 参数类型为 `**struct` 时, `Struct` 方法内部将会自动创建该 `struct` 对象,并修改传递变量指向的指针地址。 ```go package main import ( "github.com/gogf/gf/v2/frame/g" "github.com/gogf/gf/v2/util/gconv" ) func main() { type User struct { Uid int Name string } params := g.Map{ "uid": 1, "name": "john", } var user *User if err := gconv.Struct(params, &user); err != nil { panic(err) } g.Dump(user) } ``` 执行后,输出结果为: ``` { Uid: 1, Name: "john", } ``` ## `Struct` 递归转换 递归转换是指当 `struct` 对象包含子对象时,并且子对象是 `embedded` 方式定义时,可以将 `params` 参数数据(第一个参数)同时递归地映射到其子对象上,常用于带有继承对象的 `struct` 上。 ```go package main import ( "github.com/gogf/gf/v2/frame/g" "github.com/gogf/gf/v2/util/gconv" ) func main() { type Ids struct { Id int `json:"id"` Uid int `json:"uid"` } type Base struct { Ids CreateTime string `json:"create_time"` } type User struct { Base Passport string `json:"passport"` Password string `json:"password"` Nickname string `json:"nickname"` } data := g.Map{ "id" : 1, "uid" : 100, "passport" : "john", "password" : "123456", "nickname" : "John", "create_time" : "2019", } user := new(User) gconv.Struct(data, user) g.Dump(user) } ``` 执行后,终端输出结果为: ``` { Id: 1, Uid: 100, CreateTime: "2019", Passport: "john", Password: "123456", Nickname: "John", } ``` ## 示例1,基本使用 ```go package main import ( "github.com/gogf/gf/v2/frame/g" "github.com/gogf/gf/v2/util/gconv" ) type User struct { Uid int Name string SiteUrl string NickName string Pass1 string `c:"password1"` Pass2 string `c:"password2"` } func main() { var user *User // 使用默认映射规则绑定属性值到对象 user = new(User) params1 := g.Map{ "uid": 1, "Name": "john", "site_url": "https://goframe.org", "nick_name": "johng", "PASS1": "123", "PASS2": "456", } if err := gconv.Struct(params1, user); err == nil { g.Dump(user) } // 使用struct tag映射绑定属性值到对象 user = new(User) params2 := g.Map{ "uid": 2, "name": "smith", "site-url": "https://goframe.org", "nick name": "johng", "password1": "111", "password2": "222", } if err := gconv.Struct(params2, user); err == nil { g.Dump(user) } } ``` 可以看到,我们可以直接通过 `Struct` 方法将 `map` 按照默认规则绑定到 `struct` 上,也可以使用 `struct tag` 的方式进行灵活的设置。此外, `Struct` 方法有第三个 `map` 参数,用于指定自定义的参数名称到属性名称的映射关系。 执行后,输出结果为: ``` { Uid: 1, Name: "john", SiteUrl: "https://goframe.org", NickName: "johng", Pass1: "123", Pass2: "456", } { Uid: 2, Name: "smith", SiteUrl: "https://goframe.org", NickName: "johng", Pass1: "111", Pass2: "222", } ``` ## 示例2,复杂属性类型 属性支持 `struct` 对象或者 `struct` 对象指针(目标为指针且为 `nil` 时,转换时会自动初始化)转换。 ```go package main import ( "github.com/gogf/gf/v2/util/gconv" "github.com/gogf/gf/v2/frame/g" "fmt" ) func main() { type Score struct { Name string Result int } type User1 struct { Scores Score } type User2 struct { Scores *Score } user1 := new(User1) user2 := new(User2) scores := g.Map{ "Scores": g.Map{ "Name": "john", "Result": 100, }, } if err := gconv.Struct(scores, user1); err != nil { fmt.Println(err) } else { g.Dump(user1) } if err := gconv.Struct(scores, user2); err != nil { fmt.Println(err) } else { g.Dump(user2) } } ``` 执行后,输出结果为: ``` { Scores: { Name: "john", Result: 100, }, } { Scores: { Name: "john", Result: 100, }, } ``` # 类型转换-Structs转换 ## 基本介绍 我们之前提到可以使用 `Struct` 方法实现对 `struct` 对象的转换,那么我们当然也可以实现对 `struct` 数组的转换, `struct` 数组转换使用的是 `Structs` 方法实现。 `Structs` 方法建立在 `Struct` 方法的基础之上,所有的转换规则与 `Struct` 相同,只是增加了对 `struct` 数组类型的支持。在了解 `Structs` 方法之前,建议先了解 `Struct` 方法介绍: [类型转换-Struct转换](./gconv-struct.md) ## 方法定义 `Structs` 方法定义如下: ```go // Structs converts any slice to given struct slice. func Structs(params interface{}, pointer interface{}, mapping ...map[string]string) (err error) ``` 其中 `pointer` 目标转换参数类型需要为 `*[]struct/*[]*struct`。 ## 使用示例 我们来看一个简单示例即可理解。 ```go package main import ( "github.com/gogf/gf/v2/frame/g" "github.com/gogf/gf/v2/util/gconv" ) func main() { type User struct { Uid int Name string } params := g.Slice{ g.Map{ "uid": 1, "name": "john", }, g.Map{ "uid": 2, "name": "smith", }, } var users []*User if err := gconv.Structs(params, &users); err != nil { panic(err) } g.Dump(users) } ``` 执行后,终端输出: ``` [ { Uid: 1, Name: "john", }, { Uid: 2, Name: "smith", }, ] ``` # 类型转换-Scan转换 前面关于复杂类型的转换功能如果大家觉得还不够的话,那么可以了解下 `Scan` 转换方法,该方法可以实现对任意参数到 `基础数据类型/struct/struct数组/map/map数组` 的转换,并且根据开发者输入的转换目标参数自动识别执行转换。 该方法定义如下: ```go // Scan automatically checks the type of `pointer` and converts `params` to `pointer`. // It supports various types of parameter conversions, including: // 1. Basic types (int, string, float, etc.) // 2. Pointer types // 3. Slice types // 4. Map types // 5. Struct types // // The `paramKeyToAttrMap` parameter is used for mapping between attribute names and parameter keys. func Scan(srcValue any, dstPointer any, paramKeyToAttrMap ...map[string]string) (err error) ``` ## 基础数据类型转换 `Scan` 方法在`v2.9`版本中增强了对基础数据类型的转换支持,可以将各种类型的值转换为基础数据类型,如 `int`、`string`、`bool` 等。 ### 整数类型转换 ```go package main import ( "github.com/gogf/gf/v2/frame/g" "github.com/gogf/gf/v2/util/gconv" ) func main() { // 字符串转整数 var num int if err := gconv.Scan("123", &num); err != nil { panic(err) } g.Dump(num) // 输出: 123 // 浮点数转整数 var num2 int if err := gconv.Scan(123.45, &num2); err != nil { panic(err) } g.Dump(num2) // 输出: 123 // 布尔值转整数 var num3 int if err := gconv.Scan(true, &num3); err != nil { panic(err) } g.Dump(num3) // 输出: 1 } ``` ### 字符串类型转换 ```go package main import ( "github.com/gogf/gf/v2/frame/g" "github.com/gogf/gf/v2/util/gconv" ) func main() { // 整数转字符串 var str string if err := gconv.Scan(123, &str); err != nil { panic(err) } g.Dump(str) // 输出: "123" // 浮点数转字符串 var str2 string if err := gconv.Scan(123.45, &str2); err != nil { panic(err) } g.Dump(str2) // 输出: "123.45" // 布尔值转字符串 var str3 string if err := gconv.Scan(true, &str3); err != nil { panic(err) } g.Dump(str3) // 输出: "true" } ``` ### 布尔类型转换 ```go package main import ( "github.com/gogf/gf/v2/frame/g" "github.com/gogf/gf/v2/util/gconv" ) func main() { // 整数转布尔值 var b1 bool if err := gconv.Scan(1, &b1); err != nil { panic(err) } g.Dump(b1) // 输出: true // 字符串转布尔值 var b2 bool if err := gconv.Scan("true", &b2); err != nil { panic(err) } g.Dump(b2) // 输出: true // 0值转布尔值 var b3 bool if err := gconv.Scan(0, &b3); err != nil { panic(err) } g.Dump(b3) // 输出: false } ``` ### 指针类型转换 `Scan` 方法还支持指针类型的转换,可以将值转换为指针类型,或者将指针类型转换为另一个指针类型。 ```go package main import ( "github.com/gogf/gf/v2/frame/g" "github.com/gogf/gf/v2/util/gconv" ) func main() { // 值转指针 var numPtr *int if err := gconv.Scan(123, &numPtr); err != nil { panic(err) } g.Dump(numPtr) // 输出: 123 (指针指向的值) // 指针转指针 var num = 456 var numPtr2 *int if err := gconv.Scan(&num, &numPtr2); err != nil { panic(err) } g.Dump(numPtr2) // 输出: 456 (指针指向的值) } ``` ## 自动识别转换 `Struct` ```go package main import ( "github.com/gogf/gf/v2/frame/g" "github.com/gogf/gf/v2/util/gconv" ) func main() { type User struct { Uid int Name string } params := g.Map{ "uid": 1, "name": "john", } var user *User if err := gconv.Scan(params, &user); err != nil { panic(err) } g.Dump(user) } ``` 执行后,输出结果为: ``` { Uid: 1, Name: "john", } ``` ## 自动识别转换 `Struct` 数组 ```go package main import ( "github.com/gogf/gf/v2/frame/g" "github.com/gogf/gf/v2/util/gconv" ) func main() { type User struct { Uid int Name string } params := g.Slice{ g.Map{ "uid": 1, "name": "john", }, g.Map{ "uid": 2, "name": "smith", }, } var users []*User if err := gconv.Scan(params, &users); err != nil { panic(err) } g.Dump(users) } ``` 执行后,终端输出: ``` [ { Uid: 1, Name: "john", }, { Uid: 2, Name: "smith", }, ] ``` ## 自动识别转换Map ```go package main import ( "github.com/gogf/gf/v2/frame/g" "github.com/gogf/gf/v2/util/gconv" ) func main() { var ( user map[string]string params = g.Map{ "uid": 1, "name": "john", } ) if err := gconv.Scan(params, &user); err != nil { panic(err) } g.Dump(user) } ``` 执行后,输出结果为: ``` { "uid": "1", "name": "john", } ``` ## 自动识别转换 `Map` 数组 ```go package main import ( "github.com/gogf/gf/v2/frame/g" "github.com/gogf/gf/v2/util/gconv" ) func main() { var ( users []map[string]string params = g.Slice{ g.Map{ "uid": 1, "name": "john", }, g.Map{ "uid": 2, "name": "smith", }, } ) if err := gconv.Scan(params, &users); err != nil { panic(err) } g.Dump(users) } ``` 执行后,输出结果为: ``` [ { "uid": "1", "name": "john", }, { "uid": "2", "name": "smith", }, ] ``` ## Scan 高级选项(v2.10+) 从 `v2.10.0` 版本开始,`gconv` 包新增了 `ScanWithOptions` 函数,支持更灵活的转换控制选项,特别是 `OmitEmpty` 和 `OmitNil` 选项。 ### 方法定义 ```go // ScanWithOptions 自动检查 dstPointer 的类型并将 srcValue 转换为 dstPointer。 // 它与 Scan 函数相同,但接受一个或多个 ScanOption 值以进行额外的转换控制。 // // 使用 ScanWithOptions 时,"忽略"(omit)意味着跳过从源到目标的赋值, // 从而保留目标字段中的现有值。 // // - option.OmitEmpty,当设置为 true 时,跳过空源值的赋值(例如:空字符串、 // 零数值、零时间值、空切片或映射),保留目标中任何现有的非空值。 // // - option.OmitNil,当设置为 true 时,跳过 nil 源值的赋值, // 当源包含 nil 时保留目标中的现有值。 // func ScanWithOptions(srcValue any, dstPointer any, option ...ScanOption) (err error) ``` ### OmitEmpty 选项 `OmitEmpty` 选项用于在转换时跳过空值字段的赋值,保留目标结构体中已有的非空值。 **什么是空值?** - 空字符串(`""`) - 零数值(`0`、`0.0`) - 零时间值 - 空切片或空映射(`nil` 或长度为`0`) **使用场景:** 当需要将源数据合并到目标数据中,但不希望源数据中的空值覆盖目标数据中已有的值时,可以使用此选项。 **示例:** ```go package main import ( "github.com/gogf/gf/v2/frame/g" "github.com/gogf/gf/v2/util/gconv" ) func main() { type User struct { Name string Age int Email string } // 目标结构体包含初始值 person := User{ Name: "张三", Age: 30, Email: "zhangsan@example.com", } // 源数据包含一些空值 sourceData := g.Map{ "Name": "", // 空字符串 "Age": 25, // 非空值 "Email": "", // 空字符串 } // 使用 OmitEmpty 选项进行转换 err := gconv.ScanWithOptions(sourceData, &person, gconv.ScanOption{ OmitEmpty: true, }) if err != nil { panic(err) } g.Dump(person) } ``` 执行后,输出结果为: ```js { Name: "张三", // 保持原值,因为源值为空字符串 Age: 25, // 更新为源值 Email: "zhangsan@example.com", // 保持原值,因为源值为空字符串 } ``` ### OmitNil 选项 `OmitNil` 选项用于在转换时跳过 `nil` 值字段的赋值,保留目标结构体中已有的值。 **使用场景:** 当的源数据是 `map[string]any` 类型,其中可能包含 `nil` 值,而不希望这些 `nil` 值覆盖目标结构体中已有的值时,可以使用此选项。 **示例:** ```go package main import ( "github.com/gogf/gf/v2/frame/g" "github.com/gogf/gf/v2/util/gconv" ) func main() { type Person struct { Name string Age int Email string } // 目标结构体包含初始值 person := Person{ Name: "李四", Age: 0, Email: "lisi@example.com", } // 源数据包含 nil 值 sourceData := map[string]any{ "Name": nil, // nil 值 "Age": 30, // 非 nil 值 "Email": nil, // nil 值 } // 使用 OmitNil 选项进行转换 err := gconv.ScanWithOptions(sourceData, &person, gconv.ScanOption{ OmitNil: true, }) if err != nil { panic(err) } g.Dump(person) } ``` 执行后,输出结果为: ```js { Name: "李四", // 保持原值,因为源值为 nil Age: 30, // 更新为源值 Email: "lisi@example.com", // 保持原值,因为源值为 nil } ``` ### 同时使用 OmitEmpty 和 OmitNil 可以同时使用这两个选项来实现更灵活的转换控制: ```go package main import ( "github.com/gogf/gf/v2/frame/g" "github.com/gogf/gf/v2/util/gconv" ) func main() { type User struct { Name string Age int Email string } // 目标结构体包含初始值 user := User{ Name: "王五", Age: 0, Email: "wangwu@example.com", } // 源数据同时包含空值和 nil 值 sourceData := map[string]any{ "Name": "", // 空字符串 "Age": 25, // 非空值 "Email": nil, // nil 值 } // 同时使用 OmitEmpty 和 OmitNil 选项 err := gconv.ScanWithOptions(sourceData, &user, gconv.ScanOption{ OmitEmpty: true, OmitNil: true, }) if err != nil { panic(err) } g.Dump(user) } ``` 执行后,输出结果为: ```js { Name: "王五", // 保持原值,因为源值为空字符串 Age: 25, // 更新为源值 Email: "wangwu@example.com", // 保持原值,因为源值为 nil } ``` # 类型转换-Converter `gconv.Converter`是`GoFrame v2.9`版本新增的类型转换接口,通过创建转换对象的形式来使用类型转换能力,提供了一种更严谨、更灵活的类型转换机制。与传统的`gconv`包方法不同,`Converter`接口在转换失败时会返回错误,而不是默认返回零值或空值。 **功能特点**: 1. **严谨的错误处理**:转换失败时返回错误信息,避免静默失败导致的潜在问题 2. **类型安全**:提供更严格的类型检查,减少运行时错误 3. **可扩展性**:支持注册自定义类型转换函数,适应复杂业务场景 4. **一致的API**:所有转换方法遵循相同的模式,使用更加一致 5. **独立实例**:可以创建多个转换器实例,每个实例可以有不同的配置和注册的转换函数 ## 基本介绍 `Converter`接口是一个组合接口,包含了多个子接口,提供了全面的类型转换能力: ```go type Converter interface { ConverterForConvert ConverterForRegister ConverterForInt ConverterForUint ConverterForTime ConverterForFloat ConverterForMap ConverterForSlice ConverterForStruct ConverterForBasic } ``` 这些子接口分别提供了不同类型的转换功能,例如: - `ConverterForBasic`:基本类型转换(字符串、布尔值等) - `ConverterForInt`:整数类型转换 - `ConverterForUint`:无符号整数类型转换 - `ConverterForFloat`:浮点数类型转换 - `ConverterForTime`:时间类型转换 - `ConverterForMap`:`Map`类型转换 - `ConverterForSlice`:切片类型转换 - `ConverterForStruct`:结构体类型转换 - `ConverterForConvert`:自定义类型转换 - `ConverterForRegister`:注册自定义转换函数 ## 创建`Converter`对象 使用`gconv.NewConverter()`函数可以创建一个新的`Converter`对象: ```go converter := gconv.NewConverter() ``` ## 基本用法 ### 基本类型转换 ```go package main import ( "fmt" "github.com/gogf/gf/v2/util/gconv" ) func main() { // 创建转换对象 converter := gconv.NewConverter() // 整数转换 intValue, err := converter.Int("123") if err != nil { fmt.Println("转换失败:", err) } else { fmt.Println("Int转换结果:", intValue) // 输出: 123 } // 浮点数转换 floatValue, err := converter.Float64("123.456") if err != nil { fmt.Println("转换失败:", err) } else { fmt.Println("Float64转换结果:", floatValue) // 输出: 123.456 } // 布尔值转换 boolValue, err := converter.Bool("true") if err != nil { fmt.Println("转换失败:", err) } else { fmt.Println("Bool转换结果:", boolValue) // 输出: true } // 字符串转换 strValue, err := converter.String(123) if err != nil { fmt.Println("转换失败:", err) } else { fmt.Println("String转换结果:", strValue) // 输出: 123 } } ``` ### 结构体转换 ```go package main import ( "fmt" "github.com/gogf/gf/v2/util/gconv" ) type User struct { Id int Name string Age int } func main() { converter := gconv.NewConverter() // Map转结构体 data := map[string]interface{}{ "id": 1, "name": "John", "age": 30, } var user User err := converter.Struct(data, &user, gconv.StructOption{}) if err != nil { fmt.Println("转换失败:", err) } else { fmt.Printf("结构体转换结果: %+v\n", user) // 输出: {Id:1 Name:John Age:30} } } ``` ### 自定义类型转换 > **注意**:自定义类型转换主要用于复杂类型(如结构体、切片、映射等)之间的转换,不支持基础类型的别名类型。这是为了提高转换效率,避免不必要的性能开销。 以下是一个使用复杂类型的自定义转换示例: ```go package main import ( "fmt" "time" "github.com/gogf/gf/v2/util/gconv" ) // UserInfo 自定义类型 type UserInfo struct { ID int Name string Birthday time.Time } type UserDTO struct { UserID string UserName string Age int } func userInfoT2UserDTO(in UserInfo) (*UserDTO, error) { if in.ID <= 0 { return nil, fmt.Errorf("invalid user ID: %d", in.ID) } // 计算年龄 age := time.Now().Year() - in.Birthday.Year() return &UserDTO{ UserID: fmt.Sprintf("U%d", in.ID), UserName: in.Name, Age: age, }, nil } func main() { converter := gconv.NewConverter() // 注册自定义类型转换函数 - 从UserInfo到UserDTO err := converter.RegisterTypeConverterFunc(userInfoT2UserDTO) if err != nil { fmt.Println("注册转换函数失败:", err) return } // 使用自定义类型转换 userInfo := UserInfo{ ID: 101, Name: "张三", Birthday: time.Date(1990, 5, 15, 0, 0, 0, 0, time.Local), } var userDTO UserDTO err = converter.Scan(userInfo, &userDTO) if err != nil { fmt.Println("转换失败:", err) } else { fmt.Printf("自定义类型转换结果: %#v\n", userDTO) // 输出类似: 自定义类型转换结果: main.UserDTO{UserID:"U101", UserName:"张三", Age:35} } // 测试错误处理 invalidUser := UserInfo{ID: -1, Name: "无效用户"} err = converter.Scan(invalidUser, &userDTO) if err != nil { fmt.Println("预期的错误:", err) // 输出错误信息: invalid user ID: -1 } } ``` 在上面的示例中,我们定义了两个复杂类型:`UserInfo`(源类型)和`UserDTO`(目标类型),并注册了一个自定义转换函数,用于将`UserInfo`转换为`UserDTO`。这种转换不仅仅是简单的字段映射,还包含了业务逻辑(如计算年龄)。 更多关于自定义类型转换的介绍,请参考章节:[类型转换-自定义类型转换](./gconv-register-converter-func.md) ## 与传统`gconv`包方法的区别 ### 错误处理 传统的`gconv`包方法在转换失败时会默认返回零值或空值,而不会返回错误信息: ```go // 传统方法 value := gconv.Int("not-a-number") // 返回0,不返回错误 // Converter方法 converter := gconv.NewConverter() value, err := converter.Int("not-a-number") // 返回0和错误信息 ``` ### 转换选项 `Converter`接口的方法支持更多的转换选项参数,可以更精细地控制转换行为: ```go // 结构体转换选项 structOption := gconv.StructOption{ Mapping: map[string]string{"ID": "UserId"}, RecursiveOption: gconv.RecursiveOptionTrue, ContinueOnError: false, PrivateAttribute: true, } // 使用选项进行转换 converter.Struct(data, &user, structOption) ``` # 类型转换-自定义类型转换 `gconv`转换组件提供了强大的类型转换功能,允许开发者自定义类型之间的转换逻辑,提供了两种主要的自定义转换方法注册机制:`RegisterTypeConverterFunc`和`RegisterAnyConverterFunc`。 **说明** 自定义类型转换类型**不支持**基础类型(如 `int`、`string` 等)的自定义转换,仅支持复杂数据结构类型(如 `slice`、`map`、`struct` 等),这种设计是为了提高转换效率。 ## RegisterTypeConverterFunc ### 函数定义 `RegisterTypeConverterFunc`方法用于注册特定类型之间的转换函数,定义如下: ```go func RegisterTypeConverterFunc(fn interface{}) (err error) ``` 转换函数的签名必须符合以下格式: ```go func(T1) (*T2, error) ``` 其中 `T1` 需要为非指针对象, `T2` 需要为指针类型,如果类型不正确转换方法注册将会报错。 **提示** 输入参数(`T1`)必须为非指针对象的设计,目的是为了保证输入参数的安全,尽可能避免在转换方法内部修改输入参数引起作用域外问题。 ### 使用示例 #### 结构体类型转换 `RegisterTypeConverterFunc`最常见的应用场景是结构体之间的类型转换。我们来看一个使用`RegisterTypeConverterFunc`的例子: ```go package main import ( "fmt" "time" "github.com/gogf/gf/v2/util/gconv" ) // UserInfo 自定义类型 type UserInfo struct { ID int Name string Birthday time.Time } type UserDTO struct { UserID string UserName string Age int } func userInfoT2UserDTO(in UserInfo) (*UserDTO, error) { if in.ID <= 0 { return nil, fmt.Errorf("invalid user ID: %d", in.ID) } // 计算年龄 age := time.Now().Year() - in.Birthday.Year() return &UserDTO{ UserID: fmt.Sprintf("U%d", in.ID), UserName: in.Name, Age: age, }, nil } func main() { converter := gconv.NewConverter() // 注册自定义类型转换函数 - 从UserInfo到UserDTO err := converter.RegisterTypeConverterFunc(userInfoT2UserDTO) if err != nil { fmt.Println("注册转换函数失败:", err) return } // 使用自定义类型转换 userInfo := UserInfo{ ID: 101, Name: "张三", Birthday: time.Date(1990, 5, 15, 0, 0, 0, 0, time.Local), } var userDTO UserDTO err = converter.Scan(userInfo, &userDTO) if err != nil { fmt.Println("转换失败:", err) } else { fmt.Printf("自定义类型转换结果: %#v\n", userDTO) // 输出类似: 自定义类型转换结果: main.UserDTO{UserID:"U101", UserName:"张三", Age:35} } // 测试错误处理 invalidUser := UserInfo{ID: -1, Name: "无效用户"} err = converter.Scan(invalidUser, &userDTO) if err != nil { fmt.Println("预期的错误:", err) // 输出错误信息: invalid user ID: -1 } } ``` 在该示例代码中,演示了两种类型转换场景:自定义结构体转换,以及结构体作为属性的自动转换。转换方法使用的是通用的结构体转换方法 `gconv.Scan`,该方法在内部实现中会自动判断如果存在自定义类型转换函数,会优先使用自定义类型转换函数,否则会走默认的转换逻辑。 #### 别名类型转换 我们也可以使用 `RegisterTypeConverterFunc` 实现 **别名类型** 的转换。下面是一个示例: ```go package main import ( "fmt" "github.com/gogf/gf/v2/os/gtime" "github.com/gogf/gf/v2/util/gconv" ) type MyTime = *gtime.Time type Src struct { A MyTime } type Dst struct { B string } type SrcWrap struct { Value Src } type DstWrap struct { Value Dst } // MyTimeToDstConverter 自定义MyTime到Dst的转换函数 func MyTimeToDstConverter(src MyTime) (dst *Dst, err error) { if src == nil { return &Dst{B: ""}, nil } return &Dst{B: src.String()}, nil } func main() { // 注册自定义转换函数 err := gconv.RegisterTypeConverterFunc(MyTimeToDstConverter) if err != nil { panic(err) } // 创建测试数据 now := gtime.Now() src := Src{A: now} // 执行转换 var dst *Dst err = gconv.Scan(src, &dst) if err != nil { panic(err) } fmt.Println("原始时间:", now) fmt.Println("转换结果:", dst.B) } ``` ## RegisterAnyConverterFunc `RegisterAnyConverterFunc` 提供了更灵活的类型转换机制,允许一个转换函数处理多种类型的转换。这在处理复杂的数据结构转换需求时特别有用。 ### 函数定义 `RegisterAnyConverterFunc`方法提供了更灵活的类型转换机制,允许处理多种类型的转换,定义如下: ```go func RegisterAnyConverterFunc(f AnyConvertFunc, types ...reflect.Type) ``` 其中`AnyConvertFunc`的定义为: ```go // AnyConvertFunc is the function type for converting any to specified type. type AnyConvertFunc func(from any, to reflect.Value) error ``` 这种方式允许开发者注册一个通用的转换函数,可以处理多种类型之间的转换。 ### 使用示例 #### 结构体转换示例 下面是一个使用 `RegisterAnyConverterFunc` 的基本示例,演示如何实现自定义结构体转换: ```go package main import ( "fmt" "reflect" "github.com/gogf/gf/v2/util/gconv" ) // UserInput 源结构体 type UserInput struct { Name string Age int IsActive bool } // UserModel 目标结构体 type UserModel struct { ID int FullName string Age int Status int } func userInput2Model(in any, out reflect.Value) error { // 检查输出类型是否为 UserModel if out.Type() == reflect.TypeOf(&UserModel{}) { // 处理 UserInput 到 UserModel 的转换 if input, ok := in.(UserInput); ok { model := UserModel{ ID: 1, FullName: input.Name, Age: input.Age, Status: 0, } if input.IsActive { model.Status = 1 } out.Elem().Set(reflect.ValueOf(model)) return nil } return fmt.Errorf("不支持从 %T 转换到 UserModel", in) } return fmt.Errorf("不支持转换到 %s", out.Type()) } func main() { // 创建转换器 converter := gconv.NewConverter() // 注册自定义转换函数 converter.RegisterAnyConverterFunc(userInput2Model, reflect.TypeOf(UserModel{})) // 测试转换 var ( model UserModel input = UserInput{Name: "张三", Age: 30, IsActive: true} ) err := converter.Scan(input, &model) if err != nil { fmt.Println("转换失败:", err) } else { fmt.Printf( "转换结果: ID=%d, 名称=%s, 年龄=%d, 状态=%d\n", model.ID, model.FullName, model.Age, model.Status, ) } } ``` #### 复杂结构体嵌套转换示例 下面是一个带有嵌套数据结构的较复杂示例,演示如何使用 `RegisterAnyConverterFunc` 处理复杂的嵌套结构体转换: ```go package main import ( "fmt" "reflect" "github.com/gogf/gf/v2/util/gconv" ) // UserDetail 源数据结构 type UserDetail struct { UserID int Name string Address Address } // UserDTO 目标数据结构 type UserDTO struct { ID string DisplayName string Location string } type Address struct { City string Country string } func userDetail2DTO(in any, out reflect.Value) error { // 检查输出类型是否为 UserDTO if out.Type() == reflect.TypeOf(&UserDTO{}) { // 处理 UserDetail 到 UserDTO 的转换 if detail, ok := in.(UserDetail); ok { dto := UserDTO{ ID: fmt.Sprintf("USER-%d", detail.UserID), DisplayName: detail.Name, Location: fmt.Sprintf("%s, %s", detail.Address.City, detail.Address.Country), } out.Elem().Set(reflect.ValueOf(dto)) return nil } return fmt.Errorf("不支持从 %T 转换到 UserDTO", in) } return fmt.Errorf("不支持转换到 %s", out.Type()) } func main() { // 创建转换器 converter := gconv.NewConverter() // 注册自定义转换函数 converter.RegisterAnyConverterFunc(userDetail2DTO, reflect.TypeOf(UserDTO{})) // 测试转换 var ( dto UserDTO detail = UserDetail{ UserID: 12345, Name: "张三", Address: Address{ City: "北京", Country: "中国", }, } ) if err := converter.Scan(detail, &dto); err != nil { fmt.Println("转换失败:", err) } else { fmt.Printf( "转换结果: ID=%s, 名称=%s, 地址=%s\n", dto.ID, dto.DisplayName, dto.Location, ) } } ``` 这个示例演示了如何使用 `RegisterAnyConverterFunc` 实现复杂的嵌套结构体转换,包括从 `UserDetail` 到 `UserDTO` 的转换,以及从嵌套 `map` 到 `UserDTO` 的转换。这种复杂的数据结构转换是 `RegisterAnyConverterFunc` 的主要应用场景。 ## 两种转换函数的比较 两种注册方式各有其优缺点和适用场景: ### RegisterTypeConverterFunc **优点:** 1. **类型安全**:由于函数签名已经定义了输入和输出类型,编译器可以在编译时进行类型检查。 2. **使用简单**:定义和使用相对直观,不需要手动处理反射。 3. **适合特定类型转换**:当只需要处理两个特定类型之间的转换时,这种方式更加清晰。 **缺点:** 1. **灵活性有限**:每个函数只能处理一种特定类型到另一种特定类型的转换。 2. **需要多个函数**:如果需要处理多种类型的转换,则需要定义和注册多个函数。 ### RegisterAnyConverterFunc **优点:** 1. **高度灵活**:一个函数可以处理多种输入类型到特定输出类型的转换。 2. **代码重用**:可以在一个函数中集中处理相关的转换逻辑,减少重复代码。 3. **动态判断**:可以在运行时根据输入类型动态决定如何转换。 **缺点:** 1. **类型安全性降低**:使用反射和接口类型,编译器无法检查类型错误。 2. **使用复杂**:需要手动处理反射和类型断言,增加了复杂性。 3. **错误处理更复杂**:需要手动处理各种类型错误和边界情况,易出错。 ### 选择建议 1. 当只需要处理两个特定类型之间的转换时,使用 `RegisterTypeConverterFunc`。 2. 当需要处理多种输入类型到同一输出类型的转换,或者需要基于输入值的内容动态决定转换逻辑时,使用 `RegisterAnyConverterFunc`。 3. 在大型项目中,可以结合使用这两种方式,简单的类型转换使用 `RegisterTypeConverterFunc`,复杂的多类型转换使用 `RegisterAnyConverterFunc`。 ## 最佳实践 1. **在启动时注册**:将自定义转换函数的注册放在应用程序的启动过程中,避免在运行时重复注册。 2. **错误处理**:始终返回有意义的错误信息,便于调试和问题跟踪。 3. **性能考虑**:对于频繁调用的转换函数,尽量优化性能,避免不必要的反射操作。 4. **保持兼容性**:在更新自定义转换函数时,确保其与现有代码的兼容性。 5. **测试覆盖**:为自定义转换函数编写全面的单元测试,确保其在各种情况下都能正常工作。 # 类型转换-UnmarshalValue 当然,想必已经猜到了,在对一些复杂类型(如 `struct`)的转换时, `gconv` 模块内部其实使用了反射的特性来实现的。这虽然为开发者提供了极大的便捷,但是这确实是以性能损耗为代价的。其实在对于 `struct` 转换时,如果开发者已经 **明确转换规则**,并且对于其中的性能损耗比较在意,那么可以对特定的 `struct` 实现 `UnmarshalValue` 接口来实现 **自定义转换**。当使用 `gconv` 模块对该 `struct` 进行转换时,无论该 `struct` 是直接作为转换对象或者作为转换对象的属性, `gconv` 都将会自动识别其实现的 `UnmarshalValue` 接口并直接调用该接口实现类型转换,而不会使用反射特性来实现转换。 **提示** 标准库的常用反序列化接口,如 `UnmarshalText(text []byte) error` 其实也是支持的哟,使用方式同 `UnmarshalValue`,只是参数不同。 ## 接口定义 ```go // apiUnmarshalValue is the interface for custom defined types customizing value assignment. // Note that only pointer can implement interface apiUnmarshalValue. type apiUnmarshalValue interface { UnmarshalValue(interface{}) error } ``` 可以看到,自定义的类型可以通过定义 `UnmarshalValue` 方法来实现自定义的类型转换。这里的输入参数为 `interface{}` 类型,开发者可以在实际使用场景中通过 类型断言 或者其他方式进行类型转换。 **注意** 需要特别注意,由于 `UnmarshalValue` 类型转换会修改当前对象的属性值,因此需要保证该接口实现的接受者( `Receiver`)是指针类型。 正确的接口实现定义示例(使用指针接受): ```go func (c *Receiver) UnmarshalValue(interface{}) error ``` **错误的** 接口实现定义示例(使用了值传递): ```go func (c Receiver) UnmarshalValue(interface{}) error ``` ## 使用示例 ### 1、自定义数据表查询结果 `struct` 转换 数据表结构: ```sql CREATE TABLE `user` ( id bigint unsigned NOT NULL AUTO_INCREMENT, passport varchar(45), password char(32) NOT NULL, nickname varchar(45) NOT NULL, create_time timestamp NOT NULL, PRIMARY KEY (id) ) ; ``` 示例代码: ```go package main import ( "fmt" "github.com/gogf/gf/v2/container/garray" "github.com/gogf/gf/v2/database/gdb" "github.com/gogf/gf/v2/errors/gerror" "github.com/gogf/gf/v2/frame/g" "github.com/gogf/gf/v2/os/gtime" "reflect" ) type User struct { Id int Passport string Password string Nickname string CreateTime *gtime.Time } // 实现UnmarshalValue接口,用于自定义结构体转换 func (user *User) UnmarshalValue(value interface{}) error { if record, ok := value.(gdb.Record); ok { *user = User{ Id: record["id"].Int(), Passport: record["passport"].String(), Password: "", Nickname: record["nickname"].String(), CreateTime: record["create_time"].GTime(), } return nil } return gerror.Newf(`unsupported value type for UnmarshalValue: %v`, reflect.TypeOf(value)) } func main() { var ( err error users []*User ) array := garray.New(true) for i := 1; i <= 10; i++ { array.Append(g.Map{ "id": i, "passport": fmt.Sprintf(`user_%d`, i), "password": fmt.Sprintf(`pass_%d`, i), "nickname": fmt.Sprintf(`name_%d`, i), "create_time": gtime.NewFromStr("2018-10-24 10:00:00").String(), }) } // 写入数据 _, err = g.Model("user").Data(array).Insert() if err != nil { panic(err) } // 查询数据 err = g.Model("user").Order("id asc").Scan(&users) if err != nil { panic(err) } g.Dump(users) } ``` 执行后,终端输出: ``` [ { Id: 1, Passport: "user_1", Password: "", Nickname: "name_1", CreateTime: "2018-10-24 10:00:00", }, { Id: 2, Passport: "user_2", Password: "", Nickname: "name_2", CreateTime: "2018-10-24 10:00:00", }, { Id: 3, Passport: "user_3", Password: "", Nickname: "name_3", CreateTime: "2018-10-24 10:00:00", }, { Id: 4, Passport: "user_4", Password: "", Nickname: "name_4", CreateTime: "2018-10-24 10:00:00", }, { Id: 5, Passport: "user_5", Password: "", Nickname: "name_5", CreateTime: "2018-10-24 10:00:00", }, { Id: 6, Passport: "user_6", Password: "", Nickname: "name_6", CreateTime: "2018-10-24 10:00:00", }, { Id: 7, Passport: "user_7", Password: "", Nickname: "name_7", CreateTime: "2018-10-24 10:00:00", }, { Id: 8, Passport: "user_8", Password: "", Nickname: "name_8", CreateTime: "2018-10-24 10:00:00", }, { Id: 9, Passport: "user_9", Password: "", Nickname: "name_9", CreateTime: "2018-10-24 10:00:00", }, { Id: 10, Passport: "user_10", Password: "", Nickname: "name_10", CreateTime: "2018-10-24 10:00:00", }, ] ``` **提示** 可以看到自定义的 `UnmarshalValue` 类型转换方法中没有使用到反射特性,因此转换的性能会得到极大的提升。小伙伴们可以尝试着增加写入的数据量(例如 `100W`),同时对比一下去掉 `UnmarshalValue` 后的类型转换所开销的时间。 ### 2、自定义二进制TCP数据解包 一个TCP通信的数据包解包示例。 ```go package main import ( "errors" "fmt" "github.com/gogf/gf/v2/crypto/gcrc32" "github.com/gogf/gf/v2/encoding/gbinary" "github.com/gogf/gf/v2/util/gconv" ) type Pkg struct { Length uint16 // Total length. Crc32 uint32 // CRC32. Data []byte } // NewPkg creates and returns a package with given data. func NewPkg(data []byte) *Pkg { return &Pkg{ Length: uint16(len(data) + 6), Crc32: gcrc32.Encrypt(data), Data: data, } } // Marshal encodes the protocol struct to bytes. func (p *Pkg) Marshal() []byte { b := make([]byte, 6+len(p.Data)) copy(b, gbinary.EncodeUint16(p.Length)) copy(b[2:], gbinary.EncodeUint32(p.Crc32)) copy(b[6:], p.Data) return b } // UnmarshalValue decodes bytes to protocol struct. func (p *Pkg) UnmarshalValue(v interface{}) error { b := gconv.Bytes(v) if len(b) < 6 { return errors.New("invalid package length") } p.Length = gbinary.DecodeToUint16(b[:2]) if len(b) < int(p.Length) { return errors.New("invalid data length") } p.Crc32 = gbinary.DecodeToUint32(b[2:6]) p.Data = b[6:] if gcrc32.Encrypt(p.Data) != p.Crc32 { return errors.New("crc32 validation failed") } return nil } func main() { var p1, p2 *Pkg // Create a demo pkg as p1. p1 = NewPkg([]byte("123")) fmt.Println(p1) // Convert bytes from p1 to p2 using gconv.Struct. err := gconv.Struct(p1.Marshal(), &p2) if err != nil { panic(err) } fmt.Println(p2) } ``` 执行后,终端输出: ``` &{9 2286445522 [49 50 51]} &{9 2286445522 [49 50 51]} ``` # 类型转换-性能测试 基本类型转换性能基准测试: ``` john@john-B85M:~/Workspace/Go/GOPATH/src/github.com/gogf/gf/v2/util/gconv$ go test *.go -bench=".*" -benchmem goos: linux goarch: amd64 BenchmarkString-4 20000000 71.8 ns/op 24 B/op 2 allocs/op BenchmarkInt-4 100000000 22.2 ns/op 8 B/op 1 allocs/op BenchmarkInt8-4 100000000 24.5 ns/op 8 B/op 1 allocs/op BenchmarkInt16-4 50000000 23.8 ns/op 8 B/op 1 allocs/op BenchmarkInt32-4 100000000 24.1 ns/op 8 B/op 1 allocs/op BenchmarkInt64-4 100000000 21.7 ns/op 8 B/op 1 allocs/op BenchmarkUint-4 100000000 22.2 ns/op 8 B/op 1 allocs/op BenchmarkUint8-4 50000000 25.6 ns/op 8 B/op 1 allocs/op BenchmarkUint16-4 50000000 32.1 ns/op 8 B/op 1 allocs/op BenchmarkUint32-4 50000000 27.7 ns/op 8 B/op 1 allocs/op BenchmarkUint64-4 50000000 28.1 ns/op 8 B/op 1 allocs/op BenchmarkFloat32-4 10000000 155 ns/op 24 B/op 2 allocs/op BenchmarkFloat64-4 10000000 177 ns/op 24 B/op 2 allocs/op BenchmarkTime-4 5000000 240 ns/op 72 B/op 4 allocs/op BenchmarkTimeDuration-4 50000000 26.2 ns/op 8 B/op 1 allocs/op BenchmarkBytes-4 10000000 149 ns/op 128 B/op 3 allocs/op BenchmarkStrings-4 10000000 223 ns/op 40 B/op 3 allocs/op BenchmarkInts-4 20000000 55.0 ns/op 16 B/op 2 allocs/op BenchmarkFloats-4 10000000 186 ns/op 32 B/op 3 allocs/op BenchmarkInterfaces-4 20000000 66.6 ns/op 24 B/op 2 allocs/op PASS ok command-line-arguments 35.356s ``` # 缓存管理 ## 基本介绍 `gcache` 是提供统一的缓存管理模块,提供了开发者可自定义灵活接入的缓存适配接口,并默认提供了高速内存缓存适配实现。 **使用方式**: ```go import "github.com/gogf/gf/v2/os/gcache" ``` **接口文档**: [https://pkg.go.dev/github.com/gogf/gf/v2/os/gcache](https://pkg.go.dev/github.com/gogf/gf/v2/os/gcache) **简要介绍:** 1. `gcache` 默认提供默认的高速内存缓存对象,可以通过包方法操作内存缓存,也可以通过 `New` 方法创建内存缓存对象。在通过包方法使用缓存功能时,操作的是 `gcache` 默认提供的一个 `gcache.Cache` 对象,具有全局性,因此在使用时注意全局键名的覆盖。 2. `gcache` 使用的键名类型是 `interface{}`,而不是 `string` 类型,这意味着我们可以使用任意类型的变量作为键名,但大多数时候建议使用 `string` 或者 `[]byte` 作为键名,并且统一键名的数据类型,以便维护。 3. `gcache` 存储的键值类型是 `interface{}`,也就是说可以存储任意的数据类型,当获取数据时返回的也是 `interface{}` 类型,若需要转换为其他的类型可以通过 `gcache` 的 `Get*` 方法便捷获取常见类型。注意,如果确定知道自己使用的是内存缓存,那么可以直接使用断言方式对返回的 `interface{}` 变量进行类型转换,否则建议通过获取到的泛型对象对应方法完成类型转换。 4. 另外需要注意的是, `gcache` 的缓存过期时间参数 `duration` 的类型为 `time.Duration` 类型,在 `Set` 缓存变量时,如果缓存时间参数 `duration = 0` 表示不过期, `duration < 0` 表示立即过期, `duration > 0` 表示超时过期。 ## 注意事项 ### 关于键名数据类型 大家可以发现缓存组件中关于键值对的数据类型都是 `interface{}`,这种设计主要是为了考虑通用性和易用性,但是使用上需要注意 `interface{}` 的比较:只有 **数据** 和 **类型** 都相等才算真正匹配。我们来看个例子。 ```go package main import ( "fmt" "github.com/gogf/gf/v2/os/gcache" "github.com/gogf/gf/v2/os/gctx" ) func main() { var ( ctx = gctx.New() key1 int32 = 1 key2 float64 = 1 value = `value` ) _ = gcache.Set(ctx, key1, value, 0) fmt.Println(gcache.MustGet(ctx, key1).Val()) fmt.Println(gcache.MustGet(ctx, key2).Val()) } ``` 执行后,终端输出: ``` value ``` 可以看到,虽然 `key1` 和 `key2` 的数值都是相同的,但是两者类型不同,因此通过 `key2` 无法获取到键值。 ### 关于获取对象键值 由于键值的类型也是 `interface{}`,往往在获取后会通过类型转换为需要的数据类型。常见的类型转换是直接使用类型断言,这种做法有种风险,就是由于 `gcache` 组件采用 **适配器接口设计模式**,因此底层的实现上(除了默认的内存适配器)往往会改变原有的数据类型(非内存实现往往都会涉及到序列化/反序列化存储),因此不推荐大家直接使用类型断言进行数据类型转换。 因此缓存组件在获取键值上做了改进,并不是直接返回 `interface{}` 的键值类型,而是以框架泛型 `*gvar.Var` 对象返回,开发者根据业务场景转换为所需的数据类型。特别是针对于对象缓存存储和读取的场景特别有用,我们来看一个示例: ```go package main import ( "fmt" "github.com/gogf/gf/v2/os/gcache" "github.com/gogf/gf/v2/os/gctx" ) func main() { type User struct { Id int Name string Site string } var ( ctx = gctx.New() user *User key = `UserKey` value = &User{ Id: 1, Name: "GoFrame", Site: "https://goframe.org", } ) err := gcache.Set(ctx, key, value, 0) if err != nil { panic(err) } v, err := gcache.Get(ctx, key) if err != nil { panic(err) } if err = v.Scan(&user); err != nil { panic(err) } fmt.Printf(`%#v`, user) } ``` 执行后,终端输出: ```bash &main.User{Id:1, Name:"GoFrame", Site:"https://goframe.org"} ``` # 缓存管理-接口设计 缓存组件采用了接口化设计,提供了`Adapter`接口,任何实现了`Adapter`接口的对象均可注册到缓存管理对象中,使得开发者可以对缓存管理对象进行灵活的自定义实现和扩展。 ## 接口定义 `Adapter` 接口定义如下: [https://pkg.go.dev/github.com/gogf/gf/v2/os/gcache#Adapter](https://pkg.go.dev/github.com/gogf/gf/v2/os/gcache#Adapter) ## 注册接口实现 通过该方法将实现的`adapter`应用到对应的`Cache`对象上: ```go // SetAdapter changes the adapter for this cache. // Be very note that, this setting function is not concurrent-safe, which means you should not call // this setting function concurrently in multiple goroutines. func (c *Cache) SetAdapter(adapter Adapter) ``` 具体示例请参考 [缓存管理-Redis缓存](./gcache-redis.md) 章节。 ## 获取接口实现 通过该方法获取当前注册的`adapter`接口实现对象上: ```go // GetAdapter returns the adapter that is set in current Cache. func (c *Cache) GetAdapter() Adapter ``` 具体示例请参考 [缓存管理-Redis缓存](./gcache-redis.md) 章节。 # 缓存管理-内存缓存 缓存组件默认提供了一个高速的内存缓存,操作效率非常高效,`CPU`性能损耗在`ns`纳秒级别。 ## 使用示例 ### 基本使用 ```go package main import ( "fmt" "github.com/gogf/gf/v2/os/gcache" "github.com/gogf/gf/v2/os/gctx" ) func main() { // 创建一个缓存对象, // 当然也可以便捷地直接使用gcache包方法 var ( ctx = gctx.New() cache = gcache.New() ) // 设置缓存,不过期 err := cache.Set(ctx, "k1", "v1", 0) if err != nil { panic(err) } // 获取缓存值 value, err := cache.Get(ctx, "k1") if err != nil { panic(err) } fmt.Println(value) // 获取缓存大小 size, err := cache.Size(ctx) if err != nil { panic(err) } fmt.Println(size) // 缓存中是否存在指定键名 b, err := cache.Contains(ctx, "k1") if err != nil { panic(err) } fmt.Println(b) // 删除并返回被删除的键值 removedValue, err := cache.Remove(ctx, "k1") if err != nil { panic(err) } fmt.Println(removedValue) // 关闭缓存对象,让GC回收资源 if err = cache.Close(ctx); err != nil { panic(err) } } ``` 执行后,输出结果为: ```v1 1 true v1 ``` ### 过期控制 ```go package main import ( "fmt" "github.com/gogf/gf/v2/os/gcache" "github.com/gogf/gf/v2/os/gctx" "time" ) func main() { var ( ctx = gctx.New() ) // 当键名不存在时写入,设置过期时间1000毫秒 _, err := gcache.SetIfNotExist(ctx, "k1", "v1", time.Second) if err != nil { panic(err) } // 打印当前的键名列表 keys, err := gcache.Keys(ctx) if err != nil { panic(err) } fmt.Println(keys) // 打印当前的键值列表 values, err := gcache.Values(ctx) if err != nil { panic(err) } fmt.Println(values) // 获取指定键值,如果不存在时写入,并返回键值 value, err := gcache.GetOrSet(ctx, "k2", "v2", 0) if err != nil { panic(err) } fmt.Println(value) // 打印当前的键值对 data1, err := gcache.Data(ctx) if err != nil { panic(err) } fmt.Println(data1) // 等待1秒,以便k1:v1自动过期 time.Sleep(time.Second) // 再次打印当前的键值对,发现k1:v1已经过期,只剩下k2:v2 data2, err := gcache.Data(ctx) if err != nil { panic(err) } fmt.Println(data2) } ``` 执行后,输出结果为: ``` [k1] [v1] v2 map[k1:v1 k2:v2] map[k2:v2] ``` ### `GetOrSetFunc*` `GetOrSetFunc` 获取一个缓存值,当缓存不存在时执行指定的 `f func(context.Context) (interface{}, error)`,缓存该 `f` 方法的结果值,并返回该结果。 需要注意的是, `GetOrSetFunc` 的缓存方法参数 `f` 是在缓存的 **锁机制外执行**,因此在 `f` 内部也可以嵌套调用 `GetOrSetFunc`。但如果 `f` 的执行比较耗时,高并发的时候容易出现 `f` 被多次执行的情况(缓存设置只有第一个执行的 `f` 返回结果能够设置成功,其余的被抛弃掉)。而 `GetOrSetFuncLock` 的缓存方法 `f` 是在缓存的 **锁机制内执行**,因此可以保证当缓存项不存在时只会执行一次 `f`,但是缓存写锁的时间随着 `f` 方法的执行时间而定。 我们来看一个示例: ```go package main import ( "fmt" "github.com/gogf/gf/v2/os/gcache" "github.com/gogf/gf/v2/os/gctx" "time" ) func main() { var ( ch = make(chan struct{}, 0) ctx = gctx.New() key = `key` value = `value` ) for i := 0; i < 10; i++ { go func(index int) { <-ch _, err := gcache.GetOrSetFuncLock(ctx, key, func(ctx context.Context) (interface{}, error) { fmt.Println(index, "entered") return value, nil }, 0) if err != nil { panic(err) } }(i) } close(ch) time.Sleep(time.Second) } ``` 执行后,终端输出(带有随机性,但是只会输出一条信息): ``` 9 entered ``` 可以看到,多个 `goroutine` 同时调用 `GetOrSetFuncLock` 方法时,由于该方法有并发安全控制,因此最终只有一个 `goroutine` 的数值生成函数执行成功,成功之后其他 `goroutine` 拿到数据后则立即返回不再执行对应的数值生成函数。 ### `LRU` 缓存淘汰控制 ```go package main import ( "fmt" "github.com/gogf/gf/v2/os/gcache" "github.com/gogf/gf/v2/os/gctx" "time" ) func main() { var ( ctx = gctx.New() cache = gcache.New(2) // 设置LRU淘汰数量 ) // 添加10个元素,不过期 for i := 0; i < 10; i++ { if err := cache.Set(ctx, i, i, 0); err != nil { panic(err) } } size, err := cache.Size(ctx) if err != nil { panic(err) } fmt.Println(size) keys, err := cache.Keys(ctx) if err != nil { panic(err) } fmt.Println(keys) // 读取键名1,保证该键名是优先保留 value, err := cache.Get(ctx, 1) if err != nil { panic(err) } fmt.Println(value) // 等待一定时间后(默认1秒检查一次), // 元素会被按照从旧到新的顺序进行淘汰 time.Sleep(3 * time.Second) size, err = cache.Size(ctx) if err != nil { panic(err) } fmt.Println(size) keys, err = cache.Keys(ctx) if err != nil { panic(err) } fmt.Println(keys) } ``` 执行后,输出结果为: ```10 [2 3 5 6 7 0 1 4 8 9] 1 2 [1 9] ``` ## 性能测试 ### 测试环境 ``` CPU: Intel(R) Core(TM) i5-4460 CPU @ 3.20GHz MEM: 8GB SYS: Ubuntu 16.04 amd64 ``` ### 测试结果 ``` john@john-B85M:~/Workspace/Go/GOPATH/src/github.com/gogf/gf/v2/os/gcache$ go test *.go -bench=".*" -benchmem goos: linux goarch: amd64 Benchmark_CacheSet-4 2000000 897 ns/op 249 B/op 4 allocs/op Benchmark_CacheGet-4 5000000 202 ns/op 49 B/op 1 allocs/op Benchmark_CacheRemove-4 50000000 35.7 ns/op 0 B/op 0 allocs/op Benchmark_CacheLruSet-4 2000000 880 ns/op 399 B/op 4 allocs/op Benchmark_CacheLruGet-4 3000000 212 ns/op 33 B/op 1 allocs/op Benchmark_CacheLruRemove-4 50000000 35.9 ns/op 0 B/op 0 allocs/op Benchmark_InterfaceMapWithLockSet-4 3000000 477 ns/op 73 B/op 2 allocs/op Benchmark_InterfaceMapWithLockGet-4 10000000 149 ns/op 0 B/op 0 allocs/op Benchmark_InterfaceMapWithLockRemove-4 50000000 39.8 ns/op 0 B/op 0 allocs/op Benchmark_IntMapWithLockWithLockSet-4 5000000 304 ns/op 53 B/op 0 allocs/op Benchmark_IntMapWithLockGet-4 20000000 164 ns/op 0 B/op 0 allocs/op Benchmark_IntMapWithLockRemove-4 50000000 33.1 ns/op 0 B/op 0 allocs/op PASS ok command-line-arguments 47.503s ``` # 缓存管理-Redis缓存 ## 基本介绍 缓存组件同时提供了 `gcache` 的 `Redis` 缓存适配实现。 `Redis` 缓存在多节点保证缓存的数据一致性时非常有用,特别是 `Session` 共享、数据库查询缓存等场景中。 ## 使用示例 ```go func ExampleCache_SetAdapter() { var ( err error ctx = gctx.New() cache = gcache.New() redisConfig = &gredis.Config{ Address: "127.0.0.1:6379", Db: 9, } cacheKey = `key` cacheValue = `value` ) // Create redis client object. redis, err := gredis.New(redisConfig) if err != nil { panic(err) } // Create redis cache adapter and set it to cache object. cache.SetAdapter(gcache.NewAdapterRedis(redis)) // Set and Get using cache object. err = cache.Set(ctx, cacheKey, cacheValue, time.Second) if err != nil { panic(err) } fmt.Println(cache.MustGet(ctx, cacheKey).String()) // Get using redis client. fmt.Println(redis.MustDo(ctx, "GET", cacheKey).String()) // Output: // value // value } ``` ## 注意事项 ### 关于 `Clear/Size` 等方法 首先,相同的 `gredis.Config` 总是会连接到相同的 `redis db` 中,而由于 `Redis` 本身没有数据分组功能,所以当多个 `gcache.Cache` 对象连接到同个 `redis db` 时,将会共享整个 `redis db`,而不会单独开辟一个分组用来存储当前 `gcache.Cache` 对象中的内容。因此当使用 `Clear`、 `Size` 这类操作时,将会对整个 `redis db` 进行操作,而不是与内存缓存一样的仅操作当前 `gcache.Cache` 对象中的内容,存在一定的反直觉,请务必谨慎使用。 ### 建议使用不同的 `redis db` 区分业务缓存类型 另外,建议大家在使用 `Redis` 缓存时,单独配置不同的 `db` 来使用,而不是和其他的业务数据共用一个 `db`。 # 缓存管理-方法介绍 **提示** 以下常用方法列表,文档更新可能滞后于代码新特性,更多的方法及示例请参考代码文档: [https://pkg.go.dev/github.com/gogf/gf/v2/os/gcache](https://pkg.go.dev/github.com/gogf/gf/v2/os/gcache) ## `Set` - 说明:使用`key-value`键值对设置缓存,键值可以是任意类型。 - 格式: ```go Set(ctx context.Context, key interface{}, value interface{}, duration time.Duration) error ``` - 示例:将`slice`切片设置到键名`k1`的缓存中。 ```go func ExampleCache_Set() { c := gcache.New() c.Set(ctx, "k1", g.Slice{1, 2, 3, 4, 5, 6, 7, 8, 9}, 0) fmt.Println(c.Get(ctx, "k1")) // Output: // [1,2,3,4,5,6,7,8,9] } ``` ## `SetAdapter` - 说明: `SetAdapter` 更改此缓存对象的底层适配器。请注意,此设置函数不是并发安全的。 - 格式: ```go SetAdapter(adapter Adapter) ``` - 示例:可以自己根据需要实现任意缓存适配器,实现接口方法即可。 ```go func ExampleCache_SetAdapters() { c := gcache.New() adapter := gcache.New() c.SetAdapter(adapter) c.Set(ctx, "k1", g.Slice{1, 2, 3, 4, 5, 6, 7, 8, 9}, 0) fmt.Println(c.Get(ctx, "k1")) // Output: // [1,2,3,4,5,6,7,8,9] } ``` ## `SetIfNotExist` - 说明: 当指定 `key` 的键值不存在时设置其对应的键值 `value` 并返回 `true`,否则什么都不做并返回 `false`。 - 格式: ```go SetIfNotExist(ctx context.Context, key interface{}, value interface{}, duration time.Duration) (ok bool, err error) ``` - 示例:通过 `SetIfNotExist` 直接 `判断写入`,并设置过期时间。 ```go func ExampleCache_SetIfNotExist() { c := gcache.New() // Write when the key name does not exist, and set the expiration time to 1000 milliseconds k1, err := c.SetIfNotExist(ctx, "k1", "v1", 1000*time.Millisecond) fmt.Println(k1, err) // Returns false when the key name already exists k2, err := c.SetIfNotExist(ctx, "k1", "v2", 1000*time.Millisecond) fmt.Println(k2, err) // Print the current list of key values keys1, _ := c.Keys(ctx) fmt.Println(keys1) // It does not expire if `duration` == 0. It deletes the `key` if `duration` < 0 or given `value` is nil. c.SetIfNotExist(ctx, "k1", 0, -10000) // Wait 1 second for K1: V1 to expire automatically time.Sleep(1200 * time.Millisecond) // Print the current key value pair again and find that K1: V1 has expired keys2, _ := c.Keys(ctx) fmt.Println(keys2) // Output: // true // false // [k1] // [] } ``` ## `SetMap` - 说明: 批量设置键值对,输入参数类型为 `map[interface{}]interface{}`。 - 格式: ```go SetMap(ctx context.Context, data map[interface{}]interface{}, duration time.Duration) error ``` - 示例: ```go func ExampleCache_SetMap() { c := gcache.New() // map[interface{}]interface{} data := g.MapAnyAny{ "k1": "v1", "k2": "v2", "k3": "v3", } c.SetMap(ctx, data, 1000*time.Millisecond) // Gets the specified key value v1, _ := c.Get(ctx, "k1") v2, _ := c.Get(ctx, "k2") v3, _ := c.Get(ctx, "k3") fmt.Println(v1, v2, v3) // Output: // v1 v2 v3 } ``` ## `Size` - 说明: `Size` 返回缓存中的 `项数`。 - 格式: ```go Size(ctx context.Context) (size int, err error) ``` - 示例: ```go func ExampleCache_Size() { c := gcache.New() // Add 10 elements without expiration for i := 0; i < 10; i++ { c.Set(ctx, i, i, 0) } // Size returns the number of items in the cache. n, _ := c.Size(ctx) fmt.Println(n) // Output: // 10 } ``` ## `Update` - 说明: `Update` 更新 `key` 的对应的键值,但不更改其 `过期时间`,并返回旧值。如果缓存中不存在 `key`,则返回的 `exist` 值为 `false`。 - 格式: ```go Update(ctx context.Context, key interface{}, value interface{}) (oldValue *gvar.Var, exist bool, err error) ``` - 示例:通过 `SetMap` 添加多个缓存,通过 `Update` 指定 `key` 修改 `value`。 ```go func ExampleCache_Update() { c := gcache.New() c.SetMap(ctx, g.MapAnyAny{"k1": "v1", "k2": "v2", "k3": "v3"}, 0) k1, _ := c.Get(ctx, "k1") fmt.Println(k1) k2, _ := c.Get(ctx, "k2") fmt.Println(k2) k3, _ := c.Get(ctx, "k3") fmt.Println(k3) re, exist, _ := c.Update(ctx, "k1", "v11") fmt.Println(re, exist) re1, exist1, _ := c.Update(ctx, "k4", "v44") fmt.Println(re1, exist1) kup1, _ := c.Get(ctx, "k1") fmt.Println(kup1) kup2, _ := c.Get(ctx, "k2") fmt.Println(kup2) kup3, _ := c.Get(ctx, "k3") fmt.Println(kup3) // Output: // v1 // v2 // v3 // v1 true // false // v11 // v2 // v3 } ``` ## `UpdateExpire` - 说明: `UpdateExpire` 更新 `key` 的过期时间并返回旧的 `过期时间值`。如果缓存中不存在 `key`,则返回 `-1`。 - 格式: ```go UpdateExpire(ctx context.Context, key interface{}, duration time.Duration) (oldDuration time.Duration, err error) ``` - 示例:通过 `UpdateExpire` 更新 `key` 的过期时间并打印查看。 ```go func ExampleCache_UpdateExpire() { c := gcache.New() c.Set(ctx, "k1", "v1", 1000*time.Millisecond) expire, _ := c.GetExpire(ctx, "k1") fmt.Println(expire) c.UpdateExpire(ctx, "k1", 500*time.Millisecond) expire1, _ := c.GetExpire(ctx, "k1") fmt.Println(expire1) // Output: // 1s // 500ms } ``` ## `Values` - 说明: 通过 `Values` 获取缓存中的所有值,以切片方式返回。 - 格式: ```go Values(ctx context.Context) (values []interface{}, err error) ``` - 示例: ```go func ExampleCache_Values() { c := gcache.New() c.Set(ctx, "k1", g.Map{"k1": "v1", "k2": "v2"}, 0) // Values returns all values in the cache as slice. data, _ := c.Values(ctx) fmt.Println(data) // May Output: // [map[k1:v1 k2:v2]] } ``` ## `Close` - 说明: 关闭缓存,让 `GC` 回收资源,默认情况下 `可不关闭`。 - 格式: ```go Close(ctx context.Context) error ``` - 示例:通过 `Close` 即可关闭缓存。 ```go func ExampleCache_Close() { c := gcache.New() c.Set(ctx, "k1", "v", 0) data, _ := c.Get(ctx, "k1") fmt.Println(data) // Close closes the cache if necessary. c.Close(ctx) data1, _ := c.Get(ctx, "k1") fmt.Println(data1) // Output: // v // v } ``` ## `Contains` - 说明: 如果缓存中存在指定的 `key`,则 `Contains` 返回 `true`,否则返回 `false`。 - 格式: ```go Contains(ctx context.Context, key interface{}) (bool, error) ``` - 示例: ```go func ExampleCache_Contains() { c := gcache.New() // Set Cache c.Set(ctx, "k", "v", 0) data, _ := c.Contains(ctx, "k") fmt.Println(data) // return false data1, _ := c.Contains(ctx, "k1") fmt.Println(data1) // Output: // true // false } ``` ## `Data` - 说明: 数据以 `map` 类型返回缓存中所有 `键值对('key':'value')` 的拷贝。 - 格式: ```go Data(ctx context.Context) (data map[interface{}]interface{}, err error) ``` - 示例:获取所有缓存数据以 `map[interface{}]interface{}` 返回 ```go func ExampleCache_Data() { c := gcache.New() c.SetMap(ctx, g.MapAnyAny{"k1": "v1"}, 0) //c.Set(ctx, "k5", "v5", 0) data, _ := c.Data(ctx) fmt.Println(data) // Output: // map[k1:v1] } ``` ## `Get` - 说明: `Get` 检索并返回给定 `key` 的关联值。如果它不存在、值为零或已过期,则返回 `nil`。 - 格式: ```go Get(ctx context.Context, key interface{}) (*gvar.Var, error) ``` - 示例: ```go func ExampleCache_Get() { c := gcache.New() // Set Cache Object c.Set(ctx, "k1", "v1", 0) data, _ := c.Get(ctx, "k1") fmt.Println(data) // Output: // v1 } ``` ## `GetExpire` - 说明: `GetExpire` 检索并返回缓存中 `key` 的过期时间。注意,如果 `key` 为永不过期,则返回 `0`。如果缓存中不存在 `key`,则返回 `-1`。 - 格式: ```go GetExpire(ctx context.Context, key interface{}) (time.Duration, error) ``` - 示例: ```go func ExampleCache_GetExpire() { c := gcache.New() // Set cache without expiration c.Set(ctx, "k", "v", 10000*time.Millisecond) expire, _ := c.GetExpire(ctx, "k") fmt.Println(expire) // Output: // 10s } ``` ## `GetOrSet` - 说明: 检索并返回 `key` 的值,或者设置 `key-value` 对,如果缓存中不存在 `key`,则直接设置。 - 格式: ```go GetOrSet(ctx context.Context, key interface{}, value interface{}, duration time.Duration) (result *gvar.Var, err error) ``` - 示例:用 `GetOrSet` 判断 `key` 不存在则直接设置,并设置 `duration` 时间。 ```go func ExampleCache_GetOrSet() { c := gcache.New() data, _ := c.GetOrSet(ctx, "k", "v", 10000*time.Millisecond) fmt.Println(data) data1, _ := c.Get(ctx, "k") fmt.Println(data1) // Output: // v // v } ``` ## `GetOrSetFunc` - 说明: 检索并返回 `key` 的值,如果 `key` 对应的值不存在则使用函数 `func` 的结果设置 `key`,如果缓存中存在 `key`,则返回其结果。 - 格式: ```go GetOrSetFunc(ctx context.Context, key interface{}, f func(ctx context.Context) (interface{}, error), duration time.Duration) (result *gvar.Var, err error) ``` - 示例: `k1` 的设置返回 `func` 执行结果, `k2` 返回 `nil` 则不执行任何操作。 ```go func ExampleCache_GetOrSetFunc() { c := gcache.New() c.GetOrSetFunc(ctx, "k1", func(ctx context.Context) (value interface{}, err error) { return "v1", nil }, 10000*time.Millisecond) v, _ := c.Get(ctx, "k1") fmt.Println(v) c.GetOrSetFunc(ctx, "k2", func(ctx context.Context) (value interface{}, err error) { return nil, nil }, 10000*time.Millisecond) v1, _ := c.Get(ctx, "k2") fmt.Println(v1) // Output: // v1 } ``` ## `GetOrSetFuncLock` - 说明: 与 `GetOrSetFunc` 一致,但是不能重复或者 `覆盖注册` 缓存。 - 格式: ```go GetOrSetFuncLock(ctx context.Context, key interface{}, f func(ctx context.Context) (interface{}, error), duration time.Duration) (result *gvar.Var, err error) ``` - 示例:第1次的设置返回 `func` 执行结果,第2次设置操作则失效。 ```go func ExampleCache_GetOrSetFuncLock() { c := gcache.New() c.GetOrSetFuncLock(ctx, "k1", func(ctx context.Context) (value interface{}, err error) { return "v1", nil }, 0) v, _ := c.Get(ctx, "k1") fmt.Println(v) c.GetOrSetFuncLock(ctx, "k1", func(ctx context.Context) (value interface{}, err error) { return "update v1", nil }, 0) v, _ = c.Get(ctx, "k1") fmt.Println(v) c.Remove(ctx, g.Slice{"k1"}...) // Output: // v1 // v1 } ``` ## `Keys` - 说明: 缓存中所有的 `key` 所有键名并以 `切片格式(Slice)` 返回。 - 格式: ```go Keys(ctx context.Context) (keys []interface{}, err error) ``` - 示例: ```go func ExampleCache_Keys() { c := gcache.New() c.SetMap(ctx, g.MapAnyAny{"k1": "v1"}, 0) // Print the current list of key values keys1, _ := c.Keys(ctx) fmt.Println(keys1) // Output: // [k1] } ``` ## `KeyStrings` - 说明: `KeyStrings` 以字符串 `切片` 的形式返回缓存中的 `所有键`。 - 格式: ```go func (c *Cache) KeyStrings(ctx context.Context) ([]string, error) { keys, err := c.Keys(ctx) if err != nil { return nil, err } return gconv.Strings(keys), nil } ``` - 示例: ```go func ExampleCache_KeyStrings() { c := gcache.New() c.SetMap(ctx, g.MapAnyAny{"k1": "v1", "k2": "v2"}, 0) // KeyStrings returns all keys in the cache as string slice. keys,_ := c.KeyStrings(ctx) fmt.Println(keys) // May Output: // [k1 k2] } ``` ## `Remove` - 说明: 移除从缓存中 `删除一个或多个键`,并返回最后一个删除的键值。 - 格式: ```go Remove(ctx context.Context, keys ...interface{}) (lastValue *gvar.Var, err error) ``` - 示例: ```go func ExampleCache_Remove() { c := gcache.New() c.SetMap(ctx, g.MapAnyAny{"k1": "v1", "k2": "v2"}, 0) c.Remove(ctx, "k1") data, _ := c.Data(ctx) fmt.Println(data) // Output: // map[k2:v2] } ``` ## `Removes` - 说明: 从缓存中 `删除多个键`。 - 格式: ```go func (c *Cache) Removes(ctx context.Context, keys []interface{}) error { _, err := c.Remove(ctx, keys...) return err } ``` - 示例: ```go func ExampleCache_Removes() { c := gcache.New() c.SetMap(ctx, g.MapAnyAny{"k1": "v1", "k2": "v2", "k3": "v3", "k4": "v4"}, 0) c.Removes(ctx, g.Slice{"k1", "k2", "k3"}) data, _ := c.Data(ctx) fmt.Println(data) // Output: // map[k4:v4] } ``` ## `Clear` - 说明: 清除 `所有缓存`。 - 格式: ```go func (c *Cache) Clear(ctx context.Context) error ``` - 示例: ```go func ExampleCache_Clear() { c := gcache.New() c.SetMap(ctx, g.MapAnyAny{"k1": "v1", "k2": "v2", "k3": "v3", "k4": "v4"}, 0) c.Clear(ctx) data, _ := c.Data(ctx) fmt.Println(data) // Output: // map[] } ``` ## `MustGet` - 说明: `MustGet` 检索并返回给定 `key` 的关联值。如果它不存在、值为零或已过期,则返回 `nil` 如果 `err` 返回不为空则会 `panic(err)`。 - 格 式: ```go func (c *Cache) MustGet(ctx context.Context, key interface{}) *gvar.Var { v, err := c.Get(ctx, key) if err != nil { panic(err) } return v } ``` - 示例: ```go func ExampleCache_MustGet() { c := gcache.New() c.Set(ctx, "k1", "v1", 0) k2 := c.MustGet(ctx, "k2") k1 := c.MustGet(ctx, "k1") fmt.Println(k1) fmt.Println(k2) // Output: // v1 // } ``` ## `MustGetOrSet` - 说明: 检索并返回 `key` 的值,或者设置 `key-value` 对,如果缓存中不存在 `key`,则直接设置。如果 `err` 返回不为空则会 `panic(err)`。 - 格 式: ```go func (c *Cache) MustGetOrSet(ctx context.Context, key interface{}, value interface{}, duration time.Duration) *gvar.Var { v, err := c.GetOrSet(ctx, key, value, duration) if err != nil { panic(err) } return v } ``` - 示例: ```go func ExampleCache_MustGetOrSet() { // Create a cache object, // Of course, you can also easily use the gcache package method directly c := gcache.New() // MustGetOrSet acts like GetOrSet, but it panics if any error occurs. k1 := c.MustGetOrSet(ctx, "k1", "v1", 0) fmt.Println(k1) k2 := c.MustGetOrSet(ctx, "k1", "v2", 0) fmt.Println(k2) // Output: // v1 // v1 } ``` ## `MustGetOrSetFunc` - 说明: 检索并返回 `key` 的值,如果 `key` 对应的值不存在则使用函数 `func` 的结果设置 `key`,如果缓存中存在 `key`,则返回其结果。如果 `err` 返回不为空则会 `panic(err)`。 - 格 式: ```go func (c *Cache) MustGetOrSetFunc(ctx context.Context, key interface{}, f func(ctx context.Context) (interface{}, error), duration time.Duration) *gvar.Var { v, err := c.GetOrSetFunc(ctx, key, f, duration) if err != nil { panic(err) } return v } ``` - 示例: ```go func ExampleCache_MustGetOrSetFunc() { c := gcache.New() c.MustGetOrSetFunc(ctx, 1, func(ctx context.Context) (interface{}, error) { return 111, nil }, 10000*time.Millisecond) v := c.MustGet(ctx, 1) fmt.Println(v) c.MustGetOrSetFunc(ctx, 2, func(ctx context.Context) (interface{}, error) { return nil, nil }, 10000*time.Millisecond) v1 := c.MustGet(ctx, 2) fmt.Println(v1) // Output: // 111 // } ``` ## `MustGetOrSetFuncLock` - 说明: 与 `MustGetOrSetFunc` 一致,但是不能重复或者 `覆盖注册` 缓存。如果 `err` 返回不为空则会 `panic(err)`。 - 格 式: ```go func (c *Cache) MustGetOrSetFuncLock(ctx context.Context, key interface{}, f func(ctx context.Context) (interface{}, error), duration time.Duration) *gvar.Var { v, err := c.GetOrSetFuncLock(ctx, key, f, duration) if err != nil { panic(err) } return v } ``` - 示例: ```go func ExampleCache_MustGetOrSetFuncLock() { c := gcache.New() c.MustGetOrSetFuncLock(ctx, "k1", func(ctx context.Context) (interface{}, error) { return "v1", nil }, 0) v := c.MustGet(ctx, "k1") fmt.Println(v) // Modification failed c.MustGetOrSetFuncLock(ctx, "k1", func(ctx context.Context) (interface{}, error) { return "update v1", nil }, 0) v = c.MustGet(ctx, "k1") fmt.Println(v) // Output: // v1 // v1 } ``` ## `MustContains` - 说明: 如果缓存中存在指定的 `key`,则 `Contains` 返回 `true`,否则返回 `false`。如果 `err` 返回不为空则会 `panic(err)`。 - 格 式: ```go func (c *Cache) MustContains(ctx context.Context, key interface{}) bool { v, err := c.Contains(ctx, key) if err != nil { panic(err) } return v } ``` - 示例: ```go func ExampleCache_MustContains() { c := gcache.New() // Set Cache c.Set(ctx, "k", "v", 0) // Contains returns true if `key` exists in the cache, or else returns false. // return true data := c.MustContains(ctx, "k") fmt.Println(data) // return false data1 := c.MustContains(ctx, "k1") fmt.Println(data1) // Output: // true // false } ``` ## `MustGetExpire` - 说明:  `MustGetExpire` 检索并返回缓存中 `key` 的过期时间。注意,如果 `key` 为永不过期,则返回 `0`。如果缓存中不存在 `key`,则返回 `-1`,如果 `err` 返回不为空则会 `panic(err)`。 - 格 式: ```go func (c *Cache) MustGetExpire(ctx context.Context, key interface{}) time.Duration { v, err := c.GetExpire(ctx, key) if err != nil { panic(err) } return v } ``` - 示例: ```go func ExampleCache_MustGetExpire() { c := gcache.New() // Set cache without expiration c.Set(ctx, "k", "v", 10000*time.Millisecond) // MustGetExpire acts like GetExpire, but it panics if any error occurs. expire := c.MustGetExpire(ctx, "k") fmt.Println(expire) // May Output: // 10s } ``` ## `MustSize` - 说明:  `MustSize` 返回缓存中的项数。如果 `err` 返回不为空则会 `panic(err)`。 - 格 式: ```go func (c *Cache) MustSize(ctx context.Context) int { v, err := c.Size(ctx) if err != nil { panic(err) } return v } ``` - 示例: ```go func ExampleCache_MustSize() { c := gcache.New() // Add 10 elements without expiration for i := 0; i < 10; i++ { c.Set(ctx, i, i, 0) } // Size returns the number of items in the cache. n := c.MustSize(ctx) fmt.Println(n) // Output: // 10 } ``` ## `MustData` - 说明: 数据以 `map` 类型返回缓存中所有 `键值对('key':'value')` 的拷贝。如果 `err` 返回不为空则会 `panic(err)`。 - 格 式: ```go func (c *Cache) MustData(ctx context.Context) map[interface{}]interface{} { v, err := c.Data(ctx) if err != nil { panic(err) } return v } ``` - 示例: ```go func ExampleCache_MustData() { c := gcache.New() c.SetMap(ctx, g.MapAnyAny{"k1": "v1"}, 0) data := c.MustData(ctx) fmt.Println(data) // Set Cache c.Set(ctx, "k5", "v5", 0) data1, _ := c.Get(ctx, "k1") fmt.Println(data1) // Output: // map[k1:v1] // v1 } ``` ## `MustKeys` - 说明: `MustKeys` 以 `(slice)切片` 的形式返回缓存中的所有键,如果 `err` 返回不为空则会 `panic(err)`。 - 格 式: ```go func (c *Cache) MustKeys(ctx context.Context) []interface{} { v, err := c.Keys(ctx) if err != nil { panic(err) } return v } ``` - 示例: ```go func ExampleCache_MustKeys() { c := gcache.New() c.SetMap(ctx, g.MapAnyAny{"k1": "v1", "k2": "v2"}, 0) // MustKeys acts like Keys, but it panics if any error occurs. keys1 := c.MustKeys(ctx) fmt.Println(keys1) // May Output: // [k1 k2] } ``` ## `MustKeyStrings` - 说明: `MustKeyStrings` 以 `字符串(slice)切片` 的形式返回缓存中的 `所有键`。如果 `err` 返回不为空则会 `panic(err)`。 - 格 式: ```go func (c *Cache) MustKeyStrings(ctx context.Context) []string { v, err := c.KeyStrings(ctx) if err != nil { panic(err) } return v } ``` - 示例: ```go func ExampleCache_MustKeyStrings() { c := gcache.New() c.SetMap(ctx, g.MapAnyAny{"k1": "v1", "k2": "v2"}, 0) // MustKeyStrings returns all keys in the cache as string slice. // MustKeyStrings acts like KeyStrings, but it panics if any error occurs. keys := c.MustKeyStrings(ctx) fmt.Println(keys) // May Output: // [k1 k2] } ``` ## `MustValues` - 说明: `MustValues` 以 `(slice)切片` 的形式返回缓存中的所有值,如果 `err` 返回不为空则会 `panic(err)`。 - 格 式: ```go func (c *Cache) MustValues(ctx context.Context) []interface{} { v, err := c.Values(ctx) if err != nil { panic(err) } return v } ``` - 示例: ```go func ExampleCache_MustValues() { c := gcache.New() // Write value c.Set(ctx, "k1", "v1", 0) // Values returns all values in the cache as slice. data := c.MustValues(ctx) fmt.Println(data) // Output: // [v1] } ``` # 模板引擎 ## 模板引擎特点 1. 简单、易用、强大; 2. 支持多模板目录搜索; 3. 支持 `layout` 模板设计; 4. 支持模板视图对象单例模式; 5. 与配置管理模块原生集成,使用方便; 6. 底层采用了二级缓存设计,性能高效; 7. 新增模板标签及大量的内置模板变量、模板函数; 8. 支持模板文件修改后自动更新缓存机制,对开发过程更友好; 9. `define`/ `template` 标签支持跨模板调用(同一模板路径包括子目录下的模板文件); 10. `include` 标签支持任意路径的模板文件引入; ## 通用视图管理 通用视图管理即使用原生的模板引擎 `gview` 模块来实现模板管理,包括模板读取展示,模板变量渲染等等。可以使用通过方法 `gview.Instance` 来获取视图单例对象,并可以按照单例对象名称进行获取。同时也可以通过对象管理器 `g.View()` 来获取默认的单例 `gview` 对象。 **接口文档**: [https://pkg.go.dev/github.com/gogf/gf/v2/os/gview](https://pkg.go.dev/github.com/gogf/gf/v2/os/gview) 简要说明: 1. `gview.Get` 用于根据给定的一个模板目录路径,获得对应的单例模板引擎对象; 2. `gview.New` 同样可以根据给定的模板目录路径创建模板引擎对象,但没有单例管理; 3. `SetPath/AddPath` 用于设置/添加当前模板引擎对象的模板目录路径,其中 `SetPath` 会覆盖所有的模板目录设置,推荐使用 `AddPath`; 4. `Assign/Assigns` 用于设置模板变量,通过该模板引擎解析的所有模板均可以使用这些模板变量; 5. `BindFunc` 用于绑定模板函数,具体使用方法参考后续示例程序; 6. `Parse/ParseContent` 用于解析模板文件/内容,可以在解析时给定临时的模板变量及模板函数; 7. `SetDelimiters` 用于设置该模板引擎对象的模板解析分隔符号,默认为 `{{ }}`(与 `vuejs` 前端框架有冲突); **注意** 需要注意,从 `goframe v1.16` 版本开始,所有模板的解析方法都额外增加了第一个输入参数为 `Context` 上下文变量的传递。 ### 示例1,解析模板文件 `index.tpl` ``` id:{{.id}}, name:{{.name}} ``` `main.go` ```go package main import ( "github.com/gogf/gf/v2/frame/g" "github.com/gogf/gf/v2/net/ghttp" ) func main() { s := g.Server() s.BindHandler("/template", func(r *ghttp.Request) { r.Response.WriteTpl("index.tpl", g.Map{ "id": 123, "name": "john", }) }) s.SetPort(8199) s.Run() } ``` 执行后,访问 [http://127.0.0.1:8199/template](http://127.0.0.1:8199/template) 可以看到解析后的内容为: `id:123, name:john` ### 示例2,解析模板内容 ```go package main import ( "github.com/gogf/gf/v2/frame/g" "github.com/gogf/gf/v2/net/ghttp" ) func main() { s := g.Server() s.BindHandler("/template", func(r *ghttp.Request){ tplContent := `id:{{.id}}, name:{{.name}}` r.Response.WriteTplContent(tplContent, g.Map{ "id" : 123, "name" : "john", }) }) s.SetPort(8199) s.Run() } ``` 执行后,访问 [http://127.0.0.1:8199/template](http://127.0.0.1:8199/template) 可以看到解析后的内容为: `id:123, name:john` ### 示例3,自定义模板分隔符 在项目中我们经常会遇到 `Golang` 默认模板变量分隔符号与 `Vue` 的变量分隔符号冲突的情况(都使用的是 `{{ }}`),我们可以使用 `SetDelimiters` 方法来自定义全局的 `Golang` 模板变量分隔符号: ```go // main.go package main import ( "context" "fmt" "github.com/gogf/gf/v2/frame/g" ) func main() { v := g.View() v.SetDelimiters("${", "}") b, err := v.Parse( context.TODO(), "gview_delimiters.tpl", map[string]interface{}{ "k": "v", }) fmt.Println(err) fmt.Println(b) } ``` ```html test.tpl content, vars: ${.} ``` 执行后,输出结果为: ```html test.tpl content, vars: map[k:v] ``` ## 目录配置方法 `GoFrame` 框架的模板引擎支持非常灵活的多目录自动搜索功能,通过 `SetPath` 可以修改模板目录为唯一的一个目录地址,同时,我们可以通过 `AddPath` 方法添加多个搜索目录,模板引擎底层将会按照添加目录的顺序作为优先级进行自动检索。直到检索到一个匹配的文件路径为止,如果在所有搜索目录下查找不到模板文件,那么会返回失败。 **默认目录配置**: `gview` 视图对象初始化时,默认会自动添加以下模板文件搜索目录: 1. **当前工作目录及其下的 `template` 目录**:例如当前的工作目录为 `/home/www` 时,将会添加 `/home/www` 及 `/home/www/template`; 2. **当前可执行文件所在目录及其下的 `template` 目录**:例如二进制文件所在目录为 `/tmp` 时,将会添加 `/tmp` 及 `/tmp/template`; 3. **当前 `main` 源代码包所在目录及其下的 `template` 目录**(仅对源码开发环境有效):例如 `main` 包所在目录为 `/home/john/workspace/gf-app` 时,将会添加 `/home/john/workspace/gf-app` 及 `/home/john/workspace/gf-app/template`; ## 修改模板目录 我们可以通过以下方式修改视图对象的模板文件搜索目录,视图对象将会只在该指定目录执行配置文件检索: 1. (推荐)单例模式获取全局View对象,通过 `SetPath` 方法手动修改; 2. 修改命令行启动参数 - `gf.gview.path`; 3. 修改指定的环境变量 - `GF_GVIEW_PATH`; 例如,我们的执行程序文件为 `main`,那么可以通过以下方式修改模板引擎的模板目录(Linux下): 1. (推荐)通过单例模式 ```go g.View().SetPath("/opt/template") ``` 2. 通过命令行参数 ```shell ./main --gf.gview.path=/opt/template/ ``` 3. 通过环境变量 - 启动时修改环境变量: ```shell GF_GVIEW_PATH=/opt/config/; ./main ``` - 使用 `genv` 模块来修改环境变量: ```go genv.Set("GF_GVIEW_PATH", "/opt/template") ``` ## 自动检测更新 模板引擎使用了精心设计的 **缓存机制**,当模板文件第一次被读取后会被缓存到内存,下一次读取时将会直接从缓存中获取,以提高执行效率。并且,模板引擎提供了对模板文件的 **自动检测更新机制**,当模板文件在外部被修改后,模板引擎能够即时地监控到并刷新模板文件的缓存内容。 # 模板引擎-模板配置 视图组件是`GoFrame`框架核心的组件之一,当然也支持非常方便的配置管理功能。 ## 配置对象 配置对象定义: [https://pkg.go.dev/github.com/gogf/gf/v2/os/gview#Config](https://pkg.go.dev/github.com/gogf/gf/v2/os/gview#Config) ## 配置文件 视图组件支持配置文件,当使用`g.View(单例名称)`获取`View`单例对象时,将会自动通过默认的配置管理对象获取对应的`View`配置。默认情况下会读取`viewer.单例名称`配置项,当该配置项不存在时,将会读取`viewer`配置项。 完整配置文件配置项及说明如下,其中配置项名称不区分大小写: ``` [viewer] Paths = ["/var/www/template"] # 模板文件搜索目录路径,建议使用绝对路径。默认为当前程序工作路径 DefaultFile = "index.html" # 默认解析的模板引擎文件。默认为"index.html" Delimiters = ["${", "}"] # 模板引擎变量分隔符号。默认为 ["{{", "}}"] AutoEncode = false # 是否默认对变量内容进行XSS编码。默认为false [viewer.Data] # 自定义的全局Key-Value键值对,将在模板解析中可被直接使用到 Key1 = "Value1" Key2 = "Value2" ``` ### 示例1,默认配置项 ``` [viewer] paths = ["template", "/var/www/template"] defaultFile = "index.html" delimiters = ["${", "}"] [viewer.data] name = "gf" version = "1.10.0" ``` 随后可以使用 `g.View()` 获取默认的单例对象时自动获取并设置该配置。 ### 示例2,多个配置项 多个 `View` 对象的配置示例: ``` [viewer] paths = ["template", "/var/www/template"] defaultFile = "index.html" delimiters = ["${", "}"] [viewer.data] name = "gf" version = "1.10.0" [viewer.view1] defaultFile = "layout.html" delimiters = ["${", "}"] [viewer.view2] defaultFile = "main.html" delimiters = ["#{", "}"] ``` 我们可以通过单例对象名称获取对应配置的 `View` 单例对象: ```go // 对应 viewer.view1 配置项 v1 := g.View("view1") // 对应 viewer.view2 配置项 v2 := g.View("view2") // 对应默认配置项 viewer v3 := g.View("none") // 对应默认配置项 viewer v4 := g.View() ``` ### 配置方法 方法列表: [https://pkg.go.dev/github.com/gogf/gf/v2/os/gview](https://pkg.go.dev/github.com/gogf/gf/v2/os/gview) 简要说明: 1. 可以通过 `SetConfig` 及 `SetConfigWithMap` 来设置。 2. 也可以使用 `View` 对象的 `Set*` 方法进行特定配置的设置。 3. 主要注意的是,配置项在 `View` 对象执行视图解析之前设置,避免并发安全问题。 ### `SetConfigWithMap` 方法 我们可以使用 `SetConfigWithMap` 方法通过 `Key-Value` 键值对来设置/修改 `View` 的特定配置,其余的配置使用默认配置即可。其中 `Key` 的名称即是 `Config` 这个 `struct` 中的属性名称,并且不区分大小写,单词间也支持使用 `-`/ `_`/ `空格` 符号连接,具体可参考 [类型转换-Struct转换](./gconv-struct.md) 章节转换规则。 简单示例: ```go package main import ( "context" "fmt" "github.com/gogf/gf/v2/frame/g" "github.com/gogf/gf/v2/os/gview" ) func main() { view := gview.New() view.SetConfigWithMap(g.Map{ "Paths": []string{"template"}, "DefaultFile": "index.html", "Delimiters": []string{"${", "}"}, "Data": g.Map{ "name": "gf", "version": "1.10.0", }, }) result, err := view.ParseContent(context.TODO(), "hello ${.name}, version: ${.version}") if err != nil { panic(err) } fmt.Println(result) } ``` 其中 `DefaultFile` 表示默认解析的模板文件,键名也可以使用 `defaultFile`, `default-File`, `default_file`, `default file`,其他配置属性以此类推。 ## 注意事项 经常有小伙伴问,为神马我的模板解析没有生效?为神马页面上直接将我写的标签原木原样显示出来了? 这个时候请检查的配置文件中是否有设置模板标签,常见的是 `delimiters` 设置为了 `["${", "}"]`,但是模板中使用的是 `["{{", "}}"]`。 # 模板引擎-模板标签 模板引擎默认使用了`{{`和`}}`作为左右闭合标签,开发者可通过`gview.SetDelimiters`方法设置自定义的模板闭合标签。 使用`.`来访问当前对象的值(模板局部变量)。 使用`$`来引用当前模板根级的上下文(模板全局变量)。 使用`$var`来访问特定的模板变量。 **模板中支持的`go`语言符号** ``` {{"string"}} // 一般 string {{`raw string`}} // 原始 string {{'c'}} // byte {{print nil}} // nil 也被支持 ``` **模板中的 `pipeline`** 可以是上下文的变量输出,也可以是函数通过管道传递的返回值 ``` {{. | FuncA | FuncB | FuncC}} ``` 当 `pipeline` 的值等于: - `false` 或 `0` - `nil的指针` 或 `interface` - 长度为 `0` 的 `array`, `slice`, `map`, `string` 那么这个 `pipeline` 被认为是空。 **注意** 需要注意:在 `gf` 模板引擎中,当模板中展示的指定变量不存在时,将会显示为空(标准库模板引擎会展示 ``)。 ## `if … else … end` ``` {{if pipeline}}...{{end}} ``` `if` 判断时, `pipeline` 为空时,相当于判断为 `false`。 支持嵌套的循环 ``` {{if .condition}} ... {{else}} {{if .condition2}} ... {{end}} {{end}} ``` 也可以使用 `else if` 进行 ``` {{if .condition}} ... {{else if .condition2}} ... {{else}} ... {{end}} ``` ## `range … end` ``` {{range pipeline}} {{.}} {{end}} ``` `pipeline` 支持的类型为 `slice`, `map`, `channel`。 注意:在 `range` 循环内部, `.` 符号会被覆盖为以上类型的子元素(局部变量)。如果想在循环中访问外部变量(全局变量),请加上 `$` 符号,如: `{{$.Session.Name}}` 此外,对应的值长度为 `0` 时, `range` 不会执行, `.` 不会改变。 例如,遍历 `map`: ``` {{range $key, $value := .MapContent}} {{$key}}:{{$value}} {{end}} ``` 例如,遍历 `slice`: ``` {{range $index, $elem := .SliceContent}} {{range $key, $value := $elem}} {{$key}}:{{$value}} {{end}} {{end}} ``` ## `with … end` ``` {{with pipeline}}...{{end}} ``` `with` 用于重定向 `pipeline` ``` {{with .Field.NestField.SubField}} {{.Var}} {{end}} ``` ## `define` `define` 可以用来 **自定义模板内容块**(给一段模板内容定义一个模板名称),可用于模块定义和模板嵌套(使用在 `template` 标签中)。 ``` {{define "loop"}}
  • {{.Name}}
  • {{end}} ``` 其中 `loop` 为该模板内容块的名称,随后可使用 `template` 标签调用模板: ```
      {{range .Items}} {{template "loop" .}} {{end}}
    ``` **注意** `define` 标签需要结合 `template` 标签一起使用,并且支持跨模板使用(在同一模板目录/子目录下有效,原理是使用的 `ParseFiles` 方法解析模板文件)。 ## `template` ``` {{template "模板名称" pipeline}} ``` 将对应的上下文 `pipeline` 传给模板,才可以在模板中调用。 注意: `template` 标签参数为 `模板名称`,而不是模板文件路径, `template` 标签不支持模板文件路径。 **注意** `template` 标签需要结合 `define` 标签一起使用,并且支持跨模板使用(在同一模板目录/子目录下有效,原理是使用的 `ParseFiles` 方法解析模板文件)。 ## `include` **该标签为 `gf` 框架模板引擎新增标签** ``` {{include "模板文件名(需要带完整文件名后缀)" pipeline}} ``` 在模板中可以使用 `include` 标签载入其他模板(任意路径),模板文件名支持 **相对路径** 以及文件的系统 **绝对路径**。如果想要把当前模板的模板变量传递给子模板(嵌套模板),可以这样: ``` {{include "模板文件名(需要带完整文件名后缀)" .}} ``` 与 `template` 标签的区别是: `include` 仅支持 **文件路径**,不支持 **模板名称**;而 `tempalte` 标签仅支持 **模板名称**,不支持 **文件路径**。 ## 注释 允许多行文本注释,不允许嵌套。 ``` {{/* comment content support new line */}} ``` ## 删除空白符号 删除移除空格换行符 ``` # 使用{{-语法去除模板内容左侧的所有空白符号, 使用-}}去除模板内容右侧的所有空白符号。 # 注意:-要紧挨{{和}},同时与模板值之间需要使用空格分隔。 {{- .Name -}} {{- range $key, $value := .list}} "{{$value}}" {{- end}} ``` ## 更多使用说明 [template package - html/template - Go Packages](https://pkg.go.dev/html/template) [template package - text/template - Go Packages](https://pkg.go.dev/text/template) # 模板引擎-模板变量 ## 内置变量 `WebServer`内置变量请参考[数据返回-模板解析](../web/response-template.md)章节。 ## 变量对象 我们可以在模板中使用自定义的对象,并可在模板中访问对象的属性及调用其方法。 示例: ```go package main import ( "context" "github.com/gogf/gf/v2/frame/g" ) type T struct { Name string } func (t *T) Hello(name string) string { return "Hello " + name } func (t *T) Test() string { return "This is test" } func main() { t := &T{"John"} v := g.View() content := `{{.t.Hello "there"}}, my name's {{.t.Name}}. {{.t.Test}}.` if r, err := v.ParseContent(context.TODO(), content, g.Map{"t": t}); err != nil { g.Dump(err) } else { g.Dump(r) } } ``` 其中,赋值给模板的变量既可以是`对象指针`也可以是`对象变量`。但是注意定义的对象方法,如果为对象指针那么只能调用方法接收器为对象指针的方法;如果为对象变量,那么只能调用方法接收器为对象的方法。 执行后,输出结果为: ``` Hello there, my name's John. This is test. ``` # 模板引擎-模板布局 `gview`模板引擎支持两种`layout`模板布局方式: 1. `define` + `template`方式 2. `include`模板嵌入方式 这两种方式均支持对模板变量的传入。 ### `define` + `template` 由于`gview`底层采用了`ParseFiles`方式批量解析模板文件,因此可以使用`define`标签定义模板内容块,通过`template`标签在其他任意的模板文件中引入指定的模板内容块。`template`标签支持跨模板引用,也就是说`define`标签定义的模板内容块可能是在其他模板文件中,`template`也可以随意引入。 **注意** 注意: - 为嵌套的子模板传递模板变量时,应当使用: `{{template "xxx" .}}` 的语法。 - 模板文件的后缀名,要跟 `define template` 文件的后缀名保持一致。 使用示例: 1. `layout.html` ```html GoFrame Layout {{template "header" .}}
    {{template "container" .}}
    ``` 2. `header.html` ```html {{define "header"}}

    {{.header}}

    {{end}} ``` 1. `container.html` ```html {{define "container"}}

    {{.container}}

    {{end}} ``` 1. `footer.html` ```html {{define "footer"}}

    {{.footer}}

    {{end}} ``` 1. `main.go` ```go package main import ( "github.com/gogf/gf/v2/frame/g" "github.com/gogf/gf/v2/net/ghttp" ) func main() { s := g.Server() s.BindHandler("/", func(r *ghttp.Request) { r.Response.WriteTpl("layout.html", g.Map{ "header": "This is header", "container": "This is container", "footer": "This is footer", }) }) s.SetPort(8199) s.Run() } ``` 执行后,访问 [http://127.0.0.1:8199](http://127.0.0.1:8199/) 结果如下: ### `include`模板嵌入 当然我们也可以使用`include`标签来实现页面布局。 **注意** 注意,为嵌套的子模板传递模板变量时,应当使用: `{{include "xxx" .}}` 的语法。 使用示例: 1. `layout.html` ```html {{include "header.html" .}} {{include .mainTpl .}} {{include "footer.html" .}} ``` 2. `header.html` ```html

    HEADER

    ``` 3. `footer.html` ```html

    FOOTER

    ``` 4. `main1.html` ```html

    MAIN1

    ``` 1. `main2.html` ```html

    MAIN2

    ``` 1. `main.go` ```go package main import ( "github.com/gogf/gf/v2/frame/g" "github.com/gogf/gf/v2/net/ghttp" ) func main() { s := g.Server() s.BindHandler("/main1", func(r *ghttp.Request) { r.Response.WriteTpl("layout.html", g.Map{ "mainTpl": "main/main1.html", }) }) s.BindHandler("/main2", func(r *ghttp.Request) { r.Response.WriteTpl("layout.html", g.Map{ "mainTpl": "main/main2.html", }) }) s.SetPort(8199) s.Run() } ``` 执行后,访问不同的路由地址,将会看到不同的结果: 1. [http://127.0.0.1:8199/main1](http://127.0.0.1:8199/main1) 2. [http://127.0.0.1:8199/main2](http://127.0.0.1:8199/main2) # 模板引擎-XSS处理 默认情况下,模板引擎对所有的变量输出并没有使用 `HTML` 转码处理,也就是说,如果开发者处理不好,可能会存在XSS漏洞。 不用担心, `GoFrame` 框架当然已经充分考虑到这点,并且为开发者提供了比较灵活的配置参数来控制是否默认转义变量输出的 `HTML` 内容。该特性可以通过 `AutoEncode` 配置项,或者 `SetAutoEncode` 方法来开启/关闭。 **提示** 需要注意的是,该特性并不会影响 `include` 模板的内置函数。 使用示例: 1、配置文件 ```toml [viewer] delimiters = ["${", "}"] autoencode = true ``` 2、示例代码 ```go package main import ( "context" "fmt" "github.com/gogf/gf/v2/frame/g" ) func main() { result, _ := g.View().ParseContent(context.TODO(), "姓名: ${.name}", g.Map{ "name": "", }) fmt.Println(result) } ``` 3、执行输出 ```html 姓名: <script>alert('john');</script> ``` # 模板引擎-其他使用 ## `I18N`支持 模板引擎支持`i18n`特性,可以通过给上下文注入特定的`i18n`语言来实现不同的请求/页面使用不同的`i18n`语言渲染。例如: ```go package main import ( "context" "fmt" "github.com/gogf/gf/v2/frame/g" "github.com/gogf/gf/v2/i18n/gi18n" ) func main() { var ( ctxCN = gi18n.WithLanguage(context.TODO(), "zh-CN") ctxJa = gi18n.WithLanguage(context.TODO(), "ja") content = `{{.name}} says "{#hello}{#world}!"` ) result1, _ := g.View().ParseContent(ctxCN, content, g.Map{ "name": "john", }) fmt.Println(result1) result2, _ := g.View().ParseContent(ctxJa, content, g.Map{ "name": "john", }) fmt.Println(result2) } ``` 执行后,输出结果为:(保证当前运行目录带有 `i18n` 转译配置文件) ```html john says "你好世界!" john says "こんにちは世界!" ``` ## HTTP对象视图 `GoFrame`框架的`WebServer`的返回对象中提供了基础的模板解析方法,如下: ```go func (r *Response) WriteTpl(tpl string, params map[string]interface{}, funcMap ...map[string]interface{}) error func (r *Response) WriteTplContent(content string, params map[string]interface{}, funcMap ...map[string]interface{}) error ``` 其中`WriteTpl`用于输出模板文件,`WriteTplContent`用于直接解析输出模板内容。 使用示例: ```go package main import ( "github.com/gogf/gf/v2/frame/g" "github.com/gogf/gf/v2/net/ghttp" ) func main() { s := g.Server() s.BindHandler("/", func(r *ghttp.Request) { r.Cookie.Set("theme", "default") r.Session.Set("name", "john") r.Response.WriteTplContent(`Cookie:{{.Cookie.theme}}, Session:{{.Session.name}}`, nil) }) s.SetPort(8199) s.Run() } ``` 执行后,输出结果为: ``` Cookie:default, Session:john ``` ## 控制器视图管理 `GoFrame`为路由控制器注册方式提供了良好的模板引擎支持,由`gmvc.View`视图对象进行管理,提供了良好的数据`隔离性`。控制器视图是并发安全设计的,允许在多线程中异步操作。 **注意** 控制器注册方式类似于PHP执行流程,相对来说性能比较低效,因此未来不再推荐使用控制器注册方式。 ```go func (view *View) Assign(key string, value interface{}) func (view *View) Assigns(data gview.Params) func (view *View) Parse(file string) ([]byte, error) func (view *View) ParseContent(content string) ([]byte, error) func (view *View) Display(files ...string) error func (view *View) DisplayContent(content string) error func (view *View) LockFunc(f func(vars map[string]interface{})) func (view *View) RLockFunc(f func(vars map[string]interface{})) ``` 使用示例1: ```go package main import ( "github.com/gogf/gf/v2/net/ghttp" "github.com/gogf/gf/v2/frame/gmvc" ) type ControllerTemplate struct { gmvc.Controller } func (c *ControllerTemplate) Info() { c.View.Assign("name", "john") c.View.Assigns(map[string]interface{}{ "age" : 18, "score" : 100, }) c.View.Display("index.tpl") } func main() { s := ghttp.GetServer() s.BindController("/template", new(ControllerTemplate)) s.SetPort(8199) s.Run() } ``` 其中 `index.tpl` 的模板内容如下: ``` gf template engine

    Name: {{.name}}

    Age: {{.age}}

    Score:{{.score}}

    ``` 执行后,访问 [http://127.0.0.1:8199/template/info](http://127.0.0.1:8199/template/info) 可以看到模板被解析并展示到页面上。如果页面报错找不到模板文件,没有关系,因为这里并没有对模板目录做设置,默认是当前可行文件的执行目录( `Linux&Mac` 下是 `/tmp` 目录, `Windows` 下是 `C:\Documents and Settings\用户名\Local Settings\Temp`)。 其中,给定的模板文件file参数是需要带完整的文件名后缀,例如: `index.tpl`, `index.html` 等等,模板引擎对模板文件后缀名没有要求,用户可完全自定义。此外,模板文件参数也支持文件的绝对路径(完整的文件路径)。 当然,我们也可以直接解析模板内容,例如: ```go package main import ( "github.com/gogf/gf/v2/net/ghttp" "github.com/gogf/gf/v2/frame/gmvc" ) type ControllerTemplate struct { gmvc.Controller } func (c *ControllerTemplate) Info() { c.View.Assign("name", "john") c.View.Assigns(map[string]interface{}{ "age" : 18, "score" : 100, }) c.View.DisplayContent(` gf template engine

    Name: {{.name}}

    Age: {{.age}}

    Score:{{.score}}

    `) } func main() { s := ghttp.GetServer() s.BindController("/template", new(ControllerTemplate{})) s.SetPort(8199) s.Run() } ``` 执行后,访问 [http://127.0.0.1:8199/template/info](http://127.0.0.1:8199/template/info) 可以看到解析后的内容如下: ``` gf template engine

    Name: john

    Age: 18

    Score:100

    ``` # 模板函数-基础函数 **提示** 以下为`Golang`标准库的一些基础语法和基础函数,`GoFrame`框架对部分基础函数做了必要的改进。 变量可以使用符号 `|` 在函数间传递 ``` {{.value | Func1 | Func2}} ``` 使用括号 ``` {{printf "nums is %s %d" (printf "%d %d" 1 2) 3}} ``` ## `and` ``` {{and .X .Y .Z}} ``` `and` 会逐一判断每个参数,将返回第一个为空的参数,否则就返回最后一个非空参数 ## `call` ``` {{call .Field.Func .Arg1 .Arg2}} ``` `call` 可以调用函数,并传入参数 调用的函数需要返回 1 个值 或者 2 个值,返回两个值时,第二个值用于返回 `error` 类型的错误。返回的错误不等于 `nil` 时,执行将终止。 ## `index` `index` 支持 `map`, `slice`, `array`, `string`,读取指定类型对应下标的值。 ``` {{index .Maps "name"}} ``` ## `len` ``` {{printf "The content length is %d" (.Content|len)}} ``` 返回对应类型的长度,支持类型: `map`, `slice`, `array`, `string`, `chan`。 ## `not` `not` 返回输入参数的否定值。 例如,判断是否变量是否为空: ``` {{if not .Var}} // 执行为空操作(.Var为空, 如: nil, 0, "", 长度为0的slice/map) {{else}} // 执行非空操作(.Var不为空) {{end}} ``` ## `or` ``` {{or .X .Y .Z}} ``` `or` 会逐一判断每个参数,将返回第一个非空的参数,否则就返回最后一个参数。 ## `print` 同 `fmt.Sprint`。 ## `printf` 同 `fmt.Sprintf`。 ## `println` 同 `fmt.Sprintln`。 ## `urlquery` ``` {{urlquery "http://johng.cn"}} ``` 将返回 ``` http%3A%2F%2Fjohng.cn ``` ## `eq / ne / lt / le / gt / ge` 这类函数一般配合在 `if` 中使用 ``` `eq`: arg1 == arg2 `ne`: arg1 != arg2 `lt`: arg1 < arg2 `le`: arg1 <= arg2 `gt`: arg1 > arg2 `ge`: arg1 >= arg2 ``` `eq` 和其他函数不一样的地方是,支持多个参数。 ``` {{eq arg1 arg2 arg3 arg4}} ``` 和下面的逻辑判断相同: ``` arg1==arg2 || arg1==arg3 || arg1==arg4 ... ``` 与 `if` 一起使用 ``` {{if eq true .Var1 .Var2 .Var3}}...{{end}} ``` ``` {{if lt 100 200}}...{{end}} ``` 例如,判断变量不为空时执行: ``` {{if .Var}} // 执行非空操作(.Var不为空) {{else}} // 执行为空操作(.Var为空, 如: nil, 0, "", 长度为0的slice/map) {{end}} ``` ### 对比函数改进 `GoFrame` 框架模板引擎对标准库自带的对比模板函数 `eq/ne/lt/le/gt/ge` 做了必要的改进,以便支持任意数据类型的比较。例如,在标准库模板中的以下比较: ``` {{eq 1 "1"}} ``` 将会引发错误: ``` panic: template: at : error calling eq: incompatible types for comparison ``` 由于比较的两个参数不是同一数据类型,因此引发了 `panic` 错误。 在 `GoFrame` 框架的模板引擎中,将会自动将两个参数进行数据转换,转换为同一类型后再进行比较,这样的开发体验更好、灵活性更高。如果两个参数均为整型变量(或者整型字符串),那么将会转换为整型进行比较,否则转换为字符串变量进行比较(区分大小写)。 ### 改进运行示例 我们来看一个 `GoFrame` 框架的模板引擎中的对比模板函数运行示例。 ```go package main import ( "context" "fmt" "github.com/gogf/gf/v2/frame/g" ) func main() { tplContent := ` eq: eq "a" "a": {{eq "a" "a"}} eq "1" "1": {{eq "1" "1"}} eq 1 "1": {{eq 1 "1"}} ne: ne 1 "1": {{ne 1 "1"}} ne "a" "a": {{ne "a" "a"}} ne "a" "b": {{ne "a" "b"}} lt: lt 1 "2": {{lt 1 "2"}} lt 2 2 : {{lt 2 2 }} lt "a" "b": {{lt "a" "b"}} le: le 1 "2": {{le 1 "2"}} le 2 1 : {{le 2 1 }} le "a" "a": {{le "a" "a"}} gt: gt 1 "2": {{gt 1 "2"}} gt 2 1 : {{gt 2 1 }} gt "a" "a": {{gt "a" "a"}} ge: ge 1 "2": {{ge 1 "2"}} ge 2 1 : {{ge 2 1 }} ge "a" "a": {{ge "a" "a"}} ` content, err := g.View().ParseContent(context.TODO(), tplContent, nil) if err != nil { panic(err) } fmt.Println(content) } ``` 运行后,输出结果为: ``` eq: eq "a" "a": true eq "1" "1": true eq 1 "1": true ne: ne 1 "1": false ne "a" "a": false ne "a" "b": true lt: lt 1 "2": true lt 2 2 : false lt "a" "b": true le: le 1 "2": true le 2 1 : false le "a" "a": true gt: gt 1 "2": false gt 2 1 : true gt "a" "a": false ge: ge 1 "2": false ge 2 1 : true ge "a" "a": true ``` # 模板函数-内置函数 ## `plus/minus/times/divide` | 函数 | 说明 | 格式 | 示例 | 备注 | | --- | --- | --- | --- | --- | | `plus` | **加** | `{{.value1 \| plus .value2}}` | `{{2 \| plus 3}} => 5` | `3+2` | | `minus` | **减** | `{{.value1 \| minus .value2}}` | `{{2 \| minus 3}} => 1` | `3-2` | | `times` | **乘** | `{{.value1 \| times .value2}}` | `{{2 \| times 3}} => 6` | `3*2` | | `divide` | **除** | `{{.value1 \| divide .value2}}` | `{{2 \| divide 3}} => 1.5` | `3/2` | ## `text` ``` {{.value | text}} ``` 将`value`变量值去掉HTML标签,仅显示文字内容(并且去掉`script`标签)。示例: ``` {{"
    测试
    "|text}} // 输出: 测试 ``` ## `htmlencode/encode/html` ``` {{.value | htmlencode}} {{.value | encode}} {{.value | html}} ``` 将`value`变量值进行html转义。示例: ``` {{"
    测试
    "|html}} // 输出: <div>测试</div> ``` ## `htmldecode/decode` ``` {{.value | htmldecode}} {{.value | decode}} ``` 将 `value` 变量值进行html反转义。 示例: ``` {{"<div>测试</div>" | htmldecode}} // 输出:
    测试
    ``` ## `urlencode/url` ``` {{.url | url}} ``` 将 `url` 变量值进行 `url` 转义。 示例: ``` {{"https://goframe.org" | url}} // 输出: https%3A%2F%2Fgoframe.org ``` ## `urldecode` ``` {{.url | urldecode}} ``` 将 `url` 变量值进行 `url` 反转义。 示例: ``` {{"https%3A%2F%2Fgoframe.org"|urldecode}} // 输出: https://goframe.org ``` ## `date` ``` {{.timestamp | date .format}} {{date .format .timestamp}} {{date .format}} ``` 将`timestamp`时间戳变量进行时间日期格式化,类似PHP的`date`方法,`format`参数支持[PHP date](http://php.net/manual/en/function.date.php)方法格式,亦可参考[时间管理-gtime](../components/os-gtime.md)。 当`timestamp`变量为`空`(或者`0`)时,表示以当前时间作为时间戳参数执行打印。 示例: ``` {{1540822968 | date "Y-m-d"}} {{"1540822968" | date "Y-m-d H:i:s"}} {{date "Y-m-d H:i:s"}} // 输出: // 2018-10-29 // 2018-10-29 22:22:48 // 2018-12-05 10:22:00 ``` ## `compare` ``` {{compare .str1 .str2}} {{.str2 | compare .str1}} ``` 将`str1`和`str2`进行字符串比较,返回值: - 0 : `str1` == `str2` - 1 : `str1` > `str2` - -1 : `str1` < `str2` 示例: ``` {{compare "A" "B"}} {{compare "1" "2"}} {{compare 2 1}} {{compare 1 1}} // 输出: // -1 // -1 // 1 // 0 ``` ## `replace` ``` {{.str | replace .search .replace}} {{replace .search .replace .str}} ``` 将`str`中的`search`替换为`replace`。示例: ``` {{"I'm中国人" | replace "I'm" "我是"}} // 输出: // 我是中国人 ``` ## `substr` ``` {{.str | substr .start .length}} {{substr .start .length .str}} ``` 将`str`从`start`索引位置(索引从0开始)进行字符串截取`length`,支持中文,类似PHP的`substr`函数。示例: ``` {{"我是中国人" | substr 2 -1}} {{"我是中国人" | substr 2 2}} // 输出: // 中国人 // 中国 ``` ## `strlimit` ``` {{.str | strlimit .length .suffix}} ``` 将`str`字符串截取`length`长度,支持中文,超过长度则追加`suffix`字符串到末尾。示例: ``` {{"我是中国人" | strlimit 2 "..."}} // 输出: // 我是... ``` ## `concat` ``` {{concat .str1 .str2 .str3...}} ``` 拼接字符串。 示例: ``` {{concat "我" "是" "中" "国" "人"}} // 输出: // 我是中国人 ``` ## `hidestr` ``` {{.str | hidestr .percent .hide}} ``` 将`str`字符串按照`percent`百分比从字符串中间向两边隐藏字符(主要用于姓名、手机号、邮箱地址、身份证号等的隐藏),隐藏字符由`hide`变量定义。支持中文,支持email格式。示例: ``` {{"热爱GF热爱生活" | hidestr 20 "*"}} {{"热爱GF热爱生活" | hidestr 50 "*"}} // 输出: // 热爱GF*爱生活 // 热爱****生活 ``` ## `highlight` ``` {{.str | highlight .key .color}} ``` 将 `str` 字符串中的关键字 `key` 按照定义的颜色 `color` 进行前置颜色高亮。 示例: ``` {{"热爱GF热爱生活" | highlight "GF" "red"}} // 输出: // 热爱GF热爱生活 ``` ## `toupper/tolower` ``` {{.str | toupper}} {{.str | tolower}} ``` 将 `str` 字符串进行大小写转换。 示例: ``` {{"gf" | toupper}} {{"GF" | tolower}} // 输出: // GF // gf ``` ## `nl2br` ``` {{.str | nl2br}} ``` 将 `str` 字符串中的 `\n/\r` 替换为html中的 `
    ` 标签。 示例: ``` {{"Go\nFrame" | nl2br}} // 输出: // Go
    Frame ``` ## `dump` ``` {{dump .var}} ``` 格式化打印变量,功能类似于 `g.Dump` 方法,常用于开发调试。 示例: ``` gview.Assign("var", g.Map{ "name" : "john", }) ``` ``` {{dump .var}} // 输出: // ``` ## `map` ``` {{map .var}} ``` 将模板变量转换为 `map[string]interface{}` 类型,常用于 `range...end` 遍历。 ## `maps` ``` {{maps .var}} ``` 将模板变量转换为 `[]map[string]interface{}` 类型,常用于 `range...end` 遍历。 ## `json/xml/ini/yaml/yamli/toml` | 函数 | 说明 | 格式 | | --- | --- | --- | | `json` | 将模板变量转换为 `JSON` 格式字符串。 | `{{json .var}}` | | `xml` | 将模板变量转换为 `XML` 格式字符串。 | `{{xml .var}}` | | `ini` | 将模板变量转换为 `INI` 格式字符串。 | `{{ini .var}}` | | `yaml` | 将模板变量转换为 `YAML` 格式字符串。 | `{{yaml .var}}` | | `yamli` | 将模板变量转换为带有自定义缩进的 `YAML` 格式字符串。 | `{{yamli .var .indent}}` | | `toml` | 将模板变量转换为 `TOML` 格式字符串。 | `{{toml .var}}` | # 模板函数-自定义函数 ## 基本介绍 开发者可以自定义模板函数,全局绑定模板函数到指定的视图对象中。 **提示** 也可以将自定义的对象赋值给模板,随后通过对象来调用其封装的方法。 ## 使用示例 ```go package main import ( "context" "fmt" "github.com/gogf/gf/v2/frame/g" ) // 用于测试的带参数的内置函数 func funcHello(name string) string { return fmt.Sprintf(`Hello %s`, name) } func main() { // 绑定全局的模板函数 g.View().BindFunc("hello", funcHello) // 普通方式传参 parsed1, err := g.View().ParseContent(context.TODO(), `{{hello "GoFrame"}}`, nil) if err != nil { panic(err) } fmt.Println(string(parsed1)) // 通过管道传参 parsed2, err := g.View().ParseContent(context.TODO(), `{{"GoFrame" | hello}}`, nil) if err != nil { panic(err) } fmt.Println(string(parsed2)) } ``` 执行后,输出结果为: ``` Hello GoFrame Hello GoFrame ``` # 数据库ORM ## 驱动引入 为了将数据库驱动与框架主库解耦,从 `v2.1` 版本开始,所有的数据库驱动都需要通过社区包手动引入。 数据库驱动的安装和引入请参考: [https://github.com/gogf/gf/tree/master/contrib/drivers](https://github.com/gogf/gf/tree/master/contrib/drivers) ## 基本介绍 `GoFrame` 框架的 `ORM` 功能由 `gdb` 模块实现,用于常用关系型数据库的 `ORM` 操作。 **提示** `gdb` 数据库引擎底层采用了 **链接池设计**,当链接不再使用时会自动关闭,因此链接对象不用的时候不需要显式使用 `Close` 方法关闭数据库连接。 **备注** 注意:为提高数据库操作安全性,在 `ORM` 操作中不建议直接将参数拼接成 `SQL` 字符串执行,建议使用预处理的方式(充分使用 `?` 占位符)来传递 `SQL` 参数。 `gdb` 的底层实现中均采用的是预处理的方式处理开发者传递的参数,以充分保证数据库操作安全性。 **接口文档:** [https://pkg.go.dev/github.com/gogf/gf/v2/database/gdb](https://pkg.go.dev/github.com/gogf/gf/v2/database/gdb) ## 组件特性 `GoFrame ORM` 组件具有以下显著特点: 1. 全自动化支持嵌套事务。 2. 面向接口化设计、易使用易扩展。 3. 内置支持主流数据库类型驱动,并易于扩展。 4. 强大的配置管理,使用框架统一的配置组件。 5. 支持单例模式获取配置同一分组数据库对象。 6. 支持原生SQL方法操作、ORM链式操作两种方式。 7. 支持 `OpenTelemetry` 可观测性:链路跟踪、日志记录、指标上报。 8. 通过 `Scan` 方法自动识别 `Map/Struct` 接收查询结果,自动化查询结果初始化、结构体类型转换。 9. 通过返回结果 `nil` 识别为空,无需 `sql.ErrNoRows` 识别查询数据为空的情况。 10. 全自动化的结构体属性-字段映射,无需显示定义结构体标签维护属性-字段映射关系。 11. 自动化的给定 `Map/Struct/Slice` 参数类型中的字段识别、过滤,大大提高查询条件输入、结果接收。 12. 完美支持 `GoFrame` 框架层面的 `DAO` 设计,全自动化 `Model/DAO` 代码生成,极大提高开发效率。 13. 支持调试模式、日志输出、 `DryRun`、自定义 `Handler`、自动类型类型转换、自定义接口转换等等高级特性。 14. 支持查询缓存、软删除、自动化时间更新、模型关联、数据库集群配置(软件主从模式)等等实用特性。 ## 组件关联 `GoFrame ORM Dependencies` ## `g.DB` 与 `gdb.New`、 `gdb.Instance` 获取数据库操作对象有三种方式,一种是使用 `g.DB` 方法(推荐),一种是使用原生 `gdb.New` 方法,还有一种是使用包原生单例方法 `gdb.Instance`,而第一种是推荐的使用方式。这三种方式的区别如下: 1. `g.DB` 对象管理方式获取的是单例对象,整合了配置文件的管理功能,支持配置文件热更新。 2. `gdb.New` 是根据给定的数据库节点配置创建一个新的数据库对象(非单例),无法使用配置文件。 3. `gdb.Instance` 是包原生单例管理方法,需要结合配置方法一起使用,通过分组名称(非必需)获取对应配置的数据库单例对象。 **提示** 有这么多对象获取方式原因在于 `GoFrame` 是一个模块化设计的框架,每个模块皆可单独使用。 ### `New` 创建数据库对象 ```go db, err := gdb.New(gdb.ConfigNode{ Link: "mysql:root:12345678@tcp(127.0.0.1:3306)/test", }) ``` ### 获取数据库对象单例 ```go // 获取默认配置的数据库对象(配置名称为"default") db := g.DB() // 获取配置分组名称为"user"的数据库对象 db := g.DB("user") // 使用原生单例管理方法获取数据库对象单例 db, err := gdb.Instance() db, err := gdb.Instance("user") ``` # ORM方法操作(原生) ## 方法操作 方法操作用于原生 `SQL` 执行,相对链式操作更偏底层操作一些,在 `ORM` 链式操作执行不了太过于复杂的 `SQL` 操作时,可以交给方法操作来处理。 **接口文档:** [https://pkg.go.dev/github.com/gogf/gf/v2/database/gdb#DB](https://pkg.go.dev/github.com/gogf/gf/v2/database/gdb#DB) **常用方法:** 本文档的方法列表可能滞后于于代码,详细的方法列表请查看接口文档,以下方法仅供参考。 ```go // SQL操作方法,返回原生的标准库sql对象 Query(ctx context.Context, query string, args ...interface{}) (*sql.Rows, error) Exec(ctx context.Context, query string, args ...interface{}) (sql.Result, error) Prepare(ctx context.Context, query string) (*sql.Stmt, error) // 数据表记录查询: // 查询单条记录、查询多条记录、获取记录对象、查询单个字段值(链式操作同理) GetAll(ctx context.Context, sql string, args ...interface{}) (Result, error) GetOne(ctx context.Context, sql string, args ...interface{}) (Record, error) GetValue(ctx context.Context, sql string, args ...interface{}) (Value, error) GetArray(ctx context.Context, sql string, args ...interface{}) ([]Value, error) GetCount(ctx context.Context, sql string, args ...interface{}) (int, error) GetScan(ctx context.Context, objPointer interface{}, sql string, args ...interface{}) error // 数据单条操作 Insert(ctx context.Context, table string, data interface{}, batch...int) (sql.Result, error) Replace(ctx context.Context, table string, data interface{}, batch...int) (sql.Result, error) Save(ctx context.Context, table string, data interface{}, batch...int) (sql.Result, error) // 数据修改/删除 Update(ctx context.Context, table string, data interface{}, condition interface{}, args ...interface{}) (sql.Result, error) Delete(ctx context.Context, table string, condition interface{}, args ...interface{}) (sql.Result, error) ``` **简要说明:** 1. `Query` 是原始的数据查询方法,返回的是原生的标准库的结果集对象,需要自行解析。推荐使用 `Get*` 方法,会对结果自动做解析。 2. `Exec` 方法用于写入/更新的 `SQL` 的操作。 3.  在执行数据查询时推荐使用 `Get*` 系列查询方法。 4. `Insert`/ `Replace`/ `Save` 方法中的 `data` 参数支持的数据类型为: `string/map/slice/struct/*struct`,当传递为 `slice` 类型时,自动识别为批量操作,此时 `batch` 参数有效。 ## 操作示例 ### 1\. `ORM` 对象 ```go // 获取默认配置的数据库对象(配置名称为"default") db := g.DB() // 获取配置分组名称为"user-center"的数据库对象 db := g.DB("user-center") // 使用原生单例管理方法获取数据库对象单例 db, err := gdb.Instance() db, err := gdb.Instance("user-center") // 注意不用的时候不需要使用Close方法关闭数据库连接(并且gdb也没有提供Close方法), // 数据库引擎底层采用了链接池设计,当链接不再使用时会自动关闭 ``` ### 2\. 数据写入 ```go r, err := db.Insert(ctx, "user", gdb.Map { "name": "john", }) ``` ### 3\. 数据查询(列表) ```go list, err := db.GetAll(ctx, "select * from user limit 2") list, err := db.GetAll(ctx, "select * from user where age > ? and name like ?", g.Slice{18, "%john%"}) list, err := db.GetAll(ctx, "select * from user where status=?", g.Slice{1}) ``` ### 4\. 数据查询(单条) ```go one, err := db.GetOne(ctx, "select * from user limit 2") one, err := db.GetOne(ctx, "select * from user where uid=1000") one, err := db.GetOne(ctx, "select * from user where uid=?", 1000) one, err := db.GetOne(ctx, "select * from user where uid=?", g.Slice{1000}) ``` ### 5\. 数据保存 ```go r, err := db.Save(ctx, "user", gdb.Map { "uid" : 1, "name" : "john", }) ``` ### 6\. 批量操作 其中 `batch` 参数用于指定批量操作中分批写入条数数量(默认是 `10`)。 ```go _, err := db.Insert(ctx, "user", gdb.List { {"name": "john_1"}, {"name": "john_2"}, {"name": "john_3"}, {"name": "john_4"}, }, 10) ``` ### 7\. 数据更新/删除 ```go // db.Update/db.Delete 同理 // UPDATE `user` SET `name`='john' WHERE `uid`=10000 r, err := db.Update(ctx, "user", gdb.Map {"name": "john"}, "uid=?", 10000) // UPDATE `user` SET `name`='john' WHERE `uid`=10000 r, err := db.Update(ctx, "user", "name='john'", "uid=10000") // UPDATE `user` SET `name`='john' WHERE `uid`=10000 r, err := db.Update(ctx, "user", "name=?", "uid=?", "john", 10000) ``` 注意,参数域支持并建议使用预处理模式(使用 `?` 占位符)进行输入,避免 `SQL` 注入风险。 # ORM时区处理 ## 基本介绍 这个问题由于大家问得比较多,因此单独开了一个章节详细介绍一下 `ORM` 中的时区处理是怎么一回事。我们这里以 `MySQL` 数据库为例来介绍时区转换的事情,本地时区我们设定为 `+8` 时区,数据库时区也是 `+8` 时区。 `MySQL` 数据库驱动用得最多的是这个第三方包: [https://github.com/go-sql-driver/mysql](https://github.com/go-sql-driver/mysql),在这个第三方包中有这么一个参数: 大概的意思是,当提交的时间参数为 `time.Time` 时,该参数用来转换参数时区的。当在连接数据库时,该参数传递 `loc=Local`,那么该 `driver` 将会自动将提交的 `time.Time` 参数转换为本地程序设置的时区,没有手动设置时,那么该时区为 `UTC` 时区。那么我们来看两个例子。 ## 转换示例 ### 示例1,设置 `loc=Local` **配置文件** ```yaml database: link: "mysql:root:12345678@tcp(127.0.0.1:3306)/test?loc=Local" ``` **代码示例** ```go t1, _ := time.Parse("2006-01-02 15:04:05", "2020-10-27 10:00:00") t2, _ := time.Parse("2006-01-02 15:04:05", "2020-10-27 11:00:00") db.Model("user").Ctx(ctx).Where("create_time>? and create_time'2020-10-27 18:00:00' AND create_time<'2020-10-27 19:00:00' ``` 这里由于通过 `time.Parse` 创建的 `time.Time` 时间对象是 `UTC` 时区,那么提交到数据库执行时将会被底层的 `driver` 修改为 `+8` 时区。 ```go t1, _ := time.ParseInLocation("2006-01-02 15:04:05", "2020-10-27 10:00:00", time.Local) t2, _ := time.ParseInLocation("2006-01-02 15:04:05", "2020-10-27 11:00:00", time.Local) db.Model("user").Ctx(ctx).Where("create_time>? and create_time'2020-10-27 10:00:00' AND create_time<'2020-10-27 11:00:00' ``` 这里由于通过 `time.ParseInLocation` 创建的 `time.Time` 时间对象是 `+8` 时区,和 `loc=Local` 的时区一致,那么提交到数据库执行时不会被底层的 `driver` 修改。 **注意** 注意在写入数据中包含 `time.Time` 参数时,也需要注意时区转换的问题。 ### 示例2,不设置 `loc` 参数 **配置文件** ```yaml database: link: "mysql:root:12345678@tcp(127.0.0.1:3306)/test" ``` **代码示例** ```go t1, _ := time.Parse("2006-01-02 15:04:05", "2020-10-27 10:00:00") t2, _ := time.Parse("2006-01-02 15:04:05", "2020-10-27 11:00:00") db.Model("user").Ctx(ctx).Where("create_time>? and create_time'2020-10-27 10:00:00' AND create_time<'2020-10-27 11:00:00' ``` 这里由于通过 `time.Parse` 创建的 `time.Time` 时间对象是 `UTC` 时区,那么提交到数据库执行时将不会被底层的 `driver` 修改。 ```go t1, _ := time.ParseInLocation("2006-01-02 15:04:05", "2020-10-27 10:00:00", time.Local) t2, _ := time.ParseInLocation("2006-01-02 15:04:05", "2020-10-27 11:00:00", time.Local) db.Model("user").Ctx(ctx).Where("create_time>? and create_time'2020-10-27 02:00:00' AND create_time<'2020-10-27 03:00:00' ``` 这里由于通过 `time.ParseInLocation` 创建的 `time.Time` 时间对象是 `+8` 时区,那么提交到数据库执行时会被底层的 `driver` 修改为 `UTC` 时区。 **注意** 注意在写入数据中包含 `time.Time` 参数时,也需要注意时区转换的问题。 ## 改进建议 建议在配置中统一加上 `locl` 配置,例如(MySQL): `loc=Local&parseTime=true`。以下是一个可供参考的配置: ```yaml database: logger: level: "all" stdout: true default: link: "mysql:root:12345678@tcp(192.168.1.10:3306)/mydb?loc=Local&parseTime=true" debug: true order: link: "mysql:root:12345678@tcp(192.168.1.20:3306)/order?loc=Local&parseTime=true" debug: true ``` # ORM模型生成 ## 模型自动生成 `GoFrame` 框架支持非常便捷的数据表模型生成功能,通过 `gf gen dao/model` 工具链命令实现,具体请参考 [开发工具](../cli.md) 章节。 # ORM上下文变量 `ORM` 支持传递自定义的 `context` 上下文变量,用于异步 `IO` 控制、上下文信息传递(特别是链路跟踪信息的传递)、以及嵌套事务支持。 我们可以通过 `Ctx` 方法传递自定义的上下文变量给 `ORM` 对象, `Ctx` 方法其实是一个链式操作方法,该上下文传递进去后仅对当前 `DB` 接口对象有效,方法定义如下: ```go func Ctx(ctx context.Context) DB ``` ## 请求超时控制 我们来看一个通过上下文变量控制请求超时时间的示例。 ```go ctx, cancel := context.WithTimeout(context.Background(), time.Second) defer cancel() _, err := db.Ctx(ctx).Query("SELECT SLEEP(10)") fmt.Println(err) ``` 该示例中执行会 `sleep 10` 秒中,因此必定会引发请求的超时。执行后,输出结果为: ```html context deadline exceeded, SELECT SLEEP(10) ``` ## 链路跟踪信息 上下文变量也可以传递链路跟踪信息,并且可以和日志组件结合,将链路信息打印到日志中(仅当 `ORM` 日志开启时),具体请参考链路跟踪专题介绍章节: [服务链路跟踪](../obs/tracing.md) ## 模型上下文操作 模型对象也支持上下文变量的传递,同样也是通过 `Ctx` 方法。我们来看一个简单的示例,我们将示例2的例子通过模型操作调整一下。 ```go package main import ( "github.com/gogf/gf/v2/frame/g" "github.com/gogf/gf/v2/os/gctx" ) func main() { _, err := g.DB().Model("user").Ctx(gctx.New()).All() if err != nil { panic(err) } } ``` 执行后,终端输出为: ```html 2020-12-28 23:46:56.349 [DEBU] {38d45cbf2743db16f1062074f7473e5c} [ 5 ms] [default] [rows:0 ] SHOW FULL COLUMNS FROM `user` 2020-12-28 23:46:56.354 [DEBU] {38d45cbf2743db16f1062074f7473e5c} [ 5 ms] [default] [rows:100] SELECT * FROM `user` ``` **提示** 其中 ``SHOW FULL COLUMNS FROM `user` `` 为 `ORM` 组件的数据表字段获取查询,每个表在执行操作之前仅会查询一次并缓存结果到内存中。 ## 嵌套事务支持 嵌套事务的支持依赖 `Context` 上下文变量的层级传递,具体请参考章节: [ORM事务处理](./gdb-transaction.md) # ORM常见问题 ## `ORM`是否可以直接执行SQL文件 1. 首先,`ORM`底层依赖的数据库`driver`从安全性考虑,通常默认不支持同时执行多条`SQL`语句。可以读取`SQL`文件内容,将内容拆分为单条`SQL`语句(通过`;`分隔符号分隔多条`SQL`语句),然后调用`ORM`的`Exec`方法来执行。 2. 其次,如果想要允许底层`driver`一次性执行多条`SQL`语句,可以参考底层`driver`的配置,比如`mysql`的配置,可以设置`multiStatements=true`(参考[mysql-driver配置](https://github.com/go-sql-driver/mysql?tab=readme-ov-file#multistatements)),这样`ORM`的`Exec`方法就可以执行多条`SQL`语句了。配置项示例: ```yaml database: default: link: "mysql:root:123456@tcp(127.0.0.1:3306)/test?charset=utf8mb4&parseTime=True&loc=Local&multiStatements=true" ``` ## `driver: bad connection` 如果数据库执行出现该错误,可能是由于本地数据库连接池的连接已经过期,可以检查一下客户端配置的 `MaxLifeTime` 配置是否超过数据库服务端设置的连接最大超时时间。更多客户端配置请参考章节: [ORM使用配置](./gdb-config.md) ## `update/insert` 操作不生效 使用 `orm` 时,配置文件中: ```toml dryRun = "(可选)ORM空跑(只读不写)" ``` 这行配置一定要删掉或者设置为0 否则出现 `update insert` 操作不生效的现象。 ## `cannot find database driver for specified database type "xxx", did you misspell type name "xxx" or forget importing the database driver?` 程序代码没有引入依赖的数据库驱动,需要注意从 `GoFrame v2.1` 版本开始,需要手动引入社区驱动,请参考: - [https://github.com/gogf/gf/tree/master/contrib/drivers](https://github.com/gogf/gf/tree/master/contrib/drivers) ## 数据库打开 `DEBUG` 日志后,查询的 `SQL` 语句中发现出现 `WHERE 0=1` 的语句 出现 `WHERE 0=1` 的情况是由于查询条件中存在数组条件,并且数组的长度为 `0`。这种情况 `ORM` 无法自动过滤这种空数组条件(这种条件过滤可能会引起业务异常),需要开发者根据业务场景,显示调用 `OmitEmpty` 或者 `OmitEmptyWhere` 来告诉 `ORM` 可以过滤这些空数组的条件。 ## MYSQL中的表情,用SQL查询后,乱码问题 解决办法: `config.toml` 文件 数据库配置的 `charset` 设置为 `utf8mb4` 默认是 `utf8` `MySQL` 存储表情时注意: - 数据库编码 `utf8mb4` - 表的编码是 `utf8mb4` - 表中内容字段是 `utf8mb4` ## 怎么让有UUID类型的数据库支持UUID处理 由于ORM类型转换在底层是使用的gconv实现的,所以我们可以自己添加新的converter就可以了。如以下示例,在程序初始化时调用就可以了。 ```golang gconv.RegisterTypeConverterFunc(func(src uuid.UUID) (val *string, err error) { v := src.String() return &v, nil }) gconv.RegisterTypeConverterFunc(func(src uuid.UUIDs) (val *[]string, err error) { v := src.Strings() return &v, nil }) gconv.RegisterTypeConverterFunc(func(src string) (val *uuid.UUID, err error) { var v uuid.UUID val = &v src = gstr.Trim(src) if src == "" { return } v, err = uuid.Parse(src) return }) ``` # ORM使用配置-配置文件 我们推荐使用配置组件来管理数据库配置,并使用 `g` 对象管理模块中的 `g.DB("数据库分组名称")` 方法获取数据库操作对象,数据库对象将会自动读取配置组件中的相应配置项,并自动初始化该数据库操作的单例对象。数据库配置管理功能使用的是配置管理组件实现(配置组件采用接口化设计默认使用文件系统实现),同样支持多种数据格式如: `toml/yaml/json/xml/ini/properties`。默认并且推荐的配置文件数据格式为 `yaml`。 ## 简单配置 **提示** 从 `v2.2.0` 版本开始,使用 `link` 进行数据库配置时,数据库组件统一了不同数据库类型的配置格式,以简化配置管理。 简化配置通过配置项 `link` 指定,格式如下: ```text type:username:password@protocol(address)[/dbname][?param1=value1&...¶mN=valueN] ``` 即: ```text 类型:账号:密码@协议(地址)/数据库名称?特性配置 ``` 其中: - **数据库名称** 及 **特性配置** 为非必须参数,其他参数为必须参数。 - **协议** 可选配置为: `tcp/udp/unix/file`,常见配置为 `tcp` - **特性配置** 根据不同的数据库类型,由其底层实现的第三方驱动定义,具体需要参考第三方驱动官网。例如,针对 `mysql` 驱动而言,使用的第三方驱动为: [https://github.com/go-sql-driver/mysql](https://github.com/go-sql-driver/mysql) 支持的特性配置如 `multiStatements` 和 `loc` 等。 示例: ```yaml database: default: link: "mysql:root:12345678@tcp(127.0.0.1:3306)/test" user: link: "sqlite::@file(/var/data/db.sqlite3)" local: link: "mysql:username:password@unix(/tmp/mysql.sock)/dbname" ``` 不同数据类型对应的 `link` 示例如下: | 类型 | link示例 | extra参数 | | --- | --- | --- | | `mysql` | ```mysql:root:12345678@tcp(127.0.0.1:3306)/test?loc=Local&parseTime=true``` | [mysql](https://github.com/go-sql-driver/mysql) | | `mariadb` | ```mariadb:root:12345678@tcp(127.0.0.1:3306)/test?loc=Local&parseTime=true``` | [mysql](https://github.com/go-sql-driver/mysql) | | `tidb` | ```tidb:root:12345678@tcp(127.0.0.1:3306)/test?loc=Local&parseTime=true``` | [mysql](https://github.com/go-sql-driver/mysql) | | `pgsql` | ```pgsql:root:12345678@tcp(127.0.0.1:5432)/test``` | [pq](https://github.com/lib/pq) | | `mssql` | ```mssql:root:12345678@tcp(127.0.0.1:1433)/test?encrypt=disable``` | [go-mssqldb](https://github.com/microsoft/go-mssqldb) | | `sqlite` | ```sqlite::@file(/var/data/db.sqlite3)``` | pure go: [go-sqlite](https://github.com/glebarez/go-sqlite)
    32bit-cgo: [go-sqlite3](https://github.com/mattn/go-sqlite3) | | `oracle` | ```oracle:root:12345678@tcp(127.0.0.1:5432)/test``` | [go-ora](https://github.com/sijms/go-ora) | | `clickhouse` | ```clickhouse:root:12345678@tcp(127.0.0.1:9000)/test``` | [clickhouse-go](https://github.com/ClickHouse/clickhouse-go) | | `dm` | ```dm:root:12345678@tcp(127.0.0.1:5236)/test``` | [dm](https://gitee.com/chunanyong/dm) | | `gaussdb` | ```gaussdb:root:12345678@tcp(127.0.0.1:5432)/test``` (`v2.10.0+`) | [openGauss-connector-go-pq](https://gitee.com/opengauss/openGauss-connector-go-pq) | **提示** 更多框架支持的数据库类型请参考: [https://github.com/gogf/gf/tree/master/contrib/drivers](https://github.com/gogf/gf/tree/master/contrib/drivers) ## 完整配置 完整的 `config.yaml` 数据库配置项的数据格式形如下: ```yaml database: default: # 分组名称,可自定义,默认为default host: "127.0.0.1" # 地址 port: "3306" # 端口 user: "root" # 账号 pass: "your_password" # 密码 name: "your_database" # 数据库名称 type: "mysql" # 数据库类型(如:mariadb/tidb/mysql/pgsql/mssql/sqlite/oracle/clickhouse/dm) link: "" # (可选)自定义数据库链接信息,当该字段被设置值时,以上链接字段(Host,Port,User,Pass,Name,Type)将失效 extra: "" # (可选)不同数据库的额外特性配置,由底层数据库driver定义,具体有哪些配置请查看具体的数据库driver介绍 role: "master" # (可选)数据库主从角色(master/slave),默认为master。如果不使用应用主从机制请不配置或留空即可。 debug: false # (可选)开启调试模式 prefix: "gf_" # (可选)表名前缀 dryRun: false # (可选)ORM空跑(只读不写) charset: "utf8" # (可选)数据库编码(如: utf8mb4/utf8/gbk/gb2312),一般设置为utf8mb4。默认为utf8。 protocol: "tcp" # (可选)数据库连接协议,默认为TCP weight: 100 # (可选)负载均衡权重,用于负载均衡控制,不使用应用层的负载均衡机制请置空 timezone: "Local" # (可选)时区配置,例如:Local namespace: "" # (可选)用以支持个别数据库服务Catalog&Schema区分的问题,原有的Schema代表数据库名称,而NameSpace代表个别数据库服务的Schema maxIdle: 10 # (可选)连接池最大闲置的连接数(默认10) maxOpen: 100 # (可选)连接池最大打开的连接数(默认无限制) maxLifetime: "30s" # (可选)连接对象可重复使用的时间长度(默认30秒) maxIdleConnTime: "30s" # (可选,v2.10新增)连接池中空闲连接的最大生存时间(默认30秒)。可以通过配置文件或SetConnMaxIdleTime方法设置,避免长时间空闲连接占用资源。 queryTimeout: "0" # (可选)查询语句超时时长(默认无限制,同时受ctx超时时间影响)。值为time.Parse支持的格式,如30s, 1m。 execTimeout: "0" # (可选)写入语句超时时长(默认无限制,同时受ctx超时时间影响)。值为time.Parse支持的格式,如30s, 1m。 tranTimeout: "0" # (可选)事务处理超时时长(默认无限制,同时受ctx超时时间影响)。值为time.Parse支持的格式,如30s, 1m。 prepareTimeout: "0" # (可选)预准备SQL语句执行超时时长(默认无限制,同时受ctx超时时间影响)。值为time.Parse支持的格式,如30s, 1m。 createdAt: "created_at" # (可选)自动创建时间字段名称 updatedAt: "updated_at" # (可选)自动更新时间字段名称 deletedAt: "deleted_at" # (可选)软删除时间字段名称 timeMaintainDisabled: false # (可选)是否完全关闭时间更新特性,为true时CreatedAt/UpdatedAt/DeletedAt都将失效 ``` **备注** 使用该配置方式时, **为保证数据库安全,默认底层不支持多行 `SQL` 语句执行**。为了得到更多配置项控制,请参考推荐的`link`简化配置,并了解清楚简化配置项中每个连接参数的功能作用,以及对应驱动的官方额外配置参数。 配置示例: ```yaml database: default: link: "mysql:root:12345678@tcp(127.0.0.1:3306)/test" extra: "charset=utf8mb4&parseTime=True&loc=Local" mysql: type: "mysql" host: "127.0.0.1" port: "3306" user: "root" pass: "12345678" name: "test" charset: "utf8mb4" timezone: "Local" maxIdle: "10" maxOpen: "100" maxLifetime: "30s" extra: "parseTime=True" ``` ## 集群模式 `gdb` 的配置支持集群模式,数据库配置中每一项分组配置均可以是多个节点,支持负载均衡权重策略,例如: ```yaml database: default: - link: "mysql:root:12345678@tcp(127.0.0.1:3306)/test" role: "master" - link: "mysql:root:12345678@tcp(127.0.0.1:3306)/test" role: "slave" user: - link: "mysql:root:12345678@tcp(127.0.0.1:3306)/user" role: "master" - link: "mysql:root:12345678@tcp(127.0.0.1:3306)/user" role: "slave" - link: "mysql:root:12345678@tcp(127.0.0.1:3306)/user" role: "slave" ``` 以上数据库配置示例中包含两个数据库分组 `default` 和 `user`,其中 `default` 分组包含一主一从, `user` 分组包含一主两从。在代码中可以通过 `g.DB()` 和 `g.DB("user")` 获取对应的数据库连接对象。 ## 日志配置 `gdb` 支持日志输出,内部使用的是 `glog.Logger` 对象实现日志管理,并且可以通过配置文件对日志对象进行配置。默认情况下 `gdb` 关闭了 `DEBUG` 日志输出,如果需要打开 `DEBUG` 信息需要将数据库的 `debug` 参数设置为 `true`。以下是为一个配置文件示例: ```yaml database: logger: path: "/var/log/gf-app/sql" level: "all" stdout: true default: link: "mysql:root:12345678@tcp(127.0.0.1:3306)/user_center" debug: true ``` 其中 `database.logger` 即为 `gdb` 的日志配置,这是一项特殊的配置项,当该配置不存在时,将会使用日志组件的默认配置, 具体请参考 [日志组件-配置管理](./glog-config.md) 章节。 **注意** 需要注意哦:由于 `ORM` 底层都是采用安全的预处理执行方式,提交到底层的 `SQL` 与参数其实是分开的,因此日志中记录的完整 `SQL` 仅作参考方便人工阅读,并不是真正提交到底层的 `SQL` 语句。 # ORM使用配置-配置方法 **提示** 以下为数据库底层管理配置介绍,如果对数据库的底层配置管理比较感兴趣,可继续阅读后续章节。 ## 数据结构 `gdb` 数据库管理模块的内部配置管理数据结构如下: `ConfigNode` 用于存储一个数据库节点信息; `ConfigGroup` 用于管理多个数据库节点组成的配置分组(一般一个分组对应一个业务数据库集群); `Config` 用于管理多个 `ConfigGroup` 配置分组。 **配置管理特点:** 1. 支持多节点数据库集群管理; 2. 每个节点可以单独配置连接属性; 3. 采用单例模式管理数据库实例化对象; 4. 支持对数据库集群分组管理,按照分组名称获取实例化的数据库操作对象; 5. 支持多种关系型数据库管理,可通过 `ConfigNode.Type` 属性进行配置; 6. 支持 `Master-Slave` 读写分离,可通过 `ConfigNode.Role` 属性进行配置; 7. 支持客户端的负载均衡管理,可通过 `ConfigNode.Weight` 属性进行配置,值越大,优先级越高; ```go type Config map[string]ConfigGroup // 数据库配置对象 type ConfigGroup []ConfigNode // 数据库分组配置 // 数据库配置项(一个分组配置对应多个配置项) type ConfigNode struct { Host string // 地址 Port string // 端口 User string // 账号 Pass string // 密码 Name string // 数据库名称 Type string // 数据库类型:mysql, sqlite, mssql, pgsql, oracle Link string // (可选)自定义链接信息,当该字段被设置值时,以上链接字段(Host,Port,User,Pass,Name)将失效(该字段是一个扩展功能) Extra string // (可选)不同数据库的额外特性配置,由底层数据库driver定义 Role string // (可选,默认为master)数据库的角色,用于主从操作分离,至少需要有一个master,参数值:master, slave Debug bool // (可选)开启调试模式 Charset string // (可选,默认为 utf8)编码,默认为 utf8 Prefix string // (可选)表名前缀 Weight int // (可选)用于负载均衡的权重计算,当集群中只有一个节点时,权重没有任何意义 MaxIdleConnCount int // (可选)连接池最大闲置的连接数 MaxOpenConnCount int // (可选)连接池最大打开的连接数 MaxConnLifetime time.Duration // (可选,单位秒)连接对象可重复使用的时间长度 } ``` 特别说明, `gdb` 的配置管理最大的 **特点** 是,(同一进程中)所有的数据库集群信息都使用同一个配置管理模块进行统一维护, **不同业务的数据库集群配置使用不同的分组名称** 进行配置和获取。 ## 配置方法 这是原生调用 `gdb` 模块来配置管理数据库。如果开发者想要自行控制数据库配置管理可以参考以下方法。若无需要可忽略该章节。 接口文档: [https://pkg.go.dev/github.com/gogf/gf/v2/database/gdb](https://pkg.go.dev/github.com/gogf/gf/v2/database/gdb) ```go // 添加一个数据库节点到指定的分组中 func AddConfigNode(group string, node ConfigNode) // 添加一个配置分组到数据库配置管理中(同名覆盖) func AddConfigGroup(group string, nodes ConfigGroup) // 添加一个数据库节点到默认的分组中(默认为default,可修改) func AddDefaultConfigNode(node ConfigNode) // 添加一个配置分组到数据库配置管理中(默认分组为default,可修改) func AddDefaultConfigGroup(nodes ConfigGroup) // 设置默认的分组名称,获取默认数据库对象时将会自动读取该分组配置 func SetDefaultGroup(groupName string) // 设置数据库配置为定义的配置信息,会将原有配置覆盖 func SetConfig(c Config) // 获取所有数据库配置信息(从版本v2.10.0开始支持) func GetAllConfig() Config ``` 默认分组表示,如果获取数据库对象时不指定配置分组名称,那么 `gdb` 默认读取的配置分组。例如: `gdb.NewByGroup()` 可获取一个默认分组的数据库对象。简单的做法,我们可以通过 `gdb` 包的 `SetConfig` 配置管理方法进行自定义的数据库全局配置,例如: ```go gdb.SetConfig(gdb.Config { "default" : gdb.ConfigGroup { gdb.ConfigNode { Host : "192.168.1.100", Port : "3306", User : "root", Pass : "123456", Name : "test", Type : "mysql", Role : "master", Weight : 100, }, gdb.ConfigNode { Host : "192.168.1.101", Port : "3306", User : "root", Pass : "123456", Name : "test", Type : "mysql", Role : "slave", Weight : 100, }, }, "user-center" : gdb.ConfigGroup { gdb.ConfigNode { Host : "192.168.1.110", Port : "3306", User : "root", Pass : "123456", Name : "test", Type : "mysql", Role : "master", Weight : 100, }, }, }) ``` 随后,我们可以使用 `gdb.NewByGroup("数据库分组名称")` 来获取一个数据库操作对象。该对象用于后续的数据库一系列方法/链式操作。 ## 获取所有配置 从版本`v2.10.0`开始,`gdb`提供了`GetAllConfig`方法用于获取所有数据库配置信息,方便进行配置验证、配置导出等业务操作。 ### 方法定义 ```go func GetAllConfig() Config ``` 该方法返回类型为`Config`,即`map[string]ConfigGroup`,包含了所有已注册的数据库分组配置。 ### 使用示例 ```go package main import ( "fmt" "github.com/gogf/gf/v2/database/gdb" ) func main() { // 获取所有数据库配置 allConfig := gdb.GetAllConfig() // 遍历所有配置分组 for groupName, configGroup := range allConfig { fmt.Printf("数据库分组: %s\n", groupName) // 遍历分组中的所有节点 for i, node := range configGroup { fmt.Printf(" 节点 %d:\n", i+1) fmt.Printf(" Host: %s\n", node.Host) fmt.Printf(" Port: %s\n", node.Port) fmt.Printf(" Name: %s\n", node.Name) fmt.Printf(" Type: %s\n", node.Type) fmt.Printf(" Role: %s\n", node.Role) } } } ``` ### 应用场景 `GetAllConfig`方法主要用于以下场景: 1. **配置验证**:在应用启动时验证所有数据库配置是否正确。 2. **配置导出**:将当前数据库配置导出到文件或其他存储介质。 3. **监控和诊断**:在运行时检查数据库配置,用于故障排查。 4. **配置管理界面**:为管理后台提供配置查看和编辑功能。 # ORM使用配置-常见问题 ## 如何实现数据库账号密码在配置文件中加密 在某些场景下,数据库的账号密码无法明文配置到配置文件中,需要进行一定的加密。在连接数据库的时候, 再对配置文件中加密的字段进行解密处理。这种需求可以通过自定义 `Driver` 来实现(关于 `Driver` 的详细介绍请参考章节: [ORM接口开发](./gdb-interface.md))。以 `mysql` 为例,我们可以自己编写一个 `Driver`,包裹框架社区组件中的 `mysql driver`,并且覆盖它的 `Open` 方法即可。 这里有一个完整的可运行示例代码:https://goframe.org/examples/database/encoded-pass # ORM链式操作 ## 基本介绍 `gdb` 链式操作使用方式简单灵活,是 `GoFrame` 框架官方推荐的数据库操作方式。链式操作可以通过数据库对象的 `db.Model` 方法或者事务对象的 `tx.Model` 方法,基于指定的数据表返回一个链式操作对象 `*Model`,该对象可以执行以下方法。当前方法列表可能滞后于源代码,详细的方法列表请参考接口文档: [https://pkg.go.dev/github.com/gogf/gf/v2/database/gdb#Model](https://pkg.go.dev/github.com/gogf/gf/v2/database/gdb#Model) ## 部分不支持的操作 以下是最新版本的支持情况 | 类型 | Replace | Save | InsertIgnore | InsertGetId | LastInsertId | Transaction | RowsAffected | | --- | --- | --- | --- | --- | --- | --- | --- | | `mysql` | 支持 | 支持 | 支持 | 支持 | 支持 | 支持 | 支持 | | `mariadb` | 支持 | 支持 | 支持 | 支持 | 支持 | 支持 | 支持 | | `tidb` | 支持 | 支持 | 支持 | 支持 | 支持 | 支持 | 支持 | | `pgsql` | 不支持 | 支持 | 不支持 | 支持 | 支持 | 支持 | 支持 | | `mssql` | 不支持 | 支持 | 支持 | 支持 | 不支持 | 支持 | 支持 | | `sqlite` | 不支持 | 支持 | 支持 | 支持 | 支持 | 支持 | 支持 | | `oracle` | 不支持 | 支持 | 支持 | 支持 | 不支持 | 支持 | 支持 | | `dm` | 不支持 | 支持 | 支持 | 支持 | 支持 | 支持 | 支持 | | `clickhouse` | 不支持 | 不支持 | 不支持 | 不支持 | 支持 | 不支持 | 不支持 | # ORM链式操作-模型创建 ## 模型创建 ### `Model` `Model` 方法用于创建基于数据表的 `Model` 对象。常见的,也可以使用 `g` 对象管理模块中的 `Model` 方法在默认的数据库配置上创建 `Model` 对象。 使用示例: ```go g.Model("user") // 或者 g.DB().Model("user") ``` 此外,在某些场景下,我们也可以通过 `DB` 方法切换当前模型的数据库对象,例如: ```go m := g.Model("user") m = m.DB(g.DB("order")) ``` 其效果与以下操作是一样的: ```go m := g.DB("order").Model("user") ``` ### `Raw` `Raw` 方法用于创建一个基于原始 `SQL` 语句的 `Model` 对象。也可以使用 `g` 对象管理模块中的 `ModelRaw` 方法通过给定的 `SQL` 语句在默认的数据库配置上创建 `Model` 对象。 ```go s := "SELECT * FROM `user`" m, _ := g.ModelRaw(s).WhereLT("age", 18).Limit(10).OrderAsc("id").All() // SELECT * FROM `user` WHERE `age`<18 ORDER BY `id` ASC LIMIT 10 ``` ```go s := "SELECT * FROM `user` WHERE `status` IN(?)" m, _ := g.ModelRaw(s, g.Slice{1,2,3}).WhereLT("age", 18).Limit(10).OrderAsc("id").All() // SELECT * FROM `user` WHERE `status` IN(1,2,3) AND `age`<18 ORDER BY `id` ASC LIMIT 10 ``` ## 链式安全 `链式安全` 只是模型操作的两种方式区别:一种会修改当前 `model` 对象(不安全,默认),一种不会(安全)但是模型属性修改/条件叠加需要使用赋值操作,仅此而已。 ### 默认情况 在默认情况下, `gdb` 是 `非链式安全` 的,也就是说链式操作的每一个方法都将对当前操作的 `Model` 属性进行修改,因此该 `Model` 对象 **不可以重复使用**。例如,当存在多个分开查询的条件时,我们可以这么来使用 `Model` 对象: ```go user := g.Model("user") user.Where("status", g.Slice{1,2,3}) if vip { // 查询条件自动叠加,修改当前模型对象 user.Where("money>=?", 1000000) } else { // 查询条件自动叠加,修改当前模型对象 user.Where("money= 1000000 // !vip: SELECT * FROM user WHERE status IN(1,2,3) AND money < 1000000 r, err := user.All() // vip: SELECT COUNT(1) FROM user WHERE status IN(1,2,3) AND money >= 1000000 // !vip: SELECT COUNT(1) FROM user WHERE status IN(1,2,3) AND money < 1000000 n, err := user.Count() ``` 可以看到,如果是分开执行链式操作,链式的每一个操作都会修改已有的 `Model` 对象,查询条件会自动叠加,因此 `user` 对象不可重复使用,否则条件会不停叠加。并且在这种使用方式中,每次我们需要操作 `user` 用户表,都得使用 `g.DB().Model("user")` 这样的语法创建一个新的 `user` 模型对象,相对来说会比较繁琐。 默认情况下,基于性能以及GC优化考虑,模型对象为 `非链式安全`,防止产生过多的临时模型对象。 **提示** 不过需要注意的是,如果使用的是cli工具 `gen dao` 生成的 `dao`,如 `user := dao.User.Ctx(ctx)`,此时获取到的 `user` `Model` 对象默认是链式安全的(已自动调用过 `.Safe()`)。 ### `Clone` 方法 此外,我们也可以手动调动 `Clone` 方法克隆当前模型,创建一个新的模型来实现链式安全,由于是新的模型对象,因此并不担心会修改已有的模型对象的问题。例如: ```go // 定义一个用户模型单例 user := g.Model("user") ``` ```go // 克隆一个新的用户模型 m := user.Clone() m.Where("status", g.Slice{1,2,3}) if vip { m.Where("money>=?", 1000000) } else { m.Where("money= 1000000 // !vip: SELECT * FROM user WHERE status IN(1,2,3) AND money < 1000000 r, err := m.All() // vip: SELECT COUNT(1) FROM user WHERE status IN(1,2,3) AND money >= 1000000 // !vip: SELECT COUNT(1) FROM user WHERE status IN(1,2,3) AND money < 1000000 n, err := m.Count() ``` ### `Safe` 方法 当然,我们可以通过 `Safe` 方法设置当前模型为 `链式安全` 的对象,后续的每一个链式操作都将返回一个新的 `Model` 对象,该 `Model` 对象可重复使用。但需要特别注意的是,模型属性的修改,或者操作条件的叠加,需要通过变量赋值的方式( `m = m.xxx`)覆盖原有的模型对象来实现。例如: ```go // 定义一个用户模型单例 user := g.Model("user").Safe() ``` ```go m := user.Where("status", g.Slice{1,2,3}) if vip { // 查询条件通过赋值叠加 m = m.Where("money>=?", 1000000) } else { // 查询条件通过赋值叠加 m = m.Where("money= 1000000 // !vip: SELECT * FROM user WHERE status IN(1,2,3) AND money < 1000000 r, err := m.All() // vip: SELECT COUNT(1) FROM user WHERE status IN(1,2,3) AND money >= 1000000 // !vip: SELECT COUNT(1) FROM user WHERE status IN(1,2,3) AND money < 1000000 n, err := m.Count() ``` 可以看到,示例中的用户模型单例对象 `user` 可以重复使用,而不用担心被「污染」的问题。在这种链式安全的方式下,我们可以创建一个用户单例对象 `user`,并且可以重复使用到后续的各种查询中。但是存在多个查询条件时,条件的叠加需要通过模型赋值操作( `m = m.xxx`)来实现。 **提示** 使用 `Safe` 方法标记之后,每一个链式操作都将会创建一个新的临时模型对象(内部自动使用 `Clone` 实现模型克隆),从而实现链式安全。这种使用方式在模型操作中比较常见。 # ORM链式操作-写入保存 ## 常用方法 ### `Insert/Replace/Save` 这几个链式操作方法用于数据的写入,并且支持自动的单条或者批量的数据写入,区别如下: 1. `Insert` 使用 `INSERT INTO` 语句进行数据库写入,如果写入的数据中存在主键或者唯一索引时,返回失败,否则写入一条新数据。 2. `Replace` 使用 `REPLACE INTO` 语句进行数据库写入,如果写入的数据中存在主键或者唯一索引时,会删除原有的记录,必定会写入一条新记录。 3. `Save` 使用 `INSERT INTO` 语句进行数据库写入,如果写入的数据中存在主键或者唯一索引时,更新原有数据,否则写入一条新数据。对于部分数据库,例如 `PgSQL, SQL server, Oracle` 可以使用 `OnConflict` 方法指定冲突键。 ```go db.Model(table).Data(g.Map{ "id": 1, "passport": "p1", "password": "pw1", }).OnConflict("id").Save() ``` > 在部分数据库类型中,并不支持 `Replace/Save` 方法,具体请参考 [链式操作](./gdb-chaining.md) 介绍章节。 这几个方法需要结合 `Data` 方法使用,该方法用于传递数据参数,用于数据写入/更新等写操作。 ### `InsertIgnore` 用于写入数据时如果写入的数据中存在主键或者唯一索引时,忽略错误继续执行写入。该方法定义如下: ```go func (m *Model) InsertIgnore(data ...interface{}) (result sql.Result, err error) ``` ### `InsertAndGetId` 用于写入数据时并直接返回自增字段的 `ID`。该方法定义如下: ```go func (m *Model) InsertAndGetId(data ...interface{}) (lastInsertId int64, err error) ``` ### `OnDuplicate/OnDuplicateEx` `OnDuplicate/OnDuplicateEx` 方法需要结合 `Save` 方法一起使用,用于指定 `Save` 方法的更新/不更新字段,参数为字符串、字符串数组、 `Map`。例如: ```go OnDuplicate("nickname, age") OnDuplicate("nickname", "age") OnDuplicate(g.Map{ "nickname": gdb.Raw("CONCAT('name_', VALUES(`nickname`))"), }) OnDuplicate(g.Map{ "nickname": "passport", }) ``` 其中 `OnDuplicateEx` 用于排除指定忽略更新的字段,排除的字段需要在写入的数据集合中。 #### 使用 `gdb.Counter` 实现字段自增 在使用 `OnDuplicate` 方法时,可以结合 `gdb.Counter` 类型来实现字段的自增操作。`gdb.Counter` 类型包含两个属性:`Field` 和 `Value`,分别表示要自增的字段名和自增的值。 ```go _, err := db.Model("user").OnConflict("id").OnDuplicate(g.Map{ "count": gdb.Counter{Field: "count", Value: 1}, // count = count + 1 "amount": gdb.Counter{Field: "amount", Value: 9.99}, // amount = amount + 9.99 }).Data(g.Map{ "id": 1, "name": "john", "count": 0, "amount": 0, }).Save() ``` 上述示例中,当插入数据时遇到 `id` 冲突,会将现有记录的 `count` 字段值增加 `1`,将 `amount` 字段值增加 `9.99`。这在需要统计计数或累加金额等场景中非常有用。 ## 使用示例 ### 示例1,基本使用 数据写入/保存方法需要结合 `Data` 方法使用,方法的参数类型可以为 `Map/Struct/Slice`: ```go // INSERT INTO `user`(`name`) VALUES('john') g.Model("user").Data(g.Map{"name": "john"}).Insert() // INSERT IGNORE INTO `user`(`uid`,`name`) VALUES(10000,'john') g.Model("user").Data(g.Map{"uid": 10000, "name": "john"}).InsertIgnore() // REPLACE INTO `user`(`uid`,`name`) VALUES(10000,'john') g.Model("user").Data(g.Map{"uid": 10000, "name": "john"}).Replace() // INSERT INTO `user`(`uid`,`name`) VALUES(10001,'john') ON DUPLICATE KEY UPDATE `uid`=VALUES(`uid`),`name`=VALUES(`name`) g.Model("user").Data(g.Map{"uid": 10001, "name": "john"}).Save() ``` 也可以不使用 `Data` 方法,而给写入/保存方法直接传递数据参数: ```go g.Model("user").Insert(g.Map{"name": "john"}) g.Model("user").Replace(g.Map{"uid": 10000, "name": "john"}) g.Model("user").Save(g.Map{"uid": 10001, "name": "john"}) ``` 数据参数也常用 `struct` 类型,例如当表字段为 `uid/name/site` 时: ```go type User struct { Uid int `orm:"uid"` Name string `orm:"name"` Site string `orm:"site"` } user := &User{ Uid: 1, Name: "john", Site: "https://goframe.org", } // INSERT INTO `user`(`uid`,`name`,`site`) VALUES(1,'john','https://goframe.org') g.Model("user").Data(user).Insert() ``` ### 示例2,数据批量写入 通过给 `Data` 方法输入 `Slice` 数组类型的参数,用以实现批量写入。数组元素需要为 `Map` 或者 `Struct` 类型,以便于数据库组件自动获取字段信息并生成批量操作 `SQL`。 ```go // INSERT INTO `user`(`name`) VALUES('john_1'),('john_2'),('john_3') g.Model("user").Data(g.List{ {"name": "john_1"}, {"name": "john_2"}, {"name": "john_3"}, }).Insert() ``` 可以通过 `Batch` 方法指定批量操作中分批写入条数数量(默认是 `10`),以下示例将会被拆分为两条写入请求: ```go // INSERT INTO `user`(`name`) VALUES('john_1'),('john_2') // INSERT INTO `user`(`name`) VALUES('john_3') g.Model("user").Data(g.List{ {"name": "john_1"}, {"name": "john_2"}, {"name": "john_3"}, }).Batch(2).Insert() ``` ### 示例3,数据批量保存 批量保存操作与单条保存操作原理是一样的,当写入的数据中存在主键或者唯一索引时将会更新原有记录值,否则新写入一条记录。 > `oracle, dm, mssql` 不支持批量保存。 ```go // INSERT INTO `user`(`uid`,`name`) VALUES(10000,'john_1'),(10001,'john_2'),(10002,'john_3') // ON DUPLICATE KEY UPDATE `uid`=VALUES(`uid`),`name`=VALUES(`name`) g.Model("user").Data(g.List{ {"uid":10000, "name": "john_1"}, {"uid":10001, "name": "john_2"}, {"uid":10002, "name": "john_3"}, }).Save() ``` ## `RawSQL` 语句嵌入 `gdb.Raw` 是字符串类型,该类型的参数将会直接作为 `SQL` 片段嵌入到提交到底层的 `SQL` 语句中,不会被自动转换为字符串参数类型、也不会被当做预处理参数。更详细的介绍请参考章节: [ORM高级特性-RawSQL](./gdb-senior-raw-sql.md)。例如: ```go // INSERT INTO `user`(`id`,`passport`,`password`,`nickname`,`create_time`) VALUES('id+2','john','123456','now()') g.Model("user").Data(g.Map{ "id": "id+2", "passport": "john", "password": "123456", "nickname": "JohnGuo", "create_time": "now()", }).Insert() // 执行报错:Error Code: 1136. Column count doesn't match value count at row 1 ``` 使用 `gdb.Raw` 改造后: ```go // INSERT INTO `user`(`id`,`passport`,`password`,`nickname`,`create_time`) VALUES(id+2,'john','123456',now()) g.Model("user").Data(g.Map{ "id": gdb.Raw("id+2"), "passport": "john", "password": "123456", "nickname": "JohnGuo", "create_time": gdb.Raw("now()"), }).Insert() ``` # ORM链式操作-更新删除 **注意** 为安全性保证、防止误操作, `Update` 及 `Delete` 方法必须带有 `Where` 条件才能提交执行,否则将会错误返回,错误信息如: `there should be WHERE condition statement for XXX operation`。 `goframe` 是一款用于企业生产级别的框架,各个模块设计严谨,工程实践的细节处理得比较好。 ## `Update` 更新方法 `Update` 用于数据的更新,往往需要结合 `Data` 及 `Where` 方法共同使用。 `Data` 方法用于指定需要更新的数据, `Where` 方法用于指定更新的条件范围。同时, `Update` 方法也支持直接给定数据和条件参数。 使用示例: ```go // UPDATE `user` SET `name`='john guo' WHERE name='john' g.Model("user").Data(g.Map{"name" : "john guo"}).Where("name", "john").Update() g.Model("user").Data("name='john guo'").Where("name", "john").Update() // UPDATE `user` SET `status`=1 WHERE `status`=0 ORDER BY `login_time` asc LIMIT 10 g.Model("user").Data("status", 1).Order("login_time asc").Where("status", 0).Limit(10).Update() // UPDATE `user` SET `status`=1 WHERE 1 g.Model("user").Data("status=1").Where(1).Update() g.Model("user").Data("status", 1).Where(1).Update() g.Model("user").Data(g.Map{"status" : 1}).Where(1).Update() ``` 也可以直接给 `Update` 方法传递 `data` 及 `where` 参数: ```go // UPDATE `user` SET `name`='john guo' WHERE name='john' g.Model("user").Update(g.Map{"name" : "john guo"}, "name", "john") g.Model("user").Update("name='john guo'", "name", "john") // UPDATE `user` SET `status`=1 WHERE 1 g.Model("user").Update("status=1", 1) g.Model("user").Update(g.Map{"status" : 1}, 1) ``` ## `Counter` 更新特性 可以使用 `Counter` 类型参数对特定的字段进行数值操作,例如:增加、减少操作。 `Counter` 数据结构定义: ```go // Counter is the type for update count. type Counter struct { Field string Value float64 } ``` `Counter` 使用示例,字段自增: ```go updateData := g.Map{ "views": &gdb.Counter{ Field: "views", Value: 1, }, } // UPDATE `article` SET `views`=`views`+1 WHERE `id`=1 result, err := db.Update("article", updateData, "id", 1) ``` `Counter` 也可以实现非自身字段的自增,例如: ```go updateData := g.Map{ "views": &gdb.Counter{ Field: "clicks", Value: 1, }, } // UPDATE `article` SET `views`=`clicks`+1 WHERE `id`=1 result, err := db.Update("article", updateData, "id", 1) ``` ## `Increment/Decrement` 自增/减 我们可以通过 `Increment` 和 `Decrement` 方法实现对指定字段的自增/自减常用操作。两个方法的定义如下: ```go // Increment increments a column's value by a given amount. func (m *Model) Increment(column string, amount float64) (sql.Result, error) // Decrement decrements a column's value by a given amount. func (m *Model) Decrement(column string, amount float64) (sql.Result, error) ``` 使用示例: ```go // UPDATE `article` SET `views`=`views`+10000 WHERE `id`=1 g.Model("article").Where("id", 1).Increment("views", 10000) // UPDATE `article` SET `views`=`views`-10000 WHERE `id`=1 g.Model("article").Where("id", 1).Decrement("views", 10000) ``` ## `RawSQL` 语句嵌入 `gdb.Raw` 是字符串类型,该类型的参数将会直接作为 `SQL` 片段嵌入到提交到底层的 `SQL` 语句中,不会被自动转换为字符串参数类型、也不会被当做预处理参数。更详细的介绍请参考章节: [ORM高级特性-RawSQL](./gdb-senior-raw-sql.md)。例如: ```go // UPDATE `user` SET login_count='login_count+1',update_time='now()' WHERE id=1 g.Model("user").Data(g.Map{ "login_count": "login_count+1", "update_time": "now()", }).Where("id", 1).Update() // 执行报错:Error Code: 1136. Column count doesn't match value count at row 1 ``` 使用 `gdb.Raw` 改造后: ```go // UPDATE `user` SET login_count=login_count+1,update_time=now() WHERE id=1 g.Model("user").Data(g.Map{ "login_count": gdb.Raw("login_count+1"), "update_time": gdb.Raw("now()"), }).Where("id", 1).Update() ``` ## `Delete` 删除方法 `Delete` 方法用于数据的删除。 使用示例: ```go // DELETE FROM `user` WHERE uid=10 g.Model("user").Where("uid", 10).Delete() // DELETE FROM `user` ORDER BY `login_time` asc LIMIT 10 g.Model("user").Order("login_time asc").Limit(10).Delete() ``` 也可以直接给 `Delete` 方法传递 `where` 参数: ```go // DELETE FROM `user` WHERE `uid`=10 g.Model("user").Delete("uid", 10) // DELETE FROM `user` WHERE `score`<60 g.Model("user").Delete("score < ", 60) ``` ## 软删除特性 软删除特性请查看章节: [ORM链式操作-时间维护](./gdb-chaining-soft-time.md) # ORM链式操作-对象输入 `Data/Where/WherePri/And/Or` 方法支持任意的 `string/map/slice/struct/*struct` 数据类型参数,该特性为 `gdb` 提供了很高的灵活性。当使用 `struct`/ `*struct` 对象作为输入参数时,将会被自动解析为 `map` 类型,只有 `struct` 的 **公开属性** 能够被转换,并且支持 `orm`/ `gconv`/ `json` 标签,用于定义转换后的键名,即与表字段的映射关系。 例如: ```go type User struct { Uid int `orm:"user_id"` Name string `orm:"user_name"` NickName string `orm:"nick_name"` } // 或者 type User struct { Uid int `gconv:"user_id"` Name string `gconv:"user_name"` NickName string `gconv:"nick_name"` } // 或者 type User struct { Uid int `json:"user_id"` Name string `json:"user_name"` NickName string `json:"nick_name"` } ``` 其中, `struct` 的属性应该是公开属性(首字母大写), `orm` 标签对应的是数据表的字段名称。表字段的对应关系标签既可以使用 `orm`,也可以用 `gconv`,还可以使用传统的 `json` 标签,但是当三种标签都存在时, `orm` 标签的优先级更高。为避免将 `struct` 对象转换为 `JSON` 数据格式返回时与 `JSON` 编码标签冲突,推荐使用 `orm` 标签来实现数据库 `ORM` 的映射关系。更详细的转换规则请查看 [类型转换-Map转换](./gconv-map.md) 章节。 # ORM链式操作-字段过滤 ## `Fields/FieldsEx` 字段过滤 1. `Fields` 用于指定需要操作的表字段,包括查询字段、写入字段、更新字段等过滤; 2. `FieldsEx` 用于例外的字段指定,可用于查询字段、写入字段、更新字段等过滤; ### `Fields` 示例 1. 假如 `user` 表有4个字段 `uid`, `nickname`, `passport`, `password`。 2. 查询字段过滤 ```go // SELECT `uid`,`nickname` FROM `user` ORDER BY `uid` asc g.Model("user").Fields("uid, nickname").Order("uid asc").All() ``` 3. 写入字段过滤 ```go m := g.Map{ "uid" : 10000, "nickname" : "John Guo", "passport" : "john", "password" : "123456", } g.Model(table).Fields("nickname,passport,password").Data(m).Insert() // INSERT INTO `user`(`nickname`,`passport`,`password`) VALUES('John Guo','john','123456') ``` 4. 支持`gdb.Raw`输入 ```go // SELECT 1 FROM `user` WHERE `id`=10 g.Model("user").Fields(gdb.Raw("1")).Where("id", 10).Value() ``` ### `FieldsEx` 示例 1. 假如 `user` 表有4个字段 `uid`, `nickname`, `passport`, `password`。 2. 查询字段排除 ```go // SELECT `uid`,`nickname` FROM `user` g.Model("user").FieldsEx("passport, password").All() ``` 1. 写入字段排除 ```go m := g.Map{ "uid" : 10000, "nickname" : "John Guo", "passport" : "john", "password" : "123456", } g.Model(table).FieldsEx("uid").Data(m).Insert() // INSERT INTO `user`(`nickname`,`passport`,`password`) VALUES('John Guo','john','123456') ``` ## `OmitEmpty` 空值过滤 当 `map`/ `struct` 中存在空值如 `nil`, `""`, `0` 时,默认情况下, `gdb` 将会将其当做正常的输入参数,因此这些参数也会被更新到数据表。 `OmitEmpty` 特性可以在将数据写入到数据库之前过滤空值数据的字段。 相关方法: ```go func (m *Model) OmitEmpty() *Model func (m *Model) OmitEmptyWhere() *Model func (m *Model) OmitEmptyData() *Model ``` `OmitEmpty` 方法会同时过滤 `Where` 及 `Data` 中的空值数据,而通过 `OmitEmptyWhere/OmitEmptyData` 方法可以执行特定的字段过滤。 ### 写入/更新操作 空值会影响于写入/更新操作方法,如 `Insert`, `Replace`, `Update`, `Save` 操作。如以下操作(以 `map` 为例, `struct` 同理): ```go // UPDATE `user` SET `name`='john',update_time=null WHERE `id`=1 g.Model("user").Data(g.Map{ "name" : "john", "update_time" : nil, }).Where("id", 1).Update() ``` 针对空值情况,我们可以通过 `OmitEmpty` 方法来过滤掉这些空值。例如,以上示例可以修改为: ```go // UPDATE `user` SET `name`='john' WHERE `id`=1 g.Model("user").OmitEmpty().Data(g.Map{ "name" : "john", "update_time" : nil, }).Where("id", 1).Update() ``` 对于 `struct` 数据参数,我们也可以进行空值过滤。操作示例: ```go type User struct { Id int `orm:"id"` Passport string `orm:"passport"` Password string `orm:"password"` NickName string `orm:"nickname"` CreateTime string `orm:"create_time"` UpdateTime string `orm:"update_time"` } user := User{ Id : 1, NickName : "john", UpdateTime: gtime.Now().String(), } g.Model("user").OmitEmpty().Data(user).Insert() // INSERT INTO `user`(`id`,`nickname`,`update_time`) VALUES(1,'john','2019-10-01 12:00:00') ``` **注意** 注意哟,批量写入/更新操作中 `OmitEmpty` 方法将会失效,因为在批量操作中,必须保证每个写入记录的字段是统一的。 关于 `omitempty` 标签与 `OmitEmpty` 方法: 1. 针对于 `struct` 的空值过滤大家会想到 `omitempty` 的标签。该标签常用于 `json` 转换的空值过滤,也在某一些第三方的 `ORM` 库中用作 `struct` 到数据表字段的空值过滤,即当属性为空值时不做转换。 2. `omitempty` 标签与 `OmitEmpty` 方法所达到的效果是一样的。在 `ORM` 操作中,我们不建议对 `struct` 使用 `omitempty` 的标签来控制字段的空值过滤,而建议使用 `OmitEmpty` 方法来做控制。因为该标签一旦加上之后便绑定到了 `struct` 上,没有办法做灵活控制;而通过 `OmitEmpty` 方法使得开发者可以选择性地、根据业务场景对 `struct` 做空值过滤,操作更加灵活。 ### 数据查询操作 空值也会影响数据查询操作,主要是影响 `where` 条件参数。我们可以通过 `OmitEmpty` 方法过滤条件参数中的空值。 使用示例: ```go // SELECT * FROM `user` WHERE `passport`='john' LIMIT 1 r, err := g.Model("user").Where(g.Map{ "nickname" : "", "passport" : "john", }).OmitEmpty().One() ``` ```go type User struct { Id int `orm:"id"` Passport string `orm:"passport"` Password string `orm:"password"` NickName string `orm:"nickname"` CreateTime string `orm:"create_time"` UpdateTime string `orm:"update_time"` } user := User{ Passport : "john", } r, err := g.Model("user").OmitEmpty().Where(user).One() // SELECT * FROM `user` WHERE `passport`='john' LIMIT 1 ``` ## `OmitNil` 空值过滤 ### 基本介绍 当 `map`/ `struct` 中存在空值如 `nil` 时,默认情况下, `gdb` 将会将其当做正常的输入参数,因此这些参数也会被更新到数据表。 `OmitNil` 特性可以在将数据写入到数据库之前过滤空值数据的字段。与 `OmitEmpty` 特性的区别在于, `OmitNil` 只会过滤值为 `nil` 的空值字段,其他空值如 `""`, `0` 并不会被过滤。 相关方法: ```go func (m *Model) OmitNil() *Model func (m *Model) OmitNilWhere() *Model func (m *Model) OmitNilData() *Model ``` `OmitEmpty` 方法会同时过滤 `Where` 及 `Data` 中的空值数据,而通过 `OmitEmptyWhere/OmitEmptyData` 方法可以执行特定的字段过滤。 ### 使用 `do` 对象进行字段过滤 如果使用 `GoFrame` 工程目录,通过 `gf gen dao` 或者 `make dao` 指令会自动根据配置的数据库生成对应的数据表 `dao/entity/do` 文件,如果在数据库操作中使用 `do` 对象,那么将会自动过滤未赋值的字段。例如: 生成的 `do` 对象结构体定义 ```go // User is the golang structure of table user for DAO operations like Where/Data. type User struct { g.Meta `orm:"table:user, do:true"` Id interface{} // User ID Passport interface{} // User Passport Password interface{} // User Password Nickname interface{} // User Nickname CreateAt *gtime.Time // Created Time UpdateAt *gtime.Time // Updated Time } ``` 数据写入: ```go dao.User.Transaction(ctx, func(ctx context.Context, tx gdb.TX) error { _, err = dao.User.Ctx(ctx).Data(do.User{ Passport: in.Passport, Password: in.Password, Nickname: in.Nickname, }).Insert() return err }) ``` 数据查询: ```go var user *entity.User err = dao.User.Ctx(ctx).Where(do.User{ Passport: in.Passport, Password: in.Password, }).Scan(&user) ``` ## `Filter` 字段过滤(已内置) ~~`gdb` 可以自动同步 **数据表结构** 到程序缓存中(缓存不过期,直至程序重启/重新部署),并且可以过滤提交参数中不符合表结构的数据项,该特性可以使用 `Filter` 方法实现。常用于新增/删除操作中输入 `map/struct/[]map/[]string` 参数类型的场景。~~ ~~使用示例,假如 `user` 表有4个字段 `uid`, `nickname`, `passport`, `password`:~~ ```go r, err := g.Model("user").Filter().Data(g.Map{ "id" : 1, "uid" : 1, "passport" : "john", "password" : "123456", }).Insert() // INSERT INTO user(uid,passport,password) VALUES(1, "john", "123456") ``` ~~其中 `id` 为不存在的字段,在写入数据时将会被过滤掉,不至于被构造成写入SQL中产生执行错误。~~ **提示** ~~数据库没有设计为 `Data` 方法做自动过滤,而是需要开发者调用 `Filter` 方法来手动指定过滤,目的是友好地提醒开发者可能误写/传递错误了字段名称。如果强制性的自动过滤可能会引起难以预料的业务逻辑异常,例如,由于字段名称拼写错误导致自动过滤了本来需要输入的字段,导致写入数据库的数据不完整。~~ **注意** 从 `GoFrame v1.15.7` 版本开始,根据社区整体反馈,为提高组件易用性, `filter` 特性被设置为默认开启,不再需要显示调用, `Filter` 方法已被标记废弃。 # ORM链式操作-字段获取 ## `GetFieldsStr/GetFieldsExStr` 字段获取 1. `GetFieldsStr` 用于获取指定表的字段,并可给定字段前缀,字段之间使用" `,`"符号连接成字符串返回; 2. `GetFieldsExStr` 用于获取指定表中例外的字段,并可给定字段前缀,字段之间使用" `,`"符号连接成字符串返回; ### `GetFieldsStr` 示例 1. 假如 `user` 表有4个字段 `uid`, `nickname`, `passport`, `password`。 2. 查询字段 ```go // uid,nickname,passport,password g.Model("user").GetFieldsStr() ``` 3. 查询字段给指定前缀 ```go // gf_uid,gf_nickname,gf_passport,gf_password g.Model("user").GetFieldsStr("gf_") ``` ### `GetFieldsExStr` 示例 1. 假如 `user` 表有4个字段 `uid`, `nickname`, `passport`, `password`。 2. 查询字段排除 ```go // uid,nickname g.Model("user").GetFieldsExStr("passport, password") ``` 3. 查询字段排除并给定前缀 ```go // gf_uid,gf_nickname g.Model("user").GetFieldsExStr("passport, password", "gf_") ``` # ORM链式操作-事务处理 `Model` 对象也可以通过 `TX` 事务接口创建,通过事务对象创建的 `Model` 对象与通过 `DB` 数据库对象创建的 `Model` 对象功能是一样的,只不过前者的所有操作都是基于事务,而当事务提交或者回滚后,对应的 `Model` 对象不能被继续使用,否则会返回错误。因为该 `TX` 接口不能被继续使用,一个事务对象仅对应于一个事务流程, `Commit`/ `Rollback` 后即结束。 本章节仅对链式操作涉及到的事务处理方法做简单介绍,更详细的介绍请参考 [ORM事务处理](./gdb-transaction.md) 章节。 ## 示例1,通过 `Transaction` 为方便事务操作, `gdb` 提供了事务的闭包操作,通过 `Transaction` 方法实现,该方法定义如下: ```go func (db DB) Transaction(ctx context.Context, f func(ctx context.Context, tx TX) error) (err error) ``` 当给定的闭包方法返回的 `error` 为 `nil` 时,那么闭包执行结束后当前事务自动执行 `Commit` 提交操作;否则自动执行 `Rollback` 回滚操作。 **提示** 如果闭包内部操作产生 `panic` 中断,该事务也将进行回滚。 ```go func Register() error { return g.DB().Transaction(ctx, func(ctx context.Context, tx gdb.TX) error { var ( result sql.Result err error ) // 写入用户基础数据 result, err = tx.Table("user").Insert(g.Map{ "name": "john", "score": 100, //... }) if err != nil { return err } // 写入用户详情数据,需要用到上一次写入得到的用户uid result, err = tx.Table("user_detail").Insert(g.Map{ "uid": result.LastInsertId(), "phone": "18010576258", //... }) return err }) } ``` ## 示例2,通过 `TX` 链式操作 我们也可以在链式操作中通过 `TX` 方法切换绑定的事务对象。多次链式操作可以绑定同一个事务对象,在该事务对象中执行对应的链式操作。 ```go func Register() error { var ( uid int64 err error ) tx, err := g.DB().Begin() if err != nil { return err } // 方法退出时检验返回值, // 如果结果成功则执行tx.Commit()提交, // 否则执行tx.Rollback()回滚操作。 defer func() { if err != nil { tx.Rollback() } else { tx.Commit() } }() // 写入用户基础数据 uid, err = AddUserInfo(tx, g.Map{ "name": "john", "score": 100, //... }) if err != nil { return err } // 写入用户详情数据,需要用到上一次写入得到的用户uid err = AddUserDetail(tx, g.Map{ "uid": uid, "phone": "18010576259", //... }) return err } func AddUserInfo(tx gdb.TX, data g.Map) (int64, error) { result, err := g.Model("user").TX(tx).Data(data).Insert() if err != nil { return 0, err } uid, err := result.LastInsertId() if err != nil { return 0, err } return uid, nil } func AddUserDetail(tx gdb.TX, data g.Map) error { _, err := g.Model("user_detail").TX(tx).Data(data).Insert() return err } ``` # ORM链式操作-主从切换 从之前的介绍我们知道 `gdb` 支持基于应用层的主从配置以及读写分离,并且所有的特性仅需要通过简单的配置即可实现, `gdb` 内部将会对SQL请求自动地进行主从切换。以下是一个简单的主从配置,包含一主一从: ```yaml database: default: - type: "mysql" link: "root:12345678@tcp(192.168.1.1:3306)/test" role: "master" - type: "mysql" link: "root:12345678@tcp(192.168.1.2:3306)/test" role: "slave" ``` 在大部分的场景中,我们的写入请求是到 `Master` 主节点,而读取请求是到 `Slave` 从节点,这样的好处是能够对数据库的请求进行压力分摊,并提高数据库的可用性。但在某些场景中,我们期望读取操作在 `Master` 节点上执行,特别是一些对于即时性要求比较高的场景(因为主从节点之间的数据同步是有延迟的)。 开发者可以通过 `Master` 和 `Slave` 方法自定义决定当前链式操作执行在哪个节点上。 我们来一个简单的示例。我们有一个订单系统,每天的流量比较大,因此数据库在主从同步时往往会存在 `1-500ms` 时间的延迟。在业务需求中,创建订单后需要立即展示订单列表页面。可以预料到如果该订单列表页面默认往从节点读取数据的话,很有可能用户在创建订单后在订单列表页面看不到最新创建的订单(因为数据库主从同步延迟)。这个问题,我们可以在订单列表页面设置为往主节点读取数据即可解决。 1. 在订单创建的时候,没有必要指定操作的节点,因为写入操作默认是在主节点上执行的。为简化示例,我们这里仅展示关键的代码: ```go g.Model("order").Data(g.Map{ "uid" : 1000, "price" : 99.99, // ... }).Insert() ``` 2. 在订单列表页面查询时,我们需要使用 `Master` 方法指定查询操作是在主节点上进行,以避免读取延迟。 ```go g.Model("order").Master().Where("uid", 1000).All() ``` ` ` # ORM链式操作-查询缓存 ## 查询缓存 `gdb` 支持对查询结果的缓存处理,常用于多读少写的查询缓存场景,并支持手动的缓存清理。需要注意的是,查询缓存仅支持链式操作,且在事务操作下不可用。 相关方法: ```go type CacheOption struct { // Duration is the TTL for the cache. // If the parameter `Duration` < 0, which means it clear the cache with given `Name`. // If the parameter `Duration` = 0, which means it never expires. // If the parameter `Duration` > 0, which means it expires after `Duration`. Duration time.Duration // Name is an optional unique name for the cache. // The Name is used to bind a name to the cache, which means you can later control the cache // like changing the `duration` or clearing the cache with specified Name. Name string // Force caches the query result whatever the result is nil or not. // It is used to avoid Cache Penetration. Force bool } // Cache sets the cache feature for the model. It caches the result of the sql, which means // if there's another same sql request, it just reads and returns the result from cache, it // but not committed and executed into the database. // // Note that, the cache feature is disabled if the model is performing select statement // on a transaction. func (m *Model) Cache(option CacheOption) *Model ``` ## 缓存管理 ### 缓存对象 `ORM` 对象默认情况下提供了缓存管理对象,该缓存对象类型为 `*gcache.Cache`,也就是说同时也支持 `*gcache.Cache` 的所有特性。可以通过 `GetCache() *gcache.Cache` 接口方法获得该缓存对象,并通过返回的对象实现自定义的各种缓存操作,例如: `g.DB().GetCache().Keys()`。 ### 缓存适配( `Redis` 缓存) 默认情况下 `ORM` 的 `*gcache.Cache` 缓存对象提供的是单进程内存缓存,虽然性能非常高效,但是只能在单进程内使用。如果服务如果采用多节点部署,多节点之间的缓存可能会产生数据不一致的情况,因此大多数场景下我们都是通过 `Redis` 服务器来实现对数据库查询数据的缓存。 `*gcache.Cache` 对象采用了适配器设计模式,可以轻松实现从单进程内存缓存切换为分布式的 `Redis` 缓存。使用示例: ```go redisCache := gcache.NewAdapterRedis(g.Redis()) g.DB().GetCache().SetAdapter(redisCache) ``` 更多介绍请参考: [缓存管理-Redis缓存](./gcache-redis.md) ### 管理方法 为简化数据库的查询缓存管理,从 `v2.2.0` 版本开始,提供了两个缓存管理方法: ```go // ClearCache removes cached sql result of certain table. func (c *Core) ClearCache(ctx context.Context, table string) (err error) // ClearCacheAll removes all cached sql result from cache func (c *Core) ClearCacheAll(ctx context.Context) (err error) ``` 方法介绍如注释。可以看到这两个方法是挂载 `Core` 对象上的,而底层的 `Core` 对象已经通过 `DB` 接口暴露,因此我们这么来获取 `Core` 对象: ```go g.DB().GetCore() ``` ## 使用示例 ### 数据表结构 ```sql CREATE TABLE `user` ( `uid` int(10) unsigned NOT NULL AUTO_INCREMENT, `name` varchar(30) NOT NULL DEFAULT '' COMMENT '昵称', `site` varchar(255) NOT NULL DEFAULT '' COMMENT '主页', PRIMARY KEY (`uid`) ) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8; ``` ### 示例代码 ```go package main import ( "time" "github.com/gogf/gf/v2/database/gdb" "github.com/gogf/gf/v2/frame/g" "github.com/gogf/gf/v2/os/gctx" ) func main() { var ( db = g.DB() ctx = gctx.New() ) // 开启调试模式,以便于记录所有执行的SQL db.SetDebug(true) // 写入测试数据 _, err := g.Model("user").Ctx(ctx).Data(g.Map{ "name": "john", "site": "https://goframe.org", }).Insert() // 执行2次查询并将查询结果缓存1小时,并可执行缓存名称(可选) for i := 0; i < 2; i++ { r, _ := g.Model("user").Ctx(ctx).Cache(gdb.CacheOption{ Duration: time.Hour, Name: "vip-user", Force: false, }).Where("uid", 1).One() g.Log().Debug(ctx, r.Map()) } // 执行更新操作,并清理指定名称的查询缓存 _, err = g.Model("user").Ctx(ctx).Cache(gdb.CacheOption{ Duration: -1, Name: "vip-user", Force: false, }).Data(gdb.Map{"name": "smith"}).Where("uid", 1).Update() if err != nil { g.Log().Fatal(ctx, err) } // 再次执行查询,启用查询缓存特性 r, _ := g.Model("user").Ctx(ctx).Cache(gdb.CacheOption{ Duration: time.Hour, Name: "vip-user", Force: false, }).Where("uid", 1).One() g.Log().Debug(ctx, r.Map()) } ``` 执行后输出结果为(测试表数据结构仅供示例参考): ```html 2022-02-08 17:36:19.817 [DEBU] {c0424c75f1c5d116d0df0f7197379412} {"name":"john","site":"https://goframe.org","uid":1} 2022-02-08 17:36:19.817 [DEBU] {c0424c75f1c5d116d0df0f7197379412} {"name":"john","site":"https://goframe.org","uid":1} 2022-02-08 17:36:19.817 [DEBU] {c0424c75f1c5d116d0df0f7197379412} [ 0 ms] [default] [rows:1 ] UPDATE `user` SET `name`='smith' WHERE `uid`=1 2022-02-08 17:36:19.818 [DEBU] {c0424c75f1c5d116d0df0f7197379412} [ 1 ms] [default] [rows:1 ] SELECT * FROM `user` WHERE `uid`=1 LIMIT 1 2022-02-08 17:36:19.818 [DEBU] {c0424c75f1c5d116d0df0f7197379412} {"name":"smith","site":"https://goframe.org","uid":1} ``` 可以看到: 1. 为了方便展示缓存效果,这里开启了数据 `debug` 特性,当有任何的SQL操作时将会输出到终端。 2. 执行两次 `One` 方法数据查询,第一次走了SQL查询,第二次直接使用到了缓存,SQL没有提交到数据库执行,因此这里只打印了一条查询SQL,并且两次查询的结果也是一致的。 3. 注意这里为该查询的缓存设置了一个自定义的名称 `vip-user`,以便于后续清空更新缓存。如果缓存不需要清理,那么可以不用设置缓存名称。 4. 当执行 `Update` 更新操作时,同时根据名称清空指定的缓存。 5. 随后再执行 `One` 方法数据查询,这时重新缓存新的数据。 # ORM链式操作-数据库切换 我们知道数据库的配置中有支持对默认数据库的配置,因此 `DB` 对象及 `Model` 对象在初始化的时候已经绑定到了特定的数据库上。运行时切换数据库有几种方案(假如我们的数据库有 `user` 用户数据库和 `order` 订单数据库): 1. 通过不同的配置分组来实现。这需要在配置文件中配置不同的分组配置,随后在程序中可以通过 `g.DB("分组名称")` 来获取特定数据库的单例对象。 2. 通过运行时 `DB.SetSchema` 方法切换单例对象的数据库,需要注意的是由于修改的是单例对象的数据库配置,因此影响是全局的: ```go g.DB().SetSchema("user-schema") g.DB().SetSchema("order-schema") ``` 3. 通过链式操作 `Schema` 方法创建 `Schema` 数据库对象,并通过该数据库对象创建模型对象并执行后续链式操作: ```go g.DB().Schema("user-schema").Model("user").All() g.DB().Schema("order-schema").Model("order").All() ``` 4. 也可以通过链式操作 `Model.Schema` 方法设置当前链式操作对应的数据库,没有设置的情况下使用的是其 `DB` 或者 `TX` 默认连接的数据库: ```go g.Model("user").Schema("user-schema").All() g.Model("order").Schema("order-schema").All() ``` **提示** 注意两种使用方式的差别,前一种方式来自于 `Schema` 对象创建 `Model` 对象后执行操作;后一种方式是通过修改当前 `Model` 对象操作的数据库名称达到切换数据库的目的。 5. 此外,假如当前数据库操作配置的用户有权限,那么可以直接通过表名中带数据库名称实现跨域操作,甚至跨域关联查询: ```go // SELECT * FROM `order`.`order` o LEFT JOIN `user`.`user` u ON (o.uid=u.id) WHERE u.id=1 LIMIT 1 g.Model("order.order o").LeftJoin("user.user u", "o.uid=u.id").Where("u.id", 1).One() ``` # ORM链式操作-Hook特性 `Hook` 特性允许我们对特性的 `Model` 绑定 `CRUD` 钩子处理。 ## 相关定义 相关 `Hook` 函数: ```go type ( HookFuncSelect func(ctx context.Context, in *HookSelectInput) (result Result, err error) HookFuncInsert func(ctx context.Context, in *HookInsertInput) (result sql.Result, err error) HookFuncUpdate func(ctx context.Context, in *HookUpdateInput) (result sql.Result, err error) HookFuncDelete func(ctx context.Context, in *HookDeleteInput) (result sql.Result, err error) ) // HookHandler manages all supported hook functions for Model. type HookHandler struct { Select HookFuncSelect Insert HookFuncInsert Update HookFuncUpdate Delete HookFuncDelete } ``` `Hook` 注册方法: ```go // Hook sets the hook functions for current model. func (m *Model) Hook(hook HookHandler) *Model ``` ## 使用示例 查询 `birth_day` 字段时,同时计算出当前用户的年龄: ```go // Hook function definition. var hook = gdb.HookHandler{ Select: func(ctx context.Context, in *gdb.HookSelectInput) (result gdb.Result, err error) { result, err = in.Next(ctx) if err != nil { return } for i, record := range result { if !record["birth_day"].IsEmpty() { age := gtime.Now().Sub(record["birth_day"].GTime()).Hours() / 24 / 365 record["age"] = gvar.New(age) } result[i] = record } return }, } // It registers hook function, creates and returns a new `model`. model := g.Model("user").Hook(hook) // The hook function takes effect on each ORM operation when using the `model`. all, err := model.Where("status", "online").OrderAsc(`id`).All() ``` # ORM链式操作-Handler特性 `Handler` 特性允许轻松地复用常见的逻辑。 ## 示例1,查询 ```go func AmountGreaterThan1000(m *gdb.Model) *gdb.Model { return m.WhereGT("amount", 1000) } func PaidWithCreditCard(m *gdb.Model) *gdb.Model { return m.Where("pay_mode_sign", "credit_card") } func PaidWithCod(m *gdb.Model) *gdb.Model { return m.Where("pay_mode_sign", "cod") } func OrderStatus(statuses []string) func(m *gdb.Model) *gdb.Model { return func(m *gdb.Model) *gdb.Model { return m.Where("status", statuses) } } var ( m = g.Model("product_order") ) m.Handler(AmountGreaterThan1000, PaidWithCreditCard).Scan(&orders) // SELECT * FROM `product_order` WHERE `amount`>1000 AND `pay_mode_sign`='credit_card' // 查找所有金额大于 1000 的信用卡订单 m.Handler(AmountGreaterThan1000, PaidWithCod).Scan(&orders) // SELECT * FROM `product_order` WHERE `amount`>1000 AND `pay_mode_sign`='cod' // 查找所有金额大于 1000 的 COD 订单 m.Handler(AmountGreaterThan1000, OrderStatus([]string{"paid", "shipped"})).Scan(&orders) // SELECT * FROM `product_order` WHERE `amount`>1000 AND `status` IN('paid','shipped') // 查找所有金额大于1000 的已付款或已发货订单 ``` ## 示例2,分页 ```go func Paginate(r *ghttp.Request) func(m *gdb.Model) *gdb.Model { return func(m *gdb.Model) *gdb.Model { type Pagination struct { Page int Size int } var pagination Pagination _ = r.Parse(&pagination) switch { case pagination.Size > 100: pagination.Size = 100 case pagination.Size <= 0: pagination.Size = 10 } return m.Page(pagination.Page, pagination.Size) } } m.Handler(Paginate(r)).Scan(&users) m.Handler(Paginate(r)).Scan(&articles) ``` # ORM链式操作-悲观锁 & 乐观锁 `悲观锁(Pessimistic Lock)`,顾名思义,就是很悲观,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会阻塞直到它拿到锁。传统的关系型数据库里边就用到了很多这种锁机制,比如行锁、表锁、读锁、写锁等,都是在做操作之前先上锁。 `乐观锁(Optimistic Lock)`,顾名思义,就是很乐观,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,可以使用版本号等机制实现。乐观锁适用于多读的应用类型,这样可以提高吞吐量。 ### 悲观锁使用 相关方法: ```go func (m *Model) LockUpdate() *Model func (m *Model) LockShared() *Model func (m *Model) LockUpdateSkipLocked() *Model ``` `gdb` 模块的链式操作提供了两个方法帮助在 `SQL` 语句中实现「悲观锁」。可以在查询中使用 `LockShared` 方法从而在运行语句时带一把」共享锁「。共享锁可以避免被选择的行被修改直到事务提交: ```go g.Model("users").Ctx(ctx).Where("votes>?", 100).LockShared().All(); ``` 上面这个查询等价于下面这条 SQL 语句: ```sql SELECT * FROM `users` WHERE `votes` > 100 LOCK IN SHARE MODE ``` 此外还可以使用 `LockUpdate` 方法。该方法用于创建 `FOR UPDATE` 锁,避免选择行被其它共享锁修改或删除: ```go g.Model("users").Ctx(ctx).Where("votes>?", 100).LockUpdate().All(); ``` 上面这个查询等价于下面这条 SQL 语句: ```sql SELECT * FROM `users` WHERE `votes` > 100 FOR UPDATE ``` #### 跳过已锁定的行(Skip Locked) **提示** 版本要求:`v2.10.0` 从 `v2.10.0` 版本开始,新增了 `LockUpdateSkipLocked` 方法,支持在高并发场景下跳过已被锁定的行,避免等待,提升系统吞吐量。 **使用方法:** ```go g.Model("tasks").Ctx(ctx).Where("status", "pending").Limit(10).LockUpdateSkipLocked().All() ``` 上面这个查询等价于下面这条`SQL`语句: ```sql SELECT * FROM `tasks` WHERE `status` = 'pending' LIMIT 10 FOR UPDATE SKIP LOCKED ``` **应用场景:** `SKIP LOCKED` 特别适用于任务队列、工单分配等高并发场景。当多个工作进程同时争抢任务时: - 使用 `LockUpdate()`:所有进程会排队等待锁释放,导致性能下降 - 使用 `LockUpdateSkipLocked()`:每个进程跳过已被其他进程锁定的行,立即获取可用的任务,提升并发处理能力 **示例:任务队列处理** ```go package main import ( "context" "github.com/gogf/gf/v2/database/gdb" "github.com/gogf/gf/v2/frame/g" "github.com/gogf/gf/v2/os/gctx" ) func main() { ctx := gctx.New() // 开启事务 err := g.DB().Transaction(ctx, func(ctx context.Context, tx gdb.TX) error { // 获取未被锁定的待处理任务 records, err := g.Model("tasks").Ctx(ctx). Where("status", "pending"). Limit(10). LockUpdateSkipLocked(). // 跳过已被其他进程锁定的任务 All() if err != nil { return err } // 处理任务... for _, record := range records { // 更新任务状态为处理中 _, err = g.Model("tasks").Ctx(ctx). Data(g.Map{"status": "processing"}). Where("id", record["id"]). Update() if err != nil { return err } } return nil }) if err != nil { g.Log().Error(ctx, err) } } ``` **数据库支持:** `SKIP LOCKED` 功能由以下数据库支持: - `PostgreSQL 9.5+` - `Oracle` - `MySQL 8.0+` - `MariaDB 10.6+` **注意** 使用 `LockUpdateSkipLocked()` 前请确认的数据库版本支持该特性,否则会导致`SQL`执行错误。 **性能对比:** 在高并发任务分配场景下: - 使用 `LockUpdate()`:`10`个工作进程处理`100`个任务可能需要`10`秒(排队等待) - 使用 `LockUpdateSkipLocked()`:`10`个工作进程可以同时各自获取`10`个任务并行处理,时间缩短为`1`秒 #### 锁机制对比 `FOR UPDATE` 与 `LOCK IN SHARE MODE` 都是用于确保被选中的记录值不能被其它事务更新(上锁),两者的区别在于 `LOCK IN SHARE MODE` 不会阻塞其它事务读取被锁定行记录的值,而 `FOR UPDATE` 会阻塞其他锁定性读对锁定行的读取(非锁定性读仍然可以读取这些记录, `LOCK IN SHARE MODE` 和 `FOR UPDATE` 都是锁定性读)。 这么说比较抽象,我们举个计数器的例子:在一条语句中读取一个值,然后在另一条语句中更新这个值。使用 `LOCK IN SHARE MODE` 的话可以允许两个事务读取相同的初始化值,所以执行两个事务之后最终计数器的值 `+1`;而如果使用 `FOR UPDATE` 的话,会锁定第二个事务对记录值的读取直到第一个事务执行完成,这样计数器的最终结果就是 `+2` 了。 ### 乐观锁使用 乐观锁,大多是基于数据版本 ( `Version`)记录机制实现。何谓数据版本?即为数据增加一个版本标识,在基于数据库表的版本解决方案中,一般是通过为数据库表增加一个 " `version`" 字段来实现。 读取出数据时,将此版本号一同读出,之后更新时,对此版本号加一。此时,将提交数据的版本数据与数据库表对应记录的当前版本信息进行比对,如果提交的数据版本号大于数据库表当前版本号,则予以更新,否则认为是过期数据。 ### 锁机制总结 两种锁各有优缺点,不可认为一种好于另一种,像乐观锁适用于写比较少的情况下,即冲突真的很少发生的时候,这样可以省去了锁的开销,加大了系统的整个吞吐量。但如果经常产生冲突,上层应用会不断的进行重试,这样反倒是降低了性能,所以这种情况下用悲观锁就比较合适。 # ORM查询-Where条件 `ORM` 组件提供了一些常用的条件查询方法,并且条件方法支持多种数据类型输入。`Where`条件方法比较多,本章节方法更新可能会不及时,完整的接口列表请参考接口文档:https://pkg.go.dev/github.com/gogf/gf/v2/database/gdb#Model ```go func (m *Model) Where(where interface{}, args...interface{}) *Model func (m *Model) Wheref(format string, args ...interface{}) *Model func (m *Model) WherePri(where interface{}, args ...interface{}) *Model func (m *Model) WhereBetween(column string, min, max interface{}) *Model func (m *Model) WhereLike(column string, like interface{}) *Model func (m *Model) WhereIn(column string, in interface{}) *Model func (m *Model) WhereNull(columns ...string) *Model func (m *Model) WhereLT(column string, value interface{}) *Model func (m *Model) WhereLTE(column string, value interface{}) *Model func (m *Model) WhereGT(column string, value interface{}) *Model func (m *Model) WhereGTE(column string, value interface{}) *Model func (m *Model) WhereNotBetween(column string, min, max interface{}) *Model func (m *Model) WhereNotLike(column string, like interface{}) *Model func (m *Model) WhereNotIn(column string, in interface{}) *Model func (m *Model) WhereNotNull(columns ...string) *Model func (m *Model) WhereOr(where interface{}, args ...interface{}) *Model func (m *Model) WhereOrBetween(column string, min, max interface{}) *Model func (m *Model) WhereOrLike(column string, like interface{}) *Model func (m *Model) WhereOrIn(column string, in interface{}) *Model func (m *Model) WhereOrNull(columns ...string) *Model func (m *Model) WhereOrLT(column string, value interface{}) *Model func (m *Model) WhereOrLTE(column string, value interface{}) *Model func (m *Model) WhereOrGT(column string, value interface{}) *Model func (m *Model) WhereOrGTE(column string, value interface{}) *Model func (m *Model) WhereOrNotBetween(column string, min, max interface{}) *Model func (m *Model) WhereOrNotLike(column string, like interface{}) *Model func (m *Model) WhereOrNotIn(column string, in interface{}) *Model func (m *Model) WhereOrNotNull(columns ...string) *Model func (m *Model) WhereExists(subQuery *Model) *Model func (m *Model) WhereNotExists(subQuery *Model) *Model ``` 下面我们对其中的几个常用方法做简单介绍,其他条件查询方法用法类似。 ## `Where/WhereOr` 查询条件 ### 基本介绍 这两个方法用于传递查询条件参数,支持的参数为任意的 `string/map/slice/struct/*struct` 类型。 `Where` 条件参数推荐使用字符串的参数传递方式(并使用 `?` 占位符预处理),因为 `map`/ `struct` 类型作为查询参数无法保证顺序性,且在部分情况下(数据库有时会帮助自动进行查询索引优化),数据库的索引和传递的查询条件顺序有一定关系。 当使用多个 `Where` 方法连接查询条件时,多个条件之间使用 `And` 进行连接。 此外,当存在多个查询条件时, `gdb` 会默认将多个条件分别使用 `()` 符号进行包含,这种设计可以非常友好地支持查询条件分组。 使用示例: ```go // WHERE `uid`=1 Where("uid=1") Where("uid", 1) Where("uid=?", 1) Where(g.Map{"uid" : 1}) // WHERE `uid` <= 1000 AND `age` >= 18 Where(g.Map{ "uid <=" : 1000, "age >=" : 18, }) // WHERE (`uid` <= 1000) AND (`age` >= 18) Where("uid <=?", 1000).Where("age >=?", 18) // WHERE `level`=1 OR `money`>=1000000 Where("level=? OR money >=?", 1, 1000000) // WHERE (`level`=1) OR (`money`>=1000000) Where("level", 1).WhereOr("money >=", 1000000) // WHERE `uid` IN(1,2,3) Where("uid IN(?)", g.Slice{1,2,3}) ``` 使用 `struct` 参数的示例,其中 `orm` 的 `tag` 用于指定 `struct` 属性与表字段的映射关系: ```go type Condition struct{ Sex int `orm:"sex"` Age int `orm:"age"` } Where(Condition{1, 18}) // WHERE `sex`=1 AND `age`=18 ``` ### 使用示例 `Where + string`,条件参数使用字符串和预处理。 ```go // 查询多条记录并使用Limit分页 // SELECT * FROM user WHERE uid>1 LIMIT 0,10 g.Model("user").Where("uid > ?", 1).Limit(0, 10).All() // 使用Fields方法查询指定字段 // 未使用Fields方法指定查询字段时,默认查询为* // SELECT uid,name FROM user WHERE uid>1 LIMIT 0,10 g.Model("user").Fields("uid,name").Where("uid > ?", 1).Limit(0, 10).All() // 支持多种Where条件参数类型 // SELECT * FROM user WHERE uid=1 LIMIT 1 g.Model("user").Where("uid=1").One() g.Model("user").Where("uid", 1).One() g.Model("user").Where("uid=?", 1).One() // SELECT * FROM user WHERE (uid=1) AND (name='john') LIMIT 1 g.Model("user").Where("uid", 1).Where("name", "john").One() g.Model("user").Where("uid=?", 1).Where("name=?", "john").One() // SELECT * FROM user WHERE (uid=1) OR (name='john') LIMIT 1 g.Model("user").Where("uid=?", 1).WhereOr("name=?", "john").One() ``` `Where + slice`,预处理参数可直接通过 `slice` 参数给定。 ```go // SELECT * FROM user WHERE age>18 AND name like '%john%' g.Model("user").Where("age>? AND name like ?", g.Slice{18, "%john%"}).All() // SELECT * FROM user WHERE status=1 g.Model("user").Where("status=?", g.Slice{1}).All() ``` `Where + map`,条件参数使用任意 `map` 类型传递。 ```go // SELECT * FROM user WHERE uid=1 AND name='john' LIMIT 1 g.Model("user").Where(g.Map{"uid" : 1, "name" : "john"}).One() // SELECT * FROM user WHERE uid=1 AND age>18 LIMIT 1 g.Model("user").Where(g.Map{"uid" : 1, "age>" : 18}).One() ``` `Where + struct/*struct`, `struct` 标签支持 `orm/json`,映射属性到字段名称关系。 ```go type User struct { Id int `json:"uid"` UserName string `orm:"name"` } // SELECT * FROM user WHERE uid =1 AND name='john' LIMIT 1 g.Model("user").Where(User{ Id : 1, UserName : "john"}).One() // SELECT * FROM user WHERE uid =1 LIMIT 1 g.Model("user").Where(&User{ Id : 1}).One() ``` 以上的查询条件相对比较简单,我们来看一个比较复杂的查询示例。 ```go condition := g.Map{ "title like ?" : "%九寨%", "online" : 1, "hits between ? and ?" : g.Slice{1, 10}, "exp > 0" : nil, "category" : g.Slice{100, 200}, } // SELECT * FROM article WHERE title like '%九寨%' AND online=1 AND hits between 1 and 10 AND exp > 0 AND category IN(100,200) g.Model("article").Where(condition).All() ``` ## `Wheref` 格式化条件字符串 在某些场景中,在输入带有字符串的条件语句时,往往需要结合 `fmt.Sprintf` 来格式化条件(当然,注意在字符串中使用占位符代替变量的输入而不是直接将变量格式化),因此我们提供了 `Where+fmt.Sprintf` 结合的便捷方法 `Wheref`。使用示例: ```go // WHERE score > 100 and status in('succeeded','completed') Wheref(`score > ? and status in (?)`, 100, g.Slice{"succeeded", "completed"}) ``` ## `WherePri` 支持主键的查询条件 `WherePri` 方法的功能同 `Where`,但提供了对表主键的智能识别,常用于根据主键的便捷数据查询。假如 `user` 表的主键为 `uid`,我们来看一下 `Where` 与 `WherePri` 的区别: ```go // WHERE `uid`=1 Where("uid", 1) WherePri(1) // WHERE `uid` IN(1,2,3) Where("uid", g.Slice{1,2,3}) WherePri(g.Slice{1,2,3}) ``` 可以看到,当使用 `WherePri` 方法且给定参数为单一的参数基本类型或者 `slice` 类型时,将会被识别为主键的查询条件值。 ## `WhereBuilder` 复杂条件组合 `WhereBuilder` 用以组合生成复杂的 `Where` 条件。 ### 对象创建 我们可以使用 `Model` 的 `Builder` 方法生成 `WhereBuilder` 对象。该方法定义如下: ```go // Builder creates and returns a WhereBuilder. func (m *Model) Builder() *WhereBuilder ``` ### 使用示例 ```go // SELECT * FROM `user` WHERE `id`=1 AND `address`="USA" AND (`status`="active" OR `status`="pending") m := g.Model("user") all, err := m.Where("id", 1).Where("address", "USA").Where( m.Builder().Where("status", "active").WhereOr("status", "pending"), ).All() ``` ## `WhereExists/WhereNotExists` 子查询条件 ### 基本介绍 `WhereExists` 和 `WhereNotExists` 方法用于创建 `EXISTS` 和 `NOT EXISTS` 子查询条件,这是 SQL 中常用的高级查询技术。这两个方法接受一个 `*Model` 类型的参数作为子查询。 ```go func (b *WhereBuilder) WhereExists(subQuery *Model) *WhereBuilder func (b *WhereBuilder) WhereNotExists(subQuery *Model) *WhereBuilder ``` `EXISTS` 子查询用于检查子查询是否返回任何行,如果子查询返回至少一行,则 `EXISTS` 条件为真;如果子查询不返回任何行,则 `EXISTS` 条件为假。`NOT EXISTS` 则相反,当子查询不返回任何行时条件为真。 这两个方法特别适合于以下场景: 1. 需要检查一个表中的记录是否在另一个表中存在关联记录 2. 需要查找满足或不满足某些复杂条件的记录 3. 替代复杂的 JOIN 查询,提高查询性能 ### 使用示例 #### 基本用法 ```go // 查询存在关联订单的用户 // SELECT * FROM user WHERE EXISTS (SELECT 1 FROM order WHERE order.user_id = user.id) subQuery := g.Model("order").Fields("1").Where("order.user_id = user.id") users, err := g.Model("user").WhereExists(subQuery).All() // SELECT * FROM user WHERE EXISTS (SELECT 1 FROM order WHERE order.user_id = user.id) ``` #### 使用表别名 ```go // 查询没有任何订单的用户 subQuery := g.Model("order").Fields("id").Where("user_id = u.id") users, err := g.Model("user u").WhereNotExists(subQuery).All() // SELECT * FROM user u WHERE NOT EXISTS (SELECT id FROM order WHERE user_id = u.id) ``` #### 与其他条件组合 ```go // 查询活跃状态且有订单的用户 subQuery := g.Model("order").Fields("id").Where("user_id = user.id") users, err := g.Model("user").Where("status", "active").WhereExists(subQuery).All() // SELECT * FROM user WHERE (status='active') AND EXISTS (SELECT id FROM order WHERE user_id = user.id) ``` #### 复杂子查询 ```go // 查询有未完成订单的VIP用户 subQuery := g.Model("order").Fields("id").Where("user_id = u.id").Where("status", "pending") users, err := g.Model("user u").Where("vip", 1).WhereExists(subQuery).All() // SELECT * FROM user u WHERE (vip=1) AND EXISTS (SELECT id FROM order WHERE user_id = u.id AND status='pending') ``` #### 使用原始SQL引用 在子查询中引用外部查询的字段时,可以使用 `db.Raw()` 方法: ```go // 查询有相同名称产品的类别 subQuery := g.Model("product").Fields("id").Where("name = ?", db.Raw("category.name")) categories, err := g.Model("category").WhereExists(subQuery).All() // SELECT * FROM category WHERE EXISTS (SELECT id FROM product WHERE name = category.name) ``` ## 注意事项:空数组条件引发的 `0=1` 条件 我们来看例子: `SQL1`: ```go m := g.Model("auth") m.Where("status", g.Slice{"permitted", "inherited"}).Where("uid", 1).All() // SELECT * FROM `auth` WHERE (`status` IN('permitted','inherited')) AND (`uid`=1) ``` `SQL2`: ```go m := g.Model("auth") m.Where("status", g.Slice{}).Where("uid", 1).All() // SELECT * FROM `auth` WHERE (0=1) AND (`uid`=1) ``` 可以看到,当给定的数组条件为空数组时,生成的 `SQL` 出现了 `0=1` 的无效条件,这是为什么呢? 在开发者没有显示声明可以过滤空数组条件时, `ORM` 不会自动过滤空数组条件,以避免程序逻辑绕过 `SQL` 限制条件,引发不可预知的业务问题。如果开发者确定 `SQL` 限制条件是可以过滤的,那么可以显示调用 `OmitEmpty/OmitEmptyWhere` 方法来执行空条件过滤,如下: ```go m := g.Model("auth") m.Where("status", g.Slice{}).Where("uid", 1).OmitEmpty().All() // SELECT * FROM `auth` WHERE `uid`=1 ``` # ORM查询-常用查询方法 ## 基本介绍 数据查询比较常用的几个方法: ```go func (m *Model) All(where ...interface{} (Result, error) func (m *Model) One(where ...interface{}) (Record, error) func (m *Model) Array(fieldsAndWhere ...interface{}) ([]Value, error) func (m *Model) Value(fieldsAndWhere ...interface{}) (Value, error) func (m *Model) Count(where ...interface{}) (int, error) func (m *Model) CountColumn(column string) (int, error) ``` 简要说明: 1. `All` 用于查询并返回多条记录的列表/数组。 2. `One` 用于查询并返回单条记录。 3. `Array` 用于查询指定字段列的数据,返回数组。 4. `Value` 用于查询并返回一个字段值,往往需要结合 `Fields` 方法使用。 5. `Count` 用于查询并返回记录数。 此外,也可以看得到这五个方法定义中也支持条件参数的直接输入,参数类型与 `Where` 方法一致。但需要注意,其中 `Array` 和 `Value` 方法的参数中至少应该输入字段参数。 ## 使用示例 ```go // SELECT * FROM `user` WHERE `score`>60 Model("user").Where("score>?", 60).All() // SELECT * FROM `user` WHERE `score`>60 LIMIT 1 Model("user").Where("score>?", 60).One() // SELECT `name` FROM `user` WHERE `score`>60 Model("user").Fields("name").Where("score>?", 60).Array() // SELECT `name` FROM `user` WHERE `uid`=1 LIMIT 1 Model("user").Fields("name").Where("uid", 1).Value() // SELECT COUNT(1) FROM `user` WHERE `status` IN(1,2,3) Model("user").Where("status", g.Slice{1,2,3}).Count() ``` # ORM查询-AllAndCount ## 基本介绍 该方法从 `v2.5.0` 版本开始提供,用于同时查询数据记录列表及总数量,一般用于分页查询场景中,简化分页查询逻辑。 方法定义: ```go // AllAndCount retrieves all records and the total count of records from the model. // If useFieldForCount is true, it will use the fields specified in the model for counting; // otherwise, it will use a constant value of 1 for counting. // It returns the result as a slice of records, the total count of records, and an error if any. // The where parameter is an optional list of conditions to use when retrieving records. // // Example: // // var model Model // var result Result // var count int // where := []interface{}{"name = ?", "John"} // result, count, err := model.AllAndCount(true) // if err != nil { // // Handle error. // } // fmt.Println(result, count) func (m *Model) AllAndCount(useFieldForCount bool) (result Result, totalCount int, err error) ``` 在方法内部查询总数量时,将会忽略查询中的 `Limit/Page` 操作。 ## 使用示例 ```go // SELECT `uid`,`name` FROM `user` WHERE `status`='deleted' LIMIT 0,10 // SELECT COUNT(`uid`,`name`) FROM `user` WHERE `status`='deleted' all, count, err := Model("user").Fields("uid", "name").Where("status", "deleted").Limit(0, 10).AllAndCount(true) // SELECT `uid`,`name` FROM `user` WHERE `status`='deleted' LIMIT 0,10 // SELECT COUNT(1) FROM `user` WHERE `status`='deleted' all, count, err := Model("user").Fields("uid", "name").Where("status", "deleted").Limit(0, 10).AllAndCount(false) ``` # ORM查询-Exist `Exist`方法可以更高效地检索所给的`Where`条件数据是否存在,而不是查询完整的数据结果后返回。 方法定义: ```go func (m *Model) Exist(where ...interface{}) (bool, error) ``` ## 示例SQL 这是后续示例代码中用到的`MySQL`表结构。 ```sql CREATE TABLE `user` ( `id` int(10) unsigned NOT NULL AUTO_INCREMENT, `name` varchar(45) NOT NULL PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8; ``` ## 使用示例 查询完整数据: ```go // SELECT * FROM `user` WHERE (id > 1) AND `deleted_at`=0 g.Model("user").Where("id > ?", 1).All() ``` 使用`Exist`方法: ```go // SELECT 1 FROM `user` WHERE (id > 1) AND `deleted_at`=0 LIMIT 1 g.Model("user").Where("id > ?", 1).Exist() ``` 可以看到底层是使用`SELECT 1`来查询结果,即如果结果存在则返回`1`,否则什么也不返回。 # ORM查询-Scan `Scan` 方法支持将查询结果转换为结构体或者结构体数组, `Scan` 方法将会根据给定的参数类型自动识别执行的转换类型。 ## `struct` 对象 `Scan` 支持将查询结果转换为一个 `struct` 对象,查询结果应当是特定的一条记录,并且 `pointer` 参数应当为 `struct` 对象的指针地址( `*struct` 或者 `**struct`),使用方式例如: ```go type User struct { Id int Passport string Password string NickName string CreateTime *gtime.Time } user := User{} g.Model("user").Where("id", 1).Scan(&user) ``` 或者 ```go var user = User{} g.Model("user").Where("id", 1).Scan(&user) ``` 前两种方式都是预先初始化对象(提前分配内存),推荐的方式: ```go var user *User g.Model("user").Where("id", 1).Scan(&user) ``` 这种方式只有在查询到数据的时候才会执行初始化及内存分配。注意在用法上的区别,特别是传递参数类型的差别(前两种方式传递的参数类型是 `*User`,这里传递的参数类型其实是 `**User`)。 ## `struct` 数组 `Scan` 支持将多条查询结果集转换为一个 `[]struct/[]*struct` 数组,查询结果应当是多条记录组成的结果集,并且 `pointer` 应当为数组的指针地址,使用方式例如: ```go var users []User g.Model("user").Scan(&users) ``` 或者 ```go var users []*User g.Model("user").Scan(&users) ``` # ORM查询-ScanAndCount ## 基本介绍 在分页查询场景中,我们往往需要先调用 `Scan` 方法结合 `Limit/Page` 链式操作方法查询列表,随后再去掉 `Limit/Page` 链式操作方法查询总数量。这一过程较为繁琐,因此从 `v2.5.0` 版本开始,框架提供了 `ScanAndCount` 方法,用于简化分页查询的场景。 ## 使用示例 **提示** 示例代码来源于业务项目案例,仅供参考理解,无法独立运行。 使用传统的分页查询逻辑代码: ```go // GetList 获取实例的用户列表. func (s sUserInfo) GetList(ctx context.Context, in model.UserInfoGetListInput) (items []entity.UserInfo, total int, err error) { items = make([]entity.UserInfo, 0) orm := dao.UserInfo.Ctx(ctx).Where(do.UserInfo{ ResourceId: in.ResourceId, Status: in.Statuses, }) err = orm.Order(in.OrderBy, in.OrderDirection).Limit(in.Offset, in.Limit).Scan(&items) if err != nil { return } total, err = orm.Count() return } ``` 使用 `ScanAndCount` 方法实现分页查询: ```go // GetList 获取实例的用户列表. func (s sUserInfo) GetList(ctx context.Context, in model.UserInfoGetListInput) (items []entity.UserInfo, total int, err error) { items = make([]entity.UserInfo, 0) err = dao.UserInfo.Ctx(ctx).Where(do.UserInfo{ ResourceId: in.ResourceId, Status: in.Statuses, }). Order(in.OrderBy, in.OrderDirection). Limit(in.Offset, in.Limit). ScanAndCount(&items, &total, false) return } ``` ## 注意事项 - 仅用于需要同时查询数据和总数量的场景,一般为分页场景。 - `ScanAndCount` 的第 `3` 个参数 `useFieldForCount` 表示是否在执行 `Count` 操作的时候将 `Fields` 作为 `Count` 参数,一般为 `false` 即可,表示执行 `COUNT(1)` 查询总数量。传递 `true` 表示执行使用查询的字段作为 `COUNT` 方法的参数。 # ORM查询-Join查询 ## `*Join`系列方法 1. `LeftJoin` 左关联查询。 2. `RightJoin` 右关联查询。 3. `InnerJoin` 内关联查询。 **备注** 其实我们并不推荐使用 `Join` 进行联表查询,特别是在数据量比较大、并发请求量比较高的场景中,容易产生性能问题,也容易提高维护的复杂度。建议在确定有此必要的场景下使用。 此外,也可以参考 [ORM链式操作-模型关联](./gdb-chaining-relation-scan-list.md) 章节,数据库只负责存储数据和简单的单表操作,通过 `ORM` 提供的功能在代码层面实现数据聚合。 使用示例: ```go // 查询符合条件的单条记录(第一条) // SELECT u.*,ud.site FROM user u LEFT JOIN user_detail ud ON u.uid=ud.uid WHERE u.uid=1 LIMIT 1 g.Model("user u").LeftJoin("user_detail ud", "u.uid=ud.uid").Fields("u.*,ud.site").Where("u.uid", 1).One() // 查询指定字段值 // SELECT ud.site FROM user u RIGHT JOIN user_detail ud ON u.uid=ud.uid WHERE u.uid=1 LIMIT 1 g.Model("user u").RightJoin("user_detail ud", "u.uid=ud.uid").Fields("ud.site").Where("u.uid", 1).Value() // 分组及排序 // SELECT u.*,ud.city FROM user u INNER JOIN user_detail ud ON u.uid=ud.uid GROUP BY city ORDER BY register_time asc g.Model("user u").InnerJoin("user_detail ud", "u.uid=ud.uid").Fields("u.*,ud.city").Group("city").Order("register_time asc").All() // 不使用join的联表查询 // SELECT u.*,ud.city FROM user u,user_detail ud WHERE u.uid=ud.uid g.Model("user u,user_detail ud").Where("u.uid=ud.uid").Fields("u.*,ud.city").All() ``` ## 自定义数据表别名 ```go // SELECT * FROM `user` AS u LEFT JOIN `user_detail` as ud ON(ud.id=u.id) WHERE u.id=1 LIMIT 1 g.Model("user", "u").LeftJoin("user_detail", "ud", "ud.id=u.id").Where("u.id", 1).One() g.Model("user").As("u").LeftJoin("user_detail", "ud", "ud.id=u.id").Where("u.id", 1).One() ``` ## `*JoinOnFields`系列方法 `LeftJoinOnFields/RightJoinOnFields/InnerJoinOnFields`这三个方法可以指定字段和操作符进行`join`查询,使用示例: ```go // 查询符合条件的单条记录(第一条) // SELECT user.*,user_detail.address FROM user LEFT JOIN user_detail ON (user.id = user_detail.uid) WHERE user.id=1 LIMIT 1 g.Model("user").LeftJoinOnFields("user_detail", "id", "=", "uid").Fields("user.*,user_detail.address").Where("id", 1).One() // 查询多条记录 // SELECT user.*,user_detail.address FROM user RIGHT JOIN user_detail ON (user.id = user_detail.uid) g.Model("user").RightJoinOnFields("user_detail", "id", "=", "uid").Fields("user.*,user_detail.address").All() ``` ## 结合 `dao` 使用示例 ```go // SELECT resource_task_schedule.id,...,time_window.time_window // FROM `resource_task_schedule` // LEFT JOIN `time_window` ON (`resource_task_schedule`.`resource_id`=`time_window`.`resource_id`) // WHERE (time_window.`status`="valid") AND (`time_window`.`start_time` <= 3600) var ( orm = dao.ResourceTaskSchedule.Ctx(ctx) tsTable = dao.ResourceTaskSchedule.Table() tsCls = dao.ResourceTaskSchedule.Columns() twTable = dao.TimeWindow.Table() twCls = dao.TimeWindow.Columns() scheduleItems []scheduleItem ) orm = orm.FieldsPrefix(tsTable, tsCls) orm = orm.FieldsPrefix(twTable, twCls.TimeWindow) orm = orm.LeftJoinOnField(twTable, twCls.ResourceId) orm = orm.WherePrefix(twTable, twCls.Status, "valid") orm = orm.WherePrefixLTE(twTable, twCls.StartTime, 3600) err = orm.Scan(&scheduleItems) ``` ```go // SELECT DISTINCT resource_info.* FROM `resource_info` // LEFT JOIN `resource_network` ON (`resource_info`.`resource_id`=`resource_network`.`resource_id`) // WHERE (`resource_info`.`resource_id` like '%10.0.1.3%') // or (`resource_info`.`resource_name` like '%10.0.1.3%') // or (`resource_network`.`vip`like '%10.0.1.3%') // ORDER BY `id` Desc LIMIT 0,2 var ( orm = dao.ResourceInfo.Ctx(ctx).OmitEmpty() rTable = dao.ResourceInfo.Table() rCls = dao.ResourceInfo.Columns() nTable = dao.ResourceNetwork.Table() nCls = dao.ResourceNetwork.Columns() ) orm = orm.LeftJoinOnField(nTable, rCls.ResourceId) orm = orm.WherePrefix(rTable, do.ResourceInfo{ AppId: req.AppIds, ResourceId: req.ResourceIds, Region: req.Regions, Zone: req.Zones, ResourceName: req.ResourceNames, Status: req.Statuses, BusinessType: req.Products, Engine: req.Engines, Version: req.Versions, }) orm = orm.WherePrefix(nTable, do.ResourceNetwork{ Vip: req.Vips, VpcId: req.VpcIds, SubnetId: req.SubnetIds, }) // Fuzzy like querying. if req.Key != "" { var ( keyLike = "%" + req.Key + "%" ) whereFormat := fmt.Sprintf( "(`%s`.`%s` like ?) or (`%s`.`%s` like ?) or (`%s`.`%s`like ?) ", rTable, rCls.ResourceId, rTable, rCls.ResourceName, nTable, nCls.Vip, ) orm = orm.Where(whereFormat, keyLike, keyLike, keyLike) } // Resource items. err = orm.Distinct().FieldsPrefix(rTable, "*").Order(req.Order, req.OrderDirection).Limit(req.Offset, req.Limit).Scan(&res.Items) ``` # ORM查询-分组排序过滤 ## `Group/Order` 分组与排序 `Group` 方法用于查询分组, `Order` 方法用于查询排序。使用示例: ```go // SELECT COUNT(*) total,age FROM `user` GROUP BY age g.Model("user").Fields("COUNT(*) total,age").Group("age").All() // SELECT * FROM `student` ORDER BY class asc,course asc,score desc g.Model("student").Order("class asc,course asc,score desc").All() ``` 同时, `goframe` 的 `ORM` 提供了一些常用的排序方法: ```go // 按照指定字段递增排序 func (m *Model) OrderAsc(column string) *Model // 按照指定字段递减排序 func (m *Model) OrderDesc(column string) *Model // 随机排序 func (m *Model) OrderRandom() *Model ``` 使用示例: ```go // SELECT `id`,`title` FROM `article` ORDER BY `created_at` ASC g.Model("article").Fields("id,title").OrderAsc("created_at").All() // SELECT `id`,`title` FROM `article` ORDER BY `views` DESC g.Model("article").Fields("id,title").OrderDesc("views").All() // SELECT `id`,`title` FROM `article` ORDER BY RAND() g.Model("article").Fields("id,title").OrderRandom().All() ``` ## `Having` 条件过滤 `Having` 方法用于查询结果的条件过滤。使用示例: ```go // SELECT COUNT(*) total,age FROM `user` GROUP BY age HAVING total>100 g.Model("user").Fields("COUNT(*) total,age").Group("age").Having("total>100").All() // SELECT * FROM `student` ORDER BY class HAVING score>60 g.Model("student").Order("class").Having("score>?", 60).All() ``` # ORM查询-Union/UnionAll `GoFrame ORM` 组件支持 `Union/UnionAll` 操作, `Union/UnionAll` 操作符用于连接两个以上的 `SELECT` 语句的结果组合到一个结果集合中,关于 `Union/UnionAll` 组合查询的相关介绍可以参考MySQL的官方文档介绍 [https://dev.mysql.com/doc/refman/8.0/en/union.html](https://dev.mysql.com/doc/refman/8.0/en/union.html)。我们可以通过链式操作或者方法操作来实现 `Union/UnionAll` 操作。 ## 方法定义 ```go // Union does "(SELECT xxx FROM xxx) UNION (SELECT xxx FROM xxx) ..." statement. func (c *Core) Union(unions ...*Model) *Model // UnionAll does "(SELECT xxx FROM xxx) UNION ALL (SELECT xxx FROM xxx) ..." statement. func (c *Core) UnionAll(unions ...*Model) *Model ``` ## `Union` 使用 `Union` 操作符,多个 `SELECT` 语句会删除重复的数据。 ```go // 获取默认配置的数据库对象(配置名称为"default") db := g.DB() db.Union( db.Model("user").Where("id", 1), db.Model("user").Where("id", 2), db.Model("user").WhereIn("id", g.Slice{1, 2, 3}), ).OrderDesc("id").All() // (SELECT * FROM `user` WHERE `id`=1) // UNION // (SELECT * FROM `user` WHERE `id`=2) // UNION // (SELECT * FROM `user` WHERE `id` IN (1,2,3) // ORDER BY `id` DESC) ORDER BY `id` DESC ``` 也可以通过 `dao` 链式操作实现: ```go dao.User.Union( dao.User.Where(dao.User.Columns.Id, 1), dao.User.Where(dao.User.Columns.Id, 2), dao.User.WhereIn(dao.User.Columns.Id, g.Slice{1, 2, 3}), ).OrderDesc(dao.User.Columns.Id).All() // (SELECT * FROM `user` WHERE `id`=1) // UNION // (SELECT * FROM `user` WHERE `id`=2) // UNION // (SELECT * FROM `user` WHERE `id` IN (1,2,3) // ORDER BY `id` DESC) ORDER BY `id` DESC ``` ## `UnionAll` 使用 `UnionAll` 操作符,多个 `SELECT` 语句不会删除重复的数据。 ```go db.UnionAll( db.Model("user").Where("id", 1), db.Model("user").Where("id", 2), db.Model(table).WhereIn("id", g.Slice{1, 2, 3}), ).OrderDesc("id").All() // (SELECT * FROM `user` WHERE `id`=1) // UNION ALL // (SELECT * FROM `user` WHERE `id`=2) // UNION ALL // (SELECT * FROM `user` WHERE `id` IN (1,2,3) // ORDER BY `id` DESC) ORDER BY `id` DESC ``` 也可以通过 `dao` 链式操作实现: ```go dao.User.UnionAll( dao.User.Where(dao.User.Columns.Id, 1), dao.User.Where(dao.User.Columns.Id, 2), dao.User.WhereIn(dao.User.Columns.Id, g.Slice{1, 2, 3}), ).OrderDesc(dao.User.Columns.Id).All() // (SELECT * FROM `user` WHERE `id`=1) // UNION ALL // (SELECT * FROM `user` WHERE `id`=2) // UNION ALL // (SELECT * FROM `user` WHERE `id` IN (1,2,3) // ORDER BY `id` DESC) ORDER BY `id` DESC ``` # ORM查询-子查询特性 `ORM` 组件目前支持常见的三种语法的子查询: `Where` 子查询、 `Having` 子查询及 `From` 子查询。 ## `Where` 子查询 我们可以在 `Where` 条件中使用子查询语句,示例: ```go g.Model("orders").Where("amount > ?", g.Model("orders").Fields("AVG(amount)")).Scan(&orders) // SELECT * FROM "orders" WHERE amount > (SELECT AVG(amount) FROM "orders") ``` ## `Having` 子查询 我们可以在 `Having` 条件中使用子查询语句,示例: ```go subQuery := g.Model("users").Fields("AVG(age)").WhereLike("name", "name%") g.Model("users").Fields("AVG(age) as avgage").Group("name").Having("AVG(age) > ?", subQuery).Scan(&results) // SELECT AVG(age) as avgage FROM `users` GROUP BY `name` HAVING AVG(age) > (SELECT AVG(age) FROM `users` WHERE name LIKE "name%") ``` ## `From` 子查询 我们可以在使用 `Model` 方法创建模型时使用子查询语句,示例: ```go g.Model("? as u", g.Model("users").Fields("name", "age")).Where("age", 18).Scan(&users) // SELECT * FROM (SELECT `name`,`age` FROM `users`) as u WHERE `age` = 18 subQuery1 := g.Model("users").Fields("name") subQuery2 := g.Model("pets").Fields("name") g.Model("? as u, ? as p", subQuery1, subQuery2).Scan(&users) // SELECT * FROM (SELECT `name` FROM `users`) as u, (SELECT `name` FROM `pets`) as p ``` # ORM查询-常用操作示例 ## `in` 查询 使用字符串、 `slice` 参数类型。当使用 `slice` 参数类型时,预处理占位符只需要一个 `?` 即可。 ```go // SELECT * FROM user WHERE uid IN(100,10000,90000) g.Model("user").Where("uid IN(?,?,?)", 100, 10000, 90000).All() g.Model("user").Where("uid", g.Slice{100, 10000, 90000}).All() // SELECT * FROM user WHERE gender=1 AND uid IN(100,10000,90000) g.Model("user").Where("gender=? AND uid IN(?)", 1, g.Slice{100, 10000, 90000}).All() // SELECT COUNT(*) FROM user WHERE age in(18,50) g.Model("user").Where("age IN(?,?)", 18, 50).Count() g.Model("user").Where("age", g.Slice{18, 50}).Count() ``` 使用任意 `map` 参数类型。 ```go // SELECT * FROM user WHERE gender=1 AND uid IN(100,10000,90000) g.Model("user").Where(g.Map{ "gender" : 1, "uid" : g.Slice{100,10000,90000}, }).All() ``` 使用 `struct` 参数类型,注意查询条件的顺序和 `struct` 的属性定义顺序有关。 ```go type User struct { Id []int `orm:"uid"` Gender int `orm:"gender"` } // SELECT * FROM `user` WHERE uid IN(100,10000,90000) AND gender=1 g.Model("user").Where(User{ Gender: 1, Id: []int{100, 10000, 90000}, }).All() ``` 为提高易用性,当传递的 `slice` 参数为空或 `nil` 时,查询并不会报错,而是转换为一个 `false` 条件语句。 ```go // SELECT * FROM `user` WHERE 0=1 g.Model("user").Where("uid", g.Slice{}).All() // SELECT * FROM `user` WHERE `uid` IS NULL g.Model("user").Where("uid", nil).All() ``` `ORM` 同时也提供了常用条件方法 `WhereIn/WhereNotIn/WhereOrIn/WhereOrNotIn` 方法,用于常用的 `In` 查询条件过滤。方法定义如下: ```go func (m *Model) WhereIn(column string, in interface{}) *Model func (m *Model) WhereNotIn(column string, in interface{}) *Model func (m *Model) WhereOrIn(column string, in interface{}) *Model func (m *Model) WhereOrNotIn(column string, in interface{}) *Model ``` 使用示例: ```go // SELECT * FROM `user` WHERE (`gender`=1) AND (`type` IN(1,2,3)) g.Model("user").Where("gender", 1).WhereIn("type", g.Slice{1,2,3}).All() // SELECT * FROM `user` WHERE (`gender`=1) AND (`type` NOT IN(1,2,3)) g.Model("user").Where("gender", 1).WhereNotIn("type", g.Slice{1,2,3}).All() // SELECT * FROM `user` WHERE (`gender`=1) OR (`type` IN(1,2,3)) g.Model("user").Where("gender", 1).WhereOrIn("type", g.Slice{1,2,3}).All() // SELECT * FROM `user` WHERE (`gender`=1) OR (`type` NOT IN(1,2,3)) g.Model("user").Where("gender", 1).WhereOrNotIn("type", g.Slice{1,2,3}).All() ``` ## `like ` 查询 ```go // SELECT * FROM `user` WHERE name like '%john%' g.Model("user").Where("name like ?", "%john%").All() // SELECT * FROM `user` WHERE birthday like '1990-%' g.Model("user").Where("birthday like ?", "1990-%").All() ``` 从 `goframe v1.16` 版本开始, `goframe` 的 `ORM` 同时也提供了常用条件方法 `WhereLike/WhereNotLike/WhereOrLike/WhereOrNotLike` 方法,用于常用的 `Like` 查询条件过滤。方法定义如下: ```go func (m *Model) WhereLike(column string, like interface{}) *Model func (m *Model) WhereNotLike(column string, like interface{}) *Model func (m *Model) WhereOrLike(column string, like interface{}) *Model func (m *Model) WhereOrNotLike(column string, like interface{}) *Model ``` 使用示例: ```go // SELECT * FROM `user` WHERE (`gender`=1) AND (`name` LIKE 'john%') g.Model("user").Where("gender", 1).WhereLike("name", "john%").All() // SELECT * FROM `user` WHERE (`gender`=1) AND (`name` NOT LIKE 'john%') g.Model("user").Where("gender", 1).WhereNotLike("name", "john%").All() // SELECT * FROM `user` WHERE (`gender`=1) OR (`name` LIKE 'john%') g.Model("user").Where("gender", 1).WhereOrLike("name", "john%").All() // SELECT * FROM `user` WHERE (`gender`=1) OR (`name` NOT LIKE 'john%') g.Model("user").Where("gender", 1).WhereOrNotLike("name", "john%").All() ``` ## `min/max/avg/sum` 我们直接将统计方法使用在 `Fields` 方法上,例如: ```go // SELECT MIN(score) FROM `user` WHERE `uid`=1 LIMIT 1 g.Model("user").Fields("MIN(score)").Where("uid", 1).Value() // SELECT MAX(score) FROM `user` WHERE `uid`=1 LIMIT 1 g.Model("user").Fields("MAX(score)").Where("uid", 1).Value() // SELECT AVG(score) FROM `user` WHERE `uid`=1 LIMIT 1 g.Model("user").Fields("AVG(score)").Where("uid", 1).Value() // SELECT SUM(score) FROM `user` WHERE `uid`=1 LIMIT 1 g.Model("user").Fields("SUM(score)").Where("uid", 1).Value() ``` 从 `goframe v1.16` 版本开始, `goframe` 的 `ORM` 同时也提供了常用统计方法 `Min/Max/Avg/Sum` 方法,用于常用的字段统计查询。方法定义如下: ```go func (m *Model) Min(column string) (float64, error) func (m *Model) Max(column string) (float64, error) func (m *Model) Avg(column string) (float64, error) func (m *Model) Sum(column string) (float64, error) ``` 上面的示例使用快捷统计方法改造后: ```go // SELECT MIN(`score`) FROM `user` WHERE `uid`=1 LIMIT 1 g.Model("user").Where("uid", 1).Min("score") // SELECT MAX(`score`) FROM `user` WHERE `uid`=1 LIMIT 1 g.Model("user").Where("uid", 1).Max("score") // SELECT AVG(`score`) FROM `user` WHERE `uid`=1 LIMIT 1 g.Model("user").Where("uid", 1).Avg("score") // SELECT SUM(`score`) FROM `user` WHERE `uid`=1 LIMIT 1 g.Model("user").Where("uid", 1).Sum("score") ``` ## `count` 查询 ```go // SELECT COUNT(1) FROM `user` WHERE `birthday`='1990-10-01' g.Model("user").Where("birthday", "1990-10-01").Count() // SELECT COUNT(uid) FROM `user` WHERE `birthday`='1990-10-01' g.Model("user").Fields("uid").Where("birthday", "1990-10-01").Count() ``` 从 `goframe v1.16` 版本开始, `goframe` 的 `ORM` 同时也提供了一个按照字段进行 `Count` 的常用方法 `CountColumn`。方法定义如下: ```go func (m *Model) CountColumn(column string) (int, error) ``` 使用示例: ```go g.Model("user").Where("birthday", "1990-10-01").CountColumn("uid") ``` ## `distinct` 查询 ```go // SELECT DISTINCT uid,name FROM `user` g.Model("user").Fields("DISTINCT uid,name").All() // SELECT COUNT(DISTINCT uid,name) FROM `user` g.Model("user").Fields("DISTINCT uid,name").Count() ``` 从 `goframe v1.16` 版本开始, `goframe` 的 `ORM` 同时也提供了一个字段唯一性过滤标记方法 `Distinct`。方法定义如下: ```go func (m *Model) Distinct() *Model ``` 使用示例: ```go // SELECT COUNT(DISTINCT `name`) FROM `user` g.Model("user").Distinct().CountColumn("name") // SELECT COUNT(DISTINCT uid,name) FROM `user` g.Model("user").Distinct().CountColumn("uid,name") // SELECT DISTINCT group,age FROM `user` g.Model("user").Fields("group, age").Distinct().All() ``` ## `between` 查询 ```go // SELECT * FROM `user` WHERE age between 18 and 20 g.Model("user").Where("age between ? and ?", 18, 20).All() ``` 从 `goframe v1.16` 版本开始, `goframe` 的 `ORM` 同时也提供了常用条件方法 `WhereBetween/WhereNotBetween/WhereOrBetween/WhereOrNotBetween` 方法,用于常用的 `Between` 查询条件过滤。方法定义如下: ```go func (m *Model) WhereBetween(column string, min, max interface{}) *Model func (m *Model) WhereNotBetween(column string, min, max interface{}) *Model func (m *Model) WhereOrBetween(column string, min, max interface{}) *Model func (m *Model) WhereOrNotBetween(column string, min, max interface{}) *Model ``` 使用示例: ```go // SELECT * FROM `user` WHERE (`gender`=0) AND (`age` BETWEEN 16 AND 20) g.Model("user").Where("gender", 0).WhereBetween("age", 16, 20).All() // SELECT * FROM `user` WHERE (`gender`=0) AND (`age` NOT BETWEEN 16 AND 20) g.Model("user").Where("gender", 0).WhereNotBetween("age", 16, 20).All() // SELECT * FROM `user` WHERE (`gender`=0) OR (`age` BETWEEN 16 AND 20) g.Model("user").Where("gender", 0).WhereOrBetween("age", 16, 20).All() // SELECT * FROM `user` WHERE (`gender`=0) OR (`age` NOT BETWEEN 16 AND 20) g.Model("user").Where("gender", 0).WhereOrNotBetween("age", 16, 20).All() ``` ## `null` 查询 `ORM` 提供了常用条件方法 `WhereNull/WhereNotNull/WhereOrNull/WhereOrNotNull` 方法,用于常用的 `Null` 查询条件过滤。方法定义如下: ```go func (m *Model) WhereNull(columns ...string) *Model func (m *Model) WhereNotNull(columns ...string) *Model func (m *Model) WhereOrNull(columns ...string) *Model func (m *Model) WhereOrNotNull(columns ...string) *Model ``` 使用示例: ```go // SELECT * FROM `user` WHERE (`created_at`>'2021-05-01 00:00:00') AND (`inviter` IS NULL) g.Model("user").Where("created_at>?", gtime.New("2021-05-01")).WhereNull("inviter").All() // SELECT * FROM `user` WHERE (`created_at`>'2021-05-01 00:00:00') AND (`inviter` IS NOT NULL) g.Model("user").Where("created_at>?", gtime.New("2021-05-01")).WhereNotNull("inviter").All() // SELECT * FROM `user` WHERE (`created_at`>'2021-05-01 00:00:00') OR (`inviter` IS NULL) g.Model("user").Where("created_at>?", gtime.New("2021-05-01")).WhereOrNull("inviter").All() // SELECT * FROM `user` WHERE (`created_at`>'2021-05-01 00:00:00') OR (`inviter` IS NOT NULL) g.Model("user").Where("created_at>?", gtime.New("2021-05-01")).WhereOrNotNull("inviter").All() ``` 同时,这几个方法的参数支持多个字段输入,例如: ```go // SELECT * FROM `user` WHERE (`created_at`>'2021-05-01 00:00:00') AND (`inviter` IS NULL) AND (`creator` IS NULL) g.Model("user").Where("created_at>?", gtime.New("2021-05-01")).WhereNull("inviter", "creator").All() ``` # 模型关联-动态关联-ScanList `gf` 的 `ORM` 没有采用其他 `ORM` 常见的 `BelongsTo`, `HasOne`, `HasMany`, `ManyToMany` 这样的模型关联设计,这样的关联关系维护较繁琐,例如外键约束、额外的标签备注等,对开发者有一定的心智负担。因此 `gf` 框架不倾向于通过向模型结构体中注入过多复杂的标签内容、关联属性或方法,并一如既往地尝试着简化设计,目标是使得模型关联查询尽可能得易于理解、使用便捷。 **注意** 接下来关于 `gf ORM` 提供的模型关联实现,从 `GoFrame v1.13.6` 版本开始提供,目前属于实验性特性。 那么我们就使用一个例子来介绍 `gf ORM` 提供的模型关联吧。 ### 数据结构 为简化示例,我们这里设计得表都尽可能简单,每张表仅包含3-4个字段,方便阐述关联关系即可。 ``` # 用户表 CREATE TABLE `user` ( uid int(10) unsigned NOT NULL AUTO_INCREMENT, name varchar(45) NOT NULL, PRIMARY KEY (uid) ) ENGINE=InnoDB DEFAULT CHARSET=utf8; # 用户详情 CREATE TABLE `user_detail` ( uid int(10) unsigned NOT NULL AUTO_INCREMENT, address varchar(45) NOT NULL, PRIMARY KEY (uid) ) ENGINE=InnoDB DEFAULT CHARSET=utf8; # 用户学分 CREATE TABLE `user_scores` ( id int(10) unsigned NOT NULL AUTO_INCREMENT, uid int(10) unsigned NOT NULL, score int(10) unsigned NOT NULL, course varchar(45) NOT NULL, PRIMARY KEY (id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8; ``` ### 数据模型 根据表定义,我们可以得知: 1. 用户表与用户详情是 `1:1` 关系。 2. 用户表与用户学分是 `1:N` 关系。 3. 这里并没有演示 `N:N` 的关系,因为相比较于 `1:N` 的查询只是多了一次关联、或者一次查询,最终处理方式和 `1:N` 类似。 那么 `Golang` 的模型可定义如下: ```go // 用户表 type EntityUser struct { Uid int `orm:"uid"` Name string `orm:"name"` } // 用户详情 type EntityUserDetail struct { Uid int `orm:"uid"` Address string `orm:"address"` } // 用户学分 type EntityUserScores struct { Id int `orm:"id"` Uid int `orm:"uid"` Score int `orm:"score"` Course string `orm:"course"` } // 组合模型,用户信息 type Entity struct { User *EntityUser UserDetail *EntityUserDetail UserScores []*EntityUserScores } ``` 其中, `EntityUser`, `EntityUserDetail`, `EntityUserScores` 分别对应的是用户表、用户详情、用户学分数据表的数据模型。 `Entity` 是一个组合模型,对应的是一个用户的所有详细信息。 ### 数据写入 写入数据时涉及到简单的数据库事务即可。 ```go err := g.DB().Transaction(ctx, func(ctx context.Context, tx gdb.TX) error { r, err := tx.Model("user").Save(EntityUser{ Name: "john", }) if err != nil { return err } uid, err := r.LastInsertId() if err != nil { return err } _, err = tx.Model("user_detail").Save(EntityUserDetail{ Uid: int(uid), Address: "Beijing DongZhiMen #66", }) if err != nil { return err } _, err = tx.Model("user_scores").Save(g.Slice{ EntityUserScores{Uid: int(uid), Score: 100, Course: "math"}, EntityUserScores{Uid: int(uid), Score: 99, Course: "physics"}, }) return err }) ``` ### 数据查询 #### 单条数据记录 查询单条模型数据比较简单,直接使用 `Scan` 方法即可,该方法会自动识别绑定查询结果到单个对象属性还是数组对象属性中。例如: ```go // 定义用户列表 var user Entity // 查询用户基础数据 // SELECT * FROM `user` WHERE `name`='john' err := g.Model("user").Scan(&user.User, "name", "john") if err != nil { return err } // 查询用户详情数据 // SELECT * FROM `user_detail` WHERE `uid`=1 err := g.Model("user_detail").Scan(&user.UserDetail, "uid", user.User.Uid) // 查询用户学分数据 // SELECT * FROM `user_scores` WHERE `uid`=1 err := g.Model("user_scores").Scan(&user.UserScores, "uid", user.User.Uid) ``` 该方法在之前的章节中已经有介绍,因此这里不再赘述。 #### 多条数据记录 查询多条数据记录并绑定数据到数据模型数组中,需要使用到 `ScanList` 方法,该方法会需要用户指定结果字段与模型属性的关系,随后底层会遍历数组并自动执行数据绑定。例如: ```go // 定义用户列表 var users []Entity // 查询用户基础数据 // SELECT * FROM `user` err := g.Model("user").ScanList(&users, "User") // 查询用户详情数据 // SELECT * FROM `user_detail` WHERE `uid` IN(1,2) err := g.Model("user_detail"). WhereIn("uid", gdb.ListItemValuesUnique(users, "User", "Uid")). ScanList(&users, "UserDetail", "User", "uid:Uid") // 查询用户学分数据 // SELECT * FROM `user_scores` WHERE `uid` IN(1,2) err := g.Model("user_scores"). WhereIn("uid", gdb.ListItemValuesUnique(users, "User", "Uid")). ScanList(&users, "UserScores", "User", "uid:Uid") ``` 这其中涉及到两个比较重要的方法: **1\. `ScanList`** 方法定义: ```go // ScanList converts to struct slice which contains other complex struct attributes. // Note that the parameter should be type of *[]struct/*[]*struct. // Usage example: // // type Entity struct { // User *EntityUser // UserDetail *EntityUserDetail // UserScores []*EntityUserScores // } // var users []*Entity // or // var users []Entity // // ScanList(&users, "User") // ScanList(&users, "UserDetail", "User", "uid:Uid") // ScanList(&users, "UserScores", "User", "uid:Uid") // The parameters "User"/"UserDetail"/"UserScores" in the example codes specify the target attribute struct // that current result will be bound to. // The "uid" in the example codes is the table field name of the result, and the "Uid" is the relational // struct attribute name. It automatically calculates the HasOne/HasMany relationship with given // parameter. // See the example or unit testing cases for clear understanding for this function. func (m *Model) ScanList(listPointer interface{}, attributeName string, relation ...string) (err error) ``` 该方法用于将查询到的数组数据绑定到指定的列表上,例如: - `ScanList(&users, "User")` 表示将查询到的用户信息数组数据绑定到 `users` 列表中每一项的 `User` 属性上。 - `ScanList(&users, "UserDetail", "User", "uid:Uid")` 表示将查询到用户详情数组数据绑定到 `users` 列表中每一项的 `UserDetail` 属性上,并且和另一个 `User` 对象属性通过 `uid:Uid` 的 `字段:属性` 关联,内部将会根据这一关联关系自动进行数据绑定。其中 `uid:Uid` 前面的 `uid` 表示查询结果字段中的 `uid` 字段,后面的 `Uid` 表示目标关联对象中的 `Uid` 属性。 - `ScanList(&users, "UserScores", "User", "uid:Uid")` 表示将查询到用户详情数组数据绑定到 `users` 列表中每一项的 `UserScores` 属性上,并且和另一个 `User` 对象属性通过 `uid:Uid` 的 `字段:属性` 关联,内部将会根据这一关联关系自动进行数据绑定。由于 `UserScores` 是一个数组类型 `[]*EntityUserScores`,因此该方法内部可以自动识别到 `User` 到 `UserScores` 其实是 `1:N` 的关系,自动完成数据绑定。 需要提醒的是,如果关联数据中对应的关联属性数据不存在,那么该属性不会被初始化并将保持 `nil`。 **2\. `ListItemValues/ListItemValuesUnique`** 方法定义: ```go // ListItemValues retrieves and returns the elements of all item struct/map with key . // Note that the parameter should be type of slice which contains elements of map or struct, // or else it returns an empty slice. // // The parameter supports types like: // []map[string]interface{} // []map[string]sub-map // []struct // []struct:sub-struct // Note that the sub-map/sub-struct makes sense only if the optional parameter is given. func ListItemValues(list interface{}, key interface{}, subKey ...interface{}) (values []interface{}) // ListItemValuesUnique retrieves and returns the unique elements of all struct/map with key . // Note that the parameter should be type of slice which contains elements of map or struct, // or else it returns an empty slice. // See gutil.ListItemValuesUnique. func ListItemValuesUnique(list interface{}, key string, subKey ...interface{}) []interface{} ``` `ListItemValuesUnique` 与 `ListItemValues` 方法的区别在于过滤重复的返回值,保证返回的列表数据中不带有重复值。这两个方法都会在当给定的列表中包含 `struct`/ `map` 数据项时,用于获取指定属性/键名的数据值,构造成数组 `[]interface{}` 返回。示例: - `gdb.ListItemValuesUnique(users, "Uid")` 用于获取 `users` 数组中,每一个 `Uid` 属性,构造成 `[]interface{}` 数组返回。这里以便根据 `uid` 构造成 `SELECT...IN...` 查询。 - `gdb.ListItemValuesUnique(users, "User", "Uid")` 用于获取 `users` 数组中,每一个 `User` 属性项中的 `Uid` 属性,构造成 `[]interface{}` 数组返回。这里以便根据 `uid` 构造成 `SELECT...IN...` 查询。 # 模型关联-静态关联-With特性 ## 一、设计背景 大家都知道易用性和易维护性一直是 `goframe` 一直努力建设的,也是 `goframe` 有别其他框架和组件比较大的一点差异。 `goframe` 没有采用其他 `ORM` 常见的 `BelongsTo`,`HasOne`,`HasMany`,`ManyToMany` 这样的模型关联设计,这样的关联关系维护较繁琐,例如外键约束、额外的标签备注等,对开发者有一定的心智负担。因此框架不倾向于通过向模型结构体中注入过多复杂的标签内容、关联属性或方法,并一如既往地尝试着简化设计,目标是使得模型关联查询尽可能得易于理解、使用便捷。因此在之前推出了 `ScanList` 方案,建议大家在继续了解 `With` 特性之前先了解一下 [模型关联-动态关联-ScanList](./gdb-chaining-relation-scan-list.md)。 经过一系列的项目实践,我们发现 `ScanList` 虽然从运行时业务逻辑的角度来维护了模型关联关系,但是这种关联关系维护也不如期望的简便。因此,我们继续改进推出了可以通过模型简单维护关联关系的 `With` 模型关联特性,当然,这种特性仍然致力于提升整体框架的易用性和维护性,可以把 `With` 特性看做 `ScanList` 与模型关联关系维护的一种结合和改进。 **注意** `With` 特性目前属于实验性特性。 ## 二、举个例子 我们先来一个简单的示例,便于大家更好理解 `With` 特性,该示例来自于之前的 `ScanList` 章节的相同示例,改进版。 ### 1、数据结构 ```sql # 用户表 CREATE TABLE `user` ( id int(10) unsigned NOT NULL AUTO_INCREMENT, name varchar(45) NOT NULL, PRIMARY KEY (id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8; # 用户详情 CREATE TABLE `user_detail` ( uid int(10) unsigned NOT NULL AUTO_INCREMENT, address varchar(45) NOT NULL, PRIMARY KEY (uid) ) ENGINE=InnoDB DEFAULT CHARSET=utf8; # 用户学分 CREATE TABLE `user_scores` ( id int(10) unsigned NOT NULL AUTO_INCREMENT, uid int(10) unsigned NOT NULL, score int(10) unsigned NOT NULL, PRIMARY KEY (id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8; ``` ### 2、数据结构 根据表定义,我们可以得知: 1. 用户表与用户详情是 `1:1` 关系。 2. 用户表与用户学分是 `1:N` 关系。 3. 这里并没有演示 `N:N` 的关系,因为相比较于 `1:N` 的查询只是多了一次关联、或者一次查询,最终处理方式和 `1:N` 类似。 那么 `Golang` 的模型可定义如下: ```go // 用户详情 type UserDetail struct { g.Meta `orm:"table:user_detail"` Uid int `json:"uid"` Address string `json:"address"` } // 用户学分 type UserScores struct { g.Meta `orm:"table:user_scores"` Id int `json:"id"` Uid int `json:"uid"` Score int `json:"score"` } // 用户信息 type User struct { g.Meta `orm:"table:user"` Id int `json:"id"` Name string `json:"name"` UserDetail *UserDetail `orm:"with:uid=id"` UserScores []*UserScores `orm:"with:uid=id"` } ``` ### 3、数据写入 为简化示例,我们这里创建 `5` 条用户数据,采用事务操作方式写入: - 用户信息, `id` 为 `1-5`, `name` 为 `name_1` 到 `name_5`。 - 同时创建 `5` 条用户详情数据, `address` 数据为 `address_1` 到 `address_5`。 - 每个用户创建 `5` 条学分信息,学分为 `1-5`。 ```go g.DB().Transaction(ctx, func(ctx context.Context, tx gdb.TX) error { for i := 1; i <= 5; i++ { // User. user := User{ Name: fmt.Sprintf(`name_%d`, i), } lastInsertId, err := g.Model(user).Data(user).OmitEmpty().InsertAndGetId() if err != nil { return err } // Detail. userDetail := UserDetail{ Uid: int(lastInsertId), Address: fmt.Sprintf(`address_%d`, lastInsertId), } _, err = g.Model(userDetail).Data(userDetail).OmitEmpty().Insert() if err != nil { return err } // Scores. for j := 1; j <= 5; j++ { userScore := UserScores{ Uid: int(lastInsertId), Score: j, } _, err = g.Model(userScore).Data(userScore).OmitEmpty().Insert() if err != nil { return err } } } return nil }) ``` 执行成功后,数据库数据如下: ```text mysql> show tables; +----------------+ | Tables_in_test | +----------------+ | user | | user_detail | | user_score | +----------------+ 3 rows in set (0.01 sec) mysql> select * from `user`; +----+--------+ | id | name | +----+--------+ | 1 | name_1 | | 2 | name_2 | | 3 | name_3 | | 4 | name_4 | | 5 | name_5 | +----+--------+ 5 rows in set (0.01 sec) mysql> select * from `user_detail`; +-----+-----------+ | uid | address | +-----+-----------+ | 1 | address_1 | | 2 | address_2 | | 3 | address_3 | | 4 | address_4 | | 5 | address_5 | +-----+-----------+ 5 rows in set (0.00 sec) mysql> select * from `user_score`; +----+-----+-------+ | id | uid | score | +----+-----+-------+ | 1 | 1 | 1 | | 2 | 1 | 2 | | 3 | 1 | 3 | | 4 | 1 | 4 | | 5 | 1 | 5 | | 6 | 2 | 1 | | 7 | 2 | 2 | | 8 | 2 | 3 | | 9 | 2 | 4 | | 10 | 2 | 5 | | 11 | 3 | 1 | | 12 | 3 | 2 | | 13 | 3 | 3 | | 14 | 3 | 4 | | 15 | 3 | 5 | | 16 | 4 | 1 | | 17 | 4 | 2 | | 18 | 4 | 3 | | 19 | 4 | 4 | | 20 | 4 | 5 | | 21 | 5 | 1 | | 22 | 5 | 2 | | 23 | 5 | 3 | | 24 | 5 | 4 | | 25 | 5 | 5 | +----+-----+-------+ 25 rows in set (0.00 sec) ``` ### 4、数据查询 新的 `With` 特性下,数据查询相当简便,例如,我们查询一条数据: ```go // 重新声明一下,防止大家上下来回拉动 // 用户详情 type UserDetail struct { g.Meta `orm:"table:user_detail"` Uid int `json:"uid"` Address string `json:"address"` } // 用户学分 type UserScores struct { g.Meta `orm:"table:user_scores"` Id int `json:"id"` Uid int `json:"uid"` Score int `json:"score"` } // 用户信息 type User struct { g.Meta `orm:"table:user"` Id int `json:"id"` Name string `json:"name"` UserDetail *UserDetail `orm:"with:uid=id"` UserScores []*UserScores `orm:"with:uid=id"` } var user *User // WithAll 会查询带有with tag的字段,这个例子中,将会查询 UserDetail结构体对应的表和 UserScores结构体对应的表 g.Model(tableUser).WithAll().Where("id", 3).Scan(&user) ``` 以上语句将会查询到用户ID为 `3` 的用户信息、用户详情以及用户学分信息,以上语句将会在数据库中自动执行以下 `SQL` 语句: ```text 2021-05-02 22:29:52.634 [DEBU] [ 2 ms] [default] SHOW FULL COLUMNS FROM `user` 2021-05-02 22:29:52.635 [DEBU] [ 1 ms] [default] SELECT * FROM `user` WHERE `id`=3 LIMIT 1 2021-05-02 22:29:52.636 [DEBU] [ 1 ms] [default] SHOW FULL COLUMNS FROM `user_detail` 2021-05-02 22:29:52.637 [DEBU] [ 1 ms] [default] SELECT `uid`,`address` FROM `user_detail` WHERE `uid`=3 LIMIT 1 2021-05-02 22:29:52.643 [DEBU] [ 6 ms] [default] SHOW FULL COLUMNS FROM `user_score` 2021-05-02 22:29:52.644 [DEBU] [ 0 ms] [default] SELECT `id`,`uid`,`score` FROM `user_score` WHERE `uid`=3 ``` 执行后,通过 `g.Dump(user)` 打印的用户信息如下: ```js { Id: 3, Name: "name_3", UserDetail: { Uid: 3, Address: "address_3", }, UserScores: [ { Id: 11, Uid: 3, Score: 1, }, { Id: 12, Uid: 3, Score: 2, }, { Id: 13, Uid: 3, Score: 3, }, { Id: 14, Uid: 3, Score: 4, }, { Id: 15, Uid: 3, Score: 5, }, ], } ``` ### 5、列表查询 我们来一个通过 `With` 特性查询列表的示例: ```go var users []*User // With(UserDetail{}) 只查询User结构体中的UserDetail对应的表 g.Model(users).With(UserDetail{}).Where("id>?", 3).Scan(&users) ``` 执行后,通过 `g.Dump(users)` 打印用户数据如下: ```js [ { Id: 4, Name: "name_4", UserDetail: { Uid: 4, Address: "address_4", }, UserScores: [], }, { Id: 5, Name: "name_5", UserDetail: { Uid: 5, Address: "address_5", }, UserScores: [], }, ] ``` ### 6、条件与排序 通过 `With` 特性关联时可以指定关联的额外条件,以及在多数据结果下指定排序规则。例如: ```go type User struct { g.Meta `orm:"table:user"` Id int `json:"id"` Name string `json:"name"` UserDetail *UserDetail `orm:"with:uid=id, where:uid > 3"` UserScores []*UserScores `orm:"with:uid=id, where:score>1 and score<5, order:score desc"` } ``` 通过 `orm` 标签中的 `where` 子标签以及 `order` 子标签指定额外关联条件体积排序规则。 ### 7、`unscoped`标签 在`with`结构体标签中,支持使用`unscoped`特性,例如: ```go type User struct { g.Meta `orm:"table:user"` Id int `json:"id"` Name string `json:"name"` UserDetail *UserDetail `orm:"with:uid=id, unscoped:true"` UserScores []*UserScore `orm:"with:uid=id, unscoped:true"` } ``` ## 三、详细说明 想必一定对上面的某些使用比较好奇,比如 `gmeta` 包、比如 `WithAll` 方法、比如 `orm` 标签中的 `with` 语句、比如 `Model` 方法给定 `struct` 参数识别数据表名等等,那这就对啦,接下来,我们详细聊聊吧。 ### 1、 `gmeta` 包 我们可以看到在上面的结构体数据结构中都使用 `embed` 方式嵌入了一个 `g.Meta` 结构体,例如: ```go type UserDetail struct { g.Meta `orm:"table:user_detail"` Uid int `json:"uid"` Address string `json:"address"` } ``` 其实在 `GoFrame` 框架中有很多这种小组件包用以实现特定的便捷功能。 `gmeta` 包的作用主要用于嵌入到用户自定义的结构体中,并且通过标签的形式给 `gmeta` 包的结构体(例如这里的 `g.Meta`)打上自定义的标签内容(列如这里的 `` `orm:"table:user_detail"` ``),并在运行时可以特定方法动态获取这些自定义的标签内容。详情请参考章节: [元数据-gmeta](../components/util-gmeta.md) 因此,这里嵌入 `g.Meta` 的目的是为了标记该结构体关联的数据表名称。 ### 2、模型关联指定 在如下结构体中: ```go type User struct { g.Meta `orm:"table:user"` Id int `json:"id"` Name string `json:"name"` UserDetail *UserDetail `orm:"with:uid=id"` UserScores []*UserScore `orm:"with:uid=id"` } ``` 我们通过给指定的结构体属性绑定 `orm` 标签,并在 `orm` 标签中通过 `with` 语句指定当前结构体(数据表)与目标结构体(数据表)的关联关系, `with` 语句的语法如下: ```text with:当前属性对应表关联字段=当前结构体对应数据表关联字段 ``` 并且字段名称 **忽略大小写以及特殊字符匹配**,例如以下形式的关联关系都是能够自动识别的: ```text with:UID=ID with:Uid=Id with:U_ID=id ``` 如果两个表的关联字段都是同一个名称,那么也可以直接写一个即可,例如: ```text with:uid ``` 在本示例中, `UserDetail` 属性对应的数据表为 `user_detail`, `UserScores` 属性对应的数据表为 `user_score`,两者与当前 `User` 结构体对应的表 `user` 都是使用 `uid` 进行关联,并且目标关联的 `user` 表的对应字段为 `id`。 ### 3、 `With/WithAll` #### 1)基本介绍 默认情况下,即使我们的结构体属性中的 `orm` 标签带有 `with` 语句, `ORM` 组件并不会默认启用 `With` 特性进行关联查询,而是需要依靠 `With/WithAll` 方法启用该查询特性。 - `With`:指定启用关联查询的数据表,通过给定的属性对象指定。 - `WithAll`:启用操作对象中所有带有 `with` 语句的属性结构体关联查询。 在我们本示例中,使用的是 `WithAll` 方法,因此自动启用了 `User` 表中的所有属性的模型关联查询,只要属性结构体关联了数据表,并且 `orm` 标签中带有 `with` 语句,那么都将会自动查询数据并根据模型结构的关联关系进行数据绑定。假如我们只启用某部分关联查询,并不启用全部属性模型的关联查询,那么可以使用 `With` 方法来指定。并且 `With` 方法可以指定启用多个关联模型的自动查询,在本示例中的 `WithAll` 就相当于: ```go var user *User g.Model(tableUser).With(UserDetail{}, UserScore{}).Where("id", 3).Scan(&user) ``` 也可以这样: ```go var user *User g.Model(tableUser).With(User{}.UserDetail, User{}.UserScore).Where("id", 3).Scan(&user) ``` #### 2)仅关联用户详情模型 假如我们只需要查询用户详情,并不需要查询用户学分,那么我们可以使用 `With` 方法来启用指定对象对应数据表的关联查询,例如: ```go var user *User g.Model(tableUser).With(UserDetail{}).Where("id", 3).Scan(&user) ``` 也可以这样: ```go var user *User g.Model(tableUser).With(User{}.UserDetail).Where("id", 3).Scan(&user) ``` 执行后,通过 `g.Dump(user)` 打印用户数据如下: ```js { "id": 3, "name": "name_3", "UserDetail": { "uid": 3, "address": "address_3" }, "UserScores": [] } ``` #### 3)仅关联用户学分模型 我们也可以只关联查询用户学分信息,例如: ```go var user *User g.Model(tableUser).With(UserScore{}).Where("id", 3).Scan(&user) ``` 也可以这样: ```go var user *User g.Model(tableUser).With(User{}.UserScore).Where("id", 3).Scan(&user) ``` 执行后,通过 `g.Dump(user)` 打印用户数据如下: ```js { "id": 3, "name": "name_3", "UserDetail": null, "UserScores": [ { "id": 11, "uid": 3, "score": 1 }, { "id": 12, "uid": 3, "score": 2 }, { "id": 13, "uid": 3, "score": 3 }, { "id": 14, "uid": 3, "score": 4 }, { "id": 15, "uid": 3, "score": 5 } ] } ``` #### 4)不关联任何模型查询 假如,我们不需要关联查询,那么更简单,例如: ```go var user *User g.Model(tableUser).Where("id", 3).Scan(&user) ``` 执行后,通过 `g.Dump(user)` 打印用户数据如下: ```js { "id": 3, "name": "name_3", "UserDetail": null, "UserScores": [] } ``` ## 四、使用限制 ### 1、字段查询与过滤 可以看到,在我们上面的示例中,并没有指定查询的字段,但是在打印的 `SQL` 日志中可以看到查询语句不是简单的 `SELECT *` 而是执行了具体的字段查询。在 `With` 特性下,将会自动按照关联模型对象的属性进行查询,属性的名称将会与数据表的字段做自动映射,并且会自动过滤掉无法自动映射的字段查询。 所以,在 `With` 特性下,我们无法做到仅查询属性中对应的某几个字段。如果需要实现仅查询并赋值某几个字段,建议对 `model` 数据结构按照业务场景进行裁剪,创建满足特定业务场景的数据结构,而不是使用一个数据结构满足不同的多个场景。 我们来一个示例更好说明。假如我们有一个实体对象数据结构 `Content`,一个常见的 `CMS` 系统的内容模型如下,该模型与数据表字段一一对应: ```go type Content struct { Id uint `orm:"id,primary" json:"id"` // 自增ID Key string `orm:"key" json:"key"` // 唯一键名,用于程序硬编码,一般不常用 Type string `orm:"type" json:"type"` // 内容模型: topic, ask, article等,具体由程序定义 CategoryId uint `orm:"category_id" json:"category_id"` // 栏目ID UserId uint `orm:"user_id" json:"user_id"` // 用户ID Title string `orm:"title" json:"title"` // 标题 Content string `orm:"content" json:"content"` // 内容 Sort uint `orm:"sort" json:"sort"` // 排序,数值越低越靠前,默认为添加时的时间戳,可用于置顶 Brief string `orm:"brief" json:"brief"` // 摘要 Thumb string `orm:"thumb" json:"thumb"` // 缩略图 Tags string `orm:"tags" json:"tags"` // 标签名称列表,以JSON存储 Referer string `orm:"referer" json:"referer"` // 内容来源,例如github/gitee Status uint `orm:"status" json:"status"` // 状态 0: 正常, 1: 禁用 ReplyCount uint `orm:"reply_count" json:"reply_count"` // 回复数量 ViewCount uint `orm:"view_count" json:"view_count"` // 浏览数量 ZanCount uint `orm:"zan_count" json:"zan_count"` // 赞 CaiCount uint `orm:"cai_count" json:"cai_count"` // 踩 CreatedAt *gtime.Time `orm:"created_at" json:"created_at"` // 创建时间 UpdatedAt *gtime.Time `orm:"updated_at" json:"updated_at"` // 修改时间 } ``` 内容的列表页又不需要展示这么详细的内容,特别是其中的 `Content` 字段非常大,我们列表页只需要查询几个字段而已。那么我们可以单独定义一个用于列表的返回数据结构(字段裁剪),而不是直接使用数据表实体对象数据结构。例如: ```go type ContentListItem struct { Id uint `json:"id"` // 自增ID CategoryId uint `json:"category_id"` // 栏目ID UserId uint `json:"user_id"` // 用户ID Title string `json:"title"` // 标题 CreatedAt *gtime.Time `json:"created_at"` // 创建时间 UpdatedAt *gtime.Time `json:"updated_at"` // 修改时间 } ``` ### 2、必须存在关联字段属性 由于 `With` 特性是通过识别数据结构关联关系,并自动执行多条SQL查询来实现的,因此关联的字段也必须作为对象的属性便于关联字段值得自动获取。简单地讲, `with` 标签中的字段必须存在于关联对象的属性上。 ## 五、递归关联 如果关联的模型属性也带有 `with` 标签,那么将会递归执行关联查询。 `With` 特性支持无限层级的递归关联。以下示例,仅供参考: ```go // 用户详情 type UserDetail struct { g.Meta `orm:"table:user_detail"` Uid int `json:"uid"` Address string `json:"address"` } // 用户学分 - 必修课 type UserScoresRequired struct { g.Meta `orm:"table:user_scores"` Id int `json:"id"` Uid int `json:"uid"` Score int `json:"score"` } // 用户学分 - 选修课 type UserScoresOptional struct { g.Meta `orm:"table:user_scores"` Id int `json:"id"` Uid int `json:"uid"` Score int `json:"score"` } // 用户学分 type UserScores struct { g.Meta `orm:"table:user_scores"` Id int `json:"id"` Uid int `json:"uid"` Required []UserScoresRequired `orm:"with:id, where:type=1"` Optional []UserScoresOptional `orm:"with:id, where:type=2"` } // 用户信息 type User struct { g.Meta `orm:"table:user"` Id int `json:"id"` Name string `json:"name"` UserDetail *UserDetail `orm:"with:uid=id"` UserScores []*UserScores `orm:"with:uid=id"` } ``` ## 六、模型示例 根据当前的数据表,这里给了更多的一些模型编写示例供大家参考。 ### 1、关联模型嵌套 ```go type UserDetail struct { g.Meta `orm:"table:user_detail"` Uid int `json:"uid"` Address string `json:"address"` } type UserScores struct { g.Meta `orm:"table:user_scores"` Id int `json:"id"` Uid int `json:"uid"` Score int `json:"score"` } type User struct { g.Meta `orm:"table:user"` *UserDetail `orm:"with:uid=id"` Id int `json:"id"` Name string `json:"name"` UserScores []*UserScores `orm:"with:uid=id"` } ``` 嵌套的模型也支持嵌套,只要是结构体嵌套的都支持自动数据赋值。例如: ```go type UserDetail struct { Uid int `json:"uid"` Address string `json:"address"` } type UserDetailEmbedded struct { UserDetail } type UserScores struct { Id int `json:"id"` Uid int `json:"uid"` Score int `json:"score"` } type User struct { *UserDetailEmbedded `orm:"with:uid=id"` Id int `json:"id"` Name string `json:"name"` UserScores []*UserScores `orm:"with:uid=id"` } ``` ### 2、基础模型嵌套 ```go type UserDetail struct { g.Meta `orm:"table:user_detail"` Uid int `json:"uid"` Address string `json:"address"` } type UserScores struct { g.Meta `orm:"table:user_scores"` Id int `json:"id"` Uid int `json:"uid"` Score int `json:"score"` } type UserEmbedded struct { Id int `json:"id"` Name string `json:"name"` } type User struct { g.Meta `orm:"table:user"` UserEmbedded *UserDetail `orm:"with:uid=id"` UserScores []*UserScores `orm:"with:uid=id"` } ``` ### 3、模型不带 `meta` 信息 模型中的 `meta` 结构重要的是指定数据表名称,当不存在 `meta` 信息时,查询的数据表将会自动以结构体名称的 `CaseSnake` 名称。例如, `UserDetail` 将会自动使用 `user_detail` 数据表名称, `UserScores` 将会自动使用 `user_scores` 数据表名称。 ```go type UserDetail struct { Uid int `json:"uid"` Address string `json:"address"` } type UserScores struct { Id int `json:"id"` Uid int `json:"uid"` Score int `json:"score"` } type User struct { *UserDetail `orm:"with:uid=id"` Id int `json:"id"` Name string `json:"name"` UserScores []*UserScores `orm:"with:uid=id"` } ``` ## 七、后续改进 - 目前 `With` 特性仅实现了查询操作,还不支持写入更新等操作。 # ORM链式操作-时间维护 ## 基本介绍 **注意** 需要注意,该特性仅对链式操作有效。 `gdb` 模块支持对数据记录的写入、更新、删除时间自动填充,提高开发维护效率。为了便于时间字段名称、类型的统一维护,如果使用该特性,我们约定: - 字段的类型可以为时间类型、数字整型或者布尔型,如: `date`, `datetime`, `timestamp`, `int`, `uint`, `big int`, `bool`等。 - 字段的名称支持自定义设置,默认名称约定为: - `created_at` 用于记录创建时更新,仅会写入一次。 - `updated_at` 用于记录修改时更新,每次记录变更时更新。 - `deleted_at` 用于记录的软删除特性,只有当记录删除时会写入一次。 字段名称不区分大小写,也会忽略特殊字符,例如 `CreatedAt`, `UpdatedAt`, `DeletedAt` 也是支持的。 ## 特性配置 时间字段名称可以通过配置文件进行自定义修改,并可使用 `timeMaintainDisabled` 配置在数据库实例上完整关闭该特性。 在配置文件中对应配置项: ```yaml database: default: # 分组名称,可自定义,默认为default createdAt: "created_at" # (可选)自动创建时间字段名称 updatedAt: "updated_time" # (可选)自动更新时间字段名称 deletedAt: "is_deleted" # (可选)软删除时间字段名称 timeMaintainDisabled: false # (可选)是否完全关闭时间更新特性,为true时CreatedAt/UpdatedAt/DeletedAt都将失效 ``` **提示** 特别是针对历史项目,本身已经存在不一样的时间字段名称时,可以通过配置项灵活配置时间字段名称。 完整的数据库配置请参考 [ORM使用配置](./gdb-config.md) 章节。 ## 特性启用 当数据表包含 `created_at`、 `updated_at`、 `deleted_at` 任意一个或多个字段时,或者包含配置文件中对应的配置字段时,该特性自动启用。 # 时间维护-基本使用 以下的示例中,我们默认示例中的数据表均包含了`created_at`、 `updated_at`、 `deleted_at`这3个字段,并且字段类型为`datetime`。 ## 示例SQL 这是后续示例代码中用到的`MySQL`表结构。 ```sql CREATE TABLE `user` ( `id` int(10) unsigned NOT NULL AUTO_INCREMENT, `name` varchar(45) NOT NULL, `status` tinyint DEFAULT 0, `created_at` datetime DEFAULT NULL, `updated_at` datetime DEFAULT NULL, `deleted_at` datetime DEFAULT NULL, PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8; CREATE TABLE IF NOT EXISTS `user_detail` ( `id` int(10) unsigned NOT NULL AUTO_INCREMENT, `address` varchar(45) NOT NULL, PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8; ``` **提示** - 如果选择使用时间字段类型,那么需要将该字段设置为允许为`NULL`,这样软删除才能起作用。 - 如果尝试测试查看`ORM`操作执行的`SQL`语句,建议打开`debug`模式(相关文档:[调试模式](./gdb-senior-debugging.md)),`SQL`语句将会自动打印到日志输出。 ## `created_at` 写入时间 在执行 `Insert/InsertIgnore/BatchInsert/BatchInsertIgnore` 方法时自动写入该时间,随后的更新/删除操作不会引起`created_at`字段内容的改变。 **注意** 需要注意的是 `Replace` 方法也会更新该字段,因为该操作相当于删除已存在的旧数据并重新写一条数据。 ```go // INSERT INTO `user`(`name`,`created_at`,`updated_at`) VALUES('john', `2020-06-06 21:00:00`, `2020-06-06 21:00:00`) g.Model("user").Data(g.Map{"name": "john"}).Insert() // INSERT IGNORE INTO `user`(`id`,`name`,`created_at`,`updated_at`) VALUES(10000,'john', `2020-06-06 21:00:00`, `2020-06-06 21:00:00`) g.Model("user").Data(g.Map{"id": 10000, "name": "john"}).InsertIgnore() // REPLACE INTO `user`(`id`,`name`,`created_at`,`updated_at`) VALUES(10000,'john', `2020-06-06 21:00:00`, `2020-06-06 21:00:00`) g.Model("user").Data(g.Map{"id": 10000, "name": "john"}).Replace() // INSERT INTO `user`(`id`,`name`,`created_at`,`updated_at`) VALUES(10001,'john', `2020-06-06 21:00:00`, `2020-06-06 21:00:00`) ON DUPLICATE KEY UPDATE `id`=VALUES(`id`),`name`=VALUES(`name`),`updated_at`=VALUES(`updated_at`) g.Model("user").Data(g.Map{"id": 10001, "name": "john"}).Save() ``` ## `deleted_at` 数据软删除 当软删除存在时(即`deleted_at`字段存在时),所有的查询语句都将会自动加上 `deleted_at` 的条件。 ```go // UPDATE `user` SET `deleted_at`='2020-06-06 21:00:00' WHERE id=10 AND `deleted_at` IS NULL g.Model("user").Where("id", 10).Delete() ``` 查询的时候会发生一些变化,例如: ```go // SELECT * FROM `user` WHERE id>1 AND `deleted_at` IS NULL g.Model("user").Where("id>?", 1).All() ``` 可以看到当数据表中存在 `deleted_at` 字段时,所有涉及到该表的查询操作都将自动加上 `deleted_at IS NULL` 的条件。 ## `updated_at` 更新时间 在执行 `Save/Update` 方法时自动写入该时间。 **注意** 需要注意的是 `Replace` 方法也会更新该字段,因为该操作相当于删除已存在的旧数据并重新写一条数据。 ```go // UPDATE `user` SET `name`='john guo',`updated_at`='2020-06-06 21:00:00' WHERE name='john' AND `deleted_at` IS NULL g.Model("user").Data(g.Map{"name" : "john guo"}).Where("name", "john").Update() // UPDATE `user` SET `status`=1,`updated_at`='2020-06-06 21:00:00' WHERE `deleted_at` IS NULL ORDER BY `id` ASC LIMIT 10 g.Model("user").Data("status", 1).OrderAsc("id").Limit(10).Update() // INSERT INTO `user`(`id`,`name`,`update_at`) VALUES(1,'john guo','2020-12-29 20:16:14') ON DUPLICATE KEY UPDATE `id`=VALUES(`id`),`name`=VALUES(`name`),`update_at`=VALUES(`update_at`) g.Model("user").Data(g.Map{"id": 1, "name": "john guo"}).Save() ``` ## 联表查询的场景 如果关联查询的几个表都启用了软删除特性时,会发生以下这种情况,即条件语句中会增加所有相关表的软删除时间判断。 ```go // SELECT * FROM `user` AS `u` LEFT JOIN `user_detail` AS `ud` ON (ud.id=u.id) WHERE (`u`.`id`=10) AND `u`.`deleted_at` IS NULL LIMIT 1 g.Model("user", "u").LeftJoin("user_detail", "ud", "ud.id=u.id").Where("u.id", 10).One() ``` ## `Unscoped` 忽略时间特性 `Unscoped` 用于在链式操作中忽略自动时间更新特性,例如上面的示例,加上 `Unscoped` 方法后: ```go // SELECT * FROM `user` WHERE id>1 g.Model("user").Unscoped().Where("id>?", 1).All() // SELECT * FROM `user` AS `u` LEFT JOIN `user_detail` AS `ud` ON (ud.id=u.id) WHERE u.id=10 LIMIT 1 g.Model("user", "u").LeftJoin("user_detail", "ud", "ud.id=u.id").Where("u.id", 10).Unscoped().One() ``` # 时间维护-整型字段 从`v2.8`版本开始,如果时间字段`created_at`、 `updated_at`、 `deleted_at`为整型字段,ORM组件会自动识别支持,并写入**秒级的时间戳**数值。 ## 示例SQL 这是后续示例代码中用到的`MySQL`表结构。 ```sql CREATE TABLE `user` ( `id` int(10) unsigned NOT NULL AUTO_INCREMENT, `name` varchar(45) NOT NULL, `status` tinyint DEFAULT 0, `created_at` int(10) unsigned DEFAULT NULL, `updated_at` int(10) unsigned DEFAULT NULL, `deleted_at` int(10) unsigned DEFAULT NULL, PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8; CREATE TABLE IF NOT EXISTS `user_detail` ( `id` int(10) unsigned NOT NULL AUTO_INCREMENT, `address` varchar(45) NOT NULL, PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8; ``` **提示** 如果尝试测试查看`ORM`操作执行的`SQL`语句,建议打开`debug`模式(相关文档:[调试模式](./gdb-senior-debugging.md)),`SQL`语句将会自动打印到日志输出。 ## `created_at` 写入时间 在执行 `Insert/InsertIgnore/BatchInsert/BatchInsertIgnore` 方法时自动写入该时间,随后的更新/删除操作不会引起`created_at`字段内容的改变。 **注意** 需要注意的是 `Replace` 方法也会更新该字段,因为该操作相当于删除已存在的旧数据并重新写一条数据。 ```go // INSERT INTO `user`(`name`,`created_at`,`updated_at`,`deleted_at`) VALUES('john',1731481488,1731481488,0) g.Model("user").Data(g.Map{"name": "john"}).Insert() // INSERT IGNORE INTO `user`(`id`,`name`,`created_at`,`updated_at`,`deleted_at`) VALUES(10000,'john',1731481518,1731481518,0) g.Model("user").Data(g.Map{"id": 10000, "name": "john"}).InsertIgnore() // REPLACE INTO `user`(`id`,`name`,`created_at`,`updated_at`,`deleted_at`) VALUES(10000,'john',1731481747,1731481747,0) g.Model("user").Data(g.Map{"id": 10000, "name": "john"}).Replace() // INSERT INTO `user`(`id`,`name`,`created_at`,`updated_at`,`deleted_at`) VALUES(10001,'john',1731481766,1731481766,0) ON DUPLICATE KEY UPDATE `id`=VALUES(`id`),`name`=VALUES(`name`),`updated_at`=VALUES(`updated_at`),`deleted_at`=VALUES(`deleted_at`) g.Model("user").Data(g.Map{"id": 10001, "name": "john"}).Save() ``` ## `deleted_at` 数据软删除 当软删除存在时(即`deleted_at`字段存在时),所有的查询语句都将会自动加上 `deleted_at` 的条件。 ```go // UPDATE `user` SET `deleted_at`=1731481948 WHERE (`id`=10) AND `deleted_at`=0 g.Model("user").Where("id", 10).Delete() ``` 查询的时候会发生一些变化,例如: ```go // SELECT * FROM `user` WHERE (id>1) AND `deleted_at`=0 g.Model("user").Where("id>?", 1).All() ``` 可以看到当数据表中存在 `deleted_at` 字段时,所有涉及到该表的查询操作都将自动加上 `deleted_at=0` 的条件。 ## `updated_at` 更新时间 在执行 `Save/Update` 方法时自动写入该时间。需要注意的是 `Replace` 方法也会更新该字段,因为该操作相当于删除已存在的旧数据并重新写一条数据。 **说明** 如果同时存在`deleted_at`软删除字段,那么更新操作语句中同样会出现`deleted_at` 的条件。 ```go // UPDATE `user` SET `name`='john guo',`updated_at`=1731481821 WHERE (`name`='john') AND `deleted_at`=0 g.Model("user").Data(g.Map{"name" : "john guo"}).Where("name", "john").Update() // UPDATE `user` SET `status`=1,`updated_at`=1731481895 WHERE `deleted_at`=0 ORDER BY `id` ASC LIMIT 10 g.Model("user").Data("status", 1).OrderAsc("id").Limit(10).Update() // INSERT INTO `user`(`id`,`name`,`created_at`,`updated_at`,`deleted_at`) VALUES(1,'john guo',1731481915,1731481915,0) ON DUPLICATE KEY UPDATE `id`=VALUES(`id`),`name`=VALUES(`name`),`updated_at`=VALUES(`updated_at`),`deleted_at`=VALUES(`deleted_at`) g.Model("user").Data(g.Map{"id": 1, "name": "john guo"}).Save() ``` ## 联表查询的场景 如果关联查询的几个表都启用了软删除特性时,会发生以下这种情况,即条件语句中会增加所有相关表的软删除时间判断。 ```go // SELECT * FROM `user` AS `u` LEFT JOIN `user_detail` AS `ud` ON (ud.id=u.id) WHERE (`u`.`id`=10) AND `u`.`deleted_at`=0 LIMIT 1 g.Model("user", "u").LeftJoin("user_detail", "ud", "ud.id=u.id").Where("u.id", 10).One() ``` ## 控制写入时间粒度 本章节的时间字段数值写入默认是秒级时间戳,但如果我们想要控制时间写入的粒度,写入毫秒级时间戳怎么做呢?我们可以使用[SoftTimeOption](./gdb-chaining-soft-time-option.md)。 # 时间维护-布尔字段 从`v2.8`版本开始, 如果时间字段`created_at`、 `updated_at`、 `deleted_at`为布尔字段,ORM组件会自动识别支持,并写入布尔类型数值(写入数值通过`0`和`1`表示)。 通常布尔字段为`deleted_at`字段,我们这里只演示`deleted_at`字段类型为`bool`的情况。 ## 示例SQL 这是后续示例代码中用到的`MySQL`表结构。 ```sql CREATE TABLE `user` ( `id` int(10) unsigned NOT NULL AUTO_INCREMENT, `name` varchar(45) NOT NULL, `status` tinyint DEFAULT 0, `created_at` int(10) unsigned DEFAULT NULL, `updated_at` int(10) unsigned DEFAULT NULL, `deleted_at` bit(1) DEFAULT NULL, PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8; ``` 我们建议使用`bit(1)`来表示字段的`bool`类型,而非`tinyint(1)`或者`int(1)`。因为`tinyint(1)/int(1)`字段类型表示的范围是`-127~127`,通常可能会被用作状态字段类型。而`bit(1)`的类型范围为`0/1`,可以很好的表示`bool`类型的两个值`false/true`。 **提示** 如果尝试测试查看`ORM`操作执行的`SQL`语句,建议打开`debug`模式(相关文档:[调试模式](./gdb-senior-debugging.md)),`SQL`语句将会自动打印到日志输出。 ## `created_at` 写入时间 ```go // INSERT INTO `user`(`name`,`created_at`,`updated_at`,`deleted_at`) VALUES('john',1731481488,1731481488,0) g.Model("user").Data(g.Map{"name": "john"}).Insert() ``` ## `deleted_at` 数据软删除 ```go // UPDATE `user` SET `deleted_at`=1 WHERE (`id`=10) AND `deleted_at`=0 g.Model("user").Where("id", 10).Delete() ``` 查询的时候会发生一些变化,例如: ```go // SELECT * FROM `user` WHERE (id>1) AND `deleted_at`=0 g.Model("user").Where("id>?", 1).All() ``` # 时间维护-SoftTimeOption 在前面的[整型字段](./gdb-chaining-soft-time-numeric-fields.md)的示例中,时间字段写入的都是秒级时间戳,但如果我们想要控制时间写入的粒度,写入毫秒级时间戳怎么做呢? 我们可以使用`SoftTimeOption`来控制写入的时间数值粒度。 ## 示例SQL 这是后续示例代码中用到的`MySQL`表结构。由于需要写入比秒级更细粒度的数值,因此字段类型使用`big int`来存储。 ```sql CREATE TABLE `user` ( `id` int(10) unsigned NOT NULL AUTO_INCREMENT, `name` varchar(45) NOT NULL, `status` tinyint DEFAULT 0, `created_at` bigint unsigned DEFAULT NULL, `updated_at` bigint unsigned DEFAULT NULL, `deleted_at` bigint unsigned DEFAULT NULL, PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8; ``` **提示** 如果尝试测试查看`ORM`操作执行的`SQL`语句,建议打开`debug`模式(相关文档:[调试模式](./gdb-senior-debugging.md)),`SQL`语句将会自动打印到日志输出。 ## `created_at` 写入时间 ```go // INSERT INTO `user`(`name`,`created_at`,`updated_at`,`deleted_at`) VALUES('john',1731484186556,1731484186556,0) g.Model("user").SoftTime(gdb.SoftTimeOption{ SoftTimeType: gdb.SoftTimeTypeTimestampMilli, }).Data(g.Map{"name": "john"}).Insert() ``` 其中`SoftTimeType`控制时间粒度,粒度选项如下: ```go const ( SoftTimeTypeAuto SoftTimeType = 0 // (Default)Auto detect the field type by table field type. SoftTimeTypeTime SoftTimeType = 1 // Using datetime as the field value. SoftTimeTypeTimestamp SoftTimeType = 2 // In unix seconds. SoftTimeTypeTimestampMilli SoftTimeType = 3 // In unix milliseconds. SoftTimeTypeTimestampMicro SoftTimeType = 4 // In unix microseconds. SoftTimeTypeTimestampNano SoftTimeType = 5 // In unix nanoseconds. ) ``` # ORM事务处理 使用 `GoFrame ORM` 组件进行事务操作非常简便、安全,可以通过两种操作方式来实现。 1. 常规操作:通过 `Begin` 开启事务之后会返回一个事务操作接口 `gdb.TX`,随后可以使用该接口进行如之前章节介绍的方法操作和链式操作。常规操作容易漏掉关闭事务,有一定的事务操作安全风险。 2. 闭包操作:通过 `Transaction` 闭包方法的形式来操作事务,所有的事务逻辑在闭包中实现,闭包结束后自动关闭事务保障事务操作安全。并且闭包操作支持非常便捷的 **嵌套事务**,嵌套事务在业务操作中透明无感知。 **提示** 我们推荐事务操作均统一采用 `Transaction` 闭包方式实现。 接口文档: [https://pkg.go.dev/github.com/gogf/gf/v2/database/gdb#TX](https://pkg.go.dev/github.com/gogf/gf/v2/database/gdb#TX) # ORM事务处理-常规操作 常规的事务操作方法为 `Begin/Commit/Rollback`,每一个方法指定特定的事务操作。开启事务操作可以通过执行 `db.Begin` 方法,该方法返回事务的操作接口,类型为 `gdb.Tx`,通过该对象执行后续的数据库操作,并可通过 `tx.Commit` 提交修改,或者通过 `tx.Rollback` 回滚修改。 **注意** 常见问题注意:开启事务操作后,请务必在不需要使用该事务对象时,通过 `Commit`/ `Rollback` 操作关闭掉该事务,建议充分利用好 `defer` 方法。如果事务使用后不关闭,在应用侧会引起 `goroutine` 不断激增泄露,在数据库侧会引起事务线程数量被打满,以至于后续的事务请求执行超时。此外,建议尽可能使用后续介绍的 `Transaction` 闭包方法来安全实现事务操作: [ORM事务处理-闭包操作](./gdb-transaction-closure.md) ## 一、开启事务操作 ```go db := g.DB() if tx, err := db.Begin(ctx); err == nil { fmt.Println("开启事务操作") } ``` 事务操作对象可以执行所有 `db` 对象的方法,具体请参考 [API文档](https://pkg.go.dev/github.com/gogf/gf/v2/database/gdb)。 ## 二、事务回滚操作 ```go if tx, err := db.Begin(ctx); err == nil { r, err := tx.Save("user", g.Map{ "id" : 1, "name" : "john", }) if err != nil { tx.Rollback() } fmt.Println(r) } ``` ## 三、事务提交操作 ```go if tx, err := db.Begin(ctx); err == nil { r, err := tx.Save("user", g.Map{ "id" : 1, "name" : "john", }) if err == nil { tx.Commit() } fmt.Println(r) } ``` ## 四、事务链式操作 事务操作对象仍然可以通过 `tx.Model` 方法返回一个链式操作的对象,该对象与 `db.Model` 方法返回值相同,只不过数据库操作在事务上执行,可提交或回滚。 ```go if tx, err := db.Begin(); err == nil { r, err := tx.Model("user").Data(g.Map{"id":1, "name": "john_1"}).Save() if err == nil { tx.Commit() } fmt.Println(r) } ``` 其他链式操作请参考 [ORM链式操作(重点)](./gdb-chaining.md) 章节。 # ORM事务处理-闭包操作 ## 一、痛点描述 可以看到,通过常规的事务方法来管理事务有一些问题: - **冗余代码较多**。代码中存在很多重复性的 `tx.Commit/Rollback` 操作。 - **操作风险较大**。非常容易遗忘执行 `tx.Commit/Rollback` 操作,或者由于代码逻辑判断 `BUG`,引发事务操作未正常关闭。在自行管理事务操作的情况下,大部分程序员都会踩到这个坑。作者已经见过了很多起由于事务未正常关闭引发的现网事故。我现在特意过来更新这段描述(2023-08-09),也是因为一个朋友由于自行通过 `tx.Commit/Rollback` 管理事务操作,细节未处理好,引发了现网事故。 - **嵌套事务实现复杂**。假如业务逻辑中存在多层级的事务处理(嵌套事务),需要考虑如何加个 `tx` 对象往下传递,处理起来更加繁琐。 ## 二、闭包操作 因此为方便安全执行事务操作, `ORM` 组件同样提供了事务的闭包操作,通过 `Transaction` 方法实现,该方法定义如下: ```go func (db DB) Transaction(ctx context.Context, f func(ctx context.Context, tx TX) error) (err error) ``` 当给定的闭包方法返回的 `error` 为 `nil` 时,那么闭包执行结束后当前事务自动执行 `Commit` 提交操作;否则自动执行 `Rollback` 回滚操作。闭包中的 `context.Context` 参数为 `goframe v1.16` 版本后新增的上下文变量,主要用于链路跟踪传递以及嵌套事务管理。由于上下文变量是嵌套事务管理的重要参数,因此上下文变量通过显示的参数传递定义。 **提示** 如果闭包内部操作产生 `panic` 中断,该事务也将自动进行回滚,以保证操作安全。 使用示例: ```go g.DB().Transaction(context.TODO(), func(ctx context.Context, tx gdb.TX) error { // user result, err := tx.Ctx(ctx).Insert("user", g.Map{ "passport": "john", "password": "12345678", "nickname": "JohnGuo", }) if err != nil { return err } // user_detail id, err := result.LastInsertId() if err != nil { return err } _, err = tx.Ctx(ctx).Insert("user_detail", g.Map{ "uid": id, "site": "https://johng.cn", "true_name": "GuoQiang", }) if err != nil { return err } return nil }) ``` 通过闭包操作的方式可以很简便地实现嵌套事务,且对上层业务开发同学来说无感知,具体可以继续阅读章节: [ORM事务处理-嵌套事务](./gdb-transaction-nested.md) # ORM事务处理-嵌套事务 从 `GoFrame ORM` 支持数据库嵌套事务,嵌套事务在业务项目中用得比较多,特别是业务模块之间的相互调用,保证各个业务模块的数据库操作都处于一个事务中,其原理是通过传递的 `context` 上下文来隐式传递和关联同一个事务对象。需要注意的是,数据库服务往往并不支持嵌套事务,而是依靠 `ORM` 组件层通过 `Transaction Save Point` 特性实现的。同样的,我们推荐使用 `Transaction` 闭包方法来实现嵌套事务操作。为了保证文档的完整性,因此我们这里仍然从最基本的事务操作方法开始来介绍嵌套事务操作。 ## 一、示例SQL 一个简单的示例 `SQL`,包含两个字段 `id` 和 `name`: ```sql CREATE TABLE `user` ( `id` int(10) unsigned NOT NULL COMMENT '用户ID', `name` varchar(45) NOT NULL COMMENT '用户名称', PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; ``` ## 二、常规操作(不推荐) ```go db := g.DB() tx, err := db.Begin() if err != nil { panic(err) } if err = tx.Begin(); err != nil { panic(err) } _, err = tx.Model(table).Data(g.Map{"id": 1, "name": "john"}).Insert() if err = tx.Rollback(); err != nil { panic(err) } _, err = tx.Model(table).Data(g.Map{"id": 2, "name": "smith"}).Insert() if err = tx.Commit(); err != nil { panic(err) } ``` ### 1、 `db.Begin` 与 `tx.Begin` 可以看到,在我们的嵌套事务中出现了 `db.Begin` 和 `tx.Begin` 两种事务开启方式,两者有什么区别呢? `db.Begin` 是在数据库服务上真正开启一个事务操作,并返回一个事务操作对象 `tx`,随后所有的事务操作都是通过该 `tx` 事务对象来操作管理。 `tx.Begin` 表示在当前事务操作中开启嵌套事务,默认情况下会对嵌套事务的 `SavePoint` 采用自动命名,命名格式为 `transactionN`,其中的 `N` 表示嵌套的层级数量,如果看到日志中出现 ``SAVEPOINT `transaction1` `` 表示当前嵌套层级为 `2`(从 `0` 开始计算)。 ### 2、更详细的日志 `goframe` 的 `ORM` 拥有相当完善的日志记录机制,如果打开 `SQL` 日志,那么将会看到以下日志信息,展示了整个数据库请求的详细执行流程: ```html 2021-05-22 21:12:10.776 [DEBU] [ 4 ms] [default] [txid:1] BEGIN 2021-05-22 21:12:10.776 [DEBU] [ 0 ms] [default] [txid:1] SAVEPOINT `transaction0` 2021-05-22 21:12:10.789 [DEBU] [ 13 ms] [default] [txid:1] SHOW FULL COLUMNS FROM `user` 2021-05-22 21:12:10.790 [DEBU] [ 1 ms] [default] [txid:1] INSERT INTO `user`(`id`,`name`) VALUES(1,'john') 2021-05-22 21:12:10.791 [DEBU] [ 1 ms] [default] [txid:1] ROLLBACK TO SAVEPOINT `transaction0` 2021-05-22 21:12:10.791 [DEBU] [ 0 ms] [default] [txid:1] INSERT INTO `user`(`id`,`name`) VALUES(2,'smith') 2021-05-22 21:12:10.792 [DEBU] [ 1 ms] [default] [txid:1] COMMIT ``` 其中的 `[txid:1]` 表示 `ORM` 组件记录的事务ID,多个真实的事务同时操作时,每个事务的ID将会不同。在同一个真实事务下的嵌套事务的事务ID是一样的。 执行后查询数据库结果: ``` mysql> select * from `user`; +----+-------+ | id | name | +----+-------+ | 2 | smith | +----+-------+ 1 row in set (0.00 sec) ``` 可以看到第一个操作被成功回滚,只有第二个操作执行并提交成功。 ## 三、闭包操作(推荐) 我们也可以通过闭包操作来实现嵌套事务,同样也是通过 `Transaction` 方法实现。 ```go db.Transaction(ctx, func(ctx context.Context, tx gdb.Tx) error { // Nested transaction 1. if err := tx.Transaction(ctx, func(ctx context.Context, tx gdb.Tx) error { _, err := tx.Model(table).Ctx(ctx).Data(g.Map{"id": 1, "name": "john"}).Insert() return err }); err != nil { return err } // Nested transaction 2, panic. if err := tx.Transaction(ctx, func(ctx context.Context, tx gdb.Tx) error { _, err := tx.Model(table).Ctx(ctx).Data(g.Map{"id": 2, "name": "smith"}).Insert() // Create a panic that can make this transaction rollback automatically. panic("error") return err }); err != nil { return err } return nil }) ``` 嵌套事务的闭包嵌套中也可以不使用其中的 `tx` 对象,而是直接使用 `db` 对象或者 `dao` 包,这种方式更常见一些。特别是在方法层级调用时,使得对于开发者来说并不用关心 `tx` 对象的传递,也并不用关心当前事务是否需要嵌套执行,一切都由组件自动维护,极大减少开发者的心智负担。但是务必记得将 `ctx` 上下文变量层层传递下去哦。例如: ```go db.Transaction(ctx, func(ctx context.Context, tx gdb.Tx) error { // Nested transaction 1. if err := db.Transaction(ctx, func(ctx context.Context, tx gdb.Tx) error { _, err := db.Model(table).Ctx(ctx).Data(g.Map{"id": 1, "name": "john"}).Insert() return err }); err != nil { return err } // Nested transaction 2, panic. if err := db.Transaction(ctx, func(ctx context.Context, tx gdb.Tx) error { _, err := db.Model(table).Ctx(ctx).Data(g.Map{"id": 2, "name": "smith"}).Insert() // Create a panic that can make this transaction rollback automatically. panic("error") return err }); err != nil { return err } return nil }) ``` 如果打开 `SQL` 日志,那么执行后将会看到以下日志信息,展示了整个数据库请求的详细执行流程: ```html 2021-05-22 21:18:46.672 [DEBU] [ 2 ms] [default] [txid:1] BEGIN 2021-05-22 21:18:46.672 [DEBU] [ 0 ms] [default] [txid:1] SAVEPOINT `transaction0` 2021-05-22 21:18:46.673 [DEBU] [ 0 ms] [default] [txid:1] SHOW FULL COLUMNS FROM `user` 2021-05-22 21:18:46.674 [DEBU] [ 0 ms] [default] [txid:1] INSERT INTO `user`(`id`,`name`) VALUES(1,'john') 2021-05-22 21:18:46.674 [DEBU] [ 0 ms] [default] [txid:1] RELEASE SAVEPOINT `transaction0` 2021-05-22 21:18:46.675 [DEBU] [ 1 ms] [default] [txid:1] SAVEPOINT `transaction0` 2021-05-22 21:18:46.675 [DEBU] [ 0 ms] [default] [txid:1] INSERT INTO `user`(`name`,`id`) VALUES('smith',2) 2021-05-22 21:18:46.675 [DEBU] [ 0 ms] [default] [txid:1] ROLLBACK TO SAVEPOINT `transaction0` 2021-05-22 21:18:46.676 [DEBU] [ 1 ms] [default] [txid:1] ROLLBACK ``` **注意** 假如 `ctx` 上下文变量没有层层传递下去,那么嵌套事务将会失败,我们来看一个错误的例子: ```go db.Transaction(ctx, func(ctx context.Context, tx gdb.Tx) error { // Nested transaction 1. if err := db.Transaction(ctx, func(ctx context.Context, tx gdb.Tx) error { _, err := db.Model(table).Ctx(ctx).Data(g.Map{"id": 1, "name": "john"}).Insert() return err }); err != nil { return err } // Nested transaction 2, panic. if err := db.Transaction(ctx, func(ctx context.Context, tx gdb.Tx) error { _, err := db.Model(table).Data(g.Map{"id": 2, "name": "smith"}).Insert() // Create a panic that can make this transaction rollback automatically. panic("error") return err }); err != nil { return err } return nil }) ``` 打开 `SQL` 执行日志,执行后,将会看到以下日志内容: ```html 2021-05-22 21:29:38.841 [DEBU] [ 3 ms] [default] [txid:1] BEGIN 2021-05-22 21:29:38.842 [DEBU] [ 1 ms] [default] [txid:1] SAVEPOINT `transaction0` 2021-05-22 21:29:38.843 [DEBU] [ 1 ms] [default] [txid:1] SHOW FULL COLUMNS FROM `user` 2021-05-22 21:29:38.845 [DEBU] [ 2 ms] [default] [txid:1] INSERT INTO `user`(`id`,`name`) VALUES(1,'john') 2021-05-22 21:29:38.845 [DEBU] [ 0 ms] [default] [txid:1] RELEASE SAVEPOINT `transaction0` 2021-05-22 21:29:38.846 [DEBU] [ 1 ms] [default] [txid:1] SAVEPOINT `transaction0` 2021-05-22 21:29:38.847 [DEBU] [ 1 ms] [default] INSERT INTO `user`(`id`,`name`) VALUES(2,'smith') 2021-05-22 21:29:38.848 [DEBU] [ 0 ms] [default] [txid:1] ROLLBACK TO SAVEPOINT `transaction0` 2021-05-22 21:29:38.848 [DEBU] [ 0 ms] [default] [txid:1] ROLLBACK ``` 可以看到,第二条 `INSERT` 操作 ``INSERT INTO `user`(`id`,`name`) VALUES(2,'smith') `` 没有事务ID打印,表示没有使用到事务,那么该操作将会被真正提交到数据库执行,并不能被回滚。 ## 四、 `SavePoint/RollbackTo` 开发者也可以灵活使用 `Transaction Save Point` 特性,并实现自定义的 `SavePoint` 命名以及指定 `Point` 回滚操作。 ```go tx, err := db.Begin() if err != nil { panic(err) } defer func() { if err := recover(); err != nil { _ = tx.Rollback() } }() if _, err = tx.Model(table).Data(g.Map{"id": 1, "name": "john"}).Insert(); err != nil { panic(err) } if err = tx.SavePoint("MyPoint"); err != nil { panic(err) } if _, err = tx.Model(table).Data(g.Map{"id": 2, "name": "smith"}).Insert(); err != nil { panic(err) } if _, err = tx.Model(table).Data(g.Map{"id": 3, "name": "green"}).Insert(); err != nil { panic(err) } if err = tx.RollbackTo("MyPoint"); err != nil { panic(err) } if err = tx.Commit(); err != nil { panic(err) } ``` 如果打开 `SQL` 日志,那么将会看到以下日志信息,展示了整个数据库请求的详细执行流程: ```html 2021-05-22 21:38:51.992 [DEBU] [ 3 ms] [default] [txid:1] BEGIN 2021-05-22 21:38:52.002 [DEBU] [ 9 ms] [default] [txid:1] SHOW FULL COLUMNS FROM `user` 2021-05-22 21:38:52.002 [DEBU] [ 0 ms] [default] [txid:1] INSERT INTO `user`(`id`,`name`) VALUES(1,'john') 2021-05-22 21:38:52.003 [DEBU] [ 1 ms] [default] [txid:1] SAVEPOINT `MyPoint` 2021-05-22 21:38:52.004 [DEBU] [ 1 ms] [default] [txid:1] INSERT INTO `user`(`id`,`name`) VALUES(2,'smith') 2021-05-22 21:38:52.005 [DEBU] [ 1 ms] [default] [txid:1] INSERT INTO `user`(`id`,`name`) VALUES(3,'green') 2021-05-22 21:38:52.006 [DEBU] [ 0 ms] [default] [txid:1] ROLLBACK TO SAVEPOINT `MyPoint` 2021-05-22 21:38:52.006 [DEBU] [ 0 ms] [default] [txid:1] COMMIT ``` 执行后查询数据库结果: ``` mysql> select * from `user`; +----+------+ | id | name | +----+------+ | 1 | john | +----+------+ 1 row in set (0.00 sec) ``` 可以看到,通过在第一个 `Insert` 操作后保存了一个 `SavePoint` 名称 `MyPoint`,随后的几次操作都通过 `RollbackTo` 方法被回滚掉了,因此只有第一次 `Insert` 操作被成功提交执行。 ## 五、嵌套事务在工程中的参考示例 为了简化示例,我们还是使用用户模块相关的示例,例如用户注册,通过事务操作保存用户基本信息( `user`)、详细信息( `user_detail`)两个表,任一个表操作失败整个注册操作都将失败。为展示嵌套事务效果,我们将用户基本信息管理和用户详细信息管理划分为了两个 `dao` 对象。 假如我们的项目按照 `goframe` 标准项目工程化分为三层 `api-service-dao`,那么我们的嵌套事务操作可能是这样的。 ### `controller` ```go // 用户注册HTTP接口 func (*cUser) Signup(r *ghttp.Request) { // .... service.User().Signup(r.Context(), userServiceSignupReq) // ... } ``` 承接HTTP请求,并且将 `Context` 上下文边变量传递给后续的流程。 ### `service` ```go // 用户注册业务逻辑处理 func (*userService) Signup(ctx context.Context, r *model.UserServiceSignupReq) { // .... dao.User.Transaction(ctx, func(ctx context.Context, tx gdb.Tx) error { err := dao.User.Ctx(ctx).Save(r.UserInfo) if err != nil { return err } err := dao.UserDetail.Ctx(ctx).Save(r.UserDetail) if err != nil { return err } return nil }) // ... } ``` 可以看到,内部的 `user` 表和 `user_detail` 表使用了嵌套事务来统一执行事务操作。注意在闭包内部需要通过 `Ctx` 方法将上下文变量传递给下一层级。假如在闭包中存在对其他 `service` 对象的调用,那么也需要将 `ctx` 变量传递过去,例如: ```go func (*userService) Signup(ctx context.Context, r *model.UserServiceSignupReq) { // .... dao.User.Transaction(ctx, func(ctx context.Context, tx gdb.Tx) (err error) { if err = dao.User.Ctx(ctx).Save(r.UserInfo); err != nil { return err } if err = dao.UserDetail.Ctx(ctx).Save(r.UserDetail); err != nil { return err } if err = service.XXXA().Call(ctx, ...); err != nil { return err } if err = service.XXXB().Call(ctx, ...); err != nil { return err } if err = service.XXXC().Call(ctx, ...); err != nil { return err } // ... return nil }) // ... } ``` ### `dao` `dao` 层的代码由 `goframe cli` 工具全自动化生成及维护即可。 # ORM事务处理-事务传播 事务传播(`Transaction Propagation`)是`GoFrame ORM`框架提供的一种高级事务管理机制,用于控制嵌套事务的行为。在复杂的业务场景中,一个事务方法可能会调用其他事务方法,此时需要明确定义事务的传播行为,以确保数据的一致性和完整性。 ## 基本介绍 事务传播定义了事务方法被另一个事务方法调用时应该如何表现。`GoFrame ORM`支持多种事务传播行为,开发者可以根据业务需求选择合适的传播类型。 `GoFrame ORM`支持以下事务传播类型: - `PropagationNested` (默认):如果当前存在事务,则创建嵌套事务(使用保存点);如果不存在事务,则创建新事务。 - `PropagationRequired`:如果当前存在事务,则加入该事务;如果不存在事务,则创建新事务。 - `PropagationSupports`:如果当前存在事务,则加入该事务;如果不存在事务,则以非事务方式执行。 - `PropagationRequiresNew`:创建新事务,如果当前存在事务,则挂起当前事务。 - `PropagationNotSupported`:以非事务方式执行,如果当前存在事务,则挂起当前事务。 - `PropagationMandatory`:如果当前存在事务,则加入该事务;如果不存在事务,则抛出异常。 - `PropagationNever`:以非事务方式执行,如果当前存在事务,则抛出异常。 **说明** 注意事项:为保证和旧版本事务处理的兼容性,`GoFrame ORM`默认的事务传播类型为`PropagationNested`,而不是`PropagationRequired`。 ## 使用优势 1. **灵活的事务管理**:根据业务需求选择不同的事务传播行为,实现更精细的事务控制。 2. **提高代码复用性**:服务层方法可以独立定义事务行为,无需关心调用环境是否已有事务。 3. **降低事务冲突**:通过适当的传播行为,减少长事务导致的锁定和冲突问题。 4. **增强错误隔离**:使用独立事务(如`PropagationRequiresNew`)可以隔离错误影响,避免整体事务回滚。 5. **简化复杂业务逻辑**:在复杂业务流程中,不同步骤可以使用不同的事务传播策略,使代码更清晰。 ## 应用场景 1. **服务组合**:当一个服务方法调用多个其他服务方法时,可以根据业务需求选择是共享同一事务还是使用独立事务。 2. **日志记录**:业务操作需要记录日志,但日志写入失败不应影响主要业务逻辑,可以使用`PropagationRequiresNew`。 3. **批量处理**:在批量处理中,可能希望每个项目使用独立事务,以便某个项目失败不影响其他项目。 4. **事务补偿**:在分布式系统中,可以使用不同的传播行为实现事务补偿机制。 5. **嵌套业务逻辑**:复杂业务逻辑中的某些步骤可能需要作为一个独立的事务单元,可以使用`PropagationNested`。 ## 使用示例 在开始示例之前,我们先创建一个用于测试的数据表: ```sql CREATE TABLE IF NOT EXISTS `user` ( id INT PRIMARY KEY, username VARCHAR(50) ); ``` **提示** 如果想逐步运行以下示例,需要在每次运行新示例之前清空一下`user`表数据。 ### PropagationNested (嵌套事务,默认) 如果当前存在事务,则创建嵌套事务(使用保存点);如果不存在事务,则创建新事务。 ```go package main import ( _ "github.com/gogf/gf/contrib/drivers/mysql/v2" "context" "fmt" "github.com/gogf/gf/v2/database/gdb" "github.com/gogf/gf/v2/frame/g" ) func main() { var ( ctx = context.Background() db = g.DB() ) db.SetDebug(true) // 执行事务 err := db.Transaction(ctx, func(ctx context.Context, tx gdb.TX) error { // 在外层事务中插入数据 _, err := tx.Insert("user", g.Map{ "id": 1, "username": "outer_user", }) if err != nil { return err } // 嵌套事务 - 使用PropagationNested创建嵌套事务(使用保存点) err = tx.Transaction(ctx, func(ctx context.Context, tx2 gdb.TX) error { // 在嵌套事务中插入数据 _, err = tx2.Insert("user", g.Map{ "id": 2, "username": "nested_user", }) if err != nil { return err } // 模拟错误,导致嵌套事务回滚到保存点 return fmt.Errorf("嵌套事务故意失败") }) // 嵌套事务失败,但外层事务可以继续 fmt.Println("嵌套事务错误:", err) // 继续在外层事务中插入数据 _, err = tx.Insert("user", g.Map{ "id": 3, "username": "outer_after_nested", }) // 外层事务正常提交 return nil }) if err != nil { fmt.Println("事务执行失败:", err) return } // 查询结果 result, err := db.Model("user").All() if err != nil { fmt.Println("查询失败:", err) return } // 数据中只有id为1和3的数据,没有2 fmt.Println("查询结果:", result) } ``` 上述代码执行的`SQL`语句类似于: ```sql -- 开始外层事务 BEGIN; -- 插入外层数据 INSERT INTO user(id,username) VALUES(1,'outer_user'); -- 创建保存点 SAVEPOINT sp1; -- 插入嵌套事务数据 INSERT INTO user(id,username) VALUES(2,'nested_user'); -- 嵌套事务失败,回滚到保存点 ROLLBACK TO SAVEPOINT sp1; -- 继续外层事务 INSERT INTO user(id,username) VALUES(3,'outer_after_nested'); -- 提交外层事务 COMMIT; ``` ### PropagationRequired (保证事务) 事务传播类型`PropagationRequired`与`PropagationNested`的主要区别在于,`PropagationRequired`在当前存在事务时加入该事务, 而在没有事务时创建新事务,**并不会创建嵌套事务**。 ```go package main import ( _ "github.com/gogf/gf/contrib/drivers/mysql/v2" "context" "fmt" "github.com/gogf/gf/v2/database/gdb" "github.com/gogf/gf/v2/frame/g" ) func main() { var ( ctx = context.Background() db = g.DB() ) db.SetDebug(true) // 执行事务 err := db.Transaction(ctx, func(ctx context.Context, tx gdb.TX) error { // 在外层事务中插入数据 // 在事务闭包中使用tx对象或者db对象来操作数据表都是等价的 _, err := tx.Insert("user", g.Map{ "id": 1, "username": "outer_user", }) if err != nil { return err } // 嵌套事务 - 默认使用PropagationRequired err = tx.TransactionWithOptions(ctx, gdb.TxOptions{ Propagation: gdb.PropagationRequired, }, func(ctx context.Context, tx2 gdb.TX) error { // 在嵌套事务中插入数据(使用相同的事务) // 在事务闭包中使用tx2对象或者db对象来操作数据表都是等价的 _, err = tx2.Insert("user", g.Map{ "id": 2, "username": "inner_user", }) return err }) return err }) if err != nil { fmt.Println("事务执行失败:", err) return } // 查询结果 result, err := db.Model("user").All() if err != nil { fmt.Println("查询失败:", err) return } fmt.Println("查询结果:", result) } ``` 上述代码执行的`SQL`语句类似于: ```sql -- 开始外层事务 BEGIN; -- 插入外层数据 INSERT INTO user(id,username) VALUES(1,'outer_user'); -- 插入内层数据(使用相同事务) INSERT INTO user(id,username) VALUES(2,'inner_user'); -- 提交事务 COMMIT; ``` ### PropagationRequiresNew (创建新事务) 创建新事务,如果当前存在事务,则挂起当前事务。 ```go package main import ( _ "github.com/gogf/gf/contrib/drivers/mysql/v2" "context" "fmt" "github.com/gogf/gf/v2/database/gdb" "github.com/gogf/gf/v2/frame/g" ) func main() { var ( ctx = context.Background() db = g.DB() ) db.SetDebug(true) // 执行事务 err := db.Transaction(ctx, func(ctx context.Context, tx gdb.TX) error { // 在外层事务中插入数据 _, err := tx.Insert("user", g.Map{ "id": 1, "username": "outer_user", }) if err != nil { return err } // 嵌套事务 - 使用PropagationRequiresNew创建新事务 err = tx.TransactionWithOptions(ctx, gdb.TxOptions{ Propagation: gdb.PropagationRequiresNew, }, func(ctx context.Context, tx2 gdb.TX) error { // 在新事务中插入数据 _, err = tx2.Insert("user", g.Map{ "id": 2, "username": "new_tx_user", }) // 模拟错误,导致内层事务回滚 return fmt.Errorf("内层事务故意失败") }) // 内层事务失败不影响外层事务 fmt.Println("内层事务错误:", err) // 继续在外层事务中插入数据 _, err = tx.Insert("user", g.Map{ "id": 3, "username": "outer_after_error", }) // 外层事务正常提交 return nil }) if err != nil { fmt.Println("事务执行失败:", err) return } // 查询结果 result, err := db.Model("user").All() if err != nil { fmt.Println("查询失败:", err) return } fmt.Println("查询结果:", result) } ``` 上述代码执行的`SQL`语句类似于: ```sql -- 开始外层事务 BEGIN; -- 插入外层数据 INSERT INTO user(id,username) VALUES(1,'outer_user'); -- 开始新的独立事务 BEGIN; -- 插入内层数据(使用新事务) INSERT INTO user(id,username) VALUES(2,'new_tx_user'); -- 内层事务回滚 ROLLBACK; -- 继续外层事务 INSERT INTO user(id,username) VALUES(3,'outer_after_error'); -- 提交外层事务 COMMIT; ``` ### PropagationSupports (支持当前事务) 如果当前存在事务,则加入该事务;如果不存在事务,则以非事务方式执行。 ```go package main import ( _ "github.com/gogf/gf/contrib/drivers/mysql/v2" "context" "fmt" "github.com/gogf/gf/v2/database/gdb" "github.com/gogf/gf/v2/frame/g" ) func main() { var ( ctx = context.Background() db = g.DB() ) db.SetDebug(true) // 场景1: 有外部事务时,加入外部事务 err := db.Transaction(ctx, func(ctx context.Context, tx gdb.TX) error { // 在外层事务中插入数据 _, err := tx.Insert("user", g.Map{ "id": 1, "username": "outer_user", }) if err != nil { return err } // 嵌套事务 - 使用PropagationSupports err = tx.TransactionWithOptions(ctx, gdb.TxOptions{ Propagation: gdb.PropagationSupports, }, func(ctx context.Context, tx2 gdb.TX) error { // 在支持当前事务的方式下插入数据(使用外层事务) _, err = tx2.Insert("user", g.Map{ "id": 2, "username": "supports_user", }) return err }) return err }) if err != nil { fmt.Println("场景1执行失败:", err) return } // 查询结果 result, err := db.Model("user").All() if err != nil { fmt.Println("查询失败:", err) return } fmt.Println("场景1查询结果:", result) // 清空数据表 _, err = db.Exec(ctx, `TRUNCATE TABLE user`) if err != nil { fmt.Println("执行失败:", err) return } // 场景2: 没有外部事务时,非事务方式执行 err = db.TransactionWithOptions(ctx, gdb.TxOptions{ Propagation: gdb.PropagationSupports, }, func(ctx context.Context, tx gdb.TX) error { // 以非事务方式插入数据 _, err = tx.Insert("user", g.Map{ "id": 3, "username": "non_tx_user", }) return err }) if err != nil { fmt.Println("场景2执行失败:", err) return } // 查询结果 result, err = db.Model("user").All() if err != nil { fmt.Println("查询失败:", err) return } fmt.Println("场景2查询结果:", result) } ``` 上述代码执行的`SQL`语句类似于: ```sql -- 场景1: 有外部事务 -- 开始外层事务 BEGIN; -- 插入外层数据 INSERT INTO user(id,username) VALUES(1,'outer_user'); -- 插入内层数据(使用外层事务) INSERT INTO user(id,username) VALUES(2,'supports_user'); -- 提交事务 COMMIT; -- 场景2: 没有外部事务 -- 非事务方式直接插入数据 INSERT INTO user(id,username) VALUES(3,'non_tx_user'); ``` ### PropagationMandatory (强制使用事务) 如果当前存在事务,则加入该事务;如果不存在事务,则抛出异常。 ```go package main import ( _ "github.com/gogf/gf/contrib/drivers/mysql/v2" "context" "fmt" "github.com/gogf/gf/v2/database/gdb" "github.com/gogf/gf/v2/frame/g" ) func main() { var ( ctx = context.Background() db = g.DB() ) db.SetDebug(true) // 场景1: 有外部事务时,加入外部事务 err := db.Transaction(ctx, func(ctx context.Context, tx gdb.TX) error { // 在外层事务中插入数据 _, err := tx.Insert("user", g.Map{ "id": 1, "username": "outer_user", }) if err != nil { return err } // 嵌套事务 - 使用PropagationMandatory err = tx.TransactionWithOptions(ctx, gdb.TxOptions{ Propagation: gdb.PropagationMandatory, }, func(ctx context.Context, tx2 gdb.TX) error { // 在强制使用当前事务的方式下插入数据(使用外层事务) _, err = tx2.Insert("user", g.Map{ "id": 2, "username": "mandatory_user", }) return err }) return err }) if err != nil { fmt.Println("场景1执行失败:", err) return } // 查询结果 result, err := db.Model("user").All() if err != nil { fmt.Println("查询失败:", err) return } fmt.Println("场景1查询结果:", result) // 清空数据表 _, err = db.Exec(ctx, `TRUNCATE TABLE user`) if err != nil { fmt.Println("执行失败:", err) return } // 场景2: 没有外部事务时,将抛出异常 fmt.Println("场景2: 没有外部事务时使用PropagationMandatory") err = db.TransactionWithOptions(ctx, gdb.TxOptions{ Propagation: gdb.PropagationMandatory, }, func(ctx context.Context, tx gdb.TX) error { // 这里的代码不会执行,因为没有外部事务时会抛出异常 _, err = tx.Insert("user", g.Map{ "id": 3, "username": "will_not_insert", }) return err }) // 应该有错误,因为没有外部事务 fmt.Println("场景2错误:", err) } ``` 上述代码执行的`SQL`语句类似于: ```sql -- 场景1: 有外部事务 -- 开始外层事务 BEGIN; -- 插入外层数据 INSERT INTO user(id,username) VALUES(1,'outer_user'); -- 插入内层数据(使用外层事务) INSERT INTO user(id,username) VALUES(2,'mandatory_user'); -- 提交事务 COMMIT; -- 场景2: 没有外部事务 -- 抛出异常: "mandatory transaction is required, but none exists" ``` ### PropagationNever (不允许在事务中执行) 以非事务方式执行,如果当前存在事务,则抛出异常。 ```go package main import ( _ "github.com/gogf/gf/contrib/drivers/mysql/v2" "context" "fmt" "github.com/gogf/gf/v2/database/gdb" "github.com/gogf/gf/v2/frame/g" ) func main() { var ( ctx = context.Background() db = g.DB() ) db.SetDebug(true) // 场景1: 有外部事务时,将抛出异常 fmt.Println("场景1: 有外部事务时使用PropagationNever") err := db.Transaction(ctx, func(ctx context.Context, tx gdb.TX) error { // 在外层事务中插入数据 _, err := tx.Insert("user", g.Map{ "id": 1, "username": "outer_user", }) if err != nil { return err } // 嵌套事务 - 使用PropagationNever err = tx.TransactionWithOptions(ctx, gdb.TxOptions{ Propagation: gdb.PropagationNever, }, func(ctx context.Context, tx2 gdb.TX) error { // 这里的代码不会执行,因为在外部事务中使用PropagationNever会抛出异常 _, err := tx2.Insert("user", g.Map{ "id": 2, "username": "will_not_insert", }) return err }) // 应该有错误 fmt.Println("嵌套事务错误:", err) // 继续外层事务 return nil }) if err != nil { fmt.Println("场景1执行失败:", err) return } // 查询结果 result, err := db.Model("user").All() if err != nil { fmt.Println("查询失败:", err) return } // 应该只有id=1的记录 fmt.Println("场景1查询结果:", result) // 清空数据表 _, err = db.Exec(ctx, `TRUNCATE TABLE user`) if err != nil { fmt.Println("执行失败:", err) return } // 场景2: 没有外部事务时,非事务方式执行 err = db.TransactionWithOptions(ctx, gdb.TxOptions{ Propagation: gdb.PropagationNever, }, func(ctx context.Context, tx gdb.TX) error { // 以非事务方式插入数据 _, err = tx.Insert("user", g.Map{ "id": 3, "username": "non_tx_user", }) return err }) if err != nil { fmt.Println("场景2执行失败:", err) return } // 查询结果 result, err = db.Model("user").All() if err != nil { fmt.Println("查询失败:", err) return } // 应该有id=3的记录 fmt.Println("场景2查询结果:", result) } ``` 上述代码执行的`SQL`语句类似于: ```sql -- 场景1: 有外部事务 -- 开始外层事务 BEGIN; -- 插入外层数据 INSERT INTO user(id,username) VALUES(1,'outer_user'); -- 抛出异常: "transaction is existing, but never transaction is required" -- 提交外层事务 COMMIT; -- 场景2: 没有外部事务 -- 非事务方式直接插入数据 INSERT INTO user(id,username) VALUES(3,'non_tx_user'); ``` ### PropagationNotSupported (非事务方式执行) 以非事务方式执行,如果当前存在事务,则挂起当前事务。 ```go package main import ( _ "github.com/gogf/gf/contrib/drivers/mysql/v2" "context" "fmt" "github.com/gogf/gf/v2/database/gdb" "github.com/gogf/gf/v2/frame/g" ) func main() { var ( ctx = context.Background() db = g.DB() ) db.SetDebug(true) // 执行事务 err := db.Transaction(ctx, func(ctx context.Context, tx gdb.TX) error { // 在事务中插入数据 _, err := tx.Insert("user", g.Map{ "id": 1, "username": "tx_user", }) if err != nil { return err } // 使用PropagationNotSupported挂起当前事务 err = tx.TransactionWithOptions(ctx, gdb.TxOptions{ Propagation: gdb.PropagationNotSupported, }, func(ctx context.Context, tx2 gdb.TX) error { // 非事务方式写入数据 _, err = tx2.Insert("user", g.Map{ "id": 2, "username": "non_tx_user", }) return err }) if err != nil { return err } // 模拟错误,导致外层事务回滚 return fmt.Errorf("外层事务故意失败") }) fmt.Println("事务执行结果:", err) // 查询结果 result, err := db.Model("user").All() if err != nil { fmt.Println("查询失败:", err) return } // 应该只看到id=2的记录,因为id=1的记录在事务回滚时被撤销 fmt.Println("查询结果:", result) } ``` 上述代码执行的`SQL`语句类似于: ```sql -- 开始事务 BEGIN; -- 插入事务内数据 INSERT INTO user(id,username) VALUES(1,'tx_user'); -- 非事务方式执行(直接提交) INSERT INTO user(id,username) VALUES(2,'non_tx_user'); -- 外层事务回滚 ROLLBACK; ``` ## 总结 `GoFrame ORM`的事务传播功能为开发者提供了灵活的事务管理机制,能够满足复杂业务场景下的事务处理需求。通过合理选择事务传播行为,可以实现更精细的事务控制,提高系统的可靠性和性能。 在实际应用中,建议根据业务特点选择合适的传播行为: 1. 对于大多数场景,默认的`PropagationRequired`已经能够满足需求。 2. 当需要独立事务以隔离错误影响时,可以使用`PropagationRequiresNew`。 3. 对于需要保存点功能的场景,可以使用`PropagationNested`。 4. 当某些操作不需要事务保护时,可以使用`PropagationNotSupported`。 通过合理使用事务传播机制,可以构建更加健壮和可维护的应用程序。 # ORM事务处理-隔离级别 事务隔离级别是数据库事务处理的重要特性,它定义了一个事务可能受其他并发事务影响的程度。`GoFrame ORM`组件支持标准的`SQL`事务隔离级别,使开发者能够根据应用需求选择适当的隔离级别。 ## 基本概念 事务隔离级别主要解决并发事务执行时可能出现的以下问题: 1. **脏读(Dirty Read)**:一个事务读取了另一个未提交事务修改过的数据。 2. **不可重复读(Non-repeatable Read)**:在同一事务内,多次读取同一数据返回的结果有所不同。 3. **幻读(Phantom Read)**:在同一事务内,多次执行同一查询返回不同的数据集(行数变化)。 ## 隔离级别类型 `GoFrame ORM` 支持以下标准的事务隔离级别: | 隔离级别 | 脏读 | 不可重复读 | 幻读 | 说明 | |---------|------|-----------|------|------| | 读未提交(`Read Uncommitted`) | 可能 | 可能 | 可能 | 最低的隔离级别,允许读取未提交的数据变更 | | 读已提交(`Read Committed`) | 不可能 | 可能 | 可能 | 只能读取已经提交的数据 | | 可重复读(`Repeatable Read`) | 不可能 | 不可能 | 可能 | MySQL 的默认级别,确保同一事务中多次读取数据一致 | | 串行化(`Serializable`) | 不可能 | 不可能 | 不可能 | 最高的隔离级别,完全串行执行 | ## 优势与应用场景 不同的隔离级别适用于不同的应用场景: ### 读未提交(`Read Uncommitted`) **优势**: - 性能最好,没有锁开销 - 并发度最高 **应用场景**: - 对数据一致性要求极低的场景 - 只读统计分析,允许少量不精确数据 - 性能要求极高且能容忍数据不一致的场景 ### 读已提交(`Read Committed`) **优势**: - 避免脏读问题 - 较好的并发性能 - 大多数数据库的默认级别 **应用场景**: - 需要避免脏读但可以接受不可重复读的场景 - 读多写少的应用 - 对数据一致性有一定要求但也关注性能的场景 ### 可重复读(`Repeatable Read`) **优势**: - 避免脏读和不可重复读问题 - MySQL 的默认隔离级别 - 在同一事务中保证数据读取一致性 **应用场景**: - 需要在事务内多次读取并保证数据一致的场景 - 财务类应用 - 对数据一致性要求较高的业务逻辑 ### 串行化(`Serializable`) **优势**: - 完全避免并发问题 - 提供最高级别的数据一致性保证 **应用场景**: - 对数据一致性要求极高的关键业务 - 银行转账等金融核心交易 - 数据完整性比性能更重要的场景 ## 在 GoFrame 中使用事务隔离级别 `GoFrame ORM` 通过 `TxOptions` 结构体支持设置事务隔离级别。以下是使用不同隔离级别的示例: ### 基本用法 ```go // 使用特定隔离级别开始事务 tx, err := db.BeginWithOptions(ctx, gdb.TxOptions{ Isolation: sql.LevelReadCommitted, }) if err != nil { // 处理错误 return } // 确保事务最终会提交或回滚 defer func() { if err := recover(); err != nil { tx.Rollback() panic(err) } }() // 执行事务操作 _, err = tx.Insert(ctx, "user", g.Map{"name": "john"}) // SQL: INSERT INTO user(name) VALUES('john') // 提交事务 if err = tx.Commit(); err != nil { // 处理错误 tx.Rollback() return } ``` ### 使用不同隔离级别的示例 #### 读已提交(`Read Committed`) ```go // 使用读已提交隔离级别 tx, err := db.BeginWithOptions(ctx, gdb.TxOptions{ Isolation: sql.LevelReadCommitted, }) if err != nil { return } defer tx.Rollback() // 查询用户余额 balance, err := tx.Model("account").Where("user_id", 1).Value("balance") // SQL: SELECT balance FROM account WHERE user_id=1 // 更新余额 _, err = tx.Model("account").Where("user_id", 1).Update(g.Map{"balance": balance.Int() + 100}) // SQL: UPDATE account SET balance=balance+100 WHERE user_id=1 if err = tx.Commit(); err != nil { return } ``` #### 可重复读(`Repeatable Read`) ```go // 使用可重复读隔离级别(MySQL默认) tx, err := db.BeginWithOptions(ctx, gdb.TxOptions{ Isolation: sql.LevelRepeatableRead, }) if err != nil { return } defer tx.Rollback() // 第一次查询用户数据 user1, err := tx.Model("user").Where("id", 1).One() // SQL: SELECT * FROM user WHERE id=1 // ... 其他操作 // 第二次查询相同用户数据,在可重复读级别下,即使其他事务修改了该数据,这里读取的结果仍与第一次相同 user2, err := tx.Model("user").Where("id", 1).One() // SQL: SELECT * FROM user WHERE id=1 if err = tx.Commit(); err != nil { return } ``` #### 串行化(`Serializable`) ```go // 使用串行化隔离级别 tx, err := db.BeginWithOptions(ctx, gdb.TxOptions{ Isolation: sql.LevelSerializable, }) if err != nil { return } defer tx.Rollback() // 查询满足条件的所有用户 users, err := tx.Model("user").Where("status", "active").All() // SQL: SELECT * FROM user WHERE status='active' // 在串行化级别下,其他事务无法同时修改或添加符合此条件的记录 // 这确保了在事务执行期间,查询结果的一致性 if err = tx.Commit(); err != nil { return } ``` ### 使用事务闭包函数 `GoFrame ORM`还提供了便捷的事务闭包函数,可以同时指定隔离级别: ```go err := db.TransactionWithOptions(ctx, gdb.TxOptions{ Isolation: sql.LevelSerializable, }, func(ctx context.Context, tx gdb.TX) error { // 执行事务操作 _, err := tx.Insert(ctx, "user", g.Map{"name": "john"}) // SQL: INSERT INTO user(name) VALUES('john') if err != nil { // 返回错误会自动回滚事务 return err } // 返回nil会自动提交事务 return nil }) ``` ## 注意事项 1. 隔离级别越高,并发性能越低,请根据业务需求选择合适的隔离级别。 2. 不同数据库对隔离级别的实现可能有所不同,请参考具体数据库的文档。 3. `MySQL`的默认隔离级别是「可重复读」(`Repeatable Read`)。 4. 在使用高隔离级别时,需要注意死锁的可能性。 5. 如果不指定隔离级别,`GoFrame ORM`将使用数据库的默认隔离级别。 ## 实际应用示例 ### 电商订单处理 ```go // 使用可重复读隔离级别处理订单 err := db.TransactionWithOptions(ctx, gdb.TxOptions{ Isolation: sql.LevelRepeatableRead, }, func(ctx context.Context, tx gdb.TX) error { // 1. 查询商品库存 stock, err := tx.Model("product").Where("id", productId).Value("stock") // SQL: SELECT stock FROM product WHERE id=? if err != nil { return err } // 2. 检查库存是否充足 if stock.Int() < quantity { return errors.New("库存不足") } // 3. 创建订单 orderId, err := tx.Model("order").InsertAndGetId(g.Map{ "user_id": userId, "product_id": productId, "quantity": quantity, "status": "pending", }) // SQL: INSERT INTO order(user_id,product_id,quantity,status) VALUES(?,?,?,'pending') if err != nil { return err } // 4. 减少库存 _, err = tx.Model("product").Where("id", productId). Update(g.Map{"stock": gdb.Raw("stock - ?", quantity)}) // SQL: UPDATE product SET stock=stock-? WHERE id=? return err }) ``` ### 银行转账 ```go // 使用串行化隔离级别处理转账 err := db.TransactionWithOptions(ctx, gdb.TxOptions{ Isolation: sql.LevelSerializable, }, func(ctx context.Context, tx gdb.TX) error { // 1. 检查源账户余额 sourceBalance, err := tx.Model("account").Where("id", sourceAccountId).Value("balance") // SQL: SELECT balance FROM account WHERE id=? if err != nil { return err } if sourceBalance.Float64() < amount { return errors.New("余额不足") } // 2. 减少源账户余额 _, err = tx.Model("account").Where("id", sourceAccountId). Update(g.Map{"balance": gdb.Raw("balance - ?", amount)}) // SQL: UPDATE account SET balance=balance-? WHERE id=? if err != nil { return err } // 3. 增加目标账户余额 _, err = tx.Model("account").Where("id", targetAccountId). Update(g.Map{"balance": gdb.Raw("balance + ?", amount)}) // SQL: UPDATE account SET balance=balance+? WHERE id=? // 4. 记录交易历史 _, err = tx.Model("transaction").Insert(g.Map{ "source_account_id": sourceAccountId, "target_account_id": targetAccountId, "amount": amount, "time": gtime.Now(), }) // SQL: INSERT INTO transaction(source_account_id,target_account_id,amount,time) VALUES(?,?,?,?) return err }) ``` ## 总结 事务隔离级别是数据库并发控制的重要机制,`GoFrame ORM` 通过支持标准的`SQL`隔离级别,使开发者能够根据应用需求灵活选择合适的隔离级别,在数据一致性和性能之间取得平衡。 在实际应用中,建议根据业务场景的具体需求选择合适的隔离级别: - 对于一般的 Web 应用,「读已提交」通常是一个好的选择 - 对于财务和订单处理,「可重复读」提供了更好的数据一致性保证 - 对于极其重要的金融交易,可以考虑使用「串行化」,但需要注意性能影响 # ORM事务处理-只读模式 ## 基本介绍 事务只读模式(`ReadOnly`)是`GoFrame ORM`提供的一种事务处理特性,它允许开发者将事务标记为只读状态。 在只读事务中,数据库会禁止任何修改操作(如`INSERT`、`UPDATE`、`DELETE`等),只允许执行查询操作。 `GoFrame ORM`通过`TxOptions`结构体的`ReadOnly`字段支持此功能。 ## 只读模式的优点 1. **提高安全性**:防止意外的数据修改操作,确保事务中只能进行读取操作 2. **优化性能**:数据库可以针对只读事务进行特定优化,如避免获取写锁,减少锁竞争 3. **减少资源消耗**:只读事务通常消耗更少的数据库资源,特别是在高并发场景下 4. **提高并发度**:多个只读事务可以并行执行而不会相互阻塞 5. **支持负载均衡**:只读事务可以被路由到只读副本,减轻主库压力 ## 应用场景 1. **数据报表生成**:生成复杂报表时需要查询大量数据但不需要修改数据 2. **数据导出功能**:导出数据时确保数据一致性,同时防止意外修改 3. **复杂数据查询**:需要在事务中执行多个相关查询以确保数据一致性 4. **只读API接口**:为只提供数据查询的API接口提供事务保障 5. **数据分析操作**:执行复杂的数据分析查询时确保不会修改数据 6. **审计和日志查询**:查看历史记录和审计日志时防止意外修改 ## 基本使用 `GoFrame ORM`提供了简单易用的API来创建只读事务,通过`BeginWithOptions`或`TransactionWithOptions`方法并设置`ReadOnly: true`即可启用只读模式。 ### 使用`BeginWithOptions` ```go // 开始一个只读事务 tx, err := db.BeginWithOptions(ctx, gdb.TxOptions{ ReadOnly: true, }) // SQL: BEGIN if err != nil { // 处理错误 return } // 确保事务最终会提交或回滚 defer func() { if err := recover(); err != nil { tx.Rollback() panic(err) } }() // 执行查询操作 users, err := tx.Model("user").All() // SQL: SELECT * FROM user // 尝试执行修改操作会失败 _, err = tx.Update("user", g.Map{"status": "active"}, "id=1") // 在只读事务中,这将返回错误 // 提交事务 if err = tx.Commit(); err != nil { tx.Rollback() return } ``` ### 使用`TransactionWithOptions` ```go // 使用事务闭包函数 err := db.TransactionWithOptions(ctx, gdb.TxOptions{ ReadOnly: true, }, func(ctx context.Context, tx gdb.TX) error { // 执行查询操作 users, err := tx.Model("user").Where("status", "active").All() // SQL: SELECT * FROM user WHERE status='active' if err != nil { return err } // 处理查询结果 fmt.Println("Active users count:", len(users)) // 尝试执行修改操作会失败并自动回滚事务 _, err = tx.Model("user").Data(g.Map{"login_time": gtime.Now()}).Where("id", 1).Update() // 在只读事务中,这将返回错误 return err // 返回nil会自动提交事务,返回错误会自动回滚 }) ``` ## MySQL中的只读事务 在`MySQL`中,只读事务通过以下`SQL`语句实现: ```sql -- 开始一个只读事务 SET TRANSACTION READ ONLY; START TRANSACTION; -- 执行查询操作 SELECT * FROM user; -- 尝试执行修改操作会失败 UPDATE user SET status = 'active' WHERE id = 1; -- 错误: ERROR 1792 (25006): Cannot execute statement in a READ ONLY transaction. -- 提交事务 COMMIT; ``` ## 注意事项 1. 只读事务中尝试执行数据修改操作会导致错误,事务将被回滚 2. 不同数据库对只读事务的支持和实现可能有所不同 3. 只读事务主要用于提高安全性和性能,而不是强制的访问控制机制 4. 在高并发系统中,合理使用只读事务可以显著提高性能 5. 只读事务通常可以被路由到数据库的只读副本上执行 ## 实际应用示例 ### 报表生成 ```go // 使用只读事务生成销售报表 err := db.TransactionWithOptions(ctx, gdb.TxOptions{ ReadOnly: true, }, func(ctx context.Context, tx gdb.TX) error { // 1. 查询总销售额 totalSales, err := tx.Model("order").Where("status", "completed").Sum("amount") // SQL: SELECT SUM(amount) FROM order WHERE status='completed' if err != nil { return err } // 2. 查询各产品销售情况 productSales, err := tx.Model("order_item") .Fields("product_id, SUM(quantity) as total_quantity, SUM(amount) as total_amount") .Group("product_id") .Order("total_amount DESC") .All() // SQL: SELECT product_id, SUM(quantity) as total_quantity, SUM(amount) as total_amount // FROM order_item GROUP BY product_id ORDER BY total_amount DESC if err != nil { return err } // 3. 查询热门客户 topCustomers, err := tx.Model("order") .Fields("customer_id, COUNT(*) as order_count, SUM(amount) as total_spent") .Group("customer_id") .Order("total_spent DESC") .Limit(10) .All() // SQL: SELECT customer_id, COUNT(*) as order_count, SUM(amount) as total_spent // FROM order GROUP BY customer_id ORDER BY total_spent DESC LIMIT 10 // 生成报表逻辑... return nil // 返回nil自动提交事务 }) ``` ### 数据导出 ```go // 使用只读事务导出用户数据 err := db.TransactionWithOptions(ctx, gdb.TxOptions{ ReadOnly: true, }, func(ctx context.Context, tx gdb.TX) error { // 查询用户基本信息 users, err := tx.Model("user").All() // SQL: SELECT * FROM user if err != nil { return err } // 对每个用户查询其详细信息 for _, user := range users { userId := user["id"] // 查询用户地址 address, err := tx.Model("user_address").Where("user_id", userId).One() // SQL: SELECT * FROM user_address WHERE user_id=? if err != nil { return err } // 查询用户订单 orders, err := tx.Model("order").Where("user_id", userId).All() // SQL: SELECT * FROM order WHERE user_id=? if err != nil { return err } // 导出数据处理逻辑... } return nil }) ``` ## 事务只读模式与`DryRun`特性的区别 `GoFrame ORM`除了提供事务的只读模式外,还提供了`DryRun`特性(相关章节 [ORM高级特性-空跑特性](./gdb-senior-dry-run.md))。 虽然这两个特性都能防止数据修改,但它们在实现机制、应用场景和影响范围上有明显区别: ### 实现机制 1. **只读事务(ReadOnly)**: - 在数据库层面实现,通过设置事务的只读属性 - 由数据库引擎强制执行,尝试修改操作会返回数据库错误 - 通过`TxOptions`结构体的`ReadOnly`字段启用,仅影响当前事务 2. **DryRun特性**: - 在`ORM`层面实现,是一种模拟执行机制 - 由`GoFrame`框架拦截并忽略写操作,不会实际发送到数据库 - 通过配置文件、命令行参数或环境变量全局启用,影响所有数据库操作 ### 应用场景 1. **只读事务适用于**: - 生产环境中需要保证数据一致性的只读操作 - 需要利用数据库只读副本的场景 - 需要在事务中执行多个查询以保证数据一致性 - 对性能有较高要求的高并发只读操作 2. **DryRun特性适用于**: - 开发和测试环境中验证SQL语句的正确性 - 调试程序逻辑而不想实际修改数据库 - 演示系统功能而不改变数据状态 - 批处理脚本的预检查 ### 影响范围 1. **只读事务**: - 仅影响特定的事务实例 - 可以与普通事务并存于同一应用中 - 需要显式地在每个事务中启用 2. **DryRun特性**: - 全局影响所有数据库操作(包括非事务操作) - 通常作为应用级别的配置选项 - 一旦启用,所有写操作都将被忽略 ### 错误处理 1. **只读事务**: - 尝试执行写操作会立即返回数据库错误 - 错误会导致事务回滚 - 可以在代码中捕获和处理这些错误 2. **DryRun特性**: - 写操作不会返回错误,而是被静默忽略 - 应用程序会认为写操作成功执行 - 通常与日志和调试模式配合使用以查看SQL语句 ### 配置方式 1. **只读事务**: ```go // 通过代码配置,仅影响当前事务 tx, err := db.BeginWithOptions(ctx, gdb.TxOptions{ ReadOnly: true, }) ``` 2. **DryRun特性**: ```yaml # 通过配置文件启用 database: default: - link: "mysql:root:password@tcp(127.0.0.1:3306)/db" debug: true dryRun: true ``` ```bash # 或通过命令行参数启用 $ ./app --gf.gdb.dryrun=true # 或通过环境变量启用 $ export GF_GDB_DRYRUN=true $ ./app ``` ### 如何选择 - 如果需要在**生产环境**中保证数据一致性并提高性能,选择**只读事务** - 如果是在**开发/测试环境**中调试SQL语句或验证程序逻辑,选择`DryRun`特性 - 如果需要**细粒度控制**哪些操作是只读的,选择**只读事务** - 如果需要**全局禁止**所有写操作(如演示环境),选择`DryRun`特性 两者可以结合使用,例如在开发环境中使用`DryRun`进行全局调试,同时在特定场景中使用只读事务来测试事务隔离和错误处理逻辑。 ## 总结 事务只读模式是`GoFrame ORM`提供的一种重要特性,通过将事务标记为只读状态,可以提高数据库操作的安全性和性能。在只读事务中,数据库会禁止任何修改操作,只允许执行查询操作,这对于需要确保数据一致性但不需要修改数据的场景非常有用。 在实际应用中,建议在以下情况下考虑使用只读事务: - 需要在事务中执行多个查询以确保数据一致性 - 需要防止意外的数据修改操作 - 高并发系统中的只读操作 - 需要利用数据库只读副本的场景 合理使用只读事务可以显著提高系统的安全性、性能和可扩展性。与`DryRun`特性相比,只读事务更适合生产环境中的实际业务需求,而`DryRun`则更适合开发和测试阶段的调试工作。 # ORM结果处理-结果类型 ## 一、数据结构 查询结果的数据结构如下: ```go type Value = *gvar.Var // 返回数据表记录值 type Record map[string]Value // 返回数据表记录键值对 type Result []Record // 返回数据表记录列表 ``` 1. `Value/Record/Result` 用于ORM操作的结果数据类型。 2. `Result` 表示 **数据表记录列表**, `Record` 表示 **一条数据表记录**, `Value` 表示记录中的 **一条键值数据**。 3. `Value` 是 `*gvar.Var` 类型的别名类型,是一个运行时泛型,以便支持数据表不同的字段类型,方便于后续的数据类型转换。 举个: ## 二、 `Record` 数据记录 接口文档: [https://pkg.go.dev/github.com/gogf/gf/v2/database/gdb](https://pkg.go.dev/github.com/gogf/gf/v2/database/gdb) `gdb` 为数据表记录操作提供了极高的灵活性和简便性,除了支持以 `map` 的形式访问/操作数据表记录以外,也支持将数据表记录转换为 `struct` 进行处理。我们以下使用一个简单的示例来演示该特性。 首先,我们的用户表结构是这样的(简单设计的示例表): ```sql CREATE TABLE `user` ( `uid` int(10) unsigned NOT NULL AUTO_INCREMENT, `name` varchar(30) NOT NULL DEFAULT '' COMMENT '昵称', `site` varchar(255) NOT NULL DEFAULT '' COMMENT '主页', PRIMARY KEY (`uid`) ) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8; ``` 其次,我们的表数据如下: ``` uid name site 1 john https://goframe.org ``` 最后,我们的示例程序如下: ```go package main import ( "github.com/gogf/gf/v2/frame/g" "github.com/gogf/gf/v2/os/gctx" ) type User struct { Uid int Name string } func main() { var ( user *User ctx = gctx.New() ) err := g.DB().Model("user").Where("uid", 1).Scan(&user) if err != nil { g.Log().Header(false).Fatal(ctx, err) } if user != nil { g.Log().Header(false).Print(ctx, user) } } ``` 执行后,输出结果为: ```json {"Uid":1,"Name":"john"} ``` 这里,我们自定义了一个 `struct`,里面只包含了 `Uid` 和 `Name` 属性,可以看到它的属性并不和数据表的字段一致,这也是 `ORM` 灵活的特性之一:支持指定属性获取。 通过 `gdb.Model.Scan` 方法可以将查询到的数据记录转换为 `struct` 对象或者 `struct` 对象数组。由于这里传递的参数为 `&user` 即 `**User` 类型,那么将会转换为一个 **结构体对象**,如果传递为 `[]*User` 类型的参数,将会转换为 **结构体数组** 结果,请查看后续示例。具体方法介绍请查看链式操作章节。 **属性字段映射规则:** 需要注意的是, `map` 中的键名为 `uid,name,site`,而 `struct` 中的属性为 `Uid,Name`,那么他们之间是如何执行映射的呢?主要是以下几点简单的规则: 1. `struct` 中需要匹配的属性必须为 `公开属性`(首字母大写); 2. 记录结果中键名会自动按照 **`不区分大小写`** 且 **忽略 `-/_/空格` 符号** 的形式与 `struct` 属性进行匹配; 3. 如果匹配成功,那么将键值赋值给属性,如果无法匹配,那么忽略该键值; 以下是几个匹配的示例: ```html 记录键名 struct属性 是否匹配 name Name match Email Email match nickname NickName match NICKNAME NickName match Nick-Name NickName match nick_name NickName match nick_name Nick_Name match NickName Nick_Name match Nick-Name Nick_Name match ``` **提示** 由于数据库结果集转 `struct` 的底层是依靠 `gconv.Struct` 方法实现的,因此如果想要实现 **自定义的属性转换**,以及更详细的映射规则说明,请参考 [类型转换-Struct转换](./gconv-struct.md) 章节。 ## 三、 `Result` 数据集合 `Result/Record` 数据类型根据数据结果集操作的需要,往往需要根据记录中 **特定的字段** 作为键名进行数据检索,因此它包含多个用于转换 `Map/List` 的方法,同时也包含了常用数据结构 `JSON/XML` 的转换方法。 接口文档: [https://pkg.go.dev/github.com/gogf/gf/v2/database/gdb](https://pkg.go.dev/github.com/gogf/gf/v2/database/gdb) 由于方法比较简单,这里便不再举例说明。需要注意的是两个方法 `Record.Map` 及 `Result.List`,这两个方法也是使用比较频繁的方法,用以将 `ORM` 查询结果信息转换为可做展示的数据类型。由于结果集字段值底层为 `[]byte` 类型,虽然使用了新的 `Value` 类型做了封装,并且也提供了数十种常见的类型转换方法(具体请阅读 [泛型类型-gvar](../components/container-gvar.md) 章节),但是大多数时候需要直接将结果 `Result` 或者 `Record` 直接作为 `json` 或者 `xml` 数据结构返回,就需要做转换才行。 使用示例: ```go package main import ( "database/sql" "github.com/gogf/gf/v2/frame/g" "github.com/gogf/gf/v2/os/gctx" ) type User struct { Uid int Name string Site string } func main() { var ( user []*User ctx = gctx.New() ) err := g.DB().Model("user").Where("uid", 1).Scan(&user) if err != nil && err != sql.ErrNoRows { g.Log().Header(false).Fatal(ctx, err) } if user != nil { g.Log().Header(false).Print(ctx, user) } } ``` 执行后,输出结果为: ```json [{"Uid":1,"Name":"john","Site":"https://goframe.org"}] ``` # ORM结果处理-为空判断 使用 `GoFrame ORM` 对返回结果为空判断非常简便,大部分场景下直接判断返回的数据是否为 `nil` 或者长度为 `0`,或者使用 `IsEmpty/IsNil` 方法。 ## 一、数据集合(多条) ```go r, err := g.Model("order").Where("status", 1).All() if err != nil { return err } if len(r) == 0 { // 结果为空 } ``` 也可以使用 `IsEmpty` 方法: ```go r, err := g.Model("order").Where("status", 1).All() if err != nil { return err } if r.IsEmpty() { // 结果为空 } ``` ## 二、数据记录(单条) ```go r, err := g.Model("order").Where("status", 1).One() if err != nil { return err } if len(r) == 0 { // 结果为空 } ``` 也可以使用 `IsEmpty` 方法: ```go r, err := g.Model("order").Where("status", 1).One() if err != nil { return err } if r.IsEmpty() { // 结果为空 } ``` ## 三、数据字段值 返回的是一个"泛型"变量,这个只能使用 `IsEmpty` 来判断是否为空了。 ```go r, err := g.Model("order").Where("status", 1).Value() if err != nil { return err } if r.IsEmpty() { // 结果为空 } ``` ## 四、字段值数组 查询返回字段值数组本身类型为 `[]gdb.Value` 类型,因此直接判断长度是否为 `0` 即可。 ```go // Array/FindArray r, err := g.Model("order").Fields("id").Where("status", 1).Array() if err != nil { return err } if len(r) == 0 { // 结果为空 } ``` ## 五、 `Struct` 对象(注意) 关于 `Struct` 转换对象来说 **会有一点不一样**,我们直接看例子吧。 当传递的对象 **本身就是一个空指针时**,如果查询到数据,那么会在内部 **自动创建这个对象**;如果没有查询到数据,那么这个空指针仍旧是一个空指针,内部并不会做任何处理。 ```go var user *User err := g.Model("order").Where("status", 1).Scan(&user) if err != nil { return err } if user == nil { // 结果为空 } ``` 当传递的对象 **本身已经是一个初始化的对象**,如果查询到数据,那么会在内部将数据赋值给这个对象; **如果没有查询到数据,那么此时就没办法将对象做 `nil` 判断空结果**。因此 `ORM` 会返回一个 `sql.ErrNoRows` 错误,提醒开发者没有查询到任何数据并且对象没有做任何赋值,对象的所有属性还是给定的初始化数值,以便开发者可以做进一步的空结果判断。 ```go var user = new(User) err := g.Model("order").Where("status", 1).Scan(&user) if err != nil && err != sql.ErrNoRows { return err } if err == sql.ErrNoRows { // 结果为空 } ``` **提示** 所以我们推荐开发者不要传递一个初始化过后的对象给 `ORM`,而是直接传递一个对象的指针的指针类型( `**struct` 类型), `ORM` 内部会根据查询结果智能地做自动初始化。 ## 六、 `Struct` 数组 当传递的对象数组本身是一个空数组(长度为 `0`),如果查询到数据,那么会在内部自动赋值给数组;如果没有查询到数据,那么这个空数组仍旧是一个空数组,内部并不会做任何处理。 ```go var users []*User err := g.Model("order").Where("status", 1).Scan(&users) if err != nil { return err } if len(users) == 0 { // 结果为空 } ``` 当传递的对象数组本身不是空数组,如果查询到数据,那么会在内部自动从索引 `0` 位置覆盖到数组上;如果没有查询到数据,那么此时就没办法将数组做长度为 `0` 判断空结果。因此 `ORM` 会返回一个 `sql.ErrNoRows` 错误,提醒开发者没有查询到任何数据并且数组没有做任何赋值,以便开发者可以做进一步的空结果判断。 ```go var users = make([]*User, 100) err := g.Model("order").Where("status", 1).Scan(&users) if err != nil { return err } if err == sql.ErrNoRows { // 结果为空 } ``` **注意** 由于 `struct` 转换利用了 `Golang` 反射特性,执行性能会有一定的损耗。如果涉及到大量查询结果数据的 `struct` 数组对象转换,并且需要提高转换性能,请参考自定义实现对应 `struct` 的 `UnmarshalValue` 方法: [类型转换-UnmarshalValue](./gconv-unmarshal-value.md) # ORM结果处理-空数组结构返回 ## 痛点描述 经过前面的章节介绍,如果给定一个未初始化的数组(值为 `nil`),在 `ORM` 根据给定条件未查询到数据时,并不会自动初始化该数组。因此该未初始化的数组结果如果通过 `JSON` 进行编码后,会被转换为 `null` 值。 ```go package main import ( _ "github.com/gogf/gf/contrib/drivers/mysql/v2" "fmt" "github.com/gogf/gf/v2/encoding/gjson" "github.com/gogf/gf/v2/frame/g" "github.com/gogf/gf/v2/os/gtime" ) func main() { type User struct { Id uint64 // 主键 Passport string // 账号 Password string // 密码 NickName string // 昵称 CreatedAt *gtime.Time // 创建时间 UpdatedAt *gtime.Time // 更新时间 } type Response struct { Users []User } var res = &Response{} err := g.Model("user").WhereGT("id", 10).Scan(&res.Users) fmt.Println(err) fmt.Println(gjson.MustEncodeString(res)) } ``` 执行后,终端展示结果为: ```html {"Users":null} ``` 在大部分场景下, `ORM` 查询的数据需要渲染展示在浏览器页面上,也就意味着返回的数据需要给前端 `JS` 进行处理。为了对前端 `JS` 处理后端返回数据时更加友好,如果在后端查询不到数据时,期望返回一个空的数组结构,而不是返回一个 `null` 属性值。 ## 改进方案 针对这种场景,可以给 `ORM` 的 `Scan` 方法一个初始化的空数组即可。当 `ORM` 查询不到数据时,该数组属性仍然是一个空数组,而不是 `nil`,返回 `JSON` 编码后也不会是 `null` 值。 ```go package main import ( _ "github.com/gogf/gf/contrib/drivers/mysql/v2" "fmt" "github.com/gogf/gf/v2/encoding/gjson" "github.com/gogf/gf/v2/frame/g" "github.com/gogf/gf/v2/os/gtime" ) func main() { type User struct { Id uint64 // 主键 Passport string // 账号 Password string // 密码 NickName string // 昵称 CreatedAt *gtime.Time // 创建时间 UpdatedAt *gtime.Time // 更新时间 } type Response struct { Users []User } var res = &Response{ Users: make([]User, 0), } err := g.Model("user").WhereGT("id", 10).Scan(&res.Users) fmt.Println(err) fmt.Println(gjson.MustEncodeString(res)) } ``` 执行后,终端展示结果为: ```html {"Users":[]} ``` # ORM分库分表 ## 延伸阅读 - [电商系统案例,Go语言数据库分库分表实战](https://goframe.org/articles/go-ecommerce-database-sharding-millions) # ORM分库分表-分表特性 ## 基本介绍 `ORM`分表(`Table Sharding`)是`GoFrame ORM`提供的一种数据分片解决方案,允许开发者将一个逻辑表的数据分散存储到多个物理表中。分表是解决单表数据量过大问题的有效手段,通过将数据分散到不同的表中,可以显著提高查询性能和系统可扩展性。 ## 分表的优点 1. **提高查询性能**:通过减少单表数据量,降低索引深度,提高查询效率 2. **优化写入性能**:分散写入压力,减少锁竞争,提高并发写入能力 3. **简化维护**:便于按分片进行数据备份、恢复和归档 4. **突破单表数据量限制**:规避数据库单表数据量上限的限制 5. **提高可用性**:单个分片故障不会影响整个系统 ## 应用场景 1. **大规模用户系统**:按用户ID分表,应对海量用户数据 2. **订单系统**:按订单ID或时间分表,处理高并发订单写入和查询 3. **日志系统**:按时间分表,便于日志的存储和清理 4. **物联网数据**:按设备ID或时间分表,处理海量传感器数据 5. **社交媒体**:按用户ID分表,处理用户产生的大量社交数据 ## 基本使用 `GoFrame ORM`提供了简单易用的分表API,通过`Sharding`和`ShardingValue`方法即可实现分表功能。 ### 分表配置 ```go // 创建分表配置 shardingConfig := gdb.ShardingConfig{ Table: gdb.ShardingTableConfig{ Enable: true, // 启用分表 Prefix: "user_", // 分表前缀 Rule: &gdb.DefaultShardingRule{ TableCount: 4, // 分表数量 }, }, } // 使用分表配置和分片值 model := db.Model("user"). Sharding(shardingConfig). ShardingValue(10001) // 分片值,用于计算数据路由到哪个表,通常是主键ID值 ``` ### 默认分表规则 `GoFrame ORM`提供了默认的分表规则`DefaultShardingRule`,它基于分片值的哈希取模来确定表名: ```go // 默认分表规则实现 func (r *DefaultShardingRule) TableName(ctx context.Context, config ShardingTableConfig, value any) (string, error) { if r.TableCount == 0 { return "", gerror.NewCode( gcode.CodeInvalidParameter, "table count should not be 0 using DefaultShardingRule when table sharding enabled", ) } hashValue, err := getHashValue(value) if err != nil { return "", err } tableIndex := hashValue % uint64(r.TableCount) return fmt.Sprintf("%s%d", config.Prefix, tableIndex), nil } ``` ## 完整示例 首先,以`MySQL`数据库为例,我们需要创建相应的数据库表结构。对于分表示例,我们需要创建多个具有相同结构的物理表: ```sql CREATE TABLE `user_0` ( `id` int(11) NOT NULL, `name` varchar(255) NOT NULL, PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; CREATE TABLE `user_1` ( `id` int(11) NOT NULL, `name` varchar(255) NOT NULL, PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; CREATE TABLE `user_2` ( `id` int(11) NOT NULL, `name` varchar(255) NOT NULL, PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; CREATE TABLE `user_3` ( `id` int(11) NOT NULL, `name` varchar(255) NOT NULL, PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; ``` ### 基本CRUD操作 以下示例展示了如何在分表环境下进行基本的`CRUD`操作: ```go package main import ( _ "github.com/gogf/gf/contrib/drivers/mysql/v2" "github.com/gogf/gf/v2/database/gdb" "github.com/gogf/gf/v2/frame/g" ) // User 用户结构体 type User struct { Id int `json:"id"` Name string `json:"name"` } func main() { // 创建分表配置 shardingConfig := gdb.ShardingConfig{ Table: gdb.ShardingTableConfig{ Enable: true, // 启用分表 Prefix: "user_", // 分表前缀 Rule: &gdb.DefaultShardingRule{ TableCount: 4, // 分表数量 }, }, } // 准备测试数据 user := User{ Id: 1, Name: "John", } // 创建分表模型 db := g.DB() db.SetDebug(true) model := db.Model("user"). Sharding(shardingConfig). ShardingValue(user.Id) // 使用用户ID作为分片值 // 插入数据 _, err := model.Data(user).Insert() if err != nil { panic(err) } // INSERT INTO `user_1`(`id`,`name`) VALUES(1,'John') // 查询数据 var result User err = model.Where("id", user.Id).Scan(&result) if err != nil { panic(err) } // SELECT * FROM `user_1` WHERE `id`=1 LIMIT 1 g.DumpJson(result) // 更新数据 _, err = model.Data(g.Map{"name": "John Doe"}). Where("id", user.Id). Update() if err != nil { panic(err) } // UPDATE `user_1` SET `name`='John Doe' WHERE `id`=1 // 删除数据 _, err = model.Where("id", user.Id).Delete() if err != nil { panic(err) } // DELETE FROM `user_1` WHERE `id`=1 } ``` ### 自定义分表规则 可以通过实现`ShardingRule`接口来自定义分表规则: ```go package main import ( _ "github.com/gogf/gf/contrib/drivers/mysql/v2" "context" "fmt" "time" "github.com/gogf/gf/v2/database/gdb" "github.com/gogf/gf/v2/frame/g" "github.com/gogf/gf/v2/os/gtime" ) // TimeShardingRule 按时间分表的规则 type TimeShardingRule struct{} // TableName 实现按月份分表的规则 func (r *TimeShardingRule) TableName(ctx context.Context, config gdb.ShardingTableConfig, value any) (string, error) { // 将分片值转换为时间 t, ok := value.(time.Time) if !ok { return "", fmt.Errorf("sharding value must be time.Time for TimeShardingRule") } // 按年月生成表名,例如: log_202501 return fmt.Sprintf("%s%04d%02d", config.Prefix, t.Year(), t.Month()), nil } // SchemaName 实现分库规则接口 func (r *TimeShardingRule) SchemaName(ctx context.Context, config gdb.ShardingSchemaConfig, value any) (string, error) { // 这里不实现分库,返回空字符串 return "", nil } func main() { // 创建按时间分表的配置 shardingConfig := gdb.ShardingConfig{ Table: gdb.ShardingTableConfig{ Enable: true, // 启用分表 Prefix: "log_", // 分表前缀 Rule: &TimeShardingRule{}, // 自定义分表规则 }, } // 当前时间作为分片值 now := gtime.Now().Time // 创建分表模型 db := g.DB() db.SetDebug(true) model := db.Model("log"). Sharding(shardingConfig). ShardingValue(now) // 使用时间作为分片值 // 插入日志数据 _, err := model.Data(g.Map{ "content": "系统启动", "level": "info", "time": now, }).Insert() if err != nil { panic(err) } // INSERT INTO `log_202503`(`content`,`level`,`time`) VALUES('系统启动','info','2025-03-13 12:02:54') } ``` ## 注意事项 1. **分片值必须提供**:启用分表后,必须通过`ShardingValue`方法提供分片值,否则会返回错误 2. **分表规则必须设置**:启用分表后,必须设置分表规则,否则会返回错误 3. **跨分片查询限制**:默认情况下,查询只会路由到单个分片表,如需跨分片查询,需要自行实现 4. **事务限制**:分表事务只能在同一个数据库内进行,跨库分表事务需要使用分布式事务 5. **表结构一致性**:所有分片表的结构应保持一致 ## 高级用法 ### 使用`Hook`实现动态分表 `GoFrame ORM`提供了强大的`Hook`机制,可以用于实现更灵活的分表策略: ```go // 使用Hook实现动态分表 model := db.Model("log").Hook(gdb.HookHandler{ Select: func(ctx context.Context, in *gdb.HookSelectInput) (result gdb.Result, err error) { // 根据查询条件动态确定表名 in.Table = "log_" + determineTableSuffix(in.Model.GetWhere()) return in.Next(ctx) }, Insert: func(ctx context.Context, in *gdb.HookInsertInput) (result sql.Result, err error) { // 根据插入数据动态确定表名 in.Table = "log_" + determineTableSuffixFromData(in.Data) return in.Next(ctx) }, // 其他Hook方法... }) ``` 关于`Hook`特性介绍,请参考章节:[ORM链式操作-Hook特性](./gdb-chaining-hook.md) ## 总结 `GoFrame ORM`的分表特性提供了简单而强大的数据分片解决方案,适用于处理大规模数据的场景。通过合理配置分表策略,可以显著提高系统的性能和可扩展性。无论是使用内置的默认分表规则,还是实现自定义分表逻辑,`GoFrame`都提供了灵活的`API`支持。 在实际应用中,建议根据业务特点选择合适的分片键和分表策略,并注意处理好跨分片查询和事务等问题。 # ORM分库分表-分库特性 ## 基本介绍 `ORM`分库(`Schema Sharding`)是`GoFrame ORM`提供的一种数据分片解决方案,允许开发者将一个逻辑表的数据分散存储到多个物理数据库中。分库是实现数据库水平扩展的重要手段,通过将数据分散到不同的数据库节点,可以显著提高系统的处理能力和可扩展性。 ## 分库的优点 1. **突破单机性能瓶颈**:通过将数据分散到多个数据库节点,突破单个数据库服务器的性能限制 2. **提高并发处理能力**:分散读写压力到多个数据库节点,提高整体并发处理能力 3. **提高系统可用性**:单个数据库节点故障不会影响整个系统,提高系统的可用性 4. **地理分布优势**:可以根据地理位置分布数据,降低访问延迟,提高用户体验 5. **灵活的数据管理**:可以根据业务需求将不同类型的数据分布到不同的数据库节点 ## 应用场景 1. **大规模用户系统**:按用户ID或地理位置分库,应对全球用户分布 2. **电商平台**:按商户或产品类别分库,处理大规模商品数据 3. **金融系统**:按账户类型或业务线分库,实现业务隔离和高可用性 4. **多租户SaaS平台**:按租户分库,确保数据隔离和安全 5. **大数据分析系统**:按时间或数据类型分库,优化大规模数据处理 ## 基本使用 `GoFrame ORM`提供了简单易用的分库API,通过`Sharding`和`ShardingValue`方法即可实现分库功能。 ### 分库配置 ```go // 创建分库配置 shardingConfig := gdb.ShardingConfig{ Schema: gdb.ShardingSchemaConfig{ Enable: true, // 启用分库 Prefix: "db_", // 分库前缀 Rule: &gdb.DefaultShardingRule{ SchemaCount: 2, // 分库数量 }, }, } // 使用分库配置和分片值 model := db.Model("user"). Sharding(shardingConfig). ShardingValue(10001) // 分片值,用于计算数据路由到哪个数据库 ``` ### 默认分库规则 `GoFrame ORM`提供了默认的分库规则`DefaultShardingRule`,它基于分片值的哈希取模来确定数据库名: ```go // 默认分库规则实现 func (r *DefaultShardingRule) SchemaName(ctx context.Context, config ShardingSchemaConfig, value any) (string, error) { if r.SchemaCount == 0 { return "", gerror.NewCode( gcode.CodeInvalidParameter, "schema count should not be 0 using DefaultShardingRule when schema sharding enabled", ) } hashValue, err := getHashValue(value) if err != nil { return "", err } nodeIndex := hashValue % uint64(r.SchemaCount) return fmt.Sprintf("%s%d", config.Prefix, nodeIndex), nil } ``` ## 完整示例 首先,以`MySQL`数据库为例,我们需要创建多个数据库和相应的表结构。对于分库示例,我们需要创建多个具有相同表结构的数据库: ```sql -- 创建分库所需的数据库 CREATE DATABASE IF NOT EXISTS `db_0`; CREATE DATABASE IF NOT EXISTS `db_1`; -- 在每个数据库中创建相同结构的表 -- 在 db_0 中创建表 USE `db_0`; CREATE TABLE `user` ( `id` int(11) NOT NULL, `name` varchar(255) NOT NULL, PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- 在 db_1 中创建表 USE `db_1`; CREATE TABLE `user` ( `id` int(11) NOT NULL, `name` varchar(255) NOT NULL, PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; ``` ### 基本CRUD操作 以下示例展示了如何在分库环境下进行基本的`CRUD`操作: ```go package main import ( _ "github.com/gogf/gf/contrib/drivers/mysql/v2" "github.com/gogf/gf/v2/database/gdb" "github.com/gogf/gf/v2/frame/g" ) // User 用户结构体 type User struct { Id int `json:"id"` Name string `json:"name"` } func main() { // 创建分库配置 shardingConfig := gdb.ShardingConfig{ Schema: gdb.ShardingSchemaConfig{ Enable: true, // 启用分库 Prefix: "db_", // 分库前缀 Rule: &gdb.DefaultShardingRule{ SchemaCount: 2, // 分库数量 }, }, } // 准备测试数据 user := User{ Id: 1, Name: "John", } // 创建分库模型 db := g.DB() db.SetDebug(true) model := db.Model("user"). Sharding(shardingConfig). ShardingValue(user.Id) // 使用用户ID作为分片值 // 插入数据 _, err := model.Data(user).Insert() if err != nil { panic(err) } // INSERT INTO `user`(`id`,`name`) VALUES(1,'John') // 注意:实际操作的是 db_1 数据库中的 user 表 // 查询数据 var result User err = model.Where("id", user.Id).Scan(&result) if err != nil { panic(err) } // SELECT * FROM `user` WHERE `id`=1 LIMIT 1 // 注意:实际查询的是 db_1 数据库中的 user 表 g.DumpJson(result) // 更新数据 _, err = model.Data(g.Map{"name": "John Doe"}). Where("id", user.Id). Update() if err != nil { panic(err) } // UPDATE `user` SET `name`='John Doe' WHERE `id`=1 // 注意:实际更新的是 db_1 数据库中的 user 表 // 删除数据 _, err = model.Where("id", user.Id).Delete() if err != nil { panic(err) } // DELETE FROM `user` WHERE `id`=1 // 注意:实际删除的是 db_1 数据库中的 user 表 } ``` ### 自定义分库规则 可以通过实现`ShardingRule`接口来自定义分库规则: ```go package main import ( _ "github.com/gogf/gf/contrib/drivers/mysql/v2" "context" "fmt" "github.com/gogf/gf/v2/database/gdb" "github.com/gogf/gf/v2/frame/g" ) // RegionShardingRule 按地区分库的规则 type RegionShardingRule struct { // 地区到数据库的映射 RegionMapping map[string]string } // SchemaName 实现按地区分库的规则 func (r *RegionShardingRule) SchemaName(ctx context.Context, config gdb.ShardingSchemaConfig, value any) (string, error) { // 将分片值转换为地区信息 region, ok := value.(string) if !ok { return "", fmt.Errorf("sharding value must be string for RegionShardingRule") } // 获取地区对应的数据库名 if dbName, exists := r.RegionMapping[region]; exists { return dbName, nil } // 如果没有找到对应的地区,使用默认数据库 return config.Prefix + "default", nil } // TableName 实现分表规则接口 func (r *RegionShardingRule) TableName(ctx context.Context, config gdb.ShardingTableConfig, value any) (string, error) { // 这里不实现分表,返回空字符串 return "", nil } func main() { // 创建地区到数据库的映射 regionMapping := map[string]string{ "east": "db_east", "west": "db_west", "north": "db_north", "south": "db_south", } // 创建按地区分库的配置 shardingConfig := gdb.ShardingConfig{ Schema: gdb.ShardingSchemaConfig{ Enable: true, // 启用分库 Prefix: "db_", // 分库前缀 Rule: &RegionShardingRule{RegionMapping: regionMapping}, // 自定义分库规则 }, } // 分片值为用户所在地区 region := "east" // 创建分库模型 db := g.DB() db.SetDebug(true) model := g.DB().Model("user"). Sharding(shardingConfig). ShardingValue(region) // 使用地区作为分片值 // 插入用户数据 _, err := model.Data(g.Map{ "id": 1001, "name": "John", "region": region, }).Insert() if err != nil { panic(err) } // INSERT INTO `user`(`id`,`name`,`region`) VALUES(1001,'John','east') // 注意:实际操作的是 db_east 数据库中的 user 表 } ``` ### 结合分库分表 `GoFrame ORM`支持同时配置分库和分表,实现更精细的数据分片: ```go // 同时配置分库和分表 shardingConfig := gdb.ShardingConfig{ Schema: gdb.ShardingSchemaConfig{ Enable: true, // 启用分库 Prefix: "db_", // 分库前缀 Rule: &gdb.DefaultShardingRule{ SchemaCount: 2, // 分库数量 }, }, Table: gdb.ShardingTableConfig{ Enable: true, // 启用分表 Prefix: "user_", // 分表前缀 Rule: &gdb.DefaultShardingRule{ TableCount: 4, // 分表数量 }, }, } // 使用分库分表配置 model := g.DB().Model("user"). Sharding(shardingConfig). ShardingValue(10001) // 同一个分片值用于计算分库和分表 ``` ## 使用`Hook`实现动态分库 `GoFrame ORM`提供了强大的`Hook`机制,可以用于实现更灵活的分库策略: ```go // 使用Hook实现动态分库 model := db.Model("user").Hook(gdb.HookHandler{ Select: func(ctx context.Context, in *gdb.HookSelectInput) (result gdb.Result, err error) { // 根据查询条件动态确定数据库 in.Schema = determineSchema(in.Model.GetWhere()) return in.Next(ctx) }, Insert: func(ctx context.Context, in *gdb.HookInsertInput) (result sql.Result, err error) { // 根据插入数据动态确定数据库 in.Schema = determineSchemaFromData(in.Data) return in.Next(ctx) }, // 其他Hook方法... }) ``` 关于`Hook`特性介绍,请参考章节:[ORM链式操作-Hook特性](./gdb-chaining-hook.md) ## 注意事项 1. **分片值必须提供**:启用分库后,必须通过`ShardingValue`方法提供分片值,否则会返回错误 2. **分库规则必须设置**:启用分库后,必须设置分库规则,否则会返回错误 3. **跨库事务限制**:默认情况下,事务无法跨多个数据库,需要使用分布式事务来解决 4. **数据库配置要求**:所有分库节点需要在配置文件中正确配置,确保可以连接 5. **跨库查询限制**:默认情况下,查询只会路由到单个分库,如需跨库查询,需要自行实现 ## 总结 `GoFrame ORM`的分库特性提供了灵活而强大的数据库水平分片解决方案,适用于需要大规模数据处理能力的场景。通过合理配置分库策略,可以显著提高系统的性能和可扩展性。无论是使用内置的默认分库规则,还是实现自定义分库逻辑,`GoFrame`都提供了灵活的`API`支持。 在实际应用中,建议根据业务特点和数据分布情况选择合适的分库策略,并注意处理好跨库查询和事务等问题。 # ORM高级特性-RawSQL 由于 `ORM` 的安全性保障,所有输入的参数在底层都将使用预处理模式执行,防止常见的 `SQL` 注入风险。在某一些场景中,我们期望在生成执行的SQL语句中嵌入自定义的SQL语句,那么我们可以使用 `ORM` 的 `RawSQL` 特性,通过 `gdb.Raw` 类型来实现。我们来看几个示例。 ## 在 `Insert` 中使用 `RawSQL` `gdb.Raw` 是字符串类型,该类型的参数将会直接作为 `SQL` 片段嵌入到提交到底层的 `SQL` 语句中,不会被自动转换为字符串参数类型、也不会被当做预处理参数。例如: ```go // INSERT INTO `user`(`id`,`passport`,`password`,`nickname`,`create_time`) VALUES('id+2','john','123456','now()') g.Model("user").Data(g.Map{ "id": "id+2", "passport": "john", "password": "123456", "nickname": "JohnGuo", "create_time": "now()", }).Insert() // 执行报错:Error Code: 1136. Column count doesn't match value count at row 1 ``` 使用 `gdb.Raw` 改造后: ```go // INSERT INTO `user`(`id`,`passport`,`password`,`nickname`,`create_time`) VALUES(id+2,'john','123456',now()) g.Model("user").Data(g.Map{ "id": gdb.Raw("id+2"), "passport": "john", "password": "123456", "nickname": "JohnGuo", "create_time": gdb.Raw("now()"), }).Insert() ``` ## 在 `Update` 中使用 `RawSQL` ```go // UPDATE `user` SET login_count='login_count+1',update_time='now()' WHERE id=1 g.Model("user").Data(g.Map{ "login_count": "login_count+1", "update_time": "now()", }).Where("id", 1).Update() // 执行报错:Error Code: 1136. Column count doesn't match value count at row 1 ``` 使用 `gdb.Raw` 改造后: ```go // UPDATE `user` SET login_count=login_count+1,update_time=now() WHERE id=1 g.Model("user").Data(g.Map{ "login_count": gdb.Raw("login_count+1"), "update_time": gdb.Raw("now()"), }).Where("id", 1).Update() ``` ## 在 `Select` 中使用 `RawSQL` 时间函数 `now()` 将会被转换为字符串作为 `SQL` 参数执行: ```go // SELECT * FROM `user` WHERE `created_at`<'now()' g.Model("user").WhereLT("created_at", "now()").All() ``` 使用 `gdb.Raw` 改造后: ```go // SELECT * FROM `user` WHERE `created_at` `Value` 类型是 `*gvar.Var` 类型的别名,因此可以使用 `gvar.Var` 数据类型的所有转换方法,具体请查看 [泛型类型-gvar](../components/container-gvar.md) 章节 使用示例: 首先,数据表定义如下: ``` # 商品表 CREATE TABLE `goods` ( `id` int(10) unsigned NOT NULL AUTO_INCREMENT, `title` varchar(300) NOT NULL COMMENT '商品名称', `price` decimal(10,2) NOT NULL COMMENT '商品价格', ... PRIMARY KEY (`id`) ) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8; ``` 其次,数据表中的数据如下: ``` id title price 1 IPhoneX 5999.99 ``` 最后,示例代码如下: ```go if r, err := g.Model("goods").FindOne(1); err == nil { fmt.Printf("goods id: %d\n", r["id"].Int()) fmt.Printf("goods title: %s\n", r["title"].String()) fmt.Printf("goods proce: %.2f\n", r["price"].Float32()) } else { g.Log().Error(gctx.New(), err) } ``` 执行后,输出结果为: ``` goods id: 1 goods title: IPhoneX goods proce: 5999.99 ``` # ORM高级特性-连接池状态 我们可以通过 `DB.Stats` 方法获取 `orm` 对象的连接池状态。我们来看个示例: ```go package main import ( _ "github.com/gogf/gf/contrib/drivers/mysql/v2" "github.com/gogf/gf/v2/database/gdb" "github.com/gogf/gf/v2/frame/g" "github.com/gogf/gf/v2/os/gctx" ) func main() { var ctx = gctx.New() db, err := gdb.New(gdb.ConfigNode{ Link: "mysql:root:12345678@tcp(127.0.0.1:3306)/test", }) if err != nil { g.Log().Fatal(ctx, err) } err = db.PingMaster() if err != nil { g.Log().Fatal(ctx, err) } stats := db.Stats(ctx) g.Dump(stats) } ``` 执行后,终端输出如下,可以看到每个链接的数据库节点以及对应的连接池状态信息。 ```html [ { node: { Host: "127.0.0.1", Port: "3306", User: "root", Pass: "12345678", Name: "test", Type: "mysql", Link: "", Extra: "", Role: "", Debug: false, Prefix: "", DryRun: false, Weight: 0, Charset: "utf8", Protocol: "tcp", Timezone: "", Namespace: "", MaxIdleConnCount: 0, MaxOpenConnCount: 0, MaxConnLifeTime: 0, QueryTimeout: 0, ExecTimeout: 0, TranTimeout: 0, PrepareTimeout: 0, CreatedAt: "", UpdatedAt: "", DeletedAt: "", TimeMaintainDisabled: false, }, stats: { MaxOpenConnections: 0, OpenConnections: 1, InUse: 0, Idle: 1, WaitCount: 0, WaitDuration: 0, MaxIdleClosed: 0, MaxIdleTimeClosed: 0, MaxLifetimeClosed: 0, }, }, ] ``` # ORM高级特性-内嵌结构支持 `GoFrame ORM` 组件针对于 `struct` 内嵌结构提供了良好的支持,包括参数传递、结果处理。例如: ```go type Base struct { Uid int `orm:"uid"` CreateAt *gtime.Time `orm:"create_at"` UpdateAt *gtime.Time `orm:"update_at"` DeleteAt *gtime.Time `orm:"delete_at"` } type User struct { Base Passport string `orm:"passport"` Password string `orm:"password"` Nickname string `orm:"nickname"` } ``` 并且,无论多少层级的 `struct` 嵌套, `ORM` 的参数传递和结果处理都支持。 # ORM高级特性-自定义类型转换 当我们需要将查询的结果转换到自定义的类型中,无论是作为直接转换的类型还是作为 `struct` 的属性,都可以通过实现特定的接口来实现。详细介绍请参考 [类型转换-UnmarshalValue](./gconv-unmarshal-value.md) 章节。 # ORM接口开发 `gdb` 模块使用了非常灵活且扩展性强的接口设计,接口设计允许开发者可以非常方便地自定义实现和替换接口定义中的任何方法。 ## `DB` 接口 接口文档: [https://pkg.go.dev/github.com/gogf/gf/v2/database/gdb#DB](https://pkg.go.dev/github.com/gogf/gf/v2/database/gdb#DB) `DB` 接口是数据库操作的核心接口,也是我们通过 `ORM` 操作数据库时最常用的接口,这里主要对接口的几个重要方法做说明: 1. `Open` 方法用于创建特定的数据库连接对象,返回的是标准库的 `*sql.DB` 通用数据库对象。 2. `Do*` 系列方法的第一个参数 `link` 为 `Link` 接口对象,该对象在 `master-slave` 模式下可能是一个主节点对象,也可能是从节点对象,因此如果在继承的驱动对象实现中使用该 `link` 参数时,注意当前的运行模式。 `slave` 节点在大部分的数据库主从模式中往往是不可写的。 3. `HandleSqlBeforeCommit` 方法将会在每一条 `SQL` 提交给数据库服务端执行时被调用做一些提交前的回调处理。 4. 其他接口方法详见接口文档或者源码文件。 ## `DB` 接口关系 `GoFrame ORM Relations` ## `Driver` 接口 接口文档: [https://pkg.go.dev/github.com/gogf/gf/v2/database/gdb#Driver](https://pkg.go.dev/github.com/gogf/gf/v2/database/gdb#Driver) 开发者自定义的驱动需要实现以下接口: ```go // Driver is the interface for integrating sql drivers into package gdb. type Driver interface { // New creates and returns a database object for specified database server. New(core *Core, node *ConfigNode) (DB, error) } ``` 其中的 `New` 方法用于根据 `Core` 数据库基础对象以及 `ConfigNode` 配置对象创建驱动对应的数据库操作对象,需要注意的是,返回的数据库对象需要实现 `DB` 接口。而数据库基础对象 `Core` 已经实现了 `DB` 接口,因此开发者只需要」继承」 `Core` 对象,然后根据需要覆盖对应的接口实现方法即可。 # ORM接口开发-回调处理 ## 基本介绍 自定义回调处理是最常见的接口开发实现,开发中只需要对接口中的部分方法进行 **替换与修改**,在驱动默认实现逻辑中注入自定义逻辑。参考接口关系图( [ORM接口开发](./gdb-interface.md))我们可以知道,所有的 `SQL` 语句执行必定会通过 `DoQuery` 或者 `DoExec` 或者 `DoFilter` 接口,根据需求在自定义的驱动中 **实现并覆盖** 相关接口方法实现所需功能即可。 其中,最常见的使用场景是在 `ORM` 底层实现对 `SQL` 的 **日志记录或者鉴权等统一判断操作**。 ## 使用示例 我们来看一个自定义回调处理的示例,现需要将所有执行的 `SQL` 语句记录到 `monitor` 表中,以方便于进行 `SQL` 审计。因此通过自定义 `Driver` 然后覆盖 `ORM` 的底层接口方法来实现是最简单的。为简化示例编写,以下代码实现了一个自定义的 `MySQL` 驱动,该驱动继承于 `drivers` 下 `mysql` 模块内已经实现的 `Driver`。 ```go package driver import ( "context" "github.com/gogf/gf/contrib/drivers/mysql/v2" "github.com/gogf/gf/v2/database/gdb" "github.com/gogf/gf/v2/os/gtime" ) // MyDriver is a custom database driver, which is used for testing only. // For simplifying the unit testing case purpose, MyDriver struct inherits the mysql driver // gdb.Driver and overwrites its functions DoQuery and DoExec. // So if there's any sql execution, it goes through MyDriver.DoQuery/MyDriver.DoExec firstly // and then gdb.Driver.DoQuery/gdb.Driver.DoExec. // You can call it sql "HOOK" or "HiJack" as your will. type MyDriver struct { *mysql.Driver } var ( // customDriverName is my driver name, which is used for registering. customDriverName = "MyDriver" ) func init() { // It here registers my custom driver in package initialization function "init". // You can later use this type in the database configuration. if err := gdb.Register(customDriverName, &MyDriver{}); err != nil { panic(err) } } // New creates and returns a database object for mysql. // It implements the interface of gdb.Driver for extra database driver installation. func (d *MyDriver) New(core *gdb.Core, node *gdb.ConfigNode) (gdb.DB, error) { return &MyDriver{ &mysql.Driver{ Core: core, }, }, nil } // DoCommit commits current sql and arguments to underlying sql driver. func (d *MyDriver) DoCommit(ctx context.Context, in gdb.DoCommitInput) (out gdb.DoCommitOutput, err error) { tsMilliStart := gtime.TimestampMilli() out, err = d.Core.DoCommit(ctx, in) tsMilliFinished := gtime.TimestampMilli() _, _ = in.Link.ExecContext(ctx, "INSERT INTO `monitor`(`sql`,`cost`,`time`,`error`) VALUES(?,?,?,?)", gdb.FormatSqlWithArgs(in.Sql, in.Args), tsMilliFinished-tsMilliStart, gtime.Now(), err, ) return } ``` 我们看到,这里在包初始化方法 `init` 中使用了 `gdb.Register("MyDriver", &MyDriver{})` 来注册了了一个自定义名称的驱动。我们也可以通过 `gdb.Register("mysql", &MyDriver{})` 来覆盖已有的框架 `mysql` 驱动为自己的驱动。 **提示** 驱动名称 `mysql` 为框架默认的 `DriverMysql` 驱动的名称。 由于这里我们使用了一个新的驱动名称 `MyDriver`,因此在 `gdb` 配置中的 `type` 数据库类型时,需要填写该驱动名称。以下是一个使用配置的示例: ```yaml database: default: - link: "MyDriver:root:12345678@tcp(127.0.0.1:3306)/user" ``` ## 注意事项 在接口方法实现中,需要使用接口的 `Link` 输入对象参数来操作数据库,如果使用 `g.DB` 方法获取数据库对象来操作可能会引起死锁问题。 # ORM接口开发-驱动开发 框架数据库组件的驱动意义在于,数据库上层业务使用的各种方法操作不用变化,只需要修改配置中的数据库类型即可切换支持到新的数据库。 我们可以通过数据库组件的接口设计实现:新增框架默认不支持的第三方数据库驱动、对已有支持的驱动进行定制化修改等。驱动的开发并不是完整地开发一类数据库的协议实现代码,而是使用第三方已有的数据库驱动,通过实现框架数据库组件的接口,将该第三方数据库驱动对接到框架数据库组件上来,保证上层操作的一致。 ## 驱动注册 之前我们有提到 `Driver` 的驱动接口,在实现该接口之后,我们可以通过以下方法注册自定义驱动到 `gdb` 模块: ```go // Register registers custom database driver to gdb. func Register(name string, driver Driver) error ``` 其中的驱动名称 `name` 可以是已有的驱动名称,例如 `mysql`, `mssql`, `pgsql` 等等,当出现同名的驱动注册时,新的驱动将会覆盖老的驱动。 ## 驱动实现 开发一个自定义的驱动并注册到 `gdb` 模块中非常简单,可以参考 `gdb` 模块源码中已对接的数据库类型代码示例: [https://github.com/gogf/gf/tree/master/contrib/drivers](https://github.com/gogf/gf/tree/master/contrib/drivers) 需要说明的是,最常见的驱动开发或者修改方式是直接继承于现有 `*Core` 类型,因为在 `Driver` 接口中会传递该类型的对象,例如: ```go // DriverMysql is the driver for mysql database. type DriverMysql struct { *Core } // New creates and returns a database object for mysql. // It implements the interface of gdb.Driver for extra database driver installation. func (d *DriverMysql) New(core *Core, node *ConfigNode) (DB, error) { return &DriverMysql{ Core: core, }, nil } ``` ## 注意事项 一个新的驱动至少应该实现以下接口方法: | 接口方法 | 说明 | | --- | --- | | `Open` | 用于创建数据库连接。 | | `GetChars` | 用于获取该数据库的安全/转义符号。 | | `Tables` | 返回当前/指定数据库的数据表列表。 | | `TableFields` | 返回指定数据表的字段列表信息。 | # ORM最佳实践 数据库操作往往是业务项目中最复杂的部分, `GoFrame` 的数据库组件其实提供了非常强大和灵活的操作方式,我们在这里提供了一些最佳实践的案例,方便大家参考学习。 # 利用指针属性和do对象实现灵活的修改接口 大家都知道框架自带的开发工具可以生成 `do` 对象代码,该 `do` 对象主要用于查询、修改、写入等操作时对操作字段的自动 `nil` 过滤。 今天教给大家一个新的玩法,通过指针结合 `do` 对象快速实现灵活、便捷的修改操作 `API` 实现。 ## 数据结构 以下是我们使用的用户表数据结构: ```sql CREATE TABLE `user`( `id` int(10) unsigned NOT NULL AUTO_INCREMENT COMMENT '用户ID', `passport` varchar(32) NOT NULL COMMENT '账号', `password` varchar(32) NOT NULL COMMENT '密码', `nickname` varchar(32) NOT NULL COMMENT '昵称', `status` varchar(32) NOT NULL COMMENT '状态', `brief` varchar(512) NOT NULL COMMENT '备注信息', `create_at` datetime DEFAULT NULL COMMENT '创建时间', `update_at` datetime DEFAULT NULL COMMENT '修改时间', PRIMARY KEY (`id`), UNIQUE KEY `uniq_passport` (`passport`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8; ``` 其中的用户状态,我们使用了单独的类型定义,用以实现枚举值: ```go type Status string const ( StatusEnabled Status = "enabled" StatusDisabled Status = "disabled" ) ``` 通过 `gf gen dao` 命令,我们自动生成的 `do` 对象如下: ```go type User struct { g.Meta `table:"user" orm:"do:true"` Id interface{} Passport interface{} Password interface{} Nickname interface{} Status interface{} Brief interface{} CreatedAt interface{} UpdatedAt interface{} } ``` ## 请求API定义 我们来实现一个用户信息修改的 `API` 接口,这是一个运维管理接口,可以通过用户账号名称来修改用户信息。该 `API` 的定义如下: ```go type UpdateReq struct { g.Meta `path:"/user/{Id}" method:"post" summary:"修改用户信息"` Passport string `v:"required" dc:"用户账号"` Password *string `dc:"修改用户密码"` Nickname *string `dc:"修改用户昵称"` Status *Status `dc:"修改用户状态"` Brief *string `dc:"修改用户描述"` } ``` 其中,用户的可修改信息为密码、昵称、状态和描述,可能同时修改一项或者多项。这里使用了 **指针类型** 的属性参数, **用于实现:当传递该参数时执行修改,不传递时不修改。** ## 业务逻辑实现 为了简化实例,我们这里直接在控制器中将指针参数传递给 `do` 对象。我们知道当调用端没有传递该参数时,该参数为 `nil`,那么传递给 `do` 对象的字段时,仍然是 `nil`,这个时候执行数据库更新操作时, `do` 对象中的 `nil` 字段将会被自动过滤掉。 ```go func (c *Controller) Update(ctx context.Context, req *v1.UpdateReq) (res *v1.UpdateRes, err error) { _, err = dao.User.Ctx(ctx).Data(do.User{ Password: req.Password, Nickname: req.Nickname, Status: req.Status, Brief: req.Brief, }).Where(do.User{ Passport: req.Passport, }).Update() return } ``` # 复杂类型尽量使用json存储,便于Scan到对象时自动化转换,避免自定义解析 举一个。假如我们需要实现产品售卖规格列表,其中包含可选择的分片数量、分片容量以及副本数量,如下图(非现网代码,仅供示例学习): 我们的表设计如下: ```sql CREATE TABLE `sell_spec` ( `id` int(10) unsigned NOT NULL AUTO_INCREMENT COMMENT '主键', `product` varchar(45) NOT NULL COMMENT '产品名称', `resources` json NOT NULL COMMENT '资源规格(cpu:memory),例如:["0:0.25", "0:1", "1:2"]', `disk_min` int(10) DEFAULT NULL COMMENT '磁盘最小容量', `disk_max` int(10) DEFAULT NULL COMMENT '磁盘最大容量', `disk_step` int(10) DEFAULT NULL COMMENT '磁盘递增大小', `shards` json NOT NULL COMMENT '分片规格,例如:[1,3,5,8,12,16,24,32,40,48,64,80,96,128]', `replicas` json NOT NULL COMMENT '副本规格,例如:[1,2,3,4,5,6,7,8,9,12]', `created_at` datetime DEFAULT NULL COMMENT '创建时间', `updated_at` datetime DEFAULT NULL COMMENT '更新时间', PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='售卖规格配置'; ``` 其中的 `resources, shards, replicas` 我们定义为 `json` 格式,目的是可以存储自定义的资源、分片、副本规格列表(非顺序性)。那么我们的 `go struct` 定义如下: ```go // SellSpec 是通过GoFrame工具自动生成的数据结构,由工具维护。 type SellSpec struct { Id uint `description:"主键"` Product string `description:"产品名称"` Resources string `description:"资源规格(cpu:memory),例如:[\"0:0.25\", \"0:1\", \"1:2\"]"` DiskMin int `description:"磁盘最小容量"` DiskMax int `description:"磁盘最大容量"` DiskStep int `description:"磁盘递增大小"` Shards string `description:"分片规格,例如:[1,3,5,8,12,16,24,32,40,48,64,80,96,128]"` Replicas string `description:"副本规格,例如:[1,2,3,4,5,6,7,8,9,12]"` CreatedAt *gtime.Time `description:"创建时间"` UpdatedAt *gtime.Time `description:"更新时间"` } // SellSpecItem 是扩展entity的自定义数据结构, // 其中部分字段Resources/Shards/Replicas被覆盖为了数组类型,方便ORM操作时自动进行类型转换。 type SellSpecItem struct { entity.SellSpec Resources []string `dc:"资源规格"` Shards []int `dc:"分片规格"` Replicas []int `dc:"副本规格"` } ``` 那么在程序中我们可以这么来写入和查询数据记录。 数据写入: ```go _, err = dao.SellSpec.Ctx(ctx).Data(v1.SellSpecItem{ SellSpec: entity.SellSpec{ Product: "redis", DiskMin: 50, DiskMax: 1000, DiskStep: 10, }, Resources: []string{"1:2", "2:4", "4:8"}, Shards: []int{1, 3, 5, 8, 12, 16, 24, 32, 40, 48, 64, 80, 96, 128}, Replicas: []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 12}, }).Insert() ``` 数据查询, `ORM` 组件将会自动地将数据表中的记录转换为 `go struct` 对应的数组类型属性: ```go var items []v1.SellSpecItem err = dao.SellSpec.Ctx(ctx).Scan(&items) ``` # 查询时避免返回对象初始化及sql.ErrNoRows判断 ## 查询时避免返回对象初始化及 `sql.ErrNoRows` 判断 执行`SQL`查询时,请避免提前将查询结果初始化,以避免结构对象默认值的影响,避免创建不必要的对象内存。通过返回对象指针 `nil` 判断避免 `sql.ErrNoRows` 使用,降低代码对 `error` 处理的复杂度、统一项目中对空查询结果处理逻辑。 一个反面例子: ```go func (s *Service) GetOne(ctx context.Context, id uint64) (out *entity.ResourceTask, err error) { out = new(model.TaskDetail) err = dao.ResourceTask.Ctx(ctx).WherePri(id).Scan(out) if err != nil { if err == sql.ErrNoRows { err = gerror.Newf(`record not found for "%d"`, id) } return } return } ``` 在该例子中,实际返回的 `out` 对象由于对象初始化的缘故有了默认值(无论SQL是否查询到数据),并且 `sql.ErrNoRows` 的判断增加了代码逻辑中对 `error` 处理的复杂度。 建议改进如下: ```go func (s *Service) GetOne(ctx context.Context, id uint64) (out *entity.ResourceTask, err error) { err = dao.ResourceTask.Ctx(ctx).WherePri(id).Scan(&out) if err != nil { return } if out == nil { err = gerror.Newf(`record not found for "%d"`, id) } return } ``` **注意** 注意代码中 `&out` 的使用。 更多的介绍请参考: [ORM结果处理-为空判断](./gdb-result-empty-check.md) # I18N国际化 ## 基本介绍 `GoFrame` 框架提供了常用的 `I18N` 国际化组件,由 `gi18n` 模块实现。 **使用方式:** ```go import "github.com/gogf/gf/v2/i18n/gi18n" ``` **接口文档**: [https://pkg.go.dev/github.com/gogf/gf/v2/i18n/gi18n](https://pkg.go.dev/github.com/gogf/gf/v2/i18n/gi18n) # I18N国际化-配置管理 作为框架的核心组件之一,传承 `goframe` 一贯的便捷化设计思想, `I18N` 国际化组件的配置管理非常简便。 ## 文件格式 `gi18n` 国际化组件支持框架通用的五种配置文件格式: `xml/ini/yaml/toml/json/properties`(更详细的文件支持列表具体以配置管理章节为主: [配置管理](./gcfg.md))。同样的,和配置管理模块一样,框架推荐使用 `toml` 文件格式。 ## 读取路径 默认情况下 `gi18n` 会自动查找并读取当前项目源码根目录(或者当前 `PWD` 运行目录下)下的以下目录: - `manifest/i18n` - `i18n` 默认将查找到的目录作为国际化转译文件存储目录。开发者也可以通过 `SetPath` 方法自定义 `i18n` 文件的存储目录路径。 ## 文件存储 在 `i18n` 目录下可以直接按照国际化名称命名的文件如: `en.toml`/ `ja.toml`/ `zh-CN.toml`;也可以给定国际化名称目录,目录下随意自定义配置文件,如: `en/editor.toml`/ `en/user.toml`、 `zh-CN/editor.toml`/ `zh-CN/user.toml`。使用纯文件管理或者增加一级目录管理都是可以的, `gi18n` 可以智能识别加载。 **提示** 国际化的文件/目录名称都是开发者可自行定义和维护的,该名称主要用于程序中设置和使用。建议按照标准化的国际化地区语言码进行命名,具体请参考WIKI: [https://zh.wikipedia.org/wiki/ISO\_639-1](https://zh.wikipedia.org/wiki/ISO_639-1) 例如,以下的 `i18n` 目录结构以及文件格式都是支持的。 **通过单独的 `i18n` 文件区分不同的语言** ``` └── i18n ├── en.toml ├── ja.toml ├── ru.toml ├── zh-CN.toml └── zh-TW.toml ``` **通过不同的目录名称区分不同的语言** ``` └── i18n ├── en │ ├── hello.toml │ └── world.toml ├── ja │ ├── hello.yaml │ └── world.yaml ├── ru │ ├── hello.ini │ └── world.ini ├── zh-CN │ ├── hello.json │ └── world.json └── zh-TW ├── hello.xml └── world.xml ``` **不同语言可以存在不同文件格式** ``` └── i18n ├── en.toml ├── ja.yaml ├── ru.ini ├── zh-CN.json └── zh-TW.xml ``` ## 资源管理器 `gi18n` 默认支持资源管理器(详情请参考章节: [资源管理](./gres.md)),默认情况下会从 `gres` 配置管理器中检索 `manifest/i18n` 及 `i18n` 目录,或者开发者设置的 `i18n` 目录路径。 # I18N国际化-使用介绍 ## 对象创建 ### 单例对象 大多数场景下,我们推荐使用 `g.I18n` 单例对象,并可自定义配置不同的单例对象,但是需要注意的是,单例对象的配置修改是全局有效的。例如: ```go g.I18n().T(context.TODO(), "{#hello} {#world}") ``` **提示** 在所有的转译方法中,第一个参数都要求输入 `Context` 上下文变量参数,用于上下文变量的传递、翻译语言的指定、后续的可扩展能力。该参数虽然直接传递 `nil` 也是可以的,但是为保证程序的严谨性,我们建议当不知道传递什么或者没有特殊要求的时候传递 `context.TODO()` 或者 `context.Background()` 来替代。 ### 独立对象 其次,我们也可以模块化独立使用 `gi18n` 模块,通过 `gi18n.New()` 方法创建独立的 `i18n` 对象,然后开发者自行进行管理。例如: ```go i18n := gi18n.New() i18n.T(context.TODO(), "{#hello} {#world}") ``` ## 语言设置 设置转译语言有两种方式,一种是通过 `SetLanguage` 方法设置当前 `I18N` 对象统一的转译语言,另一种是通过上下文设置当前执行转译的语言。 ### `SetLanguage` 例如,我们通过 `g.I18n().SetLanguage("zh-CN")` 即可设置当前转译对象的转译语言,随后使用该对象都将使用 `zh-CN` 进行转译。需要注意的是,组件的配置方法往往都不是并发安全的,该方法也同样如此,需要在程序初始化时进行设置,随后不能在运行时进行更改。 ### `WithLanguage` `WithLanguage` 方法可以创建一个新的上下文变量,并临时设置当前转译的语言,由于该方法作用于 `Context` 上下文,因此是并发安全的,常用于运行时转译语言设置。我们来看一个例子: ```go ctx := gi18n.WithLanguage(context.TODO(), "zh-CN") i18n.Translate(ctx, `hello`) ``` 其中 `WithLanguage` 方法定义如下: ```go // WithLanguage append language setting to the context and returns a new context. func WithLanguage(ctx context.Context, language string) context.Context ``` 用于将转译语言设置到上下文变量中,并返回一个新的上下文变量,该变量可用于后续的转译方法。 ## 常用方法 ### `T` 方法 `T` 方法为 `Translate` 方法的别名,也是大多数时候我们推荐使用的方法名称。 `T` 方法可以给定关键字名称,也可以直接给定模板内容,将会被自动转译并返回转译后的字符串内容。 此外, `T` 方法可以通过第二个语言参数指定需要转译的目标语言名称,该名称需要和配置文件/路径中的名称一致,往往是标准化的国际化语言缩写名称例如: `en/ja/ru/zh-CN/zh-TW` 等等。否则,将会自动使用 `Manager` 转译对象中设置的语言进行转译。 方法定义: ```go // T translates with configured language and returns the translated content. func T(ctx context.Context, content string) ``` **关键字转译** 关键字的转译直接给 `T` 方法传递关键字即可,例如: `T(context.TODO(), "hello")`、 `T(context.TODO(), "world")`。 `I18N` 组件将会优先将给定的关键字进行转译,转译成后返回转译后的内容,否则直接展示原内容。 **模板内容转译** `T` 方法支持模板内容转换,模板中的关键字默认使用 `{#` 和 `}` 标签进行包含,模板解析时将会自动替换该标签中的关键字内容。使用示例: #### 1)目录结构 ```bash ├── main.go └── i18n ├── en.toml ├── ja.toml ├── ru.toml └── zh-CN.toml ``` #### 2)转译文件 `ja.toml` ```bash hello = "こんにちは" world = "世界" ``` `ru.toml` ```bash hello = "Привет" world = "мир" ``` `zh-CN.toml` ```bash hello = "你好" world = "世界" ``` #### 3)示例代码 ```go package main import ( "fmt" "github.com/gogf/gf/v2/os/gctx" "github.com/gogf/gf/v2/i18n/gi18n" ) func main() { var ( ctx = gctx.New() i18n = gi18n.New() ) i18n.SetLanguage("en") fmt.Println(i18n.Translate(ctx, `hello`)) fmt.Println(i18n.Translate(ctx, `GF says: {#hello}{#world}!`)) i18n.SetLanguage("ja") fmt.Println(i18n.Translate(ctx, `hello`)) fmt.Println(i18n.Translate(ctx, `GF says: {#hello}{#world}!`)) i18n.SetLanguage("ru") fmt.Println(i18n.Translate(ctx, `hello`)) fmt.Println(i18n.Translate(ctx, `GF says: {#hello}{#world}!`)) ctx = gi18n.WithLanguage(ctx, "zh-CN") fmt.Println(i18n.Translate(ctx, `hello`)) fmt.Println(i18n.Translate(ctx, `GF says: {#hello}{#world}!`)) } ``` 执行后,终端输出为: ```bash Hello GF says: HelloWorld! こんにちは GF says: こんにちは世界! Привет GF says: Приветмир! 你好 GF says: 你好世界! ``` ### `Tf` 方法 我们都知道,模板内容中也会存在一些变量,这些变量可以通过 `Tf` 方法进行转译。 `Tf` 为 `TranslateFormat` 的别名,该方法支持格式化转译内容,字符串格式化语法参考标准库 `fmt` 包的 `Sprintf` 方法。 **方法定义:** ```go // Tf translates, formats and returns the with configured language // and given . func Tf(ctx context.Context, format string, values ...interface{}) string ``` 我们来看一个简单的示例。 #### 1)目录结构 ```bash ├── main.go └── i18n ├── en.toml └── zh-CN.toml ``` #### 2) **转译文件** `en.toml` ```bash OrderPaid = "You have successfully complete order #%d payment, paid amount: ¥%0.2f." ``` `zh-CN.toml ` ```bash OrderPaid = "您已成功完成订单号 #%d 支付,支付金额¥%.2f。" ``` #### 3) **示例代码** ```go package main import ( "fmt" "github.com/gogf/gf/v2/i18n/gi18n" "github.com/gogf/gf/v2/os/gctx" ) func main() { var ( ctx = gctx.New() orderId = 865271654 orderAmount = 99.8 ) i18n := gi18n.New() i18n.SetLanguage("en") fmt.Println(i18n.Tf(ctx, `{#OrderPaid}`, orderId, orderAmount)) i18n.SetLanguage("zh-CN") fmt.Println(i18n.Tf(ctx, `{#OrderPaid}`, orderId, orderAmount)) } ``` 执行后,终端输出为: ```bash You have successfully complete order #865271654 payment, paid amount: ¥99.80. 您已成功完成订单号 #865271654 支付,支付金额¥99.80。 ``` **备注** 为方便演示,该示例中对支付金额的处理比较简单,在实际项目中往往需要在业务代码中对支付金额的单位按照区域做自动转换,再渲染 `i18n` 显示内容。 ### 上下文设置转译语言 我们将上面的示例做些改动来演示。 #### 1)目录结构 ```bash ├── main.go └── i18n ├── en.toml └── zh-CN.toml ``` #### 2)转译文件 `en.toml` ```bash OrderPaid = "You have successfully complete order #%d payment, paid amount: ¥%0.2f." ``` `zh-CN.toml` ```bash OrderPaid = "您已成功完成订单号 #%d 支付,支付金额¥%.2f。" ``` #### 3)示例代码 ```go package main import ( "context" "fmt" "github.com/gogf/gf/v2/frame/g" "github.com/gogf/gf/v2/i18n/gi18n" ) func main() { var ( orderId = 865271654 orderAmount = 99.8 ) fmt.Println(g.I18n().Tf( gi18n.WithLanguage(context.TODO(), `en`), `{#OrderPaid}`, orderId, orderAmount, )) fmt.Println(g.I18n().Tf( gi18n.WithLanguage(context.TODO(), `zh-CN`), `{#OrderPaid}`, orderId, orderAmount, )) } ``` 执行后,终端输出为: ```bash You have successfully complete order #865271654 payment, paid amount: ¥99.80. 您已成功完成订单号 #865271654 支付,支付金额¥99.80。 ``` **备注** 为方便演示,该示例中对支付金额的处理比较简单,在实际项目中往往需要在业务代码中对支付金额的单位按照区域做自动转换,再渲染 `i18n` 显示内容。 ## `I18N` 与视图引擎 `gi18n` 默认已经集成到了 `GoFrame` 框架的视图引擎中,直接在模板文件/内容中使用 `gi18n` 的关键字标签即可。我们同样可以通过上下文变量的形式来设置当前请求的转译语言。 **提示** 此外,我们也可以通过设置模板变量 `I18nLanguage` 设置当前模板的解析语言,该变量可以控制不同的模板内容按照不同的国际化语言进行解析。 使用示例: ```go package main import ( "github.com/gogf/gf/v2/frame/g" "github.com/gogf/gf/v2/i18n/gi18n" "github.com/gogf/gf/v2/net/ghttp" ) func main() { s := g.Server() s.Group("/", func(group *ghttp.RouterGroup) { group.Middleware(func(r *ghttp.Request) { r.SetCtx(gi18n.WithLanguage(r.Context(), r.Get("lang", "zh-CN").String())) r.Middleware.Next() }) group.ALL("/", func(r *ghttp.Request) { r.Response.WriteTplContent(`{#hello}{#world}!`) }) }) s.SetPort(8199) s.Run() } ``` 执行后,访问以下页面,将会输出: 1. [http://127.0.0.1:8199](http://127.0.0.1:8199/) ```bash 你好世界! ``` 2. [http://127.0.0.1:8199/?lang=ja](http://127.0.0.1:8199/?lang=ja) ```bash こんにちは世界! ``` # 资源管理 ## 基本介绍 `资源管理` 是指可以将任意文件/目录打包为 `Golang` 源码文件,并且编译到可执行文件中,随着可执行文件发布。 资源文件在程序启动时将会自解压释放到内存中,供程序只读访问,可以将它当做基于内存的文件管理器。同时, `GoFrame` 的资源管理特性也支持将文件/目录打包为独立的二进制资源文件使用。由于资源文件在程序运行时是基于内存的文件操作,没有磁盘 `IO` 的开销,因此其文件操作效率非常高。 **使用方式**: ```go import "github.com/gogf/gf/v2/os/gres" ``` **接口文档**: [https://pkg.go.dev/github.com/gogf/gf/v2/os/gres](https://pkg.go.dev/github.com/gogf/gf/v2/os/gres) ## 组件特性 `gres` 资源管理组件具有以下显著特点: 1. 可将任意的文件/目录打包为 `Go` 文件,支持自定义加解密。 2. 打包的 `Go` 文件/资源文件自动压缩,常见 `css/js` 等文件可达到 `50~90%` 的压缩率。 3. 支持对打包为 `Go` 文件的资源内容,方便地导出到本地文件系统。 4. 资源管理器内容完全基于内存,并且内容只读,无法动态修改。 5. 资源管理器默认整合支持到了 `WebServer`、配置管理、模板引擎模块中。 6. 任意文件如网站静态文件、配置文件等可编译到二进制文件中,也可编译到发布的可执行文件中。 7. 开发者可只需编译发布一个可执行文件,除了方便了软件分发,也为保护软件知识产权内容提供了可能。 ## 与 `embed.FS` 的比较 从 `Golang v1.16` 版本开始官方提供了静态文件嵌入的 `embed.FS` 特性,整体底层设计和 `gres` 组件类似,压缩比、执行效率也差不多,只是在使用设计和工程化管理上有比较大的差别。 `GoFrame` 资源管理组件的功能更加丰富,框架的核心组件已经完全对接了 `gres` 资源管理组件,并且在 `GoFrame` 提供的标准的工程化管理下能够做到对开发者无感知地使用资源管理特性,具体可以参考章节 [资源管理-最佳实践](./gres-practice.md)。 未来 `GoFrame` 基础框架底层不会考虑内置对 `embed.FS` 组件的支持, `embed.FS` 与 `gres` 组件可以独立使用互不影响。 # 资源管理-工具打包 我们可以通过`gf`命令行工具的`pack`命令实现对任意文件/目录的打包,关于`gf`命令行工具的安装和使用具体请查看[资源打包-pack](../cli/pack.md)章节。由于通过命令行工具进行打包比较简便,因此也是推荐的打包方式。 ## `gf pack`生成`Go`文件 比较推荐的方式是将`Go`文件直接生成到`boot`启动目录,并设置生成`Go`文件的包名为`boot`,这样该资源文件将会被自动引入到项目中。我们将项目的`config`、`public`、`template`三个目录的文件打包到`Go`文件,打包命令为: ``` gf pack config,public,template packed/data.go -n packed ``` 生成的`Go`文件内容类似于: ```go package packed import "github.com/gogf/gf/v2/os/gres" func init() { if err := gres.Add("H4sIAAAAAAAC/5y8c5Bl0Zbuu9O2bVaq0rZZ6Urbtm3bNnfatipto9"); err != nil { panic(err) } } ``` 可以看到,生成的`Go`文件中通过`gres.Add`方法将资源文件的二进制内容添加到默认的资源管理器中,该方法的参数是压缩过后的`BASE64`字符串,将会在程序启动的时候做解压并在内存中生成一个文件树对象,便于在运行时快速操作文件。 ## 使用打包的`Go`文件 ### 在`boot`包中优先引入`packed`资源包 在项目的`boot`程序启动设置包中自动引入`packed`资源包,并且应当作为第一个引入的包,以便于其他引入的包在初始化时(`init`方法中)便能使用到资源内容,例如像这样(`module`名称为`my-app`): ```go import ( _ "my-app/packed" // 其他包 ) ``` 这里建议引入`packed`包和其他包之间加入一个空行以作区分,特别是`Goland` IDE的`import`插件不会将引入包进行自动排序。 ### 在`main`包中优先引入`boot`包 由于项目的`main`入口程序文件会引入`boot`包,并且应当作为第一个引入的包: ```go import ( _ "my-app/boot" // 其他包 ) ``` 这里建议引入`boot`包和其他包之间加入一个空行以作区分,特别是`Goland` IDE的`import`插件不会将引入包进行自动排序。 随后可以在项目的任何地方使用`gres`模块来访问打包的资源文件。 > 如果使用`GoFrame`推荐的项目目录结构(新建项目),在目录结构中会存在`boot`目录(对应包名也是`boot`),用于程序启动设置。因此如果将`Go`文件生成到`boot`目录下,那么将会被自动编译进可执行文件中。 ## 打印资源管理文件列表 可以通过`gres.Dump()`方法打印出当前资源管理器中所有的文件列表,输出内容类似于: ``` 2019-09-15T13:36:28+00:00 0.00B config 2019-07-27T07:26:12+00:00 1.34K config/config.toml 2019-09-15T13:36:28+00:00 0.00B public 2019-06-25T17:03:56+00:00 0.00B public/resource 2018-12-04T12:50:16+00:00 0.00B public/resource/css 2018-12-17T12:54:26+00:00 0.00B public/resource/css/document 2018-12-17T12:54:26+00:00 4.20K public/resource/css/document/style.css 2018-08-24T01:46:58+00:00 32.00B public/resource/css/index.css 2019-05-23T03:51:24+00:00 0.00B public/resource/image 2018-08-20T05:02:08+00:00 24.01K public/resource/image/cover.png 2019-05-23T03:51:24+00:00 4.19K public/resource/image/favicon.ico 2018-08-23T01:44:50+00:00 4.19K public/resource/image/gf.ico 2018-12-04T13:04:34+00:00 0.00B public/resource/js 2019-06-27T11:06:12+00:00 0.00B public/resource/js/document 2019-06-27T11:06:12+00:00 11.67K public/resource/js/document/index.js 2019-09-15T13:36:28+00:00 0.00B template 2019-02-02T09:08:56+00:00 0.00B template/document 2018-12-04T12:49:08+00:00 0.00B template/document/include 2018-12-04T12:49:08+00:00 329.00B template/document/include/404.html 2019-03-06T01:52:56+00:00 3.42K template/document/index.html ... ``` 需要注意的是,在使用资源管理器中的资源文件时,需要严格按照其路径进行检索获取。 # 资源管理-方法打包 **提示** 本章节的示例演示打包/解包的同时也演示了对数据的加密/解密。大部分业务项目其实并不需要加密/解密操作,因此直接使用工具打包即可。 在上一章节我们介绍通过 `gf` 工具链进行文件/目录打包,并生成 `Go` 文件编译到可执行文件中。在本章节中,我们介绍资源管理中涉及到的方法,并通过一个打包/解包二进制资源文件的示例,介绍这些方法实现自定义的打包/解包功能。同时,我们也演示了如何通过自定义加解密来保护我们的资源文件内容。 **接口文档:** [https://pkg.go.dev/github.com/gogf/gf/v2/os/gres](https://pkg.go.dev/github.com/gogf/gf/v2/os/gres) **简要介绍:** 1. 通过 `Pack*`/ `Unpack*` 方法可以实现对任意文件的打包/解包功能,可以打包到二进制文件或者Go代码文件。 2. 资源管理由 `Resource` 对象实现,可实现对打包内容的添加,文件的检索查找,以及对目录的遍历等功能。 3. 资源文件由 `File` 对象实现,该文件对象和 `os.File` 文件对象类似,并且该对象实现了 `http.File` 接口。 4. `ScanDir` 用于针对于特定目录下的文件/目录检索,并且支持递归检索。 5. `ScanDirFile` 用于针对于特定目录下的文件检索,并且支持递归检索。 6. 通过 `Dump` 方法在终端打印出 `Resource` 资源对象所有的文件列表,资源管理器中的文件分隔符号统一为 `/`。 7. 此外, `gres` 资源管理模块提供了默认的 `Resource` 对象,并通过包方法提供了对该默认对象的操作。 ## 自定义打包示例 我们将项目根目录下的`public`和`config`目录打包为`data.bin`二进制文件,并通过`gaes`加密算法对生成的二进制内容进行加密。 ```go package main import ( "github.com/gogf/gf/v2/crypto/gaes" "github.com/gogf/gf/v2/os/gfile" "github.com/gogf/gf/v2/os/gres" ) var ( CryptoKey = []byte("x76cgqt36i9c863bzmotuf8626dxiwu0") ) func main() { binContent, err := gres.Pack("public,config") if err != nil { panic(err) } binContent, err = gaes.Encrypt(binContent, CryptoKey) if err != nil { panic(err) } if err := gfile.PutBytes("data.bin", binContent); err != nil { panic(err) } } ``` ## 自定义解包示例 我们使用将刚才打包生成的`data.bin`,需要解密和解包两步操作。 ```go package main import ( "github.com/gogf/gf/v2/crypto/gaes" "github.com/gogf/gf/v2/os/gfile" "github.com/gogf/gf/v2/os/gres" ) var ( CryptoKey = []byte("x76cgqt36i9c863bzmotuf8626dxiwu0") ) func main() { binContent := gfile.GetBytes("data.bin") binContent, err := gaes.Decrypt(binContent, CryptoKey) if err != nil { panic(err) } if err := gres.Add(binContent); err != nil { panic(err) } gres.Dump() } ``` 最后,我们使用 `gres.Dump()` 打印出添加成功的文件列表查看资源文件是否添加成功。 # 资源管理-方法介绍 **提示** 以下常用方法列表,文档更新可能滞后于代码新特性,更多的方法及示例请参考代码文档: [https://pkg.go.dev/github.com/gogf/gf/v2/os/gres](https://pkg.go.dev/github.com/gogf/gf/v2/os/gres) ## Add - 说明:`Add`将`content`解压并添加到默认资源对象中。`prefix`是非必要参数,表示存储到当前资源对象中的每个文件的前缀。 - 格式: ```go func Add(content string, prefix ...string) error ``` - 示例: ```go package main import "github.com/gogf/gf/v2/os/gres" func main() { if err := gres.Add("H4sIAAAAAAAC/5TWV1TTydsH8B+EpoAginEBDVIEQYp0kUTpSjAJhCwsxSwlgEtRwVCkKwJSo9IWCCChSAkoTZoivfeF0JEEQpMgvQrvcc/x1X/Zi/9zMTfzzPfMc/OZQcFBLKcBDoAD6DTsRQE/FS9wDLC/64xzl3PHuXng3GRt3R5iTFgBJt6X83cexsJLqjT4mrd9w0fQ6OPh+uAVEbGTj1k4TK1bdBu6nrT2Sab5GyC7ao5yc613l3Ly29Yfbl6fSovmph6AbK6L+TBrhl03yQhO9w7hHUcIcWSAEJd/z6I0MtJHaaKscBAb6STB5unZoVdhgDIFqQaCzyEuLwhza87xLoKCvMJtmjmYMYn63Sjx/X3IuE66VuXJKcVB08OurMS3IObBV6ilar8HjNhjTAGCzixCKecGJI4HfU3NBhngff1mmvfuihCWagSzqpwiiWaLhGkW8ZUMtrUOV0nZWSiGUl8ADw2xVAvrVMqUy2Fdmmmo4nl5CLKwynbmI/hlC5qZSd5INHqjEshnzRH8Sy60vatCdrS2MpPTWqGMEQEfxExDuC0T6/aRAmQZc8NLZ/0reVtVbeKKswKpFFBw4NFq4J7WWY2yE9p6i/oLwNCCT2HkioAirGf/cEcnDGH+6SCBPXAaCsiyOHNOMrfy8MpzqcyqbmcIUh9NqM3d240wHkieqIsLVQvYCCrFHtxYwQsjoEQNW6VrAbc/d9estPr1Cspt8kdMzResBrnITixfkXV+dH6IvwaET9i1fEFbKv2SHOHvYj8S86uPXU3zWv6LnglKVf37PpeU0WIVfU/lJFEJS++4ZAPt2x7NU1Ci+DwkxJCcWDN2IknmvjBEBoro/g0UJw2rLTQ17sCt42H05ZJ1KHyoWakNRBajN4lEB7di4EUDnXH9yfEp40qd719eKB6BRsV2o63HvMbKgno9JmOKCeTCKsmcLKPqdg+xwplYW9fUSI1IqgeMEV1/710WoQUzxyPavxZc3se1g7g4r9fS8uaZYvb7d0QmP1Qgof6MyYqy8ojAytWI2CZkYyHDerexvnz/tHI3+bNekf4Ted1wm4CRRXXwPpNvaf/RKdEvJ0PVw1h4zbCdtZVH5/Y3nlk9ciD31aPS+v4wPytp7FgDk2t6hUhHQ3pUryFB05l6FWr8Y50oQUWOCdzmfMJaGR5zDKIizdPHTiZBLNr2KKSEiQdJ/XyDq790DfrfjEmWdw/fqATXWcJqy8GeaC4g3dVTUAxU0vjc1n3Rcwwvv+RPt2iKQStpOVGrPat55gbqaiaq8LgBNkRdj3T7nBUE1+OB2o7aCJNRjZP7QFukFSRqGONqF7atay17bt2payyZJ5YEWuBmVnMC7KqjKHXLUCj0y3ZTgsnp3ZyPMc7U7ArlJj5cckJfjJFD3PM6ZL7o7zuVxT5eZWttT8do+Lwy+5fzU63SjjL41Mk9fzWuui63P1nLWsLF19Y+NmEDbqWVPDu3UZuzgWUCgKMjFJydY3YuFzkBAgAHVgD4H4xwwnn/vxHOg6p8+cPMLL0pmJqUvac3hnndgyJUmS8oRH7R8yrZw65fJCKuLSzOHKykcdwIGoL0zB2pnefaBe+QnvRMsV25wEJjYk2l6E6yahC5s051Gl8yKcNwSdom5bmPkaKirYs9P0SSM9wXevPii0iGUv2PxJTM8bRPKsfL1lfvk1O6kqX0Exhj3mnyDZf1FMCqqZ9sinQaph574+UOFgU3H0uIwNuVcYbRZ2S06elo89E8pajoz3OZCPHLq9Ikd4M8GjMX0TsiFdasw8gQumbczMQLiVK64At+IEO9jrKshRT0FtiHS97tHnJXik2CrTTmKLzeEigV2yHBamdlVzt/Oy+uY388ttv0KnXsEJHtLZdrDZS7HDyhpvC3oMhdPgf7KClBBwlHi5HY11hxb1Zrd33LYKE7Dfbs6lCLeyliUt7Jt++vi4dq3VXJ8ctsGM6ylFyVlfsietoLY6+Axns8mbGLdyFY28cyncgHm4QKfxIK1Bo9gZeKdGrmdngtkd6WoJ8aDksDw1uwbCXZX1MkUC9JW8/G4UPxBltxXLbVrnrCjg/gOr1hlRKEnrZZY8adj5KcebmdIkRpi9C8+fKrIImcV8QL8UeTe8uGHNTbyMpmSpLG80n/xvUXIQ0Or7Qe7xMgVKZqecdUOPnrznT9G2FU7cAWnfc859l7PAm65sBNtB84QJeueNjRvxFoVKWQqZ6ngI9Qi2JFaj8t3lQvNxMprPhzl3oT+dd6pfXUHySH34lvNyUT6plmYFEMuU3DCIr628SKmRGY0OZqsdwfpdoybrHg2HEx3Y8Du2vsvB08nypJIn1/Xerq8icM1iM7HK+LRZ2KYeFnGLnBtk4UXIljwBTrPsNDUkWmyhtquc055AbRDTqOqtUfK6adlDC2AreiOSUzDUhW2bZK0vG/uLedasI62TKvzrOdCTqz3Fb2m0UUsRFNC7ORz56dyDxWk0agq/fh2QfcorOXDhTuqJHqUy7uVCe60IDcvI8ZJZinotfsta+Li0HUFgweztjFPM4It8cqO+H5pa2KwcTKc3Iho0R72ssr+fQnzhlWmPJhq6CYItPnckjp9nuHkSnRmzym6C2GN5I8I8WVbWKju75d+OvbFldDghD25N2GIFMzPee7ngK5wwbJ7cGaR8NJqa1r8gEM053Lh9BKii86rjgew7aqAoS/f33+2YdCXrDwEM7kkAzBhl4d3UX2HGTXkQ1iz1RpJLp2okp8fL9aJt9zWTnvAEncndWB9RusvDMeNiehnp/zO86fTyUk+BZkmS4r2gnR4zbV98UF8TlTjf1RuItnalBGiXnMoa0dydUvRMX9fV2cd212waFRny7EQ4L4/FqalQRFkzC5C+ocOv3L+FOSL97dku7TetZ7Ur3g1Ko2ZnvuAbPyEvmAme6vmBQ1QJq0v42aWHzE4u+9Z/RG69ZtoEs6h9IbrccQe+yK9kgl+yIyE7jPpk2wjL6/SVacpfnZKef+FdwlZMZZgDbBalpLcncNr++XOPVuXbbzHuqjLmpdmuad6MtiPLWK2fSvWSHSbuSedlgifhWqgA/FebVe9NopPn5Dd9xRPFBrlB5KL5q+SbnDSP0gQHe7r5+o2VHNaYUIaRHkWygeRyC72aU5sT6mRuUR0W/4eBw0ZQvzfDpz1/TuZ7T4zfLpSJfOz6GbMkrTitSSv4ws1kn55L1h4RyMbcUaB4p/VVU+4vmOKYfAsggbKwBEsP3AFACgAWFGP2PK+h3TvwUNyejBfjv7cwcKzsR8GvSD4p+R/kbx98oM+rb+4+ft32N+vt6/xoixAP/s+4+Y/z7L9yfiSPPBMeA/JmNl+7YLAkBAOwAA5L/b/y8AAP//A6tAlY0KAAA="); err != nil { panic("add binary content to resource manager failed: " + err.Error()) } } ``` ## `Load` - 说明:`Load`加载、解压并将路径为`path`的文件数据读取到默认资源对象中。`prefix`是非必要参数,表示存储到当前资源对象中的每个文件的前缀。 - 格式: ```go func Load(path string, prefix ...string) error ``` - 示例: ```go package main import "github.com/gogf/gf/v2/os/gres" func main() { if err := gres.Load("../res/myfile"); err != nil { panic("load binary content to resource manager failed: " + err.Error()) } } ``` ## `Get` - 说明:`Get` 返回指定路径的文件。 - 格式: ```go func Get(path string) *File ``` - 示例: ```go package main import ( "fmt" "github.com/gogf/gf/v2/os/gctx" "github.com/gogf/gf/v2/os/glog" "github.com/gogf/gf/v2/os/gres" ) func main() { file := gres.Get("../res/myfile") if file == nil { glog.Error(gctx.New(), "get file failed!") return } fmt.Println("Get File Name:", file.Name()) } ``` ## `GetWithIndex` - 说明: `GetWithIndex` 用给定路径path搜索文件,如果文件是目录,那么它会在这个目录下进行索引文件搜索。 `GetWithIndex` 通常用于http静态文件服务。 - 格式: ```go func GetWithIndex(path string, indexFiles []string) *File ``` - 示例: ```go package main import ( "fmt" "github.com/gogf/gf/v2/os/gctx" "github.com/gogf/gf/v2/os/glog" "github.com/gogf/gf/v2/os/gres" ) func main() { file := gres.GetWithIndex("../res", []string{"myfile", "myconfig"}) if file == nil { glog.Error(gctx.New(), "get file failed!") return } fmt.Println("Get File Name:", file.Name()) } ``` ## `GetContent` - 说明:`GetContent` 在默认资源对象中直接返回路径为 `path` 的内容。 - 格式: ```go func GetContent(path string) []byte ``` - 示例: ```go package main import ( "fmt" "github.com/gogf/gf/v2/os/gres" ) func main() { fileContent := gres.GetContent("../res/myfile") fmt.Println("Get File Content:", fileContent) } ``` ## `Contains` - 说明: `Contains` 检查路径为 `path` 的资源是否存在于默认资源对象中。 - 格式: ```go func Contains(path string) bool ``` - 示例: ```go package main import ( "fmt" "github.com/gogf/gf/v2/os/gres" ) func main() { if gres.Contains("../res/myfile") { fmt.Println("myfile is exist!") } else{ fmt.Println("myfile is not exist!") } } ``` ## `IsEmpty` - 说明: `IsEmpty` 检查并返回资源管理器是否为空。 - 格式: ```go func IsEmpty() bool ``` - 示例: ```go package main import ( "fmt" "github.com/gogf/gf/v2/os/gres" ) func main() { fmt.Println(gres.IsEmpty()) gres.Add("xxxxxxxxxxxxxxxxx") fmt.Println(gres.IsEmpty()) // Output: // true // false } ``` ## `ScanDir` - 说明:`ScanDir` 返回给定路径下的文件,参数 `path` 应该是文件夹类型。参数 `pattern` 支持多个文件名模式,使用 `,` 符号分隔多个模式。如果参数 `recursive` 为 `true`,它会递归地扫描目录。 - 格式: ```go func ScanDir(path string, pattern string, recursive ...bool) []*File ``` - 示例: ```go package main import ( "fmt" "github.com/gogf/gf/v2/os/gres" ) func main() { files := gres.ScanDir("../res", "*.doc,*.go", true) if len(files) > 0 { for _, file := range files { fmt.Println("ScanDir Result:", file.Name()) } } } ``` ## `ScanDirFile` - 说明:`ScanDirFile` 返回所有具有给定 `path` 的绝对路径的子文件,如果参数 `recursive` 为 `true`,则会递归扫描目录。 - 注意:只返回文件,不返回目录。 - 格式: ```go func ScanDirFile(path string, pattern string, recursive ...bool) []*File ``` - 示例: ```go package main import ( "fmt" "github.com/gogf/gf/v2/os/gres" ) func main() { files := gres.ScanDirFile("../res", "*.*", true) if len(files) > 0 { for _, file := range files { fmt.Println("ScanDirFile Result:", file.Name()) } } } ``` ## `Export` - 说明:`Export` 将指定路径 `src` 及其所有子文件递归保存到指定的系统路径 `dst`。 - 格式: ```go func Export(src, dst string, option ...ExportOption) error ``` - 示例: ```go package main import ( "fmt" "github.com/gogf/gf/v2/os/gres" ) func main() { err := gres.Export("../res/src", "../res/dst") if err != nil { fmt.Println("gres.Export Error:", err) } } ``` ## `Dump` - 说明:`Dump` 打印默认资源对象的文件。 - 格式: ```go func Dump() ``` - 示例: ```go package main import ( "github.com/gogf/gf/v2/os/gres" ) func main() { gres.Add("xxxxxxxxx") gres.Dump() } ``` # 资源管理-使用示例 我们来看一个使用示例,在该示例中,展示了资源管理在`WebServer`的静态服务、配置管理、模板引擎中的使用。 ### 资源文件 资源文件源码: [https://github.com/gogf/gf/tree/master/os/gres/testdata/example/files](https://github.com/gogf/gf/tree/master/os/gres/testdata/example/files) 资源文件打包: [https://github.com/gogf/gf/tree/master/os/gres/testdata/example/boot](https://github.com/gogf/gf/tree/master/os/gres/testdata/example/boot) 资源文件列表: ``` 2020-03-28T13:04:10+00:00 0.00B config 2020-03-28T13:03:06+00:00 135.00B config/config.toml 2020-03-28T13:04:10+00:00 0.00B public 2020-03-28T12:57:54+00:00 6.00B public/index.html 2020-03-28T13:04:10+00:00 0.00B template 2020-03-28T13:03:17+00:00 15.00B template/index.tpl TOTAL FILES: 6 ``` 三个文件的内容分别为: 1. `config.toml` ```toml [server] Address = ":8888" ServerRoot = "public" [viewer] DefaultFile = "index.tpl" Delimiters = ["${", "}"] ``` 该文件为应用的配置文件。 2. `index.html` ```html Hello! ``` 该文件用于静态资源请求。 3. `index.tpl` ```html Hello ${.name}! ``` 该文件用于模板文件解析展示。 ### 创建应用 ```go package main import ( _ "github.com/gogf/gf/v2/os/gres/testdata/example/boot" "github.com/gogf/gf/v2/frame/g" "github.com/gogf/gf/v2/net/ghttp" ) func main() { s := g.Server() s.Group("/", func(group *ghttp.RouterGroup) { group.GET("/template", func(r *ghttp.Request) { r.Response.WriteTplDefault(g.Map{ "name": "GoFrame", }) }) }) s.Run() } ``` 可以看到,整个代码中除了`import`中额外增加了一个`_ "github.com/gogf/gf/v2/os/gres/testdata/example/boot"`的包引入外,没有其他任何设置。这也是`GoFrame`框架的资源管理比较便捷的地方,资源管理并不需要开发阶段对代码做任何特殊设置,在应用程序部署之前打包好资源文件,并通过`import`增加资源文件的引入即可。 运行后,终端输出: ```html 2020-03-28 21:36:19.828 75892: http server started listening on [:8888] SERVER | DOMAIN | ADDRESS | METHOD | ROUTE | HANDLER | MIDDLEWARE |---------|---------|---------|--------|-----------|-------------------|------------| default | default | :8888 | GET | /template | main.main.func1.1 | |---------|---------|---------|--------|-----------|-------------------|------------| ``` 可以看到,配置文件被自动读取并应用到了`WebServer`上。 我们通过`curl`命令测试一下静态文件以及模板引擎的访问。 ```bash $ curl http://127.0.0.1:8888/ Hello! $ curl http://127.0.0.1:8888/template Hello GoFrame! ``` 可以看到,`index.html`静态文件以及`index.tpl`模板文件都被成功访问到了。 # 资源管理-最佳实践 资源管理设计的目标之一是在开发阶段不影响静态文件的开发管理,只有在发布的时候执行打包即可,打包完毕后清理临时文件,因此只会影响生成的二进制可执行文件,对开发者来说无感知且使用便捷。 ## 准备工程 建议大家使用 `GoFrame` 官方提供的工程化目录结构,并且使用 `CLI` 工具来创建自己的项目工程,因为整个框架工程化思想以及一些示例都是基于标准化的工程目录结构,这样更有利于大家学习和使用整个开发框架,提高开发效率。特别是,在工程目录中存在一个 `packed` 目录,用以存放资源管理组件的相关打包内容,在默认情况下,里面是一个空的 `go` 文件,什么都不会做。 ## 开发阶段 在开发阶段,一般来说,开发者并不需要关心资源管理的事情,该怎么撸码就怎么撸,静态的文件按照建议放到 `resource` 目录下。在开发阶段的静态文件访问都会直接通过文件系统访问。 ## 准备发布 当开发完毕后,想要将静态文件、模板文件、配置文件打包到二进制可执行文件中,随着二进制可执行文件发布。好的,这个时候资源组件开始介入展示能力了。需要配置到交叉编译的配置,具体请参考命令行相关章节 [交叉编译-build](../cli/build.md)。我们需要使用到CLI工具来执行可执行文件编译,将的编译配置通过配置文件管理起来(放到hack/config.yaml文件中),一个参考的编译配置如下: ``` gfcli: build: name: "my-app" arch: "amd64" system: "linux" mod: "none" cgo: 0 packSrc: "manifest/config,manifest/i18n,resource/public,resource/template" version: "" output: "./bin" extra: "" ``` 请注意其中的 `pack` 配置,表示我们在执行编译的时候自动打包到二进制可执行文件中的目录。随后,我们在项目根目录下执行编译命令: ``` gf build ``` 该命令在编译时会自动将配置文件中指定的目录进行打包成临时的打包 `go` 文件,再执行编译,编译完毕后自动清理临时的打包 `go` 文件。 **提示** 大部分场景下,配置文件可能不需要打包进二进制可执行文件,具体根据业务场景自行选择。 ## 发布运行 将二进制执行发布执行即可。 # 开始使用 **提示** `GoFrame` 是一款基础设施建设比较完善的模块化框架, `WebServer` 模块是其中比较核心的模块,我们这里将 `Web` 服务开发作为框架入门的选择,便于大家更容易学习和理解。 `GoFrame` 框架提供了非常强大的 `WebServer`,由 `ghttp` 模块实现。实现了丰富完善的相关组件,例如: `Router`、 `Cookie`、 `Session`、路由注册、配置管理、模板引擎、缓存控制等等,支持热重启、热更新、多域名、多端口、多实例、 `HTTPS`、 `Rewrite`、 `PProf` 等等特性。 接口文档地址: [https://pkg.go.dev/github.com/gogf/gf/v2/net/ghttp](https://pkg.go.dev/github.com/gogf/gf/v2/net/ghttp) ## 哈喽世界 老规矩,我们先来一个 `Hello World`: ```go package main import ( "github.com/gogf/gf/v2/frame/g" "github.com/gogf/gf/v2/net/ghttp" ) func main() { s := g.Server() s.BindHandler("/", func(r *ghttp.Request) { r.Response.Write("哈喽世界!") }) s.Run() } ``` 这便是一个最简单的服务,默认情况下它不支持静态文件处理,只有一个功能,访问 [http://127.0.0.1/](http://127.0.0.1/) 的时候,它会返回 `哈喽世界!`。 任何时候,都可以通过 `g.Server()` 方法获得一个默认的 `Server` 对象,该方法采用 `单例模式` 设计,也就是说,多次调用该方法,返回的是同一个 `Server` 对象。通过 `Run()` 方法执行 `Server` 的监听运行,在没有任何额外设置的情况下,它默认监听 `80` 端口。 关于其中的路由注册,我们将会在后续的章节中介绍,我们继续来看看如何创建一个支持静态文件的 `Server`。 ## 静态服务 创建并运行一个支持静态文件的 `WebServer`: ```go package main import ( "github.com/gogf/gf/v2/frame/g" ) func main() { s := g.Server() s.SetIndexFolder(true) s.SetServerRoot("/home/www/") s.Run() } ``` 创建了 `Server` 对象之后,我们可以使用 `Set*` 方法来设置 `Server` 的属性,我们这里的示例中涉及到了两个属性设置方法: 1. `SetIndexFolder` 用来设置是否允许列出 `Server` 主目录的文件列表(默认为 `false`)。 2. `SetServerRoot` 用来设置 `Server` 的主目录(类似 `nginx` 中的 `root` 配置,默认为空)。 `Server` 默认情况下是没有任何主目录的设置,只有设置了主目录,才支持对应主目录下的静态文件的访问。 ## 多端口监听 `Server` 同时支持多端口监听,只需要往 `SetPort` 参数设置多个端口号即可(当然,针对于 `HTTPS` 服务,我们也同样可以通过 `SetHTTPSPort` 来设置绑定并支持多个端口号的监听, `HTTPS` 服务的介绍请参看后续对应章节)。 我们来看一个例子: ```go package main import ( "github.com/gogf/gf/v2/frame/g" "github.com/gogf/gf/v2/net/ghttp" ) func main() { s := g.Server() s.BindHandler("/", func(r *ghttp.Request){ r.Response.Writeln("go frame!") }) s.SetPort(8100, 8200, 8300) s.Run() } ``` 执行以上示例后,我们访问以下URL将会得到期望的相同结果: ``` http://127.0.0.1:8100/ http://127.0.0.1:8200/ http://127.0.0.1:8300/ ``` ## 多实例支持 `Server` 支持同一进程多实例运行,下面我们来看一个例子: ```go package main import ( "github.com/gogf/gf/v2/frame/g" ) func main() { s1 := g.Server("s1") s1.SetPort(8080) s1.SetIndexFolder(true) s1.SetServerRoot("/home/www/static1") s1.Start() s2 := g.Server("s2") s2.SetPort(8088) s2.SetIndexFolder(true) s2.SetServerRoot("/home/www/static2") s2.Start() g.Wait() } ``` 可以看到我们在支持多个 `Server` 的语句中,给 `g.Server` 方法传递了不同的单例名称参数,该参数用于标识不同的 `Server` 实例,因此需要保证唯一性。如果需要获取同一个 `Server` 实例,那么传入同一个名称即可。例如在多个 `goroutine` 中,或者不同的模块中,都可以通过 `g.Server` 获取到同一个 `Server` 实例。 ## 域名绑定 `Server` 支持多域名绑定,并且不同的域名可以绑定不同的服务。 我们来看一个简单的例子: ```go package main import ( "github.com/gogf/gf/v2/frame/g" "github.com/gogf/gf/v2/net/ghttp" ) func Hello1(r *ghttp.Request) { r.Response.Write("127.0.0.1: Hello1!") } func Hello2(r *ghttp.Request) { r.Response.Write("localhost: Hello2!") } func main() { s := g.Server() s.Domain("127.0.0.1").BindHandler("/", Hello1) s.Domain("localhost").BindHandler("/", Hello2) s.Run() } ``` 我们访问 [http://127.0.0.1/](http://127.0.0.1/) 和 [http://localhost/](http://localhost/) 可以看输出不同的内容。 此外, `Domain` 方法支持多个域名参数,使用英文「,」号分隔,例如: ``` s.Domain("localhost1,localhost2,localhost3").BindHandler("/", Hello2) ``` 这条语句的表示将 `Hello2` 方法注册到指定的3个域名中( `localhost1~3`),对其他域名不可见。 需要注意的是: `Domain` 方法的参数必须是 **准确的** 域名, **不支持泛域名形式**,例如: `*.goframe.org` 或者 `.goframe.org` 是不支持的, `api.goframe.org` 或者 `goframe.org` 才被认为是正确的域名参数。 ## 路由特性 `Server` 提供了非常出色的路由特性,我们先来看一个简单的示例: ```go package main import ( "github.com/gogf/gf/v2/net/ghttp" "github.com/gogf/gf/v2/frame/g" ) func main() { s := g.Server() s.BindHandler("/{class}-{course}/:name/*act", func(r *ghttp.Request) { r.Response.Writef( "%v %v %v %v", r.Get("class"), r.Get("course"), r.Get("name"), r.Get("act"), ) }) s.SetPort(8199) s.Run() } ``` 这是一个混合的路由规则示例,用于展示某个班级、某个学科、某个学生、对应的操作,运行后,我们可以通过例如该地址: [http://127.0.0.1:8199/class3-math/john/score](http://127.0.0.1:8199/class3-math/john/score) 看到测试结果。在页面上可以看得到对应的路由规则都被一一解析,业务层可以根据解析的参数进行对应的业务逻辑处理。 具体的路由注册管理介绍请查看后续 [路由管理-路由规则](./router-pattern.md) 章节。 ## 配置管理 `GoFrame` 的核心组件均实现了便捷的配置管理特性,仅需通过配置文件的修改即可实现组件的功能配置。大多数的场景中我们推荐使用配置文件来管理组件的配置, `Server` 的配置请查看 [服务配置](./server-config.md) 章节。 ## 平滑重启 `Server` 内置支持平滑重启特性,详细介绍请参考 [平滑重启特性](./senior-hot-reload.md) 章节。 ## HTTPS支持 `Server` 支持 `HTTPS` 服务,并且也同时支持单进程提供 `HTTP&HTTPS` 服务,HTTPS的详细介绍请参考 [HTTPS&TLS](./senior-https.md) 章节。 ## 更多功能特性 更多功能及特性请继续阅读后续章节。 # Cookie ## 基本介绍 接口文档: [https://pkg.go.dev/github.com/gogf/gf/v2/net/ghttp#Cookie](https://pkg.go.dev/github.com/gogf/gf/v2/net/ghttp#Cookie) 常用方法: ```go type Cookie func GetCookie(r *Request) *Cookie func (c *Cookie) Contains(key string) bool func (c *Cookie) Flush() func (c *Cookie) Get(key string, def ...string) string func (c *Cookie) GetSessionId() string func (c *Cookie) Map() map[string]string func (c *Cookie) Remove(key string) func (c *Cookie) RemoveCookie(key, domain, path string) func (c *Cookie) Set(key, value string) func (c *Cookie) SetCookie(key, value, domain, path string, maxAge time.Duration, httpOnly ...bool) func (c *Cookie) SetHttpCookie(httpCookie *http.Cookie) func (c *Cookie) SetSessionId(id string) ``` 任何时候都可以通过 `*ghttp.Request` 对象获取到当前请求对应的 `Cookie` 对象,因为 `Cookie` 和 `Session` 都是和请求会话相关,因此都属于 `ghttp.Request` 的成员对象,并对外公开。 `Cookie` 对象不需要手动 `Close`,请求流程结束后, `HTTP Server` 会自动关闭掉。 此外, `Cookie` 中封装了两个 `SessionId` 相关的方法: 1. `Cookie.GetSessionId()` 用于获取当前请求提交的 `SessionId`,每个请求的 `SessionId` 都是唯一的,并且伴随整个请求流程,该值可能为空。 2. `Cookie.SetSessionId(id string)` 用于自定义设置 `SessionId` 到 `Cookie` 中,返回给客户端(往往是浏览器)存储,随后客户端每一次请求在 `Cookie` 中可带上该 `SessionId`。 在设置 `Cookie` 变量的时候可以给定过期时间,该时间为可选参数,默认的 `Cookie` 过期时间为一年。 **提示** 默认的 `SessionId` 在 `Cookie` 中的存储名称为 `gfsession`。 ## 使用示例 ```go package main import ( "github.com/gogf/gf/v2/frame/g" "github.com/gogf/gf/v2/os/gtime" "github.com/gogf/gf/v2/net/ghttp" ) func main() { s := g.Server() s.BindHandler("/cookie", func(r *ghttp.Request) { datetime := r.Cookie.Get("datetime") r.Cookie.Set("datetime", gtime.Datetime()) r.Response.Write("datetime:", datetime) }) s.SetPort(8199) s.Run() } ``` 执行外层的 `main.go`,可以尝试刷新页面 [http://127.0.0.1:8199/cookie](http://127.0.0.1:8199/cookie),显示的时间在一直变化。 对于控制器对象而言,从基类控制器中继承了很多会话相关的对象指针,可以看做alias,可以直接使用,他们都是指向的同一个对象: ```go type Controller struct { Request *ghttp.Request // 请求数据对象 Response *ghttp.Response // 返回数据对象(r.Response) Server *ghttp.Server // WebServer对象(r.Server) Cookie *ghttp.Cookie // COOKIE操作对象(r.Cookie) Session *ghttp.Session // SESSION操作对象 View *View // 视图对象 } ``` 由于对于Web开发者来讲, `Cookie` 都已经是非常熟悉的组件了,相关 `API` 也非常简单,这里便不再赘述。 ## `Cookie` 会话过期 `Cookie` 的有效期默认是1年,如果我们期望Cookie随着用户的浏览会话过期,像这样: 那么我们仅需要通过 `SetCookie` 来设置 `Cookie` 键值对并将 `maxAge` 参数设置为 `0` 即可。像这样: ``` r.Cookie.SetCookie("MyCookieKey", "MyCookieValue", "", "/", 0) ``` # 异常处理 由于这个问题问得比较多,因此专门为大家撰写了这一章节。 ## 基本介绍 目前大多数 `Golang` 的第三方 `WebServer` 库均没有默认对 `HTTP` 请求处理过程中产生的异常进行捕获,轻者错误产生后无法记录到日志造成排查错困难,重则异常造成进程直接崩溃,服务不可用。 当选择 `goframe`,很幸运。作为一款企业级的基础开发框架,默认情况下,执行过程中产生的 `panic` 是有被 `Server` 自动捕获的,产生 `panic` 时当前执行流程会立即中止,但是绝对不会影响进程直接崩溃。 ## 获取异常错误 `HTTP` 执行流程中产生 `panic` 异常时,默认处理是记录到 `Server` 的日志文件中。当然,开发者也可以通过注册中间件方式手动捕获,然后自定义相关的错误处理。这一操作其实在中间件章节的示例中也有介绍,我们这里来再仔细说明下。 ### 相关方法 异常的捕获我们通过 `Request` 对象中的 `GetError` 方法获取。 **提示** 开发者不能通过 `recover` 方法来捕获异常,因为 `goframe` 框架的 `Server` 已经做了捕获,并且为保证默认情况下异常不会引起进程崩溃,因此不会再次往上抛异常。 ```go // GetError returns the error occurs in the procedure of the request. // It returns nil if there's no error. func (r *Request) GetError() error ``` 该方法往往使用在流程控制组件中,如后置中间件或者 `HOOK` 钩子方法中。 ### 使用示例 我们这里使用一个全局的后置中间件来捕获异常,当异常产生后,捕获并写入到指定的日志文件中,返回固定友好的提示信息,避免敏感的报错信息暴露给调用端。 **提示** 需要注意的是: - 即使开发者有自己捕获记录异常错误的日志,但是 `Server` 依旧会打印到 `Server` 自己的错误日志文件中。由开发者调用日志接口方法输出的日志属于业务日志(与业务相关),而 `Server` 自行管理的日志属于服务日志(类似于 `nginx` 的 `error.log`)。 - 由于 `goframe` 框架大部分的底层错误都包含有错误时的堆栈信息,如果对于 `error` 的具体堆栈信息感兴趣(具体调用链、报错文件路径、源码行号等),可以使用 `gerror` 来获取,具体请参考 [错误处理-堆栈特性](../core/gerror-stack.md) 章节。如果异常包含堆栈信息,默认情况下会打印到 `Server` 的 `error` 日志文件中。 ```go package main import ( "github.com/gogf/gf/v2/frame/g" "github.com/gogf/gf/v2/net/ghttp" ) func MiddlewareErrorHandler(r *ghttp.Request) { r.Middleware.Next() if err := r.GetError(); err != nil { // 记录到自定义错误日志文件 g.Log("exception").Error(err) //返回固定的友好信息 r.Response.ClearBuffer() r.Response.Writeln("服务器居然开小差了,请稍后再试吧!") } } func main() { s := g.Server() s.Use(MiddlewareErrorHandler) s.Group("/api.v2", func(group *ghttp.RouterGroup) { group.ALL("/user/list", func(r *ghttp.Request) { panic("db error: sql is xxxxxxx") }) }) s.SetPort(8199) s.Run() } ``` 执行后,我们通过 `curl` 工具来试试吧: ```bash $ curl -v "http://127.0.0.1:8199/api.v2/user/list" > GET /api.v2/user/list HTTP/1.1 > Host: 127.0.0.1:8199 > User-Agent: curl/7.61.1 > Accept: */* > < HTTP/1.1 500 Internal Server Error < Server: GoFrame HTTP Server < Date: Sun, 19 Jul 2020 07:44:30 GMT < Content-Length: 52 < Content-Type: text/plain; charset=utf-8 < 服务器居然开小差了,请稍后再试吧! ``` ## 获取异常堆栈 ### 异常堆栈信息 `WebServer` 本身在捕获异常时,如果抛出的异常信息并不包含堆栈内容,那么 `WebServer` 会自动获取抛出异常位点(即 `panic` 的位置)的堆栈并创建一个新的包含该堆栈信息的错误对象。我们来看一个示例。 ```go package main import ( "github.com/gogf/gf/v2/frame/g" "github.com/gogf/gf/v2/net/ghttp" ) func MiddlewareErrorHandler(r *ghttp.Request) { r.Middleware.Next() if err := r.GetError(); err != nil { r.Response.ClearBuffer() r.Response.Writef("%+v", err) } } func main() { s := g.Server() s.Use(MiddlewareErrorHandler) s.Group("/api.v2", func(group *ghttp.RouterGroup) { group.ALL("/user/list", func(r *ghttp.Request) { panic("db error: sql is xxxxxxx") }) }) s.SetPort(8199) s.Run() } ``` 可以看到,我们通过 `%+v` 的格式化打印来获取异常错误中的堆栈信息,具体原理请参考章节: [错误处理-堆栈特性](../core/gerror-stack.md)。执行后,我们通过 `curl` 工具来测试下: ```bash $ curl "http://127.0.0.1:8199/api.v2/user/list" db error: sql is xxxxxxx 1. db error: sql is xxxxxxx 1). main.main.func1.1 /Users/john/Workspace/Go/GOPATH/src/github.com/gogf/gf/.example/other/test.go:25 2). github.com/gogf/gf/v2/net/ghttp.(*middleware).Next.func1.8 /Users/john/Workspace/Go/GOPATH/src/github.com/gogf/gf/v2/net/ghttp/ghttp_request_middleware.go:111 3). github.com/gogf/gf/v2/net/ghttp.niceCallFunc /Users/john/Workspace/Go/GOPATH/src/github.com/gogf/gf/v2/net/ghttp/ghttp_func.go:46 4). github.com/gogf/gf/v2/net/ghttp.(*middleware).Next.func1 /Users/john/Workspace/Go/GOPATH/src/github.com/gogf/gf/v2/net/ghttp/ghttp_request_middleware.go:110 5). github.com/gogf/gf/v2/util/gutil.TryCatch /Users/john/Workspace/Go/GOPATH/src/github.com/gogf/gf/v2/util/gutil/gutil.go:46 6). github.com/gogf/gf/v2/net/ghttp.(*middleware).Next /Users/john/Workspace/Go/GOPATH/src/github.com/gogf/gf/v2/net/ghttp/ghttp_request_middleware.go:47 7). main.MiddlewareErrorHandler /Users/john/Workspace/Go/GOPATH/src/github.com/gogf/gf/.example/other/test.go:10 8). github.com/gogf/gf/v2/net/ghttp.(*middleware).Next.func1.9 /Users/john/Workspace/Go/GOPATH/src/github.com/gogf/gf/v2/net/ghttp/ghttp_request_middleware.go:117 9). github.com/gogf/gf/v2/net/ghttp.niceCallFunc /Users/john/Workspace/Go/GOPATH/src/github.com/gogf/gf/v2/net/ghttp/ghttp_func.go:46 10). github.com/gogf/gf/v2/net/ghttp.(*middleware).Next.func1 /Users/john/Workspace/Go/GOPATH/src/github.com/gogf/gf/v2/net/ghttp/ghttp_request_middleware.go:116 11). github.com/gogf/gf/v2/util/gutil.TryCatch /Users/john/Workspace/Go/GOPATH/src/github.com/gogf/gf/v2/util/gutil/gutil.go:46 12). github.com/gogf/gf/v2/net/ghttp.(*middleware).Next /Users/john/Workspace/Go/GOPATH/src/github.com/gogf/gf/v2/net/ghttp/ghttp_request_middleware.go:47 13). github.com/gogf/gf/v2/net/ghttp.(*Server).ServeHTTP /Users/john/Workspace/Go/GOPATH/src/github.com/gogf/gf/v2/net/ghttp/ghttp_server_handler.go:122 ``` ### 错误堆栈信息 如果抛出的异常是一个通过 `gerror` 组件的错误对象,或者实现堆栈打印接口的错误对象,由于该异常的错误对象已经包含了详细的堆栈信息,那么 `WebServer` 将会直接返回该错误对象,不会自动创建错误对象。我们来看一个示例。 ```go package main import ( "github.com/gogf/gf/v2/errors/gerror" "github.com/gogf/gf/v2/frame/g" "github.com/gogf/gf/v2/net/ghttp" ) func MiddlewareErrorHandler(r *ghttp.Request) { r.Middleware.Next() if err := r.GetError(); err != nil { r.Response.ClearBuffer() r.Response.Writef("%+v", err) } } func DbOperation() error { // ... return gerror.New("DbOperation error: sql is xxxxxxx") } func UpdateData() { err := DbOperation() if err != nil { panic(gerror.Wrap(err, "UpdateData error")) } } func main() { s := g.Server() s.Use(MiddlewareErrorHandler) s.Group("/api.v2", func(group *ghttp.RouterGroup) { group.ALL("/user/list", func(r *ghttp.Request) { UpdateData() }) }) s.SetPort(8199) s.Run() } ``` 执行后,我们通过 `curl` 工具来测试下: ```bash $ curl "http://127.0.0.1:8199/api.v2/user/list" UpdateData error: DbOperation error: sql is xxxxxxx 1. UpdateData error 1). main.UpdateData /Users/john/Workspace/Go/GOPATH/src/github.com/gogf/gf/.example/other/test.go:25 2). main.main.func1.1 /Users/john/Workspace/Go/GOPATH/src/github.com/gogf/gf/.example/other/test.go:34 3). github.com/gogf/gf/v2/net/ghttp.(*middleware).Next.func1.8 /Users/john/Workspace/Go/GOPATH/src/github.com/gogf/gf/v2/net/ghttp/ghttp_request_middleware.go:111 4). github.com/gogf/gf/v2/net/ghttp.niceCallFunc /Users/john/Workspace/Go/GOPATH/src/github.com/gogf/gf/v2/net/ghttp/ghttp_func.go:46 5). github.com/gogf/gf/v2/net/ghttp.(*middleware).Next.func1 /Users/john/Workspace/Go/GOPATH/src/github.com/gogf/gf/v2/net/ghttp/ghttp_request_middleware.go:110 6). github.com/gogf/gf/v2/util/gutil.TryCatch /Users/john/Workspace/Go/GOPATH/src/github.com/gogf/gf/v2/util/gutil/gutil.go:46 7). github.com/gogf/gf/v2/net/ghttp.(*middleware).Next /Users/john/Workspace/Go/GOPATH/src/github.com/gogf/gf/v2/net/ghttp/ghttp_request_middleware.go:47 8). main.MiddlewareErrorHandler /Users/john/Workspace/Go/GOPATH/src/github.com/gogf/gf/.example/other/test.go:10 9). github.com/gogf/gf/v2/net/ghttp.(*middleware).Next.func1.9 /Users/john/Workspace/Go/GOPATH/src/github.com/gogf/gf/v2/net/ghttp/ghttp_request_middleware.go:117 10). github.com/gogf/gf/v2/net/ghttp.niceCallFunc /Users/john/Workspace/Go/GOPATH/src/github.com/gogf/gf/v2/net/ghttp/ghttp_func.go:46 11). github.com/gogf/gf/v2/net/ghttp.(*middleware).Next.func1 /Users/john/Workspace/Go/GOPATH/src/github.com/gogf/gf/v2/net/ghttp/ghttp_request_middleware.go:116 12). github.com/gogf/gf/v2/util/gutil.TryCatch /Users/john/Workspace/Go/GOPATH/src/github.com/gogf/gf/v2/util/gutil/gutil.go:46 13). github.com/gogf/gf/v2/net/ghttp.(*middleware).Next /Users/john/Workspace/Go/GOPATH/src/github.com/gogf/gf/v2/net/ghttp/ghttp_request_middleware.go:47 14). github.com/gogf/gf/v2/net/ghttp.(*Server).ServeHTTP /Users/john/Workspace/Go/GOPATH/src/github.com/gogf/gf/v2/net/ghttp/ghttp_server_handler.go:122 2. DbOperation error: sql is xxxxxxx 1). main.DbOperation /Users/john/Workspace/Go/GOPATH/src/github.com/gogf/gf/.example/other/test.go:19 2). main.UpdateData /Users/john/Workspace/Go/GOPATH/src/github.com/gogf/gf/.example/other/test.go:23 3). main.main.func1.1 /Users/john/Workspace/Go/GOPATH/src/github.com/gogf/gf/.example/other/test.go:34 4). github.com/gogf/gf/v2/net/ghttp.(*middleware).Next.func1.8 /Users/john/Workspace/Go/GOPATH/src/github.com/gogf/gf/v2/net/ghttp/ghttp_request_middleware.go:111 5). github.com/gogf/gf/v2/net/ghttp.niceCallFunc /Users/john/Workspace/Go/GOPATH/src/github.com/gogf/gf/v2/net/ghttp/ghttp_func.go:46 6). github.com/gogf/gf/v2/net/ghttp.(*middleware).Next.func1 /Users/john/Workspace/Go/GOPATH/src/github.com/gogf/gf/v2/net/ghttp/ghttp_request_middleware.go:110 7). github.com/gogf/gf/v2/util/gutil.TryCatch /Users/john/Workspace/Go/GOPATH/src/github.com/gogf/gf/v2/util/gutil/gutil.go:46 8). github.com/gogf/gf/v2/net/ghttp.(*middleware).Next /Users/john/Workspace/Go/GOPATH/src/github.com/gogf/gf/v2/net/ghttp/ghttp_request_middleware.go:47 9). main.MiddlewareErrorHandler /Users/john/Workspace/Go/GOPATH/src/github.com/gogf/gf/.example/other/test.go:10 10). github.com/gogf/gf/v2/net/ghttp.(*middleware).Next.func1.9 /Users/john/Workspace/Go/GOPATH/src/github.com/gogf/gf/v2/net/ghttp/ghttp_request_middleware.go:117 11). github.com/gogf/gf/v2/net/ghttp.niceCallFunc /Users/john/Workspace/Go/GOPATH/src/github.com/gogf/gf/v2/net/ghttp/ghttp_func.go:46 12). github.com/gogf/gf/v2/net/ghttp.(*middleware).Next.func1 /Users/john/Workspace/Go/GOPATH/src/github.com/gogf/gf/v2/net/ghttp/ghttp_request_middleware.go:116 13). github.com/gogf/gf/v2/util/gutil.TryCatch /Users/john/Workspace/Go/GOPATH/src/github.com/gogf/gf/v2/util/gutil/gutil.go:46 14). github.com/gogf/gf/v2/net/ghttp.(*middleware).Next /Users/john/Workspace/Go/GOPATH/src/github.com/gogf/gf/v2/net/ghttp/ghttp_request_middleware.go:47 15). github.com/gogf/gf/v2/net/ghttp.(*Server).ServeHTTP /Users/john/Workspace/Go/GOPATH/src/github.com/gogf/gf/v2/net/ghttp/ghttp_server_handler.go:122 ``` # 性能测试 ## 关于性能测试 需要注意,GoFrame框架是一个模块化、低耦合的基础开发框架,HTTP Server只是其中一个核心组件。 这里只是展示HTTP Server的性能测试报告,以中立的第三方性能测试报告为准。 ## 全框架/组件性能测试报告 [https://www.techempower.com/benchmarks/#section=data-r21&test=composite](https://www.techempower.com/benchmarks/#section=data-r21&test=composite) ## Go框架/组件性能测试报告 # 常见问题 ## 服务端频繁出现 `context cancel` 错误 当 **客户端主动取消请求**(例如主动取消,或者请求超过客户端设置的时间)后,特别是浏览器访问时直接取消请求,服务端可能会遇到 `context canceled` 的错误。这个属于正常现象, `Golang` 标准库的 `http server` 设计亦如此。当客户端不再需要这个请求的结果时会取消请求,这时服务端继续往下执行也没有了意义。如果介意这个错误,可以使用一个自定义的中间件增加 `NeverDoneCtx` 的处理逻辑,或者直接使用框架提供的`ghttp.MiddlewareNeverDoneCtx`中间件。这个时候服务端会忽略客户端的取消请求并继续往下执行。 ```go // MiddlewareNeverDoneCtx sets the context never done for current process. func MiddlewareNeverDoneCtx(r *Request) { r.SetCtx(r.GetNeverDoneCtx()) r.Middleware.Next() } ``` # 路由管理 路由管理是 `HTTP Web Server` 开发的基础,我们尽可能地通过详尽的示例让能更好理解本章节。 # 路由管理-路由规则 `GoFrame` 框架自建了非常强大的路由功能,提供了比任何同类框架更加出色的路由特性,支持流行的命名匹配规则、模糊匹配规则及字段匹配规则,并提供了优秀的优先级管理机制。 ## 一个示例 在真正开启本章的核心内容之前,我们先来看一个简单的动态路由使用示例: ```go package main import ( "github.com/gogf/gf/v2/net/ghttp" "github.com/gogf/gf/v2/frame/g" ) func main() { s := g.Server() s.BindHandler("/:name", func(r *ghttp.Request){ r.Response.Writeln(r.Router.Uri) }) s.BindHandler("/:name/update", func(r *ghttp.Request){ r.Response.Writeln(r.Router.Uri) }) s.BindHandler("/:name/:action", func(r *ghttp.Request){ r.Response.Writeln(r.Router.Uri) }) s.BindHandler("/:name/*any", func(r *ghttp.Request){ r.Response.Writeln(r.Router.Uri) }) s.BindHandler("/user/list/{field}.html", func(r *ghttp.Request){ r.Response.Writeln(r.Router.Uri) }) s.SetPort(8199) s.Run() } ``` 以上示例中展示了 `goframe` 框架支持的三种模糊匹配路由规则, `:name`、 `*any`、 `{field}` 分别表示 **命名匹配规则**、 **模糊匹配规则** 及 **字段匹配规则**。不同的规则中使用 `/` 符号来划分层级,路由检索采用 **深度优先算法**,层级越深的规则优先级也会越高。我们运行以上示例,通过访问几个 `URL` 来看看效果: ```html URL 结果 http://127.0.0.1:8199/user/list/2.html /user/list/{field}.html http://127.0.0.1:8199/user/update /:name/update http://127.0.0.1:8199/user/info /:name/:action http://127.0.0.1:8199/user /:name/*any ``` 在这个示例中我们也可以看到,由于优先级的限制,路由规则 `/:name` 会被 `/:name/*any` 规则覆盖,将会无法被匹配到,所以在分配路由规则的时候,需要进行统一规划和管理,避免类似情况的产生。 ## 注册规则 ### 路由注册参数 **最基础的** 路由绑定方法是 `BindHandler` 方法,我们来看一下之前一直使用的 `BindHandler` 的原型: ```go func (s *Server) BindHandler(pattern string, handler interface{}) ``` #### `pattern` 参数 其中的 `pattern` 为路由注册规则字符串,在其他路由注册方法中也会使用到,参数格式如下: ```html [HTTPMethod:]路由规则[@域名] ``` 其中 `HTTPMethod`( `GET/PUT/POST/DELETE/PATCH/HEAD/CONNECT/OPTIONS/TRACE`)和 `@域名` 为 **非必需参数**,大部分场景下直接给定路由规则参数即可, `BindHandler` 会自动绑定 **所有的** 请求方式,如果给定 `HTTPMethod`,那么路由规则仅会在该请求方式下有效。 `@域名` 可以指定生效的域名名称,那么该路由规则仅会在该域名下生效。 **提示** `BindHandler` 是最原生的路由注册方法,在大部分场景中,我们通常使用 **分组路由** 方式来管理理由,后续章节将会介绍: [路由注册-分组路由](./router-registering-group.md)。 #### `handler` 参数 其中的 `handler` 参数通常用于指定路由函数,我们最基础的示例都是使用函数来注册路由,一个路由函数需要满足以下定义,即只要能接收请求对象 `ghttp.Request` 即可: ```go func(r *ghttp.Request) { // ... } ``` 我们来看一个例子: ```go package main import ( "github.com/gogf/gf/v2/net/ghttp" "github.com/gogf/gf/v2/frame/g" ) func main() { s := g.Server() // 该路由规则仅会在GET请求下有效 s.BindHandler("GET:/{table}/list/{page}.html", func(r *ghttp.Request){ r.Response.WriteJson(r.Router) }) // 该路由规则仅会在GET请求及localhost域名下有效 s.BindHandler("GET:/order/info/{order_id}@localhost", func(r *ghttp.Request){ r.Response.WriteJson(r.Router) }) // 该路由规则仅会在DELETE请求下有效 s.BindHandler("DELETE:/comment/{id}", func(r *ghttp.Request){ r.Response.WriteJson(r.Router) }) s.SetPort(8199) s.Run() } ``` 其中返回的参数 `r.Router` 是当前匹配的路由规则信息,访问当该方法的时候,服务端会输出当前匹配的路由规则信息。执行后,我们在终端使用 `curl` 命令进行测试: ```bash $ curl -XGET http://127.0.0.1:8199/order/list/1.html {"Domain":"default","Method":"GET","Priority":3,"Uri":"/{table}/list/{page}.html"} $ curl -XGET http://127.0.0.1:8199/order/info/1 Not Found $ curl -XGET http://localhost:8199/order/info/1 {"Domain":"localhost","Method":"GET","Priority":3,"Uri":"/order/info/{order_id}"} $ curl -XDELETE http://127.0.0.1:8199/comment/1000 {"Domain":"default","Method":"DELETE","Priority":2,"Uri":"/comment/{id}"} $ curl -XGET http://127.0.0.1:8199/comment/1000 Not Found ``` ### 精准匹配规则 精准匹配规则即 **未使用任何动态规则** 的规则,如: `user`、 `order`、 `info` 等等这种 **确定名称** 的规则。在大多数场景下,精准匹配规则会和动态规则一起使用来进行路由注册(例如: `/:name/list`,其中层级1 `:name` 为命名匹配规则,层级2 `list` 是精准匹配规则)。 ### 动态路由规则 动态路由规则分为三种: **命名匹配规则**、 **模糊匹配规则** 和 **字段匹配规则**。动态路由的底层数据结构是由层级 `哈希表` 和 `双向链表` 构建的 `路由树`,层级哈希表便于高效率地层级匹配 `URI`;数据链表用于优先级控制,同一层级的路由规则按照优先级进行排序,优先级高的规则排在链表头。底层的路由规则与请求 `URI` 的匹配计算采用的是正则表达式,并充分使用了缓存机制,执行效率十分高效。 所有匹配到的参数都将会以 `Router` 参数的形式传递给业务层,可以通过 `ghttp.Request` 对象的以下方法获取匹配到的路由参数: ```go func (r *Request) GetRouter(key string, def ...interface{}) *gvar.Var ``` 也可以使用 `ghttp.Request.Get` 方法获取匹配到的路由参数。 #### 命名匹配规则 使用 `:name` 方式进行匹配( `name` 为自定义的匹配名称),对 `URI` 指定层级的参数进行命名匹配(类似正则 `([^/]+)`,该 `URI` 层级必须有值),对应匹配参数会被解析为 `Router` 参数并传递给注册的服务接口使用。 匹配示例1: ```html rule: /user/:user /user/john match /user/you match /user/john/profile no match /user/ no match ``` 匹配示例2: ```html rule: /:name/action /john/name no match /john/action match /smith/info no match /smith/info/age no match /smith/action match ``` 匹配示例3: ```html rule: /:name/:action /john/name match /john/info match /smith/info match /smith/info/age no match /smith/action/del no match ``` #### 模糊匹配规则 使用 `*any` 方式进行匹配( `any` 为自定义的匹配名称),对 `URI` 指定位置之后的参数进行模糊匹配(类似正则 `(.*)`,该 `URI` 层级可以为空),并将匹配参数解析为 `Router` 参数并传递给注册的服务接口使用。 匹配示例1: ```html rule: /src/*path /src/ match /src/somefile.go match /src/subdir/somefile.go match /user/ no match /user/john no match ``` 匹配示例2: ```html rule: /src/*path/:action /src/ no match /src/somefile.go match /src/somefile.go/del match /src/subdir/file.go/del match ``` 匹配示例3: ```html rule: /src/*path/show /src/ no match /src/somefile.go no match /src/somefile.go/del no match /src/somefile.go/show match /src/subdir/file.go/show match /src/show match ``` #### 字段匹配规则 使用 `{field}` 方式进行匹配( `field` 为自定义的匹配名称),可对 `URI` **任意位置** 的参数进行截取匹配(类似正则 `([\w\.\-]+)`,该 `URI` 层级必须有值,并且可以在同一层级进行多个字段匹配),并将匹配参数解析为 `Router` 参数并传递给注册的服务接口使用。 匹配示例1: ```html rule: /order/list/{page}.php /order/list/1.php match /order/list/666.php match /order/list/2.php5 no match /order/list/1 no match /order/list no match ``` 匹配示例2: ```html rule: /db-{table}/{id} /db-user/1 match /db-user/2 match /db/user/1 no match /db-order/100 match /database-order/100 no match ``` 匹配示例3: ```html rule: /{obj}-{act}/*param /user-delete/10 match /order-update/20 match /log-list match /log/list/1 no match /comment/delete/10 no match ``` ### 动态路由示例 ```go package main import ( "github.com/gogf/gf/v2/net/ghttp" "github.com/gogf/gf/v2/frame/g" ) func main() { s := g.Server() // 一个简单的分页路由示例 s.BindHandler("/user/list/{page}.html", func(r *ghttp.Request){ r.Response.Writeln(r.Get("page")) }) // {xxx} 规则与 :xxx 规则混合使用 s.BindHandler("/{object}/:attr/{act}.php", func(r *ghttp.Request){ r.Response.Writeln(r.Get("object")) r.Response.Writeln(r.Get("attr")) r.Response.Writeln(r.Get("act")) }) // 多种模糊匹配规则混合使用 s.BindHandler("/{class}-{course}/:name/*act", func(r *ghttp.Request){ r.Response.Writeln(r.Get("class")) r.Response.Writeln(r.Get("course")) r.Response.Writeln(r.Get("name")) r.Response.Writeln(r.Get("act")) }) s.SetPort(8199) s.Run() } ``` 执行后,我们可以通过 `curl` 命令或者浏览器访问的方式进行测试,以下为测试结果: ```bash $ curl -XGET http://127.0.0.1:8199/user/list/1.html 1 $ curl -XGET http://127.0.0.1:8199/user/info/save.php user info save $ curl -XGET http://127.0.0.1:8199/class3-math/john/score class3 math john score ``` ## 优先级控制 优先级控制按照 **深度优先策略**,简要计算策略: 1. **层级越深的规则优先级越高**; 2. **同一层级下,精准匹配优先级高于模糊匹配**; 3. **同一层级下,模糊匹配优先级:字段匹配 > 命名匹配 \> 模糊匹配**; 我们来看示例(左边的规则优先级比右边高): ```html /:name > /*any /user/name > /user/:action /:name/info > /:name/:action /:name/:action > /:name/*action /:name/{action} > /:name/:action /src/path/del > /src/path /src/path/del > /src/path/:action /src/path/*any > /src/path ``` # 路由管理-路由注册 ## 基本介绍 `WebServer` 提供服务需要方法/对象的支持, `ghttp` 包支持多种路由注册模式,为开发者提供非常强大和灵活的接口功能。 路由注册是整个 `WebServer` 最核心的部分,也是 `goframe` 框架中最精心设计的一个模块。 接口文档: [https://pkg.go.dev/github.com/gogf/gf/v2/net/ghttp](https://pkg.go.dev/github.com/gogf/gf/v2/net/ghttp) ## 注意事项 从框架 `v2` 版本开始,增加了 **规范路由** 的特性,更符合业务项目工程化的场景,推荐在复杂的业务项目中使用规范路由: [路由注册-规范路由](./router-registering-strict-router.md) # 路由注册-函数注册 ## 函数注册 **函数注册** 方式是最简单且最灵活的的路由注册方式,注册的路由 `handler` 可以是一个实例化 **对象的方法地址**,也可以是一个 **包方法地址**。相关方法: ```go func (s *Server) BindHandler(pattern string, handler interface{}) ``` ## 使用示例 ### `Hello World` 我们来看一个简单的示例: ```go package main import ( "github.com/gogf/gf/v2/frame/g" "github.com/gogf/gf/v2/net/ghttp" ) func main() { s := g.Server() s.BindHandler("/", func(r *ghttp.Request) { r.Response.Write("哈喽世界!") }) s.SetPort(8199) s.Run() } ``` 执行后,我们访问 [http://127.0.0.1:8199](http://127.0.0.1:8199) 可以看到我们熟悉的内容。 ### 包方法注册 注册的路由函数 `handler` 参数可以是一个包方法,例如: ```go package main import ( "github.com/gogf/gf/v2/container/gtype" "github.com/gogf/gf/v2/frame/g" "github.com/gogf/gf/v2/net/ghttp" ) var ( total = gtype.NewInt() ) func Total(r *ghttp.Request) { r.Response.Write("total:", total.Add(1)) } func main() { s := g.Server() s.BindHandler("/total", Total) s.SetPort(8199) s.Run() } ``` 在该示例中,我们通过包方法的形式来注册路由。该方法返回总共访问的次数,由于涉及到并发安全,因此 `total` 变量使用了 `gtype.Int` 并发安全类型。执行后,当我们不停访问 [http://127.0.0.1:8199/total](http://127.0.0.1:8199/total) 时,可以看到返回的数值不停递增。 ### 对象方法注册 注册的路由函数 `handler` 参数可以是一个对象的方法,例如: ```go package main import ( "github.com/gogf/gf/v2/container/gtype" "github.com/gogf/gf/v2/frame/g" "github.com/gogf/gf/v2/net/ghttp" ) type Controller struct { total *gtype.Int } func (c *Controller) Total(r *ghttp.Request) { r.Response.Write("total:", c.total.Add(1)) } func main() { s := g.Server() c := &Controller{ total: gtype.NewInt(), } s.BindHandler("/total", c.Total) s.SetPort(8199) s.Run() } ``` 该示例与示例1的实现的效果一致,但我们使用了对象来封装业务逻辑所需的变量,使用路由函数注册来灵活注册对应的对象方法。 # 路由注册-对象注册 **对象注册** 是通过一个实例化的对象来执行路由注册,以后每一个请求都交给该对象(同一对象)处理, **该对象常驻内存不释放**。 **相关方法**: ```go func (s *Server) BindObject(pattern string, object interface{}, methods ...string) error func (s *Server) BindObjectMethod(pattern string, object interface{}, method string) error func (s *Server) BindObjectRest(pattern string, object interface{}) error ``` **前置约定**:需要进行路由注册的方法必须为 **公开方法**,并且 **方法定义** 如下: ```go func(r *ghttp.Request) ``` 否则无法完成注册,路由注册时将会有错误提示,例如: ``` panic: interface conversion: interface {} is xxx, not func(*ghttp.Request) ``` ## 对象注册- `BindObject` 我们可以通过 `BindObject` 方法完成对象的注册。 ```go package main import ( "github.com/gogf/gf/v2/frame/g" "github.com/gogf/gf/v2/net/ghttp" ) type Controller struct{} func (c *Controller) Index(r *ghttp.Request) { r.Response.Write("index") } func (c *Controller) Show(r *ghttp.Request) { r.Response.Write("show") } func main() { s := g.Server() c := new(Controller) s.BindObject("/object", c) s.SetPort(8199) s.Run() } ``` 可以看到,对象在进行路由注册时便生成了一个对象(对象在 `Server` 启动时便生成),此后不管多少请求进入, `Server` 都是将请求转交给该对象对应的方法进行处理。该示例执行后,终端输出的路由表如下: ```text SERVER | DOMAIN | ADDRESS | METHOD | ROUTE | HANDLER | MIDDLEWARE |---------|---------|---------|--------|---------------|--------------------------|------------| default | default | :8199 | ALL | /object | main.(*Controller).Index | |---------|---------|---------|--------|---------------|--------------------------|------------| default | default | :8199 | ALL | /object/index | main.(*Controller).Index | |---------|---------|---------|--------|---------------|--------------------------|------------| default | default | :8199 | ALL | /object/show | main.(*Controller).Show | |---------|---------|---------|--------|---------------|--------------------------|------------| ``` 随后可以通过 [http://127.0.0.1:8199/object/show](http://127.0.0.1:8199/object/show) 查看效果。 **提示** 控制器中的 `Index` 方法是一个特殊的方法,例如,当注册的路由规则为 `/user` 时,HTTP请求到 `/user` 时,将会自动映射到控制器的 `Index` 方法。也就是说,访问地址 `/user` 和 `/user/index` 将会达到相同的执行效果。 ### 路由内置变量 当使用 `BindObject` 方法进行对象注册时,在路由规则中可以使用两个内置的变量: `{.struct}` 和 `{.method}`,前者表示当前 **对象名称**,后者表示当前注册的 **方法名**。我们来看一个例子: ```go package main import ( "github.com/gogf/gf/v2/frame/g" "github.com/gogf/gf/v2/net/ghttp" ) type Order struct{} func (o *Order) List(r *ghttp.Request) { r.Response.Write("list") } func main() { s := g.Server() o := new(Order) s.BindObject("/{.struct}-{.method}", o) s.SetPort(8199) s.Run() } ``` 执行后,终端输出的路由表如下: ```text SERVER | DOMAIN | ADDRESS | METHOD | ROUTE | HANDLER | MIDDLEWARE |---------|---------|---------|--------|-------------|--------------------|------------| default | default | :8199 | ALL | /order-list | main.(*Order).List | |---------|---------|---------|--------|-------------|--------------------|------------| ``` 我们尝试着访问 [http://127.0.0.1:8199/order-list](http://127.0.0.1:8199/order-list),可以看到页面输出 `list`。如果路由规则中不使用内置变量,那么默认的情况下,方法将会被追加到指定的路由规则末尾。 ### 命名风格规则 通过对象进行路由注册时,可以根据对象及方法名称自动生成路由规则,默认的路由规则为:当方法名称带有多个 `单词`(按照字符大写区分单词)时,路由控制器默认会自动使用英文连接符号 `-` 进行拼接,因此访问的时候方法名称需要带 `-` 号。 例如,方法名为 `UserName` 时,生成的路由为 `user-name`;方法名为 `ShowListItems` 时,生成的路由为 `show-list-items`;以此类推。 此外,我们可以通过 `.Server.SetNameToUriType` 方法来设置对象方法名称的路由生成方式。支持的方式目前有 `4` 种,对应 `4` 个常量定义: ``` UriTypeDefault = 0 // (默认)全部转为小写,单词以'-'连接符号连接 UriTypeFullName = 1 // 不处理名称,以原有名称构建成URI UriTypeAllLower = 2 // 仅转为小写,单词间不使用连接符号 UriTypeCamel = 3 // 采用驼峰命名方式 ``` 注意:需要在通过对象进行路由注册前进行该参数的设置,在路由注册后设置将不会生效,那么将使用默认规则。 我们来看一个示例: ```go package main import ( "github.com/gogf/gf/v2/frame/g" "github.com/gogf/gf/v2/net/ghttp" ) type User struct{} func (u *User) ShowList(r *ghttp.Request) { r.Response.Write("list") } func main() { u := new(User) s1 := g.Server("UriTypeDefault") s2 := g.Server("UriTypeFullName") s3 := g.Server("UriTypeAllLower") s4 := g.Server("UriTypeCamel") s1.SetNameToUriType(ghttp.UriTypeDefault) s2.SetNameToUriType(ghttp.UriTypeFullName) s3.SetNameToUriType(ghttp.UriTypeAllLower) s4.SetNameToUriType(ghttp.UriTypeCamel) s1.BindObject("/{.struct}/{.method}", u) s2.BindObject("/{.struct}/{.method}", u) s3.BindObject("/{.struct}/{.method}", u) s4.BindObject("/{.struct}/{.method}", u) s1.SetPort(8100) s2.SetPort(8200) s3.SetPort(8300) s4.SetPort(8400) s1.Start() s2.Start() s3.Start() s4.Start() g.Wait() } ``` 为了对比演示效果,这个示例采用了 `多Server` 运行方式,将不同的名称转换方式使用了不同的 `Server` 来配置运行,因此我们可以方便地在同一个程序中,访问不同的 `Server`(通过不同的端口绑定)看到不同的结果。 执行后,终端输出的路由表如下: ```text SERVER | DOMAIN | ADDRESS | METHOD | ROUTE | HANDLER | MIDDLEWARE -----------------|---------|---------|--------|-----------------|-----------------------|------------- UriTypeDefault | default | :8100 | ALL | /user/show-list | main.(*User).ShowList | -----------------|---------|---------|--------|-----------------|-----------------------|------------- SERVER | DOMAIN | ADDRESS | METHOD | ROUTE | HANDLER | MIDDLEWARE ------------------|---------|---------|--------|----------------|-----------------------|------------- UriTypeFullName | default | :8200 | ALL | /User/ShowList | main.(*User).ShowList | ------------------|---------|---------|--------|----------------|-----------------------|------------- SERVER | DOMAIN | ADDRESS | METHOD | ROUTE | HANDLER | MIDDLEWARE ------------------|---------|---------|--------|----------------|-----------------------|------------- UriTypeAllLower | default | :8300 | ALL | /user/showlist | main.(*User).ShowList | ------------------|---------|---------|--------|----------------|-----------------------|------------- SERVER | DOMAIN | ADDRESS | METHOD | ROUTE | HANDLER | MIDDLEWARE ---------------|---------|---------|--------|----------------|-----------------------|------------- UriTypeCamel | default | :8400 | ALL | /user/showList | main.(*User).ShowList | ---------------|---------|---------|--------|----------------|-----------------------|------------- ``` 可以分别访问以下 `URL` 地址得到期望的结果: ``` http://127.0.0.1:8100/user/show-list http://127.0.0.1:8200/User/ShowList http://127.0.0.1:8300/user/showlist http://127.0.0.1:8400/user/showList ``` ### 对象方法注册 假如控制器中有若干公开方法,但是我只想注册其中几个,其余的方法我不想对外公开,怎么办?我们可以通过 `BindObject` 传递 **第三个非必需参数** 替换实现,参数支持传入 **多个** 方法名称,多个名称以英文 `,` 号分隔( **方法名称参数区分大小写**)。 使用示例: ```go package main import ( "github.com/gogf/gf/v2/frame/g" "github.com/gogf/gf/v2/net/ghttp" ) type Controller struct{} func (c *Controller) Index(r *ghttp.Request) { r.Response.Write("index") } func (c *Controller) Show(r *ghttp.Request) { r.Response.Write("show") } func main() { s := g.Server() c := new(Controller) s.BindObject("/object", c, "Show") s.SetPort(8199) s.Run() } ``` 执行后,终端输出路由表为: ```text SERVER | DOMAIN | ADDRESS | METHOD | ROUTE | HANDLER | MIDDLEWARE |---------|---------|---------|--------|--------------|-------------------------|------------| default | default | :8199 | ALL | /object/show | main.(*Controller).Show | |---------|---------|---------|--------|--------------|-------------------------|------------| ``` ## 绑定路由方法- `BindObjectMethod` 我们可以通过 `BindObjectMethod` 方法绑定指定的路由到指定的方法执行( **方法名称参数区分大小写**)。 **提示** `BindObjectMethod` 和 `BindObject` 的区别: `BindObjectMethod` 将对象中的指定方法与指定路由规则进行绑定,第三个 `method` 参数只能指定一个方法名称; `BindObject` 注册时,所有的路由都是对象方法名称按照规则生成的,第三个 `methods` 参数可以指定多个注册的方法名称。 来看一个例子: ```go package main import ( "github.com/gogf/gf/v2/frame/g" "github.com/gogf/gf/v2/net/ghttp" ) type Controller struct{} func (c *Controller) Index(r *ghttp.Request) { r.Response.Write("index") } func (c *Controller) Show(r *ghttp.Request) { r.Response.Write("show") } func main() { s := g.Server() c := new(Controller) s.BindObjectMethod("/show", c, "Show") s.SetPort(8199) s.Run() } ``` 执行后,终端输出的路由表如下: ```text SERVER | DOMAIN | ADDRESS | METHOD | ROUTE | HANDLER | MIDDLEWARE |---------|---------|---------|--------|-------|-------------------------|------------| default | default | :8199 | ALL | /show | main.(*Controller).Show | |---------|---------|---------|--------|-------|-------------------------|------------| ``` ## `RESTful` 对象注册- `BindObjectRest` `RESTful` 设计方式的控制器,通常用于 `API` 服务。 **在这种模式下, `HTTP` 的 `Method` 将会映射到控制器对应的方法名称**,例如: `POST` 方式将会映射到控制器的 `Post` 方法中(公开方法,首字母大写), `DELETE` 方式将会映射到控制器的 `Delete` 方法中,以此类推。其他非 `HTTP Method` 命名的方法,即使是定义的包公开方法,将不会自动注册,对于应用端不可见。当然,如果控制器并未定义对应 `HTTP Method` 的方法,该 `Method` 请求下将会返回 `HTTP Status 404`。 **注意** `GoFrame` 的这种 `RESTful` 对象注册方式是一种严格的 `REST` 路由注册方式,我们可以将控制器的对象看做 `REST` 中的资源,而其中的 `HTTP Method` 方法即为 `REST` 规范的资源操作方法。如果大家不太熟悉 `REST` 规范,或者不想太过严格的 `RESTful` 路由设计,那么请忽略该章节。 我们可以通过 `BindObjectRest` 方法完成 `REST` 对象的注册,示例: ```go package main import ( "github.com/gogf/gf/v2/frame/g" "github.com/gogf/gf/v2/net/ghttp" ) type Controller struct{} // RESTFul - GET func (c *Controller) Get(r *ghttp.Request) { r.Response.Write("GET") } // RESTFul - POST func (c *Controller) Post(r *ghttp.Request) { r.Response.Write("POST") } // RESTFul - DELETE func (c *Controller) Delete(r *ghttp.Request) { r.Response.Write("DELETE") } // 该方法无法映射,将会无法访问到 func (c *Controller) Hello(r *ghttp.Request) { r.Response.Write("Hello") } func main() { s := g.Server() c := new(Controller) s.BindObjectRest("/object", c) s.SetPort(8199) s.Run() } ``` 执行后,终端输出路由表如下; ```text SERVER | DOMAIN | ADDRESS | METHOD | ROUTE | HANDLER | MIDDLEWARE |---------|---------|---------|--------|---------|---------------------------|------------| default | default | :8199 | DELETE | /object | main.(*Controller).Delete | |---------|---------|---------|--------|---------|---------------------------|------------| default | default | :8199 | GET | /object | main.(*Controller).Get | |---------|---------|---------|--------|---------|---------------------------|------------| default | default | :8199 | POST | /object | main.(*Controller).Post | |---------|---------|---------|--------|---------|---------------------------|------------| ``` ## 构造方法 `Init` 与析构方法 `Shut` 对象中的 `Init` 和 `Shut` 是两个在 `HTTP` 请求流程中被 `Server` 自动调用的特殊方法(类似 `构造函数` 和 `析构函数` 的作用)。 1. `Init` 回调方法 对象收到请求时的初始化方法,在服务接口调用之前被回调执行。 方法定义: ```go // "构造函数"对象方法 func (c *Controller) Init(r *ghttp.Request) { } ``` 2. `Shut` 回调方法 当请求结束时被 `Server` 自动调用,可以用于对象执行一些收尾处理的操作。 方法定义: ```go // "析构函数"对象方法 func (c *Controller) Shut(r *ghttp.Request) { } ``` 举个例子: ```go package main import ( "github.com/gogf/gf/v2/frame/g" "github.com/gogf/gf/v2/net/ghttp" ) type Controller struct{} func (c *Controller) Init(r *ghttp.Request) { r.Response.Writeln("Init") } func (c *Controller) Shut(r *ghttp.Request) { r.Response.Writeln("Shut") } func (c *Controller) Hello(r *ghttp.Request) { r.Response.Writeln("Hello") } func main() { s := g.Server() c := new(Controller) s.BindObject("/object", c) s.SetPort(8199) s.Run() } ``` 执行后,终端输出路由表如下: ```text SERVER | DOMAIN | ADDRESS | METHOD | ROUTE | HANDLER | MIDDLEWARE |---------|---------|---------|--------|---------------|--------------------------|------------| default | default | :8199 | ALL | /object/hello | main.(*Controller).Hello | |---------|---------|---------|--------|---------------|--------------------------|------------| ``` 可以看到,并没有自动注册 `Init` 和 `Shut` 这两个方法的路由,我们访问 [http://127.0.0.1:8199/object/hello](http://127.0.0.1:8199/object/hello) 后,输出结果为: ```Init Hello Shut ``` # 路由注册-分组路由 分组路由是业务项目中主要使用的路由注册方式。 ## 分组路由 `GoFrame` 框架支持分组路由的注册方式,可以给分组路由指定一个 `prefix` 前缀(也可以直接给定 `/` 前缀,表示注册在根路由下),在该分组下的所有路由注册都将注册在该路由前缀下。 **分组路由注册方式也是推荐的路由注册方式**。 **接口文档**: [https://pkg.go.dev/github.com/gogf/gf/v2/net/ghttp#RouterGroup](https://pkg.go.dev/github.com/gogf/gf/v2/net/ghttp#RouterGroup) ```go // 创建分组路由 func (s *Server) Group(prefix string, groups ...func(g *RouterGroup)) *RouterGroup // 注册Method路由 func (g *RouterGroup) ALL(pattern string, object interface{}, params...interface{}) func (g *RouterGroup) GET(pattern string, object interface{}, params...interface{}) func (g *RouterGroup) PUT(pattern string, object interface{}, params...interface{}) func (g *RouterGroup) POST(pattern string, object interface{}, params...interface{}) func (g *RouterGroup) DELETE(pattern string, object interface{}, params...interface{}) ... // 中间件绑定 func (g *RouterGroup) Middleware(handlers ...HandlerFunc) *RouterGroup // 批量注册 func (g *RouterGroup) Map(m map[string]interface{}) func (g *RouterGroup) ALLMap(m map[string]interface{}) ``` **简要介绍:** 1. `Group` 方法用于创建一个分组路由对象,并且支持在指定域名对象上创建。 2. 以 `HTTP Method` 命名的方法用于绑定指定的 `HTTP Method` 路由;其中 `ALL` 方法用于注册所有的 `HTTP Method` 到指定的函数/对象/控制器上; `REST` 方法用于注册 `RESTful` 风格的路由,需给定一个执行对象或者控制器对象。 3. `Middleware` 方法用于绑定一个或多个中间件到当前分组的路由上,具体详见中间件章节。 ## 简单示例 ```go package main import ( "github.com/gogf/gf/v2/frame/g" "github.com/gogf/gf/v2/net/ghttp" ) func main() { s := g.Server() s.Group("/api", func(group *ghttp.RouterGroup) { group.ALL("/all", func(r *ghttp.Request) { r.Response.Write("all") }) group.GET("/get", func(r *ghttp.Request) { r.Response.Write("get") }) group.POST("/post", func(r *ghttp.Request) { r.Response.Write("post") }) }) s.SetPort(8199) s.Run() } ``` 执行后,终端打印出路由表如下: ```text SERVER | DOMAIN | ADDRESS | METHOD | ROUTE | HANDLER | MIDDLEWARE |---------|---------|---------|--------|-----------|-----------------|------------| default | default | :8199 | ALL | /api/all | main.main.func1 | |---------|---------|---------|--------|-----------|-----------------|------------| default | default | :8199 | GET | /api/get | main.main.func2 | |---------|---------|---------|--------|-----------|-----------------|------------| default | default | :8199 | POST | /api/post | main.main.func3 | |---------|---------|---------|--------|-----------|-----------------|------------| ``` 其中, `/api/get` 仅允许 `GET` 方式访问, `/api/post` 仅允许 `POST` 方式访问, `/api/all` 允许所有的方式访问。 我们使用 `curl` 工具来测试一下: 1. `/api/get` ```bash $ curl http://127.0.0.1:8199/api/get get $ curl -X POST http://127.0.0.1:8199/api/get Not Found ``` 1. `/api/post` ```bash $ curl http://127.0.0.1:8199/api/post Not Found $ curl -X POST http://127.0.0.1:8199/api/post post ``` 3. `/api/all` ```bash $ curl http://127.0.0.1:8199/api/all all $ curl -X POST http://127.0.0.1:8199/api/all all $ curl -X DELETE http://127.0.0.1:8199/api/all all $ curl -X OPTIONS http://127.0.0.1:8199/api/all all ``` ## 层级注册 **提示** `GoFrame` 框架的层级路由注册方式灵感来源于 `PHP Laravel` 框架。 推荐使用路由层级注册方式,注册的路由代码更清晰直观。 `GoFrame` 框架的分组路由注册支持更加直观优雅层级的注册方式,以便于开发者更方便地管理路由列表。路由层级注册方式也是推荐的路由注册方式。我们来看一个比较完整的示例,该示例中注册了使用到了中间件、 `HOOK` 以及不同 `HTTP Method` 绑定的路由注册: ```go package main import ( "fmt" "net/http" "github.com/gogf/gf/v2/frame/g" "github.com/gogf/gf/v2/net/ghttp" ) func MiddlewareAuth(r *ghttp.Request) { if r.Get("token").String() != "123456" { r.Response.WriteStatus(http.StatusForbidden) return } r.Middleware.Next() } func MiddlewareCORS(r *ghttp.Request) { r.Response.CORSDefault() r.Middleware.Next() } func MiddlewareLog(r *ghttp.Request) { r.Middleware.Next() fmt.Println(r.Response.Status, r.URL.Path) } func main() { s := g.Server() s.Use(MiddlewareLog) s.Group("/api.v2", func(group *ghttp.RouterGroup) { group.Middleware(MiddlewareAuth, MiddlewareCORS) group.GET("/test", func(r *ghttp.Request) { r.Response.Write("test") }) group.Group("/order", func(group *ghttp.RouterGroup) { group.GET("/list", func(r *ghttp.Request) { r.Response.Write("list") }) group.PUT("/update", func(r *ghttp.Request) { r.Response.Write("update") }) }) group.Group("/user", func(group *ghttp.RouterGroup) { group.GET("/info", func(r *ghttp.Request) { r.Response.Write("info") }) group.POST("/edit", func(r *ghttp.Request) { r.Response.Write("edit") }) group.DELETE("/drop", func(r *ghttp.Request) { r.Response.Write("drop") }) }) group.Group("/hook", func(group *ghttp.RouterGroup) { group.Hook("/*", ghttp.HookBeforeServe, func(r *ghttp.Request) { r.Response.Write("hook any") }) group.Hook("/:name", ghttp.HookBeforeServe, func(r *ghttp.Request) { r.Response.Write("hook name") }) }) }) s.SetPort(8199) s.Run() } ``` 执行后,注册的路由列表如下: ```text SERVER | DOMAIN | ADDRESS | METHOD | ROUTE | HANDLER | MIDDLEWARE |---------|---------|---------|--------|----------------------|---------------------|-----------------------------------------| default | default | :8199 | ALL | /* | main.MiddlewareLog | GLOBAL MIDDLEWARE |---------|---------|---------|--------|----------------------|---------------------|-----------------------------------------| default | default | :8199 | ALL | /api.v2/hook/* | main.main.func1.4.1 | HOOK_BEFORE_SERVE |---------|---------|---------|--------|----------------------|---------------------|-----------------------------------------| default | default | :8199 | ALL | /api.v2/hook/:name | main.main.func1.4.2 | HOOK_BEFORE_SERVE |---------|---------|---------|--------|----------------------|---------------------|-----------------------------------------| default | default | :8199 | GET | /api.v2/order/list | main.main.func1.2.1 | main.MiddlewareAuth,main.MiddlewareCORS |---------|---------|---------|--------|----------------------|---------------------|-----------------------------------------| default | default | :8199 | PUT | /api.v2/order/update | main.main.func1.2.2 | main.MiddlewareAuth,main.MiddlewareCORS |---------|---------|---------|--------|----------------------|---------------------|-----------------------------------------| default | default | :8199 | GET | /api.v2/test | main.main.func1.1 | main.MiddlewareAuth,main.MiddlewareCORS |---------|---------|---------|--------|----------------------|---------------------|-----------------------------------------| default | default | :8199 | DELETE | /api.v2/user/drop | main.main.func1.3.3 | main.MiddlewareAuth,main.MiddlewareCORS |---------|---------|---------|--------|----------------------|---------------------|-----------------------------------------| default | default | :8199 | POST | /api.v2/user/edit | main.main.func1.3.2 | main.MiddlewareAuth,main.MiddlewareCORS |---------|---------|---------|--------|----------------------|---------------------|-----------------------------------------| default | default | :8199 | GET | /api.v2/user/info | main.main.func1.3.1 | main.MiddlewareAuth,main.MiddlewareCORS |---------|---------|---------|--------|----------------------|---------------------|-----------------------------------------| ``` ## 批量注册 ### `Map` 通过 `Map` 方法可以实现批量的分组路由注册,但是如果是相同的 `URI` 不同的 `HTTP Method` 需要按照路由规范给定 `HTTP Method`。使用示例: ```go package main import ( "github.com/gogf/gf/v2/frame/g" "github.com/gogf/gf/v2/net/ghttp" ) func UserGet(r *ghttp.Request) { r.Response.Write("get") } func UserDelete(r *ghttp.Request) { r.Response.Write("delete") } func main() { s := g.Server() s.Group("/api", func(group *ghttp.RouterGroup) { group.Map(g.Map{ "GET: /user": UserGet, "DELETE: /user": UserDelete, }) }) s.SetPort(8199) s.Run() } ``` ### `AllMap` 也可以使用 `ALLMap` 方法实现批量的分组路由注册,通过该方法注册的路由将把路由函数/对象应用到所有的 `HTTP Method`。使用示例: ```go s := g.Server() // 前台系统路由注册 s.Group("/", func(group *ghttp.RouterGroup) { group.Middleware(service.Middleware.Ctx) group.ALLMap(g.Map{ "/": api.Index, // 首页 "/login": api.Login, // 登录 "/register": api.Register, // 注册 "/category": api.Category, // 栏目 "/topic": api.Topic, // 主题 "/topic/:id": api.Topic.Detail, // 主题 - 详情 "/ask": api.Ask, // 问答 "/ask/:id": api.Ask.Detail, // 问答 - 详情 "/article": api.Article, // 文章 "/article/:id": api.Article.Detail, // 文章 - 详情 "/reply": api.Reply, // 回复 "/search": api.Search, // 搜索 "/captcha": api.Captcha, // 验证码 "/user/:id": api.User.Index, // 用户 - 主页 }) // 权限控制路由 group.Group("/", func(group *ghttp.RouterGroup) { group.Middleware(service.Middleware.Auth) group.ALLMap(g.Map{ "/user": api.User, // 用户 "/content": api.Content, // 内容 "/interact": api.Interact, // 交互 "/file": api.File, // 文件 }) }) }) ``` # 路由注册-规范路由 ## 基本介绍 从 `v2.0` 版本开始,框架的 `Server` 组件额外提供了规范化的路由注册方式,更加适合团队规范化的使用场景、业务复杂度更高的项目。规范路由实现了以下特性: - 规范化 `API` 按照结构化编程设计 - 规范化 `API` 接口方法参数风格定义 - 更加简化的路由注册与维护 - 统一接口返回数据格式设计 - 保障代码与接口文档同步维护 - 自动的 `API` 参数对象化接收与校验 - 自动生成基于标准 `OpenAPIv3` 协议的接口文档 - 自动生成 `SwaggerUI` 页面 **提示** 注意一下哦: **规范路由** 与原有的 **函数、对象、分组** 路由方式都是框架的 `HTTP Server` 组件并存支持的路由注册方式,是为了解决一些规范化和自动化管理接口的场景,更适用于团队多人协作使用。其他的路由方式,特别是框架 `v1` 版本的函数、对象注册方式也是在新版本支持的哈。看个人使用习惯进行合理选择哟。 # 规范路由-基本示例 ## 配置文件 这里使用 `YAML` 配置文件: `config.yaml` ```yaml server: address: ":8199" openapiPath: "/api.json" swaggerPath: "/swagger" ``` ## 示例代码 我们从一个简单的 `Hello` 例子开始: ```go package main import ( "context" "fmt" "github.com/gogf/gf/v2/frame/g" "github.com/gogf/gf/v2/net/ghttp" ) type HelloReq struct { g.Meta `path:"/hello" method:"get"` Name string `v:"required" dc:"Your name"` } type HelloRes struct { Reply string `dc:"Reply content"` } type Hello struct{} func (Hello) Say(ctx context.Context, req *HelloReq) (res *HelloRes, err error) { g.Log().Debugf(ctx, `receive say: %+v`, req) res = &HelloRes{ Reply: fmt.Sprintf(`Hi %s`, req.Name), } return } func main() { s := g.Server() s.Use(ghttp.MiddlewareHandlerResponse) s.Group("/", func(group *ghttp.RouterGroup) { group.Bind( new(Hello), ) }) s.Run() } ``` 拷贝这段代码,我们运行起来试试,终端输出信息如下: ```html 2021-11-19 23:31:35.277 25580: http server started listening on [:8199] SERVER | DOMAIN | ADDRESS | METHOD | ROUTE | HANDLER | MIDDLEWARE ----------|---------|---------|--------|------------|-----------------------------------------------------------|-------------------- default | default | :8199 | ALL | /* | github.com/gogf/gf/v2/net/ghttp.MiddlewareHandlerResponse | GLOBAL MIDDLEWARE ----------|---------|---------|--------|------------|-----------------------------------------------------------|-------------------- default | default | :8199 | ALL | /api.json | github.com/gogf/gf/v2/net/ghttp.(*Server).openapiSpec-fm | ----------|---------|---------|--------|------------|-----------------------------------------------------------|-------------------- default | default | :8199 | GET | /hello | main.(*Hello).Say | ----------|---------|---------|--------|------------|-----------------------------------------------------------|-------------------- default | default | :8199 | ALL | /swagger/* | github.com/gogf/gf/v2/net/ghttp.(*Server).swaggerUI-fm | HOOK_BEFORE_SERVE ----------|---------|---------|--------|------------|-----------------------------------------------------------|-------------------- ``` 可以看到,除了我们的业务路由之外, `Server` 自动帮我们注册了两个路由: `/api.json` 和 `/swagger/*`。前者是自动生成的基于标准的 `OpenAPIv3` 协议的接口文档,后者是自动生成 `SwaggerUI` 页面,方便开发者查看和调试。这两个功能默认是关闭的,开发者可以通过前面示例中的 `openapiPath` 和 `swaggerPath` 两个配置项开启。 ## 接口文档 接口文档通过 `OpenAPIv3` 协议生成,一般来说需要结合相应的 `UI` 工具查看,地址: [http://127.0.0.1:8199/api.json](http://127.0.0.1:8199/api.json) **提示** 由于 `OpenAPIv3` 协议是规范的接口定义协议,因此开发者根据协议内容可以做很多事,例如:自定义UI展示、Client代码生成、协议转换等等。 ## SwaggerUI 我们来看看这个 `SwaggerUI` 页面: [http://127.0.0.1:8199/swagger/](http://127.0.0.1:8199/swagger/) 这里只有一个我们的路由地址以及对应的输入输出结构体。当然,这只是个简单的示例,可以在真实的项目中通过一些配置使得页面更加丰富多彩。 我们接着在这个页面上做下接口测试吧: 嗯,接口返回了一个固定数据格式的 `Json` 内容,但是能看到其中的 `data` 为我们需要的返回结果。 **提示** 提示:最新版本的 `SwaggerUI` 页面已经不支持接口测试功能,如果对此有要求的同学可以自定义 `SwaggerUI`(参考 [接口文档-自定义UI](./api-document-swagger-ui.md)), 也可以导入接口文件 `api.json` 到第三方工具(例如 `apifox`)进行测试: ## 返回中间件 等等,似乎漏掉了什么东西?是的,我们这里使用了一个 `Server` 组件提供的中间件,它是拿来做什么的呢?我们开看看它的方法定义: 是的,它在我们没有提供自定义的返回数据格式处理中间件时,使用了一个默认的中间件处理我们的请求,并返回了一个默认的数据格式。 # 规范路由-如何使用 ## 路由方法定义 从上面的例子中,我们可以看到,路由方法定义使用固定的格式: ```go func Handler(ctx context.Context, req *Request) (res *Response, err error) ``` 其中输入参数和输出参数都是两个,并且都是必须的一个都不能少。简单介绍下: | 参数 | 说明 | 注意事项 | | --- | --- | --- | | `ctx context.Context` | 上下文 | `Server` 组件会自动从请求中获取并传递给接口方法 | | `req *Request` | 请求对象 | 就算没有接收参数也要定义,因为请求结构体中不仅仅包含请求参数的定义,也包含了接口的请求定义。 | | `res *Response` | 返回对象 | 就算没有返回参数也要定义,因为返回结构体中不仅仅包含返回参数的定义,也可以包含接口返回定义。 | | `err error` | 错误对象 | `Server` 通过该参数判断接口执行成功或失败。 | ## 路由统一注册 ### `Group.Bind` 方法 我们推荐使用对象化的方式来管理所有路由方法,并通过 **分组路由** 的 `Bind` 方法执行统一注册。需要注意的是,在规范化路由方式下,路由地址以及请求方式将由请求结构体在 `g.Meta` 元数据对象中通过标签定义,通过分组路由可以定义分组下的所有路由前缀。 ### `BindHandler` 方法 我们也可以通过基础的 `BindHandler` 方法来注册规范路由,但是该方法只能注册一个路由函数。使用示例: ```go s := g.Server() s.BindHandler("/user/{uid}", func(ctx context.Context, req *SaveReq) (res *SaveRes, err error) { // ... }) ``` ## 规范参数结构 在规范化路由注册中,非常重要的是请求/返回参数结构体的定义,在该结构体不仅仅包含了输入参数的定义,也包含了接口的定义,特别是路由地址、请求方法、接口描述等信息。通过结构体方式的数据结构参数维护,更有利于更丰富的接口能力扩展、团队间的接口交互、长期的接口维护、自动化的接口文档生成。 为保证命名规范化,输入数据结构以`XxxReq`方式命名,输出数据结构以`XxxRes`方式命名。即便输入或者输出参数为空,也需要定义相应的数据结构,这样的目的一个是便于后续扩展,另一个是便于接口信息的管理。关于结构体中涉及到 `OpenAPIv3` 协议的标签介绍请查看后续章节。 **提示** 请求参数自动转换到请求数据结构,字段映射转换不区分大小写,也会自动忽略特殊字符。 ## 输入数据校验 请求结构体在进入 `API` 接口执行前将会被自动执行校验,如果其中一条规则校验失败,那么将终止后续规则的校验(自动使用了 `bail` 校验修饰规则)。校验功能使用的是框架统一的校验组件,具体请参考: [数据校验](../core/gvalid.md) **注意** 需要特别注意:如果参数校验存在多条校验规则,并且规则中存在 `required*` 规则时,那么建议将 `required*` 校验规则放置于所有规则之前,否则规范路由中内置启用的 `bail` 校验规则的特性(校验失败即停止后续校验)有可能会引起后续规则的 `required*` 规则不生效。 ## 统一返回中间件 接口的数据返回处理需要设置 **统一的后置中间件**,当然也可以使用 `Server` 默认提供的数据返回中间件。开发者自定义中间件时可以参考 `Server` 默认提供的中间件 `MiddlewareHandlerResponse`。 这里顺便提一下,在自定义返回中间件时一个重要的方法: ```go // GetHandlerResponse retrieves and returns the handler response object and its error. func (r *Request) GetHandlerResponse() interface{} ``` 通过后置中间件执行时通过请求对象的 `GetHandlerResponse` 方法获取当前业务执行的结果,并根据需要做相应处理。 ## 扩展介绍 ### `OpenAPIv3` 协议 `Server` 组件自动生成的接口文档使用的是最新的 `OpenAPIv3` 协议。更多介绍请参考章节: [接口文档](./api-document.md) ### `Ctx` 中的 `Request` 对象 我们可以通过 `RequestFromCtx/g.RequestFromCtx` 方法从 `ctx` 中获取 `Request` 对象。 方法定义: ```go func RequestFromCtx(ctx context.Context) *Request ``` 使用示例: ```go func (c *cHello) Hello(ctx context.Context, req *apiv1.HelloReq) (res *apiv1.HelloRes, err error) { g.RequestFromCtx(ctx).Response.Writeln("Hello World!") return } ``` # 规范路由-常见问题 ## 在规范路由下,同一接口如何支持多种 `HTTP Method` 提交方式 首先,一个接口应当只做一件事情, `HTTP Method` 是有意义的(例如 `RESTful` 接口风格设计),一个接口支持多种 `HTTP Method` 方式通常是接口设计不合理,建议重新审视接口设计。 通常是不存在一个 `API` 需要绑定多个 `HTTP Method` 的场景的。例如,拿用户接口来讲,一个 `CRUD` 接口在 `RESTful` 实现应当有 `4-5` 个 `API` 定义,实现不同的业务逻辑。那么可能存在以下 `API` 定义的 `RESTful` 接口: ``` 接口名称 Method Path 创建用户 PUT /user 用户列表 GET /user 用户详情 GET /user/{uid} 修改用户 POST /user/{uid} 删除用户 DELETE /user/{uid} ``` 如果确实存在一个接口支持多个 `HTTP Method` 的场景,那么可以通过在 `Meta` 标签中的 `method` 属性中使用英文 `,` 号分隔各个 `HTTP Method` 即可,例如: ```go type SaveReq struct { g.Meta `path:"/user/{uid}" method:"put,post" summary:"保存用户" tags:"用户管理"` Uid int64 `dc:"用户ID"` Name string `dc:"用户名"` // ... } ``` ## 使用默认提供的 `Response` 结构体,如何让 `Data` 字段只返回数组而无需指定名称的键值对 使用类型别名即可。 源码地址:[JSON数组响应](../../examples/httpserver/response-json-array.md) 结果示例: # 路由管理-中间件/拦截器 ## 延伸阅读 - [为Go应用增加完整的权限认证能力](https://goframe.org/articles/rbac-permission-authentication-in-go) # 中间件/拦截器-基本介绍 `GoFrame` 提供了优雅的中间件请求控制方式, 该方式也是主流的 `WebServer` 提供的请求流程控制方式, 基于中间件设计可以为 `WebServer` 提供更灵活强大的插件机制。经典的中间件洋葱模型: ## 中间件定义 中间件的定义和普通HTTP执行方法 `HandlerFunc` 一样,但是可以在 `Request` 参数中使用 `Middleware` 属性对象来控制请求流程。 我们拿一个跨域请求的中间件定义来示例说明一下: ```go func MiddlewareCORS(r *ghttp.Request) { r.Response.CORSDefault() r.Middleware.Next() } ``` 可以看到在该中间件中执行完成跨域请求处理的逻辑后,使用 `r.Middleware.Next()` 方法进一步执行下一个流程;如果这个时候直接退出不调用 `r.Middleware.Next()` 方法的话,将会退出后续的执行流程(例如可以用于请求的鉴权处理)。 ## 中间件类型 中间件的类型分为两种:前置中间件和后置中间件。前置即在路由服务函数调用之前调用,后置即在其后调用。 ### 前置中间件 其定义类似于: ```go func Middleware(r *ghttp.Request) { // 中间件处理逻辑 r.Middleware.Next() } ``` ### 后置中间件 其定义类似于: ```go func Middleware(r *ghttp.Request) { r.Middleware.Next() // 中间件处理逻辑 } ``` ## 中间件注册 中间件的注册有多种方式,参考接口文档: [https://pkg.go.dev/github.com/gogf/gf/v2/net/ghttp](https://pkg.go.dev/github.com/gogf/gf/v2/net/ghttp) ### 全局中间件 ```go func (s *Server) Use(handlers ...HandlerFunc) ``` 全局中间件是可以 **独立使用** 的请求拦截方法,通过路由规则的方式进行注册,绑定到 `Server` 上,由于中间件需要执行请求拦截操作,因此往往是使用"模糊匹配"或者"命名匹配"规则。 **提示** 全局中间件仅对动态请求拦截有效,无法拦截静态文件请求。 ### 分组路由中间件 ```go func (g *RouterGroup) Middleware(handlers ...HandlerFunc) *RouterGroup ``` 分组路由中注册的中间件绑定到当前分组路由中的所有的服务请求上,当服务请求被执行前会调用到其绑定的中间件方法。 分组路由仅有一个 `Middleware` 的中间件注册方法。分组路由中间件与全局中间件不同之处在于, **分组路由中间件无法独立使用**,必须在分组路由注册中使用,并且绑定到当前分组路由中所有的路由上作为路由方法的一部分。 ## 执行优先级 ### 全局中间件 由于全局中间件也是通过路由规则执行,那么也会存在执行优先级: 1. 首先,由于全局中间件是基于模糊路由匹配,因此 **当同一个路由匹配到多个中间件时,会按照路由的深度优先规则执行**,具体请查看路由章节; 2. 其次, **同一个路由规则下,会按照中间件的注册先后顺序执行**,中间件的注册方法也支持同时按照先后顺序注册多个中间件; 3. 最后,为避免优先级混淆和后续管理,建议将所有中间件放到同一个地方进行先后顺序注册来控制执行优先级; **提示** 这里的建议来参考于 `gRPC` 的拦截器设计,没有过多的路由控制,仅在一个地方同一个方法统一注册。往往越简单,越容易理解,也便于长期维护。 ### 分组路由中间件 分组路由中间件是绑定到分组路由上的服务方法,不存在路由规则匹配,因此只会按照注册的先后顺序执行。参考后续示例或如下代码的执行结果。 展开源码 ```go package main import ( "context" "github.com/gogf/gf/v2/frame/g" "github.com/gogf/gf/v2/net/ghttp" "github.com/gogf/gf/v2/os/glog" ) type HelloReq struct { g.Meta `path:"/hello" method:"get"` } type HelloRes struct { } type Hello struct{} func (Hello) Say(ctx context.Context, req *HelloReq) (res *HelloRes, err error) { glog.Debug(ctx, "中") return } func RequestHandle1(r *ghttp.Request) { glog.Debug(r.GetCtx(), "前1") r.Middleware.Next() } func RequestHandle2(r *ghttp.Request) { glog.Debug(r.GetCtx(), "前2") r.Middleware.Next() } func RequestHandle3(r *ghttp.Request) { glog.Debug(r.GetCtx(), "前3") r.Middleware.Next() } func RequestHandle4(r *ghttp.Request) { glog.Debug(r.GetCtx(), "前4") r.Middleware.Next() } func ResponseHandle1(r *ghttp.Request) { r.Middleware.Next() glog.Debug(r.GetCtx(), "后1") } func ResponseHandle2(r *ghttp.Request) { r.Middleware.Next() glog.Debug(r.GetCtx(), "后2") } func ResponseHandle3(r *ghttp.Request) { r.Middleware.Next() glog.Debug(r.GetCtx(), "后3") } func ResponseHandle4(r *ghttp.Request) { r.Middleware.Next() glog.Debug(r.GetCtx(), "后4") } func main() { s := g.Server() s.Use(ghttp.MiddlewareHandlerResponse) s.Group("/", func(group *ghttp.RouterGroup) { // 前置中间件 group.Middleware(RequestHandle1) group.Middleware(RequestHandle2) // 后置中间件 group.Middleware(ResponseHandle1) group.Middleware(ResponseHandle2) group.Group("/sub", func(group *ghttp.RouterGroup) { // 前置中间件 group.Middleware(RequestHandle3) group.Middleware(RequestHandle4) // 后置中间件 group.Middleware(ResponseHandle3) group.Middleware(ResponseHandle4) group.Bind(new(Hello)) }) }) s.Run() } ``` 执行结果如下: ## 执行中断 在 **分组路由中间件** 中,我们可以在 **前置中间件** 的 `Next()` 调用之前通过 `return` 对本次请求进行中断,在打断后, **后续** **所有** 的 **前置中间件**、 **同级及子级** 的 **后置中间件**、 **请求处理方法** 将不会被执行。 如上面用来展示分组路由中间件优先级的代码中: - 在 `RequestHandle1` 的 `Next()` 调用前进行中断,则只会执行 `RequestHandle1` - 在 `RequestHandle2` 的 `Next()` 调用前进行中断,则只会执行 `RequestHandle1`、 `RequestHandle2` - 在 `RequestHandle3` 的 `Next()` 调用前进行中断,则会执行 `RequestHandle1`、 `RequestHandle2`、 `RequestHandle3` 以及 `ResponseHandle2`、 `ResponseHandle1` - 在 `RequestHandle4` 的 `Next()` 调用前进行中断,则会执行 `RequestHandle1`、 `RequestHandle2`、 `RequestHandle3`、 `RequestHandle4` 以及 `ResponseHandle2`、 `ResponseHandle1` 上述的中断实例都不会执行 **请求处理方法**。 除了常见的return `在` 中间件中终端后续处理流程大方式外,框架还提供了 `Exit` 相关的方法来在代码执行处强制中断执行流程,具体可参考章节: [数据返回-Exit控制](./response-exit-exitall-exithook.md) # 中间件/拦截器-使用示例 ## 允许跨域请求 第一个例子,也是比较常见的功能需求。 我们需要在所有API请求之前增加允许跨域请求的返回 `Header` 信息,该功能可以通过中间件实现: ```go package main import ( "github.com/gogf/gf/v2/frame/g" "github.com/gogf/gf/v2/net/ghttp" ) func MiddlewareCORS(r *ghttp.Request) { r.Response.CORSDefault() r.Middleware.Next() } func main() { s := g.Server() s.Group("/api.v2", func(group *ghttp.RouterGroup) { group.Middleware(MiddlewareCORS) group.ALL("/user/list", func(r *ghttp.Request) { r.Response.Writeln("list") }) }) s.SetPort(8199) s.Run() } ``` 执行后,终端打印出路由表信息如下: ```text SERVER | DOMAIN | ADDRESS | METHOD | ROUTE | HANDLER | MIDDLEWARE |---------|---------|---------|--------|-------------------|-------------------|---------------------| default | default | :8199 | ALL | /api.v2/user/list | main.main.func1.1 | main.MiddlewareCORS |---------|---------|---------|--------|-------------------|-------------------|---------------------| ``` 这里我们使用 `group.Middleware(MiddlewareCORS)` 将跨域中间件通过分组路由的形式注册绑定到了 `/api.v2` 路由下所有的服务函数中。随后我们可以通过请求 [http://127.0.0.1:8199/api.v2/user/list](http://127.0.0.1:8199/api.v2/user/list) 来查看允许跨域请求的 `Header` 信息是否有返回。 ## 请求鉴权处理 我们在跨域请求中间件的基础之上加上鉴权中间件。 为了简化示例,在该示例中,当请求带有 `token` 参数,并且参数值为 `123456` 时可以通过鉴权,并且允许跨域请求,执行请求方法;否则返回 `403 Forbidden` 状态码。 ```go package main import ( "net/http" "github.com/gogf/gf/v2/frame/g" "github.com/gogf/gf/v2/net/ghttp" ) func MiddlewareAuth(r *ghttp.Request) { token := r.Get("token") if token.String() == "123456" { r.Response.Writeln("auth") r.Middleware.Next() } else { r.Response.WriteStatus(http.StatusForbidden) } } func MiddlewareCORS(r *ghttp.Request) { r.Response.Writeln("cors") r.Response.CORSDefault() r.Middleware.Next() } func main() { s := g.Server() s.Group("/api.v2", func(group *ghttp.RouterGroup) { group.Middleware(MiddlewareCORS, MiddlewareAuth) group.ALL("/user/list", func(r *ghttp.Request) { r.Response.Writeln("list") }) }) s.SetPort(8199) s.Run() } ``` 执行后,终端打印出路由表信息如下: ```text SERVER | DOMAIN | ADDRESS | METHOD | ROUTE | HANDLER | MIDDLEWARE |---------|---------|---------|--------|-------------------|-------------------|-----------------------------------------| default | default | :8199 | ALL | /api.v2/user/list | main.main.func1.1 | main.MiddlewareCORS,main.MiddlewareAuth |---------|---------|---------|--------|-------------------|-------------------|-----------------------------------------| ``` 可以看到,我们的服务方法绑定了两个中间件,跨域中间件和而鉴权中间件。 请求时将会按照中间件注册的先后顺序,先执行 `MiddlewareCORS` 全局中间件,再执行 `MiddlewareAuth` 分组中间件。 随后我们可以通过请求 [http://127.0.0.1:8199/api.v2/user/list](http://127.0.0.1:8199/api.v2/user/list) 和 [http://127.0.0.1:8199/api.v2/user/list?token=123456](http://127.0.0.1:8199/api.v2/user/list?token=123456) 对比来查看效果。 ## 鉴权例外处理 使用分组路由中间件可以很方便地添加鉴权例外,因为只有当前分组路由下注册的服务方法才会绑定并执行鉴权中间件,否则并不会执行到鉴权中间件。 ```go package main import ( "net/http" "github.com/gogf/gf/v2/frame/g" "github.com/gogf/gf/v2/net/ghttp" ) func MiddlewareAuth(r *ghttp.Request) { token := r.Get("token") if token.String() == "123456" { r.Middleware.Next() } else { r.Response.WriteStatus(http.StatusForbidden) } } func main() { s := g.Server() s.Group("/admin", func(group *ghttp.RouterGroup) { group.ALL("/login", func(r *ghttp.Request) { r.Response.Writeln("login") }) group.Group("/", func(group *ghttp.RouterGroup) { group.Middleware(MiddlewareAuth) group.ALL("/dashboard", func(r *ghttp.Request) { r.Response.Writeln("dashboard") }) }) }) s.SetPort(8199) s.Run() } ``` 执行后,终端打印出路由表信息如下: ```text SERVER | ADDRESS | DOMAIN | METHOD | P | ROUTE | HANDLER | MIDDLEWARE |---------|---------|---------|--------|---|------------------|---------------------|---------------------| default | :8199 | default | ALL | 2 | /admin/dashboard | main.main.func1.2.1 | main.MiddlewareAuth |---------|---------|---------|--------|---|------------------|---------------------|---------------------| default | :8199 | default | ALL | 2 | /admin/login | main.main.func1.1 | |---------|---------|---------|--------|---|------------------|---------------------|---------------------| ``` 可以看到,只有 `/admin/dashboard` 路由的服务方法绑定了鉴权中间件 `main.MiddlewareAuth`,而 `/admin/login` 路由的服务方法并没有添加鉴权处理。 随后我们访问以下URL查看效果: 1. [http://127.0.0.1:8199/admin/login](http://127.0.0.1:8199/admin/login) 2. [http://127.0.0.1:8199/admin/dashboard](http://127.0.0.1:8199/admin/dashboard) 3. [http://127.0.0.1:8199/admin/dashboard?token=123456](http://127.0.0.1:8199/admin/dashboard?token=123456) ## 统一的错误处理 基于中间件,我们可以在服务函数执行完成后做一些后置判断的工作,特别是统一数据格式返回、结果处理、错误判断等等。这种需求我们可以使用后置的中间件类型来实现。我们使用一个简单的例子,用来演示如何使用中间件对所有的接口请求做后置判断处理,作为一个抛砖引玉作用。 ```go package main import ( "net/http" "github.com/gogf/gf/v2/frame/g" "github.com/gogf/gf/v2/net/ghttp" ) func MiddlewareAuth(r *ghttp.Request) { token := r.Get("token") if token.String() == "123456" { r.Middleware.Next() } else { r.Response.WriteStatus(http.StatusForbidden) } } func MiddlewareCORS(r *ghttp.Request) { r.Response.CORSDefault() r.Middleware.Next() } func MiddlewareErrorHandler(r *ghttp.Request) { r.Middleware.Next() if r.Response.Status >= http.StatusInternalServerError { r.Response.ClearBuffer() r.Response.Writeln("哎哟我去,服务器居然开小差了,请稍后再试吧!") } } func main() { s := g.Server() s.Use(MiddlewareCORS) s.Group("/api.v2", func(group *ghttp.RouterGroup) { group.Middleware(MiddlewareAuth, MiddlewareErrorHandler) group.ALL("/user/list", func(r *ghttp.Request) { panic("db error: sql is xxxxxxx") }) }) s.SetPort(8199) s.Run() } ``` 执行后,终端打印出路由表信息如下: ```text SERVER | DOMAIN | ADDRESS | METHOD | ROUTE | HANDLER | MIDDLEWARE |---------|---------|---------|--------|-------------------|---------------------|-------------------------------------------------| default | default | :8199 | ALL | /* | main.MiddlewareCORS | GLOBAL MIDDLEWARE |---------|---------|---------|--------|-------------------|---------------------|-------------------------------------------------| default | default | :8199 | ALL | /api.v2/user/list | main.main.func1.1 | main.MiddlewareAuth,main.MiddlewareErrorHandler |---------|---------|---------|--------|-------------------|---------------------|-------------------------------------------------| ``` 在该示例中,我们在后置中间件中判断有无系统错误,如果有则返回固定的提示信息,而不是把敏感的报错信息展示给用户。当然,在真实的项目场景中,往往还有是需要解析返回缓冲区的数据,例如 `JSON` 数据,根据当前的执行结果进行封装返回固定的数据格式等等。 执行该示例后,访问 [http://127.0.0.1:8199/api.v2/user/list?token=123456](http://127.0.0.1:8199/api.v2/user/list?token=123456) 查看效果。 ## 自定义日志处理 我们来更进一步完善一下以上示例,我们将请求日志包括状态码输出到终端。这里我们必须得使用」全局中间件」了,这样可以拦截处理到所有的服务请求,甚至 `404` 请求。 ```go package main import ( "net/http" "github.com/gogf/gf/v2/frame/g" "github.com/gogf/gf/v2/net/ghttp" ) func MiddlewareAuth(r *ghttp.Request) { token := r.Get("token") if token.String() == "123456" { r.Middleware.Next() } else { r.Response.WriteStatus(http.StatusForbidden) } } func MiddlewareCORS(r *ghttp.Request) { r.Response.CORSDefault() r.Middleware.Next() } func MiddlewareLog(r *ghttp.Request) { r.Middleware.Next() errStr := "" if err := r.GetError(); err != nil { errStr = err.Error() } g.Log().Println(r.Response.Status, r.URL.Path, errStr) } func main() { s := g.Server() s.SetConfigWithMap(g.Map{ "AccessLogEnabled": false, "ErrorLogEnabled": false, }) s.Use(MiddlewareLog, MiddlewareCORS) s.Group("/api.v2", func(group *ghttp.RouterGroup) { group.Middleware(MiddlewareAuth) group.ALL("/user/list", func(r *ghttp.Request) { panic("啊!我出错了!") }) }) s.SetPort(8199) s.Run() } ``` 可以看到,我们注册了一个全局的日志处理中间件以及跨域中间件,而鉴权中间件是注册到 `/api.v2` 路由下。 执行后,我们可以通过请求 [http://127.0.0.1:8199/api.v2/user/list](http://127.0.0.1:8199/api.v2/user/list) 和 [http://127.0.0.1:8199/api.v2/user/list?token=123456](http://127.0.0.1:8199/api.v2/user/list?token=123456) 对比来查看效果,并查看终端的日志输出情况。 # 中间件/拦截器-常见问题 ## 如何通过中间件修改请求参数 在了解如何修改请求参数之前,请先了解一下请求参数输入的一些细节知识: [请求输入](./request.md) ### 自定义参数覆盖 修改请求参数最简单的方式,是通过 **自定义参数** 来覆盖客户端提交的参数。因为自定义参数在参数解析的优先级最高,因此在 **全量获取请求参数**(不是通过特定的提交方式获取参数)时,会使用自定义参数来覆盖掉其他提交方式的参数。这种修改方式比较常见,特别是使用规范路由时,接收参数时一个 `struct` 对象,底层是通过全量获取参数后再转换为 `struct` 对象。 但是如果用户是用过 `r.GetQuery, r.GetForm` 来获取参数时,自定义参数覆盖将会失效。而 `r.Get` 或者 `r.GetRequest` 方式获取参数,也可以实现参数覆盖的效果。 ### 修改特定请求方式的参数 也可以在中间件中通过修改 `r.URL.RawQuery` 或者 `r.Body` 来修改原始参数,但是在修改完毕后,需要调用 `r.ReloadParam()` 来标识下一次获取请求参数时将重新解析参数。 ## 通过中间件修改请求Body的注意事项 当通过中间件修改 `r.Body` 时常遇到的一个问题,是 `r.Body` 只允许读取一次,后续的读取都将读取不到数据,这是标准库 `http.Request` 的设计。在 `GoFrame` 框架的 `ghttp.Request` 对象中,允许通过框架 `ghttp.Request` 的 `GetBody`、 `GetBodyString` 方法连续读取 `Body` 内容。 但是,如果是在中间件中自行通过标准库 `http.Request` 对象直接读取 `r.Body`,那么建议通过重新赋值 `r.Body` 或者通过 `io.NopCloser` 重新包裹赋值给 `r.Body` 便于后续中间件或者流程进一步读取 `Body` 内容。 # 接口文档 ## 基本介绍 `GoFrame` 框架支持全自动化的接口文档生成,保障代码与接口文档同步维护,自动生成的接口文档采用的是标准的 `OpenAPIv3` 协议。在介绍 `OpenAPIv3` 协议之前,建议先阅读了解框架的规范路由特性: [路由注册-规范路由](./router-registering-strict-router.md)。 # 接口文档-OpenAPIv3 **提示** `OpenAPIv3` 协议主要使用在规范路由中,阅读接口文档协议介绍之前,请先了解一下规范路由: [路由注册-规范路由](./router-registering-strict-router.md) ## 一、 `OpenAPIv3` 详细的 `OpenAPIv3` 协议介绍请参考: [https://swagger.io/specification/](https://swagger.io/specification/) ## 二、 `g.Meta` 元数据 接口的元数据信息可以通过为输入结构体`embedded`方式嵌入`g.Meta`结构,并通过`g.Meta`的属性标签方式来实现。 关于元数据组件的介绍,详情请参考章节: [元数据-gmeta](../components/util-gmeta.md) ## 三、常用协议标签 输入输出结构体中的属性的标签完整支持 `OpenAPIv3` 协议,因此只要增加了对应的协议标签,那么生成的 `OpenAPIv3` 接口信息中将会自动包含该属性。 大部分的标签属性已经被 `Server` 组件自动生成,开发者需要手动设置的标签不多。 ### 1、基础标签 常见的基础标签包括: | 常见标签 | 说明 | 备注 | | --- | --- | --- | | `path` | 结合注册时的前缀共同构成接口URI路径 | 用于 `g.Meta` 标识接口元数据 | | `tags` | 接口所属的标签,用于接口分类 | 用于 `g.Meta` 标识接口元数据 | | `method` | 接口的请求方式: `ALL/GET/PUT/POST/DELETE...(不区分大小写)`。支持组合方式如`GET,POST`,多种方式使用`,`分隔。 | 用于 `g.Meta` 标识接口元数据 | | `deprecated` | 标记该接口废弃 | 用于 `g.Meta` 标识接口元数据 | | `summary` | 接口/参数概要描述 | 缩写 `sm` | | `description` | 接口/参数详细描述 | 缩写 `dc` | | `in` | 参数的提交方式 | `header/path/query/cookie` | | `default` | 参数的默认值 | 缩写 `d` | | `mime` | 接口的 `MIME` 类型,例如 `multipart/form-data` 一般是全局设置,默认为 `application/json`。 | 用于 `g.Meta` 标识接口元数据 | | `type` | 参数的类型,一般不需要设置,特殊参数需要手动设置,例如 `file` | 仅用于参数属性 | **提示** 更多标签请参考标准的 `OpenAPIv3` 协议: [https://swagger.io/specification/](https://swagger.io/specification/) ### 2、扩展标签 在 `OpenAPI` 规范里面,所有名称以 `x-` 开头的标签是开发者可自定义的扩展标签。扩展标签可以在任意的接口、属性中以 `Golang struct tag` 的形式定义,在接口文档生成时,将会作为独立的字段返回。例如: ```go package main import ( "context" "github.com/gogf/gf/v2/errors/gcode" "github.com/gogf/gf/v2/errors/gerror" "github.com/gogf/gf/v2/frame/g" "github.com/gogf/gf/v2/net/ghttp" ) type GetListReq struct { g.Meta `path:"/user" tags:"User" method:"get" x-group:"User/Info" summary:"Get user list with basic info."` Page int `dc:"Page number" d:"1" x-sort:"1"` Size int `dc:"Size for per page." d:"10" x-sort:"2"` } type GetListRes struct{} type Controller struct{} func (c *Controller) GetList(ctx context.Context, req *GetListReq) (res *GetListRes, err error) { return nil, gerror.NewCode(gcode.CodeNotImplemented) } func main() { s := g.Server() s.Group("/", func(group *ghttp.RouterGroup) { group.Bind(new(Controller)) }) s.SetOpenApiPath("/api.json") s.SetSwaggerPath("/swagger") s.SetPort(8199) s.Run() } ``` 执行后,访问地址 [http://127.0.0.1:8199/swagger](http://127.0.0.1:8199/swagger) 可以查看 `swagger ui`,访问 [http://127.0.0.1:8199/api.json](http://127.0.0.1:8199/api.json) 可以查看对应的 `OpenAPIv3` 接口文档。其中生成的 `OpenAPIv3` 接口文档如下: ``` { "openapi": "3.0.0", "components": { "schemas": { "main.GetListReq": { "properties": { "Page": { "default": 1, "description": "Page number", "format": "int", "properties": {}, "type": "integer", "x-sort": "1" }, "Size": { "default": 10, "description": "Size for per page.", "format": "int", "properties": {}, "type": "integer", "x-sort": "2" } }, "type": "object", "x-group": "User/Info" }, "main.GetListRes": { "properties": {}, "type": "object" } } }, "info": { "title": "", "version": "" }, "paths": { "/user": { "get": { "parameters": [ { "description": "Page number", "in": "query", "name": "Page", "schema": { "default": 1, "description": "Page number", "format": "int", "properties": {}, "type": "integer", "x-sort": "1" }, "x-sort": "1" }, { "description": "Size for per page.", "in": "query", "name": "Size", "schema": { "default": 10, "description": "Size for per page.", "format": "int", "properties": {}, "type": "integer", "x-sort": "2" }, "x-sort": "2" } ], "responses": { "200": { "content": { "application/json": { "schema": { "$ref": "#/components/schemas/main.GetListRes" } } }, "description": "" } }, "summary": "Get user list with basic info.", "tags": [ "User" ], "x-group": "User/Info" }, "x-group": "User/Info" } } } ``` 可以看到,扩展标签已经生成到了接口文档中。 ## 四、数据校验规则自动转换 `GoFrame`的`goai`组件能够自动将结构体字段的数据校验规则(`validation rules`)转换为`OpenAPIv3`中对应的校验数据结构,使接口文档更加准确和完整。这意味着只需在结构体字段中定义常规的数据校验规则,`goai`组件就会自动将其转换为`OpenAPIv3`规范中的对应约束。 ### 1. 支持的校验规则 以下是`goai`组件自动识别并转换的校验规则: | 校验规则 | 说明 | OpenAPIv3对应结构 | | --- | --- | --- | | `required` | 必填字段 | `required: true` | | `min:{数值}` | 最小值限制 | `minimum: {数值}` | | `max:{数值}` | 最大值限制 | `maximum: {数值}` | | `length:{最小长度},{最大长度}` | 字符串长度范围 | `minLength: {最小长度}, maxLength: {最大长度}` | | `min-length:{长度}` | 字符串最小长度 | `minLength: {长度}` | | `max-length:{长度}` | 字符串最大长度 | `maxLength: {长度}` | | `between:{最小值},{最大值}` | 数值范围 | `minimum: {最小值}, maximum: {最大值}` | | `in:{值1},{值2},...` | 枚举值限制 | `enum: [{值1}, {值2}, ...]` | ### 2. 使用示例 ```go type CreateUserReq struct { g.Meta `path:"/user" method:"post" summary:"创建用户"` Name string `v:"required|length:2,30" dc:"用户名"` Age int `v:"required|between:18,60" dc:"年龄"` Email string `v:"required|email" dc:"电子邮箱"` Score int `v:"min:0|max:100" dc:"分数"` Type string `v:"in:admin,user,guest" dc:"用户类型"` } ``` 生成的`OpenAPIv3`文档将包含以下校验信息: - `Name`字段:`required: true, minLength: 2, maxLength: 30` - `Age`字段:`required: true, minimum: 18, maximum: 60` - `Email`字段:`required: true` - `Score`字段:`minimum: 0, maximum: 100` - `Type`字段:`enum: ["admin", "user", "guest"]` ### 3. 注意事项 - 数值类型(如`int`、`float`等)的字段才会转换`min`、`max`和`between`规则为`minimum`和`maximum` - 字符串类型的字段会转换`min-length`、`max-length`和`length`规则为`minLength`和`maxLength` - `required`规则会转换为参数的`required: true`属性 - `in`规则会转换为`enum`数组,如果所有值都是数字,则会转换为数字类型的枚举,否则为字符串类型 ## 五、扩展 `OpenAPIv3` 信息 核心的接口信息已经自动生成,如果开发者想要更进一步完善接口信息,可以通过 `s.GetOpenApi()` 接口获取到 `OpenAPIv3` 的结构体对象,并手动填充对应的属性内容即可。我们来看一个示例,在该示例中,我们设计每个接口外层公共的数据结构: 我们可以发现通过通用的 `OpenAPIv3` 对象我们可以自定义修改其内容,并且根据它生成其他各种自定义类型的接口文档。 ## 五、添加`api.json(swagger)`自定义鉴权 对于需要进行`api`文档鉴权的情况,可以使用 `ghttp.BindHookHandler` 方法对 `s.GetOpenApiPath()` 路由绑定前置方法进行鉴权,示例如下: ``` go func main() { s := g.Server() // if api.json requires authentication, add openApiBasicAuth handler s.BindHookHandler(s.GetOpenApiPath(), ghttp.HookBeforeServe, openApiBasicAuth) s.Run() } func openApiBasicAuth(r *ghttp.Request) { if !r.BasicAuth("OpenApiAuthUserName", "OpenApiAuthPass", "Restricted") { r.ExitAll() return } } ``` 详细介绍请参考代码示例文档:[swagger-auth](../../examples/httpserver/swagger-auth.md) # 接口文档-扩展Response ## 一、概述 `RESTful API`设计中,不同的状态码表示不同的响应情况,例如 `200` 表示成功,`201` 表示创建成功,`400` 表示请求错误,`401` 表示未授权等。在`GoFrame`的`OpenAPIv3`接口文档中,默认情况下只会生成成功状态(通常是 `200`)的响应文档。 然而,在实际应用中,我们常常需要为一个`API`提供多种可能的响应状态,例如错误处理、权限验证失败等情况。为了满足这一需求,`GoFrame`在 `goai` 组件中提供了 `IEnhanceResponseStatus` 接口,开发者可以通过实现该接口来扩展响应结构体的信息,添加多种状态码的响应文档。 ## 二、接口定义 `IEnhanceResponseStatus` 接口的相关定义如下: ``` go type EnhancedStatusCode = int type EnhancedStatusType struct { Response any Examples any } type IEnhanceResponseStatus interface { EnhanceResponseStatus() map[EnhancedStatusCode]EnhancedStatusType } ``` ## 三、接口字段说明 - `EnhancedStatusCode`:表示 HTTP 状态码,例如 `200`、`201`、`400`、`401`、`403`、`404`、`500` 等。 - `EnhancedStatusType`:包含两个字段: - `Response`:响应结构体,可以是任意类型(`any`)。可以为它添加 `g.Meta` 标签来添加文档信息,如果设置了 `mime` 标签,该结构体会覆盖通用响应结构体的内容。 - `Examples`:响应示例,可以是任意类型(`any`)。可以使用错误码列表自动生成示例内容并显示在文档中,这样可以保证文档内容与实际业务内容的同步。 - `IEnhanceResponseStatus`:接口定义,包含一个方法 `EnhanceResponseStatus()`,该方法返回一个映射,将 HTTP 状态码映射到相应的 `EnhancedStatusType` 类型。 ## 四、使用方法 要使用 `IEnhanceResponseStatus` 接口,需要在响应结构体中实现 `EnhanceResponseStatus()` 方法。该方法返回一个映射,将 HTTP 状态码映射到相应的响应结构体和示例。 ### 1. 实现接口 在响应结构体中实现 `EnhanceResponseStatus()` 方法,返回不同状态码对应的响应结构体和示例。 ### 2. 设置响应状态码 可以在响应结构体的 `g.Meta` 标签中使用 `status` 属性来设置默认的响应状态码,例如 `g.Meta `status:"201"`。 ### 3. 使用示例数据 可以在 `EnhanceResponseStatus()` 方法中返回示例数据,这些数据将会显示在生成的 OpenAPIv3 文档中。 ## 五、完整示例 下面是一个完整的示例,演示如何使用 `IEnhanceResponseStatus` 接口来扩展响应结构体的信息: ``` go package main import ( "context" "github.com/gogf/gf/v2/errors/gcode" "github.com/gogf/gf/v2/errors/gerror" "github.com/gogf/gf/v2/frame/g" "github.com/gogf/gf/v2/net/ghttp" "github.com/gogf/gf/v2/net/goai" ) type StoreMessageReq struct { g.Meta `path:"/messages" method:"post" summary:"Store a message"` Content string `json:"content"` } type StoreMessageRes struct { g.Meta `status:"201"` Id string `json:"id"` } type EmptyRes struct { g.Meta `mime:"application/json"` } type CommonRes struct { Code int `json:"code"` Message string `json:"message"` Data interface{} `json:"data"` } var StoreMessageErr = map[int]gcode.Code{ 500: gcode.New(1, "Server Dead", nil), } func (r StoreMessageRes) EnhanceResponseStatus() (resList map[int]goai.EnhancedStatusType) { examples := []interface{}{} example500 := CommonRes{ Code: StoreMessageErr[500].Code(), Message: StoreMessageErr[500].Message(), Data: nil, } examples = append(examples, example500) return map[int]goai.EnhancedStatusType{ 403: { Response: EmptyRes{}, }, 500: { Response: struct{}{}, Examples: examples, }, } } type Controller struct{} func (c *Controller) StoreMessage(ctx context.Context, req *StoreMessageReq) (res *StoreMessageRes, err error) { return nil, gerror.NewCode(gcode.CodeNotImplemented) } func main() { s := g.Server() s.Group("/", func(group *ghttp.RouterGroup) { group.Bind(new(Controller)) }) oai := s.GetOpenApi() oai.Config.CommonResponse = CommonRes{} oai.Config.CommonResponseDataField = `Data` s.SetOpenApiPath("/api.json") s.SetSwaggerPath("/swagger") s.SetPort(8199) s.Run() } ``` 执行后,访问地址 [http://127.0.0.1:8199/swagger](http://127.0.0.1:8199/swagger) 可以查看 `swagger ui`,访问 [http://127.0.0.1:8199/api.json](http://127.0.0.1:8199/api.json) 可以查看对应的 `OpenAPIv3` 接口文档。其中生成的 `OpenAPIv3` 接口文档如下: ``` json { "openapi": "3.0.0", "components": { "schemas": { "main.StoreMessageReq": { "properties": { "content": { "format": "string", "type": "string" } }, "type": "object" }, "main.StoreMessageRes": { "properties": { "id": { "format": "string", "type": "string" } }, "type": "object" }, "interface": { "properties": {}, "type": "object" }, "main.EmptyRes": { "properties": {}, "type": "object" }, "struct": { "properties": {}, "type": "object" } } }, "info": { "title": "", "version": "" }, "paths": { "/messages": { "post": { "requestBody": { "content": { "application/json": { "schema": { "$ref": "#/components/schemas/main.StoreMessageReq" } } } }, "responses": { "201": { "content": { "application/json": { "schema": { "properties": { "code": { "format": "int", "type": "integer" }, "message": { "format": "string", "type": "string" }, "data": { "properties": { "id": { "format": "string", "type": "string" } }, "type": "object" } }, "type": "object" } } }, "description": "" }, "403": { "content": { "application/json": { "schema": { "$ref": "#/components/schemas/main.EmptyRes" } } }, "description": "" }, "500": { "content": { "application/json": { "schema": { "properties": { "code": { "format": "int", "type": "integer" }, "message": { "format": "string", "type": "string" }, "data": { "properties": {}, "type": "object" } }, "type": "object" }, "examples": { "example 1": { "value": { "code": 1, "message": "Server Dead", "data": null } } } } }, "description": "" } }, "summary": "Store a message" } } } } ``` 可以看到默认的响应状态码已经被更改为 `201`,并且响应示例也自动生成。 ## 六、常见状态码使用场景 在实际应用中,不同的 HTTP 状态码用于表示不同的响应情况。以下是一些常见的状态码及其使用场景: - `200 OK`:请求成功,返回请求的数据。 - `201 Created`:资源创建成功,通常用于 POST 请求。 - `204 No Content`:请求成功,但没有返回内容,通常用于 DELETE 请求。 - `400 Bad Request`:请求参数错误或格式不正确。 - `401 Unauthorized`:未授权,需要身份验证。 - `403 Forbidden`:已授权,但没有权限访问资源。 - `404 Not Found`:请求的资源不存在。 - `500 Internal Server Error`:服务器内部错误。 在实现 `IEnhanceResponseStatus` 接口时,可以根据实际业务需求,为这些不同的状态码提供相应的响应结构体和示例数据。 ## 七、使用建议 1. **结构体复用**:对于类似的响应结构,可以定义公共的结构体进行复用,减少代码重复。 2. **错误码管理**:将错误码集中管理,例如使用 `map` 或常量来定义不同状态码对应的错误码,便于维护和扩展。 3. **示例数据的真实性**:提供的示例数据应尽量真实,反映实际业务场景,便于前端开发者理解和处理。 4. **文档同步更新**:当业务逻辑或错误码变更时,及时更新接口文档,保持文档与代码的一致性。 ## 八、使用资源文件加载示例数据 在实际应用中,示例数据可能会比较大或复杂,直接在代码中编写可能会影响代码的可读性。`GoFrame`的`goai`组件提供了从外部文件加载示例数据的功能,支持从`gres`资源管理器或本地文件系统中读取示例数据,文件格式可以为`json/xml/yaml/ini`等`gjson`组件支持的格式。 ### 1. 使用`g.Meta`标签指定示例文件 可以在响应结构体的`g.Meta`标签中使用`resEg`属性指定示例文件的路径: ```go type MyResponse struct { g.Meta `status:"201" resEg:"testdata/examples/201.json"` // ...其他字段 } ``` 其中`testdata/examples/201.json`是示例文件的路径,可以是相对路径或绝对路径。 ### 2. 示例文件格式 示例文件可以是一个`JSON`对象或`JSON`数组: - **JSON对象**:每个键将作为示例的名称,值作为示例的内容。 ```json { "example1": { "code": 0, "message": "Success", "data": { "id": 1 } }, "example2": { "code": 1, "message": "Failed", "data": null } } ``` - **JSON数组**:数组中的每个元素将作为一个示例,示例名称会自动生成为`example 1`、`example 2`等。 ```json [ { "code": 0, "message": "Success", "data": { "id": 1 } }, { "code": 1, "message": "Failed", "data": null } ] ``` ### 3. 使用`gres`资源管理器 `GoFrame`的`goai`组件会首先尝试从`gres`资源管理器中读取指定路径的文件,如果找不到,则会尝试从本地文件系统中读取。这使得可以将示例数据打包到程序中,方便分发和部署。 要使用`gres`资源管理器,需要先将示例文件打包到资源文件中: ```go package main import ( "github.com/gogf/gf/v2/os/gres" "github.com/gogf/gf/v2/frame/g" ) func main() { // 打包资源文件 gres.Dump() // 初始化服务器 s := g.Server() // ...其他配置 s.Run() } ``` ### 4. 完整示例 下面是一个使用资源文件加载示例数据的完整示例: ```go package main import ( "context" "github.com/gogf/gf/v2/frame/g" "github.com/gogf/gf/v2/net/ghttp" ) type CreateUserReq struct { g.Meta `path:"/users" method:"post" summary:"创建用户"` Name string `json:"name" v:"required" dc:"用户名"` Email string `json:"email" v:"required|email" dc:"电子邮箱"` } type CreateUserRes struct { g.Meta `status:"201" resEg:"testdata/examples/user/create_success.json"` Id int `json:"id" dc:"用户ID"` } func (r CreateUserRes) EnhanceResponseStatus() map[int]goai.EnhancedStatusType { return map[int]goai.EnhancedStatusType{ 400: { Response: struct{}{}, // 从文件中加载错误示例 }, 500: { Response: struct{}{}, // 从文件中加载错误示例 }, } } type Controller struct{} func (c *Controller) CreateUser(ctx context.Context, req *CreateUserReq) (res *CreateUserRes, err error) { // 实际业务逻辑 return &CreateUserRes{Id: 1}, nil } func main() { s := g.Server() s.Group("/", func(group *ghttp.RouterGroup) { group.Bind(new(Controller)) }) s.SetOpenApiPath("/api.json") s.SetSwaggerPath("/swagger") s.Run() } ``` 其中`testdata/examples/user/create_success.json`文件内容可能是: ```json { "成功示例": { "code": 0, "message": "创建成功", "data": { "id": 1 } } } ``` 这种方式可以使示例数据与代码分离,更容易维护和管理。 ## 九、总结 `GoFrame`的 `IEnhanceResponseStatus` 接口提供了一种灵活、强大的方式来扩展 `OpenAPIv3`接口文档中的响应信息。通过实现该接口,开发者可以: 1. 为`API`提供多种响应状态的文档,使文档更符合`RESTful API`设计规范。 2. 自定义不同状态码的响应结构体,提供更精确的接口定义。 3. 通过示例数据展示不同状态的响应格式,便于前端开发者理解和处理。 4. 保持文档与代码的一致性,提高开发效率和协作质量。 通过合理使用 `IEnhanceResponseStatus` 接口,可以显著提升`API`文档的质量和完整性,为前后端开发者提供更好的开发体验。 # 接口文档-自定义UI ## 基本介绍 `GoFrame` 的 `Server` 默认自带的 `OpenAPI` 接口文档UI是 `redoc` 开源组件,该组件不支持页面 `Try It Out` 功能的。很多同学都在问,能否使用 `SwaggerUI` 页面来展示 `OpenAPI` 接口文档?有的企业内部并不支持连接外网的部分资源,那么能否将内部的接口文档UI替换为内部可访问的资源呢? 了解 `OpenAPI` 这个东东的小伙伴应该都知道, `OpenAPI` 只是通用的接口定义规范,而展示的接口文档UI是可以随便替换的, 并且这种`UI`界面以及平台还特别多。使用 `GoFrame Server` 来切换接口文档UI页面, 或者将接口文档对接到第三方接口文档平台非常简单。 ## 使用示例 咱们通过代码来展示一下,如何快速地将接口文档UI切换为 `SwaggerUI`。 `SwaggerUI` 的相关链接: - [https://github.com/swagger-api/swagger-ui](https://github.com/swagger-api/swagger-ui) - [https://github.com/swagger-api/swagger-ui/blob/master/docs/usage/installation.md](https://github.com/swagger-api/swagger-ui/blob/master/docs/usage/installation.md) main.go ```go package main import ( "context" "fmt" "github.com/gogf/gf/v2/frame/g" "github.com/gogf/gf/v2/net/ghttp" ) // HelloReq hello request type HelloReq struct { g.Meta `path:"/hello" method:"get" sort:"1"` Name string `v:"required" dc:"Your name"` } // HelloRes hello response type HelloRes struct { Reply string `dc:"Reply content"` } // Hello Controller type Hello struct{} // Say function func (Hello) Say(ctx context.Context, req *HelloReq) (res *HelloRes, err error) { g.Log().Debugf(ctx, `receive say: %+v`, req) res = &HelloRes{ Reply: fmt.Sprintf(`Hi %s`, req.Name), } return } const ( MySwaggerUITemplate = ` SwaggerUI
    ` ) func main() { s := g.Server() s.Use(ghttp.MiddlewareHandlerResponse) s.Group("/", func(group *ghttp.RouterGroup) { group.Bind( new(Hello), ) }) s.SetSwaggerUITemplate(MySwaggerUITemplate) s.Run() } ``` config.yaml ```yaml server: address: ":8199" openapiPath: "/api.json" swaggerPath: "/swagger" ``` 我们这里只定义了一个 `Hello` 的接口。可以看到,我们通过一个接口来展示 `SwaggerUI` 的 `HTML` 页面,并且将 `OpenAPI` 的接口文件路径定义为 `/api.json`,没有启用 `Server` 自带的UI页面。执行后,终端输出: ```html 2022-05-18 20:41:09.160 [INFO] openapi specification is serving at address: http://127.0.0.1:8199/api.json 2022-05-18 20:41:09.161 [INFO] pid[57888]: http server started listening on [:8199] ADDRESS | METHOD | ROUTE | HANDLER | MIDDLEWARE ----------|--------|-----------|-----------------------------------------------------------------|-------------------- :8199 | ALL | /* | github.com/gogf/gf/v2/net/ghttp.internalMiddlewareServerTracing | GLOBAL MIDDLEWARE ----------|--------|-----------|-----------------------------------------------------------------|-------------------- :8199 | ALL | /api.json | github.com/gogf/gf/v2/net/ghttp.(*Server).openapiSpec | ----------|--------|-----------|-----------------------------------------------------------------|-------------------- :8199 | GET | /hello | main.(*Hello).Say | ----------|--------|-----------|-----------------------------------------------------------------|-------------------- :8199 | GET | /swagger | main.main.func1.1 | ----------|--------|-----------|-----------------------------------------------------------------|-------------------- ``` 戳此链接访问: [http://127.0.0.1:8199/swagger/](http://127.0.0.1:8199/swagger/) 页面展示如下: 开发者自定义其他的接口文档UI类似如此。 ## 常见UI模板 ### swagger-ui ``` SwaggerUI
    ``` ### openapi-ui ``` openAPI UI
    ``` # 请求输入 请求输入依靠 `ghttp.Request` 对象实现, `ghttp.Request` 继承了底层的 `http.Request` 对象。 `ghttp.Request` 包含一个与当前请求对应的返回输出对象 `Response`,用于数据的返回处理。 相关方法: [https://pkg.go.dev/github.com/gogf/gf/v2/net/ghttp#Request](https://pkg.go.dev/github.com/gogf/gf/v2/net/ghttp#Request) ## 简要说明 可以看到 `Request` 对象的 **参数获取方法** 非常丰富,常用方法如下: | 常用方法 | 描述 | | --- | --- | | `Get` | 常用方法,简化参数获取, `GetRequest` 的别名。 | | `GetQuery` | 获取 `GET` 方式传递过来的参数,包括 `Query String` 及 `Body` 参数解析。 | | `GetForm` | 获取表单方式传递过来的参数,表单方式提交的参数 `Content-Type` 往往为 `application/x-www-form-urlencoded`, `application/form-data`, `multipart/form-data`, `multipart/mixed` 等等。 | | `GetRequest` | 获取客户端提交的所有参数,按照参数优先级进行覆盖,不区分提交方式。 | | `Get*Struct` | 将指定提交类型的所有请求参数绑定到指定的 `struct` 对象上,注意给定的参数为对象指针。绝大部分场景中往往使用 `Parse` 方法将请求数据转换为请求对象,具体详见后续章节。 | | `GetBody/GetBodyString` | 获取客户端提交的原始数据,该数据是客户端写入到 `body` 中的原始数据,与 `HTTP Method` 无关,例如客户端提交 `JSON/XML` 数据格式时可以通过该方法获取原始的提交数据。 | | `GetJson` | 自动将原始请求信息解析为 `gjson.Json` 对象指针返回, `gjson.Json` 对象具体在 [通用编解码-gjson](../components/encoding-gjson.md) 章节中介绍。 | | `Exit*` | 用于请求流程退出控制,详见本章后续说明。 | ## 提交方式 `GoFrame` 框架的参数获取不是通过 `HTTP Method` 来做区分,而是通过参数提交类型来区分。例如,分别通过 `HTTP Method: POST、INPUT、DELETE` 来提交表单参数,在服务端获取参数不是通过 `GetPost`/ `GetInput`/ `GetDelete` 的方式来获取,而是统一通过 `GetForm` 方法来获取表单参数,针对其他的 `HTTP Method` 也是如此。 在 `GoFrame` 框架下,有以下几种提交类型: | 提交类型 | 描述 | | --- | --- | | `Router` | **路由参数**。来源于路由规则匹配。 | | `Query` | **`Query` 参数。** `URL` 中的 `Query String` 参数解析,如: `http://127.0.0.1/index?id=1&name=john` 中的 `id=1&name=john`。 | | `Form` | **表单参数**。最常见的提交方式,提交的 `Content-Type` 往往为: `application/x-www-form-urlencoded`、 `multipart/form-data`、 `multipart/mixed`。 | | `Body` | **内容参数**。从 `Body` 中获取并解析得到的参数, `JSON`/ `XML` 请求往往使用这种方式提交。 | | `Custom` | **自定义参数**,往往在服务端的中间件、服务函数中通过 `SetParam/GetParam` 方法管理。 | ## 参数类型 获取的参数方法可以对指定键名的数据进行 **自动类型转换**,例如: `http://127.0.0.1:8199/?amount=19.66`,通过 `Get(xxx).String()` 将会返回 `19.66` 的字符串类型, `Get(xxx).Float32()`/ `Get(xxx).Float64()` 将会分别返回 `float32` 和 `float64` 类型的数值 `19.66`。但是, `Get(xxx).Int()`/ `Get(xxx).Uint()` 将会返回 `19`(如果参数为 `float` 类型的字符串,将会按照 **向下取整** 进行整型转换)。 **提示** 细心的读者一定发现了,获取到的参数都是 **泛型变量**,根据该泛型变量再根据需要调用对应的方法转换为对应的数据类型。 使用示例: ```go package main import ( "github.com/gogf/gf/v2/frame/g" "github.com/gogf/gf/v2/net/ghttp" ) func main() { s := g.Server() s.BindHandler("/", func(r *ghttp.Request) { r.Response.Writeln(r.Get("amount").String()) r.Response.Writeln(r.Get("amount").Int()) r.Response.Writeln(r.Get("amount").Float32()) }) s.SetPort(8199) s.Run() } ``` 执行后我们访问地址 [http://127.0.0.1:8199/?amount=19.66](http://127.0.0.1:8199/?amount=19.66) 页面输出 ```html 19.66 19 19.66 ``` ## 参数优先级 我们考虑一种场景,当不同的提交方式中存在同名的参数名称会怎么样?在 `GoFrame` 框架下,我们根据不同的获取方法,将会按照不同的优先级进行获取,优先级高的方式提交的参数将会优先覆盖其他方式的同名参数。优先级规则如下: 1. `Get` 及 `GetRequset` 方法: `Router < Query < Body < Form < Custom`,也就是说自定义参数的优先级最高,其次是 `Form` 表单参数,再次是 `Body` 提交参数,以此类推。例如, `Query` 和 `Form` 中都提交了同样名称的参数 `id`,参数值分别为 `1` 和 `2`,那么 `Get("id")`/ `GetForm("id")` 将会返回 `2`,而 `GetQuery("id")` 将会返回 `1`。 2. `GetQuery` 方法: `Query > Body`,也就是说 `query string` 的参数将会覆盖 `Body` 中提交的同名参数。例如, `Query` 和 `Body` 中都提交了同样名称的参数 `id`,参数值分别为 `1` 和 `2`,那么 `Get("id")` 将会返回 `2`,而 `GetQuery("id")` 将会返回 `1`。 3. `GetForm` 方法:由于该类型的方法仅用于获取 `Form` 表单参数,因此没什么优先级的差别。 使用示例: ```go package main import ( "github.com/gogf/gf/v2/frame/g" "github.com/gogf/gf/v2/net/ghttp" ) func main() { s := g.Server() s.BindHandler("/input", func(r *ghttp.Request) { r.Response.Writeln(r.Get("amount")) }) s.BindHandler("/query", func(r *ghttp.Request) { r.Response.Writeln(r.GetQuery("amount")) }) s.SetPort(8199) s.Run() } ``` 执行后,我们通过 `curl` 工具进行测试: ```bash $ curl -d "amount=1" -X POST "http://127.0.0.1:8199/input?amount=100" 1 $ curl -d "amount=1" -X POST "http://127.0.0.1:8199/query?amount=100" 100 ``` 可以看到,当我们访问 `/input` 路由时,该路由方法中采用了 `Get` 方法获取 `amount` 参数,按照同名优先级的规则返回了 `1`,即 `body` 中传递的参数。而当我们通过 `/query` 路由访问时,该路由方法内部使用了 `GetQuery` 方法获取 `amount` 参数,因此获取到的是 `query string` 参数中的 `amount` 值,返回了 `100`。 ## 大小写敏感 需要注意哦, **参数名称是大小写敏感** 的哦,例如客户端提交的参数 `name` 和 `Name` 是两个参数。 由于服务端默认是通过字符串名称获取参数,因此大小写敏感并不会产生什么问题,但是如果服务端接收的是一个API对象,那么可能需要注意一下。我们来看一个例子。 服务端: ```go package main import ( "context" "github.com/gogf/gf/v2/frame/g" "github.com/gogf/gf/v2/net/ghttp" ) type Controller struct{} type PathReq struct { g.Meta `path:"/api/v1/*path" method:"post"` Path string } type PathRes struct { Path string } func (c *Controller) Path(ctx context.Context, req *PathReq) (res *PathRes, err error) { return &PathRes{Path: req.Path}, nil } func main() { s := g.Server() s.SetPort(8199) s.Use(ghttp.MiddlewareHandlerResponse) s.Group("/", func(group *ghttp.RouterGroup) { group.Bind(&Controller{}) }) s.Run() } ``` 服务端的接口设计原意是定义一个路由参数 `path`,并且通过 `API` 对象的 `Path` 属性来接收。 客户端: ```go package main import ( "fmt" "github.com/gogf/gf/v2/frame/g" "github.com/gogf/gf/v2/os/gctx" ) func main() { var ( ctx = gctx.New() client = g.Client() ) client.SetPrefix("http://127.0.0.1:8199") for i := 0; i < 10; i++ { fmt.Println(client.PostContent(ctx, "/api/v1/user/info", `{"Path":"user/profile"}`)) } } ``` 根据我们的理解,客户端提交后,服务端应该会收到路由参数 `path` 的值为 `user/info`。但是很奇怪,由于程序 `BUG`,客户端提交的 `Body` 中同样提交了一个 `JSON`,参数名称为 `Path`,参数值为 `user/profile`。那么这个时候服务端 `API` 对象中的属性 `Path` 将会是哪个值呢? 我们将客户端反复执行多次,发现输出的结果都是: ```html {"code":0,"message":"","data":{"Path":"user/profile"}} {"code":0,"message":"","data":{"Path":"user/profile"}} {"code":0,"message":"","data":{"Path":"user/profile"}} {"code":0,"message":"","data":{"Path":"user/profile"}} {"code":0,"message":"","data":{"Path":"user/profile"}} {"code":0,"message":"","data":{"Path":"user/profile"}} {"code":0,"message":"","data":{"Path":"user/profile"}} {"code":0,"message":"","data":{"Path":"user/profile"}} {"code":0,"message":"","data":{"Path":"user/profile"}} {"code":0,"message":"","data":{"Path":"user/profile"}} ``` 即参数都是 `Body` 中提交的值,而不是路由参数中的值。其实, **由于参数大小写敏感的缘故**,服务端这个时候接受到了两个参数,一个是路由参数 `path`,一个是 `Body JSON` 提交的 `Path`。服务端在转换 `API` 参数对象的时候,由于属性 `Path` 和 `Body JSON` 中提交的 `Path` 更匹配,因此,使用的是 `Body JSON` 中的 `Path` 的值。 # 请求输入-参数处理 **提示** 本章节的复杂参数针对于传统的 `Query` 或者 `Form` 参数传递,由于这两种参数传递方式对复杂参数传递的管理维护不是很优雅,因为我们推荐大家遇到复杂参数传递的场景尽量使用 `JSON` 数据编码来管理维护。 ## 复杂参数 `ghttp.Request` 对象支持智能的参数类型解析(不区分请求提交方式及请求提交类型),以下为提交参数示例以及服务端对应解析的变量类型: | Parameter | Variable | | --- | --- | | `k=m&k=n` | `map[k:n]` | | `k1=m&k2=n` | `map[k1:m k2:n]` | | `k[]=m&k[]=n` | `map[k:[m n]]` | | `k[a][]=m&k[a][]=n` | `map[k:map[a:[m n]]]` | | `k[a]=m&k[b]=n` | `map[k:map[a:m b:n]]` | | `k[a][a]=m&k[a][b]=n` | `map[k:map[a:map[a:m b:n]]]` | | `k=m&k[a]=n` | `error` | ## 同名参数 同名参数提交格式形如: `k=v1&k=v2`,后续的变量值将会覆盖前面的变量值。 ```go package main import ( "github.com/gogf/gf/v2/frame/g" "github.com/gogf/gf/v2/net/ghttp" ) func main() { s := g.Server() s.BindHandler("/", func(r *ghttp.Request) { r.Response.Write(r.Get("name")) }) s.SetPort(8199) s.Run() } ``` 执行后,我们访问 [http://127.0.0.1:8199/?name=john&name=smith](http://127.0.0.1:8199/?name=john&name=smith) 后,将会得到返回值 `smith`。 **注意** 注意:框架的 `HTTP Server` 在这块的处理逻辑参考 `PHP`,会和 `Go` 标准库的处理逻辑有所差异。在 `Go` 标准库 `net/http` 处理中,提交的同名参数将会被转换为字符串数组。 ## 数组参数 数组参数提交格式形如: `k[]=v1&k[]=v2`,即以空的中括号 `[]` 来表示数组参数。 ```go package main import ( "github.com/gogf/gf/v2/frame/g" "github.com/gogf/gf/v2/net/ghttp" ) func main() { s := g.Server() s.BindHandler("/", func(r *ghttp.Request) { r.Response.Write(r.Get("array")) }) s.SetPort(8199) s.Run() } ``` 执行后,我们访问 [http://127.0.0.1:8199/?array\[\]=john&array\[\]=smith](http://127.0.0.1:8199/?array[]=john&array[]=smith) 后,将会得到返回值 `["john","smith"]`。 **注意** 注意:如果传递的参数带有中括号以及 **索引号**,那么该参数按照前面介绍的复杂参数转换规则将会被转换为 `map`。例如, `array[0]=john&array[1]=smith`,将会被转换为 `map{"0":"john","1":"smith"}`。 ## Map参数 `Map` 参数提交格式形如: `k[a]=m&k[b]=n`,并且支持多级 `Map`,例如: `k[a][a]=m&k[a][b]=n`。 ```go package main import ( "github.com/gogf/gf/v2/frame/g" "github.com/gogf/gf/v2/net/ghttp" ) func main() { s := g.Server() s.BindHandler("/", func(r *ghttp.Request) { r.Response.Write(r.Get("map")) }) s.SetPort(8199) s.Run() } ``` 执行后,我们访问 [http://127.0.0.1:8199/?map\[id\]=1&map\[name\]=john](http://127.0.0.1:8199/?map[id]=1&map[name]=john) 后,将会得到返回值 `{"id":"1","name":"john"}`。 我们再试试多级 `Map`,手动访问以下地址 [http://127.0.0.1:8199/?map\[user1\]\[id\]=1&map\[user1\]\[name\]=john&map\[user2\]\[id\]=2&map\[user2\]\[name\]=smith](http://127.0.0.1:8199/?map[user1][id]=1&map[user1][name]=john&map[user2][id]=2&map[user2][name]=smith) 将会得到返回值 `{"user1":{"id":"1","name":"john"},"user2":{"id":"2","name":"smith"}}`。 ## Header参数 通过`GetHeader`方法获取`HTTP`请求头信息。 **方法定义:** ```go func (r *Request) GetHeader(key string, def ...string) string ``` 该方法根据给定的键名 `key` 获取请求头的值。如果指定的请求头不存在,则返回可选的默认值 `def`。 **版本信息:** 默认值参数支持从 `v2.10.0` 版本开始提供。 **使用示例:** ```go package main import ( "github.com/gogf/gf/v2/frame/g" "github.com/gogf/gf/v2/net/ghttp" ) func main() { s := g.Server() s.BindHandler("/header", func(r *ghttp.Request) { // 获取请求头,如果不存在则返回空字符串 userAgent := r.GetHeader("User-Agent") // 获取请求头,如果不存在则返回默认值 customHeader := r.GetHeader("X-Custom-Header", "default-value") r.Response.Writef("User-Agent: %s\n", userAgent) r.Response.Writef("X-Custom-Header: %s\n", customHeader) }) s.SetPort(8199) s.Run() } ``` 执行后,我们通过 `curl` 工具进行测试: ```bash $ curl -H "X-Custom-Header: my-value" http://127.0.0.1:8199/header User-Agent: curl/7.68.0 X-Custom-Header: my-value $ curl http://127.0.0.1:8199/header User-Agent: curl/7.68.0 X-Custom-Header: default-value ``` 可以看到,当客户端没有传递 `X-Custom-Header` 请求头时,`GetHeader` 方法返回了我们设置的默认值 `default-value`。 # 请求输入-对象处理 ## 对象转换 对象转换在请求处理中非常常见。我们推荐将输入和输出定义为 `struct` 结构体对象,以便于结构化的参数输入输出维护。 `GoFrame` 框架支持非常便捷的对象转换,支持将客户端提交的参数如 `Query` 参数、表单参数、内容参数、 `JSON/XML` 等参数非常便捷地转换为指定的 `struct` 结构体,并且支持提交参数与 `struct` 属性的映射关系维护。 对象转换方法使用 `Request` 对象的 `Parse` 方法或者 `Get*Struct` 方法,具体方法定义请参考API文档: [https://pkg.go.dev/github.com/gogf/gf/v2/net/ghttp#Request](https://pkg.go.dev/github.com/gogf/gf/v2/net/ghttp#Request) ## 参数映射 ### 默认规则 客户端提交的参数如果需要映射到服务端定义的 `struct` 属性上,可以采用默认的映射关系,这一点非常方便。默认的转换规则如下: 1. `struct` 中需要匹配的属性必须为 **`公开属性`**(首字母大写)。 2. 参数名称会自动按照 **`不区分大小写`** 且 **忽略 `-/_/空格` 符号** 的形式与 `struct` 属性进行匹配。 3. 如果匹配成功,那么将键值赋值给属性,如果无法匹配,那么忽略该键值。 以下是几个匹配的示例: ```html map键名 struct属性 是否匹配 name Name match Email Email match nickname NickName match NICKNAME NickName match Nick-Name NickName match nick_name NickName match nick name NickName match NickName Nick_Name match Nick-name Nick_Name match nick_name Nick_Name match nick name Nick_Name match ``` 由于底层对象转换实现使用的是 `gconv` 模块,因此也支持 `c/gconv/json` 标签,更详细的规则可以参考 [类型转换-Struct转换](../core/gconv-struct.md) 章节。 ### 自定义规则 请在对象属性与参数名字差异较大的业务场景下使用自定义映射规则,否则请使用默认的参数映射规则。因为大量的自定义规则标签会增加代码的维护成本。 自定义的参数映射规则可以通过为 `struct` 属性绑定 `tag` 实现, `tag` 名称可以为 `p/param/params`。例如: ```go type User struct{ Id int Name string Pass1 string `p:"password1"` Pass2 string `p:"password2"` } ``` 其中我们使用了 `p` 标签来指定该属性绑定的参数名称。 `password1` 参数将会映射到 `Pass1` 属性, `password2` 将会映射到 `Pass2` 属性上。其他属性采用默认的转换规则即可,无需设置 `tag`。 ## `Parse` 转换 我们同时可以使用 `Parse` 方法来实现 `struct` 的转换,该方法是一个便捷方法,内部会自动进行转换及数据校验,但如果 `struct` 中没有校验 `tag` 的绑定将不会执行校验逻辑。 **注意** 从 `GoFrame v2` 版本开始,我们推荐使用结构化的方式来定义路由方法,更便捷地管理输入输出数据结构及其实例对象,具体请参考: [路由注册-规范路由](./router-registering-strict-router.md) 使用示例: ```go package main import ( "github.com/gogf/gf/v2/frame/g" "github.com/gogf/gf/v2/net/ghttp" ) type RegisterReq struct { Name string Pass string `p:"password1"` Pass2 string `p:"password2"` } type RegisterRes struct { Code int `json:"code"` Error string `json:"error"` Data interface{} `json:"data"` } func main() { s := g.Server() s.BindHandler("/register", func(r *ghttp.Request) { var req *RegisterReq if err := r.Parse(&req); err != nil { r.Response.WriteJsonExit(RegisterRes{ Code: 1, Error: err.Error(), }) } // ... r.Response.WriteJsonExit(RegisterRes{ Data: req, }) }) s.SetPort(8199) s.Run() } ``` 在该示例中,我们定义了两个结构体: `RegisterReq` 用于参数接收, `RegisterRes` 用于数据返回。 其中,我们使用 `r.Parse(&req)` 将客户端提交的参数转换为 `RegisterReq` 对象,当转换成功时 `req` 变量将会被初始化赋值(默认情况下为 `nil`),否则该方法返回 `err` 并且 `req` 变量为 `nil`。返回数据结构通过 `RegisterRes` 定义,并且返回格式为 `JSON`,通过 `r.Response.WriteJsonExit` 实现,该方法将 `RegisterRes` 根据其内部定义的 `json` 标签转换为 `JSON` 格式并退出当前服务方法,不再执行该服务方法的后续逻辑。 为了演示测试效果,这里在正常的返回结果 `Data` 属性中将 `RegisterReq` 对象返回,由于该对象没有绑定 `json` 标签,因此返回的 `JSON` 字段将会为其属性名称。 执行后,我们通过 `curl` 工具来测试一下: ```bash $ curl "http://127.0.0.1:8199/register?name=john&password1=123&password2=456" {"code":0,"error":"","data":{"Name":"john","Pass":"123","Pass2":"456"}} $ curl -d "name=john&password1=123&password2=456" -X POST "http://127.0.0.1:8199/register" {"code":0,"error":"","data":{"Name":"john","Pass":"123","Pass2":"456"}} ``` 我们使用了 `GET` 和 `POST` 两种提交方式来做测试,可以看到服务端都能完美地获取到提交参数并完成对象转换。 # 请求输入-请求校验 `Request` 对象支持非常完美的请求校验能力,通过给结构体属性绑定 `v` 标签即可。由于底层校验功能通过 `gvalid` 模块实现, 更详细的校验规则和介绍请参考 [数据校验-Struct校验](../core/gvalid-parameter-type-struct-example.md) 章节。 **注意** 以下示例代码中的请求参数转结构体方式适用于框架 `v1` 版本, `v2` 版本开始建议通过规范路由实现自动化的参数结构体转换和校验: [路由注册-规范路由](./router-registering-strict-router.md) ## 示例1,基本使用 我们将之前的示例做下调整,增加 `v` 校验标签。 ```go package main import ( "github.com/gogf/gf/v2/frame/g" "github.com/gogf/gf/v2/net/ghttp" ) // RegisterReq 注册请求数据结构 type RegisterReq struct { Name string `p:"username" v:"required|length:4,30#请输入账号|账号长度为:{min}到:{max}位"` Pass string `p:"password1" v:"required|length:6,30#请输入密码|密码长度不够"` Pass2 string `p:"password2" v:"required|length:6,30|same:password1#请确认密码|密码长度不够|两次密码不一致"` } // RegisterRes 注册返回数据结构 type RegisterRes struct { Code int `json:"code"` Error string `json:"error"` Data interface{} `json:"data"` } func main() { s := g.Server() s.Group("/", func(group *ghttp.RouterGroup) { group.ALL("/register", func(r *ghttp.Request) { var req *RegisterReq if err := r.Parse(&req); err != nil { r.Response.WriteJsonExit(RegisterRes{ Code: 1, Error: err.Error(), }) } // ... r.Response.WriteJsonExit(RegisterRes{ Data: req, }) }) }) s.SetPort(8199) s.Run() } ``` 在该示例中,我们定义了两个结构体: `RegisterReq` 用于参数接收, `RegisterRes` 用于数据返回。由于该接口返回的是 `JSON` 数据结构,可以看到,只有返回的结构体中存在 `json` 标签,而接收的结构体中只有 `p` 标签。因为 `RegisterReq` 仅用于参数接收,无需设置返回的 `json` 标签。 **提示** `p` 标签是可选的,默认情况下会通过 **忽略特殊字符+不区分大小写** 的规则进行属性名称匹配转换,默认匹配规则满足绝大部分业务场景。 为了演示测试效果,这里在正常的返回结果 `Data` 属性中将 `RegisterReq` 对象返回,由于该对象没有绑定 `json` 标签,因此返回的 `JSON` 字段将会为其属性名称。 执行后,我们通过 `curl` 工具来测试一下: ```bash $ curl "http://127.0.0.1:8199/register?name=john&password1=123456&password2=123456" {"code":0,"error":"","data":{"Name":"john","Pass":"123456","Pass2":"123456"}} $ curl "http://127.0.0.1:8199/register?name=john&password1=123456&password2=12345" {"code":1,"error":"密码长度不够","data":null} $ curl "http://127.0.0.1:8199/register" {"code":1,"error":"请输入账号","data":null} ``` ## 示例2,校验错误处理 **提示** 最新版已经改成只返回第一个错误。 可以看到在以上示例中,当请求校验错误时,所有校验失败的错误都返回了,这样对于用户体验不是特别友好。当产生错误时,我们可以将校验错误转换为 `gvalid.Error` 接口对象,随后可以通过灵活的方法控制错误的返回。 ```go package main import ( "github.com/gogf/gf/v2/frame/g" "github.com/gogf/gf/v2/net/ghttp" "github.com/gogf/gf/v2/util/gvalid" ) type RegisterReq struct { Name string `p:"username" v:"required|length:4,30#请输入账号|账号长度为:{min}到:{max}位"` Pass string `p:"password1" v:"required|length:6,30#请输入密码|密码长度不够"` Pass2 string `p:"password2" v:"required|length:6,30|same:password1#请确认密码|密码长度不够|两次密码不一致"` } type RegisterRes struct { Code int `json:"code"` Error string `json:"error"` Data interface{} `json:"data"` } func main() { s := g.Server() s.Group("/", func(group *ghttp.RouterGroup) { group.ALL("/register", func(r *ghttp.Request) { var req *RegisterReq if err := r.Parse(&req); err != nil { // Validation error. if v, ok := err.(gvalid.Error); ok { r.Response.WriteJsonExit(RegisterRes{ Code: 1, Error: v.FirstError().Error(), }) } // Other error. r.Response.WriteJsonExit(RegisterRes{ Code: 1, Error: err.Error(), }) } // ... r.Response.WriteJsonExit(RegisterRes{ Data: req, }) }) }) s.SetPort(8199) s.Run() } ``` 可以看到,当错误产生后,我们可以通过 `err.(gvalid.Error)` 断言的方式判断错误是否为校验错误,如果是的话则返回第一条校验错误,而不是所有都返回。更详细的错误控制方法,请参考 [数据校验-校验结果](../core/gvalid-result-handling.md) 章节。 **提示** 此外,我们这里也可以使用 `gerror.Current` 获取第一条报错信息,而不是使用断言判断。例如: ```go var req *RegisterReq if err := r.Parse(&req); err != nil { r.Response.WriteJsonExit(RegisterRes{ Code: 1, Error: gerror.Current(err).Error(), }) } ``` 执行后,我们通过 `curl` 工具来测试一下: ```bash $ curl "http://127.0.0.1:8199/register" {"code":1,"error":"请输入账号","data":null} $ curl "http://127.0.0.1:8199/register?name=john&password1=123456&password2=1234567" {"code":1,"error":"两次密码不一致","data":null} ``` # 请求输入-JSON/XML 从 `GoFrame v1.11` 版本开始, `Request` 对象提供了对客户端提交的 `JSON/XML` 数据格式的原生支持,为开发者提供了更便捷的数据获取特性,以进一步提高开发效率。 ## 示例1,简单示例 ```go package main import ( "github.com/gogf/gf/v2/frame/g" "github.com/gogf/gf/v2/net/ghttp" ) func main() { s := g.Server() s.BindHandler("/", func(r *ghttp.Request) { r.Response.Writef("name: %v, pass: %v", r.Get("name"), r.Get("pass")) }) s.SetPort(8199) s.Run() } ``` 执行后,我们通过 `curl` 工具提交数据来测试一下: 1. `Query` 数据格式 ```bash $ curl "http://127.0.0.1:8199/?name=john&pass=123" name: john, pass: 123 ``` 2. `Form` 表单提交 ```bash $ curl -d "name=john&pass=123" "http://127.0.0.1:8199/" name: john, pass: 123 ``` 3. `JSON` 数据格式 ```bash $ curl -d '{"name":"john","pass":"123"}' "http://127.0.0.1:8199/" name: john, pass: 123 ``` 4. `XML` 数据格式 ```bash $ curl -d 'john123' "http://127.0.0.1:8199/" name: john, pass: 123 $ curl -d 'john123' "http://127.0.0.1:8199/" name: john, pass: 123 ``` ## 示例2,对象转换及校验 ```go package main import ( "github.com/gogf/gf/v2/frame/g" "github.com/gogf/gf/v2/net/ghttp" "github.com/gogf/gf/v2/util/gvalid" ) type RegisterReq struct { Name string `p:"username" v:"required|length:6,30#请输入账号|账号长度为:{min}到:{max}位"` Pass string `p:"password1" v:"required|length:6,30#请输入密码|密码长度不够"` Pass2 string `p:"password2" v:"required|length:6,30|same:password1#请确认密码|密码长度不够|两次密码不一致"` } type RegisterRes struct { Code int `json:"code"` Error string `json:"error"` Data interface{} `json:"data"` } func main() { s := g.Server() s.BindHandler("/register", func(r *ghttp.Request) { var req *RegisterReq if err := r.Parse(&req); err != nil { // Validation error. if v, ok := err.(gvalid.Error); ok { r.Response.WriteJsonExit(RegisterRes{ Code: 1, Error: v.FirstString(), }) } // Other error. r.Response.WriteJsonExit(RegisterRes{ Code: 1, Error: err.Error(), }) } // ... r.Response.WriteJsonExit(RegisterRes{ Data: req, }) }) s.SetPort(8199) s.Run() } ``` 执行后,我们通过 `curl` 工具提交数据来测试一下: 1. `JSON` 数据格式 ```bash $ curl -d '{"username":"johngcn","password1":"123456","password2":"123456"}' "http://127.0.0.1:8199/register" {"code":0,"error":"","data":{"Name":"johngcn","Pass":"123456","Pass2":"123456"}} $ curl -d '{"username":"johngcn","password1":"123456","password2":"1234567"}' "http://127.0.0.1:8199/register" {"code":1,"error":"两次密码不一致","data":null} ``` 可以看到,我们提交的 `JSON` 内容也被 `Parse` 方法智能地转换为了结构体对象。 2. `XML` 数据格式 ```bash $ curl -d 'johngcn123456123456' "http://127.0.0.1:8199/register" {"code":0,"error":"","data":{"Name":"johngcn","Pass":"123456","Pass2":"123456"}} $ curl -d 'johngcn1234561234567' "http://127.0.0.1:8199/register" {"code":1,"error":"两次密码不一致","data":null} ``` 可以看到,我们提交的 `XML` 内容也被 `Parse` 方法智能地转换为了结构体对象。 # 请求输入-默认值绑定 从 `v1.15` 版本开始, `Request` 请求对象支持通过 `struct tag` 的方式为输入对象的属性绑定默认值。默认值的 `struct tag` 名称为 `d`(也可以使用 `default`)。 我们来看一个示例以便更好理解。 ## 参数对象定义 ```go type GetListReq struct { g.Meta `path:"/" method:"get"` Type string `v:"required#请选择内容模型" dc:"内容模型"` Page int `v:"min:0#分页号码错误" dc:"分页号码" d:"1"` Size int `v:"max:50#分页数量最大50条" dc:"分页数量,最大50" d:"10"` Sort int `v:"in:0,1,2#排序类型不合法" dc:"排序类型(0:最新, 默认。1:活跃, 2:热度)"` } type GetListRes struct { Items []Item `dc:"内容列表"` } type Item struct { Id int64 `dc:"内容ID"` Title string `dc:"内容标题"` } ``` 这个是一个查询内容列表请求的参数接受对象,其中我们通过 `d` 的标签为属性 `Page` 和 `Size` 指定了默认值,当这两个参数不传递时,默认为 `1` 和 `10`,表示分页从第 `1` 页开始,每页查询数量为 `10`。 ## 参数对象使用 ```go package main import ( "context" "github.com/gogf/gf/v2/frame/g" "github.com/gogf/gf/v2/net/ghttp" ) type GetListReq struct { g.Meta `path:"/" method:"get"` Type string `v:"required#请选择内容模型" dc:"内容模型"` Page int `v:"min:0#分页号码错误" dc:"分页号码" d:"1"` Size int `v:"max:50#分页数量最大50条" dc:"分页数量,最大50" d:"10"` Sort int `v:"in:0,1,2#排序类型不合法" dc:"排序类型(0:最新, 默认。1:活跃, 2:热度)"` } type GetListRes struct { Items []Item `dc:"内容列表"` } type Item struct { Id int64 `dc:"内容ID"` Title string `dc:"内容标题"` } type Controller struct{} func (Controller) GetList(ctx context.Context, req *GetListReq) (res *GetListRes, err error) { g.Log().Info(ctx, req) return } func main() { s := g.Server() s.Group("/content", func(group *ghttp.RouterGroup) { group.Middleware(ghttp.MiddlewareHandlerResponse) group.Bind(&Controller{}) }) s.SetPort(8199) s.Run() } ``` 我们访问以下地址,并查看服务端终端输出结果: [http://127.0.0.1:8199/content?type=ask](http://127.0.0.1:8199/content?type=ask) ```html 2023-03-21 21:58:23.058 [INFO] {2883f9c2dc734e170a35c73ea3560b4b} {"Type":"ask","Page":1,"Size":10,"Sort":0} ``` [http://127.0.0.1:8199/content?type=ask&page=](http://127.0.0.1:8199/content?type=ask&page=) ```html 2023-03-21 21:58:32.555 [INFO] {b86e22f9de734e170b35c73edf07859d} {"Type":"ask","Page":1,"Size":10,"Sort":0} ``` [http://127.0.0.1:8199/content?type=ask&page=2](http://127.0.0.1:8199/content?type=ask&page=2) ```html 2023-03-21 22:01:02.907 [INFO] {a016c8fa01744e170f35c73e99082f53} {"Type":"ask","Page":2,"Size":10,"Sort":0} ``` 可以看到,当调用端不传递或者传递空的 `page` 参数时,服务端都将使用定义的默认值;而当调用端传递具体的 `page` 参数时,默认值并不会生效。 ## 注意事项 默认值参数绑定是根据客户端 **未提交该参数** 来识别是否启用默认值,如果客户端已经提交了该参数, **即便该参数值是空字符串** 也会被当做客户端已经传递了具体的值,那么服务端数据结构上的默认值标签将不会生效。 # 请求输入-自定义参数 开发者可以在请求中自定义一些变量设置,自定义变量的获取优先级是最高的,可以覆盖原有的客户端提交参数。 **提示** 自定义变量往往也可以做请求流程的变量共享,但是需要注意的是该变量会成为请求参数的一部分,是对业务执行流程公开的变量。 方法列表: ```go func (r *Request) SetParam(key string, value interface{}) func (r *Request) GetParam(key string, def ...interface{}) interface{} func (r *Request) GetParamVar(key string, def ...interface{}) *gvar.Var ``` 自定义变量可以通过 `SetParam` 方法进行设置。自定义变量的获取可以通过请求参数的获取方法获得到,例如: `Get/GetVar/GetMap` 等等,也可以通过特定的自定义变量方法获取到 `GetParam/GetParamVar`。 使用示例: ```go package main import ( "github.com/gogf/gf/v2/frame/g" "github.com/gogf/gf/v2/net/ghttp" ) // 前置中间件1 func MiddlewareBefore1(r *ghttp.Request) { r.SetParam("name", "GoFrame") r.Response.Writeln("set name") r.Middleware.Next() } // 前置中间件2 func MiddlewareBefore2(r *ghttp.Request) { r.SetParam("site", "https://goframe.org") r.Response.Writeln("set site") r.Middleware.Next() } func main() { s := g.Server() s.Group("/", func(group *ghttp.RouterGroup) { group.Middleware(MiddlewareBefore1, MiddlewareBefore2) group.ALL("/", func(r *ghttp.Request) { r.Response.Writefln( "%s: %s", r.GetParam("name").String(), r.GetParam("site").String(), ) }) }) s.SetPort(8199) s.Run() } ``` 可以看到,我们可以通过 `SetParam` 和 `GetParam` 来设置和获取自定义的变量,该变量生命周期仅限于当前请求流程。 执行后,访问 [http://127.0.0.1:8199/](http://127.0.0.1:8199/),页面输出内容为: ``` set name set site GoFrame: https://goframe.org ``` # 请求输入-Context ## 基本介绍 请求流程往往会在上下文中共享一些自定义设置的变量,例如在请求开始之前通过中间件设置一些变量,随后在路由服务方法中可以获取该变量并相应对一些处理。这种需求非常常见。在 `GoFrame` 框架中,我们推荐使用 `Context` 上下文对象来处理流程共享的上下文变量,甚至将该对象进一步传递到依赖的各个模块方法中。该 `Context` 对象类型实现了标准库的 `context.Context` 接口,该接口往往会作为模块间调用方法的第一个参数,该接口参数也是 `Golang` 官方推荐的在模块间传递上下文变量的推荐方式。 **方法列表:** ```go func (r *Request) GetCtx() context.Context func (r *Request) SetCtx(ctx context.Context) func (r *Request) GetCtxVar(key interface{}, def ...interface{}) *gvar.Var func (r *Request) SetCtxVar(key interface{}, value interface{}) ``` **简要说明:** 1. `GetCtx` 方法用于获取当前的 `context.Context` 对象,作用同 `Context` 方法。 2. `SetCtx` 方法用于设置自定义的 `context.Context` 上下文对象。 3. `GetCtxVar` 方法用于获取上下文变量,并可给定当该变量不存在时的默认值。 4. `SetCtxVar` 方法用于设置上下文变量。 ## 使用示例 ### 示例1, `SetCtxVar/GetCtxVar` ```go package main import ( "github.com/gogf/gf/v2/frame/g" "github.com/gogf/gf/v2/net/ghttp" ) const ( TraceIdName = "trace-id" ) func main() { s := g.Server() s.Group("/", func(group *ghttp.RouterGroup) { group.Middleware(func(r *ghttp.Request) { r.SetCtxVar(TraceIdName, "HBm876TFCde435Tgf") r.Middleware.Next() }) group.ALL("/", func(r *ghttp.Request) { r.Response.Write(r.GetCtxVar(TraceIdName)) }) }) s.SetPort(8199) s.Run() } ``` 可以看到,我们可以通过 `SetCtxVar` 和 `GetCtxVar` 来设置和获取自定义的变量,该变量生命周期仅限于当前请求流程。 执行后,访问 [http://127.0.0.1:8199/](http://127.0.0.1:8199/),页面输出内容为: ```text HBm876TFCde435Tgf ``` ### 示例2, `SetCtx` `SetCtx` 方法常用于中间件中整合一些第三方的组件,例如第三方的链路跟踪组件等等。 为简化示例,这里我们将上面的例子通过 `SetCtx` 方法来改造一下来做演示。 ```go package main import ( "context" "github.com/gogf/gf/v2/frame/g" "github.com/gogf/gf/v2/net/ghttp" ) const ( TraceIdName = "trace-id" ) func main() { s := g.Server() s.Group("/", func(group *ghttp.RouterGroup) { group.Middleware(func(r *ghttp.Request) { ctx := context.WithValue(r.Context(), TraceIdName, "HBm876TFCde435Tgf") r.SetCtx(ctx) r.Middleware.Next() }) group.ALL("/", func(r *ghttp.Request) { r.Response.Write(r.Context().Value(TraceIdName)) // 也可以使用 // r.Response.Write(r.GetCtxVar(TraceIdName)) }) }) s.SetPort(8199) s.Run() } ``` 执行后,访问 [http://127.0.0.1:8199/](http://127.0.0.1:8199/),页面输出内容为: ```text HBm876TFCde435Tgf ``` # 请求输入-文件上传 文件上传请具体参考 [HTTPClient-文件上传](./http-client-file-uploading.md) 章节。 # 数据返回 ## 基本介绍 `HTTP Server` 的数据返回通过 `ghttp.Response` 对象实现, `ghttp.Response` 对象实现了标准库的 `http.ResponseWriter` 接口。数据输出使用 `Write*` 相关方法实现,并且数据输出采用了 `Buffer` 机制,因此数据的处理效率比较高。任何时候可以通过 `OutputBuffer` 方法输出缓冲区数据到客户端,并清空缓冲区数据。 常用方法:更详细的接口列表请参考 [https://pkg.go.dev/github.com/gogf/gf/v2/net/ghttp#Response](https://pkg.go.dev/github.com/gogf/gf/v2/net/ghttp#Response) ```go func (r *Response) Write(content ...interface{}) func (r *Response) WriteExit(content ...interface{}) func (r *Response) WriteJson(content interface{}) error func (r *Response) WriteJsonExit(content interface{}) error func (r *Response) WriteJsonP(content interface{}) error func (r *Response) WriteJsonPExit(content interface{}) error func (r *Response) WriteOver(content ...interface{}) func (r *Response) WriteOverExit(content ...interface{}) func (r *Response) WriteStatus(status int, content ...interface{}) func (r *Response) WriteStatusExit(status int, content ...interface{}) func (r *Response) WriteTpl(tpl string, params ...gview.Params) error func (r *Response) WriteTplContent(content string, params ...gview.Params) error func (r *Response) WriteTplDefault(params ...gview.Params) error func (r *Response) WriteXml(content interface{}, rootTag ...string) error func (r *Response) WriteXmlExit(content interface{}, rootTag ...string) error func (r *Response) Writef(format string, params ...interface{}) func (r *Response) WritefExit(format string, params ...interface{}) func (r *Response) Writefln(format string, params ...interface{}) func (r *Response) WriteflnExit(format string, params ...interface{}) func (r *Response) Writeln(content ...interface{}) func (r *Response) WritelnExit(content ...interface{}) ``` 简要说明: 1. `Write*` 方法用于往返回的数据缓冲区追加写入数据,参数可为任意的数据格式,内部通过断言对参数做自动分析。 2. `Write*Exit` 方法用于往返回的数据缓冲区追加写入数据后退出当前执行的 `HTTP Handler` 方法,可用于替代 `return` 返回方法。 3. `WriteOver*` 方法用于覆盖缓冲区写入,原有缓冲区的数据将会被覆盖为新写入的数据。 4. `WriteStatus*` 方法用于设置当前请求执行返回的状态码。 5. `WriteJson*`/ `WriteXml` 方法用于特定数据格式的输出,这是为开发者提供的简便方法。 6. `WriteTpl*` 方法用于模板输出,解析并输出模板文件,也可以直接解析并输出给定的模板内容。 7. 其他方法详见接口文档; 此外,需要提一下, `Header` 的操作可以通过标准库的方法来实现,例如: ``` Response.Header().Set("Content-Type", "text/plain; charset=utf-8") ``` # 数据返回-缓冲控制 ## 基本介绍 `Response` 输出采用了缓冲控制,输出的内容预先写入到一块缓冲区,等待服务方法执行完毕后才真正地输出到客户端。该特性在提高执行效率同时为输出内容的控制提供了更高的灵活性。 相关方法: ```go func (r *Response) Buffer() []byte func (r *Response) BufferLength() int func (r *Response) BufferString() string func (r *Response) Flush() func (r *Response) SetBuffer(data []byte) ``` ## 使用示例 我们通过后置中间件统一对返回的数据做处理,如果服务方法产生了异常,那么不能将敏感错误信息推送到客户端,而统一设置错误提示信息。 ```go package main import ( "github.com/gogf/gf/v2/frame/g" "github.com/gogf/gf/v2/net/ghttp" "net/http" ) func MiddlewareErrorHandler(r *ghttp.Request) { r.Middleware.Next() if r.Response.Status >= http.StatusInternalServerError { r.Response.ClearBuffer() r.Response.Writeln("服务器居然开小差了,请稍后再试吧!") } } func main() { s := g.Server() s.Group("/api.v2", func(group *ghttp.RouterGroup) { group.Middleware(MiddlewareErrorHandler) group.ALL("/user/list", func(r *ghttp.Request) { panic("db error: sql is xxxxxxx") }) }) s.SetPort(8199) s.Run() } ``` 访问 [http://127.0.0.1:8199/api.v2/user/list](http://127.0.0.1:8199/api.v2/user/list) 可以看到,页面输出了: ```text 服务器居然开小差了,请稍后再试吧! ``` # 数据返回-JSON/XML 相关方法: ```go func (r *Response) WriteJson(content interface{}) error func (r *Response) WriteJsonExit(content interface{}) error func (r *Response) WriteJsonP(content interface{}) error func (r *Response) WriteJsonPExit(content interface{}) error func (r *Response) WriteXml(content interface{}, rootTag ...string) error func (r *Response) WriteXmlExit(content interface{}, rootTag ...string) error ``` `Response` 提供了对 `JSON/XML` 数据格式输出的原生支持,通过以下方法实现: 1. `WriteJson*` 方法用于返回 `JSON` 数据格式,参数为任意类型,可以为 `string`、 `map`、 `struct` 等等。返回的 `Content-Type` 为 `application/json`。 2. `WriteXml*` 方法用于返回 `XML` 数据格式,参数为任意类型,可以为 `string`、 `map`、 `struct` 等等。返回的 `Content-Type` 为 `application/xml`。 **提示** 对 `JSON` 数据格式支持的同时,同时也支持 `JSONP` 协议。 ## `JSON` ```go package main import ( "github.com/gogf/gf/v2/frame/g" "github.com/gogf/gf/v2/net/ghttp" ) func main() { s := g.Server() s.Group("/", func(group *ghttp.RouterGroup) { group.ALL("/json", func(r *ghttp.Request) { r.Response.WriteJson(g.Map{ "id": 1, "name": "john", }) }) }) s.SetPort(8199) s.Run() } ``` 执行后,我们通过 `curl` 工具测试下: ```bash $ curl -i http://127.0.0.1:8199/json HTTP/1.1 200 OK Content-Type: application/json Server: GoFrame HTTP Server Date: Sun, 05 Jan 2020 02:49:31 GMT Content-Length: 22 {"id":1,"name":"john"} ``` ## `JSONP` 需要注意使用 `JSONP` 协议时必须通过 `Query` 方式提供 `callback` 参数。 ```go package main import ( "github.com/gogf/gf/v2/frame/g" "github.com/gogf/gf/v2/net/ghttp" ) func main() { s := g.Server() s.Group("/", func(group *ghttp.RouterGroup) { group.ALL("/jsonp", func(r *ghttp.Request) { r.Response.WriteJsonP(g.Map{ "id": 1, "name": "john", }) }) }) s.SetPort(8199) s.Run() } ``` 执行后,我们通过 `curl` 工具测试下: ```bash $ curl -i "http://127.0.0.1:8199/jsonp?callback=MyCallback" HTTP/1.1 200 OK Server: GoFrame HTTP Server Date: Sun, 05 Jan 2020 02:50:42 GMT Content-Length: 34 Content-Type: text/plain; charset=utf-8 MyCallback({"id":1,"name":"john"}) ``` ## `XML` ```go package main import ( "github.com/gogf/gf/v2/frame/g" "github.com/gogf/gf/v2/net/ghttp" ) func main() { s := g.Server() s.Group("/", func(group *ghttp.RouterGroup) { group.ALL("/xml", func(r *ghttp.Request) { r.Response.Write(``) r.Response.WriteXml(g.Map{ "id": 1, "name": "john", }) }) }) s.SetPort(8199) s.Run() } ``` 执行后,我们通过 `curl` 工具测试下: ```bash $ curl -i http://127.0.0.1:8199/xml HTTP/1.1 200 OK Content-Type: application/xml Server: GoFrame HTTP Server Date: Sun, 05 Jan 2020 03:00:55 GMT Content-Length: 76 1john ``` # 数据返回-Redirect 我们可以通过 `RedirectTo/RedirectBack` 来实现页面之间的跳转,该功能通过 `Location Header` 实现。相关方法: ```go func (r *Response) RedirectBack(code ...int) func (r *Response) RedirectTo(location string, code ...int) ``` ## `RedirectTo` `ReditectTo` 用以引导客户端跳转到指定的地址,地址可以是一个本地服务的相对路径,也可以是一个完整的 `HTTP` 地址。使用示例: ```go package main import ( "github.com/gogf/gf/v2/frame/g" "github.com/gogf/gf/v2/net/ghttp" ) func main() { s := g.Server() s.BindHandler("/", func(r *ghttp.Request) { r.Response.RedirectTo("/login") }) s.BindHandler("/login", func(r *ghttp.Request) { r.Response.Writeln("Login First") }) s.SetPort(8199) s.Run() } ``` 运行后,我们通过浏览器访问 [http://127.0.0.1:8199/](http://127.0.0.1:8199/) 随后可以 发现浏览器立即跳转到了 [http://127.0.0.1:8199/login](http://127.0.0.1:8199/login) 页面。 ## `RedirectBack` `RedirectBack` 用以引导客户端跳转到上一页面地址,上一页面地址是通过 `Referer Header` 获取的,一般来说浏览器客户端都会传递这一Header。使用示例: ```go package main import ( "github.com/gogf/gf/v2/frame/g" "github.com/gogf/gf/v2/net/ghttp" ) func main() { s := g.Server() s.BindHandler("/page", func(r *ghttp.Request) { r.Response.Writeln(`back`) }) s.BindHandler("/back", func(r *ghttp.Request) { r.Response.RedirectBack() }) s.SetPort(8199) s.Run() } ``` 运行后,我们通过浏览器访问 [http://127.0.0.1:8199/page](http://127.0.0.1:8199/page) 点击页面的 `back` 连接,可以发现点击后页面随后又跳转了回来。 # 数据返回-Exit控制 ## `Exit`, `ExitAll` 与 `ExitHook` 1. `Exit`: 仅退出当前执行的逻辑方法,不退出后续的请求流程,可用于替代 `return`。 2. `ExitAll`: 强行中断当前执行流程,当前执行方法的后续逻辑以及后续所有的逻辑方法将不再执行,常用于权限控制。 3. `ExitHook`: 当路由匹配到多个 `HOOK` 方法时,默认是按照路由匹配优先级顺序执行 `HOOK` 方法。当在 `HOOK` 方法中调用 `ExitHook` 方法后,后续的 `HOOK` 方法将不会被继续执行,作用类似 `HOOK` 方法覆盖。 4. 这三个退出函数仅在服务函数和 `HOOK` 事件回调函数中有效,无法控制中间件的执行流程。 由于 `ExitAll` 和 `ExitHook` 方法在应用层比较少用,因此这里仅介绍 `Exit` 方法的使用。 **提示** `Exit*` 流程退出特性底层采用的是 `panic...recover...` 机制来实现的,CPU执行损耗大约几十纳秒( `ns`),通过极少的运行时开销来提高易用性。 ## `Exit` 返回方法 ```go package main import ( "github.com/gogf/gf/v2/frame/g" "github.com/gogf/gf/v2/net/ghttp" ) func main() { s := g.Server() s.BindHandler("/", func(r *ghttp.Request) { if r.Get("type").Int() == 1 { r.Response.Writeln("john") } r.Response.Writeln("smith") }) s.SetPort(8199) s.Run() } ``` 执行后,我们访问 [http://127.0.0.1:8199/?type=1](http://127.0.0.1:8199/?type=1),可以看到页面输出了: ```text john smith ``` 我们将以上代码稍微调整一下: ```go package main import ( "github.com/gogf/gf/v2/frame/g" "github.com/gogf/gf/v2/net/ghttp" ) func main() { s := g.Server() s.BindHandler("/", func(r *ghttp.Request) { if r.Get("type").Int() == 1 { r.Response.Writeln("john") r.Exit() } r.Response.Writeln("smith") }) s.SetPort(8199) s.Run() } ``` 执行后,我们再次访问 [http://127.0.0.1:8199/?type=1](http://127.0.0.1:8199/?type=1),可以看到页面输出了: ```text john ``` 此外, `Response` 对象中提供了很多 `Write*Exit` 的方法,表示输出内容后立即调用 `Exit` 方法退出当前服务方法。 # 数据返回-文件下载 `Response` 对象支持文件下载。 相关方法: ```go func (r *Response) ServeFile(path string, allowIndex ...bool) func (r *Response) ServeFileDownload(path string, name ...string) ``` ## `ServeFile` 通过给定文件路径 `path`, `ServeFile` 方法将会自动识别文件格式,如果是目录或者文本内容将会直接展示文件内容。如果 `path` 参数为目录,那么第二个参数 `allowIndex` 控制是否可以展示目录下的文件列表。 使用示例: ```go package main import ( "github.com/gogf/gf/v2/frame/g" "github.com/gogf/gf/v2/net/ghttp" ) func main() { s := g.Server() s.BindHandler("/", func(r *ghttp.Request) { r.Response.ServeFile("test.txt") }) s.SetPort(8999) s.Run() } ``` 访问 [http://127.0.0.1:8999](http://127.0.0.1:8999) 可以发现文件内容被展示到了页面。 ## `ServeFileDownload` `ServeFileDownload` 是相对使用频率比较高的方法,用于直接引导客户端下载指定路径的文件,并可以重新给定下载的文件名称。 `ServeFileDownload` 方法采用的是流式下载控制,对内存占用较少。使用示例,我们把上面示例中的 `ServeFile` 方法改为 `ServeFileDownload` 方法: ```go package main import ( "github.com/gogf/gf/v2/frame/g" "github.com/gogf/gf/v2/net/ghttp" ) func main() { s := g.Server() s.BindHandler("/", func(r *ghttp.Request) { r.Response.ServeFileDownload("test.txt") }) s.SetPort(8999) s.Run() } ``` 访问 [http://127.0.0.1:8999](http://127.0.0.1:8999) 可以发现文件被引导下载,而不是展示页面内容。 # 数据返回-模板解析 相关方法: ```go func (r *Response) WriteTpl(tpl string, params ...gview.Params) error func (r *Response) WriteTplContent(content string, params ...gview.Params) error func (r *Response) WriteTplDefault(params ...gview.Params) error func (r *Response) ParseTpl(tpl string, params ...gview.Params) (string, error) func (r *Response) ParseTplContent(content string, params ...gview.Params) (string, error) func (r *Response) ParseTplDefault(params ...gview.Params) (string, error) ``` `Response` 支持模板文件/内容解析输出,或者模板文件/内容解析返回。与直接使用模板对象解析模板功能不同的是, `Response` 的解析支持一些请求相关的内置变量。模板解析包含以下方法: 1. `WriteTpl*` 方法用于模板输出,解析并输出模板文件,也可以直接解析并输出给定的模板内容。 2. `ParseTpl*` 方法用于模板解析,解析模板文件或者模板内容,返回解析后的内容。 **提示** 解析模板时组件底层会自动通过 `Request` 对象获取当前链路的 `Context` 上下文变量并传递给模板引擎,因此开发者不用显示给模板引擎传递 `Context` 上下文变量。 ## 内置变量 ### `Config` 访问默认的配置管理( `config.toml`)对象配置项。 **使用方式**: ``` {{.Config.配置项}} ``` ### `Cookie` 访问当前请求的 `Cookie` 对象参数值。 **使用方式**: ``` {{.Cookie.键名}} ``` ### `Session` 访问当前请求的 `Session` 对象参数值。 **使用方式**: ``` {{.Session.键名}} ``` ### `Query` 访问当前 `Query String` 中的请求参数值。 **使用方式**: ``` {{.Query.键名}} ``` ### `Form` 访问当前表单请求参数值。 **使用方式**: ``` {{.Form.键名}} ``` ### `Request` 访问当前请求参数值(不区分参数提交方式)。 **使用方式**: ``` {{.Request.键名}} ``` ## 使用示例 ```go package main import ( "github.com/gogf/gf/v2/frame/g" "github.com/gogf/gf/v2/net/ghttp" ) func main() { s := g.Server() s.BindHandler("/", func(r *ghttp.Request){ r.Cookie.Set("theme", "default") r.Session.Set("name", "john") content :=`Config:{{.Config.redis.cache}}, Cookie:{{.Cookie.theme}}, Session:{{.Session.name}}, Query:{{.Query.name}}` r.Response.WriteTplContent(content, nil) }) s.SetPort(8199) s.Run() } ``` 其中, `config.toml` 内容为: ```toml # Redis数据库配置 [redis] disk = "127.0.0.1:6379,0" cache = "127.0.0.1:6379,1" ``` 执行后,访问 [http://127.0.0.1:8199/?name=john](http://127.0.0.1:8199/?name=john),输出结果为: ```html Config:127.0.0.1:6379,1, Cookie:default, Session:john, Query:john ``` # 数据返回-Stream返回 我们可以很简单实现`HTTP SSE`流式数据返回。 ## 框架版本 >= `v2.4` 由于以上操作有点繁琐,因此在该版本以后做了一些操作上的改进,如果当前使用的框架版本在 `v2.4` 正式版以上,那么可以使用以下方式快速实现流式数据返回。 ```go package main import ( "time" "github.com/gogf/gf/v2/frame/g" "github.com/gogf/gf/v2/net/ghttp" ) func main() { s := g.Server() s.BindHandler("/", func(r *ghttp.Request) { r.Response.Header().Set("Content-Type", "text/event-stream") r.Response.Header().Set("Cache-Control", "no-cache") r.Response.Header().Set("Connection", "keep-alive") for i := 0; i < 100; i++ { r.Response.Writefln("data: %d", i) r.Response.Flush() time.Sleep(time.Millisecond * 200) } }) s.SetPort(8999) s.Run() } ``` ## 框架版本 < `v2.4` 如果当前使用的框架版本小于 `v2.4` 正式版,使用以下方式返回(标准库原生写法)。 ```go package main import ( "fmt" "net/http" "time" "github.com/gogf/gf/v2/frame/g" "github.com/gogf/gf/v2/net/ghttp" ) func main() { s := g.Server() s.BindHandler("/", func(r *ghttp.Request) { rw := r.Response.RawWriter() flusher, ok := rw.(http.Flusher) if !ok { http.Error(rw, "Streaming unsupported!", http.StatusInternalServerError) return } r.Response.Header().Set("Content-Type", "text/event-stream") r.Response.Header().Set("Cache-Control", "no-cache") r.Response.Header().Set("Connection", "keep-alive") for i := 0; i < 100; i++ { _, err := fmt.Fprintf(rw, "data: %d\n", i) if err != nil { panic(err) } flusher.Flush() time.Sleep(time.Millisecond * 200) } }) s.SetPort(8999) s.Run() } ``` ## 示例结果 执行后访问 [http://127.0.0.1:8999/](http://127.0.0.1:8999/) 可以看到数据通过流式方式不断地将数据返回给调用端。 ## 延伸阅读 - [打造高性能实时通信:Go语言SSE实现指南](https://goframe.org/articles/go-sse-implementation-guide) ## 注意事项 - 如果是在控制器中使用, `Request` 对象的获取可以通过 `g.RequestFromCtx(ctx)` 方法。 - 如果有前置统一输入输出处理的中间件,请将该方法放置于中间件作用域之外,或者使用 `r.ExitAll()` 方法跳出中间件控制。 ## 参考资料 - [Server-Sent Events (SSE)](https://www.ruanyifeng.com/blog/2017/05/server-sent_events.html) # 服务配置 `GoFrame` 框架的 `WebServer` 配置管理非常方便,支持通过配置文件配置、多种配置文件格式、多种配置方式以及若干配置方法。 # 服务配置-配置文件 `Server` 对象支持通过配置文件进行便捷的配置。支持的配置选项以及配置说明请查看接口文档说明,文档中有详细说明,以下章节不会对配置选项作介绍。 当使用 `g.Server(单例名称)` 获取 `Server` 单例对象时,将会自动通过默认的配置管理对象获取对应的 `Server` 配置。默认情况下会读取 `server.单例名称` 配置项,当该配置项不存在时,将会读取 `server` 配置项。 支持的配置文件配置项请参考 `Server` 配置管理对象属性: [https://pkg.go.dev/github.com/gogf/gf/v2/net/ghttp#ServerConfig](https://pkg.go.dev/github.com/gogf/gf/v2/net/ghttp#ServerConfig) ## 示例1,默认配置项 ```yaml server: address: ":80" serverRoot: "/var/www/Server" ``` 随后可以使用 `g.Server()` 获取默认的单例对象时自动获取并设置该配置。 **注意** 在没有配置 `address` 的情况下, `http server` 将会使用本地网卡的所有 `ip` 地址加上 **随机空闲端口** 来启动(默认配置 `:0`)。如果想要指定 `ip` 但是使用随机空闲端口启动 `http server`,可以使用 `ip:0` 的格式来配置 `address`,例如: `192.168.1.1:0, 10.0.1.1:0`。 ## 示例2,多个配置项 多个 `Server` 的配置示例: ```yaml server: address: ":80" serverRoot: "/var/www/Server" server1: address: ":8080" serverRoot: "/var/www/Server1" server2: address: ":8088" serverRoot: "/var/www/Server2" ``` 我们可以通过单例对象名称获取对应配置的 `Server` 单例对象: ```go // 对应 server.server1 配置项 s1 := g.Server("server1") // 对应 server.server2 配置项 s2 := g.Server("server2") // 对应默认配置项 server s3 := g.Server("none") // 对应默认配置项 server s4 := g.Server() ``` ## 示例3,较完整示例 比如上一个章节的示例,对应的配置文件如下: ```yaml server: address: ":8199" serverRoot: "/var/www/Server" indexFiles: ["index.html", "main.html"] accessLogEnabled: true errorLogEnabled: true pprofEnabled: true logPath: "/var/log/ServerLog" sessionIdName: "MySessionId" sessionPath: "/tmp/MySessionStoragePath" sessionMaxAge: "24h" dumpRouterMap: false ``` 同理,配置属性项的名称也不区分大小写,单词间也支持使用 `-`/ `_` 符号连接。也就是说以下配置文件效果和上面的配置文件一致: ```yaml server: address: ":8199" serverRoot: "/var/www/Server" indexFiles: ["index.html", "main.html"] accessLogEnabled: true errorLogEnabled: true pprofEnabled: true log-path: "/var/log/ServerLog" session_Id_Name: "MySessionId" Session-path: "/tmp/MySessionStoragePath" session_MaxAge: "24h" DumpRouterMap: false ``` **提示** 我们推荐配置文件中的配置项名称统一使用小驼峰的格式。 ## 上传限制 `Server` 对于客户端提交的数据是由大小限制的,主要有两个配置参数控制: - `MaxHeaderBytes`:请求头大小限制,请求头包括客户端提交的 `Cookie` 数据,默认设置为 `10KB`。 - `ClientMaxBodySize`:客户端提交的 `Body` 大小限制,同时也影响文件上传大小,默认设置为 `8MB`。 由于安全性的考虑,默认的上传限制都不是很高,特别是 `ClientMaxBodySize` 的大小限制,在需要文件上传的场景可适当考虑调整,通过配置文件配置即可,例如: ```yaml server: maxHeaderBytes: "20KB" clientMaxBodySize: "200MB" ``` 这样便修改请求头大小限制为 `20KB`,文件上传大小限制为 `200MB`。如果不想做上传大小做任何限制,那么将 `clientMaxBodySize` 设置为 `0` 即可。 ## 日志配置 从 `v2` 版本开始, `Server` 增加了对配置文件中 `Logger` 的配置项支持,主要是为了统一日志组件配置、并解决日志滚动切分的问题。配置示例: ```yaml server: address: ":8080" logger: path: "/var/log/server" file: "{Y-m-d}.log" stdout: false rotateSize: "100M" rotateBackupLimit: 10 rotateBackupExpire: "60d" rotateBackupCompress: 9 rotateCheckInterval: "24h" ``` 关于其中 `logger` 项的详细介绍请参考 [日志组件-配置管理](../core/glog-config.md) 章节。 # 服务配置-配置文件模板 所有的配置项请参考 `ServerConfig` 对象属性: [https://pkg.go.dev/github.com/gogf/gf/v2/net/ghttp#ServerConfig](https://pkg.go.dev/github.com/gogf/gf/v2/net/ghttp#ServerConfig) 以下为配置文件模板: ```yaml server: # 基本配置 address: ":80" # 本地监听地址。默认":80",多个地址以","号分隔。例如:"192.168.2.3:8000,10.0.3.10:8001" httpsAddr: ":443" # TLS/HTTPS配置,同时需要配置证书和密钥。默认关闭。配置格式同上。 httpsCertPath: "" # TLS/HTTPS证书文件本地路径,建议使用绝对路径。默认关闭 httpsKeyPath: "" # TLS/HTTPS密钥文件本地路径,建议使用绝对路径。默认关闭 readTimeout: "60s" # 请求读取超时时间,一般不需要配置。默认为60秒 writeTimeout: "0" # 数据返回写入超时时间,一般不需要配置。默认不超时(0) idleTimeout: "60s" # 仅当Keep-Alive开启时有效,请求闲置时间。默认为60秒 maxHeaderBytes: "10240" # 请求Header大小限制(Byte)。默认为10KB keepAlive: true # 是否开启Keep-Alive功能。默认true serverAgent: "GoFrame HTTP Server" # 服务端Agent信息。默认为"GoFrame HTTP Server" # 接口文档 openapiPath: "/api.json" # OpenAPI接口文档地址 swaggerPath: "/swagger" # 内置SwaggerUI展示地址 # 静态服务配置 indexFiles: ["index.html","index.htm"] # 自动首页静态文件检索。默认为["index.html", "index.htm"] indexFolder: false # 当访问静态文件目录时,是否展示目录下的文件列表。默认关闭,那么请求将返回403 serverRoot: "/var/www" # 静态文件服务的目录根路径,配置时自动开启静态文件服务。默认关闭 searchPaths: ["/home/www","/var/lib/www"] # 提供静态文件服务时额外的文件搜索路径,当根路径找不到时则按照顺序在搜索目录查找。默认关闭 fileServerEnabled: false # 静态文件服务总开关。默认false # Cookie配置 cookieMaxAge: "365d" # Cookie有效期。默认为365天 cookiePath: "/" # Cookie有效路径。默认为"/"表示全站所有路径下有效 cookieDomain: "" # Cookie有效域名。默认为当前配置Cookie时的域名 # Sessions配置 sessionMaxAge: "24h" # Session有效期。默认为24小时 sessionIdName: "gfsessionid" # SessionId的键名名称。默认为gfsessionid sessionCookieOutput: true # Session特性开启时,是否将SessionId返回到Cookie中。默认true sessionPath: "/tmp/gsessions" # Session存储的文件目录路径。默认为当前系统临时目录下的gsessions目录 # 日志基本配置 # 此配置类似nginx,主要对请求日志的记录 logPath: "" # 日志文件存储目录路径,建议使用绝对路径。默认为空,表示关闭 logStdout: true # 日志是否输出到终端。默认为true errorStack: true # 当Server捕获到异常时是否记录堆栈信息到日志中。默认为true errorLogEnabled: true # 是否记录异常日志信息到日志中。默认为true errorLogPattern: "error-{Ymd}.log" # 异常错误日志文件格式。默认为"error-{Ymd}.log" accessLogEnabled: false # 是否记录访问日志。默认为false accessLogPattern: "access-{Ymd}.log" # 访问日志文件格式。默认为"access-{Ymd}.log" # 日志扩展配置(参数日志组件配置) # 此配置主要影响server(ghttp模块)日志,如web服务监听端口号、web服务路由,不会对 g.Log 产生影响 logger: path: "/var/log/" # 日志文件路径。默认为空,表示关闭,仅输出到终端 file: "{Y-m-d}.log" # 日志文件格式。默认为"{Y-m-d}.log" prefix: "" # 日志内容输出前缀。默认为空 level: "all" # 日志输出级别 timeFormat: "2006-01-02T15:04:05" # 自定义日志输出的时间格式,使用Golang标准的时间格式配置 ctxKeys: [] # 自定义Context上下文变量名称,自动打印Context的变量到日志中。默认为空 header: true # 是否打印日志的头信息。默认true stdout: true # 日志是否同时输出到终端。默认true rotateSize: 0 # 按照日志文件大小对文件进行滚动切分。默认为0,表示关闭滚动切分特性 rotateExpire: 0 # 按照日志文件时间间隔对文件滚动切分。默认为0,表示关闭滚动切分特性 rotateBackupLimit: 0 # 按照切分的文件数量清理切分文件,当滚动切分特性开启时有效。默认为0,表示不备份,切分则删除 rotateBackupExpire: 0 # 按照切分的文件有效期清理切分文件,当滚动切分特性开启时有效。默认为0,表示不备份,切分则删除 rotateBackupCompress: 0 # 滚动切分文件的压缩比(0-9)。默认为0,表示不压缩 rotateCheckInterval: "1h" # 滚动切分的时间检测间隔,一般不需要设置。默认为1小时 stdoutColorDisabled: false # 关闭终端的颜色打印。默认开启 writerColorEnable: false # 日志文件是否带上颜色。默认false,表示不带颜色 # PProf配置 pprofEnabled: false # 是否开启PProf性能调试特性。默认为false pprofPattern: "" # 开启PProf时有效,表示PProf特性的页面访问路径,对当前Server绑定的所有域名有效。 # 平滑重启特性 graceful: false # 是否开启平滑重启特性,开启时将会在本地增加10000的本地TCP端口用于进程间通信。默认false gracefulTimeout: 2 # 父进程在平滑重启后多少秒退出,默认2秒。若请求耗时大于该值,可能会导致请求中断 gracefulShutdownTimeout: 5 # 关闭Server时如果存在正在执行的HTTP请求,Server等待多少秒才执行强行关闭 # 其他配置 clientMaxBodySize: 810241024 # 客户端最大Body上传限制大小,影响文件上传大小(Byte)。默认为8*1024*1024=8MB formParsingMemory: 1048576 # 解析表单时的缓冲区大小(Byte),一般不需要配置。默认为1024*1024=1MB nameToUriType: 0 # 路由注册中使用对象注册时的路由生成规则。默认为0 routeOverWrite: false # 当遇到重复路由注册时是否强制覆盖。默认为false,重复路由存在时将会在启动时报错退出 dumpRouterMap: true # 是否在Server启动时打印所有的路由列表。默认为true ``` # 服务配置-配置方法 ## 配置对象 配置对象定义: [https://pkg.go.dev/github.com/gogf/gf/v2/net/ghttp#ServerConfig](https://pkg.go.dev/github.com/gogf/gf/v2/net/ghttp#ServerConfig) ## 配置方法 方法列表: [https://pkg.go.dev/github.com/gogf/gf/v2/net/ghttp#Server](https://pkg.go.dev/github.com/gogf/gf/v2/net/ghttp#Server) 简要说明: 1. 可以通过 `SetConfig` 及 `SetConfigWithMap` 来设置。 2. 也可以使用 `Server` 对象的 `Set*/Enable*` 方法进行特定配置的设置。 3. 主要注意的是,配置项在 `Server` 执行 `Start` 之后便不能再修改,防止产生并发安全问题。 ### `SetConfigWithMap` 方法 我们可以使用 `SetConfigWithMap` 方法通过 `Key-Value` 键值对来设置/修改 `Server` 的特定配置,其余的配置使用默认配置即可。其中 `Key` 的名称即是 `ServerConfig` 这个 `struct` 中的属性名称,并且不区分大小写,单词间也支持使用 `-`/ `_`/ `空格` 符号连接,具体可参考 [类型转换-Struct转换](../core/gconv-struct.md) 章节。 简单示例: ```go s := g.Server() s.SetConfigWithMap(g.Map{ "Address": ":80", "ServerRoot": "/var/www/MyServerRoot", }) s.Run() ``` 其中 `ServerRoot` 的键名也可以使用 `serverRoot`, `server-root`, `server_root`, `server root`,其他配置属性以此类推。 一个比较完整的示例: ```go s := g.Server() s.SetConfigWithMap(g.Map{ "address": ":80", "serverRoot": "/var/www/Server", "indexFiles": g.Slice{"index.html", "main.html"}, "accessLogEnabled": true, "errorLogEnabled": true, "pprofEnabled": true, "logPath": "/var/log/ServerLog", "sessionIdName": "MySessionId", "sessionPath": "/tmp/MySessionStoragePath", "sessionMaxAge": 24 * time.Hour, "dumpRouterMap": false, }) s.Run() ``` # Session `GoFrame` 框架提供了完善的 `Session` 管理能力,由 `gsession` 组件实现。由于 `Session` 机制在 `HTTP` 服务中最常用,因此后续章节中将着重以 `HTTP` 服务为示例介绍 `Session` 的使用。 ## 基本介绍 接口文档: [https://pkg.go.dev/github.com/gogf/gf/v2/os/gsession](https://pkg.go.dev/github.com/gogf/gf/v2/os/gsession) 任何时候都可以通过 `ghttp.Request` 获取 `Session` 对象,因为 `Cookie` 和 `Session` 都是和请求会话相关,因此都属于 `Request` 的成员对象,并对外公开。 `GoFrame` 框架的 `Session` 默认过期时间是 `24小时`。 `SessionId` 默认通过 `Cookie` 来传递,并且也支持客户端通过 `Header` 传递 `SessionId`, `SessionId` 的识别名称可以通过 `ghttp.Server` 的 `SetSessionIdName` 进行修改。 `Session` 的操作是支持 `并发安全` 的,这也是框架在对 `Session` 的设计上不采用直接以 `map` 的形式操作数据的原因。在 `HTTP` 请求流程中,我们可以通过 `ghttp.Request` 对象来获取 `Session` 对象,并执行相应的数据操作。 此外, `ghttp.Server` 中的 `SessionId` 使用的是客户端的 `RemoteAddr + Header` 请求信息通过 `guid` 模块来生成的,保证随机及唯一性: [https://github.com/gogf/gf/blob/master/net/ghttp/ghttp\_request.go](https://github.com/gogf/gf/blob/master/net/ghttp/ghttp_request.go) ## `gsession` 模块 `Session` 的管理功能由独立的 `gsession` 模块实现,并已完美整合到了 `ghttp.Server` 中。由于该模块是解耦独立的,因此可以应用到更多不同的场景中,例如: `TCP` 通信、 `gRPC` 接口服务等等。在 `gsession` 模块中有比较重要的三个对象/接口: 1. `gsession.Manager`:管理 `Session` 对象、 `Storage` 持久化存储对象、以及过期时间控制。 2. `gsession.Session`:单个 `Session` 会话管理对象,用于 `Session` 参数的增删查改等数据管理操作。 3. `gsession.Storage`:这是一个接口定义,用于 `Session` 对象的持久化存储、数据写入/读取、存活更新等操作,开发者可基于该接口实现自定义的持久化存储特性。 该接口定义详见: [https://github.com/gogf/gf/blob/master/os/gsession/gsession\_storage.go](https://github.com/gogf/gf/blob/master/os/gsession/gsession_storage.go) ## 存储实现方式 `gsession` 实现并为开发者提供了常见的四种 `Session` 存储实现方式: | Storage | 支持分布式 | 支持持久化 | 内存占用 | 执行效率 | 简要介绍 | | --- | --- | --- | --- | --- | --- | | `StorageFile` | 否 | 是 | 中 | 中 | 基于文件存储(默认)。单节点部署方式下比较高效的持久化存储方式: [Session-File](./session-file.md) | | `StorageMemory` | 否 | 否 | 高 | 高 | 基于纯内存存储。单节点部署,性能最高效,但是无法持久化保存,重启即丢失: [Session-Memory](./session-memory.md) | | `StorageRedis` | 是 | 是 | 中 | 中 | 基于 `Redis` 存储( `Key-Value`)。远程 `Redis` 节点存储 `Session` 数据,支持应用多节点部署: [Session-Redis-KeyValue](./session-redis-key-value.md) | | `StorageRedisHashTable` | 是 | 是 | 低 | 低 | 基于 `Redis` 存储( `HashTable`)。远程 `Redis` 节点存储 `Session` 数据,支持应用多节点部署: [Session-Redis-HashTable](./session-redis-hash-table.md) | 四种方式各有优劣,详细介绍请查看对应章节。 ## `Session` 的初始化 以常见的HTTP请求为例。 `ghttp.Request` 中的 `Session` 对象采用了" **懒初始化( `LazyInitialization`)**"设计方式,默认在 `Request` 中有一个 `Session` 属性对象,但是并未初始化(一个空对象),只有在使用 `Session` 属性对象的方法时才会真正执行初始化。这样的设计既保障了未使用 `Session` 特性的请求执行性能,也保证了组件使用的易用性。 ## `Session` 的销毁/注销 用户 `Session` 不再使用,例如用户注销登录状态,需要从存储中硬删除,那么可以调用 `RemoveAll` 方法。 # Session-File ## 文件存储 在默认情况下, `ghttp.Server` 的 `Session` 存储使用了 `内存+文件` 的方式,使用 `StorageFile` 对象实现。具体原理为: 1. `Session` 的数据操作完全基于内存; 2. 使用 `gcache` 进程缓存模块控制数据过期; 3. 使用文件存储持久化存储管理 `Session` 数据; 4. 当且仅有当 `Session` 被标记为 `dirty` 时(数据有更新)才会执行 `Session` 序列化并执行文件持久化存储; 5. 当且仅当内存中的 `Session` 不存在时,才会从文件存储中反序列化恢复 `Session` 数据到内存中,降低 `IO` 调用; 6. 序列化/反序列化使用的是标准库的 `json.Marshal/UnMarshal` 方法; 从原理可知,当 `Session` 为读多写少的场景中, `Session` 的数据操作非常高效。 **提示** 有个注意的细节,由于文件存储涉及到文件操作,为便于降低 `IO` 开销并提高 `Session` 操作性能,并不是每一次 `Session` 请求结束后都会立即刷新对应 `Session` 的 `TTL` 时间。而只有当涉及到更新操作(被标记为 `dirty`)时才会立即刷新其 `TTL`;针对于读取请求,将会每隔 `一分钟` 更新前一分钟内读取操作对应的 `Session` 文件 `TTL` 时间,以便于 `Session` 自动续活。 ## 使用示例 ```go package main import ( "github.com/gogf/gf/v2/frame/g" "github.com/gogf/gf/v2/net/ghttp" "github.com/gogf/gf/v2/os/gtime" "time" ) func main() { s := g.Server() s.SetSessionMaxAge(time.Minute) s.Group("/", func(group *ghttp.RouterGroup) { group.ALL("/set", func(r *ghttp.Request) { r.Session.Set("time", gtime.Timestamp()) r.Response.Write("ok") }) group.ALL("/get", func(r *ghttp.Request) { r.Response.Write(r.Session.Data()) }) group.ALL("/del", func(r *ghttp.Request) { _ = r.Session.RemoveAll() r.Response.Write("ok") }) }) s.SetPort(8199) s.Run() } ``` 在该实例中,为了方便观察过期失效,我们将 `Session` 的过期时间设置为 `1分钟`。执行后, 1. 首先,访问 [http://127.0.0.1:8199/set](http://127.0.0.1:8199/set) 设置一个 `Session` 变量; 2. 随后,访问 [http://127.0.0.1:8199/get](http://127.0.0.1:8199/get) 可以看到该 `Session` 变量已经设置并成功获取; 3. 接着,我们停止程序,并重新启动,再次访问 [http://127.0.0.1:8199/get](http://127.0.0.1:8199/get),可以看到 `Session` 变量已经从文件存储中恢复; 4. 等待1分钟后,再次访问 [http://127.0.0.1:8199/get](http://127.0.0.1:8199/get) 可以看到已经无法获取该 `Session`,因为该 `Session` 已经过期; # Session-Memory ## 内存存储 内存存储比较简单,性能也很高效,但没有持久化存储 `Session` 数据,因此应用程序重启之后便会丢失 `Session` 数据,可用于特定的业务场景中。 `gsession` 的 `内存` 存储使用 `StorageMemory` 对象实现, ## 使用示例 ```go package main import ( "github.com/gogf/gf/v2/frame/g" "github.com/gogf/gf/v2/net/ghttp" "github.com/gogf/gf/v2/os/gsession" "github.com/gogf/gf/v2/os/gtime" "time" ) func main() { s := g.Server() s.SetSessionMaxAge(time.Minute) s.SetSessionStorage(gsession.NewStorageMemory()) s.Group("/", func(group *ghttp.RouterGroup) { group.ALL("/set", func(r *ghttp.Request) { r.Session.MustSet("time", gtime.Timestamp()) r.Response.Write("ok") }) group.ALL("/get", func(r *ghttp.Request) { r.Response.Write(r.Session.Data()) }) group.ALL("/del", func(r *ghttp.Request) { _ = r.Session.RemoveAll() r.Response.Write("ok") }) }) s.SetPort(8199) s.Run() } ``` 在该实例中,为了方便观察过期失效,我们将 `Session` 的过期时间设置为 `1分钟`。执行后, 1. 首先,访问 [http://127.0.0.1:8199/set](http://127.0.0.1:8199/set) 设置一个 `Session` 变量; 2. 随后,访问 [http://127.0.0.1:8199/get](http://127.0.0.1:8199/get) 可以看到该 `Session` 变量已经设置并成功获取; 3. 接着,我们停止程序,并重新启动,再次访问 [http://127.0.0.1:8199/get](http://127.0.0.1:8199/get),可以看到 `Session` 变量已经没有了; # Session-Redis-KeyValue ## Redis KeyValue Storage 文件存储的方式在单节点的场景下非常不错,但是涉及到对应用进行多节点部署的场景下,各个节点的 `Session` 无法共享,因此需要将 `Session` 存储单独剥离出来管理, `Redis` 服务器是比较常见的一个选择。 `gsession` 的 `Redis` 存储使用 `StorageRedis` 对象实现,与文件存储比较类似,为了提高执行效率,也是采用了 `内存+Redis` 的方式。与文件存储唯一不同的是,在每一次请求中如果需要对 `Session` 进行操作时,将会从 `Redis` 中拉取一次最新的 `Session` 数据(而文件存储只会在 `Session` 不存在时读取一次文件)。在每一次请求结束之后,将全量的 `Session` 数据通过 `JSON` 序列化之后通过 `KeyValue` 方式更新到 `Redis` 服务中。 **提示** 如果单个用户下(以用户维度举例) `Session` 数据量不大的业务场景中,都推荐使用这种 `Storage` 方式。如果单个用户 `Session` 数据量较大(例如 `>10MB`),可以参考 `HashTable` 的 `Storage` 方式: [Session-Redis-HashTable](./session-redis-hash-table.md) ## 使用示例 ```go package main import ( "github.com/gogf/gf/v2/frame/g" "github.com/gogf/gf/v2/net/ghttp" "github.com/gogf/gf/v2/os/gsession" "github.com/gogf/gf/v2/os/gtime" "time" ) func main() { s := g.Server() s.SetSessionMaxAge(time.Minute) s.SetSessionStorage(gsession.NewStorageRedis(g.Redis())) s.Group("/", func(group *ghttp.RouterGroup) { group.ALL("/set", func(r *ghttp.Request) { r.Session.Set("time", gtime.Timestamp()) r.Response.Write("ok") }) group.ALL("/get", func(r *ghttp.Request) { r.Response.Write(r.Session.Data()) }) group.ALL("/del", func(r *ghttp.Request) { _ = r.Session.RemoveAll() r.Response.Write("ok") }) }) s.SetPort(8199) s.Run() } ``` 在该实例中,为了方便观察过期失效,我们将 `Session` 的过期时间设置为 `1分钟`。执行后, 1. 首先,访问 [http://127.0.0.1:8199/set](http://127.0.0.1:8199/set) 设置一个 `Session` 变量; 2. 随后,访问 [http://127.0.0.1:8199/get](http://127.0.0.1:8199/get) 可以看到该 `Session` 变量已经设置并成功获取; 3. 接着,我们停止程序,并重新启动,再次访问 [http://127.0.0.1:8199/get](http://127.0.0.1:8199/get),可以看到 `Session` 变量已经从 `Redis` 存储中恢复;如果我们手动修改 `Redis` 中的对应键值数据,页面刷新时也会读取到最新的值; 4. 等待1分钟后,再次访问 [http://127.0.0.1:8199/get](http://127.0.0.1:8199/get) 可以看到已经无法获取该 `Session`,因为该 `Session` 已经过期; # Session-Redis-HashTable ## RedisHashTableStorage 与 `RedisKeyValueStorage` 不同的地方在于 `RedisHashTableStorage` 底层使用 `HashTable` 存储 `Session` 数据,每一次对 `Session` 的增删查改都是直接访问 `Redis` 服务实现(单条数据项操作),不存在像 `RedisKeyValueStorage` 那样初始化全量拉取一次,请求结束后如有修改再全量更新到 `Redis` 服务的操作。 ## 使用示例 ```go package main import ( "github.com/gogf/gf/v2/frame/g" "github.com/gogf/gf/v2/net/ghttp" "github.com/gogf/gf/v2/os/gsession" "github.com/gogf/gf/v2/os/gtime" "time" ) func main() { s := g.Server() s.SetSessionMaxAge(time.Minute) s.SetSessionStorage(gsession.NewStorageRedisHashTable(g.Redis())) s.Group("/", func(group *ghttp.RouterGroup) { group.ALL("/set", func(r *ghttp.Request) { r.Session.Set("time", gtime.Timestamp()) r.Response.Write("ok") }) group.ALL("/get", func(r *ghttp.Request) { r.Response.Write(r.Session.Data()) }) group.ALL("/del", func(r *ghttp.Request) { _ = r.Session.RemoveAll() r.Response.Write("ok") }) }) s.SetPort(8199) s.Run() } ``` 在该实例中,为了方便观察过期失效,我们将 `Session` 的过期时间设置为 `1分钟`。执行后, 1. 首先,访问 [http://127.0.0.1:8199/set](http://127.0.0.1:8199/set) 设置一个 `Session` 变量; 2. 随后,访问 [http://127.0.0.1:8199/get](http://127.0.0.1:8199/get) 可以看到该 `Session` 变量已经设置并成功获取; 3. 接着,我们停止程序,并重新启动,再次访问 [http://127.0.0.1:8199/get](http://127.0.0.1:8199/get),可以看到 `Session` 变量已经从 `Redis` 存储中恢复;如果我们手动修改 `Redis` 中的对应键值数据,页面刷新时也会读取到最新的值; 4. 等待1分钟后,再次访问 [http://127.0.0.1:8199/get](http://127.0.0.1:8199/get) 可以看到已经无法获取该 `Session`,因为该 `Session` 已经过期; # Session-Storage接口开发 大部分场景下,通过 `gsession` 组件内置提供的常见 `Storage` 实现已经能够满足需求。如果有特殊的场景需要制定不易开发 `Storage` 当然也是支持的,因为 `gsession` 的功能都采用了接口化设计。 ## Storage定义 [https://github.com/gogf/gf/blob/master/os/gsession/gsession\_storage.go](https://github.com/gogf/gf/blob/master/os/gsession/gsession_storage.go) ```go // Storage is the interface definition for session storage. type Storage interface { // New creates a custom session id. // This function can be used for custom session creation. New(ctx context.Context, ttl time.Duration) (id string, err error) // Get retrieves and returns session value with given key. // It returns nil if the key does not exist in the session. Get(ctx context.Context, id string, key string) (value interface{}, err error) // GetMap retrieves all key-value pairs as map from storage. GetMap(ctx context.Context, id string) (data map[string]interface{}, err error) // GetSize retrieves and returns the size of key-value pairs from storage. GetSize(ctx context.Context, id string) (size int, err error) // Set sets one key-value session pair to the storage. // The parameter `ttl` specifies the TTL for the session id. Set(ctx context.Context, id string, key string, value interface{}, ttl time.Duration) error // SetMap batch sets key-value session pairs as map to the storage. // The parameter `ttl` specifies the TTL for the session id. SetMap(ctx context.Context, id string, data map[string]interface{}, ttl time.Duration) error // Remove deletes key with its value from storage. Remove(ctx context.Context, id string, key string) error // RemoveAll deletes all key-value pairs from storage. RemoveAll(ctx context.Context, id string) error // GetSession returns the session data as `*gmap.StrAnyMap` for given session id from storage. // // The parameter `ttl` specifies the TTL for this session. // The parameter `data` is the current old session data stored in memory, // and for some storage it might be nil if memory storage is disabled. // // This function is called ever when session starts. It returns nil if the TTL is exceeded. GetSession(ctx context.Context, id string, ttl time.Duration, data *gmap.StrAnyMap) (*gmap.StrAnyMap, error) // SetSession updates the data for specified session id. // This function is called ever after session, which is changed dirty, is closed. // This copy all session data map from memory to storage. SetSession(ctx context.Context, id string, data *gmap.StrAnyMap, ttl time.Duration) error // UpdateTTL updates the TTL for specified session id. // This function is called ever after session, which is not dirty, is closed. UpdateTTL(ctx context.Context, id string, ttl time.Duration) error } ``` 每一个方法的调用时机都在注释中详细介绍了,开发者在实现自定义的 `Storage` 时,可以充分参考内置的几种 `Storage` 实现。 ## 注意事项 - `Storage` 接口中,并不是所有的接口方法都需要实现,开发者仅需要根据业务需要,实现特定调用时机的一些接口即可。 - 为了提高 `Session` 的执行性能,接口有 `gmap.StrAnyMap` 容器类型的使用,开发时可以参考一下章节: [字典类型-gmap](../components/container-gmap.md) # Session-RegenerateId ## 会话固定攻击简介 会话固定攻击(`Session Fixation Attacks`)是一种常见的Web应用安全漏洞,攻击者通过设置用户的会话标识符(`Session ID`)来控制用户的会话。这种攻击的基本流程如下: 1. 攻击者获取一个有效的 `Session ID`(通过访问目标网站或其他方式) 2. 攻击者诱使受害者使用这个`Session ID`访问目标网站(例如通过特制的URL、XSS攻击等) 3. 受害者使用这个预设的`Session ID`登录网站 4. 登录成功后,该`Session ID`与受害者的账户关联 5. 攻击者使用同一个`Session ID`访问网站,从而获得受害者的身份和权限 这种攻击的危害在于,攻击者无需破解用户的密码就能获取用户的身份,从而访问用户的敏感信息或执行未授权的操作。 ## 防范会话固定攻击 防范会话固定攻击的最佳实践是在用户成功认证(登录)后重新生成`Session ID`。这样,即使攻击者设法让用户使用特定的`Session ID`,在登录成功后该 ID 也会被替换,从而使攻击失效。 `GoFrame` 框架提供了 `RegenerateId` 和 `MustRegenerateId` 方法来实现这一安全机制。 ## `RegenerateId` 方法 在一些安全性要求较高的应用场景中,为了防止**会话固定攻击**(`Session Fixation Attacks`),通常需要在用户登录成功后重新生成 `Session ID`。`GoFrame` 框架提供了 `RegenerateId` 和 `MustRegenerateId` 方法来实现这一功能。 ### 实现原理 `RegenerateId` 方法的实现原理如下: 1. 生成新的`Session ID` 2. 将当前会话数据复制到新的`Session ID`下 3. 根据`deleteOld`参数决定是否删除旧的会话数据 4. 更新当前会话的ID为新生成的 ID 这样就实现了会话数据的无缝迁移,同时保证了安全性。 ### `RegenerateId` 方法 - 说明:`RegenerateId` 方法用于为当前会话重新生成一个新的 `Session ID`,同时保留会话中的所有数据。这在用户登录成功后特别有用,可以防止会话固定攻击。 - 格式: ```go RegenerateId(deleteOld bool) (newId string, err error) ``` - 参数说明: - `deleteOld`:指定是否立即删除旧的会话数据 - 如果为 `true`:旧的会话数据将被立即删除 - 如果为 `false`:旧的会话数据将被保留,并根据其 TTL 自动过期 - 返回值: - `newId`:新生成的`Session ID` - `err`:操作过程中可能出现的错误 ### `MustRegenerateId` 方法 - 说明:`MustRegenerateId` 方法与 `RegenerateId` 功能相同,但如果操作过程中出现错误,它会直接`panic`。这在确保`Session ID`必须被重新生成的场景中非常有用。 - 格式: ```go MustRegenerateId(deleteOld bool) string ``` - 参数说明: - `deleteOld`:与 `RegenerateId` 方法中的参数含义相同 - 返回值: - 新生成的`Session ID` ### 使用示例 ```go package main import ( "github.com/gogf/gf/v2/frame/g" "github.com/gogf/gf/v2/net/ghttp" "github.com/gogf/gf/v2/os/gtime" ) func main() { s := g.Server() // 模拟登录接口 s.BindHandler("/login", func(r *ghttp.Request) { username := r.Get("username").String() // password := r.Get("password").String() // 假设这里进行了用户名密码验证 // password ... // 验证通过后,存储用户信息到 Session r.Session.MustSet("user", g.Map{ "username": username, "login_time": gtime.Now(), }) // 重要:登录成功后重新生成 Session ID,防止会话固定攻击 // 参数为 true 表示立即删除旧的会话数据 // 返回的session id可以不使用,server将自动通过header返回最新的session id _, err := r.Session.RegenerateId(true) if err != nil { r.Response.WriteJson(g.Map{ "code": 500, "message": "Session ID 重新生成失败", }) return } r.Response.WriteJson(g.Map{ "code": 0, "message": "登录成功", }) }) // 获取用户信息接口 // 注意需要通过header提交session id s.BindHandler("/user/info", func(r *ghttp.Request) { user := r.Session.MustGet("user") if user == nil { r.Response.WriteJson(g.Map{ "code": 403, "message": "未登录或会话已过期", }) return } r.Response.WriteJson(g.Map{ "code": 0, "message": "获取成功", "data": user, }) }) // 注销登录接口 s.BindHandler("/logout", func(r *ghttp.Request) { // 清除所有会话数据 _ = r.Session.RemoveAll() r.Response.WriteJson(g.Map{ "code": 0, "message": "注销成功", }) }) s.SetPort(8000) s.Run() } ``` ### 安全建议 1. **登录后重新生成 Session ID**:在用户成功登录后,始终调用 `RegenerateId` 方法重新生成`Session ID`,这是防止会话固定攻击的基本做法。 2. **敏感操作后重新生成 Session ID**:在用户执行密码修改、权限变更等敏感操作后,也应考虑重新生成`Session ID`。 3. **删除旧会话数据**:在重新生成`Session ID`时,通常建议将 `deleteOld` 参数设置为 `true`,立即删除旧的会话数据,防止会话数据被恶意利用。 4. **使用 HTTPS**:始终使用`HTTPS`协议传输`Session ID`,防止会话信息被网络安全工具或中间人攻击捕获。 5. **设置正确的 Cookie 属性**:对于存储`Session ID`的`Cookie`,应设置 `HttpOnly`、`Secure` 和 `SameSite` 属性,防止`XSS`攻击和`CSRF`攻击。 # HTTPClient ## 基本介绍 `GoFrame` 框架提供了强大便捷易用的 `HTTP` 客户端,由 `gclient` 组件实现,对象创建可以通过 `gclient.New()` 包方法,也可以通过 `g.Client()` 方法调用。推荐使用 `g.Client()` 来便捷地创建 `HTTP` 客户端对象。由于 `gclient.Client` 内部封装扩展于标准库的 `http.Client` 对象,因此标准库 `http.Client` 有的特性, `gclient.Client` 也是支持的。 **方法列表**:[https://pkg.go.dev/github.com/gogf/gf/v2/net/gclient](https://pkg.go.dev/github.com/gogf/gf/v2/net/gclient) **简要说明**: 1. 我们可以使用 `New` 创建一个自定义的HTTP客户端对象 `Client`,随后可以使用该对象执行请求,该对象底层使用了连接池设计,因此没有 `Close` 关闭方法。 `HTTP` 客户端对象也可以通过 `g.Client()` 快捷方法创建。 2. 客户端提供了一系列以 `HTTP Method` 命名的方法,调用这些方法将会发起对应的 `HTTP Method` 请求。常用的方法是 `Get` 和 `Post` 方法,同时 `DoRequest` 是核心的请求方法,用户可以调用该方法实现自定义的 `HTTP Method` 发送请求。 3. 请求返回结果为 `*ClientResponse` 对象,可以通过该结果对象获取对应的返回结果,通过 `ReadAll`/ `ReadAllString` 方法可以获得返回的内容,该对象在使用完毕后需要通过 `Close` 方法关闭,防止内存溢出。 4. `*Bytes` 方法用于获得服务端返回的二进制数据,如果请求失败返回 `nil`; `*Content` 方法用于请求获得字符串结果数据,如果请求失败返回空字符串; `Set*` 方法用于 `Client` 的参数设置。 5. `*Var` 方法直接请求并获取HTTP接口结果为泛型类型便于转换。如果请求失败或者请求结果为空,会返回一个空的 `g.Var` 泛型对象,不影响转换方法调用。 6. 可以看到,客户端的请求参数的数据参数 `data` 数据类型为 `interface{}` 类型,也就是说可以传递任意的数据类型,常见的参数数据类型为 `string`/ `map`,如果参数为 `map` 类型,参数值将会被自动 `urlencode` 编码。 **注意** 请使用给定的方法创建 `Client` 对象,而不要使用 `new(ghttp.Client)` 或者 `&ghttp.Client{}` 创建客户端对象,否则,哼哼。 ## 链式操作 `GoFrame` 框架的客户端支持便捷的链式操作,常用方法如下(文档方法列表可能滞后于源码,建议查看接口文档或源码 [https://pkg.go.dev/github.com/gogf/gf/v2/net/gclient](https://pkg.go.dev/github.com/gogf/gf/v2/net/gclient)): ```go func (c *Client) Timeout(t time.Duration) *Client func (c *Client) Cookie(m map[string]string) *Client func (c *Client) Header(m map[string]string) *Client func (c *Client) HeaderRaw(headers string) *Client func (c *Client) ContentType(contentType string) *Client func (c *Client) ContentJson() *Client func (c *Client) ContentXml() *Client func (c *Client) BasicAuth(user, pass string) *Client func (c *Client) Retry(retryCount int, retryInterval time.Duration) *Client func (c *Client) Prefix(prefix string) *Client func (c *Client) Proxy(proxyURL string) *Client func (c *Client) RedirectLimit(redirectLimit int) *Client func (c *Client) Dump(dump ...bool) *Client func (c *Client) Use(handlers ...HandlerFunc) *Client ``` 简要说明: 1. `Timeout` 方法用于设置当前请求超时时间。 2. `Cookie` 方法用于设置当前请求的自定义 `Cookie` 信息。 3. `Header*` 方法用于设置当前请求的自定义 `Header` 信息。 4. `Content*` 方法用于设置当前请求的 `Content-Type` 信息,并且支持根据该信息自动检查提交参数并自动编码。 5. `BasicAuth` 方法用于设置 `HTTP Basic Auth` 校验信息。 6. `Retry` 方法用于设置请求失败时重连次数和重连间隔。 7. `Proxy` 方法用于设置http访问代理。 8. `RedirectLimit` 方法用于限制重定向跳转次数。 ## 返回对象 `gclient.Response` 为HTTP对应请求的返回结果对象,该对象继承于 `http.Response`,可以使用 `http.Response` 的所有方法。在此基础之上增加了以下几个方法: ```go func (r *Response) GetCookie(key string) string func (r *Response) GetCookieMap() map[string]string func (r *Response) Raw() string func (r *Response) RawDump() func (r *Response) RawRequest() string func (r *Response) RawResponse() string func (r *Response) ReadAll() []byte func (r *Response) ReadAllString() string func (r *Response) Close() error ``` 这里也要提醒的是, `Response` 需要手动调用 `Close` 方法关闭,也就是说,不管使用不使用返回的 `Response` 对象,都需要将该返回对象赋值给一个变量,并且手动调用其 `Close` 方法进行关闭(往往使用 `defer r.Close()`),否则会造成文件句柄溢出、内存溢出。 ## 重要说明 1. `ghttp` 客户端默认关闭了 `KeepAlive` 功能以及对服务端 `TLS` 证书的校验功能,如果需要启用可自定义客户端的 `Transport` 属性。 2. **连接池参数设定**、 **连接代理设置** 等这些高级功能也可以通过自定义客户端的 `Transport` 属性实现,该数据继承于标准库的 `http.Transport` 对象。 # HTTPClient-基本使用 ## 基本使用 最基本的 `HTTP` 客户端使用是通过 `HTTP Method` 同名的几个操作方法来发送请求, **但需要注意的是返回的结果对象需要执行 `Close` 防止内存溢出**。我们来看几个 `HTTP` 客户端请求的简单示例。 ### 发送 `GET` 请求,打印出返回值 ```go r, err := g.Client().Get(ctx, "https://goframe.org") if err != nil { panic(err) } defer r.Close() fmt.Println(r.ReadAllString()) ``` ### 发送 `GET` 请求,下载远程文件 ```go r, err := g.Client().Get(ctx, "https://goframe.org/cover.png") if err != nil { panic(err) } defer r.Close() gfile.PutBytes("/Users/john/Temp/cover.png", r.ReadAll()) ``` 下载文件操作,小文件下载非常简单。需要注意的是,如果远程文件比较大时,服务端会分批返回数据,因此会需要客户端发多个 `GET` 请求,每一次通过 `Header` 来请求分批的文件范围长度,感兴趣的同学可自行研究相关细节。 ### 发送 `POST` 请求,打印出返回值 ```go r, err := g.Client().Post(ctx, "http://127.0.0.1:8199/form", "name=john&age=18") if err != nil { panic(err) } defer r.Close() fmt.Println(r.ReadAllString()) ``` 传递多参数的时候用户可以使用 `&` 符号进行连接,注意参数值往往需要通过 `gurl.Encode` 编码一下。 ### 发送 `POST` 请求,参数为 `map` 类型,打印出返回值 ```go r, err := g.Client().Post( ctx, "http://127.0.0.1:8199/form", g.Map{ "submit" : "1", "callback" : "http://127.0.0.1/callback?url=http://baidu.com", }, ) if err != nil { panic(err) } defer r.Close() fmt.Println(r.ReadAllString()) ``` 传递多参数的时候用户可以使用 `&` 符号进行连接,也可以直接使用 `map`( **其实之前也提到,任意数据类型都支持,包括 `struct`**)。 ### 发送 `POST` 请求,参数为 `JSON` 数据,打印出返回值 ```go r, err := g.Client().Post( ctx, "http://user.svc/v1/user/create", `{"passport":"john","password":"123456","password-confirm":"123456"}`, ) if err != nil { panic(err) } defer r.Close() fmt.Println(r.ReadAllString()) ``` 可以看到,通过 `ghttp` 客户端发送 `JSON` 数据请求非常方便,直接通过 `Post` 方法提交即可。当没有显式设置 `ContentType` 时,客户端会自动识别参数类型并将请求的 `Content-Type` 设置为 `application/json`。 ### 发送 `DELETE` 请求,打印出返回值 ```go r, err := g.Client().Delete(ctx, "http://user.svc/v1/user/delete/1", "10000") if err != nil { panic(err) } defer r.Close() fmt.Println(r.ReadAllString()) ``` ## `*Bytes` 及 `*Content` 方法 以 `Bytes` 及 `Content` 后缀结尾的请求方法为直接获取返回内容的 **快捷方法**,这些方法将会自动读取服务端返回内容 **并自动关闭请求连接**。 `*Bytes` 方法用于获取 `[]byte` 类型结果, `*Content` 方法用于获取 `string` 类型结果。 **需要注意的是,如果请求执行失败,返回内容将会为空。** ### 发送 `GET` 请求,打印出返回值 ```go // 返回content为[]bytes类型 content := g.Client().GetBytes(ctx, "https://goframe.org") ``` ```go // 返回content为string类型 content := g.Client().GetContent(ctx, "https://goframe.org") ``` ### 发送 `POST` 请求,打印出返回值 ```go // 返回content为[]bytes类型 content := g.Client().PostBytes( ctx, "http://user.svc/v1/user/create", `{"passport":"john","password":"123456","password-confirm":"123456"}`, ) ``` ```go // 返回content为string类型 content := g.Client().PostContent( ctx, "http://user.svc/v1/user/create", `{"passport":"john","password":"123456","password-confirm":"123456"}`, ) ``` ## `*Var` 方法 以 `Var` 后缀结尾的请求方法直接请求并获取 `HTTP` 接口结果为 `g.Var` 泛型类型 **便于下一步执行类型转换,特别是将请求结果转换到结构体对象上**。往往用于服务端返回格式为 `JSON/XML` 的情况,通过返回的 `g.Var` 泛型对象可根据需要自动解析。此外,如果请求失败或者请求结果为空,会返回一个空的 `g.Var` 泛型对象,不影响转换方法调用。 使用示例: ```go type User struct { Id int Name string } ``` ```go // Struct var user *User g.Client().GetVar(ctx, url).Scan(&user) ``` ```go // Struct数组 var users []*User g.Client().GetVar(ctx, url).Scan(&users) ``` # HTTPClient-文件上传 `GoFrame` 支持非常方便的表单文件上传功能,并且HTTP客户端对上传功能进行了必要的封装并极大简化了上传功能调用。 **注意** 注意哦:上传文件大小受到 `ghttp.Server` 的 `ClientMaxBodySize` 配置影响: [https://pkg.go.dev/github.com/gogf/gf/v2/net/ghttp#ServerConfig](https://pkg.go.dev/github.com/gogf/gf/v2/net/ghttp#ServerConfig) 默认支持的上传文件大小为 `8MB`。 ## 服务端 在服务端通过 `Request` 对象获取上传文件: ```go package main import ( "github.com/gogf/gf/v2/frame/g" "github.com/gogf/gf/v2/net/ghttp" ) // Upload uploads files to /tmp . func Upload(r *ghttp.Request) { files := r.GetUploadFiles("upload-file") names, err := files.Save("/tmp/") if err != nil { r.Response.WriteExit(err) } r.Response.WriteExit("upload successfully: ", names) } // UploadShow shows uploading simgle file page. func UploadShow(r *ghttp.Request) { r.Response.Write(` GoFrame Upload File Demo
    `) } // UploadShowBatch shows uploading multiple files page. func UploadShowBatch(r *ghttp.Request) { r.Response.Write(` GoFrame Upload Files Demo
    `) } func main() { s := g.Server() s.Group("/upload", func(group *ghttp.RouterGroup) { group.POST("/", Upload) group.ALL("/show", UploadShow) group.ALL("/batch", UploadShowBatch) }) s.SetPort(8199) s.Run() } ``` 该服务端提供了3个接口: 1. [http://127.0.0.1:8199/upload/show](http://127.0.0.1:8199/upload/show) 地址用于展示单个文件上传的H5页面; 2. [http://127.0.0.1:8199/upload/batch](http://127.0.0.1:8199/upload/batch) 地址用于展示多个文件上传的H5页面; 3. [http://127.0.0.1:8199/upload](http://127.0.0.1:8199/upload) 接口用于真实的表单文件上传,该接口同时支持单个文件或者多个文件上传; 我们这里访问 [http://127.0.0.1:8199/upload/show](http://127.0.0.1:8199/upload/show) 选择需要上传的单个文件,提交之后可以看到文件上传成功到服务器上。 **关键代码说明** 1. 我们在服务端可以通过 `r.GetUploadFiles` 方法获得上传的所有文件对象,也可以通过 `r.GetUploadFile` 获取单个上传的文件对象。 2. 在 `r.GetUploadFiles("upload-file")` 中的参数 `"upload-file"` 为本示例中客户端上传时的表单文件域名称,开发者可以根据前后端约定在客户端中定义,以方便服务端接收表单文件域参数。 3. 通过 `files.Save` 可以将上传的多个文件方便地保存到指定的目录下,并返回保存成功的文件名。如果是批量保存,只要任意一个文件保存失败,都将会立即返回错误。此外, `Save` 方法的第二个参数支持随机自动命名上传文件。 4. 通过 `group.POST("/", Upload)` 注册的路由仅支持 `POST` 方式访问。 ## 客户端 ### 单文件上传 ```go package main import ( "fmt" "github.com/gogf/gf/v2/frame/g" "github.com/gogf/gf/v2/os/gctx" "github.com/gogf/gf/v2/os/glog" ) func main() { var ( ctx = gctx.New() path = "/home/john/Workspace/Go/github.com/gogf/gf/v2/version.go" ) result, err := g.Client().Post(ctx, "http://127.0.0.1:8199/upload", "upload-file=@file:"+path) if err != nil { glog.Fatalf(ctx, `%+v`, err) } defer result.Close() fmt.Println(result.ReadAllString()) } ``` 注意到了吗?文件上传参数格式使用了 `参数名=@file:文件路径`,HTTP客户端将会自动解析 **文件路径** 对应的文件内容并读取提交给服务端。原本复杂的文件上传操作被 `gf` 进行了封装处理,用户只需要使用 `@file:+文件路径` 来构成参数值即可。其中, `文件路径` 请使用本地文件绝对路径。 首先运行服务端程序之后,我们再运行这个上传客户端(注意修改上传的文件路径为本地真实文件路径),执行后可以看到文件被成功上传到服务器的指定路径下。 ### 多文件上传 ```go package main import ( "fmt" "github.com/gogf/gf/v2/frame/g" "github.com/gogf/gf/v2/os/gctx" "github.com/gogf/gf/v2/os/glog" ) func main() { var ( ctx = gctx.New() path1 = "/Users/john/Pictures/logo1.png" path2 = "/Users/john/Pictures/logo2.png" ) result, err := g.Client().Post( ctx, "http://127.0.0.1:8199/upload", fmt.Sprintf(`upload-file=@file:%s&upload-file=@file:%s`, path1, path2), ) if err != nil { glog.Fatalf(ctx, `%+v`, err) } defer result.Close() fmt.Println(result.ReadAllString()) } ``` 可以看到,多个文件上传提交参数格式为 `参数名=@file:xxx&参数名=@file:xxx...`,也可以使用 `参数名[]=@file:xxx&参数名[]=@file:xxx...` 的形式。 首先运行服务端程序之后,我们再运行这个上传客户端(注意修改上传的文件路径为本地真实文件路径),执行后可以看到文件被成功上传到服务器的指定路径下。 ## 自定义文件名称 很简单,修改 `FileName` 属性即可。 ```go s := g.Server() s.BindHandler("/upload", func(r *ghttp.Request) { file := r.GetUploadFile("TestFile") if file == nil { r.Response.Write("empty file") return } file.Filename = "MyCustomFileName.txt" fileName, err := file.Save(gfile.TempDir()) if err != nil { r.Response.Write(err) return } r.Response.Write(fileName) }) s.SetPort(8999) s.Run() ``` ## 规范路由接收上传文件 服务端如果通过规范路由方式,那么可以通过结构化的参数获取上传文件: - 参数接收的数据类型使用 `*ghttp.UploadFile` - 如果需要接口文档也支持文件类型,那么参数的标签中设置 `type` 为 `file` 类型 # HTTPClient-自定义Cookie HTTP客户端发起请求时可以自定义发送给服务端的 `Cookie` 内容,该特性使用 `SetCookie*` 相关方法实现。 方法列表: ```go func (c *Client) SetCookie(key, value string) *Client func (c *Client) SetCookieMap(m map[string]string) *Client ``` 我们来看一个客户端自定义 `Cookie` 的示例。 ### 服务端 ```go package main import ( "github.com/gogf/gf/v2/frame/g" "github.com/gogf/gf/v2/net/ghttp" ) func main() { s := g.Server() s.BindHandler("/", func(r *ghttp.Request){ r.Response.Write(r.Cookie.Map()) }) s.SetPort(8199) s.Run() } ``` 由于是作为示例,服务端的逻辑很简单,直接将接收到的 `Cookie` 参数全部返回给客户端。 ### 客户端 1. 使用 `SetCookie` 方法 ```go package main import ( "fmt" "github.com/gogf/gf/v2/frame/g" "github.com/gogf/gf/v2/os/gctx" ) func main() { c := g.Client() c.SetCookie("name", "john") c.SetCookie("score", "100") if r, e := c.Get(gctx.New(), "http://127.0.0.1:8199/"); e != nil { panic(e) } else { fmt.Println(r.ReadAllString()) } } ``` 通过 `g.Client()` 创建一个自定义的HTTP请求客户端对象,并通过 `c.SetCookie` 方法设置自定义的 `Cookie`,这里我们设置了两个示例用的 `Cookie` 参数,一个 `name`,一个 `score`。 2. 使用 `SetCookieMap` 方法 这个方法更加简单,可以批量设置 `Cookie` 键值对。 ```go package main import ( "fmt" "github.com/gogf/gf/v2/frame/g" "github.com/gogf/gf/v2/os/gctx" ) func main() { c := g.Client() c.SetCookieMap(g.MapStrStr{ "name": "john", "score": "100", }) if r, e := c.Get(gctx.New(), "http://127.0.0.1:8199/"); e != nil { panic(e) } else { fmt.Println(r.ReadAllString()) } } ``` 3. 执行结果 客户端代码执行后,终端将会打印出服务端的返回结果,如下: ``` map[name:john score:100] ``` 可以看到,服务端已经接收到了客户端自定义的 `Cookie` 参数。 # HTTPClient-自定义Header HTTP客户端发起请求时可以自定义发送给服务端的 `Header` 内容,该特性使用 `SetHeader*` 相关方法实现。 方法列表: ```go func (c *Client) SetHeader(key, value string) *Client func (c *Client) SetHeaderMap(m map[string]string) *Client func (c *Client) SetHeaderRaw(headers string) *Client ``` 我们来看一个客户端通过 `Header` 来自定义发送自定义链路跟踪信息 `Span-Id` 及 `Trace-Id` 消息头的示例。 ### 服务端 ```go package main import ( "github.com/gogf/gf/v2/frame/g" "github.com/gogf/gf/v2/net/ghttp" ) func main() { s := g.Server() s.BindHandler("/", func(r *ghttp.Request) { r.Response.Writef( "Span-Id:%s,Trace-Id:%s", r.Header.Get("Span-Id"), r.Header.Get("Trace-Id"), ) }) s.SetPort(8199) s.Run() } ``` 由于是作为示例,服务端的逻辑很简单,直接将接收到的 `Span-Id` 及 `Trace-Id` 参数返回给客户端。 ### 客户端 1. 使用 `SetHeader` 方法 ```go package main import ( "fmt" "github.com/gogf/gf/v2/frame/g" "github.com/gogf/gf/v2/os/gctx" ) func main() { c := g.Client() c.SetHeader("Span-Id", "0.0.1") c.SetHeader("Trace-Id", "NBC56410N97LJ016FQA") if r, e := c.Get(gctx.New(), "http://127.0.0.1:8199/"); e != nil { panic(e) } else { fmt.Println(r.ReadAllString()) } } ``` 通过 `g.Client()` 创建一个自定义的HTTP请求客户端对象,并通过 `c.SetHeader` 设置自定义的 `Header` 信息。 2. 使用 `SetHeaderRaw` 方法 这个方法更加简单,可以通过原始的Header字符串来设置客户端请求Header。 ```go package main import ( "fmt" "github.com/gogf/gf/v2/frame/g" "github.com/gogf/gf/v2/os/gctx" ) func main() { c := g.Client() c.SetHeaderRaw(` Referer: https://localhost Span-Id: 0.0.1 Trace-Id: NBC56410N97LJ016FQA User-Agent: MyTestClient `) if r, e := c.Get(gctx.New(), "http://127.0.0.1:8199/"); e != nil { panic(e) } else { fmt.Println(r.ReadAllString()) } } ``` 3. 执行结果 客户端代码执行后,终端将会打印出服务端的返回结果,如下: ``` Span-Id:0.0.1,Trace-Id:NBC56410N97LJ016FQA ``` # HTTPClient-自定义ContentType ## 示例1,提交 `Json` 请求 ```go g.Client().ContentJson().PostContent(ctx, "http://order.svc/v1/order", g.Map{ "uid" : 1, "sku_id" : 10000, "amount" : 19.99, "create_time" : "2020-03-26 12:00:00", }) ``` 通过调用 `ContentJson` 链式操作方法,该请求将会将 `Content-Type` 设置为 `application/json`,并且将提交参数自动编码为 `Json`: ``` {"uid":1,"sku_id":10000,"amount":19.99,"create_time":"2020-03-26 12:00:00"} ``` ## 示例2,提交 `Xml` 请求 ```go g.Client().ContentXml().PostContent(ctx, "http://order.svc/v1/order", g.Map{ "uid" : 1, "sku_id" : 10000, "amount" : 19.99, "create_time" : "2020-03-26 12:00:00", }) ``` 通过调用 `ContentXml` 链式操作方法,该请求将会将 `Content-Type` 设置为 `application/xml`,并且将提交参数自动编码为 `Xml`: ``` 19.992020-03-26 12:00:00100001 ``` ## 示例3,自定义 `ContentType` 我们可以通过 `ContentType` 方法自定义客户端请求的 `ContentType`。如果是给定的 `string/[]byte` 参数,客户端将会直接将参数提交给服务端;如果是其他数据类型将会自动对参数执行 `url encode` 再提交到服务端。 示例1: ```go g.Client().ContentType("application/json").PostContent( ctx, "http://order.svc/v1/order", `{"uid":1,"sku_id":10000,"amount":19.99,"create_time":"2020-03-26 12:00:00"}`, ) ``` 或 ```go g.Client().ContentType("application/json; charset=utf-8").PostContent( ctx, "http://order.svc/v1/order", `{"uid":1,"sku_id":10000,"amount":19.99,"create_time":"2020-03-26 12:00:00"}`, ) ``` 提交的参数如下: ``` {"uid":1,"sku_id":10000,"amount":19.99,"create_time":"2020-03-26 12:00:00"} ``` 示例2: ```go g.Client().ContentType("application/x-www-form-urlencoded; charset=utf-8").GetContent( ctx, "http://order.svc/v1/order", g.Map{ "category" : 1, "sku_id" : 10000, }, ) ``` 提交的参数如下: ``` category=1&sku_id=10000 ``` # HTTPClient-自定义Transport 由于 `gclient.Client` 内部封装扩展于标准库的 `http.Client` 对象,因此标准库 `http.Client` 有的特性, `gclient.Client` 也是支持的。我们这里提到的例子是 `Transport` 使用。来看几个示例: ## 使用 `Unix Socket` 客户端和服务端使用 `Unix Socket` 通信,使用 `Transport` 来实现。以下代码为真实项目代码摘选,无法独立运行,仅做参考。 ```go func (*Guardian) ConvertContainerPathToHostPath( ctx context.Context, namespace, podName, containerName, containerPath string, ) (string, error) { var ( client = g.Client() url = "http://localhost/api/v1/pod/path" req = webservice.HostPathInfoReq{ Namespace: namespace, PodName: podName, ContainerName: containerName, ContainerPath: containerPath, } res *webservice.HostPathInfoRes ) client.Transport = &http.Transport{ DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) { return net.Dial("unix", serviceSocketPath) }, } err := client.ContentJson().GetVar(ctx, url, req).Scan(&res) if err != nil { return "", gerror.Wrapf( err, `request guardian failed for url: %s, req: %s`, url, gjson.MustEncodeString(req), ) } if res == nil { return "", gerror.Newf( `nil response from guardian request url: %s, req: %s`, url, gjson.MustEncodeString(req), ) } return res.HostPath, nil } ``` ## 设置客户端连接池大小参数 ```go func ExampleNew_MultiConn_Recommend() { var ( ctx = gctx.New() client = g.Client() ) // controls the maximum idle(keep-alive) connections to keep per-host client.Transport.(*http.Transport).MaxIdleConnsPerHost = 5 for i := 0; i < 5; i++ { go func() { if r, err := client.Get(ctx, "http://127.0.0.1:8999/var/json"); err != nil { panic(err) } else { fmt.Println(r.ReadAllString()) r.Close() } }() } time.Sleep(time.Second * 1) // Output: //{"id":1,"name":"john"} //{"id":1,"name":"john"} //{"id":1,"name":"john"} //{"id":1,"name":"john"} //{"id":1,"name":"john"} } ``` # HTTPClient-请求信息打印 ## 基本介绍 `http` 客户端支持对HTTP请求的输入与输出原始信息获取与打印,方便调试,相关方法如下: ```go func (r *Response) Raw() string func (r *Response) RawDump() func (r *Response) RawRequest() string func (r *Response) RawResponse() string ``` 可以看到,所有的方法均绑定在 `Response` 对象上,也就是说必须要请求结束后才能打印。 ## 使用示例 ```go package main import ( "github.com/gogf/gf/v2/frame/g" "github.com/gogf/gf/v2/os/gctx" ) func main() { response, err := g.Client().Post( gctx.New(), "https://goframe.org", g.Map{ "name": "john", }, ) if err != nil { panic(err) } response.RawDump() } ``` 执行后,终端输出为: ``` +---------------------------------------------+ | REQUEST | +---------------------------------------------+ POST / HTTP/1.1 Host: goframe.org User-Agent: GoFrameHTTPClient v2.0.0-beta Content-Length: 9 Content-Type: application/x-www-form-urlencoded Accept-Encoding: gzip name=john +---------------------------------------------+ | RESPONSE | +---------------------------------------------+ HTTP/1.1 405 Method Not Allowed Connection: close Transfer-Encoding: chunked Allow: GET Cache-Control: no-store Content-Security-Policy: frame-ancestors 'self' Content-Type: text/html;charset=UTF-8 Date: Fri, 03 Dec 2021 09:43:29 GMT Expires: Thu, 01 Jan 1970 00:00:00 GMT Server: nginx ... ``` # HTTPClient-代理Proxy设置 ## 代理 `Proxy` 设置 HTTP客户端发起请求时可以设置代理服务器地址 `proxyURL`,该该特性使用 `SetProxy*` 相关方法实现。代理主要支持 `http` 和 `socks5` 两种形式,分别为 `http://USER:PASSWORD@IP:PORT` 或 `socks5://USER:PASSWORD@IP:PORT` 形式。 方法列表: ```go func (c *Client) SetProxy(proxyURL string) func (c *Client) Proxy(proxyURL string) *Client ``` 我们来看下客户端设置 `proxyURL` 的示例。 ## 普通调用示例 使用 `SetProxy` 配置方法。 ```go client := g.Client() client.SetProxy("http://127.0.0.1:1081") client.SetTimeout(5 * time.Second) response, err := client.Get(gctx.New(), "https://api.ip.sb/ip") if err != nil { fmt.Println(err) } response.RawDump() ``` ## 链式调用示例 使用 `Proxy` 链式方法。 ```go client := g.Client() response, err := client.Proxy("http://127.0.0.1:1081").Get(gctx.New(), "https://api.ip.sb/ip") if err != nil { fmt.Println(err) } fmt.Println(response.RawResponse()) ``` # HTTPClient-拦截器/中间件 ## 基本介绍 `HTTPClient` 支持强大的拦截器/中间件特性,该特性使得对于客户端的全局请求拦截及注入成为了可能,例如修改/注入提交参数、修改/注入返回参数、基于客户端的参数校验等等。中间件的注入通过以下方法实现: ```go func (c *Client) Use(handlers ...HandlerFunc) *Client ``` 在中间件中通过 `Next` 方法执行下一步流程, `Next` 方法定义如下: ```go func (c *Client) Next(req *http.Request) (*Response, error) ``` ## 中间件类型 `HTTPClient` 中间件功能同 `HTTPServer` 的中间件功能类似,同样也是分为了前置中间件和后置中间件两种。 ### 前置中间件 处理逻辑位于 `Next` 方法之前,格式形如: ```go c := g.Client() c.Use(func(c *gclient.Client, r *http.Request) (resp *gclient.Response, err error) { // 自定义处理逻辑 resp, err = c.Next(r) return resp, err }) ``` ### 后置中间件 处理逻辑位于 `Next` 方法之后,格式形如: ```go c := g.Client() c.Use(func(c *gclient.Client, r *http.Request) (resp *gclient.Response, err error) { resp, err = c.Next(r) // 自定义处理逻辑 return resp, err }) ``` ## 使用示例 我们来一个代码示例更好介绍使用,该示例通过给客户端增加拦截器,对提交的JSON数据注入自定义的额外参数,这些额外参数实现对提交参数的签名生成体积签名相关参数提交,也就是实现一版简单的接口参数安全校验。 ### 服务端 服务端的逻辑很简单,就是把客户端提交的 `JSON` 参数按照 `map` 解析后再构造成 `JSON` 字符串返回给客户端。 **备注** 往往服务端也需要通过中间件进行签名校验,我这里偷了一个懒,直接返回了客户端提交的数据。体谅一下文档维护作者。 ```go package main import ( "github.com/gogf/gf/v2/frame/g" "github.com/gogf/gf/v2/net/ghttp" ) func main() { s := g.Server() s.Group("/", func(group *ghttp.RouterGroup) { group.ALL("/", func(r *ghttp.Request) { r.Response.Write(r.GetMap()) }) }) s.SetPort(8199) s.Run() } ``` ### 客户端 客户端的逻辑是实现基本的客户端参数提交、拦截器注入、签名相关参数注入以及签名参数生成。 ```go package main import ( "bytes" "fmt" "io/ioutil" "net/http" "github.com/gogf/gf/v2/container/garray" "github.com/gogf/gf/v2/crypto/gmd5" "github.com/gogf/gf/v2/frame/g" "github.com/gogf/gf/v2/internal/json" "github.com/gogf/gf/v2/net/gclient" "github.com/gogf/gf/v2/os/gctx" "github.com/gogf/gf/v2/os/gtime" "github.com/gogf/gf/v2/util/gconv" "github.com/gogf/gf/v2/util/guid" "github.com/gogf/gf/v2/util/gutil" ) const ( appId = "123" appSecret = "456" ) // 注入统一的接口签名参数 func injectSignature(jsonContent []byte) []byte { var m map[string]interface{} _ = json.Unmarshal(jsonContent, &m) if len(m) > 0 { m["appid"] = appId m["nonce"] = guid.S() m["timestamp"] = gtime.Timestamp() var ( keyArray = garray.NewSortedStrArrayFrom(gutil.Keys(m)) sigContent string ) keyArray.Iterator(func(k int, v string) bool { sigContent += v sigContent += gconv.String(m[v]) return true }) m["signature"] = gmd5.MustEncryptString(gmd5.MustEncryptString(sigContent) + appSecret) jsonContent, _ = json.Marshal(m) } return jsonContent } func main() { c := g.Client() c.Use(func(c *gclient.Client, r *http.Request) (resp *gclient.Response, err error) { bodyBytes, _ := ioutil.ReadAll(r.Body) if len(bodyBytes) > 0 { // 注入签名相关参数,修改Request原有的提交参数 bodyBytes = injectSignature(bodyBytes) r.Body = ioutil.NopCloser(bytes.NewBuffer(bodyBytes)) r.ContentLength = int64(len(bodyBytes)) } return c.Next(r) }) content := c.ContentJson().PostContent(gctx.New(), "http://127.0.0.1:8199/", g.Map{ "name": "goframe", "site": "https://goframe.org", }) fmt.Println(content) } ``` ### 运行测试 先运行服务端: ```bash $ go run server.go SERVER | DOMAIN | ADDRESS | METHOD | ROUTE | HANDLER | MIDDLEWARE ----------|---------|---------|--------|-------|-------------------|------------- default | default | :8199 | ALL | / | main.main.func1.1 | ----------|---------|---------|--------|-------|-------------------|------------- 2021-05-18 09:23:41.865 97906: http server started listening on [:8199] ``` 再运行客户端: ```bash $ go run client.go {"appid":"123","name":"goframe","nonce":"12vd8tx23l6cbfz9k59xehk1002pixfo","signature":"578a90b67bdc63d551d6a18635307ba2","site":"https://goframe.org","timestamp":1621301076} $ ``` 可以看到,服务端接受到的参数多了多了几项,包括 `appid/nonce/timestamp/signature`,这些参数往往都是签名校验算法所需要的参数。 # HTTPClient-常见问题 ## 是否需要保存复用创建的 `gclient.Client` 对象 无论是通过 `g.Client` 或者 `gclient.New` 方法创建 `gclient.Client` 对象,该对象都应该保存下来复用,而不是每一次都新建 `Client` 对象,这样可以提高效率、降低资源使用、使用方式对 `GC` 友好。该对象内建连接池设计,可充分管理大量的短连接请求。由于 `Client` 对象对资源消耗不是很高,所以很多同学可能都没太注意这个点。 什么情况下我应该新建 `gclient.Client` 对象而不是复用呢?可以按照业务模块的解耦设计,每个业务模块单独管理维护自己的 `gclient.Client` 对象。也可以当针对不同的场景,使用 `Client` 的配置不同时,那么可以新建不同的 `Client` 来使用。 ## `invalid semicolon separator in query` **问题原因**:默认 **表单请求** 中带 `;` 字符是非法的(需要 `urlencode`)。具体请参考讨论: [https://github.com/golang/go/issues/25192](https://github.com/golang/go/issues/25192) **错误示例**: ```bash curl localhost:8000/Execute -d '{ "Component": "mysql", "ResourceId": "cdb-gy6hm0ee", "Port": 6379, "SQL": "show databases;", "UserName": "root", "Password": "" }' ``` **修复示例**: 提交请求的时候需要注明 `ContentType`,例如这里应当标明是 `JSON` 请求。 ```bash curl -X POST -H "Content-Type: application/json" localhost:8000/Execute -d '{ "Component": "mysql", "ResourceId": "cdb-gy6hm0ee", "Port": 6379, "SQL": "show databases;", "UserName": "root", "Password": "" }' ``` # HTTPClient-监控指标 `HTTP` 客户端支持监控指标能力,默认是关闭的不影响性能,只有在 `metric` 特性全局开启时该组件才会默认开启监控指标计算和生成功能。 ## 指标列表 | **指标名称** | **指标类型** | **指标单位** | **指标描述** | | --- | --- | --- | --- | | `http.client.request.duration` | `Histogram` | `ms` | 客户端请求执行的时间开销。 | | `http.client.request.duration_total` | `Counter` | `ms` | 每个请求使用的总时间开销。 | | `http.client.connection.duration` | `Histogram` | `ms` | 创建连接所使用的时间开销。 | | `http.client.request.total` | `Counter` | | 已经执行完毕的请求总数。 | | `http.client.request.active` | `Gauge` | | 当前正在处理的请求数量。 | | `http.client.request.body_size` | `Counter` | `bytes` | 请求的字节总大小。 | | `http.client.response.body_size` | `Counter` | `bytes` | 返回的字节总大小。 | ## 属性列表 | **Label名称** | **Label描述** | **Label示例** | | --- | --- | --- | | `server.address` | 请求的目标服务地址。可能是域名、IP地址。 | `goframe.org`
    `10.0.1.132` | | `server.port` | 请求的目标服务端口。 | `8000` | | `http.request.method` | 请求的方法名称。 | `GET`; `POST`; `DELETE` | | `http.response.status_code` | 处理返回的 `HTTP` 状态码。 | `200` | | `url.schema` | 使用的请求协议。 | `http`
    `https` | | `network.protocol.version` | 请求协议版本。 | `1.0`
    `1.1` | # 分页管理 **注意**:废弃提示 从版本`v2.10.0`开始,`gpage`组件已被标记为**废弃**,将在未来版本中被移除。 **推荐替代方案**: 1. **使用ORM分页功能**:`GoFrame`的`gdb` ORM组件提供了完善的分页支持 2. **自定义分页逻辑**:根据业务需求实现分页,前端使用现代化的UI组件库 3. **API分页**:返回分页数据结构,由前端框架处理分页显示 ## 基本介绍 分页管理由 `gpage` 模块实现, `gpage` 提供了强大的动态分页及静态分页功能,并且为开发者自定义分页样式提供了极高的灵活度。 **提示** `gpage` 模块主要用于生成分页的HTML代码,常用于 `MVC` 开发场景。 **使用方式**: ```go import "github.com/gogf/gf/v2/util/gpage" ``` **接口文档**: [https://pkg.go.dev/github.com/gogf/gf/v2/util/gpage](https://pkg.go.dev/github.com/gogf/gf/v2/util/gpage) **分页对象**: ```go // Page is the pagination implementer. // All the attributes are public, you can change them when necessary. type Page struct { TotalSize int // Total size. TotalPage int // Total page, which is automatically calculated. CurrentPage int // Current page number >= 1. UrlTemplate string // Custom url template for page url producing. LinkStyle string // CSS style name for HTML link tag . SpanStyle string // CSS style name for HTML span tag , which is used for first, current and last page tag. SelectStyle string // CSS style name for HTML select tag
    ``` 注意我们这里的服务端连接地址为: `ws://127.0.0.1:8199/ws`。 客户端的功能很简单,主要实现了这几个功能: - 与服务端 `websocket` 连接状态保持及信息展示; - 界面输入内容并发送信息到 `websocket` 服务端; - 接收到 `websocket` 的返回信息后回显在界面上; ## WebSocket服务端 ```go package main import ( "net/http" "github.com/gogf/gf/v2/frame/g" "github.com/gogf/gf/v2/net/ghttp" "github.com/gorilla/websocket" ) func main() { var ( s = g.Server() logger = g.Log() wsUpGrader = websocket.Upgrader{ // CheckOrigin allows any origin in development // In production, implement proper origin checking for security CheckOrigin: func(r *http.Request) bool { return true }, // Error handler for upgrade failures Error: func(w http.ResponseWriter, r *http.Request, status int, reason error) { // Implement error handling logic here }, } ) // Bind WebSocket handler to /ws endpoint s.BindHandler("/ws", func(r *ghttp.Request) { // Upgrade HTTP connection to WebSocket ws, err := wsUpGrader.Upgrade(r.Response.Writer, r.Request, nil) if err != nil { r.Response.Write(err.Error()) return } defer ws.Close() // Get request context for logging var ctx = r.Context() // Message handling loop for { // Read incoming WebSocket message msgType, msg, err := ws.ReadMessage() if err != nil { break // Connection closed or error occurred } // Log received message logger.Infof(ctx, "received message: %s", msg) // Echo the message back to client if err = ws.WriteMessage(msgType, msg); err != nil { break // Error writing message } } // Log connection closure logger.Info(ctx, "websocket connection closed") }) // Configure static file serving s.SetServerRoot("static") // Set server port s.SetPort(8000) // Start the server s.Run() } ``` 可以看到,服务端的代码相当简单,这里需要着重说明的是这几个地方: 1. **WebSocket方法** `websocket` 服务端的路由注册方式和普通的 `http` 回调函数注册方式一样,但是在接口处理中我们需要通过 `ghttp.Request.WebSocket` 方法(这里直接使用指针对象 `r.WebSocket()`)将请求转换为 `websocket` 操作,并返回一个 `WebSocket对象`,该对象用于后续的 `websocket` 通信操作。当然,如果客户端请求并非为 `websocket` 操作时,转换将会失败,该方法会返回错误信息,使用时请注意判断方法的 `error` 返回值。 1. **ReadMessage & WriteMessage** 读取消息以及写入消息对应的是 `websocket` 的数据读取以及写入操作( `ReadMessage & WriteMessage`),需要注意的是这两个方法都有一个 `msgType` 的变量,表示请求读取及写入数据的类型,常见的两种数据类型为:字符串数据或者二进制数据。在使用过程中,由于接口双方都会约定统一的数据格式,因此读取和写入的 `msgType` 几乎都是一致的,所以在本示例中的返回消息时,数据类型参数直接使用的是读取到的 `msgType`。 ## HTTPS的WebSocket 如果需要支持 `HTTPS` 的 `WebSocket` 服务,只需要依赖的 `WebServer` 支持 `HTTPS` 即可,访问的 `WebSocket` 地址需要使用 `wss://` 协议访问。以上客户端 `HTML5` 页面中的 `WebSocket` 访问地址需要修改为: `wss://127.0.0.1:8199/ws`。服务端示例代码: ```go package main import ( "net/http" "github.com/gogf/gf/v2/frame/g" "github.com/gogf/gf/v2/net/ghttp" "github.com/gorilla/websocket" ) func main() { var ( s = g.Server() logger = g.Log() wsUpGrader = websocket.Upgrader{ CheckOrigin: func(r *http.Request) bool { // In production, you should implement proper origin checking return true }, Error: func(w http.ResponseWriter, r *http.Request, status int, reason error) { // Error callback function. }, } ) s.BindHandler("/ws", func(r *ghttp.Request) { ws, err := wsUpGrader.Upgrade(r.Response.Writer, r.Request, nil) if err != nil { r.Response.Write(err.Error()) return } defer ws.Close() var ctx = r.Context() for { msgType, msg, err := ws.ReadMessage() if err != nil { break } logger.Infof(ctx, "received message: %s", msg) if err = ws.WriteMessage(msgType, msg); err != nil { break } } logger.Info(ctx, "websocket connection closed") }) s.EnableHTTPS("certs/server.crt", "certs/server.key") s.SetServerRoot("static") s.SetPort(8000) s.Run() } ``` ## 示例结果展示 我们首先执行示例代码 `main.go`,随后访问页面 [http://127.0.0.1:8199/](http://127.0.0.1:8199/),随意输入请求内容并提交,随后在服务端关闭程序。可以看到,页面会回显提交的内容信息,并且即时展示 `websocket` 的连接状态的改变,当服务端关闭时,客户端也会即时地打印出关闭信息。 ## Websocket安全校验 `GoFrame` 框架的 `websocket` 模块并不会做同源检查( `origin`),也就是说,这种条件下的`websocket`允许完全跨域。 安全的校验需要由业务层来处理,安全校验主要包含以下几个方面: 1. `origin` 的校验: 业务层在执行 `r.WebSocket()` 之前需要进行 `origin` 同源请求的校验;或者按照自定义的处理对请求进行校验(如果请求提交参数);如果未通过校验,那么调用 `r.Exit()` 终止请求。 2. `websocket` 通信数据校验: 数据通信往往都有一些自定义的数据结构,在这些通信数据中加上鉴权处理逻辑; ## WebSocket Client 客户端 ```go package main import ( "context" "github.com/gogf/gf/v2/frame/g" "github.com/gorilla/websocket" ) func main() { var ( ctx = context.Background() logger = g.Log() ) // Connect to WebSocket server using default dialer ws, _, err := websocket.DefaultDialer.Dial("ws://127.0.0.1:8000/ws", nil) if err != nil { logger.Fatalf(ctx, "dial failed: %+v", err) } // Ensure connection is closed when function returns defer ws.Close() // Send a test message to the server err = ws.WriteMessage(websocket.TextMessage, []byte("hello")) if err != nil { logger.Fatalf(ctx, "ws.WriteMessage failed: %+v", err) } // Read the server's response _, msg, err := ws.ReadMessage() if err != nil { logger.Fatalf(ctx, "ws.ReadMessage failed: %+v", err) return } logger.Infof(ctx, `received message: %s`, msg) // Cleanly close the connection by sending a close message // This is important for proper connection cleanup err = ws.WriteMessage(websocket.CloseMessage, []byte("going to close")) if err != nil { logger.Fatalf(ctx, "ws.WriteMessage failed: %+v", err) } } ``` # 自定义状态码处理 我们可以对WebServer指定的状态码进行自定义处理,例如针对常见的 `404/403/500` 等错误,我们可以展示自定义的错误信息、页面内容,或者跳转到一个特定的页面。 相关方法如下: ```go func (s *Server) BindStatusHandler(status int, handler HandlerFunc) func (s *Server) BindStatusHandlerByMap(handlerMap map[int]HandlerFunc) func (d *Domain) BindStatusHandler(status int, handler HandlerFunc) func (d *Domain) BindStatusHandlerByMap(handlerMap map[int]HandlerFunc) ``` 可以看到,我们可以使用 `BindStatusHandler` 或者 `BindStatusHandlerMap` 来实现针对指定的状态码进行自定义的回调函数处理,并且该特性也支持针对特定的域名绑定。我们来看几个简单的示例。 ## 基本使用 ```go package main import ( "github.com/gogf/gf/v2/frame/g" "github.com/gogf/gf/v2/net/ghttp" ) func main() { s := g.Server() s.BindHandler("/", func(r *ghttp.Request){ r.Response.Writeln("halo 世界!") }) s.BindStatusHandler(404, func(r *ghttp.Request){ r.Response.Writeln("This is customized 404 page") }) s.SetPort(8199) s.Run() } ``` 执行后,我们访问没有绑定的路由页面,例如 [http://127.0.0.1:8199/test](http://127.0.0.1:8199/test),可以看到,页面显示了我们期望的返回结果: `This is customized 404 page`。 此外,常见的Web页面请求错误状态码处理方式,是引导用户跳转到指定的错误页面,因此,在状态码回调处理函数中,我们可以使用 `r.RedirectTo` 方法来进行页面跳转,示例如下: ```go package main import ( "github.com/gogf/gf/v2/frame/g" "github.com/gogf/gf/v2/net/ghttp" ) func main() { s := g.Server() s.BindHandler("/status/:status", func(r *ghttp.Request) { r.Response.Write("woops, status ", r.Get("status"), " found") }) s.BindStatusHandler(404, func(r *ghttp.Request){ r.Response.RedirectTo("/status/404") }) s.SetPort(8199) s.Run() } ``` 执行后,我们手动通过浏览器访问一个不存在的页面,例如 [http://127.0.0.1:8199/test](http://127.0.0.1:8199/test),可以看到,页面被引导跳转到了 [http://127.0.0.1:8199/status/404](http://127.0.0.1:8199/status/404) 页面,并且可以看到页面返回内容: `woops, status 404 found` ## 批量设置 ```go package main import ( "github.com/gogf/gf/v2/frame/g" "github.com/gogf/gf/v2/net/ghttp" ) func main() { s := g.Server() s.BindStatusHandlerByMap(map[int]ghttp.HandlerFunc { 403 : func(r *ghttp.Request){r.Response.Writeln("403")}, 404 : func(r *ghttp.Request){r.Response.Writeln("404")}, 500 : func(r *ghttp.Request){r.Response.Writeln("500")}, }) s.SetPort(8199) s.Run() } ``` 可以看到,我们可以通过 `BindStatusHandlerByMap` 方法对需要自定义的状态码进行批量设置。该示例程序执行后,当服务接口返回的状态码为 `403/404/500` 时,接口将会返回对应的状态码数字。 ## 注意事项 在自定义状态码处理方法中如果涉及到内容的输出,往往需要使用 `r.Response.ClearBuffer()` 方法将原本缓冲区的输出内容清空。 # PProf服务性能分析 `GoFrame` 框架的 `Web Server` 提供了非常强大和简便的服务性能分析功能,内部完美集成了 `pprof` 性能分析工具,可以在任何时候通过 `EnablePProf` 方法启用性能分析特性,并可自定义性能分析工具页面路由地址,不传递路由地址时,默认URI地址为 `/debug/pprof`。 ## `PProf` 启用 **注意** `PProf` 特性的启用会对程序性能产生一定影响,具体影响程度需要根据当前业务场景在 `PProd` 启用前后进行对比。 ### `EnablePProf` 我们来看一个简单的例子: ```go package main import ( "github.com/gogf/gf/v2/frame/g" "github.com/gogf/gf/v2/net/ghttp" "runtime" ) func main() { runtime.SetMutexProfileFraction(1) // (非必需)开启对锁调用的跟踪 runtime.SetBlockProfileRate(1) // (非必需)开启对阻塞操作的跟踪 s := g.Server() s.EnablePProf() s.BindHandler("/", func(r *ghttp.Request) { r.Response.Writeln("哈喽世界!") }) s.SetPort(8199) s.Run() } ``` 这个例子使用了 `s.EnablePProf()` 启用了性能分析,默认会自动注册以下几个路由规则: ```html /debug/pprof/*action /debug/pprof/cmdline /debug/pprof/profile /debug/pprof/symbol /debug/pprof/trace ``` 其中 `/debug/pprof/*action` 为页面访问的路由,其他几个地址为 `go tool pprof` 命令准备的。 ### `StartPProfServer` 也可以使用 `StartPProfServer` 方法,快速开启一个独立的 `PProf Server`,常用于一些没有 `HTTP Server` 的常驻的进程中(例如定时任务、 `GRPC` 服务中),可以快速开启一个 `PProf Server` 用于程序性能分析。该方法的定义如下: ```go func StartPProfServer(port int, pattern ...string) ``` 一般的场景是使用异步 `goroutine` 运行该 `PProd Server`,即往往是这么来使用: ```go package main import ( "github.com/gogf/gf/v2/net/ghttp" ) func main() { go ghttp.StartPProfServer(8199) // 其他服务启动、运行 // ... } ``` 以上示例可以改进为: ```go package main import ( "github.com/gogf/gf/v2/frame/g" "github.com/gogf/gf/v2/net/ghttp" ) func main() { go ghttp.StartPProfServer(8299) s := g.Server() s.EnablePProf() s.BindHandler("/", func(r *ghttp.Request) { r.Response.Writeln("哈喽世界!") }) s.SetPort(8199) s.Run() } ``` ## `PProf` 指标 - `heap`: 报告内存分配样本;用于监视当前和历史内存使用情况,并检查内存泄漏。 - `threadcreate`: 报告了导致创建新OS线程的程序部分。 - `goroutine`: 报告所有当前 `goroutine` 的堆栈跟踪。 - `block`: 显示 `goroutine` 在哪里阻塞同步原语(包括计时器通道)的等待。默认情况下未启用,需要手动调用 `runtime.SetBlockProfileRate` 启用。 - `mutex`: 报告锁竞争。默认情况下未启用,需要手动调用 `runtime.SetMutexProfileFraction` 启用。 ## `PProf` 页面 简单的性能分析我们直接访问 `/debug/pprof` 地址即可,内容如下: 1、 `pprof` 页面 2、堆使用量 3、当前进程中的 `goroutine` 详情 ## 性能采集分析 **提示** 以下示例截图来源于示例项目,仅供参考。 如果想要进行详细的性能分析,基本上离不开 `go tool pprof` 命令行工具的支持,在开启性能分析支持后,我们可以使用以下命令执行性能采集分析: ```bash go tool pprof -http :8080 "http://127.0.0.1:8199/debug/pprof/profile" ``` 也可以将pprof文件导出后再通过go tool pprof命令打开: ```bash curl http://127.0.0.1:8199/debug/pprof/profile > pprof.profile go tool pprof -http :8080 pprof.profile ``` 执行后 `profile` 的 `pprof` 工具经过约 `30` 秒左右的接口信息采集(这 `30` 秒期间 `WebServer` 应当有流量进入),然后生成性能分析报告,随后可以通过 `top10`/ `web` 等 `pprof` 命令查看报告结果,更多命令可使用 `go tool pprof` 查看。关于 `pprof` 的详细使用介绍,请查看 `Golang` 官方: [blog.golang.org/profiling-go-programs](https://blog.golang.org/profiling-go-programs) ### CPU性能分析 本示例中的命令行性能分析结果如下: ```bash $ go tool pprof -http :8080 "http://127.0.0.1:8199/debug/pprof/profile" Serving web UI on http://localhost:8080 ``` **提示** 图形化展示 `pprof` 需要安装 `Graphviz` 图形化工具,以我目前的系统为 `Ubuntu` 为例,直接执行 `sudo apt-get install graphviz` 命令即可安装完成图形化工具(如果是 `MacOS`,使用 `brew install Graphviz` 安装)。 运行后将会使用默认的浏览器打开以下图形界面,展示这段时间抓取的CPU开销链路: 也可以查看火炬图,可能更形象一些: ### 内存使用分析 与 `CPU` 性能分析类似,内存使用分析同样使用到 `go tool pprof` 命令: ```bash $ go tool pprof -http :8080 "http://127.0.0.1:8199/debug/pprof/heap" Serving web UI on http://localhost:8080 ``` 图形展示类似这样的: 同样的,也可以查看火炬图,可能更形象一些: ### goroutine使用分析 与上面的分析类似, `goroutine` 使用分析同样使用到 `go tool pprof` 命令: ```bash $ go tool pprof -http :8080 "http://127.0.0.1:8199/debug/pprof/goroutine" Serving web UI on http://localhost:8080 ``` 图形展示类似这样的,通常 `goroutine` 看这个图的话就差不多了,当然也有火炬图。 # SameSite设置 ## SameSite 介绍 ### 参考文档 [https://web.dev/samesite-cookies-explained/](https://web.dev/samesite-cookies-explained/) [https://web.dev/samesite-cookie-recipes/](https://web.dev/samesite-cookie-recipes/) [https://web.dev/schemeful-samesite/](https://web.dev/schemeful-samesite/) ### chrome89开始协议不同也属于跨站请求 [https://www.chromestatus.com/feature/5096179480133632](https://www.chromestatus.com/feature/5096179480133632) ## 如何设置? ```go func main() { s := g.Server() s.BindHandler("/", func(r *ghttp.Request) { r.Cookie.SetHttpCookie(&http.Cookie{ Name: "test", Value: "1234", Secure: true, SameSite: http.SameSiteNoneMode,// 自定义samesite,配合secure一起使用 }) }) s.SetAddr("127.0.0.1:8080") s.Run() } ``` # HTTPServer-监控指标 `HTTP Server` 支持监控指标能力,默认是关闭的不影响性能,只有在 `metric` 特性全局开启时该组件才会默认开启监控指标计算和生成功能。 ## 指标列表 | **指标名称** | **指标类型** | **指标单位** | **指标描述** | | --- | --- | --- | --- | | `http.server.request.duration` | `Histogram` | `ms` | 对 `Server` 端请求执行的时间开销分组。 | | `http.server.request.duration_total` | `Counter` | `ms` | 每个请求的执行时间总开销。 | | `http.server.request.total` | `Counter` | | 已经执行完毕的请求总数,不包含正在执行的请求数。 | | `http.server.request.active` | `Gauge` | | 当前正在处理的请求数量。 | | `http.server.request.body_size` | `Counter` | `bytes` | 请求的字节总大小。 | | `http.server.response.body_size` | `Counter` | `bytes` | 返回的字节总大小。 | ## 属性列表 | **Label名称** | **Label描述** | **Label示例** | | --- | --- | --- | | `server.address` | 接受请求的请求地址。来源于 `Request.Host`,可能是域名也可能是IP地址。 | `goframe.org`
    `10.0.1.132` | | `server.port` | 接受请求的服务端口。同一服务可能有多个端口地址,当前请求连接的是哪个端口就记录哪个端口。 | `8000` | | `http.route` | 请求的路由规则。 | `/api/v1/user/:id` | | `url.schema` | 请求协议名称。 | `http`; `https` | | `network.protocol.version` | 请求的协议版本。 | `1.0`; `1.1` | | `http.request.method` | 请求的方法名称。 | `GET`; `POST`; `DELETE` | | `error.code` | 请求返回的业务自定义错误码,字符串类型以提高兼容性。 | `-1`; `0`; `51` | | `http.response.status_code` | 处理返回的 `HTTP` 状态码。 | `200` | # 微服务开发 ## 基本介绍 `GoFrame` 框架从 `v2.4` 版本开始全面支持微服务模式开发,提供了常用的微服务组件、开发工具和开发教程,帮助开发团队快速实现微服务架构转型。 ## 简单示例 `GoFrame` 的微服务组件采用低耦合和通用化设计,组件化的使用方式支持大部分的微服务通信协议。在官方文档中,我们以 `HTTP` 和 `gRPC` 协议为示例,介绍微服务的开发以及组件工具的使用。由于 `HTTP Web` 开发已经有比较丰富完善的独立章节介绍,因此微服务章节主要以 `gRPC` 为例进行讲解。 ### `HTTP` 微服务示例 [https://github.com/gogf/gf/tree/master/example/registry/file](https://github.com/gogf/gf/tree/master/example/registry/file) #### `server.go` ```go package main import ( "github.com/gogf/gf/contrib/registry/file/v2" "github.com/gogf/gf/v2/frame/g" "github.com/gogf/gf/v2/net/ghttp" "github.com/gogf/gf/v2/net/gsvc" "github.com/gogf/gf/v2/os/gfile" ) func main() { gsvc.SetRegistry(file.New(gfile.Temp("gsvc"))) s := g.Server(`hello.svc`) s.BindHandler("/", func(r *ghttp.Request) { g.Log().Info(r.Context(), `request received`) r.Response.Write(`Hello world`) }) s.Run() } ``` 可以看到,一个 `HTTP` 微服务端和普通的 `Web Server` 基本一致,但是顶部多了一行代码: ```go gsvc.SetRegistry(file.New(gfile.Temp("gsvc"))) ``` 这行代码用于启用并配置当前服务使用的服务注册发现组件。在该示例中使用的 `file.New(gfile.Temp("gsvc"))` 是基于本地文件系统的服务注册发现组件,其中的 `gfile.Temp("gsvc")` 指定了存放服务信息文件的路径。例如在 `Linux`/ `MacOS` 系统下,该路径指向 `/tmp/gsvc` 目录。 **注意** 基于文件系统的注册发现组件仅适用于本地开发和测试,不支持跨节点通信。 在生产环境中,我们需要使用分布式的服务注册发现组件,例如 `etcd`、 `polaris`、 `zookeeper` 等。框架的社区组件库中已经提供了这些常用服务注册发现组件的实现。 在该示例中,我们为 `Server` 设置了名称 `hello.svc`,这是该服务在微服务架构中的唯一标识。服务名称用于服务间的识别和通信,是微服务治理的基础。当启用服务注册组件后, `HTTP Server` 在运行时会自动将自己的访问地址(IP和端口)注册到服务注册中心,使得其他服务能够通过服务名称发现并访问该服务。 #### `client.go` ```go package main import ( "time" "github.com/gogf/gf/contrib/registry/file/v2" "github.com/gogf/gf/v2/frame/g" "github.com/gogf/gf/v2/net/gsvc" "github.com/gogf/gf/v2/os/gctx" "github.com/gogf/gf/v2/os/gfile" ) func main() { gsvc.SetRegistry(file.New(gfile.Temp("gsvc"))) client := g.Client() for i := 0; i < 10; i++ { ctx := gctx.New() res, err := client.Get(ctx, `http://hello.svc/`) if err != nil { panic(err) } g.Log().Debug(ctx, res.ReadAllString()) res.Close() time.Sleep(time.Second) } } ``` 客户端通过 `g.Client()` 创建一个 `HTTP Client` 对象,并通过 `http://hello.svc/` 地址访问服务端。其中的 `hello.svc` 是之前服务端绑定的微服务名称。当客户端使用微服务名称进行访问时,服务注册发现组件会自动在底层进行服务发现,查询该服务名称对应的实际服务地址,然后建立连接进行通信。 #### 执行结果 先执行 `server.go` 启动服务端,然后再执行 `client.go` 通过服务名称请求服务。 执行后,客户端输出: ```bash $ go run client.go 2023-03-14 20:22:10.006 [DEBU] {8054f3a48c484c1760fb416bb3df20a4} Hello world 2023-03-14 20:22:11.007 [DEBU] {6831cae08c484c1761fb416b9d4df851} Hello world 2023-03-14 20:22:12.008 [DEBU] {9035761c8d484c1762fb416b1e648b81} Hello world 2023-03-14 20:22:13.011 [DEBU] {a05a32588d484c1763fb416bc19ff667} Hello world 2023-03-14 20:22:14.012 [DEBU] {40fdea938d484c1764fb416b8459fc43} Hello world 2023-03-14 20:22:15.014 [DEBU] {686c9acf8d484c1765fb416b3697d369} Hello world 2023-03-14 20:22:16.015 [DEBU] {906a470b8e484c1766fb416b85b9867e} Hello world 2023-03-14 20:22:17.017 [DEBU] {28c7fd468e484c1767fb416b86e5557f} Hello world 2023-03-14 20:22:18.018 [DEBU] {90d2ad828e484c1768fb416bfcde738f} Hello world 2023-03-14 20:22:19.019 [DEBU] {d05559be8e484c1769fb416baad06f23} Hello world ``` 服务端输出: ```bash $ go run server.go 2023-03-14 20:20:06.364 [INFO] pid[96421]: http server started listening on [:61589] 2023-03-14 20:20:06.364 [INFO] openapi specification is disabled 2023-03-14 20:20:06.364 [DEBU] service register: &{Head: Deployment: Namespace: Name:hello.svc Version: Endpoints:10.35.12.81:61589 Metadata:map[insecure:true protocol:http]} SERVER | DOMAIN | ADDRESS | METHOD | ROUTE | HANDLER | MIDDLEWARE ------------|---------|---------|--------|-------|-----------------------------------------------------------------|-------------------- hello.svc | default | :61589 | ALL | / | main.main.func1 | ------------|---------|---------|--------|-------|-----------------------------------------------------------------|-------------------- hello.svc | default | :61589 | ALL | /* | github.com/gogf/gf/v2/net/ghttp.internalMiddlewareServerTracing | GLOBAL MIDDLEWARE ------------|---------|---------|--------|-------|-----------------------------------------------------------------|-------------------- 2023-03-14 20:22:10.006 [INFO] {8054f3a48c484c1760fb416bb3df20a4} request received 2023-03-14 20:22:11.007 [INFO] {6831cae08c484c1761fb416b9d4df851} request received 2023-03-14 20:22:12.008 [INFO] {9035761c8d484c1762fb416b1e648b81} request received 2023-03-14 20:22:13.010 [INFO] {a05a32588d484c1763fb416bc19ff667} request received 2023-03-14 20:22:14.012 [INFO] {40fdea938d484c1764fb416b8459fc43} request received 2023-03-14 20:22:15.013 [INFO] {686c9acf8d484c1765fb416b3697d369} request received 2023-03-14 20:22:16.015 [INFO] {906a470b8e484c1766fb416b85b9867e} request received 2023-03-14 20:22:17.016 [INFO] {28c7fd468e484c1767fb416b86e5557f} request received 2023-03-14 20:22:18.017 [INFO] {90d2ad828e484c1768fb416bfcde738f} request received 2023-03-14 20:22:19.019 [INFO] {d05559be8e484c1769fb416baad06f23} request received ``` ### `GRPC` 微服务示例 [https://github.com/gogf/gf/tree/master/example/rpc/grpcx/basic](https://github.com/gogf/gf/tree/master/example/rpc/grpcx/basic) #### `helloworld.proto` `gRPC` 和 `HTTP` 协议的一个显著区别是, `gRPC` 需要通过 `protobuf` 来定义 `API` 接口和数据结构。 `Protobuf` 是一种语言无关、平台无关的可扩展序列化数据结构,相比 `JSON` 和 `XML` 更小更快,是 `gRPC` 的核心特性。 ``` syntax = "proto3"; package protobuf; option go_package = "github.com/gogf/gf/grpc/example/helloworld/protobuf"; // The greeting service definition. service Greeter { // Sends a greeting rpc SayHello (HelloRequest) returns (HelloReply) {} } // The request message containing the user's name. message HelloRequest { string name = 1; } // The response message containing the greetings message HelloReply { string message = 1; } ``` 以上 `protobuf` 文件通过以下命令执行编译(请提前安装 `protoc` 工具): ``` gf gen pb ``` 将会生成对应的 `proto go` 数据结构文件以及 `grpc` 接口文件: ``` helloworld.pb.go helloworld_grpc.pb.go ``` #### `controller.go` 控制器用于实现 `proto` 文件中定义的接口方法。如果使用框架的标准化工程目录结构,该控制器代码文件也可以由框架的 `gf gen pb` 工具自动生成,开发者只需要填充对应方法的具体实现逻辑即可。 ```go type Controller struct { protobuf.UnimplementedGreeterServer } func Register(s *grpcx.GrpcServer) { protobuf.RegisterGreeterServer(s.Server, &Controller{}) } // SayHello implements helloworld.GreeterServer func (s *Controller) SayHello(ctx context.Context, in *protobuf.HelloRequest) (*protobuf.HelloReply, error) { return &protobuf.HelloReply{Message: "Hello " + in.GetName()}, nil } ``` #### `config.yaml` 服务端配置文件,在该配置文件中指定了微服务的名称为 `demo`。微服务名称是服务间通信的唯一标识符。当未显式配置服务端的监听端口时,服务端会自动监听一个可用的本地端口。在微服务模式下,由于使用服务名称进行通信,服务端端口通常不需要显式指定,采用随机监听即可。 ``` grpc: name: "demo" logPath: "./log" logStdout: true errorLogEnabled: true accessLogEnabled: true errorStack: true ``` #### `server.go` `gRPC` 服务端的启动代码。当未显式指定服务端使用的服务注册发现组件时,服务端默认使用基于文件系统的注册发现组件,该组件仅适用于单机测试。其中的 `controller.Register(s)` 调用了我们通过工具生成的控制器注册方法,将具体的接口实现注册到服务端中。 ```go package main import ( "github.com/gogf/gf/contrib/rpc/grpcx/v2" "github.com/gogf/gf/example/rpc/grpcx/basic/controller" ) func main() { s := grpcx.Server.New() controller.Register(s) s.Run() } ``` #### `client.go` `gRPC` 客户端的调用代码。在创建连接时需要指定服务端服务的名称。这里的服务名称为 `demo`,对应的是上面配置文件中设置的微服务名称。当未显式指定客户端使用的服务注册发现组件时,客户端默认使用基于文件系统的注册发现组件,该组件仅适用于单机测试。 ```go package main import ( "github.com/gogf/gf/contrib/rpc/grpcx/v2" "github.com/gogf/gf/example/rpc/grpcx/basic/protobuf" "github.com/gogf/gf/v2/frame/g" "github.com/gogf/gf/v2/os/gctx" ) func main() { var ( ctx = gctx.New() conn = grpcx.Client.MustNewGrpcClientConn("demo") client = protobuf.NewGreeterClient(conn) ) res, err := client.SayHello(ctx, &protobuf.HelloRequest{Name: "World"}) if err != nil { g.Log().Error(ctx, err) return } g.Log().Debug(ctx, "Response:", res.Message) } ``` #### 执行结果 服务端输出: 可以看到,服务端输出了一些 `DEBU` 级别的调试信息,用于显示服务注册的详细信息。由于没有显式指定服务端的监听端口,这里随机监听了本地端口 `64517`。 ```bash $ go run server.go 2023-03-14 20:50:58.465 [DEBU] set default registry using file registry as no custom registry set 2023-03-14 20:50:58.466 [DEBU] service register: &{Head: Deployment: Namespace: Name:demo Version: Endpoints:10.35.12.81:64517 Metadata:map[protocol:grpc]} 2023-03-14 20:50:58.466 [INFO] pid[98982]: grpc server started listening on [:64517] 2023-03-14 20:52:37.059 {9898c809364a4c17da79e47f3e6c3b8f} /protobuf.Greeter/SayHello, 0.003ms, name:"World", message:"Hello World" ``` 客户端输出: 客户端通过微服务名称访问,并成功接收到了服务端的响应。需要注意的是,客户端和服务端日志中的链路跟踪 ID( `TraceID`)是相同的( `9898c809364a4c17da79e47f3e6c3b8f`),这表明它们属于同一个请求链路。 `GoFrame` 框架的微服务特性默认开启了链路跟踪能力,方便开发者进行请求链的追踪和问题排查。 ```bash $ go run client.go 2023-03-14 20:52:37.060 [DEBU] {9898c809364a4c17da79e47f3e6c3b8f} Response: Hello World ``` # 环境准备 后续微服务章节主要以 `gRPC` 协议为主,介绍微服务的开发、组件及工具使用。 ## 依赖安装 在进一步学习之前,请先了解 `gRPC` 相关概念,并在本地开发环境中安装对应的工具: - [https://grpc.io/](https://grpc.io/) - [https://grpc.io/docs/languages/go/quickstart/](https://grpc.io/docs/languages/go/quickstart/) 如果是 `MacOS` 环境,可以考虑使用 `brew` 工具安装依赖: ``` brew install grpc protoc-gen-go protoc-gen-go-grpc ``` 依赖工具安装完成后,请参考 [https://grpc.io/docs/languages/go/quickstart/](https://grpc.io/docs/languages/go/quickstart/) 章节完成基础的 `protoc` 工具使用学习。 ## 框架工具 `GoFrame` 框架的 `CLI` 开发工具请保证版本 `>= v2.4`。工具安装、升级请参考章节:[开发工具](../cli.md) 框架工具针对 `GRPC` 协议,提供了额外的命令支持,极大地简化了基于 `GRPC` 协议的微服务开发。 # 工程管理 ## 基本介绍 基于微服务开发的工程目录采用统一的框架工程目录结构,具体请参考章节: [工程目录设计](../design/project-structure.md) 我们这里以项目 [https://github.com/gogf/gf-demo-grpc](https://github.com/gogf/gf-demo-grpc) 为例说明。 ## 协议文件 协议文件定义在 `manifest/protobuf` 目录下。协议文件的目录路径遵循规则: `模块名/版本号/xxx.proto`,其中版本号采用 `v1`/ `v2` 的形式进行管理,便于维护接口的版本兼容性。 其中,涉及数据表结构的 `protobuf` 文件,由框架开发工具自动生成后存放在 `manifest/pbentity` 目录下。 ## 接口文件 通过 `proto` 协议文件编译生成的接口文档统一存放在 `api` 目录下。 ## 开发工具 ### 生成数据表结构 我们可以通过 `gf gen pbentity` 命令(或使用 `make pbentity` )自动根据数据库中的数据表结构生成对应的 `protobuf` 协议文件。该命令会读取数据库表结构信息,并自动生成符合 protobuf 规范的消息定义文件。命令详细介绍请参考章节: [数据表PB-gen pbentity](../cli/gen-pbentity.md) ### 编译 `proto` 协议文件 框架提供了 `gf gen pb` 命令(或使用 `make pb` )自动编译 `proto` 协议文件,并生成对应的接口文件以及控制器文件。该命令会扫描项目中的所有 proto 文件,使用 protoc 编译器生成 Go 语言的 gRPC 接口代码,并自动创建对应的控制器模板文件。 ## 服务的启动 服务的启动控制通过 `cmd` 目录进行维护,例如: [https://github.com/gogf/gf-demo-grpc/blob/main/internal/cmd/cmd.go](https://github.com/gogf/gf-demo-grpc/blob/main/internal/cmd/cmd.go) 在 cmd 文件中,我们需要初始化 gRPC 服务器,配置服务地址和端口,并注册所有的服务实现。 ## 接口的实现与注册 控制器用于实现 `proto` 文件中定义的接口。控制器文件可以通过 `gf gen pb` 命令自动生成,同时会自动生成 `Register` 方法,该方法用于将具体的接口实现注册到 gRPC 服务对象中。 注册方法示例: 在服务启动时进行注册: ## 标签注入与数据校验 ### 标签自动注入 使用 `gf gen pb` 命令(或 `make pb` )进行 `proto` 文件编译时,支持自动的标签注入功能。只需要通过注释的形式写到 `proto` 文件中,这些注释将会自动作为 `dc` 标签嵌入到生成的结构体属性中。如果注释规则形如 `xxx:yyy`,那么将会自动生成对应的 `xxx` 标签。例如: 生成后的结构体将包含相应的标签: 需要注意,在 `gRPC` 协议中,由于输入输出均采用结构体形式,因此无法实现像 `HTTP` 服务那样的默认值特性。 ### 数据校验插件 数据校验插件会根据标签中设置的规则对请求进行自动校验。该插件需要在服务端通过拦截器的形式手动引入: # 脚手架模块 `GoFrame` 框架包含多个微服务组件,并提供了易用的 `gRPC` 脚手架模块和工具。脚手架由 `grpcx` 社区包实现: [https://github.com/gogf/gf/tree/master/contrib/rpc/grpcx](https://github.com/gogf/gf/tree/master/contrib/rpc/grpcx) 包含多个模块。 ## 服务端- `Server` 服务端由 `grpcx.Server` 模块维护,用于实现服务端对象的创建与维护。使用示例: [https://github.com/gogf/gf/blob/master/example/rpc/grpcx/basic/server/main.go](https://github.com/gogf/gf/blob/master/example/rpc/grpcx/basic/server/main.go) 服务端的创建往往结合配置文件一起使用,关于服务端配置文件的介绍请参考章节: [服务端配置](./config.md) ```go package main import ( "github.com/gogf/gf/contrib/rpc/grpcx/v2" "github.com/gogf/gf/example/rpc/grpcx/basic/controller" ) func main() { s := grpcx.Server.New() controller.Register(s) s.Run() } ``` ## 客户端- `Client` 客户端由 `grpcx.Client` 模块维护,用于实现客户端对象的创建与维护。使用示例: [https://github.com/gogf/gf/blob/master/example/rpc/grpcx/basic/client/main.go](https://github.com/gogf/gf/blob/master/example/rpc/grpcx/basic/client/main.go) 在微服务架构中,大部分场景下服务间的访问使用服务名称而非直接的地址。 ```go package main import ( "github.com/gogf/gf/contrib/rpc/grpcx/v2" "github.com/gogf/gf/example/rpc/grpcx/basic/protobuf" "github.com/gogf/gf/v2/frame/g" "github.com/gogf/gf/v2/os/gctx" ) func main() { var ( ctx = gctx.New() conn = grpcx.Client.MustNewGrpcClientConn("demo") client = protobuf.NewGreeterClient(conn) ) res, err := client.SayHello(ctx, &protobuf.HelloRequest{Name: "World"}) if err != nil { g.Log().Error(ctx, err) return } g.Log().Debug(ctx, "Response:", res.Message) } ``` **注意** 常见问题 **是否需要保存复用创建的 `Client` 对象?** 每个 `gRPC Client` 对象实际上对应一个目标服务的访问连接。在应用中,应该将该对象保存下来复用,而不是每次请求都新建 `Client` 对象。这样可以提高效率、降低资源使用、减轻 `GC` 压力。 ## 上下文- `Ctx` 上下文由 `grpcx.Ctx` 模块维护,用于实现客户端与服务端之间、服务与服务之间的自定义数据传递。包含以下常用方法: ```go func (c Ctx) NewIncoming(ctx context.Context, data ...g.Map) context.Context func (c Ctx) NewOutgoing(ctx context.Context, data ...g.Map) context.Context func (c Ctx) IncomingToOutgoing(ctx context.Context, keys ...string) context.Context func (c Ctx) IncomingMap(ctx context.Context) *gmap.Map func (c Ctx) OutgoingMap(ctx context.Context) *gmap.Map func (c Ctx) SetIncoming(ctx context.Context, data g.Map) context.Context func (c Ctx) SetOutgoing(ctx context.Context, data g.Map) context.Context ``` 其中 `Outgoing` 用于客户端,表示将要传递给服务端的自定义数据,一般为 `Key-Value` 键值对组成的 `Map` 数据格式。`Incoming` 用于服务端,表示服务端接收到的客户端提交数据。框架相关的一些内嵌信息也会写入到 `Incoming` 键值对中,例如链路跟踪信息、客户端版本信息等。使用示例: `server.go` ```go package main import ( "github.com/gogf/gf/contrib/rpc/grpcx/v2" "github.com/gogf/gf/example/rpc/grpcx/ctx/controller" ) func main() { s := grpcx.Server.New() controller.Register(s) s.Run() } ``` `controller.go` ```go package controller import ( "context" "github.com/gogf/gf/contrib/rpc/grpcx/v2" "github.com/gogf/gf/example/rpc/grpcx/ctx/protobuf" "github.com/gogf/gf/v2/frame/g" ) type Controller struct { protobuf.UnimplementedGreeterServer } func Register(s *grpcx.GrpcServer) { protobuf.RegisterGreeterServer(s.Server, &Controller{}) } // SayHello implements helloworld.GreeterServer func (s *Controller) SayHello(ctx context.Context, in *protobuf.HelloRequest) (*protobuf.HelloReply, error) { m := grpcx.Ctx.IncomingMap(ctx) g.Log().Infof(ctx, `incoming data: %v`, m.Map()) return &protobuf.HelloReply{Message: "Hello " + in.GetName()}, nil } ``` `client.go` ```go package main import ( "github.com/gogf/gf/contrib/rpc/grpcx/v2" "github.com/gogf/gf/example/rpc/grpcx/ctx/protobuf" "github.com/gogf/gf/v2/frame/g" "github.com/gogf/gf/v2/os/gctx" ) func main() { var ( conn = grpcx.Client.MustNewGrpcClientConn("demo") client = protobuf.NewGreeterClient(conn) ctx = grpcx.Ctx.NewOutgoing(gctx.New(), g.Map{ "UserId": "1000", "UserName": "john", }) ) g.Log().Infof(ctx, `outgoing data: %v`, grpcx.Ctx.OutgoingMap(ctx).Map()) res, err := client.SayHello(ctx, &protobuf.HelloRequest{Name: "World"}) if err != nil { g.Log().Error(ctx, err) return } g.Log().Debug(ctx, "Response:", res.Message) } ``` 执行后,服务端输出: ```bash $ go run server.go 2023-03-15 19:28:45.331 [DEBU] set default registry using file registry as no custom registry set 2023-03-15 19:28:45.331 [DEBU] service register: &{Head: Deployment: Namespace: Name:demo Version: Endpoints:10.35.12.81:51707 Metadata:map[protocol:grpc]} 2023-03-15 19:28:45.332 [INFO] pid[83753]: grpc server started listening on [:51707] 2023-03-15 19:28:54.093 [INFO] {d049db1a39944c1711bd9f37d58a88f9} incoming data: map[:authority:service/default/default/demo/latest/ content-type:application/grpc traceparent:00-d049db1a39944c1711bd9f37d58a88f9-adbd2ba657ffea45-01 user-agent:grpc-go/1.49.0 userid:1000 username:john] 2023-03-15 19:28:54.093 {d049db1a39944c1711bd9f37d58a88f9} /protobuf.Greeter/SayHello, 0.049ms, name:"World", message:"Hello World" ``` 客户端输出: ```bash $ go run client.go 2023-03-15 19:28:54.087 [INFO] {d049db1a39944c1711bd9f37d58a88f9} outgoing data: map[userid:1000 username:john] 2023-03-15 19:28:54.089 [DEBU] client conn updated with addresses [{"Addr":"10.35.12.81:51707","ServerName":"demo","Attributes":{},"BalancerAttributes":null,"Type":0,"Metadata":null}] 2023-03-15 19:28:54.093 [DEBU] {d049db1a39944c1711bd9f37d58a88f9} Response: Hello World ``` ## 负载均衡- `Balancer` 负载均衡由 `grpcx.Balancer` 模块维护,用于实现当服务端存在多个访问地址时,客户端根据何种策略进行请求。当客户端未设置负载均衡策略时,默认使用轮询策略。关于负载均衡的详细介绍请参考章节: [服务负载均衡](./load-balance.md) 以下使用**随机策略**作为示例。使用随机策略时,各个服务端实例接收到的请求数分布不一定均衡,但从概率角度上能够实现负载分散。 `server.go` ```go package main import ( "github.com/gogf/gf/contrib/rpc/grpcx/v2" "github.com/gogf/gf/example/rpc/grpcx/balancer/controller" ) func main() { s := grpcx.Server.New() controller.Register(s) s.Run() } ``` `client.go` ```go package main import ( "context" "github.com/gogf/gf/contrib/rpc/grpcx/v2" "github.com/gogf/gf/example/rpc/grpcx/balancer/protobuf" "github.com/gogf/gf/v2/frame/g" "github.com/gogf/gf/v2/os/gctx" ) func main() { var ( ctx context.Context conn = grpcx.Client.MustNewGrpcClientConn("demo", grpcx.Balancer.WithRandom()) client = protobuf.NewGreeterClient(conn) ) for i := 0; i < 10; i++ { ctx = gctx.New() res, err := client.SayHello(ctx, &protobuf.HelloRequest{Name: "World"}) if err != nil { g.Log().Error(ctx, err) return } g.Log().Debug(ctx, "Response:", res.Message) } } ``` 其中的 `grpcx.Balancer.WithRandom()` 表示使用随机的请求策略。 启动两个 `server.go` 服务端,随后运行 `client.go` 客户端,查看服务端的请求日志: `server1` 终端输出: ```bash $ go run server.go 2023-03-15 19:50:44.801 [DEBU] set default registry using file registry as no custom registry set 2023-03-15 19:50:44.802 [DEBU] service register: &{Head: Deployment: Namespace: Name:demo Version: Endpoints:10.35.12.81:53962 Metadata:map[protocol:grpc]} 2023-03-15 19:50:44.802 [INFO] pid[89290]: grpc server started listening on [:53962] 2023-03-15 19:50:57.282 {7025612f6d954c17c5f335051bf10899} /protobuf.Greeter/SayHello, 0.003ms, name:"World", message:"Hello World" 2023-03-15 19:50:57.283 {60567c2f6d954c17c7f335052ce05185} /protobuf.Greeter/SayHello, 0.002ms, name:"World", message:"Hello World" 2023-03-15 19:50:57.285 {f8d09b2f6d954c17ccf33505dff1a4ea} /protobuf.Greeter/SayHello, 0.002ms, name:"World", message:"Hello World" 2023-03-15 19:50:57.287 {f0fab02f6d954c17cdf33505438b2c80} /protobuf.Greeter/SayHello, 0.001ms, name:"World", message:"Hello World" ``` `server2` 终端输出: ```bash $ go run server.go 2023-03-15 19:50:51.720 [DEBU] set default registry using file registry as no custom registry set 2023-03-15 19:50:51.721 [DEBU] service register: &{Head: Deployment: Namespace: Name:demo Version: Endpoints:10.35.12.81:53973 Metadata:map[protocol:grpc]} 2023-03-15 19:50:51.722 [INFO] pid[89351]: grpc server started listening on [:53973] 2023-03-15 19:50:57.280 {b89a0d2f6d954c17c4f33505a046817c} /protobuf.Greeter/SayHello, 0.002ms, name:"World", message:"Hello World" 2023-03-15 19:50:57.282 {28bf732f6d954c17c6f33505adedff5f} /protobuf.Greeter/SayHello, 0.002ms, name:"World", message:"Hello World" 2023-03-15 19:50:57.283 {9876832f6d954c17c8f3350580ed535b} /protobuf.Greeter/SayHello, 0.002ms, name:"World", message:"Hello World" 2023-03-15 19:50:57.284 {684e8b2f6d954c17c9f33505d56e4b05} /protobuf.Greeter/SayHello, 0.001ms, name:"World", message:"Hello World" 2023-03-15 19:50:57.284 {c045912f6d954c17caf3350599006197} /protobuf.Greeter/SayHello, 0.001ms, name:"World", message:"Hello World" 2023-03-15 19:50:57.284 {500a972f6d954c17cbf33505252b0e01} /protobuf.Greeter/SayHello, 0.001ms, name:"World", message:"Hello World" ``` 客户端终端输出: ```bash $ go run client.go 2023-03-15 19:50:57.278 [DEBU] client conn updated with addresses [{"Addr":"10.35.12.81:53962","ServerName":"demo","Attributes":{},"BalancerAttributes":null,"Type":0,"Metadata":null},{"Addr":"10.35.12.81:53973","ServerName":"demo","Attributes":{},"BalancerAttributes":null,"Type":0,"Metadata":null}] 2023-03-15 19:50:57.281 [DEBU] {b89a0d2f6d954c17c4f33505a046817c} Response: Hello World 2023-03-15 19:50:57.282 [DEBU] {7025612f6d954c17c5f335051bf10899} Response: Hello World 2023-03-15 19:50:57.282 [DEBU] {28bf732f6d954c17c6f33505adedff5f} Response: Hello World 2023-03-15 19:50:57.283 [DEBU] {60567c2f6d954c17c7f335052ce05185} Response: Hello World 2023-03-15 19:50:57.283 [DEBU] {9876832f6d954c17c8f3350580ed535b} Response: Hello World 2023-03-15 19:50:57.284 [DEBU] {684e8b2f6d954c17c9f33505d56e4b05} Response: Hello World 2023-03-15 19:50:57.284 [DEBU] {c045912f6d954c17caf3350599006197} Response: Hello World 2023-03-15 19:50:57.285 [DEBU] {500a972f6d954c17cbf33505252b0e01} Response: Hello World 2023-03-15 19:50:57.286 [DEBU] {f8d09b2f6d954c17ccf33505dff1a4ea} Response: Hello World 2023-03-15 19:50:57.287 [DEBU] {f0fab02f6d954c17cdf33505438b2c80} Response: Hello World ``` 可以看到,客户端发送了 `10` 次请求,两个服务端分别接收到了 `4` 次和 `6` 次请求,请求的负载比较随机。 ## 注册发现- `Resolver` 注册发现由 `grpcx.Resolver` 模块维护,该模块用于 `gRPC` 服务名称解析。`grpcx` 组件默认情况下使用本地文件系统实现注册发现,仅用于测试使用。如果是生产环境,则需要使用其他的注册发现组件。一个简单示例: `server.go` ```go package main import ( "github.com/gogf/gf/contrib/registry/etcd/v2" "github.com/gogf/gf/contrib/rpc/grpcx/v2" "github.com/gogf/gf/example/rpc/grpcx/resolver/controller" ) func main() { grpcx.Resolver.Register(etcd.New("127.0.0.1:2379")) s := grpcx.Server.New() controller.Register(s) s.Run() } ``` 其中的 `grpcx.Resolver.Register(etcd.New("127.0.0.1:2379"))` 用于设置服务注册发现组件为 `etcd`,仅支持 `gRPC` 协议。 `client.go` ```go package main import ( "github.com/gogf/gf/contrib/registry/etcd/v2" "github.com/gogf/gf/contrib/rpc/grpcx/v2" "github.com/gogf/gf/example/rpc/grpcx/resolver/protobuf" "github.com/gogf/gf/v2/frame/g" "github.com/gogf/gf/v2/os/gctx" ) func main() { grpcx.Resolver.Register(etcd.New("127.0.0.1:2379")) var ( ctx = gctx.New() conn = grpcx.Client.MustNewGrpcClientConn("demo") client = protobuf.NewGreeterClient(conn) ) res, err := client.SayHello(ctx, &protobuf.HelloRequest{Name: "World"}) if err != nil { g.Log().Error(ctx, err) return } g.Log().Debug(ctx, "Response:", res.Message) } ``` 启动 `etcd`: ```bash $ etcd {"level":"info","ts":"2023-03-15T20:02:50.966+0800","caller":"etcdmain/etcd.go:73","msg":"Running: ","args":["etcd"]} {"level":"info","ts":"2023-03-15T20:02:50.967+0800","caller":"etcdmain/etcd.go:100","msg":"failed to detect default host","error":"default host not supported on darwin_amd64"} {"level":"warn","ts":"2023-03-15T20:02:50.967+0800","caller":"etcdmain/etcd.go:105","msg":"'data-dir' was empty; using default","data-dir":"default.etcd"} {"level":"info","ts":"2023-03-15T20:02:50.967+0800","caller":"etcdmain/etcd.go:116","msg":"server has been already initialized","data-dir":"default.etcd","dir-type":"member"} {"level":"info","ts":"2023-03-15T20:02:50.967+0800","caller":"embed/etcd.go:124","msg":"configuring peer listeners","listen-peer-urls":["http://localhost:2380"]} {"level":"info","ts":"2023-03-15T20:02:50.968+0800","caller":"embed/etcd.go:132","msg":"configuring client listeners","listen-client-urls":["http://localhost:2379"]} ... ``` 运行 `server.go`: ```bash $ go run server.go 2023-03-15 20:02:19.472 [DEBU] service register: &{Head: Deployment: Namespace: Name:demo Version: Endpoints:10.35.12.81:55066 Metadata:map[protocol:grpc]} 2023-03-15 20:02:19.516 [DEBU] etcd put success with key "/service/default/default/demo/latest/10.35.12.81:55066", value "{"protocol":"grpc"}", lease "7587869265929863945" 2023-03-15 20:02:19.516 [INFO] pid[92040]: grpc server started listening on [:55066] 2023-03-15 20:02:29.310 {88c4c04e0e964c17dddec53b1adb54f7} /protobuf.Greeter/SayHello, 0.001ms, name:"World", message:"Hello World" ``` 运行 `client.go`: ```bash $ go run client.go 2023-03-15 20:02:29.297 [DEBU] Watch key "/service/default/default/demo/latest/" 2023-03-15 20:02:29.307 [DEBU] client conn updated with addresses [{"Addr":"10.35.12.81:55066","ServerName":"demo","Attributes":{},"BalancerAttributes":null,"Type":0,"Metadata":null}] 2023-03-15 20:02:29.308 [DEBU] client conn updated with addresses [{"Addr":"10.35.12.81:55066","ServerName":"demo","Attributes":{},"BalancerAttributes":null,"Type":0,"Metadata":null}] 2023-03-15 20:02:29.310 [DEBU] {88c4c04e0e964c17dddec53b1adb54f7} Response: Hello World ``` # 服务端配置 ## 基本介绍 gRPC 服务端支持配置文件管理,所有的配置项将会自动映射到 `GrpcServerConfig` 配置对象中。配置对象定义如下: ```go // GrpcServerConfig is the configuration for server. type GrpcServerConfig struct { Address string // (optional) Address for server listening. Name string // (optional) Name for current service. Logger *glog.Logger // (optional) Logger for server. LogPath string // (optional) LogPath specifies the directory for storing logging files. LogStdout bool // (optional) LogStdout specifies whether printing logging content to stdout. ErrorStack bool // (optional) ErrorStack specifies whether logging stack information when error. ErrorLogEnabled bool // (optional) ErrorLogEnabled enables error logging content to files. ErrorLogPattern string // (optional) ErrorLogPattern specifies the error log file pattern like: error-{Ymd}.log AccessLogEnabled bool // (optional) AccessLogEnabled enables access logging content to file. AccessLogPattern string // (optional) AccessLogPattern specifies the error log file pattern like: access-{Ymd}.log Endpoints []string // (optional) Endpoints are custom endpoints for service register, it uses Address if empty. } ``` 配置文件的自动读取逻辑与框架一致,详细介绍请参考章节: [配置管理](../core/gcfg.md) ## 配置模板 一个完整的配置模板示例如下,包含基础配置和日志扩展配置: ```yaml grpc: name: "demo" # 服务名称 address: ":8000" # 自定义服务监听地址 logPath: "./log" # 日志存储目录路径 logStdout: true # 日志是否输出到终端 errorLogEnabled: true # 是否开启错误日志记录 accessLogEnabled: true # 是否开启访问日志记录 errorStack: true # 当产生错误时,是否记录错误堆栈 endpoints: ["192.168.1.3:8000"] # 自定义服务注册地址列表 # 日志扩展配置(参数日志组件配置) logger: path: "/var/log/" # 日志文件路径。默认为空,表示关闭,仅输出到终端 file: "{Y-m-d}.log" # 日志文件格式。默认为"{Y-m-d}.log" prefix: "" # 日志内容输出前缀。默认为空 level: "all" # 日志输出级别 stdout: true # 日志是否同时输出到终端。默认true rotateSize: 0 # 按照日志文件大小对文件进行滚动切分。默认为0,表示关闭滚动切分特性 rotateExpire: 0 # 按照日志文件时间间隔对文件滚动切分。默认为0,表示关闭滚动切分特性 rotateBackupLimit: 0 # 按照切分的文件数量清理切分文件,当滚动切分特性开启时有效。默认为0,表示不备份,切分则删除 rotateBackupExpire: 0 # 按照切分的文件有效期清理切分文件,当滚动切分特性开启时有效。默认为0,表示不备份,切分则删除 rotateBackupCompress: 0 # 滚动切分文件的压缩比(0-9)。默认为0,表示不压缩 rotateCheckInterval: "1h" # 滚动切分的时间检测间隔,一般不需要设置。默认为1小时 ``` 其中的日志配置与 `HTTP Server` 保持一致,可以独立使用日志组件的配置项来配置 `gRPC Server` 的日志。关于日志组件的详细配置说明,请参考文档: [日志组件-配置管理](../core/glog-config.md) ## 注意事项 ### 访问端口配置 在没有配置 `address` 的情况下, `gRPC Server` 将会使用本地网卡的所有 `IP` 地址加上**随机空闲端口**来启动(默认配置为 `:0` )。如果想要指定 `IP` 地址但使用随机空闲端口启动 `gRPC Server`,可以使用 `ip:0` 的格式来配置 `address`,例如: `192.168.1.1:0`、 `10.0.1.1:0`。 ### 服务注册地址 服务注册地址用于对外提供服务发现和访问,可以通过 `endpoints` 配置项自定义服务注册地址。 在没有配置 `endpoints` 的情况下, `gRPC Server` 将会使用 `address` 的地址来注册服务。如果没有指定 `address` 配置,或者使用的是 `0.0.0.0` 的 `address` 配置,那么将会自动获取本机的所有局域网 `IP` 地址作为服务注册的地址。如果对注册的地址有特定要求,可以通过 `endpoints` 自定义服务注册的地址。 # 拦截器组件 `gRPC` 支持拦截器(Interceptor)特性,允许开发者在 RPC 调用前后添加自定义逻辑,从而提高了 `gRPC` 的灵活性和可扩展性。拦截器类似于 `HTTP` 服务中的中间件,可用于实现日志记录、认证验证、性能监控、错误处理等通用功能。 ## 拦截器使用 ### 服务端 使用 `grpcx.Server.ChainUnary` 方法为服务端添加额外的一元拦截器(Unary Interceptor)。拦截器将按照添加顺序依次执行,形成拦截器链: ```go c := grpcx.Server.NewConfig() c.Options = append(c.Options, []grpc.ServerOption{ grpcx.Server.ChainUnary( grpcx.Server.UnaryValidate, )}..., ) s := grpcx.Server.New(c) user.Register(s) s.Run() ``` ### 客户端 使用 `grpcx.Client.ChainUnary` 方法为客户端添加额外的一元拦截器。客户端拦截器可以在发送请求前后执行自定义逻辑: ```go conn = grpcx.Client.MustNewGrpcClientConn("demo", grpcx.Client.ChainUnary( grpcx.Client.UnaryTracing, )) ``` ## 拦截器列表 框架的 `grpcx` 组件提供了一系列常用的拦截器,部分为内置默认启用,部分可选择性引入使用。 | 拦截器 | 使用位置 | 是否内置 | 功能描述 | | --- | --- | --- | --- | | `UnaryError` | 客户端 | 是 | 集成框架错误处理组件,用于统一错误处理。 | | `UnaryTracing` | 客户端 | 是 | 支持分布式链路跟踪,自动传播 TraceID。 | | `StreamTracing` | 客户端 | 是 | 支持流式 RPC 的链路跟踪(长连接)。 | | `UnaryError` | 服务端 | 是 | 集成框架错误处理组件,用于统一错误处理。 | | `UnaryRecover` | 服务端 | 是 | 自动捕获服务端处理过程中的 `panic`,防止服务崩溃。 | | `UnaryAllowNilRes` | 服务端 | 是 | 允许返回 `nil` 的响应对象,提高灵活性。 | | `UnaryValidate` | 服务端 | 否 | 基于结构体标签的自动数据校验,需要手动引入。 | | `UnaryTracing` | 服务端 | 是 | 支持分布式链路跟踪,自动接收和传播 TraceID。 | | `StreamTracing` | 服务端 | 是 | 支持流式 RPC 的链路跟踪(长连接)。 | # 服务注册发现 ## 基本介绍 `GoFrame` 框架提供了服务注册发现组件,由 `gsvc` 组件负责管理。该组件主要定义了服务注册发现的标准接口,采用接口化设计,具体实现由社区组件提供。开发者可以在 [https://github.com/gogf/gf/tree/master/contrib/registry](https://github.com/gogf/gf/tree/master/contrib/registry) 中找到多种注册发现的实现,包括 `etcd`、 `zookeeper`、 `polaris`、 `consul`、 `nacos` 等主流注册中心。开发者可以根据项目需求灵活选择,也可以根据 `gsvc` 组件的接口定义实现自己的注册发现组件。 ## 组件启用 服务注册发现组件采用插件化设计,只有在引入具体的接口实现并配置后才会启用。例如,使用 `etcd` 实现注册发现的方式如下: ```go package main import ( "github.com/gogf/gf/contrib/registry/etcd/v2" "github.com/gogf/gf/v2/net/gsvc" ) func main() { gsvc.SetRegistry(etcd.New(`127.0.0.1:2379`)) // ... } ``` ## 常用组件 | 组件名称 | 文档说明 | 备注 | | --- | --- | --- | | `file` | [https://github.com/gogf/gf/tree/master/contrib/registry/file](https://github.com/gogf/gf/tree/master/contrib/registry/file) | 仅用于单机测试 | | `etcd` | [https://github.com/gogf/gf/tree/master/contrib/registry/etcd](https://github.com/gogf/gf/tree/master/contrib/registry/etcd) | | | `polaris` | [https://github.com/gogf/gf/tree/master/contrib/registry/polaris](https://github.com/gogf/gf/tree/master/contrib/registry/polaris) | | | `zookeeper` | [https://github.com/gogf/gf/tree/master/contrib/registry/zookeeper](https://github.com/gogf/gf/tree/master/contrib/registry/zookeeper) | | | `consul` | [https://github.com/gogf/gf/tree/master/contrib/registry/consul](https://github.com/gogf/gf/tree/master/contrib/registry/consul) | | | `nacos` | [https://github.com/gogf/gf/tree/master/contrib/registry/nacos](https://github.com/gogf/gf/tree/master/contrib/registry/nacos) | | 更多组件,请参考: [https://github.com/gogf/gf/tree/master/contrib/registry](https://github.com/gogf/gf/tree/master/contrib/registry) ## 使用示例 ### `HTTP` 可以使用 `gsvc.SetRegistry(etcd.New(`127.0.0.1:2379`))` 设置使用 `etcd` 作为注册发现的实现。其中 `etcd.New` 方法通过社区组件创建一个 `gsvc.Registry` 接口的实现对象,然后通过 `gsvc.SetRegistry` 方法将其设置为全局默认的注册发现组件。 `server.go` ```go package main import ( "github.com/gogf/gf/contrib/registry/etcd/v2" "github.com/gogf/gf/v2/frame/g" "github.com/gogf/gf/v2/net/ghttp" "github.com/gogf/gf/v2/net/gsvc" ) func main() { gsvc.SetRegistry(etcd.New(`127.0.0.1:2379`)) s := g.Server(`hello.svc`) s.BindHandler("/", func(r *ghttp.Request) { g.Log().Info(r.Context(), `request received`) r.Response.Write(`Hello world`) }) s.Run() } ``` `client.go` 客户端同样需要设置相同的注册发现组件,并通过 `client.SetDiscovery` 方法启用服务发现功能: ```go package main import ( "github.com/gogf/gf/v2/frame/g" "github.com/gogf/gf/v2/net/gsvc" "github.com/gogf/gf/v2/os/gctx" "github.com/gogf/gf/contrib/registry/etcd/v2" ) func main() { gsvc.SetRegistry(etcd.New(`127.0.0.1:2379`)) var ( ctx = gctx.New() client = g.Client() ) client.SetDiscovery(gsvc.GetRegistry()) res := client.GetContent(ctx, `http://hello.svc/`) g.Log().Info(ctx, res) } ``` 执行后,服务端输出: ```bash $ go run server.go 2023-03-15 20:55:56.256 [INFO] pid[3358]: http server started listening on [:60700] 2023-03-15 20:55:56.256 [INFO] openapi specification is disabled 2023-03-15 20:55:56.256 [DEBU] service register: &{Head: Deployment: Namespace: Name:hello.svc Version: Endpoints:10.35.12.81:60700 Metadata:map[insecure:true protocol:http]} 2023-03-15 20:55:56.297 [DEBU] etcd put success with key "/service/default/default/hello.svc/latest/10.35.12.81:60700", value "{"insecure":true,"protocol":"http"}", lease "7587869265945813002" SERVER | DOMAIN | ADDRESS | METHOD | ROUTE | HANDLER | MIDDLEWARE ------------|---------|---------|--------|-------|-----------------------------------------------------------------|-------------------- hello.svc | default | :60700 | ALL | / | main.main.func1 | ------------|---------|---------|--------|-------|-----------------------------------------------------------------|-------------------- hello.svc | default | :60700 | ALL | /* | github.com/gogf/gf/v2/net/ghttp.internalMiddlewareServerTracing | GLOBAL MIDDLEWARE ------------|---------|---------|--------|-------|-----------------------------------------------------------------|-------------------- 2023-03-15 20:56:45.739 [INFO] {880eaa8104994c17ffb384495cd4c613} request received ``` 客户端输出: ```bash $ go run client.go 2023-03-15 20:56:45.739 [INFO] {880eaa8104994c17ffb384495cd4c613} Hello world ``` ### `GRPC` **注意** 对于 `gRPC` 协议,必须使用 `grpcx.Resolver` 模块来设置服务注册发现组件。当服务端在 `config.yaml` 中没有设置 `grpc.name` 时,默认服务名称为 `default`。 `server.go` 代码中的 `etcd.New` 方法通过社区组件创建一个 `gsvc.Registry` 接口的实现对象,然后通过 `grpcx.Resolver.Register` 方法设置为全局的 `gRPC` 注册发现组件。 ```go package main import ( "github.com/gogf/gf/contrib/registry/etcd/v2" "github.com/gogf/gf/contrib/rpc/grpcx/v2" "github.com/gogf/gf/example/registry/etcd/grpc/controller" ) func main() { grpcx.Resolver.Register(etcd.New("127.0.0.1:2379")) s := grpcx.Server.New() controller.Register(s) s.Run() } ``` `config.yaml` 服务端默认的配置文件: ```yaml grpc: name: "demo" # 服务名称 address: ":8000" # 自定义服务监听地址 logPath: "./log" # 日志存储目录路径 logStdout: true # 日志是否输出到终端 errorLogEnabled: true # 是否开启错误日志记录 accessLogEnabled: true # 是否开启访问日志记录 errorStack: true # 当产生错误时,是否记录错误堆栈 ``` `client.go` ```go package main import ( "github.com/gogf/gf/contrib/registry/etcd/v2" "github.com/gogf/gf/contrib/rpc/grpcx/v2" "github.com/gogf/gf/example/registry/etcd/grpc/protobuf" "github.com/gogf/gf/v2/frame/g" "github.com/gogf/gf/v2/os/gctx" ) func main() { grpcx.Resolver.Register(etcd.New("127.0.0.1:2379")) var ( ctx = gctx.New() conn = grpcx.Client.MustNewGrpcClientConn("demo") client = protobuf.NewGreeterClient(conn) ) res, err := client.SayHello(ctx, &protobuf.HelloRequest{Name: "World"}) if err != nil { g.Log().Error(ctx, err) return } g.Log().Debug(ctx, "Response:", res.Message) } ``` 执行后,服务端输出: ```bash $ go run server.go 2023-03-15 21:06:57.204 [DEBU] service register: &{Head: Deployment: Namespace: Name:demo Version: Endpoints:10.35.12.81:61978 Metadata:map[protocol:grpc]} 2023-03-15 21:06:57.257 [DEBU] etcd put success with key "/service/default/default/demo/latest/10.35.12.81:61978", value "{"protocol":"grpc"}", lease "7587869265945813015" 2023-03-15 21:06:57.257 [INFO] pid[5786]: grpc server started listening on [:61978] 2023-03-15 21:07:04.955 {08f0aead94994c1731591d2b653ddc18} /protobuf.Greeter/SayHello, 0.002ms, name:"World", message:"Hello World" ``` 客户端输出: ```bash $ go run client.go 2023-03-15 21:07:04.950 [DEBU] Watch key "/service/default/default/demo/latest/" 2023-03-15 21:07:04.952 [DEBU] client conn updated with addresses [{"Addr":"10.35.12.81:61978","ServerName":"demo","Attributes":{},"BalancerAttributes":null,"Type":0,"Metadata":null}] 2023-03-15 21:07:04.953 [DEBU] client conn updated with addresses [{"Addr":"10.35.12.81:61978","ServerName":"demo","Attributes":{},"BalancerAttributes":null,"Type":0,"Metadata":null}] 2023-03-15 21:07:04.955 [DEBU] {08f0aead94994c1731591d2b653ddc18} Response: Hello World ``` # 服务负载均衡 ## 基本介绍 负载均衡组件应用于客户端,用于将请求合理分配到多个服务实例上。 `GoFrame` 框架提供了解耦化设计、灵活性高、扩展性强的负载均衡组件,由 `gsel` 组件负责管理。该组件定义了负载均衡的标准接口,并提供了多种内置的负载均衡策略实现。开发者也可以根据接口定义实现自定义的负载均衡策略。 ## 策略列表 `gsel` 组件提供了多种常用的负载均衡策略,开发者可以根据实际场景选择使用: | 策略名称 | 策略描述 | | --- | --- | | `LeastConnection` | 最小连接数策略,选择当前连接数最少的服务实例。 | | `Random` | 随机访问策略,从可用服务列表中随机选择一个实例。 | | `RoundRobin` | 轮询访问策略,按顺序循环选择服务实例。 | | `Weight` | 权重访问策略,根据服务权重分配请求。服务注册时需要设置 `Weight` 参数。 | ## 使用示例 ### `HTTP` `server.go` ```go package main import ( "github.com/gogf/gf/contrib/registry/etcd/v2" "github.com/gogf/gf/v2/frame/g" "github.com/gogf/gf/v2/net/ghttp" "github.com/gogf/gf/v2/net/gsvc" ) func main() { gsvc.SetRegistry(etcd.New(`127.0.0.1:2379`)) s := g.Server(`hello.svc`) s.BindHandler("/", func(r *ghttp.Request) { g.Log().Info(r.Context(), `request received`) r.Response.Write(`Hello world`) }) s.Run() } ``` `client.go` 这里使用 `gsel.SetBuilder(gsel.NewBuilderRoundRobin())` 设置全局的负载均衡策略为轮询访问策略。客户端的所有请求将按顺序分配到不同的服务实例。 ```go package main import ( "github.com/gogf/gf/contrib/registry/etcd/v2" "github.com/gogf/gf/v2/frame/g" "github.com/gogf/gf/v2/net/gsel" "github.com/gogf/gf/v2/net/gsvc" "github.com/gogf/gf/v2/os/gctx" ) func main() { gsvc.SetRegistry(etcd.New(`127.0.0.1:2379`)) gsel.SetBuilder(gsel.NewBuilderRoundRobin()) client := g.Client() client.SetDiscovery(gsvc.GetRegistry()) for i := 0; i < 10; i++ { ctx := gctx.New() res := client.GetContent(ctx, `http://hello.svc/`) g.Log().Info(ctx, res) } } ``` 分别启动两个服务端,并执行客户端。 `server1` 终端输出: ```bash $ go run server.go 2023-03-15 21:24:08.413 [INFO] pid[10219]: http server started listening on [:63956] 2023-03-15 21:24:08.413 [INFO] openapi specification is disabled 2023-03-15 21:24:08.413 [DEBU] service register: &{Head: Deployment: Namespace: Name:hello.svc Version: Endpoints:10.35.12.81:63956 Metadata:map[insecure:true protocol:http]} 2023-03-15 21:24:08.455 [DEBU] etcd put success with key "/service/default/default/hello.svc/latest/10.35.12.81:63956", value "{"insecure":true,"protocol":"http"}", lease "7587869265945813020" SERVER | DOMAIN | ADDRESS | METHOD | ROUTE | HANDLER | MIDDLEWARE ------------|---------|---------|--------|-------|-----------------------------------------------------------------|-------------------- hello.svc | default | :63956 | ALL | / | main.main.func1 | ------------|---------|---------|--------|-------|-----------------------------------------------------------------|-------------------- hello.svc | default | :63956 | ALL | /* | github.com/gogf/gf/v2/net/ghttp.internalMiddlewareServerTracing | GLOBAL MIDDLEWARE ------------|---------|---------|--------|-------|-----------------------------------------------------------------|-------------------- 2023-03-15 21:24:18.357 [INFO] {e05b6049859a4c17d1de5d62eafa5a5f} request received 2023-03-15 21:24:18.358 [INFO] {785e9349859a4c17d3de5d62049e5b51} request received 2023-03-15 21:24:18.360 [INFO] {7076ab49859a4c17d5de5d62aaa64c85} request received 2023-03-15 21:24:18.360 [INFO] {205fb849859a4c17d7de5d62cb2590f4} request received 2023-03-15 21:24:18.361 [INFO] {885fc349859a4c17d9de5d6235937e31} request received ``` `server2` 终端输出: ```bash $ go run server.go 2023-03-15 21:24:10.769 [INFO] pid[10242]: http server started listening on [:63964] 2023-03-15 21:24:10.770 [INFO] openapi specification is disabled 2023-03-15 21:24:10.770 [DEBU] service register: &{Head: Deployment: Namespace: Name:hello.svc Version: Endpoints:10.35.12.81:63964 Metadata:map[insecure:true protocol:http]} 2023-03-15 21:24:10.812 [DEBU] etcd put success with key "/service/default/default/hello.svc/latest/10.35.12.81:63964", value "{"insecure":true,"protocol":"http"}", lease "7587869265945813023" SERVER | DOMAIN | ADDRESS | METHOD | ROUTE | HANDLER | MIDDLEWARE ------------|---------|---------|--------|-------|-----------------------------------------------------------------|-------------------- hello.svc | default | :63964 | ALL | / | main.main.func1 | ------------|---------|---------|--------|-------|-----------------------------------------------------------------|-------------------- hello.svc | default | :63964 | ALL | /* | github.com/gogf/gf/v2/net/ghttp.internalMiddlewareServerTracing | GLOBAL MIDDLEWARE ------------|---------|---------|--------|-------|-----------------------------------------------------------------|-------------------- 2023-03-15 21:24:18.357 [INFO] {602d8749859a4c17d2de5d62d515e464} request received 2023-03-15 21:24:18.359 [INFO] {e0ed9b49859a4c17d4de5d628284ae62} request received 2023-03-15 21:24:18.360 [INFO] {e0e0b249859a4c17d6de5d62beda3001} request received 2023-03-15 21:24:18.361 [INFO] {7087bd49859a4c17d8de5d62f892e8aa} request received 2023-03-15 21:24:18.361 [INFO] {e8aec849859a4c17dade5d6247101836} request received ``` 客户端终端输出: ```bash $ go run client.go 2023-03-15 21:24:18.357 [INFO] {e05b6049859a4c17d1de5d62eafa5a5f} Hello world 2023-03-15 21:24:18.358 [INFO] {602d8749859a4c17d2de5d62d515e464} Hello world 2023-03-15 21:24:18.358 [INFO] {785e9349859a4c17d3de5d62049e5b51} Hello world 2023-03-15 21:24:18.359 [INFO] {e0ed9b49859a4c17d4de5d628284ae62} Hello world 2023-03-15 21:24:18.360 [INFO] {7076ab49859a4c17d5de5d62aaa64c85} Hello world 2023-03-15 21:24:18.360 [INFO] {e0e0b249859a4c17d6de5d62beda3001} Hello world 2023-03-15 21:24:18.360 [INFO] {205fb849859a4c17d7de5d62cb2590f4} Hello world 2023-03-15 21:24:18.361 [INFO] {7087bd49859a4c17d8de5d62f892e8aa} Hello world 2023-03-15 21:24:18.361 [INFO] {885fc349859a4c17d9de5d6235937e31} Hello world 2023-03-15 21:24:18.361 [INFO] {e8aec849859a4c17dade5d6247101836} Hello world ``` ### `GRPC` `server.go` ```go package main import ( "github.com/gogf/gf/contrib/rpc/grpcx/v2" "github.com/gogf/gf/example/rpc/grpcx/balancer/controller" ) func main() { s := grpcx.Server.New() controller.Register(s) s.Run() } ``` `client.go` ```go package main import ( "context" "github.com/gogf/gf/contrib/rpc/grpcx/v2" "github.com/gogf/gf/example/rpc/grpcx/balancer/protobuf" "github.com/gogf/gf/v2/frame/g" "github.com/gogf/gf/v2/os/gctx" ) func main() { var ( ctx context.Context conn = grpcx.Client.MustNewGrpcClientConn("demo", grpcx.Balancer.WithRandom()) client = protobuf.NewGreeterClient(conn) ) for i := 0; i < 10; i++ { ctx = gctx.New() res, err := client.SayHello(ctx, &protobuf.HelloRequest{Name: "World"}) if err != nil { g.Log().Error(ctx, err) return } g.Log().Debug(ctx, "Response:", res.Message) } } ``` 其中的 `grpcx.Balancer.WithRandom()` 表示使用随机请求策略,每次请求将随机选择一个可用的服务实例。启动两个 `server.go` 服务端,然后运行 `client.go` 客户端,查看服务端的请求日志: `server1` 终端输出: ```bash $ go run server.go 2023-03-15 19:50:44.801 [DEBU] set default registry using file registry as no custom registry set 2023-03-15 19:50:44.802 [DEBU] service register: &{Head: Deployment: Namespace: Name:demo Version: Endpoints:10.35.12.81:53962 Metadata:map[protocol:grpc]} 2023-03-15 19:50:44.802 [INFO] pid[89290]: grpc server started listening on [:53962] 2023-03-15 19:50:57.282 {7025612f6d954c17c5f335051bf10899} /protobuf.Greeter/SayHello, 0.003ms, name:"World", message:"Hello World" 2023-03-15 19:50:57.283 {60567c2f6d954c17c7f335052ce05185} /protobuf.Greeter/SayHello, 0.002ms, name:"World", message:"Hello World" 2023-03-15 19:50:57.285 {f8d09b2f6d954c17ccf33505dff1a4ea} /protobuf.Greeter/SayHello, 0.002ms, name:"World", message:"Hello World" 2023-03-15 19:50:57.287 {f0fab02f6d954c17cdf33505438b2c80} /protobuf.Greeter/SayHello, 0.001ms, name:"World", message:"Hello World" ``` `server2` 终端输出: ```bash $ go run server.go 2023-03-15 19:50:51.720 [DEBU] set default registry using file registry as no custom registry set 2023-03-15 19:50:51.721 [DEBU] service register: &{Head: Deployment: Namespace: Name:demo Version: Endpoints:10.35.12.81:53973 Metadata:map[protocol:grpc]} 2023-03-15 19:50:51.722 [INFO] pid[89351]: grpc server started listening on [:53973] 2023-03-15 19:50:57.280 {b89a0d2f6d954c17c4f33505a046817c} /protobuf.Greeter/SayHello, 0.002ms, name:"World", message:"Hello World" 2023-03-15 19:50:57.282 {28bf732f6d954c17c6f33505adedff5f} /protobuf.Greeter/SayHello, 0.002ms, name:"World", message:"Hello World" 2023-03-15 19:50:57.283 {9876832f6d954c17c8f3350580ed535b} /protobuf.Greeter/SayHello, 0.002ms, name:"World", message:"Hello World" 2023-03-15 19:50:57.284 {684e8b2f6d954c17c9f33505d56e4b05} /protobuf.Greeter/SayHello, 0.001ms, name:"World", message:"Hello World" 2023-03-15 19:50:57.284 {c045912f6d954c17caf3350599006197} /protobuf.Greeter/SayHello, 0.001ms, name:"World", message:"Hello World" 2023-03-15 19:50:57.284 {500a972f6d954c17cbf33505252b0e01} /protobuf.Greeter/SayHello, 0.001ms, name:"World", message:"Hello World" ``` 客户端终端输出: ```bash $ go run client.go 2023-03-15 19:50:57.278 [DEBU] client conn updated with addresses [{"Addr":"10.35.12.81:53962","ServerName":"demo","Attributes":{},"BalancerAttributes":null,"Type":0,"Metadata":null},{"Addr":"10.35.12.81:53973","ServerName":"demo","Attributes":{},"BalancerAttributes":null,"Type":0,"Metadata":null}] 2023-03-15 19:50:57.281 [DEBU] {b89a0d2f6d954c17c4f33505a046817c} Response: Hello World 2023-03-15 19:50:57.282 [DEBU] {7025612f6d954c17c5f335051bf10899} Response: Hello World 2023-03-15 19:50:57.282 [DEBU] {28bf732f6d954c17c6f33505adedff5f} Response: Hello World 2023-03-15 19:50:57.283 [DEBU] {60567c2f6d954c17c7f335052ce05185} Response: Hello World 2023-03-15 19:50:57.283 [DEBU] {9876832f6d954c17c8f3350580ed535b} Response: Hello World 2023-03-15 19:50:57.284 [DEBU] {684e8b2f6d954c17c9f33505d56e4b05} Response: Hello World 2023-03-15 19:50:57.284 [DEBU] {c045912f6d954c17caf3350599006197} Response: Hello World 2023-03-15 19:50:57.285 [DEBU] {500a972f6d954c17cbf33505252b0e01} Response: Hello World 2023-03-15 19:50:57.286 [DEBU] {f8d09b2f6d954c17ccf33505dff1a4ea} Response: Hello World 2023-03-15 19:50:57.287 [DEBU] {f0fab02f6d954c17cdf33505438b2c80} Response: Hello World ``` # 服务配置管理 ## 基本介绍 `GoFrame` 框架提供了功能强大的配置管理组件,采用了解耦化和接口化设计,可以灵活对接各种第三方配置管理中心。配置管理组件默认提供基于本地系统文件的实现,更多的第三方配置中心实现请参考社区组件: [https://github.com/gogf/gf/tree/master/contrib/config](https://github.com/gogf/gf/tree/master/contrib/config) 社区组件提供了常用的多种配置中心实现,例如 `Polaris`、 `Apollo`、 `Nacos`、 `Consul` 以及容器编排的 `Kubernetes ConfigMap`。 ## 组件启用 配置管理组件的启用采用了包初始化的方式。由于配置管理功能比较底层,需要保证社区组件包在 `main` 包的最顶部引入,确保配置组件在其他组件之前初始化。我们这里以 `Polaris` 为例,社区组件的使用说明: [https://github.com/gogf/gf/tree/master/contrib/config/polaris](https://github.com/gogf/gf/tree/master/contrib/config/polaris) 需要提供一个独立的引入包,例如 `boot`: ```go package boot import ( "github.com/gogf/gf/contrib/config/polaris/v2" "github.com/gogf/gf/v2/frame/g" "github.com/gogf/gf/v2/os/gctx" ) func init() { var ( ctx = gctx.GetInitCtx() namespace = "default" fileGroup = "TestGroup" fileName = "config.yaml" path = "manifest/config/polaris.yaml" logDir = "/tmp/polaris/log" ) // Create polaris Client that implements gcfg.Adapter. adapter, err := polaris.New(ctx, polaris.Config{ Namespace: namespace, FileGroup: fileGroup, FileName: fileName, Path: path, LogDir: logDir, Watch: true, }) if err != nil { g.Log().Fatalf(ctx, `%+v`, err) } // Change the adapter of default configuration instance. g.Cfg().SetAdapter(adapter) } ``` 其中配置参数说明: - `Namespace` 指定的是 `Polaris` 配置中的命名空间。 - `FileGroup` 指定的是 `Polaris` 中的文件分组。 - `FileName` 指定的是需要读取的 `Polaris` 中的配置文件名称。 - `Path` 指定的是 `Polaris` 服务端配置文件路径,包括 Polaris 的链接地址、监听地址、组件输出日志路径等。 `Polaris` 的服务端配置文件内容如下: ```yaml global: serverConnector: addresses: - 127.0.0.1:8091 config: configConnector: addresses: - 127.0.0.1:8093 consumer: localCache: persistDir: "/tmp/polaris/backup" ``` 随后在 `main.go` 顶部引入 `boot` 包,保证该包的引入在所有组件的最前面: ```go package main import ( _ "github.com/gogf/gf/example/config/polaris/boot" "github.com/gogf/gf/v2/frame/g" "github.com/gogf/gf/v2/os/gctx" ) func main() { var ctx = gctx.GetInitCtx() // Available checks. g.Dump(g.Cfg().Available(ctx)) // All key-value configurations. g.Dump(g.Cfg().Data(ctx)) // Retrieve certain value by key. g.Dump(g.Cfg().MustGet(ctx, "server.address")) } ``` 注意其中顶部 `import` 的引入语句: ``` _ "github.com/gogf/gf/example/config/polaris/boot" ``` ## 常用组件 | 组件名称 | 文档说明 | 备注 | | --- | --- | --- | | `file` | 框架内置 | 默认实现 | | `apollo` | [https://github.com/gogf/gf/tree/master/contrib/config/apollo](https://github.com/gogf/gf/tree/master/contrib/config/apollo) | | | `kubecm` | [https://github.com/gogf/gf/tree/master/contrib/config/kubecm](https://github.com/gogf/gf/tree/master/contrib/config/kubecm) | 常用于容器部署环境 | | `nacos` | [https://github.com/gogf/gf/tree/master/contrib/config/nacos](https://github.com/gogf/gf/tree/master/contrib/config/nacos) | | | `polaris` | [https://github.com/gogf/gf/tree/master/contrib/config/polaris](https://github.com/gogf/gf/tree/master/contrib/config/polaris) | | | `consul` | [https://github.com/gogf/gf/tree/master/contrib/config/consul](https://github.com/gogf/gf/tree/master/contrib/config/consul) | | 更多组件,请参考: [https://github.com/gogf/gf/tree/master/contrib/config](https://github.com/gogf/gf/tree/master/contrib/config) ## 使用示例 [https://github.com/gogf/gf/tree/master/example/config/polaris](https://github.com/gogf/gf/tree/master/example/config/polaris) ### 运行 `polaris` ```bash docker run -d --name polaris -p 8080:8080 -p 8090:8090 -p 8091:8091 -p 8093:8093 loads/polaris-server-standalone:1.11.2 ``` ### 运行示例 ```bash $ go run main.go true {} "failed to update local value: config file is empty" panic: failed to update local value: config file is empty goroutine 1 [running]: github.com/gogf/gf/v2/os/gcfg.(*Config).MustGet(0x0?, {0x1c1c4f8?, 0xc0000c2000?}, {0x1ac11ad?, 0x0?}, {0x0?, 0xc000002340?, 0xc000064738?}) /Users/john/Workspace/gogf/gf/os/gcfg/gcfg.go:167 +0x5e main.main() /Users/john/Workspace/gogf/gf/example/config/polaris/main.go:20 +0x1b8 ``` 可以看到最后的 `MustGet` 方法执行报错,这是因为 `Polaris` 配置中心里并没有指定的命名空间、配置分组以及配置文件。即使查询不到任何数据,但由于涉及到配置问题因此返回了错误。由于这里使用的是 `Must*` 方法,在执行返回错误时将会直接 `panic` 而不是返回错误。 那么我们进入 `polaris` 的后台添加一些测试数据再试试。 ### 添加测试数据 进入 [http://127.0.0.1:8080/#/login](http://127.0.0.1:8080/#/login) 默认账号 `polaris` 密码 `polaris` ### 再次运行示例 ```bash $ go run main.go true { "server": { "openapiPath": "/api.json", "swaggerPath": "/swagger", "address": ":8199", }, } ":8199" ``` 可以看到已经正确查询到了 `Polaris` 配置中心中的配置数据。 # 服务可观测性 作为一款工程完备的开发框架,开发者通过 `GoFrame` 可以非常简便地实现服务的可观测特性。 # 服务链路跟踪 `GoFrame` 实现了标准化的分布式链路跟踪( `Distributed Tracing`)特性。 # 链路跟踪-背景知识 ## OpenTelemetry 分布式链路跟踪(`Distributed Tracing`)的概念最早是由 `Google` 提出来的,发展至今技术已经比较成熟,也是有一些协议标准可以参考。目前在 `Tracing` 技术这块比较有影响力的是两大开源技术框架:`Netflix` 公司开源的 `OpenTracing` 和 `Google` 开源的 `OpenCensus`。两大框架都拥有比较高的开发者群体。为形成统一的技术标准,两大框架最终磨合成立了 `OpenTelemetry` 项目,简称 `otel`。具体可以参考: 1. [OpenTracing介绍](https://johng.cn/observability/opentracing-introduction) 2. [OpenTelemetry介绍](https://johng.cn/observability/opentelemetry-introduction) 因此,我们的 `Tracing` 技术方案以 `OpenTelemetry` 为实施标准,协议标准的一些 `Golang` 实现开源项目: 1. [https://github.com/open-telemetry/opentelemetry-go](https://github.com/open-telemetry/opentelemetry-go) 2. [https://github.com/open-telemetry/opentelemetry-go-contrib](https://github.com/open-telemetry/opentelemetry-go-contrib) 其他第三方的框架和系统(如 `Jaeger/Prometheus/Grafana` 等)也会按照标准化的规范来对接 `OpenTelemetry`,使得系统的开发和维护成本大大降低。 ## 重要概念 我们先看看 `OpenTelemetry` 的架构图,我们这里不会完整介绍,只会介绍其中大家常用的几个概念。 关于 `OpenTelemetry` 的内部技术架构设计介绍,可以参考 [OpenTelemetry架构](https://johng.cn/observability/opentelemetry-architecture), 关于语义约定请参考: [https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/trace/api.md](https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/trace/api.md) ### TracerProvider 主要负责创建 `Tracer`,一般是需要第三方的分布式链路跟踪管理平台提供具体的实现。默认情况是一个空的 `TracerProvider (NoopTracerProvider)`,虽然也能创建 `Tracer` 但是内部其实不会执行具体的数据流传输逻辑。 ### Tracer `Tracer` 是创建和管理 `Span` 的接口。一次完整的追踪链路(Trace)由一个或多个 `Span` 组成。下图示例表示了一个由 `8` 个 `Span` 组成的追踪链路: ``` [Span A] ←←←(the root span) | +------+------+ | | [Span B] [Span C] ←←←(Span C is a `ChildOf` Span A) | | [Span D] +---+-------+ | | [Span E] [Span F] >>> [Span G] >>> [Span H] ↑ ↑ ↑ (Span G `FollowsFrom` Span F) ``` 时间轴的展现方式会更容易理解: ``` ––|–––––––|–––––––|–––––––|–––––––|–––––––|–––––––|–––––––|–> time [Span A···················································] [Span B··············································] [Span D··········································] [Span C········································] [Span E·······] [Span F··] [Span G··] [Span H··] ``` 我们通常通过以下方式创建一个 `Tracer`: ``` gtrace.NewTracer(tracerName) ``` ### Span `Span` 是一条追踪链路中的基本组成要素,一个 `Span` 表示一个独立的工作单元,比如可以表示一次函数调用、一次 `HTTP` 请求等。`Span` 会记录如下基本要素: - 操作名称(`Operation Name`) - 服务的开始时间和结束时间 - `Attributes`(属性,`K/V` 键值对形式) - `Events`(事件,`K/V` 键值对形式) - `SpanContext`(跨进程传播的上下文信息) `Span` 是这么多对象中使用频率最高的,因此创建 `Span` 也非常简便,例如: ``` gtrace.NewSpan(ctx, spanName, opts...) ``` ### Attributes `Attributes` 以 `K/V` 键值对的形式保存用户自定义标签,主要用于链路追踪结果的查询过滤。例如:`http.method="GET"`, `http.status_code=200`。其中 `key` 值必须为字符串,`value` 必须是字符串、布尔型或者数值型。`Span` 中的 `Attributes` 仅自己可见,不会随着 `SpanContext` 传递给后续 `Span`。设置 `Attributes` 方式例如: ``` span.SetAttributes( label.String("http.remote", conn.RemoteAddr().String()), label.String("http.local", conn.LocalAddr().String()), ) ``` ### Events `Events` 与 `Attributes` 类似,也是 `K/V` 键值对形式。与 `Attributes` 不同的是,`Events` 还会记录写入事件的时间戳,因此 `Events` 主要用于记录某些事件发生的时刻。`Events` 的 `key` 值同样必须为字符串,但对 `value` 类型则没有限制。例如: ``` span.AddEvent("http.request", trace.WithAttributes( label.Any("http.request.header", headers), label.Any("http.request.baggage", gtrace.GetBaggageMap(ctx)), label.String("http.request.body", bodyContent), )) ``` ### SpanContext `SpanContext` 携带着一些用于**跨服务通信(跨进程)**的数据,主要包含: - 足够在系统中标识该 `Span` 的信息,比如:`SpanId`、`TraceId`。 - `Baggage` - 为整条追踪链保存跨服务(跨进程)的 `K/V` 格式的用户自定义数据。`Baggage` 与 `Attributes` 类似,也是 `K/V` 键值对。与 `Attributes` 不同的是: - 其 `key` 和 `value` 都只能是字符串格式 - `Baggage` 不仅当前 `Span` 可见,其会随着 `SpanContext` 传递给后续所有的子 `Span`。要小心谨慎地使用 `Baggage` - 因为在所有的 `Span` 中传递这些 `K/V` 会带来不小的网络和 `CPU` 开销。 ### Propagator `Propagator` 传播器用于端对端的上下文数据编码/解码,例如:`Client` 到 `Server` 端的数据传输,`TraceId`、`SpanId` 和 `Baggage` 都需要通过传播器来管理数据传输。业务端开发者往往对 `Propagator` 无感知,只有中间件/拦截器的开发者需要了解它的作用。`OpenTelemetry` 的标准协议实现库提供了常用的 `TextMapPropagator`,用于常见的文本数据端到端传输。此外,为保证 `TextMapPropagator` 中的传输数据兼容性,不应当包含特殊字符,具体请参考:[https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/context/api-propagators.md](https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/context/api-propagators.md) `GoFrame` 框架通过 `gtrace` 模块使用了以下传播器对象,并全局设置到了 `OpenTelemetry` 中: ```go // defaultTextMapPropagator is the default propagator for context propagation between peers. defaultTextMapPropagator = propagation.NewCompositeTextMapPropagator( propagation.TraceContext{}, propagation.Baggage{}, ) ``` ## 支持组件 **提示** `GoFrame` 的核心组件都已经全面支持 `OpenTelemetry` 标准,并且 **自动开启** 了链路跟踪特性,开发者无需显示调用、使用无感知。在没有注入外部 `TracerProvider` 的情况下,框架会使用默认的 `TracerProvider`,该 `TracerProvider` 只会自动创建 `TraceID` 及 `SpanID`,以便打通请求日志的链路,并不会执行复杂逻辑。 包括但不限于以下核心组件: | 自动支持链路跟踪特性的组件 | 组件名 | 描述 | | --- | --- | --- | | `HTTP Client` | `gclient` | `HTTP` 客户端自动启用了链路跟踪特性,具体使用示例请参考后续示例章节。 | | `HTTP Server` | `ghttp` | `HTTP` 服务端自动启用了链路跟踪特性,具体使用示例请参考后续示例章节。 | | `gRPC Client` | `contrib/rpc/grpcx` | `gRPC` 客户端自动启用了链路跟踪特性,具体使用示例请参考后续示例章节。 | | `gRPC Server` | `contrib/rpc/grpcx` | `gRPC` 服务端自动启用了链路跟踪特性,具体使用示例请参考后续示例章节。 | | `Logging` | `glog` | 日志内容中需要注入当前请求的 `TraceId`,以便通过日志快速定位问题。该特性由 `glog` 组件实现,开发者在输出日志时需要调用 `Ctx` 链式操作方法将 `context.Context` 上下文变量传递到日志输出链路中。如果没有传递 `context.Context` 上下文变量,日志内容中将会丢失 `TraceId`。 | | `ORM` | `gdb` | 数据库的执行是很重要的链路环节,`ORM` 组件会将自身的执行情况记录到链路中,作为执行链路的一部分。 | | `NoSQL Redis` | `gredis` | `Redis` 的执行也是很重要的链路环节,`Redis` 组件会将自身的执行情况记录到链路中,作为执行链路的一部分。 | | `Utils` | `gtrace` | 对于 `Tracing` 特性的管理需要做一定的封装,主要考虑的是可扩展性和易用性两方面。该封装由 `gtrace` 模块实现,文档地址: [https://pkg.go.dev/github.com/gogf/gf/v2/net/gtrace](https://pkg.go.dev/github.com/gogf/gf/v2/net/gtrace) | ## 参考资料 - [https://opentracing.io](https://opentracing.io/) - [https://opencensus.io](https://opencensus.io/) - [https://opentelemetry.io](https://opentelemetry.io/) - [https://github.com/open-telemetry/opentelemetry-specification/tree/main/specification](https://github.com/open-telemetry/opentelemetry-specification/tree/main/specification) # 链路跟踪-准备工作 对 `OpenTelemetry` 的概念有初步了解后,我们接着以 `Jaeger` 为例来演示如何在程序中使用实现链路追踪。 ## Jaeger [Jaeger](https://www.jaegertracing.io/) \\u02c8yā-gər\\ 是 Uber 开源的分布式追踪系统,是支持 `OpenTelemetry` 的系统之一,也是 `CNCF` 项目。本篇将使用 `Jaeger` 来演示如何在系统中引入分布式追踪。以下是 `OpenTracing + Jaeger` 的架构图,针对于使用 `OpenTelemetry` 也是如此。 ## 准备工作 `Jaeger` 提供了 `all-in-one` 镜像,方便我们快速开始测试: ```bash docker run --rm --name jaeger \ -e COLLECTOR_ZIPKIN_HOST_PORT=:9411 \ -p 16686:16686 \ -p 4317:4317 \ -p 4318:4318 \ -p 14250:14250 \ -p 14268:14268 \ -p 14269:14269 \ -p 9411:9411 \ jaegertracing/all-in-one:1.64.0 ``` `all-in-one` V1 相关文档地址:[https://www.jaegertracing.io/docs/1.64/getting-started/](https://www.jaegertracing.io/docs/1.64/getting-started/) `all-in-one` V2 相关文档地址:[https://www.jaegertracing.io/docs/2.1/getting-started/](https://www.jaegertracing.io/docs/2.1/getting-started/) 如果 `Docker` 镜像拉取太慢,可以尝试修改 `Docker` 拉取站点的镜像地址,例如:[http://mirrors.ustc.edu.cn/help/dockerhub.html?highlight=docker](http://mirrors.ustc.edu.cn/help/dockerhub.html?highlight=docker) 镜像启动后,通过 [http://localhost:16686](http://localhost:16686/) 可以打开 `Jaeger UI`。 ## 示例代码地址 我们的示例代码在 `gf` 主库中,地址: [https://github.com/gogf/gf/tree/master/example/trace](https://github.com/gogf/gf/tree/master/example/trace) ## Jaeger 注册封装(即将在 v2.6.0 版本移除) 为方便开发者使用,我们通过社区模块的形式封装好了对 `Jaeger` 的初始化逻辑,代码地址:[https://github.com/gogf/gf/tree/master/contrib/trace/jaeger](https://github.com/gogf/gf/tree/master/contrib/trace/jaeger) ## OTLP HTTP 注册封装 ## OTLP HTTP 注册封装 为方便开发者使用,我们通过社区模块的形式封装好了对 `OTLP HTTP` 的初始化逻辑,代码地址:[https://github.com/gogf/gf/tree/master/contrib/trace/otlphttp](https://github.com/gogf/gf/tree/master/contrib/trace/otlphttp) ## OTLP GRPC 注册封装 为方便开发者使用,我们通过社区模块的形式封装好了对 `OTLP GRPC` 的初始化逻辑,代码地址:[https://github.com/gogf/gf/tree/master/contrib/trace/otlpgrpc](https://github.com/gogf/gf/tree/master/contrib/trace/otlpgrpc) # 链路跟踪-基本示例 ## 单进程示例 单进程的链路跟踪即进程内方法之间的调用链关系。这种场景的跟踪没有涉及到分布式跟踪,比较简单,以该示例作为我们入门的一个例子吧。示例代码地址: [https://github.com/gogf/gf/tree/master/example/trace/inprocess](https://github.com/gogf/gf/tree/master/example/trace/inprocess) ## Root Span `Root Span` 即链路中第一个 `Span` 对象。在这里的单进程场景中,往往需要手动创建一个。随后在方法内部创建的 `Span` 都会作为它的子级 `Span`。 **提示** 在分布式架构的服务间通信场景中,往往不需要开发者手动创建 `root span`,而是由客户端/服务端请求的拦截器来自动创建。 创建 `Tracer`,生成 `Root Span`: ```go func main() { const ( serviceName = "otlp-http-client" endpoint = "tracing-analysis-dc-hz.aliyuncs.com" path = "adapt_******_******/api/otlp/traces" ) var ctx = gctx.New() shutdown, err := otlphttp.Init(serviceName, endpoint, path) if err != nil { g.Log().Fatal(ctx, err) } defer shutdown() ctx, span := gtrace.NewSpan(ctx, "main") defer span.End() // Trace 1. user1 := GetUser(ctx, 1) g.Dump(user1) // Trace 2. user100 := GetUser(ctx, 100) g.Dump(user100) } ``` 上述代码创建了一个 `root span`,并将该 `span` 通过 `context` 传递给 `GetUser` 方法,以便在 `GetUser` 方法中将追踪链继续延续下去。 ## 方法间 Span 创建 ```go // GetUser retrieves and returns hard coded user data for demonstration. func GetUser(ctx context.Context, id int) g.Map { ctx, span := gtrace.NewSpan(ctx, "GetUser") defer span.End() m := g.Map{} gutil.MapMerge( m, GetInfo(ctx, id), GetDetail(ctx, id), GetScores(ctx, id), ) return m } // GetInfo retrieves and returns hard coded user info for demonstration. func GetInfo(ctx context.Context, id int) g.Map { ctx, span := gtrace.NewSpan(ctx, "GetInfo") defer span.End() if id == 100 { return g.Map{ "id": 100, "name": "john", "gender": 1, } } return nil } // GetDetail retrieves and returns hard coded user detail for demonstration. func GetDetail(ctx context.Context, id int) g.Map { ctx, span := gtrace.NewSpan(ctx, "GetDetail") defer span.End() if id == 100 { return g.Map{ "site": "https://goframe.org", "email": "john@goframe.org", } } return nil } // GetScores retrieves and returns hard coded user scores for demonstration. func GetScores(ctx context.Context, id int) g.Map { ctx, span := gtrace.NewSpan(ctx, "GetScores") defer span.End() if id == 100 { return g.Map{ "math": 100, "english": 60, "chinese": 50, } } return nil } ``` 该示例代码展示了多层级方法间的链路信息传递,即是把 `ctx` 上下文变量作为第一个方法参数传递即可。在方法内部,我们通过的固定语法来创建/开始一个 `Span`: ```go ctx, span := gtrace.NewSpan(ctx, "xxx") defer span.End() ``` 并通过 `defer` 的方式调用 `span.End` 来结束一个 `Span`,这样可以很好地记录 `Span` 生命周期(开始和结束)信息,这些信息都将会展示到链路跟踪系统中。其中 `gtrace.NewSpan` 方法的第二个参数 `spanName` 我们直接给定方法的名称即可,这样在链路展示中比较有识别性。 ## 效果查看 执行完上面的程序后,终端输出: 打开 `Jaeger UI`: [http://localhost:16686/search](http://localhost:16686/search),可以看到链路追踪的结果: 点击详情可以查看具体信息,包括 `span` 的调用顺序、调用关系,执行时间轴,以及记录一些Attributes和 `Events` 信息,极大的方便我们定位系统中的异常和发现性能瓶颈。: 其中的 `tracing-inprocess` 是我们 `Tracer` 的名称,该名称往往是服务名称,由于我们这里只有一个进程和一个 `Tracer`,因此这里只看得到一个服务名称。其中的 `main` 为我们创建的 `Root Span` 名称,其他的 `Span` 为基于该 `Root Span` 创建的子级 `Span`。由于我们在程序中调用了两次 `GetUser` 方法,因此这里也展示了两次 `GetUser` 方法的调用。每一次 `GetUser` 调用的内部又分别去调用了 `GetInfo`、`GetDetail`、`GetScores` 三个方法,方法间的调用层级关系展示得非常清晰明了,并且每个方法的调用时长都可以看得到。 关于其中每个 `span` 记录的 `Tags` 和 `Process` 信息其实对应了 `OpenTelemetry` 中的 `Attributes` 和 `Events` 信息,这些信息我们放到后续章节去详细介绍。 # 链路跟踪-GRPC示例 在本章节中,我们将之前介绍 `HTTP Client&Server` 的示例修改为 `GRPC` 微服务,并演示如何使用 `GoFrame` 框架开发一个简单的 `GRPC` 服务端和客户端,并且为 `GRPC` 微服务增加链路跟踪特性。 本章节的示例代码位于: [https://github.com/gogf/gf/tree/master/example/trace/grpc\_with\_db](https://github.com/gogf/gf/tree/master/example/trace/grpc_with_db) ## 目录结构 ## Protobuf ``` syntax = "proto3"; package user; option go_package = "protobuf/user"; // User service for tracing demo. service User { rpc Insert(InsertReq) returns (InsertRes) {} rpc Query(QueryReq) returns (QueryRes) {} rpc Delete(DeleteReq) returns (DeleteRes) {} } message InsertReq { string Name = 1; // v: required#Please input user name. } message InsertRes { int32 Id = 1; } message QueryReq { int32 Id = 1; // v: min:1#User id is required for querying. } message QueryRes { int32 Id = 1; string Name = 2; } message DeleteReq { int32 Id = 1; // v:min:1#User id is required for deleting. } message DeleteRes {} ``` 使用 `gf gen pb` 命令编译该 `proto` 文件,将会生成对应的 `grpc` 接口文件和数据结构文件。 ## GRPC Server ```go package main import ( _ "github.com/gogf/gf/contrib/drivers/mysql/v2" _ "github.com/gogf/gf/contrib/nosql/redis/v2" "github.com/gogf/gf/contrib/registry/etcd/v2" "github.com/gogf/gf/example/trace/grpc_with_db/protobuf/user" "context" "fmt" "time" "github.com/gogf/gf/contrib/rpc/grpcx/v2" "github.com/gogf/gf/contrib/trace/otlpgrpc/v2" "github.com/gogf/gf/v2/database/gdb" "github.com/gogf/gf/v2/frame/g" "github.com/gogf/gf/v2/os/gcache" "github.com/gogf/gf/v2/os/gctx" ) type Controller struct { user.UnimplementedUserServer } const ( serviceName = "otlp-grpc-server" endpoint = "tracing-analysis-dc-bj.aliyuncs.com:8090" traceToken = "******_******" ) func main() { grpcx.Resolver.Register(etcd.New("127.0.0.1:2379")) var ctx = gctx.New() shutdown, err := otlpgrpc.Init(serviceName, endpoint, traceToken) if err != nil { g.Log().Fatal(ctx, err) } defer shutdown() // Set ORM cache adapter with redis. g.DB().GetCache().SetAdapter(gcache.NewAdapterRedis(g.Redis())) s := grpcx.Server.New() user.RegisterUserServer(s.Server, &Controller{}) s.Run() } // Insert is a route handler for inserting user info into database. func (s *Controller) Insert(ctx context.Context, req *user.InsertReq) (res *user.InsertRes, err error) { result, err := g.Model("user").Ctx(ctx).Insert(g.Map{ "name": req.Name, }) if err != nil { return nil, err } id, _ := result.LastInsertId() res = &user.InsertRes{ Id: int32(id), } return } // Query is a route handler for querying user info. It firstly retrieves the info from redis, // if there's nothing in the redis, it then does db select. func (s *Controller) Query(ctx context.Context, req *user.QueryReq) (res *user.QueryRes, err error) { err = g.Model("user").Ctx(ctx).Cache(gdb.CacheOption{ Duration: 5 * time.Second, Name: s.userCacheKey(req.Id), Force: false, }).WherePri(req.Id).Scan(&res) if err != nil { return nil, err } return } // Delete is a route handler for deleting specified user info. func (s *Controller) Delete(ctx context.Context, req *user.DeleteReq) (res *user.DeleteRes, err error) { err = g.Model("user").Ctx(ctx).Cache(gdb.CacheOption{ Duration: -1, Name: s.userCacheKey(req.Id), Force: false, }).WherePri(req.Id).Scan(&res) return } func (s *Controller) userCacheKey(id int32) string { return fmt.Sprintf(`userInfo:%d`, id) } ``` 服务端代码简要说明: 1、首先,服务端需要通过 `jaeger.Init` 方法初始化 `Jaeger`。 2、可以看到,业务逻辑和之前HTTP示例项目完全一致,只是接入层修改为了GRPC协议。 3、我们仍然通过缓存适配器的方式注入Redis缓存: ```go g.DB().GetCache().SetAdapter(gcache.NewAdapterRedis(g.Redis())) ``` 5、这里也是通过 `Cache` 方法启用 `ORM` 的缓存特性,之前已经做过介绍,这里不再赘述。 ## GRPC Client ```go package main import ( "github.com/gogf/gf/contrib/registry/etcd/v2" "github.com/gogf/gf/contrib/rpc/grpcx/v2" "github.com/gogf/gf/contrib/trace/otlpgrpc/v2" "github.com/gogf/gf/example/trace/grpc_with_db/protobuf/user" "github.com/gogf/gf/v2/frame/g" "github.com/gogf/gf/v2/net/gtrace" "github.com/gogf/gf/v2/os/gctx" ) const ( serviceName = "otlp-grpc-client" endpoint = "tracing-analysis-dc-bj.aliyuncs.com:8090" traceToken = "******_******" ) func main() { grpcx.Resolver.Register(etcd.New("127.0.0.1:2379")) var ctx = gctx.New() shutdown, err := otlpgrpc.Init(serviceName, endpoint, traceToken) if err != nil { g.Log().Fatal(ctx, err) } defer shutdown() StartRequests() } func StartRequests() { ctx, span := gtrace.NewSpan(gctx.New(), "StartRequests") defer span.End() client := user.NewUserClient(grpcx.Client.MustNewGrpcClientConn("demo")) // Baggage. ctx = gtrace.SetBaggageValue(ctx, "uid", 100) // Insert. insertRes, err := client.Insert(ctx, &user.InsertReq{ Name: "john", }) if err != nil { g.Log().Fatalf(ctx, `%+v`, err) } g.Log().Info(ctx, "insert id:", insertRes.Id) // Query. queryRes, err := client.Query(ctx, &user.QueryReq{ Id: insertRes.Id, }) if err != nil { g.Log().Errorf(ctx, `%+v`, err) return } g.Log().Info(ctx, "query result:", queryRes) // Delete. _, err = client.Delete(ctx, &user.DeleteReq{ Id: insertRes.Id, }) if err != nil { g.Log().Errorf(ctx, `%+v`, err) return } g.Log().Info(ctx, "delete id:", insertRes.Id) // Delete with error. _, err = client.Delete(ctx, &user.DeleteReq{ Id: -1, }) if err != nil { g.Log().Errorf(ctx, `%+v`, err) return } g.Log().Info(ctx, "delete id:", -1) } ``` 客户端代码简要说明: 1、首先,客户端也是需要通过 `jaeger.Init` 方法初始化 `Jaeger`。 2、客户端非常简单,内部初始化以及默认拦截器的设置已经由 `Katyusha` 框架封装好了,开发者只需要关心业务逻辑实现即可, ## 效果查看 **启动服务端:** **启动客户端:** 这里客户端的执行最后报了一个错误,那是我们 **故意为之**,目的是演示 `GRPC` 报错时的链路信息展示。我们打开 `jaeger` 查看一下链路跟踪信息: 可以看到本次请求涉及到两个服务: `tracing-grpc-client` 和 `tracing-grpc-server`,即客户端和服务端。整个请求链路涉及到 `17` 个 `span`,客户端 `5` 个 `span`,服务端 `12` 个 `span`,并且产生了 `2` 个错误。我们点击查看详情: 我们点击查看一下最后接口调用错误的 `span` 情况: 看起来像个参数校验错误,点击查看 `Events/Logs` 中的请求参数: 查看 `Process` 中的 `Log` 信息可以看到,是由于传递的参数为 `-1`,不满足校验规则,因此在数据校验的时候报错返回了。 ### GRPC Client 由于 `orm`、 `redis`、 `logging` 组件在之前的章节中已经介绍过链路信息,因此我们这里主要介绍 `GRPC Client&Server` 的链路信息。 #### Attributes | Attribute/Tag | 说明 | | --- | --- | | `net.peer.ip` | 请求的目标IP。 | | `net.peer.port` | 请求的目标端口。 | | `rpc.grpc.status_code` | `GRPC` 的内部状态码, `0` 表示成功, `非0` 表示失败。 | | `rpc.service` | `RPC` 的服务名称,注意这里是 `RPC` 而不是 `GRPC`,因为这里是通用定义,客户端支持多种 `RPC` 通信协议, `GRPC` 只是其中一种。 | | `rpc.method` | `RPC` 的方法名称。 | | `rpc.system` | `RPC` 协议类型,如: `grpc`, `thrift` 等。 | #### Events/Logs | Event/Log | 说明 | | --- | --- | | `grpc.metadata.outgoing` | `GRPC` 客户端请求提交的 `Metadata` 信息,可能会比较大。 | | `grpc.request.baggage` | `GRPC` 客户端请求提交的 `Baggage` 信息,用于服务间链路信息传递。 | | `grpc.request.message` | `GRPC` 客户端请求提交的 `Message` 数据,可能会比较大,最大只记录 `512KB`,如果超过该大小则忽略。仅对 `Unary` 请求类型有效。 | | `grpc.response.message` | `GRPC` 客户端请求接收返回的的 `Message` 信息,可能会比较大。仅对 `Unary` 请求类型有效。 | ### GRPC Server #### Attributes `GRPC Server` 端的 `Attributes` 含义同 `GRPC Client`,在同一请求中,打印的数据基本一致。 #### Events `GRPC Server` 端的 `Events` 与 `GRPC Client` 不同的是,在同一请求中,服务端接收到的 `metadata` 为 `grpc.metadata.incoming`,其他同 `GRPC Client`。 # 链路跟踪-HTTP示例 在本章节中,我们演示两个示例,一个用于演示 `baggage` 服务间上下文数据传递;一个用于演示较完整的 `HTTP+DB+Redis+Logging` 组件的链路跟踪。 # 链路跟踪-HTTP示例-Baggage ## `baggage` 链路数据传递 `baggage` 用户链路间(服务间)传递自定义的信息。 示例代码地址: [https://github.com/gogf/gf/tree/master/example/trace/http](https://github.com/gogf/gf/tree/master/example/trace/http) ## 客户端 ```go package main import ( "github.com/gogf/gf/contrib/trace/otlphttp/v2" "github.com/gogf/gf/v2/frame/g" "github.com/gogf/gf/v2/net/gtrace" "github.com/gogf/gf/v2/os/gctx" ) const ( serviceName = "otlp-http-client" endpoint = "tracing-analysis-dc-hz.aliyuncs.com" path = "adapt_******_******/api/otlp/traces" ) func main() { var ctx = gctx.New() shutdown, err := otlphttp.Init(serviceName, endpoint, path) if err != nil { g.Log().Fatal(ctx, err) } defer shutdown() StartRequests() } func StartRequests() { ctx, span := gtrace.NewSpan(gctx.New(), "StartRequests") defer span.End() ctx = gtrace.SetBaggageValue(ctx, "name", "john") content := g.Client().GetContent(ctx, "http://127.0.0.1:8199/hello") g.Log().Print(ctx, content) } ``` 客户端代码简要说明: 1. 首先,客户端也是需要通过 `jaeger.Init` 方法初始化 `Jaeger`。 2. 随后,这里通过 `gtrace.SetBaggageValue(ctx, "name", "john")` 方法设置了一个 `baggage`,该 `baggage` 将会在该请求的所有链路中传递。不过我们该示例也有两个节点,因此该 `baggage` 数据只会传递到服务端。该方法会返回一个新的 `context.Context` 上下文变量,在随后的调用链中我们将需要传递这个新的上下文变量。 3. 其中,这里通过 `g.Client()` 创建一个HTTP客户端请求对象,该客户端会自动启用链路跟踪特性,无需开发者显示调用任何方法或者任何设置。 4. 最后,这里使用了 `g.Log().Print(ctx, content)` 方法打印服务端的返回内容,其中的 `ctx` 便是将链路信息传递给日志组件,如果 `ctx` 上下文对象中存在链路信息时,日志组件会同时自动将 `TraceId` 输出到日志内容中。 ## 服务端 ```go package main import ( "github.com/gogf/gf/contrib/trace/otlphttp/v2" "github.com/gogf/gf/v2/frame/g" "github.com/gogf/gf/v2/net/ghttp" "github.com/gogf/gf/v2/net/gtrace" "github.com/gogf/gf/v2/os/gctx" ) const ( serviceName = "otlp-http-server" endpoint = "tracing-analysis-dc-hz.aliyuncs.com" path = "adapt_******_******/api/otlp/traces" ) func main() { var ctx = gctx.New() shutdown, err := otlphttp.Init(serviceName, endpoint, path) if err != nil { g.Log().Fatal(ctx, err) } defer shutdown() s := g.Server() s.Group("/", func(group *ghttp.RouterGroup) { group.GET("/hello", HelloHandler) }) s.SetPort(8199) s.Run() } func HelloHandler(r *ghttp.Request) { ctx, span := gtrace.NewSpan(r.Context(), "HelloHandler") defer span.End() value := gtrace.GetBaggageVar(ctx, "name").String() r.Response.Write("hello:", value) } ``` 服务端代码简要说明: 1. 当然,服务端也是需要通过 `jaeger.Init` 方法初始化 `Jaeger`。 2. 服务端启动启用链路跟踪特性,开发者无需显示调用任何方法或任何设置。 3. 服务端通过 `gtrace.GetBaggageVar(ctx, "name").String()` 方法获取客户端提交的 `baggage` 信息,并转换为字符串返回。 ## 效果查看 **启动服务端:** **启动客户端:** 可以看到,客户端提交的 `baggage` 已经被服务端成功接收到并打印返回。并且客户端在输出日志内容的时候也同时输出的 `TraceId` 信息。 `TraceId` 是一条链路的唯一ID,可以通过该ID检索该链路的所有日志信息,并且也可以通过该 `TraceId` 在 `Jaeger` 系统上查询该调用链路详情。 在 `Jaeger` 上查看链路信息: 可以看到在这里出现了两个服务名称: `tracing-http-client` 和 `tracing-http-server`,表示我们这次请求涉及到两个服务,分别是HTTP请求的客户端和服务端,并且每个服务中分别涉及到 `2` 个 `span` 链路节点。 我们点击这个 `trace` 的详情,可以看得到调用链的层级关系。并且可以看得到客户端请求的地址、服务端接收的路由以及服务端路由函数名称。我们这里来介绍一下客户端的 `Atttributes` 信息和 `Events` 信息,也就是 `Jaeger` 中展示的 `Tags` 信息和 `Process` 信息。 ### HTTP Client Attributes | Attribute/Tag | 说明 | | --- | --- | | `otel.instrumentation_library.name` | 当前仪表器名称,往往是当前 `span` 操作的组件名称 | | `otel.instrumentation_library.version` | 当前仪表器组件版本 | | `span.kind` | 当前 `span` 的类型,一般由组件自动写入。常见 `span` 类型为: | 类型 | 说明 | | --- | --- | | `client ` | 客户端 | | `server` | 服务端 | | `producer` | 生产者,常用于MQ | | `consumer` | 消费者,常用于MQ | | `internal` | 内部方法,一般业务使用 | | `undefined` | 未定义,较少使用 | | | `status.code` | 当前 `span` 状态, `0` 为正常, `非0` 表示失败 | | `status.message` | 当前 `span` 状态信息,往往在失败时会带有错误信息 | | `hostname` | 当前节点的主机名称 | | `ip.intranet` | 当前节点的主机内网地址列表 | | `http.address.local` | HTTP通信的本地地址和端口 | | `http.address.remote` | HTTP通信的目标地址和端口 | | `http.dns.start` | 当请求的目标地址带有域名时,开始解析的域名地址 | | `http.dns.done` | 当请求的目标地址带有域名时,解析结束之后的IP地址 | | `http.connect.start` | 开始创建连接的类型和地址 | | `http.connect.done` | 创建连接成功后的类型和地址 | ### HTTP Client Events | Event/Log | 说明 | | --- | --- | | `http.request.headers` | HTTP客户端请求提交的 `Header` 信息,可能会比较大。 | | `http.request.baggage` | HTTP客户端请求提交的 `Baggage` 信息,用于服务间链路信息传递。 | | `http.request.body` | HTTP客户端请求提交的 `Body` 数据,可能会比较大,最大只记录 `512KB`,如果超过该大小则忽略。 | | `http.response.headers` | HTTP客户端请求接收返回的的 `Header` 信息,可能会比较大。 | | `http.response.body` | HTTP客户端请求接收返回的 `Body` 数据,可能会比较大,最大只记录 `512KB`,如果超过该大小则忽略。 | ### HTTP Server Attributes `HTTP Server` 端的 `Attributes` 含义同 `HTTP Client`,在同一请求中,打印的数据基本一致。 ### HTTP Server Events `HTTP Server` 端的 `Events` 含义同 `HTTP Client`,在同一请求中,打印的数据基本一致。 # 链路跟踪-HTTP示例-数据操作 ## `HTTP+DB+Redis+Logging` 我们再来看一个相对完整一点的例子,包含几个常用核心组件的链路跟踪示例,示例代码地址: [https://github.com/gogf/gf/tree/master/example/trace/http\_with\_db](https://github.com/gogf/gf/tree/master/example/trace/http_with_db) ## 客户端 ```go package main import ( "github.com/gogf/gf/contrib/trace/otlphttp/v2" "github.com/gogf/gf/v2/database/gdb" "github.com/gogf/gf/v2/frame/g" "github.com/gogf/gf/v2/net/ghttp" "github.com/gogf/gf/v2/net/gtrace" "github.com/gogf/gf/v2/os/gctx" ) const ( serviceName = "otlp-http-client" endpoint = "tracing-analysis-dc-hz.aliyuncs.com" path = "adapt_******_******/api/otlp/traces" ) func main() { var ctx = gctx.New() shutdown, err := otlphttp.Init(serviceName, endpoint, path) if err != nil { g.Log().Fatal(ctx, err) } defer shutdown() StartRequests() } func StartRequests() { ctx, span := gtrace.NewSpan(gctx.New(), "StartRequests") defer span.End() var ( err error client = g.Client() ) // Add user info. var insertRes = struct { ghttp.DefaultHandlerResponse Data struct{ Id int64 } `json:"data"` }{} err = client.PostVar(ctx, "http://127.0.0.1:8199/user/insert", g.Map{ "name": "john", }).Scan(&insertRes) if err != nil { panic(err) } g.Log().Info(ctx, "insert result:", insertRes) if insertRes.Data.Id == 0 { g.Log().Error(ctx, "retrieve empty id string") return } // Query user info. var queryRes = struct { ghttp.DefaultHandlerResponse Data struct{ User gdb.Record } `json:"data"` }{} err = client.GetVar(ctx, "http://127.0.0.1:8199/user/query", g.Map{ "id": insertRes.Data.Id, }).Scan(&queryRes) if err != nil { panic(err) } g.Log().Info(ctx, "query result:", queryRes) // Delete user info. var deleteRes = struct { ghttp.DefaultHandlerResponse }{} err = client.PostVar(ctx, "http://127.0.0.1:8199/user/delete", g.Map{ "id": insertRes.Data.Id, }).Scan(&deleteRes) if err != nil { panic(err) } g.Log().Info(ctx, "delete result:", deleteRes) } ``` 客户端代码简要说明: 1. 首先,客户端也是需要通过 `jaeger.Init` 方法初始化 `Jaeger`。 2. 在本示例中,我们通过HTTP客户端向服务端发起了 `3` 次请求: 1. `/user/insert` 用于新增一个用户信息,成功后返回用户的ID。 2. `/user/query` 用于查询用户,使用前一个接口返回的用户ID。 3. `/user/delete` 用于删除用户,使用之前接口返回的用户ID。 ## 服务端 ```go package main import ( "context" "fmt" "time" "github.com/gogf/gf/contrib/trace/otlphttp/v2" "github.com/gogf/gf/v2/database/gdb" "github.com/gogf/gf/v2/frame/g" "github.com/gogf/gf/v2/net/ghttp" "github.com/gogf/gf/v2/os/gcache" "github.com/gogf/gf/v2/os/gctx" ) type cTrace struct{} const ( serviceName = "otlp-http-client" endpoint = "tracing-analysis-dc-hz.aliyuncs.com" path = "adapt_******_******/api/otlp/traces" ) func main() { var ctx = gctx.New() shutdown, err := otlphttp.Init(serviceName, endpoint, path) if err != nil { g.Log().Fatal(ctx, err) } defer shutdown() // Set ORM cache adapter with redis. g.DB().GetCache().SetAdapter(gcache.NewAdapterRedis(g.Redis())) // Start HTTP server. s := g.Server() s.Use(ghttp.MiddlewareHandlerResponse) s.Group("/", func(group *ghttp.RouterGroup) { group.ALL("/user", new(cTrace)) }) s.SetPort(8199) s.Run() } type InsertReq struct { Name string `v:"required#Please input user name."` } type InsertRes struct { Id int64 } // Insert is a route handler for inserting user info into database. func (c *cTrace) Insert(ctx context.Context, req *InsertReq) (res *InsertRes, err error) { result, err := g.Model("user").Ctx(ctx).Insert(req) if err != nil { return nil, err } id, _ := result.LastInsertId() res = &InsertRes{ Id: id, } return } type QueryReq struct { Id int `v:"min:1#User id is required for querying"` } type QueryRes struct { User gdb.Record } // Query is a route handler for querying user info. It firstly retrieves the info from redis, // if there's nothing in the redis, it then does db select. func (c *cTrace) Query(ctx context.Context, req *QueryReq) (res *QueryRes, err error) { one, err := g.Model("user").Ctx(ctx).Cache(gdb.CacheOption{ Duration: 5 * time.Second, Name: c.userCacheKey(req.Id), Force: false, }).WherePri(req.Id).One() if err != nil { return nil, err } res = &QueryRes{ User: one, } return } type DeleteReq struct { Id int `v:"min:1#User id is required for deleting."` } type DeleteRes struct{} // Delete is a route handler for deleting specified user info. func (c *cTrace) Delete(ctx context.Context, req *DeleteReq) (res *DeleteRes, err error) { _, err = g.Model("user").Ctx(ctx).Cache(gdb.CacheOption{ Duration: -1, Name: c.userCacheKey(req.Id), Force: false, }).WherePri(req.Id).Delete() if err != nil { return nil, err } return } func (c *cTrace) userCacheKey(id int) string { return fmt.Sprintf(`userInfo:%d`, id) } ``` 服务端代码简要说明: 1. 首先,客户端也是需要通过 `jaeger.Init` 方法初始化 `Jaeger`。 2. 在本示例中,我们使用到了数据库和数据库缓存功能,以便于同时演示 `ORM` 和 `Redis` 的链路跟踪记录。 3. 我们在程序启动时通过以下方法设置当前数据库缓存管理的适配器为 `redis`。关于缓存适配器的介绍感兴趣可以参考 [缓存管理-接口设计](../core/gcache-interface.md) 章节。 ```go g.DB().GetCache().SetAdapter(gcache.NewAdapterRedis(g.Redis())) ``` 4. 在 `ORM` 的操作中,需要通过 `Ctx` 方法将上下文变量传递到组件中, `orm` 组件会自动识别当前上下文中是否包含Tracing链路信息,如果包含则自动启用链路跟踪特性。 5. 在 `ORM` 的操作中,这里使用 `Cache` 方法缓存查询结果到 `redis` 中,并在删除操作中也使用 `Cache` 方法清除 `redis` 中的缓存结果。关于 `ORM` 的缓存管理介绍请参考 [ORM链式操作-查询缓存](../core/gdb-chaining-query-cache.md) 章节。 ## 效果查看 **启动服务端:** **启动客户端:** 在 `Jaeger` 上查看链路信息: 可以看到,这次请求总共产生了 `14` 个 `span`,其中客户端有 `4` 个 `span`,服务端有 `10` 个 `span`,每一个 `span` 代表一个链路节点。不过,我们注意到,这里产生了 `3` 个 `errors`。我们点击详情查看什么原因呢。 我们看到好像所有的 `redis` 操作都报错了,随便点击一个 `redis` 的相关 `span`,查看一下详情呢: 原来是 `redis` 连接不上报错了,这样的话所有的 `orm` 缓存功能都失效了,但是可以看到并没有影响接口逻辑,只是所有的查询都走了数据库。这个报错是因为我本地忘了打开 `redis server`,我赶紧启动一下本地的 `redis server`,再看看效果: 再把上面的客户端运行一下,查看 `jaeger`: 现在就没有报错了。 `HTTP Client&Server`、 `Logging` 组件在之前已经介绍过,因此这里我们主要关注 `orm` 和 `redis` 组件的链路跟踪信息。 ### ORM链路信息 #### Attributes/Tags 我们随便点开一个 `ORM` 链路 `Span`,看看 `Attributes/Tags` 信息: 可以看到这里的 `span.kind` 是 `internal`,也就是之前介绍过的方法内部 `span` 类型。这里很多 `Tags` 在之前已经介绍过,因此这里主要介绍关于数据库相关的 `Tags`: | Attribute/Tag | 说明 | | --- | --- | | `
    db.type
    ` | 数据库连接类型。如 `mysql`, `mssql`, `pgsql` 等等。 | | `db.link` | 数据库连接信息。其中密码字段被自动隐藏。 | | `db.group` | 在配置文件中的数据库分组名称。 | #### Events/Process | Event/Log | 说明 | | --- | --- | | `db.execution.sql` | 执行的具体 `SQL` 语句。由于ORM底层是预处理,该语句为方便查看自动拼接而成,仅供参考。 | | `db.execution.type` | 执行的 `SQL` 语句类型。常见为 `DB.ExecContext` 和 `DB.QueryContext`,分别代表写操作和读操作。 | | `db.execution.cost` | 当前 `SQL` 语句执行耗时,单位为 `ms` 毫秒。 | ### Redis链路信息 #### Attributes/Tags | Attribute/Tag | 说明 | | --- | --- | | `
    redis.host
    ` | `Redis` 连接地址。 | | `redis.port` | `Redis` 连接端口。 | | `redis.db` | `Redis` 操作 `db`。 | #### Events/Process | Event/Log | 说明 | | --- | --- | | `redis.execution.command` | `Redis` 执行指令。 | | `redis.execution.arguments` | `Redis` 执行指令参数。 | | `redis.execution.cost` | `Redis` 执行指令执行耗时,单位为 `ms` 毫秒。 | # 链路跟踪-TraceID注入和获取 ## 一、基本介绍 在链路跟踪中,`TraceID`作为在各个服务间传递的唯一标识,用于串联服务间请求关联关系,是非常重要的一项数据。`TraceID`是通过`Context`参数传递的,如果使用框架的`glog`日志组件,那么在日志打印中将会自动读取`TraceID`记录到日志内容中。因此也建议大家使用框架的`glog`日志组件来打印日志,便于完美地支持链路跟踪特性。 ## 二、TraceID的注入 ### 1、客户端 如果使用 `GoFrame` 框架的 `Client`,那么所有的请求将会自带 `TraceID` 的注入。 `GoFrame` 的 `TraceID` 使用的是 `OpenTelemetry` 规范,是由十六进制字符组成的的 `32` 字节字符串。 **提示** 强烈建议大家统一使用 `gclient` 组件,不仅功能全面而且自带链路跟踪能力。 ### 2、服务端 如果使用 `GoFrame` 框架的 `Server`,如果请求带有 `TraceID`,那么将会自动承接到 `Context` 中;否则,将会自动注入标准的 `TraceID`,并传递给后续逻辑。 ## 三、TraceID的获取 ### 1、客户端 如果使用 `GoFrame` 框架的 `Client`,这里有三种方式。 #### 1)自动生成TraceID(推荐) 通过 `gctx.New/WithCtx` 方法创建一个带有 `TraceID` 的 `Context`,该 `Context` 参数用于传递给请求方法。随后可以通过 `gctx.CtxId` 方法获取整个链路的 `TraceID`。相关方法: ```go // New creates and returns a context which contains context id. func New() context.Context // WithCtx creates and returns a context containing context id upon given parent context `ctx`. func WithCtx(ctx context.Context) context.Context ``` 使用 `WithCtx` 方法时,如果给定的 `ctx` 参数本身已经带有 `TraceID`,那么它将会直接使用该 `TraceID`,并不会新建。 #### 2)客户端自定义TraceID 这里还有个高级的用法,客户端可以自定义 `TraceID`,使用 `gtrace.WithTraceID` 方法。方法定义如下: ```go // WithTraceID injects custom trace id into context to propagate. func WithTraceID(ctx context.Context, traceID string) (context.Context, error) ``` #### 3)读取Response Header 如果是请求 `GoFrame` 框架的 `Server`,那么在返回请求的 `Header` 中将会增加 `Trace-Id` 字段,供客户端读取。 ### 2、服务端 如果使用 `GoFrame` 框架的 `Server`,直接通过 `gctx.CtxId` 方法即可获取 `TraceID`。相关方法: ```go // CtxId retrieves and returns the context id from context. func CtxId(ctx context.Context) string ``` ## 四、使用示例 ### 1、HTTP Response Header TraceID ```go package main import ( "context" "time" "github.com/gogf/gf/v2/frame/g" "github.com/gogf/gf/v2/net/ghttp" "github.com/gogf/gf/v2/os/gctx" ) func main() { s := g.Server() s.BindHandler("/", func(r *ghttp.Request) { traceID := gctx.CtxId(r.Context()) g.Log().Info(r.Context(), "handler") r.Response.Write(traceID) }) s.SetPort(8199) go s.Start() time.Sleep(time.Second) req, err := g.Client().Get(context.Background(), "http://127.0.0.1:8199/") if err != nil { panic(err) } defer req.Close() req.RawDump() } ``` 执行后,终端输出: ``` ADDRESS | METHOD | ROUTE | HANDLER | MIDDLEWARE ----------|--------|-------|-----------------------------------------------------------------|-------------------- :8199 | ALL | / | main.main.func1 | ----------|--------|-------|-----------------------------------------------------------------|-------------------- :8199 | ALL | /* | github.com/gogf/gf/v2/net/ghttp.internalMiddlewareServerTracing | GLOBAL MIDDLEWARE ----------|--------|-------|-----------------------------------------------------------------|-------------------- 2022-06-06 21:14:37.245 [INFO] pid[55899]: http server started listening on [:8199] 2022-06-06 21:14:38.247 [INFO] {908d2027560af616e218e912d2ac3972} handler +---------------------------------------------+ | REQUEST | +---------------------------------------------+ GET / HTTP/1.1 Host: 127.0.0.1:8199 User-Agent: GClient v2.1.0-rc4 at TXQIANGGUO-MB0 Traceparent: 00-908d2027560af616e218e912d2ac3972-1e291041b9afa718-01 Accept-Encoding: gzip +---------------------------------------------+ | RESPONSE | +---------------------------------------------+ HTTP/1.1 200 OK Connection: close Content-Length: 32 Content-Type: text/plain; charset=utf-8 Date: Mon, 06 Jun 2022 13:14:38 GMT Server: GoFrame HTTP Server Trace-Id: 908d2027560af616e218e912d2ac3972 908d2027560af616e218e912d2ac3972 ``` ### 2、客户端注入 `TraceID` ```go package main import ( "time" "github.com/gogf/gf/v2/frame/g" "github.com/gogf/gf/v2/net/ghttp" "github.com/gogf/gf/v2/os/gctx" ) func main() { s := g.Server() s.BindHandler("/", func(r *ghttp.Request) { traceID := gctx.CtxId(r.Context()) g.Log().Info(r.Context(), "handler") r.Response.Write(traceID) }) s.SetPort(8199) go s.Start() time.Sleep(time.Second) ctx := gctx.New() g.Log().Info(ctx, "request starts") content := g.Client().GetContent(ctx, "http://127.0.0.1:8199/") g.Log().Infof(ctx, "response: %s", content) } ``` 执行后,终端输出: ```html 2022-06-06 21:17:17.447 [INFO] pid[56070]: http server started listening on [:8199] ADDRESS | METHOD | ROUTE | HANDLER | MIDDLEWARE ----------|--------|-------|-----------------------------------------------------------------|-------------------- :8199 | ALL | / | main.main.func1 | ----------|--------|-------|-----------------------------------------------------------------|-------------------- :8199 | ALL | /* | github.com/gogf/gf/v2/net/ghttp.internalMiddlewareServerTracing | GLOBAL MIDDLEWARE ----------|--------|-------|-----------------------------------------------------------------|-------------------- 2022-06-06 21:17:18.450 [INFO] {e843f0737b0af616d8ed185d46ba65c5} request starts 2022-06-06 21:17:18.451 [INFO] {e843f0737b0af616d8ed185d46ba65c5} handler 2022-06-06 21:17:18.451 [INFO] {e843f0737b0af616d8ed185d46ba65c5} response: e843f0737b0af616d8ed185d46ba65c5 ``` ### 3、客户端自定义 `TraceID` ```go package main import ( "context" "time" "github.com/gogf/gf/v2/frame/g" "github.com/gogf/gf/v2/net/ghttp" "github.com/gogf/gf/v2/net/gtrace" "github.com/gogf/gf/v2/os/gctx" "github.com/gogf/gf/v2/text/gstr" ) func main() { s := g.Server() s.BindHandler("/", func(r *ghttp.Request) { traceID := gctx.CtxId(r.Context()) g.Log().Info(r.Context(), "handler") r.Response.Write(traceID) }) s.SetPort(8199) go s.Start() time.Sleep(time.Second) ctx, _ := gtrace.WithTraceID(context.Background(), gstr.Repeat("a", 32)) g.Log().Info(ctx, "request starts") content := g.Client().GetContent(ctx, "http://127.0.0.1:8199/") g.Log().Infof(ctx, "response: %s", content) } ``` 执行后,终端输出: ``` ADDRESS | METHOD | ROUTE | HANDLER | MIDDLEWARE ----------|--------|-------|-----------------------------------------------------------------|-------------------- :8199 | ALL | / | main.main.func1 | ----------|--------|-------|-----------------------------------------------------------------|-------------------- :8199 | ALL | /* | github.com/gogf/gf/v2/net/ghttp.internalMiddlewareServerTracing | GLOBAL MIDDLEWARE ----------|--------|-------|-----------------------------------------------------------------|-------------------- 2022-06-06 21:40:03.897 [INFO] pid[58435]: http server started listening on [:8199] 2022-06-06 21:40:04.900 [INFO] {aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa} request starts 2022-06-06 21:40:04.901 [INFO] {aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa} handler 2022-06-06 21:40:04.901 [INFO] {aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa} response: aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa ``` ## 五、常见问题 ### 1、如果没有使用 `GoFrame` 框架的 `Client/Server`,如何获取链路的 `TraceID`? 可以参考 `GoFrame` 框架的 `Client/Server` 的链路跟踪实现,并自行实现一遍,这块可能需要一定成本。 如果使用的第三方 `Client/Server` 组件,请参考第三方组件相关介绍。 ### 2、企业内部服务没有使用标准的 `OpenTelemetry` 规范,但是每个请求都带 `RequestID` 参数,形如 `33612a70-990a-11ea-87fe-fd68517e7a2d`,如何和 `TraceID` 结合起来? 根据我的分析,这个 `RequestID` 的格式和 `TraceID` 规范非常切合,使用的是 `UUID` 字符串,而 `UUID` 可直接转换为 `TraceID`。建议在自己的 `Server` 内部第一层中间件中将 `RequestID` 转换为 `TraceID`,通过自定义 `TraceID` 的方式注入到 `Context` 中,并将该 `Context` 传递给后续业务逻辑。 下面给出一个例子: ```go package main import ( "github.com/gogf/gf/v2/frame/g" "github.com/gogf/gf/v2/net/ghttp" "github.com/gogf/gf/v2/net/gtrace" "github.com/gogf/gf/v2/os/gctx" ) func main() { // 内部服务 internalServer := g.Server("internalServer") internalServer.BindHandler("/", func(r *ghttp.Request) { traceID := gctx.CtxId(r.Context()) g.Log().Info(r.Context(), "internalServer handler") r.Response.Write(traceID) }) internalServer.SetPort(8199) go internalServer.Start() // 外部服务,访问以测试 // http://127.0.0.1:8299/?RequestID=33612a70-990a-11ea-87fe-fd68517e7a2d userSideServer := g.Server("userSideServer") userSideServer.Use(func(r *ghttp.Request) { requestID := r.Get("RequestID").String() if requestID != "" { newCtx, err := gtrace.WithUUID(r.Context(), requestID) if err != nil { panic(err) } r.SetCtx(newCtx) } r.Middleware.Next() }) userSideServer.BindHandler("/", func(r *ghttp.Request) { ctx := r.Context() g.Log().Info(ctx, "request internalServer starts") content := g.Client().GetContent(ctx, "http://127.0.0.1:8199/") g.Log().Infof(ctx, "internalServer response: %s", content) r.Response.Write(content) }) userSideServer.SetPort(8299) userSideServer.Run() } ``` 为了演示服务间的链路跟踪能力,这个示例代码中运行了两个HTTP服务,一个内部服务,提供功能逻辑;一个外部服务,供外部的接口调用,它的功能是调用内部服务来实现的。执行后,我们访问: [http://127.0.0.1:8299/?RequestID=33612a70-990a-11ea-87fe-fd68517e7a2d](http://127.0.0.1:8299/?RequestID=33612a70-990a-11ea-87fe-fd68517e7a2d) 随后查看终端输出: ```html 2022-06-07 14:51:21.957 [INFO] openapi specification is disabled 2022-06-07 14:51:21.958 [INTE] ghttp_server.go:78 78198: graceful reload feature is disabled SERVER | DOMAIN | ADDRESS | METHOD | ROUTE | HANDLER | MIDDLEWARE -----------------|---------|---------|--------|-------|-----------------------------------------------------------------|-------------------- internalServer | default | :8199 | ALL | / | main.main.func1 | -----------------|---------|---------|--------|-------|-----------------------------------------------------------------|-------------------- internalServer | default | :8199 | ALL | /* | github.com/gogf/gf/v2/net/ghttp.internalMiddlewareServerTracing | GLOBAL MIDDLEWARE -----------------|---------|---------|--------|-------|-----------------------------------------------------------------|-------------------- 2022-06-07 14:51:21.959 [INFO] pid[78198]: http server started listening on [:8199] 2022-06-07 14:51:21.965 [INFO] openapi specification is disabled SERVER | DOMAIN | ADDRESS | METHOD | ROUTE | HANDLER | MIDDLEWARE -----------------|---------|---------|--------|-------|-----------------------------------------------------------------|-------------------- userSideServer | default | :8299 | ALL | / | main.main.func3 | -----------------|---------|---------|--------|-------|-----------------------------------------------------------------|-------------------- userSideServer | default | :8299 | ALL | /* | github.com/gogf/gf/v2/net/ghttp.internalMiddlewareServerTracing | GLOBAL MIDDLEWARE -----------------|---------|---------|--------|-------|-----------------------------------------------------------------|-------------------- userSideServer | default | :8299 | ALL | /* | main.main.func2 | GLOBAL MIDDLEWARE -----------------|---------|---------|--------|-------|-----------------------------------------------------------------|-------------------- 2022-06-07 14:51:21.965 [INFO] pid[78198]: http server started listening on [:8299] 2022-06-07 14:53:05.322 [INFO] {33612a70990a11ea87fefd68517e7a2d} request internalServer starts 2022-06-07 14:53:05.323 [INFO] {33612a70990a11ea87fefd68517e7a2d} internalServer handler 2022-06-07 14:53:05.323 [INFO] {33612a70990a11ea87fefd68517e7a2d} internalServer response: 33612a70990a11ea87fefd68517e7a2d ``` 我们发现, `RequestID` 作为 `TraceID` 贯通了整个服务间的链路了呢。 # 服务监控告警 监控告警特性从框架 `v2.7` 版本开始提供。 开发者通过 `GoFrame` 框架可以快速地实现监控指标及指标暴露。 # 监控告警-基本介绍 在介绍框架的监控告警之前,我们离不开业内可观测性的标准介绍, `OpenTelemetry` 关于监控告警这块的设计,以及相关规范。 `OpenTelemetry` 在这块的内容也比较多,我们选择一些重点来介绍一下。 ## OpenTelemetry ### 相关组件 我们来看一张 `OpenTelemetry` 组件的关系图,在 `OpenTelemetry Metrics` 标准的实现中,主要包含以下组件。 在标准化文档的落地时,各个组件通常都是采用接口化设计,以提高可扩展性: #### Meter Provider 用于接口化管理全局的 `Meter` 创建,相当于全局的监控指标管理工厂。 #### Meter 用于接口化创建并管理全局的 `Instrument`,不同的 `Meter` 可以看做是不同的程序组件。例如框架中的各个组件可以看做是不同的 `Meter`,比如 `gclient` 和 `ghttp` 就是两个不同的 `Meter`。 #### Instrument 用于管理不同组件下的各个不同类型的指标,例如在 `ghttp` 下,就会有 `http.server.request.duration`、 `http.server.request.body_size` 等指标。 #### Measurements 对应指标上报的具体的 `DataPoint` 指标数据,是一系列的数值项。 #### View 实现对 `Measurements` 的计算、汇总、过滤、修改等操作,通常指标都是 **数值类型**,所以这个通常都使用默认的 `View` 即可。 #### Metric Reader 用于实现对指标的数据流读取,内部定义了具体操作指标的数据结构。`OpenTelemetry` 官方社区提供了多种灵活的 `Reader` 实现,例如 `PeriodicReader`、`ManualReader` 等。 #### Metric Exporter `Exporter` 用于暴露本地指标到对应的第三方厂商,定义了具体是 `push` 还是 `pull` 的数据传输方式。`Exporter` 需要用到 `Reader`,其中 `Reader` 只有几种方式,但是 `Exporter` 会根据厂商而定,会有很多,例如:`Prometheus`、`Zipkin` 等。 一个 `Instrument` 多个 `DataPoint` 的数据流如下图所示: ### 相关类型 `OpenTelemetry` 的社区实现为了满足不同的使用场景,因此类型的设计粒度比较细,分为 `int64` 和 `float64` 数据类型,并且包含 **同步** 和 **异步** 指标类型。 #### 同步类型 **同步类型**用于快速暴露监控指标,无论 `Metrics Reader` 是否读取该监控指标,指标的计算结果已完成,待读取使用。例如,`HTTP` 的请求总数、请求大小,这些数值必须在请求执行流程中记录到对应的监控指标数值中,适合作为同步指标来管理。 | **类型** | **描述** | **示例** | | --- | --- | --- | | `Int64Counter` | `int64` 只增不减的指标。 | 请求总数、请求字节总大小 | | `Int64UpDownCounter` | `int64` 可增可减的指标。 | 当前活跃请求、执行队列大小 | | `Float64Counter` | `float64` 只增不减的指标。 | 请求总数、请求字节总大小 | | `Float64UpDownCounter` | `float64` 可增可减的指标。 | 请求总数、请求字节总大小 | | `Int64Histogram` | `int64` 可分组的指标 | 请求执行时间 `p99` | | `Float64Histogram` | `float64` 可分组的指标 | 请求执行时间 `p99` | #### 异步类型 **异步类型**的监控指标只有在 `Metrics Reader` 开始读取该监控指标时才会执行指标计算逻辑。异步类型的监控指标需要设置一个回调函数,该回调函数用于生成指标数值,并在 `Metrics Reader` 读取指标时才会触发回调函数。例如,机器 CPU、内存、磁盘使用量的指标,如果没有目标端拉取或者使用该指标时,提前计算指标值毫无意义且浪费计算资源,适合作为异步指标来管理。 | **类型** | **描述** | **示例** | | --- | --- | --- | | `Int64ObservableCouter` | `int64` 只增不减的指标。 | CPU、内存、磁盘使用量 | | `Int64ObservableUpDownCounter` | `int64` 可增可减的指标。 | CPU、内存、磁盘使用量 | | `Float64ObservableCouter` | `float64` 只增不减的指标。 | CPU、内存、磁盘使用量 | | `Float64ObservableUpDownCounter` | `float64` 可增可减的指标。 | 当前活跃请求、执行队列大小 | | `Int64ObservableGauge` | `int64` 可增可减的指标。 | CPU、内存、磁盘使用量 | | `Float64ObservableGauge` | `float64` 可增可减动态设置的指标。 | CPU、内存、磁盘使用量 | ## 框架监控组件 ### 组件抽象 框架通过 `gmetric` 组件来实现监控能力, `gmetric` 组件内部设计的组件层级关系同 `OpenTelemetry Metrics` 类似: `gmetric` 组件使用了**抽象解耦设计**,一方面是因为框架设计需要减少外部依赖;另一方面是为了实现监控的自动开关能力。组件默认情况下使用了 `NoopPerformer` 的实现对象,在默认情况下监控能力是关闭的,只有在真正引入监控接口实现时才会自动开启监控能力。 ### 指标类型 框架提供的指标类型相比较 `OpenTelemetry` 社区实现去掉了 `int64` 数值类型,而是使用了统一的 `float64` 数值类型以简化使用。但同时需要注意,开发者在设计指标数值时**尽可能避免小数设计,以避免数值计算的精度问题**。特别是在 `Histogram` 类型的 `Buckets` 设计时,不建议使用小数。 #### 同步类型 | **类型** | **描述** | **示例** | | --- | --- | --- | | `Counter` | `float64` 只增不减的指标。 | 请求总数、请求字节总大小 | | `UpDownCounter` | `float64` 可增可减的指标。 | 请求总数、请求字节总大小 | | `Histogram` | `float64` 可分组的指标 | 请求执行时间 `p99` | #### 异步类型 | **类型** | **描述** | **示例** | | --- | --- | --- | | `ObservableCounter` | `float64` 只增不减的指标。 | 请求总数、请求字节总大小 | | `ObservableUpDownCounter` | `float64` 可增可减的指标。 | 请求总数、请求字节总大小 | | `ObservableGauge` | `float64` 可增可减动态设置的指标。 | CPU、内存、磁盘使用量 | ## 参考资料 - [https://github.com/prometheus/client\_golang](https://github.com/prometheus/client_golang%20"https://github.com/prometheus/client_golang") - [https://github.com/open-telemetry/opentelemetry-go-contrib](https://github.com/open-telemetry/opentelemetry-go-contrib%20"https://github.com/open-telemetry/opentelemetry-go-contrib") - [https://opentelemetry.io/docs/specs/otel/metrics/api/](https://opentelemetry.io/docs/specs/otel/metrics/api/%20"https://opentelemetry.io/docs/specs/otel/metrics/api/") - [https://opentelemetry.io/docs/specs/otel/metrics/data-model](https://opentelemetry.io/docs/specs/otel/metrics/data-model%20"https://opentelemetry.io/docs/specs/otel/metrics/data-model") # 监控告警-基本使用 ## 基本介绍 监控指标的代码开发直接使用框架主库的 `gmetric` 组件即可,但由于 `gmetric` 组件实际上只是定义了监控指标的相关接口,并且默认提供的是 `NoopPerformer` 实现,默认监控指标特性是关闭的。因此需要引入具体的接口实现组件才能真正开启监控指标特性。框架社区提供了社区组件 `github.com/gogf/gf/contrib/metric/otelmetric/v2`,使用 `OpenTelemetry` 实现了框架的监控指标接口,引入该社区组件并执行监控指标管理对象创建即可开启监控指标特性。`otelmetric` 组件源码地址:[https://github.com/gogf/gf/tree/master/contrib/metric/otelmetric](https://github.com/gogf/gf/tree/master/contrib/metric/otelmetric) 我们通过一个简单的监控指标实现示例来介绍一下监控指标组件的基本使用。 ```go package main import ( "go.opentelemetry.io/otel/sdk/metric" "go.opentelemetry.io/otel/sdk/metric/metricdata" "github.com/gogf/gf/contrib/metric/otelmetric/v2" "github.com/gogf/gf/v2/frame/g" "github.com/gogf/gf/v2/os/gctx" "github.com/gogf/gf/v2/os/gmetric" ) var ( meter = gmetric.GetGlobalProvider().Meter(gmetric.MeterOption{ Instrument: "github.com/gogf/gf/example/metric/basic", InstrumentVersion: "v1.0", }) counter = meter.MustCounter( "goframe.metric.demo.counter", gmetric.MetricOption{ Help: "This is a simple demo for Counter usage", Unit: "bytes", }, ) ) func main() { var ( ctx = gctx.New() reader = metric.NewManualReader() ) provider := otelmetric.MustProvider(otelmetric.WithReader(reader)) provider.SetAsGlobal() defer provider.Shutdown(ctx) counter.Inc(ctx) counter.Add(ctx, 10) var ( rm = metricdata.ResourceMetrics{} err = reader.Collect(ctx, &rm) ) if err != nil { g.Log().Fatal(ctx, err) } g.DumpJson(rm) } ``` ## 指标管理组件的创建 通过 `gmetric.GetGlobalProvider()` 方法可以获取全局的监控**指标管理对象**,**该对象是一个单例设计,全局只能有一个**。并通过该对象的 `Meter` 方法可以创建或获取对应的**组件对象**。组件对象用于管理该组件下所有的监控指标。在创建组件对象时通常需要定义组件(`Instrument`)的名称以及版本(虽然也可以为空,但为了方便后续维护建议都设置上)。 ``` meter = gmetric.GetGlobalProvider().Meter(gmetric.MeterOption{ Instrument: "github.com/gogf/gf/example/metric/basic", InstrumentVersion: "v1.0", }) ``` 其中的 `gmetric.MeterOption` 数据结构如下: ```go // MeterOption holds the creation option for a Meter. type MeterOption struct { // Instrument is the instrumentation name to bind this Metric to a global MeterProvider. // This is an optional configuration for a metric. Instrument string // InstrumentVersion is the instrumentation version to bind this Metric to a global MeterProvider. // This is an optional configuration for a metric. InstrumentVersion string // Attributes holds the constant key-value pair description metadata for all metrics of Meter. // This is an optional configuration for a meter. Attributes Attributes } ``` ## 监控指标对象的创建 通过 `Meter` 接口对象,我们可以在该组件下创建对应的各种指标。指标有各种数据类型,因此 `Meter` 接口的定义如下: ```go // Meter hold the functions for kinds of Metric creating. type Meter interface { // Counter creates and returns a new Counter. Counter(name string, option MetricOption) (Counter, error) // UpDownCounter creates and returns a new UpDownCounter. UpDownCounter(name string, option MetricOption) (UpDownCounter, error) // Histogram creates and returns a new Histogram. Histogram(name string, option MetricOption) (Histogram, error) // ObservableCounter creates and returns a new ObservableCounter. ObservableCounter(name string, option MetricOption) (ObservableCounter, error) // ObservableUpDownCounter creates and returns a new ObservableUpDownCounter. ObservableUpDownCounter(name string, option MetricOption) (ObservableUpDownCounter, error) // ObservableGauge creates and returns a new ObservableGauge. ObservableGauge(name string, option MetricOption) (ObservableGauge, error) // MustCounter creates and returns a new Counter. // It panics if any error occurs. MustCounter(name string, option MetricOption) Counter // MustUpDownCounter creates and returns a new UpDownCounter. // It panics if any error occurs. MustUpDownCounter(name string, option MetricOption) UpDownCounter // MustHistogram creates and returns a new Histogram. // It panics if any error occurs. MustHistogram(name string, option MetricOption) Histogram // MustObservableCounter creates and returns a new ObservableCounter. // It panics if any error occurs. MustObservableCounter(name string, option MetricOption) ObservableCounter // MustObservableUpDownCounter creates and returns a new ObservableUpDownCounter. // It panics if any error occurs. MustObservableUpDownCounter(name string, option MetricOption) ObservableUpDownCounter // MustObservableGauge creates and returns a new ObservableGauge. // It panics if any error occurs. MustObservableGauge(name string, option MetricOption) ObservableGauge // RegisterCallback registers callback on certain metrics. // A callback is bound to certain component and version, it is called when the associated metrics are read. // Multiple callbacks on the same component and version will be called by their registered sequence. RegisterCallback(callback Callback, canBeCallbackMetrics ...ObservableMetric) error // MustRegisterCallback performs as RegisterCallback, but it panics if any error occurs. MustRegisterCallback(callback Callback, canBeCallbackMetrics ...ObservableMetric) } ``` 以本示例代码中使用的 `meter.MustCounter` 方法来介绍,该方法是创建一个 `Counter` 同步指标,同时由于我们偷懒使用了 `Must*` 方法,也就是说如果创建指标失败,那么这个方法会 `panic` 报错。在创建指标对象时,指标名称 `name` 是必需参数,另外的 `MetricOption` 是可选参数,用于进一步描述指标信息。`MetricOption` 的数据结构定义如下: ```go // MetricOption holds the basic options for creating a metric. type MetricOption struct { // Help provides information about this Histogram. // This is an optional configuration for a metric. Help string // Unit is the unit for metric value. // This is an optional configuration for a metric. Unit string // Attributes holds the constant key-value pair description metadata for this metric. // This is an optional configuration for a metric. Attributes Attributes // Buckets defines the buckets into which observations are counted. // For Histogram metric only. // A histogram metric uses default buckets if no explicit buckets configured. Buckets []float64 // Callback function for metric, which is called when metric value changes. // For observable metric only. // If an observable metric has either Callback attribute nor global callback configured, it does nothing. Callback MetricCallback } ``` ## 初始化监控指标实现 通过 `otelmetric.MustProvider` 创建方法即可创建并初始化监控指标管理对象。 ```go provider := otelmetric.MustProvider(otelmetric.WithReader(reader)) provider.SetAsGlobal() defer provider.Shutdown(ctx) ``` 前面我们有介绍到,`GlobalProvider` 其实是一个单例的指标管理对象,因此这里通过 `provider.SetAsGlobal` 方法调用可以将该对象设置为全局的指标管理对象,便于后续的指标创建均基于该对象创建。 我们在 `main` 函数中通过 `defer provider.Shutdown` 方法调用,便于在程序结束时优雅关闭指标管理对象,例如将对象中的指标缓存及时输出到目标端。 ## 监控指标对象的使用 不同的指标对象有不同的操作方法用于实现指标数值的变化。以示例中的 `Counter` 指标类型为例,其接口定义如下: ```go // Counter is a Metric that represents a single numerical value that can ever // goes up. type Counter interface { Metric CounterPerformer } // CounterPerformer performs operations for Counter metric. type CounterPerformer interface { // Inc increments the counter by 1. Use Add to increment it by arbitrary // non-negative values. Inc(ctx context.Context, option ...Option) // Add adds the given value to the counter. It panics if the value is < 0. Add(ctx context.Context, increment float64, option ...Option) } ``` 可以看到, `Counter` 指标主要可以执行 `Inc` 及 `Add` 两个操作方法。其中还有一个 `Metric` 的接口,所有的指标均实现了该接口用于获取当前指标的基础信息,其接口定义如下: ```go // Metric models a single sample value with its metadata being exported. type Metric interface { // Info returns the basic information of a Metric. Info() MetricInfo } // MetricInfo exports information of the Metric. type MetricInfo interface { Key() string // Key returns the unique string key of the metric. Name() string // Name returns the name of the metric. Help() string // Help returns the help description of the metric. Unit() string // Unit returns the unit name of the metric. Type() MetricType // Type returns the type of the metric. Attributes() Attributes // Attributes returns the constant attribute slice of the metric. Instrument() InstrumentInfo // InstrumentInfo returns the instrument info of the metric. } // InstrumentInfo exports the instrument information of a metric. type InstrumentInfo interface { Name() string // Name returns the instrument name of the metric. Version() string // Version returns the instrument version of the metric. } ``` ## 监控指标的数据读取 通过上一章节介绍的 `OpenTelemetry` 组件关系我们知道,如果想要使用指标必须要通过 `MetricReader`,因此在该示例代码中我们通过 `OpenTelemetry` 官方社区开源组件中最常用的 `ManualReader` 来简单实现指标数据读取,`ManualReader` 是 `OpenTelemetry` 官方社区提供的实现。 ``` reader = metric.NewManualReader() ``` 并通过 `WithReader` 方法配置到 `Provider` 中: ```go provider := otelmetric.MustProvider(otelmetric.WithReader(reader)) ``` 随后通过 `Collect` 方法可以获取当前所有的指标数据: ```go var ( rm = metricdata.ResourceMetrics{} err = reader.Collect(ctx, &rm) ) if err != nil { g.Log().Fatal(ctx, err) } g.DumpJson(rm) ``` 执行后,终端输出: ``` ... "ScopeMetrics": [ { "Scope": { "Name": "github.com/gogf/gf/example/metric/basic", "Version": "v1.0", "SchemaURL": "" }, "Metrics": [ { "Name": "goframe.metric.demo.counter", "Description": "This is a simple demo for Counter usage", "Unit": "bytes", "Data": { "DataPoints": [ { "Attributes": [], "StartTime": "2024-03-25T10:13:19.326977+08:00", "Time": "2024-03-25T10:13:19.327144+08:00", "Value": 11 } ], "Temporality": "CumulativeTemporality", "IsMonotonic": true } } ] }, ... ``` 为了简化示例介绍,我们在这里省略了一些输出内容,更详细的指标及输出介绍请参考后续章节。 # 监控告警-同步指标 ## 基本介绍 **同步类型** 用于快速暴露监控指标,无论 `metrics reader` 是否使用该监控指标,指标的计算结果已完成,待读取使用。例如,HTTP的请求总数、请求大小,这些数值必须在请求执行流程中记录到对应的监控指标数值中,适合作为同步指标来管理。 `gmetric` 提供的同步类型指标包含: `Counter, UpDownCounter, Histogram`。 我们用一个简单的示例来演示同步指标的基本使用。 ```go package main import ( "go.opentelemetry.io/otel/exporters/prometheus" "github.com/gogf/gf/contrib/metric/otelmetric/v2" "github.com/gogf/gf/v2/frame/g" "github.com/gogf/gf/v2/os/gctx" "github.com/gogf/gf/v2/os/gmetric" ) var ( meter = gmetric.GetGlobalProvider().Meter(gmetric.MeterOption{ Instrument: "github.com/gogf/gf/example/metric/basic", InstrumentVersion: "v1.0", }) counter = meter.MustCounter( "goframe.metric.demo.counter", gmetric.MetricOption{ Help: "This is a simple demo for Counter usage", Unit: "bytes", }, ) upDownCounter = meter.MustUpDownCounter( "goframe.metric.demo.updown_counter", gmetric.MetricOption{ Help: "This is a simple demo for UpDownCounter usage", Unit: "%", }, ) histogram = meter.MustHistogram( "goframe.metric.demo.histogram", gmetric.MetricOption{ Help: "This is a simple demo for histogram usage", Unit: "ms", Buckets: []float64{0, 10, 20, 50, 100, 500, 1000, 2000, 5000, 10000}, }, ) ) func main() { var ctx = gctx.New() // Prometheus exporter to export metrics as Prometheus format. exporter, err := prometheus.New( prometheus.WithoutCounterSuffixes(), prometheus.WithoutUnits(), ) if err != nil { g.Log().Fatal(ctx, err) } // OpenTelemetry provider. provider := otelmetric.MustProvider(otelmetric.WithReader(exporter)) provider.SetAsGlobal() defer provider.Shutdown(ctx) // Counter. counter.Inc(ctx) counter.Add(ctx, 10) // UpDownCounter. upDownCounter.Inc(ctx) upDownCounter.Add(ctx, 10) upDownCounter.Dec(ctx) // Record values for histogram. histogram.Record(1) histogram.Record(20) histogram.Record(30) histogram.Record(101) histogram.Record(2000) histogram.Record(9000) histogram.Record(20000) // HTTP Server for metrics exporting. otelmetric.StartPrometheusMetricsServer(8000, "/metrics") } ``` ## Counter&UpDownCounter 其中的 `Counter` 和 `UpDownCounter` 比较简单,这里就不详细介绍了,需要注意的是 `Counter` 和 `UpDownCounter` 虽然看起来差不多,实际上也是如此,只是为了更严谨和精细化区分使用场景。如果将这两种数据类型映射到经典的 `Prometheus` 指标类型中, `Counter` 对应的就是 `Prometheus` 的 `Counter` 指标类型,而 `UpDownCounter` 对应的是 `Prometheus` 的 `Gauge` 指标类型。 ## Histogram `Histogram` 是一种统计类型,通过该指标可以快速统计出指标的 `p95, p99` 等百分位统计结果直方图,例如时间开销、成功/失败率等指标。但需要注意该指标比较占内存和空间,不能为其添加过多的 **动态属性**,因为不同的属性会衍生出同一种 `Histogram` 指标不同的存储数值。 ## Primetheus Exporter 在该示例中,我们使用了比较常用的 `Prometheus` 协议输出指标内容,通常用于对外暴露指标供外部组件抓取。通过以下路由将指标通过 `Prometheus` 协议暴露: ``` otelmetric.StartPrometheusMetricsServer(8000, "/metrics") ``` 执行后,访问 [http://127.0.0.1:8000/metrics](http://127.0.0.1:8000/metrics) 查看暴露的指标: 我们这里只关注本次示例中的指标,其他自动暴露的指标在后续章节介绍。 # 监控告警-异步指标 ## 基本介绍 **异步类型** 的监控指标只有在 `metrics reader` 开始使用该监控指标时才会执行指标计算逻辑。异步类型的监控指标需要设置一个回调函数,该回调函数用于生成指标数值,并在 `metrics reader` 读取指标时才会触发回调函数。例如,机器CPU、内存、磁盘使用量的指标,如果没有目标端拉取或者使用该指标时,提前计算指标值毫无意义且浪费计算资源,适合作为异步指标来管理。 `gmetric` 提供的异步类型指标包含: `ObservableCounter, ObservableUpDownCounter, OvservableGauge`。异步指标类型均是使用 `Observable` 开头命名,三种异步指标的操作都差不多,均是在不同的业务场景下有不同的使用差异。 我们用一个简单的示例来演示异步指标的基本使用。 ```go package main import ( "context" "go.opentelemetry.io/otel/exporters/prometheus" "github.com/gogf/gf/contrib/metric/otelmetric/v2" "github.com/gogf/gf/v2/frame/g" "github.com/gogf/gf/v2/os/gctx" "github.com/gogf/gf/v2/os/gmetric" ) var ( meter = gmetric.GetGlobalProvider().Meter(gmetric.MeterOption{ Instrument: "github.com/gogf/gf/example/metric/basic", InstrumentVersion: "v1.0", }) observableCounter = meter.MustObservableCounter( "goframe.metric.demo.observable_counter", gmetric.MetricOption{ Help: "This is a simple demo for ObservableCounter usage", Unit: "%", }, ) observableUpDownCounter = meter.MustObservableUpDownCounter( "goframe.metric.demo.observable_updown_counter", gmetric.MetricOption{ Help: "This is a simple demo for ObservableUpDownCounter usage", Unit: "%", }, ) observableGauge = meter.MustObservableGauge( "goframe.metric.demo.observable_gauge", gmetric.MetricOption{ Help: "This is a simple demo for ObservableGauge usage", Unit: "%", }, ) ) func main() { var ctx = gctx.New() // Callback for observable metrics. meter.MustRegisterCallback(func(ctx context.Context, obs gmetric.Observer) error { obs.Observe(observableCounter, 10) obs.Observe(observableUpDownCounter, 20) obs.Observe(observableGauge, 30) return nil }, observableCounter, observableUpDownCounter, observableGauge) // Prometheus exporter to export metrics as Prometheus format. exporter, err := prometheus.New( prometheus.WithoutCounterSuffixes(), prometheus.WithoutUnits(), ) if err != nil { g.Log().Fatal(ctx, err) } // OpenTelemetry provider. provider := otelmetric.MustProvider(otelmetric.WithReader(exporter)) provider.SetAsGlobal() defer provider.Shutdown(ctx) // HTTP Server for metrics exporting. otelmetric.StartPrometheusMetricsServer(8000, "/metrics") } ``` ## Meter Callback 异步指标需要定义 `Callback` 函数来管理指标数值变化,只有在请求或使用该指标时才会执行该 `Callback` 函数。 `Callback` 函数中使用 `Observe` 函数来更新指标的数值,针对不同异步指标类型的 `Observe` 会产生不同的结果。 - 针对 `ObservableCounter/ObservableUpDownCounter` 指标类型,使用 `Observe` 函数后将会在原有指标数值上进行增减。 - 针对 `ObservableGauge` 指标类型,使用 `Observe` 函数后,该指标的值更会更新为 `Observe` 给定的数值。 ## Metric Callback 除了通过 `Meter CallBack` 来实现异步指标的数值更新,也可以在创建指标时通过 `MetricOption` 指定 `Callback` 函数。例如: ``` observableCounter = meter.MustObservableCounter( "goframe.metric.demo.observable_counter", gmetric.MetricOption{ Help: "This is a simple demo for ObservableCounter usage", Unit: "%", Callback: func(ctx context.Context, obs gmetric.MetricObserver) error { obs.Observe(10) return nil }, }, ) ``` ## Primetheus Exporter 通过以下路由将指标通过 `Prometheus` 协议暴露: ``` otelmetric.StartPrometheusMetricsServer(8000, "/metrics") ``` 执行后,访问 [http://127.0.0.1:8000/metrics](http://127.0.0.1:8000/metrics) 查看暴露的指标: 我们这里只关注本次示例中的指标,其他自动暴露的指标在后续章节介绍。 # 监控告警-指标属性 指标属性用于在更上层的指标使用中进行过滤、汇总、统计等高纬度操作。在 `GoFrame` 框架的监控告警组件中,提供了3种属性注入方式: **常量属性**、 **变量属性** 和 **全局属性**。 **提示** 在 `OpenTelemetry` 中叫做指标属性( `attributes`),但在 `Prometheus` 中叫做指标标签( `labels`),两者是一个含义。 ## 常量属性 **常量属性** 是固定的一系列属性键值对,可以绑定到 `Meter` 中,也可以直接绑定到指标对象上。如果绑定到 `Meter` 上,那么 `Meter` 下所有创建的指标对象均带有该属性键值对,如果绑定到指标对象上,那么仅会在当前指标上生效。我们来看一个示例: ```go package main import ( "context" "go.opentelemetry.io/otel/exporters/prometheus" "github.com/gogf/gf/contrib/metric/otelmetric/v2" "github.com/gogf/gf/v2/frame/g" "github.com/gogf/gf/v2/os/gctx" "github.com/gogf/gf/v2/os/gmetric" ) const ( instrument = "github.com/gogf/gf/example/metric/basic" instrumentVersion = "v1.0" ) var ( meter = gmetric.GetGlobalProvider().Meter(gmetric.MeterOption{ Instrument: instrument, InstrumentVersion: instrumentVersion, Attributes: gmetric.Attributes{ gmetric.NewAttribute("meter_const_attr_1", 1), }, }) counter = meter.MustCounter( "goframe.metric.demo.counter", gmetric.MetricOption{ Help: "This is a simple demo for Counter usage", Unit: "bytes", Attributes: gmetric.Attributes{ gmetric.NewAttribute("metric_const_attr_1", 1), }, }, ) observableCounter = meter.MustObservableCounter( "goframe.metric.demo.observable_counter", gmetric.MetricOption{ Help: "This is a simple demo for ObservableCounter usage", Unit: "%", Attributes: gmetric.Attributes{ gmetric.NewAttribute("metric_const_attr_2", 2), }, }, ) ) func main() { var ctx = gctx.New() // Callback for observable metrics. meter.MustRegisterCallback(func(ctx context.Context, obs gmetric.Observer) error { obs.Observe(observableCounter, 10) return nil }, observableCounter) // Prometheus exporter to export metrics as Prometheus format. exporter, err := prometheus.New( prometheus.WithoutCounterSuffixes(), prometheus.WithoutUnits(), ) if err != nil { g.Log().Fatal(ctx, err) } // OpenTelemetry provider. provider := otelmetric.MustProvider( otelmetric.WithReader(exporter), ) provider.SetAsGlobal() defer provider.Shutdown(ctx) // Counter. counter.Inc(ctx) counter.Add(ctx, 10) // HTTP Server for metrics exporting. otelmetric.StartPrometheusMetricsServer(8000, "/metrics") } ``` 可以看到,我们通过创建 `Meter` 对象或者 `Metric` 对象时的 `MeterOption` 或 `MetricOption` 参数中的 `Attributes` 属性字段来指定常量属性。 执行后,我们访问 [http://127.0.0.1:8000/metrics](http://127.0.0.1:8000/metrics) 看看结果。可以看到绑定到 `Meter` 上的常量属性在两个指标上生效了,但是在各个指标上绑定的常量属性仅在对应的指标上生效了。 ## 变量属性 **变量属性** 是在指标运行时指定的属性键值对,通常只能在运行时才能确定属性的键值对信息,并且键值对可能根据不同的执行场景而发生变化,因此叫做变量属性。 我们来看一个示例: ```go package main import ( "context" "go.opentelemetry.io/otel/exporters/prometheus" "github.com/gogf/gf/contrib/metric/otelmetric/v2" "github.com/gogf/gf/v2/frame/g" "github.com/gogf/gf/v2/os/gctx" "github.com/gogf/gf/v2/os/gmetric" ) const ( instrument = "github.com/gogf/gf/example/metric/basic" instrumentVersion = "v1.0" ) var ( meter = gmetric.GetGlobalProvider().Meter(gmetric.MeterOption{ Instrument: instrument, InstrumentVersion: instrumentVersion, Attributes: gmetric.Attributes{ gmetric.NewAttribute("meter_const_attr_1", 1), }, }) counter = meter.MustCounter( "goframe.metric.demo.counter", gmetric.MetricOption{ Help: "This is a simple demo for Counter usage", Unit: "bytes", Attributes: gmetric.Attributes{ gmetric.NewAttribute("metric_const_attr_1", 1), }, }, ) observableCounter = meter.MustObservableCounter( "goframe.metric.demo.observable_counter", gmetric.MetricOption{ Help: "This is a simple demo for ObservableCounter usage", Unit: "%", Attributes: gmetric.Attributes{ gmetric.NewAttribute("metric_const_attr_2", 2), }, }, ) ) func main() { var ctx = gctx.New() // Callback for observable metrics. meter.MustRegisterCallback(func(ctx context.Context, obs gmetric.Observer) error { obs.Observe(observableCounter, 10, gmetric.Option{ Attributes: gmetric.Attributes{ gmetric.NewAttribute("dynamic_attr_1", 1), }, }) return nil }, observableCounter) // Prometheus exporter to export metrics as Prometheus format. exporter, err := prometheus.New( prometheus.WithoutCounterSuffixes(), prometheus.WithoutUnits(), ) if err != nil { g.Log().Fatal(ctx, err) } // OpenTelemetry provider. provider := otelmetric.MustProvider( otelmetric.WithReader(exporter), ) provider.SetAsGlobal() defer provider.Shutdown(ctx) // Counter. counter.Inc(ctx, gmetric.Option{ Attributes: gmetric.Attributes{ gmetric.NewAttribute("dynamic_attr_2", 2), }, }) counter.Add(ctx, 10, gmetric.Option{ Attributes: gmetric.Attributes{ gmetric.NewAttribute("dynamic_attr_3", 3), }, }) // HTTP Server for metrics exporting. otelmetric.StartPrometheusMetricsServer(8000, "/metrics") } ``` 可以看到,我们在运行通过 `Option` 参数中的 `Attributes` 属性字段来指定指标变量属性,变量属性比较灵活,相同的指标也可以使用不同的变量属性。 同样的,执行后,我们访问 [http://127.0.0.1:8000/metrics](http://127.0.0.1:8000/metrics) 看看结果。 ## 全局属性 **全局属性** 是一种更加灵活的指标属性注入方式,可以根据 `Instrument` 信息进行属性自动注入,并且可以根据 `Instrument` 名称的正则匹配来判断是否往该 `Instrument` 下所有的指标注入指标属性信息。 我们来看一个示例: ```go package main import ( "context" "go.opentelemetry.io/otel/exporters/prometheus" "github.com/gogf/gf/contrib/metric/otelmetric/v2" "github.com/gogf/gf/v2/frame/g" "github.com/gogf/gf/v2/os/gctx" "github.com/gogf/gf/v2/os/gmetric" ) const ( instrument = "github.com/gogf/gf/example/metric/basic" instrumentVersion = "v1.0" ) var ( meter = gmetric.GetGlobalProvider().Meter(gmetric.MeterOption{ Instrument: instrument, InstrumentVersion: instrumentVersion, Attributes: gmetric.Attributes{ gmetric.NewAttribute("meter_const_attr_1", 1), }, }) counter = meter.MustCounter( "goframe.metric.demo.counter", gmetric.MetricOption{ Help: "This is a simple demo for Counter usage", Unit: "bytes", Attributes: gmetric.Attributes{ gmetric.NewAttribute("metric_const_attr_1", 1), }, }, ) observableCounter = meter.MustObservableCounter( "goframe.metric.demo.observable_counter", gmetric.MetricOption{ Help: "This is a simple demo for ObservableCounter usage", Unit: "%", Attributes: gmetric.Attributes{ gmetric.NewAttribute("metric_const_attr_2", 2), }, }, ) ) func main() { var ctx = gctx.New() gmetric.SetGlobalAttributes(gmetric.Attributes{ gmetric.NewAttribute("global_attr_1", 1), }, gmetric.SetGlobalAttributesOption{ Instrument: instrument, InstrumentVersion: instrumentVersion, InstrumentPattern: "", }) // Callback for observable metrics. meter.MustRegisterCallback(func(ctx context.Context, obs gmetric.Observer) error { obs.Observe(observableCounter, 10, gmetric.Option{ Attributes: gmetric.Attributes{ gmetric.NewAttribute("dynamic_attr_1", 1), }, }) return nil }, observableCounter) // Prometheus exporter to export metrics as Prometheus format. exporter, err := prometheus.New( prometheus.WithoutCounterSuffixes(), prometheus.WithoutUnits(), ) if err != nil { g.Log().Fatal(ctx, err) } // OpenTelemetry provider. provider := otelmetric.MustProvider( otelmetric.WithReader(exporter), ) provider.SetAsGlobal() defer provider.Shutdown(ctx) // Counter. counter.Inc(ctx, gmetric.Option{ Attributes: gmetric.Attributes{ gmetric.NewAttribute("dynamic_attr_2", 2), }, }) counter.Add(ctx, 10, gmetric.Option{ Attributes: gmetric.Attributes{ gmetric.NewAttribute("dynamic_attr_3", 3), }, }) // HTTP Server for metrics exporting. otelmetric.StartPrometheusMetricsServer(8000, "/metrics") } ``` 通过 `gmetric.SetGlobalAttributes` 方法设置全局属性,并且根据参数 `gmetric.SetGlobalAttributesOption` 限制影响的指标范围。 同样的,执行后,我们访问 [http://127.0.0.1:8000/metrics](http://127.0.0.1:8000/metrics) 看看结果。可以看到,全局属性已经自动添加到了指标中。 # 监控告警-内置指标 ## 基本介绍 框架内置了 `Go` 基础指标,默认情况下是关闭的,需要手动开启,可以通过创建 `Provider` 时通过 `otelmetric.WithBuiltInMetrics()` 参数开启。 ```go package main import ( "go.opentelemetry.io/otel/exporters/prometheus" "github.com/gogf/gf/contrib/metric/otelmetric/v2" "github.com/gogf/gf/v2/frame/g" "github.com/gogf/gf/v2/os/gctx" "github.com/gogf/gf/v2/os/gmetric" ) const ( instrument = "github.com/gogf/gf/example/metric/basic" instrumentVersion = "v1.0" ) var ( meter = gmetric.GetGlobalProvider().Meter(gmetric.MeterOption{ Instrument: instrument, InstrumentVersion: instrumentVersion, }) counter = meter.MustCounter( "goframe.metric.demo.counter", gmetric.MetricOption{ Help: "This is a simple demo for Counter usage", Unit: "bytes", }, ) ) func main() { var ctx = gctx.New() // Prometheus exporter to export metrics as Prometheus format. exporter, err := prometheus.New( prometheus.WithoutCounterSuffixes(), prometheus.WithoutUnits(), ) if err != nil { g.Log().Fatal(ctx, err) } // OpenTelemetry provider. provider := otelmetric.MustProvider( otelmetric.WithReader(exporter), otelmetric.WithBuiltInMetrics(), ) provider.SetAsGlobal() defer provider.Shutdown(ctx) // Counter. counter.Inc(ctx) counter.Add(ctx, 10) // HTTP Server for metrics exporting. otelmetric.StartPrometheusMetricsServer(8000, "/metrics") } ``` 执行后,访问 [http://127.0.0.1:8000/metrics](http://127.0.0.1:8000/metrics) 查看结果。 ## 内置指标说明 | **指标名称** | **指标类型** | **指标单位** | **指标描述** | | --- | --- | --- | --- | | `process.runtime.go.cgo.calls` | `Counter` | | 当前进程调用的 CGO 次数。 | | `process.runtime.go.gc.count` | `Counter` | | 已完成的 GC 周期的次数。 | | `process.runtime.go.gc.pause_ns` | `Histogram` | `ns` | 在 GC stop-the-world 中暂停的纳秒数量。 | | `process.runtime.go.gc.pause_total_ns` | `Counter` | `ns` | 自程序启动以来,GC stop-the-world 的累计纳秒计数。 | | `process.runtime.go.goroutines` | `Gauge` | | 当前运行的协程数量。 | | `process.runtime.go.lookups` | `Counter` | | 运行时执行的指针查找的数量。 | | `process.runtime.go.mem.heap_alloc` | `Gauge` | `bytes` | 分配的堆对象的字节数。 | | `process.runtime.go.mem.heap_idle` | `Gauge` | `bytes` | 空闲(未使用)的堆内存。 | | `process.runtime.go.mem.heap_inuse` | `Gauge` | `bytes` | 已使用的堆内存。 | | `process.runtime.go.mem.heap_objects` | `Gauge` | | 已分配的堆对象数量。 | | `process.runtime.go.mem.live_objects` | `Gauge` | | 存活对象数量(Mallocs - Frees)。 | | `process.runtime.go.mem.heap_released` | `Gauge` | `bytes` | 已交还给操作系统的堆内存。 | | `process.runtime.go.mem.heap_sys` | `Gauge` | `bytes` | 从操作系统获得的堆内存。 | | `process.runtime.uptime` | `Counter` | `ms` | 自应用程序被初始化以来的毫秒数。 | # 监控告警-组件指标 已支持监控指标的的框架组件,其他组件指标在后续版本中陆续提供。 | 组件 | 文档地址 | | --- | --- | | `HTTP Client` | [HTTPClient-监控指标](../web/http-client-metrics.md) | | `HTTP Server` | [HTTPServer-监控指标](../web/senior-server-metrics.md) | # 组件列表 模块列表包含绝大部分框架的核心模块以及社区模块介绍文档。 ## 核心模块 `GoFrame` 提供了一些基础的、常用的模块,简单、易用和轻量级,并保持极少的外部依赖,这些模块由 `gf` 主仓库细致维护。 ## 社区模块 社区模块主要由社区贡献并维护,大部分也是由 `gf` 主仓库的贡献者提供及维护,存放于 `gogf` 空间下,与 `gf` 主仓库处于同一级别。有的社区模块是从 `gf` 主仓库中剥离出来单独维护的模块,这些模块并不是特别常用,或者对外部依赖较重。 目前社区模块的文档未收录,社区模块使用方法请具体查看源码 `README.MD` 文件,由于社区模块大都是核心模块的具体接口具体,因此也可以查看相关联的核心模块文档。 # 字典类型-gmap ## 基本介绍 支持并发安全开关选项的 `map` 容器,最常用的数据结构。该模块包含多个数据结构的 `map` 容器: `HashMap`、 `TreeMap` 和 `ListMap`。 | 类型 | 数据结构 | 平均复杂度 | 支持排序 | 有序遍历 | 说明 | | --- | --- | --- | --- | --- | --- | | `HashMap` | 哈希表 | `O(1)` | 否 | 否 | 高性能读写操作,内存占用较高,随机遍历 | | `ListMap` | 哈希表+双向链表 | `O(2)` | 否 | 是 | 支持按照写入顺序遍历,内存占用较高 | | `TreeMap` | 红黑树 | `O(log N)` | 是 | 是 | 内存占用紧凑,支持键名排序及有序遍历 | **提示** 此外, `gmap` 模块支持多种以哈希表为基础数据结构的常见类型 `map` 定义: `IntIntMap`、 `IntStrMap`、 `IntAnyMap`、 `StrIntMap`、 `StrStrMap`、 `StrAnyMap`。 从 `v2.10` 版本开始,`gmap` 提供了泛型映射类型,推荐在新项目中使用: - `KVMap[K, V]`:泛型哈希映射,对应 `HashMap` - `ListKVMap[K, V]`:泛型有序映射,对应 `ListMap` - `TreeKVMap[K, V]`:泛型树形映射,对应 `TreeMap` - 支持 `NewXXXWithChecker` 系列函数,可自定义 `nil` 值检查器 **使用场景**: 任何 `map`/哈希表/关联数组使用场景,尤其是并发安全场景中。 **使用方式**: ```go import "github.com/gogf/gf/v2/container/gmap" ``` **接口文档**: [https://pkg.go.dev/github.com/gogf/gf/v2/container/gmap](https://pkg.go.dev/github.com/gogf/gf/v2/container/gmap) # 字典类型-基本使用 ## 泛型映射(推荐使用) 从 `v2.10` 版本开始,`gmap` 提供了泛型映射 `KVMap[K, V]`、`ListKVMap[K, V]` 和 `TreeKVMap[K, V]`,提供类型安全的映射操作。开发者可以使用 `NewKVMap[string, int]()` 等方式创建指定类型的映射,避免类型断言和运行时错误。 ### 泛型哈希映射 `KVMap` ```go package main import ( "fmt" "github.com/gogf/gf/v2/container/gmap" ) func main() { // 创建一个键为string,值为int的泛型哈希映射 m := gmap.NewKVMap[string, int]() // 设置键值对 m.Set("apple", 1) m.Set("banana", 2) m.Set("orange", 3) // 获取值 if value, ok := m.Get("apple"); ok { fmt.Println("apple:", value) // 输出: apple: 1 } // 获取所有键值 fmt.Println("Keys:", m.Keys()) fmt.Println("Values:", m.Values()) // 遍历 m.Iterator(func(k string, v int) bool { fmt.Printf("%s: %d\n", k, v) return true }) } ``` ### 泛型有序映射 `ListKVMap` `ListKVMap` 是按照插入顺序维护键值对的有序映射。 ```go package main import ( "fmt" "github.com/gogf/gf/v2/container/gmap" ) func main() { // 创建有序映射 m := gmap.NewListKVMap[string, int]() // 按顺序插入 m.Set("third", 3) m.Set("first", 1) m.Set("second", 2) // 遍历会按插入顺序输出 m.Iterator(func(k string, v int) bool { fmt.Printf("%s: %d\n", k, v) return true }) // Output: // third: 3 // first: 1 // second: 2 } ``` ### 泛型树形映射 `TreeKVMap` `TreeKVMap` 使用红黑树实现,键值对按照比较函数定义的顺序排序。 ```go package main import ( "fmt" "github.com/gogf/gf/v2/container/gmap" "github.com/gogf/gf/v2/util/gutil" ) func main() { // 创建树形映射,使用字符串比较函数 m := gmap.NewTreeKVMap[string, int](gutil.ComparatorString) // 插入数据 m.Set("banana", 2) m.Set("apple", 1) m.Set("orange", 3) // 遍历会按照键的字母序输出 m.Iterator(func(k string, v int) bool { fmt.Printf("%s: %d\n", k, v) return true }) // Output: // apple: 1 // banana: 2 // orange: 3 } ``` ### 自定义 `nil` 值检查器 **提示** `NilChecker` 是一个可选参数,并非必须提供。它主要用于解决`typed nil`问题,并提供更好的`nil`判断性能。在默认情况下,组件使用**反射**来判断数据是否为`nil`。 对于指针类型、接口类型等,可能需要自定义 `nil` 判断逻辑。`NewKVMapWithChecker` 系列函数支持自定义 `nil` 检查器。 ```go package main import ( "fmt" "github.com/gogf/gf/v2/container/gmap" ) type User struct { ID int Name string } func main() { // 为指针类型创建映射,使用自定义nil检查器 m := gmap.NewKVMapWithChecker[string, *User](func(u *User) bool { return u == nil || u.ID == 0 }) m.Set("user1", &User{ID: 1, Name: "John"}) m.Set("user2", &User{ID: 0, Name: "Invalid"}) // 会被视为nil // GetOrSet在值为nil时会设置新值 user := m.GetOrSet("user2", &User{ID: 2, Name: "Jane"}) fmt.Printf("user2: %+v\n", user) // Output: // user2: &{ID:2 Name:Jane} } ``` ### 泛型映射特点 1. **类型安全**:在编译期就能发现类型错误,避免运行时类型断言失败。 2. **自定义 `nil` 检查(可选)**:通过 `NewXXXWithChecker` 系列函数,可以为泛型类型自定义 `nil` 判断逻辑,解决类型化 `nil` 的问题。默认情况下使用反射判断。 3. **性能优势**:减少了类型断言和反射的开销。 4. **更好的IDE支持**:IDE能够提供更准确的代码补全和类型提示。 **提示** 推荐在新项目中使用泛型映射 `KVMap[K, V]`、`ListKVMap[K, V]` 和 `TreeKVMap[K, V]`,它们提供了更好的类型安全性和开发体验。 ## 传统映射类型 下面介绍传统的映射类型,这些类型在旧版本中被广泛使用,仍然完全支持。 ### 并发安全 `gmap` 支持并发安全选项开关,在默认情况下是 `非并发安全` 的,开发者可以选择开启 `gmap` 的并发安全特性(传递初始化开关参数 `safe` 参数值为 `true`, 必须在初始化时设定,不能运行时动态设定)。如: ```go m := gmap.New(true) ``` 不仅仅是 `gmap` 模块, `goframe` 框架的其他并发安全数据结构也支持并发安全特性开关。 ### 基本使用 ```go package main import ( "fmt" "github.com/gogf/gf/v2/container/gmap" ) func main() { // 创建一个默认的gmap对象, // 默认情况下该gmap对象不支持并发安全特性, // 初始化时可以给定true参数开启并发安全特性。 m := gmap.New() // 设置键值对 for i := 0; i < 10; i++ { m.Set(i, i) } // 查询大小 fmt.Println(m.Size()) // 批量设置键值对(不同的数据类型对象参数不同) m.Sets(map[interface{}]interface{}{ 10 : 10, 11 : 11, }) fmt.Println(m.Size()) // 查询是否存在 fmt.Println(m.Contains(1)) // 查询键值 fmt.Println(m.Get(1)) // 删除数据项 m.Remove(9) fmt.Println(m.Size()) // 批量删除 m.Removes([]interface{}{10, 11}) fmt.Println(m.Size()) // 当前键名列表(随机排序) fmt.Println(m.Keys()) // 当前键值列表(随机排序) fmt.Println(m.Values()) // 查询键名,当键值不存在时,写入给定的默认值 fmt.Println(m.GetOrSet(100, 100)) // 删除键值对,并返回对应的键值 fmt.Println(m.Remove(100)) // 遍历map m.Iterator(func(k interface{}, v interface{}) bool { fmt.Printf("%v:%v ", k, v) return true }) // 自定义写锁操作 m.LockFunc(func(m map[interface{}]interface{}) { m[99] = 99 }) // 自定义读锁操作 m.RLockFunc(func(m map[interface{}]interface{}) { fmt.Println(m[99]) }) // 清空map m.Clear() // 判断map是否为空 fmt.Println(m.IsEmpty()) } ``` 执行后,输出结果为: ```10 12 true 1 11 9 [0 1 2 4 6 7 3 5 8] [3 5 8 0 1 2 4 6 7] 100 100 3:3 5:5 8:8 7:7 0:0 1:1 2:2 4:4 6:6 99 true ``` ### 有序遍历 我们来看一下三种不同类型 `map` 的有序性遍历示例。 ```go package main import ( "fmt" "github.com/gogf/gf/v2/frame/g" "github.com/gogf/gf/v2/container/gmap" "github.com/gogf/gf/v2/util/gutil" ) func main() { array := g.Slice{2, 3, 1, 5, 4, 6, 8, 7, 9} hashMap := gmap.New(true) listMap := gmap.NewListMap(true) treeMap := gmap.NewTreeMap(gutil.ComparatorInt, true) for _, v := range array { hashMap.Set(v, v) } for _, v := range array { listMap.Set(v, v) } for _, v := range array { treeMap.Set(v, v) } fmt.Println("HashMap Keys:", hashMap.Keys()) fmt.Println("HashMap Values:", hashMap.Values()) fmt.Println("ListMap Keys:", listMap.Keys()) fmt.Println("ListMap Values:", listMap.Values()) fmt.Println("TreeMap Keys:", treeMap.Keys()) fmt.Println("TreeMap Values:", treeMap.Values()) } ``` 执行后,输出结果为: ``` HashMap Keys: [4 6 8 7 9 2 3 1 5] HashMap Values: [6 8 4 3 1 5 7 9 2] ListMap Keys: [2 3 1 5 4 6 8 7 9] ListMap Values: [2 3 1 5 4 6 8 7 9] TreeMap Keys: [1 2 3 4 5 6 7 8 9] TreeMap Values: [1 2 3 4 5 6 7 8 9] ``` ### `FilterEmpty/FilterNil` 空值过滤 ```go package main import ( "fmt" "github.com/gogf/gf/v2/container/gmap" "github.com/gogf/gf/v2/frame/g" ) func main() { m1 := gmap.NewFrom(g.MapAnyAny{ "k1": "", "k2": nil, "k3": 0, "k4": 1, }) m2 := gmap.NewFrom(g.MapAnyAny{ "k1": "", "k2": nil, "k3": 0, "k4": 1, }) m1.FilterEmpty() m2.FilterNil() fmt.Println(m1.Map()) fmt.Println(m2.Map()) // Output: // map[k4:1] // map[k1: k3:0 k4:1] } ``` ### `Flip` 键值对反转 ```go package main import ( "fmt" "github.com/gogf/gf/v2/container/gmap" "github.com/gogf/gf/v2/frame/g" ) func main() { var m gmap.Map m.Sets(g.MapAnyAny{ "k1": "v1", "k2": "v2", }) m.Flip() fmt.Println(m.Map()) // May Output: // map[v1:k1 v2:k2] } ``` ### `Keys/Values` 键名/数值列表 ```go package main import ( "fmt" "github.com/gogf/gf/v2/container/gmap" "github.com/gogf/gf/v2/frame/g" ) func main() { var m gmap.Map m.Sets(g.MapAnyAny{ "k1": "v1", "k2": "v2", "k3": "v3", "k4": "v4", }) fmt.Println(m.Keys()) fmt.Println(m.Values()) // May Output: // [k1 k2 k3 k4] // [v2 v3 v4 v1] } ``` ### `Pop/Pops` 随机出栈 ```go package main import ( "fmt" "github.com/gogf/gf/v2/container/gmap" "github.com/gogf/gf/v2/frame/g" ) func main() { var m gmap.Map m.Sets(g.MapAnyAny{ "k1": "v1", "k2": "v2", "k3": "v3", "k4": "v4", }) fmt.Println(m.Pop()) fmt.Println(m.Pops(2)) fmt.Println(m.Size()) // May Output: // k1 v1 // map[k2:v2 k4:v4] // 1 } ``` ### `SetIfNotExist*` 判断性写入 判断性写入是指当指定的键名不存在时则写入并且方法返回 `true`,否则忽略写入并且方法返回 `false`。相关方法如下: - `SetIfNotExist` - `SetIfNotExistFunc` - `SetIfNotExistFuncLock` 方法具体描述请查看接口文档或源码注释。 ```go package main import ( "fmt" "github.com/gogf/gf/v2/container/gmap" ) func main() { var m gmap.Map fmt.Println(m.SetIfNotExist("k1", "v1")) fmt.Println(m.SetIfNotExist("k1", "v1")) fmt.Println(m.Map()) // Output: // true // false // map[k1:v1] } ``` ### `Merge` 字典表合并 ```go package main import ( "fmt" "github.com/gogf/gf/v2/container/gmap" ) func main() { var m1, m2 gmap.Map m1.Set("key1", "val1") m2.Set("key2", "val2") m1.Merge(&m2) fmt.Println(m1.Map()) // May Output: // map[key1:val1 key2:val2] } ``` ### `JSON` 序列化/反序列 `gmap` 模块下的所有容器类型均实现了标准库 `json` 数据格式的序列化/反序列化接口。 1\. `Marshal` ```go package main import ( "encoding/json" "fmt" "github.com/gogf/gf/v2/frame/g" "github.com/gogf/gf/v2/container/gmap" ) func main() { m := gmap.New() m.Sets(g.MapAnyAny{ "name": "john", "score": 100, }) b, _ := json.Marshal(m) fmt.Println(string(b)) } ``` 执行后,输出结果: ``` {"name":"john","score":100} ``` 2. `Unmarshal` ```go package main import ( "encoding/json" "fmt" "github.com/gogf/gf/v2/container/gmap" ) func main() { m := gmap.Map{} s := []byte(`{"name":"john","score":100}`) json.Unmarshal(s, &m) fmt.Println(m.Map()) } ``` 执行后,输出结果: ``` map[name:john score:100] ``` # 字典类型-性能测试 ## 性能测试 ### 并发安全 [https://github.com/gogf/gf/blob/master/container/gmap/gmap\_z\_bench\_safe\_test.go](https://github.com/gogf/gf/blob/master/container/gmap/gmap_z_bench_safe_test.go) ``` goos: linux goarch: amd64 Benchmark_IntIntMap_Set-4 10000000 202 ns/op 15 B/op 0 allocs/op Benchmark_IntAnyMap_Set-4 10000000 262 ns/op 29 B/op 1 allocs/op Benchmark_IntStrMap_Set-4 10000000 241 ns/op 22 B/op 0 allocs/op Benchmark_AnyAnyMap_Set-4 5000000 359 ns/op 40 B/op 2 allocs/op Benchmark_StrIntMap_Set-4 5000000 305 ns/op 26 B/op 1 allocs/op Benchmark_StrAnyMap_Set-4 5000000 354 ns/op 40 B/op 2 allocs/op Benchmark_StrStrMap_Set-4 5000000 338 ns/op 32 B/op 1 allocs/op Benchmark_IntIntMap_Get-4 20000000 86.6 ns/op 0 B/op 0 allocs/op Benchmark_IntAnyMap_Get-4 30000000 69.7 ns/op 0 B/op 0 allocs/op Benchmark_IntStrMap_Get-4 30000000 69.6 ns/op 0 B/op 0 allocs/op Benchmark_AnyAnyMap_Get-4 20000000 74.4 ns/op 0 B/op 0 allocs/op Benchmark_StrIntMap_Get-4 20000000 116 ns/op 7 B/op 0 allocs/op Benchmark_StrAnyMap_Get-4 20000000 92.3 ns/op 7 B/op 0 allocs/op Benchmark_StrStrMap_Get-4 20000000 91.9 ns/op 7 B/op 0 allocs/op ``` ### 非并发安全 [https://github.com/gogf/gf/blob/master/container/gmap/gmap\_z\_bench\_unsafe\_test.go](https://github.com/gogf/gf/blob/master/container/gmap/gmap_z_bench_unsafe_test.go) ``` goos: linux goarch: amd64 Benchmark_Unsafe_IntIntMap_Set-4 10000000 318 ns/op 62 B/op 0 allocs/op Benchmark_Unsafe_IntAnyMap_Set-4 5000000 282 ns/op 57 B/op 1 allocs/op Benchmark_Unsafe_IntStrMap_Set-4 5000000 332 ns/op 82 B/op 1 allocs/op Benchmark_Unsafe_AnyAnyMap_Set-4 3000000 471 ns/op 73 B/op 2 allocs/op Benchmark_Unsafe_StrIntMap_Set-4 5000000 429 ns/op 82 B/op 1 allocs/op Benchmark_Unsafe_StrAnyMap_Set-4 3000000 424 ns/op 73 B/op 2 allocs/op Benchmark_Unsafe_StrStrMap_Set-4 2000000 515 ns/op 96 B/op 2 allocs/op Benchmark_Unsafe_IntIntMap_Get-4 10000000 133 ns/op 0 B/op 0 allocs/op Benchmark_Unsafe_IntAnyMap_Get-4 20000000 134 ns/op 0 B/op 0 allocs/op Benchmark_Unsafe_IntStrMap_Get-4 10000000 126 ns/op 0 B/op 0 allocs/op Benchmark_Unsafe_AnyAnyMap_Get-4 10000000 166 ns/op 0 B/op 0 allocs/op Benchmark_Unsafe_StrIntMap_Get-4 5000000 246 ns/op 7 B/op 0 allocs/op Benchmark_Unsafe_StrAnyMap_Get-4 10000000 238 ns/op 7 B/op 0 allocs/op Benchmark_Unsafe_StrStrMap_Get-4 5000000 229 ns/op 7 B/op 0 allocs/op ``` ### 不同类型map性能 [https://github.com/gogf/gf/blob/master/container/gmap/gmap\_z\_bench\_maps\_test.go](https://github.com/gogf/gf/blob/master/container/gmap/gmap_z_bench_maps_test.go) ``` goos: linux goarch: amd64 Benchmark_HashMap_Set-4 5000000 349 ns/op 40 B/op 2 allocs/op Benchmark_ListMap_Set-4 3000000 455 ns/op 87 B/op 3 allocs/op Benchmark_TreeMap_Set-4 3000000 481 ns/op 28 B/op 2 allocs/op Benchmark_HashMap_Get-4 30000000 67.8 ns/op 0 B/op 0 allocs/op Benchmark_ListMap_Get-4 20000000 74.5 ns/op 0 B/op 0 allocs/op Benchmark_TreeMap_Get-4 20000000 189 ns/op 8 B/op 1 allocs/op ``` ### `gmap` 与 `sync.Map` 性能比较 go语言从 `1.9` 版本开始引入了并发安全的 `sync.Map`,但 `gmap` 比较于标准库的 `sync.Map` 性能更加优异,并且功能更加丰富。 我们来看看基准测试对比结果: [https://github.com/gogf/gf/blob/master/container/gmap/gmap\_z\_bench\_syncmap\_test.go](https://github.com/gogf/gf/blob/master/container/gmap/gmap_z_bench_syncmap_test.go) ``` goos: linux goarch: amd64 Benchmark_GMapSet-4 10000000 209 ns/op 15 B/op 0 allocs/op Benchmark_SyncMapSet-4 3000000 451 ns/op 67 B/op 3 allocs/op Benchmark_GMapGet-4 30000000 66.4 ns/op 0 B/op 0 allocs/op Benchmark_SyncMapGet-4 30000000 36.0 ns/op 0 B/op 0 allocs/op Benchmark_GMapRemove-4 10000000 207 ns/op 0 B/op 0 allocs/op Benchmark_SyncMapRmove-4 30000000 42.4 ns/op 0 B/op 0 allocs/op ``` # 字典类型-方法介绍 **提示** 以下常用方法列表,文档更新可能滞后于代码新特性,更多的方法及示例请参考代码文档: [https://pkg.go.dev/github.com/gogf/gf/v2/container/gmap](https://pkg.go.dev/github.com/gogf/gf/v2/container/gmap) ## `New` - 说明: `New` 创建并返回一个空的 `AnyAnyMap`。参数 `safe` 用于指定是否使用并发安全的 `map`,默认情况下为 `false`。 - 格式: ```go New(safe ...bool) *Map ``` - 示例: ```go func ExampleNew() { m := gmap.New() // Add data. m.Set("key1", "val1") // Print size. fmt.Println(m.Size()) addMap := make(map[interface{}]interface{}) addMap["key2"] = "val2" addMap["key3"] = "val3" addMap[1] = 1 fmt.Println(m.Values()) // Batch add data. m.Sets(addMap) // Gets the value of the corresponding key. fmt.Println(m.Get("key3")) // Get the value by key, or set it with given key-value if not exist. fmt.Println(m.GetOrSet("key4", "val4")) // Set key-value if the key does not exist, then return true; or else return false. fmt.Println(m.SetIfNotExist("key3", "val3")) // Remove key m.Remove("key2") fmt.Println(m.Keys()) // Batch remove keys. m.Removes([]interface{}{"key1", 1}) fmt.Println(m.Keys()) // Contains checks whether a key exists. fmt.Println(m.Contains("key3")) // Flip exchanges key-value of the map, it will change key-value to value-key. m.Flip() fmt.Println(m.Map()) // Clear deletes all data of the map. m.Clear() fmt.Println(m.Size()) // May Output: // 1 // [val1] // val3 // val4 // false // [key4 key1 key3 1] // [key4 key3] // true // map[val3:key3 val4:key4] // 0 } ``` ## `NewFrom` - 说明: `NewFrom` 使用给定 `map` 的数据创建并返回 `AnyAnyMap`。 - 注意:入参 `map` 将被设置为底层数据映射(无深度拷贝),在外部更改 `map` 时,可能会同时出现一些安全问题。可选参数 `safe` 指定是否在并发安全中使用此结构,默认情况下为 `false`。 - 格式: ```go NewFrom(data map[interface{}]interface{}, safe ...bool) *Map ``` - 示例: ```go func ExampleNewFrom() { m := gmap.New() m.Set("key1", "val1") fmt.Println(m) n := gmap.NewFrom(m.MapCopy(), true) fmt.Println(n) // Output: // {"key1":"val1"} // {"key1":"val1"} } ``` ## `Iterator` - 说明: `Iterator` 使用自定义回调函数 `f` 以只读方式迭代 `hashmap`。如果 `f` 返回 `true`,则继续迭代,返回 `false` 则停止。 - 格式: ```go Iterator(f func(k interface{}, v interface{}) bool) ``` - 示例: ```go func ExampleAnyAnyMap_Iterator() { m := gmap.New() for i := 0; i < 10; i++ { m.Set(i, i*2) } var totalKey, totalValue int m.Iterator(func(k interface{}, v interface{}) bool { totalKey += k.(int) totalValue += v.(int) return totalKey < 10 }) fmt.Println("totalKey:", totalKey) fmt.Println("totalValue:", totalValue) // May Output: // totalKey: 11 // totalValue: 22 } ``` ## `Clone` - 说明: `Clone` 返回一个新的 `AnyAnyMap`,其中包含当前 `map` 数据的副本。 - 格式: ```go Clone(safe ...bool) *AnyAnyMap ``` - 示例: ```go func ExampleAnyAnyMap_Clone() { m := gmap.New() m.Set("key1", "val1") fmt.Println(m) n := m.Clone() fmt.Println(n) // Output: // {"key1":"val1"} // {"key1":"val1"} } ``` ## `Map` - 说明: `Map` 返回底层数据 `map`。 - 注意:如果在并发安全中,它将返回底层数据的副本,否则返回指向底层数据的指针。 - 格式: ```go Map() map[interface{}]interface{} ``` - 示例: ```go func ExampleAnyAnyMap_Map() { // non concurrent-safety, a pointer to the underlying data m1 := gmap.New() m1.Set("key1", "val1") fmt.Println("m1:", m1) n1 := m1.Map() fmt.Println("before n1:", n1) m1.Set("key1", "val2") fmt.Println("after n1:", n1) // concurrent-safety, copy of underlying data m2 := gmap.New(true) m2.Set("key1", "val1") fmt.Println("m1:", m2) n2 := m2.Map() fmt.Println("before n2:", n2) m2.Set("key1", "val2") fmt.Println("after n2:", n2) // Output: // m1: {"key1":"val1"} // before n1: map[key1:val1] // after n1: map[key1:val2] // m1: {"key1":"val1"} // before n2: map[key1:val1] // after n2: map[key1:val1] } ``` ## `MapCopy` - 说明: `MapCopy` 返回 `map` 的数据的副本。 - 格式: ```go MapCopy() map[interface{}]interface{} ``` - 示例: ```go func ExampleAnyAnyMap_MapCopy() { m := gmap.New() m.Set("key1", "val1") m.Set("key2", "val2") fmt.Println(m) n := m.MapCopy() fmt.Println(n) // Output: // {"key1":"val1","key2":"val2"} // map[key1:val1 key2:val2] } ``` ## `MapStrAny` - 说明: `MapStrAny` 以 `map[string]interface{}` 的形式返回 `map` 的数据的副本。 - 格式: ```go MapStrAny() map[string]interface{} ``` - 示例: ```go func ExampleAnyAnyMap_MapStrAny() { m := gmap.New() m.Set(1001, "val1") m.Set(1002, "val2") n := m.MapStrAny() fmt.Println(n) // Output: // map[1001:val1 1002:val2] } ``` ## `FilterEmpty` - 说明: `FilterEmpty` 删除值为空的所有键值对。如: `0`, `nil`, `false`, `""`, `len(slice/map/chan) == 0` 这样的值被认为是空的。 - 格式: ```go FilterEmpty() ``` - 示例: ```go func ExampleAnyAnyMap_FilterEmpty() { m := gmap.NewFrom(g.MapAnyAny{ "k1": "", "k2": nil, "k3": 0, "k4": 1, }) m.FilterEmpty() fmt.Println(m.Map()) // Output: // map[k4:1] } ``` ## `FilterNil` - 说明: `FilterNil` 删除其值为 `nil` 的所有键值对。 - 格式: ```go FilterNil() ``` - 示例: ```go func ExampleAnyAnyMap_FilterNil() { m := gmap.NewFrom(g.MapAnyAny{ "k1": "", "k2": nil, "k3": 0, "k4": 1, }) m.FilterNil() fmt.Println(m.Map()) // May Output: // map[k1: k3:0 k4:1] } ``` ## `Set` - 说明: `Set` 为 `map` 设置 `key/value`。 - 格式: ```go Set(key interface{}, value interface{}) ``` - 示例: ```go func ExampleAnyAnyMap_Set() { m := gmap.New() m.Set("key1", "val1") fmt.Println(m) // Output: // {"key1":"val1"} } ``` ## `Sets` - 说明: `Sets` 为 `map` 批量设置 `key/` `value`。 - 格式: ```go Sets(data map[interface{}]interface{}) ``` - 示例: ```go func ExampleAnyAnyMap_Sets() { m := gmap.New() addMap := make(map[interface{}]interface{}) addMap["key1"] = "val1" addMap["key2"] = "val2" addMap["key3"] = "val3" m.Sets(addMap) fmt.Println(m) // Output: // {"key1":"val1","key2":"val2","key3":"val3"} } ``` ## `Search` - 说明: `Search` 使用参数 `key` 搜索 `map`。如果找到 `key`,则返回其对应的键值,并且返回参数 `found` 为 `true`,否则为 `false`。 - 格式: ```go Search(key interface{}) (value interface{}, found bool) ``` - 示例: ```go func ExampleAnyAnyMap_Search() { m := gmap.New() m.Set("key1", "val1") value, found := m.Search("key1") if found { fmt.Println("find key1 value:", value) } value, found = m.Search("key2") if !found { fmt.Println("key2 not find") } // Output: // find key1 value: val1 // key2 not find } ``` ## `Get` - 说明: `Get` 返回参数 `key` 对应的值 `value`,如 `key` 不存在,则返回 `Nil`。 - 格式: ```go Get(key interface{}) (value interface{}) ``` - 示例: ```go func ExampleAnyAnyMap_Get() { m := gmap.New() m.Set("key1", "val1") fmt.Println("key1 value:", m.Get("key1")) fmt.Println("key2 value:", m.Get("key2")) // Output: // key1 value: val1 // key2 value: } ``` ## `Pop` - 说明: `Pop` 从 `map` 中随机取出返回一个键值对,并在内部删除该键值对。 - 格式: ```go Pop() (key, value interface{}) ``` - 示例: ```go func ExampleAnyAnyMap_Pop() { var m gmap.Map m.Sets(g.MapAnyAny{ "k1": "v1", "k2": "v2", "k3": "v3", "k4": "v4", }) fmt.Println(m.Pop()) // May Output: // k1 v1 } ``` ## `Pops` - 说明: `Pops` 从 `map` 中随机取出并删除 `size` 个键值对。如果 `size == -1`,则删除并返回所有键值对。 - 格式: ```go Pops(size int) map[interface{}]interface{} ``` - 示例: ```go func ExampleAnyAnyMap_Pops() { var m gmap.Map m.Sets(g.MapAnyAny{ "k1": "v1", "k2": "v2", "k3": "v3", "k4": "v4", }) fmt.Println(m.Pops(-1)) fmt.Println("size:", m.Size()) m.Sets(g.MapAnyAny{ "k1": "v1", "k2": "v2", "k3": "v3", "k4": "v4", }) fmt.Println(m.Pops(2)) fmt.Println("size:", m.Size()) // May Output: // map[k1:v1 k2:v2 k3:v3 k4:v4] // size: 0 // map[k1:v1 k2:v2] // size: 2 } ``` ## `GetOrSet` - 说明: `GetOrSet` 如 `key` 存在,则返回 `value`,如 `key` 不存在,使用 `key` 和 `value` 给 `map` 设置键值,然后返回该值。 - 格式: ```go GetOrSet(key interface{}, value interface{}) interface{} ``` - 示例: ```go func ExampleAnyAnyMap_GetOrSet() { m := gmap.New() m.Set("key1", "val1") fmt.Println(m.GetOrSet("key1", "NotExistValue")) fmt.Println(m.GetOrSet("key2", "val2")) // Output: // val1 // val2 } ``` ## `GetOrSetFunc` - 说明: `GetOrSetFunc` 如 `key` 存在,则返回 `value`,如 `key` 不存在,使用 `key` 和 `func f` 的返回值 给 `map` 设置键值,然后返回该值。 - 格式: ```go GetOrSetFunc(key interface{}, f func() interface{}) interface{} ``` - 示例: ```go func ExampleAnyAnyMap_GetOrSetFunc() { m := gmap.New() m.Set("key1", "val1") fmt.Println(m.GetOrSetFunc("key1", func() interface{} { return "NotExistValue" })) fmt.Println(m.GetOrSetFunc("key2", func() interface{} { return "NotExistValue" })) // Output: // val1 // NotExistValue } ``` ## `GetOrSetFuncLock` - 说明: `GetOrSetFunc` 如 `key` 存在,则返回 `value`,如 `key` 不存在,使用 `key` 和 `func f` 的返回值 给 `map` 设置键值,然后返回该值。 - 注意: `GetOrSetFuncLock` 与 `GetOrSetFunc` 函数的不同之处在于它在写锁中执行函数 `f`。 - 格式: ```go GetOrSetFuncLock(key interface{}, f func() interface{}) interface{} ``` - 示例: ```go func ExampleAnyAnyMap_GetOrSetFuncLock() { m := gmap.New() m.Set("key1", "val1") fmt.Println(m.GetOrSetFuncLock("key1", func() interface{} { return "NotExistValue" })) fmt.Println(m.GetOrSetFuncLock("key2", func() interface{} { return "NotExistValue" })) // Output: // val1 // NotExistValue } ``` ## `GetVar` - 说明: `GetVar` 根据键名 `key` 查询并返回对应的键值,键值使用泛型类型 `*gvar.Var` 返回。 - 格式: ```go GetVar(key interface{}) *gvar.Var ``` - 示例: ```go func ExampleAnyAnyMap_GetVar() { m := gmap.New() m.Set("key1", "val1") fmt.Println(m.GetVar("key1")) fmt.Println(m.GetVar("key2").IsNil()) // Output: // val1 // true } ``` ## `GetVarOrSet` - 说明: `GetVarOrSet` 根据键名 `key` 查询并返回对应的键值。当对应的键值不存在时,使用 `value` 设置该键值,并返回查询/设置的键值。键值使用泛型类型 `*gvar.Var` 返回。 - 格式: ```go GetVarOrSet(key interface{}, value interface{}) *gvar.Var ``` - 示例: ```go func ExampleAnyAnyMap_GetVarOrSet() { m := gmap.New() m.Set("key1", "val1") fmt.Println(m.GetVarOrSet("key1", "NotExistValue")) fmt.Println(m.GetVarOrSet("key2", "val2")) // Output: // val1 // val2 } ``` ## `GetVarOrSetFunc` - 说明: `GetVarOrSetFunc` 根据键名 `key` 查询并返回对应的键值。当对应的键值不存在时,使用 `func f` 的返回值设置该键值,并返回查询/设置的键值。键值使用泛型类型 `*gvar.Var` 返回。 - 格式: ```go GetVarOrSetFunc(key interface{}, f func() interface{}) *gvar.Var ``` - 示例: ```go func ExampleAnyAnyMap_GetVarOrSetFunc() { m := gmap.New() m.Set("key1", "val1") fmt.Println(m.GetVarOrSetFunc("key1", func() interface{} { return "NotExistValue" })) fmt.Println(m.GetVarOrSetFunc("key2", func() interface{} { return "NotExistValue" })) // Output: // val1 // NotExistValue } ``` ## `GetVarOrSetFuncLock` - 说明: `GetVarOrSetFuncLock` 根据键名 `key` 查询并返回对应的键值。当对应的键值不存在时,使用 `func f` 的返回值设置该键值,并返回查询/设置的键值。键值使用泛型类型 `*gvar.Var` 返回。 - 注意: `GetVarOrSetFuncLock` 与 `GetVarOrSetFunc` 函数的不同之处在于它在写锁中执行函数 `f`。即当有多个 `goroutine` 同时调用该方法时,函数 `f` 将会在执行之前被阻塞。 - 格式: ```go GetVarOrSetFuncLock(key interface{}, f func() interface{}) *gvar.Var ``` - 示例: ```go func ExampleAnyAnyMap_GetVarOrSetFuncLock() { m := gmap.New() m.Set("key1", "val1") fmt.Println(m.GetVarOrSetFuncLock("key1", func() interface{} { return "NotExistValue" })) fmt.Println(m.GetVarOrSetFuncLock("key2", func() interface{} { return "NotExistValue" })) // Output: // val1 // NotExistValue } ``` ## `SetIfNotExist` - 说明:如果 `key` 不存在,则 `SetIfNotExist` 为 `map` 设置值键值对 `key/value`,并且返回 `true`。如果 `key` 存在,则返回 `false`,而 `value` 将被忽略。 - 格式: ```go SetIfNotExist(key interface{}, value interface{}) bool ``` - 示例: ```go func ExampleAnyAnyMap_SetIfNotExist() { var m gmap.Map fmt.Println(m.SetIfNotExist("k1", "v1")) fmt.Println(m.SetIfNotExist("k1", "v1")) fmt.Println(m.Map()) // Output: // true // false // map[k1:v1] } ``` ## `SetIfNotExistFunc` - 说明:如果 `key` 不存在,则 `SetIfNotExistFunc` 为 `map` 设置值为函数 `f` 的返回值,并且返回 `true`。如果 `key` 存在,则返回 `false`,并且 `value` 将被忽略。 - 格式: ```go SetIfNotExistFunc(key interface{}, f func() interface{}) bool ``` - 示例: ```go func ExampleAnyAnyMap_SetIfNotExistFunc() { var m gmap.Map fmt.Println(m.SetIfNotExistFunc("k1", func() interface{} { return "v1" })) fmt.Println(m.SetIfNotExistFunc("k1", func() interface{} { return "v1" })) fmt.Println(m.Map()) // Output: // true // false // map[k1:v1] } ``` ## `SetIfNotExistFuncLock` - 说明:如果 `key` 不存在,则 `SetIfNotExistFunc` 为 `map` 设置值为 `func c` 的返回值,然后返回 `true`。如果 `key` 存在,则返回 `false`,而 `value` 将被忽略。 - 注意: `SetIfNotExistFuncLock` 与 `SetIfNotExistFunc` 函数的不同之处在于它在 `mutex.Lock` 中执行函数 `f`。 - 格式: ```go SetIfNotExistFuncLock(key interface{}, f func() interface{}) bool ``` - 示例: ```go func ExampleAnyAnyMap_SetIfNotExistFuncLock() { var m gmap.Map fmt.Println(m.SetIfNotExistFuncLock("k1", func() interface{} { return "v1" })) fmt.Println(m.SetIfNotExistFuncLock("k1", func() interface{} { return "v1" })) fmt.Println(m.Map()) // Output: // true // false // map[k1:v1] } ``` ## `Remove` - 说明:按给定的 `key` 从 `map` 中删除 `value`,并返回此删除的 `value`。 - 格式: ```go Remove(key interface{}) (value interface{}) ``` - 示例: ```go func ExampleAnyAnyMap_Remove() { var m gmap.Map m.Set("k1", "v1") fmt.Println(m.Remove("k1")) fmt.Println(m.Remove("k2")) // Output: // v1 // } ``` ## `Removes` - 说明: `Removes` 按给定的 `key` 批量删除 `map` 的 `value`。 - 格式: ```go Removes(keys []interface{}) ``` - 示例: ```go func ExampleAnyAnyMap_Removes() { var m gmap.Map m.Sets(g.MapAnyAny{ "k1": "v1", "k2": "v2", "k3": "v3", "k4": "v4", }) removeList := make([]interface{}, 2) removeList = append(removeList, "k1") removeList = append(removeList, "k2") m.Removes(removeList) fmt.Println(m.Map()) // Output: // map[k3:v3 k4:v4] } ``` ## `Keys` - 说明: `Keys` 将 `map` 的所有 `key` 作为 `slice` 返回。 - 格式: ```go Keys() []interface{} ``` - 示例: ```go func ExampleAnyAnyMap_Keys() { var m gmap.Map m.Sets(g.MapAnyAny{ "k1": "v1", "k2": "v2", "k3": "v3", "k4": "v4", }) fmt.Println(m.Keys()) // Output: // [k1 k2 k3 k4] } ``` ## `Values` - 说明: `Values` 将 `map` 的所有 `value` 作为 `slice` 返回。 - 格式: ```go Values() []interface{} ``` - 示例: ```go func ExampleAnyAnyMap_Values() { var m gmap.Map m.Sets(g.MapAnyAny{ "k1": "v1", "k2": "v2", "k3": "v3", "k4": "v4", }) fmt.Println(m.Values()) // May Output: // [v1 v2 v3 v4] } ``` ## `Contains` - 说明: `Contains` 检查 `key` 是否存在。如果 `key` 存在,则返回 `true`,否则返回 `false`。 - 注意:键名类型为 `interface{}`,因此匹配判断需要保证类型和数值一致。 - 格式: ```go Contains(key interface{}) bool ``` - 示例: ```go func ExampleAnyAnyMap_Contains() { var m gmap.Map m.Sets(g.MapAnyAny{ "k1": "v1", "k2": "v2", "k3": "v3", "k4": "v4", }) fmt.Println(m.Contains("k1")) fmt.Println(m.Contains("k5")) // Output: // true // false } ``` ## `Size` - 说明: `Size` 返回 `map` 的大小。 - 格式: ```go Size() int ``` - 示例: ```go func ExampleAnyAnyMap_Size() { var m gmap.Map m.Sets(g.MapAnyAny{ "k1": "v1", "k2": "v2", "k3": "v3", "k4": "v4", }) fmt.Println(m.Size()) // Output: // 4 } ``` ## `IsEmpty` - 说明: `IsEmpty` 检查 `map` 是否为空。如果 `map` 为空,则返回 `true`,否则返回 `false`。 - 格式: ```go IsEmpty() bool ``` - 示例: ```go func ExampleAnyAnyMap_IsEmpty() { var m gmap.Map fmt.Println(m.IsEmpty()) m.Set("k1", "v1") fmt.Println(m.IsEmpty()) // Output: // true // false } ``` ## `Clear` - 说明: `Clear` 删除 `map` 的所有数据。 - 格式: ```go Clear() ``` - 示例: ```go func ExampleAnyAnyMap_Clear() { var m gmap.Map m.Sets(g.MapAnyAny{ "k1": "v1", "k2": "v2", "k3": "v3", "k4": "v4", }) m.Clear() fmt.Println(m.Map()) // Output: // map[] } ``` ## `Replace` - 说明: `Replace` 用给定的 `data` 完整替换 `map` 的 `value`。 - 格式: ```go Replace(data map[interface{}]interface{}) ``` - 示例: ```go func ExampleAnyAnyMap_Replace() { var m gmap.Map m.Sets(g.MapAnyAny{ "k1": "v1", }) var n gmap.Map n.Sets(g.MapAnyAny{ "k2": "v2", }) fmt.Println(m.Map()) m.Replace(n.Map()) fmt.Println(m.Map()) n.Set("k2", "v1") fmt.Println(m.Map()) // Output: // map[k1:v1] // map[k2:v2] // map[k2:v1] } ``` ## `LockFunc` - 说明: `LockFunc` 在写锁中执行函数 `f`。 - 格式: ```go LockFunc(f func(m map[interface{}]interface{})) ``` - 示例: ```go func ExampleAnyAnyMap_LockFunc() { var m gmap.Map m.Sets(g.MapAnyAny{ "k1": 1, "k2": 2, "k3": 3, "k4": 4, }) m.LockFunc(func(m map[interface{}]interface{}) { totalValue := 0 for _, v := range m { totalValue += v.(int) } fmt.Println("totalValue:", totalValue) }) // Output: // totalValue: 10 } ``` ## `RLockFunc` - 说明: `RLockFunc` 在读锁中执行函数 `f`。 - 格式: ```go RLockFunc(f func(m map[interface{}]interface{})) ``` - 示例: ```go func ExampleAnyAnyMap_RLockFunc() { var m gmap.Map m.Sets(g.MapAnyAny{ "k1": 1, "k2": 2, "k3": 3, "k4": 4, }) m.RLockFunc(func(m map[interface{}]interface{}) { totalValue := 0 for _, v := range m { totalValue += v.(int) } fmt.Println("totalValue:", totalValue) }) // Output: // totalValue: 10 } ``` ## `Flip` - 说明: `Flip` 将 `map` 的 `key` 与 `value` 进行交换。 - 格式: ```go Flip() ``` - 示例: ```go func ExampleAnyAnyMap_Flip() { var m gmap.Map m.Sets(g.MapAnyAny{ "k1": "v1", }) m.Flip() fmt.Println(m.Map()) // Output: // map[v1:k1] } ``` ## `Merge` - 说明: `Merge` 合并两个AnyAnyMap。入参 `map` 将合并到原 `map` 中。 - 格式: ```go Merge(other *AnyAnyMap) ``` - 示例: ```go func ExampleAnyAnyMap_Merge() { var m1, m2 gmap.Map m1.Set("key1", "val1") m2.Set("key2", "val2") m1.Merge(&m2) fmt.Println(m1.Map()) // May Output: // map[key1:val1 key2:val2] } ``` ## `String` - 说明: `String` 以字符串形式返回 `map`。 - 格式: ```go String() string ``` - 示例: ```go func ExampleAnyAnyMap_String() { var m gmap.Map m.Sets(g.MapAnyAny{ "k1": "v1", }) fmt.Println(m.String()) // Output: // {"k1":"v1"} } ``` ## `MarshalJSON` - 说明: `MarshalJSON` 实现 `json.Marshal` 的接口。 - 格式: ```go MarshalJSON() ([]byte, error) ``` - 示例: ```go func ExampleAnyAnyMap_MarshalJSON() { var m gmap.Map m.Sets(g.MapAnyAny{ "k1": "v1", "k2": "v2", "k3": "v3", "k4": "v4", }) bytes, err := m.MarshalJSON() if err == nil { fmt.Println(gconv.String(bytes)) } // Output: // {"k1":"v1","k2":"v2","k3":"v3","k4":"v4"} } ``` ## `UnmarshalJSON` - 说明: `UnmarshalJSON` 实现了 `json.Unmarshal` 的接口。 - 格式: ```go UnmarshalJSON(b []byte) error ``` - 示例: ```go func ExampleAnyAnyMap_UnmarshalJSON() { var m gmap.Map m.Sets(g.MapAnyAny{ "k1": "v1", "k2": "v2", "k3": "v3", "k4": "v4", }) var n gmap.Map err := n.UnmarshalJSON(gconv.Bytes(m.String())) if err == nil { fmt.Println(n.Map()) } // Output: // map[k1:v1 k2:v2 k3:v3 k4:v4] } ``` ## `UnmarshalValue` - 说明: `UnmarshalValue` 是一个接口实现,它通过任意类型的变量初始化当前 `map`。 - 格式: ```go UnmarshalValue(value interface{}) (err error) ``` - 示例: ```go func ExampleAnyAnyMap_UnmarshalValue() { var m gmap.Map m.Sets(g.MapAnyAny{ "k1": "v1", "k2": "v2", "k3": "v3", "k4": "v4", }) var n gmap.Map err := n.UnmarshalValue(m.String()) if err == nil { fmt.Println(n.Map()) } // Output: // map[k1:v1 k2:v2 k3:v3 k4:v4] } ``` # 数组类型-garray ## 基本介绍 数组容器,提供普通数组,及排序数组,支持数据项唯一性矫正,支持并发安全开关控制。 **使用场景**: 数组操作。 **使用方式**: ```go import "github.com/gogf/gf/v2/container/garray" ``` **接口文档**: [https://pkg.go.dev/github.com/gogf/gf/v2/container/garray](https://pkg.go.dev/github.com/gogf/gf/v2/container/garray) 简要说明: 1. `garray`模块下的对象及方法较多,建议仔细看看接口文档。 2. `garray`支持`int`/`string`/`interface{}`三种常用的数据类型。 3. `garray`支持普通数组和排序数组,普通数组的结构体名称定义为`*Array`格式,排序数组的结构体名称定义为`Sorted*Array`格式,如下: - `Array`, `intArray`, `StrArray` - `SortedArray`, `SortedIntArray`, `SortedStrArray` - 其中排序数组`SortedArray`,需要给定排序比较方法,在工具包`gutil`中也定义了很多`Comparator*`比较方法 4. 从`v2.10`版本开始,`garray`提供了泛型数组类型: - `TArray[T]`:泛型普通数组,提供类型安全的数组操作 - `SortedTArray[T]`:泛型排序数组,支持自定义比较函数 - 推荐在新项目中使用泛型数组,享受编译时类型检查带来的安全性和更好的IDE支持 # 数组类型-基本使用 ## 泛型数组(推荐使用) 从`v2.10`版本开始,`garray`提供了泛型数组`TArray[T]`和`SortedTArray[T]`,提供类型安全的数组操作,避免类型断言和运行时错误,享受编译时类型检查带来的安全性。 ### 泛型普通数组`TArray` ```go package main import ( "fmt" "github.com/gogf/gf/v2/container/garray" ) func main() { // 创建int类型的泛型数组 intArr := garray.NewTArray[int]() intArr.Append(10) intArr.Append(20) intArr.Append(15) intArr.Append(30) fmt.Println(intArr.Slice()) // 创建string类型的泛型数组 strArr := garray.NewTArray[string]() strArr.Append("We") strArr.Append("are") strArr.Append("GF") strArr.Append("fans") fmt.Println(strArr.Slice()) } ``` 执行后,输出结果为: ``` [10 20 15 30] [We are GF fans] ``` ### 泛型排序数组`SortedTArray` 泛型排序数组需要传入一个比较函数,用于定义元素的排序规则。`gutil`包提供了常用的比较函数`ComparatorT`。 ```go package main import ( "fmt" "github.com/gogf/gf/v2/container/garray" "github.com/gogf/gf/v2/util/gutil" ) func main() { // 创建字符串类型的排序数组(升序) s := garray.NewSortedTArray[string](gutil.ComparatorT) s.Append("b") s.Append("d") s.Append("c") s.Append("a") fmt.Println(s.Slice()) // 创建自定义比较函数的降序排序数组 intArr := garray.NewSortedTArray[int](func(a, b int) int { if a > b { return -1 } if a < b { return 1 } return 0 }) intArr.Append(2) intArr.Append(3) intArr.Append(1) fmt.Println(intArr.Slice()) } ``` 执行后,输出结果为: ``` [a b c d] [3 2 1] ``` ### 泛型数组特点 泛型数组具有以下特点: 1. **类型安全**:在编译期就能发现类型错误,避免运行时类型转换失败。 2. **更好的IDE支持**:IDE能够提供更准确的代码补全和类型提示。 3. **性能优势**:减少了类型断言的开销。 4. **与旧版本兼容**:泛型数组与传统的`Array`、`IntArray`、`StrArray`等类型可以共存。 **提示** 推荐在新项目中使用泛型数组 `TArray[T]` 和 `SortedTArray[T]`,它们提供了更好的类型安全性和开发体验。 ## 传统数组类型 下面介绍传统的数组类型,这些类型在旧版本中被广泛使用,仍然完全支持。 ### 普通数组 ```go package main import ( "fmt" "github.com/gogf/gf/v2/container/garray" ) func main () { // 创建并发安全的int类型数组 a := garray.NewIntArray(true) // 添加数据项 for i := 0; i < 10; i++ { a.Append(i) } // 获取当前数组长度 fmt.Println(a.Len()) // 获取当前数据项列表 fmt.Println(a.Slice()) // 获取指定索引项 fmt.Println(a.Get(6)) // 在指定索引后插入数据项 a.InsertAfter(9, 11) // 在指定索引前插入数据项 a.InsertBefore(10, 10) fmt.Println(a.Slice()) // 修改指定索引的数据项 a.Set(0, 100) fmt.Println(a.Slice()) // 搜索数据项,返回搜索到的索引位置 fmt.Println(a.Search(5)) // 删除指定索引的数据项 a.Remove(0) fmt.Println(a.Slice()) // 并发安全,写锁操作 a.LockFunc(func(array []int) { // 将末尾项改为100 array[len(array) - 1] = 100 }) // 并发安全,读锁操作 a.RLockFunc(func(array []int) { fmt.Println(array[len(array) - 1]) }) // 清空数组 fmt.Println(a.Slice()) a.Clear() fmt.Println(a.Slice()) } ``` 执行后,输出结果为: ```10 [0 1 2 3 4 5 6 7 8 9] 6 true [0 1 2 3 4 5 6 7 8 9 10 11] [100 1 2 3 4 5 6 7 8 9 10 11] 5 [1 2 3 4 5 6 7 8 9 10 11] 100 [1 2 3 4 5 6 7 8 9 10 100] [] ``` ### 排序数组 排序数组的方法与普通数组类似,但是带有自动排序功能及唯一性过滤功能。 ```go package main import ( "fmt" "github.com/gogf/gf/v2/container/garray" ) func main () { // 自定义排序数组,降序排序(SortedIntArray管理的数据是升序) a := garray.NewSortedArray(func(v1, v2 interface{}) int { if v1.(int) < v2.(int) { return 1 } if v1.(int) > v2.(int) { return -1 } return 0 }) // 添加数据 a.Add(2) a.Add(3) a.Add(1) fmt.Println(a.Slice()) // 添加重复数据 a.Add(3) fmt.Println(a.Slice()) // 检索数据,返回最后对比的索引位置,检索结果 // 检索结果:0: 匹配; <0:参数小于对比值; >0:参数大于对比值 fmt.Println(a.Search(1)) // 设置不可重复 a.SetUnique(true) fmt.Println(a.Slice()) a.Add(1) fmt.Println(a.Slice()) } ``` 执行后,输出结果: ``` [3 2 1] [3 3 2 1] 3 0 [3 2 1] [3 2 1] ``` ### `Iterate*` 数组遍历 ```go package main import ( "fmt" "github.com/gogf/gf/v2/container/garray" "github.com/gogf/gf/v2/frame/g" ) func main() { array := garray.NewStrArrayFrom(g.SliceStr{"a", "b", "c"}) // Iterator is alias of IteratorAsc, which iterates the array readonly in ascending order // with given callback function . // If returns true, then it continues iterating; or false to stop. array.Iterator(func(k int, v string) bool { fmt.Println(k, v) return true }) // IteratorDesc iterates the array readonly in descending order with given callback function . // If returns true, then it continues iterating; or false to stop. array.IteratorDesc(func(k int, v string) bool { fmt.Println(k, v) return true }) // Output: // 0 a // 1 b // 2 c // 2 c // 1 b // 0 a } ``` ### `Pop*` 数组项出栈 ```go package main import ( "fmt" "github.com/gogf/gf/v2/container/garray" ) func main() { array := garray.NewFrom([]interface{}{1, 2, 3, 4, 5, 6, 7, 8, 9}) // Any Pop* functions pick, delete and return the item from array. fmt.Println(array.PopLeft()) fmt.Println(array.PopLefts(2)) fmt.Println(array.PopRight()) fmt.Println(array.PopRights(2)) // Output: // 1 true // [2 3] // 9 true // [7 8] } ``` ### `Rand/PopRand` 数组项随机获取/出栈 ```go package main import ( "fmt" "github.com/gogf/gf/v2/container/garray" "github.com/gogf/gf/v2/frame/g" ) func main() { array := garray.NewFrom(g.Slice{1, 2, 3, 4, 5, 6, 7, 8, 9}) // Randomly retrieve and return 2 items from the array. // It does not delete the items from array. fmt.Println(array.Rands(2)) // Randomly pick and return one item from the array. // It deletes the picked up item from array. fmt.Println(array.PopRand()) } ``` ### `Contains/ContainsI` 包含判断 ```go package main import ( "fmt" "github.com/gogf/gf/v2/container/garray" ) func main() { var array garray.StrArray array.Append("a") fmt.Println(array.Contains("a")) fmt.Println(array.Contains("A")) fmt.Println(array.ContainsI("A")) // Output: // true // false // true } ``` ### `FilterEmpty/FilterNil` 空值过滤 ```go package main import ( "fmt" "github.com/gogf/gf/v2/container/garray" "github.com/gogf/gf/v2/frame/g" ) func main() { array1 := garray.NewFrom(g.Slice{0, 1, 2, nil, "", g.Slice{}, "john"}) array2 := garray.NewFrom(g.Slice{0, 1, 2, nil, "", g.Slice{}, "john"}) fmt.Printf("%#v\n", array1.FilterNil().Slice()) fmt.Printf("%#v\n", array2.FilterEmpty().Slice()) // Output: // []interface {}{0, 1, 2, "", []interface {}{}, "john"} // []interface {}{1, 2, "john"} } ``` ### `Reverse` 数组反转 ```go package main import ( "fmt" "github.com/gogf/gf/v2/container/garray" "github.com/gogf/gf/v2/frame/g" ) func main() { array := garray.NewFrom(g.Slice{1, 5, 3}) // Reverse makes array with elements reverse. fmt.Println(array.Reverse().Slice()) // Output: // [3 5 1] } ``` ### `Shuffle` 随机排序 ```go package main import ( "fmt" "github.com/gogf/gf/v2/container/garray" "github.com/gogf/gf/v2/frame/g" ) func main() { array := garray.NewFrom(g.Slice{1, 2, 3, 4, 5, 6, 7, 8, 9}) // Shuffle randomly shuffles the array. fmt.Println(array.Shuffle().Slice()) } ``` ### `Walk` 遍历修改 ```go package main import ( "fmt" "github.com/gogf/gf/v2/container/garray" "github.com/gogf/gf/v2/frame/g" ) func main() { var array garray.StrArray tables := g.SliceStr{"user", "user_detail"} prefix := "gf_" array.Append(tables...) // Add prefix for given table names. array.Walk(func(value string) string { return prefix + value }) fmt.Println(array.Slice()) // Output: // [gf_user gf_user_detail] } ``` ### `Join` 数组项串连 ```go package main import ( "fmt" "github.com/gogf/gf/v2/container/garray" "github.com/gogf/gf/v2/frame/g" ) func main() { array := garray.NewFrom(g.Slice{"a", "b", "c", "d"}) fmt.Println(array.Join(",")) // Output: // a,b,c,d } ``` ### `Chunk` 数组拆分 ```go package main import ( "fmt" "github.com/gogf/gf/v2/container/garray" "github.com/gogf/gf/v2/frame/g" ) func main() { array := garray.NewFrom(g.Slice{1, 2, 3, 4, 5, 6, 7, 8, 9}) // Chunk splits an array into multiple arrays, // the size of each array is determined by . // The last chunk may contain less than size elements. fmt.Println(array.Chunk(2)) // Output: // [[1 2] [3 4] [5 6] [7 8] [9]] } ``` ### `Merge` 数组合并 ```go package main import ( "fmt" "github.com/gogf/gf/v2/container/garray" "github.com/gogf/gf/v2/frame/g" ) func main() { array1 := garray.NewFrom(g.Slice{1, 2}) array2 := garray.NewFrom(g.Slice{3, 4}) slice1 := g.Slice{5, 6} slice2 := []int{7, 8} slice3 := []string{"9", "0"} fmt.Println(array1.Slice()) array1.Merge(array1) array1.Merge(array2) array1.Merge(slice1) array1.Merge(slice2) array1.Merge(slice3) fmt.Println(array1.Slice()) // Output: // [1 2] // [1 2 1 2 3 4 5 6 7 8 9 0] } ``` ### `JSON` 序列化/反序列 `garray` 模块下的所有容器类型均实现了标准库 `json` 数据格式的序列化/反序列化接口。 1. `Marshal` ```go package main import ( "encoding/json" "fmt" "github.com/gogf/gf/v2/container/garray" ) func main() { type Student struct { Id int Name string Scores *garray.IntArray } s := Student{ Id: 1, Name: "john", Scores: garray.NewIntArrayFrom([]int{100, 99, 98}), } b, _ := json.Marshal(s) fmt.Println(string(b)) } ``` 执行后,输出结果: ``` {"Id":1,"Name":"john","Scores":[100,99,98]} ``` 2. `Unmarshal` ```go package main import ( "encoding/json" "fmt" "github.com/gogf/gf/v2/container/garray" ) func main() { b := []byte(`{"Id":1,"Name":"john","Scores":[100,99,98]}`) type Student struct { Id int Name string Scores *garray.IntArray } s := Student{} json.Unmarshal(b, &s) fmt.Println(s) } ``` 执行后,输出结果: ``` {1 john [100,99,98]} ``` # 数组类型-方法介绍 **提示** 以下常用方法列表,文档更新可能滞后于代码新特性,更多的方法及示例请参考代码文档: [https://pkg.go.dev/github.com/gogf/gf/v2/container/garray](https://pkg.go.dev/github.com/gogf/gf/v2/container/garray) ## `Append` - 说明:向数组的尾部追加数据,可以添加任意数量字符串。`Append`的方法是`PushRight`的别名 - 格式: ```go Append(value ...string) *StrArray ``` - 示例:建立一个空数组,设置完数据后,并在数组尾部添加新的数据。 ```go func ExampleStrArray_Append() { s := garray.NewStrArray() s.SetArray(g.SliceStr{"We", "are", "GF", "fans"}) s.Append("a", "b", "c") fmt.Println(s) // Output: // ["We","are","GF","fans","a","b","c"] } ``` ## `At` - 说明:返回数组指定索引的数据 - 格式: ```go At(index int) (value string) ``` - 示例:建立一个数组,找到`index`为2的数据。 ```go func ExampleStrArray_At() { s := garray.NewStrArrayFrom(g.SliceStr{"We", "are", "GF", "fans", "!"}) sAt := s.At(2) fmt.Println(sAt) // Output: // GF } ``` ## `Chunk` - 说明:把指定数组按指定的大小`Size`,分割成多个数组,返回值为`[][]string`。最后一个数组包含数据的数量可能小于`Size` - 格式: ```go Chunk(size int) [][]string ``` - 示例:建立一个数组,并将该数组分割成3个数组。 ```go func ExampleStrArray_Chunk() { s := garray.NewStrArrayFrom(g.SliceStr{"a", "b", "c", "d", "e", "f", "g", "h"}) r := s.Chunk(3) fmt.Println(r) // Output: // [[a b c] [d e f] [g h]] } ``` ## `Clear` - 说明:删除当前数组中所有的数据 - 格式: ```go Clear() *StrArray ``` - 示例:建立一个空数组,赋值后,并删除该数组的数据。 ```go func ExampleStrArray_Clear() { s := garray.NewStrArray() s.SetArray(g.SliceStr{"a", "b", "c", "d", "e", "f", "g", "h"}) fmt.Println(s) fmt.Println(s.Clear()) fmt.Println(s) // Output: // ["a","b","c","d","e","f","g","h"] // [] // [] } ``` ## `Clone` - 说明:克隆当前的数组。返回一个与当前数组相同的数组拷贝 - 格式: ```go Clone() (newArray *StrArray) ``` - 示例:建立一个空数组,赋值后,克隆出一个新数组。 ```go func ExampleStrArray_Clone() { s := garray.NewStrArray() s.SetArray(g.SliceStr{"a", "b", "c", "d", "e", "f", "g", "h"}) r := s.Clone() fmt.Println(r) fmt.Println(s) // Output: // ["a","b","c","d","e","f","g","h"] // ["a","b","c","d","e","f","g","h"] } ``` ## `Contains` - 说明:判断一个数组是否包含给定的`String`值。字符串严格区分大小写。返回值为`bool` - 格式: ```go Contains(value string) bool ``` - 示例:建立一个空数组,设置完数据后,判断是否包含指定数据`e`和`z` ```go func ExampleStrArray_Contains() { s := garray.NewStrArray() s.SetArray(g.SliceStr{"a", "b", "c", "d", "e", "f", "g", "h"}) fmt.Println(s.Contains("e")) fmt.Println(s.Contains("z")) // Output: // true // false } ``` ## `ContainsI` - 说明:判断一个数组是否包含给定的 `String` 值。字符串不区分大小写。返回值为 `bool` - 格式: ```go ContainsI(value string) bool ``` - 示例:建立一个空数组,设置完数据后,判断是否包含指定数据 `E` 和 `z` ```go func ExampleStrArray_ContainsI() { s := garray.NewStrArray() s.SetArray(g.SliceStr{"a", "b", "c", "d", "e", "f", "g", "h"}) fmt.Println(s.ContainsI("E")) fmt.Println(s.ContainsI("z")) // Output: // true // false } ``` ## `CountValues` - 说明:统计每个值在数组中出现的次数。返回值为 `map[string]int` - 格式: ```go CountValues() map[string]int ``` - 示例:建立一个数组,统计数组中每个字符串包含的个数 ```go func ExampleStrArray_CountValues() { s := garray.NewStrArrayFrom(g.SliceStr{"a", "b", "c", "c", "c", "d", "d"}) fmt.Println(s.CountValues()) // Output: // map[a:1 b:1 c:3 d:2] } ``` ## `Fill` - 说明:在数组中指定的开始位置 `startIndex`,用指定的 `value` 进行填充。返回值为 `error` - 格式: ```go Fill(startIndex int, num int, value string) error ``` - 示例:建立一个数组,在数组开始位置 `index` 为2的地方,用字符串 `here` 填充3个数据 ```go func ExampleStrArray_Fill() { s := garray.NewStrArrayFrom(g.SliceStr{"a", "b", "c", "d", "e", "f", "g", "h"}) s.Fill(2, 3, "here") fmt.Println(s) // Output: // ["a","b","here","here","here","f","g","h"] } ``` ## `FilterEmpty` - 说明:过滤指定数组中的空字符串 - 格式: ```go FilterEmpty() *StrArray ``` - 示例:建立一个数组,在赋值后,过滤该数组中的空字符串 ```go func ExampleStrArray_FilterEmpty() { s := garray.NewStrArrayFrom(g.SliceStr{"a", "b", "", "c", "", "", "d"}) fmt.Println(s.FilterEmpty()) // Output: // ["a","b","c","d"] } ``` ## `Get` - 说明:返回数组中指定 `index` 的值,返回值有2个参数,返回值 `value`,和是否找到指定位置的数据 `found`,为 `true` 则找到,为 `false` 则未找到 - 格式: ```go Get(index int) (value string, found bool) ``` - 示例:建立一个数组,在赋值后,得到数组 `index` 为3的值 ```go func ExampleStrArray_Get() { s := garray.NewStrArrayFrom(g.SliceStr{"We", "are", "GF", "fans", "!"}) sGet, sBool := s.Get(3) fmt.Println(sGet, sBool) // Output: // fans true } ``` ## `InsertAfter` - 说明:在数组中指定 `index` 的位置之后插入值 `value`,返回值为 `error` - 格式: ```go InsertAfter(index int, value string) error ``` - 示例:建立一个数组,在 `index` 为1的值之后,插入字符串 `here` ```go func ExampleStrArray_InsertAfter() { s := garray.NewStrArray() s.SetArray(g.SliceStr{"a", "b", "c", "d"}) s.InsertAfter(1, "here") fmt.Println(s.Slice()) // Output: // [a b here c d] } ``` ## `InsertBefore` - 说明:在数组中指定 `index` 的位置之前插入值 `value`,返回值为 `error` - 格式: ```go InsertBefore(index int, value string) error ``` - 示例:建立一个数组并初始化,在 `index` 为1的值之前,插入字符串 `here` ```go func ExampleStrArray_InsertBefore() { s := garray.NewStrArray() s.SetArray(g.SliceStr{"a", "b", "c", "d"}) s.InsertBefore(1, "here") fmt.Println(s.Slice()) // Output: // [a here b c d] } ``` ## `Interfaces` - 说明:把当前数组作为 `[]interface{}` 进行返回 - 格式: ```go Interfaces() []interface{} ``` - 示例:建立一个数组并初始化,并打印出返回值 `[]interface{}` 的内容 ```go func ExampleStrArray_Interfaces() { s := garray.NewStrArray() s.SetArray(g.SliceStr{"a", "b", "c", "d", "e", "f", "g", "h"}) r := s.Interfaces() fmt.Println(r) // Output: // [a b c d e f g h] } ``` ## `IsEmpty` - 说明:判断当前数组是不是空数组,如果是空数组,则返回 `true`,如果不是空数组,则返回 `false` - 格式: ```go IsEmpty() bool ``` - 示例:建立2个数组并初始化,并判断是否为空数组 ```go func ExampleStrArray_IsEmpty() { s := garray.NewStrArrayFrom(g.SliceStr{"a", "b", "", "c", "", "", "d"}) fmt.Println(s.IsEmpty()) s1 := garray.NewStrArray() fmt.Println(s1.IsEmpty()) // Output: // false // true } ``` ## `Iterator` - 说明:数组遍历 - 格式: ```go Iterator(f func(k int, v string) bool) ``` - 示例:建立1个数组,并对其进行遍历 ```go func ExampleStrArray_Iterator() { s := garray.NewStrArrayFrom(g.SliceStr{"a", "b", "c"}) s.Iterator(func(k int, v string) bool { fmt.Println(k, v) return true }) // Output: // 0 a // 1 b // 2 c } ``` ## `IteratorAsc` - 说明:根据给定的回调函数 `f`,按升序对数组进行遍历,如果 `f` 返回 `true`,则继续进行遍历,否则停止遍历 - 格式: ```go IteratorAsc(f func(k int, v string) bool) ``` - 示例:建立1个数组,并按照自定义函数对其进行升序遍历 ```go func ExampleStrArray_Iterator() { s := garray.NewStrArrayFrom(g.SliceStr{"a", "b", "c"}) s.Iterator(func(k int, v string) bool { fmt.Println(k, v) return true }) // Output: // 0 a // 1 b // 2 c } ``` ## `IteratorDesc` - 说明:根据给定的回调函数 `f`,按降序对数组进行遍历,如果 `f` 返回 `true`,则继续进行遍历,否则停止遍历 - 格式: ```go IteratorAsc(f func(k int, v string) bool) ``` - 示例:建立1个数组,并按照自定义函数对其进行降序遍历 ```go func ExampleStrArray_IteratorDesc() { s := garray.NewStrArrayFrom(g.SliceStr{"a", "b", "c"}) s.IteratorDesc(func(k int, v string) bool { fmt.Println(k, v) return true }) // Output: // 2 c // 1 b // 0 a } ``` ## `Join` - 说明:将数组元素根据给定的字符串连接符 `gule`,连接起来 - 格式: ```go Join(glue string) string ``` - 示例:给定连接符 `','`,将数组中的字符串连接起来 ```go func ExampleStrArray_Join() { s := garray.NewStrArrayFrom(g.SliceStr{"a", "b", "c"}) fmt.Println(s.Join(",")) // Output: // a,b,c } ``` ## `Len` - 说明:得到数组的长度 - 格式: ```go Len() int ``` - 示例:建立一个新数组,初始化后得到该数组的长度 ```go func ExampleStrArray_Len() { s := garray.NewStrArray() s.SetArray(g.SliceStr{"a", "b", "c", "d", "e", "f", "g", "h"}) fmt.Println(s.Len()) // Output: // 8 } ``` ## `LockFunc` - 说明:通过回调函数 `f` 对数组进行写锁定 - 格式: ```go LockFunc(f func(array []string)) *StrArray ``` - 示例:建立一个新数组,并对该数组在写锁定的状态下,修改最后一个数据 ```go func ExampleStrArray_LockFunc() { s := garray.NewStrArrayFrom(g.SliceStr{"a", "b", "c"}) s.LockFunc(func(array []string) { array[len(array)-1] = "GF fans" }) fmt.Println(s) // Output: // ["a","b","GF fans"] } ``` ## `MarshalJSON` - 说明:实现 `json.Marshal` 的 `JSON` 格式的序列化接口 - 格式: ```go MarshalJSON() ([]byte, error) ``` - 示例:建立一个新 `JSON` 格式的数据,并对该数据进行序列化的操作后,打印出相应结果 ```go func ExampleStrArray_MarshalJSON() { type Student struct { Id int Name string Lessons []string } s := Student{ Id: 1, Name: "john", Lessons: []string{"Math", "English", "Music"}, } b, _ := json.Marshal(s) fmt.Println(string(b)) // Output: // {"Id":1,"Name":"john","Lessons":["Math","English","Music"]} } ``` ## `Merge` - 说明:合并数组,将指定数组中的内容合并到当前数组中。参数 `array` 可以是任意 `garray` 或 `slice` 类型。 `Merge` 和 `Append` 的主要区别是 `Append` 仅仅支持 `slice` 类型, `Merge` 则支持更多的参数类型 - 格式: ```go Merge(array interface{}) *StrArray ``` - 示例:建立2个新数组 `s1` 和 `s2`,并将 `s2` 的数据合并到 `s1` 上 ```go func ExampleStrArray_Merge() { s1 := garray.NewStrArray() s2 := garray.NewStrArray() s1.SetArray(g.SliceStr{"a", "b", "c"}) s2.SetArray(g.SliceStr{"d", "e", "f"}) s1.Merge(s2) fmt.Println(s1) // Output: // ["a","b","c","d","e","f"] } ``` ## `NewStrArray` - 说明:创建一个新数组。 `safe` 为非必需参数,布尔型,是并发安全的开关,缺省值为 `False` - 格式: ```go NewStrArray(safe ...bool) *StrArray ``` - 示例:建立一个空数组,并添加数据。此时没有指定 `Safe` 参数,默认为非并发安全设置 ```go func ExampleNewStrArray() { s := garray.NewStrArray() s.Append("We") s.Append("are") s.Append("GF") s.Append("Fans") fmt.Println(s.Slice()) // Output: // [We are GF Fans] } ``` ## `NewStrArrayFrom` - 说明:根据给定的数组内容,创建一个新数组。 `safe` 为非必需参数,布尔型,是并发安全的开关,缺省值为 `False` - 格式: ```go NewStrArrayFrom(array []string, safe ...bool) *StrArray ``` - 示例:建立一个空数组,并根据指定内容添加数据。此时没有指定 `Safe` 参数,默认为非并发安全设置 ```go func ExampleNewStrArrayFrom() { s := garray.NewStrArrayFrom(g.SliceStr{"We", "are", "GF", "fans", "!"}) fmt.Println(s.Slice(), s.Len(), cap(s.Slice())) // Output: // [We are GF fans !] 5 5 } ``` ## `NewStrArrayFromCopy` - 说明:根据给定的数组内容的拷贝,创建一个新数组。 `safe` 为非必需参数,布尔型,是并发安全的开关,缺省值为 `False` - 格式: ```go NewStrArrayFrom(array []string, safe ...bool) *StrArray ``` - 示例:建立一个空数组,并根据指定内容添加数据。此时没有指定 `Safe` 参数,默认为非并发安全设置 ```go func ExampleNewStrArrayFromCopy() { s := garray.NewStrArrayFromCopy(g.SliceStr{"We", "are", "GF", "fans", "!"}) fmt.Println(s.Slice(), s.Len(), cap(s.Slice())) // Output: // [We are GF fans !] 5 5 } ``` ## `NewStrArraySize` - 说明:根据给定的 `size` 和 `cap`,创建一个新数组。 `safe` 为非必需参数,布尔型,是并发安全的开关,缺省值为 `False` - 格式: ```go NewStrArraySize(size int, cap int, safe ...bool) *StrArray ``` - 示例:建立一个空数组, `Size为3`, `Cap为5`,并添加数据。打印出相应的内容。此时没有指定 `Safe` 参数,默认为非并发安全设置 ```go func ExampleNewStrArraySize() { s := garray.NewStrArraySize(3, 5) s.Set(0, "We") s.Set(1, "are") s.Set(2, "GF") s.Set(3, "fans") fmt.Println(s.Slice(), s.Len(), cap(s.Slice())) // Output: // [We are GF] 3 5 } ``` ## `Pad` - 说明:填充指定大小为 `size` 的值 `value` 到数组中。如果大小 `size` 是正数,则从数组的右边开始填充。如果 `size` 是负数,则从数组的左边开始填充。如果 `size` 的大小正好等于数组的长度,那么将不会填充任何数据。 - 格式: ```go Pad(size int, value string) *StrArray ``` - 示例:建立1个新数组,先从左边将数组,用指定的字符串 `here` 填充到 `size` 为7,然后用指定的字符串 `there` 将数组用字符串填充到 `size` 为10 ```go func ExampleStrArray_Pad() { s := garray.NewStrArrayFrom(g.SliceStr{"a", "b", "c"}) s.Pad(7, "here") fmt.Println(s) s.Pad(-10, "there") fmt.Println(s) // Output: // ["a","b","c","here","here","here","here"] // ["there","there","there","a","b","c","here","here","here","here"] } ``` ## `PopLeft` - 说明:从数组的左侧将一个字符串数据出栈,返回值 `value` 为出栈的字符串数据。更新后的数组数据为剩余数据。当数组为空时, `found` 为 `false。` - 格式: ```go PopLeft() (value string, found bool) ``` - 示例:建立1个新数组,将最左边的数据出栈,并打印出剩余的数据 ```go func ExampleStrArray_PopLeft() { s := garray.NewStrArray() s.SetArray(g.SliceStr{"a", "b", "c", "d"}) s.PopLeft() fmt.Println(s.Slice()) // Output: // [b c d] } ``` ## `PopLefts` - 说明:从数组的左侧将多个字符串数据出栈,返回值为出栈的字符串数据,出栈数据的个数为 `size`。如果 `size` 比数组的 `size` 大,那么方法将返回数组中所有的数据。如果 `size<=0或者为空`,那么将返回 `nil` - 格式: ```go PopLefts(size int) []string ``` - 示例:建立1个新数组,将最左边的2个数据做出栈操作,并打印出出栈的数据和原数组的剩余数据 ```go func ExampleStrArray_PopLefts() { s := garray.NewStrArray() s.SetArray(g.SliceStr{"a", "b", "c", "d", "e", "f", "g", "h"}) r := s.PopLefts(2) fmt.Println(r) fmt.Println(s) // Output: // [a b] // ["c","d","e","f","g","h"] } ``` ## `PopRand` - 说明:从数组中随机出栈1个数据,返回值为出栈的字符串数据。如果数组 `为空`,那么 `found` 将返回 `false` - 格式: ```go PopRand() (value string, found bool) ``` - 示例:建立1个新数组,从数组中随机出栈1个数据,并打印出出栈的数据 ```go func ExampleStrArray_PopRand() { s := garray.NewStrArray() s.SetArray(g.SliceStr{"a", "b", "c", "d", "e", "f", "g", "h"}) r, _ := s.PopRand() fmt.Println(r) // May Output: // e } ``` ## `PopRands` - 说明:从数组中随机出栈 `size` 个数据,返回值为出栈的字符串数据。如果 `size<=0或者为空`,那么将返回 `nil` - 格式: ```go PopRands(size int) []string ``` - 示例:建立1个新数组,从数组中随机出栈2个数据,并打印出出栈的数据 ```go func ExampleStrArray_PopRands() { s := garray.NewStrArray() s.SetArray(g.SliceStr{"a", "b", "c", "d", "e", "f", "g", "h"}) r := s.PopRands(2) fmt.Println(r) // May Output: // [e c] } ``` ## `PopRight` - 说明:从数组的右侧将一个字符串数据出栈,返回值 `value` 为出栈的字符串数据。更新后的数组数据为剩余数据。当数组为空时, `found` 为 `false。` - 格式: ```go PopRight() (value string, found bool) ``` - 示例:建立1个新数组,将最右边的数据出栈,并打印出剩余的数据 ```go func ExampleStrArray_PopRight() { s := garray.NewStrArray() s.SetArray(g.SliceStr{"a", "b", "c", "d"}) s.PopRight() fmt.Println(s.Slice()) // Output: // [a b c] } ``` ## `PopRights` - 说明:从数组的右侧将多个字符串数据出栈,返回值为出栈的字符串数据,出栈数据的个数为 `size`。如果 `size` 比数组的 `size` 大,那么方法将返回数组中所有的数据。如果 `size<=0或者为空`,那么将返回 `nil` - 格式: ```go PopRights(size int) []string ``` - 示例:建立1个新数组,将最右边的2个数据做出栈操作,并打印出出栈的数据和原数组的剩余数据 ```go func ExampleStrArray_PopRights() { s := garray.NewStrArray() s.SetArray(g.SliceStr{"a", "b", "c", "d", "e", "f", "g", "h"}) r := s.PopRights(2) fmt.Println(r) fmt.Println(s) // Output: // [g h] // ["a","b","c","d","e","f"] } ``` ## `PushLeft` - 说明:从数组的左侧入栈一个或多个字符串 - 格式: ```go PushLeft(value ...string) *StrArray ``` - 示例:建立1个新数组,从数组的左侧入栈多个字符串数据,并打印出更新后的数据 ```go func ExampleStrArray_PushLeft() { s := garray.NewStrArray() s.SetArray(g.SliceStr{"a", "b", "c", "d"}) s.PushLeft("We", "are", "GF", "fans") fmt.Println(s.Slice()) // Output: // [We are GF fans a b c d] } ``` ## `PushRight` - 说明:从数组的右侧入栈一个或多个字符串 - 格式: ```go PushRight(value ...string) *StrArray ``` - 示例:建立1个新数组,从数组的右侧入栈多个字符串数据,并打印出更新后的数据 ```go func ExampleStrArray_PushRight() { s := garray.NewStrArray() s.SetArray(g.SliceStr{"a", "b", "c", "d"}) s.PushRight("We", "are", "GF", "fans") fmt.Println(s.Slice()) // Output: // [a b c d We are GF fans] } ``` ## `Rand` - 说明:从数组中随机取出1个字符串(非删除式) - 格式: ```go Rand() (value string, found bool) ``` - 示例:建立1个新数组,从数组中随机取出一个字符串 ```go func ExampleStrArray_Rand() { s := garray.NewStrArrayFrom(g.SliceStr{"a", "b", "c", "d", "e", "f", "g", "h"}) fmt.Println(s.Rand()) // May Output: // c true } ``` ## `Rands` - 说明:从数组中随机取出 `size` 个字符串(非删除式) - 格式: ```go Rands(size int) []string ``` - 示例:建立1个新数组,从数组中随机取出3个字符串 ```go func ExampleStrArray_Rands() { s := garray.NewStrArrayFrom(g.SliceStr{"a", "b", "c", "d", "e", "f", "g", "h"}) fmt.Println(s.Rands(3)) // May Output: // [e h e] } ``` ## `Range` - 说明:获取数组中指定范围的数据。如果是在并发安全的模式下使用,则该方法返回一个 `slice` 拷贝。 - 格式: ```go Range(start int, end ...int) []string ``` - 示例:建立1个新数组,获取数组从 `index` 为2至5位置的数据 ```go func ExampleStrArray_Range() { s := garray.NewStrArray() s.SetArray(g.SliceStr{"a", "b", "c", "d", "e", "f", "g", "h"}) r := s.Range(2, 5) fmt.Println(r) // Output: // [c d e] } ``` ## `Remove` - 说明:从数组中移除位置在 `index` 处的数据。如果 `index` 超过数组的边界,则 `found` 返回 `false` - 格式: ```go Remove(index int) (value string, found bool) ``` - 示例:建立1个新数组,移除数组 `index` 为1的数据 ```go func ExampleStrArray_Remove() { s := garray.NewStrArray() s.SetArray(g.SliceStr{"a", "b", "c", "d"}) s.Remove(1) fmt.Println(s.Slice()) // Output: // [a c d] } ``` ## `RemoveValue` - 说明:从数组中移除指定的数据 `value`。如果 `value` 在数组中被找到,则 `found` 返回 `true`,否则 `found` 返回 `false` - 格式: ```go RemoveValue(value string) bool ``` - 示例:建立1个新数组,移除数组中值为 `b` 的数据 ```go func ExampleStrArray_RemoveValue() { s := garray.NewStrArray() s.SetArray(g.SliceStr{"a", "b", "c", "d"}) s.RemoveValue("b") fmt.Println(s.Slice()) // Output: // [a c d] } ``` ## `Replace` - 说明:将原字符串数组用指定的字符串数组进行替换,替换从原数组的头部位置开始。 - 格式: ```go Replace(array []string) *StrArray ``` - 示例:建立1个新数组,并用指定的字符串数组进行替换 ```go func ExampleStrArray_Replace() { s := garray.NewStrArray() s.SetArray(g.SliceStr{"We", "are", "GF", "fans", "!"}) fmt.Println(s.Slice()) s.Replace(g.SliceStr{"Happy", "coding"}) fmt.Println(s.Slice()) // Output: // [We are GF fans !] // [Happy coding GF fans !] } ``` ## `Reverse` - 说明:将整个数组反转,例如:["qaz","wsx","edc","rfv"] => ["rfv","edc","wsx","qaz"]。 - 格式: ```go Replace(array []string) *StrArray ``` - 示例:建立1个新数组,初始化后执行反转操作并打印 ```go func ExampleStrArray_Reverse() { s := garray.NewStrArrayFrom(g.SliceStr{"a", "m", "c"}) fmt.Println(s.Reverse()) // Output: // ["c","m","a"] } ``` ## `RLockFunc` - 说明:通过自定义回调函数 `f` 进行数组的读锁定 - 格式: ```go RLockFunc(f func(array []string)) *StrArray ``` - 示例:建立1个新数组,在回调函数 `f` 中,对数组进行遍历,并打印出数组元素 ```go func ExampleStrArray_RLockFunc() { s := garray.NewStrArrayFrom(g.SliceStr{"a", "b", "c", "d", "e"}) s.RLockFunc(func(array []string) { for i := 0; i < len(array); i++ { fmt.Println(array[i]) } }) // Output: // a // b // c // d // e } ``` ## `Search` - 说明:在数组中搜索指定的字符串,返回值为该值在数组中的 `index`,如果没有查询到,则返回 `-1` - 格式: ```go Search(value string) int ``` - 示例:建立1个新数组,并在该数组中搜索字符串 `e` 和z ```go func ExampleStrArray_Search() { s := garray.NewStrArray() s.SetArray(g.SliceStr{"a", "b", "c", "d", "e", "f", "g", "h"}) fmt.Println(s.Search("e")) fmt.Println(s.Search("z")) // Output: // 4 // -1 } ``` ## `Set` - 说明:给数组中指定位置的 `index` 设置值 `value`,如果 `index<0` 或者 `index` 超出了数组的边界,则返回错误 `error` - 格式: ```go Set(index int, value string) error ``` - 示例:建立1个新数组,长度为3,给数组设置值,值最终只按顺序设置到了 `index` 为2的位置,因为数组长度限制,最后一个值未设置成功 ```go func ExampleStrArray_Set() { s := garray.NewStrArraySize(3, 5) s.Set(0, "We") s.Set(1, "are") s.Set(2, "GF") s.Set(3, "fans") fmt.Println(s.Slice()) // Output: // [We are GF] } ``` ## `SetArray` - 说明:根据给定的 `slice` 数组内容给数组赋值 - 格式: ```go SetArray(array []string) *StrArray ``` - 示例:建立1个新数组,在给其赋值后,打印出来 ```go func ExampleStrArray_SetArray() { s := garray.NewStrArray() s.SetArray(g.SliceStr{"We", "are", "GF", "fans", "!"}) fmt.Println(s.Slice()) // Output: // [We are GF fans !] } ``` ## `Shuffle` - 说明:将数组中的内容进行乱序排列 - 格式: ```go Shuffle() *StrArray ``` - 示例:建立1个新数组,在给其赋值后进行乱序排列,并打印出来 ```go func ExampleStrArray_Shuffle() { s := garray.NewStrArrayFrom(g.SliceStr{"a", "b", "c", "d", "e", "f", "g", "h"}) fmt.Println(s.Shuffle()) // May Output: // ["a","c","e","d","b","g","f","h"] } ``` ## `Slice` - 说明:得到数组的 `slice` 切片数据,注意,如果是在并发安全模式下,返回的是一份拷贝数据,否则返回的是指向数据的指针 - 格式: ```go Shuffle() *StrArray ``` - 示例:建立1个新数组,在给其赋值后,并打印该数组的切片数据 ```go func ExampleStrArray_Slice() { s := garray.NewStrArray() s.SetArray(g.SliceStr{"a", "b", "c", "d", "e", "f", "g", "h"}) fmt.Println(s.Slice()) // Output: // [a b c d e f g h] } ``` ## `Sort` - 说明:对数组内容进行升序排序。 `reverse` 控制排序的方向,默认为 `true` 升序, `false` 为降序 - 格式: ```go Sort(reverse ...bool) *StrArray ``` - 示例:建立1个新数组,在给其赋值后,并按升序进行排序 ```go func ExampleStrArray_Sort() { s := garray.NewStrArray() s.SetArray(g.SliceStr{"b", "d", "a", "c"}) a := s.Sort() fmt.Println(a) // Output: // ["a","b","c","d"] } ``` ## `SortFunc` - 说明:通过自定义函数 `less` 对数组内容进行排序。 - 格式: ```go SortFunc(less func(v1, v2 string) bool) *StrArray ``` - 示例:建立1个新数组,在给其赋值后,首先用自定义函数对其进行降序排序,然后用自定义函数对其进行升序排序,并打印出相应的结果 ```go func ExampleStrArray_SortFunc() { s := garray.NewStrArrayFrom(g.SliceStr{"b", "c", "a"}) fmt.Println(s) s.SortFunc(func(v1, v2 string) bool { return gstr.Compare(v1, v2) > 0 }) fmt.Println(s) s.SortFunc(func(v1, v2 string) bool { return gstr.Compare(v1, v2) < 0 }) fmt.Println(s) // Output: // ["b","c","a"] // ["c","b","a"] // ["a","b","c"] } ``` ## `String` - 说明:将当前的数组转换成 `string`, - 格式: ```go String() string ``` - 示例:建立1个新数组,在给其赋值后,将数组转换成 `string`,并打印出相应的结果 ```go func ExampleStrArray_String() { s := garray.NewStrArrayFrom(g.SliceStr{"a", "b", "c"}) fmt.Println(s.String()) // Output: // ["a","b","c"] } ``` ## `Subslice` - 说明:根据给定的偏离值 `offset` 和长度 `length` 参数获得数组的切片,注意,如果是在并发安全模式下,返回拷贝数据,否则返回指向数据的指针。如果偏离值 `offset` 是非负数,则从数组的开始位置进行切片,否则从数组的尾部开始切片。 - 格式: ```go SubSlice(offset int, length ...int) ``` - 示例:建立1个新数组,在给其赋值后,将数组转换成 `string`,并打印出相应的结果 ```go func ExampleStrArray_SubSlice() { s := garray.NewStrArray() s.SetArray(g.SliceStr{"a", "b", "c", "d", "e", "f", "g", "h"}) r := s.SubSlice(3, 4) fmt.Println(r) // Output: // [d e f g] } ``` ## `Sum` - 说明:对数组中的整数值进行求和 - 格式: ```go Sum() (sum int) ``` - 示例:建立1个新数组,在给其赋值后,对数组中的整数值进行求和 ```go func ExampleStrArray_Sum() { s := garray.NewStrArray() s.SetArray(g.SliceStr{"3", "5", "10"}) a := s.Sum() fmt.Println(a) // Output: // 18 } ``` ## `Unique` - 说明:对数组中的数据进行去重处理 - 格式: ```go Unique() *StrArray ``` - 示例:建立1个新数组,在给其赋值后,对数组中的整数值进行求和 ```go func ExampleStrArray_Unique() { s := garray.NewStrArray() s.SetArray(g.SliceStr{"a", "b", "c", "c", "c", "d", "d"}) fmt.Println(s.Unique()) // Output: // ["a","b","c","d"] } ``` ## `UnmarshalJSON` - 说明:实现 `json.Unmarshal` 的 `UnmarshalJSON` 接口 - 格式: ```go UnmarshalJSON(b []byte) error ``` - 示例:建立1个 `byte` 切片,将其赋值给结构体后,进行反序列化操作,打印出相应的内容 ```go func ExampleStrArray_UnmarshalJSON() { b := []byte(`{"Id":1,"Name":"john","Lessons":["Math","English","Sport"]}`) type Student struct { Id int Name string Lessons *garray.StrArray } s := Student{} json.Unmarshal(b, &s) fmt.Println(s) // Output: // {1 john ["Math","English","Sport"]} } ``` ## `UnmarshalValue` - 说明:对任意类型的值实现反序列化接口 - 格式: ```go UnmarshalValue(value interface{}) error ``` - 示例:建立1个结构体,并对其值 进行反序列化操作,打印出相应的内容 ```go func ExampleStrArray_UnmarshalValue() { type Student struct { Name string Lessons *garray.StrArray } var s *Student gconv.Struct(g.Map{ "name": "john", "lessons": []byte(`["Math","English","Sport"]`), }, &s) fmt.Println(s) var s1 *Student gconv.Struct(g.Map{ "name": "john", "lessons": g.SliceStr{"Math", "English", "Sport"}, }, &s1) fmt.Println(s1) // Output: // &{john ["Math","English","Sport"]} // &{john ["Math","English","Sport"]} } ``` ## `Walk` - 说明:通过自定义函数 `f`,对数组内容进行遍历修改 - 格式: ```go Walk(f func(value string) string) *StrArray ``` - 示例:建立1个数组,对数组内容进行遍历修改,为每个字符串添加前缀,并打印出相应的内容 ```go func ExampleStrArray_Walk() { var array garray.StrArray tables := g.SliceStr{"user", "user_detail"} prefix := "gf_" array.Append(tables...) // Add prefix for given table names. array.Walk(func(value string) string { return prefix + value }) fmt.Println(array.Slice()) // Output: // [gf_user gf_user_detail] } ``` # 集合类型-gset ## 基本介绍 集合,即不可重复的一组元素,元素项可以为任意类型。 同时, `gset` 支持可选的并发安全参数选项,支持并发安全的场景。 **使用场景**: 集合操作。 **使用方式**: ```go import "github.com/gogf/gf/v2/container/gset" ``` **接口文档**: [https://pkg.go.dev/github.com/gogf/gf/v2/container/gset](https://pkg.go.dev/github.com/gogf/gf/v2/container/gset) **泛型支持**: 从 `v2.10` 版本开始,`gset` 提供了泛型集合类型: - `TSet[T]`:泛型集合,提供类型安全的集合操作 - 支持 `NewTSetWithChecker` 系列函数,可自定义 `nil` 值检查器 - 推荐在新项目中使用泛型集合,享受编译时类型检查带来的安全性 # 集合类型-基本使用 ## 泛型集合(推荐使用) 从 `v2.10` 版本开始,`gset` 提供了泛型集合 `TSet[T]`,提供类型安全的集合操作。 ### 基本使用 ```go package main import ( "fmt" "github.com/gogf/gf/v2/container/gset" ) func main() { // 创建一个string类型的泛型集合 s := gset.NewTSet[string]() // 添加元素 s.Add("apple") s.Add("banana") s.Add("orange") s.Add("apple") // 重复元素不会被添加 // 获取集合大小 fmt.Println("Size:", s.Size()) // 输出: Size: 3 // 检查元素是否存在 fmt.Println("Contains apple:", s.Contains("apple")) // 输出: Contains apple: true // 获取所有元素 fmt.Println("Elements:", s.Slice()) // 遍历集合 s.Iterator(func(v string) bool { fmt.Println("Element:", v) return true }) } ``` ### 集合运算 泛型集合支持交集、并集、差集、补集等运算。 ```go package main import ( "fmt" "github.com/gogf/gf/v2/container/gset" ) func main() { s1 := gset.NewTSetFrom[int]([]int{1, 2, 3, 4, 5}) s2 := gset.NewTSetFrom[int]([]int{4, 5, 6, 7, 8}) // 交集 fmt.Println("Intersect:", s1.Intersect(s2).Slice()) // [4 5] // 并集 fmt.Println("Union:", s1.Union(s2).Slice()) // [1 2 3 4 5 6 7 8] // 差集 fmt.Println("Diff:", s1.Diff(s2).Slice()) // [1 2 3] // 补集 full := gset.NewTSetFrom[int]([]int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}) fmt.Println("Complement:", s1.Complement(full).Slice()) // [6 7 8 9 10] } ``` ### 自定义 `nil` 值检查器 对于指针类型或接口类型,可以使用自定义 `nil` 检查器。 ```go package main import ( "fmt" "github.com/gogf/gf/v2/container/gset" ) type User struct { ID int Name string } func main() { // 创建带自定义nil检查器的集合 s := gset.NewTSetWithChecker[*User](func(u *User) bool { return u == nil || u.ID == 0 }) s.Add(&User{ID: 1, Name: "John"}) s.Add(&User{ID: 2, Name: "Jane"}) s.Add(&User{ID: 0, Name: "Invalid"}) // 会被视为nil,不会添加 fmt.Println("Size:", s.Size()) // 输出: Size: 2 } ``` ### 泛型集合特点 1. **类型安全**:在编译期就能发现类型错误,避免运行时类型断言失败。 2. **自定义 `nil` 检查(可选)**:通过 `NewTSetWithChecker` 函数,可以为泛型类型自定义 `nil` 判断逻辑,解决 typed nil 问题。默认情况下使用反射判断。 3. **性能优势**:减少了类型断言和反射的开销。 4. **更好的IDE支持**:IDE能够提供更准确的代码补全和类型提示。 **提示** 推荐在新项目中使用泛型集合 `TSet[T]`,它提供了更好的类型安全性和开发体验。 ## 传统集合类型 下面介绍传统的集合类型,这些类型在旧版本中被广泛使用,仍然完全支持。 ### 基本使用 ```go package main import ( "github.com/gogf/gf/v2/container/gset" "fmt" ) func main() { // 创建一个并发安全的集合对象 s := gset.New(true) // 添加数据项 s.Add(1) // 批量添加数据项 s.Add([]interface{}{1, 2, 3}...) // 集合数据项大小 fmt.Println(s.Size()) // 集合中是否存在指定数据项 fmt.Println(s.Contains(2)) // 返回数据项slice fmt.Println(s.Slice()) // 删除数据项 s.Remove(3) // 遍历数据项 s.Iterator(func(v interface{}) bool { fmt.Println("Iterator:", v) return true }) // 将集合转换为字符串 fmt.Println(s.String()) // 并发安全写锁操作 s.LockFunc(func(m map[interface{}]struct{}) { m[4] = struct{}{} }) // 并发安全读锁操作 s.RLockFunc(func(m map[interface{}]struct{}) { fmt.Println(m) }) // 清空集合 s.Clear() fmt.Println(s.Size()) } ``` 执行后,输出结果为: ```3 true [1 2 3] Iterator: 1 Iterator: 2 [1 2] map[1:{} 2:{} 4:{}] 0 ``` ### 交差并补集 我们可以使用以下方法实现交差并补集,并返回一个新的结果集合, ```go func (set *Set) Intersect(others ...*Set) (newSet *Set) func (set *Set) Diff(others ...*Set) (newSet *Set) func (set *Set) Union(others ...*Set) (newSet *Set) func (set *Set) Complement(full *Set) (newSet *Set) ``` 1. `Intersect`: 交集,属于set且属于others的元素为元素的集合。 2. `Diff`: 差集,属于set且不属于others的元素为元素的集合。 3. `Union`: 并集,属于set或属于others的元素为元素的集合。 4. `Complement`: 补集,(前提: set应当为full的子集)属于全集full不属于集合set的元素组成的集合。如果给定的full集合不是set的全集时,返回full与set的差集. 通过集合方法我们可以发现,交差并集方法支持多个集合参数进行计算。以下为简化示例,只使用一个参数集合。 ```go package main import ( "fmt" "github.com/gogf/gf/v2/frame/g" "github.com/gogf/gf/v2/container/gset" ) func main() { s1 := gset.NewFrom(g.Slice{1, 2, 3}) s2 := gset.NewFrom(g.Slice{4, 5, 6}) s3 := gset.NewFrom(g.Slice{1, 2, 3, 4, 5, 6, 7}) // 交集 fmt.Println(s3.Intersect(s1).Slice()) // 差集 fmt.Println(s3.Diff(s1).Slice()) // 并集 fmt.Println(s1.Union(s2).Slice()) // 补集 fmt.Println(s1.Complement(s3).Slice()) } ``` 执行后,输出结果为: ``` [1 2 3] [4 5 6 7] [1 2 3 4 5 6] [7 4 5 6] ``` ### `Contains/ContainsI` 包含判断 ```go package main import ( "fmt" "github.com/gogf/gf/v2/container/gset" ) func main() { var set gset.StrSet set.Add("a") fmt.Println(set.Contains("a")) fmt.Println(set.Contains("A")) fmt.Println(set.ContainsI("A")) // Output: // true // false // true } ``` ### `Pop/Pops` 集合项出栈 ```go package main import ( "fmt" "github.com/gogf/gf/v2/container/gset" ) func main() { var set gset.Set set.Add(1, 2, 3, 4) fmt.Println(set.Pop()) fmt.Println(set.Pops(2)) fmt.Println(set.Size()) // May Output: // 1 // [2 3] // 1 } ``` ### `Join` 集合项串连 ```go package main import ( "fmt" "github.com/gogf/gf/v2/container/gset" ) func main() { var set gset.Set set.Add("a", "b", "c", "d") fmt.Println(set.Join(",")) // May Output: // a,b,c,d } ``` ### `IsSubsetOf` 子集判断 ```go package main import ( "fmt" "github.com/gogf/gf/v2/container/gset" "github.com/gogf/gf/v2/frame/g" ) func main() { var s1, s2 gset.Set s1.Add(g.Slice{1, 2, 3}...) s2.Add(g.Slice{2, 3}...) fmt.Println(s1.IsSubsetOf(&s2)) fmt.Println(s2.IsSubsetOf(&s1)) // Output: // false // true } ``` ### `AddIfNotExist*` 判断性写入 判断性写入是指当指定的数据项不存在时则写入并且方法返回 `true`,否则忽略吸入并且方法返回 `false`。相关方法如下: - `AddIfNotExist` - `AddIfNotExistFunc` - `AddIfNotExistFuncLock` 方法具体描述请查看接口文档或源码注释。 ```go package main import ( "fmt" "github.com/gogf/gf/v2/container/gset" ) func main() { var set gset.Set fmt.Println(set.AddIfNotExist(1)) fmt.Println(set.AddIfNotExist(1)) fmt.Println(set.Slice()) // Output: // true // false // [1] } ``` ### `Walk` 遍历修改 ```go package main import ( "fmt" "github.com/gogf/gf/v2/container/gset" "github.com/gogf/gf/v2/frame/g" ) func main() { var ( set gset.StrSet names = g.SliceStr{"user", "user_detail"} prefix = "gf_" ) set.Add(names...) // Add prefix for given table names. set.Walk(func(item string) string { return prefix + item }) fmt.Println(set.Slice()) // May Output: // [gf_user gf_user_detail] } ``` ### `JSON` 序列化/反序列 `gset` 模块下的所有容器类型均实现了标准库 `json` 数据格式的序列化/反序列化接口。 1. `Marshal` ```go package main import ( "encoding/json" "fmt" "github.com/gogf/gf/v2/container/gset" ) func main() { type Student struct { Id int Name string Scores *gset.IntSet } s := Student{ Id: 1, Name: "john", Scores: gset.NewIntSetFrom([]int{100, 99, 98}), } b, _ := json.Marshal(s) fmt.Println(string(b)) } ``` 执行后,终端输出: ``` {"Id":1,"Name":"john","Scores":[100,99,98]} ``` 2. `Unmarshal` ```go package main import ( "encoding/json" "fmt" "github.com/gogf/gf/v2/container/gset" ) func main() { b := []byte(`{"Id":1,"Name":"john","Scores":[100,99,98]}`) type Student struct { Id int Name string Scores *gset.IntSet } s := Student{} json.Unmarshal(b, &s) fmt.Println(s) } ``` 执行后,输出结果: ```js {1 john [100,99,98]} ``` # 集合类型-性能测试 [https://github.com/gogf/gf/blob/master/container/gset/gset\_z\_bench\_test.go](https://github.com/gogf/gf/blob/master/container/gset/gset_z_bench_test.go) ``` goos: linux goarch: amd64 Benchmark_IntSet_Add-4 10000000 277 ns/op 8 B/op 0 allocs/op Benchmark_IntSet_Contains-4 20000000 60.6 ns/op 0 B/op 0 allocs/op Benchmark_IntSet_Remove-4 10000000 211 ns/op 0 B/op 0 allocs/op Benchmark_AnySet_Add-4 5000000 312 ns/op 21 B/op 1 allocs/op Benchmark_AnySet_Contains-4 30000000 68.2 ns/op 0 B/op 0 allocs/op Benchmark_AnySet_Remove-4 5000000 267 ns/op 0 B/op 0 allocs/op Benchmark_StrSet_Add-4 5000000 383 ns/op 20 B/op 1 allocs/op Benchmark_StrSet_Contains-4 10000000 160 ns/op 7 B/op 0 allocs/op Benchmark_StrSet_Remove-4 5000000 306 ns/op 7 B/op 0 allocs/op Benchmark_Unsafe_IntSet_Add-4 10000000 258 ns/op 35 B/op 0 allocs/op Benchmark_Unsafe_IntSet_Contains-4 20000000 146 ns/op 0 B/op 0 allocs/op Benchmark_Unsafe_IntSet_Remove-4 10000000 173 ns/op 0 B/op 0 allocs/op Benchmark_Unsafe_AnySet_Add-4 5000000 355 ns/op 41 B/op 1 allocs/op Benchmark_Unsafe_AnySet_Contains-4 10000000 150 ns/op 0 B/op 0 allocs/op Benchmark_Unsafe_AnySet_Remove-4 200000000 11.9 ns/op 0 B/op 0 allocs/op Benchmark_Unsafe_StrSet_Add-4 5000000 486 ns/op 59 B/op 1 allocs/op Benchmark_Unsafe_StrSet_Contains-4 5000000 298 ns/op 7 B/op 0 allocs/op Benchmark_Unsafe_StrSet_Remove-4 10000000 158 ns/op 7 B/op 0 allocs/op ``` # 集合类型-方法介绍 **提示** 以下常用方法列表,文档更新可能滞后于代码新特性,更多的方法及示例请参考代码文档: [https://pkg.go.dev/github.com/gogf/gf/v2/container/gset](https://pkg.go.dev/github.com/gogf/gf/v2/container/gset) **提示** 这里以 `StrSet` 类型介绍方法使用,其他集合类型的方法与此类似,不再赘述。 ## `NewStrSet` - 说明: `NewStrSet` 创建并返回一个空的集合,其中包含没有重复字符串的数据。参数 `safe` 用于指定是否在并发安全中使用,默认情况下是 `false`。 - 格式: ```go func NewStrSet(safe ...bool) *StrSet ``` - 示例: ```go func ExampleNewStrSet() { strSet := gset.NewStrSet(true) strSet.Add([]string{"str1", "str2", "str3"}...) fmt.Println(strSet.Slice()) // May Output: // [str3 str1 str2] } ``` ## `NewStrSetFrom` - 说明: `NewStrSetFrom` 通过给定的数组创建集合集合。参数 `safe` 用于指定是否在并发安全中使用,默认情况下是 `false`。 - 格式: ```go func NewStrSetFrom(items []string, safe ...bool) *StrSet ``` - 示例: ```go func ExampleNewStrSetFrom() { strSet := gset.NewStrSetFrom([]string{"str1", "str2", "str3"}, true) fmt.Println(strSet.Slice()) // May Output: // [str1 str2 str3] } ``` ## `Add` - 说明: `Add` 添加一个或多个元素项到集合中。 - 格式: ```go func (set *StrSet) Add(item ...string) ``` - 示例: ```go func ExampleStrSet_Add() { strSet := gset.NewStrSetFrom([]string{"str1", "str2", "str3"}, true) strSet.Add("str") fmt.Println(strSet.Slice()) fmt.Println(strSet.AddIfNotExist("str")) // Mya Output: // [str str1 str2 str3] // false } ``` ## `AddIfNotExist` - 说明: `Addifnotexist` 检查集合中是否存在指定元素项 `item`,如果不存在则将 `item` 添加到集合中并返回 `true`,否则什么也不做并返回 `false`。 - 格式: ```go func (set *StrSet) AddIfNotExist(item string) bool ``` - 示例: ```go func ExampleStrSet_AddIfNotExist() { strSet := gset.NewStrSetFrom([]string{"str1", "str2", "str3"}, true) strSet.Add("str") fmt.Println(strSet.Slice()) fmt.Println(strSet.AddIfNotExist("str")) // Mya Output: // [str str1 str2 str3] // false } ``` ## `AddIfNotExistFunc` - 说明:`AddIfNotExistFunc` 检查集合中存在指定元素项 `item`,如果不存在并且方法 `f` 返回 `true` 时,则将 `item` 设置到集合中并返回 `true`,否则什么也不做并返回 `false`。 - 格式: ```go func (set *StrSet) AddIfNotExistFunc(item string, f func() bool) bool ``` - 示例: ```go func ExampleStrSet_AddIfNotExistFunc() { strSet := gset.NewStrSetFrom([]string{"str1", "str2", "str3"}, true) strSet.Add("str") fmt.Println(strSet.Slice()) fmt.Println(strSet.AddIfNotExistFunc("str5", func() bool { return true })) // May Output: // [str1 str2 str3 str] // true } ``` ## `AddIfNotExistFuncLock` - 说明:`AddifnotExistFuncLock` 与 `AddIfNotExistFunc` 类似,不过当多个 `goroutine` 同时调用 `AddifnotExistFuncLock` 方法时,内部使用并发安全锁机制保证同时只能一个 `goroutine` 执行。该方法只有在创建集合时的 `safe` 参数设置 `true` 时有效,否则表现和方法一致 `AddIfNotExistFunc`。 - 格式: ```go func (set *StrSet) AddIfNotExistFuncLock(item string, f func() bool) bool ``` - 示例: ```go func ExampleStrSet_AddIfNotExistFuncLock() { strSet := gset.NewStrSetFrom([]string{"str1", "str2", "str3"}, true) strSet.Add("str") fmt.Println(strSet.Slice()) fmt.Println(strSet.AddIfNotExistFuncLock("str4", func() bool { return true })) // May Output: // [str1 str2 str3 str] // true } ``` ## `Clear` - 说明: `Clear` 删除集合的所有元素项。 - 格式: ```go func (set *StrSet) Clear() ``` - 示例: ```go func ExampleStrSet_Clear() { strSet := gset.NewStrSetFrom([]string{"str1", "str2", "str3"}, true) fmt.Println(strSet.Size()) strSet.Clear() fmt.Println(strSet.Size()) // Output: // 3 // 0 } ``` ## `Intersect` - 说明: `Intrersect` 执行集合 `set` 与 `others` 的交集,并返回一个新的集合 `newSet`,在 `newSet` 中的元素项同时存在于集合 `set` 与 `others` 中。 - 格式: ```go func (set *StrSet) Intersect(others ...*StrSet) (newSet *StrSet) ``` - 示例: ```go func ExampleStrSet_Intersect() { s1 := gset.NewStrSet(true) s1.Add([]string{"a", "b", "c"}...) var s2 gset.StrSet s2.Add([]string{"a", "b", "c", "d"}...) fmt.Println(s2.Intersect(s1).Slice()) // May Output: // [c a b] } ``` ## `Diff` - 说明: `Diff` 执行集合 `set` 与 `others` 差集操作,并返回一个新的集合 `newSet`。在 `newSet` 中的元素项存在于 `set` 但不存在于集合 `others`。注意,参数 `others` 可以指定多个集合参数。 - 格式: ```go func (set *StrSet) Diff(others ...*StrSet) (newSet *StrSet) ``` - 示例: ```go func ExampleStrSet_Diff() { s1 := gset.NewStrSetFrom([]string{"a", "b", "c"}, true) s2 := gset.NewStrSetFrom([]string{"a", "b", "c", "d"}, true) fmt.Println(s2.Diff(s1).Slice()) // Output: // [d] } ``` ## `Union` - 说明: `union` 执行集合 `set` 与 `others` 的并集操作,并返回一个新的集合 `newSet`。 - 格式: ```go func (set *StrSet) Union(others ...*StrSet) (newSet *StrSet) ``` - 示例: ```go func ExampleStrSet_Union() { s1 := gset.NewStrSet(true) s1.Add([]string{"a", "b", "c", "d"}...) s2 := gset.NewStrSet(true) s2.Add([]string{"a", "b", "d"}...) fmt.Println(s1.Union(s2).Slice()) // May Output: // [a b c d] } ``` ## `Complement` - 说明: `Complement` 执行 `set` 与 `full` 的补集操作,并返回一个新集合 `newSet`。 - 格式: ```go func (set *StrSet) Complement(full *StrSet) (newSet *StrSet) ``` - 示例: ```go func ExampleStrSet_Complement() { strSet := gset.NewStrSetFrom([]string{"str1", "str2", "str3", "str4", "str5"}, true) s := gset.NewStrSetFrom([]string{"str1", "str2", "str3"}, true) fmt.Println(s.Complement(strSet).Slice()) // May Output: // [str4 str5] } ``` ## `Contains` - 说明: `Contains` 包含检查集是否包含 `item`。 - 格式: ```go func (set *StrSet) Contains(item string) bool ``` - 示例: ```go func ExampleStrSet_Contains() { var set gset.StrSet set.Add("a") fmt.Println(set.Contains("a")) fmt.Println(set.Contains("A")) // Output: // true // false } ``` ## `ContainsI` - 说明:`ContainsI` 方法类似于 `Contains`,只是它不区分大小写比较大小。 - 格式: ```go func (set *StrSet) ContainsI(item string) bool ``` - 示例: ```go func ExampleStrSet_ContainsI() { var set gset.StrSet set.Add("a") fmt.Println(set.ContainsI("a")) fmt.Println(set.ContainsI("A")) // Output: // true // true } ``` ## `Equal` - 说明:`Equal` 检查两个集合是否完全相等(包括大小以及元素项)。 - 格式: ```go func (set *StrSet) Equal(other *StrSet) bool ``` - 示例: ```go func ExampleStrSet_Equal() { s1 := gset.NewStrSetFrom([]string{"a", "b", "c"}, true) s2 := gset.NewStrSetFrom([]string{"a", "b", "c", "d"}, true) fmt.Println(s2.Equal(s1)) s3 := gset.NewStrSetFrom([]string{"a", "b", "c"}, true) s4 := gset.NewStrSetFrom([]string{"a", "b", "c"}, true) fmt.Println(s3.Equal(s4)) // Output: // false // true } ``` ## `IsSubSetOf` - 说明: `IsSumSetOf` 检查当前集合 `set` 是否是指定集合 `other` 的子集。 - 格式: ```go func (set *StrSet) IsSubsetOf(other *StrSet) bool ``` - 示例: ```go func ExampleStrSet_IsSubsetOf() { s1 := gset.NewStrSet(true) s1.Add([]string{"a", "b", "c", "d"}...) var s2 gset.StrSet s2.Add([]string{"a", "b", "d"}...) fmt.Println(s2.IsSubsetOf(s1)) // Output: // true } ``` ## `Iterator` - 说明: `Iterator` 迭代器通过给定的回调函数 `f` 随机遍历当前集合 `set`,如果方法 `f` 返回 `true`,则继续遍历,否则停止。 - 格式: ```go func (set *StrSet) Iterator(f func(v string) bool) ``` - 示例: ```go func ExampleStrSet_Iterator() { s1 := gset.NewStrSet(true) s1.Add([]string{"a", "b", "c", "d"}...) s1.Iterator(func(v string) bool { fmt.Println("Iterator", v) return true }) // May Output: // Iterator a // Iterator b // Iterator c // Iterator d } ``` ## `Join` - 说明: `Join` 将集合中的元素项通过字符串 `glue` 拼接成新的字符串返回。 - 格式: ```go func (set *StrSet) Join(glue string) string ``` - 示例: ```go func ExampleStrSet_Join() { s1 := gset.NewStrSet(true) s1.Add([]string{"a", "b", "c", "d"}...) fmt.Println(s1.Join(",")) // May Output: // b,c,d,a } ``` ## `LockFunc` - 说明: `LockFunc` 仅在并发安全场景下有用,该方法通过写锁锁定集合 `set`,并执行回调方法 `f`。 - 格式: ```go func (set *StrSet) LockFunc(f func(m map[string]struct{})) ``` - 示例: ```go func ExampleStrSet_LockFunc() { s1 := gset.NewStrSet(true) s1.Add([]string{"1", "2"}...) s1.LockFunc(func(m map[string]struct{}) { m["3"] = struct{}{} }) fmt.Println(s1.Slice()) // May Output // [2 3 1] } ``` ## `RLockFunc` - 说明: `RLockFunc` 仅在并发安全场景下有用,该方法通过读锁锁定集合 `set`,并执行回调方法 `f`。 - 格式: ```go func (set *StrSet) RLockFunc(f func(m map[string]struct{})) ``` - 示例: ```go func ExampleStrSet_RLockFunc() { s1 := gset.NewStrSet(true) s1.Add([]string{"a", "b", "c", "d"}...) s1.RLockFunc(func(m map[string]struct{}) { fmt.Println(m) }) // Output: // map[a:{} b:{} c:{} d:{}] } ``` ## `Merge` - 说明: `Merge` 将集合 `others` 中的所有元素项合并到 `set` 中。 - 格式: ```go func (set *StrSet) Merge(others ...*StrSet) *StrSet ``` - 示例: ```go func ExampleStrSet_Merge() { s1 := gset.NewStrSet(true) s1.Add([]string{"a", "b", "c", "d"}...) s2 := gset.NewStrSet(true) fmt.Println(s1.Merge(s2).Slice()) // May Output: // [d a b c] } ``` ## `Pop` - 说明: `Pop` 随机从集合中取出一个元素项。 - 格式: ```go func (set *StrSet) Pop() string ``` - 示例: ```go func ExampleStrSet_Pop() { s1 := gset.NewStrSet(true) s1.Add([]string{"a", "b", "c", "d"}...) fmt.Println(s1.Pop()) // May Output: // a } ``` ## `Pops` - 说明: `Pops` 从集合中随机弹出 `size` 个元素项。如果 `size == -1`,则返回所有元素项。 - 格式: ```go func (set *StrSet) Pops(size int) []string ``` - 示例: ```go func ExampleStrSet_Pops() { s1 := gset.NewStrSet(true) s1.Add([]string{"a", "b", "c", "d"}...) for _, v := range s1.Pops(2) { fmt.Println(v) } // May Output: // a // b } ``` ## `Remove` - 说明: `Remove` 从集合中删除指定的元素项 `item`。 - 格式: ```go func (set *StrSet) Remove(item string) ``` - 示例: ```go func ExampleStrSet_Remove() { s1 := gset.NewStrSet(true) s1.Add([]string{"a", "b", "c", "d"}...) s1.Remove("a") fmt.Println(s1.Slice()) // May Output: // [b c d] } ``` ## `Size` - 说明: `Size` 返回集合的大小。 - 格式: ```go func (set *StrSet) Size() int ``` - 示例: ```go func ExampleStrSet_Size() { s1 := gset.NewStrSet(true) s1.Add([]string{"a", "b", "c", "d"}...) fmt.Println(s1.Size()) // Output: // 4 } ``` ## `Silce` - 说明: `Slice` 将集合中的元素项以 `slice` 的形式返回。 - 格式: ```go func (set *StrSet) Slice() []string ``` - 示例: ```go func ExampleStrSet_Slice() { s1 := gset.NewStrSet(true) s1.Add([]string{"a", "b", "c", "d"}...) fmt.Println(s1.Slice()) // May Output: // [a,b,c,d] } ``` ## `String` - 说明: `String` 将集合按照字符串返回。 - 格式: ```go func (set *StrSet) String() string ``` - 示例: ```go func ExampleStrSet_String() { s1 := gset.NewStrSet(true) s1.Add([]string{"a", "b", "c", "d"}...) fmt.Println(s1.String()) // May Output: // "a","b","c","d" } ``` ## `Sum` - 说明: `Sum` 将集合中的元素项执行求和,注意:只有元素项为数字时才有效,否则将得到一个意想不到的结果。 - 格式: ```go func (set *StrSet) Sum() (sum int) ``` - 示例: ```go func ExampleStrSet_Sum() { s1 := gset.NewStrSet(true) s1.Add([]string{"1", "2", "3", "4"}...) fmt.Println(s1.Sum()) // Output: // 10 } ``` ## `Walk` - 说明: `Walk` 按照用户给定的回调方法 `f` 遍历当前集合,并将 `f` 的返回结果重新设置当前集合。注意,在并发安全场景中,该方法内部使用写锁来保证并发安全性。 - 格式: ```go func (set *StrSet) Walk(f func(item string) string) *StrSet ``` - 示例: ```go func ExampleStrSet_Walk() { var ( set gset.StrSet names = g.SliceStr{"user", "user_detail"} prefix = "gf_" ) set.Add(names...) // Add prefix for given table names. set.Walk(func(item string) string { return prefix + item }) fmt.Println(set.Slice()) // May Output: // [gf_user gf_user_detail] } ``` ## `MarshalJSON` - 说明: `MarshalJSON` 实现了 `json.Marshal` 的 `MarshalJSON` 接口。 - 格式: ```go func (set *StrSet) MarshalJSON() ([]byte, error) ``` - 示例: ```go func ExampleStrSet_MarshalJSON() { type Student struct { Id int Name string Scores *gset.StrSet } s := Student{ Id: 1, Name: "john", Scores: gset.NewStrSetFrom([]string{"100", "99", "98"}, true), } b, _ := json.Marshal(s) fmt.Println(string(b)) // May Output: // {"Id":1,"Name":"john","Scores":["100","99","98"]} } ``` ## `UnmarshalJSON` - 说明: `UnmarshalJSON` 实现了 `json.Unmarshal` 中的 `UnmarshalJSON` 接口。 - 格式: ```go func (set *StrSet) UnmarshalJSON(b []byte) error ``` - 示例: ```go func ExampleStrSet_UnmarshalJSON() { b := []byte(`{"Id":1,"Name":"john","Scores":["100","99","98"]}`) type Student struct { Id int Name string Scores *gset.StrSet } s := Student{} json.Unmarshal(b, &s) fmt.Println(s) // May Output: // {1 john "99","98","100"} } ``` ## `UnmarshalValue` - 说明:`UnfarshalValue` 实现 `goframe` 框架内部统一的设置接口,它通过一个 `interface{}` 类型的参数初始化当前对象,至于 `interface{}` 参数的使用逻辑由该接口实现方法决定。 - 格式: ```go func (set *StrSet) UnmarshalValue(value interface{}) (err error) ``` - 示例: ```go func ExampleStrSet_UnmarshalValue() { b := []byte(`{"Id":1,"Name":"john","Scores":["100","99","98"]}`) type Student struct { Id int Name string Scores *gset.StrSet } s := Student{} json.Unmarshal(b, &s) fmt.Println(s) // May Output: // {1 john "99","98","100"} } ``` # 链表类型-glist ## 基本介绍 带并发安全开关的双向列表。 **使用场景**: 双向链表。 **使用方式:** ```go import "github.com/gogf/gf/v2/container/glist" ``` **接口文档**: [https://pkg.go.dev/github.com/gogf/gf/v2/container/glist](https://pkg.go.dev/github.com/gogf/gf/v2/container/glist) **泛型支持**: 从 `v2.10` 版本开始,`glist` 提供了泛型链表类型: - `TList[T]`:泛型双向链表,提供类型安全的链表操作 - 支持高效的首尾插入、删除操作 - 推荐在新项目中使用泛型链表,享受编译时类型检查带来的安全性 # 链表类型-基本使用 ## 泛型链表(推荐使用) 从 `v2.10` 版本开始,`glist` 提供了泛型链表 `TList[T]`,提供类型安全的双向链表操作。 ### 基本使用 ```go package main import ( "fmt" "github.com/gogf/gf/v2/container/glist" ) func main() { // 创建一个int类型的泛型链表 l := glist.NewT[int]() // 从尾部添加元素 l.PushBack(1) l.PushBack(2) l.PushBack(3) // 从头部添加元素 l.PushFront(0) // 获取长度 fmt.Println("Length:", l.Len()) // 输出: Length: 4 // 获取首尾元素 fmt.Println("Front:", l.Front().Value) // 输出: Front: 0 fmt.Println("Back:", l.Back().Value) // 输出: Back: 3 // 从头部弹出元素 front := l.PopFront() fmt.Println("PopFront:", front) // 输出: PopFront: 0 // 遍历链表 l.IteratorAsc(func(e *glist.TElement[int]) bool { fmt.Println("Element:", e.Value) return true }) } ``` ### 链表遍历 泛型链表支持正序和逆序遍历。 ```go package main import ( "fmt" "github.com/gogf/gf/v2/container/glist" ) func main() { l := glist.NewTFrom[string]([]string{"a", "b", "c", "d", "e"}) // 正序遍历 fmt.Println("Ascending:") l.IteratorAsc(func(e *glist.TElement[string]) bool { fmt.Println(e.Value) return true }) // 逆序遍历 fmt.Println("Descending:") l.IteratorDesc(func(e *glist.TElement[string]) bool { fmt.Println(e.Value) return true }) } ``` ### 泛型链表特点 1. **类型安全**:在编译期就能发现类型错误,避免运行时类型断言失败。 2. **高效操作**:支持O(1)时间复杂度的首尾插入和删除操作。 3. **更好的IDE支持**:IDE能够提供更准确的代码补全和类型提示。 **提示** 推荐在新项目中使用泛型链表 `TList[T]`,它提供了更好的类型安全性和开发体验。 ## 传统链表类型 下面介绍传统的链表类型,这些类型在旧版本中被广泛使用,仍然完全支持。 ### 基础使用 ```go package main import ( "fmt" "github.com/gogf/gf/v2/container/glist" ) func main() { // Not concurrent-safe in default. l := glist.New() // Push l.PushBack(1) //从后面插入值 l.PushBack(2) //从后面插入值 e := l.PushFront(0) //从前面插入值 // Insert l.InsertBefore(e, -1) //从0的前面插入值 l.InsertAfter(e, "a") //从0的后面插入值 fmt.Println(l) // Pop Pop 出栈后,从list里移除 fmt.Println(l.PopFront()) // 从前面出栈,返回出栈的值 fmt.Println(l.PopBack()) //从后面出栈 fmt.Println(l) // All fmt.Println(l.FrontAll()) //正序返回一个复本 fmt.Println(l.BackAll()) //逆序返回一个复本 // Output: // [-1,0,"a",1,2] // -1 // 2 // [0,"a",1] // [0 "a" 1] // [1 "a" 0] } ``` ### 链表遍历 该示例中我们将通过读锁和写锁遍历一个并发安全的链表,分别通过 `RLockFunc` 和 `LockFunc` 实现。执行后,输出结果为: ```go package main import ( "container/list" "fmt" "github.com/gogf/gf/v2/container/garray" "github.com/gogf/gf/v2/container/glist" ) func main() { // concurrent-safe list. l := glist.NewFrom(garray.NewArrayRange(1, 9, 1).Slice(), true) fmt.Println(l) // iterate reading from head. l.RLockFunc(func(list *list.List) { length := list.Len() if length > 0 { for i, e := 0, list.Front(); i < length; i, e = i+1, e.Next() { fmt.Print(e.Value) } } }) fmt.Println() // iterate reading from tail. l.RLockFunc(func(list *list.List) { length := list.Len() if length > 0 { for i, e := 0, list.Back(); i < length; i, e = i+1, e.Prev() { fmt.Print(e.Value) } } }) fmt.Println() // iterate reading from head using IteratorAsc. l.IteratorAsc(func(e *glist.Element) bool { fmt.Print(e.Value) return true }) fmt.Println() // iterate reading from tail using IteratorDesc. l.IteratorDesc(func(e *glist.Element) bool { fmt.Print(e.Value) return true }) fmt.Println() // iterate writing from head. l.LockFunc(func(list *list.List) { length := list.Len() if length > 0 { for i, e := 0, list.Front(); i < length; i, e = i+1, e.Next() { if e.Value == 6 { e.Value = "M" break } } } }) fmt.Println(l) // Output: // [1,2,3,4,5,6,7,8,9] // 123456789 // 987654321 // 123456789 // 987654321 // [1,2,3,4,5,M,7,8,9] ``` ### `Push*` 元素项入栈 ```go package main import ( "fmt" "github.com/gogf/gf/v2/container/glist" "github.com/gogf/gf/v2/frame/g" ) func main() { l := glist.NewFrom(g.Slice{1, 2, 3, 4, 5}) l.PushBack(6) fmt.Println(l) l.PushFront(0) fmt.Println(l) // 正数从右边入栈 l.PushBacks(g.Slice{7, 8}) fmt.Println(l) // 负数从左边入栈 l.PushFronts(g.Slice{-1, -2}) fmt.Println(l) l.PushFrontList(glist.NewFrom(g.Slice{"a", "b", "c"})) l.PushBackList(glist.NewFrom(g.Slice{"d", "e", "f"})) fmt.Println(l) // Output: // [1,2,3,4,5,6] // [0,1,2,3,4,5,6] // [0,1,2,3,4,5,6,7,8] // [-2,-1,0,1,2,3,4,5,6,7,8] // ["a","b","c",-2,-1,0,1,2,3,4,5,6,7,8,"d","e","f"] } ``` ### `Pop*` 元素项出栈 ```go package main import ( "fmt" "github.com/gogf/gf/v2/container/glist" "github.com/gogf/gf/v2/frame/g" ) func main() { l := glist.NewFrom(g.Slice{1, 2, 3, 4, 5, 6, 7, 8, 9}) fmt.Println(l.PopBack()) fmt.Println(l.PopBacks(2)) fmt.Println(l.PopFront()) fmt.Println(l.PopFronts(2)) fmt.Println(glist.NewFrom(g.Slice{"a", "b", "c", "d"}).PopFrontAll()) fmt.Println(glist.NewFrom(g.Slice{"a", "b", "c", "d"}).PopBackAll()) // Output: // 9 // [8 7] // 1 // [2 3] // [4,5,6] // [a b c d] // [d c b a] } ``` ### `Move*/Insert*` 元素项移动、插入 ```go package main import ( "fmt" "github.com/gogf/gf/v2/container/glist" "github.com/gogf/gf/v2/frame/g" ) func main() { l := glist.NewFrom(g.Slice{1, 2, 3, 4, 5, 6, 7, 8, 9}) l.MoveToBack(l.Front()) //将第一个元素(1)移动最右边 [2,3,4,5,6,7,8,9,1] l.MoveToFront(l.Back().Prev()) //将最后一项的前一个元素(9)移动最左边 [9,2,3,4,5,6,7,8,1] fmt.Println(l) // 将2到栈首元素的前面 l.MoveBefore(l.Front().Next(), l.Front()) // 将8到栈尾元素的后面 l.MoveAfter(l.Back().Prev(), l.Back()) fmt.Println(l) // 在栈尾元素前插入新元素 l.InsertBefore(l.Back(), "a") // 在栈首元素后插入新元素 l.InsertAfter(l.Front(), "b") // Output: // [9,2,3,4,5,6,7,8,1] // [2,9,3,4,5,6,7,1,8] // [2,"b",9,3,4,5,6,7,1,"a",8] } ``` ### `Join` 元素项串连 ```go package main import ( "fmt" "github.com/gogf/gf/v2/container/glist" "github.com/gogf/gf/v2/frame/g" ) func main() { var l glist.List l.PushBacks(g.Slice{"a", "b", "c", "d"}) fmt.Println(l.Join(",")) // Output: // a,b,c,d } ``` ### `Remove*` 元素项移除 ```go package main import ( "fmt" "github.com/gogf/gf/v2/container/glist" "github.com/gogf/gf/v2/frame/g" ) func main() { l := glist.NewFrom(g.Slice{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}) fmt.Println(l) fmt.Println(l.Remove(l.Front())) fmt.Println(l) l.Removes([]*glist.Element{l.Front(), l.Front().Next()}) fmt.Println(l) l.RemoveAll() fmt.Println(l) // Output: // [0,1,2,3,4,5,6,7,8,9] // 0 // [1,2,3,4,5,6,7,8,9] // [3,4,5,6,7,8,9] // [] } ``` ### `JSON` 序列化/反序列 `glist` 容器实现了标准库 `json` 数据格式的序列化/反序列化接口。 - `Marshal` ```go package main import ( "encoding/json" "fmt" "github.com/gogf/gf/v2/container/glist" "github.com/gogf/gf/v2/frame/g" ) func main() { type Student struct { Id int Name string Scores *glist.List } s := Student{ Id: 1, Name: "john", Scores: glist.NewFrom(g.Slice{100, 99, 98}), } b, _ := json.Marshal(s) fmt.Println(string(b)) // Output: // {"Id":1,"Name":"john","Scores":[100,99,98]} } ``` - `Unmarshal` ```go package main import ( "encoding/json" "fmt" "github.com/gogf/gf/v2/container/glist" ) func main() { b := []byte(`{"Id":1,"Name":"john","Scores":[100,99,98]}`) type Student struct { Id int Name string Scores *glist.List } s := Student{} json.Unmarshal(b, &s) fmt.Println(s) // Output: // {1 john [100,99,98]} } ``` # 链表类型-性能测试 [https://github.com/gogf/gf/blob/master/container/glist/glist\_z\_bench\_test.go](https://github.com/gogf/gf/blob/master/container/glist/glist_z_bench_test.go) ``` goos: linux goarch: amd64 pkg: github.com/gogf/gf/v2/container/glist Benchmark_PushBack-4 5000000 268 ns/op 56 B/op 2 allocs/op Benchmark_PushFront-4 10000000 435 ns/op 56 B/op 2 allocs/op Benchmark_Len-4 30000000 44.5 ns/op 0 B/op 0 allocs/op Benchmark_PopFront-4 20000000 71.1 ns/op 0 B/op 0 allocs/op Benchmark_PopBack-4 30000000 70.1 ns/op 0 B/op 0 allocs/op PASS ``` # 泛型类型-gvar ## 基本介绍 `gvar` 是一种 **运行时泛型** 实现,以较小的运行时开销提高开发便捷性以及研发效率,支持各种内置的数据类型转换,可以作为 `interface{}` 类型的替代数据类型,并且该类型支持并发安全开关。 **提示** 框架同时提供了 `g.Var` 的数据类型,其实也是 `gvar.Var` 数据类型的别名。 **使用场景**: 使用 `interface{}` 的场景,各种不固定数据类型格式,或者需要对变量进行频繁的数据类型转换的场景。 **使用方式**: ```go import "github.com/gogf/gf/v2/container/gvar" ``` **接口文档**: [https://pkg.go.dev/github.com/gogf/gf/v2/container/gvar](https://pkg.go.dev/github.com/gogf/gf/v2/container/gvar) # 泛型类型-基本使用 ### 基本使用 ```go package main import ( "github.com/gogf/gf/v2/frame/g" "fmt" ) func main() { var v g.Var v.Set("123") fmt.Println(v.Val()) // 基本类型转换 fmt.Println(v.Int()) fmt.Println(v.Uint()) fmt.Println(v.Float64()) // slice转换 fmt.Println(v.Ints()) fmt.Println(v.Floats()) fmt.Println(v.Strings()) } ``` 执行后,输出结果为: ```html 123 123 123 123 [123] [123] [123] ``` ### `JSON` 序列化/反序列 `gvar.Var` 容器实现了标准库 `json` 数据格式的序列化/反序列化接口。 1. `Marshal` ```go package main import ( "encoding/json" "fmt" "github.com/gogf/gf/v2/frame/g" ) func main() { type Student struct { Id *g.Var Name *g.Var Scores *g.Var } s := Student{ Id: g.NewVar(1), Name: g.NewVar("john"), Scores: g.NewVar([]int{100, 99, 98}), } b, _ := json.Marshal(s) fmt.Println(string(b)) } ``` 执行后,输出结果: ```bash {"Id":1,"Name":"john","Scores":[100,99,98]} ``` 2. `Unmarshal` ```go package main import ( "encoding/json" "fmt" "github.com/gogf/gf/v2/frame/g" ) func main() { b := []byte(`{"Id":1,"Name":"john","Scores":[100,99,98]}`) type Student struct { Id *g.Var Name *g.Var Scores *g.Var } s := Student{} json.Unmarshal(b, &s) fmt.Println(s) } ``` 执行后,输出结果: ```bash {1 john [100,99,98]} ``` # 泛型类型-方法介绍 **提示** 以下常用方法列表,文档更新可能滞后于代码新特性,更多的方法及示例请参考代码文档: [https://pkg.go.dev/github.com/gogf/gf/v2/container/gvar](https://pkg.go.dev/github.com/gogf/gf/v2/container/gvar) ### `New` - 说明:`New`创建并返回一个带有给定`value`的新 `Var`。可选参数 `safe`指定是否在并发安全中使用 `Var`,其默认值是`false`。 - 格式: ```go func New(value interface{}, safe ...bool) *Var ``` -  示例: ```go // New func ExampleVarNew() { v := gvar.New(400) g.Dump(v) // Output: // "400" } ``` ### `Clone` - 说明: `Clone` 将当前 `Var`的浅拷贝并返回一个指向此`Var`的指针。 - 格式: ```go func (v *Var) Clone() *Var ``` - 示例 ```go // Clone func ExampleVar_Clone() { tmp := "fisrt hello" v := gvar.New(tmp) g.DumpWithType(v.Clone()) fmt.Println(v == v.Clone()) // Output: // *gvar.Var(11) "fisrt hello" // false } ``` ### `Set` - 说明: `Set` 将 `v` 的值设置为 `value` 并返回`v`的旧值。 - 格式: ```go func (v *Var) Set(value interface{}) (old interface{}) ``` - 示例: ```go // Set func ExampleVar_Set() { var v = gvar.New(100.00) g.Dump(v.Set(200.00)) g.Dump(v) // Output: // 100 // "200" } ``` ### `Val` - 说明:`Val`返回 `v` 的当前值,类型为 `interface{}`。 - 格式: ```go func (v *Var) Val() interface{} ``` - 示例: ```go // Val func ExampleVar_Val() { var v = gvar.New(100.00) g.DumpWithType(v.Val()) // Output: // float64(100) } ``` ### `Interface` - 说明: `Interface` 是`Val`的别名。 - 格式: ```go func (v *Var) Interface() interface{} ``` - 示例: ```go // Interface func ExampleVar_Interface() { var v = gvar.New(100.00) g.DumpWithType(v.Interface()) // Output: // float64(100) } ``` ### `Bytes` - 说明:`Bytes`将`v`转换为字节数组。 - 格式: ```go func (v *Var) Bytes() []byte ``` - 示例: ```go // Bytes func ExampleVar_Bytes() { var v = gvar.New("GoFrame") g.DumpWithType(v.Bytes()) // Output: // []byte(7) "GoFrame" } ``` ### `String` - 说明: `String` 将 `v` 转换为字符串。 - 格式: ```go func (v *Var) String() string ``` - 示例: ```go // String func ExampleVar_String() { var v = gvar.New("GoFrame") g.DumpWithType(v.String()) // Output: // string(7) "GoFrame" } ``` ### `Bool` - 说明:`Bool`将`v`转换为布尔值。 - 格式: ```go func (v *Var) Bool() bool ``` - 示例: ```go // Bool func ExampleVar_Bool() { var v = gvar.New(true) g.DumpWithType(v.Bool()) // Output: // bool(true) } ``` ### `Int` - 说明: `Int` 将 `v` 转换为整型。 - 格式: ```go func (v *Var) Int() int ``` - 示例: ```go // Int func ExampleVar_Int() { var v = gvar.New(-1000) g.DumpWithType(v.Int()) // Output: // int(-1000) } ``` ### `Uint` - 说明: `Uint` 将 `v` 转换为无符号整型。 - 格式: ```go func (v *Var) Uint() uint ``` - 示例: ```go // Uint func ExampleVar_Uint() { var v = gvar.New(1000) g.DumpWithType(v.Uint()) // Output: // uint(1000) } ``` ### `Float32` - 说明: `Float32` 将 `v` 转换为 `32位` 浮点型。 - 格式: ```go func (v *Var) Float32() float32 ``` - 示例: ```go // Float32 func ExampleVar_Float32() { var price = gvar.New(100.00) g.DumpWithType(price.Float32()) // Output: // float32(100) } ``` ### `Float64` - 说明: `Float64` 将 `v` 转换为 `64位` 浮点型。 - 格式: ```go func (v *Var) Float64() float64 ``` - 示例: ```go // Float32 func ExampleVar_Float64() { var price = gvar.New(100.00) g.DumpWithType(price.Float64()) // Output: // float64(100) } ``` ### `Time` - 说明:`Time` 将 `v` 转换为 `time.Time`。参数`format`用 `gtime` 指定时间字符串的格式,例如: `Y-m-d H:i:s`。 - 格式: ```go func (v *Var) Time(format ...string) time.Time ``` - 示例: ```go // Time func ExampleVar_Time() { var v = gvar.New("2021-11-11 00:00:00") g.DumpWithType(v.Time()) // Output: // time.Time(29) "2021-11-11 00:00:00 +0800 CST" } ``` ### `GTime` - 说明: `G``Time` 将 `v` 转换为 `*gtime.Time`。参数`format`用`gtime` 指定时间字符串的格式,例如: `Y-m-d H:i:s`。 - 格式: ```go func (v *Var) GTime(format ...string) *gtime.Time ``` - 示例: ```go // GTime func ExampleVar_GTime() { var v = gvar.New("2021-11-11 00:00:00") g.DumpWithType(v.GTime()) // Output: // *gtime.Time(19) "2021-11-11 00:00:00" } ``` ### `Duration` - 说明: `Duration`将 `v` 转换为 `time.Duration`。如果 `v` 的值是字符串,那么它使用 `time.ParseDuration`进行转换。 - 格式: ```go func (v *Var) Duration() time.Duration ``` - 示例: ```go // Duration func ExampleVar_Duration() { var v = gvar.New("300s") g.DumpWithType(v.Duration()) // Output: // time.Duration(4) "5m0s" } ``` ### `MarshalJSON` - 说明:`MarshalJSON`实现了`json`接口的`MarshalJSON`。 - 格式: ```go func (v *Var) MarshalJSON() ([]byte, error) ``` - 示例: ```go // MarshalJSON func ExampleVar_MarshalJSON() { testMap := g.Map{ "code": "0001", "name": "Golang", "count": 10, } var v = gvar.New(testMap) res, err := json.Marshal(&v) if err != nil { panic(err) } g.DumpWithType(res) // Output: // []byte(42) "{"code":"0001","count":10,"name":"Golang"}" } ``` ### `UnmarshalJSON` - 说明:`UnmarshalJSON ` 实现了 `json` 接口的 ` UnmarshalJSON`。 - 格式: ```go func (v *Var) UnmarshalJSON(b []byte) error ``` - 示例: ```go // UnmarshalJSON func ExampleVar_UnmarshalJSON() { tmp := []byte(`{ "Code": "0003", "Name": "Golang Book3", "Quantity": 3000, "Price": 300, "OnSale": true }`) var v = gvar.New(map[string]interface{}{}) if err := json.Unmarshal(tmp, &v); err != nil { panic(err) } g.Dump(v) // Output: // "{\"Code\":\"0003\",\"Name\":\"Golang Book3\",\"OnSale\":true,\"Price\":300,\"Quantity\":3000}" } ``` ### `UnmarshalValue` - 说明:`UnmarshalValue`是一个接口实现,它为 `Var` 设置任何类型的值。 - 格式: ```go func (v *Var) UnmarshalValue(value interface{}) error ``` - 示例: ```go // UnmarshalValue func ExampleVar_UnmarshalValue() { tmp := g.Map{ "code": "00002", "name": "GoFrame", "price": 100, "sale": true, } var v = gvar.New(map[string]interface{}{}) if err := v.UnmarshalValue(tmp); err != nil { panic(err) } g.Dump(v) // Output: // "{\"code\":\"00002\",\"name\":\"GoFrame\",\"price\":100,\"sale\":true}" } ``` ### `IsNil` - 说明:`IsNil`判断 `v` 是否为 `nil`,为 `nil` 返回 `true`,反之返回 `false`。 - 格式: ```go func (v *Var) IsNil() bool ``` - 示例: ``` /// IsNil func ExampleVar_IsNil() { g.Dump(gvar.New(0).IsNil()) g.Dump(gvar.New(0.1).IsNil()) // true g.Dump(gvar.New(nil).IsNil()) g.Dump(gvar.New("").IsNil()) // Output: // false // false // true // false } ``` ### `IsEmpty` - 说明:`IsEmpty` 判断 `v` 是否为空,为空返回 `true`,反之返回 `false`。 - 格式: ```go func (v *Var) IsEmpty() bool ``` - 示例: ```go // IsEmpty func ExampleVar_IsEmpty() { g.Dump(gvar.New(0).IsEmpty()) g.Dump(gvar.New(nil).IsEmpty()) g.Dump(gvar.New("").IsEmpty()) g.Dump(gvar.New(g.Map{"k": "v"}).IsEmpty()) // Output: // true // true // true // false } ``` ### `IsInt` - 说明:`IsInt`判断 `v` 是否为`int`类型,如果是 `int` 类型返回 `true`,反之返回 `false`。 - 格式: ```go func (v *Var) IsInt() bool ``` - 示例: ```go // IsInt func ExampleVar_IsInt() { g.Dump(gvar.New(0).IsInt()) g.Dump(gvar.New(0.1).IsInt()) g.Dump(gvar.New(nil).IsInt()) g.Dump(gvar.New("").IsInt()) // Output: // true // false // false // false } ``` ### `IsUint` - 说明: `IsUint`判断 `v` 是否为 `uint` 类型,如果是 `uint` 类型返回 `true`,反之返回 `false`。 - 格式: ```go func (v *Var) IsUint() bool ``` - 示例: ```go // IsUint func ExampleVar_IsUint() { g.Dump(gvar.New(0).IsUint()) g.Dump(gvar.New(uint8(8)).IsUint()) g.Dump(gvar.New(nil).IsUint()) // Output: // false // true // false } ``` ### `IsFloat` - 说明:`IsFloat` 判断 `v` 是否为 `float` 类型,如果是 `float` 类型返回 `true`,反之返回 `false`。 - 格式: ```go func (v *Var) IsFloat() bool ``` - 示例: ```go // IsFloat func ExampleVar_IsFloat() { g.Dump(g.NewVar(uint8(8)).IsFloat()) g.Dump(g.NewVar(float64(8)).IsFloat()) g.Dump(g.NewVar(0.1).IsFloat()) // Output: // false // true // true } ``` ### `IsSlice` - 说明:`IsSlice` 判断 `v` 是否为切片类型,如果是 `slice` 类型返回 `true`,反之返回 `false`。 - 格式: ```go func (v *Var) IsSlice() bool ``` - 示例: ```go // IsSlice func ExampleVar_IsSlice() { g.Dump(g.NewVar(0).IsSlice()) g.Dump(g.NewVar(g.Slice{0}).IsSlice()) // Output: // false // true } ``` ### `IsMap` - 说明:`IsMap` 判断 `v` 是否为映射类型,如果是 `map` 类型返回`true`,反之返回`false`。 - 格式: ```go func (v *Var) IsMap() bool ``` - 示例: ```go // IsMap func ExampleVar_IsMap() { g.Dump(g.NewVar(0).IsMap()) g.Dump(g.NewVar(g.Map{"k": "v"}).IsMap()) g.Dump(g.NewVar(g.Slice{}).IsMap()) // Output: // false // true // false } ``` ### `IsStruct` - 说明:`IsStruct` 判断 `v` 是否为结构体类型,如果是 `struct` 类型返回 `true`,反之返回 `false`。 - 格式: ```go func (v *Var) IsStruct() bool ``` - 示例: ```go // IsStruct func ExampleVar_IsStruct() { g.Dump(g.NewVar(0).IsStruct()) g.Dump(g.NewVar(g.Map{"k": "v"}).IsStruct()) a := struct{}{} g.Dump(g.NewVar(a).IsStruct()) g.Dump(g.NewVar(&a).IsStruct()) // Output: // false // false // true // true } ``` ### `ListItemValues` - 说明:`ListItemValues`用键 `key` 检索并返回所有项结构/映射的元素。注意,参数 `list` 应该是切片类型,其中包含`map`或`struct`的元素,否则它会返回一个空切片。 - 格式: ```go func (v *Var) ListItemValues(key interface{}) (values []interface{}) ``` - 示例: ```go // ListItemValues func ExampleVar_ListItemValues() { var goods1 = g.List{ g.Map{"id": 1, "price": 100.00}, g.Map{"id": 2, "price": 0}, g.Map{"id": 3, "price": nil}, } var v = gvar.New(goods1) fmt.Println(v.ListItemValues("id")) fmt.Println(v.ListItemValues("price")) // Output: // [1 2 3] // [100 0 ] } ``` ### `ListItemValuesUnique` - 说明:`ListItemValuesUnique`检索并返回所有带有指定 `key` 的`struct/map`的唯一元素。注意,参数 `list` 应该是切片类型,其中包含 `map` 或`struct ` 的元素,否则它会返回一个空切片。 - 格式: ```go func (v *Var) ListItemValuesUnique(key string) []interface{} ``` - 示例: ```go // ListItemValuesUnique func ExampleVar_ListItemValuesUnique() { var ( goods1 = g.List{ g.Map{"id": 1, "price": 100.00}, g.Map{"id": 2, "price": 100.00}, g.Map{"id": 3, "price": nil}, } v = gvar.New(goods1) ) fmt.Println(v.ListItemValuesUnique("id")) fmt.Println(v.ListItemValuesUnique("price")) // Output: // [1 2 3] // [100 ] } ``` ### `Struct` - 说明:`Struct`将`v`的值映射到「指针」。参数`pointer`应该是指向结构体实例的指针。参数 `mapping` 用于指定键到属性的映射规则。 - 格式: ```go func (v *Var) Struct(pointer interface{}, mapping ...map[string]string) error ``` - 示例: ```go func ExampleVar_Struct() { params1 := g.Map{ "uid": 1, "Name": "john", } v := gvar.New(params1) type tartget struct { Uid int Name string } t := new(tartget) if err := v.Struct(&t); err != nil { panic(err) } g.Dump(t) // Output: // { // Uid: 1, // Name: "john", // } } ``` ### `Structs` - 说明:`Structs`将 `v` 转换为给定结构体的切片类型。参数`pointer`应该是指向结构体实例的指针。参数`mapping`用于指定键到属性的映射规则。 - 格式: ```go func (v *Var) Structs(pointer interface{}, mapping ...map[string]string) error ``` - 示例: ```go func ExampleVar_Structs() { paramsArray := []g.Map{} params1 := g.Map{ "uid": 1, "Name": "golang", } params2 := g.Map{ "uid": 2, "Name": "java", } paramsArray = append(paramsArray, params1, params2) v := gvar.New(paramsArray) type tartget struct { Uid int Name string } var t []tartget if err := v.Structs(&t); err != nil { panic(err) } g.DumpWithType(t) // Output: // []gvar_test.tartget(2) [ // gvar_test.tartget(2) { // Uid: int(1), // Name: string(6) "golang", // }, // gvar_test.tartget(2) { // Uid: int(2), // Name: string(4) "java", // }, // ] } ``` ### `Ints` - 说明:`Ints`将 `v` 转换为 `[]int`。 - 格式: ```go func (v *Var) Ints() []int ``` - 示例: ```go // Ints func ExampleVar_Ints() { var ( arr = []int{1, 2, 3, 4, 5} obj = gvar.New(arr) ) fmt.Println(obj.Ints()) // Output: // [1 2 3 4 5] } ``` ### `Int64s` - 说明:`Int64s`将 `v` 转换为`[]64int`。 - 格式: ```go func (v *Var) Int64s() []64int ``` - 示例: ```go // Int64s func ExampleVar_Int64s() { var ( arr = []int64{1, 2, 3, 4, 5} obj = gvar.New(arr) ) fmt.Println(obj.Int64s()) // Output: // [1 2 3 4 5] } ``` ### `Uints` - 说明: `Uints`将 `v` 转换为`[]uint`。 - 格式: ```go func (v *Var) Uints() []uint ``` - 示例: ```go // Uints func ExampleVar_Uints() { var ( arr = []uint{1, 2, 3, 4, 5} obj = gvar.New(arr) ) fmt.Println(obj.Uints()) // Output: // [1 2 3 4 5] } ``` ### `Uint64s` - 说明: `Uint64s`将 `v` 转换为`[]uint64`。 - 格式: ```go func (v *Var) Uint64s() []uint64 ``` - 示例: ```go // Uint64s func ExampleVar_Uint64s() { var ( arr = []uint64{1, 2, 3, 4, 5} obj = gvar.New(arr) ) fmt.Println(obj.Uint64s()) // Output: // [1 2 3 4 5] } ``` ### `Floats` - 说明: `Floats``Float64s`的别名。 - 格式: ```go func (v *Var) Floats() []float64 ``` - 示例: ```go // Floats func ExampleVar_Floats() { var ( arr = []float64{1, 2, 3, 4, 5} obj = gvar.New(arr) ) fmt.Println(obj.Floats()) // Output: // [1 2 3 4 5] } ``` ### `Float64s` - 说明: `Float64s`将 `v` 转换为 `[]float64`。 - 格式: ```go func (v *Var) Float64s() []float64 ``` - 示例: ```go // Float64s func ExampleVar_Float64s() { var ( arr = []float64{1, 2, 3, 4, 5} obj = gvar.New(arr) ) fmt.Println(obj.Float64s()) // Output: // [1 2 3 4 5] } ``` ### `Float32s` - 说明: `Float32s`将 `v` 转换为 `[]float32`。 - 格式: ```go func (v *Var) Float32s() []float32 ``` - 示例: ```go // Float32s func ExampleVar_Float32s() { var ( arr = []float32{1, 2, 3, 4, 5} obj = gvar.New(arr) ) fmt.Println(obj.Float32s()) // Output: // [1 2 3 4 5] } ``` ### `Strings` - 说明: `Strings`将 `v` 转换为`[]string`。 - 格式: ```go func (v *Var) Strings() []string ``` - 示例: ```go // Strings func ExampleVar_Strings() { var ( arr = []string{"GoFrame", "Golang"} obj = gvar.New(arr) ) fmt.Println(obj.Strings()) // Output: // [GoFrame Golang] } ``` ### `Interfaces` - 说明: `Interfaces`将 `v` 转换为 `[]interface{}`。 - 格式: ```go func (v *Var) Interfaces() []interface{} ``` - 示例: ```go // Interfaces func ExampleVar_Interfaces() { var ( arr = []string{"GoFrame", "Golang"} obj = gvar.New(arr) ) fmt.Println(obj.Interfaces()) // Output: // [GoFrame Golang] } ``` ### `Slice` - 说明: `Slice``Interfaces` 的别名。 - 格式: ```go func (v *Var) Slice() []interface{} ``` - 示例: ```go // Slice func ExampleVar_Slice() { var ( arr = []string{"GoFrame", "Golang"} obj = gvar.New(arr) ) fmt.Println(obj.Slice()) // Output: // [GoFrame Golang] } ``` ### `Array` - 说明: `Array``Interfaces` 的别名。 - 格式: ```go func (v *Var) Array() []interface{} ``` - 示例: ```go // Array func ExampleVar_Array() { var ( arr = []string{"GoFrame", "Golang"} obj = gvar.New(arr) ) fmt.Println(obj.Array()) // Output: // [GoFrame Golang] } ``` ### `Vars` - 说明: `Vars`将 `v` 转换为 `[]var`。 - 格式: ```go func (v *Var) Vars() []*Var ``` - 示例: ```go // Vars func ExampleVar_Vars() { var ( arr = []string{"GoFrame", "Golang"} obj = gvar.New(arr) ) fmt.Println(obj.Vars()) // Output: // [GoFrame Golang] } ``` ### `Map` - 说明: `Map`将 `v` 转换为 `map[string]interface{}`。 - 格式: ```go func (v *Var) Map(tags ...string) map[string]interface{} ``` - 示例: ```go // Map func ExampleVar_Map() { var ( m = g.Map{"id": 1, "price": 100.00} v = gvar.New(m) res = v.Map() ) fmt.Println(res["id"], res["price"]) // Output: // 1 100 } ``` ### `MapStrAny` - 说明: `MapStrAny`类似于 `Map` 函数,但是实现了 `MapStrAny` 接口。 - 格式: ```go func (v *Var) MapStrAny() map[string]interface{} ``` - 示例: ```go // MapStrAny func ExampleVar_MapStrAny() { var ( m1 = g.Map{"id": 1, "price": 100} v = gvar.New(m1) v2 = v.MapStrAny() ) fmt.Println(v2["price"], v2["id"]) // Output: // 100 1 } ``` ### `MapStrStr` - 说明: `MapStrStr`将 `v` 转换为 `map[string]string`。 - 格式: ```go func (v *Var) MapStrStr(tags ...string) map[string]string ``` - 示例: ```go // MapStrStr func ExampleVar_MapStrStr() { var ( m1 = g.Map{"id": 1, "price": 100} v = gvar.New(m1) v2 = v.MapStrStr() ) fmt.Println(v2["price"] + "$") // Output: // 100$ } ``` ### `MapStrVar` - 说明: `MapStrVar`将 `v` 转换为 `map[string]*Var`。 - 格式: ```go func (v *Var) MapStrVar(tags ...string) map[string]*Var ``` - 示例: ```go // MapStrVar func ExampleVar_MapStrVar() { var ( m1 = g.Map{"id": 1, "price": 100} v = gvar.New(m1) v2 = v.MapStrVar() ) fmt.Println(v2["price"].Float64() * 100) // Output: // 10000 } ``` ### `MapDeep` - 说明: `MapDeep`将 `v` 递归转换为 `map[string]interface{}`。 - 格式: ```go func (v *Var) MapDeep(tags ...string) map[string]interface{} ``` - 示例: ```go // MapDeep func ExampleVar_MapDeep() { var ( m1 = g.Map{"id": 1, "price": 100} m2 = g.Map{"product": m1} v = gvar.New(m2) v2 = v.MapDeep() ) fmt.Println(v2["product"]) // Output: // map[id:1 price:100] } ``` ### `MapStrStrDeep` - 说明: `MapStrStrDeep`将 `v` 递归转换为 `map[string]string`。 - 格式: ```go func (v *Var) MapStrStrDeep(tags ...string) map[string]string ``` - 示例: ```go // MapStrStrDeep func ExampleVar_MapStrStrDeep() { var ( m1 = g.Map{"id": 1, "price": 100} m2 = g.Map{"product": m1} v = gvar.New(m2) v2 = v.MapStrStrDeep() ) fmt.Println(v2["product"]) // Output: // {"id":1,"price":100} } ``` ### `MapStrVarDeep` - 说明: `MapStrVarDeep`将 `v` 递归转换为 `map[string]*Var`。 - 格式: ```go func (v *Var) MapStrVarDeep(tags ...string) map[string]*Var ``` - 示例: ```go // MapStrVarDeep func ExampleVar_MapStrVarDeep() { var ( m1 = g.Map{"id": 1, "price": 100} m2 = g.Map{"product": m1} v = gvar.New(m2) v2 = v.MapStrVarDeep() ) fmt.Println(v2["product"]) // Output: // {"id":1,"price":100} } ``` ### `Maps` - 说明: `Maps`将 `v` 转换为 `map[string]interface{}`。 - 格式: ```go func (v *Var) Maps(tags ...string) []map[string]interface{} ``` - 示例: ```go // Maps func ExampleVar_Maps() { var m = gvar.New(g.ListIntInt{g.MapIntInt{0: 100, 1: 200}, g.MapIntInt{0: 300, 1: 400}}) fmt.Printf("%#v", m.Maps()) // Output: // []map[string]interface {}{map[string]interface {}{"0":100, "1":200}, map[string]interface {}{"0":300, "1":400}} } ``` ### `MapsDeep` - 说明: `MapsDeep`将 `v` 递归转换为 `[]map[string]interface{}`。 - 格式: ```go func (v *Var) MapsDeep(tags ...string) []map[string]interface{} ``` - 示例: ```go // MapsDeep func ExampleVar_MapsDeep() { var ( p1 = g.MapStrAny{"product": g.Map{"id": 1, "price": 100}} p2 = g.MapStrAny{"product": g.Map{"id": 2, "price": 200}} v = gvar.New(g.ListStrAny{p1, p2}) v2 = v.MapsDeep() ) fmt.Printf("%#v", v2) // Output: // []map[string]interface {}{map[string]interface {}{"product":map[string]interface {}{"id":1, "price":100}}, map[string]interface {}{"product":map[string]interface {}{"id":2, "price":200}}} } ``` ### `MapToMap` - 说明: `MapToMap`将 `v` 转换为 `pointer` 指定的 `map` 类型,`mapping` 为指定的映射规则。 - 格式: ```go func (v *Var) MapToMap(pointer interface{}, mapping ...map[string]string) (err error) ``` - 示例: ```go // MapToMap func ExampleVar_MapToMap() { var ( m1 = gvar.New(g.MapIntInt{0: 100, 1: 200}) m2 = g.MapStrStr{} ) err := m1.MapToMap(&m2) if err != nil { panic(err) } fmt.Printf("%#v", m2) // Output: // map[string]string{"0":"100", "1":"200"} } ``` ### `MapToMaps` - 说明: `MapToMaps`将 `v` 转换为 `pointer`指定的 `map` 类型,`mapping` 为指定的映射规则。 - 格式: ```go func (v *Var) MapToMaps(pointer interface{}, mapping ...map[string]string) (err error) ``` - 示例: ```go // MapToMaps func ExampleVar_MapToMaps() { var ( p1 = g.MapStrAny{"product": g.Map{"id": 1, "price": 100}} p2 = g.MapStrAny{"product": g.Map{"id": 2, "price": 200}} v = gvar.New(g.ListStrAny{p1, p2}) v2 []g.MapStrStr ) err := v.MapToMaps(&v2) if err != nil { panic(err) } fmt.Printf("%#v", v2) // Output: // []map[string]string{map[string]string{"product":"{\"id\":1,\"price\":100}"}, map[string]string{"product":"{\"id\":2,\"price\":200}"}} } ``` ### `MapToMapsDeep` - 说明: `MapToMaps`将 `v` 递归地转换为`pointer`指定的 `map` 类型,`mapping` 为指定的映射规则。 - 格式: ```go func (v *Var) MapToMapsDeep(pointer interface{}, mapping ...map[string]string) (err error) ``` - 示例: ```go // MapToMapDeep func ExampleVar_MapToMapDeep() { var ( p1 = gvar.New(g.MapStrAny{"product": g.Map{"id": 1, "price": 100}}) p2 = g.MapStrAny{} ) err := p1.MapToMap(&p2) if err != nil { panic(err) } fmt.Printf("%#v", p2) // Output: // map[string]interface {}{"product":map[string]interface {}{"id":1, "price":100}} } ``` ### `Scan` - 说明: `Scan`会自动检查 `pointer` 的类型,并将 `params` 转换为 `pointer`,`pointer` 支持的类型为: `*map、 *[]map、 *[]*map、 *struct、 **struct、 *[]struct、 *[]*struct` - 格式: ```go func (v *Var) Scan(pointer interface{}, mapping ...map[string]string) error ``` - 示例: ```go // Scan func ExampleVar_Scan() { type Student struct { Id *g.Var Name *g.Var Scores *g.Var } var ( s Student m = g.Map{ "Id": 1, "Name": "john", "Scores": []int{100, 99, 98}, } ) if err := gconv.Scan(m, &s); err != nil { panic(err) } g.DumpWithType(s) // Output: // gvar_test.Student(3) { // Id: *gvar.Var(1) "1", // Name: *gvar.Var(4) "john", // Scores: *gvar.Var(11) "[100,99,98]", // } } ``` # 泛型类型-注意事项 ## 注意事项 虽然框架提供的泛型类型极大提高的开发的简便性,但对于业务模型来说应当慎重使用(不能滥用),因为泛型类型将会掩盖真实的数据类型,这对于业务项目长期维护来说弊大于利,特别是复杂的业务项目。业务模型的数据类型定义应当尽可能地明确、有意义、不可变,才有利于编译型语言在编译阶段做类型检查和优化、有利于业务后续长期维护。 举个例子,以下是社区热心小伙伴提供的真实业务模型案例: ```go type MiDispatchData struct { Status *g.Var `json:"status"` BrandId *g.Var `json:"brand_id"` AreaId *g.Var `json:"area_id"` Year *g.Var `json:"year"` Month *g.Var `json:"month"` Day *g.Var `json:"day"` Hour *g.Var `json:"hour"` RequestTime *g.Var `json:"request_time"` Source *g.Var `json:"source"` BikeId *g.Var `json:"bike_id"` BikeType *g.Var `json:"bike_type"` Lon *g.Var `json:"lon"` Lat *g.Var `json:"lat"` SiteId *g.Var `json:"site_id"` BikeMac *g.Var `json:"bike_mac"` } ``` 虽然这样的做法程序也能正常运行,业务场景也能正常覆盖。但却丢失了编译型语言的编译器优势(这就类似于PHP变量了),在后期项目维护时很难再确定字段的数据类型。 ## 使用建议 - 泛型在基础组件、中间件项目中使用较多。 - 如果字段在业务场景中有多重含义、类型,那么可以使用泛型代替如 `interface{}` 类型。 # 安全类型-gtype ## 基本介绍 并发安全基本类型。 **使用场景**: `gtype` 使用得非常频繁,任何需要并发安全的场景下都适用。 在普通的并发安全场景中,一个基本类型的变量,特别是一个 `struct` 含有若干的属性,往往使用互斥(读写)锁或者多把(读写)锁来进行安全管理。 但这样的使用中, `变量/struct/属性` 的操作性能 **十分低下**,且由于互斥锁机制的存在往往使得操作变得相当复杂,必须小心翼翼地维护好 `变量/属性` 的并发安全控制(特别是 `(RW)Mutex`)。 `gtype` 针对于最常用的基本数据类型,提供了对应的并发安全数据类型,便于在并发安全场景下更好地维护变量/属性,开发者无需在 `struct` 中再创建和维护繁琐的 `(RW)Mutex`。由于 `gtype` 维护的是基本类型的并发安全,因此内部基本都使用了 `atomic` 原子操作来维护并发安全性,因此效率往往会比 `(RW)Mutex` 互斥锁高出数十倍。 **使用方式**: ```go import "github.com/gogf/gf/v2/container/gtype" ``` **接口文档**: [https://pkg.go.dev/github.com/gogf/gf/v2/container/gtype](https://pkg.go.dev/github.com/gogf/gf/v2/container/gtype) # 安全类型-基本使用 `gtype` 并发安全基本类型的使用非常简单,往往就类似以下几个方法(以 `gtype.Int` 类型举例): ```go func NewInt(value ...int) *Int func (v *Int) Add(delta int) (new int) func (v *Int) Cas(old, new int) bool func (v *Int) Clone() *Int func (v *Int) Set(value int) (old int) func (v *Int) String() string func (v *Int) Val() int ``` ## 基本使用 ```go package main import ( "github.com/gogf/gf/v2/container/gtype" "fmt" ) func main() { // 创建一个Int型的并发安全基本类型对象 i := gtype.NewInt() // 设置值 fmt.Println(i.Set(10)) // 获取值 fmt.Println(i.Val()) // 数值-1,并返回修改之后的数值 fmt.Println(i.Add(-1)) } ``` 执行后,输出结果为: ```0 10 9 ``` ## `JSON` 序列化/反序列 `gtype` 模块下的所有容器类型均实现了标准库 `json` 数据格式的序列化/反序列化接口。 1、 `Marshal` ```go package main import ( "encoding/json" "fmt" "github.com/gogf/gf/v2/container/gtype" ) func main() { type Student struct { Id *gtype.Int Name *gtype.String Scores *gtype.Interface } s := Student{ Id: gtype.NewInt(1), Name: gtype.NewString("john"), Scores: gtype.NewInterface([]int{100, 99, 98}), } b, _ := json.Marshal(s) fmt.Println(string(b)) } ``` 执行后,输出结果: ``` {"Id":1,"Name":"john","Scores":[100,99,98]} ``` 2、 `Unmarshal` ```go package main import ( "encoding/json" "fmt" "github.com/gogf/gf/v2/container/gtype" ) func main() { b := []byte(`{"Id":1,"Name":"john","Scores":[100,99,98]}`) type Student struct { Id *gtype.Int Name *gtype.String Scores *gtype.Interface } s := Student{} json.Unmarshal(b, &s) fmt.Println(s) } ``` 执行后,输出结果: ``` {1 john [100,99,98]} ``` # 安全类型-性能测试 基准测试结果如下: ``` john@john-B85M:~/Workspace/Go/GOPATH/src/github.com/gogf/gf/v2/container/gtype$ go test -bench=".*" -benchmem goos: linux goarch: amd64 pkg: github.com/gogf/gf/v2/container/gtype BenchmarkInt_Set-4 300000000 5.87 ns/op 0 B/op 0 allocs/op BenchmarkInt_Val-4 2000000000 0.46 ns/op 0 B/op 0 allocs/op BenchmarkInt_Add-4 300000000 5.86 ns/op 0 B/op 0 allocs/op BenchmarkInt32_Set-4 300000000 5.87 ns/op 0 B/op 0 allocs/op BenchmarkInt32_Val-4 2000000000 0.47 ns/op 0 B/op 0 allocs/op BenchmarkInt32_Add-4 300000000 5.85 ns/op 0 B/op 0 allocs/op BenchmarkInt64_Set-4 300000000 5.88 ns/op 0 B/op 0 allocs/op BenchmarkInt64_Val-4 2000000000 0.46 ns/op 0 B/op 0 allocs/op BenchmarkInt64_Add-4 300000000 5.88 ns/op 0 B/op 0 allocs/op BenchmarkUint_Set-4 300000000 5.88 ns/op 0 B/op 0 allocs/op BenchmarkUint_Val-4 2000000000 0.46 ns/op 0 B/op 0 allocs/op BenchmarkUint_Add-4 300000000 5.87 ns/op 0 B/op 0 allocs/op BenchmarkUint32_Set-4 300000000 5.86 ns/op 0 B/op 0 allocs/op BenchmarkUint32_Val-4 2000000000 0.50 ns/op 0 B/op 0 allocs/op BenchmarkUint32_Add-4 200000000 5.86 ns/op 0 B/op 0 allocs/op BenchmarkUint64_Set-4 300000000 5.86 ns/op 0 B/op 0 allocs/op BenchmarkUint64_Val-4 2000000000 0.47 ns/op 0 B/op 0 allocs/op BenchmarkUint64_Add-4 300000000 5.85 ns/op 0 B/op 0 allocs/op BenchmarkBool_Set-4 300000000 5.85 ns/op 0 B/op 0 allocs/op BenchmarkBool_Val-4 2000000000 0.46 ns/op 0 B/op 0 allocs/op BenchmarkString_Set-4 20000000 90.1 ns/op 23 B/op 1 allocs/op BenchmarkString_Val-4 2000000000 1.58 ns/op 0 B/op 0 allocs/op BenchmarkBytes_Set-4 20000000 76.2 ns/op 35 B/op 2 allocs/op BenchmarkBytes_Val-4 2000000000 1.58 ns/op 0 B/op 0 allocs/op BenchmarkInterface_Set-4 50000000 30.7 ns/op 8 B/op 0 allocs/op BenchmarkInterface_Val-4 2000000000 0.74 ns/op 0 B/op 0 allocs/op BenchmarkAtomicValue_Store-4 50000000 27.3 ns/op 8 B/op 0 allocs/op BenchmarkAtomicValue_Load-4 2000000000 0.73 ns/op 0 B/op 0 allocs/op PASS ok github.com/gogf/gf/v2/container/gtype 49.454s ``` # 队列类型-gqueue ## 基本介绍 动态大小的并发安全队列。同时, `gqueue` 也支持固定队列大小,固定队列大小时队列效率和标准库的 `channel` 无异。 **使用场景**: 该队列是并发安全的,常用于多 `goroutine` 数据通信且支持动态队列大小的场景。 **使用方式**: ```go import "github.com/gogf/gf/v2/container/gqueue" ``` **接口文档**: [https://pkg.go.dev/github.com/gogf/gf/v2/container/gqueue](https://pkg.go.dev/github.com/gogf/gf/v2/container/gqueue) **泛型支持**: 从 `v2.10` 版本开始,`gqueue` 提供了泛型队列类型: - `TQueue[T]`:泛型队列,提供类型安全的队列操作 - 支持先进先出的数据结构操作 - 优化了队列长度计算逻辑,修复了测试用例中的循环结构问题 - 推荐在新项目中使用泛型队列 # 队列类型-基本使用 ## 泛型队列(推荐使用) 从 `v2.10` 版本开始,`gqueue` 提供了泛型队列 `TQueue[T]`,提供类型安全的队列操作,支持先进先出的数据结构。 ### 基本使用 ```go package main import ( "fmt" "github.com/gogf/gf/v2/container/gqueue" ) func main() { // 创建一个string类型的泛型队列 q := gqueue.NewTQueue[string]() // 入队 q.Push("apple") q.Push("banana") q.Push("orange") // 获取队列长度 fmt.Println("Length:", q.Len()) // 输出: Length: 3 // 出队 fmt.Println("Pop:", q.Pop()) // 输出: Pop: apple fmt.Println("Pop:", q.Pop()) // 输出: Pop: banana // 获取剩余队列长度 fmt.Println("Length:", q.Len()) // 输出: Length: 1 } ``` ### 生产者-消费者模式 泛型队列特别适合用于生产者-消费者模式的并发场景。 ```go package main import ( "fmt" "time" "github.com/gogf/gf/v2/container/gqueue" "github.com/gogf/gf/v2/os/gtime" "github.com/gogf/gf/v2/os/gtimer" ) func main() { q := gqueue.NewTQueue[string]() // 数据生产者,每隔1秒往队列写数据 gtimer.SetInterval(time.Second, func() { v := gtime.Now().String() q.Push(v) fmt.Println("Push:", v) }) // 3秒后关闭队列 gtimer.SetTimeout(3*time.Second, func() { q.Close() }) // 消费者,不停读取队列数据并输出到终端 for { if v := q.Pop(); v != nil { fmt.Println(" Pop:", *v) } else { break } } } ``` ### 泛型队列特点 1. **类型安全**:在编译期就能发现类型错误,避免运行时类型断言失败。 2. **先进先出**:严格按照FIFO顺序处理数据。 3. **并发安全**:内置并发安全机制,适合多goroutine场景。 4. **动态大小**:支持动态队列大小,也可以设置固定大小。 **提示** 推荐在新项目中使用泛型队列 `TQueue[T]`,它提供了更好的类型安全性和开发体验。 ## 传统队列类型 下面介绍传统的队列类型,这些类型在旧版本中被广泛使用,仍然完全支持。 ### 使用 `Queue.Pop` ```go package main import ( "fmt" "time" "github.com/gogf/gf/v2/os/gtimer" "github.com/gogf/gf/v2/container/gqueue" ) func main() { q := gqueue.New() // 数据生产者,每隔1秒往队列写数据 gtimer.SetInterval(time.Second, func() { v := gtime.Now().String() q.Push(v) fmt.Println("Push:", v) }) // 3秒后关闭队列 gtimer.SetTimeout(3*time.Second, func() { q.Close() }) // 消费者,不停读取队列数据并输出到终端 for { if v := q.Pop(); v != nil { fmt.Println(" Pop:", v) } else { break } } // 第3秒时关闭队列,这时程序立即退出,因此结果中只会打印2秒的数据。 执行后,输出结果为: // Output: // Push: 2021-09-07 14:03:00 // Pop: 2021-09-07 14:03:00 // Push: 2021-09-07 14:03:01 // Pop: 2021-09-07 14:03:01 } ``` ### 使用 `Queue.C` ```go package main import ( "context" "fmt" "time" _ "github.com/gogf/gf/contrib/drivers/mysql/v2" "github.com/gogf/gf/v2/container/gqueue" "github.com/gogf/gf/v2/os/gctx" "github.com/gogf/gf/v2/os/gtime" "github.com/gogf/gf/v2/os/gtimer" ) func main() { queue := gqueue.New() gtimer.AddTimes(gctx.GetInitCtx(), time.Second, 3, func(ctx context.Context) { queue.Push(gtime.Now().String()) }) for { select { case queueItem := <-queue.C: fmt.Println(queueItem) case <-time.After(3 * time.Second): fmt.Println("timeout, exit loop") return } } } ``` ### 元素入队/出队 ```go package main import ( "fmt" "time" "github.com/gogf/gf/v2/os/gtimer" "github.com/gogf/gf/v2/container/gqueue" ) func main() { q := gqueue.New() for i := 0; i < 10; i++ { q.Push(i) } fmt.Println(q.Pop()) fmt.Println(q.Pop()) fmt.Println(q.Pop()) // Output: // 0 // 1 // 2 } ``` ### 队列长度 ```go package main import ( "fmt" "time" "github.com/gogf/gf/v2/os/gtimer" "github.com/gogf/gf/v2/container/gqueue" ) func main() { q := gqueue.New() q.Push(1) q.Push(2) fmt.Println(q.Len()) // size是len方法的别称 fmt.Println(q.Size()) // May Output: // 2 // 2 } ``` ### 队列关闭 ```go package main import ( "fmt" "time" "github.com/gogf/gf/v2/os/gtimer" "github.com/gogf/gf/v2/container/gqueue" ) func main() { q := gqueue.New() for i := 0; i < 10; i++ { q.Push(i) } fmt.Println(q.Pop()) q.Close() fmt.Println(q.Pop()) fmt.Println(q.Len()) // Output: // 0 // // 0 } ``` ## `gqueue` 与 `glist` `gqueue` 的底层基于 `glist` 链表实现动态大小特性,在队列满写入数据或在队列空时读取数据会产生阻塞。 `glist` 是一个并发安全的链表,并可以允许在关闭并发安全特性的时和一个普通的 `list` 链表无异,在存储和读取数据时不会发生阻塞。 # 队列类型-性能测试 [https://github.com/gogf/gf/blob/master/container/gqueue/gqueue\_z\_bench\_test.go](https://github.com/gogf/gf/blob/master/container/gqueue/gqueue_z_bench_test.go) `gqueue` 与标准库 `channel` 的性能基准测试,其中每一次基准测试的 `b.N` 值均为 `20000000`,以保证动态队列存取一致防止 `deadlock`: ``` goos: linux goarch: amd64 pkg: github.com/gogf/gf/v2/container/gqueue Benchmark_Gqueue_StaticPushAndPop-4 20000000 84.2 ns/op Benchmark_Gqueue_DynamicPush-4 20000000 164 ns/op Benchmark_Gqueue_DynamicPop-4 20000000 121 ns/op Benchmark_Channel_PushAndPop-4 20000000 70.0 ns/op PASS ``` 可以看到标准库的 `channel` 的读写性能是非常高的,但是创建的时候由于需要初始化内存,因此创建 `channel` 的时候效率非常非常低(初始化即分配内存),并且受到队列大小的限制,写入的数据不能超过指定的队列大小。 `gqueue` 使用起来比 `channel` 更加灵活,不仅创建效率高(动态分配内存),不受队列大小限制(也可限定大小)。 # 树形类型-gtree ## 基本介绍 支持并发安全开关特性的树形容器,树形数据结构的特点是支持有序遍历、内存占用低、复杂度稳定、适合大数据量存储。该模块包含多个数据结构的树形容器: `RedBlackTree`、 `AVLTree` 和 `BTree`。 | 类型 | 数据结构 | 平均复杂度 | 支持排序 | 有序遍历 | 说明 | | --- | --- | --- | --- | --- | --- | | `RedBlackTree` | 红黑树 | `O(log N)` | 是 | 是 | 写入性能比较好 | | `AVLTree` | 高度平衡树 | `O(log N)` | 是 | 是 | 查找性能比较好 | | `BTree` | B树/B-树 | `O(log N)` | 是 | 是 | 常用于外部存储 | > 参考连接: [https://en.wikipedia.org/wiki/Binary\_tree](https://en.wikipedia.org/wiki/Binary_tree) **使用场景**: 关联数组场景、排序键值对场景、大数据量内存CRUD场景等等。 **使用方式**: ```go import "github.com/gogf/gf/v2/container/gtree" ``` **接口文档**: [https://pkg.go.dev/github.com/gogf/gf/v2/container/gtree](https://pkg.go.dev/github.com/gogf/gf/v2/container/gtree) 几种容器的API方法都非常类似,特点是需要在初始化时提供用于排序的方法。 在 `gutil` 模块中提供了常用的一些基本类型比较方法,可以直接在程序中直接使用,后续也有示例。 ```go func ComparatorByte(a, b interface{}) int func ComparatorFloat32(a, b interface{}) int func ComparatorFloat64(a, b interface{}) int func ComparatorInt(a, b interface{}) int func ComparatorInt16(a, b interface{}) int func ComparatorInt32(a, b interface{}) int func ComparatorInt64(a, b interface{}) int func ComparatorInt8(a, b interface{}) int func ComparatorRune(a, b interface{}) int func ComparatorString(a, b interface{}) int func ComparatorTime(a, b interface{}) int func ComparatorUint(a, b interface{}) int func ComparatorUint16(a, b interface{}) int func ComparatorUint32(a, b interface{}) int func ComparatorUint64(a, b interface{}) int func ComparatorUint8(a, b interface{}) int ``` ## 泛型支持 **提示** 版本要求:`v2.10.0` 从 `v2.10.0` 版本开始,`gtree` 提供了泛型版本的树形容器,包括 `RedBlackKVTree[K, V]`、`AVLKVTree[K, V]` 和 `BKVTree[K, V]`,提供类型安全的键值对操作。 ### 基本使用 使用泛型版本创建树形容器: ```go import ( "github.com/gogf/gf/v2/container/gtree" "github.com/gogf/gf/v2/util/gutil" ) // 创建泛型红黑树 tree := gtree.NewRedBlackKVTree[int, string](gutil.ComparatorInt) tree.Set(1, "value1") tree.Set(2, "value2") // 无需类型断言,直接获取类型安全的值 value, found := tree.Get(1) // value 是 string 类型 if found { fmt.Println(value) // 直接使用,无需类型转换 } ``` ### 类型安全的优势 泛型版本提供编译时类型检查: ```go // 传统方式(需要类型断言) tree := gtree.NewRedBlackTree(gutil.ComparatorInt) tree.Set(1, "value") value := tree.Get(1).(string) // 需要类型断言,运行时可能panic // 泛型方式(类型安全) tree := gtree.NewRedBlackKVTree[int, string](gutil.ComparatorInt) tree.Set(1, "value") value, _ := tree.Get(1) // value 直接是 string 类型,编译时检查 ``` ### NilChecker 支持 **提示** `NilChecker` 是一个可选参数,并非必须提供。它主要用于解决`typed nil`问题,并提供更好的`nil`判断性能。在默认情况下,组件使用**反射**来判断数据是否为`nil`。 对于指针、接口等类型,可以使用 `WithChecker` 方法自定义`nil`检查逻辑: ```go type Student struct { ID int Name string } // 使用自定义 nil 检查器 tree := gtree.NewRedBlackKVTreeWithChecker[int, *Student]( gutil.ComparatorInt, func(s *Student) bool { return s == nil || s.ID == 0 // 自定义 nil 判断 }, ) tree.Set(1, &Student{ID: 1, Name: "张三"}) tree.Set(2, &Student{ID: 0, Name: ""}) // 会被视为 nil value, found := tree.Get(2) fmt.Println(found) // false,因为被视为 nil ``` ### 泛型树类型 三种泛型树容器的使用方式类似: ```go // 红黑树(写入性能好) rbTree := gtree.NewRedBlackKVTree[int, string](gutil.ComparatorInt) // AVL树(查找性能好) avlTree := gtree.NewAVLKVTree[int, string](gutil.ComparatorInt) // B树(适合外部存储) bTree := gtree.NewBKVTree[int, string](3, gutil.ComparatorInt) // 3 是阶数 ``` # 树形类型-基本使用 ## 基本使用 ```go package main import ( "fmt" "github.com/gogf/gf/v2/container/gtree" "github.com/gogf/gf/v2/util/gutil" ) func main() { m := gtree.NewRedBlackTree(gutil.ComparatorInt) // 设置键值对 for i := 0; i < 10; i++ { m.Set(i, i*10) } // 查询大小 fmt.Println(m.Size()) // 批量设置键值对(不同的数据类型对象参数不同) m.Sets(map[interface{}]interface{}{ 10: 10, 11: 11, }) fmt.Println(m.Size()) // 查询是否存在 fmt.Println(m.Contains(1)) // 查询键值 fmt.Println(m.Get(1)) // 删除数据项 m.Remove(9) fmt.Println(m.Size()) // 批量删除 m.Removes([]interface{}{10, 11}) fmt.Println(m.Size()) // 当前键名列表(随机排序) fmt.Println(m.Keys()) // 当前键值列表(随机排序) fmt.Println(m.Values()) // 查询键名,当键值不存在时,写入给定的默认值 fmt.Println(m.GetOrSet(100, 100)) // 删除键值对,并返回对应的键值 fmt.Println(m.Remove(100)) // 遍历map m.IteratorAsc(func(k interface{}, v interface{}) bool { fmt.Printf("%v:%v ", k, v) return true }) fmt.Println() // 清空map m.Clear() // 判断map是否为空 fmt.Println(m.IsEmpty()) } ``` 执行后,输出结果为: ```text 10 12 true 10 11 9 [0 1 2 3 4 5 6 7 8] [0 10 20 30 40 50 60 70 80] 100 100 0:0 1:10 2:20 3:30 4:40 5:50 6:60 7:70 8:80 true ``` ## 前序/后续遍历 ```go package main import ( "fmt" "github.com/gogf/gf/v2/container/gtree" "github.com/gogf/gf/v2/util/gutil" ) func main() { tree := gtree.NewAVLTree(gutil.ComparatorInt) for i := 0; i < 10; i++ { tree.Set(i, i*10) } // 打印树形 tree.Print() // 前序遍历 fmt.Println("ASC:") tree.IteratorAsc(func(key, value interface{}) bool { fmt.Println(key, value) return true }) // 后续遍历 fmt.Println("DESC:") tree.IteratorDesc(func(key, value interface{}) bool { fmt.Println(key, value) return true }) } ``` 执行后,输出结果为: ```text AVLTree │ ┌── 9 │ ┌── 8 │ ┌── 7 │ │ │ ┌── 6 │ │ └── 5 │ │ └── 4 └── 3 │ ┌── 2 └── 1 └── 0 ASC: 0 0 1 10 2 20 3 30 4 40 5 50 6 60 7 70 8 80 9 90 DESC: 9 90 8 80 7 70 6 60 5 50 4 40 3 30 2 20 1 10 0 0 ``` ## 泛型使用示例 **提示** 版本要求:`v2.10.0` 从 `v2.10.0` 版本开始,`gtree` 提供了泛型版本,无需类型断言,更加类型安全。 ### 泛型红黑树 ```go package main import ( "fmt" "github.com/gogf/gf/v2/container/gtree" "github.com/gogf/gf/v2/util/gutil" ) func main() { // 创建泛型红黑树 tree := gtree.NewRedBlackKVTree[int, int](gutil.ComparatorInt) // 设置键值对 for i := 0; i < 10; i++ { tree.Set(i, i*10) } // 查询大小 fmt.Println(tree.Size()) // 批量设置键值对 tree.SetMap(map[int]int{ 10: 100, 11: 110, }) fmt.Println(tree.Size()) // 查询是否存在 fmt.Println(tree.Contains(1)) // 查询键值(无需类型断言) value, found := tree.Get(1) if found { fmt.Println(value) // 直接使用,value 是 int 类型 } // 删除数据项 tree.Remove(9) fmt.Println(tree.Size()) // 当前键名列表 fmt.Println(tree.Keys()) // 当前键值列表 fmt.Println(tree.Values()) // 查询键名,当键值不存在时,写入给定的默认值 fmt.Println(tree.GetOrSet(100, 1000)) // 遍历树(升序) tree.IteratorAsc(func(key int, value int) bool { fmt.Printf("%v:%v ", key, value) return true }) fmt.Println() // 清空树 tree.Clear() // 判断树是否为空 fmt.Println(tree.IsEmpty()) } ``` ### 泛型 AVL 树 ```go package main import ( "fmt" "github.com/gogf/gf/v2/container/gtree" "github.com/gogf/gf/v2/util/gutil" ) func main() { // 创建泛型 AVL 树 tree := gtree.NewAVLKVTree[string, int](gutil.ComparatorString) // 添加数据 tree.Set("apple", 1) tree.Set("banana", 2) tree.Set("cherry", 3) tree.Set("date", 4) // 获取值(类型安全) if value, found := tree.Get("banana"); found { fmt.Printf("banana的值: %d\n", value) } // 按序遍历 tree.IteratorAsc(func(key string, value int) bool { fmt.Printf("%s: %d\n", key, value) return true }) // 获取最小和最大键 if left := tree.Left(); left != nil { fmt.Printf("最小键: %s, 值: %d\n", left.Key, left.Value) } if right := tree.Right(); right != nil { fmt.Printf("最大键: %s, 值: %d\n", right.Key, right.Value) } } ``` ### 自定义类型示例 ```go package main import ( "fmt" "github.com/gogf/gf/v2/container/gtree" ) type User struct { ID int Name string Age int } func main() { // 创建以 int 为键,*User 为值的红黑树 // NilChecker 是可选的,用于解决 typed nil 问题和提升性能 // 不使用 NilChecker 时,组件会使用反射判断 nil tree := gtree.NewRedBlackKVTreeWithChecker[int, *User]( func(a, b int) int { if a < b { return -1 } else if a > b { return 1 } return 0 }, func(user *User) bool { // 自定义 nil 检查:user 为 nil 或 ID 为 0 视为 nil return user == nil || user.ID == 0 }, ) // 添加用户 tree.Set(1, &User{ID: 1, Name: "张三", Age: 20}) tree.Set(2, &User{ID: 2, Name: "李四", Age: 25}) tree.Set(3, &User{ID: 3, Name: "王五", Age: 30}) // 查询用户(无需类型断言) if user, found := tree.Get(2); found { fmt.Printf("用户ID: %d, 姓名: %s, 年龄: %d\n", user.ID, user.Name, user.Age) } // 遍历所有用户 tree.IteratorAsc(func(id int, user *User) bool { fmt.Printf("ID: %d, 姓名: %s, 年龄: %d\n", id, user.Name, user.Age) return true }) // 测试 nil checker tree.Set(4, &User{ID: 0, Name: "无效用户", Age: 0}) _, found := tree.Get(4) fmt.Printf("ID=0 的用户是否存在: %v\n", found) // false,因为被视为 nil } ``` # 树形类型-方法介绍 **提示** 以下常用方法列表,文档更新可能滞后于代码新特性,更多的方法及示例请参考代码文档: [https://pkg.go.dev/github.com/gogf/gf/v2/container/gtree](https://pkg.go.dev/github.com/gogf/gf/v2/container/gtree) ## `NewBTree` - 说明: `NewBTree` 使用 `m`(最大子节点数量)和自定义的比较方法创建 `BTree`。参数 `safe` 用于指定是否使用并发安全的 `tree`,默认情况下为 `false`。 - 注意:参数 `m` 必须大于等于 `3`,否则会 `panic`。 - 格式: ```go NewBTree(m int, comparator func(v1, v2 interface{}) int, safe ...bool) *BTree ``` - 示例: ```go func ExampleNewBTree() { bTree := gtree.NewBTree(3, gutil.ComparatorString) for i := 0; i < 6; i++ { bTree.Set("key"+gconv.String(i), "val"+gconv.String(i)) } fmt.Println(bTree.Map()) // Output: // map[key0:val0 key1:val1 key2:val2 key3:val3 key4:val4 key5:val5] } ``` ## `NewBTreeFrom` - 说明: `NewBTreeFrom` 使用 `m`(最大子节点数量),自定义的比较方法和类型为 `map[interface{}]interface{}` 的 `data` 创建 `BTree`。参数 `safe` 用于指定是否使用并发安全的 `tree`,默认情况下为 `false`。 - 注意:参数 `m` 必须大于等于 `3`,否则会 `panic`。 - 格式: ```go NewBTreeFrom(m int, comparator func(v1, v2 interface{}) int, data map[interface{}]interface{}, safe ...bool) *BTree ``` - 示例: ```go func ExampleNewBTreeFrom() { bTree := gtree.NewBTree(3, gutil.ComparatorString) for i := 0; i < 6; i++ { bTree.Set("key"+gconv.String(i), "val"+gconv.String(i)) } otherBTree := gtree.NewBTreeFrom(3, gutil.ComparatorString, bTree.Map()) fmt.Println(otherBTree.Map()) // Output: // map[key0:val0 key1:val1 key2:val2 key3:val3 key4:val4 key5:val5] } ``` ## `Clone` - 说明: `clone` 返回一个值为当前 `tree` 值的副本的新 `BTree`。 - 格式: ```go Clone() *BTree ``` - 示例: ```go func ExampleBTree_Clone() { b := gtree.NewBTree(3, gutil.ComparatorString) for i := 0; i < 6; i++ { b.Set("key"+gconv.String(i), "val"+gconv.String(i)) } tree := b.Clone() fmt.Println(tree.Map()) fmt.Println(tree.Size()) // Output: // map[key0:val0 key1:val1 key2:val2 key3:val3 key4:val4 key5:val5] // 6 } ``` ## `Set` - 说明: `Set` 为 `tree` 设置 `key/value`。 - 格式: ```go Set(key interface{}, value interface{}) ``` - 示例: ```go func ExampleBTree_Set() { tree := gtree.NewBTree(3, gutil.ComparatorString) for i := 0; i < 6; i++ { tree.Set("key"+gconv.String(i), "val"+gconv.String(i)) } fmt.Println(tree.Map()) fmt.Println(tree.Size()) // Output: // map[key0:val0 key1:val1 key2:val2 key3:val3 key4:val4 key5:val5] // 6 } ``` ## `Sets` - 说明: `Sets` 为 `tree` 批量设置 `key/value`。 - 格式: ```go Sets(data map[interface{}]interface{}) ``` - 示例: ```go func ExampleBTree_Sets() { tree := gtree.NewBTree(3, gutil.ComparatorString) tree.Sets(map[interface{}]interface{}{ "key1": "val1", "key2": "val2", }) fmt.Println(tree.Map()) fmt.Println(tree.Size()) // Output: // map[key1:val1 key2:val2] // 2 } ``` ## `Get` - 说明: `Get` 返回参数 `key` 对应的值 `value`,如 `key` 不存在,则返回 `Nil`。 - 格式: ```go Get(key interface{}) (value interface{}) ``` - 示例: ```go func ExampleBTree_Get() { tree := gtree.NewBTree(3, gutil.ComparatorString) for i := 0; i < 6; i++ { tree.Set("key"+gconv.String(i), "val"+gconv.String(i)) } fmt.Println(tree.Get("key1")) fmt.Println(tree.Get("key10")) // Output: // val1 // } ``` ## `GetOrSet` - 说明: `GetOrSet` 如 `key` 存在,则返回 `value`,如 `key` 不存在,使用 `key` 和 `value` 设置键值,然后返回该值。 - 格式: ```go GetOrSet(key interface{}, value interface{}) interface{} ``` - 示例: ```go func ExampleBTree_GetOrSet() { tree := gtree.NewBTree(3, gutil.ComparatorString) for i := 0; i < 6; i++ { tree.Set("key"+gconv.String(i), "val"+gconv.String(i)) } fmt.Println(tree.GetOrSet("key1", "newVal1")) fmt.Println(tree.GetOrSet("key6", "val6")) // Output: // val1 // val6 } ``` ## `GetOrSetFunc` - 说明: `GetOrSetFunc` 如 `key` 存在,则返回 `value`,如 `key` 不存在,使用 `key` 和 `func f` 的返回值设置键值,然后返回该值。 - 格式: ```go GetOrSetFunc(key interface{}, f func() interface{}) interface{} ``` - 示例: ```go func ExampleBTree_GetOrSetFunc() { tree := gtree.NewBTree(3, gutil.ComparatorString) for i := 0; i < 6; i++ { tree.Set("key"+gconv.String(i), "val"+gconv.String(i)) } fmt.Println(tree.GetOrSetFunc("key1", func() interface{} { return "newVal1" })) fmt.Println(tree.GetOrSetFunc("key6", func() interface{} { return "val6" })) // Output: // val1 // val6 } ``` ## `GetOrSetFuncLock` - 说明: `GetOrSetFunc` 如 `key` 存在,则返回 `value`,如 `key` 不存在,使用 `key` 和 `func f` 的返回值设置键值,然后返回该值。 - 注意: `GetOrSetFuncLock` 与 `GetOrSetFunc` 函数的不同之处在于它在写锁中执行函数 `f`。 - 格式: ```go GetOrSetFuncLock(key interface{}, f func() interface{}) interface{} ``` - 示例: ```go func ExampleBTree_GetOrSetFuncLock() { tree := gtree.NewBTree(3, gutil.ComparatorString) for i := 0; i < 6; i++ { tree.Set("key"+gconv.String(i), "val"+gconv.String(i)) } fmt.Println(tree.GetOrSetFuncLock("key1", func() interface{} { return "newVal1" })) fmt.Println(tree.GetOrSetFuncLock("key6", func() interface{} { return "val6" })) // Output: // val1 // val6 } ``` ## `GetVar` - 说明: `GetVar` 根据键名 `key` 查询并返回对应的键值,类型为 `*gvar.Var`。 - 注意:返回的 `gvar.Var` 是非并发安全的。 - 格式: ```go GetVar(key interface{}) *gvar.Var ``` - 示例: ```go func ExampleBTree_GetVar() { tree := gtree.NewBTree(3, gutil.ComparatorString) for i := 0; i < 6; i++ { tree.Set("key"+gconv.String(i), "val"+gconv.String(i)) } fmt.Println(tree.GetVar("key1").String()) // Output: // val1 } ``` ## `GetVarOrSet` - 说明: `GetVarOrSet` 使用 `GetOrSet` 的结果返回,类型为 `*gvar.Var`。 - 注意:返回的 `gvar.Var` 是非并发安全的。 - 格式: ```go GetVarOrSet(key interface{}, value interface{}) *gvar.Var ``` - 示例: ```go func ExampleBTree_GetVarOrSet() { tree := gtree.NewBTree(3, gutil.ComparatorString) for i := 0; i < 6; i++ { tree.Set("key"+gconv.String(i), "val"+gconv.String(i)) } fmt.Println(tree.GetVarOrSet("key1", "newVal1")) fmt.Println(tree.GetVarOrSet("key6", "val6")) // Output: // val1 // val6 } ``` ## `GetVarOrSetFunc` - 说明: `GetVarOrSetFunc` 使用 `GetOrSetFunc` 的结果返回,类型为 `*gvar.Var`。 - 注意:返回的 `gvar.Var` 是非并发安全的。 - 格式: ```go GetVarOrSetFunc(key interface{}, f func() interface{}) *gvar.Var ``` - 示例: ```go func ExampleBTree_GetVarOrSetFunc() { tree := gtree.NewBTree(3, gutil.ComparatorString) for i := 0; i < 6; i++ { tree.Set("key"+gconv.String(i), "val"+gconv.String(i)) } fmt.Println(tree.GetVarOrSetFunc("key1", func() interface{} { return "newVal1" })) fmt.Println(tree.GetVarOrSetFunc("key6", func() interface{} { return "val6" })) // Output: // val1 // val6 } ``` ## `GetVarOrSetFuncLock` - 说明: `GetVarOrSetFuncLock` 使用 `GetOrSetFuncLock` 的结果返回,类型为 `*gvar.Var`。 - 注意:返回的 `gvar.Var` 是非并发安全的。 - 格式: ```go GetVarOrSetFuncLock(key interface{}, f func() interface{}) *gvar.Var ``` - 示例: ```go func ExampleBTree_GetVarOrSetFuncLock() { tree := gtree.NewBTree(3, gutil.ComparatorString) for i := 0; i < 6; i++ { tree.Set("key"+gconv.String(i), "val"+gconv.String(i)) } fmt.Println(tree.GetVarOrSetFuncLock("key1", func() interface{} { return "newVal1" })) fmt.Println(tree.GetVarOrSetFuncLock("key6", func() interface{} { return "val6" })) // Output: // val1 // val6 } ``` ## `SetIfNotExist` - 说明:如果 `key` 不存在,则 `SetIfNotExist` 为 `map` 设置值键值对 `key/value`,并且返回 `true`。如果 `key` 存在,则返回 `false`,而 `value` 将被忽略。 - 格式: ```go SetIfNotExist(key interface{}, value interface{}) bool ``` - 示例: ```go func ExampleBTree_SetIfNotExist() { tree := gtree.NewBTree(3, gutil.ComparatorString) for i := 0; i < 6; i++ { tree.Set("key"+gconv.String(i), "val"+gconv.String(i)) } fmt.Println(tree.SetIfNotExist("key1", "newVal1")) fmt.Println(tree.SetIfNotExist("key6", "val6")) // Output: // false // true } ``` ## `SetIfNotExistFunc` - 说明:如果 `key` 不存在,则 `SetIfNotExistFunc` 设置值为函数 `f` 的返回值,并且返回 `true`。如果 `key` 存在,则返回 `false`,并且 `value` 将被忽略。 - 格式: ```go SetIfNotExistFunc(key interface{}, f func() interface{}) bool ``` - 示例: ```go func ExampleBTree_SetIfNotExistFunc() { tree := gtree.NewBTree(3, gutil.ComparatorString) for i := 0; i < 6; i++ { tree.Set("key"+gconv.String(i), "val"+gconv.String(i)) } fmt.Println(tree.SetIfNotExistFunc("key1", func() interface{} { return "newVal1" })) fmt.Println(tree.SetIfNotExistFunc("key6", func() interface{} { return "val6" })) // Output: // false // true } ``` ## `SetIfNotExistFuncLock` - 说明:如果 `key` 不存在,则 `SetIfNotExistFunc` 设置值为 `func c` 的返回值,然后返回 `true`。如果 `key` 存在,则返回 `false`,而 `value` 将被忽略。 - `SetIfNotExistFuncLock` 与 `SetIfNotExistFunc` 函数的不同之处在于它在 `mutex.Lock` 中执行函数 `f`。 - 格式: ```go SetIfNotExistFuncLock(key interface{}, f func() interface{}) bool ``` - 示例: ```go func ExampleBTree_SetIfNotExistFuncLock() { tree := gtree.NewBTree(3, gutil.ComparatorString) for i := 0; i < 6; i++ { tree.Set("key"+gconv.String(i), "val"+gconv.String(i)) } fmt.Println(tree.SetIfNotExistFuncLock("key1", func() interface{} { return "newVal1" })) fmt.Println(tree.SetIfNotExistFuncLock("key6", func() interface{} { return "val6" })) // Output: // false // true } ``` ## `Contains` - 说明: `Contains` 检查 `key` 在 `tree` 中是否存在。如果 `key` 存在,则返回 `true`,否则返回 `false`。 - 格式: ```go Contains(key interface{}) bool ``` - 示例: ```go func ExampleBTree_Contains() { tree := gtree.NewBTree(3, gutil.ComparatorString) for i := 0; i < 6; i++ { tree.Set("key"+gconv.String(i), "val"+gconv.String(i)) } fmt.Println(tree.Contains("key1")) fmt.Println(tree.Contains("key6")) // Output: // true // false } ``` ## `Remove` - 说明:按给定的 `key` 从 `tree` 中删除 `value`,并返回此删除的 `value`。 - 格式: ```go Remove(key interface{}) (value interface{}) ``` - 示例: ```go func ExampleBTree_Remove() { tree := gtree.NewBTree(3, gutil.ComparatorString) for i := 0; i < 6; i++ { tree.Set("key"+gconv.String(i), "val"+gconv.String(i)) } fmt.Println(tree.Remove("key1")) fmt.Println(tree.Remove("key6")) fmt.Println(tree.Map()) // Output: // val1 // // map[key0:val0 key2:val2 key3:val3 key4:val4 key5:val5] } ``` ## `Removes` - 说明: `Removes` 按给定的 `key` 批量删除 `tree` 的 `value`。 - 格式: ```go Removes(keys []interface{}) ``` - 示例: ```go func ExampleBTree_Removes() { tree := gtree.NewBTree(3, gutil.ComparatorString) for i := 0; i < 6; i++ { tree.Set("key"+gconv.String(i), "val"+gconv.String(i)) } removeKeys := make([]interface{}, 2) removeKeys = append(removeKeys, "key1") removeKeys = append(removeKeys, "key6") tree.Removes(removeKeys) fmt.Println(tree.Map()) // Output: // map[key0:val0 key2:val2 key3:val3 key4:val4 key5:val5] } ``` ## `IsEmpty` - 说明: `IsEmpty` 检查 `tree` 是否为空。如果 `tree` 为空,则返回 `true`,否则返回 `false`。 - 格式: ```go IsEmpty() bool ``` - 示例: ```go func ExampleBTree_IsEmpty() { tree := gtree.NewBTree(3, gutil.ComparatorString) fmt.Println(tree.IsEmpty()) for i := 0; i < 6; i++ { tree.Set("key"+gconv.String(i), "val"+gconv.String(i)) } fmt.Println(tree.IsEmpty()) // Output: // true // false } ``` ## `Size` - 说明: `Size` 返回 `tree` 的大小。 - 格式: ```go Size() int ``` - 示例: ```go func ExampleBTree_Size() { tree := gtree.NewBTree(3, gutil.ComparatorString) fmt.Println(tree.Size()) for i := 0; i < 6; i++ { tree.Set("key"+gconv.String(i), "val"+gconv.String(i)) } fmt.Println(tree.Size()) // Output: // 0 // 6 } ``` ## `Keys` - 说明: `Keys` 按升序返回所有的 `key`。 - 格式: ```go Keys() []interface{} ``` - 示例: ```go func ExampleBTree_Keys() { tree := gtree.NewBTree(3, gutil.ComparatorString) for i := 6; i > 0; i-- { tree.Set("key"+gconv.String(i), "val"+gconv.String(i)) } fmt.Println(tree.Keys()) // Output: // [key1 key2 key3 key4 key5 key6] } ``` ## `Values` - 说明: `Values` 按 `key` 的升序返回所有的 `value`。 - 格式: ```go Values() []interface{} ``` - 示例: ```go func ExampleBTree_Values() { tree := gtree.NewBTree(3, gutil.ComparatorString) for i := 6; i > 0; i-- { tree.Set("key"+gconv.String(i), "val"+gconv.String(i)) } fmt.Println(tree.Values()) // Output: // [val1 val2 val3 val4 val5 val6] } ``` ## `Map` - 说明: `Map` 以 `map` 的形式返回所有的 `key/value`。 - 格式: ```go Map() map[interface{}]interface{} ``` - 示例: ```go func ExampleBTree_Map() { tree := gtree.NewBTree(3, gutil.ComparatorString) for i := 0; i < 6; i++ { tree.Set("key"+gconv.String(i), "val"+gconv.String(i)) } fmt.Println(tree.Map()) // Output: // map[key0:val0 key1:val1 key2:val2 key3:val3 key4:val4 key5:val5] } ``` ## `MapStrAny` - 说明: `MapStrAny` 以 `map[string]interface{}` 的形式返回所有的 `key/value`。 - 格式: ```go MapStrAny() map[string]interface{} ``` - 示例: ```go func ExampleBTree_MapStrAny() { tree := gtree.NewBTree(3, gutil.ComparatorString) for i := 0; i < 6; i++ { tree.Set(1000+i, "val"+gconv.String(i)) } fmt.Println(tree.MapStrAny()) // Output: // map[1000:val0 1001:val1 1002:val2 1003:val3 1004:val4 1005:val5] } ``` ## `Clear` - 说明: `Clear` 删除 `tree` 的所有数据。 - 格式: ```go Clear() ``` - 示例: ```go func ExampleBTree_Clear() { tree := gtree.NewBTree(3, gutil.ComparatorString) for i := 0; i < 6; i++ { tree.Set(1000+i, "val"+gconv.String(i)) } fmt.Println(tree.Size()) tree.Clear() fmt.Println(tree.Size()) // Output: // 6 // 0 } ``` ## `Replace` - 说明: `Replace` 用类型为 `map[interface{}]interface{}` 的 `data` 替换 `tree` 的 `key/value`。 - 格式: ```go Replace(data map[interface{}]interface{}) ``` - 示例: ```go func ExampleBTree_Replace() { tree := gtree.NewBTree(3, gutil.ComparatorString) for i := 0; i < 6; i++ { tree.Set("key"+gconv.String(i), "val"+gconv.String(i)) } fmt.Println(tree.Map()) data := map[interface{}]interface{}{ "newKey0": "newVal0", "newKey1": "newVal1", "newKey2": "newVal2", } tree.Replace(data) fmt.Println(tree.Map()) // Output: // map[key0:val0 key1:val1 key2:val2 key3:val3 key4:val4 key5:val5] // map[newKey0:newVal0 newKey1:newVal1 newKey2:newVal2] } ``` ## `Height` - 说明: `Height` 返回 `tree` 的高度。 - 格式: ```go Height() int ``` - 示例: ```go func ExampleBTree_Height() { tree := gtree.NewBTree(3, gutil.ComparatorInt) for i := 0; i < 100; i++ { tree.Set(i, i) } fmt.Println(tree.Height()) // Output: // 6 } ``` ## `Left` - 说明: `Left` 返回最左边(最小)的类型为 `*BTreeEntry` 的 `node`,如果 `tree` 是空的,则返回 `nil`。 - 格式: ```go Left() *BTreeEntry ``` - 示例: ```go func ExampleBTree_Left() { tree := gtree.NewBTree(3, gutil.ComparatorInt) for i := 1; i < 100; i++ { tree.Set(i, i) } fmt.Println(tree.Left().Key, tree.Left().Value) emptyTree := gtree.NewBTree(3, gutil.ComparatorInt) fmt.Println(emptyTree.Left()) // Output: // 1 1 // } ``` ## `Right` - 说明: `Left` 返回最右边(最大)的类型为 `*BTreeEntry` 的 `node`,如果 `tree` 是空的,则返回 `nil`。 - 格式: ```go Right() *BTreeEntry ``` - 示例: ```go func ExampleBTree_Right() { tree := gtree.NewBTree(3, gutil.ComparatorInt) for i := 1; i < 100; i++ { tree.Set(i, i) } fmt.Println(tree.Right().Key, tree.Right().Value) emptyTree := gtree.NewBTree(3, gutil.ComparatorInt) fmt.Println(emptyTree.Left()) // Output: // 99 99 // } ``` ## `String` - 说明: `String` 返回 `tree` 的 `node` 显示(用于调试)。 - 格式: ```go String() string ``` - 示例: ```go func ExampleBTree_String() { tree := gtree.NewBTree(3, gutil.ComparatorString) for i := 0; i < 6; i++ { tree.Set("key"+gconv.String(i), "val"+gconv.String(i)) } fmt.Println(tree.String()) // Output: // key0 // key1 // key2 // key3 // key4 // key5 } ``` ## `Search` - 说明: `Search` 使用参数 `key` 搜索 `tree`。如果找到 `key`,则返回其对应的键值,并且返回参数 `found` 为 `true`,否则为 `false`。 - 格式: ```go Search(key interface{}) (value interface{}, found bool) ``` - 示例: ```go func ExampleBTree_Search() { tree := gtree.NewBTree(3, gutil.ComparatorString) for i := 0; i < 6; i++ { tree.Set("key"+gconv.String(i), "val"+gconv.String(i)) } fmt.Println(tree.Search("key0")) fmt.Println(tree.Search("key6")) // Output: // val0 true // false } ``` ## `Print` - 说明: `Print` 将 `tree` 打印到标准输出。 - 格式: ```go Print() ``` - 示例: ```go func ExampleBTree_Print() { tree := gtree.NewBTree(3, gutil.ComparatorString) for i := 0; i < 6; i++ { tree.Set("key"+gconv.String(i), "val"+gconv.String(i)) } tree.Print() // Output: // key0 // key1 // key2 // key3 // key4 // key5 } ``` ## `Iterator` - 说明: `Iterator` 等同于 `IteratorAsc`。 - 格式: ```go Iterator(f func(key, value interface{}) bool) ``` - 示例: ```go func ExampleBTree_Iterator() { tree := gtree.NewBTree(3, gutil.ComparatorString) for i := 0; i < 10; i++ { tree.Set(i, 10-i) } var totalKey, totalValue int tree.Iterator(func(key, value interface{}) bool { totalKey += key.(int) totalValue += value.(int) return totalValue < 20 }) fmt.Println("totalKey:", totalKey) fmt.Println("totalValue:", totalValue) // Output: // totalKey: 3 // totalValue: 27 } ``` ## `IteratorFrom` - 说明: `IteratorFrom` 等同于 `IteratorAscFrom`。 - 格式: ```go IteratorFrom(key interface{}, match bool, f func(key, value interface{}) bool) ``` - 示例: ```go func ExampleBTree_IteratorFrom() { m := make(map[interface{}]interface{}) for i := 1; i <= 5; i++ { m[i] = i * 10 } tree := gtree.NewBTreeFrom(3, gutil.ComparatorInt, m) tree.IteratorFrom(1, true, func(key, value interface{}) bool { fmt.Println("key:", key, ", value:", value) return true }) // Output: // key: 1 , value: 10 // key: 2 , value: 20 // key: 3 , value: 30 // key: 4 , value: 40 // key: 5 , value: 50 } ``` ## `IteratorAsc` - 说明: `IteratorAsc` 使用自定义回调函数 `f` 以只读方式按升序迭代 `tree`。如果 `f` 返回 `true`,则继续迭代,返回 `false` 则停止。 - 格式: ```go IteratorAsc(f func(key, value interface{}) bool) ``` - 示例: ```go func ExampleBTree_IteratorAsc() { tree := gtree.NewBTree(3, gutil.ComparatorString) for i := 0; i < 10; i++ { tree.Set(i, 10-i) } tree.IteratorAsc(func(key, value interface{}) bool { fmt.Println("key:", key, ", value:", value) return true }) // Output: // key: 0 , value: 10 // key: 1 , value: 9 // key: 2 , value: 8 // key: 3 , value: 7 // key: 4 , value: 6 // key: 5 , value: 5 // key: 6 , value: 4 // key: 7 , value: 3 // key: 8 , value: 2 // key: 9 , value: 1 } ``` ## `IteratorAscFrom` - 说明: `IteratorAscFrom` 使用自定义回调函数 `f` 以只读方式按升序迭代 `tree`。参数 `key` 指定从哪个 `key` 开始迭代。参数 `match` 为 `true` 时,从 `key` 完全匹配时开始迭代,否则使用索引搜索迭代。如果 `f` 返回 `true`,则继续迭代,返回 `false` 则停止。 - 格式: ```go IteratorAscFrom(key interface{}, match bool, f func(key, value interface{}) bool) ``` - 示例: ```go func ExampleBTree_IteratorAscFrom_Normal() { m := make(map[interface{}]interface{}) for i := 1; i <= 5; i++ { m[i] = i * 10 } tree := gtree.NewBTreeFrom(3, gutil.ComparatorInt, m) tree.IteratorAscFrom(1, true, func(key, value interface{}) bool { fmt.Println("key:", key, ", value:", value) return true }) // Output: // key: 1 , value: 10 // key: 2 , value: 20 // key: 3 , value: 30 // key: 4 , value: 40 // key: 5 , value: 50 } ``` ```go func ExampleBTree_IteratorAscFrom_NoExistKey() { m := make(map[interface{}]interface{}) for i := 1; i <= 5; i++ { m[i] = i * 10 } tree := gtree.NewBTreeFrom(3, gutil.ComparatorInt, m) tree.IteratorAscFrom(0, true, func(key, value interface{}) bool { fmt.Println("key:", key, ", value:", value) return true }) // Output: } ``` ```go func ExampleBTree_IteratorAscFrom_NoExistKeyAndMatchFalse() { m := make(map[interface{}]interface{}) for i := 1; i <= 5; i++ { m[i] = i * 10 } tree := gtree.NewBTreeFrom(3, gutil.ComparatorInt, m) tree.IteratorAscFrom(0, false, func(key, value interface{}) bool { fmt.Println("key:", key, ", value:", value) return true }) // Output: // key: 1 , value: 10 // key: 2 , value: 20 // key: 3 , value: 30 // key: 4 , value: 40 // key: 5 , value: 50 } ``` ## `IteratorDesc` - 说明: `IteratorDesc` 使用自定义回调函数 `f` 以只读方式按降序迭代 `tree`。如果 `f` 返回 `true`,则继续迭代,返回 `false` 则停止。 - 格式: ```go IteratorDesc(f func(key, value interface{}) bool) ``` - 示例: ```go func ExampleBTree_IteratorDesc() { tree := gtree.NewBTree(3, gutil.ComparatorString) for i := 0; i < 10; i++ { tree.Set(i, 10-i) } tree.IteratorDesc(func(key, value interface{}) bool { fmt.Println("key:", key, ", value:", value) return true }) // Output: // key: 9 , value: 1 // key: 8 , value: 2 // key: 7 , value: 3 // key: 6 , value: 4 // key: 5 , value: 5 // key: 4 , value: 6 // key: 3 , value: 7 // key: 2 , value: 8 // key: 1 , value: 9 // key: 0 , value: 10 } ``` ## `IteratorDescFrom` - 说明: `IteratorDescFrom` 使用自定义回调函数 `f` 以只读方式按降序迭代 `tree`。参数 `key` 指定从哪个 `key` 开始迭代。参数 `match` 为 `true` 时,从 `key` 完全匹配时开始迭代,否则使用索引搜索迭代。如果 `f` 返回 `true`,则继续迭代,返回 `false` 则停止。 - 格式: ```go IteratorDescFrom(key interface{}, match bool, f func(key, value interface{}) bool) ``` - 示例: ```go func ExampleBTree_IteratorDescFrom() { m := make(map[interface{}]interface{}) for i := 1; i <= 5; i++ { m[i] = i * 10 } tree := gtree.NewBTreeFrom(3, gutil.ComparatorInt, m) tree.IteratorDescFrom(5, true, func(key, value interface{}) bool { fmt.Println("key:", key, ", value:", value) return true }) // Output: // key: 5 , value: 50 // key: 4 , value: 40 // key: 3 , value: 30 // key: 2 , value: 20 // key: 1 , value: 10 } ``` ## `MarshalJson` - 说明: `MarshalJSON` 实现 `json.Marshal` 的接口。 - 格式: ```go MarshalJSON() ([]byte, error) ``` - 示例: ```go func ExampleBTree_MarshalJSON() { tree := gtree.NewBTree(3, gutil.ComparatorString) for i := 0; i < 6; i++ { tree.Set("key"+gconv.String(i), "val"+gconv.String(i)) } bytes, err := json.Marshal(tree) if err == nil { fmt.Println(gconv.String(bytes)) } // Output: // {"key0":"val0","key1":"val1","key2":"val2","key3":"val3","key4":"val4","key5":"val5"} } ``` # 对象复用-gpool ## 基本介绍 对象复用池(并发安全)。将对象进行缓存复用,支持 `过期时间`、 `创建方法` 及 `销毁方法` 定义。 **使用场景**: 任何需要支持定时过期的对象复用场景。 **使用方式**: ```go import "github.com/gogf/gf/v2/container/gpool" ``` **接口文档**: [https://pkg.go.dev/github.com/gogf/gf/v2/container/gpool](https://pkg.go.dev/github.com/gogf/gf/v2/container/gpool) 需要注意几点: 1. `New` 方法的过期时间类型为 `time.Duration`。 2. 对象 `创建方法`( `newFunc NewFunc`)返回值包含一个 `error` 返回,当对象创建失败时可由该返回值反馈原因。 3. 对象 `销毁方法`( `expireFunc...ExpireFunc`)为可选参数,用以当对象超时/池关闭时,自动调用自定义的方法销毁对象。 ## 泛型支持 **提示** 版本要求:`v2.10.0` 从 `v2.10.0` 版本开始,`gpool` 提供了泛型类型 `TPool[T]`,提供类型安全的对象池操作。 ### 基本使用 使用 `NewTPool[T]` 创建泛型对象池: ```go type MyObject struct { ID int Name string } // 创建对象池,设置 3 秒过期时间 pool := gpool.NewTPool[*MyObject]( 3*time.Second, func() (*MyObject, error) { // 对象创建方法 return &MyObject{ID: 1, Name: "example"}, nil }, func(obj *MyObject) { // 对象销毁方法(可选) fmt.Printf("Destroying object: %+v\n", obj) }, ) defer pool.Close() // 从池中获取对象 obj, err := pool.Get() if err != nil { panic(err) } fmt.Printf("Got object: %+v\n", obj) // 使用完对象后放回池中 pool.MustPut(obj) ``` ### 类型安全的优势 泛型版本提供编译时类型检查,避免了类型断言: ```go // 传统方式(需要类型断言) pool := gpool.New(3*time.Second, func() (interface{}, error) { return &MyObject{}, nil }) obj, _ := pool.Get() myObj := obj.(*MyObject) // 需要类型断言 // 泛型方式(类型安全) pool := gpool.NewTPool[*MyObject](3*time.Second, func() (*MyObject, error) { return &MyObject{}, nil }) obj, _ := pool.Get() // obj 直接是 *MyObject 类型,无需断言 ``` ## `gpool` 与 `sync.Pool` `gpool` 与 `sync.Pool` 都可以达到对象复用的作用,但是两者的设计初衷和使用场景不太一样。 `sync.Pool` 的对象生命周期不支持自定义过期时间,究其原因, `sync.Pool` 并不是一个 `Cache`; `sync.Pool` 设计初衷是为了缓解GC `压力`, `sync.Pool` 中的对象会在 `GC` 开始前全部清除;并且 `sync.Pool` 不支持对象创建方法及销毁方法。 # 对象复用-基本使用 ## 基本使用 ```go package main import ( "github.com/gogf/gf/v2/container/gpool" "fmt" "time" ) func main () { // 创建一个对象池,过期时间为1秒 p := gpool.New(time.Second, nil) // 从池中取一个对象,返回nil及错误信息 fmt.Println(p.Get()) // 丢一个对象到池中 p.Put(1) // 重新从池中取一个对象,返回1 fmt.Println(p.Get()) // 等待2秒后重试,发现对象已过期,返回nil及错误信息 time.Sleep(2*time.Second) fmt.Println(p.Get()) } ``` ## 创建及销毁方法 我们可以给定动态创建及销毁方法。 ```go package main import ( "fmt" "github.com/gogf/gf/v2/container/gpool" "github.com/gogf/gf/v2/net/gtcp" "github.com/gogf/gf/v2/os/glog" "time" ) func main() { // 创建对象复用池,对象过期时间为3秒,并给定创建及销毁方法 p := gpool.New(3*time.Second, func() (interface{}, error) { return gtcp.NewConn("www.baidu.com:80") }, func(i interface{}) { glog.Println("expired") i.(*gtcp.Conn).Close() }) conn, err := p.Get() if err != nil { panic(err) } result, err := conn.(*gtcp.Conn).SendRecv([]byte("HEAD / HTTP/1.1\n\n"), -1) if err != nil { panic(err) } fmt.Println(string(result)) // 丢回池中以便重复使用 p.Put(conn) // 等待一定时间观察过期方法调用 time.Sleep(4*time.Second) } ``` 执行后,终端输出结果: ``` HTTP/1.1 302 Found Connection: Keep-Alive Content-Length: 17931 Content-Type: text/html Date: Wed, 29 May 2019 11:23:20 GMT Etag: "54d9749e-460b" Server: bfe/1.0.8.18 2019-05-29 19:23:24.732 expired ``` ## 泛型使用示例 **提示** 版本要求:`v2.10.0` 从 `v2.10.0` 版本开始,`gpool` 提供了泛型类型 `TPool[T]`,无需类型断言,更加类型安全。 ### 基本泛型使用 ```go package main import ( "fmt" "time" "github.com/gogf/gf/v2/container/gpool" ) func main() { // 创建一个泛型对象池,过期时间为1秒 p := gpool.NewTPool[int]( time.Second, nil, // 不指定创建方法 ) // 从池中取一个对象,返回零值及错误信息 fmt.Println(p.Get()) // 丢一个对象到池中 p.Put(1) // 重新从池中取一个对象,返回 1 fmt.Println(p.Get()) // 等待2秒后重试,发现对象已过期,返回零值及错误信息 time.Sleep(2 * time.Second) fmt.Println(p.Get()) } ``` ### 泛型对象池示例 ```go package main import ( "fmt" "time" "github.com/gogf/gf/v2/container/gpool" "github.com/gogf/gf/v2/net/gtcp" "github.com/gogf/gf/v2/os/glog" ) func main() { // 创建对象复用池,对象过期时间为3秒,并给定创建及销毁方法 p := gpool.NewTPool[*gtcp.Conn]( 3*time.Second, func() (*gtcp.Conn, error) { return gtcp.NewConn("www.baidu.com:80") }, func(conn *gtcp.Conn) { glog.Println("expired") conn.Close() }, ) conn, err := p.Get() if err != nil { panic(err) } // 无需类型断言,conn 已经是 *gtcp.Conn 类型 result, err := conn.SendRecv([]byte("HEAD / HTTP/1.1\n\n"), -1) if err != nil { panic(err) } fmt.Println(string(result)) // 丢回池中以便重复使用 p.Put(conn) // 等待一定时间观察过期方法调用 time.Sleep(4 * time.Second) } ``` ### 自定义结构体示例 ```go package main import ( "fmt" "time" "github.com/gogf/gf/v2/container/gpool" ) type User struct { ID int Name string } func main() { // 创建 User 对象池 pool := gpool.NewTPool[*User]( 5*time.Second, func() (*User, error) { return &User{ ID: 1, Name: "default", }, nil }, func(user *User) { fmt.Printf("清理用户对象: %+v\n", user) }, ) defer pool.Close() // 获取对象 user, err := pool.Get() if err != nil { panic(err) } // 直接使用,无需类型断言 fmt.Printf("用户ID: %d, 名称: %s\n", user.ID, user.Name) // 修改对象 user.ID = 100 user.Name = "张三" // 放回池中复用 pool.MustPut(user) // 再次获取,得到刚才修改的对象 user2, _ := pool.Get() fmt.Printf("复用对象 - ID: %d, 名称: %s\n", user2.ID, user2.Name) } ``` # 并发安全环-gring ## 基本介绍 支持并发安全开关的环结构,循环双向链表。 **使用场景**: `ring` 这种数据结构在底层开发中用得比较多一些,如:并发锁控制、缓冲区控制、循环缓冲、滑动窗口等场景。`ring` 的特点在于,其必须有固定的大小,当不停地往 `ring` 中追加写数据时,如果数据大小超过容量大小,新值将会将旧值覆盖。 **使用方式**: ```go import "github.com/gogf/gf/v2/container/gring" ``` **接口文档**: [https://pkg.go.dev/github.com/gogf/gf/v2/container/gring](https://pkg.go.dev/github.com/gogf/gf/v2/container/gring) **提示** `gring` 支持链式操作。 ## 泛型支持 **提示** 版本要求:`v2.10.0` 从 `v2.10.0` 版本开始,`gring` 提供了泛型类型 `TRing[T]`,提供类型安全的环形队列操作。 ### 基本使用 使用 `NewTRing[T]` 创建泛型环: ```go package main import ( "fmt" "github.com/gogf/gf/v2/container/gring" ) func main() { // 创建一个容量为 10 的整型环 r := gring.NewTRing[int](10) // 向环中添加元素 for i := 0; i < 5; i++ { r.Put(i) } fmt.Println("Len:", r.Len()) // 输出: Len: 5 fmt.Println("Cap:", r.Cap()) // 输出: Cap: 10 // 移动到起始位置并获取值 r.Move(-5) fmt.Println("Val:", r.Val()) // 输出: Val: 0 } ``` ### 类型安全的优势 泛型版本提供编译时类型检查,避免了类型断言: ```go // 传统方式(需要类型断言) r := gring.New(10) r.Put(1) val := r.Val().(int) // 需要类型断言 // 泛型方式(类型安全) r := gring.NewTRing[int](10) r.Put(1) val := r.Val() // val 直接是 int 类型,无需断言 ``` ### 自定义类型示例 ```go package main import ( "fmt" "github.com/gogf/gf/v2/container/gring" ) type Player struct { Position int // 位置 Name string // 名称 Alive bool // 是否存活 } func main() { // 创建玩家环 r := gring.NewTRing[*Player](10) // 添加玩家 for i := 1; i <= 10; i++ { r.Put(&Player{ Position: i, Name: fmt.Sprintf("Player%d", i), Alive: true, }) } // 遍历环中的所有玩家 r.Move(-10) // 移动到起始位置 r.RLockIteratorNext(func(player *Player) bool { fmt.Printf("Position: %d, Name: %s, Alive: %v\n", player.Position, player.Name, player.Alive) return true }) } ``` ### 并发安全的泛型环 ```go package main import ( "fmt" "sync" "github.com/gogf/gf/v2/container/gring" ) func main() { // 创建并发安全的泛型环 r := gring.NewTRing[int](10, true) var wg sync.WaitGroup // 并发写入 for i := 0; i < 5; i++ { wg.Add(1) go func(n int) { defer wg.Done() r.Put(n * 10) }(i) } wg.Wait() fmt.Println("Len:", r.Len()) fmt.Println("Values:", r.SliceNext()) } ``` ### 泛型环的常用方法 泛型环 `TRing[T]` 提供了以下常用方法: | 方法 | 说明 | | --- | --- | | `Val() T` | 获取当前位置的元素值,无需类型断言 | | `Set(value T) *TRing[T]` | 设置当前位置的值 | | `Put(value T) *TRing[T]` | 设置当前位置的值并移动到下一位置 | | `Len() int` | 获取环中已使用的元素数量 | | `Cap() int` | 获取环的容量 | | `Move(n int) *TRing[T]` | 移动 n 个位置 | | `Next() *TRing[T]` | 移动到下一个位置 | | `Prev() *TRing[T]` | 移动到上一个位置 | | `Link(s *TRing[T]) *TRing[T]` | 连接两个环 | | `Unlink(n int) *TRing[T]` | 从环中移除 n 个元素 | | `SliceNext() []T` | 从当前位置向后获取所有元素切片 | | `SlicePrev() []T` | 从当前位置向前获取所有元素切片 | | `RLockIteratorNext(f func(value T) bool)` | 向后遍历(只读锁) | | `RLockIteratorPrev(f func(value T) bool)` | 向前遍历(只读锁) | ### 类型安全的遍历 ```go package main import ( "fmt" "github.com/gogf/gf/v2/container/gring" ) func main() { r := gring.NewTRing[string](5) words := []string{"GoFrame", "is", "awesome", "and", "powerful"} for _, word := range words { r.Put(word) } // 向后遍历 - 类型安全,无需断言 r.Move(-5) r.RLockIteratorNext(func(word string) bool { fmt.Printf("Word: %s\n", word) return true }) } ``` ## 性能说明 - `gring` 基于标准库 `container/ring` 实现,在此基础上增加了并发安全控制和更多实用方法 - 环形结构在固定大小的缓冲场景下性能优异,避免了频繁的内存分配 - 泛型版本在保持相同性能的同时,提供了更好的类型安全性 - 推荐在需要循环缓冲、滑动窗口等场景使用 # 并发安全环-基本使用 ## 约瑟夫问题 我们使用 `gring` 来模拟一下 [约瑟夫问题](https://baike.baidu.com/item/%E7%BA%A6%E7%91%9F%E5%A4%AB%E9%97%AE%E9%A2%98/3857719): **说明** 著名犹太历史学家`Josephus`有过以下的故事:在罗马人占领乔塔帕特后,`39`个犹太人与`Josephus`及他的朋友躲到一个洞中,`39`个犹太人决定宁愿死也不要被敌人抓到,于是决定了一个自杀方式,`41`个人排成一个圆圈,由第`1`个人开始报数,每报数到第`3`人该人就必须自杀,然后再由下一个重新报数,直到所有人都自杀身亡为止。 然而`Josephus`和他的朋友并不想遵从。首先从一个人开始,越过`k-2`个人(因为第一个人已经被越过),并杀掉第`k`个人。接着,再越过`k-1`个人,并杀掉第`k`个人。这个过程沿着圆圈一直进行,直到最终只剩下一个人留下,这个人就可以继续活着。问题是,给定了和,一开始要站在什么地方才能避免被处决? ### 传统方式实现 以下示例为非并发安全场景。 ```go package main import ( "fmt" "github.com/gogf/gf/v2/container/gring" ) type Player struct { position int // 位置 alive bool // 是否存活 } const ( playerCount = 41 // 玩家人数 startPos = 1 // 开始报数位置 ) var ( deadline = 3 ) func main() { r := gring.New(playerCount) // 设置所有玩家初始值 for i := 1; i <= playerCount; i++ { r.Put(&Player{i, true}) } // 如果开始报数的位置不为1,则设置开始位置 if startPos > 1 { r.Move(startPos - 1) } counter := 1 // 报数从1开始,因为下面的循环从第二个开始计算 deadCount := 0 // 死亡人数,初始值为0 // 直到所有人都死亡,否则循环一直执行 for deadCount < playerCount { // 跳到下一个人 r.Next() // 如果是活着的人,则报数 if r.Val().(*Player).alive { counter++ } // 如果报数为deadline,则此人淘汰出局 if counter == deadline { r.Val().(*Player).alive = false fmt.Printf("Player %d died!\n", r.Val().(*Player).position) deadCount++ counter = 0 } } } ``` 执行后,输出结果为: ``` Player 3 died! Player 6 died! Player 9 died! Player 12 died! Player 15 died! Player 18 died! Player 21 died! Player 24 died! Player 27 died! Player 30 died! Player 33 died! Player 36 died! Player 39 died! Player 1 died! Player 5 died! Player 10 died! Player 14 died! Player 19 died! Player 23 died! Player 28 died! Player 32 died! Player 37 died! Player 41 died! Player 7 died! Player 13 died! Player 20 died! Player 26 died! Player 34 died! Player 40 died! Player 11 died! Player 22 died! Player 35 died! Player 8 died! Player 25 died! Player 4 died! Player 29 died! Player 2 died! Player 38 died! Player 17 died! Player 12 died! Player 31 died! ``` ## 泛型版本实现 **提示** 版本要求:`v2.10.0` 从 `v2.10.0` 版本开始,可以使用泛型版本的 `TRing[T]` 实现相同功能,代码更简洁且类型安全。 ### 约瑟夫问题泛型实现 ```go package main import ( "fmt" "github.com/gogf/gf/v2/container/gring" ) type Player struct { Position int // 位置 Alive bool // 是否存活 } const ( playerCount = 41 // 玩家人数 startPos = 1 // 开始报数位置 deadline = 3 // 报数阈值 ) func main() { // 创建泛型环,类型安全 r := gring.NewTRing[*Player](playerCount) // 设置所有玩家初始值 for i := 1; i <= playerCount; i++ { r.Put(&Player{ Position: i, Alive: true, }) } // 如果开始报数的位置不为1,则设置开始位置 if startPos > 1 { r.Move(startPos - 1) } counter := 1 // 报数从1开始 deadCount := 0 // 死亡人数 // 直到所有人都死亡,否则循环一直执行 for deadCount < playerCount { // 跳到下一个人 r.Next() // 无需类型断言,直接使用 player := r.Val() // 如果是活着的人,则报数 if player.Alive { counter++ } // 如果报数为deadline,则此人淘汰出局 if counter == deadline { player.Alive = false fmt.Printf("Player %d died!\n", player.Position) deadCount++ counter = 0 } } } ``` ### 循环缓冲区示例 使用泛型环实现固定大小的循环缓冲区: ```go package main import ( "fmt" "github.com/gogf/gf/v2/container/gring" ) type LogEntry struct { Timestamp int64 Message string Level string } func main() { // 创建容量为 5 的日志缓冲环 logBuffer := gring.NewTRing[*LogEntry](5) // 添加 10 条日志(超过容量,旧的会被覆盖) for i := 1; i <= 10; i++ { logBuffer.Put(&LogEntry{ Timestamp: int64(i), Message: fmt.Sprintf("Log message %d", i), Level: "INFO", }) } fmt.Printf("Buffer capacity: %d\n", logBuffer.Cap()) fmt.Printf("Buffer length: %d\n", logBuffer.Len()) // 获取所有日志(只有最新的5条) logBuffer.Move(-5) logBuffer.RLockIteratorNext(func(entry *LogEntry) bool { fmt.Printf("Timestamp: %d, Level: %s, Message: %s\n", entry.Timestamp, entry.Level, entry.Message) return true }) } ``` 执行后,输出结果为: ``` Buffer capacity: 5 Buffer length: 5 Timestamp: 6, Level: INFO, Message: Log message 6 Timestamp: 7, Level: INFO, Message: Log message 7 Timestamp: 8, Level: INFO, Message: Log message 8 Timestamp: 9, Level: INFO, Message: Log message 9 Timestamp: 10, Level: INFO, Message: Log message 10 ``` ### 滑动窗口统计示例 使用泛型环实现滑动窗口统计: ```go package main import ( "fmt" "github.com/gogf/gf/v2/container/gring" ) func main() { // 创建一个窗口大小为 5 的统计环 window := gring.NewTRing[int](5) // 模拟数据流 dataStream := []int{10, 20, 30, 40, 50, 60, 70, 80, 90, 100} for _, value := range dataStream { // 添加新数据 window.Put(value) // 计算当前窗口的和 sum := 0 values := window.SliceNext() for _, v := range values { sum += v } // 计算平均值 count := len(values) if count > 0 { avg := float64(sum) / float64(count) fmt.Printf("Value: %d, Window: %v, Sum: %d, Avg: %.2f\n", value, values, sum, avg) } } } ``` 执行后,输出结果为: ``` Value: 10, Window: [10], Sum: 10, Avg: 10.00 Value: 20, Window: [10 20], Sum: 30, Avg: 15.00 Value: 30, Window: [10 20 30], Sum: 60, Avg: 20.00 Value: 40, Window: [10 20 30 40], Sum: 100, Avg: 25.00 Value: 50, Window: [10 20 30 40 50], Sum: 150, Avg: 30.00 Value: 60, Window: [20 30 40 50 60], Sum: 200, Avg: 40.00 Value: 70, Window: [30 40 50 60 70], Sum: 250, Avg: 50.00 Value: 80, Window: [40 50 60 70 80], Sum: 300, Avg: 60.00 Value: 90, Window: [50 60 70 80 90], Sum: 350, Avg: 70.00 Value: 100, Window: [60 70 80 90 100], Sum: 400, Avg: 80.00 ``` ## 并发安全使用 ### 并发写入示例 ```go package main import ( "fmt" "sync" "time" "github.com/gogf/gf/v2/container/gring" ) type Task struct { ID int Name string CreatedAt time.Time } func main() { // 创建并发安全的任务环 taskRing := gring.NewTRing[*Task](10, true) var wg sync.WaitGroup // 并发添加任务 for i := 0; i < 20; i++ { wg.Add(1) go func(id int) { defer wg.Done() taskRing.Put(&Task{ ID: id, Name: fmt.Sprintf("Task-%d", id), CreatedAt: time.Now(), }) }(i) } wg.Wait() // 读取所有任务 fmt.Printf("Total tasks in ring: %d\n", taskRing.Len()) taskRing.RLockIteratorNext(func(task *Task) bool { fmt.Printf("Task ID: %d, Name: %s\n", task.ID, task.Name) return true }) } ``` ## 类型安全对比 ### 传统方式 vs 泛型方式 ```go package main import ( "fmt" "github.com/gogf/gf/v2/container/gring" ) type User struct { ID int Name string } func main() { // 传统方式 - 需要类型断言 r1 := gring.New(5) r1.Put(&User{ID: 1, Name: "Alice"}) // 需要类型断言,运行时可能 panic user1 := r1.Val().(*User) fmt.Printf("User1: %+v\n", user1) // 泛型方式 - 类型安全 r2 := gring.NewTRing[*User](5) r2.Put(&User{ID: 2, Name: "Bob"}) // 无需类型断言,编译时检查 user2 := r2.Val() fmt.Printf("User2: %+v\n", user2) } ``` ## 性能说明 - 泛型版本 `TRing[T]` 与传统版本 `Ring` 性能基本一致 - 泛型版本在编译时进行类型检查,避免了运行时的类型断言开销 - 环形结构使用固定内存,不会产生额外的内存分配 - 推荐在新项目中使用泛型版本,提升代码安全性和可维护性 # 并发安全环-方法介绍 以下常用方法列表,文档更新可能滞后于代码新特性,更多的方法及示例请参考代码文档:[https://pkg.go.dev/github.com/gogf/gf/v2/container/gring](https://pkg.go.dev/github.com/gogf/gf/v2/container/gring) ## 构造方法 ### `New` - 说明:`New` 创建并返回 `cap` 元素的环形结构。可选参数 `safe` 指定是否在并发安全中使用此结构,默认情况下为 `false`。 - 格式: ```go New(cap int, safe ...bool) *Ring ``` - 示例: ```go func ExampleNew() { // Non concurrent safety gring.New(10) // Concurrent safety gring.New(10, true) // Output: } ``` ### `NewTRing` **提示** 版本要求:`v2.10.0` - 说明:`NewTRing` 创建并返回泛型环形结构。可选参数 `safe` 指定是否在并发安全中使用此结构,默认情况下为 `false`。 - 格式: ```go NewTRing[T any](cap int, safe ...bool) *TRing[T] ``` - 示例: ```go func ExampleNewTRing() { // Create a non-concurrent-safe ring with type int r1 := gring.NewTRing[int](10) // Create a concurrent-safe ring with type string r2 := gring.NewTRing[string](10, true) // Create a ring with custom type type User struct { ID int Name string } r3 := gring.NewTRing[*User](10) // Output: } ``` ## 基本方法 ### `Val` - 说明:`Val` 返回当前位置的项值。 - 格式: ```go // 传统方式 Val() interface{} // 泛型方式 (v2.10.0+) Val() T ``` - 示例: ```go func ExampleRing_Val() { r := gring.New(10) r.Set(1) fmt.Println("Val:", r.Val()) r.Next().Set("GoFrame") fmt.Println("Val:", r.Val()) // Output: // Val: 1 // Val: GoFrame } // 泛型方式 func ExampleTRing_Val() { r := gring.NewTRing[int](10) r.Set(100) fmt.Println("Val:", r.Val()) // 无需类型断言 // Output: // Val: 100 } ``` ### `Len` - 说明:`Len` 返回环的大小(已使用元素数量)。 - 格式: ```go Len() int ``` - 示例: ```go func ExampleRing_Len() { r1 := gring.New(10) for i := 0; i < 5; i++ { r1.Set(i).Next() } fmt.Println("Len:", r1.Len()) r2 := gring.New(10, true) for i := 0; i < 10; i++ { r2.Set(i).Next() } fmt.Println("Len:", r2.Len()) // Output: // Len: 5 // Len: 10 } ``` ### `Cap` - 说明:`Cap` 返回环的容量(最大元素数量)。 - 格式: ```go Cap() int ``` - 示例: ```go func ExampleRing_Cap() { r1 := gring.New(10) for i := 0; i < 5; i++ { r1.Set(i).Next() } fmt.Println("Cap:", r1.Cap()) r2 := gring.New(10, true) for i := 0; i < 10; i++ { r2.Set(i).Next() } fmt.Println("Cap:", r2.Cap()) // Output: // Cap: 10 // Cap: 10 } ``` ## 设置方法 ### `Set` - 说明:`Set` 将值设置到当前位置的项。 - 格式: ```go // 传统方式 Set(value interface{}) *Ring // 泛型方式 (v2.10.0+) Set(value T) *TRing[T] ``` - 示例: ```go func ExampleRing_Set() { r := gring.New(10) r.Set(1) fmt.Println("Val:", r.Val()) r.Next().Set("GoFrame") fmt.Println("Val:", r.Val()) // Output: // Val: 1 // Val: GoFrame } // 泛型方式 func ExampleTRing_Set() { r := gring.NewTRing[string](10) r.Set("Hello") fmt.Println("Val:", r.Val()) r.Next().Set("World") fmt.Println("Val:", r.Val()) // Output: // Val: Hello // Val: World } ``` ### `Put` - 说明:`Put` 将 `value` 设置到当前环的项,并将位置移到下一项。 - 格式: ```go // 传统方式 Put(value interface{}) *Ring // 泛型方式 (v2.10.0+) Put(value T) *TRing[T] ``` - 示例: ```go func ExampleRing_Put() { r := gring.New(10) r.Put(1) fmt.Println("Val:", r.Val()) fmt.Println("Val:", r.Prev().Val()) // Output: // Val: // Val: 1 } // 泛型方式 func ExampleTRing_Put() { r := gring.NewTRing[int](10) r.Put(100) fmt.Println("Val:", r.Val()) fmt.Println("Val:", r.Prev().Val()) // Output: // Val: 0 // Val: 100 } ``` ## 移动方法 ### `Move` - 说明:`Move` 在环中向后(`n < 0`)或向前(`n >= 0`)移动 `n % r.Len()` 个元素,并返回该环元素。`r` 不能为空。 - 格式: ```go // 传统方式 Move(n int) *Ring // 泛型方式 (v2.10.0+) Move(n int) *TRing[T] ``` - 示例: ```go func ExampleRing_Move() { r := gring.New(10) for i := 0; i < 10; i++ { r.Set(i).Next() } // ring at Pos 0 fmt.Println("CurVal:", r.Val()) r.Move(5) // ring at Pos 5 fmt.Println("CurVal:", r.Val()) // Output: // CurVal: 0 // CurVal: 5 } ``` ### `Prev` - 说明:`Prev` 返回上一个环元素。`r` 不能为空。 - 格式: ```go // 传统方式 Prev() *Ring // 泛型方式 (v2.10.0+) Prev() *TRing[T] ``` - 示例: ```go func ExampleRing_Prev() { r := gring.New(10) for i := 0; i < 5; i++ { r.Set(i).Next() } fmt.Println("Prev:", r.Prev().Val()) fmt.Println("Prev:", r.Prev().Val()) // Output: // Prev: 4 // Prev: 3 } ``` ### `Next` - 说明:`Next` 返回下一个环元素。`r` 不能为空。 - 格式: ```go // 传统方式 Next() *Ring // 泛型方式 (v2.10.0+) Next() *TRing[T] ``` - 示例: ```go func ExampleRing_Next() { r := gring.New(10) for i := 5; i > 0; i-- { r.Set(i).Prev() } fmt.Println("Next:", r.Next().Val()) fmt.Println("Next:", r.Next().Val()) // Output: // Next: 1 // Next: 2 } ``` ## 链接方法 ### `Link` - 说明:`Link` 连接环 `r` 与环 `s`,使得 `r.Next()` 变为 `s` 并返回 `r.Next()` 的原始值。`r` 不能为空。 - 如果 `r` 和 `s` 指向同一个环,则连接它们会从环中删除 `r` 和 `s` 之间的元素。删除的元素形成一个子环,结果是对该子环的引用(如果没有删除元素,则结果仍然是 `r.Next()` 的原始值,而不是 `nil`)。 - 如果 `r` 和 `s` 指向不同的环,则将它们连接起来会创建一个单环,其中 `s` 的元素插入到 `r` 之后。结果指向插入 `s` 的最后一个元素之后的元素。 - 格式: ```go // 传统方式 Link(s *Ring) *Ring // 泛型方式 (v2.10.0+) Link(s *TRing[T]) *TRing[T] ``` - 示例: ```go func ExampleRing_Link() { r := gring.New(10) for i := 0; i < 5; i++ { r.Set(i).Next() } s := gring.New(10) for i := 0; i < 10; i++ { val := i + 5 s.Set(val).Next() } r.Link(s) // Link Ring s to Ring r fmt.Println("Len:", r.Len()) fmt.Println("Cap:", r.Cap()) // Output: // Len: 15 // Cap: 20 } ``` ### `Unlink` - 说明:`Unlink` 从环 `r` 中删除 `n % r.Len()` 个元素,从 `r.Next()` 开始。如果 `n % r.Len() == 0`,则 `r` 保持不变。结果是删除的子环。`r` 不能为空。 - 格式: ```go // 传统方式 Unlink(n int) *Ring // 泛型方式 (v2.10.0+) Unlink(n int) *TRing[T] ``` - 示例: ```go func ExampleRing_Unlink() { r := gring.New(10) for i := 0; i < 10; i++ { r.Set(i).Next() } fmt.Println("Before Unlink, Len:", r.Len()) fmt.Println("Before Unlink, Cap:", r.Cap()) r.Unlink(7) fmt.Println("After Unlink, Len:", r.Len()) fmt.Println("After Unlink, Cap:", r.Cap()) // Output: // Before Unlink, Len: 10 // Before Unlink, Cap: 10 // After Unlink, Len: 3 // After Unlink, Cap: 3 } ``` ## 遍历方法 ### `RLockIteratorNext` - 说明:`RLockIteratorNext` 使用给定的回调函数 `f` 在 `RWMutex.RLock` 内向前迭代并锁定读取。如果 `f` 返回 `true`,则继续迭代;或者返回 `false` 停止。 - 格式: ```go // 传统方式 RLockIteratorNext(f func(value interface{}) bool) // 泛型方式 (v2.10.0+) RLockIteratorNext(f func(value T) bool) ``` - 示例: ```go func ExampleRing_RLockIteratorNext() { r := gring.New(10) for i := 0; i < 10; i++ { r.Set(i).Next() } r.RLockIteratorNext(func(value interface{}) bool { if value.(int) < 5 { fmt.Println("IteratorNext Success, Value:", value) return true } return false }) // Output: // IteratorNext Success, Value: 0 // IteratorNext Success, Value: 1 // IteratorNext Success, Value: 2 // IteratorNext Success, Value: 3 // IteratorNext Success, Value: 4 } // 泛型方式 func ExampleTRing_RLockIteratorNext() { r := gring.NewTRing[int](10) for i := 0; i < 10; i++ { r.Set(i).Next() } // 无需类型断言 r.RLockIteratorNext(func(value int) bool { if value < 5 { fmt.Println("IteratorNext Success, Value:", value) return true } return false }) // Output: // IteratorNext Success, Value: 0 // IteratorNext Success, Value: 1 // IteratorNext Success, Value: 2 // IteratorNext Success, Value: 3 // IteratorNext Success, Value: 4 } ``` ### `RLockIteratorPrev` - 说明:`RLockIteratorPrev` 使用给定的回调函数 `f` 在 `RWMutex.RLock` 内向后迭代并锁定读取。如果 `f` 返回 `true`,则继续迭代;或者返回 `false` 停止。 - 格式: ```go // 传统方式 RLockIteratorPrev(f func(value interface{}) bool) // 泛型方式 (v2.10.0+) RLockIteratorPrev(f func(value T) bool) ``` - 示例: ```go func ExampleRing_RLockIteratorPrev() { r := gring.New(10) for i := 0; i < 10; i++ { r.Set(i).Next() } // move r to pos 9 r.Prev() r.RLockIteratorPrev(func(value interface{}) bool { if value.(int) >= 5 { fmt.Println("IteratorPrev Success, Value:", value) return true } return false }) // Output: // IteratorPrev Success, Value: 9 // IteratorPrev Success, Value: 8 // IteratorPrev Success, Value: 7 // IteratorPrev Success, Value: 6 // IteratorPrev Success, Value: 5 } ``` ## 切片方法 ### `SliceNext` - 说明:`SliceNext` 从当前位置开始向前返回所有项值的副本作为切片。 - 格式: ```go // 传统方式 SliceNext() []interface{} // 泛型方式 (v2.10.0+) SliceNext() []T ``` - 示例: ```go func ExampleRing_SliceNext() { r := gring.New(10) for i := 0; i < 10; i++ { r.Set(i).Next() } fmt.Println(r.SliceNext()) // Output: // [0 1 2 3 4 5 6 7 8 9] } // 泛型方式 func ExampleTRing_SliceNext() { r := gring.NewTRing[string](5) words := []string{"Go", "Frame", "is", "awesome", "!"} for _, word := range words { r.Put(word) } r.Move(-5) fmt.Println(r.SliceNext()) // Output: // [Go Frame is awesome !] } ``` ### `SlicePrev` - 说明:`SlicePrev` 从当前位置开始向后返回所有项值的副本作为切片。 - 格式: ```go // 传统方式 SlicePrev() []interface{} // 泛型方式 (v2.10.0+) SlicePrev() []T ``` - 示例: ```go func ExampleRing_SlicePrev() { r := gring.New(10) for i := 0; i < 10; i++ { r.Set(i).Next() } fmt.Println(r.SlicePrev()) // Output: // [0 9 8 7 6 5 4 3 2 1] } // 泛型方式 func ExampleTRing_SlicePrev() { r := gring.NewTRing[int](10) for i := 0; i < 10; i++ { r.Set(i).Next() } fmt.Println(r.SlicePrev()) // Output: // [0 9 8 7 6 5 4 3 2 1] } ``` # 内存锁-gmlock 内存锁模块,也称之为 `动态互斥锁` 模块,支持按照 `给定键名动态生成互斥锁`,并发安全并支持 `Try*Lock` 特性。 > 当维护大量动态互斥锁的场景时,如果不再使用的互斥锁对象,请手动调用 `Remove` 方法删除掉。 **使用方式**: ```go import "github.com/gogf/gf/v2/os/gmlock" ``` **使用场景**: 需要 `动态创建互斥锁`,或者需要 `维护大量动态锁` 的场景; **接口文档**: [https://pkg.go.dev/github.com/gogf/gf/v2/os/gmlock](https://pkg.go.dev/github.com/gogf/gf/v2/os/gmlock) ```go func Lock(key string) func LockFunc(key string, f func()) func RLock(key string) func RLockFunc(key string, f func()) func RUnlock(key string) func Remove(key string) func TryLock(key string) bool func TryLockFunc(key string, f func()) bool func TryRLock(key string) bool func TryRLockFunc(key string, f func()) bool func Unlock(key string) type Locker func New() *Locker func (l *Locker) Clear() func (l *Locker) Lock(key string) func (l *Locker) LockFunc(key string, f func()) func (l *Locker) RLock(key string) func (l *Locker) RLockFunc(key string, f func()) func (l *Locker) RUnlock(key string) func (l *Locker) Remove(key string) func (l *Locker) TryLock(key string) bool func (l *Locker) TryLockFunc(key string, f func()) bool func (l *Locker) TryRLock(key string) bool func (l *Locker) TryRLockFunc(key string, f func()) bool func (l *Locker) Unlock(key string) ``` ### 示例1,基本使用 ```go package main import ( "time" "sync" "github.com/gogf/gf/v2/os/glog" "github.com/gogf/gf/v2/os/gmlock" ) func main() { key := "lock" wg := sync.WaitGroup{} for i := 0; i < 10; i++ { wg.Add(1) go func(i int) { gmlock.Lock(key) glog.Println(i) time.Sleep(time.Second) gmlock.Unlock(key) wg.Done() }(i) } wg.Wait() } ``` 该示例中,模拟了同时开启 `10` 个 `goroutine`,但同一时刻只能有一个 `goroutine` 获得锁,获得锁的 `goroutine` 执行 `1` 秒后退出,其他 `goroutine` 才能获得锁。 执行后,输出结果为: ```html 2018-10-15 23:57:28.295 9 2018-10-15 23:57:29.296 0 2018-10-15 23:57:30.296 1 2018-10-15 23:57:31.296 2 2018-10-15 23:57:32.296 3 2018-10-15 23:57:33.297 4 2018-10-15 23:57:34.297 5 2018-10-15 23:57:35.297 6 2018-10-15 23:57:36.298 7 2018-10-15 23:57:37.298 8 ``` ### 示例2,TryLock非阻塞锁 `TryLock` 方法是有返回值的,它表示用来尝试获取锁,如果获取成功,则返回 `true`;如果获取失败(即互斥锁已被其他 `goroutine` 获取),则返回 `false`。 ```go package main import ( "sync" "github.com/gogf/gf/v2/os/glog" "time" "github.com/gogf/gf/v2/os/gmlock" ) func main() { key := "lock" wg := sync.WaitGroup{} for i := 0; i < 10; i++ { wg.Add(1) go func(i int) { if gmlock.TryLock(key) { glog.Println(i) time.Sleep(time.Second) gmlock.Unlock(key) } else { glog.Println(false) } wg.Done() }(i) } wg.Wait() } ``` 同理,在该示例中,同时也只有 `1` 个 `goroutine` 能获得锁,其他 `goroutine` 在 `TryLock` 失败便直接退出了。 执行后,输出结果为: ```html 2018-10-16 00:01:59.172 9 2018-10-16 00:01:59.172 false 2018-10-16 00:01:59.172 false 2018-10-16 00:01:59.172 false 2018-10-16 00:01:59.172 false 2018-10-16 00:01:59.172 false 2018-10-16 00:01:59.172 false 2018-10-16 00:01:59.172 false 2018-10-16 00:01:59.172 false 2018-10-16 00:01:59.176 false ``` # 互斥锁-gmutex `gmutex.Mutex` 互斥锁对象支持读写控制,互斥锁功能逻辑与标准库 `sync.RWMutex` 类似,可并发读但不可并发写。 > 互斥锁的设计细节,推荐阅读轻量级高清版的实现源码: [https://github.com/gogf/gf/blob/master/os/gmutex/gmutex.go](https://github.com/gogf/gf/blob/master/os/gmutex/gmutex.go) **使用方式**: ```go import "github.com/gogf/gf/v2/os/gmutex" ``` **接口文档**: [https://pkg.go.dev/github.com/gogf/gf/v2/os/gmutex](https://pkg.go.dev/github.com/gogf/gf/v2/os/gmutex) ```go type Mutex func (m *Mutex) LockFunc(f func()) func (m *Mutex) TryLockFunc(f func()) (result bool) type RWMutex func New() *RWMutex func (m *RWMutex) LockFunc(f func()) func (m *RWMutex) RLockFunc(f func()) func (m *RWMutex) TryLockFunc(f func()) (result bool) func (m *RWMutex) TryRLockFunc(f func()) (result bool) ``` 1. 该互斥锁模块最大的特点是支持 `Try*` 方法以及 `*Func` 方法。 2. `Try*` 方法用于实现尝试获得特定类型的锁,如果获得锁成功则立即返回 `true`,否则立即返回 `false`,不会阻塞等待,这对于需要使用非阻塞锁机制的业务逻辑非常实用。 3. `*Func` 方法使用闭包匿名函数的方式实现特定作用域的并发安全锁控制,这对于特定代码块的并发安全控制特别方便,由于内部使用了 `defer` 来释放锁,因此即使函数内部产生异常错误,也不会影响锁机制的安全性控制。 ### 基准测试 `gmutex.Mutex` 与标准库的 `sync.Mutex` 及 `sync.RWMutex` 的基准测试对比结果: [gmutex\_bench\_test.go](https://github.com/gogf/gf/blob/master/os/gmutex/gmutex_bench_test.go) ``` goos: linux goarch: amd64 pkg: github.com/gogf/gf/v2/os/gmutex Benchmark_Mutex_LockUnlock-4 50000000 31.5 ns/op Benchmark_RWMutex_LockUnlock-4 30000000 54.1 ns/op Benchmark_RWMutex_RLockRUnlock-4 50000000 27.9 ns/op Benchmark_GMutex_LockUnlock-4 50000000 27.2 ns/op Benchmark_GMutex_TryLock-4 100000000 16.7 ns/op Benchmark_GMutex_RLockRUnlock-4 50000000 38.0 ns/op Benchmark_GMutex_TryRLock-4 100000000 16.8 ns/op ``` ### 示例1,基本使用 ```go package main import ( "time" "github.com/gogf/gf/v2/os/gctx" "github.com/gogf/gf/v2/os/glog" "github.com/gogf/gf/v2/os/gmutex" ) func main() { ctx := gctx.New() mu := gmutex.New() for i := 0; i < 10; i++ { go func(n int) { mu.Lock() defer mu.Unlock() glog.Print(ctx, "Lock:", n) time.Sleep(time.Second) }(i) } for i := 0; i < 10; i++ { go func(n int) { mu.RLock() defer mu.RUnlock() glog.Print(ctx, "RLock:", n) time.Sleep(time.Second) }(i) } time.Sleep(15 * time.Second) } ``` 执行后,终端输出: ```html 2019-07-13 16:19:55.417 Lock: 0 2019-07-13 16:19:56.421 Lock: 1 2019-07-13 16:19:57.424 RLock: 0 2019-07-13 16:19:57.424 RLock: 4 2019-07-13 16:19:57.425 RLock: 8 2019-07-13 16:19:57.425 RLock: 2 2019-07-13 16:19:57.425 RLock: 7 2019-07-13 16:19:57.425 RLock: 5 2019-07-13 16:19:57.425 RLock: 9 2019-07-13 16:19:57.425 RLock: 1 2019-07-13 16:19:57.425 RLock: 6 2019-07-13 16:19:57.425 RLock: 3 2019-07-13 16:19:58.429 Lock: 3 2019-07-13 16:19:59.433 Lock: 4 2019-07-13 16:20:00.438 Lock: 5 2019-07-13 16:20:01.443 Lock: 6 2019-07-13 16:20:02.448 Lock: 7 2019-07-13 16:20:03.452 Lock: 8 2019-07-13 16:20:04.456 Lock: 9 2019-07-13 16:20:05.461 Lock: 2 ``` 这里使用 `glog` 打印的目的,是可以方便地看到打印输出的时间。可以看到,在第3秒的时候,读锁抢占到了机会,由于 `gmutex.Mutex` 对象支持并发读但不支持并发写,因此读锁抢占后迅速执行完毕;而写锁依旧保持每秒打印一条日志继续执行。 ### 示例2, `*Func` 使用 ```go package main import ( "time" "github.com/gogf/gf/v2/os/glog" "github.com/gogf/gf/v2/os/gmutex" ) func main() { mu := gmutex.New() go mu.LockFunc(func() { glog.Println("lock func1") time.Sleep(1 * time.Second) }) time.Sleep(time.Millisecond) go mu.LockFunc(func() { glog.Println("lock func2") }) time.Sleep(2 * time.Second) } ``` 执行后,终端输出: ```html 2019-07-13 16:28:10.381 lock func1 2019-07-13 16:28:11.385 lock func2 ``` 可以看到,使用 `*Func` 方法实现特定作用域的锁控制非常方便。 # 上下文-gctx ## 基本介绍 `gctx` 组件实现了对上下文操作的封装,用以简化链路跟踪以及进程初始化上下文对象的管理。 ## 常用方法 这里只简单介绍几个常用的方法,其他方法请查看组件API文档或者源码。 ### `New` 用以创建一个带有链路跟踪能力的上下文对象。 ### `GetInitCtx` 用在启动进程或者init包方法中,用于获取当前进程带有链路跟踪能力的上下文对象,也用于进程间传递链路跟踪信息。 # 日志管理-glog 日志管理功能由 `glog` 模块实现,具体请参考 [日志组件](../core/glog.md) 章节。 # 缓存管理-gcache 缓存管理由`gcache`模块实现,具体请参考 [缓存管理](../core/gcache.md) 章节。 # 文件管理-gfile ## 基本介绍 `gfile` 文件管理组件提供了更加丰富的文件/目录操作能力。 **使用方式**: ```go import "github.com/gogf/gf/v2/os/gfile" ``` **接口文档**: [https://pkg.go.dev/github.com/gogf/gf/v2/os/gfile](https://pkg.go.dev/github.com/gogf/gf/v2/os/gfile) **提示** 以下常用方法列表,文档更新可能滞后于代码新特性,更多的方法及示例请参考代码文档: [https://pkg.go.dev/github.com/gogf/gf/v2/os/gfile](https://pkg.go.dev/github.com/gogf/gf/v2/os/gfile) ## 内容管理 ### `GetContents` - 说明:读取指定路径文件内容,以字符串形式返回。 - 格式: ```go func GetContents(path string) string ``` - 示例: ```go func ExampleGetContents() { // init var ( fileName = "gfile_example.txt" tempDir = gfile.TempDir("gfile_example_content") tempFile = gfile.Join(tempDir, fileName) ) // write contents gfile.PutContents(tempFile, "goframe example content") // It reads and returns the file content as string. // It returns empty string if it fails reading, for example, with permission or IO error. fmt.Println(gfile.GetContents(tempFile)) // Output: // goframe example content } ``` ### `GetContentsWithCache` - 说明:带缓存获取文件内容,可设置缓存超时,文件发生变化自动清除缓存。 - 格式: ```go func GetContentsWithCache(path string, duration ...time.Duration) string ``` - 示例: ```go func ExampleGetContentsWithCache() { // init var ( fileName = "gfile_example.txt" tempDir = gfile.TempDir("gfile_example_cache") tempFile = gfile.Join(tempDir, fileName) ) // write contents gfile.PutContents(tempFile, "goframe example content") // It reads the file content with cache duration of one minute, // which means it reads from cache after then without any IO operations within on minute. fmt.Println(gfile.GetContentsWithCache(tempFile, time.Minute)) // write new contents will clear its cache gfile.PutContents(tempFile, "new goframe example content") // There's some delay for cache clearing after file content change. time.Sleep(time.Second * 1) // read contents fmt.Println(gfile.GetContentsWithCache(tempFile)) // May Output: // goframe example content // new goframe example content } ``` ### `GetBytesWithCache` - 说明:带缓存获取文件内容,可设置缓存超时,文件发生变化自动清除缓存,返回\[\]byte。 - 格式: ```go func GetBytesWithCache(path string, duration ...time.Duration) []byte ``` - 示例: ```go func ExampleGetBytesWithCache() { // init var ( fileName = "gfile_example.txt" tempDir = gfile.TempDir("gfile_example_cache") tempFile = gfile.Join(tempDir, fileName) ) // write contents gfile.PutContents(tempFile, "goframe example content") // It reads the file content with cache duration of one minute, // which means it reads from cache after then without any IO operations within on minute. fmt.Println(gfile.GetBytesWithCache(tempFile, time.Minute)) // write new contents will clear its cache gfile.PutContents(tempFile, "new goframe example content") // There's some delay for cache clearing after file content change. time.Sleep(time.Second * 1) // read contents fmt.Println(gfile.GetBytesWithCache(tempFile)) // Output: // [103 111 102 114 97 109 101 32 101 120 97 109 112 108 101 32 99 111 110 116 101 110 116] // [110 101 119 32 103 111 102 114 97 109 101 32 101 120 97 109 112 108 101 32 99 111 110 116 101 110 116] } ``` ### `GetBytes` - 说明:读取指定路径文件内容,以字节形式返回。 - 格式: ```go func GetBytes(path string) []byte ``` - 示例: ```go func ExampleGetBytes() { // init var ( fileName = "gfile_example.txt" tempDir = gfile.TempDir("gfile_example_content") tempFile = gfile.Join(tempDir, fileName) ) // write contents gfile.PutContents(tempFile, "goframe example content") // It reads and returns the file content as []byte. // It returns nil if it fails reading, for example, with permission or IO error. fmt.Println(gfile.GetBytes(tempFile)) // Output: // [103 111 102 114 97 109 101 32 101 120 97 109 112 108 101 32 99 111 110 116 101 110 116] } ``` ### `GetBytesTilChar` - 说明:以某个字符定位截取指定长度的文件内容以字节形式返回 - 格式: ```go func GetBytesTilChar(reader io.ReaderAt, char byte, start int64) ([]byte, int64) ``` - 示例: ```go func ExampleGetBytesTilChar() { // init var ( fileName = "gfile_example.txt" tempDir = gfile.TempDir("gfile_example_content") tempFile = gfile.Join(tempDir, fileName) ) // write contents gfile.PutContents(tempFile, "goframe example content") f, _ := gfile.OpenWithFlagPerm(tempFile, os.O_RDONLY, gfile.DefaultPermOpen) // GetBytesTilChar returns the contents of the file as []byte // until the next specified byte `char` position. char, i := gfile.GetBytesTilChar(f, 'f', 0) fmt.Println(char) fmt.Println(i) // Output: // [103 111 102] // 2 } ``` ### `GetBytesByTwoOffsets` - 说明:以指定的区间读取文件内容 - 格式: ```go func GetBytesByTwoOffsets(reader io.ReaderAt, start int64, end int64) []byte ``` - 示例: ```go func ExampleGetBytesByTwoOffsets() { // init var ( fileName = "gfile_example.txt" tempDir = gfile.TempDir("gfile_example_content") tempFile = gfile.Join(tempDir, fileName) ) // write contents gfile.PutContents(tempFile, "goframe example content") f, _ := gfile.OpenWithFlagPerm(tempFile, os.O_RDONLY, gfile.DefaultPermOpen) // GetBytesTilChar returns the contents of the file as []byte // until the next specified byte `char` position. char := gfile.GetBytesByTwoOffsets(f, 0, 3) fmt.Println(char) // Output: // [103 111 102] } ``` ### `PutContents` - 说明:往指定路径文件添加字符串内容。如果文件不存在将会递归的形式自动创建。 - 格式: ```go func putContents(path string, data []byte, flag int, perm os.FileMode) error ``` - 示例: ```go func ExamplePutContents() { // init var ( fileName = "gfile_example.txt" tempDir = gfile.TempDir("gfile_example_content") tempFile = gfile.Join(tempDir, fileName) ) // It creates and puts content string into specifies file path. // It automatically creates directory recursively if it does not exist. gfile.PutContents(tempFile, "goframe example content") // read contents fmt.Println(gfile.GetContents(tempFile)) // Output: // goframe example content } ``` ### `PutBytes` - 说明:以字节形式写入指定文件,如果文件不存在将会递归的形式自动创建 - 格式: ```go func PutBytes(path string, content []byte) error ``` - 示例: ```go func ExamplePutBytes() { // init var ( fileName = "gfile_example.txt" tempDir = gfile.TempDir("gfile_example_content") tempFile = gfile.Join(tempDir, fileName) ) // write contents gfile.PutBytes(tempFile, []byte("goframe example content")) // read contents fmt.Println(gfile.GetContents(tempFile)) // Output: // goframe example content } ``` ### `PutContentsAppend` - 说明:追加字符串内容到指定文件,如果文件不存在将会递归的形式自动创建。 - 格式: ```go func PutContentsAppend(path string, content string) error ``` - 示例: ```go func ExamplePutContentsAppend() { // init var ( fileName = "gfile_example.txt" tempDir = gfile.TempDir("gfile_example_content") tempFile = gfile.Join(tempDir, fileName) ) // write contents gfile.PutContents(tempFile, "goframe example content") // read contents fmt.Println(gfile.GetContents(tempFile)) // It creates and append content string into specifies file path. // It automatically creates directory recursively if it does not exist. gfile.PutContentsAppend(tempFile, " append content") // read contents fmt.Println(gfile.GetContents(tempFile)) // Output: // goframe example content // goframe example content append content } ``` ### `PutBytesAppend` - 说明:追加字节内容到指定文件。如果文件不存在将会递归的形式自动创建。 - 格式: ```go func PutBytesAppend(path string, content []byte) error ``` - 示例: ```go func ExamplePutBytesAppend() { // init var ( fileName = "gfile_example.txt" tempDir = gfile.TempDir("gfile_example_content") tempFile = gfile.Join(tempDir, fileName) ) // write contents gfile.PutContents(tempFile, "goframe example content") // read contents fmt.Println(gfile.GetContents(tempFile)) // write contents gfile.PutBytesAppend(tempFile, []byte(" append")) // read contents fmt.Println(gfile.GetContents(tempFile)) // Output: // goframe example content // goframe example content append } ``` ### `GetNextCharOffset` - 说明:从某个偏移量开始,获取文件中指定字符所在下标 - 格式: ```go func GetNextCharOffset(reader io.ReaderAt, char byte, start int64) int64 ``` - 示例: ```go func ExampleGetNextCharOffset() { // init var ( fileName = "gfile_example.txt" tempDir = gfile.TempDir("gfile_example_content") tempFile = gfile.Join(tempDir, fileName) ) // write contents gfile.PutContents(tempFile, "goframe example content") f, err := gfile.OpenWithFlagPerm(tempFile, os.O_RDONLY, DefaultPermOpen) defer f.Close() // read contents index := gfile.GetNextCharOffset(f, 'f', 0) fmt.Println(index) // Output: // 2 } ``` ### `GetNextCharOffsetByPath` - 说明:从某个偏移量开始,获取文件中指定字符所在下标 - 格式: ```go func GetNextCharOffsetByPath(path string, char byte, start int64) int64 ``` - 示例: ```go func ExampleGetNextCharOffsetByPath() { // init var ( fileName = "gfile_example.txt" tempDir = gfile.TempDir("gfile_example_content") tempFile = gfile.Join(tempDir, fileName) ) // write contents gfile.PutContents(tempFile, "goframe example content") // read contents index := gfile.GetNextCharOffsetByPath(tempFile, 'f', 0) fmt.Println(index) // Output: // 2 } ``` ### `GetBytesTilCharByPath` - 说明:以某个字符定位截取指定长度的文件内容以字节形式返回 - 格式: ```go func GetBytesTilCharByPath(path string, char byte, start int64) ([]byte, int64) ``` - 示例: ```go func ExampleGetBytesTilCharByPath() { // init var ( fileName = "gfile_example.txt" tempDir = gfile.TempDir("gfile_example_content") tempFile = gfile.Join(tempDir, fileName) ) // write contents gfile.PutContents(tempFile, "goframe example content") // read contents fmt.Println(gfile.GetBytesTilCharByPath(tempFile, 'f', 0)) // Output: // [103 111 102] 2 } ``` ### `GetBytesByTwoOffsetsByPath` - 说明:用两个偏移量截取指定文件的内容以字节形式返回 - 格式: ```go func GetBytesByTwoOffsetsByPath(path string, start int64, end int64) []byte ``` - 示例: ```go func ExampleGetBytesByTwoOffsetsByPath() { // init var ( fileName = "gfile_example.txt" tempDir = gfile.TempDir("gfile_example_content") tempFile = gfile.Join(tempDir, fileName) ) // write contents gfile.PutContents(tempFile, "goframe example content") // read contents fmt.Println(gfile.GetBytesByTwoOffsetsByPath(tempFile, 0, 7)) // Output: // [103 111 102 114 97 109 101] } ``` ### `ReadLines` - 说明:以字符串形式逐行读取文件内容 - 格式: ```go func ReadLines(file string, callback func(text string) error) error ``` - 示例: ```go func ExampleReadLines() { // init var ( fileName = "gfile_example.txt" tempDir = gfile.TempDir("gfile_example_content") tempFile = gfile.Join(tempDir, fileName) ) // write contents gfile.PutContents(tempFile, "L1 goframe example content\nL2 goframe example content") // read contents gfile.ReadLines(tempFile, func(text string) error { // Process each line fmt.Println(text) return nil }) // Output: // L1 goframe example content // L2 goframe example content } ``` ### `ReadLinesBytes` - 说明:以字节形式逐行读取文件内容 - 格式: ```go func ReadLinesBytes(file string, callback func(bytes []byte) error) error ``` - 示例: ```go func ExampleReadLinesBytes() { // init var ( fileName = "gfile_example.txt" tempDir = gfile.TempDir("gfile_example_content") tempFile = gfile.Join(tempDir, fileName) ) // write contents gfile.PutContents(tempFile, "L1 goframe example content\nL2 goframe example content") // read contents gfile.ReadLinesBytes(tempFile, func(bytes []byte) error { // Process each line fmt.Println(bytes) return nil }) // Output: // [76 49 32 103 111 102 114 97 109 101 32 101 120 97 109 112 108 101 32 99 111 110 116 101 110 116] // [76 50 32 103 111 102 114 97 109 101 32 101 120 97 109 112 108 101 32 99 111 110 116 101 110 116] } ``` ### `Truncate` - 说明:裁剪文件为指定大小 - 注意:如果给定文件路径是软链,将会修改源文件 - 格式: ```go func Truncate(path string, size int) error ``` - 示例: ```go func ExampleTruncate(){ // init var ( path = gfile.Join(gfile.TempDir("gfile_example_basic_dir"), "file1") ) // Check whether the `path` size stat, _ := gfile.Stat(path) fmt.Println(stat.Size()) // Truncate file gfile.Truncate(path, 0) // Check whether the `path` size stat, _ = gfile.Stat(path) fmt.Println(stat.Size()) // Output: // 13 // 0 } ``` ## 内容替换 ### `ReplaceFile` - 说明:替换指定文件的指定内容为新内容 - 格式: ```go func ReplaceFile(search, replace, path string) error ``` - 示例: ```go func ExampleReplaceFile() { // init var ( fileName = "gfile_example.txt" tempDir = gfile.TempDir("gfile_example_replace") tempFile = gfile.Join(tempDir, fileName) ) // write contents gfile.PutContents(tempFile, "goframe example content") // read contents fmt.Println(gfile.GetContents(tempFile)) // It replaces content directly by file path. gfile.ReplaceFile("content", "replace word", tempFile) fmt.Println(gfile.GetContents(tempFile)) // Output: // goframe example content // goframe example replace word } ``` ### `ReplaceFileFunc` - 说明:使用自定义函数替换指定文件内容 - 格式: ```go func ReplaceFileFunc(f func(path, content string) string, path string) error ``` - 示例: ```go func ExampleReplaceFileFunc() { // init var ( fileName = "gfile_example.txt" tempDir = gfile.TempDir("gfile_example_replace") tempFile = gfile.Join(tempDir, fileName) ) // write contents gfile.PutContents(tempFile, "goframe example 123") // read contents fmt.Println(gfile.GetContents(tempFile)) // It replaces content directly by file path and callback function. gfile.ReplaceFileFunc(func(path, content string) string { // Replace with regular match reg, _ := regexp.Compile(`\d{3}`) return reg.ReplaceAllString(content, "[num]") }, tempFile) fmt.Println(gfile.GetContents(tempFile)) // Output: // goframe example 123 // goframe example [num] } ``` ### `ReplaceDir` - 说明:扫描指定目录,替换符合条件的文件的指定内容为新内容 - 格式: ```go func ReplaceDir(search, replace, path, pattern string, recursive ...bool) error ``` - 示例: ```go func ExampleReplaceDir() { // init var ( fileName = "gfile_example.txt" tempDir = gfile.TempDir("gfile_example_replace") tempFile = gfile.Join(tempDir, fileName) ) // write contents gfile.PutContents(tempFile, "goframe example content") // read contents fmt.Println(gfile.GetContents(tempFile)) // It replaces content of all files under specified directory recursively. gfile.ReplaceDir("content", "replace word", tempDir, "gfile_example.txt", true) // read contents fmt.Println(gfile.GetContents(tempFile)) // Output: // goframe example content // goframe example replace word } ``` ### `ReplaceDirFunc` - 说明:扫描指定目录,使用自定义函数替换符合条件的文件的指定内容为新内容 - 格式: ```go func ReplaceDirFunc(f func(path, content string) string, path, pattern string, recursive ...bool) error ``` - 示例: ```go func ExampleReplaceDirFunc() { // init var ( fileName = "gfile_example.txt" tempDir = gfile.TempDir("gfile_example_replace") tempFile = gfile.Join(tempDir, fileName) ) // write contents gfile.PutContents(tempFile, "goframe example 123") // read contents fmt.Println(gfile.GetContents(tempFile)) // It replaces content of all files under specified directory with custom callback function recursively. gfile.ReplaceDirFunc(func(path, content string) string { // Replace with regular match reg, _ := regexp.Compile(`\d{3}`) return reg.ReplaceAllString(content, "[num]") }, tempDir, "gfile_example.txt", true) fmt.Println(gfile.GetContents(tempFile)) // Output: // goframe example 123 // goframe example [num] } ``` ## 文件时间 ### `MTime` - 说明:获取路径修改时间 - 格式: ```go func MTime(path string) time.Time ``` - 示例: ```go func ExampleMTime() { t := gfile.MTime(gfile.TempDir()) fmt.Println(t) // May Output: // 2021-11-02 15:18:43.901141 +0800 CST } ``` ### `MTimestamp` - 说明:获取路径修改时间戳(秒) - 格式: ```go func MTimestamp(path string) int64 ``` - 示例: ```go func ExampleMTimestamp() { t := gfile.MTimestamp(gfile.TempDir()) fmt.Println(t) // May Output: // 1635838398 } ``` ### `MTimestampMilli` - 说明:获取路径修改时间戳(毫秒) - 格式: ```go func MTimestampMilli(path string) int64 ``` - 示例: ```go func ExampleMTimestampMilli() { t := gfile.MTimestampMilli(gfile.TempDir()) fmt.Println(t) // May Output: // 1635838529330 } ``` ## 文件大小 ### `Size` - 说明:获取路径大小,不进行格式化 - 格式: ```go func Size(path string) int64 ``` - 示例: ```go func ExampleSize() { // init var ( fileName = "gfile_example.txt" tempDir = gfile.TempDir("gfile_example_size") tempFile = gfile.Join(tempDir, fileName) ) // write contents gfile.PutContents(tempFile, "0123456789") fmt.Println(gfile.Size(tempFile)) // Output: // 10 } ``` ### `SizeFormat` - 说明:获取路径大小,并格式化成硬盘容量 - 格式: ```go func SizeFormat(path string) string ``` - 示例: ```go func ExampleSizeFormat() { // init var ( fileName = "gfile_example.txt" tempDir = gfile.TempDir("gfile_example_size") tempFile = gfile.Join(tempDir, fileName) ) // write contents gfile.PutContents(tempFile, "0123456789") fmt.Println(gfile.SizeFormat(tempFile)) // Output: // 10.00B } ``` ### `ReadableSize` - 说明:获取给定路径容量大小,并格式化人类易读的硬盘容量格式 - 格式: ```go func ReadableSize(path string) string ``` - 示例: ```go func ExampleReadableSize() { // init var ( fileName = "gfile_example.txt" tempDir = gfile.TempDir("gfile_example_size") tempFile = gfile.Join(tempDir, fileName) ) // write contents gfile.PutContents(tempFile, "01234567899876543210") fmt.Println(gfile.ReadableSize(tempFile)) // Output: // 20.00B } ``` ### `StrToSize` - 说明:硬盘容量大小字符串转换为大小整形 - 格式: ```go func StrToSize(sizeStr string) int64 ``` - 示例: ```go func ExampleStrToSize() { size := gfile.StrToSize("100MB") fmt.Println(size) // Output: // 104857600 } ``` ### `FormatSize` - 说明:大小整形转换为硬盘容量大小字符串\`K、m、g、t、p、e、b\` - 格式: ```go func FormatSize(raw int64) string ``` - 示例: ```go func ExampleFormatSize() { sizeStr := gfile.FormatSize(104857600) fmt.Println(sizeStr) sizeStr0 := gfile.FormatSize(1024) fmt.Println(sizeStr0) sizeStr1 := gfile.FormatSize(999999999999999999) fmt.Println(sizeStr1) // Output: // 100.00M // 1.00K // 888.18P } ``` ## 文件排序 ### `SortFiles` - 说明:排序多个路径,按首字母进行排序,数字优先。 - 格式: ```go func SortFiles(files []string) []string ``` - 示例: ```go func ExampleSortFiles() { files := []string{ "/aaa/bbb/ccc.txt", "/aaa/bbb/", "/aaa/", "/aaa", "/aaa/ccc/ddd.txt", "/bbb", "/0123", "/ddd", "/ccc", } sortOut := gfile.SortFiles(files) fmt.Println(sortOut) // Output: // [/0123 /aaa /aaa/ /aaa/bbb/ /aaa/bbb/ccc.txt /aaa/ccc/ddd.txt /bbb /ccc /ddd] } ``` ## 文件检索 ### `Search` - 说明:在指定目录(默认包含当前目录、运行目录、主函数目录;不会递归子目录)中搜索文件并返回真实路径。 - 格式: ```go func Search(name string, prioritySearchPaths ...string) (realPath string, err error) ``` - 示例: ```go func ExampleSearch() { // init var ( fileName = "gfile_example.txt" tempDir = gfile.TempDir("gfile_example_search") tempFile = gfile.Join(tempDir, fileName) ) // write contents gfile.PutContents(tempFile, "goframe example content") // search file realPath, _ := gfile.Search(fileName, tempDir) fmt.Println(gfile.Basename(realPath)) // Output: // gfile_example.txt } ``` ## 目录扫描 ### `ScanDir` - 说明:扫描指定目录,可扫描文件或目录,支持递归扫描。 - 格式: ```go func ScanDir(path string, pattern string, recursive ...bool) ([]string, error) ``` - 示例: ```go func ExampleScanDir() { // init var ( fileName = "gfile_example.txt" tempDir = gfile.TempDir("gfile_example_scan_dir") tempFile = gfile.Join(tempDir, fileName) tempSubDir = gfile.Join(tempDir, "sub_dir") tempSubFile = gfile.Join(tempSubDir, fileName) ) // write contents gfile.PutContents(tempFile, "goframe example content") gfile.PutContents(tempSubFile, "goframe example content") // scans directory recursively list, _ := gfile.ScanDir(tempDir, "*", true) for _, v := range list { fmt.Println(gfile.Basename(v)) } // Output: // gfile_example.txt // sub_dir // gfile_example.txt } ``` ### `ScanDirFile` - 说明:扫描指定目录的文件,支持递归扫描 - 格式: ```go func ScanDirFile(path string, pattern string, recursive ...bool) ([]string, error) ``` - 示例: ```go func ExampleScanDirFile() { // init var ( fileName = "gfile_example.txt" tempDir = gfile.TempDir("gfile_example_scan_dir_file") tempFile = gfile.Join(tempDir, fileName) tempSubDir = gfile.Join(tempDir, "sub_dir") tempSubFile = gfile.Join(tempSubDir, fileName) ) // write contents gfile.PutContents(tempFile, "goframe example content") gfile.PutContents(tempSubFile, "goframe example content") // scans directory recursively exclusive of directories list, _ := gfile.ScanDirFile(tempDir, "*.txt", true) for _, v := range list { fmt.Println(gfile.Basename(v)) } // Output: // gfile_example.txt // gfile_example.txt } ``` ### `ScanDirFunc` - 说明:扫描指定目录(自定义过滤方法),可扫描文件或目录,支持递归扫描 - 格式: ```go func ScanDirFunc(path string, pattern string, recursive bool, handler func(path string) string) ([]string, error) ``` - 示例: ```go func ExampleScanDirFunc() { // init var ( fileName = "gfile_example.txt" tempDir = gfile.TempDir("gfile_example_scan_dir_func") tempFile = gfile.Join(tempDir, fileName) tempSubDir = gfile.Join(tempDir, "sub_dir") tempSubFile = gfile.Join(tempSubDir, fileName) ) // write contents gfile.PutContents(tempFile, "goframe example content") gfile.PutContents(tempSubFile, "goframe example content") // scans directory recursively list, _ := gfile.ScanDirFunc(tempDir, "*", true, func(path string) string { // ignores some files if gfile.Basename(path) == "gfile_example.txt" { return "" } return path }) for _, v := range list { fmt.Println(gfile.Basename(v)) } // Output: // sub_dir } ``` ### `ScanDirFileFunc` - 说明:扫描指定目录的文件(自定义过滤方法),支持递归扫描。 - 格式: ```go func ScanDirFileFunc(path string, pattern string, recursive bool, handler func(path string) string) ([]string, error) ``` - 示例: ```go func ExampleScanDirFileFunc() { // init var ( fileName = "gfile_example.txt" tempDir = gfile.TempDir("gfile_example_scan_dir_file_func") tempFile = gfile.Join(tempDir, fileName) fileName1 = "gfile_example_ignores.txt" tempFile1 = gfile.Join(tempDir, fileName1) tempSubDir = gfile.Join(tempDir, "sub_dir") tempSubFile = gfile.Join(tempSubDir, fileName) ) // write contents gfile.PutContents(tempFile, "goframe example content") gfile.PutContents(tempFile1, "goframe example content") gfile.PutContents(tempSubFile, "goframe example content") // scans directory recursively exclusive of directories list, _ := gfile.ScanDirFileFunc(tempDir, "*.txt", true, func(path string) string { // ignores some files if gfile.Basename(path) == "gfile_example_ignores.txt" { return "" } return path }) for _, v := range list { fmt.Println(gfile.Basename(v)) } // Output: // gfile_example.txt // gfile_example.txt } ``` ## 常用目录 ### `Pwd` - 说明:获取当前工作路径。 - 格式: ```go func Pwd() string ``` - 示例: ```go func ExamplePwd() { // Get absolute path of current working directory. fmt.Println(gfile.Pwd()) // May Output: // xxx/gf/os/gfile } ``` ### `Home` - 说明:获取运行用户的主目录 - 格式: ```go func Home(names ...string) (string, error) ``` - 示例: ```go func ExampleHome() { // user's home directory homePath, _ := gfile.Home() fmt.Println(homePath) // May Output: // C:\Users\hailaz } ``` ### `Temp` - 说明:获取拼接系统临时路径后的绝对地址。 - 格式: ```go func Temp(names ...string) string ``` - 示例: ```go func ExampleTempDir() { // init var ( fileName = "gfile_example_basic_dir" ) // fetch an absolute representation of path. path := gfile.Temp(fileName) fmt.Println(path) // Output: // /tmp/gfile_example_basic_dir } ``` ### `SelfPath` - 说明:获取当前运行程序的绝对路径。 - 格式: ```go func SelfPath() string ``` - 示例: ```go func ExampleSelfPath() { // Get absolute file path of current running process fmt.Println(gfile.SelfPath()) // May Output: // xxx/___github_com_gogf_gf_v2_os_gfile__ExampleSelfPath } ``` ## 类型判断 ### `IsDir` - 说明:检查给定的路径是否是文件夹。 - 格式: ```go func IsDir(path string) bool ``` - 示例: ```go func ExampleIsDir() { // init var ( path = gfile.TempDir("gfile_example_basic_dir") filePath = gfile.Join(gfile.TempDir("gfile_example_basic_dir"), "file1") ) // Checks whether given `path` a directory. fmt.Println(gfile.IsDir(path)) fmt.Println(gfile.IsDir(filePath)) // Output: // true // false } ``` ### `IsFile` - 说明:检查给定的路径是否是文件。 - 格式: ```go func IsFile(path string) bool ``` - 示例: ```go func ExampleIsFile() { // init var ( filePath = gfile.Join(gfile.TempDir("gfile_example_basic_dir"), "file1") dirPath = gfile.TempDir("gfile_example_basic_dir") ) // Checks whether given `path` a file, which means it's not a directory. fmt.Println(gfile.IsFile(filePath)) fmt.Println(gfile.IsFile(dirPath)) // Output: // true // false } ``` ## 权限操作 ### `IsReadable` - 说明:检查给定的路径是否可读。 - 格式: ```go func IsReadable(path string) bool ``` - 示例: ```go func ExampleIsReadable() { // init var ( path = gfile.Pwd() + gfile.Separator + "testdata/readline/file.log" ) // Checks whether given `path` is readable. fmt.Println(gfile.IsReadable(path)) // Output: // true } ``` ### `IsWritable` - 说明:检查指定路径是否可写,如果路径是目录,则会创建临时文件检查是否可写,如果是文件则判断是否可以打开 - 格式: ```go func IsWritable(path string) bool ``` - 示例: ```go func ExampleIsWritable() { // init var ( path = gfile.Pwd() + gfile.Separator + "testdata/readline/file.log" ) // Checks whether given `path` is writable. fmt.Println(gfile.IsWritable(path)) // Output: // true } ``` ### `Chmod` - 说明:使用指定的权限,更改指定路径的文件权限。 - 格式: ```go func Chmod(path string, mode os.FileMode) error ``` - 示例: ```go func ExampleChmod() { // init var ( path = gfile.Join(gfile.TempDir("gfile_example_basic_dir"), "file1") ) // Get a FileInfo describing the named file. stat, err := gfile.Stat(path) if err != nil { fmt.Println(err.Error()) } // Show original mode fmt.Println(stat.Mode()) // Change file model gfile.Chmod(path, gfile.DefaultPermCopy) // Get a FileInfo describing the named file. stat, _ = gfile.Stat(path) // Show the modified mode fmt.Println(stat.Mode()) // Output: // -rw-r--r-- // -rwxrwxrwx } ``` ## 文件/目录操作 ### `Mkdir` - 说明:创建文件夹,支持递归创建(建议采用绝对路径),创建后的文件夹权限为: `drwxr-xr-x`。 - 格式: ```go func Mkdir(path string) error ``` - 示例: ```go func ExampleMkdir() { // init var ( path = gfile.TempDir("gfile_example_basic_dir") ) // Creates directory gfile.Mkdir(path) // Check if directory exists fmt.Println(gfile.IsDir(path)) // Output: // true } ``` ### `Create` - 说明:创建文件/文件夹,如果传入的路径中的文件夹不存在,则会自动创建文件夹以及文件,其中创建的文件权限为 `-rw-r–r–`。 - 注意:如果需要创建文件的已存在,则会清空该文件的内容。 - 格式: ```go func Create(path string) (*os.File, error) ``` - 示例: ```go func ExampleCreate() { // init var ( path = gfile.Join(gfile.TempDir("gfile_example_basic_dir"), "file1") dataByte = make([]byte, 50) ) // Check whether the file exists isFile := gfile.IsFile(path) fmt.Println(isFile) // Creates file with given `path` recursively fileHandle, _ := gfile.Create(path) defer fileHandle.Close() // Write some content to file n, _ := fileHandle.WriteString("hello goframe") // Check whether the file exists isFile = gfile.IsFile(path) fmt.Println(isFile) // Reset file uintptr unix.Seek(int(fileHandle.Fd()), 0, 0) // Reads len(b) bytes from the File fileHandle.Read(dataByte) fmt.Println(string(dataByte[:n])) // Output: // false // true // hello goframe } ``` ### `Open` - 说明:以只读的方式打开文件/文件夹。 - 格式: ```go func Open(path string) (*os.File, error) ``` - 示例: ```go func ExampleOpen() { // init var ( path = gfile.Join(gfile.TempDir("gfile_example_basic_dir"), "file1") dataByte = make([]byte, 4096) ) // Open file or directory with READONLY model file, _ := gfile.Open(path) defer file.Close() // Read data n, _ := file.Read(dataByte) fmt.Println(string(dataByte[:n])) // Output: // hello goframe } ``` ### `OpenFile` - 说明:以指定\`flag\`以及\`perm\`的方式打开文件/文件夹。 - 格式: ```go func OpenFile(path string, flag int, perm os.FileMode) (*os.File, error) ``` - 示例: ```go func ExampleOpenFile() { // init var ( path = gfile.Join(gfile.TempDir("gfile_example_basic_dir"), "file1") dataByte = make([]byte, 4096) ) // Opens file/directory with custom `flag` and `perm` // Create if file does not exist,it is created in a readable and writable mode,prem 0777 openFile, _ := gfile.OpenFile(path, os.O_CREATE|os.O_RDWR, gfile.DefaultPermCopy) defer openFile.Close() // Write some content to file writeLength, _ := openFile.WriteString("hello goframe test open file") fmt.Println(writeLength) // Read data unix.Seek(int(openFile.Fd()), 0, 0) n, _ := openFile.Read(dataByte) fmt.Println(string(dataByte[:n])) // Output: // 28 // hello goframe test open file } ``` ### `OpenWithFalg` - 说明:以指定\`flag\`的方式打开文件/文件夹。 - 格式: ```go func OpenWithFlag(path string, flag int) (*os.File, error) ``` - 示例: ```go func ExampleOpenWithFlag() { // init var ( path = gfile.Join(gfile.TempDir("gfile_example_basic_dir"), "file1") dataByte = make([]byte, 4096) ) // Opens file/directory with custom `flag` // Create if file does not exist,it is created in a readable and writable mode with default `perm` is 0666 openFile, _ := gfile.OpenWithFlag(path, os.O_CREATE|os.O_RDWR) defer openFile.Close() // Write some content to file writeLength, _ := openFile.WriteString("hello goframe test open file with flag") fmt.Println(writeLength) // Read data unix.Seek(int(openFile.Fd()), 0, 0) n, _ := openFile.Read(dataByte) fmt.Println(string(dataByte[:n])) // Output: // 38 // hello goframe test open file with flag } ``` ### `OpenWithFalgPerm` - 说明:以指定\`flag\`以及\`perm\`的方式打开文件/文件夹。 - 格式: ```go func OpenWithFlagPerm(path string, flag int, perm os.FileMode) (*os.File, error) ``` - 示例: ```go func ExampleOpenWithFlagPerm() { // init var ( path = gfile.Join(gfile.TempDir("gfile_example_basic_dir"), "file1") dataByte = make([]byte, 4096) ) // Opens file/directory with custom `flag` and `perm` // Create if file does not exist,it is created in a readable and writable mode with `perm` is 0777 openFile, _ := gfile.OpenWithFlagPerm(path, os.O_CREATE|os.O_RDWR, gfile.DefaultPermCopy) defer openFile.Close() // Write some content to file writeLength, _ := openFile.WriteString("hello goframe test open file with flag and perm") fmt.Println(writeLength) // Read data unix.Seek(int(openFile.Fd()), 0, 0) n, _ := openFile.Read(dataByte) fmt.Println(string(dataByte[:n])) // Output: // 38 // hello goframe test open file with flag } ``` ### `Stat` - 说明:获取给定路径的文件详情。 - 格式: ```go func Stat(path string) (os.FileInfo, error) ``` - 示例: ```go func ExampleStat() { // init var ( path = gfile.Join(gfile.TempDir("gfile_example_basic_dir"), "file1") ) // Get a FileInfo describing the named file. stat, _ := gfile.Stat(path) fmt.Println(stat.Name()) fmt.Println(stat.IsDir()) fmt.Println(stat.Mode()) fmt.Println(stat.ModTime()) fmt.Println(stat.Size()) fmt.Println(stat.Sys()) // May Output: // file1 // false // -rwxr-xr-x // 2021-12-02 11:01:27.261441694 +0800 CST // &{16777220 33261 1 8597857090 501 20 0 [0 0 0 0] {1638414088 192363490} {1638414087 261441694} {1638414087 261441694} {1638413480 485068275} 38 8 4096 0 0 0 [0 0]} } ``` ### `Copy` - 说明:支持复制文件或目录 - 格式: ```go func Copy(src string, dst string) error ``` - 示例: ```go func ExampleCopy() { // init var ( srcFileName = "gfile_example.txt" srcTempDir = gfile.TempDir("gfile_example_copy_src") srcTempFile = gfile.Join(srcTempDir, srcFileName) // copy file dstFileName = "gfile_example_copy.txt" dstTempFile = gfile.Join(srcTempDir, dstFileName) // copy dir dstTempDir = gfile.TempDir("gfile_example_copy_dst") ) // write contents gfile.PutContents(srcTempFile, "goframe example copy") // copy file gfile.Copy(srcTempFile, dstTempFile) // read contents after copy file fmt.Println(gfile.GetContents(dstTempFile)) // copy dir gfile.Copy(srcTempDir, dstTempDir) // list copy dir file fList, _ := gfile.ScanDir(dstTempDir, "*", false) for _, v := range fList { fmt.Println(gfile.Basename(v)) } // Output: // goframe example copy // gfile_example.txt // gfile_example_copy.txt } ``` ### `CopyFile` - 说明:复制文件 - 格式: ```go func CopyFile(src, dst string) (err error) ``` - 示例: ```go func ExampleCopyFile() { // init var ( srcFileName = "gfile_example.txt" srcTempDir = gfile.TempDir("gfile_example_copy_src") srcTempFile = gfile.Join(srcTempDir, srcFileName) // copy file dstFileName = "gfile_example_copy.txt" dstTempFile = gfile.Join(srcTempDir, dstFileName) ) // write contents gfile.PutContents(srcTempFile, "goframe example copy") // copy file gfile.CopyFile(srcTempFile, dstTempFile) // read contents after copy file fmt.Println(gfile.GetContents(dstTempFile)) // Output: // goframe example copy } ``` ### `CopyDir` - 说明:支持复制文件或目录 - 格式: ```go func CopyDir(src string, dst string) error ``` - 示例: ```go func ExampleCopyDir() { // init var ( srcTempDir = gfile.TempDir("gfile_example_copy_src") // copy file dstFileName = "gfile_example_copy.txt" dstTempFile = gfile.Join(srcTempDir, dstFileName) // copy dir dstTempDir = gfile.TempDir("gfile_example_copy_dst") ) // read contents after copy file fmt.Println(gfile.GetContents(dstTempFile)) // copy dir gfile.CopyDir(srcTempDir, dstTempDir) // list copy dir file fList, _ := gfile.ScanDir(dstTempDir, "*", false) for _, v := range fList { fmt.Println(gfile.Basename(v)) } // Output: // gfile_example.txt // gfile_example_copy.txt } ``` ### `Move` - 说明:将 `src` 重命名为 `dst`。 - 注意:如果 `dst` 已经存在并且是文件,将会被替换造成数据丢失。 - 格式: ```go func Move(src string, dst string) error ``` - 示例: ```go func ExampleMove() { // init var ( srcPath = gfile.Join(gfile.TempDir("gfile_example_basic_dir"), "file1") dstPath = gfile.Join(gfile.TempDir("gfile_example_basic_dir"), "file2") ) // Check is file fmt.Println(gfile.IsFile(dstPath)) // Moves `src` to `dst` path. // If `dst` already exists and is not a directory, it'll be replaced. gfile.Move(srcPath, dstPath) fmt.Println(gfile.IsFile(srcPath)) fmt.Println(gfile.IsFile(dstPath)) // Output: // false // false // true } ``` ### `Rename` - 说明: `Move` 的别名,将 `src` 重命名为 `dst`。 - 注意:如果 `dst` 已经存在并且是文件,将会被替换造成数据丢失。 - 格式: ```go func Rename(src string, dst string) error ``` - 示例: ```go func ExampleRename() { // init var ( srcPath = gfile.Join(gfile.TempDir("gfile_example_basic_dir"), "file2") dstPath = gfile.Join(gfile.TempDir("gfile_example_basic_dir"), "file1") ) // Check is file fmt.Println(gfile.IsFile(dstPath)) // renames (moves) `src` to `dst` path. // If `dst` already exists and is not a directory, it'll be replaced. gfile.Rename(srcPath, dstPath) fmt.Println(gfile.IsFile(srcPath)) fmt.Println(gfile.IsFile(dstPath)) // Output: // false // false // true } ``` ### `Remove` - 说明:删除给定路径的文件或文件夹。 - 格式: ```go func Remove(path string) error ``` - 示例: ```go func ExampleRemove() { // init var ( path = gfile.Join(gfile.TempDir("gfile_example_basic_dir"), "file1") ) // Checks whether given `path` a file, which means it's not a directory. fmt.Println(gfile.IsFile(path)) // deletes all file/directory with `path` parameter. gfile.Remove(path) // Check again fmt.Println(gfile.IsFile(path)) // Output: // true // false } ``` ### `IsEmpty` - 说明:检查给定的路径,如果是文件夹则检查是否包含文件,如果是文件则检查文件大小是否为空。 - 格式: ```go func IsEmpty(path string) bool ``` - 示例: ```go func ExampleIsEmpty() { // init var ( path = gfile.Join(gfile.TempDir("gfile_example_basic_dir"), "file1") ) // Check whether the `path` is empty fmt.Println(gfile.IsEmpty(path)) // Truncate file gfile.Truncate(path, 0) // Check whether the `path` is empty fmt.Println(gfile.IsEmpty(path)) // Output: // false // true } ``` ### `DirNames` - 说明:获取给定路径下的文件列表,返回的是一个切片。 - 格式: ```go func DirNames(path string) ([]string, error) ``` - 示例: ```go func ExampleDirNames() { // init var ( path = gfile.TempDir("gfile_example_basic_dir") ) // Get sub-file names of given directory `path`. dirNames, _ := gfile.DirNames(path) fmt.Println(dirNames) // May Output: // [file1] } ``` ### `Glob` - 说明:模糊搜索给定路径下的文件列表,支持正则,第二个参数控制返回的结果是否带上绝对路径。 - 格式: ```go func Glob(pattern string, onlyNames ...bool) ([]string, error) ``` - 示例: ```go func ExampleGlob() { // init var ( path = gfile.Pwd() + gfile.Separator + "*_example_basic_test.go" ) // Get sub-file names of given directory `path`. // Only show file name matchNames, _ := gfile.Glob(path, true) fmt.Println(matchNames) // Show full path of the file matchNames, _ = gfile.Glob(path, false) fmt.Println(matchNames) // May Output: // [gfile_z_example_basic_test.go] // [xxx/gf/os/gfile/gfile_z_example_basic_test.go] } ``` ### `MatchGlob` **提示** 版本要求:`v2.10.0` - 说明:`MatchGlob` 方法用于判断给定的文件名是否匹配指定的`shell`模式。它扩展了标准库的 `filepath.Match` 功能,支持 `**`(`globstar`)通配符模式,类似于`bash`的`globstar`和 `gitignore`的模式规则。 - 格式: ```go func MatchGlob(pattern, name string) (bool, error) ``` - 参数说明: - `pattern`:匹配模式字符串,支持通配符。 - `name`:要匹配的文件名或路径。 - 返回值:第一个返回值表示是否匹配,第二个返回值表示可能的错误(例如模式格式错误)。 - 模式语法: - `*` 匹配任意非分隔符字符序列 - `**` 匹配任意字符序列,包括路径分隔符(`globstar`) - `?` 匹配任意单个非分隔符字符 - `[abc]` 匹配括号内的任意一个字符 - `[a-z]` 匹配指定范围内的任意字符 - `[^abc]` 匹配不在括号内的任意字符(取反) - `[^a-z]` 匹配不在指定范围内的任意字符(取反) - `Globstar`规则: - `**` 仅当作为完整路径组件出现时才具有 `globstar` 语义(例如:`a/**/b`、`**/a`、`a/**`、`**`) - 像 `a**b` 或 `**a` 这样的模式中,`**` 被视为两个普通的 `*` 通配符,只匹配单个路径组件内的字符 - `/` 和 `\` 都被视为路径分隔符(支持跨平台) - 错误处理: - 对于格式错误的模式(例如未闭合的括号 `[abc`)会返回错误 - 来自 `filepath.Match` 的错误会被传播 - 示例: ```go func ExampleMatchGlob() { var pattern, name string var matched bool var err error // 基本通配符匹配 pattern = "*.go" name = "main.go" matched, err = gfile.MatchGlob(pattern, name) fmt.Printf("Pattern: %s, Name: %s, Matched: %v, Error: %v\n", pattern, name, matched, err) // 使用 ? 通配符 pattern = "test_?.go" name = "test_1.go" matched, err = gfile.MatchGlob(pattern, name) fmt.Printf("Pattern: %s, Name: %s, Matched: %v, Error: %v\n", pattern, name, matched, err) // 使用 ** 匹配任意深度的目录 pattern = "**/*.go" name = "src/foo/bar/main.go" matched, err = gfile.MatchGlob(pattern, name) fmt.Printf("Pattern: %s, Name: %s, Matched: %v, Error: %v\n", pattern, name, matched, err) // 使用 ** 匹配所有内容 pattern = "**" name = "any/path/to/file.go" matched, err = gfile.MatchGlob(pattern, name) fmt.Printf("Pattern: %s, Name: %s, Matched: %v, Error: %v\n", pattern, name, matched, err) // 使用中间的 ** 通配符 pattern = "src/**/test/*.go" name = "src/foo/bar/test/main.go" matched, err = gfile.MatchGlob(pattern, name) fmt.Printf("Pattern: %s, Name: %s, Matched: %v, Error: %v\n", pattern, name, matched, err) // 字符范围匹配 pattern = "[a-z].go" name = "x.go" matched, err = gfile.MatchGlob(pattern, name) fmt.Printf("Pattern: %s, Name: %s, Matched: %v, Error: %v\n", pattern, name, matched, err) // Output: // Pattern: *.go, Name: main.go, Matched: true, Error: // Pattern: test_?.go, Name: test_1.go, Matched: true, Error: // Pattern: **/*.go, Name: src/foo/bar/main.go, Matched: true, Error: // Pattern: **, Name: any/path/to/file.go, Matched: true, Error: // Pattern: src/**/test/*.go, Name: src/foo/bar/test/main.go, Matched: true, Error: // Pattern: [a-z].go, Name: x.go, Matched: true, Error: } ``` **提示** - `**` 通配符特别适合在构建工具、文件搜索等场景中使用,可以灵活匹配任意深度的目录结构。 - 该方法与 `.gitignore` 文件的模式规则兼容,便于实现文件过滤功能。 - 如果只需要标准的通配符匹配(不包含 `**`),可以直接使用标准库的 `filepath.Match`,性能更优。 ### `Exists` - 说明:检查给定的路径是否存在。 - 格式: ```go func Exists(path string) bool ``` - 示例: ```go func ExampleExists() { // init var ( path = gfile.Join(gfile.TempDir("gfile_example_basic_dir"), "file1") ) // Checks whether given `path` exist. fmt.Println(gfile.Exists(path)) // Output: // true } ``` ### `Chdir` - 说明:使用给定的路径,更改当前的工作路径。 - 格式: ```go func Chdir(dir string) error ``` - 示例: ```go func ExampleChdir() { // init var ( path = gfile.Join(gfile.TempDir("gfile_example_basic_dir"), "file1") ) // Get current working directory fmt.Println(gfile.Pwd()) // Changes the current working directory to the named directory. gfile.Chdir(path) // Get current working directory fmt.Println(gfile.Pwd()) // May Output: // xxx/gf/os/gfile // /tmp/gfile_example_basic_dir/file1 } ``` ## 路径操作 ### `Join` - 说明:将多个字符串路径通过\`/\`进行连接。 - 格式: ```go func Join(paths ...string) string ``` - 示例: ```go func ExampleJoin() { // init var ( dirPath = gfile.TempDir("gfile_example_basic_dir") filePath = "file1" ) // Joins string array paths with file separator of current system. joinString := gfile.Join(dirPath, filePath) fmt.Println(joinString) // Output: // /tmp/gfile_example_basic_dir/file1 } ``` ### `Abs` - 说明:返回路径的绝对路径。 - 格式: ```go func Abs(path string) string ``` - 示例: ```go func ExampleAbs() { // init var ( path = gfile.Join(gfile.TempDir("gfile_example_basic_dir"), "file1") ) // Get an absolute representation of path. fmt.Println(gfile.Abs(path)) // Output: // /tmp/gfile_example_basic_dir/file1 } ``` ### `RealPath` - 说明:获取给定路径的绝对路径地址。 - 注意:如果文件不存在则返回空。 - 格式: ```go func RealPath(path string) string ``` - 示例: ```go func ExampleRealPath() { // init var ( realPath = gfile.Join(gfile.TempDir("gfile_example_basic_dir"), "file1") worryPath = gfile.Join(gfile.TempDir("gfile_example_basic_dir"), "worryFile") ) // fetch an absolute representation of path. fmt.Println(gfile.RealPath(realPath)) fmt.Println(gfile.RealPath(worryPath)) // Output: // /tmp/gfile_example_basic_dir/file1 // } ``` ### `SelfName` - 说明:获取当前运行程序的名称。 - 格式: ```go func SelfName() string ``` - 示例: ```go func ExampleSelfName() { // Get file name of current running process fmt.Println(gfile.SelfName()) // May Output: // ___github_com_gogf_gf_v2_os_gfile__ExampleSelfName } ``` ### `Basename` - 说明:获取给定路径中的最后一个元素,包含扩展名。 - 格式: ```go func Basename(path string) string ``` - 示例: ```go func ExampleBasename() { // init var ( path = gfile.Pwd() + gfile.Separator + "testdata/readline/file.log" ) // Get the last element of path, which contains file extension. fmt.Println(gfile.Basename(path)) // Output: // file.log } ``` ### `Name` - 说明:获取给定路径中的最后一个元素,不包含扩展名。 - 格式: ```go func Name(path string) string ``` - 示例: ```go func ExampleName() { // init var ( path = gfile.Pwd() + gfile.Separator + "testdata/readline/file.log" ) // Get the last element of path without file extension. fmt.Println(gfile.Name(path)) // Output: // file } ``` ### `Dir` - 说明:获取给定路径的目录部分,排除最后的元素。 - 格式: ```go func Dir(path string) string ``` - 示例: ```go func ExampleDir() { // init var ( path = gfile.Join(gfile.TempDir("gfile_example_basic_dir"), "file1") ) // Get all but the last element of path, typically the path's directory. fmt.Println(gfile.Dir(path)) // Output: // /tmp/gfile_example_basic_dir } ``` ### `Ext` - 说明:获取给定路径的扩展名,包含\`.\`。 - 格式: ```go func Ext(path string) string ``` - 示例: ```go func ExampleExt() { // init var ( path = gfile.Pwd() + gfile.Separator + "testdata/readline/file.log" ) // Get the file name extension used by path. fmt.Println(gfile.Ext(path)) // Output: // .log } ``` ### `ExtName` - 说明:获取给定路径的扩展名,不包含\`.\`。 - 格式: ```go func ExtName(path string) string ``` - 示例: ```go func ExampleExtName() { // init var ( path = gfile.Pwd() + gfile.Separator + "testdata/readline/file.log" ) // Get the file name extension used by path but the result does not contains symbol '.'. fmt.Println(gfile.ExtName(path)) // Output: // log } ``` ### `MainPkgPath` - 说明:获取main文件(主入口)所在的绝对路径。 - 注意: - `该方法仅在开发环境中可用,同时仅在源代码开发环境中有效,build二进制后将显示源代码的路径地址。` - `第一次调用该方法时,如果处于异步的goroutine中,可能会无法获取主包的路径` - 格式: ```go func MainPkgPath() string ``` - 示例: ```go func Test() { fmt.Println("main pkg path on main :", gfile.MainPkgPath()) char := make(chan int, 1) go func() { fmt.Println("main pkg path on goroutine :", gfile.MainPkgPath()) char <- 1 }() select { case <-char: } // Output: // /xxx/xx/xxx/xx // /xxx/xx/xxx/xx } // 二进制包 $ ./testDemo main pkg path on main : /xxx/xx/xxx/xx main pkg path on goroutine : /xxx/xx/xxx/xx ``` # 资源管理-gres 资源管理由 `gres` 模块实现,具体请参考 [资源管理](../core/gres.md) 章节。 # 配置管理-gcfg 配置管理由`gcfg`模块实现,具体请参考 [配置管理](../core/gcfg.md) 章节。 # 环境变量-genv 环境变量管理组件。 **使用方式**: ```go import "github.com/gogf/gf/v2/os/genv" ``` **接口文档**: [https://pkg.go.dev/github.com/gogf/gf/v2/os/genv](https://pkg.go.dev/github.com/gogf/gf/v2/os/genv) ## `SetMap` ```go func SetMap(m map[string]string) error ``` 该方法用于批量设置环境变量。使用示例: ``` genv.SetMap(g.MapStrStr{ "APPID": "order", "THREAD": "16", "ENDPOINTS": "127.0.0.1:6379", }) ``` ## `GetWithCmd` ```go func GetWithCmd(key string, def ...interface{}) *gvar.Var ``` 该方法用于获取环境变量中指定的选项数值,如果该环境变量不存在时,则从命令行选项中读取。但是两者的名称规则会不一样。例如: `genv.GetWithCmd("gf.debug")` 将会优先去读取 `GF_DEBUG` 环境变量的值,当不存在时则去命令行中的 `gf.debug` 选项。 需要注意的是参数命名转换规则: - 环境变量会将名称转换为大写,名称中的 `.` 字符转换为 `_` 字符。 - 命令行中会将名称转换为小写,名称中的 `_` 字符转换为 `.` 字符。 ## `All` ```go func All() []string ``` 该方法表示返回环境变量中的字符串,并且以\` `key=value` \`的形式返回。 ## `Map` ```go func Map() map[string]string ``` 该方法表示返回环境变量中的字符串,并且以\` `map` \`的形式返回。 ## `Get` ```go func Get(key string, def ...interface{}) *gvar.Var ``` 该方法用于创建返回一个泛型类型的环境变量,如果给定的 `key` 不存在则返回一个默认的泛型类型的环境变量。 ## `Set` ```go func Set(key, value string) error ``` 该方法是通过存放 `key` 和 `value` 的环境变量,如果有报错则返回一个 `Error` 类型。 ## `SetMap` ```go func SetMap(m map[string]string) error ``` 该方法通过 `map` 类型的参数存放环境变量。 ## `Contains` ```go func Contains(key string) bool ``` 该方法通过检查环境变量中是否存在 `key`。 ## `Remove` ```go func Remove(key ...string) error ``` 该方法可以删除一个或者多个环境变量。 ## `Build` ```go func Build(m map[string]string) []string ``` 该方法将 `map` 的参数以数组的形式构建并且返回。 # 模板引擎-gview 模板引擎由 `gview` 模块实现,具体请参考 [模板引擎](../core/gview.md) 章节。 # 协程管理-grpool ## 基本介绍 `Go` 语言中的 `goroutine` 虽然相对于系统线程来说比较轻量级(初始栈大小仅 `2KB`),(并且支持动态扩容),而正常采用 `Java`、 `C++` 等语言启用的线程一般都是内核态的占用的内存资源一般在 `4m` 左右,而假设我们的服务器 `CPU` 内存为 `4G`,那么很明显内核态线程的并发总数量也就 `1024` 个,相反 `Go` 语言的协程则可以达到 `4*1024*1024/2=200w`,这么一看就明白了为什么Go语言天生支持高并发。 ## 痛点描述 ### 协程执行的资源消耗大 但是在高并发量下的 `goroutine` 频繁创建和销毁对于性能损耗以及 `GC` 来说压力也不小。充分将 `goroutine` 复用,减少 `goroutine` 的创建/销毁的性能损耗,这便是 `grpool` 对 `goroutine` 进行池化封装的目的。例如,针对于 `100W` 个执行任务,使用 `goroutine` 的话需要不停创建并销毁 `100W` 个 `goroutine`,而使用 `grpool` 也许底层只需要几万个 `goroutine` 便能充分复用地执行完成所有任务。 经测试, `goroutine` 池对于业务逻辑的执行效率(降低执行时间/CPU使用率)提升不大,甚至没有原生的 `goroutine` 执行快速(池化 `goroutine` 执行调度并没有底层 `Go` 调度器高效,因为池化 `goroutine` 的执行调度也是基于底层 `Go` 调度器),但是由于采用了复用的设计,池化后对内存的使用率得到极大的降低。 ### 大量协程影响全局协程调度 某些业务模块需要动态创建协程来执行,例如异步采集任务、指标计算任务等等。这些业务逻辑不是服务的核心逻辑,并且会产生协程。在极端情况下可能会引起协程大暴涨,影响底层 `Go` 引擎全局的写成调度,造成服务整体执行延迟较大。 拿异步采集任务来举个例子,每隔 `5` 秒执行一次,每次创建 `100` 个协程来采集不同的目标端。当采集出现网络延迟时,上一步的任务并未执行完,下一次的任务又新创建协程开始执行。当积累的任务越来越多,会造成协程的暴涨,影响全局的服务执行。针对这一类场景,我们可以通过池化的技术来将任务进行定量执行,当池中的任务堆积到达一定量时,后续的任务应当阻塞。例如,我们设定池中任务的最大数量为 `10000` 个,后续不停将任务丢到池中执行,当超过池的最大数量时,任务执行将会阻塞,但并不会影响全局的服务执行。 ## 概念介绍 ### `Pool` `goroutine` 池,用于管理若干可复用的 `goroutine` 协程资源。 ### `Job` 添加到池对象的任务队列中等待执行的任务,是一个 `Func` 的方法,一个 `Job` 同时只能被一个 `Worker` 获取并执行。 `Func` 的定义如下: ```go type Func func(ctx context.Context) ``` ### `Worker` 池对象中参与任务执行的 `goroutine`,一个 `Worker` 可以执行若干个 `Job`,直到队列中再无等待的 `Job`。 ## 使用介绍 **使用方式**: ```go import "github.com/gogf/gf/v2/os/grpool" ``` **使用场景**: 管理大量异步任务的场景、需要异步协程复用的场景、需要降低内存使用率的场景。 **接口文档**: ```go func Add(ctx context.Context, f Func) error func AddWithRecover(ctx context.Context, userFunc Func, recoverFunc RecoverFunc) error func Jobs() int func Size() int func New(limit ...int) *Pool func (p *Pool) Add(ctx context.Context, f Func) error func (p *Pool) AddWithRecover(ctx context.Context, userFunc Func, recoverFunc RecoverFunc) error func (p *Pool) Cap() int func (p *Pool) Close() func (p *Pool) IsClosed() bool func (p *Pool) Jobs() int func (p *Pool) Size() int ``` 通过 `grpool.New` 方法创建一个 `goroutine池` 对象,参数 `limit` 为非必需参数,用于限定池中的工作 `goroutine` 数量,默认为不限制。需要注意的是,任务可以不停地往池中添加,没有限制,但是工作的 `goroutine` 是可以做限制的。我们可以通过 `Size()` 方法查询当前的工作 `goroutine` 数量,使用 `Jobs()` 方法查询当前池中待处理的任务数量。 同时,为便于使用, `grpool` 包提供了默认的 `goroutine` 池,默认的池对象不限制 `goroutine` 数量,直接通过 `grpool.Add` 即可往默认的池中添加任务,任务参数必须是一个 `func()` 类型的函数/方法。 这个模块大家问得最多的是外部如何给 `grpool` 里面的任务传递参数,具体请看示例2。 ## 使用示例 ### 使用默认的 `goroutine` 池,限制 `100` 个 `goroutine` 执行 `1000` 个任务 ```go package main import ( "context" "fmt" "github.com/gogf/gf/v2/os/gctx" "github.com/gogf/gf/v2/os/grpool" "github.com/gogf/gf/v2/os/gtimer" "time" ) var ( ctx = gctx.New() ) func job(ctx context.Context) { time.Sleep(1*time.Second) } func main() { pool := grpool.New(100) for i := 0; i < 1000; i++ { pool.Add(ctx,job) } fmt.Println("worker:", pool.Size()) fmt.Println(" jobs:", pool.Jobs()) gtimer.SetInterval(ctx,time.Second, func(ctx context.Context) { fmt.Println("worker:", pool.Size()) fmt.Println(" jobs:", pool.Jobs()) fmt.Println() }) select {} } ``` 这段程序中的任务函数的功能是 `sleep 1秒钟`,这样便能充分展示出goroutine数量限制功能。其中,我们使用了 `gtime.SetInterval` 定时器每隔1秒钟打印出当前默认池中的工作 `goroutine` 数量以及待处理的任务数量。 ### 异步传参:来个新手容易出错的例子 > 这个例子在go版本≥1.22时不再生效,即go 1.22以后不再有循环变量陷阱。 ```go package main import ( "context" "fmt" "github.com/gogf/gf/v2/os/gctx" "github.com/gogf/gf/v2/os/grpool" "sync" ) var ( ctx = gctx.New() ) func main() { wg := sync.WaitGroup{} for i := 0; i < 10; i++ { wg.Add(1) grpool.Add(ctx,func(ctx context.Context) { fmt.Println(i) wg.Done() }) } wg.Wait() } ``` 我们这段代码的目的是要顺序地打印出0-9,然而运行后却输出: ```10 10 10 10 10 10 10 10 10 10 ``` 为什么呢?这里的执行结果无论是采用 `go` 关键字来执行还是 `grpool` 来执行都是如此。原因是,对于异步线程/协程来讲,函数进行异步执行注册时,该函数并未真正开始执行(注册时只在 `goroutine` 的栈中保存了变量 `i` 的内存地址),而一旦开始执行时函数才会去读取变量 `i` 的值,而这个时候变量 `i` 的值已经自增到了 `10`。 清楚原因之后,改进方案也很简单了,就是在注册异步执行函数的时候,把当时变量 `i` 的值也一并传递获取;或者把当前变量i的值赋值给一个不会改变的临时变量,在函数中使用该临时变量而不是直接使用变量 `i`。 改进后的示例代码如下: **1)、使用go关键字** ```go package main import ( "fmt" "sync" ) func main() { wg := sync.WaitGroup{} for i := 0; i < 10; i++ { wg.Add(1) go func(v int){ fmt.Println(v) wg.Done() }(i) } wg.Wait() } ``` 执行后,输出结果为: ```0 9 3 4 5 6 7 8 1 2 ``` 注意,异步执行时并不会保证按照函数注册时的顺序执行,以下同理。 **2)、使用临时变量** ```go package main import ( "context" "fmt" "github.com/gogf/gf/v2/os/gctx" "github.com/gogf/gf/v2/os/grpool" "sync" ) var ( ctx = gctx.New() ) func main() { wg := sync.WaitGroup{} for i := 0; i < 10; i++ { wg.Add(1) v := i grpool.Add(ctx, func(ctx context.Context) { fmt.Println(v) wg.Done() }) } wg.Wait() } ``` 执行后,输出结果为: ```9 0 1 2 3 4 5 6 7 8 ``` 这里可以看到,使用 `grpool` 进行任务注册时,注册方法为 `func(ctx context.Context)`,因此无法在任务注册时把变量 `i` 的值注册进去(请尽量不要通过 `ctx` 传递业务参数),因此只能采用临时变量的形式来传递当前变量 `i` 的值。 ### 自动捕获 `goroutine` 错误: `AddWithRecover` `AddWithRecover` 将新作业推送到具有指定恢复功能的池中。当 `userFunc` 执行过程中出现 `panic` 时,会调用可选的 `Recovery Func`。如果没有传入 `Recovery Func` 或赋空,则忽略 `userFunc` 引发的 `panic`。该作业将异步执行。 ```go package main import ( "context" "fmt" "github.com/gogf/gf/v2/container/garray" "github.com/gogf/gf/v2/os/gctx" "github.com/gogf/gf/v2/os/grpool" "time" ) var ( ctx = gctx.New() ) func main() { array := garray.NewArray(true) grpool.AddWithRecover(ctx, func(ctx context.Context) { array.Append(1) array.Append(2) panic(1) }, func(err error) { array.Append(1) }) grpool.AddWithRecover(ctx, func(ctx context.Context) { panic(1) array.Append(1) }) time.Sleep(500 * time.Millisecond) fmt.Print(array.Len()) } ``` ### 测试一下 `grpool` 和原生的 `goroutine` 之间的性能 **1)、 `grpool`** ```go package main import ( "context" "fmt" "github.com/gogf/gf/v2/os/gctx" "github.com/gogf/gf/v2/os/grpool" "github.com/gogf/gf/v2/os/gtime" "sync" "time" ) var ( ctx = gctx.New() ) func main() { start := gtime.TimestampMilli() wg := sync.WaitGroup{} for i := 0; i < 10000000; i++ { wg.Add(1) grpool.Add(ctx,func(ctx context.Context) { time.Sleep(time.Millisecond) wg.Done() }) } wg.Wait() fmt.Println(grpool.Size()) fmt.Println("time spent:", gtime.TimestampMilli() - start) } ``` **2)、 `goroutine`** ```go package main import ( "fmt" "github.com/gogf/gf/v2/os/gtime" "sync" "time" ) func main() { start := gtime.TimestampMilli() wg := sync.WaitGroup{} for i := 0; i < 10000000; i++ { wg.Add(1) go func() { time.Sleep(time.Millisecond) wg.Done() }() } wg.Wait() fmt.Println("time spent:", gtime.TimestampMilli() - start) } ``` **3)、运行结果比较** 测试结果为两个程序各运行 `3` 次取平均值。 ```shell grpool: goroutine count: 847313 memory spent: ~2.1 G time spent: 37792 ms goroutine: goroutine count: 1000W memory spent: ~4.8 GB time spent: 27085 ms ``` 可以看到池化过后,执行相同数量的任务, `goroutine` 数量减少很多,相对的内存也降低了一倍以上,CPU时间耗时也勉强可以接受。 # 构建信息-gbuild 用于获取采用`gf build`命令构建程序的构建信息。 **使用方式**: ```go import "github.com/gogf/gf/v2/os/gbuild" ``` **接口文档**: [https://pkg.go.dev/github.com/gogf/gf/v2/os/gbuild](https://pkg.go.dev/github.com/gogf/gf/v2/os/gbuild) # 对象信息-gstructs ## 基本介绍 `gstructs` 组件用于方便获取结构体的相关信息。 这是一个偏底层组件,一般业务上很少会用到,在框架、基础库、中间件编写中用到。 使用方式: ```go import "github.com/gogf/gf/v2/os/gstructs" ``` 接口文档: [https://pkg.go.dev/github.com/gogf/gf/v2/os/gstructs](https://pkg.go.dev/github.com/gogf/gf/v2/os/gstructs) ## 常用方法 ### `Fields` - 说明:`Fields ` 将输入参数 `in` 的 `Pointer ` 属性的字段以 `Field` 切片的形式返回。 - 格式: ```go Fields(in FieldsInput) ([]Field, error) ``` - 示例: ```go func main() { type User struct { Id int Name string `params:"name"` Pass string `my-tag1:"pass1" my-tag2:"pass2" params:"pass"` } var user *User fields, _ := gstructs.Fields(gstructs.FieldsInput{ Pointer: user, RecursiveOption: 0, }) g.Dump(fields) } // Output: [ { Value: "", Field: { Name: "Id", PkgPath: "", Type: "int", Tag: "", Offset: 0, Index: [ 0, ], Anonymous: false, }, TagValue: "", }, { Value: {}, Field: { Name: "Name", PkgPath: "", Type: "string", Tag: "params:\"name\"", Offset: 8, Index: [ 1, ], Anonymous: false, }, TagValue: "", }, { Value: {}, Field: { Name: "Pass", PkgPath: "", Type: "string", Tag: "my-tag1:\"pass1\" my-tag2:\"pass2\" params:\"pass\"", Offset: 24, Index: [ 2, ], Anonymous: false, }, TagValue: "", }, ] ``` ### `TagMapName` - 说明:`TagMapName` 从参数 `pointer` 中检索 `tag`,并以 `map[string]string` 的形式返回。 - 注意: - 参数 `pointer` 的类型应该是 `struct/*struct`。 - 只会返回可导出的字段(首字母大写的字段)。 - 格式: ```go TagMapName(pointer interface{}, priority []string) (map[string]string, error) ``` - 示例: ```go func main() { type User struct { Id int Name string `params:"name"` Pass string `my-tag1:"pass1" my-tag2:"pass2" params:"pass"` } var user User m, _ := gstructs.TagMapName(user, []string{"params"}) g.Dump(m) } // Output: { "name": "Name", "pass": "Pass", } ``` # 定时器-gtimer ## 基本介绍 `gtimer` 是一个并发安全的高性能定时器,类似于 `Java` 的 `Timer`。 `gtimer` 底层采用了 **优先级队列**( `PriorityQueue`)实现。 **使用场景**: 任何定时任务场景,大批量定时任务/延迟任务的场景,超时控制/频率控制的业务场景,对于定时时间准确度要求不高的业务场景。 **使用方式:** ```go import "github.com/gogf/gf/v2/os/gtimer" ``` **接口文档**: [https://pkg.go.dev/github.com/gogf/gf/v2/os/gtimer](https://pkg.go.dev/github.com/gogf/gf/v2/os/gtimer) **简要说明:** 1. `New` 方法用于创建自定义的任务定时器对象,并可在创建时`TimerOptions`传入参数,其中: - `Interval` 指定定时器的最小 `tick` 时间间隔。 - `Quick` 指定定时器是否启动时就执行一次(默认为 `false`)。 2. `Add` 方法用于添加定时任务,其中: - `interval` 参数用于指定方法的执行的时间间隔。 - `job` 参数为需要执行的任务方法。 3. `AddEntry` 方法添加定时任务,支持更多参数的控制。 4. `AddSingleton` 方法用于添加 **单例** 定时任务,即 **同时只能有一个该任务正在运行**。 5. `AddOnce` 方法用于添加只运行一次的定时任务,当运行一次数后该定时任务自动销毁。 6. `AddTimes` 方法用于添加运行指定次数的定时任务,当运行 `times` 次数后该定时任务自动销毁。 7. `Search` 方法用于根据名称进行定时任务搜索(返回定时任务 `*Entry` 对象指针)。 8. `Start` 方法用于启动定时器(使用 `New` 创建定时器时会自动启动)。 9. `Stop` 方法用于停止定时器。 10. `Close` 方法用于关闭定时器。 ## 默认定时器 大部分的场景下使用默认的定时器即可。使用 `gtimer` 的默认定时器时,默认的定时检测间隔时间为 `100ms`,因此理论的时间间隔误差范围为 `0~100ms`。可以使用以下两种方式修改默认的定时器参数: 1. 使用启动参数 - `gf.gtimer.interval=50`: 修改默认的时间刻度为 `50毫秒` 2. 使用环境变量 - `GF_GTIMER_INTERVAL=50` **注意** 需要注意,定时器默认检测间隔时间越短,对 `CPU` 的使用量会越大。 ## 注意事项 1. 由于现代的计算机都是使用软件实现的定时器, **任何的定时器都是存在误差的**,不会完全精准,可能延迟,也甚至会提前,但是不会不执行。并且在定时间隔比较大,或者并发量大,负载较高的系统中尤其明显。参考链接: [https://github.com/golang/go/issues/14410](https://github.com/golang/go/issues/14410) 2. 由于误差是不可避免的,任何定时器的实现(不仅是框架的定时器,标准库的定时器亦是如此) **都不会使用系统时间**,而是使用一种固定的 `tick` 间隔。不要在定时器任务逻辑中使用系统时间来判断间隔做逻辑运算,这个时间的判断没有任何意义。 3. 在不考虑误差的前提下,定时间隔不会考虑任务的执行时间。例如,如果一项工作需要 `3` 分钟才能执行完成,并且计划每隔 `5` 分钟运行一次,那么每次任务之间只有 `2` 分钟的空闲时间。 4. 需要注意的是 **单例模式** 运行的定时任务,任务的执行时间会影响该任务下一次执行的 **开始时间**。例如:一个每间隔 `1` 秒执行的任务,运行耗时为 `1` 秒,那么在 **第1秒** 开始运行后,下一次任务将会在 **第3秒** 开始执行。因为中间有一次运行检查时发现有当前任务正在进行,因此退出等待下一次执行检查。 ## 定时器与 `gcron` 区别 具体请查看章节 [定时任务-gcron与gtimer](./os-gcron-differ-with-gtimer.md) # 定时器-基本使用 ## 基本示例 ```go package main import ( "context" "fmt" "github.com/gogf/gf/v2/os/gctx" "github.com/gogf/gf/v2/os/gtime" "github.com/gogf/gf/v2/os/gtimer" "time" ) func main() { var ( ctx = gctx.New() now = time.Now() ) gtimer.AddTimes(ctx, time.Second, 10, func(ctx context.Context) { fmt.Println(gtime.Now(), time.Duration(time.Now().UnixNano()-now.UnixNano())) now = time.Now() }) select {} } ``` 执行后,输出结果为: ```html 2021-05-27 13:28:19 1.004516s 2021-05-27 13:28:20 997.262ms 2021-05-27 13:28:21 999.972ms 2021-05-27 13:28:22 1.00112s 2021-05-27 13:28:23 998.773ms 2021-05-27 13:28:24 999.957ms 2021-05-27 13:28:25 1.002468s 2021-05-27 13:28:26 997.468ms 2021-05-27 13:28:27 999.981ms 2021-05-27 13:28:28 1.002504s ``` ## 单例任务 ```go package main import ( "context" "github.com/gogf/gf/v2/os/gctx" "github.com/gogf/gf/v2/os/glog" "github.com/gogf/gf/v2/os/gtimer" "time" ) func main() { var ( ctx = gctx.New() interval = time.Second ) gtimer.AddSingleton(ctx, interval, func(ctx context.Context) { glog.Print(ctx, "doing") time.Sleep(5 * time.Second) }) select {} } ``` 执行后,输出结果为: ```html 2021-11-14 11:50:42.192 {189cwi9mo40cfp73guzhugo100tnuedg} doing 2021-11-14 11:50:48.190 {189cwi9mo40cfp73guzhugo100tnuedg} doing 2021-11-14 11:50:54.192 {189cwi9mo40cfp73guzhugo100tnuedg} doing 2021-11-14 11:51:00.189 {189cwi9mo40cfp73guzhugo100tnuedg} doing ... ``` ## 延迟任务 延迟任务是指在指定时间后生效的定时任务。我们可以通过 `DelayAdd*` 相关方法实现延迟任务的创建。 ```go package main import ( "context" "fmt" "github.com/gogf/gf/v2/os/gctx" "github.com/gogf/gf/v2/os/gtime" "github.com/gogf/gf/v2/os/gtimer" "time" ) func main() { var ( ctx = gctx.New() delay = time.Second interval = time.Second ) fmt.Println("Start:", gtime.Now()) gtimer.DelayAdd( ctx, delay, interval, func(ctx context.Context) { fmt.Println("Running:", gtime.Now()) }, ) select {} } ``` 执行后,终端输出: ``` Start: 2021-05-27 13:26:02 Running: 2021-05-27 13:26:04 Running: 2021-05-27 13:26:05 Running: 2021-05-27 13:26:06 Running: 2021-05-27 13:26:07 Running: 2021-05-27 13:26:08 Running: 2021-05-27 13:26:09 Running: 2021-05-27 13:26:10 Running: 2021-05-27 13:26:11 ... ``` ## `SetTimeout` 与 `SetInterval` 这两个方法来源于 `JavaScript` 常用定时方法。其中 `SetTimeout` 用于创建只执行一次的定时任务,不过可以通过递归调用 `SetTimeout` 来实现无限间隔执行。 `SetIterval` 用于创建间隔执行不退出的定时任务。 ```go package main import ( "context" "fmt" "github.com/gogf/gf/v2/os/gctx" "github.com/gogf/gf/v2/os/gtime" "github.com/gogf/gf/v2/os/gtimer" "time" ) func main() { var ( ctx = gctx.New() timeout = time.Second interval = time.Second ) gtimer.SetTimeout(ctx, timeout, func(ctx context.Context) { fmt.Println("SetTimeout:", gtime.Now()) }) gtimer.SetInterval(ctx, interval, func(ctx context.Context) { fmt.Println("SetInterval:", gtime.Now()) }) select {} } ``` 执行后,终端输出: ``` SetInterval: 2021-05-27 13:20:50 SetTimeout: 2021-05-27 13:20:50 SetInterval: 2021-05-27 13:20:51 SetInterval: 2021-05-27 13:20:52 SetInterval: 2021-05-27 13:20:53 SetInterval: 2021-05-27 13:20:54 SetInterval: 2021-05-27 13:20:55 SetInterval: 2021-05-27 13:20:56 SetInterval: 2021-05-27 13:20:57 SetInterval: 2021-05-27 13:20:58 ... ``` ## `Exit` 退出 我们可以在定时任务中通过 `Exit` 方法强制退出定时任务的继续执行,该定时任务将会被从定时器中移除。 ```go package main import ( "context" "fmt" "github.com/gogf/gf/v2/os/gctx" "github.com/gogf/gf/v2/os/gtime" "github.com/gogf/gf/v2/os/gtimer" "time" ) func main() { var ( ctx = gctx.New() ) gtimer.SetInterval(ctx, time.Second, func(ctx context.Context) { fmt.Println("exit:", gtime.Now()) gtimer.Exit() }) select {} } ``` 执行后,终端输出: ``` exit: 2021-05-27 13:31:24 ``` # 定时器-性能测试 ## 性能测试 ``` goos: linux goarch: amd64 pkg: github.com/gogf/gf/v2/os/gtimer Benchmark_Add-12 4048776 291.9 ns/op 249 B/op 6 allocs/op Benchmark_StartStop-12 100000000 10.96 ns/op 0 B/op 0 allocs/op PASS ok command-line-arguments 6.602s ``` # 时间管理-gtime ## 基本介绍 通用时间管理模块,封装了常用的时间/日期相关的方法,作为标准库 `time` 的功能性扩展,提供了更多的功能特性。支持自定义的日期格式化语法,格式化语法灵感来源于 `PHP` 的 `date` 函数语法 ( [http://php.net/manual/zh/function.date.php](http://php.net/manual/zh/function.date.php) )。 **提示** `gtime` 的时间格式语法对于熟悉 `PHP` 的开发者来说非常友好。 **使用方式**: ```go import "github.com/gogf/gf/v2/os/gtime" ``` **接口文档**: [https://pkg.go.dev/github.com/gogf/gf/v2/os/gtime](https://pkg.go.dev/github.com/gogf/gf/v2/os/gtime) # 时间管理-时间格式 **注意** 注意事项: `gtime.Time` 对象通过 `Format` 方法来实现自定义格式的时间日期转换,该方法与标准库 `time.Time` 的 `Format` 方法冲突。在 `gtime.Time` 对象中,通过 `Layout` 方法实现标准库 `time.Time` 的 `Format` 格式,例如: ``t.Layout(`2006-01-02 15:04:05`)``。 以下是 `gtime` 模块支持的时间格式语法列表: | 格式 | 说明 | 返回值示例 | | --- | --- | --- | | **日** | -- | -- | | `d` | 月份中的第几天,有前导零的 2 位数字 | 01 到 31 | | `D` | 星期中的第几天,文本表示,3 个字母 | Mon 到 Sun | | `N` | ISO-8601 格式数字表示的星期中的第几天 | 1(表示星期一)到 7(表示星期天) | | `j` | 月份中的第几天,没有前导零 | 1 到 31 | | `l` | ("L"的小写字母)星期几,完整的文本格式 | Sunday 到 Saturday | | `S` | 每月天数后面的英文后缀,2 个字符 | st,nd,rd 或者 th。可以和 j 一起用 | | `w` | 星期中的第几天,数字表示 | 0(表示星期天)到 6(表示星期六) | | `z` | 年份中的第几天 | 0 到 365 | | **周** | -- | -- | | `W` | ISO-8601 格式年份中的第几周,每周从星期一开始 | 例如:42(当年的第 42 周) | | **月** | -- | -- | | `F` | 月份,完整的文本格式 | January 到 December | | `m` | 数字表示的月份,有前导零 | 01 到 12 | | `M` | 三个字母缩写表示的月份 | Jan 到 Dec | | `n` | 数字表示的月份,没有前导零 | 1 到 12 | | `t` | 指定的月份有几天 | 28 到 31 | | **年** | -- | -- | | `Y` | 4 位数字完整表示的年份 | 例如:1999 或 2003 | | `y` | 2 位数字表示的年份 | 例如:99 或 03 | | **时间** | -- | -- | | `a` | 小写的上午和下午值 | am 或 pm | | `A` | 大写的上午和下午值 | AM 或 PM | | `g` | 小时,12 小时格式,没有前导零 | 1 到 12 | | `G` | 小时,24 小时格式,没有前导零 | 0 到 23 | | `h` | 小时,12 小时格式,有前导零 | 01 到 12 | | `H` | 小时,24 小时格式,有前导零 | 00 到 23 | | `i` | 有前导零的分钟数 | 00 到 59 | | `s` | 秒数,有前导零 | 00 到 59 | | `u` | 毫秒数(3位) | 例如:000, 123, 239 | | `U` | UNIX时间戳(秒) | 例如:1559648183 | | **时区** | -- | -- | | `O` | 与UTC相差的小时数 | 例如:+0200 | | `P` | 与UTC的差别,小时和分钟之间有冒号分隔 | 例如:+02:00 | | `T` | 时区缩写 | 例如:UTC,GMT,CST | | **日期** | -- | -- | | `c` | ISO 8601 格式的日期 | 例如:2004-02-12T15:19:21+00:00 | | `r` | RFC 822 格式的日期 | 例如:Thu, 21 Dec 2000 16:01:07 +0200 | # 时间管理-时间对象 ## 时间对象 创建 `gtime.Time` 对象可以通过标准库 `time.Time` 对象、Unix时间戳、时间字符串(如: `2018-07-18 12:01:00`)、自定义时间字符串(需要给定格式,支持自定义格式及标准库格式)。 ## 创建对象 可以通过 `gtime.New` 方法创建 `gtime.Time` 对象,该方法支持 `time.Time`、时间戳、时间字符串创建对象。其中时间戳支持到纳秒的时间整型长度。 例如: ```go // 通过时间字符串创建 gtime.New("2020-10-24 12:00:00") // 通过time.Time对象创建 gtime.New(time.Now()) // 通过时间戳(秒)创建 gtime.New(1603710586) // 通过时间戳(纳秒)创建 gtime.New(1603710586660409000) ``` 此外,时间字符串支持常见时间类型,例如: ```html 2017-12-14 04:51:34 +0805 LMT 2017-12-14 04:51:34 +0805 LMT 2006-01-02T15:04:05Z07:00 2014-01-17T01:19:15+08:00 2018-02-09T20:46:17.897Z 2018-02-09 20:46:17.897 2018-02-09T20:46:17Z 2018-02-09 20:46:17 2018/10/31 - 16:38:46 2018-02-09 2018.02.09 01-Nov-2018 11:50:28 01/Nov/2018 11:50:28 01.Nov.2018 11:50:28 01.Nov.2018:11:50:28 日期连接符号支持'-'、'/'、'.' ``` ## 使用示例 ### 示例1,自定义格式化语法 ```go package main import ( "fmt" "github.com/gogf/gf/v2/os/gtime" ) func main() { formats := []string{ "Y-m-d H:i:s.u", "D M d H:i:s T O Y", "\\T\\i\\m\\e \\i\\s: h:i:s a", "2006-01-02T15:04:05.000000000Z07:00", } t := gtime.Now() for _, f := range formats { fmt.Println(t.Format(f)) } } ``` 在该示例中,我们给定了四种 `format` 格式,并将当前时间用这四种格式转换后打印出来。执行后,输出结果如下: ```html 2018-07-22 11:17:13.797 Sun Jul 22 11:17:13 CST +0800 2018 Time is: 11:17:13 am 2006-01-02CST15:04:05.000000000Z07:00 ``` 可以看到,这个示例演示了几个需要注意的地方: 1. 如果使用的字母与格式化字符冲突时,可以使用 `\` 符号转义该字符,这样时间格式解析器会认为该字符不是格式化字符,而是普通字母。因此这里的第三个字符串示例输出为: `Time is: 11:17:13 am` 2. 使用 `Format` 方法接收的是自定义的时间格式化语法(如: `Y-m-d H:i:s`),而非标准库的时间格式语法(如: `2006-01-02 15:04:05`),因此在这里的第四个字符串示例中原样输出参数值; ### 示例2,标准库格式化语法 ```go package main import ( "fmt" "github.com/gogf/gf/v2/os/gtime" ) func main() { formats := []string{ "2006-01-02 15:04:05.000", "Mon Jan _2 15:04:05 MST 2006", "Time is: 03:04:05 PM", "2006-01-02T15:04:05.000000000Z07:00 MST", } t := gtime.Now() for _, f := range formats { fmt.Println(t.Layout(f)) } } ``` 在该示例中,我们使用四种标准库的时间格式化语法格式化当前的时间并输出结果到终端。执行后,输出结果为: ```html 2018-07-22 11:28:13.945 Sun Jul 22 11:28:13 CST 2018 Time is: 11:28:13 AM 2018-07-22T11:28:13.945153275+08:00 CST ``` 有几个需要说明的地方: 1. 自定义时间格式化语法与标准库时间格式化语法并不冲突,前者使用 `Format` 方法,后者使用 `Layout` 方法行格式化,相互独立,互不冲突,无法混用; 2. 标准库的时间格式化语法自有特点,是不是感觉有点复杂; ### 示例3,时间对象链式操作 ```go package main import ( "fmt" "github.com/gogf/gf/v2/os/gtime" "time" ) func main() { // 去年今日,系统时间 fmt.Println(gtime.Now().AddDate(-1, 0, 0).Format("Y-m-d")) // 去年今日,UTC时间 fmt.Println(gtime.Now().AddDate(-1, 0, 0).Format("Y-m-d H:i:s T")) fmt.Println(gtime.Now().AddDate(-1, 0, 0).UTC().Format("Y-m-d H:i:s T")) // 下个月1号凌晨0点整 fmt.Println(gtime.Now().AddDate(0, 1, 0).Format("Y-m-01 00:00:00")) // 1个小时前 fmt.Println(gtime.Now().Add(-time.Hour).Format("Y-m-d H:i:s")) } ``` 执行后,输出结果为: ``` 2020-09-19 2020-09-19 15:51:48 CST 2020-09-19 07:51:48 UTC 2021-10-01 00:00:00 2021-09-19 14:51:48 ``` 该示例比较简单,便不多赘述。 # 时间管理-工具方法 接口文档: [https://pkg.go.dev/github.com/gogf/gf/v2/os/gtime](https://pkg.go.dev/github.com/gogf/gf/v2/os/gtime) 方法比较简单,比较常用的是以下几个方法: 1. `Timestamp` 用于获得当前时间戳, `TimestampMilli`、 `TimestampMicro` 及 `TimestampNano` 用于获得当前的毫秒、微秒和纳秒值。 2. `Date` 和 `Datetime` 用于获得当前日期及当前日期时间。 3. `SetTimeZone` 用于设置当前进程的全局时区。 4. 其他方法说明请查看接口文档。 ### 示例1,基本使用 创建时间对象及获取当前时间戳。 ```go package main import ( "fmt" "github.com/gogf/gf/v2/os/gtime" ) func main() { fmt.Println("Date :", gtime.Date()) fmt.Println("Datetime :", gtime.Datetime()) fmt.Println("Second :", gtime.Timestamp()) fmt.Println("Millisecond:", gtime.TimestampMilli()) fmt.Println("Microsecond:", gtime.TimestampMicro()) fmt.Println("Nanosecond :", gtime.TimestampNano()) } ``` 执行后,输出结果为: ``` Date : 2018-07-22 Datetime : 2018-07-22 11:52:22 Second : 1532231542 Millisecond: 1532231542688 Microsecond: 1532231542688688 Nanosecond : 1532231542688690259 ``` ### 示例2, `StrToTime` 除了通过 `New` 方法外,也可以通过 `StrToTime` 根据常见的时间字符串解析生成 `gtime.Time` 对象,常见的时间字符串如下: ```html 2017-12-14 04:51:34 +0805 LMT 2017-12-14 04:51:34 +0805 LMT 2006-01-02T15:04:05Z07:00 2014-01-17T01:19:15+08:00 2018-02-09T20:46:17.897Z 2018-02-09 20:46:17.897 2018-02-09T20:46:17Z 2018-02-09 20:46:17 2018/10/31 - 16:38:46 2018-02-09 2018.02.09 01-Nov-2018 11:50:28 01/Nov/2018 11:50:28 01.Nov.2018 11:50:28 01.Nov.2018:11:50:28 日期连接符号支持'-'、'/'、'.' ``` 使用示例: ```go package main import ( "fmt" "github.com/gogf/gf/v2/os/glog" "github.com/gogf/gf/v2/os/gtime" "time" ) func main() { array := []string{ "2017-12-14 04:51:34 +0805 LMT", "2006-01-02T15:04:05Z07:00", "2014-01-17T01:19:15+08:00", "2018-02-09T20:46:17.897Z", "2018-02-09 20:46:17.897", "2018-02-09T20:46:17Z", "2018-02-09 20:46:17", "2018.02.09 20:46:17", "2018-02-09", "2017/12/14 04:51:34 +0805 LMT", "2018/02/09 12:00:15", "01/Nov/2018:13:28:13 +0800", "01-Nov-2018 11:50:28 +0805 LMT", "01-Nov-2018T15:04:05Z07:00", "01-Nov-2018T01:19:15+08:00", "01-Nov-2018 11:50:28 +0805 LMT", "01/Nov/2018 11:50:28", "01/Nov/2018:11:50:28", "01.Nov.2018:11:50:28", "01/Nov/2018", } cstLocal, _ := time.LoadLocation("Asia/Shanghai") for _, s := range array { if t, err := gtime.StrToTime(s); err == nil { fmt.Println(s) fmt.Println(t.UTC().String()) fmt.Println(t.In(cstLocal).String()) } else { glog.Error(s, err) } fmt.Println() } } ``` 在这个示例中,将部分时间格式串使用 `StrToTime` 方法转换为 `gtime.Time` 对象,并输出该事件的 `UTC` 时间和 `CST` 时间(上海时区时间)。 执行后,输出结果为: ```html 2017-12-14 04:51:34 +0805 LMT 2017-12-13 20:46:34 2017-12-14 04:46:34 +0800 CST 2006-01-02T15:04:05Z07:00 2006-01-02 22:04:05 2006-01-03 06:04:05 +0800 CST 2014-01-17T01:19:15+08:00 2014-01-16 17:19:15 2014-01-17 01:19:15 +0800 CST 2018-02-09T20:46:17.897Z 2018-02-09 20:46:17 2018-02-10 04:46:17.897 +0800 CST 2018-02-09 20:46:17.897 2018-02-09 12:46:17 2018-02-09 20:46:17.897 +0800 CST 2018-02-09T20:46:17Z 2018-02-09 20:46:17 2018-02-10 04:46:17 +0800 CST 2018-02-09 20:46:17 2018-02-09 12:46:17 2018-02-09 20:46:17 +0800 CST 2018.02.09 20:46:17 2018-02-09 12:46:17 2018-02-09 20:46:17 +0800 CST 2018-02-09 2018-02-08 16:00:00 2018-02-09 00:00:00 +0800 CST 2017/12/14 04:51:34 +0805 LMT 2017-12-13 20:46:34 2017-12-14 04:46:34 +0800 CST 2018/02/09 12:00:15 2018-02-09 04:00:15 2018-02-09 12:00:15 +0800 CST 01/Nov/2018:13:28:13 +0800 2018-11-01 05:28:13 2018-11-01 13:28:13 +0800 CST 01-Nov-2018 11:50:28 +0805 LMT 2018-11-01 03:45:28 2018-11-01 11:45:28 +0800 CST 01-Nov-2018T15:04:05Z07:00 2018-11-01 22:04:05 2018-11-02 06:04:05 +0800 CST 01-Nov-2018T01:19:15+08:00 2018-10-31 17:19:15 2018-11-01 01:19:15 +0800 CST 01-Nov-2018 11:50:28 +0805 LMT 2018-11-01 03:45:28 2018-11-01 11:45:28 +0800 CST 01/Nov/2018 11:50:28 2018-11-01 03:50:28 2018-11-01 11:50:28 +0800 CST 01/Nov/2018:11:50:28 2018-11-01 03:50:28 2018-11-01 11:50:28 +0800 CST 01.Nov.2018:11:50:28 2018-11-01 03:50:28 2018-11-01 11:50:28 +0800 CST 01/Nov/2018 2018-10-31 16:00:00 2018-11-01 00:00:00 +0800 CST ``` # 时间管理-时区设置 ## `SetTimeZone` 设置全局时区 ```go package main import ( "fmt" "time" "github.com/gogf/gf/v2/os/gtime" ) func main() { // 设置进程全局时区 err := gtime.SetTimeZone("Asia/Tokyo") if err != nil { panic(err) } // 使用gtime获取当前时间 fmt.Println(gtime.Now().String()) // 使用标准库获取当前时间 fmt.Println(time.Now().String()) } ``` 执行后,输出结果为: ```html 2023-01-06 15:27:38 2023-01-06 15:27:38.753909 +0900 JST m=+0.002758145 ``` ## 时区设置注意事项 ### `SetTimeZone` 方法多次调用报错 `SetTimeZone` 方法只允许全局设置一次时区,如果多次调用,并且设置的时区不一样,后续调用将会失败,并返回 `error`。 ### 业务项目中, `time` 包初始化问题 程序时区的全局设置必须要在标准库的 `time` 包 `import` 之前调用,因为标准库的 `time` 包在 `import` 时会执行初始化,之后无法再全局修改程序时区,只能通过 `ToLocation` 方法(或者标准库 `In` 方法)对特定时间对象执行时区转换。对时间对象使用 `ToLocation` 转换的例子: ```go package main import ( "fmt" "time" "github.com/gogf/gf/v2/os/gtime" ) func main() { // 设置进程全局时区 err := gtime.SetTimeZone("Asia/Tokyo") if err != nil { panic(err) } // 使用gtime获取当前时间 fmt.Println(gtime.Now()) // 使用标准库获取当前时间 fmt.Println(time.Now()) // 对特定时间对象执行时区转换 local, err := time.LoadLocation("Asia/Shanghai") if err != nil { panic(err) } fmt.Println(gtime.Now().ToLocation(local)) } ``` 执行后,终端输出: ```html 2023-01-06 15:37:38 2023-01-06 15:37:38.753909 +0900 JST m=+0.002758145 2023-01-06 14:37:38 ``` 在业务项目中,往往在 `main` 包之前会有很多业务的包 `import`,会引起 `time` 包的初始化问题。因此,如果需要全局设置时区,建议通过一个独立的包来调用 `SetTimeZone` 方法,并且在 `main` 包的最开头执行 `import` 规避 `time` 包初始化的问题。例如: 相关参考链接: [https://stackoverflow.com/questions/54363451/setting-timezone-globally-in-golang](https://stackoverflow.com/questions/54363451/setting-timezone-globally-in-golang) ```go package main import ( _ "boot/time" "fmt" "time" ) func main() { // 使用gtime获取当前时间 fmt.Println(gtime.Now().String()) // 使用标准库获取当前时间 fmt.Println(time.Now().String()) } ``` # 时间管理-方法介绍 **提示** 以下常用方法列表,文档更新可能滞后于代码新特性,更多的方法及示例请参考代码文档: [https://pkg.go.dev/github.com/gogf/gf/v2/os/gtime](https://pkg.go.dev/github.com/gogf/gf/v2/os/gtime) ## `New` - 说明: `New` 创建并返回一个具有给定参数的 `Time` 对象。 - 格式: ```go func New(param ...interface{}) *Time ``` - 示例:创建时间对象。 ```go func ExampleNew() { t1 := gtime.New(time.Now()) t2 := gtime.New("2018-08-08 08:08:08") t3 := gtime.New(1533686888) fmt.Println(t1) fmt.Println(t2) fmt.Println(t3) // Output: // 2021-11-18 14:18:27 // 2018-08-08 08:08:08 // 2018-08-08 08:08:08 ``` ## `Now` - 说明: `Now` 创建并返回一个当前时间对象。 - 格式: ```go func Now() *Time ``` - 示例:获取当前时间对象。 ```go func ExampleNow() { t := gtime.Now() fmt.Println(t) // Output: // 2021-11-06 13:41:08 } ``` ## `Format` - 说明: 格式化输出时间 - 格式: ```go func (t *Time) Format(format string) string ``` - 示例:格式化输出时间。完整的时间格式可查阅 [时间管理-时间格式](./os-gtime-format.md)。 ```go func ExampleTime_Format() { gt1 := gtime.New("2018-08-08 08:08:08") fmt.Println(gt1.Format("Y-m-d")) fmt.Println(gt1.Format("l")) fmt.Println(gt1.Format("F j, Y, g:i a")) fmt.Println(gt1.Format("j, n, Y")) fmt.Println(gt1.Format("h-i-s, j-m-y, it is w Day z")) fmt.Println(gt1.Format("D M j G:i:s T Y")) // Output: // 2018-08-08 // Wednesday // August 8, 2018, 8:08 am // 8, 8, 2018 // 08-08-08, 8-08-18, 0831 0808 3 Wedam18 219 // Wed Aug 8 8:08:08 CST 2018 } ``` ## `String` - 说明:  输出字符串 - 格式: ```go func (t *Time) String() string ``` - 示例:输出字符串类型。 ```go func ExampleTime_String() { gt := gtime.New("2018-08-08 08:08:08") t1 := gt.String() fmt.Println(t1) fmt.Println(reflect.TypeOf(t1)) // Output: // 2018-08-08 08:08:08 // string } ``` ## `Timestamp` - 说明:  获取当前对象的秒时间戳。相应的还有 `TimestampMicro/TimestampMilli/TimestampNano`。 - 格式: ```go func (t *Time) Timestamp() int64 func Timestamp() int64 ``` - 示例:获取当前对象的秒时间戳。 ```go func ExampleTime_Timestamp() { t := gtime.Now() fmt.Println(t.Timestamp()) fmt.Println(gtime.Timestamp()) fmt.Println(t.TimestampMicro()) fmt.Println(t.TimestampMilli()) fmt.Println(t.TimestampNano()) // Output: // 1533686888 // 1533686888 // 1533686888000 // 1533686888000000 // 1533686888000000000 } ``` ## `ToZone` - 说明:  设置时区。 - 格式: ```go func (t *Time) ToZone(zone string) (*Time, error) ``` - 示例:获取当前对象的秒时间戳。 ```go func ExampleTime_ToZone() { gt1 := gtime.Now() gt2, _ := gt1.ToZone("Asia/Shanghai") gt3, _ := gt1.ToZone("Asia/Tokyo") fmt.Println(gt2) fmt.Println(gt3) // May Output: // 2021-11-11 17:10:10 // 2021-11-11 18:10:10 } ``` ## `SetTimeZone` - 说明:  设置时区。 - 格式: ```go func SetTimeZone(zone string) error ``` - 示例: 设置时区。 ```go func ExampleSetTimeZone() { gtime.SetTimeZone("Asia/Shanghai") fmt.Println(gtime.Datetime()) gtime.SetTimeZone("Asia/Tokyo") fmt.Println(gtime.Datetime()) // May Output: // 2018-08-08 08:08:08 // 2018-08-08 09:08:08 } ``` ## `StrToTime` - 说明:  时间字符串转成时间对象。 - 格式: ```go func StrToTime(str string, format ...string) (*Time, error) ``` - 示例: 时间字符串转成时间对象。 ```go func ExampleStrToTime() { res, _ := gtime.StrToTime("2006-01-02T15:04:05-07:00", "Y-m-d H:i:s") fmt.Println(res) // May Output: // 2006-01-02 15:04:05 } ``` ## `Add` - 说明:  在当前时间对象上增加时间。 - 格式: ```go func (t *Time) Add(d time.Duration) *Time ``` - 示例: 在当前时间对象上增加时间。 ```go func ExampleTime_Add() { gt := gtime.New("2018-08-08 08:08:08") gt1 := gt.Add(time.Duration(10) * time.Second) fmt.Println(gt1) // Output: // 2018-08-08 08:08:18 } ``` ## `StartOfDay` - 说明:  返回今天最开始的时间对象。类似的还有 `StartOfHalf/StartOfHour/StartOfMonth/StartOfMinute/StartOfQuarter` 等。 - 格式: ```go func (t *Time) StartOfDay() *Time ``` - 示例: 返回今天最开始的时间对象。 ```go func ExampleTime_StartOfDay() { gt1 := gtime.New("2018-08-08 08:08:08") fmt.Println(gt1.StartOfDay()) // Output: // 2018-08-08 00:00:00 } ``` ## `EndOfDay` - 说明:  返回今天结束的时间对象。类似的还有 `EndOfHalf/EndOfHour/EndOfMonth/EndOfMinute/EndOfQuarter` 等。 - 格式: ```go func (t *Time) EndOfDay() *Time ``` - 示例: 返回今天结束的时间对象。 ```go func ExampleTime_EndOfDay() { gt1 := gtime.New("2018-08-08 08:08:08") fmt.Println(gt1.EndOfDay()) // Output: // 2018-08-08 23:59:59 } ``` ## `Month` - 说明:  返回这个月处于全年的索引号。例如,1月对应的是1。 - 格式: ```go func (t *Time) Month() int ``` - 示例: 返回这个月处于全年的索引号。 ```go func ExampleTime_Month() { gt := gtime.New("2018-08-08 08:08:08") t1 := gt.Month() fmt.Println(t1) // Output: // 8 } ``` ## `Second` - 说明:  返回当前分钟的秒数。例如,10:10:08 对应的秒数为8。 - 格式: ```go func (t *Time) Second() int ``` - 示例: 返回这个月处于全年的索引号。 ```go func ExampleTime_Second() { gt := gtime.New("2018-08-08 08:08:08") t1 := gt.Second() fmt.Println(t1) // Output: // 8 } ``` ## `IsZero` - 说明:  判断时间是否等于 `0001-01-01 00:00:00`。注意不代表时间戳为0, 时间戳为0是 `1970-01-01 08:00:00` - 格式: ```go func (t *Time) IsZero() bool ``` - 示例: 返回这个月处于全年的索引号。 ```go func ExampleTime_IsZero() { gt := gtime.New("0-0-0") fmt.Println(gt.IsZero()) // Output: // true } ``` ## `AddDate` - 说明:  在当前时间对象上增加指定年月日。 - 格式: ```go func (t *Time) AddDate(years int, months int, days int) *Time ``` - 示例: 在当前时间对象上增加指定年月日。 ```go func ExampleTime_AddDate() { var ( year = 1 month = 2 day = 3 ) gt := gtime.New("2018-08-08 08:08:08") gt = gt.AddDate(year, month, day) fmt.Println(gt) // Output: // 2019-10-11 08:08:08 } ``` ## `Equal` - 说明:  判断两个时间对象是否相等。 - 格式: ```go func (t *Time) Equal(u *Time) bool ``` - 示例: 判断两个时间对象是否相等。 ```go func ExampleTime_Equal() { gt1 := gtime.New("2018-08-08 08:08:08") gt2 := gtime.New("2018-08-08 08:08:08") fmt.Println(gt1.Equal(gt2)) // Output: // true } ``` ## `Before` - 说明:  判断两个时间对象前后顺序。 - 格式: ```go func (t *Time) Before(u *Time) bool ``` - 示例: 判断两个时间对象前后顺序。 ```go func ExampleTime_Before() { gt1 := gtime.New("2018-08-07 08:08:08") gt2 := gtime.New("2018-08-08 08:08:08") fmt.Println(gt1.Before(gt2)) // Output: // true } ``` ## `After` - 说明:  判断两个时间对象前后顺序。 - 格式: ```go func (t *Time) After(u *Time) bool ``` - 示例: 判断两个时间对象前后顺序。 ```go func ExampleTime_After() { gt1 := gtime.New("2018-08-07 08:08:08") gt2 := gtime.New("2018-08-08 08:08:08") fmt.Println(gt1.After(gt2)) // Output: // false } ``` ## `Layout` - 说明:  格式化输出时间。 - 格式: ```go func (t *Time) Layout(layout string) string ``` - 示例: 格式化输出时间。 ```go func ExampleTime_Layout() { gt1 := gtime.New("2018-08-08 08:08:08") fmt.Println(gt1.Layout("2006-01-02")) // Output: // 2018-08-08 } ``` ## `IsLeapYear` - 说明:  是否闰年。 - 格式: ```go func (t *Time) IsLeapYear() bool ``` - 示例: 是否闰年。 ```go func ExampleTime_IsLeapYear() { gt1 := gtime.New("2018-08-08 08:08:08") fmt.Println(gt1.IsLeapYear()) // Output: // false } ``` ## `Date` - 说明:  获取日期。 - 格式: ```go func Date() string ``` - 示例: 获取日期。 ```go func ExampleDate() { fmt.Println(gtime.Date()) // May Output: // 2006-01-02 } ``` ## `Datetime` - 说明:  获取日期时间。 - 格式: ```go func Datetime() string ``` - 示例: 获取日期。 ```go func ExampleDatetime() { fmt.Println(gtime.Datetime()) // May Output: // 2006-01-02 15:04:05 } ``` ## `ISO8601` - 说明:  以ISO8601的格式返回时间 - 格式: ```go func ISO8601() string ``` - 示例: ```go func ExampleISO8601() { fmt.Println(gtime.ISO8601()) // May Output: // 2006-01-02T15:04:05-07:00 } ``` ## `RFC822` - 说明:  以RFC822的格式返回时间 - 格式: ```go func RFC822() string ``` - 示例: ```go func ExampleRFC822() { fmt.Println(gtime.RFC822()) // May Output: // Mon, 02 Jan 06 15:04 MST } ``` ## `StrToTimeFormat` - 说明:  `StrToTimeFormat` 根据传入的时间字符串以及格式返回时间对象 - 格式: ```go func StrToTimeFormat(str string, format string) (*Time, error) ``` - 示例: ```go func ExampleStrToTimeFormat() { res, _ := gtime.StrToTimeFormat("2006-01-02 15:04:05", "Y-m-d H:i:s") fmt.Println(res) // Output: // 2006-01-02 15:04:05 } ``` ## `StrToTimeLayout` - 说明:  `StrToTimeLayout` 根据传入的时间字符串以及格式返回时间对象 - 格式: ```go func StrToTimeLayout(str string, layout string) (*Time, error) ``` - 示例: ```go func ExampleStrToTimeLayout() { res, _ := gtime.StrToTimeLayout("2018-08-08", "2006-01-02") fmt.Println(res) // Output: // 2018-08-08 00:00:00 } ``` ## `MarshalJSON` - 说明:  `MarshalJSON` 重载 `json.Marshal` 中的方法。 - 格式: ```go func (t *Time) MarshalJSON() ([]byte, error) ``` - 示例: ```go func ExampleTime_MarshalJSON() { type Person struct { Name string `json:"name"` Birthday *gtime.Time `json:"birthday"` } p := new(Person) p.Name = "goframe" p.Birthday = gtime.New("2018-08-08 08:08:08") j, _ := json.Marshal(p) fmt.Println(string(j)) // Output: // {"name":"xiaoming","birthday":"2018-08-08 08:08:08"} } ``` ## `UnmarshalJSON` - 说明:  `UnmarshalJSON` 重载 `json.Unmarshal` 中的方法。 - 格式: ```go func (t *Time) UnmarshalJSON() ([]byte, error) ``` - 示例: ```go func ExampleTime_MarshalJSON() { type Person struct { Name string `json:"name"` Birthday *gtime.Time `json:"birthday"` } p := new(Person) p.Name = "goframe" p.Birthday = gtime.New("2018-08-08 08:08:08") j, _ := json.Marshal(p) fmt.Println(string(j)) // Output: // {"name":"xiaoming","birthday":"2018-08-08 08:08:08"} } ``` ## `WeekOfYear` - 说明:  `WeekOfYear` 返回当前周处于全年第几周,从1开始计算。类似的还有 `DayOfYear/DaysInMonth` - 格式: ```go func (t *Time) WeeksOfYear() int ``` - 示例: ```go func ExampleTime_WeeksOfYear() { gt1 := gtime.New("2018-01-08 08:08:08") fmt.Println(gt1.WeeksOfYear()) // Output: // 2 }D ``` # 定时任务-gcron ## 基本介绍 `gcron` 模块提供了对定时任务的实现,支持类似 `crontab` 的配置管理方式,并支持最小粒度到 `秒` 的定时任务管理。 **使用方式**: ```go import "github.com/gogf/gf/v2/os/gcron" ``` **接口文档**: [https://pkg.go.dev/github.com/gogf/gf/v2/os/gcron](https://pkg.go.dev/github.com/gogf/gf/v2/os/gcron) **简要说明:** 1. `New` 方法用于创建自定义的定时任务管理对象。 2. `Add` 方法用于添加定时任务,其中: - \- `pattern` 参数使用 `CRON语法格式`(具体说明见本章后续相关说明)。 - \- `job` 参数为需要执行的任务方法(方法地址)。 - \- `name` 为非必需参数,用于给定时任务指定一个 **唯一的** 名称,注意如果已存在相同名称的任务,那么添加定时任务将会失败。 3. `AddSingleton` 方法用于添加单例定时任务,即同时只能有一个该任务正在运行(在内存中进行去重判断)。 4. `AddOnce` 方法用于添加只运行一次的定时任务,当运行一次数后该定时任务自动销毁。 5. `AddTimes` 方法用于添加运行指定次数的定时任务,当运行 `times` 次数后该定时任务自动销毁。 6. `Entries` 方法用于获取当前所有已注册的定时任务信息。 7. `Remove` 方法用于根据名称删除定时任务(停止并删除)。 8. `Search` 方法用于根据名称进行定时任务搜索(返回定时任务 `*Entry` 对象指针)。 9. `Start` 方法用于启动定时任务( `Add` 后自动启动定时任务), 可通过 `name` 参数指定需要启动的任务名称。 10. `Stop` 方法用于停止定时任务( `Remove` 会停止并删除), 可通过 `name` 参数指定需要停止的任务名称。 11. `Close` 方法用于关闭自定义的定时任务管理对象。 ## 注意事项 - 进程全局时区的影响:由于定时任务严格依赖时间计算,因此进程的全局时区对定时任务执行影响比较大。在添加定时任务时,请注意当前进程的全局时区设置,在没有设置全局时区时,默认使用的是系统时区。关于时区设置更多信息请参考: [时间管理-时区设置](./os-gtime-timezone.md) # 定时任务-表达式 ## 基本介绍 `cron表达式` 表示一组时间,使用 `6` 个空格分隔的字段。 ``` Seconds Minutes Hours Day Month Week ``` > 即 `秒 分 时  日 月 周` 每个字段的含义如下: ``` Field name | Allowed values | Allowed special characters ---------- | -------------- | -------------------------- Seconds | 0-59 | * / , - # Minutes | 0-59 | * / , - Hours | 0-23 | * / , - Day | 1-31 | * / , - ? Month | 1-12 or JAN-DEC | * / , - Week | 0-6 or SUN-SAT | * / , - ? ``` **注意** 月份和星期字段值英文不区分大小写。 例如传递 `SUN`、 `Sun` 和 `sun` 同样被接受。 ## 特殊字符 #### 星号( `*`) 星号表示 `cron` 表达式将匹配所有的值。例如,在第五个字段( `Month`)中使用星号表示每个月。 #### 斜线( `/`) 斜杠用于描述范围的增量。例如:第二个字段使用 `3-59/15` 表示每小时的第 `3` 分钟开始到第 `59` 分钟,每隔 `15` 分钟执行。 #### 逗号( `,`) 逗号用于分隔列表的项目。例如,第五个字段使用 `MON,WED,FRI` 将指每周一,周三和周五执行。 #### 连字符( `-`) 连字符用于定义范围。例如,第三个字段使用 `9-17` 表示每天上午 `9` 点至下午 `5` 点(含)。 #### 忽略号( `#`) 忽略号表示 `cron` 表达式将忽略这个字段的使用,目前仅秒字段支持该符号,用于将 `6` 段 `cron pattern` 无缝转换为 `5` 段 `linux crontab pattern`。 #### 问号( `?`) 可以使用 `问号` 而不是 `*` 来让 `Day` 或 `Week` 字段为空。 #### 预定义格式 可以使用几个预定义的时间来代替 `cron` 表达式。 ``` Entry | Description | Equivalent To ----- | ----------- | ------------- @yearly (or @annually) | Run once a year, midnight, Jan. 1st | 0 0 0 1 1 * @monthly | Run once a month, midnight, first of month | 0 0 0 1 * * @weekly | Run once a week, midnight between Sat/Sun | 0 0 0 * * 0 @daily (or @midnight) | Run once a day, midnight | 0 0 0 * * * @hourly | Run once an hour, beginning of hour | 0 0 * * * * ``` #### 间隔 还可以定义任务以固定的时间间隔执行,从添加时开始运行。这可以通过格式化 `cron` 规范来支持,如下所示: ``` @every ``` 其中 `duration` 是 `time.ParseDuration` 接受的字符串 ( [http://golang.org/pkg/time/#ParseDuration](http://golang.org/pkg/time/#ParseDuration))。 例如, `@every 1h30m10s` 将表示添加任务之后每隔 `1小时30分10秒` 执行。 **注意** 间隔不会考虑任务的执行开销时间。例如,如果一项工作需要 `3` 分钟才能执行完成,并且计划每隔 `5` 分钟运行一次,那么每次任务之间只有 `2` 分钟的空闲时间。 ## 表达式示例 | 表达式示例 | 表达式说明 | | --- | --- | | `* * * * * *` | 每秒执行 | | `# * * * * *` | 每分钟执行,每一次执行至少间隔 `60` 秒 | | `2 * * * * *` | 每分钟的第 `2` 秒执行 | | `*/5 * * * * *` | 每 `5` 秒执行一次 | | `# */30 * * * *` | 每 `30` 分钟执行一次 | | `# 0 2 * * *` | 每天凌晨 `2` 点执行 | | `# */30 9-18 * * *` | 每天 `9` 点到 `18` 点,每隔 `30` 分钟执行一次 | | `# 0 9 * * MON,FRI` | 每 `周一` 和 `周五` 在 `9` 点执行一次 | ## 注意事项 所有开发语言级别 `6` 段式的 `cron pattern` 设计从实践上来说,由于底层定时器的不准确性,因此都是有一定的设计缺陷。由于 `cron pattern` 精确到秒级,当延 **迟达到了秒级**,那么任务可能会存在丢失情况。而当 `golang` 引擎调度比较慢的时候,那么延迟很容易达到秒级引发程序逻辑问题。 考虑大部分的场景其实并不需要如此精准的定时任务粒度控制,因此从框架 `v2.7` 版本开始,我们对秒字段提供了忽略符号 `#`,用于将 `6` 段式的 `cron pattern` 转换为 `5` 段式的 `linux crontab pattern`,更加稳健。如果是秒级粒度的定时任务场景,请考虑使用 `gtimer` 定时器,但同时也需要注意,任何的定时器都是不准确的,不能完全依赖底层的系统时间。 # 定时任务-基本使用 ## 基本使用 ```go package main import ( "context" "github.com/gogf/gf/v2/frame/g" "github.com/gogf/gf/v2/os/gcron" "github.com/gogf/gf/v2/os/gctx" "time" ) func main() { var ( err error ctx = gctx.New() ) _, err = gcron.Add(ctx, "* * * * * *", func(ctx context.Context) { g.Log().Print(ctx, "Every second") }, "MySecondCronJob") if err != nil { panic(err) } _, err = gcron.Add(ctx, "0 30 * * * *", func(ctx context.Context) { g.Log().Print(ctx, "Every hour on the half hour") }) if err != nil { panic(err) } _, err = gcron.Add(ctx, "@hourly", func(ctx context.Context) { g.Log().Print(ctx, "Every hour") }) if err != nil { panic(err) } _, err = gcron.Add(ctx, "@every 1h30m", func(ctx context.Context) { g.Log().Print(ctx, "Every hour thirty") }) if err != nil { panic(err) } g.Dump(gcron.Entries()) time.Sleep(3 * time.Second) g.Log().Print(ctx, `stop cronjob "MySecondCronJob"`) gcron.Stop("MySecondCronJob") time.Sleep(3 * time.Second) g.Log().Print(ctx, `start cronjob "MySecondCronJob"`) gcron.Start("MySecondCronJob") time.Sleep(3 * time.Second) } ``` 执行后,输出结果为: ```text [ { Name: "MySecondCronJob", Job: 0x14077e0, Time: "2021-11-14 12:13:53.445132 +0800 CST m=+0.006167069", }, { Name: "cron-1", Job: 0x14078a0, Time: "2021-11-14 12:13:53.44515 +0800 CST m=+0.006185688", }, { Name: "cron-2", Job: 0x1407960, Time: "2021-11-14 12:13:53.445161 +0800 CST m=+0.006196483", }, { Name: "cron-3", Job: 0x1407a20, Time: "2021-11-14 12:13:53.445218 +0800 CST m=+0.006252937", }, ] 2021-11-14 12:13:54.442 {189cwi9ngk0cfp7l8gcwciw100sr9cuu} Every second 2021-11-14 12:13:55.441 {189cwi9ngk0cfp7l8gcwciw100sr9cuu} Every second 2021-11-14 12:13:56.440 {189cwi9ngk0cfp7l8gcwciw100sr9cuu} Every second 2021-11-14 12:13:56.445 {189cwi9ngk0cfp7l8gcwciw100sr9cuu} stop cronjob "MySecondCronJob" 2021-11-14 12:13:59.445 {189cwi9ngk0cfp7l8gcwciw100sr9cuu} start cronjob "MySecondCronJob" 2021-11-14 12:14:00.443 {189cwi9ngk0cfp7l8gcwciw100sr9cuu} Every second 2021-11-14 12:14:01.442 {189cwi9ngk0cfp7l8gcwciw100sr9cuu} Every second 2021-11-14 12:14:02.443 {189cwi9ngk0cfp7l8gcwciw100sr9cuu} Every second ``` ## `AddSingleton`添加单例定时任务 单例定时任务,即同时只能有一个该任务正在运行。当第二个相同的定时任务触发执行时,如果发现已有该任务正在执行,第二个任务将会退出不执行,定时器将会继续等待下一次定时任务的触发检测,以此类推。可以使用 `AddSingleton` 添加单例定时任务。 ```go package main import ( "context" "github.com/gogf/gf/v2/frame/g" "github.com/gogf/gf/v2/os/gcron" "github.com/gogf/gf/v2/os/gctx" "time" ) func main() { var ( err error ctx = gctx.New() ) _, err = gcron.AddSingleton(ctx, "* * * * * *", func(ctx context.Context) { g.Log().Print(ctx, "doing") time.Sleep(2 * time.Second) }) if err != nil { panic(err) } select {} } ``` 执行后,输出结果为: ```text 2021-11-14 12:16:54.073 {189cwi9nmm0cfp7niz319fc100zrw0ig} doing 2021-11-14 12:16:57.072 {189cwi9nmm0cfp7niz319fc100zrw0ig} doing 2021-11-14 12:17:00.072 {189cwi9nmm0cfp7niz319fc100zrw0ig} doing 2021-11-14 12:17:03.071 {189cwi9nmm0cfp7niz319fc100zrw0ig} doing 2021-11-14 12:17:06.072 {189cwi9nmm0cfp7niz319fc100zrw0ig} doing 2021-11-14 12:17:09.072 {189cwi9nmm0cfp7niz319fc100zrw0ig} doing ... ``` ## `AddOnce`添加单次定时任务 单次定时任务, `AddOnce` 方法用于添加只运行一次的定时任务,当运行一次数后该定时任务自动销毁,`Size` 方法可以查看运行状态,相关方法: ```go package main import ( "context" "github.com/gogf/gf/v2/frame/g" "github.com/gogf/gf/v2/os/gcron" "github.com/gogf/gf/v2/os/gctx" "time" ) func main() { var ( ctx = gctx.New() ) cron := gcron.New() array := garray.New(true) cron.AddOnce(ctx, "@every 2s", func(ctx context.Context) { array.Append(1) }) fmt.Println(cron.Size(),array.Len()) time.Sleep(3000 * time.Millisecond) fmt.Println(cron.Size(),array.Len()) } ``` 执行后,输出结果为: ```text 1 0 0 1 ``` ## `AddTimes`添加指定次数的定时任务 指定次数的定时任务, `AddTimes` 方法用于添加运行指定次数的定时任务,当运行 `times` 次数后该定时任务自动销毁, `Size` 方法可以查看运行状态,相关方法: ```go package main import ( "context" "github.com/gogf/gf/v2/frame/g" "github.com/gogf/gf/v2/os/gcron" "github.com/gogf/gf/v2/os/gctx" "time" ) func main() { var ( ctx = gctx.New() ) cron := gcron.New() array := garray.New(true) cron.AddTimes(ctx, "@every 2s", 2,func(ctx context.Context) { array.Append(1) }) fmt.Println(cron.Size(), array.Len()) time.Sleep(3000 * time.Millisecond) fmt.Println(cron.Size(), array.Len()) time.Sleep(3000 * time.Millisecond) fmt.Println(cron.Size(), array.Len()) } ``` 执行后,输出结果为: ```text 1 0 1 1 0 2 ``` ## `Entries`获取注册的定时任务列表 获取所有注册的定时任务信息, `Entries` 方法用于获取当前所有已注册的定时任务信息,以切片的形式返回(按注册时间 `asc` 排序),相关方法: ```go package main import ( "context" "github.com/gogf/gf/v2/frame/g" "github.com/gogf/gf/v2/os/gcron" "github.com/gogf/gf/v2/os/gctx" "time" ) func main() { var ( ctx = gctx.New() ) cron := gcron.New() array := garray.New(true) cron.AddTimes(ctx, "@every 1s", 2,func(ctx context.Context) { array.Append(1) },"cron1") cron.AddOnce(ctx, "@every 1s",func(ctx context.Context) { array.Append(1) },"cron2") entries := cron.Entries() for k, v := range entries{ fmt.Println(k,v.Name,v.Time) } time.Sleep(3000 * time.Millisecond) fmt.Println(array.Len()) } ``` 执行后,输出结果为: ```text 0 cron2 2022-02-09 10:11:47.2421345 +0800 CST m=+0.159116501 1 cron1 2022-02-09 10:11:47.2421345 +0800 CST m=+0.159116501 3 ``` ## `Search`搜索定时任务 `Search` 搜索返回具有指定「名称」的计划任务。(返回定时任务 `*Entry` 对象指针),如果找不到,则返回nil。相关方法: ```go func main() { var ( ctx = gctx.New() ) cron := gcron.New() array := garray.New(true) cron.AddTimes(ctx, "@every 1s", 2,func(ctx context.Context) { array.Append(1) },"cron1") cron.AddOnce(ctx, "@every 1s",func(ctx context.Context) { array.Append(1) },"cron2") search := cron.Search("cron2") g.Log().Print(ctx, search) time.Sleep(3000 * time.Millisecond) fmt.Println(array.Len()) // Output: // 3 } ``` 执行后,输出结果为: ```text 2022-02-09 10:52:30.011 {18a909957cfed11680c1b145da1ef096} {"Name":"cron2","Time":"2022-02-09T10:52:29.9972842+08:00"} ``` ## `Stop`暂停定时任务 `Stop` 方法用于停止定时任务( `Stop`会停止但不会删除), 可通过 `name` 参数指定需要停止的任务名称,如果没有指定 `name`,它将停止整个 `cron`。相关方法: ```go func main() { var ( ctx = gctx.New() ) cron := gcron.New() array := garray.New(true) cron.AddTimes(ctx, "@every 2s", 1,func(ctx context.Context) { array.Append(1) },"cron1") cron.AddOnce(ctx, "@every 2s",func(ctx context.Context) { array.Append(1) },"cron2") fmt.Println(array.Len(),cron.Size()) cron.Stop("cron2") fmt.Println(array.Len(),cron.Size()) time.Sleep(3000 * time.Millisecond) fmt.Println(array.Len(),cron.Size()) // Output: // 1 // 1 } ``` 执行后,输出结果为: ```text 0 2 0 2 1 1 ``` **说明** `Stop`方法会对定时任务做暂停标记,并立即返回,并不会等待正在执行的定时任务结束。但在某些场景下,我们可能需要等待当前正在执行的定时任务完成后再退出。从`v2.8`版本开始,提供了`StopGracefully`方法,用于优雅地暂停定时任务。该方法会阻塞等待正在执行的定时任务完成后再返回。 ## `Remove`停止并删除任务 `Remove` 方法用于根据名称 `name` 删除定时任务(停止并删除);相关方法: ```go func main() { var ( ctx = gctx.New() ) cron := gcron.New() array := garray.New(true) cron.AddTimes(ctx, "@every 2s", 1,func(ctx context.Context) { array.Append(1) },"cron1") cron.AddOnce(ctx, "@every 2s",func(ctx context.Context) { array.Append(1) },"cron2") fmt.Println(array.Len(),cron.Size()) cron.Remove("cron2") fmt.Println(array.Len(),cron.Size()) time.Sleep(3000 * time.Millisecond) fmt.Println(array.Len(),cron.Size()) // Output: // 0 2 // 0 1 // 1 0 } ``` 执行后,输出结果为: ```text 0 2 0 1 1 0 ``` ## `Start`启动定时任务 `Start` 方法用于启动定时任务( `Add` 后自动启动定时任务), 可通过 `name` 参数指定需要启动的任务名称。如果没有指定 `name`,它将启动整个 `cron`。相关方法: ```go func main() { var ( ctx = gctx.New() ) cron := gcron.New() array := garray.New(true) cron.AddOnce(ctx, "@every 2s",func(ctx context.Context) { array.Append(1) },"cron2") cron.Stop("cron2") time.Sleep(3000 * time.Millisecond) fmt.Println(array.Len(),cron.Size()) cron.Start("cron2") time.Sleep(3000 * time.Millisecond) fmt.Println(array.Len(),cron.Size()) // Output: // 0 1 // 1 0 } ``` 执行后,输出结果为: ```text 0 1 1 0 ``` # 定时任务-日志管理 `gcron` 支持日志功能,并可设置日志输出的文件以及级别。默认情况下仅会输出 `LEVEL_WARN | LEVEL_ERRO | LEVEL_CRIT` 错误级别的日志(包括定时任务运行异常日志),运行日志以 `LEVEL_DEBUG` 的级别进行记录,因此默认不会记录。 `gcron` 组件使用了 `goframe` 框架统一的日志组件,因此可以复用日志组件的所有特性。相关方法: ```go func SetLogger(logger glog.ILogger) func GetLogger() glog.ILogger ``` **提示** 日志组件特性请参考 [日志组件](../core/glog.md) 章节。 使用示例: ```go package main import ( "context" "github.com/gogf/gf/v2/frame/g" "github.com/gogf/gf/v2/os/gcron" "github.com/gogf/gf/v2/os/gctx" "github.com/gogf/gf/v2/os/glog" "time" ) func main() { var ( err error ctx = gctx.New() logger = glog.New() ) logger.SetLevel(glog.LEVEL_ALL) gcron.SetLogger(logger) _, err = gcron.Add(ctx, "* * * * * ?", func(ctx context.Context) { g.Log().Info(ctx, "test") }) if err != nil { panic(err) } time.Sleep(3 * time.Second) } ``` 执行后,终端输出结果为: # 定时任务-gcron与gtimer ## `gcron` 与 `gtimer` 区别 [定时任务-gcron](./os-gcron.md) 与 [定时器-gtimer](./os-gtimer.md) 区别: - `gtimer` 属于高性能模块,是框架核心模块,构建任何定时任务的基础,任何方法操作耗时均在 `纳秒` 级别。 - `gtimer` 可适用于任何的定时任务场景中,例如: TCP通信、游戏开发等场景。 - `gcron` 支持经典的 `crontab` 形式的定时任务语法,最小时间设定间隔为 `秒`。 - `gcron` 底层实现基于 `gtimer`。 | 相似模块 | 说明 | 性能 | 类Linux Crontab模式 | 底层实现 | | --- | --- | --- | --- | --- | | [定时任务-gcron](./os-gcron.md) | 定时任务。
    较上层封装,时间刻度以自然秒为单位。 | 一般 | 支持 | 基于 `gtimer` | | [定时器-gtimer](./os-gtimer.md) | 定时器。
    底层组件,时间刻度以时间槽为单位(时间槽可自定义)。 | 高效 | 不支持 | 基于 `PriorityQueue` 数据结构自实现 | # 文件监控-gfsnotify ## 基本介绍 `gfsnotify` 能监控指定文件/目录的改变,如文件的增加、删除、修改、重命名等操作。 **使用方式**: ```go import "github.com/gogf/gf/v2/os/gfsnotify" ``` **接口文档**: [https://pkg.go.dev/github.com/gogf/gf/v2/os/gfsnotify](https://pkg.go.dev/github.com/gogf/gf/v2/os/gfsnotify) 推荐使用 `gfsnotify` 模块提供的 `Add` 和 `Remove` 模块方法,用于添加监控和取消监控。推荐原因见随后章节说明。 此外也可能通过 `New` 方法创建一个监控管理对象之后再进行监控管理。其中,添加监控的时候需要给定触发监控时的回调函数,参数类型为 `*gfsnotify.Event` 对象指针。 # 文件监控-添加监控 ## 添加监控 ```go package main import ( "context" "github.com/gogf/gf/v2/frame/g" "github.com/gogf/gf/v2/os/gfsnotify" ) func main() { var ( path = "/home/john/temp" ctx = context.Background() logger = g.Log() callback = func(event *gfsnotify.Event) { if event.IsCreate() { logger.Debug(ctx, "创建文件 : ", event.Path) } if event.IsWrite() { logger.Debug(ctx, "写入文件 : ", event.Path) } if event.IsRemove() { logger.Debug(ctx, "删除文件 : ", event.Path) } if event.IsRename() { logger.Debug(ctx, "重命名文件 : ", event.Path) } if event.IsChmod() { logger.Debug(ctx, "修改权限 : ", event.Path) } logger.Debug(ctx, event) } ) _, err := gfsnotify.Add(path, callback, gfsnotify.WatchOption{}) if err != nil { logger.Fatal(ctx, err) } else { select {} } } ``` 其中 `/home/john` 参数为一个目录,当我们在 `/home/john` 目录下创建/删除/修改文件时,可以看到 `gfsnotify` 监控到了文件的修改并输出了对应的事件信息。 ## 递归监控 我们可以通过`gfsnotify.WatchOption`来设置监控的一些选项,例如是否递归监控。在默认情况下,`Add`方法会执行递归监控,也就是说当目录下的文件(包括子目录下的文件)发生变化时,也会收到文件监控信息回调。 如果在监控目录下创建新的目录,并且新的目录中继续创建新的目录或文件,以此类推,新创建的目录或文件也将自动被监控。 # 文件监控-移除监控 移除监控我们可以使用 `Remove` 方法,会移除对整个文件/目录的监控。 当对同一个文件/目录存在多个监控回调时,我们可以通过 `RemoveCallback` 方法移除指定的回调。方法参数 `callbackId` 是在添加监控时返回的 `Callback` 对象的唯一ID。 ## 使用示例1 ```go package main import ( "context" "time" "github.com/gogf/gf/v2/frame/g" "github.com/gogf/gf/v2/os/gfsnotify" "github.com/gogf/gf/v2/os/gtimer" ) func main() { var ( ctx = context.Background() logger = g.Log() ) c1, err := gfsnotify.Add("/home/john/temp/log", func(event *gfsnotify.Event) { logger.Debug(ctx, "callback1") }) if err != nil { panic(err) } c2, err := gfsnotify.Add("/home/john/temp/log", func(event *gfsnotify.Event) { logger.Debug(ctx, "callback2") }) if err != nil { panic(err) } // 5秒后移除c1的回调函数注册,仅剩c2 gtimer.SetTimeout(ctx, 5*time.Second, func(ctx context.Context) { err = gfsnotify.RemoveCallback(c1.Id) logger.Debug(ctx, "remove callback c1", err) }) // 10秒后移除c2的回调函数注册,所有的回调都移除,不再有任何打印信息输出 gtimer.SetTimeout(ctx, 10*time.Second, func(ctx context.Context) { err = gfsnotify.RemoveCallback(c2.Id) logger.Debug(ctx, "remove callback c2", err) }) select {} } ``` ## 使用示例2 ```go package main import ( "context" "time" "github.com/gogf/gf/v2/frame/g" "github.com/gogf/gf/v2/os/gfsnotify" "github.com/gogf/gf/v2/os/gtimer" ) func main() { var ( ctx = context.Background() logger = g.Log() callback = func(event *gfsnotify.Event) { logger.Debug(ctx, "callback") } ) cb, err := gfsnotify.Add("/home/john/temp", callback) if err != nil { panic(err) } // 在此期间创建文件、目录、修改文件、删除文件 // 20秒后移除回调函数注册,所有的回调都移除,不再有任何打印信息输出 gtimer.SetTimeout(ctx, 20*time.Second, func(ctx context.Context) { err = gfsnotify.RemoveCallback(cb.Id) logger.Debug(ctx, "remove callback", err) }) select {} } ``` # 文件监控-系统参数 ## 系统参数影响 在 `linux` 系统下, `gfsnotify` 模块使用的是系统的 `inotify` 特性来实现的文件/目录监控,因此该功能在使用时会受到系统的两个内核参数限制: - `fs.inotify.max_user_instances`:表示当前用户可创建的 `inotify` 监控实例数量,即 `gfsnotify.New` 方法创建的 `Watcher` 对象数量,一个 `Watcher` 对象对应系统的一个 `inotify` 实例,系统默认数量为: `128`。 - `fs.inotify.max_user_watches`:表示一个 `inotify` 实例可添加的监控文件队列大小,往同一个 `inotify` 添加的监控文件超过该数量限制则会失败,并且会有系统错误日志,系统默认数量往往为: `8192`(有的系统该数值会比较大一些)。 ## 查看与修改 以`fs.inotify.max_user_instances`为例,在`linux`系统下,可以通过以下命令查看`fs.inotify.max_user_instances`的当前值: ```bash cat /proc/sys/fs/inotify/max_user_instances ``` 如果需要修改该值,可以使用以下命令(例如将值修改为 `1024`): ```bash sudo sysctl -w fs.inotify.max_user_instances=1024 ``` 要永久修改该值,可以将以下内容添加到`/etc/sysctl.conf`文件中: ```bash fs.inotify.max_user_instances=1024 ``` 然后执行以下命令使更改生效: ```bash sudo sysctl -p ``` # 进程管理-gproc ## 基本介绍 进程管理以及进程间的通信是通过 `gproc` 模块实现的,其中进程间通信采用的是本地socket通信机制。 **使用方式**: ```go import "github.com/gogf/gf/v2/os/gproc" ``` **接口文档**: [https://pkg.go.dev/github.com/gogf/gf/v2/os/gproc](https://pkg.go.dev/github.com/gogf/gf/v2/os/gproc) **简要说明:** 1. `Manager` 对象为进程管理对象,可以同时管理多个子进程(当前执行进程为父进程); 2. `Process` 为进程对象,表示特定执行或者获取的一个进程资源; 3. 我们可以通过 `Shell`、 `ShellExec`、 `ShellRun` 来执行Shell指令: - `Shell` 表示一个原生的Shell指令执行方式,带自定义的输入和输出控制; - `ShellExec` 执行命令后将会返回输出的结果内容; - `ShellRun` 执行命令后将会直接将返回内容输出到标准输出; - 我们可以使用 `goroutine` 来实现异步的执行,如: `go ShellRun(...)` 等等; # 进程管理-基本使用 ## 执行Shell命令 ```go package main import ( "fmt" "github.com/gogf/gf/v2/os/gctx" "github.com/gogf/gf/v2/os/gproc" ) func main() { r, err := gproc.ShellExec(gctx.New(), `sleep 3; echo "hello gf!";`) fmt.Println("result:", r) fmt.Println(err) } ``` 执行后,可以看到程序等待了3秒之后,输出结果为: ``` result: hello gf! ``` ## 主进程与子进程 由 `gproc.Manager` 对象创建的进程都默认带子进程标识,在子进程程序中可以通过 `gproc.IsChild()` 方法来判断自身是否为子进程。 ```go package main import ( "os" "time" "github.com/gogf/gf/v2/frame/g" "github.com/gogf/gf/v2/os/gctx" "github.com/gogf/gf/v2/os/gproc" ) func main() { var ctx = gctx.New() if gproc.IsChild() { g.Log().Printf(ctx, "%d: Hi, I am child, waiting 3 seconds to die", gproc.Pid()) time.Sleep(time.Second) g.Log().Printf(ctx, "%d: 1", gproc.Pid()) time.Sleep(time.Second) g.Log().Printf(ctx, "%d: 2", gproc.Pid()) time.Sleep(time.Second) g.Log().Printf(ctx, "%d: 3", gproc.Pid()) } else { m := gproc.NewManager() p := m.NewProcess(os.Args[0], os.Args, os.Environ()) p.Start(ctx) p.Wait() g.Log().Printf(ctx, "%d: child died", gproc.Pid()) } } ``` 执行后,终端打印结果如下: ```html 2018-05-18 14:35:41.360 28285: Hi, I am child, waiting 3 seconds to die 2018-05-18 14:35:42.361 28285: 1 2018-05-18 14:35:43.361 28285: 2 2018-05-18 14:35:44.361 28285: 3 2018-05-18 14:35:44.362 28278: child died ``` ## 多进程管理 `gproc` 除了能够创建子进程,管理子进程之外,也能管理非自身创建的其他进程。 `gproc` 可以同时管理多个进程,这里以单个进程为例来演示对进程的管理功能。 1. 我们使用 `gedit` 软件(Linux下常用的文本编辑器)随意打开一个文件,在进程当中我们看到该gedit的进程ID为 `28536` ```shell $ ps aux | grep gedit john 28536 3.6 0.6 946208 56412 ? Sl 14:39 0:00 gedit /home/john/Documents/text ``` 2. 我们的程序如下: ```go package main import ( "fmt" "github.com/gogf/gf/v2/os/gproc" ) func main() { pid := 28536 m := gproc.NewManager() m.AddProcess(pid) m.KillAll() m.WaitAll() fmt.Printf("%d was killed\n", pid) } ``` 执行后, `gedit` 被关闭,终端输出信息为: ``` 28536 was killed ``` # 进程管理-进程通信 **警告** 目前 `gproc` 组件提供的进程通信特性属于实验性特性。 > 不要通过共享内存来通信,而应该通过通信来共享内存。 常见的进程通信方式有 `5` 种: `管道/信号/共享内存/共享文件/Socket`,这几种方式通常有着各自比较擅长的使用场景。 - `信号`:信号常用在 `*nix` 系统中,跨平台性比较差,信息传递方式和内容单一。 - `管道`:包括 **普通管道** 与 **命名管道**,这种方式在子父进程通信场景比较常用,跨不同关系进程之间通信不太适用。 - `共享内存/共享文件`:按照并发架构的设计来讲,我们尽可能地少用 `锁机制`,包括共享内存(内存锁)/共享文件(文件锁)其实都是需要依靠锁机制才能保证数据流的正确性,因为锁机制带来的维护复杂度往往会比其带来的收益更高。 `gproc` 实现的进程通信主要机制采用的是 `Socket`,这种机制优点是功能比较稳定、使用场景比较通用。 `gproc` 的进程通信API非常简便,只需通过以下两个方法实现: ```go func Send(pid int, data []byte) error func Receive() *Msg ``` 我们通过 `Send` 方法向指定的进程发送数据(每调用一次相当于发送一条消息),在指定的进程中可以通过 `Receive` 方法获得数据。其中, `Receive` 方法提供了类似消息队列的形式来收取其他进程传递的数据,当队列为空时,该方法将会 `阻塞` 等待。 我们来看一个进程间通信的基本使用示例: ```go package main import ( "context" "fmt" "github.com/gogf/gf/v2/os/gctx" "github.com/gogf/gf/v2/os/gproc" "github.com/gogf/gf/v2/os/gtime" "github.com/gogf/gf/v2/os/gtimer" "os" "time" ) var ( ctx = gctx.New() ) func main() { fmt.Printf("%d: I am child? %v\n", gproc.Pid(), gproc.IsChild()) if gproc.IsChild() { gtimer.SetInterval(ctx, time.Second, func(ctx context.Context) { err := gproc.Send(gproc.PPid(), []byte(gtime.Datetime())) if err != nil { return } }) select {} } else { m := gproc.NewManager() p := m.NewProcess(os.Args[0], os.Args, os.Environ()) p.Start(ctx) for { msg := gproc.Receive() fmt.Printf("receive from %d, data: %s\n", msg.SenderPid, string(msg.Data)) } } } ``` 该示例中,我们的主进程启动时创建了一个子进程,该子进程每隔1秒钟向主进程发送当前的时间,主进程收取到子进程发送的参数后输出到终端上。执行后,终端输出的内容如下: ``` 29978: I am child? false 29984: I am child? true receive from 29984, data: 2018-05-18 15:01:00 receive from 29984, data: 2018-05-18 15:01:01 receive from 29984, data: 2018-05-18 15:01:02 receive from 29984, data: 2018-05-18 15:01:03 receive from 29984, data: 2018-05-18 15:01:04 ... ``` # 进程管理-信号监听 ## 基本介绍 `gproc` 组件提供了统一的信号监听和回调处理功能,目的是解决在程序中多个不同组件冗余的信号处理逻辑,以及接收退出信号后无法平滑析构的痛点。在没有统一退出信号监听的组件下,当多个组件通过 `goroutine` 异步监听信号,主 `goroutine` 接收到退出信号往往会直接退出,或者等待一段无法预测的时间退出,造成程序其实无法平滑退出,可能引起一些不可预料的问题。 `gproc` 通过统一的信号注册和回调处理,使得各个组件能够有效地接收到退出信号并做相应析构处理,使得程序的信号处理逻辑更加严谨。 相关方法: ```go // AddSigHandler adds custom signal handler for custom one or more signals. func AddSigHandler(handler SigHandler, signals ...os.Signal) // AddSigHandlerShutdown adds custom signal handler for shutdown signals: // syscall.SIGINT, // syscall.SIGQUIT, // syscall.SIGKILL, // syscall.SIGTERM, // syscall.SIGABRT. func AddSigHandlerShutdown(handler ...SigHandler) // Listen blocks and does signal listening and handling. func Listen() ``` 简要介绍: - `AddSigHanlder` 方法用于添加对指定信号的监听和对应回调函数注册。 - `AddSigHandlerShutdown` 方法用于添加对常见进程退出信号的监听和对应回调函数注册,可以注册多个 `SigHandler`。 - `Listen` 方法用于阻塞监听信号并自动执行回调函数调用。 我们来看两个示例。 ## 示例1,使用标准库信号监听 使用标准库信号监听机制的常见代码逻辑如下: ```go package main import ( "fmt" "os" "os/signal" "syscall" "time" ) func signalHandlerForMQ() { var ( sig os.Signal receivedChan = make(chan os.Signal) ) signal.Notify( receivedChan, syscall.SIGINT, syscall.SIGQUIT, syscall.SIGKILL, syscall.SIGTERM, syscall.SIGABRT, ) for { sig = <-receivedChan fmt.Println("MQ is shutting down due to signal:", sig.String()) time.Sleep(time.Second) fmt.Println("MQ is shut down smoothly") return } } func main() { fmt.Println("Process start, pid:", os.Getpid()) go signalHandlerForMQ() var ( sig os.Signal receivedChan = make(chan os.Signal) ) signal.Notify( receivedChan, syscall.SIGINT, syscall.SIGQUIT, syscall.SIGKILL, syscall.SIGTERM, syscall.SIGABRT, ) for { sig = <-receivedChan fmt.Println("MainProcess is shutting down due to signal:", sig.String()) return } } ``` 我们通过 `go run` 命令来执行一下,随后通过 `Ctrl+C` 快捷键退出( `Mac` 用户通过 `Command+C`)。 ```bash $ go run signal_handler.go Process start, pid: 21928 ^CMainProcess is shutting down due to signal: interrupt MQ is shutting down due to signal: interrupt ``` 可以看到,好可惜,那个 `MQ` 的 `goroutine` 还没完全退出进程即被强行关闭。 ## 示例2,使用 `gproc` 信号监听 使用 `gproc` 组件改进后的信号监听机制如下: ```go package main import ( "fmt" "github.com/gogf/gf/v2/os/gproc" "os" "time" ) func signalHandlerForMQ(sig os.Signal) { fmt.Println("MQ is shutting down due to signal:", sig.String()) time.Sleep(time.Second) fmt.Println("MQ is shut down smoothly") } func signalHandlerForMain(sig os.Signal) { fmt.Println("MainProcess is shutting down due to signal:", sig.String()) } func main() { fmt.Println("Process start, pid:", os.Getpid()) gproc.AddSigHandlerShutdown( signalHandlerForMQ, signalHandlerForMain, ) gproc.Listen() } ``` 我们通过 `go run` 命令来执行一下,随后通过 `Ctrl+C` 快捷键退出( `Mac` 用户通过 `Command+C`)。 ```bash $ go run signal_handler_gproc.go Process start, pid: 22876 ^CMQ is shutting down due to signal: interrupt MainProcess is shutting down due to signal: interrupt MQ is shut down smoothly ``` 看到差别了吧。所有的信号监听函数都正常结束后,随后进程平滑退出。 # 进程管理-链路跟踪 ## 基本介绍 进程管理组件支持跨进程的链路跟踪特性,特别是对于一些临时运行的进程特别有用。框架整体的链路跟踪都是采用的 `OpenTelemetry` 规范。 ## 使用示例 ### 主进程 `main.go` ```go package main import ( "github.com/gogf/gf/v2/frame/g" "github.com/gogf/gf/v2/os/gctx" "github.com/gogf/gf/v2/os/gproc" ) func main() { ctx := gctx.GetInitCtx() g.Log().Debug(ctx, `this is main process`) if err := gproc.ShellRun(ctx, `go run sub.go`); err != nil { panic(err) } } ``` ### 子进程 `sub.go` ```go package main import ( "github.com/gogf/gf/v2/frame/g" "github.com/gogf/gf/v2/os/gctx" ) func main() { ctx := gctx.GetInitCtx() g.Log().Debug(ctx, `this is sub process`) } ``` ### 执行结果 执行后,终端输出如下: ```bash $ go run main.go 2022-06-21 20:35:06.196 [DEBU] {00698a61e2a2fa1661da5d7993d72e8c} this is main process 2022-06-21 20:35:07.482 [DEBU] {00698a61e2a2fa1661da5d7993d72e8c} this is sub process ``` 可以看到,链路信息已经自动传递给了子进程。 # 字符串处理-gstr `gstr` 提供了强大便捷的文本处理组件,组件内置了大量常用的字符串处理方法,比较于 `Golang` 标准库更加全面丰富,可应对绝大部分业务场景。 **使用方式**: ```go import "github.com/gogf/gf/v2/text/gstr" ``` **接口文档**: [https://pkg.go.dev/github.com/gogf/gf/v2/text/gstr](https://pkg.go.dev/github.com/gogf/gf/v2/text/gstr) **提示** 以下常用方法列表,文档更新可能滞后于代码新特性,更多的方法及示例请参考代码文档: [https://pkg.go.dev/github.com/gogf/gf/v2/text/gstr](https://pkg.go.dev/github.com/gogf/gf/v2/text/gstr) ## 字符串判断 ### `IsNumeric` - 说明:`IsNumeric` 验证字符串 `s` 是否为数字。 - 格式: ```go IsNumeric(s string) bool ``` - 示例: ```go func ExampleIsNumeric() { fmt.Println(gstr.IsNumeric("88")) fmt.Println(gstr.IsNumeric("3.1415926")) fmt.Println(gstr.IsNumeric("abc")) // Output: // true // true // false } ``` ## 字符串长度 ### `LenRune` - 说明:`LenRune` 返回 `unicode` 字符串长度。 - 格式: ```go LenRune(str string) int ``` - 示例: ```go func ExampleLenRune() { var ( str = `GoFrame框架` result = gstr.LenRune(str) ) fmt.Println(result) // Output: // 9 } ``` ## 字符串创建 ### `Repeat` - 说明:`Repeat` 返回一个由 `input` 重复 `multiplier` 次后组成的新字符串。 - 格式: ```go Repeat(input string, multiplier int) string ``` - 示例: ```go func ExampleRepeat() { var ( input = `goframe ` multiplier = 3 result = gstr.Repeat(input, multiplier) ) fmt.Println(result) // Output: // goframe goframe goframe } ``` ## 大小写转换 ### `ToLower` - 说明:`ToLower` 将 `s` 中所有 `Unicode` 字符都变为小写并返回其副本。 - 格式: ```go ToLower(s string) string ``` - 示例: ```go func ExampleToLower() { var ( s = `GOFRAME` result = gstr.ToLower(s) ) fmt.Println(result) // Output: // goframe } ``` ### `ToUpper` - 说明:`ToUpper` 将 `s` 中所有 `Unicode` 字符都变为大写并返回其副本。 - 格式: ```go ToUpper(s string) string ``` - 示例: ```go func ExampleToUpper() { var ( s = `goframe` result = gstr.ToUpper(s) ) fmt.Println(result) // Output: // GOFRAME } ``` ### `UcFirst` - 说明:`UcFirst` 将 `s` 中首字符变为大写并返回其副本。 - 格式: ```go UcFirst(s string) string ``` - 示例: ```go func ExampleUcFirst() { var ( s = `hello` result = gstr.UcFirst(s) ) fmt.Println(result) // Output: // Hello } ``` ### `LcFirst` - 说明: `LcFirst` 将 `s` 中首字符变为小写并返回其副本。 - 格式: ```go LcFirst(s string) string ``` - 示例: ```go func ExampleLcFirst() { var ( str = `Goframe` result = gstr.LcFirst(str) ) fmt.Println(result) // Output: // goframe } ``` ### `UcWords` - 说明: `UcWords` 将字符串 `str` 中每个单词的第一个字符变为大写。 - 格式: ```go UcWords(str string) string ``` - 示例: ```go func ExampleUcWords() { var ( str = `hello world` result = gstr.UcWords(str) ) fmt.Println(result) // Output: // Hello World } ``` ### `IsLetterLower` - 说明:`IsLetterLower` 验证给定的字符 `b` 是否是小写字符。 - 格式: ```go IsLetterLower(b byte) bool ``` - 示例: ```go func ExampleIsLetterLower() { fmt.Println(gstr.IsLetterLower('a')) fmt.Println(gstr.IsLetterLower('A')) // Output: // true // false } ``` ### `IsLetterUpper` - 说明:`IsLetterUpper` 验证字符 `b` 是否是大写字符。 - 格式: ```go IsLetterUpper(b byte) bool ``` - 示例: ```go func ExampleIsLetterUpper() { fmt.Println(gstr.IsLetterUpper('A')) fmt.Println(gstr.IsLetterUpper('a')) // Output: // true // false } ``` ## 字符串比较 ### `Compare` - 说明:`Compare` 返回一个按字典顺序比较两个字符串的整数。 如果 `a == b`,结果为 `0`,如果 `a < b`,结果为 `-1`,如果 `a > b`,结果为 `+1`。 - 格式: ```go Compare(a, b string) int ``` - 示例: ```go func ExampleCompare() { fmt.Println(gstr.Compare("c", "c")) fmt.Println(gstr.Compare("a", "b")) fmt.Println(gstr.Compare("c", "b")) // Output: // 0 // -1 // 1 } ``` ### `Equal` - 说明: `Equal` 返回 `a` 和 `b` 在不区分大小写的情况下是否相等。 - 格式: ```go Equal(a, b string) bool ``` - 示例: ```go func ExampleEqual() { fmt.Println(gstr.Equal(`A`, `a`)) fmt.Println(gstr.Equal(`A`, `A`)) fmt.Println(gstr.Equal(`A`, `B`)) // Output: // true // true // false } ``` ## 切分组合 ### `Split` - 说明:`Split` 用 `delimiter` 将 `str` 拆分为 `[]string`。 - 格式: ```go Split(str, delimiter string) []string ``` - 示例: ```go func ExampleSplit() { var ( str = `a|b|c|d` delimiter = `|` result = gstr.Split(str, delimiter) ) fmt.Printf(`%#v`, result) // Output: // []string{"a", "b", "c", "d"} } ``` ### `SplitAndTrim` - 说明:`SplitAndTrim` 使用 `delimiter` 将 `str` 拆分为 `[]string`,并对 `[]string` 的每个元素调用 `Trim`,并忽略在 `Trim` 之后为空的元素。 - 格式: ```go SplitAndTrim(str, delimiter string, characterMask ...string) []string ``` - 示例: ```go func ExampleSplitAndTrim() { var ( str = `a|b|||||c|d` delimiter = `|` result = gstr.SplitAndTrim(str, delimiter) ) fmt.Printf(`%#v`, result) // Output: // []string{"a", "b", "c", "d"} } ``` ### `Join` - 说明: `Join` 将 `array` 中的每一个元素连接并生成一个新的字符串。参数 `sep` 会作为新字符串的分隔符。 - 格式: ```go Join(array []string, sep string) string ``` - 示例: ```go func ExampleJoin() { var ( array = []string{"goframe", "is", "very", "easy", "to", "use"} sep = ` ` result = gstr.Join(array, sep) ) fmt.Println(result) // Output: // goframe is very easy to use } ``` ### `JoinAny` - 说明: `JoinAny` 将 `array` 中的每一个元素连接并生成一个新的字符串。参数 `sep` 会作为新字符串的分隔符。参数 `array` 可以是任意的类型。 - 格式: ```go JoinAny(array interface{}, sep string) string ``` - 示例: ```go func ExampleJoinAny() { var ( sep = `,` arr2 = []int{99, 73, 85, 66} result = gstr.JoinAny(arr2, sep) ) fmt.Println(result) // Output: // 99,73,85,66 } ``` ### `Explode` - 说明: `Explode` 使用分隔符 `delimiter` 字符串 `str` 拆分成 `[]string` - 格式: ```go Explode(delimiter, str string) []string ``` - 示例: ```go func ExampleExplode() { var ( str = `Hello World` delimiter = " " result = gstr.Explode(delimiter, str) ) fmt.Printf(`%#v`, result) // Output: // []string{"Hello", "World"} } ``` ### `Implode` - 说明: `Implode` 使用 `glue` 连接 `pieces` 字符串数组的每一个元素。 - 格式: ```go Implode(glue string, pieces []string) string ``` - 示例: ```go func ExampleImplode() { var ( pieces = []string{"goframe", "is", "very", "easy", "to", "use"} glue = " " result = gstr.Implode(glue, pieces) ) fmt.Println(result) // Output: // goframe is very easy to use } ``` ### `ChunkSplit` - 说明:`ChunkSplit` 将字符串拆分为单位为 `chunkLen` 长度更小的每一份,并用 `end` 连接每一份拆分出的字符串。 - 格式: ```go ChunkSplit(body string, chunkLen int, end string) string ``` - 示例: ```go func ExampleChunkSplit() { var ( body = `1234567890` chunkLen = 2 end = "#" result = gstr.ChunkSplit(body, chunkLen, end) ) fmt.Println(result) // Output: // 12#34#56#78#90# } ``` ### `Fields` - 说明: `Fields` 以 `[]string` 的形式返回字符串中的每个单词。 - 格式: ```go Fields(str string) []string ``` - 示例: ```go func ExampleFields() { var ( str = `Hello World` result = gstr.Fields(str) ) fmt.Printf(`%#v`, result) // Output: // []string{"Hello", "World"} } ``` ## 转义处理 ### `AddSlashes` - 说明: `AddSlashes` 将字符串中的符号前添加转义字符 `'\'` - 格式: ```go AddSlashes(str string) string ``` - 示例: ```go func ExampleAddSlashes() { var ( str = `'aa'"bb"cc\r\n\d\t` result = gstr.AddSlashes(str) ) fmt.Println(result) // Output: // \'aa\'\"bb\"cc\\r\\n\\d\\t } ``` ### `StripSlashes` - 说明: `StripSlashes` 去掉字符串 `str` 中的转义字符 `'\'`。 - 格式: ```go StripSlashes(str string) string ``` - 示例: ```go func ExampleStripSlashes() { var ( str = `C:\\windows\\GoFrame\\test` result = gstr.StripSlashes(str) ) fmt.Println(result) // Output: // C:\windows\GoFrame\test } ``` ### `QuoteMeta` - 说明:`QuoteMeta` 为str中' `. \ + * ? [ ^ ] ( $ )` 中的每个字符前添加一个转义字符 `'\'。` - 格式: ```go QuoteMeta(str string, chars ...string) string ``` - 示例: ```go func ExampleQuoteMeta() { { var ( str = `.\+?[^]()` result = gstr.QuoteMeta(str) ) fmt.Println(result) } { var ( str = `https://goframe.org/pages/viewpage.action?pageId=1114327` result = gstr.QuoteMeta(str) ) fmt.Println(result) } // Output: // \.\\\+\?\[\^\]\(\) // https://goframe\.org/pages/viewpage\.action\?pageId=1114327 } ``` ## 统计计数 ### `Count` - 说明:`Count` 计算 `substr` 在 `s` 中出现的次数。  如果在 `s` 中没有找到 `substr`,则返回 `0`。 - 格式: ```go Count(s, substr string) int ``` - 示例: ```go func ExampleCount() { var ( str = `goframe is very, very easy to use` substr1 = "goframe" substr2 = "very" result1 = gstr.Count(str, substr1) result2 = gstr.Count(str, substr2) ) fmt.Println(result1) fmt.Println(result2) // Output: // 1 // 2 } ``` ### `CountI` - 说明:`Count` 计算 `substr` 在 `s` 中出现的次数,不区分大小写。  如果在 `s` 中没有找到 `substr`,则返回 `0`。 - 格式: ```go CountI(s, substr string) int ``` - 示例: ```go func ExampleCountI() { var ( str = `goframe is very, very easy to use` substr1 = "GOFRAME" substr2 = "VERY" result1 = gstr.CountI(str, substr1) result2 = gstr.CountI(str, substr2) ) fmt.Println(result1) fmt.Println(result2) // Output: // 1 // 2 } ``` ### `CountWords` - 说明:`CountWords` 以 `map[string]int` 的形式返回 `str` 中使用的单词的统计信息。 - 格式: ```go CountWords(str string) map[string]int ``` - 示例: ```go func ExampleCountWords() { var ( str = `goframe is very, very easy to use!` result = gstr.CountWords(str) ) fmt.Printf(`%#v`, result) // Output: // map[string]int{"easy":1, "goframe":1, "is":1, "to":1, "use!":1, "very":1, "very,":1} } ``` ### `CountChars` - 说明:`CountChars` 以 `map[string]int` 的形式返回 `str` 中使用的字符的统计信息。 `noSpace` 参数可以控制是否计算空格。 - 格式: ```go CountChars(str string, noSpace ...bool) map[string]int ``` - 示例: ```go func ExampleCountChars() { var ( str = `goframe` result = gstr.CountChars(str) ) fmt.Println(result) // May Output: // map[a:1 e:1 f:1 g:1 m:1 o:1 r:1] } ``` ## 数组处理 ### `SearchArray` - 说明:`SearchArray` 在 `[]string 'a'` 中区分大小写地搜索字符串 `'s'`,返回其在 `'a'` 中的索引。 如果在 `'a'` 中没有找到 `'s'`,则返回 `-1`。 - 格式: ```go SearchArray(a []string, s string) int ``` - 示例: ```go func ExampleSearchArray() { var ( array = []string{"goframe", "is", "very", "nice"} str = `goframe` result = gstr.SearchArray(array, str) ) fmt.Println(result) // Output: // 0 } ``` ### `InArray` - 说明:`InArray校验` `[]string 'a'` 中是否有字符串 `' s '`。 - 格式: ```go InArray(a []string, s string) bool ``` - 示例: ```go func ExampleInArray() { var ( a = []string{"goframe", "is", "very", "easy", "to", "use"} s = "goframe" result = gstr.InArray(a, s) ) fmt.Println(result) // Output: // true } ``` ### `PrefixArray` - 说明: `PrefixArray` 位 `[]string array` 的每一个字符串添加 `'prefix'` 的前缀。 - 格式: ```go PrefixArray(array []string, prefix string) ``` - 示例: ```go func ExamplePrefixArray() { var ( strArray = []string{"tom", "lily", "john"} ) gstr.PrefixArray(strArray, "classA_") fmt.Println(strArray) // Output: // [classA_tom classA_lily classA_john] } ``` ## 命名转换 ### `CaseCamel` - 说明: `CaseCamel` 将字符串转换为大驼峰形式(首字母大写)。 - 格式: ```go CaseCamel(s string) string ``` - 示例: ```go func ExampleCaseCamel() { var ( str = `hello world` result = gstr.CaseCamel(str) ) fmt.Println(result) // Output: // HelloWorld } ``` ### `CaseCamelLower` - 说明: `CaseCamelLower` 将字符串转换为小驼峰形式(首字母小写)。 - 格式: ```go CaseCamelLower(s string) string ``` - 示例: ```go func ExampleCaseCamelLower() { var ( str = `hello world` result = gstr.CaseCamelLower(str) ) fmt.Println(result) // Output: // helloWorld } ``` ### `CaseSnake` - 说明: `CaseSnake` 将字符串转换中的符号(下划线,空格,点,中横线)用下划线( `_` )替换,并全部转换为小写字母。 - 格式: ```go CaseSnake(s string) string ``` - 示例: ```go func ExampleCaseSnake() { var ( str = `hello world` result = gstr.CaseSnake(str) ) fmt.Println(result) // Output: // hello_world } ``` ### `CaseSnakeScreaming` - 说明: `CaseSnakeScreaming` 把字符串中的符号(下划线,空格,点,中横线),全部替换为下划线 `'_'`,并将所有英文字母转为大写。 - 格式: ```go CaseSnakeScreaming(s string) string ``` - 示例: ```go func ExampleCaseSnakeScreaming() { var ( str = `hello world` result = gstr.CaseSnakeScreaming(str) ) fmt.Println(result) // Output: // HELLO_WORLD } ``` ### `CaseSnakeFirstUpper` - 说明: `CaseSnakeFirstUpper` 将字符串中的字母为大写时,将大写字母转换为小写字母并在其前面增加一个下划线 `'_'`,首字母大写时,只转换为小写,前面不增加下划线 `'_'`。 - 格式: ```go CaseSnakeFirstUpper(word string, underscore ...string) string ``` - 示例: ```go func ExampleCaseSnakeFirstUpper() { var ( str = `RGBCodeMd5` result = gstr.CaseSnakeFirstUpper(str) ) fmt.Println(result) // Output: // rgb_code_md5 } ``` ### `CaseKebab` - 说明: `CaseKebab` 将字符串转换中的符号(下划线,空格,点,)用中横线 `'-'` 替换,并全部转换为小写字母。 - 格式: ```go CaseKebab(s string) string ``` - 示例: ```go func ExampleCaseKebab() { var ( str = `hello world` result = gstr.CaseKebab(str) ) fmt.Println(result) // Output: // hello-world } ``` ### `CaseKebabScreaming` - 说明: `CaseKebabScreaming` 将字符串转换中的符号(下划线,空格,点,中横线)用中横线 `'-'` 替换,并全部转换为大写字母。 - 格式: ```go CaseKebabScreaming(s string) string ``` - 示例: ```go func ExampleCaseKebabScreaming() { var ( str = `hello world` result = gstr.CaseKebabScreaming(str) ) fmt.Println(result) // Output: // HELLO-WORLD } ``` ### `CaseDelimited` - 说明: `CaseDelimited` 将字符串转换中的符号进行替换。 - 格式: ```go CaseDelimited(s string, del byte) string ``` - 示例: ```go func ExampleCaseDelimited() { var ( str = `hello world` del = byte('-') result = gstr.CaseDelimited(str, del) ) fmt.Println(result) // Output: // hello-world } ``` ### `CaseDelimitedScreaming` - 说明: `CaseDelimitedScreaming` 将字符串中的符号(空格,下划线,点,中横线)用第二个参数进行替换,该函数第二个参数为替换的字符,第三个参数为大小写转换, `true` 为全部转换大写字母, `false` 为全部转为小写字母。 - 格式: ```go CaseDelimitedScreaming(s string, del uint8, screaming bool) string ``` - 示例: ```go func ExampleCaseDelimitedScreaming() { { var ( str = `hello world` del = byte('-') result = gstr.CaseDelimitedScreaming(str, del, true) ) fmt.Println(result) } { var ( str = `hello world` del = byte('-') result = gstr.CaseDelimitedScreaming(str, del, false) ) fmt.Println(result) } // Output: // HELLO-WORLD // hello-world } ``` ## 包含判断 ### `Contains` - 说明: `Contains` 返回字符串 `str` 是否包含子字符串 `substr`,区分大小写。 - 格式: ```go Contains(str, substr string) bool ``` - 示例: ```go func ExampleContains() { { var ( str = `Hello World` substr = `Hello` result = gstr.Contains(str, substr) ) fmt.Println(result) } { var ( str = `Hello World` substr = `hello` result = gstr.Contains(str, substr) ) fmt.Println(result) } // Output: // true // false } ``` ### `ContainsI` - 说明:`ContainsI` 校验 `substr` 是否在 `str` 中,不区分大小写。 - 格式: ```go ContainsI(str, substr string) bool ``` - 示例: ```go func ExampleContainsI() { var ( str = `Hello World` substr = "hello" result1 = gstr.Contains(str, substr) result2 = gstr.ContainsI(str, substr) ) fmt.Println(result1) fmt.Println(result2) // Output: // false // true } ``` ### `ContainsAny` - 说明:`ContainsAny` 校验 `s` 中是否包含 `chars`。 - 格式: ```go ContainsAny(s, chars string) bool ``` - 示例: ```go func ExampleContainsAny() { { var ( s = `goframe` chars = "g" result = gstr.ContainsAny(s, chars) ) fmt.Println(result) } { var ( s = `goframe` chars = "G" result = gstr.ContainsAny(s, chars) ) fmt.Println(result) } // Output: // true // false } ``` ## 字符串转换 ### `Chr` - 说明:`Chr` 返回一个数字 `0-255` 对应的 `ascii` 字符串。 - 格式: ```go Chr(ascii int) string ``` - 示例: ```go func ExampleChr() { var ( ascii = 65 // A result = gstr.Chr(ascii) ) fmt.Println(result) // Output: // A } ``` ### `Ord` - 说明:`Ord` 将字符串的第一个字节转换为 `0-255` 之间的值。 - 格式: ```go Ord(char string) int ``` - 示例: ```go func ExampleOrd() { var ( str = `goframe` result = gstr.Ord(str) ) fmt.Println(result) // Output: // 103 } ``` ### `OctStr` - 说明:`OctStr` 将字符串 `str` 中的八进制字符串转换为其原始字符串。 - 格式: ```go OctStr(str string) string ``` - 示例: ```go func ExampleOctStr() { var ( str = `\346\200\241` result = gstr.OctStr(str) ) fmt.Println(result) // Output: // 怡 } ``` ### `Reverse` - 说明:`Reverse` 返回 `str` 的反转字符串。 - 格式: ```go Reverse(str string) string ``` - 示例: ```go func ExampleReverse() { var ( str = `123456` result = gstr.Reverse(str) ) fmt.Println(result) // Output: // 654321 } ``` ### `NumberFormat` - 说明:`NumberFormat` 以千位分组来格式化数字。 - 参数 `decimal` 设置小数点的个数。 - 参数 `decPoint` 设置小数点的分隔符。 - 参数 `thousand` 设置千位分隔符。 - 格式: ```go NumberFormat(number float64, decimals int, decPoint, thousandsSep string) string ``` - 示例: ```go func ExampleNumberFormat() { var ( number float64 = 123456 decimals = 2 decPoint = "." thousandsSep = "," result = gstr.NumberFormat(number, decimals, decPoint, thousandsSep) ) fmt.Println(result) // Output: // 123,456.00 } ``` ### `Shuffle` - 说明:`Shuffle` 返回将 `str` 随机打散后的字符串。 - 格式: ```go Shuffle(str string) string ``` - 示例: ```go func ExampleShuffle() { var ( str = `123456` result = gstr.Shuffle(str) ) fmt.Println(result) // May Output: // 563214 } ``` ### `HideStr` - 说明: `HideStr` 将字符串 `str` 从中间字符开始,百分比 `percent` 的字符转换成 `hide` 字符串。 - 格式: ```go HideStr(str string, percent int, hide string) string ``` - 示例: ```go func ExampleHideStr() { var ( str = `13800138000` percent = 40 hide = `*` result = gstr.HideStr(str, percent, hide) ) fmt.Println(result) // Output: // 138****8000 } ``` ### `Nl2Br` - 说明:`Nl2Br` 在字符串中的所有换行符之前插入 `HTML` 换行符 `(' br ' |
    ): \n\r, \r\n, \r, \n`。 - 格式: ```go Nl2Br(str string, isXhtml ...bool) string ``` - 示例: ```go func ExampleNl2Br() { var ( str = `goframe is very easy to use` result = gstr.Nl2Br(str) ) fmt.Println(result) // Output: // goframe
    is
    very
    easy
    to
    use } ``` ### `WordWrap` - 说明: `WordWrap` 使用换行符将 `str` 换行到给定字符数(不会切分单词)。 - 格式: ```go WordWrap(str string, width int, br string) string ``` - 示例: ```go func ExampleWordWrap() { { var ( str = `A very long woooooooooooooooooord. and something` width = 8 br = "\n" result = gstr.WordWrap(str, width, br) ) fmt.Println(result) } { var ( str = `The quick brown fox jumped over the lazy dog.` width = 20 br = "
    \n" result = gstr.WordWrap(str, width, br) ) fmt.Printf("%v", result) } // Output: // A very // long // woooooooooooooooooord. // and // something // The quick brown fox
    // jumped over the lazy
    // dog. } ``` ## 域名处理 ### `IsSubDomain` - 说明:`IsSubDomain` 校验 `subDomain` 是否为 `mainDomain` 的子域名。 支持 `mainDomain` 中的 `'*'`。 - 格式: ```go IsSubDomain(subDomain string, mainDomain string) bool ``` - 示例: ```go func ExampleIsSubDomain() { var ( subDomain = `s.goframe.org` mainDomain = `goframe.org` result = gstr.IsSubDomain(subDomain, mainDomain) ) fmt.Println(result) // Output: // true } ``` ## 参数解析 ### `Parse` - 说明: `Parse` 解析字符串并以 `map[string]interface{}` 类型返回。 - 格式: ```go Parse(s string) (result map[string]interface{}, err error) ``` - 示例: ```go func ExampleParse() { { var ( str = `v1=m&v2=n` result, _ = gstr.Parse(str) ) fmt.Println(result) } { var ( str = `v[a][a]=m&v[a][b]=n` result, _ = gstr.Parse(str) ) fmt.Println(result) } { // The form of nested Slice is not yet supported. var str = `v[][]=m&v[][]=n` result, err := gstr.Parse(str) if err != nil { panic(err) } fmt.Println(result) } { // This will produce an error. var str = `v=m&v[a]=n` result, err := gstr.Parse(str) if err != nil { println(err) } fmt.Println(result) } { var ( str = `a .[[b=c` result, _ = gstr.Parse(str) ) fmt.Println(result) } // May Output: // map[v1:m v2:n] // map[v:map[a:map[a:m b:n]]] // map[v:map[]] // Error: expected type 'map[string]interface{}' for key 'v', but got 'string' // map[] // map[a___[b:c] } ``` ## 位置查找 ### `Pos` - 说明:`Pos` 返回 `needle` 在 `haystack` 中第一次出现的位置,区分大小写。 如果没有找到,则返回-1。 - 格式: ```go Pos(haystack, needle string, startOffset ...int) int ``` - 示例: ```go func ExamplePos() { var ( haystack = `Hello World` needle = `World` result = gstr.Pos(haystack, needle) ) fmt.Println(result) // Output: // 6 } ``` ### `PosRune` - 说明: `PosRune` 的作用于函数 `Pos` 相似,但支持 `haystack` 和 `needle` 为 `unicode` 字符串。 - 格式: ```go PosRune(haystack, needle string, startOffset ...int) int ``` - 示例: ```go func ExamplePosRune() { var ( haystack = `GoFrame是一款模块化、高性能、企业级的Go基础开发框架` needle = `Go` posI = gstr.PosRune(haystack, needle) posR = gstr.PosRRune(haystack, needle) ) fmt.Println(posI) fmt.Println(posR) // Output: // 0 // 22 } ``` ### `PosI` - 说明:`PosI` 返回 `needle` 在 `haystack` 中第一次出现的位置,不区分大小写。 如果没有找到,则返回-1。 - 格式: ```go PosI(haystack, needle string, startOffset ...int) int ``` - 示例: ```go func ExamplePosI() { var ( haystack = `goframe is very, very easy to use` needle = `very` posI = gstr.PosI(haystack, needle) posR = gstr.PosR(haystack, needle) ) fmt.Println(posI) fmt.Println(posR) // Output: // 11 // 17 } ``` ### `PosRuneI` - 说明: `PosRuneI` 的作用于函数 `PosI` 相似,但支持 `haystack` 和 `needle` 为 `unicode` 字符串。 - 格式: ```go PosIRune(haystack, needle string, startOffset ...int) int ``` - 示例: ```go func ExamplePosIRune() { { var ( haystack = `GoFrame是一款模块化、高性能、企业级的Go基础开发框架` needle = `高性能` startOffset = 10 result = gstr.PosIRune(haystack, needle, startOffset) ) fmt.Println(result) } { var ( haystack = `GoFrame是一款模块化、高性能、企业级的Go基础开发框架` needle = `高性能` startOffset = 30 result = gstr.PosIRune(haystack, needle, startOffset) ) fmt.Println(result) } // Output: // 14 // -1 } ``` ### `PosR` - 说明:`PosR` 返回 `needle` 在 `haystack` 中最后一次出现的位置,区分大小写。 如果没有找到,则返回-1。 - 格式: ```go PosR(haystack, needle string, startOffset ...int) int ``` - 示例: ```go func ExamplePosR() { var ( haystack = `goframe is very, very easy to use` needle = `very` posI = gstr.PosI(haystack, needle) posR = gstr.PosR(haystack, needle) ) fmt.Println(posI) fmt.Println(posR) // Output: // 11 // 17 } ``` ### `PosRuneR` - 说明: `PosRuneR` 的作用于函数 `PosR` 相似,但支持 `haystack` 和 `needle` 为 `unicode` 字符串。 - 格式: ```go PosRRune(haystack, needle string, startOffset ...int) int ``` - 示例: ```go func ExamplePosRRune() { var ( haystack = `GoFrame是一款模块化、高性能、企业级的Go基础开发框架` needle = `Go` posI = gstr.PosIRune(haystack, needle) posR = gstr.PosRRune(haystack, needle) ) fmt.Println(posI) fmt.Println(posR) // Output: // 0 // 22 } ``` ### `PosRI` - 说明:`PosRI` 返回 `needle` 在 `haystack` 中最后一次出现的位置,不区分大小写。 如果没有找到,则返回-1。 - 格式: ```go PosRI(haystack, needle string, startOffset ...int) int ``` - 示例: ```go func ExamplePosRI() { var ( haystack = `goframe is very, very easy to use` needle = `VERY` posI = gstr.PosI(haystack, needle) posR = gstr.PosRI(haystack, needle) ) fmt.Println(posI) fmt.Println(posR) // Output: // 11 // 17 } ``` ### `PosRIRune` - 说明:`PosRIRune`的作用于函数`PosRI`相似,但支持 `haystack` 和 `needle` 为 `unicode` 字符串。 - 格式: ```go PosRIRune(haystack, needle string, startOffset ...int) int ``` - 示例: ```go func ExamplePosRIRune() { var ( haystack = `GoFrame是一款模块化、高性能、企业级的Go基础开发框架` needle = `GO` posI = gstr.PosIRune(haystack, needle) posR = gstr.PosRIRune(haystack, needle) ) fmt.Println(posI) fmt.Println(posR) // Output: // 0 // 22 } ``` ## 查找替换 ### `Replace` - 说明: `Replace` 返回 `origin` 字符串中, `search` 被 `replace` 替换后的新字符串。 `search` 区分大小写。 - 格式: ```go Replace(origin, search, replace string, count ...int) string ``` - 示例: ```go func ExampleReplace() { var ( origin = `golang is very nice!` search = `golang` replace = `goframe` result = gstr.Replace(origin, search, replace) ) fmt.Println(result) // Output: // goframe is very nice! } ``` ### `ReplaceI` - 说明: `ReplaceI` 返回 `origin` 字符串中, `search` 被 `replace` 替换后的新字符串。 `search` 不区分大小写。 - 格式: ```go ReplaceI(origin, search, replace string, count ...int) string ``` - 示例: ```go func ExampleReplaceI() { var ( origin = `golang is very nice!` search = `GOLANG` replace = `goframe` result = gstr.ReplaceI(origin, search, replace) ) fmt.Println(result) // Output: // goframe is very nice! } ``` ### `ReplaceByArray` - 说明:`ReplaceByArray` 返回 `origin` 被一个切片按两个一组 `(search, replace)` 顺序替换的新字符串,区分大小写。 - 格式: ```go ReplaceByArray(origin string, array []string) string ``` - 示例: ```go func ExampleReplaceByArray() { { var ( origin = `golang is very nice` array = []string{"lang", "frame"} result = gstr.ReplaceByArray(origin, array) ) fmt.Println(result) } { var ( origin = `golang is very good` array = []string{"golang", "goframe", "good", "nice"} result = gstr.ReplaceByArray(origin, array) ) fmt.Println(result) } // Output: // goframe is very nice // goframe is very nice } ``` ### `ReplaceIByArray` - 说明:`ReplaceIByArray` 返回 `origin` 被一个切片按两个一组 `(search, replace)` 顺序替换的新字符串,不区分大小写。 - 格式: ```go ReplaceIByArray(origin string, array []string) string ``` - 示例: ```go func ExampleReplaceIByArray() { var ( origin = `golang is very Good` array = []string{"Golang", "goframe", "GOOD", "nice"} result = gstr.ReplaceIByArray(origin, array) ) fmt.Println(result) // Output: // goframe is very nice } ``` ### `ReplaceByMap` - 说明:`ReplaceByMap` 返回 `origin` 中 `map` 的 `key` 替换为 `value` 的新字符串,区分大小写。 - 格式: ```go ReplaceByMap(origin string, replaces map[string]string) string ``` - 示例: ```go func ExampleReplaceByMap() { { var ( origin = `golang is very nice` replaces = map[string]string{ "lang": "frame", } result = gstr.ReplaceByMap(origin, replaces) ) fmt.Println(result) } { var ( origin = `golang is very good` replaces = map[string]string{ "golang": "goframe", "good": "nice", } result = gstr.ReplaceByMap(origin, replaces) ) fmt.Println(result) } // Output: // goframe is very nice // goframe is very nice } ``` ### `ReplaceIByMap` - 说明:`ReplaceIByMap` 返回 `origin` 中 `map` 的 `key` 替换为 `value` 的新字符串,不区分大小写。 - 格式: ```go ReplaceIByMap(origin string, replaces map[string]string) string ``` - 示例: ```go func ExampleReplaceIByMap() { var ( origin = `golang is very nice` replaces = map[string]string{ "Lang": "frame", } result = gstr.ReplaceIByMap(origin, replaces) ) fmt.Println(result) // Output: // goframe is very nice } ``` ## 子串截取 ### `Str` - 说明: `Str` 返回从 `needle` 第一次出现的位置开始,到 `haystack` 结尾的字符串(包含 `needle` 本身)。 - 格式: ```go Str(haystack string, needle string) string ``` - 示例: ```go func ExampleStr() { var ( haystack = `xxx.jpg` needle = `.` result = gstr.Str(haystack, needle) ) fmt.Println(result) // Output: // .jpg } ``` ### `StrEx` - 说明: `StrEx` 返回从 `needle` 第一次出现的位置开始,到 `haystack` 结尾的字符串(不包含 `needle` 本身)。 - 格式: ```go StrEx(haystack string, needle string) string ``` - 示例: ```go func ExampleStrEx() { var ( haystack = `https://goframe.org/index.html?a=1&b=2` needle = `?` result = gstr.StrEx(haystack, needle) ) fmt.Println(result) // Output: // a=1&b=2 } ``` ### `StrTill` - 说明: `StrTill` 返回从 `haystack` 字符串开始到 `needle` 第一次出现的位置的字符串(包含 `needle` 本身)。 - 格式: ```go StrTill(haystack string, needle string) string ``` - 示例: ```go func ExampleStrTill() { var ( haystack = `https://goframe.org/index.html?test=123456` needle = `?` result = gstr.StrTill(haystack, needle) ) fmt.Println(result) // Output: // https://goframe.org/index.html? } ``` ### `StrTillEx` - 说明: `StrTillEx` 返回从 `haystack` 字符串开始到 `needle` 第一次出现的位置的字符串(不包含 `needle` 本身)。 - 格式: ```go StrTillEx(haystack string, needle string) string ``` - 示例: ```go func ExampleStrTillEx() { var ( haystack = `https://goframe.org/index.html?test=123456` needle = `?` result = gstr.StrTillEx(haystack, needle) ) fmt.Println(result) // Output: // https://goframe.org/index.html } ``` ### `SubStr` - 说明:`SubStr` 返回字符串 `str` 从 `start` 开始,长度为 `length` 的新字符串。 参数 `length` 是可选的,它默认使用 `str` 的长度。 - 格式: ```go SubStr(str string, start int, length ...int) (substr string) ``` - 示例: ```go func ExampleSubStr() { var ( str = `1234567890` start = 0 length = 4 subStr = gstr.SubStr(str, start, length) ) fmt.Println(subStr) // Output: // 1234 } ``` ### `SubStrRune` - 说明:`SubStrRune` 返回 `unicode` 字符串 `str` 从 `start` 开始,长度为 `length` 的新字符串。 参数 `length` 是可选的,它默认使用 `str` 的长度。 - 格式: ```go SubStrRune(str string, start int, length ...int) (substr string) ``` - 示例: ```go func ExampleSubStrRune() { var ( str = `GoFrame是一款模块化、高性能、企业级的Go基础开发框架。` start = 14 length = 3 subStr = gstr.SubStrRune(str, start, length) ) fmt.Println(subStr) // Output: // 高性能 } ``` ### `StrLimit` - 说明: `StrLimit` 取 `str` 字符串开始,长度为 `length` 的字符串,加上 `suffix...` 后返回新的字符串。 - 格式: ```go StrLimit(str string, length int, suffix ...string) string ``` - 示例: ```go func ExampleStrLimit() { var ( str = `123456789` length = 3 suffix = `...` result = gstr.StrLimit(str, length, suffix) ) fmt.Println(result) // Output: // 123... } ``` ### `StrLimitRune` - 说明: `StrLimitRune` 取 `unicode` 字符串 `str` 开始,长度为 `length` 的字符串,加上 `suffix...` 后返回新的字符串。 - 格式: ```go StrLimitRune(str string, length int, suffix ...string) string ``` - 示例: ```go func ExampleStrLimitRune() { var ( str = `GoFrame是一款模块化、高性能、企业级的Go基础开发框架。` length = 17 suffix = "..." result = gstr.StrLimitRune(str, length, suffix) ) fmt.Println(result) // Output: // GoFrame是一款模块化、高性能... } ``` ### `SubStrFrom` - 说明:`SubStrFrom` 返回字符串 `str` 从第一次出现 `need` 到 `str` 的结尾的字符串(包含 `need`)。 - 格式: ```go SubStrFrom(str string, need string) (substr string) ``` - 示例: ```go func ExampleSubStrFrom() { var ( str = "我爱GoFrameGood" need = `爱` ) fmt.Println(gstr.SubStrFrom(str, need)) // Output: // 爱GoFrameGood } ``` ### `SubStrFromEx` - 说明:`SubStrFromEx` 返回字符串 `str` 从第一次出现 `need` 到 `str` 的结尾的字符串(不包含 `need`)。 - 格式: ```go SubStrFromEx(str string, need string) (substr string) ``` - 示例: ```go func ExampleSubStrFromEx() { var ( str = "我爱GoFrameGood" need = `爱` ) fmt.Println(gstr.SubStrFromEx(str, need)) // Output: // GoFrameGood } ``` ### `SubStrFromR` - 说明:`SubStrFromR` 返回字符串 `str` 从最后一次出现 `need` 到 `str` 的结尾的字符串(包含 `need`)。 - 格式: ```go SubStrFromR(str string, need string) (substr string) ``` - 示例: ```go func ExampleSubStrFromR() { var ( str = "我爱GoFrameGood" need = `Go` ) fmt.Println(gstr.SubStrFromR(str, need)) // Output: // Good } ``` ### `SubStrFromREx` - 说明:`SubStrFromREx` 返回字符串 `str` 从最后一次出现 `need` 到 `str` 的结尾的字符串(不包含 `need`)。 - 格式: ```go SubStrFromREx(str string, need string) (substr string) ``` - 示例: ```go func ExampleSubStrFromREx() { var ( str = "我爱GoFrameGood" need = `Go` ) fmt.Println(gstr.SubStrFromREx(str, need)) // Output: // od } ``` ## 字符/子串过滤 ### `Trim` - 说明: `Trim` 从字符串的开头和结尾剪切空白(或其他字符)。 可选参数 `characterMask` 指定额外剥离的字符。 - 格式: ```go Trim(str string, characterMask ...string) string ``` - 示例: ```go func ExampleTrim() { var ( str = `*Hello World*` characterMask = "*d" result = gstr.Trim(str, characterMask) ) fmt.Println(result) // Output: // Hello Worl } ``` ### `TrimStr` - 说明:`TrimStr` 从字符串的开头和结尾去掉所有 `cut` 字符串(不会删除开头或结尾的空白)。 - 格式: ```go TrimStr(str string, cut string, count ...int) string ``` - 示例: ```go func ExampleTrimStr() { var ( str = `Hello World` cut = "World" count = -1 result = gstr.TrimStr(str, cut, count) ) fmt.Println(result) // Output: // Hello } ``` ### `TrimLeft` - 说明:`TrimLeft` 将字符串开头的空格(或其他字符)删除。 - 格式: ```go TrimLeft(str string, characterMask ...string) string ``` - 示例: ```go func ExampleTrimLeft() { var ( str = `*Hello World*` characterMask = "*" result = gstr.TrimLeft(str, characterMask) ) fmt.Println(result) // Output: // Hello World* } ``` ### `TrimLeftStr` - 说明:`TrimLeftStr` 从字符串的开头删除 `count` 个 `cut` 字符串(不会删除开头的空格)。 - 格式: ```go TrimLeftStr(str string, cut string, count ...int) string ``` - 示例: ```go func ExampleTrimLeftStr() { var ( str = `**Hello World**` cut = "*" count = 1 result = gstr.TrimLeftStr(str, cut, count) ) fmt.Println(result) // Output: // *Hello World** } ``` ### `TrimRight` - 说明: `TrimRight` 从字符串的末尾去掉空白(或其他字符)。 - 格式: ```go TrimRight(str string, characterMask ...string) string ``` - 示例: ```go func ExampleTrimRight() { var ( str = `**Hello World**` characterMask = "*def" // []byte{"*", "d", "e", "f"} result = gstr.TrimRight(str, characterMask) ) fmt.Println(result) // Output: // **Hello Worl } ``` ### `TrimRightStr` - 说明:`TrimRightStr` 从字符串的尾部删除 `count` 个 `cut` 字符串(不会删除尾部的空格)。 - 格式: ```go TrimRightStr(str string, cut string, count ...int) string ``` - 示例: ```go func ExampleTrimRightStr() { var ( str = `Hello World!` cut = "!" count = -1 result = gstr.TrimRightStr(str, cut, count) ) fmt.Println(result) // Output: // Hello World } ``` ### `TrimAll` - 说明: `TrimAll` 删除字符串 `str` 中的所有空格(或其他字符)以及 `characterMask` 字符。 - 格式: ```go TrimAll(str string, characterMask ...string) string ``` - 示例: ```go func ExampleTrimAll() { var ( str = `*Hello World*` characterMask = "*" result = gstr.TrimAll(str, characterMask) ) fmt.Println(result) // Output: // HelloWorld } ``` ### `HasPrefix` - 说明: `HasPrefix` 返回 `s` 是否以 `prefix` 开头。 - 格式: ```go HasPrefix(s, prefix string) bool ``` - 示例: ```go func ExampleHasPrefix() { var ( s = `Hello World` prefix = "Hello" result = gstr.HasPrefix(s, prefix) ) fmt.Println(result) // Output: // true } ``` ### `HasSuffix` - 说明: `HasSuffix` 返回 `s` 是否以 `suffix` 结束。 - 格式: ```go HasSuffix(s, suffix string) bool ``` - 示例: ```go func ExampleHasSuffix() { var ( s = `my best love is goframe` prefix = "goframe" result = gstr.HasSuffix(s, prefix) ) fmt.Println(result) // Output: // true } ``` ## 版本比较 ### `CompareVersion` - 说明:`CompareVersion` 将 `a` 和 `b` 作为标准 `GNU` 版本进行比较。 - 格式: ```go CompareVersion(a, b string) int ``` - 示例: ```go func ExampleCompareVersion() { fmt.Println(gstr.CompareVersion("v2.11.9", "v2.10.8")) fmt.Println(gstr.CompareVersion("1.10.8", "1.19.7")) fmt.Println(gstr.CompareVersion("2.8.beta", "2.8")) // Output: // 1 // -1 // 0 } ``` ### `CompareVersionGo` - 说明: `CompareVersionGo` 将 `a` 和 `b` 作为标准的 `Golang` 版本进行比较。 - 格式: ```go CompareVersionGo(a, b string) int ``` - 示例: ```go func ExampleCompareVersionGo() { fmt.Println(gstr.CompareVersionGo("v2.11.9", "v2.10.8")) fmt.Println(gstr.CompareVersionGo("v4.20.1", "v4.20.1+incompatible")) fmt.Println(gstr.CompareVersionGo( "v0.0.2-20180626092158-b2ccc119800e", "v1.0.1-20190626092158-b2ccc519800e", )) // Output: // 1 // 1 // -1 } ``` ## 相似计算 ### `Levenshtein` - 说明: `Levenshtein` 计算两个字符串之间的 `Levenshtein` 距离。 - 格式: ```go Levenshtein(str1, str2 string, costIns, costRep, costDel int) int ``` - 示例: ```go func ExampleLevenshtein() { var ( str1 = "Hello World" str2 = "hallo World" costIns = 1 costRep = 1 costDel = 1 result = gstr.Levenshtein(str1, str2, costIns, costRep, costDel) ) fmt.Println(result) // Output: // 2 } ``` ### `SimilarText` - 说明: `SimilarText` 计算两个字符串之间的相似度。 - 格式: ```go SimilarText(first, second string, percent *float64) int ``` - 示例: ```go func ExampleSimilarText() { var ( first = `AaBbCcDd` second = `ad` percent = 0.80 result = gstr.SimilarText(first, second, &percent) ) fmt.Println(result) // Output: // 2 } ``` ### `Soundex` - 说明: `Soundex` 用于计算字符串的 `Soundex` 键。 - 格式: ```go Soundex(str string) string ``` - 示例: ```go func ExampleSoundex() { var ( str1 = `Hello` str2 = `Hallo` result1 = gstr.Soundex(str1) result2 = gstr.Soundex(str2) ) fmt.Println(result1, result2) // Output: // H400 H400 } ``` # 正则表达式-gregex ## 基本介绍 `gregex` 提供了对正则表达式的支持,底层是对标准库 `regexp` 的封装,极大地简化了正则的使用,并采用了解析缓存设计,提高了执行效率。 **使用方式:** ```go import "github.com/gogf/gf/v2/text/gregex" ``` **接口文档**: [https://pkg.go.dev/github.com/gogf/gf/v2/text/gregex](https://pkg.go.dev/github.com/gogf/gf/v2/text/gregex) # 正则表达式-基本使用 一个简单示例: ```go package main import ( "fmt" "github.com/gogf/gf/v2/text/gregex" ) func main() { match, _ := gregex.MatchString(`(\w+).+\-\-\s*(.+)`, `GoFrame is best! -- John`) fmt.Printf(`%s says "%s" is the one he loves!`, match[2], match[1]) } ``` 执行后,输出结果为: ``` John says "GoFrame" is the one he loves! ``` # 正则表达式-方法介绍 **提示** 以下常用方法列表,文档更新可能滞后于代码新特性,更多的方法及示例请参考代码文档: [https://pkg.go.dev/github.com/gogf/gf/v2/text/gregex](https://pkg.go.dev/github.com/gogf/gf/v2/text/gregex) > 当函数名中有 `All` 的时候,它会继续查找非重叠的后续并返回 `slice` > > 当函数名中有 `String` 的时候,参数及返回值都是 `string`,否则为 `[]byte` ## `IsMatch/IsMatchString` - 说明: `IsMatch()` 方法可以测试字符串,判断是否匹配正则表达式的模式。如果发现一次匹配,该方法返回 `true`,否则返回 `false`。 - 格式: ```go IsMatch(pattern string, src []byte) bool IsMatchString(pattern string, src string) bool ``` - Tip: ` regexp` 已经在底层处理并缓存了 `Regex` 对象,以极大地提高执行效率,无需每次显式重新创建,下同。 - 示例: ```go func ExampleIsMatch() { patternStr := `\d+` g.Dump(gregex.IsMatch(patternStr, []byte("hello 2022! hello GoFrame!"))) g.Dump(gregex.IsMatch(patternStr, nil)) g.Dump(gregex.IsMatch(patternStr, []byte("hello GoFrame!"))) // Output: // true // false // false } ``` ## `Match/MatchString` - 说明:用来匹配子字符串, `Match` 只返回第一个匹配的结果。区别于原生的正则方法, `gregex` 是对 `FindSubmatch` 进行封装,直接返回第一个包括子模式结果的 `slice`。 - 格式 ```go Match(pattern string, src []byte) ([][]byte, error) MatchString(pattern string, src string) ([]string, error) ``` - 示例:匹配 `url` 中的参数。 ```go func ExampleMatch() { patternStr := `(\w+)=(\w+)` matchStr := "https://goframe.org/pages/viewpage.action?pageId=1114219&searchId=8QC5D1D2E!" // This method looks for the first match index result, err := gregex.Match(patternStr, []byte(matchStr)) g.Dump(result) g.Dump(err) // Output: // [ // "pageId=1114219", // "pageId", // "1114219", // ] // } ``` ## `MatchAll/MatchAllString` - 说明:用来匹配子字符串, `MatchAll` 返回全部的结果。区别于原生的正则方法, `gregex`的 `MatchAll` 是对 `FindAllSubmatch` 方法进行封装,返回所有结果集的 `slice`,包括结果集中的子模式的结果。 - 格式: ```go MatchAllString(pattern string, src string) ([][]string, error) MatchAll(pattern string, src []byte) ([][][]byte, error) ``` - 示例:下面这个例子是匹配 `url` 中的参数。 ```go func ExampleMatchAll() { patternStr := `(\w+)=(\w+)` matchStr := "https://goframe.org/pages/viewpage.action?pageId=1114219&searchId=8QC5D1D2E!" result, err := gregex.MatchAll(patternStr, []byte(matchStr)) g.Dump(result) g.Dump(err) // Output: // [ // [ // "pageId=1114219", // "pageId", // "1114219", // ], // [ // "searchId=8QC5D1D2E", // "searchId", // "8QC5D1D2E", // ], // ] // } func ExampleMatchAllString() { patternStr := `(\w+)=(\w+)` matchStr := "https://goframe.org/pages/viewpage.action?pageId=1114219&searchId=8QC5D1D2E!" result, err := gregex.MatchAllString(patternStr, matchStr) g.Dump(result) g.Dump(err) // Output: // [ // [ // "pageId=1114219", // "pageId", // "1114219", // ], // [ // "searchId=8QC5D1D2E", // "searchId", // "8QC5D1D2E", // ], // ] // } ``` ## `Quote` - 说明:将指定正则表达式中的特定符号进行转义。 - 格式: ```go Quote(s string) string ``` - 示例: ```go func ExampleQuote() { result := gregex.Quote(`[1-9]\d+`) g.Dump(result) // Output: // "\[1-9\]\\d\+" } ``` ## ` Replace/ReplaceString` - 说明:用来替换所有匹配的字符串并返回一个源字符串的拷贝。 - 格式: ```go Replace(pattern string, replace, src []byte) ([]byte, error) ReplaceString(pattern, replace, src string) (string, error) ``` - 示例 ```go func ExampleReplace() { var ( patternStr = `\d+` str = "hello GoFrame 2020!" repStr = "2021" result, err = gregex.Replace(patternStr, []byte(repStr), []byte(str)) ) g.Dump(err) g.Dump(result) // Output: // // "hello GoFrame 2021!" } ``` ## `ReplaceFunc/ReplaceStringFunc` - 说明:用来替换所有匹配的字符串,返回一个源字符串的拷贝。与 `Replace` 方法的区别在于,该方法可以在闭包中对查询进行二次判断或处理,而非简单的替换。 - 格式: ```go ReplaceFunc(pattern string, src []byte, replaceFunc func(b []byte) []byte) ([]byte, error) ReplaceStringFunc(pattern string, src string, replaceFunc func(s string) string) (string, error) ``` - 示例: ```go func ExampleReplaceFunc() { var ( patternStr = `(\d+)~(\d+)` str = "hello GoFrame 2018~2020!" ) // In contrast to [ExampleReplace] result, err := gregex.ReplaceFunc(patternStr, []byte(str), func(match []byte) []byte { g.Dump(match) return bytes.Replace(match, []byte("2020"), []byte("2023"), -1) }) g.Dump(result) g.Dump(err) // ReplaceStringFunc resultStr, _ := gregex.ReplaceStringFunc(patternStr, str, func(match string) string { g.Dump(match) return strings.Replace(match, "2020", "2023", -1) }) g.Dump(resultStr) g.Dump(err) // Output: // "2018~2020" // "hello gf 2018~2023!" // ReplaceFunc result // // "2018~2020" // "hello gf 2018-2023!" // ReplaceStringFunc result // } ``` ## `ReplaceFuncMatch/ReplaceStringFuncMatch` > `ReplaceFuncMatch` 返回 `src` 的拷贝,其中 `regexp` 的所有匹配都被应用于匹配字节切片的函数的返回值替换。 返回的替换直接替换。 - 说明:用来替换所有匹配的字符串,返回一个源字符串的拷贝。该方法的强大之处在于可以在闭包中对查询进行二次判断或处理,且 `MatchString` 函数包含了所有子模式的查询结果,而非简单的替换。 - 格式: ```go ReplaceFuncMatch(pattern string, src []byte, replaceFunc func(match [][]byte) []byte) ([]byte, error) ReplaceStringFuncMatch(pattern string, src string, replaceFunc func(match []string) string) (string, error) ``` - 示例: ```go func ExampleReplaceStringFuncMatch() { var ( patternStr = `([A-Z])\w+` str = "hello Golang 2018~2021!" ) // In contrast to [ExampleReplaceFunc] // the result contains the `pattern' of all subpatterns that use the matching function result, err := gregex.ReplaceFuncMatch(patternStr, []byte(str), func(match [][]byte) []byte { g.Dump(match) return []byte("GoFrame") }) g.Dump(result) g.Dump(err) // ReplaceStringFuncMatch resultStr, err := gregex.ReplaceStringFuncMatch(patternStr, str, func(match []string) string { g.Dump(match) match[0] = "Gf" return match[0] }) g.Dump(resultStr) g.Dump(err) // Output: // [ // "Golang", // "G", // ] // "hello GoFrame 2018~2021!" // ReplaceFuncMatch result // // [ // "Golang", // "G", // ] // "hello Gf 2018~2021!" // ReplaceStringFuncMatch result // } ``` ## `Split` - 说明:将文本内容由指定的正则表达式进行切割。不包含元字符,相当于 `strings.SplitN`。 - 格式: ```go Split(pattern string, src string) []string ``` - 示例: ```go func ExampleSplit() { patternStr := `\d+` str := "hello2020GoFrame" result := gregex.Split(patternStr, str) g.Dump(result) // Output: // [ // "hello", // "GoFrame", // ] } ``` ## `Validate` - 说明:基于原生方法 `compile` 封装,检查给定的正则表达式是否有效 - 格式: ```go Validate(pattern string) error ``` - 示例: ```go func ExampleValidate() { // Valid match statement g.Dump(gregex.Validate(`\d+`)) // Mismatched statement g.Dump(gregex.Validate(`[a-9]\d+`)) // Output: // // { // Code: "invalid character class range", // Expr: "a-9", // } } ``` # 二进制编解码-gbinary ## 基本介绍 `GoFrame`框架提供了独立的二进制数据操作包`gbinary`,主要用于各种数据类型与`[]byte`二进制类型之间的相互转换;以及针对于整型数据进行精准按位处理的功能。常用于网络通信时数据编码/解码,以及数据文件操作时的编码/解码。 **使用方式**: ```go import "github.com/gogf/gf/v2/encoding/gbinary" ``` **接口文档**: [https://pkg.go.dev/github.com/gogf/gf/v2/encoding/gbinary](https://pkg.go.dev/github.com/gogf/gf/v2/encoding/gbinary) 用于二进制数据结构转换处理的接口文档如下: ```go func Encode(vs ...interface{}) ([]byte, error) func EncodeInt(i int) []byte func EncodeInt8(i int8) []byte func EncodeInt16(i int16) []byte func EncodeInt32(i int32) []byte func EncodeInt64(i int64) []byte func EncodeUint(i uint) []byte func EncodeUint8(i uint8) []byte func EncodeUint16(i uint16) []byte func EncodeUint32(i uint32) []byte func EncodeUint64(i uint64) []byte func EncodeBool(b bool) []byte func EncodeFloat32(f float32) []byte func EncodeFloat64(f float64) []byte func EncodeString(s string) []byte func Decode(b []byte, vs ...interface{}) error func DecodeToInt(b []byte) int func DecodeToInt8(b []byte) int8 func DecodeToInt16(b []byte) int16 func DecodeToInt32(b []byte) int32 func DecodeToInt64(b []byte) int64 func DecodeToUint(b []byte) uint func DecodeToUint8(b []byte) uint8 func DecodeToUint16(b []byte) uint16 func DecodeToUint32(b []byte) uint32 func DecodeToUint64(b []byte) uint64 func DecodeToBool(b []byte) bool func DecodeToFloat32(b []byte) float32 func DecodeToFloat64(b []byte) float64 func DecodeToString(b []byte) string ``` 支持按位处理的接口文档如下: ```go func EncodeBits(bits []Bit, i int, l int) []Bit func EncodeBitsWithUint(bits []Bit, ui uint, l int) []Bit func EncodeBitsToBytes(bits []Bit) []byte func DecodeBits(bits []Bit) uint func DecodeBitsToUint(bits []Bit) uint func DecodeBytesToBits(bs []byte) []Bit ``` 其中的`Bit`类型表示一个二进制数字(0或1),其定义如下: ```go type Bit int8 ``` ## 使用示例 我们来看一个比较完整的二进制操作示例,基本演示了绝大部分的二进制转换操作。 ```go package main import ( "fmt" "github.com/gogf/gf/v2/encoding/gbinary" "github.com/gogf/gf/v2/os/gctx" "github.com/gogf/gf/v2/os/glog" ) func main() { // 使用gbinary.Encoded对基本数据类型进行二进制打包 if buffer := gbinary.Encode(18, 300, 1.01); buffer != nil { // glog.Error(err) } else { fmt.Println(buffer) } // 使用gbinary.Decode对整形二进制解包,注意第二个及其后参数为字长确定的整形变量的指针地址,字长确定的类型, // 例如:int8/16/32/64、uint8/16/32/64、float32/64 // 这里的1.01默认为float64类型(64位系统下) if buffer := gbinary.Encode(18, 300, 1.01); buffer != nil { //glog.Error(err) } else { var i1 int8 var i2 int16 var f3 float64 if err := gbinary.Decode(buffer, &i1, &i2, &f3); err != nil { glog.Error(gctx.New(), err) } else { fmt.Println(i1, i2, f3) } } // 编码/解析 int,自动识别变量长度 fmt.Println(gbinary.DecodeToInt(gbinary.EncodeInt(1))) fmt.Println(gbinary.DecodeToInt(gbinary.EncodeInt(300))) fmt.Println(gbinary.DecodeToInt(gbinary.EncodeInt(70000))) fmt.Println(gbinary.DecodeToInt(gbinary.EncodeInt(2000000000))) fmt.Println(gbinary.DecodeToInt(gbinary.EncodeInt(500000000000))) // 编码/解析 uint,自动识别变量长度 fmt.Println(gbinary.DecodeToUint(gbinary.EncodeUint(1))) fmt.Println(gbinary.DecodeToUint(gbinary.EncodeUint(300))) fmt.Println(gbinary.DecodeToUint(gbinary.EncodeUint(70000))) fmt.Println(gbinary.DecodeToUint(gbinary.EncodeUint(2000000000))) fmt.Println(gbinary.DecodeToUint(gbinary.EncodeUint(500000000000))) // 编码/解析 int8/16/32/64 fmt.Println(gbinary.DecodeToInt8(gbinary.EncodeInt8(int8(100)))) fmt.Println(gbinary.DecodeToInt16(gbinary.EncodeInt16(int16(100)))) fmt.Println(gbinary.DecodeToInt32(gbinary.EncodeInt32(int32(100)))) fmt.Println(gbinary.DecodeToInt64(gbinary.EncodeInt64(int64(100)))) // 编码/解析 uint8/16/32/64 fmt.Println(gbinary.DecodeToUint8(gbinary.EncodeUint8(uint8(100)))) fmt.Println(gbinary.DecodeToUint16(gbinary.EncodeUint16(uint16(100)))) fmt.Println(gbinary.DecodeToUint32(gbinary.EncodeUint32(uint32(100)))) fmt.Println(gbinary.DecodeToUint64(gbinary.EncodeUint64(uint64(100)))) // 编码/解析 string fmt.Println(gbinary.DecodeToString(gbinary.EncodeString("I'm string!"))) } ``` 以上程序执行结果为: ``` [18 44 1 41 92 143 194 245 40 240 63] 18 300 1.01 1 300 70000 2000000000 500000000000 1 300 70000 2000000000 500000000000 100 100 100 100 100 100 100 100 I'm string! ``` 1. **编码** `gbinary.Encode` 方法是一个非常强大灵活的方法,可以将所有的基本类型转换为二进制类型 `([ ]byte`)。在 `gbinary.Encode` 方法内部,会自动对变量进行长度计算,采用最小二进制长度来存放该变量的二进制值。例如,针对 `int` 类型值为 `1` 的变量, `gbinary.Encode` 将只会用 `1` 个 `byte` 来存储,而 `int` 类型值为 `300` 的变量,将会使用 `2` 个 `byte` 来存储,尽量减少二进制结果的存储空间。因此,在解析的时候要非常注意 `[ ]byte` 的长度,建议能够确定变量长度的地方,在进行二进制编码/解码时,尽量采用形如 `int8/16/32/64` 的定长基本类型来存储变量,这样解析的时候也能够采用对应的变量形式进行解析,不易产生错误。 `gbinary` 包也提供了一系列 `gbinary.Encode*` 的方法,用于将基本数据类型转换为二进制。其中, `gbinary.EncodeInt/gbinary.EncodeUint` 也是会在内部自动识别变量值大小,返回不定长度的 `[ ]byte` 值,长度范围 `1/2/4/8`。 2. **解码** 在二进制类型的解析操作中,二进制的长度( `[ ]byte` 的长度)是非常重要的,只有给定正确的长度才能执行正确的解析,因此 `gbinary.Decode` 方法给定的变量长度必须为确定长度类型的变量,例如: `int8/16/32/64`、 `uint8/16/32/64`、 `float32/64`,而如果给定的第二个变量地址对应的变量类型为 `int/uint`,无法确定长度,因此解析会失败。 此外, `gbinary` 包也提供了一系列 `gbinary.DecodeTo*` 的方法,用于将二进制转换为特定的数据类型。其中, `gbinary.DecodeToInt/gbinary.DecodeToUint` 方法会对二进制长度进行自动识别解析,支持的二进制参数长度范围 `1-8`。 # BASE64编解码-gbase64 `BASE64` 编码解析。 **使用方式**: ```go import "github.com/gogf/gf/v2/encoding/gbase64" ``` **接口文档**: [https://pkg.go.dev/github.com/gogf/gf/v2/encoding/gbase64](https://pkg.go.dev/github.com/gogf/gf/v2/encoding/gbase64) # 经典哈希函数-ghash ## 基本介绍 常用经典哈希函数Go语言实现,提供 `uint32` 及 `uint64` 类型的哈希函数。 **使用方式**: ```go import "github.com/gogf/gf/v2/encoding/ghash" ``` **接口文档**: [https://pkg.go.dev/github.com/gogf/gf/v2/encoding/ghash](https://pkg.go.dev/github.com/gogf/gf/v2/encoding/ghash) ## 基准测试 ``` goos: darwin goarch: amd64 pkg: github.com/gogf/gf/v2/encoding/ghash cpu: Intel(R) Core(TM) i7-9750H CPU @ 2.60GHz Benchmark_BKDR Benchmark_BKDR-12 39315165 26.88 ns/op Benchmark_BKDR64 Benchmark_BKDR64-12 62891215 22.61 ns/op Benchmark_SDBM Benchmark_SDBM-12 49689925 25.40 ns/op Benchmark_SDBM64 Benchmark_SDBM64-12 48860472 24.38 ns/op Benchmark_RS Benchmark_RS-12 39463418 25.52 ns/op Benchmark_RS64 Benchmark_RS64-12 53318370 19.45 ns/op Benchmark_JS Benchmark_JS-12 53751033 23.20 ns/op Benchmark_JS64 Benchmark_JS64-12 62440287 19.25 ns/op Benchmark_PJW Benchmark_PJW-12 42439626 27.85 ns/op Benchmark_PJW64 Benchmark_PJW64-12 37491696 33.28 ns/op Benchmark_ELF Benchmark_ELF-12 38034584 31.74 ns/op Benchmark_ELF64 Benchmark_ELF64-12 44047201 27.58 ns/op Benchmark_DJB Benchmark_DJB-12 46994352 22.60 ns/op Benchmark_DJB64 Benchmark_DJB64-12 61980186 19.19 ns/op Benchmark_AP Benchmark_AP-12 29544234 40.58 ns/op Benchmark_AP64 Benchmark_AP64-12 28123783 42.48 ns/op ``` ## 重复测试 测试结果与测试内容有关联性和随机性,我这里通过 `uint64` 数值的范围遍历来进行简单的重复性测试,本身不够严谨,因此仅供趣味性参考。 ```go package main import ( "encoding/binary" "fmt" "math" "github.com/gogf/gf/v2/encoding/ghash" ) func main() { var ( m = make(map[uint64]struct{}) b = make([]byte, 8) ok bool hash uint64 ) for i := uint64(0); i < math.MaxUint64; i++ { binary.LittleEndian.PutUint64(b, i) hash = ghash.PJW64(b) if _, ok = m[hash]; ok { fmt.Println("repeated found:", i) break } m[hash] = struct{}{} } } ``` 测试结果如下: | 哈希函数 | 重复位置 | 备注 | | --- | --- | --- | | `AP64` | `8388640` | | | `BKDR64` | `33536` | | | `DJB64` | `8448` | | | `ELF64` | `4096` | | | `JS64` | `734` | | | `PJW64` | `2` | | | `RS64` | - | 32G Memory OOM | | `SDBM64` | - | 32G Memory OOM | # HTML编解码-ghtml HTML编码解析。 **使用方式**: ```go import "github.com/gogf/gf/v2/encoding/ghtml" ``` **接口文档**: [https://pkg.go.dev/github.com/gogf/gf/v2/encoding/ghtml](https://pkg.go.dev/github.com/gogf/gf/v2/encoding/ghtml) # URL编解码-gurl `URL` 编码解析。 **使用方式**: ```go import "github.com/gogf/gf/v2/encoding/gurl" ``` **接口文档**: [https://pkg.go.dev/github.com/gogf/gf/v2/encoding/gurl](https://pkg.go.dev/github.com/gogf/gf/v2/encoding/gurl) ## `URL` 参数构建 ```go package main import ( "fmt" "github.com/gogf/gf/v2/encoding/gurl" "net/url" ) func main() { // 构建url参数 values := url.Values{} values.Add("name", "gopher") values.Add("limit", "20") values.Add("page", "7") // 生成URL编码查询字符串 limit=20&name=gopher&page=7 urlStr := gurl.BuildQuery(values) fmt.Println(urlStr) } ``` 执行后,输出结果为: ``` limit=20&name=gopher&page=7 ``` ## `URL` 参数编码与解码 ```go package main import ( "fmt" "github.com/gogf/gf/v2/encoding/gurl" "log" ) func main() { // 编码对字符串进行转义,以便可以将其安全地放置在URL查询中。 encodeStr := gurl.Encode("limit=20&name=gopher&page=7") fmt.Println(encodeStr) // 进行URL解码 decodeStr, err := gurl.Decode("limit%3D20%26name%3Dgopher%26page%3D7") if err != nil { log.Fatal(err) } fmt.Println(decodeStr) } ``` 执行后,输出结果为: ``` limit%3D20%26name%3Dgopher%26page%3D7 limit=20&name=gopher&page=7 ``` ## 解析 `URL` `component` 参数值可选项: | 参数值 | 说明 | | --- | --- | | -1 | all | | 1 | scheme | | 2 | host | | 4 | port | | 8 | user | | 16 | pass | | 32 | path | | 64 | query | | 128 | fragment | ```go package main import ( "fmt" "github.com/gogf/gf/v2/encoding/gurl" "log" ) func main() { // 解析URL并返回其组件 data, err := gurl.ParseURL("http://127.0.0.1:8199/goods?limit=20&name=gopher&page=7", -1) if err != nil { log.Fatal(err) } fmt.Println(data) fmt.Println(data["host"]) fmt.Println(data["query"]) fmt.Println(data["path"]) fmt.Println(data["scheme"]) fmt.Println(data["fragment"]) } ``` 执行后,输出结果为: ``` map[fragment: host:127.0.0.1 pass: path:/goods port:8199 query:limit=20&name=gopher&page=7 scheme:http user:] 127.0.0.1 limit=20&name=gopher&page=7 /goods http ``` # XML编解码-gxml XML数据格式编码解析。 **使用方式**: ```go import "github.com/gogf/gf/v2/encoding/gxml" ``` **接口文档**: [https://pkg.go.dev/github.com/gogf/gf/v2/encoding/gxml](https://pkg.go.dev/github.com/gogf/gf/v2/encoding/gxml) # INI编解码-gini INI数据格式编码解析。 **使用方式**: ```go import "github.com/gogf/gf/v2/encoding/gini" ``` **接口文档**: [https://pkg.go.dev/github.com/gogf/gf/v2/encoding/gini](https://pkg.go.dev/github.com/gogf/gf/v2/encoding/gini) # YAML编解码-gyaml `YAML` 数据格式编码解析。 **使用方式**: ```go import "github.com/gogf/gf/v2/encoding/gyaml" ``` **接口文档**: [https://pkg.go.dev/github.com/gogf/gf/v2/encoding/gyaml](https://pkg.go.dev/github.com/gogf/gf/v2/encoding/gyaml) # TOML编解码-gtoml `TOML` 数据格式编码解析。 **使用方式**: ```go import "github.com/gogf/gf/v2/encoding/gtoml" ``` **接口文档**: [https://pkg.go.dev/github.com/gogf/gf/v2/encoding/gtoml](https://pkg.go.dev/github.com/gogf/gf/v2/encoding/gtoml) # 字符集转换-gcharset `GoFrame`框架提供了强大的字符编码转换模块`gcharset`,支持常见字符集的相互转换。 支持的字符集: | 编码 | 字符集 | | --- | --- | | **中文** | `GBK/GB18030/GB2312/Big5` | | **日文** | `EUCJP/ISO2022JP/ShiftJIS` | | **韩文** | `EUCKR` | | **Unicode** | `UTF-8/UTF-16/UTF-16BE/UTF-16LE` | | **其他编码** | `macintosh/IBM*/Windows*/ISO-*` | **使用方式**: ```go import "github.com/gogf/gf/v2/encoding/gcharset" ``` **接口文档**: [https://pkg.go.dev/github.com/gogf/gf/v2/encoding/gcharset](https://pkg.go.dev/github.com/gogf/gf/v2/encoding/gcharset) **使用示例**: ```go package main import ( "fmt" "github.com/gogf/gf/v2/encoding/gcharset" ) func main() { var ( src = "~{;(F#,6@WCN^O`GW!#" srcCharset = "GB2312" dstCharset = "UTF-8" ) str, err := gcharset.Convert(dstCharset, srcCharset, src) if err != nil { panic(err) } fmt.Println(str) } ``` 执行后,终端输出结果为: ``` 花间一壶酒,独酌无相亲。 ``` # 压缩/解压-gcompress 二进制数据压缩/解压,支持 `Zlib`/ `GZip` 算法。 **使用方式**: ```go import "github.com/gogf/gf/v2/encoding/gcompress" ``` **接口文档**: [https://pkg.go.dev/github.com/gogf/gf/v2/encoding/gcompress](https://pkg.go.dev/github.com/gogf/gf/v2/encoding/gcompress) # 通用编解码-gjson `gjson` 模块实现了强大的数据编码/解析功能,支持数据层级检索、动态创建修改 `Json` 对象,并支持常见数据格式的解析和转换等特点。 特点: 1. 支持数据层级检索; 2. 支持运行时数据修改; 3. 支持动态创建层级数据结构,并转换为支持的数据格式; 4. 支持 `JSON`、 `XML`、 `INI`、 `YAML/YML`、 `TOML`、 `PROPERTIES`、 `Struct` 数据格式相互转换; **说明** 需要注意 `gjson` 包支持多种数据格式的读取、写入和转换,不仅仅针对 `json` 格式。 ## **使用方式**: ```go import "github.com/gogf/gf/v2/encoding/gjson" ``` **接口文档**: [https://pkg.go.dev/github.com/gogf/gf/v2/encoding/gjson](https://pkg.go.dev/github.com/gogf/gf/v2/encoding/gjson) # 通用编解码-对象创建 `gjson` 模块除了最基础支持的 `JSON` 数据格式创建 `Json` 对象,还支持常用的数据格式内容创建 `Json` 对象。支持的数据格式为: `JSON`, `XML`, `INI`, `YAML`, `TOML`, `PROPERTIES`。此外,也支持直接通过 `struct` 对象创建 `Json` 对象。 对象创建常用 `New` 和 `Load*` 方法,更多的方法请查看接口文档: [https://pkg.go.dev/github.com/gogf/gf/v2/encoding/gjson](https://pkg.go.dev/github.com/gogf/gf/v2/encoding/gjson) ### 使用 `New` 方法创建 #### 通过 `JSON` 数据创建 ```go jsonContent := `{"name":"john", "score":"100"}` j := gjson.New(jsonContent) fmt.Println(j.Get("name")) fmt.Println(j.Get("score")) // Output: // john // 100 ``` #### 通过 `XML` 数据创建 ```go jsonContent := `john100` j := gjson.New(jsonContent) // Note that there's root node in the XML content. fmt.Println(j.Get("doc.name")) fmt.Println(j.Get("doc.score")) // Output: // john // 100 ``` #### 通过 `Strcut` 对象创建 ```go type Me struct { Name string `json:"name"` Score int `json:"score"` } me := Me{ Name: "john", Score: 100, } j := gjson.New(me) fmt.Println(j.Get("name")) fmt.Println(j.Get("score")) // Output: // john // 100 ``` #### 自定义 `Struct` 转换标签 ```go type Me struct { Name string `tag:"name"` Score int `tag:"score"` Title string } me := Me{ Name: "john", Score: 100, Title: "engineer", } // The parameter specifies custom priority tags for struct conversion to map, // multiple tags joined with char ','. j := gjson.NewWithTag(me, "tag") fmt.Println(j.Get("name")) fmt.Println(j.Get("score")) fmt.Println(j.Get("Title")) // Output: // john // 100 // engineer ``` ### 使用 `Load*` 方法创建 最常用的是 `Load` 和 `LoadContent` 方法,前者通过文件路径读取,后者通过给定内容创建 `Json` 对象。方法内部会自动识别数据格式,并自动解析转换为 `Json` 对象。 #### 通过 `Load` 方法创建 1. `JSON` 文件 ```go jsonFilePath := gtest.DataPath("json", "data1.json") j, _ := gjson.Load(jsonFilePath) fmt.Println(j.Get("name")) fmt.Println(j.Get("score")) ``` 2. `XML` 文件 ```go jsonFilePath := gtest.DataPath("xml", "data1.xml") j, _ := gjson.Load(jsonFilePath) fmt.Println(j.Get("doc.name")) fmt.Println(j.Get("doc.score")) ``` #### 通过 `LoadContent` 创建 ```go jsonContent := `{"name":"john", "score":"100"}` j, _ := gjson.LoadContent(jsonContent) fmt.Println(j.Get("name")) fmt.Println(j.Get("score")) // Output: // john // 100 ``` # 通用编解码-层级访问 ## 层级访问 `gjson` 支持对数据内容进行层级检索访问,层级分隔符号默认为」 `.`「。该特性使得开发者可以灵活访问未知的数据结构内容变得非常简便。 ### 示例1,基本使用 ```go func main() { data := `{ "users" : { "count" : 2, "list" : [ {"name" : "Ming", "score" : 60}, {"name" : "John", "score" : 99.5} ] } }` if j, err := gjson.DecodeToJson(data); err != nil { panic(err) } else { fmt.Println("John Score:", j.Get("users.list.1.score")) } // Output: // John Score: 99.5 } ``` 可以看到, `gjson.Json` 对象可以通过非常灵活的层级筛选功能( `j.GetFloat32("users.list.1.score")`)检索到对应的变量信息。 ### 示例2,自定义层级分隔符号 ```go func main() { data := `{ "users" : { "count" : 2, "list" : [ {"name" : "Ming", "score" : 60}, {"name" : "John", "score" : 99.5} ] } }` if j, err := gjson.DecodeToJson(data); err != nil { panic(err) } else { j.SetSplitChar('#') fmt.Println("John Score:", j.Get("users#list#1#score")) } // Output: // John Score: 99.5 } ``` 可以看到,我们可以通过 `SetSplitChar` 方法设置我们自定义的分隔符号。 ### 示例3,处理键名本身带有层级符号」 `.`「的情况 ```go func main() { data := `{ "users" : { "count" : 100 }, "users.count" : 101 }` if j, err := gjson.DecodeToJson(data); err != nil { glog.Error(gctx.New(), err) } else { j.SetViolenceCheck(true) fmt.Println("Users Count:", j.Get("users.count")) } // Output: // Users Count: 101 } ``` 运行之后打印出的结果为 `101`。当键名存在」 `.`「号时,我们可以通过 `SetViolenceCheck` 设置冲突检测,随后检索优先级将会按照:键名->层级,便并不会引起歧义。但是当冲突检测开关开启时,检索效率将会变低,默认为关闭状态。 ## 注意事项 大家都知道,在 `Golang` 里面, `map/slice` 类型其实是一个」引用类型」(也叫」指针类型」),因此当对这种类型的变量 键值对/索引项 进行修改时,会同时修改到其对应的底层数据。 从效率上考虑, `gjson` 包某些获取方法返回的数据类型为 `map/slice` 时,没有对齐做值拷贝,因此当对返回的数据进行修改时,会同时修改 `gjson` 对应的底层数据。 例如: ```go func main() { jsonContent := `{"map":{"key":"value"}, "slice":[59,90]}` j, _ := gjson.LoadJson(jsonContent) m := j.Get("map") mMap := m.Map() fmt.Println(mMap) // Change the key-value pair. mMap["key"] = "john" // It changes the underlying key-value pair. fmt.Println(j.Get("map").Map()) s := j.Get("slice") sArray := s.Array() fmt.Println(sArray) // Change the value of specified index. sArray[0] = 100 // It changes the underlying slice. fmt.Println(j.Get("slice").Array()) // output: // map[key:value] // map[key:john] // [59 90] // [100 90] } ``` # 通用编解码-Struct转换 ## `Struct` 转换 `Struct` 方法用于将整个 `Json` 包含的数据内容转换为指定的数据格式或者对象。 ``` data := ` { "count" : 1, "array" : ["John", "Ming"] }` if j, err := gjson.DecodeToJson(data); err != nil { panic(err) } else { type Users struct { Count int Array []string } users := new(Users) if err := j.Scan(users); err != nil { panic(err) } fmt.Printf(`%+v`, users) } // Output: // &{Count:1 Array:[John Ming]} ``` # 通用编解码-动态创建修改 `gjson` 除了能够灵活解析、检索未知数据结构内容,还能够动态创建和修改数据结构内容。 ## 动态创建 ### 示例1,简单使用 ```go func main() { j := gjson.New(nil) j.Set("name", "John") j.Set("score", 99.5) fmt.Printf( "Name: %s, Score: %v\n", j.Get("name").String(), j.Get("score").Float32(), ) fmt.Println(j.MustToJsonString()) // Output: // Name: John, Score: 99.5 // {"name":"John","score":99.5} } ``` ### 示例2,创建数组 ```go func main() { j := gjson.New(nil) for i := 0; i < 5; i++ { j.Set(fmt.Sprintf(`%d.id`, i), i) j.Set(fmt.Sprintf(`%d.name`, i), fmt.Sprintf(`student-%d`, i)) } fmt.Println(j.MustToJsonString()) // Output: // [{"id":0,"name":"student-0"},{"id":1,"name":"student-1"},{"id":2,"name":"student-2"},{"id":3,"name":"student-3"},{"id":4,"name":"student-4"}] } ``` ## 动态修改 ```go func main() { data := `{ "users" : { "count" : 2, "list" : [ {"name" : "Ming", "score" : 60}, {"name" : "John", "score" : 59} ] } }` if j, err := gjson.DecodeToJson(data); err != nil { panic(err) } else { j.Set("users.list.1.score", 100) fmt.Println("John Score:", j.Get("users.list.1.score").Float32()) fmt.Println(j.MustToJsonString()) } // Output: // John Score: 100 // {"users":{"count":2,"list":[{"name":"Ming","score":60},{"name":"John","score":100}]}} } ``` `JSON` 数据通过 `gjson` 包读取后,可以通过 `Set` 方法改变内部变量的内容,当然也可以 `新增/删除` 内容,当需要删除内容时,设定的值为 `nil` 即可。 `gjson` 包的数据运行时修改特性非常强大,在该特性的支持下,各种数据结构的编码/解析显得异常的灵活方便。 # 通用编解码-数据格式转换 数据格式转换有很多方法,具体请查看接口文档: [https://pkg.go.dev/github.com/gogf/gf/v2/encoding/gjson](https://pkg.go.dev/github.com/gogf/gf/v2/encoding/gjson) 这里需要注意的是,有一些 `Must*` 转换方法,这些方法保证必须转换为指定的数据格式,否则直接 `panic`。 我们就来一个例子说明即可。 ``` data := `{ "users" : { "count" : 1, "array" : ["John", "Ming"] } }` if j, err := gjson.DecodeToJson(data); err != nil { panic(err) } else { fmt.Println("JSON:") fmt.Println(j.MustToJsonString()) fmt.Println("======================") fmt.Println("XML:") fmt.Println(j.MustToXmlString()) fmt.Println("======================") fmt.Println("YAML:") fmt.Println(j.MustToYamlString()) fmt.Println("======================") fmt.Println("TOML:") fmt.Println(j.MustToTomlString()) } // Output: // JSON: // {"users":{"array":["John","Ming"],"count":1}} // ====================== // XML: // JohnMing1 // ====================== // YAML: // users: // array: // - John // - Ming // count: 1 // // ====================== // TOML: // [users] // array = ["John", "Ming"] // count = 1.0 ``` `gjson` 支持将 `JSON` 转换为其他常见的数据格式,目前支持: `JSON`、 `XML`、 `INI`、 `YAML/YML`、 `TOML`、 `PROPERTIES`、 `Struct` 数据格式之间的相互转换。 # 通用编解码-方法介绍 **提示** 以下常用方法列表,文档更新可能滞后于代码新特性,更多的方法及示例请参考代码文档: [https://pkg.go.dev/github.com/gogf/gf/v2/encoding/gjson](https://pkg.go.dev/github.com/gogf/gf/v2/os/gres) ### `New` - 说明: `New` 可以用任意类型的值 `data` 创建一个 `Json` 对象,但是由于数据访问的关系, `data` 应该是一个 `map` 或者 `slice`,否则是无意义的。 - 注意: `safe` 参数决定了 `Json` 对象是否是并发安全的,默认为 `false` - 格式: ```go func New(data interface{}, safe ...bool) *Json ``` - 示例: ```go func ExampleNew() { jsonContent := `{"name":"john", "score":"100"}` j := gjson.New(jsonContent) fmt.Println(j.Get("name")) fmt.Println(j.Get("score")) // Output: // john // 100 } ``` ### `NewWithTag` - 说明: `NewWithTag` 可以用任意类型的值 `data` 创建一个 `Json` 对象,但是由于数据访问的关系, `data` 应该是一个 `map` 或者 `slice`,否则是无意义的。 - 注意: `tgts` 参数指定了结构体转换到map的标签名的优先级,多个标签用 `','` 分割。 - `safe` 参数决定了 `Json` 对象是否是并发安全的,默认为 `false` - 格式: ```go func NewWithTag(data interface{}, tags string, safe ...bool) *Json ``` - 示例: ```go func ExampleNewWithTag() { type Me struct { Name string `tag:"name"` Score int `tag:"score"` Title string } me := Me{ Name: "john", Score: 100, Title: "engineer", } j := gjson.NewWithTag(me, "tag", true) fmt.Println(j.Get("name")) fmt.Println(j.Get("score")) fmt.Println(j.Get("Title")) // Output: // john // 100 // engineer } ``` ### `NewWithOptions` - 说明: `NewWithOptions` 可以用任意类型的值 `data` 创建一个 `Json` 对象,但是由于数据访问的关系, `data` 应该是一个 `map` 或者 `slice`,否则是无意义的。 - 格式: ```go func NewWithOptions(data interface{}, options Options) *Json ``` - 示例: ```go func ExampleNewWithOptions() { type Me struct { Name string `tag:"name"` Score int `tag:"score"` Title string } me := Me{ Name: "john", Score: 100, Title: "engineer", } j := gjson.NewWithOptions(me, gjson.Options{ Tags: "tag", }) fmt.Println(j.Get("name")) fmt.Println(j.Get("score")) fmt.Println(j.Get("Title")) // Output: // john // 100 // engineer } ``` ```go func ExampleNewWithOptions_UTF8BOM() { jsonContent := `{"name":"john", "score":"100"}` content := make([]byte, 3, len(jsonContent)+3) content[0] = 0xEF content[1] = 0xBB content[2] = 0xBF content = append(content, jsonContent...) j := gjson.NewWithOptions(content, gjson.Options{ Tags: "tag", }) fmt.Println(j.Get("name")) fmt.Println(j.Get("score")) // Output: // john // 100 } ``` ### `Load` - 说明:`Load` 从指定的文件 `path` 中加载内容,并将其内容创建一个 `Json` 对象。 - 格式: ```go func Load(path string, safe ...bool) (*Json, error) ``` - 示例: ```go func ExampleLoad() { jsonFilePath := gtest.DataPath("json", "data1.json") j, _ := gjson.Load(jsonFilePath) fmt.Println(j.Get("name")) fmt.Println(j.Get("score")) notExistFilePath := gtest.DataPath("json", "data2.json") j2, _ := gjson.Load(notExistFilePath) fmt.Println(j2.Get("name")) // Output: // john // 100 } ``` ```go func ExampleLoad_Xml() { jsonFilePath := gtest.DataPath("xml", "data1.xml") j, _ := gjson.Load(jsonFilePath) fmt.Println(j.Get("doc.name")) fmt.Println(j.Get("doc.score")) } ``` ### `LoadJson` - 说明: `LoadJson` 用给定的 `JSON` 格式的内容创建一个 `Json` 对象。 - 格式: ```go func LoadJson(data interface{}, safe ...bool) (*Json, error) ``` - 示例: ```go func ExampleLoadJson() { jsonContent := `{"name":"john", "score":"100"}` j, _ := gjson.LoadJson(jsonContent) fmt.Println(j.Get("name")) fmt.Println(j.Get("score")) // Output: // john // 100 } ``` ### `LoadXml` - 说明: `LoadXml` 用给定的 `XML` 格式的内容创建一个 `Json` 对象。 - 格式: ```go func LoadXml(data interface{}, safe ...bool) (*Json, error) ``` - 示例: ```go func ExampleLoadXml() { xmlContent := ` john 100 ` j, _ := gjson.LoadXml(xmlContent) fmt.Println(j.Get("base.name")) fmt.Println(j.Get("base.score")) // Output: // john // 100 } ``` ### LoadIni - 说明: `LoadIni` 用给定的 `INI` 格式的内容创建一个 `Json` 对象。 - 格式: ```go func LoadIni(data interface{}, safe ...bool) (*Json, error) ``` - 示例: ```go func ExampleLoadIni() { iniContent := ` [base] name = john score = 100 ` j, _ := gjson.LoadIni(iniContent) fmt.Println(j.Get("base.name")) fmt.Println(j.Get("base.score")) // Output: // john // 100 } ``` ### `LoadYaml` - 说明: `LoadYaml` 用给定的 `YAML` 格式的内容创建一个 `Json` 对象。 - 格式: ```go func LoadYaml(data interface{}, safe ...bool) (*Json, error) ``` - 示例: ```go func ExampleLoadYaml() { yamlContent := `base: name: john score: 100` j, _ := gjson.LoadYaml(yamlContent) fmt.Println(j.Get("base.name")) fmt.Println(j.Get("base.score")) // Output: // john // 100 } ``` ### `LoadToml` - 说明: `LoadToml` 用给定的 `TOML` 格式的内容创建一个 `Json` 对象。 - 格式: ```go func LoadToml(data interface{}, safe ...bool) (*Json, error) ``` - 示例: ```go func ExampleLoadToml() { tomlContent := `[base] name = "john" score = 100` j, _ := gjson.LoadToml(tomlContent) fmt.Println(j.Get("base.name")) fmt.Println(j.Get("base.score")) // Output: // john // 100 } ``` ### `LoadContent` - 说明:`LoadContent` 根据给定的内容创建一个 `Json` 对象,它自动检查 `content` 的数据类型,支持的内容类型如下: `JSON, XML, INI, YAML和TOML`。 - 格式: ```go func LoadContent(data interface{}, safe ...bool) (*Json, error) ``` - 示例: ```go func ExampleLoadContent() { jsonContent := `{"name":"john", "score":"100"}` j, _ := gjson.LoadContent(jsonContent) fmt.Println(j.Get("name")) fmt.Println(j.Get("score")) // Output: // john // 100 } ``` ```go func ExampleLoadContent_UTF8BOM() { jsonContent := `{"name":"john", "score":"100"}` content := make([]byte, 3, len(jsonContent)+3) content[0] = 0xEF content[1] = 0xBB content[2] = 0xBF content = append(content, jsonContent...) j, _ := gjson.LoadContent(content) fmt.Println(j.Get("name")) fmt.Println(j.Get("score")) // Output: // john // 100 } ``` ```go func ExampleLoadContent_Xml() { xmlContent := ` john 100 ` x, _ := gjson.LoadContent(xmlContent) fmt.Println(x.Get("base.name")) fmt.Println(x.Get("base.score")) // Output: // john // 100 } ``` ### LoadContentType - 说明:`LoadContentType` 根据给定的内容和类型创建一个 `Json` 对象,支持的内容类型如下: `Json, XML, INI, YAML和TOML`。 - 格式: ```go func LoadContentType(dataType string, data interface{}, safe ...bool) (*Json, error) ``` - 示例: ```go func ExampleLoadContentType() { jsonContent := `{"name":"john", "score":"100"}` xmlContent := ` john 100 ` j, _ := gjson.LoadContentType("json", jsonContent) x, _ := gjson.LoadContentType("xml", xmlContent) j1, _ := gjson.LoadContentType("json", "") fmt.Println(j.Get("name")) fmt.Println(j.Get("score")) fmt.Println(x.Get("base.name")) fmt.Println(x.Get("base.score")) fmt.Println(j1.Get("")) // Output: // john // 100 // john // 100 } ``` ### `IsValidDataType` - 说明:`IsValidDataType` 检查给定的 `dataType` 是否是可以用于加载的有效数据内容。 - 格式: ```go func IsValidDataType(dataType string) bool ``` - 示例: ```go func ExampleIsValidDataType() { fmt.Println(gjson.IsValidDataType("json")) fmt.Println(gjson.IsValidDataType("yml")) fmt.Println(gjson.IsValidDataType("js")) fmt.Println(gjson.IsValidDataType("mp4")) fmt.Println(gjson.IsValidDataType("xsl")) fmt.Println(gjson.IsValidDataType("txt")) fmt.Println(gjson.IsValidDataType("")) fmt.Println(gjson.IsValidDataType(".json")) // Output: // true // true // true // false // false // false // false // true } ``` ### `Valid` - 说明:`Valid` 检查 `data` 是否为有效的 `JSON` 数据类型。 参数 `data` 指定 `JSON` 格式数据,可以是 `bytes` 或 `string` 类型。 - 格式: ```go func Valid(data interface{}) bool ``` - 示例: ```go func ExampleValid() { data1 := []byte(`{"n":123456789, "m":{"k":"v"}, "a":[1,2,3]}`) data2 := []byte(`{"n":123456789, "m":{"k":"v"}, "a":[1,2,3]`) fmt.Println(gjson.Valid(data1)) fmt.Println(gjson.Valid(data2)) // Output: // true // false } ``` ### Marshal - 说明:`Marshal` 是 `Encode` 的别名。 - 格式: ```go func Marshal(v interface{}) (marshaledBytes []byte, err error) ``` - 示例: ```go func ExampleMarshal() { data := map[string]interface{}{ "name": "john", "score": 100, } jsonData, _ := gjson.Marshal(data) fmt.Println(string(jsonData)) type BaseInfo struct { Name string Age int } info := BaseInfo{ Name: "Guo Qiang", Age: 18, } infoData, _ := gjson.Marshal(info) fmt.Println(string(infoData)) // Output: // {"name":"john","score":100} // {"Name":"Guo Qiang","Age":18} } ``` ### `MarshalIndent` - 说明:`MarshalIndent` 是 `json.``MarshalIndent` 的别名。 - 格式: ```go func MarshalIndent(v interface{}, prefix, indent string) (marshaledBytes []byte, err error) ``` - 示例: ```go func ExampleMarshalIndent() { type BaseInfo struct { Name string Age int } info := BaseInfo{ Name: "John", Age: 18, } infoData, _ := gjson.MarshalIndent(info, "", "\t") fmt.Println(string(infoData)) // Output: // { // "Name": "John", // "Age": 18 // } } ``` ### Unmarshal - 说明: `Unmarshal` 是 `DecodeTo` 的别名。 - 格式: ```go func Unmarshal(data []byte, v interface{}) (err error) ``` - 示例: ```go func ExampleUnmarshal() { type BaseInfo struct { Name string Score int } var info BaseInfo jsonContent := "{\"name\":\"john\",\"score\":100}" gjson.Unmarshal([]byte(jsonContent), &info) fmt.Printf("%+v", info) // Output: // {Name:john Score:100} } ``` ### Encode - 说明: `Encode` 将任意类型 `value` 序列化为内容为 `JSON` 的 `byte` 数组。 - 格式: ```go func Encode(value interface{}) ([]byte, error) ``` - 示例: ```go func ExampleEncode() { type BaseInfo struct { Name string Age int } info := BaseInfo{ Name: "John", Age: 18, } infoData, _ := gjson.Encode(info) fmt.Println(string(infoData)) // Output: // {"Name":"John","Age":18} } ``` ### `MustEncode` - 说明:`MustEncode` 执行 `Encode` 操作,但如果发生任何错误,它会 `panic`。 - 格式: ```go func MustEncode(value interface{}) []byte ``` - 示例: ```go func ExampleMustEncode() { type BaseInfo struct { Name string Age int } info := BaseInfo{ Name: "John", Age: 18, } infoData := gjson.MustEncode(info) fmt.Println(string(infoData)) // Output: // {"Name":"John","Age":18} } ``` ### `EncodeString` - 说明: `EncodeString` 将任意类型 `value` 序列化为内容为 `JSON` 的 `string`。 - 格式: ```go func EncodeString(value interface{}) (string, error) ``` - 示例: ```go func ExampleEncodeString() { type BaseInfo struct { Name string Age int } info := BaseInfo{ Name: "John", Age: 18, } infoData, _ := gjson.EncodeString(info) fmt.Println(infoData) // Output: // {"Name":"John","Age":18} } ``` ### `MustEncodeString` - 说明: `MustEncodeString` 将任意类型 `value` 序列化为内容为 `JSON` 的 `string`,但如果发生任何错误,它会 `panic`。 - 格式: ```go func MustEncodeString(value interface{}) string ``` - 示例: ```go func ExampleMustEncodeString() { type BaseInfo struct { Name string Age int } info := BaseInfo{ Name: "John", Age: 18, } infoData := gjson.MustEncodeString(info) fmt.Println(infoData) // Output: // {"Name":"John","Age":18} } ``` ### `Decode` - 说明:`Decode` 将 `JSON` 格式的内容 `data` 解码为 `interface{}`。 参数 `data` 可以是 `[]byte` 或 `string`。 - 格式: ```go func Decode(data interface{}, options ...Options) (interface{}, error) ``` - 示例: ```go func ExampleDecode() { jsonContent := `{"name":"john","score":100}` info, _ := gjson.Decode([]byte(jsonContent)) fmt.Println(info) // Output: // map[name:john score:100] } ``` ### DecodeTo - 说明:`DecodeTo` 将 `JSON` 格式的数据 `data` 解码到指定的 `interface` 类型的变量 `v` 中。参数 `data` 可以是 `[]byte` 或 `string`。参数 `v` 应该是指针类型。 - 格式: ```go func DecodeTo(data interface{}, v interface{}, options ...Options) (err error) ``` - 示例: ```go func ExampleDecodeTo() { type BaseInfo struct { Name string Score int } var info BaseInfo jsonContent := "{\"name\":\"john\",\"score\":100}" gjson.DecodeTo([]byte(jsonContent), &info) fmt.Printf("%+v", info) // Output: // {Name:john Score:100} } ``` ### `DecodeToJson` - 说明:`DecodeToJson` 将 `JSON` 格式的数据 `data` 编码为 `json` 对象。参数 `data` 可以是 `[]byte` 或 `string`。 - 格式: ```go func DecodeToJson(data interface{}, options ...Options) (*Json, error) ``` - 示例: ```go func ExampleDecodeToJson() { jsonContent := `{"name":"john","score":100}"` j, _ := gjson.DecodeToJson([]byte(jsonContent)) fmt.Println(j.Map()) // May Output: // map[name:john score:100] } ``` ### `SetSplitChar` - 说明:`SetSplitChar` 设置数据访问的层级分隔符。 - 格式: ```go func (j *Json) SetSplitChar(char byte) ``` - 示例: ```go func ExampleJson_SetSplitChar() { data := `{ "users" : { "count" : 2, "list" : [ {"name" : "Ming", "score" : 60}, {"name" : "John", "score" : 99.5} ] } }` if j, err := gjson.DecodeToJson(data); err != nil { panic(err) } else { j.SetSplitChar('#') fmt.Println("John Score:", j.Get("users#list#1#score").Float32()) } // Output: // John Score: 99.5 } ``` ### `SetViolenceCheck` - 说明: `SetViolenceCheck` 启用/禁用数据层级访问的暴力检查。 - 格式: ```go func (j *Json) SetViolenceCheck(enabled bool) ``` - 示例: ```go func ExampleJson_SetViolenceCheck() { data := `{ "users" : { "count" : 100 }, "users.count" : 101 }` if j, err := gjson.DecodeToJson(data); err != nil { fmt.Println(err) } else { j.SetViolenceCheck(true) fmt.Println("Users Count:", j.Get("users.count")) } // Output: // Users Count: 101 } ``` ### `ToJson` - 说明: `ToJson` 返回类型为 `[]byte` 的 `JSON` 内容。 - 格式: ```go func (j *Json) ToJson() ([]byte, error) ``` - 示例: ```go func ExampleJson_ToJson() { type BaseInfo struct { Name string Age int } info := BaseInfo{ Name: "John", Age: 18, } j := gjson.New(info) jsonBytes, _ := j.ToJson() fmt.Println(string(jsonBytes)) // Output: // {"Age":18,"Name":"John"} } ``` ### `ToJsonString` - 说明: `ToJsonString` 返回类型为 `string` 的 `JSON` 内容。 - 格式: ```go func (j *Json) ToJsonString() (string, error) ``` - 示例: ```go func ExampleJson_ToJsonString() { type BaseInfo struct { Name string Age int } info := BaseInfo{ Name: "John", Age: 18, } j := gjson.New(info) jsonStr, _ := j.ToJsonString() fmt.Println(jsonStr) // Output: // {"Age":18,"Name":"John"} } ``` ### ToJsonIndent - 说明: `ToJsonIndent` 返回类型为 `[]byte` 的带缩进格式的 `JSON` 内容。 - 格式: ```go func (j *Json) ToJsonIndent() ([]byte, error) ``` - 示例: ```go func ExampleJson_ToJsonIndent() { type BaseInfo struct { Name string Age int } info := BaseInfo{ Name: "John", Age: 18, } j := gjson.New(info) jsonBytes, _ := j.ToJsonIndent() fmt.Println(string(jsonBytes)) // Output: //{ // "Age": 18, // "Name": "John" //} } ``` ### `ToJsonIndentString` - 说明: `ToJsonIndentString` 返回类型为 `string` 的带缩进格式的 `JSON` 内容。 - 格式: ```go func (j *Json) ToJsonIndentString() (string, error) ``` - 示例: ```go func ExampleJson_ToJsonIndentString() { type BaseInfo struct { Name string Age int } info := BaseInfo{ Name: "John", Age: 18, } j := gjson.New(info) jsonStr, _ := j.ToJsonIndentString() fmt.Println(jsonStr) // Output: //{ // "Age": 18, // "Name": "John" //} } ``` ### MustToJson - 说明: `MustToJson` 返回类型为 `[]byte` 的 `JSON` 内容,如果发生任何错误,会发生 `panic`。 - 格式: ```go func (j *Json) MustToJson() []byte ``` - 示例: ```go func ExampleJson_MustToJson() { type BaseInfo struct { Name string Age int } info := BaseInfo{ Name: "John", Age: 18, } j := gjson.New(info) jsonBytes := j.MustToJson() fmt.Println(string(jsonBytes)) // Output: // {"Age":18,"Name":"John"} } ``` ### `MustToJsonString` - 说明: `MustToJsonString` 返回类型为 `string` 的 `JSON` 内容,如果发生任何错误,会发生 `panic`。 - 格式: ```go func (j *Json) MustToJsonString() string ``` - 示例: ```go func ExampleJson_MustToJsonString() { type BaseInfo struct { Name string Age int } info := BaseInfo{ Name: "John", Age: 18, } j := gjson.New(info) jsonStr := j.MustToJsonString() fmt.Println(jsonStr) // Output: // {"Age":18,"Name":"John"} } ``` ### MustToJsonIndent - 说明: `MustToJsonStringIndent` 返回类型为 `[]byte` 的带缩进格式的 `JSON` 内容,如果发生任何错误,会发生 `panic`。 - 格式: ```go func (j *Json) MustToJsonIndent() []byte ``` - 示例: ```go func ExampleJson_MustToJsonIndent() { type BaseInfo struct { Name string Age int } info := BaseInfo{ Name: "John", Age: 18, } j := gjson.New(info) jsonBytes := j.MustToJsonIndent() fmt.Println(string(jsonBytes)) // Output: //{ // "Age": 18, // "Name": "John" //} } ``` ### `MustToJsonIndentString` - 说明: `MustToJsonStringIndent` 返回类型为 `string` 的带缩进格式的 `JSON` 内容,如果发生任何错误,会发生 `panic`。 - 格式: ```go func (j *Json) MustToJsonIndentString() string ``` - 示例: ```go func ExampleJson_MustToJsonIndentString() { type BaseInfo struct { Name string Age int } info := BaseInfo{ Name: "John", Age: 18, } j := gjson.New(info) jsonStr := j.MustToJsonIndentString() fmt.Println(jsonStr) // Output: //{ // "Age": 18, // "Name": "John" //} } ``` ### ToXml - 说明: `ToXml` 返回类型为 `[]byte` 格式为 `XML` 的内容。 - 格式: ```go func (j *Json) ToXml(rootTag ...string) ([]byte, error) ``` - 示例: ```go func ExampleJson_ToXml() { type BaseInfo struct { Name string Age int } info := BaseInfo{ Name: "John", Age: 18, } j := gjson.New(info) xmlBytes, _ := j.ToXml() fmt.Println(string(xmlBytes)) // Output: // 18John } ``` ### `ToXmlString` - 说明: `ToXmlString` 返回类型为 `string` 格式为 `XML` 的内容。 - 格式: ```go func (j *Json) ToXmlString(rootTag ...string) (string, error) ``` - 示例: ```go func ExampleJson_ToXmlString() { type BaseInfo struct { Name string Age int } info := BaseInfo{ Name: "John", Age: 18, } j := gjson.New(info) xmlStr, _ := j.ToXmlString() fmt.Println(string(xmlStr)) // Output: // 18John } ``` ### ToXmlIndent - 说明: `ToXmlIndent` 返回类型为 `[]byte` 的带缩进格式的 `XML` 内容。 - 格式: ```go func (j *Json) ToXmlIndent(rootTag ...string) ([]byte, error) ``` - 示例: ```go func ExampleJson_ToXmlIndent() { type BaseInfo struct { Name string Age int } info := BaseInfo{ Name: "John", Age: 18, } j := gjson.New(info) xmlBytes, _ := j.ToXmlIndent() fmt.Println(string(xmlBytes)) // Output: // // 18 // John // } ``` ### `ToXmlIndentString` - 说明: `ToXmlIndentString` 返回类型为 `string` 的带缩进格式的 `XML` 内容。 - 格式: ```go func (j *Json) ToXmlIndentString(rootTag ...string) (string, error) ``` - 示例: ```go func ExampleJson_ToXmlIndentString() { type BaseInfo struct { Name string Age int } info := BaseInfo{ Name: "John", Age: 18, } j := gjson.New(info) xmlStr, _ := j.ToXmlIndentString() fmt.Println(string(xmlStr)) // Output: // // 18 // John // } ``` ### MustToXml - 说明: `MustToXml` 返回类型为 `[]byte` 格式为 `XML` 的内容。如果发生任何错误,会发生 `panic`。 - 格式: ```go func (j *Json) MustToXml(rootTag ...string) []byte ``` - 示例: ```go func ExampleJson_MustToXml() { type BaseInfo struct { Name string Age int } info := BaseInfo{ Name: "John", Age: 18, } j := gjson.New(info) xmlBytes := j.MustToXml() fmt.Println(string(xmlBytes)) // Output: // 18John } ``` ### `MustToXmlString` - 说明: `MustToXmlString` 返回类型为 `string` 格式为 `XML` 的内容。如果发生任何错误,会发生 `panic`。 - 格式: ```go func (j *Json) MustToXmlString(rootTag ...string) string ``` - 示例: ```go func ExampleJson_MustToXmlString() { type BaseInfo struct { Name string Age int } info := BaseInfo{ Name: "John", Age: 18, } j := gjson.New(info) xmlStr := j.MustToXmlString() fmt.Println(string(xmlStr)) // Output: // 18John } ``` ### MustToXmlIndent - 说明: `MustToXmlStringIndent` 返回类型为 `[]byte` 带缩进格式的 `XML` 内容。如果发生任何错误,会发生 `panic`。 - 格式: ```go func (j *Json) MustToXmlIndent(rootTag ...string) []byte ``` - 示例: ```go func ExampleJson_MustToXmlIndent() { type BaseInfo struct { Name string Age int } info := BaseInfo{ Name: "John", Age: 18, } j := gjson.New(info) xmlBytes := j.MustToXmlIndent() fmt.Println(string(xmlBytes)) // Output: // // 18 // John // } ``` ### `MustToXmlIndentString` - 说明: `MustToXmlStringIndentString` 返回类型为 `string` 带缩进格式的 `XML` 内容。如果发生任何错误,会发生 `panic`。 - 格式: ```go func (j *Json) MustToXmlIndentString(rootTag ...string) string ``` - 示例: ```go func ExampleJson_MustToXmlIndentString() { type BaseInfo struct { Name string Age int } info := BaseInfo{ Name: "John", Age: 18, } j := gjson.New(info) xmlStr := j.MustToXmlIndentString() fmt.Println(string(xmlStr)) // Output: // // 18 // John // } ``` ### ToYaml - 说明: `ToYaml` 返回类型为 `[]byte` 格式为 `YAML` 的内容。 - 格式: ```go func (j *Json) ToYaml() ([]byte, error) ``` - 示例: ```go func ExampleJson_ToYaml() { type BaseInfo struct { Name string Age int } info := BaseInfo{ Name: "John", Age: 18, } j := gjson.New(info) YamlBytes, _ := j.ToYaml() fmt.Println(string(YamlBytes)) // Output: //Age: 18 //Name: John } ``` ### ToYamlIndent - 说明: `ToYamlIndent` 返回类型为 `[]byte` 带缩进格式的 `YAML` 内容。 - 格式: ```go func (j *Json) ToYamlIndent(indent string) ([]byte, error) ``` - 示例: ```go func ExampleJson_ToYamlIndent() { type BaseInfo struct { Name string Age int } info := BaseInfo{ Name: "John", Age: 18, } j := gjson.New(info) YamlBytes, _ := j.ToYamlIndent("") fmt.Println(string(YamlBytes)) // Output: //Age: 18 //Name: John } ``` ### `ToYamlString` - 说明: `ToYamlString` 返回类型为 `string` 格式为 `YAML` 的内容。 - 格式: ```go func (j *Json) ToYamlString() (string, error) ``` - 示例: ```go func ExampleJson_ToYamlString() { type BaseInfo struct { Name string Age int } info := BaseInfo{ Name: "John", Age: 18, } j := gjson.New(info) YamlStr, _ := j.ToYamlString() fmt.Println(string(YamlStr)) // Output: //Age: 18 //Name: John } ``` ### `MustToYaml` - 说明: `MustToYaml` 返回类型为 `[]byte` 格式为 `YAML` 的内容。如果发生任何错误,会发生 `panic`。 - 格式: ```go func (j *Json) MustToYaml() []byte ``` - 示例: ```go func ExampleJson_MustToYaml() { type BaseInfo struct { Name string Age int } info := BaseInfo{ Name: "John", Age: 18, } j := gjson.New(info) YamlBytes := j.MustToYaml() fmt.Println(string(YamlBytes)) // Output: //Age: 18 //Name: John } ``` ### MustToYamlString - 说明: `MustToYamlString` 返回类型为 `string` 格式为 `YAML` 的内容。如果发生任何错误,会发生 `panic`。 - 格式: ```go func (j *Json) MustToYamlString() string ``` - 示例: ```go func ExampleJson_MustToYamlString() { type BaseInfo struct { Name string Age int } info := BaseInfo{ Name: "John", Age: 18, } j := gjson.New(info) YamlStr := j.MustToYamlString() fmt.Println(string(YamlStr)) // Output: //Age: 18 //Name: John } ``` ### `ToToml` - 说明: `ToToml` 返回类型为 `[]byte` 格式为 `TOML` 的内容。 - 格式: ```go func (j *Json) ToToml() ([]byte, error) ``` - 示例: ```go func ExampleJson_ToToml() { type BaseInfo struct { Name string Age int } info := BaseInfo{ Name: "John", Age: 18, } j := gjson.New(info) TomlBytes, _ := j.ToToml() fmt.Println(string(TomlBytes)) // Output: //Age = 18 //Name = "John" } ``` ### ToTomlString - 说明: `ToTomlString` 返回类型为 `string` 格式为 `TOML` 的内容。 - 格式: ```go func (j *Json) ToTomlString() (string, error) ``` - 示例: ```go func ExampleJson_ToTomlString() { type BaseInfo struct { Name string Age int } info := BaseInfo{ Name: "John", Age: 18, } j := gjson.New(info) TomlStr, _ := j.ToTomlString() fmt.Println(string(TomlStr)) // Output: //Age = 18 //Name = "John" } ``` ### `MustToToml` - 说明: `MustToToml` 返回类型为 `[]byte` 格式为 `TOML` 的内容。如果发生任何错误,会发生 `panic`。 - 格式: ```go func (j *Json) MustToToml() []byte ``` - 示例: ```go func ExampleJson_MustToToml() { type BaseInfo struct { Name string Age int } info := BaseInfo{ Name: "John", Age: 18, } j := gjson.New(info) TomlBytes := j.MustToToml() fmt.Println(string(TomlBytes)) // Output: //Age = 18 //Name = "John" } ``` ### MustToTomlString - 说明: `MustToTomlString` 返回类型为 `string` 格式为 `TOML` 的内容。如果发生任何错误,会发生 `panic`。 - 格式: ```go func (j *Json) MustToTomlString() string ``` - 示例: ```go func ExampleJson_MustToTomlString() { type BaseInfo struct { Name string Age int } info := BaseInfo{ Name: "John", Age: 18, } j := gjson.New(info) TomlStr := j.MustToTomlString() fmt.Println(string(TomlStr)) // Output: //Age = 18 //Name = "John" } ``` ### `ToIni` - 说明: `ToIni` 返回类型为 `[]byte` 格式为 `INI` 的内容。 - 格式: ```go func (j *Json) ToIni() ([]byte, error) ``` - 示例: ```go func ExampleJson_ToIni() { type BaseInfo struct { Name string Age int } info := BaseInfo{ Name: "John", Age: 18, } j := gjson.New(info) IniBytes, _ := j.ToIni() fmt.Println(string(IniBytes)) // May Output: //Name=John //Age=18 } ``` ### ToIniString - 说明: `ToIniString` 返回类型为 `string` 格式为 `INI` 的内容。 - 格式: ```go func (j *Json) ToIniString() (string, error) ``` - 示例: ```go func ExampleJson_ToIniString() { type BaseInfo struct { Name string } info := BaseInfo{ Name: "John", } j := gjson.New(info) IniStr, _ := j.ToIniString() fmt.Println(string(IniStr)) // Output: //Name=John } ``` ### `MustToIni` - 说明: `MustToIni` 返回类型为 `[]byte` 格式为 `INI` 的内容。如果发生任何错误,会发生 `panic`。 - 格式: ```go func (j *Json) MustToIni() []byte ``` - 示例: ```go func ExampleJson_MustToIni() { type BaseInfo struct { Name string } info := BaseInfo{ Name: "John", } j := gjson.New(info) IniBytes := j.MustToIni() fmt.Println(string(IniBytes)) // Output: //Name=John } ``` ### MustToIniString - 说明: `MustToIniString` 返回类型为 `string` 格式为 `INI` 的内容。如果发生任何错误,会发生 `panic`。 - 格式: ```go func (j *Json) MustToIniString() string ``` - 示例: ```go func ExampleJson_MustToIniString() { type BaseInfo struct { Name string } info := BaseInfo{ Name: "John", } j := gjson.New(info) IniStr := j.MustToIniString() fmt.Println(string(IniStr)) // Output: //Name=John } ``` ### MarshalJSON - 说明:`MarshalJSON` 实现了 `json.Marshal` 的接口 `MarshalJSON`。 - 格式: ```go func (j Json) MarshalJSON() ([]byte, error) ``` - 示例: ```go func ExampleJson_MarshalJSON() { type BaseInfo struct { Name string Age int } info := BaseInfo{ Name: "John", Age: 18, } j := gjson.New(info) jsonBytes, _ := j.MarshalJSON() fmt.Println(string(jsonBytes)) // Output: // {"Age":18,"Name":"John"} } ``` ### UnmarshalJSON - 说明: `UnmarshalJSON 实现了` `json.Unmarshal 的接口` `UnmarshalJSON`。 - 格式: ```go func (j *Json) UnmarshalJSON(b []byte) error ``` - 示例: ```go func ExampleJson_UnmarshalJSON() { jsonStr := `{"Age":18,"Name":"John"}` j := gjson.New("") j.UnmarshalJSON([]byte(jsonStr)) fmt.Println(j.Map()) // Output: // map[Age:18 Name:John] } ``` ### `UnmarshalValue` - 说明:`UnmarshalValue` 是一个为 `Json` 设置任何类型的值的接口实现。 - 格式: ```go func (j *Json) UnmarshalValue(value interface{}) error ``` - 示例: ```go func ExampleJson_UnmarshalValue_Yaml() { yamlContent := `base: name: john score: 100` j := gjson.New("") j.UnmarshalValue([]byte(yamlContent)) fmt.Println(j.Var().String()) // Output: // {"base":{"name":"john","score":100}} } ``` ```go func ExampleJson_UnmarshalValue_Xml() { xmlStr := `john100` j := gjson.New("") j.UnmarshalValue([]byte(xmlStr)) fmt.Println(j.Var().String()) // Output: // {"doc":{"name":"john","score":"100"}} } ``` ### `MapStrAny` - 说明: `MapStrAny` 实现了接口方法 `MapStrAny()`。 - 格式: ```go func (j *Json) MapStrAny() map[string]interface{} ``` - 示例: ```go func ExampleJson_MapStrAny() { type BaseInfo struct { Name string Age int } info := BaseInfo{ Name: "John", Age: 18, } j := gjson.New(info) fmt.Println(j.MapStrAny()) // Output: // map[Age:18 Name:John] } ``` ### Interfaces - 说明: `Interfaces` 实现了接口方法 `Interfaces()`。 - 格式: ```go func (j *Json) Interfaces() []interface{} ``` - 示例: ```go func ExampleJson_Interfaces() { type BaseInfo struct { Name string Age int } infoList := []BaseInfo{ BaseInfo{ Name: "John", Age: 18, }, BaseInfo{ Name: "Tom", Age: 20, }, } j := gjson.New(infoList) fmt.Println(j.Interfaces()) // Output: // [{John 18} {Tom 20}] } ``` ### `Interface` - 说明: `Interface` 返回 `Json` 对象的值。 - 格式: ```go func (j *Json) Interface() interface{} ``` - 示例: ```go func ExampleJson_Interface() { type BaseInfo struct { Name string Age int } info := BaseInfo{ Name: "John", Age: 18, } j := gjson.New(info) fmt.Println(j.Interface()) var nilJ *gjson.Json = nil fmt.Println(nilJ.Interface()) // Output: // map[Age:18 Name:John] // } ``` ### `Var` - 说明: `Var` 返回类型为 `*gvar.Var` 的 `Json` 对象的值。 - 格式: ```go func (j *Json) Var() *gvar.Var ``` - 示例: ```go func ExampleJson_Var() { type BaseInfo struct { Name string Age int } info := BaseInfo{ Name: "John", Age: 18, } j := gjson.New(info) fmt.Println(j.Var().String()) fmt.Println(j.Var().Map()) // Output: // {"Age":18,"Name":"John"} // map[Age:18 Name:John] } ``` ### IsNil - 说明: `IsNil` 检查Json对象值是否为 `nil`。 - 格式: ```go func (j *Json) IsNil() bool ``` - 示例: ```go func ExampleJson_IsNil() { data1 := []byte(`{"n":123456789, "m":{"k":"v"}, "a":[1,2,3]}`) data2 := []byte(`{"n":123456789, "m":{"k":"v"}, "a":[1,2,3]`) j1, _ := gjson.LoadContent(data1) fmt.Println(j1.IsNil()) j2, _ := gjson.LoadContent(data2) fmt.Println(j2.IsNil()) // Output: // false // true } ``` ### `Get` - 说明:`Get` 根据指定的 `pattern` 检索并返回值。如果 `pattern` 给的是 `"."`,将返回当前 `Json` 对象的所有值。没有 `pattern` 没有找到,则返回 `nil`。 - 格式: ```go func (j *Json) Get(pattern string, def ...interface{}) *gvar.Var ``` - 示例: ```go func ExampleJson_Get() { data := `{ "users" : { "count" : 1, "array" : ["John", "Ming"] } }` j, _ := gjson.LoadContent(data) fmt.Println(j.Get(".")) fmt.Println(j.Get("users")) fmt.Println(j.Get("users.count")) fmt.Println(j.Get("users.array")) var nilJ *gjson.Json = nil fmt.Println(nilJ.Get(".")) // Output: // {"users":{"array":["John","Ming"],"count":1}} // {"array":["John","Ming"],"count":1} // 1 // ["John","Ming"] } ``` ### `GetJson` - 说明:`GetJson` 通过指定的 `pattern` 获取值,并将其转换为一个非并发安全的 `Json` 对象。 - 格式: ```go func (j *Json) GetJson(pattern string, def ...interface{}) *Json ``` - 示例: ```go func ExampleJson_GetJson() { data := `{ "users" : { "count" : 1, "array" : ["John", "Ming"] } }` j, _ := gjson.LoadContent(data) fmt.Println(j.GetJson("users.array").Array()) // Output: // [John Ming] } ``` ### GetJsons - 说明:`GetJsons` 通过指定的 `pattern` 获取值,并将其转换为一个非并发安全的 `Json` 对象切片。 - 格式: ```go func (j *Json) GetJsons(pattern string, def ...interface{}) []*Json ``` - 示例: ```go func ExampleJson_GetJsons() { data := `{ "users" : { "count" : 3, "array" : [{"Age":18,"Name":"John"}, {"Age":20,"Name":"Tom"}] } }` j, _ := gjson.LoadContent(data) jsons := j.GetJsons("users.array") for _, json := range jsons { fmt.Println(json.Interface()) } // Output: // map[Age:18 Name:John] // map[Age:20 Name:Tom] } ``` ### `GetJsonMap` - 说明:`GetJsonMap` 通过指定的 `pattern` 获取值,并将其转换为一个非并发安全的 `Json` 对象的 `map`。 - 格式: ```go func (j *Json) GetJsonMap(pattern string, def ...interface{}) map[string]*Json ``` - 示例: ```go func ExampleJson_GetJsonMap() { data := `{ "users" : { "count" : 1, "array" : { "info" : {"Age":18,"Name":"John"}, "addr" : {"City":"Chengdu","Company":"Tencent"} } } }` j, _ := gjson.LoadContent(data) jsonMap := j.GetJsonMap("users.array") for _, json := range jsonMap { fmt.Println(json.Interface()) } // May Output: // map[City:Chengdu Company:Tencent] // map[Age:18 Name:John] } ``` ### `Set` - 说明:`Set` 设置指定 `pattern` 的值。 它默认支持通过字符 `'.'` 进行数据层级访问。 - 格式: ```go func (j *Json) Set(pattern string, value interface{}) error ``` - 示例: ```go func ExampleJson_Set() { type BaseInfo struct { Name string Age int } info := BaseInfo{ Name: "John", Age: 18, } j := gjson.New(info) j.Set("Addr", "ChengDu") j.Set("Friends.0", "Tom") fmt.Println(j.Var().String()) // Output: // {"Addr":"ChengDu","Age":18,"Friends":["Tom"],"Name":"John"} } ``` ### `MustSet` - 说明:`MustSet` 执行 `Set`,但如果有任何错误,会发生 `panic`。 - 格式: ```go func (j *Json) MustSet(pattern string, value interface{}) ``` - 示例: ```go func ExampleJson_MustSet() { type BaseInfo struct { Name string Age int } info := BaseInfo{ Name: "John", Age: 18, } j := gjson.New(info) j.MustSet("Addr", "ChengDu") fmt.Println(j.Var().String()) // Output: // {"Addr":"ChengDu","Age":18,"Name":"John"} } ``` ### `Remove` - 说明: `Remove` 删除指定 `pattern` 的值。 它默认支持通过字符 `'.'` 进行数据层级访问。 - 格式: ```go func (j *Json) Remove(pattern string) error ``` - 示例: ```go func ExampleJson_Remove() { type BaseInfo struct { Name string Age int } info := BaseInfo{ Name: "John", Age: 18, } j := gjson.New(info) j.Remove("Age") fmt.Println(j.Var().String()) // Output: // {"Name":"John"} } ``` ### MustRemove - 说明:`MustRemove` 执行`Remove`,但如果有任何错误,会发生 `panic`。 - 格式: ```go func (j *Json) MustRemove(pattern string) ``` - 示例: ```go func ExampleJson_MustRemove() { type BaseInfo struct { Name string Age int } info := BaseInfo{ Name: "John", Age: 18, } j := gjson.New(info) j.MustRemove("Age") fmt.Println(j.Var().String()) // Output: // {"Name":"John"} } ``` ### `Contains` - 说明: `Contains` 检查指定 `pattern` 的值是否存在。 - 格式: ```go func (j *Json) Contains(pattern string) bool ``` - 示例: ```go func ExampleJson_Contains() { type BaseInfo struct { Name string Age int } info := BaseInfo{ Name: "John", Age: 18, } j := gjson.New(info) fmt.Println(j.Contains("Age")) fmt.Println(j.Contains("Addr")) // Output: // true // false } ``` ### Len - 说明:`Len` 根据指定的 `pattern` 返回 值的长度/大小。 `pattern` 的值应该是 `slice` 或 `map` 的类型。 如果找不到目标值或类型无效,则返回 `-1`。 - 格式: ```go func (j *Json) Len(pattern string) int ``` - 示例: ```go func ExampleJson_Len() { data := `{ "users" : { "count" : 1, "nameArray" : ["Join", "Tom"], "infoMap" : { "name" : "Join", "age" : 18, "addr" : "ChengDu" } } }` j, _ := gjson.LoadContent(data) fmt.Println(j.Len("users.nameArray")) fmt.Println(j.Len("users.infoMap")) // Output: // 2 // 3 } ``` ### `Append` - 说明:`Append` 通过指定的 `pattern` 将值追加到 `Json` 对象中。 `pattern` 的值的类型应该是 `slice`。 - 格式: ```go func (j *Json) Append(pattern string, value interface{}) error ``` - 示例: ```go func ExampleJson_Append() { data := `{ "users" : { "count" : 1, "array" : ["John", "Ming"] } }` j, _ := gjson.LoadContent(data) j.Append("users.array", "Lily") fmt.Println(j.Get("users.array").Array()) // Output: // [John Ming Lily] } ``` ### MustAppend - 说明:`MustAppend` 执行 `Append`,但如果有任何错误,会发生 `panic`。 - 格式: ```go func (j *Json) MustAppend(pattern string, value interface{}) ``` - 示例: ```go func ExampleJson_MustAppend() { data := `{ "users" : { "count" : 1, "array" : ["John", "Ming"] } }` j, _ := gjson.LoadContent(data) j.MustAppend("users.array", "Lily") fmt.Println(j.Get("users.array").Array()) // Output: // [John Ming Lily] } ``` ### Map - 说明: `Map` 将当前 `Json` 对象转换为 `map[string]interface{}`。 如果失败,则返回 `nil`。 - 格式: ```go func (j *Json) Map() map[string]interface{} ``` - 示例: ```go func ExampleJson_Map() { data := `{ "users" : { "count" : 1, "info" : { "name" : "John", "age" : 18, "addr" : "ChengDu" } } }` j, _ := gjson.LoadContent(data) fmt.Println(j.Get("users.info").Map()) // Output: // map[addr:ChengDu age:18 name:John] } ``` ### Array - 说明:`Array` 将当前 `Json` 对象转换为 `[]interface{}`。 如果失败,则返回 `nil`。 - 格式: ```go func (j *Json) Array() []interface{} ``` - 示例: ```go func ExampleJson_Array() { data := `{ "users" : { "count" : 1, "array" : ["John", "Ming"] } }` j, _ := gjson.LoadContent(data) fmt.Println(j.Get("users.array")) // Output: // ["John","Ming"] } ``` ### `Scan` - 说明: `Scan` 自动调用 `Struct` 或 `Structs` 函数根据参数 `pointer` 的类型来进行转换。 - 格式: ```go func (j *Json) Scan(pointer interface{}, mapping ...map[string]string) error ``` - 示例: ```go func ExampleJson_Scan() { data := `{"name":"john","age":"18"}` type BaseInfo struct { Name string Age int } info := BaseInfo{} j, _ := gjson.LoadContent(data) j.Scan(&info) fmt.Println(info) // May Output: // {john 18} } ``` ### Dump - 说明: `Dump` 以可读性更高的方式打印 `Json` 对象。 - 格式: ```go func (j *Json) Dump() ``` - 示例: ```go func ExampleJson_Dump() { data := `{"name":"john","age":"18"}` j, _ := gjson.LoadContent(data) j.Dump() // May Output: //{ // "name": "john", // "age": "18", //} } ``` # 通用编解码-FAQ ## JSON中的大数字精度丢失问题 ### 问题描述 ```go package main import ( "github.com/gogf/gf/v2/encoding/gjson" "github.com/gogf/gf/v2/frame/g" ) func main() { str := `{"Id":1492404095703580672,"Name":"Jason"}` strJson := gjson.New(str) g.Dump(strJson) } ``` 执行后输出为: ``` "{\"Id\":1492404095703580700,\"Name\":\"Jason\"}" ``` ### 解决方案 ```go package main import ( "github.com/gogf/gf/v2/encoding/gjson" "github.com/gogf/gf/v2/frame/g" ) func main() { str := `{"Id":1492404095703580672,"Name":"Jason"}` strJson := gjson.NewWithOptions(str, gjson.Options{ StrNumber: true, }) g.Dump(strJson) } ``` 执行后输出为: ``` "{\"Id\":1492404095703580672,\"Name\":\"Jason\"}" ``` ### 相关连接 - [https://github.com/gogf/gf/issues/1603](https://github.com/gogf/gf/issues/1603) # 数据库ORM-gdb 数据库 `ORM` 功能由 `gdb` 模块实现,具体请参考 [数据库ORM](../core/gdb.md) 章节。 # Redis客户端-gredis `Redis` 的功能由 `gredis` 模块实现,具体请参考 [NoSQL Redis](./contrib-nosql-redis.md) 章节。 # TCP组件 ## 基本介绍 `gtcp` 模块实现简便易用、轻量级的 `TCPServer` 服务端。 **使用方式**: ```go import "github.com/gogf/gf/v2/net/gtcp" ``` **接口文档**: [https://pkg.go.dev/github.com/gogf/gf/v2/net/gtcp](https://pkg.go.dev/github.com/gogf/gf/v2/net/gtcp) ```go type Server func GetServer(name ...interface{}) *Server func NewServer(address string, handler func(*Conn), name ...string) *Server func NewServerKeyCrt(address, crtFile, keyFile string, handler func(*Conn), name ...string) *Server func NewServerTLS(address string, tlsConfig *tls.Config, handler func(*Conn), name ...string) *Server func (s *Server) Close() error func (s *Server) Run() (err error) func (s *Server) SetAddress(address string) func (s *Server) SetHandler(handler func(*Conn)) func (s *Server) SetTLSConfig(tlsConfig *tls.Config) func (s *Server) SetTLSKeyCrt(crtFile, keyFile string) error ``` 其中, `GetServer` 使用单例模式通过给定一个唯一的名称获取/创建一个单例 `Server`,后续可通过 `SetAddress` 和 `SetHandler` 方法动态修改Server属性; `NewServer` 则直接根据给定参数创建一个Server对象,并可指定名称。 我们通过实现一个简单的 `echo服务器` 来演示 `TCPServer` 的使用: ```go package main import ( "fmt" "github.com/gogf/gf/v2/net/gtcp" ) func main() { gtcp.NewServer("127.0.0.1:8999", func(conn *gtcp.Conn) { defer conn.Close() for { data, err := conn.Recv(-1) if len(data) > 0 { if err := conn.Send(append([]byte("> "), data...)); err != nil { fmt.Println(err) } } if err != nil { break } } }).Run() } ``` 在这个示例中我们使用了 `Send` 和 `Recv` 来发送和接收数据。其中 `Recv` 方法会通过阻塞方式接收数据,直到客户端」发送完毕一条数据」(执行一次 `Send`,底层Socket通信不带缓冲实现),或者关闭链接。关于其中的链接对象 `gtcp.Conn` 的介绍,请继续阅读后续章节。 执行之后我们使用 `telnet` 工具来进行测试: ```bash john@home:~$ telnet 127.0.0.1 8999 Trying 127.0.0.1... Connected to 127.0.0.1. Escape character is '^]'. hello > hello hi there > hi there ``` 每一个客户端发起的TCP链接,TCPServer都会创建一个 `goroutine` 进行处理,直至TCP链接断开。由于goroutine比较轻量级,因此可以支撑很高的并发量。 # TCP组件-工具方法 `gtcp` 模块也提供了一些常用的工具方法。 **使用方式**: ```go import "github.com/gogf/gf/v2/net/gtcp" ``` **接口文档**: [https://pkg.go.dev/github.com/gogf/gf/v2/net/gtcp](https://pkg.go.dev/github.com/gogf/gf/v2/net/gtcp) ```go func LoadKeyCrt(crtFile, keyFile string) (*tls.Config, error) func NewNetConn(addr string, timeout ...int) (net.Conn, error) func NewNetConnKeyCrt(addr, crtFile, keyFile string) (net.Conn, error) func NewNetConnTLS(addr string, tlsConfig *tls.Config) (net.Conn, error) func Send(addr string, data []byte, retry ...Retry) error func SendPkg(addr string, data []byte, option ...PkgOption) error func SendPkgWithTimeout(addr string, data []byte, timeout time.Duration, option ...PkgOption) error func SendRecv(addr string, data []byte, receive int, retry ...Retry) ([]byte, error) func SendRecvPkg(addr string, data []byte, option ...PkgOption) ([]byte, error) func SendRecvPkgWithTimeout(addr string, data []byte, timeout time.Duration, option ...PkgOption) ([]byte, error) func SendRecvWithTimeout(addr string, data []byte, receive int, timeout time.Duration, retry ...Retry) ([]byte, error) func SendWithTimeout(addr string, data []byte, timeout time.Duration, retry ...Retry) error ``` 1. `NewNetConn` 用于简化标准库连接对象 `net.Conn` 的创建; 2. `NewNetConnTLS` 和 `NewNetConnKeyCrt` 用于创建支持TLS安全加密通信的TCP客户端; 3. `Send*` 系列方法直接通过给定地址进行数据发送,并获取该请求的返回结果,用于短链接请求的情况; 以下为一个简单的示例,我们使用工具方法来访问指定的Web站点: ```go package main import ( "fmt" "github.com/gogf/gf/v2/net/gtcp" ) func main() { data, err := gtcp.SendRecv("www.baidu.com:80", []byte("HEAD / HTTP/1.1\n\n"), -1) if err != nil { panic(err) } fmt.Println(string(data)) } ``` 在这个示例中,我们通过TCP访问百度首页,模拟HTTP请求头信息,并获得返回结果。 执行后,输出结果如下: ``` HTTP/1.1 302 Found Connection: Keep-Alive Content-Length: 17931 Content-Type: text/html Date: Tue, 04 Jun 2019 15:53:09 GMT Etag: "54d9749e-460b" Server: bfe/1.0.8.18 ``` # TCP组件-TLS加密 `gtcp` 模块支持 `TLS` 加密通信服务端及客户端,在对安全要求比较高的场景中非常必要。 `TLS` 服务端创建可以通过 `NewServerTLS` 或者 `NewServerKeyCrt` 方法实现。 `TLS` 客户端创建可以通过 `NewConnKeyCrt` 或者 `NewConnTLS` 方法实现。 使用示例: [https://github.com/gogf/gf/tree/master/.example/net/gtcp/tls](https://github.com/gogf/gf/tree/master/.example/net/gtcp/tls) ```go package main import ( "fmt" "github.com/gogf/gf/v2/frame/g" "github.com/gogf/gf/v2/net/gtcp" "github.com/gogf/gf/v2/util/gconv" "time" ) func main() { address := "127.0.0.1:8999" crtFile := "server.crt" keyFile := "server.key" // TLS Server go gtcp.NewServerKeyCrt(address, crtFile, keyFile, func(conn *gtcp.Conn) { defer conn.Close() for { data, err := conn.Recv(-1) if len(data) > 0 { fmt.Println(string(data)) } if err != nil { // if client closes, err will be: EOF g.Log().Error(err) break } } }).Run() time.Sleep(time.Second) // Client conn, err := gtcp.NewConnKeyCrt(address, crtFile, keyFile) if err != nil { panic(err) } defer conn.Close() for i := 0; i < 10; i++ { if err := conn.Send([]byte(gconv.String(i))); err != nil { g.Log().Error(err) } time.Sleep(time.Second) if i == 5 { conn.Close() break } } // exit after 5 seconds time.Sleep(5 * time.Second) } ``` 执行后,可以看到客户端执行时报错: ``` panic: x509: certificate has expired or is not yet valid ``` 那是因为我们的证书是手动创建的,并且已经过期了,为了演示方便,我们在客户端代码中去掉客户端对证书的校验。 ```go package main import ( "fmt" "github.com/gogf/gf/v2/net/gtcp" "github.com/gogf/gf/v2/util/gconv" "time" ) func main() { address := "127.0.0.1:8999" crtFile := "server.crt" keyFile := "server.key" // TLS Server go gtcp.NewServerKeyCrt(address, crtFile, keyFile, func(conn *gtcp.Conn) { defer conn.Close() for { data, err := conn.Recv(-1) if len(data) > 0 { fmt.Println(string(data)) } if err != nil { // if client closes, err will be: EOF g.Log().Error(err) break } } }).Run() time.Sleep(time.Second) // Client tlsConfig, err := gtcp.LoadKeyCrt(crtFile, keyFile) if err != nil { panic(err) } tlsConfig.InsecureSkipVerify = true conn, err := gtcp.NewConnTLS(address, tlsConfig) if err != nil { panic(err) } defer conn.Close() for i := 0; i < 10; i++ { if err := conn.Send([]byte(gconv.String(i))); err != nil { g.Log().Error(err) } time.Sleep(time.Second) if i == 5 { conn.Close() break } } // exit after 5 seconds time.Sleep(5 * time.Second) } ``` 执行后,终端输出结果为: ```0 1 2 3 4 5 2019-06-05 00:13:12.488 [ERRO] EOF Stack: 1. /Users/john/Workspace/Go/GOPATH/src/github.com/gogf/gf/v2/geg/net/gtcp/tls/gtcp_server_client.go:25 ``` 其中客户端在 `5` 秒后关闭了连接,因此服务端在接收数据时获取到了一个 `EOF` 错误,这种错误在正式使用中我们直接忽略,报错时服务端直接关闭客户端连接即可。 # TCP组件-连接池特性 `gtcp` 模块提供了连接池的特性,由 `gtcp.PoolConn` 对象实现,连接池缓存固定存活时间为600秒,且内部实现了数据发送时的断开重连机制。连接池非常适合于频繁的短链接操作且连接并发量大的场景。我们接下来使用两个示例来演示一下连接池的作用。 **使用方式**: ```go import "github.com/gogf/gf/v2/net/gtcp" ``` **接口文档**: [https://pkg.go.dev/github.com/gogf/gf/v2/net/gtcp](https://pkg.go.dev/github.com/gogf/gf/v2/net/gtcp) ```go type PoolConn func NewPoolConn(addr string, timeout ...int) (*PoolConn, error) func (c *PoolConn) Close() error func (c *PoolConn) Recv(length int, retry ...Retry) ([]byte, error) func (c *PoolConn) RecvLine(retry ...Retry) ([]byte, error) func (c *PoolConn) RecvPkg(option ...PkgOption) ([]byte, error) func (c *PoolConn) RecvPkgWithTimeout(timeout time.Duration, option ...PkgOption) ([]byte, error) func (c *PoolConn) RecvWithTimeout(length int, timeout time.Duration, retry ...Retry) (data []byte, err error) func (c *PoolConn) Send(data []byte, retry ...Retry) error func (c *PoolConn) SendPkg(data []byte, option ...PkgOption) (err error) func (c *PoolConn) SendPkgWithTimeout(data []byte, timeout time.Duration, option ...PkgOption) error func (c *PoolConn) SendRecv(data []byte, receive int, retry ...Retry) ([]byte, error) func (c *PoolConn) SendRecvPkg(data []byte, option ...PkgOption) ([]byte, error) func (c *PoolConn) SendRecvPkgWithTimeout(data []byte, timeout time.Duration, option ...PkgOption) ([]byte, error) func (c *PoolConn) SendRecvWithTimeout(data []byte, receive int, timeout time.Duration, retry ...Retry) ([]byte, error) func (c *PoolConn) SendWithTimeout(data []byte, timeout time.Duration, retry ...Retry) error ``` 由于 `gtcp.PoolConn` 继承于 `gtcp.Conn` 因此同时也可以使用 `gtcp.Conn` 的方法。 ## 示例1,基本使用 ```go package main import ( "fmt" "time" "github.com/gogf/gf/v2/net/gtcp" "github.com/gogf/gf/v2/os/glog" "github.com/gogf/gf/v2/os/gtime" ) func main() { // Server go gtcp.NewServer("127.0.0.1:8999", func(conn *gtcp.Conn) { defer conn.Close() for { data, err := conn.Recv(-1) if len(data) > 0 { if err := conn.Send(append([]byte("> "), data...)); err != nil { fmt.Println(err) } } if err != nil { break } } }).Run() time.Sleep(time.Second) // Client for { if conn, err := gtcp.NewPoolConn("127.0.0.1:8999"); err == nil { if b, err := conn.SendRecv([]byte(gtime.Datetime()), -1); err == nil { fmt.Println(string(b), conn.LocalAddr(), conn.RemoteAddr()) } else { fmt.Println(err) } conn.Close() } else { glog.Error(err) } time.Sleep(time.Second) } } ``` 在这个示例中,Server创建新的goroutine异步运行,Client在main goroutine中执行。Server端是一个回显服务器,Client每隔1秒向Server端发送当前的时间,经过Server端回显返回后,在Client端打印出双方的连接端口信息。 执行后,结果如下: ``` > 2018-07-11 23:29:54 127.0.0.1:55876 127.0.0.1:8999 > 2018-07-11 23:29:55 127.0.0.1:55876 127.0.0.1:8999 > 2018-07-11 23:29:56 127.0.0.1:55876 127.0.0.1:8999 > 2018-07-11 23:29:57 127.0.0.1:55876 127.0.0.1:8999 > 2018-07-11 23:29:58 127.0.0.1:55876 127.0.0.1:8999 ... ``` 可以看到,Client的端口一直未变,每一次通过 `gtcp.NewConn("127.0.0.1:8999")` 获得的都是同一个 `gtcp.Conn` 对象,且每一次 `conn.Close()` 时并不是真正的关闭连接,而是将该对象重新丢回到连接池里循环使用。 ## 示例2,连接断开情况 这个例子是为了展示当服务端关闭连接后,该连接对象还是否有效的处理。 ```go package main import ( "fmt" "time" "github.com/gogf/gf/v2/net/gtcp" "github.com/gogf/gf/v2/os/glog" "github.com/gogf/gf/v2/os/gtime" ) func main() { // Server go gtcp.NewServer("127.0.0.1:8999", func(conn *gtcp.Conn) { defer conn.Close() for { data, err := conn.Recv(-1) if len(data) > 0 { if err := conn.Send(append([]byte("> "), data...)); err != nil { fmt.Println(err) } } if err != nil { break } return } }).Run() time.Sleep(time.Second) // Client for { if conn, err := gtcp.NewPoolConn("127.0.0.1:8999"); err == nil { if b, err := conn.SendRecv([]byte(gtime.Datetime()), -1); err == nil { fmt.Println(string(b), conn.LocalAddr(), conn.RemoteAddr()) } else { fmt.Println(err) } conn.Close() } else { glog.Error(err) } time.Sleep(time.Second) } } ``` 执行后,输出结果如下: ``` > 2018-07-20 12:56:15 127.0.0.1:59368 127.0.0.1:8999 EOF > 2018-07-20 12:56:17 127.0.0.1:59376 127.0.0.1:8999 EOF > 2018-07-20 12:56:19 127.0.0.1:59378 127.0.0.1:8999 EOF ... ``` 在这个示例中,Server每处理完毕一条请求之后便关闭链接。Client在第一条请求发送完毕后,由于连接池的IO复用特性,下一次获取到的将是同一个连接对象,由于Server链接已主动关闭,第二次请求写入成功(其实并未成功发送到Server端,需要通过下一次的读取操作才能检测到链接错误),但是读取却失败了( `EOF` 表示目标连接关闭),因此这个时候Client执行 `Close` 时将会销毁该连接操作对象,而不是进一步复用。下一次再通过 `gtcp.NewPoolConn` 获得连接对象时,Client将会与Server创建一个新的连接进行数据通信。所以看到Client的端口一直在变化,那是因为该 `gtcp.Conn` 对象已经是一个新的连接对象,之前的连接对象已经被销毁。 连接对象的IO复用涉及到十分微妙的连接状态变化问题,由于点对点网络通信本身是比较复杂的环境,连接对象的状态随时可能被动发生着变化,因此,在使用gtcp连接池特性时,需要注意当通信错误产生时的连接对象重建机制,一旦产生错误,立即丢弃( `Close`)该对象( `gtcp.PoolConn`)并重建( `gtcp.NewPoolConn`)。 # TCP组件-连接对象 `gtcp` 模块提供了简便易用的 `gtcp.Conn` 链接操作对象。 **使用方式**: ```go import "github.com/gogf/gf/v2/net/gtcp" ``` **接口文档**: [https://pkg.go.dev/github.com/gogf/gf/v2/net/gtcp](https://pkg.go.dev/github.com/gogf/gf/v2/net/gtcp) ```go type Conn func NewConn(addr string, timeout ...int) (*Conn, error) func NewConnByNetConn(conn net.Conn) *Conn func NewConnKeyCrt(addr, crtFile, keyFile string) (*Conn, error) func NewConnTLS(addr string, tlsConfig *tls.Config) (*Conn, error) func (c *Conn) Close() error func (c *Conn) LocalAddr() net.Addr func (c *Conn) Recv(length int, retry ...Retry) ([]byte, error) func (c *Conn) RecvLine(retry ...Retry) ([]byte, error) func (c *Conn) RecvWithTimeout(length int, timeout time.Duration, retry ...Retry) ([]byte, error) func (c *Conn) RemoteAddr() net.Addr func (c *Conn) Send(data []byte, retry ...Retry) error func (c *Conn) SendRecv(data []byte, receive int, retry ...Retry) ([]byte, error) func (c *Conn) SendRecvWithTimeout(data []byte, receive int, timeout time.Duration, retry ...Retry) ([]byte, error) func (c *Conn) SendWithTimeout(data []byte, timeout time.Duration, retry ...Retry) error func (c *Conn) SetDeadline(t time.Time) error func (c *Conn) SetRecvBufferWait(bufferWaitDuration time.Duration) func (c *Conn) SetRecvDeadline(t time.Time) error func (c *Conn) SetSendDeadline(t time.Time) error ``` ## 写入操作 TCP通信写入操作由 `Send` 方法实现,并提供了错误重试的机制,由第二个非必需参数 `retry` 提供。需要注意的是 `Send` 方法不带任何的缓冲机制,也就是说每调用一次 `Send` 方法将会立即调用底层的TCP Write方法写入数据(缓冲机制依靠系统底层实现)。因此,如果想要进行输出缓冲控制,请在业务层进行处理。 在进行TCP写入时,可靠的通信场景下往往是一写一读,即 `Send` 成功之后接着便开始 `Recv` 获取目标的反馈结果。因此 `gtcp.Conn` 也提供了方便的 `SendRecv` 方法。 ## 读取操作 TCP通信读取操作由 `Recv` 方法实现,同时也提供了错误重试的机制,由第二个非必需参数 `retry` 提供。 `Recv` 方法提供了内置的读取缓冲控制,读取数据时可以指定读取的长度(由 `length` 参数指定),当读取到指定长度的数据后将会立即返回。如果 `length < 0` 那么将会读取所有可读取的缓冲区数据并返回。当 `length = 0` 时表示获取一次缓冲区的数据后立即返回。 如果使用 `Recv(-1)` 可以读取所有缓冲区可读数据(长度不定,如果发送的数据包太长有可能会被截断),但需要注意包的解析问题,容易产生非完整包的情况。这个时候,业务层需要根据既定的数据包结构自己负责包的完整性处理。推荐使用后续介绍的 `简单协议` 通过 `SendPkg`/ `RecvPkg` 来实现消息包的发送/接收,具体请查看后续章节。 ## 超时处理 `gtcp.Conn` 对TCP通信时的数据写入和读取提供了超时处理,通过方法中的 `timeout` 参数指定,这块比较简单,不过多阐述。 * * * 我们接下来通过通过几个例子来看看如何使用 `gtcp.Conn` 对象。 ## 使用示例 ### 示例1,简单使用 ```go package main import ( "fmt" "time" "github.com/gogf/gf/v2/net/gtcp" "github.com/gogf/gf/v2/os/glog" "github.com/gogf/gf/v2/util/gconv" ) func main() { // Server go gtcp.NewServer("127.0.0.1:8999", func(conn *gtcp.Conn) { defer conn.Close() for { data, err := conn.Recv(-1) if len(data) > 0 { fmt.Println(string(data)) } if err != nil { break } } }).Run() time.Sleep(time.Second) // Client conn, err := gtcp.NewConn("127.0.0.1:8999") if err != nil { panic(err) } for i := 0; i < 10000; i++ { if err := conn.Send([]byte(gconv.String(i))); err != nil { glog.Error(err) } time.Sleep(time.Second) } } ``` 1. `Server` 端,接收到客户端的数据后立即打印到终端上。 2. `Client` 端,使用同一个连接对象,在循环中每隔1秒向服务端发送当前递增的数字。同时,该功能也可以演示出底层 `Socket` 通信并没有使用缓冲实现,也就是说,执行一次 `Send` 即立刻向服务端发送数据。因此,客户端需要在本地自行管理好需要发送的缓冲数据。 3. 执行结果 执行后,可以看到Server在终端上输出以下信息: ```shell 2018-07-11 22:11:08.650 0 2018-07-11 22:11:09.651 1 2018-07-11 22:11:10.651 2 2018-07-11 22:11:11.651 3 2018-07-11 22:11:12.651 4 2018-07-11 22:11:13.651 5 2018-07-11 22:11:14.652 6 2018-07-11 22:11:15.652 7 2018-07-11 22:11:16.652 8 2018-07-11 22:11:17.652 9 2018-07-11 22:11:18.652 10 2018-07-11 22:11:19.653 11 ... ``` ### 示例2,回显服务 我们将之前的回显服务改进一下: ```go package main import ( "fmt" "time" "github.com/gogf/gf/v2/net/gtcp" "github.com/gogf/gf/v2/os/glog" "github.com/gogf/gf/v2/os/gtime" ) func main() { // Server go gtcp.NewServer("127.0.0.1:8999", func(conn *gtcp.Conn) { defer conn.Close() for { data, err := conn.Recv(-1) if len(data) > 0 { if err := conn.Send(append([]byte("> "), data...)); err != nil { fmt.Println(err) } } if err != nil { break } } }).Run() time.Sleep(time.Second) // Client for { if conn, err := gtcp.NewConn("127.0.0.1:8999"); err == nil { if b, err := conn.SendRecv([]byte(gtime.Datetime()), -1); err == nil { fmt.Println(string(b), conn.LocalAddr(), conn.RemoteAddr()) } else { fmt.Println(err) } conn.Close() } else { glog.Error(err) } time.Sleep(time.Second) } } ``` 该示例程序中, `Client` 每隔1秒钟向 `Server` 发送当前的时间信息, `Server` 接收到之后返回原数据信息, `Client` 接收到 `Server` 端返回信息后立即打印到终端。 执行后,输出结果为: ``` > 2018-07-19 23:25:43 127.0.0.1:34306 127.0.0.1:8999 > 2018-07-19 23:25:44 127.0.0.1:34308 127.0.0.1:8999 > 2018-07-19 23:25:45 127.0.0.1:34312 127.0.0.1:8999 > 2018-07-19 23:25:46 127.0.0.1:34314 127.0.0.1:8999 ``` ### 示例3,HTTP客户端 我们在这个示例中使用gtcp包来实现一个简单的HTTP客户端,读取并打印出百度首页的 `header` 和 `content` 内容。 ```go package main import ( "fmt" "bytes" "github.com/gogf/gf/v2/net/gtcp" "github.com/gogf/gf/v2/util/gconv" ) func main() { conn, err := gtcp.NewConn("www.baidu.com:80") if err != nil { panic(err) } defer conn.Close() if err := conn.Send([]byte("GET / HTTP/1.1\r\n\r\n")); err != nil { panic(err) } header := make([]byte, 0) content := make([]byte, 0) contentLength := 0 for { data, err := conn.RecvLine() // header读取,解析文本长度 if len(data) > 0 { array := bytes.Split(data, []byte(": ")) // 获得页面内容长度 if contentLength == 0 && len(array) == 2 && bytes.EqualFold([]byte("Content-Length"), array[0]) { contentLength = gconv.Int(string(array[1][:len(array[1])-1])) } header = append(header, data...) header = append(header, '\n') } // header读取完毕,读取文本内容 if contentLength > 0 && len(data) == 1 { content, _ = conn.Recv(contentLength) break } if err != nil { fmt.Errorf("ERROR: %s\n", err.Error()) break } } if len(header) > 0 { fmt.Println(string(header)) } if len(content) > 0 { fmt.Println(string(content)) } } ``` 执行后,输出结果为: ``` HTTP/1.1 200 OK Accept-Ranges: bytes Cache-Control: no-cache Connection: Keep-Alive Content-Length: 14615 Content-Type: text/html Date: Sat, 21 Jul 2018 04:21:03 GMT Etag: "5b3c3650-3917" Last-Modified: Wed, 04 Jul 2018 02:52:00 GMT P3p: CP=" OTI DSP COR IVA OUR IND COM " Pragma: no-cache Server: BWS/1.1 ... (略) ``` # 连接对象-消息包处理 `gtcp` 提供了许多方便的原生操作连接数据的方法,但是在绝大多数的应用场景中,开发者需要自己设计数据结构,并进行封包/解包处理,由于 `TCP` 消息协议是没有消息边界保护的,因此复杂的网络通信环境中很容易出现 **粘包** 的情况。因此 `gtcp` 也提供了简单的数据协议,方便开发者进行消息包交互,开发者不再需要担心消息包的处理细节,包括封包/解包处理,这一切复杂的逻辑 `gtcp` 已经帮处理好了。 ## 简单协议 `gtcp` 模块提供了简单轻量级数据交互协议,效率非常高,协议格式如下: ``` 数据长度(16bit)|数据字段(变长) ``` 1. 数据长度:默认为 `16位`( `2字节`),用于标识该消息体的数据长度,单位为字节,不包含自身的2字节; 2. 数据字段:变长,根据数据长度可以知道,数据最大长度不能超过 `0xFFFF = 65535 bytes = 64 KB`; 简单协议由 `gtcp` 封装实现,如果开发者客户端和服务端如果都使用 `gtcp` 模块来通信则无需关心协议实现,专注 `数据` 字段封装/解析实现即可。如果涉及和其他开发语言对接,则需要按照该协议实现对接即可,由于简单协议非常简单轻量级,因此对接成本很低。 **提示** 数据字段也可以为空,即没有任何长度。 ## 操作方法 [https://pkg.go.dev/github.com/gogf/gf/v2/net/gtcp](https://pkg.go.dev/github.com/gogf/gf/v2/net/gtcp) ```go type Conn func (c *Conn) SendPkg(data []byte, option ...PkgOption) error func (c *Conn) SendPkgWithTimeout(data []byte, timeout time.Duration, option ...PkgOption) error func (c *Conn) SendRecvPkg(data []byte, option ...PkgOption) ([]byte, error) func (c *Conn) SendRecvPkgWithTimeout(data []byte, timeout time.Duration, option ...PkgOption) ([]byte, error) func (c *Conn) RecvPkg(option ...PkgOption) (result []byte, err error) func (c *Conn) RecvPkgWithTimeout(timeout time.Duration, option ...PkgOption) ([]byte, error) ``` 可以看到,消息包方法命名是在原有的基本连接操作方法中加上了 `Pkg` 关键词便于区分。 其中,请求参数中的 `PkgOption` 数据结构如下,用于定义消息包接收策略: ```go // 数据读取选项 type PkgOption struct { HeaderSize int // 自定义头大小(默认为2字节,最大不能超过4字节) MaxDataSize int // (byte)数据读取的最大包大小,默认最大不能超过2字节(65535 byte) Retry Retry // 失败重试策略 } ``` ## 使用示例 ### 示例1,基本使用 ```go package main import ( "fmt" "github.com/gogf/gf/v2/net/gtcp" "github.com/gogf/gf/v2/os/glog" "github.com/gogf/gf/v2/util/gconv" "time" ) func main() { // Server go gtcp.NewServer("127.0.0.1:8999", func(conn *gtcp.Conn) { defer conn.Close() for { data, err := conn.RecvPkg() if err != nil { fmt.Println(err) break } fmt.Println("receive:", data) } }).Run() time.Sleep(time.Second) // Client conn, err := gtcp.NewConn("127.0.0.1:8999") if err != nil { panic(err) } defer conn.Close() for i := 0; i < 10000; i++ { if err := conn.SendPkg([]byte(gconv.String(i))); err != nil { glog.Error(err) } time.Sleep(1*time.Second) } } ``` 这个示例比较简单,执行后,输出结果为: ``` receive: [48] receive: [49] receive: [50] receive: [51] ... ``` ### 示例2,自定义数据结构 大多数场景下,我们需要对发送的消息能自定义数据结构,开发者可以利用 `数据` 字段传递任意的消息内容实现。 以下是一个简单的自定义数据结构的示例,用于客户端上报当前主机节点的内存及CPU使用情况。在该示例中, `数据` 字段使用了 `JSON` 数据格式进行自定义,便于数据的编码/解码。 **提示** 实际场景中,开发者往往需要自定义包解析协议,或者采用较通用的 `protobuf` 二进制包封装/解析协议。 1. `types/types.go` 数据结构定义。 ```go package types import "github.com/gogf/gf/v2/frame/g" type NodeInfo struct { Cpu float32 // CPU百分比(%) Host string // 主机名称 Ip g.Map // IP地址信息(可能多个) MemUsed int // 内存使用(byte) MemTotal int // 内存总量(byte) Time int // 上报时间(时间戳) } ``` 2. `gtcp_monitor_server.go` 服务端。 ```go package main import ( "encoding/json" "github.com/gogf/gf/v2/net/gtcp" "github.com/gogf/gf/v2/os/glog" "github.com/gogf/gf/.example/net/gtcp/pkg_operations/monitor/types" ) func main() { // 服务端,接收客户端数据并格式化为指定数据结构,打印 gtcp.NewServer("127.0.0.1:8999", func(conn *gtcp.Conn) { defer conn.Close() for { data, err := conn.RecvPkg() if err != nil { if err.Error() == "EOF" { glog.Println("client closed") } break } info := &types.NodeInfo{} if err := json.Unmarshal(data, info); err != nil { glog.Errorf("invalid package structure: %s", err.Error()) } else { glog.Println(info) conn.SendPkg([]byte("ok")) } } }).Run() } ``` 3. `gtcp_monitor_client.go` 客户端。 ```go package main import ( "encoding/json" "github.com/gogf/gf/v2/frame/g" "github.com/gogf/gf/v2/net/gtcp" "github.com/gogf/gf/v2/os/glog" "github.com/gogf/gf/v2/os/gtime" "github.com/gogf/gf/.example/net/gtcp/pkg_operations/monitor/types" ) func main() { // 数据上报客户端 conn, err := gtcp.NewConn("127.0.0.1:8999") if err != nil { panic(err) } defer conn.Close() // 使用JSON格式化数据字段 info, err := json.Marshal(types.NodeInfo{ Cpu : float32(66.66), Host : "localhost", Ip : g.Map { "etho" : "192.168.1.100", "eth1" : "114.114.10.11", }, MemUsed : 15560320, MemTotal : 16333788, Time : int(gtime.Timestamp()), }) if err != nil { panic(err) } // 使用 SendRecvPkg 发送消息包并接受返回 if result, err := conn.SendRecvPkg(info); err != nil { if err.Error() == "EOF" { glog.Println("server closed") } } else { glog.Println(string(result)) } } ``` 4. 执行后 - 客户端输出结果为: ```html 2019-05-03 13:33:25.710 ok ``` - 服务端输出结果为: ```html 2019-05-03 13:33:25.710 &{66.66 localhost map[eth1:114.114.10.11 etho:192.168.1.100] 15560320 16333788 1556861605} 2019-05-03 13:33:25.710 client closed ``` # 连接对象-通信开发进阶 ## 开发进阶 针对于短连接而言,每一次发送接收数据即关闭连接,连接的处理逻辑比较简单,当然通信效率也会比较低。在大多数的TCP通信场景中,往往是使用长连接操作,并采用异步全双工的TCP通信模式,即所有的通信完全是异步。在这种场景下, `gtcp.Conn` 链接对象可能同时处于多个读写操作( `gtcp.Conn` 对象的数据读写操作是并发安全的),因此 `SendRecv` 操作在逻辑上将会失效。因为当在同一逻辑操作中发送完毕数据之后,随后立即获取数据有可能得到的是其他写操作的结果。 无论服务端还是客户端,都需要在独立的异步循环中封装使用 `Recv*` 方法获取数据并通过 `switch...case...` 处理数据(仅在一个 `goroutine` 中全权负责读取数据),根据请求数据进行业务处理的转发。 **提示** 也就是说, `Send*`/ `Recv*` 方法是并发安全的,但是发送数据时要一次性发送。由于支持异步并发写, `gtcp.Conn` 对象不带任何缓冲实现。 ## 使用示例 我们通过一个完成的示例来说明一下如何在程序中实现异步全双工通信,完成示例代码位于: [https://github.com/gogf/gf/tree/master/.example/net/gtcp/pkg\_operations/common](https://github.com/gogf/gf/tree/master/.example/net/gtcp/pkg_operations/common) 1. `types/types.go` 定义通信的数据格式,随后我们可以使用 `SendPkg`/ `RecvPkg` 方法来通信。 考虑到简化测试代码复杂度,因此这里使用 `JSON` 数据格式来传递数据。在一些对于消息包大小比较严格的场景中, `数据` 字段可以自行按照二进制进行封装解析设计。此外,需要注意的是,即使使用 `JSON` 数据格式,其中的 `Act` 字段往往定义常量来实现,大部分场景中使用 `uint8` 类型即可,以减小消息包大小,这里偷一下懒,直接使用字符串,以便演示。 ```go package types type Msg struct { Act string // 操作 Data string // 数据 } ``` 2. `funcs/funcs.go` 自定义数据格式的发送/获取定义,便于数据结构编码/解析。 ```go package funcs import ( "encoding/json" "fmt" "github.com/gogf/gf/v2/net/gtcp" "github.com/gogf/gf/.example/net/gtcp/pkg_operations/common/types" ) // 自定义格式发送消息包 func SendPkg(conn *gtcp.Conn, act string, data...string) error { s := "" if len(data) > 0 { s = data[0] } msg, err := json.Marshal(types.Msg{ Act : act, Data : s, }) if err != nil { panic(err) } return conn.SendPkg(msg) } // 自定义格式接收消息包 func RecvPkg(conn *gtcp.Conn) (msg *types.Msg, err error) { if data, err := conn.RecvPkg(); err != nil { return nil, err } else { msg = &types.Msg{} err = json.Unmarshal(data, msg) if err != nil { return nil, fmt.Errorf("invalid package structure: %s", err.Error()) } return msg, err } } ``` 3. `gtcp_common_server.go` 通信服务端。在该示例中,服务端并不主动断开连接,而是在 `10` 秒后向客户端发送 `doexit` 消息,通知客户端主动断开连接,以结束示例。 ```go package main import ( "github.com/gogf/gf/v2/net/gtcp" "github.com/gogf/gf/v2/os/glog" "github.com/gogf/gf/v2/os/gtimer" "github.com/gogf/gf/.example/net/gtcp/pkg_operations/common/funcs" "github.com/gogf/gf/.example/net/gtcp/pkg_operations/common/types" "time" ) func main() { gtcp.NewServer("127.0.0.1:8999", func(conn *gtcp.Conn) { defer conn.Close() // 测试消息, 10秒后让客户端主动退出 gtimer.SetTimeout(10*time.Second, func() { funcs.SendPkg(conn, "doexit") }) for { msg, err := funcs.RecvPkg(conn) if err != nil { if err.Error() == "EOF" { glog.Println("client closed") } break } switch msg.Act { case "hello": onClientHello(conn, msg) case "heartbeat": onClientHeartBeat(conn, msg) default: glog.Errorf("invalid message: %v", msg) break } } }).Run() } func onClientHello(conn *gtcp.Conn, msg *types.Msg) { glog.Printf("hello message from [%s]: %s", conn.RemoteAddr().String(), msg.Data) funcs.SendPkg(conn, msg.Act, "Nice to meet you!") } func onClientHeartBeat(conn *gtcp.Conn, msg *types.Msg) { glog.Printf("heartbeat from [%s]", conn.RemoteAddr().String()) } ``` 4. `gtcp_common_client.go` 通信客户端,可以看到代码结构和服务端差不多,数据获取独立处于 `for` 循环中,每个业务逻辑发送消息包时直接使用 `SendPkg` 方法进行发送。 心跳消息常用 `gtimer` 定时器实现,在该示例中,客户端每隔 `1` 秒主动向服务端发送心跳消息,在 `3` 秒后向服务端发送 `hello` 测试消息。这些都是由 `gtimer` 定时器实现的,定时器在TCP通信中比较常见。 客户端连接 `10` 秒后,服务端会给客户端发送 `doexit` 消息,客户端收到该消息后便主动断开连接,长连接结束。 ```go package main import ( "github.com/gogf/gf/v2/net/gtcp" "github.com/gogf/gf/v2/os/glog" "github.com/gogf/gf/v2/os/gtimer" "github.com/gogf/gf/.example/net/gtcp/pkg_operations/common/funcs" "github.com/gogf/gf/.example/net/gtcp/pkg_operations/common/types" "time" ) func main() { conn, err := gtcp.NewConn("127.0.0.1:8999") if err != nil { panic(err) } defer conn.Close() // 心跳消息 gtimer.SetInterval(time.Second, func() { if err := funcs.SendPkg(conn, "heartbeat"); err != nil { panic(err) } }) // 测试消息, 3秒后向服务端发送hello消息 gtimer.SetTimeout(3*time.Second, func() { if err := funcs.SendPkg(conn, "hello", "My name's John!"); err != nil { panic(err) } }) for { msg, err := funcs.RecvPkg(conn) if err != nil { if err.Error() == "EOF" { glog.Println("server closed") } break } switch msg.Act { case "hello": onServerHello(conn, msg) case "doexit": onServerDoExit(conn, msg) case "heartbeat": onServerHeartBeat(conn, msg) default: glog.Errorf("invalid message: %v", msg) break } } } func onServerHello(conn *gtcp.Conn, msg *types.Msg) { glog.Printf("hello response message from [%s]: %s", conn.RemoteAddr().String(), msg.Data) } func onServerHeartBeat(conn *gtcp.Conn, msg *types.Msg) { glog.Printf("heartbeat from [%s]", conn.RemoteAddr().String()) } func onServerDoExit(conn *gtcp.Conn, msg *types.Msg) { glog.Printf("exit command from [%s]", conn.RemoteAddr().String()) conn.Close() } ``` 5. 执行后 - ` ` 服务端输出结果 ```html 2019-05-03 14:59:13.732 heartbeat from [127.0.0.1:51220] 2019-05-03 14:59:14.732 heartbeat from [127.0.0.1:51220] 2019-05-03 14:59:15.733 heartbeat from [127.0.0.1:51220] 2019-05-03 14:59:15.733 hello message from [127.0.0.1:51220]: My name's John! 2019-05-03 14:59:16.731 heartbeat from [127.0.0.1:51220] 2019-05-03 14:59:17.733 heartbeat from [127.0.0.1:51220] 2019-05-03 14:59:18.731 heartbeat from [127.0.0.1:51220] 2019-05-03 14:59:19.730 heartbeat from [127.0.0.1:51220] 2019-05-03 14:59:20.732 heartbeat from [127.0.0.1:51220] 2019-05-03 14:59:21.732 heartbeat from [127.0.0.1:51220] 2019-05-03 14:59:22.698 client closed ``` - 客户端输出结果 ```html 2019-05-03 14:59:15.733 hello response message from [127.0.0.1:8999]: Nice to meet you! 2019-05-03 14:59:22.698 exit command from [127.0.0.1:8999] ``` # UDP组件 ## 基本介绍 `UDP (User Datagram Protocol)` 一种无连接的传输层协议,提供面向事务的简单不可靠信息传送服务。 `UDP` 服务端通过 `gudp.Server` 实现,客户端通过 `gudp.ClientConn` 对象或者工具方法实现。 **使用方式**: ```go import "github.com/gogf/gf/v2/net/gudp" ``` **接口文档**: [https://pkg.go.dev/github.com/gogf/gf/v2/net/gudp](https://pkg.go.dev/github.com/gogf/gf/v2/net/gudp) ## 使用示例 ```go package main import ( "fmt" "github.com/gogf/gf/v2/net/gudp" ) func main() { handler := func(conn *gudp.ServerConn) { defer conn.Close() for { if data, addr, _ := conn.Recv(-1); len(data) > 0 { fmt.Println(string(data), addr.String()) } } } err := gudp.NewServer("127.0.0.1:8999", handler).Run() if err != nil { fmt.Println(err) } } ``` `UDPServer` 是阻塞运行的,用户可以在自定义的回调函数中根据读取内容进行并发处理。 在 `Linux` 下可以使用以下命令向服务端发送 `UDP` 数据进行测试,随后查看服务端端是否有输出: ```bash echo "hello" > /dev/udp/127.0.0.1/8999 ``` # UDP组件-连接对象 `gudp` 模块提供了非常简便易用的 `gudp.Conn` 链接操作对象。 **使用方式**: ```go import "github.com/gogf/gf/v2/net/gudp" ``` **接口文档**: [https://pkg.go.dev/github.com/gogf/gf/v2/net/gudp](https://pkg.go.dev/github.com/gogf/gf/v2/net/gudp) ## 基本介绍 `gudp.Conn` 的操作绝大部分类似于 `gtcp` 的操作方式(大部分的方法名称也相同),但由于 `UDP` 是面向非连接的协议,因此 `gudp.Conn`(底层通信端口)也只能完成最多一次数据写入和读取,客户端下一次再与目标服务端进行通信的时候,将需要创建新的 `Conn` 对象进行通信。 ## 使用示例 ```go package main import ( "context" "fmt" "time" "github.com/gogf/gf/v2/frame/g" "github.com/gogf/gf/v2/net/gudp" "github.com/gogf/gf/v2/os/gtime" ) func main() { var ( ctx = context.Background() logger = g.Log() ) // Server go gudp.NewServer("127.0.0.1:8999", func(conn *gudp.ServerConn) { defer conn.Close() for { data, addr, err := conn.Recv(-1) if len(data) > 0 { if err = conn.Send(append([]byte("> "), data...), addr); err != nil { logger.Error(ctx, err) } } if err != nil { logger.Error(ctx, err) } } }).Run() time.Sleep(time.Second) // Client for { if conn, err := gudp.NewClientConn("127.0.0.1:8999"); err == nil { if b, err := conn.SendRecv([]byte(gtime.Datetime()), -1); err == nil { fmt.Println(string(b), conn.LocalAddr(), conn.RemoteAddr()) } else { logger.Error(ctx, err) } conn.Close() } else { logger.Error(ctx, err) } time.Sleep(time.Second) } } ``` 该示例与 `gtcp.Conn` 中的通信示例类似,不同的是,客户端与服务端无法保持连接,每次通信都需要创建的新的连接对象进行通信。 执行后,输出结果如下: ```text > 2018-07-21 23:13:31 127.0.0.1:33271 127.0.0.1:8999 > 2018-07-21 23:13:32 127.0.0.1:45826 127.0.0.1:8999 > 2018-07-21 23:13:33 127.0.0.1:58027 127.0.0.1:8999 > 2018-07-21 23:13:34 127.0.0.1:33056 127.0.0.1:8999 > 2018-07-21 23:13:35 127.0.0.1:39260 127.0.0.1:8999 > 2018-07-21 23:13:36 127.0.0.1:33967 127.0.0.1:8999 > 2018-07-21 23:13:37 127.0.0.1:52359 127.0.0.1:8999 ... ``` # UDP组件-工具方法 `gudp` 模块也提供了一些常用的工具方法。 **使用方式**: ```go import "github.com/gogf/gf/v2/net/gudp" ``` **接口文档**: [https://pkg.go.dev/github.com/gogf/gf/v2/net/gudp](https://pkg.go.dev/github.com/gogf/gf/v2/net/gudp) `gudp` 的工具相对比较简单,这里不做详细介绍。 # AES算法-gaes AES算法。 **使用方式**: ```go import "github.com/gogf/gf/v2/crypto/gaes" ``` **接口文档**: [https://pkg.go.dev/github.com/gogf/gf/v2/crypto/gaes](https://pkg.go.dev/github.com/gogf/gf/v2/crypto/gaes) **温馨提示:** 如果待解密数据经过其它编码,则要先解码再解密,如base64.decode 反过来也一样 如果希望加密完的数据编码,则将结果编码即可,如base64.encode # CRC32算法-gcrc32 CRC32算法。 **使用方式**: ```go import "github.com/gogf/gf/v2/crypto/gcrc32" ``` **接口文档**: [https://pkg.go.dev/github.com/gogf/gf/v2/crypto/gcrc32](https://pkg.go.dev/github.com/gogf/gf/v2/crypto/gcrc32) # DES算法-gdes DES算法。 **使用方式**: ```go import "github.com/gogf/gf/v2/crypto/gdes" ``` **接口文档**: [https://pkg.go.dev/github.com/gogf/gf/v2/crypto/gdes](https://pkg.go.dev/github.com/gogf/gf/v2/crypto/gdes) **关于 `gdes` 包中的补位说明:** **`gdes` 包中补位方式支持: `PKCS5PADDING`、 `NOPADDING` 两种方式,当使用 `NOPADDING` 方式时需要自定义补位方法。** **关于gdes包中的密钥的说明:** **当使用三倍长的DES算法时,密钥为16字节时,key3等于key1。** # MD5算法-gmd5 MD5算法。 **使用方式**: ```go import "github.com/gogf/gf/v2/crypto/gmd5" ``` **接口文档**: [https://pkg.go.dev/github.com/gogf/gf/v2/crypto/gmd5](https://pkg.go.dev/github.com/gogf/gf/v2/crypto/gmd5) # RSA算法-grsa ## 基本介绍 `grsa`组件提供了完整的`RSA`非对称加密算法实现,支持密钥对生成、数据加密解密等功能。该组件从`v2.10.0`版本开始提供。 ### 主要特性 1. **密钥对生成**:支持生成`PKCS#1`和`PKCS#8`格式的密钥对 2. **多种加密模式**:支持`PKCS#1 v1.5`和`OAEP`两种填充模式 3. **灵活的密钥格式**:支持`PEM`、`DER`、`Base64`等多种格式 4. **自动格式检测**:能够自动识别和处理不同格式的密钥 5. **安全性考虑**:提供推荐的`OAEP`填充模式,同时保持向后兼容 ### 安全建议 组件提供两种填充方案: 1. **PKCS#1 v1.5(传统)**:由`Encrypt*`、`DecryptPKCS1*`、`DecryptPKCS8*`等函数使用。该方案被认为安全性较低,容易受到填充预言攻击,但为了向后兼容仍然提供。 2. **OAEP(推荐)**:由`EncryptOAEP*`、`DecryptOAEP*`等函数使用。`OAEP`(最优非对称加密填充)是推荐用于新应用的填充方案,提供更好的安全保证。 **对于新项目,建议优先使用OAEP函数(`EncryptOAEP`、`DecryptOAEP`等)。** ## 使用方式 ```go import "github.com/gogf/gf/v2/crypto/grsa" ``` ## 接口文档 [https://pkg.go.dev/github.com/gogf/gf/v2/crypto/grsa](https://pkg.go.dev/github.com/gogf/gf/v2/crypto/grsa) ## 基本使用 ### 密钥对生成 ```go package main import ( "fmt" "github.com/gogf/gf/v2/crypto/grsa" ) func main() { // 生成默认2048位的PKCS#1格式密钥对 privateKey, publicKey, err := grsa.GenerateDefaultKeyPair() if err != nil { panic(err) } fmt.Println("Private Key:", string(privateKey)) fmt.Println("Public Key:", string(publicKey)) // 生成指定位数的PKCS#1格式密钥对 privateKey, publicKey, err = grsa.GenerateKeyPair(4096) if err != nil { panic(err) } // 生成PKCS#8格式密钥对 privateKey, publicKey, err = grsa.GenerateKeyPairPKCS8(2048) if err != nil { panic(err) } } ``` ### 基本加密解密(PKCS#1 v1.5) ```go package main import ( "fmt" "github.com/gogf/gf/v2/crypto/grsa" ) func main() { // 生成密钥对 privateKey, publicKey, err := grsa.GenerateDefaultKeyPair() if err != nil { panic(err) } // 要加密的数据 plainText := []byte("Hello, GoFrame!") // 使用公钥加密(自动检测密钥格式) cipherText, err := grsa.Encrypt(plainText, publicKey) if err != nil { panic(err) } fmt.Println("Encrypted:", cipherText) // 使用私钥解密(自动检测密钥格式) decrypted, err := grsa.Decrypt(cipherText, privateKey) if err != nil { panic(err) } fmt.Println("Decrypted:", string(decrypted)) } ``` ### OAEP加密解密(推荐) ```go package main import ( "fmt" "github.com/gogf/gf/v2/crypto/grsa" ) func main() { // 生成密钥对 privateKey, publicKey, err := grsa.GenerateDefaultKeyPair() if err != nil { panic(err) } // 要加密的数据 plainText := []byte("Hello, GoFrame!") // 使用OAEP模式加密(推荐) cipherText, err := grsa.EncryptOAEP(plainText, publicKey) if err != nil { panic(err) } fmt.Println("Encrypted with OAEP:", cipherText) // 使用OAEP模式解密 decrypted, err := grsa.DecryptOAEP(cipherText, privateKey) if err != nil { panic(err) } fmt.Println("Decrypted:", string(decrypted)) } ``` ### Base64编码支持 ```go package main import ( "encoding/base64" "fmt" "github.com/gogf/gf/v2/crypto/grsa" ) func main() { // 生成密钥对 privateKey, publicKey, err := grsa.GenerateDefaultKeyPair() if err != nil { panic(err) } // 将密钥转换为Base64格式(方便存储和传输) privateKeyBase64 := base64.StdEncoding.EncodeToString(privateKey) publicKeyBase64 := base64.StdEncoding.EncodeToString(publicKey) // 要加密的数据 plainText := []byte("Hello, GoFrame!") // 使用Base64格式的公钥加密,返回Base64格式的密文 cipherTextBase64, err := grsa.EncryptBase64(plainText, publicKeyBase64) if err != nil { panic(err) } fmt.Println("Encrypted (Base64):", cipherTextBase64) // 使用Base64格式的私钥解密Base64格式的密文 decrypted, err := grsa.DecryptBase64(cipherTextBase64, privateKeyBase64) if err != nil { panic(err) } fmt.Println("Decrypted:", string(decrypted)) } ``` ### 指定密钥格式加密 ```go package main import ( "fmt" "github.com/gogf/gf/v2/crypto/grsa" ) func main() { // 生成PKCS#1格式密钥对 privateKeyPKCS1, publicKeyPKCS1, _ := grsa.GenerateKeyPair(2048) // 生成PKCS#8格式密钥对 privateKeyPKCS8, publicKeyPKCS8, _ := grsa.GenerateKeyPairPKCS8(2048) plainText := []byte("Hello, GoFrame!") // 使用PKCS#1格式公钥加密 cipherText1, _ := grsa.EncryptPKCS1(plainText, publicKeyPKCS1) decrypted1, _ := grsa.DecryptPKCS1(cipherText1, privateKeyPKCS1) fmt.Println("PKCS#1:", string(decrypted1)) // 使用PKCS#8格式公钥加密 cipherText2, _ := grsa.EncryptPKCS8(plainText, publicKeyPKCS8) decrypted2, _ := grsa.DecryptPKCS8(cipherText2, privateKeyPKCS8) fmt.Println("PKCS#8:", string(decrypted2)) // 使用PKIX格式公钥加密(标准格式) cipherText3, _ := grsa.EncryptPKIX(plainText, publicKeyPKCS8) decrypted3, _ := grsa.DecryptPKCS8(cipherText3, privateKeyPKCS8) fmt.Println("PKIX:", string(decrypted3)) } ``` ### 密钥类型检测 ```go package main import ( "fmt" "github.com/gogf/gf/v2/crypto/grsa" ) func main() { // 生成不同格式的密钥 privateKeyPKCS1, _, _ := grsa.GenerateKeyPair(2048) privateKeyPKCS8, _, _ := grsa.GenerateKeyPairPKCS8(2048) // 检测密钥类型 keyType1, _ := grsa.GetPrivateKeyType(privateKeyPKCS1) fmt.Println("Key Type 1:", keyType1) // 输出: PKCS#1 keyType2, _ := grsa.GetPrivateKeyType(privateKeyPKCS8) fmt.Println("Key Type 2:", keyType2) // 输出: PKCS#8 } ``` ## 注意事项 ### 加密数据大小限制 `RSA`加密对明文大小有限制,具体取决于密钥长度和填充模式: - **PKCS#1 v1.5填充**:`最大明文长度 = 密钥字节数 - 11` - 例如:`2048位`密钥可以加密最多`245字节` - **OAEP填充**:`最大明文长度 = 密钥字节数 - 2 * 哈希长度 - 2` - 例如:`2048位`密钥使用`SHA-256`可以加密最多`190字节` 如需加密大量数据,建议使用混合加密方案:用`RSA`加密对称密钥,用对称密钥(如`AES`)加密实际数据。 ### 密钥格式说明 - **PKCS#1**:传统的`RSA`密钥格式,`PEM`头为`RSA PRIVATE KEY`和`RSA PUBLIC KEY` - **PKCS#8**:通用的私钥格式,`PEM`头为`PRIVATE KEY`,公钥格式为`PKIX`(`PEM`头为`PUBLIC KEY`) - 组件的`Encrypt`和`Decrypt`方法可以自动识别和处理这两种格式 ### 安全性建议 1. **密钥长度**:建议使用至少2048位的密钥,4096位更安全 2. **填充模式**:新项目使用`EncryptOAEP`和`DecryptOAEP` 3. **密钥保护**:私钥必须妥善保管,不要硬编码在代码中 4. **密钥轮换**:定期更换密钥对以提高安全性 5. **证书验证**:在实际应用中,建议结合数字证书进行身份验证 # SHA1算法-gsha1 SHA1算法。 **使用方式**: ```go import "github.com/gogf/gf/v2/crypto/gsha1" ``` **接口文档**: [https://pkg.go.dev/github.com/gogf/gf/v2/crypto/gsha1](https://pkg.go.dev/github.com/gogf/gf/v2/crypto/gsha1) # SHA256算法-gsha256 ## 基本介绍 `gsha256`组件提供了`SHA-256`哈希算法的完整实现,用于生成数据的`256位(32字节)`哈希值。该组件从`v2.10.0`版本开始提供。 ### 主要特性 1. **字符串哈希**:计算字符串的`SHA-256`哈希值 2. **字节数组哈希**:计算字节数组的`SHA-256`哈希值 3. **文件哈希**:计算文件内容的`SHA-256`哈希值 4. **HMAC签名**:支持`HMAC-SHA256`消息认证码 5. **多种输出格式**:支持十六进制字符串和原始字节输出 ### 应用场景 1. **数据完整性校验**:验证数据在传输或存储过程中是否被篡改 2. **密码存储**:存储密码的哈希值而不是明文(建议配合盐值使用) 3. **文件校验**:生成和验证文件的数字指纹 4. **数字签名**:作为签名算法的哈希部分 5. **区块链应用**:计算区块哈希值 6. **API签名验证**:使用`HMAC-SHA256`验证`API`请求的真实性 ## 使用方式 ```go import "github.com/gogf/gf/v2/crypto/gsha256" ``` ## 接口文档 [https://pkg.go.dev/github.com/gogf/gf/v2/crypto/gsha256](https://pkg.go.dev/github.com/gogf/gf/v2/crypto/gsha256) ## 基本使用 ### 字符串哈希 ```go package main import ( "fmt" "github.com/gogf/gf/v2/crypto/gsha256" ) func main() { // 计算字符串的SHA-256哈希 text := "Hello, GoFrame!" hash := gsha256.Encrypt(text) fmt.Println("SHA-256 Hash:", hash) // 输出: SHA-256 Hash: 8b3d9c5e1a2f4b8d... (64个十六进制字符) } ``` ### 字节数组哈希 ```go package main import ( "fmt" "github.com/gogf/gf/v2/crypto/gsha256" ) func main() { // 计算字节数组的SHA-256哈希 data := []byte("Hello, GoFrame!") hash := gsha256.EncryptBytes(data) fmt.Printf("SHA-256 Hash: %x\n", hash) // 输出32字节的哈希值 } ``` ### 文件哈希 ```go package main import ( "fmt" "github.com/gogf/gf/v2/crypto/gsha256" ) func main() { // 计算文件的SHA-256哈希 filePath := "/path/to/file.txt" hash, err := gsha256.EncryptFile(filePath) if err != nil { panic(err) } fmt.Println("File SHA-256 Hash:", hash) } ``` ### HMAC-SHA256签名 ```go package main import ( "fmt" "github.com/gogf/gf/v2/crypto/gsha256" ) func main() { // 使用密钥生成HMAC-SHA256签名 data := "Hello, GoFrame!" key := "my-secret-key" // 生成HMAC签名 signature := gsha256.MustEncryptHmac(data, key) fmt.Println("HMAC-SHA256 Signature:", signature) // 验证签名(比较哈希值是否相同) expectedSignature := gsha256.MustEncryptHmac(data, key) if signature == expectedSignature { fmt.Println("Signature is valid!") } } ``` ### 字节形式的HMAC签名 ```go package main import ( "fmt" "github.com/gogf/gf/v2/crypto/gsha256" ) func main() { data := []byte("Hello, GoFrame!") key := []byte("my-secret-key") // 生成字节形式的HMAC签名 signature := gsha256.MustEncryptHmacBytes(data, key) fmt.Printf("HMAC-SHA256 Signature: %x\n", signature) // 输出32字节的签名值 } ``` ## 实际应用示例 ### 密码哈希存储 ```go package main import ( "fmt" "github.com/gogf/gf/v2/crypto/gsha256" "github.com/gogf/gf/v2/util/grand" ) func main() { // 用户密码 password := "myPassword123" // 生成随机盐值 salt := grand.S(16) // 计算密码+盐值的哈希 passwordHash := gsha256.Encrypt(password + salt) fmt.Println("Salt:", salt) fmt.Println("Password Hash:", passwordHash) // 存储 salt 和 passwordHash 到数据库 // 验证密码时 inputPassword := "myPassword123" computedHash := gsha256.Encrypt(inputPassword + salt) if computedHash == passwordHash { fmt.Println("Password is correct!") } else { fmt.Println("Password is incorrect!") } } ``` ### 文件完整性校验 ```go package main import ( "fmt" "github.com/gogf/gf/v2/crypto/gsha256" "github.com/gogf/gf/v2/os/gfile" ) func main() { filePath := "/path/to/important/file.dat" // 计算原始文件的哈希值 originalHash, err := gsha256.EncryptFile(filePath) if err != nil { panic(err) } fmt.Println("Original Hash:", originalHash) // 保存哈希值用于后续验证 gfile.PutContents(filePath+".sha256", originalHash) // 稍后验证文件完整性 currentHash, err := gsha256.EncryptFile(filePath) if err != nil { panic(err) } savedHash := gfile.GetContents(filePath + ".sha256") if currentHash == savedHash { fmt.Println("File integrity verified!") } else { fmt.Println("File has been modified!") } } ``` ### API请求签名 ```go package main import ( "fmt" "github.com/gogf/gf/v2/crypto/gsha256" "github.com/gogf/gf/v2/encoding/gjson" "github.com/gogf/gf/v2/util/gconv" "sort" "strings" ) func main() { // API请求参数 params := map[string]interface{}{ "user_id": "12345", "timestamp": "1609459200", "action": "get_user_info", } // API密钥 apiSecret := "my-api-secret-key" // 生成签名 signature := generateSignature(params, apiSecret) fmt.Println("API Signature:", signature) // 验证签名 receivedSignature := signature // 从请求中获取 computedSignature := generateSignature(params, apiSecret) if receivedSignature == computedSignature { fmt.Println("Signature is valid!") } else { fmt.Println("Signature is invalid!") } } // generateSignature 生成API签名 func generateSignature(params map[string]interface{}, secret string) string { // 1. 将参数按键名排序 keys := make([]string, 0, len(params)) for k := range params { keys = append(keys, k) } sort.Strings(keys) // 2. 拼接参数字符串 var parts []string for _, k := range keys { parts = append(parts, fmt.Sprintf("%s=%s", k, gconv.String(params[k]))) } signString := strings.Join(parts, "&") // 3. 使用HMAC-SHA256生成签名 return gsha256.MustEncryptHmac(signString, secret) } ``` ### 数据去重(使用哈希作为唯一标识) ```go package main import ( "fmt" "github.com/gogf/gf/v2/crypto/gsha256" ) func main() { // 使用哈希值去重大量数据 seen := make(map[string]bool) data := []string{ "data1", "data2", "data1", // 重复 "data3", "data2", // 重复 } var uniqueData []string for _, item := range data { // 计算数据的哈希值 hash := gsha256.Encrypt(item) // 检查是否已经存在 if !seen[hash] { seen[hash] = true uniqueData = append(uniqueData, item) } } fmt.Println("Unique Data:", uniqueData) // 输出: Unique Data: [data1 data2 data3] } ``` ## 注意事项 ### SHA-256的不可逆性 `SHA-256`是一种单向哈希函数,**无法从哈希值还原出原始数据**。这使其适合存储敏感信息(如密码)的哈希值,但不适合需要恢复原始数据的场景。 ### 密码存储建议 对于密码存储,建议: 1. **使用盐值**:为每个密码生成随机盐值,防止彩虹表攻击 2. **使用专用算法**:考虑使用`bcrypt`、`scrypt`或`Argon2`等专为密码设计的算法 3. **多次哈希**:可以对哈希结果再次哈希(如`PBKDF2`),增加破解难度 ### 碰撞抵抗性 虽然`SHA-256`具有很强的碰撞抵抗性(找到两个不同输入产生相同哈希值的难度极大),但在某些对安全性要求极高的场景,可以考虑使用`SHA-384`或`SHA-512`。 ### 性能考虑 - 文件哈希计算的时间与文件大小成正比 - 对于大文件,考虑分块计算或异步处理 - `HMAC`操作比普通哈希略慢,因为需要进行额外的密钥处理 ## 与其他哈希算法对比 | 算法 | 输出长度 | 安全性 | 速度 | 适用场景 | |------|---------|--------|------|---------| | `MD5` | `128位` | 已破解 | 快 | 仅用于非安全场景的校验 | | `SHA-1` | `160位` | 理论破解 | 较快 | 不推荐用于安全场景 | | **`SHA-256`** | **`256位`** | **安全** | **中等** | **推荐用于大多数场景** | | `SHA-512` | `512位` | 非常安全 | 稍慢 | 高安全性要求场景 | # 随机数-grand `grand` 模块实现了对随机数操作的封装和改进,实现了极高的随机数生成性能,提供了丰富的随机数相关操作方法。 使用方式: ```go import "github.com/gogf/gf/v2/util/grand" ``` 接口文档: [https://pkg.go.dev/github.com/gogf/gf/v2/util/grand](https://pkg.go.dev/github.com/gogf/gf/v2/util/grand) 常用方法: ```go func N(min, max int) int func B(n int) []byte func S(n int, symbols ...bool) string func Str(s string, n int) string func Intn(max int) int func Digits(n int) string func Letters(n int) string func Meet(num, total int) bool func MeetProb(prob float32) bool func Perm(n int) []int func Symbols(n int) string ``` ### 字符列表 ``` 字符类型 字符列表 数字字符 0123456789 英文字符 abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ 特殊字符 !\"#$%&'()*+,-./:;<=>?@[\\]^_`{|}~ ``` ### 随机整数 1. `Intn` 方法返回大于等 `0` 且不大于 `max` 的随机整数,即: `[0, max)`。 2. `N` 方法返回 `min` 到 `max` 之间的随机整数,支持负数,包含边界,即: `[min, max]`。 ### 随机字符串 1. `B` 方法用于返回指定长度的二进制 `[]byte` 数据。 2. `S` 方法用于返回指定长度的数字、字符,第二个参数 `symbols` 用于指定知否返回的随机字符串中包含特殊字符。 3. `Str` 方法是一个比较高级的方法,用于从给定的字符列表中选择指定长度的随机字符串返回,并且支持 `unicode` 字符,例如中文。例如, `Str("中文123abc", 3)` 将可能会返回 `1a文` 的随机字符串。 4. `Digits` 方法用于返回指定长度的随机数字字符串。 5. `Letters` 方法用于返回指定长度的随机英文字符串。 6. `Symbols` 方法用于返回指定长度的随机特殊字符串。 ### 概率性计算 1. `Meet` 用于指定一个数 `num` 和总数 `total`,往往 `num<=total`,并随机计算是否满足 `num/total` 的概率。例如, `Meet(1, 100)` 将会随机计算是否满足百分之一的概率。 2. `MeetProb` 用于给定一个概率浮点数 `prob`,往往 `prob<=1.0`,并随机计算是否满足该概率。例如, `MeetProb(0.005)` 将会随机计算是否满足千分之五的概率。 # 唯一数-guid `guid` 提供了更简便更高性能的全局唯一数生成功能。生成的 `uid` 字符串仅包含 **数字及小写英文字符**。 - **优点**:性能高效、使用简便。 - **缺点**:字符范围有限、长度固定 `32` 字节。 > `guid` 模块的设计目的在于提供一种使用更简便、性能更高效且能满足绝大多数业务场景的唯一数生成。 `guid` 的设计比较简单,详情可参考实现源码。 **字符列表**: ``` 字符类型 字符列表 数字字符 0123456789 英文字符 abcdefghijklmnopqrstuvwxyz ``` **使用方式**: ```go import "github.com/gogf/gf/v2/util/guid" ``` **接口文档**: [https://pkg.go.dev/github.com/gogf/gf/v2/util/guid](https://pkg.go.dev/github.com/gogf/gf/v2/util/guid) ### 基本介绍 `guid` 通过 `S` 方法生成 `32` 字节的唯一数,该方法定义如下: ```go func S(data ...[]byte) string ``` 1. 通过不带任何参数的方式使用,该方法生成的唯一数将会有以下方式构成: `MACHash(7) + PID(4) + TimestampNano(12) + Sequence(3) + RandomString(6)` 其中: - `MAC` 表示当前机器的 `MAC` 地址哈希值,由 `7` 个字节构成; - `PID` 表示当前机器的进程ID,由 `4` 个字节构成; - `TimestampNano` 表示当前的纳秒时间戳,由 `12` 个字节构成; - `Sequence` 表示当前进程并发安全的序列号,由 `3` 个字节构成; - `RandomString` 表示随机数,由 `6` 个字节构成; 2. 通过自定义任何参数的方式使用,该方法生成的唯一数将会有以下方式构成: `DataHash(7/14) + TimestampNano(12) + Sequence(3) + RandomString(3/10)` 主要说明: - `Data` 表示自定义的参数,参数类型为 `[]byte`,最多支持 `2` 个参数输入,由 `7` 或 `14` 个字节构成; - 需要注意的是,输入的自定义参数需要在业务上具有一定的唯一识别性,使得生成的唯一数更有价值; - 不管每一个 `[]byte` 参数长度为多少,最终都将通过哈希方式生成 `7` 个字节的哈希值。 - `TimestampNano` 表示当前的纳秒时间戳,由 `12` 个字节构成; - `Sequence` 表示当前进程并发安全的序列号,由 `3` 个字节构成; - `RandomString` 表示随机数,由 `3` 或者 `10` 个字节构成,即: - 如果给定 `1` 个自定义参数,那么剩余的字节将会使用随机数占位,长度为 `10` 个字节; - 如果给定 `2` 个自定义参数,那么剩余的字节将会使用随机数占位,长度为 `3` 个字节; ### 基准测试 ``` goos: darwin goarch: amd64 pkg: github.com/gogf/gf/v2/util/guid cpu: Intel(R) Core(TM) i7-9750H CPU @ 2.60GHz Benchmark_S Benchmark_S-12 2665587 423.8 ns/op Benchmark_S_Data_1 Benchmark_S_Data_1-12 2027568 568.2 ns/op Benchmark_S_Data_2 Benchmark_S_Data_2-12 4352824 275.5 ns/op PASS ``` ### 示例1,基本使用 ```go package main import ( "fmt" "github.com/gogf/gf/v2/util/guid" ) func main() { fmt.Printf("TraceId: %s", guid.S()) } ``` 执行后,输出结果为: ``` TraceId: oa9sdw03dk0c35q9bdwcnz42p00trwfr ``` ### 示例2,自定义参数 我们的 `SessionId` 生成需要具有比较好的唯一性,且需要防止轻易的碰撞,因此可以使用以下方式: ```go func CreateSessionId(r *ghttp.Request) string { var ( address = request.RemoteAddr header = fmt.Sprintf("%v", request.Header) ) return guid.S([]byte(address), []byte(header)) } ``` 可以看到, `SessionId` 需要依靠自定义的两个输入参数 `RemoteAddr`, `Header` 来生成,这两个参数在业务上具有一定的唯一识别性,且通过 `guid.S` 方法的设计构成,生成的唯一数将会非常随机且唯一,既满足了业务需要也保证了安全。 # 元数据-gmeta ## 基本介绍 主要用于嵌入到用户自定义的结构体中,并且通过标签的形式给 `gmeta` 包的结构体打上自定义的标签内容(元数据),并在运行时可以特定方法动态获取这些自定义的标签内容。 **使用方式:** ```go import "github.com/gogf/gf/v2/util/gmeta" ``` **接口文档**: [https://pkg.go.dev/github.com/gogf/gf/v2/util/gmeta](https://pkg.go.dev/github.com/gogf/gf/v2/util/gmeta) **方法列表:** ```go func Data(object interface{}) map[string]interface{} func Get(object interface{}, key string) *gvar.Var ``` ## 使用示例 ### `Data` 方法 `Data` 方法用于获取指定 `struct` 对象的元数据标签,构成 `map` 返回。 ```go package main import ( "fmt" "github.com/gogf/gf/v2/frame/g" "github.com/gogf/gf/v2/util/gmeta" ) func main() { type User struct { g.Meta `orm:"user" db:"mysql"` Id int Name string } g.Dump(gmeta.Data(User{})) } ``` **提示** 大部分时候,在结构体定义中,我们使用的`gmeta.Meta`的别名`g.Meta`。 执行后,终端输出: ```json { "db": "mysql", "orm": "user" } ``` ### `Get` 方法 `Get` 方法用于获取指定 `struct` 对象中指定名称的元数据标签信息。 ```go package main import ( "fmt" "github.com/gogf/gf/v2/util/gmeta" ) func main() { type User struct { g.Meta `orm:"user" db:"mysql"` Id int Name string } user := User{} fmt.Println(gmeta.Get(user, "orm").String()) fmt.Println(gmeta.Get(user, "db").String()) } ``` 执行后,终端输出: ```text user mysql ``` # 类型转换-gconv 类型转换功能由 `gconv` 模块实现,具体请参考 [类型转换](../core/gconv.md) 章节。 # 数据校验-gvalid 数据/表单校验由 `gvalid` 模块实现,具体请参考 [数据校验](../core/gvalid.md) 章节。 # 分页管理-gpage **注意**:废弃提示 从版本`v2.10.0`开始,`gpage`组件已被标记为**废弃**,将在未来版本中被移除。 **推荐替代方案**: 1. **使用ORM分页功能**:`GoFrame`的`gdb` `ORM`组件提供了完善的分页支持。 2. **自定义分页逻辑**:根据业务需求实现分页,前端使用现代化的`UI`组件库。 3. **API分页**:返回分页数据结构,由前端框架处理分页显示。 分页管理由 `gpage` 模块实现,具体请参考 [分页管理](../web/paging.md) 章节。 # 工具方法-gutil ## 基本介绍 `gutil` 组件封装了一些开发中常用的工具方法。 使用方式: ```go import "github.com/gogf/gf/v2/util/gutil" ``` 接口文档: [https://pkg.go.dev/github.com/gogf/gf/v2/util/gutil](https://pkg.go.dev/github.com/gogf/gf/v2/util/gutil) ## 常用方法 ### `Dump` - 说明: `Dump` 将 `values` 以更好的可读性的方式输出在标准输出中。 - 格式: ```go Dump(values ...interface{}) ``` - 示例: ```go type User struct { Name string Age int } type Location struct { Province string City string } type UserInfo struct { U User L Location } func main() { userList := make([]UserInfo, 0) userList = append(userList, UserInfo{ U: User{ Name: "郭强", Age: 18, }, L: Location{ Province: "四川", City: "成都", }, }) userList = append(userList, UserInfo{ U: User{ Name: "黄骞", Age: 18, }, L: Location{ Province: "江苏", City: "南京", }, }) gutil.Dump(userList) } // Output: [ { U: { Name: "郭强", Age: 18, }, L: { Province: "四川", City: "成都", }, }, { U: { Name: "黄骞", Age: 18, }, L: { Province: "江苏", City: "南京", }, }, ] ``` ### `DumpWithType` - 说明: `DumpWithType` 和 `Dump` 类似,但是多了类型信息。 - 格式: ```go DumpWithType(values ...interface{}) ``` - 示例: ```go type User struct { Name string Age int } type Location struct { Province string City string } type UserInfo struct { U User L Location } func main() { userList := make([]UserInfo, 0) userList = append(userList, UserInfo{ U: User{ Name: "郭强", Age: 18, }, L: Location{ Province: "四川", City: "成都", }, }) userList = append(userList, UserInfo{ U: User{ Name: "黄骞", Age: 18, }, L: Location{ Province: "江苏", City: "南京", }, }) gutil.DumpWithType(userList) } // Output: []main.UserInfo(2) [ main.UserInfo(2) { U: main.User(2) { Name: string(6) "郭强", Age: int(18), }, L: main.Location(2) { Province: string(6) "四川", City: string(6) "成都", }, }, main.UserInfo(2) { U: main.User(2) { Name: string(6) "黄骞", Age: int(18), }, L: main.Location(2) { Province: string(6) "江苏", City: string(6) "南京", }, }, ] ``` ### `DumpTo` - 说明: `DumpTo` 将 `value` 以自定义的输出形式写入到 `write` 中。 - 格式: ```go DumpTo(writer io.Writer, value interface{}, option DumpOption) ``` - 示例: ```go package main import ( "bytes" "fmt" "github.com/gogf/gf/v2/util/gutil" "io" ) type UserInfo struct { Name string Age int Province string City string } type DumpWriter struct { Content string } func (d *DumpWriter) Write(p []byte) (n int, err error) { buffer := bytes.NewBuffer(nil) buffer.WriteString("I'm Start!\n") buffer.WriteString(string(p)) buffer.WriteString("\nI'm End!\n") d.Content = buffer.String() return buffer.Len(), nil } func main() { u := UserInfo{ "a", 18, "b", "c", } var dw io.Writer = &DumpWriter{} gutil.DumpTo(dw, u, gutil.DumpOption{}) fmt.Println(dw.(*DumpWriter).Content) } // Output: I'm Start! { Name: "a", Age: 18, Province: "b", City: "c", } I'm End! ``` # 单元测试-gtest ## 基本介绍 `gtest` 模块提供了简便化的、轻量级的、常用的单元测试方法。是基于标准库 `testing` 的功能扩展封装,主要增加实现了以下特性: - 单元测试用例多测试项的隔离。 - 增加常用的一系列测试断言方法。 - 断言方法支持多种常见格式断言。提高易用性。 - 测试失败时的错误信息格式统一。 **提示** `gtest` 设计为比较简便易用,可以满足绝大部分的单元测试场景,如果涉及更复杂的测试场景,可以考虑第三方的 `testify`、 `goconvey` 等测试框架。 **使用方式**: ```go import "github.com/gogf/gf/v2/test/gtest" ``` **接口文档**: 本章节更新可能不及时,更全面的接口介绍请参考接口文档。 [https://pkg.go.dev/github.com/gogf/gf/v2/test/gtest](https://pkg.go.dev/github.com/gogf/gf/v2/test/gtest) ```go func C(t *testing.T, f func(t *T)) func Assert(value, expect interface{}) func AssertEQ(value, expect interface{}) func AssertGE(value, expect interface{}) func AssertGT(value, expect interface{}) func AssertIN(value, expect interface{}) func AssertLE(value, expect interface{}) func AssertLT(value, expect interface{}) func AssertNE(value, expect interface{}) func AssertNI(value, expect interface{}) func AssertNil(value interface{}) func DataPath(names ...string) string func DataContent(names ...string) string func Error(message ...interface{}) func Fatal(message ...interface{}) ``` **简要说明**: 1. 使用 `C` 方法创建一个 `Case`,表示一个单元测试用例。一个单元测试方法可以包含多个 `C`,每一个 `C` 包含的用例往往表示该方法的其中一种可能性测试。 2. 断言方法 `Assert` 支持任意类型的变量比较。 `AssertEQ` 进行断言比较时,会同时比较类型,即严格断言。 3. 使用大小比较断言方法如 `AssertGE` 时,参数支持字符串及数字比较,其中字符串比较为大小写敏感。 4. 包含断言方法 `AssertIN` 及 `AssertNI` 在 v2.9 版本中增强了对 `map` 类型参数的支持。 用于单元测试的包名既可以使用 `包名_test`,也可直接使用 `包名`(即与测试包同名)。两种使用方式都比较常见,且在 `Go` 官方标准库中也均有涉及。但需要注意的是,当需要测试包的私有方法/私有变量时,必须使用 `包名` 命名形式。且在使用 `包名` 命名方式时,注意仅用于单元测试的相关方法(非 `Test*` 测试方法)一般定义为私有,不要公开。 ## 基本断言方法 ### `Assert` `Assert` 方法用于检查 `value` 和 `expect` 是否相等。它会将两个值转换为字符串进行比较,如果不相等则会触发 panic。 ```go func Assert(value, expect interface{}) ``` 示例: ```go gtest.C(t, func(t *gtest.T) { t.Assert(1, 1) // 通过 t.Assert("hello", "hello") // 通过 t.Assert(1, "1") // 通过,会转换为字符串比较 t.Assert(map[string]string{"name": "john"}, map[string]string{"name": "john"}) // 通过 }) ``` ### `AssertEQ` `AssertEQ` 方法用于检查 `value` 和 `expect` 是否相等,包括它们的类型。这是一个严格的相等检查。 ```go func AssertEQ(value, expect interface{}) ``` 示例: ```go gtest.C(t, func(t *gtest.T) { t.AssertEQ(1, 1) // 通过 t.AssertEQ("hello", "hello") // 通过 t.AssertEQ(1, "1") // 失败,类型不同 t.AssertEQ(float32(1.0), float32(1.0)) // 通过 t.AssertEQ(float32(1.0), float64(1.0)) // 失败,类型不同 }) ``` ### `AssertNE` `AssertNE` 方法用于检查 `value` 和 `expect` 是否不相等。 ```go func AssertNE(value, expect interface{}) ``` 示例: ```go gtest.C(t, func(t *gtest.T) { t.AssertNE(1, 0) // 通过 t.AssertNE("hello", "world") // 通过 t.AssertNE(1, "0") // 通过 }) ``` ### `AssertNQ` `AssertNQ` 方法用于检查 `value` 和 `expect` 是否不相等,包括它们的类型。 ```go func AssertNQ(value, expect interface{}) ``` 示例: ```go gtest.C(t, func(t *gtest.T) { t.AssertNQ(1, "1") // 通过,类型不同 t.AssertNQ(1.0, 1) // 通过,类型不同 }) ``` ## 比较断言方法 ### `AssertGT` `AssertGT` 方法用于检查 `value` 是否大于 `expect`。仅支持字符串、整数和浮点数类型的比较。 ```go func AssertGT(value, expect interface{}) ``` 示例: ```go gtest.C(t, func(t *gtest.T) { t.AssertGT(2, 1) // 通过 t.AssertGT("b", "a") // 通过 t.AssertGT(2.1, 2.0) // 通过 }) ``` ### `AssertGE` `AssertGE` 方法用于检查 `value` 是否大于等于 `expect`。仅支持字符串、整数和浮点数类型的比较。 ```go func AssertGE(value, expect interface{}) ``` 示例: ```go gtest.C(t, func(t *gtest.T) { t.AssertGE(2, 1) // 通过 t.AssertGE(1, 1) // 通过 t.AssertGE("b", "a") // 通过 t.AssertGE("a", "a") // 通过 }) ``` ### `AssertLT` `AssertLT` 方法用于检查 `value` 是否小于 `expect`。仅支持字符串、整数和浮点数类型的比较。 ```go func AssertLT(value, expect interface{}) ``` 示例: ```go gtest.C(t, func(t *gtest.T) { t.AssertLT(1, 2) // 通过 t.AssertLT("a", "b") // 通过 t.AssertLT(2.0, 2.1) // 通过 }) ``` ### `AssertLE` `AssertLE` 方法用于检查 `value` 是否小于等于 `expect`。仅支持字符串、整数和浮点数类型的比较。 ```go func AssertLE(value, expect interface{}) ``` 示例: ```go gtest.C(t, func(t *gtest.T) { t.AssertLE(1, 2) // 通过 t.AssertLE(1, 1) // 通过 t.AssertLE("a", "b") // 通过 t.AssertLE("a", "a") // 通过 }) ``` ## 包含断言方法 ### `AssertIN` `AssertIN` 方法用于检查 `value` 是否在 `expect` 中。`expect` 可以是切片、数组、字符串或映射类型。 ```go func AssertIN(value, expect interface{}) ``` 示例: ```go gtest.C(t, func(t *gtest.T) { // 切片类型 t.AssertIN("a", []string{"a", "b", "c"}) // 通过 t.AssertIN(1, []int{1, 2, 3}) // 通过 // 多个值检查 t.AssertIN([]string{"a", "b"}, []string{"a", "b", "c"}) // 通过 // 字符串类型 t.AssertIN("a", "abc") // 通过 // Map类型 (v2.9版本新增) t.AssertIN("k1", map[string]string{"k1": "v1", "k2": "v2"}) // 通过 t.AssertIN(1, map[int]string{1: "v1", 2: "v2"}) // 通过 t.AssertIN([]string{"k1", "k2"}, map[string]string{"k1": "v1", "k2": "v2"}) // 通过 }) ``` ### `AssertNI` `AssertNI` 方法用于检查 `value` 是否不在 `expect` 中。`expect` 可以是切片、数组、字符串或映射类型。 ```go func AssertNI(value, expect interface{}) ``` 示例: ```go gtest.C(t, func(t *gtest.T) { // 切片类型 t.AssertNI("d", []string{"a", "b", "c"}) // 通过 t.AssertNI(4, []int{1, 2, 3}) // 通过 // 多个值检查 t.AssertNI([]string{"d", "e"}, []string{"a", "b", "c"}) // 通过 // 字符串类型 t.AssertNI("d", "abc") // 通过 // Map类型 (v2.9版本新增) t.AssertNI("k3", map[string]string{"k1": "v1", "k2": "v2"}) // 通过 t.AssertNI(3, map[int]string{1: "v1", 2: "v2"}) // 通过 t.AssertNI([]string{"k3", "k4"}, map[string]string{"k1": "v1", "k2": "v2"}) // 通过 }) ``` ### 对`Map`类型包含的断言支持 #### `AssertIN` 对 `Map` 的支持 ```go package main import ( "testing" "github.com/gogf/gf/v2/test/gtest" ) func TestAssertIN_Map(t *testing.T) { gtest.C(t, func(t *gtest.T) { // 检查单个键是否存在于 Map 中 t.AssertIN("k1", map[string]string{"k1": "v1", "k2": "v2"}) // 支持不同类型的键 t.AssertIN(1, map[int]string{1: "v1", 2: "v2"}) // 检查多个键是否都存在于 Map 中 t.AssertIN([]string{"k1", "k2"}, map[string]string{"k1": "v1", "k2": "v2"}) }) } ``` #### `AssertNI` 对 `Map` 的支持 ```go package main import ( "testing" "github.com/gogf/gf/v2/test/gtest" ) func TestAssertNI_Map(t *testing.T) { gtest.C(t, func(t *gtest.T) { // 检查单个键是否不存在于 Map 中 t.AssertNI("k3", map[string]string{"k1": "v1", "k2": "v2"}) // 支持不同类型的键 t.AssertNI(3, map[int]string{1: "v1", 2: "v2"}) // 检查多个键是否都不存在于 Map 中 t.AssertNI([]string{"k3", "k4"}, map[string]string{"k1": "v1", "k2": "v2"}) }) } ``` ## 其他断言方法 ### `AssertNil` `AssertNil` 方法用于检查 `value` 是否为 nil。 ```go func AssertNil(value interface{}) ``` 示例: ```go gtest.C(t, func(t *gtest.T) { var p *int = nil t.AssertNil(nil) // 通过 t.AssertNil(p) // 通过 }) ``` ## 辅助方法 ### `DataPath` `DataPath` 方法用于获取当前包的测试数据路径,仅用于单元测试用例。 ```go func DataPath(names ...string) string ``` 示例: ```go gtest.C(t, func(t *gtest.T) { path := gtest.DataPath("test.txt") // 返回当前包testdata目录下的test.txt文件路径 }) ``` ### `DataContent` `DataContent` 方法用于获取指定测试数据路径的文件内容。 ```go func DataContent(names ...string) string ``` 示例: ```go gtest.C(t, func(t *gtest.T) { content := gtest.DataContent("test.txt") // 返回当前包testdata目录下的test.txt文件内容 }) ``` ## 使用示例 例如 `gstr` 模块其中一个单元测试用例: ```go package gstr_test import ( "github.com/gogf/gf/v2/test/gtest" "github.com/gogf/gf/v2/text/gstr" "testing" ) func Test_Trim(t *testing.T) { gtest.C(t, func(t *gtest.T) { t.Assert(gstr.Trim(" 123456\n "), "123456") t.Assert(gstr.Trim("#123456#;", "#;"), "123456") }) } ``` 也可以这样使用: ```go package gstr_test import ( . "github.com/gogf/gf/v2/test/gtest" "github.com/gogf/gf/v2/text/gstr" "testing" ) func Test_Trim(t *testing.T) { C(t, func(t *T) { Assert(gstr.Trim(" 123456\n "), "123456") Assert(gstr.Trim("#123456#;", "#;"), "123456") }) } ``` 一个单元测试用例可以包含多个 `C`,一个 `C` 也可以执行多个断言。 断言成功时直接PASS,但是如果断言失败,会输出如下类似的错误信息,并终止当前单元测试用例的继续执行(不会终止后续的其他单元测试用例)。 ```text === RUN Test_Trim [ASSERT] EXPECT 123456#; == 123456 1. /Users/john/Workspace/Go/GOPATH/src/github.com/gogf/gf/v2/text/gstr/gstr_z_unit_trim_test.go:20 2. /Users/john/Workspace/Go/GOPATH/src/github.com/gogf/gf/v2/text/gstr/gstr_z_unit_trim_test.go:18 --- FAIL: Test_Trim (0.00s) FAIL ``` # 错误处理-gerror 错误处理功能由 `gerror` 模块实现,具体请参考 [错误处理](../core/gerror.md) 章节。 # 错误码接口-gcode 请参考核心组件章节: [错误处理-错误码接口](../core/gerror-code-interface.md) # 调试功能-gdebug `goframe` 框架提供了丰富的调试功能,由 `gdebug` 组件实现。 **注意** 所谓的「调试」方法大多数和开发环境有一定关系,包含堆栈和调用链信息分析,并且性能往往不是特别高。 **使用方式:** ```go import "github.com/gogf/gf/v2/debug/gdebug" ``` **接口文档:** [https://pkg.go.dev/github.com/gogf/gf/v2/debug/gdebug](https://pkg.go.dev/github.com/gogf/gf/v2/debug/gdebug) **方法列表:** ```go func BinVersion() string func BinVersionMd5() string func Caller(skip ...int) (function string, path string, line int) func CallerDirectory() string func CallerFileLine() string func CallerFileLineShort() string func CallerFilePath() string func CallerFunction() string func CallerPackage() string func CallerWithFilter(filter string, skip ...int) (function string, path string, line int) func FuncName(f interface{}) string func FuncPath(f interface{}) string func GoroutineId() int func PrintStack(skip ...int) func Stack(skip ...int) string func StackWithFilter(filter string, skip ...int) string func StackWithFilters(filters []string, skip ...int) string func TestDataPath(names ...string) string ``` > 熟悉 `PHP` 的同学可能比较好理解,这里某些方法其实和 `PHP` 的部分 [魔术常量](https://www.php.net/manual/en/language.constants.predefined.php) 功能一致。 `CallerDirectory` 对应 `__DIR__`, `CallerFilePath` 对应 `__FILE__`, `CallerFunction` 对应 `__FUNCTION__`。 # NoSQL Redis ## 基本介绍 `Redis` 客户端由 `gredis` 组件实现,底层采用了链接池设计。 **提示** 为了保证通用性和扩展性, `gredis` 组件采用了 **命令通道** 的方式执行 `Redis` 操作。当不知道命令通道的参数如何传递时,可以参考终端命令行的参数传递。也就是说,所有的操作都和命令行的参数传递保持一致。 **使用方式**: 安装: ```bash go get -u github.com/gogf/gf/contrib/nosql/redis/v2 ``` 引用: ```go import ( _ "github.com/gogf/gf/contrib/nosql/redis/v2" // other imported packages. ) ``` **接口文档**: - [https://pkg.go.dev/github.com/gogf/gf/v2/database/gredis](https://pkg.go.dev/github.com/gogf/gf/v2/database/gredis) - [https://github.com/gogf/gf/tree/master/contrib/nosql/redis](https://github.com/gogf/gf/tree/master/contrib/nosql/redis) **简要介绍:** `gredis` 使用了连接池来进行 `Redis` 连接管理,通过 `Config` 配置对象或者 `Set*` 方法可以对连接池的属性进行管理,通过 `Stats` 方法可以获取连接池的统计信息。 `gredis` 使用接口化的设计来解耦对 `redis` 的底层依赖,通过社区组件的方式实现了 `100+` 项常用方法,并且提供了分组的方式来管理接口。 **注意** `gredis.Redis` 客户端对象提供了一个 `Close` 方法,该方法用于关闭 `Redis` 客户端(同时关闭客户端的连接池),而不是连接对象,开发者基本不会用到,非高级玩家请不要使用。 ## 组件特性 `gredis` 具有以下显著特性: - 使用简便,功能强大 - 统一配置组件进行配置 - 提供 `100+` 项常用方法社区组件实现 - 支持单实例及集群化操作 - 支持 `Redis` 服务所有特性 - 支持 `OpenTelemetry` 可观测性 - 支持单例对象、也支持动态创建对象 - 接口化设计,很高的灵活性和扩展性 # Redis-配置管理 `gredis` 组件支持两种方式来管理 `redis` 配置和获取 `redis` 对象,一种是通过 **配置组件+单例对象** 的方式;一种是模块化通过 **配置管理方法** 及对象创建方法。 ## 配置文件(推荐) 绝大部分情况下推荐使用 `g.Redis` 单例方式来操作 `redis`。因此同样推荐使用配置文件来管理 `Redis` 配置,在 `config.yaml` 中的配置示例如下: ### 单实例配置 ``` # Redis 配置示例 redis: # 单实例配置示例1 default: address: 127.0.0.1:6379 db: 1 # 单实例配置示例2 cache: address: 127.0.0.1:6379 db: 1 pass: 123456 idleTimeout: 600 ``` 其中的 `default` 和 `cache` 分别表示配置分组名称,我们在程序中可以通过该名称获取对应配置的 `redis` 单例对象。不传递分组名称时,默认使用 `redis.default` 配置分组项)来获取对应配置的 `redis` 客户端单例对象。 ### 集群化配置 ``` # Redis 配置示例 redis: # 集群模式配置方法 default: address: 127.0.0.1:6379,127.0.0.1:6370 db: 1 ``` ### 配置项说明 | 配置项名称 | 是否必须 | 默认值 | 说明 | | --- | --- | --- | --- | | `address` | 是 | - | 格式: `地址:端口`
    支持 `Redis` 单实例模式和集群模式配置,使用 `,` 分割多个地址。例如:
    `192.168.1.1:6379, 192.168.1.2:6379` | | `db` | 否 | `0` | 数据库索引 | | `user` | 否 | `-` | 访问授权用户 | | `pass` | 否 | `-` | 访问授权密码 | | `minIdle` | 否 | `0` | 允许闲置的最小连接数 | | `maxIdle` | 否 | `10` | 允许闲置的最大连接数( `0` 表示不限制) | | `maxActive` | 否 | `100` | 最大连接数量限制( `0` 表示不限制) | | `idleTimeout` | 否 | `10` | 连接最大空闲时间,使用时间字符串例如 `30s/1m/1d` | | `maxConnLifetime` | 否 | `30` | 连接最长存活时间,使用时间字符串例如 `30s/1m/1d` | | `waitTimeout` | 否 | `0` | 等待连接池连接的超时时间,使用时间字符串例如 `30s/1m/1d` | | `dialTimeout` | 否 | `0` | `TCP` 连接的超时时间,使用时间字符串例如 `30s/1m/1d` | | `readTimeout` | 否 | `0` | `TCP` 的 `Read` 操作超时时间,使用时间字符串例如 `30s/1m/1d` | | `writeTimeout` | 否 | `0` | `TCP` 的 `Write` 操作超时时间,使用时间字符串例如 `30s/1m/1d` | | `masterName` | 否 | `-` | 哨兵模式下使用, 设置 `MasterName` | | `tls` | 否 | `false` | 是否使用 `TLS` 认证 | | `tlsSkipVerify` | 否 | `false` | 通过 `TLS` 连接时,是否禁用服务器名称验证 | | `cluster` | 否 | `false` | 是否强制设置为集群工作模式。当 `address` 是单个endpoint的集群时,系统会自动判定为单实例模式,这时需要设置此项为 `true`。 | | `protocol` | 否 | `3` | 设置与 `Redis Server` 通信的 `RESP` 协议版本。 | | `sentinelUsername` | 否 | | `Sentinel` 模式下的账号 | | `sentinelPassword` | 否 | | `Sentinel` 模式下的密码 | 使用示例: `config.yaml` ``` # Redis 配置示例 redis: # 单实例配置示例1 default: address: 127.0.0.1:6379 db: 1 pass: "password" # 在此配置密码, 没有可去掉 ``` ```go package main import ( "fmt" _ "github.com/gogf/gf/contrib/nosql/redis/v2" "github.com/gogf/gf/v2/frame/g" "github.com/gogf/gf/v2/os/gctx" ) func main() { var ctx = gctx.New() _, err := g.Redis().Set(ctx, "key", "value") if err != nil { g.Log().Fatal(ctx, err) } value, err := g.Redis().Get(ctx, "key") if err != nil { g.Log().Fatal(ctx, err) } fmt.Println(value.String()) } ``` 执行后,输出结果为: ```value ``` ## 配置方法(高级) 由于 `GoFrame` 是模块化的框架,除了可以通过耦合且便捷的 `g` 模块来自动解析配置文件并获得单例对象之外,也支持有能力的开发者模块化使用 `gredis` 包。 `gredis` 提供了全局的分组配置功能,相关配置管理方法如下: ```go func SetConfig(config Config, name ...string) func SetConfigByMap(m map[string]interface{}, name ...string) error func GetConfig(name ...string) (config Config, ok bool) func RemoveConfig(name ...string) func ClearConfig() ``` 使用示例: ```go package main import ( "fmt" _ "github.com/gogf/gf/contrib/nosql/redis/v2" "github.com/gogf/gf/v2/database/gredis" "github.com/gogf/gf/v2/frame/g" "github.com/gogf/gf/v2/os/gctx" ) var ( config = gredis.Config{ Address: "127.0.0.1:6379", Db: 1, Pass: "password", } group = "cache" ctx = gctx.New() ) func main() { gredis.SetConfig(&config, group) _, err := g.Redis(group).Set(ctx, "key", "value") if err != nil { g.Log().Fatal(ctx, err) } value, err := g.Redis(group).Get(ctx, "key") if err != nil { g.Log().Fatal(ctx, err) } fmt.Println(value.String()) } ``` # Redis-使用示例 ## `Set/Get` 操作 ```go package main import ( "fmt" _ "github.com/gogf/gf/contrib/nosql/redis/v2" "github.com/gogf/gf/v2/frame/g" "github.com/gogf/gf/v2/os/gctx" ) func main() { var ctx = gctx.New() _, err := g.Redis().Set(ctx, "key", "value") if err != nil { g.Log().Fatal(ctx, err) } value, err := g.Redis().Get(ctx, "key") if err != nil { g.Log().Fatal(ctx, err) } fmt.Println(value.String()) } ``` 执行后,终端输出: ```bash  value ``` ## `SetEx` 操作 ```go package main import ( "fmt" "time" _ "github.com/gogf/gf/contrib/nosql/redis/v2" "github.com/gogf/gf/v2/frame/g" "github.com/gogf/gf/v2/os/gctx" ) func main() { var ctx = gctx.New() err := g.Redis().SetEX(ctx, "key", "value", 1) if err != nil { g.Log().Fatal(ctx, err) } value, err := g.Redis().Get(ctx, "key") if err != nil { g.Log().Fatal(ctx, err) } fmt.Println(value.IsNil()) fmt.Println(value.String()) time.Sleep(time.Second) value, err = g.Redis().Get(ctx, "key") if err != nil { g.Log().Fatal(ctx, err) } fmt.Println(value.IsNil()) fmt.Println(value.Val()) } ``` 执行后,终端输出: ```bash false value true ``` ## `HSet/HGetAll` 操作 ```go package main import ( "fmt" _ "github.com/gogf/gf/contrib/nosql/redis/v2" "github.com/gogf/gf/v2/frame/g" "github.com/gogf/gf/v2/os/gctx" ) func main() { var ( ctx = gctx.New() key = "key" ) _, err := g.Redis().HSet(ctx, key, g.Map{ "id": 1, "name": "john", "score": 100, }) if err != nil { g.Log().Fatal(ctx, err) } // retrieve hash map value, err := g.Redis().HGetAll(ctx, key) if err != nil { g.Log().Fatal(ctx, err) } fmt.Println(value.Map()) // scan to struct type User struct { Id uint64 Name string Score float64 } var user *User if err = value.Scan(&user); err != nil { g.Log().Fatal(ctx, err) } g.Dump(user) } ``` 执行后,终端输出: ```bash map[id:1 name:john score:100] { Id: 1, Name: "john", Score: 100, } ``` ## `HMSet/HMGet` 操作 ```go package main import ( _ "github.com/gogf/gf/contrib/nosql/redis/v2" "github.com/gogf/gf/v2/frame/g" "github.com/gogf/gf/v2/os/gctx" ) func main() { var ( ctx = gctx.New() key = "key" ) err := g.Redis().HMSet(ctx, key, g.Map{ "id": 1, "name": "john", "score": 100, }) if err != nil { g.Log().Fatal(ctx, err) } // retrieve hash map values, err := g.Redis().HMGet(ctx, key, "id", "name") if err != nil { g.Log().Fatal(ctx, err) } g.Dump(values.Strings()) } ``` 执行后,终端输出: ```bash [ "1", "john", ] ``` `As per Redis 4.0.0, HMSET is considered deprecated. Please use HSET in new code. ` 根据 `Redis 4.0.0`, `HMSET` 被视为已弃用。请在新代码中使用 `HSET`。 # Redis-命令交互 ## `Do` 方法 `Do` 是通用的命令交互方法,执行同步指令,通过向 `Redis Server` 发送对应的 `Redis API` 命令,来使用 `Redis Server` 的服务。 `Do` 方法最大的特点是使用 `Redis` 命令与服务端交互,因此扩展很强,没有提供 `Redis` 操作方法的其他命令都可以通过 `Do` 方法来实现。使用示例: ```go package main import ( "fmt" "github.com/gogf/gf/v2/frame/g" "github.com/gogf/gf/v2/os/gctx" ) func main() { var ( ctx = gctx.New() ) v, _ := g.Redis().Do(ctx, "SET", "k", "v") fmt.Println(v.String()) } ``` ## 自动序列化/反序列化 当给定的参数为 `map`, `slice`, `struct` 时, `gredis` 内部支持自动对其使用 `json` 序列化,并且读取数据时可使用 `gvar.Var` 的转换功能实现反序列化。 ### `map` 存取 ```go package main import ( "fmt" "github.com/gogf/gf/v2/container/gvar" "github.com/gogf/gf/v2/frame/g" "github.com/gogf/gf/v2/os/gctx" ) func main() { var ( ctx = gctx.New() err error result *gvar.Var key = "user" data = g.Map{ "id": 10000, "name": "john", } ) _, err = g.Redis().Do(ctx, "SET", key, data) if err != nil { panic(err) } result, err = g.Redis().Do(ctx,"GET", key) if err != nil { panic(err) } fmt.Println(result.Map()) } ``` ### `struct` 存取 ```go package main import ( "fmt" "github.com/gogf/gf/v2/container/gvar" "github.com/gogf/gf/v2/frame/g" "github.com/gogf/gf/v2/os/gctx" ) func main() { type User struct { Id int Name string } var ( ctx = gctx.New() err error result *gvar.Var key = "user" user = g.Map{ "id": 10000, "name": "john", } ) _, err = g.Redis().Do(ctx, "SET", key, user) if err != nil { panic(err) } result, err = g.Redis().Do(ctx, "GET", key) if err != nil { panic(err) } var user2 *User if err = result.Struct(&user2); err != nil { panic(err) } fmt.Println(user2.Id, user2.Name) } ``` # Redis-高级特性 ## `Conn` 对象 如果需要实现长链接性的 `Redis` 操作(例如订阅发布),那么我们可以使用 `Conn` 方法从连接池中获取一个连接对象,随后使用该连接对象进行操作。但需要注意的是,该连接对象不再使用时,应当显式调用 `Close` 方法进行关闭(丢回连接池)。 **注意** 由于该 `Conn` 是个连接对象,注意该对象存在连接超时的限制,超时和服务端配置有关。 ### 订阅/发布 我们可以通过 `Redis` 的 `Conn` 实现订阅/发布模式。 ```go package main import ( "fmt" _ "github.com/gogf/gf/contrib/nosql/redis/v2" "github.com/gogf/gf/v2/frame/g" "github.com/gogf/gf/v2/os/gctx" ) func main() { var ( ctx = gctx.New() channel = "channel" ) conn, _ := g.Redis().Conn(ctx) defer conn.Close(ctx) _, err := conn.Subscribe(ctx, channel) if err != nil { g.Log().Fatal(ctx, err) } for { msg, err := conn.ReceiveMessage(ctx) if err != nil { g.Log().Fatal(ctx, err) } fmt.Println(msg.Payload) } } ``` 执行后,程序将阻塞等待获取数据。 另外打开一个终端通过 `redis-cli` 命令进入 `Redis Server`,发布一条消息: ```bash $ redis-cli 127.0.0.1:6379> publish channel test (integer) 1 127.0.0.1:6379> ``` 随后程序终端立即打印出从 `Redis Server` 获取的数据: ```bash test ``` ## 原始客户端 `v2.9.4+` `Client` 方法返回底层的 `Redis` 客户端实例,提供对原始 `Redis` 客户端的访问,用于执行标准 `Redis` 接口未覆盖的高级操作。 ### 方法说明 ```go func (r *Redis) Client() gredis.RedisRawClient ``` 该方法返回原始的 `Redis` 客户端,通过类型断言可以将其转换为 `goredis.UniversalClient`,从而访问更多高级功能,如管道(Pipeline)操作、事务等。 ### 使用示例 以下示例展示如何使用原始客户端进行管道操作: ```go import ( "context" "errors" goredis "github.com/redis/go-redis/v9" ) func ExampleUsage(ctx context.Context, redis *gredis.Redis) error { // 获取原始客户端 client := redis.Client() // 类型断言为 UniversalClient universalClient, ok := client.(goredis.UniversalClient) if !ok { return errors.New("failed to assert to UniversalClient") } // 使用管道进行批量操作 pipe := universalClient.Pipeline() pipe.Set(ctx, "key1", "value1", 0) pipe.Set(ctx, "key2", "value2", 0) pipe.Get(ctx, "key1") // 执行管道中的所有命令 results, err := pipe.Exec(ctx) if err != nil { return err } // 处理结果 for _, result := range results { fmt.Println(result.Val()) } return nil } ``` ### 适用场景 - **批量操作**:使用管道(Pipeline)提高批量操作的性能 - **事务操作**:执行需要原子性保证的多个命令 - **Lua脚本**:执行自定义的 Lua 脚本 - **高级特性**:访问 go-redis 库的其他高级功能 **提示** 原始客户端的使用需要了解 `github.com/redis/go-redis/v9` 库的相关 API,建议在需要进行高级操作时才使用此方法。 # Redis-接口化设计 `gredis` 采用接口化设计,具有强大的灵活性和扩展性。 ## 接口定义 [https://pkg.go.dev/github.com/gogf/gf/v2/database/gredis#Adapter](https://pkg.go.dev/github.com/gogf/gf/v2/database/gredis#Adapter) ## 相关方法 ```go // SetAdapter sets custom adapter for current redis client. func (r *Redis) SetAdapter(adapter Adapter) // GetAdapter returns the adapter that is set in current redis client. func (r *Redis) GetAdapter() Adapter ``` ## 自实现Redis Adapter 框架社区组件提供了 `Redis Adapter` 的默认实现,如果开发者需要自实现 `Redis Adapter` 或者想要覆盖其中的某一些方法,可以基于该实现来扩展。 我们来看一个例子,在该例子中,我们实现一个自定义的 `Redis Adapter`,并且覆盖它的 `Do` 底层方法。为简化示例,我们这里在自实现的 `Do` 方法中打印一条日志即可,后续逻辑仍然走社区 `Redis Adapter` 的实现。 ```go package main import ( "context" "fmt" "github.com/gogf/gf/contrib/nosql/redis/v2" "github.com/gogf/gf/v2/container/gvar" "github.com/gogf/gf/v2/database/gredis" "github.com/gogf/gf/v2/frame/g" "github.com/gogf/gf/v2/os/gctx" ) var ( ctx = gctx.New() group = "cache" config = gredis.Config{ Address: "127.0.0.1:6379", Db: 1, } ) // MyRedis description type MyRedis struct { *redis.Redis } // Do implements and overwrites the underlying function Do from Adapter. func (r *MyRedis) Do(ctx context.Context, command string, args ...interface{}) (*gvar.Var, error) { fmt.Println("MyRedis Do:", command, args) return r.Redis.Do(ctx, command, args...) } func main() { gredis.RegisterAdapterFunc(func(config *gredis.Config) gredis.Adapter { r := &MyRedis{redis.New(config)} r.AdapterOperation = r // This is necessary. return r }) gredis.SetConfig(&config, group) _, err := g.Redis(group).Set(ctx, "key", "value") if err != nil { g.Log().Fatal(ctx, err) } value, err := g.Redis(group).Get(ctx, "key") if err != nil { g.Log().Fatal(ctx, err) } fmt.Println(value.String()) } ``` 执行后,终端输出: ``` MyRedis Do: Set [key value] MyRedis Do: Get [key] value ``` # 框架设计 设计思想是 GoFrame 框架的灵魂,同时对于使用者来讲,是不可或缺的内功心法。 授人予鱼不如授人以渔,比起技巧性的框架、组件使用,我们想尽可能地告诉大家为什么这样、为什么那样。 `GoFrame` 有其独特的设计思想,理解了 `GoFrame` 的设计思想,就理解了 `GoFrame` 的全部。 # 模块化设计 本章节我们先讲一讲在软件设计中,模块化的一些设计和复用原则,然后再介绍 `GoFrame` 框架的模块化设计,以便于大家更好地了解 `GoFrame` 框架模块化设计的思想。 ## 一、什么是模块 **模块** 也称作 **组件**,是软件系统中可复用的功能逻辑封装单位。在不同的软件架构层次,模块的概念会有些不太一样。在开发框架层面,模块是某一类功能逻辑的最小封装单位。在 `Golang` 代码层面中,我们也可以将 `package` 称作模块。 ## 二、模块化的目标 软件进行模块化设计的目的,是为了使得软件功能逻辑尽可能的 **解耦** 和 **复用**,终极目标也是为了保证软件开发维护的效率和质量。 ## 三、模块复用原则 ### `REP` 复用/发布等同原则 **复用/发布等同原则**( `Reuse/Release Equivalency Principle`):软件复用的最小粒度应等同于其发布的最小粒度。 直白地说,就是要复用一段代码就把它抽成模块。 ### `CCP` 共同闭包原则 **共同闭包原则**( `Common Closure Principle`):为了相同目的而同时修改的类,应该放在同一个模块中。 对大部分应用程序而言, **可维护性** 的重要性远远大于 **可复用性**,由同一个原因引起的代码修改,最好在同一个模块中,如果分散在多个模块中,那么开发、提交、部署的成本都会上升。 ### `CRP` 共同复用原则 **共同复用原则**( `Common Reuse Principle`):不要强迫一个模块依赖它不需要的东西。 相信一定有这种经历,集成了模块A,但模块A依赖了模块B、C。即使模块B、C 完全用不到,也不得不集成进来。这是因只用到了模块A的部分能力,模块A中额外的能力带来了额外的依赖。如果遵循共同复用原则,需要把A拆分,只保留要用的部分。 ### 复用原则竞争关系 `REP`、 `CCP`、 `CRP` 三个原则之间存在 **彼此竞争** 的关系。 `REP` 和 `CCP` 是 **黏合性原则**,它们会让模块变得更大,而 `CRP` 原则是 **排除性原则**,它会让模块变小。遵守 `REP`、 `CCP` 而忽略 `CRP`,就会依赖了太多没有用到的模块和类,而这些模块或类的变动会导致自己的模块进行太多不必要的发布;遵守 `REP`、 `CRP` 而忽略 `CCP`,因为模块拆分的太细了,一个需求变更可能要改 `n` 个模块,带来的成本也是巨大的。 图2\. 模块复用原则竞争关系张力图 优秀的架构师应该能在上述三角形张力区域中定位一个最适合目前研发团队状态的位置,例如在项目早期, `CCP` 比 `REP` 更重要,随着项目的发展,这个最合适的位置也要不停调整。 ## 四、框架模块设计 经过前面关于模块设计原则和复用原则的介绍,我们应该对模块开发和管理这块的原则有了大概的了解,那么我们接着介绍框架的模块化设计就比较容易理解了。 ### 单仓库包设计 根据 `REP` 原则我们了解到,一个可复用的模块是支持独立版本管理的,单仓库包设计也正是如此。 `Golang` 中很多这样的单仓库包,一个包就是一个独立的模块。单仓库包根据 `CRP` 原则可以再进一步的细化解耦拆分。我们来举个例子,在开发复杂的业务项目场景下,常见的包依赖情况,类似于这样的: ```go module business go 1.16 require ( business.com/golang/strings v1.0.0 business.com/golang/config v1.15.0 business.com/golang/container v1.1.0 business.com/golang/encoding v1.2.0 business.com/golang/files v1.2.1 business.com/golang/cache v1.7.3 business.com/framework/utils v1.30.1 github.com/pkg/errors v0.9.0 github.com/goorm/orm v1.2.1 github.com/goredis/redis v1.7.4 github.com/gokafka/kafka v0.1.0 github.com/gometrics/metrics v0.3.5 github.com/gotracing/tracing v0.8.2 github.com/gohttp/http v1.18.1 github.com/google/grpc v1.16.1 github.com/smith/env v1.0.2 github.com/htbj/command v1.1.1 github.com/kmlevel1/pool v1.1.4 github.com/anolog/logging v1.16.2 github.com/bgses123/session v1.5.1 github.com/gomytmp/template v1.3.4 github.com/govalidation/validate v1.19.2 github.com/yetme1/goi18n v0.10.0 github.com/convman/convert v1.20.0 github.com/google/uuid v1.1.2 // ... ) ``` 示例中的模块依赖,都是一些通用模块,大部分业务项目都会涉及到。模块地址是便于演示而写的随意地址,并不一定真实存在。 使用 `Golang` 开发过复杂一点的业务项目的小伙伴们,对于这样的场景大家一定不会陌生。一个正常的软件企业,往往至少有数百个这样的项目,真实的模块依赖关系比这里的例子更加复杂。在 `Golang` 项目开发中,对于模块依赖的维护性挑战是比较大的,我们往往会遇到一些痛点,主要的几点: - 实现相同功能逻辑的模块较多,选择成本增加 - 项目依赖的模块过多,项目整体的稳定性会受到影响 - 项目依赖的模块过多,项目无从下手是否应当升级这些模块版本 - 模块分散设计,不成体系,难以统一。具体请参考章节: [统一框架设计](./unified.md) 现身说法举例。 本厂的自研模块有数十个,这些模块已经被频繁使用遍布到数百个业务项目中。有一次,我们提交了对几个模块的 `bug fix`,其中有两个还是比较重要的 `bug`,紧接着,我们要求所有业务项目全部升级一下对应模块的版本号,并且这些版本号填写得务必小心。当然,这肯定也不是唯一的一次,随后相同的场景各位同学可以自行脑补。 我们也可以选择,不去主动推进所有业务项目升级模块,只要项目还没有触发这些 `bug`,那么就等着业务项目踩到了坑再由项目组自行去升级。领导如果听到这种解决方案......各位同学再自行脑补一下和谐的场景。 其实这种问题主要的原因,还是来源于模块的不稳定,模块也是需要不停迭代改进的。项目使用到这些模块,那么就与这些模块建立了 **耦合关系**,耦合模块的变化,必然会影响到依赖的相关项目。越底层的基础模块,顶层模块则对其依赖的越多,影响面也就越大。那是不是只要模块稳定了,就不会存在这样的问题了呢?风险依旧是存在的。 `Golang` 标准库大家觉得算稳定吧,但是它也是在不断的迭代改进过程中,也是不断有 `bug` 出现,只是大家幸运没踩上去而已,风险相对较低。 好的软件设计,并不是一成不变,而是能够做到快速响应变化,根据变化快速改进完善。模块的设计和管理,亦是如此。寻求能够快速改进模块逻辑、有效维护模块依赖的方案,比编写更加稳定的功能模块,更加高效和务实。 ### 模块聚合设计 `GoFrame` 的模块化管理思想更偏重于 `CCP` 原则,看重 **可维护性** 比 **可复用性** 更多。由于 `GoFrame` 是基于 **开发框架** 层面的出发点考虑,因此整体框架的设计不是单点设计的,而是自顶向下设计的。前面有提到,越底层的基础模块,顶层模块则对其依赖的越多,影响面也就越大。因此,框架将一些 **通用性的核心模块** 进行统一维护,这样做的目的是使得这些模块共同形成闭包,保证基础模块的稳定性,并通过统一的版本管理,提高开发效率和可维护性,降低接入和维护成本。 站在 `GoFrame` 框架模块化设计的角度,前面例子中的依赖情况应当变成以下的样子: ```go module business go 1.16 require ( github.com/gogf/gf v1.16.0 github.com/goorm/orm v1.15.1 github.com/goredis/redis v1.7.4 github.com/gokafka/kafka v0.1.0 github.com/google/grpc v1.16.1 // ... ) ``` `GoFrame` 只维护一些通用性的核心模块,其他非通用核心模块或者稳定性较高的模块,依旧建议使用单仓库包的形式进行依赖引入,正如 `REP` 和 `CRP` 模块复用原则倡导的那样。在这种设计模式下: - 框架核心维护较全面的通用基础模块,降低基础模块选择成本 - 我们只需要维护一个统一的框架版本,而不是数十个模块版本 - 我们只需要了解一个框架的内容变化,而不是数十个模块的内容变化 - 升级的时候只需要升级一个框架版本,而不是数十个模块版本的升级 - 减轻开发人员的心智负担,提高模块可维护性,更容易保证各业务项目的模块版本一致性 ## 五、常见问题解答 ### 1、虽然每一个模块都按照低耦合设计,模块虽然可以选择性引入,但在使用时也得全量下载完整框架代码 这对编译型开发语言来说并不是问题, **文件层面的源文件下载与模块之间的逻辑耦合没有直接关系**。这个问题的根因,主要是来源于我们对 **编译型语言** 和 **解释型语言** 理解的思路不一样。在互联网时代,解释型语言大行其道,而Golang语言在这个时代广受欢迎的同时却又独树旗帜。 - **编译型语言**:(以静态编译为例)往往以 `main` 包为入口,编译器会自动分析源码并将所有逻辑依赖模块中对应的资源进行编译处理,最终生成为静态二进制文件进行发布,自身源文件以及依赖模块(逻辑依赖)的源文件只在编译阶段使用,源码文件并不会直接用于发布,如:C/C++、Golang、Rust等。 - **解释型语言**:往往会将自身源文件(或中间码)以及依赖模块的源文件(或中间码)全部进行打包发布,例如:PHP、Java、NodeJS、Python等。这个时候,依赖模块的源码大小对于项目发布来说影响会比较大。并且,打包时候的模块依赖处理并不会检查"逻辑依赖",只要依赖配置文件中存在指定模块,那么该模块都会被共同打包发布。假如模块中有10万个函数,即使其中只有一个函数被使用到,该模块所有函数将被共同打包发布。因为解释型语言在代码发布前并没有"编译-汇编-链接"等阶段,只能在运行时对源码及模块依赖做完整解析处理。特别是PHP/Java转Go的同学,这一块的思维需要转变适应。 ### 2、框架中任一模块的版本变更都会引起框架版本的发布,框架的发布频次是否会变高? 当然,框架的模块设计也会充分考虑稳定性因素,仅会将一些 **通用性的核心模块** 按照 `CCP` 进行管理,并不会包含特定业务的逻辑封装,因为涉及到特定业务的功能逻辑实现将会为框架模块带来更多的不稳定变化。 在保证一定的稳定性前提下,模块的版本发布按照框架统一的迭代开发计划进行,除了必要的 `hot fix` 之外,版本发布设置有固定的时间窗口,以保证框架核心的稳定性。因此,框架通过模块聚合的方式进行版本管理,不仅没有增加框架的版本发布频次,反而降低了框架的版本发布频次,使得框架中的模块版本更加稳定。 ### 3、框架聚合并维护通用性的核心模块,通用性的核心模块定义是什么? 首先,它们是 **基础模块**,往往位于模块依赖链的最底层,这部分的模块变化对项目稳定性影响最大。 其次,绝大部分项目(二八定律)都会依赖的通用性基础模块,可以称作核心模块。 最后,这部分模块不包含具体的 **业务封装实现**。反例如:微信公众号/小程序、CMS/CRM、区块链等相关模块都是具体业务实现封装。 **提示** 关于模块通用性的评估无法完全准确,框架为保证核心精简会尽可能持保守态度,并且会根据实际情况在未来的迭代中逐步做调整。 以下为可供参考的模块分层: 模块分层参考 **业务实现模块**:特定业务项目逻辑实现,这里包含业务项目进一步的代码分层。 **通用业务模块**:可复用的业务逻辑封装,例如微信公众号/小程序、CMS/CRM、区块链等相关业务逻辑封装模块。 **通用基础模块**:标准库不提供或者基于标准库封装扩展的基础模块,例如:配置、校验、缓存、ORM、I18N等等。 **标准基础模块**:Golang标准库。 ### 4、框架中包含的模块多,人的精力有限,我觉得每个模块肯定没有github上其他单包的项目更好 一件事情做得少并不就能做得"好",这两个逻辑之间没有必然关系。 ### 5、框架中包含的模块多,我觉得每个模块的性能往往就不会高 哈哈。 # 统一框架设计 软件行业和建筑行业比较像,如果说我们的产品是一栋高楼大厦,那么程序代码就是建筑高楼的砖坯(我们每天的工作就像是在不停"搬砖")。如果说软件架构是高屋建瓴,那么程序代码是软件架构能够准确落地的关键构成。 程序代码如此重要,那么开发框架的重要性不言而喻。开发框架着力于代码层面,解决通用性的技术问题,目的是使得开发人员能够将精力关注于业务本身、辅助软件架构能够快速响应业务变化、提高软件的整体开发和维护效率。 本章节我们主要介绍统一开发框架建设的意义和必要性。 > 在开始本章节之前,建议先了解一下框架的: [模块化设计](./modular.md),部分灵感和同感来源于: [小马的经验分享](https://goframe.org/articles/framework-comparison-goframe-beego-iris-gin) ## 一、技术体系化 体系化更关注的是框架整体战斗力,而不是每个模块本身 框架体系化设计的显著特点: - 体系完善、组件丰富 - 规范统一、风格一致 - 统一抽象、设计严密 - 执行高效,无重复逻辑 我们这里的体系化是指微观层面的代码开发框架自顶向下统一设计,使得整个框架的设计思想是一体的,而不是分散的。从技术上,解决一个具体的问题比较简单,开发一个特定的模块也比较容易,但是如何将共性的问题进行抽象沉淀、将各个独立的模块按照统一的设计思想组织协调,并产生强大的综合战斗力却不是一件简单的事情。这要求框架的设计师具备一定的技术底蕴、经验沉淀、格局和前瞻性,而不是眼界只关注于单个模块本身。 例如,我们即使没有开发过框架,但是应该或多或少使用过,知道一个框架至少会包含哪些模块。当我们需要写日志的时候,我们知道这种组件框架必定提供,那么会去框架中寻找并从官网获得使用帮助。当我们需要WebServer、数据库访问、模板引擎等等能力的时候,我们也可以预料得到,这种组件框架必定会有提供,那么也会去框架中寻找并从官网获得使用帮助。 再例如,我们在使用框架中各种模块的时候,虽然各个模块都是低耦合设计,按需引入使用,但是发现她们的配置管理方式都是一致的,都是结构化的配置管理对象,通过相同的配置管理模块,通过固定的配置项到配置对象属性映射规则,通过 `Get`/ `Set` 开头的方法进行读取和设置(所有组件的参数获取和设置也是 `Get`/ `Set` 开头的方法),全局的环境变量/启动参数设置也是类同的。这使得开发者能够快速认知到框架行为,做到快速接入、降低学习成本的目的。 再例如,在框架层面有一个非常棒的特性 \- 全错误堆栈特性,框架所有组件的 `error` 返回均带有错误堆栈,便于顶层业务在出错时通过错误堆栈快速定位问题。目前 `Golang` 开发语言下仅 `GoFrame` 框架有此能力。 以上只是举的几点简单示例,如果感兴趣,可以从框架中发现更多有趣的点。 最后,我们可以想一想,为什么我们潜意识中能够认知到框架的行为、框架能够提供极高的便捷性和极低的接入成本、框架模块在"高内聚,低耦合"的设计思想下却整体有着非常高的组织协调性。为什么会造成这样的现象?其实这就是框架采用体系化设计,还是"东拼西凑"封装 的差异。 **提示** 举个比较贴切的比喻: `GoFrame` 是一支纪律性、凝聚力和战斗力极强的"正规军",而不是"东拼西凑"的"散兵游勇"。 ## 二、开发规范化 代码层面也是需要有一系列的开发规范,如基本的 代码结构、分层模型、封装设计等,具体请参考: [工程开发设计(重点)](./project.md)。统一的框架设计,将会使得所有的业务项目按照统一的代码设计进行编码,形成统一的开发规范。此外,框架的开发工具链也会使得开发规范更容易快速推广和落地实施: [开发工具](../cli.md)。 ## 三、组件统一化 > 这里的统一化有两层概念: > > - 多个相同功能组件统一为一个组件。 > - 多个不同功能组件统一到框架管理。 另一个痛点就是开发组件的百花齐放: - 实现相同功能逻辑的模块较多,选择成本增加 - 项目依赖的模块过多,项目整体的稳定性会受到影响 - 项目依赖的模块过多,项目无从下手是否应当升级这些模块版本 - 项目依赖的模块过多,容易出现不同模块依赖同一第三方模块不同版本,引发版本兼容问题 - 各个模块孤立设计,单独看待每个模块可替换性很高,开发体系难以建立,开发规范难以统一 统一的开发框架才能将各个模块" **各自为政**"的状态收归到" **统一管理**": - 框架自顶向下设计,形成体系化、统一化的模块设计,更有利于开发规范化的实施 - 框架核心维护较全面的通用基础模块,降低基础模块选择成本 - 我们只需要维护一个统一的框架版本,而不是数十个模块版本 - 我们只需要了解一个框架的内容变化,而不是数十个模块的内容变化 - 升级的时候只需要升级一个框架版本,而不是数十个模块版本的升级 - 统一的模块化设计可以减少不必要的逻辑实现,提高模块性能及易用性 - 减轻开发人员的心智负担,提高模块可维护性,更容易保证各业务项目的模块版本一致性 ## 四、版本一致性 版本一致性问题主要来源于项目依赖的模块过多、版本过多,造成版本很难以统一维护和升级。开发框架将模块统一化管理后,更容易保证项目模块的版本一致性。但是需要注意的是,这种一致性不是强一致性,只是降低了模块及版本的维护复杂度,但是不一致性的问题依然存在。关于痛点和改进我们在之前的章节也有介绍过,同时也可以参考章节: [模块化设计](./modular.md)。这里不再赘述。 > 行业内也有一些 **版本强一致性** 的代码管理方案,例如采用 `Monorepo` **大仓** 的代码管理方式,却各有利弊,可自行了解,这里不赘述。 ## 五、解决方案沉淀 基于统一的开发框架,更容易形成解决方案沉淀,企业与社区形成良性循环。解决方案的沉淀优先采用工具以及代码形式,而不是文档。 ## 六、避免资源浪费 当每个团队都在试图自己创造轮子时,不仅无法形成统一的开发规范,而且会出现非常多的资源浪费。 > 这在 `Golang` 语言流行的初期,或者一个初创公司技术体系不完善的初期,现象比较明显。 让项目组把精力更多的投入到业务中,相信这是大多数技术公司的共识。使用统一的开发架构,可以把共性的技术问题提炼出来,并形成通用的解决方案。避免每个项目都独自去解决遇到的各种各样的技术难题,有效的把精力释放出来。 # 全链路跟踪设计 可观测性是很重要的,目前已经有比较不错的 `OpenTelemetry` 标准,各个第三方组件以及厂商都按照这套标准库来暴露和对接观测数据将会更加简便。我们本次聊到的,是链路跟踪,它是可观测性中很重要的一项指标。 **提示** `GoFrame` 框架具有很高的前瞻性,在 `OpenTelemetry` 标准草稿阶段便开始持续关注,并在发布 `OTEL` 标准发布了 `alpha` 版本时,框架便开始支持。目前 `OTEL` 的 `Golang` 实现已经稳定。 `GoFrame` 是目前众多"框架"中对 `OTEL` 标准及实现支持得最好的框架,使用 `GoFrame` 框架即隐式自带链路跟踪特性。并且可观测性也是框架未来发展的重点特性。 ## 一、项目实践的痛点 在项目实践中,实现链路跟踪往往会遇到以下常见的痛点。 ### 1、组件没有严格执行标准 虽然已经有 `OTEL` 标准,但是第三方组件没有严格执行。例如,在日志和 `ORM` 组件中,没有严格约束传递 `ctx` 上下文变量。 ### 2、第三方组件杂乱无章 业务项目拼凑使用的第三方组件众多,有的组件没有实现链路跟踪支持,更别提 `OTEL` 标准。例如,命令管理、配置管理、缓存管理、数据校验、定时任务等等常用组件,表象就是没有提供 `ctx` 上下文变量传递。当使用到的组件不支持链路传递时,链路信息便丢失。 ### 3、业务逻辑易丢失链路 业务项目中,链路跟踪没有检测手段,也就是写着写着,链路跟踪不小心就弄丢了。例如在链路传递中新建 `ctx` 或者传递 `nil` 的 `ctx`。当战略设计在战术实施时遭遇到了猪队友,遇到问题需要定位时就GG。特别是在 `toB` 业务中,遇到工单限时会很抓狂。 ## 二、框架全链路跟踪 ### 1、统一框架 由于 `GoFrame` 是一款工程完备的基础框架,提供了项目所需的通用核心基础组件,因此可以非常方便统一地的基础组件实现链路跟踪标准。 ### 2、标准落地 在框架的核心组件中均增加了对 `ctx` 上下文变量的支持,并严格执行 `OTEL` 标准,保障标准的顺利落地。 ### 3、日志支持 在工程实践中,日志对于链路跟踪来说是非常重要的组件,在大部分的业务场景中,我们需要通过链路跟踪和日志内容来排查和定位具体问题。`GoFrame` 框架的日志组件也支持并且严格执行 `OTEL` 标准,因此只要使用框架的日志组件,那么也会自动打印链路相关信息。 ### 4、规范检测工具 框架通过开发工具提供工程规范检测功能,可以自动检测出业务项目中的链路丢失问题,进一步推进 `OTEL` 标准落地,保障项目质量。 ### 5、链路传递支持 链路的传递也需要统一组件。目前比较常见的协议是 `HTTP/GRPC` 协议,因此框架也提供了 `HTTP Client/Server` 以及 `GRPC Client/Server` 组件来保障链路的传递。并且为提高介入性和易用性,屏蔽复杂的底层功能细节,这种链路传递是底层 **隐式实现** 的,使用者对此事完全无感知的。 # 全错误堆栈设计 ## 一、项目错误处理痛点 我们在业务项目中,经常会遇到以下痛点。 ### 1、缺少统一错误处理方案,代码中随处可见的日志打桩 为了方便接口出错时定位问题,代码中随处可见的日志打桩,并将其看做是一件理所当然的事,然而毫无章法的庞大日志量除了提高维护的工作量,却通常难以达到它该有的目的。 ### 2、请求执行报错后缺少错误堆栈,难以快速定位问题 如下,当底层出现 `error` 级别的错误时,在顶层看到的就一个错误信息,请问如何排查? 一张真实的现网案例排查截图 ### 3、第三方组件执行返回的错误,本身不带有堆栈信息 不仅仅是第三方组件,连标准库所有方法返回的 `error` 都不带有堆栈,这对业务层统一错误处理造成了很大的挑战。几乎所有业务层代码调用返回的错误,都需要自行使用类似于 `Wrap` 方法再包裹一层,以便于业务层自己可以实现错误堆栈返回。这样的维护成本比较大,几乎只能靠 `CodeReview` 来人肉保障,一不小心可能会漏掉 `Wrap` 处理。 ### 4、错误组件多样,自身项目往往还想当然再封装一层 错误处理的第三方组件也比较多,如何选择?甚至业务项目往往也想自己再封装一层,进一步提高错误处理组件的维护成本。 ## 二、框架全错误堆栈设计 ### 1、统一错误组件 `GoFrame` 框架提供了业内功能最强大的错误处理组件,并且该组件也是框架内部广泛使用的错误组件,降低业务团队的选择成本。 ### 2、统一错误处理方案 `GoFrame` 框架提供了强大的工程设计规范,其中包含了必要的统一的错误处理方案。按照统一框架的工程设计,一些通用性的痛点已通过组件、工具的方式得以解决,使得业务团队能够将精力聚焦于业务本身,开发工作将会事半功倍。 在统一的错误处理方案下,项目中所有的方法调用将会以 `error` 返回值作为执行成功与否的依据。如果 `error` 不为 `nil` 时,及时返回,并将其层层往上传递,在最顶层统一做错误处理。并且,在框架的关键组件已经提供了默认的错误处理逻辑。 ### 3、全组件支持堆栈错误 `GoFrame` 框架 **所有基础组件** 的 `error` 返回对象均带有错误堆栈。 `GoFrame` 框架 **所有基础组件** 的 `error` 返回对象均带有错误堆栈。 `GoFrame` 框架 **所有基础组件** 的 `error` 返回对象均带有错误堆栈。 重要的事情说三遍。 这是一件很难做到的事情,因为框架提供的组件几乎能够覆盖了大部分业务项目的所有需求,但是框架确实做到了。虽然框架在这块投入的成本比较大(单独一个版本来实现了这个特性),但却是前期一次性投入、长期收益的事情。这也就意味着,如果业务项目在使用统一的 `GoFrame` 基础框架下,错误处理将会更加简便,错误堆栈丢失的风险得到了极大的降低,项目将会更加稳健、易于快速排错。 ### 4、关键组件支持错误堆栈打印 在框架的关键组件中,提供了对错误堆栈打印的 **默认处理**,以提高易用性,简化使用者负担。这些关键的组件是程序的入口,如 `HTTP/GRPC Server`、 `Command` 命令行。 # 接口化与泛型设计 ## 一、基本介绍 接口化是更高层次的抽象。框架组件的设计尽可能使用了接口化,而不是尽可能提供具体实现。接口化设计的最大的好处,是允许使用者自定义实现,来替换组件底层的接口层,以实现很强的灵活性和扩展性。 ## 二、组件接口化 `GoFrame` 框架的核心组件均采用了接口化设计,举个例子,如下图展示了部分组件的接口化实现概览: 大部分的组件,使用了 `Adapter` 作为自身接口层的名字,通过 `SetAdapter` 方法来设置当前的接口实现,通过 `GetAdapter` 方法获取当前组件的接口实现对象。此外,为了提高易用性,组件都会提供一些默认的 `Adapter` 实现,可供使用者选择。拿 `gsession` 组件举例: 底层接口采用了 `Storage` 进行定义,并提供了 `File/Memory/Redis/RedisHashTable` 四种实现供选择,默认实现为 `File`。 ## 三、接口化与泛型 组件的接口化设计可扩展性是很高的,但是在具体落地的时候需要结合泛型来实现更灵活的使用。同样拿 `gsession` 组件举例,参数的返回均采用了泛型,在业务使用时根据需要再转换为对应的数据类型。 ### 提高参数灵活性、简化使用复杂度 在不使用泛型的情况下,我们的接口要么提供各种类型的方法、要么使用 `interface{}` 类型返回,使用的复杂度都比较高。统一通过泛型的数据类型返回,使得参数类型更加灵活,极大地降低了使用复杂度。 泛型支持转换为各种类型: 根据业务场景需要转换为对应的数据类型。类型转换使用了框架统一的类型转换组件,底层会优先使用断言进行类型识别,以保证转换的效率。 ### 统一使用方式、屏蔽底层影响 针对于一些复杂类型的接口化场景,接口的底层实现上可能会存在外部存储的情况、更会产生序列化/反序列化操作,可能会改变/丢失数据类型。使用泛型将能够通过统一的使用方式屏蔽底层实现的影响。例如以下示例,无论底层 `Session` 实现如何变化,上层使用均通过泛型的 `Scan` 方法转换为目标对象。 ## 四、注意事项 虽然框架提供了泛型设计,但是并不推荐在业务中广泛使用泛型。业务层的数据结构设计,包括接口和业务模型数据结构,应当是准确的、确定的。 # 隐式与显式初始化 我们知道在程序启动时会需要执行一些"初始化"的逻辑操作,例如: `Server` 配置、各种数据库( `MySQL`、 `Redis`、 `Kafka` 等)配置、业务对象配置等等。绝大多数场景下,我们有两种初始化方式:隐式初始化和显式初始化。 ## 一、隐式初始化 **注意** 特别注意:在 `Golang v1.21` 版本后, `init` 的初始化执行顺序发生了变化,并且可能会引起依赖 `init` 来执行初始化逻辑的包的问题。因此,不建议在 `init` 中执行 **复杂的初始化逻辑**, **建议是通过显式调用方式来实现模块复杂的初始化逻辑**。 隐式初始化一般通过包初始化方法 `init` 执行初始化。需要注意的是,如果初始化逻辑存在错误的可能,由于 `init` 方法的错误无法被上层捕获,初始化出错时往往直接终止程序启动。例如: 隐式初始化出错时往往直接终止程序启动 隐式初始化的好处是不需要手动调用初始化方法,对于开发者隐藏了初始化细节,因此开发者没有心智负担。但是缺点也同样如此,开发者不知道初始化细节,一旦出现错误时,很难快速定位错误原因。因此使用隐式初始化时,往往要求在初始化出错时将详细的错误以及堆栈信息打印出来便于错误定位。 `GoFrame` 框架的很多模块都采用了隐式初始化,隐藏模块的初始化细节,减少开发者的心智负担。例如: `GoFrame` 中的模块普遍存在隐式初始化设计 使用 `GoFrame` 框架的 `main` 包隐式 `imports` 关于包 `init` 方法的初始化流程: ## 二、显式初始化 显式初始化要求开发在程序启动时,如在 `main` 或者 `boot` 模块中,调用特定的方法来执行初始化操作。一般来说,基础组件的初始化往往采用隐式初始化多一些,因为对于使用者来讲并不关心底层基础模块的初始化逻辑,而业务模块的初始化大多数会采用显式初始化。例如: 在 `boot` 包中按照顺序执行显式初始化 在 `main` 包调用 `boot.Boot()` 方法执行初始化 ## 三、如何选择 在业务场景下,非特殊必要,我们建议大家采用 **显式初始化** 的方式,以保证更好的可维护性。 # Golang枚举值管理 ## Go实现枚举值 `Go` 语言并没有提供 `enum` 的定义,我们可以使用 `const` 来模拟枚举类型,这也是 `Go` 语言中约定俗成的方式。 例如,在 `Kubernetes` 项目中,有大量的以常量形式定义的"枚举值": ```go // PodPhase is a label for the condition of a pod at the current time. type PodPhase string // These are the valid statuses of pods. const ( // PodPending means the pod has been accepted by the system, but one or more of the containers // has not been started. This includes time before being bound to a node, as well as time spent // pulling images onto the host. PodPending PodPhase = "Pending" // PodRunning means the pod has been bound to a node and all of the containers have been started. // At least one container is still running or is in the process of being restarted. PodRunning PodPhase = "Running" // PodSucceeded means that all containers in the pod have voluntarily terminated // with a container exit code of 0, and the system is not going to restart any of these containers. PodSucceeded PodPhase = "Succeeded" // PodFailed means that all containers in the pod have terminated, and at least one container has // terminated in a failure (exited with a non-zero exit code or was stopped by the system). PodFailed PodPhase = "Failed" // PodUnknown means that for some reason the state of the pod could not be obtained, typically due // to an error in communicating with the host of the pod. PodUnknown PodPhase = "Unknown" ) ``` ## 如何跨服务高效维护枚举值 如果只是项目内部使用枚举值比较简单,定义完了内部使用即可,但涉及到跨服务之间调用,或者前后端协作时,效率就比较低了。当服务需要给外部调用者展示接口能力时,往往需要生成 `API` 接口文档(或者接口定义文件,例如 `proto`),往往也需要根据接口文档(文件)生成调用的客户端 `SDK`。 如果是接口定义文件,例如 `proto`,往往可以直接查看源码来解决这个问题,问题不大。我们这里主要讨论的是接口文档维护枚举值的问题,特别是前后端协作时通过 `OpenAPI` 标准协议来维护枚举值的问题。这里我们提供了专门的工具来维护这些枚举值,具体请参考章节: [枚举维护-gen enums](../cli/gen-enums.md) # Context共享变量 `Context` 指的是标准库的 `context.Context`,是一个接口对象,常用于 **异步 `IO` 控制** 以及 **上下文流程变量的传递**。本文将要介绍的,是如何使用 `Context` 传递流程间共享变量。 在 `Go` 的执行流程中,特别是 `HTTP/RPC` 执行流程中,不存在」全局变量」获取请求参数的方式,只有将上下文 `Context` 变量传递到后续流程的方法中,而 `Context` 上下文变量即包含了所有需要传递的共享变量。并且该 `Context` 中的共享变量应当是事先约定的,并且往往存储为对象指针形式。 通过 `Context` 上下文共享变量非常简单,以下我们通过一个项目中的示例来展示如何在实战化项目中传递和使用通用的共享变量。 ## 一、结构定义 上下文对象中往往存储一些需要共享的变量,这些变量通常使用结构化的对象来存储,以方便维护。例如,我们在 `model` 定义一个上下文中的共享变量: ```go const ( // 上下文变量存储键名,前后端系统共享 ContextKey = "ContextKey" ) // 请求上下文结构 type Context struct { Session *ghttp.Session // 当前Session管理对象 User *ContextUser // 上下文用户信息 Data g.Map // 自定KV变量,业务模块根据需要设置,不固定 } // 请求上下文中的用户信息 type ContextUser struct { Id uint // 用户ID Passport string // 用户账号 Nickname string // 用户名称 Avatar string // 用户头像 } ``` 其中: 1. `model.ContextKey` 常量表示存储在 `context.Context` 上下文变量中的键名,该键名用于从传递的 `context.Context` 变量中存储/获取业务自定义的共享变量。 2. `model.Context` 结构体中的 `Session` 表示当前请求的 `Session` 对象,在 `GoFrame` 框架中每个 `HTTP` 请求对象中都会有一个空的 `Session` 对象,该对象采用了懒初始化设计,只有在真正执行读写操作时才会初始化。 3. `model.Context` 结构体中的 `User` 表示当前登录的用户基本信息,只有在用户登录后才有数据,否则是 `nil`。 4. `model.Context` 结构体中的 `Data` 属性用于存储自定义的 `KV` 变量,因此一般来说开发者无需再往 `context.Context` 上下文变量中增加自定义的键值对,而是直接使用 `model.` `Context` 对象的这个 `Data` 属性即可。详见后续介绍。 ## 二、逻辑封装 由于该上下文对象也是和业务逻辑相关的,因此我们需要通过 `service` 对象将上下文变量封装起来以方便其他模块使用。 ```go // 上下文管理服务 var Context = new(contextService) type contextService struct{} // 初始化上下文对象指针到上下文对象中,以便后续的请求流程中可以修改。 func (s *contextService) Init(r *ghttp.Request, customCtx *model.Context) { r.SetCtxVar(model.ContextKey, customCtx) } // 获得上下文变量,如果没有设置,那么返回nil func (s *contextService) Get(ctx context.Context) *model.Context { value := ctx.Value(model.ContextKey) if value == nil { return nil } if localCtx, ok := value.(*model.Context); ok { return localCtx } return nil } // 将上下文信息设置到上下文请求中,注意是完整覆盖 func (s *contextService) SetUser(ctx context.Context, ctxUser *model.ContextUser) { s.Get(ctx).User = ctxUser } ``` ## 三、上下文变量注入 上下文的变量必须在请求一开始便注入到请求流程中,以便于其他方法调用。在 `HTTP` 请求中我们可以使用 `GoFrame` 的中间件来实现。在 `GRPC` 请求中我们也可以使用拦截器来实现。在 `service` 层的 `middleware` 管理对象中,我们可以这样来定义: ```go // 自定义上下文对象 func (s *middlewareService) Ctx(r *ghttp.Request) { // 初始化,务必最开始执行 customCtx := &model.Context{ Session: r.Session, Data: make(g.Map), } service.Context.Init(r, customCtx) if userEntity := Session.GetUser(r.Context()); userEntity != nil { customCtx.User = &model.ContextUser{ Id: userEntity.Id, Passport: userEntity.Passport, Nickname: userEntity.Nickname, Avatar: userEntity.Avatar, } } // 将自定义的上下文对象传递到模板变量中使用 r.Assigns(g.Map{ "Context": customCtx, }) // 执行下一步请求逻辑 r.Middleware.Next() } ``` 该中间件初始化了用户执行流程共享的对象,并且存储到 `context.Context` 变量中的对象是指针类型 `*model.Context`。这样任何一个地方获取到这个指针,既可以获取到里面的数据,也能够直接修改里面的数据。 其中,如果 `Session` 中存在用户登录后的存储信息,那么也会将需要共享的用户基本信息写入到 `*model.Context` 中。 ## 四、上下文变量使用 ### 方法定义 约定俗成的,方法定义的第一个输入参数往往预留给 `context.Context` 类型参数使用,以便接受上下文变量,特别是 `service` 层的方法。例如: ```go // 执行用户登录 func (s *userService) Login(ctx context.Context, loginReq *define.UserServiceLoginReq) error { ... } // 查询内容列表 func (s *contentService) GetList(ctx context.Context, r *define.ContentServiceGetListReq) (*define.ContentServiceGetListRes, error) { ... } // 创建回复内容 func (s *replyService) Create(ctx context.Context, r *define.ReplyServiceCreateReq) error { ... } ``` 此外,约定俗成的,方法的最后一个返回参数往往是 `error` 类型。如果确定此方法内部永不会产生 `error`,那么可以忽略。 ### `Context` 对象获取 通过 `service` 中封装的以下方法,将 `context.Context` 上下文变量传递进去即可。 `context.Context` 上下文变量在 `GoFrame` 框架的 `HTTP` 请求中可以通过 `r.Context()` 方法获取,在 `GRPC` 请求中,编译生成的 `pb` 文件中执行方法的第一个参数即固定是 `context.Context`。 ```go service.Context.Get(ctx) ``` ### 自定义 `Key-Value` 通过以下方式设置/获取自定义的 `key-value` 键值对。 ```go // 设置自定义键值对 service.Context.Get(ctx).Data[key] = value ... // 获取自定义键值对 service.Context.Get(ctx).Data[key] ``` ## 五、注意事项 1. 上下文变量只传递必须的链路参数数据,不要什么参数都往里面塞。特别是一些方法参数传参的数据,别往里面塞,而应当显示传递方法参数。 2. 上下文变量仅用作运行时临时使用,不可持久化存储长期使用。例如将 `ctx` 序列化后存储到数据库,并再下一次请求中读取出来反序列化使用是错误做法。 # 工程开发设计 ## 基本介绍 由于模块化的设计方式, `GoFrame` 框架既可以当做工具库使用,也可以直接用于完整的开发业务项目。 考虑到大家使用框架大部分场景下是用于开发业务项目(微服务项目),因此框架也提供了工程开发的一些设计和实践,便于大家直接开箱即用。 ## 注意事项 工程开发设计是很复杂的"学问",不同的团队有不同的设计风格,甚至某些场景下还和团队 `leader` 喜好有较大关系。 框架官网提供的工程开发设计,是基于编写该章节的作者经验,以及通过 `Go` 实践的项目踩坑而来。她能帮助各位解决常见的工程开发难题,但我们不认为她能符合所有的开发团队的喜好。 查阅这里所有的设计章节,理解每一块设计本身的背景和思路,比直接使用框架组件和工具劳动更有价值。 # 代码分层设计 **提示** 代码分层的意义在于将程序逻辑进一步解耦,将层级之间的数据流和依赖关系设计为单向链路,使得系统架构更加灵活易扩展。 ## 一、基本介绍 `GoFrame` 作为一款工程化完备的基础开发框架,有其独特的框架设计理念,这一章节我们来介绍一下她的代码分层设计。对于服务端业务代码的分层设计模式中,我们比较常见的是 `MVC` 设计模式和三层架构设计模式( `3-Tier Architecture`)。 ## 二、 `MVC` 设计模式 我们先来回顾一下经典的 `MVC` 设计模式。 图1\. MVC设计模式 ### 简要介绍 `M` 代表模型( `Model`),表示业务规则封装。在 `MVC` 的三个部件中,模型拥有最多的处理任务。被模型返回的数据是中立的,模型与数据格式无关,这样一个模型能为多个视图提供数据,由于应用于模型的代码只需写一次就可以被多个视图重用,所以减少了代码的重复性。 `V` 代表视图( `View`),用户看到并与之交互的界面。比如由 `HTML` 元素组成的网页界面,或者软件的客户端界面。 `MVC` 的好处之一在于它能为应用程序处理很多不同的视图。在视图中其实没有真正的处理发生,它只是作为一种输出数据并允许用户操纵的方式。 `C` 代表控制器( `Controller`),接受用户的输入并调用模型和视图去完成用户的需求,控制器本身不输出任何东西和做任何处理。它只是接收请求并决定调用哪个模型构件去处理请求,然后再确定用哪个视图来显示返回的数据。 这种设计模式比较简单,比较合适于需要服务端渲染页面的业务场景,对于 `SEO` 来说也比较友好。但随着 `MVVM` 开发模式的兴起,以及前端技术的快速发展,特别是一些前端开发框架如 `Vue`、 `React`、 `Angular` 之类的项目出现,服务端的 `MVC` 设计模式使用场景变得越来越少。 ### 痛点描述 针对于业务逻辑并不是特别复杂的业务场景项目, `MVC` 还能游刃有余,但随着业务逻辑变得庞大复杂, `MVC` 设计模式的项目维护成本上升的问题变得越来越明显。特别是随着互联网项目微服务架构的发展, `MVC` 设计模式在大部分的互联网项目开发中变得越来越鸡肋。究其原因,主要的几点: - 视图展示与数据操作方式的进一步剥离,特别是移动端的发展,前端 `MVVM` 框架的发展,我们大多数场景下已不再需要服务端渲染 `View`。 - `MVC` 的代码分层设计模式其实粒度较粗: - `Model` 层级的代码既维护着数据,也封装着业务逻辑,随着业务逻辑变得越来越复杂,这一层功能逻辑会变得越来越臃肿不易维护。 - 对于团队管理来讲, `Controller` 和 `Model` 的职责边界比较模糊,对于开发人员写好代码的要求会比较高。概念看似简单,但是却难以适应工业化的软件生产。 ## 三、 `3-Tier Architecture` `GoFrame` 框架推荐的代码分层设计模式为三层架构设计( `3-Tier Architecture`),但在具体的实现中,对这种结构设计模式进行了一定的改进。三层架构设计能够很好地体现出软件设计"高内聚,低耦合"的设计思想。 图2\. 三层架构设计模式 传统的三层架构设计如上图,将项目代码划分了三层,每一层有其独自的职责边界。但在大多数的场景中,我们常看到的是以下的分层结构,将数据结构模型再进一步地抽离了出来统一维护。 图3\. 常见三层架构设计模型 ### 表示层 \- `UI` `User Interface` 位于三层构架的最上层,与用户直接接触,主要是 `B/S` 中的 `WEB` 页面,也可以是 `API` 接口。表示层的主要功能是实现系统数据的传入与输出,在此过程中不需要借助逻辑判断操作就可以将数据传送到 `BLL` 系统中进行数据处理,处理后会将处理结果反馈到表示层中。换句话说,表示层就是实现用户界面/ `API` 接口功能,将用户的需求传达和反馈,并用 `BLL` 或者是 `Model` 进行调试,保证用户体验。 ### 业务逻辑层 - `BLL` `Business Logic Layer` 的功能是对具体问题进行逻辑判断与执行操作,接收到表现层 `UI` 的用户指令后,会连接数据访问层 `DAL`,业务逻辑层在三层构架中位于表示层与数据层中间位置,同时也是表示层与数据层的桥梁,实现三层之间的数据连接和指令传达,可以对接收数据进行逻辑处理,实现数据的增删改查等功能,并将处理结果反馈到表示层 `UI` 中,实现软件功能。 ### 数据访问层 - `DAL` `Data Access Layer` 是数据库的主要操控系统,实现数据的增删改查等操作,并将操作结果反馈到业务逻辑层 `BLL`。在实际运行的过程中,数据访问层没有逻辑判断能力,为了实现代码编写的严谨性,提高代码阅读程度,一般软件开发人员会在该层中实现通用数据能力进行封装(例如通过 `ORM` 组件)来保证数据访问层 `DAL` 数据处理功能。 ### 模型定义层 - `Model` 模型定义也常用 `Entity` 实体对象来表示,主要用于数据库表的映射对象,在信息系统软件实际开发的过程中,要建立对象实例,将关系数据库表采用对象实体化的方式表现出来,辅助软件开发中对各个系统功能的控制与操作执行。建立实体类库,进而实现各个结构层的参数传输,提高代码的阅读性。从本质上看,实体类库主要服务于表示层、业务逻辑层以及数据访问层,在三层之间进行数据参数传输,强化数据表示的简约性。 需要注意区分的是,这里的 `Model` 和 `MVC` 设计模式中的 `Model` 虽然都是一个名字但是差别巨大,职责完全不同。 ### 三层架构设计与 `MVC` 由于 `MVC` 也是三层结构,因此有的同学也会将 `MVC` 笼统地归纳于三层架构设计中,从字面意义上来讲似乎也没什么问题。不过两者还是存在一定的区别。 图4\. 三层架构设计与MVC 可以看到,在三层架构设计中, `UI` 表示层即相当于 `MVC` 的 `View` 和 `Controller` 层,原本在 `MVC` 中这两层的逻辑应当是比较"轻量"的,因此被合并为一层进行统一管理也可以理解。比较重要的一点是,原本 `MVC` 中的 `Model` 被拆分为了 `BLL` 和 `DAL`,即将业务逻辑与数据访问进行分离,将原本臃肿的 `Model` 进行了进一步的解耦,有利于项目的更好维护。 **提示** 软件架构的演变过程,特别是互联网软件架构的演变过程,本质也就是将业务逻辑不断解耦的过程。 ## 四、更进一步了解 代码分层思想是最基础的工程设计入门,我们需要将分层思想进行落地,具体请参考: [工程目录设计](./project-structure.md) # 工程目录设计 工程目录设计是代码分层设计的进一步落地,建议先仔细阅读: [代码分层设计](./project-layer.md) **提示** 这是 `GoFrame` 框架针对 **业务项目** 的目录设计,主体的思想来源于三层架构,但在具体实现中,对其进行了一定的改进和细化使其更符合工程实践和时代进步。 ## 一、工程目录结构 `GoFrame` 业务项目基本目录结构如下(以 `Single Repo` 为例): ```text / ├── api ├── hack ├── internal │ ├── cmd │ ├── consts │ ├── controller │ ├── dao │ ├── model │ | ├── do │ │ └── entity │ └── service ├── manifest ├── resource ├── utility ├── go.mod └── main.go ``` **说明**:提示 框架的工程目录采用了 **通用化的设计**,以满足不同复杂程度业务项目的需求,但实际项目中可以根据项目需要 **适当增减默认的目录**。 | 目录/文件名称 | 说明 | 描述 | | ---------------- | -------- | ----------------------------------------------------------------------------------------- | | `api` | 对外接口 | 对外提供服务的输入/输出数据结构定义。考虑到版本管理需要,往往以 `api/xxx/v1...` 存在。 | | `hack` | 工具脚本 | 存放项目开发工具、脚本等内容。例如, `CLI` 工具的配置,各种 `shell/bat` 脚本等文件。 | | `internal` | 内部逻辑 | 业务逻辑存放目录。通过 `Golang internal` 特性对外部隐藏可见性。 | | `  - cmd` | 入口指令 | 命令行管理目录。可以管理维护多个命令行。 | | `  - consts` | 常量定义 | 项目所有常量定义。 | | `  - controller` | 接口处理 | 接收/解析用户输入参数的入口/接口层。 | | `  - dao` | 数据访问 | 数据访问对象,这是一层抽象对象,用于和底层数据库交互,仅包含最基础的 `CRUD` 方法 | | `  - model` | 结构模型 | 数据结构管理模块,管理数据实体对象,以及输入与输出数据结构定义。 | | `    - do` | 领域对象 | 用于 `dao` 数据操作中业务模型与实例模型转换,由工具维护,用户不能修改。 | | `    - entity` | 数据模型 | 数据模型是模型与数据集合的一对一关系,由工具维护,用户不能修改。 | | `  - service` | 业务实现 | 业务逻辑的具体实现和封装,如`service/user`用户模块、`service/order`订单模块等。 | | `manifest` | 交付清单 | 包含程序编译、部署、运行、配置的文件。常见内容如下: | | `  - config` | 配置管理 | 配置文件存放目录。 | | `  - docker` | 镜像文件 | `Docker` 镜像相关依赖文件,脚本文件等等。 | | `  - deploy` | 部署文件 | 部署相关的文件。默认提供了 `Kubernetes` 集群化部署的 `Yaml` 模板,通过 `kustomize` 管理。 | | `  - protobuf` | 协议文件 | `GRPC` 协议时使用的 `protobuf` 协议定义文件,协议文件编译后生成 `go` 文件到 `api` 目录。 | | `resource` | 静态资源 | 静态资源文件。这些文件往往可以通过 资源打包/镜像编译 的形式注入到发布文件中。 | | `go.mod` | 依赖管理 | 使用 `Go Module` 包管理的依赖描述文件。 | | `main.go` | 入口文件 | 程序入口文件。 | ### 对外接口 - `api` + `controller` 对外接口包含两部分:接口定义( `api`)+接口实现( `controller`)。 服务接口的职责类似于三层架构设计中的 `UI` 表示层,负责接收并响应客户端的输入与输出,包括对输入参数的过滤、转换、校验,对输出数据结构的维护,并调用 `service` 实现业务逻辑处理。 #### 接口定义 - `api` `api` 包用于与客户端约定的数据结构输入输出定义,往往与具体的业务场景强绑定。 #### 接口实现 - `controller` `controller` 用于接收 `api` 的输入,可以直接在 `controller` 中实现业务逻辑,或者调用一个或多个 `service` 包实现业务逻辑,将执行结果封装为约定的 `api` 输出数据结构。 ### 业务实现 - `service` 业务实现的职责类似于三层架构设计中的 `BLL` 业务逻辑层,负责具体业务逻辑的实现以及封装。在该目录下,具体的业务模块使用子目录独立封装维护,如下示例: ```bash service ├── user # 用户模块 ├── order # 订单模块 └── ... # 其他业务模块 ``` ### 结构模型 - `model` `model` 包的职责类似于三层架构中的 `Model` 模型定义层。模型定义代码层中仅包含全局的、公共的数据结构定义,供项目中所有的业务模块共同引用。 #### 数据模型 - `entity` 与数据集合绑定的程序数据结构定义,通常和数据表一一对应,字段类型与数据表字段类型一致。 #### 领域模型 - `do` 字段与数据表字段一一对应的程序数据结构定义,主要用于 `dao` 数据访问层中数据的输入输出定义,供 `service` 层调用使用。 #### 业务模型 - `model` 与业务相关的通用数据结构定义,供项目中所有的业务模块共同引用。 ### 数据访问 - `dao` `dao` 包的职责类似于三层架构中的 `DAL` 数据访问层,数据访问层负责所有的数据访问收口。 三层架构设计与框架代码分层映射关系 ## 二、请求分层流转 ### cmd `cmd` 层负责引导程序启动,显著的工作是初始化逻辑、注册路由对象、启动 `server` 监听、阻塞运行程序直至 `server` 退出。 ### api 上层 `server` 服务接收客户端请求,转换为 `api` 中定义的 `Req` 接收对象、执行请求参数到 `Req` 对象属性的类型转换、执行 `Req` 对象中绑定的基础校验并转交 `Req` 请求对象给 `controller` 层。 ### controller `controller` 层负责接收 `Req` 请求对象后做一些业务逻辑校验,可以直接在 `controller` 中实现业务逻辑,或者调用一个或多个 `service` 实现业务逻辑,将执行结果封装为约定的 `Res` 数据结构对象返回。 ### model `model` 层中维护了所有公共的业务模型数据结构。 ### service `service` 层的业务逻辑需要通过调用 `dao` 来实现数据的操作,调用 `dao` 时需要传递 `do` 数据结构对象,用于传递查询条件、输入数据。 ### dao `dao` 层通过框架的 `ORM` 抽象层组件与底层真实的数据库交互。 ## 三、常见问题解答 ### 框架是否支持常见的 `MVC` 开发模式 **当然。** 作为一款模块化设计的基础开发框架, `GoFrame` 不会局限代码设计模式,并且框架提供了非常强大的模板引擎核心组件,可快速用于 `MVC` 模式中常见的模板渲染开发。相比较 `MVC` 开发模式,在复杂业务场景中,我们更推荐使大家用三层架构设计模式。 ### 当 `api` 与 `model` 层存在重复数据结构时如何维护 在 `api` 中定义的数据结构是 **对外使用的**,与具体的业务场景绑定(如具体的页面交互逻辑、单一的接口功能),数据结构是由上层展示层前置决定的; `model` 中定义的数据结构是 **服务内部公共使用的**,并且 `model` 中的数据结构可以随意在内部修改而并不会影响对外 `api` 接口的兼容性。 **注意 `model` 中的数据结构不应该直接暴露给外部使用**,并且在框架的工程设计中刻意将 `model` 目录放到了 `internal` 目录下。也不应该在 `api` 层中对 `model` 中的数据结构进行 **别名类型定义** 供外部访问。一旦将 `model` 中的数据结构应用到了 `api` 层中,内部 `model` 数据结构的修改会直接影响到 `api` 接口的兼容性。 **如果两者出现重复的数据结构、常量、枚举类型等,建议将数据结构定义到 `api` 层中**。服务内部逻辑可以直接访问 `api` 层的数据结构。 `model` 层的数据结构也可以直接引用 `api` 层的数据结构,但是反之则不行。 我们来看一个示例,便于更好地理解: ### 如何清晰界定和管理 `service` 和 `controller` 的分层职责 `controller` 层处理 `Req/Res` 外部接口请求。负责接收、校验请求参数,可以直接在 `controller` 中实现业务处理逻辑,或者调用 **一个或多个** `service` 来实现业务逻辑处理,将执行结果封装为约定的 `api` 输出数据结构返回。 `service` 层处理 `Input/Output` 内部方法调用。负责内部 **可复用** 的业务逻辑封装,封装的方法粒度往往比较细。 **通常来讲,开发接口时只需要编写 `controller` 层中的接口实现业务逻辑即可,当存在重复代码逻辑时,再从各个 `controller` 接口实现逻辑中抽象沉淀到 `service` 层**。如果从 `controller` 层直接透传 `Req` 对象给 `service`,同时 `service` 直接返回 `Res` 数据结构对象,该方法也就与外部接口耦合,仅面向外部接口服务,难以复用,这样会增加技术债务成本。 ### 如何清晰界定和管理 `service` 和 `dao` 的分层职责 这是一个很经典的问题。 **痛点:** 常见的,开发者易把 **数据相关的业务逻辑实现** 封装到 `dao` 代码层中,而 `service` 代码层只是简单的 `dao` 调用,这么做的话会使得原本负责维护数据的 `dao` 层代码越来越繁重,反而业务逻辑 `service` 层代码显得比较轻。开发者存在困惑,我写的业务逻辑代码到底应该放到 `dao` 还是 `service` 中? 业务逻辑其实绝大部分时候都是对数据的 `CRUD` 处理,这样做会使得几乎所有的业务逻辑会逐步沉淀在 `dao` 层中,业务逻辑的改变其实会频繁对 `dao` 层的代码产生修改。例如:数据查询需求,在初期的时候可能只是简单的逻辑,目前代码放到 `dao` 好像也没问题,但是查询需求增加或变化变得复杂之后,那么必定会继续维护修改原有的 `dao` 代码,同时 `service` 代码也可能同时做更新。原本仅限于 `service` 层的业务逻辑代码职责与 `dao` 层代码职责模糊不清、耦合较重,原本只需要修改 `service` 代码的需求变成了同时修改 `service` + `dao`,使得项目中后期的开发维护成本大大增加。 **建议:** 我们的建议。 `dao` 层的代码应该尽量保证通用性,并且大部分场景下不需要增加额外方法,只需要使用一些通用的链式操作方法拼凑即可满足。业务逻辑、包括看似只是简单的数据操作的逻辑都应当封装到 `service` 中。 `service` 中包含多个业务模块,每个模块独自管理自己的 `dao` 对象。理想情况下, `service` 与 `service` 之间通过相互调用方法来实现数据通信,而不是随意去调用其他 `service` 模块的 `dao` 对象。 ### 为什么要使用 `internal` 目录包含业务代码 `internal` 目录是 `Golang` 语言专有的特性,防止同级目录外的其他目录引用其下面的内容。业务项目中存在该目录的目的,是避免若项目中存在多个子项目(特别是大仓管理模式时),多个项目之间无限制随意访问,造成难以避免的多项目不同包之间耦合。 # 结构化编程设计 ## 一、基本介绍 结构化编程,简单来理解,就是通过定义结构体传递和返回参数。 我们建议在必要的场景下使用结构化定义来管理输入/输出,尤其是在 `controller` 和 `service` 两层的代码设计中。 ### 1、 `controller` 非结构化痛点 - 难以确定接口输入/输出数据结构,大多数的场景是在代码中硬编码参数接收名称,易把名称写错造成不可预料的问题 - 接口参数往往只定义一个 `HttpRequest/HttpContext` 对象指针,执行结果直接写入到对象,难以确定接口是否成功/失败 - 参数接收、校验、转换处理工作繁琐 - 接口文档生成以及维护极为困难 ### 2、 `service` 非结构化痛点 - 当方法参数较多的时,定义丑陋,使用别扭 - 当方法参数数量、类型不太确定时,任意的参数变化都是非兼容的(例如新增参数),会引起较高的修改成本 - 方法参数注释不简便,以至于绝大部分业务项目都不会有方法参数注释 ## 二、结构化编程 ### 1、 `controller` 结构化改进 **结构化优点:** - 通过结构化管理接口输入/输出参数,参数接收不再需要硬编码参数名称,降低维护成本,避免参数名称硬编码错误问题 - 可以做到自动化的参数接收、转换、校验,提高生产力 - 可以规范化接口编写 - 使得接口管理能够像普通的函数管理那么方便,通过返回 `error` 来判断接口处理结果,并可以规范化统一错误机制 - 使得自动化的接口文档生成变为了可能,并保障了接口结构定义和接口文档同步维护 **结构化示例:** 结构定义: 方法使用: ### 2、 `service` 结构化改进 **结构化优点:** - 当方法参数较多的时,通过结构体优雅管理参数 - 当方法参数数量、类型不太确定时,参数的增加对方法调用来说都是兼容性的 - 对结构体属性的注释描述更加便捷,提高代码维护质量 **结构化示例:** ## 三、注意事项 - `service` 层的方法在使用结构化管理输入/输出参数时,结构体中任意参数都将会被看做非必需参数。因此需要根据业务场景合理评估可行性。 # 数据模型与业务模型 在之前的章节 [代码分层设计](./project-layer.md) 有提及到" **模型**"的概念。 这一章节,我们集中介绍一下在 `GoFrame` 中关于模型的定义以及对应的管理。 ## 一、数据模型 **数据模型** 又叫做 **实体模型**,主要是来自于底层持久化数据库的数据结构,例如: `MySQL`、 `Redis`、 `MongoDB`、 `Kafka` 等等。这部分数据结构是由第三方系统维护的,可以通过工具对其集合数据结构进行识别,并自动化生成对应的程序数据模型代码。这部分数据模型的代码位于 `/internal/model/entity` 目录下。开发者不需要手动在程序中维护数据模型,在 `GoFrame` 框架规范中,数据模型统一使用 `CLI` 工具统一维护,代码自动生成。 数据模型示例 ## 二、业务模型 业务模型主要包含两类: **接口输入/输出模型** 与 **业务输入/输出模型**。 ### 接口输入/输出模型 接口输入/输出模型用于系统/服务间的接口交互,定义在 `api` 接口层中,供工程项目所有的层级调用,例如 `controller, service, model` 中均可以调用 `api` 层的输入输出模型,但是 `api` 层仅用于与外部服务的接口交互,该模型中不能调用或者引用内部的模型如 `model` 模型。在 `GoFrame` 框架规范中,这部分输出输出模型名称以 `XxxReq` 和 `XxxRes` 格式命名。 接口输入模型示例 ### 业务输入/输出模型 **业务输入/输出模型** 用于服务 **内部** 模块/组件之间的方法调用交互,特别是 `controller->service` 或者 `service->service` 之间的调用。这部分模型定义在 `model` 模型层中。在 `GoFrame` 框架规范中,这部分输入输出模型名称通常以 `XxxInput` 和 `XxxOutput` 格式命名。 业务输入模型与业务输出模型示例 ### 特殊的业务模型 `DO` 在 `GoFrame` 有一类特殊的业务模型 `DO`,介于业务模型与数据模型之间,主要用于结合框架强大的 `ORM` 组件大大简便 `DAO` 数据访问操作。 DO主要用于DAO数据访问操作 ## 三、其他模型 还有内部私有的模型,用于模块内部调用,例如在 `service` 各个业务模块内部定义的模型,用于内部逻辑,不对外公开。 # 微服务大仓管理模式 本文旨在介绍 `GoFrame` 框架对微服务-单仓管理( `mono-repo`)模式的支持,指导开发者如何在微服务-单仓管理( `mono-repo`)模式下进行代码开发和分工协作。 ## 一、前置阅读 在开始本章节之前,建议先了解一下单体仓库( `monolith`)、微服务-多仓管理( `multi-repo`)、 微服务-单仓管理( `mono-repo`)的基本概念以及各自的优缺点: [单体仓库与多仓库都有哪些优势劣势,微服务选择哪种方案比较好?](https://johng.cn/management/monorepo-vs-multirepo) 代码仓库的管理约束并不属于框架职责的一部分, `GoFrame` 框架的脚手架本身也支持两种仓库项目初始化的命令 \- 单仓库( `mono-repo`)、多仓库( `monolith/multi-repo`),以满足不同团队的需求。具体选择哪种代码仓库管理模式由开发团队根据自身需求、场景、习惯来自行选择。 **提示** 为简化和清晰微服务-单仓管理( `mono-repo`)的描述,后续我们统一以 **大仓管理** 来指代微服务-单仓管理( `mono-repo`)模式。 ## 二、大仓管理 ### 1、仓库职责范围的划分 通过前置阅读的文章大家也知道,这个世界上没有银弹,大仓有优点也有缺点,其中最明显的缺点就是 **权限的管控** 以及 **仓库的膨胀**。为了更好地管理代码仓库,避免这两点缺陷带来更高的成本,我们建议尽可能减小大仓中的微服务规模。至于仓库中需要维护哪些微服务,需要根据服务之间的协作频率来决定。 #### 1)当团队内部的协作频率高于团队间协作频率时 - 典型的场景是针对 **非微服务化架构的产品**,可以将服务管理的权限职责按照各个业务团队进行划分。这样团队内部可以将分散的若干服务通过统一的代码仓库维护起来,充分利用大仓管理的优势,提高团队内部的开发和维护效率。 - 另外一种场景是业务的微服务数量本身不多(例如 `50` 个以内),这个时候也可以合并成一个大仓进行管理。需要注意,大仓管理的服务数量并不是由组织架构中的人员数量来决定的。 #### 2)当多个团队之间的多个服务协作频率非常高时 当业务的微服务数量比较多,并且各个服务之间的交互协作比较频繁,那么可以考虑将这些服务合并到大仓中进行管理,可以极大提高协作效率。这种情况大部分出现在微服务在同一产品线、跨团队但不跨中心或部门的情况下。但由于涉及跨多个团队的协作,这对人员的组织架构管理有一定要求,需要由一定权限的管理者来推动。 **提示** 微服务的管理并不仅是代码的组织管理,更是人员组织架构的管理。 ### 2、大仓下的微服务间如何协作 #### 1)代码可见性的管理 服务与服务之间能够暴露的仅仅是接口,也就是 `API`。各个服务内部的逻辑应当对外不可见。在 `Golang` 里面有很好的 `internal` 特性,正好可以满足可见性管理的要求。如下图大仓代码示例,在 `app` 目录下管理了若干的服务,每一个服务暴露了自身的 `api` 目录,供其他服务直接引用(提高服务间协作效率),但内部的业务逻辑包含在了 `internal` 目录下,对其他服务不可见(也就无法引用)。 #### 2)服务间接口的调用 协议文件单独维护到各自服务目录下,如果涉及到协议文件编译,那么编译的文件也存放到自身的服务目录下。调用端不需要单独再对目标服务的协议文件重新编译管理。以 `HTTP API` 的接口定义为例,调用端可以直接引用目标端服务的 `API` 接口定义:(以下截图中 `khaos-shark` 为调用端, `khaos-oss` 为服务端) 针对于微服务间的 `RPC` 接口调用也是同样的道理:(以下截图中 `user-api` 为调用端, `user-rpc` 为服务端) #### 3)兼容性的严格要求 通过以上介绍可以发现,通过大仓的代码管理,使得大仓中所有服务的版本保持一致,每当依赖的服务 `API` 更新时,调用端服务(使用的 `SDK`)也将自动得到更新。这就要求仓库中所有服务的接口设计, **必须严格保证兼容性**,否则接口间调用将会出现问题:轻者调用端服务编译失败需要调整代码,重者编译成功但运行时报错影响业务。此外,公共引用的大仓基础组件也会受到兼容性的影响。 保证兼容性代码设计的几个要点: - **不随意删减接口参数,修改参数名称、参数类型、参数校验逻辑。** - **当接口必须要进行非兼容更新时,应当使用接口版本号来管理(如 `v1, v2, v3...`)。** - **公共组件尽量使用稳定成熟的外部组件,如果是必要的自定义组件,需要保证对外暴露方法的兼容性。** _举个例子:一些很基础的功能,比如说 `json.Marshal&Unmarshal`,有的人封装了一些库/函数,但是后面的人可能都不知道这个库,也不太信任这个函数,就会又重新写一个...久而久之这些库/函数却又无人维护。_ ### 3、大仓下的微服务容器化支持 #### 1)镜像仓库的统一管理 分散的镜像仓库将会降低服务容器化的管理维护效率。为便于统一服务容器化管理,我们建议大仓下的服务使用统一的镜像仓库。镜像仓库的地址统一维护到各个服务下的工具配置文件中: #### 2)统一编译、提交指令 框架提供了常用的指令来实现程序的编译、镜像的编译、镜像的提交。 - `make build` 编译程序,生成二进制文件。 更多介绍请参考文档: [交叉编译-build](../cli/build.md) - `make image` 编译程序并编译镜像,生成 `Docker` 镜像。 通过 `make image TAG=xxx` 可以指定编译生成的镜像标签名称。 更多介绍请参考文档: [镜像编译-docker](../cli/docker.md) - `make image.push` 编译程序、编译镜像并推送镜像到已配置好的镜像仓库。 通过 `make image.push TAG=xxx` 可以指定编译生成的镜像标签名称。 #### 3)统一部署、调试指令 框架提供了常用的指令来实现 `Kubernetes` 集群的容器化部署,以及一体化编译部署的开发指令。 - `make deploy TAG=xxx` 部署当前服务到本地 `kubeconfig` 已连接的 `kubernetes` 集群中,其中的 `TAG` 用于指定 `deploy` 目录下的 `overlays` 目录。管理部署 `yaml` 文件使用的是行业内常用的 `kustomize` 工具,具体介绍文档请参考: [https://kubernetes.io/zh-cn/docs/tasks/manage-kubernetes-objects/kustomization/](https://kubernetes.io/zh-cn/docs/tasks/manage-kubernetes-objects/kustomization/) - `make image.push deploy TAG=xxx` 该命令是开发调试指令,用于一条指令 编译二进制文件、编译并推送 `Docker` 镜像、部署 `Kubernetes` 应用并重启应用。 ### 4、大仓下的框架其他指令 框架针对于项目工程管理提供了丰富的工具指令支持,这些指令往往需要再特定的服务目录下执行,例如 `./app/服务名称` #### 1) `make cli` 用于升级本地的框架 `CLI` 到最新稳定版本。 #### 2) `make up` 用于升级本地的框架到最新社区稳定版本。 更多介绍请参考文档: [框架升级-up](../cli/up.md) #### 3) `make dao` 用于生成 `DAO/Entity/DO` 代码文件。 更多介绍请参考文档: [数据规范-gen dao](../cli/gen-dao.md) #### 4) `make enums` 用于解析指定代码目录(默认为 `api` 目录)并自动生成 `enums` 加载代码。 更多介绍请参考文档: [枚举维护-gen enums](../cli/gen-enums.md) #### 5)更多指令 更多的指令支持请参考框架官网工具介绍章节: [开发工具](../cli.md) # DAO-组件痛点及改进 关于 `DAO` 数据访问对象设计其实是关于 `GoFrame` 框架工程化实践中比较重要一块设计。 `DAO` 设计结合 `GoFrame` 的 `ORM` 组件性能和易用性都很强,可以极大提高开发和维护效率。看完本章节内容之后,小伙伴们应该能够理解并体会到使用 `DAO` 数据库访问对象设计的优点。 **说明** 我每年都会来回重新审视这篇文章,看看是否可以删除一些地方。可是每次都倍感失望,因为这篇文章对当今现状仍旧适用。并且今年,我还新增了内容。 ## 一、现有 `ORM` 使用示例 ### 1、需要定义模型 用户基础表(仅作演示,真实的表有数十个字段) 医生信息表(仅作演示,真实的表有上百个字段) ### 2、 `GRPC` 接口实现示例 一个简单的 `GRPC` 查询信息接口。 一个简单的 `GRPC` 数据查询接口 ## 二、现有痛点描述 ### 1、必须要定义 `tag` 关联表结构与 `struct` 属性,无法做到自动映射 表字段与实体对象属性名称之间原本就有一定的关联规则,没有必要定义和维护大量的 `tag` 定义。 大量非必要的 `tag` 定义,用于指定数据表字段到实体对象属性映射 ### 2、不支持通过返回对象指定需要查询的字段 无法通过返回的对象数据结构指定查询字段,要么只能 `SELECT *`,要么只能通过额外的方法手动录入查询字段,效率很低下。 常见的 `SELECT *` 操作,无法根据接口对象指定查询字段 ### 3、无法对输入对象属性名称进行自动字段过滤 定义了输入与输出数据结构,输出的数据结构已经包含我们需要查询的字段名称。开发者输入定义的返回对象,期望在查询的时候仅查询我需要的字段名称,多余的属性则不会执行查询,自动过滤掉。 ### 4、需要创建中间查询结果对象执行赋值转换 查询结果不支持 `struct` 智能转换,需要额外定义一个中间 `model` 模型,再通过其他工具进行复制,效率低。 存在中间临时的模型对象,用于承接查询结果及返回结构对象赋值转换 ### 5、需要提前初始化返回对象,不管有无查询到数据 这种方式不仅不优雅,对性能也有影响,还对 `GC` 不太友好。期望查询到数据时再自动创建返回对象,没有查询到数据时什么都不要做。 需要预先初始化返回对象,不管有无查询到数据 ### 6、项目通篇使用底层裸 `DB` 对象操作,没有对象封装操作 大部分的 `Golang` 初学者似乎都倾向于使用一个全局的 `DB` 对象,在查询的时候通过 `DB` 对象生成特定表的 `Model` 对象再执行 `CRUD` 操作,这是一种面向过程的使用方式。这种方式并没有代码分层的设计可言, **使得数据操作和业务逻辑高度耦合**。 原始数据库对象操作方式,没有 `DAO` 封装 ### 7、随处可见的字符串硬编码,如表名和字段的硬编码 举个例子, `userId` 这个字段假如一不小心写成了 `UserId` 或者 `userid`,测试的时候如果没有完全覆盖到,在一定的条件下才触发查询操作,是不是会造成新的一场事故呢? 大量的字符串硬编码 ### 8、底层ORM引起太多的指针属性定义 指针属性对象为业务逻辑处理埋下隐患,开发者在代码逻辑中需要在指针与属性之间来回切换,特别是一些基础类型往往需要通过重新取值的方式传递参数。如果输入参数是 `interface{}` 类型,那么更容易引起 `BUG`。 `BUG` 示例,指针属性使用不当,引起地址比较逻辑错误。 同时也影响了业务模型结构体定义设计,对开发者造成了错误习惯引导(上层业务模型的指针属性往往是为了迎合底层数据表实体对象,方便数据传递)。 值得注意一个常见错误,就是将底层数据实体模型当做顶层业务模型使用。特别是在底层数据实体对象使用指针属性的场景下,该问题十分明显。 ### 9、可观测性的支持:Tracing、Metrics、Logging 数据库ORM作为业务项目最关键核心的组件,可观测性的支持至关重要。 ### 10、数据集合与代码数据实体结构不一致 当通过人工维护数据实体结构时,数据集合与代码数据实体结构往往会出现不一致的风险,开发和维护成本高。 ## 三、改进方案设计 1、查询结果对象无需特殊标签定义,全自动关联映射 2、支持根据指定对象自动识别查询字段,而不是全部 `SELECT *` 3、支持根据指定对象自动过滤不存在的字段内容 4、使用 `DAO` 对象封装代码设计,通过对象方式操作数据表 5、 `DAO` 对象将关联的表名及字段名进行封装,避免字符串硬编码 6、无需提前定义实体对象接受返回结果,无需创建中间实体对象用于接口返回对象的赋值转换 7、查询结果对象无需提前初始化,查询到数据时才会自动创建 8、内置支持 `OpenTelemetry` 标准,实现可观测性,极大提高维护效率、降低成本 9、支持 `SQL` 日志输出能力,支持开关功能 10、数据模型、数据操作、业务逻辑解耦,支持 `Dao` 及 `Model` 代码工具化自动生成,保证数据集合与代码数据结构一致,提高开发效率,便于规范落地 11、等等。 采用 `DAO` 设计改进后的代码示例 # DAO-工程痛点及改进 ## 一、基本介绍 我们都知道,开发业务项目离不开数据库操作组件的使用,数据库是绝大部分业务项目的核心,这也是"CRUD工程师"戏称的由来。业务项目在进行数据库操作时,比较 `low` 的方式是直接 `Open/New` 然后各种 `SQL` 字符串操作一把梭。稍微正常一点的项目可能会考虑物色或者自己封装一层ORM抽象,提高CRUD效率,降低数据操作风险。再严谨一点的项目可能会在项目工程管理上考虑下,再进一步增加 `DAO/DTO/VO` 之类的设计模式和概念。 信息时代,数据是十分重要的,数据操作是十分敏感的,因此 `GoFrame` 框架对于数据操作管理的工程化思路是严谨的。我们提供了必要的ORM抽象、必要的DAO封装、必要的工程化规范约束。同时,我们并不会采用八股文设计,而是依旧保持简便、灵活、易扩展的工程设计思路。 ## 二、工程化痛点 在一些严谨的业务项目中,已经有了 `ORM&DAO` 抽象、并且项目已有初步工程化设计的前提,依旧存在以下常见的痛点。 ### 1、数据集合与代码结构不同步 当在代码中手动维护数据集合对应的数据结构时,这个坑就算挖好了,就看后面谁掉进去了。 ### 2、数据模型与业务模型模糊不清 混淆了数据模型与业务的职责,并将数据模型与业务逻辑、接口定义形成了耦合,耦合越大,相关方法、接口维护的成本越高,对数据模型改动产生的风险也越大。常见痛点: - 在 `model` 中既有业务相关的数据结构(业务模型),又有数据集合对应的数据结构(数据模型),如何高效隔离和管理呢? - 在业务流程中,将数据模型当做业务流程的 **输入参数** 使用。甚至,将数据模型直接嵌入到API接口输入数据结构定义中(总是想法设法将数据模型用到业务模型中)。 ### 3、DAO层沉淀太多的业务逻辑封装 是否有一种感觉,只要是数据操作,都有理由往 `DAO` 里面丢? ### 4、将数据模型作为ORM/DAO操作的参数 有可能认为这么做是对的,但是不明确的数据结构都意味着成本和风险。任何的操作,都应当能够明确输入/输出,否则都是不严谨的,对待数据的操作尤其应当严谨。 ### 5、数据操作权限开放,项目任何地方都可以随意调用 数据的操作权限应当尽可能收口,如果过于开放那么当业务及人员复杂之后,项目的维护成本和风险都会曲线增加。 ### 6、从顶层业务到底层数据集合操作,通篇使用同一个数据结构 常见的问题,是设计一个大的结构体,例如数据模型(更有甚者,将属性全部设计为指针或者 `interface{}`),从顶层业务到底层数据操作层层透传,方法逻辑根据是否输入特定的属性来判断传参。会造成什么问题呢: - 方法参数定义不明确,不明确的定义意味着会增加额外的协作成本,额外的不明确风险 - 同一数据结构与多数方法形成耦合,数据结构的任一变动将会影响所有相关方法 - 相关方法无法充分复用(特别是 `service` 层的方法) ## 三、工程化改进 ### 1、自动化的数据模型管理 通过工具自动化实现数据集合到数据模型的代码生成,避免人工维护造成的不同步。 ### 2、数据与业务模型的隔离 将数据模型通过 `entity` 包维护,业务模型通过 `model` 包维护,通过不同的包职责来做区分。数据模型由工具化维护,业务模型根据业务场景由开发者定义和维护。 ### 3、自动化的DAO代码管理 通过工具自动化实现数据集合的 `DAO` 代码生成,提高生产效率。 `DAO` 中只有自动化生成的基础数据操作,不封装特定业务逻辑。 ### 4、DO数据转换模型的引入 避免数据模型直接被当做 `DAO` 参数使用,避免踩坑。 `GoFrame` 框架引入了 `DO` 包,在 `DAO` 操作时自动化转换为数据集合对应的数据结构,提高 `DAO` 操作的效率,降低操作风险。 ### 5、对数据操作权限进行收口 由于数据操作已经由 `DAO` 包进行了统一维护,可以将 `DAO` 包迁移到了对应 `service` 层业务模块的 `internal` 目录下,实现项目工程下仅仅只有 `service` 层的对应业务逻辑代码可以通过 `DAO` 执行数据操作。 这是一个较严格的使用限制,由开发者根据需要选择性使用,框架默认的项目模板以及工具没有对数据操作权限收口。可通过框架开发工具配置来实现生成 `dao` 代码到不同的 `service` 业务模块位置下。 # 开发工具 `GoFrame` 框架提供了功能强大的 `gf` 命令行开发辅助工具,是框架发展的一个重要组成部分,工具地址: - [https://github.com/gogf/gf/tree/master/cmd/gf](https://github.com/gogf/gf/tree/master/cmd/gf) 工具安装请参考仓库页面。工具安装成功后,可以通过 `gf` 或者 `gf -h` 查看所有支持的命令。复杂的命令可以通过 `gf COMMAND -h` 查看更详细的使用帮助信息,例如: `gf gen -h`。 ## 工具职责 1. 简化工程开发,提高开发效率 2. 支持框架工程设计规范准确落地 ## 注意事项 1. 部分命令需要先安装好 `Golang` 基础的开发环境,环境安装具体请参考 [环境安装](./install-go/index.md) 章节。 2. 最新的 `CLI` 工具版本会随着最新的框架版本走。 ## 配置支持 **工具的所有命令均同时支持命令行及配置文件配置参数,以提高易用性。当给定命令行参数时优先读取命令行参数,如果命令行参数不存在时,自动读取配置文件中对应的参数名称。** 配置文件路径优先查找当前目录下的 `hack` 目录( `hack/config.yaml`),其次按照框架默认的配置路径检索配置文件。框架默认的配置文件检索路径请参考章节: [配置管理-文件配置](./core/gcfg-file.md) 配置文件的格式示例: ```yaml # GoFrame CLI tool configuration. gfcli: gen: dao: - link: "mysql:root:12345678@tcp(127.0.0.1:3306)/test" tables: "user" removePrefix: "gf_" descriptionTag: true noModelComment: true docker: build: "-a amd64 -s linux -p temp" tagPrefixes: - ccr.ccs.tencentyun.com/xxx - hkccr.ccs.tencentyun.com/xxx - sgccr.ccs.tencentyun.com/xxx ``` 注意以上配置示例仅供参考,具体配置项请参考具体命令帮助。 ## 工具调试 当在工具的使用中遇到问题时,可以尝试打开工具的调试模式获得更详细的工具执行日志信息,打开工具调试模式可以通过 `debug` 命令行选项开启,例如: ```bash gf build main.go --debug ``` 由于 `gf` 工具也是使用 `GoFrame` 框架开发,因此调试信息的开启也是同框架方式一致,更详细的介绍请参考框架介绍文档: [调试模式](./core/debugging.md) ## 命令总览 当前帮助文档以 `gf cli v2.0.0` 版本为例进行简单的介绍,详细的介绍信息请查看命令行帮助信息。本章内容信息可能会有滞后,最新的具体详细介绍请查看工具帮助信息。 ```text $ gf USAGE gf COMMAND [OPTION] COMMAND env show current Golang environment variables run running go codes with hot-compiled-like feature gen automatically generate go files for dao/dto/entity/pb/pbentity... init create and initialize an empty GoFrame project pack packing any file/directory to a resource file, or a go file build cross-building go project for lots of platforms docker build docker image for current GoFrame project install install gf binary to system (might need root/admin permission) version show version information of current binary OPTION -y, --yes all yes for all command without prompt ask -v, --version show version information of current binary -d, --debug show internal detailed debugging information -h, --help more information about this command ADDITIONAL Use "gf COMMAND -h" for details about a command. ``` # 工具安装-install 该命令仅针对于预编译二进制下载安装。如果通过 `go install` 命名安装的工具的话,不需要手动再使用 `install` 命令安装 `gf` 工具。 ## 下载安装 ### 最新版下载 #### `Mac`&`Linux`  快捷下载命令 ```bash wget -O gf https://github.com/gogf/gf/releases/latest/download/gf_$(go env GOOS)_$(go env GOARCH) && chmod +x gf && ./gf install -y && rm ./gf ``` #### Windows 需手动下载 确定自己当前项目的 `goframe` 依赖版本,查看自己的系统信息: ```bash go env GOOS go env GOARCH ``` 下载地址: [releases](https://github.com/gogf/gf/releases) ### 通过 `go install` 安装 注意:需要将 `$GOPATH/bin` 加入到系统环境变量中,通过 `go env GOPATH` 查看。 #### 最新版本 ```bash go install github.com/gogf/gf/cmd/gf/v2@latest ``` #### 指定版本(版本需要 >= v2.5.5) ```bash go install github.com/gogf/gf/cmd/gf/v2@v2.5.5 ``` ### 其它版本下载 #### v2 版本 预编译二进制下载: [releases](https://github.com/gogf/gf/releases) 源码:[gf/cmd/gf](https://github.com/gogf/gf/tree/master/cmd/gf) #### v1 版本 预编译二进制下载: [releases](https://github.com/gogf/gf-cli/releases) 源码: [gogf/gf-cli](https://github.com/gogf/gf-cli) ## 使用方式 项目地址: [https://github.com/gogf/gf/tree/master/cmd/gf](https://github.com/gogf/gf/tree/master/cmd/gf) 使用方式: `./gf install` 该命令往往是在 `gf` 命令行工具下载到本地后执行(注意执行权限),用于将 `gf` 命令安装到系统环境变量默认支持的目录路径中,以便于在系统任何的地方直接可以使用 `gf` 工具。 **备注** 部分系统需要管理员权限支持。 如果是 `MacOS` 下使用 `zsh` 的小伙伴可能会遇到别名冲突问题,可以通过 `alias gf=gf` 来解决,运行一次之后 `gf` 工具会自动修改 `profile` 中的别名设置,用户重新登录(或者重开终端)就好了。 ## 使用示例 ```bash $ ./gf_darwin_amd64 install I found some installable paths for you(from $PATH): Id | Writable | Installed | Path 0 | true | true | /usr/local/bin 1 | true | false | /Users/john/Workspace/Go/GOPATH/bin 2 | true | false | /Users/john/.gvm/bin 4 | true | false | /Users/john/.ft please choose one installation destination [default 0]: gf binary is successfully installed to: /usr/local/bin ``` # 版本查看-version ## 使用方式 - `gf -v` - `gf version` 用以查看当前 `gf` 命令行工具编译时的版本信息。 ## 使用示例 ### `>= v2.5.7` ```text $ gf version v2.7.2 Welcome to GoFrame! Env Detail: Go Version: go1.22.2 linux/amd64 GF Version(go.mod): github.com/gogf/gf/contrib/drivers/mysql/v2@v2.7.2 github.com/gogf/gf/v2@v2.7.2 CLI Detail: Installed At: /data/home/v_hlaghuang/go/bin/gf Built Go Version: go1.20.8 Built GF Version: v2.7.2 Git Commit: 2024-06-26 10:08:04 b11caba5b03ed54fbb1415151f7d62b6d913179d Built Time: 2024-06-26 10:09:50 Others Detail: Docs: https://goframe.org Now : 2024-07-17T15:48:57+08:00 ``` ### `< v2.5.6` ```text $ gf version GoFrame CLI Tool v2.0.0, https://goframe.org GoFrame Version: v2.0.0-beta.0.20211214160159-19c9f0a48845 in current go.mod CLI Installed At: /Users/john/Workspace/Go/GOPATH/src/github.com/gogf/gf-cli/main CLI Built Detail: Go Version: go1.16.3 GF Version: v2.0.0-beta Git Commit: 2021-12-15 22:43:12 7884058b5df346d34ebab035224e415afb556c19 Build Time: 2021-12-15 23:00:43 ``` ## 注意事项 在打印的版本信息中会自动检测当前项目使用的 `GoFrame` 版本(自动解析 `go.mod`),并以 `GoFrame Version` 的信息打印出来。 在 `CLI Built Detail` 信息中展示的是当前二进制编译时使用的各种 `Golang` 版本以及 `GoFrame` 版本信息,编译时的 `Git` 提交版本、当前二进制文件的编译时间。 **注意** 大家请勿将 `GoFrame Version` 和 `CLI Built Detail` 中的 `GF Version` 混淆。 # 框架升级-up **提示** 该命令从框架 `v2.3` 版本开始提供。 ## 使用方式 ```text $ gf up -h USAGE gf up [OPTION] OPTION -a, --all upgrade both version and cli, auto fix codes -c, --cli also upgrade CLI tool -f, --fix auto fix codes(it only make sense if cli is to be upgraded) -h, --help more information about this command EXAMPLE gf up gf up -a gf up -c gf up -cf ``` 用以实现版本更新,同时更新主框架版本、社区组件版本到最新版本。 选项说明: | 名称 | 含义 | | --- | --- | | `all` | 同时更新 `cli` 工具版本,并且自动修复本地代码在升级中的不兼容变更 | | `fix` | 升级时自动修复本地代码在升级中的不兼容变更 | | `cli` | 升级时同时更新 `cli` 工具版本 | ## 使用示例 ```text $ gf up -a start upgrading version... upgrading "github.com/gogf/gf/contrib/drivers/mysql/v2" from "v2.2.4" to "latest" go: upgraded github.com/BurntSushi/toml v1.1.0 => v1.2.1 go: upgraded github.com/cespare/xxhash/v2 v2.1.2 => v2.2.0 go: upgraded github.com/clbanning/mxj/v2 v2.5.6 => v2.5.7 go: upgraded github.com/fsnotify/fsnotify v1.5.4 => v1.6.0 go: upgraded github.com/go-sql-driver/mysql v1.6.0 => v1.7.0 go: upgraded github.com/gogf/gf/contrib/drivers/mysql/v2 v2.2.4 => v2.2.6 go: upgraded github.com/gogf/gf/v2 v2.2.4 => v2.2.6 go: upgraded github.com/magiconair/properties v1.8.6 => v1.8.7 go: upgraded github.com/mattn/go-colorable v0.1.12 => v0.1.13 go: upgraded github.com/mattn/go-isatty v0.0.14 => v0.0.17 go: upgraded github.com/mattn/go-runewidth v0.0.13 => v0.0.14 go: upgraded github.com/rivo/uniseg v0.2.0 => v0.4.3 go: upgraded go.opentelemetry.io/otel v1.7.0 => v1.11.2 go: upgraded go.opentelemetry.io/otel/sdk v1.7.0 => v1.11.2 go: upgraded golang.org/x/net v0.0.0-20220621193019-9d032be2e588 => v0.5.0 go: upgraded golang.org/x/sys v0.0.0-20220615213510-4f61da869c0c => v0.4.0 go: upgraded golang.org/x/text v0.3.8-0.20220509174342-b4bca84b0361 => v0.6.0 go: upgraded golang.org/x/tools v0.1.11-0.20220504162446-54c7ba520b92 => v0.1.12 upgrading "github.com/gogf/gf/v2" from "v2.2.4" to "latest" auto fixing path "/Users/john/Workspace/Go/GOPATH/src/github.com/Khaos/eros"... done! ``` ## 注意事项 命令执行前请 `git` 提交本地修改内容或执行目录备份。 # 项目创建-init **提示** 从`v2`版本开始,项目的创建不再依赖远端获取,仓库模板已经通过 [资源管理](../core/gres.md) 的方式内置到了工具二进制文件中,因此项目创建速度非常迅速,无需网络访问。 ## 使用方式 ```bash $ gf init -h USAGE gf init ARGUMENT [OPTION] ARGUMENT NAME name for the project. It will create a folder with NAME in current directory. The NAME will also be the module name for the project. OPTION -m, --mono initialize a mono-repo instead a single-repo -a, --monoApp initialize a mono-repo-app instead a single-repo -u, --update update to the latest goframe version -g, --module custom go module -r, --repo remote repository URL for template download -s, --select enable interactive version selection for remote template -i, --interactive enable interactive mode to select template -h, --help more information about this command EXAMPLE gf init my-project gf init my-mono-repo -m gf init my-mono-repo -a gf init my-project -u gf init my-project -g "github.com/myorg/myproject" gf init -r github.com/gogf/template-single my-project gf init -r github.com/gogf/examples/httpserver/jwt my-jwt gf init -r github.com/gogf/gf/cmd/gf/v2@v2.9.7 mygf gf init -r github.com/gogf/gf/cmd/gf/v2 mygf -s gf init -i ``` 我们可以使用`init`命令在当前目录生成一个示例的`GoFrame`空框架项目,并可给定项目名称参数。生成的项目目录结构根据业务项目具体情况可自行调整。生成的目录结构详细介绍请参考 [工程目录设计](../design/project-structure.md) 章节。 **提示** - `GoFrame`框架开发推荐统一使用官方的`go module`特性进行依赖包管理,因此空项目根目录下也有一个`go.mod`文件。 - 工程目录采用了通用化的设计,实际项目中可以根据项目需要适当增减模板给定的目录。例如,没有`kubernetes`部署需求的场景,直接删除对应`deploy`目录即可。 ## 选项说明 | 选项 | 缩写 | 说明 | |---|---|---| | `--mono` | `-m` | 创建`MonoRepo`(大仓)项目,而非默认的`SingleRepo`项目 | | `--monoApp` | `-a` | 在`MonoRepo`大仓中创建一个子应用项目 | | `--update` | `-u` | 项目创建后自动升级`GoFrame`依赖到最新版本 | | `--module` | `-g` | 自定义`Go`模块路径,不指定时默认使用项目名称 | | `--repo` | `-r` | 指定远程仓库`URL`作为项目模板来源 | | `--select` | `-s` | 配合`-r`使用,启用交互式版本选择 | | `--interactive` | `-i` | 启用完整的交互式引导模式创建项目 | ## 模板类型 `gf init`命令支持三种内置项目模板: | 模板类型 | 选项 | 说明 | |---|---|---| | `SingleRepo` | 默认 | 单仓项目,适用于大多数独立项目开发场景 | | `MonoRepo` | `-m` | 大仓项目,适用于微服务架构的统一仓库管理 | | `MonoRepoApp` | `-a` | 大仓子应用,在已有`MonoRepo`仓库中创建新的子服务 | ## 使用示例 ### 在当前目录下初始化项目 使用`.`作为项目名称,直接在当前目录下初始化项目,模块名称默认使用当前目录名称。 ```bash $ gf init . initializing... initialization done! you can now run 'gf run main.go' to start your journey, enjoy! ``` ### 创建一个指定名称的项目 指定项目名称后,会在当前目录下创建一个同名文件夹,模块名称默认与项目名称一致。 ```bash $ gf init myapp initializing... initialization done! you can now run 'cd myapp && gf run main.go' to start your journey, enjoy! ``` ### 创建一个`MonoRepo`项目 默认情况下创建的是`SingleRepo`项目,若有需要也可以创建一个`MonoRepo`(大仓)项目,通过使用`-m`选项即可。 ```bash $ gf init mymono -m initializing... initialization done! ``` 关于大仓的介绍请参考章节: [微服务大仓管理模式](../design/project-mono-repo.md) ### 创建一个`MonoRepoApp`项目 若需要在`MonoRepo`(大仓)下创建一个子应用,在大仓项目根目录下,给定需要生成的项目路径,并使用`-a`选项即可。 ```bash $ gf init app/user -a initializing... initialization done! ``` ### 自定义`Go`模块路径 默认情况下项目的`go.mod`中的模块名称(`module`)与项目文件夹名称一致。如果需要自定义`Go`模块路径(例如使用完整的仓库地址),可以通过`-g`选项指定。 ```bash $ gf init myapp -g "github.com/myorg/myproject" initializing... initialization done! you can now run 'cd myapp && gf run main.go' to start your journey, enjoy! ``` 执行后生成的`go.mod`文件中模块路径为`github.com/myorg/myproject`,项目中所有相关的`import`路径也会自动替换为该模块路径。 ### 创建项目并升级`GoFrame`版本 使用`-u`选项可以在项目创建完成后自动将`GoFrame`依赖升级到**最新版本**,并执行`go mod tidy`。 ```bash $ gf init myapp -u initializing... initialization done! you can now run 'cd myapp && gf run main.go' to start your journey, enjoy! ``` ## 远程模板 除了使用内置模板外,`gf init`还支持从远程仓库拉取模板来创建项目,通过`-r`选项指定远程仓库地址。远程模板支持以下两种来源: - **`Go`模块**:通过`Go`模块代理下载,支持使用`@version`语法指定版本。 - **`Git`仓库子目录**:通过`git sparse checkout`下载指定子目录,适合从示例仓库中提取特定项目。 **提示**:注意 使用远程模板时需要确保本地已安装`Go`环境。如果使用`Git`仓库子目录方式,还需要安装`git`工具。远程模板模式下,目标目录必须为空,否则会返回错误。 ### 使用远程`Go`模块模板 ```bash $ gf init -r github.com/gogf/template-single my-project ``` ### 使用远程仓库子目录模板 可以从一个`Git`仓库的子目录中提取模板,适合从示例仓库中获取特定类型的项目模板。 ```bash $ gf init -r github.com/gogf/examples/httpserver/jwt my-jwt ``` ### 指定模板版本 可以通过`@version`语法指定模板的版本号。 ```bash $ gf init -r github.com/gogf/gf/cmd/gf/v2@v2.9.7 mygf ``` ### 交互式选择模板版本 使用`-s`选项配合`-r`选项,可以在命令执行时列出可用版本供用户选择。工具会展示最新的版本列表(按版本号倒序排列),用户可以通过编号选择或直接输入版本号。 ```bash $ gf init -r github.com/gogf/gf/cmd/gf/v2 mygf -s ``` ## 交互式模式 使用`-i`选项可以启用完整的交互式引导模式,工具会分步引导用户完成项目创建的所有配置。 ```bash $ gf init -i ``` 交互式模式分为两个阶段: **第一步:选择初始化方式** - **内置模板**(默认):使用工具内置的模板快速创建项目,无需网络访问。 - **远程模板**:从远程仓库拉取模板创建项目。 **第二步(内置模板方式):** - 选择项目类型:`SingleRepo`/`MonoRepo`/`MonoRepoApp` - 输入项目名称 - 输入`Go`模块路径(可选,默认使用项目名称) - 选择是否升级到最新`GoFrame`版本 **第二步(远程模板方式):** - 选择预设模板(`template-single`、`template-mono`)或输入自定义仓库`URL` - 输入项目名称 - 输入`Go`模块路径(可选) - 选择是否升级依赖 **说明** 交互式模式对新手用户非常友好,如果不确定各参数的用法,推荐使用该模式完成项目创建。 ## 注意事项 - 当目标目录已存在且非空时,使用内置模板会提示是否覆盖;使用远程模板则会直接报错,要求目标目录为空。 - 使用`.`作为项目名称时,会直接在当前目录下初始化项目,模块名默认取当前目录名称。 - `-u`选项在内置模板模式下会升级`GoFrame`相关依赖;在远程模板模式下会升级所有依赖并执行`go mod tidy`。 - 项目创建完成后,所有生成的`.go`文件中的`import`路径会自动替换为实际的模块路径。 # 交叉编译-build ## 使用方式 `gf build` 命令用于 Go 项目的交叉编译,是 `go build` 的强大封装。 使用 `gf build -h` 查看完整帮助信息。支持为多种操作系统和架构编译可执行文件。 ## 内置编译变量 `gf build` 命令在编译时会自动嵌入一组内置变量到二进制文件中,开发者也可以自定义这些变量。运行时可通过 `gbuild` 组件读取这些变量。 默认嵌入的变量包括(可通过 `gf -v` 查看): - 当前 `Go` 版本 - 当前 `GoFrame` 版本 - 当前 `Git Commit`(如果存在) - 当前编译时间 这些信息有助于版本管理和问题追溯。 ## 编译配置文件 `gf build` 支持从命令行参数和配置文件两种方式指定编译选项。GoFrame 的配置管理采用统一的配置组件,默认配置文件及使用方法请参考 [配置管理](../core/gcfg.md)。 以下是一个配置文件示例: ```yaml gfcli: build: name: "gf" arch: "all" system: "all" mod: "none" packSrc: "resource,manifest" version: "v1.0.0" output: "./bin" extra: "" ``` 配置选项与命令行同名参数含义一致。各配置项说明如下: | 名称 | 默认值 | 含义 | 示例 | | --- | --- | --- | --- | | `name` | 与程序入口 `go` 文件同名 | 生成的可执行文件名称。如果是 `windows` 平台,那么默认会加上 `.exe` 后缀 | `gf` | | `arch` | 当前系统架构 | 编译架构,多个以 `,` 号分隔,如果是 `all` 表示编译所有支持架构 | `386,amd64,arm` [更多](https://golang.google.cn/doc/install/source#environment) | | `system` | `当前系统平台` | 编译平台,多个以 `,` 号分隔,如果是 `all` 表示编译所有支持平台 | `linux,darwin,windows` [更多](https://golang.google.cn/doc/install/source#environment) | | `path` | `./bin` | 编译可执行文件存储的 **目录地址** | `./bin` | | `mod` | | 同 `go build -mod` 编译选项,不常用 | `none` | | `cgo` | `false` | 是否开启 `CGO`,默认是关闭的。如果开启,那么交叉编译可能会有问题。 | | | `packSrc` | | 需要打包的目录,多个以 `,` 号分隔,生成到 `internal/packed/build_pack_data.go` | `public,template,manifest` | | `packDst` | `internal/packed/build_pack_data.go` | 打包后生成的 `Go` 文件路径,一般使用相对路径指定到本项目目录中 | | | `version` | | 程序版本,如果指定版本信息,那么程序生成的路径中会多一层以版本名称的目录 | `v1.0.0` | | `output` | | 输出的可执行文件路径,当该参数指定时, `name` 和 `path` 参数失效,常用于编译单个可执行文件。 | `./bin/gf.exe` | | `extra` | | 额外自定义的编译参数,会直接传递给 `go build` 命令 | | | `varMap` | | 自定义的内置变量键值对,构建的二进制中可以通过 `gbuild` 包获取编译信息。 | ```
    gfcli:
    build:
    name: "gf"
    arch: "all"
    system: "all"
    mod: "none"
    cgo: 0
    varMap:
    k1: v1
    k2: v2
    ``` | | `exitWhenError` | `false` | 当编译发生错误时,立即停止后续执行,并退出编译流程(使用 `os.Exit(1)`) | | | `dumpEnv` | `false` | 每次编译之前在终端打印当前编译环境的环境变量信息 | | **提示** 编译时的内置变量可以在运行时通过 `gbuild` 包 [构建信息-gbuild](../components/os-gbuild.md) 获取。 ## 使用示例 ```text $ gf build 2020-12-31 00:35:25.562 start building... 2020-12-31 00:35:25.562 go build -o ./bin/darwin_amd64/gf main.go 2020-12-31 00:35:28.381 go build -o ./bin/freebsd_386/gf main.go 2020-12-31 00:35:30.650 go build -o ./bin/freebsd_amd64/gf main.go 2020-12-31 00:35:32.957 go build -o ./bin/freebsd_arm/gf main.go 2020-12-31 00:35:35.824 go build -o ./bin/linux_386/gf main.go 2020-12-31 00:35:38.082 go build -o ./bin/linux_amd64/gf main.go 2020-12-31 00:35:41.076 go build -o ./bin/linux_arm/gf main.go 2020-12-31 00:35:44.369 go build -o ./bin/linux_arm64/gf main.go 2020-12-31 00:35:47.352 go build -o ./bin/linux_ppc64/gf main.go 2020-12-31 00:35:50.293 go build -o ./bin/linux_ppc64le/gf main.go 2020-12-31 00:35:53.166 go build -o ./bin/linux_mips/gf main.go 2020-12-31 00:35:55.840 go build -o ./bin/linux_mipsle/gf main.go 2020-12-31 00:35:58.423 go build -o ./bin/linux_mips64/gf main.go 2020-12-31 00:36:01.062 go build -o ./bin/linux_mips64le/gf main.go 2020-12-31 00:36:03.502 go build -o ./bin/netbsd_386/gf main.go 2020-12-31 00:36:06.280 go build -o ./bin/netbsd_amd64/gf main.go 2020-12-31 00:36:09.332 go build -o ./bin/netbsd_arm/gf main.go 2020-12-31 00:36:11.811 go build -o ./bin/openbsd_386/gf main.go 2020-12-31 00:36:14.140 go build -o ./bin/openbsd_amd64/gf main.go 2020-12-31 00:36:17.859 go build -o ./bin/openbsd_arm/gf main.go 2020-12-31 00:36:20.327 go build -o ./bin/windows_386/gf.exe main.go 2020-12-31 00:36:22.994 go build -o ./bin/windows_amd64/gf.exe main.go 2020-12-31 00:36:25.795 done! ``` # 自动编译-run ## 注意事项 由于 `Go` 是不支持热编译特性的,每一次代码变更后都要重新手动停止、编译、运行代码文件。 `run` 命令也不是实现热编译功能,而是提供了自动编译功能,当开发者修改了项目中的 `go` 文件时,该命令将会自动编译当前程序,并停止原有程序,运行新版的程序。 **提示** `run` 命令会递归监控 **当前运行目录** 的所有 `go` 文件变化来实现自动编译。 ## 使用帮助 ```text $ gf run -h USAGE gf run FILE [OPTION] ARGUMENT FILE building file path. OPTION -p, --path output directory path for built binary file. it's "./" in default -e, --extra the same options as "go run"/"go build" except some options as follows defined -a, --args custom arguments for your process -w, --watchPaths watch additional paths for live reload, separated by ",". i.e. "manifest/config/*.yaml" -h, --help more information about this command EXAMPLE gf run main.go gf run main.go --args "server -p 8080" gf run main.go -mod=vendor gf run main.go -w "manifest/config/*.yaml" DESCRIPTION The "run" command is used for running go codes with hot-compiled-like feature, which compiles and runs the go codes asynchronously when codes change. ``` 配置文件格式示例: ```yaml gfcli: run: path: "./bin" extra: "" args: "all" watchPaths: - api/*.go - internal/controller/*.go ``` 参数介绍: | 名称 | 默认值 | 含义 | 示例 | | --- | --- | --- | --- | | `path` | `./` | 指定编译后生成的二进制文件存放目录。 | | | `extra` | | 指定用于底层 `go build` 的命令参数 | | | `args` | | 指定启动运行二进制文件的命令行参数 | | | `watchPath` | | 指定本地项目文件监听的文件路径格式,支持多个路径使用 `,` 覆盖分隔。该参数的格式同标准库的 `filepath.Match` 方法参数 | `internal/*.go` | ## 使用示例 一般 `gf run main.go` 即可 ```text $ gf run main.go --swagger 2020-12-31 00:40:16.948 build: main.go 2020-12-31 00:40:16.994 producing swagger files... 2020-12-31 00:40:17.145 done! 2020-12-31 00:40:17.216 gf pack swagger packed/swagger.go -n packed -y 2020-12-31 00:40:17.279 done! 2020-12-31 00:40:17.282 go build -o bin/main main.go 2020-12-31 00:40:18.696 go file changes: "/Users/john/Workspace/Go/GOPATH/src/github.com/gogf/gf-demos/packed/swagger.go": WRITE 2020-12-31 00:40:18.696 build: main.go 2020-12-31 00:40:18.775 producing swagger files... 2020-12-31 00:40:18.911 done! 2020-12-31 00:40:19.045 gf pack swagger packed/swagger.go -n packed -y 2020-12-31 00:40:19.136 done! 2020-12-31 00:40:19.144 go build -o bin/main main.go 2020-12-31 00:40:21.367 bin/main 2020-12-31 00:40:21.372 build running pid: 40954 2020-12-31 00:40:21.437 [DEBU] [ghttp] SetServerRoot path: /Users/john/Workspace/Go/GOPATH/src/github.com/gogf/gf-demos/public 2020-12-31 00:40:21.440 40954: http server started listening on [:8199] ... ``` ## 常见问题 [too many open files on macOS](https://github.com/fsnotify/fsnotify/issues/129) # 资源打包-pack ## 使用方式 ```text $ gf pack -h USAGE gf pack SRC DST ARGUMENT SRC source path for packing, which can be multiple source paths. DST destination file path for packed file. if extension of the filename is ".go" and "-n" option is given, it enables packing SRC to go file, or else it packs SRC into a binary file. OPTION -n, --name package name for output go file, it's set as its directory name if no name passed -p, --prefix prefix for each file packed into the resource file -k, --keepPath keep the source path from system to resource file, usually for relative path -h, --help more information about this command EXAMPLE gf pack public data.bin gf pack public,template data.bin gf pack public,template packed/data.go gf pack public,template,config packed/data.go gf pack public,template,config packed/data.go -n=packed -p=/var/www/my-app gf pack /var/www/public packed/data.go -n=packed ``` 该命令用以将任意的文件打包为资源文件或者 `Go` 代码文件,可将任意文件打包后随着可执行文件一同发布。此外,在 `build` 命令中支持打包+编译一步进行,具体请查看 `build` 命令帮助信息。关于资源管理的介绍请参考 [资源管理](../core/gres.md) 章节。 ## 使用示例 ```text $ gf pack public,template packed/data.go done! $ ll packed total 184 -rw-r--r-- 1 john staff 89K Dec 31 00:44 data.go ``` ## 延伸阅读 - [资源管理-最佳实践](../core/gres-practice.md) # 镜像编译-docker **提示** 从 `v2.5` 版本开始,为提高命令解耦性,`gf docker` 命令默认不再执行二进制编译。推荐使用 `Makefile` 构建脚本组合 `gf build`、`gf gen enums`、`gf docker` 等命令(工程项目中提供了 `make build`、`make enums`、`make docker` 命令),这种方式更灵活且易于维护。 ## 使用方式 ```text $ gf docker -h USAGE gf docker [MAIN] [OPTION] ARGUMENT MAIN main file path for "gf build", it's "main.go" in default. empty string for no binary build OPTION -f, --file file path of the Dockerfile. it's "manifest/docker/Dockerfile" in default -s, --shell path of the shell file which is executed before docker build -b, --build binary build options before docker image build, it's "-a amd64 -s linux" in default -tn, --tagName tag name for this docker, pattern like "image:tag". this option is required with TagPrefixes -tp, --tagPrefixes tag prefixes for this docker, which are used for docker push. this option is required with TagName -p, --push auto push the docker image to docker registry if "-t" option passed -e, --extra extra build options passed to "docker image" -h, --help more information about this command EXAMPLE gf docker gf docker -t hub.docker.com/john/image:tag gf docker -p -t hub.docker.com/john/image:tag gf docker main.go gf docker main.go -t hub.docker.com/john/image:tag gf docker main.go -t hub.docker.com/john/image:tag gf docker main.go -p -t hub.docker.com/john/image:tag DESCRIPTION The "docker" command builds the GF project to a docker images. It runs "gf build" firstly to compile the project to binary file. It then runs "docker build" command automatically to generate the docker image. You should have docker installed, and there must be a Dockerfile in the root of the project. ``` 自动编译并生成 Docker 镜像。`MAIN` 参数为可选的编译文件路径,默认为 `main.go`。`OPTIONS` 参数为可选的 `docker build` 命令参数及选项。 该命令会自动完成以下流程: 1. 执行 `gf build` 编译项目为二进制文件 2. 执行 `docker build` 生成镜像 3. 可选执行 `docker push` 推送镜像到仓库 前提条件: - 系统已安装 Docker - 项目根目录存在 Dockerfile 文件 ## 使用示例 ```text $ gf docker main.go -p -tn loads/gf-demos:test 2020-12-31 00:47:28.207 start building... 2020-12-31 00:47:28.207 go build -o ./bin/linux_amd64/main main.go 2020-12-31 00:47:35.894 done! Sending build context to Docker daemon 37.63MB Step 1/10 : FROM loads/alpine:3.8 ---> f9fb622e6db2 Step 2/10 : LABEL maintainer="john@goframe.org" ---> Using cache ---> da238418d031 Step 3/10 : ENV WORKDIR /var/www/gf-demos ---> Using cache ---> 3e7129c087c9 Step 4/10 : ADD ./bin/linux_amd64/main $WORKDIR/main ---> 3661a9dea494 Step 5/10 : RUN chmod +x $WORKDIR/main ---> Running in 1d49d5d91080 Removing intermediate container 1d49d5d91080 ---> a03ee04e3380 Step 6/10 : ADD public $WORKDIR/public ---> 63dd06d0e1a3 Step 7/10 : ADD config $WORKDIR/config ---> fa7a57eba577 Step 8/10 : ADD template $WORKDIR/template ---> 7075609b0447 Step 9/10 : WORKDIR $WORKDIR ---> Running in a34ef38e1031 Removing intermediate container a34ef38e1031 ---> 580077998eaf Step 10/10 : CMD ./main ---> Running in ed286b518ad9 Removing intermediate container ed286b518ad9 ---> fbbc05842901 Successfully built fbbc05842901 Successfully tagged loads/gf-demos:test The push refers to repository [docker.io/loads/gf-demos] b4025b95a79f: Preparing 9e0369a57507: Preparing 46c68dcc8e12: Preparing 59adbc083ee5: Preparing 10e0b999ba57: Preparing 8e850d7b086e: Waiting d5e057db20a2: Waiting 92e898fd7f84: Waiting d9ff549177a9: Waiting ... ``` ## 配置文件示例 推荐使用配置文件管理工具配置,在 `hack/config.yaml` 文件中维护。`docker` 命令配置示例: ```yaml gfcli: docker: build: "-a amd64 -s linux -p temp -ew" tagPrefixes: - ccr.ccs.tencentyun.com/cdb.khaos.eros ``` # 兼容修复-fix **提示** 该命令从框架 `v2.3` 版本开始提供。 ## 使用场景 GoFrame框架在版本升级时会尽最大努力保证向下兼容性。当遇到以下情况时,可使用 `gf fix` 命令自动修复兼容性问题: 1. 框架升级涉及较小的不兼容变更 2. 新增大版本号成本较高的场景 3. 需要批量更新代码适配新版本 该命令可重复执行,不会产生副作用。 ## 使用方式 ```text $ gf fix -h USAGE gf fix OPTION -/--path directory path, it uses current working directory in default -h, --help more information about this command ``` 用于在低版本(当前 `go.mod` 中的 `GoFrame` 版本)升级到高版本(当前 `CLI` 使用的 `GoFrame` 版本)时,自动更新本地代码的不兼容变更。 命令会自动检测项目中使用的GoFrame版本,并应用相应的修复规则。主要修复内容包括: - 结构体到接口的类型变更(如 `*gdb.TX` 到 `gdb.TX`) - 函数名称变更(如网络包函数重命名) - API调整和废弃接口替换 ## 注意事项 **执行命令前务必做好代码备份:** - 使用 `git` 提交本地修改内容 - 或对执行目录进行完整备份 该命令会直接修改源代码文件,虽然可重复执行,但建议在有版本控制的情况下使用。 # 工具帮助-help 使用方式: - `gf -h` - `gf [COMMAND] -h` 任何不懂的,就用 `help` 看看吧。 # 代码生成-gen **说明** 从 `v2` 版本开始,最新的 `CLI` 工具版本功能会随着 `GoFrame` 框架的最新版本编译,引入如果本地的 `CLI` 工具自动化生成的代码与项目的 `GoFrame` 框架版本出现兼容性问题时,建议升级项目框架版本,或者自定义安装旧版本的 `CLI` 工具。旧版本CLI工具安装方式参考仓库首页介绍: [https://github.com/gogf/gf-cli](https://github.com/gogf/gf-cli) ## 重要说明 - `CLI` 工具提供的代码生成功能,目的是 **规范化项目代码编写**、 **简化项目开发复杂度**, **让开发者能够把精力聚焦于业务逻辑本身**。 - `CLI` 工具本身会需要有一定前置的学习和理解成本(尽量理解为什么),但在熟练之后,大家的开发工作将会事半功倍。 - `CLI` 工具的代码生成功能针对于企业级项目、多成员的团队性项目中收益会非常高。但针对于单人小型项目,开发者可根据个人意愿评估是否选择使用。 `GoFrame` 框架本身只是提供了基础组件,采用了组件化的灵活设计,不会对项目代码做严格的要求;但 `CLI` 工具会有一定的条框限制,目的是使得团队中每个成员的步调和风格一致,不会使得开发者的代码编写过于随意。 ## 使用方式 ```text $ gf gen -h USAGE gf gen COMMAND [OPTION] COMMAND ctrl parse api definitions to generate controller/sdk go files dao automatically generate go files for dao/do/entity enums parse go files in current project and generate enums go file pb parse proto files and generate protobuf go files pbentity generate entity message files in protobuf3 format service parse struct and associated functions from packages to generate service go file DESCRIPTION The "gen" command is designed for multiple generating purposes. It's currently supporting generating go files for ORM models, protobuf and protobuf entity files. Please use "gf gen dao -h" for specified type help. ``` # 接口规范-gen ctrl **提示** 该功能特性从 `v2.5` 版本开始提供。该命令目前仅支持 `HTTP` 接口开发, `GRPC` 部分请参考 `gen pb` 命令。未来会考虑 `HTTP` 及 `GRPC` 统一使用该命令生成控制器及 `SDK` 源代码。 ## 基本介绍 ### 解决痛点 在开发项目时,通常需要先根据业务需求设计 API 接口,使用 `proto` 或 `golang struct` 定义 API 的输入输出,然后创建对应的控制器实现,最后可能还需要提供 SDK(同为 Golang 语言条件下)供内外部服务调用。开发过程中会遇到以下痛点: - **重复性的代码工作繁琐**。在 `api` 中创建输入输出定义文件后,还需要在控制器目录下创建对应的文件、编写控制器初始化代码、从 `api` 代码中反复拷贝输入输出结构名称,过程繁琐。 - **API与控制器之间的关联缺乏可靠规范约束**。除了 `API` 有一定的命名约束外,控制器的创建和方法命名灵活度较高,`API` 的结构名称与控制器方法名称难以约束对应,当接口越来越多时维护成本增加。 - **团队开发多人协作时代码文件冲突概率大**。多人同时修改同一个文件时,文件冲突概率增大,处理这种冲突的精力开销毫无意义。 - **缺少API的HTTP SDK自动生成工具**。开发完 `API` 后,往往需要立即给内部或外部调用,缺少便捷的 `SDK` 生成工具,手动维护 `SDK` 代码的成本很高。 ### 命令特性 - 规范了 `API` 定义与控制器文件命名、控制器方法命名。 - 规范了 `API` 定义与控制器代码之间的关联关系,便于快速定位 `API` 实现。 - 根据 `API` 定义自动生成控制器接口、初始化文件及代码、接口初始化代码。 - 根据 `API` 定义自动生成易用的 `HTTP SDK` 代码。该功能可配置,默认关闭。 - 支持 `File Watch` 自动化生成模式:当 `API` 结构定义文件发生变化时,自动增量更新对应的控制器、`SDK` 代码。 ## 前置约定 ### 重要规范 该命令的目的之一是规范化 `api` 代码编写,以下是必须遵守的重要规范(否则无法生成代码): - `api` 层的接口定义文件路径需要满足 `/api/模块/版本/定义文件.go`,例如:`/api/user/v1/user.go`、`/api/user/v1/user_delete.go`等。 - **模块**:指 `API` 的模块划分,可以按照不同的 **业务属性** 拆分,方便聚合维护。也可以将模块理解为具体的业务资源。 - **版本**:通常使用 `v1`/`v2` 等形式定义,用于 `API` 兼容性的版本控制。当相同的 `API` 出现兼容性更新时,需要通过不同版本号区分。默认使用 `v1` 管理第一个版本。 - **定义文件**:指 `API` 的输入输出定义文件,通常每个 `API` 需要单独定义一个 `go` 文件来独立维护。也支持将多个 `API` 放到一个 `go` 文件中统一维护。 - `api` 定义的结构体名称需要满足 `操作+Req` 及 `操作+Res` 的命名方式。例如:`GetOneReq/GetOneRes`、`GetListReq/GetListRes`、`DeleteReq/DeleteRes` 等。 - **操作**:当前 `API` 模块的操作名称,通常对应 `CRUD` 是:`Create`、`Read`、`Update`、`Delete`。 以下是项目工程模板中的 `Hello` 接口示例: ### 建议性的命名 我们对一些常用的接口定义做了一些建议性的命名,供大家参考: | 操作名称 | 建议命名 | 备注 | | --- | --- | --- | | **查询列表** | `GetListReq/Res` | 通常是从数据库中分页查询数据记录 | | **查询详情** | `GetOneReq/Res` | 通常接口需要传递主键条件,从数据库中查询记录详情 | | **创建资源** | `CreateReq/Res` | 通常是往数据表中插入一条或多条数据记录 | | **修改资源** | `UpdateReq/Res` | 通常是按照一定条件修改数据表中的一条或多条数据记录 | | **删除资源** | `DeleteReq/Res` | 通常是按照一定条件删除数据表中的一条或多条数据记录 | ## 命令使用 该命令通过分析给定的 `api` 接口定义目录下的代码,自动生成对应的控制器/ `SDK Go` 代码文件。 ### 手动模式 如果是手动执行命令行,直接在项目根目录下执行 `gf gen ctrl` 即可,她将完整扫描 `api` 接口定义目录,并生成对应代码。 ```text $ gf gen ctrl -h USAGE gf gen ctrl [OPTION] OPTION -s, --srcFolder source folder path to be parsed. default: api -d, --dstFolder destination folder path storing automatically generated go files. default: internal/controller -w, --watchFile used in file watcher, it re-generates go files only if given file is under srcFolder -k, --sdkPath also generate SDK go files for api definitions to specified directory -v, --sdkStdVersion use standard version prefix for generated sdk request path -n, --sdkNoV1 do not add version suffix for interface module name if version is v1 -c, --clear auto delete generated and unimplemented controller go files if api definitions are missing -m, --merge generate all controller files into one go file by name of api definition source go file -h, --help more information about this command EXAMPLE gf gen ctrl ``` **提示** 如果使用框架推荐的项目工程脚手架,并且系统安装了 `make` 工具,也可以使用 `make ctrl` 快捷指令。 参数说明: | 名称 | 必须 | 默认值 | 含义 | | --- | --- | --- | --- | | `srcFolder` | 否 | `api` | 指向 `api` 接口定义文件目录地址 | | `dstFolder` | 否 | `internal/controller` | 指向生成的控制器文件存放目录 | | `watchFile` | 否 | | 用在IDE的文件监控中,用于根据当文件发生变化时自动执行生成操作 | | `sdkPath` | 否 | | 如果需要生成 `HTTP SDK`,该参数用于指定生成的SDK代码目录存放路径 | | `sdkStdVersion` | 否 | `false` | 生成的 `HTTP SDK` 是否使用标准的版本管理。标准的版本管理将自动根据 `API` 版本增加请求的路由前缀。例如 `v1` 版本的API将会自动增加 `/api/v1` 的请求路由前缀。 | | `sdkNoV1` | 否 | `false` | 生成的 `HTTP SDK` 中,当接口为 `v1` 版本时,接口模块名称是否不带 `V1` 后缀。 | | `clear` | 否 | `false` | 是否删除 `controller` 中与 `api` 层定义不存在的控制器接口文件。 | | `merge` | 否 | `false` | **用以控制生成的 `ctrl` 控制器代码文件按照 `api` 层的文件生成,而不是默认按照 `api` 接口拆分为不同的接口实现文件。** | ### 自动模式(推荐) 如果是使用的 `GolandIDE`,那么可以使用我们提供的配置文件:[watchers.xml](gen-ctrl-watchers.xml)  自动监听代码文件修改时自动生成接口文件。使用方式,如下图: ## 使用示例 ### 自动生成的接口定义文件 ### 自动生成的控制器代码文件 ### 自动生成的 `HTTP SDK` 代码文件 ## 常见问题 ### 为什么每一个 `api` 接口生成一个 `controller` 文件而不是合并到一个 `controller` 文件中 当然,针对小型项目或者个人简单项目、一个 `api` 模块只有几个接口的项目而言,管理的方式并不会成为什么问题,可以根据个人喜好维护代码文件即可。我们这里以较复杂的业务项目,或者企业级项目,在一个 `api` 模块的接口比较多的场景来展开描述一下。 - 首先,开发 `api` 接口时,查找 `api` 接口实现更加清晰,而不是在一个动则上千行的代码文件中查找。 - 其次,在多人协作的项目中,如果多人同时修改同一个 `controller` 文件在版本管理中容易出现文件冲突。一个 `api` 接口对应一个 `controller` 实现文件的维护方式能最大减少代码协作时的文件冲突概率,大部分开发者也不希望花费自己宝贵的时间一次又一次地解决文件冲突上。 - 最后, `controller` 层的代码有它自身的职责: - 校验输入参数:客户端提交的参数都是不可信任的,大部分场景下都需要做数据校验。 - 实现接口逻辑:直接在 `controller` 中实现接口逻辑,或者调用一个或多个 `service` 接口、第三方服务接口来实现接口逻辑。注意事项,不能在 `service` 层的接口中去实现 `api` 接口逻辑,因为 `api` 接口是与具体的业务场景绑定的,无法复用。 **大部分常见的错误是 `controller` 直接把请求透传给 `service` 接口来实现 `api` 接口逻辑,造成了 `controller` 看起来可有可无、 `service` 层的实现越来越重且无法复用。** - 生成返回数据:组织内部产生的结果数据,生成接口定义的返回数据接口。 - 这些职责也就意味着 `controller` 的代码也是比较复杂,分开维护能减少开发者心智负担、易于清晰维护 `api` 接口实现逻辑。 **一些建议**: 如果一个 `api` 模块下的接口文件太多,建议将复杂的 `api` 模块进一步划分为子模块。这样可以对复杂的 `api` 模块进行解耦,也能通过多目录的方式来维护 `api` 接口定义和 `controller` 接口实现文件。目录结构会更清晰,更利于多人协作和版本管理。 _看完以上关于对此的设计后,如果仍然想使用单源码文件来管理所有接口,可以参考 `merge` 参数。_ ### 根据 `api` 模块生成对应的 `controller` 模块中为何存在一个空的 `go` 文件 **例如**: **说明**: 每个 `api` 模块会生成一个空的该模块 `controller` 下的 `go` 文件,该文件只会生成一次,用户可以在里面填充必要的预定义代码内容,例如,该模块 `controller` 内部使用的变量、常量、数据结构定义,或者包初始化 `init` 方法定义等等。 _我们提倡好的代码管理习惯,模块下的 **预定义内容** 尽量统一维护到该模块下以模块名称命名的 `go` 文件中( `模块.go`),而不是分散到各个 `go` 文件中,以便于更好地维护代码。_ 如果该 `controller` 目前没有需要自定义填充的代码内容,那么保留该文件为空即可,为未来预留扩展能力。 # 数据规范-gen dao `gen dao` 命令是 `CLI` 中最频繁使用、也是框架工程规范能否准确落地的关键命令。该命令用于生成 `dao`(数据访问对象)、`do`(数据转化模型)及 `entity`(实体数据模型)的 Go 代码文件。由于该命令的参数、选项较多,推荐使用配置文件来管理生成规则。 **提示** 关于框架项目工程规范介绍请查看 [代码分层设计](../design/project-layer.md) 章节。 ## 使用方式 大部分场景下,进入项目根目录执行 `gf gen dao` 即可。以下为命令行帮助信息: ```text $ gf gen dao -h USAGE gf gen dao [OPTION] OPTION -p, --path directory path for generated files (default: "internal") -l, --link database configuration, the same as the ORM configuration of GoFrame -t, --tables generate models only for given tables, multiple table names separated with ',' -x, --tablesEx generate models excluding given tables, multiple table names separated with ',' -sp, --shardingPattern sharding pattern for table name, e.g. "users_?" will be replace tables "users_001, users_002,..." to "users" dao -g, --group specifying the configuration group name of database for generated ORM instance, it's not necessary and the default value is "default" (default: "default") -f, --prefix add prefix for all table of specified link/database tables -r, --removePrefix remove specified prefix of the table, multiple prefix separated with ',' -rf, --removeFieldPrefix remove specified prefix of the field, multiple prefix separated with ',' -j, --jsonCase generated json tag case for model struct, cases are as follows: | Case | Example | |---------------- |--------------------| | Camel | AnyKindOfString | | CamelLower | anyKindOfString | | Snake | any_kind_of_string | | SnakeScreaming | ANY_KIND_OF_STRING | | SnakeFirstUpper | rgb_code_md5 | | Kebab | any-kind-of-string | | KebabScreaming | ANY-KIND-OF-STRING | (default: "CamelLower") -i, --importPrefix custom import prefix for generated go files -d, --daoPath directory path for storing generated dao files under path (default: "dao") -tp, --tablePath directory path for storing generated table files under path (default: "table") -o, --doPath directory path for storing generated do files under path (default: "model/do") -e, --entityPath directory path for storing generated entity files under path (default: "model/entity") -t0, --tplDaoTablePath template file path for dao table file -t1, --tplDaoIndexPath template file path for dao index file -t2, --tplDaoInternalPath template file path for dao internal file -t3, --tplDaoDoPath template file path for dao do file -t4, --tplDaoEntityPath template file path for dao entity file -s, --stdTime use time.Time from stdlib instead of gtime.Time for generated time/date fields of tables -w, --withTime add created time for auto produced go files -n, --gJsonSupport use gJsonSupport to use *gjson.Json instead of string for generated json fields of tables -v, --overwriteDao overwrite all dao files both inside/outside internal folder -c, --descriptionTag add comment to description tag for each field -k, --noJsonTag no json tag will be added for each field -m, --noModelComment no model comment will be added for each field -a, --clear delete all generated go files that do not exist in database -gt, --genTable generate table files -y, --typeMapping custom local type mapping for generated struct attributes relevant to fields of table -fm, --fieldMapping custom local type mapping for generated struct attributes relevant to specific fields of table -/--genItems -h, --help more information about this command EXAMPLE gf gen dao gf gen dao -l "mysql:root:12345678@tcp(127.0.0.1:3306)/test" gf gen dao -p ./model -g user-center -t user,user_detail,user_login gf gen dao -r user_ CONFIGURATION SUPPORT Options are also supported by configuration file. It's suggested using configuration file instead of command line arguments making producing. The configuration node name is "gfcli.gen.dao", which also supports multiple databases, for example(config.yaml): gfcli: gen: dao: - link: "mysql:root:12345678@tcp(127.0.0.1:3306)/test" tables: "order,products" jsonCase: "CamelLower" - link: "mysql:root:12345678@tcp(127.0.0.1:3306)/primary" path: "./my-app" prefix: "primary_" tables: "user, userDetail" typeMapping: decimal: type: decimal.Decimal import: github.com/shopspring/decimal numeric: type: string fieldMapping: table_name.field_name: type: decimal.Decimal import: github.com/shopspring/decimal ``` **提示** 如果使用框架推荐的项目工程脚手架,并且系统安装了 `make` 工具,也可以使用 `make dao` 快捷指令。 ## 配置示例 文件配置示例: ```yaml title="hack/config.yaml" gfcli: gen: dao: - link: "mysql:root:12345678@tcp(127.0.0.1:3306)/test" tables: "order,products" jsonCase: "CamelLower" - link: "mysql:root:12345678@tcp(127.0.0.1:3306)/primary" path: "./my-app" prefix: "primary_" tables: "user, userDetail" # sqlite需要自行编译带sqlite驱动的gf,下载库代码后修改路径文件(gf\cmd\gf\internal\cmd\cmd_gen_dao.go)的import包,取消注释即可。sqlite驱动依赖了gcc - link: "sqlite:./file.db" ``` ## 参数说明 | 名称 | 默认值 | 含义 | 示例 | | --- | --- | --- | --- | | `gfcli.gen.dao` | | `dao` 代码生成配置项,可以有多个配置项构成数组,支持多个数据库生成。不同的数据库可以设置不同的生成规则,例如可以生成到不同的位置或者文件。 | - | | `link`| | **必须参数**。分为两部分,第一部分表示连接的数据库类型 `mysql`, `postgresql` 等, 第二部分就是连接数据库的 `dsn` 信息。具体请参考 [ORM使用配置](../core/gdb-config.md) 章节。 | - | | `path` | `internal` | 生成 `dao` 和 `model` 文件的存储 **目录** 地址。 | `./app` | | `group` | `default` | 在数据库配置中的数据库分组名称。只能配置一个名称。数据库在配置文件中的分组名称往往确定之后便不再修改。 | `default`
    `order`
    `user` | | `prefix` | | 生成数据库对象及文件的前缀,以便区分不同数据库或者不同数据库中的相同表名,防止数据表同名覆盖。 | `order_`
    `user_` | | `removePrefix` | | 删除数据表的指定前缀名称。多个前缀以 `,` 号分隔。 | `gf_` | | `removeFieldPrefix` | | 删除字段名称的指定前缀名称。多个前缀以 `,` 号分隔。 | `f_` | | `tables` | | 指定当前数据库中需要执行代码生成的数据表。如果为空,表示数据库的所有表都会生成。**从版本v2.10.0开始,支持通配符模式**,可以使用 `*` 和 `?` 通配符匹配多个表名,例如 `user_*` 匹配所有以 `user_` 开头的表,`user_*, order_*` 匹配多组表。 | `user, user_detail`
    `user_*, order_*` | | `tablesEx` | | `Tables Excluding`,指定当前数据库中需要排除代码生成的数据表。**从版本v2.10.0开始,支持通配符模式**,可以使用 `*` 匹配任意数量字符(包括空字符),使用 `?` 匹配单个字符,例如 `temp_*` 排除所有以 `temp_` 开头的临时表,`test_?` 排除如 `test_1`、`test_a` 等单字符后缀的表。 | `product, order`
    `temp_*, test_?` | | `jsonCase` | `CamelLower` | 指定 `model` 中生成的数据实体对象中 `json` 标签名称规则,参数不区分大小写。参数可选为: `Camel`、 `CamelLower`、 `Snake`、 `SnakeScreaming`、 `SnakeFirstUpper`、 `Kebab`、 `KebabScreaming`。具体介绍请参考命名行帮助示例。 | `Snake` | | `stdTime` | `false` | 当数据表字段类型为时间类型时,代码生成的属性类型使用标准库的 `time.Time` 而不是框架的 `*gtime.Time` 类型。 | `true` | | `withTime` | `false` | 为每个自动生成的代码文件增加生成时间注释 | | | `gJsonSupport` | `false` | 当数据表字段类型为 `JSON` 类型时,代码生成的属性类型使用 `*gjson.Json` 类型。 | `true` | | `overwriteDao` | `false` | 每次生成 `dao` 代码时是否重新生成覆盖 `dao/internal` 目录外层的文件。注意 `dao/internal` 目录外层的文件可能由开发者自定义扩展了功能,覆盖可能会产生风险。 | `true` | | `importPrefix` | 通过 `go.mod` 自动检测 | 用于指定生成 `Go` 文件的 `import` 路径前缀。特别是针对于不是在项目根目录下使用 `gen dao` 命令,或者想要将代码文件生成到自定义的其他目录,这个时候配置该参数十分必要。 | `github.com/gogf/gf` | | `descriptionTag` | `false` | 用于指定是否为数据模型结构体属性增加 `description` 的标签,内容为对应的数据表字段注释。 | `true` | | `noJsonTag` | `false` | 生成的数据模型中,字段不带有json标签 | | | `noModelComment` | `false` | 用于指定是否关闭数据模型结构体属性的注释自动生成,内容为数据表对应字段的注释。 | `true` | | `clear` | `false` | 自动删除数据库中不存在对应数据表的本地 `dao/do/entity` 代码文件。请谨慎使用该参数。 | | | `daoPath` | `dao` | 代码生成的 `DAO` 文件存放目录 | | | `doPath` | `model/do` | 代码生成 `DO` 文件存放目录 | | | `entityPath` | `model/entity` | 代码生成的 `Entity` 文件存放目录 | | | `tablePath` | `table` | 代码生成的 `Table` 文件存放目录 | | | `tplDaoIndexPath` | | 自定义 `DAO Index` 代码生成模板文件路径,使用该参数请参考源码 | | | `tplDaoInternalPath` | | 自定义 `DAO Internal` 代码生成模板文件路径,使用该参数请参考源码 | | | `tplDaoDoPath` | | 自定义 `DO` 代码生成模板文件路径,使用该参数请参考源码 | | | `tplDaoEntityPath` | | 自定义 `Entity` 代码生成模板文件路径,使用该参数请参考源码 | | | `tplDaoTablePath` | | 自定义 `Table` 代码生成模板文件路径,使用该参数请参考源码 | | | `typeMapping` | | **从版本v2.5开始支持**。用于自定义数据表字段类型到生成的Go文件中对应属性类型映射。 | | | `fieldMapping` | | **从版本v2.8开始支持**。用于自定义数据表具体字段到生成的Go文件中对应属性类型映射。| | | `shardingPattern` | | **从版本v2.9开始支持**。用于自定义数据表分表规则。| | | `genTable` | `false` | **从版本v2.9.5开始支持**。用于控制是否生成数据库表字段定义文件。 每个表会生成一个对应的 `Go` 文件,文件中包含了该表所有字段的详细定义,如字段名、类型、索引、是否为空等信息。这些生成的文件主要用于 `gdb` 内部理解表结构,每张表都有一个 `SetXxxTableFields` 函数,可以将表字段定义注册到数据库实例中| `true` | ### 参数:`tables` 参数`tables`用于指定需要生成代码的数据表。**从版本v2.10.0开始,支持通配符模式**,可以使用通配符来匹配多个表名,简化配置。 #### 通配符支持 支持的通配符: - `*`:匹配任意数量的字符(包括空字符) - `?`:匹配单个字符 #### 使用场景 当数据库中存在大量具有相同前缀或模式的表时,使用通配符可以避免逐一列举表名,简化配置并提高维护效率。例如: - 用户相关表:`user_info`、`user_profile`、`user_settings` 等 - 订单相关表:`order_main`、`order_detail`、`order_log` 等 - 产品相关表:`product_category`、`product_item`、`product_stock` 等 #### 配置示例 **单个通配符模式:** ```yaml gfcli: gen: dao: - link: "mysql:root:12345678@tcp(127.0.0.1:3306)/test" tables: "user_*" # Match all tables starting with 'user_' ``` **多个通配符模式:** ```yaml gfcli: gen: dao: - link: "mysql:root:12345678@tcp(127.0.0.1:3306)/test" tables: "user_*, order_*, product_*" # Match multiple table groups ``` **混合使用:** ```yaml gfcli: gen: dao: - link: "mysql:root:12345678@tcp(127.0.0.1:3306)/test" tables: "user_*, order_main, product_?" # Mix wildcards and exact names ``` #### 通配符示例 假设数据库中有以下表: ```text user_info user_profile user_settings order_main order_detail product_a product_b admin_log ``` 不同通配符配置的匹配结果: | 配置 | 匹配的表 | | --- | --- | | `user_*` | `user_info`, `user_profile`, `user_settings` | | `order_*` | `order_main`, `order_detail` | | `product_?` | `product_a`, `product_b` | | `user_*, order_*` | `user_info`, `user_profile`, `user_settings`, `order_main`, `order_detail` | | `*_log` | `admin_log` | #### 注意事项 1. **版本要求**:通配符模式支持需要`GoFrame CLI`版本 >= `v2.10.0`。 2. **区别于分表**:通配符模式与 `shardingPattern` 参数不同。通配符用于选择需要生成代码的表,每个匹配的表都会生成独立的 `DAO` 文件;而 `shardingPattern` 用于将多个分片表识别为一个逻辑表,只生成一个 `DAO` 文件。 3. **优先级**:如果同时配置了 `tables` 和 `tablesEx`,会先匹配 `tables` 的通配符,然后排除 `tablesEx` 指定的表。 4. **性能考虑**:通配符模式会扫描数据库中的所有表进行匹配,如果数据库表数量非常多,建议使用更具体的模式以提高性能。 ### 参数:`typeMapping` 参数`typeMapping`支持配置数据库字段类型对应的`Go`数据类型,默认值为: ```yaml decimal: type: float64 money: type: float64 numeric: type: float64 smallmoney: type: float64 ``` 该配置支持通过`import`配置项引入第三方包,例如: ```yaml decimal: type: decimal.Decimal import: github.com/shopspring/decimal ``` ### 参数:`fieldMapping` 参数`fieldMapping`提供细粒度的字段类型映射配置,支持配置指定数据库字段生成的`Go`数据类型。除了配置名称不一样外,配置内容与`typeMapping`一致。配置示例: ```yaml paid_orders.amount: type: decimal.Decimal import: github.com/shopspring/decimal ``` 示例中,`paid_orders`为表名称,`amount`为字段名称,`type`表示生成的`Go`代码中对应的数据类型名称,`import`表示生成的代码中需要引入第三方包。 ### 参数:`shardingPattern` 参数`shardingPattern`用于配置分库分表规则。该参数在`v2.9`版本中新增,用于识别和处理分片表(`Sharding Tables`)。分片表是指按照某种规则拆分的多个具有相同结构的数据表,例如按照时间分片的`orders_202301`、`orders_202302`等表,或者按照用户ID分片的`users_0001`、`users_0002`等表。 #### 参数说明 - 类型:字符串数组 - 格式:使用`?`作为通配符来匹配表名中的分片部分 - 作用:将多个匹配同一模式的表识别为同一个逻辑表,并生成支持分片的`DAO`代码 #### 工作原理 当使用`shardingPattern`参数时,`gen dao`命令会: 1. 根据提供的模式匹配数据库中的表名 2. 将匹配同一模式的多个表识别为同一个逻辑表 3. 移除表名中的分片标识符部分 4. 为该逻辑表生成支持分片的`DAO`代码 #### 参数示例 假设数据库中有以下表: ```text users_0001 users_0002 users_0003 products ``` 使用以下`shardingPattern`配置: ```yaml gendao: - link: "mysql:root:12345678@tcp(127.0.0.1:3306)/test" shardingPattern: - "users_?" ``` 生成的结果将包含: 1. 一个名为`users.go`的`DAO`文件,而不是为每个分片表生成单独的`DAO`文件 2. 该`DAO`文件中会包含分片配置代码,自动处理对不同分片表的操作 3. 产品表会正常生成`products.go`的`DAO`文件 生成的`DAO`文件示例: ```go // ... type usersDao struct { *internal.UsersDao } var ( // Users is a globally accessible object for table users_0001 operations. Users = usersDao{internal.NewUsersDao(userShardingHandler)} ) // userShardingHandler is the handler for sharding operations. // You can fill this sharding handler with your custom implementation. func userShardingHandler(m *gdb.Model) *gdb.Model { m = m.Sharding(gdb.ShardingConfig{ Table: gdb.ShardingTableConfig{ Enable: true, Prefix: "", // Replace Rule field with your custom sharding rule. // Or you can use "&gdb.DefaultShardingRule{}" for default sharding rule. Rule: nil, }, Schema: gdb.ShardingSchemaConfig{}, }) return m } // ... ``` 其中,需要自行设定`DAO`文件中的`userShardingHandler`函数中`Sharding`的分表规则,也可以设定分库规则。如果生成分表`DAO`文件但是未手动设置分表规则,直接调用`DAO`对象将会报错。 #### 多个分片模式 可以同时指定多个分片模式的配置示例: ```yaml gendao: - link: "mysql:root:12345678@tcp(127.0.0.1:3306)/test" shardingPattern: - "users_?" - "orders_?" ``` 这将同时处理`users_`和`orders_`前缀的分片表,生成`users.go`和`orders.go`的`DAO`文件。 #### 注意事项 1. 分片表必须具有相同的表结构。 2. 分片标识符可以是数字、日期或其他格式,只要能用`?`通配符匹配即可。 3. 生成的`DAO`代码会自动包含所有匹配的分片表,如果后续添加新的分片表,需要重新生成`DAO`代码。 4. 需要在生成的`DAO`文件中,为每个分片表添加`Sharding`方法,用于指定分表规则,具体请参考章节:[ORM分库分表-分表特性](../core/gdb-sharding-table.md)。 ### 参数:`genTable` 参数`genTable`用于控制是否生成数据库表字段定义文件。该参数在`v2.9.5`版本中新增,每个表会生成一个对应的 `Go` 文件,文件中包含了该表所有字段的详细定义,如字段名、类型、索引、是否为空等信息。这些生成的文件主要用于 `gdb` 内部理解表结构,其中包含一个 `SetXxxTableFields` 函数。 向数据库实例中注册表字段信息可以让`gdb`构建`sql`不再依赖于实体数据库连接,方便用户使用`ToSQL`和`CatchSQL`直接获取最终`sql`。 生成`Table`文件示例: ```go // ================================================================================= // This file is auto-generated by the GoFrame CLI tool. You may modify it as needed. // ================================================================================= package table import ( "context" "github.com/gogf/gf/v2/database/gdb" ) // User defines the fields of table "user" with their properties. // This map is used internally by GoFrame ORM to understand table structure. var User = map[string]*gdb.TableField{ "id": { Index: 0, Name: "id", Type: "bigint", Null: false, Key: "PRI", Default: nil, Extra: "auto_increment", Comment: "", }, "tenant_id": { Index: 1, Name: "tenant_id", Type: "varchar(64)", Null: false, Key: "MUL", Default: nil, Extra: "", Comment: "", }, "username": { Index: 2, Name: "username", Type: "varchar(100)", Null: false, Key: "", Default: nil, Extra: "", Comment: "", }, "email": { Index: 3, Name: "email", Type: "varchar(150)", Null: true, Key: "", Default: nil, Extra: "", Comment: "", }, "created_at": { Index: 4, Name: "created_at", Type: "datetime(3)", Null: false, Key: "", Default: "CURRENT_TIMESTAMP(3)", Extra: "DEFAULT_GENERATED", Comment: "", }, "updated_at": { Index: 5, Name: "updated_at", Type: "datetime(3)", Null: false, Key: "", Default: "CURRENT_TIMESTAMP(3)", Extra: "DEFAULT_GENERATED on update CURRENT_TIMESTAMP(3)", Comment: "", }, "deleted_at": { Index: 6, Name: "deleted_at", Type: "datetime(3)", Null: true, Key: "MUL", Default: nil, Extra: "", Comment: "", }, } // SetUserTableFields registers the table fields definition to the database instance. // db: database instance that implements gdb.DB interface. // schema: optional schema/namespace name, especially for databases that support schemas. func SetUserTableFields(ctx context.Context, db gdb.DB, schema ...string) error { return db.GetCore().SetTableFields(ctx, "user", User, schema...) } ``` ## 使用示例 仓库地址: [https://github.com/gogf/focus-single](https://github.com/gogf/focus-single) 1、以下 `3` 个目录的文件由 `dao` 命令生成: | 路径 | 说明 | 详细介绍 | | --- | --- | --- | | `/internal/dao` | 数据操作对象 | 通过对象方式访问底层数据源,底层基于 `ORM` 组件实现。往往需要结合 `entity` 和 `do` 共同使用。该目录下的文件开发者可扩展修改。 | | `/internal/model/do` | 数据转换模型 | 数据转换模型用于业务模型到数据模型的转换,由工具维护,用户不能修改。工具每次生成代码文件将会覆盖该目录。关于 `do` 文件的介绍请参考:
    - [数据模型与业务模型](../design/project-models.md)
    - [DAO-工程痛点及改进](../design/project-dao-improvement.md)
    - [利用指针属性和do对象实现灵活的修改接口](../core/gdb-practice-using-pointer-and-do-for-update-api.md) | | `/internal/model/entity` | 数据模型 | 数据模型由工具维护,用户不能修改。工具每次生成代码文件将会覆盖该目录。 | 2、 `model` 中的模型分为两类: **数据模型** 和 **业务模型**。 **数据模型:** 通过 `CLI` 工具自动生成 `model/entity` 目录文件,数据库的数据表都会生成到该目录下,这个目录下的文件对应的模型为数据模型。数据模型即与数据表一一对应的数据结构,开发者往往不需要去修改并且也不应该去修改,数据模型只有在数据表结构变更时通过 `CLI` 工具自动更新。数据模型由 `CLI` 工具生成及统一维护。 **业务模型:** 业务模型即是与业务相关的数据结构,按需定义,例如 `service` 的输入输出数据结构定义、内部的一些数据结构定义等。业务模型由开发者根据业务需要自行定义维护,定义到 `model` 目录下。 3、 `dao` 中的文件按照数据表名称进行命名,一个数据表一个文件及其一个对应的 `DAO` 对象。操作数据表即是通过 `DAO` 对象以及相关操作方法实现。 `dao` 操作采用规范化设计,必须传递 `ctx` 参数,并在生成的代码中必须通过 `Ctx` 或者 `Transaction` 方法创建对象来链式操作数据表。 ## 注意事项 ### 需要手动编译的数据库类型 `gen dao` 命令涉及到数据访问相关代码生成时,默认支持常用的若干类型数据库。如果需要 `Oracle` 数据库类型支持,需要开发者自己修改源码文件后自行本地手动编译生成 `CLI` 工具随后安装,因为这两个数据库的驱动需要 `CGO` 支持,无法预编译生成给大家直接使用。 ### 关于 `bool` 类型对应的数据表字段 由于大部分数据库类型都没有 `bool` 类型的数据表字段类型, 我们推荐使用`bit(1)`来表示字段的`bool`类型,而非`tinyint(1)`或者`int(1)`。因为`tinyint(1)/int(1)`字段类型表示的范围是`-127~127`,通常可能会被用作状态字段类型。而`bit(1)`的类型范围为`0/1`,可以很好的表示`bool`类型的两个值`false/true`。 例如,表字段: 生成的属性: # 模块规范-gen service **注意** 该功能特性为 **实验性特性**。建议开发者以 `logic` 下的模块划分为主,梳理模块间关联关系,避免循环依赖,充分利用 `Golang` 编译器的循环依赖检测特性编写更高质量的项目代码。 **提示** 该功能特性从 `v2.1` 版本开始提供。 ## 基本介绍 ### 设计背景 在业务项目实践中,业务逻辑封装往往是最复杂的部分,同时,业务模块之间的依赖十分复杂、边界模糊,较难采用 `Golang` 包管理的形式。如何有效管理项目中的业务逻辑封装部分,对于每个采用 `Golang` 开发的项目都是必定会遇到的难题。 在标准的软件设计流程中,模块与模块之间的依赖会先明确接口定义,在软件开发的实施过程中再通过代码来具体实现。但在大部分高节奏的互联网工程下,并没有严谨的软件设计流程,甚至开发人员的质量水平也参差不齐,大部分开发人员首先关心的是如何去实现需求场景对应的功能逻辑,尽可能地提高开发效率。 ### 设计目标 1. 提供一种代码管理方式,可以通过具体模块实现直接生成模块接口定义、模块注册代码。 2. 简化业务逻辑实现与接口分离的实现,降低模块方法与接口定义的重复操作,提高模块与模块之间的透明度与调用便捷性。 ### 设计实现 1. 增加 `logic` 分类目录,将所有业务逻辑代码迁移到 `logic` 分类目录下,采用包管理形式来管理业务模块。 2. 业务模块之间的依赖通过接口化解耦,将原有的 `service` 分类调整为接口目录。这样每个业务模块将会各自维护、更加灵活。 3. 可以按照一定的编码规范,从 `logic` 业务逻辑代码生成 `service` 接口定义代码。同时,也允许人工维护这部分 `service` 接口。 ## 注意事项 **注意** 再次提醒,通过 `logic` 实现去生成 `service` 接口 **并不是一个代码管理的标准化做法**,只是提供另一个 **可供选择的**、便捷的代码管理方式。这种管理方式有优点也有缺点,优点是针对微服务场景的业务模块的接口自动生成比较方便;缺点是无法识别语法继承关系、无法生成父级嵌套类型的方法、抛弃了 `Golang` 编译时检测循环依赖的特性。 **框架的工程管理当然也支持标准的接口代码管理方式**,即支持先定义 `service` 接口,再编码 `logic` 具体实现。需要注意的是,这个 `service` 的源代码中不能出现顶部工具的注释信息(工具依靠这个注释来判断该文件是否可覆盖),很多同学复制粘贴的时候把文件顶部注释保留了,就会引起手动维护接口文件失效。具体见截图注释: ## 命令使用 该命令通过分析给定的 `logic` 业务逻辑模块目录下的代码,自动生成 `service` 目录接口代码。 **说明** 需要注意: 1. 由于该命令是根据业务模块生成 `service` 接口,因此只会解析二级目录下的 `go` 代码文件,并不会无限递归分析代码文件。以 `logic` 目录为例,该命令只会解析 `logic/xxx/*.go` 文件。因此,需要 `logic` 层代码结构满足一定规范。 2. 不同业务模块中定义的结构体名称在生成的 `service` 接口名称时可能会重复覆盖,因此需要在设计业务模块时保证名称不能冲突。 该命令的示例项目请参考: [https://github.com/gogf/gf-demo-user](https://github.com/gogf/gf-demo-user) ### 手动模式 如果是手动执行命令行,直接在项目根目录下执行 `gf gen service` 即可。 ```text $ gf gen service -h USAGE gf gen service [OPTION] OPTION -s, --srcFolder source folder path to be parsed. default: internal/logic -d, --dstFolder destination folder path storing automatically generated go files. default: internal/service -f, --dstFileNameCase destination file name storing automatically generated go files, cases are as follows: | Case | Example | |---------------- |--------------------| | Lower | anykindofstring | | Camel | AnyKindOfString | | CamelLower | anyKindOfString | | Snake | any_kind_of_string | default | SnakeScreaming | ANY_KIND_OF_STRING | | SnakeFirstUpper | rgb_code_md5 | | Kebab | any-kind-of-string | | KebabScreaming | ANY-KIND-OF-STRING | -w, --watchFile used in file watcher, it re-generates all service go files only if given file is under srcFolder -a, --stPattern regular expression matching struct name for generating service. default: ^s([A-Z]\\w+)$ -p, --packages produce go files only for given source packages -i, --importPrefix custom import prefix to calculate import path for generated importing go file of logic -l, --clear delete all generated go files that are not used any further -h, --help more information about this command EXAMPLE gf gen service gf gen service -f Snake ``` **提示** 如果使用框架推荐的项目工程脚手架,并且系统安装了 `make` 工具,也可以使用 `make service` 快捷指令。 参数说明: | 名称 | 必须 | 默认值 | 含义 | | --- | --- | --- | --- | | `srcFolder` | 是 | `internal/logic` | 指向logic代码目录地址 | | `dstFolder` | 是 | `internal/service` | 指向生成的接口文件存放目录 | | `dstFileNameCase` | 否 | `Snake` | 生成的文件名名称格式 | | `stPattern` | 否 | `s([A-A]\w+)` | 使用正则指定业务模块结构体定义格式,便于解析业务接口定义名称。在默认的正则下,所有小写 `s` 开头,大写字母随后的结构体都将被当做业务模块接口名称。例如: | logic结构体名称 | service接口名称 | | --- | --- | | `sUser` | `User` | | `sMetaData` | `MetaData` | | | `watchFile` | | | 用在代码文件监听中,代表当前改变的代码文件路径 | | `packages` | | | 仅生成指定包名的接口文件,给定字符串数组,通过命令行传参则给定 `JSON` 字符串,命令行组件自动转换数据类型 | | `importPrefix` | | | 指定生成业务引用文件中的引用包名前缀 | | `overwrite` | | `true` | 生成代码文件时是否覆盖已有文件 | | `clear` | | `false` | 自动删除 `logic` 中不存在的接口文件(仅删除自动维护的文件) | ### 自动模式 #### `Goland/Idea` 如果是使用的 `GolandIDE`,那么可以使用我们提供的配置文件: [watchers.xml](gen-service-watchers.xml)  自动监听代码文件修改时自动生成接口文件。使用方式,如下图: #### `Visual Studio Code` 如果是使用的 `Visual Studio Code`,那么可以安装插件 [RunOnSave](https://marketplace.visualstudio.com/items?itemName=emeraldwalk.RunOnSave) 随后配置插件: ```json "emeraldwalk.runonsave": { "commands": [ { "match": ".*logic.*go", "isAsync": true, "cmd": "gf gen service" } ] } ``` ## 具体使用手摸手 ### Step1:引入我们提供的配置 我们建议在使用 `Goland IDE` 时,使用我们提供的配置文件: [watchers.xml](gen-service-watchers.xml) ### Step2:编写的业务逻辑代码 ### Step3:生成接口及服务注册文件 如果已经按照 `Step1` 做好了配置,那么这一步可以忽略。因为在编写代码的时候, `service` 便同时生成了接口定义文件。 否则,每一次在开发/更新完成 `logic` 业务模块后,需要手动执行一下 `gf gen service` 命令。 ### Step4:注意服务的实现注入部分(仅一次) 只有在生成完成接口文件后,才能在每个业务模块中加上接口的具体实现注入。该方法每个业务模块加一次即可。 ### Step5:在启动文件中引用接口实现注册(仅一次) 可以发现,该命令除了生成接口文件之外,还生成了一个接口实现注册文件。该文件用于在程序启动时,将接口的具体实现在启动时执行注册。 该文件的引入需要在 `main` 包的最顶部引入,需要注意 `import` 的顺序,放到最顶部,后面加一个空行。如果同时存在 `packed` 包的引入,那么放到 `packed` 包后面。像这样: ### Step6:Start&Enjoy 启动 `main.go` 即可。 ## 常见问题FAQ ### 当 `logic` 中的结构体存在嵌套时,无法自动生成嵌套类型的方法 这种场景建议手动维护 `service` 接口定义,不使用工具的自动生成。手动维护的接口定义文件不会被工具覆盖,手动和自动可以同时使用。 ### 快速定位接口的具体实现 **项目业务模块采用接口化解耦后体验非常棒。但是我在开发和调试过程中,想要快速找到指定接口的具体实现有点困难,能给点指导思路吗?** \> 这里我推荐使用 `Goland IDE`,有个很棒的接口实现定位功能,具体如图。找到接口定义后,点击左边的小图标可快速定位具体的实现。如果Goland不显示小图标,可以尝试升级使用最新版本的 `Goland` 哈。 或者在左侧没有小图标的时候,可以右键选择 `Go To → Implementation(s)` # 枚举维护-gen enums **提示** 该功能特性为 **实验性特性**,从 `v2.4` 版本开始提供。 ## 基本介绍 该命令用于分析指定代码目录源码,按照规范生成枚举值信息以及 `Go` 代码文件, **主要用以完善 `OpenAPIv3` 文档中的枚举值维护**。 ## 解决痛点 ### 痛点描述 - `API` 文档中枚举值类型参数不展示枚举值可选项的问题。 - `API` 文档中的枚举值维护困难的问题,代码与文档脱离维护的问题。降低了与调用端,特别是前后端的协作效率 > 例如,以下接口定义中,任务包含多种状态,这些状态都是枚举值,如果后端来维护成本比较高,并且容易遗漏状态的维护,造成状态枚举值不完整。 ### 痛点解决 通过工具解析源码,将枚举值解析生成到启动包 `Go` 文件中,在服务运行时自动加载枚举值,降低手工维护成本,避免枚举值遗漏维护问题。 > 例如,在以下接口定义中,通过工具来维护枚举值,提高了开发效率。 ## 命令使用 ```text $ gf gen enums -h USAGE gf gen enums [OPTION] OPTION -s, --src source folder path to be parsed -p, --path output go file path storing enums content -x, --prefixes only exports packages that starts with specified prefixes -h, --help more information about this command EXAMPLE gf gen enums gf gen enums -p internal/boot/boot_enums.go gf gen enums -p internal/boot/boot_enums.go -s . gf gen enums -x github.com/gogf ``` 参数说明: | 名称 | 必须 | 默认值 | 含义 | | --- | --- | --- | --- | | `src` | 否 | `.` | 指定分析的源码目录路径,默认为当前项目根目录 | | `path` | 否 | `internal/boot/boot_enums.go` | 指定生成的枚举值注册Go代码文件路径 | | `prefixes` | 否 | - | 只会生成包名称前缀的带有指定关键字的枚举值,支持多个前缀配置 | **说明** 该命令底层使用了`AST`解析实现,将会递归分析所有依赖包的`enums`定义。如果业务项目依赖较复杂,生成`enums`可能会比较多,但绝大部分时候我们只关心自身项目或者个别依赖包的`enums`定义,那么这个时候可以使用`prefixes`参数来控制只生成特定包名前缀的`enums`。 工具配置项`yaml`格式示例: ```yaml title="hack/config.yaml" gfcli: gen: enums: src: "api" path: "internal/boot/boot_enums.go" prefixes: - github.com/gogf - myexample/project ``` ## 生成文件的使用 执行 `gf gen enums` 命令生成枚举分析文件 `internal/boot/boot_enums.go`,生成文件之后,需要在项目入口文件匿名引入: ```go import ( _ "项目模块名/internal/boot" ) ``` ## 扩展阅读 ### 如何规范定义枚举值 请参考章节: [Golang枚举值管理](../design/enums.md) ### 如何对枚举值进行校验 如果规范化定义了枚举值,并且通过命令生成了枚举值维护文件,那么在参数校验中可以使用 `enums` 规则对枚举值字段进行校验,具体规则介绍请参考章节: [数据校验-校验规则](../core/gvalid-rules.md) # 协议编译-gen pb **提示** 该功能特性从 `v2.4` 版本开始提供。 ## 基本介绍 该命令用于编译 `proto` 文件,生成对应的 `protobuf go` 文件以及对应的控制器文件。 ## 命令使用 ```text $ gf gen pb -h USAGE gf gen pb [OPTION] OPTION -p, --path protobuf file folder path -a, --api output folder path storing generated go files of api -c, --ctrl output folder path storing generated go files of controller -h, --help more information about this command EXAMPLE gf gen pb gf gen pb -p . -a . -p . ``` **提示** 如果使用框架推荐的项目工程脚手架,并且系统安装了 `make` 工具,也可以使用 `make pb` 快捷指令。 参数说明: | 名称 | 必须 | 默认值 | 含义 | | --- | --- | --- | --- | | `path` | 否 | `manifest/protobuf` | 指向 `proto` 协议定义文件 | | `api` | 否 | `api` | 指向生成的接口文件存放目录 | | `ctrl` | 否 | `internal/controller` | 指向生成的控制器文件存放目录 | ## 注意事项 - 在生成控制器文件时,会自动识别是否已经存在对应的接口实现方法,如果已经存在则不再重复生成对应的接口方法,防止覆盖。 - 如果在 `proto` 目录执行该命令,并且指定的 `path` 目录不存在时,那么将会自动编译本地 `proto` 文件,且编译后的文件生成到当前目录,并自动关闭控制器文件的生成功能。 # 数据表PB-gen pbentity **提示** 该功能特性从 `v2.4` 版本开始提供。 ## 基本介绍 该命令用于读取配置的数据库,根据数据表生成对应的 `proto` 数据结构文件。 ## 命令使用 ```text $ gf gen pbentity -h USAGE gf gen pbentity [OPTION] OPTION -p, --path directory path for generated files storing -k, --package package path for all entity proto files -l, --link database configuration, the same as the ORM configuration of GoFrame -t, --tables generate models only for given tables, multiple table names separated with ',' -f, --prefix add specified prefix for all entity names and entity proto files -r, --removePrefix remove specified prefix of the table, multiple prefix separated with ',' -rf, --removeFieldPrefix remove specified prefix of the field, multiple prefix separated with ',' -n, --nameCase case for message attribute names, default is "Camel": | Case | Example | |---------------- |--------------------| | Camel | AnyKindOfString | | CamelLower | anyKindOfString | default | Snake | any_kind_of_string | | SnakeScreaming | ANY_KIND_OF_STRING | | SnakeFirstUpper | rgb_code_md5 | | Kebab | any-kind-of-string | | KebabScreaming | ANY-KIND-OF-STRING | -j, --jsonCase case for message json tag, cases are the same as "nameCase", default "CamelLower". set it to "none" to ignore json tag generating. -o, --option extra protobuf options -h, --help more information about this command EXAMPLE gf gen pbentity gf gen pbentity -l "mysql:root:12345678@tcp(127.0.0.1:3306)/test" gf gen pbentity -p ./protocol/demos/entity -t user,user_detail,user_login gf gen pbentity -r user_ -k github.com/gogf/gf/example/protobuf gf gen pbentity -r user_ CONFIGURATION SUPPORT Options are also supported by configuration file. It's suggested using configuration file instead of command line arguments making producing. The configuration node name is "gf.gen.pbentity", which also supports multiple databases, for example(config.yaml): gfcli: gen: - pbentity: link: "mysql:root:12345678@tcp(127.0.0.1:3306)/test" path: "protocol/demos/entity" tables: "order,products" package: "demos" - pbentity: link: "mysql:root:12345678@tcp(127.0.0.1:3306)/primary" path: "protocol/demos/entity" prefix: "primary_" tables: "user, userDetail" package: "demos" option: | option go_package = "protobuf/demos"; option java_package = "protobuf/demos"; option php_namespace = "protobuf/demos"; typeMapping: json: type: google.protobuf.Value import: google/protobuf/struct.proto jsonb: type: google.protobuf.Value import: google/protobuf/struct.proto ``` **提示** 如果使用框架推荐的项目工程脚手架,并且系统安装了 `make` 工具,也可以使用 `make pbentity` 快捷指令。 参数说明: | 名称 | 默认值 | 含义 | 示例 | | --- | --- | --- | --- | | `gfcli.gen.pbentity` | | 代码生成配置项,可以有多个配置项构成数组,支持多个数据库生成。不同的数据库可以设置不同的生成规则,例如可以生成到不同的位置或者文件。 | - | | `path` | `manifest/protobuf/pbentity` | 生成 `proto` 文件的存储 **目录** 地址。 | `protobuf/pbentity` | | `package` | 自动识别 `go.mod` | 生成的 `proto` 文件中的 `go_package` 路径,并自动识别 `package` 名称 | - | | `link` | | 分为两部分,第一部分表示连接的数据库类型 `mysql`, `postgresql` 等, 第二部分就是连接数据库的 `dsn` 信息。具体请参考 [ORM使用配置](../core/gdb-config.md) 章节。 | - | | `prefix` | | 生成数据库对象及文件的前缀,以便区分不同数据库或者不同数据库中的相同表名,防止数据表同名覆盖。 | `order_`
    `user_` | | `removePrefix` | | 删除数据表的指定前缀名称。多个前缀以 `,` 号分隔。 | `gf_` | | `removeFieldPrefix` | | 删除字段名称的指定前缀名称。多个前缀以 `,` 号分隔。 | `f_` | | `tables` | | 指定当前数据库中需要执行代码生成的数据表。如果为空,表示数据库的所有表都会生成。 | `user, user_detail` | | `nameCase` | `CamelLower` | 生成的 `message` 属性字段名称格式。参数可选为: `Camel`、 `CamelLower`、 `Snake`、 `SnakeScreaming`、 `SnakeFirstUpper`、 `Kebab`、 `KebabScreaming`。具体介绍请参考命名行帮助示例。 | `Snake` | | `option` | | 额外的 `proto option` 配置列表 | | | `typeMapping` | | 用于自定义数据表字段类型到生成的Go文件中对应属性类型映射 | | | `fieldMapping` | | 用于自定义数据表具体字段到生成的Go文件中对应属性类型映射 | | ### 参数:`typeMapping` 参数`typeMapping`支持配置数据库字段类型对应的`Go`数据类型,默认值为: ```yaml decimal: type: float64 money: type: float64 numeric: type: float64 smallmoney: type: float64 ``` 该配置支持通过`import`配置项引入第三方包,例如: ```yaml decimal: type: decimal.Decimal import: github.com/shopspring/decimal ``` ### 参数:`fieldMapping` 参数`fieldMapping`提供细粒度的字段类型映射配置,支持配置指定数据库字段生成的`Go`数据类型。除了配置名称不一样外,配置内容与`typeMapping`一致。配置示例: ```yaml paid_orders.amount: type: decimal.Decimal import: github.com/shopspring/decimal ``` 示例中,`paid_orders`为表名称,`amount`为字段名称,`type`表示生成的`Go`代码中对应的数据类型名称,`import`表示生成的代码中需要引入第三方包。 ## 与 `gen dao` 中的 `entity` 差别 ### 相同之处 - 两者生成的内容都是 `entity` 内容,即从数据集合(数据库表)中生成对应的 `Golang` 实体对象供程序方便使用。并且都是单向生成,即只能从数据集合生成实体对象代码,以保证实体对象数据结构的同步。 - `gen dao` 生成的 `entity` 数据实体对象是对于 `Golang` 语言来说是通用的,但目前主要为 `HTTP` 协议服务。在 `HTTP` 服务中, `gen dao` 中生成的 `entity` 虽然是在 `internal` 目录下,但最终也会作为 `HTTP API` 返回的一部分服务客户端。 ### 不同之处 - 在 `GRPC` 服务中, `gen dao` 生成的 `entity` 数据结构无法提供给 `GRPC` 接口使用,因为 `GRPC` 的数据结构需要使用 `proto` 文件来定义。因此,在 `GRPC` 服务中就需要使用到 `gen pbentity` 中生成的 `pbentity proto` 文件。同时,在 `GRPC` 微服务开发中, `gen dao` 生成的 `entity` 已经没有具体作用。 - 取名 `pbentity` 而不是 `entity` 的名称,是为了防止和 `gen dao` 中的 `entity` 含义冲突。 # 独立部署 使用 `GoFrame` 开发的应用程序可以独立地部署到服务器上,设置为后台守护进程运行即可。这种模式常用在简单的API服务项目中。 服务器我们推荐使用 `*nix` 服务器系列(包括: `Linux`, `MacOS`, `*BSD`),以下使用 `Ubuntu` 系统为例,介绍如何部署使用 `GoFrame` 框架开发的项目。 ## \*nix ### 1\. `nohup` 我们可以使用简单的 `nohup` 命令来运行应用程序,使其作为后台守护进程运行,即使远程连接的SSH断开也不会影响程序的执行。在流行的Linux发行版中往往都默认安装好了 `nohup` 命令工具。 命令如下: ```bash nohup ./gf-app & ``` ### 2\. `tmux` `tmux` 是一款 `Linux` 下的终端复用工具,可以开启不同的终端窗口来将应用程序作为后台守护进程执行,即使远程连接的 `SSH` 断开也不会影响程序的执行。 在 `ubuntu` 系统下直接使用 `sudo apt-get install tmux` 安装即可。使用以下步骤将应用程序后台运行。 1. `tmux new -s gf-app`; 2. 在新终端窗口中执行 `./gf-app` 即可; 3. 使用 `ctrl` \+ `B & D` 快捷键可以退出当前终端窗口; 4. 使用 `tmux attach -t gf-app` 可进入到之前的终端窗口; ### 3\. `supervisor` `supervisor` 是用 `Python` 开发的一套通用的进程管理程序,能将一个普通的命令行进程变为后台 `daemon`,并监控进程状态,异常退出时能自动重启。官方网站: [http://supervisord.org/](http://supervisord.org/) 常见配置如下: ```ini [program:gf-app] user = root directory = /var/www command = /var/www/main stdout_logfile = /var/log/gf-app-stdout.log stderr_logfile = /var/log/gf-app-stderr.log autostart = true autorestart = true ``` 使用步骤如下: 1. 使用 `sudo service supervisor start` 启动 `supervisor` 服务; 2. 创建应用配置文件 `/etc/supervisor/conf.d/gf-app.conf`, 内容如上; 3. 使用 `sudo supervisorctl` 进入 `supervisor` 管理终端; 4. 使用 `reload` 重新读取配置文件并重启当前 `supoervisor` 管理的所有进程; 5. 也可以使用 `update` 重新加载配置(默认不重启),随后使用 `start gf-app` 启动指定的应用程序; 6. 随后可以使用 `status` 指令查看当前 `supervisor` 管理的进程状态; 踩坑分享经验: 1. `conf` 配置文件变更后需要在 `supervisorctl` 中执行 `reload` 才能更新使用到最新的配置。 2. `directory` 配置一般是不能缺少的,指定当前的工作目录路径,且必须配置在 `command` 之前。 3. `command` 命令需要使用绝对路径,否则会找不到执行文件。 ### 4\. `systemctl` `Systemd` 是 `Linux` 系统工具,用来启动守护进程,已成为大多数发行版的标准配置。 而 `systemctl` 是 `Systemd` 的主命令,用于管理系统。可以参考 [阮一峰对于 Systemd 的解读](http://www.ruanyifeng.com/blog/2016/03/systemd-tutorial-commands.html),文章的第四、五章节。 其实我们大部分服务都有使用 `systemctl` 管理,比如 `MySQL、Nginx` 等等。 常见配置如下: ```ini [Unit] # 单元描述 Description=GF APP # 在什么服务启动之后再执行本程序 After=mysql.service [Service] Type=simple # 程序执行的目录 WorkingDirectory=/data/server/gfapp/ # 启动的脚本命令 ExecStart=/data/server/gfapp/gfapp # 重启条件 Restart=always # 几秒后重启 RestartSec=5 [Install] WantedBy=multi-user.target ``` 使用方法: 1. 创建应用配置文件 `/etc/systemd/system/gfapp.service`, 内容如上; 2. 使用 `systemctl daemon-reload` 重新加载服务; 3. 执行 `systemctl start gfapp` 来启动服务; 4. 最后执行 `systemctl status gfapp` 来查看服务运行的状态信息; 5. 执行 `systemctl enable gfapp` 将服务添加到开机启动项; 6. 注意:执行的 `gfapp` 是使用文件名作为服务名; 7. 常见的命令有: `start(启动), stop(停止), restart(重启), status(查看运行状态), enable(添加到开机启动项), disable(将程序从开机启动中移除)` ### 5\. `screen` `Screen` 是一款由 `GNU` 计划开发的用于命令行终端切换的自由软件。用户可以通过该软件同时连接多个本地或远程的命令行会话,并在其间自由切换。 `GNU Screen` 可以看作是窗口管理器的命令行界面版本。它提供了统一的管理多个会话的界面和相应的功能。 安装方式: `sudo apt install -y screen `( `debian` 系列) `sudo yum install -y screen`  ( `centos`) **常用参数:** 1. `screen -S yourname` -\> 新建一个叫 yourname 的 session 2. `screen -ls` -\> 列出当前所有的 session 3. `screen -r yourname` -\> 回到 yourname 这个 session 4. `screen -d yourname` -\> 远程detach某个 session 5. `screen -d -r yourname` -\> 结束当前 session 并回到 yourname 这个 session **使用方法:** 1. 使用命令 `screen -S gfapp` 创建一个 session; 2. 在新终端窗口中执行 `./gf-app` 即可; 3. 执行 `ctrl-a, ctrl-d` 暂时离开当前session; 4. 执行 `screen -r gfapp` 返回命令窗口; 若返回不成功, 可能是该窗口被占用( `Attached`)了, 可以尝试使用 `screen -Dr gfapp`; 5. 执行 `screen -X -S gfapp quit` 结束程序; ## windows ### 1. `NSSM` [小巧又实用的NSSM封装windows服务工具介绍 - 知乎 (zhihu.com)](https://zhuanlan.zhihu.com/p/455904037) # 代理部署 代理部署即前置一层第三方的 `WebServer` 服务器处理所有的请求,将部分请求(往往是动态处理请求)有选择性地转交给后端的 `Golang` 应用程序执行,后端部署的 `Golang` 应用程序可以配置有多个。这种模式常用在复杂的 `WebServer` 配置中,常见的场景例如:需要静态文件分离、需要配置多个域名及证书、需要自建负载均衡层,等等。 虽然 `Golang` 实现的 `WebServer` 也能够处理静态文件,但是相比较于专业性的 `WebServer` 如 `nginx`/ `apache` 来说比较简单,性能也较弱。因此不推荐使用 `Golang WebServer` 作为前端服务直接处理静态文件请求。 ## `Nginx` 我们推荐使用 `Nginx` 作为反向代理的前端接入层,有两种配置方式实现动静态请求的拆分。 ### 静态文件后缀 这种方式通过文件名后缀区分,将指定的静态文件转交给 `nginx` 处理,其他的请求转交给 `golang` 应用。 配置示例如下: ```conf server { listen 80; server_name goframe.org; access_log /var/log/gf-app-access.log; error_log /var/log/gf-app-error.log; location ~ .*\.(gif|jpg|jpeg|png|js|css|eot|ttf|woff|svg|otf)$ { access_log off; expires 1d; root /var/www/gf-app/public; try_files $uri @backend; } location / { try_files $uri @backend; } location @backend { proxy_pass http://127.0.0.1:8199; proxy_redirect off; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; } } ``` 其中, `8199` 为本地的 `golang` 应用 `WebServer` 监听端口。 例如,在该配置下,我们可以通过 `http://goframe.org/my.png` 访问到指定的静态文件。 ### 静态文件目录 这种方式通过文件目录区分,将指定目录的访问请求转交给 `nginx` 处理,其他的请求转交给 `golang` 应用。 配置示例如下: ```conf server { listen 80; server_name goframe.org; access_log /var/log/gf-app-access.log; error_log /var/log/gf-app-error.log; location ^~ /public { access_log off; expires 1d; root /var/www/gf-app; try_files $uri @backend; } location / { try_files $uri @backend; } location @backend { proxy_pass http://127.0.0.1:8199; proxy_redirect off; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; } } ``` 其中, `8199` 为本地的 `golang` 应用 `WebServer` 监听端口。 例如,在该配置下,我们可以通过 `http://goframe.org/public/my.png` 访问静态文件。 # 容器部署 容器部署即使用 `docker` 化部署 `golang` 应用程序,这是在云服务时代最流行的部署方式,也是最推荐的部署方式。 > 在以下我们的示例中,统一使用 `main` 作为项目名称。 ## 1\. 编译程序 跨平台交叉编译是 `golang` 的特点之一,可以非常方便地编译出我们需要的目标服务器平台的版本,而且是静态编译,非常容易地解决了运行依赖问题。 使用以下指令可以静态编译 `Linux` 平台 `amd64` 架构的可执行文件: ```bash CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o main main.go ``` 生成的 `main` 便是我们静态编译的,可部署于 `Linux amd64` 上的可执行文件。 ## 2\. 编译镜像 我们需要将该可执行文件 `main` 编译生成 `docker` 镜像,以便于分发及部署。 `Golang` 的运行环境推荐使用 `alpine` 基础系统镜像,编译出的容器镜像约为 `20MB` 左右。 一个参考的 `Dockerfile` 文件如下: ```dockerfile FROM loads/alpine:3.8 LABEL maintainer="john@goframe.org" ############################################################################### # INSTALLATION ############################################################################### # 设置固定的项目路径 ENV WORKDIR /app/main # 添加应用可执行文件,并设置执行权限 ADD ./main $WORKDIR/main RUN chmod +x $WORKDIR/main # 添加静态资源文件 ADD resource $WORKDIR/resource ############################################################################### # START ############################################################################### WORKDIR $WORKDIR CMD ./main ``` 其中,我们的基础镜像使用了 `loads/alpine:3.8`,中国国内的用户推荐使用该基础镜像,基础镜像的 `Dockerfile` 地址: [https://github.com/gqcn/dockerfiles](https://github.com/gqcn/dockerfiles),仓库地址: [https://hub.docker.com/u/loads](https://hub.docker.com/u/loads) 随后使用 `docker build -t main .` 指令编译生成名为 `main` 的 `docker` 镜像。 **注意事项** 需要注意的是,在某些项目的架构设计中, **静态文件** 和 **配置文件** 可能不会随着镜像进行编译发布,而是分开进行管理和发布。 例如,使用 `MVVM` 模式的项目中(例如使用 `vue` 框架),往往是前后端非常独立的,因此在镜像中往往并不会包含 `public` 目录。而使用了 `配置管理中心`(例如使用 `consul`/ `etcd`/ `zookeeper`)的项目中,也往往并不需要 `config` 目录。 因此对于以上示例的 `Dockerfile` 的使用,仅作参考,根据实际情况请进行必要的调整。 ## 3\. 运行镜像 使用以下指令可直接运行刚才编译成的镜像: ```bash docker run main ``` ## 4\. 镜像分发 容器的分发可以使用 `docker` 官方的平台: [https://hub.docker.com/](https://hub.docker.com/),国内也可以考虑使用阿里云: [https://www.aliyun.com/product/acr](https://www.aliyun.com/product/acr)。 ## 5\. 容器编排 在企业级生产环境中, `docker` 容器往往需要结合 `kubernetes` 或者 `docker swarm` 容器编排工具一起使用。 容器编排涉及到的内容比较多,感兴趣的同学可以参考以下资料: - [https://kubernetes.io/](https://kubernetes.io/) - [https://docs.docker.com/swarm/](https://docs.docker.com/swarm/) # AI Vibe Coding ## 关于本栏目 未来软件开发的大趋势是与`AI`的深度结合。随着大模型能力的持续演进,`AI`正在深刻改变开发者的工作方式 - 从代码生成、架构设计到问题排查,`AI`已成为不可或缺的生产力工具。 `GoFrame`开源团队未来也会倾注更多的精力以增强对`AI`的适配能力,使得`AI`能更高质量地生成项目代码、解决工程问题。无论是提供更完善的上下文信息、更规范的接口设计,还是与主流`AI`编辑器的深度集成,`GoFrame`都将持续探索与`AI`协作的最佳实践。 ## 相关内容 # GoFrame Skills ## 关于GoFrame Skills `GoFrame Skills` 是专为`GoFrame`框架量身定制的`AI`技能集,旨在让`AI`编辑器深度理解`GoFrame`的开发规范与最佳实践,从而生成高质量、生产就绪的代码。它提供: - **完整的文档覆盖**:涵盖命令行管理、配置管理、日志组件、错误处理、数据校验、类型转换、缓存管理、模板引擎、数据库`ORM`、`I18N`国际化等核心组件的设计介绍、使用说明、最佳实践和注意事项。 - **丰富的实战示例**:包含`HTTP`服务、`gRPC`微服务、服务注册发现、配置中心集成、可观测性、`JWT`认证、文件上传、限流、反向代理等多种项目类型的完整代码示例。 - **`AI`驱动开发**:赋能`AI`深度理解`GoFrame`约定与最佳实践,生成符合框架规范的高质量代码。 **说明**:提示 目前`GoFrame Skills`处于`Beta`阶段,欢迎通过 [Issues](https://github.com/gogf/skills/issues) 和 [Pull Requests](https://github.com/gogf/skills/pulls) 参与贡献。 ## 支持的 AI 编辑器 `GoFrame Skills` 基于开放的 [Agent Skills](https://agentskills.io/) 规范构建,支持市面上所有主流的`AI`编辑器。以下是部分已支持的编辑器: | 编辑器 | 编辑器 | 编辑器 | 编辑器 | |---|---|---|---| | `Claude Code` | `Cursor` | `GitHub Copilot` | `Windsurf` | | `Cline` | `Continue` | `Roo Code` | `OpenCode` | | `Codex` | `Gemini CLI` | `Augment` | `Trae` | | `Amp` | `Goose` | `OpenHands` | `Kilo Code` | | `Qwen Code` | `Zencoder` | `Replit` | `Junie` | 完整的支持列表请参考:[Supported Agents](https://github.com/vercel-labs/skills?tab=readme-ov-file#supported-agents)。 安装工具会自动检测当前系统中已安装的`AI`编辑器,并将技能安装到对应的目录。 ## 安装 GoFrame Skills `GoFrame Skills`通过`npx skills`命令行工具进行安装,直接执行以下命令即可: ```bash npx skills add github.com/gogf/skills ``` 执行后,工具会自动检测本机已安装的`AI`编辑器,并交互式地引导完成安装。 ### 安装范围 安装时可以选择**项目级**或**全局**安装: | 范围 | 参数 | 安装路径 | 说明 | |---|---|---|---| | 项目级(默认) | 无 | `.//skills/` | 随项目提交,团队共享 | | 全局 | `-g` | `~//skills/` | 对所有项目生效 | 全局安装示例: ```bash npx skills add github.com/gogf/skills -g ``` ## 更新 GoFrame Skills ### 检查更新 运行以下命令检查`GoFrame Skills`是否有可用的新版本: ```bash npx skills check ``` ### 安装更新 确认有更新后,执行以下命令将所有已安装的技能更新到最新版本: ```bash npx skills update ``` ## 常见使用场景 安装`GoFrame Skills`后,`AI`编辑器将具备对`GoFrame`框架的深度理解能力。以下是一些典型的使用场景: ### 代码生成 根据自然语言描述,`AI`可以自动生成符合`GoFrame`规范的项目结构和代码片段,帮助开发者快速搭建项目框架。 **示例 Prompt:** ```text 使用 GoFrame 构建一个用户服务,设计 RESTful API 接口,包含基本的增删改查操作。 ``` ### 功能开发 在现有项目基础上,`AI`能够理解`GoFrame`的模块划分、依赖注入等设计模式,快速为项目添加新功能模块。 **示例 Prompt:** ```text 为项目添加 JWT 鉴权功能。只有使用有效凭证登录的用户才能访问 /user/profile 等受保护的接口。 ``` ### 代码优化 `AI`可以分析现有的项目代码,结合框架最佳实践提供优化建议,改善代码的性能、可读性和可维护性。 **示例 Prompt:** ```text 请帮我审查当前项目的数据库查询代码,根据 GoFrame ORM 的最佳实践给出优化建议。 ``` ### 问题解答 针对开发者在使用`GoFrame`过程中遇到的问题,`AI`能够结合框架文档和最佳实践提供专业、准确的解答与解决方案。 **示例 Prompt:** ```text 为什么我使用 GoFrame ORM 执行事务时,在嵌套函数中获取不到同一个事务对象? 应该如何正确地在 GoFrame 中传递事务上下文? ``` ### 代码迁移与升级 当需要将旧版`GoFrame`项目升级到新版本,或从其他框架迁移到`GoFrame`时,`AI`可以提供针对性的迁移方案和代码改写建议。 **示例 Prompt:** ```text 我有一个基于 Gin 框架编写的 HTTP 服务, 请帮我将其迁移到 GoFrame,保持原有的路由结构和中间件逻辑。 ``` ### 单元测试生成 `AI`可以根据现有的`GoFrame`业务代码,自动生成符合`GoFrame`测试规范的单元测试用例,提升项目的测试覆盖率。 **示例 Prompt:** ```text 请为 internal/service/user 目录下的用户注册逻辑生成单元测试, 使用 GoFrame 推荐的测试方式,并覆盖正常流程和异常流程。 ``` ## 常见问题 ### GoFrame Skills没有被AI编辑器触发? - 在`AI`编辑器的`Chat`窗口询问"当前已安装的技能有哪些?"以确认技能是否正确安装。如果未被识别出,那么请查询自己的编辑器识别的技能目录是什么(可参考[Supported Agents](https://github.com/vercel-labs/skills?tab=readme-ov-file#supported-agents)),并重新使用安装命令交互式安装到正确的目录下。 - 由于`Skills`的触发机制依赖给定提示词中的关键字,可尝试在提示词中增加更详细和明确的信息,例如"帮助我开发一个用户服务"的提示词也许无法触发`GoFrame Skills`,但"使用`GoFrame`开发一个用户服务"、"使用`GoFrame Skills`开发一个用户服务"或者"使用`Go`开发一个用户服务"则更有可能被正确识别和触发。 ### GoFrame Skills生成的代码质量不符合预期? - 通常来讲,不太需要怀疑`GoFrame Skills`的质量,因为它是基于框架最佳实践和官方文档构建的。如果生成的代码不符合预期,可能是提示词不够明确或具体,建议优化提示词以获得更符合需求的代码。 - 使用不同的模型可能会产生不同的代码质量和风格,可以根据实际需求选择合适的模型。例如使用`Claude Haiku 4.5`模式生成的代码质量或者技能效果远不及`Claude Opus 4.6`模型。 ## 相关链接 - `GoFrame Skills`仓库:[https://github.com/gogf/skills](https://github.com/gogf/skills) - 通用`skills`安装工具:[https://github.com/vercel-labs/skills](https://github.com/vercel-labs/skills) # gRPC GitHub 源码: https://github.com/gogf/examples/tree/main/grpc ## 介绍 本分类包含了一些 `GoFrame` 框架中 `gRPC` 相关的示例代码。 ## 注意事项 1. 示例代码主要用于演示目的 2. 生产环境使用时需要根据实际需求进行调整 ## 示例列表 # 基础示例 GitHub 源码: https://github.com/gogf/examples/tree/main/grpc/basic ## 介绍 本示例展示了在 `GoFrame` 应用程序中使用 `gRPC` 的基础示例。 ## 目录结构 ```text . ├── client/ # 客户端示例 │ └── client.go # 客户端实现 ├── controller/ # 服务控制器 │ └── hello.go # Hello 服务实现 ├── protobuf/ # protobuf协议定义 ├── server/ # 服务器示例 │ ├── config.yaml # 服务器配置 │ └── server.go # 服务器实现 ├── go.mod # Go 模块文件 └── go.sum # Go 模块校验和 ``` ## 环境要求 - [Go](https://golang.org/dl/) `1.23` 或更高版本 - [Git](https://git-scm.com/downloads) - [GoFrame](https://goframe.org) - [Protocol Buffers](https://developers.google.com/protocol-buffers) ## 使用说明 1. 启动服务器: ```bash cd server go run server.go ``` 2. 运行客户端: ```bash cd client go run client.go ``` # 服务发现 GitHub 源码: https://github.com/gogf/examples/tree/main/grpc/resolver ## 介绍 本示例展示了如何在 `GoFrame` 的 `gRPC` 服务中使用 `etcd` 进行服务发现。 ## 目录结构 ```text . ├── client/ # 客户端示例 │ └── client.go # 带服务发现的客户端 ├── controller/ # 服务控制器 │ └── helloworld.go # Hello 服务实现 ├── protobuf/ # protobuf协议定义 │ └── helloworld.proto # 服务和消息定义 ├── server/ # 服务器示例 │ └── server.go # 带服务注册的服务器 ├── go.mod # Go 模块文件 └── go.sum # Go 模块校验和 ``` ## 环境要求 - [Go](https://golang.org/dl/) `1.23` 或更高版本 - [Git](https://git-scm.com/downloads) - [GoFrame](https://goframe.org) - [Protocol Buffers](https://developers.google.com/protocol-buffers) - [etcd](https://etcd.io/) ## 使用说明 1. 启动 `etcd`: ```bash docker run -d --name etcd -p 2379:2379 -e ALLOW_NONE_AUTHENTICATION=yes bitnami/etcd:3.4.24 ``` 2. 启动服务器: ```bash cd server go run server.go ``` 3. 运行客户端: ```bash cd client go run client.go ``` # 负载均衡 GitHub 源码: https://github.com/gogf/examples/tree/main/grpc/balancer ## 介绍 本示例展示了如何在 `GoFrame` 应用程序中实现 `gRPC` 负载均衡。 ## 目录结构 ```text . ├── client/ # 客户端示例 │ └── client.go # 带负载均衡的客户端实现 ├── controller/ # 服务控制器 │ └── hello.go # Hello 服务实现 ├── protobuf/ # protobuf协议定义 ├── server/ # 服务器示例 │ ├── config.yaml # 服务器配置 │ └── server.go # 服务器实现 ├── go.mod # Go 模块文件 └── go.sum # Go 模块校验和 ``` ## 环境要求 - [Go](https://golang.org/dl/) `1.23` 或更高版本 - [Git](https://git-scm.com/downloads) - [GoFrame](https://goframe.org) - [Protocol Buffers](https://developers.google.com/protocol-buffers) ## 使用说明 1. 启动多个服务器实例(使用随机端口): ```bash cd server go run server.go # 启动第二个服务器实例 go run server.go # 启动第三个服务器实例 go run server.go ``` 2. 运行客户端: ```bash cd client go run client.go ``` # 上下文示例 GitHub 源码: https://github.com/gogf/examples/tree/main/grpc/ctx ## 介绍 本示例展示了如何在 `GoFrame` 的 `gRPC` 中使用上下文和元数据。 ## 目录结构 ```text . ├── client/ # 客户端示例 │ └── client.go # 带上下文处理的客户端实现 ├── controller/ # 服务控制器 │ └── helloworld.go # 带上下文处理的 Hello 服务 ├── protobuf/ # protobuf协议定义 ├── server/ # 服务器示例 │ ├── config.yaml # 服务器配置 │ └── server.go # 服务器实现 ├── go.mod # Go 模块文件 └── go.sum # Go 模块校验和 ``` ## 环境要求 - [Go](https://golang.org/dl/) `1.23` 或更高版本 - [GoFrame](https://goframe.org) - [Protocol Buffers](https://developers.google.com/protocol-buffers) ## 使用说明 1. 启动服务器: ```bash cd server go run server.go ``` 2. 运行客户端: ```bash cd client go run client.go ``` # 原生 gRPC 示例 GitHub 源码: https://github.com/gogf/examples/tree/main/grpc/rawgrpc ## 介绍 本示例展示了如何在 `GoFrame` 中实现原生 `gRPC` 服务,不使用额外的抽象层。 ## 目录结构 ```text . ├── client/ # 客户端示例 │ └── client.go # 原生 gRPC 客户端实现 ├── helloworld/ # protobuf协议定义 │ └── helloworld.proto # 服务和消息定义 ├── server/ # 服务器示例 │ └── server.go # 原生 gRPC 服务器实现 ├── go.mod # Go 模块文件 └── go.sum # Go 模块校验和 ``` ## 环境要求 - [Go](https://golang.org/dl/) `1.23` 或更高版本 - [Git](https://git-scm.com/downloads) - [GoFrame](https://goframe.org) - [Protocol Buffers](https://developers.google.com/protocol-buffers) ## 使用说明 1. 启动服务器: ```bash cd server go run server.go ``` 2. 运行客户端: ```bash cd client go run client.go ``` # NoSQL GitHub 源码: https://github.com/gogf/examples/tree/main/nosql ## 介绍 本分类包含了一些在 `GoFrame` 框架中使用 `NoSQL` 数据库的示例。 ## 注意事项 1. 示例代码主要用于演示目的 2. 生产环境使用时需要根据实际需求进行调整 ## 示例列表 # Redis GitHub 源码: https://github.com/gogf/examples/tree/main/nosql/redis 此示例演示了如何在 `GoFrame` 框架中使用 `Redis`。 ## 概述 本示例展示了: 1. 如何使用 `YAML` 配置文件配置 `Redis` 连接 2. 如何创建 `Redis` 客户端 3. 基本的 `Redis` 操作(`SET/GET`) ## 环境要求 - `Go 1.23` 或更高版本 - `Redis` 服务器 - `GoFrame v2` ## 代码结构 - `main.go`: 包含主要逻辑和 `Redis` 客户端初始化 - `config.yaml`: `Redis` 配置文件 ## 配置说明 `Redis` 配置存储在 `config.yaml` 文件中: ```yaml redis: address: "127.0.0.1:6379" password: ``` 可以根据自己的 `Redis` 服务器配置修改这些设置。 ## 使用 Docker 运行 Redis 如果本地没有安装 `Redis`,可以使用 `Docker` 快速启动一个 `Redis` 实例: ```bash # 运行 Redis 容器 docker run --name redis-test -p 6379:6379 -d redis:latest # 验证容器是否正在运行 docker ps # 如果需要停止容器 docker stop redis-test # 如果需要删除容器 docker rm redis-test ``` 如果需要带密码认证的 `Redis`: ```bash # 运行带密码的 Redis docker run --name redis-test -p 6379:6379 -d redis:latest redis-server --requirepass your_password # 记得相应地更新 config.yaml: # redis: # address: "127.0.0.1:6379" # password: "your_password" ``` ## 运行示例 1. 确保 `Redis` 服务器正在运行 2. 根据需要更新 `config.yaml` 3. 运行示例: ```bash go run main.go ``` ## 更多参考 更多 `Redis` 的使用方法请参考第三方组件 [github.com/redis/go-redis](https://github.com/redis/go-redis)。 # MongoDB GitHub 源码: https://github.com/gogf/examples/tree/main/nosql/mongodb 此示例演示了如何在 `GoFrame` 框架中使用 `MongoDB`。 ## 概述 本示例展示了: 1. 如何使用 `YAML` 配置文件配置 `MongoDB` 连接 2. 如何创建 `MongoDB` 客户端 3. 基本的 `MongoDB` 操作(`INSERT`、`FIND`、`UPDATE`) 4. 如何使用 `BSON` 进行文档操作 ## 环境要求 - `Go 1.23` 或更高版本 - `MongoDB` 服务器 - `GoFrame v2` ## 代码结构 - `main.go`: 包含主要逻辑和 `MongoDB` 客户端初始化 - `config.yaml`: `MongoDB` 配置文件 ## 配置说明 `MongoDB` 配置存储在 `config.yaml` 文件中: ```yaml mongo: database: "user" address: "mongodb://127.0.0.1:27017/test?retryWrites=true" ``` 可以根据自己的 `MongoDB` 服务器配置修改这些设置。 ## 使用 Docker 运行 MongoDB 如果本地没有安装 `MongoDB`,可以使用 `Docker` 快速启动一个 `MongoDB` 实例: ```bash # 运行 MongoDB 容器 docker run --name mongo-test -p 27017:27017 -d mongo:latest # 验证容器是否正在运行 docker ps # 如果需要停止容器 docker stop mongo-test # 如果需要删除容器 docker rm mongo-test ``` 如果需要带认证的 `MongoDB`: ```bash # 运行带认证的 MongoDB docker run --name mongo-test -p 27017:27017 -e MONGO_INITDB_ROOT_USERNAME=admin -e MONGO_INITDB_ROOT_PASSWORD=password -d mongo:latest # 记得相应地更新 config.yaml: # mongo: # database: "user" # address: "mongodb://admin:password@127.0.0.1:27017/test?retryWrites=true" ``` ## 运行示例 1. 确保 `MongoDB` 服务器正在运行 2. 根据需要更新 `config.yaml` 3. 运行示例: ```bash go run main.go ``` ## 更多参考 更多 `MongoDB` 的使用方法请参考官方 `MongoDB` Go 驱动 [github.com/mongodb/mongo-go-driver](https://github.com/mongodb/mongo-go-driver)。 # 加密密码数据库连接 GitHub 源码: https://github.com/gogf/examples/tree/main/database/encoded-pass ## 介绍 本示例展示了如何在`GoFrame`中实现一个自定义数据库驱动,用于处理加密的数据库密码。该实现允许在配置文件中存储加密的密码,这些密码在建立数据库连接时会自动解密。 示例使用`AES`加密结合`Base64`编码来保护数据库密码,为应用程序的数据库凭据提供额外的安全层。 ## 环境要求 - [Go](https://golang.org/dl/) `1.23` 或更高版本 - [Git](https://git-scm.com/downloads) - [GoFrame](https://goframe.org) - `MySQL` 数据库 ## 目录结构 ```text . ├── config.yaml # 包含加密数据库密码的配置文件 ├── dbdriver/ # 自定义数据库驱动实现 │ ├── dbdriver.go # 带密码解密功能的自定义 MySQL 驱动 │ └── dbdriver_test.go # 密码加密/解密测试 ├── go.mod # Go 模块文件 ├── go.sum # Go 模块校验和 └── main.go # 应用程序入口点 ``` ## 功能特点 - 自定义数据库驱动实现 - 使用`AES-CBC`进行密码加密 - 使用`Base64`编码处理二进制数据 - 与`GoFrame`数据库操作无缝集成 - 安全存储数据库凭据 ## 设置步骤 1. 克隆代码库: ```bash git clone https://github.com/gogf/examples.git cd examples/database/encoded-pass ``` 2. 安装依赖: ```bash go mod tidy ``` 3. 设置 MySQL 数据库: ```bash docker run --name mysql -p 3306:3306 -e MYSQL_ROOT_PASSWORD=12345678 -e MYSQL_DATABASE=test -d mysql:8.0 ``` 4. 使用加密的数据库密码更新配置文件(参见下面的密码加密部分)。 5. 运行应用程序: ```bash go run main.go ``` ## 密码加密 要加密的数据库密码: 1. 运行演示加密过程的测试函数: ```bash cd dbdriver go test -v -run Test_Encode ``` 2. 测试将输出: - 原始密码 - `Base64`编码的密码 - `AES`加密并 `Base64` 编码的密码(在 `config.yaml` 中使用这个) - 解密后的密码(用于验证) 3. 使用的加密密码更新 `config.yaml` 文件: ```yaml database: default: link: "mysql:root:您的加密密码@tcp(127.0.0.1:3306)/test?loc=Local&parseTime=true" ``` ## 实现细节 自定义数据库驱动扩展了`GoFrame`提供的标准`MySQL`驱动,并重写了`Open`接口方法来处理密码解密: 1. 从数据库连接字符串中提取加密密码 2. 对密码进行 `Base64` 解码,获取二进制加密数据 3. 使用预定义的加密密钥应用 `AES-CBC` 解密 4. 对解密后的数据再次进行 `Base64` 解码,获取原始密码 5. 使用原始密码建立数据库连接 ## 注意事项 - 在此示例中,加密密钥是硬编码的(`encodeKey`)。在生产环境中,应该使用安全的方法来管理此密钥。 - 这种方法可以扩展到加密配置文件中的其他敏感信息。 - 示例使用`AES-CBC`加密,但可以修改它以使用`GoFrame`提供的其他加密算法。 # HTTP Server GitHub 源码: https://github.com/gogf/examples/tree/main/httpserver ## 介绍 本分类包含了一些使用 `GoFrame` 框架构建 `HTTP` 服务器的各种示例。每个示例都提供了详细的介绍和代码示例。 ## 注意事项 1. 示例代码主要用于演示目的 2. 生产环境使用时需要根据实际需求进行调整 ## 示例列表 # JWT认证 GitHub 源码: https://github.com/gogf/examples/tree/main/httpserver/jwt 本示例展示了如何在`GoFrame`HTTP服务器中使用 `github.com/golang-jwt/jwt` 包实现`JWT(JSON Web Token)`认证。 ## 功能特性 - 用户登录接口,生成`JWT`令牌 - 使用`JWT`中间件保护路由 - 令牌验证和解析 - 受保护资源访问示例 - 标准的`GoFrame`项目结构 ## 项目结构 ```text jwt/ ├── api/ │ └── v1/ │ └── auth.go # API接口定义 ├── internal/ │ ├── controller/ │ │ └── auth.go # 业务逻辑实现 │ └── middleware/ │ └── jwt.go # JWT中间件 └── main.go # 入口文件 ``` ## API接口 1. 登录接口: `POST /login` ```json { "username": "admin", "password": "password" } ``` 2. 受保护资源: `GET /api/protected` - 需要在`Authorization`头部携带`Bearer`令牌 - 示例: `Authorization: Bearer your-token-here` ## 运行示例 1. 启动服务器: ```bash go run main.go ``` 2. 服务器将在`8000`端口启动 ## 测试API 1. 登录获取令牌: ```bash curl -X POST http://localhost:8000/login \ -H "Content-Type: application/json" \ -d '{"username":"admin","password":"password"}' ``` 2. 访问受保护的接口: ```bash curl http://localhost:8000/api/protected \ -H "Authorization: Bearer your-token-here" ``` ## 安全注意事项 - 在生产环境中,请替换硬编码的密钥为安全的值 - 将用户凭证存储在数据库中 - 实现适当的密码哈希 - 考虑实现刷新令牌机制 - 为登录尝试添加速率限制 ## 参考资料 有关`JWT`实现的更多详细信息,请参考第三方组件文档: - [github.com/golang-jwt/jwt](https://github.com/golang-jwt/jwt) # 反向代理 GitHub 源码: https://github.com/gogf/examples/tree/main/httpserver/proxy ## 介绍 本示例展示了如何使用 `GoFrame` 创建一个反向代理服务器。示例包含两个服务器: 1. 在 `8198` 端口运行的后端服务器,提供实际服务 2. 在 `8000` 端口运行的代理服务器,将请求转发到后端服务器 代理服务器实现了以下功能: - 使用 `httputil.NewSingleHostReverseProxy` 实现反向代理功能 - 自定义代理失败的错误处理 - `URL` 路径重写 - 请求体处理 - 详细的代理操作日志 ## 环境要求 - [Go](https://golang.org/dl/) `1.23` 或更高版本 - [Git](https://git-scm.com/downloads) - [GoFrame](https://goframe.org) ## 使用说明 1. 运行服务: ```bash go run main.go ``` 2. 服务监听两个端口: - 后端服务器在 http://127.0.0.1:8198 - 代理服务器在 http://127.0.0.1:8000 3. 测试代理: - 通过代理访问:http://127.0.0.1:8000/proxy/user/1 - 直接访问后端:http://127.0.0.1:8198/user/1 # 访问限流 GitHub 源码: https://github.com/gogf/examples/tree/main/httpserver/rate-limit ## 介绍 本示例展示了如何在使用 `GoFrame` 的 HTTP 服务器中实现限流。 它展示了如何使用 `golang.org/x/time/rate` 包实现的令牌桶算法来保护 `API` 端点免受过多请求的影响。 ## 环境要求 - [Go](https://golang.org/dl/) `1.23` 或更高版本 - [Git](https://git-scm.com/downloads) - [GoFrame](https://goframe.org) ## 目录结构 - `go.mod`: Go 模块文件 - `main.go`: 主程序入口 ## 使用说明 1. 启动服务器: ```bash go run main.go ``` 2. 测试限流: ```bash # 正常请求 curl http://localhost:8000/hello?name=world # 快速发送多个请求测试限流 for i in {1..20}; do curl http://localhost:8000/hello?name=world; done ``` # 文件上传 GitHub 源码: https://github.com/gogf/examples/tree/main/httpserver/upload-file ## 介绍 本示例展示了如何使用 `GoFrame` 在 HTTP 服务器中实现文件上传功能。它展示了: - 现代化且用户友好的文件上传界面 - 服务器端文件上传处理 - 文件上传进度跟踪 - 适当的错误处理和验证 - 最大文件大小配置 本示例同时提供了 `REST API` 端点和 `Web` 界面用于文件上传。 ## 环境要求 - [Go](https://golang.org/dl/) `1.23` 或更高版本 - [Git](https://git-scm.com/downloads) - [GoFrame](https://goframe.org) ## 目录结构 - `go.mod`: Go 模块文件,用于依赖管理 - `main.go`: 主程序入口 - `resource/`: 静态资源目录 - `public/`: 公共资源 - `upload.html`: 上传页面 - `upload/`: 上传文件存储目录 ## 使用说明 1. 启动服务: ```bash go run main.go ``` 2. 上传页面:http://localhost:8000/upload.html # WebSocket GitHub 源码: https://github.com/gogf/examples/tree/main/httpserver/websocket ## 描述 本示例展示了如何使用 `GoFrame` 实现 `WebSocket` 通信。示例包含 `HTTP` 和 `HTTPS` 两种实现,主要组件包括: 1. 支持安全和非安全连接的 `WebSocket` 服务器 2. 支持 `HTTP` 和 `HTTPS` 的 `WebSocket` 客户端实现 3. `WebSocket` 消息交换示例 实现展示了以下功能: - 基本的 `WebSocket` 服务器设置 - 安全 `WebSocket`(`WSS`)实现 - 客户端连接处理 - 客户端和服务器之间的消息交换 - 正确的连接清理 ## 环境要求 - [Go](https://golang.org/dl/) `1.23` 或更高版本 - [Git](https://git-scm.com/downloads) - [GoFrame](https://goframe.org) - [Gorilla WebSocket](https://github.com/gorilla/websocket) ## 目录结构 ```text websocket/ ├── http/ │ ├── client.go # HTTP WebSocket客户端 │ ├── server.go # HTTP WebSocket服务器 │ └── static/ # 静态文件目录 ├── https/ │ ├── client.go # HTTPS WebSocket客户端 │ ├── server.go # HTTPS WebSocket服务器 │ ├── static/ # 静态文件目录 │ └── certs/ # SSL证书 └── README.MD ``` ## 功能特性 - `WebSocket` 服务器实现 - 安全 `WebSocket`(`WSS`)支持 - 客户端连接处理 - 消息回显功能 - 连接生命周期管理 - 来源检查(可配置) - 错误处理 ## 安装设置 1. 克隆仓库: ```bash git clone https://github.com/gogf/examples.git cd examples/httpserver/websocket ``` 2. 安装依赖: ```bash go mod tidy ``` 3. 对于`HTTPS/WSS`支持,生成自签名证书(可选): ```bash mkdir -p https/certs openssl req -x509 -nodes -days 365 -newkey rsa:2048 -keyout https/certs/server.key -out https/certs/server.crt ``` ## 使用方法 ### HTTP WebSocket 1. 启动服务器: ```bash cd http go run server.go ``` 2. 运行客户端: ```bash cd http go run client.go ``` 3. 通过浏览器访问演示页面: 在浏览器中打开 `http://127.0.0.1:8000` 查看 `WebSocket` 演示效果。页面提供了一个简单的聊天界面,可以实时发送和接收消息。 ### HTTPS WebSocket 1. 启动安全服务器: ```bash cd https go run server.go ``` 2. 运行安全客户端: ```bash cd https go run client.go ``` 3. 通过浏览器访问演示页面: 在浏览器中打开 `https://127.0.0.1:8000` 查看 `WebSocket` 演示效果。 注意:由于使用自签名证书,浏览器可能会显示安全警告,这是正常的。 ## 实现细节 ### 服务器特性 - `WebSocket` 升级处理 - 消息回显功能 - 连接生命周期管理 - 可配置的来源检查 - 错误处理和日志记录 ### 客户端特性 - 连接建立 - 消息发送和接收 - 安全连接的 `TLS` 配置 - 清理的连接关闭 ## 注意事项 - `WebSocket` 服务器会回显收到的任何消息 - `HTTPS`/`WSS` 示例中使用了自签名证书 - 在生产环境中,需要实现适当的来源检查 - 正确处理连接关闭以避免资源泄漏 ## 更多信息 更多关于`WebSocket`实现的详细信息,请参考: - [GoFrame WebSocket指南](https://goframe.org/docs/web/senior-websocket) - [Gorilla WebSocket文档](https://github.com/gorilla/websocket) # JSON数组响应 GitHub 源码: https://github.com/gogf/examples/tree/main/httpserver/response-json-array ## 介绍 本示例展示了如何使用 `GoFrame` 实现一个返回 `JSON` 数组响应的 `HTTP` 服务器。它展示了如何: - 将 `API` 响应构造为 `JSON` 数组 - 配置 `OpenAPI/Swagger` 文档 - 使用 `GoFrame` 的中间件实现一致的响应处理 - 定义类型安全的请求和响应结构 本示例实现了一个返回 `JSON` 数组格式用户列表的 `/user` 端点。 ## 环境要求 - [Go](https://golang.org/dl/) `1.23` 或更高版本 - [Git](https://git-scm.com/downloads) - [GoFrame](https://goframe.org) ## 目录结构 - `go.mod`: Go 模块文件 - `main.go`: 主程序入口 ## 使用说明 1. 启动服务: ```bash go run main.go ``` 2. 测试接口:http://localhost:8000/user 3. 接口文档:http://127.0.0.1:8000/swagger # 基本认证 GitHub 源码: https://github.com/gogf/examples/tree/main/httpserver/basic-auth ## 介绍 本示例展示了如何使用 `GoFrame` 框架实现 `HTTP` 基本认证(`Basic Authentication`)。基本认证是一种简单的认证机制,允许服务器在授予对受保护资源的访问权限之前请求客户端提供凭据。 示例展示了如何: - 设置带有受保护资源的基本 HTTP 服务器 - 使用 `GoFrame` 内置的 `BasicAuth` 方法实现基本认证 - 处理认证成功和失败的情况 - 自定义认证领域(`realm`)消息 ## 环境要求 - [Go](https://golang.org/dl/) `1.23` 或更高版本 - [Git](https://git-scm.com/downloads) - [GoFrame](https://goframe.org) ## 目录结构 ```text basic-auth/ ├── README.MD # 英文文档 ├── README.ZH.MD # 中文文档 ├── go.mod # Go 模块文件 └── main.go # 主程序入口 ``` ## 功能特点 - 简单且安全的 `HTTP` 基本认证 - 自动处理认证头部 - 可自定义认证领域消息 - 清晰分离认证逻辑 ## 安装设置 1. 克隆仓库: ```bash git clone https://github.com/gogf/examples.git cd examples/httpserver/basic-auth ``` 2. 安装依赖: ```bash go mod tidy ``` 3. 运行应用: ```bash go run main.go ``` ## 使用方法 1. 启动服务器: ```bash go run main.go ``` 2. 服务器将在 `8000` 端口启动。 3. 访问受保护的资源: - 网址:http://127.0.0.1:8000/ - 当提示时,输入以下凭据: - 用户:`user` - 密码:`pass` 4. 认证成功后,将看到消息:"`Authentication successful!`" ## 实现细节 服务器使用 `GoFrame` 的 `BasicAuth` 方法实现 `HTTP` 基本认证。该方法: 1. 检查请求是否包含有效的基本认证凭据 2. 如果凭据缺失或无效,它会自动: - 设置带有指定领域的 `WWW-Authenticate` 头部 - 返回 `401` 未授权状态码 - 使浏览器显示认证对话框 3. 如果认证成功,它返回 `true` 并允许处理程序继续处理受保护的内容 实现非常简洁,只需要很少的代码: ```go if r.BasicAuth("user", "pass", "Please enter username and password") { // 认证成功后的处理 r.Response.Write("Authentication successful!") } // 如果认证失败,BasicAuth 方法会自动处理响应 ``` ## 注意事项 - 基本认证以 `base64` 编码传输凭据,在纯 `HTTP` 上不安全 - 在生产环境中,始终将基本认证与 `HTTPS` 结合使用 - 本示例中的凭据是硬编码的,仅用于演示目的;在实际应用中,应使用安全的凭据存储 # GoFrame MCP HTTP 示例 GitHub 源码: https://github.com/gogf/examples/tree/main/httpserver/mcp-http 本示例展示了如何使用 GoFrame 的 `ghttp` 服务器和 `mark3labs/mcp-go` SDK 实现通过 HTTP 流式传输的 [Model Context Protocol (MCP)](https://modelcontextprotocol.io/) 服务器。 ## 先决条件 - Go 1.23 或更高版本 - MCP 客户端(例如 Claude Desktop 或 IDE 扩展) ## 安装 1. 初始化模块(如果尚未完成): ```bash go mod tidy ``` 2. 运行服务器: ```bash go run main.go ``` ## 用法 服务器暴露了一个端点: - `POST /mcp`: 用于处理 MCP JSON-RPC 消息的 HTTP 端点。 ### 连接 Claude Desktop 要在 Claude Desktop 中使用此 MCP 服务器,请将以下配置添加到的 `claude_desktop_config.json`: ```json { "mcpServers": { "goframe-mcp": { "command": "go", "args": ["run", "path/to/your/project/httpserver/mcp-http/main.go"] } } } ``` *注意:此示例使用 HTTP 流式传输(StreamableHTTP),通过单一 HTTP 端点处理所有 MCP 通信。* ### 直接 HTTP 测试 1. 启动服务器。 2. 发送 POST 请求到 `http://localhost:8080/mcp`。 示例请求: ```bash curl -X POST http://localhost:8080/mcp \ -H "Content-Type: application/json" \ -d '{ "jsonrpc": "2.0", "id": 1, "method": "initialize", "params": { "protocolVersion": "2024-11-05", "capabilities": {}, "clientInfo": { "name": "client", "version": "1.0.0" } } }' ``` ## MCP 客户端配置 如果的客户端或 IDE 支持通过配置文件定义 MCP 服务器,可以在项目根目录下创建 `.vscode/mcp.json`,示例内容如下: ```json { "servers": { "add": { "url": "http://localhost:8080/mcp", "type": "http" } }, "inputs": [] } ``` 说明: - `servers` 下的键名(例如 `add`)为服务器标识名称。 - `url` 指向示例的 HTTP 端点 `http://localhost:8080/mcp`。 - `type` 表示连接类型,此处为 `http`(HTTP 流式传输)。 - `inputs` 可选,用于在客户端中预置输入。 保存后,支持该配置的客户端即可使用该文件连接到本示例 MCP 服务。 ## 功能 - **工具**: 暴露一个 `add` 工具,可接收两个数字(`a` 和 `b`),返回它们的和。 - **示例**: 可以通过 MCP 客户端发送如下计算请求: ``` mcp 计算 一加一 ``` 或英文: ``` mcp add 1 and 1 ``` 服务器将返回: ``` 结果: 1 + 1 = 2 ``` ## 代码结构 - `main.go`: 包含服务器设置、工具注册以及使用 `ghttp.WrapH` 进行的路由绑定。 ## StreamableHTTP 与 SSE 的区别 | 特性 | StreamableHTTP | SSE | |------|----------------|-----| | 协议 | HTTP 1.1+ | HTTP (Server-Sent Events) | | 连接方式 | 单一 POST 端点 | 分离的 GET (SSE) 和 POST 端点 | | 流式传输 | 支持 | 支持 | | 客户端实现 | 更简单 | 需要处理事件流 | | 浏览器支持 | 标准 HTTP | 需要 EventSource API | | 集成方式 | 适合 API 网关 | 适合实时推送场景 | StreamableHTTP 提供了一种更简洁的方式来实现 MCP 通信,通过单一的 HTTP 端点处理所有请求和响应,更易于集成到现有的 HTTP 服务架构中。 # GoFrame MCP SSE 示例 GitHub 源码: https://github.com/gogf/examples/tree/main/httpserver/mcp-sse 本示例展示了如何使用 GoFrame 的 `ghttp` 服务器和 `mark3labs/mcp-go` SDK 实现 [Model Context Protocol (MCP)](https://modelcontextprotocol.io/) 服务器。 ## 先决条件 - Go 1.23 或更高版本 - MCP 客户端(例如 Claude Desktop 或 IDE 扩展) ## 安装 1. 初始化模块(如果尚未完成): ```bash go mod tidy ``` 2. 运行服务器: ```bash go run main.go ``` ## 用法 服务器暴露了两个端点: - `GET /sse`: 用于建立连接的 Server-Sent Events 端点。 - `POST /message`: 用于发送 JSON-RPC 消息的端点。 ### 连接 Claude Desktop 要在 Claude Desktop 中使用此 MCP 服务器,请将以下配置添加到的 `claude_desktop_config.json`: ```json { "mcpServers": { "goframe-mcp": { "command": "go", "args": ["run", "path/to/your/project/httpserver/mcp/main.go"] } } } ``` *注意:由于此示例运行 HTTP 服务器,可能需要使用支持 SSE 的 MCP 客户端。上述配置假设将其作为本地进程运行。如果想通过 HTTP (SSE) 连接,的客户端必须支持远程 MCP 连接。* 直接 SSE 测试: 1. 启动服务器。 2. 连接到 `http://localhost:8080/sse`。 ## MCP 客户端配置 如果的客户端或 IDE 支持通过配置文件定义 MCP 服务器,可以在项目根目录下创建 `.vscode/mcp.json`,示例内容如下: ```json { "servers": { "add": { "url": "http://localhost:8080/sse", "type": "http" } }, "inputs": [] } ``` 说明: - `servers` 下的键名(例如 `add`)为服务器标识名称。 - `url` 指向示例的 SSE 端点 `http://localhost:8080/sse`。 - `type` 表示连接类型,此处为 `http`(SSE)。 - `inputs` 可选,用于在客户端中预置输入。 保存后,支持该配置的客户端即可使用该文件连接到本示例 MCP 服务。 ## 功能 - **工具**: 暴露一个 `add` 工具,可接收两个数字(`a` 和 `b`),返回它们的和。 - **示例**: 可以通过 MCP 客户端发送如下计算请求: ``` mcp 计算 一加一 ``` 或英文: ``` mcp add 1 and 1 ``` 服务器将返回: ``` 结果: 1 + 1 = 2 ``` ## 代码结构 - `main.go`: 包含服务器设置、工具注册以及使用 `ghttp.WrapH` 进行的路由绑定。 # Server-Sent Events(SSE) GitHub 源码: https://github.com/gogf/examples/tree/main/httpserver/sse ## 描述 本示例演示了如何使用 `GoFrame` 实现服务器发送事件(SSE)。服务器发送事件是一种允许服务器通过`HTTP`连接向客户端推送更新的技术。与`WebSocket`不同,`SSE`是单向的 - 只有服务器可以向客户端发送数据。 本示例实现了一个简单的`SSE`服务端实现,逐字符流式传输响应,模拟现代AI聊天应用程序中常见的实时打字效果。 ## 要求 - [Go](https://golang.org/dl/) `1.23` 或更高版本 - [Git](https://git-scm.com/downloads) - [GoFrame](https://goframe.org) ## 结构 ```text - go.mod: Go模块文件 - main.go: 包含SSE实现的主应用程序入口点 ``` ## 特性 - 服务器发送事件实现 - 模拟`AI`聊天逐字符流式响应 - 适用于`SSE`的正确`HTTP`头 ## 设置 1. 克隆仓库: ```bash git clone https://github.com/gogf/examples.git cd examples/httpserver/sse ``` 2. 安装依赖: ```bash go mod tidy ``` 3. 运行应用程序: ```bash go run main.go ``` ## 使用方法 1. 运行示例: ```bash go run main.go ``` 2. 服务器将在以下地址启动:http://127.0.0.1:8000 3. 访问SSE端点: - 在浏览器中,导航至:http://127.0.0.1:8000/ai/chat - 应该会看到单词逐个出现,产生打字效果 ## 实现细节 `SSE`实现包括: - 设置正确的`SSE` HTTP头: - `Content-Type: text/event-stream` - `Cache-Control: no-cache` - `Connection: keep-alive` - 使用适当的刷新进行流式响应 - 使用`time.Sleep`模拟打字延迟 - 使用`GoFrame`的路由器进行简单的HTTP路由 ## 注意事项 - `SSE`连接保持打开状态,直到客户端明确关闭 - 服务器必须在每条消息后刷新响应缓冲区 - 对于生产环境,请考虑实现重连逻辑和事件ID - `SSE`通过常规`HTTP/HTTPS`工作,并支持浏览器中的自动重连 # Swagger认证 GitHub 源码: https://github.com/gogf/examples/tree/main/httpserver/swagger-auth ## 描述 本示例演示了如何使用`GoFrame`框架为`Swagger API`文档实现基础认证(`Basic Authentication`)。示例包含以下功能: 1. 设置带有`OpenAPI/Swagger`文档的基础HTTP服务器 2. 使用基础认证保护`Swagger`文档 3. 定义并实现一个简单的`REST API`端点 ## 依赖要求 - [Go](https://golang.org/dl/) 1.23 或更高版本 - [Git](https://git-scm.com/downloads) - [GoFrame](https://goframe.org) ## 目录结构 ```text . ├── README.MD # 英文文档 ├── README.ZH.MD # 中文文档 ├── go.mod # Go模块文件 └── main.go # 主程序入口 ``` ## 特性 - `OpenAPI/Swagger`文档集成 - API文档的基础认证保护 - `RESTful API`端点实现 - 请求/响应验证 - `API`文档注释 ## 安装设置 1. 克隆仓库: ```bash git clone https://github.com/gogf/examples.git cd examples/httpserver/swagger-auth ``` 2. 安装依赖: ```bash go mod tidy ``` 3. 运行应用: ```bash go run main.go ``` ## 使用方法 1. 启动服务器: ```bash go run main.go ``` 2. 访问受保护的`Swagger`文档: - 地址:http://localhost:8000/swagger - 基础认证凭据: - 用户:`admin` - 密码:`123456` 3. 测试API端点: - 访问地址:http://localhost:8000/hello?name=GoFrame - 请求方法:`GET` - 响应:包含问候消息的`JSON` ## 实现细节 本示例展示了几个关键特性: 1. `Swagger`集成: - `OpenAPI JSON`地址:`/api.json` - `Swagger UI`地址:`/swagger` - 两者都受基础认证保护 2. 基础认证: - 用户:`admin` - 密码:`123456` - 认证域:`Restricted` 3. API端点: - 路径:`/hello` - 方法:`GET` - 必需参数:`name` - 响应:带有问候消息的`JSON` ## 注意事项 - 基础认证使用`GoFrame`内置的`BasicAuth`方法实现 - `API`文档根据代码注释和结构体标签自动生成 - 服务器默认运行在`8000`端口 - 所有`API`响应都通过中间件自动包装成标准格式 # 配置管理 GitHub 源码: https://github.com/gogf/examples/tree/main/config ## 介绍 本分类包含了一些在 `GoFrame` 框架中使用配置管理组件相关的示例代码。 主要展示了如何使用 `GoFrame` 对接不同配置管理服务的能力。 ## 注意事项 1. 示例代码主要用于演示目的 2. 生产环境使用时需要根据实际需求进行调整 ## 示例列表 # Apollo GitHub 源码: https://github.com/gogf/examples/tree/main/config/apollo ## 介绍 本示例展示了如何在 `GoFrame` 应用程序中通过配置管理组件集成 `Apollo` 配置中心。 ## 目录结构 ```text . ├── boot/ # 启动配置 │ └── boot.go # Apollo 客户端初始化 ├── main.go # 主程序入口 ├── go.mod # Go 模块文件 └── go.sum # Go 模块校验和 ``` ## 环境要求 - [Go](https://golang.org/dl/) `1.23` 或更高版本 - [Git](https://git-scm.com/downloads) - [GoFrame](https://goframe.org) - [GoFrame Apollo Config](https://github.com/gogf/gf/tree/master/contrib/config/apollo) ## 使用说明 1. 确保 `Apollo` 服务端已启动 2. 配置 `Apollo` 连接信息: ```yaml apollo: appId: "your-app-id" cluster: "default" namespaceName: "application" ip: "localhost:8080" ``` 3. 运行示例: ```bash go run main.go ``` # Consul GitHub 源码: https://github.com/gogf/examples/tree/main/config/consul ## 介绍 本示例展示了如何在 `GoFrame` 应用程序中通过配置管理组件集成 `Consul` 配置中心。 ## 目录结构 ```text . ├── boot/ # 启动配置 │ └── boot.go # Consul 客户端初始化 ├── main.go # 主程序入口 ├── go.mod # Go 模块文件 └── go.sum # Go 模块校验和 ``` ## 环境要求 - [Go](https://golang.org/dl/) `1.23` 或更高版本 - [Git](https://git-scm.com/downloads) - [GoFrame](https://goframe.org) - [GoFrame Consul Config](https://github.com/gogf/gf/tree/master/contrib/config/consul) ## 使用说明 1. 确保 `Consul` 服务端已启动 2. 配置 `Consul` 连接信息: ```yaml consul: address: "localhost:8500" scheme: "http" datacenter: "dc1" ``` 3. 运行示例: ```bash go run main.go ``` # Nacos GitHub 源码: https://github.com/gogf/examples/tree/main/config/nacos ## 介绍 本示例展示了如何在 `GoFrame` 应用程序中通过配置管理组件集成 `Nacos` 配置中心。 ## 目录结构 ```text . ├── boot/ # 启动配置 │ └── boot.go # Nacos 客户端初始化 ├── main.go # 主程序入口 ├── go.mod # Go 模块文件 └── go.sum # Go 模块校验和 ``` ## 环境要求 - [Go](https://golang.org/dl/) `1.23` 或更高版本 - [Git](https://git-scm.com/downloads) - [GoFrame](https://goframe.org) - [GoFrame Nacos Config](https://github.com/gogf/gf/tree/master/contrib/config/nacos) - [Nacos Server](https://nacos.io/) ## 使用说明 1. 确保 `Nacos` 服务端已启动 2. 配置 `Nacos` 连接信息: ```yaml nacos: serverAddr: "127.0.0.1:8848" namespace: "public" dataId: "example" group: "DEFAULT_GROUP" ``` 3. 运行示例: ```bash go run main.go ``` # Polaris GitHub 源码: https://github.com/gogf/examples/tree/main/config/polaris ## 介绍 本示例展示了如何在 `GoFrame` 应用程序中通过配置管理组件集成 `Polaris` 配置中心。 ## 目录结构 ```text . ├── boot/ # 启动配置 │ └── boot.go # Polaris 客户端初始化 ├── main.go # 主程序入口 ├── go.mod # Go 模块文件 └── go.sum # Go 模块校验和 ``` ## 环境要求 - [Go](https://golang.org/dl/) `1.23` 或更高版本 - [Git](https://git-scm.com/downloads) - [GoFrame](https://goframe.org) - [GoFrame Polaris Config](https://github.com/gogf/gf/tree/master/contrib/config/polaris) - [Polaris Server](https://polarismesh.cn/) ## 使用说明 1. 启动 `Polaris`: ```bash docker run -d --name polaris \ -p 8090:8090 -p 8091:8091 -p 8093:8093 -p 9090:9090 -p 9091:9091 \ polarismesh/polaris-standalone:v1.17.2 ``` 2. 启动服务: ```bash go run main.go ``` # Kubernetes ConfigMap GitHub 源码: https://github.com/gogf/examples/tree/main/config/kubecm ## 介绍 本示例展示了如何在 `GoFrame` 应用程序中通过配置管理组件集成 `Kubernetes ConfigMap`。 ## 目录结构 ```text . ├── boot_in_pod/ # Pod内部署的启动配置 │ └── boot.go # Pod内客户端初始化 ├── boot_out_pod/ # Pod外部署的启动配置 │ └── boot.go # Pod外客户端初始化 ├── main.go # 主程序入口 ├── go.mod # Go 模块文件 └── go.sum # Go 模块校验和 ``` ## 环境要求 - [Go](https://golang.org/dl/) `1.23` 或更高版本 - [Git](https://git-scm.com/downloads) - [GoFrame](https://goframe.org) - [GoFrame Kubernetes ConfigMap Config](https://github.com/gogf/gf/tree/master/contrib/config/kubecm) # 注册发现 GitHub 源码: https://github.com/gogf/examples/tree/main/registry ## 介绍 本分类包含了一些 `GoFrame` 框架中注册发现相关的示例代码。这些示例展示了如何在不同场景下使用 `GoFrame` 的注册发现功能。 ## 注意事项 1. 示例代码主要用于演示目的 2. 生产环境使用时需要根据实际需求进行调整 ## 示例列表 # Consul GitHub 源码: https://github.com/gogf/examples/tree/main/registry/consul ## 简介 本示例演示了如何在 `GoFrame` 应用程序中集成 `Consul` 服务注册中心。主要展示: - 使用 `Consul` 注册服务 - 使用 `Consul` 发现服务 - 实现服务健康检查 - 构建分布式系统 ## 目录结构 ```text . ├── docker-compose/ # 运行Consul的Docker配置文件 ├── grpc/ # gRPC服务示例 │ ├── client/ # gRPC客户端实现 │ ├── controller/ # gRPC服务控制器 │ ├── protobuf/ # Protocol buffer定义 │ └── server/ # gRPC服务器实现 │ ├── main.go # 服务器启动代码 │ └── config.yaml # 服务器配置 ├── http/ # HTTP服务示例 │ ├── client/ # HTTP客户端实现 │ └── server/ # HTTP服务器实现 ├── go.mod # Go模块文件 └── go.sum # Go模块校验和 ``` ## 环境要求 - `Go` `1.23` 或更高版本 - `Docker` (用于运行 `Consul`) - `Consul` ## 使用说明 1. 启动 `Consul` 服务: ```bash cd docker-compose docker-compose up -d ``` 2. `HTTP` 服务示例: ```bash # 启动HTTP服务器 cd http/server go run server.go # 运行HTTP客户端 cd http/client go run client.go ``` 3. `gRPC` 服务示例: ```bash # 启动gRPC服务器 cd grpc/server go run server.go # 运行gRPC客户端 cd grpc/client go run client.go ``` ## 实现说明 1. `HTTP` 服务实现 - 服务器使用 `g.Server` 创建 `HTTP` 服务 - 通过 `gsvc.SetRegistry` 设置 `Consul` 注册中心 - 客户端使用 `g.Client()` 自动发现并访问服务 2. `gRPC` 服务实现 - 服务器使用 `grpc.Server` 创建 `gRPC` 服务 - 通过 `gsvc.SetRegistry` 设置 `Consul` 注册中心 - 客户端使用 `grpc.Client` 自动发现并访问服务 ## 注意事项 1. 确保 `Consul` 服务正常运行在 `127.0.0.1:8500` 2. 服务注册时会自动添加健康检查 3. 客户端会自动使用服务发现进行负载均衡 # Etcd GitHub 源码: https://github.com/gogf/examples/tree/main/registry/etcd ## 简介 本示例演示了如何在 `GoFrame` 应用程序中集成 `Etcd` 服务注册中心。主要展示: - 使用 `Etcd` 注册服务 - 使用 `Etcd` 发现服务 - 实现服务健康检查 - 构建分布式系统 ## 目录结构 ```text . ├── grpc/ # gRPC服务示例 │ ├── client/ # gRPC客户端实现 │ ├── controller/ # gRPC服务控制器 │ ├── protobuf/ # Protocol buffer定义 │ └── server/ # gRPC服务器实现 │ ├── main.go # 服务器启动代码 │ └── config.yaml # 服务器配置 ├── http/ # HTTP服务示例 │ ├── client/ # HTTP客户端实现 │ └── server/ # HTTP服务器实现 ├── go.mod # Go模块文件 └── go.sum # Go模块校验和 ``` ## 功能特性 本示例展示了以下功能: 1. 服务注册 - 自动服务注册 - 基于TTL的健康检查 - 元数据管理 - 租约管理 2. 服务发现 - 动态服务发现 - 负载均衡 - 故障转移支持 - Watch机制监控 3. 协议支持 - `HTTP` 服务注册与发现 - `gRPC` 服务注册与发现 ## 环境要求 - `Go` `1.23` 或更高版本 - `Docker` (用于运行 `Etcd`) - `Etcd` 3.4+ ## 使用说明 1. 启动 `Etcd` 服务: ```bash docker run -d --name etcd -p 2379:2379 -e ALLOW_NONE_AUTHENTICATION=yes bitnami/etcd:3.4.24 ``` 2. `HTTP` 服务示例: ```bash # 启动HTTP服务器 cd http/server go run server.go # 运行HTTP客户端 cd http/client go run client.go ``` 3. `gRPC` 服务示例: ```bash # 启动gRPC服务器 cd grpc/server go run server.go # 运行gRPC客户端 cd grpc/client go run client.go ``` ## 实现说明 1. `HTTP` 服务实现 - 服务器使用 `g.Server` 创建 `HTTP` 服务 - 通过 `gsvc.SetRegistry` 设置 `Etcd` 注册中心 - 客户端使用 `g.Client()` 自动发现并访问服务 - 支持自动的健康检查和租约续期 2. `gRPC` 服务实现 - 服务器使用 `grpc.Server` 创建 `gRPC` 服务 - 通过 `gsvc.SetRegistry` 设置 `Etcd` 注册中心 - 客户端使用 `grpc.Client` 自动发现并访问服务 - 支持自动的健康检查和租约续期 # File GitHub 源码: https://github.com/gogf/examples/tree/main/registry/file ## 简介 本示例演示了如何在 `GoFrame` 应用程序中使用基于文件系统的服务注册中心。主要展示: - 使用文件系统注册服务 - 从文件中发现服务 - 实现简单的服务发现 - 构建基础的分布式系统 ## 目录结构 ```text . ├── client/ # 客户端示例 │ └── client.go # 基于文件的服务发现客户端实现 ├── server/ # 服务器示例 │ └── server.go # 基于文件的服务注册服务器实现 ├── go.mod # Go模块文件 └── go.sum # Go模块校验和 ``` ## 功能特性 本示例展示了以下功能: 1. 服务注册 - 基于文件的服务注册 - 简单的元数据存储 - 本地文件系统集成 - 自动的文件管理 2. 服务发现 - 基于文件的服务发现 - 本地服务查找 - 基础负载均衡 - 简单的故障转移支持 3. 协议支持 - `HTTP` 服务支持 - 可扩展支持其他协议 ## 环境要求 - `Go` `1.23` 或更高版本 - 本地文件系统读写权限 ## 使用说明 1. 启动服务器: ```bash cd server go run server.go ``` 2. 运行客户端: ```bash cd client go run client.go ``` ## 实现说明 1. 服务器实现 - 使用 `g.Server` 创建 `HTTP` 服务 - 通过 `gsvc.SetRegistry` 设置文件注册中心 - 服务信息存储在系统临时目录 - 自动管理服务注册文件 2. 客户端实现 - 使用相同的临时目录路径 - 监控文件变化自动更新 - 支持基本的负载均衡 - 实现简单的故障转移 ## 注意事项 1. 确保服务器和客户端使用相同的注册目录 2. 适合开发和测试环境,不建议用于生产环境 3. 服务信息存储在本地文件系统,需要确保读写权限 4. 支持基本的服务发现和负载均衡功能 # Nacos GitHub 源码: https://github.com/gogf/examples/tree/main/registry/nacos ## 简介 本示例演示了如何在 `GoFrame` 应用程序中集成 `Nacos` 服务注册中心。主要展示: - 使用 `Nacos` 注册服务 - 使用 `Nacos` 发现服务 - 实现服务健康检查 - 构建分布式系统 ## 目录结构 ```text . ├── docker-compose/ # Docker配置文件 │ ├── docker-compose.yml # Docker编排配置 │ └── env/ # 环境变量配置 │ └── nacos.env # Nacos环境配置 ├── grpc/ # gRPC服务示例 │ ├── client/ # gRPC客户端实现 │ ├── controller/ # gRPC服务控制器 │ ├── protobuf/ # Protocol buffer定义 │ └── server/ # gRPC服务器实现 │ ├── main.go # 服务器启动代码 │ └── config.yaml # 服务器配置 ├── http/ # HTTP服务示例 │ ├── client/ # HTTP客户端实现 │ └── server/ # HTTP服务器实现 ├── go.mod # Go模块文件 └── go.sum # Go模块校验和 ``` ## 功能特性 本示例展示了以下功能: 1. 服务注册 - 自动服务注册 - 健康检查配置 - 元数据管理 - 命名空间和分组管理 2. 服务发现 - 动态服务发现 - 负载均衡 - 故障转移支持 - 服务订阅机制 3. 协议支持 - `HTTP` 服务注册与发现 - `gRPC` 服务注册与发现 ## 环境要求 - `Go` `1.23` 或更高版本 - `Docker` (用于运行 `Nacos`) - `Nacos` 2.1.2+ ## 使用说明 1. 启动 `Nacos` 服务: ```bash cd docker-compose docker-compose up -d ``` 2. `HTTP` 服务示例: ```bash # 启动HTTP服务器 cd http/server go run server.go # 运行HTTP客户端 cd http/client go run client.go ``` 3. `gRPC` 服务示例: ```bash # 启动gRPC服务器 cd grpc/server go run server.go # 运行gRPC客户端 cd grpc/client go run client.go ``` ## 实现说明 1. `HTTP` 服务实现 - 服务器使用 `g.Server` 创建 `HTTP` 服务 - 通过 `gsvc.SetRegistry` 设置 `Nacos` 注册中心 - 客户端使用 `g.Client()` 自动发现并访问服务 - 支持自动的健康检查和服务发现 2. `gRPC` 服务实现 - 服务器使用 `grpc.Server` 创建 `gRPC` 服务 - 通过 `gsvc.SetRegistry` 设置 `Nacos` 注册中心 - 客户端使用 `grpc.Client` 自动发现并访问服务 - 支持自动的健康检查和服务发现 # Polaris GitHub 源码: https://github.com/gogf/examples/tree/main/registry/polaris ## 简介 本示例演示了如何在 `GoFrame` 应用程序中集成 `Polaris` 服务注册中心。主要展示: - 使用 `Polaris` 注册服务 - 使用 `Polaris` 发现服务 - 实现服务健康检查 - 构建分布式系统 ## 目录结构 ```text . ├── client/ # 客户端示例 │ └── client.go # 基于Polaris的服务发现客户端实现 ├── server/ # 服务器示例 │ └── server.go # 基于Polaris的服务注册服务器实现 ├── go.mod # Go模块文件 └── go.sum # Go模块校验和 ``` ## 功能特性 本示例展示了以下功能: 1. 服务注册 - 自动服务注册 - 健康检查配置 - 元数据管理 - 服务路由 - 限流控制 2. 服务发现 - 动态服务发现 - 负载均衡 - 故障转移支持 - 熔断机制 - 容错处理 3. 协议支持 - `HTTP` 服务 - 自定义协议 - 协议扩展 ## 环境要求 - `Go` `1.23` 或更高版本 - `Polaris` 服务器 - 本地缓存目录读写权限 ## 使用说明 1. 配置 `Polaris` 服务器地址: ```go conf := config.NewDefaultConfiguration([]string{"183.47.111.80:8091"}) ``` 2. 配置本地缓存: ```go conf.Consumer.LocalCache.SetPersistDir("/tmp/polaris/backup") api.SetLoggersDir("/tmp/polaris/log") ``` 3. 启动服务器: ```bash cd server go run server.go ``` 4. 运行客户端: ```bash cd client go run client.go ``` ## 实现说明 1. 服务器实现 - 使用默认配置创建 `Polaris` 客户端 - 配置本地缓存提升性能 - 支持服务路由和限流配置 - 提供健康检查和故障恢复 2. 客户端实现 - 自动服务发现和负载均衡 - 支持熔断和容错机制 - 本地缓存提供离线能力 - 自动重试和故障转移 ## 高级特性 1. 本地缓存 - 服务信息持久化 - 离线运行支持 - 性能优化 2. 可靠性保障 - 熔断保护 - 限流控制 - 故障转移 - 容错处理 ## 注意事项 1. 确保配置正确的Polaris服务器地址 2. 本地缓存目录需要有读写权限 3. 建议开启日志记录便于调试 4. 合理配置熔断和限流参数 5. 注意监控服务健康状态 # 负载均衡 GitHub 源码: https://github.com/gogf/examples/tree/main/balancer ## 介绍 本分类包含了一些在 `GoFrame` 框架中实现负载均衡相关的示例代码。 ## 注意事项 1. 示例代码主要用于演示目的 2. 生产环境使用时需要根据实际需求进行调整 ## 示例列表 # HTTP负载均衡 GitHub 源码: https://github.com/gogf/examples/tree/main/balancer/http ## 介绍 本示例展示了如何在 `GoFrame` 应用程序中实现 HTTP 服务的负载均衡。主要演示: - 使用 `etcd` 配置负载均衡器 - 服务注册和服务发现 - 请求路由和负载分发 ## 目录结构 ```text . ├── server/ # HTTP 服务器 ├── client/ # HTTP 客户端 ├── go.mod # Go 模块文件 └── go.sum # Go 模块校验 ``` ## 环境要求 - [Go](https://golang.org/dl/) `1.23` 或更高版本 - [Git](https://git-scm.com/downloads) - [GoFrame](https://goframe.org) ## 使用说明 1. 启动服务器: ```bash go run server/main.go ``` 2. 在另一个终端启动客户端: ```bash go run client/main.go ``` # 使用Polaris实现负载均衡 GitHub 源码: https://github.com/gogf/examples/tree/main/balancer/polaris ## 介绍 本示例展示了如何在 `GoFrame` 中使用 `Polaris` 实现负载均衡。主要演示: - `Polaris` 负载均衡集成 - 服务发现机制 - 动态路由配置 - 故障转移实现 ## 目录结构 ```text . ├── server/ # HTTP 服务器 ├── client/ # HTTP 客户端 ├── go.mod # Go 模块文件 └── go.sum # Go 模块校验 ``` ## 环境要求 - [Go](https://golang.org/dl/) `1.23` 或更高版本 - [Git](https://git-scm.com/downloads) - [GoFrame](https://goframe.org) - [GoFrame Polaris Registry](https://github.com/gogf/gf/tree/master/contrib/registry/polaris) ## 配置说明 1. `Polaris` 配置示例: ```yaml balancer: polaris: address: "localhost:8091" namespace: "default" service: name: "demo-service" metadata: version: "1.0.0" loadbalancer: type: "weighted_random" weights: "instance-1": 3 "instance-2": 2 circuit_breaker: enabled: true error_rate: 0.5 min_request: 10 ``` ## 使用说明 1. 启动 `Polaris`: ```bash docker run -d --name polaris \ -p 8090:8090 -p 8091:8091 -p 8093:8093 -p 9090:9090 -p 9091:9091 \ polarismesh/polaris-standalone:v1.17.2 ``` 2. 启动服务: ```bash go run main.go ``` # 可观测性 GitHub 源码: https://github.com/gogf/examples/tree/main/observability ## 介绍 本分类包含了一些使用 `GoFrame` 框架实现可观测性的各种示例。 ## 示例列表 # 指标收集 GitHub 源码: https://github.com/gogf/examples/tree/main/observability/metric ## 介绍 本分类包含了一些在 GoFrame 框架中实现指标收集相关的示例代码。这些示例展示了如何在不同场景下使用 GoFrame 的指标收集功能。 ## 注意事项 1. 示例代码主要用于演示目的 2. 生产环境使用时需要根据实际需求进行调整 ## 示例列表 # 基础用法 GitHub 源码: https://github.com/gogf/examples/tree/main/observability/metric/basic ## 简介 本示例演示了如何在 `GoFrame` 中使用 `OpenTelemetry` 和 `Prometheus` 集成来实现基础的指标收集功能。主要展示: - 创建和使用不同类型的指标 - 配置指标属性 - 导出 `Prometheus` 格式的指标 - 设置指标暴露端点 ## 目录结构 ```text . ├── go.mod # Go模块文件 ├── go.sum # Go模块校验和 └── main.go # 主程序,演示指标使用 ``` ## 功能特性 本示例展示了以下指标类型: 1. Counter (计数器) - 累积型测量 - 只能增加 - 用于事件计数 - 示例:请求总数、字节数 2. UpDownCounter (上下计数器) - 双向计数器 - 可增可减 - 用于测量变化量 - 示例:队列长度、连接数 3. Histogram (直方图) - 测量值分布 - 可配置的桶 - 用于延迟/大小测量 - 示例:请求延迟、响应大小 4. Observable指标 - ObservableCounter - ObservableUpDownCounter - ObservableGauge - 通过回调更新 - 示例:CPU使用率、内存使用 ## 环境要求 - `Go` `1.23` 或更高版本 - `GoFrame` 框架 - `GoFrame OpenTelemetry Metric` 扩展 ## 使用说明 1. 运行示例: ```bash go run main.go ``` 2. 访问指标: ```bash # 使用curl curl http://localhost:8000/metrics # 或在浏览器中打开 http://localhost:8000/metrics ``` 3. 指标输出示例: ```text # HELP goframe_metric_demo_counter 这是Counter使用的简单演示 goframe_metric_demo_counter{const_attr_1="1"} 11 # HELP goframe_metric_demo_histogram 这是histogram使用的简单演示 goframe_metric_demo_histogram_bucket{const_attr_3="3",le="0"} 0 goframe_metric_demo_histogram_bucket{const_attr_3="3",le="10"} 1 ... ``` ## 实现说明 1. Counter实现 ```go counter := meter.MustCounter( "goframe.metric.demo.counter", gmetric.MetricOption{ Help: "计数器使用示例", Unit: "bytes", }, ) counter.Inc(ctx) // 增加1 counter.Add(ctx, 10) // 增加指定值 ``` 2. UpDownCounter实现 ```go upDownCounter := meter.MustUpDownCounter( "goframe.metric.demo.updown_counter", gmetric.MetricOption{ Help: "上下计数器使用示例", Unit: "%", }, ) upDownCounter.Inc(ctx) // 增加1 upDownCounter.Dec(ctx) // 减少1 ``` 3. Histogram实现 ```go histogram := meter.MustHistogram( "goframe.metric.demo.histogram", gmetric.MetricOption{ Help: "直方图使用示例", Unit: "ms", Buckets: []float64{0, 10, 20, 50, 100}, }, ) histogram.Record(30) // 记录测量值 ``` 4. Observable指标实现 ```go observableGauge := meter.MustObservableGauge( "goframe.metric.demo.observable_gauge", gmetric.MetricOption{ Help: "可观察量表使用示例", Unit: "%", }, ) meter.MustRegisterCallback(func(ctx context.Context, obs gmetric.Observer) error { obs.Observe(observableGauge, 30) return nil }, observableGauge) ``` # 回调处理 GitHub 源码: https://github.com/gogf/examples/tree/main/observability/metric/callback ## 简介 本示例演示了如何在 `GoFrame` 中使用 `OpenTelemetry` 和 `Prometheus` 集成来实现基于回调的指标收集。主要展示: - 创建带有回调函数的指标 - 通过回调自动更新指标值 - 配置指标属性 - 导出 `Prometheus` 格式的指标 ## 目录结构 ```text . ├── go.mod # Go 模块文件 ├── go.sum # Go 模块校验和 └── main.go # 主程序,演示回调式指标使用 ``` ## 功能特性 本示例展示了以下功能: 1. 基于回调的指标 - 自动值更新 - 自定义回调函数 - 无需手动更新 - 适用于动态值监控 2. 常规指标 - 手动值更新 - 直接控制值 - 与回调方式对比 - 适用于事件计数 3. 指标配置 - 自定义属性 - 帮助文本 - 单位设置 - 常量标签 ## 环境要求 - `Go` `1.23` 或更高版本 - `GoFrame` 框架 - `GoFrame OpenTelemetry Metric` 扩展 ## 使用说明 1. 运行示例: ```bash go run main.go ``` 2. 访问指标: ```bash # 使用 curl curl http://localhost:8000/metrics # 或在浏览器中打开 http://localhost:8000/metrics ``` 3. 指标输出示例: ```text # HELP goframe_metric_demo_counter 这是 Counter 使用的简单演示 goframe_metric_demo_counter{const_attr_1="1"} 11 # HELP goframe_metric_demo_observable_counter 这是 ObservableCounter 使用的简单演示 goframe_metric_demo_observable_counter{const_attr_3="3"} 10 ``` ## 实现说明 1. 回调式指标实现 ```go observableCounter := meter.MustObservableCounter( "goframe.metric.demo.observable_counter", gmetric.MetricOption{ Help: "回调计数器示例", Unit: "%", Callback: func(ctx context.Context, obs gmetric.MetricObserver) error { obs.Observe(10) // 自动设置值为 10 return nil }, }, ) ``` 2. 常规指标实现 ```go counter := meter.MustCounter( "goframe.metric.demo.counter", gmetric.MetricOption{ Help: "普通计数器示例", Unit: "%", }, ) counter.Inc(ctx) // 手动增加 1 counter.Add(ctx, 10) // 手动增加指定值 ``` # 动态属性 GitHub 源码: https://github.com/gogf/examples/tree/main/observability/metric/dynamic_attributes ## 简介 本示例演示了如何在 `GoFrame` 中使用 `OpenTelemetry` 和 `Prometheus` 集成来实现动态指标属性。主要展示: - 在运行时为指标添加动态属性 - 组合使用常量属性和动态属性 - 在常规指标和可观察指标中使用属性 - 导出带有动态属性的指标 ## 目录结构 ```text . ├── go.mod # Go模块文件 ├── go.sum # Go模块校验和 └── main.go # 主程序,演示动态属性使用 ``` ## 功能特性 本示例展示了以下功能: 1. 动态属性 - 运行时属性赋值 - 属性组合使用 - 基于值的属性 - 灵活的标签管理 2. 带属性的指标类型 - 带动态属性的计数器 - 带动态属性的可观察计数器 - 常量属性基线 - 属性继承机制 3. 属性管理 - 属性创建 - 值类型处理 - 属性作用域 - 属性生命周期 ## 环境要求 - `Go` `1.23` 或更高版本 - `GoFrame` 框架 - `GoFrame OpenTelemetry Metric` 扩展 ## 使用说明 1. 运行示例: ```bash go run main.go ``` 2. 访问指标: ```bash # 使用curl curl http://localhost:8000/metrics # 或在浏览器中打开 http://localhost:8000/metrics ``` 3. 指标输出示例: ```text # HELP goframe_metric_demo_counter 这是Counter使用的简单演示 goframe_metric_demo_counter{const_attr_1="1",dynamic_attr_2="2"} 11 # HELP goframe_metric_demo_observable_counter 这是ObservableCounter使用的简单演示 goframe_metric_demo_observable_counter{const_attr_4="4",dynamic_attr_1="1"} 10 ``` ## 实现说明 1. 常规指标的动态属性 ```go counter := meter.MustCounter( "goframe.metric.demo.counter", gmetric.MetricOption{ Help: "计数器示例", Unit: "bytes", Attributes: gmetric.Attributes{ gmetric.NewAttribute("const_attr_1", 1), // 常量属性 }, }, ) // 添加动态属性 counter.Add(ctx, 10, gmetric.Option{ Attributes: gmetric.Attributes{ gmetric.NewAttribute("dynamic_attr_2", 2), // 动态属性 }, }) ``` 2. 可观察指标的动态属性 ```go observableCounter := meter.MustObservableCounter( "goframe.metric.demo.observable_counter", gmetric.MetricOption{ Help: "可观察计数器示例", Unit: "%", Attributes: gmetric.Attributes{ gmetric.NewAttribute("const_attr_4", 4), // 常量属性 }, }, ) // 在回调中添加动态属性 meter.MustRegisterCallback(func(ctx context.Context, obs gmetric.Observer) error { obs.Observe(observableCounter, 10, gmetric.Option{ Attributes: gmetric.Attributes{ gmetric.NewAttribute("dynamic_attr_1", 1), // 动态属性 }, }) return nil }, observableCounter) ``` # 全局属性 GitHub 源码: https://github.com/gogf/examples/tree/main/observability/metric/global_attributes ## 简介 本示例演示了如何在 `GoFrame` 中使用 `OpenTelemetry` 和 `Prometheus` 集成来实现全局指标属性。主要展示: - 设置和管理全局属性 - 在多个指标中应用全局属性 - 配置属性作用域和匹配模式 - 组合使用全局属性和局部属性 ## 目录结构 ```text . ├── go.mod # Go模块文件 ├── go.sum # Go模块校验和 └── main.go # 主程序,演示全局属性使用 ``` ## 功能特性 本示例展示了以下功能: 1. 全局属性 - 全局属性配置 - 属性继承机制 - 基于模式的应用 - 版本控制支持 2. 带全局属性的指标类型 - 带全局属性的计数器 - 带全局属性的可观察计数器 - 局部属性组合 - 属性优先级 3. 属性管理 - 全局属性作用域 - 基于版本的过滤 - 基于模式的过滤 - 属性继承规则 ## 环境要求 - `Go` `1.23` 或更高版本 - `GoFrame` 框架 - `GoFrame OpenTelemetry Metric` 扩展 ## 使用说明 1. 运行示例: ```bash go run main.go ``` 2. 访问指标: ```bash # 使用curl curl http://localhost:8000/metrics # 或在浏览器中打开 http://localhost:8000/metrics ``` 3. 指标输出示例: ```text # HELP goframe_metric_demo_counter 这是Counter使用的简单演示 goframe_metric_demo_counter{const_attr_1="1",global_attr_1="1"} 11 # HELP goframe_metric_demo_observable_counter 这是ObservableCounter使用的简单演示 goframe_metric_demo_observable_counter{const_attr_2="2",global_attr_1="1"} 10 ``` ## 实现说明 1. 设置全局属性 ```go gmetric.SetGlobalAttributes(gmetric.Attributes{ gmetric.NewAttribute("global_attr_1", 1), // 全局属性 }, gmetric.SetGlobalAttributesOption{ Instrument: instrument, // 仅应用于特定的instrument InstrumentVersion: instrumentVersion, // 仅应用于特定版本 InstrumentPattern: "", // 空模式表示应用于所有指标 }) ``` 2. 带全局属性的指标 ```go counter := meter.MustCounter( "goframe.metric.demo.counter", gmetric.MetricOption{ Help: "计数器示例", Unit: "bytes", Attributes: gmetric.Attributes{ gmetric.NewAttribute("const_attr_1", 1), // 局部常量属性 }, }, ) ``` 3. 在可观察指标中使用全局属性 ```go meter.MustRegisterCallback(func(ctx context.Context, obs gmetric.Observer) error { obs.Observe(observableCounter, 10) // 全局属性会自动添加 return nil }, observableCounter) ``` # HTTP客户端 GitHub 源码: https://github.com/gogf/examples/tree/main/observability/metric/http_client ## 简介 本示例演示了如何在 `GoFrame` 中使用 `OpenTelemetry` 和 `Prometheus` 集成来收集和监控HTTP客户端指标。主要展示: - 监控HTTP客户端请求 - 跟踪请求持续时间 - 收集响应状态码 - 导出客户端指标数据 ## 目录结构 ```text . ├── go.mod # Go模块文件 ├── go.sum # Go模块校验和 └── main.go # 主程序,演示HTTP客户端指标收集 ``` ## 功能特性 本示例展示了以下指标: 1. 请求指标 - 总请求数(Total Requests Count) - 活跃请求数(Active Requests) - 请求持续时间(Request Duration) - 请求大小(Request Size) 2. 响应指标 - 响应状态码(Response Status Codes) - 响应大小(Response Size) - 错误计数(Error Count) - 响应持续时间(Response Duration) 3. 连接指标 - 连接池统计(Connection Pool Stats) - DNS查询持续时间(DNS Lookup Duration) - TLS握手持续时间(TLS Handshake Duration) - 连接建立时间(Connection Establishment Time) ## 环境要求 - `Go` `1.23` 或更高版本 - `GoFrame` 框架 - `GoFrame OpenTelemetry Metric` 扩展 ## 使用说明 1. 运行示例: ```bash go run main.go ``` 2. 访问指标: ```bash # 使用curl curl http://localhost:8000/metrics # 或在浏览器中打开 http://localhost:8000/metrics ``` 3. 指标输出示例: ```text # HELP goframe_http_client_request_duration_seconds HTTP请求的持续时间 goframe_http_client_request_duration_seconds_bucket{method="GET",status="200",url="https://goframe.org",le="0.1"} 1 # HELP goframe_http_client_requests_total HTTP请求的总数 goframe_http_client_requests_total{method="GET",status="200",url="https://goframe.org"} 1 # HELP goframe_http_client_response_size_bytes HTTP响应的大小 goframe_http_client_response_size_bytes{method="GET",status="200",url="https://goframe.org"} 12345 ``` ## 实现说明 1. 配置指标导出器 ```go // 创建Prometheus导出器,用于指标数据的导出 exporter, err := prometheus.New( prometheus.WithoutCounterSuffixes(), // 移除计数器后缀以保持指标名称简洁 prometheus.WithoutUnits(), // 移除单位后缀以保持指标名称简洁 ) if err != nil { g.Log().Fatal(ctx, err) } ``` 2. 初始化OpenTelemetry提供者 ```go // 初始化并配置OpenTelemetry提供者 provider := otelmetric.MustProvider( otelmetric.WithReader(exporter), // 配置指标读取器 otelmetric.WithBuiltInMetrics(), // 启用内置指标收集 ) provider.SetAsGlobal() // 设置为全局指标提供者 defer provider.Shutdown(ctx) // 确保程序退出时正确关闭提供者 ``` 3. 使用HTTP客户端 ```go // 发起HTTP请求并自动收集指标 url := `https://goframe.org` content := g.Client().GetContent(ctx, url) // 发起HTTP GET请求 ``` # HTTP服务器 GitHub 源码: https://github.com/gogf/examples/tree/main/observability/metric/http_server ## 简介 本示例演示了如何在 `GoFrame` 中使用 `OpenTelemetry` 和 `Prometheus` 集成来收集和监控HTTP服务器指标。主要展示: - 监控HTTP服务器请求 - 跟踪请求延迟 - 收集错误率 - 导出服务器端指标 ## 目录结构 ```text . ├── go.mod # Go模块文件 ├── go.sum # Go模块校验和 └── main.go # 主程序,演示HTTP服务器指标收集 ``` ## 功能特性 本示例展示了以下端点和指标: 1. 端点(Endpoints) - `/`: 基础端点,返回"ok" - `/error`: 触发错误的端点 - `/sleep`: 带5秒延迟的端点 - `/metrics`: Prometheus指标端点 2. 请求指标 - 总请求数(Total Requests Count) - 活跃请求数(Active Requests) - 请求持续时间(Request Duration) - 请求大小(Request Size) 3. 响应指标 - 响应状态码(Response Status Codes) - 响应大小(Response Size) - 错误计数(Error Count) - 响应延迟(Response Latency) 4. 服务器指标 - 协程数量(Goroutine Count) - 内存使用(Memory Usage) - GC统计(GC Statistics) - 连接统计(Connection Stats) ## 环境要求 - `Go` `1.23` 或更高版本 - `GoFrame` 框架 - `GoFrame OpenTelemetry Metric` 扩展 ## 使用说明 1. 运行示例: ```bash go run main.go ``` 2. 测试不同端点: ```bash # 基础请求 curl http://localhost:8000/ # 错误请求 curl http://localhost:8000/error # 慢请求 curl http://localhost:8000/sleep ``` 3. 查看指标: ```bash # 使用curl curl http://localhost:8000/metrics # 或在浏览器中打开 http://localhost:8000/metrics ``` 4. 指标输出示例: ```text # HELP goframe_http_server_requests_total HTTP请求总数 goframe_http_server_requests_total{method="GET",path="/",status="200"} 1 # HELP goframe_http_server_request_duration_seconds HTTP请求持续时间 goframe_http_server_request_duration_seconds_bucket{method="GET",path="/sleep",status="200",le="5.0"} 1 # HELP goframe_http_server_panics_total 导致panic的HTTP请求总数 goframe_http_server_panics_total{method="GET",path="/error"} 1 ``` ## 实现说明 1. 配置指标导出器 ```go // 创建Prometheus导出器,用于指标数据的导出 exporter, err := prometheus.New( prometheus.WithoutCounterSuffixes(), // 移除计数器后缀以保持指标名称简洁 prometheus.WithoutUnits(), // 移除单位后缀以保持指标名称简洁 ) ``` 2. 初始化OpenTelemetry提供者 ```go // 初始化并配置OpenTelemetry提供者 provider := otelmetric.MustProvider( otelmetric.WithReader(exporter), // 配置指标读取器 otelmetric.WithBuiltInMetrics(), // 启用内置指标收集 ) provider.SetAsGlobal() // 设置为全局指标提供者 defer provider.Shutdown(ctx) // 确保程序退出时正确关闭提供者 ``` 3. 配置HTTP服务器端点 ```go s := g.Server() // 基础端点 s.BindHandler("/", func(r *ghttp.Request) { r.Response.Write("ok") }) // 错误端点 s.BindHandler("/error", func(r *ghttp.Request) { panic("error") }) // 慢请求端点 s.BindHandler("/sleep", func(r *ghttp.Request) { time.Sleep(time.Second * 5) r.Response.Write("ok") }) // 指标端点 s.BindHandler("/metrics", otelmetric.PrometheusHandler) ``` # 指标计量器属性 GitHub 源码: https://github.com/gogf/examples/tree/main/observability/metric/meter_attributes ## 简介 本示例演示了如何在 `GoFrame` 中使用 `OpenTelemetry` 和 `Prometheus` 集成来配置和管理指标计量器级别的属性。主要展示: - 配置计量器级别属性 - 在所有指标中应用属性 - 组合计量器和指标属性 - 管理属性继承关系 ## 目录结构 ```text . ├── go.mod # Go模块文件 ├── go.sum # Go模块校验和 └── main.go # 主程序,演示计量器属性使用 ``` ## 功能特性 本示例展示了以下功能: 1. 计量器属性 - 全局计量器配置 - 计量器级别属性 - 属性继承机制 2. 带继承属性的指标类型 - 带组合属性的计数器(Counter) - 带组合属性的可观察计数器(Observable Counter) - 属性优先级规则 3. 属性管理 - 计量器属性配置 - 基于版本的属性 - 属性组合方式 ## 环境要求 - `Go` `1.23` 或更高版本 - `GoFrame` 框架 - `GoFrame OpenTelemetry Metric` 扩展 ## 使用说明 1. 运行示例: ```bash go run main.go ``` 2. 访问指标: ```bash # 使用curl curl http://localhost:8000/metrics # 或在浏览器中打开 http://localhost:8000/metrics ``` 3. 指标输出示例: ```text # HELP goframe_metric_demo_counter 这是Counter用法的简单演示 goframe_metric_demo_counter{const_attr_1="1",meter_label_1="1",meter_label_2="2"} 11 # HELP goframe_metric_demo_observable_counter 这是ObservableCounter用法的简单演示 goframe_metric_demo_observable_counter{const_attr_2="2",meter_label_1="1",meter_label_2="2"} 10 ``` ## 实现说明 1. 配置计量器属性 ```go // 创建全局计量器实例,配置计量器级别属性 meter = gmetric.GetGlobalProvider().Meter(gmetric.MeterOption{ Instrument: instrument, // 指标标识符 InstrumentVersion: instrumentVersion, // 指标版本 Attributes: gmetric.Attributes{ // 计量器级别属性 gmetric.NewAttribute("meter_label_1", 1), gmetric.NewAttribute("meter_label_2", 2), }, }) ``` 2. 创建带属性的指标 ```go // 创建Counter指标,包含计量器属性和指标特定属性 counter = meter.MustCounter( "goframe.metric.demo.counter", gmetric.MetricOption{ Help: "这是Counter用法的简单演示", Unit: "bytes", Attributes: gmetric.Attributes{ gmetric.NewAttribute("const_attr_1", 1), }, }, ) ``` 3. 注册可观察指标回调 ```go // 注册回调函数,自动继承计量器属性 meter.MustRegisterCallback(func(ctx context.Context, obs gmetric.Observer) error { obs.Observe(observableCounter, 10) return nil }, observableCounter) ``` # Prometheus集成 GitHub 源码: https://github.com/gogf/examples/tree/main/observability/metric/prometheus ## 简介 本示例演示了如何在 `GoFrame` 中直接集成 `Prometheus` 指标,而不使用 `OpenTelemetry`。主要展示: - 直接创建Prometheus指标 - 向Prometheus注册表注册指标 - 通过HTTP端点暴露指标 - 动态更新指标值 ## 目录结构 ```text . ├── go.mod # Go模块文件 ├── go.sum # Go模块校验和 └── prometheus.go # 主程序,演示Prometheus直接集成 ``` ## 功能特性 本示例展示了以下功能: 1. 指标类型 - 计数器(Counter):单调递增的值 - 仪表盘(Gauge):可上下变化的值 2. 指标操作 - 指标注册 - 值更新 - HTTP暴露 - 随机值生成 3. HTTP端点 - `/`: 触发指标更新 - `/metrics`: 暴露Prometheus指标 ## 环境要求 - `Go` `1.23` 或更高版本 - `GoFrame` 框架 - `Prometheus` 客户端库 ## 使用说明 1. 运行示例: ```bash go run prometheus.go ``` 2. 测试应用: ```bash # 生成一些指标值 curl http://localhost:8000/ # 查看指标 curl http://localhost:8000/metrics ``` 3. 指标输出示例: ```text # HELP demo_counter 这是一个演示计数器 # TYPE demo_counter counter demo_counter 1 # HELP demo_gauge 这是一个演示仪表盘 # TYPE demo_gauge gauge demo_gauge 42 ``` ## 实现说明 1. 定义指标变量 ```go // 创建计数器类型指标 metricCounter = promauto.NewCounter( prometheus.CounterOpts{ Name: "demo_counter", // Prometheus格式的指标名称 Help: "这是一个演示计数器", // 指标描述 }, ) // 创建仪表盘类型指标 metricGauge = promauto.NewGauge( prometheus.GaugeOpts{ Name: "demo_gauge", // Prometheus格式的指标名称 Help: "这是一个演示仪表盘", // 指标描述 }, ) ``` 2. 注册指标 ```go // 创建Prometheus注册表 registry := prometheus.NewRegistry() // 注册指标 registry.MustRegister( metricCounter, // 注册计数器指标 metricGauge, // 注册仪表盘指标 ) ``` 3. 配置HTTP服务器 ```go s := g.Server() // 更新指标的处理器 s.BindHandler("/", func(r *ghttp.Request) { metricCounter.Add(1) // 增加计数器 metricGauge.Set(float64(grand.N(1, 100))) // 设置随机仪表盘值 r.Response.Write("fake ok") }) // 暴露指标的处理器 s.BindHandler("/metrics", ghttp.WrapH(promhttp.Handler())) ``` # 链路跟踪 GitHub 源码: https://github.com/gogf/examples/tree/main/observability/trace ## 介绍 本分类包含了一些在 GoFrame 框架中链路跟踪相关的示例代码。这些示例展示了如何在不同场景下使用 GoFrame 的链路跟踪功能。 ## 注意事项 1. 示例代码主要用于演示目的 2. 生产环境使用时需要根据实际需求进行调整 ## 示例列表 # gRPC带数据库 GitHub 源码: https://github.com/gogf/examples/tree/main/observability/trace/grpc-with-db ## 简介 本示例演示了如何在 `GoFrame` 中实现gRPC服务与数据库交互的分布式跟踪。主要展示: - 配置gRPC服务的跟踪 - 跟踪数据库操作 - 传播跟踪上下文 - 可视化分布式跟踪 ## 环境要求 - `Go` `1.23` 或更高版本 - `GoFrame` 框架 - `GoFrame gRPCx` - `GoFrame MySQL` 驱动 - `GoFrame Etcd` 注册中心 - `GoFrame OpenTelemetry` 跟踪 ## 目录结构 ```text . ├── client/ # 客户端示例 │ └── client.go # 带跟踪的客户端 ├── controller/ # 服务控制器 ├── protobuf/ # 协议定义 │ └── user/ # 用户服务proto文件 ├── server/ # 服务端示例 │ ├── server.go # 带跟踪的服务端 │ └── config.yaml # 服务端配置 ├── sql.sql # 数据库模式 ├── go.mod # Go模块文件 └── go.sum # Go模块校验和 ``` ## 前置条件 1. 运行 `MySQL` 实例: ```bash docker run -d --name mysql \ -p 3306:3306 \ -e MYSQL_DATABASE=test \ -e MYSQL_ROOT_PASSWORD=12345678 \ mysql:5.7 ``` 2. 初始化数据库模式: ```bash # 连接MySQL容器 docker exec -i mysql mysql -uroot -p12345678 test < sql.sql ``` 3. 运行 `Redis` 实例: ```bash docker run -d --name redis \ -p 6379:6379 \ redis:6.0 ``` 4. 运行 `Jaeger` 实例: ```bash docker run --rm --name jaeger \ -e COLLECTOR_ZIPKIN_HOST_PORT=:9411 \ -p 6831:6831/udp \ -p 6832:6832/udp \ -p 5778:5778 \ -p 16686:16686 \ -p 4317:4317 \ -p 4318:4318 \ -p 14250:14250 \ -p 14268:14268 \ -p 14269:14269 \ -p 9411:9411 \ jaegertracing/all-in-one:1.55 ``` 5. 安装 `Protocol buffer` 编译器: ```bash # macOS brew install protobuf # 安装protoc-gen-go和protoc-gen-go-grpc go install google.golang.org/protobuf/cmd/protoc-gen-go@latest go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest ``` ## 配置说明 服务端配置在 `server/config.yaml` 中定义: ```yaml grpc: name: "demo" # 服务名称 logStdout: true # 启用标准输出日志 errorLogEnabled: true # 启用错误日志 accessLogEnabled: true # 启用访问日志 errorStack: true # 启用错误堆栈跟踪 database: logger: level: "all" # 记录所有SQL操作 stdout: true # 打印到标准输出 default: link: "mysql:root:12345678@tcp(127.0.0.1:3306)/test" debug: true # 启用调试模式 redis: default: address: 127.0.0.1:6379 # 默认Redis实例 db: 0 cache: address: 127.0.0.1:6379 # 缓存Redis实例 db: 1 ``` ## 使用说明 1. 生成协议代码: ```bash cd protobuf protoc --go_out=paths=source_relative:. --go-grpc_out=paths=source_relative:. ./user/*.proto ``` 2. 启动服务端: ```bash cd server go run server.go ``` 3. 运行客户端: ```bash cd client go run client.go ``` 4. 查看跟踪: 在浏览器中打开 http://localhost:16686 查看 `Jaeger` UI中的跟踪信息。 ## 实现说明 1. 服务端实现 ```go // 配置服务发现 grpcx.Resolver.Register(etcd.New("127.0.0.1:2379")) // 初始化跟踪 shutdown, err := otlpgrpc.Init(serviceName, endpoint, traceToken) // 配置Redis作为ORM缓存适配器 g.DB().GetCache().SetAdapter(gcache.NewAdapterRedis(g.Redis())) ``` 2. 客户端实现 ```go // 创建新的跟踪span ctx, span := gtrace.NewSpan(gctx.New(), "StartRequests") defer span.End() // 设置跟踪的baggage值 ctx = gtrace.SetBaggageValue(ctx, "uid", 100) // 创建gRPC客户端 client := user.NewUserClient(grpcx.Client.MustNewGrpcClientConn("demo")) ``` # HTTP服务 GitHub 源码: https://github.com/gogf/examples/tree/main/observability/trace/http ## 简介 本示例演示了如何在 `GoFrame` 中实现HTTP服务的分布式跟踪。主要展示: - 配置HTTP服务的跟踪 - 跟踪HTTP请求和响应 - 传播跟踪上下文 - 可视化分布式跟踪 ## 环境要求 - `Go` `1.23` 或更高版本 - `GoFrame` 框架 - `GoFrame OpenTelemetry` 跟踪 ## 目录结构 ```text . ├── client/ # 客户端示例 │ └── client.go # 带跟踪的客户端 ├── server/ # 服务端示例 │ └── server.go # 带跟踪的服务端 ├── go.mod # Go模块文件 └── go.sum # Go模块校验和 ``` ## 前置条件 1. 运行 `Jaeger` 实例: ```bash docker run --rm --name jaeger \ -e COLLECTOR_ZIPKIN_HOST_PORT=:9411 \ -p 6831:6831/udp \ -p 6832:6832/udp \ -p 5778:5778 \ -p 16686:16686 \ -p 4317:4317 \ -p 4318:4318 \ -p 14250:14250 \ -p 14268:14268 \ -p 14269:14269 \ -p 9411:9411 \ jaegertracing/all-in-one:1.55 ``` ## 使用说明 1. 启动服务端: ```bash cd server go run server.go ``` 2. 运行客户端: ```bash cd client go run client.go ``` 3. 查看跟踪: 在浏览器中打开 http://localhost:16686 查看 `Jaeger` UI中的跟踪信息。 ## API接口 服务器提供以下HTTP接口: 1. Hello World ```text GET /hello 响应: "Hello World" ``` ## 实现说明 1. 服务端实现 ```go // 初始化跟踪 shutdown, err := otlphttp.Init(serviceName, endpoint, path) // 创建HTTP服务器 s := g.Server() s.Group("/", func(group *ghttp.RouterGroup) { group.GET("/hello", HelloHandler) }) // 处理请求并跟踪 func HelloHandler(r *ghttp.Request) { ctx, span := gtrace.NewSpan(r.Context(), "HelloHandler") defer span.End() value := gtrace.GetBaggageVar(ctx, "name").String() r.Response.Write("hello:", value) } ``` 2. 客户端实现 ```go // 创建新的跟踪span ctx, span := gtrace.NewSpan(gctx.New(), "StartRequests") defer span.End() // 设置跟踪的baggage值 ctx = gtrace.SetBaggageValue(ctx, "name", "GoFrame") // 发送HTTP请求 response, err := g.Client().Get(ctx, "http://127.0.0.1:8000/hello") ``` # HTTP带数据库 GitHub 源码: https://github.com/gogf/examples/tree/main/observability/trace/http-with-db ## 简介 本示例演示了如何在 `GoFrame` 中实现HTTP服务与数据库交互的分布式跟踪。主要展示: - 配置HTTP服务的跟踪 - 跟踪数据库操作 - 传播跟踪上下文 - 可视化分布式跟踪 ## 环境要求 - `Go` `1.23` 或更高版本 - `GoFrame` 框架 - `GoFrame MySQL` 驱动 - `GoFrame Redis` 驱动 - `GoFrame OpenTelemetry` 跟踪 ## 目录结构 ```text . ├── client/ # 客户端示例 │ └── client.go # 带跟踪的客户端 ├── server/ # 服务端示例 │ ├── server.go # 带跟踪的服务端 │ └── config.yaml # 服务端配置 ├── sql.sql # 数据库模式 ├── go.mod # Go模块文件 └── go.sum # Go模块校验和 ``` ## 前置条件 1. 运行 `MySQL` 实例: ```bash docker run -d --name mysql \ -p 3306:3306 \ -e MYSQL_DATABASE=test \ -e MYSQL_ROOT_PASSWORD=12345678 \ mysql:5.7 ``` 2. 初始化数据库模式: ```bash # 连接MySQL容器 docker exec -i mysql mysql -uroot -p12345678 test < sql.sql ``` 3. 运行 `Redis` 实例: ```bash docker run -d --name redis \ -p 6379:6379 \ redis:6.0 ``` 4. 运行 `Jaeger` 实例: ```bash docker run --rm --name jaeger \ -e COLLECTOR_ZIPKIN_HOST_PORT=:9411 \ -p 6831:6831/udp \ -p 6832:6832/udp \ -p 5778:5778 \ -p 16686:16686 \ -p 4317:4317 \ -p 4318:4318 \ -p 14250:14250 \ -p 14268:14268 \ -p 14269:14269 \ -p 9411:9411 \ jaegertracing/all-in-one:1.55 ``` ## 配置说明 服务端配置在`server/config.yaml`中定义: ```yaml database: logger: level: "all" # 记录所有SQL操作 stdout: true # 打印到标准输出 default: link: "mysql:root:12345678@tcp(127.0.0.1:3306)/test" debug: true # 启用调试模式 redis: default: address: 127.0.0.1:6379 # 默认Redis实例 db: 0 cache: address: 127.0.0.1:6379 # 缓存Redis实例 db: 1 ``` ## 使用说明 1. 启动服务端: ```bash cd server go run server.go ``` 2. 运行客户端: ```bash cd client go run client.go ``` 3. 查看跟踪: 在浏览器中打开 http://localhost:16686 查看 `Jaeger` UI中的跟踪信息。 ## API接口 服务器提供以下HTTP接口: 1. 插入用户 ```text POST /insert 请求: {"Name": "string"} 响应: {"ID": number} ``` 2. 查询用户 ```text GET /query 请求: {"ID": number} 响应: {"User": object} ``` 3. 删除用户 ```text DELETE /delete 请求: {"Id": number} 响应: {} ``` ## 实现说明 1. 服务端实现 ```go // 配置Redis作为ORM缓存适配器 g.DB().GetCache().SetAdapter(gcache.NewAdapterRedis(g.Redis())) // 启动HTTP服务器 s := g.Server() s.Use(ghttp.MiddlewareHandlerResponse) s.Group("/", func(group *ghttp.RouterGroup) { group.ALL("/user", new(cTrace)) }) // 查询用户信息 func (c *cTrace) Query(ctx context.Context, req *QueryReq) (res *QueryRes, err error) { one, err := g.Model("user").Ctx(ctx).Cache(gdb.CacheOption{ Duration: 5 * time.Second, Name: c.userCacheKey(req.ID), Force: false, }).WherePri(req.ID).One() if err != nil { return nil, err } res = &QueryRes{ User: one, } return } ``` 2. 客户端实现 ```go // 创建新的跟踪span ctx, span := gtrace.NewSpan(gctx.New(), "StartRequests") defer span.End() // 发送HTTP请求 err = client.PostVar(ctx, "http://127.0.0.1:8000/user/insert", g.Map{ "name": "john", }).Scan(&insertRes) ``` # 进程内服务 (HTTP导出器) GitHub 源码: https://github.com/gogf/examples/tree/main/observability/trace/inprocess ## 简介 本示例演示了如何使用GoFrame和基于HTTP的OpenTelemetry导出器在进程内服务中实现分布式跟踪。主要展示: - 使用HTTP导出器配置单进程跟踪 - 跟踪函数调用和操作 - 传播跟踪上下文 - 可视化分布式跟踪 ## 环境要求 - Go `1.23` 或更高版本 - GoFrame框架 - GoFrame OpenTelemetry HTTP跟踪 ## 目录结构 ``` . ├── main.go # 带HTTP跟踪的主应用程序 ├── go.mod # Go模块文件 └── go.sum # Go模块校验和 ``` ## 功能特性 本示例展示了以下功能: 1. 基于HTTP导出器的分布式跟踪 - 基于HTTP的跟踪数据传输 - Span管理 - 跟踪可视化 2. 函数调用跟踪 - 函数进入/退出跟踪 - 上下文传播 - 错误处理 3. 数据操作 - 用户数据检索 - 数据聚合 - 错误处理 ## 前置条件 1. 运行Jaeger实例: ```bash docker run --rm --name jaeger \ -e COLLECTOR_ZIPKIN_HOST_PORT=:9411 \ -p 6831:6831/udp \ -p 6832:6832/udp \ -p 5778:5778 \ -p 16686:16686 \ -p 4317:4317 \ -p 4318:4318 \ -p 14250:14250 \ -p 14268:14268 \ -p 14269:14269 \ -p 9411:9411 \ jaegertracing/all-in-one:1.55 ``` ## 使用说明 1. 运行应用程序: ```bash go run main.go ``` 2. 查看跟踪: 在浏览器中打开 http://localhost:16686 查看Jaeger UI中的跟踪信息。 ## 实现说明 本示例演示了: 1. 用户数据操作 ```go GetUser(ctx, id) // 检索完整的用户数据 ├── GetInfo(ctx, id) // 获取基本用户信息 ├── GetDetail(ctx, id) // 获取详细用户信息 └── GetScores(ctx, id) // 获取用户成绩 ``` 2. 跟踪上下文流程 ```go // 创建根span ctx, span := gtrace.NewSpan(ctx, "main") defer span.End() // 创建子span并传播上下文 ctx, span := gtrace.NewSpan(ctx, "GetUser") defer span.End() ``` 3. 错误处理 ```go // 处理不存在的用户ID if id == 100 { return g.Map{ "id": 100, "name": "john", "gender": 1, } } return nil ``` 4. 基于HTTP的跟踪导出 ```go // 初始化HTTP跟踪导出器 shutdown, err = otlphttp.Init(serviceName, endpoint, path) if err != nil { g.Log().Fatal(ctx, err) } defer shutdown(ctx) ``` ## 故障排除 1. 应用程序问题: - 检查应用程序是否正常运行 - 验证函数调用输出 - 查看错误日志 2. 跟踪问题: - 验证Jaeger是否运行:`docker ps | grep jaeger` - 检查Jaeger UI可访问性:http://localhost:16686 - 确保配置中的HTTP端点正确 3. HTTP导出问题: - 检查HTTP连接状态 - 验证HTTP端点可访问性 - 查看HTTP错误消息 ## 示例输出 对于用户ID 100: ```go { "id": 100, "name": "john", "gender": 1, "site": "https://goframe.org", "email": "john@goframe.org", "math": 100, "english": 60, "chinese": 50 } ``` 对于不存在的用户ID: ```go {} ``` ## 最佳实践 1. 跟踪配置 - 设置合适的服务名称,便于在Jaeger UI中识别 - 配置适当的采样率,平衡性能和可观测性 - 使用有意义的span名称和标签 2. 错误处理 - 在span中记录错误信息 - 使用合适的状态码标识错误类型 - 保持日志和跟踪的一致性 3. 性能优化 - 避免创建过多的span - 及时关闭span - 合理使用上下文传播 # 进程内服务 (gRPC导出器) GitHub 源码: https://github.com/gogf/examples/tree/main/observability/trace/inprocess-grpc ## 简介 本示例演示了如何使用GoFrame和基于gRPC的OpenTelemetry导出器在进程内服务中实现分布式跟踪。主要展示: - 使用gRPC导出器配置单进程跟踪 - 跟踪函数调用和操作 - 通过gRPC传播跟踪上下文 - 可视化分布式跟踪 ## 环境要求 - Go `1.23` 或更高版本 - GoFrame框架 - GoFrame OpenTelemetry gRPC跟踪 ## 目录结构 ``` . ├── main.go # 带gRPC跟踪的主应用程序 ├── go.mod # Go模块文件 └── go.sum # Go模块校验和 ``` ## 前置条件 1. 运行Jaeger实例: ```bash docker run --rm --name jaeger \ -e COLLECTOR_ZIPKIN_HOST_PORT=:9411 \ -p 6831:6831/udp \ -p 6832:6832/udp \ -p 5778:5778 \ -p 16686:16686 \ -p 4317:4317 \ -p 4318:4318 \ -p 14250:14250 \ -p 14268:14268 \ -p 14269:14269 \ -p 9411:9411 \ jaegertracing/all-in-one:1.55 ``` ## 使用说明 1. 运行应用程序: ```bash go run main.go ``` 2. 查看跟踪: 在浏览器中打开 http://localhost:16686 查看Jaeger UI中的跟踪信息。 ## 实现说明 本示例演示了: 1. 用户数据操作 ```go GetUser(ctx, id) // 检索完整的用户数据 ├── GetInfo(ctx, id) // 获取基本用户信息 ├── GetDetail(ctx, id) // 获取详细用户信息 └── GetScores(ctx, id) // 获取用户成绩 ``` 2. 跟踪上下文流程 ```go // 创建带gRPC上下文的根span ctx, span := gtrace.NewSpan(ctx, "main") defer span.End() // 创建带gRPC元数据的子span ctx, span := gtrace.NewSpan(ctx, "GetUser") defer span.End() ``` 3. 错误处理 ```go // 处理不存在的用户ID if id == 100 { return g.Map{ "id": 100, "name": "john", "gender": 1, } } return nil ``` 4. 基于gRPC的跟踪导出 ```go // 初始化gRPC跟踪导出器 shutdown, err = otlpgrpc.Init(serviceName, endpoint, traceToken) if err != nil { g.Log().Fatal(ctx, err) } defer shutdown(ctx) ``` ## 示例输出 对于用户ID 100: ```go { "id": 100, "name": "john", "gender": 1, "site": "https://goframe.org", "email": "john@goframe.org", "math": 100, "english": 60, "chinese": 50 } ``` 对于不存在的用户ID: ```go {} ``` # 多进程示例 GitHub 源码: https://github.com/gogf/examples/tree/main/observability/trace/processes ## 简介 本目录包含了使用GoFrame不同进程管理方式实现多进程分布式链路跟踪的示例。包括: 1. 命令行进程管理 (`gcmd/`) - 使用`gcmd`包进行进程管理 - 演示基于命令行的进程创建 - 展示父子进程间的链路跟踪上下文传播 2. 程序化进程管理 (`gproc/`) - 使用`gproc`包进行进程管理 - 演示程序化的进程创建 - 展示进程层级中的链路跟踪传播 ## 目录结构 ``` . ├── gcmd/ # 命令行进程管理示例 │ ├── main.go # 主进程实现 │ └── sub/ # 子进程实现 │ └── sub.go # 子进程代码 ├── gproc/ # 进程管理示例 │ ├── main.go # 主进程实现 │ └── sub/ # 子进程实现 │ └── sub.go # 子进程代码 ├── go.mod # Go模块文件 └── go.sum # Go模块校验和 ``` ## 环境要求 - Go `1.23` 或更高版本 - GoFrame框架 - GoFrame链路跟踪支持 ## 功能特性 本示例展示了以下功能: 1. 进程管理 ```go // gcmd方式 Main = &gcmd.Command{ Name: "main", Brief: "main process", Func: func(ctx context.Context, parser *gcmd.Parser) (err error) { return gproc.ShellRun(ctx, `go run sub/sub.go`) }, } // gproc方式 if err := gproc.ShellRun(ctx, `go run sub/sub.go`); err != nil { panic(err) } ``` 2. 链路跟踪上下文传播 ```go // 主进程 ctx := gctx.GetInitCtx() g.Log().Debug(ctx, `this is main process`) // 子进程 ctx := gctx.GetInitCtx() g.Log().Debug(ctx, `this is sub process`) ``` 3. 日志和调试 - 进程标识 - 调试日志 - 错误报告 ## 管理方式对比 ### 命令行管理 (gcmd/) 1. 特点: - 命令行界面 - 结构化命令处理 - 内置帮助和文档 - 命令层级支持 2. 使用场景: - CLI应用程序 - 命令驱动工具 - 交互式应用 ### 程序化管理 (gproc/) 1. 特点: - 程序化进程控制 - 直接进程操作 - Shell命令执行 - 进程同步 2. 使用场景: - 后台进程 - 服务管理 - 进程自动化 ## 使用说明 ### 命令行示例 1. 进入gcmd示例目录: ```bash cd gcmd ``` 2. 运行示例: ```bash go run main.go ``` ### 程序化管理示例 1. 进入gproc示例目录: ```bash cd gproc ``` 2. 运行示例: ```bash go run main.go ``` ## 实现说明 两个示例都演示了: 1. 进程创建 - 主进程初始化 - 子进程启动 - 进程环境设置 2. 上下文管理 ```go // 创建和初始化上下文 ctx := gctx.GetInitCtx() // 在进程间传播上下文 g.Log().Debug(ctx, `process message`) ``` 3. 错误处理 ```go // 进程执行错误处理 if err := gproc.ShellRun(ctx, command); err != nil { panic(err) } // 命令执行错误处理 if err := cmd.Run(ctx); err != nil { return err } ``` # OpenTelemetry示例 GitHub 源码: https://github.com/gogf/examples/tree/main/observability/trace/otlp ## 简介 本目录包含了演示在GoFrame应用程序中使用不同方法导出OpenTelemetry链路跟踪数据的示例。包括: 1. 基于gRPC的导出 (`grpc/`) - 使用gRPC协议传输链路跟踪数据 - 适用于高性能、流式链路跟踪数据导出 - 支持双向流和连接复用 2. 基于HTTP的导出 (`http/`) - 使用HTTP协议传输链路跟踪数据 - 适用于有HTTP代理或防火墙限制的环境 - 配置和调试更简单 ## 目录结构 ``` . ├── grpc/ # 基于gRPC的链路跟踪示例 │ └── main.go # gRPC链路跟踪导出器实现 ├── http/ # 基于HTTP的链路跟踪示例 │ └── main.go # HTTP链路跟踪导出器实现 ├── go.mod # Go模块文件 └── go.sum # Go模块校验和 ``` ## 环境要求 - Go `1.23` 或更高版本 - GoFrame框架 - GoFrame OpenTelemetry gRPC链路跟踪 - GoFrame OpenTelemetry HTTP链路跟踪 ## 前置条件 1. 运行Jaeger实例: ```bash docker run --rm --name jaeger \ -e COLLECTOR_ZIPKIN_HOST_PORT=:9411 \ -p 6831:6831/udp \ -p 6832:6832/udp \ -p 5778:5778 \ -p 16686:16686 \ -p 4317:4317 \ -p 4318:4318 \ -p 14250:14250 \ -p 14268:14268 \ -p 14269:14269 \ -p 9411:9411 \ jaegertracing/all-in-one:1.55 ``` ## 导出方法对比 ### gRPC导出 (grpc/) 1. 优势: - 更高性能 - 双向流传输 - 连接复用 - 更适合大量链路跟踪数据 2. 配置: - 需要gRPC端点 - 支持认证令牌 - 可配置连接设置 3. 使用场景: - 大量链路跟踪数据 - 微服务架构 - 性能关键系统 ### HTTP导出 (http/) 1. 优势: - 设置更简单 - 可通过HTTP代理 - 更易调试 - 更好的防火墙兼容性 2. 配置: - 需要HTTP端点 - 支持路径配置 - 标准HTTP设置 3. 使用场景: - 有HTTP代理的环境 - 简单部署要求 - 开发和测试 ## 使用说明 ### gRPC导出示例 1. 进入gRPC示例目录: ```bash cd grpc ``` 2. 运行示例: ```bash go run main.go ``` ### HTTP导出示例 1. 进入HTTP示例目录: ```bash cd http ``` 2. 运行示例: ```bash go run main.go ``` 3. 查看链路跟踪: 在浏览器中打开 http://localhost:16686 查看Jaeger UI中的链路跟踪信息。 ## 实现说明 两个示例都演示了: 1. 链路跟踪上下文管理 ```go // 创建新的链路跟踪span ctx, span := gtrace.NewSpan(gctx.New(), "StartRequests") defer span.End() // 设置链路跟踪的baggage值 ctx = gtrace.SetBaggageValue(ctx, "name", "john") ``` 2. 错误处理 ```go // 初始化导出器 shutdown, err = otlpgrpc.Init(serviceName, endpoint, traceToken) if err != nil { g.Log().Fatal(ctx, err) } defer shutdown(ctx) ``` 3. 配置管理 ```go // gRPC配置 const ( serviceName = "otlp-grpc-client" endpoint = "tracing-analysis-dc-bj.aliyuncs.com:8090" traceToken = "******_******" ) // HTTP配置 const ( serviceName = "otlp-http-client" endpoint = "tracing-analysis-dc-hz.aliyuncs.com" path = "adapt_******_******/api/otlp/traces" ) ``` # OpenTelemetry Provider GitHub 源码: https://github.com/gogf/examples/tree/main/observability/trace/provider ## 简介 本目录包含了在GoFrame应用程序中使用不同OpenTelemetry链路跟踪Provider配置的示例。包括: 1. gRPC Provider (`grpc/`) - 使用gRPC协议传输链路跟踪数据 - 配置带gRPC导出器的链路跟踪Provider - 演示资源和采样器配置 2. HTTP Provider (`http/`) - 使用HTTP协议传输链路跟踪数据 - 配置带HTTP导出器的链路跟踪Provider - 展示资源和采样器配置 3. 内部组件 (`internal/`) - 通用Provider初始化代码 - 共享常量和配置 - 链路跟踪设置工具函数 ## 目录结构 ``` . ├── grpc/ # gRPC provider示例 │ └── main.go # gRPC provider实现 ├── http/ # HTTP provider示例 │ └── main.go # HTTP provider实现 ├── internal/ # 共享组件 │ ├── consts.go # 常量定义 │ ├── provider.go # Provider初始化 │ └── request.go # 请求工具 ├── go.mod # Go模块文件 └── go.sum # Go模块校验和 ``` ## 环境要求 - Go `1.23` 或更高版本 - GoFrame框架 - OpenTelemetry支持 ## 功能特性 本示例展示了以下功能: 1. Provider配置 ```go // 资源配置 res, err = resource.New(ctx, resource.WithFromEnv(), resource.WithProcess(), resource.WithTelemetrySDK(), resource.WithHost(), resource.WithAttributes( semconv.ServiceNameKey.String(internal.GRPCServiceName), semconv.HostNameKey.String(serverIP), ), ) // 采样器配置 trace.WithSampler(trace.TraceIDRatioBased(0.1)) ``` 2. 资源属性 - 服务名称 - 主机信息 - 进程详情 - 自定义属性 3. 采样策略 ```go // 可用的采样策略 trace.WithSampler(trace.AlwaysSample()) // 总是采样 trace.WithSampler(trace.NeverSample()) // 从不采样 trace.WithSampler(trace.ParentBased(...)) // 基于父级的采样 trace.WithSampler(trace.TraceIDRatioBased()) // 基于跟踪ID的采样 ``` 4. Span处理 ```go // 简单Span处理器 trace.WithSpanProcessor(trace.NewSimpleSpanProcessor(exporter)) // 批量Span处理器 trace.WithSpanProcessor(trace.NewBatchSpanProcessor(exporter)) ``` ## Provider对比 ### gRPC Provider (grpc/) 1. 特点: - 高性能流式传输 - 双向通信 - 连接复用 - 压缩支持 2. 配置: ```go otlptracegrpc.NewClient( otlptracegrpc.WithInsecure(), otlptracegrpc.WithEndpoint(internal.Endpoint), otlptracegrpc.WithHeaders(map[string]string{ "Authentication": internal.TraceToken, }), otlptracegrpc.WithCompressor(gzip.Name), ) ``` 3. 使用场景: - 大规模链路跟踪 - 性能关键系统 - 流式跟踪数据 ### HTTP Provider (http/) 1. 特点: - 标准HTTP协议 - 配置简单 - 防火墙友好 - 压缩支持 2. 配置: ```go otlptracehttp.NewClient( otlptracehttp.WithEndpoint(internal.HTTPEndpoint), otlptracehttp.WithURLPath(internal.HTTPPath), otlptracehttp.WithInsecure(), otlptracehttp.WithCompression(1), ) ``` 3. 使用场景: - 基本链路跟踪需求 - HTTP代理环境 - 简单部署 ## 使用说明 ### gRPC Provider示例 1. 进入gRPC示例目录: ```bash cd grpc ``` 2. 运行示例: ```bash go run main.go ``` ### HTTP Provider示例 1. 进入HTTP示例目录: ```bash cd http ``` 2. 运行示例: ```bash go run main.go ``` ## 实现说明 两个示例都演示了: 1. Provider设置 ```go // 初始化链路跟踪Provider func InitTracer(opts ...trace.TracerProviderOption) (func(ctx context.Context), error) { tracerProvider := trace.NewTracerProvider(opts...) otel.SetTextMapPropagator(propagation.NewCompositeTextMapPropagator( propagation.TraceContext{}, propagation.Baggage{}, )) otel.SetTracerProvider(tracerProvider) return func(ctx context.Context) { // 优雅关闭 ctx, cancel := context.WithTimeout(ctx, time.Second) defer cancel() if err := tracerProvider.Shutdown(ctx); err != nil { g.Log().Errorf(ctx, "关闭tracerProvider失败 err:%+v", err) } }, nil } ``` 2. 资源配置 - 服务标识 - 主机信息 - 进程属性 - 自定义标签 3. 采样配置 - 采样策略选择 - 采样率配置 - 父级上下文处理 4. 错误处理 - 连接错误 - 导出错误 - 关闭处理 # 依赖注入 GitHub 源码: https://github.com/gogf/examples/tree/main/practices/injection ## 简介 本示例展示了如何在使用`GoFrame`的应用程序中实现依赖注入。 主要演示: 1. 使用大仓模式管理代码仓库 2. 基础依赖注入设置 3. 使用`DI`的服务层实现 4. 集成`gRPC`的控制器层 5. 使用模拟依赖的单元测试 实现重点在于使核心业务逻辑易于测试: - 使用依赖注入分离关注点 - 为服务依赖提供清晰的接口 - 每一层都可以独立执行单元测试,保障代码质量 ## 环境要求 - [Go](https://golang.org/dl/) `1.23` 或更高版本 - [Git](https://git-scm.com/downloads) - [GoFrame](https://goframe.org) - [MongoDB](https://www.mongodb.com) - [Redis](https://redis.io) - [github.com/samber/do](https://github.com/samber/do) ## 目录结构 ```text injection/ ├── app/ │ ├── gateway/ # API网关服务,调用user grpc服务实现HTTP接口 │ │ ├── api/ # API定义 │ │ ├── internal/ # 内部实现 │ │ │ ├── cmd/ # 命令行工具 │ │ │ ├── controller/ # 控制器 │ │ │ ├── model/ # 数据模型 │ │ │ └── service/ # 业务逻辑 │ │ └── manifest/ # 配置文件 │ └── user/ # 用户微服务 │ ├── api/ # API定义 │ │ ├── entity/ # 实体定义 │ │ └── user/ # 用户API pb │ ├── internal/ # 内部实现 │ │ ├── cmd/ # 命令行工具 │ │ ├── controller/ # 控制器 │ │ ├── dao/ # 数据访问对象 │ │ ├── model/ # 数据模型 │ │ └── service/ # 业务逻辑 │ └── manifest/ # 配置文件 ├── hack/ # 开发工具 └── utility/ # 通用工具 ├── injection/ # DI工具 └── mongohelper/ # MongoDB辅助工具 ``` ## 功能特性 - 依赖注入使用 - `MongoDB`和`Redis`集成 - `gRPC`服务实现 - 完整的单元测试 - 清理资源的关闭处理 - 命名依赖支持 ## 安装设置 1. 克隆仓库: ```bash git clone https://github.com/gogf/examples.git cd examples/practices/injection ``` 2. 安装依赖: ```bash go mod tidy ``` 3. 使用`Docker`启动所需服务: ```bash # 启动MongoDB docker run -d --name mongo -p 27017:27017 mongo:latest # 启动Redis docker run -d --name redis -p 6379:6379 redis:latest ``` ## 使用方法 1. 运行`gRPC Server`服务: ```bash cd examples/practices/injection/app/user go run main.go server ``` 2. 运行`HTTP Server`服务: ```bash cd examples/practices/injection/app/gateway go run main.go server ``` 3. (可选)运行异步守护进程,仅演示多命令功能,无实际逻辑: ```bash cd examples/practices/injection/app/gateway go run main.go worker ``` 4. (可选)运行测试: ```bash go test ./... ``` ## 实现细节 ### 依赖注入设置 - 使用`github.com/samber/do`包进行依赖管理 - 支持命名和未命名依赖管理 - 提供常用操作的辅助函数 ### 服务层 - 清晰的关注点分离 - 基于接口的设计 - 易于使用模拟实现进行测试 ### 控制器层 - 支持`DI`的`gRPC`集成 - 清晰的错误处理 - 合理的资源管理 ### 可测试性 - 数据层(`dao`)、业务层(`service`)、接口层(`controller`)完整的单元测试 - 依赖模拟,单元测试使用的数据库配置通过`manifest/config`管理 - 通过配置文件管理微服务链接的服务地址或者域名,以便于控制依赖的服务模拟 ## 注意事项 - 依赖注入的注册时,需要在`Shutdown`方法中正确清理资源 - 使用依赖注入时,如果需要同类型的多个实例时使用命名依赖 # MVC开发实例 GitHub 源码: https://github.com/gogf/examples/tree/main/practices/mvc-demo-chat ## 简介 `mvc-demo-chat`是一个展示`GoFrame MVC(Model-View-Controller)`模式实现的实时聊天应用,采用`WebSocket`技术。它提供了完整的聊天室体验,包括昵称唯一性验证、实时消息广播、用户列表管理和消息发送限流等功能。该示例展示了如何使用`GoFrame`的模板引擎、会话管理和`WebSocket`支持构建交互式全栈`Web`应用。 ## 项目结构 ```text mvc-demo-chat/ ├── api/ # API接口定义 │ └── v1/ │ └── chat.go # 聊天API定义 ├── internal/ # 内部实现 │ ├── cmd/ │ │ └── cmd.go # 服务器初始化和路由 │ ├── consts/ │ │ ├── consts.go # 通用常量 │ │ └── consts_chat.go # 聊天专用常量 │ ├── controller/ │ │ └── chat.go # 聊天控制器及WebSocket逻辑 │ └── model/ │ └── chat.go # 聊天消息模型 ├── manifest/ # 交付清单 │ ├── config/ │ │ └── config.yaml # 服务器和日志配置 │ ├── deploy/ # 部署配置 │ └── docker/ # Docker配置 ├── resource/ # 静态资源和模板 │ ├── public/ # 静态文件(CSS、JavaScript、图片) │ │ ├── plugin/ # 前端插件 │ │ └── resource/ # 静态资源 │ └── template/ # HTML模板 │ └── chat/ │ ├── include/ # 局部模板 │ └── index.html # 主页模板 ├── main.go # 应用程序入口 └── go.mod # Go模块定义 ``` ## 特性 - **实时WebSocket通信**:双向通信实现即时消息传递 - **MVC架构**:使用`Model-View-Controller`模式清晰分离关注点 - **会话管理**:用户会话处理实现昵称持久化 - **模板渲染**:服务端HTML模板渲染与动态内容 - **昵称唯一性验证**:确保聊天室内无重复昵称 - **用户列表广播**:用户加入/离开时实时更新用户列表 - **消息限流**:1秒速率限制防止消息刷屏 - **并发安全操作**:线程安全的用户和昵称管理 - **HTML消毒**:通过`HTML`实体编码防止`XSS`攻击 - **错误处理**:为验证和速率限制提供友好的错误消息 - **OpenAPI文档**:自动生成API文档和`Swagger UI` - **静态文件服务**:从`resource/public`提供前端资源 ## 要求 - `Go 1.23.0`或更高版本 ## 安装 ```bash cd /path/to/examples/practices/mvc-demo-chat go mod tidy ``` ## 使用 **启动服务:** ```bash go run main.go ``` 服务将在`http://localhost:8199`启动。 **访问应用:** - 聊天应用: http://localhost:8199/chat - `Swagger UI`: http://localhost:8199/swagger - `OpenAPI`规范: http://localhost:8199/api.json ## 工作原理 ### 用户流程 1. **输入昵称**:用户首先输入唯一昵称(`3-21`个字符) 2. **加入聊天**:验证通过后,用户进入聊天室 3. **发送消息**:用户可以发送消息,有1秒限流 4. **实时更新**:所有用户即时看到新消息和用户列表变化 5. **离开聊天**:关闭浏览器标签页会从聊天室移除用户 ### 消息类型 应用处理三种类型的消息: - **`send`**:用户发送的常规聊天消息 - **`list`**:有人加入或离开时的用户列表更新 - **`error`**:验证失败或速率限制的错误消息 ### 会话管理 用户昵称存储在会话存储中: - `ChatName`:验证后的确认昵称 - `ChatNameTemp`:验证期间的临时昵称 - `ChatNameError`:昵称已被占用时的错误消息 ## API参考 ### GET /chat 显示聊天主页。如果用户未设置昵称则显示昵称输入表单,否则显示聊天界面。 ```bash curl http://localhost:8199/chat ``` ### POST /chat/name 设置用户昵称。验证唯一性和长度(最多21个字符)。 **请求:** ```bash curl -X POST http://localhost:8199/chat/name \ -H "Content-Type: application/json" \ -d '{ "name": "john" }' ``` **验证规则:** - 必填字段 - 最多`21`个字符 - 必须唯一(未被其他用户占用) **重复时的响应:** ```json { "code": 51, "message": "Nickname \"john\" is already token by others", "data": null } ``` ### GET /chat/websocket 建立`WebSocket`连接用于实时聊天通信。 **WebSocket URL:** ``` ws://localhost:8199/chat/websocket ``` **消息格式:** ```json { "type": "send", "data": "大家好!", "name": "john" } ``` **服务器响应(广播给所有用户):** ```json { "type": "send", "data": "大家好!", "name": "john" } ``` **用户列表更新:** ```json { "type": "list", "data": ["alice", "bob", "john"], "name": "" } ``` **错误响应(速率限制):** ```json { "type": "error", "data": "Message sending too frequently, why not a rest first", "name": "" } ``` ## 实现细节 ### 控制器架构 `hChat`控制器管理所有聊天操作: ```go type hChat struct { Users *gmap.Map // 聊天中的所有用户(websocket -> 昵称映射) Names *gset.StrSet // 所有昵称用于唯一性检查 } ``` **关键特性:** - **并发安全集合**:使用`gmap.Map`和`gset.StrSet`实现线程安全操作 - **WebSocket管理**:维护活跃的`WebSocket`连接 - **广播机制**:向所有连接的客户端发送消息 ### WebSocket生命周期 1. **连接**:用户通过`/chat/websocket`连接 2. **注册**:昵称添加到用户列表,广播给所有客户端 3. **消息循环**:持续从客户端读取消息 4. **断开连接**:从列表中移除用户,通知所有客户端 5. **清理**:连接关闭时清除会话数据 ### 速率限制 消息限流使用`GoFrame`的缓存和`TTL`: ```go cacheKey := fmt.Sprintf("ChatWebSocket:%p", ws) gcache.SetIfNotExist(ctx, cacheKey, struct{}{}, consts.ChatIntervalLimit) ``` - **限制**:消息间隔1秒 - **方法**:基于指针键的缓存 - **反馈**:违反限制时向用户发送错误消息 ### HTML消毒 用户输入经过消毒以防止`XSS`攻击: - **昵称**:`ghtml.Entities()`转换特殊字符 - **消息**:`ghtml.SpecialChars()`转义HTML实体 - **保护**:防止恶意脚本注入 ### 模板渲染 应用使用`GoFrame`的模板引擎: - **主模板**:`resource/template/chat/index.html` - **包含文件**:基于会话状态的条件渲染 - `resource/template/chat/include/main.html`:昵称输入表单 - `resource/template/chat/include/chat.html`:聊天界面 ## 配置 配置通过`manifest/config/config.yaml`管理: ```yaml server: address: ":8199" serverRoot: "resource/public" openapiPath: "/api.json" swaggerPath: "/swagger" dumpRouterMap: true routeOverWrite: true accessLogEnabled: true logger: level: "all" stdout: true ``` **关键设置:** - **serverRoot**:从`resource/public`提供静态文件 - **accessLogEnabled**:记录所有`HTTP`请求 - **dumpRouterMap**:启动时显示路由映射 ## 前端集成 应用使用从`resource/public/`提供的静态资源: - **CSS**:聊天界面样式 - **JavaScript**:`WebSocket`客户端逻辑和`DOM`操作 - **Images**:图标和头像 - **Plugins**:第三方前端库 # 快速开发示例 GitHub 源码: https://github.com/gogf/examples/tree/main/practices/quick-demo ## 简介 `quick-demo`是一个快速入门`HTTP`服务示例,展示如何使用`GoFrame`框架进行快速开发。它提供了完整的`RESTful API`实现,包含用户`CRUD`操作、`MySQL`数据库集成以及`OpenAPI/Swagger`文档自动生成。该示例遵循`GoFrame`的标准项目结构和最佳实践,是学习`GoFrame`或构建新项目的理想起点。 ## 项目结构 ```text quick-demo/ ├── api/ # API接口定义 │ ├── hello/ # Hello模块API │ │ └── v1/ │ │ └── hello.go # Hello API定义 │ └── user/ # 用户模块API │ └── v1/ │ └── user.go # 用户CRUD API定义 ├── internal/ # 内部实现 │ ├── cmd/ # 命令层 │ │ └── cmd.go # 服务器初始化 │ ├── consts/ # 常量 │ ├── controller/ # 控制器层(自动生成 + 接口实现) │ │ ├── hello/ # Hello控制器 │ │ └── user/ # 用户CRUD控制器 │ ├── dao/ # 数据访问对象(自动生成) │ ├── model/ # 数据模型 │ │ ├── do/ # 数据对象(自动生成) │ │ └── entity/ # 数据库实体(自动生成) │ └── service/ # 业务逻辑层 ├── manifest/ # 应用清单 │ ├── config/ # 配置文件 │ │ └── config.yaml # 主配置 │ ├── deploy/ # 部署配置 │ ├── docker/ # Docker配置 │ └── i18n/ # 国际化 ├── resource/ # 静态资源 ├── utility/ # 工具函数 ├── main.go # 应用程序入口 └── go.mod # Go模块定义 ``` ## 特性 - **RESTful API设计**:遵循`RESTful`规范的完整用户`CRUD`操作 - **MySQL集成**:使用`GoFrame`的`DAO`模式和`ORM`进行数据库操作 - **OpenAPI/Swagger**:自动生成`API`文档和`Swagger UI` - **标准结构**:遵循`GoFrame`推荐的项目结构,具有良好的可扩展性 - **代码自动生成**:从数据库表结构自动生成`DAO`、`entity`和`model`代码 - **请求验证**:内置请求参数验证规则 - **配置管理**:基于`YAML`的配置,支持环境变量 - **中间件**:统一的响应处理中间件,确保`API`响应格式一致 - **Hello World示例**:简单示例展示基本的`HTTP`处理 ## 要求 - `Go 1.23.0`或更高版本 - `MySQL 8.0`或更高版本 ## 前置条件 **使用Docker启动MySQL:** ```bash docker run -d \ --name mysql \ -p 3306:3306 \ -e MYSQL_ROOT_PASSWORD=12345678 \ -e MYSQL_DATABASE=test \ mysql:8.0 ``` **创建用户表:** ```sql CREATE TABLE `user` ( `id` int unsigned NOT NULL AUTO_INCREMENT COMMENT 'user id', `name` varchar(50) NOT NULL COMMENT 'user name', `status` int NOT NULL DEFAULT '0' COMMENT 'user status', `age` int unsigned NOT NULL COMMENT 'user age', PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; ``` ## 安装 ```bash cd /path/to/examples/practices/quick-demo go mod tidy ``` ## 使用 **启动服务:** ```bash go run main.go ``` 服务将在`http://localhost:8000`启动。 **访问API文档:** - `Swagger UI`: http://localhost:8000/swagger - `OpenAPI`规范: http://localhost:8000/api.json ## API参考 ### Hello API **GET /hello** 返回"Hello World!"消息。 ```bash curl http://localhost:8000/hello ``` ### 用户APIs **创建用户** `POST /user` ```bash curl -X POST http://localhost:8000/user \ -H "Content-Type: application/json" \ -d '{ "name": "john", "age": 25 }' ``` 响应: ```json { "code": 0, "message": "", "data": { "id": 1 } } ``` **更新用户** `PUT /user/{id}` ```bash curl -X PUT http://localhost:8000/user/1 \ -H "Content-Type: application/json" \ -d '{ "name": "john_updated", "age": 26, "status": 0 }' ``` **删除用户** `DELETE /user/{id}` ```bash curl -X DELETE http://localhost:8000/user/1 ``` **获取单个用户** `GET /user/{id}` ```bash curl http://localhost:8000/user/1 ``` 响应: ```json { "code": 0, "message": "", "data": { "id": 1, "name": "john", "status": 0, "age": 25 } } ``` **获取用户列表** `GET /user` ```bash # 获取所有用户 curl http://localhost:8000/user # 按年龄过滤 curl "http://localhost:8000/user?age=25" # 按状态过滤 curl "http://localhost:8000/user?status=0" ``` 响应: ```json { "code": 0, "message": "", "data": { "list": [ { "id": 1, "name": "john", "status": 0, "age": 25 } ] } } ``` ## 实现细节 ### API定义 `API`接口使用`GoFrame`的`g.Meta`标签定义,实现自动路由和文档生成: ```go type CreateReq struct { g.Meta `path:"/user" method:"post" tags:"User" summary:"Create user"` Name string `v:"required|length:3,10" dc:"user name"` Age uint `v:"required|between:18,200" dc:"user age"` } ``` ### 控制器实现 控制器实现每个`API`端点的业务逻辑: ```go func (c *ControllerV1) Create(ctx context.Context, req *v1.CreateReq) (res *v1.CreateRes, err error) { insertId, err := dao.User.Ctx(ctx).Data(do.User{ Name: req.Name, Status: v1.StatusOK, Age: req.Age, }).InsertAndGetId() if err != nil { return nil, err } res = &v1.CreateRes{Id: insertId} return } ``` ### DAO模式 项目使用`GoFrame`的`DAO`模式进行数据库操作,代码自动生成: - `dao/`:数据访问层(自动生成) - `model/do/`:数据库操作的数据对象(自动生成) - `model/entity/`:数据库表结构(自动生成) ### 响应中间件 统一响应处理中间件确保`API`响应格式一致,包含`code`、`message`和`data`字段。 ## 配置 配置通过`manifest/config/config.yaml`管理: ```yaml server: address: ":8000" openapiPath: "/api.json" swaggerPath: "/swagger" database: default: link: "mysql:root:12345678@tcp(127.0.0.1:3306)/test" ``` # gRPC项目示例 GitHub 源码: https://github.com/gogf/examples/tree/main/practices/user-grpc-service ## 介绍 本示例展示了一个完整的基于`gRPC`和`GoFrame`框架构建的用户微服务。该示例包含: - 用户的完整`CRUD`操作(创建、查询、列表、删除) - 基于`Protocol Buffers`的`gRPC`服务实现 - `MySQL`数据库集成与`DAO`模式数据访问 - `Service`层业务逻辑封装 - 从数据库`Schema`自动生成代码 - 生产级项目结构组织 ## 目录结构 ```text . ├── api/ # API 定义 │ ├── pbentity/ # 自动生成的protobuf实体(自动生成) │ └── user/ # 用户服务 API │ └── v1/ # API版本v1(自动生成) ├── hack/ # 开发工具 │ └── config.yaml # CLI 工具配置 ├── internal/ # 内部包 │ ├── cmd/ # 命令定义 │ ├── consts/ # 常量 │ ├── controller/ # gRPC 控制器 │ │ └── user/ # 用户控制器 │ ├── dao/ # 数据访问对象(自动生成) │ ├── model/ # 数据模型 │ │ ├── do/ # 领域对象(自动生成) │ │ └── entity/ # 数据库实体(自动生成) │ └── service/ # 业务逻辑 │ └── user/ # 用户服务 ├── manifest/ # 部署清单 │ ├── config/ # 配置文件 │ │ └── config.yaml # 应用配置 │ ├── deploy/ # 部署文件 │ ├── docker/ # Docker 文件 │ ├── protobuf/ # Protocol Buffer 定义 │ │ ├── pbentity/ # 实体定义(自动生成) │ │ └── user/ # 用户服务定义 │ └── sql/ # SQL 脚本 │ └── create.sql # 数据库表结构 ├── main.go # 应用入口 ├── go.mod # Go 模块文件 └── Makefile # 构建自动化 ``` ## 功能特性 本示例展示了以下功能特性: ### 服务实现 - 基于`Protocol Buffers`的`gRPC`服务器 - 用户`CRUD`操作(`Create`、`GetOne`、`GetList`、`Delete`) - 请求参数验证 - 错误处理 - 上下文管理 ### 数据库集成 - `MySQL`数据库连接 - `DAO`模式数据访问 - 自动生成`DAO`、`DO`和`Entity`代码 - 事务支持 - 查询构建器 ### 项目组织 - 标准`GoFrame`项目结构 - 关注点分离(`Controller`、`Service`、`DAO`) - 配置管理 - 日志支持 - `Makefile`构建自动化 ## 环境要求 - [Go](https://golang.org/dl/) `1.23`或更高版本 - [MySQL](https://www.mysql.com/) `5.7`或更高版本 - [Protocol Buffers 编译器](https://grpc.io/docs/protobuf/) ## 前置准备 ### 安装GoFrame CLI工具 ```bash go install github.com/gogf/gf/cmd/gf/v2@latest ``` 或者使用 `Makefile`: ```bash make cli ``` ### 配置 MySQL 数据库 使用`Docker`运行`MySQL`数据库: ```bash docker run -d \ --name mysql-user-service \ -p 3306:3306 \ -e MYSQL_ROOT_PASSWORD=12345678 \ -e MYSQL_DATABASE=test \ mysql:8.0 ``` ### 初始化数据库 执行`SQL`脚本创建用户表: ```bash # 连接到 MySQL docker exec -i mysql-user-service mysql -uroot -p12345678 test < manifest/sql/create.sql ``` 或者手动执行以下`SQL`: ```sql CREATE TABLE `user` ( `id` int unsigned NOT NULL AUTO_INCREMENT COMMENT 'User ID', `passport` varchar(45) NOT NULL COMMENT 'User Passport', `password` varchar(45) NOT NULL COMMENT 'User Password', `nickname` varchar(45) NOT NULL COMMENT 'User Nickname', `create_at` datetime DEFAULT NULL COMMENT 'Created Time', `update_at` datetime DEFAULT NULL COMMENT 'Updated Time', `delete_at` datetime DEFAULT NULL COMMENT 'Deleted Time', PRIMARY KEY (`id`), UNIQUE KEY `uniq_passport` (`passport`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; ``` ## 配置说明 在`manifest/config/config.yaml`中更新数据库配置: ```yaml database: default: link: "mysql:root:12345678@tcp(127.0.0.1:3306)/test" debug: true ``` ## 使用说明 ### 生成代码 从数据库生成`DAO`、`DO`和`Entity`代码: ```bash make dao ``` 生成`Protocol Buffer Go`文件: ```bash make pb ``` 从数据库生成`Protocol Buffer`实体文件: ```bash make pbentity ``` ### 运行服务器 启动`gRPC`服务器: ```bash go run main.go ``` 服务器将在端口`8000`上启动。 ### 测试服务 可以使用`gRPC`客户端测试服务。以下是使用`grpcurl`的示例: 1. 安装 `grpcurl`: ```bash brew install grpcurl ``` 2. 列出可用的服务: ```bash grpcurl -plaintext localhost:8000 list ``` 3. 创建用户: ```bash grpcurl -plaintext -d '{"Passport":"user001","Password":"123456","Nickname":"测试用户"}' \ localhost:8000 user.User/Create ``` 4. 根据 ID 查询用户: ```bash grpcurl -plaintext -d '{"Id":1}' \ localhost:8000 user.User/GetOne ``` 5. 获取用户列表: ```bash grpcurl -plaintext -d '{"Page":1,"Size":10}' \ localhost:8000 user.User/GetList ``` 6. 删除用户: ```bash grpcurl -plaintext -d '{"Id":1}' \ localhost:8000 user.User/Delete ``` ## 接口文档 ### 用户服务 #### Create 创建新用户。 **请求:** ```protobuf message CreateReq { string Passport = 1; // 必填 string Password = 2; // 必填 string Nickname = 3; // 必填 } ``` **响应:** ```protobuf message CreateRes {} ``` #### GetOne 根据`ID`获取用户详情。 **请求:** ```protobuf message GetOneReq { uint64 Id = 1; // 必填 } ``` **响应:** ```protobuf message GetOneRes { pbentity.User User = 1; } ``` #### GetList 获取分页用户列表。 **请求:** ```protobuf message GetListReq { int32 Page = 1; int32 Size = 2; } ``` **响应:** ```protobuf message GetListRes { repeated pbentity.User Users = 1; } ``` #### Delete 根据`ID`删除用户。 **请求:** ```protobuf message DeleteReq { uint64 Id = 1; // 必填,最小值:1 } ``` **响应:** ```protobuf message DeleteRes {} ``` ## 实现细节 ### Controller 层 `internal/controller/user/user.go`实现了`gRPC`服务接口: - 处理传入的`gRPC`请求 - 验证请求参数 - 调用`Service`层处理业务逻辑 - 返回格式化的响应 ### Service 层 `internal/service/user/user.go`包含业务逻辑: - 根据`ID`查询用户 - 删除用户 - 可扩展更复杂的业务规则 ### DAO 层 `internal/dao/user.go`提供数据库访问: - 从数据库`Schema`自动生成 - 提供类型安全的数据库操作 - 支持链式查询构建器 ### 数据模型 - **DO (Domain Object):** 用于数据库操作 - **Entity:** 表示数据库表结构 - **PBEntity:** 用于`API`响应的`Protocol Buffer`实体 ## 开发指南 ### 生成所有代码 ```bash # 生成 DAO 文件 make dao # 生成 Service 文件 make service # 生成 Protocol Buffer 文件 make pb # 生成 Protocol Buffer Entity 文件 make pbentity ``` ### 构建 Docker 镜像 ```bash make image ``` ### 部署到 Kubernetes ```bash make deploy ``` ## 注意事项 - 启动应用前请确保`MySQL`已运行 - 默认数据库凭据为`root:12345678`(生产环境请修改) - 服务默认使用端口`8000` - `Protocol Buffer`文件位于`manifest/protobuf` - `SQL Schema`位于`manifest/sql/create.sql` # HTTP项目示例 GitHub 源码: https://github.com/gogf/examples/tree/main/practices/user-http-service ## 介绍 本示例展示了一个完整的基于`HTTP RESTful API`和`GoFrame`框架的用户管理服务。该示例包含: - 用户注册、登录和登出功能 - 基于会话的认证和授权机制 - 用于上下文注入和认证的`HTTP`中间件 - 自动生成的`OpenAPI`文档和`Swagger` UI - `MySQL`数据库集成与`DAO`模式数据访问 - 请求参数验证和错误处理 - 生产级项目结构组织 ## 目录结构 ```text . ├── api/ # API 定义 │ └── user/ # 用户服务 API │ ├── user.go # 接口定义 │ └── v1/ # API 版本 v1 ├── hack/ # 开发工具 │ └── config.yaml # CLI 工具配置 ├── internal/ # 内部包 │ ├── cmd/ # 命令定义 │ │ └── cmd.go # 主命令与服务器设置 │ ├── consts/ # 常量 │ ├── controller/ # HTTP 控制器 │ │ └── user/ # 用户控制器(自动生成+补充实现) │ ├── dao/ # 数据访问对象(自动生成) │ ├── model/ # 数据模型 │ │ ├── do/ # 领域对象(自动生成) │ │ └── entity/ # 数据库实体(自动生成) │ └── service/ # 业务逻辑 │ ├── bizctx/ # 业务上下文服务 │ ├── middleware/ # 中间件服务 │ ├── session/ # 会话服务 │ └── user/ # 用户服务 ├── manifest/ # 部署清单 │ ├── config/ # 配置文件 │ │ └── config.yaml # 应用配置 │ ├── deploy/ # 部署文件 │ ├── docker/ # Docker 文件 │ └── sql/ # SQL 脚本 │ └── create.sql # 数据库表结构 ├── main.go # 应用入口 ├── go.mod # Go 模块文件 └── Makefile # 构建自动化 ``` ## 功能特性 本示例展示了以下功能特性: ### 用户管理 - 用户注册(`SignUp`)并检查重复 - 用户认证(`SignIn`)并验证密码 - 用户登出(`SignOut`)并清理会话 - 已认证用户的用户资料查询(`Profile`) - 用户名/昵称可用性检查 ### 认证与授权 - 基于会话的用户认证 - 用于请求处理的自定义业务上下文 - 基于中间件的授权机制 - 需要认证的受保护路由 - 跨域请求的`CORS` 支持 ### 接口文档 - 自动生成的`OpenAPI`规范 - 交互式`Swagger UI`位于`/swagger` - `API`文档位于`/api.json` - 请求/响应结构定义 - 接口描述和标签 ### 数据库集成 - `MySQL`数据库连接 - `DAO`模式数据访问 - 自动生成`DAO`、`DO`和`Entity`代码 - 事务支持 - 带验证的查询构建器 ### 请求处理 - 自动的请求/响应`JSON`处理 - 使用`v`标签的内置验证 - 标准化响应格式 - 错误处理和状态码 - `HTTP`方法的路由映射 ## 环境要求 - [Go](https://golang.org/dl/) `1.23`或更高版本 - [MySQL](https://www.mysql.com/) `5.7`或更高版本 ## 前置准备 ### 安装 GoFrame CLI 工具 ```bash go install github.com/gogf/gf/cmd/gf/v2@latest ``` 或者使用 `Makefile`: ```bash make cli ``` ### 配置 MySQL 数据库 使用`Docker`运行`MySQL`数据库: ```bash docker run -d \ --name mysql-user-http \ -p 3306:3306 \ -e MYSQL_ROOT_PASSWORD=12345678 \ -e MYSQL_DATABASE=test \ mysql:8.0 ``` ### 初始化数据库 执行`SQL`脚本创建用户表: ```bash # 连接到 MySQL docker exec -i mysql-user-http mysql -uroot -p12345678 test < manifest/sql/create.sql ``` 或者手动执行以下 SQL: ```sql CREATE TABLE `user`( `id` int(10) unsigned NOT NULL AUTO_INCREMENT COMMENT 'User ID', `passport` varchar(45) NOT NULL COMMENT 'User Passport', `password` varchar(45) NOT NULL COMMENT 'User Password', `nickname` varchar(45) NOT NULL COMMENT 'User Nickname', `create_at` datetime DEFAULT NULL COMMENT 'Created Time', `update_at` datetime DEFAULT NULL COMMENT 'Updated Time', PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; ``` ## 配置说明 在`manifest/config/config.yaml`中更新数据库配置: ```yaml server: address: ":8000" openapiPath: "/api.json" swaggerPath: "/swagger" database: default: link: "mysql:root:12345678@tcp(127.0.0.1:3306)/test" debug: true ``` ## 使用说明 ### 生成代码 从数据库生成`DAO`、`DO`和`Entity`代码: ```bash make dao ``` ### 运行服务器 启动`HTTP`服务器: ```bash go run main.go ``` 服务器将在端口`8000`上启动,提供以下接口: - `Swagger UI`:http://localhost:8000/swagger - `OpenAPI`规范:http://localhost:8000/api.json ### 测试服务 #### 使用 Swagger UI 在浏览器中打开 http://localhost:8000/swagger,使用内置的`Swagger`界面与`API`交互。 #### 使用 cURL 1. **注册** - 注册新用户: ```bash curl -X POST http://localhost:8000/user/sign-up \ -H "Content-Type: application/json" \ -d '{ "Passport": "user001", "Password": "123456", "Password2": "123456", "Nickname": "测试用户" }' ``` 2. **检查账号可用性**: ```bash curl -X POST http://localhost:8000/user/check-passport \ -H "Content-Type: application/json" \ -d '{"Passport": "user001"}' ``` 3. **登录** - 用户认证: ```bash curl -X POST http://localhost:8000/user/sign-in \ -H "Content-Type: application/json" \ -d '{ "Passport": "user001", "Password": "123456" }' \ -c cookies.txt ``` 4. **检查登录状态**: ```bash curl -X POST http://localhost:8000/user/is-signed-in \ -b cookies.txt ``` 5. **获取用户资料**(需要认证): ```bash curl -X GET http://localhost:8000/user/profile \ -b cookies.txt ``` 6. **登出**: ```bash curl -X POST http://localhost:8000/user/sign-out \ -b cookies.txt ``` ## 接口文档 ### 公开接口 #### SignUp **POST** `/user/sign-up` 注册新用户账号。 **请求体:** ```json { "Passport": "string (6-16 字符,必填)", "Password": "string (6-16 字符,必填)", "Password2": "string (必须与 Password 相同,必填)", "Nickname": "string (可选)" } ``` **响应:** ```json { "code": 0, "message": "", "data": {} } ``` #### SignIn **POST** `/user/sign-in` 用户认证并创建会话。 **请求体:** ```json { "Passport": "string (必填)", "Password": "string (必填)" } ``` **响应:** ```json { "code": 0, "message": "", "data": {} } ``` #### CheckPassport **POST** `/user/check-passport` 检查账号是否可用于注册。 **请求体:** ```json { "Passport": "string (必填)" } ``` **响应:** ```json { "code": 0, "message": "", "data": {} } ``` #### CheckNickname **POST** `/user/check-nickname` 检查昵称是否可用于注册。 **请求体:** ```json { "Nickname": "string (必填)" } ``` **响应:** ```json { "code": 0, "message": "", "data": {} } ``` #### IsSignedIn **POST** `/user/is-signed-in` 检查当前用户是否已登录。 **响应:** ```json { "code": 0, "message": "", "data": { "OK": true } } ``` #### SignOut **POST** `/user/sign-out` 登出当前用户并清除会话。 **响应:** ```json { "code": 0, "message": "", "data": {} } ``` ### 受保护接口 #### Profile **GET** `/user/profile` 获取当前已登录用户的资料(需要认证)。 **响应:** ```json { "code": 0, "message": "", "data": { "id": 1, "passport": "user001", "nickname": "测试用户", "create_at": "2026-02-10 10:30:00", "update_at": "2026-02-10 10:30:00" } } ``` ## 实现细节 ### Controller 层 `internal/controller/user/`处理`HTTP`请求: - 接收并验证请求参数 - 调用`Service`层处理业务逻辑 - 返回标准化的`JSON`响应 - 从`g.Meta`标签自动绑定路由 ### Service 层 `internal/service/`包含业务逻辑: #### 用户服务 - 带验证的用户创建 - 认证和会话管理 - 用户资料查询 - 账号/昵称可用性检查 #### 中间件服务 - 自定义业务数据的上下文注入 - 受保护路由的认证中间件 - 跨域支持的 `CORS` 中间件 #### 会话服务 - 用户会话管理 - 会话存储和检索 - 请求中的用户上下文 #### 业务上下文服务 - 自定义上下文数据注入 - 请求上下文中的用户信息 - 会话集成 ### DAO 层 `internal/dao/`提供数据库访问: - 从数据库`Schema`自动生成 - 类型安全的数据库操作 - 链式查询构建器 - 事务支持 ### 数据模型 - **DO (Domain Object):** 用于数据库操作 - **Entity:** 表示数据库表结构 - **API Models:** 带验证的请求/响应结构 ## 其他操作 ### 构建 Docker 镜像 ```bash make image ``` ### 部署到 Kubernetes ```bash make deploy ``` ## 注意事项 - 启动应用前请确保 `MySQL` 已运行 - 默认数据库凭据为 `root:12345678`(生产环境请修改) - 服务默认使用端口 `8000` - 会话默认存储在内存中(生产环境请配置 Redis) - 密码应在生产环境中加密(这是演示项目) - `Swagger UI`位于 http://localhost:8000/swagger - `OpenAPI`规范位于 http://localhost:8000/api.json - `SQL Schema`位于`manifest/sql/create.sql` ## 安全建议 这是一个演示项目。生产环境使用时请考虑: - 使用`bcrypt`或类似方式加密密码 - 使用`HTTPS`进行安全通信 - 实现速率限制 - 添加`CSRF`保护 - 使用`Redis`或类似方式存储会话 - 实现适当的日志记录和监控 - 使用`JWT`令牌实现无状态认证 # 如何从v1愉快升级到v2 ## 写在前面 `GoFrame` 框架发布了 `v2` 版本,这是一个里程碑版本,包含了很多新功能特性和大量改进,并且发布了一些开创性的特性。 如果是新用户,请直接使用 `v2` 起飞。如果是老用户,墙裂推荐从 `v1` 升级为 `v2`,更加稳定可靠。 `v2` 升级的几点重要说明: 1. 为保证兼容性,按照 `Golang` 官方 `module` 管理的规范,我们将 `import` 路径变化了,因此需要全局替换 `import` 路径。 2. 由于是大版本升级,因此存在部分方法删减、更新的情况,大家放心一切都有更好的方案提供啦。 3. 一般来说升级后重新编译,根据编译错误提示修改代码即可完成升级。 4. `gredis` 组件由于支持集群化,因此配置发生了变化,这块需要注意。 我们没有提供升级工具,因为我们觉得提供升级指导即可。 ## v1与v2的兼容性 为了保证项目兼容性,能够同时依赖 `v1` 和 `v2` 版本,因此我们发布了 `v1` 最后一个版本 `v1.16.7`,大家有需要可以升级。并且解决了 `client_tracing.go:73:3: undefined: attribute.Any` 的常见问题。但同时依赖两个版本的 `GoFrame` 可能会降低项目维护性,因此建议大家尽快升级到 `v2` 版本。 ## 将依赖替换为v2 全局替换源代码即可,规则如下: ```go "github.com/gogf/gf/ => "github.com/gogf/gf/v2/ ``` 像这样: ## 下载最新v2版本 ```bash go get -u github.com/gogf/gf/v2@latest ``` ## 工程目录的调整 如果使用的是 `GoFrame` 官方推荐的工程目录结构,可以参考最新的工程目录结构手动调整即可: [工程目录设计](../design/project-structure.md) 需要注意的是,最新的 `cli` 工具不再支持旧版工程目录的项目创建。 ## 编译运行修改 运行的项目,如果遇到编译问题,根据错误提示进行手动修改,如此循环。 如果不知道如何修改,请在本文档评论,我们的社区团队小伙伴将会及时给与升级帮助。 ## CLI的重要变化 1. 去掉了 `swagger` 命令。 `v1` 版本的 `gf swagger` 命令时通过自动安装第三方的 `swag` 工具,解析源代码中的注释生成的 `swagger` 文档。这种文档的管理维护方式会有一些问题:仅支持 `Swagger2.0` 协议、使用体验很差、注释难以和代码同步维护,造成接口文档与代码不一致的问题。新版本有了规范路由,该命令即废弃掉了。如果需要继续使用该命令的功能,可以手动安装使用第三方 `swag` 工具: [https://github.com/swaggo/swag](https://github.com/swaggo/swag) 2. 去掉了 `update` 命令。 `v2` 版本开始, `CLI` 工具的安装下载统一走 `github`,以减少 `CLI` 工具的维护工作量。后续可能会重新增加该命令。 ## 一些重要说明 1. `gf-cli` 的仓库已经搬迁到了 `gf` 主库维护,便于保证工具与框架版本同步。原有仓库不再维护。具体请查看说明: [https://github.com/gogf/gf/tree/master/cmd/gf](https://github.com/gogf/gf/tree/master/cmd/gf) 2. 框架核心组件采用了接口化设计,为保证接口通用性,对外暴露的方法有个别删减。 3. 框架核心组件大量使用了 `gvar` 泛型,以提高易用性、屏蔽底层的具体类型实现。为保证稳定性和易用性,框架在未来 `2-3` 年内不会考虑使用 `Golang` 官方泛型。官方泛型在框架部分组件的部分特性下有改造价值。 # 格式化打印占位符 `*printf` 系列函数都支持 `format` 格式化参数,在这里我们按照占位符将被替换的变量类型划分,方便查询和记忆。 ## 占位符列表 | 分类 | 占位符 | 说明 | | --- | --- | --- | | **通用占位符** | `%v` | 值的默认格式表示 | | `%+v` | 类似 `%v`,但输出结构体时会添加字段名 | | `%#v` | 值的Go语法表示 | | `%T` | 打印值的类型 | | `%%` | 百分号 | | **布尔型** | `%t` | `true` 或 `false` | | **整型** | `%b` | 表示为二进制 | | `%c` | 该值对应的 `Unicode` 码值 | | `%d` | 表示为十进制 | | `%o` | 表示为八进制 | | `%x` | 表示为十六进制,使用 `a-f` | | `%X` | 表示为十六进制,使用 `A-F` | | `%U` | 表示为 `Unicode` 格式: `U+1234`,等价于 `U+%04X` | | `%q` | 该值对应的单引号括起来的Go语法字符字面值,必要时会采用安全的转义表示 | | **浮点数与复数** | `%b` | 无小数部分、二进制指数的科学计数法,如: `-123456p-78` | | `%e` | 科学计数法,如: `-1234.456e+78` | | `%E` | 科学计数法,如: `-1234.456E+78` | | `%f` | 有小数部分但无指数部分,如: `123.456` | | `%F` | 等价于 `%f` | | `%g` | 根据实际情况采用 `%e` 或 `%f` 格式(以获得更简洁、准确的输出) | | `%G` | 根据实际情况采用 `%E` 或 `%F` 格式(以获得更简洁、准确的输出) | | **字符串和\[\]byte** | `%s` | 直接输出字符串或者 `[]byte` | | `%q` | 该值对应的双引号括起来的Go语法字符串字面值,必要时会采用安全的转义表示 | | `%x` | 每个字节用两字符十六进制数表示(使用 `a-f`) | | `%X` | 每个字节用两字符十六进制数表示(使用 `A-F`) | | **指针** | `%p` | 表示为十六进制,并加上前导的 `0x` | | **宽度标识符** | `%f` | 默认宽度,默认精度 | | `%9f` | 宽度9,默认精度 | | `%.2f` | 默认宽度,精度2 | | `%9.2f` | 宽度9,精度2 | | `%9.f` | 宽度9,精度0 | | **占位修饰符** | `+` | 总是输出数值的正负号;对 `%q`( `%+q`)会生成全部是ASCII字符的输出(通过转义) | | ` ` | 空格,对数值,正数前加空格而负数前加负号;对字符串采用 `%x` 或 `%X` 时( `% x` 或 `% X`)会给各打印的字节之间加空格 | | `-` | 在输出右边填充空白而不是默认的左边(即从默认的右对齐切换为左对齐) | | `#` | 八进制数前加 `0`( `%#o`),十六进制数前加 `0x`( `%#x`)或 `0X`( `%#X`),指针去掉前面的 `0x`( `%#p`)对 `%q`( `%#q`),对 `%U`( `%#U`)会输出空格和单引号括起来的Go字面值 | | `0` | 使用 `0` 而不是空格填充,对于数值类型会把填充的 `0` 放在正负号后面 | | | | | ## 参考资料 - [https://pkg.go.dev/fmt](https://pkg.go.dev/fmt) - [https://www.liwenzhou.com/posts/Go/go\_fmt/](https://www.liwenzhou.com/posts/Go/go_fmt/) # 附录:系统信号列表 ## Linux | 信号 | 取值 | 默认动作 | 含义(发出信号的原因) | | --- | --- | --- | --- | | `SIGHUP` | 1 | Term | 终端的挂断或进程死亡 | | `SIGINT` | 2 | Term | 来自键盘的中断信号 | | `SIGQUIT` | 3 | Core | 来自键盘的离开信号 | | `SIGILL` | 4 | Core | 非法指令 | | `SIGABRT` | 6 | Core | 来自 abort 的异常信号 | | `SIGFPE` | 8 | Core | 浮点例外 | | `SIGKILL` | 9 | Term | 杀死 | | `SIGSEGV` | 11 | Core | 段非法错误 (内存引用无效) | | `SIGPIPE` | 13 | Term | 管道损坏:向一个没有读进程的管道写数据 | | `SIGALRM` | 14 | Term | 来自 alarm 的计时器到时信号 | | `SIGTERM` | 15 | Term | 终止 | | `SIGUSR1` | 30,10,16 | Term | 用户自定义信号 1 | | `SIGUSR2` | 31,12,17 | Term | 用户自定义信号 2 | | `SIGCHLD` | 20,17,18 | Ign | 子进程停止或终止 | | `SIGCONT` | 19,18,25 | Cont | 如果停止,继续执行 | | `SIGSTOP` | 17,19,23 | Stop | 非来自终端的停止信号 | | `SIGTSTP` | 18,20,24 | Stop | 来自终端的停止信号 | | `SIGTTIN` | 21,21,26 | Stop | 后台进程读终端 | | `SIGTTOU` | 22,22,27 | Stop | 后台进程写终端 | | `SIGBUS` | 10,7,10 | Core | 总线错误(内存访问错误) | | `SIGPOLL` | | Term | Pollable 事件发生 (Sys V),与 SIGIO 同义 | | `SIGPROF` | 27,27,29 | Term | 统计分布图用计时器到时 | | `SIGSYS` | 12,-,12 | Core | 非法系统调用 (SVr4) | | `SIGTRAP` | 5 | Core | 跟踪 / 断点自陷 | | `SIGURG` | 16,23,21 | Ign | socket 紧急信号 (4.2BSD) | | `SIGVTALRM` | 26,26,28 | Term | 虚拟计时器到时 (4.2BSD) | | `SIGXCPU` | 24,24,30 | Core | 超过 CPU 时限 (4.2BSD) | | `SIGXFSZ` | 25,25,31 | Core | 超过文件长度限制 (4.2BSD) | | `SIGIOT` | 6 | Core | IOT 自陷,与 SIGABRT 同义 | | `SIGEMT` | 7,-,7 | | Term | | `SIGSTKFLT` | -,16,- | Term | 协处理器堆栈错误 (不使用) | | `SIGIO` | 23,29,22 | Term | 描述符上可以进行 I/O 操作 | | `SIGCLD` | -,-,18 | Ign | 与 SIGCHLD 同义 | | `SIGPWR` | 29,30,19 | Term | 电力故障 (System V) | | `SIGINFO` | 29,-,- | | 与 SIGPWR 同义 | | `SIGLOST` | -,-,- | Term | 文件锁丢失 | | `SIGWINCH` | 28,28,20 | Ign | 窗口大小改变 (4.3BSD, Sun) | | `SIGUNUSED` | -,31,- | Term | 未使用信号 (will be SIGSYS) | ## Window | 信号 | 说明 | | --- | --- | | **`SIGABRT`** | 异常终止 | | **`SIGFPE`** | 浮点错误 | | **`SIGILL`** | 非法指令 | | **`SIGINT`** | Ctrl+C 信号 | | **`SIGSEGV`** | 非法存储区访问 | | **`SIGTERM`** | 终止请求 | # 环境安装 该章节为手摸手安装 `Golang` 开发环境及 `IDE` 配置教程,仅针对 `Golang` 萌新,老司机请忽略。 ## Go开发环境安装 ### Step1 - 下载Go开发包 访问`Go`国内镜像站下载页面 [https://golang.google.cn/dl/](https://golang.google.cn/dl/),并在页面最上方的版本中选择当前的系统版本,会下载最新版本的`Go`开发包: ### Step2 - 安装引导 访问官方安装介绍页面 [https://golang.google.cn/doc/install](https://golang.google.cn/doc/install),按照当前系统版本执行对应的安装流程即可。 Windows( `msi`)和MacOS( `pkg`)推荐使用安装包的方式来安装。作者当前MacOS安装包( `pkg`)安装过程如下图所示: `Go`的开发包升级也是同样的过程。 ## IDE开发环境安装 目前 `Go` 的 `IDE` 有两款比较流行,一款是 `VSCode+Plugins`(免费),另一款是 `JetBrains` 公司的 `Goland`(收费)。由于 `JetBrains` 也是 `GoFrame` 框架的赞助商,因此我们这里优先推荐使用 `Goland` 来作为开发IDE,下载及注册请参考网上教程( [百度](https://www.baidu.com/s?wd=goland%20安装) 或 [Google](https://www.google.com/search?q=goland+安装))。 `JetBrains` 的官方网站为: [https://www.jetbrains.com](https://www.jetbrains.com/?from=GoFrame) ### Goland的使用 我们来创建第一个 `Go` 程序吧,老规矩,上 `hello world`。 #### Step1. 打开IDE #### Step2. 创建项目 这里需要注意的是 `Go` 安装文件的路径( `SDK`), [官方安装文档](https://golang.google.cn/doc/install) 有详细说明,请仔细阅读。 其中的 `Location` 随意选择一个本地路径即可。 #### Step3. 创建程序 编辑 `main.go`,并输入以下代码: ```go package main import "fmt" func main() { fmt.Println("hello world!") } ``` #### Step4. 执行运行 菜单栏 `Run` - `Run` - `go build hello.go`。 完成第一个 `Go` 程序便成功了。 ### VSCode的使用 #### [Step1 - 下载安装](https://code.visualstudio.com/) #### [Step2 - 安装Go扩展](https://docs.microsoft.com/zh-cn/learn/modules/go-get-started/4-install-visual-studio-code?ns-enrollment-type=learningpath&ns-enrollment-id=learn.languages.go-first-steps) #### [Step3 - Hello World](https://docs.microsoft.com/zh-cn/learn/modules/go-get-started/5-hello-world) # Go Module `Go Module` 是从 Go 版本 `1.11.1` 开始官方提供的包管理工具,用于解决`Go`项目的包管理及依赖,类似于`PHP`的 `composer`、`Nodejs`的 `npm`。 本章节会对 `Go Module` 的一些常用的实用的命令/设置进行介绍,更详细的介绍请查看官方文档: [https://go.dev/wiki/Modules](https://go.dev/wiki/Modules) ## 关于 `go.mod` `go.mod` 是 Go 项目的依赖描述文件,该文件主要用来描述两个事情: 1. 当前项目名( `module`)是什么。每个项目都应该设置一个名称,当前项目中的包( `package`)可以使用该名称进行相互调用。 2. 当前项目依赖的第三方包名称。项目运行时会自动分析项目中的代码依赖,生成 `go.sum` 依赖分析结果,随后 go 编译器会去下载这些第三方包,然后再编译运行。 我们可以看到之前的 `hello world` 项目下有一个自动生成的 `go.mod` 文件,其内容如下: ```go module hello ``` 其中, `hello` 为当前项目的名称,在我们初始化项目的时候`Goland IDE`自动帮助我们生成了该文件,默认情况下该`module`的名称为目录的名称,该名称可以随意设置。 ## 关于GOPROXY `GOPROXY` 是一个环境变量,用于设置代理地址加速下载依赖或下载自己的私有依赖。 详见 Go 官网说明: [https://go.dev/wiki/Modules](https://go.dev/wiki/Modules#are-there-always-on-module-repositories-and-enterprise-proxies) 常见的公共`GOPROXY`反向代理地址有: - [`https://goproxy.cn`](https://goproxy.cn) - [`https://goproxy.io`](https://goproxy.io) - [`https://mirrors.aliyun.com/goproxy/`](https://developer.aliyun.com/mirror/goproxy) ## 使用 `go.mod` ### 使用 Goland IDE 1. 设置 `Goland` 启用 `Go Module`特性 在下载第三方依赖包时,需要科学上网。笔者本地设置了一个环境变量`GOPROXY`用于科学上网拉取依赖: ```bash GOPROXY=https://goproxy.cn ``` 如果本地环境已经有 `VPN` 功能,那么可以忽略 `GOPROXY` 的设置,可以添加`direct` 后缀表示不使用代理。 ```bash GOPROXY=https://goproxy.cn,direct ``` 2. 手动修改 `go.mod` 文件如下: ```go module hello require github.com/gogf/gf/v2 latest ``` 增加 `GoFrame` 框架的依赖,其中 `latest` 表示使用`github.com/gogf/gf/v2`最新版本,`IDE`将会立即去更新下载框架代码。成功后,`IDE`将会修改 `go.mod` 文件并生成 `go.sum` 依赖分析文件。该`go.sum`文件为该项目所有的第三方依赖,通常也应该推送到版本管理仓库中。 3. 随后 `go.mod` 文件被自动更新为: ```go module hello require github.com/gogf/gf/v2 v2.8.1 ``` 其中 `v2.8.1` 表示`Go Module`检测到的最新框架版本。 ### 使用命令行 1. 打开 `Terminal`,在项目根目录下执行: ```bash go env -w GO111MODULE=on go env -w GOPROXY=https://goproxy.cn,direct go get -u github.com/gogf/gf/v2 ``` 推荐使用`go env -w`的方式设置Go的环境变量(其他方式请[参考](https://goproxy.cn/#usage)),以上命令将会使用代理立即下载最新稳定版本的 `GoFrame` 框架。 2. 随后 `go.mod` 文件内容被自动更新为: ```go module hello require github.com/gogf/gf/v2 v2.8.1 ``` 且生成了新的 `go.sum` 依赖分析文件。 # 开发环境配置 ## Go环境变量 为方便开发,在开发环境往往需要设置三个环境变量: 1. `$GOROOT`: `go` 的安装目录,配置后不会再更改; 2. `$GOPATH`: `go` 项目在本地的开发环境的的项目根路径(以便项目编译, `go build`, `go install`),不同的项目在编译的时候该环境变量可以不同; 3. `$PATH`(重要):需要将 `go` 的 `bin` 目录添加到系统 `$PATH` 中以便方便使用go的相关命令,配置后也不会再更改; `Go`的环境变量在官方文档中也有详情的说明,请参考链接: [https://golang.google.cn/doc/install/source](https://golang.google.cn/doc/install/source) **提示** 环境变量中的 `$GOOS` 和 `$GOARCH` 是比较实用的两个变量,可以用在不同平台的交叉编译中,只需要在 `go build` 之前设置这两个变量即可,这也是`Go`语言的优势之一:可以编译生成跨平台运行的可执行文件。例如,在 `Linux amd64` 架构下编译 `Windows x86` 的可执行文件,可以使用如下命令: ``` CGO_ENABLED=0 GOOS=windows GOARCH=386 go build hello.go ``` 遗憾的是交叉编译暂不支持 `cgo` 方式,因此需要将环境变量 `$CGO_ENABLED` 设置为`0`,这样执行之后会在当前目录生成一个 `hello.exe` 的 `windows x86` 架构的可执行文件。 ### 环境变量设置 除了 `$PATH` 环境外,其他环境变量都是可选的。 为什么说这个步骤可选呢?因为未来的 `Go` 版本慢慢开始移除对 `$GOPATH`/ `$GOROOT` 的支持。此外,在 `Goland` 这个`IDE`中集成有 `Terminal` 功能,直接使用这个功能中已经设置好了环境变量。 ### `*nix` 下设置环境变量 在 `*nix` 系统下(`Linux/Unix/MacOS/*BSD` 等等),需要在 `/etc/profile` 中增加以下环境变量设置后,执行命令 `#source /etc/profile` 重新加载profile配置文件(或重新登录),将以下变量添加到用户的环境变量中: ```bash export GOROOT=/usr/local/go export GOPATH=/Users/john/Workspace/Go/GOPATH export PATH=$GOPATH/bin:$GOROOT/bin:$PATH ``` ### `Windows` 下设置环境变量 `Windows`如何修改系统环境变量,以及修改环境变量 `PATH`,请参考网上教程( [百度](https://www.baidu.com/s?wd=Windows%20%E4%BF%AE%E6%94%B9%E7%B3%BB%E7%BB%9F%E7%8E%AF%E5%A2%83%E5%8F%98%E9%87%8F%20PATH) 或 [Google](https://www.google.com/search?q=Windows+修改系统环境变量+PATH))。 ## IDE工具配置 本文以 `Goland` 开发工具为基础,介绍在该IDE下的常用工具配置。 常用的工具包括: 1. `go fmt` : 统一的代码格式化工具(必须)。 2. `golangci-lint` : 静态代码质量检测工具,用于包的质量分析(推荐)。 3. `goimports` : 自动 `import` 依赖包工具(可选)。 4. `golint` : 代码规范检测,并且也检测单文件的代码质量,比较出名的Go质量评估站点 [Go Report](https://goreportcard.com) 在使用(可选)。 ### `go fmt`, `goimports`, `golangci-lint` 由于这三个工具是 `Goland` 自带的,因此配置比较简单,参考以下图文操作示例: 1. 在 `Goland` 的设置中,选择 `Tools` - `File Watchers`,随后选择添加 2. 依次点击添加这3个工具,使用默认的配置即可 3. 随后在撸代码的过程中保存代码文件时将会自动触发这3个工具的自动检测。 ### `golint` 工具的安装及配置(可选) #### `golint` 的安装 由于 `Goland` 没有自带 `golint` 工具,因此首先要自己去下载安装该工具。 **如果有goproxy配置了**,可以直接 `go install golang.org/x/lint/golint@latest` 安装,就不需要使用下方命令了。 使用以下命令安装: ```bash mkdir -p $GOPATH/src/golang.org/x/ cd $GOPATH/src/golang.org/x/ git clone https://github.com/golang/lint.git git clone https://github.com/golang/tools.git cd $GOPATH/src/golang.org/x/lint/golint go install ``` 安装成功之后将会在 `$GOPATH/bin` 目录下看到自动生成了 `golint` 二进制工具文件。 #### `golint` 的配置 1. 随后在 `Goland` 的 `Tools` - `File Watchers` 配置下,通过复制 `go fmt` 的配置 2. 修改 `Name`, `Program`, `Arguments` 三项配置,其中 `Arguments` 需要加上 `-set_exit_status` 参数,如图所示: 3. 保存即可,随后在代码编写中执行保存操作时将会自动触发 `golint` 工具检测。 ### `golangci-lint` 的配置(可选) 1.  随后在 `Goland` 的 `Tools` \- `File Watchers` 配置下,通过复制 `go fmt` 的配置 2. 修改 `Name`, `Program`, `Arguments` 三项配置,其中 `Arguments` 需要加上 `run $FileDir$` 参数, `注意:` 如 `Advanced Options`的选项可以在机器比较慢的时取消选择,如图所示: 3. 保存即可,随后在代码编写中执行保存操作时将会自动触发 `golangci-lint` 工具检测。 4. 通过 `go Linter` 插件管理 `golangci-lint` 工具的配置,如下是 `go Linter` 的安装以及配置。 ## IDE代码风格配置 # 私有依赖管理 ## 版本选择算法 当项目中存在同一个第三方包依赖,并且依赖版本不一致时, `Go Modules` 使用的「最小版本选择算法」( `The minimal version selection algorithm`: [https://github.com/golang/go/wiki/Modules#version-selection](https://github.com/golang/go/wiki/Modules#version-selection) )。 例如,如果的模块依赖于具有 `require D v1.0.0` 的模块A,并且的模块还依赖于具有 `require D v1.1.1` 的模块B,则最小版本选择将会选择D的 `v1.1.1` 版本用以构建(使用最高版本)。 > 请不要问我为什么这个算法名字叫「最小版本选择算法」,然而内容却是「最高版本选择算法」,若有纠结于此的同学欢迎向官方提issue: [https://github.com/golang/go/issues](https://github.com/golang/go/issues) ## 私有依赖管理 如果可以通过 `go.mod` 完美地管理当前的项目包依赖,那么可以忽略该章节。如果在处理项目的包依赖管理中遇到了问题,那么建议继续阅读该章节,可以找到解决问题的灵感。 前面章节我们非常详细、图文并茂地介绍了基本的开发环境安装/配置、 `Go Module` 安装使用,在实际的项目开发中,会发现更多的问题,常见的: 1. 虽然 `GoFrame` 足够强大,但多数时候依赖的包不仅仅是 `GoFrame`,还包括一些额外的第三方包,特别是 `golang.org` 的包,需要自带梯子翻墙下载,即使本地方便处理,但是在自动部署系统上可能会稍麻烦; 2. 一些自研开发的第三方包,特别是一些业务依赖包,是不允许公开下载的(私有库),并且版本库也可能不支持 `HTTPS` 协议,因此无法使用 `go get` 或者 `go.mod` 进行下载和管理; 3. 等等; 如果遇到了上面所提到的问题,我们建议的解决方案:通过 `GOPRIVATE` 的方式设置私有包有效域名。 例如以下命令行方式: ```bash export GOPROXY=https://goproxy.cn export GOPRIVATE=git.xxx.com CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o main main.go ``` > 该特性需要 `Go v1.13` 以上版本支持。 在 `Goland` 中这么设置: # 常见问题(FAQ) 寄语: - 请使用页面右上角的搜索功能,全文快速检索常见问题。 - 欢迎大家积极参与编辑,把自己遇到的坑怎么填的记录起来。 **众人拾柴火焰高** ## 一、Golang基础 ### 1、程序产生异常,但是程序直接崩溃未被框架自动捕获 使用 `GoFrame` 框架是严谨和安全的,如果程序产生了异常,会默认被框架捕获。如果未被自动捕获,那么可能是由于程序逻辑自行开了新的 `goroutine`,在新的 `goroutine` 中产生了异常。因此这里有两个方案可供大家选择: - 不建议在请求中再开 `goroutine` 来处理请求,这样或使得 `goroutine` 快速膨胀,当 `goroutine` 多了之后也会在 `Go` 引擎层面影响程序的整体调度。 - 如果实在有必要新开 `goroutine` 的场景下,可以考虑使用 `grpool.AddWithRecover` 来创建新的 `goroutine`,见名知意,它会自动捕获异常。更详细的介绍请参考: [协程管理-grpool](./components/os-grpool.md) ### 2、 `json` 输出时屏蔽掉一些字段 可以通过结构体嵌套的方式实现,通过使用 `*struct{}` 类型不占用空间以及 `omitempty` 字段为空不输出字段的特性 ```go type User struct { Pwd string `json:"pwd"` Age int `json:"age"` } type UserOut struct { User Pwd *struct{} `json:"pwd,omitempty"`// 这里的字段json名需要和嵌套的字段json名一致,否则无效 } func TestJson(t *testing.T) { u := User{Pwd: "123", Age: 1} bb := UserOut{User: u} b, _ := json.MarshalIndent(bb, "", " ") t.Log(string(b)) } ``` ## 二、兼容性相关 ### 1、 `client_tracing.go:73:3: undefined: attribute.Any` 以下错误: ```bash D:\Program Files\Go\bin\pkg\mod\github.com\gogf\gf@v1.16.6\net\ghttp\internal\client\client_tracing.go:73:3: undefined: attribute.Any D:\Program Files\Go\bin\pkg\mod\github.com\gogf\gf@v1.16.6\net\ghttp\internal\client\client_tracing_tracer.go:150:3: undefined: attribute.Any D:\Program Files\Go\bin\pkg\mod\github.com\gogf\gf@v1.16.6\net\ghttp\internal\client\client_tracing_tracer.go:151:3: undefined: attribute.Any ``` 导致该错误的原因在于目前正在使用的 `goframe` 依赖的 `otel` 包版本过低( `otel` 包是 `OpenTelemetry` 使用 `Golang` 实现的第三方包,比较常用,很多第三方基础组件都会依赖),而项目中其他的第三方依赖的 `otel` 包过高,按照 `Golang module` 的管理策略,项目将会使用最新的 `otel` 包,于是导致了版本不兼容。 根因还是在于 `otel` 的包在迭代中出现了不兼容升级导致,不过目前 `otel` 包已经较稳定,出现不兼容的可能性降低。 解决的办法是只有升级 `goframe` 的版本, `goframe` 最新版本已经更新使用了稳定的 `otel` 包。如果使用的已经是 `v1` 的最新版本( `v1.16`),那么请升级为 `v2` 版本解决。 ### 2、使用 `gf` 依赖 `v1.16.2` 时 `go mod tidy` 失败 `found (v0.36.0), but does not contain package go.opentelemetry.io/otel/metric/registry` 解决办法,升级 `gf` 依赖到 `v1.16.9` 再 `go mod tidy` ### 3、升级引起兼容性报错 ..\..\net\ghttp\ghttp_server.go:300:9: table.SetHeader undefined (type *tablewriter.Table has no field or method SetHeader) 使用`go get -u`导致间接依赖版本提升,出现如下的报错。 >..\..\net\ghttp\ghttp_server.go:300:9: table.SetHeader undefined (type *tablewriter.Table has no field or method SetHeader) ..\..\net\ghttp\ghttp_server.go:301:9: table.SetRowLine undefined (type *tablewriter.Table has no field or method SetRowLine) ..\..\net\ghttp\ghttp_server.go:302:9: table.SetBorder undefined (type *tablewriter.Table has no field or method SetBorder) ..\..\net\ghttp\ghttp_server.go:303:9: table.SetCenterSeparator undefined (type *tablewriter.Table has no field or method SetCenterSeparator) 原因: 使用`go get -u`命令升级了,如"github.com/gogf/gf/contrib/drivers/mysql/v2"这样的依赖了"github.com/gogf/gf/v2"主库的组件,会导致间接依赖被同时升级到最新版本。 间接依赖被升级到最新版本后,如果间接依赖不兼容的旧版就会报错。 解决方式: ```bash // linux go list -m -f '{{if and .Indirect (not .Main)}}{{.Path}}{{end}}' all | xargs -I {} go mod edit -droprequire={} && go mod tidy // windows cmd (FOR /F "tokens=*" %G IN ('go list -m -f "{{if and .Indirect (not .Main)}}{{.Path}}{{end}}" all') DO @go mod edit -droprequire=%G) & go mod tidy // 以上方式还不能解决的时候,手动修改 go.mod 的版本号降级,然后执行 go mod tidy // 例如 // 将 go.mod 文件中的 tablewriter 依赖版本从较高版本降级为 v0.0.5: // 修改前(go.mod 部分): // github.com/olekukonko/tablewriter v1.0.9 // indirect // 修改后(go.mod 部分): // github.com/olekukonko/tablewriter v0.0.5 // indirect // 保存 go.mod 后,执行 go mod tidy 以更新依赖。 ``` ## 三、数据库相关 请参考章节: [ORM常见问题](./core/gdb-faq.md) ## 四、使用相关 ### 1、不同环境如何,加载不同的配置文件? 不同环境指的是:开发环境/测试环境/预发环境/生产环境等。 - 首先,在一些互联网项目中,特别是分布式或者微服务化的架构下,一般会使用配置管理中心,不同的环境会对应不同的配置管理中心,所以这样的场景不会存在这样的问题。 - 其次,如果是传统的项目管理方式下,可能会将配置文件放到代码仓库中共同管理,这样的方式是不推荐的。如果仍然想要这么做,可以通过系统环境变量或者命令行启动参数,让程序自动选择配置文件或者指定配置目录,参考 [配置管理](./core/gcfg.md) 章节。例如: `./app --gf.gcfg.file config-prod.toml ` 则通过命令行启动参数的方式将默认读取的配置文件修改为了 `config-prod.toml` 文件。 我们不建议在程序中通过代码逻辑来区分和读取不同环境的配置文件。 ### 2、 `glog with "ERROR: logging before flag.Parse"` `Golang` 官方有个简单的日志库包名也叫做 `glog`,检查文件顶部 `import` 的包名,将 `github.com/golang/glog` 修改为框架的日志组件即可,日志组件使用请参考: [日志组件](./core/glog.md) ### 3、 `gcron` 与 `http` 如何同时使用? ```go func main() { //定时任务1 gcron.AddSingleton("*/5 * * * * *", func() { task.Test() glog.Debug("gcron1") }) //定时任务2 gcron.AddSingleton("*/10 * * * * *", func() { glog.Debug("gcron2") }) //接收http请求 g.Server().Run() } ``` 注意, `gcron` 一定要在 `g.Server().Run` 的前面。 ### 4、 `GoFrame` 的 `struct tag`(标签) 有哪些? 参数请求、数据校验、 `OpenAPIv3`、命令管理、数据库ORM。 | Tag(简写) | 全称 | 描述 | 相关文档 | | --- | --- | --- | --- | | `v` | `valid` | 数据校验标签。 | [Struct校验-基本使用](./core/gvalid-parameter-type-struct-example.md) | | `p` | `param` | 自定义请求参数匹配。 | [请求输入-对象处理](./web/request-struct-converting.md) | | `d` | `default` | 请求参数默认值绑定。 | [请求输入-默认值绑定](./web/request-default-value.md) | | `orm` | `orm` | ORM标签,用于指定表名、关联关系。 | [数据规范-gen dao](./cli/gen-dao.md)
    [模型关联-静态关联-With特性](./core/gdb-chaining-relation-with.md) | | `dc` | `description` | 通用结构体属性描述,ORM和接口都用到。属于框架默认的属性描述标签。 | | 其他: - 命令行结构化管理参数: [命令管理-结构化参数](./core/gcmd-struct.md) - 框架常用标签标签集中管理到了 `gtag` 组件下: [https://github.com/gogf/gf/blob/master/util/gtag/gtag.go](https://github.com/gogf/gf/blob/master/util/gtag/gtag.go) - 在接口文档章节,由于采用了标签形式生成 `OpenAPI` 文档,因此标签比较多,具体请参考章节: [接口文档](./web/api-document.md) ### 5、 `HTTP Server` 出现 `context cancel` 报错 从框架 `v2.5` 版本开始,框架的 `HTTP Server` 的 `Request` 对象将会直接继承与标准库的 `http.Request` 对象, 其中就包括其中的 `context` 上下文对象。当客户端例如浏览器、 `HTTP Client` 取消请求时, 服务端会接收到 `context cancel` 操作( `context.Done`),但是服务端并不会直接报出 `context cancel` 的错误。 这种错误往往在业务逻辑调用了底层的数据库、消息组件等组件时,由这些组件识别到 `context cancel` 操作, 将会停止执行并往上抛出 `context cancel` 错误提醒上层已经终止执行。 这是符合标准库设计的行为,客户端终止请求后,服务端也没有继续执行下去的必要。 [服务端频繁出现contextcancel错误](./web/faq.md) ## 五、环境相关 ### 1、 `Linux` 下执行 `go build main.go` 提示连接超时 `connection timed out` ```bash go: github.com/gogf/gf@v1.14.6-0.20201214132204-c685876e6f67: Get "https://proxy.golang.org/github.com/gogf/gf/@v/v1.14.6-0.20201214132204-c685876e6f67.mod": dial tcp 172.217.160.113:443: connect: connection timed out ``` 解决办法: ```bash export GO111MODULE=on export GOPROXY=https://goproxy.cn ``` 具体请看: - [Go Module](./install-go/go-module.md) - [https://goproxy.cn](https://goproxy.cn) ### 2、 `Linux` 下安装 `gf` 提示命令不存在 `command not found` ```bash ./gf install 安装后 执行gf -v 提示gf: command not found 且/usr/bin目录下并没有gf文件 解决方法: 拷贝sh文件到 /usr/bin目录 cp gf /usr/bin 然后执行 gf -v 就会看到 GoFrame CLI Tool v1.15.4, https://goframe.org Install Path: /bin/gf Build Detail: Go Version: go1.16.2 GF Version: v1.15.3 Git Commit: 22011e76dc3e14006936164cc89e2d4c9190a36d Build Time: 2021-03-30 15:43:22 ``` ### 3、 `Win10` 提示 `gf` 命令不存在 解决办法:安装 `gf.exe` 参考: [开发工具](./cli.md)