1. 程式人生 > >【Go】筆記二 | 命令原始碼檔案與命令列引數

【Go】筆記二 | 命令原始碼檔案與命令列引數

核心知識

  • 自定義命令引數
  • 建立私有的命令引數容器

首知

  • 環境變數 GOPATH 指向的是一個或多個工作區,而每個工作區中都會有以程式碼包為基本組織形式的原始碼檔案。
  • 原始碼檔案又分為三種,即:命令原始碼檔案、庫原始碼檔案和測試原始碼檔案,它們都有著不同的用途和編寫規則。

命令原始碼檔案的用途是什麼,怎樣編寫它?

典型回答

  • 命令原始碼檔案是程式的執行入口,是每個可獨立執行的程式必須擁有的。
  • 我們可以通過構建或安裝生成與其對應的可執行檔案,後者一般會與該命令原始碼檔案的直接父目錄同名。

通過構建或安裝命令原始碼檔案生成的可執行檔案就可以被視為“命令”,既然是命令,那麼就應該具備接收引數的能力。

例如,要實現
根據執行程式時給定的引數問候某人
程式碼

package main

import (
    "flag" // [1]
    "fmt"
)

var name string

func init() {
    flag.StringVar(&name, "name", "everyone", "The greeting object.") // [2]
}

func main() {
    flag.Parse() // [3]
    fmt.Printf("Hello, %s!\n", name)
}


解析

[1]匯入flag包,這是Go 語言標準庫中專門用於接收和解析命令引數的包。

[2]設定命令引數
接受 4 個引數。

  • 第 1 個引數是用於儲存該命令引數的值的地址,具體到這裡就是在前面宣告的變數name的地址了,由表示式&name表示。
  • 第 2 個引數是為了指定該命令引數的名稱,這裡是name.
  • 第 3 個引數是為了指定在未追加該命令引數時的預設值,這裡是everyone
  • 第 4 個函式引數,即是該命令引數的簡短說明了,這在列印命令說明時會用到

還有一個與flag.StringVar函式類似的函式,叫flag.String

這兩個函式的區別是,後者會直接返回一個已經分配好的用於儲存命令引數值的地址

var name = flag.String
("name", "everyone", "The greeting object.")

這裡的name保持的就是地址

[3]用於真正解析命令引數,並把它們的值賦給相應的變數。

因此最好把flag.Parse()放在main函式的函式體的第一行。

檢視該命令原始碼檔案的引數說明,–help:

像上面這一串/tmp/go-build343904153/b001/exe/demo1程式碼
這是命令構建上述命令原始碼檔案時臨時生成的可執行檔案的完整路徑

如果我們先構建這個命令原始碼檔案再執行生成的可執行檔案,就會這樣:

自定義命令原始碼檔案的引數使用說明

這有很多種方式,最簡單的一種方式就是對變數flag.Usag重新賦值

flag.Usag重新賦值

flag.Usage的型別是func(),即一種無引數宣告且無結果宣告的函式型別。

注意,對flag.Usage的賦值必須在呼叫flag.Parse函式之前

程式碼

import (
    "flag" // [1]
    "fmt"
    "os"
)

var name string

func init() {
    flag.StringVar(&name, "name", "everyone", "The greeting object.") // [2]
}

func main() {
    flag.Usage = func() {
        fmt.Fprintf(os.Stderr, "Usage of %s:\n", "question")
        flag.PrintDefaults()
    }
    flag.Parse() // [3]
    fmt.Printf("Hello, %s!\n", name)
}

執行

flag.CommandLine重新賦值

現在再深入一層,我們在呼叫flag包中的一些函式(比如StringVar、Parse等等)的時候,實際上是在呼叫flag.CommandLine變數的對應方法

flag.CommandLine相當於預設情況下的命令引數容器。所以,通過對flag.CommandLine重新賦值,我們可以更深層次地定製當前命令原始碼檔案的引數使用說明。

現在我們把main函式體中的那條對flag.Usage變數的賦值語句登出掉,然後在init函式體的開始處新增如下程式碼:

flag.CommandLine = flag.NewFlagSet("", flag.ExitOnError)
flag.CommandLine.Usage = func() {
    fmt.Fprintf(os.Stderr, "Usage of %s:\n", "question")
    flag.PrintDefaults()
}

執行 go run demo.go –help後,其輸出會與上一次的輸出的一致。不過後面這種定製的方法更加靈活。比如,當我們把為flag.CommandLine賦值的那條語句改為

flag.CommandLine = flag.NewFlagSet("", flag.PanicOnError)

執行結果

會產生另一種輸出效果。
這是由於我們在這裡傳給flag.NewFlagSet函式的第二個引數值是flag.PanicOnError

flag.PanicOnError和flag.ExitOnError都是預定義在flag包中的常量

flag.ExitOnError的含義是,告訴命令引數容器,當命令後跟–help或者引數設定的不正確的時候,在列印命令引數使用說明後以狀態碼2結束當前程式

狀態碼2代表使用者錯誤地使用了命令,而flag.PanicOnError與之的區別是在最後丟擲“執行時恐慌(panic)”。

上述兩種情況都會在我們呼叫flag.Parse函式時被觸發。

執行時恐慌”是 Go 程式錯誤處理方面的概念

建立私有的命令引數容器

在函式外再新增一個變數宣告:

var cmdLine = flag.NewFlagSet("question", flag.ExitOnError)

完整程式碼

package main

import (
        "flag" // [1]
        "fmt"
        "os"
)

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

func init() {
        cmdLine.Usage = func() {
                fmt.Fprintf(os.Stderr, "Usage of %s:\n", "question")
                cmdLine.PrintDefaults()
        }
        cmdLine.StringVar(&name, "name", "everyone", "The greeting object.") // [2]
}

func main() {
        cmdLine.Parse(os.Args[1:]) // [3]
        fmt.Printf("Hello, %s!\n", name)
}

其中的os.Args[1:]指的就是我們給定的那些命令引數。這樣做就完全脫離了flag.CommandLine。

這樣更加靈活定製命令引數容器,並且完全不會影響到那個全域性變數flag.CommandLine。

額外知識

預設情況下,我們可以讓命令原始碼檔案接受哪些型別的引數值?

int(int|int64|uint|uint64),
float(float|float64)
string,
bool,
duration(時間),
var(自定義)

我們可以把自定義的資料型別作為引數值的型別嗎?如果可以,怎樣做?

關鍵就是使用flag.var(),關鍵點在於需要實現flag包的Value介面。

小結

  • 原始碼檔案分為三種:命令,庫,測試。
  • 編寫命令原始碼檔案的命令引數關鍵包: flag。
  • 可以通過重新賦值建立新的命令引數容器