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

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

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

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

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


标准库 flag


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

./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程序,上面所示的四个参数,是我自定义的。

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

./goapi -debug -ip 192.168.1.1
./goapi -port 8080
./goapi -version


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

./goapi -version=false


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

-isbool    #同于 -isbool=true
-age=x     #-和等号
-age x     #-和空格
--age=x    #2个-和等号
--age x    #2个-和空格


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

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


如下是详细的绑定方式:


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(),注意一定要在使用参数前调用哦,使用过程像下面这样:

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提供了一种简写的方式,不用额外定义中间变量。像下面这样

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类型为例。

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是一个指针,所以只要改变指向的内容,就可以影响到外部绑定所用的变量:

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包的设计中有两个重要的类型,Flag和FlagSet分别表示某个特定的参数,和一个无重复的参数集合。


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


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的时候,会对参数解析并为变量赋值,使用时就可以得到真实值。展开看看它的代码

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函数,看看它的内容:


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就是解析命令行输入绑定变量的过程了:

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长这样

type Value interface {
    String() string
    Set(string) error
}


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

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方法创建。并且对它的所有方法封装了一下直接对外。


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

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


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

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

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

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

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

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个参数是程序名本身)。


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


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的使用方法,参数变量绑定的两种方式,还通过源码解析了内部实现是如何的巧妙。


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


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


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


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

相关文章
|
23小时前
|
缓存 Go 调度
浅谈在go语言中的锁
【5月更文挑战第11天】本文评估了Go标准库`sync`中的`Mutex`和`RWMutex`性能。`Mutex`包含状态`state`和信号量`sema`,不应复制已使用的实例。`Mutex`适用于保护数据,而`RWMutex`在高并发读取场景下更优。测试显示,小并发时`Mutex`性能较好,但随着并发增加,其性能下降;`RWMutex`的读性能稳定,写性能在高并发时低于`Mutex`。
115 0
浅谈在go语言中的锁
|
1天前
|
存储 安全 编译器
go语言中进行不安全的类型操作
【5月更文挑战第10天】Go语言中的`unsafe`包提供了一种不安全但强大的方式来处理类型转换和底层内存操作。包含两个文档用途的类型和八个函数,本文也比较了不同变量和结构体的大小与对齐系数,强调了字段顺序对内存分配的影响。
48 8
go语言中进行不安全的类型操作
|
1天前
|
Go
配置go语言下载包 - 蓝易云
这个命令会将包下载到你的GOPATH目录下,并自动安装它。
26 1
|
2天前
|
安全 Go 调度
Go语言中的并发编程
Go语言自带了强大的并发编程能力,它的协程机制可以让程序轻松地实现高并发。本文将从并发编程的基础概念出发,介绍Go语言中的协程机制、通道和锁等相关知识点,帮助读者更好地理解并发编程在Go语言中的实践应用。
|
4天前
|
Ubuntu Unix Linux
【GO基础】1. Go语言环境搭建
【GO基础】1. Go语言环境搭建
|
5天前
|
JSON 前端开发 Go
lucky - go 语言实现的快速开发平台
go 语言实现的快速开发平台,自动生成crud代码,前端页面通过json配置,无需编写前端代码。
11 0
|
6天前
|
存储 Java Go
Go 语言切片如何扩容?(全面解析原理和过程)
Go 语言切片如何扩容?(全面解析原理和过程)
16 2
|
6天前
|
负载均衡 Go 调度
使用Go语言构建高性能的Web服务器:协程与Channel的深度解析
在追求高性能Web服务的今天,Go语言以其强大的并发性能和简洁的语法赢得了开发者的青睐。本文将深入探讨Go语言在构建高性能Web服务器方面的应用,特别是协程(goroutine)和通道(channel)这两个核心概念。我们将通过示例代码,展示如何利用协程处理并发请求,并通过通道实现协程间的通信和同步,从而构建出高效、稳定的Web服务器。
|
6天前
|
算法 Go 分布式数据库
构建高可用的分布式数据库集群:使用Go语言与Raft共识算法
随着数据量的爆炸式增长,单一数据库服务器已难以满足高可用性和可扩展性的需求。在本文中,我们将探讨如何使用Go语言结合Raft共识算法来构建一个高可用的分布式数据库集群。我们不仅会介绍Raft算法的基本原理,还会详细阐述如何利用Go语言的并发特性和网络编程能力来实现这一目标。此外,我们还将分析构建过程中可能遇到的挑战和解决方案,为读者提供一个完整的实践指南。
|
6天前
|
消息中间件 Go API
基于Go语言的微服务架构实践
随着云计算和容器化技术的兴起,微服务架构成为了现代软件开发的主流趋势。Go语言,以其高效的性能、简洁的语法和强大的并发处理能力,成为了构建微服务应用的理想选择。本文将探讨基于Go语言的微服务架构实践,包括微服务的设计原则、服务间的通信机制、以及Go语言在微服务架构中的优势和应用案例。

推荐镜像

更多