CLI中用os.Exit()退出前必须先输出错误信息到os.Stderr,避免空退出;flag.Parse()后须检查flag.NArg()及参数合法性,防止索引错位或非法输入。
os.Exit()退出前必须先输出错误Go CLI工具里常见错误是调用os.Exit(1)后没打印任何信息,导致用户只看到空退出码,完全不知道哪错了。Go本身不强制要求错误输出,但CLI交互必须让用户知道发生了什么。
正确做法是在os.Exit()前显式写入os.Stderr,且避免用log.F——它会额外打时间戳和文件名,破坏CLI的简洁输出风格。
atal()
if err != nil {
os.Exit(1)
}if err != nil {
fmt.Fprintln(os.Stderr, "error:", err)
os.Exit(1)
}die()函数统一处理,确保每次退出都带前缀、换行、stderr重定向flag.Parse()之后要检查flag.NArg()和参数合法性很多CLI工具只校验flag,忽略位置参数(如mytool file.txt --verbose里的file.txt)。一旦flag.Parse()成功,不代表业务逻辑就安全了——用户可能少输、多输或传入非法路径。
Go标准库不会帮你做这层校验,全靠手动判断。尤其要注意flag.NArg()返回的是未被flag解析的剩余参数个数,不是len(os.Args)。
立即学习“go语言免费学习笔记(深入)”;
os.Args[1]取第一个参数,结果flag.Parse()已挪动os.Args,导致索引错位flag.Parse()
if flag.NArg() < 1 {
fmt.Fprintln(os.Stderr, "error: missing required argument")
os.Exit(1)
}
filename := flag.Arg(0) // 用 flag.Arg(i),不是 os.Args[i+1]os.Stat(),提前暴露no such file类错误,而不是等到真正读取时才报fmt.Errorf()的%w动词提升上下文可追溯性CLI工具链路长(比如解析配置→连接API→处理响应),单纯用errors.New()或字符串拼接会丢失原始错误类型和堆栈线索。Go 1.13+的%w动词能包装错误并保留底层错误,方便上层做类型断言或日志分级。
但要注意:不是所有地方都适合用%w。比如用户输入明显非法(如负数端口号),应返回新错误而非包装;只有“下游失败导致当前操作不可行”时才用%w。
func loadConfig(path string) error {
data, err := os.ReadFile(path)
if err != nil {
return fmt.Errorf("failed to read config %s: %w", path, err)
}
// ...
}
// 调用方可以:
if errors.Is(err, os.ErrNotExist) { ... }%w会让错误消息冗长,且errors.Unwrap()需多次调用才能触底err.Error()),所以包装时要把用户需要的关键信息放在最外层字符串里Command.Run必须独立处理错误,不能依赖全局recover用spf13/cobra或类似库时,有人试图在根命令RunE里用defer/recover兜底panic,但这对CLI是危险的——它掩盖了本该由用户修正的问题(比如传错flag类型),也干扰了退出码语义(panic被捕获后仍返回0)。
Go CLI的错误退出码有约定俗成含义:0成功,1通用错误,2用法错误(如--help缺失参数)。用recover会破坏这个契约。
RunE函数返回error,由cobra自动转为os.Exit(1)并输出到stderrRunE里调用os.Exit()——这会让cobra无法统一控制退出流程,比如无法触发PersistentPostRun钩子defer func() {
if r := recover(); r != nil {
fmt.Fprintln(os.Stderr, "panic:", r)
os.Exit(1)
}
}()if err != nil里。