文章详情

短信预约-IT技能 免费直播动态提醒

请输入下面的图形验证码

提交验证

短信预约提醒成功

从源码的角度看Go语言Flag库如何解析命令行参数!

2024-12-02 23:50

关注

我上周五喝酒喝到晚上3点多,确实有点罩不住啊,整个周末都在休息和睡觉,文章鸽了几天,想不到就有两个人跑了。

不得不感叹一下,自媒体的太残酷了,时效就那么几天,断更就没人爱。你们说好了爱我的,爱呢?哼

昨晚就在写这篇文章了,没想到晚上又遇到发版本,确实不容易,且看且珍惜。

标准库 flag

命令行程序应该能打印出帮助信息,传递其他命令行参数,比如-h就是flag库的默认帮助参数。

  1. ./goapi -h 
  2. Usage of ./goapi: 
  3.   -debug 
  4.         is debug 
  5.   -ip string 
  6.         Input bind address (default "127.0.0.1"
  7.   -port int 
  8.         Input bind port (default 80) 
  9.   -version 
  10.         show version information 

goapi是我build出来的一个二进制go程序,上面所示的四个参数,是我自定义的。

按提示的方法,可以像这样使用参数。

  1. ./goapi -debug -ip 192.168.1.1 
  2. ./goapi -port 8080 
  3. ./goapi -version 

像上面-version这样的参数是bool类型的,只要指定了就会设置为true,不指定时为默认值,假如默认值是true,想指定为false要像下面这样显式的指定(因为源码里是这样写的)。

  1. ./goapi -version=false 

下面这几种格式都是兼容的

  1. -isbool    #同于 -isbool=true 
  2. -age=x     #-和等号 
  3. -age x     #-和空格 
  4. --age=x    #2个-和等号 
  5. --age x    #2个-和空格 

flag库绑定参数的过程很简单,格式为

  1. flag.(name string, value bool, usage string) *类型 

如下是详细的绑定方式:

  1. var ( 
  2.     showVersion = flag.Bool("version"false"show version information"
  3.     isDebug = flag.Bool("debug"false"is debug"
  4.     ip      = flag.String("ip""127.0.0.1""Input bind address"
  5.     port    = flag.Int("port", 80, "Input bind port"

可以定义任意类型的变量,比如可以表示是否debug模式、让它来输出版本信息、传入需要绑定的ip和端口等功能。

绑定完参数还没完,还得调用解析函数flag.Parse(),注意一定要在使用参数前调用哦,使用过程像下面这样:

  1. func main() { 
  2.  flag.Parse() 
  3.  if *showVersion { 
  4.   fmt.Println(version) 
  5.   os.Exit(0) 
  6.  } 
  7.  if *isDebug { 
  8.   fmt.Println("set log level: debug"
  9.  } 
  10.  fmt.Println(fmt.Sprintf("bind address: %s:%d successfully",*ip,*port)) 

全部放在main函数里,不太雅观,建议把这些单独放到一个包里,或者放在main函数的init()里,看起来不仅舒服,也便于阅读。

flag的简写方式

有时候可能我们要给某个全局配置变量赋值,flag提供了一种简写的方式,不用额外定义中间变量。像下面这样

  1. var ( 
  2.  ip          string 
  3.  port        int 
  4.  
  5. func init() { 
  6.  flag.StringVar(&ip, "ip""127.0.0.1""Input bind address(default: 127.0.0.1)"
  7.  flag.IntVar(&port, "port", 80, "Input bind port(default: 80)"
  8. func main() { 
  9.  flag.Parse() 
  10.  fmt.Println(fmt.Sprintf("bind address: %s:%d successfully", ip, port)) 

这样写可以省掉很多判断的代码,也避免了使用指针,命令行的使用方法还是一样的。

从源码来看flag如何解析参数

其实我们把之前的绑定方式打开来看,在源码里就是调用了xxVar函数,以Bool类型为例。

  1. func (f *FlagSet) Bool(name string, value bool, usage string) *bool { 
  2.  p := new(bool) 
  3.  f.BoolVar(p, name, value, usage) 
  4.  return p 

上面的代码用到了BoolVal函数,它的功能是把需要绑定的变量设置为默认值,并调用f.Var进一步处理,这里p是一个指针,所以只要改变指向的内容,就可以影响到外部绑定所用的变量:

  1. func (f *FlagSet) BoolVar(p *bool, name string, value bool, usage string) { 
  2.  f.Var(newBoolValue(value, p), name, usage) 
  3.  
  4. type boolValue bool 
  5.  
  6. func newBoolValue(val bool, p *bool) *boolValue { 
  7.  *p = val 
  8.  return (*boolValue)(p) 

f.Var函数的作用就是把参数封装成Flag,并合并到FlagSet中,下面的代码就是核心过程:

  1. func (f *FlagSet) Var(value Value, name string, usage string) { 
  2.  // Remember the default value as a string; it won't change. 
  3.  flag := &Flag{name, usage, value, value.String()} 
  4.  _, alreadythere := f.formal[name
  5.  if alreadythere { 
  6.   //...错误处理省略 
  7.  } 
  8.  if f.formal == nil { 
  9.   f.formal = make(map[string]*Flag) 
  10.  } 
  11.  f.formal[name] = flag 

FlagSet结构体中起作用的是formal map[string]*Flag类型,所以说,flag把程序中需要绑定的变量包装成一个字典,后面解析的时候再一一赋值。

我们已经知道了,在调用Parse的时候,会对参数解析并为变量赋值,使用时就可以得到真实值。展开看看它的代码

  1. func Parse() { 
  2.  // Ignore errors; CommandLine is set for ExitOnError. 
  3.  // 调用了FlagSet.Parse 
  4.  CommandLine.Parse(os.Args[1:]) 
  5. // 返回一个FlagSet 
  6. var CommandLine = NewFlagSet(os.Args[0], ExitOnError) 

Parse的代码里用到了一个,CommandLine共享变量,这就是内部库维护的FlagSet,所有的参数都会插到里面的变量地址向地址的指向赋值绑定。

上面提到FlagSet绑定的Parse函数,看看它的内容:

  1. func (f *FlagSet) Parse(arguments []string) error { 
  2.  f.parsed = true 
  3.  f.args = arguments 
  4.  for { 
  5.   seen, err := f.parseOne() 
  6.   if seen { continue } 
  7.   if err == nil {...} 
  8.   switch f.errorHandling { 
  9.   case ContinueOnError: return err 
  10.   case ExitOnError: 
  11.    if err == ErrHelp { os.Exit(0) } 
  12.    os.Exit(2) 
  13.   case PanicOnError: panic(err) 
  14.   } 
  15.  } 
  16.  return nil 

parseOne就是解析命令行输入绑定变量的过程了:

  1. func (f *FlagSet) parseOne() (bool, error) { 
  2.  //... 
  3.  s := f.args[0] 
  4.  //... 
  5.  if s[1] == '-' { ...} 
  6.  name := s[numMinuses:] 
  7.  if len(name) == 0 || name[0] == '-' || name[0] == '=' { 
  8.   return false, f.failf("bad flag syntax: %s", s) 
  9.  } 
  10.  
  11.  f.args = f.args[1:] 
  12.  //... 
  13.  m := f.formal 
  14.  flag, alreadythere := m[name] // BUG 
  15.  // ...如果不存在,或者需要输出帮助信息,则返回 
  16.  // ...设置真实值调用到 flag.Value.Set(value) 
  17.  if f.actual == nil { 
  18.   f.actual = make(map[string]*Flag) 
  19.  } 
  20.  f.actual[name] = flag 
  21.  return true, nil 

flag.Value.Set(value) 这里是设置数据真实值的代码,Value长这样

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

它被设计成一个接口,不同的数据类型自己实现这个接口,返回给用户的地址就是这个接口的实例数据,解析过程中,可以通过 Set 方法修改它的值,这个设计确实还挺巧妙的。

  1. func (b *boolValue) String() string { 
  2.   return strconv.FormatBool(bool(*b))  
  3. func (b *boolValue) Set(s string) error { 
  4.     v, err := strconv.ParseBool(s) 
  5.     if err != nil { 
  6.         err = errParse   
  7.     } 
  8.     *b = boolValue(v) 
  9.     return err 

从源码想到的拓展用法

flag的常用方法也学会了,基本原理也了解了,我怎么那么厉害。哈哈哈。

有没有注意到整个过程都围绕了FlagSet这个结构体,它是最核心的解析类。

在库内部提供了一个 *FlagSet 的实例对象 CommandLine,它通过NewFlagSet方法创建。并且对它的所有方法封装了一下直接对外。

官方的意思很明确了,说明我们可以用到它做些更高级的事情。先看看官方怎么用的。

  1. var CommandLine = NewFlagSet(os.Args[0], ExitOnError) 

可以看到调用的时候是传入命令行第一个参数,第二个参数表示报错时应该呈现怎样的错误。

那就意味着我们可以根据命令行第一个参数不同而呈现不同的表现!

我定义了两个参数foo或者bar,代表两个不同的指令集合,每个指令集匹配不同的命令参数,效果如下:

  1. $ ./subcommands  
  2. expected 'foo' or 'bar' subcommands 
  3.  
  4. $ ./subcommands foo -h 
  5. Usage of foo: 
  6.   -enable 
  7.         enable 
  8.          
  9. $./subcommands foo -enable 
  10. subcommand 'foo' 
  11.   enable: true 
  12.   tail: [] 

这是怎么实现的呢?其实就是用NewFlagSet方法创建多个FlagSet再分别绑定变量,如下:

  1. fooCmd := flag.NewFlagSet("foo", flag.ExitOnError) 
  2. fooEnable := fooCmd.Bool("enable"false"enable"
  3.  
  4. barCmd := flag.NewFlagSet("bar", flag.ExitOnError) 
  5. barLevel := barCmd.Int("level", 0, "level"
  6.  
  7. if len(os.Args) < 2 { 
  8.     fmt.Println("expected 'foo' or 'bar' subcommands"
  9.     os.Exit(1) 

然后根据第一个参数,判断应该匹配到哪个指令集:

  1. switch os.Args[1] { 
  2. case "foo"
  3.     fooCmd.Parse(os.Args[2:]) 
  4.     fmt.Println("subcommand 'foo'"
  5.     fmt.Println("  enable:", *fooEnable) 
  6.     fmt.Println("  tail:", fooCmd.Args()) 
  7. case "bar"
  8.     barCmd.Parse(os.Args[2:]) 
  9.     fmt.Println("subcommand 'bar'"
  10.     fmt.Println("  level:", *barLevel) 
  11.     fmt.Println("  tail:", barCmd.Args()) 
  12. default
  13.     fmt.Println("expected 'foo' or 'bar' subcommands"
  14.     os.Exit(1) 

补充:使用NewFlagSet时,flag 提供三种错误处理的方式:

小结

通过本节我们了解到了标准库flag的使用方法,参数变量绑定的两种方式,还通过源码解析了内部实现是如何的巧妙。

我们还使用源码暴露出来的函数,接收不同参数匹配不同指令集,这种方式可以让应用呈现完成不同的功能;

我想到的是用来通过环境变量改变命令用法、或者让程序复用大段逻辑呈现不同作用时使用。

但现在微服务那么流行,大多功能集成在一个服务里是不科学的,如果有重复代码应该提炼成共同模块才是王道。

你还想到能哪些使用场景呢?

引用

源码包 https://golang.org/src/flag/flag.go

命令行子命令 https://gobyexample-cn.github.io/command-line-subcommands

命令行解析库 flag https://segmentfault.com/a/1190000021143456

腾讯云文档flag https://cloud.tencent.com/developer/section/1141707#stage-100022105

往期精彩回顾

本文转载自微信公众号「机智的程序员小熊」,可以通过以下二维码关注。转载本文请联系机智的程序员小熊公众号。

 

来源:机智的程序员小熊内容投诉

免责声明:

① 本站未注明“稿件来源”的信息均来自网络整理。其文字、图片和音视频稿件的所属权归原作者所有。本站收集整理出于非商业性的教育和科研之目的,并不意味着本站赞同其观点或证实其内容的真实性。仅作为临时的测试数据,供内部测试之用。本站并未授权任何人以任何方式主动获取本站任何信息。

② 本站未注明“稿件来源”的临时测试数据将在测试完成后最终做删除处理。有问题或投稿请发送至: 邮箱/279061341@qq.com QQ/279061341

软考中级精品资料免费领

  • 历年真题答案解析
  • 备考技巧名师总结
  • 高频考点精准押题
  • 2024年上半年信息系统项目管理师第二批次真题及答案解析(完整版)

    难度     813人已做
    查看
  • 【考后总结】2024年5月26日信息系统项目管理师第2批次考情分析

    难度     354人已做
    查看
  • 【考后总结】2024年5月25日信息系统项目管理师第1批次考情分析

    难度     318人已做
    查看
  • 2024年上半年软考高项第一、二批次真题考点汇总(完整版)

    难度     435人已做
    查看
  • 2024年上半年系统架构设计师考试综合知识真题

    难度     224人已做
    查看

相关文章

发现更多好内容

猜你喜欢

AI推送时光机
位置:首页-资讯-后端开发
咦!没有更多了?去看看其它编程学习网 内容吧
首页课程
资料下载
问答资讯