日常开发中,我们经常需要快速查看或查询 JSON 数据——API 返回的响应、配置文件、日志中的 JSON 片段。虽然 jq 很强大,但并不是每台机器都装了它。今天我们用 OpenCode,4 分钟写一个纯 Go 标准库的 JSON 查询格式化工具,支持美化输出、压缩输出、以及点号路径查询(包括数组索引),只有一个二进制文件,零依赖,随处可用。
我对 OpenCode 说:
> 帮我用 Go 写一个命令行 JSON 格式化工具,从标准输入读取 JSON 并美化输出,使用 encoding/json 的 json.Indent。同时解析 flag 参数 -c 控制是否压缩输出。
OpenCode 几秒钟就生成了完整的 main.go,包含 flag 解析、stdin 读取和格式化逻辑。这里有个细节很贴心——它直接用 json.Indent 而不是 json.MarshalIndent,因为输入本身就是有效 JSON,不需要重新序列化。
运行 go mod init jsonq && go build,然后试一下:
echo '{"name":"张三","age":28,"skills":["Go","Python"]}' | ./jsonq
输出:
{
"age": 28,
"name": "张三",
"skills": [
"Go",
"Python"
]
}
加上 -c 就是压缩输出,非常适合在管道中传递。
我对 OpenCode 说:
> 加上 -f 参数,支持从文件读取 JSON。如果同时提供了 -f 和 stdin,优先使用 -f。
OpenCode 很聪明地加上了 os.ReadFile,并用条件分支处理了两种输入源。它甚至考虑到了"管道中可能没有数据"的边界情况——虽然我们不需要处理得那么复杂,但能看出来它的代码意识很好。
改动量很小,就加了 5 行,编译通过,功能立刻生效:
./jsonq -f data.json
我对 OpenCode 说:
> 添加 -q 参数,支持用点号分隔的键路径查询 JSON 中的嵌套字段。比如 -q person.address.city 会提取那个路径下的值。如果路径不存在,输出 "null"。
这里是最有趣的部分。OpenCode 生成了一个递归遍历 map[string]interface{} 的函数,逐层查找。我看了下代码,逻辑清晰:
按 . 分割路径
逐层断言为 map[string]interface{}
找不到就返回 nil
来测试一下复杂 JSON:
echo '{"user":{"profile":{"name":"李四","scores":[85,92,78]}}}' | ./jsonq -q user.profile.name
输出:"李四"
我对 OpenCode 说:
> 查询路径也要支持数组索引,比如 user.profile.scores.0 应该能取出数组的第一个元素。
OpenCode 只改了一个地方——在 queryJSON 函数中加了数组索引的判断。它用 strconv.Atoi 把字符串键转成数字,如果能转成功且当前值是数组,就按索引取值。改动不超过 10 行,功能却很实用:
echo '{"data":{"items":[{"id":1,"title":"Go入门"},{"id":2,"title":"Rust实战"}]}}' | ./jsonq -q data.items.0.title
输出:"Go入门"
到这里,一个功能完整的 JSON 查询格式化工具就完成了。
package main
import (
"encoding/json"
"flag"
"fmt"
"io"
"os"
"strconv"
"strings"
)
var (
compact bool
filePath string
query string
)
func main() {
flag.BoolVar(&compact, "c", false, "compact output")
flag.StringVar(&filePath, "f", "", "input JSON file")
flag.StringVar(&query, "q", "", "query key path (e.g., user.name or items.0.title)")
flag.Parse()
var raw []byte
if filePath != "" {
raw, _ = os.ReadFile(filePath)
} else {
raw, _ = io.ReadAll(os.Stdin)
}
var data interface{}
if err := json.Unmarshal(raw, &data); err != nil {
fmt.Fprintf(os.Stderr, "parse error: %v\n", err)
os.Exit(1)
}
if query != "" {
result := getPath(data, query)
printJSON(result)
return
}
printJSON(data)
}
func printJSON(v interface{}) {
var out []byte
if compact {
out, _ = json.Marshal(v)
} else {
out, _ = json.MarshalIndent(v, "", " ")
}
fmt.Println(string(out))
}
func getPath(data interface{}, path string) interface{} {
for _, key := range strings.Split(path, ".") {
switch v := data.(type) {
case map[string]interface{}:
val, ok := v[key]
if !ok {
return nil
}
data = val
case []interface{}:
idx, err := strconv.Atoi(key)
if err != nil || idx < 0 || idx >= len(v) {
return nil
}
data = v[idx]
default:
return nil
}
}
return data
}
整个开发过程大约 4 分钟,95 行代码,零外部依赖。用 OpenCode 开发这种小工具的感觉是:你只需要"描述需求",它负责"翻译成代码"。大部分时间花在测试和微调参数上,而不是写代码本身。
和纯手工写相比,效率提升明显——省去了查标准库 API、处理边界情况、写模板代码的功夫。而且 OpenCode 生成的代码可读性不错,不是那种"能跑就行"的风格,变量命名和代码结构都算讲究。
如果你日常会和各种 JSON 打交道,不妨现在就打开终端,用 OpenCode 试试看。