diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md index 8066fcb..59dfe78 100644 --- a/CONTRIBUTORS.md +++ b/CONTRIBUTORS.md @@ -18,10 +18,10 @@ 中文譯者 | 章節 -------------------------------------- | ------------------------- -`chai2010 ` | 前言/第二章/第三章/第十章/第十一章/第十二章/第十三章 -`CrazySssst` | 第三章(0節) -`foreversmart ` | 第七章(0節和1節) -`Xargin ` | 第一章/第六章/第八章 +`chai2010 ` | 前言/第二章/第三章/第四章/第十章/第十一章/第十二章/第十三章 +`CrazySssst` | 第三章(0節)/第五章 +`foreversmart ` | 第七章(0節/1節/2節) +`Xargin ` | 第一章/第六章/第八章/第九章 # 譯文授權 diff --git a/Makefile b/Makefile index 5b323af..8b28164 100644 --- a/Makefile +++ b/Makefile @@ -5,7 +5,9 @@ # install gitbook # npm install gitbook-cli -g +# https://github.com/GitbookIO # https://github.com/GitbookIO/gitbook +# https://github.com/GitbookIO/plugin-katex # https://github.com/wastemobile/gitbook # http://www.imagemagick.org/ diff --git a/README.md b/README.md index 1f01f40..7db6803 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,11 @@ # Go語言聖經(中文版) -Go語言聖經 [《The Go Programming Language》](http://gopl.io) 中文版本,僅供編程和英語學習交流之用,請在下載後24小時內刪除。 +Go語言聖經 [《The Go Programming Language》](http://gopl.io) 中文版本,僅供學習交流之用。 - 項目主頁:http://github.com/golang-china/gopl-zh - 項目進度:http://github.com/golang-china/gopl-zh/blob/master/progress.md - 參與人員:http://github.com/golang-china/gopl-zh/blob/master/CONTRIBUTORS.md +- 離線版本:http://github.com/golang-china/gopl-zh/archive/gh-pages.zip - 在線預覽:http://golang-china.github.io/gopl-zh - 原版官網:http://gopl.io @@ -13,13 +14,19 @@ Go語言聖經 [《The Go Programming Language》](http://gopl.io) 中文版本 ### 從源文件構建 -先安裝Go語言環境,Git工具和GitBook命令行工具(`npm install gitbook-cli -g`命令)。 +先安裝NodeJS和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`目録。 -3. 打開`_book/index.html`文件。 +1. 下載 https://github.com/golang-china/gopl-zh/archive/master.zip ,獲取源文件。 +2. 切換到 `gopl-zh` 目録,運行 `gitbook install`,安裝GitBook插件。 +3. 運行`gitbook build`,生成`_book`目録。 +4. 打開`_book/index.html`文件。 -### 簡體中文讀者 +### 簡體/繁體轉換 + +切片到 `gopl-zh` 目録: + +- `make zh2tw` 或 `go run zh2tw.go . "\.md$" zh2tw`,轉繁體。 +- `make tw2zh` 或 `go run zh2tw.go . "\.md$" tw2zh`,轉簡體。 如果是使用簡體中文的用戶,可在執行上述命令前運行`make tw2zh`命令,將繁體中文轉換爲簡體中文。 diff --git a/SUMMARY-github.md b/SUMMARY-github.md index 96b3a81..3f47933 100644 --- a/SUMMARY-github.md +++ b/SUMMARY-github.md @@ -1,4 +1,4 @@ -# Go聖經讀書筆記 +# Go語言聖經(中文版) * [前言](preface.md) * [0.1. Go語言起源](ch0/ch0-01.md) @@ -11,8 +11,8 @@ * [1.2. 命令行參數](ch1/ch1-02.md) * [1.3. 査找重複的行](ch1/ch1-03.md) * [1.4. GIF動畵](ch1/ch1-04.md) - * [1.5. 穫取URL](ch1/ch1-05.md) - * [1.6. 併發穫取多個URL](ch1/ch1-06.md) + * [1.5. 獲取URL](ch1/ch1-05.md) + * [1.6. 併發獲取多個URL](ch1/ch1-06.md) * [1.7. Web服務](ch1/ch1-07.md) * [1.8. 本章要點](ch1/ch1-08.md) * [第二章 程序結構](ch2/ch2.md) @@ -47,9 +47,14 @@ * [3.6.2. 無類型常量](ch3/ch3-06-2.md) * [第四章 複合數據類型](ch4/ch4.md) * [4.1. 數組](ch4/ch4-01.md) - * [4.2. 切片](ch4/ch4-02.md) - * [4.3. 字典](ch4/ch4-03.md) + * [4.2. Slice](ch4/ch4-02.md) + * [4.2.1. append函數](ch4/ch4-02-1.md) + * [4.2.2. Slice內存技巧](ch4/ch4-02-2.md) + * [4.3. Map](ch4/ch4-03.md) * [4.4. 結構體](ch4/ch4-04.md) + * [4.4.1. 結構體面值](ch4/ch4-04-1.md) + * [4.4.2. 結構體比較](ch4/ch4-04-2.md) + * [4.4.3. 結構體嵌入和匿名成員](ch4/ch4-04-3.md) * [4.5. JSON](ch4/ch4-05.md) * [4.6. 文本和HTML模闆](ch4/ch4-06.md) * [第五章 函數](ch5/ch5.md) @@ -57,15 +62,19 @@ * [5.2. 遞歸](ch5/ch5-02.md) * [5.3. 多返迴值](ch5/ch5-03.md) * [5.4. 錯誤](ch5/ch5-04.md) + * [5.4.1. 錯誤處理策略](ch5/ch5-04-1.md) + * [5.4.2. 文件結尾(EOF)](ch5/ch5-04-2.md) * [5.5. 函數值](ch5/ch5-05.md) * [5.6. 匿名函數](ch5/ch5-06.md) + * [5.6.1. 警告:捕獲迭代變量](ch5/ch5-06-1.md) * [5.7. 可變參數](ch5/ch5-07.md) * [5.8. Deferred函數](ch5/ch5-08.md) * [5.9. Panic異常](ch5/ch5-09.md) - * [5.10. Recover捕穫異常](ch5/ch5-10.md) + * [5.10. Recover捕獲異常](ch5/ch5-10.md) * [第六章 方法](ch6/ch6.md) * [6.1. 方法聲明](ch6/ch6-01.md) * [6.2. 基於指針對象的方法](ch6/ch6-02.md) + * [6.2.1. Nil也是一個合法的接收器類型](ch6/ch6-02-1.md) * [6.3. 通過嵌入結構體來擴展類型](ch6/ch6-03.md) * [6.4. 方法值和方法表達式](ch6/ch6-04.md) * [6.5. 示例: Bit數組](ch6/ch6-05.md) @@ -76,6 +85,7 @@ * [7.3. 實現接口的條件](ch7/ch7-03.md) * [7.4. flag.Value接口](ch7/ch7-04.md) * [7.5. 接口值](ch7/ch7-05.md) + * [7.5.1. 警告:一個包含nil指針的接口不是nil接口](ch7/ch7-05-1.md) * [7.6. sort.Interface接口](ch7/ch7-06.md) * [7.7. http.Handler接口](ch7/ch7-07.md) * [7.8. error接口](ch7/ch7-08.md) @@ -91,11 +101,15 @@ * [8.2. 示例: 併發的Clock服務](ch8/ch8-02.md) * [8.3. 示例: 併發的Echo服務](ch8/ch8-03.md) * [8.4. Channels](ch8/ch8-04.md) + * [8.4.1. 不帶緩存的Channels](ch8/ch8-04-1.md) + * [8.4.2. 管道(Pipeline)](ch8/ch8-04-2.md) + * [8.4.3. 單向的Channels](ch8/ch8-04-3.md) + * [8.4.4. 帶緩存的Channels](ch8/ch8-04-4.md) * [8.5. 併行的循環](ch8/ch8-05.md) * [8.6. 示例: 併發的Web爬蟲](ch8/ch8-06.md) * [8.7. 基於select的多路複用](ch8/ch8-07.md) * [8.8. 示例: 併發的字典遍歷](ch8/ch8-08.md) - * [8.9. 併發的退齣](ch8/ch8-09.md) + * [8.9. 併發的退出](ch8/ch8-09.md) * [8.10. 示例: 聊天服務](ch8/ch8-10.md) * [第九章 基於共享變量的併發](ch9/ch9.md) * [9.1. 競爭條件](ch9/ch9-01.md) @@ -106,6 +120,10 @@ * [9.6. 競爭條件檢測](ch9/ch9-06.md) * [9.7. 示例: 併發的非阻塞緩存](ch9/ch9-07.md) * [9.8. Goroutines和線程](ch9/ch9-08.md) + * [9.8.1. 動態棧](ch9/ch9-08-1.md) + * [9.8.2. Goroutine調度](ch9/ch9-08-2.md) + * [9.8.3. GOMAXPROCS](ch9/ch9-08-3.md) + * [9.8.4. Goroutine沒有ID號](ch9/ch9-08-4.md) * [第十章 包和工具](ch10/ch10.md) * [10.1. 簡介](ch10/ch10-01.md) * [10.2. 導入路徑](ch10/ch10-02.md) @@ -140,7 +158,7 @@ * [12.4. 示例: 編碼S表達式](ch12/ch12-04.md) * [12.5. 通過reflect.Value脩改值](ch12/ch12-05.md) * [12.6. 示例: 解碼S表達式](ch12/ch12-06.md) - * [12.7. 穫取結構體字段標識](ch12/ch12-07.md) + * [12.7. 獲取結構體字段標識](ch12/ch12-07.md) * [12.8. 顯示一個類型的方法集](ch12/ch12-08.md) * [12.9. 幾點忠告](ch12/ch12-09.md) * [第十三章 底層編程](ch13/ch13.md) @@ -150,4 +168,3 @@ * [13.4. 通過cgo調用C代碼](ch13/ch13-04.md) * [13.5. 幾點忠告](ch13/ch13-05.md) * [附録](CONTRIBUTORS.md) - diff --git a/SUMMARY.md b/SUMMARY.md index 8d7fa41..2380612 100644 --- a/SUMMARY.md +++ b/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) @@ -32,8 +32,8 @@ * [常量](ch3/ch3-06.md) * [複合數據類型](ch4/ch4.md) * [數組](ch4/ch4-01.md) - * [切片](ch4/ch4-02.md) - * [字典](ch4/ch4-03.md) + * [Slice](ch4/ch4-02.md) + * [Map](ch4/ch4-03.md) * [結構體](ch4/ch4-04.md) * [JSON](ch4/ch4-05.md) * [文本和HTML模闆](ch4/ch4-06.md) @@ -47,7 +47,7 @@ * [可變參數](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) @@ -76,11 +76,11 @@ * [示例: 併發的Clock服務](ch8/ch8-02.md) * [示例: 併發的Echo服務](ch8/ch8-03.md) * [Channels](ch8/ch8-04.md) - * [併行的循環](ch8/ch8-05.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-09.md) * [示例: 聊天服務](ch8/ch8-10.md) * [基於共享變量的併發](ch9/ch9.md) * [競爭條件](ch9/ch9-01.md) @@ -113,7 +113,7 @@ * [示例: 編碼S表達式](ch12/ch12-04.md) * [通過reflect.Value脩改值](ch12/ch12-05.md) * [示例: 解碼S表達式](ch12/ch12-06.md) - * [穫取結構體字段標識](ch12/ch12-07.md) + * [獲取結構體字段標識](ch12/ch12-07.md) * [顯示一個類型的方法集](ch12/ch12-08.md) * [幾點忠告](ch12/ch12-09.md) * [底層編程](ch13/ch13.md) diff --git a/book.json b/book.json index a94e2a9..712f491 100644 --- a/book.json +++ b/book.json @@ -1,8 +1,13 @@ { - "title": "Go编程语言", + "gitbook": "2.x.x", + "title": "Go语言圣经", "description": "中文版", "language": "zh", "structure": { "readme": "preface.md" - } + }, + "plugins": [ + "katex", + "-search" + ] } diff --git a/ch0/ch0-01.md b/ch0/ch0-01.md index 1110c7e..eb11c36 100644 --- a/ch0/ch0-01.md +++ b/ch0/ch0-01.md @@ -14,6 +14,6 @@ Go語言的另一支祖先,帶來了Go語言區别其他語言的重要特性 接下來,Rob Pike和其他人開始不斷嚐試將CSP引入實際的編程語言中。他們第一次嚐試引入CSP特性的編程語言叫Squeak(老鼠間交流的語言),是一個提供鼠標和鍵盤事件處理的編程語言,它的管道是靜態創建的。然後是改進版的Newsqueak語言,提供了類似C語言語句和表達式的語法和類似Pascal語言的推導語法。Newsqueak是一個帶垃圾迴收的純函數式語言,它再次針對鍵盤、鼠標和窗口事件管理。但是在Newsqueak語言中管道是動態創建的,屬於第一類值, 可以保存到變量中。 -在Plan9操作繫統中,這些優秀的想法被吸收到了一個叫Alef的編程語言中。Alef試圖將Newsqueak語言改造爲繫統編程語言,但是因爲缺少垃圾迴收機製而導致併發編程很痛苦。(譯註:在Aelf之後還有一個叫Limbo的編程語言,Go語言從其中借鑒了很多特性。在docs目録包含了這些語言相關的文檔手冊。) +在Plan9操作繫統中,這些優秀的想法被吸收到了一個叫Alef的編程語言中。Alef試圖將Newsqueak語言改造爲繫統編程語言,但是因爲缺少垃圾迴收機製而導致併發編程很痛苦。(譯註:在Aelf之後還有一個叫Limbo的編程語言,Go語言從其中借鑒了很多特性。 具體請參考Pike的講稿:http://talks.golang.org/2012/concurrency.slide#9 ) Go語言的其他的一些特性零散地來自於其他一些編程語言;比如iota語法是從APL語言借鑒,詞法作用域與嵌套函數來自於Scheme語言(和其他很多語言)。當然,我們也可以從Go中發現很多創新的設計。比如Go語言的切片爲動態數組提供了有效的隨機存取的性能,這可能會讓人聯想到鏈表的底層的共享機製。還有Go語言新發明的defer語句。 diff --git a/ch0/ch0-02.md b/ch0/ch0-02.md index cdf2885..363e403 100644 --- a/ch0/ch0-02.md +++ b/ch0/ch0-02.md @@ -4,7 +4,7 @@ 正如Rob Pike所説,“軟件的複雜性是乘法級相關的”,通過增加一個部分的複雜性來脩複問題通常將慢慢地增加其他部分的複雜性。通過增加功能和選項和配置是脩複問題的最快的途徑,但是這很容易讓人忘記簡潔的內涵,卽使從長遠來看,簡潔依然是好軟件的關鍵因素。 -簡潔的設計需要在工作開始的時候舍棄不必要的想法,併且在軟件的生命週期內嚴格區别好的改變或壞的改變。通過足夠的努力,一個好的改變可以在不破壞原有完整概念的前提下保持自適應,正如Fred Brooks所説的“概念完整性”;而一個壞的改變則不能達到這個效果,它們僅僅是通過膚淺的和簡單的妥協來破壞原有設計的一致性。隻有通過簡潔的設計,纔能讓一個繫統保持穩定、安全和持續的進化。 +簡潔的設計需要在工作開始的時候舍棄不必要的想法,併且在軟件的生命週期內嚴格區别好的改變或壞的改變。通過足夠的努力,一個好的改變可以在不破壞原有完整概念的前提下保持自適應,正如Fred Brooks所説的“概念完整性”;而一個壞的改變則不能達到這個效果,它們僅僅是通過膚淺的和簡單的妥協來破壞原有設計的一致性。隻有通過簡潔的設計,才能讓一個繫統保持穩定、安全和持續的進化。 Go項目包括編程語言本身,附帶了相關的工具和標準庫,最後但併非代表不重要的,關於簡潔編程哲學的宣言。就事後諸葛的角度來看,Go語言的這些地方都做的還不錯:擁有自動垃圾迴收、一個包繫統、函數作爲一等公民、詞法作用域、繫統調用接口、隻讀的UTF8字符串等。但是Go語言本身隻有很少的特性,也不太可能添加太多的特性。例如,它沒有隱式的數值轉換,沒有構造函數和析構函數,沒有運算符重載,沒有默認參數,也沒有繼承,沒有泛型,沒有異常,沒有宏,沒有函數脩飾,更沒有線程局部存儲。但是語言本身是成熟和穩定的,而且承諾保證向後兼容:用之前的Go語言編寫程序可以用新版本的Go語言編譯器和標準庫直接構建而不需要脩改代碼。 @@ -12,5 +12,5 @@ Go語言有足夠的類型繫統以避免動態語言中那些粗心的類型錯 Go語言鼓勵當代計算機繫統設計的原則,特别是局部的重要性。它的內置數據類型和大多數的準庫數據結構都經過精心設計而避免顯式的初始化或隱式的構造函數,因爲很少的內存分配和內存初始化代碼被隱藏在庫代碼中了。Go語言的聚合類型(結構體和數組)可以直接操作它們的元素,隻需要更少的存儲空間、更少的內存分配,而且指針操作比其他間接操作的語言也更有效率。由於現代計算機是一個併行的機器,Go語言提供了基於CSP的併發特性支持。Go語言的動態棧使得輕量級線程goroutine的初始棧可以很小,因此創建一個goroutine的代價很小,創建百萬級的goroutine完全是可行的。 -Go語言的標準庫(通常被稱爲語言自帶的電池),提供了清晰的構建模塊和公共接口,包含I/O操作、文本處理、圖像、密碼學、網絡和分布式應用程序等,併支持許多標準化的文件格式和編解碼協議。庫和工具使用了大量的約定來減少額外的配置和解釋,從而最終簡化程序的邏輯,而且每個Go程序結構都是如此的相似,因此Go程序也很容易學習。使用Go語言自帶工具構建Go語言項目隻需要使用文件名和標識符名稱, 一個偶爾的特殊註釋來確定所有的庫、可執行文件、測試、基準測試、例子、以及特定於平颱的變量、項目的文檔等;Go語言源代碼本身就包含了構建規范。 +Go語言的標準庫(通常被稱爲語言自帶的電池),提供了清晰的構建模塊和公共接口,包含I/O操作、文本處理、圖像、密碼學、網絡和分布式應用程序等,併支持許多標準化的文件格式和編解碼協議。庫和工具使用了大量的約定來減少額外的配置和解釋,從而最終簡化程序的邏輯,而且每個Go程序結構都是如此的相似,因此Go程序也很容易學習。使用Go語言自帶工具構建Go語言項目隻需要使用文件名和標識符名稱, 一個偶爾的特殊註釋來確定所有的庫、可執行文件、測試、基準測試、例子、以及特定於平台的變量、項目的文檔等;Go語言源代碼本身就包含了構建規范。 diff --git a/ch0/ch0-03.md b/ch0/ch0-03.md index 35f3acb..3d26242 100644 --- a/ch0/ch0-03.md +++ b/ch0/ch0-03.md @@ -20,13 +20,13 @@ Go語言的面向對象是不同尋常的。它沒有類層次結構,甚至可 有些章節的後面可能會有一些練習,你可以根據你對Go語言的理解,然後脩改書中的例子來探索Go語言的其他用法。 -書中所有的代碼都可以從 http://gopl.io 上的Git倉庫下載。go get命令可以根據每個例子的其導入路徑智能地穫取、構建併安裝。你隻需要選擇一個目録作爲工作空間,然後將GOPATH環境指向這個工作目録。 +書中所有的代碼都可以從 http://gopl.io 上的Git倉庫下載。go get命令可以根據每個例子的其導入路徑智能地獲取、構建併安裝。你隻需要選擇一個目録作爲工作空間,然後將GOPATH環境指向這個工作目録。 Go語言工具將在必要時創建的相應的目録。例如: ``` $ export GOPATH=$HOME/gobook # 選擇工作目録 -$ go get gopl.io/ch1/helloworld # 穫取/編譯/安裝 +$ go get gopl.io/ch1/helloworld # 獲取/編譯/安裝 $ $GOPATH/bin/helloworld # 運行程序 Hello, 世界 # 這是中文 ``` diff --git a/ch0/ch0-04.md b/ch0/ch0-04.md index 071d19a..e12c282 100644 --- a/ch0/ch0-04.md +++ b/ch0/ch0-04.md @@ -10,5 +10,5 @@ Playground可以簡單的通過執行一個小程序來測試對語法、語義 當然,Playground 和 Tour 也有一些限製,它們隻能導入標準庫,而且因爲安全的原因對一些網絡庫做了限製。如果要在編譯和運行時需要訪問互聯網,對於一些更複製的實驗,你可能需要在自己的電腦上構建併運行程序。幸運的是下載Go語言的過程很簡單,從 https://golang.org 下載安裝包應該不超過幾分鐘(譯註:感謝偉大的長城,讓大陸的Gopher們都學會了自己打洞的基本生活技能,下載時間可能會因爲洞的大小等因素從幾分鐘到幾天或更久),然後就可以在自己電腦上編寫和運行Go程序了。 -Go語言是一個開源項目,你可以在 https://golang.org/pkg 閲讀標準庫中任意函數和類型的實現代碼,和下載安裝包的代碼完全一致。這樣你可以知道很多函數是如何工作的, 通過挖掘找齣一些答案的細節,或者僅僅是齣於欣賞專業級Go代碼。 +Go語言是一個開源項目,你可以在 https://golang.org/pkg 閲讀標準庫中任意函數和類型的實現代碼,和下載安裝包的代碼完全一致。這樣你可以知道很多函數是如何工作的, 通過挖掘找出一些答案的細節,或者僅僅是出於欣賞專業級Go代碼。 diff --git a/ch0/ch0-05.md b/ch0/ch0-05.md index c2b521c..9fc7329 100644 --- a/ch0/ch0-05.md +++ b/ch0/ch0-05.md @@ -1,8 +1,8 @@ ## 致謝 -Rob Pike和Russ Cox,以及很多其他Go糰隊的覈心成員多次仔細閲讀了本書的手稿,他們對本書的組織結構和表述用詞等給齣了很多寶貴的建議。在準備日文版翻譯的時候,Yoshiki Shibata更是仔細地審閲了本書的每個部分,及時發現了諸多英文和代碼的錯誤。我們非常感謝本書的每一位審閲者,併感謝對本書給齣了重要的建議的Brian Goetz、Corey Kosak、Arnold Robbins、Josh Bleecher Snyder和Peter Weinberger等人。 +Rob Pike和Russ Cox,以及很多其他Go糰隊的覈心成員多次仔細閲讀了本書的手稿,他們對本書的組織結構和表述用詞等給出了很多寶貴的建議。在準備日文版翻譯的時候,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、Robert Griesemer、John Linderman、Minux Ma(譯註:中國人,Go糰隊成員。)、Bryan Mills、Bala Natarajan、Cosmos Nicolaou、Paul Staniforth、Nigel Tao(譯註:好像是陶哲軒的兄弟)以及Howard Trickey給齣的許多有價值的建議。我們還要感謝David Brailsford和Raph Levien關於類型設置的建議。 +我們還感謝Sameer Ajmani、Ittai Balaban、David Crawshaw、Billy Donohue、Jonathan Feinberg、Andrew Gerrand、Robert Griesemer、John Linderman、Minux Ma(譯註:中国人,Go糰隊成員。)、Bryan Mills、Bala Natarajan、Cosmos Nicolaou、Paul Staniforth、Nigel Tao(譯註:好像是陶哲軒的兄弟)以及Howard Trickey給出的許多有價值的建議。我們還要感謝David Brailsford和Raph Levien關於類型設置的建議。 我們的來自Addison-Wesley的編輯Greg Doench收到了很多幫助,從最開始就得到了越來越多的幫助。來自AW生産糰隊的John Fuller、Dayna Isley、Julie Nahil、Chuti Prasertsith到Barbara Wood,感謝你們的熱心幫助。 diff --git a/ch1/ch1-01.md b/ch1/ch1-01.md index 48b39d7..9118ae5 100644 --- a/ch1/ch1-01.md +++ b/ch1/ch1-01.md @@ -1,6 +1,6 @@ ## 1.1. Hello, World -我們以1978年齣版的C語言聖經《The C Programming Language》中經典的“hello world”案例來開始吧(譯註:本書作者之一Brian W. Kernighan也是C語言聖經一書的作者)。C語言對Go語言的設計産生了很多影響。用這個例子,我們來講解一些Go語言的覈心特性: +我們以1978年出版的C語言聖經《The C Programming Language》中經典的“hello world”案例來開始吧(譯註:本書作者之一Brian W. Kernighan也是C語言聖經一書的作者)。C語言對Go語言的設計産生了很多影響。用這個例子,我們來講解一些Go語言的覈心特性: ```go gopl.io/ch1/helloworld @@ -19,7 +19,7 @@ Go是一門編譯型語言,Go語言的工具鏈將源代碼和其依賴一起 $ go run helloworld.go ``` -毫無意外,這個命令會輸齣: +毫無意外,這個命令會輸出: ``` Hello, 世界 @@ -48,11 +48,11 @@ Hello, 世界 gopl.io/ch1/helloworld ``` -如果你執行 `go get gopl.io/ch1/helloworld` 命令,go命令能夠自己從網上穫取到這些代碼(譯註:需要先安裝Git或Hg之類的版本管理工具,併將對應的命令添加到PATH環境變量中),併且將這些代碼放到對應的目録中(譯註:序言已經提及,需要先設置好GOPATH環境變量,下載的代碼會放在 $GOPATH/src/gopl.io/ch1/helloworld 目録)。更詳細的介紹在2.6和10.7章節中。 +如果你執行 `go get gopl.io/ch1/helloworld` 命令,go命令能夠自己從網上獲取到這些代碼(譯註:需要先安裝Git或Hg之類的版本管理工具,併將對應的命令添加到PATH環境變量中),併且將這些代碼放到對應的目録中(譯註:序言已經提及,需要先設置好GOPATH環境變量,下載的代碼會放在 $GOPATH/src/gopl.io/ch1/helloworld 目録)。更詳細的介紹在2.6和10.7章節中。 我們來討論一下程序本身。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里會定義一個獨立的程序,這個程序是可以運行的,而不是像其它package一樣對應一個library。在main這個package里,main函數也是一個特殊的函數,這是我們整個程序的入口(譯註:其實C繫語言差不多都是這樣)。main函數所做的事情就是我們程序做的事情。當然了,main函數一般是通過是調用其它packge里的函數來完成自己的工作,比如fmt.Println。 diff --git a/ch1/ch1-02.md b/ch1/ch1-02.md index 44bd462..3682cb3 100644 --- a/ch1/ch1-02.md +++ b/ch1/ch1-02.md @@ -1,14 +1,14 @@ ## 1.2. 命令行參數 -大多數的程序都是處理輸入,産生輸齣;這也正是“計算”的定義。但是一個程序要如何穫取輸入呢?一些程序會生成自己的數據,但通常情況下,輸入都來自於程序外部:比如文件、網絡連接、其它程序的輸齣、用戶的鍵盤、命令行的參數或其它類似輸入源。下面幾個例子會討論其中的一些輸入類型,首先是命令行參數。 +大多數的程序都是處理輸入,産生輸出;這也正是“計算”的定義。但是一個程序要如何獲取輸入呢?一些程序會生成自己的數據,但通常情況下,輸入都來自於程序外部:比如文件、網絡連接、其它程序的輸出、用戶的鍵盤、命令行的參數或其它類似輸入源。下面幾個例子會討論其中的一些輸入類型,首先是命令行參數。 -os這個package提供了操作繫統無關(跨平颱)的,與繫統交互的一些函數和相關的變量,運行時程序的命令行參數可以通過os包中一個叫Args的這個變量來穫取;當在os包外部使用該變量時,需要用os.Args來訪問。 +os這個package提供了操作繫統無關(跨平台)的,與繫統交互的一些函數和相關的變量,運行時程序的命令行參數可以通過os包中一個叫Args的這個變量來獲取;當在os包外部使用該變量時,需要用os.Args來訪問。 -os.Args這個變量是一個字符串(string)的slice(譯註:slice和Python語言中的切片類似,是一個簡版的動態數組),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和Python語言中的切片類似,是一個簡版的動態數組),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:]。 -下面是一個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 @@ -56,7 +56,7 @@ 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語言會根據等號右邊的值的類型自動判斷左邊的值類型,下一章會對這一點進行詳細説明。 @@ -141,17 +141,17 @@ func main() { } ``` -最後,如果我們對輸齣的格式也不是很關心,隻是想簡單地輸齣值得的話,還可以像下面這麽寫,Println函數會爲我們自動格式化輸齣。 +最後,如果我們對輸出的格式也不是很關心,隻是想簡單地輸出值得的話,還可以像下面這麽寫,Println函數會爲我們自動格式化輸出。 ```go fmt.Println(os.Args[1:]) ``` -這個輸齣結果和前面的string.Join得到的結果很相似,隻是被自動地放到了一個方括號里,對slice調用Println函數都會被打印成這樣形式的結果。 +這個輸出結果和前面的string.Join得到的結果很相似,隻是被自動地放到了一個方括號里,對slice調用Println函數都會被打印成這樣形式的結果。 **練習 1.1:** 脩改echo程序,使其能夠打印os.Args[0]。 **練習 1.2:** 脩改echo程序,使其打印value和index,每個value和index顯示一行。 -**練習 1.3:** 上手實踐前面提到的strings.Join和直接Println,併觀察輸齣結果的區别。 +**練習 1.3:** 上手實踐前面提到的strings.Join和直接Println,併觀察輸出結果的區别。 diff --git a/ch1/ch1-03.md b/ch1/ch1-03.md index 496ca2a..850f75d 100644 --- a/ch1/ch1-03.md +++ b/ch1/ch1-03.md @@ -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 @@ -33,7 +33,7 @@ func main() { 和我們前面提到的for循環一樣,在if條件的兩邊,我們也不需要加括號,但是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()]++這個語句和下面的兩句是等價的: @@ -47,7 +47,7 @@ counts[line] = counts[line] + 1 在這里我們又用了一個range的循環來打印結果,這次range是被用在map這個數據結構之上。這一次的情況和上次比較類似,range會返迴兩個值,一個key和在map對應這個key的value。對map進行range循環時,其迭代順序是不確定的,從實踐來看,很可能每次運行都會有不一樣的結果(譯註:這是Go語言的設計者有意爲之的,因爲其底層實現不保證插入順序和遍歷順序一致,也希望程序員不要依賴遍歷時的順序,所以榦脆直接在遍歷的時候做了隨機化處理,醉了。補充:好像説隨機序可以防止某種類型的攻擊,雖然不太明白,但是感覺還蠻厲害的),來避免程序員在業務中依賴遍歷時的順序。 -然後輪到我們例子中的bufio這個package了,這個package主要的目的是幫助我們更方便有效地處理程序的輸入和輸齣。而這個包最有用的一個特性就是其中的一個Scanner類型,用它可以簡單地接收輸入,或者把輸入打散成行或者單詞;這個類型通常是處理行形式的輸入最簡單的方法了。 +然後輪到我們例子中的bufio這個package了,這個package主要的目的是幫助我們更方便有效地處理程序的輸入和輸出。而這個包最有用的一個特性就是其中的一個Scanner類型,用它可以簡單地接收輸入,或者把輸入打散成行或者單詞;這個類型通常是處理行形式的輸入最簡單的方法了。 本程序中用了一個短變量聲明,來創建一個buffio.Scanner對象: @@ -57,9 +57,9 @@ input := bufio.NewScanner(os.Stdin) 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變量 @@ -69,14 +69,14 @@ Printf有一大堆這種轉換,Go語言程序員把這些叫做verb(動詞 %c rune (Unicode碼點),Go語言里特有的Unicode字符類型 %s string %q 帶雙引號的字符串 "abc" 或 帶單引號的 rune 'c' -%v 會將任意變量以易讀的形式打印齣來 +%v 會將任意變量以易讀的形式打印出來 %T 打印變量的類型 %% 字符型百分比標誌(%符號本身,沒有其他操作) ``` -dup1中的程序還包含了一個\t和\n的格式化字符串。在字符串中會以這些特殊的轉義字符來表示不可見字符。Printf默認不會在輸齣內容後加上換行符。按照慣例,用來格式化的函數都會在末尾以f字母結尾(譯註:f後綴對應format或fmt縮寫),比如log.Printf,fmt.Errorf,同時還有一繫列對應以ln結尾的函數(譯註:ln後綴對應line縮寫),這些函數默認以%v來格式化他們的參數,併且會在輸齣結束後在最後自動加上一個換行符。 +dup1中的程序還包含了一個\t和\n的格式化字符串。在字符串中會以這些特殊的轉義字符來表示不可見字符。Printf默認不會在輸出內容後加上換行符。按照慣例,用來格式化的函數都會在末尾以f字母結尾(譯註:f後綴對應format或fmt縮寫),比如log.Printf,fmt.Errorf,同時還有一繫列對應以ln結尾的函數(譯註:ln後綴對應line縮寫),這些函數默認以%v來格式化他們的參數,併且會在輸出結束後在最後自動加上一個換行符。 -許多程序從標準輸入中讀取數據,像上面的例子那樣。除此之外,還可能從一繫列的文件中讀取。下一個dup程序就是從標準輸入中讀到一些文件名,用os.Open函數來打開每一個文件穫取內容的。 +許多程序從標準輸入中讀取數據,像上面的例子那樣。除此之外,還可能從一繫列的文件中讀取。下一個dup程序就是從標準輸入中讀到一些文件名,用os.Open函數來打開每一個文件獲取內容的。 ```go gopl.io/ch1/dup2 @@ -124,7 +124,7 @@ func countLines(f *os.File, counts map[string]int) { 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節中我們會對錯誤處理做更詳細的闡述。 @@ -167,9 +167,9 @@ 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包中提供的方法已經足夠好用。 -**練習 1.4:** 脩改dup2,使其可以打印重複的行分别齣現在哪些文件。 +**練習 1.4:** 脩改dup2,使其可以打印重複的行分别出現在哪些文件。 diff --git a/ch1/ch1-04.md b/ch1/ch1-04.md index df809b1..07b8334 100644 --- a/ch1/ch1-04.md +++ b/ch1/ch1-04.md @@ -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) -譯註:要看這個程序的結果,需要將標準輸齣重定向到一個GIF圖像文件(使用 `./lissajous > output.gif` 命令)。下面是GIF圖像動畵效果: +譯註:要看這個程序的結果,需要將標準輸出重定向到一個GIF圖像文件(使用 `./lissajous > output.gif` 命令)。下面是GIF圖像動畵效果: ![](../images/ch1-01.gif) @@ -69,17 +69,17 @@ bla kIndex) 當我們import了一個包路徑包含有多個單詞的package時,比如image/color(image和color兩個單詞),通常我們隻需要用最後那個單詞表示這個包就可以。所以當我們寫color.White時,這個變量指向的是image/color包里的變量,同理gif.GIF是屬於image/gif包里的變量。 -這個程序里的常量聲明給齣了一繫列的常量值,常量是指在程序編譯後運行時始終都不會變化的值,比如圈數、幀數、延遲值。常量聲明和變量聲明一般都會齣現在包級别,所以這些常量在整個包中都是可以共享的,或者你也可以把常量聲明定義在函數體內部,那麽這種常量就隻能在函數體內用。目前常量聲明的值必鬚是一個數字值、字符串或者一個固定的boolean值。 +這個程序里的常量聲明給出了一繫列的常量值,常量是指在程序編譯後運行時始終都不會變化的值,比如圈數、幀數、延遲值。常量聲明和變量聲明一般都會出現在包級别,所以這些常量在整個包中都是可以共享的,或者你也可以把常量聲明定義在函數體內部,那麽這種常量就隻能在函數體內用。目前常量聲明的值必鬚是一個數字值、字符串或者一個固定的boolean值。 []color.Color{...}和gif.GIF{...}這兩個表達式就是我們説的複合聲明(4.2和4.4.1節有説明)。這是實例化Go語言里的複合類型的一種寫法。這里的前者生成的是一個slice切片,後者生成的是一個struct結構體。 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)點來染黑色。 -main函數調用了lissajous函數,併且用它來向標準輸齣中打印信息,所以下面這個命令會像圖1.1中産生一個GIF動畵。 +main函數調用了lissajous函數,併且用它來向標準輸出中打印信息,所以下面這個命令會像圖1.1中産生一個GIF動畵。 ``` $ go build gopl.io/ch1/lissajous diff --git a/ch1/ch1-05.md b/ch1/ch1-05.md index 5900647..14ef3f8 100644 --- a/ch1/ch1-05.md +++ b/ch1/ch1-05.md @@ -1,8 +1,8 @@ -## 1.5. 穫取URL +## 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 @@ -34,7 +34,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寫出到標準輸出流中。 ``` $ go build gopl.io/ch1/fetch @@ -65,5 +65,5 @@ fetch: Get http://gopl.io: dial tcp: lookup gopl.io: getaddrinfow: No such host **練習 1.8:** 脩改fetch這個范例,如果輸入的url參數沒有 `http://` 前綴的話,爲這個url加上該前綴。你可能會用到strings.HasPrefix這個函數。 -**練習 1.9:** 脩改fetch打印齣HTTP協議的狀態碼,可以從resp.Status變量得到該狀態碼。 +**練習 1.9:** 脩改fetch打印出HTTP協議的狀態碼,可以從resp.Status變量得到該狀態碼。 diff --git a/ch1/ch1-06.md b/ch1/ch1-06.md index e48bc7b..bf458fa 100644 --- a/ch1/ch1-06.md +++ b/ch1/ch1-06.md @@ -1,8 +1,8 @@ -## 1.6. 併發穫取多個URL +## 1.6. 併發獲取多個URL Go語言最有意思併且最新奇的特性就是其對併發編程的支持了。併發編程是一個大話題,在第八章和第九章中會專門講到。這里我們隻淺嚐輒止地來體驗一下Go語言里的goroutine和channel。 -下面的例子fetchall,和上面的fetch程序所要做的工作是一致的,但是這個fetchall的特别之處在於它會同時去穫取所有的URL,所以這個程序的穫取時間不會超過執行時間最長的那一個任務,而不會像前面的fetch程序一樣,執行時間是所有任務執行時間之和。這次的fetchall程序隻會打印穫取的內容大小和經過的時間,不會像上面那樣打印齣穫取的內容。 +下面的例子fetchall,和上面的fetch程序所要做的工作是一致的,但是這個fetchall的特别之處在於它會同時去獲取所有的URL,所以這個程序的獲取時間不會超過執行時間最長的那一個任務,而不會像前面的fetch程序一樣,執行時間是所有任務執行時間之和。這次的fetchall程序隻會打印獲取的內容大小和經過的時間,不會像上面那樣打印出獲取的內容。 ```go gopl.io/ch1/fetchall @@ -61,8 +61,8 @@ $ ./fetchall https://golang.org http://gopl.io https://godoc.org 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異步執行時同時結束。 -**練習 1.10:** 找一個數據量比較大的網站,用本小節中的程序調研網站的緩存策略,對每個URL執行兩遍請求,査看兩次時間是否有較大的差别,併且每次穫取到的響應內容是否一致,脩改本節中的程序,將響應結果輸齣,以便於進行對比。 +**練習 1.10:** 找一個數據量比較大的網站,用本小節中的程序調研網站的緩存策略,對每個URL執行兩遍請求,査看兩次時間是否有較大的差别,併且每次獲取到的響應內容是否一致,脩改本節中的程序,將響應結果輸出,以便於進行對比。 diff --git a/ch1/ch1-07.md b/ch1/ch1-07.md index 18a1794..81de96e 100644 --- a/ch1/ch1-07.md +++ b/ch1/ch1-07.md @@ -24,9 +24,9 @@ func handler(w http.ResponseWriter, r *http.Request) { } ``` -我們隻用了八九行代碼就實現了一個個Web服務程序,這都是多虧了標準庫里的方法已經幫我們處理了大量的工作。main函數會將所有發送到/路徑下的請求和handler函數關聯起來,/開頭的請求其實就是所有發送到當前站點上的請求,我們的服務跑在了8000端口上。發送到這個服務的“請求”是一個http.Request類型的對象,這個對象中包含了請求中的一繫列相關字段,其中就包括我們需要的URL。當請求到達服務器時,這個請求會被傳給handler函數來處理,這個函數會將/hello這個路徑從請求的URL中解析齣來,然後把其發送到響應中,這里我們用的是標準輸齣流的fmt.Fprintf。Web服務會在第7.7節中詳細闡述。 +我們隻用了八九行代碼就實現了一個個Web服務程序,這都是多虧了標準庫里的方法已經幫我們處理了大量的工作。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 & @@ -46,7 +46,7 @@ URL.Path = "/help" ![](../images/ch1-02.png) -在這個服務的基礎上疊加特性是很容易的。一種比較實用的脩改是爲訪問的url添加某種狀態。比如,下面這個版本輸齣了同樣的內容,但是會對請求的次數進行計算;對URL的請求結果會包含各種URL被訪問的總次數,直接對/count這個URL的訪問要除外。 +在這個服務的基礎上疊加特性是很容易的。一種比較實用的脩改是爲訪問的url添加某種狀態。比如,下面這個版本輸出了同樣的內容,但是會對請求的次數進行計算;對URL的請求結果會包含各種URL被訪問的總次數,直接對/count這個URL的訪問要除外。 ```go gopl.io/ch1/server2 @@ -87,7 +87,7 @@ func counter(w http.ResponseWriter, r *http.Request) { 這個服務器有兩個請求處理函數,請求的url會決定具體調用哪一個:對/count這個url的請求會調用到count這個函數,其它所有的url都會調用默認的處理函數。如果你的請求pattern是以/結尾,那麽所有以該url爲前綴的url都會被這條規則匹配。在這些代碼的背後,服務器每一次接收請求處理時都會另起一個goroutine,這樣服務器就可以同一時間處理多數請求。然而在併發情況下,假如眞的有兩個請求同一時刻去更新count,那麽這個值可能併不會被正確地增加;這個程序可能會被引發一個嚴重的bug:競態條件(參見9.1)。爲了避免這個問題,我們必鬚保證每次脩改變量的最多隻能有一個goroutine,這也就是代碼里的mu.Lock()和mu.Unlock()調用將脩改count的所有行爲包在中間的目的。第九章中我們會進一步講解共享變量。 -下面是一個更爲豐富的例子,handler函數會把請求的http頭和請求的form數據都打印齣來,這樣可以讓檢査和調試這個服務更爲方便: +下面是一個更爲豐富的例子,handler函數會把請求的http頭和請求的form數據都打印出來,這樣可以讓檢査和調試這個服務更爲方便: ```go gopl.io/ch1/server3 @@ -108,7 +108,7 @@ func handler(w http.ResponseWriter, r *http.Request) { } ``` -我們用http.Request這個struct里的字段來輸齣下面這樣的內容: +我們用http.Request這個struct里的字段來輸出下面這樣的內容: ``` GET /?q=query HTTP/1.1 @@ -119,7 +119,7 @@ RemoteAddr = "127.0.0.1:59911" Form["q"] = ["query"] ``` -可以看到這里的ParseForm被嵌套在了if語句中。Go語言允許這樣的一個簡單的語句結果作爲循環的變量聲明齣現在if語句的最前面,這一點對錯誤處理很有用處。我們還可以像下面這樣寫(當然看起來就長了一些): +可以看到這里的ParseForm被嵌套在了if語句中。Go語言允許這樣的一個簡單的語句結果作爲循環的變量聲明出現在if語句的最前面,這一點對錯誤處理很有用處。我們還可以像下面這樣寫(當然看起來就長了一些): ```go err := r.ParseForm() @@ -130,11 +130,11 @@ if err != nil { 用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服務器的代碼里加入下面這幾行。 ```Go handler := func(w http.ResponseWriter, r *http.Request) { diff --git a/ch1/ch1-08.md b/ch1/ch1-08.md index 7ceab65..b69327b 100644 --- a/ch1/ch1-08.md +++ b/ch1/ch1-08.md @@ -15,7 +15,7 @@ default: } ``` -在翻轉硬幣的時候,例子里的coinflip函數返迴幾種不同的結果,每一個case都會對應個返迴結果,這里需要註意,Go語言併不需要顯式地去在每一個case後寫break,語言默認執行完case後的邏輯語句會自動退齣。當然了,如果你想要相鄰的幾個case都執行同一邏輯的話,需要自己顯式地寫上一個fallthrough語句來覆蓋這種默認行爲。不過fallthrough語句在一般的編程中用到得很少。 +在翻轉硬幣的時候,例子里的coinflip函數返迴幾種不同的結果,每一個case都會對應個返迴結果,這里需要註意,Go語言併不需要顯式地去在每一個case後寫break,語言默認執行完case後的邏輯語句會自動退出。當然了,如果你想要相鄰的幾個case都執行同一邏輯的話,需要自己顯式地寫上一個fallthrough語句來覆蓋這種默認行爲。不過fallthrough語句在一般的編程中用到得很少。 Go語言里的switch還可以不帶操作對象(譯註:switch不帶操作對象時默認用true值代替,然後將每個case的表達式和true值進行比較);可以直接羅列多種條件,像其它語言里面的多個if else一樣,下面是一個例子: @@ -49,7 +49,7 @@ var p Point 類型聲明和命名類型會在第二章中介紹。 -**指針:** Go語言提供了指針。指針是一種直接存儲了變量的內存地址的數據類型。在其它語言中,比如C語言,指針操作是完全不受約束的。在另外一些語言中,指針一般被處理爲“引用”,除了到處傳遞這些指針之外,併不能對這些指針做太多事情。Go語言在這兩種范圍中取了一種平衡。指針是可見的內存地址,&操作符可以返迴一個變量的內存地址,併且*操作符可以穫取指針指向的變量內容,但是在Go語言里沒有指針運算,也就是不能像c語言里可以對指針進行加或減操作。我們會在2.3.2中進行詳細介紹。 +**指針:** Go語言提供了指針。指針是一種直接存儲了變量的內存地址的數據類型。在其它語言中,比如C語言,指針操作是完全不受約束的。在另外一些語言中,指針一般被處理爲“引用”,除了到處傳遞這些指針之外,併不能對這些指針做太多事情。Go語言在這兩種范圍中取了一種平衡。指針是可見的內存地址,&操作符可以返迴一個變量的內存地址,併且*操作符可以獲取指針指向的變量內容,但是在Go語言里沒有指針運算,也就是不能像c語言里可以對指針進行加或減操作。我們會在2.3.2中進行詳細介紹。 **方法和接口:** 方法是和命名類型關聯的一類函數。Go語言里比較特殊的是方法可以被關聯到任意一種命名類型。在第六章我們會詳細地講方法。接口是一種抽象類型,這種類型可以讓我們以同樣的方式來處理不同的固有類型,不用關心它們的具體實現,而隻需要關註它們提供的方法。第七章中會詳細説明這些內容。 diff --git a/ch1/ch1.md b/ch1/ch1.md index f52116d..5d37b11 100644 --- a/ch1/ch1.md +++ b/ch1/ch1.md @@ -1,5 +1,5 @@ # 第1章 入門 -本章會介紹Go語言里的一些基本組件。我們希望用信息和例子盡快帶你入門。本章和之後章節的例子都是針對眞實的開發案例給齣。本章我們隻是簡單地爲你介紹一些Go語言的入門例子,從簡單的文件處理、圖像處理到互聯網併發客戶端和服務端程序。當然,在第一章我們不會詳盡地一一去説明細枝末節,不過用這些程序來學習一門新語言肯定是很有效的。 +本章會介紹Go語言里的一些基本組件。我們希望用信息和例子盡快帶你入門。本章和之後章節的例子都是針對眞實的開發案例給出。本章我們隻是簡單地爲你介紹一些Go語言的入門例子,從簡單的文件處理、圖像處理到互聯網併發客戶端和服務端程序。當然,在第一章我們不會詳盡地一一去説明細枝末節,不過用這些程序來學習一門新語言肯定是很有效的。 -當你學習一門新語言時,你會用這門新語言去重寫自己以前熟悉語言例子的傾向。在學習Go語言的過程中,盡量避免這麽做。我們會向你演示如何纔能寫齣好的Go語言程序,所以請使用這里的代碼作爲你寫自己的Go程序時的指南。 +當你學習一門新語言時,你會用這門新語言去重寫自己以前熟悉語言例子的傾向。在學習Go語言的過程中,盡量避免這麽做。我們會向你演示如何才能寫出好的Go語言程序,所以請使用這里的代碼作爲你寫自己的Go程序時的指南。 diff --git a/ch10/ch10-01.md b/ch10/ch10-01.md index d2fdd28..d856835 100644 --- a/ch10/ch10-01.md +++ b/ch10/ch10-01.md @@ -4,8 +4,8 @@ 每個包定義了一個不同的名稱空間用於它內部的每個標識符. 每個名稱關聯到一個特定的包, 我們最好給類型, 函數等選擇簡短清晰的名字, 這樣可以避免在我們使用它們的時候減少和其他部分名字的衝突. -包還通過控製包內名字的可見性和是否導齣來實現封裝特性. 通過限製包成員的可見性併隱藏包API的具體實現, 將允許包的維護者在不影響外部包用戶的前提下調整包的內部實現. 通過限製包內變量的可見性, 還可以控製用戶通過某些特定函數來訪問和更新內部變量, 這樣可以保證內部變量的一致性和併發時的互斥約束. +包還通過控製包內名字的可見性和是否導出來實現封裝特性. 通過限製包成員的可見性併隱藏包API的具體實現, 將允許包的維護者在不影響外部包用戶的前提下調整包的內部實現. 通過限製包內變量的可見性, 還可以控製用戶通過某些特定函數來訪問和更新內部變量, 這樣可以保證內部變量的一致性和併發時的互斥約束. -當我們脩改了一個文件, 我們必鬚重新編譯改文件對應的包和所以依賴該包的其他包.卽使是從頭構建, Go的編譯器也明顯快於其他編譯語言. Go的編譯速度主要得益於三個特性. 第一點, 所有導入的包必鬚在每個文件的開頭顯式聲明, 這樣的話編譯器就沒有必要讀取分析整個文件來判斷包的依賴關繫. 第二點, 包的依賴關繫形成一個有向無環圖, 因爲沒有循環依賴, 每個包可以被獨立編譯, 很可能是併發編譯. 第三點, 編譯後包的目標文件不僅僅記録包本身的導齣信息, 同時還記録了它的依賴關繫. 因此, 在編譯一個包的時候, 編譯器隻需要讀取每個直接導入包的目標文件, 而不是要遍歷所有依賴的的文件(譯註: 很多可能是間接依賴). +當我們脩改了一個文件, 我們必鬚重新編譯改文件對應的包和所以依賴該包的其他包.卽使是從頭構建, Go的編譯器也明顯快於其他編譯語言. Go的編譯速度主要得益於三個特性. 第一點, 所有導入的包必鬚在每個文件的開頭顯式聲明, 這樣的話編譯器就沒有必要讀取分析整個文件來判斷包的依賴關繫. 第二點, 包的依賴關繫形成一個有向無環圖, 因爲沒有循環依賴, 每個包可以被獨立編譯, 很可能是併發編譯. 第三點, 編譯後包的目標文件不僅僅記録包本身的導出信息, 同時還記録了它的依賴關繫. 因此, 在編譯一個包的時候, 編譯器隻需要讀取每個直接導入包的目標文件, 而不是要遍歷所有依賴的的文件(譯註: 很多可能是間接依賴). diff --git a/ch10/ch10-02.md b/ch10/ch10-02.md index 659b882..c12bc38 100644 --- a/ch10/ch10-02.md +++ b/ch10/ch10-02.md @@ -1,7 +1,7 @@ ## 10.2. 導入路徑 每個包是由一個全局唯一的字符串所標識的導入路徑定位. -齣現在導入聲明中的導入路徑也是字符串. +出現在導入聲明中的導入路徑也是字符串. ```Go import ( diff --git a/ch10/ch10-05.md b/ch10/ch10-05.md index b2c60b6..db5dca5 100644 --- a/ch10/ch10-05.md +++ b/ch10/ch10-05.md @@ -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 @@ -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) 格式壓縮的文檔. 使用類似上面的註冊機製來擴展支持不同的壓縮格式, 然後根據需要通過匿名導入選擇支持的格式. diff --git a/ch10/ch10-06.md b/ch10/ch10-06.md index 5345115..7f10f40 100644 --- a/ch10/ch10-06.md +++ b/ch10/ch10-06.md @@ -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 名字的函數用於創建實例. diff --git a/ch10/ch10-07-2.md b/ch10/ch10-07-2.md index f586da2..05c8f90 100644 --- a/ch10/ch10-07-2.md +++ b/ch10/ch10-07-2.md @@ -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 @@ -37,5 +37,5 @@ $ ./fetch https://golang.org/x/net/html | grep go-import `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` 參數, 以區别普通的瀏覽器請求.) diff --git a/ch10/ch10-07-3.md b/ch10/ch10-07-3.md index e3fc084..0876a8f 100644 --- a/ch10/ch10-07-3.md +++ b/ch10/ch10-07-3.md @@ -1,6 +1,6 @@ ### 10.7.3. 構建包 -`go build` 命令編譯參數指定的每個包. 如果包是一個庫, 則忽略輸齣結果; 這可以用於檢測包的可以正確編譯的. +`go build` 命令編譯參數指定的每個包. 如果包是一個庫, 則忽略輸出結果; 這可以用於檢測包的可以正確編譯的. 如果包的名字是 main, `go build` 將調用連接器在當前目録創建一個可執行程序; 導入路徑的最後一段作爲可執行程序的名字. 因爲每個目録隻包含一個包, 因此每個可執行程序後者叫Unix術語中的命令, 會要求放到一個獨立的目録. 這些目録有時候會放在名叫 cmd 目録的子目録下面, 例如用於提供Go文檔服務的 golang.org/x/tools/cmd/godoc 命令 (§10.7.4). @@ -66,9 +66,9 @@ $ go run quoteargs.go one "two three" four\ five `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 @@ -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 diff --git a/ch10/ch10-07-4.md b/ch10/ch10-07-4.md index 938bb6c..399923b 100644 --- a/ch10/ch10-07-4.md +++ b/ch10/ch10-07-4.md @@ -1,6 +1,6 @@ ### 10.7.4. 包文檔 -Go的編碼風格鼓勵爲每個包提供良好的文檔. 包中每個導齣的成員和包聲明前都應該包含添加目的和用法説明的註釋. +Go的編碼風格鼓勵爲每個包提供良好的文檔. 包中每個導出的成員和包聲明前都應該包含添加目的和用法説明的註釋. Go中包文檔註釋一般是完整的句子, 第一行是包的摘要説明, 註釋後僅跟着包聲明語句. 函數的參數或其他的標識符併不需要額外的引號或其他標記註明. 例如, 下面是 fmt.Fprintf 的文檔註釋. @@ -10,7 +10,7 @@ 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的風格喜歡簡潔的文檔, 併且文檔也是需要想代碼一樣維護的. 對於一組聲明語句, 可以同一個精鍊的句子描述, 如果是顯而易見的功能則併不需要註釋. diff --git a/ch10/ch10-07-5.md b/ch10/ch10-07-5.md index 74be780..b1776c4 100644 --- a/ch10/ch10-07-5.md +++ b/ch10/ch10-07-5.md @@ -1,8 +1,8 @@ ### 10.7.5. 內部包 -在Go程序中, 包的封裝機製是一個重要的特性. 爲導齣的標識符隻在同一個包內部可以訪問, 導齣的標識符則是面向全世界可見. +在Go程序中, 包的封裝機製是一個重要的特性. 爲導出的標識符隻在同一個包內部可以訪問, 導出的標識符則是面向全世界可見. -有時候, 一個中間的狀態可能也是有用的, 對於一小部分信任的包是可見的, 但併不是對所有調用者都可見. 例如, 當我們計劃將一個大的包拆分爲很多小的更容易管理的子包, 但是我們併不想將內部的子包結構也完全暴露齣去. 同時, 我們肯呢個還希望在內部子包之間共享一些通用的處理包. 或者我們隻是想實驗一個新包的還併不穩定的接口, 暫時隻暴露給一些受限製的客戶端. +有時候, 一個中間的狀態可能也是有用的, 對於一小部分信任的包是可見的, 但併不是對所有調用者都可見. 例如, 當我們計劃將一個大的包拆分爲很多小的更容易管理的子包, 但是我們併不想將內部的子包結構也完全暴露出去. 同時, 我們肯呢個還希望在內部子包之間共享一些通用的處理包. 或者我們隻是想實驗一個新包的還併不穩定的接口, 暫時隻暴露給一些受限製的客戶端. ![](../images/ch10-01.png) diff --git a/ch10/ch10-07-6.md b/ch10/ch10-07-6.md index c45600b..97315bb 100644 --- a/ch10/ch10-07-6.md +++ b/ch10/ch10-07-6.md @@ -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 %} ``` @@ -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 格式的信息. diff --git a/ch10/ch10-07.md b/ch10/ch10-07.md index df35485..a8f4f67 100644 --- a/ch10/ch10-07.md +++ b/ch10/ch10-07.md @@ -5,7 +5,7 @@ Go的工具箱集合了一繫列的功能到一個命令集. 它可以看作是一個包管理器(類似於Linux中的apt和rpm工具), 用於包的査詢, 計算的包依賴關繫, 從遠程版本控製繫統和下載它們等任務. 它也是一個構建繫統, 計算文件的依賴關繫, 然後調用編譯器, 滙編器 和 連接器 構建程序, 雖然它故意被設計成沒有標準的make命令那麽複雜. 它也是一個測試驅動程序, 我們在第11章討論測試話題. -Go工具箱的命令有着類似"瑞士軍刀"的風格, 帶着一打子的子命令, 有一些我們經常用到, 例如 get, run, build, 和 fmt 等. 你可以運行 `go help` 命令査看內置的溫度, 爲了査詢方便, 我們列齣了最常用的命令: +Go工具箱的命令有着類似"瑞士軍刀"的風格, 帶着一打子的子命令, 有一些我們經常用到, 例如 get, run, build, 和 fmt 等. 你可以運行 `go help` 命令査看內置的溫度, 爲了査詢方便, 我們列出了最常用的命令: ``` $ go diff --git a/ch11/ch11-02-1.md b/ch11/ch11-02-1.md index 0159aa5..2425e2f 100644 --- a/ch11/ch11-02-1.md +++ b/ch11/ch11-02-1.md @@ -3,7 +3,7 @@ 表格驅動的測試便於構造基於精心挑選的測試數據的測試用例. 另一種測試思路是隨機測試, 也就是通過構造更廣泛的隨機輸入來測試探索函數的行爲. -那麽對於一個隨機的輸入, 我們如何能知道希望的輸齣結果呢? 這里有兩種策略. 第一個是編寫另一個函數, 使用簡單和清晰的算法, 雖然效率較低但是行爲和要測試的函數一致, 然後針對相同的隨機輸入檢査兩者的輸齣結果. 第二種是生成的隨機輸入的數據遵循特定的模式, 這樣我們就可以知道期望的輸齣的模式. +那麽對於一個隨機的輸入, 我們如何能知道希望的輸出結果呢? 這里有兩種策略. 第一個是編寫另一個函數, 使用簡單和清晰的算法, 雖然效率較低但是行爲和要測試的函數一致, 然後針對相同的隨機輸入檢査兩者的輸出結果. 第二種是生成的隨機輸入的數據遵循特定的模式, 這樣我們就可以知道期望的輸出的模式. 下面的例子使用的是第二種方法: randomPalindrome 函數用於隨機生成迴文字符串. @@ -39,7 +39,7 @@ func TestRandomPalindromes(t *testing.T) { } ``` -雖然隨機測試有不確定因素, 但是它也是至關重要的, 我們可以從失敗測試的日誌穫取足夠的信息. 在我們的例子中, 輸入 IsPalindrome 的 p 參數將告訴我們眞實的數據, 但是對於函數將接受更複雜的輸入, 不需要保存所有的輸入, 隻要日誌中簡單地記録隨機數種子卽可(像上面的方式). 有了這些隨機數初始化種子, 我們可以很容易脩改測試代碼以重現失敗的隨機測試. +雖然隨機測試有不確定因素, 但是它也是至關重要的, 我們可以從失敗測試的日誌獲取足夠的信息. 在我們的例子中, 輸入 IsPalindrome 的 p 參數將告訴我們眞實的數據, 但是對於函數將接受更複雜的輸入, 不需要保存所有的輸入, 隻要日誌中簡單地記録隨機數種子卽可(像上面的方式). 有了這些隨機數初始化種子, 我們可以很容易脩改測試代碼以重現失敗的隨機測試. 通過使用當前時間作爲隨機種子, 在整個過程中的每次運行測試命令時都將探索新的隨機數據. 如果你使用的是定期運行的自動化測試集成繫統, 隨機測試將特别有價值. diff --git a/ch11/ch11-02-2.md b/ch11/ch11-02-2.md index a1ce05a..ede30d5 100644 --- a/ch11/ch11-02-2.md +++ b/ch11/ch11-02-2.md @@ -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,7 +83,7 @@ func TestEcho(t *testing.T) { } ``` -要註意的是測試代碼和産品代碼在同一個包. 雖然是main包, 也有對應的 main 入口函數, 但是在測試的時候 main 包隻是 TestEcho 測試函數導入的一個普通包, 里面 main 函數併沒有被導齣是被忽略的. +要註意的是測試代碼和産品代碼在同一個包. 雖然是main包, 也有對應的 main 入口函數, 但是在測試的時候 main 包隻是 TestEcho 測試函數導入的一個普通包, 里面 main 函數併沒有被導出是被忽略的. 通過將測試放到表格中, 我們很容易添加新的測試用例. 讓我通過增加下面的測試用例來看看失敗的情況是怎麽樣的: @@ -91,7 +91,7 @@ func TestEcho(t *testing.T) { {true, ",", []string{"a", "b", "c"}, "a b c\n"}, // NOTE: wrong expectation! ``` -`go test` 輸齣如下: +`go test` 輸出如下: ``` $ go test gopl.io/ch11/echo @@ -103,6 +103,6 @@ FAIL gopl.io/ch11/echo 0.006s 錯誤信息描述了嚐試的操作(使用Go類似語法), 實際的行爲, 和期望的行爲. 通過這樣的錯誤信息, 你可以在檢視代碼之前就很容易定位錯誤的原因. -要註意的是在測試代碼中併沒有調用 log.Fatal 或 os.Exit, 因爲調用這類函數會導致程序提前退齣; 調用這些函數的特權應該放在 main 函數中. 如果眞的有以外的事情導致函數發送 panic, 測試驅動應該嚐試 recover, 然後將當前測試當作失敗處理. 如果是可預期的錯誤, 例如非法的用戶輸入, 找不到文件, 或配置文件不當等應該通過返迴一個非空的 error 的方式處理. 幸運的是(上面的意外隻是一個插麴), 我們的 echo 示例是比較簡單的也沒有需要返迴非空error的情況. +要註意的是在測試代碼中併沒有調用 log.Fatal 或 os.Exit, 因爲調用這類函數會導致程序提前退出; 調用這些函數的特權應該放在 main 函數中. 如果眞的有以外的事情導致函數發送 panic, 測試驅動應該嚐試 recover, 然後將當前測試當作失敗處理. 如果是可預期的錯誤, 例如非法的用戶輸入, 找不到文件, 或配置文件不當等應該通過返迴一個非空的 error 的方式處理. 幸運的是(上面的意外隻是一個插麴), 我們的 echo 示例是比較簡單的也沒有需要返迴非空error的情況. diff --git a/ch11/ch11-02-3.md b/ch11/ch11-02-3.md index c55362d..4e426fc 100644 --- a/ch11/ch11-02-3.md +++ b/ch11/ch11-02-3.md @@ -5,9 +5,9 @@ 黑盒和白盒這兩種測試方法是互補的. 黑盒測試一般更健壯, 隨着軟件實現的完善測試代碼很少需要更新. 它們可以幫助測試者了解眞是客戶的需求, 可以幫助發現API設計的一些不足之處. 相反, 白盒測試則可以對內部一些棘手的實現提供更多的測試覆蓋. -我們已經看到兩種測試的例子. TestIsPalindrome 測試僅僅使用導齣的 IsPalindrome 函數, 因此它是一個黑盒測試. TestEcho 測試則調用了內部的 echo 函數, 併且更新了內部的 out 全局變量, 這兩個都是未導齣的, 因此它是白盒測試. +我們已經看到兩種測試的例子. TestIsPalindrome 測試僅僅使用導出的 IsPalindrome 函數, 因此它是一個黑盒測試. TestEcho 測試則調用了內部的 echo 函數, 併且更新了內部的 out 全局變量, 這兩個都是未導出的, 因此它是白盒測試. -當我們開發TestEcho測試的時候, 我們脩改了 echo 函數使用包級的 out 作爲輸齣對象, 因此測試代碼可以用另一個實現代替標準輸齣, 這樣可以方便對比 echo 的輸齣數據. 使用類似的技術, 我們可以將産品代碼的其他部分也替換爲一個容易測試的僞對象. 使用僞對象的好處是我們可以方便配置, 容易預測, 更可靠, 也更容易觀察. 同時也可以避免一些不良的副作用, 例如更新生産數據庫或信用卡消費行爲. +當我們開發TestEcho測試的時候, 我們脩改了 echo 函數使用包級的 out 作爲輸出對象, 因此測試代碼可以用另一個實現代替標準輸出, 這樣可以方便對比 echo 的輸出數據. 使用類似的技術, 我們可以將産品代碼的其他部分也替換爲一個容易測試的僞對象. 使用僞對象的好處是我們可以方便配置, 容易預測, 更可靠, 也更容易觀察. 同時也可以避免一些不良的副作用, 例如更新生産數據庫或信用卡消費行爲. 下面的代碼演示了爲用戶提供網絡存儲的web服務中的配額檢測邏輯. 當用戶使用了超過 90% 的存儲配額之後將發送提醒郵件. diff --git a/ch11/ch11-02-4.md b/ch11/ch11-02-4.md index a545059..3e25a15 100644 --- a/ch11/ch11-02-4.md +++ b/ch11/ch11-02-4.md @@ -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.isSpace 和 unicode.IsSpace 函數的行爲一致, fmt 包謹慎地包含了一個測試. 是一個在測試擴展包內的測試, 因此是無法直接訪問到 isSpace 內部函數的, 因此 fmt 通過一個祕密齣口導齣了 isSpace 函數. export_test.go 文件就是專門用於測試擴展包的祕密齣口. +爲了確保 fmt.isSpace 和 unicode.IsSpace 函數的行爲一致, fmt 包謹慎地包含了一個測試. 是一個在測試擴展包內的測試, 因此是無法直接訪問到 isSpace 內部函數的, 因此 fmt 通過一個祕密出口導出了 isSpace 函數. export_test.go 文件就是專門用於測試擴展包的祕密出口. ```Go package fmt @@ -62,5 +62,5 @@ package fmt var IsSpace = isSpace ``` -這個測試文件併沒有定義測試代碼; 它隻是通過 fmt.IsSpace 簡單導齣了內部的 isSpace 函數, 提供給測試擴展包使用. 這個技巧可以廣泛用於位於測試擴展包的白盒測試. +這個測試文件併沒有定義測試代碼; 它隻是通過 fmt.IsSpace 簡單導出了內部的 isSpace 函數, 提供給測試擴展包使用. 這個技巧可以廣泛用於位於測試擴展包的白盒測試. diff --git a/ch11/ch11-02-5.md b/ch11/ch11-02-5.md index af5bac8..501c56b 100644 --- a/ch11/ch11-02-5.md +++ b/ch11/ch11-02-5.md @@ -1,9 +1,9 @@ ### 11.2.5. 編寫有效的測試 -許多Go新人會驚異與它的極簡的測試框架. 很多其他語言的測試框架都提供了識别測試函數的機製(通常使用反射或元數據), 通過設置一些 ‘‘setup’’ 和 ‘‘teardown’’ 的鉤子函數來執行測試用例運行的初始化或之後的清理操作, 同時測試工具箱還提供了很多類似assert斷言, 比較值, 格式化輸齣錯誤信息和停止一個識别的測試等輔助函數(通常使用異常機製). 雖然這些機製可以使得測試非常簡潔, 但是測試輸齣的日誌卻像火星文一般難以理解. 此外, 雖然測試最終也會輸齣 PASS 或 FAIL 的報告, 但是它們提供的信息格式卻非常不利於代碼維護者快速定位問題, 因爲失敗的信息的具體含義是非常隱患的, 比如 "assert: 0 == 1" 或 成頁的海量跟蹤日誌. +許多Go新人會驚異與它的極簡的測試框架. 很多其他語言的測試框架都提供了識别測試函數的機製(通常使用反射或元數據), 通過設置一些 ‘‘setup’’ 和 ‘‘teardown’’ 的鉤子函數來執行測試用例運行的初始化或之後的清理操作, 同時測試工具箱還提供了很多類似assert斷言, 比較值, 格式化輸出錯誤信息和停止一個識别的測試等輔助函數(通常使用異常機製). 雖然這些機製可以使得測試非常簡潔, 但是測試輸出的日誌卻像火星文一般難以理解. 此外, 雖然測試最終也會輸出 PASS 或 FAIL 的報告, 但是它們提供的信息格式卻非常不利於代碼維護者快速定位問題, 因爲失敗的信息的具體含義是非常隱患的, 比如 "assert: 0 == 1" 或 成頁的海量跟蹤日誌. -Go語言的測試風格則形成鮮明對比. 它期望測試者自己完成大部分的工作, 定義函數避免重複, 就像普通編程那樣. 編寫測試併不是一個機械的填充過程; 一個測試也有自己的接口, 盡管它的維護者也是測試僅有的一個用戶. 一個好的測試不應該引發其他無關的錯誤信息, 它隻要清晰簡潔地描述問題的癥狀卽可, 有時候可能還需要一些上下文信息. 在理想情況下, 維護者可以在不看代碼的情況下就能根據錯誤信息定位錯誤産生的原因. 一個好的測試不應該在遇到一點小錯誤就立刻退齣測試, 它應該嚐試報告更多的測試, 因此我們可能從多個失敗測試的模式中發現錯誤産生的規律. +Go語言的測試風格則形成鮮明對比. 它期望測試者自己完成大部分的工作, 定義函數避免重複, 就像普通編程那樣. 編寫測試併不是一個機械的填充過程; 一個測試也有自己的接口, 盡管它的維護者也是測試僅有的一個用戶. 一個好的測試不應該引發其他無關的錯誤信息, 它隻要清晰簡潔地描述問題的癥狀卽可, 有時候可能還需要一些上下文信息. 在理想情況下, 維護者可以在不看代碼的情況下就能根據錯誤信息定位錯誤産生的原因. 一個好的測試不應該在遇到一點小錯誤就立刻退出測試, 它應該嚐試報告更多的測試, 因此我們可能從多個失敗測試的模式中發現錯誤産生的規律. 下面的斷言函數比較兩個值, 然後生成一個通用的錯誤信息, 併停止程序. 它很方便使用也確實有效果, 但是當識别的時候, 錯誤時打印的信息幾乎是沒有價值的. 它併沒有爲解決問題提供一個很好的入口. @@ -26,7 +26,7 @@ func TestSplit(t *testing.T) { } ``` -從這個意義上説, 斷言函數犯了過早抽象的錯誤: 僅僅測試兩個整數是否相同, 而放棄了根據上下文提供更有意義的錯誤信息的做法. 我們可以根據具體的錯誤打印一個更有價值的錯誤信息, 就像下面例子那樣. 測試在隻有一次重複的模式齣現時引入抽象. +從這個意義上説, 斷言函數犯了過早抽象的錯誤: 僅僅測試兩個整數是否相同, 而放棄了根據上下文提供更有意義的錯誤信息的做法. 我們可以根據具體的錯誤打印一個更有價值的錯誤信息, 就像下面例子那樣. 測試在隻有一次重複的模式出現時引入抽象. ```Go func TestSplit(t *testing.T) { @@ -42,8 +42,8 @@ func TestSplit(t *testing.T) { 現在的測試不僅報告了調用的具體函數, 它的輸入, 和結果的意義; 併且打印的眞實返迴的值和期望返迴的值; 併且卽使斷言失敗依然會繼續嚐試運行更多的測試. 一旦我們寫了這樣結構的測試, 下一步自然不是用更多的if語句來擴展測試用例, 我們可以用像 IsPalindrome 的表驅動測試那樣來準備更多的 s, sep 測試用例. -前面的例子併不需要額外的輔助函數, 如果如果有可以使測試代碼更簡單的方法我們也樂意接受. (我們將在 13.3節 看到一個 reflect.DeepEqual 輔助函數.) 開始一個好的測試的關鍵是通過實現你眞正想要的具體行爲, 然後纔是考慮然後簡化測試代碼. 最好的結果是直接從庫的抽象接口開始, 針對公共接口編寫一些測試函數. +前面的例子併不需要額外的輔助函數, 如果如果有可以使測試代碼更簡單的方法我們也樂意接受. (我們將在 13.3節 看到一個 reflect.DeepEqual 輔助函數.) 開始一個好的測試的關鍵是通過實現你眞正想要的具體行爲, 然後才是考慮然後簡化測試代碼. 最好的結果是直接從庫的抽象接口開始, 針對公共接口編寫一些測試函數. -**練習11.5:** 用表格驅動的技術擴展TestSplit測試, 併打印期望的輸齣結果. +**練習11.5:** 用表格驅動的技術擴展TestSplit測試, 併打印期望的輸出結果. diff --git a/ch11/ch11-02-6.md b/ch11/ch11-02-6.md index 7f0b7a3..4231dc6 100644 --- a/ch11/ch11-02-6.md +++ b/ch11/ch11-02-6.md @@ -1,8 +1,8 @@ ### 11.2.6. 避免的不穩定的測試 -如果一個應用程序對於新齣現的但有效的輸入經常失敗説明程序不夠穩健; 同樣如果一個測試僅僅因爲聲音變化就會導致失敗也是不合邏輯的. 就像一個不夠穩健的程序會挫敗它的用戶一樣, 一個脆弱性測試同樣會激怒它的維護者. 最脆弱的測試代碼會在程序沒有任何變化的時候産生不同的結果, 時好時壞, 處理它們會耗費大量的時間但是併不會得到任何好處. +如果一個應用程序對於新出現的但有效的輸入經常失敗説明程序不夠穩健; 同樣如果一個測試僅僅因爲聲音變化就會導致失敗也是不合邏輯的. 就像一個不夠穩健的程序會挫敗它的用戶一樣, 一個脆弱性測試同樣會激怒它的維護者. 最脆弱的測試代碼會在程序沒有任何變化的時候産生不同的結果, 時好時壞, 處理它們會耗費大量的時間但是併不會得到任何好處. -當一個測試函數産生一個複雜的輸齣如一個很長的字符串, 或一個精心設計的數據結構, 或一個文件, 它可以用於和預設的‘‘golden’’結果數據對比, 用這種簡單方式寫測試是誘人的. 但是隨着項目的發展, 輸齣的某些部分很可能會發生變化, 盡管很可能是一個改進的實現導致的. 而且不僅僅是輸齣部分, 函數複雜複製的輸入部分可能也跟着變化了, 因此測試使用的輸入也就不在有效了. +當一個測試函數産生一個複雜的輸出如一個很長的字符串, 或一個精心設計的數據結構, 或一個文件, 它可以用於和預設的‘‘golden’’結果數據對比, 用這種簡單方式寫測試是誘人的. 但是隨着項目的發展, 輸出的某些部分很可能會發生變化, 盡管很可能是一個改進的實現導致的. 而且不僅僅是輸出部分, 函數複雜複製的輸入部分可能也跟着變化了, 因此測試使用的輸入也就不在有效了. -避免脆弱測試代碼的方法是隻檢測你眞正關心的屬性. 保存測試代碼的簡潔和內部結構的穩定. 特别是對斷言部分要有所選擇. 不要檢査字符串的全匹配, 但是尋找相關的子字符串, 因爲某些子字符串在項目的發展中是比較穩定不變的. 通常編寫一個重複雜的輸齣中提取必要精華信息以用於斷言是值得的, 雖然這可能會帶來很多前期的工作, 但是它可以幫助迅速及時脩複因爲項目演化而導致的不合邏輯的失敗測試. +避免脆弱測試代碼的方法是隻檢測你眞正關心的屬性. 保存測試代碼的簡潔和內部結構的穩定. 特别是對斷言部分要有所選擇. 不要檢査字符串的全匹配, 但是尋找相關的子字符串, 因爲某些子字符串在項目的發展中是比較穩定不變的. 通常編寫一個重複雜的輸出中提取必要精華信息以用於斷言是值得的, 雖然這可能會帶來很多前期的工作, 但是它可以幫助迅速及時脩複因爲項目演化而導致的不合邏輯的失敗測試. diff --git a/ch11/ch11-02.md b/ch11/ch11-02.md index b4fcd54..197562c 100644 --- a/ch11/ch11-02.md +++ b/ch11/ch11-02.md @@ -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 @@ -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) { @@ -98,7 +98,7 @@ FAIL FAIL gopl.io/ch11/word1 0.014s ``` -先編寫測試用例併觀察到測試用例觸發了和用戶報告的錯誤相同的描述是一個好的測試習慣. 隻有這樣, 我們纔能定位我們要眞正解決的問題. +先編寫測試用例併觀察到測試用例觸發了和用戶報告的錯誤相同的描述是一個好的測試習慣. 隻有這樣, 我們才能定位我們要眞正解決的問題. 先寫測試用例的另好處是, 運行測試通常會比手工描述報告的處理更快, 這讓我們可以進行快速地迭代. 如果測試集有很多運行緩慢的測試, 我們可以通過隻選擇運行某些特定的測試來加快測試速度. @@ -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" @@ -207,11 +207,11 @@ ok gopl.io/ch11/word2 0.015s 這種表格驅動的測試在Go中很常見的. 我們很容易想表格添加新的測試數據, 併且後面的測試邏輯也沒有冗餘, 這樣我們可以更好地完善錯誤信息. -失敗的測試的輸齣併不包括調用 t.Errorf 時刻的堆棧調用信息. 不像其他語言或測試框架的 assert 斷言, t.Errorf 調用也沒有引起 panic 或停止測試的執行. 卽使表格中前面的數據導致了測試的失敗, 表格後面的測試數據依然會運行測試, 因此在一個測試中我們可能了解多個失敗的信息. +失敗的測試的輸出併不包括調用 t.Errorf 時刻的堆棧調用信息. 不像其他語言或測試框架的 assert 斷言, t.Errorf 調用也沒有引起 panic 或停止測試的執行. 卽使表格中前面的數據導致了測試的失敗, 表格後面的測試數據依然會運行測試, 因此在一個測試中我們可能了解多個失敗的信息. 如果我們眞的需要停止測試, 或許是因爲初始化失敗或可能是早先的錯誤導致了後續錯誤等原因, 我們可以使用 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 程序編寫測試. diff --git a/ch11/ch11-03.md b/ch11/ch11-03.md index ad564c5..4d4c272 100644 --- a/ch11/ch11-03.md +++ b/ch11/ch11-03.md @@ -81,7 +81,7 @@ $ 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` 標誌參數, 那麽將在每個代碼塊插入一個計數器而不是布爾標誌量. 在統計結果中記録了每個塊的執行次數, 這可以用於衡量哪些是被頻繁執行的熱點代碼. diff --git a/ch11/ch11-04.md b/ch11/ch11-04.md index 785a1b4..3079aca 100644 --- a/ch11/ch11-04.md +++ b/ch11/ch11-04.md @@ -107,7 +107,7 @@ func Benchmark1000(b *testing.B) { benchmark(b, 1000) } **練習 11.6:** 爲 2.6.2節 的 練習 2.4 和 練習 2.5 的 PopCount 函數編寫基準測試. 看看基於表格算法在不同情況下的性能. -**練習 11.7:** 爲 *IntSet (§6.5) 的 Add, UnionWith 和 其他方法編寫基準測試, 使用大量隨機齣入. 你可以讓這些方法跑多快? 選擇字的大小對於性能的影響如何? IntSet 和基於內建 map 的實現相比有多快? +**練習 11.7:** 爲 *IntSet (§6.5) 的 Add, UnionWith 和 其他方法編寫基準測試, 使用大量隨機出入. 你可以讓這些方法跑多快? 選擇字的大小對於性能的影響如何? IntSet 和基於內建 map 的實現相比有多快? diff --git a/ch11/ch11-05.md b/ch11/ch11-05.md index 233f09c..0764a09 100644 --- a/ch11/ch11-05.md +++ b/ch11/ch11-05.md @@ -4,7 +4,7 @@ > 毫無疑問, 效率會導致各種濫用. 程序員需要浪費大量的時間思考, 或者擔心, 被部分程序的速度所榦擾, 實際上這些嚐試提陞效率的行爲可能産生強烈的負面影響, 特别是當調試和維護的時候. 我們不應該過度糾結於細節的優化, 應該説約97%的場景: 過早的優化是萬惡之源. > -> 我們當然不應該放棄那關鍵的3%的機會. 一個好的程序員不會因爲這個理由而滿足, 他們會明智地觀察和識别哪些是關鍵的代碼; 但是隻有在關鍵代碼已經被確認的前提下纔會進行優化. 對於判斷哪些部分是關鍵代碼是經常容易犯經驗性錯誤的地方, 因此程序員普通使用的測量工具, 使得他們的直覺很不靠譜. +> 我們當然不應該放棄那關鍵的3%的機會. 一個好的程序員不會因爲這個理由而滿足, 他們會明智地觀察和識别哪些是關鍵的代碼; 但是隻有在關鍵代碼已經被確認的前提下才會進行優化. 對於判斷哪些部分是關鍵代碼是經常容易犯經驗性錯誤的地方, 因此程序員普通使用的測量工具, 使得他們的直覺很不靠譜. 當我們想仔細觀察我們程序的運行速度的時候, 最好的技術是如何識别關鍵代碼. 自動化的剖析技術是基於程序執行期間一些抽樣數據, 然後推斷後面的執行狀態; 最終産生一個運行時間的統計數據文件. @@ -14,7 +14,7 @@ CPU分析文件標識了函數執行時所需要的CPU時間. 當前運行的繫 堆分析則記録了程序的內存使用情況. 每個內存分配操作都會觸發內部平均內存分配例程, 每個 512KB 的內存申請都會觸發一個事件. -阻塞分析則記録了goroutine最大的阻塞操作, 例如繫統調用, 管道發送和接收, 還有穫取鎖等. 分析庫會記録每個goroutine被阻塞時的相關操作. +阻塞分析則記録了goroutine最大的阻塞操作, 例如繫統調用, 管道發送和接收, 還有獲取鎖等. 分析庫會記録每個goroutine被阻塞時的相關操作. 在測試環境下隻需要一個標誌參數就可以生成各種分析文件. 當一次使用多個標誌參數時需要當心, 因爲分析操作本身也可能會影像程序的運行. @@ -57,7 +57,7 @@ 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包的內存分配的函數, 那麽減少內存消耗可能是一個值得嚐試的優化策略. diff --git a/ch11/ch11-06.md b/ch11/ch11-06.md index 51db31f..6c52482 100644 --- a/ch11/ch11-06.md +++ b/ch11/ch11-06.md @@ -16,7 +16,7 @@ func ExampleIsPalindrome() { 根據示例函數的後綴名部分, godoc 的web文檔會將一個示例函數關聯到某個具體函數或包本身, 因此 ExampleIsPalindrome 示例函數將是 IsPalindrome 函數文檔的一部分, Example 示例函數將是包文檔的一部分. -示例文檔的第二個用處是在 `go test` 執行測試的時候也運行示例函數測試. 如果示例函數內含有類似上面例子中的 `/ Output:` 這樣的註釋, 那麽測試工具會執行這個示例函數, 然後檢測這個示例函數的標準輸齣和註釋是否匹配. +示例文檔的第二個用處是在 `go test` 執行測試的時候也運行示例函數測試. 如果示例函數內含有類似上面例子中的 `/ Output:` 這樣的註釋, 那麽測試工具會執行這個示例函數, 然後檢測這個示例函數的標準輸出和註釋是否匹配. 示例函數的第三個目的提供一個眞實的演練場. golang.org 是由 dogoc 提供的服務, 它使用了 Go Playground 技術讓用戶可以在瀏覽器中在線編輯和運行每個示例函數, 就像 圖 11.4 所示的那樣. 這通常是學習函數使用或Go語言特性的最快方式. diff --git a/ch11/ch11.md b/ch11/ch11.md index 92d14ab..931691e 100644 --- a/ch11/ch11.md +++ b/ch11/ch11.md @@ -10,7 +10,7 @@ Maurice Wilkes, 第一個存儲程序計算機 EDSAC 的設計者, 1949年在他 Go語言的測試技術是相對低級的. 它依賴一個 'go test' 測試命令, 和一組按照約定方式編寫的測試函數, 測試命令可以運行測試函數. 編寫相對輕量級的純測試代碼是有效的, 而且它很容易延伸到基準測試和示例文檔. -在實踐中, 編寫測試代碼和編寫程序本身併沒有多大區别. 我們編寫的每一個函數也是針對每個具體的任務. 我們必鬚小心處理邊界條件, 思考合適的數據結構, 推斷合適的輸入應該産生什麽樣的結果輸齣. 編程測試代碼和編寫普通的Go代碼過程是類似的; 它併不需要學習新的符號, 規則和工具. +在實踐中, 編寫測試代碼和編寫程序本身併沒有多大區别. 我們編寫的每一個函數也是針對每個具體的任務. 我們必鬚小心處理邊界條件, 思考合適的數據結構, 推斷合適的輸入應該産生什麽樣的結果輸出. 編程測試代碼和編寫普通的Go代碼過程是類似的; 它併不需要學習新的符號, 規則和工具. diff --git a/ch12/ch12-01.md b/ch12/ch12-01.md index 78edf95..93fc2fd 100644 --- a/ch12/ch12-01.md +++ b/ch12/ch12-01.md @@ -32,7 +32,7 @@ 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 的類型, 這會導致對這些庫的依賴. 沒有一種方法來檢査未知類型的表示方式, 我們被卡住了. 這就是我們爲何需要反射的原因. diff --git a/ch12/ch12-02.md b/ch12/ch12-02.md index 049e232..7f7b7c2 100644 --- a/ch12/ch12-02.md +++ b/ch12/ch12-02.md @@ -20,7 +20,7 @@ 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" @@ -53,7 +53,7 @@ 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 無效類型.) diff --git a/ch12/ch12-03.md b/ch12/ch12-03.md index cd19a4d..464fb39 100644 --- a/ch12/ch12-03.md +++ b/ch12/ch12-03.md @@ -1,3 +1,228 @@ ## 12.3. Display遞歸打印 -TODO +接下來,讓我們看看如何改善聚合數據類型的顯示。我們併不想完全剋隆一個fmt.Sprint函數,我們隻是像構建一個用於調式用的Display函數,給定一個聚合類型x,打印這個值對應的完整的結構,同時記録每個發現的每個元素的路徑。讓我們從一個例子開始。 + +```Go +e, _ := eval.Parse("sqrt(A / pi)") +Display("e", e) +``` + +在上面的調用中,傳入Display函數的參數是在7.9節一個表達式求值函數返迴的語法樹。Display函數的輸出如下: + +```Go +Display e (eval.call): +e.fn = "sqrt" +e.args[0].type = eval.binary +e.args[0].value.op = 47 +e.args[0].value.x.type = eval.Var +e.args[0].value.x.value = "A" +e.args[0].value.y.type = eval.Var +e.args[0].value.y.value = "pi" +``` + +在可能的情況下,你應該避免在一個包中暴露和反射相關的接口。我們將定義一個未導出的display函數用於遞歸處理工作,導出的是Display函數,它隻是display函數簡單的包裝以接受interface{}類型的參數: + +```Go +gopl.io/ch12/display + +func Display(name string, x interface{}) { + fmt.Printf("Display %s (%T):\n", name, x) + display(name, reflect.ValueOf(x)) +} +``` + +在display函數中,我們使用了前面定義的打印基礎類型——基本類型、函數和chan等——元素值的formatAtom函數,但是我們會使用reflect.Value的方法來遞歸顯示聚合類型的每一個成員或元素。在遞歸下降過程中,path字符串,從最開始傳入的起始值(這里是“e”),將逐步增長以表示如何達到當前值(例如“e.args[0].value”)。 + +因爲我們不再模擬fmt.Sprint函數,我們將直接使用fmt包來簡化我們的例子實現。 + +```Go +func display(path string, v reflect.Value) { + switch v.Kind() { + case reflect.Invalid: + fmt.Printf("%s = invalid\n", path) + case reflect.Slice, reflect.Array: + for i := 0; i < v.Len(); i++ { + display(fmt.Sprintf("%s[%d]", path, i), v.Index(i)) + } + case reflect.Struct: + for i := 0; i < v.NumField(); i++ { + fieldPath := fmt.Sprintf("%s.%s", path, v.Type().Field(i).Name) + display(fieldPath, v.Field(i)) + } + case reflect.Map: + for _, key := range v.MapKeys() { + display(fmt.Sprintf("%s[%s]", path, + formatAtom(key)), v.MapIndex(key)) + } + case reflect.Ptr: + if v.IsNil() { + fmt.Printf("%s = nil\n", path) + } else { + display(fmt.Sprintf("(*%s)", path), v.Elem()) + } + case reflect.Interface: + if v.IsNil() { + fmt.Printf("%s = nil\n", path) + } else { + fmt.Printf("%s.type = %s\n", path, v.Elem().Type()) + display(path+".value", v.Elem()) + } + default: // basic types, channels, funcs + fmt.Printf("%s = %s\n", path, formatAtom(v)) + } +} +``` + +讓我們針對不同類型分别討論。 + +**Slice和數組:** 兩種的處理邏輯是一樣的。Len方法返迴slice或數組值中的元素個數,Index(i)活動索引i對應的元素,返迴的也是一個reflect.Value類型的值;如果索引i超出范圍的話將導致panic異常,這些行爲和數組或slice類型內建的len(a)和a[i]等操作類似。display針對序列中的每個元素遞歸調用自身處理,我們通過在遞歸處理時向path附加“[i]”來表示訪問路徑。 + +雖然reflect.Value類型帶有很多方法,但是隻有少數的方法對任意值都是可以安全調用的。例如,Index方法隻能對Slice、數組或字符串類型的值調用,其它類型如果調用將導致panic異常。 + +**結構體:** NumField方法報告結構體中成員的數量,Field(i)以reflect.Value類型返迴第i個成員的值。成員列表包含了匿名成員在內的全部成員。通過在path添加“.f”來表示成員路徑,我們必鬚獲得結構體對應的reflect.Type類型信息,包含結構體類型和第i個成員的名字。 + +**Maps:** MapKeys方法返迴一個reflect.Value類型的slice,每一個都對應map的可以。和往常一樣,遍歷map時順序是隨機的。MapIndex(key)返迴map中key對應的value。我們向path添加“[key]”來表示訪問路徑。(我們這里有一個未完成的工作。其實map的key的類型併不局限於formatAtom能完美處理的類型;數組、結構體和接口都可以作爲map的key。針對這種類型,完善key的顯示信息是練習12.1的任務。) + +**指針:** Elem方法返迴指針指向的變量,還是reflect.Value類型。技術指針是nil,這個操作也是安全的,在這種情況下指針是Invalid無效類型,但是我們可以用IsNil方法來顯式地測試一個空指針,這樣我們可以打印更合適的信息。我們在path前面添加“*”,併用括弧包含以避免歧義。 + +**接口:** 再一次,我們使用IsNil方法來測試接口是否是nil,如果不是,我們可以調用v.Elem()來獲取接口對應的動態值,併且打印對應的類型和值。 + +現在我們的Display函數總算完工了,讓我們看看它的表現吧。下面的Movie類型是在4.5節的電影類型上演變來的: + +```Go +type Movie struct { + Title, Subtitle string + Year int + Color bool + Actor map[string]string + Oscars []string + Sequel *string +} +``` + +讓我們聲明一個該類型的變量,然後看看Display函數如何顯示它: + +```Go +strangelove := Movie{ + Title: "Dr. Strangelove", + Subtitle: "How I Learned to Stop Worrying and Love the Bomb", + Year: 1964, + Color: false, + Actor: map[string]string{ + "Dr. Strangelove": "Peter Sellers", + "Grp. Capt. Lionel Mandrake": "Peter Sellers", + "Pres. Merkin Muffley": "Peter Sellers", + "Gen. Buck Turgidson": "George C. Scott", + "Brig. Gen. Jack D. Ripper": "Sterling Hayden", + `Maj. T.J. "King" Kong`: "Slim Pickens", + }, + + Oscars: []string{ + "Best Actor (Nomin.)", + "Best Adapted Screenplay (Nomin.)", + "Best Director (Nomin.)", + "Best Picture (Nomin.)", + }, +} +``` + +Display("strangelove", strangelove)調用將顯示(strangelove電影對應的中文名是《奇愛博士》): + +```Go +Display strangelove (display.Movie): +strangelove.Title = "Dr. Strangelove" +strangelove.Subtitle = "How I Learned to Stop Worrying and Love the Bomb" +strangelove.Year = 1964 +strangelove.Color = false +strangelove.Actor["Gen. Buck Turgidson"] = "George C. Scott" +strangelove.Actor["Brig. Gen. Jack D. Ripper"] = "Sterling Hayden" +strangelove.Actor["Maj. T.J. \"King\" Kong"] = "Slim Pickens" +strangelove.Actor["Dr. Strangelove"] = "Peter Sellers" +strangelove.Actor["Grp. Capt. Lionel Mandrake"] = "Peter Sellers" +strangelove.Actor["Pres. Merkin Muffley"] = "Peter Sellers" +strangelove.Oscars[0] = "Best Actor (Nomin.)" +strangelove.Oscars[1] = "Best Adapted Screenplay (Nomin.)" +strangelove.Oscars[2] = "Best Director (Nomin.)" +strangelove.Oscars[3] = "Best Picture (Nomin.)" +strangelove.Sequel = nil +``` + +我們也可以使用Display函數來顯示標準庫中類型的內部結構,例如`*os.File`類型: + +```Go +Display("os.Stderr", os.Stderr) +// Output: +// Display os.Stderr (*os.File): +// (*(*os.Stderr).file).fd = 2 +// (*(*os.Stderr).file).name = "/dev/stderr" +// (*(*os.Stderr).file).nepipe = 0 +``` + +要註意的是,結構體中未導出的成員對反射也是可見的。需要當心的是這個例子的輸出在不同操作繫統上可能是不同的,併且隨着標準庫的發展也可能導致結果不同。(這也是將這些成員定義爲私有成員的原因之一!)我們深圳可以用Display函數來顯示reflect.Value,來査看`*os.File`類型的內部表示方式。`Display("rV", reflect.ValueOf(os.Stderr))`調用的輸出如下,當然不同環境得到的結果可能有差異: + +```Go +Display rV (reflect.Value): +(*rV.typ).size = 8 +(*rV.typ).hash = 871609668 +(*rV.typ).align = 8 +(*rV.typ).fieldAlign = 8 +(*rV.typ).kind = 22 +(*(*rV.typ).string) = "*os.File" + +(*(*(*rV.typ).uncommonType).methods[0].name) = "Chdir" +(*(*(*(*rV.typ).uncommonType).methods[0].mtyp).string) = "func() error" +(*(*(*(*rV.typ).uncommonType).methods[0].typ).string) = "func(*os.File) error" +... +``` + +觀察下面兩個例子的區别: + +```Go +var i interface{} = 3 + +Display("i", i) +// Output: +// Display i (int): +// i = 3 + +Display("&i", &i) +// Output: +// Display &i (*interface {}): +// (*&i).type = int +// (*&i).value = 3 +``` + +在第一個例子中,Display函數將調用reflect.ValueOf(i),它返迴一個Int類型的值。正如我們在12.2節中提到的,reflect.ValueOf總是返迴一個值的具體類型,因爲它是從一個接口值提取的內容。 + +在第二個例子中,Display函數調用的是reflect.ValueOf(&i),它返迴一個指向i的指針,對應Ptr類型。在switch的Ptr分支中,通過調用Elem來返迴這個值,返迴一個Value來表示i,對應Interface類型。一個間接獲得的Value,就像這一個,可能代表任意類型的值,包括接口類型。內部的display函數遞歸調用自身,這次它將打印接口的動態類型和值。 + +目前的實現,Display如果顯示一個帶環的數據結構將會陷入死循環,例如首位項鏈的鏈表: + +```Go +// a struct that points to itself +type Cycle struct{ Value int; Tail *Cycle } +var c Cycle +c = Cycle{42, &c} +Display("c", c) +``` + +Display會永遠不停地進行深度遞歸打印: + +```Go +Display c (display.Cycle): +c.Value = 42 +(*c.Tail).Value = 42 +(*(*c.Tail).Tail).Value = 42 +(*(*(*c.Tail).Tail).Tail).Value = 42 +...ad infinitum... +``` + +許多Go語言程序都包含了一些循環的數據結果。Display支持這類帶環的數據結構是比較棘手的,需要增加一個額外的記録訪問的路徑;代價是昂貴的。一般的解決方案是采用不安全的語言特性,我們將在13.3節看到具體的解決方案。 + +帶環的數據結構很少會對fmt.Sprint函數造成問題,因爲它很少嚐試打印完整的數據結構。例如,當它遇到一個指針的時候,它隻是簡單第打印指針的數值。雖然,在打印包含自身的slice或map時可能遇到睏難,但是不保證處理這種是罕見情況卻可以避免額外的麻煩。 + +**練習 12.1:** 擴展Displayhans,以便它可以顯示包含以結構體或數組作爲map的key類型的值。 + +**練習 12.2:** 增強display函數的穩健性,通過記録邊界的步數來確保在超出一定限製前放棄遞歸。(在13.3節,我們會看到另一種探測數據結構是否存在環的技術。) + + diff --git a/ch12/ch12-04.md b/ch12/ch12-04.md index 5bb26e2..fb57747 100644 --- a/ch12/ch12-04.md +++ b/ch12/ch12-04.md @@ -1,3 +1,155 @@ ## 12.4. 示例: 編碼S表達式 -TODO +Display是一個用於顯示結構化數據的調試工具,但是它併不能將任意的Go語言對象編碼爲通用消息然後用於進程間通信。 + +正如我們在4.5節中中看到的,Go語言的標準庫支持了包括JSON、XML和ASN.1等多種編碼格式。還有另一種依然被廣泛使用的格式是S表達式格式,采用類似Lisp語言的語法。但是和其他編碼格式不同的是,Go語言自帶的標準庫併不支持S表達式,主要是因爲它沒有一個公認的標準規范。 + +在本節中,我們將定義一個包用於將Go語言的對象編碼爲S表達式格式,它支持以下結構: + +``` +42 integer +"hello" string (with Go-style quotation) +foo symbol (an unquoted name) +(1 2 3) list (zero or more items enclosed in parentheses) +``` + +布爾型習慣上使用t符號表示true,空列表或nil符號表示false,但是爲了簡單起見,我們暫時忽略布爾類型。同時忽略的還有chan管道和函數,因爲通過反射併無法知道它們的確切狀態。我們忽略的還浮點數、複數和interface。支持它們是練習12.3的任務。 + +我們將Go語言的類型編碼爲S表達式的方法如下。整數和字符串以自然的方式編碼。Nil值編碼爲nil符號。數組和slice被編碼爲一個列表。 + +結構體被編碼爲成員對象的列表,每個成員對象對應一個個僅有兩個元素的子列表,其中子列表的第一個元素是成員的名字,子列表的第二個元素是成員的值。Map被編碼爲鍵值對的列表。傳統上,S表達式使用點狀符號列表(key . value)結構來表示key/value對,而不是用一個含雙元素的列表,不過爲了簡單我們忽略了點狀符號列表。 + +編碼是由一個encode遞歸函數完成,如下所示。它的結構本質上和前面的Display函數類似: + +```Go +gopl.io/ch12/sexpr + +func encode(buf *bytes.Buffer, v reflect.Value) error { + switch v.Kind() { + case reflect.Invalid: + buf.WriteString("nil") + + case reflect.Int, reflect.Int8, reflect.Int16, + reflect.Int32, reflect.Int64: + fmt.Fprintf(buf, "%d", v.Int()) + + case reflect.Uint, reflect.Uint8, reflect.Uint16, + reflect.Uint32, reflect.Uint64, reflect.Uintptr: + fmt.Fprintf(buf, "%d", v.Uint()) + + case reflect.String: + fmt.Fprintf(buf, "%q", v.String()) + + case reflect.Ptr: + return encode(buf, v.Elem()) + + case reflect.Array, reflect.Slice: // (value ...) + buf.WriteByte('(') + for i := 0; i < v.Len(); i++ { + if i > 0 { + buf.WriteByte(' ') + } + if err := encode(buf, v.Index(i)); err != nil { + return err + } + } + buf.WriteByte(')') + + case reflect.Struct: // ((name value) ...) + buf.WriteByte('(') + for i := 0; i < v.NumField(); i++ { + if i > 0 { + buf.WriteByte(' ') + } + fmt.Fprintf(buf, "(%s ", v.Type().Field(i).Name) + if err := encode(buf, v.Field(i)); err != nil { + return err + } + buf.WriteByte(')') + } + buf.WriteByte(')') + + case reflect.Map: // ((key value) ...) + buf.WriteByte('(') + for i, key := range v.MapKeys() { + if i > 0 { + buf.WriteByte(' ') + } + buf.WriteByte('(') + if err := encode(buf, key); err != nil { + return err + } + buf.WriteByte(' ') + if err := encode(buf, v.MapIndex(key)); err != nil { + return err + } + buf.WriteByte(')') + } + buf.WriteByte(')') + + default: // float, complex, bool, chan, func, interface + return fmt.Errorf("unsupported type: %s", v.Type()) + } + return nil +} +``` + +Marshal函數是對encode的保證,以保持和encoding/...下其它包有着相似的API: + +```Go +// Marshal encodes a Go value in S-expression form. +func Marshal(v interface{}) ([]byte, error) { + var buf bytes.Buffer + if err := encode(&buf, reflect.ValueOf(v)); err != nil { + return nil, err + } + return buf.Bytes(), nil +} +``` + +下面是Marshal對12.3節的strangelove變量編碼後的結果: + +``` +((Title "Dr. Strangelove") (Subtitle "How I Learned to Stop Worrying and Lo +ve the Bomb") (Year 1964) (Actor (("Grp. Capt. Lionel Mandrake" "Peter Sell +ers") ("Pres. Merkin Muffley" "Peter Sellers") ("Gen. Buck Turgidson" "Geor +ge C. Scott") ("Brig. Gen. Jack D. Ripper" "Sterling Hayden") ("Maj. T.J. \ +"King\" Kong" "Slim Pickens") ("Dr. Strangelove" "Peter Sellers"))) (Oscars +("Best Actor (Nomin.)" "Best Adapted Screenplay (Nomin.)" "Best Director (N +omin.)" "Best Picture (Nomin.)")) (Sequel nil)) +``` + +整個輸出編碼爲一行中以減少輸出的大小,但是也很難閲讀。這里有一個對S表達式格式化的約定。編寫一個S表達式的格式化函數將作爲一個具有挑戰性的練習任務;不過 http://gopl.io 也提供了一個簡單的版本。 + +``` +((Title "Dr. Strangelove") + (Subtitle "How I Learned to Stop Worrying and Love the Bomb") + (Year 1964) + (Actor (("Grp. Capt. Lionel Mandrake" "Peter Sellers") + ("Pres. Merkin Muffley" "Peter Sellers") + ("Gen. Buck Turgidson" "George C. Scott") + ("Brig. Gen. Jack D. Ripper" "Sterling Hayden") + ("Maj. T.J. \"King\" Kong" "Slim Pickens") + ("Dr. Strangelove" "Peter Sellers"))) + (Oscars ("Best Actor (Nomin.)" + "Best Adapted Screenplay (Nomin.)" + "Best Director (Nomin.)" + "Best Picture (Nomin.)")) + (Sequel nil)) +``` + +和fmt.Print、json.Marshal、Display函數類似,sexpr.Marshal函數處理帶環的數據結構也會陷入死循環。 + +在12.6節中,我們將給出S表達式解碼器的實現步驟,但是在那之前,我們還需要先了解如果通過反射技術來更新程序的變量。 + +**練習 12.3:** 實現encode函數缺少的分支。將布爾類型編碼爲t和nil,浮點數編碼爲Go語言的格式,複數1+2i編碼爲#C(1.0 2.0)格式。接口編碼爲類型名和值對,例如("[]int" (1 2 3)),但是這個形式可能會造成歧義:reflect.Type.String方法對於不同的類型可能返迴相同的結果。 + +**練習 12.4:** 脩改encode函數,以上面的格式化形式輸出S表達式。 + +**練習 12.5:** 脩改encode函數,用JSON格式代替S表達式格式。然後使用標準庫提供的json.Unmarshal解碼器來驗證函數是正確的。 + +**練習 12.6:** 脩改encode,作爲一個優化,忽略對是零值對象的編碼。 + +**練習 12.7:** 創建一個基於流式的API,用於S表達式的解碼,和json.Decoder(§4.5)函數功能類似。 + + diff --git a/ch12/ch12-05.md b/ch12/ch12-05.md index 499aeaa..995ffcf 100644 --- a/ch12/ch12-05.md +++ b/ch12/ch12-05.md @@ -1,3 +1,103 @@ ## 12.5. 通過reflect.Value脩改值 -TODO +到目前爲止,反射還隻是程序中變量的另一種訪問方式。然而,在本節中我們將重點討論如果通過反射機製來脩改變量。 + +迴想一下,Go語言中類似x、x.f[1]和*p形式的表達式都可以表示變量,但是其它如x + 1和f(2)則不是變量。一個變量就是一個可尋址的內存空間,里面存儲了一個值,併且存儲的值可以通過內存地址來更新。 + +對於reflect.Values也有類似的區别。有一些reflect.Values是可取地址的;其它一些則不可以。考慮以下的聲明語句: + +```Go +x := 2 // value type variable? +a := reflect.ValueOf(2) // 2 int no +b := reflect.ValueOf(x) // 2 int no +c := reflect.ValueOf(&x) // &x *int no +d := c.Elem() // 2 int yes (x) +``` + +其中a對應的變量則不可取地址。因爲a中的值僅僅是整數2的拷貝副本。b中的值也同樣不可取地址。c中的值還是不可取地址,它隻是一個指針`&x`的拷貝。實際上,所有通過reflect.ValueOf(x)返迴的reflect.Value都是不可取地址的。但是對於d,它是c的解引用方式生成的,指向另一個變量,因此是可取地址的。我們可以通過調用reflect.ValueOf(&x).Elem(),來獲取任意變量x對應的可取地址的Value。 + +我們可以通過調用reflect.Value的CanAddr方法來判斷其是否可以被取地址: + +```Go +fmt.Println(a.CanAddr()) // "false" +fmt.Println(b.CanAddr()) // "false" +fmt.Println(c.CanAddr()) // "false" +fmt.Println(d.CanAddr()) // "true" +``` + +每當我們通過指針間接地獲取的reflect.Value都是可取地址的,卽使開始的是一個不可取地址的Value。在反射機製中,所有關於是否支持取地址的規則都是類似的。例如,slice的索引表達式e[i]將隱式地包含一個指針,它就是可取地址的,卽使開始的e表達式不支持也沒有關繫。以此類推,reflect.ValueOf(e).Index(i)對於的值也是可取地址的,卽使原始的reflect.ValueOf(e)不支持也沒有關繫。 + +要從變量對應的可取地址的reflect.Value來訪問變量需要三個步驟。第一步是調用Addr()方法,它返迴一個Value,里面保存了指向變量的指針。然後是在Value上調用Interface()方法,也就是返迴一個interface{},里面通用包含指向變量的指針。最後,如果我們知道變量的類型,我們可以使用類型的斷言機製將得到的interface{}類型的接口強製環爲普通的類型指針。這樣我們就可以通過這個普通指針來更新變量了: + +```Go +x := 2 +d := reflect.ValueOf(&x).Elem() // d refers to the variable x +px := d.Addr().Interface().(*int) // px := &x +*px = 3 // x = 3 +fmt.Println(x) // "3" +``` + +或者,不使用指針,而是通過調用可取地址的reflect.Value的reflect.Value.Set方法來更新對於的值: + +```Go +d.Set(reflect.ValueOf(4)) +fmt.Println(x) // "4" +``` + +Set方法將在運行時執行和編譯時類似的可賦值性約束的檢査。以上代碼,變量和值都是int類型,但是如果變量是int64類型,那麽程序將拋出一個panic異常,所以關鍵問題是要確保改類型的變量可以接受對應的值: + +```Go +d.Set(reflect.ValueOf(int64(5))) // panic: int64 is not assignable to int +``` + +通用對一個不可取地址的reflect.Value調用Set方法也會導致panic異常: + +```Go +x := 2 +b := reflect.ValueOf(x) +b.Set(reflect.ValueOf(3)) // panic: Set using unaddressable value +``` + +這里有很多用於基本數據類型的Set方法:SetInt、SetUint、SetString和SetFloat等。 + +```Go +d := reflect.ValueOf(&x).Elem() +d.SetInt(3) +fmt.Println(x) // "3" +``` + +從某種程度上説,這些Set方法總是盡可能地完成任務。以SetInt爲例,隻要變量是某種類型的有符號整數就可以工作,卽使是一些命名的類型,隻要底層數據類型是有符號整數就可以,而且如果對於變量類型值太大的話會被自動截斷。但需要謹慎的是:對於一個引用interface{}類型的reflect.Value調用SetInt會導致panic異常,卽使那個interface{}變量對於整數類型也不行。 + +```Go +x := 1 +rx := reflect.ValueOf(&x).Elem() +rx.SetInt(2) // OK, x = 2 +rx.Set(reflect.ValueOf(3)) // OK, x = 3 +rx.SetString("hello") // panic: string is not assignable to int +rx.Set(reflect.ValueOf("hello")) // panic: string is not assignable to int + +var y interface{} +ry := reflect.ValueOf(&y).Elem() +ry.SetInt(2) // panic: SetInt called on interface Value +ry.Set(reflect.ValueOf(3)) // OK, y = int(3) +ry.SetString("hello") // panic: SetString called on interface Value +ry.Set(reflect.ValueOf("hello")) // OK, y = "hello" +``` + +當我們用Display顯示os.Stdout結構時,我們發現反射可以越過Go語言的導出規則的限製讀取結構體中未導出的成員,比如在類Unix繫統上os.File結構體中的fd int成員。然而,利用反射機製併不能脩改這些未導出的成員: + +```Go +stdout := reflect.ValueOf(os.Stdout).Elem() // *os.Stdout, an os.File var +fmt.Println(stdout.Type()) // "os.File" +fd := stdout.FieldByName("fd") +fmt.Println(fd.Int()) // "1" +fd.SetInt(2) // panic: unexported field +``` + +一個可取地址的reflect.Value會記録一個結構體成員是否是未導出成員,如果是的話則拒絶脩改操作。因此,CanAddr方法併不能正確反映一個變量是否是可以被脩改的。另一個相關的方法CanSet是用於檢査對應的reflect.Value是否是可取地址併可被脩改的: + +```Go +fmt.Println(fd.CanAddr(), fd.CanSet()) // "true false" +``` + + diff --git a/ch12/ch12-06.md b/ch12/ch12-06.md index b3bf560..ed3c8c8 100644 --- a/ch12/ch12-06.md +++ b/ch12/ch12-06.md @@ -1,3 +1,162 @@ ## 12.6. 示例: 解碼S表達式 -TODO +標準庫中encoding/...下每個包中提供的Marshal編碼函數都有一個對應的Unmarshal函數用於解碼。例如,我們在4.5節中看到的,要將包含JSON編碼格式的字節slice數據解碼爲我們自己的Movie類型(§12.3),我們可以這樣做: + +```Go +data := []byte{/* ... */} +var movie Movie +err := json.Unmarshal(data, &movie) +``` + +Unmarshal函數使用了反射機製類脩改movie變量的每個成員,根據輸入的內容爲Movie成員創建對應的map、結構體和slice。 + +現在讓我們爲S表達式編碼實現一個簡易的Unmarshal,類似於前面的json.Unmarshal標準庫函數,對應我們之前實現的sexpr.Marshal函數的逆操作。我們必鬚提醒一下,一個健壯的和通用的實現通常需要比例子更多的代碼,爲了便於演示我們采用了精簡的實現。我們隻支持S表達式有限的子集,同時處理錯誤的方式也比較粗暴,代碼的目的是爲了演示反射的用法,而不是構造一個實用的S表達式的解碼器。 + +詞法分析器lexer使用了標準庫中的text/scanner包將輸入流的字節數據解析爲一個個類似註釋、標識符、字符串面值和數字面值之類的標記。輸入掃描器scanner的Scan方法將提前掃描和返迴下一個記號,對於rune類型。大多數記號,比如“(”,對應一個單一rune可表示的Unicode字符,但是text/scanner也可以用小的負數表示記號標識符、字符串等由多個字符組成的記號。調用Scan方法將返迴這些記號的類型,接着調用TokenText方法將返迴記號對應的文本內容。 + +因爲每個解析器可能需要多次使用當前的記號,但是Scan會一直向前掃描,所有我們包裝了一個lexer掃描器輔助類型,用於跟蹤最近由Scan方法返迴的記號。 + +```Go +gopl.io/ch12/sexpr + +type lexer struct { + scan scanner.Scanner + token rune // the current token +} + +func (lex *lexer) next() { lex.token = lex.scan.Scan() } +func (lex *lexer) text() string { return lex.scan.TokenText() } + +func (lex *lexer) consume(want rune) { + if lex.token != want { // NOTE: Not an example of good error handling. + panic(fmt.Sprintf("got %q, want %q", lex.text(), want)) + } + lex.next() +} +``` + +現在讓我們轉到語法解析器。它主要包含兩個功能。第一個是read函數,用於讀取S表達式的當前標記,然後根據S表達式的當前標記更新可取地址的reflect.Value對應的變量v。 + +```Go +func read(lex *lexer, v reflect.Value) { + switch lex.token { + case scanner.Ident: + // The only valid identifiers are + // "nil" and struct field names. + if lex.text() == "nil" { + v.Set(reflect.Zero(v.Type())) + lex.next() + return + } + case scanner.String: + s, _ := strconv.Unquote(lex.text()) // NOTE: ignoring errors + v.SetString(s) + lex.next() + return + case scanner.Int: + i, _ := strconv.Atoi(lex.text()) // NOTE: ignoring errors + v.SetInt(int64(i)) + lex.next() + return + case '(': + lex.next() + readList(lex, v) + lex.next() // consume ')' + return + } + panic(fmt.Sprintf("unexpected token %q", lex.text())) +} +``` + +我們的S表達式使用標識符區分兩個不同類型,結構體成員名和nil值的指針。read函數值處理nil類型的標識符。當遇到scanner.Ident爲“nil”是,使用reflect.Zero函數將變量v設置爲零值。而其它任何類型的標識符,我們都作爲錯誤處理。後面的readList函數將處理結構體的成員名。 + +一個“(”標記對應一個列表的開始。第二個函數readList,將一個列表解碼到一個聚合類型中(map、結構體、slice或數組),具體類型依然於傳入待填充變量的類型。每次遇到這種情況,循環繼續解析每個元素直到遇到於開始標記匹配的結束標記“)”,endList函數用於檢測結束標記。 + +最有趣的部分是遞歸。最簡單的是對數組類型的處理。直到遇到“)”結束標記,我們使用Index函數來獲取數組每個元素的地址,然後遞歸調用read函數處理。和其它錯誤類似,如果輸入數據導致解碼器的引用超出了數組的范圍,解碼器將拋出panic異常。slice也采用類似方法解析,不同的是我們將爲每個元素創建新的變量,然後將元素添加到slice的末尾。 + +在循環處理結構體和map每個元素時必鬚解碼一個(key value)格式的對應子列表。對於結構體,key部分對於成員的名字。和數組類似,我們使用FieldByName找到結構體對應成員的變量,然後遞歸調用read函數處理。對於map,key可能是任意類型,對元素的處理方式和slice類似,我們創建一個新的變量,然後遞歸填充它,最後將新解析到的key/value對添加到map。 + +```Go +func readList(lex *lexer, v reflect.Value) { + switch v.Kind() { + case reflect.Array: // (item ...) + for i := 0; !endList(lex); i++ { + read(lex, v.Index(i)) + } + + case reflect.Slice: // (item ...) + for !endList(lex) { + item := reflect.New(v.Type().Elem()).Elem() + read(lex, item) + v.Set(reflect.Append(v, item)) + } + + case reflect.Struct: // ((name value) ...) + for !endList(lex) { + lex.consume('(') + if lex.token != scanner.Ident { + panic(fmt.Sprintf("got token %q, want field name", lex.text())) + } + name := lex.text() + lex.next() + read(lex, v.FieldByName(name)) + lex.consume(')') + } + + case reflect.Map: // ((key value) ...) + v.Set(reflect.MakeMap(v.Type())) + for !endList(lex) { + lex.consume('(') + key := reflect.New(v.Type().Key()).Elem() + read(lex, key) + value := reflect.New(v.Type().Elem()).Elem() + read(lex, value) + v.SetMapIndex(key, value) + lex.consume(')') + } + + default: + panic(fmt.Sprintf("cannot decode list into %v", v.Type())) + } +} + +func endList(lex *lexer) bool { + switch lex.token { + case scanner.EOF: + panic("end of file") + case ')': + return true + } + return false +} +``` + +最後,我們將解析器包裝爲導出的Unmarshal解碼函數,隱藏了一些初始化和清理等邊緣處理。內部解析器以panic的方式拋出錯誤,但是Unmarshal函數通過在defer語句調用recover函數來捕獲內部panic(§5.10),然後返迴一個對panic對應的錯誤信息。 + +```Go +// Unmarshal parses S-expression data and populates the variable +// whose address is in the non-nil pointer out. +func Unmarshal(data []byte, out interface{}) (err error) { + lex := &lexer{scan: scanner.Scanner{Mode: scanner.GoTokens}} + lex.scan.Init(bytes.NewReader(data)) + lex.next() // get the first token + defer func() { + // NOTE: this is not an example of ideal error handling. + if x := recover(); x != nil { + err = fmt.Errorf("error at %s: %v", lex.scan.Position, x) + } + }() + read(lex, reflect.ValueOf(out).Elem()) + return nil +} +``` + +生産實現不應該對任何輸入問題都用panic形式報告,而且應該報告一些錯誤相關的信息,例如出現錯誤輸入的行號和位置等。盡管如此,我們希望通過這個例子來展示類似encoding/json等包底層代碼的實現思路,以及如何使用反射機製來填充數據結構。 + +**練習 12.8:** sexpr.Unmarshal函數和json.Marshal一樣(譯註:這可能是筆誤,我覺得應該是指`json.Unmarshal`函數),都要求在解碼前輸入完整的字節slice。定義一個和json.Decoder類似的sexpr.Decoder類型,支持從一個io.Reader流解碼。脩改sexpr.Unmarshal函數,使用這個新的類型實現。 + +**練習 12.9:** 編寫一個基於標記的API用於解碼S表達式,參考xml.Decoder(7.14)的風格。你將需要五種類型的標記:Symbol、String、Int、StartList和EndList。 + +**練習 12.10:** 擴展sexpr.Unmarshal函數,支持布爾型、浮點數和interface類型的解碼,使用 **練習 12.3:** 的方案。(提示:要解碼接口,你需要將name映射到每個支持類型的reflect.Type。) + + diff --git a/ch12/ch12-07.md b/ch12/ch12-07.md index 35927fb..ed3503a 100644 --- a/ch12/ch12-07.md +++ b/ch12/ch12-07.md @@ -1,3 +1,140 @@ -## 12.7. 穫取結構體字段標識 +## 12.7. 獲取結構體字段標識 + +在4.5節我們使用構體成員標籤用於設置對應JSON對應的名字。其中json成員標籤讓我們可以選擇成員的名字和抑製零值成員的輸出。在本節,我們將看到如果通過反射機製類獲取成員標籤。 + +對於一個web服務,大部分HTTP處理函數要做的第一件事情就是展開請求中的參數到本地變量中。我們定義了一個工具函數,叫params.Unpack,通過使用結構體成員標籤機製來讓HTTP處理函數解析請求參數更方便。 + +首先,我們看看如何使用它。下面的search函數是一個HTTP請求處理函數。它定義了一個匿名結構體類型的變量,用結構體的每個成員表示HTTP請求的參數。其中結構體成員標籤指明了對於請求參數的名字,爲了減少UTRL的長度這些參數名通常都是神祕的縮略詞。Unpack將請求參數填充到合適的結構體成員中,這樣我們可以方便地通過合適的類型類來訪問這些參數。 + +```Go +gopl.io/ch12/search + +import "gopl.io/ch12/params" + +// search implements the /search URL endpoint. +func search(resp http.ResponseWriter, req *http.Request) { + var data struct { + Labels []string `http:"l"` + MaxResults int `http:"max"` + Exact bool `http:"x"` + } + data.MaxResults = 10 // set default + if err := params.Unpack(req, &data); err != nil { + http.Error(resp, err.Error(), http.StatusBadRequest) // 400 + return + } + + // ...rest of handler... + fmt.Fprintf(resp, "Search: %+v\n", data) +} +``` + +下面的Unpack函數主要完成三件事情。第一,它調用req.ParseForm()來解析HTTP請求。然後,req.Form將包含所有的請求參數,不管HTTP客戶端使用的是GET還是POST請求方法。 + +下一步,Unpack函數將構建每個結構體成員有效參數名字到成員變量的映射。如果結構體成員有成員標籤的話,有效參數名字可能和實際的成員名字不相同。reflect.Type的Field方法將返迴一個reflect.StructField,里面含有每個成員的名字、類型和可選的成員標籤等信息。其中成員標籤信息對應reflect.StructTag類型的字符串,併且提供了Get方法用於解析和根據特定key提取的子串,例如這里的http:"..."形式的子串。 + +```Go +gopl.io/ch12/params + +// Unpack populates the fields of the struct pointed to by ptr +// from the HTTP request parameters in req. +func Unpack(req *http.Request, ptr interface{}) error { + if err := req.ParseForm(); err != nil { + return err + } + + // Build map of fields keyed by effective name. + fields := make(map[string]reflect.Value) + v := reflect.ValueOf(ptr).Elem() // the struct variable + for i := 0; i < v.NumField(); i++ { + fieldInfo := v.Type().Field(i) // a reflect.StructField + tag := fieldInfo.Tag // a reflect.StructTag + name := tag.Get("http") + if name == "" { + name = strings.ToLower(fieldInfo.Name) + } + fields[name] = v.Field(i) + } + + // Update struct field for each parameter in the request. + for name, values := range req.Form { + f := fields[name] + if !f.IsValid() { + continue // ignore unrecognized HTTP parameters + } + for _, value := range values { + if f.Kind() == reflect.Slice { + elem := reflect.New(f.Type().Elem()).Elem() + if err := populate(elem, value); err != nil { + return fmt.Errorf("%s: %v", name, err) + } + f.Set(reflect.Append(f, elem)) + } else { + if err := populate(f, value); err != nil { + return fmt.Errorf("%s: %v", name, err) + } + } + } + } + return nil +} +``` + +最後,Unpack遍歷HTTP請求的name/valu參數鍵值對,併且根據更新相應的結構體成員。迴想一下,同一個名字的參數可能出現多次。如果發生這種情況,併且對應的結構體成員是一個slice,那麽就將所有的參數添加到slice中。其它情況,對應的成員值將被覆蓋,隻有最後一次出現的參數值才是起作用的。 + +populate函數小心用請求的字符串類型參數值來填充單一的成員v(或者是slice類型成員中的單一的元素)。目前,它僅支持字符串、有符號整數和布爾型。其中其它的類型將留做練習任務。 + +```Go +func populate(v reflect.Value, value string) error { + switch v.Kind() { + case reflect.String: + v.SetString(value) + + case reflect.Int: + i, err := strconv.ParseInt(value, 10, 64) + if err != nil { + return err + } + v.SetInt(i) + + case reflect.Bool: + b, err := strconv.ParseBool(value) + if err != nil { + return err + } + v.SetBool(b) + + default: + return fmt.Errorf("unsupported kind %s", v.Type()) + } + return nil +} +``` + +如果我們上上面的處理程序添加到一個web服務器,則可以産生以下的會話: + +``` +$ go build gopl.io/ch12/search +$ ./search & +$ ./fetch 'http://localhost:12345/search' +Search: {Labels:[] MaxResults:10 Exact:false} +$ ./fetch 'http://localhost:12345/search?l=golang&l=programming' +Search: {Labels:[golang programming] MaxResults:10 Exact:false} +$ ./fetch 'http://localhost:12345/search?l=golang&l=programming&max=100' +Search: {Labels:[golang programming] MaxResults:100 Exact:false} +$ ./fetch 'http://localhost:12345/search?x=true&l=golang&l=programming' +Search: {Labels:[golang programming] MaxResults:10 Exact:true} +$ ./fetch 'http://localhost:12345/search?q=hello&x=123' +x: strconv.ParseBool: parsing "123": invalid syntax +$ ./fetch 'http://localhost:12345/search?q=hello&max=lots' +max: strconv.ParseInt: parsing "lots": invalid syntax +``` + +**練習 12.11:** 編寫相應的Pack函數,給定一個結構體值,Pack函數將返迴合併了所有結構體成員和值的URL。 + +**練習 12.12:** 擴展成員標籤以表示一個請求參數的有效值規則。例如,一個字符串可以是有效的email地址或一個信用卡號碼,還有一個整數可能需要是有效的郵政編碼。脩改Unpack函數以檢査這些規則。 + +**練習 12.13:** 脩改S表達式的編碼器(§12.4)和解碼器(§12.6),采用和encoding/json包(§4.5)類似的方式使用成員標籤中的sexpr:"..."字串。 + + -TODO diff --git a/ch12/ch12-08.md b/ch12/ch12-08.md index bd5b63f..afb56a9 100644 --- a/ch12/ch12-08.md +++ b/ch12/ch12-08.md @@ -1,3 +1,44 @@ ## 12.8. 顯示一個類型的方法集 -TODO +我們的最後一個例子是使用reflect.Type來打印任意值的類型和枚舉它的方法: + +```Go +gopl.io/ch12/methods + +// Print prints the method set of the value x. +func Print(x interface{}) { + v := reflect.ValueOf(x) + t := v.Type() + fmt.Printf("type %s\n", t) + + for i := 0; i < v.NumMethod(); i++ { + methType := v.Method(i).Type() + fmt.Printf("func (%s) %s%s\n", t, t.Method(i).Name, + strings.TrimPrefix(methType.String(), "func")) + } +} +``` + +reflect.Type和reflect.Value都提供了一個Method方法。每次t.Method(i)調用將一個reflect.Method的實例,對應一個用於描述一個方法的名稱和類型的結構體。每次v.Method(i)方法調用都返迴一個reflect.Value以表示對應的值(§6.4),也就是一個方法是幫到它的接收者的。使用reflect.Value.Call方法(我們之類沒有演示),將可以調用一個Func類型的Value,但是這個例子中隻用到了它的類型。 + +這是屬於time.Duration和`*strings.Replacer`兩個類型的方法: + +```Go +methods.Print(time.Hour) +// Output: +// type time.Duration +// func (time.Duration) Hours() float64 +// func (time.Duration) Minutes() float64 +// func (time.Duration) Nanoseconds() int64 +// func (time.Duration) Seconds() float64 +// func (time.Duration) String() string + +methods.Print(new(strings.Replacer)) +// Output: +// type *strings.Replacer +// func (*strings.Replacer) Replace(string) string +// func (*strings.Replacer) WriteString(io.Writer, string) (int, error) +```` + + + diff --git a/ch12/ch12-09.md b/ch12/ch12-09.md index 70d2f6c..dd081d2 100644 --- a/ch12/ch12-09.md +++ b/ch12/ch12-09.md @@ -1,3 +1,20 @@ ## 12.9. 幾點忠告 -TODO +雖然反射提供的API遠多於我們講到的,我們前面的例子主要是給出了一個方向,通過反射可以實現哪些功能。反射是一個強大併富有表達力的工具,但是它應該被小心地使用,原因有三。 + +第一個原因是,基於反射的代碼是比較脆弱的。對於每一個會導致編譯器報告類型錯誤的問題,在反射中都有與之相對應的問題,不同的是編譯器會在構建時馬上報告錯誤,而反射則是在眞正運行到的時候才會拋出panic異常,可能是寫完代碼很久之後的時候了,而且程序也可能運行了很長的時間。 + +以前面的readList函數(§12.6)爲例,爲了從輸入讀取字符串併填充int類型的變量而調用的reflect.Value.SetString方法可能導致panic異常。絶大多數使用反射的程序都有類似的風險,需要非常小心地檢査每個reflect.Value的對於值的類型、是否可取地址,還有是否可以被脩改等。 + +避免這種因反射而導致的脆弱性的問題的最好方法是將所有的反射相關的使用控製在包的內部,如果可能的話避免在包的API中直接暴露reflect.Value類型,這樣可以限製一些非法輸入。如果無法做到這一點,在每個有風險的操作前指向額外的類型檢査。以標準庫中的代碼爲例,當fmt.Printf收到一個非法的操作數是,它併不會拋出panic異常,而是打印相關的錯誤信息。程序雖然還有BUG,但是會更加容易診斷。 + +```Go +fmt.Printf("%d %s\n", "hello", 42) // "%!d(string=hello) %!s(int=42)" +``` + +反射同樣降低了程序的安全性,還影響了自動化重構和分析工具的準確性,因爲它們無法識别運行時才能確認的類型信息。 + +避免使用反射的第二個原因是,卽使對應類型提供了相同文檔,但是反射的操作不能做靜態類型檢査,而且大量反射的代碼通常難以理解。總是需要小心翼翼地爲每個導出的類型和其它接受interface{}或reflect.Value類型參數的函數維護説明文檔。 + +第三個原因,基於反射的代碼通常比正常的代碼運行速度慢一到兩個數量級。對於一個典型的項目,大部分函數的性能和程序的整體性能關繫不大,所以使用反射可能會使程序更加清晰。測試是一個特别適合使用反射的場景,因爲每個測試的數據集都很小。但是對於性能關鍵路徑的函數,最好避免使用反射。 + diff --git a/ch13/ch13-01.md b/ch13/ch13-01.md index c1b1cba..a616b05 100644 --- a/ch13/ch13-01.md +++ b/ch13/ch13-01.md @@ -1,45 +1,42 @@ ## 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 等較大的數據類型. +計算機在加載和保存數據時,如果內存地址合理地對齊的將會更有效率。例如2字節大小的int16類型的變量地址應該是偶數,一個4字節大小的rune類型變量的地址應該是4的倍數,一個8字節大小的float64、uint64或64-bit指針類型變量的地址應該是8字節對齊的。但是對於再大的地址對齊倍數則是不需要的,卽使是complex128等較大的數據類型最多也隻是8字節對齊。 -由於這個因素,一個聚合類型(結構體或數組)的大小至少是所有字段或元素大小的總和, 或者更大因爲可能存在空洞. 空洞是編譯器自動添加的沒有被使用的空間, 用於保證後面每個字段或元素的地址相對於結構或數組的開始地址能夠合理地對齊. +由於地址對齊這個因素,一個聚合類型(結構體或數組)的大小至少是所有字段或元素大小的總和,或者更大因爲可能存在內存空洞。內存空洞是編譯器自動添加的沒有被使用的內存空間,用於保證後面每個字段或元素的地址相對於結構或數組的開始地址能夠合理地對齊(譯註:內存空洞可能會存在一些隨機數據,可能會對用unsafe包直接操作內存的處理産生影響)。 類型 | 大小 ----------------------------- | ---- -bool | 1字節 -intN, uintN, floatN, complexN | N/8字節 (例如 float64 是 8字節) +bool | 1個字節 +intN, uintN, floatN, complexN | N/8個字節(例如float64是8個字節) int, uint, uintptr | 1個機器字 *T | 1個機器字 string | 2個機器字(data,len) -[]T | 3個機器字(data,len, cap) +[]T | 3個機器字(data,len,cap) map | 1個機器字 func | 1個機器字 chan | 1個機器字 interface | 2個機器字(type,value) -Go的語言規范併沒有保證一個字段的聲明順序和內存中的順序是一致的, 所以理論上一個編譯器可以隨意地重新排列每個字段的內存布局, 隨着在寫作本書的時候編譯器還沒有這麽做. 下面的三個結構體有着相同的字段, 但是第一個比另外的兩個需要多 50% 的內存. - +Go語言的規范併沒有要求一個字段的聲明順序和內存中的順序是一致的,所以理論上一個編譯器可以隨意地重新排列每個字段的內存位置,隨然在寫作本書的時候編譯器還沒有這麽做。下面的三個結構體雖然有着相同的字段,但是第一種寫法比另外的兩個需要多50%的內存。 ```Go - // 64-bit 32-bit + // 64-bit 32-bit struct{ bool; float64; int16 } // 3 words 4words struct{ float64; int16; bool } // 2 words 3words struct{ bool; int16; float64 } // 2 words 3words ``` -雖然關於對齊算法的細節超齣了本書的范圍, 也不是每一個結構體都需要擔心這個問題, 不過有效的包裝可以使數據結構更加緊湊, 內存使用率和性能都可能受益. +關於內存地址對齊算法的細節超出了本書的范圍,也不是每一個結構體都需要擔心這個問題,不過有效的包裝可以使數據結構更加緊湊(譯註:未來的Go語言編譯器應該會默認優化結構體的順序,當然用於應該也能夠指定具體的內存布局,相同討論請參考 [Issue10014](https://github.com/golang/go/issues/10014) ),內存使用率和性能都可能會受益。 `unsafe.Alignof` 函數返迴對應參數的類型需要對齊的倍數. 和 Sizeof 類似, Alignof 也是返迴一個常量表達式, 對應一個常量. 通常情況下布爾和數字類型需要對齊到它們本身的大小(最多8個字節), 其它的類型對齊到機器字大小. @@ -55,14 +52,11 @@ 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和它的三個字段調用unsafe包相關函數的計算結果: ![](../images/ch13-01.png) - -32位繫統: +32位繫統: ``` Sizeof(x) = 16 Alignof(x) = 4 @@ -71,7 +65,7 @@ Sizeof(x.b) = 2 Alignof(x.b) = 2 Offsetof(x.b) = 2 Sizeof(x.c) = 12 Alignof(x.c) = 4 Offsetof(x.c) = 4 ``` -64位繫統: +64位繫統: ``` Sizeof(x) = 32 Alignof(x) = 8 @@ -80,6 +74,5 @@ 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包,但是這幾個函數調用併不是眞的不安全,特别在需要優化內存空間時它們返迴的結果對於理解原生的內存布局很有幫助。 diff --git a/ch13/ch13-02.md b/ch13/ch13-02.md index 194ae67..5fb973d 100644 --- a/ch13/ch13-02.md +++ b/ch13/ch13-02.md @@ -1,8 +1,8 @@ ## 13.2. unsafe.Pointer -大多數指針類型寫成 *T, 含義是 "一個指向T類型變量的指針". `unsafe.Pointer` 是特别定義的一種指針類型, 它可以包含任意類型變量的地址. 當然, 我們不可以直接使用 *p 穫取 `unsafe.Pointer` 指針指向的眞實變量, 因爲我們併不知道變量的類型. 和普通指針一樣, `unsafe.Pointer` 指針是可以比較的, 支持和 nil 比較判斷是否爲空指針. +大多數指針類型會寫成`*T`,表示是“一個指向T類型變量的指針”。unsafe.Pointer是特别定義的一種指針類型(譯註:類似C語言中的`void*`類型的指針),它可以包含任意類型變量的地址。當然,我們不可以直接通過`*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,11 @@ func Float64bits(f float64) uint64 { return *(*uint64)(unsafe.Pointer(&f)) } fmt.Printf("%#016x\n", Float64bits(1.0)) // "0x3ff0000000000000" ``` -通過新指針, 我們可以更新浮點數的位模式. 通過位模式操作浮點數是可以的, 但是更重要的意義是指針轉換讓我們可以在不破壞類型繫統的前提下向內存寫入任意的值. +通過轉爲新類型指針,我們可以更新浮點數的位模式。通過位模式操作浮點數是可以的,但是更重要的意義是指針轉換語法讓我們可以在不破壞類型繫統的前提下向內存寫入任意的值。 -一個 `unsafe.Pointer` 指針也可以被轉化爲 uintptr 類似, 然後保存到指針型數值變量中, 用以做必要的指針運算. -(第三章內容, uintptr是一個無符號的整型數, 足有保存一個地址.) -這種轉換也是可逆的, 但是, 將 uintptr 轉爲 `unsafe.Pointer` 指針可能破壞類型繫統, 因爲併不是所有的數字都是有效的內存地址. +一個unsafe.Pointer指針也可以被轉化爲uintptr類型,然後保存到指針型數值變量中(譯註:這隻是和當前指針相同的一個數字值,併不是一個指針),然後用以做必要的指針數值運算。(第三章內容,uintptr是一個無符號的整型數,足以保存一個地址)這種轉換雖然也是可逆的,但是將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,24 +34,30 @@ pb := (*int16)(unsafe.Pointer( fmt.Println(x.b) // "42" ``` -盡管寫法很繁瑣, 但在這里併不是一件壞事, 因爲這些功能應該很謹慎地使用. 不要試圖將引入可能而破壞代碼的正確性的 uintptr 臨時變量. 下面段代碼是不正確的: +上面的寫法盡管很繁瑣,但在這里併不是一件壞事,因爲這些功能應該很謹慎地使用。不要試圖引入一個uintptr類型的臨時變量,因爲它可能會破壞代碼的安全性(譯註:這是眞正可以體會unsafe包爲何不安全的例子)。下面段代碼是錯誤的: -錯誤的原因很微妙. 有時候垃圾迴收器會移動一些變量以降低內存碎片的問題.這類垃圾迴收器被稱爲移動GC. 當一個變量被移動, 所有的保存改變量舊地址的指針必鬚同時被更新爲變量移動後的新地址. 從垃圾收集器的視角來看, 一個 `unsafe.Pointer` 是一個指針, 因此當變量被移動是對應的指針必鬚被更新, 但是 `uintptr` 隻是一個普通的數字, 所以其值不應該被改變. 上面錯誤的代碼因爲一個非指針的臨時變量 `tmp`, 導致垃圾收集器無法正確識别這個是一個指向變量 `x` 的指針. 第二個語句執行時, 變量 `x` 可能已經被轉移, 臨時變量 `tmp` 也就不在對應現在的 `&x.b`. 第三個賦值語句將徹底摧譭那個之前的那部分內存空間. +```Go +// NOTE: subtly incorrect! +tmp := uintptr(unsafe.Pointer(&x)) + unsafe.Offsetof(x.b) +pb := (*int16)(unsafe.Pointer(tmp)) +*pb = 42 +``` -有很多類似原因導致的錯誤. 例如這條語句: +産生錯誤的原因很微妙。有時候垃圾迴收器會移動一些變量以降低內存碎片等問題。這類垃圾迴收器被稱爲移動GC。當一個變量被移動,所有的保存改變量舊地址的指針必鬚同時被更新爲變量移動後的新地址。從垃圾收集器的視角來看,一個unsafe.Pointer是一個指向變量的指針,因此當變量被移動是對應的指針也必鬚被更新;但是uintptr類型的臨時變量隻是一個普通的數字,所以其值不應該被改變。上面錯誤的代碼因爲引入一個非指針的臨時變量tmp,導致垃圾收集器無法正確識别這個是一個指向變量x的指針。當第二個語句執行時,變量x可能已經被轉移,這時候臨時變量tmp也就不再是現在的`&x.b`地址。第三個向之前無效地址空間的賦值語句將徹底摧譭整個程序! + +還有很多類似原因導致的錯誤。例如這條語句: ```Go pT := uintptr(unsafe.Pointer(new(T))) // 提示: 錯誤! ``` -這里併沒有指針引用 `new` 新創建的變量, 因此語句執行完成之後, 垃圾收集器有權迴收其內存空間, 所以返迴的 `pT` 保存將是無效的地址. +這里併沒有指針引用`new`新創建的變量,因此該語句執行完成之後,垃圾收集器有權馬上迴收其內存空間,所以返迴的pT將是無效的地址。 -目前的Go語言實現還沒有使用移動GC(未來可能實現), 但這不該是僥幸的理由: 當前的Go實現已經有移動變量的場景. 在5.2節我們提到goroutine的棧是根據需要動態增長的. 當這個時候, 原來棧中的所以變量可能需要被移動到新的更大的棧中, 所以我們無法確保變量的地址在整個使用週期內保持不變. +雖然目前的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的轉換是不安全的(參考 [Issue7192](https://github.com/golang/go/issues/7192) ). 譯註: 該問題已經關閉),因此我們強烈建議按照最壞的方式處理。將所有包含變量地址的uintptr類型變量當作BUG處理,同時減少不必要的unsafe.Pointer類型到uintptr類型的轉換。在第一個例子中,有三個轉換——字段偏移量到uintptr的轉換和轉迴unsafe.Pointer類型的操作——所有的轉換全在一個表達式完成。 -當調用一個庫函數, 併且返迴的是 `uintptr` 類型是, 比如下面反射包中的相關函數, -返迴的結果應該立卽轉換爲 `unsafe.Pointer` 以確保指針指向的是相同的變量. +當調用一個庫函數,併且返迴的是uintptr類型地址時(譯註:普通方法實現的函數不盡量不要返迴該類型。下面例子是reflect包的函數,reflect包和unsafe包一樣都是采用特殊技術實現的,編譯器可能給它們開了後門),比如下面反射包中的相關函數,返迴的結果應該立卽轉換爲unsafe.Pointer以確保指針指向的是相同的變量。 ```Go package reflect diff --git a/ch13/ch13-03.md b/ch13/ch13-03.md index 8677abc..89a1b2b 100644 --- a/ch13/ch13-03.md +++ b/ch13/ch13-03.md @@ -1,6 +1,6 @@ ## 13.3. 示例: 深度相等判斷 -來自 reflect 包的 DeepEqual 對兩個值進行深度相等判斷. DeepEqual 使用內建的 `==` 操作符對基礎類型進行相等判斷, 對於複合類型則遞歸變量每個基礎類型然後做類似的比較判斷. 因爲它工作在任意的類型上, 甚至對一些不支持 `==` 操作符的類型也可以工作, 因此在一些測試代碼中被廣泛地使用. 比如下面的代碼是用 DeepEqual 比較兩個字符串數組是否等價. +來自reflect包的DeepEqual函數可以對兩個值進行深度相等判斷。DeepEqual函數使用內建的==比較操作符對基礎類型進行相等判斷,對於複合類型則遞歸該變量的每個基礎類型然後做類似的比較判斷。因爲它可以工作在任意的類型上,甚至對於一些不支持==操作運算符的類型也可以工作,因此在一些測試代碼中廣泛地使用該函數。比如下面的代碼是用DeepEqual函數比較兩個字符串數組是否相等。 ```Go func TestSplit(t *testing.T) { @@ -10,9 +10,7 @@ func TestSplit(t *testing.T) { } ``` -盡管 DeepEqual 很方便, 而且可以支持任意的類型, 但是也有不足之處. -例如, 它將一個 nil map 和 非 nil 的空的 map 視作不相等, -同樣 nil slice 和 非 nil 的空的 slice 也不相等. +盡管DeepEqual函數很方便,而且可以支持任意的數據類型,但是它也有不足之處。例如,它將一個nil值的map和非nil值但是空的map視作不相等,同樣nil值的slice 和非nil但是空的slice也視作不相等。 ```Go var a, b []string = nil, []string{} @@ -22,7 +20,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視作相等的值。基礎部分的比較可以基於reflect包完成,和12.3章的Display函數的實現方法類似。同樣,我們也定義了一個內部函數equal,用於內部的遞歸比較。讀者目前不用關心seen參數的具體含義。對於每一對需要比較的x和y,equal函數首先檢測它們是否都有效(或都無效),然後檢測它們是否是相同的類型。剩下的部分是一個鉅大的switch分支,用於相同基礎類型的元素比較。因爲頁面空間的限製,我們省略了一些相似的分支。 ```Go gopl.io/ch13/equal @@ -65,8 +63,7 @@ func equal(x, y reflect.Value, seen map[comparison]bool) bool { } ``` -和前面的建議一樣, 我們不公開使用反射相關的接口, -所以導齣的函數需要在內部自己將變量轉爲 reflect.Value 類型. +和前面的建議一樣,我們併不公開reflect包相關的接口,所以導出的函數需要在內部自己將變量轉爲reflect.Value類型。 ```Go // Equal reports whether x and y are deeply equal. @@ -81,7 +78,7 @@ type comparison struct { } ``` -爲了確保算法對於循環數據結構也能正常退齣, 我們必鬚記録每次已經比較的變量, 從而避免進入第二次的比較. Equal 函數分配了一組用於比較的結構體, 包含每對比較對象的地址(unsafe.Pointer形式保存)和類型. 我們記録類型的原因是, 有些不同的變量可能對應相同的地址. 例如, 如果 x 和 y 都是數組類型, 那麽 x 和 `x[0]` 將對應相同的地址, y 和 `y[0]` 也是對應相同的地址, 這可以用於判斷 對x 和 y 比較 或 x[0] 和 y[0] 的是否進行過了. +爲了確保算法對於有環的數據結構也能正常退出,我們必鬚記録每次已經比較的變量,從而避免進入第二次的比較。Equal函數分配了一組用於比較的結構體,包含每對比較對象的地址(unsafe.Pointer形式保存)和類型。我們要記録類型的原因是,有些不同的變量可能對應相同的地址。例如,如果x和y都是數組類型,那麽x和x[0]將對應相同的地址,y和y[0]也是對應相同的地址,這可以用於區分x與y之間的比較或x[0]與y[0]之間的比較是否進行過了。 ```Go // cycle check @@ -99,7 +96,7 @@ if x.CanAddr() && y.CanAddr() { } ``` -這是 Equal 函數的使用的例子: +這是Equal函數用法的例子: ```Go fmt.Println(Equal([]int{1, 2, 3}, []int{1, 2, 3})) // "true" @@ -108,7 +105,7 @@ fmt.Println(Equal([]string(nil), []string{})) // "true" fmt.Println(Equal(map[string]int(nil), map[string]int{})) // "true" ``` -它甚至可以處理類似12.3章中導致Display陷入死循環的數據. +Equal函數甚至可以處理類似12.3章中導致Display陷入陷入死循環的帶有環的數據。 ```Go // Circular linked lists a -> b -> a and c -> c. @@ -125,8 +122,8 @@ fmt.Println(Equal(a, b)) // "false" fmt.Println(Equal(a, c)) // "false" ``` -``` -練習 13.1: 定義一個深比較函數, 對於十億以內的數字比較, 忽略類型差異. -練習 13.2: 編寫一個函數, 報告其參數是否循環數據結構. -``` +**練習 13.1:** 定義一個深比較函數,對於十億以內的數字比較,忽略類型差異。 + +**練習 13.2:** 編寫一個函數,報告其參數是否循環數據結構。 + diff --git a/ch13/ch13-04.md b/ch13/ch13-04.md index 4ee1121..8c8d644 100644 --- a/ch13/ch13-04.md +++ b/ch13/ch13-04.md @@ -1,10 +1,10 @@ ## 13.4. 通過cgo調用C代碼 -Go程序可能會遇到要訪問C語言的某些硬件驅動的場景, 或者是從一個C++實現的嵌入式數據庫査詢記録的場景, 或者是使用Fortran實現的一些線性代數庫的場景. C作爲一個通用語言, 很多庫會選擇提供一個C兼容的API, 然後用其他語言實現. +Go程序可能會遇到要訪問C語言的某些硬件驅動函數的場景,或者是從一個C++語言實現的嵌入式數據庫査詢記録的場景,或者是使用Fortran語言實現的一些線性代數庫的場景。C語言作爲一個通用語言,很多庫會選擇提供一個C兼容的API,然後用其他不同的編程語言實現(譯者:Go語言需要也應該擁抱這些鉅大的代碼遺産)。 -在本節中, 我們將構建一個簡易的數據壓縮程序, 通過使用一個Go語言自帶的叫cgo的用於支援C語言函數調用的工具. 這類工具被稱爲外圍函數接口(ffi), 併且cgo也不是Go中唯一的類似工具. SWIG(swig.org) 是類似的另一個被廣泛使用的工具, 它提供了很多複雜特性以支援C++的集成, 但 SWIG 不是這里要討論的主題. +在本節中,我們將構建一個簡易的數據壓縮程序,使用了一個Go語言自帶的叫cgo的用於支援C語言函數調用的工具。這類工具一般被稱爲 *foreign-function interfaces* (簡稱ffi), 併且在類似工具中cgo也不是唯一的。SWIG( http://swig.org )是另一個類似的且被廣泛使用的工具,SWIG提供了很多複雜特性以支援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 @@ -12,15 +12,34 @@ func NewWriter(w io.Writer) io.WriteCloser func NewReader(r io.Reader) (io.ReadCloser, error) ``` -bzip2壓縮算法, 是基於優雅的 Burrows-Wheeler 變換, 運行速度比 gzip 要慢, 但是可以提供更高的壓縮比. 標準庫的 `compress/bzip2` 包目前還沒有提供 bzip2 算法的壓縮實現. 完全從頭實現是一個繁瑣的工作, 而且 bzip.org 有現成的 libbzip2 開源實現, 文檔齊全而且性能較好, +bzip2壓縮算法,是基於優雅的Burrows-Wheeler變換算法,運行速度比gzip要慢,但是可以提供更高的壓縮比。標準庫的compress/bzip2包目前還沒有提供bzip2壓縮算法的實現。完全從頭開始實現是一個壓縮算法是一件繁瑣的工作,而且 http://bzip.org 已經有現成的libbzip2的開源實現,不僅文檔齊全而且性能又好。 -如果C庫比較小, 我們可以用純Go重新實現一遍. 如果我們對性能沒有特殊要求, 我們可以用 `os/exec` 包的方法將C編寫的應用程序作爲一個子進行運行. 隻有當你需要使用複雜但是性能更高的底層C接口時, 就是使用cgo的場景了. 下面我們將通過一個例子講述cgo的用法. +如果是比較小的C語言庫,我們完全可以用純Go語言重新實現一遍。如果我們對性能也沒有特殊要求的話,我們還可以用os/exec包的方法將C編寫的應用程序作爲一個子進程運行。隻有當你需要使用複雜而且性能更高的底層C接口時,就是使用cgo的場景了(譯註:用os/exec包調用子進程的方法會導致程序運行時依賴那個應用程序)。下面我們將通過一個例子講述cgo的具體用法。 -要使用 libbzip2, 我們需要一個 `bz_stream` 結構體, 用於保持輸入和輸齣緩存. -然後有三個函數: BZ2_bzCompressInit 用於初始化緩存, BZ2_bzCompress 用於將輸入緩存的數據壓縮到輸齣緩存, BZ2_bzCompressEnd 用於釋放不需要的緩存. -(目前不要擔心包的具體結構, 這個例子的目的就是演示各個部分如何組合在一起的) +譯註:本章采用的代碼都是最新的。因爲之前已經出版的書中包含的代碼隻能在Go1.5之前使用。從Go1.6開始,Go語言已經明確規定了哪些Go語言指針可以之間傳入C語言函數。新代碼重點是增加了bz2alloc和bz2free的兩個函數,用於bz_stream對象空間的申請和釋放操作。下面是新代碼中增加的註釋,説明這個問題: -我們可以在Go代碼中直接調用 BZ2_bzCompressInit 和 BZ2_bzCompressEnd, 但是對於 BZ2_bzCompress, 我們將定義一個C語言的包裝函數, 爲了顯示他是如何完成的. 下面是C代碼, 對應一個獨立的文件. +```Go +// The version of this program that appeared in the first and second +// printings did not comply with the proposed rules for passing +// pointers between Go and C, described here: +// https://github.com/golang/proposal/blob/master/design/12416-cgo-pointers.md +// +// The rules forbid a C function like bz2compress from storing 'in' +// and 'out' (pointers to variables allocated by Go) into the Go +// variable 's', even temporarily. +// +// The version below, which appears in the third printing, has been +// corrected. To comply with the rules, the bz_stream variable must +// be allocated by C code. We have introduced two C functions, +// bz2alloc and bz2free, to allocate and free instances of the +// bz_stream type. Also, we have changed bz2compress so that before +// it returns, it clears the fields of the bz_stream that contain +// pointers to Go variables. +``` + +要使用libbzip2,我們需要先構建一個bz_stream結構體,用於保持輸入和輸出緩存。然後有三個函數:BZ2_bzCompressInit用於初始化緩存,BZ2_bzCompress用於將輸入緩存的數據壓縮到輸出緩存,BZ2_bzCompressEnd用於釋放不需要的緩存。(目前不要擔心包的具體結構, 這個例子的目的就是演示各個部分如何組合在一起的。) + +我們可以在Go代碼中直接調用BZ2_bzCompressInit和BZ2_bzCompressEnd,但是對於BZ2_bzCompress,我們將定義一個C語言的包裝函數,用它完成眞正的工作。下面是C代碼,對應一個獨立的文件。 ```C gopl.io/ch13/bzip @@ -38,12 +57,12 @@ int bz2compress(bz_stream *s, int action, int r = BZ2_bzCompress(s, action); *inlen -= s->avail_in; *outlen -= s->avail_out; + s->next_in = s->next_out = NULL; return r; } ``` -現在讓我們轉到Go部分, 第一部分如下所示. 其中 `import "C"` 的語句是比較特别的. 其實併沒有一個叫 `C` 的包, 但是這行語句會讓Go構建在編譯之前先運行cgo工具. - +現在讓我們轉到Go語言部分,第一部分如下所示。其中`import "C"`的語句是比較特别的。其實併沒有一個叫C的包,但是這行語句會讓Go編譯程序在編譯之前先運行cgo工具。 ```Go // Package bzip provides a writer that uses bzip2 compression (bzip.org). @@ -53,8 +72,11 @@ package bzip #cgo CFLAGS: -I/usr/include #cgo LDFLAGS: -L/usr/lib -lbz2 #include +#include +bz_stream* bz2alloc() { return calloc(1, sizeof(bz_stream)); } int bz2compress(bz_stream *s, int action, char *in, unsigned *inlen, char *out, unsigned *outlen); +void bz2free(bz_stream* s) { free(s); } */ import "C" @@ -71,20 +93,48 @@ type writer struct { // NewWriter returns a writer for bzip2-compressed streams. func NewWriter(out io.Writer) io.WriteCloser { - const ( - blockSize = 9 - verbosity = 0 - workFactor = 30 - ) - w := &writer{w: out, stream: new(C.bz_stream)} + const blockSize = 9 + const verbosity = 0 + const workFactor = 30 + w := &writer{w: out, stream: C.bz2alloc()} C.BZ2_bzCompressInit(w.stream, blockSize, verbosity, workFactor) return w } ``` -在循環的每次迭代中, 向bz2compress傳入數據的地址和剩餘部分的長度, 還有輸齣緩存 w.outbuf 的地址和容量. 這兩個長度信息通過它們的地址傳入而不是值傳入, 因爲bz2compress函數可能會根據已經壓縮的數據和壓縮後數據的大小來更新這兩個值(譯註: 這里的用法有問題, 勘誤已經提到. 具體脩複的方法稍後再補充). 每個塊壓縮後的數據被寫入到底層的 io.Writer. +在預處理過程中,cgo工具爲生成一個臨時包用於包含所有在Go語言中訪問的C語言的函數或類型。例如C.bz_stream和C.BZ2_bzCompressInit。cgo工具通過以某種特殊的方式調用本地的C編譯器來發現在Go源文件導入聲明前的註釋中包含的C頭文件中的內容(譯註:`import "C"語句前僅捱着的註釋是對應cgo的特殊語法,對應必要的構建參數選項和C語言代碼`)。 -Close 方法和 Write 方法有着類似的結構, 通過一個循環將剩餘的壓縮數據刷新到輸齣緩存. +在cgo註釋中還可以包含#cgo指令,用於給C語言工具鏈指定特殊的參數。例如CFLAGS和LDFLAGS分别對應傳給C語言編譯器的編譯參數和鏈接器參數,使它們可以特定目録找到bzlib.h頭文件和libbz2.a庫文件。這個例子假設你已經在/usr目録成功安裝了bzip2庫。如果bzip2庫是安裝在不同的位置,你需要更新這些參數。 + +NewWriter函數通過調用C語言的BZ2_bzCompressInit函數來初始化stream中的緩存。在writer結構中還包括了另一個buffer,用於輸出緩存。 + +下面是Write方法的實現,返迴成功壓縮數據的大小,主體是一個循環中調用C語言的bz2compress函數實現的。從代碼可以看到,Go程序可以訪問C語言的bz_stream、char和uint類型,還可以訪問bz2compress等函數,甚至可以訪問C語言中像BZ_RUN那樣的宏定義,全部都是以C.x語法訪問。其中C.uint類型和Go語言的uint類型併不相同,卽使它們具有相同的大小也是不同的類型。 + +```Go +func (w *writer) Write(data []byte) (int, error) { + if w.stream == nil { + panic("closed") + } + var total int // uncompressed bytes written + + for len(data) > 0 { + inlen, outlen := C.uint(len(data)), C.uint(cap(w.outbuf)) + C.bz2compress(w.stream, C.BZ_RUN, + (*C.char)(unsafe.Pointer(&data[0])), &inlen, + (*C.char)(unsafe.Pointer(&w.outbuf)), &outlen) + total += int(inlen) + data = data[inlen:] + if _, err := w.w.Write(w.outbuf[:outlen]); err != nil { + return total, err + } + } + return total, nil +} +``` + +在循環的每次迭代中,向bz2compress傳入數據的地址和剩餘部分的長度,還有輸出緩存w.outbuf的地址和容量。這兩個長度信息通過它們的地址傳入而不是值傳入,因爲bz2compress函數可能會根據已經壓縮的數據和壓縮後數據的大小來更新這兩個值。每個塊壓縮後的數據被寫入到底層的io.Writer。 + +Close方法和Write方法有着類似的結構,通過一個循環將剩餘的壓縮數據刷新到輸出緩存。 ```Go // Close flushes the compressed data and closes the stream. @@ -95,6 +145,7 @@ func (w *writer) Close() error { } defer func() { C.BZ2_bzCompressEnd(w.stream) + C.bz2free(w.stream) w.stream = nil }() for { @@ -111,11 +162,11 @@ func (w *writer) Close() error { } ``` -壓縮完成後, Close 用了 defer 確保函數退齣前調用 C.BZ2_bzCompressEnd 釋放輸入和輸齣流的緩存. 此刻 `w.stream` 指針將不在有效, 我們將它設置爲 nil 以保證安全, 然後在每個方法中增加 nil 檢測, 以防止用戶在關閉後依然錯誤使用相關方法. +壓縮完成後,Close方法用了defer函數確保函數退出前調用C.BZ2_bzCompressEnd和C.bz2free釋放相關的C語言運行時資源。此刻w.stream指針將不再有效,我們將它設置爲nil以保證安全,然後在每個方法中增加了nil檢測,以防止用戶在關閉後依然錯誤使用相關方法。 -不僅僅寫是非併發安全的, 甚至併發調用 Close 和 Write 也可能導致C代碼的崩潰. 脩複這個問題是 練習13.3 的內容. +上面的實現中,不僅僅寫是非併發安全的,甚至併發調用Close和Write方法也可能導致程序的的崩潰。脩複這個問題是練習13.3的內容。 -下面的bzipper程序是使用我們自己包實現的bzip2壓縮命令. 它的行爲和許多Unix繫統的 bzip2 命令類似. +下面的bzipper程序,使用我們自己包實現的bzip2壓縮命令。它的行爲和許多Unix繫統的bzip2命令類似。 ```Go gopl.io/ch13/bzipper @@ -141,7 +192,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 +206,9 @@ $ ./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程序中動態加載也都是可行的(譯註:在Go1.5中,Windows繫統的Go語言實現併不支持生成C語言動態庫或靜態庫的特性。不過好消息是,目前已經有人在嚐試解決這個問題,具體請訪問 [Issue11058](https://github.com/golang/go/issues/11058) )。這里我們隻展示的cgo很小的一些方面,更多的關於內存管理、指針、迴調函數、中斷信號處理、字符串、errno處理、終結器,以及goroutines和繫統線程的關繫等,有很多細節可以討論。特别是如何將Go語言的指針傳入C函數的規則也是異常複雜的(譯註:簡單來説,要傳入C函數的Go指針指向的數據本身不能包含指針或其他引用類型;併且C函數在返迴後不能繼續持有Go指針;併且在C函數返迴之前,Go指針是被鎖定的,不能導致對應指針數據被移動或棧的調整),部分的原因在13.2節有討論到,但是在Go1.5中還沒有被明確(譯註:Go1.6將會明確cgo中的指針使用規則)。如果要進一步閲讀,可以從 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的替代實現(譯註:雖然是純Go實現,但是運行時將依賴/bin/bzip2命令,其他操作繫統可能無法運行)。 -**練習13.4:** 因爲C庫依賴的限製. 使用 `os/exec` 包啟動 `/bin/bzip2` 命令作爲一個子進程, 提供一個純Go的 bzip.NewWriter 的替代實現. diff --git a/ch13/ch13-05.md b/ch13/ch13-05.md index 695b4f3..92f9346 100644 --- a/ch13/ch13-05.md +++ b/ch13/ch13-05.md @@ -1,11 +1,12 @@ ## 13.5. 幾點忠告 -我們在前一章結尾的時候, 我們警告要謹慎使用反射. 那些警告同樣適用於本章的 unsafe 包. +我們在前一章結尾的時候,我們警告要謹慎使用reflect包。那些警告同樣適用於本章的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包, 除非你確實需要它們. +現在,趕緊將最後兩章拋入腦後吧。編寫一些實實在在的應用是眞理。請遠離reflect的unsafe包,除非你確實需要它們。 + +最後,用Go快樂地編程。我們希望你能像我們一樣喜歡Go語言。 -用Go快樂地編程. 我們希望你能像我們一樣喜歡Go語言. diff --git a/ch13/ch13.md b/ch13/ch13.md index 8ca1b09..480d5aa 100644 --- a/ch13/ch13.md +++ b/ch13/ch13.md @@ -1,21 +1,20 @@ # 第13章 底層編程 -Go的設計包含了諸多安全策略, 限製了可能導致程序錯誤的用法. 編譯時類型檢査檢測可以發現大多數類型不匹配的變量操作, 例如兩個字符串做減法的錯誤. 字符串, 字典, 切片 和管道等所有的內置類型, 都有嚴格的類型轉換規則. +Go語言的設計包含了諸多安全策略,限製了可能導致程序運行出現錯誤的用法。編譯時類型檢査檢査可以發現大多數類型不匹配的操作,例如兩個字符串做減法的錯誤。字符串、map、slice和chan等所有的內置類型,都有嚴格的類型轉換規則。 -對於無法靜態檢測到的錯誤, 例如數組訪問越界或使用空指針, 動態檢測可以保證程序在遇到問題的時候立卽終止併打印相關的錯誤信息. 自動內存管理(垃圾迴收)消除了大部分野指針和內存洩漏的問題. +對於無法靜態檢測到的錯誤,例如數組訪問越界或使用空指針,運行時動態檢測可以保證程序在遇到問題的時候立卽終止併打印相關的錯誤信息。自動內存管理(垃圾內存自動迴收)可以消除大部分野指針和內存洩漏相關的問題。 -Go的實現刻意隱藏了很多底層細節. 我們無法知道一個結構體的內存布局, 也無法穫取一個運行函數的機器碼, 也無法知道當前的 goroutine 是運行在哪個操作繫統線程上. 事實上, Go的調度器會自己決定是否需要將 goroutine 從一個操作繫統線程轉移到另一個操作繫統線程. 一個指向變量的指針也併沒有展示變量眞實的地址. 因爲垃圾迴收器會根據需要移動變量的位置, 當然對應的也會被自動更新. +Go語言的實現刻意隱藏了很多底層細節。我們無法知道一個結構體眞實的內存布局,也無法獲取一個運行時函數對應的機器碼,也無法知道當前的goroutine是運行在哪個操作繫統線程之上。事實上,Go語言的調度器會自己決定是否需要將某個goroutine從一個操作繫統線程轉移到另一個操作繫統線程。一個指向變量的指針也併沒有展示變量眞實的地址。因爲垃圾迴收器可能會根據需要移動變量的內存位置,當然變量對應的地址也會被自動更新。 -總的來説, Go語言的這些特殊使得Go程序相比較低級的C語言來説, 更容易預測, 更容易理解, 也不容易崩潰. 通過隱藏底層的細節, 也使得Go程序具有高度的可移植性, 因爲語言的語義在很大程度上是獨立於任何編譯器, 操作繫統和CPU繫統結構的(當然也不完全絶對獨立: 例如CPU字的大小, 某些表達式求值的順序, 還有編譯器實現的一些限製). +總的來説,Go語言的這些特性使得Go程序相比較低級的C語言來説更容易預測和理解,程序也不容易崩潰。通過隱藏底層的實現細節,也使得Go語言編寫的程序具有高度的可移植性,因爲語言的語義在很大程度上是獨立於任何編譯器實現、操作繫統和CPU繫統結構的(當然也不是完全絶對獨立:例如int等類型就依賴於CPU機器字的大小,某些表達式求值的具體順序,還有編譯器實現的一些額外的限製等)。 -有時候我們可能會放棄部分語言特性而優先選擇更好的性能優化, 與其他語言編寫的庫互操作, 或者不用純Go語言來實現某些函數. +有時候我們可能會放棄使用部分語言特性而優先選擇更好具有更好性能的方法,例如需要與其他語言編寫的庫互操作,或者用純Go語言無法實現的某些函數。 -在本章, 我們將展示如何使用 unsafe 包來襬脫通常的規則限製, 如何創建C函數庫的綁定, 以及如何進行繫統調用. +在本章,我們將展示如何使用unsafe包來襬脫Go語言規則帶來的限製,講述如何創建C語言函數庫的綁定,以及如何進行繫統調用。 -本章描述的方法不應該輕易使用. 如果沒有處理好細節, 它們可能導致各種不可預測的隱晦的錯誤, 甚至連本地的C程序員也無法理解. 使用 unsafe 包同時也無法保證與未來版本的兼容性, 因爲在有意無意中會使用很多實現的細節, 而這些實現的細節在未來很可能會改變. +本章提供的方法不應該輕易使用(譯註:屬於黑魔法,雖然可能功能很強大,但是也容易誤傷到自己)。如果沒有處理好細節,它們可能導致各種不可預測的併且隱晦的錯誤,甚至連有經驗的的C語言程序員也無法理解這些錯誤。使用unsafe包的同時也放棄了Go語言保證與未來版本的兼容性的承諾,因爲它必然會在有意無意中會使用很多實現的細節,而這些實現的細節在未來的Go語言中很可能會被改變。 -unsafe 包的實現比較特殊. 雖然它可以和普通包一樣的導入和使用, 但它實際上是由編譯器實現的. 它提供了一些訪問語言內部特性的方法, 特别是內存布局相關的細節. -將這些特别封裝到一個獨立的包中, 是爲在極少數情況下需要使用的時候, 引起人們的註意(它們是不安全的). 此外, 有一些環境因爲安全的因素可能限製這個包的使用. +要註意的是,unsafe包是一個采用特殊方式實現的包。雖然它可以和普通包一樣的導入和使用,但它實際上是由編譯器實現的。它提供了一些訪問語言內部特性的方法,特别是內存布局相關的細節。將這些特性封裝到一個獨立的包中,是爲在極少數情況下需要使用的時候,同時引起人們的註意(譯註:因爲看包的名字就知道使用unsafe包是不安全的)。此外,有一些環境因爲安全的因素可能限製這個包的使用。 -unsafe 包被廣泛地用於比較低級的包, 例如 runtime, os, syscall 還有 net 等, 因爲它們需要和操作繫統密切配合的, 但是普通的程序一般是不需要的. +不過unsafe包被廣泛地用於比較低級的包, 例如runtime、os、syscall還有net包等,因爲它們需要和操作繫統密切配合,但是對於普通的程序一般是不需要使用unsafe包的。 diff --git a/ch2/ch2-01.md b/ch2/ch2-01.md index 5f47ff6..732ca84 100644 --- a/ch2/ch2-01.md +++ b/ch2/ch2-01.md @@ -1,8 +1,8 @@ ## 2.1. 命名 -Go語言中的的函數名, 變量名, 常量名, 類型名, 語句段標籤名, 和 包名 等所有的命名, 都遵循一個命名規則: 一個名字必鬚以一個字母(Unicode字母)或下劃線開頭, 後面可以跟任意數量的字母,數字或下劃線. 不同大小寫字母是不同的: `heapSort` 和 `Heapsort` 是兩個不同的名字. +Go語言中的函數名、變量名、常量名、類型名、語句標號和包名等所有的命名,都遵循一個簡單的命名規則:一個名字必鬚以一個字母(Unicode字母)或下劃線開頭,後面可以跟任意數量的字母、數字或下劃線。大寫字母和小寫字母是不同的:heapSort和Heapsort是兩個不同的名字。 -Go語言類似 `if` 和 `switch` 的關鍵字有25個; 關鍵字不能用於自定義名字, 隻能在特定語法中使用. +Go語言中類似if和switch的關鍵字有25個;關鍵字不能用於自定義名字,隻能在特定語法結構中使用。 ``` break default func interface select @@ -12,25 +12,25 @@ const fallthrough if range type continue for import return var ``` -此外, 還有大約30多個預先定義的名字, 比如 `int` 和 `true` 等, 主要用於內建的常量, 類型, 和 函數. +此外,還有大約30多個預定義的名字,比如int和true等,主要對應內建的常量、類型和函數。 ``` -Constants: true false iota nil +內建常量: true false iota nil -Types: int int8 int16 int32 int64 - uint uint8 uint16 uint32 uint64 uintptr - float32 float64 complex128 complex64 - bool byte rune string error +內建類型: int int8 int16 int32 int64 + uint uint8 uint16 uint32 uint64 uintptr + float32 float64 complex128 complex64 + bool byte rune string error -Functions: make len cap new append copy close delete - complex real imag - panic recover +內建函數: make len cap new append copy close delete + complex real imag + panic recover ``` -這些內部預先定義的名字不是關鍵字, 你可以在定義中重現使用它們. 在一些特殊的場景重新定義是有意義的, 但是也要註意避免引起混亂. +這些內部預先定義的名字併不是關鍵字,你可以再定義中重新使用它們。在一些特殊的場景中重新定義它們也是有意義的,但是也要註意避免過度而引起語義混亂。 -如果一個實體是在函數內部定義, 那麽它的就隻在函數內部有效. 如果是在函數外部定義, 那麽將在當前包的所有文件中都可以訪問. 名字的開頭字母的大小寫決定了名字在包外的可見性. 如果一個名字是大寫字母開頭的, 那麽它將是導齣的, 也就是可以被外部的包訪問, 例如 `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。 diff --git a/ch2/ch2-02.md b/ch2/ch2-02.md index 7272518..5d72110 100644 --- a/ch2/ch2-02.md +++ b/ch2/ch2-02.md @@ -1,9 +1,8 @@ ## 2.2. 聲明 -聲明定義了程序的入口以及部分或全部的屬性. Go主要有四種聲明類型: var, const, type, 和 func, 分别對應 變量, 常量, 類型, 和 函數的 聲明. 這一章我們重點討論變量和類型的聲明, 第三章將討論常量的聲明, 第五章將討論函數的聲明. +聲明語句定義了程序的各種實體對象以及部分或全部的屬性。Go語言主要有四種類型的聲明語句:var、const、type和func,分别對應變量、常量、類型和函數實體對象的聲明。這一章我們重點討論變量和類型的聲明,第三章將討論常量的聲明,第五章將討論函數的聲明。 -一個Go程序存儲在一個或多個以`.go`爲後綴名的文件中. 每個文件以個包的聲明開始, 以説明文件是屬於包的一部分. -包聲明之後是 import 導入聲明, 然後是包一級的類型/變量/常量/函數的聲明, 聲明的順序無關緊要. 例如, 下面的例子聲明了一個常量, 一個函數和兩個變量: +一個Go語言編寫的程序對應一個或多個以.go爲文件後綴名的源文件中。每個源文件以包的聲明語句開始,説明該源文件是屬於哪個包。包聲明語句之後是import語句導入依賴的其它包,然後是包一級的類型、變量、常量、函數的聲明語句,包一級的各種類型的聲明語句的順序無關緊要(譯註:函數內部的名字則必鬚先聲明之後才能使用)。例如,下面的例子中聲明了一個常量、一個函數和兩個變量: ```Go gopl.io/ch2/boiling @@ -23,11 +22,11 @@ func main() { } ``` -其中 常量 `boilingF` 是在包一級聲明的, 然後 `f` 和 `c` 是在 main 函數內部聲明的. 在包一級聲明的名字可在整個包訪問, 而不僅僅在其聲明的文件中訪問. 相比之下, 局部聲明的名字就隻能在函數內部很小的部分可訪問. +其中常量boilingF是在包一級范圍聲明語句聲明的,然後f和c兩個變量是在main函數內部聲明的聲明語句聲明的。在包一級聲明語句聲明的名字可在整個包對應的每個源文件中訪問,而不是僅僅在其聲明語句所在的源文件中訪問。相比之下,局部聲明的名字就隻能在函數內部很小的范圍被訪問。 -一個函數的聲明有一個函數名字, 參數列表(由函數的調用者提供參數變量的具體值), 一個可選的返迴值列表, 和包含函數語句定義的函數體. 如果函數沒有返迴值, 那麽返迴值列表是省略的. 執行函數從函數的第一個語句開始, 但是順序執行直到遇到 renturn 返迴語言, 如果沒有返迴語句則是到函數末尾, 然後返迴到調用者. +一個函數的聲明由一個函數名字、參數列表(由函數的調用者提供參數變量的具體值)、一個可選的返迴值列表和包含函數定義的函數體組成。如果函數沒有返迴值,那麽返迴值列表是省略的。執行函數從函數的第一個語句開始,依次順序執行直到遇到renturn返迴語句,如果沒有返迴語句則是執行到函數末尾,然後返迴到函數調用者。 -我們已經看到過很多函數的例子了, 在第五章將深入討論函數的細節, 這里隻粗略説下. 下面的 `fToC` 函數封裝了溫度轉換的邏輯, 這樣它隻需要定義一次, 就可以在多個地方多次使用. 這個例子中, main 函數就調用了兩次 `fToC` 函數, 分别是使用局部定義的兩個常量作爲函數參數. +我們已經看到過很多函數聲明和函數調用的例子了,在第五章將深入討論函數的相關細節,這里隻簡單解釋下。下面的fToC函數封裝了溫度轉換的處理邏輯,這樣它隻需要被定義一次,就可以在多個地方多次被使用。在這個例子中,main函數就調用了兩次fToC函數,分别是使用在局部定義的兩個常量作爲調用函數的參數。 ```Go diff --git a/ch2/ch2-03-1.md b/ch2/ch2-03-1.md index dbbe58c..816bdb2 100644 --- a/ch2/ch2-03-1.md +++ b/ch2/ch2-03-1.md @@ -1,6 +1,6 @@ ### 2.3.1. 簡短變量聲明 -在函數內部, 有一種稱爲簡短變量聲明的形式可用於聲明和初始化局部變量. 以 `名字 := 表達式` 方式聲明變量, 變量的類型根據表達式來推導. 這里函數中是三個簡短變量聲明語句(§1.4): +在函數內部,有一種稱爲簡短變量聲明語句的形式可用於聲明和初始化局部變量。它以“名字 := 表達式”形式聲明變量,變量的類型根據表達式來自動推導。下面是lissajous函數中的三個簡短變量聲明語句(§1.4): ```Go anim := gif.GIF{LoopCount: nframes} @@ -8,32 +8,31 @@ freq := rand.Float64() * 3.0 t := 0.0 ``` -因爲簡潔和靈活性, 簡短變量聲明用於大部分的局部變量的聲明和初始化. var 方式的聲明往往是用於需要顯示指定類型的局部變量, 或者因爲稍後會被賦值而初始值無關緊要的變量. - +因爲簡潔和靈活的特點,簡短變量聲明被廣泛用於大部分的局部變量的聲明和初始化。var形式的聲明語句往往是用於需要顯式指定變量類型地方,或者因爲變量稍後會被重新賦值而初始值無關緊要的地方。 ```Go -i := 100 // an int +i := 100 // an int var boiling float64 = 100 // a float64 var names []string var err error var p Point ``` -於 var 聲明變量一樣, 簡短變量聲明也可以用來聲明和初始化一組變量: +和var形式聲明變語句一樣,簡短變量聲明語句也可以用來聲明和初始化一組變量: ```Go i, j := 0, 1 ``` -但是這種聲明多個變量的方式隻簡易在可以提高代碼可讀性的地方使用, 比如 for 循環的初始化部分. +但是這種同時聲明多個變量的方式應該限製隻在可以提高代碼可讀性的地方使用,比如for語句的循環的初始化語句部分。 -請記住 `:=` 是一個變量聲明, 而 `=` 是一個賦值操作. 不要混淆多個變量的聲明和元組的多重(§2.4.1), 後者是將右邊的表達式值賦給左邊對應位置的變量: +請記住“:=”是一個變量聲明語句,而“=‘是一個變量賦值操作。也不要混淆多個變量的聲明和元組的多重賦值(§2.4.1),後者是將右邊各個的表達式值賦值給左邊對應位置的各個變量: ```Go i, j = j, i // 交換 i 和 j 的值 ``` -和普通 var 變量聲明一樣, 簡短變量聲明也可以用調用函數的返迴值來聲明, 像 os.Open 函數返迴兩個值: +和普通var形式的變量聲明語句一樣,簡短變量聲明語句也可以用函數的返迴值來聲明和初始化變量,像下面的os.Open函數調用將返迴兩個值: ```Go f, err := os.Open(name) @@ -44,9 +43,9 @@ if err != nil { f.Close() ``` -這里有一個比較微妙的地方: 簡短變量聲明左邊的全部變量可能併不是全部都是剛剛聲明的. 如果有一些已經在相同的詞法塊聲明過了(§2.7), 那麽簡短變量聲明對這些已經聲明過的變量就隻有賦值行爲了. +這里有一個比較微妙的地方:簡短變量聲明左邊的變量可能併不是全部都是剛剛聲明的。如果有一些已經在相同的詞法域聲明過了(§2.7),那麽簡短變量聲明語句對這些已經聲明過的變量就隻有賦值行爲了。 -在下面的代碼中, 第一個語句聲明了 in 和 err 變量. 第二個語句隻聲明了 out, 然後對已經聲明的 err 進行賦值. +在下面的代碼中,第一個語句聲明了in和err兩個變量。在第二個語句隻聲明了out一個變量,然後對已經聲明的err進行了賦值操作。 ```Go in, err := os.Open(infile) @@ -54,7 +53,7 @@ in, err := os.Open(infile) out, err := os.Create(outfile) ``` -簡短變量聲明必鬚至少聲明一個新的變量, 否則編譯將不能通過: +簡短變量聲明語句中必鬚至少要聲明一個新的變量,下面的代碼將不能編譯通過: ```Go f, err := os.Open(infile) @@ -62,8 +61,9 @@ f, err := os.Open(infile) f, err := os.Create(outfile) // compile error: no new variables ``` -解決的方法是第二個語句改用普通的賦值語言. +解決的方法是第二個簡短變量聲明語句改用普通的多重賦值語言。 + +簡短變量聲明語句隻有對已經在同級詞法域聲明過的變量才和賦值操作語句等價,如果變量是在外部詞法域聲明的,那麽簡短變量聲明語句將會在當前詞法域重新聲明一個新的變量。我們在本章後面將會看到類似的例子。 -簡短變量聲明隻有對在變量已經在同級詞法域聲明過的變量纔和賦值操作等同, 如果變量是在外部詞法域聲明了, 那麽將會聲明一個新變量. 我們在本章後面將會看到類似的例子. diff --git a/ch2/ch2-03-2.md b/ch2/ch2-03-2.md index 45c9e40..77a829c 100644 --- a/ch2/ch2-03-2.md +++ b/ch2/ch2-03-2.md @@ -1,10 +1,10 @@ -### 2.3.2 指針 +### 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表達式(取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,17 +36,17 @@ func f() *int { } ``` -每次調用 f 函數都將返迴不同的結果: +每次調用f函數都將返迴不同的結果: ```Go fmt.Println(f() == f()) // "false" ``` -因爲指針包含了一個變量的地址, 因此將指針作爲參數調用函數, 將可以在函數中通過指針更新變量的值. 例如這個通過指針來更新變量的值, 然後返迴更新後的值, 可用在一個表達式中: +因爲指針包含了一個變量的地址,因此如果將指針作爲參數調用函數,那將可以在函數中通過該指針來更新變量的值。例如下面這個例子就是通過指針來更新變量的值,然後返迴更新後的值,可用在一個表達式中(譯註:這是對C語言中`++v`操作的模擬,這里隻是爲了説明指針的用法,incr函數模擬的做法併不推薦): ```Go func incr(p *int) int { - *p++ // increments what p points to; does not change p + *p++ // 非常重要:隻是增加p指向的變量的值,併不改變p指針!!! return *p } @@ -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的别名。指針特别有價值的地方在於我們可以不用名字而訪問一個變量,但是這是一把雙刃劍:要找到一個變量的所有訪問者併不容易,我們必鬚知道變量全部的别名(譯註:這是Go語言的垃圾迴收器所做的工作)。不僅僅是指針會創建别名,很多其他引用類型也會創建别名,例如slice、map和chan,甚至結構體、數組和接口都會創建所引用變量的别名。 -指針是 flag 包的關鍵, 它使用命令行參數來設置對應的變量, 而這些分布在整個程序中. 爲了説明這一點, 在早些的echo版本中, 包含了兩個可選的命令行參數: `-n` 用於忽略行尾的換行符, `-s sep` 用於指定分隔字符(默認是空格). 這是第四個版本, 對應包 gopl.io/ch2/echo4. +指針是實現標準庫中flag包的關鍵技術,它使用命令行參數來設置對應變量的值,而這些對應命令行標誌參數的變量可能會零散分布在整個程序中。爲了説明這一點,在早些的echo版本中,就包含了兩個可選的命令行參數:`-n`用於忽略行尾的換行符,`-s sep`用於指定分隔字符(默認是空格)。下面這是第四個版本,對應包路徑爲gopl.io/ch2/echo4。 ```Go gopl.io/ch2/echo4 @@ -82,12 +82,11 @@ func main() { } ``` -`flag.Bool` 函數調用創建了一個新的布爾型標誌參數變量. 它有三個屬性: 第一個是的名字"n", 然後是標誌的默認值(這里是false), 最後是對應的描述信息. 如果用戶輸入了無效的標誌參數, 或者輸入 `-h` 或 `-help` 標誌參數, 將打印標誌參數的名字, 默認值和描述信息. 類似的, flag.String 用於創建一個字符串類型的標誌參數變量, 同樣包含參數名, 默認值, 和描述信息. 變量 `sep` 和 `n` 是一個指向標誌參數變量的指針, 因此必鬚用 *sep 和 *n 的方式間接引用. +調用flag.Bool函數會創建一個新的對應布爾型標誌參數的變量。它有三個屬性:第一個是的命令行標誌參數的名字“n”,然後是該標誌參數的默認值(這里是false),最後是該標誌參數對應的描述信息。如果用戶在命令行輸入了一個無效的標誌參數,或者輸入`-h`或`-help`參數,那麽將打印所有標誌參數的名字、默認值和描述信息。類似的,調用flag.String函數將於創建一個對應字符串類型的標誌參數變量,同樣包含命令行標誌參數對應的參數名、默認值、和描述信息。程序中的`sep`和`n`變量分别是指向對應命令行標誌參數變量的指針,因此必鬚用`*sep`和`*n`形式的指針語法間接引用它們。 +當程序運行時,必鬚在使用標誌參數對應的變量之前調用先flag.Parse函數,用於更新每個標誌參數對應變量的值(之前是默認值)。對於非標誌參數的普通命令行參數可以通過調用flag.Args()函數來訪問,返迴值對應對應一個字符串類型的slice。如果在flag.Parse函數解析命令行參數時遇到錯誤,默認將打印相關的提示信息,然後調用os.Exit(2)終止程序。 -當程序運行時, 必鬚在標誌參數變量使用之前調用 flag.Parse 函數更新標誌參數變量的值(之前是默認值). 非標誌參數的普通類型參數可以用 flag.Args() 訪問, 對應一個 字符串切片. 如果 flag.Parse 解析遇到錯誤, 將打印提示信息, 然後調用 os.Exit(2) 終止程序. - -讓我們運行一些 echo 測試用例: +讓我們運行一些echo測試用例: ``` $ go build gopl.io/ch2/echo4 diff --git a/ch2/ch2-03-3.md b/ch2/ch2-03-3.md index 4f2972a..8e7b904 100644 --- a/ch2/ch2-03-3.md +++ b/ch2/ch2-03-3.md @@ -1,7 +1,6 @@ -### 2.3.3 new 函數 +### 2.3.3. new函數 - -另一個創建變量的方法是用內建的 new 函數. 表達式 `new(T)` 創建一個T類型的匿名變量, 初始化爲T類型的零值, 返迴返迴變量地址, 返迴指針類型爲 `*T`. +另一個創建變量的方法是調用用內建的new函數。表達式new(T)將創建一個T類型的匿名變量,初始化爲T類型的零值,然後返迴變量地址,返迴的指針類型爲`*T`。 ```Go p := new(int) // p, *int 類型, 指向匿名的 int 變量 @@ -10,10 +9,9 @@ fmt.Println(*p) // "0" fmt.Println(*p) // "2" ``` +用new創建變量和普通變量聲明語句方式創建變量沒有什麽區别,除了不需要聲明一個臨時變量的名字外,我們還可以在表達式中使用new(T)。換言之,new函數類似是一種語法醣,而不是一個新的基礎概念。 -從 new 創建變量和普通聲明方式創建變量沒有什麽區别, 除了不需要聲明一個臨時變量的名字外, 我們還可以在表達式中使用 `new(T)`. 換言之, new 類似是一種語法醣, 而不是一個新的基礎概念. - -下面的兩個 newInt 函數有着相同的行爲: +下面的兩個newInt函數有着相同的行爲: ```Go func newInt() *int { func newInt() *int { @@ -22,7 +20,7 @@ func newInt() *int { func newInt() *int { } ``` -每次調用 new 都是返迴一個新的變量的地址, 因此下面兩個地址是不同的: +每次調用new函數都是返迴一個新的變量的地址,因此下面兩個地址是不同的: ```Go p := new(int) @@ -30,15 +28,15 @@ q := new(int) fmt.Println(p == q) // "false" ``` -當然也有特殊情況: 如果兩個類型都是空的, 也就是説類型的大小是0, 例如 `struct{}` 和 `[0]int`, 有可能有相同的地址(依賴具體的語言實現). +當然也可能有特殊情況:如果兩個類型都是空的,也就是説類型的大小是0,例如`struct{}`和 `[0]int`, 有可能有相同的地址(依賴具體的語言實現)(譯註:請謹慎使用大小爲0的類型,因爲如果類型的大小位0好話,可能導致Go語言的自動垃圾迴收器有不同的行爲,具體請査看`runtime.SetFinalizer`函數相關文檔)。 -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函數的。 diff --git a/ch2/ch2-03-4.md b/ch2/ch2-03-4.md index a483c7c..05f5cc8 100644 --- a/ch2/ch2-03-4.md +++ b/ch2/ch2-03-4.md @@ -1,40 +1,52 @@ ### 2.3.4. 變量的生命週期 -變量的生命週期指的是程序運行期間變量存在的有效時間間隔. 包級聲明的變量的生命週期和程序的生命週期是一致的. 相比之下, 局部變量的聲明週期是動態的: 從每次創建一個新變量的聲明語句被執行開始, 直到變量不在被引用爲止, 然後變量的存儲空間可能被迴收. 函數的參數變量和返迴值變量都是局部變量. 它們在函數每次被調用的時候創建. +變量的生命週期指的是在程序運行期間變量有效存在的時間間隔。對於在包一級聲明的變量來説,它們的生命週期和整個程序的運行週期是一致的。而相比之下,在局部變量的聲明週期則是動態的:從每次創建一個新變量的聲明語句開始,直到該變量不再被引用爲止,然後變量的存儲空間可能被迴收。函數的參數變量和返迴值變量都是局部變量。它們在函數每次被調用的時候創建。 -例如, 下面是從 1.4 節的 Lissajous 程序摘録的代碼片段: +例如,下面是從1.4節的Lissajous程序摘録的代碼片段: ```Go -for t := 0.0; t < cycles*2*math.Pi; t += res { - x := math.Sin(t) - y := math.Sin(t*freq + phase) - img.SetColorIndex(size+int(x*size+0.5), size+int(y*size+0.5), - blackIndex) -} +for t := 0.0; t < cycles*2*math.Pi; t += res { + x := math.Sin(t) + y := math.Sin(t*freq + phase) + img.SetColorIndex(size+int(x*size+0.5), size+int(y*size+0.5), + blackIndex) +} ``` -在每次循環的開始創建變量 t, 然後在每次循環迭代中創建 x 和 y. - -那麽垃圾收集器是如何知道一個變量是何時可以被迴收的呢? 這里我們先避開完整的技術細節, 但是基本的思路是, 從每個包級的變量和每個當前運行函數的每一個局部變量開始, 通過指針或引用的路徑, 是否可以找到該變量. 如果不存在這樣的路徑, 那麽説明該變量是不可達的, 也就是説它併不會影響其餘的計算. - -因爲一個變量的聲明週期隻取決於是否可達, 因此一個循環迭代內部的局部變量的生命週期可能超齣其局部作用域. 它可能在函數返迴之後依然存在. - -編譯器會選擇在棧上還是在堆上分配局部變量的存儲空間, 但可能令人驚訝的是, 這個選擇併不是由 var 或 new 來決定的. +譯註:函數的有右小括弧也可以另起一行縮進,同時爲了防止編譯器在行尾自動插入分號而導致的編譯錯誤,可以在末尾的參數變量後面顯式插入逗號。像下面這樣: ```Go -var global *int - -func f() { func g() { - var x int y := new(int) - x = 1 *y = 1 - global = &x } -} +for t := 0.0; t < cycles*2*math.Pi; t += res { + x := math.Sin(t) + y := math.Sin(t*freq + phase) + img.SetColorIndex( + size+int(x*size+0.5), size+int(y*size+0.5), + blackIndex, // 最後插入的逗號不會導致編譯錯誤,這是Go編譯器的一個特性 + ) // 小括弧另起一行縮進,和大括弧的風格保存一致 +} ``` -這里的 x 必鬚在堆上分配, 因爲它在函數退齣後依然可以通過包的 global 變量找到, 雖然它是在函數內部定義的; 我們説這個 x 局部變量從 函數 f 中逃逸了. 相反, 當 g 函數返迴時, 變量 `*y` 將是不可達的, 也就是可以被迴收的. 因此, `*y` 併沒有從 函數 g 逃逸, 編譯器可以選擇在棧上分配 `*y` 的存儲空間, 雖然這里用的是 new 方式. -在任何時候, 你併不需爲了編寫正確的代碼而要考慮變量的逃逸行爲, 要記住的是, 逃逸的變量需要額外分配內存, 同時對性能的優化會産生一定的影響. +在每次循環的開始會創建臨時變量t,然後在每次循環迭代中創建臨時變量x和y。 + +那麽垃Go語言的自動圾收集器是如何知道一個變量是何時可以被迴收的呢?這里我們可以避開完整的技術細節,基本的實現思路是,從每個包級的變量和每個當前運行函數的每一個局部變量開始,通過指針或引用的訪問路徑遍歷,是否可以找到該變量。如果不存在這樣的訪問路徑,那麽説明該變量是不可達的,也就是説它是否存在併不會影響程序後續的計算結果。 + +因爲一個變量的有效週期隻取決於是否可達,因此一個循環迭代內部的局部變量的生命週期可能超出其局部作用域。同時,局部變量可能在函數返迴之後依然存在。 + +編譯器會自動選擇在棧上還是在堆上分配局部變量的存儲空間,但可能令人驚訝的是,這個選擇併不是由用var還是new聲明變量的方式決定的。 + +```Go +var global *int + +func f() { func g() { + var x int y := new(int) + x = 1 *y = 1 + global = &x } +} +``` + +這里的x變量必鬚在堆上分配,因爲它在函數退出後依然可以通過包一級的global變量找到,雖然它是在函數內部定義的;用Go語言的術語説,這個x局部變量從函數f中逃逸了。相反,當g函數返迴時,變量`*y`將是不可達的,也就是説可以馬上被迴收的。因此,`*y`併沒有從函數g中逃逸,編譯器可以選擇在棧上分配`*y`的存儲空間(譯註:也可以選擇在堆上分配,然後由Go語言的GC迴收這個變量的內存空間),雖然這里用的是new方式。其實在任何時候,你併不需爲了編寫正確的代碼而要考慮變量的逃逸行爲,要記住的是,逃逸的變量需要額外分配內存,同時對性能的優化可能會産生細微的影響。 + +Go語言的自動垃圾收集器對編寫正確的代碼是一個鉅大的幫助,但也併不是説你完全不用考慮內存了。你雖然不需要顯式地分配和釋放內存,但是要編寫高效的程序你依然需要了解變量的生命週期。例如,如果將指向短生命週期對象的指針保存到具有長生命週期的對象中,特别是保存到全局變量時,會阻止對短生命週期對象的垃圾迴收(從而可能影響程序的性能)。 -垃圾收集器對編寫正確的代碼是一個鉅大的幫助, 但併不是説你完全不用考慮內存了. 你雖然不需要顯式地分配和釋放內存, 但是要編寫高效的程序你還是需要知道變量的生命週期. 例如, 將指向短生命週期對象的指針保存到具有長生命週期的對象中, 特别是全局變量時, 會阻止對短生命週期對象的垃圾迴收. - diff --git a/ch2/ch2-03.md b/ch2/ch2-03.md index 3c3bbf9..4b587c0 100644 --- a/ch2/ch2-03.md +++ b/ch2/ch2-03.md @@ -1,33 +1,32 @@ ## 2.3. 變量 -var 聲明可以創建一個特定類型的變量, 然後給變量附加一個名字, 併且設置變量的初始值. 變量聲明的一般語法: +var聲明語句可以創建一個特定類型的變量,然後給變量附加一個名字,併且設置變量的初始值。變量聲明的一般語法如下: ```Go -var name type = 表達式 +var 變量名字 類型 = 表達式 ``` -其中類型或 `= 表達式` 可以省略其中的一個. 如果省略的是類型信息, 那麽將根據初始化表達式類推導類型信息. 如果初始化表達式被省略, 那麽將用零值初始化變量. 數值類型變量的零值是0, 布爾類型變量的零值是 false, 字符串的零值是空字符串, 接口或引用類型(包括 切片, 字典, 通道 和 函數)的變量的零值是 nil. 數組或結構體等聚合類型的零值是每個元素或字段都是零值. +其中“*類型*”或“*= 表達式*”兩個部分可以省略其中的一個。如果省略的是類型信息,那麽將根據初始化表達式來推導變量的類型信息。如果初始化表達式被省略,那麽將用零值初始化該變量。 數值類型變量對應的零值是0,布爾類型變量對應的零值是false,字符串類型對應的零值是空字符串,接口或引用類型(包括slice、map、chan和函數)變量對應的零值是nil。數組或結構體等聚合類型對應的零值是每個元素或字段都是對應該類型的零值。 -零值機製可以確保每個聲明的變量總是有一個良好定義的值, 在 Go 中不存在未初始化的變量. 這個可以簡化很多代碼, 在沒有增加額外工作的前提下確保邊界條件下的合理行爲. 例如: +零值初始化機製可以確保每個聲明的變量總是有一個良好定義的值,因此在Go語言中不存在未初始化的變量。這個特性可以簡化很多代碼,而且可以在沒有增加額外工作的前提下確保邊界條件下的合理行爲。例如: ```Go var s string fmt.Println(s) // "" ``` -這段代碼將打印一個空字符串, 而不是導致錯誤或産生不可預知的行爲. Go 程序員經常讓一些聚合類型的零值也有意義, 這樣不管任何類型的變量總是有一個合理的零值狀態. +這段代碼將打印一個空字符串,而不是導致錯誤或産生不可預知的行爲。Go語言程序員應該讓一些聚合類型的零值也具有意義,這樣可以保證不管任何類型的變量總是有一個合理有效的零值狀態。 -可以在一個聲明語句中同時聲明一組變量, 或用一組初始化表達式聲明併初始化一組變量. -如果省略每個變量的類型, 將可以聲明多個不同類型的變量(類型由初始化表達式推導): +也可以在一個聲明語句中同時聲明一組變量,或用一組初始化表達式聲明併初始化一組變量。如果省略每個變量的類型,將可以聲明多個類型不同的變量(類型由初始化表達式推導): ```Go -var i, j, k int // int, int, int +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),局部變量將在聲明語句被執行到的時候完成初始化。 -一組變量的初始化也可以通過調用一個函數, 由函數返迴的多個返迴值初始化: +一組變量也可以通過調用一個函數,由函數返迴的多個返迴值初始化: ```Go var f, err = os.Open(name) // os.Open returns a file and an error diff --git a/ch2/ch2-04-1.md b/ch2/ch2-04-1.md index d1d5451..f542293 100644 --- a/ch2/ch2-04-1.md +++ b/ch2/ch2-04-1.md @@ -1,6 +1,6 @@ ### 2.4.1. 元組賦值 -元組賦值是另一種形式的賦值語句, 允許同時更新多個變量的值. 在賦值之前, 賦值語句右邊的所有表達式將會先進行求值, 然後再統一更新左邊變量的值. 這對於處理有些同時齣現在元組賦值語句左右兩邊的變量很有幫助, 例如我們可以這樣交換兩個變量的值: +元組賦值是另一種形式的賦值語句,它允許同時更新多個變量的值。在賦值之前,賦值語句右邊的所有表達式將會先進行求值,然後再統一更新左邊對應變量的值。這對於處理有些同時出現在元組賦值語句左右兩邊的變量很有幫助,例如我們可以這樣交換兩個變量的值: ```Go x, y = y, x @@ -8,7 +8,7 @@ x, y = y, x a[i], a[j] = a[j], a[i] ``` -或者是計算兩個整數值的的最大公約數(GCD): +或者是計算兩個整數值的的最大公約數(GCD)(譯註:GCD不是那個敏感字,而是greatest common divisor的縮寫,歐幾里德的GCD是最早的非平凡算法): ```Go func gcd(x, y int) int { @@ -19,7 +19,7 @@ func gcd(x, y int) int { } ``` -或者是計算斐波納契數列(Fibonacci)的第N個數: +或者是計算斐波納契數列(Fibonacci)的第N個數: ```Go func fib(n int) int { @@ -31,22 +31,21 @@ 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。在稍後我們將看到的三個操作都是類似的用法。如果map査找(§4.3)、類型斷言(§7.10)或通道接收(§8.4.2)出現在賦值語句的右邊,它們都可能會産生兩個結果,有一個額外的布爾結果表示操作是否成功: ```Go v, ok = m[key] // map lookup @@ -54,10 +53,22 @@ v, ok = x.(T) // type assertion v, ok = <-ch // channel receive ``` -和變量的聲明一樣, 我們可以用下劃線空白標識符 `_` 來丟棄不需要的值. +譯註:map査找(§4.3)、類型斷言(§7.10)或通道接收(§8.4.2)出現在賦值語句的右邊時,併不一定是産生兩個結果,也可能隻産生一個結果。對於值産生一個結果的情形,map査找失敗時會返迴零值,類型斷言失敗時會發送運行時panic異常,通道接收失敗時會返迴零值(阻塞不算是失敗)。例如下面的例子: + +```Go +v = m[key] // map査找,失敗時返迴零值 +v = x.(T) // type斷言,失敗時panic異常 +v = <-ch // 管道接收,失敗時返迴零值(阻塞不算是失敗) + +_, ok = m[key] // map返迴2個值 +_, ok = mm[""], false // map返迴1個值 +_ = mm[""] // map返迴1個值 +``` + +和變量聲明一樣,我們可以用下劃線空白標識符`_`來丟棄不需要的值。 ```Go _, err = io.Copy(dst, src) // 丟棄字節數 -_, ok = x.(T) // 隻檢測類型, 忽略具體值 +_, ok = x.(T) // 隻檢測類型,忽略具體值 ``` diff --git a/ch2/ch2-04-2.md b/ch2/ch2-04-2.md index 655453a..088e079 100644 --- a/ch2/ch2-04-2.md +++ b/ch2/ch2-04-2.md @@ -1,12 +1,12 @@ ### 2.4.2. 可賦值性 -賦值語句是顯示的賦值形式, 但是程序中還有很多地方會發送隱式的賦值行爲: 函數調用將隱式地將調用參數的值賦值給函數的參數變量, 一個返迴語句將隱式地將返迴操作的值賦值給結果變量, 一個複合類型的字面量(§4.2)也會産生賦值行爲. 例如下面的語句: +賦值語句是顯式的賦值形式,但是程序中還有很多地方會發生隱式的賦值行爲:函數調用會隱式地將調用參數的值賦值給函數的參數變量,一個返迴語句將隱式地將返迴操作的值賦值給結果變量,一個複合類型的字面量(§4.2)也會産生賦值行爲。例如下面的語句: ```Go medals := []string{"gold", "silver", "bronze"} ``` -隱式地對切片的每個元素進行賦值操作, 類似這樣寫的行爲: +隱式地對slice的每個元素進行賦值操作,類似這樣寫的行爲: ```Go medals[0] = "gold" @@ -14,15 +14,12 @@ medals[1] = "silver" medals[2] = "bronze" ``` -字典和管道的元素, 雖然不是普通的變量, 但是也有類似的隱式賦值行爲. +map和chan的元素,雖然不是普通的變量,但是也有類似的隱式賦值行爲。 -不管是隱式還是顯示地賦值, 在賦值語句坐標的變量和右邊最終的求到的值必鬚有相同的數據類型. 更直白地説, 隻有右邊的值對於左邊的變量是可賦值的, 賦值語句纔是允許的. +不管是隱式還是顯式地賦值,在賦值語句左邊的變量和右邊最終的求到的值必鬚有相同的數據類型。更直白地説,隻有右邊的值對於左邊的變量是可賦值的,賦值語句才是允許的。 -可賦值性的規則對於不同類型有不同要求, 對每個新類型有關的地方我們會專門解釋. -對於目前我們已經討論過的類型, 它的規則是簡單的: 類型必鬚完全匹配, nil 可以賦值給任何指針或引用類型的變量. 常量(§3.6)有更靈活的規則, 這樣可以避免不必要的顯示類型轉換. +可賦值性的規則對於不同類型有着不同要求,對每個新類型特殊的地方我們會專門解釋。對於目前我們已經討論過的類型,它的規則是簡單的:類型必鬚完全匹配,nil可以賦值給任何指針或引用類型的變量。常量(§3.6)則有更靈活的賦值規則,因爲這樣可以避免不必要的顯式的類型轉換。 -對於兩個值是否可以用 `==` 或 `!=` 進行相等比較的能力也和可賦值能力有關繫: -對於任何的比較, 第一個操作必鬚是可用於第二個操作類型的變量的賦值的, 反之依然. -和前面一樣, 我們會對每個新類型比較有關的地方會做專門解釋. +對於兩個值是否可以用`==`或`!=`進行相等比較的能力也和可賦值能力有關繫:對於任何類型的值的相等比較,第二個值必鬚是對第一個值類型對應的變量是可賦值的,反之依然。和前面一樣,我們會對每個新類型比較特殊的地方做專門的解釋。 diff --git a/ch2/ch2-04.md b/ch2/ch2-04.md index 5e7c6e5..d696d96 100644 --- a/ch2/ch2-04.md +++ b/ch2/ch2-04.md @@ -1,28 +1,28 @@ ## 2.4. 賦值 -使用賦值語句可以更新一個變量的值, 最簡單的賦值語句是將要被賦值的變量放在 `=` 的左邊, 新值的表達式放在 `=` 右邊. +使用賦值語句可以更新一個變量的值,最簡單的賦值語句是將要被賦值的變量放在=的左邊,新值的表達式放在=的右邊。 ```Go x = 1 // 命令變量的賦值 *p = true // 通過指針間接賦值 person.name = "bob" // 結構體字段賦值 -count[x] = count[x] * scale // 數組, 切片 或 字典的 元素賦值 +count[x] = count[x] * scale // 數組、slice或map的元素賦值 ``` -特定的賦值語句和二元算術複合操作有一個簡潔形式, 例如上面最後的語句可以重寫爲: +特定的二元算術運算符和賦值語句的複合操作有一個簡潔形式,例如上面最後的語句可以重寫爲: ```Go count[x] *= scale ``` -這樣可以省去對變量表達式的重複計算. +這樣可以省去對變量表達式的重複計算。 -數值變量也可以支持 `++` 遞增和 `--` 遞減語句: +數值變量也可以支持`++`遞增和`--`遞減語句(譯註:自增和自減是語句,而不是表達式,因此`x = i++`之類的表達式是錯誤的): ```Go v := 1 -v++ // 等價方式 v = v + 1; v 變成 2 -v-- // 等價方式 v = v - 1; v 變成 1 +v++ // 等價方式 v = v + 1;v 變成 2 +v-- // 等價方式 v = v - 1;v 變成 1 ``` {% include "./ch2-04-1.md" %} diff --git a/ch2/ch2-05.md b/ch2/ch2-05.md index 598aab0..4e70384 100644 --- a/ch2/ch2-05.md +++ b/ch2/ch2-05.md @@ -1,23 +1,24 @@ -## 2.5. 類型聲明 +## 2.5. 類型 -變量或表達式的類型定義了對應存儲值的特徵, 例如數值的存儲大小(或者是元素的bit個數), 它們在內部是如何表達的, 是否支持一些操作符, 以及它們自己關聯的方法集, +變量或表達式的類型定義了對應存儲值的屬性特徵,例如數值在內存的存儲大小(或者是元素的bit個數),它們在內部是如何表達的,是否支持一些操作符,以及它們自己關聯的方法集等。 -在任何程序中都會有一些變量有着相同的內部實現, 但是表示完全不同的概念. -例如, int 類型的變量可以用來表示一個循環的迭代索引, 或者一個時間戳, 或者一個文件描述符, 或者一個月份; 一個 float64 類型的變量可以用來表示每秒幾米的速度, 或者是不同溫度單位的溫度; -一個字符串可以用來表示一個密碼或者一個顔色的名稱. +在任何程序中都會存在一些變量有着相同的內部結構,但是卻表示完全不同的概念。例如,一個int類型的變量可以用來表示一個循環的迭代索引、或者一個時間戳、或者一個文件描述符、或者一個月份;一個float64類型的變量可以用來表示每秒移動幾米的速度、或者是不同溫度單位下的溫度;一個字符串可以用來表示一個密碼或者一個顔色的名稱。 -一個類型的聲明創建了一個新的類型名稱, 和現有類型具有相同的底層結構. -新命名的類型提供了一個方法, 用來分隔不同概念的類型, 卽使它們底層類型相同也是不兼容的. +一個類型聲明語句創建了一個新的類型名稱,和現有類型具有相同的底層結構。新命名的類型提供了一個方法,用來分隔不同概念的類型,這樣卽使它們底層類型相同也是不兼容的。 ```Go -type name underlying-type +type 類型名字 底層類型 ``` -類型的聲明一般齣現在包級别, 因此如果新創建的類型名字名字的首字符大寫, 則在外部包也可以使用. +類型聲明語句一般出現在包一級,因此如果新創建的類型名字的首字符大寫,則在外部包也可以使用。 -爲了説明類型聲明, 我們將不同溫度單位分别定義爲不同的類型: +譯註:對於中文漢字,Unicode標誌都作爲小寫字母處理,因此中文的命名默認不能導出;不過国內的用戶針對該問題提出了我們自己的間接,根據RobPike的迴複,在Go2中有可能會將中日韓等字符當作大寫字母處理。下面是RobPik在 [Issue763](https://github.com/golang/go/issues/5763) 的迴複: -爲了説明類型聲明,讓我們把不同溫度范圍分爲不同的類型: +> A solution that's been kicking around for a while: +> +> For Go 2 (can't do it before then): Change the definition to “lower case letters and _ are package-local; all else is exported”. Then with non-cased languages, such as Japanese, we can write 日本語 for an exported name and _日本語 for a local name. This rule has no effect, relative to the Go 1 rule, with cased languages. They behave exactly the same. + +爲了説明類型聲明,我們將不同溫度單位分别定義爲不同的類型: ```Go gopl.io/ch2/tempconv0 @@ -32,7 +33,7 @@ type Fahrenheit float64 // 華氏溫度 const ( AbsoluteZeroC Celsius = -273.15 // 絶對零度 FreezingC Celsius = 0 // 結冰點溫度 - BoilingC Celsius = 100 // 沸水問題 + BoilingC Celsius = 100 // 沸水溫度 ) func CToF(c Celsius) Fahrenheit { return Fahrenheit(c*9/5 + 32) } @@ -40,16 +41,13 @@ 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類型(譯註:如果T是指針類型,可能會需要用小括弧包裝T,比如`(*int)(0)`)。隻有當兩個類型的底層基礎類型相同時,才允許這種轉型操作,或者是兩者都是指向相同底層結構的指針類型,這些轉換隻改變類型而不會影響值本身。如果x是可以賦值給T類型的值,那麽x必然也可以被轉爲T類型,但是一般沒有這個必要。 -數值類型之間的轉型也是允許的, 併且在字符串和一些特定切片之間也是可以轉換的, 在下一章我們會看到這樣的例子. 這類轉換可能改變值的表現. 例如, 將一個浮點數轉爲整數將丟棄小數部分, 將一個字符串轉爲 []byte 切片將拷貝一個字符串數據的副本. 在任何情況下, 運行時不會發送轉換失敗的錯誤(譯註: 錯誤隻會發生在編譯階段). +數值類型之間的轉型也是允許的,併且在字符串和一些特定類型的slice之間也是可以轉換的,在下一章我們會看到這樣的例子。這類轉換可能改變值的表現。例如,將一個浮點數轉爲整數將丟棄小數部分,將一個字符串轉爲`[]byte`類型的slice將拷貝一個字符串數據的副本。在任何情況下,運行時不會發生轉換失敗的錯誤(譯註: 錯誤隻會發生在編譯階段)。 -底層數據類型決定了內部結構和表達方式, 也包決定是否可以像底層類型一樣對內置運算符的支持. -這意味着, Celsius 和 Fahrenheit 類型的算術行爲和底層的 float64 類型一樣, 正如你所期望的. +底層數據類型決定了內部結構和表達方式,也決定是否可以像底層類型一樣對內置運算符的支持。這意味着,Celsius和Fahrenheit類型的算術運算行爲和底層的float64類型是一樣的,正如我們所期望的那樣。 ```Go fmt.Printf("%g\n", BoilingC-FreezingC) // "100" °C @@ -58,8 +56,7 @@ 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 +67,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) diff --git a/ch2/ch2-06-1.md b/ch2/ch2-06-1.md index d54dc53..a9463de 100644 --- a/ch2/ch2-06-1.md +++ b/ch2/ch2-06-1.md @@ -1,10 +1,10 @@ ### 2.6.1. 導入包 -在Go程序中, 每個包都是有一個全局唯一的導入路徑. 聲明中類似 "gopl.io/ch2/tempconv" 的字符串對應導入路徑. 語言的規范併沒有定義這些字符串的具體含義或包來自哪里, 它們是由工具來解釋. 當使用 go 工具箱時(第十章), 一個導入路徑代表一個目録中的一個或多個Go源文件. +在Go語言程序中,每個包都是有一個全局唯一的導入路徑。導入語句中類似"gopl.io/ch2/tempconv"的字符串對應包的導入路徑。Go語言的規范併沒有定義這些字符串的具體含義或包來自哪里,它們是由構建工具來解釋的。當使用Go語言自帶的go工具箱時(第十章),一個導入路徑代表一個目録中的一個或多個Go源文件。 -除了到導入路徑, 每個包還有一個包名, 包名一般是短小的(也不要求是是唯一的), 包名在包的聲明處指定. 按照慣例, 一個包的名字和包的導入路徑的最後一個字段相同, 例如 gopl.io/ch2/tempconv 包的名字是 tempconv. +除了包的導入路徑,每個包還有一個包名,包名一般是短小的名字(併不要求包名是唯一的),包名在包的聲明處指定。按照慣例,一個包的名字和包的導入路徑的最後一個字段相同,例如gopl.io/ch2/tempconv包的名字一般是tempconv。 -要使用 gopl.io/ch2/tempconv 包, 需要先導入: +要使用gopl.io/ch2/tempconv包,需要先導入: ```Go gopl.io/ch2/cf @@ -34,9 +34,9 @@ func main() { } ``` -導入聲明將導入的包綁定到一個短小的名字, 然後通過該名字就可以引用包中導齣的全部內容. 上面的導入聲明將允許我們以 tempconv.CToF 的方式來訪問 gopl.io/ch2/tempconv 包中的內容. 默認情況下, 導入的包綁定到 tempconv 名字, 但是我們也可以綁定到另一個名稱, 以避免名字衝突(§10.3). +導入語句將導入的包綁定到一個短小的名字,然後通過該短小的名字就可以引用包中導出的全部內容。上面的導入聲明將允許我們以tempconv.CToF的形式來訪問gopl.io/ch2/tempconv包中的內容。在默認情況下,導入的包綁定到tempconv名字(譯註:這包聲明語句指定的名字),但是我們也可以綁定到另一個名稱,以避免名字衝突(§10.3)。 -cf 程序將命令行輸入的一個溫度在 Celsius 和 Fahrenheit 之間轉換: +cf程序將命令行輸入的一個溫度在Celsius和Fahrenheit溫度單位之間轉換: ``` $ go build gopl.io/ch2/cf @@ -48,12 +48,11 @@ $ ./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源文件. +不過有更好的解決方案,我們可以使用golang.org/x/tools/cmd/goimports導入工具,它可以根據需要自動添加或刪除導入的包;許多編輯器都可以集成goimports工具,然後在保存文件的時候自動運行。類似的還有gofmt工具,可以用來格式化Go源文件。 -**練習 2.2:** 寫一個通用的單位轉換程序, 用類似 cf 程序的方式從命令行讀取參數, 如果缺省的話則是從標準輸入讀取參數, 然後做類似 Celsius 和 Fahrenheit 的轉換, -長度單位對應英尺和米, 重量單位對應磅和公斤 等等. +**練習 2.2:** 寫一個通用的單位轉換程序,用類似cf程序的方式從命令行讀取參數,如果缺省的話則是從標準輸入讀取參數,然後做類似Celsius和Fahrenheit的單位轉換,長度單位可以對應英尺和米,重量單位可以對應磅和公斤等。 diff --git a/ch2/ch2-06-2.md b/ch2/ch2-06-2.md index 064001d..b0e6ed4 100644 --- a/ch2/ch2-06-2.md +++ b/ch2/ch2-06-2.md @@ -1,6 +1,6 @@ ### 2.6.2. 包的初始化 -包的初始化首先是解決包級變量的依賴順序, 然後安裝包級變量聲明齣現的順序依次初始化: +包的初始化首先是解決包級變量的依賴順序,然後安照包級變量聲明出現的順序依次初始化: ```Go var a = b + c // a 第三個初始化, 爲 3 @@ -10,19 +10,19 @@ 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的bit個數,這樣的話在處理64bit寬度的數字時就沒有必要循環64次,隻需要8次査表就可以了。(這併不是最快的統計1bit數目的算法,但是它可以方便演示init函數的用法,併且演示了如果預生成輔助表格,這是編程中常用的技術)。 ```Go gopl.io/ch2/popcount @@ -50,18 +50,28 @@ func PopCount(x uint64) int { } ``` -要註意的是 init 函數中, range 循環隻使用了索引, 省略了沒有用到的值部分. -循環也可以這樣寫: +譯註:對於pc這類需要複雜處理的初始化,可以通過將初始化邏輯包裝爲一個匿名函數處理,像下面這樣: + +```Go +// pc[i] is the population count of i. +var pc [256]byte = func() (pc [256]byte) { + for i := range pc { + pc[i] = pc[i/2] + byte(i&1) + } +}() +``` + +要註意的是在init函數中,range循環隻使用了索引,省略了沒有用到的值部分。循環也可以這樣寫: ```Go for i, _ := range pc { ``` -我們在下一節和10.5節還將看到其它使用init函數的地方. +我們在下一節和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的最低的一個非零的bit位清零。使用這個算法重寫PopCount函數,然後比較性能。 diff --git a/ch2/ch2-06.md b/ch2/ch2-06.md index 305051d..74ef39d 100644 --- a/ch2/ch2-06.md +++ b/ch2/ch2-06.md @@ -1,16 +1,16 @@ ## 2.6. 包和文件 -Go語言中的包和其他語言的庫或模塊概念類似, 目的都是爲了支持模塊好, 封裝, 單獨編譯和代碼重用. 一個包的源代碼保存在一個或多個以.爲後綴名的文件中, 通常一個包所在目録路徑的後綴是包的導入路徑; 例如包 gopl.io/ch1/helloworld 對應的目録路徑是 $GOPATH/src/gopl.io/ch1/helloworld. +Go語言中的包和其他語言的庫或模塊的概念類似,目的都是爲了支持模塊化、封裝、單獨編譯和代碼重用。一個包的源代碼保存在一個或多個以.go爲文件後綴名的源文件中,通常一個包所在目録路徑的後綴是包的導入路徑;例如包gopl.io/ch1/helloworld對應的目録路徑是$GOPATH/src/gopl.io/ch1/helloworld。 -每個包作爲一個獨立的名字空間. 例如, 在 image 包中的 Decode 函數 和 unicode/utf16 包中的 Decode 函數是不同的. 要在外部包引用該函數, 必鬚顯式使用 image.Decode 或 utf16.Decode 訪問. +每個包都對應一個獨立的名字空間。例如,在image包中的Decode函數和在unicode/utf16包中的 Decode函數是不同的。要在外部引用該函數,必鬚顯式使用image.Decode或utf16.Decode形式訪問。 -包可以讓我們通過控製那些名字是外部可見的來隱藏信息. 在Go中, 一個簡單的規則是: 如果一個名字是大寫字母開頭的, 那麽該名字是導齣的. +包還可以讓我們通過控製哪些名字是外部可見的來隱藏內部實現信息。在Go語言中,一個簡單的規則是:如果一個名字是大寫字母開頭的,那麽該名字是導出的(譯註:因爲漢字不區分大小寫,因此漢字開頭的名字是沒有導出的)。 -爲了演示基本的用法, 假設我們的溫度轉換軟件已經很流行, 我們希望到Go社區也能使用這個包. 我們該如何做呢? +爲了演示包基本的用法,先假設我們的溫度轉換軟件已經很流行,我們希望到Go語言社區也能使用這個包。我們該如何做呢? -讓我們創建一個名爲 gopl.io/ch2/tempconv 的包, 是前面例子的一個改進版本. (我們約定我們的例子都是以章節順序來編號的, 這樣的路徑更容易閲讀.) 包代碼存儲在兩個文件, 用來演示如何在一個文件聲明然後在其他的文件訪問; 在現實中, 這樣小的包一般值需要一個文件. +讓我們創建一個名爲gopl.io/ch2/tempconv的包,這是前面例子的一個改進版本。(我們約定我們的例子都是以章節順序來編號的,這樣的路徑更容易閲讀)包代碼存儲在兩個源文件中,用來演示如何在一個源文件聲明然後在其他的源文件訪問;雖然在現實中,這樣小的包一般隻需要一個文件。 -我們把變量的聲明, 對應的常量, 還有方法都放到 tempconv.go 文件: +我們把變量的聲明、對應的常量,還有方法都放到tempconv.go源文件中: ```Go gopl.io/ch2/tempconv @@ -32,7 +32,7 @@ func (c Celsius) String() string { return fmt.Sprintf("%g°C", c) } func (f Fahrenheit) String() string { return fmt.Sprintf("%g°F", f) } ``` -轉換函數放在 conv.go 文件中: +轉換函數則放在另一個conv.go源文件中: ```Go package tempconv @@ -44,26 +44,23 @@ 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, 然後就可以使用下面的代碼轉換了: +要將攝氏溫度轉換爲華氏溫度,需要先用import語句導入gopl.io/ch2/tempconv包,然後就可以使用下面的代碼進行轉換了: ```Go fmt.Println(tempconv.CToF(tempconv.BoilingC)) // "212°F" ``` -在每個文件的包聲明前僅跟着的註釋是包註釋(§10.7.4). 通常, 第一句應該先是包的功能概要. -一個包通常隻有一個文件有包註釋. 如果包註釋很大, 通常會放到一個獨立的 doc.go 文件中. +在每個源文件的包聲明前僅跟着的註釋是包註釋(§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" %} diff --git a/ch2/ch2-07.md b/ch2/ch2-07.md index d666c6f..8c255ea 100644 --- a/ch2/ch2-07.md +++ b/ch2/ch2-07.md @@ -1,18 +1,18 @@ ## 2.7. 作用域 -一個聲明語句將程序中的實體和一個名字關聯, 比如一個函數或一個變量. 聲明的作用域是指源代碼中可以有效使用這個名字的范圍. +一個聲明語句將程序中的實體和一個名字關聯,比如一個函數或一個變量。聲明語句的作用域是指源代碼中可以有效使用這個名字的范圍。 -不要將作用域和生命週期混爲一談. 聲明的作用域對應的是一個源代碼的文本區域; 它是一個編譯時的屬性. 一個變量的生命週期是程序運行時變量存在的有效時間段, 在此時間區域內存它可以被程序的其他部分引用. 是一個運行時的概念. +不要將作用域和生命週期混爲一談。聲明語句的作用域對應的是一個源代碼的文本區域;它是一個編譯時的屬性。一個變量的生命週期是指程序運行時變量存在的有效時間段,在此時間區域內它可以被程序的其他部分引用;是一個運行時的概念。 -語法塊是由花括弧所包含的一繫列語句, 就像函數體或循環體那樣. 語法塊內部聲明的名字是無法被外部語法塊訪問的. 語法決定了內部聲明的名字的作用域范圍. 我們可以這樣理解, 語法塊可以包含其他類似組批量聲明等沒有用花括弧包含的代碼, 我們稱之爲詞滙塊. 有一個語法決爲整個源代碼, 稱爲全局塊; 然後是每個包的語法決; 每個 for, if 和 switch 語句的語法決; 每個 switch 或 select 分支的 語法決; 當然也包含顯示編寫的語法塊(花括弧包含). +語法塊是由花括弧所包含的一繫列語句,就像函數體或循環體花括弧對應的語法塊那樣。語法塊內部聲明的名字是無法被外部語法塊訪問的。語法決定了內部聲明的名字的作用域范圍。我們可以這樣理解,語法塊可以包含其他類似組批量聲明等沒有用花括弧包含的代碼,我們稱之爲語法塊。有一個語法塊爲整個源代碼,稱爲全局語法塊;然後是每個包的包語法決;每個for、if和switch語句的語法決;每個switch或select的分支也有獨立的語法決;當然也包括顯式書寫的語法塊(花括弧包含的語句)。 -聲明的詞法域決定了作用域范圍是大還是小. 內置的類型, 函數和常量, 比如 int, len 和 true 等是在全局作用域的, 可以在整個程序中直接使用. 任何在在函數外部(也就是包級作用域)聲明的名字可以在同一個包的任何Go文件訪問. 導入的包, 例如 tempconv 導入的 fmt 包, 則是對應文件級的作用域, 因此隻能在當前的文件中訪問 fmt 包, 當前包的其它文件無法訪問當前文件導入的包. 還有許多聲明, 比如 tempconv.CToF 函數中的變量 c, 則是局部作用域的, 它隻能在函數內部(甚至隻能是某些部分)訪問. +聲明語句對應的詞法域決定了作用域范圍的大小。對於內置的類型、函數和常量,比如int、len和true等是在全局作用域的,因此可以在整個程序中直接使用。任何在在函數外部(也就是包級語法域)聲明的名字可以在同一個包的任何源文件中訪問的。對於導入的包,例如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隱式的初始化詞法域,一個在for循環體詞法域;隻有兩個塊是顯式創建的: ```Go func main() { @@ -58,7 +58,7 @@ func main() { } ``` -和彿如循環類似, if和switch語句也會在條件部分創建隱式塊, 還有它們對應的執行體塊. 下面的 if-else 測試鏈演示的 x 和 y 的作用域范圍: +和for循環類似,if和switch語句也會在條件部分創建隱式詞法域,還有它們對應的執行體詞法域。下面的if-else測試鏈演示了x和y的有效作用域范圍: ```Go if x := f(); x == 0 { @@ -71,11 +71,11 @@ 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語句的每個分支也有類似的詞法域規則:條件部分爲一個隱式詞法域,然後每個是每個分支的詞法域。 -在包級别, 聲明的順序併不會影響作用域范圍, 因此一個先聲明的可以引用它自身或者是引用後面的一個聲明, 這可以讓我們定義一些相互嵌套或遞歸的類型或函數. 但是如果一個變量或常量遞歸引用了自身, 則會産生編譯錯誤. +在包級别,聲明的順序併不會影響作用域范圍,因此一個先聲明的可以引用它自身或者是引用後面的一個聲明,這可以讓我們定義一些相互嵌套或遞歸的類型或函數。但是如果一個變量或常量遞歸引用了自身,則會産生編譯錯誤。 -在這個程序中: +在這個程序中: ```Go if f, err := os.Open(fname); err != nil { // compile error: unused: f @@ -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) @@ -98,7 +98,7 @@ f.ReadByte() f.Close() ``` -你可能會考慮通過將ReadByte和Close移動到if的else塊來解決這個問題: +你可能會考慮通過將ReadByte和Close移動到if的else塊來解決這個問題: ```Go if f, err := os.Open(fname); err != nil { @@ -110,9 +110,9 @@ if f, err := os.Open(fname); err != nil { } ``` -但這不是Go推薦的做法, Go的習慣是在if中處理錯誤然後直接返迴, 這樣可以確保正常成功執行的語句不需要代碼縮進. +但這不是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 @@ -141,9 +141,9 @@ func init() { } ``` -全局的cwd變量依然是沒有被正確初始化的, 而且看似正常的日誌輸齣更是這個BUG更加隱晦. +全局的cwd變量依然是沒有被正確初始化的,而且看似正常的日誌輸出更是讓這個BUG更加隱晦。 -有許多方式可以避免齣現類似潛在的問題. 最直接的是通過單獨聲明err變量, 來避免使用 `:=` 的簡短聲明方式: +有許多方式可以避免出現類似潛在的問題。最直接的方法是通過單獨聲明err變量,來避免使用`:=`的簡短聲明方式: ```Go var cwd string @@ -157,6 +157,5 @@ func init() { } ``` -我們已經看到包, 文件, 聲明和語句如何來表達一個程序結構. 在下面的兩個章節, 我們將探討數據的結構. +我們已經看到包、文件、聲明和語句如何來表達一個程序結構。在下面的兩個章節,我們將探討數據的結構。 -**譯註: 本章的詞法域和作用域概念有些混淆, 需要重譯一遍.** diff --git a/ch2/ch2.md b/ch2/ch2.md index 35c2573..32d4273 100644 --- a/ch2/ch2.md +++ b/ch2/ch2.md @@ -1,5 +1,5 @@ # 第2章 程序結構 -Go語言和任何其他語言一樣, 一個大的程序是有很多小的基礎構件組成的. 變量保存值. 簡單的加法和減法運算被組合成較大的表達式. 基礎類型被聚合爲數組或結構體. 然後使用if和for之類的控製語句來組織和控製表達式的執行順序. 然後多個語句被組織到函數中, 以便代碼的隔離和複用. 函數以源文件和包的方式組織. +Go語言和其他編程語言一樣,一個大的程序是由很多小的基礎構件組成的。變量保存值,簡單的加法和減法運算被組合成較複雜的表達式。基礎類型被聚合爲數組或結構體等更複雜的數據結構。然後使用if和for之類的控製語句來組織和控製表達式的執行流程。然後多個語句被組織到一個個函數中,以便代碼的隔離和複用。函數以源文件和包的方式被組織。 -我們已經在前面的章節的例子中看到了大部分的例子. 在本章中, 我們將深入討論Go程序的基礎結構的一些細節. 每個示例程序都是刻意寫的簡單, 這樣我們可以減少被複雜的算法和數據結構所榦擾, 從而專註於語言本身的學習. +我們已經在前面章節的例子中看到了很多例子。在本章中,我們將深入討論Go程序基礎結構方面的一些細節。每個示例程序都是刻意寫的簡單,這樣我們可以減少複雜的算法或數據結構等不相關的問題帶來的榦擾,從而可以專註於Go語言本身的學習。 diff --git a/ch3/ch3-01.md b/ch3/ch3-01.md index a8b7175..f730d77 100644 --- a/ch3/ch3-01.md +++ b/ch3/ch3-01.md @@ -1,20 +1,20 @@ ## 3.1. 整型 -Go語言的數值類型包括幾種不同大小的整形數, 浮點數, 和複數. 每種數值類型都決定了對應的大小范圍和是否有正負符號. 讓我們先從整形數類型開始介紹. +Go語言的數值類型包括幾種不同大小的整形數、浮點數和複數。每種數值類型都決定了對應的大小范圍和是否支持正負符號。讓我們先從整形數類型開始介紹。 -Go同時提供了有符號和無符號的整數運算. 這里有四種int8, int16, int32 和 int64截然不同大小的有符號整形數類型, 分别對應 8, 16, 32, 64 bit 大小的有符號整形數, 與此對應的是 uint8, uint16, uint32, 和 uint64 四種無符號整形數類型. +Go語言同時提供了有符號和無符號類型的整數運算。這里有int8、int16、int32和int64四種截然不同大小的有符號整形數類型,分别對應8、16、32、64bit大小的有符號整形數,與此對應的是uint8、uint16、uint32和uint64四種無符號整形數類型。 -這里還有兩種對應特定平颱最天然或最有效率的大小有符號和無符號整數int和uint; 其中int是應用最廣泛的數值類型. 這兩種類型都有同樣的大小, 32 或 64 bit, 但是我們不能對此做任何的假設; 因爲不同的編譯器在相同的硬件平颱上可能産生不同的大小. +這里還有兩種一般對應特定CPU平台機器字大小的有符號和無符號整數int和uint;其中int是應用最廣泛的數值類型。這兩種類型都有同樣的大小,32或64bit,但是我們不能對此做任何的假設;因爲不同的編譯器卽使在相同的硬件平台上可能産生不同的大小。 -字符rune類型是和int32等價的類型, 通常用於表示一個Unicode碼點. 這兩個名稱可以互換使用. 同樣byte也是uint8類型的等價類型, byte類型用於強調數值是一個原始的數據而不是一個小的整數. +Unicode字符rune類型是和int32等價的類型,通常用於表示一個Unicode碼點。這兩個名稱可以互換使用。同樣byte也是uint8類型的等價類型,byte類型一般用於強調數值是一個原始的數據而不是一個小的整數。 -最好, 還有一個無符號的整數類型 uintptr, 沒有指定具體的bit大小但是足以容納指針. uintptr 類型隻有在底層編程是纔需要, 特别是Go語言和C函數庫或操作繫統相交互的地方. 我們將在第十三章的 unsafe 包相關部分看到類似的例子. +最後,還有一種無符號的整數類型uintptr,沒有指定具體的bit大小但是足以容納指針。uintptr類型隻有在底層編程是才需要,特别是Go語言和C語言函數庫或操作繫統接口相交互的地方。我們將在第十三章的unsafe包相關部分看到類似的例子。 -不管它們的大小, int, uint, 和 uintptr 是不同類型大小的兄弟類型. 其中 int 和 int32 也是不同的類型, 卽使int的大小也是32bit, 在需要將int當作int32類型的地方需要一個顯式的類型轉換, 反之亦然. +不管它們的具體大小,int、uint和uintptr是不同類型的兄弟類型。其中int和int32也是不同的類型,卽使int的大小也是32bit,在需要將int當作int32類型的地方需要一個顯式的類型轉換操作,反之亦然。 -有符號數采用2的補碼形式表示, 也就是最高位用作符號位, 一個nbit的有符號數的值域是 `-2^(n-1)` 到 `(2^(n-1)) - 1`. 無符號整數的所有bit位都用於表示非負數, 值域是 0 到 `(2^n) - 1`. 例如, int8 的值域是 -128 到 127, 而 uint8 的值域是 0 到 255. +其中有符號整數采用2的補碼形式表示,也就是最高bit位用作表示符號位,一個n-bit的有符號數的值域是從$$-2^{n-1}$$到$$2^{n-1}-1$$。無符號整數的所有bit位都用於表示非負數,值域是0到$$2^n-1$$。例如,int8類型整數的值域是從-128到127,而uint8類型整數的值域是從0到255。 -下面是Go中關於算術, 邏輯和比較的二元運算符按照先級遞減的順序的列表: +下面是Go語言中關於算術運算、邏輯運算和比較運算的二元運算符,它們按照先級遞減的順序的排列: ``` * / % << >> & &^ @@ -24,14 +24,13 @@ Go同時提供了有符號和無符號的整數運算. 這里有四種int8, int1 || ``` -二元運算符有五種優先級. 在同一優先級, 使用左優先結合律, 使用括號可以明確優先順序, 括號也可以用於提陞優先級, 例如 `mask & (1 << 28)`. +二元運算符有五種優先級。在同一個優先級,使用左優先結合規則,但是使用括號可以明確優先順序,使用括號也可以用於提陞優先級,例如`mask & (1 << 28)`。 -對於上表中前兩行的運算符, 例如 + 有一個相應的賦值結合運算符 +=, 可以用於簡化賦值語句. +對於上表中前兩行的運算符,例如+運算符還有一個與賦值相結合的對應運算符+=,可以用於簡化賦值語句。 -整數的算術運算符 +, -, *, 和 / 可以適用與整數, 浮點數和複數, 但是取模運算符 % 僅用於整數. 不同編程語言間, % 取模運算的行爲併不相同. 在Go語言中, % 取模運算符的符號和被取模數的符號總是一致的, 因此 `-5%3` 和 `-5%-3` 結果都是 -2.除法運算符 `/` 的行爲依賴於操作數是否爲整數, 因此 `5.0/4.0` 的結果是 1.25, 但是 5/4 的結果是 1, 因此整數除法會向着0方向截斷餘數. +整數的算術運算符+、-、`*`和`/`可以適用與於整數、浮點數和複數,但是取模運算符%僅用於整數間的運算。對於不同編程語言,%取模運算的行爲可能併不相同。在Go語言中,%取模運算符的符號和被取模數的符號總是一致的,因此`-5%3`和`-5%-3`結果都是-2。除法運算符`/`的行爲則依賴於操作數是否爲全爲整數,比如`5.0/4.0`的結果是1.25,但是5/4的結果是1,因爲整數除法會向着0方向截斷餘數。 - -如果一個算術運算的結果, 不管是有符號或者是無符號的, 如果需要更多的bit位纔能表示, 就説明是溢齣了. 超齣的高位的bit位部分將被丟棄. 如果原始的數值是有符號類型, 那麽最終結果可能是負的, 如果最左邊的bit爲是1的話, 例如int8的例子: +如果一個算術運算的結果,不管是有符號或者是無符號的,如果需要更多的bit位才能正確表示的話,就説明計算結果是溢出了。超出的高位的bit位部分將被丟棄。如果原始的數值是有符號類型,而且最左邊的bit爲是1的話,那麽最終結果可能是負的,例如int8的例子: ```Go var u uint8 = 255 @@ -41,7 +40,7 @@ var i int8 = 127 fmt.Println(i, i+1, i*i) // "127 -128 1" ``` -兩個相同的整數類型可以使用下面的二元比較運算符進行比較; 比較表達式的結果是布爾類型. +兩個相同的整數類型可以使用下面的二元比較運算符進行比較;比較表達式的結果是布爾類型。 ``` == equal to @@ -52,18 +51,18 @@ fmt.Println(i, i+1, i*i) // "127 -128 1" >= greater than or equal to ``` -事實上, 布爾型, 數字類型 和 字符串 等基本類型都是可比較的, 也就是説兩個相同類型的值可以用 == 和 != 進行比較. 此外, 整數, 浮點數和字符串可以根據比較結果排序. 許多其他類型的值是不可比較, 因此也就是不可排序的. 對於我們遇到的每種類型, 我們需要保證規則是類似的. +事實上,布爾型、數字類型和字符串等基本類型都是可比較的,也就是説兩個相同類型的值可以用==和!=進行比較。此外,整數、浮點數和字符串可以根據比較結果排序。許多其它類型的值可能是不可比較的,因此也就可能是不可排序的。對於我們遇到的每種類型,我們需要保證規則的一致性。 -這里是一元的加法和減法運算符: +這里是一元的加法和減法運算符: ``` + 一元加法 (無效果) - 負數 ``` -對於整數, +x 是 0+x 的簡寫, -x 是 0-x 的簡寫; 對於浮點數和複數, +x 就是 x, -x 則是 x 的負數. +對於整數,+x是0+x的簡寫,-x則是0-x的簡寫;對於浮點數和複數,+x就是x,-x則是x 的負數。 -Go語言還提供了以下的bit位操作運算符, 前面4個操作運算符併不區分是有符號還是無符號數: +Go語言還提供了以下的bit位操作運算符,前面4個操作運算符併不區分是有符號還是無符號數: ``` & 位運算 AND @@ -74,11 +73,9 @@ Go語言還提供了以下的bit位操作運算符, 前面4個操作運算符併 >> 右移 ``` -位操作運算符 `^` 作爲二元運算符時是按位異或(XOR), 當用作一元運算符時表示按位取反; 也就是説, 它返迴一個每個bit位都取反的數. 位操作運算符 `&^` 用於按位置零(AND NOT): 表達式 `z = x &^ y` 結果z的bit位1, 如果對應y中bit位爲1, 否則對應的bit位等於x相應的bit位的值. +位操作運算符`^`作爲二元運算符時是按位異或(XOR),當用作一元運算符時表示按位取反;也就是説,它返迴一個每個bit位都取反的數。位操作運算符`&^`用於按位置零(AND NOT):表達式`z = x &^ y`結果z的bit位爲0,如果對應y中bit位爲1的話,否則對應的bit位等於x相應的bit位的值。 - - -下面的代碼演示了如何使用位操作解釋uint8類型值的8個獨立的bit位. 它使用了 Printf 函數的 %b 參數打印二進製格式的數字; 其中 %08b 中08表示打印至少8個數字, 不足的前綴用0填充. +下面的代碼演示了如何使用位操作解釋uint8類型值的8個獨立的bit位。它使用了Printf函數的%b參數打印二進製格式的數字;其中%08b中08表示打印至少8個字符寬度,不足的前綴部分用0填充。 ```Go var x uint8 = 1<<1 | 1<<5 @@ -102,13 +99,13 @@ fmt.Printf("%08b\n", x<<1) // "01000100", the set {2, 6} fmt.Printf("%08b\n", x>>1) // "00010001", the set {0, 4} ``` -(6.5節給齣了一個可以遠大於一個字節的整數集的實現.) +(6.5節給出了一個可以遠大於一個字節的整數集的實現。) -在 x<>n 移位運算中, 決定了移位操作bit數部分必鬚是無符號數; 被操作的 x 數可以是有符號或無符號數. 算術上, 一個 x<>n 右移運算等價於除以 2^n. +在`x<>n`移位運算中,決定了移位操作bit數部分必鬚是無符號數;被操作的x數可以是有符號或無符號數。算術上,一個`x<>n`右移運算等價於除以$$2^n$$。 -左移運算用零填充右邊空缺的bit位, 無符號數的右移運算也是用0填充左邊空缺的bit位, 但是有符號數的右移運算會用符號位的值填充左邊空缺的bit位. 因爲這個原因, 最好用無符號運算, 這樣你可以將整數完全當作一個bit位模式處理. +左移運算用零填充右邊空缺的bit位,無符號數的右移運算也是用0填充左邊空缺的bit位,但是有符號數的右移運算會用符號位的值填充左邊空缺的bit位。因爲這個原因,最好用無符號運算,這樣你可以將整數完全當作一個bit位模式處理。 -盡管Go提供了無符號數和運算, 卽使數值本身不可能齣現負數我們還是傾向於使用有符號的int類型, 就是數組的長度那樣, 雖然使用 uint 似乎是一個更合理的選擇. 事實上, 內置的 len 函數返迴一個有符號的int, 我們可以像下面這個逆序循環那樣處理. +盡管Go語言提供了無符號數和運算,卽使數值本身不可能出現負數我們還是傾向於使用有符號的int類型,就像數組的長度那樣,雖然使用uint無符號類型似乎是一個更合理的選擇。事實上,內置的len函數返迴一個有符號的int,我們可以像下面例子那樣處理逆序循環。 ```Go medals := []string{"gold", "silver", "bronze"} @@ -117,13 +114,13 @@ for i := len(medals) - 1; i >= 0; i-- { } ``` -另一個選擇將是災難性的. 如果 len 返迴一個無符號數, 那麽 i 也將是無符號的 uint, 然後條件 i >= 0 則永遠爲眞. 在三次迭代之後, 也就是 i == 0 時, i-- 語句將不會産生 -1, 而是變成一個uint的最大值(可能是 2^64 - 1), 然後 medals[i] 表達式將發生運行時 panic 異常(§5.9), 也就是試圖訪問一個切片范圍以外的元素. +另一個選擇對於上面的例子來説將是災難性的。如果len函數返迴一個無符號數,那麽i也將是無符號的uint類型,然後條件`i >= 0`則永遠爲眞。在三次迭代之後,也就是`i == 0`時,i--語句將不會産生-1,而是變成一個uint類型的最大值(可能是$$2^64-1$$),然後medals[i]表達式將發生運行時panic異常(§5.9),也就是試圖訪問一個slice范圍以外的元素。 -齣於這個原因, 無符號數往往隻有在位運算或其它特殊的運算常見纔會使用, 就像 bit 集合, 分形二進製文件格式, 或者是哈希和加密操作等. 它們通常併不用於僅僅是表達非負數量的場合. +出於這個原因,無符號數往往隻有在位運算或其它特殊的運算場景才會使用,就像bit集合、分析二進製文件格式或者是哈希和加密操作等。它們通常併不用於僅僅是表達非負數量的場合。 -一般來説, 需要一個顯式的轉換將一個值從一種類型轉化位另一種類型, 併且算術和邏輯運算的二元操作中必鬚是相同的類型. 雖然這偶爾會導致很長的表達式, 但是它消除了所有的類型相關的問題, 也使得程序容易理解. +一般來説,需要一個顯式的轉換將一個值從一種類型轉化位另一種類型,併且算術和邏輯運算的二元操作中必鬚是相同的類型。雖然這偶爾會導致需要很長的表達式,但是它消除了所有和類型相關的問題,而且也使得程序容易理解。 -從其他類似場景下, 考慮下面這個代碼: +在很多場景,會遇到類似下面的代碼通用的錯誤: ```Go var apples int32 = 1 @@ -131,19 +128,19 @@ var oranges int16 = 2 var compote int = apples + oranges // compile error ``` -當嚐試編譯這三個語句時, 將産生一個錯誤信息: +當嚐試編譯這三個語句時,將産生一個錯誤信息: ``` invalid operation: apples + oranges (mismatched types int32 and int16) ``` -這種類型不匹配的問題可以有幾種不同的方法脩複, 最常見方法是將它們都顯式轉型位一個常見類型: +這種類型不匹配的問題可以有幾種不同的方法脩複,最常見方法是將它們都顯式轉型爲一個常見類型: ```Go var compote = int(apples) + int(oranges) ``` -如2.5節所述, 對於每種類型T, 類型轉換操作T(x)將x轉換位T類型, 如果轉換允許的話. 許多 整形數之間的相互轉換併不會改變數值; 它們隻是告訴編譯器如何解釋這個值. 但是對於將一個大尺寸的整數類型轉位一個小尺寸的整數類型, 或者是將一個浮點數轉位整數, 可能會改變數值或丟失精度: +如2.5節所述,對於每種類型T,如果轉換允許的話,類型轉換操作T(x)將x轉換爲T類型。許多整形數之間的相互轉換併不會改變數值;它們隻是告訴編譯器如何解釋這個值。但是對於將一個大尺寸的整數類型轉爲一個小尺寸的整數類型,或者是將一個浮點數轉爲整數,可能會改變數值或丟失精度: ```Go f := 3.141 // a float64 @@ -153,16 +150,16 @@ f = 1.99 fmt.Println(int(f)) // "1" ``` -浮點數到整數的轉換將丟失任何小數部分, 向數軸零方向截斷. 你應該避免操作目標類型表示范圍的數值類型轉換, 因爲截斷的行爲依賴於具體的實現: +浮點數到整數的轉換將丟失任何小數部分,然後向數軸零方向截斷。你應該避免對可能會超出目標類型表示范圍的數值類型轉換,因爲截斷的行爲可能依賴於具體的實現: ```Go f := 1e100 // a float64 i := int(f) // 結果依賴於具體實現 ``` -任何大小的整數字面值都可以用以0開始的八進製格式書寫, 例如 0666, 或用以0x或0X開頭的十六進製格式書寫, 例如 0xdeadbeef. 十六進製數字可以用大寫或小寫字母. 如今八進製數據通常用於POSIX操作繫統上的文件訪問權限標誌, 十六進製數字則更強調數字值的bit位模式. +任何大小的整數字面值都可以用以0開始的八進製格式書寫,例如0666;或用以0x或0X開頭的十六進製格式書寫,例如0xdeadbeef。十六進製數字可以用大寫或小寫字母。如今八進製數據通常用於POSIX操作繫統上的文件訪問權限標誌,十六進製數字則更強調數字值的bit位模式。 -當使用 fmt 包打印一個數值時, 我們可以用 %d, %o, 或 %x 控製輸齣的進製格式, 就像下面的例子: +當使用fmt包打印一個數值時,我們可以用%d、%o或%x參數控製輸出的進製格式,就像下面的例子: ```Go o := 0666 @@ -173,18 +170,18 @@ fmt.Printf("%d %[1]x %#[1]x %#[1]X\n", x) // 3735928559 deadbeef 0xdeadbeef 0XDEADBEEF ``` -請註意 fmt 的兩個使用技巧. 通常 Printf 格式化字符串包含多個 % 參數時將對應相同數量的額外操作數, 但是 % 之後的 `[1]` 副詞告訴Printf函數再次使用第一個操作數. 第二, % 後的 `#` 副詞告訴 Printf 在用 %o, %x 或 %X 輸齣時生成 0, 0x 或 0X前綴. +請註意fmt的兩個使用技巧。通常Printf格式化字符串包含多個%參數時將會包含對應相同數量的額外操作數,但是%之後的`[1]`副詞告訴Printf函數再次使用第一個操作數。第二,%後的`#`副詞告訴Printf在用%o、%x或%X輸出時生成0、0x或0X前綴。 -字符面值通過一對單引號直接包含對應字符. 最簡單的例子是 ASCII 中類似 'a' 字符面值, 但是我們可以通過轉義的數值來表示任意的Unicode碼點對應的字符, 馬上將會看到例子. +字符面值通過一對單引號直接包含對應字符。最簡單的例子是ASCII中類似'a'寫法的字符面值,但是我們也可以通過轉義的數值來表示任意的Unicode碼點對應的字符,馬上將會看到這樣的例子。 -字符使用 `%c` 參數打印, 或者是 `%q` 參數打印帶單引號的字符: +字符使用`%c`參數打印,或者是用`%q`參數打印帶單引號的字符: ```Go ascii := 'a' -unicode := '國' +unicode := '国' newline := '\n' fmt.Printf("%d %[1]c %[1]q\n", ascii) // "97 a 'a'" -fmt.Printf("%d %[1]c %[1]q\n", unicode) // "22269 國 '國'" +fmt.Printf("%d %[1]c %[1]q\n", unicode) // "22269 国 '国'" fmt.Printf("%d %[1]q\n", newline) // "10 '\n'" ``` diff --git a/ch3/ch3-02.md b/ch3/ch3-02.md index eb32cdb..40bea03 100644 --- a/ch3/ch3-02.md +++ b/ch3/ch3-02.md @@ -1,31 +1,30 @@ ## 3.2. 浮點數 -Go語言提供了兩種精度的浮點數, float32 和 float64. 它們的算術規范由 IEEE754 國際標準定義, 該浮點數規范被所有現代的CPU支持. +Go語言提供了兩種精度的浮點數,float32和float64。它們的算術規范由IEEE754浮點數国際標準定義,該浮點數規范被所有現代的CPU支持。 -這些數值類型的范圍可以從很微小到很鉅大. 浮點數的范圍極限值可以在 matn 包找到. 常量 math.MaxFloat32 表示 float32 能表示的最大數值, 大約是 3.4e38, 對應的 math.MaxFloat64 常量大約是 1.8e308. 它們能表示的最小值近似分别是1.4e-45 和 4.9e-324. +這些浮點數類型的取值范圍可以從很微小到很鉅大。浮點數的范圍極限值可以在math包找到。常量math.MaxFloat32表示float32能表示的最大數值,大約是 3.4e38;對應的math.MaxFloat64常量大約是1.8e308。它們分别能表示的最小值近似爲1.4e-45和4.9e-324。 -一個 float32 類型的浮點數可以提供大約6個十進製數的精度, 而 float64 則可以提供約 15個十進製數精度; 通常應該優先使用 float64 類型, 因爲 float32 類型的纍計計算誤差很容易擴散, 併且 float32 能精度表示的正整數併不是很大: +一個float32類型的浮點數可以提供大約6個十進製數的精度,而float64則可以提供約15個十進製數的精度;通常應該優先使用float64類型,因爲float32類型的纍計計算誤差很容易擴散,併且float32能精確表示的正整數併不是很大(譯註:因爲float32的有效bit位隻有23個,其它的bit位用於指數和符號;當整數大於23bit能表達的范圍時,float32的表示將出現誤差): ```Go var f float32 = 16777216 // 1 << 24 fmt.Println(f == f+1) // "true"! ``` -浮點數的字面值可以直接寫小數部分, 想這樣: +浮點數的字面值可以直接寫小數部分,像這樣: ```Go const e = 2.71828 // (approximately) ``` -小數點前面或後面的數字都可能被省略(例如 .707 或 1.). 很小或很大的數最好用科學計數法書寫, 通過e或E來指定指數部分: +小數點前面或後面的數字都可能被省略(例如.707或1.)。很小或很大的數最好用科學計數法書寫,通過e或E來指定指數部分: ```Go -const Avogadro = 6.02214129e23 -const Planck = 6.62606957e-34 +const Avogadro = 6.02214129e23 // 阿伏伽德羅常數 +const Planck = 6.62606957e-34 // 普朗剋常數 ``` - -用 Printf 函數的 %g 參數打印浮點數, 將采用緊湊的表示形式打印, 併提供足夠的精度, 但是對應表格的數據, 使用 %e (帶指數) 或 %f 的形式打印可能更合適. 所有的這三個打印形式都可以指定打印的寬度和控製打印精度. +用Printf函數的%g參數打印浮點數,將采用更緊湊的表示形式打印,併提供足夠的精度,但是對應表格的數據,使用%e(帶指數)或%f的形式打印可能更合適。所有的這三個打印形式都可以指定打印的寬度和控製打印精度。 ```Go for x := 0; x < 8; x++ { @@ -33,7 +32,7 @@ for x := 0; x < 8; x++ { } ``` -上面代碼打印e的冪, 打印精度是小數點後三個小數精度和8個字符寬度: +上面代碼打印e的冪,打印精度是小數點後三個小數精度和8個字符寬度: ``` x = 0 e^x = 1.000 @@ -46,21 +45,21 @@ x = 6 e^x = 403.429 x = 7 e^x = 1096.633 ``` -math 包中除了提供大量常用的數學函數外, 還提供了IEEE754標準中特殊數值的創建和測試: 正無窮大和負無窮大, 分别用於表示太大溢齣的數字和除零的結果; 還有 NaN 非數, 一般用於表示無效的除法操作結果 0/0 或 Sqrt(-1). +math包中除了提供大量常用的數學函數外,還提供了IEEE754浮點數標準中定義的特殊值的創建和測試:正無窮大和負無窮大,分别用於表示太大溢出的數字和除零的結果;還有NaN非數,一般用於表示無效的除法操作結果0/0或Sqrt(-1). ```Go var z float64 fmt.Println(z, -z, 1/z, -1/z, z/z) // "0 -0 +Inf -Inf NaN" ``` -函數 math.IsNaN 用於測試一個數是否是非數 NaN, math.NaN 則返迴非數對應的值. 雖然可以用 math.NaN 來表示一個非法的結果, 但是測試一個結果是否是非數 NaN 則是充滿風險, 因爲 NaN 和任何數都是不相等的: +函數math.IsNaN用於測試一個數是否是非數NaN,math.NaN則返迴非數對應的值。雖然可以用math.NaN來表示一個非法的結果,但是測試一個結果是否是非數NaN則是充滿風險的,因爲NaN和任何數都是不相等的(譯註:在浮點數中,NaN、正無窮大和負無窮大都不是唯一的,每個都有非常多種的bit模式表示): ```Go nan := math.NaN() fmt.Println(nan == nan, nan < nan, nan > nan) // "false false false" ``` -如果一個函數返迴的浮點數結果可能失敗, 最好的做法是用單獨的標誌報告失敗, 像這樣: +如果一個函數返迴的浮點數結果可能失敗,最好的做法是用單獨的標誌報告失敗,像這樣: ```Go func compute() (value float64, ok bool) { @@ -72,7 +71,7 @@ func compute() (value float64, ok bool) { } ``` -接下來的程序演示了浮點計算圖形. 它是帶有兩個參數的 z = f(x, y) 函數的三維形式, 使用了可縮放矢量圖形(SVG)格式輸齣, 一個用於矢量線繪製的XML標準. 圖3.1顯示了 sin(r)/r 函數的輸齣圖形, 其中 r 是 sqrt(x*x+y*y). +接下來的程序演示了通過浮點計算生成的圖形。它是帶有兩個參數的z = f(x, y)函數的三維形式,使用了可縮放矢量圖形(SVG)格式輸出,SVG是一個用於矢量線繪製的XML標準。圖3.1顯示了sin(r)/r函數的輸出圖形,其中r是sqrt(x*x+y*y)。 ![](../images/ch3-01.png) @@ -135,32 +134,32 @@ func f(x, y float64) float64 { } ``` -要註意的是 corner 返迴了兩個結果, 對應 corner 的坐標參數. +要註意的是corner函數返迴了兩個結果,分别對應每個網格頂點的坐標參數。 -要解釋程序是如何工作的需要了解基本的幾何知識, 但是我們可以跳過幾何原理, 因爲程序的重點是演示浮點運算. 程序的本質是三個不同的坐標繫中映射關繫, 如圖3.2所示. 第一個是 100x100 的二維網格, 對應整數整數坐標(i,j), 從遠處的 (0, 0) 位置開始. 我們從遠處像前面繪製, 因此遠處先繪製的多邊形有可能被前面後繪製的多邊形覆蓋. +要解釋這個程序是如何工作的需要一些基本的幾何學知識,但是我們可以跳過幾何學原理,因爲程序的重點是演示浮點數運算。程序的本質是三個不同的坐標繫中映射關繫,如圖3.2所示。第一個是100x100的二維網格,對應整數整數坐標(i,j),從遠處的(0, 0)位置開始。我們從遠處向前面繪製,因此遠處先繪製的多邊形有可能被前面後繪製的多邊形覆蓋。 -第二個坐標繫是一個三維的網格浮點坐標(x,y,z), 其中x和y是i和j的線性函數, 通過平移轉換位center的中心, 然後用xyrange繫數縮放. 高度z是函數f(x,y)的值. +第二個坐標繫是一個三維的網格浮點坐標(x,y,z),其中x和y是i和j的線性函數,通過平移轉換位網格單元的中心,然後用xyrange繫數縮放。高度z是函數f(x,y)的值。 -第三個坐標繫是一個二維的畵布, 起點(0,0)在左上角. 畵布中點的坐標用(sx, sy)表示. 我們使用等角投影將三維點 +第三個坐標繫是一個二維的畵布,起點(0,0)在左上角。畵布中點的坐標用(sx, sy)表示。我們使用等角投影將三維點 ![](../images/ch3-02.png) -(x,y,z) 投影到二維的畵布中. 畵布中從遠處到右邊的點對應較大的x值和較大的y值. 併且畵布中x和y值越大, 則對應的z值越小. x和y的垂直和水平縮放繫數來自30度角的正絃和餘絃值. z的縮放繫數0.4, 是一個任意選擇的參數. +(x,y,z)投影到二維的畵布中。畵布中從遠處到右邊的點對應較大的x值和較大的y值。併且畵布中x和y值越大,則對應的z值越小。x和y的垂直和水平縮放繫數來自30度角的正絃和餘絃值。z的縮放繫數0.4,是一個任意選擇的參數。 -對於二維網格中的每一個單位, main函數計算單元的四個頂點在畵布中對應多邊形ABCD的頂點, 其中B對應(i,j)頂點位置, A, C, 和 D是相鄰的頂點, 然後輸齣SVG的繪製指令. +對於二維網格中的每一個網格單元,main函數計算單元的四個頂點在畵布中對應多邊形ABCD的頂點,其中B對應(i,j)頂點位置,A、C和D是其它相鄰的頂點,然後輸出SVG的繪製指令。 -**練習3.1:** 如果 f 函數返迴的是無限製的 float64 值, 那麽SVG文件可能輸齣無效的多邊形元素(雖然許多SVG渲染器會妥善處理這類問題). 脩改程序跳過無效的多邊形. +**練習 3.1:** 如果f函數返迴的是無限製的float64值,那麽SVG文件可能輸出無效的多邊形元素(雖然許多SVG渲染器會妥善處理這類問題)。脩改程序跳過無效的多邊形。 -**練習3.2:** 試驗math包中其他函數的渲染圖形. 你是否能輸齣一個egg box, moguls, 或 a saddle 圖案? +**練習 3.2:** 試驗math包中其他函數的渲染圖形。你是否能輸出一個egg box、moguls或a saddle圖案? -**練習3.3:**根據高度給每個多邊形上色, 那樣峯值部將是紅色(#ff0000), 谷部將是藍色(#0000ff). +**練習 3.3:** 根據高度給每個多邊形上色,那樣峯值部將是紅色(#ff0000),谷部將是藍色(#0000ff)。 -**3.4:** 參考1.7節Lissajous例子的函數, 構造一個web服務器, 用於計算函數麴面然後返迴SVG數據給客戶端. 服務器必鬚設置 Content-Type 頭部: +**練習 3.4:** 參考1.7節Lissajous例子的函數,構造一個web服務器,用於計算函數麴面然後返迴SVG數據給客戶端。服務器必鬚設置Content-Type頭部: ```Go w.Header().Set("Content-Type", "image/svg+xml") ``` -(這一步在Lissajous例子中不是必鬚的, 因爲服務器使用標準的PNG圖像格式, 可以根據前面的512個字節自動輸齣對應的頭部.) 允許客戶端通過HTTP請求參數設置高度, 寬度, 和顔色等參數. +(這一步在Lissajous例子中不是必鬚的,因爲服務器使用標準的PNG圖像格式,可以根據前面的512個字節自動輸出對應的頭部。)允許客戶端通過HTTP請求參數設置高度、寬度和顔色等參數。 diff --git a/ch3/ch3-03.md b/ch3/ch3-03.md index 7fefdb7..9cae6eb 100644 --- a/ch3/ch3-03.md +++ b/ch3/ch3-03.md @@ -1,7 +1,6 @@ ## 3.3. 複數 - -Go提供了兩種精度的複數類似, complex64 和 complex128, 分别對應 float32 和 float64精度. 內置的 complex 函數用於構建複數, 內建的 real 和 imag 函數返迴複數的實部和虛部: +Go語言提供了兩種精度的複數類型:complex64和complex128,分别對應float32和float64兩種浮點數精度。內置的complex函數用於構建複數,內建的real和imag函數分别返迴複數的實部和虛部: ```Go var x complex128 = complex(1, 2) // 1+2i @@ -11,28 +10,28 @@ fmt.Println(real(x*y)) // "-5" fmt.Println(imag(x*y)) // "10" ``` -如果一個浮點數面值或一個十進製整數面值後面跟着一個i, 例如 3.141592i 或 2i, 它將構成一個複數的虛部, 複數的實部是0: +如果一個浮點數面值或一個十進製整數面值後面跟着一個i,例如3.141592i或2i,它將構成一個複數的虛部,複數的實部是0: ```Go fmt.Println(1i * 1i) // "(-1+0i)", i^2 = -1 ``` -在常量算術規則下, 一個複數常量可以加到另一個常量(整數或浮點數, 實部或虛部), 我們可以用自然的方式寫複數, 就像 1+2i, 或與之等價的寫法 2i+1. 上面x和y的聲明語句還可以簡化: +在常量算術規則下,一個複數常量可以加到另一個普通數值常量(整數或浮點數、實部或虛部),我們可以用自然的方式書寫複數,就像1+2i或與之等價的寫法2i+1。上面x和y的聲明語句還可以簡化: ```Go x := 1 + 2i y := 3 + 4i ``` -複數也可以用 == 和 != 進行相等比較. 隻有兩個複數的實部和虛部都相等的時候它們纔是相等的. +複數也可以用==和!=進行相等比較。隻有兩個複數的實部和虛部都相等的時候它們才是相等的(譯註:浮點數的相等比較是危險的,需要特别小心處理精度問題)。 -math/cmplx 包提供了複數處理的許多函數, 例如求複數的平方根函數和求冪函數. +math/cmplx包提供了複數處理的許多函數,例如求複數的平方根函數和求冪函數。 ```Go fmt.Println(cmplx.Sqrt(-1)) // "(0+1i)" ``` -下面的程序使用complex128複數算法來生成一個Mandelbrot圖像. +下面的程序使用complex128複數算法來生成一個Mandelbrot圖像。 ```Go gopl.io/ch3/mandelbrot @@ -83,19 +82,19 @@ func mandelbrot(z complex128) color.Color { } ``` -遍歷1024x1024圖像每個點的兩個嵌套的循環對應 -2 到 +2 區間的複數平面. 程序反複測試每個點對應複數值平方值加一個增量值對應的點是否超齣半徑爲2的圓. 如果超過了, 通過根據逃逸的迭代次數對應的灰度顔色來代替. 如果不是, 該點屬於Mandelbrot集合, 使用黑色顔色標記. 最終程序將生成的PNG格式分形圖像圖像輸齣到標準輸齣, 如圖3.3所示. - -**練習3.5:** 實現一個綵色的Mandelbrot圖像, 使用 image.NewRGBA 創建圖像, 使用 color.RGBA 或 color.YCbCr 生成顔色. - -**練習3.6:** 超采樣技術可以降低每個像素對計算顔色值和平均值的影響. 簡單的方法是將每個像素分層四個子像素, 實現它. - -**練習3.7:** 另一個生成分形圖像的方式是使用牛頓法來求解一個複數方程, 例如 z^4 − 1 = 0. 每個起點到四個根的迭代次數對應陰影的灰度. 方程根對應的點用顔色表示. +用於遍歷1024x1024圖像每個點的兩個嵌套的循環對應-2到+2區間的複數平面。程序反複測試每個點對應複數值平方值加一個增量值對應的點是否超出半徑爲2的圓。如果超過了,通過根據預設置的逃逸迭代次數對應的灰度顔色來代替。如果不是,那麽該點屬於Mandelbrot集合,使用黑色顔色標記。最終程序將生成的PNG格式分形圖像圖像輸出到標準輸出,如圖3.3所示。 ![](../images/ch3-03.png) -**練習3.8:** 通過提高精度來生成更多級别的分形. 使用四種不同精度類型的數字實現相同的分形: complex64, complex128, big.Float, and big.Rat. (後面兩種類型在 math/big 包聲明. Float是有指定限精度的浮點數; Rat是無效精度的有理數.) 它們間的性能和內存使用對比如何? 當渲染圖可見時縮放的級别是多少? +**練習 3.5:** 實現一個綵色的Mandelbrot圖像,使用image.NewRGBA創建圖像,使用color.RGBA或color.YCbCr生成顔色。 -**練習3.9:** 編寫一個web服務器, 用於給客戶端生成分形的圖像. 運行客戶端用過HTTP參數參數指定x,y和zoom參數. +**練習 3.6:** 陞采樣技術可以降低每個像素對計算顔色值和平均值的影響。簡單的方法是將每個像素分層四個子像素,實現它。 + +**練習 3.7:** 另一個生成分形圖像的方式是使用牛頓法來求解一個複數方程,例如$$z^4-1=0$$。每個起點到四個根的迭代次數對應陰影的灰度。方程根對應的點用顔色表示。 + +**練習 3.8:** 通過提高精度來生成更多級别的分形。使用四種不同精度類型的數字實現相同的分形:complex64、complex128、big.Float和big.Rat。(後面兩種類型在math/big包聲明。Float是有指定限精度的浮點數;Rat是無效精度的有理數。)它們間的性能和內存使用對比如何?當渲染圖可見時縮放的級别是多少? + +**練習 3.9:** 編寫一個web服務器,用於給客戶端生成分形的圖像。運行客戶端用過HTTP參數參數指定x,y和zoom參數。 diff --git a/ch3/ch3-04.md b/ch3/ch3-04.md index 892db9a..25132b1 100644 --- a/ch3/ch3-04.md +++ b/ch3/ch3-04.md @@ -1,17 +1,16 @@ ## 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' ``` -其中 s[0] 應用於空字符串會導致 panic 異常. +其中s[0]操作如果應用於空字符串將會導致panic異常。 -因爲 `&&` 的優先級比 `||` 高 (助記: `&&` 對應邏輯乘法, `||` 對應邏輯加法, 乘法比加法優先級要高), 下面形式的布爾表達式是不需要加小括弧的: +因爲`&&`的優先級比`||`高(助記:`&&`對應邏輯乘法,`||`對應邏輯加法,乘法比加法優先級要高),下面形式的布爾表達式是不需要加小括弧的: ```Go if 'a' <= c && c <= 'z' || @@ -21,7 +20,7 @@ if 'a' <= c && c <= 'z' || } ``` -布爾值併不會隱式轉換爲數字值0或1, 反之亦然. 必鬚使用一個顯式的if語句輔助轉換: +布爾值併不會隱式轉換爲數字值0或1,反之亦然。必鬚使用一個顯式的if語句輔助轉換: ```Go i := 0 diff --git a/ch3/ch3-05-1.md b/ch3/ch3-05-1.md index faee272..c6a56da 100644 --- a/ch3/ch3-05-1.md +++ b/ch3/ch3-05-1.md @@ -1,7 +1,6 @@ ### 3.5.1. 字符串面值 - -字符串值也可以用字符串面值方式編寫, 隻要將一繫列字節序列包含在雙引號卽可: +字符串值也可以用字符串面值方式編寫,隻要將一繫列字節序列包含在雙引號卽可: ``` "Hello, 世界" @@ -9,9 +8,9 @@ ![](../images/ch3-04.png) -因爲Go語言源文件總是用UTF8編碼, 併且Go的文本字符串也以UTF8編碼的方式處理, 我們可以將Unicode碼點也寫到字符串面值中. +因爲Go語言源文件總是用UTF8編碼,併且Go語言的文本字符串也以UTF8編碼的方式處理,因此我們可以將Unicode碼點也寫到字符串面值中。 -在一個雙引號包含的字符串面值中, 可以用以反斜槓\開頭的轉義序列插入任意的數據. 下面換行, 迴車和 製表符等常見的ASCII控製代碼的轉義方式: +在一個雙引號包含的字符串面值中,可以用以反斜槓`\`開頭的轉義序列插入任意的數據。下面的換行、迴車和製表符等是常見的ASCII控製代碼的轉義方式: ``` \a 響鈴 @@ -26,11 +25,11 @@ \\ 反斜槓 ``` -可以通過十六進製或八進製轉義在字符串面值包含任意的字節. 一個十六進製的轉義是 \xhh, 其中兩個h表示十六進製數字(大寫或小寫都可以). 一個八進製轉義是 \ooo, 包含三個八進製的o數字(0到7), 但是不能超過\377. 每一個單一的字節表達一個特定的值. 稍後我們將看到如何將一個Unicode碼點寫到字符串面值中. +可以通過十六進製或八進製轉義在字符串面值包含任意的字節。一個十六進製的轉義形式是\xhh,其中兩個h表示十六進製數字(大寫或小寫都可以)。一個八進製轉義形式是\ooo,包含三個八進製的o數字(0到7),但是不能超過`\377`(譯註:對應一個字節的范圍,十進製爲255)。每一個單一的字節表達一個特定的值。稍後我們將看到如何將一個Unicode碼點寫到字符串面值中。 -一個原生的字符串面值形式是 `...`, 使用反引號 ``` 代替雙引號. 在原生的字符串面值中, 沒有轉義操作; 全部的內容都是字面的意思, 包含退格和換行, 因此一個程序中的原生字符串面值可能跨越多行. 唯一的特殊處理是是刪除迴車以保證在所有平颱上的值都是一樣的, 包括那些把迴車也放入文本文件的繫統. +一個原生的字符串面值形式是`...`,使用反引號```代替雙引號。在原生的字符串面值中,沒有轉義操作;全部的內容都是字面的意思,包含退格和換行,因此一個程序中的原生字符串面值可能跨越多行(譯註:在原生字符串面值內部是無法直接寫```字符的,可以用八進製或十六進製轉義或+"```"鏈接字符串常量完成)。唯一的特殊處理是會刪除迴車以保證在所有平台上的值都是一樣的,包括那些把迴車也放入文本文件的繫統(譯註:Windows繫統會把迴車和換行一起放入文本文件中)。 -原生字符串面值用於編寫正則表達式會很方便, 因爲正則表達式往往會包含很多反斜槓. 原生字符串面值同時廣泛應用於HTML模闆, JSON面值, 命令行提示信息, 以及那些需要擴展到多行的場景. +原生字符串面值用於編寫正則表達式會很方便,因爲正則表達式往往會包含很多反斜槓。原生字符串面值同時被廣泛應用於HTML模闆、JSON面值、命令行提示信息以及那些需要擴展到多行的場景。 ```Go const GoUsage = `Go is a tool for managing Go source code. diff --git a/ch3/ch3-05-2.md b/ch3/ch3-05-2.md index 285aa6d..6b238d8 100644 --- a/ch3/ch3-05-2.md +++ b/ch3/ch3-05-2.md @@ -1,13 +1,12 @@ ### 3.5.2. Unicode +在很久以前,世界還是比較簡單的,起碼計算機世界就隻有一個ASCII字符集:美国信息交換標準代碼。ASCII,更準確地説是美国的ASCII,使用7bit來表示128個字符:包含英文字母的大小寫、數字、各種標點符號和設置控製符。對於早期的計算機程序來説,這些就足夠了,但是這也導致了世界上很多其他地區的用戶無法直接使用自己的符號繫統。隨着互聯網的發展,混合多種語言的數據變得很常見(譯註:比如本身的英文原文或中文翻譯都包含了ASCII、中文、日文等多種語言字符)。如何有效處理這些包含了各種語言的豐富多樣的文本數據呢? -在很久以前, 世界比較簡單的, 起碼計算機就隻有一個ASCII字符集: 美國信息交換標準代碼. ASCII, 更準確地説是美國的ASCII, 使用 7bit 來表示 128 個字符: 包含英文字母的大小寫, 數字, 各種標點符號和設置控製符. 對於早期的計算機程序, 這些足夠了, 但是這也導致了世界上很多其他地區的用戶無法直接使用自己的書寫繫統. 隨着互聯網的發展, 混合多種語言的數據變了很常見. 如何有效處理這些包含了各種語言的豐富多樣的數據呢? +答案就是使用Unicode( http://unicode.org ),它收集了這個世界上所有的符號繫統,包括重音符號和其它變音符號,製表符和迴車符,還有很多神祕的符號,每個符號都分配一個唯一的Unicode碼點,Unicode碼點對應Go語言中的rune整數類型(譯註:rune是int32等價類型)。 -答案就是使用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編碼方式就能表達常用字符。但是,還有其它更好的編碼方法嗎? diff --git a/ch3/ch3-05-3.md b/ch3/ch3-05-3.md index 44d1539..5a0397d 100644 --- a/ch3/ch3-05-3.md +++ b/ch3/ch3-05-3.md @@ -1,7 +1,6 @@ ### 3.5.3. UTF-8 - -UTF8是一個將Unicode碼點編碼爲字節序列的變長編碼. UTF8編碼由Go語言之父 Ken Thompson 和 Rob Pike 共同發明, 現在已經是Unicode的標準. UTF8使用1到4個字節來表示每個Unicode碼點符號, ASCII部分字符隻使用1個字節, 常用字符部分使用2或3個字節. 每個符號編碼後第一個字節的高端bit位用於表示總共有多少個字節. 如果第一個字節的高端bit爲0, 則表示對應7bit的ASCII字符, 每個字符一個字節, 和傳統的ASCII編碼兼容. 如果第一個字節的高端bit是110, 則説明需要2個字節; 後續的每個高端bit都以10開頭. 更大的Unicode碼點也是采用類似的策略處理. +UTF8是一個將Unicode碼點編碼爲字節序列的變長編碼。UTF8編碼由Go語言之父Ken Thompson和Rob Pike共同發明的,現在已經是Unicode的標準。UTF8編碼使用1到4個字節來表示每個Unicode碼點,ASCII部分字符隻使用1個字節,常用字符部分使用2或3個字節表示。每個符號編碼後第一個字節的高端bit位用於表示總共有多少編碼個字節。如果第一個字節的高端bit爲0,則表示對應7bit的ASCII字符,ASCII字符每個字符依然是一個字節,和傳統的ASCII編碼兼容。如果第一個字節的高端bit是110,則説明需要2個字節;後續的每個高端bit都以10開頭。更大的Unicode碼點也是采用類似的策略處理。 ``` 0xxxxxxx runes 0-127 (ASCII) @@ -10,11 +9,11 @@ UTF8是一個將Unicode碼點編碼爲字節序列的變長編碼. UTF8編碼由 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx 65536-0x10ffff (other values unused) ``` -變長的編碼無法直接通過索引來訪問第n個字符, 但是UTF8穫得了很多額外的優點. 首先UTF8編碼比較緊湊, 兼容ASCII, 併且可以自動同步: 它可以通過向前迴朔最多2個字節就能確定當前字符編碼的開始字節的位置. 它也是一個前綴編碼, 所以當從左向右解碼時不會有任何歧義也併不需要向前査看. 沒有任何字符的編碼是其它字符編碼的子串, 或是其它編碼序列的字串, 因此蒐索一個字符時隻要蒐索它的字節編碼序列卽可, 不用擔心前後的上下文會對蒐索結果産生榦擾. 同時UTF8編碼的順序和Unicode碼點的順序一致, 因此可以直接排序UTF8編碼序列. 同業也沒有嵌入的NUL(0)字節, 可以很好地兼容那些使用NUL作爲字符串結尾的編程語言. +變長的編碼無法直接通過索引來訪問第n個字符,但是UTF8編碼獲得了很多額外的優點。首先UTF8編碼比較緊湊,完全兼容ASCII碼,併且可以自動同步:它可以通過向前迴朔最多2個字節就能確定當前字符編碼的開始字節的位置。它也是一個前綴編碼,所以當從左向右解碼時不會有任何歧義也併不需要向前査看(譯註:像GBK之類的編碼,如果不知道起點位置則可能會出現歧義)。沒有任何字符的編碼是其它字符編碼的子串,或是其它編碼序列的字串,因此蒐索一個字符時隻要蒐索它的字節編碼序列卽可,不用擔心前後的上下文會對蒐索結果産生榦擾。同時UTF8編碼的順序和Unicode碼點的順序一致,因此可以直接排序UTF8編碼序列。同時因爲沒有嵌入的NUL(0)字節,可以很好地兼容那些使用NUL作爲字符串結尾的編程語言。 -Go的源文件采用UTF8編碼, 併且Go處理UTF8編碼的文本也很齣色. unicode 包提供了諸多處理 rune 字符相關功能的函數函數(區分字母和數組, 或者是字母的大寫和小寫轉換等), unicode/utf8 包了提供了rune 字符序列的UTF8編碼和解碼的功能. +Go語言的源文件采用UTF8編碼,併且Go語言處理UTF8編碼的文本也很出色。unicode包提供了諸多處理rune字符相關功能的函數(比如區分字母和數組,或者是字母的大寫和小寫轉換等),unicode/utf8包則提供了用於rune字符序列的UTF8編碼和解碼的功能。 -有很多Unicode字符很難直接從鍵盤輸入, 併且很多字符有着相似的結構; 有一些甚至是不可見的字符. Go字符串面值中的Unicode轉義字符讓我們可以通過Unicode碼點輸入特殊的字符. 有兩種形式, \uhhhh 對應16bit的碼點值, \Uhhhhhhhh 對應32bit的碼點值, 其中h是一個十六進製數字; 一般很少需要使用32bit的形式. 每一個對應碼點的UTF8編碼. 例如: 下面的字母串面值都表示相同的值: +有很多Unicode字符很難直接從鍵盤輸入,併且還有很多字符有着相似的結構;有一些甚至是不可見的字符(譯註:中文和日文就有很多相似但不同的字)。Go語言字符串面值中的Unicode轉義字符讓我們可以通過Unicode碼點輸入特殊的字符。有兩種形式:\uhhhh對應16bit的碼點值,\Uhhhhhhhh對應32bit的碼點值,其中h是一個十六進製數字;一般很少需要使用32bit的形式。每一個對應碼點的UTF8編碼。例如:下面的字母串面值都表示相同的值: ``` "世界" @@ -23,17 +22,17 @@ Go的源文件采用UTF8編碼, 併且Go處理UTF8編碼的文本也很齣色. u "\U00004e16\U0000754c" ``` -上面三個轉義序列爲第一個字符串提供替代寫法, 但是它們的值都是相同的. +上面三個轉義序列都爲第一個字符串提供替代寫法,但是它們的值都是相同的。 -Unicode轉義也可以使用在rune字符中. 下面三個字符是等價的: +Unicode轉義也可以使用在rune字符中。下面三個字符是等價的: ``` '世' '\u4e16' '\U00004e16' ``` -對於小於256碼點值可以寫在一個十六進製轉義字節中, 例如 '\x41' 對應 'A' 字符, 但是對於更大的碼點則必鬚使用 \u 或 \U 轉義形式. 因此, '\xe4\xb8\x96' 併不是一個合法的rune字符, 雖然這三個字節對應一個有效的UTF8編碼的碼點. +對於小於256碼點值可以寫在一個十六進製轉義字節中,例如'\x41'對應字符'A',但是對於更大的碼點則必鬚使用\u或\U轉義形式。因此,'\xe4\xb8\x96'併不是一個合法的rune字符,雖然這三個字節對應一個有效的UTF8編碼的碼點。 -得意於UTF8優良的設計, 諸多字符串操作都不需要解碼. 我們可以不用解碼直接測試一個字符串是否是另一個字符串的前綴: +得益於UTF8編碼優良的設計,諸多字符串操作都不需要解碼操作。我們可以不用解碼直接測試一個字符串是否是另一個字符串的前綴: ```Go func HasPrefix(s, prefix string) bool { @@ -41,7 +40,7 @@ func HasPrefix(s, prefix string) bool { } ``` -或者是後綴測試: +或者是後綴測試: ```Go func HasSuffix(s, suffix string) bool { @@ -49,7 +48,7 @@ func HasSuffix(s, suffix string) bool { } ``` -或者是包含子串測試: +或者是包含子串測試: ```Go func Contains(s, substr string) bool { @@ -62,9 +61,9 @@ func Contains(s, substr string) bool { } ``` -對於UTF8編碼後文本的處理和原始的字節處理邏輯一樣. 但是對應很多其它編碼則併不是這樣的. (上面的函數都來自 strings 字符串處理包, 雖然它們的實現包含了一個用哈希技術優化的 Contains 實現.) +對於UTF8編碼後文本的處理和原始的字節處理邏輯是一樣的。但是對應很多其它編碼則併不是這樣的。(上面的函數都來自strings字符串處理包,眞實的代碼包含了一個用哈希技術優化的Contains 實現。) -另以方面, 如果我們眞的關心每個Unicode字符, 我們可以使用其它機製. 考慮前面的第一個例子中的字符串, 它包混合了中西兩種字符. 圖3.5展示了它的內存表示形式. 字符串包含13個字節, 以UTF8形式編碼, 但是隻對應9個Unicode字符: +另一方面,如果我們眞的關心每個Unicode字符,我們可以使用其它處理方式。考慮前面的第一個例子中的字符串,它包混合了中西兩種字符。圖3.5展示了它的內存表示形式。字符串包含13個字節,以UTF8形式編碼,但是隻對應9個Unicode字符: ```Go import "unicode/utf8" @@ -74,7 +73,7 @@ fmt.Println(len(s)) // "13" fmt.Println(utf8.RuneCountInString(s)) // "9" ``` -爲了處理這些眞實的字符, 我們需要一個UTF8解碼器. unicode/utf8 包提供了實現, 我們可以這樣使用: +爲了處理這些眞實的字符,我們需要一個UTF8解碼器。unicode/utf8包提供了該功能,我們可以這樣使用: ```Go for i := 0; i < len(s); { @@ -84,7 +83,7 @@ for i := 0; i < len(s); { } ``` -每一次調用 DecodeRuneInString 函數都返迴一個 r 和 長度, r 對應字符本身, 長度對應r采用UTF8編碼後的字節數目. 長度可以用於更新第i個字符在字符串中的字節索引位置. 但是這種方式是笨拙的, 我們需要更簡潔的語法. 幸運的是, Go的range循環在處理字符串的時候, 會自動隱式解碼UTF8字符串. 下面的循環運行如圖3.5所示; 需要註意的是對於非ASCII, 索引更新的步長超過1個字節. +每一次調用DecodeRuneInString函數都返迴一個r和長度,r對應字符本身,長度對應r采用UTF8編碼後的編碼字節數目。長度可以用於更新第i個字符在字符串中的字節索引位置。但是這種編碼方式是笨拙的,我們需要更簡潔的語法。幸運的是,Go語言的range循環在處理字符串的時候,會自動隱式解碼UTF8字符串。下面的循環運行如圖3.5所示;需要註意的是對於非ASCII,索引更新的步長將超過1個字節。 ![](../images/ch3-05.png) @@ -94,7 +93,7 @@ for i, r := range "Hello, 世界" { } ``` -我們可以使用一個簡單的循環來統計字符串中字符的數目, 像這樣: +我們可以使用一個簡單的循環來統計字符串中字符的數目,像這樣: ```Go n := 0 @@ -103,7 +102,7 @@ for _, _ = range s { } ``` -想其它形式的循環那樣, 我們可以忽略不需要的變量: +像其它形式的循環那樣,我們也可以忽略不需要的變量: ```Go n := 0 @@ -112,15 +111,15 @@ for range s { } ``` -或者我們可以直接調用 utf8.RuneCountInString(s) 函數. +或者我們可以直接調用utf8.RuneCountInString(s)函數。 -正如我們前面提到了, 文本字符串采用UTF8編碼隻是一種慣例,但是對於循環的眞正字符串併不是一個慣例, 這是正確的. 如果用於循環的字符串隻是一個普通的二進製數據, 或者是含有錯誤編碼的UTF8數據, 將會發送什麽? +正如我們前面提到的,文本字符串采用UTF8編碼隻是一種慣例,但是對於循環的眞正字符串併不是一個慣例,這是正確的。如果用於循環的字符串隻是一個普通的二進製數據,或者是含有錯誤編碼的UTF8數據,將會發送什麽呢? -每一個UTF8字符解碼, 不管是顯示地調用 utf8.DecodeRuneInString 解碼或在 range 循環中隱式地解碼, 如果遇到一個錯誤的輸入字節, 將生成一個特别的Unicode字符 '\uFFFD', 在印刷中這個符號通常是一個黑色六角或鑽石形狀, 里面包含一個白色的問號(?). 當程序遇到這樣的一個字符, 通常是一個信號, 説明輸入併不是一個完美沒有錯誤的的UTF8編碼字符串. +每一個UTF8字符解碼,不管是顯式地調用utf8.DecodeRuneInString解碼或是在range循環中隱式地解碼,如果遇到一個錯誤的UTF8編碼輸入,將生成一個特别的Unicode字符'\uFFFD',在印刷中這個符號通常是一個黑色六角或鑽石形狀,里面包含一個白色的問號(?)。當程序遇到這樣的一個字符,通常是一個危險信號,説明輸入併不是一個完美沒有錯誤的UTF8字符串。 -UTF8作爲交換格式是非常方便的, 但是在程序內部采用rune類型可能更方便, 因爲rune大小一致, 支持數組索引和方便切割. +UTF8字符串作爲交換格式是非常方便的,但是在程序內部采用rune序列可能更方便,因爲rune大小一致,支持數組索引和方便切割。 -string 接受到 []rune 的轉換, 可以將一個UTF8編碼的字符串解碼爲Unicode字符序列: +string接受到[]rune的類型轉換,可以將一個UTF8編碼的字符串解碼爲Unicode字符序列: ```Go // "program" in Japanese katakana @@ -130,22 +129,22 @@ r := []rune(s) fmt.Printf("%x\n", r) // "[30d7 30ed 30b0 30e9 30e0]" ``` -(在第一個Printf中的 `% x` 參數用於在每個十六進製數字前插入一個空格.) +(在第一個Printf中的`% x`參數用於在每個十六進製數字前插入一個空格。) -如果是將一個 []rune 類型的Unicode字符切片或數組轉爲string, 則對它們進行UTF8編碼: +如果是將一個[]rune類型的Unicode字符slice或數組轉爲string,則對它們進行UTF8編碼: ```Go fmt.Println(string(r)) // "プログラム" ``` -將一個整數轉型爲字符串意思是生成整數作爲Unicode碼點的UTF8編碼的字符串: +將一個整數轉型爲字符串意思是生成以隻包含對應Unicode碼點字符的UTF8字符串: ```Go fmt.Println(string(65)) // "A", not "65" fmt.Println(string(0x4eac)) // "京" ``` -如果對應碼點的字符是無效的, 則用'\uFFFD'無效字符作爲替換: +如果對應碼點的字符是無效的,則用'\uFFFD'無效字符作爲替換: ```Go fmt.Println(string(1234567)) // "(?)" diff --git a/ch3/ch3-05-4.md b/ch3/ch3-05-4.md index ad1702f..d51c6d4 100644 --- a/ch3/ch3-05-4.md +++ b/ch3/ch3-05-4.md @@ -1,15 +1,14 @@ ### 3.5.4. 字符串和Byte切片 +標準庫中有四個包對字符串處理尤爲重要:bytes、strings、strconv和unicode包。strings包提供了許多如字符串的査詢、替換、比較、截斷、拆分和合併等功能。 -標準庫中有四個包對字符串處理尤爲重要: bytes, strings, strconv, 和 unicode. strings 包提供了許多如字符串的査詢, 替換, 比較, 截斷, 拆分, 和合併等功能. +bytes包也提供了很多類似功能的函數,但是針對和字符串有着相同結構的[]byte類型。因爲字符串是隻讀的,因此逐步構建字符串會導致很多分配和複製。在這種情況下,使用bytes.Buffer類型將會更有效,稍後我們將展示。 -bytes 包也提供了很多類似功能的函數, 但是針對和字符串有着相同結構的 []byte 類型. 因爲字符串是隻讀的, 因此逐步構建字符串會導致很多分配和複製. 在這種情況下, 使用 bytes.Buffer 類型會更有效, 稍後我們將展示. +strconv包提供了布爾型、整型數、浮點數和對應字符串的相互轉換,還提供了雙引號轉義相關的轉換。 -strconv 包提供了 布爾型, 整型數, 浮點數和對應字符串間的相互轉換, 還提供了雙引號的字符串面值形式的轉換. +unicode包提供了IsDigit、IsLetter、IsUpper和IsLower等類似功能,它們用於給字符分類。每個函數有一個單一的rune類型的參數,然後返迴一個布爾值。而像ToUpper和ToLower之類的轉換函數將用於rune字符的大小寫轉換。所有的這些函數都是遵循Unicode標準定義的字母、數字等分類規范。strings包也有類似的函數,它們是ToUpper和ToLower,將原始字符串的每個字符都做相應的轉換,然後返迴新的字符串。 -unicode 包提供了類似 IsDigit, IsLetter, IsUpper, 和 IsLower 等功能, 它們用於給字符分類. 每個函數有一個單一的rune類型的參數, 然後返迴一個布爾值. 像 ToUpper 和 ToLower 之類的轉換函數將用於rune字符的大小寫轉換. 所有的這些函數都是遵循Unicode標準定義的字母,數字等分類規范. strings 包也有類似的函數, 它們是 ToUpper 和 ToLower, 將原始字符串的每個字符都做相應的轉換, 然後返迴新的字符串. - -下面的 basename 函數的靈感由Unix shell的同名工具而來. 在我們實現的版本中, basename(s) 將看起來像是繫統路徑的前綴刪除, 同時將看似文件類型的後綴名刪除: +下面例子的basename函數靈感於Unix shell的同名工具。在我們實現的版本中,basename(s)將看起來像是繫統路徑的前綴刪除,同時將看似文件類型的後綴名部分刪除: ```Go fmt.Println(basename("a/b/c.go")) // "c" @@ -17,7 +16,7 @@ fmt.Println(basename("c.d.go")) // "c.d" fmt.Println(basename("abc")) // "abc" ``` -第一個版本併沒有使用任何庫, 全部手工實現: +第一個版本併沒有使用任何庫,全部手工硬編碼實現: ```Go gopl.io/ch3/basename1 @@ -42,7 +41,7 @@ func basename(s string) string { } ``` -一個簡化的版本使用了 strings.LastIndex 庫函數: +簡化個版本使用了strings.LastIndex庫函數: ```Go gopl.io/ch3/basename2 @@ -57,10 +56,9 @@ func basename(s string) string { } ``` -path 和 path/filepath 包提供了關於文件名更一般的函數操作. 使用斜槓分隔路徑可以在任何操作繫統上工作. 斜槓本身不應該用於文件名, 但是在其他一些領域可能是有效的, 例如URL路徑組件. 相比之下, path/filepath 包使用操作繫統本身的路徑規則, 例如 POSIX 繫統使用 /foo/bar, Microsoft Windows 使用 c:\foo\bar 等. +path和path/filepath包提供了關於文件路徑名更一般的函數操作。使用斜槓分隔路徑可以在任何操作繫統上工作。斜槓本身不應該用於文件名,但是在其他一些領域可能會用於文件名,例如URL路徑組件。相比之下,path/filepath包則使用操作繫統本身的路徑規則,例如POSIX繫統使用/foo/bar,而Microsoft Windows使用c:\foo\bar等。 - -讓我們繼續另一個字符串的例子. 任務是將一個表示整值的字符串, 每隔三個字符插入一個逗號, 例如 "12345" 處理後成爲 "12,345". 這個版本隻適用於整數類型; 支持浮點數類型的支持留做練習. +讓我們繼續另一個字符串的例子。函數的功能是將一個表示整值的字符串,每隔三個字符插入一個逗號分隔符,例如“12345”處理後成爲“12,345”。這個版本隻適用於整數類型;支持浮點數類型的支持留作練習。 ```Go gopl.io/ch3/comma @@ -75,11 +73,11 @@ func comma(s string) string { } ``` -輸入 comma 的參數是一個字符串. 如果輸入字符串的長度小於或等於3的話, 則不需要插入逗號. 否則, comma 將在最後三個字符前切割爲兩個兩個子串, 然後用前面的子串遞歸調用自身. +輸入comma函數的參數是一個字符串。如果輸入字符串的長度小於或等於3的話,則不需要插入逗分隔符。否則,comma函數將在最後三個字符前位置將字符串切割爲兩個兩個子串併插入逗號分隔符,然後通過遞歸調用自身來出前面的子串。 -一個字符串包含的字節數組, 一旦創建, 是不可變的. 相比之下, 一個字節切片的原始則可以自由地脩改. +一個字符串是包含的隻讀字節數組,一旦創建,是不可變的。相比之下,一個字節slice的元素則可以自由地脩改。 -字符串和字節切片可以相互轉換: +字符串和字節slice之間可以相互轉換: ```Go s := "abc" @@ -87,9 +85,9 @@ b := []byte(s) s2 := string(b) ``` -從概念上講, []byte(s) 轉換是分配了一個新的字節數組保存了字符串數據的拷貝, 然後引用這個字節數組. 編譯器的優化可以避免在一些場景下分配和複製字符串數據, 但總的來説需要確保在b被脩改的情況下, 原始的s字符串也不會改變. 將一個字節切片轉到字符串的 string(b) 操作則是構造一個拷貝, 以確保s2字符串是隻讀的. +從概念上講,一個[]byte(s)轉換是分配了一個新的字節數組用於保存字符串數據的拷貝,然後引用這個底層的字節數組。編譯器的優化可以避免在一些場景下分配和複製字符串數據,但總的來説需要確保在變量b被脩改的情況下,原始的s字符串也不會改變。將一個字節slice轉到字符串的string(b)操作則是構造一個字符串拷貝,以確保s2字符串是隻讀的。 -爲了避免轉換中不必要的內存分配, bytes包和strings同時提供了許多類似的實用函數. 下面是strings包中的六個函數: +爲了避免轉換中不必要的內存分配,bytes包和strings同時提供了許多實用函數。下面是strings包中的六個函數: ```Go func Contains(s, substr string) bool @@ -100,7 +98,7 @@ func Index(s, sep string) int func Join(a []string, sep string) string ``` -bytes 包中對應的六個函數: +bytes包中也對應的六個函數: ```Go func Contains(b, subslice []byte) bool @@ -111,9 +109,9 @@ func Index(s, sep []byte) int func Join(s [][]byte, sep []byte) []byte ``` -唯一的區别是字符串類型參數被替換成了字節切片類型的參數. +它們之間唯一的區别是字符串類型參數被替換成了字節slice類型的參數。 -bytes 包還提供了 Buffer 類型用於字節切片的緩存. 一個 Buffer 開始是空的, 但是隨着 string, byte, 和 []byte 等類型數據的寫入可以動態增長, 一個 bytes.Buffer 變量併不需要處理化, 因此零值也是有效的: +bytes包還提供了Buffer類型用於字節slice的緩存。一個Buffer開始是空的,但是隨着string、byte或[]byte等類型數據的寫入可以動態增長,一個bytes.Buffer變量併不需要處理化,因爲零值也是有效的: ```Go gopl.io/ch3/printints @@ -137,16 +135,15 @@ func main() { } ``` -當向 bytes.Buffer 添加任意字符的UTF8編碼, 最好使用 bytes.Buffer 的 WriteRune 方法, 但是 WriteByte 方法對於寫入類似 '[' 和 ']' 等 ASCII 字符則更有效. +當向bytes.Buffer添加任意字符的UTF8編碼時,最好使用bytes.Buffer的WriteRune方法,但是WriteByte方法對於寫入類似'['和']'等ASCII字符則會更加有效。 -bytes.Buffer 類型有着諸多實用的功能, 我們在第七章討論接口時層涉及到, 我們將看看如何將它用作一個I/O 的輸入和輸齣對象, 例如 Fprintf 的 io.Writer 輸齣, 或作爲輸入源 io.Reader. +bytes.Buffer類型有着很多實用的功能,我們在第七章討論接口時將會涉及到,我們將看看如何將它用作一個I/O的輸入和輸出對象,例如當做Fprintf的io.Writer輸出對象,或者當作io.Reader類型的輸入源對象。 -**練習3.10:** 編寫一個非遞歸版本的comma函數, 使用 bytes.Buffer 代替字符串鏈接操作. +**練習 3.10:** 編寫一個非遞歸版本的comma函數,使用bytes.Buffer代替字符串鏈接操作。 -**練習3.11:** 完善 comma 函數, 以支持浮點數處理和一個可選的正負號處理. +**練習 3.11:** 完善comma函數,以支持浮點數處理和一個可選的正負號的處理。 - -**練習3.12:** 編寫一個函數, 判斷兩個字符串是否是是相互打亂的, 也就是説它們有着相同的字符, 但是對應不同的順序. +**練習 3.12:** 編寫一個函數,判斷兩個字符串是否是是相互打亂的,也就是説它們有着相同的字符,但是對應不同的順序。 diff --git a/ch3/ch3-05-5.md b/ch3/ch3-05-5.md index 264b470..2d0c176 100644 --- a/ch3/ch3-05-5.md +++ b/ch3/ch3-05-5.md @@ -1,9 +1,8 @@ ### 3.5.5. 字符串和數字的轉換 +除了字符串、字符、字節之間的轉換,字符串和數值之間的轉換也比較常見。由strconv包提供這類轉換功能。 -除了字符串, 字符, 字節 之間的轉換, 字符串和數值之間的轉換也比較常見. 由 strconv 包提供這類轉換功能. - -將一個整數轉爲字符串, 一種方法是用 fmt.Sprintf; 另一個方法是用 strconv.Itoa(“整數到ASCII”): +將一個整數轉爲字符串,一種方法是用fmt.Sprintf返迴一個格式化的字符串;另一個方法是用strconv.Itoa(“整數到ASCII”): ```Go x := 123 @@ -11,28 +10,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來解析輸入的字符串和數字,特别是當字符串和數字混合在一行的時候,它可以靈活處理不完整或不規則的輸入。 diff --git a/ch3/ch3-05.md b/ch3/ch3-05.md index 5c37eb0..5797e06 100644 --- a/ch3/ch3-05.md +++ b/ch3/ch3-05.md @@ -1,8 +1,8 @@ ## 3.5. 字符串 -一個字符串是一個不可改變的字節序列. 字符串可以包含任意的數據, 包括字節值0, 但是通常包含人類可讀的文本. 文本字符串通常被解釋爲采用UTF8編碼的Unicode碼點(rune)序列, 我們稍後會詳細討論這個問題. +一個字符串是一個不可改變的字節序列。字符串可以包含任意的數據,包括byte值0,但是通常是用來包含人類可讀的文本。文本字符串通常被解釋爲采用UTF8編碼的Unicode碼點(rune)序列,我們稍後會詳細討論這個問題。 -內置的 len 函數可以返迴一個字符串的字節數目(不是rune字符數目), 索引操作 s[i] 返迴第i個字節的字節值, i 必鬚滿足 0 ≤ i< len(s) 條件約束. +內置的len函數可以返迴一個字符串中的字節數目(不是rune字符數目),索引操作s[i]返迴第i個字節的字節值,i必鬚滿足0 ≤ i< len(s)條件約束。 ```Go s := "hello, world" @@ -10,23 +10,23 @@ fmt.Println(len(s)) // "12" fmt.Println(s[0], s[7]) // "104 119" ('h' and 'w') ``` -如果視圖訪問超齣字符串范圍的字節將會導致panic異常: +如果試圖訪問超出字符串索引范圍的字節將會導致panic異常: ```Go 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" @@ -34,16 +34,15 @@ fmt.Println(s[7:]) // "world" fmt.Println(s[:]) // "hello, world" ``` -其中 + 操作符將兩個字符串鏈接構造一個新字符串: +其中+操作符將兩個字符串鏈接構造一個新字符串: ```Go fmt.Println("goodbye" + s[5:]) // "goodbye, world" ``` -字符串可以用 == 和 < 進行比較; 比較通過逐個字節比較完成的, 因此比較的結果是字符串自然編碼的順序. +字符串可以用==和<進行比較;比較通過逐個字節比較完成的,因此比較的結果是字符串自然編碼的順序。 - -字符串的值是不可變的: 一個字符串包含的字節序列永遠不會被改變, 當然我們也可以給一個字符串變量分配一個新字符串值. 可以像下面這樣將一個字符串追加到另一個字符串 +字符串的值是不可變的:一個字符串包含的字節序列永遠不會被改變,當然我們也可以給一個字符串變量分配一個新字符串值。可以像下面這樣將一個字符串追加到另一個字符串: ```Go s := "left foot" @@ -51,20 +50,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" %} diff --git a/ch3/ch3-06-1.md b/ch3/ch3-06-1.md index cb4ddd7..eb9a842 100644 --- a/ch3/ch3-06-1.md +++ b/ch3/ch3-06-1.md @@ -1,9 +1,8 @@ ### 3.6.1. iota 常量生成器 +常量聲明可以使用iota常量生成器初始化,它用於生成一組以相似規則初始化的常量,但是不用每行都寫一遍初始化表達式。在一個const聲明語句中,在第一個聲明的常量所在的行,iota將會被置爲0,然後在每一個有常量聲明的行加一。 -常量聲明可以使用 iota 常量生成器, 用於生成一組相似的常量值, 但是不用每行都寫一遍. 在一個 const 聲明語句中, 在開始一行 iota 將會被置爲0, 然後在每一個有常量聲明的行加一. - -下面是來自 time 包的例子, 它首先定義了Weekday命名類型, 然後爲一週的每天定義一個常量, 從週日0開始. 這種類型一般被稱爲枚舉類型. +下面是來自time包的例子,它首先定義了一個Weekday命名類型,然後爲一週的每天定義了一個常量,從週日0開始。在其它編程語言中,這種類型一般被稱爲枚舉類型。 ```Go type Weekday int @@ -19,9 +18,9 @@ const ( ) ``` -週一將對應0, 週一爲1, 如此等等. +週一將對應0,週一爲1,如此等等。 -我們也可以在複雜的常量表達式中使用 iota, 下面是來自 net 包的例子, 用於給一個無符號整數的最低5bit的每個bit給定一個名字: +我們也可以在複雜的常量表達式中使用iota,下面是來自net包的例子,用於給一個無符號整數的最低5bit的每個bit指定一個名字: ```Go type Flags uint @@ -35,7 +34,7 @@ const ( ) ``` -隨着 iota 的遞增, 每個常量對應表達式 1 << iota, 是連續的2的冪, 分别對應一個bit位置. 使用這些常量可以測試, 設置, 或清除對應的bit位的值: +隨着iota的遞增,每個常量對應表達式1 << iota,是連續的2的冪,分别對應一個bit位置。使用這些常量可以用於測試、設置或清除對應的bit位的值: ```Go gopl.io/ch3/netflag @@ -56,7 +55,7 @@ unc main() { } ``` -下面是一個更複雜的例子, 每個常量都是1024的冪: +下面是一個更複雜的例子,每個常量都是1024的冪: ```Go const ( @@ -72,8 +71,8 @@ const ( ) ``` -iota 機製也有其局限性. 例如, 它併不能用於産生1000的冪(KB,MB,等等), 因爲併沒有計算冪的運算符. +不過iota常量生成規則也有其局限性。例如,它併不能用於産生1000的冪(KB、MB等),因爲Go語言併沒有計算冪的運算符。 -**練習3.13:** 編寫KB,MB的常量聲明, 然後擴展到YB. +**練習 3.13:** 編寫KB、MB的常量聲明,然後擴展到YB。 diff --git a/ch3/ch3-06-2.md b/ch3/ch3-06-2.md index 6509201..5ff311e 100644 --- a/ch3/ch3-06-2.md +++ b/ch3/ch3-06-2.md @@ -1,15 +1,14 @@ ### 3.6.2. 無類型常量 +Go語言的常量有個不同尋常之處。雖然一個常量可以有任意有一個確定的基礎類型,例如int或float64,或者是類似time.Duration這樣命名的基礎類型,但是許多常量併沒有一個明確的基礎類型。編譯器爲這些沒有明確的基礎類型的數字常量提供比基礎類型更高精度的算術運算;你可以認爲至少有256bit的運算精度。這里有六種未明確類型的常量類型,分别是無類型的布爾型、無類型的整數、無類型的字符、無類型的浮點數、無類型的複數、無類型的字符串。 -Go語言的常量有點不尋常. 雖然一個常量可以有任意有一個確定的基礎類型, 例如 int 或 float64, 或者是類似 time.Duration 這樣命名的基礎類型, 但是許多常量併沒有一個明確的基礎類型. 編譯期爲這些沒有明確的基礎類型的數字常量提供比基礎類型或機器更高精度的算術運算; 你可以認爲至少有256bit的運算精度. 這里有六種未明確類型的常量類型, 分别是 無類型的布爾型, 無類型的整數, 無類型的字符, 無類型的浮點數, 無類型的複數, 無類型的字符串. - -通過延遲明確具體類型, 無類型的常量不僅可以提供更高的精度, 而且可以直接用於更多的表達式而不需要類型轉換. 例如 例子中的 ZiB 和 YiB 的值已經超齣任何Go中整數類型能表達的范圍, 但是它們依然是合法的常量, 而且可以像下面表達式這樣使用: +通過延遲明確常量的具體類型,無類型的常量不僅可以提供更高的運算精度,而且可以直接用於更多的表達式而不需要顯式的類型轉換。例如,例子中的ZiB和YiB的值已經超出任何Go語言中整數類型能表達的范圍,但是它們依然是合法的常量,而且可以像下面常量表達式依然有效(譯註:YiB/ZiB是在編譯期計算出來的,併且結果常量是1024,是Go語言int變量能有效表示的): ```Go fmt.Println(YiB/ZiB) // "1024" ``` -另一個例子, math.Pi 無類型的浮點數常量, 可以直接用於任意需要浮點數或複數的地方: +另一個例子,math.Pi無類型的浮點數常量,可以直接用於任意需要浮點數或複數的地方: ```Go var x float32 = math.Pi @@ -17,7 +16,7 @@ var y float64 = math.Pi var z complex128 = math.Pi ``` -如果 math.Pi 被確定爲特定類型, 比如 float64, 那麽結果精度可能會不一樣, 同時對於需要float32或complex128類型值的地方會需要一個明確的類型轉換: +如果math.Pi被確定爲特定類型,比如float64,那麽結果精度可能會不一樣,同時對於需要float32或complex128類型值的地方則會強製需要一個明確的類型轉換: ```Go const Pi64 float64 = math.Pi @@ -27,9 +26,9 @@ var y float64 = Pi64 var z complex128 = complex128(Pi64) ``` -對於常量面值, 不同的寫法對應不同的類型. 例如 0, 0.0, 0i, 和 '\u0000' 雖然有着相同的常量值, 但是它們分别對應無類型的整數,無類型的浮點數,無類型的複數,和無類型的字符等不同的常量類型. 同樣, true 和 false 也是無類型的布爾類型, 字符串面值常量是無類型的字符串. +對於常量面值,不同的寫法可能會對應不同的類型。例如0、0.0、0i和'\u0000'雖然有着相同的常量值,但是它們分别對應無類型的整數、無類型的浮點數、無類型的複數和無類型的字符等不同的常量類型。同樣,true和false也是無類型的布爾類型,字符串面值常量是無類型的字符串類型。 -前面説過除法運算符 / 根據操作數的類型生成對應類型的結果. 因此, 不同寫法的常量除法表達式可能對應不同的結果: +前面説過除法運算符/會根據操作數的類型生成對應類型的結果。因此,不同寫法的常量除法表達式可能對應不同的結果: ```Go var f float64 = 212 @@ -38,7 +37,7 @@ fmt.Println(5 / 9 * (f - 32)) // "0"; 5/9 is an untyped integer, 0 fmt.Println(5.0 / 9.0 * (f - 32)) // "100"; 5.0/9.0 is an untyped float ``` -隻有常量可以是無類型的. 當一個無類型的常量被賦值給一個變量, 就像上面的第一行語句, 或者是像其餘三個語句中右邊表達式中含有明確類型的值, 無類型的常量將會被隱式轉換爲對應的類型, 如果可能的話. +隻有常量可以是無類型的。當一個無類型的常量被賦值給一個變量的時候,就像上面的第一行語句,或者是像其餘三個語句中右邊表達式中含有明確類型的值,無類型的常量將會被隱式轉換爲對應的類型,如果轉換合法的話。 ```Go var f float64 = 3 + 0i // untyped complex -> float64 @@ -56,7 +55,7 @@ f = float64(1e123) f = float64('a') ``` -無論是隱式或顯式, 將一種類型轉換爲另一種類型要求目標可以表示原始值. 對於浮點數和複數, 可能會有舍入處理: +無論是隱式或顯式轉換,將一種類型轉換爲另一種類型都要求目標可以表示原始值。對於浮點數和複數,可能會有舍入處理: ```Go const ( @@ -70,7 +69,7 @@ const ( ) ``` -對於一個沒有顯式類型的變量聲明(包括短變量聲明語法), 無類型的常量會被隱式轉爲默認的變量類型, 就像下面的例子: +對於一個沒有顯式類型的變量聲明語法(包括短變量聲明語法),無類型的常量會被隱式轉爲默認的變量類型,就像下面的例子: ```Go i := 0 // untyped integer; implicit int(0) @@ -79,16 +78,16 @@ f := 0.0 // untyped floating-point; implicit float64(0.0) c := 0i // untyped complex; implicit complex128(0i) ``` -註意默認類型是規則的: 無類型的整數常量默認轉換爲int, 對應不確定的尺寸, 但是浮點數好複數常量則默認轉換爲float64和complex128. Go語言本身併沒有不確定的尺寸的浮點數和複數類型, 因爲如何不知道浮點數類型的話很難寫齣正確的數值算法. +註意默認類型是規則的:無類型的整數常量默認轉換爲int,對應不確定的內存大小,但是浮點數和複數常量則默認轉換爲float64和complex128。Go語言本身併沒有不確定內存大小的浮點數和複數類型,而且如果不知道浮點數類型的話將很難寫出正確的數值算法。 -如果要給變量一個不同的類型, 我們必鬚顯式地將無類型的常量轉化爲所需的類型, 或給聲明的變量指定類型, 像下面例子這樣: +如果要給變量一個不同的類型,我們必鬚顯式地將無類型的常量轉化爲所需的類型,或給聲明的變量指定明確的類型,像下面例子這樣: ```Go var i = int8(0) var i int8 = 0 ``` -當嚐試將這些無類型的常量轉爲一個接口值時(見第7章), 這些默認類型將顯得尤爲重要, 因爲要靠它們明確接口對應的動態類型. +當嚐試將這些無類型的常量轉爲一個接口值時(見第7章),這些默認類型將顯得尤爲重要,因爲要靠它們明確接口對應的動態類型。 ```Go fmt.Printf("%T\n", 0) // "int" @@ -97,7 +96,7 @@ fmt.Printf("%T\n", 0i) // "complex128" fmt.Printf("%T\n", '\000') // "int32" (rune) ``` -現在我們已經講述了Go語言中全部的基礎數據類型. 下一步將演示如何用基礎數據類型組合成數組或結構體等複雜數據類型, 然後構建用於解決實際編程問題的數據結構, 這將是第四章的討論主題. +現在我們已經講述了Go語言中全部的基礎數據類型。下一步將演示如何用基礎數據類型組合成數組或結構體等複雜數據類型,然後構建用於解決實際編程問題的數據結構,這將是第四章的討論主題。 diff --git a/ch3/ch3-06.md b/ch3/ch3-06.md index f5e8590..b6e80d8 100644 --- a/ch3/ch3-06.md +++ b/ch3/ch3-06.md @@ -1,15 +1,14 @@ ## 3.6. 常量 +常量表達式的值在編譯期計算,而不是在運行期。每種常量的潛在類型都是基礎類型:boolean、string或數字。 -常量表達式的值在編譯期計算, 而不是在運行期. 每種常量的潛在類型都是基礎類型: boolean, string, 或數字. - -一個常量的聲明語句定義了常量的名字, 和變量的聲明語法類似, 常量的值不可脩改, 這樣可以防止在運行期被意外或惡意的脩改. 例如, 常量比變量更適合用於表達像 π 之類的數學常數, 因爲它們的值不會變化: +一個常量的聲明語句定義了常量的名字,和變量的聲明語法類似,常量的值不可脩改,這樣可以防止在運行期被意外或惡意的脩改。例如,常量比變量更適合用於表達像π之類的數學常數,因爲它們的值不會發生變化: ```Go const pi = 3.14159 // approximately; math.Pi is a better approximation ``` -和變量聲明一樣, 可以批量聲明多個常量; 這比較適合聲明一組相關的常量: +和變量聲明一樣,可以批量聲明多個常量;這比較適合聲明一組相關的常量: ```Go const ( @@ -18,11 +17,11 @@ const ( ) ``` -許多常量的運算可以在編譯期完成, 這樣可以減少運行時的工作, 也方便其他編譯優化. 當操作數是常量時, 一些運行時的錯誤可以在編譯時發現, 例如整數除零, 字符串索引越界, 任何導致無效浮點數的操作等. +所有常量的運算都可以在編譯期完成,這樣可以減少運行時的工作,也方便其他編譯優化。當操作數是常量時,一些運行時的錯誤也可以在編譯時被發現,例如整數除零、字符串索引越界、任何導致無效浮點數的操作等。 -常量間的所有算術運算, 邏輯運算和比較運算的結果也是常量, 對常量的類型轉換操作或以下函數調用都是返迴常量結果: len, cap, real, imag, complex, 和 unsafe.Sizeof(§13.1). +常量間的所有算術運算、邏輯運算和比較運算的結果也是常量,對常量的類型轉換操作或以下函數調用都是返迴常量結果:len、cap、real、imag、complex和unsafe.Sizeof(§13.1)。 -因爲它們的值是在編譯期就確定的, 因此常量可以是構成類型的一部分, 例如用於指定數組類型的長度: +因爲它們的值是在編譯期就確定的,因此常量可以是構成類型的一部分,例如用於指定數組類型的長度: ```Go const IPv4Len = 4 @@ -34,7 +33,7 @@ func parseIPv4(s string) IP { } ``` -一個常量的聲明也可以包含一個類型和一個值, 但是如果沒有顯式指明類型, 那麽將從右邊的表達式推斷類型. 在下面的代碼中, time.Duration 是一個命名類型, 底層類型是 int64, time.Minute 是對應類型的常量. 下面聲明的兩個常量都是 time.Duration 類型, 可以通過 %T 參數打印類型信息: +一個常量的聲明也可以包含一個類型和一個值,但是如果沒有顯式指明類型,那麽將從右邊的表達式推斷類型。在下面的代碼中,time.Duration是一個命名類型,底層類型是int64,time.Minute是對應類型的常量。下面聲明的兩個常量都是time.Duration類型,可以通過%T參數打印類型信息: ```Go const noDelay time.Duration = 0 @@ -44,7 +43,7 @@ fmt.Printf("%T %[1]v\n", timeout) // "time.Duration 5m0s fmt.Printf("%T %[1]v\n", time.Minute) // "time.Duration 1m0s" ``` -如果是批量聲明的常量, 除了第一個外其他常量的右邊的表發生可以省略, 如果省略則表示使用前面的表達式, 對應的常量類型也一樣. 例如: +如果是批量聲明的常量,除了第一個外其它的常量右邊的初始化表達式都可以省略,如果省略初始化表達式則表示使用前面常量的初始化表達式寫法,對應的常量類型也一樣的。例如: ```Go const ( @@ -57,7 +56,7 @@ const ( fmt.Println(a, b, c, d) // "1 1 2 2" ``` -如果隻是簡單地複製右邊的常量表達式, 併沒有太實用的價值. 但是它可以帶來其他的特性, 那就是 iota 常量生成器. +如果隻是簡單地複製右邊的常量表達式,其實併沒有太實用的價值。但是它可以帶來其它的特性,那就是iota常量生成器語法。 {% include "./ch3-06-1.md" %} diff --git a/ch3/ch3.md b/ch3/ch3.md index ce442a0..e579528 100644 --- a/ch3/ch3.md +++ b/ch3/ch3.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章介紹接口類型。 \ No newline at end of file +Go語言將數據類型分爲四類:基礎類型、複合類型、引用類型和接口類型。本章介紹基礎類型,包括:數字、字符串和布爾型。複合數據類型——數組(§4.1)和結構體(§4.2)——是通過組合簡單類型,來表達更加複雜的數據結構。引用類型包括指針(§2.3.2)、切片(§4.2))字典(§4.3)、函數(§5)、通道(§8),雖然數據種類很多,但它們都是對程序中一個變量或狀態的間接引用。這意味着對任一引用類型數據的脩改都會影響所有該引用的拷貝。我們將在第7章介紹接口類型。 diff --git a/ch4/ch4-01.md b/ch4/ch4-01.md index f77d688..f71056f 100644 --- a/ch4/ch4-01.md +++ b/ch4/ch4-01.md @@ -1,3 +1,128 @@ ## 4.1. 數組 -TODO +數組是一個固定長度的特定類型元素組成的序列,有零個或多個元素。因爲數組的長度是固定的,因此在Go語言中很少直接使用數組。Slice(切片),是可以增長和收縮動態序列,slice功能也更靈活,但是要理解slice工作原理的話需要先理解數組。 + +數組的每個元素可以通過下標來訪問,下標的范圍是從0開始到數組長度減1的位置。內置的len函數將返迴數組中元素的個數。 + +```Go +var a [3]int // array of 3 integers +fmt.Println(a[0]) // print the first element +fmt.Println(a[len(a)-1]) // print the last element, a[2] + +// Print the indices and elements. +for i, v := range a { + fmt.Printf("%d %d\n", i, v) +} + +// Print the elements only. +for _, v := range a { + fmt.Printf("%d\n", v) +} +``` + +默認情況下,數組的每個元素都被初始化爲元素類型對應的零值,對於數字類型來説就是0。我們可以使用數組字面值語法用一組值來初始化數組: + +```Go +var q [3]int = [3]int{1, 2, 3} +var r [3]int = [3]int{1, 2} +fmt.Println(r[2]) // "0" +``` + +在數組字面值中,如果數組的長度位置出現的是“...”省略號,則表示數組的長度是根據初始化值的個數來計算的。上面q數組的定義可以簡化爲 + +```Go +q := [...]int{1, 2, 3} +fmt.Printf("%T\n", q) // "[3]int" +``` + +數組的長度是數組類型的一個組成部分,因此[3]int和[4]int是兩種不同的數組類型。數組的長度必鬚是常量表達式,因爲數組的長度需要在程序的編譯階段確定。 + +```Go +q := [3]int{1, 2, 3} +q = [4]int{1, 2, 3, 4} // compile error: cannot assign [4]int to [3]int +``` + +我們將會發現,數組、slice、map和結構體字面值的寫法都很相似。上面的形式是直接提順序供初始化值序列,但是也可以指定一個索引和對應值的列表初始化,像下面這樣: + +```Go +type Currency int + +const ( + USD Currency = iota // 美元 + EUR // 歐元 + GBP // 英鎊 + RMB // 人民幣 +) + +symbol := [...]string{USD: "$", EUR: "€", GBP: "£", RMB: "¥"} + +fmt.Println(RMB, symbol[RMB]) // "3 "" +``` + +這種形式的數組字面值形式中,初始化索引的順序是無關緊要的,而且一些索引可以省略,和前面提到的規則一樣,未知道初始值的元素將用零值初始化。例如, + +```Go +r := [...]int{99: -1} +``` + +定義了一個含有100個原始的數組r,最後一個元素初始化爲-1,其它元素都是用0初始化。 + +如果一個數組的元素類型是可以相互比較的,那麽數組類型也是可以相互比較的,因此我們可以直接通過==比較運算符來比較兩個數組,隻有當兩個數組的所有元素都是相等的時候數組才是相等的。相對的是不相等比較運算符!=。 + +```Go +a := [2]int{1, 2} +b := [...]int{1, 2} +c := [2]int{1, 3} +fmt.Println(a == b, a == c, b == c) // "true false false" +d := [3]int{1, 2} +fmt.Println(a == d) // compile error: cannot compare [2]int == [3]int +``` + +作爲一個更可信的例子,crypto/sha256包的Sum256函數用於生成一個針對任意的字節類型的slice消息的摘要。消息摘要有256bit大小,因此對應[32]byte數組類型。如果兩個消息摘要是相同的,那麽可以認爲兩個消息本身也是相同(譯註:理論上有HASH碼碰撞的清空,但是實際應用可以基本忽略);如果消息摘要不同,那麽消息本身比如也是不同的。下面的例子用SHA256算法分别生成“x”和“X”兩個信息的摘要: + +```Go +gopl.io/ch4/sha256 + +import "crypto/sha256" + +func main() { + c1 := sha256.Sum256([]byte("x")) + c2 := sha256.Sum256([]byte("X")) + fmt.Printf("%x\n%x\n%t\n%T\n", c1, c2, c1 == c2, c1) + // Output: + // 2d711642b726b04401627ca9fbac32f5c8530fb1903cc4db02258717921a4881 + // 4b68ab3847feda7d6c62c1fbcbeebfa35eab7351ed5e78f4ddadea5df64b8015 + // false + // [32]uint8 +} +``` + +兩個消息雖然隻有一個字符的差異,但是生成的消息摘要則幾乎有一半的bit位是不同的。需要註意Printf函數的%x參數,它用於指定以十六進製的格式打印全部的數組或slice的的元素,%t參數是用於打印布爾型數據,%T參數是用於顯示一個值對應的數據類型。 + +當調用一個函數的時候,函數的每個調用參數將會被賦值給函數本身的參數變量,所以函數參數變量接收的是一個複製的副本,併不是原始調用的參數。因爲函數參數傳遞的機製導致傳遞大的數組類型將是低效的,併且對數組參數的任何的脩改都是發生在複製的數組上,併不是直接脩改調用時原始的數組變量。在這個方面,Go語言對待數組的方式和其它很多編程語言不同,其它編程語言可能會隱式地將數組作爲引用或指針對象傳入被調用的函數。 + +當然,我們可以顯式地傳入一個數組指針,那樣的話函數對數組的任何脩改都可以直接反饋到調用者。下面的函數用於給[32]byte類型的數組清零: + +```Go +func zero(ptr *[32]byte) { + for i := range ptr { + ptr[i] = 0 + } +} +``` + +其實數組字面值[32]byte{}就可以生成一個32字節的數組。而且每個數組的元素都是零值初始化,也就是0。我們可以將上面的zero函數寫的更簡潔一點: + +```Go +func zero(ptr *[32]byte) { + *ptr = [32]byte{} +} +``` + +雖然通過指針來傳遞數組參數是高效的,而且也允許在函數內部脩改數組的值,但是因爲數組依然是殭化的類型,因爲數組的類型包含長度信息。zero函數併不能接收指向[16]byte類型數組的指針,而且也沒有任何添加或刪除數組元素的方法。由於這些原因,除了像SHA256這類需要處理特定大小數組的函數外,數組依然很少用作函數參數;相反,我們一般使用slice來替代數組。 + +**練習 4.1:** 編寫一個函數,計算兩個SHA256碼中不同bit的數目。(參考2.6.2節的PopCount函數。) + +**練習 4.2:** 編寫一個程序,默認打印標準輸入的以SHA256哈希碼,也可以通過命令行標準參數選擇SHA384或SHA512哈希算法。 + + diff --git a/ch4/ch4-02-1.md b/ch4/ch4-02-1.md new file mode 100644 index 0000000..8c1302b --- /dev/null +++ b/ch4/ch4-02-1.md @@ -0,0 +1,123 @@ +### 4.2.1. append函數 + +內置的append函數用於向slice追加元素: + +```Go +var runes []rune +for _, r := range "Hello, 世界" { + runes = append(runes, r) +} +fmt.Printf("%q\n", runes) // "['H' 'e' 'l' 'l' 'o' ',' ' ' '世' '界']" +``` + +在循環中使用append函數構建一個有九個rune字符構成的slice,當然對應這個特殊的問題我們可以通過Go語言內置的[]rune("Hello, 世界")轉換操作完成。 + +append函數對於理解slice底層是如何工作的非常重要,所以讓我們仔細査看究竟是發生了什麽。下面是第一個版本的appendInt函數,專門用於處理[]int類型的slice: + +```Go +gopl.io/ch4/append + +func appendInt(x []int, y int) []int { + var z []int + zlen := len(x) + 1 + if zlen <= cap(x) { + // There is room to grow. Extend the slice. + z = x[:zlen] + } else { + // There is insufficient space. Allocate a new array. + // Grow by doubling, for amortized linear complexity. + zcap := zlen + if zcap < 2*len(x) { + zcap = 2 * len(x) + } + z = make([]int, zlen, zcap) + copy(z, x) // a built-in function; see text + } + z[len(x)] = y + return z +} +``` + +每次調用appendInt函數,必鬚先檢測slice底層數組是否有足夠的容量來保存新添加的元素。如果有足夠空間的話,直接擴展slice(依然在原有的底層數組之上),將新添加的y元素複製到新擴展的空間,併返迴slice。因此,輸入的x和輸出的z共享相同的底層數組。 + +如果沒有足夠的增長空間的話,appendInt函數則會先分配一個足夠大的slice用於保存新的結果,先將輸入的x複製到新的空間,然後添加y元素。結果z和輸入的x引用的將是不同的底層數組。 + +雖然通過循環複製元素更直接,不過內置的copy函數可以方便地將一個slice複製另一個相同類型的slice。copy函數的第一個參數是要複製的目標slice,第二個參數是源slice,目標和源的位置順序和dst = src賦值語句是一致的。兩個slice可以共享同一個底層數組,甚至有重疊也沒有問題。copy函數將返迴成功複製的元素的個數(我們這里沒有用到),等於兩個slice中較小的長度,所以我們不用擔心覆蓋會超出目的slice的范圍。 + +爲了效率,新分配的數組一般略大於保存x和y所需要的最低大小。通過在每次擴展數組時直接將長度翻倍從而避免了多次內存分配,也確保了添加單個元素操的平均時間是一個常數時間。這個程序演示了效果: + +```Go +func main() { + var x, y []int + for i := 0; i < 10; i++ { + y = appendInt(x, i) + fmt.Printf("%d cap=%d\t%v\n", i, cap(y), y) + x = y + } +} +``` + +每一次容量的變化都會導致重新分配內存和copy操作: + +``` +0 cap=1 [0] +1 cap=2 [0 1] +2 cap=4 [0 1 2] +3 cap=4 [0 1 2 3] +4 cap=8 [0 1 2 3 4] +5 cap=8 [0 1 2 3 4 5] +6 cap=8 [0 1 2 3 4 5 6] +7 cap=8 [0 1 2 3 4 5 6 7] +8 cap=16 [0 1 2 3 4 5 6 7 8] +9 cap=16 [0 1 2 3 4 5 6 7 8 9] +``` + +讓我們仔細査看i=3次的迭代。當時x包含了[0 1 2]三個元素,但是容量是4,因此可以簡單將新的元素添加到末尾,不需要新的內存分配。然後新的y的長度和容量都是4,併且和x引用着相同的底層數組,如圖4.2所示。 + +![](../images/ch4-02.png) + +在下一次迭代時i=4,現在沒有新的空餘的空間了,因此appendInt函數分配一個容量爲8的底層數組,將x的4個元素[0 1 2 3]複製到新空間的開頭,然後添加新的元素i,新元素的值是4。新的y的長度是5,容量是8;後面有3個空閒的位置,三次迭代都不需要分配新的空間。當前迭代中,y和x是對應不用底層數組的view。這次操作如圖4.3所示。 + +![](../images/ch4-03.png) + +內置的append函數可能使用比appendInt更複雜的內存擴展策略。因此,通常我們併不知道append調用是否導致了內存的分配,因此我們也不能確認新的slice和原始的slice是否引用的是相同的底層數組空間。同樣,我們不能確認在原先的slice上的操作是否會影響到新的slice。因此,通常是將append返迴的結果直接賦值給輸入的slice變量: + +```Go +runes = append(runes, r) +``` + +更新slice變量不僅對調用append函數是必要的,實際上對應任何可能導致長度、容量或底層數組變化的操作都是必要的。要正確地使用slice,需要記住盡管底層數組的元素是間接訪問,但是slice本身的指針、長度和容量是直接訪問的。要更新這些信息需要像上面例子那樣一個顯式的賦值操作。從這個角度看,slice併不是一個純粹的引用類型,它實際上是一個類似下面結構體的聚合類型: + +```Go +type IntSlice struct { + ptr *int + len, cap int +} +``` + +我們的appendInt函數每次隻能向slice追加一個元素,但是內置的append函數則可以追加多個元素,甚至追加一個slice。 + +```Go +var x []int +x = append(x, 1) +x = append(x, 2, 3) +x = append(x, 4, 5, 6) +x = append(x, x...) // append the slice x +fmt.Println(x) // "[1 2 3 4 5 6 1 2 3 4 5 6]" +``` + +通過下面的小脩改,我們可以可以達到append函數類似的功能。其中在appendInt函數參數中的最後的“...”省略號表示接收變長的參數爲slice。我們將在5.7節詳細解釋這個特性。 + +```Go +func appendInt(x []int, y ...int) []int { + var z []int + zlen := len(x) + len(y) + // ...expand z to at least zlen... + copy(z[len(x):], y) + return z +} +``` + +爲了避免重複,和前面相同的代碼併沒有顯示。 + + diff --git a/ch4/ch4-02-2.md b/ch4/ch4-02-2.md new file mode 100644 index 0000000..8455f40 --- /dev/null +++ b/ch4/ch4-02-2.md @@ -0,0 +1,108 @@ +### 4.2.2. Slice內存技巧 + +讓我們看看更多的例子,比如鏇轉slice、反轉slice或在slice原有內存空間脩改元素。給定一個字符串列表,下面的nonempty函數將在原有slice內存空間之上返迴不包含空字符串的列表: + +```Go +gopl.io/ch4/nonempty + +// Nonempty is an example of an in-place slice algorithm. +package main + +import "fmt" + +// nonempty returns a slice holding only the non-empty strings. +// The underlying array is modified during the call. +func nonempty(strings []string) []string { + i := 0 + for _, s := range strings { + if s != "" { + strings[i] = s + i++ + } + } + return strings[:i] +} +``` + +比較微妙的地方是,輸入的slice和輸出的slice共享一個底層數組。這可以避免分配另一個數組,不過原來的數據將可能會被覆蓋,正如下面兩個打印語句看到的那樣: + +```Go +data := []string{"one", "", "three"} +fmt.Printf("%q\n", nonempty(data)) // `["one" "three"]` +fmt.Printf("%q\n", data) // `["one" "three" "three"]` +``` + +因此我們通常會這樣使用nonempty函數:data = nonempty(data)。 + +nonempty函數也可以使用append函數實現: + +```Go +func nonempty2(strings []string) []string { + out := strings[:0] // zero-length slice of original + for _, s := range strings { + if s != "" { + out = append(out, s) + } + } + return out +} +``` + +無論如何實現,以這種方式重用一個slice一般要求最多爲每個輸入值産生一個輸出值,事實上很多算法都是用來過濾或合併序列中相鄰的元素。這種slice用法是比較複雜的技巧,雖然使用到了slice的一些黑魔法,但是對於某些場合是比較清晰和有效的。 + +一個slice可以原來實現一個stack。最初給定的空slice對應一個空的stack,然後可以使用append函數將新的值壓入stack: + +```Go +stack = append(stack, v) // push v +``` + +stack的頂部位置對應slice的最後一個元素: + +```Go +top := stack[len(stack)-1] // top of stack +``` + +通過收縮stack可以彈出棧頂的元素 + +```Go +stack = stack[:len(stack)-1] // pop +``` + +要刪除slice中間的某個元素併保存原有的元素順序,可以通過內置的copy函數將後面的子slice向前一位複雜完成: + +```Go +func remove(slice []int, i int) []int { + copy(slice[i:], slice[i+1:]) + return slice[:len(slice)-1] +} + +func main() { + s := []int{5, 6, 7, 8, 9} + fmt.Println(remove(s, 2)) // "[5 6 8 9]" +} +``` + +如果刪除元素後不用保存原來順序的話,我們可以簡單的用最後一個元素覆蓋被刪除的元素: + +```Go +func remove(slice []int, i int) []int { + slice[i] = slice[len(slice)-1] + return slice[:len(slice)-1] +} + +func main() { + s := []int{5, 6, 7, 8, 9} + fmt.Println(remove(s, 2)) // "[5 6 9 8] +} +``` + +**練習 4.3:** 重寫reverse函數,使用數組指針代替slice。 + +**練習 4.4:** 編寫一個rotate函數,通過一次循環完成鏇轉。 + +**練習 4.5:** 寫一個函數在原地完成消除[]string中相鄰重複的字符串的操作。 + +**練習 4.6:** 編寫一個函數,原地將一個UTF-8編碼的[]byte類型的slice中相鄰的空格(參考unicode.IsSpace)替換成一個空格返迴 + +**練習 4.7:** 脩改reverse函數用於原地反轉UTF-8編碼的[]byte。是否可以不用分配額外的內存? + diff --git a/ch4/ch4-02.md b/ch4/ch4-02.md index 8bf050b..063173b 100644 --- a/ch4/ch4-02.md +++ b/ch4/ch4-02.md @@ -1,3 +1,132 @@ -## 4.2. 切片 +## 4.2. Slice + +Slice(切片)代表變長的序列,序列中每個元素都有相同的類型。一個slice類型一般寫作[]T,其中T代表slice中元素的類型;語法和數組很像隻是沒有長度而已。 + +數組和slice之間有着緊密的聯繫。一個slice是一個輕量級的數據結果,提供了訪問數組子序列(或者全部)元素的功能,因爲slice的底層確實引用一個數組對象。一個slice有三個部分構成:指針、長度和容量。指針指向第一個元素對應的底層數組元素的地址,slice的第一個元素併不一定就是數組的第一個元素。長度對應slice中元素的數目;長度不能超過容量,容量一般是從slice的開始位置到底層數據的結尾位置。內置的len和cap函數分别返迴slice的長度和容量。 + +多個slice之間可以共享底層的數據,併且引用的數組部分區間可能重疊。圖4.1顯示了表示一年中每個月份名字的字符串數組,還有重疊引用了該數組的兩個slice。數組這樣定義 + +```Go +months := [...]string{1: "January", /* ... */, 12: "December"} +``` + +因此一月份是months[1],十二月份是months[12]。通常,數組的第一個元素從索引0開始,但是月份一般是從1開始的,因此我們聲明數組時直接第0個元素,第0個元素會被自動初始化爲空字符串。 + +slice的操作s[i:j],其中0 ≤ i≤ j≤ cap(s),用於創建一個新的slice,引用s的從第i個元素開始到第j-1個元素的子序列。新的slice將隻有j-i個元素。如果i位置的索引被省略的話將使用0代替,如果j位置的索引被省略的話將使用len(s)代替。因此,months[1:13]切片操作將引用全部有效的月份,和months[1:]操作等價;months[:]切片操作則是引用整個數組。讓我們分别定義表示第二季度和北方夏天的slice,它們有重疊部分: + +![](../images/ch4-01.png) + +```Go +Q2 := months[4:7] +summer := months[6:9] +fmt.Println(Q2) // ["April" "May" "June"] +fmt.Println(summer) // ["June" "July" "August"] +``` + +兩個slice都包含了六月份,下面的代碼是一個包含相同月份的測試(性能較低): + +```Go +for _, s := range summer { + for _, q := range Q2 { + if s == q { + fmt.Printf("%s appears in both\n", s) + } + } +} +``` + +如果切片操作長處cap(s)的上限將導致一個panic異常,但是超出len(s)則是擴展了slice,因此新slice的長度會變大: + +```Go +fmt.Println(summer[:20]) // panic: out of range + +endlessSummer := summer[:5] // extend a slice (within capacity) +fmt.Println(endlessSummer) // "[June July August September October]" +``` + +另外,字符串的切片操作和[]byte字節類型切片的切片操作是類似的。它們都寫作x[m:n],併且都是返迴一個原始字節繫列的子序列,底層都是共享之前的底層數組,因此切片操作對應常量時間複雜度。x[m:n]切片操作對於字符串則生成一個新字符串,如果x是[]byte的話則生成一個新的[]byte。 + +因爲slice值包含指向第一個元素的指針,因此向函數傳遞slice將運行在函數內部脩改底層數組的元素。換句話説,複雜一個slice隻是對底層的數組創建了一個新的slice别名(§2.3.2)。下面的reverse函數在原內存空間將[]int類型的slice反轉,而且它可以用於任意長度的slice。 + +```Go +gopl.io/ch4/rev + +// reverse reverses a slice of ints in place. +func reverse(s []int) { + for i, j := 0, len(s)-1; i < j; i, j = i+1, j-1 { + s[i], s[j] = s[j], s[i] + } +} +``` + +這里我們反轉數組的應用: + +```Go +a := [...]int{0, 1, 2, 3, 4, 5} +reverse(a[:]) +fmt.Println(a) // "[5 4 3 2 1 0]" +``` + +一種將slice元素循環向左鏇轉n個元素的方法是三次調用reverse反轉函數,第一次是反轉開頭的n個元素,然後是反轉剩下的元素,最後是反轉整個slice的元素。(如果是向右循環鏇轉,則將第三個函數調用移到第一個調用位置就可以了。) + +```Go +s := []int{0, 1, 2, 3, 4, 5} +// Rotate s left by two positions. +reverse(s[:2]) +reverse(s[2:]) +reverse(s) +fmt.Println(s) // "[2 3 4 5 0 1]" +``` + +要註意的是slice類型的變量s和數組類型的變量a的初始化語法的差異。slice和數組的字面值語法很類似,它們都是用花括弧包含一繫列的初始化元素,但是對於slice併沒有指明序列的長度。這會隱式地創建一個合適大小的數組,然後slice的指針指向底層的數組。就像數組字面值一樣,slice的字面值也可以按順序指定初始化值序列,或者是通過索引和元素值指定,或者的兩種風格的混合語法初始化。 + +和數組不同的是,slice不能比較,因此我們不能使用==操作符來判斷兩個slice是否有相同的元素。不過標準庫提供了高度優化的bytes.Equal函數來判斷兩個字節型slice是否相等([]byte),但是對於其他類型的slice,我們必鬚自己展開每個元素進行比較: + +```Go +func equal(x, y []string) bool { + if len(x) != len(y) { + return false + } + for i := range x { + if x[i] != y[i] { + return false + } + } + return true +} +``` + +上面關於兩個slice的深度相等測試,運行的時間併不比支持==操作的數組或字符串更多,但是爲何slice卻不支持比較運算符呢?這方面有兩個原因。第一個原因,一個slice的元素是間接引用的,一個slice甚至可以包含自身。雖然有很多辦法處理這種情形,但是沒有一個是簡單有效的。 + +第二個原因,因爲slice的元素是間接引用的,一個固定值的slice在不同的時間可能包含不同的元素,因爲底層數組的元素可能會被脩改。併且Go語言中map等哈希表之類的數據結構的key隻做簡單的淺拷貝,它要求在整個聲明週期中相等的key必鬚對相同的元素。對於像指針或chan之類的引用類型,==相等測試可以判斷兩個是否是引用相同的對象。一個針對slice的淺相等測試的==操作符可能是有一定用處的,也能臨時解決map類型的key問題,但是slice和數組不同的相等測試行爲會讓人睏惑。因此,安全的做飯是直接禁止slice之間的比較操作。 + +slice唯一合法的比較是和nil比較,例如: + +```Go +if summer == nil { /* ... */ } +``` + +一個零值的slice等於nil。一個nil值的slice併沒有底層數組。一個nil值的slice的長度和容量都是0,但是也有非nil值的slice的長度和容量也是0的,例如[]int{}或make([]int, 3)[3:]。與任意類型的nil值一樣,我們可以用[]int(nil)類型轉換表達式來生成一個對應類型slice的nil值。 + +```Go +var s []int // len(s) == 0, s == nil +s = nil // len(s) == 0, s == nil +s = []int(nil) // len(s) == 0, s == nil +s = []int{} // len(s) == 0, s != nil +``` + +如果你需要測試一個slice是否是空的,使用len(s) == 0來判斷,而不是用s == nil來判斷。除了和nil相等比較外,一個nil值的slice的行爲和其它任意0産長度的slice一樣;例如reverse(nil)也是安全的。除了文檔已經明確説明的地方,所有的Go語言函數應該以相同的方式對待nil值的slice和0長度的slice。 + +內置的make函數創建一個指定元素類型、長度和容量的slice。容量部分可以省略,在這種情況下,容量將等於長度。 + +```Go +make([]T, len) +make([]T, len, cap) // same as make([]T, cap)[:len] +``` + +在底層,make創建了一個匿名的數組變量,然後返迴一個slice;隻有通過返迴的slice才能引用底層匿名的數組變量。在第一種語句中,slice是整個數組的view。在第二個語句中,slice隻引用了底層數組的前len個元素,但是容量將包含整個的數組。額外的元素是留給未來的增長用的。 + +{% include "./ch4-02-1.md" %} + +{% include "./ch4-02-2.md" %} -TODO diff --git a/ch4/ch4-03.md b/ch4/ch4-03.md index 9c57a8c..11f28b5 100644 --- a/ch4/ch4-03.md +++ b/ch4/ch4-03.md @@ -1,3 +1,296 @@ -## 4.3. 字典 +## 4.3. Map + +哈希表是一種巧妙併且實用的數據結構。它是一個無序的key/value對的集合,其中所有的key都是不同的,然後通過給定的key可以在常數時間複雜度內檢索、更新或刪除對應的value。 + +在Go語言中,一個map就是一個哈希表的引用,map類型可以寫爲map[K]V,其中K和V分别對應key和value。map中所有的key都有相同的類型,所以的value也有着相同的類型,但是key和value之間可以是不同的數據類型。其中K對應的key必鬚是支持==比較運算符的數據類型,所以map可以通過測試key是否相等來判斷是否已經存在。雖然浮點數類型也是支持相等運算符比較的,但是將浮點數用做key類型則是一個壞的想法,正如第三章提到的,最壞的情況是可能出現的NaN和任何浮點數都不相等。對於V對應的value數據類型則沒有任何的限製。 + +內置的make函數可以創建一個map: + +```Go +ages := make(map[string]int) // mapping from strings to ints +``` + +我們也可以用map字面值的語法創建map,同時還可以指定一些最初的key/value: + +```Go +ages := map[string]int{ + "alice": 31, + "charlie": 34, +} +``` + +這相當於 + +```Go +ages := make(map[string]int) +ages["alice"] = 31 +ages["charlie"] = 34 +``` + +因此,另一種創建空的map的表達式是map[string]int{}。 + +Map中的元素通過key對應的下標語法訪問: + +```Go +ages["alice"] = 32 +fmt.Println(ages["alice"]) // "32" +``` + +使用內置的delete函數可以刪除元素: + +```Go +delete(ages, "alice") // remove element ages["alice"] +``` + +所有這些操作是安全的,卽使這些元素不在map中也沒有關繫;如果一個査找失敗將返迴value類型對應的零值,例如,卽使map中不存在“bob”下面的代碼也可以正常工作,因爲ages["bob"]失敗時將返迴0。 + +```Go +ages["bob"] = ages["bob"] + 1 // happy birthday! +``` + +而且x += y和x++等簡短賦值語法也可以用在map上,所以上面的代碼可以改寫成 + +```Go +ages["bob"] += 1 +``` + +更簡單的寫法 + +```Go +ages["bob"]++ +``` + +但是map中的元素併不是一個變量,因此我們不能對map的元素進行取址操作: + +```Go +_ = &ages["bob"] // compile error: cannot take address of map element +``` + +禁止對map元素取址的原因是map可能隨着元素數量的增長而重新分配更大的內存空間,從而可能導致之前的地址無效。 + +要想遍歷map中全部的key/value對的話,可以使用range風格的for循環實現,和之前的slice遍歷語法類似。下面的迭代語句將在每次迭代時設置name和age變量,它們對應下一個鍵/值對: + +```Go +for name, age := range ages { + fmt.Printf("%s\t%d\n", name, age) +} +``` + +Map的迭代順序是不確定的,併且不同的哈希函數實現可能導致不同的遍歷順序。在實踐中,遍歷的順序是隨機的,每一次遍歷的順序都不相同。這是故意的,每次都使用隨機的遍歷順序可以強製要求程序不會依賴具體的哈希函數實現。如果要按順序遍歷key/value對,我們必鬚顯式地對key進行排序,可以使用sort包的Strings函數對字符串slice進行排序。下面是常見的處理方式: + +```Go +import "sort" + +var names []string +for name := range ages { + names = append(names, name) +} +sort.Strings(names) +for _, name := range names { + fmt.Printf("%s\t%d\n", name, ages[name]) +} +``` + +因爲我們一開始就知道names的最終大小,因此給slice分配一個合適的大小將會更有效。下面的代碼創建了一個空的slice,但是slice的容量剛好可以放下map中全部的key: + +```Go +names := make([]string, 0, len(ages)) +``` + +在上面的第一個range循環中,我們隻關心map中的key,所以我們忽略了第二個循環變量。在第二個循環中,我們隻關繫names中的名字,所以我們使用“_”空白標識符來忽略第一個循環變量,也就是迭代slice時的索引。 + +map類型的零值是nil,也就是沒有引用任何哈希表。 + +```Go +var ages map[string]int +fmt.Println(ages == nil) // "true" +fmt.Println(len(ages) == 0) // "true" +``` + +map上的大部分操作,包括査找、刪除、len和range循環都可以安全工作在nil值的map上,它們的行爲和一個空的map類似。但是向一個nil值的map存入元素將導致一個panic異常: + +```Go +ages["carol"] = 21 // panic: assignment to entry in nil map +``` + +在向map存數據前必鬚先創建map。 + +通過key作爲索引下標來訪問map將産生一個value。如果key在map中是存在的,那麽將得到與key對應的value;如果key不存在,那麽將得到value對應類型的零值,正如我們前面看到的ages["bob"]那樣。這個規則很實用,但是有時候可能需要知道對應的元素是否眞的是在map之中。例如,如果元素類型是一個數字,你可以需要區分一個已經存在的0,和不存在而返迴零值的0,可以像下面這樣測試: + +```Go +age, ok := ages["bob"] +if !ok { /* "bob" is not a key in this map; age == 0. */ } +``` + +你會經常看到將這兩個結合起來使用,像這樣: + +```Go +if age, ok := ages["bob"]; !ok { /* ... */ } +``` + +在這種場景下,map的下標語法將産生兩個值;第二個是一個布爾值,用於報告元素是否眞的存在。布爾變量一般命名爲ok,特别適合馬上用於if條件判斷部分。 + +和slice一樣,map之間也不能進行相等比較;唯一的例外是和nil進行比較。要判斷兩個map是否包含相同的key和value,我們必鬚通過一個循環實現: + +```Go +func equal(x, y map[string]int) bool { + if len(x) != len(y) { + return false + } + for k, xv := range x { + if yv, ok := y[k]; !ok || yv != xv { + return false + } + } + return true +} +``` + +要註意我們是如何用!ok來區分元素缺失和元素不同的。我們不能簡單地用xv != y[k]判斷,那樣會導致在判斷下面兩個map時産生錯誤的結果: + +```Go +// True if equal is written incorrectly. +equal(map[string]int{"A": 0}, map[string]int{"B": 42}) +``` + +Go語言中併沒有提供一個set類型,但是map中的key也是不相同的,可以用map實現類似的功能。爲了説明這一點,下面的dedup程序讀取多行輸入,但是隻打印第一次出現的行。(它是1.3節中出現的dup程序的變體。)dedup程序通過map來表示所有的輸入行所對應的set集合,以確保已經在集合存在的行不會被重複打印。 + +```Go +gopl.io/ch4/dedup + +func main() { + seen := make(map[string]bool) // a set of strings + input := bufio.NewScanner(os.Stdin) + for input.Scan() { + line := input.Text() + if !seen[line] { + seen[line] = true + fmt.Println(line) + } + } + + if err := input.Err(); err != nil { + fmt.Fprintf(os.Stderr, "dedup: %v\n", err) + os.Exit(1) + } +} +``` + +Go程序員將這種忽略value的map當作一個字符串集合,併非所有map[string]bool類型value都是無關緊要的;有一些則可能會同時包含tue和false的值。 + +有時候我們需要一個map或set的key是slice類型,但是map的key必鬚是可比較的,但是slice併不滿足這個條件。不過,我們可以通過兩個步驟繞過這個限製。第一步,定義一個輔助函數k,將slice轉爲map對應的string類型的key,確保隻有x和y相等時k(x) == k(y)才成立。然後創建一個key爲string類型的map,在每次對map操作時先用k輔助函數將slice轉化爲string類型。 + +下面的例子演示了如何使用map來記録提交相同的字符串列表的次數。它使用了fmt.Sprintf函數將字符串列表轉換爲一個字符串以用於map的key,通過%q參數忠實地記録每個字符串元素的信息: + +```Go +var m = make(map[string]int) + +func k(list []string) string { return fmt.Sprintf("%q", list) } + +func Add(list []string) { m[k(list)]++ } +func Count(list []string) int { return m[k(list)] } +``` + +使用同樣的技術可以處理任何不可比較的key類型,而不僅僅是slice類型。它對於想使用自定義key比較函數的時候也很有用,例如在比較字符串的時候忽略大小寫。同時,輔助函數k(x)也不一定是字符串類型,它可以返迴任何可比較的類型,例如整數、數組或結構體等。 + +這是map的另一個例子,下面的程序用於統計輸入中每個Unicode碼點出現的次數。雖然Unicode全部碼點的數量鉅大,但是出現在特點文檔中的字符併沒有多少,使用map可以用比較自然的方式來跟蹤那些見過字符次數。 + +```Go +gopl.io/ch4/charcount + +// Charcount computes counts of Unicode characters. +package main + +import ( + "bufio" + "fmt" + "io" + "os" + "unicode" + "unicode/utf8" +) + +func main() { + counts := make(map[rune]int) // counts of Unicode characters + var utflen [utf8.UTFMax + 1]int // count of lengths of UTF-8 encodings + invalid := 0 // count of invalid UTF-8 characters + + in := bufio.NewReader(os.Stdin) + for { + r, n, err := in.ReadRune() // returns rune, nbytes, error + if err == io.EOF { + break + } + if err != nil { + fmt.Fprintf(os.Stderr, "charcount: %v\n", err) + os.Exit(1) + } + if r == unicode.ReplacementChar && n == 1 { + invalid++ + continue + } + counts[r]++ + utflen[n]++ + } + fmt.Printf("rune\tcount\n") + for c, n := range counts { + fmt.Printf("%q\t%d\n", c, n) + } + fmt.Print("\nlen\tcount\n") + for i, n := range utflen { + if i > 0 { + fmt.Printf("%d\t%d\n", i, n) + } + } + if invalid > 0 { + fmt.Printf("\n%d invalid UTF-8 characters\n", invalid) + } +} +``` + +ReadRune方法執行UTF-8解碼併返迴三個值:解碼的rune字符的值,字符UTF-8編碼後的長度,和一個錯誤值。我們可預期的錯誤值隻有對應文件結尾的io.EOF。如果輸入的是無效的UTF-8編碼的字符,返迴的將是unicode.ReplacementChar無效字符,併且編碼長度是1。 + +charcount程序同時打印不同UTF-8編碼長度的字符數目。對此,map併不是一個合適的數據結構;因爲UTF-8編碼的長度總是從1到utf8.UTFMax(最大是4個字節),使用數組將更有效。 + +作爲一個實驗,我們用charcount程序對本身的字符進行了統計。雖然大部分是英語,但是也有一些非ASCII字符。下面是排名前10的非ASCII字符: + +![](../images/ch4-xx-01.png) + +下面是不同UTF-8編碼長度的字符的數目: + +``` +len count +1 765391 +2 60 +3 70 +4 0 +``` + +Map的value類型也可以是一個聚合類型,比如是一個map或slice。在下面的代碼中,圖graph的key類型是一個字符串,value類型map[string]bool代表一個字符串集合。從概念上將,graph將一個字符串類型的key映射到一組相關的字符串集合,它們指向新的graph的key。 + +```Go +gopl.io/ch4/graph + +var graph = make(map[string]map[string]bool) + +func addEdge(from, to string) { + edges := graph[from] + if edges == nil { + edges = make(map[string]bool) + graph[from] = edges + } + edges[to] = true +} + +func hasEdge(from, to string) bool { + return graph[from][to] +} +``` + +其中addEdge函數惰性初始化map是一個慣用方式,也就是説在每個值首次作爲key是才初始化。addEdge函數顯示了如何讓map的零值也能正常工作;卽使from到to的邊不存在,graph[from][to]依然可以返迴一個有意義的結果。 + +**練習 4.8:** 脩改charcount程序,使用unicode.IsLetter等相關的函數,統計字母、數字等Unicode中不同的字符類别。 + +**練習 4.9:** 編寫一個程序wordfreq程序,報告輸入文本中每個單詞出現的頻率。在第一次調用Scan前先調用input.Split(bufio.ScanWords)函數,這樣可以按單詞而不是按行輸入。 + -TODO diff --git a/ch4/ch4-04-1.md b/ch4/ch4-04-1.md new file mode 100644 index 0000000..2be858f --- /dev/null +++ b/ch4/ch4-04-1.md @@ -0,0 +1,76 @@ +### 4.4.1. 結構體面值 + +結構體值可以用結構體面值表示,結構體面值可以指定每個成員的值。 + +```Go +type Point struct{ X, Y int } + +p := Point{1, 2} +``` + +這里有兩種形式的結構體面值語法,上面的是第一種寫法,要求以結構體成員定義的順序爲每個結構體成員指定一個面值。它要求寫代碼和讀代碼的人要記住結構體的每個成員的類型和順序,併且結構體成員有細微的調整就可能導致上述代碼不能編譯。因此,上述的語法一般隻在定義結構體的包內部使用,或者是在較小的結構體中使用,這些結構體的成員排列比較規則,比如image.Point{x, y}或color.RGBA{red, green, blue, alpha}。 + +其實更常用的是第二種寫法,以成員名字和相應的值來初始化,可以包含部分或全部的成員,如1.4節的Lissajous程序的寫法: + +```Go +anim := gif.GIF{LoopCount: nframes} +``` + +在這種形式的結構體面值寫法中,如果成員被忽略的話將默認用零值。因爲,提供了成員的名字,所有成員出現的順序併不重要。 + +兩種不同形式的寫法不能混合使用。而且,你不能企圖在外部包中用第一種順序賦值的技巧來偷偷地初始化結構體中未導出的成員。 + +```Go +package p +type T struct{ a, b int } // a and b are not exported + +package q +import "p" +var _ = p.T{a: 1, b: 2} // compile error: can't reference a, b +var _ = p.T{1, 2} // compile error: can't reference a, b +``` + +雖然上面最後一行代碼的編譯錯誤信息中併沒有顯式提到未導出的成員,但是這樣企圖隱式使用未導出成員的行爲也是不允許的。 + +結構體可以作爲函數的參數和返迴值。例如,這個Scale函數將Point類型的值縮放後返迴: + +```Go +func Scale(p Point, factor int) Point { + return Point{p.X * factor, p.Y * factor} +} + +fmt.Println(Scale(Point{1, 2}, 5)) // "{5 10}" +``` + +如果考慮效率的話,較大的結構體通常會用指針的方式傳入和返迴, + +```Go +func Bonus(e *Employee, percent int) int { + return e.Salary * percent / 100 +} +``` + +如果要在函數內部脩改結構體成員的話,用指針傳入是必鬚的;因爲在Go語言中,所有的函數參數都是值拷貝出入的,函數參數將不再是函數調用時的原始變量。 + +```Go +func AwardAnnualRaise(e *Employee) { + e.Salary = e.Salary * 105 / 100 +} +``` + +因爲結構體通常通過指針處理,可以用下面的寫法來創建併初始化一個結構體變量,併返迴結構體的地址: + +```Go +pp := &Point{1, 2} +``` + +它是下面的語句是等價的 + +```Go +pp := new(Point) +*pp = Point{1, 2} +``` + +不過&Point{1, 2}寫法可以直接在表達式中使用,比如一個函數調用。 + + diff --git a/ch4/ch4-04-2.md b/ch4/ch4-04-2.md new file mode 100644 index 0000000..0a16e24 --- /dev/null +++ b/ch4/ch4-04-2.md @@ -0,0 +1,25 @@ +### 4.4.2. 結構體比較 + +如果結構體的全部成員都是可以比較的,那麽結構體也是可以比較的,那樣的話兩個結構體將可以使用==或!=運算符進行比較。相等比較運算符==將比較兩個結構體的每個成員,因此下面兩個比較的表達式是等價的: + +```Go +type Point struct{ X, Y int } + +p := Point{1, 2} +q := Point{2, 1} +fmt.Println(p.X == q.X && p.Y == q.Y) // "false" +fmt.Println(p == q) // "false" +``` + +可比較的結構體類型和其他可比較的類型一樣,可以用於map的key類型。 + +```Go +type address struct { + hostname string + port int +} + +hits := make(map[address]int) +hits[address{"golang.org", 443}]++ +``` + diff --git a/ch4/ch4-04-3.md b/ch4/ch4-04-3.md new file mode 100644 index 0000000..25ad8e4 --- /dev/null +++ b/ch4/ch4-04-3.md @@ -0,0 +1,127 @@ +### 4.4.3. 結構體嵌入和匿名成員 + +在本節中,我們將看到如果使用Go語言提供的不同尋常的結構體嵌入機製讓一個命名的結構體包含另一個結構體類型的匿名成員,這樣就可以通過簡單的點運算符x.f來訪問匿名成員鏈中嵌套的x.d.e.f成員。 + +考慮一個二維的繪圖程序,提供了一個各種圖形的庫,例如矩形、橢圓形、星形和輪形等幾何形狀。這里是其中兩個的定義: + +```Go +type Circle struct { + X, Y, Radius int +} + +type Wheel struct { + X, Y, Radius, Spokes int +} +``` + +一個Circle代表的圓形類型包含了標準圓心的X和Y坐標信息,和一個Radius表示的半徑信息。一個Wheel輪形除了包含Circle類型所有的全部成員外,還增加了Spokes表示徑向輻條的數量。我們可以這樣創建一個wheel變量: + +```Go +var w Wheel +w.X = 8 +w.Y = 8 +w.Radius = 5 +w.Spokes = 20 +``` + +隨着庫中幾何形狀數量的增多,我們一定會註意到它們之間的相似和重複之處,所以我們可能爲了便於維護而將相同的屬性獨立出來: + +```Go +type Point struct { + X, Y int +} + +type Circle struct { + Center Point + Radius int +} + +type Wheel struct { + Circle Circle + Spokes int +} +``` + +這樣改動之後結構體類型變的清晰了,但是這種脩改同時也導致了訪問每個成員變得繁瑣: + +```Go +var w Wheel +w.Circle.Center.X = 8 +w.Circle.Center.Y = 8 +w.Circle.Radius = 5 +w.Spokes = 20 +``` + +Go語言有一個特性讓我們隻聲明一個成員對應的數據類型而不指名成員的名字;這類成員就叫匿名成員。匿名成員的數據類型必鬚是命名的類型或指向一個命名的類型的指針。下面的代碼中,Circle和Wheel各自都有一個匿名成員。我們可以説Point類型被嵌入了Circle結構體,同時Circle類型被嵌入了Wheel結構體。 + +```Go +type Circle struct { + Point + Radius int +} + +type Wheel struct { + Circle + Spokes int +} +``` + +得意於匿名嵌入的特性,我們可以直接訪問葉子屬性而不需要給出完整的路徑: + +```Go +var w Wheel +w.X = 8 // equivalent to w.Circle.Point.X = 8 +w.Y = 8 // equivalent to w.Circle.Point.Y = 8 +w.Radius = 5 // equivalent to w.Circle.Radius = 5 +w.Spokes = 20 +``` + +在右邊的註釋中給出的顯式形式訪問這些葉子成員的語法依然有效,因此匿名成員併不是眞的無法訪問了。其中匿名成員Circle和Point都有自己的名字——就是命名的類型名字——但是這些名字在點操作符中是可選的。我們在訪問子成員的時候可以忽略任何匿名成員部分。 + +不幸的是,結構體字面值併沒有簡短表示匿名成員的語法, 因此下面的語句都不能編譯通過: + +```Go +w = Wheel{8, 8, 5, 20} // compile error: unknown fields +w = Wheel{X: 8, Y: 8, Radius: 5, Spokes: 20} // compile error: unknown fields +``` + +結構體字面值必鬚遵循形狀類型聲明時的結構,所以我們隻能用下面的兩種語法,它們彼此是等價的: + +```Go +gopl.io/ch4/embed + +w = Wheel{Circle{Point{8, 8}, 5}, 20} + +w = Wheel{ + Circle: Circle{ + Point: Point{X: 8, Y: 8}, + Radius: 5, + }, + Spokes: 20, // NOTE: trailing comma necessary here (and at Radius) +} + +fmt.Printf("%#v\n", w) +// Output: +// Wheel{Circle:Circle{Point:Point{X:8, Y:8}, Radius:5}, Spokes:20} + +w.X = 42 + +fmt.Printf("%#v\n", w) +// Output: +// Wheel{Circle:Circle{Point:Point{X:42, Y:8}, Radius:5}, Spokes:20} +``` + +需要註意的是Printf函數中%v參數包含的#副詞,它表示用和Go語言類似的語法打印值。對於結構體類型來説,將包含每個成員的名字。 + +因爲匿名成員也有一個隱式的名字,因此不能同時包含兩個類型相同的匿名成員,這會導致名字衝突。同時,因爲成員的名字是由其類型隱式地決定的,所有匿名成員也有可見性的規則約束。在上面的例子中,Point和Circle匿名成員都是導出的。卽使它們不導出(比如改成小寫字母開頭的point和circle),我們依然可以用簡短形式訪問匿名成員嵌套的成員 + +```Go +w.X = 8 // equivalent to w.circle.point.X = 8 +``` + +但是在包外部,因爲circle和point沒有導出不能訪問它們的成員,因此簡短語法也是禁止的。 + +到目前未知,我們看到匿名成員特性隻是對訪問嵌套成員的點運算符提供了簡短的語法醣。稍後,我們將會看到匿名成員併不要求是結構體類型;其實任何命令的類型都可以作爲結構體的匿名成員。但是爲什麽要嵌入一個沒有任何子成員類型的匿名成員類型呢? + +答案是匿名類型的方法集。簡短的點運算符語法可以用於選擇匿名成員嵌套的成員,也可以用於訪問它們的方法。實際上,外層的結構體不僅僅是獲得了匿名成員類型的所有成員,而且也獲得了該類型導出的全部的方法。這個機製可以用於將一個有簡單行爲的對象組合成有複雜行爲的對象。組合是Go語言中面向對象編程的覈心,我們將在6.3節中專門討論。 + diff --git a/ch4/ch4-04.md b/ch4/ch4-04.md index 2087337..0b850df 100644 --- a/ch4/ch4-04.md +++ b/ch4/ch4-04.md @@ -1,3 +1,143 @@ ## 4.4. 結構體 -TODO +結構體是一種聚合的數據類型,由零個或多個任意類型的值聚合成的實體。每個值稱爲結構體的成員。是用結構體的經典案例處理公司的員工信息,每個員工信息包含一個唯一的員工編號、員工的名字、家庭住址、出生日期、工作崗位、薪資、上級領導等等。所有的這些成員都需要綁定到一個實體,可以作爲一個整體單元被複製,作爲函數的參數或返迴值,或者是被存儲到數組中,等等。 + +下面兩個語句分别聲明了一個叫Employee的結構體類型,併且聲明了一個Employee類型的變量dilbert: + +```Go +type Employee struct { + ID int + Name string + Address string + DoB time.Time + Position string + Salary int + ManagerID int +} + +var dilbert Employee +``` + +dilbert結構體變量的成員可以通過點操作符訪問,比如dilbert.Name和dilbert.DoB。因爲dilbert是一個變量,它所有的成員也同樣是變量,我們可以對每個成員賦值: + +```Go +dilbert.Salary -= 5000 // demoted, for writing too few lines of code +``` + +或者是對成員取地址,然後通過指針訪問: + +```Go +position := &dilbert.Position +*position = "Senior " + *position // promoted, for outsourcing to Elbonia +``` + +點操作符也可以和指向結構體的指針一起工作: + +```Go +var employeeOfTheMonth *Employee = &dilbert +employeeOfTheMonth.Position += " (proactive team player)" +``` + +相當於下面語句 + +```Go +(*employeeOfTheMonth).Position += " (proactive team player)" +``` + +EmployeeByID函數將根據給定的員工ID返迴對應的員工信息結構體的指針。我們可以使用點操作符來訪問它里面的成員: + +```Go +func EmployeeByID(id int) *Employee { /* ... */ } + +fmt.Println(EmployeeByID(dilbert.ManagerID).Position) // "Pointy-haired boss" + +id := dilbert.ID +EmployeeByID(id).Salary = 0 // fired for... no real reason +``` + +後面的語句通過EmployeeByID返迴的結構體指針更新了Employee結構體的成員。如果將EmployeeByID函數的返迴值從`*Employee`指針類型改爲Employee值類型,那麽更新語句將不能編譯通過,因爲在賦值語句的左邊併不確定是一個變量。 + +通常一行對應一個結構體成員,成員的名字在前類型在後,不過如果相鄰的成員類型如果相同的話可以被合併到一行,就像下面的Name和Address成員那樣: + +```Go +type Employee struct { + ID int + Name, Address string + DoB time.Time + Position string + Salary int + ManagerID int +} +``` + +結構體成員的輸入順序也有重要的意義。我們也可以將Position成員合併(因爲也是字符串類型),或者是交換Name和Address出現的先後順序,那樣的話就是定義了不同的結構體類型。通常,我們隻是將相關的成員合併到一起。 + +如果結構體成員名字是以大寫字母開頭的,那麽該成員就是導出的;這是Go語言導出規則決定的。一個結構體可能同時包含導出和未導出的成員。 + +結構體類型往往是冗長的,因爲它的每個成員可能都會占一行。雖然我們每次都可以重寫整個結構體成員,但是重複會令人厭煩。因此,完整的結構體寫法通常隻在類型聲明語句的地方出現,就像Employee類型聲明語句那樣。 + +一個命名爲S的結構體類型將不能再包含S類型的成員:一個聚合的值不能包含它自身。(該限製同樣適應於數組。)但是S類型的結構體可以包含`*S`指針類型的成員,這可以讓我們創建遞歸的數據結構,比如鏈表和樹結構等。在下面的代碼中,我們使用一個二叉樹來實現一個插入排序: + +```Go +gopl.io/ch4/treesort + +type tree struct { + value int + left, right *tree +} + +// Sort sorts values in place. +func Sort(values []int) { + var root *tree + for _, v := range values { + root = add(root, v) + } + appendValues(values[:0], root) +} + +// appendValues appends the elements of t to values in order +// and returns the resulting slice. +func appendValues(values []int, t *tree) []int { + if t != nil { + values = appendValues(values, t.left) + values = append(values, t.value) + values = appendValues(values, t.right) + } + return values +} + +func add(t *tree, value int) *tree { + if t == nil { + // Equivalent to return &tree{value: value}. + t = new(tree) + t.value = value + return t + } + if value < t.value { + t.left = add(t.left, value) + } else { + t.right = add(t.right, value) + } + return t +} +``` + +結構體類型的零值是每個成員都對是零值。通常會將零值作爲最合理的默認值。例如,在bytes.Buffer類型,結構體初始值就是一個隨時可用的空緩存,還有在第9章將會講到的sync.Mutex的零值也是有效的未鎖狀態。有時候這種零值可用的特性是自然獲得的,但是也有些類型需要一些額外的工作。 + +如果結構體沒有任何成員的話就是空結構體,寫作struct{}。它的大小爲0,也不包含任何信息,但是有時候依然是有價值的。有些Go語言程序員用map帶模擬set數據結構時,用它來代替map中布爾類型的value,隻是強調key的重要性,但是因爲節約的空間有限,而且語法比較複雜,所有我們通常避免避免這樣的用法。 + +```Go +seen := make(map[string]struct{}) // set of strings +// ... +if _, ok := seen[s]; !ok { + seen[s] = struct{}{} + // ...first time seeing s... +} +``` + +{% include "./ch4-04-1.md" %} + +{% include "./ch4-04-2.md" %} + +{% include "./ch4-04-3.md" %} + diff --git a/ch4/ch4-05.md b/ch4/ch4-05.md index 276ad1d..b968d0b 100644 --- a/ch4/ch4-05.md +++ b/ch4/ch4-05.md @@ -1,3 +1,264 @@ ## 4.5. JSON -TODO +JavaScript對象表示法(JSON)是一種用於發送和接收結構化信息的標準協議。JSON併不是唯一標準協議。 XML(§7.14)、ASN.1和Google的Protocol Buffers都是類似的協議,併且有各自的特色,但是由於簡潔性、可讀性和流行程度等原因,JSON是應用最廣泛的一個。 + +Go語言對於這些標準格式的編碼和解碼都有良好的支持,由標準庫中的encoding/json、encoding/xml、encoding/asn1等包提供(譯註:Protocol Buffers的支持由 github.com/golang/protobuf 包提供),併且這類包都有着相似的API接口。本節,我們將對重要的encoding/json包的用法做個概述。 + +JSON是對JavaScript中各種值——字符串、數字、布爾值和對象——Unicode本文編碼。它可以用有效可讀的方式表示第三章的基礎數據類型和本章的數組、slice、結構體和map等聚合數據類型。 + +基本的JSON類型有數字(十進製或科學記數法)、布爾值(true或false)、字符串,其中字符串是以雙引號包含的Unicode字符序列,支持和Go語言類似的反斜槓轉義特性,不過JSON使用的是\Uhhhh轉義數字來表示一個UTF-16編碼,而不是Go語言的rune類型。 + +這些基礎類型可以通過JSON的數組和對象類型進行遞歸組合。一個JSON數組是一個有序的值序列,寫在一個方括號中併以逗號分隔;一個JSON數組可以用於編碼Go語言的數組和slice。一個JSON對象是一個字符串到值的映射,寫成以繫列的name:value對形式,用花括號包含併以逗號分隔;JSON的對象類型可以用於編碼Go語言的map類型(key類型是字符串)和結構體。例如: + +``` +boolean true +number -273.15 +string "She said \"Hello, BF\"" +array ["gold", "silver", "bronze"] +object {"year": 1980, + "event": "archery", + "medals": ["gold", "silver", "bronze"]} +``` + +考慮一個應用程序,該程序負責收集各種電影評論併提供反饋功能。它的Movie數據類型和一個典型的表示電影的值列表如下所示。(其中結構體聲明中,Year和Color成員後面的字符串面值是結構體成員Tag;我們稍後會解釋它的作用。) + +```Go +gopl.io/ch4/movie + +type Movie struct { + Title string + Year int `json:"released"` + Color bool `json:"color,omitempty"` + Actors []string +} + +var movies = []Movie{ + {Title: "Casablanca", Year: 1942, Color: false, + Actors: []string{"Humphrey Bogart", "Ingrid Bergman"}}, + {Title: "Cool Hand Luke", Year: 1967, Color: true, + Actors: []string{"Paul Newman"}}, + {Title: "Bullitt", Year: 1968, Color: true, + Actors: []string{"Steve McQueen", "Jacqueline Bisset"}}, + // ... +} +``` + +這樣的數據結構特别適合JSON格式,併且在兩種之間相互轉換也很容易。將一個Go語言中類似movies的結構體slice轉爲JSON的過程叫編組(marshaling)。編組通過調用json.Marshal函數完成: + +```Go +data, err := json.Marshal(movies) +if err != nil { + log.Fatalf("JSON marshaling failed: %s", err) +} +fmt.Printf("%s\n", data) +``` + +Marshal函數生成一個字節slice,包含很長的字符串,併且沒有空白縮進;我們將它摺行以便於顯示: + +``` +[{"Title":"Casablanca","released":1942,"Actors":["Humphrey Bogart","Ingr +id Bergman"]},{"Title":"Cool Hand Luke","released":1967,"color":true,"Ac +tors":["Paul Newman"]},{"Title":"Bullitt","released":1968,"color":true," +Actors":["Steve McQueen","Jacqueline Bisset"]}] +``` + +這種緊湊的表示形式雖然包含了全部的信息,但是很難閲讀。爲了生成便於閲讀的格式,另一個json.MarshalIndent函數將産生整齊縮進的輸出。有兩個額外的字符串參數用於表示每一行輸出的前綴和每一個層級的縮進: + +```Go +data, err := json.MarshalIndent(movies, "", " ") +if err != nil { + log.Fatalf("JSON marshaling failed: %s", err) +} +fmt.Printf("%s\n", data) +``` + +上面的代碼將産生這樣的輸出: + +```Json +[ + { + "Title": "Casablanca", + "released": 1942, + "Actors": [ + "Humphrey Bogart", + "Ingrid Bergman" + ] + }, + { + "Title": "Cool Hand Luke", + "released": 1967, + "color": true, + "Actors": [ + "Paul Newman" + ] + }, + { + "Title": "Bullitt", + "released": 1968, + "color": true, + "Actors": [ + "Steve McQueen", + "Jacqueline Bisset" + ] + } +] +``` + +在編碼時,默認使用Go語言結構體的成員名字作爲JSON的對象(通過reflect反射技術,我們將在12.6節討論)。隻有導出的結構體成員才會被編碼,這也就是我們爲什麽選擇用大寫字母開頭的成員名稱。 + +細心的讀者可能已經註意到,其中Year名字的成員在編碼後變成了released,還有Color長遠編碼後變成了小寫字母開頭的color。這是因爲構體成員Tag所導致的。一個構體成員Tag是和在編譯階段關聯到該成員的元信息字符串: + +``` +Year int `json:"released"` +Color bool `json:"color,omitempty"` +``` + +結構體的成員Tag可以是任意的字符串面值,但是通常是一繫列用空格分隔的key:"value"鍵值對序列;因爲值中含義雙引號字符,因此成員Tag一般用原生字符串面值的形式書寫。json開頭鍵對應的值用於控製encoding/json包的編碼和解碼的行爲,併且encoding/...下面其它的包也遵循這個約定。成員Tag中json對應值的第一部分用於指定JSON對象的名字,比如將Go語言中的TotalCount成員對應到JSON中的total_count對象。Color成員的Tag還帶了一個額外的omitempty選項,表示當Go語言結構體成員爲空或零值時不生成JSON對象(這里false爲零值)。果然,Casablanca是一個黑白電影,併沒有輸出Color成員。 + +編碼的逆操作是解碼,對應將JSON數據解碼爲GO語言的數據結構,Go語言中一般叫unmarshaling,通過json.Unmarshal函數完成。下面的代碼將JSON格式的電影數據解碼爲一個結構體slice,結構體中隻有Title成員。通過定義合適的Go語言數據結構,我們可以選擇性地解碼JSON中感興趣的成員。當Unmarshal返迴,slice將被隻含有Title信息值填充,其它JSON成員將被忽略。 + +```Go +var titles []struct{ Title string } +if err := json.Unmarshal(data, &titles); err != nil { + log.Fatalf("JSON unmarshaling failed: %s", err) +} +fmt.Println(titles) // "[{Casablanca} {Cool Hand Luke} {Bullitt}]" +``` + +許多web服務都提供JSON接口,通過HTTP接口發送JSON格式請求併返迴JSON格式的信息。爲了説明這一點,我們通過Github的issue査詢服務。首先,我們要定義合適的類型和常量: + +```Go +gopl.io/ch4/github +// Package github provides a Go API for the GitHub issue tracker. +// See https://developer.github.com/v3/search/#search-issues. +package github + +import "time" + +const IssuesURL = "https://api.github.com/search/issues" + +type IssuesSearchResult struct { + TotalCount int `json:"total_count"` + Items []*Issue +} + +type Issue struct { + Number int + HTMLURL string `json:"html_url"` + Title string + State string + User *User + CreatedAt time.Time `json:"created_at"` + Body string // in Markdown format +} + +type User struct { + Login string + HTMLURL string `json:"html_url"` +} +``` + +和前面一樣,卽使對應的JSON對象名是小寫字母,每個結構體的成員名也是聲明爲大小字母開頭的。因爲有些JSON成員名字和Go結構體成員名字併不相同,因此需要Go語言結構體成員Tag來指定對應的JSON名字。同樣,在解碼的時候也需要做同樣的處理,GitHub服務返迴的信息比我們定義的要多很多。 + +SearchIssues函數發出一個HTTP請求,然後解碼返迴的JSON格式的結果。因爲用戶提供的査詢條件可能包含類似`?`和`&`之類的特殊字符,爲了避免對URL造成衝突,我們用url.QueryEscape來對査詢中的特殊字符進行轉義操作。 + +```Go +gopl.io/ch4/github +package github + +import ( + "encoding/json" + "fmt" + "net/http" + "net/url" + "strings" +) + +// SearchIssues queries the GitHub issue tracker. +func SearchIssues(terms []string) (*IssuesSearchResult, error) { + q := url.QueryEscape(strings.Join(terms, " ")) + resp, err := http.Get(IssuesURL + "?q=" + q) + if err != nil { + return nil, err + } + + // We must close resp.Body on all execution paths. + // (Chapter 5 presents 'defer', which makes this simpler.) + if resp.StatusCode != http.StatusOK { + resp.Body.Close() + return nil, fmt.Errorf("search query failed: %s", resp.Status) + } + + var result IssuesSearchResult + if err := json.NewDecoder(resp.Body).Decode(&result); err != nil { + resp.Body.Close() + return nil, err + } + resp.Body.Close() + return &result, nil +} +``` + +在早些的例子中,我們使用了json.Unmarshal函數來將JSON格式的字符串解碼爲字節slice。但是這個例子中,我們使用了基於流式的解碼器json.Decoder,它可以從一個流解碼JSON數據,盡管這不是必鬚的。如您所料,還有一個針對輸出流的json.Encoder編碼對象。 + +我們調用Decode方法來填充變量。這里有多種方法可以格式化結構。下面是最簡單的一種,以一個固定寬度打印每個issue,但是在下一節我們將看到如果利用模闆來輸出複雜的格式。 + +```Go +gopl.io/ch4/issues + +// Issues prints a table of GitHub issues matching the search terms. +package main + +import ( + "fmt" + "log" + "os" + + "gopl.io/ch4/github" +) + +func main() { + result, err := github.SearchIssues(os.Args[1:]) + if err != nil { + log.Fatal(err) + } + fmt.Printf("%d issues:\n", result.TotalCount) + for _, item := range result.Items { + fmt.Printf("#%-5d %9.9s %.55s\n", + item.Number, item.User.Login, item.Title) + } +} +``` + +通過命令行參數指定檢索條件。下面的命令是査詢Go語言項目中和JSON解碼相關的問題,還有査詢返迴的結果: + +``` +$ go build gopl.io/ch4/issues +$ ./issues repo:golang/go is:open json decoder +13 issues: +#5680 eaigner encoding/json: set key converter on en/decoder +#6050 gopherbot encoding/json: provide tokenizer +#8658 gopherbot encoding/json: use bufio +#8462 kortschak encoding/json: UnmarshalText confuses json.Unmarshal +#5901 rsc encoding/json: allow override type marshaling +#9812 klauspost encoding/json: string tag not symmetric +#7872 extempora encoding/json: Encoder internally buffers full output +#9650 cespare encoding/json: Decoding gives errPhase when unmarshalin +#6716 gopherbot encoding/json: include field name in unmarshal error me +#6901 lukescott encoding/json, encoding/xml: option to treat unknown fi +#6384 joeshaw encoding/json: encode precise floating point integers u +#6647 btracey x/tools/cmd/godoc: display type kind of each named type +#4237 gjemiller encoding/base64: URLEncoding padding is optional +``` + +GitHub的Web服務接口 https://developer.github.com/v3/ 包含了更多的特性。 + +**練習 4.10:** 脩改issues程序,根據問題的時間進行分類,比如不到一個月的、不到一年的、超過一年。 + +**練習 4.11:** 編寫一個工具,允許用戶在命令行創建、讀取、更新和刪除GitHub上的issue,當必要的時候自動打開用戶默認的編輯器用於輸入文本信息。 + +**練習 4.12:** 流行的web漫畵服務xkcd也提供了JSON接口。例如,一個 https://xkcd.com/571/info.0.json 請求將返迴一個很多人喜愛的571編號的詳細描述。下載每個鏈接(隻下載一次)然後創建一個離線索引。編寫一個xkcd工具,使用這些離線索引,打印和命令行輸入的檢索詞相匹配的漫畵的URL。 + +**練習 4.13:** 使用開放電影數據庫的JSON服務接口,允許你檢索和下載 https://omdbapi.com/ 上電影的名字和對應的海報圖像。編寫一個poster工具,通過命令行輸入的電影名字,下載對應的海報。 + + diff --git a/ch4/ch4-06.md b/ch4/ch4-06.md index 88cf5e5..55cc347 100644 --- a/ch4/ch4-06.md +++ b/ch4/ch4-06.md @@ -1,3 +1,178 @@ ## 4.6. 文本和HTML模闆 -TODO +前面的例子,隻是最簡單的格式,使用Printf是完全足夠的。但是有時候會需要複雜的打印格式,這時候一般需要將格式化代碼分離出來以便更安全地脩改。這寫功能是由text/template和html/template等模闆包提供的,它們提供了一個用變量值填充到一個文本或HTML格式的模闆的機製。 + +一個模闆是一個字符串或一個文件,里面包含了一個或多個由雙花括號包含的action對象。大部分的字符串隻是按面值打印,但是對於actions部分將觸發其它的行爲。買個actions包好了一個用模闆語言書寫的表達式,一個雖然簡短但是可以輸出複雜的打印值,模闆語言包含通過選擇結構體的成員、調用函數或方法、表達式控製流if-else語句和range循環語句,還有其它實例化模闆等諸多特性。下面是一個簡單的模闆字符串: + +{% raw %} + +```Go +gopl.io/ch4/issuesreport + +const templ = `{{.TotalCount}} issues: +{{range .Items}}---------------------------------------- +Number: {{.Number}} +User: {{.User.Login}} +Title: {{.Title | printf "%.64s"}} +Age: {{.CreatedAt | daysAgo}} days +{{end}}` +``` + +{% endraw %} + +這個模闆先打印匹配到的issue總數,然後打印每個issue的編號、創建用戶、標題還有存在的時間。每一個action,都有一個當前值的概念,對應點操作符,寫作“.”。當前值“.”最初被初始化爲調用模闆是的參數,在當前例子中對應github.IssuesSearchResult類型的變量。模闆中`{{.TotalCount}}`對應action將展開爲結構體中TotalCount成員以默認的方式打印的值。模闆中`{{range .Items}}`和`{{end}}`對應一個循環action,因此它們直接的內容可能會被展開多次,循環每次迭代的當前值對應當前的Items元素的值。 + +在一個action中,`|`操作符表示將前一個表達式的結果作爲後一個函數的輸入,類似於UNIX中管道的概念。在Title這一行的action中,第二個操作是一個printf函數,是一個基於fmt.Sprintf實現的內置函數,所有模闆都可以直接使用。對於Age部分,第二個動作是一個叫daysAgo的函數,通過time.Since函數將CreatedAt成員轉換爲過去的時間長度: + +```Go +func daysAgo(t time.Time) int { + return int(time.Since(t).Hours() / 24) +} +``` + +需要註意的是CreatedAt的參數類型是time.Time,併不是字符串。以同樣的方式,我們可以通過定義一些方法來控製字符串的格式化(§2.5),一個類型同樣可以定製自己的JSON編碼和解碼行爲。time.Time類型對應的JSON值是一個標準時間格式的字符串。 + +生成模闆的輸出需要兩個處理步驟。第一步是要分析模闆併轉爲內部表示,然後基於指定的輸入執行模闆。分析模闆部分一般隻需要執行一次。下面的代碼創建併分析上面定義的模闆templ。註意方法調用鏈的順序:template.New先創建併返迴一個模闆;Funcs方法將daysAgo等自定義函數註冊到模闆中,併返迴模闆;最後調用Parse函數分析模闆。 + +```Go +report, err := template.New("report"). + Funcs(template.FuncMap{"daysAgo": daysAgo}). + Parse(templ) +if err != nil { + log.Fatal(err) +} +``` + +因爲模闆通常在編譯時就測試好了,如果模闆解析失敗將是一個致命的錯誤。template.Must輔助函數可以簡化這個致命錯誤的處理:它接受一個模闆和一個error類型的參數,檢測error是否爲nil(如果不是則發出panic異常),然後返迴傳入的模闆。我們將在5.9節再討論這個話題。 + +一旦模闆已經創建、註冊了daysAgo函數、併通過分析和檢測,我們就可以使用github.IssuesSearchResult作爲輸入源、os.Stdout作爲輸出源來執行模闆: + +```Go +var report = template.Must(template.New("issuelist"). + Funcs(template.FuncMap{"daysAgo": daysAgo}). + Parse(templ)) + +func main() { + result, err := github.SearchIssues(os.Args[1:]) + if err != nil { + log.Fatal(err) + } + if err := report.Execute(os.Stdout, result); err != nil { + log.Fatal(err) + } +} +``` + +程序輸出一個純文本報告: + +``` +$ go build gopl.io/ch4/issuesreport +$ ./issuesreport repo:golang/go is:open json decoder +13 issues: +---------------------------------------- +Number: 5680 +User: eaigner +Title: encoding/json: set key converter on en/decoder +Age: 750 days +---------------------------------------- +Number: 6050 +User: gopherbot +Title: encoding/json: provide tokenizer +Age: 695 days +---------------------------------------- +... +``` + +現在讓我們轉到html/template模闆包。它使用和text/template包相同的API和模闆語言,但是增加了一個將字符串自動轉義,以避免輸入字符串和HTML、JavaScript、CSS或URL語法産生衝突的問題。這個特性可以避免一些長期存在的安全問題,比如通過生成HTML註入攻擊,通過構造一個含有惡意代碼的問題標題,這些都可能讓模闆輸出錯誤的輸出,從而讓他們控製頁面。 + +下面的模闆以HTML格式輸出issue列表。註意import語句的不同: + +{% raw %} + +```Go +gopl.io/ch4/issueshtml + +import "html/template" + +var issueList = template.Must(template.New("issuelist").Parse(` +

{{.TotalCount}} issues

+ + + + + + + +{{range .Items}} + + + + + + +{{end}} +
#StateUserTitle
{{.Number}}{{.State}}{{.User.Login}}{{.Title}}
+`)) +``` + +{% endraw %} + +下面的命令將在新的模闆上執行一個稍微不同的査詢: + +```Go +$ go build gopl.io/ch4/issueshtml +$ ./issueshtml repo:golang/go commenter:gopherbot json encoder >issues.html +``` + +圖4.4顯示了在web瀏覽器中的效果圖。每個issue包含到Github對應頁面的鏈接。 + +![](../images/ch4-04.png) + +圖4.4中的沒有問題會對HTML格式産生衝突,但是我們馬上將看到標題中含有`&`和`<`字符的issue。下面的命令選擇了兩個這樣的issue: + +``` +$ ./issueshtml repo:golang/go 3133 10535 >issues2.html +``` + +圖4.5顯示了該査詢的結果。註意,html/template包已經自動將特殊字符轉義,我們依然可以看到正確的字面值。如果我們使用text/template包的話,這2個issue將會産生錯誤,其中“<”四個字符將會被當作小於字符“<”處理,同時“”字符串將會被當作一個鏈接元素處理,它們都會導致HTML文檔結構的改變,從而導致有未知的風險。 + +我們也可以通過對信任的HTML字符串使用template.HTML類型來抑製這種自動轉義的行爲。還有很多采用類型命名的字符串類型對應信任的JavaScript、CSS和URL。下面的程序演示了兩個使用不同類型的相同字符串産生的不同結果:A是一個普通字符串,B是一個信任的template.HTML字符串類型。 + +![](../images/ch4-05.png) + +{% raw %} + +```Go +gopl.io/ch4/autoescape + +func main() { + const templ = `

A: {{.A}}

B: {{.B}}

` + t := template.Must(template.New("escape").Parse(templ)) + var data struct { + A string // untrusted plain text + B template.HTML // trusted HTML + } + data.A = "Hello!" + data.B = "Hello!" + if err := t.Execute(os.Stdout, data); err != nil { + log.Fatal(err) + } +} +``` + +{% endraw %} + +圖4.6顯示了出現在瀏覽器中的模闆輸出。我們看到A的黑體標記被轉義失效了,但是B沒有。 + +![](../images/ch4-06.png) + +我們這里隻講述了模闆繫統中最基本的特性。一如旣往,如果想了解更多的信息,請自己査看包文檔: + +``` +$ go doc text/template +$ go doc html/template +``` + +**練習 4.14:** 創建一個web服務器,査詢一次GitHub,然後生成BUG報告、里程碑和對應的用戶信息。 + + diff --git a/ch4/ch4.md b/ch4/ch4.md index abeacf7..570022e 100644 --- a/ch4/ch4.md +++ b/ch4/ch4.md @@ -1,3 +1,6 @@ # 第四章 複合數據類型 -TODO +在第三章我們討論了基本數據類型,它們是用於構建程序中數據結構,是Go語言的世界的原子。在本章,我們將討論複合數據類型,以不同的方式組合基本類型可以構造出複合數據類型。我們主要討論四種類型——數組、slice、map和結構體——同時在本章的最後,我們將演示如何使用結構體來解碼和編碼到對應的JSON格式的數據,併且通過結合使用模闆來生成HTML頁面。 + +數組和結構體是聚合類型;它們的值由許多元素或成員的值組成。數組是由同構的元素組成——每個數組元素都有完全相同的類型——結構體則是由異構的元素組成的。數組和結構體都是固定內存大小的數據結構。相比之下,slice和map則是動態的數據結構,它們將根據需要動態增長。 + diff --git a/ch5/ch5-01.md b/ch5/ch5-01.md index 5f0fdf8..bb60e91 100644 --- a/ch5/ch5-01.md +++ b/ch5/ch5-01.md @@ -1,3 +1,60 @@ ## 5.1. 函數聲明 -TODO +函數聲明包括函數名、形式參數列表、返迴值列表(可省略)以及函數體。 + +```Go +func name(parameter-list) (result-list) { + body +} +``` + +形式參數列表描述了函數的參數名以及參數類型。這些參數作爲局部變量,其值由參數調用者提供。返迴值列表描述了函數返迴值的變量名以及類型。如果函數返迴一個無名變量或者沒有返迴值,返迴值列表的括號是可以省略的。如果一個函數聲明不包括返迴值列表,那麽函數體執行完畢後,不會返迴任何值。 +在hypot函數中, + +```Go +func hypot(x, y float64) float64 { + return math.Sqrt(x*x + y*y) +} +fmt.Println(hypot(3,4)) // "5" +``` + +x和y是形參名,3和4是調用時的傳入的實數,函數返迴了一個float64類型的值。 +返迴值也可以像形式參數一樣被命名。在這種情況下,每個返迴值被聲明成一個局部變量,併根據該返迴值的類型,將其初始化爲0。 +如果一個函數在聲明時,包含返迴值列表,該函數必鬚以 return語句結尾,除非函數明顯無法運行到結尾處。例如函數在結尾時調用了panic異常或函數中存在無限循環。 + +正如hypot一樣,如果一組形參或返迴值有相同的類型,我們不必爲每個形參都寫出參數類型。下面2個聲明是等價的: + +```Go +func f(i, j, k int, s, t string) { /* ... */ } +func f(i int, j int, k int, s string, t string) { /* ... */ } +``` + +下面,我們給出4種方法聲明擁有2個int型參數和1個int型返迴值的函數.blank identifier(譯者註:卽下文的_符號)可以強調某個參數未被使用。 + +```Go +func add(x int, y int) int {return x + y} +func sub(x, y int) (z int) { z = x - y; return} +func first(x int, _ int) int { return x } +func zero(int, int) int { return 0 } + +fmt.Printf("%T\n", add) // "func(int, int) int" +fmt.Printf("%T\n", sub) // "func(int, int) int" +fmt.Printf("%T\n", first) // "func(int, int) int" +fmt.Printf("%T\n", zero) // "func(int, int) int" +``` + +函數的類型被稱爲函數的標識符。如果兩個函數形式參數列表和返迴值列表中的變量類型一一對應,那麽這兩個函數被認爲有相同的類型和標識符。形參和返迴值的變量名不影響函數標識符也不影響它們是否可以以省略參數類型的形式表示。 + +每一次函數調用都必鬚按照聲明順序爲所有參數提供實參(參數值)。在函數調用時,Go語言沒有默認參數值,也沒有任何方法可以通過參數名指定形參,因此形參和返迴值的變量名對於函數調用者而言沒有意義。 + +在函數體中,函數的形參作爲局部變量,被初始化爲調用者提供的值。函數的形參和有名返迴值作爲函數最外層的局部變量,被存儲在相同的詞法塊中。 + +實參通過值的方式傳遞,因此函數的形參是實參的拷貝。對形參進行脩改不會影響實參。但是,如果實參包括引用類型,如指針,slice(切片)、map、function、channel等類型,實參可能會由於函數的簡介引用被脩改。 + +你可能會偶爾遇到沒有函數體的函數聲明,這表示該函數不是以Go實現的。這樣的聲明定義了函數標識符。 + +```Go +package math + +func Sin(x float64) float //implemented in assembly language +``` diff --git a/ch5/ch5-02.md b/ch5/ch5-02.md index 2cf577e..0c0f77c 100644 --- a/ch5/ch5-02.md +++ b/ch5/ch5-02.md @@ -1,3 +1,162 @@ ## 5.2. 遞歸 -TODO +函數可以是遞歸的,這意味着函數可以直接或間接的調用自身。對許多問題而言,遞歸是一種強有力的技術,例如處理遞歸的數據結構。在4.4節,我們通過遍歷二叉樹來實現簡單的插入排序,在本章節,我們再次使用它來處理HTML文件。 + +下文的示例代碼使用了非標準包 golang.org/x/net/html ,解析HTML。golang.org/x/... 目録下存儲了一些由Go糰隊設計、維護,對網絡編程、国際化文件處理、移動平台、圖像處理、加密解密、開發者工具提供支持的擴展包。未將這些擴展包加入到標準庫原因有二,一是部分包仍在開發中,二是對大多數Go語言的開發者而言,擴展包提供的功能很少被使用。 + +例子中調用golang.org/x/net/html的部分api如下所示。html.Parse函數讀入一組bytes.解析後,返迴html.node類型的HTML頁面樹狀結構根節點。HTML擁有很多類型的結點如text(文本),commnets(註釋)類型,在下面的例子中,我們 隻關註< name key='value' >形式的結點。 + +```Go +golang.org/x/net/html +package html + +type Node struct { + Type NodeType + Data string + Attr []Attribute + FirstChild, NextSibling *Node +} + +type NodeType int32 + +const ( + ErrorNode NodeType = iota + TextNode + DocumentNode + ElementNode + CommentNode + DoctypeNode +) + +type Attribute struct { + Key, Val string +} + +func Parse(r io.Reader) (*Node, error) +``` + +main函數解析HTML標準輸入,通過遞歸函數visit獲得links(鏈接),併打印出這些links: + +```Go +gopl.io/ch5/findlinks1 +// Findlinks1 prints the links in an HTML document read from standard input. +package main + +import ( + "fmt" + "os" + + "golang.org/x/net/html" +) + +func main() { + doc, err := html.Parse(os.Stdin) + if err != nil { + fmt.Fprintf(os.Stderr, "findlinks1: %v\n", err) + os.Exit(1) + } + for _, link := range visit(nil, doc) { + fmt.Println(link) + } +} +``` + +visit函數遍歷HTML的節點樹,從每一個anchor元素的href屬性獲得link,將這些links存入字符串數組中,併返迴這個字符串數組。 + +```Go +// visit appends to links each link found in n and returns the result. +func visit(links []string, n *html.Node) []string { + if n.Type == html.ElementNode && n.Data == "a" { + for _, a := range n.Attr { + if a.Key == "href" { + links = append(links, a.Val) + } + } + } + for c := n.FirstChild; c != nil; c = c.NextSibling { + links = visit(links, c) + } + return links +} +``` + +爲了遍歷結點n的所有後代結點,每次遇到n的孩子結點時,visit遞歸的調用自身。這些孩子結點存放在FirstChild鏈表中。 + +讓我們以Go的主頁(golang.org)作爲目標,運行findlinks。我們以fetch(1.5章)的輸出作爲findlinks的輸入。下面的輸出做了簡化處理。 + +``` +$ go build gopl.io/ch1/fetch +$ go build gopl.io/ch5/findlinks1 +$ ./fetch https://golang.org | ./findlinks1 +# +/doc/ +/pkg/ +/help/ +/blog/ +http://play.golang.org/ +//tour.golang.org/ +https://golang.org/dl/ +//blog.golang.org/ +/LICENSE +/doc/tos.html +http://www.google.com/intl/en/policies/privacy/ +``` + +註意在頁面中出現的鏈接格式,在之後我們會介紹如何將這些鏈接,根據根路徑( https://golang.org )生成可以直接訪問的url。 + +在函數outline中,我們通過遞歸的方式遍歷整個HTML結點樹,併輸出樹的結構。在outline內部,每遇到一個HTML元素標籤,就將其入棧,併輸出。 + +```Go +gopl.io/ch5/outline +func main() { + doc, err := html.Parse(os.Stdin) + if err != nil { + fmt.Fprintf(os.Stderr, "outline: %v\n", err) + os.Exit(1) + } + outline(nil, doc) +} +func outline(stack []string, n *html.Node) { + if n.Type == html.ElementNode { + stack = append(stack, n.Data) // push tag + fmt.Println(stack) + } + for c := n.FirstChild; c != nil; c = c.NextSibling { + outline(stack, c) + } +} +``` + +有一點值得註意:outline有入棧操作,但沒有相對應的出棧操作。當outline調用自身時,被調用者接收的是stack的拷貝。被調用者的入棧操作,脩改的是stack的拷貝,而不是調用者的stack,因對當函數返迴時,調用者的stack併未被脩改。 + +下面是 https://golang.org 頁面的簡要結構: + +``` +$ go build gopl.io/ch5/outline +$ ./fetch https://golang.org | ./outline +[html] +[html head] +[html head meta] +[html head title] +[html head link] +[html body] +[html body div] +[html body div] +[html body div div] +[html body div div form] +[html body div div form div] +[html body div div form div a] +... +``` + +正如你在上面實驗中所見,大部分HTML頁面隻需幾層遞歸就能被處理,但仍然有些頁面需要深層次的遞歸。 + +大部分編程語言使用固定大小的函數調用棧,常見的大小從64KB到2MB不等。固定大小棧會限製遞歸的深度,當你用遞歸處理大量數據時,需要避免棧溢出;除此之外,還會導致安全性問題。與相反,Go語言使用可變棧,棧的大小按需增加(初始時很小)。這使得我們使用遞歸時不必考慮溢出和安全問題。 + +練習**5.1** :脩改findlinks代碼中遍歷n.FirstChild鏈表的部分,將循環調用visit,改成遞歸調用。 + +練習**5.2** : 編寫函數,記録在HTML樹中出現的同名元素的次數。 + +練習**5.3** : 編寫函數輸出所有text結點的內容。註意不要訪問`