這篇文章主要介紹“Golang中的flag標準庫如何使用”,在日常操作中,相信很多人在Golang中的flag標準庫如何使用問題上存在疑惑,小編查閱了各式資料,整理出簡單好用的操作方法,希望對大家解答”Golang中的flag標準庫如何使用”的疑惑有所幫助!接下來,請跟著小編一起來學習吧!
flag 基本使用示例代碼如下:
package main
import (
"flag"
"fmt"
)
type flagVal struct {
val string
}
func (v *flagVal) String() string {
return v.val
}
func (v *flagVal) Set(s string) error {
v.val = s
return nil
}
func main() {
// 1. 使用 flag.Type() 返回 *int 類型命令行參數
var nFlag = flag.Int("n", 1234, "help message for flag n")
// 2. 使用 flag.TypeVar() 綁定命令行參數到 int 類型變量
var flagvar int
flag.IntVar(&flagvar, "flagvar", 1234, "help message for flagvar")
// 3. 使用 flag.Var() 綁定命令行參數到實現了 flag.Value 接口的自定義類型變量
val := flagVal{}
flag.Var(&val, "val", "help message for val")
// 解析命令行參數
flag.Parse()
fmt.Printf("nFlag: %d\n", *nFlag)
fmt.Printf("flagvar: %d\n", flagvar)
fmt.Printf("val: %+v\n", val)
fmt.Printf("NFlag: %v\n", flag.NFlag()) // 返回已設置的命令行標志個數
fmt.Printf("NArg: %v\n", flag.NArg()) // 返回處理完標志后剩余的參數個數
fmt.Printf("Args: %v\n", flag.Args()) // 返回處理完標志后剩余的參數列表
fmt.Printf("Arg(1): %v\n", flag.Arg(1)) // 返回處理完標志后剩余的參數列表中第 i 項
}可以通過指定 --help/-h 參數來查看這個命令行程序的使用幫助:
$ go run main.go -h
Usage of ./main:
-flagvar int
help message for flagvar (default 1234)
-n int
help message for flag n (default 1234)
-val value
help message for val
這個程序接收三個命令行參數:
int 類型的 -flagvar,默認值為 2134。
int 類型的 -n,默認值為 2134。
value 類型的 -val,無默認值。
我們可以將 -flagvar、-n、-val 稱作 flag,即「標志」,這也是 Go 內置命令行參數解析庫被命名為 flag 的原因,見名知意。
這三個參數在示例代碼中,分別使用了三種不同形式來指定:
flag.Type():
-n 標志是使用 var nFlag = flag.Int("n", 1234, "help message for flag n") 來指定的。
flag.Int 函數簽名如下:
func Int(name string, value int, usage string) *int
flag.Int 函數接收三個參數,分別是標志名稱、標志默認參數值、標志使用幫助信息。函數最終還會返回一個 *int 類型的值,表示用戶在執行命令行程序時為這個標志指定的參數。
除了使用 flag.Int 來設置 int 類型標志,flag 還支持其他多種類型,如使用 flag.String 來設置 string 類型標志。
flag.TypeVar():
-flagvar 標志是使用 flag.IntVar(&flagvar, "flagvar", 1234, "help message for flagvar") 來指定的。
flag.IntVar 函數簽名如下:
func IntVar(p *int, name string, value int, usage string)
與 flag.Int 不同的是,flag.IntVar 函數取消了返回值,而是會將用戶傳遞的命令行參數綁定到第一個參數 p *int。
除了使用 flag.IntVar 來綁定 int 類型參數到標志,flag 還提供其他多個函數來支持綁定不同類型參數到標志,如使用 flag.StringVar 來綁定 string 類型標志。
flag.Var():
-val 標志是使用 flag.Var(&val, "val", "help message for val") 來指定的。
flag.Var 函數簽名如下:
func Var(value Value, name string, usage string)
flag.Var 函數接收三個參數,后兩個參數分別是標志名稱、標志使用幫助信息。而用戶傳遞的命令行參數將被綁定到第一個參數 value。
type Value interface {
String() string
Set(string) error
}我們可以自定義類型,只要實現了 flag.Value 接口,都可以傳遞給 flag.Var,這極大的增加了 flag 包的靈活性。
定義完三個標志,我們還需要使用 flag.Parse() 來解析命令行參數,只有解析成功以后,才會將戶傳遞的命令行參數值綁定到對應的標志變量中。之后就可以使用 nFlag、flagvar、val 的變量值了。
在 main 函數底部,使用 flag.NFlag()、flag.NArg()、flag.Args()、flag.Arg(1) 幾個函數獲取并展示了命令行參數相關信息。
現在我們嘗試給這個命令行程序傳遞幾個參數并執行它,看下輸出結果:
$ go run main.go -n 100 -val test a b c d
nFlag: 100
flagvar: 1234
val: {val:test}
NFlag: 2
NArg: 4
Args: [a b c d]
Arg(1): b
我們通過 -n 100 為 -n 標志指定了參數值 100,最終會被賦值給 nFlag 變量。
由于沒有指定 flagvar 標志的參數值,所以 flagvar 變量會被賦予默認值 1234。
接著,我們又通過 -val test 為 -val 標志指定了參數值 test,最終賦值給了自定義的 flagVal 結構體的 val 字段。
因為只設置了 -n 和 -val 兩個標志的參數值,所以函數 flag.NFlag() 返回結果為 2。
a b c d 四個參數由于沒有被定義,所以 flag.NArg() 返回結果為 4。
flag.Args() 返回的切片中存儲了 a b c d 四個參數。
flag.Arg(1) 返回切片中下標為 1 位置的參數,即 b。
在上面的示例中,我們展示了 int 類型和自定義的 flag.Value 的使用,flag 包支持的所有標志類型匯總如下:
| 參數類型 | 合法值 |
|---|---|
| bool | strconv.ParseBool 能夠解析的有效值,接受:1, 0, t, f, T, F, true, false, TRUE, FALSE, True, False。 |
| time.Duration | time.ParseDuration 能夠解析的有效值,如:”300ms”, “-1.5h” or “2h55m”,合法單位:”ns”, “us” (or “µs”), “ms”, “s”, “m”, “h”。 |
| float64 | 合法的浮點數類型。 |
| int/int64/uint/uint64 | 合法的整數類型,如:1234, 0664, 0x1234,也可以是負數。 |
| string | 合法的字符串類型。 |
| flag.Value | 實現了該接口的類型。 |
除了支持幾種 Go 默認的原生類型外,如果我們想實現其他類型標志的定義,都可以通過 flag.Value 接口類型來完成。其實 flag 包內部對于 bool、int 等所有類型的定義,都實現了 flag.Value 接口,在稍后講解源碼過程中將會有所體現。
命令行標志支持多種語法:
| 語法 | 說明 |
|---|---|
| -flag | bool 類型標志可以使用,表示參數值為 true。 |
| –flag | 支持兩個 - 字符,與 -flag 等價。 |
| -flag=x | 所有類型通用,為標志 flag 傳遞參數值 x。 |
| -flag x | 作用等價于 -flag=x,但是僅限非 bool 類型標志使用,假如這樣使用 cmd -x *,其中 * 是 Unix shell 通配符,如果存在名為 0、false 等文件,則參數值結果會發生變化。 |
flag 解析參數時會在第一個非標志參數之前(單獨的一個 - 字符也是非標志參數)或終止符 -- 之后停止。
注意:本文以 Go 1.19.4 源碼為例,其他版本可能存在差異。
熟悉了 flag 包的基本使用,接下來我們就要深入到 flag 的源碼,來探究其內部是如何實現。
閱讀 flag 包的源碼,我們可以從使用 flag 包的流程來入手。
在 main 函數中,我們首先通過如下代碼定義了一個標志 -n。
var nFlag = flag.Int("n", 1234, "help message for flag n")flag.Int 函數定義如下:
func Int(name string, value int, usage string) *int {
return CommandLine.Int(name, value, usage)
}可以發現,flag.Int 函數調用并返回了 CommandLine 對象的 Int 方法,并將參數原樣傳遞進去。
來看看 CommandLine 是個什么:
var CommandLine = NewFlagSet(os.Args[0], ExitOnError)
func NewFlagSet(name string, errorHandling ErrorHandling) *FlagSet {
f := &FlagSet{
name: name,
errorHandling: errorHandling,
}
f.Usage = f.defaultUsage
return f
}CommandLine 是使用 NewFlagSet 創建的 FlagSet 結構體指針,在構造 FlagSet 對象時,需要兩個參數 os.Args[0] 和 ExitOnError。
我們知道 os.Args 存儲了程序執行時指定的所有命令行參數,os.Args[0] 就是當前命令行程序的名稱,ExitOnError 是一個常量,用來標記在出現 error 時應該如何做,ExitOnError 表示在遇到 error 時退出程序。
來看下 FlagSet 是如何定義:
type FlagSet struct {
Usage func()
name string
parsed bool
actual map[string]*Flag
formal map[string]*Flag
args []string // arguments after flags
errorHandling ErrorHandling
output io.Writer // nil means stderr; use Output() accessor
}Usage 字段是一個函數,根據名字大概能夠猜測出,這個函數會在指定 --help/-h 參數查看命令行程序使用幫助時被調用。
parsed 用來標記是否調用過 flag.Parse()。
actual 和 formal 分別用來存儲從命令行解析的標志參數和在程序中指定的默認標志參數。它們都使用 map 來存儲 Flag 類型的指針,FlagSet 可以看作是 Flag 結構體的「集合」。
args 用來保存處理完標志后剩余的參數列表。
errorHandling 標記在出現 error 時應該如何做。
output 用來設置輸出位置,這可以改變 --help/-h 時展示幫助信息的輸出位置。
現在來看下 Flag 的定義:
type Flag struct {
Name string // 標志名稱
Usage string // 幫助信息
Value Value // 標志所對應的命令行參數值
DefValue string // 用來記錄字符串類型的默認值,它不會被改變
}Flag 用來記錄一個命令行參數,里面存儲了一個標志所有信息。
可以說 Flag 和 FlagSet 兩個結構體就是 flag 包的核心,所有功能都是圍繞這兩個結構體設計的。
標志所對應的命令行參數值為 flag.Value 接口類型,在前文中已經見過了,定義如下:
type Value interface {
String() string
Set(string) error
}之所以使用接口,是為了能夠存儲任何類型的值,除了 flag 包默認支持的內置類型,用戶也可以定義自己的類型,只要實現了 Value 接口即可。
如我們在前文示例程序中定義的 flagVal 類型。
現在 CommandLine 的定義以及內部實現我們都看過了,是時候回過頭來看一看 CommandLine 對象的 Int 方法了:
func (f *FlagSet) Int(name string, value int, usage string) *int {
p := new(int)
f.IntVar(p, name, value, usage)
return p
}Int 方法內部調用了 f.IntVar() 方法,定義如下:
func (f *FlagSet) IntVar(p *int, name string, value int, usage string) {
f.Var(newIntValue(value, p), name, usage)
}IntVar 方法又調用了 f.Var() 方法。
Var 方法第一個參數為 newIntValue(value, p),我們來看看 newIntValue 函數是如何定義的:
type intValue int
func newIntValue(val int, p *int) *intValue {
*p = val
return (*intValue)(p)
}
func (i *intValue) Set(s string) error {
v, err := strconv.ParseInt(s, 0, strconv.IntSize)
if err != nil {
err = numError(err)
}
*i = intValue(v)
return err
}
func (i *intValue) Get() any { return int(*i) }
func (i *intValue) String() string { return strconv.Itoa(int(*i)) }newIntValue 是一個構造函數,用來創建一個 intValue 類型的指針,intValue 底層類型實際上是 int。
定義 intValue 類型的目的就是為了實現 flag.Value 接口。
再來看下 Var 方法如何定義:
func (f *FlagSet) Var(value Value, name string, usage string) {
// Flag must not begin "-" or contain "=".
if strings.HasPrefix(name, "-") {
panic(f.sprintf("flag %q begins with -", name))
} else if strings.Contains(name, "=") {
panic(f.sprintf("flag %q contains =", name))
}
// Remember the default value as a string; it won't change.
flag := &Flag{name, usage, value, value.String()}
_, alreadythere := f.formal[name]
if alreadythere {
var msg string
if f.name == "" {
msg = f.sprintf("flag redefined: %s", name)
} else {
msg = f.sprintf("%s flag redefined: %s", f.name, name)
}
panic(msg) // Happens only if flags are declared with identical names
}
if f.formal == nil {
f.formal = make(map[string]*Flag)
}
f.formal[name] = flag
}name 參數即為標志名,在 Var 方法內部,首先對標志名的合法性進行了校驗,不能以 - 開頭且不包含 =。
接著,根據參數創建了一個 Flag 類型,并且校驗了標志是否被重復定義。
最后將 Flag 保存在 formal 屬性中。
到這里,整個函數調用關系就結束了,我們來梳理一下代碼執行流程:
flag.Int -> CommandLine.Int -> CommandLine.IntVar -> CommandLine.Var。
經過這個調用過程,我們就得到了一個 Flag 對象,其名稱為 n、默認參數值為 1234、值的類型為 intValue、幫助信息為 help message for flag n。并將這個 Flag 對象保存在了 CommandLine 這個類型為 FlagSet 的結構體指針對象的 formal 屬性中。
我們在示例程序中還使用了另外兩種方式定義標志。
使用 flag.IntVar(&flagvar, "flagvar", 1234, "help message for flagvar") 定義標志 -flagvar。
flag.IntVar 定義如下:
func IntVar(p *int, name string, value int, usage string) {
CommandLine.Var(newIntValue(value, p), name, usage)
}可以發現,flag.IntVar 函數內部沒有調用 CommandLine.Int 和 CommandLine.IntVar 的過程,而是直接調用 CommandLine.Var。
另外,我們還使用 flag.Var(&val, "val", "help message for val") 定義了 -val 標志。
flag.Var 定義如下:
func Var(value Value, name string, usage string) {
CommandLine.Var(value, name, usage)
}flag.Var 函數內部同樣直接調用了 CommandLine.Var,并且由于參數 value 已經是 Value 接口類型,可以無需調用 newIntValue 這類構造函數將 Go 內置類型轉為 Value 類型,直接傳遞參數即可。
命令行參數定義完成了,終于到了解析部分,可以使用 flag.Parse() 解析命令行參數。
flag.Parse 函數代碼如下:
func Parse() {
CommandLine.Parse(os.Args[1:])
}內部同樣是調用 CommandLine 對象對應的方法,并且將除程序名稱以外的命令行參數都傳遞到 Parse 方法中,Parse 方法定義如下:
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:
if err == ErrHelp {
os.Exit(0)
}
os.Exit(2)
case PanicOnError:
panic(err)
}
}
return nil
}首先將 f.parsed 標記為 true,在調用 f.Parsed() 方法時會被返回:
func (f *FlagSet) Parsed() bool {
return f.parsed
}接著又將 arguments 保存在 f.args 屬性中。
然后就是循環解析命令行參數的過程,每調用一次 f.parseOne() 解析一個標志,直到解析完成或遇到 error 退出程序。
parseOne 方法實現如下:
func (f *FlagSet) parseOne() (bool, error) {
if len(f.args) == 0 {
return false, nil
}
s := f.args[0]
if len(s) < 2 || s[0] != '-' {
return false, nil
}
numMinuses := 1
if s[1] == '-' {
numMinuses++
if len(s) == 2 { // "--" terminates the flags
f.args = f.args[1:]
return false, nil
}
}
name := s[numMinuses:]
if len(name) == 0 || name[0] == '-' || name[0] == '=' {
return false, f.failf("bad flag syntax: %s", s)
}
// it's a flag. does it have an argument?
f.args = f.args[1:]
hasValue := false
value := ""
for i := 1; i < len(name); i++ { // equals cannot be first
if name[i] == '=' {
value = name[i+1:]
hasValue = true
name = name[0:i]
break
}
}
m := f.formal
flag, alreadythere := m[name] // BUG
if !alreadythere {
if name == "help" || name == "h" { // special case for nice help message.
f.usage()
return false, ErrHelp
}
return false, f.failf("flag provided but not defined: -%s", name)
}
if fv, ok := flag.Value.(boolFlag); ok && fv.IsBoolFlag() { // special case: doesn't need an arg
if hasValue {
if err := fv.Set(value); err != nil {
return false, f.failf("invalid boolean value %q for -%s: %v", value, name, err)
}
} else {
if err := fv.Set("true"); err != nil {
return false, f.failf("invalid boolean flag %s: %v", name, err)
}
}
} else {
// It must have a value, which might be the next argument.
if !hasValue && len(f.args) > 0 {
// value is the next arg
hasValue = true
value, f.args = f.args[0], f.args[1:]
}
if !hasValue {
return false, f.failf("flag needs an argument: -%s", name)
}
if err := flag.Value.Set(value); err != nil {
return false, f.failf("invalid value %q for flag -%s: %v", value, name, err)
}
}
if f.actual == nil {
f.actual = make(map[string]*Flag)
}
f.actual[name] = flag
return true, nil
}parseOne 代碼稍微多一點,不過整體脈絡還是比較清晰的。
首先對 f.args 參數進行了校驗,接著提取標志前導符號 - 的個數放到 numMinuses 變量中,然后取出標志名并對標志語法做了檢查。
接下來取出參數 value,并且判斷標志名是否為 -help/-h,如果是則說明用戶只想打印程序使用幫助信息,打印后 parseOne 會返回 ErrHelp,上層的調用者 f.Parse 就會捕獲到 ErrHelp,然后調用 os.Exit(0) 直接退出程序。
其中 f.usage() 實現了打印幫助信息的功能,內部具體實現這里就不講解了,因為基本上是內容排版的實現,不是核心功能,感興趣可以自己嘗試看一看。
最后就是根據參數值是否為 bool 類型分別進行參數綁定,將參數設置到對應的標志變量中,并將標志保存到 f.actual 中。
以上步驟都執行完成后,在執行 fmt.Printf("nFlag: %d\n", *nFlag) 時,就能夠獲取到 nFlag 被賦予的參數值了。
至此,flag 包源碼的整體脈絡都已經清晰了。
在我們的示例代碼最后,還打印了 NFlag()、NArg()、Args()、Arg(1) 幾個函數的結果。
這幾個函數實現非常簡單,代碼如下:
func NFlag() int { return len(CommandLine.actual) }
func NArg() int { return len(CommandLine.args) }
func Args() []string { return CommandLine.args }
func Arg(i int) string {
return CommandLine.Arg(i)
}由于代碼過于簡單,我就不進行解釋了,相信通過上面的講解,這幾個函數的作用你也能做到一目了然。
flag 包還有一些其他類型,如 stringValue、float64Value,這些類型實現思路都是一樣的,也不再一一講解。
到此,關于“Golang中的flag標準庫如何使用”的學習就結束了,希望能夠解決大家的疑惑。理論與實踐的搭配能更好的幫助大家學習,快去試試吧!若想繼續學習更多相關知識,請繼續關注億速云網站,小編會繼續努力為大家帶來更多實用的文章!
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。