大多數的程序都是處理輸入,産生輸出;這也正是“計算”的定義。但是一個程序要如何獲取輸入呢?一些程序會生成自己的數據,但通常情況下,輸入都來自於程序外部:比如文件、網絡連接、其它程序的輸出、用戶的鍵盤、命令行的參數或其它類似輸入源。下面幾個例子會討論其中的一些輸入類型,首先是命令行參數。
os這個package提供了操作繫統無關(跨平台)的,與繫統交互的一些函數和相關的變量,運行時程序的命令行參數可以通過os包中一個叫Args的變量來獲取;當在os包外部使用該變量時,需要用os.Args來訪問。
os.Args這個變量是一個字符串(string)的slice(譯註:slice和Python語言中的切片類似,是一個簡版的動態數組),slice在Go語言里是一個基礎的數據結構,之後我們很快會提到。現在可以先把slice當一個簡單的元素序列,可以用類似s[i]的下標訪問形式獲取其內容,併且可以用形如s[m:n]的形式來獲取到一個slice的子集(譯註:和python里的語法差不多)。其長度可以用len(s)函數來獲取。和其它大多數編程語言類似,Go語言里的這種索引形式也采用了左閉右開區間,包括m~n的第一個元素,但不包括最後那個元素(譯註:比如a = [1, 2, 3, 4, 5], a[0:3] = [1, 2, 3],不包含最後一個元素)。這樣可以簡化我們的處理邏輯。比如s[m:n]這個slice,0 ≤ m ≤ n ≤ len(s),包含n-m個元素。
os.Args的第一個元素,卽os.Args[0]是命令行執行時的命令本身;其它的元素則是執行該命令時傳給這個程序的參數。前面提到的切片表達式,s[m:n]會返迴第m到第n-1個元素,所以下一個例子里需要用到的os.Args[1:len(os.Args)]卽是除了命令本身外的所有傳入參數。如果我們省略s[m:n]里的m和n,那麽默認這個表達式會填入0:len(s),所以這里我們還可以省略掉n,寫成os.Args[1:]。
下面是一個Unix里echo命令的實現,這個命令會在單行內打印出命令行參數。程序中import了兩個package,併且用括號把這兩個package包了起來,這是import多個package時的簡化寫法。當然了分開寫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)
}
Go語言里的註釋是以//來表示。//之後的內容一直到行末都是這條註釋的一部分,這些註釋會被編譯器忽略。
按照慣例,每一個package前都需要有詳盡的註釋對該package進行説明;對於main package來説,這段註釋一般會包含幾句話,説明這個項目/程序整體是做什麽用的。
var關鍵字用來聲明變量。這個程序聲明了s和sep兩個string變量。變量可以在聲明期間直接進行初始化。如果沒有顯式初始化,Go語言會隱式地給這些未初始化的變量賦予對應其具體類型的零值,比如數值類型就是0,字符串類型就是空字符串""。在這個例子里的s和sep被隱式地賦值爲了空字符串。在第2章中我們會更詳細地講解變量和聲明。
對於數值類型,Go語言提供了常規的數值/邏輯運算符。而對於string類型,+號表示字符串的連接(譯註:和C++或者js是一樣的)。所以下面這個表達式:
sep + os.Args[i]
表示將sep字符串和os.Args[i]字符串進行連接。我們在程序里用的另外一個表達式:
s += sep + os.Args[i]
會將sep與os.Args[i]連接,然後再將得到的結果與s進行連接,再將結果併賦值給s,和下面的表達是等價:
s = s + sep + os.Args[i]
運算符+=是一個賦值運算符(assignment operator),每一種數值/邏輯運算符,例如*或者+都有其對應的賦值運算符。
echo程序可以每循環一次輸出一個參數,不過我們這里的版本是不斷地將其結果連接到一個字符串的末尾。s這個字符串在聲明的時候是一個空字符串,而之後循環每次都會被在末尾添加一段字符串;第一次迭代之後,一個空格會被插入到字符串末尾,所以每插入一個新值,都會和前一個中間有一個空格隔開。這是一種非線性的操作,當我們的參數數量變得龐大的時候(當然不是説這里的echo,一般echo也不會有太多參數)其運行開銷也會變得龐大。下面我們會介紹一繫列的echo改進版,來改進這個程序的運行效率。
在for循環中,我們用i來做下標索引,用:=符號來給i進行初始化和賦值,這是var xxx=yyy的一種簡寫形式,Go語言會根據等號右邊的值的類型自動判斷左邊的值類型,下一章會對這一點進行詳細説明。
自增表達式i++會爲i加上1;這和i += 1以及i = i + 1都是等價的。對應的還有i--是給i減去1。這些在Go語言里是語句,而不像C繫的其它語言里是表達式。所以在Go語言里j = i++是非法的,而且++和--都隻能放在變量名後面,因此--i也是非法的。
在Go語言里隻有for循環一種循環。當然了爲了滿足需求,Go的for循環有很多種形式,下面是其中的一種:
for initialization; condition; post {
// zero or more statements
}
需要註意的是,for循環的兩邊是不需要像其它語言一樣寫括號的。併且左花括號需要和for語句在同一行。
initialization部分是可選的,如果你寫了這部分的話,在for循環之前這部分的邏輯會被執行。initalization部分必須是一個簡單的語句,具體可以是一個簡短的變量聲明,一個賦值語句,或是一個函數調用。condition部分必須是一個結果爲boolean值的表達式,在每次循環之前,語言都會檢査當前是否滿足這個條件,若不滿足的話便會結束循環;post部分的語句則是在每次循環迭代結束之後被執行,之後conditon部分會在下一次執行前再次進行判斷,依此往複。當condition條件里的判斷結果變爲false之後,循環卽結束。
上面提到是for循環里的三個部分(initialization/condition/post)都是可以被省略的,如果你把initialization和post部分都省略的話,那麽連中間隔離他們的分號也是可以被省略的,比如下面這種for循環,和傳統的while循環效果完全一致:
// a traditional "while" loop
for condition {
// ...
}
當然了,如果你連唯一的條件都省了,那麽for循環就會變成一個無限循環,像下面這樣:
// a traditional infinite loop
for {
// ...
}
在無限循環中,你還是可以靠break或者return語句來終止掉循環。
如果你的遍歷對象是string或者slice類型值的話,還有另外一種循環的寫法,我們來看看另一個版本的echo:
gopl.io/ch1/echo2
// Echo2 prints its command-line arguments.
package main
import (
"fmt"
)
func main() {
s, sep := "", ""
for _, arg := range os.Args[1:] {
s += sep + arg
sep = " "
}
fmt.Println(s)
}
每一次循環迭代,range都會返迴一對兒結果;當前迭代的下標以及在該下標處的元素的值。這個例子不需要這個下標,但是因爲range函數要求我們必須同時處理下標和元素兩個返迴值。這種時候可以在聲明一個接收下標的臨時變量來解決這個問題,但Go語言又不允許隻聲明變量而在後續代碼里不使用,如果你這樣做了編譯器會返迴一個編譯錯誤。
Go語言中這種情況的解決方法是用空白標識符,對,就是代碼里那個下劃線_。空白標識符可以在任何你需要接收自己不想處理的值時使用。這里使用它來忽略掉range返迴的那個沒什麽用的下標值。大多數的Go程序員都會像上面這樣來寫類似的os.Args遍歷,由於遍歷os.Args的下標索引是隱式自動生成的,這里也併不需要關心。
上面這個版本將s和sep的聲明和初始化都放到了一起,但是我們可以等價地將聲明和賦值分開來寫,下面這些寫法都是等價的:
s := ""
var s string
var s = ""
var s string = ""
那麽這些等價的形式應該怎麽做選擇呢?這里提供一些建議:第一種形式,隻能用在一個函數內部,而package級别的變量不應該這麽做。第二種形式依賴於string類型的內部初始化機製,被初始化爲空字符串。第三種形式使用得很少,除非同時聲明多個變量。第四種形式會顯式地標明變量的類型,在多變量同時聲明時可以用到。實踐中你應該隻使用上面的前兩種形式,顯式地指定變量的類型,讓編譯器自己去初始化其值,或者直接用隱式初始化,表明初始值怎麽樣併不重要。
像上面提到的,每次循環迭代中字符串s都會得到一個新內容。+=語句會分配一個新的字符串,併將老字符串連接起來的值賦予給它。而目標字符串的舊的字面值在得到新值以後就失去了用處,這些臨時值會被Go語言的垃圾收集器榦掉。
如果不斷連接的字符串數量衆多,那麽上面這種操作就是成本非常高的操作。更簡單併且有效的一種方式是使用strings包提供的Join函數,像下面這樣:
gopl.io/ch1/echo3
func main() {
fmt.Println(strings.Join(os.Args[1:], " "))
}
最後,如果我們對輸出的格式也不是很關心,隻是想簡單地輸出值得的話,還可以像下面這麽寫,Println函數會爲我們自動格式化輸出。
fmt.Println(os.Args[1:])
這個輸出結果和前面的strings.Join得到的結果很相似,隻是輸出被放到了一個方括號里,對slice類型調用Println函數都會被打印成這種形式的結果。
練習 1.1: 脩改echo程序,使其能夠打印os.Args[0]。
練習 1.2: 脩改echo程序,使其打印value和index,每個value和index顯示一行。
練習 1.3: 上手實踐前面提到的strings.Join和直接Println,併觀察輸出結果的區别。