上周五晚上喝到3点多,真的是憋不住了。我整个周末都在休息和睡觉。写了几天,没想到,两个人跑了。不得不感叹,自媒体太残忍了,时效只有几天,破了也没人爱。你答应爱我,那爱情呢?嗯,我昨晚正在写这篇文章。没想到晚上又遇到了另一个版本。这真的不容易。让我们拭目以待。Standardlibraryflagflag的简写方式从源码看flag如何解析参数从源码看扩展用法总结引用了标准库flag命令行程序应该可以打印帮助信息和传递其他命令行参数,比如-h是默认标志库帮助参数。./goapi-hUsageof./goapi:-debugisdebug-ipstringInputbindaddress(default"127.0.0.1")-portintInputbindport(default80)-versionshowversioninformationgoapi是我构建的二进制go程序。上面显示的四个参数是我自定义的。正如建议的那样,参数可以这样使用。./goapi-debug-ip192.168.1.1./goapi-port8080./goapi-version上面-version这样的参数都是bool类型的,只要指定了就会被设置为true,默认就是如果未指定,则为值。默认值为true,如果要指定为false,必须如下显式指定(因为源码里写的)。./goapi-version=false以下格式兼容-isbool#sameas-isbool=true-age=x#-andequalsign-agex#-andspace--age=x#2-andetc.No.--agex#2-andspace参数绑定到flag库的过程很简单,格式为flag.(namestring,valuebool,usagestring)*详细绑定方法如下:var(showVersion=flag.Bool("version",false,"showversioninformation")isDebug=flag.Bool("debug",false,"isdebug")ip=flag.String("ip","127.0.0.1","Inputbindaddress")port=flag.Int("port",80,"Inputbindport"))可以定义任意类型的变量,比如是否指示是否调试模式,让它输出版本信息,传入需要绑定的ip和端口等。之后绑定参数,必须调用解析函数flag.Parse()。请务必在使用参数之前调用它。使用过程如下:funcmain(){flag.Parse()if*showVersion{fmt.Println(version)os.Exit(0)}if*isDebug{fmt.Println("setloglevel:debug")}fmt.println(fmt.Sprintf("bindaddress:%s:%dsuccessfully",*ip,*port))}都放在了main函数里面,不太优雅。建议将这些放在单独的包中,或者放在main函数的init()中。它看起来不仅舒适,而且易于阅读。flag的简写方式有时候我们可能想给一个全局配置变量赋值。flag提供了一种简写方式,无需额外定义中间变量。如下所示var(ipstringportint)funcinit(){flag.StringVar(&ip,"ip","127.0.0.1","Inputbindaddress(default:127.0.0.1)")flag.IntVar(&port,"port",80,"输入bindport(default:80)")}funcmain(){flag.Parse()fmt.Println(fmt.Sprintf("bindaddress:%s:%dsuccessfully",ip,port))}可以省去很多判断的代码也避免了指针的使用,使用命令行的方法还是一样的。从源码看flag是如何解析参数的其实我们打开前面的绑定方法可以看到源码中调用了xxVar函数,以Bool类型为例。func(f*FlagSet)Bool(namestring,valuebool,usagestring)*bool{p:=new(bool)f.BoolVar(p,name,value,usage)returnp}上面代码使用了BoolVal函数,它的作用是Set将要绑定的变量作为默认值,调用f.Var做进一步处理,其中p是一个指针,所以只要指向的内容发生变化,对外绑定的变量就会受到影响:func(f*FlagSet)BoolVar(p*bool,namestring,valuebool,usagestring){f.Var(newBool??Value(value,p),name,usage)}typeboolValueboolfuncnewBool??Value(valbool,p*bool)*boolValue{*p=valreturn(*boolValue)(p)}newBool??Value函数可以得到一个boolValue类型,它是由bool类型改名而来的。在这个包中可以用作参数的所有类型都有这样的定义。标志包的设计有两种重要的类型。Flag和FlagSet分别表示一个特定的参数和一个不重复的参数集。f.Var函数的作用是将参数封装到Flag中,合并到FlagSet中。下面代码是核心流程:func(f*FlagSet)Var(valueValue,namestring,usagestring){//记住defaultvalueasastring;itwon'tchange.flag:=&Flag{name,usage,value,value.String()}_,alreadythere:=f.formal[name]ifalreadythere{//...省略错误处理}iff.formal==nil{f.formal=make(map[string]*Flag)}f.formal[name]=flag}FlagSet结构工作在形式化的map[string]*Flag类型,所以,flag将程序中需要绑定的变量打包成一个Dictionary,然后在后面解析的时候一个一个赋值。我们已经知道调用Parse时,参数会被解析并赋值给变量,使用时才能获取到真正的值。展开看它的代码funcParse(){//Ignoreerrors;CommandLineissetforExitOnError.//调用了FlagSet.ParseCommandLine.Parse(os.Args[1:])}//返回一个FlagSetvarCommandLine=NewFlagSet(os.Args[0],ExitOnError)Parse代码使用了一个,CommandLine共享变量,也就是内部库维护的FlagSet,所有的参数都会插入到变量address中,进行地址赋值绑定。上面提到绑定FlagSet的Parse函数,看它的内容:func(f*FlagSet)Parse(arguments[]string)error{f.parsed=truef.args=argumentsfor{seen,err:=f.parseOne()ifseen{continue}iferr==nil{...}switchf.errorHandling{caseContinueOnError:returnerrcaseExitOnError:iferr==ErrHelp{os.Exit(0)}os.Exit(2)casePanicOnError:panic(err)}}returnnil}内容上面的函数太长了,所以我把它缩短了一点。可以看出解析过程实际上是多次调用parseOne()。它的作用是将命令行参数一个一个遍历,绑定到Flag上,就像翻页一样。使用switch来处理错误,决定退出码还是直接panic。parseOne是解析命令行输入绑定变量的过程:func(f*FlagSet)parseOne()(bool,error){//...s:=f.args[0]//...ifs[1]=='-'{...}name:=s[numMinuses:]iflen(name)==0||name[0]=='-'||name[0]=='='{returnfalse,f.failf("badflagsyntax:%s",s)}f.args=f.args[1:]//...m:=f.formalflag,alreadythere:=m[name]//BUG//...如果不存在,或者需要输出帮助信息,则返回//...设置实际值,调用flag.Value.Set(value)iff.actual==nil{f.actual=make(map[string]*Flag)}f.actual[name]=flagreturntrue,nil}parseOne内部解析一个入参,判断入参格式,获取参数值。解析过程是将程序参数一一取出,判断-、=得到参数和参数值。解析后,找出上述正式映射中是否存在该参数,并设置真实值。将已经设置了真实值的参数放到f.actual映射中,以备他用。我省略了一些错误处理和详细代码,有兴趣的可以自行查看源码。其实就是将参数一个一个解析,设置为对应指针变量的指针,从而使返回值发生变化。flag.Value.Set(value)这里是设置数据真实值的代码,Value看起来像这样typeValueinterface{String()stringSet(string)error}它被设计成一个接口,不同的数据类型通过自己,返回给用户接口的地址,就是这个接口的实例数据。在解析过程中,可以通过Set方法修改它的值。这个设计确实相当巧妙。func(b*boolValue)String()string{returnstrconv.FormatBool(bool(*b))}func(b*boolValue)Set(sstring)error{v,err:=strconv.ParseBool(s)iferr!=nil{err=errParse}*b=boolValue(v)returnerr}从源码中,我也学习了扩展flags用法的常用方法,也了解了基本原理。我怎么可以这么好。哈哈哈。大家有没有注意到,整个过程都是围绕着FlagSet这个结构体进行的,它是核心的解析类。库内部提供了一个*FlagSet的实例对象CommandLine,由NewFlagSet方法创建。并将其所有的方法直接对外封装。官方的意思很明确,说明我们可以用它来做更高级的事情。来看看官方是如何使用的。varCommandLine=NewFlagSet(os.Args[0],ExitOnError)可以看到调用的时候传入了命令行的第一个参数,第二个参数表示报错时应该显示什么样的错误。也就是说我们可以根据命令行的第一个参数来表现不同的表现!我定义了两个参数foo或者bar,分别代表两个不同的指令集,每个指令集对应不同的命令参数。效果如下:$./subcommandsexpected'foo'or'bar'subcommands$./subcommandsfoo-hUsageoffoo:-enableenable$./subcommandsfoo-enablesubcommand'foo'enable:truetail:[]这个是怎么实现的呢?其实就是用NewFlagSet方法创建多个FlagSet,分别绑定变量,如下:enable")barCmd:=flag.NewFlagSet("bar",flag.ExitOnError)barLevel:=barCmd.Int("level",0,"level")iflen(os.Args)<2{fmt.Println("预期'foo'or'bar'subcommands")os.Exit(1)}定义了两个不同的FlagSet,接受foo或bar参数。退出绑定错误。为每个FlagSet分别绑定要解析的变量。如果判断命令行的输入参数小于2则退出(因为第0个参数本身就是程序名)。然后根据第一个参数,判断应该匹配哪个指令集:switchos.Args[1]{case"foo":fooCmd.Parse(os.Args[2:])fmt.Println("subcommand'foo'")fmt.Println("enable:",*fooEnable)fmt.Println("tail:",fooCmd.Args())case"bar":barCmd.Parse(os.Args[2:])fmt.Println("subcommand'bar'")fmt.Println("level:",*barLevel)fmt.Println("tail:",barCmd.Args())default:fmt.Println("expected'foo'or'bar'subcommands")os.Exit(1)}使用switch切换命令行参数,绑定不同的变量。输出对应不同变量的不同表现。x.Args()可以打印其他不匹配的参数。补充:使用NewFlagSet时,flag提供三种错误处理方式:ContinueOnError:通过Parse的返回值返回错误ExitOnError:调用os.Exit(2)直接退出程序,这是默认的处理方式PanicOnError:调用panictothrowanerror总结通过这一节,我们学习了标准库flag的使用,绑定参数变量的两种方式,也通过源码分析了内部实现有多么巧妙。我们也使用源码暴露的函数来接收不同的参数来匹配不同的指令集。这样,应用程序可以执行不同的功能;我想到的是通过环境变量改变命令的用法,或者让程序重用大段逻辑。用于不同的角色。但是现在微服务这么流行,把大部分功能都集成到一个服务中是不科学的。如果有重复的代码,应该将它们细化为公共模块。您还想到了哪些其他使用场景?参考源码包https://golang.org/src/flag/flag.go命令行子命令https://gobyexample-cn.github.io/command-line-subcommands命令行解析库flaghttps://segmentfault.com/a/1190000021143456腾讯云文档flaghttps://cloud.tencent.com/developer/section/1141707#stage-100022105之前的精彩回顾《程序员熊》,可以通过以下二维码关注。转载本文请联系机智的程序员小熊公众号。
