os.Args
os.Args 是一个字符串切片。os.Args[0] 是程序名,其余元素是参数,与 shell 传入的完全一致。
在编辑器的“参数”面板中,每个输入框就是一个参数,原样传入。在三个输入框中分别填入 hello、two words 和 -v 试试:程序看到三个参数,two words 仍然是一个中间带空格的参数。在终端中由 shell 负责拆分参数,所以同样的效果要写成 go run . hello "two words" -v。
取索引之前一定要检查 len(os.Args)。没有参数时访问 os.Args[1] 会 panic,报错 index out of range [1] with length 1。
参数都是字符串。用 strconv.Atoi 或 strconv.ParseFloat 转换数字并处理错误,因为用户什么都可能输入。
flag 包
像 -port 8080 -verbose 这样的选项,请使用 flag。它负责解析、转换类型、报告错误,还能生成帮助信息。
不带参数时,这段程序打印一次 hello, world。在“参数”面板中,先在两个输入框里填入 -name 和 Gopher,再加上 -count=3、-loud 和 extra 三个。程序会打印三次 HELLO, GOPHER!,以及 remaining args: [extra]。
flag 的工作方式:
- 每个定义函数(
flag.String、flag.Int、flag.Bool、flag.Float64、flag.Duration、flag.Uint64……)都接受名称、默认值和用法说明,并返回一个指针。在flag.Parse()之后用*name读取值。 Var系列函数绑定到你已有的变量:flag.IntVar(&cfg.Port, "port", 8080, "port")。用于配置结构体时这种写法更整洁。- 用户可以写
-name value、-name=value、--name value或--name=value。Go 不区分一个短横线和两个短横线。 - 布尔 flag 要用
=才能接收值。-loud设为 true,-loud=false设为 false,但-loud false会设为 true,并把false留作位置参数。 - 解析在遇到第一个非 flag 参数时停止(或遇到
--时)。prog file.txt -v会把-v当作位置参数。请把 flag 放在前面。 flag.Args()返回剩下的位置参数,flag.NArg()返回它们的个数,flag.Arg(i)返回其中一个。
未知的 flag 或错误的值会打印一条错误和用法说明,并以状态码 2 退出。-h 或 -help 会打印用法说明并以状态码 0 退出(从 Go 1.15 开始)。用法说明根据你的定义自动生成:
Usage of greet:
-count int
how many times (default 1)
-delay duration
pause between greetings, e.g. 10ms
-loud
shout the greeting
-name string
who to greet (default "world")
把 flag.Usage 设为一个函数,就可以在调用 flag.PrintDefaults() 之前打印你自己的标题。
用 FlagSet 实现子命令
像 git commit -m msg 这样的工具有带各自 flag 的子命令。为每个子命令创建一个 flag.FlagSet,并用 switch 根据第一个参数选择:
flag.ContinueOnError 让 Parse 返回错误而不是直接退出,这样函数就可以测试。run 把参数作为形参接收,而不是读取 os.Args,所以测试可以直接调用 run([]string{"list", "-all"})。在“参数”面板中试试 list 和 -all,或者填 delete 看看出错时的流程。
对于带嵌套命令、shell 自动补全和自动生成文档的大型命令行工具,大多数项目会使用第三方的 github.com/spf13/cobra。标准库的 flag 包足以应付小工具。
环境变量
- 变量不存在和变量被设为空字符串时,
os.Getenv都返回""。os.LookupEnv可以区分这两种情况。 - 值永远是字符串。请在启动时转换并校验它们,出错时给出清楚的提示,而不是在处理请求的半路失败。
os.Setenv影响当前进程以及之后启动的子进程。它无法改变启动你的程序的那个 shell 的环境。os.Environ()以"KEY=value"字符串的形式返回所有变量。
常见的配置方式:由人输入的内容用 flag,部署相关的设置用环境变量(容器平台会设置它们),flag 覆盖环境变量,环境变量覆盖默认值。
退出码与 os.Exit
main 返回时,Go 程序以状态码 0 退出。os.Exit(code) 会以该状态码立即结束进程。按照惯例,0 表示成功,1 表示一般错误,2 表示用法错误(flag 包使用 2)。
os.Exit 不会执行延迟函数。 文件不会被刷新,defer 的清理工作也会被跳过。log.Fatal 会调用 os.Exit(1),效果相同。只在一个地方调用 os.Exit,也就是 main 的末尾,把真正的程序逻辑放进一个返回错误的 run 函数里,就像子命令示例那样:
func main() {
if err := run(os.Args[1:]); err != nil {
fmt.Fprintln(os.Stderr, "error:", err)
os.Exit(1)
}
}
把错误信息写到 os.Stderr 而不是 os.Stdout,这样在输出被重定向到文件或通过管道传给另一个命令时,错误信息仍然可见。未被 recover 的 panic 会以状态码 2 退出。
常见错误
- 不检查长度就对
os.Args取索引。 缺少参数时会 panic。 - 在
flag.Parse()之前读取 flag。 你只会得到默认值。 - 忘记写
*。fmt.Println(port)打印的是类似0xc000012345的地址,而不是值。 - 把 flag 放在位置参数后面。
prog input.txt -v不会解析-v。 - 对布尔 flag 写
-verbose false。 请写-verbose=false。 - 在程序深处调用
os.Exit或log.Fatal。 延迟的清理永远不会执行,代码也无法测试。请把错误一路返回到main。
常见问题
Go 中如何获取命令行参数?
os.Args 是一个 []string,索引 0 是程序名,后面是各个参数。os.Args[1:] 就是用户输入的参数。取索引之前先检查 len(os.Args),否则缺少参数时程序会 panic。
Go 中如何使用 flag 包?
先声明 flag,调用 flag.Parse(),然后读取它们:port := flag.Int("port", 8080, "port to listen on")、flag.Parse()、fmt.Println(*port)。这些函数返回的是指针。用户可以写 -port=9000、-port 9000 或 --port 9000,-h 会打印自动生成的用法说明。
Go 中如何读取环境变量?
os.Getenv("HOME") 返回变量的值,变量未设置时返回空字符串。要区分“未设置”和“设置为空”,请用 v, ok := os.LookupEnv("HOME")。os.Setenv 会改变当前进程以及之后它启动的子进程的环境。
Go 中的 os.Exit 会执行 defer 函数吗?
不会。os.Exit 以给定的状态码立即结束进程,延迟调用不会执行,所以缓冲中的输出可能丢失,文件也可能没有刷新到磁盘。常见的写法是 func main() { if err := run(); err != nil { fmt.Fprintln(os.Stderr, err); os.Exit(1) } },把所有实际工作和 defer 都放在 run 里。