实现Todo CLI需手动解析子命令、隔离flag上下文、校验JSON零值、用bufio安全读输入、谨慎使用-ldflags;细节处理比语法更重要。

直接上手写一个能跑起来、有输入输出、带简单持久化的命令行工具,比空谈“整合基础能力”更能检验真实掌握程度。下面以实现一个本地待办事项(Todo)管理 CLI 为例,聚焦实际编码中必须面对的取舍和细节。
用 flag 解析命令行参数时,别忽略子命令与参数顺序依赖
Go 的 flag 包不原生支持子命令(如 todo add "buy milk"),强行用 flag.String 拿全部剩余参数容易出错。更稳妥的做法是手动切分 os.Args,先识别子命令,再按需初始化对应 flag.FlagSet。
- 子命令名必须严格匹配,比如
"add"和"list"建议用map[string]func()分发,避免长 if 链 -
flag.Parse()调用前,务必用flag.CommandLine = flag.NewFlagSet(...)隔离不同子命令的参数解析上下文,否则重复调用会 panic - 用户输入
todo add -p high "fix bug"时,-p是add子命令的 flag,不是主命令的 —— 这点初学者常混淆
用 encoding/json 读写文件时,注意结构体字段导出与默认零值陷阱
JSON 序列化只处理导出字段(首字母大写),且反序列化时不会调用构造逻辑。如果 Todo 结构体里有 CreatedAt time.Time 字段,但 JSON 文件里没这个 key,反序列化后它就是零值 0001-01-01T00:00:00Z,而非当前时间。
- 给时间字段加
json:",omitempty"标签不能解决零值问题,只会让字段在为零时不输出;真正需要的是反序列化后手动补全:if t.CreatedAt.IsZero() { t.CreatedAt = time.Now() } - 用
json.RawMessage延迟解析嵌套字段(比如优先级字段未来可能从字符串扩展为对象),避免一改结构体就导致旧数据解析失败 - 写入前先
os.WriteFile(filename, data, 0644),别用os.Create+Write组合——前者原子性更好,且自动处理文件不存在时的创建
用 fmt.Scanln 做交互输入时,记得清理缓冲区和处理 EOF
当程序在终端里提示 “Enter todo text:” 并等待用户输入时,fmt.Scanln 对换行符敏感,且遇到 Ctrl+D(Unix)或 Ctrl+Z(Windows)会返回 io.EOF 错误。不检查这个错误,程序可能静默退出或 panic。
立即学习“go语言免费学习笔记(深入)”;
- 用
bufio.NewReader(os.Stdin)+ReadString('\n')更可控,能明确区分空输入和中断 - 读完一行后,用
strings.TrimSpace()清除首尾空白,否则用户输个空格再回车,会存入空 todo - 如果后续要支持多行输入(比如 todo 描述),别硬套
Scanln,改用循环读直到遇到特定结束标记(如单独一行.)
交叉编译和打包成单文件时,go build -ldflags="-s -w" 不是万能的
加 -s -w 确实能去掉符号表和调试信息,减小二进制体积,但如果你用了 embed 或依赖某些 cgo 特性(哪怕只是间接依赖),这个 flag 可能让链接失败或运行时报错。
- 确认是否启用 cgo:执行
go env CGO_ENABLED,交叉编译 Windows 到 Linux 时通常要设为0 - 用
upx进一步压缩前,先验证原始二进制能否正常运行 —— UPX 加壳后某些杀软会误报,生产环境慎用 - Linux 下发布时,别假设用户有
/usr/local/bin写权限;提供./todo直接运行方式,比强调“请复制到 PATH”更友好
真正卡住人的,往往不是语法不会,而是 flag 参数没隔离清楚、JSON 零值没校验、输入换行没吃掉、或者交叉编译时忘了关 cgo。这些点不写进代码注释里,下次自己回头看也得花十分钟定位。










