在最近的Go 1.16版本中,我最喜欢的一个特性是在flag包中增加了一个很受欢迎的功能:flag. func()函数。使得在应用程序中解析自定义命令行参数变得更加容易。
例如,假设你想将--pause=10s这个命令行参数解析为time.Duration类型,或者将--urls="http://example.com http://example.org"直接解析到[]string切片中,如果在以前你有两种选择。你可以创建一个自定义类型来实现flag.Value接口,或者使用第三方包例如pflag。
但现在flag.Func()函数提供了一个非常简单轻量的选择。在本文中,我们通过几个例子来看看怎么在代码中使用这个函数。
解析自定义参数类型
为了演示它是如何工作的,让我们从上面给出的两个示例开始,并创建一个示例程序,该程序接受一个url列表,然后在它们之间进行暂停一段时间并打印它们。类似于:
$ go run . --pause=3s --urls="http://example.com http://example.org http://example.net"
2021/03/08 08:16:04 http://example.com
2021/03/08 08:16:07 http://example.org
2021/03/08 08:16:10 http://example.net
要做到这一点,我们需要做两件事:
- 将--pause的值从人为可读的字符串例如200ms、5s或者10m转为Go的time.Duration类型。我们可以使用time.ParseDuration()函数实现。
- 将--urls参数转换为字符串切片,这样就可以循环遍历。strings.Field函数非常适合实现这个任务。
我们结合flag.Func()使用如下:
package main
import (
"flag"
"log"
"strings"
"time"
)
func main() {
// 首先需要定义所需类型来存放命令参数,需要设定默认值,如果
// 命令行参数未提供对应值,运行时可使用默认值
var (
urls []string //切片变量定义,默认为空
pause time.Duration = time.Second //暂停时间默认1s
)
// flag.Func函数需要三个参数: 命令行参数名称, 参数描述和一个
// func(string) error匿名函数,在程序运行时调用这个函数处理命令行参数
// 并将其赋值给需要的变量。在这里例子中,我们用strings.Fields()函数
// 根据空格将字符串切分并将结果赋值给定义的urls变量,函数返回nil
// 表示解析无任何错误。
flag.Func("urls", "List of URLs to print", func(flagValue string) error {
urls = strings.Fields(flagValue)
return nil
})
// 同样,我们可以做同样的事情来解析暂停时间。time.ParseDuration()
// 函数可能会在这里抛出一个错误,因此我们要确保从函数中返回错误。
flag.Func("pause", "Duration to pause between printing URLs", func(flagValue string) error {
var err error
pause, err = time.ParseDuration(flagValue)
return err
})
// 重要的是,调用flag.Parse()来触发参数的解析。
flag.Parse()
// 输出urls,在每次迭代之间暂停。
for _, u := range urls {
log.Println(u)
time.Sleep(pause)
}
}
如果您尝试运行这个程序,您应该会发现参数被解析,并且工作方式与您期望的一样。例如:
$ go run . --pause=500ms --urls="http://example.com http://example.org http://example.net"
2021/03/08 08:22:33 http://example.com
2021/03/08 08:22:34 http://example.org
2021/03/08 08:22:34 http://example.net
然而,如果您提供了一个无效的命令行参数,在一个flag. func()函数中报错,Go将自动显示相应的错误信息并退出。例如:
$ go run . --pause=500xx --urls="http://example.com http://example.org http://example.net"
invalid value "500xx" for flag -pause: time: unknown unit "xx" in duration "500xx"
Usage of /tmp/go-build3141872390/b001/exe/example.text:
-pause value
Duration to pause between printing URLs
-urls value
List of URLs to print
exit status 2
需要注意的是如果某个命令行参数没有提供,相应的flag.Func()函数将不会执行。这意味着不能在flag.Func()中对相应变量设定默认值,因此下面的代码不会生效:
flag.Func("pause", "Duration to pause between printing URLs (default 1s)", func(flagValue string) error {
// 不要这样做!如果参数值为""则不会调用此函数.
if flagValue == "" {
pause = time.Second
return nil
}
var err error
pause, err = time.ParseDuration(flagValue)
return err
})
不过,从好的方面来说,flag.Func()函数中所包含的代码没有限制,所以如果你愿意,可以用它来做更多的事情,可将urls解析为[]*url.URL切片而不是[]string。如下所示:
var (
urls []*url.URL
pause time.Duration = time.Second
)
flag.Func("urls", "List of URLs to print", func(flagValue string) error {
for _, u := range strings.Fields(flagValue) {
parsedURL, err := url.Parse(u)
if err != nil {
return err
}
urls = append(urls, parsedURL)
}
return nil
})
校验命令行参数值
flag.Func()函数提供了校验命令行参数内容的机会。例如,我们的应用程序有一个environment参数,你需要限制该参数只能赋值为development、staging和production三个值。
要实现这个功能,你可以实现类似下面flag.Func()函数
package main
import (
"errors"
"flag"
"fmt"
)
func main() {
var (
environment string = "development"
)
flag.Func("environment", "Operating environment", func(flagValue string) error {
for _, allowedValue := range []string{"development", "staging", "production"} {
if flagValue == allowedValue {
environment = flagValue
return nil
}
}
return errors.New(`must be one of "development", "staging" or "production"`)
})
flag.Parse()
fmt.Printf("The operating environment is: %s\n", environment)
}
使用flag.Func创建帮助函数
如果您发现自己在flag.Func()函数中重复相同的代码,或者逻辑变得过于复杂,可以将其分解为一个可重用的帮助函数。例如,我们可以重写上面的例子,通过泛型enumFlag()函数来处理--environment参数,如下所示:
package main
import (
"flag"
"fmt"
)
func main() {
var (
environment string = "development"
)
enumFlag(&environment, "environment", []string{"development", "staging", "production"}, "Operating environment")
flag.Parse()
fmt.Printf("The operating environment is: %s\n", environment)
}
func enumFlag(target *string, name string, safelist []string, usage string) {
flag.Func(name, usage, func(flagValue string) error {
for _, allowedValue := range safelist {
if flagValue == allowedValue {
*target = flagValue
return nil
}
}
return fmt.Errorf("must be one of %v", safelist)
})
}