Go 每日一庫之 flag
緣起
我一直在想,有什麼方式可以讓人比較輕易地保持每日學習,持續輸出的狀態。寫部落格是一種方式,但不是每天都有想寫的,值得寫的東西。
有時候一個技術比較複雜,寫部落格的時候經常會寫著寫著發現自己的理解有偏差,或者細節還沒有完全掌握,要去查資料,瞭解了之後又繼續寫,如此反覆。
這樣會導致一篇部落格的耗時過長。
我在每天瀏覽思否、掘金和Github的過程中,發現一些比較好的想法,有JS 每日一題,NodeJS 每日一庫,每天一道面試題等等等等。
https://github.com/parro-it/awesome-micro-npm-packages這個倉庫收集 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 埠。
快速使用
學習一個庫的第一步當然是使用它。我們先看看flag
庫的基本使用:
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) }
可以先編譯程式,然後執行(我使用的是 Win10 + Git Bash):
$ go build -o main.exe main.go
$ ./main.exe -intflag 12 -boolflag 1 -stringflag test
輸出:
int flag: 12
bool flag: true
string flag: test
如果不設定某個選項,相應變數會取預設值:
$ ./main.exe -intflag 12 -boolflag 1
輸出:
int flag: 12
bool flag: true
string flag: default
可以看到沒有設定的選項stringflag
為預設值default
。
還可以直接使用go run
,這個命令會先編譯程式生成可執行檔案,然後執行該檔案,將命令列中的其它選項傳給這個程式。
$ go run main.go -intflag 12 -boolflag 1
可以使用-h
顯示選項幫助資訊:
$ ./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")
總結一下,使用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
庫支援三種命令列選項格式。
-flag
-flag=x
-flag x
-
和--
都可以使用,它們的作用是一樣的。有些庫使用-
表示短選項,--
表示長選項。相對而言,flag
使用起來更簡單。
第一種形式只支援布林型別的選項,出現即為true
,不出現為預設值。
第三種形式不支援布林型別的選項。因為這種形式的布林選項在類 Unix 系統中可能會出現意想不到的行為。看下面的命令:
cmd -x *
其中,*
是 shell 萬用字元。如果有名字為 0、false的檔案,布林選項-x
將會取false
。反之,布林選項-x
將會取true
。而且這個選項消耗了一個引數。
如果要顯示設定一個布林選項為false
,只能使用-flag=false
這種形式。
遇到第一個非選項引數(即不是以-
和--
開頭的)或終止符--
,解析停止。執行下面程式:
$ ./main.exe noflag -intflag 12
將會輸出:
int flag: 0
bool flag: false
string flag: default
因為解析遇到noflag
就停止了,後面的選項-intflag
沒有被解析到。所以所有選項都取的預設值。
執行下面的程式:
$ ./main.exe -intflag 12 -- -boolflag=true
將會輸出:
int flag: 12
bool flag: false
string flag: default
首先解析了選項intflag
,設定其值為 12。遇到--
後解析終止了,後面的--boolflag=true
沒有被解析到,所以boolflag
選項取預設值false
。
解析終止之後如果還有命令列引數,flag
庫會儲存下來,通過flag.Args
方法返回這些引數的切片。
可以通過flag.NArg
方法獲取未解析的引數數量,flag.Arg(i)
訪問位置i
(從 0 開始)上的引數。
選項個數也可以通過呼叫flag.NFlag
方法獲取。
稍稍修改一下上面的程式:
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())
}
編譯執行該程式:
$ go build -o main.exe main.go
$ ./main.exe -intflag 12 -- -stringflag test
輸出:
[-stringflag test]
Non-Flag Argument Count: 2
Argument 0: -stringflag
Argument 1: test
解析遇到--
終止後,剩餘引數-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
等)會自動為我們分配變數,返回該變數的地址。用法與前一種方式類似:
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)
}
編譯並執行程式:
$ go build -o main.exe main.go
$ ./main.exe -intflag 12
將輸出:
int flag: 12
bool flag: false
string flag: default
除了使用時需要解引用,其它與前一種方式基本相同。
高階用法
定義短選項
flag
庫並沒有顯示支援短選項,但是可以通過給某個相同的變數設定不同的選項來實現。即兩個選項共享同一個變數。
由於初始化順序不確定,必須保證它們擁有相同的預設值。否則不傳該選項時,行為是不確定的。
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)
}
編譯、執行程式:
$ go build -o main.exe main.go
$ ./main.exe -log_type WARNING
$ ./main.exe -l WARNING
使用長、短選項均輸出:
log level: WARNING
不傳入該選項,輸出預設值:
$ ./main.exe
log level: DEBUG
解析時間間隔
除了能使用基本型別作為選項,flag
庫還支援time.Duration
型別,即時間間隔。時間間隔支援的格式非常之多,例如"300ms"、"-1.5h"、"2h45m"等等等等。
時間單位可以是 ns/us/ms/s/m/h/day 等。實際上flag
內部會呼叫time.ParseDuration
。具體支援的格式可以參見time(需fq)庫的文件。
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()
}
根據傳入的命令列選項period
,程式睡眠相應的時間,預設 1 秒。編譯、執行程式:
$ go build -o main.exe main.go
$ ./main.exe
Sleeping for 1s...
$ ./main.exe -period 1m30s
Sleeping for 1m30s...
自定義選項
除了使用flag
庫提供的選項型別,我們還可以自定義選項型別。我們分析一下標準庫中提供的案例:
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)
}
首先定義一個新型別,這裡定義型別interval
。
新型別必須實現flag.Value
介面:
// src/flag/flag.go
type Value interface {
String() string
Set(string) error
}
其中String
方法格式化該型別的值,flag.Parse
方法在執行時遇到自定義型別的選項會將選項值作為引數呼叫該型別變數的Set
方法。
這裡將以,
分隔的時間間隔解析出來存入一個切片中。
自定義型別選項的定義必須使用flag.Var
方法。
編譯、執行程式:
$ go build -o main.exe main.go
$ ./main.exe -deltaT 30s
[30s]
$ ./main.exe -deltaT 30s,1m,1m30s
[30s 1m0s 1m30s]
如果指定的選項值非法,Set
方法返回一個error
型別的值,Parse
執行終止,列印錯誤和使用幫助。
$ ./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
解析程式中的字串
有時候選項並不是通過命令列傳遞的。例如,從配置表中讀取或程式生成的。這時候可以使用flag.FlagSet
結構的相關方法來解析這些選項。
實際上,我們前面呼叫的flag
庫的方法,都會間接呼叫FlagSet
結構的方法。flag
庫中定義了一個FlagSet
型別的全域性變數CommandLine
專門用於解析命令列選項。
前面呼叫的flag
庫的方法只是為了提供便利,它們內部都是呼叫的CommandLine
的相應方法:
// 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) }
同樣的,我們也可以自己建立FlagSet
型別變數來解析選項。
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)
}
NewFlagSet
方法有兩個引數,第一個引數是程式名稱,輸出幫助或出錯時會顯示該資訊。第二個引數是解析出錯時如何處理,有幾個選項:
ContinueOnError
:發生錯誤後繼續解析,CommandLine
就是使用這個選項;ExitOnError
:出錯時呼叫os.Exit(2)
退出程式;PanicOnError
:出錯時產生 panic。
隨便看一眼flag
庫中的相關程式碼:
// 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
}
與直接使用flag
庫的方法有一點不同,FlagSet
呼叫Parse
方法時需要顯示傳入字串切片作為引數。因為flag.Parse
在內部呼叫了CommandLine.Parse(os.Args[1:])
。
示例程式碼都放在GitHub上了。
參考
- flag庫文件
我
我的部落格
歡迎關注我的微信公眾號【GoUpUp】,共同學習,一起進步~
本文由部落格一文多發平臺 OpenWrite 釋出!