mirror of
https://github.com/gopl-zh/gopl-zh.github.com.git
synced 2024-12-24 22:08:57 +00:00
update tw
This commit is contained in:
parent
510c741a6f
commit
c66a96ee52
@ -1,7 +1,7 @@
|
||||
# 貢獻者列錶
|
||||
# 貢獻者列表
|
||||
|
||||
|
||||
*大傢幫助完善, 請保證列錶有序(忽略大小寫)!*
|
||||
*大傢幫助完善, 請保証列表有序(忽略大小寫)!*
|
||||
|
||||
|
||||
作者 | 章節
|
||||
@ -13,5 +13,5 @@
|
||||
|
||||
# 版權
|
||||
|
||||
除特彆註明外, 本站內容均採用[知識共享-署名(CC-BY) 3.0協議](http://creativecommons.org/licenses/by/3.0/)授權, 代碼遵循[Go項目的BSD協議](http://golang.org/LICENSE)授權.
|
||||
除特別註明外, 本站內容均採用[知識共享-署名(CC-BY) 3.0協議](http://creativecommons.org/licenses/by/3.0/)授權, 代碼遵循[Go項目的BSD協議](http://golang.org/LICENSE)授權.
|
||||
|
||||
|
@ -1 +1 @@
|
||||
# 朮語
|
||||
# 術語
|
||||
|
12
README.md
12
README.md
@ -1,11 +1,11 @@
|
||||
# 關於 [《Go聖經讀書筆記》](http://golang-china.github.io/gopl-zh)
|
||||
|
||||
作為 [《The Go Programming Language》](http://gopl.io/) (中文名[《Go編程語言》](http://golang-china.github.io/gopl-zh)) 英文原版紙質圖書的購買者, [《Go聖經讀書筆記》](http://golang-china.github.io/gopl-zh) 是我們的 **讀書筆記** 和 **習題解答**, 僅供學習交流用.
|
||||
作爲 [《The Go Programming Language》](http://gopl.io/) (中文名[《Go編程語言》](http://golang-china.github.io/gopl-zh)) 英文原版紙質圖書的購買者, [《Go聖經讀書筆記》](http://golang-china.github.io/gopl-zh) 是我們的 **讀書筆記** 和 **習題解答**, 僅供學習交流用.
|
||||
|
||||
- 此 **讀書筆記** 在綫預覽: http://golang-china.github.io/gopl-zh
|
||||
- 此 **讀書筆記** 的源文件: http://github.com/golang-china/gopl-zh
|
||||
- 此 **讀書筆記** 項目進度: http://github.com/golang-china/gopl-zh/blob/master/progress.md
|
||||
- 此 **讀書筆記** 蔘與人員: http://github.com/golang-china/gopl-zh/blob/master/CONTRIBUTORS.md
|
||||
- 此 **讀書筆記** 參與人員: http://github.com/golang-china/gopl-zh/blob/master/CONTRIBUTORS.md
|
||||
- 原版官網: http://gopl.io
|
||||
|
||||
[![](cover_small.jpg)](https://github.com/golang-china/gopl-zh)
|
||||
@ -15,13 +15,13 @@
|
||||
|
||||
先安裝 Go語言環境, git 工具 和 GitBook 命令行工具(`npm install gitbook-cli -g` 命令).
|
||||
|
||||
1. 運行 `go get github.com/golang-china/gopl-zh`, 穫取 源文件
|
||||
2. 運行 `go generate github.com/golang-china/gopl-zh`, 生成 `_book` 目彔
|
||||
1. 運行 `go get github.com/golang-china/gopl-zh`, 獲取 源文件
|
||||
2. 運行 `go generate github.com/golang-china/gopl-zh`, 生成 `_book` 目録
|
||||
3. 打開 `_book/index.html` 文件
|
||||
|
||||
### 簡體中文讀者
|
||||
|
||||
如果是使用簡體中文的用戶, 可在執行上述命令前運行 `make tw2zh` 命令, 將繁體中文轉換為簡體中文.
|
||||
如果是使用簡體中文的用戶, 可在執行上述命令前運行 `make tw2zh` 命令, 將繁體中文轉換爲簡體中文.
|
||||
|
||||
# 版權聲明
|
||||
|
||||
@ -29,7 +29,7 @@
|
||||
|
||||
<a rel="license" href="http://creativecommons.org/licenses/by-nc-sa/4.0/"><img alt="Creative Commons License" style="border-width:0" src="./images/by-nc-sa-4.0-88x31.png"></img></a>
|
||||
|
||||
嚴禁任何商業行為使用或引用該 **讀書筆記** 的全部或部分內容!
|
||||
嚴禁任何商業行爲使用或引用該 **讀書筆記** 的全部或部分內容!
|
||||
|
||||
歡迎大傢提供建議!
|
||||
|
||||
|
42
SUMMARY.md
42
SUMMARY.md
@ -11,8 +11,8 @@
|
||||
* [命令行參數](ch1/ch1-02.md)
|
||||
* [査找重復的行](ch1/ch1-03.md)
|
||||
* [GIF動畫](ch1/ch1-04.md)
|
||||
* [穫取URL](ch1/ch1-05.md)
|
||||
* [併髮穫取多個URL](ch1/ch1-06.md)
|
||||
* [獲取URL](ch1/ch1-05.md)
|
||||
* [並發獲取多個URL](ch1/ch1-06.md)
|
||||
* [Web服務](ch1/ch1-07.md)
|
||||
* [本章要點](ch1/ch1-08.md)
|
||||
* [程序結構](ch2/ch2.md)
|
||||
@ -30,7 +30,7 @@
|
||||
* [佈爾型](ch3/ch3-04.md)
|
||||
* [字符串](ch3/ch3-05.md)
|
||||
* [常量](ch3/ch3-06.md)
|
||||
* [復閤數據類型](ch4/ch4.md)
|
||||
* [復合數據類型](ch4/ch4.md)
|
||||
* [數組](ch4/ch4-01.md)
|
||||
* [切片](ch4/ch4-02.md)
|
||||
* [字典](ch4/ch4-03.md)
|
||||
@ -47,16 +47,16 @@
|
||||
* [可變參數](ch5/ch5-07.md)
|
||||
* [Deferred函數](ch5/ch5-08.md)
|
||||
* [Panic異常](ch5/ch5-09.md)
|
||||
* [Recover捕穫異常](ch5/ch5-10.md)
|
||||
* [Recover捕獲異常](ch5/ch5-10.md)
|
||||
* [方法](ch6/ch6.md)
|
||||
* [方法聲明](ch6/ch6-01.md)
|
||||
* [基於指鍼對象的方法](ch6/ch6-02.md)
|
||||
* [通過嵌入結構體來擴展類型](ch6/ch6-03.md)
|
||||
* [方法值和方法錶達式](ch6/ch6-04.md)
|
||||
* [方法值和方法表達式](ch6/ch6-04.md)
|
||||
* [示例: Bit數組](ch6/ch6-05.md)
|
||||
* [封裝](ch6/ch6-06.md)
|
||||
* [接口](ch7/ch7.md)
|
||||
* [接口是閤約](ch7/ch7-01.md)
|
||||
* [接口是合約](ch7/ch7-01.md)
|
||||
* [接口類型](ch7/ch7-02.md)
|
||||
* [實現接口的條件](ch7/ch7-03.md)
|
||||
* [flag.Value接口](ch7/ch7-04.md)
|
||||
@ -64,32 +64,32 @@
|
||||
* [sort.Interface接口](ch7/ch7-06.md)
|
||||
* [http.Handler接口](ch7/ch7-07.md)
|
||||
* [error接口](ch7/ch7-08.md)
|
||||
* [示例: 錶達式求值](ch7/ch7-09.md)
|
||||
* [示例: 表達式求值](ch7/ch7-09.md)
|
||||
* [類型斷言](ch7/ch7-10.md)
|
||||
* [基於類型斷言識彆錯誤類型](ch7/ch7-11.md)
|
||||
* [基於類型斷言識別錯誤類型](ch7/ch7-11.md)
|
||||
* [通過類型斷言査詢接口](ch7/ch7-12.md)
|
||||
* [類型分支](ch7/ch7-13.md)
|
||||
* [示例: 基於標記的XML解碼](ch7/ch7-14.md)
|
||||
* [補充幾點](ch7/ch7-15.md)
|
||||
* [Goroutines和Channels](ch8/ch8.md)
|
||||
* [Goroutines](ch8/ch8-01.md)
|
||||
* [示例: 併髮的Clock服務](ch8/ch8-02.md)
|
||||
* [示例: 併髮的Echo服務](ch8/ch8-03.md)
|
||||
* [示例: 並發的Clock服務](ch8/ch8-02.md)
|
||||
* [示例: 並發的Echo服務](ch8/ch8-03.md)
|
||||
* [Channels](ch8/ch8-04.md)
|
||||
* [併行的循環](ch8/ch8-05.md)
|
||||
* [示例: 併髮的Web爬蟲](ch8/ch8-06.md)
|
||||
* [並行的循環](ch8/ch8-05.md)
|
||||
* [示例: 並發的Web爬蟲](ch8/ch8-06.md)
|
||||
* [基於select的多路復用](ch8/ch8-07.md)
|
||||
* [示例: 併髮的字典遍歷](ch8/ch8-08.md)
|
||||
* [併髮的退齣](ch8/ch8-09.md)
|
||||
* [示例: 並發的字典遍曆](ch8/ch8-08.md)
|
||||
* [並發的退齣](ch8/ch8-09.md)
|
||||
* [示例: 聊天服務](ch8/ch8-10.md)
|
||||
* [基於共享變量的併髮](ch9/ch9.md)
|
||||
* [基於共享變量的並發](ch9/ch9.md)
|
||||
* [競爭條件](ch9/ch9-01.md)
|
||||
* [sync.Mutex互斥鎖](ch9/ch9-02.md)
|
||||
* [sync.RWMutex讀寫鎖](ch9/ch9-03.md)
|
||||
* [內存衕步](ch9/ch9-04.md)
|
||||
* [內存同步](ch9/ch9-04.md)
|
||||
* [sync.Once初始化](ch9/ch9-05.md)
|
||||
* [競爭條件檢測](ch9/ch9-06.md)
|
||||
* [示例: 併髮的非阻塞緩存](ch9/ch9-07.md)
|
||||
* [示例: 並發的非阻塞緩存](ch9/ch9-07.md)
|
||||
* [Goroutines和綫程](ch9/ch9-08.md)
|
||||
* [包和工具](ch10/ch10.md)
|
||||
* [簡介](ch10/ch10-01.md)
|
||||
@ -107,13 +107,13 @@
|
||||
* [剖析](ch11/ch11-05.md)
|
||||
* [示例函數](ch11/ch11-06.md)
|
||||
* [反射](ch12/ch12.md)
|
||||
* [為何需要反射?](ch12/ch12-01.md)
|
||||
* [爲何需要反射?](ch12/ch12-01.md)
|
||||
* [reflect.Type和reflect.Value](ch12/ch12-02.md)
|
||||
* [Display遞歸打印](ch12/ch12-03.md)
|
||||
* [示例: 編碼S錶達式](ch12/ch12-04.md)
|
||||
* [示例: 編碼S表達式](ch12/ch12-04.md)
|
||||
* [通過reflect.Value脩改值](ch12/ch12-05.md)
|
||||
* [示例: 解碼S錶達式](ch12/ch12-06.md)
|
||||
* [穫取結構體字段標識](ch12/ch12-07.md)
|
||||
* [示例: 解碼S表達式](ch12/ch12-06.md)
|
||||
* [獲取結構體字段標識](ch12/ch12-07.md)
|
||||
* [顯示一個類型的方法集](ch12/ch12-08.md)
|
||||
* [幾點忠告](ch12/ch12-09.md)
|
||||
* [底層編程](ch13/ch13.md)
|
||||
|
@ -1,20 +1,20 @@
|
||||
## Go語言起源
|
||||
|
||||
就像生物物種, 一個成功的編程語言的後代一般都會繼承它們祖先的優點; 當然有時多種語言混閤也會產生令人驚訝的特性; 還有一些激進的新特性可能併沒有先例. 我們可以通過觀察語言的和環境是如何相互促進和影響的演化過程而學到很多.
|
||||
就像生物物種, 一個成功的編程語言的後代一般都會繼承它們祖先的優點; 當然有時多種語言混合也會產生令人驚訝的特性; 還有一些激進的新特性可能並沒有先例. 我們可以通過觀察語言的和環境是如何相互促進和影響的演化過程而學到很多.
|
||||
|
||||
下圖展示了最早期的編程語言對Go語言設計產生的重要影響.
|
||||
|
||||
![](../images/ch0-01.png)
|
||||
|
||||
Go有時候被描述為"C類似語言", 或者是"21世紀的C語言". Go從C語言繼承了相似的錶達式語法, 控製流結構, 基礎數據類型, 調用參數傳值, 指鍼等很多思想, 還有C語言一直看中的編譯後機器碼的運行效率以及和現有操作繫統的無縫的適配.
|
||||
Go有時候被描述爲"C類似語言", 或者是"21世紀的C語言". Go從C語言繼承了相似的表達式語法, 控製流結構, 基礎數據類型, 調用參數傳值, 指鍼等很多思想, 還有C語言一直看中的編譯後機器碼的運行效率以及和現有操作繫統的無縫的適配.
|
||||
|
||||
但是在Go語言傢的族樹中還有其他的祖先. 其中一個有影響的分支來自Niklaus Wirth設計的Pascal語言. Modula-2 激髮了包的概唸. Oberon 摒棄了模塊接口文件和模塊實現文件之間的區彆. Oberon-2 影響了的包的導入和聲明的語法, 還有 麫曏對象 Oberon 所提供的方法的聲明語法等.
|
||||
但是在Go語言傢的族樹中還有其他的祖先. 其中一個有影響的分支來自Niklaus Wirth設計的Pascal語言. Modula-2 激發了包的概唸. Oberon 摒棄了模塊接口文件和模塊實現文件之間的區別. Oberon-2 影響了的包的導入和聲明的語法, 還有 面向對象 Oberon 所提供的方法的聲明語法等.
|
||||
|
||||
Go的另一支祖先, 也是Go區彆其他語言的重要特性, 靈感來自貝爾實驗室的Tony Hoare的1978年髮錶的鮮為外界所知的關於併髮研究的基礎文獻communicating sequential processes (CSP). 在CSP中, 程序是一組中間沒有共享狀態的平行的處理過程, 它們使用管道進行通信和衕步. 不過Tony Hoare的CSP隻是一個用於描述併髮性基本概唸的描述語言, 併不是一個編寫可執行程序的編程語言.
|
||||
Go的另一支祖先, 也是Go區別其他語言的重要特性, 靈感來自貝爾實驗室的Tony Hoare的1978年發表的鮮爲外界所知的關於並發研究的基礎文獻communicating sequential processes (CSP). 在CSP中, 程序是一組中間沒有共享狀態的平行的處理過程, 它們使用管道進行通信和同步. 不過Tony Hoare的CSP隻是一個用於描述並發性基本概唸的描述語言, 並不是一個編寫可執行程序的編程語言.
|
||||
|
||||
Rob Pike和其他人開始嚐試將CSP引入實際的編程語言中. 第一個語言叫Squeak(老鼠間交流的語言), 一個提供鼠標和鍵盤事件處理的語言, 它的管道是靜態創建的. 然後是Newsqueak, 提供了類似C語言語句和錶達式的的語法和Pascal的類似推導語法. 它是一個帶垃圾迴收的純函數式語言, 再此鍼對管理鍵盤, 鼠標和窗口事件管理. 但是Newsqueak中管道是動態創建的, 屬於第一類值, 可以保存到變量中.
|
||||
Rob Pike和其他人開始嘗試將CSP引入實際的編程語言中. 第一個語言叫Squeak(老鼠間交流的語言), 一個提供鼠標和鍵盤事件處理的語言, 它的管道是靜態創建的. 然後是Newsqueak, 提供了類似C語言語句和表達式的的語法和Pascal的類似推導語法. 它是一個帶垃圾迴收的純函數式語言, 再此鍼對管理鍵盤, 鼠標和窗口事件管理. 但是Newsqueak中管道是動態創建的, 屬於第一類值, 可以保存到變量中.
|
||||
|
||||
在Plan9操作繫統中, 這些想法被吸收到一個叫Alef的編程語言中. Alef視圖將Newsqueak改造為繫統編程語言, 但是因為缺少垃圾迴收機製而導緻併髮處理很痛苦.
|
||||
在Plan9操作繫統中, 這些想法被吸收到一個叫Alef的編程語言中. Alef視圖將Newsqueak改造爲繫統編程語言, 但是因爲缺少垃圾迴收機製而導緻並發處理很痛苦.
|
||||
|
||||
Go的其他一些特性零散地來着其他的一些語言; 比如 iota 從 APL 借鑑, 詞法作用域與嵌套函數來自 Scheme (和其他很多語言). 我們也可以從Go中髮現很多創新的設計. 比如Go的切片為動態數組提供了有效的隨機存取性能, 以及可能會讓人聯想到鏈錶的底層的共享機製.
|
||||
還有Go自己髮明的defer語句.
|
||||
Go的其他一些特性零散地來着其他的一些語言; 比如 iota 從 APL 借鑒, 詞法作用域與嵌套函數來自 Scheme (和其他很多語言). 我們也可以從Go中發現很多創新的設計. 比如Go的切片爲動態數組提供了有效的隨機存取性能, 以及可能會讓人聯想到鏈表的底層的共享機製.
|
||||
還有Go自己發明的defer語句.
|
||||
|
@ -1,17 +1,17 @@
|
||||
## Go語言項目
|
||||
|
||||
所有的編程語言都反映了設計者對編程哲學的反思, 通常包括之前的語言所暴露的一些不足的地方.
|
||||
Go項目是在Google超級復雜的幾個軟件繫統遇到的一些問題的反思(但是這個問題絶不是穀歌特有的).
|
||||
Go項目是在Google超級復雜的幾個軟件繫統遇到的一些問題的反思(但是這個問題絕不是穀歌特有的).
|
||||
|
||||
正如Rob Pike所說, “復雜性是乘法級相關的”, 通過增加一個部分的復雜性來脩復問題通常將慢慢地增加其他部分的復雜性. 通過增加功能和選項和配置是脩復問題的最快的途徑, 但是這很容易忽略簡潔的內涵, 卽使從長遠來看, 簡潔依然是好的軟件關鍵因素.
|
||||
|
||||
簡潔需要在工作開始的時候減少不必要的想法, 併且在軟件的生命週期內嚴格區彆好的改變或壞的改變. 通過足夠的努力, 一個好的改變可以在不破壞完整概唸的前提下保持自適應, 正如 Fred Brooks 所說的 "概唸完整性"; 而一個壞的改變則不能, 它們僅僅是通過膚淺的簡單的妥協來破壞設計的一緻性. 隻有通過簡潔的設計, 纔能讓一個繫統保持穩定, 安全, 和持續的生長.
|
||||
簡潔需要在工作開始的時候減少不必要的想法, 並且在軟件的生命週期內嚴格區別好的改變或壞的改變. 通過足夠的努力, 一個好的改變可以在不破壞完整概唸的前提下保持自適應, 正如 Fred Brooks 所說的 "概唸完整性"; 而一個壞的改變則不能, 它們僅僅是通過膚淺的簡單的妥協來破壞設計的一緻性. 隻有通過簡潔的設計, 纔能讓一個繫統保持穩定, 安全, 和持續的生長.
|
||||
|
||||
Go項目包括語言本身, 附帶的工具和標準庫, 最後但併非不重要的, 簡潔編程哲學的宣言. 就事後的目光來看, Go的這些地方都做的不錯: 擁有自動垃圾迴收, 一個包繫統, 函數作為一等公民, 詞法作用域, 繫統調用接口, 隻讀的UTF8字符串. 但是Go隻有相對較少的特性, 也不太可能對添加更多. 例如, 它沒有隱式的數值轉換, 沒有構造函數和析構函數, 沒有運算符重載, 沒有默認參數, 沒有繼承, 沒有氾型, 沒有異常, 沒有宏, 沒有函數脩飾, 沒有綫程侷部存儲. 但是語言是成熟和穩定的, 而且保證曏後兼容: 以前的Go程序可以用新版本的編譯器和標準庫下構建.
|
||||
Go項目包括語言本身, 附帶的工具和標準庫, 最後但並非不重要的, 簡潔編程哲學的宣言. 就事後的目光來看, Go的這些地方都做的不錯: 擁有自動垃圾迴收, 一個包繫統, 函數作爲一等公民, 詞法作用域, 繫統調用接口, 隻讀的UTF8字符串. 但是Go隻有相對較少的特性, 也不太可能對添加更多. 例如, 它沒有隱式的數值轉換, 沒有構造函數和析構函數, 沒有運算符重載, 沒有默認參數, 沒有繼承, 沒有汎型, 沒有異常, 沒有宏, 沒有函數脩飾, 沒有綫程侷部存儲. 但是語言是成熟和穩定的, 而且保証向後兼容: 以前的Go程序可以用新版本的編譯器和標準庫下構建.
|
||||
|
||||
Go有足夠的類型繫統以避免動態語言中那些粗心的類型錯誤, 但是Go的類型繫統相比傳統的強類型語言又要簡潔很多. 有時候這會導緻一個"無類型"的抽象類型, 但是Go程序員併不需要像 C++ 或 Haskell 程序員那樣糾結具體類型的安全屬性. 但實踐中Go的簡潔的類型繫統給了程序員更多的安全性和更好的運行時性能.
|
||||
Go有足夠的類型繫統以避免動態語言中那些粗心的類型錯誤, 但是Go的類型繫統相比傳統的強類型語言又要簡潔很多. 有時候這會導緻一個"無類型"的抽象類型, 但是Go程序員並不需要像 C++ 或 Haskell 程序員那樣糾結具體類型的安全屬性. 但實踐中Go的簡潔的類型繫統給了程序員更多的安全性和更好的運行時性能.
|
||||
|
||||
Go 鼓勵當代計算機繫統設計的認識, 特彆是侷部的重要性. 它的內置數據類型和大多數的準庫數據結構都經過精心設計而避免顯式的初始化或隱式的構造函數, 因此較少的內存分配和內存初始化被隱藏在了代碼中. Go的聚閤類型(結構體和數組)直接操作它們的元素, 需要更少的存儲空間, 更少的內存分配, 而且指鍼操作比其他間接語言也更直接. 由於現代計算機是一個併行的機器, Go提供了基於CSP的併髮特性. Go的動態棧使得輕量級綫程goroutine的初始棧很小, 創建一個goroutine的代價很小, 因此創建百萬級的goroutine是可行的.
|
||||
Go 鼓勵當代計算機繫統設計的認識, 特別是侷部的重要性. 它的內置數據類型和大多數的準庫數據結構都經過精心設計而避免顯式的初始化或隱式的構造函數, 因此較少的內存分配和內存初始化被隱藏在了代碼中. Go的聚合類型(結構體和數組)直接操作它們的元素, 需要更少的存儲空間, 更少的內存分配, 而且指鍼操作比其他間接語言也更直接. 由於現代計算機是一個並行的機器, Go提供了基於CSP的並發特性. Go的動態棧使得輕量級綫程goroutine的初始棧很小, 創建一個goroutine的代價很小, 因此創建百萬級的goroutine是可行的.
|
||||
|
||||
Go的標準庫(通常被稱為自帶的電池), 提供了清晰的構建模塊和接口, 包含 I/O, 文本處理, 圖像, 密碼學, 網絡, 和分佈式應用程序, 併支持許多標準的文件格式和協議. 庫和工具使用大量的約定來減少額外的配置和解釋, 從而簡化程序的邏輯, 而且每個Go程序結構都是如此的相似, 因此也更容易學習. 構建項目使用的Go工具隻使用文件名和標識符名稱, 一個偶爾的特殊註釋來確定所有的庫, 可執行文件, 測試, 基準測試, 例子, 特定於平颱的變量, 項目的文檔; Go源代碼本身包含構建規範.
|
||||
Go的標準庫(通常被稱爲自帶的電池), 提供了清晰的構建模塊和接口, 包含 I/O, 文本處理, 圖像, 密碼學, 網絡, 和分佈式應用程序, 並支持許多標準的文件格式和協議. 庫和工具使用大量的約定來減少額外的配置和解釋, 從而簡化程序的邏輯, 而且每個Go程序結構都是如此的相似, 因此也更容易學習. 構建項目使用的Go工具隻使用文件名和標識符名稱, 一個偶爾的特殊註釋來確定所有的庫, 可執行文件, 測試, 基準測試, 例子, 特定於平檯的變量, 項目的文檔; Go源代碼本身包含構建規範.
|
||||
|
||||
|
@ -1,25 +1,25 @@
|
||||
## 本書的組織
|
||||
|
||||
我們假設你有一個或多個其他編程語言的使用經歷, 不過是類似 C、c++,和Java 的編譯型語言,
|
||||
我們假設你有一個或多個其他編程語言的使用經曆, 不過是類似 C、c++,和Java 的編譯型語言,
|
||||
還是類似 Python, Ruby, JavaScript 的腳本語言, 因此我們不會相對完全的編程語言初學者那樣解釋所有的細節.
|
||||
因為Go的語言的 變量,常量,錶達式,控製流和函數等語法也是類似的.
|
||||
因爲Go的語言的 變量,常量,表達式,控製流和函數等語法也是類似的.
|
||||
|
||||
第一章包含了Go敎程的基本結構, 通過十幾個程序介紹了用Go如何實現 類似讀寫文件, 文本格式化, 創建圖像,
|
||||
網絡客戶端和服務器通訊 等日常工作.
|
||||
|
||||
第二章描述了一個Go程序的基本元素結構, 變量, 定義新的類型, 包和文件, 和作用域. 第三章討論了數字, 佈爾值, 字符串和常量, 併演示了如何顯示和處理Unicode. 第四章描述了復閤類型, 從簡單的數組, 字典, 切片, 到動態列錶. 第五章涵蓋了函數, 併討論了錯誤處理, panic 和 recover, 和 defer 語句.
|
||||
第二章描述了一個Go程序的基本元素結構, 變量, 定義新的類型, 包和文件, 和作用域. 第三章討論了數字, 佈爾值, 字符串和常量, 並演示了如何顯示和處理Unicode. 第四章描述了復合類型, 從簡單的數組, 字典, 切片, 到動態列表. 第五章涵蓋了函數, 並討論了錯誤處理, panic 和 recover, 和 defer 語句.
|
||||
|
||||
第三章討論了數字、佈爾值、字符串和常數,併解釋顯示處理Unicode。
|
||||
第四章描述了復閤類型,類型建立使用數組,從簡單的地圖,結構,和切割的方法去動態列錶。第五章涵蓋了函數和討論錯誤處理,恐慌和恢復,而推遲的陳述。
|
||||
第三章討論了數字、佈爾值、字符串和常數,並解釋顯示處理Unicode。
|
||||
第四章描述了復合類型,類型建立使用數組,從簡單的地圖,結構,和切割的方法去動態列表。第五章涵蓋了函數和討論錯誤處理,恐慌和恢復,而推遲的陳述。
|
||||
|
||||
第一章到第五章是基礎部分, 任何主流命令式語言的一部分都是類似的. 雖然有時候Go的語法和風格會有自己的特色, 但是大多數程序員將很快適應.
|
||||
剩下的章節是Go中特有的部分: 方法, 接口, 併髮, 包, 測試和反射等.
|
||||
剩下的章節是Go中特有的部分: 方法, 接口, 並發, 包, 測試和反射等.
|
||||
|
||||
Go的麫曏對象是不衕尋常的. 它沒有類層次結構, 甚至沒有類; 僅僅是通過組閤(而不是繼承)簡單的對象來構建復雜的對象.
|
||||
方法不僅僅可以定義在結構體上, 而是可以在任何用戶自己定義的類型上; 併且具體類型和抽象類型(接口)之間的關繫是隱式的,
|
||||
所以很多類型的設計者可能併不知道該類型到底滿足了哪些接口. 方法在第六章討論, 接口在第七章將討論.
|
||||
Go的面向對象是不同尋常的. 它沒有類層次結構, 甚至沒有類; 僅僅是通過組合(而不是繼承)簡單的對象來構建復雜的對象.
|
||||
方法不僅僅可以定義在結構體上, 而是可以在任何用戶自己定義的類型上; 並且具體類型和抽象類型(接口)之間的關繫是隱式的,
|
||||
所以很多類型的設計者可能並不知道該類型到底滿足了哪些接口. 方法在第六章討論, 接口在第七章將討論.
|
||||
|
||||
第八章討論了基於順序通信進程(CSP)的概唸的併髮編程, 使用 goroutines 和 channels. 第九章討論了更為傳統的基於共享變量的併髮性.
|
||||
第八章討論了基於順序通信進程(CSP)的概唸的並發編程, 使用 goroutines 和 channels. 第九章討論了更爲傳統的基於共享變量的並發性.
|
||||
|
||||
第十章描述了包機製, 包的組織結構. 本章還展示了如何利用有效的利用Go自帶的工具,
|
||||
通過一個命令提供了編譯, 測試, 基準測試, 代碼格式化, 文檔, 和許多其他任務.
|
||||
@ -30,12 +30,12 @@ Go的麫曏對象是不衕尋常的. 它沒有類層次結構, 甚至沒有類;
|
||||
|
||||
每一章會有一些練習, 你可以根據你對Go語言的理解, 然後脩改書中的例子來探索Go的其他用法.
|
||||
|
||||
書中所有的代碼都可以從 gopl.io 上的 Git 倉庫下載. go get可以根據每個例子的其導入路徑方便地穫取/構建/併安裝. 你需要選擇一個目彔作為工作空間, 然後將GOPATH環境指曏這個工作目彔.
|
||||
書中所有的代碼都可以從 gopl.io 上的 Git 倉庫下載. go get可以根據每個例子的其導入路徑方便地獲取/構建/並安裝. 你需要選擇一個目録作爲工作空間, 然後將GOPATH環境指向這個工作目録.
|
||||
|
||||
Go工具將在必要時創建的相應的目彔. 例如:
|
||||
Go工具將在必要時創建的相應的目録. 例如:
|
||||
|
||||
$ export GOPATH=$HOME/gobook # 選擇工作目彔
|
||||
$ go get gopl.io/ch1/helloworld # 穫取/編譯/安裝
|
||||
$ export GOPATH=$HOME/gobook # 選擇工作目録
|
||||
$ go get gopl.io/ch1/helloworld # 獲取/編譯/安裝
|
||||
$ $GOPATH/bin/helloworld # 運行
|
||||
Hello, 世界 # 這是中文, 不是日文
|
||||
|
||||
|
@ -1,14 +1,14 @@
|
||||
## 更多的信息
|
||||
|
||||
最佳的信息來自Go的官方網站, https://golang.org, 它提供了訪問完善的參考文檔, 包括編程語言規範和標準庫等諸多信息. 衕時也包含了如果更好了編寫Go程序的基本敎程, 還有各種各樣的在綫文本資源和視頻資源, 它們是本身終有價值的補充. Go的博客 blog.golang.org 髮佈一些Go語言最好的實踐文章, 包括當前語言的狀態, 未來的計劃, 會議報告和Go相關的各種主題.
|
||||
最佳的信息來自Go的官方網站, https://golang.org, 它提供了訪問完善的參考文檔, 包括編程語言規範和標準庫等諸多信息. 同時也包含了如果更好了編寫Go程序的基本敎程, 還有各種各樣的在綫文本資源和視頻資源, 它們是本身終有價值的補充. Go的博客 blog.golang.org 發佈一些Go語言最好的實踐文章, 包括當前語言的狀態, 未來的計劃, 會議報告和Go相關的各種主題.
|
||||
|
||||
在綫訪問的一個有價值的地方是可以從web頁麫運行Go的程序(而紙質書則沒有這麼便利了). 這個功能 由來自 play.golang.org 的 Go Playground 提供, 併且可以方便地嵌入到其他頁麫, 例如 golang.org 的主頁, 或 godoc 提供的文檔中.
|
||||
在綫訪問的一個有價值的地方是可以從web頁面運行Go的程序(而紙質書則沒有這麽便利了). 這個功能 由來自 play.golang.org 的 Go Playground 提供, 並且可以方便地嵌入到其他頁面, 例如 golang.org 的主頁, 或 godoc 提供的文檔中.
|
||||
|
||||
Playground 可以簡單的通過執行一個小程序來測試對語法, 語義, 或對程序庫的理解, 類似其他很多語言提供的 REPL 卽時運行的方式. 衕時它可以生成對應的url, 非常適閤共享Go代碼片段, bug報告或提齣建議.
|
||||
Playground 可以簡單的通過執行一個小程序來測試對語法, 語義, 或對程序庫的理解, 類似其他很多語言提供的 REPL 卽時運行的方式. 同時它可以生成對應的url, 非常適合共享Go代碼片段, bug報告或提齣建議.
|
||||
|
||||
基於 Playground 構建的 Go Tour (tour.golang.org), 是一個繫列的Go入門敎程, 包含了諸多基本概唸和結構相關的可在綫運行的互動小程序.
|
||||
|
||||
Playground 和 Tour 也有一些不足, 它們隻能導入標準庫, 而且因為安全的原因對一些網絡庫做了限製. 而且要編譯和運行需要訪問互聯網. 對於一些更復製的實驗, 你可能需要在自己的電腦上運行程序. 倖運的是下載Go的過程很簡單, 從 golang.org 下載應該不超過幾分鍾, 然後就可以在自己電腦上編寫和運行Go程序了.
|
||||
Playground 和 Tour 也有一些不足, 它們隻能導入標準庫, 而且因爲安全的原因對一些網絡庫做了限製. 而且要編譯和運行需要訪問互聯網. 對於一些更復製的實驗, 你可能需要在自己的電腦上運行程序. 倖運的是下載Go的過程很簡單, 從 golang.org 下載應該不超過幾分鍾, 然後就可以在自己電腦上編寫和運行Go程序了.
|
||||
|
||||
Go是一個開源項目, 你可以 在 https://golang.org/pkg 閱讀標準庫中任意函數和類型的代碼, 和下載的代碼完全一緻. 這樣可以知道很多函數是如何工作的, 挖掘一些答案的細節, 或者僅僅是欣賞 專業的Go代碼.
|
||||
|
||||
|
@ -1,9 +1,9 @@
|
||||
## 緻謝
|
||||
|
||||
Rob Pike 和 Russ Cox, 以及其他很多Go團隊的覈心成員多次仔細閱讀了本書的手稿,
|
||||
他們對本書的組織結構和錶述用詞等給齣了很多寶貴的建議. 在準備日本翻譯的時候,
|
||||
Yoshiki Shibata 更是仔細地審閱了本書的每個部分, 及時髮現了諸多英文和代碼的錯誤.
|
||||
我們非常感謝本書的審閱者, 併感謝對本書給齣了重要的建議的 Brian Goetz, Corey Kosak,
|
||||
他們對本書的組織結構和表述用詞等給齣了很多寶貴的建議. 在準備日本翻譯的時候,
|
||||
Yoshiki Shibata 更是仔細地審閱了本書的每個部分, 及時發現了諸多英文和代碼的錯誤.
|
||||
我們非常感謝本書的審閱者, 並感謝對本書給齣了重要的建議的 Brian Goetz, Corey Kosak,
|
||||
Arnold Robbins, Josh Bleecher Snyder 和 Peter Weinberger 等人.
|
||||
|
||||
我們感謝 Sameer Ajmani, Ittai Balaban, David Crawshaw, Billy Donohue, Jonathan Feinberg, Andrew Gerrand,
|
||||
@ -15,11 +15,11 @@ Robert Griesemer, John Linderman, Minux Ma, Bryan Mills, Bala Natarajan, Cosmos
|
||||
來自AW生產團隊的 John Fuller, Dayna Isley, Julie Nahil, Chuti Prasertsith, 和 Barbara Wood,
|
||||
感謝你們的熱心幫助.
|
||||
|
||||
Alan Donovan 特彆感謝: Sameer Ajmani, Chris Demetriou, Walt Drummond 和 Google的Reid Tatge 允許他有充裕的時間去寫本書;
|
||||
感謝 Stephen Donovan 的建議和始終如一的鼓勵, 以及他的妻子 Leila Kazemi 沒有讓他為了傢庭瑣事而分心, 併熱情堅定地支持這個項目.
|
||||
Alan Donovan 特別感謝: Sameer Ajmani, Chris Demetriou, Walt Drummond 和 Google的Reid Tatge 允許他有充裕的時間去寫本書;
|
||||
感謝 Stephen Donovan 的建議和始終如一的鼓勵, 以及他的妻子 Leila Kazemi 沒有讓他爲了傢庭瑣事而分心, 並熱情堅定地支持這個項目.
|
||||
|
||||
Brian Kernighan特彆感謝: 朋友和衕事的耐心和寬容他, 讓他慢慢地梳理本身的寫作思路.
|
||||
衕時感謝他的妻子 Meg 和其他很多朋友對他寫作事業的支持.
|
||||
Brian Kernighan特別感謝: 朋友和同事的耐心和寬容他, 讓他慢慢地梳理本身的寫作思路.
|
||||
同時感謝他的妻子 Meg 和其他很多朋友對他寫作事業的支持.
|
||||
|
||||
2015年 10月 於 紐約
|
||||
|
||||
|
@ -1,6 +1,6 @@
|
||||
## 1.1. Hello, World
|
||||
|
||||
我們以1978年,c語言歷史上經典的hello world案例來開始吧。C語言對Go語言的設計產生了很多影響。用這個例子,我們來講解一些Go語言的覈心特性:
|
||||
我們以1978年,c語言曆史上經典的hello world案例來開始吧。C語言對Go語言的設計產生了很多影響。用這個例子,我們來講解一些Go語言的覈心特性:
|
||||
|
||||
```go
|
||||
//gopl.io/ch1/helloworld
|
||||
@ -13,7 +13,7 @@ func main() {
|
||||
}
|
||||
```
|
||||
|
||||
Go是一門編譯型語言,Go的工具鏈將源代碼和其依賴一起打包,生成機器的本地指令(譯註:靜態編譯)。Go語言提供的工具可以通過go下的一繫列子命令來調用。最簡單的一個子命令就是run。這個命令會將一個或多個以.go結束的源文件,和關聯庫鏈接到一起,然後運行最終的可執行文件。(本書將用$錶示命令行的提示符)
|
||||
Go是一門編譯型語言,Go的工具鏈將源代碼和其依賴一起打包,生成機器的本地指令(譯註:靜態編譯)。Go語言提供的工具可以通過go下的一繫列子命令來調用。最簡單的一個子命令就是run。這個命令會將一個或多個以.go結束的源文件,和關聯庫鏈接到一起,然後運行最終的可執行文件。(本書將用$表示命令行的提示符)
|
||||
|
||||
```
|
||||
$ go run helloworld.go
|
||||
@ -25,13 +25,13 @@ Hello, BF
|
||||
|
||||
Go原生支持Unicode,所以你可以用Go處理世界上的任何語言。
|
||||
|
||||
如果你希望自己的程序不隻是簡單的一次性實驗,那麼你一定會希望能夠編譯這個程序,併且能夠將編譯結果保存下來以備將來之用。這個可以用build子命令來實現:
|
||||
如果你希望自己的程序不隻是簡單的一次性實驗,那麽你一定會希望能夠編譯這個程序,並且能夠將編譯結果保存下來以備將來之用。這個可以用build子命令來實現:
|
||||
```
|
||||
$ go build helloworld.go
|
||||
```
|
||||
這會創建一個名為helloworld的可執行的二進製文件,之後你可以在任何時間去運行這個二進製文件,不需要其它的任何處理(譯註:因為是靜態編譯,所以也不用擔心在繫統庫更新的時候衝突,倖福感滿滿)。
|
||||
這會創建一個名爲helloworld的可執行的二進製文件,之後你可以在任何時間去運行這個二進製文件,不需要其它的任何處理(譯註:因爲是靜態編譯,所以也不用擔心在繫統庫更新的時候沖突,倖福感滿滿)。
|
||||
|
||||
下麫是運行我們的編譯結果樣例:
|
||||
下面是運行我們的編譯結果樣例:
|
||||
```
|
||||
$ ./helloworld
|
||||
Hello, BF
|
||||
@ -39,28 +39,28 @@ Hello, BF
|
||||
|
||||
本書中我們所有的例子都做了一個特殊標記,你可以通過這些標記在gopl.io在綫網站上找到這些樣例代碼,比如這個 gopl.io/ch1/helloworld
|
||||
|
||||
如果你執行go get gopl.io/ch1/helloworld,go能夠自己從網上穫取到這些代碼,併且將這些代碼放到對應的目彔中。更詳細的介紹在2.6和10.7章節中。
|
||||
如果你執行go get gopl.io/ch1/helloworld,go能夠自己從網上獲取到這些代碼,並且將這些代碼放到對應的目録中。更詳細的介紹在2.6和10.7章節中。
|
||||
|
||||
我們來討論一下程序本身。Go的代碼是用package來組織的,package的概唸和你知道的其它語言裏的libraries或者modules比較類似。一個package會包含一個或多個.go結束的源代碼文件。每一個源文件都是以一個package xxx的聲明開頭的,比如我們的例子裏就是package main。這行聲明錶示該文件是屬於哪一個package,緊跟着是一繫列import的package名,錶示這個文件中引入的package。再之後是本文件本身的代碼
|
||||
我們來討論一下程序本身。Go的代碼是用package來組織的,package的概唸和你知道的其它語言裡的libraries或者modules比較類似。一個package會包含一個或多個.go結束的源代碼文件。每一個源文件都是以一個package xxx的聲明開頭的,比如我們的例子裡就是package main。這行聲明表示該文件是屬於哪一個package,緊跟着是一繫列import的package名,表示這個文件中引入的package。再之後是本文件本身的代碼
|
||||
|
||||
Go的標準庫已經提供了100多個package,用來完成一門程序語言的一些基本任務,比如輸入、輸齣、排序或者字符串/文本處理。比如fmt這個package,就包括接收輸入、格式化輸齣的各種函數。Println是其中的一個函數,可以用這個函數來打印一個或多個值,該函數會將這些參數用空格隔開進行輸齣,併在輸齣完畢之後在行末加上一個換行符。
|
||||
Go的標準庫已經提供了100多個package,用來完成一門程序語言的一些基本任務,比如輸入、輸齣、排序或者字符串/文本處理。比如fmt這個package,就包括接收輸入、格式化輸齣的各種函數。Println是其中的一個函數,可以用這個函數來打印一個或多個值,該函數會將這些參數用空格隔開進行輸齣,並在輸齣完畢之後在行末加上一個換行符。
|
||||
|
||||
package main比較特殊。這個package裏會定義一個獨立的程序,這個程序是可以運行的,而不是像其它package一樣的library。在main這個package裏,main函數也是一個特殊的函數,這是我們整個程序的入口(譯註:其實c繫語言差不多都是這樣)。main函數所做的事情就是我們程序做的事情。當然了,main函數一般完成的工作是調用其它packge裏的函數來完成自己的工作,比如fmt.Println。
|
||||
package main比較特殊。這個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語言必鬚引入所有用到的package的原則,假如你沒有在代碼裡import需要用到的package,程序將無法編譯通過,當你import了沒有用到的package,也會無法編譯通過(譯註:爭議特性之一)。
|
||||
|
||||
import聲明必鬚跟在文件的package聲明之後。在import之後,則是各種方法、變量、常量、類型的聲明(分彆用關鍵字func, var, const, type來進行定義)。這些內容的聲明順序併沒有什麼規定,可以隨便(譯註:最好還是定一下規範)。我們例子裏的程序比較簡單,隻包含了一個函數。併且在該函數裏也隻調用了一個其它函數。為了節省空間,有些時候的例子我們會省略package和import聲明,但是讀者需要註意這些聲明是一定要包含在源文件裏的。
|
||||
import聲明必鬚跟在文件的package聲明之後。在import之後,則是各種方法、變量、常量、類型的聲明(分別用關鍵字func, var, const, type來進行定義)。這些內容的聲明順序並沒有什麽規定,可以隨便(譯註:最好還是定一下規範)。我們例子裡的程序比較簡單,隻包含了一個函數。並且在該函數裡也隻調用了一個其它函數。爲了節省空間,有些時候的例子我們會省略package和import聲明,但是讀者需要註意這些聲明是一定要包含在源文件裡的。
|
||||
|
||||
一個函數的聲明包含func這個關鍵字、函數名、參數列錶(我們例子裏的main函數是空)、返迴結果列錶(這裏的例子也是空)以及包含在大括號裏的函數體。關於函數的更詳細描述在第五章。
|
||||
一個函數的聲明包含func這個關鍵字、函數名、參數列表(我們例子裡的main函數是空)、返迴結果列表(這裡的例子也是空)以及包含在大括號裡的函數體。關於函數的更詳細描述在第五章。
|
||||
|
||||
Go是一門不需要分號作為語句或者聲明結束的語言,除非要在一行中將多個語句、聲明隔開。然而在編譯時,編譯器會主動在一些特定的符號(譯註:比如行末是,一個標識符、一個整數、浮點數、虛數、字符或字符串文字、關鍵字break、continue、fallthrough或return中的一個、運算符和分隔符++、--、)、]或}中的一個) 後添加分號,所以在哪裏加分號閤適是取決於Go的代碼的。例如:在Go語言中的函數聲明和 { 必鬚在衕一行,而在x + y的錶達式中,在+號後換行可以,但是在+號前換行則會有問題。
|
||||
Go是一門不需要分號作爲語句或者聲明結束的語言,除非要在一行中將多個語句、聲明隔開。然而在編譯時,編譯器會主動在一些特定的符號(譯註:比如行末是,一個標識符、一個整數、浮點數、虛數、字符或字符串文字、關鍵字break、continue、fallthrough或return中的一個、運算符和分隔符++、--、)、]或}中的一個) 後添加分號,所以在哪裡加分號合適是取決於Go的代碼的。例如:在Go語言中的函數聲明和 { 必鬚在同一行,而在x + y的表達式中,在+號後換行可以,但是在+號前換行則會有問題。
|
||||
|
||||
Go語言在代碼格式上採取了很強硬的態度。gofmt工具會將你的代碼格式化為標準格式,併且go工具中的fmt子命令會自動對特定package下的所有.go源文件應用gofmt。如果不指定package,則默認對當前目彔下的源文件進行格式化。本書中的所有代碼已經是執行過gofmt後的標準格式代碼。你應該在自己的代碼上也執行這種格式化。規定一種標準的代碼格式可以規避掉無盡的無意義的撕逼。當然了,也可以避免由於代碼格式導緻的邏輯上的歧義。
|
||||
Go語言在代碼格式上採取了很強硬的態度。gofmt工具會將你的代碼格式化爲標準格式,並且go工具中的fmt子命令會自動對特定package下的所有.go源文件應用gofmt。如果不指定package,則默認對當前目録下的源文件進行格式化。本書中的所有代碼已經是執行過gofmt後的標準格式代碼。你應該在自己的代碼上也執行這種格式化。規定一種標準的代碼格式可以規避掉無儘的無意義的撕逼。當然了,也可以避免由於代碼格式導緻的邏輯上的歧義。
|
||||
|
||||
|
||||
很多文本編輯器都可以設置為保存文件時自動執行gofmt,所以你的源代碼應該總是會被格式化。這裏還有一個相關的工具,goimports,會自動地添加你代碼裏需要用到的import聲明以及需要移除的import聲明。這個工具併沒有包含在標準的分髮包中,然而你可以自行安裝:
|
||||
很多文本編輯器都可以設置爲保存文件時自動執行gofmt,所以你的源代碼應該總是會被格式化。這裡還有一個相關的工具,goimports,會自動地添加你代碼裡需要用到的import聲明以及需要移除的import聲明。這個工具並沒有包含在標準的分發包中,然而你可以自行安裝:
|
||||
```
|
||||
$ go get golang.org/x/tools/cmd/goimports
|
||||
```
|
||||
|
@ -1,13 +1,13 @@
|
||||
## 1.2. 命令行參數
|
||||
大多數的程序都是處理輸入,產生輸齣;這也正是“計算”的定義。但是一個程序要如何穫取輸入呢?一些程序會生成自己的數據,但通常情況下,輸入都來自於程序外部:比如文件、網絡連接、其它程序的輸齣、用戶的鍵盤、命令行的參數或其它類似輸入源。下麫幾個例子會討論其中的一些輸入類型,首先是命令行參數。
|
||||
大多數的程序都是處理輸入,產生輸齣;這也正是“計算”的定義。但是一個程序要如何獲取輸入呢?一些程序會生成自己的數據,但通常情況下,輸入都來自於程序外部:比如文件、網絡連接、其它程序的輸齣、用戶的鍵盤、命令行的參數或其它類似輸入源。下面幾個例子會討論其中的一些輸入類型,首先是命令行參數。
|
||||
|
||||
os這個package提供了操作繫統無關(跨平颱)的,與繫統交互的一些函數和相關的變量,運行時程序的命令行參數可以用一個叫os包中的Args這個變量來穫取;在外部需要使用該變量時,需要用os.Args來訪問。
|
||||
os這個package提供了操作繫統無關(跨平檯)的,與繫統交互的一些函數和相關的變量,運行時程序的命令行參數可以用一個叫os包中的Args這個變量來獲取;在外部需要使用該變量時,需要用os.Args來訪問。
|
||||
|
||||
os.Args這個變量是一個字符串(string)的slice,slice在go語言裏是一個基礎的數據結構,之後我們很快會提到。現在可以先把slice當一個簡單的元素序列,可以用類似s[i]的下標訪問形式穫取其內容,併且可以用形如s[m:n]的形式來穫取到一個slice的子集(譯註:和python裏的差不多)。其長度可以用len(s)函數來穫取。和其它大多數語言差不多,go語言裏的這種索引形式也採用了開區間,包括m~n的第一個元素,但不包括最後那個元素(譯註:比如a = [1, 2, 3, 4, 5], a[0: 3] =[1, 2, 3],不包含最後一個元素)。這樣可以簡化我們的邏輯。比如s[m:n]這個slice,0 ≤ m ≤ n ≤ len(s),包含n-m個元素。
|
||||
os.Args這個變量是一個字符串(string)的slice,slice在go語言裡是一個基礎的數據結構,之後我們很快會提到。現在可以先把slice當一個簡單的元素序列,可以用類似s[i]的下標訪問形式獲取其內容,並且可以用形如s[m:n]的形式來獲取到一個slice的子集(譯註:和python裡的差不多)。其長度可以用len(s)函數來獲取。和其它大多數語言差不多,go語言裡的這種索引形式也採用了開區間,包括m~n的第一個元素,但不包括最後那個元素(譯註:比如a = [1, 2, 3, 4, 5], a[0: 3] =[1, 2, 3],不包含最後一個元素)。這樣可以簡化我們的邏輯。比如s[m:n]這個slice,0 ≤ m ≤ n ≤ len(s),包含n-m個元素。
|
||||
|
||||
os.Args的第一個元素,卽os.Args[0]是命令行執行時的命令本身;其它的元素則是執行該命令時傳給這個程序的參數。前麫提到的切片錶達式,s[m:n]會返迴第m到第n-1個元素,所以下一個例子裏需要用到的os.Args[1:len(os.Args)]卽是除了命令本身外的所有傳入參數。如果我們省略s[m:n]裏的m和n,那麼默認這個錶達式會填入0:len(s),所以這裏我們還可以省略掉n,寫os.Args[1:]。
|
||||
os.Args的第一個元素,卽os.Args[0]是命令行執行時的命令本身;其它的元素則是執行該命令時傳給這個程序的參數。前面提到的切片表達式,s[m:n]會返迴第m到第n-1個元素,所以下一個例子裡需要用到的os.Args[1:len(os.Args)]卽是除了命令本身外的所有傳入參數。如果我們省略s[m:n]裡的m和n,那麽默認這個表達式會填入0:len(s),所以這裡我們還可以省略掉n,寫os.Args[1:]。
|
||||
|
||||
下麫是一個Unix裏echo命令的實現,這個命令會在單行內打印齣命令行參數。這個程序import了兩個package,併且用括號把這兩個package包了起來,這是分彆import各個package聲明的簡化寫法。當然了你分開來寫import也沒有什麼問題,隻是一般為了方便我們都會像下麫這樣來導入多個package。我們自己寫的導入順序併不重要,因為gofmt工具會幫助我們按照字母順序來排列好這些導入包名。(本書中如果一個例子有多種版本時,我們會用編號標記齣來)
|
||||
下面是一個Unix裡echo命令的實現,這個命令會在單行內打印齣命令行參數。這個程序import了兩個package,並且用括號把這兩個package包了起來,這是分別import各個package聲明的簡化寫法。當然了你分開來寫import也沒有什麽問題,隻是一般爲了方便我們都會像下面這樣來導入多個package。我們自己寫的導入順序並不重要,因爲gofmt工具會幫助我們按照字母順序來排列好這些導入包名。(本書中如果一個例子有多種版本時,我們會用編號標記齣來)
|
||||
```go
|
||||
gopl.io/ch1/echo1
|
||||
// Echo1 prints its command-line arguments.
|
||||
@ -25,25 +25,25 @@ func main() {
|
||||
fmt.Println(s)
|
||||
}
|
||||
```
|
||||
Go裏的註釋是以//來錶示。//後的內容一直到行末都是這條註釋的一部分,併且這些註釋會被編譯器忽略。
|
||||
Go裡的註釋是以//來表示。//後的內容一直到行末都是這條註釋的一部分,並且這些註釋會被編譯器忽略。
|
||||
|
||||
按照慣例,我們會在每一個package前麫放上這個package的詳盡的註釋對其進行說明;對於一個main package來說,一般這段評論會包含幾句話來說明這個項目/程序整體是做什麼用的。
|
||||
按照慣例,我們會在每一個package前面放上這個package的詳儘的註釋對其進行說明;對於一個main package來說,一般這段評論會包含幾句話來說明這個項目/程序整體是做什麽用的。
|
||||
|
||||
var關鍵字用來做變量聲明。這裏聲明了s和sep兩個string變量。變量可以在聲明期間直接進行初始化。如果沒有顯式地初始化的話,Go會隱式地給這些未初始化的變量賦予對應其類型的零值,比如數值類型就是0,字符串類型就是“”空字符串。在這個例子裏的s和sep被隱式地賦值為了空字符串。在第2章中我們會更詳細地講解變量和聲明。
|
||||
var關鍵字用來做變量聲明。這裡聲明了s和sep兩個string變量。變量可以在聲明期間直接進行初始化。如果沒有顯式地初始化的話,Go會隱式地給這些未初始化的變量賦予對應其類型的零值,比如數值類型就是0,字符串類型就是“”空字符串。在這個例子裡的s和sep被隱式地賦值爲了空字符串。在第2章中我們會更詳細地講解變量和聲明。
|
||||
|
||||
對於數字類型,Go語言提供了常規的數值計算和邏輯運算符。而對於string類型,+號錶示字符串的連接(譯註:和C++或者js是一樣的)。所以下麫這個錶達式:
|
||||
對於數字類型,Go語言提供了常規的數值計算和邏輯運算符。而對於string類型,+號表示字符串的連接(譯註:和C++或者js是一樣的)。所以下面這個表達式:
|
||||
|
||||
```go
|
||||
sep + os.Args[i]
|
||||
```
|
||||
|
||||
錶示將sep字符串和os.Args[i]字符串進行連接。我們在程序裏用的另外一個錶達式:
|
||||
表示將sep字符串和os.Args[i]字符串進行連接。我們在程序裡用的另外一個表達式:
|
||||
|
||||
```go
|
||||
s += sep + os.Args[i]
|
||||
```
|
||||
|
||||
會將sep與os.Args[i]連接,然後再將得到的結果與s進行連接,這種方式和下麫的錶達是等價的:
|
||||
會將sep與os.Args[i]連接,然後再將得到的結果與s進行連接,這種方式和下面的表達是等價的:
|
||||
|
||||
```go
|
||||
s = s + sep + os.Args[i]
|
||||
@ -51,24 +51,24 @@ s = s + sep + os.Args[i]
|
||||
|
||||
運算符+=是一個賦值運算符(assignment operator),每一種數值和邏輯運算符,例如*或者+都有其對應的賦值運算符。
|
||||
|
||||
echo程序可以每循環一次輸齣一個參數,不過我們這裏的版本是不斷地將其結果連接到一個字符串的末尾。s這個字符串在聲明的時候是一個空字符串,而之後循環每次都會被在末尾添加一段字符串;第一次迭代之後,一個空格會被插入到字符串末尾,所以每插入一個新值,都會和前一個中間有一個空格隔開。這是一種非綫性的操作,當我們的參數數量變得龐大的時候(當然不是說這裏的echo,一般echo也不會有太多參數)其運行開銷也會變得龐大。下麫我們會介紹一繫列的echo改進版,來應對這裏說到的運行效率低下。
|
||||
echo程序可以每循環一次輸齣一個參數,不過我們這裡的版本是不斷地將其結果連接到一個字符串的末尾。s這個字符串在聲明的時候是一個空字符串,而之後循環每次都會被在末尾添加一段字符串;第一次迭代之後,一個空格會被插入到字符串末尾,所以每插入一個新值,都會和前一個中間有一個空格隔開。這是一種非綫性的操作,當我們的參數數量變得龐大的時候(當然不是說這裡的echo,一般echo也不會有太多參數)其運行開銷也會變得龐大。下面我們會介紹一繫列的echo改進版,來應對這裡說到的運行效率低下。
|
||||
|
||||
在for循環中,我們用到了i來做下標索引,可以看到我們用了:=符號來給i進行初始化和賦值,這是var xxx=yyy的一種簡寫形式,Go會根據等號右邊的值的類型自動判斷左邊的值類型,下一章會對這一點進行詳細說明。
|
||||
|
||||
自增錶達式i++會為i加上1;這個i += 1以及i = i + 1都是等價的。對應的還有i--是給i減去1。這些在go語言裏是語句,而不像C繫的其它語言裏是錶達式。所以在Go語言裏j = i++是非法的,而且++和--都隻能放在變量名後麫,因此--i也是非法的。
|
||||
自增表達式i++會爲i加上1;這個i += 1以及i = i + 1都是等價的。對應的還有i--是給i減去1。這些在go語言裡是語句,而不像C繫的其它語言裡是表達式。所以在Go語言裡j = i++是非法的,而且++和--都隻能放在變量名後面,因此--i也是非法的。
|
||||
|
||||
在Go語言裏隻有for循環一種循環。當然了為了滿足需求,Go的for循環有很多種形式,下麫是其中的一種:
|
||||
在Go語言裡隻有for循環一種循環。當然了爲了滿足需求,Go的for循環有很多種形式,下面是其中的一種:
|
||||
```go
|
||||
for initialization; condition; post {
|
||||
// zero or more statements
|
||||
}
|
||||
```
|
||||
|
||||
這裏需要註意,for循環的兩邊是不需要像其它語言一樣寫括號的。併且左大括號需要和for語句在衕一行。
|
||||
這裡需要註意,for循環的兩邊是不需要像其它語言一樣寫括號的。並且左大括號需要和for語句在同一行。
|
||||
|
||||
initialization部分是可選的,如果你寫了這部分的話,在for循環之前這部分的邏輯會被執行。需要註意的是這部分必鬚是一個簡單的語句,也就是說是一個簡短的變量聲明,一個賦值語句,或是一個函數調用。condition部分必鬚是一個結果為boolean值的錶達式,在每次循環之前,語言都會檢査當前是否滿足這個條件,如果不滿足的話便會結束循環;post部分的語句則是在每次循環結束之後被執行,之後conditon部分會在下一次執行前再被執行,依此往復。當condition條件裏的判斷結果變為false之後,循環卽結束。
|
||||
initialization部分是可選的,如果你寫了這部分的話,在for循環之前這部分的邏輯會被執行。需要註意的是這部分必鬚是一個簡單的語句,也就是說是一個簡短的變量聲明,一個賦值語句,或是一個函數調用。condition部分必鬚是一個結果爲boolean值的表達式,在每次循環之前,語言都會檢査當前是否滿足這個條件,如果不滿足的話便會結束循環;post部分的語句則是在每次循環結束之後被執行,之後conditon部分會在下一次執行前再被執行,依此往復。當condition條件裡的判斷結果變爲false之後,循環卽結束。
|
||||
|
||||
上麫提到是for循環裏的三個部分都是可以被省略的,如果你把initialization和post部分都省略的話,那麼連中間隔離他們的分號也是可以被省略的,比如下麫這種for循環,就和傳統的while循環是一樣的:
|
||||
上面提到是for循環裡的三個部分都是可以被省略的,如果你把initialization和post部分都省略的話,那麽連中間隔離他們的分號也是可以被省略的,比如下面這種for循環,就和傳統的while循環是一樣的:
|
||||
|
||||
```go
|
||||
// a traditional "while" loop
|
||||
@ -77,7 +77,7 @@ for condition {
|
||||
}
|
||||
```
|
||||
|
||||
當然了,如果你連唯一的條件都省了,那麼for循環就會變成一個無限循環,像下麫這樣:
|
||||
當然了,如果你連唯一的條件都省了,那麽for循環就會變成一個無限循環,像下面這樣:
|
||||
```go
|
||||
// a traditional infinite loop
|
||||
for {
|
||||
@ -87,7 +87,7 @@ for {
|
||||
|
||||
在無限循環中,你還是可以靠break或者return來終止掉循環。
|
||||
|
||||
如果你的遍歷對象是string或者slice裏的值的話,還有另外一種循環的寫法,我們來看看另一個版本的echo:
|
||||
如果你的遍曆對象是string或者slice裡的值的話,還有另外一種循環的寫法,我們來看看另一個版本的echo:
|
||||
|
||||
```go
|
||||
gopl.io/ch1/echo2
|
||||
@ -108,12 +108,12 @@ func main() {
|
||||
}
|
||||
```
|
||||
|
||||
每一次循環迭代,range都會返迴一對結果;當前迭代的下標以及在該下標處的元素的值。在這個例子裏,我們不需要這個下標,但是因為range的處理要求我們必鬚要衕時處理下標和值。我們可以在這裏聲明一個接收index的臨時變量來解決這個問題,但是go語言又不允許隻聲明而在後續代碼裏不使用這個變量,如果你這樣做了編譯器會返迴一個編譯錯誤。
|
||||
每一次循環迭代,range都會返迴一對結果;當前迭代的下標以及在該下標處的元素的值。在這個例子裡,我們不需要這個下標,但是因爲range的處理要求我們必鬚要同時處理下標和值。我們可以在這裡聲明一個接收index的臨時變量來解決這個問題,但是go語言又不允許隻聲明而在後續代碼裡不使用這個變量,如果你這樣做了編譯器會返迴一個編譯錯誤。
|
||||
|
||||
在Go語言中,應對這種情況的解決方法是用空白標識符,對,就是上麫那個下劃綫_。空白標識符可以在任何你接收自己不需要處理的值時使用。在這裏,我們用他來忽略掉range返迴的那個沒用的下標值。大多數的Go程序員都會像上麫這樣來寫類似的os.Args遍歷,可以避免錯誤的下標引用。(這裏可能有翻譯錯,附上原文)
|
||||
在Go語言中,應對這種情況的解決方法是用空白標識符,對,就是上面那個下劃綫_。空白標識符可以在任何你接收自己不需要處理的值時使用。在這裡,我們用他來忽略掉range返迴的那個沒用的下標值。大多數的Go程序員都會像上面這樣來寫類似的os.Args遍曆,可以避免錯誤的下標引用。(這裡可能有翻譯錯,附上原文)
|
||||
Most Go programmers would likely use range and _ to write the echo program as above, since the indexing over os.Args is implicit, not explicit, and thus easier to get right.
|
||||
|
||||
上麫這個版本將s和sep的聲明和初始化都放到了一起,但是我們可以等價地將聲明和賦值分開來寫,下麫這些寫法都是等價的
|
||||
上面這個版本將s和sep的聲明和初始化都放到了一起,但是我們可以等價地將聲明和賦值分開來寫,下面這些寫法都是等價的
|
||||
|
||||
```go
|
||||
s := ""
|
||||
@ -122,12 +122,12 @@ var s = ""
|
||||
var s string = ""
|
||||
```
|
||||
|
||||
那麼這些等價的形式應該怎麼做選擇呢?這裏提供一些建議:第一種形式,最好隻用在一個函數內部,而package級彆的變量,請不要使用這樣的聲明方式。第二種形式依賴於string類型的內部初始化機製,被初始化為空字符串。第三種形式使用得很少,除非衕時聲明多個變量。第四種形式會顯式地標明變量的類型,在多變量衕時聲明時可以用到。實踐中你應該隻使用上麫的前兩種形式,顯式地指定變量的類型,讓編譯器自己去初始化其值,或者直接用隱式初始化,錶明初始值怎麼樣併不重要。
|
||||
那麽這些等價的形式應該怎麽做選擇呢?這裡提供一些建議:第一種形式,最好隻用在一個函數內部,而package級別的變量,請不要使用這樣的聲明方式。第二種形式依賴於string類型的內部初始化機製,被初始化爲空字符串。第三種形式使用得很少,除非同時聲明多個變量。第四種形式會顯式地標明變量的類型,在多變量同時聲明時可以用到。實踐中你應該隻使用上面的前兩種形式,顯式地指定變量的類型,讓編譯器自己去初始化其值,或者直接用隱式初始化,表明初始值怎麽樣並不重要。
|
||||
|
||||
像上麫提到的,每次循環中字符串s都會得到一個新內容。+=語句會分配一個新的字符串,併將老字符串連接起來的值賦予給它。而目標字符串的老字麫值在得到新值以後就失去了用處,這些臨時值會被go的垃圾收集器幹掉。
|
||||
像上面提到的,每次循環中字符串s都會得到一個新內容。+=語句會分配一個新的字符串,並將老字符串連接起來的值賦予給它。而目標字符串的老字面值在得到新值以後就失去了用處,這些臨時值會被go的垃圾收集器乾掉。
|
||||
|
||||
|
||||
如果不斷連接的數據量很大,那麼上麫這種操作就是成本非常高的操作。更簡單併且有效的一種方式是使用字符串的Join函數,像下麫這樣:
|
||||
如果不斷連接的數據量很大,那麽上面這種操作就是成本非常高的操作。更簡單並且有效的一種方式是使用字符串的Join函數,像下面這樣:
|
||||
|
||||
```go
|
||||
gopl.io/ch1/echo3
|
||||
@ -136,19 +136,19 @@ func main() {
|
||||
}
|
||||
```
|
||||
|
||||
最後,如果我們對輸齣的格式也不是很關心,隻是想簡單地輸齣值得的話,還可以像下麫這麼寫,Println函數會為我們自動格式化輸齣。
|
||||
最後,如果我們對輸齣的格式也不是很關心,隻是想簡單地輸齣值得的話,還可以像下面這麽寫,Println函數會爲我們自動格式化輸齣。
|
||||
|
||||
```go
|
||||
fmt.Println(os.Args[1:])
|
||||
```
|
||||
|
||||
這個輸齣結果和前麫的string.Join得到的結果很相似,隻是被自動地放到了一個括號裏,對slice調用Println函數都會被打印成這樣形式的結果。
|
||||
這個輸齣結果和前面的string.Join得到的結果很相似,隻是被自動地放到了一個括號裡,對slice調用Println函數都會被打印成這樣形式的結果。
|
||||
|
||||
|
||||
下麫是幾道練習題:
|
||||
下面是幾道練習題:
|
||||
|
||||
```
|
||||
Exercise 1.1:脩改echo程序,使其能夠打印os.Args[0]。
|
||||
Exercise 1.2:脩改echo程序,使其打印value和index,每個value和index顯示一行。
|
||||
Exercise 1.3:上手實踐前麫提到的strings.Join和直接Println,併觀察輸齣結果的區彆。
|
||||
Exercise 1.3:上手實踐前面提到的strings.Join和直接Println,並觀察輸齣結果的區別。
|
||||
```
|
||||
|
@ -1,8 +1,8 @@
|
||||
## 1.3. 査找重復的行
|
||||
|
||||
文件拷貝、文件打印、文件蒐索、文件排序、文件統計類的程序一般都會有比較相似的程序結構:處理輸入的一個循環,在每一個輸入元素上執行計算處理,在處理的衕時或者處理完成之後進行結果輸齣。我們會展示一個叫dup程序的三個版本;這個程序的靈感來自於linux的uniq命令,我們的程序將會找到相鄰的重復的行。這個程序提供的模式可以很方便地被脩改來完成不衕的需求。
|
||||
文件拷貝、文件打印、文件蒐索、文件排序、文件統計類的程序一般都會有比較相似的程序結構:處理輸入的一個循環,在每一個輸入元素上執行計算處理,在處理的同時或者處理完成之後進行結果輸齣。我們會展示一個叫dup程序的三個版本;這個程序的靈感來自於linux的uniq命令,我們的程序將會找到相鄰的重復的行。這個程序提供的模式可以很方便地被脩改來完成不同的需求。
|
||||
|
||||
第一個版本的dup會輸齣標準輸入流中的齣現多次的行,在行內容前會有其齣現次數的計數。這個程序將引入if錶達式,map內置數據結果和bufio的package。
|
||||
第一個版本的dup會輸齣標準輸入流中的齣現多次的行,在行內容前會有其齣現次數的計數。這個程序將引入if表達式,map內置數據結果和bufio的package。
|
||||
|
||||
```go
|
||||
gopl.io/ch1/dup1
|
||||
@ -31,21 +31,21 @@ func main() {
|
||||
}
|
||||
```
|
||||
|
||||
和我們前麫提到的for循環一樣,在if條件的兩邊,我們也不需要加括號,但是if錶達式後的邏輯體的花括號是不能省略的。如果需要的話,像其它語言一樣,這個錶達式也可以有else部分,這部分邏輯會在if中的條件結果為false時被執行。
|
||||
和我們前面提到的for循環一樣,在if條件的兩邊,我們也不需要加括號,但是if表達式後的邏輯體的花括號是不能省略的。如果需要的話,像其它語言一樣,這個表達式也可以有else部分,這部分邏輯會在if中的條件結果爲false時被執行。
|
||||
|
||||
map是go語言內置的key/value數據結構,這個數據結構能夠提供常數時間的存儲、穫取、測試操作。key可以是任意數據類型,隻要該類型能夠用==來進行比較,string是最常用的key類型。而value類型的範圍就更大了,基本上什麼類型都是可以的。這個例子中的key都是string類型,value用的是int類型。我們用內置make函數來創建一個空的map,當然了,make方法還可以有彆的用處。在4.3章中我們還會對map進行更深度的討論。
|
||||
map是go語言內置的key/value數據結構,這個數據結構能夠提供常數時間的存儲、獲取、測試操作。key可以是任意數據類型,隻要該類型能夠用==來進行比較,string是最常用的key類型。而value類型的範圍就更大了,基本上什麽類型都是可以的。這個例子中的key都是string類型,value用的是int類型。我們用內置make函數來創建一個空的map,當然了,make方法還可以有別的用處。在4.3章中我們還會對map進行更深度的討論。
|
||||
|
||||
|
||||
dup程序每次讀取輸入的一行,這一行的內容會被當做一個map的key,而其value值會被+1。counts[input.Text()]++這個語句和下麫的兩句是等價的:
|
||||
dup程序每次讀取輸入的一行,這一行的內容會被當做一個map的key,而其value值會被+1。counts[input.Text()]++這個語句和下面的兩句是等價的:
|
||||
|
||||
```go
|
||||
line := input.Text()
|
||||
counts[line] = counts[line] + 1
|
||||
```
|
||||
|
||||
當然了,在這個例子裏我們併不用擔心map在沒有當前的key時就對其進行++操作會有什麼問題,因為go語言在碰到這種情況時,會自動將其初始化為0,然後再進行操作。
|
||||
當然了,在這個例子裡我們並不用擔心map在沒有當前的key時就對其進行++操作會有什麽問題,因爲go語言在碰到這種情況時,會自動將其初始化爲0,然後再進行操作。
|
||||
|
||||
在這裏我們又用了一個range的循環來打印結果,這次range是被用在map這個數據結果上。這一次的情況和上次比較類型,range會返迴兩個值,一個key和在map對應這個key的value。對map進行range循環時,其順序是不確定的,從實踐來看,很可能每次運行都會有不一樣的結果(譯註:這是go的設計者有意為之的,因為其底層實現不保證插入順序和遍歷順序一緻,而希望程序員不要依賴遍歷時的順序,所以幹脆直接在遍歷的時候做了隨機化處理,醉了),來避免程序員在業務中依賴遍歷時的順序。
|
||||
在這裡我們又用了一個range的循環來打印結果,這次range是被用在map這個數據結果上。這一次的情況和上次比較類型,range會返迴兩個值,一個key和在map對應這個key的value。對map進行range循環時,其順序是不確定的,從實踐來看,很可能每次運行都會有不一樣的結果(譯註:這是go的設計者有意爲之的,因爲其底層實現不保証插入順序和遍曆順序一緻,而希望程序員不要依賴遍曆時的順序,所以乾脆直接在遍曆的時候做了隨機化處理,醉了),來避免程序員在業務中依賴遍曆時的順序。
|
||||
|
||||
然後輪到我們例子中的bufio這個package了,這個package主要的目的是幫助我們更方便有效地處理程序的輸入和輸齣。而這個包最有用的一個特性就是其中的一個Scanner類型,用它可以簡單地接收輸入,或者把輸入打散成行或者單詞;這個類型通常是處理行形式的輸入最簡單的方法了。
|
||||
|
||||
@ -55,18 +55,18 @@ counts[line] = counts[line] + 1
|
||||
input := bufio.NewScanner(os.Stdin)
|
||||
```
|
||||
|
||||
scanner對象可以從程序的標準輸入中讀取內容。對input.Scanner的每一次調用都會調入一個新行,併且會自動將其行末的換行符去掉;其結果可以用input.Text()得到。Scan方法在讀到了新行的時候會返迴true,而在沒有新行被讀入時,會返迴false。
|
||||
scanner對象可以從程序的標準輸入中讀取內容。對input.Scanner的每一次調用都會調入一個新行,並且會自動將其行末的換行符去掉;其結果可以用input.Text()得到。Scan方法在讀到了新行的時候會返迴true,而在沒有新行被讀入時,會返迴false。
|
||||
|
||||
例子中還有一個fmt.Printf,這個函數和C繫的其它語言裏的那個printf函數差不多,都是格式化輸齣的方法。fmt.Printf的第一個參數卽是輸齣內容的格式規約,每一個參數如果格式化是取決於在格式化字符串裏齣現的“轉換字符”,這個字符串是跟着%號後的一個字母。比如%d錶示以一個整數的形式來打印一個變量,而%s,則錶示以string形式來打印一個變量。
|
||||
例子中還有一個fmt.Printf,這個函數和C繫的其它語言裡的那個printf函數差不多,都是格式化輸齣的方法。fmt.Printf的第一個參數卽是輸齣內容的格式規約,每一個參數如果格式化是取決於在格式化字符串裡齣現的“轉換字符”,這個字符串是跟着%號後的一個字母。比如%d表示以一個整數的形式來打印一個變量,而%s,則表示以string形式來打印一個變量。
|
||||
|
||||
Printf有一大堆這種轉換,Go程序員把這些叫做verb(動詞)。下麫的錶格列齣了常用的動詞,當然了不是全部,但基本也夠用了。
|
||||
Printf有一大堆這種轉換,Go程序員把這些叫做verb(動詞)。下面的表格列齣了常用的動詞,當然了不是全部,但基本也夠用了。
|
||||
|
||||
```
|
||||
%d int變量
|
||||
%x, %o, %b 分彆為16進製,8進製,2進製形式的int
|
||||
%x, %o, %b 分別爲16進製,8進製,2進製形式的int
|
||||
%f, %g, %e 浮點數: 3.141593 3.141592653589793 3.141593e+00
|
||||
%t 佈爾變量:true 或 false
|
||||
%c rune (Unicode code point),go語言裏特有的Unicode字符類型
|
||||
%c rune (Unicode code point),go語言裡特有的Unicode字符類型
|
||||
%s string
|
||||
%q quoted string "abc" or rune 'c'
|
||||
%v 會將任意變量以易讀的形式打印齣來
|
||||
@ -74,9 +74,9 @@ Printf有一大堆這種轉換,Go程序員把這些叫做verb(動詞)。下麫
|
||||
%% 字符型百分比標誌(不確定) literal percent sign (no operand)
|
||||
```
|
||||
|
||||
dup1中的程序還包含了一個\t和\n的格式化字符串。在字符串中會以這些特殊的轉義字符來錶示不可見字符。Printf默認不會在輸齣內容後加上換行符。按照慣例,用來格式化的函數都會在末尾以f字母結尾,比如log.Printf,fmt.Errorf,衕時還有一繫列對應以ln結尾的函數,這些函數默認以%v來格式化他們的參數,併且會在輸齣結束後在最後自動加上一個換行符。
|
||||
dup1中的程序還包含了一個\t和\n的格式化字符串。在字符串中會以這些特殊的轉義字符來表示不可見字符。Printf默認不會在輸齣內容後加上換行符。按照慣例,用來格式化的函數都會在末尾以f字母結尾,比如log.Printf,fmt.Errorf,同時還有一繫列對應以ln結尾的函數,這些函數默認以%v來格式化他們的參數,並且會在輸齣結束後在最後自動加上一個換行符。
|
||||
|
||||
許多程序從標準輸入中讀取數據,像上麫的例子那樣。除此之外,還可能從一繫列的文件中讀取。下一個dup程序就是從標準輸入中讀到一些文件名,用os.Open函數來打開每一個文件穫取內容的。
|
||||
許多程序從標準輸入中讀取數據,像上面的例子那樣。除此之外,還可能從一繫列的文件中讀取。下一個dup程序就是從標準輸入中讀到一些文件名,用os.Open函數來打開每一個文件獲取內容的。
|
||||
|
||||
```go
|
||||
gopl.io/ch1/dup2
|
||||
@ -122,19 +122,19 @@ func countLines(f *os.File, counts map[string]int) {
|
||||
}
|
||||
```
|
||||
|
||||
os.Open函數會返迴兩個值。第一個值是一個打開的文件類型(*os.File),這個對象在下麫的程序中被Scanner讀取。
|
||||
os.Open函數會返迴兩個值。第一個值是一個打開的文件類型(*os.File),這個對象在下面的程序中被Scanner讀取。
|
||||
|
||||
os.Open返迴的第二個值是一個go內置的error類型。如果這個error和內置值的nil(譯註:相當於其它語言裏的NULL)相等的話,說明文件被成功的打開了。之後文件被讀取,一直到文件的最後,Close函數關閉該文件,併釋放相應的佔用一切資源。另一方麫,如果err的值不是nil的話,那說明在打開文件的時候齣了某種錯誤。這種情況下,error類型的值會描述具體的問題。我們例子裏的簡單錯誤處理會在標準錯誤流中用Fprintf和%v來格式化該錯誤字符串。然後繼續處理下一個文件;continue語句會直接跳過之後的語句,直接開始執行下一次循環。
|
||||
os.Open返迴的第二個值是一個go內置的error類型。如果這個error和內置值的nil(譯註:相當於其它語言裡的NULL)相等的話,說明文件被成功的打開了。之後文件被讀取,一直到文件的最後,Close函數關閉該文件,並釋放相應的佔用一切資源。另一方面,如果err的值不是nil的話,那說明在打開文件的時候齣了某種錯誤。這種情況下,error類型的值會描述具體的問題。我們例子裡的簡單錯誤處理會在標準錯誤流中用Fprintf和%v來格式化該錯誤字符串。然後繼續處理下一個文件;continue語句會直接跳過之後的語句,直接開始執行下一次循環。
|
||||
|
||||
我們在本書中早期的例子中做了比較詳盡的錯誤處理,當然了,在實際編碼過程中,像os.Open這類的函數是一定要檢査其返迴的error值的;為了減少例子程序的代碼量,我們姑且簡化掉這些不太可能返迴錯誤的邏輯。後麫的例子裏我們會跳過錯誤檢査。在5.4節中我們會對錯誤處理做更詳細的闡述。
|
||||
我們在本書中早期的例子中做了比較詳儘的錯誤處理,當然了,在實際編碼過程中,像os.Open這類的函數是一定要檢査其返迴的error值的;爲了減少例子程序的代碼量,我們姑且簡化掉這些不太可能返迴錯誤的邏輯。後面的例子裡我們會跳過錯誤檢査。在5.4節中我們會對錯誤處理做更詳細的闡述。
|
||||
|
||||
讀者可以再觀察一下上麫的例子,我們的countLines函數是在其聲明之前就被調用了。在Go語言裏,函數和包級彆的變量可以以任意的順序被聲明,併不影響其被調用。(譯註:最好還是遵循一定的規範)
|
||||
讀者可以再觀察一下上面的例子,我們的countLines函數是在其聲明之前就被調用了。在Go語言裡,函數和包級別的變量可以以任意的順序被聲明,並不影響其被調用。(譯註:最好還是遵循一定的規範)
|
||||
|
||||
再來講講map這個數據結構,map是用make函數創建的數據結構的一個引用。當一個map被作為參數傳遞給一個函數時,函數接收到的是一份引用的拷貝,雖然本身併不是一個東西,但因為他們指曏的是衕一塊數據對象(譯註:類似於C艹裏的引用傳遞),所以你在函數裏對map裏的值進行脩改時,原始的map內的值也會改變。在我們的例子中,我們在countLines函數中插入到counts這個map裏的值,在主函數中也是看得到的。
|
||||
再來講講map這個數據結構,map是用make函數創建的數據結構的一個引用。當一個map被作爲參數傳遞給一個函數時,函數接收到的是一份引用的拷貝,雖然本身並不是一個東西,但因爲他們指向的是同一塊數據對象(譯註:類似於C艹裡的引用傳遞),所以你在函數裡對map裡的值進行脩改時,原始的map內的值也會改變。在我們的例子中,我們在countLines函數中插入到counts這個map裡的值,在主函數中也是看得到的。
|
||||
|
||||
上麫這個版本的dup是以流的形式來處理輸入,併將其打散為行。理論上這些程序也是可以以二進製形式來處理輸入的。我們也可以一次性的把整個輸入內容全部讀到內存中,然後再把其分割為多行,然後再去處理這些行內的數據。下麫的dup3這個例子就是以這種形式來進行操作的。這個例子引入了一個新函數ReadFile(從io/ioutil這個包),這個函數會把一個指定名字的文件內容一次性調入,之後我們用strings.Split函數把文件分割為多個子字符串,併存儲到slice結構中。(Split函數是strings.Join的逆函數,Join函數之前提到過)
|
||||
上面這個版本的dup是以流的形式來處理輸入,並將其打散爲行。理論上這些程序也是可以以二進製形式來處理輸入的。我們也可以一次性的把整個輸入內容全部讀到內存中,然後再把其分割爲多行,然後再去處理這些行內的數據。下面的dup3這個例子就是以這種形式來進行操作的。這個例子引入了一個新函數ReadFile(從io/ioutil這個包),這個函數會把一個指定名字的文件內容一次性調入,之後我們用strings.Split函數把文件分割爲多個子字符串,並存儲到slice結構中。(Split函數是strings.Join的逆函數,Join函數之前提到過)
|
||||
|
||||
我們簡化了dup3這個程序。首先,他隻讀取命名的文件,而不去讀標準輸入,因為ReadFile函數需要一個文件名參數。其次,我們將行計數邏輯移迴到了main函數,因為現在這個邏輯隻有一個地方需要用到。
|
||||
我們簡化了dup3這個程序。首先,他隻讀取命名的文件,而不去讀標準輸入,因爲ReadFile函數需要一個文件名參數。其次,我們將行計數邏輯移迴到了main函數,因爲現在這個邏輯隻有一個地方需要用到。
|
||||
|
||||
```go
|
||||
gopl.io/ch1/dup3
|
||||
@ -167,10 +167,10 @@ func main() {
|
||||
}
|
||||
```
|
||||
|
||||
ReadFile函數返迴一個byte的slice,這個slice必鬚被轉換為string,之後纔能夠用string.Split方法來進行處理。我們在3.5.4節中會更詳細地講解string和byte slice(字節數組)。
|
||||
ReadFile函數返迴一個byte的slice,這個slice必鬚被轉換爲string,之後纔能夠用string.Split方法來進行處理。我們在3.5.4節中會更詳細地講解string和byte slice(字節數組)。
|
||||
|
||||
在更底層一些的地方,bufio.Scanner,ioutil.ReadFile和ioutil.WriteFile使用的是*os.File的Read和Write方法,不過一般程序員併不需要去直接了解到其底層實現細節,在bufio和io/ioutil包中提供的方法已經足夠好用。
|
||||
在更底層一些的地方,bufio.Scanner,ioutil.ReadFile和ioutil.WriteFile使用的是*os.File的Read和Write方法,不過一般程序員並不需要去直接了解到其底層實現細節,在bufio和io/ioutil包中提供的方法已經足夠好用。
|
||||
|
||||
```
|
||||
Exercise 1.4: 脩改dup2,使其可以打印重復的行分彆齣現在哪些文件。
|
||||
Exercise 1.4: 脩改dup2,使其可以打印重復的行分別齣現在哪些文件。
|
||||
```
|
||||
|
@ -1,10 +1,10 @@
|
||||
## 1.4. GIF動畫
|
||||
|
||||
下麫的程序會演示Go語言標準庫裏的image這個package的用法,我們會用這個包來生成一繫列的bit-mapped圖,然後將這些圖片編碼為一個GIF動畫。我們生成的圖形名字叫利薩如圖形(Lissajous figures),這種效果是在1960年代的老電影裏齣現的一種視覺特效。他們是協振子在兩個緯度上振動所產生的麴綫,比如兩個sin正絃波分彆在x軸和y軸輸入會產生的麴綫。圖1.1是這樣的一個例子:
|
||||
下面的程序會演示Go語言標準庫裡的image這個package的用法,我們會用這個包來生成一繫列的bit-mapped圖,然後將這些圖片編碼爲一個GIF動畫。我們生成的圖形名字叫利薩如圖形(Lissajous figures),這種效果是在1960年代的老電影裡齣現的一種視覺特效。他們是協振子在兩個緯度上振動所產生的麴綫,比如兩個sin正絃波分別在x軸和y軸輸入會產生的麴綫。圖1.1是這樣的一個例子:
|
||||
|
||||
![](../images/ch1-01.png)
|
||||
|
||||
這段代碼裏我們用了一些新的結構,包括const聲明,數據struct類型,復閤聲明。和我們舉的其它的例子不太一樣,這一個例子包含了浮點數運算。這些概唸我們隻在這裏簡單地說明一下,之後的章節會更詳細地講解。
|
||||
這段代碼裡我們用了一些新的結構,包括const聲明,數據struct類型,復合聲明。和我們舉的其它的例子不太一樣,這一個例子包含了浮點數運算。這些概唸我們隻在這裡簡單地說明一下,之後的章節會更詳細地講解。
|
||||
|
||||
```go
|
||||
gopl.io/ch1/lissajous
|
||||
@ -62,19 +62,19 @@ blackIndex)
|
||||
|
||||
```
|
||||
|
||||
當我們import了一個包路徑包含有多個單詞的package時,比如image/color(image和color兩個單詞),我們隻需要用最後那個單詞錶示這個包就可以。所以當我們寫color.White時,這個變量指曏的是image/color包裏的變量,衕理gif.GIF是屬於image/gif包裏的變量。
|
||||
當我們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。
|
||||
[]color.Color{...}和gif.GIF{...}這兩個表達式就是我們說的復合聲明(4.2和4.4.1節有說明)。這是實例化Go語言裡的復合類型的一種寫法。這裡的前者生成的是一個slice,後者生成的是一個struct。
|
||||
|
||||
gif.GIF是一個struct類型(參考4.4節)。struct是一組值或者叫字段的集閤,不衕的類型集閤在一個struct可以讓我們以一個統一的單元進行處理。anim是一個gif.GIF類型的struct變量。這種寫法會生成一個struct變量,併且其內部變量LoopCount字段會被設置為nframes;而其它的字段會被設置為各自類型默認的零值。struct內部的變量可以以一個點(.)來進行訪問,就像在最後兩個賦值語句中顯式地更新了anim這個struct的Delay和Image字段。
|
||||
gif.GIF是一個struct類型(參考4.4節)。struct是一組值或者叫字段的集合,不同的類型集合在一個struct可以讓我們以一個統一的單元進行處理。anim是一個gif.GIF類型的struct變量。這種寫法會生成一個struct變量,並且其內部變量LoopCount字段會被設置爲nframes;而其它的字段會被設置爲各自類型默認的零值。struct內部的變量可以以一個點(.)來進行訪問,就像在最後兩個賦值語句中顯式地更新了anim這個struct的Delay和Image字段。
|
||||
|
||||
lissajous函數內部有兩層嵌太的for循環。外層循環會循環64次,每一次都會生成一個單獨的動畫幀。它生成了一個包含兩種顔色的201&201大小的圖片,白色和黑色。所有像素點都會被默認設置為其零值(也就是palette裏的第0個值),這裏我們設置的是白色。每次經過內存循環都會通過設置像素為黑色,生成一張新圖片。其結果會append到之前結果之後。這裏我們用到了append(參考4.2.1)這個內置函數,將結果appen到anim中的幀列錶末尾,併會設置一個默認的80ms的延遲值。最終循環結束,所有的延遲值也被編碼進了GIF圖片中,併將結果寫入到輸齣流。out這個變量是io.Writer類型,這個類型讓我們可以可以讓我們把輸齣結果寫到很多目標,很快我們就可以看到了。
|
||||
lissajous函數內部有兩層嵌太的for循環。外層循環會循環64次,每一次都會生成一個單獨的動畫幀。它生成了一個包含兩種顏色的201&201大小的圖片,白色和黑色。所有像素點都會被默認設置爲其零值(也就是palette裡的第0個值),這裡我們設置的是白色。每次經過內存循環都會通過設置像素爲黑色,生成一張新圖片。其結果會append到之前結果之後。這裡我們用到了append(參考4.2.1)這個內置函數,將結果appen到anim中的幀列表末尾,並會設置一個默認的80ms的延遲值。最終循環結束,所有的延遲值也被編碼進了GIF圖片中,並將結果寫入到輸齣流。out這個變量是io.Writer類型,這個類型讓我們可以可以讓我們把輸齣結果寫到很多目標,很快我們就可以看到了。
|
||||
|
||||
內存循環設置了兩個偏振。x軸偏振使用的是一個sin函數。y軸偏振也是一個正絃波,但是其其相對x軸的偏振是一個0-3的隨機值,併且初始偏振值是一個零值,併隨着動畫的每一幀逐漸增加。循環會一直跑到x軸完成五次完整的循環。每一步它都會調用SetColorIndex來為(x, y)點來染黑色。
|
||||
內存循環設置了兩個偏振。x軸偏振使用的是一個sin函數。y軸偏振也是一個正絃波,但是其其相對x軸的偏振是一個0-3的隨機值,並且初始偏振值是一個零值,並隨着動畫的每一幀逐漸增加。循環會一直跑到x軸完成五次完整的循環。每一步它都會調用SetColorIndex來爲(x, y)點來染黑色。
|
||||
|
||||
main函數調用了lissajous函數,併且用它來曏標準輸齣中打印信息,所以下麫這個命令會像圖1.1中產生一個GIF動畫。
|
||||
main函數調用了lissajous函數,並且用它來向標準輸齣中打印信息,所以下面這個命令會像圖1.1中產生一個GIF動畫。
|
||||
|
||||
```bash
|
||||
$ go build gopl.io/ch1/lissajous
|
||||
@ -82,6 +82,6 @@ $ ./lissajous >out.gif
|
||||
```
|
||||
|
||||
```
|
||||
Exercise 1.5: 脩改前麫的Lissajous程序裏的調色闆,由緑色改為黑色。我們可以用color.RGBA{0xRR, 0xGG, 0xBB}來得到#RRGGBB這個色值,三個十六進製的字符串分彆代錶紅、緑、藍像素。
|
||||
Exercise 1.6: 脩改Lissajous程序,脩改其調色闆來生成更豐富的顔色,然後脩改SetColorIndex的第三個參數,看看顯示結果吧。
|
||||
Exercise 1.5: 脩改前面的Lissajous程序裡的調色闆,由緑色改爲黑色。我們可以用color.RGBA{0xRR, 0xGG, 0xBB}來得到#RRGGBB這個色值,三個十六進製的字符串分別代表紅、緑、藍像素。
|
||||
Exercise 1.6: 脩改Lissajous程序,脩改其調色闆來生成更豐富的顏色,然後脩改SetColorIndex的第三個參數,看看顯示結果吧。
|
||||
```
|
||||
|
@ -1,7 +1,7 @@
|
||||
## 1.5 穫取URL
|
||||
對於很多應用來說,訪問互聯網上的信息和訪問本地文件繫統一樣重要。Go在net這個大package下提供了一繫列的package來做這件事情,使用這些包可以更簡單地用網絡收髮信息,還可以建立更底層的網絡連接,編寫服務器程序。在這些情景下,Go原生的併髮特性(在第八章中會介紹)就顯得尤其好用了。
|
||||
## 1.5 獲取URL
|
||||
對於很多應用來說,訪問互聯網上的信息和訪問本地文件繫統一樣重要。Go在net這個大package下提供了一繫列的package來做這件事情,使用這些包可以更簡單地用網絡收發信息,還可以建立更底層的網絡連接,編寫服務器程序。在這些情景下,Go原生的並發特性(在第八章中會介紹)就顯得尤其好用了。
|
||||
|
||||
為了最簡單地展示基於HTTP穫取信息的方式,下麫給齣一個示例程序fetch,這個程序將穫取對應的url,併將其源文本打印齣來;這個例子的靈感來源於curl工具(譯註:unix下的一個工具)。當然了,curl提供的功能更為復雜豐富,這裏我們隻編寫最簡單的樣例。之後我們還會在本書中經常用到這個例子。
|
||||
爲了最簡單地展示基於HTTP獲取信息的方式,下面給齣一個示例程序fetch,這個程序將獲取對應的url,並將其源文本打印齣來;這個例子的靈感來源於curl工具(譯註:unix下的一個工具)。當然了,curl提供的功能更爲復雜豐富,這裡我們隻編寫最簡單的樣例。之後我們還會在本書中經常用到這個例子。
|
||||
|
||||
```go
|
||||
gopl.io/ch1/fetch
|
||||
@ -32,7 +32,7 @@ func main() {
|
||||
}
|
||||
```
|
||||
|
||||
這個程序從兩個package中導入了函數,net/http和io/ioutil,http.Get函數是創建HTTP請求的函數,如果穫取過程沒有齣錯,那麼會在resp這個結構體中得到訪問的請求結果。resp的Body字段包括一個可讀的服務器響應流。這之後ioutil.ReadAll函數從response中讀取到全部內容;其結果保存在變量b中。resp.Body.Close這一句會關閉resp的Body流,防止資源洩露,Printf函數會將結果b寫齣到標準輸齣流中。
|
||||
這個程序從兩個package中導入了函數,net/http和io/ioutil,http.Get函數是創建HTTP請求的函數,如果獲取過程沒有齣錯,那麽會在resp這個結構體中得到訪問的請求結果。resp的Body字段包括一個可讀的服務器響應流。這之後ioutil.ReadAll函數從response中讀取到全部內容;其結果保存在變量b中。resp.Body.Close這一句會關閉resp的Body流,防止資源洩露,Printf函數會將結果b寫齣到標準輸齣流中。
|
||||
|
||||
```bash
|
||||
$ go build gopl.io/ch1/fetch
|
||||
@ -43,17 +43,17 @@ $ ./fetch http://gopl.io
|
||||
...
|
||||
```
|
||||
|
||||
HTTP請求如果失敗了的話,會得到下麫這樣的結果:
|
||||
HTTP請求如果失敗了的話,會得到下面這樣的結果:
|
||||
|
||||
```bash
|
||||
$ ./fetch http://bad.gopl.io
|
||||
fetch: Get http://bad.gopl.io: dial tcp: lookup bad.gopl.io: no such host
|
||||
```
|
||||
|
||||
無論哪種失敗原因,我們的程序都用了os.Exit函數來終止進程,併且返迴一個status錯誤碼,其值為1。
|
||||
無論哪種失敗原因,我們的程序都用了os.Exit函數來終止進程,並且返迴一個status錯誤碼,其值爲1。
|
||||
|
||||
```
|
||||
Exercise1.7: 函數調用io.Copy(dst, src)會從src中讀取內容,併將讀到的結果寫入到dst中,使用這個函數替代掉例子中的ioutil.ReadAll來拷貝響應結構體到os.Stdout,避免申請一個緩衝區(例子中的b)來存儲。記得處理io.Copy返迴結果中的錯誤。
|
||||
Exercise 1.8: 脩改fetch這個範例,如果輸入的url參數沒有http://前綴的話,為這個url加上該前綴。你可能會用到strings.HasPrefix這個函數。
|
||||
Exercise1.7: 函數調用io.Copy(dst, src)會從src中讀取內容,並將讀到的結果寫入到dst中,使用這個函數替代掉例子中的ioutil.ReadAll來拷貝響應結構體到os.Stdout,避免申請一個緩沖區(例子中的b)來存儲。記得處理io.Copy返迴結果中的錯誤。
|
||||
Exercise 1.8: 脩改fetch這個範例,如果輸入的url參數沒有http://前綴的話,爲這個url加上該前綴。你可能會用到strings.HasPrefix這個函數。
|
||||
Exercise 1.9: 脩改fetch打印齣HTTP協議的狀態碼,可以從resp.Status變量得到該狀態碼。
|
||||
```
|
||||
|
@ -1,8 +1,8 @@
|
||||
## 1.6 併髮穫取多個URL
|
||||
## 1.6 並發獲取多個URL
|
||||
|
||||
Go語言最有意思併且最新奇的特性就是其對併髮編程的支持了。併髮編程是一個大話題,在第八章和第九章中會講到。這裏我們隻淺嚐輒止地來體驗一下Go語言裏的goroutine和channel。
|
||||
Go語言最有意思並且最新奇的特性就是其對並發編程的支持了。並發編程是一個大話題,在第八章和第九章中會講到。這裡我們隻淺嘗輒止地來體驗一下Go語言裡的goroutine和channel。
|
||||
|
||||
下麫的例子fetchall,和上麫的fetch程序所要做的工作是一緻的,但是這個fetchall的特彆之處在於它會衕時去穫取所有的URL,所以這個程序的穫取時間不會超過執行時間最長的那一個任務,而不會像前麫的fetch程序一樣,執行時間是所有任務執行時間之和。這次的fetchall程序隻會打印穫取的內容大小和經過的時間,不會像上麫那樣打印齣穫取的內容。
|
||||
下面的例子fetchall,和上面的fetch程序所要做的工作是一緻的,但是這個fetchall的特別之處在於它會同時去獲取所有的URL,所以這個程序的獲取時間不會超過執行時間最長的那一個任務,而不會像前面的fetch程序一樣,執行時間是所有任務執行時間之和。這次的fetchall程序隻會打印獲取的內容大小和經過的時間,不會像上面那樣打印齣獲取的內容。
|
||||
|
||||
```go
|
||||
gopl.io/ch1/fetchall
|
||||
@ -48,7 +48,7 @@ func fetch(url string, ch chan<- string) {
|
||||
}
|
||||
|
||||
```
|
||||
下麫是一個使用的例子
|
||||
下面是一個使用的例子
|
||||
|
||||
```bash
|
||||
$ go build gopl.io/ch1/fetchall
|
||||
@ -58,10 +58,10 @@ $ ./fetchall https://golang.org http://gopl.io https://godoc.org
|
||||
0.48s 2475 http://gopl.io
|
||||
0.48s elapsed
|
||||
```
|
||||
goroutine是一種函數的併行執行方式,而channel是用來在goroutine之間進行參數傳遞。main函數卽運行在一個goroutine中,而go function則錶示創建一個新的goroutine,併讓這個函數去這個新的goroutine裏執行。
|
||||
goroutine是一種函數的並行執行方式,而channel是用來在goroutine之間進行參數傳遞。main函數卽運行在一個goroutine中,而go function則表示創建一個新的goroutine,並讓這個函數去這個新的goroutine裡執行。
|
||||
|
||||
main函數中用make函數創建了一個傳遞string類型參數的channel,對每一個命令行參數,我們都用go這個關鍵字來創建一個goroutine,併且讓函數在這個goroutine異步執行http.Get方法。這個程序裏的io.Copy會把響應的Body內容拷貝到ioutil.Discard輸齣流中,因為我們需要這個方法返迴的字節數,但是又不想要其內容。每當請求返迴內容時,fetch函數都會往ch這個channel裏寫入一個字符串,由main函數裏的第二個for循環來處理併打印channel裏的這個字符串。
|
||||
main函數中用make函數創建了一個傳遞string類型參數的channel,對每一個命令行參數,我們都用go這個關鍵字來創建一個goroutine,並且讓函數在這個goroutine異步執行http.Get方法。這個程序裡的io.Copy會把響應的Body內容拷貝到ioutil.Discard輸齣流中,因爲我們需要這個方法返迴的字節數,但是又不想要其內容。每當請求返迴內容時,fetch函數都會往ch這個channel裡寫入一個字符串,由main函數裡的第二個for循環來處理並打印channel裡的這個字符串。
|
||||
|
||||
當一個goroutine嚐試在一個channel上做send或者receive操作時,這個goroutine會阻塞在調用處,直到另一個goroutine往這個channel裏寫入、或者接收了值,這樣兩個goroutine纔會繼續執行操作channel完成之後的邏輯。在這個例子中,每一個fetch函數在執行時都會往channel裏髮送一個值(ch <- expression),主函數接收這些值(<-ch)。這個程序中我們用main函數來所有fetch函數傳迴的字符串,可以避免在goroutine異步執行時衕時結束。
|
||||
當一個goroutine嘗試在一個channel上做send或者receive操作時,這個goroutine會阻塞在調用處,直到另一個goroutine往這個channel裡寫入、或者接收了值,這樣兩個goroutine纔會繼續執行操作channel完成之後的邏輯。在這個例子中,每一個fetch函數在執行時都會往channel裡發送一個值(ch <- expression),主函數接收這些值(<-ch)。這個程序中我們用main函數來所有fetch函數傳迴的字符串,可以避免在goroutine異步執行時同時結束。
|
||||
|
||||
Exercise 1.10: 找一個數據量比較大的網站,用本小節中的程序調研網站的緩存策略,對每個URL執行兩遍請求,査看兩次時間是否有較大的差彆,併且每次穫取到的響應內容是否一緻,脩改本節中的程序,將響應結果輸齣,以便於進行對比。
|
||||
Exercise 1.10: 找一個數據量比較大的網站,用本小節中的程序調研網站的緩存策略,對每個URL執行兩遍請求,査看兩次時間是否有較大的差別,並且每次獲取到的響應內容是否一緻,脩改本節中的程序,將響應結果輸齣,以便於進行對比。
|
||||
|
@ -1,6 +1,6 @@
|
||||
## 1.7. Web服務
|
||||
|
||||
Go的內置庫讓我們寫一個像fetch這樣例子的web服務器變得異常地簡單。在本節中,我們會展示一個微型服務器,這個服務的功能是返迴當前用戶正在訪問的URL。也就是說比如用戶訪問的是http://localhost:8000/hello,那麼響應是URL.Path = "hello"。
|
||||
Go的內置庫讓我們寫一個像fetch這樣例子的web服務器變得異常地簡單。在本節中,我們會展示一個微型服務器,這個服務的功能是返迴當前用戶正在訪問的URL。也就是說比如用戶訪問的是http://localhost:8000/hello,那麽響應是URL.Path = "hello"。
|
||||
```go
|
||||
gopl.io/ch1/server1
|
||||
// Server1 is a minimal "echo" server.
|
||||
@ -23,15 +23,15 @@ func handler(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
```
|
||||
|
||||
我們隻用了八九行就實現了這個程序,這都是多虧了標準庫裏的方法已經幫我們處理了大多數的工作。main函數會將所有髮送到/目彔下的請求和handler函數關聯起來,/開頭的請求其實就是所有髮送到當前站點上的請求,我們的服務跑在了8000端口上。髮送到這個服務的“請求”是一個http.Request類型的對象,這個對象中包含了請求中的一繫列相關字段,其中就包括我們需要的URL。當請求到達服務器時,這個請求會被傳給handler函數來處理,這個函數會將/hello這個路徑從請求的URL中解析齣來,然後把其髮送到響應中,這裏我們用的是標準輸齣流的fmt.Fprintf。Web服務會在第7.7節中詳細闡述。
|
||||
我們隻用了八九行就實現了這個程序,這都是多虧了標準庫裡的方法已經幫我們處理了大多數的工作。main函數會將所有發送到/目録下的請求和handler函數關聯起來,/開頭的請求其實就是所有發送到當前站點上的請求,我們的服務跑在了8000端口上。發送到這個服務的“請求”是一個http.Request類型的對象,這個對象中包含了請求中的一繫列相關字段,其中就包括我們需要的URL。當請求到達服務器時,這個請求會被傳給handler函數來處理,這個函數會將/hello這個路徑從請求的URL中解析齣來,然後把其發送到響應中,這裡我們用的是標準輸齣流的fmt.Fprintf。Web服務會在第7.7節中詳細闡述。
|
||||
|
||||
讓我們在後颱運行這個服務程序。如果你的操作繫統是Mac OS X或者Linux,那麼在運行命令的末尾加上一個&符號,卽可讓程序簡單地跑在後颱,而在windows下,你需要在另外一個命令行窗口去運行這個程序了。
|
||||
讓我們在後檯運行這個服務程序。如果你的操作繫統是Mac OS X或者Linux,那麽在運行命令的末尾加上一個&符號,卽可讓程序簡單地跑在後檯,而在windows下,你需要在另外一個命令行窗口去運行這個程序了。
|
||||
|
||||
```
|
||||
$ go run src/gopl.io/ch1/server1/main.go &
|
||||
```
|
||||
|
||||
現在我們可以通過命令行來髮送客戶端請求了:
|
||||
現在我們可以通過命令行來發送客戶端請求了:
|
||||
|
||||
```
|
||||
$ go build gopl.io/ch1/fetch
|
||||
@ -41,10 +41,10 @@ $ ./fetch http://localhost:8000/help
|
||||
URL.Path = "/help"
|
||||
```
|
||||
|
||||
另外我們還可以直接在瀏覽器裏訪問這個URL,然後得到返迴結果,如圖1.2:
|
||||
另外我們還可以直接在瀏覽器裡訪問這個URL,然後得到返迴結果,如圖1.2:
|
||||
![](../images/ch1-02.png)
|
||||
|
||||
在這個服務的基礎上疊加特性是很容易的。一種比較實用的脩改是為訪問的url添加某種狀態。比如,下麫這個版本輸齣了衕樣的內容,但是會對請求的次數進行計算;對URL的請求結果會包含各種URL被訪問的總次數,直接對/count這個URL的訪問要除外。
|
||||
在這個服務的基礎上疊加特性是很容易的。一種比較實用的脩改是爲訪問的url添加某種狀態。比如,下面這個版本輸齣了同樣的內容,但是會對請求的次數進行計算;對URL的請求結果會包含各種URL被訪問的總次數,直接對/count這個URL的訪問要除外。
|
||||
|
||||
```go
|
||||
gopl.io/ch1/server2
|
||||
@ -83,9 +83,9 @@ 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數據都打印齣來,這樣可以讓檢査和調試這個服務更為方便
|
||||
下面是一個更爲豐富的例子,handler函數會把請求的http頭和請求的form數據都打印齣來,這樣可以讓檢査和調試這個服務更爲方便
|
||||
|
||||
```go
|
||||
gopl.io/ch1/server3
|
||||
@ -106,7 +106,7 @@ func handler(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
```
|
||||
|
||||
我們用http.Request這個struct裏的字段來輸齣下麫這樣的內容:
|
||||
我們用http.Request這個struct裡的字段來輸齣下面這樣的內容:
|
||||
|
||||
```
|
||||
GET /?q=query HTTP/1.1
|
||||
@ -117,7 +117,7 @@ RemoteAddr = "127.0.0.1:59911"
|
||||
Form["q"] = ["query"]
|
||||
```
|
||||
|
||||
可以看到這裏的ParseForm被嵌套在了if語句中。Go語言允許這樣的一個簡單的語句結果作為循環的變量聲明齣現在if語句的最前麫,這一點對錯誤處理很有用處。我們還可以像下麫這樣寫(當然看起來就長了一些):
|
||||
可以看到這裡的ParseForm被嵌套在了if語句中。Go語言允許這樣的一個簡單的語句結果作爲循環的變量聲明齣現在if語句的最前面,這一點對錯誤處理很有用處。我們還可以像下面這樣寫(當然看起來就長了一些):
|
||||
|
||||
```go
|
||||
err := r.ParseForm()
|
||||
@ -125,13 +125,13 @@ if err != nil {
|
||||
log.Print(err)
|
||||
}
|
||||
```
|
||||
用if和ParseForm結閤可以讓代碼更加簡單,併且可以限製err這個變量的作用域,這麼做是很不錯的。我們會在2.7節中講解作用域。
|
||||
用if和ParseForm結合可以讓代碼更加簡單,並且可以限製err這個變量的作用域,這麽做是很不錯的。我們會在2.7節中講解作用域。
|
||||
|
||||
在這些程序中,我們看到了很多不衕的類型被輸齣到標準輸齣流中。比如前麫的fetch程序,就把HTTP的響應數據拷貝到了os.Stdout,或者在lissajous程序裏我們輸齣的是一個文件。fetchall程序則完全忽略到了HTTP的響應體,隻是計算了一下響應體的大小,這個程序中把響應體拷貝到了ioutil.Discard。在本節的web服務器程序中則是用fmt.Fprintf直接寫到了http.ResponseWriter中。
|
||||
在這些程序中,我們看到了很多不同的類型被輸齣到標準輸齣流中。比如前面的fetch程序,就把HTTP的響應數據拷貝到了os.Stdout,或者在lissajous程序裡我們輸齣的是一個文件。fetchall程序則完全忽略到了HTTP的響應體,隻是計算了一下響應體的大小,這個程序中把響應體拷貝到了ioutil.Discard。在本節的web服務器程序中則是用fmt.Fprintf直接寫到了http.ResponseWriter中。
|
||||
|
||||
盡管這三種具體的實現流程併不太一樣,他們都實現一個共衕的接口,卽當它們被調用需要一個標準流輸齣時都可以滿足。這個接口叫作io.Writer,在7.1節中會詳細討論。
|
||||
儘管這三種具體的實現流程並不太一樣,他們都實現一個共同的接口,卽當它們被調用需要一個標準流輸齣時都可以滿足。這個接口叫作io.Writer,在7.1節中會詳細討論。
|
||||
|
||||
Go的接口機製會在第7章中講解,為了在這裏簡單說明接口能做什麼,讓我們簡單地將這裏的web服務器和之前寫的lissajous函數結閤起來,這樣GIF動畫可以被寫到HTTP的客戶端,而不是之前的標準輸齣流。隻要在web服務器的代碼裏加入下麫這幾行。
|
||||
Go的接口機製會在第7章中講解,爲了在這裡簡單說明接口能做什麽,讓我們簡單地將這裡的web服務器和之前寫的lissajous函數結合起來,這樣GIF動畫可以被寫到HTTP的客戶端,而不是之前的標準輸齣流。隻要在web服務器的代碼裡加入下面這幾行。
|
||||
|
||||
```
|
||||
handler := func(w http.ResponseWriter, r *http.Request) {
|
||||
@ -147,13 +147,13 @@ http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
|
||||
})
|
||||
```
|
||||
|
||||
HandleFunc函數的第二個參數是一個函數的字麫值,也就是一個在使用時定義的匿名函數。這些內容我們會在5.6節中講解。
|
||||
HandleFunc函數的第二個參數是一個函數的字面值,也就是一個在使用時定義的匿名函數。這些內容我們會在5.6節中講解。
|
||||
|
||||
|
||||
做完這些脩改之後,在瀏覽器裏訪問http://localhost:8000。每次你載入這個頁麫都可以看到一個像圖1.3那樣的動畫。
|
||||
做完這些脩改之後,在瀏覽器裡訪問http://localhost:8000。每次你載入這個頁面都可以看到一個像圖1.3那樣的動畫。
|
||||
|
||||
```
|
||||
Exercise 1.12:脩改Lissajour服務,從URL讀取變量,比如你可以訪問http://localhost:8000/?cycles=20這個URL,這樣訪問可以將程序裏的cycles默認的5脩改為20。字符串轉換為數字可以調用strconv.Atoi函數。你可以在dodoc裏査看strconv.Atoi的詳細說明。
|
||||
Exercise 1.12:脩改Lissajour服務,從URL讀取變量,比如你可以訪問http://localhost:8000/?cycles=20這個URL,這樣訪問可以將程序裡的cycles默認的5脩改爲20。字符串轉換爲數字可以調用strconv.Atoi函數。你可以在dodoc裡査看strconv.Atoi的詳細說明。
|
||||
```
|
||||
![](../images/ch1-03.png)
|
||||
|
||||
|
@ -1,8 +1,8 @@
|
||||
## 1.8. 本章要點
|
||||
|
||||
本章中對Go語言做了一些介紹,實際上Go語言還有很多方麫在這有限的篇幅中還沒有覆蓋到。這裏我們會把沒有講到的內容也做一些簡單的介紹,這樣讀者在之後看到完整的內容之前,也可以簡單有個印象。
|
||||
本章中對Go語言做了一些介紹,實際上Go語言還有很多方面在這有限的篇幅中還沒有覆蓋到。這裡我們會把沒有講到的內容也做一些簡單的介紹,這樣讀者在之後看到完整的內容之前,也可以簡單有個印象。
|
||||
|
||||
控製流:在本章我們隻介紹了if控製和for,但是沒有提到switch多路選擇。這裏是一個簡單的switch的例子:
|
||||
控製流:在本章我們隻介紹了if控製和for,但是沒有提到switch多路選擇。這裡是一個簡單的switch的例子:
|
||||
|
||||
```go
|
||||
switch coinflip() {
|
||||
@ -15,9 +15,9 @@ switch coinflip() {
|
||||
}
|
||||
```
|
||||
|
||||
在翻轉硬幣的時候,例子裏的coinflip函數返迴幾種不衕的結果,每一個case都會對應個返迴結果,這裏需要註意,Go語言併不需要顯式地去在每一個case後寫break,語言默認執行完case後的邏輯語句會自動退齣。當然了,如果你想要相鄰的幾個case都執行衕一邏輯的話,需要自己顯式地寫上一個fallthrough語句來覆蓋這種默認行為。不過fallthrough語句在一般的編程中用到得很少。
|
||||
在翻轉硬幣的時候,例子裡的coinflip函數返迴幾種不同的結果,每一個case都會對應個返迴結果,這裡需要註意,Go語言並不需要顯式地去在每一個case後寫break,語言默認執行完case後的邏輯語句會自動退齣。當然了,如果你想要相鄰的幾個case都執行同一邏輯的話,需要自己顯式地寫上一個fallthrough語句來覆蓋這種默認行爲。不過fallthrough語句在一般的編程中用到得很少。
|
||||
|
||||
go裏的switch還可以不帶操作對象;可以直接羅列多種條件,像其它語言裏麫的多個if else一樣,下麫是一個例子:
|
||||
go裡的switch還可以不帶操作對象;可以直接羅列多種條件,像其它語言裡面的多個if else一樣,下面是一個例子:
|
||||
|
||||
```go
|
||||
func Signum(x int) int {
|
||||
@ -34,11 +34,11 @@ func Signum(x int) int {
|
||||
|
||||
這種形式叫做無tag switch(tagless switch);這和switch true是等價的。
|
||||
|
||||
像for和if控製語句一樣,switch也可以緊跟一個簡短的變量聲明,一個自增錶達式、賦值語句,或者一個函數調用。
|
||||
像for和if控製語句一樣,switch也可以緊跟一個簡短的變量聲明,一個自增表達式、賦值語句,或者一個函數調用。
|
||||
|
||||
break和continue語句會改變控製流。和其它語言中的break和continue一樣,break會中斷當前的循環,併開始執行循環之後的內容,而continue會中跳過當前循環,併開始執行下一次循環。這兩個語句除了可以控製for循環,還可以用來控製switch和select語句(之後會講到),在1.3節中我們看到,continue會跳過是內層的循環,如果我們想跳過的是更外層的循環的話,我們可以在相應的位置加上label,這樣break和continue就可以根據我們的想法來continue和break任意循環。這看起來甚至有點像goto語句的作用了。當然,一般程序員也不會用到這種操作。這兩種行為更多地被用到機器生成的代碼中。
|
||||
break和continue語句會改變控製流。和其它語言中的break和continue一樣,break會中斷當前的循環,並開始執行循環之後的內容,而continue會中跳過當前循環,並開始執行下一次循環。這兩個語句除了可以控製for循環,還可以用來控製switch和select語句(之後會講到),在1.3節中我們看到,continue會跳過是內層的循環,如果我們想跳過的是更外層的循環的話,我們可以在相應的位置加上label,這樣break和continue就可以根據我們的想法來continue和break任意循環。這看起來甚至有點像goto語句的作用了。當然,一般程序員也不會用到這種操作。這兩種行爲更多地被用到機器生成的代碼中。
|
||||
|
||||
命名類型:類型聲明使得我們可以很方便地給一個特殊類型一個名字。因為struct類型聲明通常非常地長,所以我們總要給這種struct取一個名字。本章中就有這樣一個例子,2d點類型:
|
||||
命名類型:類型聲明使得我們可以很方便地給一個特殊類型一個名字。因爲struct類型聲明通常非常地長,所以我們總要給這種struct取一個名字。本章中就有這樣一個例子,2d點類型:
|
||||
```go
|
||||
type Point struct {
|
||||
X, Y int
|
||||
@ -48,13 +48,13 @@ var p Point
|
||||
|
||||
類型聲明和命名類型會在第二章中介紹。
|
||||
|
||||
指鍼:Go語言提供了指鍼。指鍼是一種直接存儲了變量的內存地址的數據結構。在其它語言中,比如C語言,指鍼是完全不受約束的。在另外一些語言中,指鍼一般被稱為“引用”,除了到處傳遞這些指鍼之外,併不能對這些指鍼做太多事情。go在這兩種範圍中取得了一個平衡。指鍼是可見的內存地址,&操作符可以返迴一個變量的內存地址,併且*操作符可以穫取指鍼指曏的變量內容,但是在go語言裏沒有指鍼運算,也就是不像c語言裏可以對指鍼進行加或減操作。我們會在2.3.2中進行詳細介紹。
|
||||
指鍼:Go語言提供了指鍼。指鍼是一種直接存儲了變量的內存地址的數據結構。在其它語言中,比如C語言,指鍼是完全不受約束的。在另外一些語言中,指鍼一般被稱爲“引用”,除了到處傳遞這些指鍼之外,並不能對這些指鍼做太多事情。go在這兩種範圍中取得了一個平衡。指鍼是可見的內存地址,&操作符可以返迴一個變量的內存地址,並且*操作符可以獲取指鍼指向的變量內容,但是在go語言裡沒有指鍼運算,也就是不像c語言裡可以對指鍼進行加或減操作。我們會在2.3.2中進行詳細介紹。
|
||||
|
||||
方法和接口:方法是和命名類型關聯的一類函數。Go語言裏比較特殊的是方法可以被關聯到任意一種命名類型。在第六章我們會詳細地講方法。接口是一種抽象類型,這種類型可以讓我們以衕樣的方式來處理不衕的固有類型,不用關心它們的具體實現,而隻需要關註它們提供的方法。第七章中會詳細說明這些內容。
|
||||
方法和接口:方法是和命名類型關聯的一類函數。Go語言裡比較特殊的是方法可以被關聯到任意一種命名類型。在第六章我們會詳細地講方法。接口是一種抽象類型,這種類型可以讓我們以同樣的方式來處理不同的固有類型,不用關心它們的具體實現,而隻需要關註它們提供的方法。第七章中會詳細說明這些內容。
|
||||
|
||||
包(packages):Go語言提供了一些很好用的package,併且這些package是可以擴展的。Go語言社區已經創造併且分享了很多很多。所以Go語言編程大多數情況下就是用已有的package來寫我們自己的代碼。通過這本書,我們會講解一些重要的標準庫內的package,但是還是有很多我們沒有篇幅去說明,因為我們沒法在這樣的厚度的書裏去做一部代碼大全。
|
||||
包(packages):Go語言提供了一些很好用的package,並且這些package是可以擴展的。Go語言社區已經創造並且分享了很多很多。所以Go語言編程大多數情況下就是用已有的package來寫我們自己的代碼。通過這本書,我們會講解一些重要的標準庫內的package,但是還是有很多我們沒有篇幅去說明,因爲我們沒法在這樣的厚度的書裡去做一部代碼大全。
|
||||
|
||||
在你開始寫一個新程序之前,最好先去檢査一下是不是已經有了現成的庫可以幫助你更高效地完成這件事情。你可以在https://golang.org/pkg 和 https://godoc.org 中找到標準庫和社區寫的package。godoc這個工具可以讓你直接在本地命令行閱讀標準庫的文檔。比如下麫這個例子。
|
||||
在你開始寫一個新程序之前,最好先去檢査一下是不是已經有了現成的庫可以幫助你更高效地完成這件事情。你可以在https://golang.org/pkg 和 https://godoc.org 中找到標準庫和社區寫的package。godoc這個工具可以讓你直接在本地命令行閱讀標準庫的文檔。比如下面這個例子。
|
||||
|
||||
```
|
||||
$ go doc http.ListenAndServe
|
||||
@ -64,7 +64,7 @@ func ListenAndServe(addr string, handler Handler) error
|
||||
calls Serve with handler to handle requests on incoming connections.
|
||||
...
|
||||
```
|
||||
註釋:我們之前已經提到過了在源文件的開頭寫的註釋是這個源文件的文檔。在每一個函數之前寫一個說明函數行為的註釋也是一個好習慣。這些慣例很重要,因為這些內容會被像godoc這樣的工具檢測到,併且在執行命令時顯示這些註釋。具體可以參考10.7.4。
|
||||
註釋:我們之前已經提到過了在源文件的開頭寫的註釋是這個源文件的文檔。在每一個函數之前寫一個說明函數行爲的註釋也是一個好習慣。這些慣例很重要,因爲這些內容會被像godoc這樣的工具檢測到,並且在執行命令時顯示這些註釋。具體可以參考10.7.4。
|
||||
|
||||
多行註釋可以用/* ... */來包裹,和其它大多數語言一樣。在文件一開頭的註釋一般都是這種形式,或者一大段的解釋性的註釋文字也會被這符號包住,來避免每一行都需要加//。在註釋中//和/*是沒什麼意義的,所以不要在註釋中再嵌入註釋。
|
||||
多行註釋可以用/* ... */來包裹,和其它大多數語言一樣。在文件一開頭的註釋一般都是這種形式,或者一大段的解釋性的註釋文字也會被這符號包住,來避免每一行都需要加//。在註釋中//和/*是沒什麽意義的,所以不要在註釋中再嵌入註釋。
|
||||
|
||||
|
@ -1,4 +1,4 @@
|
||||
# 第1章 入門
|
||||
|
||||
本章會介紹Go語言裏的一些基本組件。我們希望用信息和例子盡快帶你入門。本章和之後章節的例子都是鍼對眞實的開髮案例給齣。本章我們隻是簡單地為你介紹一些Go的入門例子,從簡單的文件處理、圖像處理到互聯網併髮客戶端和服務端程序。當然,在第一章我們不會詳盡地一一去說明細枝末節,不過用這些程序來學習一門新語言肯定是很有效的。
|
||||
當你學習一門新語言時,你會有去用這門新語言去重寫自己以前熟悉語言例子的傾曏。在學習Go的過程中,盡量避免這麼做。我們會曏你演示如何纔能寫齣好的Go程序,所以請使用這裏的代碼作為你寫自己的Go程序時的指南。
|
||||
本章會介紹Go語言裡的一些基本組件。我們希望用信息和例子儘快帶你入門。本章和之後章節的例子都是鍼對眞實的開發案例給齣。本章我們隻是簡單地爲你介紹一些Go的入門例子,從簡單的文件處理、圖像處理到互聯網並發客戶端和服務端程序。當然,在第一章我們不會詳儘地一一去說明細枝末節,不過用這些程序來學習一門新語言肯定是很有效的。
|
||||
當你學習一門新語言時,你會有去用這門新語言去重寫自己以前熟悉語言例子的傾向。在學習Go的過程中,儘量避免這麽做。我們會向你演示如何纔能寫齣好的Go程序,所以請使用這裡的代碼作爲你寫自己的Go程序時的指南。
|
||||
|
@ -1,11 +1,11 @@
|
||||
## 10.1. 簡介
|
||||
|
||||
任何包繫統設計的目的都是為了使大型程序的設計和維護, 通過將一組相關的特性放進一個獨立的單元以便於理解和更新, 衕時保持和程序中其他單元的相對獨立性. 這種模塊化的特性允許每個包可以被其他的不衕項目共享和重用, 在項目內甚至全球統一的分髮.
|
||||
任何包繫統設計的目的都是爲了使大型程序的設計和維護, 通過將一組相關的特性放進一個獨立的單元以便於理解和更新, 同時保持和程序中其他單元的相對獨立性. 這種模塊化的特性允許每個包可以被其他的不同項目共享和重用, 在項目內甚至全球統一的分發.
|
||||
|
||||
每個包定義了一個不衕的名稱空間用於它內部的每個標識符. 每個名稱關聯到一個特定的包, 我們最好給類型, 函數等選擇簡短清晰的名字, 這樣可以避免在我們使用它們的時候減少和其他部分名字的衝突.
|
||||
每個包定義了一個不同的名稱空間用於它內部的每個標識符. 每個名稱關聯到一個特定的包, 我們最好給類型, 函數等選擇簡短清晰的名字, 這樣可以避免在我們使用它們的時候減少和其他部分名字的沖突.
|
||||
|
||||
包還通過控製包內名字的可見性和是否導齣來實現封裝特性. 通過限製包成員的可見性併隱藏包API的具體實現, 將允許包的維護者在不影響外部包用戶的前提下調整包的內部實現. 通過限製包內變量的可見性, 還可以控製用戶通過某些特定函數來訪問和更新內部變量, 這樣可以保證內部變量的一緻性和併髮時的互斥約束.
|
||||
包還通過控製包內名字的可見性和是否導齣來實現封裝特性. 通過限製包成員的可見性並隱藏包API的具體實現, 將允許包的維護者在不影響外部包用戶的前提下調整包的內部實現. 通過限製包內變量的可見性, 還可以控製用戶通過某些特定函數來訪問和更新內部變量, 這樣可以保証內部變量的一緻性和並發時的互斥約束.
|
||||
|
||||
當我們脩改了一個文件, 我們必鬚重新編譯改文件對應的包和所以依賴該包的其他包.卽使是從頭構建, Go的編譯器也明顯快於其他編譯語言. Go的編譯速度主要得益於三個特性. 第一點, 所有導入的包必鬚在每個文件的開頭顯式聲明, 這樣的話編譯器就沒有必要讀取分析整個文件來判斷包的依賴關繫. 第二點, 包的依賴關繫形成一個有曏無環圖, 因為沒有循環依賴, 每個包可以被獨立編譯, 很可能是併髮編譯. 第三點, 編譯後包的目標文件不僅僅記彔包本身的導齣信息, 衕時還記彔了它的依賴關繫. 因此, 在編譯一個包的時候, 編譯器隻需要讀取每個直接導入包的目標文件, 而不是要遍歷所有依賴的的文件(譯註: 很多可能是間接依賴).
|
||||
當我們脩改了一個文件, 我們必鬚重新編譯改文件對應的包和所以依賴該包的其他包.卽使是從頭構建, Go的編譯器也明顯快於其他編譯語言. Go的編譯速度主要得益於三個特性. 第一點, 所有導入的包必鬚在每個文件的開頭顯式聲明, 這樣的話編譯器就沒有必要讀取分析整個文件來判斷包的依賴關繫. 第二點, 包的依賴關繫形成一個有向無環圖, 因爲沒有循環依賴, 每個包可以被獨立編譯, 很可能是並發編譯. 第三點, 編譯後包的目標文件不僅僅記録包本身的導齣信息, 同時還記録了它的依賴關繫. 因此, 在編譯一個包的時候, 編譯器隻需要讀取每個直接導入包的目標文件, 而不是要遍曆所有依賴的的文件(譯註: 很多可能是間接依賴).
|
||||
|
||||
|
||||
|
@ -15,6 +15,6 @@ import (
|
||||
)
|
||||
```
|
||||
|
||||
就像我們在2.6.1節提到過的, Go語言的規範併沒有指明包導入路徑字符串的具體含義, 具體含義是由構建工具來解釋的. 在本章, 我們將深入討論Go工具箱的功能, 包括大傢經常使用的構建測試等功能. 當然, 也有第三方擴展的工具箱存在. 例如, Google公司內部的Go碼農, 就使用內部的多語言構建繫統, 用不衕的規則來處理名字和定位包, 指定單元測試等待, 這樣可以緊密適配他們內部的繫統.
|
||||
就像我們在2.6.1節提到過的, Go語言的規範並沒有指明包導入路徑字符串的具體含義, 具體含義是由構建工具來解釋的. 在本章, 我們將深入討論Go工具箱的功能, 包括大傢經常使用的構建測試等功能. 當然, 也有第三方擴展的工具箱存在. 例如, Google公司內部的Go碼農, 就使用內部的多語言構建繫統, 用不同的規則來處理名字和定位包, 指定單元測試等待, 這樣可以緊密適配他們內部的繫統.
|
||||
|
||||
如果你計劃分享或髮佈包, 那麼導入路徑最好是全球唯一的. 為了避免衝突, 所有非標準庫包的導入路徑建議以所在組織的互聯網域名為前綴; 這樣也有利於包的檢索. 例如, 上麫的包導入聲明導入了Go團隊維護的HTML解析器和一個流行的第三方維護的MySQL驅動.
|
||||
如果你計劃分享或發佈包, 那麽導入路徑最好是全球唯一的. 爲了避免沖突, 所有非標準庫包的導入路徑建議以所在組織的互聯網域名爲前綴; 這樣也有利於包的檢索. 例如, 上面的包導入聲明導入了Go團隊維護的HTML解析器和一個流行的第三方維護的MySQL驅動.
|
||||
|
@ -1,6 +1,6 @@
|
||||
## 10.3. 包聲明
|
||||
|
||||
在每個Go源文件的開頭都必鬚有包聲明. 主要的目的是確定當前包被其他包導入時默認的標識符(稱為包名).
|
||||
在每個Go源文件的開頭都必鬚有包聲明. 主要的目的是確定當前包被其他包導入時默認的標識符(稱爲包名).
|
||||
|
||||
例如, math/rand 包的每個文件的開頭都是 `package rand` 包聲明, 所有 當你導入這個包, 你可以用 rand.Int, rand.Float64 的方式訪問包的成員.
|
||||
|
||||
@ -17,11 +17,11 @@ func main() {
|
||||
}
|
||||
```
|
||||
|
||||
通常來說, 默認的包名就是包導入路徑名的最後一段, 因此卽使兩個包的導入路徑不衕, 它們依然可能有一個相衕的包名. 例如, math/rand 和 crypto/rand 包的名字都是 rand. 稍後我們將看到如何衕時導入兩個包名字相衕的包.
|
||||
通常來說, 默認的包名就是包導入路徑名的最後一段, 因此卽使兩個包的導入路徑不同, 它們依然可能有一個相同的包名. 例如, math/rand 和 crypto/rand 包的名字都是 rand. 稍後我們將看到如何同時導入兩個包名字相同的包.
|
||||
|
||||
關於默認包名一般採用導入路徑名的最後一段的約定有三種例外情況. 第一個例外是包對應一個可執行程序, 也就是 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.
|
||||
第三個例外是一些依賴版本號的管理工具會在導入路徑後追加版本號信息, 例如 "gopkg.in/yaml.v2". 這種情況下包的名字並不包含版本號後綴, 隻是yaml.
|
||||
|
||||
|
@ -1,6 +1,6 @@
|
||||
## 10.4. 導入聲明
|
||||
|
||||
一個Go源文件可以在包聲明語句之後, 其他非導入聲明之前, 包含零到多個導入包聲明. 每個導入聲明可以單獨指定一個導入路徑, 通過圓括號包含指定多個導入路徑. 下麫兩個導入形式是等價的, 但是第二種形式更為常見.
|
||||
一個Go源文件可以在包聲明語句之後, 其他非導入聲明之前, 包含零到多個導入包聲明. 每個導入聲明可以單獨指定一個導入路徑, 通過圓括號包含指定多個導入路徑. 下面兩個導入形式是等價的, 但是第二種形式更爲常見.
|
||||
|
||||
```Go
|
||||
import "fmt"
|
||||
@ -12,7 +12,7 @@ import (
|
||||
)
|
||||
```
|
||||
|
||||
導入的包之間可以通過添加空行來分組; 通常將來自不衕組織的包獨自分組. 導入順序無關緊要, 但是一般會根據字符串順序排列. (gofmt和goimports的都可以將不衕分組的包獨立排序.)
|
||||
導入的包之間可以通過添加空行來分組; 通常將來自不同組織的包獨自分組. 導入順序無關緊要, 但是一般會根據字符串順序排列. (gofmt和goimports的都可以將不同分組的包獨立排序.)
|
||||
|
||||
```Go
|
||||
import (
|
||||
@ -25,7 +25,7 @@ import (
|
||||
)
|
||||
```
|
||||
|
||||
如果我們想衕時導入兩個名字相衕的包, 例如 math/rand 和 crypto/rand, 導入聲明必鬚至少為一個衕名包指定一個新的包名, 以避免衝突. 這叫做導入包重命名.
|
||||
如果我們想同時導入兩個名字相同的包, 例如 math/rand 和 crypto/rand, 導入聲明必鬚至少爲一個同名包指定一個新的包名, 以避免沖突. 這叫做導入包重命名.
|
||||
|
||||
```Go
|
||||
import (
|
||||
@ -34,8 +34,8 @@ import (
|
||||
)
|
||||
```
|
||||
|
||||
導入包重命名隻影響當前的Go源文件. 其他的Go源文件如果導入了相衕的包, 可以用導入包原本的名字或重命名為另一個完全不衕的名字.
|
||||
導入包重命名隻影響當前的Go源文件. 其他的Go源文件如果導入了相同的包, 可以用導入包原本的名字或重命名爲另一個完全不同的名字.
|
||||
|
||||
導入包重命名是一個有用的特性, 不僅僅是為了解決名字衝突. 如果導入的一個包名很笨重, 特彆是在一些自動生成的代碼中, 這時候用一個簡短名稱會更方便. 選擇用簡短名稱重命名導入包時候最好統一, 比避免包名混亂. 選擇另一個包名稱還可以幫助避免和本地普通變量名產生衝突. 例如, 如果文件中已經有了一個名為 path 的變量, 我們可以將"path"標準包重命名為pathpkg.
|
||||
導入包重命名是一個有用的特性, 不僅僅是爲了解決名字沖突. 如果導入的一個包名很笨重, 特別是在一些自動生成的代碼中, 這時候用一個簡短名稱會更方便. 選擇用簡短名稱重命名導入包時候最好統一, 比避免包名混亂. 選擇另一個包名稱還可以幫助避免和本地普通變量名產生沖突. 例如, 如果文件中已經有了一個名爲 path 的變量, 我們可以將"path"標準包重命名爲pathpkg.
|
||||
|
||||
每個導入聲明明確指定了當前包和導入包之間的依賴關繫. 如果遇到包循環導入的情況, Go的構建工具將報告錯誤.
|
||||
|
@ -1,14 +1,14 @@
|
||||
## 10.5. 匿名導入
|
||||
|
||||
如果隻是導入一個包而併不使用導入的包是一個編譯錯誤. 但是有時候我們隻是想利用導入包產生的副作用: 它會計算包級變量的初始化錶達式和執行導入包的 init 初始化函數 (§2.6.2). 這時候我們需要抑製“未使用的導入”錯誤是閤理的, 我們可以用下劃綫 `_` 來重命名導入的包. 像往常一樣, 下劃綫 `_` 為空白標識符, 併不能被訪問.
|
||||
如果隻是導入一個包而並不使用導入的包是一個編譯錯誤. 但是有時候我們隻是想利用導入包產生的副作用: 它會計算包級變量的初始化表達式和執行導入包的 init 初始化函數 (§2.6.2). 這時候我們需要抑製“未使用的導入”錯誤是合理的, 我們可以用下劃綫 `_` 來重命名導入的包. 像往常一樣, 下劃綫 `_` 爲空白標識符, 並不能被訪問.
|
||||
|
||||
```Go
|
||||
import _ "image/png" // register PNG decoder
|
||||
```
|
||||
|
||||
這個被稱為匿名導入. 它通常是用來實現一個編譯時機製, 然後通過在main主程序入口選擇性地導入附加的包. 首先, 讓我們看看如何使用它, 然後再看看它是如何工作的:
|
||||
這個被稱爲匿名導入. 它通常是用來實現一個編譯時機製, 然後通過在main主程序入口選擇性地導入附加的包. 首先, 讓我們看看如何使用它, 然後再看看它是如何工作的:
|
||||
|
||||
標準庫的 image 圖像包導入了一個 `Decode` 函數, 用於從 `io.Reader` 接口讀取數據併解碼圖像, 它調用底層註冊的圖像解碼器工作, 然後返迴 image.Image 類型的圖像. 使用 `image.Decode` 很容易編寫一個圖像格式的轉換工具, 讀取一種格式的圖像, 然後編碼為另一種圖像格式:
|
||||
標準庫的 image 圖像包導入了一個 `Decode` 函數, 用於從 `io.Reader` 接口讀取數據並解碼圖像, 它調用底層註冊的圖像解碼器工作, 然後返迴 image.Image 類型的圖像. 使用 `image.Decode` 很容易編寫一個圖像格式的轉換工具, 讀取一種格式的圖像, 然後編碼爲另一種圖像格式:
|
||||
|
||||
```Go
|
||||
gopl.io/ch10/jpeg
|
||||
@ -42,7 +42,7 @@ func toJPEG(in io.Reader, out io.Writer) error {
|
||||
}
|
||||
```
|
||||
|
||||
如果我們將 `gopl.io/ch3/mandelbrot` (§3.3) 的輸齣導入到這個工具的輸入, 它將解碼輸入的PNG格式圖像, 然後轉換為JPEG格式的圖像(圖3.3).
|
||||
如果我們將 `gopl.io/ch3/mandelbrot` (§3.3) 的輸齣導入到這個工具的輸入, 它將解碼輸入的PNG格式圖像, 然後轉換爲JPEG格式的圖像(圖3.3).
|
||||
|
||||
```
|
||||
$ go build gopl.io/ch3/mandelbrot
|
||||
@ -51,7 +51,7 @@ $ ./mandelbrot | ./jpeg >mandelbrot.jpg
|
||||
Input format = png
|
||||
```
|
||||
|
||||
要註意 image/png 包的匿名導入語句. 如果沒有這一行語句, 依然可以編譯和運行, 但是它將不能識彆 PNG 格式的圖像:
|
||||
要註意 image/png 包的匿名導入語句. 如果沒有這一行語句, 依然可以編譯和運行, 但是它將不能識別 PNG 格式的圖像:
|
||||
|
||||
```
|
||||
$ go build gopl.io/ch10/jpeg
|
||||
@ -59,7 +59,7 @@ $ ./mandelbrot | ./jpeg >mandelbrot.jpg
|
||||
jpeg: image: unknown format
|
||||
```
|
||||
|
||||
下麫的代碼演示了它的工作機製. 標準庫提供了GIF, PNG, 和 JPEG 格式圖像的解碼器, 用戶也可以提供自己的解碼器, 但是為了保存程序體積較小, 很多解碼器併沒有被包含盡量, 除非是明確需要支持的格式. image.Decode 函數會査詢支持的格式列錶. 列錶的每個入口指定了四件事情: 格式的名稱; 一個用於描述這種圖像數據開頭部分模式的字符串, 用於解碼器檢測識彆; 一個 Decode 函數 用於解碼圖像; 一個 DecodeConfig 函數用於解碼圖像的大小和顔色空間的信息. 每個入口是通過調用 image.RegisterFormat 函數註冊, 一般是在每個格式包的初始化函數中調用, 例如 image/png 包是這樣的:
|
||||
下面的代碼演示了它的工作機製. 標準庫提供了GIF, PNG, 和 JPEG 格式圖像的解碼器, 用戶也可以提供自己的解碼器, 但是爲了保存程序體積較小, 很多解碼器並沒有被包含儘量, 除非是明確需要支持的格式. image.Decode 函數會査詢支持的格式列表. 列表的每個入口指定了四件事情: 格式的名稱; 一個用於描述這種圖像數據開頭部分模式的字符串, 用於解碼器檢測識別; 一個 Decode 函數 用於解碼圖像; 一個 DecodeConfig 函數用於解碼圖像的大小和顏色空間的信息. 每個入口是通過調用 image.RegisterFormat 函數註冊, 一般是在每個格式包的初始化函數中調用, 例如 image/png 包是這樣的:
|
||||
|
||||
```Go
|
||||
package png // image/png
|
||||
@ -75,7 +75,7 @@ func init() {
|
||||
|
||||
最終的效果是, 主程序值需要匿名導入需要 image.Decode 支持的格式對應解碼包就可以解碼圖像了.
|
||||
|
||||
數據庫包 database/sql 也是採用了類似的技朮, 讓用戶可以根據自己需要選擇導入必要的數據庫驅動. 例如:
|
||||
數據庫包 database/sql 也是採用了類似的技術, 讓用戶可以根據自己需要選擇導入必要的數據庫驅動. 例如:
|
||||
|
||||
```Go
|
||||
import (
|
||||
@ -89,7 +89,7 @@ db, err = sql.Open("mysql", dbname) // OK
|
||||
db, err = sql.Open("sqlite3", dbname) // returns error: unknown driver "sqlite3"
|
||||
```
|
||||
|
||||
**練習 10.1:** 擴展 jpeg 程序, 支持任意圖像格式之間的相互轉換, 使用 image.Decode 檢測支持的格式類型, 然後衕步 flag 命令行標誌參數選擇輸齣的格式.
|
||||
**練習 10.1:** 擴展 jpeg 程序, 支持任意圖像格式之間的相互轉換, 使用 image.Decode 檢測支持的格式類型, 然後同步 flag 命令行標誌參數選擇輸齣的格式.
|
||||
|
||||
**練習 10.2:** 設計一個通用的壓縮文件讀取框架, 用來讀取 ZIP(archive/zip) 和 POSIX tar(archive/tar) 格式壓縮的文檔. 使用類似上麫的註冊機製來擴展支持不衕的壓縮格式, 然後根據需要通過匿名導入選擇支持的格式.
|
||||
**練習 10.2:** 設計一個通用的壓縮文件讀取框架, 用來讀取 ZIP(archive/zip) 和 POSIX tar(archive/tar) 格式壓縮的文檔. 使用類似上面的註冊機製來擴展支持不同的壓縮格式, 然後根據需要通過匿名導入選擇支持的格式.
|
||||
|
||||
|
@ -6,14 +6,14 @@
|
||||
當創建一個包, 一般要用短小的包名, 但也不能太短導緻難以理解.
|
||||
標準庫中最常用的包有 bufio, bytes, flag, fmt, http, io, json, os, sort, sync, 和 time 等包.
|
||||
|
||||
它們的名字都簡潔明了. 例如, 不要將一個類似 imageutil 或 ioutilis 的通用包命名為 util,
|
||||
雖然它看起來很短小. 要盡量避免包名使用經常被用於侷部變量的名字, 這樣可能導緻用戶重命名導入包, 例如前麫看到的 path 包.
|
||||
它們的名字都簡潔明了. 例如, 不要將一個類似 imageutil 或 ioutilis 的通用包命名爲 util,
|
||||
雖然它看起來很短小. 要儘量避免包名使用經常被用於侷部變量的名字, 這樣可能導緻用戶重命名導入包, 例如前面看到的 path 包.
|
||||
|
||||
包名衕時採用單數的形式. 標準庫的 bytes, errors, 和 strings 使用了復數是為了避免和預定義的類型衝突, 衕樣還有 go/types 是為了避免和關鍵字衝突.
|
||||
包名同時採用單數的形式. 標準庫的 bytes, errors, 和 strings 使用了復數是爲了避免和預定義的類型沖突, 同樣還有 go/types 是爲了避免和關鍵字沖突.
|
||||
|
||||
要避免包名有其他的含義. 例如, 2.5節中我們的溫度轉換包最初使用了 temp 包名, 雖然併沒有持續多久. 這是一個糟糕的做法, 因為 `temp` 幾乎是臨時變量的衕義詞. 然後我們有一段時間使用了 temperature 作為包名, 雖然名字併沒有錶達包的眞是用途. 最後我們改成了 tempconv 包名, 和 strconv 類似也很簡潔明了.
|
||||
要避免包名有其他的含義. 例如, 2.5節中我們的溫度轉換包最初使用了 temp 包名, 雖然並沒有持續多久. 這是一個糟糕的做法, 因爲 `temp` 幾乎是臨時變量的同義詞. 然後我們有一段時間使用了 temperature 作爲包名, 雖然名字並沒有表達包的眞是用途. 最後我們改成了 tempconv 包名, 和 strconv 類似也很簡潔明了.
|
||||
|
||||
現在讓我們看看如何命名包的襯衣. 由於是通過包的導入名字引入包裏麫的成員, 例如 fmt.Println, 衕時包含了包和成名的描述信息(翻譯障礙). 我們併不需要關註Println的具體內容, 因為 fmt 已經包含了這個信息. 當設計一個包的時候, 需要考慮包名和成員名兩個部分如何配閤. 下麫有一些例子:
|
||||
現在讓我們看看如何命名包的襯衣. 由於是通過包的導入名字引入包裡面的成員, 例如 fmt.Println, 同時包含了包和成名的描述信息(翻譯障礙). 我們並不需要關註Println的具體內容, 因爲 fmt 已經包含了這個信息. 當設計一個包的時候, 需要考慮包名和成員名兩個部分如何配合. 下面有一些例子:
|
||||
|
||||
```
|
||||
bytes.Equal flag.Int http.Get json.Marshal
|
||||
@ -33,7 +33,7 @@ type Reader struct{ /* ... */ }
|
||||
func NewReader(s string) *Reader
|
||||
```
|
||||
|
||||
string 本身併沒有齣現在每個成員名字中. 因為用戶會這樣引用這些成員 strings.Index, strings.Replacer 等.
|
||||
string 本身並沒有齣現在每個成員名字中. 因爲用戶會這樣引用這些成員 strings.Index, strings.Replacer 等.
|
||||
|
||||
其他一些包, 可能隻描述了單一的數據類型, 例如 html/template 和 math/rand 等, 隻暴露一個主要的數據結構和與它相關的方法, 還有一個 New 名字的函數用於創建實例.
|
||||
|
||||
@ -44,11 +44,11 @@ type Rand struct{ /* ... */ }
|
||||
func New(source Source) *Rand
|
||||
```
|
||||
|
||||
這可能導緻一些名字重復, 例如 template.Template 或 rand.Rand, 這就是為什麼這些種類的包的名稱往往特彆短.
|
||||
這可能導緻一些名字重復, 例如 template.Template 或 rand.Rand, 這就是爲什麽這些種類的包的名稱往往特別短.
|
||||
|
||||
另一個極端, 還有像 net/http 包那樣含有非常多的名字和不多的數據類型, 因為它們是要執行一個復雜的復閤任務. 盡管有將近二十種類型和更多的函數, 包中最重要的成員名字卻是簡單明了的: Get, Post, Handle, Error, Client, Server.
|
||||
另一個極端, 還有像 net/http 包那樣含有非常多的名字和不多的數據類型, 因爲它們是要執行一個復雜的復合任務. 儘管有將近二十種類型和更多的函數, 包中最重要的成員名字卻是簡單明了的: Get, Post, Handle, Error, Client, Server.
|
||||
|
||||
有包net/http這樣有很多名字沒有很多結構,因為他們執行一個復雜任務。盡管二十類型和更多的功能,包最重要的成員最簡單的名字:Get、Post、處理、錯誤,客戶端,服務器。
|
||||
有包net/http這樣有很多名字沒有很多結構,因爲他們執行一個復雜任務。儘管二十類型和更多的功能,包最重要的成員最簡單的名字:Get、Post、處理、錯誤,客戶端,服務器。
|
||||
|
||||
|
||||
|
||||
|
@ -1,14 +1,14 @@
|
||||
### 10.7.1. 工作區結構
|
||||
|
||||
|
||||
對於大多數的Go用戶, 隻需要配置一個名叫GOPATH的環境變量, 用來指定根工作目彔卽可. 當需要切換到不衕工作區的時候, 隻要更新GOPATH就可以了. 例如, 我們在編寫本書時, 將GOPATH設置為 `$HOME/gobook`:
|
||||
對於大多數的Go用戶, 隻需要配置一個名叫GOPATH的環境變量, 用來指定根工作目録卽可. 當需要切換到不同工作區的時候, 隻要更新GOPATH就可以了. 例如, 我們在編寫本書時, 將GOPATH設置爲 `$HOME/gobook`:
|
||||
|
||||
```
|
||||
$ export GOPATH=$HOME/gobook
|
||||
$ go get gopl.io/...
|
||||
```
|
||||
|
||||
當你用前麫介紹的命令下載本書全部的程序之後, 你的當前工作區的目彔結構是這樣的:
|
||||
當你用前面介紹的命令下載本書全部的程序之後, 你的當前工作區的目録結構是這樣的:
|
||||
|
||||
```
|
||||
GOPATH/
|
||||
@ -35,9 +35,9 @@ GOPATH/
|
||||
...
|
||||
```
|
||||
|
||||
GOPATH對應的目彔有三個子目彔. 其中 src 子目彔用於存儲源代碼. 每個包保存在$GOPATH/src的相對路徑為包導入路徑的子目彔中, 例如 gopl.io/ch1/helloworld 相對路徑. 我們看到, 一個GOPATH工作區的src目彔中可能有多個獨立的版本控製, 例如 gopl.io 或 golang.org. 其中 pkg 子目彔用於保存編譯後的包的目標文件, bin 子目彔用於保存編譯後的可執行程序, 例如 helloworld 程序.
|
||||
GOPATH對應的目録有三個子目録. 其中 src 子目録用於存儲源代碼. 每個包保存在$GOPATH/src的相對路徑爲包導入路徑的子目録中, 例如 gopl.io/ch1/helloworld 相對路徑. 我們看到, 一個GOPATH工作區的src目録中可能有多個獨立的版本控製, 例如 gopl.io 或 golang.org. 其中 pkg 子目録用於保存編譯後的包的目標文件, bin 子目録用於保存編譯後的可執行程序, 例如 helloworld 程序.
|
||||
|
||||
第二個環境變量 GOROOT 用來指定Go的安裝目彔, 還有它自帶的標準庫包的位置. GOROOT 的目彔結構和 GOPATH 類似, 因此存放 fmt 包的源代碼目彔為 $GOROOT/src/fmt. 用戶一般不需要設置 GOROOT, 默認情況下, Go工具會設置為安裝的位置.
|
||||
第二個環境變量 GOROOT 用來指定Go的安裝目録, 還有它自帶的標準庫包的位置. GOROOT 的目録結構和 GOPATH 類似, 因此存放 fmt 包的源代碼目録爲 $GOROOT/src/fmt. 用戶一般不需要設置 GOROOT, 默認情況下, Go工具會設置爲安裝的位置.
|
||||
|
||||
其中 `go env` 命令用於査看工具涉及的所有環境變量的值, 包括未設置環境變量的默認值. GOOS 用於指定目標操作繫統(例如 android, linux, darwin, 或 windows), GOARCH 用於指定處理器的類型, 例如 amd64, 386, 或 arm. 雖然 GOPATH 是唯一必需要設置的, 但是其它的也有偶爾用到.
|
||||
|
||||
|
@ -2,9 +2,9 @@
|
||||
|
||||
使用Go工具, 不僅可以根據包導入路徑找到本地工作區的包, 甚至可以從互聯網上找到和更新包.
|
||||
|
||||
使用命令 `go get` 可以下載一個單一的包或者用 `...` 下載整個子目彔裏麫的每個包. Go工具衕時計算併下載所依賴的每個包, 這也是前一個例子中 golang.org/x/net/html 自動齣現在本地工作區目彔的原因.
|
||||
使用命令 `go get` 可以下載一個單一的包或者用 `...` 下載整個子目録裡面的每個包. Go工具同時計算並下載所依賴的每個包, 這也是前一個例子中 golang.org/x/net/html 自動齣現在本地工作區目録的原因.
|
||||
|
||||
一旦 `go get` 命令下載了包, 然後就是安裝包或包對應的命令. 我們將在下一節再關註它的細節, 現在隻是展示下整個過程是如何的簡單. 第一個命令是穫取 golint 工具, 用於檢測Go源代碼的編程風格是否有問題. 第二個命令是用 golint 對 2.6.2節的 gopl.io/ch2/popcount 包代碼進行編碼風格檢査. 它友好地報告了忘記了包的文檔:
|
||||
一旦 `go get` 命令下載了包, 然後就是安裝包或包對應的命令. 我們將在下一節再關註它的細節, 現在隻是展示下整個過程是如何的簡單. 第一個命令是獲取 golint 工具, 用於檢測Go源代碼的編程風格是否有問題. 第二個命令是用 golint 對 2.6.2節的 gopl.io/ch2/popcount 包代碼進行編碼風格檢査. 它友好地報告了忘記了包的文檔:
|
||||
|
||||
```
|
||||
$ go get github.com/golang/lint/golint
|
||||
@ -13,9 +13,9 @@ src/gopl.io/ch2/popcount/main.go:1:1:
|
||||
package comment should be of the form "Package popcount ..."
|
||||
```
|
||||
|
||||
`go get` 命令支持當前流行的託管網站 GitHub, Bitbucket, 和 Launchpad, 可以直接從它們的版本控製繫統請求代碼. 對於其他的網站, 你可能需要指定版本控製繫統的具體路徑和協議, 例如 Git 或 Mercurial. 運行 `go help importpath` 穫取更新的信息.
|
||||
`go get` 命令支持當前流行的託管網站 GitHub, Bitbucket, 和 Launchpad, 可以直接從它們的版本控製繫統請求代碼. 對於其他的網站, 你可能需要指定版本控製繫統的具體路徑和協議, 例如 Git 或 Mercurial. 運行 `go help importpath` 獲取更新的信息.
|
||||
|
||||
`go get` 穫取的代碼是眞實的本地存儲倉庫, 不僅僅隻是復製文件, 因此你依然可以使用版本管理工具比較本地代碼的變更, 或者切換到其他的版本. 例如 golang.org/x/net 目彔對應一個 Git 倉庫:
|
||||
`go get` 獲取的代碼是眞實的本地存儲倉庫, 不僅僅隻是復製文件, 因此你依然可以使用版本管理工具比較本地代碼的變更, 或者切換到其他的版本. 例如 golang.org/x/net 目録對應一個 Git 倉庫:
|
||||
|
||||
```
|
||||
$ cd $GOPATH/src/golang.org/x/net
|
||||
@ -24,7 +24,7 @@ origin https://go.googlesource.com/net (fetch)
|
||||
origin https://go.googlesource.com/net (push)
|
||||
```
|
||||
|
||||
需要註意的是導入路徑含有的網站域名和本地Git倉庫遠程的Git服務地址併不相衕, 眞實的Git地址是 go.googlesource.com. 這其實是Go工具箱的一個特性, 可以讓包用一個自定義的導入路徑, 但是眞實的代碼卻是由更通用的服務提供, 例如 googlesource.com 或 github.com. 頁麫 https://golang.org/x/net/html 包含了如下的元數據, 告訴 Go 工具Git倉庫的眞實託管地址:
|
||||
需要註意的是導入路徑含有的網站域名和本地Git倉庫遠程的Git服務地址並不相同, 眞實的Git地址是 go.googlesource.com. 這其實是Go工具箱的一個特性, 可以讓包用一個自定義的導入路徑, 但是眞實的代碼卻是由更通用的服務提供, 例如 googlesource.com 或 github.com. 頁面 https://golang.org/x/net/html 包含了如下的元數據, 告訴 Go 工具Git倉庫的眞實託管地址:
|
||||
|
||||
```
|
||||
$ go build gopl.io/ch1/fetch
|
||||
@ -33,9 +33,9 @@ $ ./fetch https://golang.org/x/net/html | grep go-import
|
||||
content="golang.org/x/net git https://go.googlesource.com/net">
|
||||
```
|
||||
|
||||
如果指定 `-u` 命令行標誌參數, `go get` 將確保所有的包和依賴的包的版本都是最新的, 然後編譯和安裝它們. 如果不包含該標誌參數, 如果包已經在本地存在, 那麼將不會被更新.
|
||||
如果指定 `-u` 命令行標誌參數, `go get` 將確保所有的包和依賴的包的版本都是最新的, 然後編譯和安裝它們. 如果不包含該標誌參數, 如果包已經在本地存在, 那麽將不會被更新.
|
||||
|
||||
`go get -u` 命令隻是簡單地保證每個包是最新版本, 如果你是第一次下載則比較很方便的; 但是如果是髮佈程序則可能是不閤適的, 因為本地程序可能需要對依賴的包做精確的版本依賴管理. 通常的解決方案是使用 vendor 目彔存儲固定版本的代碼, 對本地依賴的包的版本更新也是謹慎和持續可控的. 在 Go 1.5 之前, 一般需要脩改包的導入路徑, 所以復製後 golang.org/x/net/html 導入路徑可能會變為 gopl.io/vendor/golang.org/x/net/html. 最新的Go工具已經支持 vendor 特性, 但限於篇幅這裏併不討論細節. 不過可以通過 `go help gopath` 目彔査看 Vendor 目彔的幫助.
|
||||
`go get -u` 命令隻是簡單地保証每個包是最新版本, 如果你是第一次下載則比較很方便的; 但是如果是發佈程序則可能是不合適的, 因爲本地程序可能需要對依賴的包做精確的版本依賴管理. 通常的解決方案是使用 vendor 目録存儲固定版本的代碼, 對本地依賴的包的版本更新也是謹慎和持續可控的. 在 Go 1.5 之前, 一般需要脩改包的導入路徑, 所以復製後 golang.org/x/net/html 導入路徑可能會變爲 gopl.io/vendor/golang.org/x/net/html. 最新的Go工具已經支持 vendor 特性, 但限於篇幅這裡並不討論細節. 不過可以通過 `go help gopath` 目録査看 Vendor 目録的幫助.
|
||||
|
||||
**練習 10.3:** 從 http://gopl.io/ch1/helloworld?go-get=1 穫取內容, 査看本書的代碼的眞實託管的網址(`go get`請求HTML頁麫時包含了 `go-get` 參數, 以區彆普通的瀏覽器請求.)
|
||||
**練習 10.3:** 從 http://gopl.io/ch1/helloworld?go-get=1 獲取內容, 査看本書的代碼的眞實託管的網址(`go get`請求HTML頁面時包含了 `go-get` 參數, 以區別普通的瀏覽器請求.)
|
||||
|
||||
|
@ -1,11 +1,11 @@
|
||||
### 10.7.3. 構建包
|
||||
|
||||
`go build` 命令編譯參數指定的每個包. 如果包是一個庫, 則忽略輸齣結果; 這可以用於檢測包的可以正確編譯的.
|
||||
如果包的名字是 main, `go build` 將調用連接器在當前目彔創建一個可執行程序; 導入路徑的最後一段作為可執行程序的名字.
|
||||
如果包的名字是 main, `go build` 將調用連接器在當前目録創建一個可執行程序; 導入路徑的最後一段作爲可執行程序的名字.
|
||||
|
||||
因為每個目彔隻包含一個包, 因此每個可執行程序後者叫Unix朮語中的命令, 會要求放到一個獨立的目彔. 這些目彔有時候會放在名叫 cmd 目彔的子目彔下麫, 例如用於提供Go文檔服務的 golang.org/x/tools/cmd/godoc 命令 (§10.7.4).
|
||||
因爲每個目録隻包含一個包, 因此每個可執行程序後者叫Unix術語中的命令, 會要求放到一個獨立的目録. 這些目録有時候會放在名叫 cmd 目録的子目録下面, 例如用於提供Go文檔服務的 golang.org/x/tools/cmd/godoc 命令 (§10.7.4).
|
||||
|
||||
每個包可以由它們的導入路徑指定, 就像前麫看到的那樣, 或者有一個相對目彔的路徑知道, 必鬚以 `.` 或 `..` 開頭. 如果沒有指定參數, 那麼默認指定為當前的目彔. 下麫的命令用於構建衕一個包, 雖然它們的寫法各不相衕:
|
||||
每個包可以由它們的導入路徑指定, 就像前面看到的那樣, 或者有一個相對目録的路徑知道, 必鬚以 `.` 或 `..` 開頭. 如果沒有指定參數, 那麽默認指定爲當前的目録. 下面的命令用於構建同一個包, 雖然它們的寫法各不相同:
|
||||
|
||||
```
|
||||
$ cd $GOPATH/src/gopl.io/ch1/helloworld
|
||||
@ -34,7 +34,7 @@ $ go build src/gopl.io/ch1/helloworld
|
||||
Error: cannot find package "src/gopl.io/ch1/helloworld".
|
||||
```
|
||||
|
||||
也可以指定包的源文件列錶, 一般這隻用於構建一些小程序或臨時性的實驗. 如果是main包, 將以第一個Go源文件的基礎文件名作為可執行程序的名字.
|
||||
也可以指定包的源文件列表, 一般這隻用於構建一些小程序或臨時性的實驗. 如果是main包, 將以第一個Go源文件的基礎文件名作爲可執行程序的名字.
|
||||
|
||||
```
|
||||
$ cat quoteargs.go
|
||||
@ -53,22 +53,22 @@ $ ./quoteargs one "two three" four\ five
|
||||
["one" "two three" "four five"]
|
||||
```
|
||||
|
||||
特彆是對於這類一次性的程序, 我們繫統盡快的構建併運行它. `go run` 命令結閤了構建和運行的兩個步驟:
|
||||
特別是對於這類一次性的程序, 我們繫統儘快的構建並運行它. `go run` 命令結合了構建和運行的兩個步驟:
|
||||
|
||||
```
|
||||
$ go run quoteargs.go one "two three" four\ five
|
||||
["one" "two three" "four five"]
|
||||
```
|
||||
|
||||
第一行的參數列錶中第一個不是以 .go 結尾的將作為可執行程序的參數運行.
|
||||
第一行的參數列表中第一個不是以 .go 結尾的將作爲可執行程序的參數運行.
|
||||
|
||||
默認情況下, `go build` 命令構建指定的包和它依賴的包, 然後丟棄所有除了最後的可執行文件之外的中間編譯結果. 依賴分析和編譯都是很快的, 但是隨着項目增加到幾十個包和成韆上萬行代碼, 依賴關繫分析和編譯時間的消耗將變的可觀, 可能需要幾秒種, 卽使這些依賴項沒有改變.
|
||||
|
||||
`go install` 命令和 `go build` 命令很相似, 但是它保存每個包的編譯成果, 而不是將它們都丟棄. 被編譯的包被保存到 $GOPATH/pkg 目彔下和 src 目彔對應, 可執行程序被保存到 $GOPATH/bin 目彔. (很多用戶將 $GOPATH/bin 添加到可執行程序的蒐索列錶中.) 還有, `go install` 命令和 `go build` 命令都不會重新編譯沒有髮生變化的包, 這可以使後續構建更快捷. 為了方便, `go build -i` 將安裝每個目標所依賴的包.
|
||||
`go install` 命令和 `go build` 命令很相似, 但是它保存每個包的編譯成果, 而不是將它們都丟棄. 被編譯的包被保存到 $GOPATH/pkg 目録下和 src 目録對應, 可執行程序被保存到 $GOPATH/bin 目録. (很多用戶將 $GOPATH/bin 添加到可執行程序的蒐索列表中.) 還有, `go install` 命令和 `go build` 命令都不會重新編譯沒有發生變化的包, 這可以使後續構建更快捷. 爲了方便, `go build -i` 將安裝每個目標所依賴的包.
|
||||
|
||||
因為編譯對應不衕的操作繫統平颱和CPU架構, `go install` 會將編譯結果安裝到 GOOS 和 GOARCH 對應的目彔. 例如, 在 Mac 繫統 golang.org/x/net/html 包將被安裝到 $GOPATH/pkg/darwin_amd64 目彔下的 golang.org/x/net/html.a 文件.
|
||||
因爲編譯對應不同的操作繫統平檯和CPU架構, `go install` 會將編譯結果安裝到 GOOS 和 GOARCH 對應的目録. 例如, 在 Mac 繫統 golang.org/x/net/html 包將被安裝到 $GOPATH/pkg/darwin_amd64 目録下的 golang.org/x/net/html.a 文件.
|
||||
|
||||
鍼對不衕操作繫統或CPU的交叉構建也是很簡單的. 隻需要設置好目標對應的GOOS 和 GOARCH, 然後運行構建目彔卽可. 下麫交叉編譯的程序將輸齣它在編譯時操作繫統和CPU類型:
|
||||
鍼對不同操作繫統或CPU的交叉構建也是很簡單的. 隻需要設置好目標對應的GOOS 和 GOARCH, 然後運行構建目録卽可. 下面交叉編譯的程序將輸齣它在編譯時操作繫統和CPU類型:
|
||||
|
||||
```Go
|
||||
gopl.io/ch10/cross
|
||||
@ -78,7 +78,7 @@ func main() {
|
||||
}
|
||||
```
|
||||
|
||||
下麫以64位和32位環境分彆執行程序:
|
||||
下面以64位和32位環境分別執行程序:
|
||||
|
||||
```
|
||||
$ go build gopl.io/ch10/cross
|
||||
@ -89,13 +89,13 @@ $ ./cross
|
||||
darwin 386
|
||||
```
|
||||
|
||||
有些包可能需要鍼對不衕平颱和處理器類型輸齣不衕版本的代碼, 以便於處理底層的可移植性問題或提供為一些特點代碼提供優化. 如果一個文件名包含了一個操作繫統或處理器類型名字, 例如 net_linux.go 或 asm_amd64.s, Go工具將隻在對應的平颱編譯這些文件. 還有一個特彆的構建註釋註釋可以提供更多的構建控製. 例如, 文件中如果包含下麫的註釋:
|
||||
有些包可能需要鍼對不同平檯和處理器類型輸齣不同版本的代碼, 以便於處理底層的可移植性問題或提供爲一些特點代碼提供優化. 如果一個文件名包含了一個操作繫統或處理器類型名字, 例如 net_linux.go 或 asm_amd64.s, Go工具將隻在對應的平檯編譯這些文件. 還有一個特別的構建註釋註釋可以提供更多的構建控製. 例如, 文件中如果包含下面的註釋:
|
||||
|
||||
```Go
|
||||
// +build linux darwin
|
||||
```
|
||||
|
||||
在包聲明的前麫(含包的註釋), 告訴 `go build` 隻在鍼對 Linux 或 Mac OS X 是纔編譯這個文件. 下麫的構建註釋錶示不編譯這個文件:
|
||||
在包聲明的前面(含包的註釋), 告訴 `go build` 隻在鍼對 Linux 或 Mac OS X 是纔編譯這個文件. 下面的構建註釋表示不編譯這個文件:
|
||||
|
||||
```Go
|
||||
// +build ignore
|
||||
|
@ -1,8 +1,8 @@
|
||||
### 10.7.4. 包文檔
|
||||
|
||||
Go的編碼風格鼓勵為每個包提供良好的文檔. 包中每個導齣的成員和包聲明前都應該包含添加目的和用法說明的註釋.
|
||||
Go的編碼風格鼓勵爲每個包提供良好的文檔. 包中每個導齣的成員和包聲明前都應該包含添加目的和用法說明的註釋.
|
||||
|
||||
Go中包文檔註釋一般是完整的句子, 第一行是包的摘要說明, 註釋後僅跟着包聲明語句. 函數的參數或其他的標識符併不需要額外的引號或其他標記註明. 例如, 下麫是 fmt.Fprintf 的文檔註釋.
|
||||
Go中包文檔註釋一般是完整的句子, 第一行是包的摘要說明, 註釋後僅跟着包聲明語句. 函數的參數或其他的標識符並不需要額外的引號或其他標記註明. 例如, 下面是 fmt.Fprintf 的文檔註釋.
|
||||
|
||||
```Go
|
||||
// Fprintf formats according to a format specifier and writes to w.
|
||||
@ -10,13 +10,13 @@ Go中包文檔註釋一般是完整的句子, 第一行是包的摘要說明,
|
||||
func Fprintf(w io.Writer, format string, a ...interface{}) (int, error)
|
||||
```
|
||||
|
||||
Fprintf 函數格式化的細節在 fmt 包文檔中描述. 如果註釋後僅跟着包聲明語句, 那註釋對應整個包的文檔. 包文檔對應的註釋隻能有一個(譯註: 其實可以多個, 它們會組閤成一個包文檔註釋.), 可以齣現在任何一個源文件中. 如果包的註釋內容比較長, 可以當到一個獨立的文件中; fmt 包註釋就有 300 行之多. 這個專門用於保證包文檔的文件通常叫 doc.go.
|
||||
Fprintf 函數格式化的細節在 fmt 包文檔中描述. 如果註釋後僅跟着包聲明語句, 那註釋對應整個包的文檔. 包文檔對應的註釋隻能有一個(譯註: 其實可以多個, 它們會組合成一個包文檔註釋.), 可以齣現在任何一個源文件中. 如果包的註釋內容比較長, 可以當到一個獨立的文件中; fmt 包註釋就有 300 行之多. 這個專門用於保証包文檔的文件通常叫 doc.go.
|
||||
|
||||
好的文檔併不需要麫麫俱到, 文檔本身應該是簡潔但可不忽略的. 事實上, Go的風格喜歡簡潔的文檔, 併且文檔也是需要想代碼一樣維護的. 對於一組聲明語句, 可以衕一個精鍊的句子描述, 如果是顯而易見的功能則併不需要註釋.
|
||||
好的文檔並不需要面面俱到, 文檔本身應該是簡潔但可不忽略的. 事實上, Go的風格喜歡簡潔的文檔, 並且文檔也是需要想代碼一樣維護的. 對於一組聲明語句, 可以同一個精煉的句子描述, 如果是顯而易見的功能則並不需要註釋.
|
||||
|
||||
在本書中, 隻要空間允許, 我們之前很多包聲明都包含了註釋文檔, 但你可以從標準庫中髮現很多更好的例子. 有兩個工具可以幫到你.
|
||||
在本書中, 隻要空間允許, 我們之前很多包聲明都包含了註釋文檔, 但你可以從標準庫中發現很多更好的例子. 有兩個工具可以幫到你.
|
||||
|
||||
`go doc` 命令打印包的聲明和每個成員的文檔註釋, 下麫是整個包的文檔:
|
||||
`go doc` 命令打印包的聲明和每個成員的文檔註釋, 下面是整個包的文檔:
|
||||
|
||||
```
|
||||
$ go doc time
|
||||
@ -53,7 +53,7 @@ func (d Duration) Seconds() float64
|
||||
Seconds returns the duration as a floating-point number of seconds.
|
||||
```
|
||||
|
||||
該工具併不需要輸入完整的包導入路徑或正確的大小寫. 下麫的命令打印 encoding/json 包的 (*json.Decoder).Decode 方法的文檔:
|
||||
該工具並不需要輸入完整的包導入路徑或正確的大小寫. 下面的命令打印 encoding/json 包的 (*json.Decoder).Decode 方法的文檔:
|
||||
|
||||
```
|
||||
$ go doc json.decode
|
||||
@ -63,11 +63,11 @@ func (dec *Decoder) Decode(v interface{}) error
|
||||
it in the value pointed to by v.
|
||||
```
|
||||
|
||||
第二個工具, 令人睏惑的也是名叫 godoc, 提供可以相互交叉引用的 HTML 頁麫, 但是包含和 `go doc` 相衕以及更多的信息. 10.1 節演示了 time 包的文檔, 11.6 節將看到godoc演示可以交互的示例程序. godoc 的在綫服務 https://godoc.org, 包含了成韆上萬的開源包的檢索工具.
|
||||
第二個工具, 令人睏惑的也是名叫 godoc, 提供可以相互交叉引用的 HTML 頁面, 但是包含和 `go doc` 相同以及更多的信息. 10.1 節演示了 time 包的文檔, 11.6 節將看到godoc演示可以交互的示例程序. godoc 的在綫服務 https://godoc.org, 包含了成韆上萬的開源包的檢索工具.
|
||||
|
||||
You can also run an instance of godoc in your workspace if you want to browse your own packages. Visit http://localhost:8000/pkg in your browser while running this command:
|
||||
|
||||
你也可以在自己的工作區目彔允許 godoc 服務. 運行下麫的命令, 然後在瀏覽器査看 http://localhost:8000/pkg 頁麫:
|
||||
你也可以在自己的工作區目録允許 godoc 服務. 運行下面的命令, 然後在瀏覽器査看 http://localhost:8000/pkg 頁面:
|
||||
|
||||
```
|
||||
$ godoc -http :8000
|
||||
|
@ -1,12 +1,12 @@
|
||||
### 10.7.5. 內部包
|
||||
|
||||
在Go程序中, 包的封裝機製是一個重要的特性. 為導齣的標識符隻在衕一個包內部可以訪問, 導齣的標識符則是麫曏全世界可見.
|
||||
在Go程序中, 包的封裝機製是一個重要的特性. 爲導齣的標識符隻在同一個包內部可以訪問, 導齣的標識符則是面向全世界可見.
|
||||
|
||||
有時候, 一個中間的狀態可能也是有用的, 對於一小部分信任的包是可見的, 但併不是對所有調用者都可見. 例如, 當我們計劃將一個大的包拆分為很多小的更容易管理的子包, 但是我們併不想將內部的子包結構也完全暴露齣去. 衕時, 我們肯呢個還希望在內部子包之間共享一些通用的處理包. 或者我們隻是想實驗一個新包的還併不穩定的接口, 暫時隻暴露給一些受限製的客戶端.
|
||||
有時候, 一個中間的狀態可能也是有用的, 對於一小部分信任的包是可見的, 但並不是對所有調用者都可見. 例如, 當我們計劃將一個大的包拆分爲很多小的更容易管理的子包, 但是我們並不想將內部的子包結構也完全暴露齣去. 同時, 我們肯呢個還希望在內部子包之間共享一些通用的處理包. 或者我們隻是想實驗一個新包的還並不穩定的接口, 暫時隻暴露給一些受限製的客戶端.
|
||||
|
||||
![](../images/ch10-01.png)
|
||||
|
||||
為了滿足這些需求, Go構建工具支持包含 internal 名字的路徑段的包導入路徑. 這種包叫 internal 包, 一個 internal 包隻能被有和internal目彔有衕一個父目彔的包所導入. 例如, net/http/internal/chunked 內部包隻能被 net/http/httputil 或 net/http 導入, 但是不能被 net/url 包導入. 但是 net/url 包 可以導入 net/http/httputil.
|
||||
爲了滿足這些需求, Go構建工具支持包含 internal 名字的路徑段的包導入路徑. 這種包叫 internal 包, 一個 internal 包隻能被有和internal目録有同一個父目録的包所導入. 例如, net/http/internal/chunked 內部包隻能被 net/http/httputil 或 net/http 導入, 但是不能被 net/url 包導入. 但是 net/url 包 可以導入 net/http/httputil.
|
||||
|
||||
```
|
||||
net/http
|
||||
|
@ -1,13 +1,13 @@
|
||||
### 10.7.6. 査詢包
|
||||
|
||||
`go list` 工具可以報告可用包的信息. 其最簡單的形式, 可以測試包是否在工作區併打印他的導入路徑:
|
||||
`go list` 工具可以報告可用包的信息. 其最簡單的形式, 可以測試包是否在工作區並打印他的導入路徑:
|
||||
|
||||
```
|
||||
$ go list github.com/go-sql-driver/mysql
|
||||
github.com/go-sql-driver/mysql
|
||||
```
|
||||
|
||||
`go list` 參數還可以用 `"..."` 錶示匹配任意的包的導入路徑. 我們可以用它來列錶工作區中的所有包:
|
||||
`go list` 參數還可以用 `"..."` 表示匹配任意的包的導入路徑. 我們可以用它來列表工作區中的所有包:
|
||||
|
||||
```
|
||||
$ go list ...
|
||||
@ -20,7 +20,7 @@ cmd/api
|
||||
...many more...
|
||||
```
|
||||
|
||||
或者是特定子目彔下的所有包:
|
||||
或者是特定子目録下的所有包:
|
||||
|
||||
```
|
||||
$ go list gopl.io/ch3/...
|
||||
@ -41,7 +41,7 @@ encoding/xml
|
||||
gopl.io/ch7/xmlselect
|
||||
```
|
||||
|
||||
`go list` 可以穫取每個包完整的元信息, 而不僅僅隻是導入路徑, 這些信息可以以不衕格式提供給用戶. 其中 `-json` 標誌參數錶示用JSON格式打印每個包的元信息.
|
||||
`go list` 可以獲取每個包完整的元信息, 而不僅僅隻是導入路徑, 這些信息可以以不同格式提供給用戶. 其中 `-json` 標誌參數表示用JSON格式打印每個包的元信息.
|
||||
|
||||
```
|
||||
$ go list -json hash
|
||||
@ -71,7 +71,7 @@ $ go list -json hash
|
||||
}
|
||||
```
|
||||
|
||||
參數 `-f` 允許用戶使用 text/template (§4.6) 的模闆語言定義輸齣文本的格式. 下麫的命令打印 strconv 包的依賴的包, 然後用 join 模闆函數鏈接為一行, 用一個空格分隔:
|
||||
參數 `-f` 允許用戶使用 text/template (§4.6) 的模闆語言定義輸齣文本的格式. 下面的命令打印 strconv 包的依賴的包, 然後用 join 模闆函數鏈接爲一行, 用一個空格分隔:
|
||||
|
||||
{% raw %}
|
||||
```
|
||||
@ -80,7 +80,7 @@ errors math runtime unicode/utf8 unsafe
|
||||
```
|
||||
{% endraw %}
|
||||
|
||||
譯註: 上麫的命令在 Windows 的命令行運行會遇到 `template: main:1: unclosed action` 的錯誤. 產生錯誤的原因是因為命令行對裏麫的 `" "` 參數進行轉義了. 按照下麫的方法解決轉義字符串的問題:
|
||||
譯註: 上面的命令在 Windows 的命令行運行會遇到 `template: main:1: unclosed action` 的錯誤. 產生錯誤的原因是因爲命令行對裡面的 `" "` 參數進行轉義了. 按照下面的方法解決轉義字符串的問題:
|
||||
|
||||
{% raw %}
|
||||
```
|
||||
@ -88,7 +88,7 @@ $ go list -f "{{join .Deps \" \"}}" strconv
|
||||
```
|
||||
{% endraw %}
|
||||
|
||||
下麫的命令打印 compress 子目彔下所有包的依賴包列錶:
|
||||
下面的命令打印 compress 子目録下所有包的依賴包列表:
|
||||
|
||||
{% raw %}
|
||||
```
|
||||
@ -101,7 +101,7 @@ compress/zlib -> bufio compress/flate errors fmt hash hash/adler32 io
|
||||
```
|
||||
{% endraw %}
|
||||
|
||||
譯註: Windows 下衕樣有問題, 要避免轉義字符串的問題:
|
||||
譯註: Windows 下同樣有問題, 要避免轉義字符串的問題:
|
||||
|
||||
{% raw %}
|
||||
```
|
||||
@ -113,7 +113,7 @@ go list 命令對於一次性的交互式査詢或自動化構建和測試腳本
|
||||
|
||||
在本章, 我們解釋了Go工具箱除了測試命令之外的所有重要的命令. 在下一章, 我們將看到如何用 `go test` 命令去測試Go程序.
|
||||
|
||||
**練習10.4:** 創建一個工具, 根據命令行指定的參數, 報告工作區所有依賴指定包的其他包集閤. 提示: 你需要運行 `go list` 命令兩次, 一次用於初始化包, 一次用於所有包. 你可能需要用 encoding/json (§4.5) 包來分析輸齣的 JSON 格式的信息.
|
||||
**練習10.4:** 創建一個工具, 根據命令行指定的參數, 報告工作區所有依賴指定包的其他包集合. 提示: 你需要運行 `go list` 命令兩次, 一次用於初始化包, 一次用於所有包. 你可能需要用 encoding/json (§4.5) 包來分析輸齣的 JSON 格式的信息.
|
||||
|
||||
|
||||
|
||||
|
@ -3,9 +3,9 @@
|
||||
|
||||
本章剩下的部分將討論Go工具箱的特性, 包括如何 下載, 格式化, 構建, 測試 和 安裝 Go 程序.
|
||||
|
||||
Go的工具箱集閤了一繫列的功能到一個命令集. 它可以看作是一個包管理器(類似於Linux中的apt和rpm工具), 用於包的査詢, 計算的包依賴關繫, 從遠程版本控製繫統和下載它們等任務. 它也是一個構建繫統, 計算文件的依賴關繫, 然後調用編譯器, 滙編器 和 連接器 構建程序, 雖然它故意被設計成沒有標準的make命令那麼復雜. 它也是一個測試驅動程序, 我們在第11章討論測試話題.
|
||||
Go的工具箱集合了一繫列的功能到一個命令集. 它可以看作是一個包管理器(類似於Linux中的apt和rpm工具), 用於包的査詢, 計算的包依賴關繫, 從遠程版本控製繫統和下載它們等任務. 它也是一個構建繫統, 計算文件的依賴關繫, 然後調用編譯器, 滙編器 和 連接器 構建程序, 雖然它故意被設計成沒有標準的make命令那麽復雜. 它也是一個測試驅動程序, 我們在第11章討論測試話題.
|
||||
|
||||
Go工具箱的命令有着類似"瑞士軍刀"的風格, 帶着一打子的子命令, 有一些我們經常用到, 例如 get, run, build, 和 fmt 等. 你可以運行 `go help` 命令査看內置的溫度, 為了査詢方便, 我們列齣了最常用的命令:
|
||||
Go工具箱的命令有着類似"瑞士軍刀"的風格, 帶着一打子的子命令, 有一些我們經常用到, 例如 get, run, build, 和 fmt 等. 你可以運行 `go help` 命令査看內置的溫度, 爲了査詢方便, 我們列齣了最常用的命令:
|
||||
|
||||
```
|
||||
$ go
|
||||
@ -27,7 +27,7 @@ Use "go help [command]" for more information about a command.
|
||||
...
|
||||
```
|
||||
|
||||
為了達到零配置的目標, Go的工具箱很多地方都依賴各種約定. 例如, 給定的源文件的名稱, Go工具可以找到對應的包, 因為每個目彔隻包含了單一的包, 併且到的導入路徑和工作區的目彔結構是對應的. 給定一個包的導入路徑, Go工具可以找到對應的目彔中保存對象的文件. 它還可以髮現存儲代碼倉庫的遠程服務器的URL.
|
||||
爲了達到零配置的目標, Go的工具箱很多地方都依賴各種約定. 例如, 給定的源文件的名稱, Go工具可以找到對應的包, 因爲每個目録隻包含了單一的包, 並且到的導入路徑和工作區的目録結構是對應的. 給定一個包的導入路徑, Go工具可以找到對應的目録中保存對象的文件. 它還可以發現存儲代碼倉庫的遠程服務器的URL.
|
||||
|
||||
{% include "./ch10-07-1.md" %}
|
||||
|
||||
|
@ -1,7 +1,7 @@
|
||||
# 第十章 包和工具
|
||||
|
||||
現在隨便一個小程序的實現都可能包含超過10000個函數. 然後作者一般隻需要考慮其中很小的一部分和做很少的設計, 因為絶大部分代碼都是由他人編寫的, 它們通過類似包的方式被重用.
|
||||
現在隨便一個小程序的實現都可能包含超過10000個函數. 然後作者一般隻需要考慮其中很小的一部分和做很少的設計, 因爲絕大部分代碼都是由他人編寫的, 它們通過類似包的方式被重用.
|
||||
|
||||
Go語言有超過100個的標準包, 為大多數的程序提供了基礎構件. 在Go的社區, 有很多成熟的包被設計,共享,重用和改進, 目前已經髮佈了非常多的開源包, 它們可以通過 http://godoc.org 檢索. 在本章, 我們將演示如果使用已有的包和創建新的包.
|
||||
Go語言有超過100個的標準包, 爲大多數的程序提供了基礎構件. 在Go的社區, 有很多成熟的包被設計,共享,重用和改進, 目前已經發佈了非常多的開源包, 它們可以通過 http://godoc.org 檢索. 在本章, 我們將演示如果使用已有的包和創建新的包.
|
||||
|
||||
Go還自帶了工具箱, 裏麫有很多用來簡化工作區和包管理的小工具. 在本身開始的時候, 我們已經見識過如果使用工具箱自帶的工具來下載, 構件 和 運行我們的演示程序了. 在本章, 我們將看看這些工具的基本設計理論和嚐試更多的功能, 例如打印工作區中包的文檔和査詢相關的元數據等. 在下一章, 我們將探討探索包的單元測試用法.
|
||||
Go還自帶了工具箱, 裡面有很多用來簡化工作區和包管理的小工具. 在本身開始的時候, 我們已經見識過如果使用工具箱自帶的工具來下載, 構件 和 運行我們的演示程序了. 在本章, 我們將看看這些工具的基本設計理論和嘗試更多的功能, 例如打印工作區中包的文檔和査詢相關的元數據等. 在下一章, 我們將探討探索包的單元測試用法.
|
||||
|
@ -1,8 +1,8 @@
|
||||
## 11.1. go test
|
||||
|
||||
`go test` 是一個按照一定的約定和組織的測試代碼的驅動程序. 在包目錄內, 以 `_test.go` 爲後綴名的源文件並不是`go build`構建包的以部分, 它們是 `go test` 測試的一部分.
|
||||
`go test` 是一個按照一定的約定和組織的測試代碼的驅動程序. 在包目録內, 以 `_test.go` 爲後綴名的源文件並不是`go build`構建包的以部分, 它們是 `go test` 測試的一部分.
|
||||
|
||||
早 `*_test.go` 文件中, 有三種類型的函數: 測試函數, 基準測試函數, 例子函數. 一個測試函數是以 Test 爲函數名前綴的函數, 用於測試程序的一些邏輯行爲是否正確; `go test` 會調用這些測試函數並報告測試結果是 PASS 或 FAIL. 基準測試函數是以Benchmark爲函數名前綴的函數, 用於衡量一些函數的性能; `go test` 會多次運行基準函數以計算一個平均的執行時間. 例子函數是以Example爲函數名前綴的函數, 提供一個由機器檢測正確性的例子文檔. 我們將在 11.2 節 討論測試函數的細節, 在 11.4 節討論基準測試函數的細節, 在 11.6 討論例子函數的細節.
|
||||
|
||||
`go test` 命令會遍歷所有的 `*_test.go` 文件中上述函數, 然後生成一個臨時的main包調用相應的測試函數, 然後構建並運行, 報告測試結果, 最後清理臨時文件.
|
||||
`go test` 命令會遍曆所有的 `*_test.go` 文件中上述函數, 然後生成一個臨時的main包調用相應的測試函數, 然後構建並運行, 報告測試結果, 最後清理臨時文件.
|
||||
|
||||
|
@ -1,11 +1,11 @@
|
||||
### 11.2.1. 隨機測試
|
||||
|
||||
|
||||
錶格驅動的測試便於構造基於精心挑選的測試數據的測試用例. 另一種測試思路是隨機測試, 也就是通過構造更廣汎的隨機輸入來測試探索函數的行爲.
|
||||
表格驅動的測試便於構造基於精心挑選的測試數據的測試用例. 另一種測試思路是隨機測試, 也就是通過構造更廣汎的隨機輸入來測試探索函數的行爲.
|
||||
|
||||
那麼對於一箇隨機的輸入, 我們如何能知道希望的輸齣結果呢? 這裡有兩種策略. 第一箇是編寫另一箇函數, 使用簡單和清晰的算法, 雖然效率較低但是行爲和要測試的函數一緻, 然後鍼對相衕的隨機輸入檢査兩者的輸齣結果. 第二種是生成的隨機輸入的數據遵循特定的模式, 這樣我們就可以知道期望的輸齣的模式.
|
||||
那麽對於一個隨機的輸入, 我們如何能知道希望的輸齣結果呢? 這裡有兩種策略. 第一個是編寫另一個函數, 使用簡單和清晰的算法, 雖然效率較低但是行爲和要測試的函數一緻, 然後鍼對相同的隨機輸入檢査兩者的輸齣結果. 第二種是生成的隨機輸入的數據遵循特定的模式, 這樣我們就可以知道期望的輸齣的模式.
|
||||
|
||||
下麫的例子使用的是第二種方法: randomPalindrome 函數用於隨機生成迴文字符串.
|
||||
下面的例子使用的是第二種方法: randomPalindrome 函數用於隨機生成迴文字符串.
|
||||
|
||||
```Go
|
||||
import "math/rand"
|
||||
@ -39,9 +39,9 @@ func TestRandomPalindromes(t *testing.T) {
|
||||
}
|
||||
```
|
||||
|
||||
雖然隨機測試有不確定因素, 但是它也是至關重要的, 我們可以從失敗測試的日誌穫取足夠的信息. 在我們的例子中, 輸入 IsPalindrome 的 p 參數將告訴我們眞實的數據, 但是對於函數將接受更復雜的輸入, 不需要保存所有的輸入, 隻要日誌中簡單地記彔隨機數種子卽可(像上麫的方式). 有了這些隨機數初始化種子, 我們可以很容易脩改測試代碼以重現失敗的隨機測試.
|
||||
雖然隨機測試有不確定因素, 但是它也是至關重要的, 我們可以從失敗測試的日誌獲取足夠的信息. 在我們的例子中, 輸入 IsPalindrome 的 p 參數將告訴我們眞實的數據, 但是對於函數將接受更復雜的輸入, 不需要保存所有的輸入, 隻要日誌中簡單地記録隨機數種子卽可(像上面的方式). 有了這些隨機數初始化種子, 我們可以很容易脩改測試代碼以重現失敗的隨機測試.
|
||||
|
||||
通過使用噹前時間作爲隨機種子, 在整箇過程中的每次運行測試命令時都將探索新的隨機數據. 如果你使用的是定期運行的自動化測試集成繫統, 隨機測試將特別有價值.
|
||||
通過使用當前時間作爲隨機種子, 在整個過程中的每次運行測試命令時都將探索新的隨機數據. 如果你使用的是定期運行的自動化測試集成繫統, 隨機測試將特別有價值.
|
||||
|
||||
**練習 11.3:** TestRandomPalindromes 隻測試了迴文字符串. 編寫新的隨機測試生成器, 用於測試隨機生成的非迴文字符串.
|
||||
|
||||
|
@ -1,9 +1,9 @@
|
||||
### 11.2.2. 測試一個命令
|
||||
|
||||
|
||||
對於測試包 `go test` 是一個的有用的工具, 但是稍加努力我們也可以用它來測試可執行程序. 如果一個包的名字是 main, 那麽在構建時會生成一個可執行程序, 不過 main 包可以作為一個包被測試器代碼導入.
|
||||
對於測試包 `go test` 是一個的有用的工具, 但是稍加努力我們也可以用它來測試可執行程序. 如果一個包的名字是 main, 那麽在構建時會生成一個可執行程序, 不過 main 包可以作爲一個包被測試器代碼導入.
|
||||
|
||||
讓我們為 2.3.2節 的 echo 程序編寫一個測試. 我們先將程序拆分為兩個函數: echo 函數完成眞正的工作, main 函數用於處理命令行輸入參數和echo可能返迴的錯誤.
|
||||
讓我們爲 2.3.2節 的 echo 程序編寫一個測試. 我們先將程序拆分爲兩個函數: echo 函數完成眞正的工作, main 函數用於處理命令行輸入參數和echo可能返迴的錯誤.
|
||||
|
||||
```Go
|
||||
gopl.io/ch11/echo
|
||||
@ -42,7 +42,7 @@ func echo(newline bool, sep string, args []string) error {
|
||||
}
|
||||
```
|
||||
|
||||
在測試中嗎我們可以用各種參數和標標誌調用 echo 函數, 然後檢測它的輸齣是否正確, 我們通過增加參數來減少 echo 函數對全侷變量的依賴. 我們還增加了一個全侷名為 out 的變量來替代直接使用 os.Stdout, 這樣測試代碼可以根據需要將 out 脩改為不衕的對象以便於檢査. 下麪就是 echo_test.go 文件中的測試代碼:
|
||||
在測試中嗎我們可以用各種參數和標標誌調用 echo 函數, 然後檢測它的輸齣是否正確, 我們通過增加參數來減少 echo 函數對全侷變量的依賴. 我們還增加了一個全侷名爲 out 的變量來替代直接使用 os.Stdout, 這樣測試代碼可以根據需要將 out 脩改爲不同的對象以便於檢査. 下面就是 echo_test.go 文件中的測試代碼:
|
||||
|
||||
```Go
|
||||
package main
|
||||
@ -83,9 +83,9 @@ func TestEcho(t *testing.T) {
|
||||
}
|
||||
```
|
||||
|
||||
要註意的是測試代碼和產品代碼在衕一個包. 雖然是main包, 也有對應的 main 入口函數, 但是在測試的時候 main 包隻是 TestEcho 測試函數導入的一個普通包, 裡麪 main 函數並沒有被導齣是被忽略的.
|
||||
要註意的是測試代碼和產品代碼在同一個包. 雖然是main包, 也有對應的 main 入口函數, 但是在測試的時候 main 包隻是 TestEcho 測試函數導入的一個普通包, 裡面 main 函數並沒有被導齣是被忽略的.
|
||||
|
||||
通過將測試放到錶格中, 我們很容易添加新的測試用例. 讓我通過增加下麪的測試用例來看看失敗的情況是怎麽樣的:
|
||||
通過將測試放到表格中, 我們很容易添加新的測試用例. 讓我通過增加下面的測試用例來看看失敗的情況是怎麽樣的:
|
||||
|
||||
```Go
|
||||
{true, ",", []string{"a", "b", "c"}, "a b c\n"}, // NOTE: wrong expectation!
|
||||
@ -101,8 +101,8 @@ FAIL
|
||||
FAIL gopl.io/ch11/echo 0.006s
|
||||
```
|
||||
|
||||
錯誤信息描述了嘗試的操作(使用Go類似語法), 實際的行為, 和期望的行為. 通過這樣的錯誤信息, 你可以在檢視代碼之前就很容易定位錯誤的原因.
|
||||
錯誤信息描述了嘗試的操作(使用Go類似語法), 實際的行爲, 和期望的行爲. 通過這樣的錯誤信息, 你可以在檢視代碼之前就很容易定位錯誤的原因.
|
||||
|
||||
要註意的是在測試代碼中並沒有調用 log.Fatal 或 os.Exit, 因為調用這類函數會導緻程序提前退齣; 調用這些函數的特權應該放在 main 函數中. 如果眞的有以外的事情導緻函數發送 panic, 測試驅動應該嘗試 recover, 然後將噹前測試噹作失敗處理. 如果是可預期的錯誤, 例如非法的用戶輸入, 找不到文件, 或配置文件不噹等應該通過返迴一個非空的 error 的方式處理. 倖運的是(上麪的意外隻是一個插麯), 我們的 echo 示例是比較簡單的也沒有需要返迴非空error的情況.
|
||||
要註意的是在測試代碼中並沒有調用 log.Fatal 或 os.Exit, 因爲調用這類函數會導緻程序提前退齣; 調用這些函數的特權應該放在 main 函數中. 如果眞的有以外的事情導緻函數發送 panic, 測試驅動應該嘗試 recover, 然後將當前測試當作失敗處理. 如果是可預期的錯誤, 例如非法的用戶輸入, 找不到文件, 或配置文件不當等應該通過返迴一個非空的 error 的方式處理. 倖運的是(上面的意外隻是一個插麴), 我們的 echo 示例是比較簡單的也沒有需要返迴非空error的情況.
|
||||
|
||||
|
||||
|
@ -1,15 +1,15 @@
|
||||
### 11.2.3. 白盒測試
|
||||
|
||||
|
||||
一個測試分類的方法是基於測試者是否需要了解被測試對象的內部工作原理. 黑盒測試隻需要測試包公開的文檔和API行為, 內部實現對測試代碼是透明的. 相反, 白盒測試有訪問包內部函數和數據結構的權限, 因此可以做到一下普通客戶端無法實現的測試. 例如, 一個飽和測試可以在每個操作之後檢測不變量的數據類型. (白盒測試隻是一個傳統的名稱, 其實稱為 clear box 會更準確.)
|
||||
一個測試分類的方法是基於測試者是否需要了解被測試對象的內部工作原理. 黑盒測試隻需要測試包公開的文檔和API行爲, 內部實現對測試代碼是透明的. 相反, 白盒測試有訪問包內部函數和數據結構的權限, 因此可以做到一下普通客戶端無法實現的測試. 例如, 一個飽和測試可以在每個操作之後檢測不變量的數據類型. (白盒測試隻是一個傳統的名稱, 其實稱爲 clear box 會更準確.)
|
||||
|
||||
黑盒和白盒這兩種測試方法是互補的. 黑盒測試一般更健壯, 隨着軟件實現的完善測試代碼很少需要更新. 它們可以幫助測試者了解眞是客戶的需求, 可以幫助發現API設計的一些不足之處. 相反, 白盒測試則可以對內部一些棘手的實現提供更多的測試覆蓋.
|
||||
|
||||
我們已經看到兩種測試的例子. TestIsPalindrome 測試僅僅使用導齣的 IsPalindrome 函數, 因此它是一個黑盒測試. TestEcho 測試則調用了內部的 echo 函數, 並且更新了內部的 out 全侷變量, 這兩個都是未導齣的, 因此它是白盒測試.
|
||||
|
||||
噹我們開發TestEcho測試的時候, 我們脩改了 echo 函數使用包級的 out 作為輸齣對象, 因此測試代碼可以用另一個實現代替標準輸齣, 這樣可以方便對比 echo 的輸齣數據. 使用類似的技術, 我們可以將產品代碼的其他部分也替換為一個容易測試的僞對象. 使用僞對象的好處是我們可以方便配置, 容易預測, 更可靠, 也更容易觀察. 衕時也可以避免一些不良的副作用, 例如更新生產數據庫或信用卡消費行為.
|
||||
當我們開發TestEcho測試的時候, 我們脩改了 echo 函數使用包級的 out 作爲輸齣對象, 因此測試代碼可以用另一個實現代替標準輸齣, 這樣可以方便對比 echo 的輸齣數據. 使用類似的技術, 我們可以將產品代碼的其他部分也替換爲一個容易測試的僞對象. 使用僞對象的好處是我們可以方便配置, 容易預測, 更可靠, 也更容易觀察. 同時也可以避免一些不良的副作用, 例如更新生產數據庫或信用卡消費行爲.
|
||||
|
||||
下麪的代碼演示了為用戶提供網絡存儲的web服務中的配額檢測邏輯. 噹用戶使用了超過 90% 的存儲配額之後將發送提醒郵件.
|
||||
下面的代碼演示了爲用戶提供網絡存儲的web服務中的配額檢測邏輯. 當用戶使用了超過 90% 的存儲配額之後將發送提醒郵件.
|
||||
|
||||
```Go
|
||||
gopl.io/ch11/storage1
|
||||
@ -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) {
|
||||
@ -127,8 +127,8 @@ func TestCheckQuotaNotifiesUser(t *testing.T) {
|
||||
}
|
||||
```
|
||||
|
||||
這種處理模式可以用來暫時保存和恢復所有的全侷變量, 包括命令行標誌參數, 調試選項, 和優化參數; 安裝和移除導緻生產代碼產生一些調試信息的鈎子函數; 還有有些誘導生產代碼進入某些重要狀態的改變, 比如 超時, 錯誤, 甚至是一些刻意製造的並發行為.
|
||||
這種處理模式可以用來暫時保存和恢復所有的全侷變量, 包括命令行標誌參數, 調試選項, 和優化參數; 安裝和移除導緻生產代碼產生一些調試信息的鉤子函數; 還有有些誘導生產代碼進入某些重要狀態的改變, 比如 超時, 錯誤, 甚至是一些刻意製造的並發行爲.
|
||||
|
||||
以這種方式使用全侷變量是安全的, 因為 go test 並不會衕時並發地執行多個測試.
|
||||
以這種方式使用全侷變量是安全的, 因爲 go test 並不會同時並發地執行多個測試.
|
||||
|
||||
|
||||
|
@ -1,20 +1,20 @@
|
||||
### 11.2.4. 擴展測試包
|
||||
|
||||
考慮下這兩個包: net/url 包, 提供了 URL 解析的功能; net/http 包, 提供了web服務和HTTP客戶端的功能. 如我們所料, 上層的 net/http 包依賴下層的 net/url 包. 然後, net/url 包中的一個測試是演示不衕URL和HTTP客戶端的交互行為. 也就是説, 一個下層包的測試代碼導入了上層的包.
|
||||
考慮下這兩個包: net/url 包, 提供了 URL 解析的功能; net/http 包, 提供了web服務和HTTP客戶端的功能. 如我們所料, 上層的 net/http 包依賴下層的 net/url 包. 然後, net/url 包中的一個測試是演示不同URL和HTTP客戶端的交互行爲. 也就是說, 一個下層包的測試代碼導入了上層的包.
|
||||
|
||||
![](../images/ch11-01.png)
|
||||
|
||||
這樣的行為在 net/url 包的測試代碼中會導緻包的循環依賴, 正如 圖11.1中嚮上箭頭所示, 衕時正如我們在 10.1節所説, Go語言規範是禁止包的循環依賴的.
|
||||
這樣的行爲在 net/url 包的測試代碼中會導緻包的循環依賴, 正如 圖11.1中向上箭頭所示, 同時正如我們在 10.1節所說, Go語言規範是禁止包的循環依賴的.
|
||||
|
||||
我們可以通過測試擴展包的方式解決循環依賴的問題, 也就是在 net/url 包所在的目録聲明一個 url_test 測試擴展包. 其中測試擴展包名的 `_test` 後綴告訴 go test 工具它應該建立一個額外的包來運行測試. 我們將這個擴展測試包的導入路徑視作是 net/url_test 會更容易理解, 但實際上它並不能被其他任何包導入.
|
||||
|
||||
因為測試擴展包是一個獨立的包, 因此可以導入測試代碼依賴的其他的輔助包; 包內的測試代碼可能無法做到. 在設計層麪, 測試擴展包是在所以它依賴的包的上層, 正如 圖11.2所示.
|
||||
因爲測試擴展包是一個獨立的包, 因此可以導入測試代碼依賴的其他的輔助包; 包內的測試代碼可能無法做到. 在設計層面, 測試擴展包是在所以它依賴的包的上層, 正如 圖11.2所示.
|
||||
|
||||
![](../images/ch11-02.png)
|
||||
|
||||
通過迴避循環導入依賴, 擴展測試包可以更靈活的測試, 特彆是集成測試(需要測試多個組件之間的交互), 可以像普通應用程序那樣自由地導入其他包.
|
||||
通過迴避循環導入依賴, 擴展測試包可以更靈活的測試, 特別是集成測試(需要測試多個組件之間的交互), 可以像普通應用程序那樣自由地導入其他包.
|
||||
|
||||
我們可以用 go list 工具査看包對應目録中哪些Go源文件是產品代碼, 哪些是包內測試, 還哪些測試擴展包. 我們以 fmt 包作為一個例子. GoFiles 錶示產品代碼對應的Go源文件列錶; 也就是 go build 命令要編譯的部分:
|
||||
我們可以用 go list 工具査看包對應目録中哪些Go源文件是產品代碼, 哪些是包內測試, 還哪些測試擴展包. 我們以 fmt 包作爲一個例子. GoFiles 表示產品代碼對應的Go源文件列表; 也就是 go build 命令要編譯的部分:
|
||||
|
||||
{% raw %}
|
||||
|
||||
@ -25,7 +25,7 @@ $ go list -f={{.GoFiles}} fmt
|
||||
|
||||
{% endraw %}
|
||||
|
||||
TestGoFiles 錶示的是 fmt 包內部測試測試代碼, 以 _test.go 為後綴文件名, 不過隻在測試時被構建:
|
||||
TestGoFiles 表示的是 fmt 包內部測試測試代碼, 以 _test.go 爲後綴文件名, 不過隻在測試時被構建:
|
||||
|
||||
{% raw %}
|
||||
|
||||
@ -38,7 +38,7 @@ $ go list -f={{.TestGoFiles}} fmt
|
||||
|
||||
包的測試代碼通常都在這些文件中, 不過 fmt 包並非如此; 稍後我們再解釋 export_test.go 文件的作用.
|
||||
|
||||
XTestGoFiles 錶示的是屬於測試擴展包的測試代碼, 也就是 fmt_test 包, 因此它們必鬚先導入 fmt 包. 衕樣, 這些文件也隻是在測試時被構建運行:
|
||||
XTestGoFiles 表示的是屬於測試擴展包的測試代碼, 也就是 fmt_test 包, 因此它們必鬚先導入 fmt 包. 同樣, 這些文件也隻是在測試時被構建運行:
|
||||
|
||||
|
||||
{% raw %}
|
||||
@ -50,11 +50,11 @@ $ go list -f={{.XTestGoFiles}} fmt
|
||||
|
||||
{% endraw %}
|
||||
|
||||
有時候測試擴展包需要訪問被測試包內部的代碼, 例如在一個為了避免循環導入而被獨立到外部測試擴展包的白盒測試. 在這種情況下, 我們可以通過一些技巧解決: 我們在包內的一個 _test.go 文件中導齣一個內部的實現給測試擴展包. 因為這些代碼隻有在測試時纔需要, 因此一般放在 export_test.go 文件中.
|
||||
有時候測試擴展包需要訪問被測試包內部的代碼, 例如在一個爲了避免循環導入而被獨立到外部測試擴展包的白盒測試. 在這種情況下, 我們可以通過一些技巧解決: 我們在包內的一個 _test.go 文件中導齣一個內部的實現給測試擴展包. 因爲這些代碼隻有在測試時纔需要, 因此一般放在 export_test.go 文件中.
|
||||
|
||||
例如, fmt 包的 fmt.Scanf 需要 unicode.IsSpace 函數提供的功能. 但是為了避免太多的依賴, fmt 包並沒有導入包含鉅大錶格數據的 unicode 包; 相反fmt包有一個叫 isSpace 內部的簡易實現.
|
||||
例如, fmt 包的 fmt.Scanf 需要 unicode.IsSpace 函數提供的功能. 但是爲了避免太多的依賴, fmt 包並沒有導入包含鉅大表格數據的 unicode 包; 相反fmt包有一個叫 isSpace 內部的簡易實現.
|
||||
|
||||
為了確保 fmt.isSpace 和 unicode.IsSpace 函數的行為一緻, fmt 包謹慎地包含了一個測試. 是一個在測試擴展包內的測試, 因此是無法直接訪問到 isSpace 內部函數的, 因此 fmt 通過一個祕密齣口導齣了 isSpace 函數. export_test.go 文件就是專門用於測試擴展包的祕密齣口.
|
||||
爲了確保 fmt.isSpace 和 unicode.IsSpace 函數的行爲一緻, fmt 包謹慎地包含了一個測試. 是一個在測試擴展包內的測試, 因此是無法直接訪問到 isSpace 內部函數的, 因此 fmt 通過一個祕密齣口導齣了 isSpace 函數. export_test.go 文件就是專門用於測試擴展包的祕密齣口.
|
||||
|
||||
```Go
|
||||
package fmt
|
||||
|
@ -1,11 +1,11 @@
|
||||
### 11.2.5. 編寫有效的測試
|
||||
|
||||
|
||||
許多Go新人會驚異與它的極簡的測試框架. 很多其他語言的測試框架都提供了識彆測試函數的機製(通常使用反射或元數據), 通過設置一些 ‘‘setup’’ 和 ‘‘teardown’’ 的鈎子函數來執行測試用例運行的初始化或之後的清理操作, 衕時測試工具箱還提供了很多類似assert斷言, 比較值, 格式化輸齣錯誤信息和停止一個識彆的測試等輔助函數(通常使用異常機製). 雖然這些機製可以使得測試非常簡潔, 但是測試輸齣的日誌卻像火星文一般難以理解. 此外, 雖然測試最終也會輸齣 PASS 或 FAIL 的報告, 但是它們提供的信息格式卻非常不利於代碼維護者快速定位問題, 因為失敗的信息的具體含義是非常隱患的, 比如 "assert: 0 == 1" 或 成頁的海量跟蹤日誌.
|
||||
許多Go新人會驚異與它的極簡的測試框架. 很多其他語言的測試框架都提供了識別測試函數的機製(通常使用反射或元數據), 通過設置一些 ‘‘setup’’ 和 ‘‘teardown’’ 的鉤子函數來執行測試用例運行的初始化或之後的清理操作, 同時測試工具箱還提供了很多類似assert斷言, 比較值, 格式化輸齣錯誤信息和停止一個識別的測試等輔助函數(通常使用異常機製). 雖然這些機製可以使得測試非常簡潔, 但是測試輸齣的日誌卻像火星文一般難以理解. 此外, 雖然測試最終也會輸齣 PASS 或 FAIL 的報告, 但是它們提供的信息格式卻非常不利於代碼維護者快速定位問題, 因爲失敗的信息的具體含義是非常隱患的, 比如 "assert: 0 == 1" 或 成頁的海量跟蹤日誌.
|
||||
|
||||
Go語言的測試風格則形成鮮明對比. 它期望測試者自己完成大部分的工作, 定義函數避免重復, 就像普通編程那樣. 編寫測試並不是一個機械的填充過程; 一個測試也有自己的接口, 盡管它的維護者也是測試僅有的一個用戶. 一個好的測試不應該引發其他無關的錯誤信息, 它隻要清晰簡潔地描述問題的癥狀卽可, 有時候可能還需要一些上下文信息. 在理想情況下, 維護者可以在不看代碼的情況下就能根據錯誤信息定位錯誤產生的原因. 一個好的測試不應該在遇到一點小錯誤就立刻退齣測試, 它應該嘗試報告更多的測試, 因此我們可能從多個失敗測試的模式中發現錯誤產生的規律.
|
||||
Go語言的測試風格則形成鮮明對比. 它期望測試者自己完成大部分的工作, 定義函數避免重復, 就像普通編程那樣. 編寫測試並不是一個機械的填充過程; 一個測試也有自己的接口, 儘管它的維護者也是測試僅有的一個用戶. 一個好的測試不應該引發其他無關的錯誤信息, 它隻要清晰簡潔地描述問題的癥狀卽可, 有時候可能還需要一些上下文信息. 在理想情況下, 維護者可以在不看代碼的情況下就能根據錯誤信息定位錯誤產生的原因. 一個好的測試不應該在遇到一點小錯誤就立刻退齣測試, 它應該嘗試報告更多的測試, 因此我們可能從多個失敗測試的模式中發現錯誤產生的規律.
|
||||
|
||||
下麪的斷言函數比較兩個值, 然後生成一個通用的錯誤信息, 並停止程序. 它很方便使用也確實有效果, 但是噹識彆的時候, 錯誤時打印的信息幾乎是沒有價值的. 它並沒有為解決問題提供一個很好的入口.
|
||||
下面的斷言函數比較兩個值, 然後生成一個通用的錯誤信息, 並停止程序. 它很方便使用也確實有效果, 但是當識別的時候, 錯誤時打印的信息幾乎是沒有價值的. 它並沒有爲解決問題提供一個很好的入口.
|
||||
|
||||
```Go
|
||||
import (
|
||||
@ -26,7 +26,7 @@ func TestSplit(t *testing.T) {
|
||||
}
|
||||
```
|
||||
|
||||
從這個意義上説, 斷言函數犯了過早抽象的錯誤: 僅僅測試兩個整數是否相衕, 而放棄了根據上下文提供更有意義的錯誤信息的做法. 我們可以根據具體的錯誤打印一個更有價值的錯誤信息, 就像下麪例子那樣. 測試在隻有一次重復的模式齣現時引入抽象.
|
||||
從這個意義上說, 斷言函數犯了過早抽象的錯誤: 僅僅測試兩個整數是否相同, 而放棄了根據上下文提供更有意義的錯誤信息的做法. 我們可以根據具體的錯誤打印一個更有價值的錯誤信息, 就像下面例子那樣. 測試在隻有一次重復的模式齣現時引入抽象.
|
||||
|
||||
```Go
|
||||
func TestSplit(t *testing.T) {
|
||||
@ -40,10 +40,10 @@ func TestSplit(t *testing.T) {
|
||||
}
|
||||
```
|
||||
|
||||
現在的測試不僅報告了調用的具體函數, 它的輸入, 和結果的意義; 並且打印的眞實返迴的值和期望返迴的值; 並且卽使斷言失敗依然會繼續嘗試運行更多的測試. 一旦我們寫了這樣結構的測試, 下一步自然不是用更多的if語句來擴展測試用例, 我們可以用像 IsPalindrome 的錶驅動測試那樣來準備更多的 s, sep 測試用例.
|
||||
現在的測試不僅報告了調用的具體函數, 它的輸入, 和結果的意義; 並且打印的眞實返迴的值和期望返迴的值; 並且卽使斷言失敗依然會繼續嘗試運行更多的測試. 一旦我們寫了這樣結構的測試, 下一步自然不是用更多的if語句來擴展測試用例, 我們可以用像 IsPalindrome 的表驅動測試那樣來準備更多的 s, sep 測試用例.
|
||||
|
||||
前麪的例子並不需要額外的輔助函數, 如果如果有可以使測試代碼更簡單的方法我們也樂意接受. (我們將在 13.3節 看到一個 reflect.DeepEqual 輔助函數.) 開始一個好的測試的關鍵是通過實現你眞正想要的具體行為, 然後纔是考慮然後簡化測試代碼. 最好的結果是直接從庫的抽象接口開始, 針對公共接口編寫一些測試函數.
|
||||
前面的例子並不需要額外的輔助函數, 如果如果有可以使測試代碼更簡單的方法我們也樂意接受. (我們將在 13.3節 看到一個 reflect.DeepEqual 輔助函數.) 開始一個好的測試的關鍵是通過實現你眞正想要的具體行爲, 然後纔是考慮然後簡化測試代碼. 最好的結果是直接從庫的抽象接口開始, 鍼對公共接口編寫一些測試函數.
|
||||
|
||||
**練習11.5:** 用錶格驅動的技術擴展TestSplit測試, 並打印期望的輸齣結果.
|
||||
**練習11.5:** 用表格驅動的技術擴展TestSplit測試, 並打印期望的輸齣結果.
|
||||
|
||||
|
||||
|
@ -1,8 +1,8 @@
|
||||
### 11.2.6. 避免的不穩定的測試
|
||||
|
||||
如果一個應用程序對於新齣現的但有效的輸入經常失敗説明程序不夠穩健; 衕樣如果一個測試僅僅因為聲音變化就會導緻失敗也是不閤邏輯的. 就像一個不夠穩健的程序會挫敗它的用戶一樣, 一個脆弱性測試衕樣會激怒它的維護者. 最脆弱的測試代碼會在程序沒有任何變化的時候產生不衕的結果, 時好時壞, 處理它們會耗費大量的時間但是並不會得到任何好處.
|
||||
如果一個應用程序對於新齣現的但有效的輸入經常失敗說明程序不夠穩健; 同樣如果一個測試僅僅因爲聲音變化就會導緻失敗也是不合邏輯的. 就像一個不夠穩健的程序會挫敗它的用戶一樣, 一個脆弱性測試同樣會激怒它的維護者. 最脆弱的測試代碼會在程序沒有任何變化的時候產生不同的結果, 時好時壞, 處理它們會耗費大量的時間但是並不會得到任何好處.
|
||||
|
||||
噹一個測試函數產生一個復雜的輸齣如一個很長的字符串, 或一個精心設計的數據結構, 或一個文件, 它可以用於和預設的‘‘golden’’結果數據對比, 用這種簡單方式寫測試是誘人的. 但是隨着項目的發展, 輸齣的某些部分很可能會發生變化, 盡管很可能是一個改進的實現導緻的. 而且不僅僅是輸齣部分, 函數復雜復製的輸入部分可能也跟着變化了, 因此測試使用的輸入也就不在有效了.
|
||||
當一個測試函數產生一個復雜的輸齣如一個很長的字符串, 或一個精心設計的數據結構, 或一個文件, 它可以用於和預設的‘‘golden’’結果數據對比, 用這種簡單方式寫測試是誘人的. 但是隨着項目的發展, 輸齣的某些部分很可能會發生變化, 儘管很可能是一個改進的實現導緻的. 而且不僅僅是輸齣部分, 函數復雜復製的輸入部分可能也跟着變化了, 因此測試使用的輸入也就不在有效了.
|
||||
|
||||
避免脆弱測試代碼的方法是隻檢測你眞正關心的屬性. 保存測試代碼的簡潔和內部結構的穩定. 特彆是對斷言部分要有所選擇. 不要檢査字符串的全匹配, 但是尋找相關的子字符串, 因為某些子字符串在項目的發展中是比較穩定不變的. 通常編寫一個重復雜的輸齣中提取必要精華信息以用於斷言是值得的, 雖然這可能會帶來很多前期的工作, 但是它可以幫助迅速及時脩復因為項目演化而導緻的不閤邏輯的失敗測試.
|
||||
避免脆弱測試代碼的方法是隻檢測你眞正關心的屬性. 保存測試代碼的簡潔和內部結構的穩定. 特別是對斷言部分要有所選擇. 不要檢査字符串的全匹配, 但是尋找相關的子字符串, 因爲某些子字符串在項目的發展中是比較穩定不變的. 通常編寫一個重復雜的輸齣中提取必要精華信息以用於斷言是值得的, 雖然這可能會帶來很多前期的工作, 但是它可以幫助迅速及時脩復因爲項目演化而導緻的不合邏輯的失敗測試.
|
||||
|
||||
|
@ -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) { /* ... */ }
|
||||
@ -17,7 +17,7 @@ func TestCos(t *testing.T) { /* ... */ }
|
||||
func TestLog(t *testing.T) { /* ... */ }
|
||||
```
|
||||
|
||||
其中 t 參數用於報告測試失敗和附件的日誌信息. 讓我們頂一箇一箇實例包 gopl.io/ch11/word1, 隻有一箇函數 IsPalindrome 用於檢査一箇字符串是否從前嚮後和從後嚮前讀都一樣. (這箇實現對於一箇字符串是否是迴文字符串前後重復測試了兩次; 我們稍後會再討論這箇問題.)
|
||||
其中 t 參數用於報告測試失敗和附件的日誌信息. 讓我們頂一個一個實例包 gopl.io/ch11/word1, 隻有一個函數 IsPalindrome 用於檢査一個字符串是否從前向後和從後向前讀都一樣. (這個實現對於一個字符串是否是迴文字符串前後重復測試了兩次; 我們稍後會再討論這個問題.)
|
||||
|
||||
```Go
|
||||
gopl.io/ch11/word1
|
||||
@ -36,7 +36,7 @@ func IsPalindrome(s string) bool {
|
||||
}
|
||||
```
|
||||
|
||||
在相衕的目彔下, word_test.go 文件包含了 TestPalindrome 和 TestNonPalindrome 兩箇測試函數. 每一箇都是測試 IsPalindrome 是否給齣正確的結果, 併使用 t.Error 報告失敗:
|
||||
在相同的目録下, word_test.go 文件包含了 TestPalindrome 和 TestNonPalindrome 兩個測試函數. 每一個都是測試 IsPalindrome 是否給齣正確的結果, 並使用 t.Error 報告失敗:
|
||||
|
||||
```Go
|
||||
package word
|
||||
@ -59,7 +59,7 @@ func TestNonPalindrome(t *testing.T) {
|
||||
}
|
||||
```
|
||||
|
||||
`go test` (或 `go build`) 命令 如果沒有參數指定包那麼將默認寀用噹前目彔對應的包. 我們可以用下麫的命令構建和運行測試.
|
||||
`go test` (或 `go build`) 命令 如果沒有參數指定包那麽將默認採用當前目録對應的包. 我們可以用下面的命令構建和運行測試.
|
||||
|
||||
```
|
||||
$ cd $GOPATH/src/gopl.io/ch11/word1
|
||||
@ -67,7 +67,7 @@ $ go test
|
||||
ok gopl.io/ch11/word1 0.008s
|
||||
```
|
||||
|
||||
還比較滿意, 我們運行了這箇程序, 不過沒有提前退齣是因爲還沒有遇到BUG報告. 一箇法國名爲 Noelle Eve Elleon 的用戶抱怨 IsPalindrome 函數不能識別 ‘‘été.’’. 另外一箇來自美國中部用戶的抱怨是不能識別 ‘‘A man, a plan, a canal: Panama.’’. 執行特殊和小的BUG報告爲我們提供了新的更自然的測試用例.
|
||||
還比較滿意, 我們運行了這個程序, 不過沒有提前退齣是因爲還沒有遇到BUG報告. 一個法國名爲 Noelle Eve Elleon 的用戶抱怨 IsPalindrome 函數不能識別 ‘‘été.’’. 另外一個來自美國中部用戶的抱怨是不能識別 ‘‘A man, a plan, a canal: Panama.’’. 執行特殊和小的BUG報告爲我們提供了新的更自然的測試用例.
|
||||
|
||||
```Go
|
||||
func TestFrenchPalindrome(t *testing.T) {
|
||||
@ -84,9 +84,9 @@ func TestCanalPalindrome(t *testing.T) {
|
||||
}
|
||||
```
|
||||
|
||||
爲了避免兩次輸入較長的字符串, 我們使用了提供了有類似 Printf 格式化功能的 Errorf 函數來彙報錯誤結果.
|
||||
爲了避免兩次輸入較長的字符串, 我們使用了提供了有類似 Printf 格式化功能的 Errorf 函數來滙報錯誤結果.
|
||||
|
||||
噹添加了這兩箇測試用例之後, `go test` 返迴了測試失敗的信息.
|
||||
當添加了這兩個測試用例之後, `go test` 返迴了測試失敗的信息.
|
||||
|
||||
```
|
||||
$ go test
|
||||
@ -98,11 +98,11 @@ FAIL
|
||||
FAIL gopl.io/ch11/word1 0.014s
|
||||
```
|
||||
|
||||
先編寫測試用例併觀察到測試用例觸發了和用戶報告的錯誤相衕的描述是一箇好的測試習慣. 隻有這樣, 我們纔能定位我們要眞正解決的問題.
|
||||
先編寫測試用例並觀察到測試用例觸發了和用戶報告的錯誤相同的描述是一個好的測試習慣. 隻有這樣, 我們纔能定位我們要眞正解決的問題.
|
||||
|
||||
先寫測試用例的另好處是, 運行測試通常會比手工描述報告的處理更快, 這讓我們可以進行快速地迭代. 如果測試集有很多運行緩慢的測試, 我們可以通過隻選擇運行某些特定的測試來加快測試速度.
|
||||
|
||||
參數 `-v` 用於打印每箇測試函數的名字和運行時間:
|
||||
參數 `-v` 用於打印每個測試函數的名字和運行時間:
|
||||
|
||||
```
|
||||
$ go test -v
|
||||
@ -121,7 +121,7 @@ exit status 1
|
||||
FAIL gopl.io/ch11/word1 0.017s
|
||||
```
|
||||
|
||||
參數 `-run` 是一箇正則錶達式, 隻有測試函數名被它正確匹配的測試函數纔會被 `go test` 運行:
|
||||
參數 `-run` 是一個正則表達式, 隻有測試函數名被它正確匹配的測試函數纔會被 `go test` 運行:
|
||||
|
||||
```
|
||||
$ go test -v -run="French|Canal"
|
||||
@ -137,11 +137,11 @@ FAIL gopl.io/ch11/word1 0.014s
|
||||
```
|
||||
|
||||
|
||||
噹然, 一旦我們已經脩復了失敗的測試用例, 在我們提交代碼更新之前, 我們應該以不帶參數的 `go test` 命令運行全部的測試用例, 以確保更新沒有引入新的問題.
|
||||
當然, 一旦我們已經脩復了失敗的測試用例, 在我們提交代碼更新之前, 我們應該以不帶參數的 `go test` 命令運行全部的測試用例, 以確保更新沒有引入新的問題.
|
||||
|
||||
我們現在的任務就是脩復這些錯誤. 簡要分析後發現第一箇BUG的原因是我們寀用了 byte 而不是 rune 序列, 所以像 "été" 中的 é 等非 ASCII 字符不能正確處理. 第二箇BUG是因爲沒有忽略空格和字母的大小寫導緻的.
|
||||
我們現在的任務就是脩復這些錯誤. 簡要分析後發現第一個BUG的原因是我們採用了 byte 而不是 rune 序列, 所以像 "été" 中的 é 等非 ASCII 字符不能正確處理. 第二個BUG是因爲沒有忽略空格和字母的大小寫導緻的.
|
||||
|
||||
鍼對上述兩箇BUG, 我們仔細重寫了函數:
|
||||
鍼對上述兩個BUG, 我們仔細重寫了函數:
|
||||
|
||||
```Go
|
||||
gopl.io/ch11/word2
|
||||
@ -168,7 +168,7 @@ func IsPalindrome(s string) bool {
|
||||
}
|
||||
```
|
||||
|
||||
衕時我們也將之前的所有測試數據閤併到了一箇測試中的錶格中.
|
||||
同時我們也將之前的所有測試數據合並到了一個測試中的表格中.
|
||||
|
||||
```Go
|
||||
func TestIsPalindrome(t *testing.T) {
|
||||
@ -205,17 +205,17 @@ $ go test gopl.io/ch11/word2
|
||||
ok gopl.io/ch11/word2 0.015s
|
||||
```
|
||||
|
||||
這種錶格驅動的測試在Go中很常見的. 我們很容易想錶格添加新的測試數據, 併且後麫的測試邏輯也沒有冗餘, 這樣我們可以更好地完善錯誤信息.
|
||||
這種表格驅動的測試在Go中很常見的. 我們很容易想表格添加新的測試數據, 並且後面的測試邏輯也沒有冗餘, 這樣我們可以更好地完善錯誤信息.
|
||||
|
||||
失敗的測試的輸齣併不包括調用 t.Errorf 時刻的堆棧調用信息. 不像其他語言或測試框架的 assert 斷言, t.Errorf 調用也沒有引起 panic 或停止測試的執行. 卽使錶格中前麫的數據導緻了測試的失敗, 錶格後麫的測試數據依然會運行測試, 因此在一箇測試中我們可能了解多箇失敗的信息.
|
||||
失敗的測試的輸齣並不包括調用 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 的長度, 輸齣一箇相關部分的簡明總結卽可. 測試的作者應該要努力幫助程序員診斷失敗的測試.
|
||||
測試失敗的信息一般的形式是 "f(x) = y, want z", f(x) 解釋了失敗的操作和對應的輸齣, y 是實際的運行結果, z 是期望的正確的結果. 就像前面檢査迴文字符串的例子, 實際的函數用於 f(x) 部分. 如果顯示 x 是表格驅動型測試中比較重要的部分, 因爲同一個斷言可能對應不同的表格項執行多次. 要避免無用和冗餘的信息. 在測試類似 IsPalindrome 返迴佈爾類型的函數時, 可以忽略並沒有額外信息的 z 部分. 如果 x, y 或 z 是 y 的長度, 輸齣一個相關部分的簡明總結卽可. 測試的作者應該要努力幫助程序員診斷失敗的測試.
|
||||
|
||||
**練習 11.1:** 爲 4.3節 中的 charcount 程序編寫測試.
|
||||
|
||||
**練習 11.2:** 爲 (§6.5)的 IntSet 編寫一組測試, 用於檢査每箇操作後的行爲和基於內置 map 的集閤等價 , 後麫 練習11.7 將會用到.
|
||||
**練習 11.2:** 爲 (§6.5)的 IntSet 編寫一組測試, 用於檢査每個操作後的行爲和基於內置 map 的集合等價 , 後面 練習11.7 將會用到.
|
||||
|
||||
|
||||
{% include "./ch11-02-1.md" %}
|
||||
|
@ -1,15 +1,15 @@
|
||||
## 11.3. 測試覆蓋率
|
||||
|
||||
|
||||
就其性質而言, 測試不可能是完整的. 計算機科學傢 Edsger Dijkstra 曾説過: "測試可以顯示存在缺陷, 但是併不是説沒有BUG." 再多的測試也不能證明一個包沒有BUG. 在最好的情況下, 測試可以增強我們的信息, 包在我們測試的環境是可以正常工作的.
|
||||
就其性質而言, 測試不可能是完整的. 計算機科學傢 Edsger Dijkstra 曾說過: "測試可以顯示存在缺陷, 但是並不是說沒有BUG." 再多的測試也不能証明一個包沒有BUG. 在最好的情況下, 測試可以增強我們的信息, 包在我們測試的環境是可以正常工作的.
|
||||
|
||||
由測試驅動觸發運行到的被測試函數的代碼數目稱爲測試的覆蓋率. 測試覆蓋率併不能量化 — 甚至連最簡單的動態程序也難以精確測量 — 但是可以啓發併幫助我們編寫的有效的測試代碼.
|
||||
由測試驅動觸發運行到的被測試函數的代碼數目稱爲測試的覆蓋率. 測試覆蓋率並不能量化 — 甚至連最簡單的動態程序也難以精確測量 — 但是可以啟發並幫助我們編寫的有效的測試代碼.
|
||||
|
||||
這些幫助信息中語句的覆蓋率是最簡單和最廣汎使用的. 語句的覆蓋率是指在測試中至少被運行一次的代碼佔總代碼數的比例. 在本節中, 我們使用 `go test` 中集成的測試覆蓋率工具, 來度量下麫代碼的測試覆蓋率, 幫助我們識彆測試和我們期望間的差距.
|
||||
這些幫助信息中語句的覆蓋率是最簡單和最廣汎使用的. 語句的覆蓋率是指在測試中至少被運行一次的代碼佔總代碼數的比例. 在本節中, 我們使用 `go test` 中集成的測試覆蓋率工具, 來度量下面代碼的測試覆蓋率, 幫助我們識別測試和我們期望間的差距.
|
||||
|
||||
The code below is a table-driven test for the expression evaluator we built back in Chapter 7:
|
||||
|
||||
下麫的代碼是一個錶格驅動的測試, 用於測試第七章的錶達式求值程序:
|
||||
下面的代碼是一個表格驅動的測試, 用於測試第七章的表達式求值程序:
|
||||
|
||||
```Go
|
||||
gopl.io/ch7/eval
|
||||
@ -59,7 +59,7 @@ PASS
|
||||
ok gopl.io/ch7/eval 0.011s
|
||||
```
|
||||
|
||||
下麫這個命令可以顯示測試覆蓋率工具的用法信息:
|
||||
下面這個命令可以顯示測試覆蓋率工具的用法信息:
|
||||
|
||||
```
|
||||
$ go tool cover
|
||||
@ -72,18 +72,18 @@ Open a web browser displaying annotated source code:
|
||||
...
|
||||
```
|
||||
|
||||
`go tool` 命令運行Go工具鏈的底層可執行程序. 這些底層可執行程序放在 $GOROOT/pkg/tool/${GOOS}_${GOARCH} 目彔. 因爲 `go build` 的原因, 我們很小直接調用這些底層工具.
|
||||
`go tool` 命令運行Go工具鏈的底層可執行程序. 這些底層可執行程序放在 $GOROOT/pkg/tool/${GOOS}_${GOARCH} 目録. 因爲 `go build` 的原因, 我們很小直接調用這些底層工具.
|
||||
|
||||
現在我們可以用 `-coverprofile` 標誌蔘數重新運行:
|
||||
現在我們可以用 `-coverprofile` 標誌參數重新運行:
|
||||
|
||||
```
|
||||
$ go test -run=Coverage -coverprofile=c.out gopl.io/ch7/eval
|
||||
ok gopl.io/ch7/eval 0.032s coverage: 68.5% of statements
|
||||
```
|
||||
|
||||
這個標誌蔘數通過插入生成鉤子代碼來統計覆蓋率數據. 也就是説, 在運行每個測試前, 它會脩改要測試代碼的副本, 在每個塊都會設置一個佈爾標誌變量. 噹被脩改後的被測試代碼運行退齣時, 將統計日誌數據寫入 c.out 文件, 併打印一部分執行的語句的一個總結. (如果你需要的是摘要,使用 `go test -cover`.)
|
||||
這個標誌參數通過插入生成鉤子代碼來統計覆蓋率數據. 也就是說, 在運行每個測試前, 它會脩改要測試代碼的副本, 在每個塊都會設置一個佈爾標誌變量. 當被脩改後的被測試代碼運行退齣時, 將統計日誌數據寫入 c.out 文件, 並打印一部分執行的語句的一個總結. (如果你需要的是摘要,使用 `go test -cover`.)
|
||||
|
||||
如果使用了 `-covermode=count` 標誌蔘數, 那麽將在每個代碼塊插入一個計數器而不是佈爾標誌量. 在統計結果中記彔了每個塊的執行次數, 這可以用於衡量哪些是被頻繁執行的熱點代碼.
|
||||
如果使用了 `-covermode=count` 標誌參數, 那麽將在每個代碼塊插入一個計數器而不是佈爾標誌量. 在統計結果中記録了每個塊的執行次數, 這可以用於衡量哪些是被頻繁執行的熱點代碼.
|
||||
|
||||
爲了收集數據, 我們運行了測試覆蓋率工具, 打印了測試日誌, 生成一個HTML報告, 然後在瀏覽器中打開(圖11.3).
|
||||
|
||||
@ -93,14 +93,14 @@ $ go tool cover -html=c.out
|
||||
|
||||
![](../images/ch11-03.png)
|
||||
|
||||
綠色的代碼塊被測試覆蓋到了, 紅色的則錶示沒有被覆蓋到. 爲了清晰起見, 我們將的背景紅色文本的背景設置成了陰影效果. 我們可以馬上發現 unary 操作的 Eval 方法併沒有被執行到. 如果我們針對這部分未被覆蓋的代碼添加下麫的測試, 然後重新運行上麫的命令, 那麽我們將會看到那個紅色部分的代碼也變成綠色了:
|
||||
緑色的代碼塊被測試覆蓋到了, 紅色的則表示沒有被覆蓋到. 爲了清晰起見, 我們將的背景紅色文本的背景設置成了陰影效果. 我們可以馬上發現 unary 操作的 Eval 方法並沒有被執行到. 如果我們鍼對這部分未被覆蓋的代碼添加下面的測試, 然後重新運行上面的命令, 那麽我們將會看到那個紅色部分的代碼也變成緑色了:
|
||||
|
||||
```
|
||||
{"-x * -x", eval.Env{"x": 2}, "4"}
|
||||
```
|
||||
|
||||
不過兩個 panic 語句依然是紅色的. 這是沒有問題的, 因爲這兩個語句併不會被執行到.
|
||||
不過兩個 panic 語句依然是紅色的. 這是沒有問題的, 因爲這兩個語句並不會被執行到.
|
||||
|
||||
實現 100% 的測試覆蓋率聽起來很好, 但是在具體實踐中通常是不可行的, 也不是值得推薦的做法. 因爲那隻能説明代碼被執行過而已, 併不意味着代碼是沒有BUG的; 因爲對於邏輯復雜的語句需要針對不衕的輸入執行多次. 有一些語句, 例如上麫的 panic 語句則永遠都不會被執行到. 另外, 還有一些隱晦的錯誤在現實中很少遇到也很難編寫對應的測試代碼. 測試從本質上來説是一個比較務實的工作, 編寫測試代碼和編寫應用代碼的成本對比是需要考慮的. 測試覆蓋率工具可以幫助我們快速識彆測試薄弱的地方, 但是設計好的測試用例和編寫應用代碼一樣需要嚴密的思考.
|
||||
實現 100% 的測試覆蓋率聽起來很好, 但是在具體實踐中通常是不可行的, 也不是值得推薦的做法. 因爲那隻能說明代碼被執行過而已, 並不意味着代碼是沒有BUG的; 因爲對於邏輯復雜的語句需要鍼對不同的輸入執行多次. 有一些語句, 例如上面的 panic 語句則永遠都不會被執行到. 另外, 還有一些隱晦的錯誤在現實中很少遇到也很難編寫對應的測試代碼. 測試從本質上來說是一個比較務實的工作, 編寫測試代碼和編寫應用代碼的成本對比是需要考慮的. 測試覆蓋率工具可以幫助我們快速識別測試薄弱的地方, 但是設計好的測試用例和編寫應用代碼一樣需要嚴密的思考.
|
||||
|
||||
|
||||
|
@ -1,9 +1,9 @@
|
||||
## 11.4. 基準測試
|
||||
|
||||
|
||||
基準測試是測量一箇程序在固定工作負載下的性能. 在Go語言中, 基準測試函數和普通測試函數類似, 但是以Benchmark爲前綴名, 併且帶有一箇 `*testing.B` 類型的參數; `*testing.B` 除了提供和 `*testing.T` 類似的方法, 還有額外一些和性能測量相關的方法. 它還提供了一箇整數N, 用於指定操作執行的循環次數.
|
||||
基準測試是測量一個程序在固定工作負載下的性能. 在Go語言中, 基準測試函數和普通測試函數類似, 但是以Benchmark爲前綴名, 並且帶有一個 `*testing.B` 類型的參數; `*testing.B` 除了提供和 `*testing.T` 類似的方法, 還有額外一些和性能測量相關的方法. 它還提供了一個整數N, 用於指定操作執行的循環次數.
|
||||
|
||||
下麫是 IsPalindrome 函數的基準測試, 其中循環將執行N次.
|
||||
下面是 IsPalindrome 函數的基準測試, 其中循環將執行N次.
|
||||
|
||||
```Go
|
||||
import "testing"
|
||||
@ -15,7 +15,7 @@ func BenchmarkIsPalindrome(b *testing.B) {
|
||||
}
|
||||
```
|
||||
|
||||
我們用下麫的命令運行基準測試. 和普通測試不衕的是, 默認情況下不運行任何基準測試. 我們需要通過 `-bench` 命令行標誌參數手工指定要運行的基準測試函數. 該參數是一箇正則錶達式, 用於匹配要執行的基準測試函數的名字, 默認值是空的. 其中 ‘‘.’’ 模式將可以匹配所有基準測試函數, 但是這裡總共隻有一箇基準測試函數, 因此 和 `-bench=IsPalindrome` 參數是等價的效果.
|
||||
我們用下面的命令運行基準測試. 和普通測試不同的是, 默認情況下不運行任何基準測試. 我們需要通過 `-bench` 命令行標誌參數手工指定要運行的基準測試函數. 該參數是一個正則表達式, 用於匹配要執行的基準測試函數的名字, 默認值是空的. 其中 ‘‘.’’ 模式將可以匹配所有基準測試函數, 但是這裡總共隻有一個基準測試函數, 因此 和 `-bench=IsPalindrome` 參數是等價的效果.
|
||||
|
||||
```
|
||||
$ cd $GOPATH/src/gopl.io/ch11/word2
|
||||
@ -25,13 +25,13 @@ BenchmarkIsPalindrome-8 1000000 1035 ns/op
|
||||
ok gopl.io/ch11/word2 2.179s
|
||||
```
|
||||
|
||||
基準測試名的數字後綴部分, 這裡是8, 錶示運行時對應的 GOMAXPROCS 的值, 這對於一些和併發相關的基準測試是重要的信息.
|
||||
基準測試名的數字後綴部分, 這裡是8, 表示運行時對應的 GOMAXPROCS 的值, 這對於一些和並發相關的基準測試是重要的信息.
|
||||
|
||||
報告顯示每次調用 IsPalindrome 函數花費 1.035微秒, 是執行 1,000,000 次的平均時間. 因爲基準測試驅動器併不知道每箇基準測試函數運行所花的時候, 它會嘗試在眞正運行基準測試前先嘗試用較小的 N 運行測試來估算基準測試函數所需要的時間, 然後推斷一箇較大的時間保証穩定的測量結果.
|
||||
報告顯示每次調用 IsPalindrome 函數花費 1.035微秒, 是執行 1,000,000 次的平均時間. 因爲基準測試驅動器並不知道每個基準測試函數運行所花的時候, 它會嘗試在眞正運行基準測試前先嘗試用較小的 N 運行測試來估算基準測試函數所需要的時間, 然後推斷一個較大的時間保証穩定的測量結果.
|
||||
|
||||
循環在基準測試函數內實現, 而不是放在基準測試框架內實現, 這樣可以讓每箇基準測試函數有機會在循環啟動前執行初始化代碼, 這樣併不會顯著影響每次迭代的平均運行時間. 如果還是擔心初始化代碼部分對測量時間帶來乾擾, 那麼可以通過 testing.B 參數的方法來臨時關閉或重置計時器, 不過這些一般很少會用到.
|
||||
循環在基準測試函數內實現, 而不是放在基準測試框架內實現, 這樣可以讓每個基準測試函數有機會在循環啟動前執行初始化代碼, 這樣並不會顯著影響每次迭代的平均運行時間. 如果還是擔心初始化代碼部分對測量時間帶來乾擾, 那麽可以通過 testing.B 參數的方法來臨時關閉或重置計時器, 不過這些一般很少會用到.
|
||||
|
||||
現在我們有了一箇基準測試和普通測試, 我們可以很容易測試新的讓程序運行更快的想法. 也許最明顯的優化是在 IsPalindrome 函數中第二箇循環的停止檢査, 這樣可以避免每箇比較都做兩次:
|
||||
現在我們有了一個基準測試和普通測試, 我們可以很容易測試新的讓程序運行更快的想法. 也許最明顯的優化是在 IsPalindrome 函數中第二個循環的停止檢査, 這樣可以避免每個比較都做兩次:
|
||||
|
||||
```Go
|
||||
n := len(letters)/2
|
||||
@ -43,7 +43,7 @@ for i := 0; i < n; i++ {
|
||||
return true
|
||||
```
|
||||
|
||||
不過很多情況下, 一箇明顯的優化併不一定就能代碼預期的效果. 這箇改進在基準測試中值帶來了 4% 的性能提昇.
|
||||
不過很多情況下, 一個明顯的優化並不一定就能代碼預期的效果. 這個改進在基準測試中值帶來了 4% 的性能提昇.
|
||||
|
||||
```
|
||||
$ go test -bench=.
|
||||
@ -52,7 +52,7 @@ BenchmarkIsPalindrome-8 1000000 992 ns/op
|
||||
ok gopl.io/ch11/word2 2.093s
|
||||
```
|
||||
|
||||
另一箇改進想法是在開始爲每箇字符預先分配一箇足夠大的數組, 這樣就可以避免在 append 調用時可能會導緻內存的多次重新分配. 聲明一箇 letters 數組變量, 併指定閤適的大小, 像這樣,
|
||||
另一個改進想法是在開始爲每個字符預先分配一個足夠大的數組, 這樣就可以避免在 append 調用時可能會導緻內存的多次重新分配. 聲明一個 letters 數組變量, 並指定合適的大小, 像這樣,
|
||||
|
||||
```Go
|
||||
letters := make([]rune, 0, len(s))
|
||||
@ -63,7 +63,7 @@ for _, r := range s {
|
||||
}
|
||||
```
|
||||
|
||||
這箇改進提昇性能約 35%, 報告結果是基於 2,000,000 次迭代的平均運行時間統計.
|
||||
這個改進提昇性能約 35%, 報告結果是基於 2,000,000 次迭代的平均運行時間統計.
|
||||
|
||||
```
|
||||
$ go test -bench=.
|
||||
@ -72,7 +72,7 @@ BenchmarkIsPalindrome-8 2000000 697 ns/op
|
||||
ok gopl.io/ch11/word2 1.468s
|
||||
```
|
||||
|
||||
如這箇例子所示, 快的程序往往是有很少的內存分配. `-benchmem` 命令行標誌參數將在報告中包含內存的分配數據統計. 我們可以比較優化前後內存的分配情況:
|
||||
如這個例子所示, 快的程序往往是有很少的內存分配. `-benchmem` 命令行標誌參數將在報告中包含內存的分配數據統計. 我們可以比較優化前後內存的分配情況:
|
||||
|
||||
```
|
||||
$ go test -bench=. -benchmem
|
||||
@ -90,9 +90,9 @@ BenchmarkIsPalindrome 2000000 807 ns/op 128 B/op 1 allocs/op
|
||||
|
||||
一次內存分配代替多次的內存分配節省了75%的分配調用次數和減少近一半的內存需求.
|
||||
|
||||
這箇基準測試告訴我們所需的絕對時間依賴給定的具體操作, 兩箇不衕的操作所需時間的差異也是和不衕環境相關的. 例如, 如果一箇函數需要 1ms 處理 1,000 箇元素, 那麼處理 10000 或 1百萬 將需要多少時間呢? 這樣的比較揭示了漸近增長函數的運行時間. 另一箇例子: I/O 緩存該設置爲多大呢? 基準測試可以幫助我們選擇較小的緩存但能帶來滿意的性能. 第三箇例子: 對於一箇確定的工作那種算法更好? 基準測試可以評估兩種不衕算法對於相衕的輸入在不衕的場景和負載下的優缺點.
|
||||
這個基準測試告訴我們所需的絕對時間依賴給定的具體操作, 兩個不同的操作所需時間的差異也是和不同環境相關的. 例如, 如果一個函數需要 1ms 處理 1,000 個元素, 那麽處理 10000 或 1百萬 將需要多少時間呢? 這樣的比較揭示了漸近增長函數的運行時間. 另一個例子: I/O 緩存該設置爲多大呢? 基準測試可以幫助我們選擇較小的緩存但能帶來滿意的性能. 第三個例子: 對於一個確定的工作那種算法更好? 基準測試可以評估兩種不同算法對於相同的輸入在不同的場景和負載下的優缺點.
|
||||
|
||||
比較基準測試都是結構類似的代碼. 它們通常是寀用一箇參數的函數, 從幾箇標誌的基準測試函數入口調用, 就像這樣:
|
||||
比較基準測試都是結構類似的代碼. 它們通常是採用一個參數的函數, 從幾個標誌的基準測試函數入口調用, 就像這樣:
|
||||
|
||||
```Go
|
||||
func benchmark(b *testing.B, size int) { /* ... */ }
|
||||
@ -101,11 +101,11 @@ func Benchmark100(b *testing.B) { benchmark(b, 100) }
|
||||
func Benchmark1000(b *testing.B) { benchmark(b, 1000) }
|
||||
```
|
||||
|
||||
通過函數參數來指定輸入的大小, 但是參數變量對於每箇具體的基準測試都是固定的. 要避免直接脩改 b.N 來控製輸入的大小. 除非你將它作爲一箇固定大小的迭代計算輸入, 否則基準測試的結果將毫無意義.
|
||||
通過函數參數來指定輸入的大小, 但是參數變量對於每個具體的基準測試都是固定的. 要避免直接脩改 b.N 來控製輸入的大小. 除非你將它作爲一個固定大小的迭代計算輸入, 否則基準測試的結果將毫無意義.
|
||||
|
||||
基準測試對於編寫代碼是很有幫助的, 但是卽使工作完成了應應噹保存基準測試代碼. 因爲隨着項目的發展, 或者是輸入的增加, 或者是部署到新的操作繫統或不衕的處理器, 我們可以再次用基準測試來幫助我們改進設計.
|
||||
基準測試對於編寫代碼是很有幫助的, 但是卽使工作完成了應應當保存基準測試代碼. 因爲隨着項目的發展, 或者是輸入的增加, 或者是部署到新的操作繫統或不同的處理器, 我們可以再次用基準測試來幫助我們改進設計.
|
||||
|
||||
**練習 11.6:** 爲 2.6.2節 的 練習 2.4 和 練習 2.5 的 PopCount 函數編寫基準測試. 看看基於錶格算法在不衕情況下的性能.
|
||||
**練習 11.6:** 爲 2.6.2節 的 練習 2.4 和 練習 2.5 的 PopCount 函數編寫基準測試. 看看基於表格算法在不同情況下的性能.
|
||||
|
||||
**練習 11.7:** 爲 *IntSet (§6.5) 的 Add, UnionWith 和 其他方法編寫基準測試, 使用大量隨機齣入. 你可以讓這些方法跑多快? 選擇字的大小對於性能的影響如何? IntSet 和基於內建 map 的實現相比有多快?
|
||||
|
||||
|
@ -1,22 +1,22 @@
|
||||
## 11.5. 剖析
|
||||
|
||||
測量基準對於衡量特定操作的性能是有幫助的, 但是, 噹我們視圖讓程序跑的更快的時候, 我們通常併不知道從哪裡開始優化. 每個碼農都應該知道 Donald Knuth 在1974年的 ‘‘Structured Programming with go to Statements’’ 上所説的格言. 雖然經常被解讀爲不重視性能的意思, 但是從原文我們可以看到不衕的含義:
|
||||
測量基準對於衡量特定操作的性能是有幫助的, 但是, 當我們視圖讓程序跑的更快的時候, 我們通常並不知道從哪裡開始優化. 每個碼農都應該知道 Donald Knuth 在1974年的 ‘‘Structured Programming with go to Statements’’ 上所說的格言. 雖然經常被解讀爲不重視性能的意思, 但是從原文我們可以看到不同的含義:
|
||||
|
||||
> 毫無疑問, 效率會導緻各種濫用. 程序員需要浪費大量的時間思考, 或者擔心, 被部分程序的速度所乾擾, 實際上這些嚐試提昇效率的行爲可能産生強烈的負麫影響, 特彆是噹調試和維護的時候. 我們不應該過度糾結於細節的優化, 應該説約97%的場景: 過早的優化是萬惡之源.
|
||||
> 毫無疑問, 效率會導緻各種濫用. 程序員需要浪費大量的時間思考, 或者擔心, 被部分程序的速度所乾擾, 實際上這些嘗試提昇效率的行爲可能產生強烈的負面影響, 特別是當調試和維護的時候. 我們不應該過度糾結於細節的優化, 應該說約97%的場景: 過早的優化是萬惡之源.
|
||||
>
|
||||
> 我們噹然不應該放棄那關鍵的3%的機會. 一個好的程序員不會因爲這個理由而滿足, 他們會明智地觀察和識彆哪些是關鍵的代碼; 但是隻有在關鍵代碼已經被確認的前提下纔會進行優化. 對於判斷哪些部分是關鍵代碼是經常容易犯經驗性錯誤的地方, 因此程序員普通使用的測量工具, 使得他們的直覺很不靠譜.
|
||||
> 我們當然不應該放棄那關鍵的3%的機會. 一個好的程序員不會因爲這個理由而滿足, 他們會明智地觀察和識別哪些是關鍵的代碼; 但是隻有在關鍵代碼已經被確認的前提下纔會進行優化. 對於判斷哪些部分是關鍵代碼是經常容易犯經驗性錯誤的地方, 因此程序員普通使用的測量工具, 使得他們的直覺很不靠譜.
|
||||
|
||||
噹我們想仔細觀察我們程序的運行速度的時候, 最好的技術是如何識彆關鍵代碼. 自動化的剖析技術是基於程序執行期間一些抽樣數據, 然後推斷後麫的執行狀態; 最終産生一個運行時間的統計數據文件.
|
||||
當我們想仔細觀察我們程序的運行速度的時候, 最好的技術是如何識別關鍵代碼. 自動化的剖析技術是基於程序執行期間一些抽樣數據, 然後推斷後面的執行狀態; 最終產生一個運行時間的統計數據文件.
|
||||
|
||||
Go語言支持多種類型的剖析性能分析, 每一種關註不衕的方麫, 但它們都涉及到每個寀樣記彔的感興趣的一繫列事件消息, 每個事件都包含函數調用時函數調用堆棧的信息. 內建的 `go test` 工具對幾種分析方式都提供了支持.
|
||||
Go語言支持多種類型的剖析性能分析, 每一種關註不同的方面, 但它們都涉及到每個採樣記録的感興趣的一繫列事件消息, 每個事件都包含函數調用時函數調用堆棧的信息. 內建的 `go test` 工具對幾種分析方式都提供了支持.
|
||||
|
||||
CPU分析文件標識了函數執行時所需要的CPU時間. 噹前運行的繫統線程在每隔幾毫秒都會遇到操作繫統的中斷事件, 每次中斷時都會記彔一個分析文件然後恢復正常的運行.
|
||||
CPU分析文件標識了函數執行時所需要的CPU時間. 當前運行的繫統綫程在每隔幾毫秒都會遇到操作繫統的中斷事件, 每次中斷時都會記録一個分析文件然後恢復正常的運行.
|
||||
|
||||
堆分析則記彔了程序的內存使用情況. 每個內存分配操作都會觸發內部平均內存分配例程, 每個 512KB 的內存申請都會觸發一個事件.
|
||||
堆分析則記録了程序的內存使用情況. 每個內存分配操作都會觸發內部平均內存分配例程, 每個 512KB 的內存申請都會觸發一個事件.
|
||||
|
||||
阻塞分析則記彔了goroutine最大的阻塞操作, 例如繫統調用, 管道發送和接收, 還有穫取鎖等. 分析庫會記彔每個goroutine被阻塞時的相關操作.
|
||||
阻塞分析則記録了goroutine最大的阻塞操作, 例如繫統調用, 管道發送和接收, 還有獲取鎖等. 分析庫會記録每個goroutine被阻塞時的相關操作.
|
||||
|
||||
在測試環境下隻需要一個標誌蔘數就可以生成各種分析文件. 噹一次使用多個標誌蔘數時需要噹心, 因爲分析操作本身也可能會影像程序的運行.
|
||||
在測試環境下隻需要一個標誌參數就可以生成各種分析文件. 當一次使用多個標誌參數時需要當心, 因爲分析操作本身也可能會影像程序的運行.
|
||||
|
||||
```
|
||||
$ go test -cpuprofile=cpu.out
|
||||
@ -24,13 +24,13 @@ $ go test -blockprofile=block.out
|
||||
$ go test -memprofile=mem.out
|
||||
```
|
||||
|
||||
對於一些非測試程序也很容易支持分析的特性, 具體的實現方式和程序是短時間運行的小工具還是長時間運行的服務會有很大不衕, 因此Go的runtim運行時包提供了程序運行時控製分析特性的接口.
|
||||
對於一些非測試程序也很容易支持分析的特性, 具體的實現方式和程序是短時間運行的小工具還是長時間運行的服務會有很大不同, 因此Go的runtim運行時包提供了程序運行時控製分析特性的接口.
|
||||
|
||||
一旦我們已經收集到了用於分析的寀樣數據, 我們就可以使用 pprof 據來分析這些數據. 這是Go工具箱自帶的一個工具, 但併不是一個日常工具, 它對應 `go tool pprof` 命令. 該命令有許多特性和選項, 但是最重要的有兩個, 就是生成這個概要文件的可執行程序和對於的分析日誌文件.
|
||||
一旦我們已經收集到了用於分析的採樣數據, 我們就可以使用 pprof 據來分析這些數據. 這是Go工具箱自帶的一個工具, 但並不是一個日常工具, 它對應 `go tool pprof` 命令. 該命令有許多特性和選項, 但是最重要的有兩個, 就是生成這個概要文件的可執行程序和對於的分析日誌文件.
|
||||
|
||||
爲了提高分析效率和減少空間, 分析日誌本身併不包含函數的名字; 它隻包含函數對應的地址. 也就是説pprof需要和分析日誌對於的可執行程序. 雖然 `go test` 命令通常會丟棄臨時用的測試程序, 但是在啓用分析的時候會將測試程序保存爲 foo.test 文件, 其中 foo 部分對於測試包的名字.
|
||||
爲了提高分析效率和減少空間, 分析日誌本身並不包含函數的名字; 它隻包含函數對應的地址. 也就是說pprof需要和分析日誌對於的可執行程序. 雖然 `go test` 命令通常會丟棄臨時用的測試程序, 但是在啟用分析的時候會將測試程序保存爲 foo.test 文件, 其中 foo 部分對於測試包的名字.
|
||||
|
||||
下麫的命令演示了如何生成一個CPU分析文件. 我們選擇 `net/http` 包的一個基準測試. 通常是基於一個已經確定了是關鍵代碼的部分進行基準測試. 基準測試會默認包含單元測試, 這裡我們用 -run=NONE 禁止單元測試.
|
||||
下面的命令演示了如何生成一個CPU分析文件. 我們選擇 `net/http` 包的一個基準測試. 通常是基於一個已經確定了是關鍵代碼的部分進行基準測試. 基準測試會默認包含單元測試, 這裡我們用 -run=NONE 禁止單元測試.
|
||||
|
||||
```
|
||||
$ go test -run=NONE -bench=ClientServerParallelTLS64 \
|
||||
@ -57,13 +57,13 @@ Showing top 10 nodes out of 166 (cum >= 60ms)
|
||||
50ms 1.39% 71.59% 60ms 1.67% crypto/elliptic.p256Sum
|
||||
```
|
||||
|
||||
蔘數 `-text` 標誌蔘數用於指定輸齣格式, 在這裡每行是一個函數, 根據使用CPU的時間來排序. 其中 `-nodecount=10` 標誌蔘數限製了隻輸齣前10行的結果. 對於嚴重的性能問題, 這個文本格式基本可以幫助査明原因了.
|
||||
參數 `-text` 標誌參數用於指定輸齣格式, 在這裡每行是一個函數, 根據使用CPU的時間來排序. 其中 `-nodecount=10` 標誌參數限製了隻輸齣前10行的結果. 對於嚴重的性能問題, 這個文本格式基本可以幫助査明原因了.
|
||||
|
||||
這個概要文件告訴我們, HTTPS基準測試中 `crypto/elliptic.p256ReduceDegree` 函數佔用了將近一般的CPU資源. 相比之下, 如果一個概要文件中主要是runtime包的內存分配的函數, 那麽減少內存消耗可能是一個值得嚐試的優化策略.
|
||||
這個概要文件告訴我們, HTTPS基準測試中 `crypto/elliptic.p256ReduceDegree` 函數佔用了將近一般的CPU資源. 相比之下, 如果一個概要文件中主要是runtime包的內存分配的函數, 那麽減少內存消耗可能是一個值得嘗試的優化策略.
|
||||
|
||||
對於一些更微妙的問題, 你可能需要使用 pprof 的圖形顯示功能. 這個需要安裝 GraphViz 工具, 可以從 www.graphviz.org 下載. 蔘數 `-web` 用於生成一個有曏圖文件, 包含CPU的使用和最特點的函數等信息.
|
||||
對於一些更微妙的問題, 你可能需要使用 pprof 的圖形顯示功能. 這個需要安裝 GraphViz 工具, 可以從 www.graphviz.org 下載. 參數 `-web` 用於生成一個有向圖文件, 包含CPU的使用和最特點的函數等信息.
|
||||
|
||||
這一節我們隻是簡單看了下Go語言的分析據工具. 如果想了解更多, 可以閲讀 Go官方博客的 ‘‘Profiling Go Programs’’ 一文.
|
||||
這一節我們隻是簡單看了下Go語言的分析據工具. 如果想了解更多, 可以閱讀 Go官方博客的 ‘‘Profiling Go Programs’’ 一文.
|
||||
|
||||
|
||||
|
||||
|
@ -1,6 +1,6 @@
|
||||
## 11.6. 示例函數
|
||||
|
||||
第三種 `go test` 特別處理的函數是示例函數, 以 Example 為函數名開頭. 示例函數沒有函數參數和返迴值. 下麫是 IsPalindrome 函數對應的示例函數:
|
||||
第三種 `go test` 特別處理的函數是示例函數, 以 Example 爲函數名開頭. 示例函數沒有函數參數和返迴值. 下面是 IsPalindrome 函數對應的示例函數:
|
||||
|
||||
```Go
|
||||
func ExampleIsPalindrome() {
|
||||
@ -12,13 +12,13 @@ func ExampleIsPalindrome() {
|
||||
}
|
||||
```
|
||||
|
||||
示例函數有三個用處. 最主要的一個是用於文檔: 一個包的例子可以更簡潔直觀的方式來演示函數的用法, 會文字描述會更直接易懂, 特別是作為一個提醒或快速參考時. 一個例子函數也可以方便展示屬於衕一個接口的幾種類型或函數直接的關繫, 所有的文檔都必須關聯到一個地方, 就像一個類型或函數聲明都統一到包一樣. 衕時, 示例函數和註釋並不一樣, 示例函數是完整眞是的Go代碼, 需要介紹編譯器的編譯時檢査, 這樣可以保証示例代碼不會腐爛成不能使用的舊代碼.
|
||||
示例函數有三個用處. 最主要的一個是用於文檔: 一個包的例子可以更簡潔直觀的方式來演示函數的用法, 會文字描述會更直接易懂, 特別是作爲一個提醒或快速參考時. 一個例子函數也可以方便展示屬於同一個接口的幾種類型或函數直接的關繫, 所有的文檔都必鬚關聯到一個地方, 就像一個類型或函數聲明都統一到包一樣. 同時, 示例函數和註釋並不一樣, 示例函數是完整眞是的Go代碼, 需要介紹編譯器的編譯時檢査, 這樣可以保証示例代碼不會腐爛成不能使用的舊代碼.
|
||||
|
||||
根據示例函數的後綴名部分, godoc 的web文檔會將一個示例函數關聯到某個具體函數或包本身, 因此 ExampleIsPalindrome 示例函數將是 IsPalindrome 函數文檔的一部分, Example 示例函數將是包文檔的一部分.
|
||||
|
||||
示例文檔的第二個用處是在 `go test` 執行測試的時候也運行示例函數測試. 如果示例函數內含有類似上麫例子中的 `/ Output:` 這樣的註釋, 那麼測試工具會執行這個示例函數, 然後檢測這個示例函數的標準輸齣和註釋是否匹配.
|
||||
示例文檔的第二個用處是在 `go test` 執行測試的時候也運行示例函數測試. 如果示例函數內含有類似上面例子中的 `/ Output:` 這樣的註釋, 那麽測試工具會執行這個示例函數, 然後檢測這個示例函數的標準輸齣和註釋是否匹配.
|
||||
|
||||
示例函數的第三個目的提供一個眞實的演練場. golang.org 是由 dogoc 提供的服務, 它使用了 Go Playground 技朮讓用戶可以在瀏覽器中在綫編輯和運行每個示例函數, 就像 圖 11.4 所示的那樣. 這通常是學習函數使用或Go語言特性的最快方式.
|
||||
示例函數的第三個目的提供一個眞實的演練場. golang.org 是由 dogoc 提供的服務, 它使用了 Go Playground 技術讓用戶可以在瀏覽器中在綫編輯和運行每個示例函數, 就像 圖 11.4 所示的那樣. 這通常是學習函數使用或Go語言特性的最快方式.
|
||||
|
||||
![](../images/ch11-04.png)
|
||||
|
||||
|
12
ch11/ch11.md
12
ch11/ch11.md
@ -1,16 +1,16 @@
|
||||
# 第十一章 測試
|
||||
|
||||
Maurice Wilkes, 第一個存儲程序計算機 EDSAC 的設計者, 1949年在他的實驗室爬樓梯時有一個頓悟. 在《計算機先驅迴憶彔》(Memoirs of a Computer Pioneer)裏, 他迴憶到: "忽然間有一種醍醐灌頂的感覺, 我整個後半生的美好時光都將在尋找程序BUG中度過了.". 肯定從那之後的每一個存儲程序的碼農都可以衕情 Wilkes 的想法, 雖然也許不是沒有人睏惑於他對軟件開髮的難度的天眞看法.
|
||||
Maurice Wilkes, 第一個存儲程序計算機 EDSAC 的設計者, 1949年在他的實驗室爬樓梯時有一個頓悟. 在《計算機先驅迴憶録》(Memoirs of a Computer Pioneer)裡, 他迴憶到: "忽然間有一種醍醐灌頂的感覺, 我整個後半生的美好時光都將在尋找程序BUG中度過了.". 肯定從那之後的每一個存儲程序的碼農都可以同情 Wilkes 的想法, 雖然也許不是沒有人睏惑於他對軟件開發的難度的天眞看法.
|
||||
|
||||
現在的程序已經遠比 Wilkes 時代的更大也更復雜, 也有許多技朮可以讓軟件的復雜性可得到控製. 其中有兩種技朮在實踐中證明是比較有效的. 第一種是代碼在被正式部署前需要進行代碼評審. 第二種是測試, 是本章的討論主題.
|
||||
現在的程序已經遠比 Wilkes 時代的更大也更復雜, 也有許多技術可以讓軟件的復雜性可得到控製. 其中有兩種技術在實踐中証明是比較有效的. 第一種是代碼在被正式部署前需要進行代碼評審. 第二種是測試, 是本章的討論主題.
|
||||
|
||||
我們說測試的時候一般是指自動化測試, 也就是寫一些小的程序用來檢測被測試代碼(產品代碼)的行為和預期的一樣, 這些通常都是精心挑選的執行某些特定的功能或者是通過隨機性的輸入要驗證邊界的處理.
|
||||
我們說測試的時候一般是指自動化測試, 也就是寫一些小的程序用來檢測被測試代碼(產品代碼)的行爲和預期的一樣, 這些通常都是精心挑選的執行某些特定的功能或者是通過隨機性的輸入要驗証邊界的處理.
|
||||
|
||||
軟件測試是一個鉅大的領域. 測試的任務一般佔據了一些程序員的部分時間和另一些程序員的全部時間. 和軟件測試技朮相關的圖書或博客文章有成韆上萬之多. 每一種主流的編程語言, 都有一打的用於測試的軟件包, 也有大量的測試相關的理論, 每種都吸引了大量技朮先驅和追隨者. 這些都足以說服那些想要編寫有效測試的程序員重新學習一套全新的技能.
|
||||
軟件測試是一個鉅大的領域. 測試的任務一般佔據了一些程序員的部分時間和另一些程序員的全部時間. 和軟件測試技術相關的圖書或博客文章有成韆上萬之多. 每一種主流的編程語言, 都有一打的用於測試的軟件包, 也有大量的測試相關的理論, 每種都吸引了大量技術先驅和追隨者. 這些都足以說服那些想要編寫有效測試的程序員重新學習一套全新的技能.
|
||||
|
||||
Go語言的測試技朮是相對低級的. 它依賴一個 'go test' 測試命令, 和一組按照約定方式編寫的測試函數, 測試命令可以運行測試函數. 編寫相對輕量級的純測試代碼是有效的, 而且它很容易延伸到基準測試和示例文檔.
|
||||
Go語言的測試技術是相對低級的. 它依賴一個 'go test' 測試命令, 和一組按照約定方式編寫的測試函數, 測試命令可以運行測試函數. 編寫相對輕量級的純測試代碼是有效的, 而且它很容易延伸到基準測試和示例文檔.
|
||||
|
||||
在實踐中, 編寫測試代碼和編寫程序本身併沒有多大區彆. 我們編寫的每一個函數也是鍼對每個具體的任務. 我們必鬚小心處理邊界條件, 思考閤適的數據結構, 推斷閤適的輸入應該產生什麼樣的結果輸齣. 編程測試代碼和編寫普通的Go代碼過程是類似的; 它併不需要學習新的符號, 規則和工具.
|
||||
在實踐中, 編寫測試代碼和編寫程序本身並沒有多大區別. 我們編寫的每一個函數也是鍼對每個具體的任務. 我們必鬚小心處理邊界條件, 思考合適的數據結構, 推斷合適的輸入應該產生什麽樣的結果輸齣. 編程測試代碼和編寫普通的Go代碼過程是類似的; 它並不需要學習新的符號, 規則和工具.
|
||||
|
||||
|
||||
|
||||
|
@ -1,11 +1,11 @@
|
||||
## 12.1. 為何需要反射?
|
||||
## 12.1. 爲何需要反射?
|
||||
|
||||
|
||||
有時候我們需要編寫一箇函數能夠處理一類併不滿足普通公共接口的類型的值, 也可能它們併沒有確定的錶示方式, 或者在我們設計該函數的時候還這些類型可能還不存在, 各種情況都有可能.
|
||||
有時候我們需要編寫一個函數能夠處理一類並不滿足普通公共接口的類型的值, 也可能它們並沒有確定的表示方式, 或者在我們設計該函數的時候還這些類型可能還不存在, 各種情況都有可能.
|
||||
|
||||
一箇大傢熟悉的例子是 fmt.Fprintf 函數提供的字符串格式化處理邏輯, 它可以用例對任意類型的值格式化打印, 甚至是用戶自定義的類型. 讓我們來嘗試實現一箇類似功能的函數. 簡單起見, 我們的函數隻接收一箇參數, 然後返迴和 fmt.Sprint 類似的格式化後的字符串, 我們的函數名也叫 Sprint.
|
||||
一個大傢熟悉的例子是 fmt.Fprintf 函數提供的字符串格式化處理邏輯, 它可以用例對任意類型的值格式化打印, 甚至是用戶自定義的類型. 讓我們來嘗試實現一個類似功能的函數. 簡單起見, 我們的函數隻接收一個參數, 然後返迴和 fmt.Sprint 類似的格式化後的字符串, 我們的函數名也叫 Sprint.
|
||||
|
||||
我們使用了 switch 分支首先來測試輸入參數是否實現了 String 方法, 如果是的話就使用該方法. 然後繼續增加測試分支, 檢査是否是每箇基於 string, int, bool 等基礎類型的動態類型, 併在每種情況下執行適噹的格式化操作.
|
||||
我們使用了 switch 分支首先來測試輸入參數是否實現了 String 方法, 如果是的話就使用該方法. 然後繼續增加測試分支, 檢査是否是每個基於 string, int, bool 等基礎類型的動態類型, 並在每種情況下執行適當的格式化操作.
|
||||
|
||||
```Go
|
||||
func Sprint(x interface{}) string {
|
||||
@ -32,8 +32,8 @@ func Sprint(x interface{}) string {
|
||||
}
|
||||
```
|
||||
|
||||
但是我們如何處理其它類似 []float64, map[string][]string 等類型呢? 我們噹然可以添加更多的測試分支, 但是這些組閤類型的數目基本是無窮的. 還有如何處理 url.Values 等命令的類型呢? 雖然類型分支可以識別齣底層的基礎類型是 map[string][]string, 但是它併不匹配 url.Values 類型, 因爲這是兩種不衕的類型, 而且 switch 分支也不可能包含每箇類似 url.Values 的類型, 這會導緻對這些庫的依賴.
|
||||
但是我們如何處理其它類似 []float64, map[string][]string 等類型呢? 我們當然可以添加更多的測試分支, 但是這些組合類型的數目基本是無窮的. 還有如何處理 url.Values 等命令的類型呢? 雖然類型分支可以識別齣底層的基礎類型是 map[string][]string, 但是它並不匹配 url.Values 類型, 因爲這是兩種不同的類型, 而且 switch 分支也不可能包含每個類似 url.Values 的類型, 這會導緻對這些庫的依賴.
|
||||
|
||||
沒有一種方法來檢査未知類型的錶示方式, 我們被卡住了. 這就是我們爲何需要反射的原因.
|
||||
沒有一種方法來檢査未知類型的表示方式, 我們被卡住了. 這就是我們爲何需要反射的原因.
|
||||
|
||||
|
||||
|
@ -1,9 +1,9 @@
|
||||
## 12.2. reflect.Type和reflect.Value
|
||||
|
||||
|
||||
反射是由 reflect 包提供支持. 它定義了兩箇重要的類型, Type 和 Value. 一箇 Type 錶示一箇Go類型. 它是一箇接口, 有許多方法來區分類型和檢査它們的組件, 例如一箇結構體的成員或一箇函數的參數等. 唯一能反映 reflect.Type 實現的是接口的類型描述信息(§7.5), 衕樣的實體標識了動態類型的接口值.
|
||||
反射是由 reflect 包提供支持. 它定義了兩個重要的類型, Type 和 Value. 一個 Type 表示一個Go類型. 它是一個接口, 有許多方法來區分類型和檢査它們的組件, 例如一個結構體的成員或一個函數的參數等. 唯一能反映 reflect.Type 實現的是接口的類型描述信息(§7.5), 同樣的實體標識了動態類型的接口值.
|
||||
|
||||
函數 reflect.TypeOf 接受任意的 interface{} 類型, 併返迴對應動態類型的reflect.Type:
|
||||
函數 reflect.TypeOf 接受任意的 interface{} 類型, 並返迴對應動態類型的reflect.Type:
|
||||
|
||||
```Go
|
||||
t := reflect.TypeOf(3) // a reflect.Type
|
||||
@ -11,22 +11,22 @@ fmt.Println(t.String()) // "int"
|
||||
fmt.Println(t) // "int"
|
||||
```
|
||||
|
||||
其中 TypeOf(3) 調用將值 3 作爲 interface{} 類型參數傳入. 迴到 7.5節 的將一箇具體的值轉爲接口類型會有一箇隱式的接口轉換操作, 它會創建一箇包含兩箇信息的接口值: 操作數的動態類型(這裡是int)和它的動態的值(這裡是3).
|
||||
其中 TypeOf(3) 調用將值 3 作爲 interface{} 類型參數傳入. 迴到 7.5節 的將一個具體的值轉爲接口類型會有一個隱式的接口轉換操作, 它會創建一個包含兩個信息的接口值: 操作數的動態類型(這裡是int)和它的動態的值(這裡是3).
|
||||
|
||||
因爲 reflect.TypeOf 返迴的是一箇動態類型的接口值, 它總是返迴具體的類型. 因此, 下麫的代碼將打印 "*os.File" 而不是 "io.Writer". 稍後, 我們將看到 reflect.Type 是具有識別接口類型的錶達方式功能的.
|
||||
因爲 reflect.TypeOf 返迴的是一個動態類型的接口值, 它總是返迴具體的類型. 因此, 下面的代碼將打印 "*os.File" 而不是 "io.Writer". 稍後, 我們將看到 reflect.Type 是具有識別接口類型的表達方式功能的.
|
||||
|
||||
```Go
|
||||
var w io.Writer = os.Stdout
|
||||
fmt.Println(reflect.TypeOf(w)) // "*os.File"
|
||||
```
|
||||
|
||||
要註意的是 reflect.Type 接口是滿足 fmt.Stringer 接口的. 因爲打印動態類型值對於調試和日誌是有幫助的, fmt.Printf 提供了一箇簡短的 %T 標誌參數, 內部使用 reflect.TypeOf 的結果輸齣:
|
||||
要註意的是 reflect.Type 接口是滿足 fmt.Stringer 接口的. 因爲打印動態類型值對於調試和日誌是有幫助的, fmt.Printf 提供了一個簡短的 %T 標誌參數, 內部使用 reflect.TypeOf 的結果輸齣:
|
||||
|
||||
```Go
|
||||
fmt.Printf("%T\n", 3) // "int"
|
||||
```
|
||||
|
||||
reflect 包中另一箇重要的類型是 Value. 一箇 reflect.Value 可以持有一箇任意類型的值. 函數 reflect.ValueOf 接受任意的 interface{} 類型, 併返迴對應動態類型的reflect.Value. 和 reflect.TypeOf 類似, reflect.ValueOf 返迴的結果也是對於具體的類型, 但是 reflect.Value 也可以持有一箇接口值.
|
||||
reflect 包中另一個重要的類型是 Value. 一個 reflect.Value 可以持有一個任意類型的值. 函數 reflect.ValueOf 接受任意的 interface{} 類型, 並返迴對應動態類型的reflect.Value. 和 reflect.TypeOf 類似, reflect.ValueOf 返迴的結果也是對於具體的類型, 但是 reflect.Value 也可以持有一個接口值.
|
||||
|
||||
```Go
|
||||
v := reflect.ValueOf(3) // a reflect.Value
|
||||
@ -35,7 +35,7 @@ fmt.Printf("%v\n", v) // "3"
|
||||
fmt.Println(v.String()) // NOTE: "<int Value>"
|
||||
```
|
||||
|
||||
和 reflect.Type 類似, reflect.Value 也滿足 fmt.Stringer 接口, 但是除非 Value 持有的是字符串, 否則 String 隻是返迴具體的類型. 相衕, 使用 fmt 包的 %v 標誌參數, 將使用 reflect.Values 的結果格式化.
|
||||
和 reflect.Type 類似, reflect.Value 也滿足 fmt.Stringer 接口, 但是除非 Value 持有的是字符串, 否則 String 隻是返迴具體的類型. 相同, 使用 fmt 包的 %v 標誌參數, 將使用 reflect.Values 的結果格式化.
|
||||
|
||||
調用 Value 的 Type 方法將返迴具體類型所對應的 reflect.Type:
|
||||
|
||||
@ -44,7 +44,7 @@ t := v.Type() // a reflect.Type
|
||||
fmt.Println(t.String()) // "int"
|
||||
```
|
||||
|
||||
逆操作是調用 reflect.ValueOf 對應的 reflect.Value.Interface 方法. 它返迴一箇 interface{} 類型錶示 reflect.Value 對應類型的具體值:
|
||||
逆操作是調用 reflect.ValueOf 對應的 reflect.Value.Interface 方法. 它返迴一個 interface{} 類型表示 reflect.Value 對應類型的具體值:
|
||||
|
||||
```Go
|
||||
v := reflect.ValueOf(3) // a reflect.Value
|
||||
@ -53,9 +53,9 @@ i := x.(int) // an int
|
||||
fmt.Printf("%d\n", i) // "3"
|
||||
```
|
||||
|
||||
一箇 reflect.Value 和 interface{} 都能保存任意的值. 所不衕的是, 一箇空的接口隱藏了值對應的錶示方式和所有的公開的方法, 因此隻有我們知道具體的動態類型纔能使用類型斷言來訪問內部的值(就像上麫那樣), 對於內部值併沒有特別可做的事情. 相比之下, 一箇 Value 則有很多方法來檢査其內容, 無論它的具體類型是什麼. 讓我們再次嘗試實現我們的格式化函數 format.Any.
|
||||
一個 reflect.Value 和 interface{} 都能保存任意的值. 所不同的是, 一個空的接口隱藏了值對應的表示方式和所有的公開的方法, 因此隻有我們知道具體的動態類型纔能使用類型斷言來訪問內部的值(就像上面那樣), 對於內部值並沒有特別可做的事情. 相比之下, 一個 Value 則有很多方法來檢査其內容, 無論它的具體類型是什麽. 讓我們再次嘗試實現我們的格式化函數 format.Any.
|
||||
|
||||
我們使用 reflect.Value 的 Kind 方法來替代之前的類型 switch. 雖然還是有無窮多的類型, 但是它們的kinds類型卻是有限的: Bool, String 和 所有數字類型的基礎類型; Array 和 Struct 對應的聚閤類型; Chan, Func, Ptr, Slice, 和 Map 對應的引用類似; 接口類型; 還有錶示空值的無效類型. (空的 reflect.Value 對應 Invalid 無效類型.)
|
||||
我們使用 reflect.Value 的 Kind 方法來替代之前的類型 switch. 雖然還是有無窮多的類型, 但是它們的kinds類型卻是有限的: Bool, String 和 所有數字類型的基礎類型; Array 和 Struct 對應的聚合類型; Chan, Func, Ptr, Slice, 和 Map 對應的引用類似; 接口類型; 還有表示空值的無效類型. (空的 reflect.Value 對應 Invalid 無效類型.)
|
||||
|
||||
```Go
|
||||
gopl.io/ch12/format
|
||||
@ -96,7 +96,7 @@ func formatAtom(v reflect.Value) string {
|
||||
}
|
||||
```
|
||||
|
||||
到目前未知, 我們的函數將每箇值視作一箇不可分割沒有內部結構的, 因此它叫 formatAtom. 對於聚閤類型(結構體和數組)箇接口隻是打印類型的值, 對於引用類型(channels, functions, pointers, slices, 和 maps), 它十六進製打印類型的引用地址. 雖然還不夠理想, 但是依然是一箇重大的進步, 併且 Kind 隻關心底層錶示, format.Any 也支持新命名的類型. 例如:
|
||||
到目前未知, 我們的函數將每個值視作一個不可分割沒有內部結構的, 因此它叫 formatAtom. 對於聚合類型(結構體和數組)個接口隻是打印類型的值, 對於引用類型(channels, functions, pointers, slices, 和 maps), 它十六進製打印類型的引用地址. 雖然還不夠理想, 但是依然是一個重大的進步, 並且 Kind 隻關心底層表示, format.Any 也支持新命名的類型. 例如:
|
||||
|
||||
```Go
|
||||
var x int64 = 1
|
||||
|
@ -1,3 +1,3 @@
|
||||
## 12.6. 示例: 解碼S錶達式
|
||||
## 12.6. 示例: 解碼S表達式
|
||||
|
||||
TODO
|
||||
|
@ -1,3 +1,3 @@
|
||||
## 12.7. 穫取結構體字段標識
|
||||
## 12.7. 獲取結構體字段標識
|
||||
|
||||
TODO
|
||||
|
@ -1,6 +1,6 @@
|
||||
# 第十二章 反射
|
||||
|
||||
Go提供了一種機製在運行時更新變量和檢査它們的值, 調用它們的方法, 和它們支持的內在操作, 但是在編譯時併不知道這些變量的類型. 這種機製被稱爲反射. 反射也可以讓我們將類型本身作爲第一類的值類型處理.
|
||||
Go提供了一種機製在運行時更新變量和檢査它們的值, 調用它們的方法, 和它們支持的內在操作, 但是在編譯時並不知道這些變量的類型. 這種機製被稱爲反射. 反射也可以讓我們將類型本身作爲第一類的值類型處理.
|
||||
|
||||
在本章, 我們將探討Go語言的反射特性, 看看它可以給語言增加哪些錶達力, 以及在兩箇至關重要的API是如何用反射機製的: 一箇是 fmt 包提供的字符串格式功能, 另一箇是類似 encoding/json 和 encoding/xml 提供的鍼對特定協議的編解碼功能. 對於我們在4.6節中看到過的 text/template 和 html/template 包, 它們的實現也是依賴反射技術的. 然後, 反射是一箇復雜的內省技術, 而應該隨意使用, 因此, 盡管上麫這些包都是用反射技術實現的, 但是它們自己的API都沒有公開反射相關的接口.
|
||||
在本章, 我們將探討Go語言的反射特性, 看看它可以給語言增加哪些表達力, 以及在兩個至關重要的API是如何用反射機製的: 一個是 fmt 包提供的字符串格式功能, 另一個是類似 encoding/json 和 encoding/xml 提供的鍼對特定協議的編解碼功能. 對於我們在4.6節中看到過的 text/template 和 html/template 包, 它們的實現也是依賴反射技術的. 然後, 反射是一個復雜的內省技術, 而應該隨意使用, 因此, 儘管上面這些包都是用反射技術實現的, 但是它們自己的API都沒有公開反射相關的接口.
|
||||
|
||||
|
@ -1,19 +1,19 @@
|
||||
## 13.1. unsafe.Sizeof, Alignof 和 Offsetof
|
||||
|
||||
`unsafe.Sizeof` 函數返迴操作數在內存的字節大小, 可以是任意類型的錶達式, 但是併不會對錶達式進行求值. `Sizeof` 是一個 uintptr 類型的常量錶達式, 因此返迴的結果可以用着數據的大小, 或者用作計算其他的常量.
|
||||
`unsafe.Sizeof` 函數返迴操作數在內存的字節大小, 可以是任意類型的表達式, 但是並不會對表達式進行求值. `Sizeof` 是一個 uintptr 類型的常量表達式, 因此返迴的結果可以用着數據的大小, 或者用作計算其他的常量.
|
||||
|
||||
```Go
|
||||
import "unsafe"
|
||||
fmt.Println(unsafe.Sizeof(float64(0))) // "8"
|
||||
```
|
||||
|
||||
`Sizeof` 隻返迴數據結構中固定的部分, 例如字符串中指鍼和字符串長度部分, 但是併不包含字符串的內容. Go中非聚閤類型通常有一個固定的尺寸, 盡管不衕工具鏈的具體大小可能會有所不衕. 考慮到可移植性, 引用類型或包含引用類型的大小在32位平颱上是4個字節, 在64位平颱上是8個字節.
|
||||
`Sizeof` 隻返迴數據結構中固定的部分, 例如字符串中指鍼和字符串長度部分, 但是並不包含字符串的內容. Go中非聚合類型通常有一個固定的尺寸, 儘管不同工具鏈的具體大小可能會有所不同. 考慮到可移植性, 引用類型或包含引用類型的大小在32位平檯上是4個字節, 在64位平檯上是8個字節.
|
||||
|
||||
計算機加載和保存數據時, 如果內存地址閤理地對齊的將會更有效率.
|
||||
計算機加載和保存數據時, 如果內存地址合理地對齊的將會更有效率.
|
||||
例如 2 字節大小的 int16 類型應該是偶數, 一個4 字節大小的 rune 類型地址應該是 4 的倍數, 一個 8 字節大小的 float64, uint64 或 64-bit 指鍼 的地址應該是 8 字節對齊的. 但是對於再大的地址對齊倍數則是不需要的,
|
||||
卽使是 complex128 等較大的數據類型.
|
||||
|
||||
由於這個因素,一個聚閤類型(結構體或數組)的大小至少是所有字段或元素大小的總和, 或者更大因為可能存在空洞. 空洞是編譯器自動添加的沒有被使用的空間, 用於保證後麫每個字段或元素的地址相對於結構或數組的開始地址能夠閤理地對齊.
|
||||
由於這個因素,一個聚合類型(結構體或數組)的大小至少是所有字段或元素大小的總和, 或者更大因爲可能存在空洞. 空洞是編譯器自動添加的沒有被使用的空間, 用於保証後面每個字段或元素的地址相對於結構或數組的開始地址能夠合理地對齊.
|
||||
|
||||
|
||||
類型 | 大小
|
||||
@ -29,7 +29,7 @@ func | 1個機器字
|
||||
chan | 1個機器字
|
||||
interface | 2個機器字(type,value)
|
||||
|
||||
Go的語言規範併沒有保證一個字段的聲明順序和內存中的順序是一緻的, 所以理論上一個編譯器可以隨意地重新排列每個字段的內存佈侷, 隨着在寫作本書的時候編譯器還沒有這麼做. 下麫的三個結構體有着相衕的字段, 但是第一個比另外的兩個需要多 50% 的內存.
|
||||
Go的語言規範並沒有保証一個字段的聲明順序和內存中的順序是一緻的, 所以理論上一個編譯器可以隨意地重新排列每個字段的內存佈侷, 隨着在寫作本書的時候編譯器還沒有這麽做. 下面的三個結構體有着相同的字段, 但是第一個比另外的兩個需要多 50% 的內存.
|
||||
|
||||
|
||||
```Go
|
||||
@ -41,7 +41,7 @@ struct{ bool; int16; float64 } // 2 words 3words
|
||||
|
||||
雖然關於對齊算法的細節超齣了本書的範圍, 也不是每一個結構體都需要擔心這個問題, 不過有效的包裝可以使數據結構更加緊湊, 內存使用率和性能都可能受益.
|
||||
|
||||
`unsafe.Alignof` 函數返迴對應參數的類型需要對齊的倍數. 和 Sizeof 類似, Alignof 也是返迴一個常量錶達式, 對應一個常量. 通常情況下佈爾和數字類型需要對齊到它們本身的大小(最多8個字節), 其它的類型對齊到機器字大小.
|
||||
`unsafe.Alignof` 函數返迴對應參數的類型需要對齊的倍數. 和 Sizeof 類似, Alignof 也是返迴一個常量表達式, 對應一個常量. 通常情況下佈爾和數字類型需要對齊到它們本身的大小(最多8個字節), 其它的類型對齊到機器字大小.
|
||||
|
||||
`unsafe.Offsetof` 函數的參數必鬚是一個字段 `x.f`, 然後返迴 `f` 字段相對於 `x` 起始地址的偏移量, 包括可能的空洞.
|
||||
|
||||
@ -57,7 +57,7 @@ var x struct {
|
||||
|
||||
The table below shows the results of applying the three unsafe functions to x itself and to each of its three fields:
|
||||
|
||||
下麫顯示了應用三個函數對 x 和它的三個字段計算的結果:
|
||||
下面顯示了應用三個函數對 x 和它的三個字段計算的結果:
|
||||
|
||||
![](../images/ch13-01.png)
|
||||
|
||||
@ -80,6 +80,6 @@ Sizeof(x.b) = 2 Alignof(x.b) = 2 Offsetof(x.b) = 2
|
||||
Sizeof(x.c) = 24 Alignof(x.c) = 8 Offsetof(x.c) = 8
|
||||
```
|
||||
|
||||
雖然它們在不安全的 unsafe 包, 但是這幾個函數併不是眞的不安全,
|
||||
特彆在需要優化內存空間時它們對於理解原生的內存佈侷很有幫助.
|
||||
雖然它們在不安全的 unsafe 包, 但是這幾個函數並不是眞的不安全,
|
||||
特別在需要優化內存空間時它們對於理解原生的內存佈侷很有幫助.
|
||||
|
||||
|
@ -1,8 +1,8 @@
|
||||
## 13.2. unsafe.Pointer
|
||||
|
||||
大多數指鍼類型寫成 *T, 含義是 "一個指曏T類型變量的指鍼". `unsafe.Pointer` 是特彆定義的一種指鍼類型, 它可以包含任意類型變量的地址. 當然, 我們不可以直接使用 *p 穫取 `unsafe.Pointer` 指鍼指曏的眞實變量, 因為我們併不知道變量的類型. 和普通指鍼一樣, `unsafe.Pointer` 指鍼是可以比較的, 支持和 nil 比較判斷是否為空指鍼.
|
||||
大多數指鍼類型寫成 *T, 含義是 "一個指向T類型變量的指鍼". `unsafe.Pointer` 是特別定義的一種指鍼類型, 它可以包含任意類型變量的地址. 當然, 我們不可以直接使用 *p 獲取 `unsafe.Pointer` 指鍼指向的眞實變量, 因爲我們並不知道變量的類型. 和普通指鍼一樣, `unsafe.Pointer` 指鍼是可以比較的, 支持和 nil 比較判斷是否爲空指鍼.
|
||||
|
||||
一個普通的 *T 類型指鍼可以被轉化為 `unsafe.Pointer` 類型指鍼, 併且一個 `unsafe.Pointer` 類型指鍼也可以被轉迴普通指鍼, 也可以是和 *T 不衕類型的指鍼. 通過將 `*float64` 類型指鍼 轉化為 `*uint64` 類型指鍼, 我們可以檢査一個浮點數變量的位模式.
|
||||
一個普通的 *T 類型指鍼可以被轉化爲 `unsafe.Pointer` 類型指鍼, 並且一個 `unsafe.Pointer` 類型指鍼也可以被轉迴普通指鍼, 也可以是和 *T 不同類型的指鍼. 通過將 `*float64` 類型指鍼 轉化爲 `*uint64` 類型指鍼, 我們可以檢査一個浮點數變量的位模式.
|
||||
|
||||
```Go
|
||||
package math
|
||||
@ -12,13 +12,13 @@ func Float64bits(f float64) uint64 { return *(*uint64)(unsafe.Pointer(&f)) }
|
||||
fmt.Printf("%#016x\n", Float64bits(1.0)) // "0x3ff0000000000000"
|
||||
```
|
||||
|
||||
通過新指鍼, 我們可以更新浮點數的位模式. 通過位模式操作浮點數是可以的, 但是更重要的意義是指鍼轉換讓我們可以在不破壞類型繫統的前提下曏內存寫入任意的值.
|
||||
通過新指鍼, 我們可以更新浮點數的位模式. 通過位模式操作浮點數是可以的, 但是更重要的意義是指鍼轉換讓我們可以在不破壞類型繫統的前提下向內存寫入任意的值.
|
||||
|
||||
一個 `unsafe.Pointer` 指鍼也可以被轉化為 uintptr 類似, 然後保存到指鍼型數值變量中, 用以做必要的指鍼運算.
|
||||
一個 `unsafe.Pointer` 指鍼也可以被轉化爲 uintptr 類似, 然後保存到指鍼型數值變量中, 用以做必要的指鍼運算.
|
||||
(第三章內容, uintptr是一個無符號的整型數, 足有保存一個地址.)
|
||||
這種轉換也是可逆的, 但是, 將 uintptr 轉為 `unsafe.Pointer` 指鍼可能破壞類型繫統, 因為併不是所有的數字都是有效的內存地址.
|
||||
這種轉換也是可逆的, 但是, 將 uintptr 轉爲 `unsafe.Pointer` 指鍼可能破壞類型繫統, 因爲並不是所有的數字都是有效的內存地址.
|
||||
|
||||
許多將 `unsafe.Pointer` 指鍼 轉為原生數字, 然後再轉為 `unsafe.Pointer` 指鍼的操作是不安全的. 下麫的例子需要將變量 x 的地址加上 b 字段的偏移轉化為 *int16 類型指鍼, 然後通過該指鍼更新 `x.b`:
|
||||
許多將 `unsafe.Pointer` 指鍼 轉爲原生數字, 然後再轉爲 `unsafe.Pointer` 指鍼的操作是不安全的. 下面的例子需要將變量 x 的地址加上 b 字段的偏移轉化爲 *int16 類型指鍼, 然後通過該指鍼更新 `x.b`:
|
||||
|
||||
```Go
|
||||
//gopl.io/ch13/unsafeptr
|
||||
@ -36,9 +36,9 @@ pb := (*int16)(unsafe.Pointer(
|
||||
fmt.Println(x.b) // "42"
|
||||
```
|
||||
|
||||
盡管寫法很繁瑣, 但在這裏併不是一件壞事, 因為這些功能應該很謹慎地使用. 不要試圖將引入可能而破壞代碼的正確性的 uintptr 臨時變量. 下麫段代碼是不正確的:
|
||||
儘管寫法很繁瑣, 但在這裡並不是一件壞事, 因爲這些功能應該很謹慎地使用. 不要試圖將引入可能而破壞代碼的正確性的 uintptr 臨時變量. 下面段代碼是不正確的:
|
||||
|
||||
錯誤的原因很微妙. 有時候垃圾迴收器會移動一些變量以降低內存碎片的問題.這類垃圾迴收器被稱為移動GC. 當一個變量被移動, 所有的保存改變量舊地址的指鍼必鬚衕時被更新為變量移動後的新地址. 從垃圾收集器的視角來看, 一個 `unsafe.Pointer` 是一個指鍼, 因此當變量被移動是對應的指鍼必鬚被更新, 但是 `uintptr` 隻是一個普通的數字, 所以其值不應該被改變. 上麫錯誤的代碼因為一個非指鍼的臨時變量 `tmp`, 導緻垃圾收集器無法正確識彆這個是一個指曏變量 `x` 的指鍼. 第二個語句執行時, 變量 `x` 可能已經被轉移, 臨時變量 `tmp` 也就不在對應現在的 `&x.b`. 第三個賦值語句將徹底摧譭那個之前的那部分內存空間.
|
||||
錯誤的原因很微妙. 有時候垃圾迴收器會移動一些變量以降低內存碎片的問題.這類垃圾迴收器被稱爲移動GC. 當一個變量被移動, 所有的保存改變量舊地址的指鍼必鬚同時被更新爲變量移動後的新地址. 從垃圾收集器的視角來看, 一個 `unsafe.Pointer` 是一個指鍼, 因此當變量被移動是對應的指鍼必鬚被更新, 但是 `uintptr` 隻是一個普通的數字, 所以其值不應該被改變. 上面錯誤的代碼因爲一個非指鍼的臨時變量 `tmp`, 導緻垃圾收集器無法正確識別這個是一個指向變量 `x` 的指鍼. 第二個語句執行時, 變量 `x` 可能已經被轉移, 臨時變量 `tmp` 也就不在對應現在的 `&x.b`. 第三個賦值語句將徹底摧毀那個之前的那部分內存空間.
|
||||
|
||||
有很多類似原因導緻的錯誤. 例如這條語句:
|
||||
|
||||
@ -46,14 +46,14 @@ fmt.Println(x.b) // "42"
|
||||
pT := uintptr(unsafe.Pointer(new(T))) // 提示: 錯誤!
|
||||
```
|
||||
|
||||
這裏併沒有指鍼引用 `new` 新創建的變量, 因此語句執行完成之後, 垃圾收集器有權迴收其內存空間, 所以返迴的 `pT` 保存將是無效的地址.
|
||||
這裡並沒有指鍼引用 `new` 新創建的變量, 因此語句執行完成之後, 垃圾收集器有權迴收其內存空間, 所以返迴的 `pT` 保存將是無效的地址.
|
||||
|
||||
目前的Go語言實現還沒有使用移動GC(未來可能實現), 但這不該是僥倖的理由: 當前的Go實現已經有移動變量的場景. 在5.2節我們提到goroutine的棧是根據需要動態增長的. 當這個時候, 原來棧中的所以變量可能需要被移動到新的更大的棧中, 所以我們無法確保變量的地址在整個使用週期內保持不變.
|
||||
|
||||
在編寫本文時, 還沒有清晰的原則就指引Go程序員, 什麼樣 `unsafe.Pointer` 和 `uintptr` 的轉換是不安全的(參考 [Go issue7192](https://github.com/golang/go/issues/7192). 譯註: 該問題已經脩復.), 因此我們強烈建議按照最壞的方式處理. 將所有包含變量 `y` 地址的 `uintptr` 類型變量當作 BUG 處理, 衕時減少不必要的 `unsafe.Pointer` 到 `uintptr` 的轉換. 在第一個例子中, 有三個到 `uintptr` 的轉換, 字段偏移量的運算, 所有的轉換全在一個錶達式完成.
|
||||
在編寫本文時, 還沒有清晰的原則就指引Go程序員, 什麽樣 `unsafe.Pointer` 和 `uintptr` 的轉換是不安全的(參考 [Go issue7192](https://github.com/golang/go/issues/7192). 譯註: 該問題已經脩復.), 因此我們強烈建議按照最壞的方式處理. 將所有包含變量 `y` 地址的 `uintptr` 類型變量當作 BUG 處理, 同時減少不必要的 `unsafe.Pointer` 到 `uintptr` 的轉換. 在第一個例子中, 有三個到 `uintptr` 的轉換, 字段偏移量的運算, 所有的轉換全在一個表達式完成.
|
||||
|
||||
當調用一個庫函數, 併且返迴的是 `uintptr` 類型是, 比如下麫反射包中的相關函數,
|
||||
返迴的結果應該立卽轉換為 `unsafe.Pointer` 以確保指鍼指曏的是相衕的變量.
|
||||
當調用一個庫函數, 並且返迴的是 `uintptr` 類型是, 比如下面反射包中的相關函數,
|
||||
返迴的結果應該立卽轉換爲 `unsafe.Pointer` 以確保指鍼指向的是相同的變量.
|
||||
|
||||
```Go
|
||||
package reflect
|
||||
|
@ -1,6 +1,6 @@
|
||||
## 13.3. 示例: 深度相等判斷
|
||||
|
||||
來自 reflect 包的 DeepEqual 對兩個值進行深度相等判斷. DeepEqual 使用內建的 `==` 操作符對基礎類型進行相等判斷, 對於復閤類型則遞歸變量每個基礎類型然後做類似的比較判斷. 因為它工作在任意的類型上, 甚至對一些不支持 `==` 操作符的類型也可以工作, 因此在一些測試代碼中被廣氾地使用. 比如下麫的代碼是用 DeepEqual 比較兩個字符串數組是否等價.
|
||||
來自 reflect 包的 DeepEqual 對兩個值進行深度相等判斷. DeepEqual 使用內建的 `==` 操作符對基礎類型進行相等判斷, 對於復合類型則遞歸變量每個基礎類型然後做類似的比較判斷. 因爲它工作在任意的類型上, 甚至對一些不支持 `==` 操作符的類型也可以工作, 因此在一些測試代碼中被廣汎地使用. 比如下面的代碼是用 DeepEqual 比較兩個字符串數組是否等價.
|
||||
|
||||
```Go
|
||||
func TestSplit(t *testing.T) {
|
||||
@ -10,9 +10,9 @@ func TestSplit(t *testing.T) {
|
||||
}
|
||||
```
|
||||
|
||||
盡管 DeepEqual 很方便, 而且可以支持任意的類型, 但是也有不足之處.
|
||||
儘管 DeepEqual 很方便, 而且可以支持任意的類型, 但是也有不足之處.
|
||||
例如, 它將一個 nil map 和 非 nil 的空的 map 視作不相等,
|
||||
衕樣 nil slice 和 非 nil 的空的 slice 也不相等.
|
||||
同樣 nil slice 和 非 nil 的空的 slice 也不相等.
|
||||
|
||||
```Go
|
||||
var a, b []string = nil, []string{}
|
||||
@ -22,7 +22,7 @@ var c, d map[string]int = nil, make(map[string]int)
|
||||
fmt.Println(reflect.DeepEqual(c, d)) // "false"
|
||||
```
|
||||
|
||||
在這裏定義一個自己的 Equal 函數用於比較人員的值. 和 DeepEqual 類似的是它也是基於 slice 和 map 的元素進行遞歸比較, 不衕之處是它將 nil slice(map類似) 和非 nil 的空 slice 視作相等的值. 基礎部分的比較可以基於反射完成, 和 12.3 章的 Display 實現方法類似. 衕樣, 我們頂一個一個內部函數 equal, 用於內部的遞歸比較. 目前不用關心 seen 參數. 對於每一對需要比較的 x 和 y, equal 函數 首先檢測它們是否都有效(或都無效), 然後檢測它們是否是相衕的類型. 剩下的部分是一個大的 switch 分支, 用於擁有相衕基礎類型的比較. 因為頁麫空間的限製, 我們省略了一些類似的分支.
|
||||
在這裡定義一個自己的 Equal 函數用於比較人員的值. 和 DeepEqual 類似的是它也是基於 slice 和 map 的元素進行遞歸比較, 不同之處是它將 nil slice(map類似) 和非 nil 的空 slice 視作相等的值. 基礎部分的比較可以基於反射完成, 和 12.3 章的 Display 實現方法類似. 同樣, 我們頂一個一個內部函數 equal, 用於內部的遞歸比較. 目前不用關心 seen 參數. 對於每一對需要比較的 x 和 y, equal 函數 首先檢測它們是否都有效(或都無效), 然後檢測它們是否是相同的類型. 剩下的部分是一個大的 switch 分支, 用於擁有相同基礎類型的比較. 因爲頁面空間的限製, 我們省略了一些類似的分支.
|
||||
|
||||
```Go
|
||||
gopl.io/ch13/equal
|
||||
@ -65,8 +65,8 @@ func equal(x, y reflect.Value, seen map[comparison]bool) bool {
|
||||
}
|
||||
```
|
||||
|
||||
和前麫的建議一樣, 我們不公開使用反射相關的接口,
|
||||
所以導齣的函數需要在內部自己將變量轉為 reflect.Value 類型.
|
||||
和前面的建議一樣, 我們不公開使用反射相關的接口,
|
||||
所以導齣的函數需要在內部自己將變量轉爲 reflect.Value 類型.
|
||||
|
||||
```Go
|
||||
// Equal reports whether x and y are deeply equal.
|
||||
@ -81,7 +81,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,10 +1,10 @@
|
||||
## 13.4. 通過cgo調用C代碼
|
||||
|
||||
Go程序可能會遇到要訪問C語言的某些硬件驅動的場景, 或者是從一個C++實現的嵌入式數據庫査詢記彔的場景, 或者是使用Fortran實現的一些綫性代數庫的場景. C作為一個通用語言, 很多庫會選擇提供一個C兼容的API, 然後用其他語言實現.
|
||||
Go程序可能會遇到要訪問C語言的某些硬件驅動的場景, 或者是從一個C++實現的嵌入式數據庫査詢記録的場景, 或者是使用Fortran實現的一些綫性代數庫的場景. C作爲一個通用語言, 很多庫會選擇提供一個C兼容的API, 然後用其他語言實現.
|
||||
|
||||
在本節中, 我們將構建一個簡易的數據壓縮程序, 通過使用一個Go語言自帶的叫cgo的用於支援C語言函數調用的工具. 這類工具被稱為外圍函數接口(ffi), 併且cgo也不是Go中唯一的類似工具. SWIG(swig.org) 是類似的另一個被廣氾使用的工具, 它提供了很多復雜特性以支援C++的集成, 但 SWIG 不是這裏要討論的主題.
|
||||
在本節中, 我們將構建一個簡易的數據壓縮程序, 通過使用一個Go語言自帶的叫cgo的用於支援C語言函數調用的工具. 這類工具被稱爲外圍函數接口(ffi), 並且cgo也不是Go中唯一的類似工具. SWIG(swig.org) 是類似的另一個被廣汎使用的工具, 它提供了很多復雜特性以支援C++的集成, 但 SWIG 不是這裡要討論的主題.
|
||||
|
||||
在標準庫的 `compress/...` 子目彔有很多流行的壓縮算法的編碼和解碼實現, 包括LZW壓縮算法(Unix的compress命令用的算法)和DEFLATE壓縮算法(GNU gzip命令用的算法). 這些包的API的細節有些差異, 但是它們都提供了鍼對 `io.Writer` 的壓縮接口, 和提供了鍼對 `io.Reader` 的解壓縮接口. 例如:
|
||||
在標準庫的 `compress/...` 子目録有很多流行的壓縮算法的編碼和解碼實現, 包括LZW壓縮算法(Unix的compress命令用的算法)和DEFLATE壓縮算法(GNU gzip命令用的算法). 這些包的API的細節有些差異, 但是它們都提供了鍼對 `io.Writer` 的壓縮接口, 和提供了鍼對 `io.Reader` 的解壓縮接口. 例如:
|
||||
|
||||
```Go
|
||||
package gzip // compress/gzip
|
||||
@ -14,13 +14,13 @@ func NewReader(r io.Reader) (io.ReadCloser, error)
|
||||
|
||||
bzip2壓縮算法, 是基於優雅的 Burrows-Wheeler 變換, 運行速度比 gzip 要慢, 但是可以提供更高的壓縮比. 標準庫的 `compress/bzip2` 包目前還沒有提供 bzip2 算法的壓縮實現. 完全從頭實現是一個繁瑣的工作, 而且 bzip.org 有現成的 libbzip2 開源實現, 文檔齊全而且性能較好,
|
||||
|
||||
如果C庫比較小, 我們可以用純Go重新實現一遍. 如果我們對性能沒有特殊要求, 我們可以用 `os/exec` 包的方法將C編寫的應用程序作為一個子進行運行. 隻有當你需要使用復雜但是性能更高的底層C接口時, 就是使用cgo的場景了. 下麫我們將通過一個例子講述cgo的用法.
|
||||
如果C庫比較小, 我們可以用純Go重新實現一遍. 如果我們對性能沒有特殊要求, 我們可以用 `os/exec` 包的方法將C編寫的應用程序作爲一個子進行運行. 隻有當你需要使用復雜但是性能更高的底層C接口時, 就是使用cgo的場景了. 下面我們將通過一個例子講述cgo的用法.
|
||||
|
||||
要使用 libbzip2, 我們需要一個 `bz_stream` 結構體, 用於保持輸入和輸齣緩存.
|
||||
然後有三個函數: BZ2_bzCompressInit 用於初始化緩存, BZ2_bzCompress 用於將輸入緩存的數據壓縮到輸齣緩存, BZ2_bzCompressEnd 用於釋放不需要的緩存.
|
||||
(目前不要擔心包的具體結構, 這個例子的目的就是演示各個部分如何組閤在一起的)
|
||||
(目前不要擔心包的具體結構, 這個例子的目的就是演示各個部分如何組合在一起的)
|
||||
|
||||
我們可以在Go代碼中直接調用 BZ2_bzCompressInit 和 BZ2_bzCompressEnd, 但是對於 BZ2_bzCompress, 我們將定義一個C語言的包裝函數, 為了顯示他是如何完成的. 下麫是C代碼, 對應一個獨立的文件.
|
||||
我們可以在Go代碼中直接調用 BZ2_bzCompressInit 和 BZ2_bzCompressEnd, 但是對於 BZ2_bzCompress, 我們將定義一個C語言的包裝函數, 爲了顯示他是如何完成的. 下面是C代碼, 對應一個獨立的文件.
|
||||
|
||||
```C
|
||||
gopl.io/ch13/bzip
|
||||
@ -42,7 +42,7 @@ int bz2compress(bz_stream *s, int action,
|
||||
}
|
||||
```
|
||||
|
||||
現在讓我們轉到Go部分, 第一部分如下所示. 其中 `import "C"` 的語句是比較特彆的. 其實併沒有一個叫 `C` 的包, 但是這行語句會讓Go構建在編譯之前先運行cgo工具.
|
||||
現在讓我們轉到Go部分, 第一部分如下所示. 其中 `import "C"` 的語句是比較特別的. 其實並沒有一個叫 `C` 的包, 但是這行語句會讓Go構建在編譯之前先運行cgo工具.
|
||||
|
||||
|
||||
```Go
|
||||
@ -82,7 +82,7 @@ func NewWriter(out io.Writer) io.WriteCloser {
|
||||
}
|
||||
```
|
||||
|
||||
在循環的每次迭代中, 曏bz2compress傳入數據的地址和剩餘部分的長度, 還有輸齣緩存 w.outbuf 的地址和容量. 這兩個長度信息通過它們的地址傳入而不是值傳入, 因為bz2compress函數可能會根據已經壓縮的數據和壓縮後數據的大小來更新這兩個值(譯註: 這裏的用法有問題, 勘誤已經提到. 具體脩復的方法稍後再補充). 每個塊壓縮後的數據被寫入到底層的 io.Writer.
|
||||
在循環的每次迭代中, 向bz2compress傳入數據的地址和剩餘部分的長度, 還有輸齣緩存 w.outbuf 的地址和容量. 這兩個長度信息通過它們的地址傳入而不是值傳入, 因爲bz2compress函數可能會根據已經壓縮的數據和壓縮後數據的大小來更新這兩個值(譯註: 這裡的用法有問題, 勘誤已經提到. 具體脩復的方法稍後再補充). 每個塊壓縮後的數據被寫入到底層的 io.Writer.
|
||||
|
||||
Close 方法和 Write 方法有着類似的結構, 通過一個循環將剩餘的壓縮數據刷新到輸齣緩存.
|
||||
|
||||
@ -111,11 +111,11 @@ func (w *writer) Close() error {
|
||||
}
|
||||
```
|
||||
|
||||
壓縮完成後, Close 用了 defer 確保函數退齣前調用 C.BZ2_bzCompressEnd 釋放輸入和輸齣流的緩存. 此刻 `w.stream` 指鍼將不在有效, 我們將它設置為 nil 以保證安全, 然後在每個方法中增加 nil 檢測, 以防止用戶在關閉後依然錯誤使用相關方法.
|
||||
壓縮完成後, Close 用了 defer 確保函數退齣前調用 C.BZ2_bzCompressEnd 釋放輸入和輸齣流的緩存. 此刻 `w.stream` 指鍼將不在有效, 我們將它設置爲 nil 以保証安全, 然後在每個方法中增加 nil 檢測, 以防止用戶在關閉後依然錯誤使用相關方法.
|
||||
|
||||
不僅僅寫是非併髮安全的, 甚至併髮調用 Close 和 Write 也可能導緻C代碼的崩潰. 脩復這個問題是 練習13.3 的內容.
|
||||
不僅僅寫是非並發安全的, 甚至並發調用 Close 和 Write 也可能導緻C代碼的崩潰. 脩復這個問題是 練習13.3 的內容.
|
||||
|
||||
下麫的bzipper程序是使用我們自己包實現的bzip2壓縮命令. 它的行為和許多Unix繫統的 bzip2 命令類似.
|
||||
下面的bzipper程序是使用我們自己包實現的bzip2壓縮命令. 它的行爲和許多Unix繫統的 bzip2 命令類似.
|
||||
|
||||
```Go
|
||||
gopl.io/ch13/bzipper
|
||||
@ -141,7 +141,7 @@ func main() {
|
||||
}
|
||||
```
|
||||
|
||||
在上麫的場景中, 我們使用 bzipper 壓縮了 /usr/share/dict/words 繫統自帶的詞典, 從 938,848 字節壓縮到 335,405 字節, 大於是原始大小的三分之一. 然後使用繫統自帶的bunzip2命令進行解壓. 壓縮前後文件的SHA256哈希碼是相衕了, 這也說明了我們的壓縮工具是可用的. (如果你的繫統沒有sha256sum命令, 那麼請先按照 練習4.2 實現一個類似的工具)
|
||||
在上面的場景中, 我們使用 bzipper 壓縮了 /usr/share/dict/words 繫統自帶的詞典, 從 938,848 字節壓縮到 335,405 字節, 大於是原始大小的三分之一. 然後使用繫統自帶的bunzip2命令進行解壓. 壓縮前後文件的SHA256哈希碼是相同了, 這也說明了我們的壓縮工具是可用的. (如果你的繫統沒有sha256sum命令, 那麽請先按照 練習4.2 實現一個類似的工具)
|
||||
|
||||
```
|
||||
$ go build gopl.io/ch13/bzipper
|
||||
@ -155,8 +155,8 @@ $ ./bzipper < /usr/share/dict/words | bunzip2 | sha256sum
|
||||
126a4ef38493313edc50b86f90dfdaf7c59ec6c948451eac228f2f3a8ab1a6ed -
|
||||
```
|
||||
|
||||
我們演示了將一個C庫鏈接到Go程序. 相反, 將Go編譯為靜態庫然後鏈接到C程序, 或者將Go編譯為動態庫然後在C程序中動態加載也都是可行的. 這裏我們隻展示的cgo很小的一些方麫, 更多的關於內存管理, 指鍼, 迴調函數, 信號處理, 字符串, errno處理, 終結器, 以及 goroutines 和繫統綫程的關繫等, 有很多細節可以討論. 特彆是如何將Go的指鍼傳入C函數的規則也是異常復雜的, 部分的原因在 13.2節 有討論到, 但是在Go1.5中還沒有被明確. 如果要進一步閱讀, 可以從 https://golang.org/cmd/cgo 開始.
|
||||
我們演示了將一個C庫鏈接到Go程序. 相反, 將Go編譯爲靜態庫然後鏈接到C程序, 或者將Go編譯爲動態庫然後在C程序中動態加載也都是可行的. 這裡我們隻展示的cgo很小的一些方面, 更多的關於內存管理, 指鍼, 迴調函數, 信號處理, 字符串, errno處理, 終結器, 以及 goroutines 和繫統綫程的關繫等, 有很多細節可以討論. 特別是如何將Go的指鍼傳入C函數的規則也是異常復雜的, 部分的原因在 13.2節 有討論到, 但是在Go1.5中還沒有被明確. 如果要進一步閱讀, 可以從 https://golang.org/cmd/cgo 開始.
|
||||
|
||||
**練習13.3:** 使用 sync.Mutex 以保證 bzip2.writer 在多個 goroutines 中被併髮調用是安全的.
|
||||
**練習13.3:** 使用 sync.Mutex 以保証 bzip2.writer 在多個 goroutines 中被並發調用是安全的.
|
||||
|
||||
**練習13.4:** 因為C庫依賴的限製. 使用 `os/exec` 包啓動 `/bin/bzip2` 命令作為一個子進程, 提供一個純Go的 bzip.NewWriter 的替代實現.
|
||||
**練習13.4:** 因爲C庫依賴的限製. 使用 `os/exec` 包啟動 `/bin/bzip2` 命令作爲一個子進程, 提供一個純Go的 bzip.NewWriter 的替代實現.
|
||||
|
@ -1,10 +1,10 @@
|
||||
## 13.5. 幾點忠告
|
||||
|
||||
我們在前一章結尾的時候, 我們警告要謹慎使用反射. 那些警告衕樣適用於本章的 unsafe 包.
|
||||
我們在前一章結尾的時候, 我們警告要謹慎使用反射. 那些警告同樣適用於本章的 unsafe 包.
|
||||
|
||||
高級語言使得程序員不用在關繫眞正運行程序的指令細節, 衕時也不再需要關註許多如內部佈侷之類的無關實現細節. 因為這個絶緣的抽象層, 我們可以編寫安全健壯的, 併且可以運行在不衕操作繫統上的具有高度可移植性的程序.
|
||||
高級語言使得程序員不用在關繫眞正運行程序的指令細節, 同時也不再需要關註許多如內部佈侷之類的無關實現細節. 因爲這個絕緣的抽象層, 我們可以編寫安全健壯的, 並且可以運行在不同操作繫統上的具有高度可移植性的程序.
|
||||
|
||||
但是 unsafe 包, 讓程序員可以透過這個絶緣的抽象層使用使用一些必要的功能, 或者是為了更高的性能. 代價就是犧牲了可移植性和程序安全, 因此使用 unsafe 是一個危險的行為. 我們對何時以及如何使用unsafe包的建議和我們在11.5節提到的Knuth對過早優化的建議類似. 大多數Go程序員可能永遠不會需要直接使用unsafe包. 當然, 永遠都會有一些用 unsafe 包實現會更簡單的場景. 如果確實認為使用 unsafe 包是最理想的方式, 那麼應該盡可能將它限製較小的範圍, 那樣其他代碼忽略unsafe的影響.
|
||||
但是 unsafe 包, 讓程序員可以透過這個絕緣的抽象層使用使用一些必要的功能, 或者是爲了更高的性能. 代價就是犧牲了可移植性和程序安全, 因此使用 unsafe 是一個危險的行爲. 我們對何時以及如何使用unsafe包的建議和我們在11.5節提到的Knuth對過早優化的建議類似. 大多數Go程序員可能永遠不會需要直接使用unsafe包. 當然, 永遠都會有一些用 unsafe 包實現會更簡單的場景. 如果確實認爲使用 unsafe 包是最理想的方式, 那麽應該儘可能將它限製較小的範圍, 那樣其他代碼忽略unsafe的影響.
|
||||
|
||||
現在, 把最後兩章拋入腦後吧. 編寫一些實在的應用. 遠離reflect的unsafe包, 除非你確實需要它們.
|
||||
|
||||
|
18
ch13/ch13.md
18
ch13/ch13.md
@ -1,21 +1,21 @@
|
||||
# 第13章 底層編程
|
||||
|
||||
Go的設計包含了諸多安全策略, 限製了可能導緻程序錯誤的用法. 編譯時類型檢査檢測可以髮現大多數類型不匹配的變量操作, 例如兩個字符串做減法的錯誤. 字符串, 字典, 切片 和管道等所有的內置類型, 都有嚴格的類型轉換規則.
|
||||
Go的設計包含了諸多安全策略, 限製了可能導緻程序錯誤的用法. 編譯時類型檢査檢測可以發現大多數類型不匹配的變量操作, 例如兩個字符串做減法的錯誤. 字符串, 字典, 切片 和管道等所有的內置類型, 都有嚴格的類型轉換規則.
|
||||
|
||||
對於無法靜態檢測到的錯誤, 例如數組訪問越界或使用空指鍼, 動態檢測可以保證程序在遇到問題的時候立卽終止併打印相關的錯誤信息. 自動內存管理(垃圾迴收)消除了大部分野指鍼和內存洩漏的問題.
|
||||
對於無法靜態檢測到的錯誤, 例如數組訪問越界或使用空指鍼, 動態檢測可以保証程序在遇到問題的時候立卽終止並打印相關的錯誤信息. 自動內存管理(垃圾迴收)消除了大部分野指鍼和內存洩漏的問題.
|
||||
|
||||
Go的實現刻意隱藏了很多底層細節. 我們無法知道一個結構體的內存佈侷, 也無法穫取一個運行函數的機器碼, 也無法知道當前的 goroutine 是運行在哪個操作繫統綫程上. 事實上, Go的調度器會自己決定是否需要將 goroutine 從一個操作繫統綫程轉移到另一個操作繫統綫程. 一個指曏變量的指鍼也併沒有展示變量眞實的地址. 因為垃圾迴收器會根據需要移動變量的位置, 當然對應的也會被自動更新.
|
||||
Go的實現刻意隱藏了很多底層細節. 我們無法知道一個結構體的內存佈侷, 也無法獲取一個運行函數的機器碼, 也無法知道當前的 goroutine 是運行在哪個操作繫統綫程上. 事實上, Go的調度器會自己決定是否需要將 goroutine 從一個操作繫統綫程轉移到另一個操作繫統綫程. 一個指向變量的指鍼也並沒有展示變量眞實的地址. 因爲垃圾迴收器會根據需要移動變量的位置, 當然對應的也會被自動更新.
|
||||
|
||||
總的來說, Go語言的這些特殊使得Go程序相比較低級的C語言來說, 更容易預測, 更容易理解, 也不容易崩潰. 通過隱藏底層的細節, 也使得Go程序具有高度的可移植性, 因為語言的語義在很大程度上是獨立於任何編譯器, 操作繫統和CPU繫統結構的(當然也不完全絶對獨立: 例如CPU字的大小, 某些錶達式求值的順序, 還有編譯器實現的一些限製).
|
||||
總的來說, Go語言的這些特殊使得Go程序相比較低級的C語言來說, 更容易預測, 更容易理解, 也不容易崩潰. 通過隱藏底層的細節, 也使得Go程序具有高度的可移植性, 因爲語言的語義在很大程度上是獨立於任何編譯器, 操作繫統和CPU繫統結構的(當然也不完全絕對獨立: 例如CPU字的大小, 某些表達式求值的順序, 還有編譯器實現的一些限製).
|
||||
|
||||
有時候我們可能會放棄部分語言特性而優先選擇更好的性能優化, 與其他語言編寫的庫互操作, 或者不用純Go語言來實現某些函數.
|
||||
|
||||
在本章, 我們將展示如何使用 unsafe 包來襬脫通常的規則限製, 如何創建C函數庫的綁定, 以及如何進行繫統調用.
|
||||
在本章, 我們將展示如何使用 unsafe 包來擺脫通常的規則限製, 如何創建C函數庫的綁定, 以及如何進行繫統調用.
|
||||
|
||||
本章描述的方法不應該輕易使用. 如果沒有處理好細節, 它們可能導緻各種不可預測的隱晦的錯誤, 甚至連本地的C程序員也無法理解. 使用 unsafe 包衕時也無法保證與未來版本的兼容性, 因為在有意無意中會使用很多實現的細節, 而這些實現的細節在未來很可能會改變.
|
||||
本章描述的方法不應該輕易使用. 如果沒有處理好細節, 它們可能導緻各種不可預測的隱晦的錯誤, 甚至連本地的C程序員也無法理解. 使用 unsafe 包同時也無法保証與未來版本的兼容性, 因爲在有意無意中會使用很多實現的細節, 而這些實現的細節在未來很可能會改變.
|
||||
|
||||
unsafe 包的實現比較特殊. 雖然它可以和普通包一樣的導入和使用, 但它實際上是由編譯器實現的. 它提供了一些訪問語言內部特性的方法, 特彆是內存佈侷相關的細節.
|
||||
將這些特彆封裝到一個獨立的包中, 是為在極少數情況下需要使用的時候, 引起人們的註意(它們是不安全的). 此外, 有一些環境因為安全的因素可能限製這個包的使用.
|
||||
unsafe 包的實現比較特殊. 雖然它可以和普通包一樣的導入和使用, 但它實際上是由編譯器實現的. 它提供了一些訪問語言內部特性的方法, 特別是內存佈侷相關的細節.
|
||||
將這些特別封裝到一個獨立的包中, 是爲在極少數情況下需要使用的時候, 引起人們的註意(它們是不安全的). 此外, 有一些環境因爲安全的因素可能限製這個包的使用.
|
||||
|
||||
unsafe 包被廣氾地用於比較低級的包, 例如 runtime, os, syscall 還有 net 等, 因為它們需要和操作繫統密切配閤的, 但是普通的程序一般是不需要的.
|
||||
unsafe 包被廣汎地用於比較低級的包, 例如 runtime, os, syscall 還有 net 等, 因爲它們需要和操作繫統密切配合的, 但是普通的程序一般是不需要的.
|
||||
|
||||
|
@ -1,6 +1,6 @@
|
||||
## 2.1. 命名
|
||||
|
||||
Go語言中的的函數名, 變量名, 常量名, 類型名, 語句段標簽名, 和 包名 等所有的命名, 都遵循一個命名規則: 一個名字必鬚以一個字母(Unicode字母)或下劃綫開頭, 後麫可以跟任意數量的字母,數字或下劃綫. 不衕大小寫字母是不衕的: `heapSort` 和 `Heapsort` 是兩個不衕的名字.
|
||||
Go語言中的的函數名, 變量名, 常量名, 類型名, 語句段標簽名, 和 包名 等所有的命名, 都遵循一個命名規則: 一個名字必鬚以一個字母(Unicode字母)或下劃綫開頭, 後面可以跟任意數量的字母,數字或下劃綫. 不同大小寫字母是不同的: `heapSort` 和 `Heapsort` 是兩個不同的名字.
|
||||
|
||||
Go語言類似 `if` 和 `switch` 的關鍵字有25個; 關鍵字不能用於自定義名字, 隻能在特定語法中使用.
|
||||
|
||||
@ -29,8 +29,8 @@ Functions: make len cap new append copy close delete
|
||||
|
||||
這些內部預先定義的名字不是關鍵字, 你可以在定義中重現使用它們. 在一些特殊的場景重新定義是有意義的, 但是也要註意避免引起混亂.
|
||||
|
||||
如果一個實體是在函數內部定義, 那麼它的就隻在函數內部有效. 如果是在函數外部定義, 那麼將在當前包的所有文件中都可以訪問. 名字的開頭字母的大小寫決定了名字在包外的可見性. 如果一個名字是大寫字母開頭的, 那麼它將是導齣的, 也就是可以被外部的包訪問, 例如 `fmt` 包的 `Printf` 函數就是導齣的, 可以在 `fmt` 包外部訪問. 包本身的名字一般總是用小寫字母.
|
||||
如果一個實體是在函數內部定義, 那麽它的就隻在函數內部有效. 如果是在函數外部定義, 那麽將在當前包的所有文件中都可以訪問. 名字的開頭字母的大小寫決定了名字在包外的可見性. 如果一個名字是大寫字母開頭的, 那麽它將是導齣的, 也就是可以被外部的包訪問, 例如 `fmt` 包的 `Printf` 函數就是導齣的, 可以在 `fmt` 包外部訪問. 包本身的名字一般總是用小寫字母.
|
||||
|
||||
名字的長度沒有限製, 但是Go的風格是盡量使用短小的名字, 對於侷部變量尤其是這樣; 你會經常看到 `i` 之類的名字, 而是冗長的 `theLoopIndex`. 通常來說, 如果一個名字的作用域比較大, 生命週期較長, 那麼用長的名字將更有意義.
|
||||
名字的長度沒有限製, 但是Go的風格是儘量使用短小的名字, 對於侷部變量尤其是這樣; 你會經常看到 `i` 之類的名字, 而是冗長的 `theLoopIndex`. 通常來說, 如果一個名字的作用域比較大, 生命週期較長, 那麽用長的名字將更有意義.
|
||||
|
||||
在習慣上, Go程序員推薦使用`駝峯式`命名, 當名字有幾個單詞的時優先使用大小寫分隔, 而不是優先用下劃綫分隔. 因此, 標準庫有 `QuoteRuneToASCII` 和 `parseRequestLine` 這樣的函數命名, 但是不會用 `quote_rune_to_ASCII` 和 `parse_request_line` 這樣的命名. 像 `ASCII` 和 `HTML` 這樣的縮略詞避免使用大小寫混閤, 它們可能被稱為 `htmlEscape`, `HTMLEscape` 或 `escapeHTML`, 但不會是 `escapeHtml`.
|
||||
在習慣上, Go程序員推薦使用`駝峯式`命名, 當名字有幾個單詞的時優先使用大小寫分隔, 而不是優先用下劃綫分隔. 因此, 標準庫有 `QuoteRuneToASCII` 和 `parseRequestLine` 這樣的函數命名, 但是不會用 `quote_rune_to_ASCII` 和 `parse_request_line` 這樣的命名. 像 `ASCII` 和 `HTML` 這樣的縮略詞避免使用大小寫混合, 它們可能被稱爲 `htmlEscape`, `HTMLEscape` 或 `escapeHTML`, 但不會是 `escapeHtml`.
|
||||
|
@ -1,9 +1,9 @@
|
||||
## 2.2. 聲明
|
||||
|
||||
聲明定義了程序的入口以及部分或全部的屬性. Go主要有四種聲明類型: var, const, type, 和 func, 分彆對應 變量, 常量, 類型, 和 函數的 聲明. 這一章我們重點討論變量和類型的聲明, 第三章將討論常量的聲明, 第五章將討論函數的聲明.
|
||||
聲明定義了程序的入口以及部分或全部的屬性. Go主要有四種聲明類型: var, const, type, 和 func, 分別對應 變量, 常量, 類型, 和 函數的 聲明. 這一章我們重點討論變量和類型的聲明, 第三章將討論常量的聲明, 第五章將討論函數的聲明.
|
||||
|
||||
一個Go程序存儲在一個或多個以`.go`為後綴名的文件中. 每個文件以個包的聲明開始, 以說明文件是屬於包的一部分.
|
||||
包聲明之後是 import 導入聲明, 然後是包一級的類型/變量/常量/函數的聲明, 聲明的順序無關緊要. 例如, 下麫的例子聲明了一個常量, 一個函數和兩個變量:
|
||||
一個Go程序存儲在一個或多個以`.go`爲後綴名的文件中. 每個文件以個包的聲明開始, 以說明文件是屬於包的一部分.
|
||||
包聲明之後是 import 導入聲明, 然後是包一級的類型/變量/常量/函數的聲明, 聲明的順序無關緊要. 例如, 下面的例子聲明了一個常量, 一個函數和兩個變量:
|
||||
|
||||
```Go
|
||||
gopl.io/ch2/boiling
|
||||
@ -25,9 +25,9 @@ func main() {
|
||||
|
||||
其中 常量 `boilingF` 是在包一級聲明的, 然後 `f` 和 `c` 是在 main 函數內部聲明的. 在包一級聲明的名字可在整個包訪問, 而不僅僅在其聲明的文件中訪問. 相比之下, 侷部聲明的名字就隻能在函數內部很小的部分可訪問.
|
||||
|
||||
一個函數的聲明有一個函數名字, 參數列錶(由函數的調用者提供參數變量的具體值), 一個可選的返迴值列錶, 和包含函數語句定義的函數體. 如果函數沒有返迴值, 那麼返迴值列錶是省略的. 執行函數從函數的第一個語句開始, 但是順序執行直到遇到 renturn 返迴語言, 如果沒有返迴語句則是到函數末尾, 然後返迴到調用者.
|
||||
一個函數的聲明有一個函數名字, 參數列表(由函數的調用者提供參數變量的具體值), 一個可選的返迴值列表, 和包含函數語句定義的函數體. 如果函數沒有返迴值, 那麽返迴值列表是省略的. 執行函數從函數的第一個語句開始, 但是順序執行直到遇到 renturn 返迴語言, 如果沒有返迴語句則是到函數末尾, 然後返迴到調用者.
|
||||
|
||||
我們已經看到過很多函數的例子了, 在第五章將深入討論函數的細節, 這裏隻粗略說下. 下麫的 `fToC` 函數封裝了溫度轉換的邏輯, 這樣它隻需要定義一次, 就可以在多個地方多次使用. 這個例子中, main 函數就調用了兩次 `fToC` 函數, 分彆是使用侷部定義的兩個常量作為函數參數.
|
||||
我們已經看到過很多函數的例子了, 在第五章將深入討論函數的細節, 這裡隻粗略說下. 下面的 `fToC` 函數封裝了溫度轉換的邏輯, 這樣它隻需要定義一次, 就可以在多個地方多次使用. 這個例子中, main 函數就調用了兩次 `fToC` 函數, 分別是使用侷部定義的兩個常量作爲函數參數.
|
||||
|
||||
|
||||
```Go
|
||||
|
@ -1,6 +1,6 @@
|
||||
### 2.3.1. 簡短變量聲明
|
||||
|
||||
在函數內部, 有一種稱為簡短變量聲明的形式可用於聲明和初始化侷部變量. 以 `名字 := 錶達式` 方式聲明變量, 變量的類型根據錶達式來推導. 這裏函數中是三個簡短變量聲明語句(§1.4):
|
||||
在函數內部, 有一種稱爲簡短變量聲明的形式可用於聲明和初始化侷部變量. 以 `名字 := 表達式` 方式聲明變量, 變量的類型根據表達式來推導. 這裡函數中是三個簡短變量聲明語句(§1.4):
|
||||
|
||||
```Go
|
||||
anim := gif.GIF{LoopCount: nframes}
|
||||
@ -8,7 +8,7 @@ freq := rand.Float64() * 3.0
|
||||
t := 0.0
|
||||
```
|
||||
|
||||
因為簡潔和靈活性, 簡短變量聲明用於大部分的侷部變量的聲明和初始化. var 方式的聲明往往是用於需要顯示指定類型的侷部變量, 或者因為稍後會被賦值而初始值無關緊要的變量.
|
||||
因爲簡潔和靈活性, 簡短變量聲明用於大部分的侷部變量的聲明和初始化. var 方式的聲明往往是用於需要顯示指定類型的侷部變量, 或者因爲稍後會被賦值而初始值無關緊要的變量.
|
||||
|
||||
|
||||
```Go
|
||||
@ -27,7 +27,7 @@ i, j := 0, 1
|
||||
|
||||
但是這種聲明多個變量的方式隻簡易在可以提高代碼可讀性的地方使用, 比如 for 循環的初始化部分.
|
||||
|
||||
請記住 `:=` 是一個變量聲明, 而 `=` 是一個賦值操作. 不要混淆多個變量的聲明和元組的多重(§2.4.1), 後者是將右邊的錶達式值賦給左邊對應位置的變量:
|
||||
請記住 `:=` 是一個變量聲明, 而 `=` 是一個賦值操作. 不要混淆多個變量的聲明和元組的多重(§2.4.1), 後者是將右邊的表達式值賦給左邊對應位置的變量:
|
||||
|
||||
```Go
|
||||
i, j = j, i // 交換 i 和 j 的值
|
||||
@ -44,9 +44,9 @@ if err != nil {
|
||||
f.Close()
|
||||
```
|
||||
|
||||
這裏有一個比較微妙的地方: 簡短變量聲明左邊的全部變量可能併不是全部都是剛剛聲明的. 如果有一些已經在相衕的詞法塊聲明過了(§2.7), 那麼簡短變量聲明對這些已經聲明過的變量就隻有賦值行為了.
|
||||
這裡有一個比較微妙的地方: 簡短變量聲明左邊的全部變量可能並不是全部都是剛剛聲明的. 如果有一些已經在相同的詞法塊聲明過了(§2.7), 那麽簡短變量聲明對這些已經聲明過的變量就隻有賦值行爲了.
|
||||
|
||||
在下麫的代碼中, 第一個語句聲明了 in 和 err 變量. 第二個語句隻聲明了 out, 然後對已經聲明的 err 進行賦值.
|
||||
在下面的代碼中, 第一個語句聲明了 in 和 err 變量. 第二個語句隻聲明了 out, 然後對已經聲明的 err 進行賦值.
|
||||
|
||||
```Go
|
||||
in, err := os.Open(infile)
|
||||
@ -64,6 +64,6 @@ f, err := os.Create(outfile) // compile error: no new variables
|
||||
|
||||
解決的方法是第二個語句改用普通的賦值語言.
|
||||
|
||||
簡短變量聲明隻有對在變量已經在衕級詞法域聲明過的變量纔和賦值操作等衕, 如果變量是在外部詞法域聲明了, 那麼將會聲明一個新變量. 我們在本章後麫將會看到類似的例子.
|
||||
簡短變量聲明隻有對在變量已經在同級詞法域聲明過的變量纔和賦值操作等同, 如果變量是在外部詞法域聲明了, 那麽將會聲明一個新變量. 我們在本章後面將會看到類似的例子.
|
||||
|
||||
|
||||
|
@ -1,10 +1,10 @@
|
||||
### 2.3.2 指鍼
|
||||
|
||||
一個變量對應一個保存了一個值的內存空間. 變量在聲明語句創建時綁定一個名字, 比如 x, 但是還有很多變量始終以錶達式方式引入, 例如 x[i] 或 x.f. 所有這些錶達式都讀取一個變量的值, 除非它們是齣現在賦值語句的左邊, 這種時候是給變量賦予一個新值.
|
||||
一個變量對應一個保存了一個值的內存空間. 變量在聲明語句創建時綁定一個名字, 比如 x, 但是還有很多變量始終以表達式方式引入, 例如 x[i] 或 x.f. 所有這些表達式都讀取一個變量的值, 除非它們是齣現在賦值語句的左邊, 這種時候是給變量賦予一個新值.
|
||||
|
||||
一個指鍼的值是一個變量的地址. 一個指鍼對應變量在內存中的存儲位置. 併不是每一個值都會有一個地址, 但是對於每一個變量必然有對應的地址. 通過指鍼, 我們可以直接讀或更新變量的值, 而不需要知道變量的名字(卽使變量有名字的話).
|
||||
一個指鍼的值是一個變量的地址. 一個指鍼對應變量在內存中的存儲位置. 並不是每一個值都會有一個地址, 但是對於每一個變量必然有對應的地址. 通過指鍼, 我們可以直接讀或更新變量的值, 而不需要知道變量的名字(卽使變量有名字的話).
|
||||
|
||||
如果這樣聲明一個變量 `var x int`, 那麼 `&x` 錶達式(x的地址)將產生一個指曏整數變量的指鍼, 對應的數據類型是 `*int`, 稱之為 "指曏 int 的指鍼". 如果指鍼名字為 p, 那麼可以說 "p 指鍼指曏 x", 或者說 "p 指鍼保存了 x 變量的地址". `*p` 對應 p 指鍼指曏的變量的值. `*p` 錶達式讀取變量的值, 為 int 類型, 衕時因為 `*p` 對應一個變量, 所以可以齣現在賦值語句的左邊, 用於更新所指曏的變量的值.
|
||||
如果這樣聲明一個變量 `var x int`, 那麽 `&x` 表達式(x的地址)將產生一個指向整數變量的指鍼, 對應的數據類型是 `*int`, 稱之爲 "指向 int 的指鍼". 如果指鍼名字爲 p, 那麽可以說 "p 指鍼指向 x", 或者說 "p 指鍼保存了 x 變量的地址". `*p` 對應 p 指鍼指向的變量的值. `*p` 表達式讀取變量的值, 爲 int 類型, 同時因爲 `*p` 對應一個變量, 所以可以齣現在賦值語句的左邊, 用於更新所指向的變量的值.
|
||||
|
||||
```Go
|
||||
x := 1
|
||||
@ -14,18 +14,18 @@ fmt.Println(*p) // "1"
|
||||
fmt.Println(x) // "2"
|
||||
```
|
||||
|
||||
對於聚閤類型, 比如結構體的每個字段, 或者是數組的每個元素, 也都是對應一個變量, 併且可以被穫取地址.
|
||||
對於聚合類型, 比如結構體的每個字段, 或者是數組的每個元素, 也都是對應一個變量, 並且可以被獲取地址.
|
||||
|
||||
變量有時候被稱為可尋址的值. 如果變量由錶達式臨時生成, 那麼錶達式必鬚能接受 `&` 取地址操作.
|
||||
變量有時候被稱爲可尋址的值. 如果變量由表達式臨時生成, 那麽表達式必鬚能接受 `&` 取地址操作.
|
||||
|
||||
任何類型的指鍼的零值都是 nil. 如果 `p != nil` 測試為眞, 那麼 p 是指曏變量. 指鍼直接也是可以進行相等測試的, 隻有當它們指曏衕一個變量或全部是 nil 時纔相等.
|
||||
任何類型的指鍼的零值都是 nil. 如果 `p != nil` 測試爲眞, 那麽 p 是指向變量. 指鍼直接也是可以進行相等測試的, 隻有當它們指向同一個變量或全部是 nil 時纔相等.
|
||||
|
||||
```Go
|
||||
var x, y int
|
||||
fmt.Println(&x == &x, &x == &y, &x == nil) // "true false false"
|
||||
```
|
||||
|
||||
在Go語言中, 返迴函數中侷部變量的地址是安全的. 例如下麫的代碼, 調用 f 函數時創建 v 侷部變量, 在地址被返迴之後依然有效, 因為指鍼 p 依然引用這個變量.
|
||||
在Go語言中, 返迴函數中侷部變量的地址是安全的. 例如下面的代碼, 調用 f 函數時創建 v 侷部變量, 在地址被返迴之後依然有效, 因爲指鍼 p 依然引用這個變量.
|
||||
|
||||
```Go
|
||||
var p = f()
|
||||
@ -36,13 +36,13 @@ func f() *int {
|
||||
}
|
||||
```
|
||||
|
||||
每次調用 f 函數都將返迴不衕的結果:
|
||||
每次調用 f 函數都將返迴不同的結果:
|
||||
|
||||
```Go
|
||||
fmt.Println(f() == f()) // "false"
|
||||
```
|
||||
|
||||
因為指鍼包含了一個變量的地址, 因此將指鍼作為參數調用函數, 將可以在函數中通過指鍼更新變量的值. 例如這個通過指鍼來更新變量的值, 然後返迴更新後的值, 可用在一個錶達式中:
|
||||
因爲指鍼包含了一個變量的地址, 因此將指鍼作爲參數調用函數, 將可以在函數中通過指鍼更新變量的值. 例如這個通過指鍼來更新變量的值, 然後返迴更新後的值, 可用在一個表達式中:
|
||||
|
||||
```Go
|
||||
func incr(p *int) int {
|
||||
@ -55,9 +55,9 @@ incr(&v) // side effect: v is now 2
|
||||
fmt.Println(incr(&v)) // "3" (and v is 3)
|
||||
```
|
||||
|
||||
每次我們對變量取地址, 或者復製指鍼, 我們都創建了變量的新的彆名. 例如, *p 是 變量 v 的彆名. 指鍼特彆有加載的地方在於我們可以不用名字而訪問一個變量, 但是這是一把雙刃劍: 要找到一個變量的所有訪問者, 我們必鬚知道變量全部的彆名. 不僅僅是指鍼創建彆名, 很多其他引用類型也會創建彆名, 例如 切片, 字典和管道, 甚至結構體, 數組和接口都會創建所引用變量的彆名.
|
||||
每次我們對變量取地址, 或者復製指鍼, 我們都創建了變量的新的別名. 例如, *p 是 變量 v 的別名. 指鍼特別有加載的地方在於我們可以不用名字而訪問一個變量, 但是這是一把雙刃劍: 要找到一個變量的所有訪問者, 我們必鬚知道變量全部的別名. 不僅僅是指鍼創建別名, 很多其他引用類型也會創建別名, 例如 切片, 字典和管道, 甚至結構體, 數組和接口都會創建所引用變量的別名.
|
||||
|
||||
指鍼是 flag 包的關鍵, 它使用命令行參數來設置對應的變量, 而這些分佈在整個程序中. 為了說明這一點, 在早些的echo版本中, 包含了兩個可選的命令行參數: `-n` 用於忽略行尾的換行符, `-s sep` 用於指定分隔字符(默認是空格). 這是第四個版本, 對應包 gopl.io/ch2/echo4.
|
||||
指鍼是 flag 包的關鍵, 它使用命令行參數來設置對應的變量, 而這些分佈在整個程序中. 爲了說明這一點, 在早些的echo版本中, 包含了兩個可選的命令行參數: `-n` 用於忽略行尾的換行符, `-s sep` 用於指定分隔字符(默認是空格). 這是第四個版本, 對應包 gopl.io/ch2/echo4.
|
||||
|
||||
```Go
|
||||
gopl.io/ch2/echo4
|
||||
@ -82,7 +82,7 @@ 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() 訪問, 對應一個 字符串切片. 如果 flag.Parse 解析遇到錯誤, 將打印提示信息, 然後調用 os.Exit(2) 終止程序.
|
||||
|
@ -1,19 +1,19 @@
|
||||
### 2.3.3 new 函數
|
||||
|
||||
|
||||
另一個創建變量的方法是用內建的 new 函數. 錶達式 `new(T)` 創建一個T類型的匿名變量, 初始化為T類型的零值, 返迴返迴變量地址, 返迴指鍼類型為 `*T`.
|
||||
另一個創建變量的方法是用內建的 new 函數. 表達式 `new(T)` 創建一個T類型的匿名變量, 初始化爲T類型的零值, 返迴返迴變量地址, 返迴指鍼類型爲 `*T`.
|
||||
|
||||
```Go
|
||||
p := new(int) // p, *int 類型, 指曏匿名的 int 變量
|
||||
p := new(int) // p, *int 類型, 指向匿名的 int 變量
|
||||
fmt.Println(*p) // "0"
|
||||
*p = 2 // 設置 int 匿名變量的值為 2
|
||||
*p = 2 // 設置 int 匿名變量的值爲 2
|
||||
fmt.Println(*p) // "2"
|
||||
```
|
||||
|
||||
|
||||
從 new 創建變量和普通聲明方式創建變量沒有什麼區彆, 除了不需要聲明一個臨時變量的名字外, 我們還可以在錶達式中使用 `new(T)`. 換言之, new 類似是一種語法醣, 而不是一個新的基礎概唸.
|
||||
從 new 創建變量和普通聲明方式創建變量沒有什麽區別, 除了不需要聲明一個臨時變量的名字外, 我們還可以在表達式中使用 `new(T)`. 換言之, new 類似是一種語法醣, 而不是一個新的基礎概唸.
|
||||
|
||||
下麫的兩個 newInt 函數有着相衕的行為:
|
||||
下面的兩個 newInt 函數有着相同的行爲:
|
||||
|
||||
```Go
|
||||
func newInt() *int { func newInt() *int {
|
||||
@ -22,7 +22,7 @@ func newInt() *int { func newInt() *int {
|
||||
}
|
||||
```
|
||||
|
||||
每次調用 new 都是返迴一個新的變量的地址, 因此下麫兩個地址是不衕的:
|
||||
每次調用 new 都是返迴一個新的變量的地址, 因此下面兩個地址是不同的:
|
||||
|
||||
```Go
|
||||
p := new(int)
|
||||
@ -30,15 +30,15 @@ q := new(int)
|
||||
fmt.Println(p == q) // "false"
|
||||
```
|
||||
|
||||
當然也有特殊情況: 如果兩個類型都是空的, 也就是說類型的大小是0, 例如 `struct{}` 和 `[0]int`, 有可能有相衕的地址(依賴具體的語言實現).
|
||||
當然也有特殊情況: 如果兩個類型都是空的, 也就是說類型的大小是0, 例如 `struct{}` 和 `[0]int`, 有可能有相同的地址(依賴具體的語言實現).
|
||||
|
||||
new 函數使用相對比較少, 因為對應結構體來說, 可以直接用字麫量語法創建新變量的方法更靈活 (§4.4.1).
|
||||
new 函數使用相對比較少, 因爲對應結構體來說, 可以直接用字面量語法創建新變量的方法更靈活 (§4.4.1).
|
||||
|
||||
由於 new 隻是一個預定義的函數, 它併不是一個關鍵字, 因此我們可以將 new 重新定義為彆的類型. 例如:
|
||||
由於 new 隻是一個預定義的函數, 它並不是一個關鍵字, 因此我們可以將 new 重新定義爲別的類型. 例如:
|
||||
|
||||
```Go
|
||||
func delta(old, new int) int { return new - old }
|
||||
```
|
||||
|
||||
因為 new 被定義為 int 類型的變量, 因此 delta 函數內部就無法在使用內置的 new 函數了.
|
||||
因爲 new 被定義爲 int 類型的變量, 因此 delta 函數內部就無法在使用內置的 new 函數了.
|
||||
|
||||
|
@ -1,8 +1,8 @@
|
||||
### 2.3.4. 變量的生命週期
|
||||
|
||||
變量的生命週期指的是程序運行期間變量存在的有效時間間隔. 包級聲明的變量的生命週期和程序的生命週期是一緻的. 相比之下, 侷部變量的聲明週期是動態的: 從每次創建一個新變量的聲明語句被執行開始, 直到變量不在被引用為止, 然後變量的存儲空間可能被迴收. 函數的參數變量和返迴值變量都是侷部變量. 它們在函數每次被調用的時候創建.
|
||||
變量的生命週期指的是程序運行期間變量存在的有效時間間隔. 包級聲明的變量的生命週期和程序的生命週期是一緻的. 相比之下, 侷部變量的聲明週期是動態的: 從每次創建一個新變量的聲明語句被執行開始, 直到變量不在被引用爲止, 然後變量的存儲空間可能被迴收. 函數的參數變量和返迴值變量都是侷部變量. 它們在函數每次被調用的時候創建.
|
||||
|
||||
例如, 下麫是從 1.4 節的 Lissajous 程序摘彔的代碼片段:
|
||||
例如, 下面是從 1.4 節的 Lissajous 程序摘録的代碼片段:
|
||||
|
||||
```Go
|
||||
for t := 0.0; t < cycles*2*math.Pi; t += res {
|
||||
@ -15,11 +15,11 @@ for t := 0.0; t < cycles*2*math.Pi; t += res {
|
||||
|
||||
在每次循環的開始創建變量 t, 然後在每次循環迭代中創建 x 和 y.
|
||||
|
||||
那麼垃圾收集器是如何知道一個變量是何時可以被迴收的呢? 這裏我們先避開完整的技朮細節, 但是基本的思路是, 從每個包級的變量和每個當前運行函數的每一個侷部變量開始, 通過指鍼或引用的路徑, 是否可以找到該變量. 如果不存在這樣的路徑, 那麼說明該變量是不可達的, 也就是說它併不會影響其餘的計算.
|
||||
那麽垃圾收集器是如何知道一個變量是何時可以被迴收的呢? 這裡我們先避開完整的技術細節, 但是基本的思路是, 從每個包級的變量和每個當前運行函數的每一個侷部變量開始, 通過指鍼或引用的路徑, 是否可以找到該變量. 如果不存在這樣的路徑, 那麽說明該變量是不可達的, 也就是說它並不會影響其餘的計算.
|
||||
|
||||
因為一個變量的聲明週期隻取決於是否可達, 因此一個循環迭代內部的侷部變量的生命週期可能超齣其侷部作用域. 它可能在函數返迴之後依然存在.
|
||||
因爲一個變量的聲明週期隻取決於是否可達, 因此一個循環迭代內部的侷部變量的生命週期可能超齣其侷部作用域. 它可能在函數返迴之後依然存在.
|
||||
|
||||
編譯器會選擇在棧上還是在堆上分配侷部變量的存儲空間, 但可能令人驚訝的是, 這個選擇併不是由 var 或 new 來決定的.
|
||||
編譯器會選擇在棧上還是在堆上分配侷部變量的存儲空間, 但可能令人驚訝的是, 這個選擇並不是由 var 或 new 來決定的.
|
||||
|
||||
```Go
|
||||
var global *int
|
||||
@ -31,10 +31,10 @@ func f() { func g() {
|
||||
}
|
||||
```
|
||||
|
||||
這裏的 x 必鬚在堆上分配, 因為它在函數退齣後依然可以通過包的 global 變量找到, 雖然它是在函數內部定義的; 我們說這個 x 侷部變量從 函數 f 中逃逸了. 相反, 當 g 函數返迴時, 變量 `*y` 將是不可達的, 也就是可以被迴收的. 因此, `*y` 併沒有從 函數 g 逃逸, 編譯器可以選擇在棧上分配 `*y` 的存儲空間, 雖然這裏用的是 new 方式.
|
||||
在任何時候, 你併不需為了編寫正確的代碼而要考慮變量的逃逸行為, 要記住的是, 逃逸的變量需要額外分配內存, 衕時對性能的優化會產生一定的影響.
|
||||
這裡的 x 必鬚在堆上分配, 因爲它在函數退齣後依然可以通過包的 global 變量找到, 雖然它是在函數內部定義的; 我們說這個 x 侷部變量從 函數 f 中逃逸了. 相反, 當 g 函數返迴時, 變量 `*y` 將是不可達的, 也就是可以被迴收的. 因此, `*y` 並沒有從 函數 g 逃逸, 編譯器可以選擇在棧上分配 `*y` 的存儲空間, 雖然這裡用的是 new 方式.
|
||||
在任何時候, 你並不需爲了編寫正確的代碼而要考慮變量的逃逸行爲, 要記住的是, 逃逸的變量需要額外分配內存, 同時對性能的優化會產生一定的影響.
|
||||
|
||||
垃圾收集器對編寫正確的代碼是一個鉅大的幫助, 但併不是說你完全不用考慮內存了. 你雖然不需要顯式地分配和釋放內存, 但是要編寫高效的程序你還是需要知道變量的生命週期. 例如, 將指曏短生命週期對象的指鍼保存到具有長生命週期的對象中, 特彆是全侷變量時, 會阻止對短生命週期對象的垃圾迴收.
|
||||
垃圾收集器對編寫正確的代碼是一個鉅大的幫助, 但並不是說你完全不用考慮內存了. 你雖然不需要顯式地分配和釋放內存, 但是要編寫高效的程序你還是需要知道變量的生命週期. 例如, 將指向短生命週期對象的指鍼保存到具有長生命週期的對象中, 特別是全侷變量時, 會阻止對短生命週期對象的垃圾迴收.
|
||||
|
||||
|
||||
|
||||
|
@ -1,31 +1,31 @@
|
||||
## 2.3. 變量
|
||||
|
||||
var 聲明可以創建一個特定類型的變量, 然後給變量附加一個名字, 併且設置變量的初始值. 變量聲明的一般語法:
|
||||
var 聲明可以創建一個特定類型的變量, 然後給變量附加一個名字, 並且設置變量的初始值. 變量聲明的一般語法:
|
||||
|
||||
```Go
|
||||
var name type = 錶達式
|
||||
var name type = 表達式
|
||||
```
|
||||
|
||||
其中類型或 `= 錶達式` 可以省略其中的一個. 如果省略的是類型信息, 那麼將根據初始化錶達式類推導類型信息. 如果初始化錶達式被省略, 那麼將用零值初始化變量. 數值類型變量的零值是0, 佈爾類型變量的零值是 false, 字符串的零值是空字符串, 接口或引用類型(包括 切片, 字典, 通道 和 函數)的變量的零值是 nil. 數組或結構體等聚閤類型的零值是每個元素或字段都是零值.
|
||||
其中類型或 `= 表達式` 可以省略其中的一個. 如果省略的是類型信息, 那麽將根據初始化表達式類推導類型信息. 如果初始化表達式被省略, 那麽將用零值初始化變量. 數值類型變量的零值是0, 佈爾類型變量的零值是 false, 字符串的零值是空字符串, 接口或引用類型(包括 切片, 字典, 通道 和 函數)的變量的零值是 nil. 數組或結構體等聚合類型的零值是每個元素或字段都是零值.
|
||||
|
||||
零值機製可以確保每個聲明的變量總是有一個良好定義的值, 在 Go 中不存在未初始化的變量. 這個可以簡化很多代碼, 在沒有增加額外工作的前提下確保邊界條件下的閤理行為. 例如:
|
||||
零值機製可以確保每個聲明的變量總是有一個良好定義的值, 在 Go 中不存在未初始化的變量. 這個可以簡化很多代碼, 在沒有增加額外工作的前提下確保邊界條件下的合理行爲. 例如:
|
||||
|
||||
```Go
|
||||
var s string
|
||||
fmt.Println(s) // ""
|
||||
```
|
||||
|
||||
這段代碼將打印一個空字符串, 而不是導緻錯誤或產生不可預知的行為. Go 程序員經常讓一些聚閤類型的零值也有意義, 這樣不管任何類型的變量總是有一個閤理的零值狀態.
|
||||
這段代碼將打印一個空字符串, 而不是導緻錯誤或產生不可預知的行爲. Go 程序員經常讓一些聚合類型的零值也有意義, 這樣不管任何類型的變量總是有一個合理的零值狀態.
|
||||
|
||||
可以在一個聲明語句中衕時聲明一組變量, 或用一組初始化錶達式聲明併初始化一組變量.
|
||||
如果省略每個變量的類型, 將可以聲明多個不衕類型的變量(類型由初始化錶達式推導):
|
||||
可以在一個聲明語句中同時聲明一組變量, 或用一組初始化表達式聲明並初始化一組變量.
|
||||
如果省略每個變量的類型, 將可以聲明多個不同類型的變量(類型由初始化表達式推導):
|
||||
|
||||
```Go
|
||||
var i, j, k int // int, int, int
|
||||
var b, f, s = true, 2.3, "four" // bool, float64, string
|
||||
```
|
||||
|
||||
初始化可以是字麫量或任意的錶達式. 包級彆聲明的變量會在 main 函數執行前完成初始化 (§2.6.2), 侷部變量將在聲明語句被執行到的時候初始化.
|
||||
初始化可以是字面量或任意的表達式. 包級別聲明的變量會在 main 函數執行前完成初始化 (§2.6.2), 侷部變量將在聲明語句被執行到的時候初始化.
|
||||
|
||||
一組變量的初始化也可以通過調用一個函數, 由函數返迴的多個返迴值初始化:
|
||||
|
||||
|
@ -1,6 +1,6 @@
|
||||
### 2.4.1. 元組賦值
|
||||
|
||||
元組賦值是另一種形式的賦值語句, 允許衕時更新多個變量的值. 在賦值之前, 賦值語句右邊的所有錶達式將會先進行求值, 然後再統一更新左邊變量的值. 這對於處理有些衕時齣現在元組賦值語句左右兩邊的變量很有幫助, 例如我們可以這樣交換兩個變量的值:
|
||||
元組賦值是另一種形式的賦值語句, 允許同時更新多個變量的值. 在賦值之前, 賦值語句右邊的所有表達式將會先進行求值, 然後再統一更新左邊變量的值. 這對於處理有些同時齣現在元組賦值語句左右兩邊的變量很有幫助, 例如我們可以這樣交換兩個變量的值:
|
||||
|
||||
```Go
|
||||
x, y = y, x
|
||||
@ -31,22 +31,22 @@ func fib(n int) int {
|
||||
}
|
||||
```
|
||||
|
||||
元組賦值也可以使一繫列瑣碎賦值更緊湊(譯註: 特彆是在for循環的初始化部分),
|
||||
元組賦值也可以使一繫列瑣碎賦值更緊湊(譯註: 特別是在for循環的初始化部分),
|
||||
|
||||
```Go
|
||||
i, j, k = 2, 3, 5
|
||||
```
|
||||
|
||||
但如果錶達式太復雜的話, 應該盡量避免元組賦值; 因為一個個單獨的賦值語句的可讀性會更好.
|
||||
但如果表達式太復雜的話, 應該儘量避免元組賦值; 因爲一個個單獨的賦值語句的可讀性會更好.
|
||||
|
||||
某些錶達式會產生多個值, 比如調用一個有多個返迴值的函數.
|
||||
當這樣一個函數調用齣現在元組賦值右邊的錶達式中時(譯註: 右邊不能再有其他錶達式), 左邊變量的數目必鬚和右邊一緻.
|
||||
某些表達式會產生多個值, 比如調用一個有多個返迴值的函數.
|
||||
當這樣一個函數調用齣現在元組賦值右邊的表達式中時(譯註: 右邊不能再有其他表達式), 左邊變量的數目必鬚和右邊一緻.
|
||||
|
||||
```Go
|
||||
f, err = os.Open("foo.txt") // function call returns two values
|
||||
```
|
||||
|
||||
通常, 這類函數會用額外的返迴值錶達某種錯誤類型, 例如 os.Open 是返迴一個 error 類型的錯誤, 還有一些是返迴佈爾值, 通常被稱為ok. 在稍後我們看到的三個操作都是類似的行為. 如果 字典査找(§4.3), 類型斷言(§7.10), 或 通道接收(§8.4.2) 齣現在賦值語句的右邊, 它們都將產生兩個結果, 有一個額外的佈爾結果錶示操作是否成功:
|
||||
通常, 這類函數會用額外的返迴值表達某種錯誤類型, 例如 os.Open 是返迴一個 error 類型的錯誤, 還有一些是返迴佈爾值, 通常被稱爲ok. 在稍後我們看到的三個操作都是類似的行爲. 如果 字典査找(§4.3), 類型斷言(§7.10), 或 通道接收(§8.4.2) 齣現在賦值語句的右邊, 它們都將產生兩個結果, 有一個額外的佈爾結果表示操作是否成功:
|
||||
|
||||
```Go
|
||||
v, ok = m[key] // map lookup
|
||||
|
@ -1,12 +1,12 @@
|
||||
### 2.4.2. 可賦值性
|
||||
|
||||
賦值語句是顯示的賦值形式, 但是程序中還有很多地方會髮送隱式的賦值行為: 函數調用將隱式地將調用參數的值賦值給函數的參數變量, 一個返迴語句將隱式地將返迴操作的值賦值給結果變量, 一個復閤類型的字麫量(§4.2)也會產生賦值行為. 例如下麫的語句:
|
||||
賦值語句是顯示的賦值形式, 但是程序中還有很多地方會發送隱式的賦值行爲: 函數調用將隱式地將調用參數的值賦值給函數的參數變量, 一個返迴語句將隱式地將返迴操作的值賦值給結果變量, 一個復合類型的字面量(§4.2)也會產生賦值行爲. 例如下面的語句:
|
||||
|
||||
```Go
|
||||
medals := []string{"gold", "silver", "bronze"}
|
||||
```
|
||||
|
||||
隱式地對切片的每個元素進行賦值操作, 類似這樣寫的行為:
|
||||
隱式地對切片的每個元素進行賦值操作, 類似這樣寫的行爲:
|
||||
|
||||
```Go
|
||||
medals[0] = "gold"
|
||||
@ -14,15 +14,15 @@ medals[1] = "silver"
|
||||
medals[2] = "bronze"
|
||||
```
|
||||
|
||||
字典和管道的元素, 雖然不是普通的變量, 但是也有類似的隱式賦值行為.
|
||||
字典和管道的元素, 雖然不是普通的變量, 但是也有類似的隱式賦值行爲.
|
||||
|
||||
不管是隱式還是顯示地賦值, 在賦值語句坐標的變量和右邊最終的求到的值必鬚有相衕的數據類型. 更直白地說, 隻有右邊的值對於左邊的變量是可賦值的, 賦值語句纔是允許的.
|
||||
不管是隱式還是顯示地賦值, 在賦值語句坐標的變量和右邊最終的求到的值必鬚有相同的數據類型. 更直白地說, 隻有右邊的值對於左邊的變量是可賦值的, 賦值語句纔是允許的.
|
||||
|
||||
可賦值性的規則對於不衕類型有不衕要求, 對每個新類型有關的地方我們會專門解釋.
|
||||
可賦值性的規則對於不同類型有不同要求, 對每個新類型有關的地方我們會專門解釋.
|
||||
對於目前我們已經討論過的類型, 它的規則是簡單的: 類型必鬚完全匹配, nil 可以賦值給任何指鍼或引用類型的變量. 常量(§3.6)有更靈活的規則, 這樣可以避免不必要的顯示類型轉換.
|
||||
|
||||
對於兩個值是否可以用 `==` 或 `!=` 進行相等比較的能力也和可賦值能力有關繫:
|
||||
對於任何的比較, 第一個操作必鬚是可用於第二個操作類型的變量的賦值的, 反之依然.
|
||||
和前麫一樣, 我們會對每個新類型比較有關的地方會做專門解釋.
|
||||
和前面一樣, 我們會對每個新類型比較有關的地方會做專門解釋.
|
||||
|
||||
|
||||
|
@ -1,6 +1,6 @@
|
||||
## 2.4. 賦值
|
||||
|
||||
使用賦值語句可以更新一個變量的值, 最簡單的賦值語句是將要被賦值的變量放在 `=` 的左邊, 新值的錶達式放在 `=` 右邊.
|
||||
使用賦值語句可以更新一個變量的值, 最簡單的賦值語句是將要被賦值的變量放在 `=` 的左邊, 新值的表達式放在 `=` 右邊.
|
||||
|
||||
```Go
|
||||
x = 1 // 命令變量的賦值
|
||||
@ -9,13 +9,13 @@ person.name = "bob" // 結構體字段賦值
|
||||
count[x] = count[x] * scale // 數組, 切片 或 字典的 元素賦值
|
||||
```
|
||||
|
||||
特定的賦值語句和二元算朮復閤操作有一個簡潔形式, 例如上麫最後的語句可以重寫為:
|
||||
特定的賦值語句和二元算術復合操作有一個簡潔形式, 例如上面最後的語句可以重寫爲:
|
||||
|
||||
```Go
|
||||
count[x] *= scale
|
||||
```
|
||||
|
||||
這樣可以省去對變量錶達式的重復計算.
|
||||
這樣可以省去對變量表達式的重復計算.
|
||||
|
||||
數值變量也可以支持 `++` 遞增和 `--` 遞減語句:
|
||||
|
||||
|
@ -1,23 +1,23 @@
|
||||
## 2.5. 類型聲明
|
||||
|
||||
變量或錶達式的類型定義了對應存儲值的特徵, 例如數值的存儲大小(或者是元素的bit個數), 它們在內部是如何錶達的, 是否支持一些操作符, 以及它們自己關聯的方法集,
|
||||
變量或表達式的類型定義了對應存儲值的特徵, 例如數值的存儲大小(或者是元素的bit個數), 它們在內部是如何表達的, 是否支持一些操作符, 以及它們自己關聯的方法集,
|
||||
|
||||
在任何程序中都會有一些變量有着相衕的內部實現, 但是錶示完全不衕的概唸.
|
||||
例如, int 類型的變量可以用來錶示一個循環的迭代索引, 或者一個時間戳, 或者一個文件描述符, 或者一個月份; 一個 float64 類型的變量可以用來錶示每秒幾米的速度, 或者是不衕溫度單位的溫度;
|
||||
一個字符串可以用來錶示一個密碼或者一個顔色的名稱.
|
||||
在任何程序中都會有一些變量有着相同的內部實現, 但是表示完全不同的概唸.
|
||||
例如, int 類型的變量可以用來表示一個循環的迭代索引, 或者一個時間戳, 或者一個文件描述符, 或者一個月份; 一個 float64 類型的變量可以用來表示每秒幾米的速度, 或者是不同溫度單位的溫度;
|
||||
一個字符串可以用來表示一個密碼或者一個顏色的名稱.
|
||||
|
||||
一個類型的聲明創建了一個新的類型名稱, 和現有類型具有相衕的底層結構.
|
||||
新命名的類型提供了一個方法, 用來分隔不衕概唸的類型, 卽使它們底層類型相衕也是不兼容的.
|
||||
一個類型的聲明創建了一個新的類型名稱, 和現有類型具有相同的底層結構.
|
||||
新命名的類型提供了一個方法, 用來分隔不同概唸的類型, 卽使它們底層類型相同也是不兼容的.
|
||||
|
||||
```Go
|
||||
type name underlying-type
|
||||
```
|
||||
|
||||
類型的聲明一般齣現在包級彆, 因此如果新創建的類型名字名字的首字符大寫, 則在外部包也可以使用.
|
||||
類型的聲明一般齣現在包級別, 因此如果新創建的類型名字名字的首字符大寫, 則在外部包也可以使用.
|
||||
|
||||
為了說明類型聲明, 我們將不衕溫度單位分彆定義為不衕的類型:
|
||||
爲了說明類型聲明, 我們將不同溫度單位分別定義爲不同的類型:
|
||||
|
||||
為了說明類型聲明,讓我們把不衕溫度範圍分為不衕的類型:
|
||||
爲了說明類型聲明,讓我們把不同溫度範圍分爲不同的類型:
|
||||
|
||||
```Go
|
||||
gopl.io/ch2/tempconv0
|
||||
@ -30,7 +30,7 @@ type Celsius float64 // 攝氏溫度
|
||||
type Fahrenheit float64 // 華氏溫度
|
||||
|
||||
const (
|
||||
AbsoluteZeroC Celsius = -273.15 // 絶對零度
|
||||
AbsoluteZeroC Celsius = -273.15 // 絕對零度
|
||||
FreezingC Celsius = 0 // 結冰點溫度
|
||||
BoilingC Celsius = 100 // 沸水問題
|
||||
)
|
||||
@ -40,16 +40,16 @@ func CToF(c Celsius) Fahrenheit { return Fahrenheit(c*9/5 + 32) }
|
||||
func FToC(f Fahrenheit) Celsius { return Celsius((f - 32) * 5 / 9) }
|
||||
```
|
||||
|
||||
這個包定義了兩種類型, Celsius 和 Fahrenheit 分彆對應不衕的溫度單位. 它們都有着相衕的底層類型 float64, 但是它們是不衕的數據類型, 因此它們不可以被相互比較或混在一個錶達式計算. 可以區分類型, 可以避免一些像無意中結閤單位的溫度進行計算的錯誤; 因為需要一個類似 Celsius(t) 或 Fahrenheit(t) 顯式的轉型操作纔能將 float64 轉為對應的類型. Celsius(t) 和 Fahrenheit(t) 是類型轉換操作, 併不是函數調用. 類型轉換不會改變值本身, 但是會使它們的語義髮生變化. 另一方麫, 函數 CToF 和 FToC 則是對兩個不衕的溫度單位進行轉換, 它們會返迴不衕的值.
|
||||
這個包定義了兩種類型, Celsius 和 Fahrenheit 分別對應不同的溫度單位. 它們都有着相同的底層類型 float64, 但是它們是不同的數據類型, 因此它們不可以被相互比較或混在一個表達式計算. 可以區分類型, 可以避免一些像無意中結合單位的溫度進行計算的錯誤; 因爲需要一個類似 Celsius(t) 或 Fahrenheit(t) 顯式的轉型操作纔能將 float64 轉爲對應的類型. Celsius(t) 和 Fahrenheit(t) 是類型轉換操作, 並不是函數調用. 類型轉換不會改變值本身, 但是會使它們的語義發生變化. 另一方面, 函數 CToF 和 FToC 則是對兩個不同的溫度單位進行轉換, 它們會返迴不同的值.
|
||||
|
||||
對於每一個類型 T, 都有一個對應的類型轉換操作 T(x), 用於將 x 轉為 T 類型.
|
||||
隻有當兩個類型的底層基礎類型相衕時, 纔允許這種轉型操作, 或者是兩者都是指曏相衕底層結構的指鍼類型,
|
||||
這些轉換隻改變類型而不會影響值本身. 如果x是可以賦值給T類型的, 那麼x必然可以被轉為T類型, 但是一般沒有必要.
|
||||
對於每一個類型 T, 都有一個對應的類型轉換操作 T(x), 用於將 x 轉爲 T 類型.
|
||||
隻有當兩個類型的底層基礎類型相同時, 纔允許這種轉型操作, 或者是兩者都是指向相同底層結構的指鍼類型,
|
||||
這些轉換隻改變類型而不會影響值本身. 如果x是可以賦值給T類型的, 那麽x必然可以被轉爲T類型, 但是一般沒有必要.
|
||||
|
||||
數值類型之間的轉型也是允許的, 併且在字符串和一些特定切片之間也是可以轉換的, 在下一章我們會看到這樣的例子. 這類轉換可能改變值的錶現. 例如, 將一個浮點數轉為整數將丟棄小數部分, 將一個字符串轉為 []byte 切片將拷貝一個字符串數據的副本. 在任何情況下, 運行時不會髮送轉換失敗的錯誤(譯註: 錯誤隻會髮生在編譯階段).
|
||||
數值類型之間的轉型也是允許的, 並且在字符串和一些特定切片之間也是可以轉換的, 在下一章我們會看到這樣的例子. 這類轉換可能改變值的表現. 例如, 將一個浮點數轉爲整數將丟棄小數部分, 將一個字符串轉爲 []byte 切片將拷貝一個字符串數據的副本. 在任何情況下, 運行時不會發送轉換失敗的錯誤(譯註: 錯誤隻會發生在編譯階段).
|
||||
|
||||
底層數據類型決定了內部結構和錶達方式, 也包決定是否可以像底層類型一樣對內置運算符的支持.
|
||||
這意味着, Celsius 和 Fahrenheit 類型的算朮行為和底層的 float64 類型一樣, 正如你所期望的.
|
||||
底層數據類型決定了內部結構和表達方式, 也包決定是否可以像底層類型一樣對內置運算符的支持.
|
||||
這意味着, Celsius 和 Fahrenheit 類型的算術行爲和底層的 float64 類型一樣, 正如你所期望的.
|
||||
|
||||
```Go
|
||||
fmt.Printf("%g\n", BoilingC-FreezingC) // "100" °C
|
||||
@ -58,8 +58,8 @@ fmt.Printf("%g\n", boilingF-CToF(FreezingC)) // "180" °F
|
||||
fmt.Printf("%g\n", boilingF-FreezingC) // compile error: type mismatch
|
||||
```
|
||||
|
||||
比較運算符 `==` 和 `<` 也可以用來比較一個命名類型的變量和另一個有相衕類型的變量或相衕的底層類型的值做比較.
|
||||
但是如果兩個值有着不衕的類型, 則不能直接進行比較:
|
||||
比較運算符 `==` 和 `<` 也可以用來比較一個命名類型的變量和另一個有相同類型的變量或相同的底層類型的值做比較.
|
||||
但是如果兩個值有着不同的類型, 則不能直接進行比較:
|
||||
|
||||
```Go
|
||||
var c Celsius
|
||||
@ -70,19 +70,19 @@ fmt.Println(c == f) // compile error: type mismatch
|
||||
fmt.Println(c == Celsius(f)) // "true"!
|
||||
```
|
||||
|
||||
註意最後那個語句. 盡管看起來想函數調用, 但是Celsius(f)類型轉換, 併不會改變值, 它僅僅是改變值的類型而已. 測試為眞的原因是因為 c 和 g 都是零值.
|
||||
註意最後那個語句. 儘管看起來想函數調用, 但是Celsius(f)類型轉換, 並不會改變值, 它僅僅是改變值的類型而已. 測試爲眞的原因是因爲 c 和 g 都是零值.
|
||||
|
||||
一個命名的類型可以提供符號方便, 特彆是可以避免一遍又一遍地書寫復雜類型(譯註: 例如用匿名的結構體定義變量). 雖然對於像float64這種簡單的底層類型沒有簡潔很多, 但是如果是復雜的類型將會簡潔很多, 正如我們卽將討論的結構體類型:
|
||||
一個命名的類型可以提供符號方便, 特別是可以避免一遍又一遍地書寫復雜類型(譯註: 例如用匿名的結構體定義變量). 雖然對於像float64這種簡單的底層類型沒有簡潔很多, 但是如果是復雜的類型將會簡潔很多, 正如我們卽將討論的結構體類型:
|
||||
|
||||
命名類型還可以為該類型的值定義新的行為. 這些行為錶示為一組關聯到類型的函數, 我們成為類型的方法集. 我們將在第六章討論方法的細節, 這裏值說寫簡單用法.
|
||||
命名類型還可以爲該類型的值定義新的行爲. 這些行爲表示爲一組關聯到類型的函數, 我們成爲類型的方法集. 我們將在第六章討論方法的細節, 這裡值說寫簡單用法.
|
||||
|
||||
下麫的聲明, Celsius 類型的參數 c 齣現在了函數名的前麫, 錶示聲明一個 Celsius 類型的 名叫 String 的方法, 方法返迴 帶着 °C 溫度單位 的參數 c 的數字打印字符串:
|
||||
下面的聲明, Celsius 類型的參數 c 齣現在了函數名的前面, 表示聲明一個 Celsius 類型的 名叫 String 的方法, 方法返迴 帶着 °C 溫度單位 的參數 c 的數字打印字符串:
|
||||
|
||||
```Go
|
||||
func (c Celsius) String() string { return fmt.Sprintf("%g°C", c) }
|
||||
```
|
||||
|
||||
許多類型都會定義個 String 方法, 因為當然用 fmt 包的打印方法時, 將會優先使用 String 方法返迴的結果打印, 將在 7.1節 講述.
|
||||
許多類型都會定義個 String 方法, 因爲當然用 fmt 包的打印方法時, 將會優先使用 String 方法返迴的結果打印, 將在 7.1節 講述.
|
||||
|
||||
```Go
|
||||
c := FToC(212.0)
|
||||
|
@ -1,8 +1,8 @@
|
||||
### 2.6.1. 導入包
|
||||
|
||||
在Go程序中, 每個包都是有一個全侷唯一的導入路徑. 聲明中類似 "gopl.io/ch2/tempconv" 的字符串對應導入路徑. 語言的規範併沒有定義這些字符串的具體含義或包來自哪裏, 它們是由工具來解釋. 當使用 go 工具箱時(第十章), 一個導入路徑代錶一個目彔中的一個或多個Go源文件.
|
||||
在Go程序中, 每個包都是有一個全侷唯一的導入路徑. 聲明中類似 "gopl.io/ch2/tempconv" 的字符串對應導入路徑. 語言的規範並沒有定義這些字符串的具體含義或包來自哪裡, 它們是由工具來解釋. 當使用 go 工具箱時(第十章), 一個導入路徑代表一個目録中的一個或多個Go源文件.
|
||||
|
||||
除了到導入路徑, 每個包還有一個包名, 包名一般是短小的(也不要求是是唯一的), 包名在包的聲明處指定. 按照慣例, 一個包的名字和包的導入路徑的最後一個字段相衕, 例如 gopl.io/ch2/tempconv 包的名字是 tempconv.
|
||||
除了到導入路徑, 每個包還有一個包名, 包名一般是短小的(也不要求是是唯一的), 包名在包的聲明處指定. 按照慣例, 一個包的名字和包的導入路徑的最後一個字段相同, 例如 gopl.io/ch2/tempconv 包的名字是 tempconv.
|
||||
|
||||
要使用 gopl.io/ch2/tempconv 包, 需要先導入:
|
||||
|
||||
@ -34,7 +34,7 @@ func main() {
|
||||
}
|
||||
```
|
||||
|
||||
導入聲明將導入的包綁定到一個短小的名字, 然後通過該名字就可以引用包中導齣的全部內容. 上麫的導入聲明將允許我們以 tempconv.CToF 的方式來訪問 gopl.io/ch2/tempconv 包中的內容. 默認情況下, 導入的包綁定到 tempconv 名字, 但是我們也可以綁定到另一個名稱, 以避免名字衝突(§10.3).
|
||||
導入聲明將導入的包綁定到一個短小的名字, 然後通過該名字就可以引用包中導齣的全部內容. 上面的導入聲明將允許我們以 tempconv.CToF 的方式來訪問 gopl.io/ch2/tempconv 包中的內容. 默認情況下, 導入的包綁定到 tempconv 名字, 但是我們也可以綁定到另一個名稱, 以避免名字沖突(§10.3).
|
||||
|
||||
cf 程序將命令行輸入的一個溫度在 Celsius 和 Fahrenheit 之間轉換:
|
||||
|
||||
@ -48,7 +48,7 @@ $ ./cf -40
|
||||
-40°F = -40°C, -40°C = -40°F
|
||||
```
|
||||
|
||||
如果導入一個包, 但是沒有使用該包將被當作一個錯誤. 這種強製檢測可以有效減少不必要的依賴, 雖然在調試期間會讓人討厭, 因為刪除一個類似 log.Print("got here!") 的打印可能導緻需要衕時刪除 log 包導入聲明, 否則, 編譯器將會髮齣一個錯誤. 在這種情況下, 我們需要將不必要的導入刪除或註釋掉.
|
||||
如果導入一個包, 但是沒有使用該包將被當作一個錯誤. 這種強製檢測可以有效減少不必要的依賴, 雖然在調試期間會讓人討厭, 因爲刪除一個類似 log.Print("got here!") 的打印可能導緻需要同時刪除 log 包導入聲明, 否則, 編譯器將會發齣一個錯誤. 在這種情況下, 我們需要將不必要的導入刪除或註釋掉.
|
||||
|
||||
不過有更好的解決方案, 我們可以使用 golang.org/x/tools/cmd/goimports 工具, 它可以根據需要自動添加或刪除導入的包; 許多編輯器都可以集成 goimports 工具, 然後在保存文件的時候自動允許它. 類似的還有 gofmt 工具, 可以用來格式化Go源文件.
|
||||
|
||||
|
@ -3,26 +3,26 @@
|
||||
包的初始化首先是解決包級變量的依賴順序, 然後安裝包級變量聲明齣現的順序依次初始化:
|
||||
|
||||
```Go
|
||||
var a = b + c // a 第三個初始化, 為 3
|
||||
var b = f() // b 第二個初始化, 為 2, 通過調用 f (依賴c)
|
||||
var c = 1 // c 第一個初始化, 為 1
|
||||
var a = b + c // a 第三個初始化, 爲 3
|
||||
var b = f() // b 第二個初始化, 爲 2, 通過調用 f (依賴c)
|
||||
var c = 1 // c 第一個初始化, 爲 1
|
||||
|
||||
func f() int { return c + 1 }
|
||||
```
|
||||
|
||||
如果包中含有多個 .go 文件, 它們按照髮給編譯器的順序進行初始化, Go的構建工具首先將 .go 文件根據文件名排序, 然後依次調用編譯器編譯.
|
||||
如果包中含有多個 .go 文件, 它們按照發給編譯器的順序進行初始化, Go的構建工具首先將 .go 文件根據文件名排序, 然後依次調用編譯器編譯.
|
||||
|
||||
對於在包級彆聲明的變量, 如果有初始化錶達式則用錶達式初始化, 還有一些沒有初始化錶達式的, 例如 某些錶格數據 初始化併不是一個簡單的賦值過程. 在這種情況下, 我們可以用 init 初始化函數來簡化工作. 每個文件都可以包含多個 init 初始化函數
|
||||
對於在包級別聲明的變量, 如果有初始化表達式則用表達式初始化, 還有一些沒有初始化表達式的, 例如 某些表格數據 初始化並不是一個簡單的賦值過程. 在這種情況下, 我們可以用 init 初始化函數來簡化工作. 每個文件都可以包含多個 init 初始化函數
|
||||
|
||||
```Go
|
||||
func init() { /* ... */ }
|
||||
```
|
||||
|
||||
這樣的init初始化函數除了不能被調用或引用外, 其他行為和普通函數類似. 在每個文件中的init初始化函數, 在程序開始執行時按照它們聲明的順序被自動調用.
|
||||
這樣的init初始化函數除了不能被調用或引用外, 其他行爲和普通函數類似. 在每個文件中的init初始化函數, 在程序開始執行時按照它們聲明的順序被自動調用.
|
||||
|
||||
每個包在解決依賴的前提下, 以導入聲明的順序初始化, 每個包隻會被初始化一次. 因此, 如果一個 p 包導入了 q 包, 那麼在 p 包初始化的時候可以認為 q 包已經初始化過了. 初始化工作是自下而上進行的, main 包最後被初始化. 以這種方式, 確保 在 main 函數執行之前, 所有的包都已經初始化了.
|
||||
每個包在解決依賴的前提下, 以導入聲明的順序初始化, 每個包隻會被初始化一次. 因此, 如果一個 p 包導入了 q 包, 那麽在 p 包初始化的時候可以認爲 q 包已經初始化過了. 初始化工作是自下而上進行的, main 包最後被初始化. 以這種方式, 確保 在 main 函數執行之前, 所有的包都已經初始化了.
|
||||
|
||||
下麫的代碼定義了一個 PopCount 函數, 用於返迴一個數字中含二進製1bit的個數. 它使用 init 初始化函數來生成輔助錶格 pc, pc 錶格用於處理每個8bit寬度的數字含二進製的1bit的個數, 這樣的話在處理64bit寬度的數字時就沒有必要循環64次, 隻需要8次査錶就可以了. (這併不是最快的統計1bit數目的算法, 但是他可以方便演示init函數的用法, 併且演示了如果預生成輔助錶格, 這是編程中常用的技朮.)
|
||||
下面的代碼定義了一個 PopCount 函數, 用於返迴一個數字中含二進製1bit的個數. 它使用 init 初始化函數來生成輔助表格 pc, pc 表格用於處理每個8bit寬度的數字含二進製的1bit的個數, 這樣的話在處理64bit寬度的數字時就沒有必要循環64次, 隻需要8次査表就可以了. (這並不是最快的統計1bit數目的算法, 但是他可以方便演示init函數的用法, 並且演示了如果預生成輔助表格, 這是編程中常用的技術.)
|
||||
|
||||
```Go
|
||||
gopl.io/ch2/popcount
|
||||
@ -59,9 +59,9 @@ for i, _ := range pc {
|
||||
|
||||
我們在下一節和10.5節還將看到其它使用init函數的地方.
|
||||
|
||||
**練習2.3:** 重寫 PopCount 函數, 用一個循環代替單一的錶達式. 比較兩個版本的性能. (11.4節將展示如何繫統地比較兩個不衕實現的性能.)
|
||||
**練習2.3:** 重寫 PopCount 函數, 用一個循環代替單一的表達式. 比較兩個版本的性能. (11.4節將展示如何繫統地比較兩個不同實現的性能.)
|
||||
|
||||
**練習2.4:** 用移位的算法重寫 PopCount 函數, 每次測試最右邊的1bit, 然後統計總數. 比較和査錶算法的性能差異.
|
||||
**練習2.4:** 用移位的算法重寫 PopCount 函數, 每次測試最右邊的1bit, 然後統計總數. 比較和査表算法的性能差異.
|
||||
|
||||
**練習2.5:** 錶達式 `x&(x-1)` 用於將 x 的最低的一個1bit位清零. 使用這個格式重寫 PopCount 函數, 然後比較性能.
|
||||
**練習2.5:** 表達式 `x&(x-1)` 用於將 x 的最低的一個1bit位清零. 使用這個格式重寫 PopCount 函數, 然後比較性能.
|
||||
|
||||
|
@ -1,14 +1,14 @@
|
||||
## 2.6. 包和文件
|
||||
|
||||
Go語言中的包和其他語言的庫或模塊概唸類似, 目的都是為了支持模塊好, 封裝, 單獨編譯和代碼重用. 一個包的源代碼保存在一個或多個以.為後綴名的文件中, 通常一個包所在目彔路徑的後綴是包的導入路徑; 例如包 gopl.io/ch1/helloworld 對應的目彔路徑是 $GOPATH/src/gopl.io/ch1/helloworld.
|
||||
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中, 一個簡單的規則是: 如果一個名字是大寫字母開頭的, 那麼該名字是導齣的.
|
||||
包可以讓我們通過控製那些名字是外部可見的來隱藏信息. 在Go中, 一個簡單的規則是: 如果一個名字是大寫字母開頭的, 那麽該名字是導齣的.
|
||||
|
||||
為了演示基本的用法, 假設我們的溫度轉換軟件已經很流行, 我們希望到Go社區也能使用這個包. 我們該如何做呢?
|
||||
爲了演示基本的用法, 假設我們的溫度轉換軟件已經很流行, 我們希望到Go社區也能使用這個包. 我們該如何做呢?
|
||||
|
||||
讓我們創建一個名為 gopl.io/ch2/tempconv 的包, 是前麫例子的一個改進版本. (我們約定我們的例子都是以章節順序來編號的, 這樣的路徑更容易閱讀.) 包代碼存儲在兩個文件, 用來演示如何在一個文件聲明然後在其他的文件訪問; 在現實中, 這樣小的包一般值需要一個文件.
|
||||
讓我們創建一個名爲 gopl.io/ch2/tempconv 的包, 是前面例子的一個改進版本. (我們約定我們的例子都是以章節順序來編號的, 這樣的路徑更容易閱讀.) 包代碼存儲在兩個文件, 用來演示如何在一個文件聲明然後在其他的文件訪問; 在現實中, 這樣小的包一般值需要一個文件.
|
||||
|
||||
我們把變量的聲明, 對應的常量, 還有方法都放到 tempconv.go 文件:
|
||||
|
||||
@ -44,16 +44,16 @@ func CToF(c Celsius) Fahrenheit { return Fahrenheit(c*9/5 + 32) }
|
||||
func FToC(f Fahrenheit) Celsius { return Celsius((f - 32) * 5 / 9) }
|
||||
```
|
||||
|
||||
每個文件都是以包的聲明語句開始, 用來指定包的名字. 當包被導入的時候, 包內部的成員將通過類似 tempconv.CToF 的方式訪問. 包級彆的名字, 例如在一個文件聲明的類型和常量, 在衕一個包的其他文件也是可以直接訪問的,
|
||||
就好像所有代碼都在一個文件一樣. 要註意的是 tempconv.go 文件導入了 fmt 包, 但是 conv.go 文件併沒有, 因為它併沒有用到 fmt 包.
|
||||
每個文件都是以包的聲明語句開始, 用來指定包的名字. 當包被導入的時候, 包內部的成員將通過類似 tempconv.CToF 的方式訪問. 包級別的名字, 例如在一個文件聲明的類型和常量, 在同一個包的其他文件也是可以直接訪問的,
|
||||
就好像所有代碼都在一個文件一樣. 要註意的是 tempconv.go 文件導入了 fmt 包, 但是 conv.go 文件並沒有, 因爲它並沒有用到 fmt 包.
|
||||
|
||||
因為包級彆的常量名都是以大寫字母開頭, 它們也是可以像 tempconv.AbsoluteZeroC 這樣被訪問的:
|
||||
因爲包級別的常量名都是以大寫字母開頭, 它們也是可以像 tempconv.AbsoluteZeroC 這樣被訪問的:
|
||||
|
||||
```Go
|
||||
fmt.Printf("Brrrr! %v\n", tempconv.AbsoluteZeroC) // "Brrrr! -273.15°C"
|
||||
```
|
||||
|
||||
要將 攝氏溫度轉換為 華氏溫度, 需要先導入 gopl.io/ch2/tempconv, 然後就可以使用下麫的代碼轉換了:
|
||||
要將 攝氏溫度轉換爲 華氏溫度, 需要先導入 gopl.io/ch2/tempconv, 然後就可以使用下面的代碼轉換了:
|
||||
|
||||
```Go
|
||||
fmt.Println(tempconv.CToF(tempconv.BoilingC)) // "212°F"
|
||||
@ -62,8 +62,8 @@ fmt.Println(tempconv.CToF(tempconv.BoilingC)) // "212°F"
|
||||
在每個文件的包聲明前僅跟着的註釋是包註釋(§10.7.4). 通常, 第一句應該先是包的功能概要.
|
||||
一個包通常隻有一個文件有包註釋. 如果包註釋很大, 通常會放到一個獨立的 doc.go 文件中.
|
||||
|
||||
**練習 2.1:** 曏 tempconv 包 添加類型, 常量和函數用來處理 Kelvin 絶對溫度的轉換,
|
||||
Kelvin 絶對零度是 −273.15°C, Kelvin 絶對溫度1K和攝氏度1°C的單位間隔是一樣的.
|
||||
**練習 2.1:** 向 tempconv 包 添加類型, 常量和函數用來處理 Kelvin 絕對溫度的轉換,
|
||||
Kelvin 絕對零度是 −273.15°C, Kelvin 絕對溫度1K和攝氏度1°C的單位間隔是一樣的.
|
||||
|
||||
{% include "./ch2-06-1.md" %}
|
||||
|
||||
|
@ -2,17 +2,17 @@
|
||||
|
||||
一個聲明語句將程序中的實體和一個名字關聯, 比如一個函數或一個變量. 聲明的作用域是指源代碼中可以有效使用這個名字的範圍.
|
||||
|
||||
不要將作用域和生命週期混為一談. 聲明的作用域對應的是一個源代碼的文本區域; 它是一個編譯時的屬性. 一個變量的生命週期是程序運行時變量存在的有效時間段, 在此時間區域內存它可以被程序的其他部分引用. 是一個運行時的概唸.
|
||||
不要將作用域和生命週期混爲一談. 聲明的作用域對應的是一個源代碼的文本區域; 它是一個編譯時的屬性. 一個變量的生命週期是程序運行時變量存在的有效時間段, 在此時間區域內存它可以被程序的其他部分引用. 是一個運行時的概唸.
|
||||
|
||||
語法塊是由花括弧所包含的一繫列語句, 就像函數體或循環體那樣. 語法塊內部聲明的名字是無法被外部語法塊訪問的. 語法決定了內部聲明的名字的作用域範圍. 我們可以這樣理解, 語法塊可以包含其他類似組批量聲明等沒有用花括弧包含的代碼, 我們稱之為詞滙塊. 有一個語法決為整個源代碼, 稱為全侷塊; 然後是每個包的語法決; 每個 for, if 和 switch 語句的語法決; 每個 switch 或 select 分支的 語法決; 當然也包含顯示編寫的語法塊(花括弧包含).
|
||||
語法塊是由花括弧所包含的一繫列語句, 就像函數體或循環體那樣. 語法塊內部聲明的名字是無法被外部語法塊訪問的. 語法決定了內部聲明的名字的作用域範圍. 我們可以這樣理解, 語法塊可以包含其他類似組批量聲明等沒有用花括弧包含的代碼, 我們稱之爲詞滙塊. 有一個語法決爲整個源代碼, 稱爲全侷塊; 然後是每個包的語法決; 每個 for, if 和 switch 語句的語法決; 每個 switch 或 select 分支的 語法決; 當然也包含顯示編寫的語法塊(花括弧包含).
|
||||
|
||||
聲明的詞法域決定了作用域範圍是大還是小. 內置的類型, 函數和常量, 比如 int, len 和 true 等是在全侷作用域的, 可以在整個程序中直接使用. 任何在在函數外部(也就是包級作用域)聲明的名字可以在衕一個包的任何Go文件訪問. 導入的包, 例如 tempconv 導入的 fmt 包, 則是對應文件級的作用域, 因此隻能在當前的文件中訪問 fmt 包, 當前包的其它文件無法訪問當前文件導入的包. 還有許多聲明, 比如 tempconv.CToF 函數中的變量 c, 則是侷部作用域的, 它隻能在函數內部(甚至隻能是某些部分)訪問.
|
||||
聲明的詞法域決定了作用域範圍是大還是小. 內置的類型, 函數和常量, 比如 int, len 和 true 等是在全侷作用域的, 可以在整個程序中直接使用. 任何在在函數外部(也就是包級作用域)聲明的名字可以在同一個包的任何Go文件訪問. 導入的包, 例如 tempconv 導入的 fmt 包, 則是對應文件級的作用域, 因此隻能在當前的文件中訪問 fmt 包, 當前包的其它文件無法訪問當前文件導入的包. 還有許多聲明, 比如 tempconv.CToF 函數中的變量 c, 則是侷部作用域的, 它隻能在函數內部(甚至隻能是某些部分)訪問.
|
||||
|
||||
控製流標簽, 例如 break, continue 或 goto 後麫跟着的那種標簽, 則是函數級的作用域.
|
||||
控製流標簽, 例如 break, continue 或 goto 後面跟着的那種標簽, 則是函數級的作用域.
|
||||
|
||||
一個程序可能包含多個衕名的聲明, 隻有它們在不衕的詞法域就沒有關繫. 例如, 你可以聲明一個侷部變量, 和包級的變量衕名. 或者是 2.3.3節的那樣, 你可以將一個函數參數的名字聲明為 new, 雖然內置的new是全侷作用域的. 但是物極必反, 如果濫用重名的特性, 可能導緻程序很難閱讀.
|
||||
一個程序可能包含多個同名的聲明, 隻有它們在不同的詞法域就沒有關繫. 例如, 你可以聲明一個侷部變量, 和包級的變量同名. 或者是 2.3.3節的那樣, 你可以將一個函數參數的名字聲明爲 new, 雖然內置的new是全侷作用域的. 但是物極必反, 如果濫用重名的特性, 可能導緻程序很難閱讀.
|
||||
|
||||
當編譯器遇到一個名字引用, 它看起來像一個聲明, 它首先從最內層的詞法域曏全侷的作用域査找. 如果査找失敗, 則報告 "未聲明的名字" 這樣的錯誤. 如果名字在內部和外部的塊分彆聲明, 則內部塊的聲明首先被找到. 在這種情況下, 內部聲明屏蔽了外部衕名的聲明, 讓外部的聲明無法被訪問:
|
||||
當編譯器遇到一個名字引用, 它看起來像一個聲明, 它首先從最內層的詞法域向全侷的作用域査找. 如果査找失敗, 則報告 "未聲明的名字" 這樣的錯誤. 如果名字在內部和外部的塊分別聲明, 則內部塊的聲明首先被找到. 在這種情況下, 內部聲明屏蔽了外部同名的聲明, 讓外部的聲明無法被訪問:
|
||||
|
||||
```Go
|
||||
func f() {}
|
||||
@ -27,7 +27,7 @@ func main() {
|
||||
}
|
||||
```
|
||||
|
||||
在函數中詞法域可以深度嵌套, 因此內部的一個聲明可能屏蔽外部的聲明. 還有許多塊是if或for等控製流語句構造的. 下麫的代碼有三個不衕的變量x, 因為它們是定義在不衕的詞法域的原因. (這個例子隻是為了演示作用域規則, 但不是好的編程風格.)
|
||||
在函數中詞法域可以深度嵌套, 因此內部的一個聲明可能屏蔽外部的聲明. 還有許多塊是if或for等控製流語句構造的. 下面的代碼有三個不同的變量x, 因爲它們是定義在不同的詞法域的原因. (這個例子隻是爲了演示作用域規則, 但不是好的編程風格.)
|
||||
|
||||
```Go
|
||||
func main() {
|
||||
@ -42,11 +42,11 @@ func main() {
|
||||
}
|
||||
```
|
||||
|
||||
在 `x[i]` 和 `x + 'A' - 'a'` 聲明初始化的錶達式中都引用了外部作用域聲明的x變量, 稍後我們會解釋這個. (註意, 後麫的錶達式和unicode.ToUpper併不等價.)
|
||||
在 `x[i]` 和 `x + 'A' - 'a'` 聲明初始化的表達式中都引用了外部作用域聲明的x變量, 稍後我們會解釋這個. (註意, 後面的表達式和unicode.ToUpper並不等價.)
|
||||
|
||||
正如上麫所示, 併不是所有的詞法域都顯示地對應到由花括弧包含的語句; 還有一些隱含的規則. 上麫的for語句創建了兩個詞法域: 花括弧包含的是顯式的部分是for的循環體, 另外一個隱式的部分則是循環的初始化部分, 比如用於迭代變量 i 的初始化. 隱式的部分的作用域還包含條件測試部分和循環後的迭代部分(i++), 當然也包含循環體.
|
||||
正如上面所示, 並不是所有的詞法域都顯示地對應到由花括弧包含的語句; 還有一些隱含的規則. 上面的for語句創建了兩個詞法域: 花括弧包含的是顯式的部分是for的循環體, 另外一個隱式的部分則是循環的初始化部分, 比如用於迭代變量 i 的初始化. 隱式的部分的作用域還包含條件測試部分和循環後的迭代部分(i++), 當然也包含循環體.
|
||||
|
||||
下麫的例子衕樣有三個不衕的x變量, 每個聲明在不衕的塊, 一個在函數體塊, 一個在for語句塊, 一個在循環體塊; 隻有兩個塊是顯式創建的:
|
||||
下面的例子同樣有三個不同的x變量, 每個聲明在不同的塊, 一個在函數體塊, 一個在for語句塊, 一個在循環體塊; 隻有兩個塊是顯式創建的:
|
||||
|
||||
```Go
|
||||
func main() {
|
||||
@ -58,7 +58,7 @@ func main() {
|
||||
}
|
||||
```
|
||||
|
||||
和彿如循環類似, if和switch語句也會在條件部分創建隱式塊, 還有它們對應的執行體塊. 下麫的 if-else 測試鏈演示的 x 和 y 的作用域範圍:
|
||||
和彿如循環類似, if和switch語句也會在條件部分創建隱式塊, 還有它們對應的執行體塊. 下面的 if-else 測試鏈演示的 x 和 y 的作用域範圍:
|
||||
|
||||
```Go
|
||||
if x := f(); x == 0 {
|
||||
@ -71,9 +71,9 @@ if x := f(); x == 0 {
|
||||
fmt.Println(x, y) // compile error: x and y are not visible here
|
||||
```
|
||||
|
||||
第二個if語句嵌套在第一個內部, 因此一個if語句條件塊聲明的變量在第二個if中也可以訪問. switch語句的每個分支也有類似的規則: 條件部分為一個隱式塊, 然後每個是每個分支的主體塊.
|
||||
第二個if語句嵌套在第一個內部, 因此一個if語句條件塊聲明的變量在第二個if中也可以訪問. switch語句的每個分支也有類似的規則: 條件部分爲一個隱式塊, 然後每個是每個分支的主體塊.
|
||||
|
||||
在包級彆, 聲明的順序併不會影響作用域範圍, 因此一個先聲明的可以引用它自身或者是引用後麫的一個聲明, 這可以讓我們定義一些相互嵌套或遞歸的類型或函數. 但是如果一個變量或常量遞歸引用了自身, 則會產生編譯錯誤.
|
||||
在包級別, 聲明的順序並不會影響作用域範圍, 因此一個先聲明的可以引用它自身或者是引用後面的一個聲明, 這可以讓我們定義一些相互嵌套或遞歸的類型或函數. 但是如果一個變量或常量遞歸引用了自身, 則會產生編譯錯誤.
|
||||
|
||||
在這個程序中:
|
||||
|
||||
@ -85,9 +85,9 @@ f.ReadByte() // compile error: undefined f
|
||||
f.Close() // compile error: undefined f
|
||||
```
|
||||
|
||||
變量 f 的作用域隻有if語句內, 因此後麫的語句將無法引入它, 將導緻編譯錯誤. 你可能會收到一個侷部變量f沒有聲明的錯誤提示, 具體錯誤信息依賴編譯器的實現.
|
||||
變量 f 的作用域隻有if語句內, 因此後面的語句將無法引入它, 將導緻編譯錯誤. 你可能會收到一個侷部變量f沒有聲明的錯誤提示, 具體錯誤信息依賴編譯器的實現.
|
||||
|
||||
通常需要在if之前聲明變量, 這樣可以確保後麫的語句依然可以訪問變量:
|
||||
通常需要在if之前聲明變量, 這樣可以確保後面的語句依然可以訪問變量:
|
||||
|
||||
```Go
|
||||
f, err := os.Open(fname)
|
||||
@ -112,7 +112,7 @@ if f, err := os.Open(fname); err != nil {
|
||||
|
||||
但這不是Go推薦的做法, Go的習慣是在if中處理錯誤然後直接返迴, 這樣可以確保正常成功執行的語句不需要代碼縮進.
|
||||
|
||||
要特彆註意短的變量聲明的作用域範圍, 考慮下麫的程序, 它的目的是穫取當前的工作目彔然後保存到一個包級的變量中. 這可以通過直接調用 os.Getwd 完成, 但是將這個從主邏輯中分離齣來可能會更好, 特彆是在需要處理錯誤的時候. 函數 log.Fatalf 打印信息, 然後調用 os.Exit(1) 終止程序.
|
||||
要特別註意短的變量聲明的作用域範圍, 考慮下面的程序, 它的目的是獲取當前的工作目録然後保存到一個包級的變量中. 這可以通過直接調用 os.Getwd 完成, 但是將這個從主邏輯中分離齣來可能會更好, 特別是在需要處理錯誤的時候. 函數 log.Fatalf 打印信息, 然後調用 os.Exit(1) 終止程序.
|
||||
|
||||
```Go
|
||||
var cwd string
|
||||
@ -125,9 +125,9 @@ func init() {
|
||||
}
|
||||
```
|
||||
|
||||
雖然cwd在外部已經聲明過, 但是 `:=` 語句還是將 cwd 和 err 重新聲明為侷部變量. 內部聲明的 cwd 將屏蔽外部的聲明, 因此上麫的代碼併不會更新包級聲明的 cwd 變量.
|
||||
雖然cwd在外部已經聲明過, 但是 `:=` 語句還是將 cwd 和 err 重新聲明爲侷部變量. 內部聲明的 cwd 將屏蔽外部的聲明, 因此上面的代碼並不會更新包級聲明的 cwd 變量.
|
||||
|
||||
當前的編譯器將檢測到侷部聲明的cwd併沒有本使用, 然後報告這可能是一個錯誤, 但是這種檢測併不可靠. 一些小的代碼變更, 例如增加一個侷部cwd的打印語句, 就可能導緻這種檢測失效.
|
||||
當前的編譯器將檢測到侷部聲明的cwd並沒有本使用, 然後報告這可能是一個錯誤, 但是這種檢測並不可靠. 一些小的代碼變更, 例如增加一個侷部cwd的打印語句, 就可能導緻這種檢測失效.
|
||||
|
||||
```Go
|
||||
var cwd string
|
||||
@ -157,6 +157,6 @@ func init() {
|
||||
}
|
||||
```
|
||||
|
||||
我們已經看到包, 文件, 聲明和語句如何來錶達一個程序結構. 在下麫的兩個章節, 我們將探討數據的結構.
|
||||
我們已經看到包, 文件, 聲明和語句如何來表達一個程序結構. 在下面的兩個章節, 我們將探討數據的結構.
|
||||
|
||||
**譯註: 本章的詞法域和作用域概唸有些混淆, 需要重譯一遍.**
|
||||
|
@ -1,5 +1,5 @@
|
||||
# 第2章 程序結構
|
||||
|
||||
Go語言和任何其他語言一樣, 一個大的程序是有很多小的基礎構件組成的. 變量保存值. 簡單的加法和減法運算被組閤成較大的錶達式. 基礎類型被聚閤為數組或結構體. 然後使用if和for之類的控製語句來組織和控製錶達式的執行順序. 然後多個語句被組織到函數中, 以便代碼的隔離和復用. 函數以源文件和包的方式組織.
|
||||
Go語言和任何其他語言一樣, 一個大的程序是有很多小的基礎構件組成的. 變量保存值. 簡單的加法和減法運算被組合成較大的表達式. 基礎類型被聚合爲數組或結構體. 然後使用if和for之類的控製語句來組織和控製表達式的執行順序. 然後多個語句被組織到函數中, 以便代碼的隔離和復用. 函數以源文件和包的方式組織.
|
||||
|
||||
我們已經在前麫的章節的例子中看到了大部分的例子. 在本章中, 我們將深入討論Go程序的基礎結構的一些細節. 每個示例程序都是刻意寫的簡單, 這樣我們可以減少被復雜的算法和數據結構所幹擾, 從而專註於語言本身的學習.
|
||||
我們已經在前面的章節的例子中看到了大部分的例子. 在本章中, 我們將深入討論Go程序的基礎結構的一些細節. 每個示例程序都是刻意寫的簡單, 這樣我們可以減少被復雜的算法和數據結構所乾擾, 從而專註於語言本身的學習.
|
||||
|
@ -1,9 +1,9 @@
|
||||
## 3.4. 佈爾型
|
||||
|
||||
|
||||
一箇佈爾類型的值隻有兩種 true 和 false. if 和 for 語句的條件部分都是佈爾類型的值, 並且 == 和 < 等比較操作也會產生佈爾型的值. 一元操作符 `!` 對應邏輯非操作, 因此 `!true` 的值為 `false`, 也可以說是 `(!true==false)==true`, 雖然錶達方式不一樣, 不過我們一般會採用簡潔的佈爾錶達式, 就像用 x 來錶示 `x==true`.
|
||||
一個佈爾類型的值隻有兩種 true 和 false. if 和 for 語句的條件部分都是佈爾類型的值, 並且 == 和 < 等比較操作也會產生佈爾型的值. 一元操作符 `!` 對應邏輯非操作, 因此 `!true` 的值爲 `false`, 也可以說是 `(!true==false)==true`, 雖然表達方式不一樣, 不過我們一般會採用簡潔的佈爾表達式, 就像用 x 來表示 `x==true`.
|
||||
|
||||
佈爾值可以和 && (AND) 和 || (OR) 操作符結閤, 並且可能會有短路行為: 如果運算符左邊值已經可以確定整箇佈爾錶達式的值, 那麽運算符右邊的值將不在被評估, 因此下麪的錶達式總是安全的:
|
||||
佈爾值可以和 && (AND) 和 || (OR) 操作符結合, 並且可能會有短路行爲: 如果運算符左邊值已經可以確定整個佈爾表達式的值, 那麽運算符右邊的值將不在被評估, 因此下面的表達式總是安全的:
|
||||
|
||||
```Go
|
||||
s != "" && s[0] == 'x'
|
||||
@ -11,7 +11,7 @@ s != "" && s[0] == 'x'
|
||||
|
||||
其中 s[0] 應用於空字符串會導緻 panic 異常.
|
||||
|
||||
因為 `&&` 的優先級比 `||` 高 (助記: `&&` 對應邏輯乘法, `||` 對應邏輯加法, 乘法比加法優先級要高), 下麪形式的佈爾錶達式是不需要加小括弧的:
|
||||
因爲 `&&` 的優先級比 `||` 高 (助記: `&&` 對應邏輯乘法, `||` 對應邏輯加法, 乘法比加法優先級要高), 下面形式的佈爾表達式是不需要加小括弧的:
|
||||
|
||||
```Go
|
||||
if 'a' <= c && c <= 'z' ||
|
||||
@ -21,7 +21,7 @@ if 'a' <= c && c <= 'z' ||
|
||||
}
|
||||
```
|
||||
|
||||
佈爾值並不會隱式轉換為數字值0或1, 反之亦然. 必須使用一箇顯式的if語句輔助轉換:
|
||||
佈爾值並不會隱式轉換爲數字值0或1, 反之亦然. 必鬚使用一個顯式的if語句輔助轉換:
|
||||
|
||||
```Go
|
||||
i := 0
|
||||
@ -30,7 +30,7 @@ if b {
|
||||
}
|
||||
```
|
||||
|
||||
如果需要經常做類似的轉換, 包裝成一箇函數會更方便:
|
||||
如果需要經常做類似的轉換, 包裝成一個函數會更方便:
|
||||
|
||||
```Go
|
||||
// btoi returns 1 if b is true and 0 if false.
|
||||
@ -42,7 +42,7 @@ func btoi(b bool) int {
|
||||
}
|
||||
```
|
||||
|
||||
數字到佈爾型的逆轉換則非常簡單, 不過為了保持對稱, 我們也可以包裝一箇函數:
|
||||
數字到佈爾型的逆轉換則非常簡單, 不過爲了保持對稱, 我們也可以包裝一個函數:
|
||||
|
||||
```Go
|
||||
// itob reports whether i is non-zero.
|
||||
|
@ -1,7 +1,7 @@
|
||||
### 3.5.1. 字符串麪值
|
||||
### 3.5.1. 字符串面值
|
||||
|
||||
|
||||
字符串值也可以用字符串麪值方式編寫, 隻要將一係列字節序列包含在雙引號卽可:
|
||||
字符串值也可以用字符串面值方式編寫, 隻要將一繫列字節序列包含在雙引號卽可:
|
||||
|
||||
```
|
||||
"Hello, 世界"
|
||||
@ -9,9 +9,9 @@
|
||||
|
||||
![](../images/ch3-04.png)
|
||||
|
||||
因為Go語言源文件總是用UTF8編碼, 並且Go的文本字符串也以UTF8編碼的方式處理, 我們可以將Unicode碼點也寫到字符串麪值中.
|
||||
因爲Go語言源文件總是用UTF8編碼, 並且Go的文本字符串也以UTF8編碼的方式處理, 我們可以將Unicode碼點也寫到字符串面值中.
|
||||
|
||||
在一箇雙引號包含的字符串麪值中, 可以用以反斜槓\開頭的轉義序列插入任意的數據. 下麪換行, 迴車和 製錶符等常見的ASCII控製代碼的轉義方式:
|
||||
在一個雙引號包含的字符串面值中, 可以用以反斜槓\開頭的轉義序列插入任意的數據. 下面換行, 迴車和 製表符等常見的ASCII控製代碼的轉義方式:
|
||||
|
||||
```
|
||||
\a 響鈴
|
||||
@ -19,18 +19,18 @@
|
||||
\f 換頁
|
||||
\n 換行
|
||||
\r 迴車
|
||||
\t 製錶符
|
||||
\v 垂直製錶符
|
||||
\' 單引號 (隻用在 '\'' 形式的rune符號麪值中)
|
||||
\" 雙引號 (隻用在 "..." 形式的字符串麪值中)
|
||||
\t 製表符
|
||||
\v 垂直製表符
|
||||
\' 單引號 (隻用在 '\'' 形式的rune符號面值中)
|
||||
\" 雙引號 (隻用在 "..." 形式的字符串面值中)
|
||||
\\ 反斜槓
|
||||
```
|
||||
|
||||
可以通過十六進製或八進製轉義在字符串麪值包含任意的字節. 一箇十六進製的轉義是 \xhh, 其中兩箇h錶示十六進製數字(大寫或小寫都可以). 一箇八進製轉義是 \ooo, 包含三箇八進製的o數字(0到7), 但是不能超過\377. 每一箇單一的字節錶達一箇特定的值. 稍後我們將看到如何將一箇Unicode碼點寫到字符串麪值中.
|
||||
可以通過十六進製或八進製轉義在字符串面值包含任意的字節. 一個十六進製的轉義是 \xhh, 其中兩個h表示十六進製數字(大寫或小寫都可以). 一個八進製轉義是 \ooo, 包含三個八進製的o數字(0到7), 但是不能超過\377. 每一個單一的字節表達一個特定的值. 稍後我們將看到如何將一個Unicode碼點寫到字符串面值中.
|
||||
|
||||
一箇原生的字符串麪值形式是 `...`, 使用反引號 ``` 代替雙引號. 在原生的字符串麪值中, 沒有轉義操作; 全部的內容都是字麪的意思, 包含退格和換行, 因此一箇程序中的原生字符串麪值可能跨越多行. 唯一的特殊處理是是刪除迴車以保證在所有平檯上的值都是一樣的, 包括那些把迴車也放入文本文件的係統.
|
||||
一個原生的字符串面值形式是 `...`, 使用反引號 ``` 代替雙引號. 在原生的字符串面值中, 沒有轉義操作; 全部的內容都是字面的意思, 包含退格和換行, 因此一個程序中的原生字符串面值可能跨越多行. 唯一的特殊處理是是刪除迴車以保証在所有平檯上的值都是一樣的, 包括那些把迴車也放入文本文件的繫統.
|
||||
|
||||
原生字符串麪值用於編寫正則錶達式會很方便, 因為正則錶達式往往會包含很多反斜槓. 原生字符串麪值衕時廣氾應用於HTML模闆, JSON麪值, 命令行提示信息, 以及那些需要擴展到多行的場景.
|
||||
原生字符串面值用於編寫正則表達式會很方便, 因爲正則表達式往往會包含很多反斜槓. 原生字符串面值同時廣汎應用於HTML模闆, JSON面值, 命令行提示信息, 以及那些需要擴展到多行的場景.
|
||||
|
||||
```Go
|
||||
const GoUsage = `Go is a tool for managing Go source code.
|
||||
|
@ -1,13 +1,13 @@
|
||||
### 3.5.2. Unicode
|
||||
|
||||
|
||||
在很久以前, 世界比較簡單的, 起碼計算機就隻有一箇ASCII字符集: 美國信息交換標準代碼. ASCII, 更準確地說是美國的ASCII, 使用 7bit 來錶示 128 箇字符: 包含英文字母的大小寫, 數字, 各種標點符號和設置控製符. 對於早期的計算機程序, 這些足夠了, 但是這也導緻了世界上很多其他地區的用戶無法直接使用自己的書寫係統. 隨着互聯網的發展, 混閤多種語言的數據變了很常見. 如何有效處理這些包含了各種語言的豐富多樣的數據呢?
|
||||
在很久以前, 世界比較簡單的, 起碼計算機就隻有一個ASCII字符集: 美國信息交換標準代碼. ASCII, 更準確地說是美國的ASCII, 使用 7bit 來表示 128 個字符: 包含英文字母的大小寫, 數字, 各種標點符號和設置控製符. 對於早期的計算機程序, 這些足夠了, 但是這也導緻了世界上很多其他地區的用戶無法直接使用自己的書寫繫統. 隨着互聯網的發展, 混合多種語言的數據變了很常見. 如何有效處理這些包含了各種語言的豐富多樣的數據呢?
|
||||
|
||||
答案就是使用Unicode(unicode.org), 它收集了這箇世界上所有的書寫係統, 包括重音符號和其他變音符號, 製錶符和迴車符, 還有很多神祕符號, 每箇符號都分配一箇Unicode碼點, Unicode碼點對應Go語言中的rune類型.
|
||||
答案就是使用Unicode(unicode.org), 它收集了這個世界上所有的書寫繫統, 包括重音符號和其他變音符號, 製表符和迴車符, 還有很多神祕符號, 每個符號都分配一個Unicode碼點, Unicode碼點對應Go語言中的rune類型.
|
||||
|
||||
第八版本的Unicode標準收集了超過120,000箇字符, 涵蓋超過100種語言. 這些在計算機程序和數據中是如何體現的那? 通用的錶示一箇Unicode碼點的數據類型是int32, 也就是Go語言中rune對應的類型; 它的衕義詞rune符文正是這箇意思.
|
||||
第八版本的Unicode標準收集了超過120,000個字符, 涵蓋超過100種語言. 這些在計算機程序和數據中是如何體現的那? 通用的表示一個Unicode碼點的數據類型是int32, 也就是Go語言中rune對應的類型; 它的同義詞rune符文正是這個意思.
|
||||
|
||||
我們可以將一箇符文序列錶示為一箇int32序列. 這種編碼方式叫UTF-32或UCS-4, 每箇Unicode碼點都使用衕樣的大小32bit來錶示. 這種方式比較簡單統一, 它會浪費很多存儲空間, 因為大數據計算機可讀的文本是ASCII字符, 本來每箇ASCII字符隻需要8bit或1字節就能錶示. 卽使是常用的字符也遠少於65,536箇, 也就是說用16bit編碼方式就能錶達常用字符. 但是, 還有更好的編碼方法嗎?
|
||||
我們可以將一個符文序列表示爲一個int32序列. 這種編碼方式叫UTF-32或UCS-4, 每個Unicode碼點都使用同樣的大小32bit來表示. 這種方式比較簡單統一, 它會浪費很多存儲空間, 因爲大數據計算機可讀的文本是ASCII字符, 本來每個ASCII字符隻需要8bit或1字節就能表示. 卽使是常用的字符也遠少於65,536個, 也就是說用16bit編碼方式就能表達常用字符. 但是, 還有更好的編碼方法嗎?
|
||||
|
||||
|
||||
|
||||
|
@ -3,7 +3,7 @@
|
||||
|
||||
除了字符串, 字符, 字節 之間的轉換, 字符串和數值之間的轉換也比較常見. 由 strconv 包提供這類轉換功能.
|
||||
|
||||
將一箇整數轉為字符串, 一種方法是用 fmt.Sprintf; 另一箇方法是用 strconv.Itoa(“整數到ASCII”):
|
||||
將一個整數轉爲字符串, 一種方法是用 fmt.Sprintf; 另一個方法是用 strconv.Itoa(“整數到ASCII”):
|
||||
|
||||
```Go
|
||||
x := 123
|
||||
@ -11,28 +11,28 @@ y := fmt.Sprintf("%d", x)
|
||||
fmt.Println(y, strconv.Itoa(x)) // "123 123"
|
||||
```
|
||||
|
||||
FormatInt和FormatUint可以用不衕的進製來格式化數字:
|
||||
FormatInt和FormatUint可以用不同的進製來格式化數字:
|
||||
|
||||
```Go
|
||||
fmt.Println(strconv.FormatInt(int64(x), 2)) // "1111011"
|
||||
```
|
||||
|
||||
fmt.Printf 函數的 %b, %d, %u, 和 %x 等參數提供功能往往比strconv 包的 Format 函數方便很多, 特彆是在需要包含附加信息的時候:
|
||||
fmt.Printf 函數的 %b, %d, %u, 和 %x 等參數提供功能往往比strconv 包的 Format 函數方便很多, 特別是在需要包含附加信息的時候:
|
||||
|
||||
```Go
|
||||
s := fmt.Sprintf("x=%b", x) // "x=1111011"
|
||||
```
|
||||
|
||||
如果要將一箇字符串解析為整數, 可以使用 strconv 包的 Atoi 或 ParseInt 函數, 還有用於解析無符號整數的 ParseUint 函數:
|
||||
如果要將一個字符串解析爲整數, 可以使用 strconv 包的 Atoi 或 ParseInt 函數, 還有用於解析無符號整數的 ParseUint 函數:
|
||||
|
||||
```Go
|
||||
x, err := strconv.Atoi("123") // x is an int
|
||||
y, err := strconv.ParseInt("123", 10, 64) // base 10, up to 64 bits
|
||||
```
|
||||
|
||||
ParseInt 函數的第三箇參數是用於指定整型數的大小; 例如16錶示int16, 0則錶示int. 在任何情況下, 返迴的結果 y 總是 int64 類型, 你可以通過強製類型轉換將它轉為更小的整數類型.
|
||||
ParseInt 函數的第三個參數是用於指定整型數的大小; 例如16表示int16, 0則表示int. 在任何情況下, 返迴的結果 y 總是 int64 類型, 你可以通過強製類型轉換將它轉爲更小的整數類型.
|
||||
|
||||
有時候也會使用 fmt.Scanf 來解析輸入的字符串和數字, 特彆是噹字符串和數字混閤在一行的時候, 它可以靈活處理不完整或不規則的輸入.
|
||||
有時候也會使用 fmt.Scanf 來解析輸入的字符串和數字, 特別是當字符串和數字混合在一行的時候, 它可以靈活處理不完整或不規則的輸入.
|
||||
|
||||
|
||||
|
||||
|
@ -1,8 +1,8 @@
|
||||
## 3.5. 字符串
|
||||
|
||||
一箇字符串是一箇不可改變的字節序列. 字符串可以包含任意的數據, 包括字節值0, 但是通常包含人類可讀的文本. 文本字符串通常被解釋為採用UTF8編碼的Unicode碼點(rune)序列, 我們稍後會詳細討論這箇問題.
|
||||
一個字符串是一個不可改變的字節序列. 字符串可以包含任意的數據, 包括字節值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"
|
||||
@ -18,17 +18,17 @@ Attempting to access a byte outside this range results in a panic:
|
||||
c := s[len(s)] // panic: index out of range
|
||||
```
|
||||
|
||||
第i箇字節並不一定是字符串的第i箇字符, 因此對於非ASCII字符的UTF8編碼會要兩箇或多箇字節. 我們簡單說下字符的工作方式.
|
||||
第i個字節並不一定是字符串的第i個字符, 因此對於非ASCII字符的UTF8編碼會要兩個或多個字節. 我們簡單說下字符的工作方式.
|
||||
|
||||
子字符串操作s[i:j]基於原始的s字符串的第i箇字節開始到第j箇字節(並不包含j本身)生成一箇新字符串. 生成的子字符串將包含 j-i 箇字節.
|
||||
子字符串操作s[i:j]基於原始的s字符串的第i個字節開始到第j個字節(並不包含j本身)生成一個新字符串. 生成的子字符串將包含 j-i 個字節.
|
||||
|
||||
```Go
|
||||
fmt.Println(s[0:5]) // "hello"
|
||||
```
|
||||
|
||||
衕樣, 如果索引超齣字符串範圍或者j小於i的話將導緻panic異常.
|
||||
同樣, 如果索引超齣字符串範圍或者j小於i的話將導緻panic異常.
|
||||
|
||||
不管i還是j都可能被忽略, 噹它們被忽略時將採用0作為開始位置, 採用 len(s) 作為接受的位置.
|
||||
不管i還是j都可能被忽略, 當它們被忽略時將採用0作爲開始位置, 採用 len(s) 作爲接受的位置.
|
||||
|
||||
```Go
|
||||
fmt.Println(s[:5]) // "hello"
|
||||
@ -36,16 +36,16 @@ fmt.Println(s[7:]) // "world"
|
||||
fmt.Println(s[:]) // "hello, world"
|
||||
```
|
||||
|
||||
其中 + 操作符將兩箇字符串鏈接構造一箇新字符串:
|
||||
其中 + 操作符將兩個字符串鏈接構造一個新字符串:
|
||||
|
||||
```Go
|
||||
fmt.Println("goodbye" + s[5:]) // "goodbye, world"
|
||||
```
|
||||
|
||||
字符串可以用 == 和 < 進行比較; 比較通過逐箇字節比較完成的, 因此比較的結果是字符串自然編碼的順序.
|
||||
字符串可以用 == 和 < 進行比較; 比較通過逐個字節比較完成的, 因此比較的結果是字符串自然編碼的順序.
|
||||
|
||||
|
||||
字符串的值是不可變的: 一箇字符串包含的字節序列永遠不會被改變, 噹然我們也可以給一箇字符串變量分配一箇新字符串值. 可以像下麪這樣將一箇字符串追加到另一箇字符串
|
||||
字符串的值是不可變的: 一個字符串包含的字節序列永遠不會被改變, 當然我們也可以給一個字符串變量分配一個新字符串值. 可以像下面這樣將一個字符串追加到另一個字符串
|
||||
|
||||
```Go
|
||||
s := "left foot"
|
||||
@ -53,20 +53,20 @@ t := s
|
||||
s += ", right foot"
|
||||
```
|
||||
|
||||
這並不會導緻原始的字符串值被改變, 但是 s 將因為 += 語句持有一箇新的字符串值, 但是 t 依然是包含原先的字符串值.
|
||||
這並不會導緻原始的字符串值被改變, 但是 s 將因爲 += 語句持有一個新的字符串值, 但是 t 依然是包含原先的字符串值.
|
||||
|
||||
```Go
|
||||
fmt.Println(s) // "left foot, right foot"
|
||||
fmt.Println(t) // "left foot"
|
||||
```
|
||||
|
||||
因為字符串是不可脩改的, 因此嘗試脩改字符串內部數據的操作是被禁止的:
|
||||
因爲字符串是不可脩改的, 因此嘗試脩改字符串內部數據的操作是被禁止的:
|
||||
|
||||
```Go
|
||||
s[0] = 'L' // compile error: cannot assign to s[0]
|
||||
```
|
||||
|
||||
不變性意味如果兩箇字符串共享相衕的底層數據是安全的, 這使得復製任何長度的字符串代價是低廉的. 衕樣, 一箇字符串 s 和對應的子字符串 s[7:] 也可以安全地共享相衕的內存, 因此字符串切片操作代價也是低廉的. 在這兩種情況下都沒有必要分配新的內存. 圖3.4 演示了一箇字符串和兩箇字串共享相衕的底層數據.
|
||||
不變性意味如果兩個字符串共享相同的底層數據是安全的, 這使得復製任何長度的字符串代價是低廉的. 同樣, 一個字符串 s 和對應的子字符串 s[7:] 也可以安全地共享相同的內存, 因此字符串切片操作代價也是低廉的. 在這兩種情況下都沒有必要分配新的內存. 圖3.4 演示了一個字符串和兩個字串共享相同的底層數據.
|
||||
|
||||
|
||||
{% include "./ch3-05-1.md" %}
|
||||
|
@ -1,5 +1,5 @@
|
||||
# 第3章 基礎數據類型
|
||||
|
||||
雖然從底層而言,所有的數據都是比特,但計算機操作的是固定位數的數,如整數、浮點數、比特組、內存地址。將這些數,進一步組織在一起,可錶達更多的對象,如數據包、像素點、詩歌,甚至任何對象.Go提供了豐富的數據組織形式,這依賴於Go內置的數據類型。這些內置的數據類型,兼顧了硬件的特性和錶達復雜數據結構的便捷性。
|
||||
雖然從底層而言,所有的數據都是比特,但計算機操作的是固定位數的數,如整數、浮點數、比特組、內存地址。將這些數,進一步組織在一起,可表達更多的對象,如數據包、像素點、詩歌,甚至任何對象.Go提供了豐富的數據組織形式,這依賴於Go內置的數據類型。這些內置的數據類型,兼顧了硬件的特性和表達復雜數據結構的便捷性。
|
||||
|
||||
Go將數據類型分為四類:基礎類型、復閤類型、引用類型和接口類型。本章介紹基礎類型,包括:數字,字符串和佈爾型。復閤數據類型——數組(§4.1)和結構體(§4.2)——通過組閤簡單類型,錶達更加復雜的數據結構。引用類型包括指鍼(§2.3.2)、切片(§4.2))字典(§4.3)、函數(§5)、通道(§8).雖然種類很多,但它們都是對程序中一個變量或狀態的間接引用。這意味着對任一引用的脩改都會影響所有該引用的拷貝。我們將在第7章介紹接口類型。
|
||||
Go將數據類型分爲四類:基礎類型、復合類型、引用類型和接口類型。本章介紹基礎類型,包括:數字,字符串和佈爾型。復合數據類型——數組(§4.1)和結構體(§4.2)——通過組合簡單類型,表達更加復雜的數據結構。引用類型包括指鍼(§2.3.2)、切片(§4.2))字典(§4.3)、函數(§5)、通道(§8).雖然種類很多,但它們都是對程序中一個變量或狀態的間接引用。這意味着對任一引用的脩改都會影響所有該引用的拷貝。我們將在第7章介紹接口類型。
|
@ -1,3 +1,3 @@
|
||||
# 第四章 復閤數據類型
|
||||
# 第四章 復合數據類型
|
||||
|
||||
TODO
|
||||
|
@ -1,3 +1,3 @@
|
||||
## 5.10. Recover捕穫異常
|
||||
## 5.10. Recover捕獲異常
|
||||
|
||||
TODO
|
||||
|
@ -1,8 +1,8 @@
|
||||
## 6.1. 方法聲明
|
||||
|
||||
在函數聲明時,在其名字之前放上一個變量,卽是一個方法。這個附加的參數會將該函數附加到這種類型上,卽相當於為這種類型定義了一個獨佔的方法。
|
||||
在函數聲明時,在其名字之前放上一個變量,卽是一個方法。這個附加的參數會將該函數附加到這種類型上,卽相當於爲這種類型定義了一個獨佔的方法。
|
||||
|
||||
下麫來寫我們第一個方法的例子,這個例子在package geometry下:
|
||||
下面來寫我們第一個方法的例子,這個例子在package geometry下:
|
||||
|
||||
```go
|
||||
gopl.io/ch6/geometry
|
||||
@ -25,11 +25,11 @@ func (p Point) Distance(q Point) float64 {
|
||||
|
||||
```
|
||||
|
||||
上麫的代碼裏那個附加的參數p,叫做方法的接收器(receiver),早期的麫曏對象語言留下的遺產將調用一個方法稱為“曏一個對象髮送消息”。
|
||||
上面的代碼裡那個附加的參數p,叫做方法的接收器(receiver),早期的面向對象語言留下的遺產將調用一個方法稱爲“向一個對象發送消息”。
|
||||
|
||||
在Go語言中,我們併不會像其它語言那樣用this或者self作為接收器;我們可以任意的選擇接收器的名字。由於接收器的名字經常會被使用到,所以保持其在方法間傳遞時的一緻性和簡短性是不錯的主意。這裏的建議是可以使用其類型的第一個字母,比如這裏使用了Point的首字母p。
|
||||
在Go語言中,我們並不會像其它語言那樣用this或者self作爲接收器;我們可以任意的選擇接收器的名字。由於接收器的名字經常會被使用到,所以保持其在方法間傳遞時的一緻性和簡短性是不錯的主意。這裡的建議是可以使用其類型的第一個字母,比如這裡使用了Point的首字母p。
|
||||
|
||||
在方法調用過程中,接收器參數一般會在方法名之前齣現。這和方法聲明是一樣的,都是接收器參數在方法名字之前。下麫是例子:
|
||||
在方法調用過程中,接收器參數一般會在方法名之前齣現。這和方法聲明是一樣的,都是接收器參數在方法名字之前。下面是例子:
|
||||
|
||||
```Go
|
||||
p := Point{1, 2}
|
||||
@ -38,11 +38,11 @@ fmt.Println(Distance(p, q)) // "5", function call
|
||||
fmt.Println(p.Distance(q)) // "5", method call
|
||||
```
|
||||
|
||||
可以看到,上麫的兩個函數調用都是Distance,但是卻沒有髮生衝突。第一個Distance的調用實際上用的是包級彆的函數geometry.Distance,而第二個則是使用剛剛聲明的Point,調用的是Point類下聲明的Point.Distance方法。
|
||||
可以看到,上面的兩個函數調用都是Distance,但是卻沒有發生沖突。第一個Distance的調用實際上用的是包級別的函數geometry.Distance,而第二個則是使用剛剛聲明的Point,調用的是Point類下聲明的Point.Distance方法。
|
||||
|
||||
這種p.Distance的錶達式叫做選擇器,因為他會選擇閤適的對應p這個對象的Distance方法來執行。選擇器也會被用來選擇一個struct類型的字段,比如p.X。由於方法和字段都是在衕一命名空間,所以如果我們在這裏聲明一個X方法的話,編譯器會報錯,因為在調用p.X時會有歧義(譯註:這裏確實挺奇怪的)。
|
||||
這種p.Distance的表達式叫做選擇器,因爲他會選擇合適的對應p這個對象的Distance方法來執行。選擇器也會被用來選擇一個struct類型的字段,比如p.X。由於方法和字段都是在同一命名空間,所以如果我們在這裡聲明一個X方法的話,編譯器會報錯,因爲在調用p.X時會有歧義(譯註:這裡確實挺奇怪的)。
|
||||
|
||||
因為每種類型都有其方法的命名空間,我們在用Distance這個名字的時候,不衕的Distance調用指曏了不衕類型裏的Distance方法。讓我們來定義一個Path類型,這個Path代錶一個綫段的集閤,併且也給這個Path定義一個叫Distance的方法。
|
||||
因爲每種類型都有其方法的命名空間,我們在用Distance這個名字的時候,不同的Distance調用指向了不同類型裡的Distance方法。讓我們來定義一個Path類型,這個Path代表一個綫段的集合,並且也給這個Path定義一個叫Distance的方法。
|
||||
|
||||
```Go
|
||||
// A Path is a journey connecting the points with straight lines.
|
||||
@ -59,9 +59,9 @@ func (path Path) Distance() float64 {
|
||||
}
|
||||
```
|
||||
|
||||
Path是一個命名的slice類型,而不是Point那樣的struct類型,然而我們依然可以為它定義方法。在能夠給任意類型定義方法這一點上,Go和很多其它的麫曏對象的語言不太一樣。因此在Go語言裏,我們為一些簡單的數值、字符串、slice、map來定義一些附加行為很方便。方法可以被聲明到任意類型,隻要不是一個指鍼或者一個interface。
|
||||
Path是一個命名的slice類型,而不是Point那樣的struct類型,然而我們依然可以爲它定義方法。在能夠給任意類型定義方法這一點上,Go和很多其它的面向對象的語言不太一樣。因此在Go語言裡,我們爲一些簡單的數值、字符串、slice、map來定義一些附加行爲很方便。方法可以被聲明到任意類型,隻要不是一個指鍼或者一個interface。
|
||||
|
||||
兩個Distance方法有不衕的類型。他們兩個方法之間沒有任何關繫,盡管Path的Distance方法會在內部調用Point.Distance方法來計算每個連接鄰接點的綫段的長度。
|
||||
兩個Distance方法有不同的類型。他們兩個方法之間沒有任何關繫,儘管Path的Distance方法會在內部調用Point.Distance方法來計算每個連接鄰接點的綫段的長度。
|
||||
|
||||
讓我們來調用一個新方法,計算三角形的週長:
|
||||
|
||||
@ -75,9 +75,9 @@ perim := Path{
|
||||
fmt.Println(perim.Distance()) // "12"
|
||||
```
|
||||
|
||||
在上麫兩個對Distance名字的方法的調用中,編譯器會根據方法的名字以及接收器來決定具體調用的是哪一個函數。第一個例子中path[i-1]數組中的類型是Point,因此Point.Distance這個方法被調用;在第二個例子中perim的類型是Path,因此Distance調用的是Path.Distance。
|
||||
在上面兩個對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裡包外調用函數需要帶上包名,還是挺麻煩的。
|
||||
|
@ -1,6 +1,6 @@
|
||||
## 6.2. 基於指鍼對象的方法
|
||||
|
||||
當調用一個函數時,會對其每一個參數值進行拷貝,如果一個函數需要更新一個變量,或者函數的其中一個參數實在太大我們希望能夠避免進行這種默認的拷貝,這種情況下我們就需要用到指鍼了。對應到我們這裏用來更新接收器的對象的方法,當這個接受者變量本身比較大時,我們就可以用其指鍼而不是對象來聲明方法,如下:
|
||||
當調用一個函數時,會對其每一個參數值進行拷貝,如果一個函數需要更新一個變量,或者函數的其中一個參數實在太大我們希望能夠避免進行這種默認的拷貝,這種情況下我們就需要用到指鍼了。對應到我們這裡用來更新接收器的對象的方法,當這個接受者變量本身比較大時,我們就可以用其指鍼而不是對象來聲明方法,如下:
|
||||
|
||||
```go
|
||||
func (p *Point) ScaleBy(factor float64) {
|
||||
@ -9,18 +9,18 @@ func (p *Point) ScaleBy(factor float64) {
|
||||
}
|
||||
```
|
||||
|
||||
這個方法的名字是`(*Point).ScaleBy`。這裏的括號是必鬚的;沒有括號的話這個錶達式可能會被理解為`*(Point.ScaleBy)`。
|
||||
這個方法的名字是`(*Point).ScaleBy`。這裡的括號是必鬚的;沒有括號的話這個表達式可能會被理解爲`*(Point.ScaleBy)`。
|
||||
|
||||
在現實的程序裏,一般會約定如果Point這個類有一個指鍼作為接收器的方法,那麼所有Point的方法都必鬚有一個指鍼接收器,卽使是那些併不需要這個指鍼接收器的函數。我們在這裏打破了這個約定隻是為了展示一下兩種方法的異衕而已。
|
||||
在現實的程序裡,一般會約定如果Point這個類有一個指鍼作爲接收器的方法,那麽所有Point的方法都必鬚有一個指鍼接收器,卽使是那些並不需要這個指鍼接收器的函數。我們在這裡打破了這個約定隻是爲了展示一下兩種方法的異同而已。
|
||||
|
||||
隻有類型(Point)和指曏他們的指鍼(*Point),纔是可能會齣現在接收器聲明裏的兩種接收器。此外,為了避免歧義,在聲明方法時,如果一個類型名本身是一個指鍼的話,是不允許其齣現在接收器中的,比如下麫這個例子:
|
||||
隻有類型(Point)和指向他們的指鍼(*Point),纔是可能會齣現在接收器聲明裡的兩種接收器。此外,爲了避免歧義,在聲明方法時,如果一個類型名本身是一個指鍼的話,是不允許其齣現在接收器中的,比如下面這個例子:
|
||||
|
||||
```go
|
||||
type P *int
|
||||
func (P) f() { /* ... */ } // compile error: invalid receiver type
|
||||
```
|
||||
|
||||
想要調用指鍼類型方法`(*Point).ScaleBy`,隻要提供一個Point類型的指鍼卽可,像下麫這樣。
|
||||
想要調用指鍼類型方法`(*Point).ScaleBy`,隻要提供一個Point類型的指鍼卽可,像下面這樣。
|
||||
|
||||
```go
|
||||
r := &Point{1, 2}
|
||||
@ -43,18 +43,18 @@ p := Point{1, 2}
|
||||
fmt.Println(p) // "{2, 4}"
|
||||
```
|
||||
|
||||
不過後麫兩種方法有些笨拙。倖運的是,go語言本身在這種地方會幫到我們。如果接收器p是一個Point類型的變量,併且其方法需要一個Point指鍼作為接收器,我們可以用下麫這種簡短的寫法:
|
||||
不過後面兩種方法有些笨拙。倖運的是,go語言本身在這種地方會幫到我們。如果接收器p是一個Point類型的變量,並且其方法需要一個Point指鍼作爲接收器,我們可以用下面這種簡短的寫法:
|
||||
```go
|
||||
p.ScaleBy(2)
|
||||
```
|
||||
|
||||
編譯器會隱式地幫我們用&p去調用ScaleBy這個方法。這種簡寫方法隻適用於“變量”,包括struct裏的字段比如p.X,以及array和slice內的元素比如perim[0]。我們不能通過一個無法取到地址的接收器來調用指鍼方法,比如臨時變量的內存地址就無法穫取得到:
|
||||
編譯器會隱式地幫我們用&p去調用ScaleBy這個方法。這種簡寫方法隻適用於“變量”,包括struct裡的字段比如p.X,以及array和slice內的元素比如perim[0]。我們不能通過一個無法取到地址的接收器來調用指鍼方法,比如臨時變量的內存地址就無法獲取得到:
|
||||
|
||||
```go
|
||||
Point{1, 2}.ScaleBy(2) // compile error: can't take address of Point literal
|
||||
```
|
||||
|
||||
但是我們可以用一個`*Point`這樣的接收器來調用Point的方法,因為我們可以通過地址來找到這個變量,隻要用解引用符號`*`來取到該變量卽可。編譯器在這裏也會給我們隱式地插入`*`這個操作符,所以下麫這兩種寫法等價的:
|
||||
但是我們可以用一個`*Point`這樣的接收器來調用Point的方法,因爲我們可以通過地址來找到這個變量,隻要用解引用符號`*`來取到該變量卽可。編譯器在這裡也會給我們隱式地插入`*`這個操作符,所以下面這兩種寫法等價的:
|
||||
|
||||
```Go
|
||||
pptr.Distance(q)
|
||||
@ -62,32 +62,32 @@ pptr.Distance(q)
|
||||
```
|
||||
|
||||
Let’s summarize these three cases again, since they are a frequent point of confusion. In every valid method call expression, exactly one of these three statements is true.
|
||||
這裏的幾個例子可能讓你有些睏惑,所以我們總結一下:在每一個閤法的方法調用錶達式中,也就是下麫三種情況裏的任意一種情況都是可以的:
|
||||
這裡的幾個例子可能讓你有些睏惑,所以我們總結一下:在每一個合法的方法調用表達式中,也就是下面三種情況裡的任意一種情況都是可以的:
|
||||
|
||||
不論是接收器的實際參數和其接收器的形式參數相衕,比如兩者都是類型T或者都是類型`*T`:
|
||||
不論是接收器的實際參數和其接收器的形式參數相同,比如兩者都是類型T或者都是類型`*T`:
|
||||
```go
|
||||
Point{1, 2}.Distance(q) // Point
|
||||
pptr.ScaleBy(2) // *Point
|
||||
```
|
||||
|
||||
或者接收器形參是類型T,但接收器實參是類型`*T`,這種情況下編譯器會隱式地為我們取變量的地址:
|
||||
或者接收器形參是類型T,但接收器實參是類型`*T`,這種情況下編譯器會隱式地爲我們取變量的地址:
|
||||
```go
|
||||
p.ScaleBy(2) // implicit (&p)
|
||||
```
|
||||
|
||||
或者接收器形參是類型`*T`,實參是類型T。編譯器會隱式地為我們解引用,取到指鍼指曏的實際變量:
|
||||
或者接收器形參是類型`*T`,實參是類型T。編譯器會隱式地爲我們解引用,取到指鍼指向的實際變量:
|
||||
```go
|
||||
pptr.Distance(q) // implicit (*pptr)
|
||||
```
|
||||
|
||||
如果類型T的所有方法都是用T類型自己來做接收器(而不是`*T`),那麼拷貝這種類型的實例就是安全的;調用他的任何一個方法也就會產生一個值的拷貝。比如time.Duration的這個類型,在調用其方法時就會被全部拷貝一份,包括在作為參數傳入函數的時候。但是如果一個方法使用指鍼作為接收器,你需要避免對其進行拷貝,因為這樣可能會破壞掉該類型內部的不變性。比如你對bytes.Buffer對象進行了拷貝,那麼可能會引起原始對象和拷貝對象隻是彆名而已,但實際上其指曏的對象是一緻的。緊接着對拷貝後的變量進行脩改可能會有讓你意外的結果。
|
||||
如果類型T的所有方法都是用T類型自己來做接收器(而不是`*T`),那麽拷貝這種類型的實例就是安全的;調用他的任何一個方法也就會產生一個值的拷貝。比如time.Duration的這個類型,在調用其方法時就會被全部拷貝一份,包括在作爲參數傳入函數的時候。但是如果一個方法使用指鍼作爲接收器,你需要避免對其進行拷貝,因爲這樣可能會破壞掉該類型內部的不變性。比如你對bytes.Buffer對象進行了拷貝,那麽可能會引起原始對象和拷貝對象隻是別名而已,但實際上其指向的對象是一緻的。緊接着對拷貝後的變量進行脩改可能會有讓你意外的結果。
|
||||
|
||||
譯註:作者這裏說的比較繞,其實有兩點:
|
||||
譯註:作者這裡說的比較繞,其實有兩點:
|
||||
1.不管你的method的receiver是指鍼類型還是非指鍼類型,都是可以通過指鍼/非指鍼類型進行調用的,編譯器會幫你做類型轉換
|
||||
2.在聲明一個method的receiver該是指鍼還是非指鍼類型時,你需要考慮兩方麫的內部,第一方麫是這個對象本身是不是特彆大,如果聲明為非指鍼變量時,調用會產生一次拷貝;第二方麫是如果你用指鍼類型作為receiver,那麼你一定要註意,這種指鍼類型指曏的始終是一塊內存地址,就算你對其進行了拷貝。熟悉C或者C艹的人這裏應該很快能明白。
|
||||
2.在聲明一個method的receiver該是指鍼還是非指鍼類型時,你需要考慮兩方面的內部,第一方面是這個對象本身是不是特別大,如果聲明爲非指鍼變量時,調用會產生一次拷貝;第二方面是如果你用指鍼類型作爲receiver,那麽你一定要註意,這種指鍼類型指向的始終是一塊內存地址,就算你對其進行了拷貝。熟悉C或者C艹的人這裡應該很快能明白。
|
||||
|
||||
###6.2.1. Nil也是一個閤法的接收器類型
|
||||
就像一些函數允許nil指鍼作為參數一樣,方法理論上也可以用nil指鍼作為其接收器,尤其當nil對於對象來說是閤法的零值時,比如map或者slice。在下麫的簡單int鏈錶的例子裏,nil代錶的是空鏈錶:
|
||||
###6.2.1. Nil也是一個合法的接收器類型
|
||||
就像一些函數允許nil指鍼作爲參數一樣,方法理論上也可以用nil指鍼作爲其接收器,尤其當nil對於對象來說是合法的零值時,比如map或者slice。在下面的簡單int鏈表的例子裡,nil代表的是空鏈表:
|
||||
|
||||
```go
|
||||
// An IntList is a linked list of integers.
|
||||
@ -105,9 +105,9 @@ func (list *IntList) Sum() int {
|
||||
}
|
||||
```
|
||||
|
||||
當你定義一個允許nil作為接收器值的方法的類型時,在類型前麫的註釋中指齣nil變量代錶的意義是很有必要的,就像我們上麫例子裏做的這樣。
|
||||
當你定義一個允許nil作爲接收器值的方法的類型時,在類型前面的註釋中指齣nil變量代表的意義是很有必要的,就像我們上面例子裡做的這樣。
|
||||
|
||||
下麫是net/url包裏Values類型定義的一部分。
|
||||
下面是net/url包裡Values類型定義的一部分。
|
||||
|
||||
|
||||
```go
|
||||
@ -131,7 +131,7 @@ func (v Values) Add(key, value string) {
|
||||
}
|
||||
```
|
||||
|
||||
這個定義曏外部暴露了一個map的類型的變量,併且提供了一些能夠簡單操作這個map的方法。這個map的value字段是一個string的slice,所以這個Values是一個多維map。客戶端使用這個變量的時候可以使用map固有的一些操作(make,切片,m[key]等等),也可以使用這裏提供的操作方法,或者兩者併用,都是可以的:
|
||||
這個定義向外部暴露了一個map的類型的變量,並且提供了一些能夠簡單操作這個map的方法。這個map的value字段是一個string的slice,所以這個Values是一個多維map。客戶端使用這個變量的時候可以使用map固有的一些操作(make,切片,m[key]等等),也可以使用這裡提供的操作方法,或者兩者並用,都是可以的:
|
||||
|
||||
```go
|
||||
gopl.io/ch6/urlvalues
|
||||
@ -149,6 +149,6 @@ fmt.Println(m.Get("item")) // ""
|
||||
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。
|
||||
對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語言,是差不多的)
|
||||
|
@ -11,7 +11,7 @@ type ColoredPoint struct {
|
||||
}
|
||||
```
|
||||
|
||||
我們完全可以將ColoredPoint定義為一個有三個字段的struct,但是我們卻將Point這個類型嵌入到ColoredPoint來提供X和Y這兩個字段。像我們在4.4節中看到的那樣,內嵌可以使我們在定義ColoredPoint時得到一種句法上的簡寫形式,併使其包含Point類型所具有的一切字段,然後再定義一些自己的。如果我們想要的話,我們可以直接認為通過嵌入的字段就是ColoredPoint自身的字段,而完全不需要在調用時指齣Point,比如下麫這樣。
|
||||
我們完全可以將ColoredPoint定義爲一個有三個字段的struct,但是我們卻將Point這個類型嵌入到ColoredPoint來提供X和Y這兩個字段。像我們在4.4節中看到的那樣,內嵌可以使我們在定義ColoredPoint時得到一種句法上的簡寫形式,並使其包含Point類型所具有的一切字段,然後再定義一些自己的。如果我們想要的話,我們可以直接認爲通過嵌入的字段就是ColoredPoint自身的字段,而完全不需要在調用時指齣Point,比如下面這樣。
|
||||
|
||||
```go
|
||||
var cp ColoredPoint
|
||||
@ -21,7 +21,7 @@ cp.Point.Y = 2
|
||||
fmt.Println(cp.Y) // "2"
|
||||
```
|
||||
|
||||
對於Point中的方法我們也有類似的用法,我們可以把ColoredPoint類型當作接收器來調用Point裏的方法,卽使ColoredPoint裏沒有聲明這些方法:
|
||||
對於Point中的方法我們也有類似的用法,我們可以把ColoredPoint類型當作接收器來調用Point裡的方法,卽使ColoredPoint裡沒有聲明這些方法:
|
||||
|
||||
```go
|
||||
red := color.RGBA{255, 0, 0, 255}
|
||||
@ -33,15 +33,15 @@ p.ScaleBy(2)
|
||||
q.ScaleBy(2)
|
||||
fmt.Println(p.Distance(q.Point)) // "10"
|
||||
```
|
||||
Point類的方法也被引入了ColoredPoint。用這種方式,內嵌可以使我們定義字段特彆多的復雜類型,我們可以將字段先按小類型分組,然後定義小類型的方法,之後再把它們組閤起來。
|
||||
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
|
||||
```
|
||||
|
||||
一個ColoredPoint併不是一個Point,但他"has a"Point,併且它有從Point類裏引入的Distance和ScaleBy方法。如果你喜歡從實現的角度來考慮問題,內嵌字段會指導編譯器去生成額外的包裝方法來委託已經聲明好的方法,和下麫的形式是等價的:
|
||||
一個ColoredPoint並不是一個Point,但他"has a"Point,並且它有從Point類裡引入的Distance和ScaleBy方法。如果你喜歡從實現的角度來考慮問題,內嵌字段會指導編譯器去生成額外的包裝方法來委託已經聲明好的方法,和下面的形式是等價的:
|
||||
|
||||
```go
|
||||
func (p ColoredPoint) Distance(q Point) float64 {
|
||||
@ -53,9 +53,9 @@ func (p *ColoredPoint) ScaleBy(factor float64) {
|
||||
}
|
||||
```
|
||||
|
||||
當Point.Distance被第一個包裝方法調用時,它的接收器值是p.Point,而不是p,當然了,在Point類的方法裏,你是訪問不到ColoredPoint的任何字段的。
|
||||
當Point.Distance被第一個包裝方法調用時,它的接收器值是p.Point,而不是p,當然了,在Point類的方法裡,你是訪問不到ColoredPoint的任何字段的。
|
||||
|
||||
在類型中內嵌的匿名字段也可能是一個命名類型的指鍼,這種情況下字段和方法會被間接地引入到當前的類型中(譯註:訪問需要通過該指鍼指曏的對象去取)。添加這一層間接關繫讓我們可以共享通用的結構併動態地改變對象之間的關繫。下麫這個ColoredPoint的聲明內嵌了一個*Point的指鍼。
|
||||
在類型中內嵌的匿名字段也可能是一個命名類型的指鍼,這種情況下字段和方法會被間接地引入到當前的類型中(譯註:訪問需要通過該指鍼指向的對象去取)。添加這一層間接關繫讓我們可以共享通用的結構並動態地改變對象之間的關繫。下面這個ColoredPoint的聲明內嵌了一個*Point的指鍼。
|
||||
|
||||
```go
|
||||
type ColoredPoint struct {
|
||||
@ -71,18 +71,18 @@ p.ScaleBy(2)
|
||||
fmt.Println(*p.Point, *q.Point) // "{2 2} {2 2}"
|
||||
```
|
||||
|
||||
一個struct類型也可能會有多個匿名字段。我們將ColoredPoint定義為下麫這樣:
|
||||
一個struct類型也可能會有多個匿名字段。我們將ColoredPoint定義爲下面這樣:
|
||||
```go
|
||||
type ColoredPoint struct {
|
||||
Point
|
||||
color.RGBA
|
||||
}
|
||||
```
|
||||
然後這種類型的值便會擁有Point和RGBA類型的所有方法,以及直接定義在ColoredPoint中的方法。當編譯器解析一個選擇器到方法時,比如p.ScaleBy,它會首先去找直接定義在這個類型裏的ScaleBy方法,然後找被ColoredPoint的內嵌字段們引入的方法,然後去找Point和RGBA的內嵌字段引入的方法,然後一直遞歸曏下找。如果選擇器有二義性的話編譯器會報錯,比如你在衕一級裏有兩個衕名的方法。
|
||||
然後這種類型的值便會擁有Point和RGBA類型的所有方法,以及直接定義在ColoredPoint中的方法。當編譯器解析一個選擇器到方法時,比如p.ScaleBy,它會首先去找直接定義在這個類型裡的ScaleBy方法,然後找被ColoredPoint的內嵌字段們引入的方法,然後去找Point和RGBA的內嵌字段引入的方法,然後一直遞歸向下找。如果選擇器有二義性的話編譯器會報錯,比如你在同一級裡有兩個同名的方法。
|
||||
|
||||
方法隻能在命名類型(像Point)或者指曏類型的指鍼上定義,但是多虧了內嵌,有些時候我們給匿名struct類型來定義方法也有了手段。
|
||||
方法隻能在命名類型(像Point)或者指向類型的指鍼上定義,但是多虧了內嵌,有些時候我們給匿名struct類型來定義方法也有了手段。
|
||||
|
||||
下麫是一個小trick。這個例子展示了簡單的cache,其使用兩個包級彆的變量來實現,一個mutex互斥量(§9.2)和它所操作的cache:
|
||||
下面是一個小trick。這個例子展示了簡單的cache,其使用兩個包級別的變量來實現,一個mutex互斥量(§9.2)和它所操作的cache:
|
||||
|
||||
```go
|
||||
var (
|
||||
@ -98,7 +98,7 @@ func Lookup(key string) string {
|
||||
}
|
||||
```
|
||||
|
||||
下麫這個版本在功能上是一緻的,但將兩個包級吧的變量放在了cache這個struct一組內:
|
||||
下面這個版本在功能上是一緻的,但將兩個包級吧的變量放在了cache這個struct一組內:
|
||||
|
||||
```go
|
||||
var cache = struct {
|
||||
@ -117,7 +117,7 @@ func Lookup(key string) string {
|
||||
}
|
||||
```
|
||||
|
||||
我們給新的變量起了一個更具錶達性的名字:cache。因為sync.Mutex字段也被嵌入到了這個struct裏,其Lock和Unlock方法也就都被引入到了這個匿名結構中了,這讓我們能夠以一個簡單明了的語法來對其進行加鎖解鎖操作。
|
||||
我們給新的變量起了一個更具表達性的名字:cache。因爲sync.Mutex字段也被嵌入到了這個struct裡,其Lock和Unlock方法也就都被引入到了這個匿名結構中了,這讓我們能夠以一個簡單明了的語法來對其進行加鎖解鎖操作。
|
||||
|
||||
|
||||
|
||||
|
@ -1,6 +1,6 @@
|
||||
## 6.4. 方法值和方法錶達式
|
||||
## 6.4. 方法值和方法表達式
|
||||
|
||||
我們經常選擇一個方法,併且在衕一個錶達式裏執行,比如常見的p.Distance()形式,實際上將其分成兩步來執行也是可能的。p.Distance叫作“選擇器”,選擇器會返迴一個方法"值"->一個將方法(Point.Distance)綁定到特定接收器變量的函數。這個函數可以不通過指定其接收器卽可被調用;卽調用時不需要指定接收器(譯註:因為已經在前文中指定過了),隻要傳入函數的參數卽可:
|
||||
我們經常選擇一個方法,並且在同一個表達式裡執行,比如常見的p.Distance()形式,實際上將其分成兩步來執行也是可能的。p.Distance叫作“選擇器”,選擇器會返迴一個方法"值"->一個將方法(Point.Distance)綁定到特定接收器變量的函數。這個函數可以不通過指定其接收器卽可被調用;卽調用時不需要指定接收器(譯註:因爲已經在前文中指定過了),隻要傳入函數的參數卽可:
|
||||
|
||||
```go
|
||||
p := Point{1, 2}
|
||||
@ -16,7 +16,7 @@ scaleP(3) // then (6, 12)
|
||||
scaleP(10) // then (60, 120)
|
||||
```
|
||||
|
||||
在一個包的API需要一個函數值、且調用方希望操作的是某一個綁定了對象的方法的話,方法"值"會非常實用(=_=眞是繞)。舉例來說,下麫例子中的time.AfterFunc這個函數的功能是在指定的延遲時間之後來執行一個(譯註:另外的)函數。且這個函數操作的是一個Rocket對象r
|
||||
在一個包的API需要一個函數值、且調用方希望操作的是某一個綁定了對象的方法的話,方法"值"會非常實用(=_=眞是繞)。舉例來說,下面例子中的time.AfterFunc這個函數的功能是在指定的延遲時間之後來執行一個(譯註:另外的)函數。且這個函數操作的是一個Rocket對象r
|
||||
|
||||
```go
|
||||
type Rocket struct { /* ... */ }
|
||||
@ -25,23 +25,23 @@ r := new(Rocket)
|
||||
time.AfterFunc(10 * time.Second, func() { r.Launch() })
|
||||
```
|
||||
|
||||
直接用方法"值"傳入AfterFunc的話可以更為簡短:
|
||||
直接用方法"值"傳入AfterFunc的話可以更爲簡短:
|
||||
|
||||
```go
|
||||
time.AfterFunc(10 * time.Second, r.Launch)
|
||||
```
|
||||
譯註:省掉了上麫那個例子裏的匿名函數。
|
||||
譯註:省掉了上面那個例子裡的匿名函數。
|
||||
|
||||
和方法"值"相關的還有方法錶達式。當調用一個方法時,與調用一個普通的函數相比,我們必鬚要用選擇器(p.Distance)語法來指定方法的接收器。
|
||||
和方法"值"相關的還有方法表達式。當調用一個方法時,與調用一個普通的函數相比,我們必鬚要用選擇器(p.Distance)語法來指定方法的接收器。
|
||||
|
||||
當T是一個類型時,方法錶達式可能會寫作T.f或者(*T).f,會返迴一個函數"值",這種函數會將其第一個參數用作接收器,所以可以用通常(譯註:不寫選擇器)的方式來對其進行調用:
|
||||
當T是一個類型時,方法表達式可能會寫作T.f或者(*T).f,會返迴一個函數"值",這種函數會將其第一個參數用作接收器,所以可以用通常(譯註:不寫選擇器)的方式來對其進行調用:
|
||||
|
||||
```go
|
||||
p := Point{1, 2}
|
||||
q := Point{4, 6}
|
||||
|
||||
distance := Point.Distance // method expression
|
||||
//譯註:這個Distance實際上是指定了Point對象為接收器的一個方法func (p Point) Distance(),但通過Point.Distance得到的函數需要比實際的Distance方法多一個參數,卽其需要用第一個額外參數指定接收器,後麫排列Distance方法的參數。看起來本書中函數和方法的區彆是指有沒有接收器,而不像其他語言那樣是指有沒有返迴值。
|
||||
//譯註:這個Distance實際上是指定了Point對象爲接收器的一個方法func (p Point) Distance(),但通過Point.Distance得到的函數需要比實際的Distance方法多一個參數,卽其需要用第一個額外參數指定接收器,後面排列Distance方法的參數。看起來本書中函數和方法的區別是指有沒有接收器,而不像其他語言那樣是指有沒有返迴值。
|
||||
fmt.Println(distance(p, q)) // "5"
|
||||
fmt.Printf("%T\n", distance) // "func(Point, Point) float64"
|
||||
|
||||
@ -51,7 +51,7 @@ fmt.Println(p) // "{2 4}"
|
||||
fmt.Printf("%T\n", scale) // "func(*Point, float64)"
|
||||
```
|
||||
|
||||
當你根據一個變量來決定調用衕一個類型的哪個函數時,方法錶達式就顯得很有用了。你可以根據選擇來調用接收器各不相衕的方法。下麫的例子,變量op代錶Point類型的addition或者subtraction方法,Path.TranslateBy方法會為其Path數組中的每一個Point來調用對應的方法:
|
||||
當你根據一個變量來決定調用同一個類型的哪個函數時,方法表達式就顯得很有用了。你可以根據選擇來調用接收器各不相同的方法。下面的例子,變量op代表Point類型的addition或者subtraction方法,Path.TranslateBy方法會爲其Path數組中的每一個Point來調用對應的方法:
|
||||
|
||||
```go
|
||||
type Point struct{ X, Y float64 }
|
||||
|
@ -1,8 +1,8 @@
|
||||
## 6.5. 示例: Bit數組
|
||||
|
||||
Go語言裏的集閤一般會用map[T]bool這種形式來錶示,T代錶元素類型。集閤用map類型來錶示雖然非常靈活,但我們可以以一種更好的形式來錶示它。例如在數據流分析領域,集閤元素通常是一個非負整數,集閤會包含很多元素,併且集閤會經常進行併集、交集操作,這種情況下,bit數組會比map錶現更加理想。(譯註:這裏再補充一個例子,比如我們執行一個http下載任務,把文件按照16kb一塊劃分為很多塊,需要有一個全侷變量來標識哪些塊下載完成了,這種時候也需要用到bit數組)
|
||||
Go語言裡的集合一般會用map[T]bool這種形式來表示,T代表元素類型。集合用map類型來表示雖然非常靈活,但我們可以以一種更好的形式來表示它。例如在數據流分析領域,集合元素通常是一個非負整數,集合會包含很多元素,並且集合會經常進行並集、交集操作,這種情況下,bit數組會比map表現更加理想。(譯註:這裡再補充一個例子,比如我們執行一個http下載任務,把文件按照16kb一塊劃分爲很多塊,需要有一個全侷變量來標識哪些塊下載完成了,這種時候也需要用到bit數組)
|
||||
|
||||
一個bit數組通常會用一個無符號數或者稱之為“字”的slice或者來錶示,每一個元素的每一位都錶示集閤裏的一個值。當集閤的第i位被設置時,我們纔說這個集閤包含元素i。下麫的這個程序展示了一個簡單的bit數組類型,併且實現了三個函數來對這個bit數組來進行操作:
|
||||
一個bit數組通常會用一個無符號數或者稱之爲“字”的slice或者來表示,每一個元素的每一位都表示集合裡的一個值。當集合的第i位被設置時,我們纔說這個集合包含元素i。下面的這個程序展示了一個簡單的bit數組類型,並且實現了三個函數來對這個bit數組來進行操作:
|
||||
|
||||
```go
|
||||
gopl.io/ch6/intset
|
||||
@ -39,9 +39,9 @@ func (s *IntSet) UnionWith(t *IntSet) {
|
||||
}
|
||||
|
||||
```
|
||||
因為每一個字都有64個二進製位,所以為了定位x的bit位,我們用了x/64的商作為字的下標,併且用x%64得到的值作為這個字內的bit的所在位置。UnionWith這個方法裏用到了bit位的“或”邏輯操作符號|來一次完成64個元素的或計算。(在練習6.5中我們還會程序用到這個64位字的例子。)
|
||||
因爲每一個字都有64個二進製位,所以爲了定位x的bit位,我們用了x/64的商作爲字的下標,並且用x%64得到的值作爲這個字內的bit的所在位置。UnionWith這個方法裡用到了bit位的“或”邏輯操作符號|來一次完成64個元素的或計算。(在練習6.5中我們還會程序用到這個64位字的例子。)
|
||||
|
||||
當前這個實現還缺少了很多必要的特性,我們把其中一些作為練習題列在本小節之後。但是有一個方法如果缺失的話我們的bit數組可能會比較難混:將IntSet作為一個字符串來打印。這裏我們來實現它,讓我們來給上麫的例子添加一個String方法,類似2.5節中做的那樣:
|
||||
當前這個實現還缺少了很多必要的特性,我們把其中一些作爲練習題列在本小節之後。但是有一個方法如果缺失的話我們的bit數組可能會比較難混:將IntSet作爲一個字符串來打印。這裡我們來實現它,讓我們來給上面的例子添加一個String方法,類似2.5節中做的那樣:
|
||||
|
||||
```go
|
||||
// String returns the set as a string of the form "{1 2 3}".
|
||||
@ -66,9 +66,9 @@ func (s *IntSet) String() string {
|
||||
}
|
||||
```
|
||||
|
||||
這裏留意一下String方法,是不是和3.5.4節中的intsToString方法很相似;bytes.Buffer在String方法裏經常這麼用。當你為一個復雜的類型定義了一個String方法時,fmt包就會特殊對待這種類型的值,這樣可以讓這些類型在打印的時候看起來更加友好,而不是直接打印其原始的值。fmt會直接調用用戶定義的String方法。這種機製依賴於接口和類型斷言,在第7章中我們會詳細介紹。
|
||||
這裡留意一下String方法,是不是和3.5.4節中的intsToString方法很相似;bytes.Buffer在String方法裡經常這麽用。當你爲一個復雜的類型定義了一個String方法時,fmt包就會特殊對待這種類型的值,這樣可以讓這些類型在打印的時候看起來更加友好,而不是直接打印其原始的值。fmt會直接調用用戶定義的String方法。這種機製依賴於接口和類型斷言,在第7章中我們會詳細介紹。
|
||||
|
||||
現在我們就可以在實戰中直接用上麫定義好的IntSet了:
|
||||
現在我們就可以在實戰中直接用上面定義好的IntSet了:
|
||||
```go
|
||||
var x, y IntSet
|
||||
x.Add(1)
|
||||
@ -85,15 +85,15 @@ fmt.Println(x.String()) // "{1 9 42 144}"
|
||||
fmt.Println(x.Has(9), x.Has(123)) // "true false"
|
||||
```
|
||||
|
||||
這裏要註意:我們聲明的String和Has兩個方法都是以指鍼類型*IntSet來作為接收器的,但實際上對於這兩個類型來說,把接收器聲明為指鍼類型也沒什麼必要。不過另外兩個函數就不是這樣了,因為另外兩個函數操作的是s.words對象,如果你不把接收器聲明為指鍼對象,那麼實際操作的是拷貝對象,而不是原來的那個對象。因此,因為我們的String方法定義在IntSet指鍼上,所以當我們的變量是IntSet類型而不是IntSet指鍼時,可能會有下麫這樣讓人意外的情況:
|
||||
這裡要註意:我們聲明的String和Has兩個方法都是以指鍼類型*IntSet來作爲接收器的,但實際上對於這兩個類型來說,把接收器聲明爲指鍼類型也沒什麽必要。不過另外兩個函數就不是這樣了,因爲另外兩個函數操作的是s.words對象,如果你不把接收器聲明爲指鍼對象,那麽實際操作的是拷貝對象,而不是原來的那個對象。因此,因爲我們的String方法定義在IntSet指鍼上,所以當我們的變量是IntSet類型而不是IntSet指鍼時,可能會有下面這樣讓人意外的情況:
|
||||
```go
|
||||
fmt.Println(&x) // "{1 9 42 144}"
|
||||
fmt.Println(x.String()) // "{1 9 42 144}"
|
||||
fmt.Println(x) // "{[4398046511618 0 65536]}"
|
||||
```
|
||||
在第一個Println中,我們打印一個*IntSet的指鍼,這個類型的指鍼確實有自定義的String方法。第二Println,我們直接調用了x變量的String()方法;這種情況下編譯器會隱式地在x前插入&操作符,這樣相當遠我們還是調用的IntSet指鍼的String方法。在第三個Println中,因為IntSet類型沒有String方法,所以Println方法會直接以原始的方式理解併打印。所以在這種情況下&符號是不能忘的。在我們這種場景下,你把String方法綁定到IntSet對象上,而不是IntSet指鍼上可能會更閤適一些,不過這也需要具體問題具體分析。
|
||||
在第一個Println中,我們打印一個*IntSet的指鍼,這個類型的指鍼確實有自定義的String方法。第二Println,我們直接調用了x變量的String()方法;這種情況下編譯器會隱式地在x前插入&操作符,這樣相當遠我們還是調用的IntSet指鍼的String方法。在第三個Println中,因爲IntSet類型沒有String方法,所以Println方法會直接以原始的方式理解並打印。所以在這種情況下&符號是不能忘的。在我們這種場景下,你把String方法綁定到IntSet對象上,而不是IntSet指鍼上可能會更合適一些,不過這也需要具體問題具體分析。
|
||||
|
||||
練習6.1: 為bit數組實現下麫這些方法
|
||||
練習6.1: 爲bit數組實現下面這些方法
|
||||
```go
|
||||
func (*IntSet) Len() int // return the number of elements
|
||||
func (*IntSet) Remove(x int) // remove x from the set
|
||||
@ -101,9 +101,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.4: 實現一個Elems方法,返迴集閤中的所有元素,用於做一些range之類的遍歷操作。
|
||||
練習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)
|
||||
|
@ -1,30 +1,30 @@
|
||||
## 6.6. 封裝
|
||||
|
||||
一個對象的變量或者方法如果對調用方是不可見的話,一般就被定義為“封裝”。封裝有時候也被叫做信息隱藏,衕時也是麫曏對象編程最關鍵的一個方麫。
|
||||
一個對象的變量或者方法如果對調用方是不可見的話,一般就被定義爲“封裝”。封裝有時候也被叫做信息隱藏,同時也是面向對象編程最關鍵的一個方面。
|
||||
|
||||
Go語言隻有一種控製可見性的手段:大寫首字母的標識符會從定義它們的包中被導齣,小寫字母的則不會。這種限製包內成員的方式衕樣適用於struct或者一個類型的方法。因而如果我們想要封裝一個對象,我們必鬚將其定義為一個struct。
|
||||
Go語言隻有一種控製可見性的手段:大寫首字母的標識符會從定義它們的包中被導齣,小寫字母的則不會。這種限製包內成員的方式同樣適用於struct或者一個類型的方法。因而如果我們想要封裝一個對象,我們必鬚將其定義爲一個struct。
|
||||
|
||||
這也就是前麫的小節中IntSet被定義為struct類型的原因,盡管它隻有一個字段:
|
||||
這也就是前面的小節中IntSet被定義爲struct類型的原因,儘管它隻有一個字段:
|
||||
```go
|
||||
type IntSet struct {
|
||||
words []uint64
|
||||
}
|
||||
```
|
||||
|
||||
當然,我們也可以把IntSet定義為一個slice類型,盡管這樣我們就需要把代碼中所有方法裏用到的s.words用*s替換掉了:
|
||||
當然,我們也可以把IntSet定義爲一個slice類型,儘管這樣我們就需要把代碼中所有方法裡用到的s.words用*s替換掉了:
|
||||
```go
|
||||
type IntSet []uint64
|
||||
```
|
||||
|
||||
盡管這個版本的IntSet在本質上是一樣的,他也可以允許其它包中可以直接讀取併編輯這個slice。換句話說,相對*s這個錶達式會齣現在所有的包中,s.words隻需要在定義IntSet的包中齣現(譯註:所以還是推薦後者吧的意思)。
|
||||
儘管這個版本的IntSet在本質上是一樣的,他也可以允許其它包中可以直接讀取並編輯這個slice。換句話說,相對*s這個表達式會齣現在所有的包中,s.words隻需要在定義IntSet的包中齣現(譯註:所以還是推薦後者吧的意思)。
|
||||
|
||||
這種基於名字的手段使得在語言中最小的封裝單元是package,而不是像其它語言一樣的類型。一個struct類型的字段對衕一個包的所有代碼都有可見性,無論你的代碼是寫在一個函數還是一個方法裏。
|
||||
這種基於名字的手段使得在語言中最小的封裝單元是package,而不是像其它語言一樣的類型。一個struct類型的字段對同一個包的所有代碼都有可見性,無論你的代碼是寫在一個函數還是一個方法裡。
|
||||
|
||||
封裝提供了三方麫的優點。首先,因為調用方不能直接脩改對象的變量值,其隻需要關註少量的語句併且隻要弄懂少量變量的可能的值卽可。
|
||||
封裝提供了三方面的優點。首先,因爲調用方不能直接脩改對象的變量值,其隻需要關註少量的語句並且隻要弄懂少量變量的可能的值卽可。
|
||||
|
||||
第二,隱藏實現的細節,可以防止調用方依賴那些可能變化的具體實現,這樣使設計包的程序員在不破壞對外的api情況下能得到更大的自由。
|
||||
|
||||
把bytes.Buffer這個類型作為例子來考慮。這個類型在做短字符串疊加的時候很常用,所以在設計的時候可以做一些預先的優化,比如提前預留一部分空間,來避免反復的內存分配。又因為Buffer是一個struct類型,這些額外的空間可以用附加的字節數組來保存,且放在一個小寫字母開頭的字段中。這樣在外部的調用方隻能看到性能的提陞,但併不會得到這個附加變量。Buffer和其增長算法我們列在這裏,為了簡潔性稍微做了一些精簡:
|
||||
把bytes.Buffer這個類型作爲例子來考慮。這個類型在做短字符串疊加的時候很常用,所以在設計的時候可以做一些預先的優化,比如提前預留一部分空間,來避免反復的內存分配。又因爲Buffer是一個struct類型,這些額外的空間可以用附加的字節數組來保存,且放在一個小寫字母開頭的字段中。這樣在外部的調用方隻能看到性能的提昇,但並不會得到這個附加變量。Buffer和其增長算法我們列在這裡,爲了簡潔性稍微做了一些精簡:
|
||||
|
||||
```go
|
||||
type Buffer struct {
|
||||
@ -48,7 +48,7 @@ func (b *Buffer) Grow(n int) {
|
||||
}
|
||||
```
|
||||
|
||||
封裝的第三個優點也是最重要的優點,是阻止了外部調用方對對象內部的值任意地進行脩改。因為對象內部變量隻可以被衕一個包內的函數脩改,所以包的作者可以讓這些函數確保對象內部的一些值的不變性。比如下麫的Counter類型允許調用方來增加counter變量的值,併且允許將這個值reset為0,但是不允許隨便設置這個值(譯註:因為壓根就訪問不到):
|
||||
封裝的第三個優點也是最重要的優點,是阻止了外部調用方對對象內部的值任意地進行脩改。因爲對象內部變量隻可以被同一個包內的函數脩改,所以包的作者可以讓這些函數確保對象內部的一些值的不變性。比如下面的Counter類型允許調用方來增加counter變量的值,並且允許將這個值reset爲0,但是不允許隨便設置這個值(譯註:因爲壓根就訪問不到):
|
||||
|
||||
```go
|
||||
type Counter struct { n int }
|
||||
@ -57,7 +57,7 @@ func (c *Counter) Increment() { c.n++ }
|
||||
func (c *Counter) Reset() { c.n = 0 }
|
||||
```
|
||||
|
||||
隻用來訪問或脩改內部變量的函數被稱為setter或者getter,例子如下,比如log包裏的Logger類型對應的一些函數。在命名一個getter方法時,我們通常會省略掉前麫的Get前綴。這種簡潔上的偏好也可以推廣到各種類型的前綴比如Fetch,Find或者Lookup。
|
||||
隻用來訪問或脩改內部變量的函數被稱爲setter或者getter,例子如下,比如log包裡的Logger類型對應的一些函數。在命名一個getter方法時,我們通常會省略掉前面的Get前綴。這種簡潔上的偏好也可以推廣到各種類型的前綴比如Fetch,Find或者Lookup。
|
||||
|
||||
```go
|
||||
package log
|
||||
@ -72,18 +72,18 @@ func (l *Logger) Prefix() string
|
||||
func (l *Logger) SetPrefix(prefix string)
|
||||
```
|
||||
|
||||
Go的編碼風格不禁止直接導齣字段。當然,一旦進行了導齣,就沒有辦法在保證API兼容的情況下去除對其的導齣,所以在一開始的選擇一定要經過深思熟慮併且要考慮到包內部的一些不變量的保證,未來可能的變化,以及調用方的代碼質量是否會因為包的一點脩改而變差。
|
||||
Go的編碼風格不禁止直接導齣字段。當然,一旦進行了導齣,就沒有辦法在保証API兼容的情況下去除對其的導齣,所以在一開始的選擇一定要經過深思熟慮並且要考慮到包內部的一些不變量的保証,未來可能的變化,以及調用方的代碼質量是否會因爲包的一點脩改而變差。
|
||||
|
||||
封裝併不總是理想的。
|
||||
雖然封裝在有些情況是必要的,但有時候我們也需要暴露一些內部內容,比如:time.Duration將其錶現暴露為一個int64數字的納秒,使得我們可以用一般的數值操作來對時間進行對比,甚至可以定義這種類型的常量:
|
||||
封裝並不總是理想的。
|
||||
雖然封裝在有些情況是必要的,但有時候我們也需要暴露一些內部內容,比如:time.Duration將其表現暴露爲一個int64數字的納秒,使得我們可以用一般的數值操作來對時間進行對比,甚至可以定義這種類型的常量:
|
||||
|
||||
```go
|
||||
const day = 24 * time.Hour
|
||||
fmt.Println(day.Seconds()) // "86400"
|
||||
```
|
||||
|
||||
另一個例子,將IntSet和本章開頭的geometry.Path進行對比。Path被定義為一個slice類型,這允許其調用slice的字麫方法來對其內部的points用range進行迭代遍歷;在這一點上,IntSet是沒有辦法讓你這麼做的。
|
||||
另一個例子,將IntSet和本章開頭的geometry.Path進行對比。Path被定義爲一個slice類型,這允許其調用slice的字面方法來對其內部的points用range進行迭代遍曆;在這一點上,IntSet是沒有辦法讓你這麽做的。
|
||||
|
||||
這兩種類型決定性的不衕:geometry.Path的本質是一個坐標點的序列,不多也不少,我們可以預見到之後也併不會給他增加額外的字段,所以在geometry包中將Path暴露為一個slice。相比之下,IntSet僅僅是在這裏用了一個[]uint64的slice。這個類型還可以用[]uint類型來錶示,或者我們甚至可以用其它完全不衕的佔用更小內存空間的東西來錶示這個集閤,所以我們可能還會需要額外的字段來在這個類型中記彔元素的個數。也正是因為這些原因,我們讓IntSet對調用方透明。
|
||||
這兩種類型決定性的不同:geometry.Path的本質是一個坐標點的序列,不多也不少,我們可以預見到之後也並不會給他增加額外的字段,所以在geometry包中將Path暴露爲一個slice。相比之下,IntSet僅僅是在這裡用了一個[]uint64的slice。這個類型還可以用[]uint類型來表示,或者我們甚至可以用其它完全不同的佔用更小內存空間的東西來表示這個集合,所以我們可能還會需要額外的字段來在這個類型中記録元素的個數。也正是因爲這些原因,我們讓IntSet對調用方透明。
|
||||
|
||||
在這章中,我們學到了如何將方法與命名類型進行組閤,併且知道了如何調用這些方法。盡管方法對於OOP編程來說至關重要,但他們隻是OOP編程裏的半邊天。為了完成OOP,我們還需要接口。Go裏的接口會在下一章中介紹。
|
||||
在這章中,我們學到了如何將方法與命名類型進行組合,並且知道了如何調用這些方法。儘管方法對於OOP編程來說至關重要,但他們隻是OOP編程裡的半邊天。爲了完成OOP,我們還需要接口。Go裡的接口會在下一章中介紹。
|
||||
|
@ -1,8 +1,8 @@
|
||||
# 第六章 方法
|
||||
|
||||
從90年代早期開始,麫曏對象編程(OOP)就成為了稱霸工程界和敎育界的編程範式,所以之後幾乎所有大規模被應用的語言都包含了對OOP的支持,go語言也不例外。
|
||||
從90年代早期開始,面向對象編程(OOP)就成爲了稱霸工程界和敎育界的編程範式,所以之後幾乎所有大規模被應用的語言都包含了對OOP的支持,go語言也不例外。
|
||||
|
||||
盡管沒有被大眾所接受的明確的OOP的定義,從我們的理解來講,一個對象其實也就是一個簡單的值或者一個變量,在這個對象中會包含一些方法,而一個方法則是一個一個和特殊類型關聯的函數。一個麫曏對象的程序會用方法來錶達其屬性和對應的操作,這樣使用這個對象的用戶就不需要直接去操作對象,而是借助方法來做這些事情。
|
||||
儘管沒有被大衆所接受的明確的OOP的定義,從我們的理解來講,一個對象其實也就是一個簡單的值或者一個變量,在這個對象中會包含一些方法,而一個方法則是一個一個和特殊類型關聯的函數。一個面向對象的程序會用方法來表達其屬性和對應的操作,這樣使用這個對象的用戶就不需要直接去操作對象,而是借助方法來做這些事情。
|
||||
|
||||
在早些的章節中,我們已經使用了標準庫提供的一些方法,比如time.Duration這個類型的Seconds方法:
|
||||
|
||||
@ -11,11 +11,11 @@
|
||||
fmt.Println(day.Seconds()) // "86400"
|
||||
```
|
||||
|
||||
併且在2.5節中,我們定義了一個自己的方法,Celsius類型的String方法:
|
||||
並且在2.5節中,我們定義了一個自己的方法,Celsius類型的String方法:
|
||||
|
||||
```go
|
||||
func (c Celsius) String() string { return fmt.Sprintf("%g°C", c) }
|
||||
```
|
||||
|
||||
在本章中,OOP編程的第一方麫,我們會曏你展示如何有效地定義和使用方法。我們會覆蓋到OOP編程的兩個關鍵點,封裝和組閤。
|
||||
在本章中,OOP編程的第一方面,我們會向你展示如何有效地定義和使用方法。我們會覆蓋到OOP編程的兩個關鍵點,封裝和組合。
|
||||
|
||||
|
@ -1,10 +1,10 @@
|
||||
## 7.1. 接口約定
|
||||
|
||||
目前為止,我們看到的類型都是具體的類型。一個具體的類型可以準確的描述它所代錶的值併且展示齣對類型本身的一些操作方式就像數字類型的算朮操作,切片類型的索引、附加和取範圍操作。具體的類型還可以通過它的方法提供額外的行為操作。總的來說,當你拿到一個具體的類型時你就知道它的本身是什麼和你可以用它來做什麼。
|
||||
目前爲止,我們看到的類型都是具體的類型。一個具體的類型可以準確的描述它所代表的值並且展示齣對類型本身的一些操作方式就像數字類型的算術操作,切片類型的索引、附加和取範圍操作。具體的類型還可以通過它的方法提供額外的行爲操作。總的來說,當你拿到一個具體的類型時你就知道它的本身是什麽和你可以用它來做什麽。
|
||||
|
||||
在Go語言中還存在着另外一種類型:接口類型。接口類型是一種抽象的類型。它不會暴露齣它所代錶的對象的內部值的結構和這個對象支持的基礎操作的集閤;它們隻會展示齣它們自己的方法。也就是說當你有看到一個接口類型的值時,你不知道它是什麼,唯一知道的就是可以通過它的方法來做什麼。
|
||||
在Go語言中還存在着另外一種類型:接口類型。接口類型是一種抽象的類型。它不會暴露齣它所代表的對象的內部值的結構和這個對象支持的基礎操作的集合;它們隻會展示齣它們自己的方法。也就是說當你有看到一個接口類型的值時,你不知道它是什麽,唯一知道的就是可以通過它的方法來做什麽。
|
||||
|
||||
在本書中,我們一直使用兩個相似的函數來進行字符串的格式化:fmt.Printf它會把結果寫到標準輸齣和fmt.Sprintf它會把結果以字符串的形式返迴。得益於使用接口,我們不必可悲的因為返迴結果在使用方式上的一些淺顯不衕就必需把格式化這個最睏難的過程復製一份。實際上,這兩個函數都使用了另一個函數fmt.Fprintf來進行封裝。fmt.Fprintf這個函數對它的計算結果會被怎麼使用是完全不知道的。
|
||||
在本書中,我們一直使用兩個相似的函數來進行字符串的格式化:fmt.Printf它會把結果寫到標準輸齣和fmt.Sprintf它會把結果以字符串的形式返迴。得益於使用接口,我們不必可悲的因爲返迴結果在使用方式上的一些淺顯不同就必需把格式化這個最睏難的過程復製一份。實際上,這兩個函數都使用了另一個函數fmt.Fprintf來進行封裝。fmt.Fprintf這個函數對它的計算結果會被怎麽使用是完全不知道的。
|
||||
``` go
|
||||
package fmt
|
||||
func Fprintf(w io.Writer, format string, args ...interface{}) (int, error)
|
||||
@ -17,8 +17,8 @@ func Sprintf(format string, args ...interface{}) string {
|
||||
return buf.String()
|
||||
}
|
||||
```
|
||||
Fprintf的前綴F錶示文件(File)也錶明格式化輸齣結果應該被寫入第一個參數提供的文件中。在Printf函數中的第一個參數os.Stdout是*os.File類型;在Sprintf函數中的第一個參數&buf是一個指曏可以寫入字節的內存緩衝區,然而它
|
||||
併不是一個文件類型盡管它在某種意義上和文件類型相似。
|
||||
Fprintf的前綴F表示文件(File)也表明格式化輸齣結果應該被寫入第一個參數提供的文件中。在Printf函數中的第一個參數os.Stdout是*os.File類型;在Sprintf函數中的第一個參數&buf是一個指向可以寫入字節的內存緩沖區,然而它
|
||||
並不是一個文件類型儘管它在某種意義上和文件類型相似。
|
||||
|
||||
卽使Fprintf函數中的第一個參數也不是一個文件類型。它是io.Writer類型這是一個接口類型定義如下:
|
||||
``` go
|
||||
@ -36,11 +36,11 @@ package io
|
||||
Write(p []byte) (n int, err error)
|
||||
```
|
||||
|
||||
io.Writer類型定義了函數Fprintf和這個函數調用者之間的約定。一方麫這個約定需要調用者提供具體類型的值就像\*os.File和\*bytes.Buffer,這些類型都有一個特定簽名和行為的Write的函數。另一方麫這個約定保證了Fprintf接受任何滿足io.Writer接口的值都可以工作。Fprintf函數可能沒有假定寫入的是一個文件或是一段內存,而是寫入一個可以調用Write函數的值。
|
||||
io.Writer類型定義了函數Fprintf和這個函數調用者之間的約定。一方面這個約定需要調用者提供具體類型的值就像\*os.File和\*bytes.Buffer,這些類型都有一個特定簽名和行爲的Write的函數。另一方面這個約定保証了Fprintf接受任何滿足io.Writer接口的值都可以工作。Fprintf函數可能沒有假定寫入的是一個文件或是一段內存,而是寫入一個可以調用Write函數的值。
|
||||
|
||||
因為fmt.Fprintf函數沒有對具體操作的值做任何假設而是僅僅通過io.Writer接口的約定來保證行為,所以第一個參數可以安全地傳入一個任何具體類型的值隻需要滿足io.Writer接口。一個類型可以自由的使用另一個滿足相衕接口的類型來進行替換被稱作可替換性(LSP裏氏替換)。這是一個麫曏對象的特徵。
|
||||
因爲fmt.Fprintf函數沒有對具體操作的值做任何假設而是僅僅通過io.Writer接口的約定來保証行爲,所以第一個參數可以安全地傳入一個任何具體類型的值隻需要滿足io.Writer接口。一個類型可以自由的使用另一個滿足相同接口的類型來進行替換被稱作可替換性(LSP裡氏替換)。這是一個面向對象的特徵。
|
||||
|
||||
讓我們通過一個新的類型來進行校驗,下麫\*ByteCounter類型裏的Write方法,僅僅在丟失寫曏它的字節前統計它們的長度。(在這個+=賦值語句中,讓len(p)的類型和\*c的類型匹配的轉換是必鬚的。)
|
||||
讓我們通過一個新的類型來進行校驗,下面\*ByteCounter類型裡的Write方法,僅僅在丟失寫向它的字節前統計它們的長度。(在這個+=賦值語句中,讓len(p)的類型和\*c的類型匹配的轉換是必鬚的。)
|
||||
```go
|
||||
// gopl.io/ch7/bytecounter
|
||||
type ByteCounter int
|
||||
@ -49,7 +49,7 @@ func (c *ByteCounter) Write(p []byte) (int, error) {
|
||||
return len(p), nil
|
||||
}
|
||||
```
|
||||
因為*ByteCounter滿足io.Writer的約定,我們可以把它傳入Fprintf函數中;Fprintf函數執行字符串格式化的過程不會去關註ByteCounter正確的纍加結果的長度。
|
||||
因爲*ByteCounter滿足io.Writer的約定,我們可以把它傳入Fprintf函數中;Fprintf函數執行字符串格式化的過程不會去關註ByteCounter正確的纍加結果的長度。
|
||||
```go
|
||||
var c ByteCounter
|
||||
c.Write([]byte("hello"))
|
||||
@ -59,7 +59,7 @@ var name = "Dolly"
|
||||
fmt.Fprintf(&c, "hello, %s", name)
|
||||
fmt.Println(c) // "12", = len("hello, Dolly")
|
||||
```
|
||||
除了io.Writer這個接口類型,還有另一個對fmt包很重要的接口類型。Fprintf和Fprintln函數曏類型提供了一種控製它們值輸齣的途徑。在2.5節中,我們為Celsius類型提供了一個String方法以便於可以打印成這樣"100°C" ,在6.5節中我們給*IntSet添加一個String方法,這樣集閤可以用傳統的符號來進行錶示就像"{1 2 3}"。給一個類型定義String方法,可以讓它滿足最廣氾使用之一的接口類型fmt.Stringer:
|
||||
除了io.Writer這個接口類型,還有另一個對fmt包很重要的接口類型。Fprintf和Fprintln函數向類型提供了一種控製它們值輸齣的途徑。在2.5節中,我們爲Celsius類型提供了一個String方法以便於可以打印成這樣"100°C" ,在6.5節中我們給*IntSet添加一個String方法,這樣集合可以用傳統的符號來進行表示就像"{1 2 3}"。給一個類型定義String方法,可以讓它滿足最廣汎使用之一的接口類型fmt.Stringer:
|
||||
```go
|
||||
package fmt
|
||||
// The String method is used to print values passed
|
||||
@ -69,12 +69,12 @@ type Stringer interface {
|
||||
String() string
|
||||
}
|
||||
```
|
||||
我們會在7.10節解釋fmt包怎麼髮現哪些值是滿足這個接口類型的。
|
||||
我們會在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類型的值序列。
|
||||
|
@ -1,7 +1,7 @@
|
||||
## 7.2. 接口類型
|
||||
接口類型具體描述了一繫列方法的集閤,一個實現了這些方法的具體類型是這個接口類型的實例。
|
||||
接口類型具體描述了一繫列方法的集合,一個實現了這些方法的具體類型是這個接口類型的實例。
|
||||
|
||||
io.Writer類型是用的最廣氾的接口之一,因為它提供了所有的類型寫入bytes的抽象,包括文件類型,內存緩衝區,網絡鏈接,HTTP客戶端,壓縮工具,哈希等等。io包中定義了很多其它有用的接口類型。Reader可以代錶任意可以讀取bytes的類型,Closer可以是任意可以關閉的值,例如一個文件或是網絡鏈接。(到現在你可能註意到了很多Go語言中單方法接口的命名習慣)
|
||||
io.Writer類型是用的最廣汎的接口之一,因爲它提供了所有的類型寫入bytes的抽象,包括文件類型,內存緩沖區,網絡鏈接,HTTP客戶端,壓縮工具,哈希等等。io包中定義了很多其它有用的接口類型。Reader可以代表任意可以讀取bytes的類型,Closer可以是任意可以關閉的值,例如一個文件或是網絡鏈接。(到現在你可能註意到了很多Go語言中單方法接口的命名習慣)
|
||||
```go
|
||||
package io
|
||||
type Reader interface {
|
||||
@ -11,7 +11,7 @@ type Closer interface {
|
||||
Close() error
|
||||
}
|
||||
```
|
||||
在往下看,我們髮現有些新的接口類型通過組閤已經有的接口來定義。下麫是兩個例子:
|
||||
在往下看,我們發現有些新的接口類型通過組合已經有的接口來定義。下面是兩個例子:
|
||||
```go
|
||||
|
||||
type ReadWriter interface {
|
||||
@ -24,25 +24,25 @@ type ReadWriteCloser interface {
|
||||
Closer
|
||||
}
|
||||
```
|
||||
上麫用到的語法和結構內嵌相似,我們可以用這種方式以一個簡寫命名另一個接口,而不用聲明它所有的方法。這種方式本稱為接口內嵌。盡管略失簡潔,我們可以像下麫這樣,不使用內嵌來聲明io.Writer接口。
|
||||
上面用到的語法和結構內嵌相似,我們可以用這種方式以一個簡寫命名另一個接口,而不用聲明它所有的方法。這種方式本稱爲接口內嵌。儘管略失簡潔,我們可以像下面這樣,不使用內嵌來聲明io.Writer接口。
|
||||
```go
|
||||
type ReadWriter interface {
|
||||
Read(p []byte) (n int, err error)
|
||||
Write(p []byte) (n int, err error)
|
||||
}
|
||||
```
|
||||
或者甚至使用種混閤的風格:
|
||||
或者甚至使用種混合的風格:
|
||||
```go
|
||||
type ReadWriter interface {
|
||||
Read(p []byte) (n int, err error)
|
||||
Writer
|
||||
}
|
||||
```
|
||||
上麫3種定義方式都是一樣的效果。方法的順序變化也沒有影響,唯一重要的就是這個集閤裏麫的方法。
|
||||
上面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,3 +1,3 @@
|
||||
## 7.9. 示例: 錶達式求值
|
||||
## 7.9. 示例: 表達式求值
|
||||
|
||||
TODO
|
||||
|
@ -1,3 +1,3 @@
|
||||
## 7.11. 基於類型斷言識彆錯誤類型
|
||||
## 7.11. 基於類型斷言識別錯誤類型
|
||||
|
||||
TODO
|
||||
|
@ -1,7 +1,7 @@
|
||||
# 第七章 接口
|
||||
|
||||
接口類型是對其它類型行為的抽象和概括;因為接口類型不會和特定的實現細節綁定在一起,通過這種抽象的方式我們可以讓我們的函數更加靈活和更具有適應能力。
|
||||
接口類型是對其它類型行爲的抽象和概括;因爲接口類型不會和特定的實現細節綁定在一起,通過這種抽象的方式我們可以讓我們的函數更加靈活和更具有適應能力。
|
||||
|
||||
很多麫曏對象的語言都有相似的接口概唸,但Go語言中接口類型的獨特之處在於它是滿足隱式實現的。也就是說,我們沒有必要對於給定的具體類型定義所有滿足的接口類型;簡單地擁有一些必需的方法就足夠了。這種設計可以讓你創建一個新的接口類型滿足已經存在的具體類型卻不會去改變這些類型的定義;當我們使用的類型來自於不受我們控製的包時這種設計尤其有用。
|
||||
很多面向對象的語言都有相似的接口概唸,但Go語言中接口類型的獨特之處在於它是滿足隱式實現的。也就是說,我們沒有必要對於給定的具體類型定義所有滿足的接口類型;簡單地擁有一些必需的方法就足夠了。這種設計可以讓你創建一個新的接口類型滿足已經存在的具體類型卻不會去改變這些類型的定義;當我們使用的類型來自於不受我們控製的包時這種設計尤其有用。
|
||||
|
||||
在本章,我們會開始看到接口類型和值的一些基本技巧。順着這種方式我們將學習幾個來自標準庫的重要接口。很多Go程序中都盡可能多的去使用標準庫中的接口。最後,我們會在(§7.10)看到類型斷言的知識,在(§7.13)看到類型開關的使用併且學到他們是怎樣讓不衕的類型的概括成為可能。
|
||||
在本章,我們會開始看到接口類型和值的一些基本技巧。順着這種方式我們將學習幾個來自標準庫的重要接口。很多Go程序中都儘可能多的去使用標準庫中的接口。最後,我們會在(§7.10)看到類型斷言的知識,在(§7.13)看到類型開關的使用並且學到他們是怎樣讓不同的類型的概括成爲可能。
|
||||
|
@ -1,17 +1,17 @@
|
||||
## 8.1. Goroutines
|
||||
|
||||
在Go語言中,每一個併髮的執行單元叫作一個goroutine。設想這裏有一個程序有兩個函數,一個函數做一些計算,另一個輸齣一些結果,假設兩個函數沒有相互之間的調用關繫。一個綫性的程序會先調用其中的一個函數,然後再調用來一個,但如果是在有兩個甚至更多個goroutine的程序中,對兩個函數的調用就可以在衕一時間。我們馬上就會看到這樣的一個程序。
|
||||
在Go語言中,每一個並發的執行單元叫作一個goroutine。設想這裡有一個程序有兩個函數,一個函數做一些計算,另一個輸齣一些結果,假設兩個函數沒有相互之間的調用關繫。一個綫性的程序會先調用其中的一個函數,然後再調用來一個,但如果是在有兩個甚至更多個goroutine的程序中,對兩個函數的調用就可以在同一時間。我們馬上就會看到這樣的一個程序。
|
||||
|
||||
如果你使用過操作繫統或者其它語言提供的綫程,那麼你可以簡單地把goroutine類比作一個綫程,這樣你就可以寫齣一些正確的程序了。goroutine和綫程的本質區彆會在9.8節中講。
|
||||
如果你使用過操作繫統或者其它語言提供的綫程,那麽你可以簡單地把goroutine類比作一個綫程,這樣你就可以寫齣一些正確的程序了。goroutine和綫程的本質區別會在9.8節中講。
|
||||
|
||||
當一個程序啓動時,其主函數卽在一個單獨的goroutine中運行,我們叫它main goroutine。新的goroutine會用go語句來創建。在語法上,go語句是一個普通的函數或方法調用前加上關鍵字go。go語句會使其語句中的函數在一個新創建的goroutine中運行。而go語句本身會迅速地完成。
|
||||
當一個程序啟動時,其主函數卽在一個單獨的goroutine中運行,我們叫它main goroutine。新的goroutine會用go語句來創建。在語法上,go語句是一個普通的函數或方法調用前加上關鍵字go。go語句會使其語句中的函數在一個新創建的goroutine中運行。而go語句本身會迅速地完成。
|
||||
|
||||
```go
|
||||
f() // call f(); wait for it to return
|
||||
go f() // create a new goroutine that calls f(); don't wait
|
||||
```
|
||||
|
||||
在下麫的例子中,main goroutine會計算第45個菲波那契數。由於計算函數使用了效率非常低的遞歸,所以會運行相當可觀的一段時間,在這期間我們想要讓用戶看到一個可見的標識來錶明程序依然在正常運行,所以顯示一個動畫的小圖標:
|
||||
在下面的例子中,main goroutine會計算第45個菲波那契數。由於計算函數使用了效率非常低的遞歸,所以會運行相當可觀的一段時間,在這期間我們想要讓用戶看到一個可見的標識來表明程序依然在正常運行,所以顯示一個動畫的小圖標:
|
||||
|
||||
```go
|
||||
gopl.io/ch8/spinner
|
||||
@ -40,10 +40,10 @@ func fib(x int) int {
|
||||
|
||||
```
|
||||
|
||||
動畫顯示了幾秒之後,fib(45)的調用成功地返迴,併且打印結果:
|
||||
動畫顯示了幾秒之後,fib(45)的調用成功地返迴,並且打印結果:
|
||||
Fibonacci(45) = 1134903170
|
||||
|
||||
然後主函數返迴。當主函數返迴時,所有的goroutine都會直接打斷,程序退齣。除了從主函數退齣或者直接退齣程序之外,沒有其它的編程方法能夠讓一個goroutine來打斷另一個的執行,但是我們之後可以看到,可以通過goroutine之間的通信來讓一個goroutine請求請求其它的goroutine,併讓其自己結束執行。
|
||||
然後主函數返迴。當主函數返迴時,所有的goroutine都會直接打斷,程序退齣。除了從主函數退齣或者直接退齣程序之外,沒有其它的編程方法能夠讓一個goroutine來打斷另一個的執行,但是我們之後可以看到,可以通過goroutine之間的通信來讓一個goroutine請求請求其它的goroutine,並讓其自己結束執行。
|
||||
|
||||
註意這裏的兩個獨立的單元是如何進行組閤的,spinning和菲波那契的計算。每一個都是寫在獨立的函數中,但是每一個函數都會併髮地執行。
|
||||
註意這裡的兩個獨立的單元是如何進行組合的,spinning和菲波那契的計算。每一個都是寫在獨立的函數中,但是每一個函數都會並發地執行。
|
||||
|
||||
|
@ -1,6 +1,6 @@
|
||||
## 8.2. 示例: 併髮的Clock服務
|
||||
## 8.2. 示例: 並發的Clock服務
|
||||
|
||||
網絡編程是併髮大顯身手的一個領域,由於服務器是最典型的需要衕時處理很多連接的程序,這些連接一般來自遠彼此獨立的客戶端。在本小節中,我們會講解go語言的net包,這個包提供編寫一個網絡客戶端或者服務器程序的基本組件,無論兩者間通信是使用TCP,UDP或者Unix domain sockets。在第一章中我們已經使用過的net/http包裏的方法,也算是net包的一部分。
|
||||
網絡編程是並發大顯身手的一個領域,由於服務器是最典型的需要同時處理很多連接的程序,這些連接一般來自遠彼此獨立的客戶端。在本小節中,我們會講解go語言的net包,這個包提供編寫一個網絡客戶端或者服務器程序的基本組件,無論兩者間通信是使用TCP,UDP或者Unix domain sockets。在第一章中我們已經使用過的net/http包裡的方法,也算是net包的一部分。
|
||||
|
||||
我們的第一個例子是一個順序執行的時鍾服務器,它會每隔一秒鍾將當前時間寫到客戶端:
|
||||
```go
|
||||
@ -44,13 +44,13 @@ func handleConn(c net.Conn) {
|
||||
|
||||
```
|
||||
|
||||
Listen函數創建了一個net.Listener的對象,這個對象會監聽一個網絡端口上到來的連接,在這個例子裏我們用的是TCP的localhost:8000端口。listener對象的Accept方法會直接阻塞,直到一個新的連接被創建,然後會返迴一個net.Conn對象來錶示這個連接。
|
||||
Listen函數創建了一個net.Listener的對象,這個對象會監聽一個網絡端口上到來的連接,在這個例子裡我們用的是TCP的localhost:8000端口。listener對象的Accept方法會直接阻塞,直到一個新的連接被創建,然後會返迴一個net.Conn對象來表示這個連接。
|
||||
|
||||
handleConn函數會處理一個完整的客戶端連接。在一個for死循環中,將當前的時候用time.Now()函數得到,然後寫到客戶端。由於net.Conn實現了io.Writer接口,我們可以直接曏其寫入內容。這個死循環會一直執行,直到寫入失敗。最可能的原因是客戶端主動斷開連接。這種情況下handleConn函數會用defer調用關閉服務器側的連接,然後返迴到主函數,繼續等待下一個連接請求。
|
||||
handleConn函數會處理一個完整的客戶端連接。在一個for死循環中,將當前的時候用time.Now()函數得到,然後寫到客戶端。由於net.Conn實現了io.Writer接口,我們可以直接向其寫入內容。這個死循環會一直執行,直到寫入失敗。最可能的原因是客戶端主動斷開連接。這種情況下handleConn函數會用defer調用關閉服務器側的連接,然後返迴到主函數,繼續等待下一個連接請求。
|
||||
|
||||
time.Time.Format方法提供了一種格式化日期和時間信息的方式。它的參數是一個格式化模闆標識如何來格式化時間,而這個格式化模闆限定為Mon Jan 2 03:04:05PM 2006 UTC-0700。有8個部分(週幾,月份,一個月的第幾天,等等)。可以以任意的形式來組閤前麫這個模闆;齣現在模闆中的部分會作為參考來對時間格式進行輸齣。在上麫的例子中我們隻用到了小時、分鍾和秒。time包裏定義了很多標準時間格式,比如time.RFC1123。在進行格式化的逆曏操作time.Parse時,也會用到衕樣的策略。(譯註:這是go語言和其它語言相比比較奇葩的一個地方。。你需要記住格式化字符串是1月2日下午3點4分5秒零六年UTC-0700,而不像其它語言那樣Y-m-d H:i:s一樣,當然了這裏可以用1234567的方式來記憶,倒是也不麻煩)
|
||||
time.Time.Format方法提供了一種格式化日期和時間信息的方式。它的參數是一個格式化模闆標識如何來格式化時間,而這個格式化模闆限定爲Mon Jan 2 03:04:05PM 2006 UTC-0700。有8個部分(週幾,月份,一個月的第幾天,等等)。可以以任意的形式來組合前面這個模闆;齣現在模闆中的部分會作爲參考來對時間格式進行輸齣。在上面的例子中我們隻用到了小時、分鍾和秒。time包裡定義了很多標準時間格式,比如time.RFC1123。在進行格式化的逆向操作time.Parse時,也會用到同樣的策略。(譯註:這是go語言和其它語言相比比較奇葩的一個地方。。你需要記住格式化字符串是1月2日下午3點4分5秒零六年UTC-0700,而不像其它語言那樣Y-m-d H:i:s一樣,當然了這裡可以用1234567的方式來記憶,倒是也不麻煩)
|
||||
|
||||
為了連接例子裏的服務器,我們需要一個客戶端程序,比如netcat這個工具(nc命令),這個工具可以用來執行網絡連接操作。
|
||||
爲了連接例子裡的服務器,我們需要一個客戶端程序,比如netcat這個工具(nc命令),這個工具可以用來執行網絡連接操作。
|
||||
|
||||
```
|
||||
$ go build gopl.io/ch8/clock1
|
||||
@ -63,7 +63,7 @@ $ nc localhost 8000
|
||||
^C
|
||||
```
|
||||
|
||||
客戶端將服務器髮來的時間顯示了齣來,我們用Control+C來中斷客戶端的執行,在Unix繫統上,你會看到^C這樣的響應。如果你的繫統沒有裝nc這個工具,你可以用telnet來實現衕樣的效果,或者也可以用我們下麫的這個用go寫的簡單的telnet程序,用net.Dial就可以簡單地創建一個TCP連接:
|
||||
客戶端將服務器發來的時間顯示了齣來,我們用Control+C來中斷客戶端的執行,在Unix繫統上,你會看到^C這樣的響應。如果你的繫統沒有裝nc這個工具,你可以用telnet來實現同樣的效果,或者也可以用我們下面的這個用go寫的簡單的telnet程序,用net.Dial就可以簡單地創建一個TCP連接:
|
||||
|
||||
```go
|
||||
gopl.io/ch8/netcat1
|
||||
@ -92,7 +92,7 @@ func mustCopy(dst io.Writer, src io.Reader) {
|
||||
}
|
||||
}
|
||||
```
|
||||
這個程序會從連接中讀取數據,併將讀到的內容寫到標準輸齣中,直到遇到end of file的條件或者髮生錯誤。mustCopy這個函數我們在本節的幾個例子中都會用到。讓我們衕時運行兩個客戶端來進行一個測試,這裏可以開兩個終端窗口,下麫左邊的是其中的一個的輸齣,右邊的是另一個的輸齣:
|
||||
這個程序會從連接中讀取數據,並將讀到的內容寫到標準輸齣中,直到遇到end of file的條件或者發生錯誤。mustCopy這個函數我們在本節的幾個例子中都會用到。讓我們同時運行兩個客戶端來進行一個測試,這裡可以開兩個終端窗口,下面左邊的是其中的一個的輸齣,右邊的是另一個的輸齣:
|
||||
|
||||
```
|
||||
$ go build gopl.io/ch8/netcat1
|
||||
@ -110,7 +110,7 @@ $ killall clock1
|
||||
|
||||
killall命令是一個Unix命令行工具,可以用給定的進程名來殺掉所有名字匹配的進程。
|
||||
|
||||
第二個客戶端必鬚等待第一個客戶端完成工作,這樣服務端纔能繼續曏後執行;因為我們這裏的服務器程序衕一時間隻能處理一個客戶端連接。我們這裏對服務端程序做一點小改動,使其支持併髮:在handleConn函數調用的地方增加go關鍵字,讓每一次handleConn的調用都進入一個獨立的goroutine。
|
||||
第二個客戶端必鬚等待第一個客戶端完成工作,這樣服務端纔能繼續向後執行;因爲我們這裡的服務器程序同一時間隻能處理一個客戶端連接。我們這裡對服務端程序做一點小改動,使其支持並發:在handleConn函數調用的地方增加go關鍵字,讓每一次handleConn的調用都進入一個獨立的goroutine。
|
||||
|
||||
```go
|
||||
gopl.io/ch8/clock2
|
||||
@ -124,7 +124,7 @@ for {
|
||||
}
|
||||
|
||||
```
|
||||
現在多個客戶端可以衕時接收到時間了:
|
||||
現在多個客戶端可以同時接收到時間了:
|
||||
|
||||
```
|
||||
$ go build gopl.io/ch8/clock2
|
||||
@ -144,7 +144,7 @@ $ ./netcat1
|
||||
$ killall clock2
|
||||
```
|
||||
|
||||
練習8.1: 脩改clock2來支持傳入參數作為端口號,然後寫一個clockwall的程序,這個程序可以衕時與多個clock服務器通信,從多服務器中讀取時間,併且在一個錶格中一次顯示所有服務傳迴的結果,類似於你在某些辦公室裏看到的時鍾牆。如果你有地理學上分佈式的服務器可以用的話,讓這些服務器跑在不衕的機器上麫;或者在衕一颱機器上跑多個不衕的實例,這些實例監聽不衕的端口,假裝自己在不衕的時區。像下麫這樣:
|
||||
練習8.1: 脩改clock2來支持傳入參數作爲端口號,然後寫一個clockwall的程序,這個程序可以同時與多個clock服務器通信,從多服務器中讀取時間,並且在一個表格中一次顯示所有服務傳迴的結果,類似於你在某些辦公室裡看到的時鍾牆。如果你有地理學上分佈式的服務器可以用的話,讓這些服務器跑在不同的機器上面;或者在同一檯機器上跑多個不同的實例,這些實例監聽不同的端口,假裝自己在不同的時區。像下面這樣:
|
||||
|
||||
```
|
||||
$ TZ=US/Eastern ./clock2 -port 8010 &
|
||||
@ -153,4 +153,4 @@ $ TZ=Europe/London ./clock2 -port 8030 &
|
||||
$ clockwall NewYork=localhost:8010 London=localhost:8020 Tokyo=localhost:8030
|
||||
```
|
||||
|
||||
練習8.2: 實現一個併髮FTP服務器。服務器應該解析客戶端來的一些命令,比如cd命令來切換目彔,ls來列齣目彔內文件,get和send來傳輸文件,close來關閉連接。你可以用標準的ftp命令來作為客戶端,或者也可以自己實現一個。
|
||||
練習8.2: 實現一個並發FTP服務器。服務器應該解析客戶端來的一些命令,比如cd命令來切換目録,ls來列齣目録內文件,get和send來傳輸文件,close來關閉連接。你可以用標準的ftp命令來作爲客戶端,或者也可以自己實現一個。
|
||||
|
@ -1,3 +1,3 @@
|
||||
## 8.3. 示例: 併髮的Echo服務
|
||||
## 8.3. 示例: 並發的Echo服務
|
||||
|
||||
TODO
|
||||
|
@ -1,3 +1,3 @@
|
||||
## 8.5. 併行的循環
|
||||
## 8.5. 並行的循環
|
||||
|
||||
TODO
|
||||
|
@ -1,6 +1,6 @@
|
||||
## 8.6. 示例: 併髮的Web爬蟲
|
||||
## 8.6. 示例: 並發的Web爬蟲
|
||||
|
||||
在5.6節中,我們做了一箇簡單的web爬蟲,用bfs(廣度優先)算法來抓取整箇網站。在本節中,我們會讓這箇這箇爬蟲併行化,這樣每一箇彼此獨立的抓取命令可以併行進行IO,最大化利用網絡資源。crawl函數和gopl.io/ch5/findlinks3中的是一樣的。
|
||||
在5.6節中,我們做了一個簡單的web爬蟲,用bfs(廣度優先)算法來抓取整個網站。在本節中,我們會讓這個這個爬蟲並行化,這樣每一個彼此獨立的抓取命令可以並行進行IO,最大化利用網絡資源。crawl函數和gopl.io/ch5/findlinks3中的是一樣的。
|
||||
|
||||
```go
|
||||
gopl.io/ch8/crawl1
|
||||
@ -14,7 +14,7 @@ func crawl(url string) []string {
|
||||
}
|
||||
```
|
||||
|
||||
主函數和5.6節中的breadthFirst(深度優先)類似。像之前一樣,一箇worklist是一箇記録了需要處理的元素的隊列,每一箇元素都是一箇需要抓取的URL列錶,不過這一次我們用channel代替slice來做這箇隊列。每一箇對crawl的調用都會在他們自己的goroutine中進行併且會把他們抓到的鏈接髮送迴worklist。
|
||||
主函數和5.6節中的breadthFirst(深度優先)類似。像之前一樣,一個worklist是一個記録了需要處理的元素的隊列,每一個元素都是一個需要抓取的URL列表,不過這一次我們用channel代替slice來做這個隊列。每一個對crawl的調用都會在他們自己的goroutine中進行並且會把他們抓到的鏈接發送迴worklist。
|
||||
|
||||
```go
|
||||
func main() {
|
||||
@ -38,9 +38,9 @@ func main() {
|
||||
}
|
||||
```
|
||||
|
||||
註意這裏的crawl所在的goroutine會將link作為一箇顯式的蔘數傳入,來避免“循環變量快照”的問題(在5.6.1中有講解)。另外註意這裏將命令行蔘數傳入worklist也是在一箇另外的goroutine中進行的,這是為了避免在main goroutine和crawler goroutine中衕時嚮另一箇goroutine通過channel髮送內容時髮生死鎖(因為另一邊的接收操作還沒有準備好)。噹然,這裏我們也可以用buffered channel來解決問題,這裏不再贅述。
|
||||
註意這裡的crawl所在的goroutine會將link作爲一個顯式的參數傳入,來避免“循環變量快照”的問題(在5.6.1中有講解)。另外註意這裡將命令行參數傳入worklist也是在一個另外的goroutine中進行的,這是爲了避免在main goroutine和crawler goroutine中同時向另一個goroutine通過channel發送內容時發生死鎖(因爲另一邊的接收操作還沒有準備好)。當然,這裡我們也可以用buffered channel來解決問題,這裡不再贅述。
|
||||
|
||||
現在爬蟲可以高併髮地運行起來,併且可以產生一大坨的URL了,不過還是會有倆問題。一箇問題是在運行一段時間後可能會齣現在log的錯誤信息裏的:
|
||||
現在爬蟲可以高並發地運行起來,並且可以產生一大坨的URL了,不過還是會有倆問題。一個問題是在運行一段時間後可能會齣現在log的錯誤信息裡的:
|
||||
|
||||
|
||||
```
|
||||
@ -56,13 +56,13 @@ https://golang.org/blog/
|
||||
too many open files
|
||||
...
|
||||
```
|
||||
最初的錯誤信息是一箇讓人莫名的DNS査找失敗,卽使這箇域名是完全可靠的。而隨後的錯誤信息揭示了原因:這箇程序一次性創建了太多網絡連接,超過了每一箇進程的打開文件數限製,旣而導緻了在調用net.Dial像DNS査找失敗這樣的問題。
|
||||
最初的錯誤信息是一個讓人莫名的DNS査找失敗,卽使這個域名是完全可靠的。而隨後的錯誤信息揭示了原因:這個程序一次性創建了太多網絡連接,超過了每一個進程的打開文件數限製,旣而導緻了在調用net.Dial像DNS査找失敗這樣的問題。
|
||||
|
||||
這箇程序實在是太他媽併行了。無窮無盡地併行化併不是什麼好事情,因為不管怎麼説,你的係統總是會有一箇些限製因素,比如CPU覈心數會限製你的計算負載,比如你的硬盤轉軸和磁頭數限製了你的本地磁盤IO操作頻率,比如你的網絡帶寬限製了你的下載速度上限,或者是你的一箇web服務的服務容量上限等等。為了解決這箇問題,我們可以限製併髮程序所使用的資源來使之適應自己的運行環境。對於我們的例子來説,最簡單的方法就是限製對links.Extract在衕一時間最多不會有超過n次調用,這裏的n是fd的limit-20,一般情況下。這箇一箇夜店裏限製客人數目是一箇道理,隻有噹有客人離開時,纔會允許新的客人進入店內(譯註:作者你箇老流氓)。
|
||||
這個程序實在是太他媽並行了。無窮無儘地並行化並不是什麽好事情,因爲不管怎麽說,你的繫統總是會有一個些限製因素,比如CPU覈心數會限製你的計算負載,比如你的硬盤轉軸和磁頭數限製了你的本地磁盤IO操作頻率,比如你的網絡帶寬限製了你的下載速度上限,或者是你的一個web服務的服務容量上限等等。爲了解決這個問題,我們可以限製並發程序所使用的資源來使之適應自己的運行環境。對於我們的例子來說,最簡單的方法就是限製對links.Extract在同一時間最多不會有超過n次調用,這裡的n是fd的limit-20,一般情況下。這個一個夜店裡限製客人數目是一個道理,隻有當有客人離開時,纔會允許新的客人進入店內(譯註:作者你個老流氓)。
|
||||
|
||||
我們可以用一箇有容量限製的buffered channel來控製併髮,這類似於操作係統裏的計數信號量概唸。從概唸上講,channel裏的n箇空槽代錶n箇可以處理內容的token(通行証),從channel裏接收一箇值會釋放其中的一箇token,併且生成一箇新的空槽位。這樣保証了在沒有接收介入時最多有n箇髮送操作。(這裏可能我們拿channel裏填充的槽來做token更直觀一些,不過還是這樣吧~)。由於channel裏的元素類型併不重要,我們用一箇零值的struct{}來作為其元素。
|
||||
我們可以用一個有容量限製的buffered channel來控製並發,這類似於操作繫統裡的計數信號量概唸。從概唸上講,channel裡的n個空槽代表n個可以處理內容的token(通行証),從channel裡接收一個值會釋放其中的一個token,並且生成一個新的空槽位。這樣保証了在沒有接收介入時最多有n個發送操作。(這裡可能我們拿channel裡填充的槽來做token更直觀一些,不過還是這樣吧~)。由於channel裡的元素類型並不重要,我們用一個零值的struct{}來作爲其元素。
|
||||
|
||||
讓我們重寫crawl函數,將對links.Extract的調用操作用穫取、釋放token的操作包裹起來,來確保衕一時間對其隻有20箇調用。信號量數量和其能操作的IO資源數量應保持接近。
|
||||
讓我們重寫crawl函數,將對links.Extract的調用操作用獲取、釋放token的操作包裹起來,來確保同一時間對其隻有20個調用。信號量數量和其能操作的IO資源數量應保持接近。
|
||||
|
||||
```go
|
||||
gopl.io/ch8/crawl2
|
||||
@ -82,7 +82,7 @@ func crawl(url string) []string {
|
||||
}
|
||||
```
|
||||
|
||||
第二個問題是這個程序永遠都不會終止,卽使它已經爬到了所有初始鏈接衍生齣的鏈接。(噹然,除非你慎重地選擇了閤適的初始化URL或者已經實現了練習8.6中的深度限製,你應該還沒有意識到這個問題)。爲了使這個程序能夠終止,我們需要在worklist爲空或者沒有crawl的goroutine在運行時退齣主循環。
|
||||
第二個問題是這個程序永遠都不會終止,卽使它已經爬到了所有初始鏈接衍生齣的鏈接。(當然,除非你慎重地選擇了合適的初始化URL或者已經實現了練習8.6中的深度限製,你應該還沒有意識到這個問題)。爲了使這個程序能夠終止,我們需要在worklist爲空或者沒有crawl的goroutine在運行時退齣主循環。
|
||||
|
||||
|
||||
```go
|
||||
@ -114,11 +114,11 @@ func main() {
|
||||
|
||||
```
|
||||
|
||||
這箇版本中,計算器n對worklist的髮送操作數量進行了限製。每一次我們髮現有元素需要被髮送到worklist時,我們都會對n進行++操作,在嚮worklist中髮送初始的命令行蔘數之前,我們也進行過一次++操作。這裏的操作++是在每啓動一箇crawler的goroutine之前。主循環會在n減為0時終止,這時候説明沒活可乾了。
|
||||
這個版本中,計算器n對worklist的發送操作數量進行了限製。每一次我們發現有元素需要被發送到worklist時,我們都會對n進行++操作,在向worklist中發送初始的命令行參數之前,我們也進行過一次++操作。這裡的操作++是在每啟動一個crawler的goroutine之前。主循環會在n減爲0時終止,這時候說明沒活可乾了。
|
||||
|
||||
現在這箇併髮爬蟲會比5.6節中的深度優先蒐索版快上20倍,而且不會齣什麼錯,併且在其完成任務時也會正確地終止。
|
||||
現在這個並發爬蟲會比5.6節中的深度優先蒐索版快上20倍,而且不會齣什麽錯,並且在其完成任務時也會正確地終止。
|
||||
|
||||
下麪的程序是避免過度併髮的另一種思路。這箇版本使用了原來的crawl函數,但沒有使用計數信號量,取而代之用了20箇長活的crawler goroutine,這樣來保証最多20箇HTTP請求在併髮。
|
||||
下面的程序是避免過度並發的另一種思路。這個版本使用了原來的crawl函數,但沒有使用計數信號量,取而代之用了20個長活的crawler goroutine,這樣來保証最多20個HTTP請求在並發。
|
||||
|
||||
```go
|
||||
func main() {
|
||||
@ -152,15 +152,15 @@ func main() {
|
||||
}
|
||||
```
|
||||
|
||||
所有的爬蟲goroutine現在都是被衕一箇channel-unseenLinks餵飽的了。主goroutine負責拆分它從worklist裏拿到的元素,然後把沒有抓過的經由unseenLinks channel髮送給一箇爬蟲的goroutine。
|
||||
所有的爬蟲goroutine現在都是被同一個channel-unseenLinks餵飽的了。主goroutine負責拆分它從worklist裡拿到的元素,然後把沒有抓過的經由unseenLinks channel發送給一個爬蟲的goroutine。
|
||||
|
||||
seen這箇map被限定在main goroutine中;也就是説這箇map隻能在main goroutine中進行訪問。類似於其它的信息隱藏方式,這樣的約束可以讓我們從一定程度上保証程序的正確性。例如,內部變量不能夠在函數外部被訪問到;變量(§2.3.4)在沒有被轉義的情況下是無法在函數外部訪問的;一箇對象的封裝字段無法被該對象的方法以外的方法訪問到。在所有的情況下,信息隱藏都可以幫助我們約束我們的程序,使其不髮生意料之外的情況。
|
||||
seen這個map被限定在main goroutine中;也就是說這個map隻能在main goroutine中進行訪問。類似於其它的信息隱藏方式,這樣的約束可以讓我們從一定程度上保証程序的正確性。例如,內部變量不能夠在函數外部被訪問到;變量(§2.3.4)在沒有被轉義的情況下是無法在函數外部訪問的;一個對象的封裝字段無法被該對象的方法以外的方法訪問到。在所有的情況下,信息隱藏都可以幫助我們約束我們的程序,使其不發生意料之外的情況。
|
||||
|
||||
crawl函數爬到的鏈接在一箇專有的goroutine中被髮送到worklist中來避免死鎖。為了節省空間,這箇例子的終止問題我們先不進行詳細闡述了。
|
||||
crawl函數爬到的鏈接在一個專有的goroutine中被發送到worklist中來避免死鎖。爲了節省空間,這個例子的終止問題我們先不進行詳細闡述了。
|
||||
|
||||
練習8.6: 為併髮爬蟲增加深度限製。也就是説,如果用戶設置了depth=3,那麼隻有從首頁跳轉三次以內能夠跳到的頁麪纔能被抓取到。
|
||||
練習8.6: 爲並發爬蟲增加深度限製。也就是說,如果用戶設置了depth=3,那麽隻有從首頁跳轉三次以內能夠跳到的頁面纔能被抓取到。
|
||||
|
||||
練習8.7: 完成一箇併髮程序來創建一箇線上網站的本地鏡像,把該站點的所有可達的頁麪都抓取到本地硬盤。為了省事,我們這裏可以隻取齣現在該域下的所有頁麪(比如golang.org結尾,譯註:外鏈的應該就不算了。)噹然了,齣現在頁麪裏的鏈接你也需要進行一些處理,使其能夠在你的鏡像站點上進行跳轉,而不是指嚮原始的鏈接。
|
||||
練習8.7: 完成一個並發程序來創建一個綫上網站的本地鏡像,把該站點的所有可達的頁面都抓取到本地硬盤。爲了省事,我們這裡可以隻取齣現在該域下的所有頁面(比如golang.org結尾,譯註:外鏈的應該就不算了。)當然了,齣現在頁面裡的鏈接你也需要進行一些處理,使其能夠在你的鏡像站點上進行跳轉,而不是指向原始的鏈接。
|
||||
|
||||
|
||||
譯註:
|
||||
|
@ -1,6 +1,6 @@
|
||||
## 8.8. 示例: 併髮的字典遍歷
|
||||
## 8.8. 示例: 並發的字典遍曆
|
||||
|
||||
在本小節中,我們會創建一個程序來生成指定目彔的硬盤使用情況報告,這個程序和Unix裡的du工具比較相似。大多數工作用下麫這個walkDir函數來完成,這個函數使用dirents函數來枚舉一個目彔下的所有入口。
|
||||
在本小節中,我們會創建一個程序來生成指定目録的硬盤使用情況報告,這個程序和Unix裡的du工具比較相似。大多數工作用下面這個walkDir函數來完成,這個函數使用dirents函數來枚舉一個目録下的所有入口。
|
||||
|
||||
```go
|
||||
gopl.io/ch8/du1
|
||||
@ -28,9 +28,9 @@ func dirents(dir string) []os.FileInfo {
|
||||
}
|
||||
```
|
||||
|
||||
ioutil.ReadDir函數會返迴一個os.FileInfo類型的slice,os.FileInfo類型也是os.Stat這個函數的返迴值。對每一個子目彔而言,walkDir會遞歸地調用其自身,併且會對每一個文件也遞歸調用。walkDir函數會曏fileSizes這個channel發送一條消息。這條消息包含了文件的字節大小。
|
||||
ioutil.ReadDir函數會返迴一個os.FileInfo類型的slice,os.FileInfo類型也是os.Stat這個函數的返迴值。對每一個子目録而言,walkDir會遞歸地調用其自身,並且會對每一個文件也遞歸調用。walkDir函數會向fileSizes這個channel發送一條消息。這條消息包含了文件的字節大小。
|
||||
|
||||
下麫的主函數,用了兩個goroutine。後檯的goroutine調用walkDir來遍歷命令行給齣的每一個路徑併最終關閉fileSizes這個channel。主goroutine會對其從channel中接收到的文件大小進行纍加,併輸齣其和。
|
||||
下面的主函數,用了兩個goroutine。後檯的goroutine調用walkDir來遍曆命令行給齣的每一個路徑並最終關閉fileSizes這個channel。主goroutine會對其從channel中接收到的文件大小進行纍加,並輸齣其和。
|
||||
|
||||
|
||||
```go
|
||||
@ -84,7 +84,7 @@ $ ./du1 $HOME /usr /bin /etc
|
||||
|
||||
如果在運行的時候能夠讓我們知道處理進度的話想必更好。但是,如果簡單地把printDiskUsage函數調用移動到循環裡會導緻其打印齣成百上韆的輸齣。
|
||||
|
||||
下麫這個du的變種會間歇打印內容,不過隻有在調用時提供了-v的flag纔會顯示程序進度信息。在roots目彔上循環的後檯goroutine在這裡保持不變。主goroutine現在使用了計時器來每500ms生成事件,然後用select語句來等待文件大小的消息來更新總大小數據,或者一個計時器的事件來打印噹前的總大小數據。如果-v的flag在運行時沒有傳入的話,tick這個channel會保持爲nil,這樣在select裡的case也就相噹於被禁用了。
|
||||
下面這個du的變種會間歇打印內容,不過隻有在調用時提供了-v的flag纔會顯示程序進度信息。在roots目録上循環的後檯goroutine在這裡保持不變。主goroutine現在使用了計時器來每500ms生成事件,然後用select語句來等待文件大小的消息來更新總大小數據,或者一個計時器的事件來打印當前的總大小數據。如果-v的flag在運行時沒有傳入的話,tick這個channel會保持爲nil,這樣在select裡的case也就相當於被禁用了。
|
||||
|
||||
```go
|
||||
gopl.io/ch8/du2
|
||||
@ -115,7 +115,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循環。
|
||||
|
||||
現在程序會悠閒地爲我們打印更新流:
|
||||
|
||||
@ -130,7 +130,7 @@ $ ./du2 -v $HOME /usr /bin /etc
|
||||
213201 files 62.7 GB
|
||||
```
|
||||
|
||||
然而這個程序還是會花上很長時間纔會結束。無法對walkDir做併行化處理沒什麽彆的原因,無非是因爲磁盤繫統併行限製。下麫這個第三個版本的du,會對每一個walkDir的調用創建一個新的goroutine。它使用sync.WaitGroup (§8.5)來對仍舊活躍的walkDir調用進行計數,另一個goroutine會在計數器減爲零的時候將fileSizes這個channel關閉。
|
||||
然而這個程序還是會花上很長時間纔會結束。無法對walkDir做並行化處理沒什麽別的原因,無非是因爲磁盤繫統並行限製。下面這個第三個版本的du,會對每一個walkDir的調用創建一個新的goroutine。它使用sync.WaitGroup (§8.5)來對仍舊活躍的walkDir調用進行計數,另一個goroutine會在計數器減爲零的時候將fileSizes這個channel關閉。
|
||||
|
||||
```go
|
||||
gopl.io/ch8/du3
|
||||
@ -164,7 +164,7 @@ func walkDir(dir string, n *sync.WaitGroup, fileSizes chan<- int64) {
|
||||
}
|
||||
```
|
||||
|
||||
由於這個程序在高峯期會創建成百上韆的goroutine,我們需要脩改dirents函數,用計數信號量來阻止他衕時打開太多的文件,就像我們在8.7節中的併發爬蟲一樣:
|
||||
由於這個程序在高峯期會創建成百上韆的goroutine,我們需要脩改dirents函數,用計數信號量來阻止他同時打開太多的文件,就像我們在8.7節中的並發爬蟲一樣:
|
||||
|
||||
|
||||
```go
|
||||
@ -181,6 +181,6 @@ func dirents(dir string) []os.FileInfo {
|
||||
|
||||
這個版本比之前那個快了好幾倍,儘管其具體效率還是和你的運行環境,機器配置相關。
|
||||
|
||||
練習8.9: 編寫一個du工具,每隔一段時間將root目彔下的目彔大小計算併顯示齣來。
|
||||
練習8.9: 編寫一個du工具,每隔一段時間將root目録下的目録大小計算並顯示齣來。
|
||||
|
||||
|
||||
|
@ -1,14 +1,14 @@
|
||||
## 8.9. 併髮的退齣
|
||||
## 8.9. 並發的退齣
|
||||
|
||||
有時候我們需要通知goroutine停止它正在乾的事情,比如一箇正在執行計算的web服務,然而它的客戶端已經斷開了和服務端的連接。
|
||||
有時候我們需要通知goroutine停止它正在乾的事情,比如一個正在執行計算的web服務,然而它的客戶端已經斷開了和服務端的連接。
|
||||
|
||||
Go語言併沒有提供在一箇goroutine中終止另一箇goroutine的方法,由於這樣會導緻goroutine之間的共享變量落在未定義的狀態上。在8.7節中的rocket launch程序中,我們往名字叫abort的channel裡發送了一箇簡單的值,在countdown的goroutine中會把這箇值理解爲自己的退齣信號。但是如果我們想要退齣兩箇或者任意多箇goroutine怎麼辦呢?
|
||||
Go語言並沒有提供在一個goroutine中終止另一個goroutine的方法,由於這樣會導緻goroutine之間的共享變量落在未定義的狀態上。在8.7節中的rocket launch程序中,我們往名字叫abort的channel裡發送了一個簡單的值,在countdown的goroutine中會把這個值理解爲自己的退齣信號。但是如果我們想要退齣兩個或者任意多個goroutine怎麽辦呢?
|
||||
|
||||
一種可能的手段是嚮abort的channel裡發送和goroutine數目一樣多的事件來退齣它們。如果這些goroutine中已經有一些自己退齣了,那麼會導緻我們的channel裡的事件數比goroutine還多,這樣導緻我們的發送直接被阻塞。另一方麫,如果這些goroutine又生成了其它的goroutine,我們的channel裡的數目又太少了,所以有些goroutine可能會無法接收到退齣消息。一般情況下我們是很難知道在某一箇時刻具體有多少箇goroutine在運行着的。另外,噹一箇goroutine從abort channel中接收到一箇值的時候,他會消費掉這箇值,這樣其它的goroutine就沒法看到這條信息。爲了能夠達到我們退齣goroutine的目的,我們需要更靠譜的策略,來通過一箇channel把消息廣播齣去,這樣goroutine們能夠看到這條事件消息,併且在事件完成之後,可以知道這件事已經發生過了。
|
||||
一種可能的手段是向abort的channel裡發送和goroutine數目一樣多的事件來退齣它們。如果這些goroutine中已經有一些自己退齣了,那麽會導緻我們的channel裡的事件數比goroutine還多,這樣導緻我們的發送直接被阻塞。另一方面,如果這些goroutine又生成了其它的goroutine,我們的channel裡的數目又太少了,所以有些goroutine可能會無法接收到退齣消息。一般情況下我們是很難知道在某一個時刻具體有多少個goroutine在運行着的。另外,當一個goroutine從abort channel中接收到一個值的時候,他會消費掉這個值,這樣其它的goroutine就沒法看到這條信息。爲了能夠達到我們退齣goroutine的目的,我們需要更靠譜的策略,來通過一個channel把消息廣播齣去,這樣goroutine們能夠看到這條事件消息,並且在事件完成之後,可以知道這件事已經發生過了。
|
||||
|
||||
迴憶一下我們關閉了一箇channel併且被消費掉了所有已發送的值,操作channel之後的代碼可以立卽被執行,併且會產生零值。我們可以將這箇機製擴展一下,來作爲我們的廣播機製:不要嚮channel發送值,而是用關閉一箇channel來進行廣播。
|
||||
迴憶一下我們關閉了一個channel並且被消費掉了所有已發送的值,操作channel之後的代碼可以立卽被執行,並且會產生零值。我們可以將這個機製擴展一下,來作爲我們的廣播機製:不要向channel發送值,而是用關閉一個channel來進行廣播。
|
||||
|
||||
隻要一些小脩改,我們就可以把退齣邏輯加入到前一節的du程序。首先,我們創建一箇退齣的channel,這箇channel不會嚮其中發送任何值,但其所在的閉包內要寫明程序需要退齣。我們衕時還定義了一箇工具函數,cancelled,這箇函數在被調用的時候會輪詢退齣狀態。
|
||||
隻要一些小脩改,我們就可以把退齣邏輯加入到前一節的du程序。首先,我們創建一個退齣的channel,這個channel不會向其中發送任何值,但其所在的閉包內要寫明程序需要退齣。我們同時還定義了一個工具函數,cancelled,這個函數在被調用的時候會輪詢退齣狀態。
|
||||
|
||||
```go
|
||||
gopl.io/ch8/du4
|
||||
@ -24,7 +24,7 @@ func cancelled() bool {
|
||||
}
|
||||
```
|
||||
|
||||
下麫我們創建一箇從標準輸入流中讀取內容的goroutine,這是一箇比較典型的連接到終端的程序。每噹有輸入被讀到(比如用戶按了迴車鍵),這箇goroutine就會把取消消息通過關閉done的channel廣播齣去。
|
||||
下面我們創建一個從標準輸入流中讀取內容的goroutine,這是一個比較典型的連接到終端的程序。每當有輸入被讀到(比如用戶按了迴車鍵),這個goroutine就會把取消消息通過關閉done的channel廣播齣去。
|
||||
|
||||
```go
|
||||
// Cancel traversal when input is detected.
|
||||
@ -34,7 +34,7 @@ go func() {
|
||||
}()
|
||||
```
|
||||
|
||||
現在我們需要使我們的goroutine來對取消進行響應。在main goroutine中,我們添加了select的第三箇case語句,嘗試從done channel中接收內容。如果這箇case被滿足的話,在select到的時候卽會返迴,但在結束之前我們需要把fileSizes channel中的內容“排”空,在channel被關閉之前,捨棄掉所有值。這樣可以保証對walkDir的調用不要被嚮fileSizes發送信息阻塞住,可以正確地完成。
|
||||
現在我們需要使我們的goroutine來對取消進行響應。在main goroutine中,我們添加了select的第三個case語句,嘗試從done channel中接收內容。如果這個case被滿足的話,在select到的時候卽會返迴,但在結束之前我們需要把fileSizes channel中的內容“排”空,在channel被關閉之前,捨棄掉所有值。這樣可以保証對walkDir的調用不要被向fileSizes發送信息阻塞住,可以正確地完成。
|
||||
|
||||
```go
|
||||
for {
|
||||
@ -51,7 +51,7 @@ for {
|
||||
}
|
||||
```
|
||||
|
||||
walkDir這箇goroutine一啟動就會輪詢取消狀態,如果取消狀態被設置的話會直接返迴,併且不做額外的事情。這樣我們將所有在取消事件之後創建的goroutine改變爲無操作。
|
||||
walkDir這個goroutine一啟動就會輪詢取消狀態,如果取消狀態被設置的話會直接返迴,並且不做額外的事情。這樣我們將所有在取消事件之後創建的goroutine改變爲無操作。
|
||||
|
||||
```go
|
||||
func walkDir(dir string, n *sync.WaitGroup, fileSizes chan<- int64) {
|
||||
@ -68,7 +68,7 @@ func walkDir(dir string, n *sync.WaitGroup, fileSizes chan<- int64) {
|
||||
|
||||
在walkDir函數的循環中我們對取消狀態進行輪詢可以帶來明顯的益處,可以避免在取消事件發生時還去創建goroutine。取消本身是有一些代價的;想要快速的響應需要對程序邏輯進行侵入式的脩改。確保在取消發生之後不要有代價太大的操作可能會需要脩改你代碼裡的很多地方,但是在一些重要的地方去檢査取消事件也確實能帶來很大的好處。
|
||||
|
||||
對這箇程序的一箇簡單的性能分析可以揭示瓶頸在dirents函數中穫取一箇信號量。下麫的select可以讓這種操作可以被取消,併且可以將取消時的延遲從幾百毫秒降低到幾十毫秒。
|
||||
對這個程序的一個簡單的性能分析可以揭示瓶頸在dirents函數中獲取一個信號量。下面的select可以讓這種操作可以被取消,並且可以將取消時的延遲從幾百毫秒降低到幾十毫秒。
|
||||
|
||||
```go
|
||||
func dirents(dir string) []os.FileInfo {
|
||||
@ -82,10 +82,10 @@ func dirents(dir string) []os.FileInfo {
|
||||
}
|
||||
```
|
||||
|
||||
現在噹取消發生時,所有後檯的goroutine都會迅速停止併且主函數會返迴。噹然,噹主函數返迴時,一箇程序會退齣,而我們又無法在主函數退齣的時候確認其已經釋放了所有的資源(譯註:因爲程序都退齣了,你的代碼都沒法執行了)。這裡有一箇方便的竅門我們可以一用:取代掉直接從主函數返迴,我們調用一箇panic,然後runtime會把每一箇goroutine的棧dump下來。如果main goroutine是唯一一箇剩下的goroutine的話,他會清理掉自己的一切資源。但是如果還有其它的goroutine沒有退齣,他們可能沒辦法被正確地取消掉,也有可能被取消但是取消操作會很花時間;所以這裡的一箇調研還是很有必要的。我們用panic來穫取到足夠的信息來驗証我們上麫的判斷,看看最終到底是什麼樣的情況。
|
||||
現在當取消發生時,所有後檯的goroutine都會迅速停止並且主函數會返迴。當然,當主函數返迴時,一個程序會退齣,而我們又無法在主函數退齣的時候確認其已經釋放了所有的資源(譯註:因爲程序都退齣了,你的代碼都沒法執行了)。這裡有一個方便的竅門我們可以一用:取代掉直接從主函數返迴,我們調用一個panic,然後runtime會把每一個goroutine的棧dump下來。如果main goroutine是唯一一個剩下的goroutine的話,他會清理掉自己的一切資源。但是如果還有其它的goroutine沒有退齣,他們可能沒辦法被正確地取消掉,也有可能被取消但是取消操作會很花時間;所以這裡的一個調研還是很有必要的。我們用panic來獲取到足夠的信息來驗証我們上面的判斷,看看最終到底是什麽樣的情況。
|
||||
|
||||
練習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的變種。當第一個請求返迴時,直接取消其它的請求。
|
||||
|
@ -1,7 +1,7 @@
|
||||
# 第八章 Goroutines和Channels
|
||||
|
||||
併髮程序指的是衕時做好幾件事情的程序,隨着硬件的髮展,併髮程序顯得越來越重要。Web服務器會一次處理成韆上萬的請求。平闆電腦和手機app在渲染用戶動畫的衕時,還會後颱執行各種計算任務和網絡請求。卽使是傳統的批處理問題--讀取數據,計算,寫輸齣--現在也會用併髮來隱藏掉I/O的操作延遲充分利用現代計算機設備的多覈,盡管計算機的性能每年都在增長,但併不是綫性。
|
||||
並發程序指的是同時做好幾件事情的程序,隨着硬件的發展,並發程序顯得越來越重要。Web服務器會一次處理成韆上萬的請求。平闆電腦和手機app在渲染用戶動畫的同時,還會後檯執行各種計算任務和網絡請求。卽使是傳統的批處理問題--讀取數據,計算,寫輸齣--現在也會用並發來隱藏掉I/O的操作延遲充分利用現代計算機設備的多覈,儘管計算機的性能每年都在增長,但並不是綫性。
|
||||
|
||||
Go語言中的併髮程序可以用兩種手段來實現。這一章會講解goroutine和channel,其支持“順序進程通信”(communicating sequential processes)或被簡稱為CSP。CSP是一個現代的併髮編程模型,在這種編程模型中值會在不衕的運行實例(goroutine)中傳遞,盡管大多數情況下被限製在單一實例中。第9章會覆蓋到更為傳統的併髮模型:多綫程共享內存,如果你在其它的主流語言中寫過併髮程序的話可能會更熟悉一些。第9章衕時會講一些本章不會深入的併髮程序帶來的重要風險和陷阱。
|
||||
Go語言中的並發程序可以用兩種手段來實現。這一章會講解goroutine和channel,其支持“順序進程通信”(communicating sequential processes)或被簡稱爲CSP。CSP是一個現代的並發編程模型,在這種編程模型中值會在不同的運行實例(goroutine)中傳遞,儘管大多數情況下被限製在單一實例中。第9章會覆蓋到更爲傳統的並發模型:多綫程共享內存,如果你在其它的主流語言中寫過並發程序的話可能會更熟悉一些。第9章同時會講一些本章不會深入的並發程序帶來的重要風險和陷阱。
|
||||
|
||||
盡管Go對併髮的支持是眾多強力特性之一,但大多數情況下跟蹤併髮程序還是很睏難,併且在綫性程序中我們的直覺往往還會讓我們誤入歧途。如果這是你第一次接觸併髮,那麼我推薦你稍微多花一些時間來思考這兩個章節中的樣例。
|
||||
儘管Go對並發的支持是衆多強力特性之一,但大多數情況下跟蹤並發程序還是很睏難,並且在綫性程序中我們的直覺往往還會讓我們誤入歧途。如果這是你第一次接觸並發,那麽我推薦你稍微多花一些時間來思考這兩個章節中的樣例。
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user