OpenCode 实践课:4 分钟用 Go 写出一个命令行 YAML 转 JSON 格式转换器

项目介绍

在日常开发中,经常需要在 YAML 和 JSON 两种格式之间来回切换——配置文件是 YAML,API 接口返回的是 JSON。今天我们用 OpenCode 花 4 分钟写一个命令行工具,一行命令就能把 YAML 转成 JSON。

技术栈:Go + gopkg.in/yaml.v3,最终代码约 90 行,支持紧凑输出和自定义输出文件。

准备工作

需要本地安装 Go(1.18+),并初始化项目:

mkdir y2j && cd y2j
go mod init y2j

OpenCode 安装配置请参考 opencode.ai,安装完成后在终端运行 opencode 即可进入交互模式。

实践过程

第一步:创建基础转换功能

打开 OpenCode,输入第一条指令:

> 我对 OpenCode 说:
> "帮我用 Go 写一个 YAML 转 JSON 的命令行工具,读取指定的 YAML 文件,输出格式化的 JSON。使用 gopkg.in/yaml.v3 库。"

OpenCode 先执行 go get gopkg.in/yaml.v3 安装依赖,然后生成 main.go,核心逻辑是 yaml.Unmarshal 解析 YAML,json.MarshalIndent 输出 JSON。代码生成后 OpenCode 自动运行 go build 验证编译。

关键要点:用 OpenCode 开发第三方库项目时,它自动帮你处理依赖安装,不需要手动 go get

第二步:增加命令行参数

基础功能跑通后,继续发号施令:

> 我对 OpenCode 说:
> "加上两个参数:-c 输出紧凑 JSON(不换行不缩进),-o 指定输出文件路径,默认输出到终端。"

OpenCode 引入 flag 标准库,添加了 -c-o 两个 flag,并根据 -output 参数判断是写入文件还是打印到 stdout。修改完成后自动重新编译验证。

关键要点:OpenCode 理解"紧凑 JSON"指的就是 json.Marshal(不带缩进),无需你解释技术细节。

第三步:修复类型转换问题

测试时发现了一个坑——用 anyinterface{})接收解析结果时,YAML 的 map key 会被解析为 map[any]any 而非 map[string]any,导致 JSON 序列化失败。

> 我对 OpenCode 说:
> "运行报错了,json: unsupported type: map[interface {}]interface {},帮我修复这个类型转换问题。"

OpenCode 添加了 convertKeys 递归函数,遍历解析结果并将 map[any]any 转换为 map[string]any,问题解决。

关键要点:把报错信息直接贴给 OpenCode,它能精准定位问题并修复——就像 pair programming 一样自然。

第四步:完善使用体验

最后做个收尾:

> 我对 OpenCode 说:
> "帮我把帮助信息写完整,include 使用示例;另外输出文件末尾要加换行。"

OpenCode 重写了 flag.Usage,添加了清晰的用法说明和示例,并在输出末尾处理了换行符。

至此,一个实用的 YAML 转 JSON 命令行工具就完成了。整个过程没有手写一行代码——全部由 OpenCode 生成。

完整代码

package main

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

	"gopkg.in/yaml.v3"
)

var (
	compact = flag.Bool("c", false, "compact output (no indentation)")
	output  = flag.String("o", "", "write to file instead of stdout")
)

func main() {
	flag.Usage = func() {
		fmt.Fprintf(os.Stderr, "y2j - Convert YAML to JSON\n\n")
		fmt.Fprintf(os.Stderr, "Usage:\n  y2j [options] <input.yaml>\n\n")
		fmt.Fprintf(os.Stderr, "Examples:\n  y2j config.yaml\n")
		fmt.Fprintf(os.Stderr, "  y2j -c config.yaml\n")
		fmt.Fprintf(os.Stderr, "  y2j -o output.json config.yaml\n\n")
		fmt.Fprintf(os.Stderr, "Options:\n")
		flag.PrintDefaults()
	}
	flag.Parse()

	if flag.NArg() < 1 {
		flag.Usage()
		os.Exit(1)
	}

	data, err := os.ReadFile(flag.Arg(0))
	if err != nil {
		fmt.Fprintf(os.Stderr, "read error: %v\n", err)
		os.Exit(1)
	}

	var doc any
	if err := yaml.Unmarshal(data, &doc); err != nil {
		fmt.Fprintf(os.Stderr, "yaml parse error: %v\n", err)
		os.Exit(1)
	}

	doc = convertKeys(doc)

	var jsonBytes []byte
	if *compact {
		jsonBytes, err = json.Marshal(doc)
	} else {
		jsonBytes, err = json.MarshalIndent(doc, "", "  ")
	}
	if err != nil {
		fmt.Fprintf(os.Stderr, "json encode error: %v\n", err)
		os.Exit(1)
	}

	out := os.Stdout
	if *output != "" {
		f, err := os.Create(*output)
		if err != nil {
			fmt.Fprintf(os.Stderr, "write error: %v\n", err)
			os.Exit(1)
		}
		defer f.Close()
		out = f
	}

	result := string(jsonBytes)
	if !strings.HasSuffix(result, "\n") {
		result += "\n"
	}
	fmt.Fprint(out, result)
}

func convertKeys(val any) any {
	switch v := val.(type) {
	case map[string]any:
		m := make(map[string]any, len(v))
		for k, val := range v {
			m[k] = convertKeys(val)
		}
		return m
	case map[any]any:
		m := make(map[string]any, len(v))
		for k, val := range v {
			m[fmt.Sprintf("%v", k)] = convertKeys(val)
		}
		return m
	case []any:
		slice := make([]any, len(v))
		for i, item := range v {
			slice[i] = convertKeys(item)
		}
		return slice
	}
	return val
}

小结

这个小工具从零到可用,全程只用了 4 分钟和 4 条自然语言指令。体会最深的有三点:

依赖管理自动化:OpenCode 识别到第三方库后自动 go get,无需手动操作

报错即修复:遇到运行时错误,把错误信息丢给 OpenCode 就能得到针对性修复

迭代式开发:从基础功能到参数控制到类型处理到体验完善,像和人对话一样逐步打磨

相比传统开发模式——查文档、写代码、调试、改 bug、再调试——用 OpenCode 的开发节奏更像"口述需求 + 验收结果",把精力集中在定义问题和验证输出,而非实现细节。

下次需要快速造个实用轮子,不妨试试用 OpenCode 把想法直接说出来。