40. 命令行參數(shù)的解析:flag 庫詳解
點擊上方“Go編程時光”,選擇“加為星標”

在 Golang 程序中有很多種方法來處理命令行參數(shù)。
簡單的情況下可以不使用任何庫,直接使用 os.Args
package?main
import?(
????"fmt"
????"os"
)
func?main()?{
????if?len(os.Args)?>?0?{
????????for?index,?arg?:=?range?os.Args?{
????????????fmt.Printf("args[%d]=%v\n",?index,?arg)
????????}
????}
}
試著運行一下,第一個參數(shù)是執(zhí)行文件的路徑。
$?go?run?demo.go?hello?world?hello?golang
args[0]=/var/folders/72/lkr7ltfd27lcf36d75jdyjr40000gp/T/go-build187785213/b001/exe/demo
args[1]=hello
args[2]=world
args[3]=hello
args[4]=golang
從上面你可以看到,os.Args 只能處理簡單的參數(shù),而且對于參數(shù)的位置有嚴格的要求。對于一些比較復雜的場景,就需要你自己定義解析規(guī)則,非常麻煩。
如果真的遇上了所謂的復雜場景,那么還可以使用 Golang 的標準庫 flag 包來處理命令行參數(shù)。
本文將介紹 Golang 標準庫中 flag 包的用法。
1. 參數(shù)種類
根據(jù)參數(shù)是否為布爾型,可以分為兩種:
布爾型參數(shù):如
--debug,后面不用再接具體的值,指定就為 True,不指定就為 False非布爾型參數(shù)非布爾型參數(shù):非布爾型,有可能是int,string 等其他類型,如
--name jack,后面可以接具體的參數(shù)值
根據(jù)參數(shù)名的長短,還可以分為:
長參數(shù):比如
--name jack就是一個長參數(shù),參數(shù)名前有兩個-短參數(shù):通常為一個或兩個字母(是對應長參數(shù)的簡寫),比如
-n,參數(shù)名前只有一個-
2. 入門示例
我先用一個字符串類型的參數(shù)的示例,拋磚引玉
package?main
import?(
????"flag"
????"fmt"
)
func?main(){
????var?name?string
????flag.StringVar(&name,?"name",?"jack",?"your?name")
flag.Parse()??//?解析參數(shù)
????fmt.Println(name)
}
flag.StringVar 定義了一個字符串參數(shù),它接收幾個參數(shù)
第一個參數(shù) :接收值后,存放在哪個變量里,需為指針
第二個參數(shù) :在命令行中使用的參數(shù)名,比如
--name jack里的 name第三個參數(shù) :若命令行中未指定該參數(shù)值,那么默認值為
jack第四個參數(shù):記錄這個參數(shù)的用途或意義
運行以上程序,輸出如下
$ go run demo.go
jack
$?go?run?demo.go?--name?wangbm
wangbm
3. 改進一下
如果你的程序只接收很少的幾個參數(shù)時,上面那樣寫也沒有什么問題。
但一旦參數(shù)數(shù)量多了以后,一大堆參數(shù)解析的代碼堆積在 main 函數(shù)里,影響代碼的可讀性、美觀性。
建議將參數(shù)解析的代碼放入 init 函數(shù)中,init 函數(shù)會先于 main 函數(shù)執(zhí)行。
package?main
import?(
????"flag"
????"fmt"
)
var?name?string
func?init()??{
????flag.StringVar(&name,?"name",?"jack",?"your?name")
}
func?main(){
????flag.Parse()
????fmt.Println(name)
}
4. 參數(shù)類型
當你在命令行中指定了參數(shù),Go 如何解析這個參數(shù),轉化成何種類型,是需要你事先定義的。
不同的參數(shù),對應著 flag 中不同的方法。
下面分別講講不同的參數(shù)類型,都該如何定義。
布爾型
實現(xiàn)效果:當不指定 --debug 時,debug 的默認值為 false,你一指定 --debug,debug 為賦值為 true。
var?debug?bool
func?init()??{
????flag.BoolVar(&debug,?"debug",?false,?"是否開啟?DEBUG?模式")
}
func?main(){
????flag.Parse()
????fmt.Println(debug)
}
運行后,執(zhí)行結果如下
$?go?run?main.go?
false
$?go?run?main.go?--debug
true
數(shù)值型
定義一個 age 參數(shù),不指定默認為 18
var?age?int
func?init()??{
????flag.IntVar(&age,?"age",?18,?"你的年齡")
}
func?main(){
????flag.Parse()
????fmt.Println(age)
}
運行后,執(zhí)行結果如下
$?go?run?main.go?
18
$?go?run?main.go?--age?20
20
int64、 uint 和 float64 類型分別對應 Int64Var 、 UintVar、Float64Var 方法,也是同理,不再贅述。
字符串
定義一個 name參數(shù),不指定默認為 jack
var?name?string
func?init()??{
????flag.StringVar(&name,?"name",?"jack",?"你的名字")
}
func?main(){
????flag.Parse()
????fmt.Println(name)
}
運行后,執(zhí)行結果如下
$?go?run?main.go?
jack
$?go?run?main.go?--name?wangbm
wangbm
時間類型
定義一個 interval 參數(shù),不指定默認為 1s
var?interval?time.Duration
func?init()??{
????flag.DurationVar(&interval,?"interval",?1?*?time.Second,?"循環(huán)間隔")
}
func?main(){
????flag.Parse()
????fmt.Println(interval)
}
驗證效果如下
$?go?run?main.go?
1s
$?go?run?main.go?--interval?2s
2s
5. 自定義類型
flag 包支持的類型有 Bool、Duration、Float64、Int、Int64、String、Uint、Uint64。
這些類型的參數(shù)被封裝到其對應的后端類型中,比如 Int 類型的參數(shù)被封裝為 intValue,String 類型的參數(shù)被封裝為 stringValue。
這些后端的類型都實現(xiàn)了 flag.Value 接口,因此可以把一個命令行參數(shù)抽象為一個 Flag 類型的實例。下面是 Value 接口和 Flag 類型的代碼:
type?Value?interface?{
????String()?string
????Set(string)?error
}
//?Flag?類型
type?Flag?struct?{
????Name?????string?//?name?as?it?appears?on?command?line
????Usage????string?//?help?message
????Value????Value??// value as set 是個 interface,因此可以是不同類型的實例。
????DefValue?string?//?default?value?(as?text);?for?usage?message
}
func?Var(value?Value,?name?string,?usage?string)?{
????CommandLine.Var(value,?name,?usage)
}
想要實現(xiàn)自定義類型的參數(shù),其實只要 Var 函數(shù)的第一個參數(shù)對象實現(xiàn) flag.Value接口即可
type?sliceValue?[]string
func?newSliceValue(vals?[]string,?p?*[]string)?*sliceValue?{
????*p?=?vals
????return?(*sliceValue)(p)
}
func?(s?*sliceValue)?Set(val?string)?error?{
?????????//?如何解析參數(shù)值
????*s?=?sliceValue(strings.Split(val,?","))
????return?nil
}
func?(s?*sliceValue)?String()?string?{
????return?strings.Join([]string(*s),?",")
}
比如我想實現(xiàn)如下效果,傳入的參數(shù)是一個字符串,以逗號分隔,flag 的解析時將其轉成 slice。
$?go?run?demo.go?-members?"Jack,Tom"
[Jack?Tom]
那我可以這樣子編寫代碼
var?members?[]string
type?sliceValue?[]string
func?newSliceValue(vals?[]string,?p?*[]string)?*sliceValue?{
????*p?=?vals
????return?(*sliceValue)(p)
}
func?(s?*sliceValue)?Set(val?string)?error?{
?????????//?如何解析參數(shù)值
????*s?=?sliceValue(strings.Split(val,?","))
????return?nil
}
func?(s?*sliceValue)?String()?string?{
????return?strings.Join([]string(*s),?",")
}
func?init()??{
????flag.Var(newSliceValue([]string{},?&members),?"members",?"會員列表")
}
func?main(){
????flag.Parse()
????fmt.Println(members)
}
有的朋友 可能會對 (*sliceValue)(p) 這行代碼有所疑問,這是什么意思呢?
關于這個,其實之前在 【2.9 詳細圖解:靜態(tài)類型與動態(tài)類型】有講過,忘記了可以前往復習。
6. 長短選項
flag 包,在使用上,其實并沒有沒有長短選項之別,你可以看下面這個例子
package?main
import?(
????"flag"
????"fmt"
)
var?name?string
func?init()??{
????flag.StringVar(&name,?"name",?"明哥",?"你的名字")
}
func?main(){
????flag.Parse()
????fmt.Println(name)
}
通過指定如下幾種參數(shù)形式
$?go?run?main.go?
明哥
$?go?run?main.go?--name?jack
jack
$?go?run?main.go?-name?jack
jack
一個 - 和兩個 - 執(zhí)行結果是相同的。
那么再加一個呢?
終于報錯了。說明最多只能指定兩個 -
$?go?run?main.go?---name?jack
bad?flag?syntax:?---name
Usage?of?/tmp/go-build245956022/b001/exe/main:
??-name?string
????????你的名字?(default?"明哥")
exit?status?2
7. 總結一下
flag 在絕大多數(shù)場景下,它是夠用的,但如果要支持更多的命令傳入格式,flag 可能并不是最好的選擇。
那些在標準庫不能解決的場景,往往會有相應的Go愛好者提供第三方解決方案。我所了解到的 cobra 就是一個非常不錯的庫。
它能夠支持 flag 不能支持的功能,比如 支持短選項,支持子命令 等等,后面找個機會再好好寫一下。
明哥原創(chuàng)文都已傳至 Github:https://github.com/iswbm/GolangCodingTime
本文永久博客鏈接:https://iswbm.com/167.html

???
