溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊(cè)×
其他方式登錄
點(diǎn)擊 登錄注冊(cè) 即表示同意《億速云用戶服務(wù)條款》

命令行參數(shù)(flag包)

發(fā)布時(shí)間:2020-05-24 10:08:59 來(lái)源:網(wǎng)絡(luò) 閱讀:996 作者:騎士救兵 欄目:編程語(yǔ)言

命令行參數(shù)

命令行參數(shù)可以直接通過(guò) os.Args 獲取,另外標(biāo)準(zhǔn)庫(kù)的 flag 包專門用于接收和解除命令行參數(shù)

os.Args

簡(jiǎn)單的只是從命令行獲取一個(gè)或一組參數(shù),可以直接使用 os.Args。下面的這種寫法,無(wú)需進(jìn)行判斷,無(wú)論是否提供了命令行參數(shù),或者提供了多個(gè),都可以處理:

// 把命令行參數(shù),依次打印,每行一個(gè)
func main() {
    for _, s := range os.Args[1:] {
        fmt.Println(s)
    }
}

flag 基本使用

下面的例子使用了兩種形式的調(diào)用方法:

package main

import (
    "flag"
    "fmt"
)

var name string

func init() {
    flag.StringVar(&name, "name", "Adam", "名字")
}

var ageP = flag.Int("age", 18, "年齡")

func main() {
    flag.Parse()
    fmt.Printf("%T %[1]v\n", name)
    fmt.Printf("%T %[1]v\n", ageP)
    fmt.Printf("%T %[1]v\n", *ageP)
}

第一種是直接把變量的指針傳遞給函數(shù)作為第一個(gè)參數(shù),函數(shù)內(nèi)部會(huì)對(duì)該變量進(jìn)行賦值。這種形式必須寫在一個(gè)函數(shù)體的內(nèi)部。
第二種是函數(shù)會(huì)把數(shù)據(jù)的指針作為函數(shù)的返回值返回,這種形式就是給變量賦值,不需要現(xiàn)在函數(shù)體內(nèi),不過(guò)拿到的返回值是指針。

切片加參數(shù)

這是上面兩個(gè)實(shí)現(xiàn)的結(jié)合,可以提供一組 os.Args 的參數(shù),另外還可以使用 flag 來(lái)進(jìn)行參數(shù)設(shè)置。
首先是不需要參數(shù)設(shè)置的情況。僅僅就是使用 flag 包提供的方法來(lái)代替使用 os.Args 的實(shí)現(xiàn):

func main() {
    flag.Parse()
    for _, arg := range flag.Args() {
        fmt.Println(arg)
    }
}

基本上沒什么差別,不過(guò)引入 flag 包之后,就可以使用參數(shù)了,比如加上一個(gè) -upper 參數(shù),讓輸出全大寫:

func main() {
    var upper bool
    flag.BoolVar(&upper, "upper", false, "是否大寫")
    flag.Parse()
    for _, arg := range flag.Args() {
        if upper {
            fmt.Println(strings.ToUpper(arg))
        } else {
            fmt.Println(arg)
        }
    }
}

自定義切片類型的實(shí)現(xiàn)下面會(huì)講。不過(guò)像這樣簡(jiǎn)單的使用,只有一個(gè)切片類型,也不需要使用自定義類型就可以方便的實(shí)現(xiàn)了:

PS G:\Steed\Documents\Go\src\localdemo\flag> go run main.go -upper hello hi bye
HELLO
HI
BYE
PS G:\Steed\Documents\Go\src\localdemo\flag>

命令行參數(shù)必須放在前面,把不需要解析的參數(shù)全部放在最后。

解析時(shí)間

時(shí)間長(zhǎng)度類的命令行標(biāo)志應(yīng)用廣泛,這個(gè)功能內(nèi)置到了 flag 包中。
先看看源碼中的示例,之后在自定義命令行標(biāo)志的時(shí)候也能有個(gè)參考。下面的示例,實(shí)現(xiàn)了暫停指定時(shí)間的功能:

var period = flag.Duration("period", 1*time.Second, "sleep period")

func main() {
    flag.Parse()
    fmt.Printf("Sleeping for %v...", *period)
    time.Sleep(*period)
    fmt.Println()
}

默認(rèn)是1秒,但是可以通過(guò)參數(shù)來(lái)控制。flag.Duration函數(shù)創(chuàng)建了一個(gè) *time.Duration 類型的標(biāo)志變量,并且允許用戶用一種友好的方式來(lái)指定時(shí)長(zhǎng)。就是用 String 方法對(duì)應(yīng)的記錄方法。這種對(duì)稱的設(shè)計(jì)提供了一個(gè)良好的用戶接口。

PS H:\Go\src\gopl\ch7\sleep> go run main.go -period 3s
Sleeping for 3s...
PS H:\Go\src\gopl\ch7\sleep> go run main.go -period 1m
Sleeping for 1m0s...
PS H:\Go\src\gopl\ch7\sleep> go run main.go -period 1.5h
Sleeping for 1h40m0s...

自定義類型

更多的情況下,是需要自己實(shí)現(xiàn)接口來(lái)進(jìn)行自定義的。

接口說(shuō)明

支持自定義類型,需要定義一個(gè)滿足 flag.Value 接口的類型:

package flag

// Value 接口代表了存儲(chǔ)在標(biāo)志內(nèi)的值
type Value interface {
    String() string
    Set(string) error
}

String 方法用于格式化標(biāo)志對(duì)應(yīng)的值,可用于輸出命令行幫助消息。
Set 方法解析了傳入的字符串參數(shù)并更新標(biāo)志值??梢哉J(rèn)為 Set 方法是 String 方法的逆操作,這兩個(gè)方法使用同樣的記法規(guī)格是一個(gè)很好的實(shí)踐。

自定義溫度解析

下面定義 celsiusFlag 類型來(lái)允許在參數(shù)中使用攝氏溫度或華氏溫度。因?yàn)?Celsius 類型原本就已經(jīng)實(shí)現(xiàn)了 String 方法,這里把 Celsius 內(nèi)嵌到了 celsiusFlag 結(jié)構(gòu)體中,這樣結(jié)構(gòu)體有就有了 String 方法(外圍結(jié)構(gòu)體類型不僅獲取了匿名成員的內(nèi)部變量,還有相關(guān)方法)。所以為了滿足接口,只須再定一個(gè) Set 方法:

type Celsius float64
type Fahrenheit float64

func CToF(c Celsius) Fahrenheit { return Fahrenheit(c*9.0/5.0 + 32.0) }
func FToC(f Fahrenheit) Celsius { return Celsius((f - 32.0) * 5.0 / 9.0) }

func (c Celsius) String() string { return fmt.Sprintf("%g°C", c) }
// 上面這些都是之前在別處定義過(guò)的內(nèi)容,是可以作為包引出過(guò)來(lái)的
// 為了說(shuō)明清楚,就單獨(dú)把需要用到的部分復(fù)制過(guò)來(lái)

// *celsiusFlag 滿足 flag.Vulue 接口
type celsiusFlag struct{ Celsius }

func (f *celsiusFlag) Set(s string) error {
    var unit string
    var value float64
    fmt.Sscanf(s, "%f%s", &value, &unit) // 無(wú)須檢查錯(cuò)誤
    switch unit {
    case "C", "°C":
        f.Celsius = Celsius(value)
        return nil
    case "F", "°F":
        f.Celsius = FToC(Fahrenheit(value))
        return nil
    }
    return fmt.Errorf("invalid temperature %q", s)
}

fmt.Sscanf 函數(shù)用于從輸入 s 解析一個(gè)浮點(diǎn)值和一個(gè)字符串。通常是需要檢查錯(cuò)誤的,但是這里如果出錯(cuò),后面的 switch 里的條件也是無(wú)法滿足的,是可以通過(guò)switch之后的錯(cuò)誤處理來(lái)一并進(jìn)行處理的。
這里還需要寫一個(gè) CelsiusFlag 函數(shù)來(lái)封裝上面的邏輯。這個(gè)函數(shù)返回了一個(gè) Celsius 的指針,它指向嵌入在 celsiusFlag 變量 f 中的一個(gè)字段。Celsius 字段在標(biāo)志處理過(guò)程中會(huì)發(fā)生變化(經(jīng)由Set
方法)。調(diào)用 Var 方法可以把這個(gè)標(biāo)志加入到程序的命令行標(biāo)記集合中,即全局變量 flag.CommandLine。如果一個(gè)程序有非常復(fù)雜的命令行接口,那么單個(gè)全局變量就不夠用了,需要多個(gè)類似的變量來(lái)支撐。最后一節(jié)“創(chuàng)建私有命令參數(shù)容器”會(huì)做簡(jiǎn)單的展開,不過(guò)也沒有實(shí)現(xiàn)到這個(gè)程度。
調(diào)用 Var 方法是會(huì)把 *celsiusFlag 實(shí)參賦給 flag.Value 形參,編譯器會(huì)在此時(shí)檢查 *celsiusFlag 類型是否有 flag.Value 所必需的方法:

func CelsiusFlag(name string, value Celsius, usage string) *Celsius {
    f := celsiusFlag{value}
    flag.CommandLine.Var(&f, name, usage)
    return &f.Celsius
}

現(xiàn)在就可以在程序中使用這個(gè)標(biāo)志了,使用代碼如下:

var temp = CelsiusFlag("temp", 20.0, "溫度")

func main() {
    flag.Parse()
    fmt.Println(*temp)
}

接下來(lái)還可以把上面的例子簡(jiǎn)單改一下,不用結(jié)構(gòu)體了,而是換成變量的別名,這樣就需要額外再實(shí)現(xiàn)一個(gè)String方法,完整的代碼如下:

package main

import (
    "flag"
    "fmt"
)

type Celsius float64
type Fahrenheit float64

func CToF(c Celsius) Fahrenheit { return Fahrenheit(c*9.0/5.0 + 32.0) }
func FToC(f Fahrenheit) Celsius { return Celsius((f - 32.0) * 5.0 / 9.0) }

func (c Celsius) String() string { return fmt.Sprintf("%g°C", c) }

// 上面這些都是之前定義過(guò)的內(nèi)容,是可以作為包引出過(guò)來(lái)的
// 為了說(shuō)明清楚,就單獨(dú)把需要用到的部分復(fù)制過(guò)來(lái)

// *celsiusValue 滿足 flag.Vulue 接口
// 同一個(gè)包不必這么麻煩,直接定義 Celsius 類型即可。這里假設(shè)是從別的包引入的類型
type celsiusValue Celsius

func (c *celsiusValue) String() string { return fmt.Sprintf("%.2f°C", *c) }
// func (c *celsiusValue) String() string { return (*Celsius)(c).String() }

func (c *celsiusValue) Set(s string) error {
    var unit string
    var value float64
    fmt.Sscanf(s, "%f%s", &value, &unit) // 無(wú)須檢查錯(cuò)誤
    switch unit {
    case "C", "°C":
        *c = celsiusValue(value)
        return nil
    case "F", "°F":
        *c = celsiusValue(FToC(Fahrenheit(value)))
        return nil
    }
    return fmt.Errorf("invalid temperature %q", s)
}

func CelsiusFlag(name string, value Celsius, usage string) *Celsius {
    p := new(Celsius) // value 是傳值進(jìn)來(lái)的,取不到地址,new一個(gè)內(nèi)存空間,存放value的值
    *p = value
    flag.CommandLine.Var((*celsiusValue)(p), name, usage)
    return p
}

func main() {
    tempP := CelsiusFlag("temp", 36.7, "溫度")
    flag.Parse()
    fmt.Printf("%T, %[1]v\n", tempP)
}

打印默認(rèn)值

使用上面最后一個(gè)例子,打印幫助,查看默認(rèn)值的提示:

PS G:\Steed\Documents\Go\src\gopl\output\flag\tempconv2> go run main.go -h
Usage of C:\Users\Steed\AppData\Local\Temp\go-build840446178\b001\exe\main.exe:
  -temp value
        溫度 (default 36.70°C)
exit status 2
PS G:\Steed\Documents\Go\src\gopl\output\flag\tempconv2> go run main.go -temp 36.7C
*main.Celsius, 36.7°C
PS G:\Steed\Documents\Go\src\gopl\output\flag\tempconv2>

默認(rèn)值打印的格式和打印只的格式是有區(qū)別的,這是因?yàn)轭愋筒煌?,調(diào)用了不同的 String 方法。
這里默認(rèn)值顯示的格式是根據(jù)接口類型的String方法定義的,在這里就是 *celsiusValue 類型的String方法。而后面打印的是 Celsius 類型,使用的是 Celsius 類型的 String 方法。這里定義了兩個(gè)String方法,但是打印的效果又不同,顯示不統(tǒng)一,這樣的做法不夠好。這里可以看出兩個(gè)問(wèn)題:

  1. 最初,使用結(jié)構(gòu)體匿名封裝的形式,避免了重復(fù)定義 String 方法。這樣就保證了自定義的結(jié)構(gòu)體類型 celsiusFlag 的String方法就是原本的 Celsius 類型的String方法。
  2. 幫助消息中打印的默認(rèn)值,實(shí)際是打印自定義類型的值。而自定義類型只在flag包中有用,解析完成后使用的都是原本的類型,這里就是 Celsius 類型。這兩個(gè)類型的String方法最好能保持一致。

所以,使用結(jié)構(gòu)體封裝應(yīng)該是一種不錯(cuò)的實(shí)現(xiàn)方式。不過(guò)flag包中的 time.Duration 類型用的就是類型別名來(lái)實(shí)現(xiàn)的:

type durationValue time.Duration

func (d *durationValue) Set(s string) error {
    v, err := time.ParseDuration(s)
    *d = durationValue(v)
    return err
}

func (d *durationValue) String() string { return (*time.Duration)(d).String() }

上面是源碼中的部分代碼,可以看出這里保持一致的方法是進(jìn)行類型轉(zhuǎn)換后,調(diào)用原來(lái)類型的String方法??赡茉径x的是值類型的String方法,也可能直接就是定義了指針類型的String方法,不過(guò)指針類型的方法包括了所有值類型的方法,所以這里不必關(guān)系原本類型的方法具體是指針?lè)椒ㄟ€是值方法。
所以最后一個(gè)示例中的String方法也可以做同樣的修改:

func (c *celsiusValue) String() string { return (*Celsius)(f).String() }

自定義切片

以字符串切片為例,這里有兩種實(shí)現(xiàn)的思路。一種是直接提供一個(gè)字符串,然后做分隔得到切片:

package main

import (
    "flag"
    "fmt"
    "strings"
)

type fullName []string

func (v *fullName) String() string {
    r := []string{}
    for _, s := range *v {
        r = append(r, fmt.Sprintf("%q", s))
    }
    return strings.Join(r, " ")
}

func (v *fullName) Set(s string) error {
    *v = nil
    // strings.Fields 可以區(qū)分連續(xù)的空格
    for _, str := range strings.Fields(s) {
        *v = append(*v, str)
    }
    return nil
}

func FullName(name string, value []string, usage string) *[]string {
    p := new([]string) // value 是傳值進(jìn)來(lái)的,取不到地址,new一個(gè)內(nèi)存空間,存放value的值
    *p = value
    flag.CommandLine.Var((*fullName)(p), name, usage)
    return p
}

func main() {
    s := FullName("name", []string{"Karl", "Lichter", "Von", "Randoll"}, "全名")
    flag.Parse()
    fmt.Printf("% q\n", *s)
}

這里就不管 String 方法和 Set 方法展示規(guī)格的一致了,String方法采用 %q 的輸出形式可以更好的把每一個(gè)元素清楚的展示出來(lái)。

還有一種方式是,可以多次調(diào)用同一個(gè)參數(shù),每一次調(diào)用,就添加一個(gè)元素:

package main

import (
    "flag"
    "fmt"
    "strings"
)

type urls []string

func (v *urls) String() string {
    r := []string{}
    for _, s := range *v {
        r = append(r, fmt.Sprintf("%q", s))
    }
    return strings.Join(r, ", ")
}

func (v *urls) Set(s string) error {
    // *v = nil // 不能再清空原有的記錄了
    // strings.Fields 可以區(qū)分連續(xù)的空格
    *v = append(*v, s)
    return nil
}

func Urls(name string, value []string, usage string) *[]string {
    p := new([]string) // value 是傳值進(jìn)來(lái)的,取不到地址,new一個(gè)內(nèi)存空間,存放value的值
    *p = value
    flag.CommandLine.Var((*urls)(p), name, usage)
    return p
}

func main() {
    s := Urls("url", []string{"baidu.com"}, "域名")
    flag.Parse()
    fmt.Printf("% q\n", *s)
}

由于每出現(xiàn)一個(gè)參數(shù),都會(huì)調(diào)用一次 Set 方法,所以只要在 Set 里對(duì)切片進(jìn)行append就可以了。不過(guò)這也帶來(lái)一個(gè)問(wèn)題,就是默認(rèn)值無(wú)法被覆蓋掉:

PS G:\Steed\Documents\Go\src\gopl\output\flag\urls> go run main.go -h
Usage of C:\Users\Steed\AppData\Local\Temp\go-build727433198\b001\exe\main.exe:
  -url value
        域名 (default "baidu.com")
exit status 2
PS G:\Steed\Documents\Go\src\gopl\output\flag\urls> go run main.go
["baidu.com"]
PS G:\Steed\Documents\Go\src\gopl\output\flag\urls> go run main.go -url shuxun.net -url 51cto.com
["baidu.com" "shuxun.net" "51cto.com"]
PS G:\Steed\Documents\Go\src\gopl\output\flag\urls>

下面這個(gè)版本的Set方法引入了一個(gè)全局變量,可以改進(jìn)上面的問(wèn)題:

var isNew bool
func (v *urls) Set(s string) error {
    if !isNew {
        *v = nil
        isNew = true
    }
    *v = append(*v, s)
    return nil
}

這里是一個(gè)方法,無(wú)法改成閉包。最好的做法就是將這個(gè)變量和原本的字符串切片封裝為一個(gè)結(jié)構(gòu)體:

type urls struct {
    data  []string
    isNew bool
}

剩下的修改,參考之前自定義溫度解析的實(shí)現(xiàn)就差不多了。

簡(jiǎn)易的自定義版本

要實(shí)現(xiàn)自定義類型,只需要實(shí)現(xiàn)接口就可以了。不過(guò)上面的例子中都額外寫了一個(gè)函數(shù),用于返回自定義類型的指針,并且還設(shè)置了默認(rèn)值。這個(gè)方法內(nèi)部也是調(diào)用 Var 方法。這里可以直接使用 flag 包里的 Var 函數(shù)調(diào)用全局的Var方法:

package main

import (
    "flag"
    "fmt"
    "strings"
)

type urls []string

func (v *urls) String() string {
    // *v = []string{"baidu.com"} // 通過(guò)指針改變初始值
    r := []string{}
    for _, s := range *v {
        r = append(r, fmt.Sprintf("%q", s))
    }
    return strings.Join(r, ", ")
}

var isNew bool
func (v *urls) Set(s string) error {
    if !isNew {
        *v = nil
        isNew = true
    }
    *v = append(*v, s)
    return nil
}

func main() {
    var value urls
    // value = append(value, "baidu.com") // 傳遞給Var函數(shù)前就設(shè)定好初始值
    flag.Var(&value, "url", "域名")
    flag.Parse()
    fmt.Printf("%T % [1]q\n", value)
    s := []string(value)
    fmt.Printf("%T % [1]q\n", s)
}

這里提供了兩個(gè)設(shè)置初始值的方法,示例中都注釋掉了。
String 方法由于內(nèi)部是獲得指針的,所以可以對(duì)變量進(jìn)行修改。并且該方法調(diào)用的時(shí)機(jī)是在解析開始時(shí)只調(diào)用一次。所以在 String 方法里設(shè)置默認(rèn)值是可行的。不過(guò)無(wú)法在打印幫助的時(shí)候把默認(rèn)值打印出來(lái)。不需要這么做,但是正好可以對(duì)String方法有進(jìn)一步的了解,還有就是這里利用指針修改參數(shù)原值的思路。
另外,由于 Var 函數(shù)需要接收一個(gè)變量,所以在定義變量的時(shí)候,就可以賦一個(gè)初始值。并且在打印幫助的時(shí)候是可以把這個(gè)初始值打印出來(lái)的。
不過(guò)簡(jiǎn)易版本最大的問(wèn)題就是 Var 函數(shù)接收和返回的值都是 Value 接口類型。所以在使用之前,需要對(duì)返回值做一次類型轉(zhuǎn)換。而設(shè)置初始值也是對(duì) Value 接口類型的值進(jìn)行設(shè)置。主要問(wèn)題就是對(duì)外暴露了 Value 類型?,F(xiàn)在調(diào)用者必須知道并且使用 Value 類型,對(duì) Value 類型進(jìn)行處理,這樣就不是很友好。而之前的示例中,調(diào)用方(就是main函數(shù)中的那些代碼)是完全可以忽略 Value 的存在的。
小結(jié):這一小段主要是為了說(shuō)明,之前示例中額外定義的函數(shù)是非常好的做法,封裝了 flag 內(nèi)部接口的細(xì)節(jié)。經(jīng)過(guò)這個(gè)函數(shù)封裝后再提供給用戶使用,用戶就可以完全忽略 flag.Value 這個(gè)接口而直接操作真正需要的類型了。這個(gè)函數(shù)的作用就是封裝接口的所有細(xì)節(jié),調(diào)用者只需要關(guān)注真正需要的操作的類型。

自定義命令參數(shù)容器

接下來(lái)就是通過(guò)包提供的方法行進(jìn)一步的自定制。以下3小節(jié)是一層一層更加接近底層的調(diào)用,做更加深入的定制。

定制 Usage

回到最基本的使用,打印一下幫助消息可以得到以下的內(nèi)容:

PS H:\Go\src\gopl\output\flag\beginning> go run main.go -h
Usage of C:\Users\Steed\AppData\Local\Temp\go-build926710106\b001\exe\main.exe:
  -age int
        年齡 (default 18)
  -name string
        名字 (default "Adam")
exit status 2
PS H:\Go\src\gopl\output\flag\beginning>

這里關(guān)注第一行,在 Usage of 后面是一長(zhǎng)串的路徑,這個(gè)是go run命令在構(gòu)建上述命令源碼文件時(shí)臨時(shí)生成的可執(zhí)行文件的完整路徑。如果是編譯之后再執(zhí)行,就是可執(zhí)行文件的相對(duì)路徑,就沒那么難看了。
這一行的內(nèi)容也是可以自定制的,但是首先來(lái)看看源碼里的實(shí)現(xiàn):

var Usage = func() {
    fmt.Fprintf(CommandLine.Output(), "Usage of %s:\n", os.Args[0])
    PrintDefaults()
}

func (f *FlagSet) Output() io.Writer {
    if f.output == nil {
        return os.Stderr
    }
    return f.output
}

看上面的代碼就清楚了,輸出的內(nèi)容就是執(zhí)行的命令本身 os.Args[0]。就會(huì)輸出的位置默認(rèn)就是標(biāo)準(zhǔn)錯(cuò)誤 os.Stderr。
這個(gè) Usage 是可導(dǎo)出的變量,值是一個(gè)匿名函數(shù),只要重新為 Usage 賦一個(gè)新值就可以完成內(nèi)容的自定制:

var name string

func init() {
    flag.StringVar(&name, "name", "Adam", "名字")
    flag.Usage = func() {
        fmt.Fprintln(os.Stderr, "請(qǐng)指定名字和年齡:")
        flag.PrintDefaults()
    }
}

var ageP = flag.Int("age", 18, "年齡")

func main() {
    flag.Parse()
    fmt.Printf("%T %[1]v\n", name)
    fmt.Printf("%T %[1]v\n", ageP)
    fmt.Printf("%T %[1]v\n", *ageP)
}

只要在 flag.Parse() 執(zhí)行前覆蓋掉 flag.Usage 即可。
下面那行 flag.PrintDefaults() 則是打印幫助信息中其他的內(nèi)容。完全可以把這行去掉,這里完全可以自定義打印更多其他內(nèi)容,甚至是執(zhí)行其他操作。

定制 CommandLine

在調(diào)用flag包中的一些函數(shù)(比如StringVar、Parse等等)的時(shí)候,實(shí)際上是在調(diào)用flag.CommandLine變量的對(duì)應(yīng)方法。
flag.CommandLine相當(dāng)于默認(rèn)情況下的命令參數(shù)容?。通過(guò)對(duì)flag.CommandLine重新賦值,就可以更深層次地定制當(dāng)前命令源碼文件的參數(shù)使用說(shuō)明。
flag包提供了NewFlagSet函數(shù)用于創(chuàng)建自定制的 CommandLine 。在上一個(gè)簡(jiǎn)單例子的基礎(chǔ)上,修改一下其中的init函數(shù)的內(nèi)容:

var name string
var age int

func init() {
    flag.CommandLine = flag.NewFlagSet("", flag.ExitOnError)
    flag.StringVar(&name, "name", "Adam", "名字")
    age = *flag.Int("age", 18, "年齡")
    // 和上面兩句效果一樣
    // flag.CommandLine.StringVar(&name, "name", "Adam", "名字")
    // var ageP = flag.CommandLine.Int("age", 18, "年齡")
    flag.CommandLine.Usage = func() {
        fmt.Fprintln(os.Stderr, "請(qǐng)指定名字和年齡:")
        flag.PrintDefaults()
    }
}

其實(shí)這里只加了一行語(yǔ)句。所有flag包的操作都要在flag.NewFlagSet執(zhí)行之后,否則之前執(zhí)行的內(nèi)容會(huì)被覆蓋掉。所以這里把flag.Int的調(diào)用移到了包內(nèi),否則在全局中的賦值語(yǔ)句會(huì)在這之前就運(yùn)行了,然后被flag.NewFlagSet方法覆蓋掉。
這里無(wú)論是 flag.StringVar 或者是 flag.CommandLine.StringVar,最終都是使用flag.NewFlagSet創(chuàng)建的 *FlagSet 對(duì)象的方法來(lái)調(diào)用的。不過(guò)本質(zhì)上是有差別的:

  • flag.StringVar : 使用默認(rèn) CommandLine 的對(duì)象調(diào)用,但是第一行語(yǔ)句 flag.CommandLine = flag.NewFlagSet("", flag.ExitOnError) 則是把它的值覆蓋為新創(chuàng)建的對(duì)象。
  • flag.CommandLine.StringVar : 使用 flag.NewFlagSet 函數(shù)創(chuàng)建的對(duì)象來(lái)調(diào)用,所以和上面是一個(gè)東西。

第一個(gè)方式是專門為默認(rèn)的容器提供的便捷調(diào)用方式。第二個(gè)是則是通用的方法,之后創(chuàng)建私有命令參數(shù)容器的時(shí)候就需要用通用的方式來(lái)調(diào)用了。
Usage 必須用flag.CommandLine調(diào)用。另外不定制的話,包里也準(zhǔn)備了默認(rèn)的方法可以使用:

func (f *FlagSet) defaultUsage() {
    if f.name == "" {
        fmt.Fprintf(f.Output(), "Usage:\n")
    } else {
        fmt.Fprintf(f.Output(), "Usage of %s:\n", f.name)
    }
    f.PrintDefaults()
}

第一個(gè)參數(shù)的作用基本就是顯示一個(gè)名稱,也可以用空字符串,向上面這樣。而第二個(gè)參數(shù)可以是下面三種常量:

const (
    ContinueOnError ErrorHandling = iota // Return a descriptive error.
    ExitOnError                          // Call os.Exit(2).
    PanicOnError                         // Call panic with a descriptive error.
)

效果一看就明白了。定義在解析遇到問(wèn)題后,是執(zhí)行何種操作。默認(rèn)的就是ExitOnError,所以在--help執(zhí)行打印說(shuō)明后,最后一行會(huì)出現(xiàn)“exit status 2”,以狀態(tài)碼2退出。這里可以根據(jù)需要定制為拋出Panic。
使用-h參數(shù)打印幫助信息也算是解析出錯(cuò),如果是Panic則會(huì)在打印幫助信息后Panic,如果是Continue則先打印幫助信息然后按照默認(rèn)值執(zhí)行。所以如果要使用另外兩種模式,最好修改一下-h參數(shù)的行為,就是上面講的定制Usage。使用-h參數(shù)之后程序?qū)?zhí)行的就是Usage指定的函數(shù)。

創(chuàng)建私有命令參數(shù)容器

上一個(gè)例子依然是使用flag包提供的命令參數(shù)容器,只是重新進(jìn)行了創(chuàng)建和賦值。這里依然是調(diào)用flag.NewFlagSet()函數(shù)創(chuàng)建命令參數(shù)容器,不過(guò)這次賦值給自定義的變量:

package main

import (
    "flag"
    "fmt"
    "os"
)

var cmdLine = flag.NewFlagSet("", flag.ExitOnError)
var name string
var age int

func init() {
    cmdLine.StringVar(&name, "name", "Adam", "名字")
    age = *cmdLine.Int("age", 18, "年齡")
}

func main() {
    cmdLine.Parse(os.Args[1:])
    fmt.Printf("%T %[1]v\n", name)
    fmt.Printf("%T %[1]v\n", age)
}

首先通過(guò) flag.NewFlagSet 函數(shù)創(chuàng)建了私有的命令參數(shù)容器。然后調(diào)用其他方法的接收者都使用這個(gè)容器。另外還有很多方法可以調(diào)用,可以繼續(xù)探索。

向AI問(wèn)一下細(xì)節(jié)

免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點(diǎn)不代表本網(wǎng)站立場(chǎng),如果涉及侵權(quán)請(qǐng)聯(lián)系站長(zhǎng)郵箱:is@yisu.com進(jìn)行舉報(bào),并提供相關(guān)證據(jù),一經(jīng)查實(shí),將立刻刪除涉嫌侵權(quán)內(nèi)容。

AI