<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          從源碼的角度看Go語言flag庫如何解析命令行參數(shù)!

          共 10580字,需瀏覽 22分鐘

           ·

          2021-08-15 12:43

          點擊上面關注下我嘛,拜托拜托!

          我上周五喝酒喝到晚上3點多,確實有點罩不住啊,整個周末都在休息和睡覺,文章鴿了幾天,想不到就有兩個人跑了。

          不得不感嘆一下,自媒體的太殘酷了,時效就那么幾天,斷更就沒人愛。你們說好了愛我的,愛呢?哼

          昨晚就在寫這篇文章了,沒想到晚上又遇到發(fā)版本,確實不容易,且看且珍惜。

          • 標準庫 flag

          • flag的簡寫方式

          • 從源碼來看flag如何解析參數(shù)

          • 從源碼想到的拓展用法

          • 小結(jié)

          • 引用

          • 往期精彩回顧

          標準庫 flag

          命令行程序應該能打印出幫助信息,傳遞其他命令行參數(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類型的,只要指定了就會設置為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ù),它的功能是把需要綁定的變量設置為默認值,并調(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包的設計中有兩個重要的類型,FlagFlagSet分別表示某個特定的參數(shù),和一個無重復的參數(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對應處理錯誤,決定退出碼或直接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
           // ...如果不存在,或者需要輸出幫助信息,則返回
           // ...設置真實值調(diào)用到 flag.Value.Set(value)
           if f.actual == nil {
            f.actual = make(map[string]*Flag)
           }
           f.actual[name] = flag
           return truenil
          }

          • parseOne 內(nèi)部會解析一個輸入?yún)?shù),判斷輸入?yún)?shù)格式,獲取參數(shù)值。
          • 解析過程就是逐個取出程序參數(shù),判斷-、=取參數(shù)與參數(shù)值
          • 解析后查找之前提到的formal map中有沒有存在此參數(shù),并設置真實值。
          • 把設置完畢真實值的參數(shù)放到f.actual map中,以供它用。
          • 一些錯誤處理和細節(jié)的代碼我省略掉了,感興趣可以自行看源碼。
          • 實際上就是逐個參數(shù)解析并設置到對應的指針變量的指向上,讓返回值出現(xiàn)變化。

          flag.Value.Set(value) 這里是設置數(shù)據(jù)真實值的代碼,Value長這樣

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

          它被設計成一個接口,不同的數(shù)據(jù)類型自己實現(xiàn)這個接口,返回給用戶的地址就是這個接口的實例數(shù)據(jù),解析過程中,可以通過 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這個結(jié)構(gòu)體,它是最核心的解析類。

          在庫內(nèi)部提供了一個 *FlagSet 的實例對象 CommandLine,它通過NewFlagSet方法創(chuàng)建。并且對它的所有方法封裝了一下直接對外。

          官方的意思很明確了,說明我們可以用到它做些更高級的事情。先看看官方怎么用的。

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

          可以看到調(diào)用的時候是傳入命令行第一個參數(shù),第二個參數(shù)表示報錯時應該呈現(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,接受foobar參數(shù)。
          • 綁定錯誤時退出。
          • 分別為每個FlagSet綁定要解析的變量。
          • 如果判斷命令行輸入?yún)?shù)少于2個時退出(因為第0個參數(shù)是程序名本身)。

          然后根據(jù)第一個參數(shù),判斷應該匹配到哪個指令集:

          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ù),綁定不同的變量。
          • 對應不同變量輸出不同表現(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ù)匹配不同指令集,這種方式可以讓應用呈現(xiàn)完成不同的功能;

          我想到的是用來通過環(huán)境變量改變命令用法、或者讓程序復用大段邏輯呈現(xiàn)不同作用時使用。

          但現(xiàn)在微服務那么流行,大多功能集成在一個服務里是不科學的,如果有重復代碼應該提煉成共同模塊才是王道。

          你還想到能哪些使用場景呢?

          引用

          • 源碼包 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

          往期精彩回顧

          PS:本來還想寫一下kingpin、cobra再拓展到配置解析的,沒想到加上源碼解讀內(nèi)容實在太多了,繼續(xù)關注我下次走起。

          歡迎評論指正,一經(jīng)采納,獎勵紅包!
          內(nèi)推與面試交流群點此,Go實戰(zhàn)交流群直接加微信 qupzhi

          如有收獲,點個在看,誠摯感謝

          瀏覽 83
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          評論
          圖片
          表情
          推薦
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  免费观看日本一级A片 | 丁香激情五月天 | 婷婷偷拍视频 | 一级无线免费视频 | 国产网址 |