7.4. flag.Value接口

在本章,我們會學到另一個標準的接口類型flag.Value是怎麼幫助命令行標記定義新的符號的。思考下面這個會休眠特定時間的程序:

gopl.io/ch7/sleep

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()
}

在它休眠前它會打印出休眠的時間週期。fmt包調用time.Duration的String方法打印這個時間週期是以用戶友好的註解方式,而不是一個納秒數字:

$ go build gopl.io/ch7/sleep
$ ./sleep
Sleeping for 1s...

默認情況下,休眠週期是一秒,但是可以通過 -period 這個命令行標記來控制。flag.Duration函數創建一個time.Duration類型的標記變量並且允許用戶通過多種用戶友好的方式來設置這個變量的大小,這種方式還包括和String方法相同的符號排版形式。這種對稱設計使得用戶交互良好。

$ ./sleep -period 50ms
Sleeping for 50ms...
$ ./sleep -period 2m30s
Sleeping for 2m30s...
$ ./sleep -period 1.5h
Sleeping for 1h30m0s...
$ ./sleep -period "1 day"
invalid value "1 day" for flag -period: time: invalid duration 1 day

因為時間週期標記值非常的有用,所以這個特性被構建到了flag包中;但是我們為我們自己的數據類型定義新的標記符號是簡單容易的。我們只需要定義一個實現flag.Value接口的類型,如下:

package flag

// Value is the interface to the value stored in a flag.
type Value interface {
    String() string
    Set(string) error
}

String方法格式化標記的值用在命令行幫助消息中;這樣每一個flag.Value也是一個fmt.Stringer。Set方法解析它的字符串參數並且更新標記變量的值。實際上,Set方法和String是兩個相反的操作,所以最好的辦法就是對他們使用相同的註解方式。

讓我們定義一個允許通過攝氏度或者華氏溫度變換的形式指定溫度的celsiusFlag類型。注意celsiusFlag內嵌了一個Celsius類型(§2.5),因此不用實現本身就已經有String方法了。為了實現flag.Value,我們只需要定義Set方法:

gopl.io/ch7/tempconv

// *celsiusFlag satisfies the flag.Value interface.
type celsiusFlag struct{ Celsius }

func (f *celsiusFlag) Set(s string) error {
    var unit string
    var value float64
    fmt.Sscanf(s, "%f%s", &value, &unit) // no error check needed
    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函數從輸入s中解析一個浮點數(value)和一個字符串(unit)。雖然通常必須檢查Sscanf的錯誤返回,但是在這個例子中我們不需要因為如果有錯誤發生,就沒有switch case會匹配到。

下面的CelsiusFlag函數將所有邏輯都封裝在一起。它返回一個內嵌在celsiusFlag變量f中的Celsius指針給調用者。Celsius字段是一個會通過Set方法在標記處理的過程中更新的變量。調用Var方法將標記加入應用的命令行標記集合中,有異常複雜命令行接口的全局變量flag.CommandLine.Programs可能有幾個這個類型的變量。調用Var方法將一個*celsiusFlag參數賦值給一個flag.Value參數,導致編譯器去檢查*celsiusFlag是否有必須的方法。

// CelsiusFlag defines a Celsius flag with the specified name,
// default value, and usage, and returns the address of the flag variable.
// The flag argument must have a quantity and a unit, e.g., "100C".
func CelsiusFlag(name string, value Celsius, usage string) *Celsius {
    f := celsiusFlag{value}
    flag.CommandLine.Var(&f, name, usage)
    return &f.Celsius
}

現在我們可以開始在我們的程序中使用新的標記:

gopl.io/ch7/tempflag

var temp = tempconv.CelsiusFlag("temp", 20.0, "the temperature")

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

下面是典型的場景:

$ go build gopl.io/ch7/tempflag
$ ./tempflag
20°C
$ ./tempflag -temp -18C
-18°C
$ ./tempflag -temp 212°F
100°C
$ ./tempflag -temp 273.15K
invalid value "273.15K" for flag -temp: invalid temperature "273.15K"
Usage of ./tempflag:
  -temp value
        the temperature (default 20°C)
$ ./tempflag -help
Usage of ./tempflag:
  -temp value
        the temperature (default 20°C)

練習 7.6: 對tempFlag加入支持開爾文溫度。

練習 7.7: 解釋為什麼幫助信息在它的默認值是20.0沒有包含°C的情況下輸出了°C。