## 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工具格式化時按照字母順序對包名排序。(示例有多個版本時,我們會對示例編號, 這樣可以明確當前正在討論的是哪個。)
<u><i>gopl.io/ch1/echo1</i></u>
```go
// 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是一樣的)。所以表達式:
```go
sep + os.Args[i]
```
表示連接字符串sep和os.Args。程序中使用的語句:
```go
s += sep + os.Args[i]
```
是一條*賦值語句*, 將s的舊值跟sep與os.Args[i]連接后賦值回s,等價于:
```go
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循環有多種形式,其中一種如下所示:
```go
for initialization; condition; post {
// zero or more statements
}
```
for循環三個部分不需括號包圍。大括號強制要求, 左大括號必須和*post*語句在同一行。
*initialization*語句是可選的,在循環開始前執行。*initalization*如果存在,必須是一條*簡單語句*(simple statement),即,短變量聲明、自增語句、賦值語句或函數調用。`condition`是一個布爾表達式(boolean expression),其值在每次循環迭代開始時計算。如果為`true`則執行循環體語句。`post`語句在循環體執行結束后執行,之后再次對`condition`求值。`condition`值為`false`時,循環結束。
for循環的這三個部分每個都可以省略,如果省略`initialization`和`post`,分號也可以省略:
```go
// a traditional "while" loop
for condition {
// ...
}
```
如果連`condition`也省略了,像下面這樣:
```go
// a traditional infinite loop
for {
// ...
}
```
這就變成一個無限循環,盡管如此,還可以用其他方式終止循環, 如一條`break`或`return`語句。
`for`循環的另一種形式, 在某種數據類型的區間(range)上遍歷,如字符串或切片。`echo`的第二版本展示了這種形式:
<u><i>gopl.io/ch1/echo2</i></u>
```go
// 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`,也可以將這兩個變量分開聲明,聲明一個變量有好幾種方式,下面這些都等價:
```go
s := ""
var s string
var s = ""
var s string = ""
```
用哪種不用哪種,為什么呢?第一種形式,是一條短變量聲明,最簡潔,但只能用在函數內部,而不能用于包變量。第二種形式依賴于字符串的默認初始化零值機制,被初始化為""。第三種形式用得很少,除非同時聲明多個變量。第四種形式顯式地標明變量的類型,當變量類型與初值類型相同時,類型冗余,但如果兩者類型不同,變量類型就必須了。實踐中一般使用前兩種形式中的某個,初始值重要的話就顯式地指定變量的類型,否則使用隱式初始化。
如前文所述,每次循環迭代字符串s的內容都會更新。`+=`連接原字符串、空格和下個參數,產生新字符串, 并把它賦值給`s`。`s`原來的內容已經不再使用,將在適當時機對它進行垃圾回收。
如果連接涉及的數據量很大,這種方式代價高昂。一種簡單且高效的解決方案是使用`strings`包的`Join`函數:
<u><i>gopl.io/ch1/echo3</i></u>
```go
func main() {
fmt.Println(strings.Join(os.Args[1:], " "))
}
```
最后,如果不關心輸出格式,只想看看輸出值,或許只是為了調試,可以用`Println`為我們格式化輸出。
```go
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節展示了如何寫標準測試程序,以得到系統性的性能評測。)
- 前言
- Go語言起源
- Go語言項目
- 本書的組織
- 更多的信息
- 致謝
- 入門
- Hello, World
- 命令行參數
- 查找重復的行
- GIF動畫
- 獲取URL
- 并發獲取多個URL
- Web服務
- 本章要點
- 程序結構
- 命名
- 聲明
- 變量
- 賦值
- 類型
- 包和文件
- 作用域
- 基礎數據類型
- 整型
- 浮點數
- 復數
- 布爾型
- 字符串
- 常量
- 復合數據類型
- 數組
- Slice
- Map
- 結構體
- JSON
- 文本和HTML模板
- 函數
- 函數聲明
- 遞歸
- 多返回值
- 錯誤
- 函數值
- 匿名函數
- 可變參數
- Deferred函數
- Panic異常
- Recover捕獲異常
- 方法
- 方法聲明
- 基于指針對象的方法
- 通過嵌入結構體來擴展類型
- 方法值和方法表達式
- 示例: Bit數組
- 封裝
- 接口
- 接口是合約
- 接口類型
- 實現接口的條件
- flag.Value接口
- 接口值
- sort.Interface接口
- http.Handler接口
- error接口
- 示例: 表達式求值
- 類型斷言
- 基于類型斷言識別錯誤類型
- 通過類型斷言查詢接口
- 類型分支
- 示例: 基于標記的XML解碼
- 補充幾點
- Goroutines和Channels
- Goroutines
- 示例: 并發的Clock服務
- 示例: 并發的Echo服務
- Channels
- 并發的循環
- 示例: 并發的Web爬蟲
- 基于select的多路復用
- 并發的退出
- 示例: 聊天服務
- 基于共享變量的并發
- 競爭條件
- sync.Mutex互斥鎖
- sync.RWMutex讀寫鎖
- 內存同步
- 競爭條件檢測
- 示例: 并發的非阻塞緩存
- Goroutines和線程
- 包和工具
- 包簡介
- 導入路徑
- 包聲明
- 導入聲明
- 包的匿名導入
- 包和命名
- 工具
- 測試
- go test
- 測試函數
- 測試覆蓋率
- 基準測試
- 剖析
- 示例函數
- 反射
- 為何需要反射?
- reflect.Type和reflect.Value
- Display遞歸打印
- 示例: 編碼S表達式
- 通過reflect.Value修改值
- 示例: 解碼S表達式
- 顯示一個類型的方法集
- 幾點忠告
- 底層編程
- unsafe.Sizeof, Alignof 和 Offsetof
- unsafe.Pointer
- 示例: 深度相等判斷
- 通過cgo調用C代碼
- 幾點忠告
- 附錄
- 附錄A:原文勘誤
- 附錄B:作者譯者
- 附錄C:譯文授權
- 附錄D:其它語言