9.3 KiB
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语句是可選的,在循環开始前執行。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
的第二版本展示了这种形式:
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
的语法要求, 要處理元素, 必须处理索引。一种思路是把索引赋值给一个臨時變量, 如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
函數:
gopl.io/ch1/echo3
func main() {
fmt.Println(strings.Join(os.Args[1:], " "))
}
最後,如果不關心輸出格式,隻想看看輸出值,或许只是为了调试,可以用Println
为我们格式化輸出。
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节展示了如何写标准测试程序,以得到系统性的性能评测。)