Menu

Golang 命令行参数:os.Args、flag 与环境变量

Go 程序如何读取命令行:os.Args、用于类型化选项的 flag 包、用 FlagSet 实现子命令、用 os.Getenv 和 os.LookupEnv 读取环境变量,以及用 os.Exit 设置退出码。

本页包含可运行的编辑器。编辑、运行,立即查看输出。

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 里。

Coddy 编程语言插图

和 Coddy 一起学编程

开始