mirror of
https://github.com/gopl-zh/gopl-zh.github.com.git
synced 2024-12-25 14:28:58 +00:00
Merge branch 'master' of github.com:golang-china/gopl-zh
This commit is contained in:
commit
7058cbd96a
@ -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)
|
||||
|
@ -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)
|
||||
|
@ -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.)
|
||||
|
||||
|
@ -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章
|
||||
|
22
appendix/appendix-d-translations.md
Normal file
22
appendix/appendix-d-translations.md
Normal 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
|
@ -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 提供的説明安裝。
|
||||
|
||||
|
@ -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排名較低,因爲缺少撕逼的話題)。當然了,這可以避免由於代碼格式導致的邏輯上的歧義。
|
||||
|
||||
|
@ -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]。
|
||||
|
||||
|
@ -167,7 +167,7 @@ func main() {
|
||||
}
|
||||
```
|
||||
|
||||
ReadFile函數返迴一個byte的slice,這個slice必鬚被轉換爲string,之後才能夠用string.Split方法來進行處理。我們在3.5.4節中會更詳細地講解string和byte slice(字節數組)。
|
||||
ReadFile函數返迴一個byte的slice,這個slice必須被轉換爲string,之後才能夠用strings.Split方法來進行處理。我們在3.5.4節中會更詳細地講解string和byte slice(字節數組)。
|
||||
|
||||
在更底層一些的地方,bufio.Scanner,ioutil.ReadFile和ioutil.WriteFile使用的是*os.File的Read和Write方法,不過一般程序員併不需要去直接了解到其底層實現細節,在bufio和io/ioutil包中提供的方法已經足夠好用。
|
||||
|
||||
|
@ -69,7 +69,7 @@ bla kIndex)
|
||||
|
||||
當我們import了一個包路徑包含有多個單詞的package時,比如image/color(image和color兩個單詞),通常我們隻需要用最後那個單詞表示這個包就可以。所以當我們寫color.White時,這個變量指向的是image/color包里的變量,同理gif.GIF是屬於image/gif包里的變量。
|
||||
|
||||
這個程序里的常量聲明給出了一繫列的常量值,常量是指在程序編譯後運行時始終都不會變化的值,比如圈數、幀數、延遲值。常量聲明和變量聲明一般都會出現在包級别,所以這些常量在整個包中都是可以共享的,或者你也可以把常量聲明定義在函數體內部,那麽這種常量就隻能在函數體內用。目前常量聲明的值必鬚是一個數字值、字符串或者一個固定的boolean值。
|
||||
這個程序里的常量聲明給出了一繫列的常量值,常量是指在程序編譯後運行時始終都不會變化的值,比如圈數、幀數、延遲值。常量聲明和變量聲明一般都會出現在包級别,所以這些常量在整個包中都是可以共享的,或者你也可以把常量聲明定義在函數體內部,那麽這種常量就隻能在函數體內用。目前常量聲明的值必須是一個數字值、字符串或者一個固定的boolean值。
|
||||
|
||||
[]color.Color{...}和gif.GIF{...}這兩個表達式就是我們説的複合聲明(4.2和4.4.1節有説明)。這是實例化Go語言里的複合類型的一種寫法。這里的前者生成的是一個slice切片,後者生成的是一個struct結構體。
|
||||
|
||||
|
@ -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數據都打印出來,這樣可以讓檢査和調試這個服務更爲方便:
|
||||
|
||||
|
@ -6,4 +6,4 @@
|
||||
|
||||
每個包還通過控製包內名字的可見性和是否導出來實現封裝特性。通過限製包成員的可見性併隱藏包API的具體實現,將允許包的維護者在不影響外部包用戶的前提下調整包的內部實現。通過限製包內變量的可見性,還可以強製用戶通過某些特定函數來訪問和更新內部變量,這樣可以保證內部變量的一致性和併發時的互斥約束。
|
||||
|
||||
當我們脩改了一個源文件,我們必鬚重新編譯該源文件對應的包和所有依賴該包的其他包。卽使是從頭構建,Go語言編譯器的編譯速度也明顯快於其它編譯語言。Go語言的閃電般的編譯速度主要得益於三個語言特性。第一點,所有導入的包必鬚在每個文件的開頭顯式聲明,這樣的話編譯器就沒有必要讀取和分析整個源文件來判斷包的依賴關繫。第二點,禁止包的環狀依賴,因爲沒有循環依賴,包的依賴關繫形成一個有向無環圖,每個包可以被獨立編譯,而且很可能是被併發編譯。第三點,編譯後包的目標文件不僅僅記録包本身的導出信息,目標文件同時還記録了包的依賴關繫。因此,在編譯一個包的時候,編譯器隻需要讀取每個直接導入包的目標文件,而不需要遍歷所有依賴的的文件(譯註:很多都是重複的間接依賴)。
|
||||
當我們脩改了一個源文件,我們必須重新編譯該源文件對應的包和所有依賴該包的其他包。卽使是從頭構建,Go語言編譯器的編譯速度也明顯快於其它編譯語言。Go語言的閃電般的編譯速度主要得益於三個語言特性。第一點,所有導入的包必須在每個文件的開頭顯式聲明,這樣的話編譯器就沒有必要讀取和分析整個源文件來判斷包的依賴關繫。第二點,禁止包的環狀依賴,因爲沒有循環依賴,包的依賴關繫形成一個有向無環圖,每個包可以被獨立編譯,而且很可能是被併發編譯。第三點,編譯後包的目標文件不僅僅記録包本身的導出信息,目標文件同時還記録了包的依賴關繫。因此,在編譯一個包的時候,編譯器隻需要讀取每個直接導入包的目標文件,而不需要遍歷所有依賴的的文件(譯註:很多都是重複的間接依賴)。
|
||||
|
@ -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。
|
||||
|
@ -25,7 +25,7 @@ import (
|
||||
)
|
||||
```
|
||||
|
||||
如果我們想同時導入兩個有着名字相同的包,例如math/rand包和crypto/rand包,那麽導入聲明必鬚至少爲一個同名包指定一個新的包名以避免衝突。這叫做導入包的重命名。
|
||||
如果我們想同時導入兩個有着名字相同的包,例如math/rand包和crypto/rand包,那麽導入聲明必須至少爲一個同名包指定一個新的包名以避免衝突。這叫做導入包的重命名。
|
||||
|
||||
```Go
|
||||
import (
|
||||
|
@ -4,7 +4,7 @@
|
||||
|
||||
因爲每個目録隻包含一個包,因此每個對應可執行程序或者叫Unix術語中的命令的包,會要求放到一個獨立的目録中。這些目録有時候會放在名叫cmd目録的子目録下面,例如用於提供Go文檔服務的golang.org/x/tools/cmd/godoc命令就是放在cmd子目録(§10.7.4)。
|
||||
|
||||
每個包可以由它們的導入路徑指定,就像前面看到的那樣,或者用一個相對目録的路徑知指定,相對路徑必鬚以`.`或`..`開頭。如果沒有指定參數,那麽默認指定爲當前目録對應的包。 下面的命令用於構建同一個包, 雖然它們的寫法各不相同:
|
||||
每個包可以由它們的導入路徑指定,就像前面看到的那樣,或者用一個相對目録的路徑知指定,相對路徑必須以`.`或`..`開頭。如果沒有指定參數,那麽默認指定爲當前目録對應的包。 下面的命令用於構建同一個包, 雖然它們的寫法各不相同:
|
||||
|
||||
```
|
||||
$ cd $GOPATH/src/gopl.io/ch1/helloworld
|
||||
|
@ -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的情況.
|
||||
|
||||
|
||||
|
@ -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) {
|
||||
|
@ -38,7 +38,7 @@ $ go list -f={{.TestGoFiles}} fmt
|
||||
|
||||
包的測試代碼通常都在這些文件中, 不過 fmt 包併非如此; 稍後我們再解釋 export_test.go 文件的作用.
|
||||
|
||||
XTestGoFiles 表示的是屬於測試擴展包的測試代碼, 也就是 fmt_test 包, 因此它們必鬚先導入 fmt 包. 同樣, 這些文件也隻是在測試時被構建運行:
|
||||
XTestGoFiles 表示的是屬於測試擴展包的測試代碼, 也就是 fmt_test 包, 因此它們必須先導入 fmt 包. 同樣, 這些文件也隻是在測試時被構建運行:
|
||||
|
||||
|
||||
{% raw %}
|
||||
|
@ -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 的長度, 輸出一個相關部分的簡明總結卽可. 測試的作者應該要努力幫助程序員診斷失敗的測試.
|
||||
|
||||
|
@ -12,7 +12,7 @@ func ExampleIsPalindrome() {
|
||||
}
|
||||
```
|
||||
|
||||
示例函數有三個用處. 最主要的一個是用於文檔: 一個包的例子可以更簡潔直觀的方式來演示函數的用法, 會文字描述會更直接易懂, 特别是作爲一個提醒或快速參考時. 一個例子函數也可以方便展示屬於同一個接口的幾種類型或函數直接的關繫, 所有的文檔都必鬚關聯到一個地方, 就像一個類型或函數聲明都統一到包一樣. 同時, 示例函數和註釋併不一樣, 示例函數是完整眞是的Go代碼, 需要介紹編譯器的編譯時檢査, 這樣可以保證示例代碼不會腐爛成不能使用的舊代碼.
|
||||
示例函數有三個用處. 最主要的一個是用於文檔: 一個包的例子可以更簡潔直觀的方式來演示函數的用法, 會文字描述會更直接易懂, 特别是作爲一個提醒或快速參考時. 一個例子函數也可以方便展示屬於同一個接口的幾種類型或函數直接的關繫, 所有的文檔都必須關聯到一個地方, 就像一個類型或函數聲明都統一到包一樣. 同時, 示例函數和註釋併不一樣, 示例函數是完整眞是的Go代碼, 需要介紹編譯器的編譯時檢査, 這樣可以保證示例代碼不會腐爛成不能使用的舊代碼.
|
||||
|
||||
根據示例函數的後綴名部分, godoc 的web文檔會將一個示例函數關聯到某個具體函數或包本身, 因此 ExampleIsPalindrome 示例函數將是 IsPalindrome 函數文檔的一部分, Example 示例函數將是包文檔的一部分.
|
||||
|
||||
|
@ -10,7 +10,7 @@ Maurice Wilkes, 第一個存儲程序計算機 EDSAC 的設計者, 1949年在他
|
||||
|
||||
Go語言的測試技術是相對低級的. 它依賴一個 'go test' 測試命令, 和一組按照約定方式編寫的測試函數, 測試命令可以運行測試函數. 編寫相對輕量級的純測試代碼是有效的, 而且它很容易延伸到基準測試和示例文檔.
|
||||
|
||||
在實踐中, 編寫測試代碼和編寫程序本身併沒有多大區别. 我們編寫的每一個函數也是針對每個具體的任務. 我們必鬚小心處理邊界條件, 思考合適的數據結構, 推斷合適的輸入應該産生什麽樣的結果輸出. 編程測試代碼和編寫普通的Go代碼過程是類似的; 它併不需要學習新的符號, 規則和工具.
|
||||
在實踐中, 編寫測試代碼和編寫程序本身併沒有多大區别. 我們編寫的每一個函數也是針對每個具體的任務. 我們必須小心處理邊界條件, 思考合適的數據結構, 推斷合適的輸入應該産生什麽樣的結果輸出. 編程測試代碼和編寫普通的Go代碼過程是類似的; 它併不需要學習新的符號, 規則和工具.
|
||||
|
||||
|
||||
|
||||
|
@ -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的任務。)
|
||||
|
||||
|
@ -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函數處理。對於map,key可能是任意類型,對元素的處理方式和slice類似,我們創建一個新的變量,然後遞歸填充它,最後將新解析到的key/value對添加到map。
|
||||
在循環處理結構體和map每個元素時必須解碼一個(key value)格式的對應子列表。對於結構體,key部分對於成員的名字。和數組類似,我們使用FieldByName找到結構體對應成員的變量,然後遞歸調用read函數處理。對於map,key可能是任意類型,對元素的處理方式和slice類似,我們創建一個新的變量,然後遞歸填充它,最後將新解析到的key/value對添加到map。
|
||||
|
||||
```Go
|
||||
func readList(lex *lexer, v reflect.Value) {
|
||||
|
@ -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位機器上的典型的內存. 灰色區域是空洞.
|
||||
|
||||
|
@ -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`地址。第三個向之前無效地址空間的賦值語句將徹底摧譭整個程序!
|
||||
|
||||
還有很多類似原因導致的錯誤。例如這條語句:
|
||||
|
||||
|
@ -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
|
||||
|
@ -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命名。通常來説,如果一個名字的作用域比較大,生命週期也比較長,那麽用長的名字將會更有意義。
|
||||
|
||||
|
@ -2,7 +2,7 @@
|
||||
|
||||
聲明語句定義了程序的各種實體對象以及部分或全部的屬性。Go語言主要有四種類型的聲明語句:var、const、type和func,分别對應變量、常量、類型和函數實體對象的聲明。這一章我們重點討論變量和類型的聲明,第三章將討論常量的聲明,第五章將討論函數的聲明。
|
||||
|
||||
一個Go語言編寫的程序對應一個或多個以.go爲文件後綴名的源文件中。每個源文件以包的聲明語句開始,説明該源文件是屬於哪個包。包聲明語句之後是import語句導入依賴的其它包,然後是包一級的類型、變量、常量、函數的聲明語句,包一級的各種類型的聲明語句的順序無關緊要(譯註:函數內部的名字則必鬚先聲明之後才能使用)。例如,下面的例子中聲明了一個常量、一個函數和兩個變量:
|
||||
一個Go語言編寫的程序對應一個或多個以.go爲文件後綴名的源文件中。每個源文件以包的聲明語句開始,説明該源文件是屬於哪個包。包聲明語句之後是import語句導入依賴的其它包,然後是包一級的類型、變量、常量、函數的聲明語句,包一級的各種類型的聲明語句的順序無關緊要(譯註:函數內部的名字則必須先聲明之後才能使用)。例如,下面的例子中聲明了一個常量、一個函數和兩個變量:
|
||||
|
||||
```Go
|
||||
gopl.io/ch2/boiling
|
||||
|
@ -53,7 +53,7 @@ in, err := os.Open(infile)
|
||||
out, err := os.Create(outfile)
|
||||
```
|
||||
|
||||
簡短變量聲明語句中必鬚至少要聲明一個新的變量,下面的代碼將不能編譯通過:
|
||||
簡短變量聲明語句中必須至少要聲明一個新的變量,下面的代碼將不能編譯通過:
|
||||
|
||||
```Go
|
||||
f, err := os.Open(infile)
|
||||
|
@ -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測試用例:
|
||||
|
||||
|
@ -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函數都是返迴一個新的變量的地址,因此下面兩個地址是不同的:
|
||||
|
@ -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語言的自動垃圾收集器對編寫正確的代碼是一個鉅大的幫助,但也併不是説你完全不用考慮內存了。你雖然不需要顯式地分配和釋放內存,但是要編寫高效的程序你依然需要了解變量的生命週期。例如,如果將指向短生命週期對象的指針保存到具有長生命週期的對象中,特别是保存到全局變量時,會阻止對短生命週期對象的垃圾迴收(從而可能影響程序的性能)。
|
||||
|
||||
|
@ -39,7 +39,7 @@ i, j, k = 2, 3, 5
|
||||
|
||||
但如果表達式太複雜的話,應該盡量避免過度使用元組賦值;因爲每個變量單獨賦值語句的寫法可讀性會更好。
|
||||
|
||||
有些表達式會産生多個值,比如調用一個有多個返迴值的函數。當這樣一個函數調用出現在元組賦值右邊的表達式中時(譯註:右邊不能再有其它表達式),左邊變量的數目必鬚和右邊一致。
|
||||
有些表達式會産生多個值,比如調用一個有多個返迴值的函數。當這樣一個函數調用出現在元組賦值右邊的表達式中時(譯註:右邊不能再有其它表達式),左邊變量的數目必須和右邊一致。
|
||||
|
||||
```Go
|
||||
f, err = os.Open("foo.txt") // function call returns two values
|
||||
|
@ -16,10 +16,10 @@ medals[2] = "bronze"
|
||||
|
||||
map和chan的元素,雖然不是普通的變量,但是也有類似的隱式賦值行爲。
|
||||
|
||||
不管是隱式還是顯式地賦值,在賦值語句左邊的變量和右邊最終的求到的值必鬚有相同的數據類型。更直白地説,隻有右邊的值對於左邊的變量是可賦值的,賦值語句才是允許的。
|
||||
不管是隱式還是顯式地賦值,在賦值語句左邊的變量和右邊最終的求到的值必須有相同的數據類型。更直白地説,隻有右邊的值對於左邊的變量是可賦值的,賦值語句才是允許的。
|
||||
|
||||
可賦值性的規則對於不同類型有着不同要求,對每個新類型特殊的地方我們會專門解釋。對於目前我們已經討論過的類型,它的規則是簡單的:類型必鬚完全匹配,nil可以賦值給任何指針或引用類型的變量。常量(§3.6)則有更靈活的賦值規則,因爲這樣可以避免不必要的顯式的類型轉換。
|
||||
可賦值性的規則對於不同類型有着不同要求,對每個新類型特殊的地方我們會專門解釋。對於目前我們已經討論過的類型,它的規則是簡單的:類型必須完全匹配,nil可以賦值給任何指針或引用類型的變量。常量(§3.6)則有更靈活的賦值規則,因爲這樣可以避免不必要的顯式的類型轉換。
|
||||
|
||||
對於兩個值是否可以用`==`或`!=`進行相等比較的能力也和可賦值能力有關繫:對於任何類型的值的相等比較,第二個值必鬚是對第一個值類型對應的變量是可賦值的,反之依然。和前面一樣,我們會對每個新類型比較特殊的地方做專門的解釋。
|
||||
對於兩個值是否可以用`==`或`!=`進行相等比較的能力也和可賦值能力有關繫:對於任何類型的值的相等比較,第二個值必須是對第一個值類型對應的變量是可賦值的,反之依然。和前面一樣,我們會對每個新類型比較特殊的地方做專門的解釋。
|
||||
|
||||
|
||||
|
@ -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語言中,一個簡單的規則是:如果一個名字是大寫字母開頭的,那麽該名字是導出的(譯註:因爲漢字不區分大小寫,因此漢字開頭的名字是沒有導出的)。
|
||||
|
||||
|
@ -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集合、分析二進製文件格式或者是哈希和加密操作等。它們通常併不用於僅僅是表達非負數量的場合。
|
||||
|
||||
一般來説,需要一個顯式的轉換將一個值從一種類型轉化位另一種類型,併且算術和邏輯運算的二元操作中必鬚是相同的類型。雖然這偶爾會導致需要很長的表達式,但是它消除了所有和類型相關的問題,而且也使得程序容易理解。
|
||||
一般來説,需要一個顯式的轉換將一個值從一種類型轉化位另一種類型,併且算術和邏輯運算的二元操作中必須是相同的類型。雖然這偶爾會導致需要很長的表達式,但是它消除了所有和類型相關的問題,而且也使得程序容易理解。
|
||||
|
||||
在很多場景,會遇到類似下面的代碼通用的錯誤:
|
||||
|
||||
|
@ -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請求參數設置高度、寬度和顔色等參數。
|
||||
|
||||
|
||||
|
@ -20,7 +20,7 @@ if 'a' <= c && c <= 'z' ||
|
||||
}
|
||||
```
|
||||
|
||||
布爾值併不會隱式轉換爲數字值0或1,反之亦然。必鬚使用一個顯式的if語句輔助轉換:
|
||||
布爾值併不會隱式轉換爲數字值0或1,反之亦然。必須使用一個顯式的if語句輔助轉換:
|
||||
|
||||
```Go
|
||||
i := 0
|
||||
|
@ -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編碼優良的設計,諸多字符串操作都不需要解碼操作。我們可以不用解碼直接測試一個字符串是否是另一個字符串的前綴:
|
||||
|
||||
|
@ -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"
|
||||
|
@ -80,7 +80,7 @@ c := 0i // untyped complex; implicit complex128(0i)
|
||||
|
||||
註意默認類型是規則的:無類型的整數常量默認轉換爲int,對應不確定的內存大小,但是浮點數和複數常量則默認轉換爲float64和complex128。Go語言本身併沒有不確定內存大小的浮點數和複數類型,而且如果不知道浮點數類型的話將很難寫出正確的數值算法。
|
||||
|
||||
如果要給變量一個不同的類型,我們必鬚顯式地將無類型的常量轉化爲所需的類型,或給聲明的變量指定明確的類型,像下面例子這樣:
|
||||
如果要給變量一個不同的類型,我們必須顯式地將無類型的常量轉化爲所需的類型,或給聲明的變量指定明確的類型,像下面例子這樣:
|
||||
|
||||
```Go
|
||||
var i = int8(0)
|
||||
|
@ -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}
|
||||
|
@ -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引用的將是不同的底層數組。
|
||||
|
||||
|
@ -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比較,例如:
|
||||
|
||||
|
@ -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參數忠實地記録每個字符串元素的信息:
|
||||
|
||||
|
@ -50,7 +50,7 @@ func Bonus(e *Employee, percent int) int {
|
||||
}
|
||||
```
|
||||
|
||||
如果要在函數內部脩改結構體成員的話,用指針傳入是必鬚的;因爲在Go語言中,所有的函數參數都是值拷貝傳入的,函數參數將不再是函數調用時的原始變量。
|
||||
如果要在函數內部脩改結構體成員的話,用指針傳入是必須的;因爲在Go語言中,所有的函數參數都是值拷貝傳入的,函數參數將不再是函數調用時的原始變量。
|
||||
|
||||
```Go
|
||||
func AwardAnnualRaise(e *Employee) {
|
||||
|
@ -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節中專門討論。
|
||||
|
@ -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,但是在下一節我們將看到如果利用模闆來輸出複雜的格式。
|
||||
|
||||
|
@ -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語言沒有默認參數值,也沒有任何方法可以通過參數名指定形參,因此形參和返迴值的變量名對於函數調用者而言沒有意義。
|
||||
|
||||
在函數體中,函數的形參作爲局部變量,被初始化爲調用者提供的值。函數的形參和有名返迴值作爲函數最外層的局部變量,被存儲在相同的詞法塊中。
|
||||
|
||||
|
100
ch5/ch5-02.md
100
ch5/ch5-02.md
@ -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。
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
|
@ -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和應用程序用戶界面的一個重要組成部分,程序運行失敗僅被認爲是幾個預期的結果之一。
|
||||
|
||||
|
@ -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 }
|
||||
|
@ -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) {
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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里包外調用函數需要帶上包名,還是挺麻煩的。
|
||||
|
@ -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語言,是差不多的)
|
||||
|
@ -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" %}
|
||||
|
@ -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
|
||||
|
@ -34,7 +34,7 @@ time.AfterFunc(10 * time.Second, r.Launch)
|
||||
|
||||
譯註:省掉了上面那個例子里的匿名函數。
|
||||
|
||||
和方法"值"相關的還有方法表達式。當調用一個方法時,與調用一個普通的函數相比,我們必鬚要用選擇器(p.Distance)語法來指定方法的接收器。
|
||||
和方法"值"相關的還有方法表達式。當調用一個方法時,與調用一個普通的函數相比,我們必須要用選擇器(p.Distance)語法來指定方法的接收器。
|
||||
|
||||
當T是一個類型時,方法表達式可能會寫作T.f或者(*T).f,會返迴一個函數"值",這種函數會將其第一個參數用作接收器,所以可以用通常(譯註:不寫選擇器)的方式來對其進行調用:
|
||||
|
||||
|
@ -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)
|
||||
|
@ -2,7 +2,7 @@
|
||||
|
||||
一個對象的變量或者方法如果對調用方是不可見的話,一般就被定義爲“封裝”。封裝有時候也被叫做信息隱藏,同時也是面向對象編程最關鍵的一個方面。
|
||||
|
||||
Go語言隻有一種控製可見性的手段:大寫首字母的標識符會從定義它們的包中被導出,小寫字母的則不會。這種限製包內成員的方式同樣適用於struct或者一個類型的方法。因而如果我們想要封裝一個對象,我們必鬚將其定義爲一個struct。
|
||||
Go語言隻有一種控製可見性的手段:大寫首字母的標識符會從定義它們的包中被導出,小寫字母的則不會。這種限製包內成員的方式同樣適用於struct或者一個類型的方法。因而如果我們想要封裝一個對象,我們必須將其定義爲一個struct。
|
||||
|
||||
這也就是前面的小節中IntSet被定義爲struct類型的原因,盡管它隻有一個字段:
|
||||
|
||||
|
@ -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類型的值序列。
|
||||
|
@ -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
|
||||
|
@ -1,5 +1,5 @@
|
||||
## 7.3. 實現接口的條件
|
||||
一個類型如果擁有一個接口需要的所有方法,那麽這個類型就實現了這個接口。例如,*os.File類型實現了io.Reader,Writer,Closer,和ReadWriter接口。*bytes.Buffer實現了Reader,Writer,和ReadWriter這些接口,但是它沒有實現Closer接口因爲它不具有Close方法。Go的程序員經常會簡要的把一個具體的類型描述成一個特定的接口類型。舉個例子,*bytes.Buffer是io.Writer;*os.Files是io.ReadWriter。
|
||||
一個類型如果擁有一個接口需要的所有方法,那麽這個類型就實現了這個接口。例如,\*os.File類型實現了io.Reader,Writer,Closer,和ReadWriter接口。\*bytes.Buffer實現了Reader,Writer,和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{}值持有一個boolean,float,string,map,pointer,或者任意其它的類型;我們當然不能直接對它持有的值做操作,因爲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語言中我們可以在需要的時候定義一個新的抽象或者特定特點的組,而不需要脩改具體類型的定義。當具體的類型來自不同的作者時這種方式會特别有用。當然也確實沒有必要在具體的類型中指出這些共性。
|
||||
|
@ -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,
|
||||
|
@ -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動作:
|
||||
|
||||
|
@ -80,7 +80,7 @@ func printTracks(tracks []*Track) {
|
||||
}
|
||||
```
|
||||
|
||||
爲了能按照Artist字段對播放列表進行排序,我們會像對StringSlice那樣定義一個新的帶有必鬚Len,Less和Swap方法的切片類型。
|
||||
爲了能按照Artist字段對播放列表進行排序,我們會像對StringSlice那樣定義一個新的帶有必須Len,Less和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
|
||||
|
197
ch7/ch7-07.md
197
ch7/ch7-07.md
@ -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)可能會對你有幫助。
|
||||
|
@ -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節了解到其它滿足這個接口的類型。
|
||||
|
309
ch7/ch7-09.md
309
ch7/ch7-09.md
@ -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字段隻能是pow,sin或者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接口中。這個包隻會對外公開Expr,Env,和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的這個方法會計算對於pow,sin,或者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有相同的籤名,但是它計算一個用戶提供的表達式。環境變量中定義了x,y和半徑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的計算器程序。
|
||||
|
@ -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...
|
||||
}
|
||||
```
|
||||
|
@ -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命令來作爲客戶端,或者也可以自己實現一個。
|
||||
|
@ -72,7 +72,7 @@ func request(hostname string) (response string) { /* ... */ }
|
||||
|
||||
關於無緩存或帶緩存channels之間的選擇,或者是帶緩存channels的容量大小的選擇,都可能影響程序的正確性。無緩存channel更強地保證了每個發送操作與相應的同步接收操作;但是對於帶緩存channel,這些操作是解耦的。同樣,卽使我們知道將要發送到一個channel的信息的數量上限,創建一個對應容量大小帶緩存channel也是不現實的,因爲這要求在執行任何接收操作之前緩存所有已經發送的值。如果未能分配足夠的緩衝將導致程序死鎖。
|
||||
|
||||
Channel的緩存也可能影響程序的性能。想象一家蛋糕店有三個廚師,一個烘焙,一個上醣衣,還有一個將每個蛋糕傳遞到它下一個廚師在生産線。在狹小的廚房空間環境,每個廚師在完成蛋糕後必鬚等待下一個廚師已經準備好接受它;這類似於在一個無緩存的channel上進行溝通。
|
||||
Channel的緩存也可能影響程序的性能。想象一家蛋糕店有三個廚師,一個烘焙,一個上糖衣,還有一個將每個蛋糕傳遞到它下一個廚師在生産線。在狹小的廚房空間環境,每個廚師在完成蛋糕後必須等待下一個廚師已經準備好接受它;這類似於在一個無緩存的channel上進行溝通。
|
||||
|
||||
如果在每個廚師之間有一個放置一個蛋糕的額外空間,那麽每個廚師就可以將一個完成的蛋糕臨時放在那里而馬上進入下一個蛋糕在製作中;這類似於將channel的緩存隊列的容量設置爲1。隻要每個廚師的平均工作效率相近,那麽其中大部分的傳輸工作將是迅速的,個體之間細小的效率差異將在交接過程中瀰補。如果廚師之間有更大的額外空間——也是就更大容量的緩存隊列——將可以在不停止生産線的前提下消除更大的效率波動,例如一個廚師可以短暫地休息,然後在加快趕上進度而不影響其其他人。
|
||||
|
||||
|
@ -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是最合適的呢?
|
||||
|
@ -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/)。
|
||||
|
@ -131,4 +131,4 @@ channel的零值是nil。也許會讓你覺得比較奇怪,nil的channel有時
|
||||
|
||||
這使得我們可以用nil來激活或者禁用case,來達成處理其它輸入或輸出事件時超時和取消的邏輯。我們會在下一節中看到一個例子。
|
||||
|
||||
練習8.8: 使用select來改造8.3節中的echo服務器,爲其增加超時,這樣服務器可以在客戶端10秒中沒有任何喊話時自動斷開連接。
|
||||
**練習 8.8:** 使用select來改造8.3節中的echo服務器,爲其增加超時,這樣服務器可以在客戶端10秒中沒有任何喊話時自動斷開連接。
|
||||
|
@ -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目録下的目録大小計算併顯示出來。
|
||||
|
@ -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的變種。當第一個請求返迴時,直接取消其它的請求。
|
||||
|
@ -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中發消息。
|
||||
|
116
ch9/ch9-01.md
116
ch9/ch9-01.md
@ -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个字节)更大的类型时,事情就变得更麻烦了,比如interface,string或者slice类型都是如此。下面的代码会并发地更新两个不同长度的slice:
|
||||
如果數據競爭的對象是一個比一個機器字(譯註:32位機器上一個字=4個字節)更大的類型時,事情就變得更麻煩了,比如interface,string或者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。
|
||||
|
||||
|
@ -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的字段。
|
||||
|
@ -20,5 +20,5 @@ Balance函數現在調用了RLock和RUnlock方法來獲取和釋放一個讀取
|
||||
|
||||
RLock隻能在臨界區共享變量沒有任何寫入操作時可用。一般來説,我們不應該假設邏輯上的隻讀函數/方法也不會去更新某一些變量。比如一個方法功能是訪問一個變量,但它也有可能會同時去給一個內部的計數器+1(譯註:可能是記録這個方法的訪問次數啥的),或者去更新緩存--使卽時的調用能夠更快。如果有疑惑的話,請使用互斥鎖。
|
||||
|
||||
RWMutex隻有當獲得鎖的大部分goroutine都是讀操作,而鎖在競爭條件下,也就是説,goroutine們必鬚等待才能獲取到鎖的時候,RWMutex才是最能帶來好處的。RWMutex需要更複雜的內部記録,所以會讓它比一般的無競爭鎖的mutex慢一些。
|
||||
RWMutex隻有當獲得鎖的大部分goroutine都是讀操作,而鎖在競爭條件下,也就是説,goroutine們必須等待才能獲取到鎖的時候,RWMutex才是最能帶來好處的。RWMutex需要更複雜的內部記録,所以會讓它比一般的無競爭鎖的mutex慢一些。
|
||||
|
||||
|
@ -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函數吧:
|
||||
|
||||
|
@ -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有多少個核心?
|
||||
|
@ -1,5 +1,5 @@
|
||||
# 第九章 基於共享變量的併發
|
||||
|
||||
前一章我們介紹了一些使用goroutine和channel這樣直接而自然的方式來實現併發的方法。然而這樣做我們實際上屏蔽掉了在寫併發代碼時必鬚處理的一些重要而且細微的問題。
|
||||
前一章我們介紹了一些使用goroutine和channel這樣直接而自然的方式來實現併發的方法。然而這樣做我們實際上屏蔽掉了在寫併發代碼時必須處理的一些重要而且細微的問題。
|
||||
|
||||
在本章中,我們會細致地了解併發機製。尤其是在多goroutine之間的共享變量,併發問題的分析手段,以及解決這些問題的基本模式。最後我們會解釋goroutine和操作繫統線程之間的技術上的一些區别。
|
||||
|
@ -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語言快樂地編程。
|
||||
|
||||
|
13
progress.md
13
progress.md
@ -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
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user