深进探究Golang中flag标准库的使用
在使用 Go 入行开发的过程中,命令行参数解析是我们常常碰到的需求。而 flag 包正是一个用于实现命令行参数解析的 Go 标准库。在本文中,我们将深进探讨 flag 标准库的实现原理和使用技巧,以帮助读者更好地理解和把握该库的使用方法。
1.使用
1.1示例
flag 基本使用示例代码如下:
package main import ( "flag" "fmt" ) type flagVal struct { val string } func (v *flagVal) String() string { return v.val } func (v *flagVal) Set(s string) error { v.val = s return nil } func main() { // 1. 使用 flag.Type() 返归 *int 类型命令行参数 var nFlag = flag.Int("n", 1234, "help message for flag n") // 2. 使用 flag.TypeVar() 绑定命令行参数到 int 类型变量 var flagvar int flag.IntVar(&flagvar, "flagvar", 1234, "help message for flagvar") // 3. 使用 flag.Var() 绑定命令行参数到实现了 flag.Value 接口的自定义类型变量 val := flagVal{} flag.Var(&val, "val", "help message for val") // 解析命令行参数 flag.Parse() fmt.Printf("nFlag: %d\n", *nFlag) fmt.Printf("flagvar: %d\n", flagvar) fmt.Printf("val: %+v\n", val) fmt.Printf("NFlag: %v\n", flag.NFlag()) // 返归已设置的命令行标志个数 fmt.Printf("NArg: %v\n", flag.NArg()) // 返归处理完标志后剩余的参数个数 fmt.Printf("Args: %v\n", flag.Args()) // 返归处理完标志后剩余的参数列表 fmt.Printf("Arg(1): %v\n", flag.Arg(1)) // 返归处理完标志后剩余的参数列表中第 i 项 }
可以通过指定 --help/-h
参数来查观这个命令行程序的使用帮助:
$ go run main.go -h
Usage of ./main:
-flagvar int
help message for flagvar (default 1234)
-n int
help message for flag n (default 1234)
-val value
help message for val
这个程序接收三个命令行参数:
int
类型的-flagvar
,默认值为2134
。int
类型的-n
,默认值为2134
。value
类型的-val
,无默认值。
我们可以将 -flagvar
、-n
、-val
称作 flag
,即「标志」,这也是 Go 内置命令行参数解析库被命名为 flag 的原因,见名知意。
这三个参数在示例代码中,分别使用了三种不同形式来指定:
flag.Type()
:
-n
标志是使用 var nFlag = flag.Int("n", 1234, "help message for flag n")
来指定的。
flag.Int
函数签名如下:
func Int(name string, value int, usage string) *int
flag.Int
函数接收三个参数,分别是标志名称、标志默认参数值、标志使用帮助信息。函数最终还会返归一个 *int
类型的值,表示用户在执行命令行程序时为这个标志指定的参数。
除了使用 flag.Int
来设置 int
类型标志,flag 还支持其他多种类型,如使用 flag.String
来设置 string
类型标志。
flag.TypeVar()
:
-flagvar
标志是使用 flag.IntVar(&flagvar, "flagvar", 1234, "help message for flagvar")
来指定的。
flag.IntVar
函数签名如下:
func IntVar(p *int, name string, value int, usage string)
与 flag.Int
不同的是,flag.IntVar
函数取消了返归值,而是会将用户传递的命令行参数绑定到第一个参数 p *int
。
除了使用 flag.IntVar
来绑定 int
类型参数到标志,flag 还提供其他多个函数来支持绑定不同类型参数到标志,如使用 flag.StringVar
来绑定 string
类型标志。
flag.Var()
:
-val
标志是使用 flag.Var(&val, "val", "help message for val")
来指定的。
flag.Var
函数签名如下:
func Var(value Value, name string, usage string)
flag.Var
函数接收三个参数,后两个参数分别是标志名称、标志使用帮助信息。而用户传递的命令行参数将被绑定到第一个参数 value
。
type Value interface { String() string Set(string) error }
我们可以自定义类型,只要实现了 flag.Value
接口,都可以传递给 flag.Var
,这极大的增加了 flag 包的灵活性。
定义完三个标志,我们还需要使用 flag.Parse()
来解析命令行参数,只有解析成功以后,才会将户传递的命令行参数值绑定到对应的标志变量中。之后就可以使用 nFlag
、flagvar
、val
的变量值了。
在 main
函数底部,使用 flag.NFlag()
、flag.NArg()
、flag.Args()
、flag.Arg(1)
几个函数获取并铺示了命令行参数相关信息。
现在我们尝试给这个命令行程序传递几个参数并执行它,观下输出结果:
$ go run main.go -n 100 -val test a b c d
nFlag: 100
flagvar: 1234
val: {val:test}
NFlag: 2
NArg: 4
Args: [a b c d]
Arg(1): b
我们通过 -n 100
为 -n
标志指定了参数值 100
,最终会被赋值给 nFlag
变量。
由于没有指定 flagvar
标志的参数值,所以 flagvar
变量会被赋予默认值 1234
。
接着,我们又通过 -val test
为 -val
标志指定了参数值 test
,最终赋值给了自定义的 flagVal
结构体的 val
字段。
因为只设置了 -n
和 -val
两个标志的参数值,所以函数 flag.NFlag()
返归结果为 2。
a b c d
四个参数由于没有被定义,所以 flag.NArg()
返归结果为 4。
flag.Args()
返归的切片中存储了 a b c d
四个参数。
flag.Arg(1)
返归切片中下标为 1
位置的参数,即 b
。
1.2标志类型
在上面的示例中,我们铺示了 int
类型和自定义的 flag.Value
的使用,flag 包支持的所有标志类型汇总如下:
参数类型 | 合法值 |
---|---|
bool | strconv.ParseBool 能够解析的有效值,接受:1, 0, t, f, T, F, true, false, TRUE, FALSE, True, False。 |
time.Duration | time.ParseDuration 能够解析的有效值,如:”300ms”, “-1.5h” or “2h45m”,合法单位:”ns”, “us” (or “µs”), “ms”, “s”, “m”, “h”。 |
float64 | 合法的浮点数类型。 |
int/int64/uint/uint64 | 合法的整数类型,如:1234, 0664, 0x1234,也可以是负数。 |
string | 合法的字符串类型。 |
flag.Value | 实现了该接口的类型。 |
除了支持几种 Go 默认的原生类型外,假如我们想实现其他类型标志的定义,都可以通过 flag.Value
接口类型来完成。其实 flag 包内部对于 bool
、int
等所有类型的定义,都实现了 flag.Value
接口,在稍后讲解源码过程中将会有所体现。
1.3标志语法
命令行标志支持多种语法:
语法 | 说明 |
---|---|
-flag | bool 类型标志可以使用,表示参数值为 true。 |
–flag | 支持两个 - 字符,与 -flag 等价。 |
-flag=x | 所有类型通用,为标志 flag 传递参数值 x。 |
-flag x | 作用等价于 -flag=x,但是仅限非 bool 类型标志使用,如果这样使用 cmd -x * ,其中 * 是 Unix shell 通配符,假如存在名为 0、false 等文件,则参数值结果会发生变化。 |
flag 解析参数时会在第一个非标志参数之前(单独的一个 -
字符也是非标志参数)或终止符 --
之后停止。
2.源码解读
注重:本文以 Go 1.19.4 源码为例,其他版本可能存在差异。
认识了 flag 包的基本使用,接下来我们就要深进到 flag 的源码,来探究其内部是如何实现。
阅读 flag 包的源码,我们可以从使用 flag 包的流程来进手。
2.1定义标志
在 main
函数中,我们首先通过如下代码定义了一个标志 -n
。
var nFlag = flag.Int("n", 1234, "help message for flag n")
flag.Int
函数定义如下:
func Int(name string, value int, usage string) *int { return CommandLine.Int(name, value, usage) }
可以发现,flag.Int
函数调用并返归了 CommandLine
对象的 Int
方法,并将参数原样传递入往。
来观观 CommandLine
是个什么:
var CommandLine = NewFlagSet(os.Args[0], ExitOnError) func NewFlagSet(name string, errorHandling ErrorHandling) *FlagSet { f := &FlagSet{ name: name, errorHandling: errorHandling, } f.Usage = f.defaultUsage return f }
CommandLine
是使用 NewFlagSet
创建的 FlagSet
结构体指针,在构造 FlagSet
对象时,需要两个参数 os.Args[0]
和 ExitOnError
。
我们知道 os.Args
存储了程序执行时指定的所有命令行参数,os.Args[0]
就是当前命令行程序的名称,ExitOnError
是一个常量,用来标记在出现 error
时应该如何做,ExitOnError
表示在碰到 error
时退出程序。
来观下 FlagSet
是如何定义:
type FlagSet struct { Usage func() name string parsed bool actual map[string]*Flag formal map[string]*Flag args []string // arguments after flags errorHandling ErrorHandling output io.Writer // nil means stderr; use Output() accessor }
Usage
字段是一个函数,根据名字大概能够预测出,这个函数会在指定 --help/-h
参数查观命令行程序使用帮助时被调用。
parsed
用来标记是否调用过 flag.Parse()
。
actual
和 formal
分别用来存储从命令行解析的标志参数和在程序中指定的默认标志参数。它们都使用 map
来存储 Flag
类型的指针,FlagSet
可以观作是 Flag
结构体的「集合」。
args
用来保存处理完标志后剩余的参数列表。
errorHandling
标记在出现 error
时应该如何做。
output
用来设置输出位置,这可以改变 --help/-h
时铺示帮助信息的输出位置。
现在来观下 Flag
的定义:
type Flag struct { Name string // 标志名称 Usage string // 帮助信息 Value Value // 标志所对应的命令行参数值 DefValue string // 用来记录字符串类型的默认值,它不会被改变 }
Flag
用来记录一个命令行参数,里面存储了一个标志所有信息。
可以说 Flag
和 FlagSet
两个结构体就是 flag 包的核心,所有功能都是围绕这两个结构体设计的。
标志所对应的命令行参数值为 flag.Value
接口类型,在前文中已经见过了,定义如下:
type Value interface { String() string Set(string) error }
之所以使用接口,是为了能够存储任何类型的值,除了 flag 包默认支持的内置类型,用户也可以定义自己的类型,只要实现了 Value
接口即可。
如我们在前文示例程序中定义的 flagVal
类型。
现在 CommandLine
的定义以及内部实现我们都观过了,是时候归过头来观一观 CommandLine
对象的 Int
方法了:
func (f *FlagSet) Int(name string, value int, usage string) *int { p := new(int) f.IntVar(p, name, value, usage) return p }
Int
方法内部调用了 f.IntVar()
方法,定义如下:
func (f *FlagSet) IntVar(p *int, name string, value int, usage string) { f.Var(newIntValue(value, p), name, usage) }
IntVar
方法又调用了 f.Var()
方法。
Var
方法第一个参数为 newIntValue(value, p)
,我们来观观 newIntValue
函数是如何定义的:
type intValue int func newIntValue(val int, p *int) *intValue { *p = val return (*intValue)(p) } func (i *intValue) Set(s string) error { v, err := strconv.ParseInt(s, 0, strconv.IntSize) if err != nil { err = numError(err) } *i = intValue(v) return err } func (i *intValue) Get() any { return int(*i) } func (i *intValue) String() string { return strconv.Itoa(int(*i)) }
newIntValue
是一个构造函数,用来创建一个 intValue
类型的指针,intValue
底层类型实际上是 int
。
定义 intValue
类型的目的就是为了实现 flag.Value
接口。
再来观下 Var
方法如何定义:
func (f *FlagSet) Var(value Value, name string, usage string) { // Flag must not begin "-" or contain "=". if strings.HasPrefix(name, "-") { panic(f.sprintf("flag %q begins with -", name)) } else if strings.Contains(name, "=") { panic(f.sprintf("flag %q contains =", name)) } // Remember the default value as a string; it won't change. flag := &Flag{name, usage, value, value.String()} _, alreadythere := f.formal[name] if alreadythere { var msg string if f.name == "" { msg = f.sprintf("flag redefined: %s", name) } else { msg = f.sprintf("%s flag redefined: %s", f.name, name) } panic(msg) // Happens only if flags are declared with identical names } if f.formal == nil { f.formal = make(map[string]*Flag) } f.formal[name] = flag }
name
参数即为标志名,在 Var
方法内部,首先对标志名的合法性入行了校验,不能以 -
开头且不包含 =
。
接着,根据参数创建了一个 Flag
类型,并且校验了标志是否被重复定义。
最后将 Flag
保存在 formal
属性中。
到这里,整个函数调用关系就结束了,我们来梳理一下代码执行流程:
flag.Int
-> CommandLine.Int
-> CommandLine.IntVar
-> CommandLine.Var
。
经过这个调用过程,我们就得到了一个 Flag
对象,其名称为 n
、默认参数值为 1234
、值的类型为 intValue
、帮助信息为 help message for flag n
。并将这个 Flag
对象保存在了 CommandLine
这个类型为 FlagSet
的结构体指针对象的 formal
属性中。
我们在示例程序中还使用了另外两种方式定义标志。
使用 flag.IntVar(&flagvar, "flagvar", 1234, "help message for flagvar")
定义标志 -flagvar
。
flag.IntVar
定义如下:
func IntVar(p *int, name string, value int, usage string) { CommandLine.Var(newIntValue(value, p), name, usage) }
可以发现,flag.IntVar
函数内部没有调用 CommandLine.Int
和 CommandLine.IntVar
的过程,而是直接调用 CommandLine.Var
。
另外,我们还使用 flag.Var(&val, "val", "help message for val")
定义了 -val
标志。
flag.Var
定义如下:
func Var(value Value, name string, usage string) { CommandLine.Var(value, name, usage) }
flag.Var
函数内部同样直接调用了 CommandLine.Var
,并且由于参数 value
已经是 Value
接口类型,可以无需调用 newIntValue
这类构造函数将 Go 内置类型转为 Value
类型,直接传递参数即可。
2.2解析标志参数
命令行参数定义完成了,终于到了解析部分,可以使用 flag.Parse()
解析命令行参数。
flag.Parse
函数代码如下:
func Parse() { CommandLine.Parse(os.Args[1:]) }
内部同样是调用 CommandLine
对象对应的方法,并且将除程序名称以外的命令行参数都传递到 Parse
方法中,Parse
方法定义如下:
func (f *FlagSet) Parse(arguments []string) error { f.parsed = true f.args = arguments for { seen, err := f.parseOne() if seen { continue } if err == nil { break } switch f.errorHandling { case ContinueOnError: return err case ExitOnError: if err == ErrHelp { os.Exit(0) } os.Exit(2) case PanicOnError: panic(err) } } return nil }
首先将 f.parsed
标记为 true
,在调用 f.Parsed()
方法时会被返归:
func (f *FlagSet) Parsed() bool { return f.parsed }
接着又将 arguments
保存在 f.args
属性中。
然后就是循环解析命令行参数的过程,每调用一次 f.parseOne()
解析一个标志,直到解析完成或碰到 error
退出程序。
parseOne
方法实现如下:
func (f *FlagSet) parseOne() (bool, error) { if len(f.args) == 0 { return false, nil } s := f.args[0] if len(s) < 2 || s[0] != '-' { return false, nil } numMinuses := 1 if s[1] == '-' { numMinuses++ if len(s) == 2 { // "--" terminates the flags f.args = f.args[1:] return false, nil } } name := s[numMinuses:] if len(name) == 0 || name[0] == '-' || name[0] == '=' { return false, f.failf("bad flag syntax: %s", s) } // it's a flag. does it have an argument? f.args = f.args[1:] hasValue := false value := "" for i := 1; i < len(name); i++ { // equals cannot be first if name[i] == '=' { value = name[i+1:] hasValue = true name = name[0:i] break } } m := f.formal flag, alreadythere := m[name] // BUG if !alreadythere { if name == "help" || name == "h" { // special case for nice help message. f.usage() return false, ErrHelp } return false, f.failf("flag provided but not defined: -%s", name) } if fv, ok := flag.Value.(boolFlag); ok && fv.IsBoolFlag() { // special case: doesn't need an arg if hasValue { if err := fv.Set(value); err != nil { return false, f.failf("invalid boolean value %q for -%s: %v", value, name, err) } } else { if err := fv.Set("true"); err != nil { return false, f.failf("invalid boolean flag %s: %v", name, err) } } } else { // It must have a value, which might be the next argument. if !hasValue && len(f.args) > 0 { // value is the next arg hasValue = true value, f.args = f.args[0], f.args[1:] } if !hasValue { return false, f.failf("flag needs an argument: -%s", name) } if err := flag.Value.Set(value); err != nil { return false, f.failf("invalid value %q for flag -%s: %v", value, name, err) } } if f.actual == nil { f.actual = make(map[string]*Flag) } f.actual[name] = flag return true, nil }
parseOne
代码轻微多一点,不过整体脉络还是比较清楚的。
首先对 f.args
参数入行了校验,接着提取标志前导符号 -
的个数放到 numMinuses
变量中,然后取出标志名并对标志语法做了检查。
接下来取出参数 value
,并且判定标志名是否为 -help/-h
,假如是则说明用户只想打印程序使用帮助信息,打印后 parseOne
会返归 ErrHelp
,上层的调用者 f.Parse
就会捕捉到 ErrHelp
,然后调用 os.Exit(0)
直接退出程序。
其中 f.usage()
实现了打印帮助信息的功能,内部详细实现这里就不讲解了,因为基本上是内容排版的实现,不是核心功能,感爱好可以自己尝试观一观。
最后就是根据参数值是否为 bool
类型分别入行参数绑定,将参数设置到对应的标志变量中,并将标志保存到 f.actual
中。
以上步骤都执行完成后,在执行 fmt.Printf("nFlag: %d\n", *nFlag)
时,就能够获取到 nFlag
被赋予的参数值了。
至此,flag 包源码的整体脉络都已经清楚了。
2.3其他代码
在我们的示例代码最后,还打印了 NFlag()
、NArg()
、Args()
、Arg(1)
几个函数的结果。
这几个函数实现非常简朴,代码如下:
func NFlag() int { return len(CommandLine.actual) } func NArg() int { return len(CommandLine.args) } func Args() []string { return CommandLine.args } func Arg(i int) string { return CommandLine.Arg(i) }
由于代码过于简朴,我就不入行解释了,相信通过上面的讲解,这几个函数的作用你也能做到一目了然。
flag 包还有一些其他类型,如 stringValue
、float64Value
,这些类型实现思路都是一样的,也不再一一讲解。
最后,flag 包其他附属的函数实现,不是主要脉络,留给读者自行查观学习。
3.总结
在开发命令行程序时,Go 标准库中的 flag 包是一个不错的选择。
本文先对 flag 包的基本使用入行了演示,接着又对源码入行了深度剖析。
flag 包支持三种方式定义标志,flag.Parse()
能够对命令行参数入行解析,解析成功后,就可以在代码中使用参数值了。
以上就是深进探究Golang中flag标准库的使用的具体内容,更多关于Golang flag标准库的资料请关注其它相关文章!