flag — 命令行参数解析
flag 是 Go 内置的命令行参数解析库,标准库命令与多数服务端进程都用它。语法与行为以下均按 go1.27.0 实测核对。
1. 标志语法
包文档(src/flag/flag.go)列出的合法形式:
-flag
--flag // 单双横线等价
-flag=x
-flag x // 仅限非布尔标志(non-boolean flags only)布尔标志不能用空格带值,关掉它必须写 -flag=false。解析在第一个非标志参数处停止(单独一个 - 也算非标志参数),-- 是显式终止符;整数标志接受 1234、0664、0x1234,可为负。由此产生经典陷阱(实测):
fs := flag.NewFlagSet("c1", flag.ContinueOnError)
verbose := fs.Bool("verbose", false, "verbose output")
port := fs.Int("port", 8080, "server port")
fs.Parse([]string{"-verbose", "true", "-port", "9090"})
// 实测:verbose=true port=8080 Args=["true" "-port" "9090"]-verbose 自身即为 true;true 是第一个非标志参数,解析到此停止,-port 根本没被处理。布尔标志后跟值不是赋值,是提前终止。
2. 定义与解析
返回指针:
name := flag.String("name", "default", "your name")
age := flag.Int("age", 0, "your age")
verbose := flag.Bool("verbose", false, "verbose output")绑定到已有变量(避免解引用,库代码更推荐):
var name string
flag.StringVar(&name, "name", "default", "your name")自定义类型用 flag.Var(实现 String()/Set(string) error,见第 6 节完整示例);不需要存值的"出现即回调"用 flag.BoolFunc(Go 1.21,api/go1.21.txt 核实),实测 -reload 时回调收到 "true"、-reload=now 收到 "now"。标志注册完必须调用 flag.Parse()(解析 os.Args[1:]),否则一律是默认值。
包级函数都转发到包级变量 flag.CommandLine(一个 *FlagSet,默认名 os.Args[0]、ExitOnError 模式);flag.CommandLine.Init(name, errorHandling) 可改错误提示里的名字与退出策略。
注意:标志名建议小写加连字符(
server-addr);usage 文本里用反引号括词可替换帮助信息中显示的类型名(见第 5 节)。
3. 解析结果与非标志参数
flag.Parse()
flag.Args() // []string,标志之后的剩余参数
flag.Arg(0) // 按下标取
flag.NArg() // 剩余参数个数
flag.NFlag() // 已设置的标志个数实测(完整程序见第 6 节):-verbose=false -name=alice -list a,b -reload -reload=now file1.txt file2.txt 解析后 Args=["file1.txt" "file2.txt"] NArg=2 NFlag=4。
4. 子命令
flag 不直接支持子命令,用 flag.NewFlagSet 按 os.Args[1] 手动分派(switch 到各 xxxCmd(os.Args[2:])):
func serveCmd(args []string) {
fs := flag.NewFlagSet("serve", flag.ExitOnError)
port := fs.Int("port", 8080, "server port")
fs.Parse(args)
fmt.Println("serving on port", *port)
}5. 帮助信息与退出码
覆盖 flag.Usage(打印自定用法行再调 flag.PrintDefaults())。PrintDefaults 实测输出(-name/-port 显示反引号自定义类型名;布尔标志省略类型,默认值为零值时不带 “(default …)"):
-name name
your name (default "guest")
-port port
server port (default 8080)
-verbose
verbose output-h/-help 未定义为标志时 Parse 返回 flag.ErrHelp(实测 ContinueOnError 下 errors.Is(err, flag.ErrHelp) 为 true)。ExitOnError(包级默认)实测:-h 打印用法后进程退出码 0;未知标志打印 flag provided but not defined: -bogus 加用法后退出码 2。
6. 自定义 Var + BoolFunc 完整示例
以下程序 go1.27 实际运行,输出为真实结果:
package main
import (
"errors"
"flag"
"fmt"
"strings"
)
type stringsFlag struct{ vals []string }
func (s *stringsFlag) String() string { return strings.Join(s.vals, ",") }
func (s *stringsFlag) Set(v string) error {
if v == "" {
return errors.New("must not be empty")
}
s.vals = append(s.vals, strings.Split(v, ",")...)
return nil
}
func main() {
fs := flag.NewFlagSet("demo", flag.ExitOnError)
name := fs.String("name", "guest", "user name")
verbose := fs.Bool("verbose", false, "verbose output")
var list stringsFlag
fs.Var(&list, "list", "comma-separated items")
var calls []string
fs.BoolFunc("reload", "reload config", func(v string) error {
calls = append(calls, fmt.Sprintf("fn(%q)", v))
return nil
})
fs.Parse([]string{"-verbose=false", "-name=alice", "-list", "a,b",
"-reload", "-reload=now", "file1.txt", "file2.txt"})
fmt.Printf("name=%q verbose=%v list=%q calls=%v Args=%q NArg=%d NFlag=%d\n",
*name, *verbose, list.String(), calls, fs.Args(), fs.NArg(), fs.NFlag())
}name="alice" verbose=false list="a,b" calls=[fn("true") fn("now")] Args=["file1.txt" "file2.txt"] NArg=2 NFlag=4Set 每个值调用一次(同一标志多次出现会累加);返回的 error 视为标志值解析错误(ContinueOnError 下从 Parse 返回)。
7. 常见陷阱
| 陷阱 | 说明 |
|---|---|
忘记 flag.Parse() |
所有标志静默保持默认值 |
布尔标志写成 -verbose true |
解析在 true 处提前停止,后续全进 Args(第 1 节实测) |
| 并发注册/解析 | Parse 前后在其他 goroutine 里注册标志是 data race;在 main 启动并发前完成注册与解析 |
| 子命令里用包级函数 | 包级 Parse 只认 os.Args[1:];子命令必须 fs.Parse(args) |
| Usage 显示临时路径名 | go run 时程序名是缓存 exe 全路径;用 fs.Init/fs.SetName 固定 |
8. 实战建议
单工具小程序用包级 flag 就够;需要子命令、可测试解析或"出错不退出"时改用 flag.NewFlagSet + ContinueOnError,用 SetOutput 接管文案。多级子命令、必填校验、shell 补全这类需求直接上 cobra/pflag(后者语法对 POSIX 短参数更宽容),标准库 flag 的边界要心里有数。