Merge branch 'master' of github.com:golang-china/gopl-zh

pull/1/head
Xargin 2016-01-18 16:14:23 +08:00
commit 7058cbd96a
91 changed files with 1001 additions and 317 deletions

View File

@ -171,3 +171,4 @@
* [附録A原文勘誤](appendix/appendix-a-errata.md)
* [附録B作者譯者](appendix/appendix-b-author.md)
* [附録C譯文授權](appendix/appendix-c-cpoyright.md)
* [附録D其它語言](appendix/appendix-d-translations.md)

View File

@ -126,3 +126,4 @@
* [附録A原文勘誤](appendix/appendix-a-errata.md)
* [附録B作者譯者](appendix/appendix-b-author.md)
* [附録C譯文授權](appendix/appendix-c-cpoyright.md)
* [附録D其它語言](appendix/appendix-d-translations.md)

View File

@ -6,15 +6,17 @@
`rand.Seed(time.Now().UTC().UnixNano())`
**p.15, ¶2:** For "inner loop", read "outer loop". (Thanks to Ralph Corderoy, 2015-11-28.)
**p.15, ¶2:** For "inner loop", read "outer loop". (Thanks to Ralph Corderoy, 2015-11-28. Corrected in the third printing.)
**p.19, ¶2:** For "Go's libraries makes", read "Go's library makes". (Thanks to Victor Farazdagi, 2015-11-30.)
**p.19, ¶2:** For "Go's libraries makes", read "Go's library makes". (Thanks to Victor Farazdagi, 2015-11-30. Corrected in the third printing.)
**p.40, ¶4:** For "value of the underlying type", read "value of an unnamed type with the same underlying type". (Thanks to Carlos Romero Brox, 2015-12-19.)
**p.40, ¶1:** The paragraph should end with a period, not a comma. (Thanks to Victor Farazdagi, 2015-11-30.)
**p.40, ¶1:** The paragraph should end with a period, not a comma. (Thanks to Victor Farazdagi, 2015-11-30. Corrected in the third printing.)
**p.43, ¶3:** Import declarations are explained in §10.4, not §10.3. (Thanks to Peter Jurgensen, 2015-11-21.)
**p.43, ¶3:** Import declarations are explained in §10.4, not §10.3. (Thanks to Peter Jurgensen, 2015-11-21. Corrected in the third printing.)
**p.48:** `f.ReadByte()` serves as an example of a reference to f, but `*os.File` has no such method. For "ReadByte", read "Stat", four times. (Thanks to Peter Olsen, 2016-01-06. Corrected in the third printing.)
**p.52, ¶2:** for "an synonym", read "a synonym", twice. (Corrected in the second printing.)
@ -38,19 +40,19 @@
**p.76:** the comment `// "time.Duration 5ms0s` should have a closing double-quotation mark. (Corrected in the second printing.)
**p.79, ¶4:** "When an untyped constant is assigned to a variable, as in the first statement below, or
appears on the right-hand side of a variable declaration with an explicit type, as in the other three statements, ..." has it backwards: the <i>first</i> statement is a declaration; the other three are assignments. (Thanks to Yoshiki Shibata, 2015-11-09.)
appears on the right-hand side of a variable declaration with an explicit type, as in the other three statements, ..." has it backwards: the <i>first</i> statement is a declaration; the other three are assignments. (Thanks to Yoshiki Shibata, 2015-11-09. Corrected in the third printing.)
**p.132, code display following ¶3:** the final comment should read: `// compile error: can't assign func(int, int) int to func(int) int` (Thanks to Toni Suter, 2015-11-21.)
**p.132, code display following ¶3:** the final comment should read: `// compile error: can't assign func(int, int) int to func(int) int` (Thanks to Toni Suter, 2015-11-21. Corrected in the third printing.)
**p.166, ¶2:** for "way", read "a way".
**p.166, ¶2:** for "way", read "a way". (Corrected in the third printing.)
**p.200, TestEval function:** the format string in the final call to t.Errorf should format test.env with %v, not %s. (Thanks to Mitsuteru Sawa, 2015-12-07. Corrected in the third printing.)
**p.222. Exercise 8.1:** The port numbers for `London` and `Tokyo` should be swapped in the final command to match the earlier commands. (Thanks to Kiyoshi Kamishima, 2016-01-08.)
**p.288, code display following ¶4:** In the import declaration, for `"database/mysql"`, read `"database/sql"`. (Thanks to Jose Colon Rodriguez, 2016-01-09.)
**p.200, TestEval function:** the format string in the final call to t.Errorf should format test.env with %v, not %s. (Thanks to Mitsuteru Sawa, 2015-12-07.)
**p.347, Exercise 12.8:** for "like json.Marshal", read "like json.Unmarshal". (Thanks to @chai2010, 2016-01-01.)
**p.362:** the `gopl.io/ch13/bzip` program does not comply with the [proposed rules for passing pointers between Go and C code](https://github.com/golang/proposal/blob/master/design/12416-cgo-pointers.md) because the C function `bz2compress` temporarily stores a Go pointer (in) into the Go heap (the `bz_stream` variable). The `bz_stream` variable should be allocated, and explicitly freed after the call to `BZ2_bzCompressEnd`, by C functions. (Thanks to Joe Tsai, 2015-11-18.)
**p.362:** the `gopl.io/ch13/bzip` program does not comply with the [proposed rules for passing pointers between Go and C code](https://github.com/golang/proposal/blob/master/design/12416-cgo-pointers.md) because the C function `bz2compress` temporarily stores a Go pointer (in) into the Go heap (the `bz_stream` variable). The `bz_stream` variable should be allocated, and explicitly freed after the call to `BZ2_bzCompressEnd`, by C functions. (Thanks to Joe Tsai, 2015-11-18. Corrected in the third printing.)

View File

@ -12,6 +12,6 @@
中文譯者 | 章節
-------------------------------------- | -------------------------
`chai2010 <chaishushan@gmail.com>` | 前言/第2~4章/第10~13章
`Xargin <cao1988228@163.com>` | 第1章/第6章/第8~9章
`CrazySssst` | 第5章
`foreversmart <njutree@gmail.com>` | 第7章
`Xargin <cao1988228@163.com>` | 第1章/第6章/第8~9章

View File

@ -0,0 +1,22 @@
## 附録D其它語言
下表是 [The Go Programming Language](http://www.gopl.io/) 其它語言版本:
語言 | 鏈接 | 時間 | 譯者 | ISBN
---- | ---- | ---- | ---- | ----
中文 | [《Go語言聖經》][gopl-zh] | 2016/2/1 | [chai2010][chai2010], [Xargin][Xargin], [CrazySssst][CrazySssst], [foreversmart][foreversmart] | ?
韓語 | [Acorn Publishing (Korea)](http://www.acornpub.co.kr/) | 2016 | ? | ?
俄語 | [Williams Publishing (Russia)](http://www.williamspublishing.com/) | 2016 | ? | ?
波蘭語 | [Helion (Poland)](http://helion.pl/) | 2016 | ? | ?
日語 | [Maruzen Publishing (Japan)](http://www.maruzen.co.jp/corp/en/services/publishing.html) | 2017 | Yoshiki Shibata | ?
葡萄牙語 | [Novatec Editora (Brazil)](http://novatec.com.br/) |2017 | ? | ?
中文簡體 | [Pearson Education Asia](http://www.pearsonapac.com/) |2017 | ? | ?
中文繁體 | [Gotop Information (Taiwan)](http://www.gotop.com.tw/) | 2017 | ? | ?
[gopl-zh]: http://golang-china.github.io/gopl-zh/ "《Go語言聖經》"
[chai2010]: https://github.com/chai2010
[Xargin]: https://github.com/cch123
[CrazySssst]: https://github.com/CrazySssst
[foreversmart]: https://github.com/foreversmart

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

@ -56,15 +56,15 @@ Go的標準庫已經提供了100多個package用來完成一門程序語言
package main是一個比較特殊的package。這個package里會定義一個獨立的程序這個程序是可以運行的而不是像其它package一樣對應一個library。在main這個package里main函數也是一個特殊的函數這是我們整個程序的入口譯註其實C繫語言差不多都是這樣。main函數所做的事情就是我們程序做的事情。當然了main函數一般是通過是調用其它packge里的函數來完成自己的工作比如fmt.Println。
我們必告訴編譯器如何要正確地執行這個源文件需要用到哪些package這就是import在這個文件里扮演的角色。上述的hello world例子隻用到了一個其它的package就是fmt。一般情況下需要import的package可能不隻一個。
我們必告訴編譯器如何要正確地執行這個源文件需要用到哪些package這就是import在這個文件里扮演的角色。上述的hello world例子隻用到了一個其它的package就是fmt。一般情況下需要import的package可能不隻一個。
這也正是因爲go語言必引入所有要用到的package的原則假如你沒有在代碼里import需要用到的package程序將無法編譯通過同時當你import了沒有用到的package也會無法編譯通過譯註Go語言編譯過程沒有警告信息爭議特性之一
這也正是因爲go語言必引入所有要用到的package的原則假如你沒有在代碼里import需要用到的package程序將無法編譯通過同時當你import了沒有用到的package也會無法編譯通過譯註Go語言編譯過程沒有警告信息爭議特性之一
import聲明必跟在文件的package聲明之後。在import語句之後則是各種方法、變量、常量、類型的聲明語句(分别用關鍵字func, var, const, type來進行定義)。這些內容的聲明順序併沒有什麽規定,可以隨便調整順序(譯註:最好還是定一下規范)。我們例子里的程序比較簡單隻包含了一個函數。併且在該函數里也隻調用了一個其它函數。爲了節省空間有些時候的例子我們會省略package和import聲明但是讀者需要註意這些聲明是一定要包含在源文件里的。
import聲明必跟在文件的package聲明之後。在import語句之後則是各種方法、變量、常量、類型的聲明語句(分别用關鍵字func, var, const, type來進行定義)。這些內容的聲明順序併沒有什麽規定,可以隨便調整順序(譯註:最好還是定一下規范)。我們例子里的程序比較簡單隻包含了一個函數。併且在該函數里也隻調用了一個其它函數。爲了節省空間有些時候的例子我們會省略package和import聲明但是讀者需要註意這些聲明是一定要包含在源文件里的。
一個函數的聲明包含func這個關鍵字、函數名、參數列表、返迴結果列表我們例子里的main函數參數列表和返迴值都是空的以及包含在大括號里的函數體。關於函數的更詳細描述在第五章。
Go語言是一門不需要分號作爲語句或者聲明結束的語言除非要在一行中將多個語句、聲明隔開。然而在編譯時編譯器會主動在一些特定的符號譯註比如行末是一個標識符、一個整數、浮點數、虛數、字符或字符串文字、關鍵字break、continue、fallthrough或return中的一個、運算符和分隔符++、--、)、]或}中的一個) 後添加分號所以在哪里加分號合適是取決於Go語言代碼的。例如在Go語言中的函數聲明和 { 大括號必在同一行而在x + y這樣的表達式中在+號後換行可以,但是在+號前換行則會有問題(譯註:以+結尾的話不會被插入分號分隔符但是以x結尾的話則會被分號分隔符從而導致編譯錯誤
Go語言是一門不需要分號作爲語句或者聲明結束的語言除非要在一行中將多個語句、聲明隔開。然而在編譯時編譯器會主動在一些特定的符號譯註比如行末是一個標識符、一個整數、浮點數、虛數、字符或字符串文字、關鍵字break、continue、fallthrough或return中的一個、運算符和分隔符++、--、)、]或}中的一個) 後添加分號所以在哪里加分號合適是取決於Go語言代碼的。例如在Go語言中的函數聲明和 { 大括號必在同一行而在x + y這樣的表達式中在+號後換行可以,但是在+號前換行則會有問題(譯註:以+結尾的話不會被插入分號分隔符但是以x結尾的話則會被分號分隔符從而導致編譯錯誤
Go語言在代碼格式上采取了很強硬的態度。gofmt工具會將你的代碼格式化爲標準格式譯註這個格式化工具沒有任何可以調整代碼格式的參數Go語言就是這麽任性併且go工具中的fmt子命令會自動對特定package下的所有.go源文件應用gofmt工具格式化。如果不指定package則默認對當前目録下的源文件進行格式化。本書中的所有代碼已經是執行過gofmt後的標準格式代碼。你應該在自己的代碼上也執行這種格式化。規定一種標準的代碼格式可以規避掉無盡的無意義的撕逼譯註也導致了Go語言的TIOBE排名較低因爲缺少撕逼的話題。當然了這可以避免由於代碼格式導致的邏輯上的歧義。

View File

@ -72,7 +72,7 @@ for initialization; condition; post {
這里需要註意for循環的兩邊是不需要像其它語言一樣寫括號的。併且左大括號需要和for語句在同一行。
initialization部分是可選的如果你寫了這部分的話在for循環之前這部分的邏輯會被執行。需要註意的是這部分必是一個簡單的語句也就是説是一個簡短的變量聲明一個賦值語句或是一個函數調用。condition部分必是一個結果爲boolean值的表達式在每次循環之前語言都會檢査當前是否滿足這個條件如果不滿足的話便會結束循環post部分的語句則是在每次循環迭代結束之後被執行之後conditon部分會在下一次執行前再被執行依此往複。當condition條件里的判斷結果變爲false之後循環卽結束。
initialization部分是可選的如果你寫了這部分的話在for循環之前這部分的邏輯會被執行。需要註意的是這部分必是一個簡單的語句也就是説是一個簡短的變量聲明一個賦值語句或是一個函數調用。condition部分必是一個結果爲boolean值的表達式在每次循環之前語言都會檢査當前是否滿足這個條件如果不滿足的話便會結束循環post部分的語句則是在每次循環迭代結束之後被執行之後conditon部分會在下一次執行前再被執行依此往複。當condition條件里的判斷結果變爲false之後循環卽結束。
上面提到是for循環里的三個部分都是可以被省略的如果你把initialization和post部分都省略的話那麽連中間隔離他們的分號也是可以被省略的比如下面這種for循環就和傳統的while循環是一樣的
@ -115,7 +115,7 @@ func main() {
}
```
每一次循環迭代range都會返迴一對結果當前迭代的下標以及在該下標處的元素的值。在這個例子里我們不需要這個下標但是因爲range的處理要求我們必要同時處理下標和值。我們可以在這里聲明一個接收index的臨時變量來解決這個問題但是Go語言又不允許隻聲明而在後續代碼里不使用這個變量如果你這樣做了編譯器會返迴一個編譯錯誤。
每一次循環迭代range都會返迴一對結果當前迭代的下標以及在該下標處的元素的值。在這個例子里我們不需要這個下標但是因爲range的處理要求我們必要同時處理下標和值。我們可以在這里聲明一個接收index的臨時變量來解決這個問題但是Go語言又不允許隻聲明而在後續代碼里不使用這個變量如果你這樣做了編譯器會返迴一個編譯錯誤。
在Go語言中應對這種情況的解決方法是用空白標識符就是上面那個下劃線_。空白標識符可以在任何你接收自己不需要處理的值時使用。在這里我們用它來忽略掉range返迴的那個沒用的下標值。大多數的Go程序員都會像上面這樣來寫類似的os.Args遍歷由於遍歷os.Args的下標索引是隱式自動生成的可以避免因顯式更新索引導致的錯誤。
@ -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

@ -69,7 +69,7 @@ bla kIndex)
當我們import了一個包路徑包含有多個單詞的package時比如image/colorimage和color兩個單詞通常我們隻需要用最後那個單詞表示這個包就可以。所以當我們寫color.White時這個變量指向的是image/color包里的變量同理gif.GIF是屬於image/gif包里的變量。
這個程序里的常量聲明給出了一繫列的常量值,常量是指在程序編譯後運行時始終都不會變化的值,比如圈數、幀數、延遲值。常量聲明和變量聲明一般都會出現在包級别,所以這些常量在整個包中都是可以共享的,或者你也可以把常量聲明定義在函數體內部,那麽這種常量就隻能在函數體內用。目前常量聲明的值必是一個數字值、字符串或者一個固定的boolean值。
這個程序里的常量聲明給出了一繫列的常量值,常量是指在程序編譯後運行時始終都不會變化的值,比如圈數、幀數、延遲值。常量聲明和變量聲明一般都會出現在包級别,所以這些常量在整個包中都是可以共享的,或者你也可以把常量聲明定義在函數體內部,那麽這種常量就隻能在函數體內用。目前常量聲明的值必是一個數字值、字符串或者一個固定的boolean值。
[]color.Color{...}和gif.GIF{...}這兩個表達式就是我們説的複合聲明4.2和4.4.1節有説明。這是實例化Go語言里的複合類型的一種寫法。這里的前者生成的是一個slice切片後者生成的是一個struct結構體。

View File

@ -85,7 +85,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的所有行爲包在中間的目的。第九章中我們會進一步講解共享變量。
這個服務器有兩個請求處理函數請求的url會決定具體調用哪一個對/count這個url的請求會調用到count這個函數其它所有的url都會調用默認的處理函數。如果你的請求pattern是以/結尾那麽所有以該url爲前綴的url都會被這條規則匹配。在這些代碼的背後服務器每一次接收請求處理時都會另起一個goroutine這樣服務器就可以同一時間處理多數請求。然而在併發情況下假如眞的有兩個請求同一時刻去更新count那麽這個值可能併不會被正確地增加這個程序可能會被引發一個嚴重的bug競態條件參見9.1)。爲了避免這個問題,我們必保證每次脩改變量的最多隻能有一個goroutine這也就是代碼里的mu.Lock()和mu.Unlock()調用將脩改count的所有行爲包在中間的目的。第九章中我們會進一步講解共享變量。
下面是一個更爲豐富的例子handler函數會把請求的http頭和請求的form數據都打印出來這樣可以讓檢査和調試這個服務更爲方便

View File

@ -6,4 +6,4 @@
每個包還通過控製包內名字的可見性和是否導出來實現封裝特性。通過限製包成員的可見性併隱藏包API的具體實現將允許包的維護者在不影響外部包用戶的前提下調整包的內部實現。通過限製包內變量的可見性還可以強製用戶通過某些特定函數來訪問和更新內部變量這樣可以保證內部變量的一致性和併發時的互斥約束。
當我們脩改了一個源文件,我們必重新編譯該源文件對應的包和所有依賴該包的其他包。卽使是從頭構建Go語言編譯器的編譯速度也明顯快於其它編譯語言。Go語言的閃電般的編譯速度主要得益於三個語言特性。第一點所有導入的包必在每個文件的開頭顯式聲明,這樣的話編譯器就沒有必要讀取和分析整個源文件來判斷包的依賴關繫。第二點,禁止包的環狀依賴,因爲沒有循環依賴,包的依賴關繫形成一個有向無環圖,每個包可以被獨立編譯,而且很可能是被併發編譯。第三點,編譯後包的目標文件不僅僅記録包本身的導出信息,目標文件同時還記録了包的依賴關繫。因此,在編譯一個包的時候,編譯器隻需要讀取每個直接導入包的目標文件,而不需要遍歷所有依賴的的文件(譯註:很多都是重複的間接依賴)。
當我們脩改了一個源文件,我們必重新編譯該源文件對應的包和所有依賴該包的其他包。卽使是從頭構建Go語言編譯器的編譯速度也明顯快於其它編譯語言。Go語言的閃電般的編譯速度主要得益於三個語言特性。第一點所有導入的包必在每個文件的開頭顯式聲明,這樣的話編譯器就沒有必要讀取和分析整個源文件來判斷包的依賴關繫。第二點,禁止包的環狀依賴,因爲沒有循環依賴,包的依賴關繫形成一個有向無環圖,每個包可以被獨立編譯,而且很可能是被併發編譯。第三點,編譯後包的目標文件不僅僅記録包本身的導出信息,目標文件同時還記録了包的依賴關繫。因此,在編譯一個包的時候,編譯器隻需要讀取每個直接導入包的目標文件,而不需要遍歷所有依賴的的文件(譯註:很多都是重複的間接依賴)。

View File

@ -1,6 +1,6 @@
## 10.3. 包聲明
在每個Go語音源文件的開頭都必有包聲明語句。包聲明語句的主要目的是確定當前包被其它包導入時默認的標識符(也稱爲包名)。
在每個Go語音源文件的開頭都必有包聲明語句。包聲明語句的主要目的是確定當前包被其它包導入時默認的標識符(也稱爲包名)。
例如math/rand包的每個源文件的開頭都包含`package rand`包聲明語句所以當你導入這個包你就可以用rand.Int、rand.Float64類似的方式訪問包的成員。
@ -19,8 +19,8 @@ func main() {
通常來説默認的包名就是包導入路徑名的最後一段因此卽使兩個包的導入路徑不同它們依然可能有一個相同的包名。例如math/rand包和crypto/rand包的包名都是rand。稍後我們將看到如何同時導入兩個有相同包名的包。
關於默認包名一般采用導入路徑名的最後一段的約定也有三種例外情況。第一個例外包對應一個可執行程序也就是main包這時候main包本身的導入路徑是無關緊要的。名字爲main的包是給go build§10.7.3)構建命令一個信息,這個包編譯完之後必調用連接器生成一個可執行程序。
關於默認包名一般采用導入路徑名的最後一段的約定也有三種例外情況。第一個例外包對應一個可執行程序也就是main包這時候main包本身的導入路徑是無關緊要的。名字爲main的包是給go build§10.7.3)構建命令一個信息,這個包編譯完之後必調用連接器生成一個可執行程序。
第二個例外包所在的目録中可能有一些文件名是以_test.go爲後綴的Go源文件譯註前面必有其它的字符,因爲以`_`前綴的源文件是被忽略的併且這些源文件聲明的包名也是以_test爲後綴名的。這種目録可以包含兩種包一種普通包加一種則是測試的外部擴展包。所有以_test爲後綴包名的測試外部擴展包都由go test命令獨立編譯普通包和測試的外部擴展包是相互獨立的。測試的外部擴展包一般用來避免測試代碼中的循環導入依賴具體細節我們將在11.2.4節中介紹。
第二個例外包所在的目録中可能有一些文件名是以_test.go爲後綴的Go源文件譯註前面必有其它的字符,因爲以`_`前綴的源文件是被忽略的併且這些源文件聲明的包名也是以_test爲後綴名的。這種目録可以包含兩種包一種普通包加一種則是測試的外部擴展包。所有以_test爲後綴包名的測試外部擴展包都由go test命令獨立編譯普通包和測試的外部擴展包是相互獨立的。測試的外部擴展包一般用來避免測試代碼中的循環導入依賴具體細節我們將在11.2.4節中介紹。
第三個例外,一些依賴版本號的管理工具會在導入路徑後追加版本號信息,例如"gopkg.in/yaml.v2"。這種情況下包的名字併不包含版本號後綴而是yaml。

View File

@ -25,7 +25,7 @@ import (
)
```
如果我們想同時導入兩個有着名字相同的包例如math/rand包和crypto/rand包那麽導入聲明必至少爲一個同名包指定一個新的包名以避免衝突。這叫做導入包的重命名。
如果我們想同時導入兩個有着名字相同的包例如math/rand包和crypto/rand包那麽導入聲明必至少爲一個同名包指定一個新的包名以避免衝突。這叫做導入包的重命名。
```Go
import (

View File

@ -4,7 +4,7 @@
因爲每個目録隻包含一個包因此每個對應可執行程序或者叫Unix術語中的命令的包會要求放到一個獨立的目録中。這些目録有時候會放在名叫cmd目録的子目録下面例如用於提供Go文檔服務的golang.org/x/tools/cmd/godoc命令就是放在cmd子目録§10.7.4)。
每個包可以由它們的導入路徑指定,就像前面看到的那樣,或者用一個相對目録的路徑知指定,相對路徑必以`.`或`..`開頭。如果沒有指定參數,那麽默認指定爲當前目録對應的包。 下面的命令用於構建同一個包, 雖然它們的寫法各不相同:
每個包可以由它們的導入路徑指定,就像前面看到的那樣,或者用一個相對目録的路徑知指定,相對路徑必以`.`或`..`開頭。如果沒有指定參數,那麽默認指定爲當前目録對應的包。 下面的命令用於構建同一個包, 雖然它們的寫法各不相同:
```
$ cd $GOPATH/src/gopl.io/ch1/helloworld

View File

@ -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的情況.

View File

@ -110,7 +110,7 @@ func TestCheckQuotaNotifiesUser(t *testing.T) {
}
```
這里有一個問題: 當測試函數返迴後, CheckQuota 將不能正常工作, 因爲 notifyUsers 依然使用的是測試函數的僞發送郵件函數. (當更新全局對象的時候總會有這種風險.) 我們必脩改測試代碼恢複 notifyUsers 原先的狀態以便後續其他的測試沒有影響, 要確保所有的執行路徑後都能恢複, 包括測試失敗或 panic 情形. 在這種情況下, 我們建議使用 defer 處理恢複的代碼.
這里有一個問題: 當測試函數返迴後, CheckQuota 將不能正常工作, 因爲 notifyUsers 依然使用的是測試函數的僞發送郵件函數. (當更新全局對象的時候總會有這種風險.) 我們必脩改測試代碼恢複 notifyUsers 原先的狀態以便後續其他的測試沒有影響, 要確保所有的執行路徑後都能恢複, 包括測試失敗或 panic 情形. 在這種情況下, 我們建議使用 defer 處理恢複的代碼.
```Go
func TestCheckQuotaNotifiesUser(t *testing.T) {

View File

@ -38,7 +38,7 @@ $ go list -f={{.TestGoFiles}} fmt
包的測試代碼通常都在這些文件中, 不過 fmt 包併非如此; 稍後我們再解釋 export_test.go 文件的作用.
XTestGoFiles 表示的是屬於測試擴展包的測試代碼, 也就是 fmt_test 包, 因此它們必先導入 fmt 包. 同樣, 這些文件也隻是在測試時被構建運行:
XTestGoFiles 表示的是屬於測試擴展包的測試代碼, 也就是 fmt_test 包, 因此它們必先導入 fmt 包. 同樣, 這些文件也隻是在測試時被構建運行:
{% raw %}

View File

@ -1,7 +1,7 @@
## 11.2. 測試函數
每個測試函數必導入 testing 包. 測試函數有如下的籤名:
每個測試函數必導入 testing 包. 測試函數有如下的籤名:
```Go
func TestName(t *testing.T) {
@ -9,7 +9,7 @@ func TestName(t *testing.T) {
}
```
測試函數的名字必鬚以Test開頭, 可選的後綴名必鬚以大寫字母開頭:
測試函數的名字必須以Test開頭, 可選的後綴名必須以大寫字母開頭:
```Go
func TestSin(t *testing.T) { /* ... */ }
@ -209,7 +209,7 @@ ok gopl.io/ch11/word2 0.015s
失敗的測試的輸出併不包括調用 t.Errorf 時刻的堆棧調用信息. 不像其他語言或測試框架的 assert 斷言, t.Errorf 調用也沒有引起 panic 或停止測試的執行. 卽使表格中前面的數據導致了測試的失敗, 表格後面的測試數據依然會運行測試, 因此在一個測試中我們可能了解多個失敗的信息.
如果我們眞的需要停止測試, 或許是因爲初始化失敗或可能是早先的錯誤導致了後續錯誤等原因, 我們可以使用 t.Fatal 或 t.Fatalf 停止測試. 它們必在和測試函數同一個 goroutine 內調用.
如果我們眞的需要停止測試, 或許是因爲初始化失敗或可能是早先的錯誤導致了後續錯誤等原因, 我們可以使用 t.Fatal 或 t.Fatalf 停止測試. 它們必在和測試函數同一個 goroutine 內調用.
測試失敗的信息一般的形式是 "f(x) = y, want z", f(x) 解釋了失敗的操作和對應的輸出, y 是實際的運行結果, z 是期望的正確的結果. 就像前面檢査迴文字符串的例子, 實際的函數用於 f(x) 部分. 如果顯示 x 是表格驅動型測試中比較重要的部分, 因爲同一個斷言可能對應不同的表格項執行多次. 要避免無用和冗餘的信息. 在測試類似 IsPalindrome 返迴布爾類型的函數時, 可以忽略併沒有額外信息的 z 部分. 如果 x, y 或 z 是 y 的長度, 輸出一個相關部分的簡明總結卽可. 測試的作者應該要努力幫助程序員診斷失敗的測試.

View File

@ -12,7 +12,7 @@ func ExampleIsPalindrome() {
}
```
示例函數有三個用處. 最主要的一個是用於文檔: 一個包的例子可以更簡潔直觀的方式來演示函數的用法, 會文字描述會更直接易懂, 特别是作爲一個提醒或快速參考時. 一個例子函數也可以方便展示屬於同一個接口的幾種類型或函數直接的關繫, 所有的文檔都必關聯到一個地方, 就像一個類型或函數聲明都統一到包一樣. 同時, 示例函數和註釋併不一樣, 示例函數是完整眞是的Go代碼, 需要介紹編譯器的編譯時檢査, 這樣可以保證示例代碼不會腐爛成不能使用的舊代碼.
示例函數有三個用處. 最主要的一個是用於文檔: 一個包的例子可以更簡潔直觀的方式來演示函數的用法, 會文字描述會更直接易懂, 特别是作爲一個提醒或快速參考時. 一個例子函數也可以方便展示屬於同一個接口的幾種類型或函數直接的關繫, 所有的文檔都必關聯到一個地方, 就像一個類型或函數聲明都統一到包一樣. 同時, 示例函數和註釋併不一樣, 示例函數是完整眞是的Go代碼, 需要介紹編譯器的編譯時檢査, 這樣可以保證示例代碼不會腐爛成不能使用的舊代碼.
根據示例函數的後綴名部分, godoc 的web文檔會將一個示例函數關聯到某個具體函數或包本身, 因此 ExampleIsPalindrome 示例函數將是 IsPalindrome 函數文檔的一部分, Example 示例函數將是包文檔的一部分.

View File

@ -10,7 +10,7 @@ Maurice Wilkes, 第一個存儲程序計算機 EDSAC 的設計者, 1949年在他
Go語言的測試技術是相對低級的. 它依賴一個 'go test' 測試命令, 和一組按照約定方式編寫的測試函數, 測試命令可以運行測試函數. 編寫相對輕量級的純測試代碼是有效的, 而且它很容易延伸到基準測試和示例文檔.
在實踐中, 編寫測試代碼和編寫程序本身併沒有多大區别. 我們編寫的每一個函數也是針對每個具體的任務. 我們必小心處理邊界條件, 思考合適的數據結構, 推斷合適的輸入應該産生什麽樣的結果輸出. 編程測試代碼和編寫普通的Go代碼過程是類似的; 它併不需要學習新的符號, 規則和工具.
在實踐中, 編寫測試代碼和編寫程序本身併沒有多大區别. 我們編寫的每一個函數也是針對每個具體的任務. 我們必小心處理邊界條件, 思考合適的數據結構, 推斷合適的輸入應該産生什麽樣的結果輸出. 編程測試代碼和編寫普通的Go代碼過程是類似的; 它併不需要學習新的符號, 規則和工具.

View File

@ -79,7 +79,7 @@ func display(path string, v reflect.Value) {
雖然reflect.Value類型帶有很多方法但是隻有少數的方法對任意值都是可以安全調用的。例如Index方法隻能對Slice、數組或字符串類型的值調用其它類型如果調用將導致panic異常。
**結構體:** NumField方法報告結構體中成員的數量Field(i)以reflect.Value類型返迴第i個成員的值。成員列表包含了匿名成員在內的全部成員。通過在path添加“.f”來表示成員路徑我們必獲得結構體對應的reflect.Type類型信息包含結構體類型和第i個成員的名字。
**結構體:** NumField方法報告結構體中成員的數量Field(i)以reflect.Value類型返迴第i個成員的值。成員列表包含了匿名成員在內的全部成員。通過在path添加“.f”來表示成員路徑我們必獲得結構體對應的reflect.Type類型信息包含結構體類型和第i個成員的名字。
**Maps:** MapKeys方法返迴一個reflect.Value類型的slice每一個都對應map的可以。和往常一樣遍歷map時順序是隨機的。MapIndex(key)返迴map中key對應的value。我們向path添加“[key]”來表示訪問路徑。我們這里有一個未完成的工作。其實map的key的類型併不局限於formatAtom能完美處理的類型數組、結構體和接口都可以作爲map的key。針對這種類型完善key的顯示信息是練習12.1的任務。)

View File

@ -10,7 +10,7 @@ err := json.Unmarshal(data, &movie)
Unmarshal函數使用了反射機製類脩改movie變量的每個成員根據輸入的內容爲Movie成員創建對應的map、結構體和slice。
現在讓我們爲S表達式編碼實現一個簡易的Unmarshal類似於前面的json.Unmarshal標準庫函數對應我們之前實現的sexpr.Marshal函數的逆操作。我們必提醒一下一個健壯的和通用的實現通常需要比例子更多的代碼爲了便於演示我們采用了精簡的實現。我們隻支持S表達式有限的子集同時處理錯誤的方式也比較粗暴代碼的目的是爲了演示反射的用法而不是構造一個實用的S表達式的解碼器。
現在讓我們爲S表達式編碼實現一個簡易的Unmarshal類似於前面的json.Unmarshal標準庫函數對應我們之前實現的sexpr.Marshal函數的逆操作。我們必提醒一下一個健壯的和通用的實現通常需要比例子更多的代碼爲了便於演示我們采用了精簡的實現。我們隻支持S表達式有限的子集同時處理錯誤的方式也比較粗暴代碼的目的是爲了演示反射的用法而不是構造一個實用的S表達式的解碼器。
詞法分析器lexer使用了標準庫中的text/scanner包將輸入流的字節數據解析爲一個個類似註釋、標識符、字符串面值和數字面值之類的標記。輸入掃描器scanner的Scan方法將提前掃描和返迴下一個記號對於rune類型。大多數記號比如“(”對應一個單一rune可表示的Unicode字符但是text/scanner也可以用小的負數表示記號標識符、字符串等由多個字符組成的記號。調用Scan方法將返迴這些記號的類型接着調用TokenText方法將返迴記號對應的文本內容。
@ -74,7 +74,7 @@ func read(lex *lexer, v reflect.Value) {
最有趣的部分是遞歸。最簡單的是對數組類型的處理。直到遇到“)”結束標記我們使用Index函數來獲取數組每個元素的地址然後遞歸調用read函數處理。和其它錯誤類似如果輸入數據導致解碼器的引用超出了數組的范圍解碼器將拋出panic異常。slice也采用類似方法解析不同的是我們將爲每個元素創建新的變量然後將元素添加到slice的末尾。
在循環處理結構體和map每個元素時必解碼一個(key value)格式的對應子列表。對於結構體key部分對於成員的名字。和數組類似我們使用FieldByName找到結構體對應成員的變量然後遞歸調用read函數處理。對於mapkey可能是任意類型對元素的處理方式和slice類似我們創建一個新的變量然後遞歸填充它最後將新解析到的key/value對添加到map。
在循環處理結構體和map每個元素時必解碼一個(key value)格式的對應子列表。對於結構體key部分對於成員的名字。和數組類似我們使用FieldByName找到結構體對應成員的變量然後遞歸調用read函數處理。對於mapkey可能是任意類型對元素的處理方式和slice類似我們創建一個新的變量然後遞歸填充它最後將新解析到的key/value對添加到map。
```Go
func readList(lex *lexer, v reflect.Value) {

View File

@ -40,7 +40,7 @@ struct{ bool; int16; float64 } // 2 words 3words
`unsafe.Alignof` 函數返迴對應參數的類型需要對齊的倍數. 和 Sizeof 類似, Alignof 也是返迴一個常量表達式, 對應一個常量. 通常情況下布爾和數字類型需要對齊到它們本身的大小(最多8個字節), 其它的類型對齊到機器字大小.
`unsafe.Offsetof` 函數的參數必是一個字段 `x.f`, 然後返迴 `f` 字段相對於 `x` 起始地址的偏移量, 包括可能的空洞.
`unsafe.Offsetof` 函數的參數必是一個字段 `x.f`, 然後返迴 `f` 字段相對於 `x` 起始地址的偏移量, 包括可能的空洞.
圖 13.1 顯示了一個結構體變量 x 以及其在32位和64位機器上的典型的內存. 灰色區域是空洞.

View File

@ -43,7 +43,7 @@ pb := (*int16)(unsafe.Pointer(tmp))
*pb = 42
```
産生錯誤的原因很微妙。有時候垃圾迴收器會移動一些變量以降低內存碎片等問題。這類垃圾迴收器被稱爲移動GC。當一個變量被移動所有的保存改變量舊地址的指針必同時被更新爲變量移動後的新地址。從垃圾收集器的視角來看一個unsafe.Pointer是一個指向變量的指針因此當變量被移動是對應的指針也必被更新但是uintptr類型的臨時變量隻是一個普通的數字所以其值不應該被改變。上面錯誤的代碼因爲引入一個非指針的臨時變量tmp導致垃圾收集器無法正確識别這個是一個指向變量x的指針。當第二個語句執行時變量x可能已經被轉移這時候臨時變量tmp也就不再是現在的`&x.b`地址。第三個向之前無效地址空間的賦值語句將徹底摧譭整個程序!
産生錯誤的原因很微妙。有時候垃圾迴收器會移動一些變量以降低內存碎片等問題。這類垃圾迴收器被稱爲移動GC。當一個變量被移動所有的保存改變量舊地址的指針必同時被更新爲變量移動後的新地址。從垃圾收集器的視角來看一個unsafe.Pointer是一個指向變量的指針因此當變量被移動是對應的指針也必被更新但是uintptr類型的臨時變量隻是一個普通的數字所以其值不應該被改變。上面錯誤的代碼因爲引入一個非指針的臨時變量tmp導致垃圾收集器無法正確識别這個是一個指向變量x的指針。當第二個語句執行時變量x可能已經被轉移這時候臨時變量tmp也就不再是現在的`&x.b`地址。第三個向之前無效地址空間的賦值語句將徹底摧譭整個程序!
還有很多類似原因導致的錯誤。例如這條語句:

View File

@ -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

View File

@ -1,6 +1,6 @@
## 2.1. 命名
Go語言中的函數名、變量名、常量名、類型名、語句標號和包名等所有的命名都遵循一個簡單的命名規則一個名字必以一個字母Unicode字母或下劃線開頭後面可以跟任意數量的字母、數字或下劃線。大寫字母和小寫字母是不同的heapSort和Heapsort是兩個不同的名字。
Go語言中的函數名、變量名、常量名、類型名、語句標號和包名等所有的命名都遵循一個簡單的命名規則一個名字必以一個字母Unicode字母或下劃線開頭後面可以跟任意數量的字母、數字或下劃線。大寫字母和小寫字母是不同的heapSort和Heapsort是兩個不同的名字。
Go語言中類似if和switch的關鍵字有25個關鍵字不能用於自定義名字隻能在特定語法結構中使用。
@ -29,7 +29,7 @@ continue for import return var
這些內部預先定義的名字併不是關鍵字,你可以再定義中重新使用它們。在一些特殊的場景中重新定義它們也是有意義的,但是也要註意避免過度而引起語義混亂。
如果一個名字是在函數內部定義,那麽它的就隻在函數內部有效。如果是在函數外部定義,那麽將在當前包的所有文件中都可以訪問。名字的開頭字母的大小寫決定了名字在包外的可見性。如果一個名字是大寫字母開頭的(譯註:必是在函數外部定義的包級名字包級函數名本身也是包級名字那麽它將是導出的也就是説可以被外部的包訪問例如fmt包的Printf函數就是導出的可以在fmt包外部訪問。包本身的名字一般總是用小寫字母。
如果一個名字是在函數內部定義,那麽它的就隻在函數內部有效。如果是在函數外部定義,那麽將在當前包的所有文件中都可以訪問。名字的開頭字母的大小寫決定了名字在包外的可見性。如果一個名字是大寫字母開頭的(譯註:必是在函數外部定義的包級名字包級函數名本身也是包級名字那麽它將是導出的也就是説可以被外部的包訪問例如fmt包的Printf函數就是導出的可以在fmt包外部訪問。包本身的名字一般總是用小寫字母。
名字的長度沒有邏輯限製但是Go語言的風格是盡量使用短小的名字對於局部變量尤其是這樣你會經常看到i之類的短名字而不是冗長的theLoopIndex命名。通常來説如果一個名字的作用域比較大生命週期也比較長那麽用長的名字將會更有意義。

View File

@ -2,7 +2,7 @@
聲明語句定義了程序的各種實體對象以及部分或全部的屬性。Go語言主要有四種類型的聲明語句var、const、type和func分别對應變量、常量、類型和函數實體對象的聲明。這一章我們重點討論變量和類型的聲明第三章將討論常量的聲明第五章將討論函數的聲明。
一個Go語言編寫的程序對應一個或多個以.go爲文件後綴名的源文件中。每個源文件以包的聲明語句開始説明該源文件是屬於哪個包。包聲明語句之後是import語句導入依賴的其它包然後是包一級的類型、變量、常量、函數的聲明語句包一級的各種類型的聲明語句的順序無關緊要譯註函數內部的名字則必先聲明之後才能使用)。例如,下面的例子中聲明了一個常量、一個函數和兩個變量:
一個Go語言編寫的程序對應一個或多個以.go爲文件後綴名的源文件中。每個源文件以包的聲明語句開始説明該源文件是屬於哪個包。包聲明語句之後是import語句導入依賴的其它包然後是包一級的類型、變量、常量、函數的聲明語句包一級的各種類型的聲明語句的順序無關緊要譯註函數內部的名字則必先聲明之後才能使用)。例如,下面的例子中聲明了一個常量、一個函數和兩個變量:
```Go
gopl.io/ch2/boiling

View File

@ -53,7 +53,7 @@ in, err := os.Open(infile)
out, err := os.Create(outfile)
```
簡短變量聲明語句中必至少要聲明一個新的變量,下面的代碼將不能編譯通過:
簡短變量聲明語句中必至少要聲明一個新的變量,下面的代碼將不能編譯通過:
```Go
f, err := os.Open(infile)

View File

@ -16,7 +16,7 @@ fmt.Println(x) // "2"
對於聚合類型每個成員——比如結構體的每個字段、或者是數組的每個元素——也都是對應一個變量,因此可以被取地址。
變量有時候被稱爲可尋址的值。卽使變量由表達式臨時生成,那麽表達式也必能接受`&`取地址操作。
變量有時候被稱爲可尋址的值。卽使變量由表達式臨時生成,那麽表達式也必能接受`&`取地址操作。
任何類型的指針的零值都是nil。如果`p != nil`測試爲眞那麽p是指向某個有效變量。指針之間也是可以進行相等測試的隻有當它們指向同一個變量或全部是nil時才相等。
@ -55,7 +55,7 @@ incr(&v) // side effect: v is now 2
fmt.Println(incr(&v)) // "3" (and v is 3)
```
每次我們對一個變量取地址,或者複製指針,我們都是爲原變量創建了新的别名。例如,`*p`就是是 變量v的别名。指針特别有價值的地方在於我們可以不用名字而訪問一個變量但是這是一把雙刃劍要找到一個變量的所有訪問者併不容易我們必知道變量全部的别名譯註這是Go語言的垃圾迴收器所做的工作。不僅僅是指針會創建别名很多其他引用類型也會創建别名例如slice、map和chan甚至結構體、數組和接口都會創建所引用變量的别名。
每次我們對一個變量取地址,或者複製指針,我們都是爲原變量創建了新的别名。例如,`*p`就是是 變量v的别名。指針特别有價值的地方在於我們可以不用名字而訪問一個變量但是這是一把雙刃劍要找到一個變量的所有訪問者併不容易我們必知道變量全部的别名譯註這是Go語言的垃圾迴收器所做的工作。不僅僅是指針會創建别名很多其他引用類型也會創建别名例如slice、map和chan甚至結構體、數組和接口都會創建所引用變量的别名。
指針是實現標準庫中flag包的關鍵技術它使用命令行參數來設置對應變量的值而這些對應命令行標誌參數的變量可能會零散分布在整個程序中。爲了説明這一點在早些的echo版本中就包含了兩個可選的命令行參數`-n`用於忽略行尾的換行符,`-s sep`用於指定分隔字符默認是空格。下面這是第四個版本對應包路徑爲gopl.io/ch2/echo4。
@ -82,9 +82,9 @@ func main() {
}
```
調用flag.Bool函數會創建一個新的對應布爾型標誌參數的變量。它有三個屬性第一個是的命令行標誌參數的名字“n”然後是該標誌參數的默認值這里是false最後是該標誌參數對應的描述信息。如果用戶在命令行輸入了一個無效的標誌參數或者輸入`-h`或`-help`參數那麽將打印所有標誌參數的名字、默認值和描述信息。類似的調用flag.String函數將於創建一個對應字符串類型的標誌參數變量同樣包含命令行標誌參數對應的參數名、默認值、和描述信息。程序中的`sep`和`n`變量分别是指向對應命令行標誌參數變量的指針,因此必用`*sep`和`*n`形式的指針語法間接引用它們。
調用flag.Bool函數會創建一個新的對應布爾型標誌參數的變量。它有三個屬性第一個是的命令行標誌參數的名字“n”然後是該標誌參數的默認值這里是false最後是該標誌參數對應的描述信息。如果用戶在命令行輸入了一個無效的標誌參數或者輸入`-h`或`-help`參數那麽將打印所有標誌參數的名字、默認值和描述信息。類似的調用flag.String函數將於創建一個對應字符串類型的標誌參數變量同樣包含命令行標誌參數對應的參數名、默認值、和描述信息。程序中的`sep`和`n`變量分别是指向對應命令行標誌參數變量的指針,因此必用`*sep`和`*n`形式的指針語法間接引用它們。
當程序運行時,必在使用標誌參數對應的變量之前調用先flag.Parse函數用於更新每個標誌參數對應變量的值之前是默認值。對於非標誌參數的普通命令行參數可以通過調用flag.Args()函數來訪問返迴值對應對應一個字符串類型的slice。如果在flag.Parse函數解析命令行參數時遇到錯誤默認將打印相關的提示信息然後調用os.Exit(2)終止程序。
當程序運行時,必在使用標誌參數對應的變量之前調用先flag.Parse函數用於更新每個標誌參數對應變量的值之前是默認值。對於非標誌參數的普通命令行參數可以通過調用flag.Args()函數來訪問返迴值對應對應一個字符串類型的slice。如果在flag.Parse函數解析命令行參數時遇到錯誤默認將打印相關的提示信息然後調用os.Exit(2)終止程序。
讓我們運行一些echo測試用例

View File

@ -9,15 +9,19 @@ fmt.Println(*p) // "0"
fmt.Println(*p) // "2"
```
用new創建變量和普通變量聲明語句方式創建變量沒有什麽區别除了不需要聲明一個臨時變量的名字外我們還可以在表達式中使用new(T)。換言之new函數類似是一種語法,而不是一個新的基礎概念。
用new創建變量和普通變量聲明語句方式創建變量沒有什麽區别除了不需要聲明一個臨時變量的名字外我們還可以在表達式中使用new(T)。換言之new函數類似是一種語法,而不是一個新的基礎概念。
下面的兩個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函數都是返迴一個新的變量的地址因此下面兩個地址是不同的

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

@ -39,7 +39,7 @@ i, j, k = 2, 3, 5
但如果表達式太複雜的話,應該盡量避免過度使用元組賦值;因爲每個變量單獨賦值語句的寫法可讀性會更好。
有些表達式會産生多個值,比如調用一個有多個返迴值的函數。當這樣一個函數調用出現在元組賦值右邊的表達式中時(譯註:右邊不能再有其它表達式),左邊變量的數目必和右邊一致。
有些表達式會産生多個值,比如調用一個有多個返迴值的函數。當這樣一個函數調用出現在元組賦值右邊的表達式中時(譯註:右邊不能再有其它表達式),左邊變量的數目必和右邊一致。
```Go
f, err = os.Open("foo.txt") // function call returns two values

View File

@ -16,10 +16,10 @@ medals[2] = "bronze"
map和chan的元素雖然不是普通的變量但是也有類似的隱式賦值行爲。
不管是隱式還是顯式地賦值,在賦值語句左邊的變量和右邊最終的求到的值必有相同的數據類型。更直白地説,隻有右邊的值對於左邊的變量是可賦值的,賦值語句才是允許的。
不管是隱式還是顯式地賦值,在賦值語句左邊的變量和右邊最終的求到的值必有相同的數據類型。更直白地説,隻有右邊的值對於左邊的變量是可賦值的,賦值語句才是允許的。
可賦值性的規則對於不同類型有着不同要求,對每個新類型特殊的地方我們會專門解釋。對於目前我們已經討論過的類型,它的規則是簡單的:類型必完全匹配nil可以賦值給任何指針或引用類型的變量。常量§3.6)則有更靈活的賦值規則,因爲這樣可以避免不必要的顯式的類型轉換。
可賦值性的規則對於不同類型有着不同要求,對每個新類型特殊的地方我們會專門解釋。對於目前我們已經討論過的類型,它的規則是簡單的:類型必完全匹配nil可以賦值給任何指針或引用類型的變量。常量§3.6)則有更靈活的賦值規則,因爲這樣可以避免不必要的顯式的類型轉換。
對於兩個值是否可以用`==`或`!=`進行相等比較的能力也和可賦值能力有關繫:對於任何類型的值的相等比較,第二個值必是對第一個值類型對應的變量是可賦值的,反之依然。和前面一樣,我們會對每個新類型比較特殊的地方做專門的解釋。
對於兩個值是否可以用`==`或`!=`進行相等比較的能力也和可賦值能力有關繫:對於任何類型的值的相等比較,第二個值必是對第一個值類型對應的變量是可賦值的,反之依然。和前面一樣,我們會對每個新類型比較特殊的地方做專門的解釋。

View File

@ -2,7 +2,7 @@
Go語言中的包和其他語言的庫或模塊的概念類似目的都是爲了支持模塊化、封裝、單獨編譯和代碼重用。一個包的源代碼保存在一個或多個以.go爲文件後綴名的源文件中通常一個包所在目録路徑的後綴是包的導入路徑例如包gopl.io/ch1/helloworld對應的目録路徑是$GOPATH/src/gopl.io/ch1/helloworld。
每個包都對應一個獨立的名字空間。例如在image包中的Decode函數和在unicode/utf16包中的 Decode函數是不同的。要在外部引用該函數顯式使用image.Decode或utf16.Decode形式訪問。
每個包都對應一個獨立的名字空間。例如在image包中的Decode函數和在unicode/utf16包中的 Decode函數是不同的。要在外部引用該函數顯式使用image.Decode或utf16.Decode形式訪問。
包還可以讓我們通過控製哪些名字是外部可見的來隱藏內部實現信息。在Go語言中一個簡單的規則是如果一個名字是大寫字母開頭的那麽該名字是導出的譯註因爲漢字不區分大小寫因此漢字開頭的名字是沒有導出的

View File

@ -101,7 +101,7 @@ fmt.Printf("%08b\n", x>>1) // "00010001", the set {0, 4}
6.5節給出了一個可以遠大於一個字節的整數集的實現。)
在`x<<n``x>>n`移位運算中決定了移位操作bit數部分必是無符號數被操作的x數可以是有符號或無符號數。算術上一個`x<<n`$$2^n$$`x>>n`右移運算等價於除以$$2^n$$。
在`x<<n``x>>n`移位運算中決定了移位操作bit數部分必是無符號數被操作的x數可以是有符號或無符號數。算術上一個`x<<n`$$2^n$$`x>>n`右移運算等價於除以$$2^n$$。
左移運算用零填充右邊空缺的bit位無符號數的右移運算也是用0填充左邊空缺的bit位但是有符號數的右移運算會用符號位的值填充左邊空缺的bit位。因爲這個原因最好用無符號運算這樣你可以將整數完全當作一個bit位模式處理。
@ -118,7 +118,7 @@ for i := len(medals) - 1; i >= 0; i-- {
出於這個原因無符號數往往隻有在位運算或其它特殊的運算場景才會使用就像bit集合、分析二進製文件格式或者是哈希和加密操作等。它們通常併不用於僅僅是表達非負數量的場合。
一般來説,需要一個顯式的轉換將一個值從一種類型轉化位另一種類型,併且算術和邏輯運算的二元操作中必是相同的類型。雖然這偶爾會導致需要很長的表達式,但是它消除了所有和類型相關的問題,而且也使得程序容易理解。
一般來説,需要一個顯式的轉換將一個值從一種類型轉化位另一種類型,併且算術和邏輯運算的二元操作中必是相同的類型。雖然這偶爾會導致需要很長的表達式,但是它消除了所有和類型相關的問題,而且也使得程序容易理解。
在很多場景,會遇到類似下面的代碼通用的錯誤:

View File

@ -154,12 +154,12 @@ func f(x, y float64) float64 {
**練習 3.3** 根據高度給每個多邊形上色,那樣峯值部將是紅色(#ff0000),谷部將是藍色(#0000ff)。
**練習 3.4** 參考1.7節Lissajous例子的函數構造一個web服務器用於計算函數麴面然後返迴SVG數據給客戶端。服務器必設置Content-Type頭部
**練習 3.4** 參考1.7節Lissajous例子的函數構造一個web服務器用於計算函數麴面然後返迴SVG數據給客戶端。服務器必設置Content-Type頭部
```Go
w.Header().Set("Content-Type", "image/svg+xml")
```
這一步在Lissajous例子中不是必因爲服務器使用標準的PNG圖像格式可以根據前面的512個字節自動輸出對應的頭部。允許客戶端通過HTTP請求參數設置高度、寬度和顔色等參數。
這一步在Lissajous例子中不是必因爲服務器使用標準的PNG圖像格式可以根據前面的512個字節自動輸出對應的頭部。允許客戶端通過HTTP請求參數設置高度、寬度和顔色等參數。

View File

@ -20,7 +20,7 @@ if 'a' <= c && c <= 'z' ||
}
```
布爾值併不會隱式轉換爲數字值0或1反之亦然。必使用一個顯式的if語句輔助轉換
布爾值併不會隱式轉換爲數字值0或1反之亦然。必使用一個顯式的if語句輔助轉換
```Go
i := 0

View File

@ -30,7 +30,7 @@ Unicode轉義也可以使用在rune字符中。下面三個字符是等價的
'世' '\u4e16' '\U00004e16'
```
對於小於256碼點值可以寫在一個十六進製轉義字節中例如'\x41'對應字符'A',但是對於更大的碼點則必使用\u或\U轉義形式。因此'\xe4\xb8\x96'併不是一個合法的rune字符雖然這三個字節對應一個有效的UTF8編碼的碼點。
對於小於256碼點值可以寫在一個十六進製轉義字節中例如'\x41'對應字符'A',但是對於更大的碼點則必使用\u或\U轉義形式。因此'\xe4\xb8\x96'併不是一個合法的rune字符雖然這三個字節對應一個有效的UTF8編碼的碼點。
得益於UTF8編碼優良的設計諸多字符串操作都不需要解碼操作。我們可以不用解碼直接測試一個字符串是否是另一個字符串的前綴

View File

@ -2,7 +2,7 @@
一個字符串是一個不可改變的字節序列。字符串可以包含任意的數據包括byte值0但是通常是用來包含人類可讀的文本。文本字符串通常被解釋爲采用UTF8編碼的Unicode碼點rune序列我們稍後會詳細討論這個問題。
內置的len函數可以返迴一個字符串中的字節數目不是rune字符數目索引操作s[i]返迴第i個字節的字節值i必滿足0 ≤ i< len(s)
內置的len函數可以返迴一個字符串中的字節數目不是rune字符數目索引操作s[i]返迴第i個字節的字節值i必滿足0 ≤ i< len(s)
```Go
s := "hello, world"

View File

@ -80,7 +80,7 @@ c := 0i // untyped complex; implicit complex128(0i)
註意默認類型是規則的無類型的整數常量默認轉換爲int對應不確定的內存大小但是浮點數和複數常量則默認轉換爲float64和complex128。Go語言本身併沒有不確定內存大小的浮點數和複數類型而且如果不知道浮點數類型的話將很難寫出正確的數值算法。
如果要給變量一個不同的類型,我們必顯式地將無類型的常量轉化爲所需的類型,或給聲明的變量指定明確的類型,像下面例子這樣:
如果要給變量一個不同的類型,我們必顯式地將無類型的常量轉化爲所需的類型,或給聲明的變量指定明確的類型,像下面例子這樣:
```Go
var i = int8(0)

View File

@ -35,7 +35,7 @@ q := [...]int{1, 2, 3}
fmt.Printf("%T\n", q) // "[3]int"
```
數組的長度是數組類型的一個組成部分,因此[3]int和[4]int是兩種不同的數組類型。數組的長度必是常量表達式,因爲數組的長度需要在編譯階段確定。
數組的長度是數組類型的一個組成部分,因此[3]int和[4]int是兩種不同的數組類型。數組的長度必是常量表達式,因爲數組的長度需要在編譯階段確定。
```Go
q := [3]int{1, 2, 3}

View File

@ -38,7 +38,7 @@ func appendInt(x []int, y int) []int {
}
```
每次調用appendInt函數先檢測slice底層數組是否有足夠的容量來保存新添加的元素。如果有足夠空間的話直接擴展slice依然在原有的底層數組之上將新添加的y元素複製到新擴展的空間併返迴slice。因此輸入的x和輸出的z共享相同的底層數組。
每次調用appendInt函數先檢測slice底層數組是否有足夠的容量來保存新添加的元素。如果有足夠空間的話直接擴展slice依然在原有的底層數組之上將新添加的y元素複製到新擴展的空間併返迴slice。因此輸入的x和輸出的z共享相同的底層數組。
如果沒有足夠的增長空間的話appendInt函數則會先分配一個足夠大的slice用於保存新的結果先將輸入的x複製到新的空間然後添加y元素。結果z和輸入的x引用的將是不同的底層數組。

View File

@ -80,7 +80,7 @@ fmt.Println(s) // "[2 3 4 5 0 1]"
要註意的是slice類型的變量s和數組類型的變量a的初始化語法的差異。slice和數組的字面值語法很類似它們都是用花括弧包含一繫列的初始化元素但是對於slice併沒有指明序列的長度。這會隱式地創建一個合適大小的數組然後slice的指針指向底層的數組。就像數組字面值一樣slice的字面值也可以按順序指定初始化值序列或者是通過索引和元素值指定或者的兩種風格的混合語法初始化。
和數組不同的是slice之間不能比較因此我們不能使用==操作符來判斷兩個slice是否含有全部相等元素。不過標準庫提供了高度優化的bytes.Equal函數來判斷兩個字節型slice是否相等[]byte但是對於其他類型的slice我們必自己展開每個元素進行比較:
和數組不同的是slice之間不能比較因此我們不能使用==操作符來判斷兩個slice是否含有全部相等元素。不過標準庫提供了高度優化的bytes.Equal函數來判斷兩個字節型slice是否相等[]byte但是對於其他類型的slice我們必自己展開每個元素進行比較:
```Go
func equal(x, y []string) bool {
@ -98,7 +98,7 @@ func equal(x, y []string) bool {
上面關於兩個slice的深度相等測試運行的時間併不比支持==操作的數組或字符串更多但是爲何slice不直接支持比較運算符呢這方面有兩個原因。第一個原因一個slice的元素是間接引用的一個slice甚至可以包含自身。雖然有很多辦法處理這種情形但是沒有一個是簡單有效的。
第二個原因因爲slice的元素是間接引用的一個固定值的slice在不同的時間可能包含不同的元素因爲底層數組的元素可能會被脩改。併且Go語言中map等哈希表之類的數據結構的key隻做簡單的淺拷貝它要求在整個聲明週期中相等的key必對相同的元素。對於像指針或chan之類的引用類型==相等測試可以判斷兩個是否是引用相同的對象。一個針對slice的淺相等測試的==操作符可能是有一定用處的也能臨時解決map類型的key問題但是slice和數組不同的相等測試行爲會讓人睏惑。因此安全的做飯是直接禁止slice之間的比較操作。
第二個原因因爲slice的元素是間接引用的一個固定值的slice在不同的時間可能包含不同的元素因爲底層數組的元素可能會被脩改。併且Go語言中map等哈希表之類的數據結構的key隻做簡單的淺拷貝它要求在整個聲明週期中相等的key必對相同的元素。對於像指針或chan之類的引用類型==相等測試可以判斷兩個是否是引用相同的對象。一個針對slice的淺相等測試的==操作符可能是有一定用處的也能臨時解決map類型的key問題但是slice和數組不同的相等測試行爲會讓人睏惑。因此安全的做飯是直接禁止slice之間的比較操作。
slice唯一合法的比較操作是和nil比較例如

View File

@ -2,7 +2,7 @@
哈希表是一種巧妙併且實用的數據結構。它是一個無序的key/value對的集合其中所有的key都是不同的然後通過給定的key可以在常數時間複雜度內檢索、更新或刪除對應的value。
在Go語言中一個map就是一個哈希表的引用map類型可以寫爲map[K]V其中K和V分别對應key和value。map中所有的key都有相同的類型所以的value也有着相同的類型但是key和value之間可以是不同的數據類型。其中K對應的key必是支持==比較運算符的數據類型所以map可以通過測試key是否相等來判斷是否已經存在。雖然浮點數類型也是支持相等運算符比較的但是將浮點數用做key類型則是一個壞的想法正如第三章提到的最壞的情況是可能出現的NaN和任何浮點數都不相等。對於V對應的value數據類型則沒有任何的限製。
在Go語言中一個map就是一個哈希表的引用map類型可以寫爲map[K]V其中K和V分别對應key和value。map中所有的key都有相同的類型所以的value也有着相同的類型但是key和value之間可以是不同的數據類型。其中K對應的key必是支持==比較運算符的數據類型所以map可以通過測試key是否相等來判斷是否已經存在。雖然浮點數類型也是支持相等運算符比較的但是將浮點數用做key類型則是一個壞的想法正如第三章提到的最壞的情況是可能出現的NaN和任何浮點數都不相等。對於V對應的value數據類型則沒有任何的限製。
內置的make函數可以創建一個map
@ -76,7 +76,7 @@ for name, age := range ages {
}
```
Map的迭代順序是不確定的併且不同的哈希函數實現可能導致不同的遍歷順序。在實踐中遍歷的順序是隨機的每一次遍歷的順序都不相同。這是故意的每次都使用隨機的遍歷順序可以強製要求程序不會依賴具體的哈希函數實現。如果要按順序遍歷key/value對我們必顯式地對key進行排序可以使用sort包的Strings函數對字符串slice進行排序。下面是常見的處理方式
Map的迭代順序是不確定的併且不同的哈希函數實現可能導致不同的遍歷順序。在實踐中遍歷的順序是隨機的每一次遍歷的順序都不相同。這是故意的每次都使用隨機的遍歷順序可以強製要求程序不會依賴具體的哈希函數實現。如果要按順序遍歷key/value對我們必顯式地對key進行排序可以使用sort包的Strings函數對字符串slice進行排序。下面是常見的處理方式
```Go
import "sort"
@ -113,7 +113,7 @@ map上的大部分操作包括査找、刪除、len和range循環都可以安
ages["carol"] = 21 // panic: assignment to entry in nil map
```
在向map存數據前必先創建map。
在向map存數據前必先創建map。
通過key作爲索引下標來訪問map將産生一個value。如果key在map中是存在的那麽將得到與key對應的value如果key不存在那麽將得到value對應類型的零值正如我們前面看到的ages["bob"]那樣。這個規則很實用但是有時候可能需要知道對應的元素是否眞的是在map之中。例如如果元素類型是一個數字你可以需要區分一個已經存在的0和不存在而返迴零值的0可以像下面這樣測試
@ -130,7 +130,7 @@ if age, ok := ages["bob"]; !ok { /* ... */ }
在這種場景下map的下標語法將産生兩個值第二個是一個布爾值用於報告元素是否眞的存在。布爾變量一般命名爲ok特别適合馬上用於if條件判斷部分。
和slice一樣map之間也不能進行相等比較唯一的例外是和nil進行比較。要判斷兩個map是否包含相同的key和value我們必通過一個循環實現:
和slice一樣map之間也不能進行相等比較唯一的例外是和nil進行比較。要判斷兩個map是否包含相同的key和value我們必通過一個循環實現:
```Go
func equal(x, y map[string]int) bool {
@ -178,7 +178,7 @@ func main() {
Go程序員將這種忽略value的map當作一個字符串集合併非所有`map[string]bool`類型value都是無關緊要的有一些則可能會同時包含tue和false的值。
有時候我們需要一個map或set的key是slice類型但是map的key必是可比較的類型但是slice併不滿足這個條件。不過我們可以通過兩個步驟繞過這個限製。第一步定義一個輔助函數k將slice轉爲map對應的string類型的key確保隻有x和y相等時k(x) == k(y)才成立。然後創建一個key爲string類型的map在每次對map操作時先用k輔助函數將slice轉化爲string類型。
有時候我們需要一個map或set的key是slice類型但是map的key必是可比較的類型但是slice併不滿足這個條件。不過我們可以通過兩個步驟繞過這個限製。第一步定義一個輔助函數k將slice轉爲map對應的string類型的key確保隻有x和y相等時k(x) == k(y)才成立。然後創建一個key爲string類型的map在每次對map操作時先用k輔助函數將slice轉化爲string類型。
下面的例子演示了如何使用map來記録提交相同的字符串列表的次數。它使用了fmt.Sprintf函數將字符串列表轉換爲一個字符串以用於map的key通過%q參數忠實地記録每個字符串元素的信息

View File

@ -50,7 +50,7 @@ func Bonus(e *Employee, percent int) int {
}
```
如果要在函數內部脩改結構體成員的話,用指針傳入是必因爲在Go語言中所有的函數參數都是值拷貝傳入的函數參數將不再是函數調用時的原始變量。
如果要在函數內部脩改結構體成員的話,用指針傳入是必因爲在Go語言中所有的函數參數都是值拷貝傳入的函數參數將不再是函數調用時的原始變量。
```Go
func AwardAnnualRaise(e *Employee) {

View File

@ -52,7 +52,7 @@ w.Circle.Radius = 5
w.Spokes = 20
```
Go語言有一個特性讓我們隻聲明一個成員對應的數據類型而不指名成員的名字這類成員就叫匿名成員。匿名成員的數據類型必是命名的類型或指向一個命名的類型的指針。下面的代碼中Circle和Wheel各自都有一個匿名成員。我們可以説Point類型被嵌入到了Circle結構體同時Circle類型被嵌入到了Wheel結構體。
Go語言有一個特性讓我們隻聲明一個成員對應的數據類型而不指名成員的名字這類成員就叫匿名成員。匿名成員的數據類型必是命名的類型或指向一個命名的類型的指針。下面的代碼中Circle和Wheel各自都有一個匿名成員。我們可以説Point類型被嵌入到了Circle結構體同時Circle類型被嵌入到了Wheel結構體。
```Go
type Circle struct {
@ -85,7 +85,7 @@ w = Wheel{8, 8, 5, 20} // compile error: unknown fields
w = Wheel{X: 8, Y: 8, Radius: 5, Spokes: 20} // compile error: unknown fields
```
結構體字面值必遵循形狀類型聲明時的結構,所以我們隻能用下面的兩種語法,它們彼此是等價的:
結構體字面值必遵循形狀類型聲明時的結構,所以我們隻能用下面的兩種語法,它們彼此是等價的:
```Go
gopl.io/ch4/embed
@ -121,6 +121,6 @@ w.X = 8 // equivalent to w.circle.point.X = 8
但是在包外部因爲circle和point沒有導出不能訪問它們的成員因此簡短的匿名成員訪問語法也是禁止的。
到目前未知,我們看到匿名成員特性隻是對訪問嵌套成員的點運算符提供了簡短的語法醣。稍後,我們將會看到匿名成員併不要求是結構體類型;其實任何命令的類型都可以作爲結構體的匿名成員。但是爲什麽要嵌入一個沒有任何子成員類型的匿名成員類型呢?
到目前爲止,我們看到匿名成員特性隻是對訪問嵌套成員的點運算符提供了簡短的語法糖。稍後,我們將會看到匿名成員併不要求是結構體類型;其實任何命令的類型都可以作爲結構體的匿名成員。但是爲什麽要嵌入一個沒有任何子成員類型的匿名成員類型呢?
答案是匿名類型的方法集。簡短的點運算符語法可以用於選擇匿名成員嵌套的成員也可以用於訪問它們的方法。實際上外層的結構體不僅僅是獲得了匿名成員類型的所有成員而且也獲得了該類型導出的全部的方法。這個機製可以用於將一個有簡單行爲的對象組合成有複雜行爲的對象。組合是Go語言中面向對象編程的核心我們將在6.3節中專門討論。

View File

@ -199,7 +199,7 @@ func SearchIssues(terms []string) (*IssuesSearchResult, error) {
}
```
在早些的例子中我們使用了json.Unmarshal函數來將JSON格式的字符串解碼爲字節slice。但是這個例子中我們使用了基於流式的解碼器json.Decoder它可以從一個輸入流解碼JSON數據盡管這不是必的。如您所料還有一個針對輸出流的json.Encoder編碼對象。
在早些的例子中我們使用了json.Unmarshal函數來將JSON格式的字符串解碼爲字節slice。但是這個例子中我們使用了基於流式的解碼器json.Decoder它可以從一個輸入流解碼JSON數據盡管這不是必的。如您所料還有一個針對輸出流的json.Encoder編碼對象。
我們調用Decode方法來填充變量。這里有多種方法可以格式化結構。下面是最簡單的一種以一個固定寬度打印每個issue但是在下一節我們將看到如果利用模闆來輸出複雜的格式。

View File

@ -20,7 +20,7 @@ fmt.Println(hypot(3,4)) // "5"
x和y是形參名,3和4是調用時的傳入的實數函數返迴了一個float64類型的值。
返迴值也可以像形式參數一樣被命名。在這種情況下每個返迴值被聲明成一個局部變量併根據該返迴值的類型將其初始化爲0。
如果一個函數在聲明時,包含返迴值列表,該函數必以 return語句結尾除非函數明顯無法運行到結尾處。例如函數在結尾時調用了panic異常或函數中存在無限循環。
如果一個函數在聲明時,包含返迴值列表,該函數必以 return語句結尾除非函數明顯無法運行到結尾處。例如函數在結尾時調用了panic異常或函數中存在無限循環。
正如hypot一樣如果一組形參或返迴值有相同的類型我們不必爲每個形參都寫出參數類型。下面2個聲明是等價的
@ -45,7 +45,7 @@ fmt.Printf("%T\n", zero) // "func(int, int) int"
函數的類型被稱爲函數的標識符。如果兩個函數形式參數列表和返迴值列表中的變量類型一一對應,那麽這兩個函數被認爲有相同的類型和標識符。形參和返迴值的變量名不影響函數標識符也不影響它們是否可以以省略參數類型的形式表示。
每一次函數調用都必按照聲明順序爲所有參數提供實參參數值。在函數調用時Go語言沒有默認參數值也沒有任何方法可以通過參數名指定形參因此形參和返迴值的變量名對於函數調用者而言沒有意義。
每一次函數調用都必按照聲明順序爲所有參數提供實參參數值。在函數調用時Go語言沒有默認參數值也沒有任何方法可以通過參數名指定形參因此形參和返迴值的變量名對於函數調用者而言沒有意義。
在函數體中,函數的形參作爲局部變量,被初始化爲調用者提供的值。函數的形參和有名返迴值作爲函數最外層的局部變量,被存儲在相同的詞法塊中。

View File

@ -11,25 +11,25 @@ golang.org/x/net/html
package html
type Node struct {
Type NodeType
Data string
Attr []Attribute
FirstChild, NextSibling *Node
Type NodeType
Data string
Attr []Attribute
FirstChild, NextSibling *Node
}
type NodeType int32
const (
ErrorNode NodeType = iota
TextNode
DocumentNode
ElementNode
CommentNode
DoctypeNode
ErrorNode NodeType = iota
TextNode
DocumentNode
ElementNode
CommentNode
DoctypeNode
)
type Attribute struct {
Key, Val string
Key, Val string
}
func Parse(r io.Reader) (*Node, error)
@ -43,21 +43,21 @@ gopl.io/ch5/findlinks1
package main
import (
"fmt"
"os"
"fmt"
"os"
"golang.org/x/net/html"
"golang.org/x/net/html"
)
func main() {
doc, err := html.Parse(os.Stdin)
if err != nil {
fmt.Fprintf(os.Stderr, "findlinks1: %v\n", err)
os.Exit(1)
}
for _, link := range visit(nil, doc) {
fmt.Println(link)
}
doc, err := html.Parse(os.Stdin)
if err != nil {
fmt.Fprintf(os.Stderr, "findlinks1: %v\n", err)
os.Exit(1)
}
for _, link := range visit(nil, doc) {
fmt.Println(link)
}
}
```
@ -66,17 +66,17 @@ visit函數遍歷HTML的節點樹從每一個anchor元素的href屬性獲得l
```Go
// visit appends to links each link found in n and returns the result.
func visit(links []string, n *html.Node) []string {
if n.Type == html.ElementNode && n.Data == "a" {
for _, a := range n.Attr {
if a.Key == "href" {
links = append(links, a.Val)
}
}
}
for c := n.FirstChild; c != nil; c = c.NextSibling {
links = visit(links, c)
}
return links
if n.Type == html.ElementNode && n.Data == "a" {
for _, a := range n.Attr {
if a.Key == "href" {
links = append(links, a.Val)
}
}
}
for c := n.FirstChild; c != nil; c = c.NextSibling {
links = visit(links, c)
}
return links
}
```
@ -109,21 +109,21 @@ http://www.google.com/intl/en/policies/privacy/
```Go
gopl.io/ch5/outline
func main() {
doc, err := html.Parse(os.Stdin)
if err != nil {
fmt.Fprintf(os.Stderr, "outline: %v\n", err)
os.Exit(1)
}
outline(nil, doc)
doc, err := html.Parse(os.Stdin)
if err != nil {
fmt.Fprintf(os.Stderr, "outline: %v\n", err)
os.Exit(1)
}
outline(nil, doc)
}
func outline(stack []string, n *html.Node) {
if n.Type == html.ElementNode {
stack = append(stack, n.Data) // push tag
fmt.Println(stack)
}
for c := n.FirstChild; c != nil; c = c.NextSibling {
outline(stack, c)
}
if n.Type == html.ElementNode {
stack = append(stack, n.Data) // push tag
fmt.Println(stack)
}
for c := n.FirstChild; c != nil; c = c.NextSibling {
outline(stack, c)
}
}
```
@ -153,10 +153,10 @@ $ ./fetch https://golang.org | ./outline
大部分編程語言使用固定大小的函數調用棧常見的大小從64KB到2MB不等。固定大小棧會限製遞歸的深度當你用遞歸處理大量數據時需要避免棧溢出除此之外還會導致安全性問題。與相反,Go語言使用可變棧棧的大小按需增加(初始時很小)。這使得我們使用遞歸時不必考慮溢出和安全問題。
練習**5.1** :脩改findlinks代碼中遍歷n.FirstChild鏈表的部分將循環調用visit改成遞歸調用。
**練習 5.1** 脩改findlinks代碼中遍歷n.FirstChild鏈表的部分將循環調用visit改成遞歸調用。
練習**5.2** : 編寫函數記録在HTML樹中出現的同名元素的次數。
**練習 5.2** 編寫函數記録在HTML樹中出現的同名元素的次數。
練習**5.3** : 編寫函數輸出所有text結點的內容。註意不要訪問`<script>``<style>`,
**練習 5.3** 編寫函數輸出所有text結點的內容。註意不要訪問`<script>``<style>`,
練習**5.4** : 擴展vist函數使其能夠處理其他類型的結點如images、scripts和style sheets。
**練習 5.4** 擴展vist函數使其能夠處理其他類型的結點如images、scripts和style sheets。

View File

@ -41,9 +41,9 @@ func findLinks(url string) ([]string, error) {
在findlinks中有4處return語句每一處return都返迴了一組值。前三處return將http和html包中的錯誤信息傳遞給findlinks的調用者。第一處return直接返迴錯誤信息其他兩處通過fmt.Errorf§7.8輸出詳細的錯誤信息。如果findlinks成功結束最後的return語句將一組解析獲得的連接返迴給用戶。
在finallinks中我們必確保resp.Body被關閉釋放網絡資源。雖然Go的垃圾迴收機製會迴收不被使用的內存但是這不包括操作繫統層面的資源比如打開的文件、網絡連接。因此我們必顯式的釋放這些資源。
在finallinks中我們必確保resp.Body被關閉釋放網絡資源。雖然Go的垃圾迴收機製會迴收不被使用的內存但是這不包括操作繫統層面的資源比如打開的文件、網絡連接。因此我們必顯式的釋放這些資源。
調用多返迴值函數時,返迴給調用者的是一組值,調用者必顯式的將這些值分配給變量:
調用多返迴值函數時,返迴給調用者的是一組值,調用者必顯式的將這些值分配給變量:
```Go
links, err := findLinks(url)

View File

@ -1,6 +1,6 @@
### 5.4.2. 文件結尾錯誤EOF
函數經常會返迴多種錯誤,這對終端用戶來説可能會很有趣,但對程序而言,這使得情況變得複雜。很多時候,程序必根據錯誤類型作出不同的響應。讓我們考慮這樣一個例子從文件中讀取n個字節。如果n等於文件的長度讀取過程的任何錯誤都表示失敗。如果n小於文件的長度調用者會重複的讀取固定大小的數據直到文件結束。這會導致調用者必分别處理由文件結束引起的各種錯誤。基於這樣的原因io包保證任何由文件結束引起的讀取失敗都返迴同一個錯誤——io.EOF該錯誤在io包中定義
函數經常會返迴多種錯誤,這對終端用戶來説可能會很有趣,但對程序而言,這使得情況變得複雜。很多時候,程序必根據錯誤類型作出不同的響應。讓我們考慮這樣一個例子從文件中讀取n個字節。如果n等於文件的長度讀取過程的任何錯誤都表示失敗。如果n小於文件的長度調用者會重複的讀取固定大小的數據直到文件結束。這會導致調用者必分别處理由文件結束引起的各種錯誤。基於這樣的原因io包保證任何由文件結束引起的讀取失敗都返迴同一個錯誤——io.EOF該錯誤在io包中定義
```Go
package io

View File

@ -1,11 +1,11 @@
## 5.4. 錯誤
在Go中有一部分函數總是能成功的運行。比如string.Contains和strconv.FormatBool函數對各種可能的輸入都做了良好的處理使得運行時幾乎不會失敗除非遇到災難性的、不可預料的情況比如運行時的內存溢出。導致這種錯誤的原因很複雜難以處理從錯誤中恢複的可能性也很低。
在Go中有一部分函數總是能成功的運行。比如strings.Contains和strconv.FormatBool函數對各種可能的輸入都做了良好的處理使得運行時幾乎不會失敗除非遇到災難性的、不可預料的情況比如運行時的內存溢出。導致這種錯誤的原因很複雜難以處理從錯誤中恢複的可能性也很低。
還有一部分函數隻要輸入的參數滿足一定條件也能保證運行成功。比如time.Date函數該函數將年月日等參數構造成time.Time對象除非最後一個參數時區是nil。這種情況下會引發panic異常。panic是來自被調函數的信號表示發生了某個已知的bug。一個良好的程序永遠不應該發生panic異常。
對於大部分函數而言永遠無法確保能否成功運行。這是因爲錯誤的原因超出了程序員的控製。舉個例子任何進行I/O操作的函數都會面臨出現錯誤的可能隻有沒有經驗的程序員才會相信讀寫操作不會失敗卽時是簡單的讀寫。因此當本該可信的操作出乎意料的失敗後我們必弄清楚導致失敗的原因。
對於大部分函數而言永遠無法確保能否成功運行。這是因爲錯誤的原因超出了程序員的控製。舉個例子任何進行I/O操作的函數都會面臨出現錯誤的可能隻有沒有經驗的程序員才會相信讀寫操作不會失敗卽時是簡單的讀寫。因此當本該可信的操作出乎意料的失敗後我們必弄清楚導致失敗的原因。
在Go的錯誤處理中錯誤是軟件包API和應用程序用戶界面的一個重要組成部分程序運行失敗僅被認爲是幾個預期的結果之一。

View File

@ -21,7 +21,7 @@
```Go
var f func(int) int
f(3) // 此處f的值爲nil,會引起panic錯誤
f(3) // 此處f的值爲nil, 會引起panic錯誤
```
函數值可以與nil比較
@ -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

@ -36,7 +36,7 @@ squares的例子證明函數值不僅僅是一串代碼還記録了狀態
通過這個例子我們看到變量的生命週期不由它的作用域決定squares返迴後變量x仍然隱式的存在於f中。
接下來,我們討論一個有點學術性的例子,考慮這樣一個問題:給定一些計算機課程,每個課程都有前置課程,隻有完成了前置課程才可以開始當前課程的學習;我們的目標是選擇出一組課程,這組課程必確保按順序學習時,能全部被完成。每個課程的前置課程如下:
接下來,我們討論一個有點學術性的例子,考慮這樣一個問題:給定一些計算機課程,每個課程都有前置課程,隻有完成了前置課程才可以開始當前課程的學習;我們的目標是選擇出一組課程,這組課程必確保按順序學習時,能全部被完成。每個課程的前置課程如下:
```Go
gopl.io/ch5/toposort
@ -91,7 +91,7 @@ func topoSort(m map[string][]string) []string {
}
```
當匿名函數需要被遞歸調用時,我們必首先聲明一個變量(在上面的例子中,我們首先聲明了 visitAll再將匿名函數賦值給這個變量。如果不分成兩部函數字面量無法與visitAll綁定我們也無法遞歸調用該匿名函數。
當匿名函數需要被遞歸調用時,我們必首先聲明一個變量(在上面的例子中,我們首先聲明了 visitAll再將匿名函數賦值給這個變量。如果不分成兩部函數字面量無法與visitAll綁定我們也無法遞歸調用該匿名函數。
```Go
visitAll := func(items []string) {

View File

@ -120,7 +120,7 @@ func trace(msg string) func() {
每一次bigSlowOperation被調用程序都會記録函數的進入退出持續時間。我們用time.Sleep模擬一個耗時的操作
```powershell
```
$ go build gopl.io/ch5/trace
$ ./trace
2015/11/18 09:53:26 enter bigSlowOperation

View File

@ -4,7 +4,7 @@ Go的類型繫統會在編譯時捕獲很多錯誤但有些錯誤隻能在運
一般而言當panic異常發生時程序會中斷運行併立卽執行在該goroutine可以先理解成線程在第8章會詳細介紹中被延遲的函數defer 機製。隨後程序崩潰併輸出日誌信息。日誌信息包括panic value和函數調用的堆棧跟蹤信息。panic value通常是某種錯誤信息。對於每個goroutine日誌信息中都會有與之相對的發生panic時的函數調用堆棧跟蹤信息。通常我們不需要再次運行程序去定位問題日誌信息已經提供了足夠的診斷依據。因此在我們填寫問題報告時一般會將panic異常和日誌信息一併記録。
不是所有的panic異常都來自運行時直接調用內置的panic函數也會引發panic異常panic函數接受任何值作爲參數。 當某些不應該發生的場景發生時我們就應該調用panic。比如當程序到達了某條邏輯上不可能到達的路徑
不是所有的panic異常都來自運行時直接調用內置的panic函數也會引發panic異常panic函數接受任何值作爲參數。當某些不應該發生的場景發生時我們就應該調用panic。比如當程序到達了某條邏輯上不可能到達的路徑
```Go
switch s := suit(drawCard()); s {
@ -17,7 +17,7 @@ switch s := suit(drawCard()); s {
}
```
斷言函數必滿足的前置條件是明智的做法,但這很容易被濫用。除非你能提供更多的錯誤信息,或者能更快速的發現錯誤,否則不需要使用斷言,編譯器在運行時會幫你檢査代碼。
斷言函數必滿足的前置條件是明智的做法,但這很容易被濫用。除非你能提供更多的錯誤信息,或者能更快速的發現錯誤,否則不需要使用斷言,編譯器在運行時會幫你檢査代碼。
```Go
func Reset(x *Buffer) {
@ -67,7 +67,7 @@ func f(x int) {
上例中的運行輸出如下:
```bash
```
f(3)
f(2)
f(1)
@ -78,7 +78,7 @@ defer 3
當f(0)被調用時發生panic異常之前被延遲執行的的3個fmt.Printf被調用。程序中斷執行後panic信息和堆棧信息會被輸出下面是簡化的輸出
```powershell
```
panic: runtime error: integer divide by zero
main.f(0)
src/gopl.io/ch5/defer1/defer.go:14
@ -111,7 +111,7 @@ func printStack() {
printStack的簡化輸出如下下面隻是printStack的輸出不包括panic的日誌信息
```bash
```
goroutine 1 [running]:
main.printStack()
src/gopl.io/ch5/defer2/defer.go:20

View File

@ -77,7 +77,7 @@ fmt.Println(perim.Distance()) // "12"
在上面兩個對Distance名字的方法的調用中編譯器會根據方法的名字以及接收器來決定具體調用的是哪一個函數。第一個例子中path[i-1]數組中的類型是Point因此Point.Distance這個方法被調用在第二個例子中perim的類型是Path因此Distance調用的是Path.Distance。
對於一個給定的類型,其內部的方法都必有唯一的方法名但是不同的類型卻可以有同樣的方法名比如我們這里Point和Path就都有Distance這個名字的方法所以我們沒有必要非在方法名之前加類型名來消除歧義比如PathDistance。這里我們已經看到了方法比之函數的一些好處方法名可以簡短。當我們在包外調用的時候這種好處就會被放大因爲我們可以使用這個短名字而可以省略掉包的名字下面是例子
對於一個給定的類型,其內部的方法都必有唯一的方法名但是不同的類型卻可以有同樣的方法名比如我們這里Point和Path就都有Distance這個名字的方法所以我們沒有必要非在方法名之前加類型名來消除歧義比如PathDistance。這里我們已經看到了方法比之函數的一些好處方法名可以簡短。當我們在包外調用的時候這種好處就會被放大因爲我們可以使用這個短名字而可以省略掉包的名字下面是例子
```Go
import "gopl.io/ch6/geometry"
@ -87,4 +87,4 @@ fmt.Println(geometry.PathDistance(perim)) // "12", standalone function
fmt.Println(perim.Distance()) // "12", method of geometry.Path
```
譯註如果我們要用方法去計算perim的distance還需要去寫全geometry的包名和其函數名但是因爲Path這個變量定義了一個可以直接用的Distance方法所以我們可以直接寫perim.Distance()。相當於可以少打很多字作者應該是這個意思。因爲在Go里包外調用函數需要帶上包名還是挺麻煩的。
**譯註:** 如果我們要用方法去計算perim的distance還需要去寫全geometry的包名和其函數名但是因爲Path這個變量定義了一個可以直接用的Distance方法所以我們可以直接寫perim.Distance()。相當於可以少打很多字作者應該是這個意思。因爲在Go里包外調用函數需要帶上包名還是挺麻煩的。

View File

@ -63,4 +63,4 @@ m.Add("item", "3") // panic: assignment to entry in nil map
對Get的最後一次調用中nil接收器的行爲卽是一個空map的行爲。我們可以等價地將這個操作寫成Value(nil).Get("item")但是如果你直接寫nil.Get("item")的話是無法通過編譯的因爲nil的字面量編譯器無法判斷其準備類型。所以相比之下最後的那行m.Add的調用就會産生一個panic因爲他嚐試更新一個空map。
由於url.Values是一個map類型併且間接引用了其key/value對因此url.Values.Add對這個map里的元素做任何的更新、刪除操作對調用方都是可見的。實際上就像在普通函數中一樣雖然可以通過引用來操作內部值但在方法想要脩改引用本身是不會影響原始值的比如把他置爲nil或者讓這個引用指向了其它的對象調用方都不會受影響。(譯註因爲傳入的是存儲了內存地址的變量你改變這個變量是影響不了原始的變量的想想C語言是差不多的)
由於url.Values是一個map類型併且間接引用了其key/value對因此url.Values.Add對這個map里的元素做任何的更新、刪除操作對調用方都是可見的。實際上就像在普通函數中一樣雖然可以通過引用來操作內部值但在方法想要脩改引用本身是不會影響原始值的比如把他置爲nil或者讓這個引用指向了其它的對象調用方都不會受影響。譯註因爲傳入的是存儲了內存地址的變量你改變這個變量是影響不了原始的變量的想想C語言是差不多的

View File

@ -9,9 +9,9 @@ func (p *Point) ScaleBy(factor float64) {
}
```
這個方法的名字是`(*Point).ScaleBy`。這里的括號是必的;沒有括號的話這個表達式可能會被理解爲`*(Point.ScaleBy)`。
這個方法的名字是`(*Point).ScaleBy`。這里的括號是必的;沒有括號的話這個表達式可能會被理解爲`*(Point.ScaleBy)`。
在現實的程序里一般會約定如果Point這個類有一個指針作爲接收器的方法那麽所有Point的方法都必有一個指針接收器,卽使是那些併不需要這個指針接收器的函數。我們在這里打破了這個約定隻是爲了展示一下兩種方法的異同而已。
在現實的程序里一般會約定如果Point這個類有一個指針作爲接收器的方法那麽所有Point的方法都必有一個指針接收器,卽使是那些併不需要這個指針接收器的函數。我們在這里打破了這個約定隻是爲了展示一下兩種方法的異同而已。
隻有類型(Point)和指向他們的指針(*Point),才是可能會出現在接收器聲明里的兩種接收器。此外,爲了避免歧義,在聲明方法時,如果一個類型名本身是一個指針的話,是不允許其出現在接收器中的,比如下面這個例子:
@ -85,9 +85,9 @@ pptr.Distance(q) // implicit (*pptr)
如果類型T的所有方法都是用T類型自己來做接收器(而不是`*T`)那麽拷貝這種類型的實例就是安全的調用他的任何一個方法也就會産生一個值的拷貝。比如time.Duration的這個類型在調用其方法時就會被全部拷貝一份包括在作爲參數傳入函數的時候。但是如果一個方法使用指針作爲接收器你需要避免對其進行拷貝因爲這樣可能會破壞掉該類型內部的不變性。比如你對bytes.Buffer對象進行了拷貝那麽可能會引起原始對象和拷貝對象隻是别名而已但實際上其指向的對象是一致的。緊接着對拷貝後的變量進行脩改可能會有讓你意外的結果。
譯註:作者這里説的比較繞,其實有兩點:
**譯註:** 作者這里説的比較繞,其實有兩點:
1.不管你的method的receiver是指針類型還是非指針類型都是可以通過指針/非指針類型進行調用的,編譯器會幫你做類型轉換
2.在聲明一個method的receiver該是指針還是非指針類型時你需要考慮兩方面的內部第一方面是這個對象本身是不是特别大如果聲明爲非指針變量時調用會産生一次拷貝第二方面是如果你用指針類型作爲receiver那麽你一定要註意這種指針類型指向的始終是一塊內存地址就算你對其進行了拷貝。熟悉C或者C艹的人這里應該很快能明白。
1. 不管你的method的receiver是指針類型還是非指針類型都是可以通過指針/非指針類型進行調用的,編譯器會幫你做類型轉換
2. 在聲明一個method的receiver該是指針還是非指針類型時你需要考慮兩方面的內部第一方面是這個對象本身是不是特别大如果聲明爲非指針變量時調用會産生一次拷貝第二方面是如果你用指針類型作爲receiver那麽你一定要註意這種指針類型指向的始終是一塊內存地址就算你對其進行了拷貝。熟悉C或者C艹的人這里應該很快能明白。
{% include "./ch6-02-1.md" %}

View File

@ -37,7 +37,7 @@ fmt.Println(p.Distance(q.Point)) // "10"
Point類的方法也被引入了ColoredPoint。用這種方式內嵌可以使我們定義字段特别多的複雜類型我們可以將字段先按小類型分組然後定義小類型的方法之後再把它們組合起來。
讀者如果對基於類來實現面向對象的語言比較熟悉的話可能會傾向於將Point看作一個基類而ColoredPoint看作其子類或者繼承類或者將ColoredPoint看作"is a" Point類型。但這是錯誤的理解。請註意上面例子中對Distance方法的調用。Distance有一個參數是Point類型但q併不是一個Point類所以盡管q有着Point這個內嵌類型我們也必要顯式地選擇它。嚐試直接傳q的話你會看到下面這樣的錯誤
讀者如果對基於類來實現面向對象的語言比較熟悉的話可能會傾向於將Point看作一個基類而ColoredPoint看作其子類或者繼承類或者將ColoredPoint看作"is a" Point類型。但這是錯誤的理解。請註意上面例子中對Distance方法的調用。Distance有一個參數是Point類型但q併不是一個Point類所以盡管q有着Point這個內嵌類型我們也必要顯式地選擇它。嚐試直接傳q的話你會看到下面這樣的錯誤
```go
p.Distance(q) // compile error: cannot use q (ColoredPoint) as Point

View File

@ -34,7 +34,7 @@ time.AfterFunc(10 * time.Second, r.Launch)
譯註:省掉了上面那個例子里的匿名函數。
和方法"值"相關的還有方法表達式。當調用一個方法時,與調用一個普通的函數相比,我們必要用選擇器(p.Distance)語法來指定方法的接收器。
和方法"值"相關的還有方法表達式。當調用一個方法時,與調用一個普通的函數相比,我們必要用選擇器(p.Distance)語法來指定方法的接收器。
當T是一個類型時方法表達式可能會寫作T.f或者(*T).f會返迴一個函數"值",這種函數會將其第一個參數用作接收器,所以可以用通常(譯註:不寫選擇器)的方式來對其進行調用:

View File

@ -105,9 +105,9 @@ func (*IntSet) Clear() // remove all elements from the set
func (*IntSet) Copy() *IntSet // return a copy of the set
```
練習6.2: 定義一個變參方法(*IntSet).AddAll(...int)這個方法可以爲一組IntSet值求和比如s.AddAll(1,2,3)。
**練習 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)
**練習 6.5** 我們這章定義的IntSet里的每個字都是用的uint64類型但是64位的數值可能在32位的平台上不高效。脩改程序使其使用uint類型這種類型對於32位平台來説更合適。當然了這里我們可以不用簡單粗暴地除64可以定義一個常量來決定是用32還是64這里你可能會用到平台的自動判斷的一個智能表達式32 << (^uint(0) >> 63)

View File

@ -2,7 +2,7 @@
一個對象的變量或者方法如果對調用方是不可見的話,一般就被定義爲“封裝”。封裝有時候也被叫做信息隱藏,同時也是面向對象編程最關鍵的一個方面。
Go語言隻有一種控製可見性的手段大寫首字母的標識符會從定義它們的包中被導出小寫字母的則不會。這種限製包內成員的方式同樣適用於struct或者一個類型的方法。因而如果我們想要封裝一個對象我們必將其定義爲一個struct。
Go語言隻有一種控製可見性的手段大寫首字母的標識符會從定義它們的包中被導出小寫字母的則不會。這種限製包內成員的方式同樣適用於struct或者一個類型的方法。因而如果我們想要封裝一個對象我們必將其定義爲一個struct。
這也就是前面的小節中IntSet被定義爲struct類型的原因盡管它隻有一個字段

View File

@ -43,7 +43,7 @@ io.Writer類型定義了函數Fprintf和這個函數調用者之間的約定。
因爲fmt.Fprintf函數沒有對具體操作的值做任何假設而是僅僅通過io.Writer接口的約定來保證行爲所以第一個參數可以安全地傳入一個任何具體類型的值隻需要滿足io.Writer接口。一個類型可以自由的使用另一個滿足相同接口的類型來進行替換被稱作可替換性(LSP里氏替換)。這是一個面向對象的特徵。
讓我們通過一個新的類型來進行校驗,下面\*ByteCounter類型里的Write方法僅僅在丟失寫向它的字節前統計它們的長度。(在這個+=賦值語句中讓len(p)的類型和\*c的類型匹配的轉換是必的。)
讓我們通過一個新的類型來進行校驗,下面\*ByteCounter類型里的Write方法僅僅在丟失寫向它的字節前統計它們的長度。(在這個+=賦值語句中讓len(p)的類型和\*c的類型匹配的轉換是必的。)
```go
// gopl.io/ch7/bytecounter
@ -79,12 +79,12 @@ type Stringer interface {
我們會在7.10節解釋fmt包怎麽發現哪些值是滿足這個接口類型的。
練習7.1:使用來自ByteCounter的思路實現一個針對對單詞和行數的計數器。你會發現bufio.ScanWords非常的有用。
**練習 7.1** 使用來自ByteCounter的思路實現一個針對對單詞和行數的計數器。你會發現bufio.ScanWords非常的有用。
練習7.2:寫一個帶有如下函數籤名的函數CountingWriter傳入一個io.Writer接口類型返迴一個新的Writer類型把原來的Writer封裝在里面和一個表示寫入新的Writer字節數的int64類型指針
**練習 7.2** 寫一個帶有如下函數籤名的函數CountingWriter傳入一個io.Writer接口類型返迴一個新的Writer類型把原來的Writer封裝在里面和一個表示寫入新的Writer字節數的int64類型指針
```go
func CountingWriter(w io.Writer) (io.Writer, *int64)
```
練習7.3:爲在gopl.io/ch4/treesort (§4.4)的*tree類型實現一個String方法去展示tree類型的值序列。
**練習 7.3** 爲在gopl.io/ch4/treesort (§4.4)的*tree類型實現一個String方法去展示tree類型的值序列。

View File

@ -46,9 +46,9 @@ type ReadWriter interface {
上面3種定義方式都是一樣的效果。方法的順序變化也沒有影響唯一重要的就是這個集合里面的方法。
練習7.4:strings.NewReader函數通過讀取一個string參數返迴一個滿足io.Reader接口類型的值和其它值。實現一個簡單版本的NewReader併用它來構造一個接收字符串輸入的HTML解析器§5.2
**練習 7.4** strings.NewReader函數通過讀取一個string參數返迴一個滿足io.Reader接口類型的值和其它值。實現一個簡單版本的NewReader併用它來構造一個接收字符串輸入的HTML解析器§5.2
練習7.5:io包里面的LimitReader函數接收一個io.Reader接口類型的r和字節數n併且返迴另一個從r中讀取字節但是當讀完n個字節後就表示讀到文件結束的Reader。實現這個LimitReader函數
**練習 7.5** io包里面的LimitReader函數接收一個io.Reader接口類型的r和字節數n併且返迴另一個從r中讀取字節但是當讀完n個字節後就表示讀到文件結束的Reader。實現這個LimitReader函數
```go
func LimitReader(r io.Reader, n int64) io.Reader

View File

@ -1,5 +1,5 @@
## 7.3. 實現接口的條件
一個類型如果擁有一個接口需要的所有方法,那麽這個類型就實現了這個接口。例如,*os.File類型實現了io.ReaderWriterCloser和ReadWriter接口。*bytes.Buffer實現了ReaderWriter和ReadWriter這些接口但是它沒有實現Closer接口因爲它不具有Close方法。Go的程序員經常會簡要的把一個具體的類型描述成一個特定的接口類型。舉個例子*bytes.Buffer是io.Writer*os.Files是io.ReadWriter。
一個類型如果擁有一個接口需要的所有方法,那麽這個類型就實現了這個接口。例如,\*os.File類型實現了io.ReaderWriterCloser和ReadWriter接口。\*bytes.Buffer實現了ReaderWriter和ReadWriter這些接口但是它沒有實現Closer接口因爲它不具有Close方法。Go的程序員經常會簡要的把一個具體的類型描述成一個特定的接口類型。舉個例子\*bytes.Buffer是io.Writer\*os.Files是io.ReadWriter。
接口指定的規則非常簡單:表達一個類型屬於某個接口隻要這個類型實現這個接口。所以:
@ -13,34 +13,44 @@ var rwc io.ReadWriteCloser
rwc = os.Stdout // OK: *os.File has Read, Write, Close methods
rwc = new(bytes.Buffer) // compile error: *bytes.Buffer lacks Close method
```
這個規則甚至適用於等式右邊本身也是一個接口類型
```go
w = rwc // OK: io.ReadWriteCloser has Write method
rwc = w // compile error: io.Writer lacks Close method
```
因爲ReadWriter和ReadWriteCloser包含所有Writer的方法所以任何實現了ReadWriter和ReadWriteCloser的類型必定也實現了Writer接口
在進一步學習前,必先解釋表示一個類型持有一個方法當中的細節。迴想在6.2章中對於每一個命名過的具體類型T它一些方法的接收者是類型T本身然而另一些則是一個*T的指針。還記得在T類型的參數上調用一個*T的方法是合法的隻要這個參數是一個變量編譯器隱式的獲取了它的地址。但這僅僅是一個語法T類型的值不擁有所有*T指針的方法那這樣它就可能隻實現更少的接口。
在進一步學習前,必先解釋表示一個類型持有一個方法當中的細節。迴想在6.2章中對於每一個命名過的具體類型T它一些方法的接收者是類型T本身然而另一些則是一個*T的指針。還記得在T類型的參數上調用一個*T的方法是合法的隻要這個參數是一個變量編譯器隱式的獲取了它的地址。但這僅僅是一個語法T類型的值不擁有所有*T指針的方法那這樣它就可能隻實現更少的接口。
舉個例子可能會更清晰一點。在第6.5章中IntSet類型的String方法的接收者是一個指針類型所以我們不能在一個不能尋址的IntSet值上調用這個方法
```go
type IntSet struct { /* ... */ }
func (*IntSet) String() string
var _ = IntSet{}.String() // compile error: String requires *IntSet receiver
```
但是我們可以在一個IntSet值上調用這個方法
```go
var s IntSet
var _ = s.String() // OK: s is a variable and &s has a String method
```
然而,由於隻有*IntSet類型有String方法所有也隻有*IntSet類型實現了fmt.Stringer接口
```go
var _ fmt.Stringer = &s // OK
var _ fmt.Stringer = s // compile error: IntSet lacks String method
```
12.8章包含了一個打印出任意值的所有方法的程序然後可以使用godoc -analysis=type tool(§10.7.4)展示每個類型的方法和具體類型和接口之間的關繫
就像信封封裝和隱藏信件起來一樣,接口類型封裝和隱藏具體類型和它的值。卽使具體類型有其它的方法也隻有接口類型暴露出來的方法會被調用到:
```go
os.Stdout.Write([]byte("hello")) // OK: *os.File has Write method
os.Stdout.Close() // OK: *os.File has Close method
@ -50,9 +60,11 @@ w = os.Stdout
w.Write([]byte("hello")) // OK: io.Writer has Write method
w.Close() // compile error: io.Writer lacks Close method
```
一個有更多方法的接口類型比如io.ReadWriter和少一些方法的接口類型,例如io.Reader進行對比更多方法的接口類型會告訴我們更多關於它的值持有的信息併且對實現它的類型要求更加嚴格。那麽關於interface{}類型,它沒有任何方法,請講出哪些具體的類型實現了它?
這看上去好像沒有用但實際上interface{}被稱爲空接口類型是不可或缺的。因爲空接口類型對實現它的類型沒有要求,所以我們可以將任意一個值賦給空接口類型。
```go
var any interface{}
any = true
@ -61,26 +73,32 @@ any = "hello"
any = map[string]int{"one": 1}
any = new(bytes.Buffer)
```
盡管不是很明顯從本書最早的的例子中我們就已經在使用空接口類型。它允許像fmt.Println或者5.7章中的errorf函數接受任何類型的參數。
對於創建的一個interface{}值持有一個booleanfloatstringmappointer或者任意其它的類型我們當然不能直接對它持有的值做操作因爲interface{}沒有任何方法。我們會在7.10章中學到一種用類型斷言來獲取interface{}中值的方法。
因爲接口實現隻依賴於判斷的兩個類型的方法,所以沒有必要定義一個具體類型和它實現的接口之間的關繫。也就是説,嚐試文檔化和斷言這種關繫幾乎沒有用,所以併沒有通過程序強製定義。下面的定義在編譯期斷言一個*bytes.Buffer的值實現了io.Writer接口類型:
```go
// *bytes.Buffer must satisfy io.Writer
var w io.Writer = new(bytes.Buffer)
```
因爲任意*bytes.Buffer的值甚至包括nil通過(*bytes.Buffer)(nil)進行顯示的轉換都實現了這個接口所以我們不必分配一個新的變量。併且因爲我們絶不會引用變量w我們可以使用空標識符來來進行代替。總的看這些變化可以讓我們得到一個更樸素的版本
```go
// *bytes.Buffer must satisfy io.Writer
var _ io.Writer = (*bytes.Buffer)(nil)
```
非空的接口類型比如io.Writer經常被指針類型實現尤其當一個或多個接口方法像Write方法那樣隱式的給接收者帶來變化的時候。一個結構體的指針是非常常見的承載方法的類型。
但是併不意味着隻有指針類型滿足接口類型甚至連一些有設置方法的接口類型也可能會被Go語言中其它的引用類型實現。我們已經看過slice類型的方法(geometry.Path, §6.1)和map類型的方法(url.Values, §6.2.1),後面還會看到函數類型的方法的例子(http.HandlerFunc, §7.7)。甚至基本的類型也可能會實現一些接口就如我們在7.4章中看到的time.Duration類型實現了fmt.Stringer接口。
一個具體的類型可能實現了很多不相關的接口。考慮在一個組織出售數字文化産品比如音樂,電影和書籍的程序中可能定義了下列的具體類型:
``` go
```
Album
Book
Movie
@ -89,7 +107,9 @@ Podcast
TVEpisode
Track
```
我們可以把每個抽象的特點用接口來表示。一些特性對於所有的這些文化産品都是共通的,例如標題,創作日期和作者列表。
```go
type Artifact interface {
Title() string
@ -98,6 +118,7 @@ type Artifact interface {
}
```
其它的一些特性隻對特定類型的文化産品才有。和文字排版特性相關的隻有books和magazines還有隻有movies和TV劇集和屏幕分辨率相關。
```go
type Text interface {
Pages() int
@ -116,7 +137,9 @@ type Video interface {
Resolution() (x, y int)
}
```
這些接口不止是一種有用的方式來分組相關的具體類型和表示他們之間的共同特定。我們後面可能會發現其它的分組。舉例如果我們發現我們需要以同樣的方式處理Audio和Video我們可以定義一個Streamer接口來代表它們之間相同的部分而不必對已經存在的類型做改變。
```go
type Streamer interface {
Stream() (io.ReadCloser, error)
@ -124,4 +147,5 @@ type Streamer interface {
Format() string
}
```
每一個具體類型的組基於它們相同的行爲可以表示成一個接口類型。不像基於類的語言他們一個類實現的接口集合需要進行顯式的定義在Go語言中我們可以在需要的時候定義一個新的抽象或者特定特點的組而不需要脩改具體類型的定義。當具體的類型來自不同的作者時這種方式會特别有用。當然也確實沒有必要在具體的類型中指出這些共性。

View File

@ -72,9 +72,9 @@ func (f *celsiusFlag) Set(s string) error {
}
```
調用fmt.Sscanf函數從輸入s中解析一個浮點數value和一個字符串unit。雖然通常必檢査Sscanf的錯誤返迴但是在這個例子中我們不需要因爲如果有錯誤發生就沒有switch case會匹配到。
調用fmt.Sscanf函數從輸入s中解析一個浮點數value和一個字符串unit。雖然通常必檢査Sscanf的錯誤返迴但是在這個例子中我們不需要因爲如果有錯誤發生就沒有switch case會匹配到。
下面的CelsiusFlag函數將所有邏輯都封裝在一起。它返迴一個內嵌在celsiusFlag變量f中的Celsius指針給調用者。Celsius字段是一個會通過Set方法在標記處理的過程中更新的變量。調用Var方法將標記加入應用的命令行標記集合中有異常複雜命令行接口的全局變量flag.CommandLine.Programs可能有幾個這個類型的變量。調用Var方法將一個*celsiusFlag參數賦值給一個flag.Value參數,導致編譯器去檢査*celsiusFlag是否有必的方法。
下面的CelsiusFlag函數將所有邏輯都封裝在一起。它返迴一個內嵌在celsiusFlag變量f中的Celsius指針給調用者。Celsius字段是一個會通過Set方法在標記處理的過程中更新的變量。調用Var方法將標記加入應用的命令行標記集合中有異常複雜命令行接口的全局變量flag.CommandLine.Programs可能有幾個這個類型的變量。調用Var方法將一個*celsiusFlag參數賦值給一個flag.Value參數,導致編譯器去檢査*celsiusFlag是否有必的方法。
```go
// CelsiusFlag defines a Celsius flag with the specified name,

View File

@ -42,7 +42,7 @@ w = os.Stdout
w.Write([]byte("hello")) // "hello"
```
通常在編譯期,我們不知道接口值的動態類型是什麽,所以一個接口上的調用必鬚使用動態分配。因爲不是直接進行調用,所以編譯器必鬚把代碼生成在類型描述符的方法Write上然後間接調用那個地址。這個調用的接收者是一個接口動態值的拷貝os.Stdout。效果和下面這個直接調用一樣
通常在編譯期,我們不知道接口值的動態類型是什麽,所以一個接口上的調用必須使用動態分配。因爲不是直接進行調用,所以編譯器必須把代碼生成在類型描述符的方法Write上然後間接調用那個地址。這個調用的接收者是一個接口動態值的拷貝os.Stdout。效果和下面這個直接調用一樣
```go
os.Stdout.Write([]byte("hello")) // "hello"
@ -93,7 +93,7 @@ var x interface{} = []int{1, 2, 3}
fmt.Println(x == x) // panic: comparing uncomparable type []int
```
考慮到這點,接口類型是非常與衆不同的。其它類型要麽是安全的可比較類型(如基本類型和指針)要麽是完全不可比較的類型(如切片,映射類型,和函數),但是在比較接口值或者包含了接口值的聚合類型時,我們必要意識到潛在的panic。同樣的風險也存在於使用接口作爲map的鍵或者switch的操作數。隻能比較你非常確定它們的動態值是可比較類型的接口值。
考慮到這點,接口類型是非常與衆不同的。其它類型要麽是安全的可比較類型(如基本類型和指針)要麽是完全不可比較的類型(如切片,映射類型,和函數),但是在比較接口值或者包含了接口值的聚合類型時,我們必要意識到潛在的panic。同樣的風險也存在於使用接口作爲map的鍵或者switch的操作數。隻能比較你非常確定它們的動態值是可比較類型的接口值。
當我們處理錯誤或者調試的過程中得知接口值的動態類型是非常有幫助的。所以我們使用fmt包的%T動作:

View File

@ -80,7 +80,7 @@ func printTracks(tracks []*Track) {
}
```
爲了能按照Artist字段對播放列表進行排序我們會像對StringSlice那樣定義一個新的帶有必LenLess和Swap方法的切片類型。
爲了能按照Artist字段對播放列表進行排序我們會像對StringSlice那樣定義一個新的帶有必LenLess和Swap方法的切片類型。
```go
type byArtist []*Track
@ -89,7 +89,7 @@ func (x byArtist) Less(i, j int) bool { return x[i].Artist < x[j].Artist }
func (x byArtist) Swap(i, j int) { x[i], x[j] = x[j], x[i] }
```
爲了調用通用的排序程序,我們必先將tracks轉換爲新的byArtist類型它定義了具體的排序
爲了調用通用的排序程序,我們必先將tracks轉換爲新的byArtist類型它定義了具體的排序
```go
sort.Sort(byArtist(tracks))
@ -137,7 +137,7 @@ func Reverse(data Interface) Interface { return reverse{data} }
reverse的另外兩個方法Len和Swap隱式地由原有內嵌的sort.Interface提供。因爲reverse是一個不公開的類型所以導出函數Reverse函數返迴一個包含原有sort.Interface值的reverse類型實例。
爲了可以按照不同的列進行排序,我們必定義一個新的類型例如byYear
爲了可以按照不同的列進行排序,我們必定義一個新的類型例如byYear
```go
type byYear []*Track

View File

@ -1,3 +1,198 @@
## 7.7. http.Handler接口
TODO
在第一章中我們粗略的了解了怎麽用net/http包去實現網絡客戶端(§1.5)和服務器(§1.7)。在這個小節中我們會對那些基於http.Handler接口的服務器API做更進一步的學習
```go
net/http
package http
type Handler interface {
ServeHTTP(w ResponseWriter, r *Request)
}
func ListenAndServe(address string, h Handler) error
```
ListenAndServe函數需要一個例如“localhost:8000”的服務器地址和一個所有請求都可以分派的Handler接口實例。它會一直運行直到這個服務因爲一個錯誤而失敗或者啟動失敗它的返迴值一定是一個非空的錯誤。
想象一個電子商務網站爲了銷售它的數據庫將它物品的價格映射成美元。下面這個程序可能是能想到的最簡單的實現了。它將庫存清單模型化爲一個命名爲database的map類型我們給這個類型一個ServeHttp方法這樣它可以滿足http.Handler接口。這個handler會遍歷整個map併輸出物品信息。
```go
gopl.io/ch7/http1
func main() {
db := database{"shoes": 50, "socks": 5}
log.Fatal(http.ListenAndServe("localhost:8000", db))
}
type dollars float32
func (d dollars) String() string { return fmt.Sprintf("$%.2f", d) }
type database map[string]dollars
func (db database) ServeHTTP(w http.ResponseWriter, req *http.Request) {
for item, price := range db {
fmt.Fprintf(w, "%s: %s\n", item, price)
}
}
```
如果我們啟動這個服務,
```
$ go build gopl.io/ch7/http1
$ ./http1 &
```
然後用1.5節中的獲取程序如果你更喜歡可以使用web瀏覽器來連接服務器,我們得到下面的輸出:
```
$ go build gopl.io/ch1/fetch
$ ./fetch http://localhost:8000
shoes: $50.00
socks: $5.00
```
目前爲止這個服務器不考慮URL隻能爲每個請求列出它全部的庫存清單。更眞實的服務器會定義多個不同的URL每一個都會觸發一個不同的行爲。讓我們使用/list來調用已經存在的這個行爲併且增加另一個/price調用表明單個貨品的價格像這樣/price?item=socks來指定一個請求參數。
```go
gopl.io/ch7/http2
func (db database) ServeHTTP(w http.ResponseWriter, req *http.Request) {
switch req.URL.Path {
case "/list":
for item, price := range db {
fmt.Fprintf(w, "%s: %s\n", item, price)
}
case "/price":
item := req.URL.Query().Get("item")
price, ok := db[item]
if !ok {
w.WriteHeader(http.StatusNotFound) // 404
fmt.Fprintf(w, "no such item: %q\n", item)
return
}
fmt.Fprintf(w, "%s\n", price)
default:
w.WriteHeader(http.StatusNotFound) // 404
fmt.Fprintf(w, "no such page: %s\n", req.URL)
}
}
```
現在handler基於URL的路徑部分req.URL.Path來決定執行什麽邏輯。如果這個handler不能識别這個路徑它會通過調用w.WriteHeader(http.StatusNotFound)返迴客戶端一個HTTP錯誤這個檢査應該在向w寫入任何值前完成。順便提一下http.ResponseWriter是另一個接口。它在io.Writer上增加了發送HTTP相應頭的方法。等效地我們可以使用實用的http.Error函數
```go
msg := fmt.Sprintf("no such page: %s\n", req.URL)
http.Error(w, msg, http.StatusNotFound) // 404
```
/price的case會調用URL的Query方法來將HTTP請求參數解析爲一個map或者更準確地説一個net/url包中url.Values(§6.2.1)類型的多重映射。然後找到第一個item參數併査找它的價格。如果這個貨品沒有找到會返迴一個錯誤。
這里是一個和新服務器會話的例子:
```
$ go build gopl.io/ch7/http2
$ go build gopl.io/ch1/fetch
$ ./http2 &
$ ./fetch http://localhost:8000/list
shoes: $50.00
socks: $5.00
$ ./fetch http://localhost:8000/price?item=socks
$5.00
$ ./fetch http://localhost:8000/price?item=shoes
$50.00
$ ./fetch http://localhost:8000/price?item=hat
no such item: "hat"
$ ./fetch http://localhost:8000/help
no such page: /help
```
顯然我們可以繼續向ServeHTTP方法中添加case但在一個實際的應用中將每個case中的邏輯定義到一個分開的方法或函數中會很實用。此外相近的URL可能需要相似的邏輯例如幾個圖片文件可能有形如/images/\*.png的URL。因爲這些原因net/http包提供了一個請求多路器ServeMux來簡化URL和handlers的聯繫。一個ServeMux將一批http.Handler聚集到一個單一的http.Handler中。再一次我們可以看到滿足同一接口的不同類型是可替換的web服務器將請求指派給任意的http.Handler
而不需要考慮它後面的具體類型。
對於更複雜的應用一些ServeMux可以通過組合來處理更加錯綜複雜的路由需求。Go語言目前沒有一個權威的web框架就像Ruby語言有Rails和python有Django。這併不是説這樣的框架不存在而是Go語言標準庫中的構建模塊就已經非常靈活以至於這些框架都是不必要的。此外盡管在一個項目早期使用框架是非常方便的但是它們帶來額外的複雜度會使長期的維護更加睏難。
在下面的程序中我們創建一個ServeMux併且使用它將URL和相應處理/list和/price操作的handler聯繫起來這些操作邏輯都已經被分到不同的方法中。然後我門在調用ListenAndServe函數中使用ServeMux最爲主要的handler。
```go
gopl.io/ch7/http3
func main() {
db := database{"shoes": 50, "socks": 5}
mux := http.NewServeMux()
mux.Handle("/list", http.HandlerFunc(db.list))
mux.Handle("/price", http.HandlerFunc(db.price))
log.Fatal(http.ListenAndServe("localhost:8000", mux))
}
type database map[string]dollars
func (db database) list(w http.ResponseWriter, req *http.Request) {
for item, price := range db {
fmt.Fprintf(w, "%s: %s\n", item, price)
}
}
func (db database) price(w http.ResponseWriter, req *http.Request) {
item := req.URL.Query().Get("item")
price, ok := db[item]
if !ok {
w.WriteHeader(http.StatusNotFound) // 404
fmt.Fprintf(w, "no such item: %q\n", item)
return
}
fmt.Fprintf(w, "%s\n", price)
}
```
讓我們關註這兩個註冊到handlers上的調用。第一個db.list是一個方法值 (§6.4),它是下面這個類型的值
```go
func(w http.ResponseWriter, req *http.Request)
```
也就是説db.list的調用會援引一個接收者是db的database.list方法。所以db.list是一個實現了handler類似行爲的函數但是因爲它沒有方法所以它不滿足http.Handler接口併且不能直接傳給mux.Handle。
語句http.HandlerFunc(db.list)是一個轉換而非一個函數調用因爲http.HandlerFunc是一個類型。它有如下的定義
```go
net/http
package http
type HandlerFunc func(w ResponseWriter, r *Request)
func (f HandlerFunc) ServeHTTP(w ResponseWriter, r *Request) {
f(w, r)
}
```
HandlerFunc顯示了在Go語言接口機製中一些不同尋常的特點。這是一個有實現了接口http.Handler方法的函數類型。ServeHTTP方法的行爲調用了它本身的函數。因此HandlerFunc是一個讓函數值滿足一個接口的適配器這里函數和這個接口僅有的方法有相同的函數籤名。實際上這個技巧讓一個單一的類型例如database以多種方式滿足http.Handler接口一種通過它的list方法一種通過它的price方法等等。
因爲handler通過這種方式註冊非常普遍ServeMux有一個方便的HandleFunc方法它幫我們簡化handler註冊代碼成這樣
```go
gopl.io/ch7/http3a
mux.HandleFunc("/list", db.list)
mux.HandleFunc("/price", db.price)
```
從上面的代碼很容易看出應該怎麽構建一個程序它有兩個不同的web服務器監聽不同的端口的併且定義不同的URL將它們指派到不同的handler。我們隻要構建另外一個ServeMux併且在調用一次ListenAndServe可能併行的。但是在大多數程序中一個web服務器就足夠了。此外在一個應用程序的多個文件中定義HTTP handler也是非常典型的如果它們必須全部都顯示的註冊到這個應用的ServeMux實例上會比較麻煩。
所以爲了方便net/http包提供了一個全局的ServeMux實例DefaultServerMux和包級别的http.Handle和http.HandleFunc函數。現在爲了使用DefaultServeMux作爲服務器的主handler我們不需要將它傳給ListenAndServe函數nil值就可以工作。
然後服務器的主函數可以簡化成:
```go
gopl.io/ch7/http4
func main() {
db := database{"shoes": 50, "socks": 5}
http.HandleFunc("/list", db.list)
http.HandleFunc("/price", db.price)
log.Fatal(http.ListenAndServe("localhost:8000", nil))
}
```
最後一個重要的提示就像我們在1.7節中提到的web服務器在一個新的協程中調用每一個handler所以當handler獲取其它協程或者這個handler本身的其它請求也可以訪問的變量時一定要使用預防措施比如鎖機製。我們後面的兩章中講到併發相關的知識。
**練習 7.11** 增加額外的handler讓客服端可以創建讀取更新和刪除數據庫記録。例如一個形如 `/update?item=socks&price=6` 的請求會更新庫存清單里一個貨品的價格併且當這個貨品不存在或價格無效時返迴一個錯誤值。(註意:這個脩改會引入變量同時更新的問題)
**練習 7.12** 脩改/list的handler讓它把輸出打印成一個HTML的表格而不是文本。html/template包(§4.6)可能會對你有幫助。

View File

@ -1,3 +1,75 @@
## 7.8. error接口
TODO
從本書的開始我們就已經創建和使用過神祕的預定義error類型而且沒有解釋它究竟是什麽。實際上它就是interface類型這個類型有一個返迴錯誤信息的單一方法
```go
type error interface {
Error() string
}
```
創建一個error最簡單的方法就是調用errors.New函數它會根據傳入的錯誤信息返迴一個新的error。整個errors包僅隻有4行
```go
package errors
func New(text string) error { return &errorString{text} }
type errorString struct { text string }
func (e *errorString) Error() string { return e.text }
```
承載errorString的類型是一個結構體而非一個字符串這是爲了保護它表示的錯誤避免粗心或有意的更新。併且因爲是指針類型*errorString滿足error接口而非errorString類型所以每個New函數的調用都分配了一個獨特的和其他錯誤不相同的實例。我們也不想要重要的error例如io.EOF和一個剛好有相同錯誤消息的error比較後相等。
```go
fmt.Println(errors.New("EOF") == errors.New("EOF")) // "false"
```
調用errors.New函數是非常稀少的因爲有一個方便的封裝函數fmt.Errorf它還會處理字符串格式化。我們曾多次在第5章中用到它。
```go
package fmt
import "errors"
func Errorf(format string, args ...interface{}) error {
return errors.New(Sprintf(format, args...))
}
```
雖然*errorString可能是最簡單的錯誤類型但遠非隻有它一個。例如syscall包提供了Go語言底層繫統調用API。在多個平台上它定義一個實現error接口的數字類型Errno併且在Unix平台上Errno的Error方法會從一個字符串表中査找錯誤消息如下面展示的這樣
```go
package syscall
type Errno uintptr // operating system error code
var errors = [...]string{
1: "operation not permitted", // EPERM
2: "no such file or directory", // ENOENT
3: "no such process", // ESRCH
// ...
}
func (e Errno) Error() string {
if 0 <= int(e) && int(e) < len(errors) {
return errors[e]
}
return fmt.Sprintf("errno %d", e)
}
```
下面的語句創建了一個持有Errno值爲2的接口值表示POSIX ENOENT狀況
```go
var err error = syscall.Errno(2)
fmt.Println(err.Error()) // "no such file or directory"
fmt.Println(err) // "no such file or directory"
```
err的值圖形化的呈現在圖7.6中。
![](../images/ch7-06.png)
Errno是一個繫統調用錯誤的高效表示方式它通過一個有限的集合進行描述併且它滿足標準的錯誤接口。我們會在第7.11節了解到其它滿足這個接口的類型。

View File

@ -1,3 +1,310 @@
## 7.9. 示例: 表達式求值
TODO
在本節中我們會構建一個簡單算術表達式的求值器。我們將使用一個接口Expr來表示Go語言中任意的表達式。現在這個接口不需要有方法但是我們後面會爲它增加一些。
```go
// An Expr is an arithmetic expression.
type Expr interface{}
```
我們的表達式語言由浮點數符號(小數點);二元操作符+-\* 和/;一元操作符-x和+x調用pow(x,y)sin(x)和sqrt(x)的函數例如x和pi的變量當然也有括號和標準的優先級運算符。所有的值都是float64類型。這下面是一些表達式的例子
```go
sqrt(A / pi)
pow(x, 3) + pow(y, 3)
(F - 32) * 5 / 9
```
下面的五個具體類型表示了具體的表達式類型。Var類型表示對一個變量的引用。我們很快會知道爲什麽它可以被輸出。literal類型表示一個浮點型常量。unary和binary類型表示有一到兩個運算對象的運算符表達式這些操作數可以是任意的Expr類型。call類型表示對一個函數的調用我們限製它的fn字段隻能是powsin或者sqrt。
```go
gopl.io/ch7/eval
// A Var identifies a variable, e.g., x.
type Var string
// A literal is a numeric constant, e.g., 3.141.
type literal float64
// A unary represents a unary operator expression, e.g., -x.
type unary struct {
op rune // one of '+', '-'
x Expr
}
// A binary represents a binary operator expression, e.g., x+y.
type binary struct {
op rune // one of '+', '-', '*', '/'
x, y Expr
}
// A call represents a function call expression, e.g., sin(x).
type call struct {
fn string // one of "pow", "sin", "sqrt"
args []Expr
}
```
爲了計算一個包含變量的表達式我們需要一個environment變量將變量的名字映射成對應的值
```go
type Env map[Var]float64
```
我們也需要每個表示式去定義一個Eval方法這個方法會根據給定的environment變量返迴表達式的值。因爲每個表達式都必須提供這個方法我們將它加入到Expr接口中。這個包隻會對外公開ExprEnv和Var類型。調用方不需要獲取其它的表達式類型就可以使用這個求值器。
```go
type Expr interface {
// Eval returns the value of this Expr in the environment env.
Eval(env Env) float64
}
```
下面給大家展示一個具體的Eval方法。Var類型的這個方法對一個environment變量進行査找如果這個變量沒有在environment中定義過這個方法會返迴一個零值literal類型的這個方法簡單的返迴它眞實的值。
```go
func (v Var) Eval(env Env) float64 {
return env[v]
}
func (l literal) Eval(_ Env) float64 {
return float64(l)
}
```
unary和binary的Eval方法會遞歸的計算它的運算對象然後將運算符op作用到它們上。我們不將被零或無窮數除作爲一個錯誤因爲它們都會産生一個固定的結果無限。最後call的這個方法會計算對於powsin或者sqrt函數的參數值然後調用對應在math包中的函數。
```go
func (u unary) Eval(env Env) float64 {
switch u.op {
case '+':
return +u.x.Eval(env)
case '-':
return -u.x.Eval(env)
}
panic(fmt.Sprintf("unsupported unary operator: %q", u.op))
}
func (b binary) Eval(env Env) float64 {
switch b.op {
case '+':
return b.x.Eval(env) + b.y.Eval(env)
case '-':
return b.x.Eval(env) - b.y.Eval(env)
case '*':
return b.x.Eval(env) * b.y.Eval(env)
case '/':
return b.x.Eval(env) / b.y.Eval(env)
}
panic(fmt.Sprintf("unsupported binary operator: %q", b.op))
}
func (c call) Eval(env Env) float64 {
switch c.fn {
case "pow":
return math.Pow(c.args[0].Eval(env), c.args[1].Eval(env))
case "sin":
return math.Sin(c.args[0].Eval(env))
case "sqrt":
return math.Sqrt(c.args[0].Eval(env))
}
panic(fmt.Sprintf("unsupported function call: %s", c.fn))
}
```
一些方法會失敗。例如一個call表達式可能未知的函數或者錯誤的參數個數。用一個無效的運算符如!或者<去構建一個unary或者binary表達式也是可能會發生的盡管下面提到的Parse函數不會這樣做。這些錯誤會讓Eval方法panic。其它的錯誤像計算一個沒有在environment變量中出現過的Var隻會讓Eval方法返迴一個錯誤的結果。所有的這些錯誤都可以通過在計算前檢査Expr來發現。這是我們接下來要講的Check方法的工作但是讓我們先測試Eval方法。
下面的TestEval函數是對evaluator的一個測試。它使用了我們會在第11章講解的testing包但是現在知道調用t.Errof會報告一個錯誤就足夠了。這個函數循環遍歷一個表格中的輸入這個表格中定義了三個表達式和針對每個表達式不同的環境變量。第一個表達式根據給定圓的面積A計算它的半徑第二個表達式通過兩個變量x和y計算兩個立方體的體積之和第三個表達式將華氏溫度F轉換成攝氏度。
```go
func TestEval(t *testing.T) {
tests := []struct {
expr string
env Env
want string
}{
{"sqrt(A / pi)", Env{"A": 87616, "pi": math.Pi}, "167"},
{"pow(x, 3) + pow(y, 3)", Env{"x": 12, "y": 1}, "1729"},
{"pow(x, 3) + pow(y, 3)", Env{"x": 9, "y": 10}, "1729"},
{"5 / 9 * (F - 32)", Env{"F": -40}, "-40"},
{"5 / 9 * (F - 32)", Env{"F": 32}, "0"},
{"5 / 9 * (F - 32)", Env{"F": 212}, "100"},
}
var prevExpr string
for _, test := range tests {
// Print expr only when it changes.
if test.expr != prevExpr {
fmt.Printf("\n%s\n", test.expr)
prevExpr = test.expr
}
expr, err := Parse(test.expr)
if err != nil {
t.Error(err) // parse error
continue
}
got := fmt.Sprintf("%.6g", expr.Eval(test.env))
fmt.Printf("\t%v => %s\n", test.env, got)
if got != test.want {
t.Errorf("%s.Eval() in %v = %q, want %q\n",
test.expr, test.env, got, test.want)
}
}
}
```
對於表格中的每一條記録這個測試會解析它的表達式然後在環境變量中計算它輸出結果。這里我們沒有空間來展示Parse函數但是如果你使用go get下載這個包你就可以看到這個函數。
go test(§11.1) 命令會運行一個包的測試用例:
```
$ go test -v gopl.io/ch7/eval
```
這個-v標識可以讓我們看到測試用例打印的輸出正常情況下像這個一樣成功的測試用例會阻止打印結果的輸出。這里是測試用例里fmt.Printf語句的輸出
```
sqrt(A / pi)
map[A:87616 pi:3.141592653589793] => 167
pow(x, 3) + pow(y, 3)
map[x:12 y:1] => 1729
map[x:9 y:10] => 1729
5 / 9 * (F - 32)
map[F:-40] => -40
map[F:32] => 0
map[F:212] => 100
```
幸運的是目前爲止所有的輸入都是適合的格式,但是我們的運氣不可能一直都有。甚至在解釋型語言中,爲了靜態錯誤檢査語法是非常常見的;靜態錯誤就是不用運行程序就可以檢測出來的錯誤。通過將靜態檢査和動態的部分分開,我們可以快速的檢査錯誤併且對於多次檢査隻執行一次而不是每次表達式計算的時候都進行檢査。
讓我們往Expr接口中增加另一個方法。Check方法在一個表達式語義樹檢査出靜態錯誤。我們馬上會説明它的vars參數。
```go
type Expr interface {
Eval(env Env) float64
// Check reports errors in this Expr and adds its Vars to the set.
Check(vars map[Var]bool) error
}
```
具體的Check方法展示在下面。literal和Var類型的計算不可能失敗所以這些類型的Check方法會返迴一個nil值。對於unary和binary的Check方法會首先檢査操作符是否有效然後遞歸的檢査運算單元。相似地對於call的這個方法首先檢査調用的函數是否已知併且有沒有正確個數的參數然後遞歸的檢査每一個參數。
```go
func (v Var) Check(vars map[Var]bool) error {
vars[v] = true
return nil
}
func (literal) Check(vars map[Var]bool) error {
return nil
}
func (u unary) Check(vars map[Var]bool) error {
if !strings.ContainsRune("+-", u.op) {
return fmt.Errorf("unexpected unary op %q", u.op)
}
return u.x.Check(vars)
}
func (b binary) Check(vars map[Var]bool) error {
if !strings.ContainsRune("+-*/", b.op) {
return fmt.Errorf("unexpected binary op %q", b.op)
}
if err := b.x.Check(vars); err != nil {
return err
}
return b.y.Check(vars)
}
func (c call) Check(vars map[Var]bool) error {
arity, ok := numParams[c.fn]
if !ok {
return fmt.Errorf("unknown function %q", c.fn)
}
if len(c.args) != arity {
return fmt.Errorf("call to %s has %d args, want %d",
c.fn, len(c.args), arity)
}
for _, arg := range c.args {
if err := arg.Check(vars); err != nil {
return err
}
}
return nil
}
var numParams = map[string]int{"pow": 2, "sin": 1, "sqrt": 1}
```
我們在兩個組中有選擇地列出有問題的輸入和它們得出的錯誤。Parse函數這里沒有出現會報出一個語法錯誤和Check函數會報出語義錯誤。
```
x % 2 unexpected '%'
math.Pi unexpected '.'
!true unexpected '!'
"hello" unexpected '"'
log(10) unknown function "log"
sqrt(1, 2) call to sqrt has 2 args, want 1
```
Check方法的參數是一個Var類型的集合這個集合聚集從表達式中找到的變量名。爲了保證成功的計算這些變量中的每一個都必須出現在環境變量中。從邏輯上講這個集合就是調用Check方法返迴的結果但是因爲這個方法是遞歸調用的所以對於Check方法填充結果到一個作爲參數傳入的集合中會更加的方便。調用方在初始調用時必須提供一個空的集合。
在第3.2節中我們繪製了一個在編譯器才確定的函數f(x,y)。現在我們可以解析檢査和計算在字符串中的表達式我們可以構建一個在運行時從客戶端接收表達式的web應用併且它會繪製這個函數的表示的麴面。我們可以使用集合vars來檢査表達式是否是一個隻有兩個變量,x和y的函數——實際上是3個因爲我們爲了方便會提供半徑大小r。併且我們會在計算前使用Check方法拒絶有格式問題的表達式這樣我們就不會在下面函數的40000個計算過程100x100個柵格每一個有4個角重複這些檢査。
這個ParseAndCheck函數混合了解析和檢査步驟的過程
```go
gopl.io/ch7/surface
import "gopl.io/ch7/eval"
func parseAndCheck(s string) (eval.Expr, error) {
if s == "" {
return nil, fmt.Errorf("empty expression")
}
expr, err := eval.Parse(s)
if err != nil {
return nil, err
}
vars := make(map[eval.Var]bool)
if err := expr.Check(vars); err != nil {
return nil, err
}
for v := range vars {
if v != "x" && v != "y" && v != "r" {
return nil, fmt.Errorf("undefined variable: %s", v)
}
}
return expr, nil
}
```
爲了編寫這個web應用所有我們需要做的就是下面這個plot函數這個函數有和http.HandlerFunc相似的籤名
```go
func plot(w http.ResponseWriter, r *http.Request) {
r.ParseForm()
expr, err := parseAndCheck(r.Form.Get("expr"))
if err != nil {
http.Error(w, "bad expr: "+err.Error(), http.StatusBadRequest)
return
}
w.Header().Set("Content-Type", "image/svg+xml")
surface(w, func(x, y float64) float64 {
r := math.Hypot(x, y) // distance from (0,0)
return expr.Eval(eval.Env{"x": x, "y": y, "r": r})
})
}
```
![](../images/ch7-07.png)
這個plot函數解析和檢査在HTTP請求中指定的表達式併且用它來創建一個兩個變量的匿名函數。這個匿名函數和來自原來surface-plotting程序中的固定函數f有相同的籤名但是它計算一個用戶提供的表達式。環境變量中定義了xy和半徑r。最後plot調用surface函數它就是gopl.io/ch3/surface中的主要函數脩改後它可以接受plot中的函數和輸出io.Writer作爲參數而不是使用固定的函數f和os.Stdout。圖7.7中顯示了通過程序産生的3個麴面。
**練習 7.13** 爲Expr增加一個String方法來打印美觀的語法樹。當再一次解析的時候檢査它的結果是否生成相同的語法樹。
**練習 7.14** 定義一個新的滿足Expr接口的具體類型併且提供一個新的操作例如對它運算單元中的最小值的計算。因爲Parse函數不會創建這個新類型的實例爲了使用它你可能需要直接構造一個語法樹或者繼承parser接口
**練習 7.15** 編寫一個從標準輸入中讀取一個單一表達式的程序,用戶及時地提供對於任意變量的值,然後在結果環境變量中計算表達式的值。優雅的處理所有遇到的錯誤。
**練習 7.16** 編寫一個基於web的計算器程序。

View File

@ -1,3 +1,57 @@
## 7.10. 類型斷言
TODO
類型斷言是一個使用在接口值上的操作。語法上它看起來像x.(T)被稱爲斷言類型這里x表示一個接口的類型和T表示一個類型。一個類型斷言檢査它操作對象的動態類型是否和斷言的類型匹配。
這里有兩種可能。第一種如果斷言的類型T是一個具體類型然後類型斷言檢査x的動態類型是否和T相同。如果這個檢査成功了類型斷言的結果是x的動態值當然它的類型是T。換句話説具體類型的類型斷言從它的操作對象中獲得具體的值。如果檢査失敗接下來這個操作會拋出panic。例如
```go
var w io.Writer
w = os.Stdout
f := w.(*os.File) // success: f == os.Stdout
c := w.(*bytes.Buffer) // panic: interface holds *os.File, not *bytes.Buffer
```
第二種如果相反斷言的類型T是一個接口類型然後類型斷言檢査是否x的動態類型滿足T。如果這個檢査成功了動態值沒有獲取到這個結果仍然是一個有相同類型和值部分的接口值但是結果有類型T。換句話説對一個接口類型的類型斷言改變了類型的表述方式改變了可以獲取的方法集合通常更大但是它保護了接口值內部的動態類型和值的部分。
在下面的第一個類型斷言後w和rw都持有os.Stdout因此它們每個有一個動態類型*os.File但是變量w是一個io.Writer類型隻對外公開出文件的Write方法然而rw變量也隻公開它的Read方法。
```go
var w io.Writer
w = os.Stdout
rw := w.(io.ReadWriter) // success: *os.File has both Read and Write
w = new(ByteCounter)
rw = w.(io.ReadWriter) // panic: *ByteCounter has no Read method
```
如果斷言操作的對象是一個nil接口值那麽不論被斷言的類型是什麽這個類型斷言都會失敗。我們幾乎不需要對一個更少限製性的接口類型更少的方法集合做斷言因爲它表現的就像賦值操作一樣除了對於nil接口值的情況。
```go
w = rw // io.ReadWriter is assignable to io.Writer
w = rw.(io.Writer) // fails only if rw == nil
```
經常地我們對一個接口值的動態類型是不確定的併且我們更願意去檢驗它是否是一些特定的類型。如果類型斷言出現在一個預期有兩個結果的賦值操作中例如如下的定義這個操作不會在失敗的時候發生panic但是代替地返迴一個額外的第二個結果這個結果是一個標識成功的布爾值
```go
var w io.Writer = os.Stdout
f, ok := w.(*os.File) // success: ok, f == os.Stdout
b, ok := w.(*bytes.Buffer) // failure: !ok, b == nil
```
第二個結果常規地賦值給一個命名爲ok的變量。如果這個操作失敗了那麽ok就是false值第一個結果等於被斷言類型的零值在這個例子中就是一個nil的*bytes.Buffer類型。
這個ok結果經常立卽用於決定程序下面做什麽。if語句的擴展格式讓這個變的很簡潔
```go
if f, ok := w.(*os.File); ok {
// ...use f...
}
```
當類型斷言的操作對象是一個變量,你有時會看見原來的變量名重用而不是聲明一個新的本地變量,這個重用的變量會覆蓋原來的值,如下面這樣:
```go
if w, ok := w.(*os.File); ok {
// ...use w...
}
```

View File

@ -112,7 +112,7 @@ $ killall clock1
killall命令是一個Unix命令行工具可以用給定的進程名來殺掉所有名字匹配的進程。
第二個客戶端必等待第一個客戶端完成工作這樣服務端才能繼續向後執行因爲我們這里的服務器程序同一時間隻能處理一個客戶端連接。我們這里對服務端程序做一點小改動使其支持併發在handleConn函數調用的地方增加go關鍵字讓每一次handleConn的調用都進入一個獨立的goroutine。
第二個客戶端必等待第一個客戶端完成工作這樣服務端才能繼續向後執行因爲我們這里的服務器程序同一時間隻能處理一個客戶端連接。我們這里對服務端程序做一點小改動使其支持併發在handleConn函數調用的地方增加go關鍵字讓每一次handleConn的調用都進入一個獨立的goroutine。
```go
gopl.io/ch8/clock2
@ -147,7 +147,7 @@ $ ./netcat1
$ killall clock2
```
練習8.1: 脩改clock2來支持傳入參數作爲端口號然後寫一個clockwall的程序這個程序可以同時與多個clock服務器通信從多服務器中讀取時間併且在一個表格中一次顯示所有服務傳迴的結果類似於你在某些辦公室里看到的時鐘牆。如果你有地理學上分布式的服務器可以用的話讓這些服務器跑在不同的機器上面或者在同一台機器上跑多個不同的實例這些實例監聽不同的端口假裝自己在不同的時區。像下面這樣
**練習 8.1** 脩改clock2來支持傳入參數作爲端口號然後寫一個clockwall的程序這個程序可以同時與多個clock服務器通信從多服務器中讀取時間併且在一個表格中一次顯示所有服務傳迴的結果類似於你在某些辦公室里看到的時鐘牆。如果你有地理學上分布式的服務器可以用的話讓這些服務器跑在不同的機器上面或者在同一台機器上跑多個不同的實例這些實例監聽不同的端口假裝自己在不同的時區。像下面這樣
```
$ TZ=US/Eastern ./clock2 -port 8010 &
@ -156,4 +156,4 @@ $ TZ=Europe/London ./clock2 -port 8030 &
$ clockwall NewYork=localhost:8010 Tokyo=localhost:8020 London=localhost:8030
```
練習8.2: 實現一個併發FTP服務器。服務器應該解析客戶端來的一些命令比如cd命令來切換目録ls來列出目録內文件get和send來傳輸文件close來關閉連接。你可以用標準的ftp命令來作爲客戶端或者也可以自己實現一個。
**練習 8.2** 實現一個併發FTP服務器。服務器應該解析客戶端來的一些命令比如cd命令來切換目録ls來列出目録內文件get和send來傳輸文件close來關閉連接。你可以用標準的ftp命令來作爲客戶端或者也可以自己實現一個。

View File

@ -72,7 +72,7 @@ func request(hostname string) (response string) { /* ... */ }
關於無緩存或帶緩存channels之間的選擇或者是帶緩存channels的容量大小的選擇都可能影響程序的正確性。無緩存channel更強地保證了每個發送操作與相應的同步接收操作但是對於帶緩存channel這些操作是解耦的。同樣卽使我們知道將要發送到一個channel的信息的數量上限創建一個對應容量大小帶緩存channel也是不現實的因爲這要求在執行任何接收操作之前緩存所有已經發送的值。如果未能分配足夠的緩衝將導致程序死鎖。
Channel的緩存也可能影響程序的性能。想象一家蛋糕店有三個廚師一個烘焙一個上衣,還有一個將每個蛋糕傳遞到它下一個廚師在生産線。在狹小的廚房空間環境,每個廚師在完成蛋糕後必等待下一個廚師已經準備好接受它這類似於在一個無緩存的channel上進行溝通。
Channel的緩存也可能影響程序的性能。想象一家蛋糕店有三個廚師一個烘焙一個上衣,還有一個將每個蛋糕傳遞到它下一個廚師在生産線。在狹小的廚房空間環境,每個廚師在完成蛋糕後必等待下一個廚師已經準備好接受它這類似於在一個無緩存的channel上進行溝通。
如果在每個廚師之間有一個放置一個蛋糕的額外空間那麽每個廚師就可以將一個完成的蛋糕臨時放在那里而馬上進入下一個蛋糕在製作中這類似於將channel的緩存隊列的容量設置爲1。隻要每個廚師的平均工作效率相近那麽其中大部分的傳輸工作將是迅速的個體之間細小的效率差異將在交接過程中瀰補。如果廚師之間有更大的額外空間——也是就更大容量的緩存隊列——將可以在不停止生産線的前提下消除更大的效率波動例如一個廚師可以短暫地休息然後在加快趕上進度而不影響其其他人。

View File

@ -176,14 +176,14 @@ func makeThumbnails6(filenames <-chan string) int64 {
}
```
註意Add和Done方法的不對策。Add是爲計數器加一在worker goroutine開始之前調用而不是在goroutine中否則的話我們沒辦法確定Add是在"closer" goroutine調用Wait之前被調用。併且Add還有一個參數但Done卻沒有任何參數其實它和Add(-1)是等價的。我們使用defer來確保計數器卽使是在出錯的情況下依然能夠正確地被減掉。上面的程序代碼結構是當我們使用併發循環但又不知道迭代次數時很通常而且很地道的寫法。
註意Add和Done方法的不對策。Add是爲計數器加一在worker goroutine開始之前調用而不是在goroutine中否則的話我們沒辦法確定Add是在"closer" goroutine調用Wait之前被調用。併且Add還有一個參數但Done卻沒有任何參數其實它和Add(-1)是等價的。我們使用defer來確保計數器卽使是在出錯的情況下依然能夠正確地被減掉。上面的程序代碼結構是當我們使用併發循環但又不知道迭代次數時很通常而且很地道的寫法。
sizes channel攜帶了每一個文件的大小到main goroutine在main goroutine中使用了range loop來計算總和。觀察一下我們是怎樣創建一個closer goroutine併讓其等待worker們在關閉掉sizes channel之前退出的。兩步操作wait和close是基於sizes的循環的併發。考慮一下另一種方案如果等待操作被放在了main goroutine中在循環之前這樣的話就永遠都不會結束了如果在循環之後那麽又變成了不可達的部分因爲沒有任何東西去關閉這個channel這個循環就永遠都不會終止。
sizes channel攜帶了每一個文件的大小到main goroutine在main goroutine中使用了range loop來計算總和。觀察一下我們是怎樣創建一個closer goroutine併讓其等待worker們在關閉掉sizes channel之前退出的。兩步操作wait和close是基於sizes的循環的併發。考慮一下另一種方案如果等待操作被放在了main goroutine中在循環之前這樣的話就永遠都不會結束了如果在循環之後那麽又變成了不可達的部分因爲沒有任何東西去關閉這個channel這個循環就永遠都不會終止。
圖8.5 表明了makethumbnails6函數中事件的序列。縱列表示goroutine。窄線段代表sleep粗線段代表活動。斜線箭頭代表用來同步兩個goroutine的事件。時間向下流動。註意main goroutine是如何大部分的時間被喚醒執行其range循環等待worker發送值或者closer來關閉channel的。
![](../images/ch8-05.png)
練習8.4: 脩改reverb2服務器在每一個連接中使用sync.WaitGroup來計數活躍的echo goroutine。當計數減爲零時關閉TCP連接的寫入像練習8.3中一樣。驗證一下你的脩改版netcat3客戶端會一直等待所有的併發“喊叫”完成卽使是在標準輸入流已經關閉的情況下。
**練習 8.4** 脩改reverb2服務器在每一個連接中使用sync.WaitGroup來計數活躍的echo goroutine。當計數減爲零時關閉TCP連接的寫入像練習8.3中一樣。驗證一下你的脩改版netcat3客戶端會一直等待所有的併發“喊叫”完成卽使是在標準輸入流已經關閉的情況下。
練習8.5: 使用一個已有的CPU綁定的順序程序比如在3.3節中我們寫的Mandelbrot程序或者3.2節中的3-D surface計算程序併將他們的主循環改爲併發形式使用channel來進行通信。在多核計算機上這個程序得到了多少速度上的改進使用多少個goroutine是最合適的呢
**練習 8.5** 使用一個已有的CPU綁定的順序程序比如在3.3節中我們寫的Mandelbrot程序或者3.2節中的3-D surface計算程序併將他們的主循環改爲併發形式使用channel來進行通信。在多核計算機上這個程序得到了多少速度上的改進使用多少個goroutine是最合適的呢

View File

@ -159,11 +159,10 @@ 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結尾譯註外鏈的應該就不算了。)當然了,出現在頁面里的鏈接你也需要進行一些處理,使其能夠在你的鏡像站點上進行跳轉,而不是指向原始的鏈接。
譯註:
拓展閲讀:
http://marcio.io/2015/07/handling-1-million-requests-per-minute-with-golang/
**譯註:**
拓展閲讀 [Handling 1 Million Requests per Minute with Go](http://marcio.io/2015/07/handling-1-million-requests-per-minute-with-golang/)。

View File

@ -131,4 +131,4 @@ channel的零值是nil。也許會讓你覺得比較奇怪nil的channel有時
這使得我們可以用nil來激活或者禁用case來達成處理其它輸入或輸出事件時超時和取消的邏輯。我們會在下一節中看到一個例子。
練習8.8: 使用select來改造8.3節中的echo服務器爲其增加超時這樣服務器可以在客戶端10秒中沒有任何喊話時自動斷開連接。
**練習 8.8** 使用select來改造8.3節中的echo服務器爲其增加超時這樣服務器可以在客戶端10秒中沒有任何喊話時自動斷開連接。

View File

@ -116,7 +116,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循環。
現在程序會悠閒地爲我們打印更新流:
@ -181,4 +181,4 @@ func dirents(dir string) []os.FileInfo {
這個版本比之前那個快了好幾倍,盡管其具體效率還是和你的運行環境,機器配置相關。
練習8.9: 編寫一個du工具每隔一段時間將root目録下的目録大小計算併顯示出來。
**練習 8.9** 編寫一個du工具每隔一段時間將root目録下的目録大小計算併顯示出來。

View File

@ -83,8 +83,6 @@ func dirents(dir string) []os.FileInfo {
現在當取消發生時所有後台的goroutine都會迅速停止併且主函數會返迴。當然當主函數返迴時一個程序會退出而我們又無法在主函數退出的時候確認其已經釋放了所有的資源(譯註:因爲程序都退出了,你的代碼都沒法執行了)。這里有一個方便的竅門我們可以一用取代掉直接從主函數返迴我們調用一個panic然後runtime會把每一個goroutine的棧dump下來。如果main goroutine是唯一一個剩下的goroutine的話他會清理掉自己的一切資源。但是如果還有其它的goroutine沒有退出他們可能沒辦法被正確地取消掉也有可能被取消但是取消操作會很花時間所以這里的一個調研還是很有必要的。我們用panic來獲取到足夠的信息來驗證我們上面的判斷看看最終到底是什麽樣的情況。
練習8.10: HTTP請求可能會因http.Request結構體中Cancel channel的關閉而取消。脩改8.6節中的web crawler來支持取消http請求。
**練習 8.10** HTTP請求可能會因http.Request結構體中Cancel channel的關閉而取消。脩改8.6節中的web crawler來支持取消http請求。提示http.Get併沒有提供方便地定製一個請求的方法。你可以用http.NewRequest來取而代之設置它的Cancel字段然後用http.DefaultClient.Do(req)來進行這個http請求。
提示: http.Get併沒有提供方便地定製一個請求的方法。你可以用http.NewRequest來取而代之設置它的Cancel字段然後用http.DefaultClient.Do(req)來進行這個http請求。
練習8.11:緊接着8.4.4中的mirroredQuery流程實現一個併發請求url的fetch的變種。當第一個請求返迴時直接取消其它的請求。
**練習 8.11** 緊接着8.4.4中的mirroredQuery流程實現一個併發請求url的fetch的變種。當第一個請求返迴時直接取消其它的請求。

View File

@ -114,7 +114,10 @@ You are 127.0.0.1:64216 127.0.0.1:64216 has arrived
當與n個客戶端保持聊天session時這個程序會有2n+2個併發的goroutine然而這個程序卻併不需要顯式的鎖(§9.2)。clients這個map被限製在了一個獨立的goroutine中broadcaster所以它不能被併發地訪問。多個goroutine共享的變量隻有這些channel和net.Conn的實例兩個東西都是併發安全的。我們會在下一章中更多地解決約束併發安全以及goroutine中共享變量的含義。
練習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.12** 使broadcaster能夠將arrival事件通知當前所有的客戶端。爲了達成這個目的你需要有一個客戶端的集合併且在entering和leaving的channel中記録客戶端的名字。
**練習 8.13** 使聊天服務器能夠斷開空閒的客戶端連接比如最近五分鐘之後沒有發送任何消息的那些客戶端。提示可以在其它goroutine中調用conn.Close()來解除Read調用就像input.Scanner()所做的那樣。
**練習 8.14** 脩改聊天服務器的網絡協議這樣每一個客戶端就可以在entering時可以提供它們的名字。將消息前綴由之前的網絡地址改爲這個名字。
**練習 8.15** 如果一個客戶端沒有及時地讀取數據可能會導致所有的客戶端被阻塞。脩改broadcaster來跳過一條消息而不是等待這個客戶端一直到其準備好寫。或者爲每一個客戶端的消息發出channel建立緩衝區這樣大部分的消息便不會被丟掉broadcaster應該用一個非阻塞的send向這個channel中發消息。

View File

@ -1,18 +1,18 @@
## 9.1. 競爭條件
在一个线性(就是说只有一个goroutine的)的程序中,程序的执行顺序只由程序的逻辑来决定。例如,我们有一段语句序列,第一个在第二个之前(废话)以此类推。在有两个或更多goroutine的程序中每一个goroutine内的语句也是按照既定的顺序去执行的但是一般情况下我们没法去知道分别位于两个goroutine的事件x和y的执行顺序x是在y之前还是之后还是同时发生是没法判断的。当我们能够没有办法自信地确认一个事件是在另一个事件的前面或者后面发生的话就说明x和y这两个事件是并发的。
在一個線性(就是説隻有一個goroutine的)的程序中,程序的執行順序隻由程序的邏輯來決定。例如,我們有一段語句序列,第一個在第二個之前(廢話)以此類推。在有兩個或更多goroutine的程序中每一個goroutine內的語句也是按照旣定的順序去執行的但是一般情況下我們沒法去知道分别位於兩個goroutine的事件x和y的執行順序x是在y之前還是之後還是同時發生是沒法判斷的。當我們能夠沒有辦法自信地確認一個事件是在另一個事件的前面或者後面發生的話就説明x和y這兩個事件是併發的。
虑一下,一个函数在线性程序中可以正确地工作。如果在并发的情况下,这个函数依然可以正确地工作的话,那么我们就说这个函数是并发安全的,并发安全的函数不需要额外的同步工作。我们可以把这个概念概括为一个特定类型的一些方法和操作函数,如果这个类型是并发安全的话,那么所有它的访问方法和操作就都是并发安全的。
慮一下,一個函數在線性程序中可以正確地工作。如果在併發的情況下,這個函數依然可以正確地工作的話,那麽我們就説這個函數是併發安全的,併發安全的函數不需要額外的同步工作。我們可以把這個概念概括爲一個特定類型的一些方法和操作函數,如果這個類型是併發安全的話,那麽所有它的訪問方法和操作就都是併發安全的。
在一个程序中有非并发安全的类型的情况下我们依然可以使这个程序并发安全。确实并发安全的类型是例外而不是规则所以只有当文档中明确地说明了其是并发安全的情况下你才可以并发地去访问它。我们会避免并发访问大多数的类型无论是将变量局限在单一的一个goroutine内还是用互斥条件维持更高级别的不变性都是为了这个目的。我们会在本章中说明这些术语
在一個程序中有非併發安全的類型的情況下我們依然可以使這個程序併發安全。確實併發安全的類型是例外而不是規則所以隻有當文檔中明確地説明了其是併發安全的情況下你才可以併發地去訪問它。我們會避免併發訪問大多數的類型無論是將變量局限在單一的一個goroutine內還是用互斥條件維持更高級别的不變性都是爲了這個目的。我們會在本章中説明這些術語
相反,导出包级别的函数一般情况下都是并发安全的。由于package级的变量没法被限制在单一的gorouine所以修改这些变量“必须”使用互斥条件。
相反,導出包級别的函數一般情況下都是併發安全的。由於package級的變量沒法被限製在單一的gorouine所以脩改這些變量“必須”使用互斥條件。
个函数在并发调用时没法工作的原因太多了,比如死锁(deadlock)、活锁(livelock)和饿死(resource starvation)。我们没有空去讨论所有的问题,这里我们只聚焦在竞争条件上。
個函數在併發調用時沒法工作的原因太多了,比如死鎖(deadlock)、活鎖(livelock)和餓死(resource starvation)。我們沒有空去討論所有的問題,這里我們隻聚焦在競爭條件上。
竞争条件指的是程序在多个goroutine交叉执行操作时没有给出正确的结果。竞争条件是很恶劣的一种场景因为这种问题会一直潜伏在你的程序里然后在非常少见的时候蹦出来或许只是会在很大的负载时才会发生又或许是会在使用了某一个编译器、某一种平台或者某一种架构的时候才会出现。这些使得竞争条件带来的问题非常难以复现而且难以分析诊断
競爭條件指的是程序在多個goroutine交叉執行操作時沒有給出正確的結果。競爭條件是很惡劣的一種場景因爲這種問題會一直潛伏在你的程序里然後在非常少見的時候蹦出來或許隻是會在很大的負載時才會發生又或許是會在使用了某一個編譯器、某一種平台或者某一種架構的時候才會出現。這些使得競爭條件帶來的問題非常難以複現而且難以分析診斷
传统上经常用经济损失来为竞争条件做比喻,所以我们来看一个简单的银行账户程序。
傳統上經常用經濟損失來爲競爭條件做比喻,所以我們來看一個簡單的銀行賬戶程序。
```go
// Package bank implements a bank with only one account.
@ -22,22 +22,22 @@ func Deposit(amount int) { balance = balance + amount }
func Balance() int { return balance }
```
(当然我们也可以把Deposit存款函数写成balance += amount这种形式也是等价的不过长一些的形式解释起来更方便一些。)
(當然我們也可以把Deposit存款函數寫成balance += amount這種形式也是等價的不過長一些的形式解釋起來更方便一些。)
对于这个具体的程序而言我们可以瞅一眼各种存款和查余额的顺序调用都能给出正确的结果。也就是说Balance函数会给出之前的所有存入的额度之和。然而当我们并发地而不是顺序地调用这些函数的话Balance就再也没办法保证结果正确了。考虑一下下面的两个goroutine其代表了一个银行联合账户的两笔交易:
對於這個具體的程序而言我們可以瞅一眼各種存款和査餘額的順序調用都能給出正確的結果。也就是説Balance函數會給出之前的所有存入的額度之和。然而當我們併發地而不是順序地調用這些函數的話Balance就再也沒辦法保證結果正確了。考慮一下下面的兩個goroutine其代表了一個銀行聯合賬戶的兩筆交易:
```go
// Alice:
go func() {
bank.Deposit(200) // A1
fmt.Println("=", bank.Balance()) // A2
bank.Deposit(200) // A1
fmt.Println("=", bank.Balance()) // A2
}()
// Bob:
go bank.Deposit(100) // B
```
Alice存了$200后检查她的余额同时Bob存了$100。因为A1和A2是和B并发执行的我们没法预测他们发生的先后顺序。直观地来看的话我们会认为其执行顺序只有三种可能性“Alice先”“Bob先”以及“Alice/Bob/Alice”交错执行。下面的表格会展示经过每一步骤后balance变量的值。引号里的字符串表示余额单
Alice存了$200後檢査她的餘額同時Bob存了$100。因爲A1和A2是和B併發執行的我們沒法預測他們發生的先後順序。直觀地來看的話我們會認爲其執行順序隻有三種可能性“Alice先”“Bob先”以及“Alice/Bob/Alice”交錯執行。下面的表格會展示經過每一步驟後balance變量的值。引號里的字符串表示餘額單
```
Alice first Bob first Alice/Bob/Alice
@ -47,9 +47,9 @@ A2 "=200" A1 300 B 300
B 300 A2 "=300" A2 "=300"
```
所有情况下最终的余额都是$300。唯一的变数是Alice的余额单是否包含了Bob交易不过无论怎么着客户都不会在意。
所有情況下最終的餘額都是$300。唯一的變數是Alice的餘額單是否包含了Bob交易不過無論怎麽着客戶都不會在意。
但是事实是上面的直觉推断是错误的。第四种可能的结果是事实存在的这种情况下Bob的存款会在Alice存款操作中间在余额被读到(balance + amount)之后,在余额被更新之前(balance = ...)这样会导致Bob的交易丢失。而这是因为Alice的存款操作A1实际上是两个操作的一个序列读取然后写可以称之为A1r和A1w。下面是交叉时产生的问题
但是事實是上面的直覺推斷是錯誤的。第四種可能的結果是事實存在的這種情況下Bob的存款會在Alice存款操作中間在餘額被讀到(balance + amount)之後,在餘額被更新之前(balance = ...)這樣會導致Bob的交易丟失。而這是因爲Alice的存款操作A1實際上是兩個操作的一個序列讀取然後寫可以稱之爲A1r和A1w。下面是交叉時産生的問題
```
Data race
@ -60,11 +60,11 @@ A1w 200 balance = ...
A2 "= 200"
```
在A1r之balance + amount会被计算为200所以这是A1w会写入的值并不受其它存款操作的干预。最终的余额是$200。银行的账户上的资产比Bob实际的资产多了$100。(译注因为丢失了Bob的存款操作所以其实是说Bob的钱丢了)
在A1r之balance + amount會被計算爲200所以這是A1w會寫入的值併不受其它存款操作的榦預。最終的餘額是$200。銀行的賬戶上的資産比Bob實際的資産多了$100。(譯註因爲丟失了Bob的存款操作所以其實是説Bob的錢丟了)
这个程序包含了一个特定的竞争条件叫作数据竞争。无论任何时候只要有两个goroutine并发访问同一变量且至少其中的一个是写操作的时候就会发生数据竞争
這個程序包含了一個特定的競爭條件叫作數據競爭。無論任何時候隻要有兩個goroutine併發訪問同一變量且至少其中的一個是寫操作的時候就會發生數據競爭
如果数据竞争的对象是一个比一个机器字(译注32位机器上一个字=4个字节)更大的类型时事情就变得更麻烦了比如interfacestring或者slice类型都是如此。下面的代码会并发地更新两个不同长度的slice
如果數據競爭的對象是一個比一個機器字(譯註32位機器上一個字=4個字節)更大的類型時事情就變得更麻煩了比如interfacestring或者slice類型都是如此。下面的代碼會併發地更新兩個不同長度的slice
```go
var x []int
@ -73,13 +73,13 @@ go func() { x = make([]int, 1000000) }()
x[999999] = 1 // NOTE: undefined behavior; memory corruption possible!
```
后一个语句中的x的值是未定义的其可能是nil或者也可能是一个长度为10的slice也可能是一个程度为1,000,000的slice。但是回忆一下slice的三个组成部分指针(pointer)、长度(length)和容量(capacity)。如果指针是从第一个make调用来而长度从第二个make来x就变成了一个混合体一个自称长度为1,000,000但实际上内部只有10个元素的slice。这样导致的结果是存储999,999元素的位置会碰撞一个遥远的内存位置这种情况下难以对值进行预测而且定位和debug也会变成噩梦。这种语义雷区被称为未定义行为对C程序员来说应该很熟悉幸运的是在Go语言里造成的麻烦要比C里小得多。
後一個語句中的x的值是未定義的其可能是nil或者也可能是一個長度爲10的slice也可能是一個程度爲1,000,000的slice。但是迴憶一下slice的三個組成部分指針(pointer)、長度(length)和容量(capacity)。如果指針是從第一個make調用來而長度從第二個make來x就變成了一個混合體一個自稱長度爲1,000,000但實際上內部隻有10個元素的slice。這樣導致的結果是存儲999,999元素的位置會碰撞一個遙遠的內存位置這種情況下難以對值進行預測而且定位和debug也會變成噩夢。這種語義雷區被稱爲未定義行爲對C程序員來説應該很熟悉幸運的是在Go語言里造成的麻煩要比C里小得多。
尽管并发程序的概念让我们知道并发并不是简单的语句交叉执行。我们将会在9.4节中看到数据竞争可能会有奇怪的结果。许多程序员甚至一些非常聪明的人也还是会偶尔提出一些理由来允许数据竞争比如“互斥条件代价太高”“这个逻辑只是用来做logging”“我不介意丢失一些消息”等等。因为在他们的编译器或者平台上很少遇到问题可能给了他们错误的信心。一个好的经验法则是根本就没有什么所谓的良性数据竞争。所以我们一定要避免数据竞争那么在我们的程序中要如何做到呢?
盡管併發程序的概念讓我們知道併發併不是簡單的語句交叉執行。我們將會在9.4節中看到數據競爭可能會有奇怪的結果。許多程序員甚至一些非常聰明的人也還是會偶爾提出一些理由來允許數據競爭比如“互斥條件代價太高”“這個邏輯隻是用來做logging”“我不介意丟失一些消息”等等。因爲在他們的編譯器或者平台上很少遇到問題可能給了他們錯誤的信心。一個好的經驗法則是根本就沒有什麽所謂的良性數據競爭。所以我們一定要避免數據競爭那麽在我們的程序中要如何做到呢?
们来重复一下数据竞争的定义因为实在太重要了数据竞争会在两个以上的goroutine并发访问相同的变量且至少其中一个为写操作时发生。根据上述定义有三种方式可以避免数据竞争
們來重複一下數據競爭的定義因爲實在太重要了數據競爭會在兩個以上的goroutine併發訪問相同的變量且至少其中一個爲寫操作時發生。根據上述定義有三種方式可以避免數據競爭
第一种方法是不要去写变量。考虑一下下面的map会被“懒”填充也就是说在每个key被第一次请求到的时候才会去填值。如果Icon是被顺序调用的话这个程序会工作很正常但如果Icon被并发调用那么对于这个map来说就会存在数据竞争
第一種方法是不要去寫變量。考慮一下下面的map會被“懶”填充也就是説在每個key被第一次請求到的時候才會去填值。如果Icon是被順序調用的話這個程序會工作很正常但如果Icon被併發調用那麽對於這個map來説就會存在數據競爭
```go
var icons = make(map[string]image.Image)
@ -87,36 +87,36 @@ func loadIcon(name string) image.Image
// NOTE: not concurrency-safe!
func Icon(name string) image.Image {
icon, ok := icons[name]
if !ok {
icon = loadIcon(name)
icons[name] = icon
}
return icon
icon, ok := icons[name]
if !ok {
icon = loadIcon(name)
icons[name] = icon
}
return icon
}
```
反之,如果我们在创建goroutine之前的初始化阶段就初始化了map中的所有条目并且再也不去修改它们那么任意数量的goroutine并发访问Icon都是安全的因为每一个goroutine都只是去读取而已。
反之,如果我們在創建goroutine之前的初始化階段就初始化了map中的所有條目併且再也不去脩改它們那麽任意數量的goroutine併發訪問Icon都是安全的因爲每一個goroutine都隻是去讀取而已。
```go
var icons = map[string]image.Image{
"spades.png": loadIcon("spades.png"),
"hearts.png": loadIcon("hearts.png"),
"diamonds.png": loadIcon("diamonds.png"),
"clubs.png": loadIcon("clubs.png"),
"spades.png": loadIcon("spades.png"),
"hearts.png": loadIcon("hearts.png"),
"diamonds.png": loadIcon("diamonds.png"),
"clubs.png": loadIcon("clubs.png"),
}
// Concurrency-safe.
func Icon(name string) image.Image { return icons[name] }
```
上面的例子里icons变量在包初始化阶段就已经被赋值了包的初始化是在程序main函数开始执行之前就完成了的。只要初始化完成了icons就再也不会修改的或者不变量是本来就并发安全的这种变量不需要进行同步。不过显然我们没法用这种方法因为update操作是必要的操作尤其对于银行账户来说
上面的例子里icons變量在包初始化階段就已經被賦值了包的初始化是在程序main函數開始執行之前就完成了的。隻要初始化完成了icons就再也不會脩改的或者不變量是本來就併發安全的這種變量不需要進行同步。不過顯然我們沒法用這種方法因爲update操作是必要的操作尤其對於銀行賬戶來説
第二种避免数据竞争的方法是避免从多个goroutine访问变量。这也是前一章中大多数程序所采用的方法。例如前面的并发web爬虫(§8.6)的main goroutine是唯一一个能够访问seen map的goroutine而聊天服务器(§8.10)中的broadcaster goroutine是唯一一个能够访问clients map的goroutine。这些变量都被限定在了一个单独的goroutine中。
第二種避免數據競爭的方法是避免從多個goroutine訪問變量。這也是前一章中大多數程序所采用的方法。例如前面的併發web爬蟲(§8.6)的main goroutine是唯一一個能夠訪問seen map的goroutine而聊天服務器(§8.10)中的broadcaster goroutine是唯一一個能夠訪問clients map的goroutine。這些變量都被限定在了一個單獨的goroutine中。
于其它的goroutine不能够直接访问变量它们只能使用一个channel来发送给指定的goroutine请求来查询更新变量。这也就是Go的口头禅“不要使用共享数据来通信使用通信来共享数据”。一个提供对一个指定的变量通过cahnnel来请求的goroutine叫做这个变量的监控(monitor)goroutine。例如broadcaster goroutine会监控(monitor)clients map的全部访问
於其它的goroutine不能夠直接訪問變量它們隻能使用一個channel來發送給指定的goroutine請求來査詢更新變量。這也就是Go的口頭禪“不要使用共享數據來通信使用通信來共享數據”。一個提供對一個指定的變量通過cahnnel來請求的goroutine叫做這個變量的監控(monitor)goroutine。例如broadcaster goroutine會監控(monitor)clients map的全部訪問
下面是一个重写了的银行的例子这个例子中balance变量被限制在了monitor goroutine中名为teller
下面是一個重寫了的銀行的例子這個例子中balance變量被限製在了monitor goroutine中名爲teller
```go
gopl.io/ch9/bank1
@ -130,45 +130,45 @@ func Deposit(amount int) { deposits <- amount }
func Balance() int { return <-balances }
func teller() {
var balance int // balance is confined to teller goroutine
for {
select {
case amount := <-deposits:
balance += amount
case balances <- balance:
}
}
var balance int // balance is confined to teller goroutine
for {
select {
case amount := <-deposits:
balance += amount
case balances <- balance:
}
}
}
func init() {
go teller() // start the monitor goroutine
go teller() // start the monitor goroutine
}
```
即使当一个变量无法在其整个生命周期内被绑定到一个独立的goroutine绑定依然是并发问题的一个解决方案。例如在一条流水线上的goroutine之间共享变量是很普遍的行为在这两者间会通过channel来传输地址信息。如果流水线的每一个阶段都能够避免在将变量传送到下一阶段时再去访问它那么对这个变量的所有访问就是线性的。其效果是变量会被绑定到流水线的一个阶段传送完之后被绑定到下一个以此类推。这种规则有时被称为串行绑定。
卽使當一個變量無法在其整個生命週期內被綁定到一個獨立的goroutine綁定依然是併發問題的一個解決方案。例如在一條流水線上的goroutine之間共享變量是很普遍的行爲在這兩者間會通過channel來傳輸地址信息。如果流水線的每一個階段都能夠避免在將變量傳送到下一階段時再去訪問它那麽對這個變量的所有訪問就是線性的。其效果是變量會被綁定到流水線的一個階段傳送完之後被綁定到下一個以此類推。這種規則有時被稱爲串行綁定。
下面的例子中Cakes会被严格地顺序访问先是baker gorouine然后是icer gorouine
下面的例子中Cakes會被嚴格地順序訪問先是baker gorouine然後是icer gorouine
```go
type Cake struct{ state string }
func baker(cooked chan<- *Cake) {
for {
cake := new(Cake)
cake.state = "cooked"
cooked <- cake // baker never touches this cake again
}
for {
cake := new(Cake)
cake.state = "cooked"
cooked <- cake // baker never touches this cake again
}
}
func icer(iced chan<- *Cake, cooked <-chan *Cake) {
for cake := range cooked {
cake.state = "iced"
iced <- cake // icer never touches this cake again
}
for cake := range cooked {
cake.state = "iced"
iced <- cake // icer never touches this cake again
}
}
```
第三种避免数据竞争的方法是允许很多goroutine去访问变量但是在同一个时刻最多只有一个goroutine在访问。这种方式被称为“互斥”在下一节来讨论这个主题
第三種避免數據競爭的方法是允許很多goroutine去訪問變量但是在同一個時刻最多隻有一個goroutine在訪問。這種方式被稱爲“互斥”在下一節來討論這個主題
练习 9.1: 给gopl.io/ch9/bank1程序添加一个Withdraw(amount int)取款函数。其返回结果应该要表明事务是成功了还是因为没有足够资金失败了。这条消息会被发送给monitor的goroutine且消息需要包含取款的额度和一个新的channel这个新channel会被monitor goroutine来把boolean结果发回给Withdraw。
**練習 9.1** 給gopl.io/ch9/bank1程序添加一個Withdraw(amount int)取款函數。其返迴結果應該要表明事務是成功了還是因爲沒有足夠資金失敗了。這條消息會被發送給monitor的goroutine且消息需要包含取款的額度和一個新的channel這個新channel會被monitor goroutine來把boolean結果發迴給Withdraw。

View File

@ -1,6 +1,6 @@
## 9.2. sync.Mutex互斥鎖
在8.6节中我们使用了一个buffered channel作为一个计数信号量来保证最多只有20个goroutine会同时执行HTTP请求。同理我们可以用一个容量只有1的channel来保证最多只有一个goroutine在同一时刻访问一个共享变量。一个只能为1和0的信号量叫做二元信号量(binary semaphore)。
在8.6節中我們使用了一個buffered channel作爲一個計數信號量來保證最多隻有20個goroutine會同時執行HTTP請求。同理我們可以用一個容量隻有1的channel來保證最多隻有一個goroutine在同一時刻訪問一個共享變量。一個隻能爲1和0的信號量叫做二元信號量(binary semaphore)。
```go
gopl.io/ch9/bank2
@ -23,7 +23,7 @@ func Balance() int {
}
```
这种互斥很实用而且被sync包里的Mutex类型直接支持。它的Lock方法能够获取到token(这里叫锁)并且Unlock方法会释放这个token
這種互斥很實用而且被sync包里的Mutex類型直接支持。它的Lock方法能夠獲取到token(這里叫鎖)併且Unlock方法會釋放這個token
```go
gopl.io/ch9/bank3
@ -48,14 +48,13 @@ func Balance() int {
}
```
每次一个goroutine访问bank变量时(这里只有balance余额变量)它都会调用mutex的Lock方法来获取一个互斥锁。如果其它的goroutine已经获得了这个锁的话这个操作会被阻塞直到其它goroutine调用了Unlock使该锁变回可用状态。mutex会保护共享变量。惯例来说被mutex所保护的变量是在mutex变量声明之后立刻声明的。如果你的做法和惯例不符确保在文档里对你的做法进行说明。
每次一個goroutine訪問bank變量時(這里隻有balance餘額變量)它都會調用mutex的Lock方法來獲取一個互斥鎖。如果其它的goroutine已經獲得了這個鎖的話這個操作會被阻塞直到其它goroutine調用了Unlock使該鎖變迴可用狀態。mutex會保護共享變量。慣例來説被mutex所保護的變量是在mutex變量聲明之後立刻聲明的。如果你的做法和慣例不符確保在文檔里對你的做法進行説明。
在Lock和Unlock之间的代码段中的内容goroutine可以随便读取或者修改这个代码段叫做临界区。goroutine在结束后释放锁是必要的无论以哪条路径通过函数都需要释放即使是在错误路径中也要记得释放。
在Lock和Unlock之間的代碼段中的內容goroutine可以隨便讀取或者脩改這個代碼段叫做臨界區。goroutine在結束後釋放鎖是必要的無論以哪條路徑通過函數都需要釋放卽使是在錯誤路徑中也要記得釋放。
上面的bank程序例证了一种通用的并发模式。一系列的导出函数封装了一个或多个变量那么访问这些变量唯一的方式就是通过这些函数来做(或者方法,对于一个对象的变量来说)。每一个函数在一开始就获取互斥锁并在最后释放锁从而保证共享变量不会被并发访问。这种函数、互斥锁和变量的编排叫作监控monitor(这种老式单词的monitor是受"monitor goroutine"的术语启发而来的。两种用法都是一个代理人保证变量被顺序访问)。
由于在存款和查询余额函数中的临界区代码这么短--只有一行,没有分支调用--在代码最后去调用Unlock就显得更为直截了当。在更复杂的临界区的应用中尤其是必须要尽早处理错误并返回的情况下就很难去(靠人)判断对Lock和Unlock的调用是在所有路径中都能够严格配对的了。Go语言里的defer简直就是这种情况下的救星我们用defer来调用Unlock临界区会隐式地延伸到函数作用域的最后这样我们就从“总要记得在函数返回之后或者发生错误返回时要记得调用一次Unlock”这种状态中获得了解放。Go会自动帮我们完成这些事情。
上面的bank程序例證了一種通用的併發模式。一繫列的導出函數封裝了一個或多個變量那麽訪問這些變量唯一的方式就是通過這些函數來做(或者方法,對於一個對象的變量來説)。每一個函數在一開始就獲取互斥鎖併在最後釋放鎖從而保證共享變量不會被併發訪問。這種函數、互斥鎖和變量的編排叫作監控monitor(這種老式單詞的monitor是受"monitor goroutine"的術語啟發而來的。兩種用法都是一個代理人保證變量被順序訪問)。
由於在存款和査詢餘額函數中的臨界區代碼這麽短--隻有一行,沒有分支調用--在代碼最後去調用Unlock就顯得更爲直截了當。在更複雜的臨界區的應用中尤其是必須要盡早處理錯誤併返迴的情況下就很難去(靠人)判斷對Lock和Unlock的調用是在所有路徑中都能夠嚴格配對的了。Go語言里的defer簡直就是這種情況下的救星我們用defer來調用Unlock臨界區會隱式地延伸到函數作用域的最後這樣我們就從“總要記得在函數返迴之後或者發生錯誤返迴時要記得調用一次Unlock”這種狀態中獲得了解放。Go會自動幫我們完成這些事情。
```go
func Balance() int {
@ -65,11 +64,11 @@ func Balance() int {
}
```
上面的例子里Unlock会在return语句读取完balance的值之后执行所以Balance函数是并发安全的。这带来的另一点好处是我们再也不需要一个本地变量b了。
上面的例子里Unlock會在return語句讀取完balance的值之後執行所以Balance函數是併發安全的。這帶來的另一點好處是我們再也不需要一個本地變量b了。
此外,一个deferred Unlock即使在临界区发生panic时依然会执行这对于用recover (§5.10)来恢复的程序来说是很重要的。defer调用只会比显式地调用Unlock成本高那么一点点不过却在很大程度上保证了代码的整洁性。大多数情况下对于并发程序来说代码的整洁性比过度的优化更重要。如果可能的话尽量使用defer来将临界区扩展到函数的结束。
此外,一個deferred Unlock卽使在臨界區發生panic時依然會執行這對於用recover (§5.10)來恢複的程序來説是很重要的。defer調用隻會比顯式地調用Unlock成本高那麽一點點不過卻在很大程度上保證了代碼的整潔性。大多數情況下對於併發程序來説代碼的整潔性比過度的優化更重要。如果可能的話盡量使用defer來將臨界區擴展到函數的結束。
虑一下下面的Withdraw函数。成功的时候它会正确地减掉余额并返回true。但如果银行记录资金对交易来说不足那么取款就会恢复余额并返回false。
慮一下下面的Withdraw函數。成功的時候它會正確地減掉餘額併返迴true。但如果銀行記録資金對交易來説不足那麽取款就會恢複餘額併返迴false。
```go
// NOTE: not atomic!
@ -83,9 +82,9 @@ func Withdraw(amount int) bool {
}
```
数终于给出了正确的结果但是还有一点讨厌的副作用。当过多的取款操作同时执行时balance可能会瞬时被减到0以下。这可能会引起一个并发的取款被不合逻辑地拒绝。所以如果Bob尝试买一辆sports car时Alice可能就没办法为她的早咖啡付款了。这里的问题是取款不是一个原子操作它包含了三个步骤每一步都需要去获取并释放互斥锁但任何一次锁都不会锁上整个取款流程。
數終於給出了正確的結果但是還有一點討厭的副作用。當過多的取款操作同時執行時balance可能會瞬時被減到0以下。這可能會引起一個併發的取款被不合邏輯地拒絶。所以如果Bob嚐試買一輛sports car時Alice可能就沒辦法爲她的早咖啡付款了。這里的問題是取款不是一個原子操作它包含了三個步驟每一步都需要去獲取併釋放互斥鎖但任何一次鎖都不會鎖上整個取款流程。
理想情况下,取款应该只在整个操作中获得一次互斥锁。下面这样的尝试是错误的:
理想情況下,取款應該隻在整個操作中獲得一次互斥鎖。下面這樣的嚐試是錯誤的:
```go
// NOTE: incorrect!
@ -101,11 +100,11 @@ func Withdraw(amount int) bool {
}
```
上面这个例子中Deposit会调用mu.Lock()第二次去获取互斥锁但因为mutex已经锁上了而无法被重入(译注go里没有重入锁关于重入锁的概念请参考java)--也就是说没法对一个已经锁上的mutex来再次上锁--这会导致程序死锁没法继续执行下去Withdraw会永远阻塞下去。
上面這個例子中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 {
@ -135,9 +134,6 @@ func Balance() int {
func deposit(amount int) { balance += amount }
```
當然這里的存款deposit函數很小實際上取款withdraw函數不需要理會對它的調用盡管如此這里的表達還是表明了規則。
当然这里的存款deposit函数很小实际上取款withdraw函数不需要理会对它的调用尽管如此这里的表达还是表明了规则。
封装(§6.6), 用限制一个程序中的意外交互的方式可以使我们获得数据结构的不变性。因为某种原因封装还帮我们获得了并发的不变性。当你使用mutex时确保mutex和其保护的变量没有被导出(在go里也就是小写且不要被大写字母开头的函数访问啦)无论这些变量是包级的变量还是一个struct的字段。
封裝(§6.6), 用限製一個程序中的意外交互的方式可以使我們獲得數據結構的不變性。因爲某種原因封裝還幫我們獲得了併發的不變性。當你使用mutex時確保mutex和其保護的變量沒有被導出(在go里也就是小寫且不要被大寫字母開頭的函數訪問啦)無論這些變量是包級的變量還是一個struct的字段。

View File

@ -20,5 +20,5 @@ Balance函數現在調用了RLock和RUnlock方法來獲取和釋放一個讀取
RLock隻能在臨界區共享變量沒有任何寫入操作時可用。一般來説我們不應該假設邏輯上的隻讀函數/方法也不會去更新某一些變量。比如一個方法功能是訪問一個變量,但它也有可能會同時去給一個內部的計數器+1(譯註:可能是記録這個方法的訪問次數啥的),或者去更新緩存--使卽時的調用能夠更快。如果有疑惑的話,請使用互斥鎖。
RWMutex隻有當獲得鎖的大部分goroutine都是讀操作而鎖在競爭條件下也就是説goroutine們必等待才能獲取到鎖的時候RWMutex才是最能帶來好處的。RWMutex需要更複雜的內部記録所以會讓它比一般的無競爭鎖的mutex慢一些。
RWMutex隻有當獲得鎖的大部分goroutine都是讀操作而鎖在競爭條件下也就是説goroutine們必等待才能獲取到鎖的時候RWMutex才是最能帶來好處的。RWMutex需要更複雜的內部記録所以會讓它比一般的無競爭鎖的mutex慢一些。

View File

@ -87,7 +87,7 @@ func Icon(name string) image.Image {
```
上面的代碼有兩個臨界區。goroutine首先會獲取一個寫鎖査詢map然後釋放鎖。如果條目被找到了(一般情況下)那麽會直接返迴。如果沒有找到那goroutine會獲取一個寫鎖。不釋放共享鎖的話也沒有任何辦法來將一個共享鎖陞級爲一個互斥鎖所以我們必重新檢査icons變量是否爲nil以防止在執行這一段代碼的時候icons變量已經被其它gorouine初始化過了。
上面的代碼有兩個臨界區。goroutine首先會獲取一個寫鎖査詢map然後釋放鎖。如果條目被找到了(一般情況下)那麽會直接返迴。如果沒有找到那goroutine會獲取一個寫鎖。不釋放共享鎖的話也沒有任何辦法來將一個共享鎖陞級爲一個互斥鎖所以我們必重新檢査icons變量是否爲nil以防止在執行這一段代碼的時候icons變量已經被其它gorouine初始化過了。
上面的模闆使我們的程序能夠更好的併發但是有一點太複雜且容易出錯。幸運的是sync包爲我們提供了一個專門的方案來解決這種一次性初始化的問題sync.Once。概念上來講一次性的初始化需要一個互斥量mutex和一個boolean變量來記録初始化是不是已經完成了互斥量用來保護boolean變量和客戶端數據結構。Do這個唯一的方法需要接收初始化函數作爲其參數。讓我們用sync.Once來簡化前面的Icon函數吧

View File

@ -18,6 +18,6 @@ $ GOMAXPROCS=2 go run hacker-cliché.go
010101010101010101011001100101011010010100110...
```
在第一次執行時最多同時隻能有一個goroutine被執行。初始情況下隻有main goroutine被執行所以會打印很多1。過了一段時間後GO調度器會將其置爲休眠併喚醒另一個goroutine這時候就開始打印很多0了在打印的時候goroutine是被調度到操作繫統線程上的。在第二次執行時我們使用了兩個操作繫統線程所以兩個goroutine可以一起被執行以同樣的頻率交替打印0和1。我們必強調的是goroutine的調度是受很多因子影響的而runtime也是在不斷地發展演進的所以這里的你實際得到的結果可能會因爲版本的不同而與我們運行的結果有所不同。
在第一次執行時最多同時隻能有一個goroutine被執行。初始情況下隻有main goroutine被執行所以會打印很多1。過了一段時間後GO調度器會將其置爲休眠併喚醒另一個goroutine這時候就開始打印很多0了在打印的時候goroutine是被調度到操作繫統線程上的。在第二次執行時我們使用了兩個操作繫統線程所以兩個goroutine可以一起被執行以同樣的頻率交替打印0和1。我們必強調的是goroutine的調度是受很多因子影響的而runtime也是在不斷地發展演進的所以這里的你實際得到的結果可能會因爲版本的不同而與我們運行的結果有所不同。
練習9.6: 測試一下計算密集型的併發程序(練習8.5那樣的)會被GOMAXPROCS怎樣影響到。在你的電腦上最佳的值是多少你的電腦CPU有多少個核心

View File

@ -1,5 +1,5 @@
# 第九章 基於共享變量的併發
前一章我們介紹了一些使用goroutine和channel這樣直接而自然的方式來實現併發的方法。然而這樣做我們實際上屏蔽掉了在寫併發代碼時必處理的一些重要而且細微的問題。
前一章我們介紹了一些使用goroutine和channel這樣直接而自然的方式來實現併發的方法。然而這樣做我們實際上屏蔽掉了在寫併發代碼時必處理的一些重要而且細微的問題。
在本章中我們會細致地了解併發機製。尤其是在多goroutine之間的共享變量併發問題的分析手段以及解決這些問題的基本模式。最後我們會解釋goroutine和操作繫統線程之間的技術上的一些區别。

View File

@ -29,13 +29,13 @@ Go語言聖經 [《The Go Programming Language》](http://gopl.io) 中文版本
從早期提交日誌中也可以看出Go語言是從[Ken Thompson](http://genius.cat-v.org/ken-thompson/)發明的B語言、[Dennis M. Ritchie](http://genius.cat-v.org/dennis-ritchie/)發明的C語言逐步演化過來的是C語言家族的成員因此很多人將Go語言稱爲21世紀的C語言。縱觀這幾年來的發展趨勢Go語言已經成爲雲計算、雲存儲時代最重要的基礎編程語言。
在C語言發明之後約5年的時間之後1978年[Brian W. Kernighan](http://www.cs.princeton.edu/~bwk/)和[Dennis M. Ritchie](http://genius.cat-v.org/dennis-ritchie/)合作編寫出版了C語言方面的經典敎材《[The C Programming Language](The C Programming Language)》該書被譽爲C語言程序員的聖經作者也被大家親切地稱爲[K&R](https://en.wikipedia.org/wiki/K%26R)。同樣在Go語言正式發布2009年約5年之後2014年開始寫作2015年出版由Go語言核心糰隊成員[Alan A. A. Donovan](https://github.com/adonovan)和[K&R](https://en.wikipedia.org/wiki/K%26R)中的[Brian W. Kernighan](http://www.cs.princeton.edu/~bwk/)合作編寫了Go語言方面的經典敎材《[The Go Programming Language](http://gopl.io)》。Go語言被譽爲21世紀的C語言如果説[K&R](https://en.wikipedia.org/wiki/K%26R)所著的是聖經的舊約那麽D&K所著的必將成爲聖經的新約。該書介紹了Go語言幾乎全部特性併且隨着語言的深入層層遞進對每個細節都解讀得非常細致每一節內容都精綵不容錯過是廣大Gopher的必讀書目。同時,大部分Go語言核心糰隊的成員都參與了該書校對工作因此該書的質量是可以完全放心的。
在C語言發明之後約5年的時間之後1978年[Brian W. Kernighan](http://www.cs.princeton.edu/~bwk/)和[Dennis M. Ritchie](http://genius.cat-v.org/dennis-ritchie/)合作編寫出版了C語言方面的經典敎材《[The C Programming Language](http://s3-us-west-2.amazonaws.com/belllabs-microsite-dritchie/cbook/index.html)》該書被譽爲C語言程序員的聖經作者也被大家親切地稱爲[K&R](https://en.wikipedia.org/wiki/K%26R)。同樣在Go語言正式發布2009年約5年之後2014年開始寫作2015年出版由Go語言核心糰隊成員[Alan A. A. Donovan](https://github.com/adonovan)和[K&R](https://en.wikipedia.org/wiki/K%26R)中的[Brian W. Kernighan](http://www.cs.princeton.edu/~bwk/)合作編寫了Go語言方面的經典敎材《[The Go Programming Language](http://gopl.io)》。Go語言被譽爲21世紀的C語言如果説[K&R](https://en.wikipedia.org/wiki/K%26R)所著的是聖經的舊約那麽D&K所著的必將成爲聖經的新約。該書介紹了Go語言幾乎全部特性併且隨着語言的深入層層遞進對每個細節都解讀得非常細致每一節內容都精綵不容錯過是廣大Gopher的必讀書目。大部分Go語言核心糰隊的成員都參與了該書校對工作因此該書的質量是可以完全放心的。
同時,單憑閲讀和學習其語法結構併不能眞正地掌握一門編程語言,必進行足夠多的編程實踐——親自編寫一些程序併研究學習别人寫的程序。要從利用Go語言良好的特性使得程序模塊化充分利用Go的標準函數庫以Go語言自己的風格來編寫程序。書中包含了上百個精心挑選的習題希望大家能先用自己的方式嚐試完成習題然後再參考官方給出的解決方案。
同時,單憑閲讀和學習其語法結構併不能眞正地掌握一門編程語言,必進行足夠多的編程實踐——親自編寫一些程序併研究學習别人寫的程序。要從利用Go語言良好的特性使得程序模塊化充分利用Go的標準函數庫以Go語言自己的風格來編寫程序。書中包含了上百個精心挑選的習題希望大家能先用自己的方式嚐試完成習題然後再參考官方給出的解決方案。
該書英文版約從2015年10月開始公開發售同時同步發售的還有日文版本。不過比較可惜的是,中文版併沒有在同步發售之列,甚至連中文版是否會引進、是由哪個出版社引進、卽使引進將由何人來翻譯、何時能出版都成了一個祕密。中国的Go語言社區是全球最大的Go語言社區我們從一開始就始終緊跟着Go語言的發展腳步。我們應該也完全有能力以中国Go語言社區的力量同步完成Go語言聖經中文版的翻譯工作。與此同時国內有很多Go語言愛好者也在積極關註該書本人也在第一時間購買了紙質版本[亞馬遜價格314人民幣](http://www.amazon.cn/The-Go-Programming-Language-Donovan-Alan-A-A/dp/0134190440/)。爲了Go語言的學習和交流大家決定合作免費翻譯該書。
該書英文版約從2015年10月開始公開發售其中日文版本最早參與翻譯和審校參考致謝部分。在2015年10月我們併不知道中文版是否會及時引進、將由哪家出版社引進、引進將由何人來翻譯、何時能出版這些信息都成了一個祕密。中国的Go語言社區是全球最大的Go語言社區我們從一開始就始終緊跟着Go語言的發展腳步。我們應該也完全有能力以中国Go語言社區的力量同步完成Go語言聖經中文版的翻譯工作。與此同時国內有很多Go語言愛好者也在積極關註該書本人也在第一時間購買了紙質版本[亞馬遜價格314人民幣](http://www.amazon.cn/The-Go-Programming-Language-Donovan-Alan-A-A/dp/0134190440/)。爲了Go語言的學習和交流大家決定合作免費翻譯該書。
翻譯工作從2015年11月20日前後開始到2016年1月底初步完成前後歷時約2個月時間。其中[chai2010](https://github.com/chai2010)翻譯了前言、第2~4章、第10~13章[Xargin](https://github.com/cch123)翻譯了第1章、第6章、第8~9章[CrazySssst](https://github.com/CrazySssst)翻譯了第5章[foreversmart](https://github.com/foreversmart)翻譯了第7章大家共同參與了基本的校驗工作還有其他一些朋友提供了積極的反饋建議。如果大家還有任何問題或建議可以直接到中文版項目頁面提交[Issue](https://github.com/golang-china/gopl-zh/issues),如果發現英文版原文在[勘誤](http://www.gopl.io/errata.html)中未提到的任何錯誤,可以直接去[英文版項目](https://github.com/adonovan/gopl.io/)提交。
翻譯工作從2015年11月20日前後開始到2016年1月底初步完成前後歷時約2個月時間(在其它語言版本中,全球第一個完成翻譯的,基本做到和原版同步)。其中,[chai2010](https://github.com/chai2010)翻譯了前言、第2~4章、第10~13章[Xargin](https://github.com/cch123)翻譯了第1章、第6章、第8~9章[CrazySssst](https://github.com/CrazySssst)翻譯了第5章[foreversmart](https://github.com/foreversmart)翻譯了第7章大家共同參與了基本的校驗工作還有其他一些朋友提供了積極的反饋建議。如果大家還有任何問題或建議可以直接到中文版項目頁面提交[Issue](https://github.com/golang-china/gopl-zh/issues),如果發現英文版原文在[勘誤](http://www.gopl.io/errata.html)中未提到的任何錯誤,可以直接去[英文版項目](https://github.com/adonovan/gopl.io/)提交。
最後希望這本書能夠幫助大家用Go語言快樂地編程。

View File

@ -57,10 +57,10 @@
- [x] 7.4 Parsing Flags with flag.Value
- [x] 7.5 Interface Values
- [x] 7.6 Sorting with sort.Interface
- [ ] 7.7 The http.Handler Interface
- [ ] 7.8 The error Interface
- [ ] 7.9 Example: Expression Evaluator
- [ ] 7.10 Type Assertions
- [x] 7.7 The http.Handler Interface
- [x] 7.8 The error Interface
- [x] 7.9 Example: Expression Evaluator
- [x] 7.10 Type Assertions
- [ ] 7.11 Discriminating Errors with Type Assertions
- [ ] 7.12 Querying Behaviors with Interface Type Assertions
- [ ] 7.13 Type Switches
@ -78,8 +78,8 @@
- [x] 8.9 Cancellation
- [x] 8.10 Example: Chat Server
- [x] Chapter 9: Concurrency with Shared Variables
- [ ] 9.1 Race Conditions
- [ ] 9.2 Mutual Exclusion: sync.Mutex
- [x] 9.1 Race Conditions
- [x] 9.2 Mutual Exclusion: sync.Mutex
- [x] 9.3 Read/Write Mutexes: sync.RWMutex
- [x] 9.4 Memory Synchronization
- [x] 9.5 Lazy Initialization: sync.Once
@ -117,4 +117,3 @@
- [x] 13.3 Example: Deep Equivalence
- [x] 13.4 Calling C Code with cgo
- [x] 13.5 Another Word of Caution

View File

@ -207,6 +207,8 @@ var zh2twMapPatch = map[rune]rune{
'国': '国',
'获': '獲',
'核': '核',
'糖': '糖',
'须': '須',
}
var _TSCharactersMap = map[rune]rune{