Go 标准库 flag 的一个坑

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

发表回复

您的电子邮箱地址不会被公开。 必填项已用 * 标注