Go“冷门”库之ohler55/ojg,兼容性更好持续维护的JSONPath库

现在用 Go 写业务的越来越多了,虽然没有干倒 JAVA,但抢占了很多 PHP、Node、Python Web 的份额。不过由于 Go 相对来说时间较短,在生态上差一点,有些场景一下找不到现成的库。因此我打算出个Go“冷门”库系列,介绍一些项目中实际验证过的、但鲜为人知的库。期望更多人了解到这些库,使用后多给开源社区反馈。

在 JAVA 时代,XmlPath 特别流行,对应的就有 JSONPath,方便直接获取 JSON 中的值。在 Go 里有个特别出名的库 gjson 就是这个用处,但它没有按 JSONPath 规范实现,单独用没有问题,但要和别的模块(非Go)交互时就得使用大家都有的。json-path-comparison 上倒是有几款 Go 的库,但很多都年久失修,就剩 ohler55/ojgspyzhov/ajson 还在维护,当时选择了 ohler55/ojg,因为作者一直在做JSON相关的库,有成熟的 Ruby、C 的实现,看着经验非常丰富。

ojg 这个库,不仅仅只有 JSONPath 的功能还包含常规 JSON 库的 Marshal/Unmarshal,但这个赛道太卷了,后面有机会再介绍。这里主要介绍其 JSONPath 功能。使用上也很简单:

oj.ParseString 是将 JSON 字符串解析为 Go 内置结构,等价于 Unmarshal 为 any(你用其它库进行这一步也是可以的)。但和一般 JSON 库默认的行为不一样,会判断类型,特别是 int64(其它库默认是float64),转换为对应的数据类型:

jp.ParseString 则是生成提取规则(可以重复使用),应用于前面解析的 any 上,进行提取。用法可以说是非常简单。

特别的,按 JSONPath 设计,返回的结果是个数组,即使明确提取出来的是一个值,就像上面的例子,直觉的结果应该是4,不过所有库都这样。但如果就想返回4呢?不能直接取[0],因为JSONPath是支持切片操作的,list[0:1]仍然需要返回一个list。那如何判断了,好在 jp.ParseString 的规则有类型,可以判断最后一步操作是不是切片等。

另外,因为 JSONPath 设计比较粗糙或者说很多细节不清晰,所有很多库可能会不一致,比如我提的这个 issue,JSON key 中有 - 就必须用[]方式(["foo-bar"])提取了,不能用 .foo-bar。

所以相对 JSONPath 我更推荐 jmespath,它除了功能更强大,另外一个优点就是规范定义比较清晰,各个语言的库都是官方主导实现,这样更方便跨语言交互。不过 Go 的实现推荐使用 jmespath-community/go-jmespath 版本,还在持续维护。

N天学会Go语言

之前一直有了解 Go 语言,但没系统的学习,最近兴趣盎然生计所迫,得学一学了。整体来说 Go 语法很简洁,有其它语言基础的话学起来很快的。本文记录了一些学习笔记,加深理解。

基础语法

学语言,最重要的就是官方教程(https://go.dev/tour/list),建议不要一开始就跟着杂七杂八的教程学习。

变量

我们都知道 Go 语言是强类型的语言,所以定义变量的时候需要定义其类型,但像 C、Java,都是把类型放在前面:

咋一看有些别扭,但这有讲究的,比如符合英语系自然语言习惯,还有写起来就很方便,更突出变量名而不是类型(而且类型可以省略),官方博客有更多的介绍:Go's Declaration Syntax。还有,Go 和 Python一样,不需要写分号。

也还有简写的方式,因为定义变量的时候其实类型是确定的,编译器很容易推导,可以不用写类型。

看到var i = 1是不是有 JS 和 Java lombok/JDK10 的感觉,这样就很亲切了。

Methods are functions

一个函数定义:func add(x int, y int) int {},参数类型,返回值类型一目了然,这里可以看出还是类型后置,这点其实后面 Python 3.5 加入类型提示(限制)也是这样:def add(x: int, y: int) -> int:。这里顺带提一下,Python、PHP 这些无类型声明的语言并不是真的无类型(动态类型,运行时检查),而且新版都增加了类型检查的语法,解决了重构等痛点问题。

方法和函数其实一样,只是接收方参数不一样。

不过 Go 的函数没有 Python 的那么强大,没有默认参数、关键字参数等,虽然说 Go 讲究简洁,但这些语法糖还是很有用的,典型场景就是 Python requests 库:https://docs.python-requests.org/en/latest/api/#requests.request

Defer

延迟调用,这个比较特殊,defer定义的操作会在方法 return 后执行,但比如传的参数就是定义时那样,官方例子修改下:

输出

这个有什么作用呢,其实类似 Java 中 try/catch 中的 finally,可以用于资源的清理(经典应用就是释放锁)。官方博客有介绍:Defer, Panic, and Recover

Pointers

Go 的指针语法上来看,比 C 简单些,没有特殊的指针变量,&就是取地址,*就是取值(或者定义指针类型),也没有指针运算(其实也可以)。说到指针,又会牵扯到一个话题,传值还是传引用,简单来说 Go 传的是值,但并不代表改动不影响原数据,因为 map、slice等,可以理解本身数据结构就是指针,这些类型修改会影响原有数据,表现和其它语言一致。还有一个就是性能问题,本能的觉得传引用(指针)性能更高(其实还会带来 GC 等问题),比如字符串。

输出为:

咋一看 s2, s3 都是一个新的内存地址,这不影响性能嘛。但再看看:

输出:

可以看出 Go 对字符串等有两个优化:

1. 定义变量 literals(字面量)一致时(s1 和 s2),会指向同一个地址。

2. 变量传值时(s3 := s1)复制的是 string 这个数据结构。

data 指针指向的内容没有复制。因为要存储这个新的结构体,所以 &s3 是这个结构体的内存地址,而 s1,s3 结构体中 data 指向的地址是一样的。

指针这块细节可能理解不是太准确,但意思是语言层面已经做了很多优化,正常使用即可,不要过度优化到处使用指针。 Continue Reading...