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

9.8 KiB
Raw Blame History

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部分是可選的如果你寫了這部分的話在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併觀察輸出結果的區别。