自拍偷在线精品自拍偷,亚洲欧美中文日韩v在线观看不卡

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

開發(fā) 后端
Parse的代碼里用到了一個,CommandLine共享變量,這就是內(nèi)部庫維護的FlagSet,所有的參數(shù)都會插到里面的變量地址向地址的指向賦值綁定。

 [[416691]]

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

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

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

  • 標準庫 flag
  • flag的簡寫方式
  • 從源碼來看flag如何解析參數(shù)
  • 從源碼想到的拓展用法
  • 小結(jié)
  • 引用

標準庫 flag

命令行程序應(yīng)該能打印出幫助信息,傳遞其他命令行參數(shù),比如-h就是flag庫的默認幫助參數(shù)。

  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程序,上面所示的四個參數(shù),是我自定義的。

按提示的方法,可以像這樣使用參數(shù)。

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

像上面-version這樣的參數(shù)是bool類型的,只要指定了就會設(shè)置為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庫綁定參數(shù)的過程很簡單,格式為

  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和端口等功能。

綁定完參數(shù)還沒完,還得調(diào)用解析函數(shù)flag.Parse(),注意一定要在使用參數(shù)前調(diào)用哦,使用過程像下面這樣:

  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函數(shù)里,不太雅觀,建議把這些單獨放到一個包里,或者放在main函數(shù)的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如何解析參數(shù)

其實我們把之前的綁定方式打開來看,在源碼里就是調(diào)用了xxVar函數(shù),以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函數(shù),它的功能是把需要綁定的變量設(shè)置為默認值,并調(diào)用f.Var進一步處理,這里p是一個指針,所以只要改變指向的內(nèi)容,就可以影響到外部綁定所用的變量:

  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) 
  • newBoolValue 函數(shù)可以得到一個boolValue類型,它是bool類型重命名的。在此包中所有可作為參數(shù)的類型都有這樣的定義。
  • 在flag包的設(shè)計中有兩個重要的類型,F(xiàn)lag和FlagSet分別表示某個特定的參數(shù),和一個無重復(fù)的參數(shù)集合。

f.Var函數(shù)的作用就是把參數(shù)封裝成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結(jié)構(gòu)體中起作用的是formal map[string]*Flag類型,所以說,flag把程序中需要綁定的變量包裝成一個字典,后面解析的時候再一一賦值。

我們已經(jīng)知道了,在調(diào)用Parse的時候,會對參數(shù)解析并為變量賦值,使用時就可以得到真實值。展開看看它的代碼

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

Parse的代碼里用到了一個,CommandLine共享變量,這就是內(nèi)部庫維護的FlagSet,所有的參數(shù)都會插到里面的變量地址向地址的指向賦值綁定。

上面提到FlagSet綁定的Parse函數(shù),看看它的內(nèi)容:

  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 
  • 上面的函數(shù)內(nèi)容太長了,我收縮了一下。
  • 可看到解析的過程實際上是多次調(diào)用了parseOne(),它的作用是逐個遍歷命令行參數(shù),綁定到Flag,就像翻頁一樣。
  • 用switch對應(yīng)處理錯誤,決定退出碼或直接panic。

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.  // ...設(shè)置真實值調(diào)用到 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 
  • 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長這樣

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

它被設(shè)計成一個接口,不同的數(shù)據(jù)類型自己實現(xiàn)這個接口,返回給用戶的地址就是這個接口的實例數(shù)據(jù),解析過程中,可以通過 Set 方法修改它的值,這個設(shè)計確實還挺巧妙的。

  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的常用方法也學(xué)會了,基本原理也了解了,我怎么那么厲害。哈哈哈。

有沒有注意到整個過程都圍繞了FlagSet這個結(jié)構(gòu)體,它是最核心的解析類。

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

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

  1. 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ù),效果如下:

  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: [] 

這是怎么實現(xiàn)的呢?其實就是用NewFlagSet方法創(chuàng)建多個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) 
  • 定義兩個不同的FlagSet,接受foo或bar參數(shù)。
  • 綁定錯誤時退出。
  • 分別為每個FlagSet綁定要解析的變量。
  • 如果判斷命令行輸入?yún)?shù)少于2個時退出(因為第0個參數(shù)是程序名本身)。

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

  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) 
  • 使用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)系機智的程序員小熊公眾號。

 

責(zé)任編輯:武曉燕 來源: 機智的程序員小熊
相關(guān)推薦

2021-01-13 05:29:26

命令行

2023-07-05 08:38:48

GolangGo語言

2024-05-15 17:05:16

GoLangflagcast

2023-10-30 01:00:42

Go語言Cobra庫

2021-11-15 14:30:49

Pythonargparse編程語言

2018-09-10 09:30:25

Linux命令應(yīng)用

2014-06-16 09:28:08

Linux命令行

2021-11-08 10:45:07

Python命令工具

2010-03-10 17:23:37

Python 命令行參

2021-01-06 09:47:51

內(nèi)存Go語言

2010-11-24 15:33:59

mysql命令行參數(shù)

2009-07-20 09:55:30

華為命令行解析華為認證

2010-08-20 10:05:23

用戶命令

2010-07-15 10:47:22

Perl命令行

2011-08-22 11:51:13

Linuxconfigure

2010-07-20 14:02:38

Perl命令行參數(shù)

2010-07-26 09:32:41

Perl命令行

2020-12-10 16:16:08

工具代碼開發(fā)

2020-12-11 06:44:16

命令行工具開發(fā)

2010-07-15 11:08:23

Perl命令行
點贊
收藏

51CTO技術(shù)棧公眾號