Cobra本身不提供交互能力,必须搭配survey或promptui等库;交互逻辑须封装为独立函数并用RunE替代Run,避免破坏生命周期控制、信号捕获失效及后续功能扩展失控。

交互式命令行终端不是靠 fmt.Scanln 堆出来的,Cobra 本身不提供交互能力,必须搭配 survey 或 promptui 这类库才能实现真交互;语言学习工程化也不是把单词表塞进 CLI 就完事,关键在可扩展的命令结构、可插拔的练习模块和可复用的状态管理。
为什么不能直接在 Run 函数里写 fmt.Print + fmt.Scan
这种写法会破坏 Cobra 的生命周期控制:信号中断(SIGINT)无法捕获、子命令无法共享上下文、帮助信息与交互逻辑割裂、测试几乎不可行。更严重的是,它让整个 CLI 退化成单点脚本,后续加「错词回顾」「艾宾浩斯复习调度」「导出学习报告」等能力时,代码会迅速失控。
- 所有交互逻辑必须封装为独立函数,返回
error而非靠os.Exit(1)中断流程 -
RunE替代Run—— 它支持返回错误,Cobra 会自动打印并退出码,便于统一错误处理 - 交互前先校验必要依赖(如词库路径、用户配置),避免进入交互后才报
open words.yaml: no such file
survey 和 promptui 怎么选
两者都能做交互,但设计哲学不同:survey 是 declarative(声明式),适合结构化问答;promptui 是 imperative(命令式),适合动态渲染和状态驱动界面。
- 背单词场景用
survey更稳:survey.Ask一次定义问题列表,自动处理空输入、类型转换、验证失败重试 - 需要实时显示记忆曲线图表或滚动错题列表?用
promptui,它允许你在循环中调用promptui.Select或promptui.Prompt,并随时刷新界面 - 注意
survey默认不兼容 Windows 控制台旧版编码,加survey.WithStdio(os.Stdin, os.Stdout, os.Stderr)显式指定流
如何让「学习模式」成为可插拔命令
别把所有功能硬编码进 rootCmd。真正的工程化是把每个学习模式(如 spaced-repetition、flashcard、dictation)做成独立 *cobra.Command,通过统一接口接入。
Go 配置库,使用 spf13/viper — 分层优先级(flag > env >file > KV > default),提供 BindPFlag/BindPFlags、SetEnvPrefix + SetEnvKeyReplace 等功能。
立即学习“go语言免费学习笔记(深入)”;
- 定义公共接口:
type Practice interface { Init(*viper.Viper) error; Run(context.Context) error } - 每个模式实现该接口,并在
init()函数里注册自己到全局 map:practices["spaced-repetition"] = &SpacedRepetition{} - 主命令解析子命令名后,查 map 实例化对应 Practice,再调用
Init和Run—— 新增模式只需新增文件,不改原有逻辑 - 这样做的副作用:flag 注册必须提前完成,
cmd.Flags().String("wordlist", "", "path to word list")得在init()阶段就挂上,不能等到RunE里才解析
容易被忽略的初始化顺序陷阱
很多语言学习 CLI 在首次运行时崩溃,不是因为词库格式错,而是 viper 加载顺序和 cobra flag 解析时机打架。
正确顺序只能是:viper.SetConfigName("config") → viper.AddConfigPath(".") → viper.ReadInConfig() 必须在 rootCmd.Execute() 之前完成,且要在 rootCmd.PersistentFlags() 注册完所有 flag 后、viper.BindPFlags(rootCmd.PersistentFlags()) 之前执行。否则 flag 值不会覆盖配置文件里的同名项,用户传了 --wordlist ./my.yaml 却还是读默认路径。

















