命令行參數(shù)的解析:Flag 庫詳解
在 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ù)的位置有嚴格的要求。對于一些比較復(fù)雜的場景,就需要你自己定義解析規(guī)則,非常麻煩。
如果真的遇上了所謂的復(fù)雜場景,那么還可以使用 Golang 的標(biāo)準庫 flag 包來處理命令行參數(shù)。
本文將介紹 Golang 標(biāo)準庫中 flag 包的用法。
1. 參數(shù)種類
根據(jù)參數(shù)是否為布爾型,可以分為兩種:
- 布爾型參數(shù):如 --debug,后面不用再接具體的值,指定就為 True,不指定就為 False非布爾型參數(shù)
- 非布爾型參數(shù):非布爾型,有可能是int,string 等其他類型,如 --name jack ,后面可以接具體的參數(shù)值
根據(jù)參數(shù)名的長短,還可以分為:
- 長參數(shù):比如 --name jack 就是一個長參數(shù),參數(shù)名前有兩個 -
- 短參數(shù):通常為一個或兩個字母(是對應(yīng)長參數(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ù)類型
當(dāng)你在命令行中指定了參數(shù),Go 如何解析這個參數(shù),轉(zhuǎn)化成何種類型,是需要你事先定義的。
不同的參數(shù),對應(yīng)著 flag 中不同的方法。
下面分別講講不同的參數(shù)類型,都該如何定義。
布爾型
實現(xiàn)效果:當(dāng)不指定 --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í)行結(jié)果如下
- $ 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í)行結(jié)果如下
- $ go run main.go
- 18
- $ go run main.go --age 20
- 20
int64、 uint 和 float64 類型分別對應(yīng) Int64Var 、 UintVar、Float64Var 方法,也是同理,不再贅述。
字符串
定義一個 name參數(shù),不指定默認為 jack
- var name string
- func init() {
- flag.StringVar(&name, "name", "jack", "你的名字")
- }
- func main(){
- flag.Parse()
- fmt.Println(name)
- }
運行后,執(zhí)行結(jié)果如下
- $ 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ù)被封裝到其對應(yīng)的后端類型中,比如 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 的解析時將其轉(zhuǎn)成 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) 這行代碼有所疑問,這是什么意思呢?
關(guān)于這個,其實之前在 【2.9 詳細圖解:靜態(tài)類型與動態(tài)類型】有講過,忘記了可以前往復(fù)習(xí)。
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í)行結(jié)果是相同的。
那么再加一個呢?
終于報錯了。說明最多只能指定兩個 -
- $ 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. 總結(jié)一下
flag 在絕大多數(shù)場景下,它是夠用的,但如果要支持更多的命令傳入格式,flag 可能并不是最好的選擇。
那些在標(biāo)準庫不能解決的場景,往往會有相應(yīng)的Go愛好者提供第三方解決方案。我所了解到的 cobra 就是一個非常不錯的庫。
本文轉(zhuǎn)載自微信公眾號「Go編程時光」,可以通過以下二維碼關(guān)注。轉(zhuǎn)載本文請聯(lián)系Go編程時光公眾號。