深色模式
Go 命令行参数解析
概述
Go 写命令行程序时,参数解析通常绕不开两套东西:最底层的 os.Args,以及标准库里的 flag。
它们不是谁替代谁的关系,而是抽象层次不同。os.Args 更原始,适合完全自定义;flag 更像一个轻量解析器,适合大多数普通 CLI。再往上,如果命令带子命令,就该用 flag.FlagSet 把不同命令拆开。
先分清三种用法
os.Args:拿到原始参数数组,自己决定怎么解析。flag:适合单命令程序,直接解析常规参数。flag.FlagSet:适合serve、build、migrate这种子命令结构。
如果只是写一个小工具,先上 flag 就够了。只有当参数格式明显超出它的默认能力时,再回退到 os.Args 或上更重的 CLI 框架。
os.Args 是最原始的一层
os.Args 的类型是 []string。其中:
os.Args[0]是当前程序路径。os.Args[1:]才是用户传入的参数。
例如:
go
package main
import (
"fmt"
"os"
)
func main() {
fmt.Println(os.Args)
}执行:
sh
go run main.go --name jack dev输出通常类似:
txt
[/tmp/go-build.../main --name jack dev]os.Args 的优点是自由,缺点也是自由。你要自己处理:
- 参数个数校验
- 参数名和参数值的对应关系
- 帮助信息
- 默认值
所以它更适合格式非常特殊,或者必须完全掌控解析过程的场景。
普通 CLI 先用 flag
标准库 flag 已经能覆盖大部分简单工具的需求。
例如:
go
package main
import (
"flag"
"fmt"
)
func main() {
var addr string
var port int
var debug bool
flag.StringVar(&addr, "addr", "127.0.0.1", "监听地址")
flag.IntVar(&port, "port", 8080, "监听端口")
flag.BoolVar(&debug, "debug", false, "是否开启调试模式")
flag.Parse()
fmt.Println(addr, port, debug)
fmt.Println("位置参数:", flag.Args())
}执行:
sh
go run main.go -addr 0.0.0.0 -port 9000 -debug task-a这里有两类参数:
- 标识参数:
-addr、-port、-debug - 位置参数:
task-a
解析完成后,位置参数可以通过 flag.Args() 或 flag.Arg(i) 读取。
flag 有几个很容易踩的点
标识参数通常应该写在位置参数前面
标准库 flag 默认会在遇到第一个非标识参数后停止继续解析后面的标识参数。
例如:
sh
somecmd -port 9000 dev -debug这里的 -debug 很可能就不会再被当成标识参数解析了。
更稳的写法是:
sh
somecmd -port 9000 -debug dev布尔参数可以单独出现
布尔类型参数不一定非要显式传值:
sh
somecmd -debug这通常就等于把 debug 设为 true。
当然也可以写成:
sh
somecmd -debug=falseflag 读不到 os.Args[0]
flag.Args() 返回的是位置参数,不包含程序路径。需要程序路径时还是得看 os.Args[0]。
子命令不要硬堆在一个 flag.Parse() 里
如果命令像这样:
sh
app serve -port 8080
app build -o dist就不要把所有参数都塞给默认的 flag.CommandLine。更清楚的做法是给每个子命令单独建一个 FlagSet。
go
package main
import (
"flag"
"fmt"
"os"
)
func main() {
if len(os.Args) < 2 {
fmt.Println("expected subcommand: serve or build")
os.Exit(1)
}
switch os.Args[1] {
case "serve":
serveCmd := flag.NewFlagSet("serve", flag.ExitOnError)
port := serveCmd.Int("port", 8080, "监听端口")
_ = serveCmd.Parse(os.Args[2:])
fmt.Println("serve on", *port)
case "build":
buildCmd := flag.NewFlagSet("build", flag.ExitOnError)
output := buildCmd.String("o", "dist", "输出目录")
_ = buildCmd.Parse(os.Args[2:])
fmt.Println("build to", *output)
default:
fmt.Println("unknown subcommand:", os.Args[1])
os.Exit(1)
}
}这样每个子命令的参数边界都很清楚,也更方便后续扩展帮助信息和默认值。
什么时候不该继续坚持标准库 flag
标准库 flag 很好用,但它并不是全能的。下面这些需求一多,通常就该考虑更上层的 CLI 库了:
- 命令层级很多
- 帮助文档需要自动生成
- 参数校验和补全逻辑明显变复杂
- 同一个命令下有大量共享选项
不过在项目早期,先用标准库通常更稳。上来就引入复杂 CLI 框架,很多时候只是把简单问题做重了。
