This commit is contained in:
foreversmart 2016-01-18 23:28:06 +08:00
commit 2da4b3e7f8
13 changed files with 394 additions and 49 deletions

View File

@ -2,11 +2,9 @@
Go語言聖經 [《The Go Programming Language》](http://gopl.io) 中文版本,僅供學習交流之用。
- 項目主頁http://github.com/golang-china/gopl-zh
- 項目進度http://github.com/golang-china/gopl-zh/blob/master/progress.md
- 參與人員http://github.com/golang-china/gopl-zh/blob/master/CONTRIBUTORS.md
- 離線版本http://github.com/golang-china/gopl-zh/archive/gh-pages.zip
- 在線預覽http://golang-china.github.io/gopl-zh
- 項目主頁http://github.com/golang-china/gopl-zh
- 離線版本http://github.com/golang-china/gopl-zh/archive/gh-pages.zip
- 原版官網http://gopl.io
[![](cover_middle.jpg)](https://github.com/golang-china/gopl-zh)

View File

@ -1,28 +1,28 @@
## 本書的組織
我們假設你已經有一個或多個其他編程語言的使用經歷不管是類似C、c++或Java的編譯型語言還是類似Python、Ruby、JavaScript的腳本語言因此我們不會像對完全的編程語言初學者那樣解釋所有的細節。因爲Go語言的 變量、常量、表達式、控製流和函數等基本語法也是類似的。
我們假設你已經有一種或多種其他編程語言的使用經歷不管是類似C、c++或Java的編譯型語言還是類似Python、Ruby、JavaScript的腳本語言因此我們不會像對完全的編程語言初學者那樣解釋所有的細節。因爲Go語言的變量、常量、表達式、控製流和函數等基本語法也是類似的。
第一章包含了本敎程的基本結構通過十幾個程序介紹了用Go語言如何實現 類似讀寫文件、文本格式化、創建圖像、網絡客戶端和服務器通訊等日常工作。
第二章描述了一個Go語言程序的基本元素結構、變量、定義類型、包和文件、以及作用域的概念。第三章討論了數字、布爾值、字符串和常量併演示了如何顯示和處理Unicode字符。第四章描述了複合類型從簡單的數組、字典、切片到動態列表。第五章涵蓋了函數併討論了錯誤處理、panic和recover還有defer語句。
第二章描述了Go語言程序的基本元素結構、變量、新類型定義、包和文件、以及作用域的概念。第三章討論了數字、布爾值、字符串和常量併演示了如何顯示和處理Unicode字符。第四章描述了複合類型從簡單的數組、字典、切片到動態列表。第五章涵蓋了函數併討論了錯誤處理、panic和recover還有defer語句。
第一章到第五章是基礎部分,對於任何主流命令式編程語言這個部分都是類似的。雖然有時候Go語言的語法和風格會有自己的特色但是大多數程序員將能很快地適應。剩下的章節是Go語言中特有地方:方法、接口、併發、包、測試和反射等語言特性。
第一章到第五章是基礎部分,主流命令式編程語言這部分都類似。個别之處Go語言有自己特色的語法和風格但是大多數程序員能很快適應。其餘章節是Go語言特有的:方法、接口、併發、包、測試和反射等語言特性。
Go語言的面向對象是不同尋常的。它沒有類層次結構,甚至可以説沒有類;僅僅通過組合(而不是繼承)簡單的對象來構建複雜的對象。方法不僅可以定義在結構體上, 而且可以定義在任何用戶自定義的類型上;併且具體類型和抽象類型(接口)之間的關繫是隱式的,所以很多類型的設計者可能併不知道該類型到底滿足了哪些接口。方法將在第六章討論,接口將在第七章將討論。
Go語言的面向對象機製與一般語言不同。它沒有類層次結構,甚至可以説沒有類;僅僅通過組合(而不是繼承)簡單的對象來構建複雜的對象。方法不僅可以定義在結構體上, 而且可以定義在任何用戶自定義的類型上;併且具體類型和抽象類型(接口)之間的關繫是隱式的,所以很多類型的設計者可能併不知道該類型到底實現了哪些接口。方法在第六章討論,接口在第七章討論。
第八章討論了基於順序通信進程(CSP)概念的併發編程,通過使用goroutines和channels處理併發編程。第九章則討論了更爲傳統的基於共享變量的併發編程。
第八章討論了基於順序通信進程(CSP)概念的併發編程使用goroutines和channels處理併發編程。第九章則討論了傳統的基於共享變量的併發編程。
第十章描述了包機製和包的組織結構。這一章還展示了如何有效的利用Go自帶的工具通過一個命令提供了編譯、測試、基準測試、代碼格式化、文檔和許多其他任務。
第十章描述了包機製和包的組織結構。這一章還展示了如何有效的利用Go自帶的工具使用單個命令完成編譯、測試、基準測試、代碼格式化、文檔以及其他諸多任務。
第十一章討論了單元測試Go語言的工具和標準庫中集成的輕量級的測試功能,從而避免了采用強大但複雜的測試框架。測試庫提供一些基本的構件,如果有必要可以用來構建更複雜的測試構件。
第十一章討論了單元測試Go語言的工具和標準庫中集成了輕量級的測試功能,避免了強大但複雜的測試框架。測試庫提供了一些基本構件,必要時可以用來構建複雜的測試構件。
第十二章討論了反射,一個程序在運行期間來審視自己的能力。反射是一個強大的編程工具,不過要謹慎地使用;這一章通過用利用反射機製實現一些重要的Go語言庫函數來展示了反射的強大用法。第十三章解釋了底層編程的細節通過使用unsafe包來繞過Go語言安全的類型繫統當然有時這是必要的
第十二章討論了反射,一種程序在運行期間審視自己的能力。反射是一個強大的編程工具不過要謹慎地使用這一章利用反射機製實現一些重要的Go語言庫函數, 展示了反射的強大用法。第十三章解釋了底層編程的細節在必要時可以使用unsafe包繞過Go語言安全的類型繫統
有些章節的後面可能會有一些練習你可以根據你對Go語言的理解然後脩改書中的例子來探索Go語言的其他用法。
部分章節的後面有練習題根據對Go語言的理解脩改書中的例子來探索Go語言的用法。
書中所有的代碼都可以從 http://gopl.io 上的Git倉庫下載。go get命令可以根據每個例子的導入路徑智能地獲取、構建併安裝。隻需要選擇一個目録作爲工作空間然後將GOPATH環境指向這個工作目録
書中所有的代碼都可以從 http://gopl.io 上的Git倉庫下載。go get命令根據每個例子的導入路徑智能地獲取、構建併安裝。隻需要選擇一個目録作爲工作空間然後將GOPATH環境變量設置爲該路徑
Go語言工具將在必要時創建的相應的目録。例如:
必要時Go語言工具會創建目録。例如:
```
$ export GOPATH=$HOME/gobook # 選擇工作目録
@ -31,12 +31,12 @@ $ $GOPATH/bin/helloworld # 運行程序
Hello, 世界 # 這是中文
```
要運行這些例子, 你需要安裝Go1.5以上的版本.
運行這些例子需要安裝Go1.5以上的版本。
```
$ go version
go version go1.5 linux/amd64
```
如果你用的是其他的操作繫統, 請參考 https://golang.org/doc/install 提供的説明安裝。
如果使用其他的操作繫統, 請參考 https://golang.org/doc/install 提供的説明安裝。

View File

@ -147,7 +147,7 @@ func main() {
fmt.Println(os.Args[1:])
```
這個輸出結果和前面的string.Join得到的結果很相似隻是被自動地放到了一個方括號里對slice調用Println函數都會被打印成這樣形式的結果。
這個輸出結果和前面的strings.Join得到的結果很相似隻是被自動地放到了一個方括號里對slice調用Println函數都會被打印成這樣形式的結果。
**練習 1.1** 脩改echo程序使其能夠打印os.Args[0]。

View File

@ -167,7 +167,7 @@ func main() {
}
```
ReadFile函數返迴一個byte的slice這個slice必須被轉換爲string之後才能夠用string.Split方法來進行處理。我們在3.5.4節中會更詳細地講解string和byte slice字節數組
ReadFile函數返迴一個byte的slice這個slice必須被轉換爲string之後才能夠用strings.Split方法來進行處理。我們在3.5.4節中會更詳細地講解string和byte slice字節數組
在更底層一些的地方bufio.Scannerioutil.ReadFile和ioutil.WriteFile使用的是*os.File的Read和Write方法不過一般程序員併不需要去直接了解到其底層實現細節在bufio和io/ioutil包中提供的方法已經足夠好用。

View File

@ -1,8 +1,7 @@
## 11.1. go test
`go test` 是一個按照一定的約定和組織的測試代碼的驅動程序. 在包目録內, 以 `_test.go` 爲後綴名的源文件併不是`go build`構建包的以部分, 它們是 `go test` 測試的一部分.
go test命令是一個按照一定的約定和組織的測試代碼的驅動程序。在包目録內所有以_test.go爲後綴名的源文件併不是go build構建包的一部分它們是go test測試的一部分。
`*_test.go` 文件中, 有三種類型的函數: 測試函數, 基準測試函數, 例子函數. 一個測試函數是以 Test 爲函數名前綴的函數, 用於測試程序的一些邏輯行爲是否正確; `go test` 會調用這些測試函數併報告測試結果是 PASS 或 FAIL. 基準測試函數是以Benchmark爲函數名前綴的函數, 用於衡量一些函數的性能; `go test` 會多次運行基準函數以計算一個平均的執行時間. 例子函數是以Example爲函數名前綴的函數, 提供一個由機器檢測正確性的例子文檔. 我們將在 11.2 節 討論測試函數的細節, 在 11.4 節討論基準測試函數的細節, 在 11.6 討論例子函數的細節.
`go test` 命令會遍歷所有的 `*_test.go` 文件中上述函數, 然後生成一個臨時的main包調用相應的測試函數, 然後構建併運行, 報告測試結果, 最後清理臨時文件.
在\*_test.go文件中有三種類型的函數測試函數、基準測試函數、示例函數。一個測試函數是以Test爲函數名前綴的函數用於測試程序的一些邏輯行爲是否正確go test命令會調用這些測試函數併報告測試結果是PASS或FAIL。基準測試函數是以Benchmark爲函數名前綴的函數它們用於衡量一些函數的性能go test命令會多次運行基準函數以計算一個平均的執行時間。示例函數是以Example爲函數名前綴的函數提供一個由編譯器保證正確性的示例文檔。我們將在11.2節討論測試函數的所有細節病在11.4節討論基準測試函數的細節然後在11.6節討論示例函數的細節。
go test命令會遍歷所有的\*_test.go文件中符合上述命名規則的函數然後生成一個臨時的main包用於調用相應的測試函數然後構建併運行、報告測試結果最後清理測試中生成的臨時文件。

View File

@ -1,16 +1,13 @@
# 第十一章 測試
Maurice Wilkes, 第一個存儲程序計算機 EDSAC 的設計者, 1949年在他的實驗室爬樓梯時有一個頓悟. 在《計算機先驅迴憶録》(Memoirs of a Computer Pioneer)里, 他迴憶到: "忽然間有一種醍醐灌頂的感覺, 我整個後半生的美好時光都將在尋找程序BUG中度過了.". 肯定從那之後的每一個存儲程序的碼農都可以同情 Wilkes 的想法, 雖然也許不是沒有人睏惑於他對軟件開發的難度的天眞看法.
Maurice Wilkes第一個存儲程序計算機EDSAC的設計者1949年他在實驗室爬樓梯時有一個頓悟。在《計算機先驅迴憶録》Memoirs of a Computer Pioneer他迴憶到“忽然間有一種醍醐灌頂的感覺我整個後半生的美好時光都將在尋找程序BUG中度過了”。肯定從那之後的大部分正常的碼農都會同情Wilkes過份悲觀的想法雖然也許不是沒有人睏惑於他對軟件開發的難度的天眞看法。
現在的程序已經遠比 Wilkes 時代的更大也更複雜, 也有許多技術可以讓軟件的複雜性可得到控製. 其中有兩種技術在實踐中證明是比較有效的. 第一種是代碼在被正式部署前需要進行代碼評審. 第二種是測試, 是本章的討論主題.
現在的程序已經遠比Wilkes時代的更大也更複雜也有許多技術可以讓軟件的複雜性可得到控製。其中有兩種技術在實踐中證明是比較有效的。第一種是代碼在被正式部署前需要進行代碼評審。第二種則是測試也就是本章的討論主題。
我們説測試的時候一般是指自動化測試, 也就是寫一些小的程序用來檢測被測試代碼(産品代碼)的行爲和預期的一樣, 這些通常都是精心挑選的執行某些特定的功能或者是通過隨機性的輸入要驗證邊界的處理.
軟件測試是一個鉅大的領域. 測試的任務一般占據了一些程序員的部分時間和另一些程序員的全部時間. 和軟件測試技術相關的圖書或博客文章有成韆上萬之多. 每一種主流的編程語言, 都有一打的用於測試的軟件包, 也有大量的測試相關的理論, 每種都吸引了大量技術先驅和追隨者. 這些都足以説服那些想要編寫有效測試的程序員重新學習一套全新的技能.
Go語言的測試技術是相對低級的. 它依賴一個 'go test' 測試命令, 和一組按照約定方式編寫的測試函數, 測試命令可以運行測試函數. 編寫相對輕量級的純測試代碼是有效的, 而且它很容易延伸到基準測試和示例文檔.
在實踐中, 編寫測試代碼和編寫程序本身併沒有多大區别. 我們編寫的每一個函數也是針對每個具體的任務. 我們必須小心處理邊界條件, 思考合適的數據結構, 推斷合適的輸入應該産生什麽樣的結果輸出. 編程測試代碼和編寫普通的Go代碼過程是類似的; 它併不需要學習新的符號, 規則和工具.
我們説測試的時候一般是指自動化測試,也就是寫一些小的程序用來檢測被測試代碼(産品代碼)的行爲和預期的一樣,這些通常都是精心設計的執行某些特定的功能或者是通過隨機性的輸入要驗證邊界的處理。
軟件測試是一個鉅大的領域。測試的任務可能已經占據了一些程序員的部分時間和另一些程序員的全部時間。和軟件測試技術相關的圖書或博客文章有成韆上萬之多。對於每一種主流的編程語言,都會有一打的用於測試的軟件包,同時也有大量的測試相關的理論,而且每種都吸引了大量技術先驅和追隨者。這些都足以説服那些想要編寫有效測試的程序員重新學習一套全新的技能。
Go語言的測試技術是相對低級的。它依賴一個go test測試命令和一組按照約定方式編寫的測試函數測試命令可以運行這些測試函數。編寫相對輕量級的純測試代碼是有效的而且它很容易延伸到基準測試和示例文檔。
在實踐中編寫測試代碼和編寫程序本身併沒有多大區别。我們編寫的每一個函數也是針對每個具體的任務。我們必須小心處理邊界條件思考合適的數據結構推斷合適的輸入應該産生什麽樣的結果輸出。編程測試代碼和編寫普通的Go代碼過程是類似的它併不需要學習新的符號、規則和工具。

View File

@ -14,9 +14,13 @@ fmt.Println(*p) // "2"
下面的兩個newInt函數有着相同的行爲
```Go
func newInt() *int { func newInt() *int {
return new(int) var dummy int
} return &dummy
func newInt() *int {
return new(int)
}
func newInt() *int {
var dummy int
return &dummy
}
```

View File

@ -37,14 +37,19 @@ for t := 0.0; t < cycles*2*math.Pi; t += res {
```Go
var global *int
func f() { func g() {
var x int y := new(int)
x = 1 *y = 1
global = &x }
func f() {
var x int
x = 1
global = &x
}
func g() {
y := new(int)
*y = 1
}
```
這里的x變量必須在堆上分配因爲它在函數退出後依然可以通過包一級的global變量找到雖然它是在函數內部定義的用Go語言的術語説這個x局部變量從函數f中逃逸了。相反當g函數返迴時變量`*y`將是不可達的,也就是説可以馬上被迴收的。因此,`*y`併沒有從函數g中逃逸編譯器可以選擇在棧上分配`*y`的存儲空間譯註也可以選擇在堆上分配然後由Go語言的GC迴收這個變量的內存空間雖然這里用的是new方式。其實在任何時候你併不需爲了編寫正確的代碼而要考慮變量的逃逸行爲要記住的是逃逸的變量需要額外分配內存同時對性能的優化可能會産生細微的影響。
f函數里的x變量必須在堆上分配因爲它在函數退出後依然可以通過包一級的global變量找到雖然它是在函數內部定義的用Go語言的術語説這個x局部變量從函數f中逃逸了。相反當g函數返迴時變量`*y`將是不可達的,也就是説可以馬上被迴收的。因此,`*y`併沒有從函數g中逃逸編譯器可以選擇在棧上分配`*y`的存儲空間譯註也可以選擇在堆上分配然後由Go語言的GC迴收這個變量的內存空間雖然這里用的是new方式。其實在任何時候你併不需爲了編寫正確的代碼而要考慮變量的逃逸行爲要記住的是逃逸的變量需要額外分配內存同時對性能的優化可能會産生細微的影響。
Go語言的自動垃圾收集器對編寫正確的代碼是一個鉅大的幫助但也併不是説你完全不用考慮內存了。你雖然不需要顯式地分配和釋放內存但是要編寫高效的程序你依然需要了解變量的生命週期。例如如果將指向短生命週期對象的指針保存到具有長生命週期的對象中特别是保存到全局變量時會阻止對短生命週期對象的垃圾迴收從而可能影響程序的性能

View File

@ -1,7 +1,7 @@
## 5.4. 錯誤
在Go中有一部分函數總是能成功的運行。比如string.Contains和strconv.FormatBool函數對各種可能的輸入都做了良好的處理使得運行時幾乎不會失敗除非遇到災難性的、不可預料的情況比如運行時的內存溢出。導致這種錯誤的原因很複雜難以處理從錯誤中恢複的可能性也很低。
在Go中有一部分函數總是能成功的運行。比如strings.Contains和strconv.FormatBool函數對各種可能的輸入都做了良好的處理使得運行時幾乎不會失敗除非遇到災難性的、不可預料的情況比如運行時的內存溢出。導致這種錯誤的原因很複雜難以處理從錯誤中恢複的可能性也很低。
還有一部分函數隻要輸入的參數滿足一定條件也能保證運行成功。比如time.Date函數該函數將年月日等參數構造成time.Time對象除非最後一個參數時區是nil。這種情況下會引發panic異常。panic是來自被調函數的信號表示發生了某個已知的bug。一個良好的程序永遠不應該發生panic異常。

View File

@ -35,7 +35,7 @@
但是函數值之間是不可比較的也不能用函數值作爲map的key。
函數值使得我們不僅僅可以通過數據來參數化函數亦可通過行爲。標準庫中包含許多這樣的例子。下面的代碼展示了如何使用這個技巧。string.Map對字符串中的每個字符調用add1函數併將每個add1函數的返迴值組成一個新的字符串返迴給調用者。
函數值使得我們不僅僅可以通過數據來參數化函數亦可通過行爲。標準庫中包含許多這樣的例子。下面的代碼展示了如何使用這個技巧。strings.Map對字符串中的每個字符調用add1函數併將每個add1函數的返迴值組成一個新的字符串返迴給調用者。
```Go
func add1(r rune) rune { return r + 1 }

View File

@ -1,3 +1,347 @@
## 9.7. 示例: 併發的非阻塞緩存
TODO
本節中我們會做一個無阻塞的緩存,這種工具可以幫助我們來解決現實世界中併發程序出現但沒有現成的庫可以解決的問題。這個問題叫作緩存(memoizing)函數(譯註Memoization的定義 memoization 一詞是Donald Michie 根據拉丁語memorandum杜撰的一個詞。相應的動詞、過去分詞、ing形式有memoiz、memoized、memoizing.),也就是説,我們需要緩存函數的返迴結果,這樣在對函數進行調用的時候,我們就隻需要一次計算,之後隻要返迴計算的結果就可以了。我們的解決方案會是併發安全且會避免對整個緩存加鎖而導致所有操作都去爭一個鎖的設計。
我們將使用下面的httpGetBody函數作爲我們需要緩存的函數的一個樣例。這個函數會去進行HTTP GET請求併且獲取http響應body。對這個函數的調用本身開銷是比較大的所以我們盡量盡量避免在不必要的時候反複調用。
```go
func httpGetBody(url string) (interface{}, error) {
resp, err := http.Get(url)
if err != nil {
return nil, err
}
defer resp.Body.Close()
return ioutil.ReadAll(resp.Body)
}
```
最後一行稍微隱藏了一些細節。ReadAll會返迴兩個結果一個[]byte數組和一個錯誤不過這兩個對象可以被賦值給httpGetBody的返迴聲明里的interface{}和error類型所以我們也就可以這樣返迴結果併且不需要額外的工作了。我們在httpGetBody中選用這種返迴類型是爲了使其可以與緩存匹配。
下面是我們要設計的cache的第一個“草稿”
```go
gopl.io/ch9/memo1
// Package memo provides a concurrency-unsafe
// memoization of a function of type Func.
package memo
// A Memo caches the results of calling a Func.
type Memo struct {
f Func
cache map[string]result
}
// Func is the type of the function to memoize.
type Func func(key string) (interface{}, error)
type result struct {
value interface{}
err error
}
func New(f Func) *Memo {
return &Memo{f: f, cache: make(map[string]result)}
}
// NOTE: not concurrency-safe!
func (memo *Memo) Get(key string) (interface{}, error) {
res, ok := memo.cache[key]
if !ok {
res.value, res.err = memo.f(key)
memo.cache[key] = res
}
return res.value, res.err
}
```
Memo實例會記録需要緩存的函數f(類型爲Func),以及緩存內容(里面是一個string到result映射的map)。每一個result都是都是簡單的函數返迴的值對兒--一個值和一個錯誤值。繼續下去我們會展示一些Memo的變種不過所有的例子都會遵循這些上面的這些方面。
下面是一個使用Memo的例子。對於流入的URL的每一個元素我們都會調用Get併打印調用延時以及其返迴的數據大小的log
```go
m := memo.New(httpGetBody)
for url := range incomingURLs() {
start := time.Now()
value, err := m.Get(url)
if err != nil {
log.Print(err)
}
fmt.Printf("%s, %s, %d bytes\n",
url, time.Since(start), len(value.([]byte)))
}
```
我們可以使用測試包(第11章的主題)來繫統地鑒定緩存的效果。從下面的測試輸出我們可以看到URL流包含了一些重複的情況盡管我們第一次對每一個URL的(\*Memo).Get的調用都會花上幾百毫秒但第二次就隻需要花1毫秒就可以返迴完整的數據了。
```
$ go test -v gopl.io/ch9/memo1
=== RUN Test
https://golang.org, 175.026418ms, 7537 bytes
https://godoc.org, 172.686825ms, 6878 bytes
https://play.golang.org, 115.762377ms, 5767 bytes
http://gopl.io, 749.887242ms, 2856 bytes
https://golang.org, 721ns, 7537 bytes
https://godoc.org, 152ns, 6878 bytes
https://play.golang.org, 205ns, 5767 bytes
http://gopl.io, 326ns, 2856 bytes
--- PASS: Test (1.21s)
PASS
ok gopl.io/ch9/memo1 1.257s
```
這個測試是順序地去做所有的調用的。
由於這種彼此獨立的HTTP請求可以很好地併發我們可以把這個測試改成併發形式。可以使用sync.WaitGroup來等待所有的請求都完成之後再返迴。
```go
m := memo.New(httpGetBody)
var n sync.WaitGroup
for url := range incomingURLs() {
n.Add(1)
go func(url string) {
start := time.Now()
value, err := m.Get(url)
if err != nil {
log.Print(err)
}
fmt.Printf("%s, %s, %d bytes\n",
url, time.Since(start), len(value.([]byte)))
n.Done()
}(url)
}
n.Wait()
```
這次測試跑起來更快了然而不幸的是貌似這個測試不是每次都能夠正常工作。我們註意到有一些意料之外的cache miss(緩存未命中),或者命中了緩存但卻返迴了錯誤的值,或者甚至會直接崩潰。
但更糟糕的是,有時候這個程序還是能正確的運行(譯也就是最讓人崩潰的偶發bug)所以我們甚至可能都不會意識到這個程序有bug。。但是我們可以使用-race這個flag來運行程序競爭檢測器(§9.6)會打印像下面這樣的報告:
```
$ go test -run=TestConcurrent -race -v gopl.io/ch9/memo1
=== RUN TestConcurrent
...
WARNING: DATA RACE
Write by goroutine 36:
runtime.mapassign1()
~/go/src/runtime/hashmap.go:411 +0x0
gopl.io/ch9/memo1.(*Memo).Get()
~/gobook2/src/gopl.io/ch9/memo1/memo.go:32 +0x205
...
Previous write by goroutine 35:
runtime.mapassign1()
~/go/src/runtime/hashmap.go:411 +0x0
gopl.io/ch9/memo1.(*Memo).Get()
~/gobook2/src/gopl.io/ch9/memo1/memo.go:32 +0x205
...
Found 1 data race(s)
FAIL gopl.io/ch9/memo1 2.393s
```
memo.go的32行出現了兩次説明有兩個goroutine在沒有同步榦預的情況下更新了cache map。這表明Get不是併發安全的存在數據競爭。
```go
28 func (memo *Memo) Get(key string) (interface{}, error) {
29 res, ok := memo.cache(key)
30 if !ok {
31 res.value, res.err = memo.f(key)
32 memo.cache[key] = res
33 }
34 return res.value, res.err
35 }
```
最簡單的使cache併發安全的方式是使用基於監控的同步。隻要給Memo加上一個mutex在Get的一開始獲取互斥鎖return的時候釋放鎖就可以讓cache的操作發生在臨界區內了
```go
gopl.io/ch9/memo2
type Memo struct {
f Func
mu sync.Mutex // guards cache
cache map[string]result
}
// Get is concurrency-safe.
func (memo *Memo) Get(key string) (value interface{}, err error) {
res, ok := memo.cache[key] if!ok{
res.value, res.err = memo.f(key)
memo.cache[key] = res
memo.mu.Lock()
res, ok := memo.cache[key]
if !ok {
res.value, res.err = memo.f(key)
memo.cache[key] = res
}
memo.mu.Unlock()
return res.value, res.err
}
```
測試依然併發進行但這迴競爭檢査器“沉默”了。不幸的是對於Memo的這一點改變使我們完全喪失了併發的性能優點。每次對f的調用期間都會持有鎖Get將本來可以併行運行的I/O操作串行化了。我們本章的目的是完成一個無鎖緩存而不是現在這樣的將所有請求串行化的函數的緩存。
下一個Get的實現調用Get的goroutine會兩次獲取鎖査找階段獲取一次如果査找沒有返迴任何內容那麽進入更新階段會再次獲取。在這兩次獲取鎖的中間階段其它goroutine可以隨意使用cache。
```go
gopl.io/ch9/memo3
func (memo *Memo) Get(key string) (value interface{}, err error) {
memo.mu.Lock()
res, ok := memo.cache[key]
memo.mu.Unlock()
if !ok {
res.value, res.err = memo.f(key)
// Between the two critical sections, several goroutines
// may race to compute f(key) and update the map.
memo.mu.Lock()
memo.cache[key] = res
memo.mu.Unlock()
}
return res.value, res.err
}
```
這些脩改使性能再次得到了提陞但有一些URL被獲取了兩次。這種情況在兩個以上的goroutine同一時刻調用Get來請求同樣的URL時會發生。多個goroutine一起査詢cache發現沒有值然後一起調用f這個慢不拉嘰的函數。在得到結果後也都會去去更新map。其中一個獲得的結果會覆蓋掉另一個的結果。
理想情況下是應該避免掉多餘的工作的。而這種“避免”工作一般被稱爲duplicate suppression(重複抑製/避免)。下面版本的Memo每一個map元素都是指向一個條目的指針。每一個條目包含對函數f調用結果的內容緩存。與之前不同的是這次entry還包含了一個叫ready的channel。在條目的結果被設置之後這個channel就會被關閉以向其它goroutine廣播(§8.9)去讀取該條目內的結果是安全的了。
```go
gopl.io/ch9/memo4
type entry struct {
res result
ready chan struct{} // closed when res is ready
}
func New(f Func) *Memo {
return &Memo{f: f, cache: make(map[string]*entry)}
}
type Memo struct {
f Func
mu sync.Mutex // guards cache
cache map[string]*entry
}
func (memo *Memo) Get(key string) (value interface{}, err error) {
memo.mu.Lock()
e := memo.cache[key]
if e == nil {
// This is the first request for this key.
// This goroutine becomes responsible for computing
// the value and broadcasting the ready condition.
e = &entry{ready: make(chan struct{})}
memo.cache[key] = e
memo.mu.Unlock()
e.res.value, e.res.err = memo.f(key)
close(e.ready) // broadcast ready condition
} else {
// This is a repeat request for this key.
memo.mu.Unlock()
<-e.ready // wait for ready condition
}
return e.res.value, e.res.err
}
```
現在Get函數包括下面這些步驟了獲取互斥鎖來保護共享變量cache map査詢map中是否存在指定條目如果沒有找到那麽分配空間插入一個新條目釋放互斥鎖。如果存在條目的話且其值沒有寫入完成(也就是有其它的goroutine在調用f這個慢函數)時goroutine必須等待值ready之後才能讀到條目的結果。而想知道是否ready的話可以直接從ready channel中讀取由於這個讀取操作在channel關閉之前一直是阻塞。
如果沒有條目的話需要向map中插入一個沒有ready的條目當前正在調用的goroutine就需要負責調用慢函數、更新條目以及向其它所有goroutine廣播條目已經ready可讀的消息了。
條目中的e.res.value和e.res.err變量是在多個goroutine之間共享的。創建條目的goroutine同時也會設置條目的值其它goroutine在收到"ready"的廣播消息之後立刻會去讀取條目的值。盡管會被多個goroutine同時訪問但卻併不需要互斥鎖。ready channel的關閉一定會發生在其它goroutine接收到廣播事件之前因此第一個goroutine對這些變量的寫操作是一定發生在這些讀操作之前的。不會發生數據競爭。
這樣併發、不重複、無阻塞的cache就完成了。
上面這樣Memo的實現使用了一個互斥量來保護多個goroutine調用Get時的共享map變量。不妨把這種設計和前面提到的把map變量限製在一個單獨的monitor goroutine的方案做一些對比後者在調用Get時需要發消息。
Func、result和entry的聲明和之前保持一致
```go
// Func is the type of the function to memoize.
type Func func(key string) (interface{}, error)
// A result is the result of calling a Func.
type result struct {
value interface{}
err error
}
type entry struct {
res result
ready chan struct{} // closed when res is ready
}
```
然而Memo類型現在包含了一個叫做requests的channelGet的調用方用這個channel來和monitor goroutine來通信。requests channel中的元素類型是request。Get的調用方會把這個結構中的兩組key都填充好實際上用這兩個變量來對函數進行緩存的。另一個叫response的channel會被拿來發送響應結果。這個channel隻會傳迴一個單獨的值。
```go
gopl.io/ch9/memo5
// A request is a message requesting that the Func be applied to key.
type request struct {
key string
response chan<- result // the client wants a single result
}
type Memo struct{ requests chan request }
// New returns a memoization of f. Clients must subsequently call Close.
func New(f Func) *Memo {
memo := &Memo{requests: make(chan request)}
go memo.server(f)
return memo
}
func (memo *Memo) Get(key string) (interface{}, error) {
response := make(chan result)
memo.requests <- request{key, response}
res := <-response
return res.value, res.err
}
func (memo *Memo) Close() { close(memo.requests) }
```
上面的Get方法會創建一個response channel把它放進request結構中然後發送給monitor goroutine然後馬上又會接收到它。
cache變量被限製在了monitor goroutine (\*Memo).server中下面會看到。monitor會在循環中一直讀取請求直到request channel被Close方法關閉。每一個請求都會去査詢cache如果沒有找到條目的話那麽就會創建/插入一個新的條目。
```go
func (memo *Memo) server(f Func) {
cache := make(map[string]*entry)
for req := range memo.requests {
e := cache[req.key]
if e == nil {
// This is the first request for this key.
e = &entry{ready: make(chan struct{})}
cache[req.key] = e
go e.call(f, req.key) // call f(key)
}
go e.deliver(req.response)
}
}
func (e *entry) call(f Func, key string) {
// Evaluate the function.
e.res.value, e.res.err = f(key)
// Broadcast the ready condition.
close(e.ready)
}
func (e *entry) deliver(response chan<- result) {
// Wait for the ready condition.
<-e.ready
// Send the result to the client.
response <- e.res
}
```
和基於互斥量的版本類似第一個對某個key的請求需要負責去調用函數f併傳入這個key將結果存在條目里併關閉ready channel來廣播條目的ready消息。使用(\*entry).call來完成上述工作。
緊接着對同一個key的請求會發現map中已經有了存在的條目然後會等待結果變爲ready併將結果從response發送給客戶端的goroutien。上述工作是用(\*entry).deliver來完成的。對call和deliver方法的調用必須在自己的goroutine中進行以確保monitor goroutines不會因此而被阻塞住而沒法處理新的請求。
這個例子説明我們無論可以用上鎖,還是通信來建立併發程序都是可行的。
上面的兩種方案併不好説特定情境下哪種更好,不過了解他們還是有價值的。有時候從一種方式切換到另一種可以使你的代碼更爲簡潔。(譯註不是説好的golang推崇通信併發麽)
**練習 9.3** 擴展Func類型和(\*Memo).Get方法支持調用方提供一個可選的done channel使其具備通過該channel來取消整個操作的能力(§8.9)。一個被取消了的Func的調用結果不應該被緩存。

View File

@ -2,11 +2,9 @@
Go語言聖經 [《The Go Programming Language》](http://gopl.io) 中文版本,僅供學習交流之用。
- 項目主頁http://github.com/golang-china/gopl-zh
- 項目進度http://github.com/golang-china/gopl-zh/blob/master/progress.md
- 參與人員http://github.com/golang-china/gopl-zh/blob/master/CONTRIBUTORS.md
- 離線版本http://github.com/golang-china/gopl-zh/archive/gh-pages.zip
- 在線預覽http://golang-china.github.io/gopl-zh
- 項目主頁http://github.com/golang-china/gopl-zh
- 離線版本http://github.com/golang-china/gopl-zh/archive/gh-pages.zip
- 原版官網http://gopl.io
[![](cover_middle.jpg)](https://github.com/golang-china/gopl-zh)

View File

@ -84,7 +84,7 @@
- [x] 9.4 Memory Synchronization
- [x] 9.5 Lazy Initialization: sync.Once
- [x] 9.6 The Race Detector
- [ ] 9.7 Example: Concurrent Non-Blocking Cache
- [x] 9.7 Example: Concurrent Non-Blocking Cache
- [x] 9.8 Goroutines and Threads
- [x] Chapter 10: Packages and the Go Tool
- [x] 10.1 Introduction