gopl-zh.github.com/ch1/ch1-02.md

158 lines
10 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

## 1.2. 命令行參數
大多數的程序都是處理輸入,産生輸出;這也正是“計算”的定義。但是一個程序要如何獲取輸入呢?一些程序會生成自己的數據,但通常情況下,輸入都來自於程序外部:比如文件、網絡連接、其它程序的輸出、用戶的鍵盤、命令行的參數或其它類似輸入源。下面幾個例子會討論其中的一些輸入類型,首先是命令行參數。
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]這個slice0 ≤ 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工具格式化時會按照字母順序來排列好這些被導入的包名。本書中代碼范例的不同版本會用編號來標記
```go
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是一樣的。所以下面這個表達式
```go
sep + os.Args[i]
```
表示將sep字符串和os.Args[i]字符串進行連接。我們在程序里用的另外一個表達式:
```go
s += sep + os.Args[i]
```
會將sep與os.Args[i]連接然後再將得到的結果與s進行連接再將結果併賦值給s和下面的表達是等價
```go
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循環有很多種形式下面是其中的一種
```go
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循環效果完全一致
```go
// a traditional "while" loop
for condition {
// ...
}
```
當然了如果你連唯一的條件都省了那麽for循環就會變成一個無限循環像下面這樣
```go
// a traditional infinite loop
for {
// ...
}
```
在無限循環中你還是可以靠break或者return語句來終止掉循環。
如果你的遍歷對象是string或者slice類型值的話還有另外一種循環的寫法我們來看看另一個版本的echo
```go
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的聲明和初始化都放到了一起但是我們可以等價地將聲明和賦值分開來寫下面這些寫法都是等價的
```go
s := ""
var s string
var s = ""
var s string = ""
```
那麽這些等價的形式應該怎麽做選擇呢這里提供一些建議第一種形式隻能用在一個函數內部而package級别的變量不應該這麽做。第二種形式依賴於string類型的內部初始化機製被初始化爲空字符串。第三種形式使用得很少除非同時聲明多個變量。第四種形式會顯式地標明變量的類型在多變量同時聲明時可以用到。實踐中你應該隻使用上面的前兩種形式顯式地指定變量的類型讓編譯器自己去初始化其值或者直接用隱式初始化表明初始值怎麽樣併不重要。
像上面提到的每次循環迭代中字符串s都會得到一個新內容。+=語句會分配一個新的字符串併將老字符串連接起來的值賦予給它。而目標字符串的舊的字面值在得到新值以後就失去了用處這些臨時值會被Go語言的垃圾收集器榦掉。
如果不斷連接的字符串數量衆多那麽上面這種操作就是成本非常高的操作。更簡單併且有效的一種方式是使用strings包提供的Join函數像下面這樣
```go
gopl.io/ch1/echo3
func main() {
fmt.Println(strings.Join(os.Args[1:], " "))
}
```
最後如果我們對輸出的格式也不是很關心隻是想簡單地輸出值得的話還可以像下面這麽寫Println函數會爲我們自動格式化輸出。
```go
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併觀察輸出結果的區别。