From d76cb8576d4a66e10c4f8b033c72686515faaaba Mon Sep 17 00:00:00 2001 From: Xargin Date: Fri, 15 Jan 2016 17:05:40 +0800 Subject: [PATCH 01/12] =?UTF-8?q?=E4=BF=AE=E6=94=B9=E4=B8=8D=E9=80=9A?= =?UTF-8?q?=E9=A1=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ch9/ch9-02.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ch9/ch9-02.md b/ch9/ch9-02.md index 2de49a7..ade7abe 100644 --- a/ch9/ch9-02.md +++ b/ch9/ch9-02.md @@ -103,9 +103,9 @@ func Withdraw(amount int) bool { 上面这个例子中,Deposit会调用mu.Lock()第二次去获取互斥锁,但因为mutex已经锁上了,而无法被重入(译注:go里没有重入锁,关于重入锁的概念,请参考java)--也就是说没法对一个已经锁上的mutex来再次上锁--这会导致程序死锁,没法继续执行下去,Withdraw会永远阻塞下去。 -关于Go的互斥量不能重入这一点我们有很充分的理由。互斥量的目的是为了确保共享变量在程序执行时的关键点上能够保证不变性。不变性的其中之一是“没有goroutine访问共享变量”。但实际上对于mutex保护的变量来说,不变性还包括其它方面。当一个goroutine获得了一个互斥锁时,它会断定这种不变性能够被保持。其获取并保持锁期间,可能会去更新共享变量,这样不变性只是短暂地被破坏。然而当其释放锁之后,它必须保证不变性已经恢复原样。尽管一个可以重入的mutex也可以保证没有其它的goroutine在访问共享变量,但这种方式没法保证这些变量额外的不变性。(译注:这段翻译有点晕) +关于Go的互斥量不能重入这一点我们有很充分的理由。互斥量的目的是为了确保共享变量在程序执行时的关键点上能够保证不变性。不变性的其中之一是“没有goroutine访问共享变量”。但实际上对于mutex保护的变量来说,不变性还包括其它方面。当一个goroutine获得了一个互斥锁时,它会断定这种不变性能够被保持。其获取并持有锁期间,可能会去更新共享变量,这样不变性只是短暂地被破坏。然而当其释放锁之后,它必须保证不变性已经恢复原样。尽管一个可以重入的mutex也可以保证没有其它的goroutine在访问共享变量,但这种方式没法保证这些变量额外的不变性。(译注:这段翻译有点晕) -一个通用的解决方案是将一个函数分离为多个函数,比如我们把Deposit分离成两个:一个不导出的函数deposit,这个函数假设锁总是会被保持并去做实际的操作,另一个是导出的函数Deposit,这个函数会调用deposit,但在调用前会先去获取锁。同理我们可以将Withdraw也表示成这种形式: +一个通用的解决方案是将一个函数分离为多个函数,比如我们把Deposit分离成两个:一个不导出的函数deposit,这个函数假设锁总是会被持有并去做实际的操作,另一个是导出的函数Deposit,这个函数会调用deposit,但在调用前会先去获取锁。同理我们可以将Withdraw也表示成这种形式: ```go func Withdraw(amount int) bool { From e16eebfefa003161b25d638edf5f6e72a8593294 Mon Sep 17 00:00:00 2001 From: chai2010 Date: Mon, 18 Jan 2016 12:07:14 +0800 Subject: [PATCH 02/12] Fix typo Fixes #204 --- ch1/ch1-02.md | 2 +- ch1/ch1-03.md | 2 +- ch5/ch5-04.md | 2 +- ch5/ch5-05.md | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) 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/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 } From 1a780a2a52c69552043b17a65a55e5ec23dd1ca1 Mon Sep 17 00:00:00 2001 From: chai2010 Date: Mon, 18 Jan 2016 12:14:26 +0800 Subject: [PATCH 03/12] Fixes #205 --- ch2/ch2-03-3.md | 12 ++++++++---- ch2/ch2-03-4.md | 15 ++++++++++----- 2 files changed, 18 insertions(+), 9 deletions(-) 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語言的自動垃圾收集器對編寫正確的代碼是一個鉅大的幫助,但也併不是説你完全不用考慮內存了。你雖然不需要顯式地分配和釋放內存,但是要編寫高效的程序你依然需要了解變量的生命週期。例如,如果將指向短生命週期對象的指針保存到具有長生命週期的對象中,特别是保存到全局變量時,會阻止對短生命週期對象的垃圾迴收(從而可能影響程序的性能)。 From b505a826cb831b85cd4acfb55bfeed00fb3e8e82 Mon Sep 17 00:00:00 2001 From: Prife Zhu Date: Mon, 18 Jan 2016 13:05:27 +0800 Subject: [PATCH 04/12] polish up chapter ch0-03 --- ch0/ch0-03.md | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) 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 提供的説明安裝。 From 46e485dad111f106282f74c9f5ca705ec9dc4e43 Mon Sep 17 00:00:00 2001 From: Xargin Date: Mon, 18 Jan 2016 16:11:36 +0800 Subject: [PATCH 05/12] 9.7 done --- ch9/ch9-07.md | 346 +++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 345 insertions(+), 1 deletion(-) diff --git a/ch9/ch9-07.md b/ch9/ch9-07.md index c859fa0..29e460f 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的调用结果不应该被缓存。 + From 1dcc27524c508eaec661ea8eca54922d9a4b05d7 Mon Sep 17 00:00:00 2001 From: chai2010 Date: Mon, 18 Jan 2016 17:47:28 +0800 Subject: [PATCH 06/12] Update progress.md --- progress.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 From 687549b05dc16232d7e3556367ef5da0f890cc57 Mon Sep 17 00:00:00 2001 From: chai2010 Date: Mon, 18 Jan 2016 17:47:12 +0800 Subject: [PATCH 07/12] ch9-7: make loop --- ch9/ch9-07.md | 62 +++++++++++++++++++++++++-------------------------- 1 file changed, 31 insertions(+), 31 deletions(-) diff --git a/ch9/ch9-07.md b/ch9/ch9-07.md index 29e460f..567e3ce 100644 --- a/ch9/ch9-07.md +++ b/ch9/ch9-07.md @@ -1,8 +1,8 @@ ## 9.7. 示例: 併發的非阻塞緩存 -本节中我们会做一个无阻塞的缓存,这种工具可以帮助我们来解决现实世界中并发程序出现但没有现成的库可以解决的问题。这个问题叫作缓存(memoizing)函数(译注:Memoization的定义: memoization 一词是Donald Michie 根据拉丁语memorandum杜撰的一个词。相应的动词、过去分词、ing形式有memoiz、memoized、memoizing.),也就是说,我们需要缓存函数的返回结果,这样在对函数进行调用的时候,我们就只需要一次计算,之后只要返回计算的结果就可以了。我们的解决方案会是并发安全且会避免对整个缓存加锁而导致所有操作都去争一个锁的设计。 +本節中我們會做一個無阻塞的緩存,這種工具可以幫助我們來解決現實世界中併發程序出現但沒有現成的庫可以解決的問題。這個問題叫作緩存(memoizing)函數(譯註:Memoization的定義: memoization 一詞是Donald Michie 根據拉丁語memorandum杜撰的一個詞。相應的動詞、過去分詞、ing形式有memoiz、memoized、memoizing.),也就是説,我們需要緩存函數的返迴結果,這樣在對函數進行調用的時候,我們就隻需要一次計算,之後隻要返迴計算的結果就可以了。我們的解決方案會是併發安全且會避免對整個緩存加鎖而導致所有操作都去爭一個鎖的設計。 -我们将使用下面的httpGetBody函数作为我们需要缓存的函数的一个样例。这个函数会去进行HTTP GET请求并且获取http响应body。对这个函数的调用本身开销是比较大的,所以我们尽量尽量避免在不必要的时候反复调用。 +我們將使用下面的httpGetBody函數作爲我們需要緩存的函數的一個樣例。這個函數會去進行HTTP GET請求併且獲取http響應body。對這個函數的調用本身開銷是比較大的,所以我們盡量盡量避免在不必要的時候反複調用。 ```go func httpGetBody(url string) (interface{}, error) { @@ -15,9 +15,9 @@ func httpGetBody(url string) (interface{}, error) { } ``` -最后一行稍微隐藏了一些细节。ReadAll会返回两个结果,一个[]byte数组和一个错误,不过这两个对象可以被赋值给httpGetBody的返回声明里的interface{}和error类型,所以我们也就可以这样返回结果并且不需要额外的工作了。我们在httpGetBody中选用这种返回类型是为了使其可以与缓存匹配。 +最後一行稍微隱藏了一些細節。ReadAll會返迴兩個結果,一個[]byte數組和一個錯誤,不過這兩個對象可以被賦值給httpGetBody的返迴聲明里的interface{}和error類型,所以我們也就可以這樣返迴結果併且不需要額外的工作了。我們在httpGetBody中選用這種返迴類型是爲了使其可以與緩存匹配。 -下面是我们要设计的cache的第一个“草稿”: +下面是我們要設計的cache的第一個“草稿”: ```go gopl.io/ch9/memo1 @@ -54,9 +54,9 @@ func (memo *Memo) Get(key string) (interface{}, error) { } ``` -Memo实例会记录需要缓存的函数f(类型为Func),以及缓存内容(里面是一个string到result映射的map)。每一个result都是都是简单的函数返回的值对儿--一个值和一个错误值。继续下去我们会展示一些Memo的变种,不过所有的例子都会遵循这些上面的这些方面。 +Memo實例會記録需要緩存的函數f(類型爲Func),以及緩存內容(里面是一個string到result映射的map)。每一個result都是都是簡單的函數返迴的值對兒--一個值和一個錯誤值。繼續下去我們會展示一些Memo的變種,不過所有的例子都會遵循這些上面的這些方面。 -下面是一个使用Memo的例子。对于流入的URL的每一个元素我们都会调用Get,并打印调用延时以及其返回的数据大小的log: +下面是一個使用Memo的例子。對於流入的URL的每一個元素我們都會調用Get,併打印調用延時以及其返迴的數據大小的log: ```go m := memo.New(httpGetBody) @@ -71,7 +71,7 @@ for url := range incomingURLs() { } ``` -我们可以使用测试包(第11章的主题)来系统地鉴定缓存的效果。从下面的测试输出,我们可以看到URL流包含了一些重复的情况,尽管我们第一次对每一个URL的(\*Memo).Get的调用都会花上几百毫秒,但第二次就只需要花1毫秒就可以返回完整的数据了。 +我們可以使用測試包(第11章的主題)來繫統地鑒定緩存的效果。從下面的測試輸出,我們可以看到URL流包含了一些重複的情況,盡管我們第一次對每一個URL的(\*Memo).Get的調用都會花上幾百毫秒,但第二次就隻需要花1毫秒就可以返迴完整的數據了。 ``` $ go test -v gopl.io/ch9/memo1 @@ -89,9 +89,9 @@ PASS ok gopl.io/ch9/memo1 1.257s ``` -这个测试是顺序地去做所有的调用的。 +這個測試是順序地去做所有的調用的。 -由于这种彼此独立的HTTP请求可以很好地并发,我们可以把这个测试改成并发形式。可以使用sync.WaitGroup来等待所有的请求都完成之后再返回。 +由於這種彼此獨立的HTTP請求可以很好地併發,我們可以把這個測試改成併發形式。可以使用sync.WaitGroup來等待所有的請求都完成之後再返迴。 ```go m := memo.New(httpGetBody) @@ -113,9 +113,9 @@ n.Wait() ``` -这次测试跑起来更快了,然而不幸的是貌似这个测试不是每次都能够正常工作。我们注意到有一些意料之外的cache miss(缓存未命中),或者命中了缓存但却返回了错误的值,或者甚至会直接崩溃。 +這次測試跑起來更快了,然而不幸的是貌似這個測試不是每次都能夠正常工作。我們註意到有一些意料之外的cache miss(緩存未命中),或者命中了緩存但卻返迴了錯誤的值,或者甚至會直接崩潰。 -但更糟糕的是,有时候这个程序还是能正确的运行(译:也就是最让人崩溃的偶发bug),所以我们甚至可能都不会意识到这个程序有bug。。但是我们可以使用-race这个flag来运行程序,竞争检测器(§9.6)会打印像下面这样的报告: +但更糟糕的是,有時候這個程序還是能正確的運行(譯:也就是最讓人崩潰的偶發bug),所以我們甚至可能都不會意識到這個程序有bug。。但是我們可以使用-race這個flag來運行程序,競爭檢測器(§9.6)會打印像下面這樣的報告: ``` $ go test -run=TestConcurrent -race -v gopl.io/ch9/memo1 @@ -138,7 +138,7 @@ Found 1 data race(s) FAIL gopl.io/ch9/memo1 2.393s ``` -memo.go的32行出现了两次,说明有两个goroutine在没有同步干预的情况下更新了cache map。这表明Get不是并发安全的,存在数据竞争。 +memo.go的32行出現了兩次,説明有兩個goroutine在沒有同步榦預的情況下更新了cache map。這表明Get不是併發安全的,存在數據競爭。 ```go 28 func (memo *Memo) Get(key string) (interface{}, error) { @@ -151,7 +151,7 @@ memo.go的32行出现了两次,说明有两个goroutine在没有同步干预 35 } ``` -最简单的使cache并发安全的方式是使用基于监控的同步。只要给Memo加上一个mutex,在Get的一开始获取互斥锁,return的时候释放锁,就可以让cache的操作发生在临界区内了: +最簡單的使cache併發安全的方式是使用基於監控的同步。隻要給Memo加上一個mutex,在Get的一開始獲取互斥鎖,return的時候釋放鎖,就可以讓cache的操作發生在臨界區內了: ```go gopl.io/ch9/memo2 @@ -177,9 +177,9 @@ func (memo *Memo) Get(key string) (value interface{}, err error) { } ``` -测试依然并发进行,但这回竞争检查器“沉默”了。不幸的是对于Memo的这一点改变使我们完全丧失了并发的性能优点。每次对f的调用期间都会持有锁,Get将本来可以并行运行的I/O操作串行化了。我们本章的目的是完成一个无锁缓存,而不是现在这样的将所有请求串行化的函数的缓存。 +測試依然併發進行,但這迴競爭檢査器“沉默”了。不幸的是對於Memo的這一點改變使我們完全喪失了併發的性能優點。每次對f的調用期間都會持有鎖,Get將本來可以併行運行的I/O操作串行化了。我們本章的目的是完成一個無鎖緩存,而不是現在這樣的將所有請求串行化的函數的緩存。 -下一个Get的实现,调用Get的goroutine会两次获取锁:查找阶段获取一次,如果查找没有返回任何内容,那么进入更新阶段会再次获取。在这两次获取锁的中间阶段,其它goroutine可以随意使用cache。 +下一個Get的實現,調用Get的goroutine會兩次獲取鎖:査找階段獲取一次,如果査找沒有返迴任何內容,那麽進入更新階段會再次獲取。在這兩次獲取鎖的中間階段,其它goroutine可以隨意使用cache。 ```go gopl.io/ch9/memo3 @@ -200,9 +200,9 @@ func (memo *Memo) Get(key string) (value interface{}, err error) { } ``` -这些修改使性能再次得到了提升,但有一些URL被获取了两次。这种情况在两个以上的goroutine同一时刻调用Get来请求同样的URL时会发生。多个goroutine一起查询cache,发现没有值,然后一起调用f这个慢不拉叽的函数。在得到结果后,也都会去去更新map。其中一个获得的结果会覆盖掉另一个的结果。 +這些脩改使性能再次得到了提陞,但有一些URL被獲取了兩次。這種情況在兩個以上的goroutine同一時刻調用Get來請求同樣的URL時會發生。多個goroutine一起査詢cache,發現沒有值,然後一起調用f這個慢不拉嘰的函數。在得到結果後,也都會去去更新map。其中一個獲得的結果會覆蓋掉另一個的結果。 -理想情况下是应该避免掉多余的工作的。而这种“避免”工作一般被称为duplicate suppression(重复抑制/避免)。下面版本的Memo每一个map元素都是指向一个条目的指针。每一个条目包含对函数f调用结果的内容缓存。与之前不同的是这次entry还包含了一个叫ready的channel。在条目的结果被设置之后,这个channel就会被关闭,以向其它goroutine广播(§8.9)去读取该条目内的结果是安全的了。 +理想情況下是應該避免掉多餘的工作的。而這種“避免”工作一般被稱爲duplicate suppression(重複抑製/避免)。下面版本的Memo每一個map元素都是指向一個條目的指針。每一個條目包含對函數f調用結果的內容緩存。與之前不同的是這次entry還包含了一個叫ready的channel。在條目的結果被設置之後,這個channel就會被關閉,以向其它goroutine廣播(§8.9)去讀取該條目內的結果是安全的了。 ```go gopl.io/ch9/memo4 @@ -245,17 +245,17 @@ func (memo *Memo) Get(key string) (value interface{}, err error) { } ``` -现在Get函数包括下面这些步骤了:获取互斥锁来保护共享变量cache map,查询map中是否存在指定条目,如果没有找到那么分配空间插入一个新条目,释放互斥锁。如果存在条目的话且其值没有写入完成(也就是有其它的goroutine在调用f这个慢函数)时,goroutine必须等待值ready之后才能读到条目的结果。而想知道是否ready的话,可以直接从ready channel中读取,由于这个读取操作在channel关闭之前一直是阻塞。 +現在Get函數包括下面這些步驟了:獲取互斥鎖來保護共享變量cache map,査詢map中是否存在指定條目,如果沒有找到那麽分配空間插入一個新條目,釋放互斥鎖。如果存在條目的話且其值沒有寫入完成(也就是有其它的goroutine在調用f這個慢函數)時,goroutine必須等待值ready之後才能讀到條目的結果。而想知道是否ready的話,可以直接從ready channel中讀取,由於這個讀取操作在channel關閉之前一直是阻塞。 -如果没有条目的话,需要向map中插入一个没有ready的条目,当前正在调用的goroutine就需要负责调用慢函数、更新条目以及向其它所有goroutine广播条目已经ready可读的消息了。 +如果沒有條目的話,需要向map中插入一個沒有ready的條目,當前正在調用的goroutine就需要負責調用慢函數、更新條目以及向其它所有goroutine廣播條目已經ready可讀的消息了。 -条目中的e.res.value和e.res.err变量是在多个goroutine之间共享的。创建条目的goroutine同时也会设置条目的值,其它goroutine在收到"ready"的广播消息之后立刻会去读取条目的值。尽管会被多个goroutine同时访问,但却并不需要互斥锁。ready channel的关闭一定会发生在其它goroutine接收到广播事件之前,因此第一个goroutine对这些变量的写操作是一定发生在这些读操作之前的。不会发生数据竞争。 +條目中的e.res.value和e.res.err變量是在多個goroutine之間共享的。創建條目的goroutine同時也會設置條目的值,其它goroutine在收到"ready"的廣播消息之後立刻會去讀取條目的值。盡管會被多個goroutine同時訪問,但卻併不需要互斥鎖。ready channel的關閉一定會發生在其它goroutine接收到廣播事件之前,因此第一個goroutine對這些變量的寫操作是一定發生在這些讀操作之前的。不會發生數據競爭。 -这样并发、不重复、无阻塞的cache就完成了。 +這樣併發、不重複、無阻塞的cache就完成了。 -上面这样Memo的实现使用了一个互斥量来保护多个goroutine调用Get时的共享map变量。不妨把这种设计和前面提到的把map变量限制在一个单独的monitor goroutine的方案做一些对比,后者在调用Get时需要发消息。 +上面這樣Memo的實現使用了一個互斥量來保護多個goroutine調用Get時的共享map變量。不妨把這種設計和前面提到的把map變量限製在一個單獨的monitor goroutine的方案做一些對比,後者在調用Get時需要發消息。 -Func、result和entry的声明和之前保持一致: +Func、result和entry的聲明和之前保持一致: ```go // Func is the type of the function to memoize. @@ -273,7 +273,7 @@ type entry struct { } ``` -然而Memo类型现在包含了一个叫做requests的channel,Get的调用方用这个channel来和monitor goroutine来通信。requests channel中的元素类型是request。Get的调用方会把这个结构中的两组key都填充好,实际上用这两个变量来对函数进行缓存的。另一个叫response的channel会被拿来发送响应结果。这个channel只会传回一个单独的值。 +然而Memo類型現在包含了一個叫做requests的channel,Get的調用方用這個channel來和monitor goroutine來通信。requests channel中的元素類型是request。Get的調用方會把這個結構中的兩組key都填充好,實際上用這兩個變量來對函數進行緩存的。另一個叫response的channel會被拿來發送響應結果。這個channel隻會傳迴一個單獨的值。 ```go gopl.io/ch9/memo5 @@ -301,9 +301,9 @@ func (memo *Memo) Get(key string) (interface{}, error) { func (memo *Memo) Close() { close(memo.requests) } ``` -上面的Get方法,会创建一个response channel,把它放进request结构中,然后发送给monitor goroutine,然后马上又会接收到它。 +上面的Get方法,會創建一個response channel,把它放進request結構中,然後發送給monitor goroutine,然後馬上又會接收到它。 -cache变量被限制在了monitor goroutine (\*Memo).server中,下面会看到。monitor会在循环中一直读取请求,直到request channel被Close方法关闭。每一个请求都会去查询cache,如果没有找到条目的话,那么就会创建/插入一个新的条目。 +cache變量被限製在了monitor goroutine (\*Memo).server中,下面會看到。monitor會在循環中一直讀取請求,直到request channel被Close方法關閉。每一個請求都會去査詢cache,如果沒有找到條目的話,那麽就會創建/插入一個新的條目。 ```go func (memo *Memo) server(f Func) { @@ -335,13 +335,13 @@ func (e *entry) deliver(response chan<- result) { } ``` -和基于互斥量的版本类似,第一个对某个key的请求需要负责去调用函数f并传入这个key,将结果存在条目里,并关闭ready channel来广播条目的ready消息。使用(\*entry).call来完成上述工作。 +和基於互斥量的版本類似,第一個對某個key的請求需要負責去調用函數f併傳入這個key,將結果存在條目里,併關閉ready channel來廣播條目的ready消息。使用(\*entry).call來完成上述工作。 -紧接着对同一个key的请求会发现map中已经有了存在的条目,然后会等待结果变为ready,并将结果从response发送给客户端的goroutien。上述工作是用(\*entry).deliver来完成的。对call和deliver方法的调用必须在自己的goroutine中进行以确保monitor goroutines不会因此而被阻塞住而没法处理新的请求。 +緊接着對同一個key的請求會發現map中已經有了存在的條目,然後會等待結果變爲ready,併將結果從response發送給客戶端的goroutien。上述工作是用(\*entry).deliver來完成的。對call和deliver方法的調用必須在自己的goroutine中進行以確保monitor goroutines不會因此而被阻塞住而沒法處理新的請求。 -这个例子说明我们无论可以用上锁,还是通信来建立并发程序都是可行的。 +這個例子説明我們無論可以用上鎖,還是通信來建立併發程序都是可行的。 -上面的两种方案并不好说特定情境下哪种更好,不过了解他们还是有价值的。有时候从一种方式切换到另一种可以使你的代码更为简洁。(译注:不是说好的golang推崇通信并发么) +上面的兩種方案併不好説特定情境下哪種更好,不過了解他們還是有價值的。有時候從一種方式切換到另一種可以使你的代碼更爲簡潔。(譯註:不是説好的golang推崇通信併發麽) -练习 9.3: 扩展Func类型和(\*Memo).Get方法,支持调用方提供一个可选的done channel,使其具备通过该channel来取消整个操作的能力(§8.9)。一个被取消了的Func的调用结果不应该被缓存。 +練習 9.3: 擴展Func類型和(\*Memo).Get方法,支持調用方提供一個可選的done channel,使其具備通過該channel來取消整個操作的能力(§8.9)。一個被取消了的Func的調用結果不應該被緩存。 From 86650d2d26147eda75bfc714c475d209ff295da5 Mon Sep 17 00:00:00 2001 From: chai2010 Date: Mon, 18 Jan 2016 17:51:17 +0800 Subject: [PATCH 08/12] ch9-7: fmt code --- ch9/ch9-07.md | 234 +++++++++++++++++++++++++------------------------- 1 file changed, 117 insertions(+), 117 deletions(-) diff --git a/ch9/ch9-07.md b/ch9/ch9-07.md index 567e3ce..ce1846f 100644 --- a/ch9/ch9-07.md +++ b/ch9/ch9-07.md @@ -6,12 +6,12 @@ ```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) + resp, err := http.Get(url) + if err != nil { + return nil, err + } + defer resp.Body.Close() + return ioutil.ReadAll(resp.Body) } ``` @@ -27,30 +27,30 @@ package memo // A Memo caches the results of calling a Func. type Memo struct { - f Func - cache map[string]result + 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 + value interface{} + err error } func New(f Func) *Memo { - return &Memo{f: f, cache: make(map[string]result)} + 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 + res, ok := memo.cache[key] + if !ok { + res.value, res.err = memo.f(key) + memo.cache[key] = res + } + return res.value, res.err } ``` @@ -61,13 +61,13 @@ Memo實例會記録需要緩存的函數f(類型爲Func),以及緩存內容( ```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))) + 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))) } ``` @@ -97,17 +97,17 @@ ok gopl.io/ch9/memo1 1.257s 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.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() ``` @@ -156,24 +156,24 @@ memo.go的32行出現了兩次,説明有兩個goroutine在沒有同步榦預 ```go gopl.io/ch9/memo2 type Memo struct { - f Func - mu sync.Mutex // guards cache - cache map[string]result + 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 + 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 } ``` @@ -184,19 +184,19 @@ func (memo *Memo) Get(key string) (value interface{}, err error) { ```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) + 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 + // 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 } ``` @@ -207,41 +207,41 @@ func (memo *Memo) Get(key string) (value interface{}, err error) { ```go gopl.io/ch9/memo4 type entry struct { - res result - ready chan struct{} // closed when res is ready + res result + ready chan struct{} // closed when res is ready } func New(f Func) *Memo { - return &Memo{f: f, cache: make(map[string]*entry)} + return &Memo{f: f, cache: make(map[string]*entry)} } type Memo struct { - f Func - mu sync.Mutex // guards cache - cache map[string]*entry + 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() + 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) + 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() + 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 + <-e.ready // wait for ready condition + } + return e.res.value, e.res.err } ``` @@ -263,13 +263,13 @@ type Func func(key string) (interface{}, error) // A result is the result of calling a Func. type result struct { - value interface{} - err error + value interface{} + err error } type entry struct { - res result - ready chan struct{} // closed when res is ready + res result + ready chan struct{} // closed when res is ready } ``` @@ -279,23 +279,23 @@ type entry struct { 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 + 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 + 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 + response := make(chan result) + memo.requests <- request{key, response} + res := <-response + return res.value, res.err } func (memo *Memo) Close() { close(memo.requests) } @@ -307,31 +307,31 @@ cache變量被限製在了monitor goroutine (\*Memo).server中,下面會看到 ```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) - } + 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) + // 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 + // Wait for the ready condition. + <-e.ready + // Send the result to the client. + response <- e.res } ``` @@ -343,5 +343,5 @@ func (e *entry) deliver(response chan<- result) { 上面的兩種方案併不好説特定情境下哪種更好,不過了解他們還是有價值的。有時候從一種方式切換到另一種可以使你的代碼更爲簡潔。(譯註:不是説好的golang推崇通信併發麽) -練習 9.3: 擴展Func類型和(\*Memo).Get方法,支持調用方提供一個可選的done channel,使其具備通過該channel來取消整個操作的能力(§8.9)。一個被取消了的Func的調用結果不應該被緩存。 +**練習 9.3:** 擴展Func類型和(\*Memo).Get方法,支持調用方提供一個可選的done channel,使其具備通過該channel來取消整個操作的能力(§8.9)。一個被取消了的Func的調用結果不應該被緩存。 From 91f8a12710aef46986cc0b79313fccdb2a23fb20 Mon Sep 17 00:00:00 2001 From: chai2010 Date: Mon, 18 Jan 2016 17:53:46 +0800 Subject: [PATCH 09/12] =?UTF-8?q?=E7=B2=BE=E7=AE=80=E9=A6=96=E9=A1=B5?= =?UTF-8?q?=E9=93=BE=E6=8E=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 5 +---- preface.md | 5 +---- 2 files changed, 2 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index e310163..64a1eff 100644 --- a/README.md +++ b/README.md @@ -2,11 +2,8 @@ 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/archive/gh-pages.zip - 原版官網:http://gopl.io [![](cover_middle.jpg)](https://github.com/golang-china/gopl-zh) diff --git a/preface.md b/preface.md index 17893b0..09d3b7f 100644 --- a/preface.md +++ b/preface.md @@ -2,11 +2,8 @@ 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/archive/gh-pages.zip - 原版官網:http://gopl.io [![](cover_middle.jpg)](https://github.com/golang-china/gopl-zh) From 8555a5dceaceb7a2d272684de4b7bdcad79beb61 Mon Sep 17 00:00:00 2001 From: chai2010 Date: Mon, 18 Jan 2016 18:01:37 +0800 Subject: [PATCH 10/12] =?UTF-8?q?=E9=A6=96=E9=A1=B5=E4=BF=9D=E7=95=99?= =?UTF-8?q?=E9=A1=B9=E7=9B=AE=E4=B8=BB=E9=A1=B5=E9=93=BE=E6=8E=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 1 + preface.md | 1 + 2 files changed, 2 insertions(+) diff --git a/README.md b/README.md index 64a1eff..5cc3305 100644 --- a/README.md +++ b/README.md @@ -3,6 +3,7 @@ Go語言聖經 [《The Go Programming Language》](http://gopl.io) 中文版本,僅供學習交流之用。 - 在線預覽: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 diff --git a/preface.md b/preface.md index 09d3b7f..c2eca5e 100644 --- a/preface.md +++ b/preface.md @@ -3,6 +3,7 @@ Go語言聖經 [《The Go Programming Language》](http://gopl.io) 中文版本,僅供學習交流之用。 - 在線預覽: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 From c0173cc69eb03d4e5b8a661b280211973bfe43eb Mon Sep 17 00:00:00 2001 From: chai2010 Date: Mon, 18 Jan 2016 22:32:17 +0800 Subject: [PATCH 11/12] ch11: review --- ch11/ch11.md | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) 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代碼過程是類似的;它併不需要學習新的符號、規則和工具。 From 790b9d3ddecab468113a1b716d8508081a50c578 Mon Sep 17 00:00:00 2001 From: chai2010 Date: Mon, 18 Jan 2016 22:38:01 +0800 Subject: [PATCH 12/12] ch11-1: review --- ch11/ch11-01.md | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) 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包用於調用相應的測試函數,然後構建併運行、報告測試結果,最後清理測試中生成的臨時文件。