原文鏈接:https://gopl-zh.github.io/ch1/ch1-02.html
大多數(shù)的程序都是處理輸入,產(chǎn)生輸出;這也正是“計算”的定義。但是,程序如何獲取要處理的輸入數(shù)據(jù)呢?一些程序生成自己的數(shù)據(jù),但通常情況下,輸入來自于程序外部:文件、網(wǎng)絡(luò)連接、其它程序的輸出、敲鍵盤的用戶、命令行參數(shù)或其它類似輸入源。下面幾個例子會討論其中幾個輸入源,首先是命令行參數(shù)。
?os
?包以跨平臺的方式,提供了一些與操作系統(tǒng)交互的函數(shù)和變量。程序的命令行參數(shù)可從 ?os
?包的 ?Args
?變量獲??;?os
?包外部使用 ?os.Args
? 訪問該變量。
os.Args
變量是一個字符串(string)的 切片(slice)(譯注:slice 和 Python 語言中的切片類似,是一個簡版的動態(tài)數(shù)組),切片是 Go 語言的基礎(chǔ)概念,稍后詳細(xì)介紹。現(xiàn)在先把切片 s
當(dāng)作數(shù)組元素序列,序列的長度動態(tài)變化,用 s[i]
訪問單個元素,用 s[m:n]
獲取子序列(譯注:和
Python 里的語法差不多)。序列的元素數(shù)目為 len(s)
。和大多數(shù)編程語言類似,區(qū)間索引時,Go 語言里也采用左閉右開形式,即,區(qū)間包括第一個索引元素,不包括最后一個,因為這樣可以簡化邏輯。(譯注:比如 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]
,是命令本身的名字;其它的元素則是程序啟動時傳給它的參數(shù)。s[m:n]
形式的切片表達(dá)式,產(chǎn)生從第 m
個元素到第 n-1
個元素的切片,下個例子用到的元素包含在 os.Args[1:len(os.Args)]
切片中。如果省略切片表達(dá)式的 m
或 n
,會默認(rèn)傳入 0
或 len(s)
,因此前面的切片可以簡寫成 os.Args[1:]
。
下面是 Unix 里 echo
命令的一份實現(xiàn),echo
把它的命令行參數(shù)打印成一行。程序?qū)肓藘蓚€包,用括號把它們括起來寫成列表形式,而沒有分開寫成獨立的 import
聲明。兩種形式都合法,列表形式習(xí)慣上用得多。包導(dǎo)入順序并不重要;gofmt
工具格式化時按照字母順序?qū)Π判颉#ㄊ纠卸鄠€版本時,我們會對示例編號,這樣可以明確當(dāng)前正在討論的是哪個。)
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)
}
注釋語句以 //
開頭。對于程序員來說,//
之后到行末之間所有的內(nèi)容都是注釋,被編譯器忽略。按照慣例,我們在每個包的包聲明前添加注釋;對于 main package
,注釋包含一句或幾句話,從整體角度對程序做個描述。
var
聲明定義了兩個 string
類型的變量 s
和 sep
。變量會在聲明時直接初始化。如果變量沒有顯式初始化,則被隱式地賦予其類型的 零值(zero value),數(shù)值類型是 0
,字符串類型是空字符串 ""
。這個例子里,聲明把 s
和 sep
隱式地初始化成空字符串。第
2 章再來詳細(xì)地講解變量和聲明。
對數(shù)值類型,Go 語言提供了常規(guī)的數(shù)值和邏輯運算符。而對 string
類型,+
運算符連接字符串(譯注:和 C++ 或者 JavaScript 是一樣的)。所以表達(dá)式: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),每種數(shù)值運算符或邏輯運算符,如 +
或 *
,都有對應(yīng)的賦值運算符。
echo
程序可以每循環(huán)一次輸出一個參數(shù),這個版本卻是不斷地把新文本追加到末尾來構(gòu)造字符串。字符串 s
開始為空,即值為 ""
,每次循環(huán)會添加一些文本;第一次迭代之后,還會再插入一個空格,因此循環(huán)結(jié)束時每個參數(shù)中間都有一個空格。這是一種二次加工(quadratic process),當(dāng)參數(shù)數(shù)量龐大時,開銷很大,但是對于 echo
,這種情形不大可能出現(xiàn)。本章會介紹 echo
的若干改進(jìn)版,下一章解決低效問題。
循環(huán)索引變量 i
在 for
循環(huán)的第一部分中定義。符號 :=
是 短變量聲明(short variable declaration)的一部分,這是定義一個或多個變量并根據(jù)它們的初始值為這些變量賦予適當(dāng)類型的語句。下一章有這方面更多說明。
自增語句 i++
給 i
加 1
;這和 i+=1
以及 i=i+1
都是等價的。對應(yīng)的還有 i--
給 i
減 1
。它們是語句,而不像 C 系的其它語言那樣是表達(dá)式。所以 j=i++
非法,而且 ++
和 --
都只能放在變量名后面,因此 --i
也非法。
Go 語言只有 for
循環(huán)這一種循環(huán)語句。for
循環(huán)有多種形式,其中一種如下所示:
for initialization; condition; post {
// zero or more statements
}
for
循環(huán)三個部分不需括號包圍。大括號強(qiáng)制要求,左大括號必須和 post
語句在同一行。
initialization
語句是可選的,在循環(huán)開始前執(zhí)行。initalization
如果存在,必須是一條 簡單語句(simple statement),即,短變量聲明、自增語句、賦值語句或函數(shù)調(diào)用。condition
是一個布爾表達(dá)式(boolean expression),其值在每次循環(huán)迭代開始時計算。如果為 true
則執(zhí)行循環(huán)體語句。post
語句在循環(huán)體執(zhí)行結(jié)束后執(zhí)行,之后再次對 condition
求值。condition
值為 false
時,循環(huán)結(jié)束。
for 循環(huán)的這三個部分每個都可以省略,如果省略 initialization
和 post
,分號也可以省略:
// a traditional "while" loop
for condition {
// ...
}
如果連 condition
也省略了,像下面這樣:
// a traditional infinite loop
for {
// ...
}
這就變成一個無限循環(huán),盡管如此,還可以用其他方式終止循環(huán),如一條 ?break
?或 ?return
?語句。
?for
?循環(huán)的另一種形式,在某種數(shù)據(jù)類型的區(qū)間(range)上遍歷,如字符串或切片。?echo
?的第二版本展示了這種形式:
gopl.io/ch1/echo2
// 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)
}
每次循環(huán)迭代,range
產(chǎn)生一對值;索引以及在該索引處的元素值。這個例子不需要索引,但 range
的語法要求,要處理元素,必須處理索引。一種思路是把索引賦值給一個臨時變量(如 temp
)然后忽略它的值,但 Go 語言不允許使用無用的局部變量(local variables),因為這會導(dǎo)致編譯錯誤。
Go 語言中這種情況的解決方法是用 空標(biāo)識符(blank identifier),即 _
(也就是下劃線)??諛?biāo)識符可用于在任何語法需要變量名但程序邏輯不需要的時候(如:在循環(huán)里)丟棄不需要的循環(huán)索引,并保留元素值。大多數(shù)的 Go 程序員都會像上面這樣使用 range
和 _
寫 echo
程序,因為隱式地而非顯式地索引 os.Args
,容易寫對。
echo
的這個版本使用一條短變量聲明來聲明并初始化 s
和 seps
,也可以將這兩個變量分開聲明,聲明一個變量有好幾種方式,下面這些都等價:
s := ""
var s string
var s = ""
var s string = ""
用哪種不用哪種,為什么呢?第一種形式,是一條短變量聲明,最簡潔,但只能用在函數(shù)內(nèi)部,而不能用于包變量。第二種形式依賴于字符串的默認(rèn)初始化零值機(jī)制,被初始化為 ""
。第三種形式用得很少,除非同時聲明多個變量。第四種形式顯式地標(biāo)明變量的類型,當(dāng)變量類型與初值類型相同時,類型冗余,但如果兩者類型不同,變量類型就必須了。實踐中一般使用前兩種形式中的某個,初始值重要的話就顯式地指定變量的類型,否則使用隱式初始化。
如前文所述,每次循環(huán)迭代字符串 s
的內(nèi)容都會更新。+=
連接原字符串、空格和下個參數(shù),產(chǎn)生新字符串,并把它賦值給 s
。s
原來的內(nèi)容已經(jīng)不再使用,將在適當(dāng)時機(jī)對它進(jìn)行垃圾回收。
如果連接涉及的數(shù)據(jù)量很大,這種方式代價高昂。一種簡單且高效的解決方案是使用 strings
包的 Join
函數(shù):
gopl.io/ch1/echo3
func main() {
fmt.Println(strings.Join(os.Args[1:], " "))
}
最后,如果不關(guān)心輸出格式,只想看看輸出值,或許只是為了調(diào)試,可以用 Println
為我們格式化輸出。
fmt.Println(os.Args[1:])
這條語句的輸出結(jié)果跟 strings.Join
得到的結(jié)果很像,只是被放到了一對方括號里。切片都會被打印成這種格式。
練習(xí) 1.1: 修改 echo
程序,使其能夠打印 os.Args[0]
,即被執(zhí)行命令本身的名字。
練習(xí) 1.2: 修改 echo
程序,使其打印每個參數(shù)的索引和值,每個一行。
練習(xí) 1.3: 做實驗測量潛在低效的版本和使用了 strings.Join
的版本的運行時間差異。(1.6 節(jié)講解了部分 time
包,11.4 節(jié)展示了如何寫標(biāo)準(zhǔn)測試程序,以得到系統(tǒng)性的性能評測。)
![]() |
![]() |
更多建議: