Go 每日一庫之 flag

熱烈歡迎你,相識是一種緣分,Echa 哥為了你的到來特意準備了一份驚喜,go學習資料《 》

緣起

我一直在想,有什麼方式可以讓人比較輕易地保持每日學習,持續輸出的狀態。寫博客是一種方式,但不是每天都有想寫的,值得寫的東西。 有時候一個技術比較複雜,寫博客的時候經常會寫著寫著發現自己的理解有偏差,或者細節還沒有完全掌握,要去查資料,瞭解了之後又繼續寫,如此反覆。 這樣會導致一篇博客的耗時過長。


Go 每日一庫之 flag


我在每天瀏覽思否、掘金和Github的過程中,發現一些比較好的想法,有JS 每日一題NodeJS 每日一庫每天一道面試題等等等等。 github.com/parro-it/aw…這個倉庫收集 NodeJS 小型庫,一天看一個不是夢!這也是我這個系列的靈感。 我計劃每天學習一個 Go 語言的庫,輸出一篇介紹型的博文。每天一庫當然是理想狀態,我心中的預期是一週 3-5 個。

今天是第一天,我們從一個基礎庫聊起————Go 標準庫中的flag。

簡介

flag用於解析命令行選項。有過類 Unix 系統使用經驗的童鞋對命令行選項應該不陌生。例如命令ls -al列出當前目錄下所有文件和目錄的詳細信息,其中-al就是命令行選項。

命令行選項在實際開發中很常用,特別是在寫工具的時候。

  • 指定配置文件的路徑,如redis-server ./redis.conf與當前目錄下的配置文件redis.conf啟動 Redis 服務器;
  • 自定義某些參數,如python -m SimpleHTTPServer 8080啟動一個 HTTP 服務器,監聽 8080 端口。如果不指定,則默認監聽 8000 端口。

Go 每日一庫之 flag

快速使用

學習一個庫的第一步當然是使用它。我們先看看flag庫的基本使用:

<code>package main

import (
"fmt"
"flag"
)

var (
intflag int
boolflag bool
stringflag string
)

func init() {
flag.IntVar(&intflag, "intflag", 0, "int flag value")
flag.BoolVar(&boolflag, "boolflag", false, "bool flag value")
flag.StringVar(&stringflag, "stringflag", "default", "string flag value")
}

func main() {
flag.Parse()

fmt.Println("int flag:", intflag)
fmt.Println("bool flag:", boolflag)
fmt.Println("string flag:", stringflag)
}
複製代碼/<code>

可以先編譯程序,然後運行(我使用的是 Win10 + Git Bash):

<code>$ go build -o main.exe main.go
$ ./main.exe -intflag 12 -boolflag 1 -stringflag test
複製代碼/<code>

輸出:

<code>int flag: 12
bool flag: true

string flag: test
複製代碼/<code>

如果不設置某個選項,相應變量會取默認值:

<code>$ ./main.exe -intflag 12 -boolflag 1
複製代碼/<code>

輸出:

<code>int flag: 12
bool flag: true
string flag: default
複製代碼/<code>

可以看到沒有設置的選項stringflag為默認值default。

還可以直接使用go run,這個命令會先編譯程序生成可執行文件,然後執行該文件,將命令行中的其它選項傳給這個程序。

<code>$ go run main.go -intflag 12 -boolflag 1
複製代碼/<code>

可以使用-h顯示選項幫助信息:

<code>$ ./main.exe -h
Usage of D:\\code\\golang\\src\\github.com\\darjun\\cmd\\flag\\main.exe:
-boolflag
bool flag value
-intflag int
int flag value
-stringflag string
string flag value (default "default")
複製代碼/<code>

總結一下,使用flag庫的一般步驟:

  • 定義一些全局變量存儲選項的值,如這裡的intflag/boolflag/stringflag;
  • 在init方法中使用flag.TypeVar方法定義選項,這裡的Type可以為基本類型Int/Uint/Float64/Bool,還可以是時間間隔time.Duration。定義時傳入變量的地址、選項名、默認值和幫助信息;
  • 在main方法中調用flag.Parse從os.Args[1:]中解析選項。因為os.Args[0]為可執行程序路徑,會被剔除。

注意點:

flag.Parse方法必須在所有選項都定義之後調用,且flag.Parse調用之後不能再定義選項。如果按照前面的步驟,基本不會出現問題。 因為init在所有代碼之前執行,將選項定義都放在init中,main函數中執行flag.Parse時所有選項都已經定義了。

選項格式

flag庫支持三種命令行選項格式。

<code>-flag
-flag=x
-flag x
複製代碼/<code>

-和--都可以使用,它們的作用是一樣的。有些庫使用-表示短選項,--表示長選項。相對而言,flag使用起來更簡單。

第一種形式只支持布爾類型的選項,出現即為true,不出現為默認值。 第三種形式不支持布爾類型的選項。因為這種形式的布爾選項在類 Unix 系統中可能會出現意想不到的行為。看下面的命令:

<code>cmd -x *
複製代碼/<code>

其中,*是 shell 通配符。如果有名字為 0、false的文件,布爾選項-x將會取false。反之,布爾選項-x將會取true。而且這個選項消耗了一個參數。 如果要顯示設置一個布爾選項為false,只能使用-flag=false這種形式。

遇到第一個非選項參數(即不是以-和--開頭的)或終止符--,解析停止。運行下面程序:

<code>$ ./main.exe noflag -intflag 12
複製代碼/<code>

將會輸出:

<code>int flag: 0
bool flag: false
string flag: default
複製代碼/<code>

因為解析遇到noflag就停止了,後面的選項-intflag沒有被解析到。所以所有選項都取的默認值。

運行下面的程序:

<code>$ ./main.exe -intflag 12 -- -boolflag=true
複製代碼/<code>

將會輸出:

<code>int flag: 12
bool flag: false
string flag: default
複製代碼/<code>

首先解析了選項intflag,設置其值為 12。遇到--後解析終止了,後面的--boolflag=true沒有被解析到,所以boolflag選項取默認值false。

解析終止之後如果還有命令行參數,flag庫會存儲下來,通過flag.Args方法返回這些參數的切片。 可以通過flag.NArg方法獲取未解析的參數數量,flag.Arg(i)訪問位置i(從 0 開始)上的參數。 選項個數也可以通過調用flag.NFlag方法獲取。


Go 每日一庫之 flag


稍稍修改一下上面的程序:

<code>func main() {
flag.Parse()

fmt.Println(flag.Args())
fmt.Println("Non-Flag Argument Count:", flag.NArg())
for i := 0; i < flag.NArg(); i++ {
fmt.Printf("Argument %d: %s\\n", i, flag.Arg(i))
}

fmt.Println("Flag Count:", flag.NFlag())
}
複製代碼/<code>

編譯運行該程序:

<code>$ go build -o main.exe main.go
$ ./main.exe -intflag 12 -- -stringflag test
複製代碼/<code>

輸出:

<code>[-stringflag test]
Non-Flag Argument Count: 2
Argument 0: -stringflag
Argument 1: test
複製代碼/<code>

解析遇到--終止後,剩餘參數-stringflag test保存在flag中,可以通過Args/NArg/Arg等方法訪問。

整數選項值可以接受 1234(十進制)、0664(八進制)和 0x1234(十六進制)的形式,並且可以是負數。實際上flag在內部使用strconv.ParseInt方法將字符串解析成int。 所以理論上,ParseInt接受的格式都可以。

布爾類型的選項值可以為:

  • 取值為true的:1、t、T、true、TRUE、True;
  • 取值為false的:0、f、F、false、FALSE、False。

另一種定義選項的方式

上面我們介紹了使用flag.TypeVar定義選項,這種方式需要我們先定義變量,然後變量的地址。 還有一種方式,調用flag.Type(其中Type可以為Int/Uint/Bool/Float64/String/Duration等)會自動為我們分配變量,返回該變量的地址。用法與前一種方式類似:

<code>package main

import (
"fmt"
"flag"
)

var (
intflag *int
boolflag *bool
stringflag *string
)

func init() {
intflag = flag.Int("intflag", 0, "int flag value")
boolflag = flag.Bool("boolflag", false, "bool flag value")
stringflag = flag.String("stringflag", "default", "string flag value")
}

func main() {
flag.Parse()

fmt.Println("int flag:", *intflag)
fmt.Println("bool flag:", *boolflag)
fmt.Println("string flag:", *stringflag)
}
複製代碼/<code>

編譯並運行程序:

<code>$ go build -o main.exe main.go
$ ./main.exe -intflag 12
複製代碼/<code>

將輸出:

<code>int flag: 12
bool flag: false
string flag: default
複製代碼/<code>

除了使用時需要解引用,其它與前一種方式基本相同。

高級用法

定義短選項

flag庫並沒有顯示支持短選項,但是可以通過給某個相同的變量設置不同的選項來實現。即兩個選項共享同一個變量。 由於初始化順序不確定,必須保證它們擁有相同的默認值。否則不傳該選項時,行為是不確定的。

<code>package main

import (
"fmt"
"flag"
)

var logLevel string

func init() {
const (
defaultLogLevel = "DEBUG"
usage = "set log level value"
)


flag.StringVar(&logLevel, "log_type", defaultLogLevel, usage)
flag.StringVar(&logLevel, "l", defaultLogLevel, usage + "(shorthand)")
}

func main() {
flag.Parse()

fmt.Println("log level:", logLevel)
}
複製代碼/<code>

編譯、運行程序:

<code>$ go build -o main.exe main.go
$ ./main.exe -log_type WARNING
$ ./main.exe -l WARNING
複製代碼/<code>

使用長、短選項均輸出:

<code>log level: WARNING
複製代碼/<code>

不傳入該選項,輸出默認值:

<code>$ ./main.exe
log level: DEBUG
複製代碼/<code>

解析時間間隔

除了能使用基本類型作為選項,flag庫還支持time.Duration類型,即時間間隔。時間間隔支持的格式非常之多,例如"300ms"、"-1.5h"、"2h45m"等等等等。 時間單位可以是 ns/us/ms/s/m/h/day 等。實際上flag內部會調用time.ParseDuration。具體支持的格式可以參見time(需fq)庫的文檔。

<code>package main

import (
"flag"
"fmt"
"time"
)

var (
period time.Duration
)

func init() {
flag.DurationVar(&period, "period", 1*time.Second, "sleep period")
}

func main() {
flag.Parse()
fmt.Printf("Sleeping for %v...", period)
time.Sleep(period)
fmt.Println()
}
複製代碼/<code>

根據傳入的命令行選項period,程序睡眠相應的時間,默認 1 秒。編譯、運行程序:

<code>$ go build -o main.exe main.go
$ ./main.exe
Sleeping for 1s...

$ ./main.exe -period 1m30s
Sleeping for 1m30s...
複製代碼/<code>

自定義選項

除了使用flag庫提供的選項類型,我們還可以自定義選項類型。我們分析一下標準庫中提供的案例:

<code>package main

import (
"errors"
"flag"
"fmt"

"strings"
"time"
)

type interval []time.Duration

func (i *interval) String() string {
return fmt.Sprint(*i)
}

func (i *interval) Set(value string) error {
if len(*i) > 0 {
return errors.New("interval flag already set")
}
for _, dt := range strings.Split(value, ",") {
duration, err := time.ParseDuration(dt)
if err != nil {
return err
}
*i = append(*i, duration)
}
return nil
}

var (
intervalFlag interval
)

func init() {
flag.Var(&intervalFlag, "deltaT", "comma-seperated list of intervals to use between events")
}

func main() {
flag.Parse()

fmt.Println(intervalFlag)
}
複製代碼/<code>

首先定義一個新類型,這裡定義類型interval。

新類型必須實現flag.Value接口:

<code>// src/flag/flag.go
type Value interface {
String() string
Set(string) error

}
複製代碼/<code>

其中String方法格式化該類型的值,flag.Parse方法在執行時遇到自定義類型的選項會將選項值作為參數調用該類型變量的Set方法。 這裡將以,分隔的時間間隔解析出來存入一個切片中。

自定義類型選項的定義必須使用flag.Var方法。

編譯、執行程序:

<code>$ go build -o main.exe main.go
$ ./main.exe -deltaT 30s
[30s]
$ ./main.exe -deltaT 30s,1m,1m30s
[30s 1m0s 1m30s]
複製代碼/<code>

如果指定的選項值非法,Set方法返回一個error類型的值,Parse執行終止,打印錯誤和使用幫助。

<code>$ ./main.exe -deltaT 30x
invalid value "30x" for flag -deltaT: time: unknown unit x in duration 30x
Usage of D:\\code\\golang\\src\\github.com\\darjun\\go-daily-lib\\flag\\self-defined\\main.exe:
-deltaT value
comma-seperated list of intervals to use between events
複製代碼/<code>

解析程序中的字符串

有時候選項並不是通過命令行傳遞的。例如,從配置表中讀取或程序生成的。這時候可以使用flag.FlagSet結構的相關方法來解析這些選項。

實際上,我們前面調用的flag庫的方法,都會間接調用FlagSet結構的方法。flag庫中定義了一個FlagSet類型的全局變量CommandLine專門用於解析命令行選項。 前面調用的flag庫的方法只是為了提供便利,它們內部都是調用的CommandLine的相應方法:

<code>// src/flag/flag.go
var CommandLine = NewFlagSet(os.Args[0], ExitOnError)

func Parse() {
CommandLine.Parse(os.Args[1:])
}

func IntVar(p *int, name string, value int, usage string) {
CommandLine.Var(newIntValue(value, p), name, usage)
}

func Int(name string, value int, usage string) *int {
return CommandLine.Int(name, value, usage)
}

func NFlag() int { return len(CommandLine.actual) }

func Arg(i int) string {
return CommandLine.Arg(i)
}

func NArg() int { return len(CommandLine.args) }
複製代碼/<code>

同樣的,我們也可以自己創建FlagSet類型變量來解析選項。

<code>package main

import (
"flag"
"fmt"
)

func main() {
args := []string{"-intflag", "12", "-stringflag", "test"}

var intflag int

var boolflag bool
var stringflag string

fs := flag.NewFlagSet("MyFlagSet", flag.ContinueOnError)
fs.IntVar(&intflag, "intflag", 0, "int flag value")
fs.BoolVar(&boolflag, "boolflag", false, "bool flag value")
fs.StringVar(&stringflag, "stringflag", "default", "string flag value")

fs.Parse(args)

fmt.Println("int flag:", intflag)
fmt.Println("bool flag:", boolflag)
fmt.Println("string flag:", stringflag)
}
複製代碼/<code>

NewFlagSet方法有兩個參數,第一個參數是程序名稱,輸出幫助或出錯時會顯示該信息。第二個參數是解析出錯時如何處理,有幾個選項:

  • ContinueOnError:發生錯誤後繼續解析,CommandLine就是使用這個選項;
  • ExitOnError:出錯時調用os.Exit(2)退出程序;
  • PanicOnError:出錯時產生 panic。

隨便看一眼flag庫中的相關代碼:

<code>// src/flag/flag.go
func (f *FlagSet) Parse(arguments []string) error {
f.parsed = true
f.args = arguments
for {
seen, err := f.parseOne()
if seen {
continue
}
if err == nil {
break

}
switch f.errorHandling {
case ContinueOnError:
return err
case ExitOnError:
os.Exit(2)
case PanicOnError:
panic(err)
}
}
return nil
}
複製代碼/<code>

與直接使用flag庫的方法有一點不同,FlagSet調用Parse方法時需要顯示傳入字符串切片作為參數。因為flag.Parse在內部調用了CommandLine.Parse(os.Args[1:])。 示例代碼都放在GitHub上了。

參考

  1. flag庫文檔


分享到:


相關文章: