平时会写一些内部用的命令行小工具,Go 是我最常用的语言:编译出来就是一个可执行文件,丢到服务器上就能跑。工具写多了,慢慢形成了几个固定的习惯,整理出来分享一下。
1. main 只负责组装,逻辑放进 run
main 函数里不写业务逻辑,只做参数解析和退出码处理,真正的逻辑放进一个返回 error 的 run 函数。这样既方便写单元测试,也不会到处散落 os.Exit。
go
func main() {
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
defer stop()
if err := run(ctx, os.Args[1:]); err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
}
2. 用 context 统一处理退出信号
上面的 signal.NotifyContext 会在收到 Ctrl+C 时取消 ctx。只要下游的 HTTP 请求、数据库查询都接收这个 ctx,按下 Ctrl+C 后工具就能干净地退出,不会留下写了一半的文件。
3. 给慢操作加超时
调用外部接口时,单独包一层超时,避免网络抖动时工具一直卡住:
go
reqCtx, cancel := context.WithTimeout(ctx, 10*time.Second)
defer cancel()
req, _ := http.NewRequestWithContext(reqCtx, http.MethodGet, url, nil)
4. 错误信息写给使用者看
返回错误时带上"在做什么"和关键参数,比如 fmt.Errorf("读取配置 %s 失败: %w", path, err)。用工具的人往往不看源码,一条清楚的错误信息能省掉很多来回沟通。
5. 分清 stdout 和 stderr
结果输出到 stdout,进度和日志输出到 stderr。这样别人可以放心地把结果用管道交给 jq 或 grep 处理,不会被日志混进去。
小结
这些习惯都不复杂,但坚持下来之后,工具的可维护性和使用体验都会好很多。如果你也有类似的小经验,欢迎一起交流。