從源碼的角度看Go語言Flag庫如何解析命令行參數(shù)!
我上周五喝酒喝到晚上3點多,確實有點罩不住啊,整個周末都在休息和睡覺,文章鴿了幾天,想不到就有兩個人跑了。
不得不感嘆一下,自媒體的太殘酷了,時效就那么幾天,斷更就沒人愛。你們說好了愛我的,愛呢?哼
昨晚就在寫這篇文章了,沒想到晚上又遇到發(fā)版本,確實不容易,且看且珍惜。
- 標準庫 flag
- flag的簡寫方式
- 從源碼來看flag如何解析參數(shù)
- 從源碼想到的拓展用法
- 小結(jié)
- 引用
標準庫 flag
命令行程序應(yīng)該能打印出幫助信息,傳遞其他命令行參數(shù),比如-h就是flag庫的默認幫助參數(shù)。
- ./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程序,上面所示的四個參數(shù),是我自定義的。
按提示的方法,可以像這樣使用參數(shù)。
- ./goapi -debug -ip 192.168.1.1
- ./goapi -port 8080
- ./goapi -version
像上面-version這樣的參數(shù)是bool類型的,只要指定了就會設(shè)置為true,不指定時為默認值,假如默認值是true,想指定為false要像下面這樣顯式的指定(因為源碼里是這樣寫的)。
- ./goapi -version=false
下面這幾種格式都是兼容的
- -isbool #同于 -isbool=true
- -age=x #-和等號
- -age x #-和空格
- --age=x #2個-和等號
- --age x #2個-和空格
flag庫綁定參數(shù)的過程很簡單,格式為
- 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和端口等功能。
綁定完參數(shù)還沒完,還得調(diào)用解析函數(shù)flag.Parse(),注意一定要在使用參數(shù)前調(diào)用哦,使用過程像下面這樣:
- 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函數(shù)里,不太雅觀,建議把這些單獨放到一個包里,或者放在main函數(shù)的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如何解析參數(shù)
其實我們把之前的綁定方式打開來看,在源碼里就是調(diào)用了xxVar函數(shù),以Bool類型為例。
- func (f *FlagSet) Bool(name string, value bool, usage string) *bool {
- p := new(bool)
- f.BoolVar(p, name, value, usage)
- return p
- }
上面的代碼用到了BoolVal函數(shù),它的功能是把需要綁定的變量設(shè)置為默認值,并調(diào)用f.Var進一步處理,這里p是一個指針,所以只要改變指向的內(nèi)容,就可以影響到外部綁定所用的變量:
- 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 函數(shù)可以得到一個boolValue類型,它是bool類型重命名的。在此包中所有可作為參數(shù)的類型都有這樣的定義。
- 在flag包的設(shè)計中有兩個重要的類型,F(xiàn)lag和FlagSet分別表示某個特定的參數(shù),和一個無重復(fù)的參數(shù)集合。
f.Var函數(shù)的作用就是把參數(shù)封裝成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結(jié)構(gòu)體中起作用的是formal map[string]*Flag類型,所以說,flag把程序中需要綁定的變量包裝成一個字典,后面解析的時候再一一賦值。
我們已經(jīng)知道了,在調(diào)用Parse的時候,會對參數(shù)解析并為變量賦值,使用時就可以得到真實值。展開看看它的代碼
- func Parse() {
- // Ignore errors; CommandLine is set for ExitOnError.
- // 調(diào)用了FlagSet.Parse
- CommandLine.Parse(os.Args[1:])
- }
- // 返回一個FlagSet
- var CommandLine = NewFlagSet(os.Args[0], ExitOnError)
Parse的代碼里用到了一個,CommandLine共享變量,這就是內(nèi)部庫維護的FlagSet,所有的參數(shù)都會插到里面的變量地址向地址的指向賦值綁定。
上面提到FlagSet綁定的Parse函數(shù),看看它的內(nèi)容:
- 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
- }
- 上面的函數(shù)內(nèi)容太長了,我收縮了一下。
- 可看到解析的過程實際上是多次調(diào)用了parseOne(),它的作用是逐個遍歷命令行參數(shù),綁定到Flag,就像翻頁一樣。
- 用switch對應(yīng)處理錯誤,決定退出碼或直接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
- // ...如果不存在,或者需要輸出幫助信息,則返回
- // ...設(shè)置真實值調(diào)用到 flag.Value.Set(value)
- if f.actual == nil {
- f.actual = make(map[string]*Flag)
- }
- f.actual[name] = flag
- return true, nil
- }
- parseOne 內(nèi)部會解析一個輸入?yún)?shù),判斷輸入?yún)?shù)格式,獲取參數(shù)值。
- 解析過程就是逐個取出程序參數(shù),判斷-、=取參數(shù)與參數(shù)值
- 解析后查找之前提到的formal map中有沒有存在此參數(shù),并設(shè)置真實值。
- 把設(shè)置完畢真實值的參數(shù)放到f.actual map中,以供它用。
- 一些錯誤處理和細節(jié)的代碼我省略掉了,感興趣可以自行看源碼。
- 實際上就是逐個參數(shù)解析并設(shè)置到對應(yīng)的指針變量的指向上,讓返回值出現(xiàn)變化。
flag.Value.Set(value) 這里是設(shè)置數(shù)據(jù)真實值的代碼,Value長這樣
- type Value interface {
- String() string
- Set(string) error
- }
它被設(shè)計成一個接口,不同的數(shù)據(jù)類型自己實現(xiàn)這個接口,返回給用戶的地址就是這個接口的實例數(shù)據(jù),解析過程中,可以通過 Set 方法修改它的值,這個設(shè)計確實還挺巧妙的。
- 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的常用方法也學(xué)會了,基本原理也了解了,我怎么那么厲害。哈哈哈。
有沒有注意到整個過程都圍繞了FlagSet這個結(jié)構(gòu)體,它是最核心的解析類。
在庫內(nèi)部提供了一個 *FlagSet 的實例對象 CommandLine,它通過NewFlagSet方法創(chuàng)建。并且對它的所有方法封裝了一下直接對外。
官方的意思很明確了,說明我們可以用到它做些更高級的事情。先看看官方怎么用的。
- var CommandLine = NewFlagSet(os.Args[0], ExitOnError)
可以看到調(diào)用的時候是傳入命令行第一個參數(shù),第二個參數(shù)表示報錯時應(yīng)該呈現(xiàn)怎樣的錯誤。
那就意味著我們可以根據(jù)命令行第一個參數(shù)不同而呈現(xiàn)不同的表現(xiàn)!
我定義了兩個參數(shù)foo或者bar,代表兩個不同的指令集合,每個指令集匹配不同的命令參數(shù),效果如下:
- $ ./subcommands
- expected 'foo' or 'bar' subcommands
- $ ./subcommands foo -h
- Usage of foo:
- -enable
- enable
- $./subcommands foo -enable
- subcommand 'foo'
- enable: true
- tail: []
這是怎么實現(xiàn)的呢?其實就是用NewFlagSet方法創(chuàng)建多個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,接受foo或bar參數(shù)。
- 綁定錯誤時退出。
- 分別為每個FlagSet綁定要解析的變量。
- 如果判斷命令行輸入?yún)?shù)少于2個時退出(因為第0個參數(shù)是程序名本身)。
然后根據(jù)第一個參數(shù),判斷應(yīng)該匹配到哪個指令集:
- 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來切換命令行參數(shù),綁定不同的變量。
- 對應(yīng)不同變量輸出不同表現(xiàn)。
- x.Args()可以打印未匹配到的其他參數(shù)。
補充:使用NewFlagSet時,flag 提供三種錯誤處理的方式:
- ContinueOnError: 通過 Parse 的返回值返回錯誤
- ExitOnError: 調(diào)用 os.Exit(2) 直接退出程序,這是默認的處理方式
- PanicOnError: 調(diào)用 panic 拋出錯誤
小結(jié)
通過本節(jié)我們了解到了標準庫flag的使用方法,參數(shù)變量綁定的兩種方式,還通過源碼解析了內(nèi)部實現(xiàn)是如何的巧妙。
我們還使用源碼暴露出來的函數(shù),接收不同參數(shù)匹配不同指令集,這種方式可以讓應(yīng)用呈現(xiàn)完成不同的功能;
我想到的是用來通過環(huán)境變量改變命令用法、或者讓程序復(fù)用大段邏輯呈現(xiàn)不同作用時使用。
但現(xiàn)在微服務(wù)那么流行,大多功能集成在一個服務(wù)里是不科學(xué)的,如果有重復(fù)代碼應(yīng)該提煉成共同模塊才是王道。
你還想到能哪些使用場景呢?
引用
源碼包 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
往期精彩回顧
本文轉(zhuǎn)載自微信公眾號「機智的程序員小熊」,可以通過以下二維碼關(guān)注。轉(zhuǎn)載本文請聯(lián)系機智的程序員小熊公眾號。