OpenCode 实践课:4 分钟用 Go 写出一个命令行 JSON 转结构体工具

项目介绍

在 Go 开发中,对接第三方 API 时经常需要根据 JSON 返回体手写结构体定义——字段一多起来,既枯燥又容易出错。今天我们用 OpenCode 写一个命令行工具 json2go:输入一段 JSON,自动生成对应的 Go struct 代码,嵌套对象自动生成子结构体,数组元素智能推断类型。

  • 语言:Go
  • 代码量:约 100 行
  • 核心功能:stdin 读 JSON → 解析 → 生成 Go 结构体 → 输出到 stdout

最终效果:

$ echo '{"name":"张三","age":28,"tags":["Go","Linux"],"addr":{"city":"深圳"}}' | go run main.go
package main

type Addr struct {
    City string `json:"city"`
}

type Root struct {
    Name string `json:"name"`
    Age  int    `json:"age"`
    Tags []string `json:"tags"`
    Addr Addr   `json:"addr"`
}

准备工作

  • Go 1.18+
  • 已安装 OpenCode(参考 opencode.ai
  • 终端进入工作目录

实践过程

整个过程我会用自然语言指挥 OpenCode 完成开发,我自己一行代码都不写。

第一步:初始化项目

我对 OpenCode 说:

> 帮我创建一个 Go 项目 json2go,go mod 模块名就叫 json2go,新建 main.go 文件,留好 package main 声明。

OpenCode 秒完成:

$ mkdir json2go && cd json2go
$ go mod init json2go

同时生成了 main.go 骨架:

package main

func main() {
}

要点:OpenCode 会自动执行 shell 命令创建目录和 go mod,不需要手动操作。

第二步:实现 JSON 读取与解析

我继续对 OpenCode 说:

> 在 main.go 中实现从 stdin 读取 JSON,用 json.Decoder 解析成 map[string]interface{},解析失败则输出错误并退出。

OpenCode 直接在当前文件中追加了完整逻辑:

import (
    "encoding/json"
    "fmt"
    "os"
)

func main() {
    var data map[string]interface{}
    decoder := json.NewDecoder(os.Stdin)
    if err := decoder.Decode(&data); err != nil {
        fmt.Fprintf(os.Stderr, "JSON 解析失败: %v\n", err)
        os.Exit(1)
    }
}

要点:不需要指定行号或位置,OpenCode 理解上下文,自动把 import 和 main 函数放到正确的位置。

第三步:生成结构体代码

这是核心功能。我对 OpenCode 说:

> 添加一个 generateStruct 函数,接收 map[string]interface{} 和一个结构体名称,遍历 map 的 key 作为字段名(转驼峰),value 根据类型映射到 Go 类型——string→string、float64→int(整数值时)或 float64、bool→bool、nil→interface{},最后返回拼好的 struct 字符串。同时再加一个 toCamelCase 辅助函数做下划线转驼峰。

这是一句比较长的 prompt,但 OpenCode 完全理解了——直接生成了类型推断 + 驼峰命名 + 字符串拼接的完整代码。

关键要点:给 OpenCode 描述需求时,按"输入→处理逻辑→输出"的结构来描述,它能精确理解。

第四步:让 AI 自己搞定嵌套结构和数组

我对 OpenCode 说:

> 刚才的 generateStruct 只能处理平铺字段。现在让 value 为 map[string]interface{} 时递归生成子结构体;value 为 []interface{} 时,如果数组元素是对象则生成子结构体并用 [],如果元素是基础类型则直接用 []string / []int 这样的切片类型。生成的子结构体放在父结构体前面。

OpenCode 自动在代码里加入了递归调用和数组元素判别逻辑,全程无需我操心调用顺序或缩进格式。

要点:不需要事先想好所有边界情况,先跑通核心逻辑,再用对话逐步迭代增强,这是用 AI 编程最舒服的方式。

第五步:加命令行参数

我对 OpenCode 说:

> 用 flag 包加一个 -name 参数,默认值 "Root",让用户指定根结构体的名称。

OpenCode 在 main 函数里补上 flag 声明和解析,然后把 generateStruct 调用里的名字换成了 *name。

就这样,5 轮对话,一个约 100 行的实用工具就写完了。全程零手写代码,只靠自然语言描述需求。

完整代码

以下是聊天窗口"历史记录"自动生成的最终代码——是的,OpenCode 还会自动帮你排版和保存文件。

package main

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

func toCamelCase(s string) string {
	var result strings.Builder
	capitalize := true
	for _, r := range s {
		if r == '_' || r == '-' || r == ' ' {
			capitalize = true
			continue
		}
		if capitalize {
			result.WriteRune(unicode.ToUpper(r))
			capitalize = false
		} else {
			result.WriteRune(r)
		}
	}
	return result.String()
}

func goType(v interface{}) string {
	switch v := v.(type) {
	case bool:
		return "bool"
	case float64:
		if v == float64(int64(v)) {
			return "int"
		}
		return "float64"
	case string:
		return "string"
	case nil:
		return "interface{}"
	case []interface{}:
		if len(v) > 0 {
			if _, ok := v[0].(map[string]interface{}); ok {
				return "struct"
			}
			return goType(v[0])
		}
		return "interface{}"
	default:
		return "interface{}"
	}
}

func generateStruct(name string, data map[string]interface{}, indent int) string {
	var sb strings.Builder
	prefix := strings.Repeat("\t", indent)
	sb.WriteString(fmt.Sprintf("%stype %s struct {\n", prefix, toCamelCase(name)))

	var subStructs string
	for key, val := range data {
		fieldName := toCamelCase(key)
		jsonTag := key

		switch v := val.(type) {
		case map[string]interface{}:
			subStructs += generateStruct(fieldName, v, indent) + "\n"
			sb.WriteString(fmt.Sprintf("%s\t%s %s `json:\"%s\"`\n", prefix, fieldName, fieldName, jsonTag))
		case []interface{}:
			if len(v) > 0 {
				if elem, ok := v[0].(map[string]interface{}); ok {
					subStructs += generateStruct(fieldName, elem, indent) + "\n"
					sb.WriteString(fmt.Sprintf("%s\t%s []%s `json:\"%s\"`\n", prefix, fieldName, fieldName, jsonTag))
				} else {
					sb.WriteString(fmt.Sprintf("%s\t%s []%s `json:\"%s\"`\n", prefix, fieldName, goType(v[0]), jsonTag))
				}
			} else {
				sb.WriteString(fmt.Sprintf("%s\t%s []interface{} `json:\"%s\"`\n", prefix, fieldName, jsonTag))
			}
		default:
			sb.WriteString(fmt.Sprintf("%s\t%s %s `json:\"%s\"`\n", prefix, fieldName, goType(v), jsonTag))
		}
	}

	sb.WriteString(fmt.Sprintf("%s}\n", prefix))
	return subStructs + sb.String()
}

func main() {
	name := flag.String("name", "Root", "根结构体名称")
	flag.Parse()

	var data map[string]interface{}
	decoder := json.NewDecoder(os.Stdin)
	if err := decoder.Decode(&data); err != nil {
		fmt.Fprintf(os.Stderr, "JSON 解析失败: %v\n", err)
		os.Exit(1)
	}

	fmt.Println("package main\n")
	fmt.Print(generateStruct(*name, data, 0))
}

保存为 main.go,同一目录下执行 go mod init json2go 后就能跑。

小结

用 OpenCode 开发这个工具,从零到可用大约花了 4 分钟。对比传统方式:

| 环节 | 传统开发 | OpenCode |
|------|---------|----------|
| 初始化项目 | 手动 mkdir + go mod init | 一句话 |
| JSON 解析 | 查文档、写代码 | 一句话 |
| 类型映射 + 驼峰 | 编码 20 分钟 | 一句话 |
| 嵌套结构递归 | 设计递归逻辑 15 分钟 | 一句话 |
| 命令行参数 | 查 flag 包用法 | 一句话 |

真正的效率提升不在于"打字少了",而在于不需要在编码、查文档、调细节之间频繁切换注意力。你始终停留在"描述需求"的思维层面,OpenCode 负责把需求翻译成可运行的代码。

这个工具本身也很实用——下次对接新 API,把返回的 JSON 往终端一丢,秒出结构体定义。你也可以试着扩展它:支持 YAML 输入、添加 omitempty 标签、支持指针类型字段,一句话就能让 OpenCode 帮你加上。