1.2. 命令行參數

大多數的程序都是處理輸入,產生輸出;這也正是“計算”的定義。但是,程序如何獲取要處理的輸入數據呢?一些程序生成自己的數據,但通常情況下,輸入來自於程序外部:文件、網絡連接、其它程序的輸出、敲鍵盤的用戶、命令行參數或其它類似輸入源。下面幾個例子會討論其中幾個輸入源,首先是命令行參數。

os包以跨平臺的方式,提供了一些與操作系統交互的函數和變量。程序的命令行參數可從os包的Args變量獲取;os包外部使用os.Args訪問該變量。

os.Args變量是一個字符串(string)的切片(slice)(譯註:slice和Python語言中的切片類似,是一個簡版的動態數組),切片是Go語言的基礎概念,稍後詳細介紹。現在先把切片s當作數組元素序列,序列的長度動態變化,用s[i]訪問單個元素,用s[m:n]獲取子序列(譯註:和python裡的語法差不多)。序列的元素數目為len(s)。和大多數編程語言類似,區間索引時,Go語言裡也採用左閉右開形式,即,區間包括第一個索引元素,不包括最後一個,因為這樣可以簡化邏輯。(譯註:比如a = [1, 2, 3, 4, 5], a[0:3] = [1, 2, 3],不包含最後一個元素)。比如s[m:n]這個切片,0 ≤ m ≤ n ≤ len(s),包含n-m個元素。

os.Args的第一個元素:os.Args[0],是命令本身的名字;其它的元素則是程序啟動時傳給它的參數。s[m:n]形式的切片表達式,產生從第m個元素到第n-1個元素的切片,下個例子用到的元素包含在os.Args[1:len(os.Args)]切片中。如果省略切片表達式的m或n,會默認傳入0或len(s),因此前面的切片可以簡寫成os.Args[1:]。

下面是Unix裡echo命令的一份實現,echo把它的命令行參數打印成一行。程序導入了兩個包,用括號把它們括起來寫成列表形式,而沒有分開寫成獨立的import聲明。兩種形式都合法,列表形式習慣上用得多。包導入順序並不重要;gofmt工具格式化時按照字母順序對包名排序。(示例有多個版本時,我們會對示例編號,這樣可以明確當前正在討論的是哪個。)

gopl.io/ch1/echo1

// Echo1 prints its command-line arguments.
package main

import (
    "fmt"
    "os"
)

func main() {
    var s, sep string
    for i := 1; i < len(os.Args); i++ {
        s += sep + os.Args[i]
        sep = " "
    }
    fmt.Println(s)
}

註釋語句以//開頭。對於程序員來說,//之後到行末之間所有的內容都是註釋,被編譯器忽略。按照慣例,我們在每個包的包聲明前添加註釋;對於main package,註釋包含一句或幾句話,從整體角度對程序做個描述。

var聲明定義了兩個string類型的變量s和sep。變量會在聲明時直接初始化。如果變量沒有顯式初始化,則被隱式地賦予其類型的零值(zero value),數值類型是0,字符串類型是空字符串""。這個例子裡,聲明把s和sep隱式地初始化成空字符串。第2章再來詳細地講解變量和聲明。

對數值類型,Go語言提供了常規的數值和邏輯運算符。而對string類型,+運算符連接字符串(譯註:和C++或者js是一樣的)。所以表達式:

sep + os.Args[i]

表示連接字符串sep和os.Args。程序中使用的語句:

s += sep + os.Args[i]

是一條賦值語句,將s的舊值跟sep與os.Args[i]連接後賦值回s,等價於:

s = s + sep + os.Args[i]

運算符+=是賦值運算符(assignment operator),每種數值運算符或邏輯運算符,如+*,都有對應的賦值運算符。

echo程序可以每循環一次輸出一個參數,這個版本卻是不斷地把新文本追加到末尾來構造字符串。字符串s開始為空,即值為"",每次循環會添加一些文本;第一次迭代之後,還會再插入一個空格,因此循環結束時每個參數中間都有一個空格。這是一種二次加工(quadratic process),當參數數量龐大時,開銷很大,但是對於echo,這種情形不大可能出現。本章會介紹echo的若干改進版,下一章解決低效問題。

循環索引變量i在for循環的第一部分中定義。符號:=短變量聲明(short variable declaration)的一部分,這是定義一個或多個變量並根據它們的初始值為這些變量賦予適當類型的語句。下一章有這方面更多說明。

自增語句i++i加1;這和i += 1以及i = i + 1都是等價的。對應的還有i--i減1。它們是語句,而不像C系的其它語言那樣是表達式。所以j = i++非法,而且++和--都只能放在變量名後面,因此--i也非法。

Go語言只有for循環這一種循環語句。for循環有多種形式,其中一種如下所示:

for initialization; condition; post {
    // zero or more statements
}

for循環三個部分不需括號包圍。大括號強制要求,左大括號必須和post語句在同一行。

initialization語句是可選的,在循環開始前執行。initalization如果存在,必須是一條簡單語句(simple statement),即,短變量聲明、自增語句、賦值語句或函數調用。condition是一個布爾表達式(boolean expression),其值在每次循環迭代開始時計算。如果為true則執行循環體語句。post語句在循環體執行結束後執行,之後再次對condition求值。condition值為false時,循環結束。

for循環的這三個部分每個都可以省略,如果省略initializationpost,分號也可以省略:

// a traditional "while" loop
for condition {
    // ...
}

如果連condition也省略了,像下面這樣:

// a traditional infinite loop
for {
    // ...
}

這就變成一個無限循環,儘管如此,還可以用其他方式終止循環,如一條breakreturn語句。

for循環的另一種形式,在某種數據類型的區間(range)上遍歷,如字符串或切片。echo的第二版本展示了這種形式:

gopl.io/ch1/echo2

// Echo2 prints its command-line arguments.
package main

import (
    "fmt"
    "os"
)

func main() {
    s, sep := "", ""
    for _, arg := range os.Args[1:] {
        s += sep + arg
        sep = " "
    }
    fmt.Println(s)
}

每次循環迭代,range產生一對值;索引以及在該索引處的元素值。這個例子不需要索引,但range的語法要求,要處理元素,必須處理索引。一種思路是把索引賦值給一個臨時變量(如temp)然後忽略它的值,但Go語言不允許使用無用的局部變量(local variables),因為這會導致編譯錯誤。

Go語言中這種情況的解決方法是用空標識符(blank identifier),即_(也就是下劃線)。空標識符可用於在任何語法需要變量名但程序邏輯不需要的時候(如:在循環裡)丟棄不需要的循環索引,並保留元素值。大多數的Go程序員都會像上面這樣使用range_echo程序,因為隱式地而非顯式地索引os.Args,容易寫對。

echo的這個版本使用一條短變量聲明來聲明並初始化sseps,也可以將這兩個變量分開聲明,聲明一個變量有好幾種方式,下面這些都等價:

s := ""
var s string
var s = ""
var s string = ""

用哪種不用哪種,為什麼呢?第一種形式,是一條短變量聲明,最簡潔,但只能用在函數內部,而不能用於包變量。第二種形式依賴於字符串的默認初始化零值機制,被初始化為""。第三種形式用得很少,除非同時聲明多個變量。第四種形式顯式地標明變量的類型,當變量類型與初值類型相同時,類型冗餘,但如果兩者類型不同,變量類型就必須了。實踐中一般使用前兩種形式中的某個,初始值重要的話就顯式地指定變量的類型,否則使用隱式初始化。

如前文所述,每次循環迭代字符串s的內容都會更新。+=連接原字符串、空格和下個參數,產生新字符串,並把它賦值給ss原來的內容已經不再使用,將在適當時機對它進行垃圾回收。

如果連接涉及的數據量很大,這種方式代價高昂。一種簡單且高效的解決方案是使用strings包的Join函數:

gopl.io/ch1/echo3

func main() {
    fmt.Println(strings.Join(os.Args[1:], " "))
}

最後,如果不關心輸出格式,只想看看輸出值,或許只是為了調試,可以用Println為我們格式化輸出。

fmt.Println(os.Args[1:])

這條語句的輸出結果跟strings.Join得到的結果很像,只是被放到了一對方括號裡。切片都會被打印成這種格式。

練習 1.1: 修改echo程序,使其能夠打印os.Args[0],即被執行命令本身的名字。

練習 1.2: 修改echo程序,使其打印每個參數的索引和值,每個一行。

練習 1.3: 做實驗測量潛在低效的版本和使用了strings.Join的版本的運行時間差異。(1.6節講解了部分time包,11.4節展示瞭如何寫標準測試程序,以得到系統性的性能評測。)