在 Go 开发中,对接第三方 API 时经常需要根据 JSON 返回体手写结构体定义——字段一多起来,既枯燥又容易出错。今天我们用 OpenCode 写一个命令行工具 json2go:输入一段 JSON,自动生成对应的 Go struct 代码,嵌套对象自动生成子结构体,数组元素智能推断类型。
最终效果:
$ 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"`
}
整个过程我会用自然语言指挥 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,不需要手动操作。
我继续对 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 描述需求时,按"输入→处理逻辑→输出"的结构来描述,它能精确理解。
我对 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 帮你加上。