flag 包学习笔记概要:

  • 如何定义命令行参数,即 flags ?
  • flags 解析规则及语法格式 ?
  • flag 包有哪些重要的变量、函数以及类型 ?
  • 如何绑定用户自定义的数据类型作为参数值的类型?

1. Flag

Flag:解析命令行参数,其实现方法是将命令行标志与程序中的某一变量进行绑定,开发人员使用变量在程序中进行逻辑处理。

2. 定义flags

有三种方式定义 flags

2.1. 方式一

通过 flag.Type(name, defValue, usage) *Type {} 方法,该方法返回一个对应数据类型的指针, 其中 Type 为:String、Int、Bool等。

1
2
3
 var port = flag.String("port", "80", "http port.(default:80)")
 var num = flag.Int()
 // ...

2.2. 方式二

通过 flag.TypeVar(&flagvar, name, defValue, usage) Type {} 方法将flag绑定到一个变量, 该方法返回结果为值类型

1
2
3
4
5
var num int
func init() {
    flag.IntVar(&num, "num", 1234, "help message for flagname")
    // ...
}

2.3. 方式三

通过 flag.Var() 方法绑定自定义类型,自定义类型需要实现Value接口(要求receiver是指针)

1
flag.Var(&flagVal, "name", "help message for flagname")

注:这种方式是没有提供默认值的,所以默认值就是类型的零值。(备注:具体如何实现自定义类型绑定会在后面介绍)

3. 命令行参数解析及语法格式

命令行参数定义好了,该如何解析呢?

3.1. 解析

在所有flag定义完成之后,调用 flag.Parse() 解析

  • 对该函数的调用必须在所有命令参数存储载体的声明(即:上面对 num 和 port的声明)和设置(即: 上面调用 flag.IntVar()) 之后,并且在读取任何命令参数值之前进行。正因为如此,我们最好把 flag.Parse() 放在 main 函数第一行.
  • 解析函数将会在碰到第一个 非 flag 命令行参数 时停止,非flag命令行参数是指不满足命令行语法的参数,如命令行参数为 cmd –flag=true abc 则第一个非flag 命令行参数为 “abc”

注:调用 Parse 解析后,就可以直接使用 flag 本身(指针类型)或者绑定的变量了(值类型), 还可通过 flag.Args(), flag.Arg(i) 来获取非flag命令行参数

3.2. 命令行语法

命令行语法格式:

1
2
3
4
5
6
7
-flag       // 只支持bool类型

-flag xxx   // 使用空格,一个 - 符号
--flag xxx  // 使用空格,两个 - 符号

-flag=xxx   // 使用等号,一个 - 符号
--flag=xxx  // 使用等号两个 - 符号

注:布尔类型的参数防止解析时的二义性,应该使用等号的方式指定。

4. 变量、类型和函数

4.1. 变量

  • flag.ErrHelp:该错误类型用于当命令行指定了 –help 参数但没有定义时。

  • flag.Usage:这是一个函数,用于输出所有定义了的命令行参数和帮助信息(Usage Message)。 当命令行参数解析出错,该函数会被调用。我们可通过 flag.Usage = func() {} 指定自定义的Usage。

    • flag.Usage 的类型是func(),即一种无参数声明且无结果声明的函数类型

注意,对flag.Usage的赋值必须在调用 flag.Parse() 函数之前

  • flag.CommandLine: 类型 flag.FlagSet 的实例, 相当于默认情况下的命令参数容器.

4.2. 函数

4.3. 类型

4.3.1. ErrorHandling

1
type ErrorHandling int 

该类型定义了在参数解析出错时,处理错误定义的三个常量

1
2
3
4
5
const (
    ContinueOnError ErrorHandling = iota
    ExitOnError
    PanicOnError
)

4.3.2. FlagSet

FlagSet 是该包的核心结构

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
// A FlagSet represents a set of defined flags.
type FlagSet struct {
    // Usage is the function called when an error occurs while parsing flags.
    // The field is a function (not a method) that may be changed to point to
    // a custom error handler.
    Usage func()
    name          string            // FlagSet的名字。commandLine 给的是os.Args[0]
    parsed        bool              // 是否执行过Parse()
    actual        map[string]*Flag  // 存放实际传递了的参数(即命令行参数)
    formal        map[string]*Flag  // 存放所有已定义命令行参数
    args          []string          // arguments after flags 存放非标志的参数列表(即标志后面的参数)
    exitOnError  bool               // does the program exit if there's an error?
    errorHandling ErrorHandling     // 当解析出错时,处理错误的方式
    output        io.Writer         // nil means stderr; use out() accessor
}

该类型同时提供了一系列的方法集合 <MethodSet>, 通过这些方式来实现CLI的灵活处理

4.3.3. Flag

Flag 结构如下

1
2
3
4
5
6
type Flag struct {
	Name     string // name as it appears on command line
	Usage    string // help message
	Value    Value  // value as set
	DefValue string // default value (as text); for usage message
}

4.3.4. Value

Value定义:

1
2
3
4
type Value interface {
  String() string
  Set(string) error
}

4.4. 主要类型的方法

4.4.1. 实例化方式:flag.NewFlagSet()

flag.NewFlagSet() 用于实例化 FlagSet。例如:预定义的 FlagSet实例 subCommand 的定义方式:

1
2
subCommand := flag.NewFlagSet("push", ExitOnError)
subCommand.Parse(os.Args[1:])   // os.Args[1:] 指的就是我们给定的那些命令参数

注:可用于定义子命令

  • 由于FlagSet的字段没有 export,其他方式获得FlagSet的实例后,比如 FlagSet{}、new(FlagSet), 应该调用 Init() 方法初始化 name和errorHandling
  • 这样做的好处依然是更灵活地定制命令参数容器。但更重要的是,你的定制完全不会影响到那个全局变量flag.CommandLine

4.4.2. 解析参数:flag.Parse()

该方法应该在flag参数定义后, 具体参数值被访问之前调用。

4.4.2.1. 解析停止

  • 第一个non-flag参数

    当遇到单独的一个”-”或不是”-”开始时,会停止解析

  • 两个连续的 ‘-’

5. 将 flag 绑定用户自定义类型

首先,用户自定义类型需要重新实现 flag.Value 接口

flag.Value 接口定义如下:

1
2
3
4
type Value interface{
    String() string
    Set(string) error
}

Note:

  • String(): 方法输出结果为一个字符串值
  • Set(string) error: 方法定义了如何解析flag参数指定的值

例如:解析数据库集群服务器的地址到一个 slice 中 ?

5.1. 重写 flag.Value

  • 自定义flag参数值接收类型
1
type addrs []string
  • 实现 String() string 方法
1
2
3
func(i *addrs) String() string {
    return fmt.Sprintf("%v", *i)
}
  • 实现 Set(string) error 方法
1
2
3
4
func (i *addrs) Set(value string) error {
    *i = append(*i, value)
    return nil
}

5.2. 解析

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
var DBAdds addrs

func init() {
    flag.Var(&DBAdds, "db_addr", "database cluster server address")
    flag.Parse()
} 

func main() {
    fmt.Println(DBAdds)
}
1
2
3
$ ./app -db_addr 192.168.1.100 -db_addr 192.168.1.101 -db_addr 192.168.1.102

# output: 

完整代码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
package main

import (
    "fmt"
    "flag"
)

// 自定义flag参数值接收类型
type addrs []string

// 实现 String() string 方法
func(i *addrs) String() string {
    return fmt.Sprintf("%v", *i)
}

// 实现 Set(string) error 方法
func (i *addrs) Set(value string) error {
    *i = append(*i, value)
    return nil
}

// DBAdds 声明接收命令行参数的变量
var DBAdds addrs

// 定义 flag 并完成解析
func init() {
    flag.Var(&DBAdds, "db_addr", "database cluster server address")
    flag.Parse()
} 

func main() {
    fmt.Println(DBAdds)
}

6. See Also

Thanks to the authors 🙂