diff --git a/README.md b/README.md index e310163..5cc3305 100644 --- a/README.md +++ b/README.md @@ -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) diff --git a/ch0/ch0-03.md b/ch0/ch0-03.md index 3d26242..e838d67 100644 --- a/ch0/ch0-03.md +++ b/ch0/ch0-03.md @@ -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 提供的説明安裝。 diff --git a/ch1/ch1-02.md b/ch1/ch1-02.md index 362929e..6d1d9c6 100644 --- a/ch1/ch1-02.md +++ b/ch1/ch1-02.md @@ -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]。 diff --git a/ch1/ch1-03.md b/ch1/ch1-03.md index df633a0..14cda4e 100644 --- a/ch1/ch1-03.md +++ b/ch1/ch1-03.md @@ -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.Scanner,ioutil.ReadFile和ioutil.WriteFile使用的是*os.File的Read和Write方法,不過一般程序員併不需要去直接了解到其底層實現細節,在bufio和io/ioutil包中提供的方法已經足夠好用。 diff --git a/ch11/ch11-01.md b/ch11/ch11-01.md index 22c2b6d..db9bb3e 100644 --- a/ch11/ch11-01.md +++ b/ch11/ch11-01.md @@ -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包用於調用相應的測試函數,然後構建併運行、報告測試結果,最後清理測試中生成的臨時文件。 diff --git a/ch11/ch11.md b/ch11/ch11.md index 6ff3413..c8da1b6 100644 --- a/ch11/ch11.md +++ b/ch11/ch11.md @@ -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代碼過程是類似的;它併不需要學習新的符號、規則和工具。 diff --git a/ch2/ch2-03-3.md b/ch2/ch2-03-3.md index 87aae4b..87201cf 100644 --- a/ch2/ch2-03-3.md +++ b/ch2/ch2-03-3.md @@ -14,10 +14,14 @@ 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 +} ``` 每次調用new函數都是返迴一個新的變量的地址,因此下面兩個地址是不同的: diff --git a/ch2/ch2-03-4.md b/ch2/ch2-03-4.md index 33588f0..1413cb2 100644 --- a/ch2/ch2-03-4.md +++ b/ch2/ch2-03-4.md @@ -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語言的自動垃圾收集器對編寫正確的代碼是一個鉅大的幫助,但也併不是説你完全不用考慮內存了。你雖然不需要顯式地分配和釋放內存,但是要編寫高效的程序你依然需要了解變量的生命週期。例如,如果將指向短生命週期對象的指針保存到具有長生命週期的對象中,特别是保存到全局變量時,會阻止對短生命週期對象的垃圾迴收(從而可能影響程序的性能)。 diff --git a/ch5/ch5-04.md b/ch5/ch5-04.md index 3b61b67..7d83d97 100644 --- a/ch5/ch5-04.md +++ b/ch5/ch5-04.md @@ -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異常。 diff --git a/ch5/ch5-05.md b/ch5/ch5-05.md index a517eae..1cf680f 100644 --- a/ch5/ch5-05.md +++ b/ch5/ch5-05.md @@ -35,7 +35,7 @@ 但是函數值之間是不可比較的,也不能用函數值作爲map的key。 -函數值使得我們不僅僅可以通過數據來參數化函數,亦可通過行爲。標準庫中包含許多這樣的例子。下面的代碼展示了如何使用這個技巧。string.Map對字符串中的每個字符調用add1函數,併將每個add1函數的返迴值組成一個新的字符串返迴給調用者。 +函數值使得我們不僅僅可以通過數據來參數化函數,亦可通過行爲。標準庫中包含許多這樣的例子。下面的代碼展示了如何使用這個技巧。strings.Map對字符串中的每個字符調用add1函數,併將每個add1函數的返迴值組成一個新的字符串返迴給調用者。 ```Go func add1(r rune) rune { return r + 1 } diff --git a/ch9/ch9-07.md b/ch9/ch9-07.md index c859fa0..ce1846f 100644 --- a/ch9/ch9-07.md +++ b/ch9/ch9-07.md @@ -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的channel,Get的調用方用這個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的調用結果不應該被緩存。 + diff --git a/preface.md b/preface.md index 17893b0..c2eca5e 100644 --- a/preface.md +++ b/preface.md @@ -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) diff --git a/progress.md b/progress.md index 2bb4bf1..fee5f7a 100644 --- a/progress.md +++ b/progress.md @@ -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