应使用结构体封装任务而非map[string]bool,因后者无法处理特殊字符且缺乏元数据;持久化优先json但需规范时间序列化;命令行解析用flag包;sqlite需正确建表与配置wal模式。

用 map[string]bool 存待办事项太危险
很多人一上来就用 map[string]bool 表示“是否完成”,比如 tasks["买牛奶"] = true。问题在于:任务内容含空格、中文、特殊符号时,map 键无法区分语义;更致命的是,它不保存创建时间、优先级、截止时间等必要字段,后期加字段就得全量重构。
实操建议:
- 用结构体封装任务,哪怕最简也要带 ID 和 Text
- ID 必须是唯一且稳定的(别用 uuid.New() 每次都生成新值,除非你真需要分布式 ID)
- 如果只是本地 CLI 工具,用自增整数 int 做 ID 更轻量、可读性高
- 示例:
type Task struct {<br> ID int `json:"id"`<br> Text string `json:"text"`<br> Done bool `json:"done"`<br> CreatedAt time.Time `json:"created_at"`<br>}
文件持久化选 json.Marshal 还是 gob
新手常卡在“怎么把任务存到硬盘”。json.Marshal 看起来直观,但实际踩坑多:时间字段默认序列化成字符串(如 "2024-05-21T14:22:03.123Z"),反序列化时若没配好 time.Time 的 UnmarshalJSON 方法,会静默失败为零值;而 gob 虽然 Go 原生支持、速度快、保留类型,但它不跨语言、不可读、升级结构体字段时容易 panic。
实操建议:
- 小型 CLI 工具首选 json,但必须显式处理 time.Time
- 在结构体上加 json:"created_at,string" 标签,并确保写入前调用 task.CreatedAt.Format(time.RFC3339)
- 避免直接 os.WriteFile("tasks.json", data, 0644) —— 先写临时文件,再 os.Rename 原子替换,防止程序崩溃时损坏数据
- 不要用 gob 除非你确定永远只用 Go 读写、且结构体长期稳定
命令行参数解析别手写 os.Args
看到 os.Args 就想自己切片拼逻辑?很快会发现:带空格的任务文本(如 add "买咖啡和面包")被错误拆成两个参数;--done 123 和 -d 123 风格不统一;帮助信息要手动维护,错一个就对不上。
实操建议:
- 直接用 flag 包,不用第三方(除非你真需要子命令嵌套)
- 所有操作动词(add、done、list)做成独立命令,用 os.Args[1] 分流,别塞进一个 flag 里
- 对用户输入的文本,用 flag.String 并允许空格(flag.String("text", "", "task content")),然后通过 -text="xxx" 或 --text "xxx" 传入
- 错误提示别只写 fmt.Println("invalid id"),加上 os.Exit(1),让脚本调用者能靠退出码判断失败类型
SQLite 嵌入不是“加个驱动就完事”
一旦任务量过百、需要模糊搜索或按日期筛选,纯文件 JSON 就开始卡顿。这时有人立刻切 SQLite,结果跑出 no such table: tasks 或 database is locked —— 因为没建表、没设 pragma、没控制连接生命周期。
实操建议:
- 初始化 DB 时必须执行建表语句,且检查 sqlite.ErrNoDatabase 类错误,不能假设文件一定存在
- 设 PRAGMA journal_mode = WAL 提升并发读写能力(CLI 应用虽无高并发,但避免 INSERT 时整个 DB 被锁死)
- 每次操作用 db.Exec / db.QueryRow 即可,别长期持有 *sql.Tx —— CLI 是短命进程,事务粒度到单条命令就够了
- 表结构至少包含 id INTEGER PRIMARY KEY AUTOINCREMENT,别用 TEXT 当主键,否则排序、索引都失效
事情说清了就结束。真正难的不是写完第一个版本,而是当用户开始往任务里粘贴代码片段、URL、emoji 时,你的 Text 字段还能不能 round-trip 不丢字节。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











