ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

OpenCloud 中的 GJSON Path 语法完全指南:从基础取值到 Multipath 与自定义 Modifier

2026/9/18 23:07:44 拓冰建站 浏览量
OpenCloud 中的 GJSON Path 语法完全指南:从基础取值到 Multipath 与自定义 Modifier OpenCloud 中的 GJSON Path 语法完全指南从基础取值到 Multipath 与自定义 Modifier【免费下载链接】opencloud️ OpenCloud is the open source platform for file management, sharing and collaboration. Simple and sovereign.项目地址: https://gitcode.com/GitHub_Trending/op/opencloudGJSON Path 是 Go 生态中一款轻量、零依赖的 JSON 取值查询语法它把从 JSON 里快速取出想要的值压缩成一行字符串表达式。本文以 OpenCloud 仓库内置的vendor/github.com/tidwall/gjson/SYNTAX.md为骨架结合 GJSON v1.19.0见 go.mod的源码实现系统讲解 Path 的组成结构、通配符、转义、数组查询、.与|的语义差异、内置 Modifier、Multipath 与 JSON Literals并给出 OpenCloud 各服务中 GJSON 的真实用法作为佐证。什么是 GJSON PathGJSON Path 是一段纯文本字符串语法用来描述一种搜索模式从而从一段 JSON 载荷中快速取回目标值。它不需要先反序列化成 Go struct而是直接在原始 JSON 文本上做扫描匹配因此特别适合拿原始字节、取一个字段就走的场景。从 gjson.go 的公开函数列表可以看到核心入口是Get(json, path string) Resultgjson.go解析字符串并执行路径查询GetBytes(json []byte, path string) Resultgjson.go直接处理[]byte避免字符串拷贝Parse/ParseBytesgjson.go先解析出Result再在结果上多次调用.Get(path)Valid/ValidBytesgjson.go只做合法性校验。查询结果统一封装为Result类型gjson.go 起提供String()、Bool()、Int()、Float()、Array()、Map()、Exists()、Raw等便捷方法。注意Result.Raw返回的是原始 JSON 片段而String()对字符串值会额外做一次 JSON 反转义——在需要拿原始字节做二次处理的场景下Raw更常用。Path 的基本结构一个 GJSON Path 由若干组件组成组件之间用.分隔。除.之外以下字符都有特殊含义| # \ * ! ?其中|是另一种分隔符、#用于数组定位与查询、用于 Modifier、\用于转义、*与?是通配符、!用于 JSON Literals 声明。下面以文档中的标准示例 JSON 贯穿全文{ name: {first: Tom, last: Anderson}, age:37, children: [Sara,Alex,Jack], fav.movie: Deer Hunter, friends: [ {first: Dale, last: Murphy, age: 44, nets: [ig, fb, tw]}, {first: Roger, last: Craig, age: 68, nets: [fb, tw]}, {first: Jane, last: Murphy, age: 47, nets: [ig, tw]} ] }基础取值对象键与数组下标绝大多数场景只需要按对象名或数组下标取值name.last Anderson name.first Tom age 37 children [Sara,Alex,Jack] children.0 Sara children.1 Alex friends.1 {first: Roger, last: Craig, age: 68} friends.1.first Roger对象键、数字下标、多层嵌套都可以直接连写无需任何额外符号。GJSON 在源码中分别用parseObjectPathgjson.go解析对象键路径、用parseArrayPathgjson.go解析数组路径含#与查询二者会在查询过程中被交替调用。通配符*与?键名里可以出现通配符字符*匹配任意0 个或更多字符?匹配恰好 1 个字符。child*.2 Jack c?ildren.0 Sarachild*匹配children再取下标 2 得到Jackc?ildren中?占住h的位置同样命中children。通配符的实际字符级匹配由matchLimitgjson.go实现底层复用同仓库 vendor/github.com/tidwall/match 的匹配逻辑。转义字符\键名本身可能包含特殊字符比如点号、星号、问号此时必须用\转义。示例 JSON 中fav.movie是一个完整的键名直接写fav.movie会被拆成两层路径因此必须写成fav\.movie Deer Hunter在 Go / Rust 源码里硬编码路径时还要注意字符串字面量本身对\的处理// Go val : gjson.Get(json, fav\\.movie) // must escape the slash val : gjson.Get(json, fav\.movie) // no need to escape the slash// Rust let val gjson::get(json, fav\\.movie) // must escape the slash let val gjson::get(json, r#fav\.movie#) // no need to escape the slashGo 的双引号字符串里\\才是真正的反斜杠因此需要写两层使用反引号原始字符串或 Rust 的r#...#原始字符串则只需一层推荐后者以降低出错概率。数组#的三种用法#字符用来钻进 JSON 数组求长度单独一个#跟在数组名后返回数组长度整数值批量取字段#.field会把数组每个元素的field提取出来拼成一个新数组查询过滤#(...)取第一个匹配项#(...)#取所有匹配项见下一节。friends.# 3 friends.#.age [44,68,47]friends.#返回 3 个元素friends.#.age依次取出三个好友的age组成[44,68,47]。这在 OpenCloud 的搜索索引一致性检查里是常见的取子集再比较模式见下文源码佐证。查询#(...)与#(...)#查询语法允许按条件过滤数组元素#(cond)返回第一个匹配元素#(cond)#返回所有匹配元素组成的数组。支持比较运算符、!、、、、以及模式匹配运算符%like和!%not likefriends.#(lastMurphy).first Dale friends.#(lastMurphy)#.first [Dale,Jane] friends.#(age45)#.last [Craig,Murphy] friends.#(first%D*).last Murphy friends.#(first!%D*).last Craig前两行演示首匹配 vs 全匹配lastMurphy匹配 Dale 和 Jane#(...)取到第一个Dale#(...)#把两人都取出来%与!%使用*/?风格的简单通配模式first%D*匹配以D开头的名字first!%D*则排除它们。对数组里的非对象值如纯字符串数组查询时运算符右侧可以留空直接对元素本身做比较children.#(!%*a*) Alex children.#(%*a*)# [Sara,Jack]这里%*a*匹配名字里含字母a的元素Sara、Jack!%*a*取第一个不含a的元素Alex。查询还允许嵌套friends.#(nets.#(fb))#.first [Dale,Roger]含义是先对每个friends元素在其nets数组里查询是否存在等于fb的元素然后取满足条件的好友的first字段结果为 Dale 与 Roger。兼容性提醒在 v1.3.0 之前查询语法使用#[...]方括号形式。v1.3.0 为了不与新增的 Multipath 语法冲突而改为#(...)旧写法出于向后兼容会继续工作直到下一个大版本才可能移除。新代码请一律使用#(...)。波浪号运算符~把值转成布尔再比较~tilde运算符会在比较前把值转换成布尔语义支持四种比较形式~true Converts true-ish values to true ~false Converts false-ish and non-existent values to true ~null Converts null and non-existent values to true ~* Converts any existing value to true语义细节b~trueb 的值是真值true-ish才成立b~falseb 是假值false-ish或不存在都成立b~nullb 为null或不存在成立b~*只要 b存在即成立b!~*b不存在才成立。GJSON 源码中trueish、falseish、nullishgjson.go分别实现了这三类判定queryMatchesgjson.go把它们统一接入查询求值。判定规则是宽松的例如字符串1、true、数字1都算 true-ish0、0、false、null以及缺失字段都算 false-ish。用下面这份测试 JSON 来验证{ vals: [ { a: 1, b: data }, { a: 2, b: true }, { a: 3, b: false }, { a: 4, b: 0 }, { a: 5, b: 0 }, { a: 6, b: 1 }, { a: 7, b: 1 }, { a: 8, b: true }, { a: 9, b: false }, { a: 10, b: null }, { a: 11 } ] }查询所有真值和假值vals.#(b~true)#.a [2,6,7,8] vals.#(b~false)#.a [3,4,5,9,10,11]注意b~false把{ a: 11 }字段缺失也算进去了因为不存在的值被当作false。再区分null与显式存在vals.#(b~null)#.a [10,11] vals.#(b~*)#.a [1,2,3,4,5,6,7,8,9,10] vals.#(b!~*)#.a [11]~null同时命中null值和缺失字段~*要求字段存在命中了 110缺字段的 11 被排除!~*则只命中缺失字段的 11。Dot vs Pipe.与|的区别.是标准分隔符|也可以用作分隔符绝大多数情况下两者结果一致。真正的差异出现在|用于#数组长度以及查询#(...)#之后的场景此时.是对数组的每个元素应用后续路径而|是对前一步整体结果应用后续路径。对照示例friends.0.first Dale friends|0.first Dale friends.0|first Dale friends|0|first Dale friends|# 3 friends.# 3 friends.#(lastMurphy)# [{first: Dale, last: Murphy, age: 44},{first: Jane, last: Murphy, age: 47}] friends.#(lastMurphy)#.first [Dale,Jane] friends.#(lastMurphy)#|first non-existent friends.#(lastMurphy)#.0 [] friends.#(lastMurphy)#|0 {first: Dale, last: Murphy, age: 44} friends.#(lastMurphy)#.# [] friends.#(lastMurphy)#|# 2逐一拆解关键几行friends.#(lastMurphy)#单独执行结果是一个由两个对象组成的数组[{first: Dale, last: Murphy, age: 44},{first: Jane, last: Murphy, age: 47}].first后缀先对查询结果这个数组的每个元素应用first得到[Dale,Jane]|first后缀对前一步的整体结果一个数组应用first——数组不是对象没有first字段因此结果是non-existent|0后缀对前一步结果数组取下标 0得到{first: Dale, last: Murphy, age: 44}|#后缀对前一步结果数组取长度得到 2。简言之.是逐元素投影|是整体管道。splitPossiblePipegjson.go在源码层面负责把含|的路径拆成左右两段左右两段分别求值后把右段作用在左段的结果上这解释了上面的全部差异。Modifier内置与自定义Modifier 是对 JSON 做自定义处理的路径组件写法是name或name:arg。比如内置的reverse可以把children数组倒序children.reverse [Jack,Alex,Sara] children.reverse.0 Jack当前 GJSON 提供以下内置 ModifierModifier作用reverse反转数组或对象的成员顺序ugly去掉 JSON 中所有空白pretty让 JSON 更易读可加参数this返回当前元素可用于取回根元素valid校验 JSON 文档是否合法flatten把嵌套数组展平join把多个对象合并成一个对象keys返回对象的键组成的数组values返回对象的值组成的数组tostr把 JSON 值包装成 JSON 字符串fromstr把 JSON 字符串解包成值group对对象数组按字段分组dig无需写出完整路径即可搜索值源码中execModifiergjson.go是 Modifier 的分发入口内置列表注册在init()gjson.go中例如reverse、ugly、pretty、this、valid、flatten、join、keys、values、tostr、fromstr等。pretty的底层排版实现来自同仓库的 vendor/github.com/tidwall/pretty。Modifier 参数Modifier 可以接收可选参数参数可以是合法 JSON 或普通字符。pretty接受一个 JSON 对象作为参数pretty:{sortKeys:true}效果是美化输出并且按字典序排序所有键{ age:37, children: [Sara,Alex,Jack], fav.movie: Deer Hunter, friends: [ {age: 44, first: Dale, last: Murphy}, {age: 68, first: Roger, last: Craig}, {age: 47, first: Jane, last: Murphy} ], name: {first: Tom, last: Anderson} }pretty支持的完整选项为sortKeys排序键、indent缩进字符、prefix行前缀、width换行宽度底层由modPrettygjson.go与pretty包配合实现。自定义 Modifier通过gjson.AddModifier(name, fn)可以注册自己的 Modifier回调签名是func(json, arg string) string入参是当前 JSON 片段与参数返回值是处理后的 JSON 片段。下面的例子注册了一个把 JSON 内容整体转大写/小写的caseModifiergjson.AddModifier(case, func(json, arg string) string { if arg upper { return strings.ToUpper(json) } if arg lower { return strings.ToLower(json) } return json }) children.case:upper [SARA,ALEX,JACK] children.case:lower.reverse [jack,alex,sara]注册函数AddModifier位于 gjson.go它把名字写入全局 modifier 表之后所有路径解析都会命中该表。注意自定义 Modifier 目前仅 Go 版可用Rust 版尚未提供该能力。Multipath 多路径一次查询拼出新文档从 v1.3.0 起GJSON 支持把多条路径拼接成一个新文档。把逗号分隔的路径用[...]包裹得到新数组用{...}包裹得到新对象。例如{name.first,age,the_murphys:friends.#(lastMurphy)#.first}这条路径同时选了name.first名字、age年龄以及姓氏为 Murphy 的好友们的 first 列表{first:Tom,age:37,the_murphys:[Dale,Jane]}命名规则路径前可以用key:显式指定结果键如the_murphys不指定时沿用源字段名如first无法确定字段名时使用_占位。GJSON 的 v1.3.0 版本正是因为引入了{...}/[...]这一 Multipath 语法才把旧查询语法从#[...]迁移到#(...)避免方括号歧义。Literals用!构造静态 JSON 片段从 v1.12.0 起GJSON 支持 JSON Literals以!开头的声明字符后跟一段静态 JSON用于在 Multipath 中构造常量块很适合从源数据里挑几个字段 追加几个固定字段的场景。例如{name.first,age,company:!Happysoft,employed:!true}取到name.first与age之后再追加company字符串常量与employed布尔常量{first:Tom,age:37,company:Happysoft,employed:true}字符串字面量必须用双引号包裹并带!前缀如!Happysoft裸值如!true、!123、!null可直接使用。OpenCloud 中的 GJSON 实际用法GJSON 在 OpenCloud 中被广泛用于以原始 JSON 文本做快速检索/对比的场景几个可查证的例子搜索索引一致性检查services/search/pkg/opensearch/index.go在 OpenSearch 索引映射的对账逻辑中本地与远端索引的映射 JSON 分别用gjson.ParseBytes(localIndexB)/gjson.ParseBytes(remoteIndexB)解析为gjson.Result随后通过r.local.Get(settings.analysis).Raw、r.remote.Get(settings.index.analysis).Rawindex.go以及r.local.Get(mappings.properties).Raw、r.remote.Get(mappings).Rawindex.go等路径直接提取子片段。这正是Result.Get()与Result.Raw的典型用法先解析一次多次按路径取值拿到原始 JSON 片段做深度比较或原样回传。代码注释还点明了一个易踩坑的细节——gjson path 为空字符串时表示未命中两个未设置的值会被判定为相等index.go因此对账逻辑必须先处理空串情况再比较。Web 主题服务测试services/web/pkg/theme/service_test.go响应体w.Body.String()直接交给gjson.Parse(...)后按路径断言避免为一次测试专门定义解析 struct。Graph 服务与搜索索引测试如 services/graph/pkg/service/v0/graph_test.go、services/search/pkg/opensearch/index_test.go同样依赖 GJSON 在测试中断言 JSON 响应的子字段。这些用例共同说明当数据以[]byte或字符串形式存在、且你只关心其中几个字段时GJSON Path 是比全量反序列化 struct 绑定更轻量的选择——这正是 OpenCloud 引入该依赖github.com/tidwall/gjson v1.19.0见 go.mod的动机所在。速查表语法含义示例a.b.c按对象键逐层取值name.first→Toma.0数组下标children.0→Saraa.*/a.?通配任意长度 / 单个字符child*.2→Jacka\.b转义特殊字符fav\.movie→Deer Hunterarr.#数组长度friends.#→3arr.#.k提取每元素的k组成数组friends.#.age→[44,68,47]arr.#(cond)/#(cond)#第一个匹配 / 全部匹配friends.#(age45)#.last%/!%like / not like 模式匹配first%D*~true等转布尔后比较b~nullarr.#(c)#.k查询后逐元素投影.得到[Dale,Jane]arr.#(c)#\|k查询后对整体结果取路径\|\|0取第一个对象reverse等内置 Modifierchildren.reversepretty:{sortKeys:true}带参数的 Modifier美化 排序键{p1,p2,k:p3}Multipath 组装新对象见上文示例!str/!trueJSON Literals 静态值company:!Happysoft掌握这套语法后无论你是要在 OpenCloud 源码里阅读搜索索引对账逻辑还是在自己的 Go 服务中做零依赖的 JSON 字段抽取都可以直接用一行 Path 表达式完成任务再结合自定义 Modifier 与 MultipathGJSON 甚至可以承担轻量 JSON 转换管道的职责。【免费下载链接】opencloud️ OpenCloud is the open source platform for file management, sharing and collaboration. Simple and sovereign.项目地址: https://gitcode.com/GitHub_Trending/op/opencloud创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考