我們已經(jīng)知道,環(huán)境變量 GOPATH 指向的是一個(gè)或多個(gè)工作區(qū),每個(gè)工作區(qū)中都會(huì)有以代碼包為基本組織形式的源碼文件。 這里的源碼文件又分為三種,即:命令源碼文件、庫(kù)源碼文件和測(cè)試源碼文件,它們都有著不同的用途和編寫規(guī)則。
今天,我們就沿著命令源碼文件的知識(shí)點(diǎn),展開更深層級(jí)的學(xué)習(xí)。 一旦開始學(xué)習(xí)用編程語(yǔ)言編寫程序,我們就一定希望在編碼的過(guò)程中及時(shí)地得到反饋,只有這樣才能清楚對(duì)錯(cuò)。實(shí)際上,我們的有效學(xué)習(xí)和進(jìn)步,都是通過(guò)不斷地接受反饋和執(zhí)行修正實(shí)現(xiàn)的。 對(duì)于 Go 語(yǔ)言學(xué)習(xí)者來(lái)說(shuō),你在學(xué)習(xí)階段中,也一定會(huì)經(jīng)常編寫可以直接運(yùn)行的程序。這樣的程序肯定會(huì)涉及命令源碼文件的編寫,而且,命令源碼文件也可以很方便地用go run命令啟動(dòng)。 那么,我今天的問(wèn)題就是:命令源碼文件的用途是什么,怎樣編寫它? 這里,我給出你一個(gè)參考的回答:命令源碼文件是程序的運(yùn)行入口,是每個(gè)可獨(dú)立運(yùn)行的程序必須擁有的。我們可以通過(guò)構(gòu)建或安裝,生成與其對(duì)應(yīng)的可執(zhí)行文件,后者一般會(huì)與該命令源碼文件的直接父目錄同名。 如果一個(gè)源碼文件聲明屬于main包,并且包含一個(gè)無(wú)參數(shù)聲明且無(wú)結(jié)果聲明的main函數(shù),那么它就是命令源碼文件。 就像下面這段代碼: package main import "fmt" func main() { fmt.Println("Hello, world!") } 如果你把這段代碼存成 demo1.go 文件,那么運(yùn)行g(shù)o run demo1.go命令后就會(huì)在屏幕(標(biāo)準(zhǔn)輸出)中看到Hello, world! 當(dāng)需要模塊化編程時(shí),我們往往會(huì)將代碼拆分到多個(gè)文件,甚至拆分到不同的代碼包中。但無(wú)論怎樣,對(duì)于一個(gè)獨(dú)立的程序來(lái)說(shuō),命令源碼文件永遠(yuǎn)只會(huì)也只能有一個(gè)。如果有與命令源碼文件同包的源碼文件,那么它們也應(yīng)該聲明屬于main包。 問(wèn)題解析 命令源碼文件如此重要,以至于它毫無(wú)疑問(wèn)地成為了我們學(xué)習(xí) Go 語(yǔ)言的第一助手。不過(guò),只會(huì)打印Hello, world是遠(yuǎn)遠(yuǎn)不夠的,咱們千萬(wàn)不要成為“Hello, world”黨。既然決定學(xué)習(xí) Go 語(yǔ)言,你就應(yīng)該從每一個(gè)知識(shí)點(diǎn)深入下去。 無(wú)論是 Linux 還是 Windows,如果你用過(guò)命令行(command line)的話,肯定就會(huì)知道幾乎所有命令(command)都是可以接收參數(shù)(argument)的。通過(guò)構(gòu)建或安裝命令源碼文件,生成的可執(zhí)行文件就可以被視為“命令”,既然是命令,那么就應(yīng)該具備接收參數(shù)的能力。 下面,我就帶你深入了解一下與命令參數(shù)的接收和解析有關(guān)的一系列問(wèn)題。 知識(shí)精講 1. 命令源碼文件怎樣接收參數(shù) 我們先看一段不完整的代碼: package main import ( // 需在此處添加代碼。[1] "fmt" ) var name string func init() { // 需在此處添加代碼。[2] } func main() { // 需在此處添加代碼。[3] fmt.Printf("Hello, %s!\n", name) } 如果邀請(qǐng)你幫助我,在注釋處添加相應(yīng)的代碼,并讓程序?qū)崿F(xiàn)”根據(jù)運(yùn)行程序時(shí)給定的參數(shù)問(wèn)候某人”的功能,你會(huì)打算怎樣做? 如果你知道做法,請(qǐng)現(xiàn)在就動(dòng)手實(shí)現(xiàn)它。如果不知道也不要著急,咱們一起來(lái)搞定。 首先,Go 語(yǔ)言標(biāo)準(zhǔn)庫(kù)中有一個(gè)代碼包專門用于接收和解析命令參數(shù)。這個(gè)代碼包的名字叫flag。 我之前說(shuō)過(guò),如果想要在代碼中使用某個(gè)包中的程序?qū)嶓w,那么應(yīng)該先導(dǎo)入這個(gè)包。因此,我們需要在[1]處添加代碼"flag"。注意,這里應(yīng)該在代碼包導(dǎo)入路徑的前后加上英文半角的引號(hào)。如此一來(lái),上述代碼導(dǎo)入了flag和fmt這兩個(gè)包。 其次,人名肯定是由字符串代表的。所以我們要在[2]處添加調(diào)用flag包的StringVar函數(shù)的代碼。就像這樣: flag.StringVar(&name, "name", "everyone", "The greeting object.") 函數(shù)flag.StringVar接受 4 個(gè)參數(shù)。
順便說(shuō)一下,還有一個(gè)與flag.StringVar函數(shù)類似的函數(shù),叫flag.String。這兩個(gè)函數(shù)的區(qū)別是,后者會(huì)直接返回一個(gè)已經(jīng)分配好的用于存儲(chǔ)命令參數(shù)值的地址。如果使用它的話,我們就需要把 var name string 改為 var name = flag.String("name", "everyone", "The greeting object.") 所以,如果我們使用flag.String函數(shù)就需要改動(dòng)原有的代碼。這樣并不符合上述問(wèn)題的要求。 再說(shuō)最后一個(gè)填空。我們需要在[3]處添加代碼flag.Parse()。函數(shù)flag.Parse用于真正解析命令參數(shù),并把它們的值賦給相應(yīng)的變量。 對(duì)該函數(shù)的調(diào)用必須在所有命令參數(shù)存儲(chǔ)載體的聲明(這里是對(duì)變量name的聲明)和設(shè)置(這里是在[2]處對(duì)flag.StringVar函數(shù)的調(diào)用)之后,并且在讀取任何命令參數(shù)值之前進(jìn)行。 正因?yàn)槿绱耍覀冏詈冒裦lag.Parse()放在main函數(shù)的函數(shù)體的第一行。
2. 怎樣在運(yùn)行命令源碼文件的時(shí)候傳入?yún)?shù),又怎樣查看參數(shù)的使用說(shuō)明 如果我們把上述代碼存成名為 demo2.go 的文件,那么運(yùn)行如下命令就可以為參數(shù)name傳值: go run demo2.go -name="Robert" 運(yùn)行后,打印到標(biāo)準(zhǔn)輸出(stdout)的內(nèi)容會(huì)是: Hello, Robert! 另外,如果想查看該命令源碼文件的參數(shù)說(shuō)明,可以這樣做: go run demo2.go --help 其中的$表示我們是在命令提示符后運(yùn)行g(shù)o run命令的。運(yùn)行后輸出的內(nèi)容會(huì)類似: Usage of /var/folders/ts/7lg_tl_x2gd_k1lm5g_48c7w0000gn/T/go-build155438482/b001/exe/demo2: -name string The greeting object. (default "everyone") exit status 2 你可能不明白下面這段輸出代碼的意思。 /var/folders/ts/7lg_tl_x2gd_k1lm5g_48c7w0000gn/T/go-build155438482/b001/exe/demo2 這其實(shí)是go run命令構(gòu)建上述命令源碼文件時(shí)臨時(shí)生成的可執(zhí)行文件的完整路徑。 如果我們先構(gòu)建這個(gè)命令源碼文件再運(yùn)行生成的可執(zhí)行文件,像這樣: $ go build demo2.go $ ./demo2 --help 那么輸出就會(huì)是 Usage of ./demo2: -name string The greeting object. (default "everyone")
3. 怎樣自定義命令源碼文件的參數(shù)使用說(shuō)明 這有很多種方式,最簡(jiǎn)單的一種方式就是對(duì)變量flag.Usage重新賦值。flag.Usage的類型是func(),即一種無(wú)參數(shù)聲明且無(wú)結(jié)果聲明的函數(shù)類型。 flag.Usage變量在聲明時(shí)就已經(jīng)被賦值了,所以我們才能夠在運(yùn)行命令go run demo2.go --help時(shí)看到正確的結(jié)果。 注意,對(duì)flag.Usage的賦值必須在調(diào)用flag.Parse函數(shù)之前。 現(xiàn)在,我們把 demo2.go 另存為 demo3.go,然后在main函數(shù)體的開始處加入如下代碼。 flag.Usage = func() { fmt.Fprintf(os.Stderr, "Usage of %s:\n", "question") flag.PrintDefaults() } 那么當(dāng)運(yùn)行 $ go run demo3.go --help 后,就會(huì)看到 Usage of question: -name string The greeting object. (default "everyone") exit status 2 現(xiàn)在再深入一層,我們?cè)谡{(diào)用flag包中的一些函數(shù)(比如StringVar、Parse等等)的時(shí)候,實(shí)際上是在調(diào)用flag.CommandLine變量的對(duì)應(yīng)方法。 flag.CommandLine相當(dāng)于默認(rèn)情況下的命令參數(shù)容器。所以,通過(guò)對(duì)flag.CommandLine重新賦值,我們可以更深層次地定制當(dāng)前命令源碼文件的參數(shù)使用說(shuō)明。 現(xiàn)在我們把main函數(shù)體中的那條對(duì)flag.Usage變量的賦值語(yǔ)句注銷掉,然后在init函數(shù)體的開始處添加如下代碼: flag.CommandLine = flag.NewFlagSet("", flag.ExitOnError) flag.CommandLine.Usage = func() { fmt.Fprintf(os.Stderr, "Usage of %s:\n", "question") flag.PrintDefaults() } 再運(yùn)行命令go run demo3.go --help后,其輸出會(huì)與上一次的輸出的一致。不過(guò)后面這種定制的方法更加靈活。比如,當(dāng)我們把為flag.CommandLine賦值的那條語(yǔ)句改為 flag.CommandLine = flag.NewFlagSet("", flag.PanicOnError) 后,再運(yùn)行g(shù)o run demo3.go --help命令就會(huì)產(chǎn)生另一種輸出效果。這是由于我們?cè)谶@里傳給flag.NewFlagSet函數(shù)的第二個(gè)參數(shù)值是flag.PanicOnError。flag.PanicOnError和flag.ExitOnError都是預(yù)定義在flag包中的常量。 flag.ExitOnError的含義是,告訴命令參數(shù)容器,當(dāng)命令后跟--help或者參數(shù)設(shè)置的不正確的時(shí)候,在打印命令參數(shù)使用說(shuō)明后以狀態(tài)碼2結(jié)束當(dāng)前程序。 狀態(tài)碼2代表用戶錯(cuò)誤地使用了命令,而flag.PanicOnError與之的區(qū)別是在最后拋出“運(yùn)行時(shí)恐慌(panic)”。 上述兩種情況都會(huì)在我們調(diào)用flag.Parse函數(shù)時(shí)被觸發(fā)。順便提一句,“運(yùn)行時(shí)恐慌”是 Go 程序錯(cuò)誤處理方面的概念。關(guān)于它的拋出和恢復(fù)方法,我在本專欄的后續(xù)部分中會(huì)講到。0 下面再進(jìn)一步,我們索性不用全局的flag.CommandLine變量,轉(zhuǎn)而自己創(chuàng)建一個(gè)私有的命令參數(shù)容器。我們?cè)诤瘮?shù)外再添加一個(gè)變量聲明: var cmdLine = flag.NewFlagSet("question", flag.ExitOnError) 然后,我們把對(duì)flag.StringVar的調(diào)用替換為對(duì)cmdLine.StringVar調(diào)用,再把flag.Parse()替換為cmdLine.Parse(os.Args[1:])。 其中的os.Args[1:]指的就是我們給定的那些命令參數(shù)。這樣做就完全脫離了flag.CommandLine。*flag.FlagSet類型的變量cmdLine擁有很多有意思的方法。你可以去探索一下。我就不在這里一一講述了。 這樣做的好處依然是更靈活地定制命令參數(shù)容器。但更重要的是,你的定制完全不會(huì)影響到那個(gè)全局變量flag.CommandLine。
總結(jié) 恭喜你!你現(xiàn)在已經(jīng)走出了 Go 語(yǔ)言編程的第一步。你可以用 Go 編寫命令,并可以讓它們像眾多操作系統(tǒng)命令那樣被使用,甚至可以把它們嵌入到各種腳本中。 雖然我為你講解了命令源碼文件的基本編寫方法,并且也談到了為了讓它接受參數(shù)而需要做的各種準(zhǔn)備工作,但這并不是全部。 別擔(dān)心,我在后面會(huì)經(jīng)常提到它的。另外,如果你想詳細(xì)了解flag包的用法,可以到這個(gè)網(wǎng)址查看文檔。或者直接使用godoc命令在本地啟動(dòng)一個(gè) Go 語(yǔ)言文檔服務(wù)器。怎樣使用godoc命令?你可以參看這里。
思考題
|
|