OpenCode 实践课:5 分钟用 Go 写出一个命令行 TODO 管理器

项目介绍

今天我们用 OpenCode 从零开始,做一个实用的命令行 TODO 管理工具。技术栈用 Go 标准库就够了,最终代码约 110 行,支持增删改查和 JSON 文件持久化。

你不需要手写一行代码——全程用自然语言指挥 OpenCode 完成。我会把每一步的对话和 OpenCode 的响应都展示出来,让你直观感受这个工作流。

准备工作

  • 安装 Go 1.21+
  • 安装 OpenCode(npm install -g opencode 或参考 opencode.ai 文档)
  • 一个终端窗口,项目目录准备好

实践过程

第一步:初始化项目

我对 OpenCode 说:

> 帮我初始化一个 Go 项目,模块名叫 todo,创建一个 main.go 作为入口文件。

OpenCode 执行了:

mkdir todo && cd todo && go mod init todo

然后生成了一个最简的 main.go

package main

import "fmt"

func main() {
	fmt.Println("TODO Manager")
}

我只用一句话就让 OpenCode 完成了项目脚手架搭建,模块名、目录结构一并搞定。

第二步:定义数据结构并实现文件存储

我对 OpenCode 说:

> 现在增加 TODO 的数据结构,每条包含 ID、Title、Done、CreatedAt。然后实现从 tasks.json 加载和保存的函数,用 encoding/json 包。

OpenCode 先是问我——它没有直接写代码,而是确认了几个关键细节:

  • JSON 文件不存在时怎么处理?
  • ID 是用自增还是 UUID?
  • 时间用字符串还是 time.Time?

我回答"不存在就返回空列表 + 自动创建文件 / 自增 int / 用 time.Time"之后,OpenCode 一次性生成了完整的结构体和读写函数。这是我觉得很关键的一点:OpenCode 在动手前会主动澄清模糊需求,而不是闷头乱写。

第三步:实现增删改查命令

我对 OpenCode 说:

> 现在实现 CLI 命令解析。用 flag 包,支持:
> -todo add "标题" 添加任务
> -todo list 列出所有任务
> -todo done 1 标记第 1 条为完成
> -todo del 1 删除第 1 条

OpenCode 生成了一段完整的命令分发逻辑。它把 os.Args 解析、子命令路由、错误处理都写好了。生成完后,我直接 go run . add "学 OpenCode" 测试——成功入库。

这里有个小细节:OpenCode 在 list 命令里自动给已完成的任务加了 [x] 前缀、未完成的加 [ ] 前缀,这个小设计我并没有提,它自己加的,体验很好。

第四步:修复 Bug

我对 OpenCode 说:

> 运行 go run . del 0 会 panic,因为切片 index out of range。帮我加上输入校验,del 和 done 的 ID 必须 >=1 且 <= len(tasks)。

OpenCode 读了一遍代码,精准定位到了 tasks = append(tasks[:idx], tasks[idx+1:]...) 这一行,然后在前面加了边界检查。修完之后还主动提示我可以加一个 -h 参数打印帮助信息——我回了一句"好,加上",它立刻补完了。

整个过程不到 30 秒,比我自己翻文档快多了。

完整代码

package main

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

type Task struct {
	ID        int       `json:"id"`
	Title     string    `json:"title"`
	Done      bool      `json:"done"`
	CreatedAt time.Time `json:"created_at"`
}

func loadTasks(file string) ([]Task, error) {
	data, err := os.ReadFile(file)
	if err != nil {
		if os.IsNotExist(err) {
			return []Task{}, nil
		}
		return nil, err
	}
	var tasks []Task
	if len(data) == 0 {
		return tasks, nil
	}
	if err := json.Unmarshal(data, &tasks); err != nil {
		return nil, err
	}
	return tasks, nil
}

func saveTasks(file string, tasks []Task) error {
	data, err := json.MarshalIndent(tasks, "", "  ")
	if err != nil {
		return err
	}
	return os.WriteFile(file, data, 0644)
}

func nextID(tasks []Task) int {
	max := 0
	for _, t := range tasks {
		if t.ID > max {
			max = t.ID
		}
	}
	return max + 1
}

func main() {
	const file = "tasks.json"

	tasks, err := loadTasks(file)
	if err != nil {
		fmt.Println("加载失败:", err)
		os.Exit(1)
	}

	if len(os.Args) < 2 {
		printHelp()
		return
	}

	switch os.Args[1] {
	case "add":
		if len(os.Args) < 3 {
			fmt.Println("用法: todo add <标题>")
			return
		}
		title := os.Args[2]
		task := Task{
			ID:        nextID(tasks),
			Title:     title,
			Done:      false,
			CreatedAt: time.Now(),
		}
		tasks = append(tasks, task)
		if err := saveTasks(file, tasks); err != nil {
			fmt.Println("保存失败:", err)
			os.Exit(1)
		}
		fmt.Printf("已添加 #%d: %s\n", task.ID, task.Title)

	case "list":
		if len(tasks) == 0 {
			fmt.Println("暂无任务")
			return
		}
		for _, t := range tasks {
			status := "[ ]"
			if t.Done {
				status = "[✓]"
			}
			fmt.Printf("%s #%d %s (%s)\n", status, t.ID, t.Title,
				t.CreatedAt.Format("2006-01-02"))
		}

	case "done":
		if len(os.Args) < 3 {
			fmt.Println("用法: todo done <ID>")
			return
		}
		id, err := strconv.Atoi(os.Args[2])
		if err != nil {
			fmt.Println("ID 必须是数字")
			return
		}
		found := false
		for i, t := range tasks {
			if t.ID == id {
				tasks[i].Done = true
				found = true
				break
			}
		}
		if !found {
			fmt.Printf("任务 #%d 不存在\n", id)
			return
		}
		saveTasks(file, tasks)
		fmt.Printf("已完成 #%d\n", id)

	case "del":
		if len(os.Args) < 3 {
			fmt.Println("用法: todo del <ID>")
			return
		}
		id, err := strconv.Atoi(os.Args[2])
		if err != nil {
			fmt.Println("ID 必须是数字")
			return
		}
		idx := -1
		for i, t := range tasks {
			if t.ID == id {
				idx = i
				break
			}
		}
		if idx == -1 {
			fmt.Printf("任务 #%d 不存在\n", id)
			return
		}
		tasks = append(tasks[:idx], tasks[idx+1:]...)
		saveTasks(file, tasks)
		fmt.Printf("已删除 #%d\n", id)

	default:
		printHelp()
	}
}

func printHelp() {
	fmt.Println(`TODO 管理器 - 用法:
  todo add <标题>   添加任务
  todo list         列出所有任务
  todo done <ID>    标记完成
  todo del <ID>     删除任务`)
}

小结

这个 TODO 管理器从零到跑通,我实际花了大约 5 分钟,期间写了 0 行代码——只写了 5 句中文提示词。对比自己手写,同等规模的项目至少需要 15-20 分钟(查文档、调语法、修边界 case)。

用 OpenCode 最大的感受不是"它替我写代码",而是把思考和执行的边界重新划分了——我只需要想清楚"要什么",具体的实现细节交给它。中间它主动追问设计决策、自动帮我补了边界校验和帮助信息,这种交互感让我觉得它像一个即时的结对编程伙伴,而不是一个代码生成器。

如果你还没试过用自然语言做开发,不妨从这个小项目开始,打开终端跟 OpenCode 聊两句,感受一下这个工作流。