Go 标准库 flag 的一个坑
最近整理工程代码,碰到个flag库的一个小坑。工程服务通过不同参数,处理做不同的业务。为了简化说明,这里假定工程启动参数为 x, y, z 这三个参数(参数是其他类型和命名)。
原来代码这样:
func main() {
var x, y, z int64
flag.Int64Var(&x, "x", 0, "x var")
flag.Int64Var(&y, "y", 0, "y var")
flag.Int64Var(&z, "z", 0, "z var")
flag.Parse()
fmt.Printf("x:%d;y:%d;z:%d.\n", x, y, z) // -x 1 -y 2 -z 3 -> x:1;y:2;z:3.
}
x 是个type,根据不同的值走不同的 job 类型。
某日按需求,增加 -p 参数,同时整理下代码,发现 -z 不是必须的,算是个冗余参数,可以优化摘除。三两下修改好了,调整后代码类似如下:
func main() {
var x, y, p int64
flag.Int64Var(&x, "x", 0, "x var")
flag.Int64Var(&y, "y", 0, "y var")
flag.Int64Var(&p, "p", 0, "p var")
flag.Parse()
fmt.Printf("x:%d;y:%d;p:%d.\n", x, y, p) // -x 1 -y 2 -p 3 -> x:1;y:2;p:3.
}
测试跑起来也没有问题。但突然想起不少部署的服务有 -z 参数来拉起服务,跑下”go run task -x 1 -y 2 -z 3″,程序错误退出,酱紫:
flag provided but not defined: -z
-x int
x var
-y int
y var
-p int
p var
exit status 2
也就是说程序打印了一个usage 说明后退出了,因为z 没有接收解析。但是对于参数z,该参数是描述性的,非必要的。本期望程序能够无视z,能解析 p 和 x 就可以了,但是程序却退出了,不是预期。部署在各处的服务,大都带着-z 参数。这些服务是通过jenkins 自动部署代码到各个机器的。想挨个去摘除这个命令,是不太踏实的,如果漏了某个服务没摘掉,一旦代码部署后,该服务就跑不起来了,那就麻烦大了。真坑~得想想办法。
咋办?期望是碰到z,忽略就好,程序能继续跑。
查看flag 包源码Parse的实现(https://github.com/golang/go/blob/master/src/flag/flag.go) 有部分代码:
func Parse() {
// Ignore errors; CommandLine is set for ExitOnError.
CommandLine.Parse(os.Args[1:])
}
func (f *FlagSet) Parse(arguments []string) error {
// ....
for {
seen, err := f.parseOne()
// ...
switch f.errorHandling {
case ContinueOnError:
return err
case ExitOnError:
if err == ErrHelp {
os.Exit(0)
}
os.Exit(2) // (2)
case PanicOnError:
panic(err)
}
}
return nil
}
var CommandLine = NewFlagSet(os.Args[0], ExitOnError) // (1)
在上面代码中,flag.Parse 进入 CommandLine.Parse(), 之后进入f.parseOne, 在解析z 时候,由于没有设置接收解析该值,进入了switch f.errorHandling 流程。f.errorHandling 在上面代码(1)处构造CommandLine 时候指定了ExitOnError,所以在(2)处exit(2)程序退出了。
看了源码,我们如何调整服务代码,使得能够无痛升级?留意到f.errorHandling 如果是ContinueOnError,函数直接返回错误,程序处理下错误后能够继续跑。但是,没有看到 有接口设置这个 errorHandling。突然注意到这个CommandLine 是大写的,public 的,我们服务启动时候设置下就可。再调整下代码,如下:
func main() {
// flag.CommandLine 这个一定要在 Int64Var 前面,否则Int64Var调用会无效掉
flag.CommandLine = flag.NewFlagSet(os.Args[0], flag.ContinueOnError)
var x, y, p int64
flag.Int64Var(&x, "x", 0, "x var")
flag.Int64Var(&y, "y", 0, "y var")
flag.Int64Var(&p, "p", 0, "p var")
flag.Parse()
fmt.Printf("x:%d;\ny:%d;\np:%d.\n", x, y, p) // -x 1 -y 2 -z 3 -> x:1;y:2;p:0.
}
也就是在程序前面设置下 flag.CommandLine = flag.NewFlagSet(os.Args[0], flag.ContinueOnError)
, 这样服务对于“go run task -x 1 -y 2 -z 3”也能跑起来了。虽然也打印了日志“flag provided but not defined: -z” 提示,但对于老部署的服务,能达到预期运行了。
需要注意的是,NewFlagSet 设置一定要在 flag.Int64Var 之前,否则 flag.Int64Var 设置x,y,在后,依然会 flag provided but not defined: -x。
该问题算告一段落。
思考,如果部署的服务z 后面还有必选参数k,如“-x 1 -y 2 -z 3 -k 4”,那么k 依然还是解析不了的,因为flag.Parse() 解析z时候,就 error 了,后面的参数是解析不了了。对于某服务如果一定以来k 的,就不能随便摘除z 这个冗余参数了,如果还想摘除,得写不少代码来做兼容。这样,就不如保留这个冗余z,或者不如自己手动解析参数算了。
得一些启示:函数或者程序,对于非必要参数,不要塞在里面,或者尽量排在后面。这对代码的重构和维护,会方便很多。
(全文完)
(欢迎转载本站文章,但请注明作者和出处 云域 – Yuccn )