Go語言的命令源碼檔案
來源:互聯網
上載者:User
> 編者按> ###### *大家好,我是郝林,我在極客時間上面開設了專欄《[Go語言核心36講]( https://time.geekbang.org/column/intro/112?utm_source=website&utm_medium=goyuyanzhongwenwang&utm_campaign=112-presell&utm_content=link0814)》,將自己十多年對 Go 語言的實戰經驗以及心得都記錄在了這個專欄裡面,很感謝大家前段時間對我的健康情況的關心,目前手術完畢,正在一段長時間的休養中,我也希望通過休養的這段時間去給大家沉澱和分享更多的技術知識,感謝大家的支援和關心~!*我們已經知道,環境變數`GOPATH`指向的是一個或多個工作區,而每個工作區中都會有以程式碼封裝為基主要組織形式的源碼檔案。這裡的源碼檔案又分為三種,即:命令源碼檔案、庫源碼檔案和測試源碼檔案,它們都有著不同的用途和編寫規則。一旦開始學慣用程式設計語言編寫程式,我們就一定希望在編碼的過程中及時地得到反饋,只有這樣才能知道對錯。實際上,我們的有效學習和進步都是通過不斷地接受反饋和執行修正實現的。對於Go語言,你在學習階段一定會經常編寫可以直接啟動並執行程式。直至今日,我如果要做一些實驗的話,依然會使用這種方法。這樣肯定會涉及命令源碼檔案的編寫,而且,命令源碼檔案可以很方便地用`go run`命令啟動。**問題:命令源碼檔案的用途是什麼,怎樣編寫它?****典型回答**命令源碼檔案是程式的運行入口,是每個可獨立啟動並執行程式必須擁有的。我們可以通過構建或安裝產生與其對應的可執行檔,後者一般會與該命令源碼檔案的直接父目錄同名。如果一個源碼檔案聲明屬於`main`包,並且包含一個無參數聲明且無結果聲明的`main`函數,那麼就是命令源碼檔案。就像這樣:```package mainimport "fmt"func main() {fmt.Println("Hello, world!")}```如果你把這段代碼存成demo1.go檔案,那麼運行`go run demo1.go`命令後就會在螢幕(標準輸出)中看到`Hello, world!`。當需要模組化編程時,我們往往會將代碼拆分到多個檔案,甚至拆分到不同的程式碼封裝中。但無論怎樣,對於一個獨立的程式來說命令源碼檔案永遠只會也只能有一個。如果有與命令源碼檔案同包的源碼檔案,那麼它們也應該聲明屬於`main`包。**問題解析**命令源碼檔案如此重要,以至於它毫無疑問地成為了我們學習Go語言的第一助手。不過,只會列印`Hello, world!`是遠遠不夠的,咱們千萬不要成為“Hello, world”黨。既然決定學習Go語言,你就應該從每一個知識點深入下去。無論是Linux還是Windows,如果你用過命令列(command line)的話,肯定就會知道幾乎所有命令(command)都是可以接收參數(argument)的。通過構建或安裝命令源碼檔案產生的可執行檔就可以被視為“命令”,既然是命令,那麼就應該具備接收參數的能力。下面,我就帶你深入瞭解一下與命令參數的接收和解析有關的一系列問題。**知識擴充****1 命令源碼檔案怎樣接收參數**先看一段不完整的代碼:```package mainimport (// 需在此處添加代碼。[1]"fmt")var name stringfunc init() {// 需在此處添加代碼。[2]}func main() {// 需在此處添加代碼。[3]fmt.Printf("Hello, %s!\n", name)}```如果我想讓你幫我在注釋處添加相應的代碼,並讓程式實現”根據運行程式時給定的參數問候某人”的功能,你打算怎樣做?如果你知道做法,請現在就動手實現它。如果不知道也不要著急,咱們一起來搞定。首先,Go語言標準庫中有一個程式碼封裝專門用於接收和解析命令參數。這個程式碼封裝的名字叫`flag`。我之前說過,如果想要在代碼中使用某個包中的程式實體,那麼應該先匯入這個包。因此,我們需要在`[1]`處添加代碼`"flag"`。注意,這裡應該在程式碼封裝匯入路徑的前後加上英文半形的引號。如此一來,上述代碼匯入了`flag`和`fmt`這兩個包。其次,人名肯定是由字串代表的。所以我們要在`[2]`處添加調用`flag`包的`StringVar`函數的代碼。就像這樣:```flag.StringVar(&name, "name", "everyone", "The greeting object.")```函數`flag.StringVar`接受4個參數。第1個參數是用於儲存該命令參數的值的地址,具體到這裡就是在前面聲明的變數`name`的地址了,由運算式`&name`表示。第2個參數是為了指定該命令參數的名稱,這裡是`name`。第3個參數是為了指定在未追加該命令參數時的預設值,這裡是`everyone`。至於第4個函數參數,即是該命令參數的簡短說明了,這在列印命令說明時會用到。順便說一下,還有一個與`flag.StringVar`函數類似的函數,叫`flag.String`。這兩個函數的區別是,後者會直接返回一個已經分配好的用於儲存命令參數值的地址。如果使用它的話,我們就需要把```var name string```改為```var name = flag.String("name", "everyone", "The greeting object.")```所以,如果我們使用`flag.String`函數就需要改動原有的代碼。這樣並不符合上述問題的要求。再說最後一個填空。我們需要在`[3]`處添加代碼`flag.Parse()`。函數`flag.Parse`用於真正解析命令參數,並把它們的值賦給相應的變數。對該函數的調用必須在所有命令參數儲存載體的聲明(這裡是對變數`name`的聲明)和設定(這裡是在`[2]`處對`flag.StringVar`函數的調用)之後,並且在讀取任何命令參數值之前進行。正因為如此,我們最好把`flag.Parse()`放在`main`函數的函數體的第一行。**2 怎樣在運行命令源碼檔案的時候傳入參數,又怎樣查看參數的使用說明**如果我們把上述代碼存成名為demo2.go的檔案,那麼運行如下命令就可以為參數`name`傳值:```go run demo2.go -name="Robert"``` 運行後,列印到標準輸出(stdout)的內容會是:```Hello, Robert!```另外,如果想查看該命令源碼檔案的參數說明,可以這樣做:```$ go run demo2.go --help```其中的`$ `表示我們是在命令提示字元後運行`go run`命令的。運行後輸出的內容會類似:```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`是什麼東西。這其實是`go run`命令構建上述命令源碼檔案時臨時產生的可執行檔的完整路徑。如果我們先構建這個命令源碼檔案再運行產生的可執行檔,像這樣:```$ go build demo2.go$ ./demo2 --help```那麼輸出就會是```Usage of ./demo2: -name string The greeting object. (default "everyone")```**3 怎樣自訂命令源碼檔案的參數使用說明**這有很多種方式,最簡單的一種方式就是對變數`flag.Usage`重新賦值。`flag.Usage`的類型是`func()`,即一種無參數聲明且無結果聲明的函數類型。`flag.Usage`變數在聲明時就已經被賦值了,所以我們才能夠在運行命令`go run demo2.go --help`時看到正確的結果。注意,對`flag.Usage`的賦值必須在調用`flag.Parse`函數之前。現在,我們把demo2.go另存新檔demo3.go,然後在`main`函數體的開始處加入如下代碼。```flag.Usage = func() { fmt.Fprintf(os.Stderr, "Usage of %s:\n", "question") flag.PrintDefaults()}```那麼當運行```$ go run demo3.go --help```後,就會看到```Usage of question: -name string The greeting object. (default "everyone")exit status 2```現在再深入一層,我們在調用`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 demo3.go --help`後,其輸出會與上一次的輸出的一致。不過後面這種定製的方法更加靈活。比如,當我們把為`flag.CommandLine`賦值的那條語句改為```flag.CommandLine = flag.NewFlagSet("", flag.PanicOnError)```後,再運行`go run demo3.go --help`命令就會產生另一種輸出效果。這是由於我們在這裡傳給`flag.NewFlagSet`函數的第二個參數值是`flag.PanicOnError`。`flag.PanicOnError和`flag.ExitOnError`都是預定義在`flag`包中的常量。`flag.ExitOnError`的含義是,告訴命令參數容器,當命令後跟`--help`或者參數設定的不正確的時候,在列印命令參數使用說明後以狀態代碼`2`結束當前程式。狀態代碼`2`代表使用者錯誤地使用了命令,而`flag.PanicOnError`與之的區別是在最後拋出“運行時恐慌(panic)”。上述兩種情況都會在我們調用`flag.Parse`函數時被觸發。順便提一句,“運行時恐慌”是Go程式錯誤處理方面的概念。關於它的拋出和恢複方法,我在本專欄的後續部分中會講到。下面再進一步,我們索性不用全域的`flag.CommandLine`變數,轉而自己建立一個私人的命令參數容器。我們在函數外再添加一個變數聲明:```var cmdLine = flag.NewFlagSet("question", flag.ExitOnError)```然後,我們把對`flag.StringVar`的調用替換為對`cmdLine.StringVar`調用,再把`flag.Parse()`替換為`cmdLine.Parse(os.Args[1:])`。其中的`os.Args[1:]`指的就是我們給定的那些命令參數。這樣做就完全脫離了`flag.CommandLine`。`*flag.FlagSet`類型的變數`cmdLine`擁有很多有意思的方法。你可以去探索一下。我就不在這裡一一講述了。這樣做的好處依然是更靈活地定製命令參數容器。但更重要的是,你的定製完全不會影響到那個全域變數`flag.CommandLine`。**總結**恭喜你!你現在已經走出了Go語言編程的第一步。你可以用Go編寫命令,並可以讓它們像眾多作業系統命令那樣被使用,甚至可以把它們嵌入到各種指令碼中。雖然我為你講解了命令源碼檔案的基本編寫方法,並且也談到了為了讓它接受參數而需要做的各種準備工作,但這並不是全部。別擔心,我在後面會經常提到它的。另外,如果你想詳細瞭解`flag`包的用法,可以到[這個網址](https://golang.google.cn/pkg/flag/)查看文檔。或者直接使用`godoc`命令在本地啟動一個Go語言文檔伺服器。怎樣使用`godoc`命令?你可以參看[這裡](https://github.com/hyper0x/go_command_tutorial/blob/master/0.5.md)。**思考題**我們已經見識過為命令源碼檔案傳入字串類型的參數值的方法,那還可以傳入別的嗎?這就是今天我留下的思考題。1. 預設情況下,我們可以讓命令源碼檔案接受哪些類型的參數值?2. 我們可以把自訂的資料類型作為參數值的類型嗎?如果可以,怎樣做?你可以通過查閱文檔獲得第一個問題的答案。記住,快速查看和理解文檔是一項必備的技能。至於第二個問題,你回答起來可能會有些困難,因為這涉及了另一個問題:“怎樣聲明自己的資料類型?”(這個問題我在專欄的後續部分中也會講到)。如果是這樣,我希望你記下它和這裡說的另一問題,並在能解決後者之後再來回答前者。[你也可以點擊文字串連或掃描下方二維碼訂閱我的專欄與我一起討論。](https://time.geekbang.org/column/intro/112?utm_source=website&utm_medium=goyuyanzhongwenwang&utm_campaign=112-presell&utm_content=link0814)限時福利:1、掃描海報二維碼,即可立減6元,相當於39元訂閱;2、訂閱專欄後,通過專屬分享海報二維碼,每邀請一位好友,即可獲得12元的現金返現,提現方式(極客時間 APP-我的-分享有賞)![go 語言中文網1.jpg](https://static.studygolang.com/180814/3aa8f148cc3097962d38f253998de964.jpg)450 次點擊