命令行參數
os包提供了一些與操作系統交互的函數和變量,并且go對其做了一些封裝。程序的命令行參數可以從os包的Args變量獲取;os包外部使用os.Args訪問該變量。
os.Args變量是一個字符串的切片(slice)。學過Python的同學可以很容易理解切片的概念。現在先把切片s當做數組元素序列,序列的成長度動態變化,用s[i]訪問單個元素,用s[m:n]獲取子序列。序列的元素個數為len(s)。和大多數編程語言類似,區間索引時,Go言里也采用左閉右開形式, 即,區間包括第一個索引元素,不包括最后一個, 因為這樣可以簡化邏輯。
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工具格式化時按照字母順序對包名排序。
// 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類型,+運算符連接字符串。所以表達式:
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
}
or循環三個部分不需括號包圍。大括號強制要求, 左大括號必須和post語句在同一行。
initialization語句是可選的,在循環開始前執行。initalization如果存在,必須是一條簡單語句(simple statement),即,短變量聲明、自增語句、賦值語句或函數調用。condition是一個布爾表達式(boolean expression),其值在每次循環迭代開始時計算。如果為true則執行循環體語句。post語句在循環體執行結束后執行,之后再次對conditon求值。condition值為false時,循環結束。
for循環的這三個部分每個都可以省略,如果省略initialization和post,分號也可以省略:
// a traditional "while" loop
for condition {
// ...
}
如果連condition也省略了,像下面這樣:
// a traditional infinite loop
for {
// ...
}
這就變成一個無限循環,盡管如此,還可以用其他方式終止循環, 如一條break或return語句。
for循環的另一種形式, 在某種數據類型的區間(range)上遍歷,如字符串或切片。echo的第二版本展示了這種形式:
// 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的這個版本使用一條短變量聲明來聲明并初始化s和seps,也可以將這兩個變量分開聲明,聲明一個變量有好幾種方式,下面這些都等價:
s := ""
var s string
var s = ""
var s string = ""
用哪種不用哪種,為什么呢?第一種形式,是一條短變量聲明,最簡潔,但只能用在函數內部,而不能用于包變量。第二種形式依賴于字符串的默認初始化零值機制,被初始化為""。第三種形式用得很少,除非同時聲明多個變量。第四種形式顯式地標明變量的類型,當變量類型與初值類型相同時,類型冗余,但如果兩者類型不同,變量類型就必須了。實踐中一般使用前兩種形式中的某個,初始值重要的話就顯式地指定變量的類型,否則使用隱式初始化。
如前文所述,每次循環迭代字符串s的內容都會更新。+=連接原字符串、空格和下個參數,產生新字符串, 并把它賦值給s。s原來的內容已經不再使用,將在適當時機對它進行垃圾回收。
如果連接涉及的數據量很大,這種方式代價高昂。一種簡單且高效的解決方案是使用strings包的Join函數:
func main() {
fmt.Println(strings.Join(os.Args[1:], " "))
}
最后,如果不關心輸出格式,只想看看輸出值,或許只是為了調試,可以用Println為我們格式化輸出。
fmt.Println(os.Args[1:])
這條語句的輸出結果跟strings.Join得到的結果很像,只是被放到了一對方括號里。切片都會被打印成這種格式。
練習 1.1: 修改echo程序,使其能夠打印os.Args[0],即被執行命令本身的名字。
package main
import (
"fmt"
"os"
)
func main() {
s, sep := "", ""
fmt.Println("file:",os.Args[0])
for _, arg := range os.Args[1:] {
s += sep + arg
sep = " "
}
fmt.Println(s)
}
練習 1.2: 修改echo程序,使其打印每個參數的索引和值,每個一行。
package main
import (
"fmt"
"os"
)
func main() {
for idx, arg := range os.Args[1:] {
fmt.Println(idx,arg)
}
}
練習 1.3: 做實驗測量潛在低效的版本和使用了strings.Join的版本的運行時間差異。(1.6節講解了部分time包,11.4節展示了如何寫標準測試程序,以得到系統性的性能評測。)