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

项目介绍

很多开发者在终端里处理 JSON 时都装过 jq,但 jq 的查询语法并不直观。今天我们用 OpenCode 4 分钟写一个更轻量的命令行 JSON 工具——支持格式化美化路径查询语法校验三种模式,最终代码约 120 行,零外部依赖。

技术栈:Go 1.18+,纯标准库实现。

最终效果:

# 格式化 JSON
cat data.json | jsonx fmt

# 按路径取值
cat data.json | jsonx get users.0.name

# 校验 JSON 语法
cat data.json | jsonx validate

准备工作

  • 安装 Go 1.18 以上版本
  • 安装 OpenCode(npm i -g @opencode/cli 或前往 opencode.ai 获取)
  • 一个空的 Go 项目目录

实践过程

第一步:初始化 Go 模块

我对 OpenCode 说:

> 在当前目录初始化一个 Go 模块,module 名为 jsonx

OpenCode 执行了 go mod init jsonxgo.mod 文件自动生成。

第二步:创建主程序骨架

我对 OpenCode 说:

> 创建 main.go,实现一个命令行工具 jsonx,支持三个子命令:fmt(格式化 JSON)、get(按点号分隔路径查询 JSON 值)、validate(校验 JSON 语法)。输入来自 stdin。用 flag 包解析子命令。

这个提示词描述了完整的程序结构。OpenCode 生成了以下骨架:

package main

import (
	"encoding/json"
	"flag"
	"fmt"
	"io"
	"os"
	"strings"
)

func main() {
	flag.Parse()
	args := flag.Args()
	if len(args) == 0 {
		fmt.Println("Usage: jsonx <fmt|get|validate> [path]")
		return
	}

	data, _ := io.ReadAll(os.Stdin)

	switch args[0] {
	case "fmt":
		format(data)
	case "get":
		if len(args) < 2 {
			fmt.Println("get requires a path")
			return
		}
		queryGet(data, args[1])
	case "validate":
		validate(data)
	}
}

关键点:我一次性描述了三个子命令的职责,OpenCode 准确理解了"子命令 + 参数"的 CLI 模式,并生成了对应的 switch 分支。

第三步:实现格式化功能

我对 OpenCode 说:

> 实现 format 函数:将 JSON 字节解析后重新用 json.MarshalIndent 输出,缩进两个空格。如果 JSON 无效则输出错误信息。

遇到 JSON 解析失败时,OpenCode 会自动意识到需要输出可读的错误信息,而不是 panic。生成结果:

func format(data []byte) {
	var v interface{}
	if err := json.Unmarshal(data, &v); err != nil {
		fmt.Fprintln(os.Stderr, "invalid JSON:", err)
		os.Exit(1)
	}
	out, _ := json.MarshalIndent(v, "", "  ")
	fmt.Println(string(out))
}

第四步:实现路径查询功能

我对 OpenCode 说:

> 实现 queryGet 函数:用 strings.Split(path, ".") 分割路径,逐层访问 JSON。支持数组索引(把 "0"、"1" 等纯数字路径段转换成 int)。如果路径不存在则返回 "null"。

这是最复杂的部分。OpenCode 需要理解 JSON 的嵌套结构遍历逻辑。它生成了:

func queryGet(data []byte, path string) {
	var v interface{}
	json.Unmarshal(data, &v)
	parts := strings.Split(path, ".")
	for _, part := range parts {
		switch val := v.(type) {
		case map[string]interface{}:
			var ok bool
			v, ok = val[part]
			if !ok {
				fmt.Println("null")
				return
			}
		case []interface{}:
			idx := 0
			fmt.Sscanf(part, "%d", &idx)
			if idx < 0 || idx >= len(val) {
				fmt.Println("null")
				return
			}
			v = val[idx]
		default:
			fmt.Println("null")
			return
		}
	}
	out, _ := json.MarshalIndent(v, "", "  ")
	fmt.Println(string(out))
}

这里展示了一个关键能力:我只说了"支持数组索引",OpenCode 就自动实现了用 fmt.Sscanf 把字符串转成 int,并在 []interface{} 分支中按索引取值。边界处理(index 越界返回 null)也是它自己加的。

第五步:实现校验功能

我对 OpenCode 说:

> 实现 validate 函数:校验 JSON 是否合法,合法输出 "valid JSON",不合法输出具体错误信息。

OpenCode 生成了一个简洁的校验函数:

func validate(data []byte) {
	var v interface{}
	if err := json.Unmarshal(data, &v); err != nil {
		fmt.Fprintln(os.Stderr, "invalid JSON:", err)
		os.Exit(1)
	}
	fmt.Println("valid JSON")
}

收尾编译与测试

我对 OpenCode 说:

> 编译并测试:用 echo 构造一段 JSON 分别测试 fmt、get、validate 三个命令

OpenCode 依次运行:

go build -o jsonx.exe .

echo '{"users":[{"name":"Alice"},{"name":"Bob"}]}' | ./jsonx.exe fmt
echo '{"users":[{"name":"Alice"},{"name":"Bob"}]}' | ./jsonx.exe get users.1.name
echo '{"users":[{"name":"Alice"},{"name":"Bob"}]}' | ./jsonx.exe validate

全部输出正确。

完整代码

package main

import (
	"encoding/json"
	"flag"
	"fmt"
	"io"
	"os"
	"strings"
)

func main() {
	flag.Parse()
	args := flag.Args()
	if len(args) == 0 {
		fmt.Fprintln(os.Stderr, "Usage: jsonx <fmt|get|validate> [path]")
		fmt.Fprintln(os.Stderr, "  fmt      - pretty-print JSON")
		fmt.Fprintln(os.Stderr, "  get PATH - extract value by dot-path, e.g. users.0.name")
		fmt.Fprintln(os.Stderr, "  validate - check if input is valid JSON")
		os.Exit(1)
	}

	data, err := io.ReadAll(os.Stdin)
	if err != nil {
		fmt.Fprintln(os.Stderr, "read stdin:", err)
		os.Exit(1)
	}

	switch args[0] {
	case "fmt":
		format(data)
	case "get":
		if len(args) < 2 {
			fmt.Fprintln(os.Stderr, "get requires a path argument")
			os.Exit(1)
		}
		queryGet(data, args[1])
	case "validate":
		validate(data)
	default:
		fmt.Fprintf(os.Stderr, "unknown command: %s\n", args[0])
		os.Exit(1)
	}
}

func format(data []byte) {
	var v any
	if err := json.Unmarshal(data, &v); err != nil {
		fmt.Fprintln(os.Stderr, "invalid JSON:", err)
		os.Exit(1)
	}
	out, _ := json.MarshalIndent(v, "", "  ")
	fmt.Println(string(out))
}

func queryGet(data []byte, path string) {
	var v any
	if err := json.Unmarshal(data, &v); err != nil {
		fmt.Fprintln(os.Stderr, "invalid JSON:", err)
		os.Exit(1)
	}

	parts := strings.Split(path, ".")
	for _, part := range parts {
		switch val := v.(type) {
		case map[string]any:
			var ok bool
			v, ok = val[part]
			if !ok {
				fmt.Println("null")
				return
			}
		case []any:
			idx := 0
			fmt.Sscanf(part, "%d", &idx)
			if idx < 0 || idx >= len(val) {
				fmt.Println("null")
				return
			}
			v = val[idx]
		default:
			fmt.Println("null")
			return
		}
	}

	out, _ := json.MarshalIndent(v, "", "  ")
	fmt.Println(string(out))
}

func validate(data []byte) {
	var v any
	if err := json.Unmarshal(data, &v); err != nil {
		fmt.Fprintln(os.Stderr, "invalid JSON:", err)
		os.Exit(1)
	}
	fmt.Println("valid JSON")
}

小结

这个约 120 行的工具从零到可运行,与 OpenCode 对话不超过 10 个来回,耗时约 4 分钟。真正有价值的地方在于:

  • 不需要自己查 Go 文档json.Unmarshal 的类型断言逻辑、fmt.Sscanf 的用法 OpenCode 都直接生成。
  • 边界情况自动处理:数组越界、路径不存在、非法 JSON 输入,OpenCode 比手写时还细心。
  • 描述即代码:只要你把"三个子命令做什么"说清楚,OpenCode 就能产出正确结构。

如果你每天和 API、配置文件、日志里的 JSON 打交道,花 4 分钟给自己造个趁手工具,比折腾 jq 语法划算得多。