mirror of
https://github.com/gopl-zh/gopl-zh.github.com.git
synced 2024-11-05 05:53:45 +00:00
make loop
This commit is contained in:
parent
82ec0c025d
commit
e15e88dad7
@ -95,7 +95,7 @@
|
||||
* [8.6. 示例: 併發的Web爬蟲](ch8/ch8-06.md)
|
||||
* [8.7. 基於select的多路複用](ch8/ch8-07.md)
|
||||
* [8.8. 示例: 併發的字典遍歷](ch8/ch8-08.md)
|
||||
* [8.9. 併發的退齣](ch8/ch8-09.md)
|
||||
* [8.9. 併發的退出](ch8/ch8-09.md)
|
||||
* [8.10. 示例: 聊天服務](ch8/ch8-10.md)
|
||||
* [第九章 基於共享變量的併發](ch9/ch9.md)
|
||||
* [9.1. 競爭條件](ch9/ch9-01.md)
|
||||
|
@ -80,7 +80,7 @@
|
||||
* [示例: 併發的Web爬蟲](ch8/ch8-06.md)
|
||||
* [基於select的多路複用](ch8/ch8-07.md)
|
||||
* [示例: 併發的字典遍歷](ch8/ch8-08.md)
|
||||
* [併發的退齣](ch8/ch8-09.md)
|
||||
* [併發的退出](ch8/ch8-09.md)
|
||||
* [示例: 聊天服務](ch8/ch8-10.md)
|
||||
* [基於共享變量的併發](ch9/ch9.md)
|
||||
* [競爭條件](ch9/ch9-01.md)
|
||||
|
@ -4,7 +4,7 @@
|
||||
|
||||
正如Rob Pike所説,“軟件的複雜性是乘法級相關的”,通過增加一個部分的複雜性來脩複問題通常將慢慢地增加其他部分的複雜性。通過增加功能和選項和配置是脩複問題的最快的途徑,但是這很容易讓人忘記簡潔的內涵,卽使從長遠來看,簡潔依然是好軟件的關鍵因素。
|
||||
|
||||
簡潔的設計需要在工作開始的時候舍棄不必要的想法,併且在軟件的生命週期內嚴格區别好的改變或壞的改變。通過足夠的努力,一個好的改變可以在不破壞原有完整概念的前提下保持自適應,正如Fred Brooks所説的“概念完整性”;而一個壞的改變則不能達到這個效果,它們僅僅是通過膚淺的和簡單的妥協來破壞原有設計的一致性。隻有通過簡潔的設計,纔能讓一個繫統保持穩定、安全和持續的進化。
|
||||
簡潔的設計需要在工作開始的時候舍棄不必要的想法,併且在軟件的生命週期內嚴格區别好的改變或壞的改變。通過足夠的努力,一個好的改變可以在不破壞原有完整概念的前提下保持自適應,正如Fred Brooks所説的“概念完整性”;而一個壞的改變則不能達到這個效果,它們僅僅是通過膚淺的和簡單的妥協來破壞原有設計的一致性。隻有通過簡潔的設計,才能讓一個繫統保持穩定、安全和持續的進化。
|
||||
|
||||
Go項目包括編程語言本身,附帶了相關的工具和標準庫,最後但併非代表不重要的,關於簡潔編程哲學的宣言。就事後諸葛的角度來看,Go語言的這些地方都做的還不錯:擁有自動垃圾迴收、一個包繫統、函數作爲一等公民、詞法作用域、繫統調用接口、隻讀的UTF8字符串等。但是Go語言本身隻有很少的特性,也不太可能添加太多的特性。例如,它沒有隱式的數值轉換,沒有構造函數和析構函數,沒有運算符重載,沒有默認參數,也沒有繼承,沒有泛型,沒有異常,沒有宏,沒有函數脩飾,更沒有線程局部存儲。但是語言本身是成熟和穩定的,而且承諾保證向後兼容:用之前的Go語言編寫程序可以用新版本的Go語言編譯器和標準庫直接構建而不需要脩改代碼。
|
||||
|
||||
|
@ -10,5 +10,5 @@ Playground可以簡單的通過執行一個小程序來測試對語法、語義
|
||||
|
||||
當然,Playground 和 Tour 也有一些限製,它們隻能導入標準庫,而且因爲安全的原因對一些網絡庫做了限製。如果要在編譯和運行時需要訪問互聯網,對於一些更複製的實驗,你可能需要在自己的電腦上構建併運行程序。幸運的是下載Go語言的過程很簡單,從 https://golang.org 下載安裝包應該不超過幾分鐘(譯註:感謝偉大的長城,讓大陸的Gopher們都學會了自己打洞的基本生活技能,下載時間可能會因爲洞的大小等因素從幾分鐘到幾天或更久),然後就可以在自己電腦上編寫和運行Go程序了。
|
||||
|
||||
Go語言是一個開源項目,你可以在 https://golang.org/pkg 閲讀標準庫中任意函數和類型的實現代碼,和下載安裝包的代碼完全一致。這樣你可以知道很多函數是如何工作的, 通過挖掘找齣一些答案的細節,或者僅僅是齣於欣賞專業級Go代碼。
|
||||
Go語言是一個開源項目,你可以在 https://golang.org/pkg 閲讀標準庫中任意函數和類型的實現代碼,和下載安裝包的代碼完全一致。這樣你可以知道很多函數是如何工作的, 通過挖掘找出一些答案的細節,或者僅僅是出於欣賞專業級Go代碼。
|
||||
|
||||
|
@ -1,8 +1,8 @@
|
||||
## 致謝
|
||||
|
||||
Rob Pike和Russ Cox,以及很多其他Go糰隊的覈心成員多次仔細閲讀了本書的手稿,他們對本書的組織結構和表述用詞等給齣了很多寶貴的建議。在準備日文版翻譯的時候,Yoshiki Shibata更是仔細地審閲了本書的每個部分,及時發現了諸多英文和代碼的錯誤。我們非常感謝本書的每一位審閲者,併感謝對本書給齣了重要的建議的Brian Goetz、Corey Kosak、Arnold Robbins、Josh Bleecher Snyder和Peter Weinberger等人。
|
||||
Rob Pike和Russ Cox,以及很多其他Go糰隊的覈心成員多次仔細閲讀了本書的手稿,他們對本書的組織結構和表述用詞等給出了很多寶貴的建議。在準備日文版翻譯的時候,Yoshiki Shibata更是仔細地審閲了本書的每個部分,及時發現了諸多英文和代碼的錯誤。我們非常感謝本書的每一位審閲者,併感謝對本書給出了重要的建議的Brian Goetz、Corey Kosak、Arnold Robbins、Josh Bleecher Snyder和Peter Weinberger等人。
|
||||
|
||||
我們還感謝Sameer Ajmani、Ittai Balaban、David Crawshaw、Billy Donohue、Jonathan Feinberg、Andrew Gerrand、Robert Griesemer、John Linderman、Minux Ma(譯註:中國人,Go糰隊成員。)、Bryan Mills、Bala Natarajan、Cosmos Nicolaou、Paul Staniforth、Nigel Tao(譯註:好像是陶哲軒的兄弟)以及Howard Trickey給齣的許多有價值的建議。我們還要感謝David Brailsford和Raph Levien關於類型設置的建議。
|
||||
我們還感謝Sameer Ajmani、Ittai Balaban、David Crawshaw、Billy Donohue、Jonathan Feinberg、Andrew Gerrand、Robert Griesemer、John Linderman、Minux Ma(譯註:中國人,Go糰隊成員。)、Bryan Mills、Bala Natarajan、Cosmos Nicolaou、Paul Staniforth、Nigel Tao(譯註:好像是陶哲軒的兄弟)以及Howard Trickey給出的許多有價值的建議。我們還要感謝David Brailsford和Raph Levien關於類型設置的建議。
|
||||
|
||||
我們的來自Addison-Wesley的編輯Greg Doench收到了很多幫助,從最開始就得到了越來越多的幫助。來自AW生産糰隊的John Fuller、Dayna Isley、Julie Nahil、Chuti Prasertsith到Barbara Wood,感謝你們的熱心幫助。
|
||||
|
||||
|
@ -1,6 +1,6 @@
|
||||
## 1.1. Hello, World
|
||||
|
||||
我們以1978年齣版的C語言聖經《The C Programming Language》中經典的“hello world”案例來開始吧(譯註:本書作者之一Brian W. Kernighan也是C語言聖經一書的作者)。C語言對Go語言的設計産生了很多影響。用這個例子,我們來講解一些Go語言的覈心特性:
|
||||
我們以1978年出版的C語言聖經《The C Programming Language》中經典的“hello world”案例來開始吧(譯註:本書作者之一Brian W. Kernighan也是C語言聖經一書的作者)。C語言對Go語言的設計産生了很多影響。用這個例子,我們來講解一些Go語言的覈心特性:
|
||||
|
||||
```go
|
||||
gopl.io/ch1/helloworld
|
||||
@ -19,7 +19,7 @@ Go是一門編譯型語言,Go語言的工具鏈將源代碼和其依賴一起
|
||||
$ go run helloworld.go
|
||||
```
|
||||
|
||||
毫無意外,這個命令會輸齣:
|
||||
毫無意外,這個命令會輸出:
|
||||
|
||||
```
|
||||
Hello, 世界
|
||||
@ -52,7 +52,7 @@ gopl.io/ch1/helloworld
|
||||
|
||||
我們來討論一下程序本身。Go語言的代碼是通過package來組織的,package的概念和你知道的其它語言里的libraries或者modules概念比較類似。一個package會包含一個或多個.go結束的源代碼文件。每一個源文件都是以一個package xxx的聲明語句開頭的,比如我們的例子里就是package main。這行聲明語句表示該文件是屬於哪一個package,緊跟着是一繫列import的package名,表示這個文件中引入的package。再之後是本文件本身的代碼。
|
||||
|
||||
Go的標準庫已經提供了100多個package,用來完成一門程序語言的一些常見的基本任務,比如輸入、輸齣、排序或者字符串/文本處理。比如fmt這個package,就包括接收輸入、格式化輸齣的各種函數。Println是其中的一個常用的函數,可以用這個函數來打印一個或多個值,該函數會將這些參數用空格隔開進行輸齣,併在輸齣完畢之後在行末加上一個換行符。
|
||||
Go的標準庫已經提供了100多個package,用來完成一門程序語言的一些常見的基本任務,比如輸入、輸出、排序或者字符串/文本處理。比如fmt這個package,就包括接收輸入、格式化輸出的各種函數。Println是其中的一個常用的函數,可以用這個函數來打印一個或多個值,該函數會將這些參數用空格隔開進行輸出,併在輸出完畢之後在行末加上一個換行符。
|
||||
|
||||
package main是一個比較特殊的package。這個package里會定義一個獨立的程序,這個程序是可以運行的,而不是像其它package一樣對應一個library。在main這個package里,main函數也是一個特殊的函數,這是我們整個程序的入口(譯註:其實C繫語言差不多都是這樣)。main函數所做的事情就是我們程序做的事情。當然了,main函數一般是通過是調用其它packge里的函數來完成自己的工作,比如fmt.Println。
|
||||
|
||||
|
@ -1,6 +1,6 @@
|
||||
## 1.2. 命令行參數
|
||||
|
||||
大多數的程序都是處理輸入,産生輸齣;這也正是“計算”的定義。但是一個程序要如何穫取輸入呢?一些程序會生成自己的數據,但通常情況下,輸入都來自於程序外部:比如文件、網絡連接、其它程序的輸齣、用戶的鍵盤、命令行的參數或其它類似輸入源。下面幾個例子會討論其中的一些輸入類型,首先是命令行參數。
|
||||
大多數的程序都是處理輸入,産生輸出;這也正是“計算”的定義。但是一個程序要如何穫取輸入呢?一些程序會生成自己的數據,但通常情況下,輸入都來自於程序外部:比如文件、網絡連接、其它程序的輸出、用戶的鍵盤、命令行的參數或其它類似輸入源。下面幾個例子會討論其中的一些輸入類型,首先是命令行參數。
|
||||
|
||||
os這個package提供了操作繫統無關(跨平颱)的,與繫統交互的一些函數和相關的變量,運行時程序的命令行參數可以通過os包中一個叫Args的這個變量來穫取;當在os包外部使用該變量時,需要用os.Args來訪問。
|
||||
|
||||
@ -8,7 +8,7 @@ os.Args這個變量是一個字符串(string)的slice(譯註:slice和Pyt
|
||||
|
||||
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也沒有什麽問題,隻是一般爲了方便我們都會像下面這樣來導入多個package。我們自己寫的導入順序併不重要,因爲gofmt工具會幫助我們按照字母順序來排列好這些導入包名。(本書中如果一個例子有多種版本時,我們會用編號標記齣來)
|
||||
下面是一個Unix里echo命令的實現,這個命令會在單行內打印出命令行參數。這個程序import了兩個package,併且用括號把這兩個package包了起來,這是分别import各個package聲明的簡化寫法。當然了你分開來寫import也沒有什麽問題,隻是一般爲了方便我們都會像下面這樣來導入多個package。我們自己寫的導入順序併不重要,因爲gofmt工具會幫助我們按照字母順序來排列好這些導入包名。(本書中如果一個例子有多種版本時,我們會用編號標記出來)
|
||||
|
||||
```go
|
||||
gopl.io/ch1/echo1
|
||||
@ -56,7 +56,7 @@ s = s + sep + os.Args[i]
|
||||
|
||||
運算符+=是一個賦值運算符(assignment operator),每一種數值和邏輯運算符,例如*或者+都有其對應的賦值運算符。
|
||||
|
||||
echo程序可以每循環一次輸齣一個參數,不過我們這里的版本是不斷地將其結果連接到一個字符串的末尾。s這個字符串在聲明的時候是一個空字符串,而之後循環每次都會被在末尾添加一段字符串;第一次迭代之後,一個空格會被插入到字符串末尾,所以每插入一個新值,都會和前一個中間有一個空格隔開。這是一種非線性的操作,當我們的參數數量變得龐大的時候(當然不是説這里的echo,一般echo也不會有太多參數)其運行開銷也會變得龐大。下面我們會介紹一繫列的echo改進版,來應對這里説到的運行效率低下。
|
||||
echo程序可以每循環一次輸出一個參數,不過我們這里的版本是不斷地將其結果連接到一個字符串的末尾。s這個字符串在聲明的時候是一個空字符串,而之後循環每次都會被在末尾添加一段字符串;第一次迭代之後,一個空格會被插入到字符串末尾,所以每插入一個新值,都會和前一個中間有一個空格隔開。這是一種非線性的操作,當我們的參數數量變得龐大的時候(當然不是説這里的echo,一般echo也不會有太多參數)其運行開銷也會變得龐大。下面我們會介紹一繫列的echo改進版,來應對這里説到的運行效率低下。
|
||||
|
||||
在for循環中,我們用到了i來做下標索引,可以看到我們用了:=符號來給i進行初始化和賦值,這是var xxx=yyy的一種簡寫形式,Go語言會根據等號右邊的值的類型自動判斷左邊的值類型,下一章會對這一點進行詳細説明。
|
||||
|
||||
@ -141,17 +141,17 @@ func main() {
|
||||
}
|
||||
```
|
||||
|
||||
最後,如果我們對輸齣的格式也不是很關心,隻是想簡單地輸齣值得的話,還可以像下面這麽寫,Println函數會爲我們自動格式化輸齣。
|
||||
最後,如果我們對輸出的格式也不是很關心,隻是想簡單地輸出值得的話,還可以像下面這麽寫,Println函數會爲我們自動格式化輸出。
|
||||
|
||||
```go
|
||||
fmt.Println(os.Args[1:])
|
||||
```
|
||||
|
||||
這個輸齣結果和前面的string.Join得到的結果很相似,隻是被自動地放到了一個方括號里,對slice調用Println函數都會被打印成這樣形式的結果。
|
||||
這個輸出結果和前面的string.Join得到的結果很相似,隻是被自動地放到了一個方括號里,對slice調用Println函數都會被打印成這樣形式的結果。
|
||||
|
||||
**練習 1.1:** 脩改echo程序,使其能夠打印os.Args[0]。
|
||||
|
||||
**練習 1.2:** 脩改echo程序,使其打印value和index,每個value和index顯示一行。
|
||||
|
||||
**練習 1.3:** 上手實踐前面提到的strings.Join和直接Println,併觀察輸齣結果的區别。
|
||||
**練習 1.3:** 上手實踐前面提到的strings.Join和直接Println,併觀察輸出結果的區别。
|
||||
|
||||
|
@ -1,8 +1,8 @@
|
||||
## 1.3. 査找重複的行
|
||||
|
||||
文件拷貝、文件打印、文件蒐索、文件排序、文件統計類的程序一般都會有比較相似的程序結構:一個處理輸入的循環,在每一個輸入元素上執行計算處理,在處理的同時或者處理完成之後進行結果輸齣。我們會展示一個叫dup程序的三個版本;這個程序的靈感來自於linux的uniq命令,我們的程序將會找到相鄰的重複的行。這個程序提供的模式可以很方便地被脩改來完成不同的需求。
|
||||
文件拷貝、文件打印、文件蒐索、文件排序、文件統計類的程序一般都會有比較相似的程序結構:一個處理輸入的循環,在每一個輸入元素上執行計算處理,在處理的同時或者處理完成之後進行結果輸出。我們會展示一個叫dup程序的三個版本;這個程序的靈感來自於linux的uniq命令,我們的程序將會找到相鄰的重複的行。這個程序提供的模式可以很方便地被脩改來完成不同的需求。
|
||||
|
||||
第一個版本的dup會輸齣標準輸入流中的齣現多次的行,在行內容前會有其齣現次數的計數。這個程序將引入if表達式,map內置數據結構和bufio的package。
|
||||
第一個版本的dup會輸出標準輸入流中的出現多次的行,在行內容前會有其出現次數的計數。這個程序將引入if表達式,map內置數據結構和bufio的package。
|
||||
|
||||
```go
|
||||
gopl.io/ch1/dup1
|
||||
@ -47,7 +47,7 @@ counts[line] = counts[line] + 1
|
||||
|
||||
在這里我們又用了一個range的循環來打印結果,這次range是被用在map這個數據結構之上。這一次的情況和上次比較類似,range會返迴兩個值,一個key和在map對應這個key的value。對map進行range循環時,其迭代順序是不確定的,從實踐來看,很可能每次運行都會有不一樣的結果(譯註:這是Go語言的設計者有意爲之的,因爲其底層實現不保證插入順序和遍歷順序一致,也希望程序員不要依賴遍歷時的順序,所以榦脆直接在遍歷的時候做了隨機化處理,醉了。補充:好像説隨機序可以防止某種類型的攻擊,雖然不太明白,但是感覺還蠻厲害的),來避免程序員在業務中依賴遍歷時的順序。
|
||||
|
||||
然後輪到我們例子中的bufio這個package了,這個package主要的目的是幫助我們更方便有效地處理程序的輸入和輸齣。而這個包最有用的一個特性就是其中的一個Scanner類型,用它可以簡單地接收輸入,或者把輸入打散成行或者單詞;這個類型通常是處理行形式的輸入最簡單的方法了。
|
||||
然後輪到我們例子中的bufio這個package了,這個package主要的目的是幫助我們更方便有效地處理程序的輸入和輸出。而這個包最有用的一個特性就是其中的一個Scanner類型,用它可以簡單地接收輸入,或者把輸入打散成行或者單詞;這個類型通常是處理行形式的輸入最簡單的方法了。
|
||||
|
||||
本程序中用了一個短變量聲明,來創建一個buffio.Scanner對象:
|
||||
|
||||
@ -57,9 +57,9 @@ input := bufio.NewScanner(os.Stdin)
|
||||
|
||||
scanner對象可以從程序的標準輸入中讀取內容。對input.Scanner的每一次調用都會調入一個新行,併且會自動將其行末的換行符去掉;其結果可以用input.Text()得到。Scan方法在讀到了新行的時候會返迴true,而在沒有新行被讀入時,會返迴false。
|
||||
|
||||
例子中還有一個fmt.Printf,這個函數和C繫的其它語言里的那個printf函數差不多,都是格式化輸齣的方法。fmt.Printf的第一個參數卽是輸齣內容的格式規約,每一個參數如何格式化是取決於在格式化字符串里齣現的“轉換字符”,這個字符串是跟着%號後的一個字母。比如%d表示以一個整數的形式來打印一個變量,而%s,則表示以string形式來打印一個變量。
|
||||
例子中還有一個fmt.Printf,這個函數和C繫的其它語言里的那個printf函數差不多,都是格式化輸出的方法。fmt.Printf的第一個參數卽是輸出內容的格式規約,每一個參數如何格式化是取決於在格式化字符串里出現的“轉換字符”,這個字符串是跟着%號後的一個字母。比如%d表示以一個整數的形式來打印一個變量,而%s,則表示以string形式來打印一個變量。
|
||||
|
||||
Printf有一大堆這種轉換,Go語言程序員把這些叫做verb(動詞)。下面的表格列齣了常用的動詞,當然了不是全部,但基本也夠用了。
|
||||
Printf有一大堆這種轉換,Go語言程序員把這些叫做verb(動詞)。下面的表格列出了常用的動詞,當然了不是全部,但基本也夠用了。
|
||||
|
||||
```
|
||||
%d int變量
|
||||
@ -69,12 +69,12 @@ Printf有一大堆這種轉換,Go語言程序員把這些叫做verb(動詞
|
||||
%c rune (Unicode碼點),Go語言里特有的Unicode字符類型
|
||||
%s string
|
||||
%q 帶雙引號的字符串 "abc" 或 帶單引號的 rune 'c'
|
||||
%v 會將任意變量以易讀的形式打印齣來
|
||||
%v 會將任意變量以易讀的形式打印出來
|
||||
%T 打印變量的類型
|
||||
%% 字符型百分比標誌(%符號本身,沒有其他操作)
|
||||
```
|
||||
|
||||
dup1中的程序還包含了一個\t和\n的格式化字符串。在字符串中會以這些特殊的轉義字符來表示不可見字符。Printf默認不會在輸齣內容後加上換行符。按照慣例,用來格式化的函數都會在末尾以f字母結尾(譯註:f後綴對應format或fmt縮寫),比如log.Printf,fmt.Errorf,同時還有一繫列對應以ln結尾的函數(譯註:ln後綴對應line縮寫),這些函數默認以%v來格式化他們的參數,併且會在輸齣結束後在最後自動加上一個換行符。
|
||||
dup1中的程序還包含了一個\t和\n的格式化字符串。在字符串中會以這些特殊的轉義字符來表示不可見字符。Printf默認不會在輸出內容後加上換行符。按照慣例,用來格式化的函數都會在末尾以f字母結尾(譯註:f後綴對應format或fmt縮寫),比如log.Printf,fmt.Errorf,同時還有一繫列對應以ln結尾的函數(譯註:ln後綴對應line縮寫),這些函數默認以%v來格式化他們的參數,併且會在輸出結束後在最後自動加上一個換行符。
|
||||
|
||||
許多程序從標準輸入中讀取數據,像上面的例子那樣。除此之外,還可能從一繫列的文件中讀取。下一個dup程序就是從標準輸入中讀到一些文件名,用os.Open函數來打開每一個文件穫取內容的。
|
||||
|
||||
@ -124,7 +124,7 @@ func countLines(f *os.File, counts map[string]int) {
|
||||
|
||||
os.Open函數會返迴兩個值。第一個值是一個打開的文件類型(*os.File),這個對象在下面的程序中被Scanner讀取。
|
||||
|
||||
os.Open返迴的第二個值是一個Go語言內置的error類型。如果這個error和內置值的nil(譯註:相當於其它語言里的NULL)相等的話,説明文件被成功的打開了。之後文件被讀取,一直到文件的最後,文件的Close方法關閉該文件,併釋放相應的占用一切資源。另一方面,如果err的值不是nil的話,那説明在打開文件的時候齣了某種錯誤。這種情況下,error類型的值會描述具體的問題。我們例子里的簡單錯誤處理會在標準錯誤流中用Fprintf和%v來格式化該錯誤字符串。然後繼續處理下一個文件;continue語句會直接跳過之後的語句,直接開始執行下一個循環迭代。
|
||||
os.Open返迴的第二個值是一個Go語言內置的error類型。如果這個error和內置值的nil(譯註:相當於其它語言里的NULL)相等的話,説明文件被成功的打開了。之後文件被讀取,一直到文件的最後,文件的Close方法關閉該文件,併釋放相應的占用一切資源。另一方面,如果err的值不是nil的話,那説明在打開文件的時候出了某種錯誤。這種情況下,error類型的值會描述具體的問題。我們例子里的簡單錯誤處理會在標準錯誤流中用Fprintf和%v來格式化該錯誤字符串。然後繼續處理下一個文件;continue語句會直接跳過之後的語句,直接開始執行下一個循環迭代。
|
||||
|
||||
我們在本書中早期的例子中做了比較詳盡的錯誤處理,當然了,在實際編碼過程中,像os.Open這類的函數是一定要檢査其返迴的error值的;爲了減少例子程序的代碼量,我們姑且簡化掉這些不太可能返迴錯誤的處理邏輯。後面的例子里我們會跳過錯誤檢査。在5.4節中我們會對錯誤處理做更詳細的闡述。
|
||||
|
||||
@ -167,9 +167,9 @@ func main() {
|
||||
}
|
||||
```
|
||||
|
||||
ReadFile函數返迴一個byte的slice,這個slice必鬚被轉換爲string,之後纔能夠用string.Split方法來進行處理。我們在3.5.4節中會更詳細地講解string和byte slice(字節數組)。
|
||||
ReadFile函數返迴一個byte的slice,這個slice必鬚被轉換爲string,之後才能夠用string.Split方法來進行處理。我們在3.5.4節中會更詳細地講解string和byte slice(字節數組)。
|
||||
|
||||
在更底層一些的地方,bufio.Scanner,ioutil.ReadFile和ioutil.WriteFile使用的是*os.File的Read和Write方法,不過一般程序員併不需要去直接了解到其底層實現細節,在bufio和io/ioutil包中提供的方法已經足夠好用。
|
||||
|
||||
**練習 1.4:** 脩改dup2,使其可以打印重複的行分别齣現在哪些文件。
|
||||
**練習 1.4:** 脩改dup2,使其可以打印重複的行分别出現在哪些文件。
|
||||
|
||||
|
@ -1,10 +1,10 @@
|
||||
## 1.4. GIF動畵
|
||||
|
||||
下面的程序會演示Go語言標準庫里的image這個package的用法,我們會用這個包來生成一繫列的bit-mapped圖,然後將這些圖片編碼爲一個GIF動畵。我們生成的圖形名字叫利薩如圖形(Lissajous figures),這種效果是在1960年代的老電影里齣現的一種視覺特效。它們是協振子在兩個緯度上振動所産生的麴線,比如兩個sin正絃波分别在x軸和y軸輸入會産生的麴線。圖1.1是這樣的一個例子:
|
||||
下面的程序會演示Go語言標準庫里的image這個package的用法,我們會用這個包來生成一繫列的bit-mapped圖,然後將這些圖片編碼爲一個GIF動畵。我們生成的圖形名字叫利薩如圖形(Lissajous figures),這種效果是在1960年代的老電影里出現的一種視覺特效。它們是協振子在兩個緯度上振動所産生的麴線,比如兩個sin正絃波分别在x軸和y軸輸入會産生的麴線。圖1.1是這樣的一個例子:
|
||||
|
||||
![](../images/ch1-01.png)
|
||||
|
||||
譯註:要看這個程序的結果,需要將標準輸齣重定向到一個GIF圖像文件(使用 `./lissajous > output.gif` 命令)。下面是GIF圖像動畵效果:
|
||||
譯註:要看這個程序的結果,需要將標準輸出重定向到一個GIF圖像文件(使用 `./lissajous > output.gif` 命令)。下面是GIF圖像動畵效果:
|
||||
|
||||
![](../images/ch1-01.gif)
|
||||
|
||||
@ -69,17 +69,17 @@ bla kIndex)
|
||||
|
||||
當我們import了一個包路徑包含有多個單詞的package時,比如image/color(image和color兩個單詞),通常我們隻需要用最後那個單詞表示這個包就可以。所以當我們寫color.White時,這個變量指向的是image/color包里的變量,同理gif.GIF是屬於image/gif包里的變量。
|
||||
|
||||
這個程序里的常量聲明給齣了一繫列的常量值,常量是指在程序編譯後運行時始終都不會變化的值,比如圈數、幀數、延遲值。常量聲明和變量聲明一般都會齣現在包級别,所以這些常量在整個包中都是可以共享的,或者你也可以把常量聲明定義在函數體內部,那麽這種常量就隻能在函數體內用。目前常量聲明的值必鬚是一個數字值、字符串或者一個固定的boolean值。
|
||||
這個程序里的常量聲明給出了一繫列的常量值,常量是指在程序編譯後運行時始終都不會變化的值,比如圈數、幀數、延遲值。常量聲明和變量聲明一般都會出現在包級别,所以這些常量在整個包中都是可以共享的,或者你也可以把常量聲明定義在函數體內部,那麽這種常量就隻能在函數體內用。目前常量聲明的值必鬚是一個數字值、字符串或者一個固定的boolean值。
|
||||
|
||||
[]color.Color{...}和gif.GIF{...}這兩個表達式就是我們説的複合聲明(4.2和4.4.1節有説明)。這是實例化Go語言里的複合類型的一種寫法。這里的前者生成的是一個slice切片,後者生成的是一個struct結構體。
|
||||
|
||||
gif.GIF是一個struct類型(參考4.4節)。struct是一組值或者叫字段的集合,不同的類型集合在一個struct可以讓我們以一個統一的單元進行處理。anim是一個gif.GIF類型的struct變量。這種寫法會生成一個struct變量,併且其內部變量LoopCount字段會被設置爲nframes;而其它的字段會被設置爲各自類型默認的零值。struct內部的變量可以以一個點(.)來進行訪問,就像在最後兩個賦值語句中顯式地更新了anim這個struct的Delay和Image字段。
|
||||
|
||||
lissajous函數內部有兩層嵌套的for循環。外層循環會循環64次,每一次都會生成一個單獨的動畵幀。它生成了一個包含兩種顔色的201&201大小的圖片,白色和黑色。所有像素點都會被默認設置爲其零值(也就是palette里的第0個值),這里我們設置的是白色。每次經過內存循環都會通過設置像素爲黑色,生成一張新圖片。其結果會append到之前結果之後。這里我們用到了append(參考4.2.1)這個內置函數,將結果appen到anim中的幀列表末尾,併會設置一個默認的80ms的延遲值。最終循環結束,所有的延遲值也被編碼進了GIF圖片中,併將結果寫入到輸齣流。out這個變量是io.Writer類型,這個類型讓我們可以可以讓我們把輸齣結果寫到很多目標,很快我們就可以看到了。
|
||||
lissajous函數內部有兩層嵌套的for循環。外層循環會循環64次,每一次都會生成一個單獨的動畵幀。它生成了一個包含兩種顔色的201&201大小的圖片,白色和黑色。所有像素點都會被默認設置爲其零值(也就是palette里的第0個值),這里我們設置的是白色。每次經過內存循環都會通過設置像素爲黑色,生成一張新圖片。其結果會append到之前結果之後。這里我們用到了append(參考4.2.1)這個內置函數,將結果appen到anim中的幀列表末尾,併會設置一個默認的80ms的延遲值。最終循環結束,所有的延遲值也被編碼進了GIF圖片中,併將結果寫入到輸出流。out這個變量是io.Writer類型,這個類型讓我們可以可以讓我們把輸出結果寫到很多目標,很快我們就可以看到了。
|
||||
|
||||
內存循環設置了兩個偏振。x軸偏振使用的是一個sin函數。y軸偏振也是一個正絃波,但是其其相對x軸的偏振是一個0-3的隨機值,併且初始偏振值是一個零值,併隨着動畵的每一幀逐漸增加。循環會一直跑到x軸完成五次完整的循環。每一步它都會調用SetColorIndex來爲(x, y)點來染黑色。
|
||||
|
||||
main函數調用了lissajous函數,併且用它來向標準輸齣中打印信息,所以下面這個命令會像圖1.1中産生一個GIF動畵。
|
||||
main函數調用了lissajous函數,併且用它來向標準輸出中打印信息,所以下面這個命令會像圖1.1中産生一個GIF動畵。
|
||||
|
||||
```
|
||||
$ go build gopl.io/ch1/lissajous
|
||||
|
@ -2,7 +2,7 @@
|
||||
|
||||
對於很多現代應用來説,訪問互聯網上的信息和訪問本地文件繫統一樣重要。Go語言在net這個強大package的幫助下提供了一繫列的package來做這件事情,使用這些包可以更簡單地用網絡收發信息,還可以建立更底層的網絡連接,編寫服務器程序。在這些情景下,Go語言原生的併發特性(在第八章中會介紹)就顯得尤其好用了。
|
||||
|
||||
爲了最簡單地展示基於HTTP穫取信息的方式,下面給齣一個示例程序fetch,這個程序將穫取對應的url,併將其源文本打印齣來;這個例子的靈感來源於curl工具(譯註:unix下的一個網絡相關的工具)。當然了,curl提供的功能更爲複雜豐富,這里我們隻編寫最簡單的樣例。之後我們還會在本書中經常用到這個例子。
|
||||
爲了最簡單地展示基於HTTP穫取信息的方式,下面給出一個示例程序fetch,這個程序將穫取對應的url,併將其源文本打印出來;這個例子的靈感來源於curl工具(譯註:unix下的一個網絡相關的工具)。當然了,curl提供的功能更爲複雜豐富,這里我們隻編寫最簡單的樣例。之後我們還會在本書中經常用到這個例子。
|
||||
|
||||
```go
|
||||
gopl.io/ch1/fetch
|
||||
@ -34,7 +34,7 @@ func main() {
|
||||
}
|
||||
```
|
||||
|
||||
這個程序從兩個package中導入了函數,net/http和io/ioutil包,http.Get函數是創建HTTP請求的函數,如果穫取過程沒有齣錯,那麽會在resp這個結構體中得到訪問的請求結果。resp的Body字段包括一個可讀的服務器響應流。這之後ioutil.ReadAll函數從response中讀取到全部內容;其結果保存在變量b中。resp.Body.Close這一句會關閉resp的Body流,防止資源洩露,Printf函數會將結果b寫齣到標準輸齣流中。
|
||||
這個程序從兩個package中導入了函數,net/http和io/ioutil包,http.Get函數是創建HTTP請求的函數,如果穫取過程沒有出錯,那麽會在resp這個結構體中得到訪問的請求結果。resp的Body字段包括一個可讀的服務器響應流。這之後ioutil.ReadAll函數從response中讀取到全部內容;其結果保存在變量b中。resp.Body.Close這一句會關閉resp的Body流,防止資源洩露,Printf函數會將結果b寫出到標準輸出流中。
|
||||
|
||||
```
|
||||
$ go build gopl.io/ch1/fetch
|
||||
@ -65,5 +65,5 @@ fetch: Get http://gopl.io: dial tcp: lookup gopl.io: getaddrinfow: No such host
|
||||
|
||||
**練習 1.8:** 脩改fetch這個范例,如果輸入的url參數沒有 `http://` 前綴的話,爲這個url加上該前綴。你可能會用到strings.HasPrefix這個函數。
|
||||
|
||||
**練習 1.9:** 脩改fetch打印齣HTTP協議的狀態碼,可以從resp.Status變量得到該狀態碼。
|
||||
**練習 1.9:** 脩改fetch打印出HTTP協議的狀態碼,可以從resp.Status變量得到該狀態碼。
|
||||
|
||||
|
@ -2,7 +2,7 @@
|
||||
|
||||
Go語言最有意思併且最新奇的特性就是其對併發編程的支持了。併發編程是一個大話題,在第八章和第九章中會專門講到。這里我們隻淺嚐輒止地來體驗一下Go語言里的goroutine和channel。
|
||||
|
||||
下面的例子fetchall,和上面的fetch程序所要做的工作是一致的,但是這個fetchall的特别之處在於它會同時去穫取所有的URL,所以這個程序的穫取時間不會超過執行時間最長的那一個任務,而不會像前面的fetch程序一樣,執行時間是所有任務執行時間之和。這次的fetchall程序隻會打印穫取的內容大小和經過的時間,不會像上面那樣打印齣穫取的內容。
|
||||
下面的例子fetchall,和上面的fetch程序所要做的工作是一致的,但是這個fetchall的特别之處在於它會同時去穫取所有的URL,所以這個程序的穫取時間不會超過執行時間最長的那一個任務,而不會像前面的fetch程序一樣,執行時間是所有任務執行時間之和。這次的fetchall程序隻會打印穫取的內容大小和經過的時間,不會像上面那樣打印出穫取的內容。
|
||||
|
||||
```go
|
||||
gopl.io/ch1/fetchall
|
||||
@ -61,8 +61,8 @@ $ ./fetchall https://golang.org http://gopl.io https://godoc.org
|
||||
|
||||
goroutine是一種函數的併發執行方式,而channel是用來在goroutine之間進行參數傳遞。main函數也是運行在一個goroutine中,而go function則表示創建一個新的goroutine,併在這個這個新的goroutine里執行這個函數。
|
||||
|
||||
main函數中用make函數創建了一個傳遞string類型參數的channel,對每一個命令行參數,我們都用go這個關鍵字來創建一個goroutine,併且讓函數在這個goroutine異步執行http.Get方法。這個程序里的io.Copy會把響應的Body內容拷貝到ioutil.Discard輸齣流中(譯註:這是一個垃圾桶,可以向里面寫一些不需要的數據),因爲我們需要這個方法返迴的字節數,但是又不想要其內容。每當請求返迴內容時,fetch函數都會往ch這個channel里寫入一個字符串,由main函數里的第二個for循環來處理併打印channel里的這個字符串。
|
||||
main函數中用make函數創建了一個傳遞string類型參數的channel,對每一個命令行參數,我們都用go這個關鍵字來創建一個goroutine,併且讓函數在這個goroutine異步執行http.Get方法。這個程序里的io.Copy會把響應的Body內容拷貝到ioutil.Discard輸出流中(譯註:這是一個垃圾桶,可以向里面寫一些不需要的數據),因爲我們需要這個方法返迴的字節數,但是又不想要其內容。每當請求返迴內容時,fetch函數都會往ch這個channel里寫入一個字符串,由main函數里的第二個for循環來處理併打印channel里的這個字符串。
|
||||
|
||||
當一個goroutine嚐試在一個channel上做send或者receive操作時,這個goroutine會阻塞在調用處,直到另一個goroutine往這個channel里寫入、或者接收了值,這樣兩個goroutine纔會繼續執行操作channel完成之後的邏輯。在這個例子中,每一個fetch函數在執行時都會往channel里發送一個值(ch <- expression),主函數接收這些值(<-ch)。這個程序中我們用main函數來所有fetch函數傳迴的字符串,可以避免在goroutine異步執行時同時結束。
|
||||
當一個goroutine嚐試在一個channel上做send或者receive操作時,這個goroutine會阻塞在調用處,直到另一個goroutine往這個channel里寫入、或者接收了值,這樣兩個goroutine才會繼續執行操作channel完成之後的邏輯。在這個例子中,每一個fetch函數在執行時都會往channel里發送一個值(ch <- expression),主函數接收這些值(<-ch)。這個程序中我們用main函數來所有fetch函數傳迴的字符串,可以避免在goroutine異步執行時同時結束。
|
||||
|
||||
**練習 1.10:** 找一個數據量比較大的網站,用本小節中的程序調研網站的緩存策略,對每個URL執行兩遍請求,査看兩次時間是否有較大的差别,併且每次穫取到的響應內容是否一致,脩改本節中的程序,將響應結果輸齣,以便於進行對比。
|
||||
**練習 1.10:** 找一個數據量比較大的網站,用本小節中的程序調研網站的緩存策略,對每個URL執行兩遍請求,査看兩次時間是否有較大的差别,併且每次穫取到的響應內容是否一致,脩改本節中的程序,將響應結果輸出,以便於進行對比。
|
||||
|
@ -24,7 +24,7 @@ func handler(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
```
|
||||
|
||||
我們隻用了八九行代碼就實現了一個個Web服務程序,這都是多虧了標準庫里的方法已經幫我們處理了大量的工作。main函數會將所有發送到/路徑下的請求和handler函數關聯起來,/開頭的請求其實就是所有發送到當前站點上的請求,我們的服務跑在了8000端口上。發送到這個服務的“請求”是一個http.Request類型的對象,這個對象中包含了請求中的一繫列相關字段,其中就包括我們需要的URL。當請求到達服務器時,這個請求會被傳給handler函數來處理,這個函數會將/hello這個路徑從請求的URL中解析齣來,然後把其發送到響應中,這里我們用的是標準輸齣流的fmt.Fprintf。Web服務會在第7.7節中詳細闡述。
|
||||
我們隻用了八九行代碼就實現了一個個Web服務程序,這都是多虧了標準庫里的方法已經幫我們處理了大量的工作。main函數會將所有發送到/路徑下的請求和handler函數關聯起來,/開頭的請求其實就是所有發送到當前站點上的請求,我們的服務跑在了8000端口上。發送到這個服務的“請求”是一個http.Request類型的對象,這個對象中包含了請求中的一繫列相關字段,其中就包括我們需要的URL。當請求到達服務器時,這個請求會被傳給handler函數來處理,這個函數會將/hello這個路徑從請求的URL中解析出來,然後把其發送到響應中,這里我們用的是標準輸出流的fmt.Fprintf。Web服務會在第7.7節中詳細闡述。
|
||||
|
||||
讓我們在後颱運行這個服務程序。如果你的操作繫統是Mac OS X或者Linux,那麽在運行命令的末尾加上一個&符號,卽可讓程序簡單地跑在後颱,而在windows下,你需要在另外一個命令行窗口去運行這個程序了。
|
||||
|
||||
@ -46,7 +46,7 @@ URL.Path = "/help"
|
||||
|
||||
![](../images/ch1-02.png)
|
||||
|
||||
在這個服務的基礎上疊加特性是很容易的。一種比較實用的脩改是爲訪問的url添加某種狀態。比如,下面這個版本輸齣了同樣的內容,但是會對請求的次數進行計算;對URL的請求結果會包含各種URL被訪問的總次數,直接對/count這個URL的訪問要除外。
|
||||
在這個服務的基礎上疊加特性是很容易的。一種比較實用的脩改是爲訪問的url添加某種狀態。比如,下面這個版本輸出了同樣的內容,但是會對請求的次數進行計算;對URL的請求結果會包含各種URL被訪問的總次數,直接對/count這個URL的訪問要除外。
|
||||
|
||||
```go
|
||||
gopl.io/ch1/server2
|
||||
@ -87,7 +87,7 @@ func counter(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
這個服務器有兩個請求處理函數,請求的url會決定具體調用哪一個:對/count這個url的請求會調用到count這個函數,其它所有的url都會調用默認的處理函數。如果你的請求pattern是以/結尾,那麽所有以該url爲前綴的url都會被這條規則匹配。在這些代碼的背後,服務器每一次接收請求處理時都會另起一個goroutine,這樣服務器就可以同一時間處理多數請求。然而在併發情況下,假如眞的有兩個請求同一時刻去更新count,那麽這個值可能併不會被正確地增加;這個程序可能會被引發一個嚴重的bug:競態條件(參見9.1)。爲了避免這個問題,我們必鬚保證每次脩改變量的最多隻能有一個goroutine,這也就是代碼里的mu.Lock()和mu.Unlock()調用將脩改count的所有行爲包在中間的目的。第九章中我們會進一步講解共享變量。
|
||||
|
||||
下面是一個更爲豐富的例子,handler函數會把請求的http頭和請求的form數據都打印齣來,這樣可以讓檢査和調試這個服務更爲方便:
|
||||
下面是一個更爲豐富的例子,handler函數會把請求的http頭和請求的form數據都打印出來,這樣可以讓檢査和調試這個服務更爲方便:
|
||||
|
||||
```go
|
||||
gopl.io/ch1/server3
|
||||
@ -108,7 +108,7 @@ func handler(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
```
|
||||
|
||||
我們用http.Request這個struct里的字段來輸齣下面這樣的內容:
|
||||
我們用http.Request這個struct里的字段來輸出下面這樣的內容:
|
||||
|
||||
```
|
||||
GET /?q=query HTTP/1.1
|
||||
@ -119,7 +119,7 @@ RemoteAddr = "127.0.0.1:59911"
|
||||
Form["q"] = ["query"]
|
||||
```
|
||||
|
||||
可以看到這里的ParseForm被嵌套在了if語句中。Go語言允許這樣的一個簡單的語句結果作爲循環的變量聲明齣現在if語句的最前面,這一點對錯誤處理很有用處。我們還可以像下面這樣寫(當然看起來就長了一些):
|
||||
可以看到這里的ParseForm被嵌套在了if語句中。Go語言允許這樣的一個簡單的語句結果作爲循環的變量聲明出現在if語句的最前面,這一點對錯誤處理很有用處。我們還可以像下面這樣寫(當然看起來就長了一些):
|
||||
|
||||
```go
|
||||
err := r.ParseForm()
|
||||
@ -130,11 +130,11 @@ if err != nil {
|
||||
|
||||
用if和ParseForm結合可以讓代碼更加簡單,併且可以限製err這個變量的作用域,這麽做是很不錯的。我們會在2.7節中講解作用域。
|
||||
|
||||
在這些程序中,我們看到了很多不同的類型被輸齣到標準輸齣流中。比如前面的fetch程序,就把HTTP的響應數據拷貝到了os.Stdout,或者在lissajous程序里我們輸齣的是一個文件。fetchall程序則完全忽略到了HTTP的響應體,隻是計算了一下響應體的大小,這個程序中把響應體拷貝到了ioutil.Discard。在本節的web服務器程序中則是用fmt.Fprintf直接寫到了http.ResponseWriter中。
|
||||
在這些程序中,我們看到了很多不同的類型被輸出到標準輸出流中。比如前面的fetch程序,就把HTTP的響應數據拷貝到了os.Stdout,或者在lissajous程序里我們輸出的是一個文件。fetchall程序則完全忽略到了HTTP的響應體,隻是計算了一下響應體的大小,這個程序中把響應體拷貝到了ioutil.Discard。在本節的web服務器程序中則是用fmt.Fprintf直接寫到了http.ResponseWriter中。
|
||||
|
||||
盡管這三種具體的實現流程併不太一樣,他們都實現一個共同的接口,卽當它們被調用需要一個標準流輸齣時都可以滿足。這個接口叫作io.Writer,在7.1節中會詳細討論。
|
||||
盡管這三種具體的實現流程併不太一樣,他們都實現一個共同的接口,卽當它們被調用需要一個標準流輸出時都可以滿足。這個接口叫作io.Writer,在7.1節中會詳細討論。
|
||||
|
||||
Go語言的接口機製會在第7章中講解,爲了在這里簡單説明接口能做什麽,讓我們簡單地將這里的web服務器和之前寫的lissajous函數結合起來,這樣GIF動畵可以被寫到HTTP的客戶端,而不是之前的標準輸齣流。隻要在web服務器的代碼里加入下面這幾行。
|
||||
Go語言的接口機製會在第7章中講解,爲了在這里簡單説明接口能做什麽,讓我們簡單地將這里的web服務器和之前寫的lissajous函數結合起來,這樣GIF動畵可以被寫到HTTP的客戶端,而不是之前的標準輸出流。隻要在web服務器的代碼里加入下面這幾行。
|
||||
|
||||
```Go
|
||||
handler := func(w http.ResponseWriter, r *http.Request) {
|
||||
|
@ -15,7 +15,7 @@ default:
|
||||
}
|
||||
```
|
||||
|
||||
在翻轉硬幣的時候,例子里的coinflip函數返迴幾種不同的結果,每一個case都會對應個返迴結果,這里需要註意,Go語言併不需要顯式地去在每一個case後寫break,語言默認執行完case後的邏輯語句會自動退齣。當然了,如果你想要相鄰的幾個case都執行同一邏輯的話,需要自己顯式地寫上一個fallthrough語句來覆蓋這種默認行爲。不過fallthrough語句在一般的編程中用到得很少。
|
||||
在翻轉硬幣的時候,例子里的coinflip函數返迴幾種不同的結果,每一個case都會對應個返迴結果,這里需要註意,Go語言併不需要顯式地去在每一個case後寫break,語言默認執行完case後的邏輯語句會自動退出。當然了,如果你想要相鄰的幾個case都執行同一邏輯的話,需要自己顯式地寫上一個fallthrough語句來覆蓋這種默認行爲。不過fallthrough語句在一般的編程中用到得很少。
|
||||
|
||||
Go語言里的switch還可以不帶操作對象(譯註:switch不帶操作對象時默認用true值代替,然後將每個case的表達式和true值進行比較);可以直接羅列多種條件,像其它語言里面的多個if else一樣,下面是一個例子:
|
||||
|
||||
|
@ -1,5 +1,5 @@
|
||||
# 第1章 入門
|
||||
|
||||
本章會介紹Go語言里的一些基本組件。我們希望用信息和例子盡快帶你入門。本章和之後章節的例子都是針對眞實的開發案例給齣。本章我們隻是簡單地爲你介紹一些Go語言的入門例子,從簡單的文件處理、圖像處理到互聯網併發客戶端和服務端程序。當然,在第一章我們不會詳盡地一一去説明細枝末節,不過用這些程序來學習一門新語言肯定是很有效的。
|
||||
本章會介紹Go語言里的一些基本組件。我們希望用信息和例子盡快帶你入門。本章和之後章節的例子都是針對眞實的開發案例給出。本章我們隻是簡單地爲你介紹一些Go語言的入門例子,從簡單的文件處理、圖像處理到互聯網併發客戶端和服務端程序。當然,在第一章我們不會詳盡地一一去説明細枝末節,不過用這些程序來學習一門新語言肯定是很有效的。
|
||||
|
||||
當你學習一門新語言時,你會用這門新語言去重寫自己以前熟悉語言例子的傾向。在學習Go語言的過程中,盡量避免這麽做。我們會向你演示如何纔能寫齣好的Go語言程序,所以請使用這里的代碼作爲你寫自己的Go程序時的指南。
|
||||
當你學習一門新語言時,你會用這門新語言去重寫自己以前熟悉語言例子的傾向。在學習Go語言的過程中,盡量避免這麽做。我們會向你演示如何才能寫出好的Go語言程序,所以請使用這里的代碼作爲你寫自己的Go程序時的指南。
|
||||
|
@ -4,8 +4,8 @@
|
||||
|
||||
每個包定義了一個不同的名稱空間用於它內部的每個標識符. 每個名稱關聯到一個特定的包, 我們最好給類型, 函數等選擇簡短清晰的名字, 這樣可以避免在我們使用它們的時候減少和其他部分名字的衝突.
|
||||
|
||||
包還通過控製包內名字的可見性和是否導齣來實現封裝特性. 通過限製包成員的可見性併隱藏包API的具體實現, 將允許包的維護者在不影響外部包用戶的前提下調整包的內部實現. 通過限製包內變量的可見性, 還可以控製用戶通過某些特定函數來訪問和更新內部變量, 這樣可以保證內部變量的一致性和併發時的互斥約束.
|
||||
包還通過控製包內名字的可見性和是否導出來實現封裝特性. 通過限製包成員的可見性併隱藏包API的具體實現, 將允許包的維護者在不影響外部包用戶的前提下調整包的內部實現. 通過限製包內變量的可見性, 還可以控製用戶通過某些特定函數來訪問和更新內部變量, 這樣可以保證內部變量的一致性和併發時的互斥約束.
|
||||
|
||||
當我們脩改了一個文件, 我們必鬚重新編譯改文件對應的包和所以依賴該包的其他包.卽使是從頭構建, Go的編譯器也明顯快於其他編譯語言. Go的編譯速度主要得益於三個特性. 第一點, 所有導入的包必鬚在每個文件的開頭顯式聲明, 這樣的話編譯器就沒有必要讀取分析整個文件來判斷包的依賴關繫. 第二點, 包的依賴關繫形成一個有向無環圖, 因爲沒有循環依賴, 每個包可以被獨立編譯, 很可能是併發編譯. 第三點, 編譯後包的目標文件不僅僅記録包本身的導齣信息, 同時還記録了它的依賴關繫. 因此, 在編譯一個包的時候, 編譯器隻需要讀取每個直接導入包的目標文件, 而不是要遍歷所有依賴的的文件(譯註: 很多可能是間接依賴).
|
||||
當我們脩改了一個文件, 我們必鬚重新編譯改文件對應的包和所以依賴該包的其他包.卽使是從頭構建, Go的編譯器也明顯快於其他編譯語言. Go的編譯速度主要得益於三個特性. 第一點, 所有導入的包必鬚在每個文件的開頭顯式聲明, 這樣的話編譯器就沒有必要讀取分析整個文件來判斷包的依賴關繫. 第二點, 包的依賴關繫形成一個有向無環圖, 因爲沒有循環依賴, 每個包可以被獨立編譯, 很可能是併發編譯. 第三點, 編譯後包的目標文件不僅僅記録包本身的導出信息, 同時還記録了它的依賴關繫. 因此, 在編譯一個包的時候, 編譯器隻需要讀取每個直接導入包的目標文件, 而不是要遍歷所有依賴的的文件(譯註: 很多可能是間接依賴).
|
||||
|
||||
|
||||
|
@ -1,7 +1,7 @@
|
||||
## 10.2. 導入路徑
|
||||
|
||||
每個包是由一個全局唯一的字符串所標識的導入路徑定位.
|
||||
齣現在導入聲明中的導入路徑也是字符串.
|
||||
出現在導入聲明中的導入路徑也是字符串.
|
||||
|
||||
```Go
|
||||
import (
|
||||
|
@ -42,7 +42,7 @@ func toJPEG(in io.Reader, out io.Writer) error {
|
||||
}
|
||||
```
|
||||
|
||||
如果我們將 `gopl.io/ch3/mandelbrot` (§3.3) 的輸齣導入到這個工具的輸入, 它將解碼輸入的PNG格式圖像, 然後轉換爲JPEG格式的圖像(圖3.3).
|
||||
如果我們將 `gopl.io/ch3/mandelbrot` (§3.3) 的輸出導入到這個工具的輸入, 它將解碼輸入的PNG格式圖像, 然後轉換爲JPEG格式的圖像(圖3.3).
|
||||
|
||||
```
|
||||
$ go build gopl.io/ch3/mandelbrot
|
||||
@ -89,7 +89,7 @@ db, err = sql.Open("mysql", dbname) // OK
|
||||
db, err = sql.Open("sqlite3", dbname) // returns error: unknown driver "sqlite3"
|
||||
```
|
||||
|
||||
**練習 10.1:** 擴展 jpeg 程序, 支持任意圖像格式之間的相互轉換, 使用 image.Decode 檢測支持的格式類型, 然後同步 flag 命令行標誌參數選擇輸齣的格式.
|
||||
**練習 10.1:** 擴展 jpeg 程序, 支持任意圖像格式之間的相互轉換, 使用 image.Decode 檢測支持的格式類型, 然後同步 flag 命令行標誌參數選擇輸出的格式.
|
||||
|
||||
**練習 10.2:** 設計一個通用的壓縮文件讀取框架, 用來讀取 ZIP(archive/zip) 和 POSIX tar(archive/tar) 格式壓縮的文檔. 使用類似上面的註冊機製來擴展支持不同的壓縮格式, 然後根據需要通過匿名導入選擇支持的格式.
|
||||
|
||||
|
@ -33,7 +33,7 @@ type Reader struct{ /* ... */ }
|
||||
func NewReader(s string) *Reader
|
||||
```
|
||||
|
||||
string 本身併沒有齣現在每個成員名字中. 因爲用戶會這樣引用這些成員 strings.Index, strings.Replacer 等.
|
||||
string 本身併沒有出現在每個成員名字中. 因爲用戶會這樣引用這些成員 strings.Index, strings.Replacer 等.
|
||||
|
||||
其他一些包, 可能隻描述了單一的數據類型, 例如 html/template 和 math/rand 等, 隻暴露一個主要的數據結構和與它相關的方法, 還有一個 New 名字的函數用於創建實例.
|
||||
|
||||
|
@ -2,7 +2,7 @@
|
||||
|
||||
使用Go工具, 不僅可以根據包導入路徑找到本地工作區的包, 甚至可以從互聯網上找到和更新包.
|
||||
|
||||
使用命令 `go get` 可以下載一個單一的包或者用 `...` 下載整個子目録里面的每個包. Go工具同時計算併下載所依賴的每個包, 這也是前一個例子中 golang.org/x/net/html 自動齣現在本地工作區目録的原因.
|
||||
使用命令 `go get` 可以下載一個單一的包或者用 `...` 下載整個子目録里面的每個包. Go工具同時計算併下載所依賴的每個包, 這也是前一個例子中 golang.org/x/net/html 自動出現在本地工作區目録的原因.
|
||||
|
||||
一旦 `go get` 命令下載了包, 然後就是安裝包或包對應的命令. 我們將在下一節再關註它的細節, 現在隻是展示下整個過程是如何的簡單. 第一個命令是穫取 golint 工具, 用於檢測Go源代碼的編程風格是否有問題. 第二個命令是用 golint 對 2.6.2節的 gopl.io/ch2/popcount 包代碼進行編碼風格檢査. 它友好地報告了忘記了包的文檔:
|
||||
|
||||
|
@ -1,6 +1,6 @@
|
||||
### 10.7.3. 構建包
|
||||
|
||||
`go build` 命令編譯參數指定的每個包. 如果包是一個庫, 則忽略輸齣結果; 這可以用於檢測包的可以正確編譯的.
|
||||
`go build` 命令編譯參數指定的每個包. 如果包是一個庫, 則忽略輸出結果; 這可以用於檢測包的可以正確編譯的.
|
||||
如果包的名字是 main, `go build` 將調用連接器在當前目録創建一個可執行程序; 導入路徑的最後一段作爲可執行程序的名字.
|
||||
|
||||
因爲每個目録隻包含一個包, 因此每個可執行程序後者叫Unix術語中的命令, 會要求放到一個獨立的目録. 這些目録有時候會放在名叫 cmd 目録的子目録下面, 例如用於提供Go文檔服務的 golang.org/x/tools/cmd/godoc 命令 (§10.7.4).
|
||||
@ -68,7 +68,7 @@ $ go run quoteargs.go one "two three" four\ five
|
||||
|
||||
因爲編譯對應不同的操作繫統平颱和CPU架構, `go install` 會將編譯結果安裝到 GOOS 和 GOARCH 對應的目録. 例如, 在 Mac 繫統 golang.org/x/net/html 包將被安裝到 $GOPATH/pkg/darwin_amd64 目録下的 golang.org/x/net/html.a 文件.
|
||||
|
||||
針對不同操作繫統或CPU的交叉構建也是很簡單的. 隻需要設置好目標對應的GOOS 和 GOARCH, 然後運行構建目録卽可. 下面交叉編譯的程序將輸齣它在編譯時操作繫統和CPU類型:
|
||||
針對不同操作繫統或CPU的交叉構建也是很簡單的. 隻需要設置好目標對應的GOOS 和 GOARCH, 然後運行構建目録卽可. 下面交叉編譯的程序將輸出它在編譯時操作繫統和CPU類型:
|
||||
|
||||
```Go
|
||||
gopl.io/ch10/cross
|
||||
@ -89,13 +89,13 @@ $ ./cross
|
||||
darwin 386
|
||||
```
|
||||
|
||||
有些包可能需要針對不同平颱和處理器類型輸齣不同版本的代碼, 以便於處理底層的可移植性問題或提供爲一些特點代碼提供優化. 如果一個文件名包含了一個操作繫統或處理器類型名字, 例如 net_linux.go 或 asm_amd64.s, Go工具將隻在對應的平颱編譯這些文件. 還有一個特别的構建註釋註釋可以提供更多的構建控製. 例如, 文件中如果包含下面的註釋:
|
||||
有些包可能需要針對不同平颱和處理器類型輸出不同版本的代碼, 以便於處理底層的可移植性問題或提供爲一些特點代碼提供優化. 如果一個文件名包含了一個操作繫統或處理器類型名字, 例如 net_linux.go 或 asm_amd64.s, Go工具將隻在對應的平颱編譯這些文件. 還有一個特别的構建註釋註釋可以提供更多的構建控製. 例如, 文件中如果包含下面的註釋:
|
||||
|
||||
```Go
|
||||
// +build linux darwin
|
||||
```
|
||||
|
||||
在包聲明的前面(含包的註釋), 告訴 `go build` 隻在針對 Linux 或 Mac OS X 是纔編譯這個文件. 下面的構建註釋表示不編譯這個文件:
|
||||
在包聲明的前面(含包的註釋), 告訴 `go build` 隻在針對 Linux 或 Mac OS X 是才編譯這個文件. 下面的構建註釋表示不編譯這個文件:
|
||||
|
||||
```Go
|
||||
// +build ignore
|
||||
|
@ -1,6 +1,6 @@
|
||||
### 10.7.4. 包文檔
|
||||
|
||||
Go的編碼風格鼓勵爲每個包提供良好的文檔. 包中每個導齣的成員和包聲明前都應該包含添加目的和用法説明的註釋.
|
||||
Go的編碼風格鼓勵爲每個包提供良好的文檔. 包中每個導出的成員和包聲明前都應該包含添加目的和用法説明的註釋.
|
||||
|
||||
Go中包文檔註釋一般是完整的句子, 第一行是包的摘要説明, 註釋後僅跟着包聲明語句. 函數的參數或其他的標識符併不需要額外的引號或其他標記註明. 例如, 下面是 fmt.Fprintf 的文檔註釋.
|
||||
|
||||
@ -10,7 +10,7 @@ Go中包文檔註釋一般是完整的句子, 第一行是包的摘要説明,
|
||||
func Fprintf(w io.Writer, format string, a ...interface{}) (int, error)
|
||||
```
|
||||
|
||||
Fprintf 函數格式化的細節在 fmt 包文檔中描述. 如果註釋後僅跟着包聲明語句, 那註釋對應整個包的文檔. 包文檔對應的註釋隻能有一個(譯註: 其實可以多個, 它們會組合成一個包文檔註釋.), 可以齣現在任何一個源文件中. 如果包的註釋內容比較長, 可以當到一個獨立的文件中; fmt 包註釋就有 300 行之多. 這個專門用於保證包文檔的文件通常叫 doc.go.
|
||||
Fprintf 函數格式化的細節在 fmt 包文檔中描述. 如果註釋後僅跟着包聲明語句, 那註釋對應整個包的文檔. 包文檔對應的註釋隻能有一個(譯註: 其實可以多個, 它們會組合成一個包文檔註釋.), 可以出現在任何一個源文件中. 如果包的註釋內容比較長, 可以當到一個獨立的文件中; fmt 包註釋就有 300 行之多. 這個專門用於保證包文檔的文件通常叫 doc.go.
|
||||
|
||||
好的文檔併不需要面面俱到, 文檔本身應該是簡潔但可不忽略的. 事實上, Go的風格喜歡簡潔的文檔, 併且文檔也是需要想代碼一樣維護的. 對於一組聲明語句, 可以同一個精鍊的句子描述, 如果是顯而易見的功能則併不需要註釋.
|
||||
|
||||
|
@ -1,8 +1,8 @@
|
||||
### 10.7.5. 內部包
|
||||
|
||||
在Go程序中, 包的封裝機製是一個重要的特性. 爲導齣的標識符隻在同一個包內部可以訪問, 導齣的標識符則是面向全世界可見.
|
||||
在Go程序中, 包的封裝機製是一個重要的特性. 爲導出的標識符隻在同一個包內部可以訪問, 導出的標識符則是面向全世界可見.
|
||||
|
||||
有時候, 一個中間的狀態可能也是有用的, 對於一小部分信任的包是可見的, 但併不是對所有調用者都可見. 例如, 當我們計劃將一個大的包拆分爲很多小的更容易管理的子包, 但是我們併不想將內部的子包結構也完全暴露齣去. 同時, 我們肯呢個還希望在內部子包之間共享一些通用的處理包. 或者我們隻是想實驗一個新包的還併不穩定的接口, 暫時隻暴露給一些受限製的客戶端.
|
||||
有時候, 一個中間的狀態可能也是有用的, 對於一小部分信任的包是可見的, 但併不是對所有調用者都可見. 例如, 當我們計劃將一個大的包拆分爲很多小的更容易管理的子包, 但是我們併不想將內部的子包結構也完全暴露出去. 同時, 我們肯呢個還希望在內部子包之間共享一些通用的處理包. 或者我們隻是想實驗一個新包的還併不穩定的接口, 暫時隻暴露給一些受限製的客戶端.
|
||||
|
||||
![](../images/ch10-01.png)
|
||||
|
||||
|
@ -71,7 +71,7 @@ $ go list -json hash
|
||||
}
|
||||
```
|
||||
|
||||
參數 `-f` 允許用戶使用 text/template (§4.6) 的模闆語言定義輸齣文本的格式. 下面的命令打印 strconv 包的依賴的包, 然後用 join 模闆函數鏈接爲一行, 用一個空格分隔:
|
||||
參數 `-f` 允許用戶使用 text/template (§4.6) 的模闆語言定義輸出文本的格式. 下面的命令打印 strconv 包的依賴的包, 然後用 join 模闆函數鏈接爲一行, 用一個空格分隔:
|
||||
|
||||
{% raw %}
|
||||
```
|
||||
@ -113,7 +113,7 @@ go list 命令對於一次性的交互式査詢或自動化構建和測試腳本
|
||||
|
||||
在本章, 我們解釋了Go工具箱除了測試命令之外的所有重要的命令. 在下一章, 我們將看到如何用 `go test` 命令去測試Go程序.
|
||||
|
||||
**練習10.4:** 創建一個工具, 根據命令行指定的參數, 報告工作區所有依賴指定包的其他包集合. 提示: 你需要運行 `go list` 命令兩次, 一次用於初始化包, 一次用於所有包. 你可能需要用 encoding/json (§4.5) 包來分析輸齣的 JSON 格式的信息.
|
||||
**練習10.4:** 創建一個工具, 根據命令行指定的參數, 報告工作區所有依賴指定包的其他包集合. 提示: 你需要運行 `go list` 命令兩次, 一次用於初始化包, 一次用於所有包. 你可能需要用 encoding/json (§4.5) 包來分析輸出的 JSON 格式的信息.
|
||||
|
||||
|
||||
|
||||
|
@ -5,7 +5,7 @@
|
||||
|
||||
Go的工具箱集合了一繫列的功能到一個命令集. 它可以看作是一個包管理器(類似於Linux中的apt和rpm工具), 用於包的査詢, 計算的包依賴關繫, 從遠程版本控製繫統和下載它們等任務. 它也是一個構建繫統, 計算文件的依賴關繫, 然後調用編譯器, 滙編器 和 連接器 構建程序, 雖然它故意被設計成沒有標準的make命令那麽複雜. 它也是一個測試驅動程序, 我們在第11章討論測試話題.
|
||||
|
||||
Go工具箱的命令有着類似"瑞士軍刀"的風格, 帶着一打子的子命令, 有一些我們經常用到, 例如 get, run, build, 和 fmt 等. 你可以運行 `go help` 命令査看內置的溫度, 爲了査詢方便, 我們列齣了最常用的命令:
|
||||
Go工具箱的命令有着類似"瑞士軍刀"的風格, 帶着一打子的子命令, 有一些我們經常用到, 例如 get, run, build, 和 fmt 等. 你可以運行 `go help` 命令査看內置的溫度, 爲了査詢方便, 我們列出了最常用的命令:
|
||||
|
||||
```
|
||||
$ go
|
||||
|
@ -3,7 +3,7 @@
|
||||
|
||||
表格驅動的測試便於構造基於精心挑選的測試數據的測試用例. 另一種測試思路是隨機測試, 也就是通過構造更廣泛的隨機輸入來測試探索函數的行爲.
|
||||
|
||||
那麽對於一個隨機的輸入, 我們如何能知道希望的輸齣結果呢? 這里有兩種策略. 第一個是編寫另一個函數, 使用簡單和清晰的算法, 雖然效率較低但是行爲和要測試的函數一致, 然後針對相同的隨機輸入檢査兩者的輸齣結果. 第二種是生成的隨機輸入的數據遵循特定的模式, 這樣我們就可以知道期望的輸齣的模式.
|
||||
那麽對於一個隨機的輸入, 我們如何能知道希望的輸出結果呢? 這里有兩種策略. 第一個是編寫另一個函數, 使用簡單和清晰的算法, 雖然效率較低但是行爲和要測試的函數一致, 然後針對相同的隨機輸入檢査兩者的輸出結果. 第二種是生成的隨機輸入的數據遵循特定的模式, 這樣我們就可以知道期望的輸出的模式.
|
||||
|
||||
下面的例子使用的是第二種方法: randomPalindrome 函數用於隨機生成迴文字符串.
|
||||
|
||||
|
@ -42,7 +42,7 @@ func echo(newline bool, sep string, args []string) error {
|
||||
}
|
||||
```
|
||||
|
||||
在測試中嗎我們可以用各種參數和標標誌調用 echo 函數, 然後檢測它的輸齣是否正確, 我們通過增加參數來減少 echo 函數對全局變量的依賴. 我們還增加了一個全局名爲 out 的變量來替代直接使用 os.Stdout, 這樣測試代碼可以根據需要將 out 脩改爲不同的對象以便於檢査. 下面就是 echo_test.go 文件中的測試代碼:
|
||||
在測試中嗎我們可以用各種參數和標標誌調用 echo 函數, 然後檢測它的輸出是否正確, 我們通過增加參數來減少 echo 函數對全局變量的依賴. 我們還增加了一個全局名爲 out 的變量來替代直接使用 os.Stdout, 這樣測試代碼可以根據需要將 out 脩改爲不同的對象以便於檢査. 下面就是 echo_test.go 文件中的測試代碼:
|
||||
|
||||
```Go
|
||||
package main
|
||||
@ -83,7 +83,7 @@ func TestEcho(t *testing.T) {
|
||||
}
|
||||
```
|
||||
|
||||
要註意的是測試代碼和産品代碼在同一個包. 雖然是main包, 也有對應的 main 入口函數, 但是在測試的時候 main 包隻是 TestEcho 測試函數導入的一個普通包, 里面 main 函數併沒有被導齣是被忽略的.
|
||||
要註意的是測試代碼和産品代碼在同一個包. 雖然是main包, 也有對應的 main 入口函數, 但是在測試的時候 main 包隻是 TestEcho 測試函數導入的一個普通包, 里面 main 函數併沒有被導出是被忽略的.
|
||||
|
||||
通過將測試放到表格中, 我們很容易添加新的測試用例. 讓我通過增加下面的測試用例來看看失敗的情況是怎麽樣的:
|
||||
|
||||
@ -91,7 +91,7 @@ func TestEcho(t *testing.T) {
|
||||
{true, ",", []string{"a", "b", "c"}, "a b c\n"}, // NOTE: wrong expectation!
|
||||
```
|
||||
|
||||
`go test` 輸齣如下:
|
||||
`go test` 輸出如下:
|
||||
|
||||
```
|
||||
$ go test gopl.io/ch11/echo
|
||||
@ -103,6 +103,6 @@ FAIL gopl.io/ch11/echo 0.006s
|
||||
|
||||
錯誤信息描述了嚐試的操作(使用Go類似語法), 實際的行爲, 和期望的行爲. 通過這樣的錯誤信息, 你可以在檢視代碼之前就很容易定位錯誤的原因.
|
||||
|
||||
要註意的是在測試代碼中併沒有調用 log.Fatal 或 os.Exit, 因爲調用這類函數會導致程序提前退齣; 調用這些函數的特權應該放在 main 函數中. 如果眞的有以外的事情導致函數發送 panic, 測試驅動應該嚐試 recover, 然後將當前測試當作失敗處理. 如果是可預期的錯誤, 例如非法的用戶輸入, 找不到文件, 或配置文件不當等應該通過返迴一個非空的 error 的方式處理. 幸運的是(上面的意外隻是一個插麴), 我們的 echo 示例是比較簡單的也沒有需要返迴非空error的情況.
|
||||
要註意的是在測試代碼中併沒有調用 log.Fatal 或 os.Exit, 因爲調用這類函數會導致程序提前退出; 調用這些函數的特權應該放在 main 函數中. 如果眞的有以外的事情導致函數發送 panic, 測試驅動應該嚐試 recover, 然後將當前測試當作失敗處理. 如果是可預期的錯誤, 例如非法的用戶輸入, 找不到文件, 或配置文件不當等應該通過返迴一個非空的 error 的方式處理. 幸運的是(上面的意外隻是一個插麴), 我們的 echo 示例是比較簡單的也沒有需要返迴非空error的情況.
|
||||
|
||||
|
||||
|
@ -5,9 +5,9 @@
|
||||
|
||||
黑盒和白盒這兩種測試方法是互補的. 黑盒測試一般更健壯, 隨着軟件實現的完善測試代碼很少需要更新. 它們可以幫助測試者了解眞是客戶的需求, 可以幫助發現API設計的一些不足之處. 相反, 白盒測試則可以對內部一些棘手的實現提供更多的測試覆蓋.
|
||||
|
||||
我們已經看到兩種測試的例子. TestIsPalindrome 測試僅僅使用導齣的 IsPalindrome 函數, 因此它是一個黑盒測試. TestEcho 測試則調用了內部的 echo 函數, 併且更新了內部的 out 全局變量, 這兩個都是未導齣的, 因此它是白盒測試.
|
||||
我們已經看到兩種測試的例子. TestIsPalindrome 測試僅僅使用導出的 IsPalindrome 函數, 因此它是一個黑盒測試. TestEcho 測試則調用了內部的 echo 函數, 併且更新了內部的 out 全局變量, 這兩個都是未導出的, 因此它是白盒測試.
|
||||
|
||||
當我們開發TestEcho測試的時候, 我們脩改了 echo 函數使用包級的 out 作爲輸齣對象, 因此測試代碼可以用另一個實現代替標準輸齣, 這樣可以方便對比 echo 的輸齣數據. 使用類似的技術, 我們可以將産品代碼的其他部分也替換爲一個容易測試的僞對象. 使用僞對象的好處是我們可以方便配置, 容易預測, 更可靠, 也更容易觀察. 同時也可以避免一些不良的副作用, 例如更新生産數據庫或信用卡消費行爲.
|
||||
當我們開發TestEcho測試的時候, 我們脩改了 echo 函數使用包級的 out 作爲輸出對象, 因此測試代碼可以用另一個實現代替標準輸出, 這樣可以方便對比 echo 的輸出數據. 使用類似的技術, 我們可以將産品代碼的其他部分也替換爲一個容易測試的僞對象. 使用僞對象的好處是我們可以方便配置, 容易預測, 更可靠, 也更容易觀察. 同時也可以避免一些不良的副作用, 例如更新生産數據庫或信用卡消費行爲.
|
||||
|
||||
下面的代碼演示了爲用戶提供網絡存儲的web服務中的配額檢測邏輯. 當用戶使用了超過 90% 的存儲配額之後將發送提醒郵件.
|
||||
|
||||
|
@ -50,11 +50,11 @@ $ go list -f={{.XTestGoFiles}} fmt
|
||||
|
||||
{% endraw %}
|
||||
|
||||
有時候測試擴展包需要訪問被測試包內部的代碼, 例如在一個爲了避免循環導入而被獨立到外部測試擴展包的白盒測試. 在這種情況下, 我們可以通過一些技巧解決: 我們在包內的一個 _test.go 文件中導齣一個內部的實現給測試擴展包. 因爲這些代碼隻有在測試時纔需要, 因此一般放在 export_test.go 文件中.
|
||||
有時候測試擴展包需要訪問被測試包內部的代碼, 例如在一個爲了避免循環導入而被獨立到外部測試擴展包的白盒測試. 在這種情況下, 我們可以通過一些技巧解決: 我們在包內的一個 _test.go 文件中導出一個內部的實現給測試擴展包. 因爲這些代碼隻有在測試時才需要, 因此一般放在 export_test.go 文件中.
|
||||
|
||||
例如, fmt 包的 fmt.Scanf 需要 unicode.IsSpace 函數提供的功能. 但是爲了避免太多的依賴, fmt 包併沒有導入包含鉅大表格數據的 unicode 包; 相反fmt包有一個叫 isSpace 內部的簡易實現.
|
||||
|
||||
爲了確保 fmt.isSpace 和 unicode.IsSpace 函數的行爲一致, fmt 包謹慎地包含了一個測試. 是一個在測試擴展包內的測試, 因此是無法直接訪問到 isSpace 內部函數的, 因此 fmt 通過一個祕密齣口導齣了 isSpace 函數. export_test.go 文件就是專門用於測試擴展包的祕密齣口.
|
||||
爲了確保 fmt.isSpace 和 unicode.IsSpace 函數的行爲一致, fmt 包謹慎地包含了一個測試. 是一個在測試擴展包內的測試, 因此是無法直接訪問到 isSpace 內部函數的, 因此 fmt 通過一個祕密出口導出了 isSpace 函數. export_test.go 文件就是專門用於測試擴展包的祕密出口.
|
||||
|
||||
```Go
|
||||
package fmt
|
||||
@ -62,5 +62,5 @@ package fmt
|
||||
var IsSpace = isSpace
|
||||
```
|
||||
|
||||
這個測試文件併沒有定義測試代碼; 它隻是通過 fmt.IsSpace 簡單導齣了內部的 isSpace 函數, 提供給測試擴展包使用. 這個技巧可以廣泛用於位於測試擴展包的白盒測試.
|
||||
這個測試文件併沒有定義測試代碼; 它隻是通過 fmt.IsSpace 簡單導出了內部的 isSpace 函數, 提供給測試擴展包使用. 這個技巧可以廣泛用於位於測試擴展包的白盒測試.
|
||||
|
||||
|
@ -1,9 +1,9 @@
|
||||
### 11.2.5. 編寫有效的測試
|
||||
|
||||
|
||||
許多Go新人會驚異與它的極簡的測試框架. 很多其他語言的測試框架都提供了識别測試函數的機製(通常使用反射或元數據), 通過設置一些 ‘‘setup’’ 和 ‘‘teardown’’ 的鉤子函數來執行測試用例運行的初始化或之後的清理操作, 同時測試工具箱還提供了很多類似assert斷言, 比較值, 格式化輸齣錯誤信息和停止一個識别的測試等輔助函數(通常使用異常機製). 雖然這些機製可以使得測試非常簡潔, 但是測試輸齣的日誌卻像火星文一般難以理解. 此外, 雖然測試最終也會輸齣 PASS 或 FAIL 的報告, 但是它們提供的信息格式卻非常不利於代碼維護者快速定位問題, 因爲失敗的信息的具體含義是非常隱患的, 比如 "assert: 0 == 1" 或 成頁的海量跟蹤日誌.
|
||||
許多Go新人會驚異與它的極簡的測試框架. 很多其他語言的測試框架都提供了識别測試函數的機製(通常使用反射或元數據), 通過設置一些 ‘‘setup’’ 和 ‘‘teardown’’ 的鉤子函數來執行測試用例運行的初始化或之後的清理操作, 同時測試工具箱還提供了很多類似assert斷言, 比較值, 格式化輸出錯誤信息和停止一個識别的測試等輔助函數(通常使用異常機製). 雖然這些機製可以使得測試非常簡潔, 但是測試輸出的日誌卻像火星文一般難以理解. 此外, 雖然測試最終也會輸出 PASS 或 FAIL 的報告, 但是它們提供的信息格式卻非常不利於代碼維護者快速定位問題, 因爲失敗的信息的具體含義是非常隱患的, 比如 "assert: 0 == 1" 或 成頁的海量跟蹤日誌.
|
||||
|
||||
Go語言的測試風格則形成鮮明對比. 它期望測試者自己完成大部分的工作, 定義函數避免重複, 就像普通編程那樣. 編寫測試併不是一個機械的填充過程; 一個測試也有自己的接口, 盡管它的維護者也是測試僅有的一個用戶. 一個好的測試不應該引發其他無關的錯誤信息, 它隻要清晰簡潔地描述問題的癥狀卽可, 有時候可能還需要一些上下文信息. 在理想情況下, 維護者可以在不看代碼的情況下就能根據錯誤信息定位錯誤産生的原因. 一個好的測試不應該在遇到一點小錯誤就立刻退齣測試, 它應該嚐試報告更多的測試, 因此我們可能從多個失敗測試的模式中發現錯誤産生的規律.
|
||||
Go語言的測試風格則形成鮮明對比. 它期望測試者自己完成大部分的工作, 定義函數避免重複, 就像普通編程那樣. 編寫測試併不是一個機械的填充過程; 一個測試也有自己的接口, 盡管它的維護者也是測試僅有的一個用戶. 一個好的測試不應該引發其他無關的錯誤信息, 它隻要清晰簡潔地描述問題的癥狀卽可, 有時候可能還需要一些上下文信息. 在理想情況下, 維護者可以在不看代碼的情況下就能根據錯誤信息定位錯誤産生的原因. 一個好的測試不應該在遇到一點小錯誤就立刻退出測試, 它應該嚐試報告更多的測試, 因此我們可能從多個失敗測試的模式中發現錯誤産生的規律.
|
||||
|
||||
下面的斷言函數比較兩個值, 然後生成一個通用的錯誤信息, 併停止程序. 它很方便使用也確實有效果, 但是當識别的時候, 錯誤時打印的信息幾乎是沒有價值的. 它併沒有爲解決問題提供一個很好的入口.
|
||||
|
||||
@ -26,7 +26,7 @@ func TestSplit(t *testing.T) {
|
||||
}
|
||||
```
|
||||
|
||||
從這個意義上説, 斷言函數犯了過早抽象的錯誤: 僅僅測試兩個整數是否相同, 而放棄了根據上下文提供更有意義的錯誤信息的做法. 我們可以根據具體的錯誤打印一個更有價值的錯誤信息, 就像下面例子那樣. 測試在隻有一次重複的模式齣現時引入抽象.
|
||||
從這個意義上説, 斷言函數犯了過早抽象的錯誤: 僅僅測試兩個整數是否相同, 而放棄了根據上下文提供更有意義的錯誤信息的做法. 我們可以根據具體的錯誤打印一個更有價值的錯誤信息, 就像下面例子那樣. 測試在隻有一次重複的模式出現時引入抽象.
|
||||
|
||||
```Go
|
||||
func TestSplit(t *testing.T) {
|
||||
@ -42,8 +42,8 @@ func TestSplit(t *testing.T) {
|
||||
|
||||
現在的測試不僅報告了調用的具體函數, 它的輸入, 和結果的意義; 併且打印的眞實返迴的值和期望返迴的值; 併且卽使斷言失敗依然會繼續嚐試運行更多的測試. 一旦我們寫了這樣結構的測試, 下一步自然不是用更多的if語句來擴展測試用例, 我們可以用像 IsPalindrome 的表驅動測試那樣來準備更多的 s, sep 測試用例.
|
||||
|
||||
前面的例子併不需要額外的輔助函數, 如果如果有可以使測試代碼更簡單的方法我們也樂意接受. (我們將在 13.3節 看到一個 reflect.DeepEqual 輔助函數.) 開始一個好的測試的關鍵是通過實現你眞正想要的具體行爲, 然後纔是考慮然後簡化測試代碼. 最好的結果是直接從庫的抽象接口開始, 針對公共接口編寫一些測試函數.
|
||||
前面的例子併不需要額外的輔助函數, 如果如果有可以使測試代碼更簡單的方法我們也樂意接受. (我們將在 13.3節 看到一個 reflect.DeepEqual 輔助函數.) 開始一個好的測試的關鍵是通過實現你眞正想要的具體行爲, 然後才是考慮然後簡化測試代碼. 最好的結果是直接從庫的抽象接口開始, 針對公共接口編寫一些測試函數.
|
||||
|
||||
**練習11.5:** 用表格驅動的技術擴展TestSplit測試, 併打印期望的輸齣結果.
|
||||
**練習11.5:** 用表格驅動的技術擴展TestSplit測試, 併打印期望的輸出結果.
|
||||
|
||||
|
||||
|
@ -1,8 +1,8 @@
|
||||
### 11.2.6. 避免的不穩定的測試
|
||||
|
||||
如果一個應用程序對於新齣現的但有效的輸入經常失敗説明程序不夠穩健; 同樣如果一個測試僅僅因爲聲音變化就會導致失敗也是不合邏輯的. 就像一個不夠穩健的程序會挫敗它的用戶一樣, 一個脆弱性測試同樣會激怒它的維護者. 最脆弱的測試代碼會在程序沒有任何變化的時候産生不同的結果, 時好時壞, 處理它們會耗費大量的時間但是併不會得到任何好處.
|
||||
如果一個應用程序對於新出現的但有效的輸入經常失敗説明程序不夠穩健; 同樣如果一個測試僅僅因爲聲音變化就會導致失敗也是不合邏輯的. 就像一個不夠穩健的程序會挫敗它的用戶一樣, 一個脆弱性測試同樣會激怒它的維護者. 最脆弱的測試代碼會在程序沒有任何變化的時候産生不同的結果, 時好時壞, 處理它們會耗費大量的時間但是併不會得到任何好處.
|
||||
|
||||
當一個測試函數産生一個複雜的輸齣如一個很長的字符串, 或一個精心設計的數據結構, 或一個文件, 它可以用於和預設的‘‘golden’’結果數據對比, 用這種簡單方式寫測試是誘人的. 但是隨着項目的發展, 輸齣的某些部分很可能會發生變化, 盡管很可能是一個改進的實現導致的. 而且不僅僅是輸齣部分, 函數複雜複製的輸入部分可能也跟着變化了, 因此測試使用的輸入也就不在有效了.
|
||||
當一個測試函數産生一個複雜的輸出如一個很長的字符串, 或一個精心設計的數據結構, 或一個文件, 它可以用於和預設的‘‘golden’’結果數據對比, 用這種簡單方式寫測試是誘人的. 但是隨着項目的發展, 輸出的某些部分很可能會發生變化, 盡管很可能是一個改進的實現導致的. 而且不僅僅是輸出部分, 函數複雜複製的輸入部分可能也跟着變化了, 因此測試使用的輸入也就不在有效了.
|
||||
|
||||
避免脆弱測試代碼的方法是隻檢測你眞正關心的屬性. 保存測試代碼的簡潔和內部結構的穩定. 特别是對斷言部分要有所選擇. 不要檢査字符串的全匹配, 但是尋找相關的子字符串, 因爲某些子字符串在項目的發展中是比較穩定不變的. 通常編寫一個重複雜的輸齣中提取必要精華信息以用於斷言是值得的, 雖然這可能會帶來很多前期的工作, 但是它可以幫助迅速及時脩複因爲項目演化而導致的不合邏輯的失敗測試.
|
||||
避免脆弱測試代碼的方法是隻檢測你眞正關心的屬性. 保存測試代碼的簡潔和內部結構的穩定. 特别是對斷言部分要有所選擇. 不要檢査字符串的全匹配, 但是尋找相關的子字符串, 因爲某些子字符串在項目的發展中是比較穩定不變的. 通常編寫一個重複雜的輸出中提取必要精華信息以用於斷言是值得的, 雖然這可能會帶來很多前期的工作, 但是它可以幫助迅速及時脩複因爲項目演化而導致的不合邏輯的失敗測試.
|
||||
|
||||
|
@ -36,7 +36,7 @@ func IsPalindrome(s string) bool {
|
||||
}
|
||||
```
|
||||
|
||||
在相同的目録下, word_test.go 文件包含了 TestPalindrome 和 TestNonPalindrome 兩個測試函數. 每一個都是測試 IsPalindrome 是否給齣正確的結果, 併使用 t.Error 報告失敗:
|
||||
在相同的目録下, word_test.go 文件包含了 TestPalindrome 和 TestNonPalindrome 兩個測試函數. 每一個都是測試 IsPalindrome 是否給出正確的結果, 併使用 t.Error 報告失敗:
|
||||
|
||||
```Go
|
||||
package word
|
||||
@ -67,7 +67,7 @@ $ go test
|
||||
ok gopl.io/ch11/word1 0.008s
|
||||
```
|
||||
|
||||
還比較滿意, 我們運行了這個程序, 不過沒有提前退齣是因爲還沒有遇到BUG報告. 一個法國名爲 Noelle Eve Elleon 的用戶抱怨 IsPalindrome 函數不能識别 ‘‘été.’’. 另外一個來自美國中部用戶的抱怨是不能識别 ‘‘A man, a plan, a canal: Panama.’’. 執行特殊和小的BUG報告爲我們提供了新的更自然的測試用例.
|
||||
還比較滿意, 我們運行了這個程序, 不過沒有提前退出是因爲還沒有遇到BUG報告. 一個法國名爲 Noelle Eve Elleon 的用戶抱怨 IsPalindrome 函數不能識别 ‘‘été.’’. 另外一個來自美國中部用戶的抱怨是不能識别 ‘‘A man, a plan, a canal: Panama.’’. 執行特殊和小的BUG報告爲我們提供了新的更自然的測試用例.
|
||||
|
||||
```Go
|
||||
func TestFrenchPalindrome(t *testing.T) {
|
||||
@ -98,7 +98,7 @@ FAIL
|
||||
FAIL gopl.io/ch11/word1 0.014s
|
||||
```
|
||||
|
||||
先編寫測試用例併觀察到測試用例觸發了和用戶報告的錯誤相同的描述是一個好的測試習慣. 隻有這樣, 我們纔能定位我們要眞正解決的問題.
|
||||
先編寫測試用例併觀察到測試用例觸發了和用戶報告的錯誤相同的描述是一個好的測試習慣. 隻有這樣, 我們才能定位我們要眞正解決的問題.
|
||||
|
||||
先寫測試用例的另好處是, 運行測試通常會比手工描述報告的處理更快, 這讓我們可以進行快速地迭代. 如果測試集有很多運行緩慢的測試, 我們可以通過隻選擇運行某些特定的測試來加快測試速度.
|
||||
|
||||
@ -121,7 +121,7 @@ exit status 1
|
||||
FAIL gopl.io/ch11/word1 0.017s
|
||||
```
|
||||
|
||||
參數 `-run` 是一個正則表達式, 隻有測試函數名被它正確匹配的測試函數纔會被 `go test` 運行:
|
||||
參數 `-run` 是一個正則表達式, 隻有測試函數名被它正確匹配的測試函數才會被 `go test` 運行:
|
||||
|
||||
```
|
||||
$ go test -v -run="French|Canal"
|
||||
@ -207,11 +207,11 @@ ok gopl.io/ch11/word2 0.015s
|
||||
|
||||
這種表格驅動的測試在Go中很常見的. 我們很容易想表格添加新的測試數據, 併且後面的測試邏輯也沒有冗餘, 這樣我們可以更好地完善錯誤信息.
|
||||
|
||||
失敗的測試的輸齣併不包括調用 t.Errorf 時刻的堆棧調用信息. 不像其他語言或測試框架的 assert 斷言, t.Errorf 調用也沒有引起 panic 或停止測試的執行. 卽使表格中前面的數據導致了測試的失敗, 表格後面的測試數據依然會運行測試, 因此在一個測試中我們可能了解多個失敗的信息.
|
||||
失敗的測試的輸出併不包括調用 t.Errorf 時刻的堆棧調用信息. 不像其他語言或測試框架的 assert 斷言, t.Errorf 調用也沒有引起 panic 或停止測試的執行. 卽使表格中前面的數據導致了測試的失敗, 表格後面的測試數據依然會運行測試, 因此在一個測試中我們可能了解多個失敗的信息.
|
||||
|
||||
如果我們眞的需要停止測試, 或許是因爲初始化失敗或可能是早先的錯誤導致了後續錯誤等原因, 我們可以使用 t.Fatal 或 t.Fatalf 停止測試. 它們必鬚在和測試函數同一個 goroutine 內調用.
|
||||
|
||||
測試失敗的信息一般的形式是 "f(x) = y, want z", f(x) 解釋了失敗的操作和對應的輸齣, y 是實際的運行結果, z 是期望的正確的結果. 就像前面檢査迴文字符串的例子, 實際的函數用於 f(x) 部分. 如果顯示 x 是表格驅動型測試中比較重要的部分, 因爲同一個斷言可能對應不同的表格項執行多次. 要避免無用和冗餘的信息. 在測試類似 IsPalindrome 返迴布爾類型的函數時, 可以忽略併沒有額外信息的 z 部分. 如果 x, y 或 z 是 y 的長度, 輸齣一個相關部分的簡明總結卽可. 測試的作者應該要努力幫助程序員診斷失敗的測試.
|
||||
測試失敗的信息一般的形式是 "f(x) = y, want z", f(x) 解釋了失敗的操作和對應的輸出, y 是實際的運行結果, z 是期望的正確的結果. 就像前面檢査迴文字符串的例子, 實際的函數用於 f(x) 部分. 如果顯示 x 是表格驅動型測試中比較重要的部分, 因爲同一個斷言可能對應不同的表格項執行多次. 要避免無用和冗餘的信息. 在測試類似 IsPalindrome 返迴布爾類型的函數時, 可以忽略併沒有額外信息的 z 部分. 如果 x, y 或 z 是 y 的長度, 輸出一個相關部分的簡明總結卽可. 測試的作者應該要努力幫助程序員診斷失敗的測試.
|
||||
|
||||
**練習 11.1:** 爲 4.3節 中的 charcount 程序編寫測試.
|
||||
|
||||
|
@ -81,7 +81,7 @@ $ go test -run=Coverage -coverprofile=c.out gopl.io/ch7/eval
|
||||
ok gopl.io/ch7/eval 0.032s coverage: 68.5% of statements
|
||||
```
|
||||
|
||||
這個標誌參數通過插入生成鉤子代碼來統計覆蓋率數據. 也就是説, 在運行每個測試前, 它會脩改要測試代碼的副本, 在每個塊都會設置一個布爾標誌變量. 當被脩改後的被測試代碼運行退齣時, 將統計日誌數據寫入 c.out 文件, 併打印一部分執行的語句的一個總結. (如果你需要的是摘要,使用 `go test -cover`.)
|
||||
這個標誌參數通過插入生成鉤子代碼來統計覆蓋率數據. 也就是説, 在運行每個測試前, 它會脩改要測試代碼的副本, 在每個塊都會設置一個布爾標誌變量. 當被脩改後的被測試代碼運行退出時, 將統計日誌數據寫入 c.out 文件, 併打印一部分執行的語句的一個總結. (如果你需要的是摘要,使用 `go test -cover`.)
|
||||
|
||||
如果使用了 `-covermode=count` 標誌參數, 那麽將在每個代碼塊插入一個計數器而不是布爾標誌量. 在統計結果中記録了每個塊的執行次數, 這可以用於衡量哪些是被頻繁執行的熱點代碼.
|
||||
|
||||
|
@ -107,7 +107,7 @@ func Benchmark1000(b *testing.B) { benchmark(b, 1000) }
|
||||
|
||||
**練習 11.6:** 爲 2.6.2節 的 練習 2.4 和 練習 2.5 的 PopCount 函數編寫基準測試. 看看基於表格算法在不同情況下的性能.
|
||||
|
||||
**練習 11.7:** 爲 *IntSet (§6.5) 的 Add, UnionWith 和 其他方法編寫基準測試, 使用大量隨機齣入. 你可以讓這些方法跑多快? 選擇字的大小對於性能的影響如何? IntSet 和基於內建 map 的實現相比有多快?
|
||||
**練習 11.7:** 爲 *IntSet (§6.5) 的 Add, UnionWith 和 其他方法編寫基準測試, 使用大量隨機出入. 你可以讓這些方法跑多快? 選擇字的大小對於性能的影響如何? IntSet 和基於內建 map 的實現相比有多快?
|
||||
|
||||
|
||||
|
||||
|
@ -4,7 +4,7 @@
|
||||
|
||||
> 毫無疑問, 效率會導致各種濫用. 程序員需要浪費大量的時間思考, 或者擔心, 被部分程序的速度所榦擾, 實際上這些嚐試提陞效率的行爲可能産生強烈的負面影響, 特别是當調試和維護的時候. 我們不應該過度糾結於細節的優化, 應該説約97%的場景: 過早的優化是萬惡之源.
|
||||
>
|
||||
> 我們當然不應該放棄那關鍵的3%的機會. 一個好的程序員不會因爲這個理由而滿足, 他們會明智地觀察和識别哪些是關鍵的代碼; 但是隻有在關鍵代碼已經被確認的前提下纔會進行優化. 對於判斷哪些部分是關鍵代碼是經常容易犯經驗性錯誤的地方, 因此程序員普通使用的測量工具, 使得他們的直覺很不靠譜.
|
||||
> 我們當然不應該放棄那關鍵的3%的機會. 一個好的程序員不會因爲這個理由而滿足, 他們會明智地觀察和識别哪些是關鍵的代碼; 但是隻有在關鍵代碼已經被確認的前提下才會進行優化. 對於判斷哪些部分是關鍵代碼是經常容易犯經驗性錯誤的地方, 因此程序員普通使用的測量工具, 使得他們的直覺很不靠譜.
|
||||
|
||||
當我們想仔細觀察我們程序的運行速度的時候, 最好的技術是如何識别關鍵代碼. 自動化的剖析技術是基於程序執行期間一些抽樣數據, 然後推斷後面的執行狀態; 最終産生一個運行時間的統計數據文件.
|
||||
|
||||
@ -57,7 +57,7 @@ Showing top 10 nodes out of 166 (cum >= 60ms)
|
||||
50ms 1.39% 71.59% 60ms 1.67% crypto/elliptic.p256Sum
|
||||
```
|
||||
|
||||
參數 `-text` 標誌參數用於指定輸齣格式, 在這里每行是一個函數, 根據使用CPU的時間來排序. 其中 `-nodecount=10` 標誌參數限製了隻輸齣前10行的結果. 對於嚴重的性能問題, 這個文本格式基本可以幫助査明原因了.
|
||||
參數 `-text` 標誌參數用於指定輸出格式, 在這里每行是一個函數, 根據使用CPU的時間來排序. 其中 `-nodecount=10` 標誌參數限製了隻輸出前10行的結果. 對於嚴重的性能問題, 這個文本格式基本可以幫助査明原因了.
|
||||
|
||||
這個概要文件告訴我們, HTTPS基準測試中 `crypto/elliptic.p256ReduceDegree` 函數占用了將近一般的CPU資源. 相比之下, 如果一個概要文件中主要是runtime包的內存分配的函數, 那麽減少內存消耗可能是一個值得嚐試的優化策略.
|
||||
|
||||
|
@ -16,7 +16,7 @@ func ExampleIsPalindrome() {
|
||||
|
||||
根據示例函數的後綴名部分, godoc 的web文檔會將一個示例函數關聯到某個具體函數或包本身, 因此 ExampleIsPalindrome 示例函數將是 IsPalindrome 函數文檔的一部分, Example 示例函數將是包文檔的一部分.
|
||||
|
||||
示例文檔的第二個用處是在 `go test` 執行測試的時候也運行示例函數測試. 如果示例函數內含有類似上面例子中的 `/ Output:` 這樣的註釋, 那麽測試工具會執行這個示例函數, 然後檢測這個示例函數的標準輸齣和註釋是否匹配.
|
||||
示例文檔的第二個用處是在 `go test` 執行測試的時候也運行示例函數測試. 如果示例函數內含有類似上面例子中的 `/ Output:` 這樣的註釋, 那麽測試工具會執行這個示例函數, 然後檢測這個示例函數的標準輸出和註釋是否匹配.
|
||||
|
||||
示例函數的第三個目的提供一個眞實的演練場. golang.org 是由 dogoc 提供的服務, 它使用了 Go Playground 技術讓用戶可以在瀏覽器中在線編輯和運行每個示例函數, 就像 圖 11.4 所示的那樣. 這通常是學習函數使用或Go語言特性的最快方式.
|
||||
|
||||
|
@ -10,7 +10,7 @@ Maurice Wilkes, 第一個存儲程序計算機 EDSAC 的設計者, 1949年在他
|
||||
|
||||
Go語言的測試技術是相對低級的. 它依賴一個 'go test' 測試命令, 和一組按照約定方式編寫的測試函數, 測試命令可以運行測試函數. 編寫相對輕量級的純測試代碼是有效的, 而且它很容易延伸到基準測試和示例文檔.
|
||||
|
||||
在實踐中, 編寫測試代碼和編寫程序本身併沒有多大區别. 我們編寫的每一個函數也是針對每個具體的任務. 我們必鬚小心處理邊界條件, 思考合適的數據結構, 推斷合適的輸入應該産生什麽樣的結果輸齣. 編程測試代碼和編寫普通的Go代碼過程是類似的; 它併不需要學習新的符號, 規則和工具.
|
||||
在實踐中, 編寫測試代碼和編寫程序本身併沒有多大區别. 我們編寫的每一個函數也是針對每個具體的任務. 我們必鬚小心處理邊界條件, 思考合適的數據結構, 推斷合適的輸入應該産生什麽樣的結果輸出. 編程測試代碼和編寫普通的Go代碼過程是類似的; 它併不需要學習新的符號, 規則和工具.
|
||||
|
||||
|
||||
|
||||
|
@ -32,7 +32,7 @@ func Sprint(x interface{}) string {
|
||||
}
|
||||
```
|
||||
|
||||
但是我們如何處理其它類似 []float64, map[string][]string 等類型呢? 我們當然可以添加更多的測試分支, 但是這些組合類型的數目基本是無窮的. 還有如何處理 url.Values 等命令的類型呢? 雖然類型分支可以識别齣底層的基礎類型是 map[string][]string, 但是它併不匹配 url.Values 類型, 因爲這是兩種不同的類型, 而且 switch 分支也不可能包含每個類似 url.Values 的類型, 這會導致對這些庫的依賴.
|
||||
但是我們如何處理其它類似 []float64, map[string][]string 等類型呢? 我們當然可以添加更多的測試分支, 但是這些組合類型的數目基本是無窮的. 還有如何處理 url.Values 等命令的類型呢? 雖然類型分支可以識别出底層的基礎類型是 map[string][]string, 但是它併不匹配 url.Values 類型, 因爲這是兩種不同的類型, 而且 switch 分支也不可能包含每個類似 url.Values 的類型, 這會導致對這些庫的依賴.
|
||||
|
||||
沒有一種方法來檢査未知類型的表示方式, 我們被卡住了. 這就是我們爲何需要反射的原因.
|
||||
|
||||
|
@ -20,7 +20,7 @@ var w io.Writer = os.Stdout
|
||||
fmt.Println(reflect.TypeOf(w)) // "*os.File"
|
||||
```
|
||||
|
||||
要註意的是 reflect.Type 接口是滿足 fmt.Stringer 接口的. 因爲打印動態類型值對於調試和日誌是有幫助的, fmt.Printf 提供了一個簡短的 %T 標誌參數, 內部使用 reflect.TypeOf 的結果輸齣:
|
||||
要註意的是 reflect.Type 接口是滿足 fmt.Stringer 接口的. 因爲打印動態類型值對於調試和日誌是有幫助的, fmt.Printf 提供了一個簡短的 %T 標誌參數, 內部使用 reflect.TypeOf 的結果輸出:
|
||||
|
||||
```Go
|
||||
fmt.Printf("%T\n", 3) // "int"
|
||||
@ -53,7 +53,7 @@ i := x.(int) // an int
|
||||
fmt.Printf("%d\n", i) // "3"
|
||||
```
|
||||
|
||||
一個 reflect.Value 和 interface{} 都能保存任意的值. 所不同的是, 一個空的接口隱藏了值對應的表示方式和所有的公開的方法, 因此隻有我們知道具體的動態類型纔能使用類型斷言來訪問內部的值(就像上面那樣), 對於內部值併沒有特别可做的事情. 相比之下, 一個 Value 則有很多方法來檢査其內容, 無論它的具體類型是什麽. 讓我們再次嚐試實現我們的格式化函數 format.Any.
|
||||
一個 reflect.Value 和 interface{} 都能保存任意的值. 所不同的是, 一個空的接口隱藏了值對應的表示方式和所有的公開的方法, 因此隻有我們知道具體的動態類型才能使用類型斷言來訪問內部的值(就像上面那樣), 對於內部值併沒有特别可做的事情. 相比之下, 一個 Value 則有很多方法來檢査其內容, 無論它的具體類型是什麽. 讓我們再次嚐試實現我們的格式化函數 format.Any.
|
||||
|
||||
我們使用 reflect.Value 的 Kind 方法來替代之前的類型 switch. 雖然還是有無窮多的類型, 但是它們的kinds類型卻是有限的: Bool, String 和 所有數字類型的基礎類型; Array 和 Struct 對應的聚合類型; Chan, Func, Ptr, Slice, 和 Map 對應的引用類似; 接口類型; 還有表示空值的無效類型. (空的 reflect.Value 對應 Invalid 無效類型.)
|
||||
|
||||
|
@ -36,7 +36,7 @@ struct{ float64; int16; bool } // 2 words 3words
|
||||
struct{ bool; int16; float64 } // 2 words 3words
|
||||
```
|
||||
|
||||
關於內存地址對齊算法的細節超齣了本書的范圍,也不是每一個結構體都需要擔心這個問題,不過有效的包裝可以使數據結構更加緊湊(譯註:未來的Go語言編譯器應該會默認優化結構體的順序,當然用於應該也能夠指定具體的內存布局,相同討論請參考 [Issue10014](https://github.com/golang/go/issues/10014) ),內存使用率和性能都可能會受益。
|
||||
關於內存地址對齊算法的細節超出了本書的范圍,也不是每一個結構體都需要擔心這個問題,不過有效的包裝可以使數據結構更加緊湊(譯註:未來的Go語言編譯器應該會默認優化結構體的順序,當然用於應該也能夠指定具體的內存布局,相同討論請參考 [Issue10014](https://github.com/golang/go/issues/10014) ),內存使用率和性能都可能會受益。
|
||||
|
||||
`unsafe.Alignof` 函數返迴對應參數的類型需要對齊的倍數. 和 Sizeof 類似, Alignof 也是返迴一個常量表達式, 對應一個常量. 通常情況下布爾和數字類型需要對齊到它們本身的大小(最多8個字節), 其它的類型對齊到機器字大小.
|
||||
|
||||
|
@ -63,7 +63,7 @@ func equal(x, y reflect.Value, seen map[comparison]bool) bool {
|
||||
}
|
||||
```
|
||||
|
||||
和前面的建議一樣,我們併不公開reflect包相關的接口,所以導齣的函數需要在內部自己將變量轉爲reflect.Value類型。
|
||||
和前面的建議一樣,我們併不公開reflect包相關的接口,所以導出的函數需要在內部自己將變量轉爲reflect.Value類型。
|
||||
|
||||
```Go
|
||||
// Equal reports whether x and y are deeply equal.
|
||||
@ -78,7 +78,7 @@ type comparison struct {
|
||||
}
|
||||
```
|
||||
|
||||
爲了確保算法對於有環的數據結構也能正常退齣,我們必鬚記録每次已經比較的變量,從而避免進入第二次的比較。Equal函數分配了一組用於比較的結構體,包含每對比較對象的地址(unsafe.Pointer形式保存)和類型。我們要記録類型的原因是,有些不同的變量可能對應相同的地址。例如,如果x和y都是數組類型,那麽x和x[0]將對應相同的地址,y和y[0]也是對應相同的地址,這可以用於區分x與y之間的比較或x[0]與y[0]之間的比較是否進行過了。
|
||||
爲了確保算法對於有環的數據結構也能正常退出,我們必鬚記録每次已經比較的變量,從而避免進入第二次的比較。Equal函數分配了一組用於比較的結構體,包含每對比較對象的地址(unsafe.Pointer形式保存)和類型。我們要記録類型的原因是,有些不同的變量可能對應相同的地址。例如,如果x和y都是數組類型,那麽x和x[0]將對應相同的地址,y和y[0]也是對應相同的地址,這可以用於區分x與y之間的比較或x[0]與y[0]之間的比較是否進行過了。
|
||||
|
||||
```Go
|
||||
// cycle check
|
||||
|
@ -4,7 +4,7 @@ Go程序可能會遇到要訪問C語言的某些硬件驅動函數的場景,
|
||||
|
||||
在本節中,我們將構建一個簡易的數據壓縮程序,使用了一個Go語言自帶的叫cgo的用於支援C語言函數調用的工具。這類工具一般被稱爲 *foreign-function interfaces* (簡稱ffi), 併且在類似工具中cgo也不是唯一的。SWIG( http://swig.org )是另一個類似的且被廣泛使用的工具,SWIG提供了很多複雜特性以支援C++的特性,但SWIG併不是我們要討論的主題。
|
||||
|
||||
在標準庫的`compress/...`子包有很多流行的壓縮算法的編碼和解碼實現,包括流行的LZW壓縮算法(Unix的compress命令用的算法)和DEFLATE壓縮算法(GNU gzip命令用的算法)。這些包的API的細節雖然有些差異,但是它們都提供了針對 io.Writer類型輸齣的壓縮接口和提供了針對io.Reader類型輸入的解壓縮接口。例如:
|
||||
在標準庫的`compress/...`子包有很多流行的壓縮算法的編碼和解碼實現,包括流行的LZW壓縮算法(Unix的compress命令用的算法)和DEFLATE壓縮算法(GNU gzip命令用的算法)。這些包的API的細節雖然有些差異,但是它們都提供了針對 io.Writer類型輸出的壓縮接口和提供了針對io.Reader類型輸入的解壓縮接口。例如:
|
||||
|
||||
```Go
|
||||
package gzip // compress/gzip
|
||||
@ -16,7 +16,7 @@ bzip2壓縮算法,是基於優雅的Burrows-Wheeler變換算法,運行速度
|
||||
|
||||
如果是比較小的C語言庫,我們完全可以用純Go語言重新實現一遍。如果我們對性能也沒有特殊要求的話,我們還可以用os/exec包的方法將C編寫的應用程序作爲一個子進程運行。隻有當你需要使用複雜而且性能更高的底層C接口時,就是使用cgo的場景了(譯註:用os/exec包調用子進程的方法會導致程序運行時依賴那個應用程序)。下面我們將通過一個例子講述cgo的具體用法。
|
||||
|
||||
譯註:本章采用的代碼都是最新的。因爲之前已經齣版的書中包含的代碼隻能在Go1.5之前使用。從Go1.6開始,Go語言已經明確規定了哪些Go語言指針可以之間傳入C語言函數。新代碼重點是增加了bz2alloc和bz2free的兩個函數,用於bz_stream對象空間的申請和釋放操作。下面是新代碼中增加的註釋,説明這個問題:
|
||||
譯註:本章采用的代碼都是最新的。因爲之前已經出版的書中包含的代碼隻能在Go1.5之前使用。從Go1.6開始,Go語言已經明確規定了哪些Go語言指針可以之間傳入C語言函數。新代碼重點是增加了bz2alloc和bz2free的兩個函數,用於bz_stream對象空間的申請和釋放操作。下面是新代碼中增加的註釋,説明這個問題:
|
||||
|
||||
```Go
|
||||
// The version of this program that appeared in the first and second
|
||||
@ -37,7 +37,7 @@ bzip2壓縮算法,是基於優雅的Burrows-Wheeler變換算法,運行速度
|
||||
// pointers to Go variables.
|
||||
```
|
||||
|
||||
要使用libbzip2,我們需要先構建一個bz_stream結構體,用於保持輸入和輸齣緩存。然後有三個函數:BZ2_bzCompressInit用於初始化緩存,BZ2_bzCompress用於將輸入緩存的數據壓縮到輸齣緩存,BZ2_bzCompressEnd用於釋放不需要的緩存。(目前不要擔心包的具體結構, 這個例子的目的就是演示各個部分如何組合在一起的。)
|
||||
要使用libbzip2,我們需要先構建一個bz_stream結構體,用於保持輸入和輸出緩存。然後有三個函數:BZ2_bzCompressInit用於初始化緩存,BZ2_bzCompress用於將輸入緩存的數據壓縮到輸出緩存,BZ2_bzCompressEnd用於釋放不需要的緩存。(目前不要擔心包的具體結構, 這個例子的目的就是演示各個部分如何組合在一起的。)
|
||||
|
||||
我們可以在Go代碼中直接調用BZ2_bzCompressInit和BZ2_bzCompressEnd,但是對於BZ2_bzCompress,我們將定義一個C語言的包裝函數,用它完成眞正的工作。下面是C代碼,對應一個獨立的文件。
|
||||
|
||||
@ -106,7 +106,7 @@ func NewWriter(out io.Writer) io.WriteCloser {
|
||||
|
||||
在cgo註釋中還可以包含#cgo指令,用於給C語言工具鏈指定特殊的參數。例如CFLAGS和LDFLAGS分别對應傳給C語言編譯器的編譯參數和鏈接器參數,使它們可以特定目録找到bzlib.h頭文件和libbz2.a庫文件。這個例子假設你已經在/usr目録成功安裝了bzip2庫。如果bzip2庫是安裝在不同的位置,你需要更新這些參數。
|
||||
|
||||
NewWriter函數通過調用C語言的BZ2_bzCompressInit函數來初始化stream中的緩存。在writer結構中還包括了另一個buffer,用於輸齣緩存。
|
||||
NewWriter函數通過調用C語言的BZ2_bzCompressInit函數來初始化stream中的緩存。在writer結構中還包括了另一個buffer,用於輸出緩存。
|
||||
|
||||
下面是Write方法的實現,返迴成功壓縮數據的大小,主體是一個循環中調用C語言的bz2compress函數實現的。從代碼可以看到,Go程序可以訪問C語言的bz_stream、char和uint類型,還可以訪問bz2compress等函數,甚至可以訪問C語言中像BZ_RUN那樣的宏定義,全部都是以C.x語法訪問。其中C.uint類型和Go語言的uint類型併不相同,卽使它們具有相同的大小也是不同的類型。
|
||||
|
||||
@ -132,9 +132,9 @@ func (w *writer) Write(data []byte) (int, error) {
|
||||
}
|
||||
```
|
||||
|
||||
在循環的每次迭代中,向bz2compress傳入數據的地址和剩餘部分的長度,還有輸齣緩存w.outbuf的地址和容量。這兩個長度信息通過它們的地址傳入而不是值傳入,因爲bz2compress函數可能會根據已經壓縮的數據和壓縮後數據的大小來更新這兩個值。每個塊壓縮後的數據被寫入到底層的io.Writer。
|
||||
在循環的每次迭代中,向bz2compress傳入數據的地址和剩餘部分的長度,還有輸出緩存w.outbuf的地址和容量。這兩個長度信息通過它們的地址傳入而不是值傳入,因爲bz2compress函數可能會根據已經壓縮的數據和壓縮後數據的大小來更新這兩個值。每個塊壓縮後的數據被寫入到底層的io.Writer。
|
||||
|
||||
Close方法和Write方法有着類似的結構,通過一個循環將剩餘的壓縮數據刷新到輸齣緩存。
|
||||
Close方法和Write方法有着類似的結構,通過一個循環將剩餘的壓縮數據刷新到輸出緩存。
|
||||
|
||||
```Go
|
||||
// Close flushes the compressed data and closes the stream.
|
||||
@ -162,7 +162,7 @@ func (w *writer) Close() error {
|
||||
}
|
||||
```
|
||||
|
||||
壓縮完成後,Close方法用了defer函數確保函數退齣前調用C.BZ2_bzCompressEnd和C.bz2free釋放相關的C語言運行時資源。此刻w.stream指針將不再有效,我們將它設置爲nil以保證安全,然後在每個方法中增加了nil檢測,以防止用戶在關閉後依然錯誤使用相關方法。
|
||||
壓縮完成後,Close方法用了defer函數確保函數退出前調用C.BZ2_bzCompressEnd和C.bz2free釋放相關的C語言運行時資源。此刻w.stream指針將不再有效,我們將它設置爲nil以保證安全,然後在每個方法中增加了nil檢測,以防止用戶在關閉後依然錯誤使用相關方法。
|
||||
|
||||
上面的實現中,不僅僅寫是非併發安全的,甚至併發調用Close和Write方法也可能導致程序的的崩潰。脩複這個問題是練習13.3的內容。
|
||||
|
||||
|
@ -1,6 +1,6 @@
|
||||
# 第13章 底層編程
|
||||
|
||||
Go語言的設計包含了諸多安全策略,限製了可能導致程序運行齣現錯誤的用法。編譯時類型檢査檢査可以發現大多數類型不匹配的操作,例如兩個字符串做減法的錯誤。字符串、map、slice和chan等所有的內置類型,都有嚴格的類型轉換規則。
|
||||
Go語言的設計包含了諸多安全策略,限製了可能導致程序運行出現錯誤的用法。編譯時類型檢査檢査可以發現大多數類型不匹配的操作,例如兩個字符串做減法的錯誤。字符串、map、slice和chan等所有的內置類型,都有嚴格的類型轉換規則。
|
||||
|
||||
對於無法靜態檢測到的錯誤,例如數組訪問越界或使用空指針,運行時動態檢測可以保證程序在遇到問題的時候立卽終止併打印相關的錯誤信息。自動內存管理(垃圾內存自動迴收)可以消除大部分野指針和內存洩漏相關的問題。
|
||||
|
||||
|
@ -29,7 +29,7 @@ continue for import return var
|
||||
|
||||
這些內部預先定義的名字併不是關鍵字,你可以再定義中重新使用它們。在一些特殊的場景中重新定義它們也是有意義的,但是也要註意避免過度而引起語義混亂。
|
||||
|
||||
如果一個名字是在函數內部定義,那麽它的就隻在函數內部有效。如果是在函數外部定義,那麽將在當前包的所有文件中都可以訪問。名字的開頭字母的大小寫決定了名字在包外的可見性。如果一個名字是大寫字母開頭的(譯註:必鬚是在函數外部定義的包級名字;包級函數名本身也是包級名字),那麽它將是導齣的,也就是説可以被外部的包訪問,例如fmt包的Printf函數就是導齣的,可以在fmt包外部訪問。包本身的名字一般總是用小寫字母。
|
||||
如果一個名字是在函數內部定義,那麽它的就隻在函數內部有效。如果是在函數外部定義,那麽將在當前包的所有文件中都可以訪問。名字的開頭字母的大小寫決定了名字在包外的可見性。如果一個名字是大寫字母開頭的(譯註:必鬚是在函數外部定義的包級名字;包級函數名本身也是包級名字),那麽它將是導出的,也就是説可以被外部的包訪問,例如fmt包的Printf函數就是導出的,可以在fmt包外部訪問。包本身的名字一般總是用小寫字母。
|
||||
|
||||
名字的長度沒有邏輯限製,但是Go語言的風格是盡量使用短小的名字,對於局部變量尤其是這樣;你會經常看到i之類的短名字,而不是冗長的theLoopIndex命名。通常來説,如果一個名字的作用域比較大,生命週期也比較長,那麽用長的名字將會更有意義。
|
||||
|
||||
|
@ -2,7 +2,7 @@
|
||||
|
||||
聲明語句定義了程序的各種實體對象以及部分或全部的屬性。Go語言主要有四種類型的聲明語句:var、const、type和func,分别對應變量、常量、類型和函數實體對象的聲明。這一章我們重點討論變量和類型的聲明,第三章將討論常量的聲明,第五章將討論函數的聲明。
|
||||
|
||||
一個Go語言編寫的程序對應一個或多個以.go爲文件後綴名的源文件中。每個源文件以包的聲明語句開始,説明該源文件是屬於哪個包。包聲明語句之後是import語句導入依賴的其它包,然後是包一級的類型、變量、常量、函數的聲明語句,包一級的各種類型的聲明語句的順序無關緊要(譯註:函數內部的名字則必鬚先聲明之後纔能使用)。例如,下面的例子中聲明了一個常量、一個函數和兩個變量:
|
||||
一個Go語言編寫的程序對應一個或多個以.go爲文件後綴名的源文件中。每個源文件以包的聲明語句開始,説明該源文件是屬於哪個包。包聲明語句之後是import語句導入依賴的其它包,然後是包一級的類型、變量、常量、函數的聲明語句,包一級的各種類型的聲明語句的順序無關緊要(譯註:函數內部的名字則必鬚先聲明之後才能使用)。例如,下面的例子中聲明了一個常量、一個函數和兩個變量:
|
||||
|
||||
```Go
|
||||
gopl.io/ch2/boiling
|
||||
|
@ -63,7 +63,7 @@ f, err := os.Create(outfile) // compile error: no new variables
|
||||
|
||||
解決的方法是第二個簡短變量聲明語句改用普通的多重賦值語言。
|
||||
|
||||
簡短變量聲明語句隻有對已經在同級詞法域聲明過的變量纔和賦值操作語句等價,如果變量是在外部詞法域聲明的,那麽簡短變量聲明語句將會在當前詞法域重新聲明一個新的變量。我們在本章後面將會看到類似的例子。
|
||||
簡短變量聲明語句隻有對已經在同級詞法域聲明過的變量才和賦值操作語句等價,如果變量是在外部詞法域聲明的,那麽簡短變量聲明語句將會在當前詞法域重新聲明一個新的變量。我們在本章後面將會看到類似的例子。
|
||||
|
||||
|
||||
|
||||
|
@ -1,10 +1,10 @@
|
||||
### 2.3.2. 指針
|
||||
|
||||
一個變量對應一個保存了變量對應類型值的內存空間。普通變量在聲明語句創建時被綁定到一個變量名,比如叫x的變量,但是還有很多變量始終以表達式方式引入,例如x[i]或x.f變量。所有這些表達式一般都是讀取一個變量的值,除非它們是齣現在賦值語句的左邊,這種時候是給對應變量賦予一個新的值。
|
||||
一個變量對應一個保存了變量對應類型值的內存空間。普通變量在聲明語句創建時被綁定到一個變量名,比如叫x的變量,但是還有很多變量始終以表達式方式引入,例如x[i]或x.f變量。所有這些表達式一般都是讀取一個變量的值,除非它們是出現在賦值語句的左邊,這種時候是給對應變量賦予一個新的值。
|
||||
|
||||
一個指針的值是另一個變量的地址。一個指針對應變量在內存中的存儲位置。併不是每一個值都會有一個內存地址,但是對於每一個變量必然有對應的內存地址。通過指針,我們可以直接讀或更新對應變量的值,而不需要知道該變量的名字(如果變量有名字的話)。
|
||||
|
||||
如果用“var x int”聲明語句聲明一個x變量,那麽&x表達式(取x變量的內存地址)將産生一個指向該整數變量的指針,指針對應的數據類型是`*int`,指針被稱之爲“指向int類型的指針”。如果指針名字爲p,那麽可以説“p指針指向變量x”,或者説“p指針保存了x變量的內存地址”。同時`*p`表達式對應p指針指向的變量的值。一般`*p`表達式讀取指針指向的變量的值,這里爲int類型的值,同時因爲`*p`對應一個變量,所以該表達式也可以齣現在賦值語句的左邊,表示更新指針所指向的變量的值。
|
||||
如果用“var x int”聲明語句聲明一個x變量,那麽&x表達式(取x變量的內存地址)將産生一個指向該整數變量的指針,指針對應的數據類型是`*int`,指針被稱之爲“指向int類型的指針”。如果指針名字爲p,那麽可以説“p指針指向變量x”,或者説“p指針保存了x變量的內存地址”。同時`*p`表達式對應p指針指向的變量的值。一般`*p`表達式讀取指針指向的變量的值,這里爲int類型的值,同時因爲`*p`對應一個變量,所以該表達式也可以出現在賦值語句的左邊,表示更新指針所指向的變量的值。
|
||||
|
||||
```Go
|
||||
x := 1
|
||||
@ -18,7 +18,7 @@ fmt.Println(x) // "2"
|
||||
|
||||
變量有時候被稱爲可尋址的值。卽使變量由表達式臨時生成,那麽表達式也必鬚能接受`&`取地址操作。
|
||||
|
||||
任何類型的指針的零值都是nil。如果`p != nil`測試爲眞,那麽p是指向某個有效變量。指針之間也是可以進行相等測試的,隻有當它們指向同一個變量或全部是nil時纔相等。
|
||||
任何類型的指針的零值都是nil。如果`p != nil`測試爲眞,那麽p是指向某個有效變量。指針之間也是可以進行相等測試的,隻有當它們指向同一個變量或全部是nil時才相等。
|
||||
|
||||
```Go
|
||||
var x, y int
|
||||
|
@ -30,7 +30,7 @@ for t := 0.0; t < cycles*2*math.Pi; t += res {
|
||||
|
||||
那麽垃Go語言的自動圾收集器是如何知道一個變量是何時可以被迴收的呢?這里我們可以避開完整的技術細節,基本的實現思路是,從每個包級的變量和每個當前運行函數的每一個局部變量開始,通過指針或引用的訪問路徑遍歷,是否可以找到該變量。如果不存在這樣的訪問路徑,那麽説明該變量是不可達的,也就是説它是否存在併不會影響程序後續的計算結果。
|
||||
|
||||
因爲一個變量的有效週期隻取決於是否可達,因此一個循環迭代內部的局部變量的生命週期可能超齣其局部作用域。同時,局部變量可能在函數返迴之後依然存在。
|
||||
因爲一個變量的有效週期隻取決於是否可達,因此一個循環迭代內部的局部變量的生命週期可能超出其局部作用域。同時,局部變量可能在函數返迴之後依然存在。
|
||||
|
||||
編譯器會自動選擇在棧上還是在堆上分配局部變量的存儲空間,但可能令人驚訝的是,這個選擇併不是由用var還是new聲明變量的方式決定的。
|
||||
|
||||
@ -44,7 +44,7 @@ func f() { func g() {
|
||||
}
|
||||
```
|
||||
|
||||
這里的x變量必鬚在堆上分配,因爲它在函數退齣後依然可以通過包一級的global變量找到,雖然它是在函數內部定義的;用Go語言的術語説,這個x局部變量從函數f中逃逸了。相反,當g函數返迴時,變量`*y`將是不可達的,也就是説可以馬上被迴收的。因此,`*y`併沒有從函數g中逃逸,編譯器可以選擇在棧上分配`*y`的存儲空間(譯註:也可以選擇在堆上分配,然後由Go語言的GC迴收這個變量的內存空間),雖然這里用的是new方式。其實在任何時候,你併不需爲了編寫正確的代碼而要考慮變量的逃逸行爲,要記住的是,逃逸的變量需要額外分配內存,同時對性能的優化可能會産生細微的影響。
|
||||
這里的x變量必鬚在堆上分配,因爲它在函數退出後依然可以通過包一級的global變量找到,雖然它是在函數內部定義的;用Go語言的術語説,這個x局部變量從函數f中逃逸了。相反,當g函數返迴時,變量`*y`將是不可達的,也就是説可以馬上被迴收的。因此,`*y`併沒有從函數g中逃逸,編譯器可以選擇在棧上分配`*y`的存儲空間(譯註:也可以選擇在堆上分配,然後由Go語言的GC迴收這個變量的內存空間),雖然這里用的是new方式。其實在任何時候,你併不需爲了編寫正確的代碼而要考慮變量的逃逸行爲,要記住的是,逃逸的變量需要額外分配內存,同時對性能的優化可能會産生細微的影響。
|
||||
|
||||
Go語言的自動垃圾收集器對編寫正確的代碼是一個鉅大的幫助,但也併不是説你完全不用考慮內存了。你雖然不需要顯式地分配和釋放內存,但是要編寫高效的程序你依然需要了解變量的生命週期。例如,如果將指向短生命週期對象的指針保存到具有長生命週期的對象中,特别是保存到全局變量時,會阻止對短生命週期對象的垃圾迴收(從而可能影響程序的性能)。
|
||||
|
||||
|
@ -1,6 +1,6 @@
|
||||
### 2.4.1. 元組賦值
|
||||
|
||||
元組賦值是另一種形式的賦值語句,它允許同時更新多個變量的值。在賦值之前,賦值語句右邊的所有表達式將會先進行求值,然後再統一更新左邊對應變量的值。這對於處理有些同時齣現在元組賦值語句左右兩邊的變量很有幫助,例如我們可以這樣交換兩個變量的值:
|
||||
元組賦值是另一種形式的賦值語句,它允許同時更新多個變量的值。在賦值之前,賦值語句右邊的所有表達式將會先進行求值,然後再統一更新左邊對應變量的值。這對於處理有些同時出現在元組賦值語句左右兩邊的變量很有幫助,例如我們可以這樣交換兩個變量的值:
|
||||
|
||||
```Go
|
||||
x, y = y, x
|
||||
@ -39,13 +39,13 @@ i, j, k = 2, 3, 5
|
||||
|
||||
但如果表達式太複雜的話,應該盡量避免過度使用元組賦值;因爲每個變量單獨賦值語句的寫法可讀性會更好。
|
||||
|
||||
有些表達式會産生多個值,比如調用一個有多個返迴值的函數。當這樣一個函數調用齣現在元組賦值右邊的表達式中時(譯註:右邊不能再有其它表達式),左邊變量的數目必鬚和右邊一致。
|
||||
有些表達式會産生多個值,比如調用一個有多個返迴值的函數。當這樣一個函數調用出現在元組賦值右邊的表達式中時(譯註:右邊不能再有其它表達式),左邊變量的數目必鬚和右邊一致。
|
||||
|
||||
```Go
|
||||
f, err = os.Open("foo.txt") // function call returns two values
|
||||
```
|
||||
|
||||
通常,這類函數會用額外的返迴值來表達某種錯誤類型,例如os.Open是用額外的返迴值返迴一個error類型的錯誤,還有一些是用來返迴布爾值,通常被稱爲ok。在稍後我們將看到的三個操作都是類似的用法。如果map査找(§4.3)、類型斷言(§7.10)或通道接收(§8.4.2)齣現在賦值語句的右邊,它們都可能會産生兩個結果,有一個額外的布爾結果表示操作是否成功:
|
||||
通常,這類函數會用額外的返迴值來表達某種錯誤類型,例如os.Open是用額外的返迴值返迴一個error類型的錯誤,還有一些是用來返迴布爾值,通常被稱爲ok。在稍後我們將看到的三個操作都是類似的用法。如果map査找(§4.3)、類型斷言(§7.10)或通道接收(§8.4.2)出現在賦值語句的右邊,它們都可能會産生兩個結果,有一個額外的布爾結果表示操作是否成功:
|
||||
|
||||
```Go
|
||||
v, ok = m[key] // map lookup
|
||||
@ -53,7 +53,7 @@ v, ok = x.(T) // type assertion
|
||||
v, ok = <-ch // channel receive
|
||||
```
|
||||
|
||||
譯註:map査找(§4.3)、類型斷言(§7.10)或通道接收(§8.4.2)齣現在賦值語句的右邊時,併不一定是産生兩個結果,也可能隻産生一個結果。對於值産生一個結果的情形,map査找失敗時會返迴零值,類型斷言失敗時會發送運行時panic異常,通道接收失敗時會返迴零值(阻塞不算是失敗)。例如下面的例子:
|
||||
譯註:map査找(§4.3)、類型斷言(§7.10)或通道接收(§8.4.2)出現在賦值語句的右邊時,併不一定是産生兩個結果,也可能隻産生一個結果。對於值産生一個結果的情形,map査找失敗時會返迴零值,類型斷言失敗時會發送運行時panic異常,通道接收失敗時會返迴零值(阻塞不算是失敗)。例如下面的例子:
|
||||
|
||||
```Go
|
||||
v = m[key] // map査找,失敗時返迴零值
|
||||
|
@ -16,7 +16,7 @@ medals[2] = "bronze"
|
||||
|
||||
map和chan的元素,雖然不是普通的變量,但是也有類似的隱式賦值行爲。
|
||||
|
||||
不管是隱式還是顯式地賦值,在賦值語句左邊的變量和右邊最終的求到的值必鬚有相同的數據類型。更直白地説,隻有右邊的值對於左邊的變量是可賦值的,賦值語句纔是允許的。
|
||||
不管是隱式還是顯式地賦值,在賦值語句左邊的變量和右邊最終的求到的值必鬚有相同的數據類型。更直白地説,隻有右邊的值對於左邊的變量是可賦值的,賦值語句才是允許的。
|
||||
|
||||
可賦值性的規則對於不同類型有着不同要求,對每個新類型特殊的地方我們會專門解釋。對於目前我們已經討論過的類型,它的規則是簡單的:類型必鬚完全匹配,nil可以賦值給任何指針或引用類型的變量。常量(§3.6)則有更靈活的賦值規則,因爲這樣可以避免不必要的顯式的類型轉換。
|
||||
|
||||
|
@ -10,9 +10,9 @@
|
||||
type 類型名字 底層類型
|
||||
```
|
||||
|
||||
類型聲明語句一般齣現在包一級,因此如果新創建的類型名字的首字符大寫,則在外部包也可以使用。
|
||||
類型聲明語句一般出現在包一級,因此如果新創建的類型名字的首字符大寫,則在外部包也可以使用。
|
||||
|
||||
譯註:對於中文漢字,Unicode標誌都作爲小寫字母處理,因此中文的命名默認不能導齣;不過國內的用戶針對該問題提齣了我們自己的間接,根據RobPike的迴複,在Go2中有可能會將中日韓等字符當作大寫字母處理。下面是RobPik在 [Issue763](https://github.com/golang/go/issues/5763) 的迴複:
|
||||
譯註:對於中文漢字,Unicode標誌都作爲小寫字母處理,因此中文的命名默認不能導出;不過國內的用戶針對該問題提出了我們自己的間接,根據RobPike的迴複,在Go2中有可能會將中日韓等字符當作大寫字母處理。下面是RobPik在 [Issue763](https://github.com/golang/go/issues/5763) 的迴複:
|
||||
|
||||
> A solution that's been kicking around for a while:
|
||||
>
|
||||
@ -41,13 +41,13 @@ func CToF(c Celsius) Fahrenheit { return Fahrenheit(c*9/5 + 32) }
|
||||
func FToC(f Fahrenheit) Celsius { return Celsius((f - 32) * 5 / 9) }
|
||||
```
|
||||
|
||||
我们在這個包声明了兩種類型:Celsius和Fahrenheit分别對應不同的溫度單位。它們虽然有着相同的底層類型float64,但是它們是不同的數據類型,因此它們不可以被相互比較或混在一個表達式运算。刻意區分類型,可以避免一些像無意中使用不同單位的溫度混合計算导致的錯誤;因此需要一個類似Celsius(t)或Fahrenheit(t)形式的顯式轉型操作纔能將float64轉爲對應的類型。Celsius(t)和Fahrenheit(t)是類型轉換操作,它们併不是函數調用。類型轉換不會改變值本身,但是會使它們的語義發生變化。另一方面,CToF和FToC两个函数則是對不同溫度單位下的温度进行换算,它們會返迴不同的值。
|
||||
我們在這個包聲明了兩種類型:Celsius和Fahrenheit分别對應不同的溫度單位。它們雖然有着相同的底層類型float64,但是它們是不同的數據類型,因此它們不可以被相互比較或混在一個表達式運算。刻意區分類型,可以避免一些像無意中使用不同單位的溫度混合計算導致的錯誤;因此需要一個類似Celsius(t)或Fahrenheit(t)形式的顯式轉型操作才能將float64轉爲對應的類型。Celsius(t)和Fahrenheit(t)是類型轉換操作,它們併不是函數調用。類型轉換不會改變值本身,但是會使它們的語義發生變化。另一方面,CToF和FToC兩個函數則是對不同溫度單位下的溫度進行換算,它們會返迴不同的值。
|
||||
|
||||
對於每一個類型T,都有一個對應的類型轉換操作T(x),用於將x轉爲T類型(译注:如果T是指针类型,可能会需要用小括弧包装T,比如`(*int)(0)`)。隻有當兩個類型的底層基礎類型相同時,才允許這種轉型操作,或者是兩者都是指向相同底層結構的指針類型,這些轉換隻改變類型而不會影響值本身。如果x是可以賦值給T類型的值,那麽x必然也可以被轉爲T類型,但是一般沒有这个必要。
|
||||
對於每一個類型T,都有一個對應的類型轉換操作T(x),用於將x轉爲T類型(譯註:如果T是指針類型,可能會需要用小括弧包裝T,比如`(*int)(0)`)。隻有當兩個類型的底層基礎類型相同時,才允許這種轉型操作,或者是兩者都是指向相同底層結構的指針類型,這些轉換隻改變類型而不會影響值本身。如果x是可以賦值給T類型的值,那麽x必然也可以被轉爲T類型,但是一般沒有這個必要。
|
||||
|
||||
數值類型之間的轉型也是允許的,併且在字符串和一些特定类型的slice之間也是可以轉換的,在下一章我們會看到這樣的例子。這類轉換可能改變值的表現。例如,將一個浮點數轉爲整數將丟棄小數部分,將一個字符串轉爲`[]byte`类型的slice將拷貝一個字符串數據的副本。在任何情況下,運行時不會發生轉換失敗的錯誤(譯註: 錯誤隻會發生在編譯階段)。
|
||||
數值類型之間的轉型也是允許的,併且在字符串和一些特定類型的slice之間也是可以轉換的,在下一章我們會看到這樣的例子。這類轉換可能改變值的表現。例如,將一個浮點數轉爲整數將丟棄小數部分,將一個字符串轉爲`[]byte`類型的slice將拷貝一個字符串數據的副本。在任何情況下,運行時不會發生轉換失敗的錯誤(譯註: 錯誤隻會發生在編譯階段)。
|
||||
|
||||
底層數據類型決定了內部結構和表達方式,也決定是否可以像底層類型一樣對內置運算符的支持。這意味着,Celsius和Fahrenheit類型的算術运算行爲和底層的float64類型是一樣的,正如我们所期望的那样。
|
||||
底層數據類型決定了內部結構和表達方式,也決定是否可以像底層類型一樣對內置運算符的支持。這意味着,Celsius和Fahrenheit類型的算術運算行爲和底層的float64類型是一樣的,正如我們所期望的那樣。
|
||||
|
||||
```Go
|
||||
fmt.Printf("%g\n", BoilingC-FreezingC) // "100" °C
|
||||
@ -69,17 +69,17 @@ fmt.Println(c == Celsius(f)) // "true"!
|
||||
|
||||
註意最後那個語句。盡管看起來想函數調用,但是Celsius(f)是類型轉換操作,它併不會改變值,僅僅是改變值的類型而已。測試爲眞的原因是因爲c和g都是零值。
|
||||
|
||||
一個命名的類型可以提供书写方便,特别是可以避免一遍又一遍地書寫複雜類型(譯註:例如用匿名的結構體定義變量)。雖然對於像float64這種簡單的底層類型沒有簡潔很多,但是如果是複雜的類型將會簡潔很多,特别是我們卽將討論的結構體類型。
|
||||
一個命名的類型可以提供書寫方便,特别是可以避免一遍又一遍地書寫複雜類型(譯註:例如用匿名的結構體定義變量)。雖然對於像float64這種簡單的底層類型沒有簡潔很多,但是如果是複雜的類型將會簡潔很多,特别是我們卽將討論的結構體類型。
|
||||
|
||||
命名類型還可以爲該類型的值定義新的行为。這些行爲表示爲一組關聯到该類型的函數集合,我們称爲類型的方法集。我們將在第六章中討論方法的細節,這里值説寫簡單用法。
|
||||
命名類型還可以爲該類型的值定義新的行爲。這些行爲表示爲一組關聯到該類型的函數集合,我們稱爲類型的方法集。我們將在第六章中討論方法的細節,這里值説寫簡單用法。
|
||||
|
||||
下面的聲明语句,Celsius類型的參數c齣現在了函數名的前面,表示聲明的是Celsius類型的一个叫名叫String的方法,该方法返迴该类型对象c帶着°C溫度單位的字符串:
|
||||
下面的聲明語句,Celsius類型的參數c出現在了函數名的前面,表示聲明的是Celsius類型的一個叫名叫String的方法,該方法返迴該類型對象c帶着°C溫度單位的字符串:
|
||||
|
||||
```Go
|
||||
func (c Celsius) String() string { return fmt.Sprintf("%g°C", c) }
|
||||
```
|
||||
|
||||
許多類型都會定義一個String方法,因爲當使用fmt包的打印方法時,將會優先使用该类型对应的String方法返迴的結果打印,我们將在7.1節講述。
|
||||
許多類型都會定義一個String方法,因爲當使用fmt包的打印方法時,將會優先使用該類型對應的String方法返迴的結果打印,我們將在7.1節講述。
|
||||
|
||||
```Go
|
||||
c := FToC(212.0)
|
||||
|
@ -34,7 +34,7 @@ func main() {
|
||||
}
|
||||
```
|
||||
|
||||
導入聲明將導入的包綁定到一個短小的名字, 然後通過該名字就可以引用包中導齣的全部內容. 上面的導入聲明將允許我們以 tempconv.CToF 的方式來訪問 gopl.io/ch2/tempconv 包中的內容. 默認情況下, 導入的包綁定到 tempconv 名字, 但是我們也可以綁定到另一個名稱, 以避免名字衝突(§10.3).
|
||||
導入聲明將導入的包綁定到一個短小的名字, 然後通過該名字就可以引用包中導出的全部內容. 上面的導入聲明將允許我們以 tempconv.CToF 的方式來訪問 gopl.io/ch2/tempconv 包中的內容. 默認情況下, 導入的包綁定到 tempconv 名字, 但是我們也可以綁定到另一個名稱, 以避免名字衝突(§10.3).
|
||||
|
||||
cf 程序將命令行輸入的一個溫度在 Celsius 和 Fahrenheit 之間轉換:
|
||||
|
||||
@ -48,7 +48,7 @@ $ ./cf -40
|
||||
-40°F = -40°C, -40°C = -40°F
|
||||
```
|
||||
|
||||
如果導入一個包, 但是沒有使用該包將被當作一個錯誤. 這種強製檢測可以有效減少不必要的依賴, 雖然在調試期間會讓人討厭, 因爲刪除一個類似 log.Print("got here!") 的打印可能導致需要同時刪除 log 包導入聲明, 否則, 編譯器將會發齣一個錯誤. 在這種情況下, 我們需要將不必要的導入刪除或註釋掉.
|
||||
如果導入一個包, 但是沒有使用該包將被當作一個錯誤. 這種強製檢測可以有效減少不必要的依賴, 雖然在調試期間會讓人討厭, 因爲刪除一個類似 log.Print("got here!") 的打印可能導致需要同時刪除 log 包導入聲明, 否則, 編譯器將會發出一個錯誤. 在這種情況下, 我們需要將不必要的導入刪除或註釋掉.
|
||||
|
||||
不過有更好的解決方案, 我們可以使用 golang.org/x/tools/cmd/goimports 工具, 它可以根據需要自動添加或刪除導入的包; 許多編輯器都可以集成 goimports 工具, 然後在保存文件的時候自動允許它. 類似的還有 gofmt 工具, 可以用來格式化Go源文件.
|
||||
|
||||
|
@ -1,6 +1,6 @@
|
||||
### 2.6.2. 包的初始化
|
||||
|
||||
包的初始化首先是解決包級變量的依賴順序, 然後安裝包級變量聲明齣現的順序依次初始化:
|
||||
包的初始化首先是解決包級變量的依賴順序, 然後安裝包級變量聲明出現的順序依次初始化:
|
||||
|
||||
```Go
|
||||
var a = b + c // a 第三個初始化, 爲 3
|
||||
|
@ -4,7 +4,7 @@ Go語言中的包和其他語言的庫或模塊概念類似, 目的都是爲了
|
||||
|
||||
每個包作爲一個獨立的名字空間. 例如, 在 image 包中的 Decode 函數 和 unicode/utf16 包中的 Decode 函數是不同的. 要在外部包引用該函數, 必鬚顯式使用 image.Decode 或 utf16.Decode 訪問.
|
||||
|
||||
包可以讓我們通過控製那些名字是外部可見的來隱藏信息. 在Go中, 一個簡單的規則是: 如果一個名字是大寫字母開頭的, 那麽該名字是導齣的.
|
||||
包可以讓我們通過控製那些名字是外部可見的來隱藏信息. 在Go中, 一個簡單的規則是: 如果一個名字是大寫字母開頭的, 那麽該名字是導出的.
|
||||
|
||||
爲了演示基本的用法, 假設我們的溫度轉換軟件已經很流行, 我們希望到Go社區也能使用這個包. 我們該如何做呢?
|
||||
|
||||
|
@ -112,7 +112,7 @@ if f, err := os.Open(fname); err != nil {
|
||||
|
||||
但這不是Go推薦的做法, Go的習慣是在if中處理錯誤然後直接返迴, 這樣可以確保正常成功執行的語句不需要代碼縮進.
|
||||
|
||||
要特别註意短的變量聲明的作用域范圍, 考慮下面的程序, 它的目的是穫取當前的工作目録然後保存到一個包級的變量中. 這可以通過直接調用 os.Getwd 完成, 但是將這個從主邏輯中分離齣來可能會更好, 特别是在需要處理錯誤的時候. 函數 log.Fatalf 打印信息, 然後調用 os.Exit(1) 終止程序.
|
||||
要特别註意短的變量聲明的作用域范圍, 考慮下面的程序, 它的目的是穫取當前的工作目録然後保存到一個包級的變量中. 這可以通過直接調用 os.Getwd 完成, 但是將這個從主邏輯中分離出來可能會更好, 特别是在需要處理錯誤的時候. 函數 log.Fatalf 打印信息, 然後調用 os.Exit(1) 終止程序.
|
||||
|
||||
```Go
|
||||
var cwd string
|
||||
@ -141,9 +141,9 @@ func init() {
|
||||
}
|
||||
```
|
||||
|
||||
全局的cwd變量依然是沒有被正確初始化的, 而且看似正常的日誌輸齣更是這個BUG更加隱晦.
|
||||
全局的cwd變量依然是沒有被正確初始化的, 而且看似正常的日誌輸出更是這個BUG更加隱晦.
|
||||
|
||||
有許多方式可以避免齣現類似潛在的問題. 最直接的是通過單獨聲明err變量, 來避免使用 `:=` 的簡短聲明方式:
|
||||
有許多方式可以避免出現類似潛在的問題. 最直接的是通過單獨聲明err變量, 來避免使用 `:=` 的簡短聲明方式:
|
||||
|
||||
```Go
|
||||
var cwd string
|
||||
|
@ -8,7 +8,7 @@ Go同時提供了有符號和無符號的整數運算. 這里有四種int8, int1
|
||||
|
||||
字符rune類型是和int32等價的類型, 通常用於表示一個Unicode碼點. 這兩個名稱可以互換使用. 同樣byte也是uint8類型的等價類型, byte類型用於強調數值是一個原始的數據而不是一個小的整數.
|
||||
|
||||
最好, 還有一個無符號的整數類型 uintptr, 沒有指定具體的bit大小但是足以容納指針. uintptr 類型隻有在底層編程是纔需要, 特别是Go語言和C函數庫或操作繫統相交互的地方. 我們將在第十三章的 unsafe 包相關部分看到類似的例子.
|
||||
最好, 還有一個無符號的整數類型 uintptr, 沒有指定具體的bit大小但是足以容納指針. uintptr 類型隻有在底層編程是才需要, 特别是Go語言和C函數庫或操作繫統相交互的地方. 我們將在第十三章的 unsafe 包相關部分看到類似的例子.
|
||||
|
||||
不管它們的大小, int, uint, 和 uintptr 是不同類型大小的兄弟類型. 其中 int 和 int32 也是不同的類型, 卽使int的大小也是32bit, 在需要將int當作int32類型的地方需要一個顯式的類型轉換, 反之亦然.
|
||||
|
||||
@ -31,7 +31,7 @@ Go同時提供了有符號和無符號的整數運算. 這里有四種int8, int1
|
||||
整數的算術運算符 +, -, *, 和 / 可以適用與整數, 浮點數和複數, 但是取模運算符 % 僅用於整數. 不同編程語言間, % 取模運算的行爲併不相同. 在Go語言中, % 取模運算符的符號和被取模數的符號總是一致的, 因此 `-5%3` 和 `-5%-3` 結果都是 -2.除法運算符 `/` 的行爲依賴於操作數是否爲整數, 因此 `5.0/4.0` 的結果是 1.25, 但是 5/4 的結果是 1, 因此整數除法會向着0方向截斷餘數.
|
||||
|
||||
|
||||
如果一個算術運算的結果, 不管是有符號或者是無符號的, 如果需要更多的bit位纔能表示, 就説明是溢齣了. 超齣的高位的bit位部分將被丟棄. 如果原始的數值是有符號類型, 那麽最終結果可能是負的, 如果最左邊的bit爲是1的話, 例如int8的例子:
|
||||
如果一個算術運算的結果, 不管是有符號或者是無符號的, 如果需要更多的bit位才能表示, 就説明是溢出了. 超出的高位的bit位部分將被丟棄. 如果原始的數值是有符號類型, 那麽最終結果可能是負的, 如果最左邊的bit爲是1的話, 例如int8的例子:
|
||||
|
||||
```Go
|
||||
var u uint8 = 255
|
||||
@ -102,13 +102,13 @@ fmt.Printf("%08b\n", x<<1) // "01000100", the set {2, 6}
|
||||
fmt.Printf("%08b\n", x>>1) // "00010001", the set {0, 4}
|
||||
```
|
||||
|
||||
(6.5節給齣了一個可以遠大於一個字節的整數集的實現.)
|
||||
(6.5節給出了一個可以遠大於一個字節的整數集的實現.)
|
||||
|
||||
在 x<<n 和 x>>n 移位運算中, 決定了移位操作bit數部分必鬚是無符號數; 被操作的 x 數可以是有符號或無符號數. 算術上, 一個 x<<n 左移運算等價於乘以 2^n, 一個 x>>n 右移運算等價於除以 2^n.
|
||||
|
||||
左移運算用零填充右邊空缺的bit位, 無符號數的右移運算也是用0填充左邊空缺的bit位, 但是有符號數的右移運算會用符號位的值填充左邊空缺的bit位. 因爲這個原因, 最好用無符號運算, 這樣你可以將整數完全當作一個bit位模式處理.
|
||||
|
||||
盡管Go提供了無符號數和運算, 卽使數值本身不可能齣現負數我們還是傾向於使用有符號的int類型, 就是數組的長度那樣, 雖然使用 uint 似乎是一個更合理的選擇. 事實上, 內置的 len 函數返迴一個有符號的int, 我們可以像下面這個逆序循環那樣處理.
|
||||
盡管Go提供了無符號數和運算, 卽使數值本身不可能出現負數我們還是傾向於使用有符號的int類型, 就是數組的長度那樣, 雖然使用 uint 似乎是一個更合理的選擇. 事實上, 內置的 len 函數返迴一個有符號的int, 我們可以像下面這個逆序循環那樣處理.
|
||||
|
||||
```Go
|
||||
medals := []string{"gold", "silver", "bronze"}
|
||||
@ -119,7 +119,7 @@ for i := len(medals) - 1; i >= 0; i-- {
|
||||
|
||||
另一個選擇將是災難性的. 如果 len 返迴一個無符號數, 那麽 i 也將是無符號的 uint, 然後條件 i >= 0 則永遠爲眞. 在三次迭代之後, 也就是 i == 0 時, i-- 語句將不會産生 -1, 而是變成一個uint的最大值(可能是 2^64 - 1), 然後 medals[i] 表達式將發生運行時 panic 異常(§5.9), 也就是試圖訪問一個切片范圍以外的元素.
|
||||
|
||||
齣於這個原因, 無符號數往往隻有在位運算或其它特殊的運算常見纔會使用, 就像 bit 集合, 分形二進製文件格式, 或者是哈希和加密操作等. 它們通常併不用於僅僅是表達非負數量的場合.
|
||||
出於這個原因, 無符號數往往隻有在位運算或其它特殊的運算常見才會使用, 就像 bit 集合, 分形二進製文件格式, 或者是哈希和加密操作等. 它們通常併不用於僅僅是表達非負數量的場合.
|
||||
|
||||
一般來説, 需要一個顯式的轉換將一個值從一種類型轉化位另一種類型, 併且算術和邏輯運算的二元操作中必鬚是相同的類型. 雖然這偶爾會導致很長的表達式, 但是它消除了所有的類型相關的問題, 也使得程序容易理解.
|
||||
|
||||
@ -162,7 +162,7 @@ i := int(f) // 結果依賴於具體實現
|
||||
|
||||
任何大小的整數字面值都可以用以0開始的八進製格式書寫, 例如 0666, 或用以0x或0X開頭的十六進製格式書寫, 例如 0xdeadbeef. 十六進製數字可以用大寫或小寫字母. 如今八進製數據通常用於POSIX操作繫統上的文件訪問權限標誌, 十六進製數字則更強調數字值的bit位模式.
|
||||
|
||||
當使用 fmt 包打印一個數值時, 我們可以用 %d, %o, 或 %x 控製輸齣的進製格式, 就像下面的例子:
|
||||
當使用 fmt 包打印一個數值時, 我們可以用 %d, %o, 或 %x 控製輸出的進製格式, 就像下面的例子:
|
||||
|
||||
```Go
|
||||
o := 0666
|
||||
@ -173,7 +173,7 @@ fmt.Printf("%d %[1]x %#[1]x %#[1]X\n", x)
|
||||
// 3735928559 deadbeef 0xdeadbeef 0XDEADBEEF
|
||||
```
|
||||
|
||||
請註意 fmt 的兩個使用技巧. 通常 Printf 格式化字符串包含多個 % 參數時將對應相同數量的額外操作數, 但是 % 之後的 `[1]` 副詞告訴Printf函數再次使用第一個操作數. 第二, % 後的 `#` 副詞告訴 Printf 在用 %o, %x 或 %X 輸齣時生成 0, 0x 或 0X前綴.
|
||||
請註意 fmt 的兩個使用技巧. 通常 Printf 格式化字符串包含多個 % 參數時將對應相同數量的額外操作數, 但是 % 之後的 `[1]` 副詞告訴Printf函數再次使用第一個操作數. 第二, % 後的 `#` 副詞告訴 Printf 在用 %o, %x 或 %X 輸出時生成 0, 0x 或 0X前綴.
|
||||
|
||||
字符面值通過一對單引號直接包含對應字符. 最簡單的例子是 ASCII 中類似 'a' 字符面值, 但是我們可以通過轉義的數值來表示任意的Unicode碼點對應的字符, 馬上將會看到例子.
|
||||
|
||||
|
@ -46,7 +46,7 @@ x = 6 e^x = 403.429
|
||||
x = 7 e^x = 1096.633
|
||||
```
|
||||
|
||||
math 包中除了提供大量常用的數學函數外, 還提供了IEEE754標準中特殊數值的創建和測試: 正無窮大和負無窮大, 分别用於表示太大溢齣的數字和除零的結果; 還有 NaN 非數, 一般用於表示無效的除法操作結果 0/0 或 Sqrt(-1).
|
||||
math 包中除了提供大量常用的數學函數外, 還提供了IEEE754標準中特殊數值的創建和測試: 正無窮大和負無窮大, 分别用於表示太大溢出的數字和除零的結果; 還有 NaN 非數, 一般用於表示無效的除法操作結果 0/0 或 Sqrt(-1).
|
||||
|
||||
```Go
|
||||
var z float64
|
||||
@ -72,7 +72,7 @@ func compute() (value float64, ok bool) {
|
||||
}
|
||||
```
|
||||
|
||||
接下來的程序演示了浮點計算圖形. 它是帶有兩個參數的 z = f(x, y) 函數的三維形式, 使用了可縮放矢量圖形(SVG)格式輸齣, 一個用於矢量線繪製的XML標準. 圖3.1顯示了 sin(r)/r 函數的輸齣圖形, 其中 r 是 sqrt(x*x+y*y).
|
||||
接下來的程序演示了浮點計算圖形. 它是帶有兩個參數的 z = f(x, y) 函數的三維形式, 使用了可縮放矢量圖形(SVG)格式輸出, 一個用於矢量線繪製的XML標準. 圖3.1顯示了 sin(r)/r 函數的輸出圖形, 其中 r 是 sqrt(x*x+y*y).
|
||||
|
||||
![](../images/ch3-01.png)
|
||||
|
||||
@ -147,11 +147,11 @@ func f(x, y float64) float64 {
|
||||
|
||||
(x,y,z) 投影到二維的畵布中. 畵布中從遠處到右邊的點對應較大的x值和較大的y值. 併且畵布中x和y值越大, 則對應的z值越小. x和y的垂直和水平縮放繫數來自30度角的正絃和餘絃值. z的縮放繫數0.4, 是一個任意選擇的參數.
|
||||
|
||||
對於二維網格中的每一個單位, main函數計算單元的四個頂點在畵布中對應多邊形ABCD的頂點, 其中B對應(i,j)頂點位置, A, C, 和 D是相鄰的頂點, 然後輸齣SVG的繪製指令.
|
||||
對於二維網格中的每一個單位, main函數計算單元的四個頂點在畵布中對應多邊形ABCD的頂點, 其中B對應(i,j)頂點位置, A, C, 和 D是相鄰的頂點, 然後輸出SVG的繪製指令.
|
||||
|
||||
**練習3.1:** 如果 f 函數返迴的是無限製的 float64 值, 那麽SVG文件可能輸齣無效的<polygon>多邊形元素(雖然許多SVG渲染器會妥善處理這類問題). 脩改程序跳過無效的多邊形.
|
||||
**練習3.1:** 如果 f 函數返迴的是無限製的 float64 值, 那麽SVG文件可能輸出無效的<polygon>多邊形元素(雖然許多SVG渲染器會妥善處理這類問題). 脩改程序跳過無效的多邊形.
|
||||
|
||||
**練習3.2:** 試驗math包中其他函數的渲染圖形. 你是否能輸齣一個egg box, moguls, 或 a saddle 圖案?
|
||||
**練習3.2:** 試驗math包中其他函數的渲染圖形. 你是否能輸出一個egg box, moguls, 或 a saddle 圖案?
|
||||
|
||||
**練習3.3:**根據高度給每個多邊形上色, 那樣峯值部將是紅色(#ff0000), 谷部將是藍色(#0000ff).
|
||||
|
||||
@ -161,6 +161,6 @@ func f(x, y float64) float64 {
|
||||
w.Header().Set("Content-Type", "image/svg+xml")
|
||||
```
|
||||
|
||||
(這一步在Lissajous例子中不是必鬚的, 因爲服務器使用標準的PNG圖像格式, 可以根據前面的512個字節自動輸齣對應的頭部.) 允許客戶端通過HTTP請求參數設置高度, 寬度, 和顔色等參數.
|
||||
(這一步在Lissajous例子中不是必鬚的, 因爲服務器使用標準的PNG圖像格式, 可以根據前面的512個字節自動輸出對應的頭部.) 允許客戶端通過HTTP請求參數設置高度, 寬度, 和顔色等參數.
|
||||
|
||||
|
||||
|
@ -24,7 +24,7 @@ x := 1 + 2i
|
||||
y := 3 + 4i
|
||||
```
|
||||
|
||||
複數也可以用 == 和 != 進行相等比較. 隻有兩個複數的實部和虛部都相等的時候它們纔是相等的.
|
||||
複數也可以用 == 和 != 進行相等比較. 隻有兩個複數的實部和虛部都相等的時候它們才是相等的.
|
||||
|
||||
math/cmplx 包提供了複數處理的許多函數, 例如求複數的平方根函數和求冪函數.
|
||||
|
||||
@ -83,7 +83,7 @@ func mandelbrot(z complex128) color.Color {
|
||||
}
|
||||
```
|
||||
|
||||
遍歷1024x1024圖像每個點的兩個嵌套的循環對應 -2 到 +2 區間的複數平面. 程序反複測試每個點對應複數值平方值加一個增量值對應的點是否超齣半徑爲2的圓. 如果超過了, 通過根據逃逸的迭代次數對應的灰度顔色來代替. 如果不是, 該點屬於Mandelbrot集合, 使用黑色顔色標記. 最終程序將生成的PNG格式分形圖像圖像輸齣到標準輸齣, 如圖3.3所示.
|
||||
遍歷1024x1024圖像每個點的兩個嵌套的循環對應 -2 到 +2 區間的複數平面. 程序反複測試每個點對應複數值平方值加一個增量值對應的點是否超出半徑爲2的圓. 如果超過了, 通過根據逃逸的迭代次數對應的灰度顔色來代替. 如果不是, 該點屬於Mandelbrot集合, 使用黑色顔色標記. 最終程序將生成的PNG格式分形圖像圖像輸出到標準輸出, 如圖3.3所示.
|
||||
|
||||
**練習3.5:** 實現一個綵色的Mandelbrot圖像, 使用 image.NewRGBA 創建圖像, 使用 color.RGBA 或 color.YCbCr 生成顔色.
|
||||
|
||||
|
@ -12,7 +12,7 @@ UTF8是一個將Unicode碼點編碼爲字節序列的變長編碼. UTF8編碼由
|
||||
|
||||
變長的編碼無法直接通過索引來訪問第n個字符, 但是UTF8穫得了很多額外的優點. 首先UTF8編碼比較緊湊, 兼容ASCII, 併且可以自動同步: 它可以通過向前迴朔最多2個字節就能確定當前字符編碼的開始字節的位置. 它也是一個前綴編碼, 所以當從左向右解碼時不會有任何歧義也併不需要向前査看. 沒有任何字符的編碼是其它字符編碼的子串, 或是其它編碼序列的字串, 因此蒐索一個字符時隻要蒐索它的字節編碼序列卽可, 不用擔心前後的上下文會對蒐索結果産生榦擾. 同時UTF8編碼的順序和Unicode碼點的順序一致, 因此可以直接排序UTF8編碼序列. 同業也沒有嵌入的NUL(0)字節, 可以很好地兼容那些使用NUL作爲字符串結尾的編程語言.
|
||||
|
||||
Go的源文件采用UTF8編碼, 併且Go處理UTF8編碼的文本也很齣色. unicode 包提供了諸多處理 rune 字符相關功能的函數函數(區分字母和數組, 或者是字母的大寫和小寫轉換等), unicode/utf8 包了提供了rune 字符序列的UTF8編碼和解碼的功能.
|
||||
Go的源文件采用UTF8編碼, 併且Go處理UTF8編碼的文本也很出色. unicode 包提供了諸多處理 rune 字符相關功能的函數函數(區分字母和數組, 或者是字母的大寫和小寫轉換等), unicode/utf8 包了提供了rune 字符序列的UTF8編碼和解碼的功能.
|
||||
|
||||
有很多Unicode字符很難直接從鍵盤輸入, 併且很多字符有着相似的結構; 有一些甚至是不可見的字符. Go字符串面值中的Unicode轉義字符讓我們可以通過Unicode碼點輸入特殊的字符. 有兩種形式, \uhhhh 對應16bit的碼點值, \Uhhhhhhhh 對應32bit的碼點值, 其中h是一個十六進製數字; 一般很少需要使用32bit的形式. 每一個對應碼點的UTF8編碼. 例如: 下面的字母串面值都表示相同的值:
|
||||
|
||||
|
@ -139,7 +139,7 @@ func main() {
|
||||
|
||||
當向 bytes.Buffer 添加任意字符的UTF8編碼, 最好使用 bytes.Buffer 的 WriteRune 方法, 但是 WriteByte 方法對於寫入類似 '[' 和 ']' 等 ASCII 字符則更有效.
|
||||
|
||||
bytes.Buffer 類型有着諸多實用的功能, 我們在第七章討論接口時層涉及到, 我們將看看如何將它用作一個I/O 的輸入和輸齣對象, 例如 Fprintf 的 io.Writer 輸齣, 或作爲輸入源 io.Reader.
|
||||
bytes.Buffer 類型有着諸多實用的功能, 我們在第七章討論接口時層涉及到, 我們將看看如何將它用作一個I/O 的輸入和輸出對象, 例如 Fprintf 的 io.Writer 輸出, 或作爲輸入源 io.Reader.
|
||||
|
||||
**練習3.10:** 編寫一個非遞歸版本的comma函數, 使用 bytes.Buffer 代替字符串鏈接操作.
|
||||
|
||||
|
@ -10,7 +10,7 @@ fmt.Println(len(s)) // "12"
|
||||
fmt.Println(s[0], s[7]) // "104 119" ('h' and 'w')
|
||||
```
|
||||
|
||||
如果視圖訪問超齣字符串范圍的字節將會導致panic異常:
|
||||
如果視圖訪問超出字符串范圍的字節將會導致panic異常:
|
||||
|
||||
```Go
|
||||
c := s[len(s)] // panic: index out of range
|
||||
@ -24,7 +24,7 @@ c := s[len(s)] // panic: index out of range
|
||||
fmt.Println(s[0:5]) // "hello"
|
||||
```
|
||||
|
||||
同樣, 如果索引超齣字符串范圍或者j小於i的話將導致panic異常.
|
||||
同樣, 如果索引超出字符串范圍或者j小於i的話將導致panic異常.
|
||||
|
||||
不管i還是j都可能被忽略, 當它們被忽略時將采用0作爲開始位置, 采用 len(s) 作爲接受的位置.
|
||||
|
||||
|
@ -3,7 +3,7 @@
|
||||
|
||||
Go語言的常量有點不尋常. 雖然一個常量可以有任意有一個確定的基礎類型, 例如 int 或 float64, 或者是類似 time.Duration 這樣命名的基礎類型, 但是許多常量併沒有一個明確的基礎類型. 編譯期爲這些沒有明確的基礎類型的數字常量提供比基礎類型或機器更高精度的算術運算; 你可以認爲至少有256bit的運算精度. 這里有六種未明確類型的常量類型, 分别是 無類型的布爾型, 無類型的整數, 無類型的字符, 無類型的浮點數, 無類型的複數, 無類型的字符串.
|
||||
|
||||
通過延遲明確具體類型, 無類型的常量不僅可以提供更高的精度, 而且可以直接用於更多的表達式而不需要類型轉換. 例如 例子中的 ZiB 和 YiB 的值已經超齣任何Go中整數類型能表達的范圍, 但是它們依然是合法的常量, 而且可以像下面表達式這樣使用:
|
||||
通過延遲明確具體類型, 無類型的常量不僅可以提供更高的精度, 而且可以直接用於更多的表達式而不需要類型轉換. 例如 例子中的 ZiB 和 YiB 的值已經超出任何Go中整數類型能表達的范圍, 但是它們依然是合法的常量, 而且可以像下面表達式這樣使用:
|
||||
|
||||
```Go
|
||||
fmt.Println(YiB/ZiB) // "1024"
|
||||
@ -79,7 +79,7 @@ f := 0.0 // untyped floating-point; implicit float64(0.0)
|
||||
c := 0i // untyped complex; implicit complex128(0i)
|
||||
```
|
||||
|
||||
註意默認類型是規則的: 無類型的整數常量默認轉換爲int, 對應不確定的尺寸, 但是浮點數好複數常量則默認轉換爲float64和complex128. Go語言本身併沒有不確定的尺寸的浮點數和複數類型, 因爲如何不知道浮點數類型的話很難寫齣正確的數值算法.
|
||||
註意默認類型是規則的: 無類型的整數常量默認轉換爲int, 對應不確定的尺寸, 但是浮點數好複數常量則默認轉換爲float64和complex128. Go語言本身併沒有不確定的尺寸的浮點數和複數類型, 因爲如何不知道浮點數類型的話很難寫出正確的數值算法.
|
||||
|
||||
如果要給變量一個不同的類型, 我們必鬚顯式地將無類型的常量轉化爲所需的類型, 或給聲明的變量指定類型, 像下面例子這樣:
|
||||
|
||||
|
@ -29,7 +29,7 @@ func (p Point) Distance(q Point) float64 {
|
||||
|
||||
在Go語言中,我們併不會像其它語言那樣用this或者self作爲接收器;我們可以任意的選擇接收器的名字。由於接收器的名字經常會被使用到,所以保持其在方法間傳遞時的一致性和簡短性是不錯的主意。這里的建議是可以使用其類型的第一個字母,比如這里使用了Point的首字母p。
|
||||
|
||||
在方法調用過程中,接收器參數一般會在方法名之前齣現。這和方法聲明是一樣的,都是接收器參數在方法名字之前。下面是例子:
|
||||
在方法調用過程中,接收器參數一般會在方法名之前出現。這和方法聲明是一樣的,都是接收器參數在方法名字之前。下面是例子:
|
||||
|
||||
```Go
|
||||
p := Point{1, 2}
|
||||
|
@ -13,7 +13,7 @@ func (p *Point) ScaleBy(factor float64) {
|
||||
|
||||
在現實的程序里,一般會約定如果Point這個類有一個指針作爲接收器的方法,那麽所有Point的方法都必鬚有一個指針接收器,卽使是那些併不需要這個指針接收器的函數。我們在這里打破了這個約定隻是爲了展示一下兩種方法的異同而已。
|
||||
|
||||
隻有類型(Point)和指向他們的指針(*Point),纔是可能會齣現在接收器聲明里的兩種接收器。此外,爲了避免歧義,在聲明方法時,如果一個類型名本身是一個指針的話,是不允許其齣現在接收器中的,比如下面這個例子:
|
||||
隻有類型(Point)和指向他們的指針(*Point),才是可能會出現在接收器聲明里的兩種接收器。此外,爲了避免歧義,在聲明方法時,如果一個類型名本身是一個指針的話,是不允許其出現在接收器中的,比如下面這個例子:
|
||||
|
||||
```go
|
||||
type P *int
|
||||
@ -105,7 +105,7 @@ func (list *IntList) Sum() int {
|
||||
}
|
||||
```
|
||||
|
||||
當你定義一個允許nil作爲接收器值的方法的類型時,在類型前面的註釋中指齣nil變量代表的意義是很有必要的,就像我們上面例子里做的這樣。
|
||||
當你定義一個允許nil作爲接收器值的方法的類型時,在類型前面的註釋中指出nil變量代表的意義是很有必要的,就像我們上面例子里做的這樣。
|
||||
|
||||
下面是net/url包里Values類型定義的一部分。
|
||||
|
||||
|
@ -11,7 +11,7 @@ type ColoredPoint struct {
|
||||
}
|
||||
```
|
||||
|
||||
我們完全可以將ColoredPoint定義爲一個有三個字段的struct,但是我們卻將Point這個類型嵌入到ColoredPoint來提供X和Y這兩個字段。像我們在4.4節中看到的那樣,內嵌可以使我們在定義ColoredPoint時得到一種句法上的簡寫形式,併使其包含Point類型所具有的一切字段,然後再定義一些自己的。如果我們想要的話,我們可以直接認爲通過嵌入的字段就是ColoredPoint自身的字段,而完全不需要在調用時指齣Point,比如下面這樣。
|
||||
我們完全可以將ColoredPoint定義爲一個有三個字段的struct,但是我們卻將Point這個類型嵌入到ColoredPoint來提供X和Y這兩個字段。像我們在4.4節中看到的那樣,內嵌可以使我們在定義ColoredPoint時得到一種句法上的簡寫形式,併使其包含Point類型所具有的一切字段,然後再定義一些自己的。如果我們想要的話,我們可以直接認爲通過嵌入的字段就是ColoredPoint自身的字段,而完全不需要在調用時指出Point,比如下面這樣。
|
||||
|
||||
```go
|
||||
var cp ColoredPoint
|
||||
|
@ -2,7 +2,7 @@
|
||||
|
||||
Go語言里的集合一般會用map[T]bool這種形式來表示,T代表元素類型。集合用map類型來表示雖然非常靈活,但我們可以以一種更好的形式來表示它。例如在數據流分析領域,集合元素通常是一個非負整數,集合會包含很多元素,併且集合會經常進行併集、交集操作,這種情況下,bit數組會比map表現更加理想。(譯註:這里再補充一個例子,比如我們執行一個http下載任務,把文件按照16kb一塊劃分爲很多塊,需要有一個全局變量來標識哪些塊下載完成了,這種時候也需要用到bit數組)
|
||||
|
||||
一個bit數組通常會用一個無符號數或者稱之爲“字”的slice或者來表示,每一個元素的每一位都表示集合里的一個值。當集合的第i位被設置時,我們纔説這個集合包含元素i。下面的這個程序展示了一個簡單的bit數組類型,併且實現了三個函數來對這個bit數組來進行操作:
|
||||
一個bit數組通常會用一個無符號數或者稱之爲“字”的slice或者來表示,每一個元素的每一位都表示集合里的一個值。當集合的第i位被設置時,我們才説這個集合包含元素i。下面的這個程序展示了一個簡單的bit數組類型,併且實現了三個函數來對這個bit數組來進行操作:
|
||||
|
||||
```go
|
||||
gopl.io/ch6/intset
|
||||
@ -103,7 +103,7 @@ func (*IntSet) Copy() *IntSet // return a copy of the set
|
||||
|
||||
練習6.2: 定義一個變參方法(*IntSet).AddAll(...int),這個方法可以爲一組IntSet值求和,比如s.AddAll(1,2,3)。
|
||||
|
||||
練習6.3: (*IntSet).UnionWith會用|操作符計算兩個集合的交集,我們再爲IntSet實現另外的幾個函數IntersectWith(交集:元素在A集合B集合均齣現),DifferenceWith(差集:元素齣現在A集合,未齣現在B集合),SymmetricDifference(併差集:元素齣現在A但沒有齣現在B,或者齣現在B沒有齣現在A)。
|
||||
練習6.3: (*IntSet).UnionWith會用|操作符計算兩個集合的交集,我們再爲IntSet實現另外的幾個函數IntersectWith(交集:元素在A集合B集合均出現),DifferenceWith(差集:元素出現在A集合,未出現在B集合),SymmetricDifference(併差集:元素出現在A但沒有出現在B,或者出現在B沒有出現在A)。
|
||||
練習6.4: 實現一個Elems方法,返迴集合中的所有元素,用於做一些range之類的遍歷操作。
|
||||
|
||||
練習6.5: 我們這章定義的IntSet里的每個字都是用的uint64類型,但是64位的數值可能在32位的平颱上不高效。脩改程序,使其使用uint類型,這種類型對於32位平颱來説更合適。當然了,這里我們可以不用簡單粗暴地除64,可以定義一個常量來決定是用32還是64,這里你可能會用到平颱的自動判斷的一個智能表達式:32 << (^uint(0) >> 63)
|
||||
|
@ -2,7 +2,7 @@
|
||||
|
||||
一個對象的變量或者方法如果對調用方是不可見的話,一般就被定義爲“封裝”。封裝有時候也被叫做信息隱藏,同時也是面向對象編程最關鍵的一個方面。
|
||||
|
||||
Go語言隻有一種控製可見性的手段:大寫首字母的標識符會從定義它們的包中被導齣,小寫字母的則不會。這種限製包內成員的方式同樣適用於struct或者一個類型的方法。因而如果我們想要封裝一個對象,我們必鬚將其定義爲一個struct。
|
||||
Go語言隻有一種控製可見性的手段:大寫首字母的標識符會從定義它們的包中被導出,小寫字母的則不會。這種限製包內成員的方式同樣適用於struct或者一個類型的方法。因而如果我們想要封裝一個對象,我們必鬚將其定義爲一個struct。
|
||||
|
||||
這也就是前面的小節中IntSet被定義爲struct類型的原因,盡管它隻有一個字段:
|
||||
```go
|
||||
@ -16,7 +16,7 @@ type IntSet struct {
|
||||
type IntSet []uint64
|
||||
```
|
||||
|
||||
盡管這個版本的IntSet在本質上是一樣的,他也可以允許其它包中可以直接讀取併編輯這個slice。換句話説,相對*s這個表達式會齣現在所有的包中,s.words隻需要在定義IntSet的包中齣現(譯註:所以還是推薦後者吧的意思)。
|
||||
盡管這個版本的IntSet在本質上是一樣的,他也可以允許其它包中可以直接讀取併編輯這個slice。換句話説,相對*s這個表達式會出現在所有的包中,s.words隻需要在定義IntSet的包中出現(譯註:所以還是推薦後者吧的意思)。
|
||||
|
||||
這種基於名字的手段使得在語言中最小的封裝單元是package,而不是像其它語言一樣的類型。一個struct類型的字段對同一個包的所有代碼都有可見性,無論你的代碼是寫在一個函數還是一個方法里。
|
||||
|
||||
@ -72,7 +72,7 @@ func (l *Logger) Prefix() string
|
||||
func (l *Logger) SetPrefix(prefix string)
|
||||
```
|
||||
|
||||
Go的編碼風格不禁止直接導齣字段。當然,一旦進行了導齣,就沒有辦法在保證API兼容的情況下去除對其的導齣,所以在一開始的選擇一定要經過深思熟慮併且要考慮到包內部的一些不變量的保證,未來可能的變化,以及調用方的代碼質量是否會因爲包的一點脩改而變差。
|
||||
Go的編碼風格不禁止直接導出字段。當然,一旦進行了導出,就沒有辦法在保證API兼容的情況下去除對其的導出,所以在一開始的選擇一定要經過深思熟慮併且要考慮到包內部的一些不變量的保證,未來可能的變化,以及調用方的代碼質量是否會因爲包的一點脩改而變差。
|
||||
|
||||
封裝併不總是理想的。
|
||||
雖然封裝在有些情況是必要的,但有時候我們也需要暴露一些內部內容,比如:time.Duration將其表現暴露爲一個int64數字的納秒,使得我們可以用一般的數值操作來對時間進行對比,甚至可以定義這種類型的常量:
|
||||
|
@ -1,10 +1,10 @@
|
||||
## 7.1. 接口約定
|
||||
|
||||
目前爲止,我們看到的類型都是具體的類型。一個具體的類型可以準確的描述它所代表的值併且展示齣對類型本身的一些操作方式就像數字類型的算術操作,切片類型的索引、附加和取范圍操作。具體的類型還可以通過它的方法提供額外的行爲操作。總的來説,當你拿到一個具體的類型時你就知道它的本身是什麽和你可以用它來做什麽。
|
||||
目前爲止,我們看到的類型都是具體的類型。一個具體的類型可以準確的描述它所代表的值併且展示出對類型本身的一些操作方式就像數字類型的算術操作,切片類型的索引、附加和取范圍操作。具體的類型還可以通過它的方法提供額外的行爲操作。總的來説,當你拿到一個具體的類型時你就知道它的本身是什麽和你可以用它來做什麽。
|
||||
|
||||
在Go語言中還存在着另外一種類型:接口類型。接口類型是一種抽象的類型。它不會暴露齣它所代表的對象的內部值的結構和這個對象支持的基礎操作的集合;它們隻會展示齣它們自己的方法。也就是説當你有看到一個接口類型的值時,你不知道它是什麽,唯一知道的就是可以通過它的方法來做什麽。
|
||||
在Go語言中還存在着另外一種類型:接口類型。接口類型是一種抽象的類型。它不會暴露出它所代表的對象的內部值的結構和這個對象支持的基礎操作的集合;它們隻會展示出它們自己的方法。也就是説當你有看到一個接口類型的值時,你不知道它是什麽,唯一知道的就是可以通過它的方法來做什麽。
|
||||
|
||||
在本書中,我們一直使用兩個相似的函數來進行字符串的格式化:fmt.Printf它會把結果寫到標準輸齣和fmt.Sprintf它會把結果以字符串的形式返迴。得益於使用接口,我們不必可悲的因爲返迴結果在使用方式上的一些淺顯不同就必需把格式化這個最睏難的過程複製一份。實際上,這兩個函數都使用了另一個函數fmt.Fprintf來進行封裝。fmt.Fprintf這個函數對它的計算結果會被怎麽使用是完全不知道的。
|
||||
在本書中,我們一直使用兩個相似的函數來進行字符串的格式化:fmt.Printf它會把結果寫到標準輸出和fmt.Sprintf它會把結果以字符串的形式返迴。得益於使用接口,我們不必可悲的因爲返迴結果在使用方式上的一些淺顯不同就必需把格式化這個最睏難的過程複製一份。實際上,這兩個函數都使用了另一個函數fmt.Fprintf來進行封裝。fmt.Fprintf這個函數對它的計算結果會被怎麽使用是完全不知道的。
|
||||
``` go
|
||||
package fmt
|
||||
func Fprintf(w io.Writer, format string, args ...interface{}) (int, error)
|
||||
@ -17,7 +17,7 @@ func Sprintf(format string, args ...interface{}) string {
|
||||
return buf.String()
|
||||
}
|
||||
```
|
||||
Fprintf的前綴F表示文件(File)也表明格式化輸齣結果應該被寫入第一個參數提供的文件中。在Printf函數中的第一個參數os.Stdout是*os.File類型;在Sprintf函數中的第一個參數&buf是一個指向可以寫入字節的內存緩衝區,然而它
|
||||
Fprintf的前綴F表示文件(File)也表明格式化輸出結果應該被寫入第一個參數提供的文件中。在Printf函數中的第一個參數os.Stdout是*os.File類型;在Sprintf函數中的第一個參數&buf是一個指向可以寫入字節的內存緩衝區,然而它
|
||||
併不是一個文件類型盡管它在某種意義上和文件類型相似。
|
||||
|
||||
卽使Fprintf函數中的第一個參數也不是一個文件類型。它是io.Writer類型這是一個接口類型定義如下:
|
||||
@ -59,7 +59,7 @@ var name = "Dolly"
|
||||
fmt.Fprintf(&c, "hello, %s", name)
|
||||
fmt.Println(c) // "12", = len("hello, Dolly")
|
||||
```
|
||||
除了io.Writer這個接口類型,還有另一個對fmt包很重要的接口類型。Fprintf和Fprintln函數向類型提供了一種控製它們值輸齣的途徑。在2.5節中,我們爲Celsius類型提供了一個String方法以便於可以打印成這樣"100°C" ,在6.5節中我們給*IntSet添加一個String方法,這樣集合可以用傳統的符號來進行表示就像"{1 2 3}"。給一個類型定義String方法,可以讓它滿足最廣泛使用之一的接口類型fmt.Stringer:
|
||||
除了io.Writer這個接口類型,還有另一個對fmt包很重要的接口類型。Fprintf和Fprintln函數向類型提供了一種控製它們值輸出的途徑。在2.5節中,我們爲Celsius類型提供了一個String方法以便於可以打印成這樣"100°C" ,在6.5節中我們給*IntSet添加一個String方法,這樣集合可以用傳統的符號來進行表示就像"{1 2 3}"。給一個類型定義String方法,可以讓它滿足最廣泛使用之一的接口類型fmt.Stringer:
|
||||
```go
|
||||
package fmt
|
||||
// The String method is used to print values passed
|
||||
|
@ -1,8 +1,8 @@
|
||||
## 8.1. Goroutines
|
||||
|
||||
在Go語言中,每一個併發的執行單元叫作一個goroutine。設想這里有一個程序有兩個函數,一個函數做一些計算,另一個輸齣一些結果,假設兩個函數沒有相互之間的調用關繫。一個線性的程序會先調用其中的一個函數,然後再調用來一個,但如果是在有兩個甚至更多個goroutine的程序中,對兩個函數的調用就可以在同一時間。我們馬上就會看到這樣的一個程序。
|
||||
在Go語言中,每一個併發的執行單元叫作一個goroutine。設想這里有一個程序有兩個函數,一個函數做一些計算,另一個輸出一些結果,假設兩個函數沒有相互之間的調用關繫。一個線性的程序會先調用其中的一個函數,然後再調用來一個,但如果是在有兩個甚至更多個goroutine的程序中,對兩個函數的調用就可以在同一時間。我們馬上就會看到這樣的一個程序。
|
||||
|
||||
如果你使用過操作繫統或者其它語言提供的線程,那麽你可以簡單地把goroutine類比作一個線程,這樣你就可以寫齣一些正確的程序了。goroutine和線程的本質區别會在9.8節中講。
|
||||
如果你使用過操作繫統或者其它語言提供的線程,那麽你可以簡單地把goroutine類比作一個線程,這樣你就可以寫出一些正確的程序了。goroutine和線程的本質區别會在9.8節中講。
|
||||
|
||||
當一個程序啟動時,其主函數卽在一個單獨的goroutine中運行,我們叫它main goroutine。新的goroutine會用go語句來創建。在語法上,go語句是一個普通的函數或方法調用前加上關鍵字go。go語句會使其語句中的函數在一個新創建的goroutine中運行。而go語句本身會迅速地完成。
|
||||
|
||||
@ -43,7 +43,7 @@ func fib(x int) int {
|
||||
動畵顯示了幾秒之後,fib(45)的調用成功地返迴,併且打印結果:
|
||||
Fibonacci(45) = 1134903170
|
||||
|
||||
然後主函數返迴。當主函數返迴時,所有的goroutine都會直接打斷,程序退齣。除了從主函數退齣或者直接退齣程序之外,沒有其它的編程方法能夠讓一個goroutine來打斷另一個的執行,但是我們之後可以看到,可以通過goroutine之間的通信來讓一個goroutine請求請求其它的goroutine,併讓其自己結束執行。
|
||||
然後主函數返迴。當主函數返迴時,所有的goroutine都會直接打斷,程序退出。除了從主函數退出或者直接退出程序之外,沒有其它的編程方法能夠讓一個goroutine來打斷另一個的執行,但是我們之後可以看到,可以通過goroutine之間的通信來讓一個goroutine請求請求其它的goroutine,併讓其自己結束執行。
|
||||
|
||||
註意這里的兩個獨立的單元是如何進行組合的,spinning和菲波那契的計算。每一個都是寫在獨立的函數中,但是每一個函數都會併發地執行。
|
||||
|
||||
|
@ -48,7 +48,7 @@ Listen函數創建了一個net.Listener的對象,這個對象會監聽一個
|
||||
|
||||
handleConn函數會處理一個完整的客戶端連接。在一個for死循環中,將當前的時候用time.Now()函數得到,然後寫到客戶端。由於net.Conn實現了io.Writer接口,我們可以直接向其寫入內容。這個死循環會一直執行,直到寫入失敗。最可能的原因是客戶端主動斷開連接。這種情況下handleConn函數會用defer調用關閉服務器側的連接,然後返迴到主函數,繼續等待下一個連接請求。
|
||||
|
||||
time.Time.Format方法提供了一種格式化日期和時間信息的方式。它的參數是一個格式化模闆標識如何來格式化時間,而這個格式化模闆限定爲Mon Jan 2 03:04:05PM 2006 UTC-0700。有8個部分(週幾,月份,一個月的第幾天,等等)。可以以任意的形式來組合前面這個模闆;齣現在模闆中的部分會作爲參考來對時間格式進行輸齣。在上面的例子中我們隻用到了小時、分鐘和秒。time包里定義了很多標準時間格式,比如time.RFC1123。在進行格式化的逆向操作time.Parse時,也會用到同樣的策略。(譯註:這是go語言和其它語言相比比較奇葩的一個地方。。你需要記住格式化字符串是1月2日下午3點4分5秒零六年UTC-0700,而不像其它語言那樣Y-m-d H:i:s一樣,當然了這里可以用1234567的方式來記憶,倒是也不麻煩)
|
||||
time.Time.Format方法提供了一種格式化日期和時間信息的方式。它的參數是一個格式化模闆標識如何來格式化時間,而這個格式化模闆限定爲Mon Jan 2 03:04:05PM 2006 UTC-0700。有8個部分(週幾,月份,一個月的第幾天,等等)。可以以任意的形式來組合前面這個模闆;出現在模闆中的部分會作爲參考來對時間格式進行輸出。在上面的例子中我們隻用到了小時、分鐘和秒。time包里定義了很多標準時間格式,比如time.RFC1123。在進行格式化的逆向操作time.Parse時,也會用到同樣的策略。(譯註:這是go語言和其它語言相比比較奇葩的一個地方。。你需要記住格式化字符串是1月2日下午3點4分5秒零六年UTC-0700,而不像其它語言那樣Y-m-d H:i:s一樣,當然了這里可以用1234567的方式來記憶,倒是也不麻煩)
|
||||
|
||||
爲了連接例子里的服務器,我們需要一個客戶端程序,比如netcat這個工具(nc命令),這個工具可以用來執行網絡連接操作。
|
||||
|
||||
@ -63,7 +63,7 @@ $ nc localhost 8000
|
||||
^C
|
||||
```
|
||||
|
||||
客戶端將服務器發來的時間顯示了齣來,我們用Control+C來中斷客戶端的執行,在Unix繫統上,你會看到^C這樣的響應。如果你的繫統沒有裝nc這個工具,你可以用telnet來實現同樣的效果,或者也可以用我們下面的這個用go寫的簡單的telnet程序,用net.Dial就可以簡單地創建一個TCP連接:
|
||||
客戶端將服務器發來的時間顯示了出來,我們用Control+C來中斷客戶端的執行,在Unix繫統上,你會看到^C這樣的響應。如果你的繫統沒有裝nc這個工具,你可以用telnet來實現同樣的效果,或者也可以用我們下面的這個用go寫的簡單的telnet程序,用net.Dial就可以簡單地創建一個TCP連接:
|
||||
|
||||
```go
|
||||
gopl.io/ch8/netcat1
|
||||
@ -92,7 +92,7 @@ func mustCopy(dst io.Writer, src io.Reader) {
|
||||
}
|
||||
}
|
||||
```
|
||||
這個程序會從連接中讀取數據,併將讀到的內容寫到標準輸齣中,直到遇到end of file的條件或者發生錯誤。mustCopy這個函數我們在本節的幾個例子中都會用到。讓我們同時運行兩個客戶端來進行一個測試,這里可以開兩個終端窗口,下面左邊的是其中的一個的輸齣,右邊的是另一個的輸齣:
|
||||
這個程序會從連接中讀取數據,併將讀到的內容寫到標準輸出中,直到遇到end of file的條件或者發生錯誤。mustCopy這個函數我們在本節的幾個例子中都會用到。讓我們同時運行兩個客戶端來進行一個測試,這里可以開兩個終端窗口,下面左邊的是其中的一個的輸出,右邊的是另一個的輸出:
|
||||
|
||||
```
|
||||
$ go build gopl.io/ch8/netcat1
|
||||
@ -110,7 +110,7 @@ $ killall clock1
|
||||
|
||||
killall命令是一個Unix命令行工具,可以用給定的進程名來殺掉所有名字匹配的進程。
|
||||
|
||||
第二個客戶端必鬚等待第一個客戶端完成工作,這樣服務端纔能繼續向後執行;因爲我們這里的服務器程序同一時間隻能處理一個客戶端連接。我們這里對服務端程序做一點小改動,使其支持併發:在handleConn函數調用的地方增加go關鍵字,讓每一次handleConn的調用都進入一個獨立的goroutine。
|
||||
第二個客戶端必鬚等待第一個客戶端完成工作,這樣服務端才能繼續向後執行;因爲我們這里的服務器程序同一時間隻能處理一個客戶端連接。我們這里對服務端程序做一點小改動,使其支持併發:在handleConn函數調用的地方增加go關鍵字,讓每一次handleConn的調用都進入一個獨立的goroutine。
|
||||
|
||||
```go
|
||||
gopl.io/ch8/clock2
|
||||
@ -153,4 +153,4 @@ $ TZ=Europe/London ./clock2 -port 8030 &
|
||||
$ clockwall NewYork=localhost:8010 London=localhost:8020 Tokyo=localhost:8030
|
||||
```
|
||||
|
||||
練習8.2: 實現一個併發FTP服務器。服務器應該解析客戶端來的一些命令,比如cd命令來切換目録,ls來列齣目録內文件,get和send來傳輸文件,close來關閉連接。你可以用標準的ftp命令來作爲客戶端,或者也可以自己實現一個。
|
||||
練習8.2: 實現一個併發FTP服務器。服務器應該解析客戶端來的一些命令,比如cd命令來切換目録,ls來列出目録內文件,get和send來傳輸文件,close來關閉連接。你可以用標準的ftp命令來作爲客戶端,或者也可以自己實現一個。
|
||||
|
@ -40,7 +40,7 @@ func main() {
|
||||
|
||||
註意這里的crawl所在的goroutine會將link作爲一個顯式的參數傳入,來避免“循環變量快照”的問題(在5.6.1中有講解)。另外註意這里將命令行參數傳入worklist也是在一個另外的goroutine中進行的,這是爲了避免在main goroutine和crawler goroutine中同時向另一個goroutine通過channel發送內容時發生死鎖(因爲另一邊的接收操作還沒有準備好)。當然,這里我們也可以用buffered channel來解決問題,這里不再贅述。
|
||||
|
||||
現在爬蟲可以高併發地運行起來,併且可以産生一大坨的URL了,不過還是會有倆問題。一個問題是在運行一段時間後可能會齣現在log的錯誤信息里的:
|
||||
現在爬蟲可以高併發地運行起來,併且可以産生一大坨的URL了,不過還是會有倆問題。一個問題是在運行一段時間後可能會出現在log的錯誤信息里的:
|
||||
|
||||
|
||||
```
|
||||
@ -58,7 +58,7 @@ https://golang.org/blog/
|
||||
```
|
||||
最初的錯誤信息是一個讓人莫名的DNS査找失敗,卽使這個域名是完全可靠的。而隨後的錯誤信息揭示了原因:這個程序一次性創建了太多網絡連接,超過了每一個進程的打開文件數限製,旣而導致了在調用net.Dial像DNS査找失敗這樣的問題。
|
||||
|
||||
這個程序實在是太他媽併行了。無窮無盡地併行化併不是什麽好事情,因爲不管怎麽説,你的繫統總是會有一個些限製因素,比如CPU覈心數會限製你的計算負載,比如你的硬盤轉軸和磁頭數限製了你的本地磁盤IO操作頻率,比如你的網絡帶寬限製了你的下載速度上限,或者是你的一個web服務的服務容量上限等等。爲了解決這個問題,我們可以限製併發程序所使用的資源來使之適應自己的運行環境。對於我們的例子來説,最簡單的方法就是限製對links.Extract在同一時間最多不會有超過n次調用,這里的n是fd的limit-20,一般情況下。這個一個夜店里限製客人數目是一個道理,隻有當有客人離開時,纔會允許新的客人進入店內(譯註:作者你個老流氓)。
|
||||
這個程序實在是太他媽併行了。無窮無盡地併行化併不是什麽好事情,因爲不管怎麽説,你的繫統總是會有一個些限製因素,比如CPU覈心數會限製你的計算負載,比如你的硬盤轉軸和磁頭數限製了你的本地磁盤IO操作頻率,比如你的網絡帶寬限製了你的下載速度上限,或者是你的一個web服務的服務容量上限等等。爲了解決這個問題,我們可以限製併發程序所使用的資源來使之適應自己的運行環境。對於我們的例子來説,最簡單的方法就是限製對links.Extract在同一時間最多不會有超過n次調用,這里的n是fd的limit-20,一般情況下。這個一個夜店里限製客人數目是一個道理,隻有當有客人離開時,才會允許新的客人進入店內(譯註:作者你個老流氓)。
|
||||
|
||||
我們可以用一個有容量限製的buffered channel來控製併發,這類似於操作繫統里的計數信號量概念。從概念上講,channel里的n個空槽代表n個可以處理內容的token(通行證),從channel里接收一個值會釋放其中的一個token,併且生成一個新的空槽位。這樣保證了在沒有接收介入時最多有n個發送操作。(這里可能我們拿channel里填充的槽來做token更直觀一些,不過還是這樣吧~)。由於channel里的元素類型併不重要,我們用一個零值的struct{}來作爲其元素。
|
||||
|
||||
@ -82,7 +82,7 @@ func crawl(url string) []string {
|
||||
}
|
||||
```
|
||||
|
||||
第二個問題是這個程序永遠都不會終止,卽使它已經爬到了所有初始鏈接衍生齣的鏈接。(當然,除非你慎重地選擇了合適的初始化URL或者已經實現了練習8.6中的深度限製,你應該還沒有意識到這個問題)。爲了使這個程序能夠終止,我們需要在worklist爲空或者沒有crawl的goroutine在運行時退齣主循環。
|
||||
第二個問題是這個程序永遠都不會終止,卽使它已經爬到了所有初始鏈接衍生出的鏈接。(當然,除非你慎重地選擇了合適的初始化URL或者已經實現了練習8.6中的深度限製,你應該還沒有意識到這個問題)。爲了使這個程序能夠終止,我們需要在worklist爲空或者沒有crawl的goroutine在運行時退出主循環。
|
||||
|
||||
|
||||
```go
|
||||
@ -116,7 +116,7 @@ func main() {
|
||||
|
||||
這個版本中,計算器n對worklist的發送操作數量進行了限製。每一次我們發現有元素需要被發送到worklist時,我們都會對n進行++操作,在向worklist中發送初始的命令行參數之前,我們也進行過一次++操作。這里的操作++是在每啟動一個crawler的goroutine之前。主循環會在n減爲0時終止,這時候説明沒活可榦了。
|
||||
|
||||
現在這個併發爬蟲會比5.6節中的深度優先蒐索版快上20倍,而且不會齣什麽錯,併且在其完成任務時也會正確地終止。
|
||||
現在這個併發爬蟲會比5.6節中的深度優先蒐索版快上20倍,而且不會出什麽錯,併且在其完成任務時也會正確地終止。
|
||||
|
||||
下面的程序是避免過度併發的另一種思路。這個版本使用了原來的crawl函數,但沒有使用計數信號量,取而代之用了20個長活的crawler goroutine,這樣來保證最多20個HTTP請求在併發。
|
||||
|
||||
@ -158,9 +158,9 @@ seen這個map被限定在main goroutine中;也就是説這個map隻能在main
|
||||
|
||||
crawl函數爬到的鏈接在一個專有的goroutine中被發送到worklist中來避免死鎖。爲了節省空間,這個例子的終止問題我們先不進行詳細闡述了。
|
||||
|
||||
練習8.6: 爲併發爬蟲增加深度限製。也就是説,如果用戶設置了depth=3,那麽隻有從首頁跳轉三次以內能夠跳到的頁面纔能被抓取到。
|
||||
練習8.6: 爲併發爬蟲增加深度限製。也就是説,如果用戶設置了depth=3,那麽隻有從首頁跳轉三次以內能夠跳到的頁面才能被抓取到。
|
||||
|
||||
練習8.7: 完成一個併發程序來創建一個線上網站的本地鏡像,把該站點的所有可達的頁面都抓取到本地硬盤。爲了省事,我們這里可以隻取齣現在該域下的所有頁面(比如golang.org結尾,譯註:外鏈的應該就不算了。)當然了,齣現在頁面里的鏈接你也需要進行一些處理,使其能夠在你的鏡像站點上進行跳轉,而不是指向原始的鏈接。
|
||||
練習8.7: 完成一個併發程序來創建一個線上網站的本地鏡像,把該站點的所有可達的頁面都抓取到本地硬盤。爲了省事,我們這里可以隻取出現在該域下的所有頁面(比如golang.org結尾,譯註:外鏈的應該就不算了。)當然了,出現在頁面里的鏈接你也需要進行一些處理,使其能夠在你的鏡像站點上進行跳轉,而不是指向原始的鏈接。
|
||||
|
||||
|
||||
譯註:
|
||||
|
@ -30,7 +30,7 @@ func dirents(dir string) []os.FileInfo {
|
||||
|
||||
ioutil.ReadDir函數會返迴一個os.FileInfo類型的slice,os.FileInfo類型也是os.Stat這個函數的返迴值。對每一個子目録而言,walkDir會遞歸地調用其自身,併且會對每一個文件也遞歸調用。walkDir函數會向fileSizes這個channel發送一條消息。這條消息包含了文件的字節大小。
|
||||
|
||||
下面的主函數,用了兩個goroutine。後颱的goroutine調用walkDir來遍歷命令行給齣的每一個路徑併最終關閉fileSizes這個channel。主goroutine會對其從channel中接收到的文件大小進行纍加,併輸齣其和。
|
||||
下面的主函數,用了兩個goroutine。後颱的goroutine調用walkDir來遍歷命令行給出的每一個路徑併最終關閉fileSizes這個channel。主goroutine會對其從channel中接收到的文件大小進行纍加,併輸出其和。
|
||||
|
||||
|
||||
```go
|
||||
@ -82,9 +82,9 @@ $ ./du1 $HOME /usr /bin /etc
|
||||
213201 files 62.7 GB
|
||||
```
|
||||
|
||||
如果在運行的時候能夠讓我們知道處理進度的話想必更好。但是,如果簡單地把printDiskUsage函數調用移動到循環里會導致其打印齣成百上韆的輸齣。
|
||||
如果在運行的時候能夠讓我們知道處理進度的話想必更好。但是,如果簡單地把printDiskUsage函數調用移動到循環里會導致其打印出成百上韆的輸出。
|
||||
|
||||
下面這個du的變種會間歇打印內容,不過隻有在調用時提供了-v的flag纔會顯示程序進度信息。在roots目録上循環的後颱goroutine在這里保持不變。主goroutine現在使用了計時器來每500ms生成事件,然後用select語句來等待文件大小的消息來更新總大小數據,或者一個計時器的事件來打印當前的總大小數據。如果-v的flag在運行時沒有傳入的話,tick這個channel會保持爲nil,這樣在select里的case也就相當於被禁用了。
|
||||
下面這個du的變種會間歇打印內容,不過隻有在調用時提供了-v的flag才會顯示程序進度信息。在roots目録上循環的後颱goroutine在這里保持不變。主goroutine現在使用了計時器來每500ms生成事件,然後用select語句來等待文件大小的消息來更新總大小數據,或者一個計時器的事件來打印當前的總大小數據。如果-v的flag在運行時沒有傳入的話,tick這個channel會保持爲nil,這樣在select里的case也就相當於被禁用了。
|
||||
|
||||
```go
|
||||
gopl.io/ch8/du2
|
||||
@ -115,7 +115,7 @@ loop:
|
||||
printDiskUsage(nfiles, nbytes) // final totals
|
||||
}
|
||||
```
|
||||
由於我們的程序不再使用range循環,第一個select的case必鬚顯式地判斷fileSizes的channel是不是已經被關閉了,這里可以用到channel接收的二值形式。如果channel已經被關閉了的話,程序會直接退齣循環。這里的break語句用到了標籤break,這樣可以同時終結select和for兩個循環;如果沒有用標籤就break的話隻會退齣內層的select循環,而外層的for循環會使之進入下一輪select循環。
|
||||
由於我們的程序不再使用range循環,第一個select的case必鬚顯式地判斷fileSizes的channel是不是已經被關閉了,這里可以用到channel接收的二值形式。如果channel已經被關閉了的話,程序會直接退出循環。這里的break語句用到了標籤break,這樣可以同時終結select和for兩個循環;如果沒有用標籤就break的話隻會退出內層的select循環,而外層的for循環會使之進入下一輪select循環。
|
||||
|
||||
現在程序會悠閒地爲我們打印更新流:
|
||||
|
||||
@ -130,7 +130,7 @@ $ ./du2 -v $HOME /usr /bin /etc
|
||||
213201 files 62.7 GB
|
||||
```
|
||||
|
||||
然而這個程序還是會花上很長時間纔會結束。無法對walkDir做併行化處理沒什麽别的原因,無非是因爲磁盤繫統併行限製。下面這個第三個版本的du,會對每一個walkDir的調用創建一個新的goroutine。它使用sync.WaitGroup (§8.5)來對仍舊活躍的walkDir調用進行計數,另一個goroutine會在計數器減爲零的時候將fileSizes這個channel關閉。
|
||||
然而這個程序還是會花上很長時間才會結束。無法對walkDir做併行化處理沒什麽别的原因,無非是因爲磁盤繫統併行限製。下面這個第三個版本的du,會對每一個walkDir的調用創建一個新的goroutine。它使用sync.WaitGroup (§8.5)來對仍舊活躍的walkDir調用進行計數,另一個goroutine會在計數器減爲零的時候將fileSizes這個channel關閉。
|
||||
|
||||
```go
|
||||
gopl.io/ch8/du3
|
||||
@ -181,6 +181,6 @@ func dirents(dir string) []os.FileInfo {
|
||||
|
||||
這個版本比之前那個快了好幾倍,盡管其具體效率還是和你的運行環境,機器配置相關。
|
||||
|
||||
練習8.9: 編寫一個du工具,每隔一段時間將root目録下的目録大小計算併顯示齣來。
|
||||
練習8.9: 編寫一個du工具,每隔一段時間將root目録下的目録大小計算併顯示出來。
|
||||
|
||||
|
||||
|
@ -1,14 +1,14 @@
|
||||
## 8.9. 併發的退齣
|
||||
## 8.9. 併發的退出
|
||||
|
||||
有時候我們需要通知goroutine停止它正在榦的事情,比如一個正在執行計算的web服務,然而它的客戶端已經斷開了和服務端的連接。
|
||||
|
||||
Go語言併沒有提供在一個goroutine中終止另一個goroutine的方法,由於這樣會導致goroutine之間的共享變量落在未定義的狀態上。在8.7節中的rocket launch程序中,我們往名字叫abort的channel里發送了一個簡單的值,在countdown的goroutine中會把這個值理解爲自己的退齣信號。但是如果我們想要退齣兩個或者任意多個goroutine怎麽辦呢?
|
||||
Go語言併沒有提供在一個goroutine中終止另一個goroutine的方法,由於這樣會導致goroutine之間的共享變量落在未定義的狀態上。在8.7節中的rocket launch程序中,我們往名字叫abort的channel里發送了一個簡單的值,在countdown的goroutine中會把這個值理解爲自己的退出信號。但是如果我們想要退出兩個或者任意多個goroutine怎麽辦呢?
|
||||
|
||||
一種可能的手段是向abort的channel里發送和goroutine數目一樣多的事件來退齣它們。如果這些goroutine中已經有一些自己退齣了,那麽會導致我們的channel里的事件數比goroutine還多,這樣導致我們的發送直接被阻塞。另一方面,如果這些goroutine又生成了其它的goroutine,我們的channel里的數目又太少了,所以有些goroutine可能會無法接收到退齣消息。一般情況下我們是很難知道在某一個時刻具體有多少個goroutine在運行着的。另外,當一個goroutine從abort channel中接收到一個值的時候,他會消費掉這個值,這樣其它的goroutine就沒法看到這條信息。爲了能夠達到我們退齣goroutine的目的,我們需要更靠譜的策略,來通過一個channel把消息廣播齣去,這樣goroutine們能夠看到這條事件消息,併且在事件完成之後,可以知道這件事已經發生過了。
|
||||
一種可能的手段是向abort的channel里發送和goroutine數目一樣多的事件來退出它們。如果這些goroutine中已經有一些自己退出了,那麽會導致我們的channel里的事件數比goroutine還多,這樣導致我們的發送直接被阻塞。另一方面,如果這些goroutine又生成了其它的goroutine,我們的channel里的數目又太少了,所以有些goroutine可能會無法接收到退出消息。一般情況下我們是很難知道在某一個時刻具體有多少個goroutine在運行着的。另外,當一個goroutine從abort channel中接收到一個值的時候,他會消費掉這個值,這樣其它的goroutine就沒法看到這條信息。爲了能夠達到我們退出goroutine的目的,我們需要更靠譜的策略,來通過一個channel把消息廣播出去,這樣goroutine們能夠看到這條事件消息,併且在事件完成之後,可以知道這件事已經發生過了。
|
||||
|
||||
迴憶一下我們關閉了一個channel併且被消費掉了所有已發送的值,操作channel之後的代碼可以立卽被執行,併且會産生零值。我們可以將這個機製擴展一下,來作爲我們的廣播機製:不要向channel發送值,而是用關閉一個channel來進行廣播。
|
||||
|
||||
隻要一些小脩改,我們就可以把退齣邏輯加入到前一節的du程序。首先,我們創建一個退齣的channel,這個channel不會向其中發送任何值,但其所在的閉包內要寫明程序需要退齣。我們同時還定義了一個工具函數,cancelled,這個函數在被調用的時候會輪詢退齣狀態。
|
||||
隻要一些小脩改,我們就可以把退出邏輯加入到前一節的du程序。首先,我們創建一個退出的channel,這個channel不會向其中發送任何值,但其所在的閉包內要寫明程序需要退出。我們同時還定義了一個工具函數,cancelled,這個函數在被調用的時候會輪詢退出狀態。
|
||||
|
||||
```go
|
||||
gopl.io/ch8/du4
|
||||
@ -24,7 +24,7 @@ func cancelled() bool {
|
||||
}
|
||||
```
|
||||
|
||||
下面我們創建一個從標準輸入流中讀取內容的goroutine,這是一個比較典型的連接到終端的程序。每當有輸入被讀到(比如用戶按了迴車鍵),這個goroutine就會把取消消息通過關閉done的channel廣播齣去。
|
||||
下面我們創建一個從標準輸入流中讀取內容的goroutine,這是一個比較典型的連接到終端的程序。每當有輸入被讀到(比如用戶按了迴車鍵),這個goroutine就會把取消消息通過關閉done的channel廣播出去。
|
||||
|
||||
```go
|
||||
// Cancel traversal when input is detected.
|
||||
@ -82,7 +82,7 @@ func dirents(dir string) []os.FileInfo {
|
||||
}
|
||||
```
|
||||
|
||||
現在當取消發生時,所有後颱的goroutine都會迅速停止併且主函數會返迴。當然,當主函數返迴時,一個程序會退齣,而我們又無法在主函數退齣的時候確認其已經釋放了所有的資源(譯註:因爲程序都退齣了,你的代碼都沒法執行了)。這里有一個方便的竅門我們可以一用:取代掉直接從主函數返迴,我們調用一個panic,然後runtime會把每一個goroutine的棧dump下來。如果main goroutine是唯一一個剩下的goroutine的話,他會清理掉自己的一切資源。但是如果還有其它的goroutine沒有退齣,他們可能沒辦法被正確地取消掉,也有可能被取消但是取消操作會很花時間;所以這里的一個調研還是很有必要的。我們用panic來穫取到足夠的信息來驗證我們上面的判斷,看看最終到底是什麽樣的情況。
|
||||
現在當取消發生時,所有後颱的goroutine都會迅速停止併且主函數會返迴。當然,當主函數返迴時,一個程序會退出,而我們又無法在主函數退出的時候確認其已經釋放了所有的資源(譯註:因爲程序都退出了,你的代碼都沒法執行了)。這里有一個方便的竅門我們可以一用:取代掉直接從主函數返迴,我們調用一個panic,然後runtime會把每一個goroutine的棧dump下來。如果main goroutine是唯一一個剩下的goroutine的話,他會清理掉自己的一切資源。但是如果還有其它的goroutine沒有退出,他們可能沒辦法被正確地取消掉,也有可能被取消但是取消操作會很花時間;所以這里的一個調研還是很有必要的。我們用panic來穫取到足夠的信息來驗證我們上面的判斷,看看最終到底是什麽樣的情況。
|
||||
|
||||
練習8.10: HTTP請求可能會因http.Request結構體中Cancel channel的關閉而取消。脩改8.6節中的web crawler來支持取消http請求。
|
||||
|
||||
|
@ -22,7 +22,7 @@ func main() {
|
||||
}
|
||||
```
|
||||
|
||||
然後是broadcaster的goroutine。他的內部變量clients會記録當前建立連接的客戶端集合。其記録的內容是每一個客戶端的消息發齣channel的"資格"信息。
|
||||
然後是broadcaster的goroutine。他的內部變量clients會記録當前建立連接的客戶端集合。其記録的內容是每一個客戶端的消息發出channel的"資格"信息。
|
||||
|
||||
|
||||
```go
|
||||
@ -55,9 +55,9 @@ func broadcaster() {
|
||||
}
|
||||
```
|
||||
|
||||
broadcaster監聽來自全局的entering和leaving的channel來穫知客戶端的到來和離開事件。當其接收到其中的一個事件時,會更新clients集合,當該事件是離開行爲時,它會關閉客戶端的消息發齣channel。broadcaster也會監聽全局的消息channel,所有的客戶端都會向這個channel中發送消息。當broadcaster接收到什麽消息時,就會將其廣播至所有連接到服務端的客戶端。
|
||||
broadcaster監聽來自全局的entering和leaving的channel來穫知客戶端的到來和離開事件。當其接收到其中的一個事件時,會更新clients集合,當該事件是離開行爲時,它會關閉客戶端的消息發出channel。broadcaster也會監聽全局的消息channel,所有的客戶端都會向這個channel中發送消息。當broadcaster接收到什麽消息時,就會將其廣播至所有連接到服務端的客戶端。
|
||||
|
||||
現在讓我們看看每一個客戶端的goroutine。handleConn函數會爲它的客戶端創建一個消息發齣channel併通過entering channel來通知客戶端的到來。然後它會讀取客戶端發來的每一行文本,併通過全局的消息channel來將這些文本發送齣去,併爲每條消息帶上發送者的前綴來標明消息身份。當客戶端發送完畢後,handleConn會通過leaving這個channel來通知客戶端的離開併關閉連接。
|
||||
現在讓我們看看每一個客戶端的goroutine。handleConn函數會爲它的客戶端創建一個消息發出channel併通過entering channel來通知客戶端的到來。然後它會讀取客戶端發來的每一行文本,併通過全局的消息channel來將這些文本發送出去,併爲每條消息帶上發送者的前綴來標明消息身份。當客戶端發送完畢後,handleConn會通過leaving這個channel來通知客戶端的離開併關閉連接。
|
||||
|
||||
```go
|
||||
func handleConn(conn net.Conn) {
|
||||
@ -87,7 +87,7 @@ func clientWriter(conn net.Conn, ch <-chan string) {
|
||||
}
|
||||
```
|
||||
|
||||
另外,handleConn爲每一個客戶端創建了一個clientWriter的goroutine來接收向客戶端發齣消息channel中發送的廣播消息,併將它們寫入到客戶端的網絡連接。客戶端的讀取方循環會在broadcaster接收到leaving通知併關閉了channel後終止。
|
||||
另外,handleConn爲每一個客戶端創建了一個clientWriter的goroutine來接收向客戶端發出消息channel中發送的廣播消息,併將它們寫入到客戶端的網絡連接。客戶端的讀取方循環會在broadcaster接收到leaving通知併關閉了channel後終止。
|
||||
|
||||
下面演示的是當服務器有兩個活動的客戶端連接,併且在兩個窗口中運行的情況,使用netcat來聊天:
|
||||
```
|
||||
@ -118,4 +118,4 @@ You are 127.0.0.1:64216 127.0.0.1:64216 has arrived
|
||||
練習8.12: 使broadcaster能夠將arrival事件通知當前所有的客戶端。爲了達成這個目的,你需要有一個客戶端的集合,併且在entering和leaving的channel中記録客戶端的名字。
|
||||
練習8.13: 使聊天服務器能夠斷開空閒的客戶端連接,比如最近五分鐘之後沒有發送任何消息的那些客戶端。提示:可以在其它goroutine中調用conn.Close()來解除Read調用,就像input.Scanner()所做的那樣。
|
||||
練習8.14: 脩改聊天服務器的網絡協議這樣每一個客戶端就可以在entering時可以提供它們的名字。將消息前綴由之前的網絡地址改爲這個名字。
|
||||
練習8.15: 如果一個客戶端沒有及時地讀取數據可能會導致所有的客戶端被阻塞。脩改broadcaster來跳過一條消息,而不是等待這個客戶端一直到其準備好寫。或者爲每一個客戶端的消息發齣channel建立緩衝區,這樣大部分的消息便不會被丟掉;broadcaster應該用一個非阻塞的send向這個channel中發消息。
|
||||
練習8.15: 如果一個客戶端沒有及時地讀取數據可能會導致所有的客戶端被阻塞。脩改broadcaster來跳過一條消息,而不是等待這個客戶端一直到其準備好寫。或者爲每一個客戶端的消息發出channel建立緩衝區,這樣大部分的消息便不會被丟掉;broadcaster應該用一個非阻塞的send向這個channel中發消息。
|
||||
|
@ -1,6 +1,6 @@
|
||||
# 第八章 Goroutines和Channels
|
||||
|
||||
併發程序指的是同時做好幾件事情的程序,隨着硬件的發展,併發程序顯得越來越重要。Web服務器會一次處理成韆上萬的請求。平闆電腦和手機app在渲染用戶動畵的同時,還會後颱執行各種計算任務和網絡請求。卽使是傳統的批處理問題--讀取數據,計算,寫輸齣--現在也會用併發來隱藏掉I/O的操作延遲充分利用現代計算機設備的多覈,盡管計算機的性能每年都在增長,但併不是線性。
|
||||
併發程序指的是同時做好幾件事情的程序,隨着硬件的發展,併發程序顯得越來越重要。Web服務器會一次處理成韆上萬的請求。平闆電腦和手機app在渲染用戶動畵的同時,還會後颱執行各種計算任務和網絡請求。卽使是傳統的批處理問題--讀取數據,計算,寫輸出--現在也會用併發來隱藏掉I/O的操作延遲充分利用現代計算機設備的多覈,盡管計算機的性能每年都在增長,但併不是線性。
|
||||
|
||||
Go語言中的併發程序可以用兩種手段來實現。這一章會講解goroutine和channel,其支持“順序進程通信”(communicating sequential processes)或被簡稱爲CSP。CSP是一個現代的併發編程模型,在這種編程模型中值會在不同的運行實例(goroutine)中傳遞,盡管大多數情況下被限製在單一實例中。第9章會覆蓋到更爲傳統的併發模型:多線程共享內存,如果你在其它的主流語言中寫過併發程序的話可能會更熟悉一些。第9章同時會講一些本章不會深入的併發程序帶來的重要風險和陷阱。
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user