OpenCode 实践课:4 分钟用 Go 写出一个命令行 JSON 查询格式化工具

项目介绍

日常开发中,我们经常需要快速查看或查询 JSON 数据——API 返回的响应、配置文件、日志中的 JSON 片段。虽然 jq 很强大,但并不是每台机器都装了它。今天我们用 OpenCode,4 分钟写一个纯 Go 标准库的 JSON 查询格式化工具,支持美化输出、压缩输出、以及点号路径查询(包括数组索引),只有一个二进制文件,零依赖,随处可用。

  • 技术栈:Go + 标准库
  • 核心功能:JSON 美化 / 压缩 / 键路径查询
  • 代码量:约 100 行

准备工作

  • 安装 Go(1.21+)
  • 安装 OpenCode
  • 一个终端

实践过程

第一步:项目骨架 + 基础格式化

我对 OpenCode 说:

> 帮我用 Go 写一个命令行 JSON 格式化工具,从标准输入读取 JSON 并美化输出,使用 encoding/jsonjson.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 试试看。