前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
MCP广场
社区首页 >专栏 >从源码的角度看Go语言flag库如何解析命令行参数!

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

作者头像
机智的程序员小熊
发布于 2021-08-12 03:41:05
发布于 2021-08-12 03:41:05
82000
代码可运行
举报
文章被收录于专栏:技术面面观技术面面观
运行总次数:0
代码可运行

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

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

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

  • 标准库 flag
  • flag的简写方式
  • 从源码来看flag如何解析参数
  • 从源码想到的拓展用法
  • 小结
  • 引用
  • 往期精彩回顾

标准库 flag

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

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
./goapi -h
Usage of ./goapi:
  -debug
        is debug
  -ip string
        Input bind address (default "127.0.0.1")
  -port int
        Input bind port (default 80)
  -version
        show version information

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

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

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
./goapi -debug -ip 192.168.1.1
./goapi -port 8080
./goapi -version

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

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
./goapi -version=false

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

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
-isbool    #同于 -isbool=true
-age=x     #-和等号
-age x     #-和空格
--age=x    #2-和等号
--age x    #2-和空格

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

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
flag.(name string, value bool, usage string) *类型

如下是详细的绑定方式:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
var (
    showVersion = flag.Bool("version", false, "show version information")
    isDebug = flag.Bool("debug", false, "is debug")
    ip      = flag.String("ip", "127.0.0.1", "Input bind address")
    port    = flag.Int("port", 80, "Input bind port")
)

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

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

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
func main() {
 flag.Parse()
 if *showVersion {
  fmt.Println(version)
  os.Exit(0)
 }
 if *isDebug {
  fmt.Println("set log level: debug")
 }
 fmt.Println(fmt.Sprintf("bind address: %s:%d successfully",*ip,*port))
}

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

flag的简写方式

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

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
var (
 ip          string
 port        int
)

func init() {
 flag.StringVar(&ip, "ip", "127.0.0.1", "Input bind address(default: 127.0.0.1)")
 flag.IntVar(&port, "port", 80, "Input bind port(default: 80)")
}
func main() {
 flag.Parse()
 fmt.Println(fmt.Sprintf("bind address: %s:%d successfully", ip, port))
}

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

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

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

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
func (f *FlagSet) Bool(name string, value bool, usage string) *bool {
 p := new(bool)
 f.BoolVar(p, name, value, usage)
 return p
}

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

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
func (f *FlagSet) BoolVar(p *bool, name string, value bool, usage string) {
 f.Var(newBoolValue(value, p), name, usage)
}

type boolValue bool

func newBoolValue(val bool, p *bool) *boolValue {
 *p = val
 return (*boolValue)(p)
}
  • newBoolValue 函数可以得到一个boolValue类型,它是bool类型重命名的。在此包中所有可作为参数的类型都有这样的定义。
  • flag包的设计中有两个重要的类型,FlagFlagSet分别表示某个特定的参数,和一个无重复的参数集合。

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

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
func (f *FlagSet) Var(value Value, name string, usage string) {
 // Remember the default value as a string; it won't change.
 flag := &Flag{name, usage, value, value.String()}
 _, alreadythere := f.formal[name]
 if alreadythere {
  //...错误处理省略
 }
 if f.formal == nil {
  f.formal = make(map[string]*Flag)
 }
 f.formal[name] = flag
}

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

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

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
func Parse() {
 // Ignore errors; CommandLine is set for ExitOnError.
 // 调用了FlagSet.Parse
 CommandLine.Parse(os.Args[1:])
}
// 返回一个FlagSet
var CommandLine = NewFlagSet(os.Args[0], ExitOnError)

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

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

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
func (f *FlagSet) Parse(arguments []string) error {
 f.parsed = true
 f.args = arguments
 for {
  seen, err := f.parseOne()
  if seen { continue }
  if err == nil {...}
  switch f.errorHandling {
  case ContinueOnError: return err
  case ExitOnError:
   if err == ErrHelp { os.Exit(0) }
   os.Exit(2)
  case PanicOnError: panic(err)
  }
 }
 return nil
}
  • 上面的函数内容太长了,我收缩了一下。
  • 可看到解析的过程实际上是多次调用了parseOne(),它的作用是逐个遍历命令行参数,绑定到Flag,就像翻页一样。
  • switch对应处理错误,决定退出码或直接panic

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

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
func (f *FlagSet) parseOne() (bool, error) {
 //...
 s := f.args[0]
 //...
 if s[1] == '-' { ...}
 name := s[numMinuses:]
 if len(name) == 0 || name[0] == '-' || name[0] == '=' {
  return false, f.failf("bad flag syntax: %s", s)
 }

 f.args = f.args[1:]
 //...
 m := f.formal
 flag, alreadythere := m[name] // BUG
 // ...如果不存在,或者需要输出帮助信息,则返回
 // ...设置真实值调用到 flag.Value.Set(value)
 if f.actual == nil {
  f.actual = make(map[string]*Flag)
 }
 f.actual[name] = flag
 return true, nil
}

  • parseOne 内部会解析一个输入参数,判断输入参数格式,获取参数值。
  • 解析过程就是逐个取出程序参数,判断-=取参数与参数值
  • 解析后查找之前提到的formal map中有没有存在此参数,并设置真实值。
  • 把设置完毕真实值的参数放到f.actual map中,以供它用。
  • 一些错误处理和细节的代码我省略掉了,感兴趣可以自行看源码。
  • 实际上就是逐个参数解析并设置到对应的指针变量的指向上,让返回值出现变化。

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

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
type Value interface {
    String() string
    Set(string) error
}

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

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
func (b *boolValue) String() string {
  return strconv.FormatBool(bool(*b)) 
}
func (b *boolValue) Set(s string) error {
    v, err := strconv.ParseBool(s)
    if err != nil {
        err = errParse  
    }
    *b = boolValue(v)
    return err
}

从源码想到的拓展用法

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

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

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

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

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
var CommandLine = NewFlagSet(os.Args[0], ExitOnError)

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

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

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

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
$ ./subcommands 
expected 'foo' or 'bar' subcommands

$ ./subcommands foo -h
Usage of foo:
  -enable
        enable
        
$./subcommands foo -enable
subcommand 'foo'
  enable: true
  tail: []

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

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
fooCmd := flag.NewFlagSet("foo", flag.ExitOnError)
fooEnable := fooCmd.Bool("enable", false, "enable")

barCmd := flag.NewFlagSet("bar", flag.ExitOnError)
barLevel := barCmd.Int("level", 0, "level")

if len(os.Args) < 2 {
    fmt.Println("expected 'foo' or 'bar' subcommands")
    os.Exit(1)
}
  • 定义两个不同的FlagSet,接受foobar参数。
  • 绑定错误时退出。
  • 分别为每个FlagSet绑定要解析的变量。
  • 如果判断命令行输入参数少于2个时退出(因为第0个参数是程序名本身)。

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

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
switch os.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: 调用 panic 抛出错误

小结

通过本节我们了解到了标准库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
本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2021-08-10,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 机智的程序员小熊 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
暂无评论
推荐阅读
Web开发---单页面应用(签到日报--技术实现)
疫情前期,员工分布在各个地区,需要上报个人的健康状态和位置信息,于是做了一个单页面应用(当时钉钉和微信上的健康上报模板还没出现)
MiaoGIS
2020/03/16
8580
Web开发---单页面应用(签到日报--技术实现)
基于web页面开发串口程序界面---html代码
代码实现只展示读取设备参数的功能,写入即设置设备参数目前没有开发需求。 首先是html代码如下: <!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="utf-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1"> <
MiaoGIS
2020/09/14
4K0
基于web页面开发串口程序界面---html代码
Python进阶31-Django 分页器
-多年互联网运维工作经验,曾负责过大规模集群架构自动化运维管理工作。 -擅长Web集群架构与自动化运维,曾负责国内某大型金融公司运维工作。 -devops项目经理兼DBA。 -开发过一套自动化运维平台(功能如下): 1)整合了各个公有云API,自主创建云主机。 2)ELK自动化收集日志功能。 3)Saltstack自动化运维统一配置管理工具。 4)Git、Jenkins自动化代码上线及自动化测试平台。 5)堡垒机,连接Linux、Windows平台及日志审计。 6)SQL执行及审批流程。 7)慢查询日志分析web界面。
DriverZeng
2022/09/26
1.6K0
Python进阶31-Django 分页器
Bootstrap Bootstrap表格插件bootstrap-table配置与应用小结
https://gitee.com/ishouke/front_end_plugin/blob/master/jquery-3.2.1.min.js
授客
2019/09/10
13.4K0
Bootstrap Bootstrap表格插件bootstrap-table配置与应用小结
[机器学习]线性回归-基于tensorflow.js
《传热学》横掠管外对流换热系数测定实验中,奴赛尔数Nu与雷诺数Re的关系式,通过实验测定,并确定公式中的系数C和指数n。这里使用机器学习进行线性回归。
周星星9527
2021/07/20
8650
家乡主题网页设计代码 旅游主题网页设计 html静态网页设计制作 dw静态网页成品模板素材网页 web前端网页设计与制作 div静态网页设计
家乡旅游景点网页作业制作 网页代码运用了DIV盒子的使用方法,如盒子的嵌套、浮动、margin、border、background等属性的使用,外部大盒子设定居中,内部左中右布局,下方横向浮动排列,大学学习的前端知识点和布局方式都有运用,CSS的代码量也很足、很细致,使用hover来完成过渡效果、鼠标滑过效果等,使用表格、表单补充模块,为方便新手学习页面中没有使用js有需要的可以自行添加。 <font color='#b44846' size='4px'> ❤</font> 【作者主页——🔥获取更多优质
IT司马青衫
2022/08/18
5.9K0
家乡主题网页设计代码 旅游主题网页设计 html静态网页设计制作 dw静态网页成品模板素材网页 web前端网页设计与制作 div静态网页设计
(三)Java高并发秒杀系统API之Web层开发
SpringMvc默认就会默认去WEB-INF下查找默认规范的配置文件,像我这里配置的servlet-name是seckill-dispatchServlet的话,则默认会寻找WEB-INF一个名为seckill-dispatchServlet-Servlet.xml的配置文件
Java团长
2018/12/25
2.8K0
最火的秒杀是如何实现的?
使用联合查询避免同一用户多次秒杀同一商品(利用在插入购物明细表中的秒杀id和用户的唯一标识来避免)。
互扯程序
2018/10/25
1.6K0
最火的秒杀是如何实现的?
Python测试开发-创建模态框及保存数据
模态框是指的在覆盖在父窗体上的子窗体。可用来做交互,我们经常会看到模态框用来登录、确定等等,到底是怎么实现这种弹出效果,bootstrap已经为我们提供了相应的组件。
测试开发社区
2019/09/20
1.3K0
Python测试开发-创建模态框及保存数据
python测试开发django-121.bootstrap-table弹出模态框修表格数据提交
整个body内容如下,模态框设置id属性id=”myModal” 修改按钮的id属性id=”btn_edit”
上海-悠悠
2021/09/14
1.4K0
前端开发---异步上传文件
有一个名为ajaxFileUpload的JQuery插件可以利用iframe来实现前端页面中异步上传文件。
MiaoGIS
2020/11/25
1.5K0
前端开发---异步上传文件
基于Jquery WeUI的微信开发H5页面控件的经验总结(2)
  在微信开发H5页面的时候,往往借助于WeUI或者Jquery WeUI等基础上进行界面效果的开发,由于本人喜欢在Asp.net的Web界面上使用JQuery,因此比较倾向于使用 jQuery WeUI,本篇随笔结合官方案例和自己的项目实际开发过程的经验总结,对在H5页面开发过程中设计到的界面控件进行逐一的分析和总结,以期能够给大家在H5页面开发过程中提供有用的参考。
不会飞的小鸟
2020/03/25
1.6K0
常用的CSS框架
常用的CSS框架 之前在写自己的个人网站的时候,由于自己Web前端不是特别好,于是就去找相关的CSS框架来搭建页面了。 找到以下这么一篇文章(列出了很多常用的CSS框架): http://w3schools.wang/report/top-UI-open-source-framework-summary.html Bootstrap Semantic-ui Foundation Materialize Material-ui Phantomjs Pure Flat-ui Jquery-ui React-bo
Java3y
2018/03/15
3.4K0
常用的CSS框架
bootstrap
花了一天时间学了下bootstrap入门,想必大家用css写前端页面的时候都很痛苦,bootstrap就是来解决这个问题的,它封装了css的很多样式,开发的时候直接拿来用就可以了,提高了开发效率
用户3112896
2019/09/26
3.5K0
bootstrap
【合肥信息技术职业学院】《PHP网站开发》作业设计
作业名称 系 别 信息工程学院 专业班级 2021级计算机应用技术*班 学 号 学生姓名
德宏大魔王
2023/08/08
2930
【合肥信息技术职业学院】《PHP网站开发》作业设计
BootStrap
iconfont的使用:https://www.cnblogs.com/changxin7/p/11479216.html
changxin7
2022/05/06
5.7K0
BootStrap
Spring Security 自定义登陆页面
在这个 Spring Security 教程中,我们将学到怎么创建一个自定义登陆页面来实现 Spring Security 基于表单的验证。
Jimmy_is_jimmy
2023/11/28
3830
Spring Security 自定义登陆页面
vue2基础
半月无霜
2023/10/18
3360
Larave-vue-crud-laravel-和vue-增删改查
看到successfully代表laravel安装成功,如果没有成功请换淘宝镜像重写安装.
胡哥有话说
2019/07/25
2.4K0
Larave-vue-crud-laravel-和vue-增删改查
【玩转全栈】----Django制作部门管理页面
我先给个大致效果,基本融合了Django、Bootstrap、css、html等等。
用户11404404
2025/01/24
1450
【玩转全栈】----Django制作部门管理页面
推荐阅读
相关推荐
Web开发---单页面应用(签到日报--技术实现)
更多 >
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档