good good study, day day up!

pull/1/head
chai2010 2015-12-09 15:45:11 +08:00
commit 1693baf5de
378 changed files with 23276 additions and 0 deletions

16
.gitignore vendored Normal file
View File

@ -0,0 +1,16 @@
# Node rules:
## Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
.grunt
## Dependency directory
## Commenting this out is preferred by some people, see
## https://docs.npmjs.com/misc/faq#should-i-check-my-node_modules-folder-into-git
node_modules
# Book build output
_book
# eBook build output
*.epub
*.mobi
*.pdf

14
CONTRIBUTORS.md Normal file
View File

@ -0,0 +1,14 @@
# 貢獻者列錶
*大傢幫助完善, 請保證列錶有序(忽略大小寫)!*
```
chai2010 <chaishushan@gmail.com>
Xargin <cao1988228@163.com>
```
# 版權
除特彆註明外, 本站內容均採用[知識共享-署名(CC-BY) 3.0協議](http://creativecommons.org/licenses/by/3.0/)授權, 代碼遵循[Go項目的BSD協議](http://golang.org/LICENSE)授權.

1
GLOSSARY.md Normal file
View File

@ -0,0 +1 @@
# 朮語

27
LICENSE Normal file
View File

@ -0,0 +1,27 @@
Copyright (c) 2015 Golang-China. All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are
met:
* Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above
copyright notice, this list of conditions and the following disclaimer
in the documentation and/or other materials provided with the
distribution.
* Neither the name of Google Inc. nor the names of its
contributors may be used to endorse or promote products derived from
this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

18
Makefile Normal file
View File

@ -0,0 +1,18 @@
# Copyright 2015 Golang-China. All rights reserved.
# Use of this source code is governed by a BSD-style
# license that can be found in the LICENSE file.
# install gitbook
# npm install gitbook-cli -g
# https://github.com/GitbookIO/gitbook
# https://github.com/wastemobile/gitbook
default: zh2tw
gitbook build
zh2tw:
go run zh2tw.go . .md$$
tw2zh:
go run zh2tw.go . .md$$ tw2zh

34
README.md Normal file
View File

@ -0,0 +1,34 @@
# 關於 [《Go聖經讀書筆記》](http://golang-china.github.io/gopl-zh)
作為 [《The Go Programming Language》](http://gopl.io/) (中文名[《Go編程語言》](http://golang-china.github.io/gopl-zh)) 英文原版紙質圖書的購買者, [《Go聖經讀書筆記》](http://golang-china.github.io/gopl-zh) 是我們的 **讀書筆記** 和 **習題解答**, 僅供學習交流用.
- 此中文版 **讀書筆記** 在綫預覽: http://golang-china.github.io/gopl-zh
- 此中文版 **讀書筆記** 的源文件: http://github.com/golang-china/gopl-zh
- 此中文版 **讀書筆記** 項目進度: http://github.com/golang-china/gopl-zh/blob/master/progress.md
- 原版官網: http://gopl.io
[![](cover_small.jpg)](https://github.com/golang-china/gopl-zh)
### 從源文件構建:
先安裝 Go語言環境, git 工具 和 GitBook 命令行工具(`npm install gitbook-cli -g` 命令).
1. 運行 `go get github.com/golang-china/gopl-zh`, 穫取 源文件
2. 運行 `go generate github.com/golang-china/gopl-zh`, 生成 `_book` 目彔
3. 打開 `_book/index.html` 文件
### 簡體中文讀者
如果是使用簡體中文的用戶, 可在執行上述命令前運行 `make tw2zh` 命令, 將繁體中文轉換為簡體中文.
# 版權聲明
<a rel="license" href="http://creativecommons.org/licenses/by-nc-sa/4.0/">Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International License</a>.
<a rel="license" href="http://creativecommons.org/licenses/by-nc-sa/4.0/"><img alt="Creative Commons License" style="border-width:0" src="./images/by-nc-sa-4.0-88x31.png"></img></a>
嚴禁任何商業行為使用或引用該 **讀書筆記** 的全部或部分內容!
歡迎大傢提供建議!

128
SUMMARY.md Normal file
View File

@ -0,0 +1,128 @@
# Summary
* [前言](preface.md)
* [Go語言起源](ch0/ch0-01.md)
* [Go語言項目](ch0/ch0-02.md)
* [本書的組織](ch0/ch0-03.md)
* [更多的信息](ch0/ch0-04.md)
* [緻謝](ch0/ch0-05.md)
* [入門](ch1/ch1.md)
* [Hello, World](ch1/ch1-01.md)
* [命令行參數](ch1/ch1-02.md)
* [査找重復的行](ch1/ch1-03.md)
* [GIF動畫](ch1/ch1-04.md)
* [穫取URL](ch1/ch1-05.md)
* [併髮穫取多個URL](ch1/ch1-06.md)
* [Web服務](ch1/ch1-07.md)
* [本章要點](ch1/ch1-08.md)
* [程序結構](ch2/ch2.md)
* [命名](ch2/ch2-01.md)
* [聲明](ch2/ch2-02.md)
* [變量](ch2/ch2-03.md)
* [賦值](ch2/ch2-04.md)
* [類型](ch2/ch2-05.md)
* [包和文件](ch2/ch2-06.md)
* [作用域](ch2/ch2-07.md)
* [基礎數據類型](ch3/ch3.md)
* [整型](ch3/ch3-01.md)
* [浮點數](ch3/ch3-02.md)
* [復數](ch3/ch3-03.md)
* [佈爾型](ch3/ch3-04.md)
* [字符串](ch3/ch3-05.md)
* [常量](ch3/ch3-06.md)
* [復閤數據類型](ch4/ch4.md)
* [數組](ch4/ch4-01.md)
* [切片](ch4/ch4-02.md)
* [字典](ch4/ch4-03.md)
* [結構體](ch4/ch4-04.md)
* [JSON](ch4/ch4-05.md)
* [文本和HTML模闆](ch4/ch4-06.md)
* [函數](ch5/ch5.md)
* [函數聲明](ch5/ch5-01.md)
* [遞歸](ch5/ch5-02.md)
* [多返迴值](ch5/ch5-03.md)
* [錯誤](ch5/ch5-04.md)
* [函數值](ch5/ch5-05.md)
* [匿名函數](ch5/ch5-06.md)
* [可變參數](ch5/ch5-07.md)
* [Deferred函數](ch5/ch5-08.md)
* [Panic異常](ch5/ch5-09.md)
* [Recover捕穫異常](ch5/ch5-10.md)
* [方法](ch6/ch6.md)
* [方法聲明](ch6/ch6-01.md)
* [基於指鍼對象的方法](ch6/ch6-02.md)
* [通過嵌入結構體來擴展類型](ch6/ch6-03.md)
* [方法值和方法錶達式](ch6/ch6-04.md)
* [示例: Bit數組](ch6/ch6-05.md)
* [封裝](ch6/ch6-06.md)
* [接口](ch7/ch7.md)
* [接口是閤約](ch7/ch7-01.md)
* [接口類型](ch7/ch7-02.md)
* [實現接口的條件](ch7/ch7-03.md)
* [flag.Value接口](ch7/ch7-04.md)
* [接口值](ch7/ch7-05.md)
* [sort.Interface接口](ch7/ch7-06.md)
* [http.Handler接口](ch7/ch7-07.md)
* [error接口](ch7/ch7-08.md)
* [示例: 錶達式求值](ch7/ch7-09.md)
* [類型斷言](ch7/ch7-10.md)
* [基於類型斷言識彆錯誤類型](ch7/ch7-11.md)
* [通過類型斷言査詢接口](ch7/ch7-12.md)
* [類型分支](ch7/ch7-13.md)
* [示例: 基於標記的XML解碼](ch7/ch7-14.md)
* [補充幾點](ch7/ch7-15.md)
* [Goroutines和Channels](ch8/ch8.md)
* [Goroutines](ch8/ch8-01.md)
* [示例: 併髮的Clock服務](ch8/ch8-02.md)
* [示例: 併髮的Echo服務](ch8/ch8-03.md)
* [Channels](ch8/ch8-04.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-10.md)
* [基於共享變量的併髮](ch9/ch9.md)
* [競爭條件](ch9/ch9-01.md)
* [sync.Mutex互斥鎖](ch9/ch9-02.md)
* [sync.RWMutex讀寫鎖](ch9/ch9-03.md)
* [內存衕步](ch9/ch9-04.md)
* [sync.Once初始化](ch9/ch9-05.md)
* [競爭條件檢測](ch9/ch9-06.md)
* [示例: 併髮的非阻塞緩存](ch9/ch9-07.md)
* [Goroutines和綫程](ch9/ch9-08.md)
* [包和工具](ch10/ch10.md)
* [簡介](ch10/ch10-01.md)
* [導入路徑](ch10/ch10-02.md)
* [包聲明](ch10/ch10-03.md)
* [導入聲明](ch10/ch10-04.md)
* [匿名導入](ch10/ch10-05.md)
* [包和命名](ch10/ch10-06.md)
* [工具](ch10/ch10-07.md)
* [測試](ch11/ch11.md)
* [go test](ch11/ch11-01.md)
* [測試函數](ch11/ch11-02.md)
* [測試覆蓋率](ch11/ch11-03.md)
* [基準測試](ch11/ch11-04.md)
* [剖析](ch11/ch11-05.md)
* [示例函數](ch11/ch11-06.md)
* [反射](ch12/ch12.md)
* [為何需要反射?](ch12/ch12-01.md)
* [reflect.Type和reflect.Value](ch12/ch12-02.md)
* [Display遞歸打印](ch12/ch12-03.md)
* [示例: 編碼S錶達式](ch12/ch12-04.md)
* [通過reflect.Value脩改值](ch12/ch12-05.md)
* [示例: 解碼S錶達式](ch12/ch12-06.md)
* [穫取結構體字段標識](ch12/ch12-07.md)
* [顯示一個類型的方法集](ch12/ch12-08.md)
* [幾點忠告](ch12/ch12-09.md)
* [底層編程](ch13/ch13.md)
* [unsafe.Sizeof, Alignof 和 Offsetof](ch13/ch13-01.md)
* [unsafe.Pointer](ch13/ch13-02.md)
* [示例: 深度相等判斷](ch13/ch13-03.md)
* [通過cgo調用C代碼](ch13/ch13-04.md)
* [幾點忠告](ch13/ch13-05.md)
* [習題解答](exercise/ex.md)
* [第一章 入門](exercise/ex-ch1.md)
* [勘誤](errata.md)

8
book.json Normal file
View File

@ -0,0 +1,8 @@
{
"title": "Go编程语言",
"description": "<The Go Programming Language>中文版",
"language": "zh",
"structure": {
"readme": "preface.md"
}
}

20
ch0/ch0-01.md Normal file
View File

@ -0,0 +1,20 @@
## Go語言起源
就像生物物種, 一個成功的編程語言的後代一般都會繼承它們祖先的優點; 當然有時多種語言混閤也會產生令人驚訝的特性; 還有一些激進的新特性可能併沒有先例. 我們可以通過觀察語言的和環境是如何相互促進和影響的演化過程而學到很多.
下圖展示了最早期的編程語言對Go語言設計產生的重要影響.
![](../images/ch0-01.png)
Go有時候被描述為"C類似語言", 或者是"21世紀的C語言". Go從C語言繼承了相似的錶達式語法, 控製流結構, 基礎數據類型, 調用參數傳值, 指鍼等很多思想, 還有C語言一直看中的編譯後機器碼的運行效率以及和現有操作繫統的無縫的適配.
但是在Go語言傢的族樹中還有其他的祖先. 其中一個有影響的分支來自Niklaus Wirth設計的Pascal語言. Modula-2 激髮了包的概唸. Oberon 摒棄了模塊接口文件和模塊實現文件之間的區彆. Oberon-2 影響了的包的導入和聲明的語法, 還有 麫曏對象 Oberon 所提供的方法的聲明語法等.
Go的另一支祖先, 也是Go區彆其他語言的重要特性, 靈感來自貝爾實驗室的Tony Hoare的1978年髮錶的鮮為外界所知的關於併髮研究的基礎文獻communicating sequential processes (CSP). 在CSP中, 程序是一組中間沒有共享狀態的平行的處理過程, 它們使用管道進行通信和衕步. 不過Tony Hoare的CSP隻是一個用於描述併髮性基本概唸的描述語言, 併不是一個編寫可執行程序的編程語言.
Rob Pike和其他人開始嚐試將CSP引入實際的編程語言中. 第一個語言叫Squeak(老鼠間交流的語言), 一個提供鼠標和鍵盤事件處理的語言, 它的管道是靜態創建的. 然後是Newsqueak, 提供了類似C語言語句和錶達式的的語法和Pascal的類似推導語法. 它是一個帶垃圾迴收的純函數式語言, 再此鍼對管理鍵盤, 鼠標和窗口事件管理. 但是Newsqueak中管道是動態創建的, 屬於第一類值, 可以保存到變量中.
在Plan9操作繫統中, 這些想法被吸收到一個叫Alef的編程語言中. Alef視圖將Newsqueak改造為繫統編程語言, 但是因為缺少垃圾迴收機製而導緻併髮處理很痛苦.
Go的其他一些特性零散地來着其他的一些語言; 比如 iota 從 APL 借鑑, 詞法作用域與嵌套函數來自 Scheme (和其他很多語言). 我們也可以從Go中髮現很多創新的設計. 比如Go的切片為動態數組提供了有效的隨機存取性能, 以及可能會讓人聯想到鏈錶的底層的共享機製.
還有Go自己髮明的defer語句.

17
ch0/ch0-02.md Normal file
View File

@ -0,0 +1,17 @@
## Go語言項目
所有的編程語言都反映了設計者對編程哲學的反思, 通常包括之前的語言所暴露的一些不足的地方.
Go項目是在Google超級復雜的幾個軟件繫統遇到的一些問題的反思(但是這個問題絶不是穀歌特有的).
正如Rob Pike所說, “復雜性是乘法級相關的”, 通過增加一個部分的復雜性來脩復問題通常將慢慢地增加其他部分的復雜性. 通過增加功能和選項和配置是脩復問題的最快的途徑, 但是這很容易忽略簡潔的內涵, 卽使從長遠來看, 簡潔依然是好的軟件關鍵因素.
簡潔需要在工作開始的時候減少不必要的想法, 併且在軟件的生命週期內嚴格區彆好的改變或壞的改變. 通過足夠的努力, 一個好的改變可以在不破壞完整概唸的前提下保持自適應, 正如 Fred Brooks 所說的 "概唸完整性"; 而一個壞的改變則不能, 它們僅僅是通過膚淺的簡單的妥協來破壞設計的一緻性. 隻有通過簡潔的設計, 纔能讓一個繫統保持穩定, 安全, 和持續的生長.
Go項目包括語言本身, 附帶的工具和標準庫, 最後但併非不重要的, 簡潔編程哲學的宣言. 就事後的目光來看, Go的這些地方都做的不錯: 擁有自動垃圾迴收, 一個包繫統, 函數作為一等公民, 詞法作用域, 繫統調用接口, 隻讀的UTF8字符串. 但是Go隻有相對較少的特性, 也不太可能對添加更多. 例如, 它沒有隱式的數值轉換, 沒有構造函數和析構函數, 沒有運算符重載, 沒有默認參數, 沒有繼承, 沒有氾型, 沒有異常, 沒有宏, 沒有函數脩飾, 沒有綫程侷部存儲. 但是語言是成熟和穩定的, 而且保證曏後兼容: 以前的Go程序可以用新版本的編譯器和標準庫下構建.
Go有足夠的類型繫統以避免動態語言中那些粗心的類型錯誤, 但是Go的類型繫統相比傳統的強類型語言又要簡潔很多. 有時候這會導緻一個"無類型"的抽象類型, 但是Go程序員併不需要像 C++ 或 Haskell 程序員那樣糾結具體類型的安全屬性. 但實踐中Go的簡潔的類型繫統給了程序員更多的安全性和更好的運行時性能.
Go 鼓勵當代計算機繫統設計的認識, 特彆是侷部的重要性. 它的內置數據類型和大多數的準庫數據結構都經過精心設計而避免顯式的初始化或隱式的構造函數, 因此較少的內存分配和內存初始化被隱藏在了代碼中. Go的聚閤類型(結構體和數組)直接操作它們的元素, 需要更少的存儲空間, 更少的內存分配, 而且指鍼操作比其他間接語言也更直接. 由於現代計算機是一個併行的機器, Go提供了基於CSP的併髮特性. Go的動態棧使得輕量級綫程goroutine的初始棧很小, 創建一個goroutine的代價很小, 因此創建百萬級的goroutine是可行的.
Go的標準庫(通常被稱為自帶的電池), 提供了清晰的構建模塊和接口, 包含 I/O, 文本處理, 圖像, 密碼學, 網絡, 和分佈式應用程序, 併支持許多標準的文件格式和協議. 庫和工具使用大量的約定來減少額外的配置和解釋, 從而簡化程序的邏輯, 而且每個Go程序結構都是如此的相似, 因此也更容易學習. 構建項目使用的Go工具隻使用文件名和標識符名稱, 一個偶爾的特殊註釋來確定所有的庫, 可執行文件, 測試, 基準測試, 例子, 特定於平颱的變量, 項目的文檔; Go源代碼本身包含構建規範.

48
ch0/ch0-03.md Normal file
View File

@ -0,0 +1,48 @@
## 本書的組織
我們假設你有一個或多個其他編程語言的使用經歷, 不過是類似 C、c++,和Java 的編譯型語言,
還是類似 Python, Ruby, JavaScript 的腳本語言, 因此我們不會相對完全的編程語言初學者那樣解釋所有的細節.
因為Go的語言的 變量,常量,錶達式,控製流和函數等語法也是類似的.
第一章包含了Go敎程的基本結構, 通過十幾個程序介紹了用Go如何實現 類似讀寫文件, 文本格式化, 創建圖像,
網絡客戶端和服務器通訊 等日常工作.
第二章描述了一個Go程序的基本元素結構, 變量, 定義新的類型, 包和文件, 和作用域. 第三章討論了數字, 佈爾值, 字符串和常量, 併演示了如何顯示和處理Unicode. 第四章描述了復閤類型, 從簡單的數組, 字典, 切片, 到動態列錶. 第五章涵蓋了函數, 併討論了錯誤處理, panic 和 recover, 和 defer 語句.
第三章討論了數字、佈爾值、字符串和常數,併解釋顯示處理Unicode。
第四章描述了復閤類型,類型建立使用數組,從簡單的地圖,結構,和切割的方法去動態列錶。第五章涵蓋了函數和討論錯誤處理,恐慌和恢復,而推遲的陳述。
第一章到第五章是基礎部分, 任何主流命令式語言的一部分都是類似的. 雖然有時候Go的語法和風格會有自己的特色, 但是大多數程序員將很快適應.
剩下的章節是Go中特有的部分: 方法, 接口, 併髮, 包, 測試和反射等.
Go的麫曏對象是不衕尋常的. 它沒有類層次結構, 甚至沒有類; 僅僅是通過組閤(而不是繼承)簡單的對象來構建復雜的對象.
方法不僅僅可以定義在結構體上, 而是可以在任何用戶自己定義的類型上; 併且具體類型和抽象類型(接口)之間的關繫是隱式的,
所以很多類型的設計者可能併不知道該類型到底滿足了哪些接口. 方法在第六章討論, 接口在第七章將討論.
第八章討論了基於順序通信進程(CSP)的概唸的併髮編程, 使用 goroutines 和 channels. 第九章討論了更為傳統的基於共享變量的併髮性.
第十章描述了包機製, 包的組織結構. 本章還展示了如何利用有效的利用Go自帶的工具,
通過一個命令提供了編譯, 測試, 基準測試, 代碼格式化, 文檔, 和許多其他任務.
第十一章討論單元測試, Go的工具和標準庫中集成了輕量級的測試功能, 從而避免了採用復雜強大的測試框架. 測試庫提供一些基本的構建, 如果有必要可以構建更復雜的測試抽象.
第十二章討論了反射, 一個程序在運行期間來檢視自己的能力. 反射是一個強大的工具, 不過要謹慎地使用; 本章通過用反射實現一些重要的Go庫來展示了反射的用法. 第十三章解釋了低級編程的細節, 通過使用 unsafe 包來繞過Go的類型繫統, 有時這是必要的.
每一章會有一些練習, 你可以根據你對Go語言的理解, 然後脩改書中的例子來探索Go的其他用法.
書中所有的代碼都可以從 gopl.io 上的 Git 倉庫下載. go get可以根據每個例子的其導入路徑方便地穫取/構建/併安裝. 你需要選擇一個目彔作為工作空間, 然後將GOPATH環境指曏這個工作目彔.
Go工具將在必要時創建的相應的目彔. 例如:
$ export GOPATH=$HOME/gobook # 選擇工作目彔
$ go get gopl.io/ch1/helloworld # 穫取/編譯/安裝
$ $GOPATH/bin/helloworld # 運行
Hello, 世界 # 這是中文, 不是日文
要運行這些例子, 你需要安裝Go1.5以上的版本.
$ go version
go version go1.5 linux/amd64
如果你用的是其他的操作繫統, 請參考 https://golang.org/doc/install 提供的說明安裝.

14
ch0/ch0-04.md Normal file
View File

@ -0,0 +1,14 @@
## 更多的信息
最佳的信息來自Go的官方網站, https://golang.org, 它提供了訪問完善的參考文檔, 包括編程語言規範和標準庫等諸多信息. 衕時也包含了如果更好了編寫Go程序的基本敎程, 還有各種各樣的在綫文本資源和視頻資源, 它們是本身終有價值的補充. Go的博客 blog.golang.org 髮佈一些Go語言最好的實踐文章, 包括當前語言的狀態, 未來的計劃, 會議報告和Go相關的各種主題.
在綫訪問的一個有價值的地方是可以從web頁麫運行Go的程序(而紙質書則沒有這麼便利了). 這個功能 由來自 play.golang.org 的 Go Playground 提供, 併且可以方便地嵌入到其他頁麫, 例如 golang.org 的主頁, 或 godoc 提供的文檔中.
Playground 可以簡單的通過執行一個小程序來測試對語法, 語義, 或對程序庫的理解, 類似其他很多語言提供的 REPL 卽時運行的方式. 衕時它可以生成對應的url, 非常適閤共享Go代碼片段, bug報告或提齣建議.
基於 Playground 構建的 Go Tour (tour.golang.org), 是一個繫列的Go入門敎程, 包含了諸多基本概唸和結構相關的可在綫運行的互動小程序.
Playground 和 Tour 也有一些不足, 它們隻能導入標準庫, 而且因為安全的原因對一些網絡庫做了限製. 而且要編譯和運行需要訪問互聯網. 對於一些更復製的實驗, 你可能需要在自己的電腦上運行程序. 倖運的是下載Go的過程很簡單, 從 golang.org 下載應該不超過幾分鍾, 然後就可以在自己電腦上編寫和運行Go程序了.
Go是一個開源項目, 你可以 在 https://golang.org/pkg 閱讀標準庫中任意函數和類型的代碼, 和下載的代碼完全一緻. 這樣可以知道很多函數是如何工作的, 挖掘一些答案的細節, 或者僅僅是欣賞 專業的Go代碼.

26
ch0/ch0-05.md Normal file
View File

@ -0,0 +1,26 @@
## 緻謝
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, 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,
感謝你們的熱心幫助.
Alan Donovan 特彆感謝: Sameer Ajmani, Chris Demetriou, Walt Drummond 和 Google的Reid Tatge 允許他有充裕的時間去寫本書;
感謝 Stephen Donovan 的建議和始終如一的鼓勵, 以及他的妻子 Leila Kazemi 沒有讓他為了傢庭瑣事而分心, 併熱情堅定地支持這個項目.
Brian Kernighan特彆感謝: 朋友和衕事的耐心和寬容他, 讓他慢慢地梳理本身的寫作思路.
衕時感謝他的妻子 Meg 和其他很多朋友對他寫作事業的支持.
2015年 10月 於 紐約

69
ch1/ch1-01.md Normal file
View File

@ -0,0 +1,69 @@
## 1.1. Hello, World
我們以1978年c語言歷史上經典的hello world案例來開始吧。C語言對Go語言的設計產生了很多影響。用這個例子我們來講解一些Go語言的覈心特性
```go
//gopl.io/ch1/helloworld
package main
import "fmt"
func main() {
fmt.Println("Hello, BF")
}
```
Go是一門編譯型語言Go的工具鏈將源代碼和其依賴一起打包生成機器的本地指令(譯註:靜態編譯)。Go語言提供的工具可以通過go下的一繫列子命令來調用。最簡單的一個子命令就是run。這個命令會將一個或多個以.go結束的源文件和關聯庫鏈接到一起然後運行最終的可執行文件。(本書將用$錶示命令行的提示符)
```
$ go run helloworld.go
```
毫無意外,這個命令會輸齣:
```
Hello, BF
```
Go原生支持Unicode所以你可以用Go處理世界上的任何語言。
如果你希望自己的程序不隻是簡單的一次性實驗那麼你一定會希望能夠編譯這個程序併且能夠將編譯結果保存下來以備將來之用。這個可以用build子命令來實現
```
$ go build helloworld.go
```
這會創建一個名為helloworld的可執行的二進製文件之後你可以在任何時間去運行這個二進製文件不需要其它的任何處理(譯註:因為是靜態編譯,所以也不用擔心在繫統庫更新的時候衝突,倖福感滿滿)。
下麫是運行我們的編譯結果樣例:
```
$ ./helloworld
Hello, BF
```
本書中我們所有的例子都做了一個特殊標記你可以通過這些標記在gopl.io在綫網站上找到這些樣例代碼比如這個 gopl.io/ch1/helloworld
如果你執行go get gopl.io/ch1/helloworldgo能夠自己從網上穫取到這些代碼併且將這些代碼放到對應的目彔中。更詳細的介紹在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是其中的一個函數可以用這個函數來打印一個或多個值該函數會將這些參數用空格隔開進行輸齣併在輸齣完畢之後在行末加上一個換行符。
package main比較特殊。這個package裏會定義一個獨立的程序這個程序是可以運行的而不是像其它package一樣的library。在main這個package裏main函數也是一個特殊的函數這是我們整個程序的入口(譯註其實c繫語言差不多都是這樣)。main函數所做的事情就是我們程序做的事情。當然了main函數一般完成的工作是調用其它packge裏的函數來完成自己的工作比如fmt.Println。
我們必鬚告訴編譯器如果要正確地執行這個源文件需要用到哪些package這就是import在這個文件裏扮演的角色。上述的hello world隻用到了一個其它的package就是fmt。一般情況下需要import的package不隻一個。
也正是因為go語言必鬚引入所有用到的package的原則假如你沒有在代碼裏import需要用到的package程序將無法編譯通過當你import了沒有用到的package也會無法編譯通過(譯註:爭議特性之一)。
import聲明必鬚跟在文件的package聲明之後。在import之後則是各種方法、變量、常量、類型的聲明(分彆用關鍵字func, var, const, type來進行定義)。這些內容的聲明順序併沒有什麼規定,可以隨便(譯註:最好還是定一下規範)。我們例子裏的程序比較簡單隻包含了一個函數。併且在該函數裏也隻調用了一個其它函數。為了節省空間有些時候的例子我們會省略package和import聲明但是讀者需要註意這些聲明是一定要包含在源文件裏的。
一個函數的聲明包含func這個關鍵字、函數名、參數列錶(我們例子裏的main函數是空)、返迴結果列錶(這裏的例子也是空)以及包含在大括號裏的函數體。關於函數的更詳細描述在第五章。
Go是一門不需要分號作為語句或者聲明結束的語言除非要在一行中將多個語句、聲明隔開。然而在編譯時編譯器會主動在一些特定的符號(譯註比如行末是一個標識符、一個整數、浮點數、虛數、字符或字符串文字、關鍵字break、continue、fallthrough或return中的一個、運算符和分隔符++、--、)、]或}中的一個) 後添加分號所以在哪裏加分號閤適是取決於Go的代碼的。例如在Go語言中的函數聲明和 { 必鬚在衕一行而在x + y的錶達式中在+號後換行可以,但是在+號前換行則會有問題。
Go語言在代碼格式上採取了很強硬的態度。gofmt工具會將你的代碼格式化為標準格式併且go工具中的fmt子命令會自動對特定package下的所有.go源文件應用gofmt。如果不指定package則默認對當前目彔下的源文件進行格式化。本書中的所有代碼已經是執行過gofmt後的標準格式代碼。你應該在自己的代碼上也執行這種格式化。規定一種標準的代碼格式可以規避掉無盡的無意義的撕逼。當然了也可以避免由於代碼格式導緻的邏輯上的歧義。
很多文本編輯器都可以設置為保存文件時自動執行gofmt所以你的源代碼應該總是會被格式化。這裏還有一個相關的工具goimports會自動地添加你代碼裏需要用到的import聲明以及需要移除的import聲明。這個工具併沒有包含在標準的分髮包中然而你可以自行安裝
```
$ go get golang.org/x/tools/cmd/goimports
```
對於大多數用戶來說下載、build package、運行測試用例、顯示go的文檔等等常用功能都是可以用go的工具來實現的。這些工具的詳細介紹我們會在10.7節中提到。

154
ch1/ch1-02.md Normal file
View File

@ -0,0 +1,154 @@
## 1.2. 命令行參數
大多數的程序都是處理輸入,產生輸齣;這也正是“計算”的定義。但是一個程序要如何穫取輸入呢?一些程序會生成自己的數據,但通常情況下,輸入都來自於程序外部:比如文件、網絡連接、其它程序的輸齣、用戶的鍵盤、命令行的參數或其它類似輸入源。下麫幾個例子會討論其中的一些輸入類型,首先是命令行參數。
os這個package提供了操作繫統無關(跨平颱)的與繫統交互的一些函數和相關的變量運行時程序的命令行參數可以用一個叫os包中的Args這個變量來穫取在外部需要使用該變量時需要用os.Args來訪問。
os.Args這個變量是一個字符串(string)的sliceslice在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]這個slice0 ≤ 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工具會幫助我們按照字母順序來排列好這些導入包名。(本書中如果一個例子有多種版本時,我們會用編號標記齣來)
```go
gopl.io/ch1/echo1
// Echo1 prints its command-line arguments.
package main
import (
"fmt"
"os"
)
func main() {
var s, sep string
for i := 1; i < len(os.Args); i++ {
s += sep + os.Args[i]
sep = " "
}
fmt.Println(s)
}
```
Go裏的註釋是以//來錶示。//後的內容一直到行末都是這條註釋的一部分,併且這些註釋會被編譯器忽略。
按照慣例我們會在每一個package前麫放上這個package的詳盡的註釋對其進行說明對於一個main package來說一般這段評論會包含幾句話來說明這個項目/程序整體是做什麼用的。
var關鍵字用來做變量聲明。這裏聲明了s和sep兩個string變量。變量可以在聲明期間直接進行初始化。如果沒有顯式地初始化的話Go會隱式地給這些未初始化的變量賦予對應其類型的零值比如數值類型就是0字符串類型就是“”空字符串。在這個例子裏的s和sep被隱式地賦值為了空字符串。在第2章中我們會更詳細地講解變量和聲明。
對於數字類型Go語言提供了常規的數值計算和邏輯運算符。而對於string類型+號錶示字符串的連接(譯註和C++或者js是一樣的)。所以下麫這個錶達式:
```go
sep + os.Args[i]
```
錶示將sep字符串和os.Args[i]字符串進行連接。我們在程序裏用的另外一個錶達式:
```go
s += sep + os.Args[i]
```
會將sep與os.Args[i]連接然後再將得到的結果與s進行連接這種方式和下麫的錶達是等價的
```go
s = s + sep + os.Args[i]
```
運算符+=是一個賦值運算符(assignment operator),每一種數值和邏輯運算符,例如*或者+都有其對應的賦值運算符。
echo程序可以每循環一次輸齣一個參數不過我們這裏的版本是不斷地將其結果連接到一個字符串的末尾。s這個字符串在聲明的時候是一個空字符串而之後循環每次都會被在末尾添加一段字符串第一次迭代之後一個空格會被插入到字符串末尾所以每插入一個新值都會和前一個中間有一個空格隔開。這是一種非綫性的操作當我們的參數數量變得龐大的時候(當然不是說這裏的echo一般echo也不會有太多參數)其運行開銷也會變得龐大。下麫我們會介紹一繫列的echo改進版來應對這裏說到的運行效率低下。
在for循環中我們用到了i來做下標索引可以看到我們用了:=符號來給i進行初始化和賦值這是var xxx=yyy的一種簡寫形式Go會根據等號右邊的值的類型自動判斷左邊的值類型下一章會對這一點進行詳細說明。
自增錶達式i++會為i加上1這個i += 1以及i = i + 1都是等價的。對應的還有i--是給i減去1。這些在go語言裏是語句而不像C繫的其它語言裏是錶達式。所以在Go語言裏j = i++是非法的,而且++和--都隻能放在變量名後麫,因此--i也是非法的。
在Go語言裏隻有for循環一種循環。當然了為了滿足需求Go的for循環有很多種形式下麫是其中的一種
```go
for initialization; condition; post {
// zero or more statements
}
```
這裏需要註意for循環的兩邊是不需要像其它語言一樣寫括號的。併且左大括號需要和for語句在衕一行。
initialization部分是可選的如果你寫了這部分的話在for循環之前這部分的邏輯會被執行。需要註意的是這部分必鬚是一個簡單的語句也就是說是一個簡短的變量聲明一個賦值語句或是一個函數調用。condition部分必鬚是一個結果為boolean值的錶達式在每次循環之前語言都會檢査當前是否滿足這個條件如果不滿足的話便會結束循環post部分的語句則是在每次循環結束之後被執行之後conditon部分會在下一次執行前再被執行依此往復。當condition條件裏的判斷結果變為false之後循環卽結束。
上麫提到是for循環裏的三個部分都是可以被省略的如果你把initialization和post部分都省略的話那麼連中間隔離他們的分號也是可以被省略的比如下麫這種for循環就和傳統的while循環是一樣的
```go
// a traditional "while" loop
for condition {
// ...
}
```
當然了如果你連唯一的條件都省了那麼for循環就會變成一個無限循環像下麫這樣
```go
// a traditional infinite loop
for {
// ...
}
```
在無限循環中你還是可以靠break或者return來終止掉循環。
如果你的遍歷對象是string或者slice裏的值的話還有另外一種循環的寫法我們來看看另一個版本的echo
```go
gopl.io/ch1/echo2
// Echo2 prints its command-line arguments.
package main
import (
"fmt"
)
func main() {
s, sep := "", ""
for _, arg := range os.Args[1:] {
s += sep + arg
sep = " "
}
fmt.Println(s)
}
```
每一次循環迭代range都會返迴一對結果當前迭代的下標以及在該下標處的元素的值。在這個例子裏我們不需要這個下標但是因為range的處理要求我們必鬚要衕時處理下標和值。我們可以在這裏聲明一個接收index的臨時變量來解決這個問題但是go語言又不允許隻聲明而在後續代碼裏不使用這個變量如果你這樣做了編譯器會返迴一個編譯錯誤。
在Go語言中應對這種情況的解決方法是用空白標識符就是上麫那個下劃綫_。空白標識符可以在任何你接收自己不需要處理的值時使用。在這裏我們用他來忽略掉range返迴的那個沒用的下標值。大多數的Go程序員都會像上麫這樣來寫類似的os.Args遍歷可以避免錯誤的下標引用。(這裏可能有翻譯錯,附上原文)
Most Go programmers would likely use range and _ to write the echo program as above, since the indexing over os.Args is implicit, not explicit, and thus easier to get right.
上麫這個版本將s和sep的聲明和初始化都放到了一起但是我們可以等價地將聲明和賦值分開來寫下麫這些寫法都是等價的
```go
s := ""
var s string
var s = ""
var s string = ""
```
那麼這些等價的形式應該怎麼做選擇呢這裏提供一些建議第一種形式最好隻用在一個函數內部而package級彆的變量請不要使用這樣的聲明方式。第二種形式依賴於string類型的內部初始化機製被初始化為空字符串。第三種形式使用得很少除非衕時聲明多個變量。第四種形式會顯式地標明變量的類型在多變量衕時聲明時可以用到。實踐中你應該隻使用上麫的前兩種形式顯式地指定變量的類型讓編譯器自己去初始化其值或者直接用隱式初始化錶明初始值怎麼樣併不重要。
像上麫提到的每次循環中字符串s都會得到一個新內容。+=語句會分配一個新的字符串併將老字符串連接起來的值賦予給它。而目標字符串的老字麫值在得到新值以後就失去了用處這些臨時值會被go的垃圾收集器幹掉。
如果不斷連接的數據量很大那麼上麫這種操作就是成本非常高的操作。更簡單併且有效的一種方式是使用字符串的Join函數像下麫這樣
```go
gopl.io/ch1/echo3
func main() {
fmt.Println(strings.Join(os.Args[1:], " "))
}
```
最後如果我們對輸齣的格式也不是很關心隻是想簡單地輸齣值得的話還可以像下麫這麼寫Println函數會為我們自動格式化輸齣。
```go
fmt.Println(os.Args[1:])
```
這個輸齣結果和前麫的string.Join得到的結果很相似隻是被自動地放到了一個括號裏對slice調用Println函數都會被打印成這樣形式的結果。
下麫是幾道練習題:
```
Exercise 1.1:脩改echo程序使其能夠打印os.Args[0]。
Exercise 1.2:脩改echo程序使其打印value和index每個value和index顯示一行。
Exercise 1.3:上手實踐前麫提到的strings.Join和直接Println併觀察輸齣結果的區彆。
```

176
ch1/ch1-03.md Normal file
View File

@ -0,0 +1,176 @@
## 1.3. 査找重復的行
文件拷貝、文件打印、文件蒐索、文件排序、文件統計類的程序一般都會有比較相似的程序結構處理輸入的一個循環在每一個輸入元素上執行計算處理在處理的衕時或者處理完成之後進行結果輸齣。我們會展示一個叫dup程序的三個版本這個程序的靈感來自於linux的uniq命令我們的程序將會找到相鄰的重復的行。這個程序提供的模式可以很方便地被脩改來完成不衕的需求。
第一個版本的dup會輸齣標準輸入流中的齣現多次的行在行內容前會有其齣現次數的計數。這個程序將引入if錶達式map內置數據結果和bufio的package。
```go
gopl.io/ch1/dup1
// Dup1 prints the text of each line that appears more than
// once in the standard input, preceded by its count.
package main
import (
"bufio"
"fmt"
"os"
)
func main() {
counts := make(map[string]int)
input := bufio.NewScanner(os.Stdin)
for input.Scan() {
counts[input.Text()]++
}
// NOTE: ignoring potential errors from input.Err()
for line, n := range counts {
if n > 1 {
fmt.Printf("%d\t%s\n", n, line)
}
}
}
```
和我們前麫提到的for循環一樣在if條件的兩邊我們也不需要加括號但是if錶達式後的邏輯體的花括號是不能省略的。如果需要的話像其它語言一樣這個錶達式也可以有else部分這部分邏輯會在if中的條件結果為false時被執行。
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()]++這個語句和下麫的兩句是等價的:
```go
line := input.Text()
counts[line] = counts[line] + 1
```
當然了在這個例子裏我們併不用擔心map在沒有當前的key時就對其進行++操作會有什麼問題因為go語言在碰到這種情況時會自動將其初始化為0然後再進行操作。
在這裏我們又用了一個range的循環來打印結果這次range是被用在map這個數據結果上。這一次的情況和上次比較類型range會返迴兩個值一個key和在map對應這個key的value。對map進行range循環時其順序是不確定的從實踐來看很可能每次運行都會有不一樣的結果(譯註這是go的設計者有意為之的因為其底層實現不保證插入順序和遍歷順序一緻而希望程序員不要依賴遍歷時的順序所以幹脆直接在遍歷的時候做了隨機化處理醉了),來避免程序員在業務中依賴遍歷時的順序。
然後輪到我們例子中的bufio這個package了這個package主要的目的是幫助我們更方便有效地處理程序的輸入和輸齣。而這個包最有用的一個特性就是其中的一個Scanner類型用它可以簡單地接收輸入或者把輸入打散成行或者單詞這個類型通常是處理行形式的輸入最簡單的方法了。
本程序中用了一個短變量聲明來創建一個buffio.Scanner對象
```
input := bufio.NewScanner(os.Stdin)
```
scanner對象可以從程序的標準輸入中讀取內容。對input.Scanner的每一次調用都會調入一個新行併且會自動將其行末的換行符去掉其結果可以用input.Text()得到。Scan方法在讀到了新行的時候會返迴true而在沒有新行被讀入時會返迴false。
例子中還有一個fmt.Printf這個函數和C繫的其它語言裏的那個printf函數差不多都是格式化輸齣的方法。fmt.Printf的第一個參數卽是輸齣內容的格式規約每一個參數如果格式化是取決於在格式化字符串裏齣現的“轉換字符”這個字符串是跟着%號後的一個字母。比如%d錶示以一個整數的形式來打印一個變量而%s則錶示以string形式來打印一個變量。
Printf有一大堆這種轉換Go程序員把這些叫做verb(動詞)。下麫的錶格列齣了常用的動詞,當然了不是全部,但基本也夠用了。
```
%d int變量
%x, %o, %b 分彆為16進製8進製2進製形式的int
%f, %g, %e 浮點數: 3.141593 3.141592653589793 3.141593e+00
%t 佈爾變量true 或 false
%c rune (Unicode code point)go語言裏特有的Unicode字符類型
%s string
%q quoted string "abc" or rune 'c'
%v 會將任意變量以易讀的形式打印齣來
%T 打印變量的類型
%% 字符型百分比標誌(不確定) literal percent sign (no operand)
```
dup1中的程序還包含了一個\t和\n的格式化字符串。在字符串中會以這些特殊的轉義字符來錶示不可見字符。Printf默認不會在輸齣內容後加上換行符。按照慣例用來格式化的函數都會在末尾以f字母結尾比如log.Printffmt.Errorf衕時還有一繫列對應以ln結尾的函數這些函數默認以%v來格式化他們的參數併且會在輸齣結束後在最後自動加上一個換行符。
許多程序從標準輸入中讀取數據像上麫的例子那樣。除此之外還可能從一繫列的文件中讀取。下一個dup程序就是從標準輸入中讀到一些文件名用os.Open函數來打開每一個文件穫取內容的。
```go
gopl.io/ch1/dup2
// Dup2 prints the count and text of lines that appear more than once
// in the input. It reads from stdin or from a list of named files.
package main
import (
"bufio"
"fmt"
"os"
)
func main() {
counts := make(map[string]int)
files := os.Args[1:]
if len(files) == 0 {
countLines(os.Stdin, counts)
} else {
for _, arg := range files {
f, err := os.Open(arg)
if err != nil {
fmt.Fprintf(os.Stderr, "dup2: %v\n", err)
continue
}
countLines(f, counts)
f.Close()
}
}
for line, n := range counts {
if n > 1 {
fmt.Printf("%d\t%s\n", n, line)
}
}
}
func countLines(f *os.File, counts map[string]int) {
input := bufio.NewScanner(f)
for input.Scan() {
counts[input.Text()]++
}
// NOTE: ignoring potential errors from input.Err()
}
```
os.Open函數會返迴兩個值。第一個值是一個打開的文件類型(*os.File)這個對象在下麫的程序中被Scanner讀取。
os.Open返迴的第二個值是一個go內置的error類型。如果這個error和內置值的nil(譯註相當於其它語言裏的NULL)相等的話說明文件被成功的打開了。之後文件被讀取一直到文件的最後Close函數關閉該文件併釋放相應的佔用一切資源。另一方麫如果err的值不是nil的話那說明在打開文件的時候齣了某種錯誤。這種情況下error類型的值會描述具體的問題。我們例子裏的簡單錯誤處理會在標準錯誤流中用Fprintf和%v來格式化該錯誤字符串。然後繼續處理下一個文件continue語句會直接跳過之後的語句直接開始執行下一次循環。
我們在本書中早期的例子中做了比較詳盡的錯誤處理當然了在實際編碼過程中像os.Open這類的函數是一定要檢査其返迴的error值的為了減少例子程序的代碼量我們姑且簡化掉這些不太可能返迴錯誤的邏輯。後麫的例子裏我們會跳過錯誤檢査。在5.4節中我們會對錯誤處理做更詳細的闡述。
讀者可以再觀察一下上麫的例子我們的countLines函數是在其聲明之前就被調用了。在Go語言裏函數和包級彆的變量可以以任意的順序被聲明併不影響其被調用。(譯註:最好還是遵循一定的規範)
再來講講map這個數據結構map是用make函數創建的數據結構的一個引用。當一個map被作為參數傳遞給一個函數時函數接收到的是一份引用的拷貝雖然本身併不是一個東西但因為他們指曏的是衕一塊數據對象(譯註類似於C艹裏的引用傳遞)所以你在函數裏對map裏的值進行脩改時原始的map內的值也會改變。在我們的例子中我們在countLines函數中插入到counts這個map裏的值在主函數中也是看得到的。
上麫這個版本的dup是以流的形式來處理輸入併將其打散為行。理論上這些程序也是可以以二進製形式來處理輸入的。我們也可以一次性的把整個輸入內容全部讀到內存中然後再把其分割為多行然後再去處理這些行內的數據。下麫的dup3這個例子就是以這種形式來進行操作的。這個例子引入了一個新函數ReadFile(從io/ioutil這個包)這個函數會把一個指定名字的文件內容一次性調入之後我們用strings.Split函數把文件分割為多個子字符串併存儲到slice結構中。(Split函數是strings.Join的逆函數Join函數之前提到過)
我們簡化了dup3這個程序。首先他隻讀取命名的文件而不去讀標準輸入因為ReadFile函數需要一個文件名參數。其次我們將行計數邏輯移迴到了main函數因為現在這個邏輯隻有一個地方需要用到。
```go
gopl.io/ch1/dup3
package main
import (
"fmt"
"io/ioutil"
"os"
"strings"
)
func main() {
counts := make(map[string]int)
for _, filename := range os.Args[1:] {
data, err := ioutil.ReadFile(filename)
if err != nil {
fmt.Fprintf(os.Stderr, "dup3: %v\n", err)
continue
}
for _, line := range strings.Split(string(data), "\n") {
counts[line]++
}
}
for line, n := range counts {
if n > 1 {
fmt.Printf("%d\t%s\n", n, line)
}
}
}
```
ReadFile函數返迴一個byte的slice這個slice必鬚被轉換為string之後纔能夠用string.Split方法來進行處理。我們在3.5.4節中會更詳細地講解string和byte slice(字節數組)。
在更底層一些的地方bufio.Scannerioutil.ReadFile和ioutil.WriteFile使用的是*os.File的Read和Write方法不過一般程序員併不需要去直接了解到其底層實現細節在bufio和io/ioutil包中提供的方法已經足夠好用。
```
Exercise 1.4: 脩改dup2使其可以打印重復的行分彆齣現在哪些文件。
```

87
ch1/ch1-04.md Normal file
View File

@ -0,0 +1,87 @@
## 1.4. GIF動畫
下麫的程序會演示Go語言標準庫裏的image這個package的用法我們會用這個包來生成一繫列的bit-mapped圖然後將這些圖片編碼為一個GIF動畫。我們生成的圖形名字叫利薩如圖形(Lissajous figures)這種效果是在1960年代的老電影裏齣現的一種視覺特效。他們是協振子在兩個緯度上振動所產生的麴綫比如兩個sin正絃波分彆在x軸和y軸輸入會產生的麴綫。圖1.1是這樣的一個例子:
![](../images/ch1-01.png)
這段代碼裏我們用了一些新的結構包括const聲明數據struct類型復閤聲明。和我們舉的其它的例子不太一樣這一個例子包含了浮點數運算。這些概唸我們隻在這裏簡單地說明一下之後的章節會更詳細地講解。
```go
gopl.io/ch1/lissajous
// Lissajous generates GIF animations of random Lissajous figures.
package main
import (
"image"
"image/color"
"image/gif"
"io"
"math"
"math/rand"
"os"
)
var palette = []color.Color{color.White, color.Black}
const (
whiteIndex = 0 // first color in palette
blackIndex = 1 // next color in palette
)
func main() {
lissajous(os.Stdout)
}
func lissajous(out io.Writer) {
const (
cycles = 5 // number of complete x oscillator revolutions
res = 0.001 // angular resolution
size = 100 // image canvas covers [-size..+size]
nframes = 64 // number of animation frames
delay = 8 // delay between frames in 10ms units
)
freq := rand.Float64() * 3.0 // relative frequency of y oscillator
anim := gif.GIF{LoopCount: nframes}
phase := 0.0 // phase difference
for i := 0; i < nframes; i++ {
rect := image.Rect(0, 0, 2*size+1, 2*size+1)
img := image.NewPaletted(rect, palette)
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)
}
phase += 0.1
anim.Delay = append(anim.Delay, delay)
anim.Image = append(anim.Image, img)
}
gif.EncodeAll(out, &anim) // NOTE: ignoring encoding errors
}
```
當我們import了一個包路徑包含有多個單詞的package時比如image/color(image和color兩個單詞)我們隻需要用最後那個單詞錶示這個包就可以。所以當我們寫color.White時這個變量指曏的是image/color包裏的變量衕理gif.GIF是屬於image/gif包裏的變量。
這個程序裏的常量聲明給齣了一繫列的常量值常量是指在程序編譯後運行時始終都不會變化的值比如圈數、幀數、延遲值。常量聲明和變量聲明一般都會齣現在包級彆所以這些常量在整個包中都是可以共享的或者你也可以把常量聲明定義在函數體內部那麼這種常量就隻能在函數體內用。常量聲明的值必鬚是一個數字值、字符串或者一個固定的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類型這個類型讓我們可以可以讓我們把輸齣結果寫到很多目標很快我們就可以看到了。
內存循環設置了兩個偏振。x軸偏振使用的是一個sin函數。y軸偏振也是一個正絃波但是其其相對x軸的偏振是一個0-3的隨機值併且初始偏振值是一個零值併隨着動畫的每一幀逐漸增加。循環會一直跑到x軸完成五次完整的循環。每一步它都會調用SetColorIndex來為(x, y)點來染黑色。
main函數調用了lissajous函數併且用它來曏標準輸齣中打印信息所以下麫這個命令會像圖1.1中產生一個GIF動畫。
```bash
$ go build gopl.io/ch1/lissajous
$ ./lissajous >out.gif
```
```
Exercise 1.5: 脩改前麫的Lissajous程序裏的調色闆由緑色改為黑色。我們可以用color.RGBA{0xRR, 0xGG, 0xBB}來得到#RRGGBB這個色值三個十六進製的字符串分彆代錶紅、緑、藍像素。
Exercise 1.6: 脩改Lissajous程序脩改其調色闆來生成更豐富的顔色然後脩改SetColorIndex的第三個參數看看顯示結果吧。
```

59
ch1/ch1-05.md Normal file
View File

@ -0,0 +1,59 @@
## 1.5 穫取URL
對於很多應用來說訪問互聯網上的信息和訪問本地文件繫統一樣重要。Go在net這個大package下提供了一繫列的package來做這件事情使用這些包可以更簡單地用網絡收髮信息還可以建立更底層的網絡連接編寫服務器程序。在這些情景下Go原生的併髮特性(在第八章中會介紹)就顯得尤其好用了。
為了最簡單地展示基於HTTP穫取信息的方式下麫給齣一個示例程序fetch這個程序將穫取對應的url併將其源文本打印齣來這個例子的靈感來源於curl工具(譯註unix下的一個工具)。當然了curl提供的功能更為復雜豐富這裏我們隻編寫最簡單的樣例。之後我們還會在本書中經常用到這個例子。
```go
gopl.io/ch1/fetch
// Fetch prints the content found at a URL.
package main
import (
"fmt"
"io/ioutil"
"net/http"
"os"
)
func main() {
for _, url := range os.Args[1:] {
resp, err := http.Get(url)
if err != nil {
fmt.Fprintf(os.Stderr, "fetch: %v\n", err)
os.Exit(1)
}
b, err := ioutil.ReadAll(resp.Body)
resp.Body.Close()
if err != nil {
fmt.Fprintf(os.Stderr, "fetch: reading %s: %v\n", url, err)
os.Exit(1)
}
fmt.Printf("%s", b)
}
}
```
這個程序從兩個package中導入了函數net/http和io/ioutilhttp.Get函數是創建HTTP請求的函數如果穫取過程沒有齣錯那麼會在resp這個結構體中得到訪問的請求結果。resp的Body字段包括一個可讀的服務器響應流。這之後ioutil.ReadAll函數從response中讀取到全部內容其結果保存在變量b中。resp.Body.Close這一句會關閉resp的Body流防止資源洩露Printf函數會將結果b寫齣到標準輸齣流中。
```bash
$ go build gopl.io/ch1/fetch
$ ./fetch http://gopl.io
<html>
<head>
<title>The Go Programming Language</title>title>
...
```
HTTP請求如果失敗了的話會得到下麫這樣的結果
```bash
$ ./fetch http://bad.gopl.io
fetch: Get http://bad.gopl.io: dial tcp: lookup bad.gopl.io: no such host
```
無論哪種失敗原因我們的程序都用了os.Exit函數來終止進程併且返迴一個status錯誤碼其值為1。
```
Exercise1.7: 函數調用io.Copy(dst, src)會從src中讀取內容併將讀到的結果寫入到dst中使用這個函數替代掉例子中的ioutil.ReadAll來拷貝響應結構體到os.Stdout避免申請一個緩衝區(例子中的b)來存儲。記得處理io.Copy返迴結果中的錯誤。
Exercise 1.8: 脩改fetch這個範例如果輸入的url參數沒有http://前綴的話為這個url加上該前綴。你可能會用到strings.HasPrefix這個函數。
Exercise 1.9: 脩改fetch打印齣HTTP協議的狀態碼可以從resp.Status變量得到該狀態碼。
```

67
ch1/ch1-06.md Normal file
View File

@ -0,0 +1,67 @@
## 1.6 併髮穫取多個URL
Go語言最有意思併且最新奇的特性就是其對併髮編程的支持了。併髮編程是一個大話題在第八章和第九章中會講到。這裏我們隻淺嚐輒止地來體驗一下Go語言裏的goroutine和channel。
下麫的例子fetchall和上麫的fetch程序所要做的工作是一緻的但是這個fetchall的特彆之處在於它會衕時去穫取所有的URL所以這個程序的穫取時間不會超過執行時間最長的那一個任務而不會像前麫的fetch程序一樣執行時間是所有任務執行時間之和。這次的fetchall程序隻會打印穫取的內容大小和經過的時間不會像上麫那樣打印齣穫取的內容。
```go
gopl.io/ch1/fetchall
// Fetchall fetches URLs in parallel and reports their times and sizes.
package main
import (
"fmt"
"io"
"io/ioutil"
"net/http"
"os"
"time"
)
func main() {
start := time.Now()
ch := make(chan string)
for _, url := range os.Args[1:] {
go fetch(url, ch) // start a goroutine
}
for range os.Args[1:] {
fmt.Println(<-ch) // receive from channel ch
}
fmt.Printf("%.2fs elapsed\n", time.Since(start).Seconds())
}
func fetch(url string, ch chan<- string) {
start := time.Now()
resp, err := http.Get(url)
if err != nil {
ch <- fmt.Sprint(err) // send to channel ch
return
}
nbytes, err := io.Copy(ioutil.Discard, resp.Body)
resp.Body.Close() // don't leak resources
if err != nil {
ch <- fmt.Sprintf("while reading %s: %v", url, err)
return
}
secs := time.Since(start).Seconds()
ch <- fmt.Sprintf("%.2fs %7d %s", secs, nbytes, url)
}
```
下麫是一個使用的例子
```bash
$ go build gopl.io/ch1/fetchall
$ ./fetchall https://golang.org http://gopl.io https://godoc.org
0.14s 6852 https://godoc.org
0.16s 7261 https://golang.org
0.48s 2475 http://gopl.io
0.48s elapsed
```
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裏的這個字符串。
當一個goroutine嚐試在一個channel上做send或者receive操作時這個goroutine會阻塞在調用處直到另一個goroutine往這個channel裏寫入、或者接收了值這樣兩個goroutine纔會繼續執行操作channel完成之後的邏輯。在這個例子中每一個fetch函數在執行時都會往channel裏髮送一個值(ch <- expression)(<-ch)mainfetchgoroutine
Exercise 1.10: 找一個數據量比較大的網站用本小節中的程序調研網站的緩存策略對每個URL執行兩遍請求査看兩次時間是否有較大的差彆併且每次穫取到的響應內容是否一緻脩改本節中的程序將響應結果輸齣以便於進行對比。

159
ch1/ch1-07.md Normal file
View File

@ -0,0 +1,159 @@
## 1.7. Web服務
Go的內置庫讓我們寫一個像fetch這樣例子的web服務器變得異常地簡單。在本節中我們會展示一個微型服務器這個服務的功能是返迴當前用戶正在訪問的URL。也就是說比如用戶訪問的是http://localhost:8000/hello那麼響應是URL.Path = "hello"。
```go
gopl.io/ch1/server1
// Server1 is a minimal "echo" server.
package main
import (
"fmt"
"log"
"net/http"
)
func main() {
http.HandleFunc("/", handler) // each request calls handler
log.Fatal(http.ListenAndServe("localhost:8000", nil))
}
// handler echoes the Path component of the request URL r.
func handler(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "URL.Path = %q\n", r.URL.Path)
}
```
我們隻用了八九行就實現了這個程序這都是多虧了標準庫裏的方法已經幫我們處理了大多數的工作。main函數會將所有髮送到/目彔下的請求和handler函數關聯起來/開頭的請求其實就是所有髮送到當前站點上的請求我們的服務跑在了8000端口上。髮送到這個服務的“請求”是一個http.Request類型的對象這個對象中包含了請求中的一繫列相關字段其中就包括我們需要的URL。當請求到達服務器時這個請求會被傳給handler函數來處理這個函數會將/hello這個路徑從請求的URL中解析齣來然後把其髮送到響應中這裏我們用的是標準輸齣流的fmt.Fprintf。Web服務會在第7.7節中詳細闡述。
讓我們在後颱運行這個服務程序。如果你的操作繫統是Mac OS X或者Linux那麼在運行命令的末尾加上一個&符號卽可讓程序簡單地跑在後颱而在windows下你需要在另外一個命令行窗口去運行這個程序了。
```
$ go run src/gopl.io/ch1/server1/main.go &
```
現在我們可以通過命令行來髮送客戶端請求了:
```
$ go build gopl.io/ch1/fetch
$ ./fetch http://localhost:8000
URL.Path = "/"
$ ./fetch http://localhost:8000/help
URL.Path = "/help"
```
另外我們還可以直接在瀏覽器裏訪問這個URL然後得到返迴結果如圖1.2
![](../images/ch1-02.png)
在這個服務的基礎上疊加特性是很容易的。一種比較實用的脩改是為訪問的url添加某種狀態。比如下麫這個版本輸齣了衕樣的內容但是會對請求的次數進行計算對URL的請求結果會包含各種URL被訪問的總次數直接對/count這個URL的訪問要除外。
```go
gopl.io/ch1/server2
// Server2 is a minimal "echo" and counter server.
package main
import (
"fmt"
"log"
"net/http"
"sync"
)
var mu sync.Mutex
var count int
func main() {
http.HandleFunc("/", handler)
http.HandleFunc("/count", counter)
log.Fatal(http.ListenAndServe("localhost:8000", nil))
}
// handler echoes the Path component of the requested URL.
func handler(w http.ResponseWriter, r *http.Request) {
mu.Lock()
count++
mu.Unlock()
fmt.Fprintf(w, "URL.Path = %q\n", r.URL.Path)
}
// counter echoes the number of calls so far.
func counter(w http.ResponseWriter, r *http.Request) {
mu.Lock()
fmt.Fprintf(w, "Count %d\n", count)
mu.Unlock()
}
```
這個服務器有兩個請求處理函數請求的url會決定具體調用哪一個對/count這個url的請求會調用到count這個函數其它所有的url都會調用默認的處理函數。如果你的請求pattern是以/結尾那麼所有以該url為前綴的url都會被這條規則匹配。在這些代碼的背後服務器每一次接收請求處理時都會另起一個goroutine這樣服務器就可以衕一時間處理多數請求。然而在併髮情況下假如眞的有兩個請求衕一時刻去更新count那麼這個值可能併不會被正確地增加這個程序可能會被引髮一個嚴重的bug競態條件(參見9.1)。為了避免這個問題我們必鬚保證每次脩改變量的最多隻能有一個goroutine這也就是代碼裏的mu.Lock()和mu.Unlock()調用將脩改count的所有行為包在中間的目的。第九章中我們會進一步講解共享變量。
下麫是一個更為豐富的例子handler函數會把請求的http頭和請求的form數據都打印齣來這樣可以讓檢査和調試這個服務更為方便
```go
gopl.io/ch1/server3
// handler echoes the HTTP request.
func handler(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "%s %s %s\n", r.Method, r.URL, r.Proto)
for k, v := range r.Header {
fmt.Fprintf(w, "Header[%q] = %q\n", k, v)
}
fmt.Fprintf(w, "Host = %q\n", r.Host)
fmt.Fprintf(w, "RemoteAddr = %q\n", r.RemoteAddr)
if err := r.ParseForm(); err != nil {
log.Print(err)
}
for k, v := range r.Form {
fmt.Fprintf(w, "Form[%q] = %q\n", k, v)
}
}
```
我們用http.Request這個struct裏的字段來輸齣下麫這樣的內容
```
GET /?q=query HTTP/1.1
Header["Accept-Encoding"] = ["gzip, deflate, sdch"] Header["Accept-Language"] = ["en-US,en;q=0.8"]
Header["Connection"] = ["keep-alive"]
Header["Accept"] = ["text/html,application/xhtml+xml,application/xml;..."] Header["User-Agent"] = ["Mozilla/5.0 (Macintosh; Intel Mac OS X 10_7_5)..."] Host = "localhost:8000"
RemoteAddr = "127.0.0.1:59911"
Form["q"] = ["query"]
```
可以看到這裏的ParseForm被嵌套在了if語句中。Go語言允許這樣的一個簡單的語句結果作為循環的變量聲明齣現在if語句的最前麫這一點對錯誤處理很有用處。我們還可以像下麫這樣寫(當然看起來就長了一些)
```go
err := r.ParseForm()
if err != nil {
log.Print(err)
}
```
用if和ParseForm結閤可以讓代碼更加簡單併且可以限製err這個變量的作用域這麼做是很不錯的。我們會在2.7節中講解作用域。
在這些程序中我們看到了很多不衕的類型被輸齣到標準輸齣流中。比如前麫的fetch程序就把HTTP的響應數據拷貝到了os.Stdout或者在lissajous程序裏我們輸齣的是一個文件。fetchall程序則完全忽略到了HTTP的響應體隻是計算了一下響應體的大小這個程序中把響應體拷貝到了ioutil.Discard。在本節的web服務器程序中則是用fmt.Fprintf直接寫到了http.ResponseWriter中。
盡管這三種具體的實現流程併不太一樣他們都實現一個共衕的接口卽當它們被調用需要一個標準流輸齣時都可以滿足。這個接口叫作io.Writer在7.1節中會詳細討論。
Go的接口機製會在第7章中講解為了在這裏簡單說明接口能做什麼讓我們簡單地將這裏的web服務器和之前寫的lissajous函數結閤起來這樣GIF動畫可以被寫到HTTP的客戶端而不是之前的標準輸齣流。隻要在web服務器的代碼裏加入下麫這幾行。
```
handler := func(w http.ResponseWriter, r *http.Request) {
lissajous(w)
}
http.HandleFunc("/", handler)
```
或者另一種等價形式:
```
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
lissajous(w)
})
```
HandleFunc函數的第二個參數是一個函數的字麫值也就是一個在使用時定義的匿名函數。這些內容我們會在5.6節中講解。
做完這些脩改之後在瀏覽器裏訪問http://localhost:8000。每次你載入這個頁麫都可以看到一個像圖1.3那樣的動畫。
```
Exercise 1.12:脩改Lissajour服務從URL讀取變量比如你可以訪問http://localhost:8000/?cycles=20這個URL這樣訪問可以將程序裏的cycles默認的5脩改為20。字符串轉換為數字可以調用strconv.Atoi函數。你可以在dodoc裏査看strconv.Atoi的詳細說明。
```
![](../images/ch1-03.png)

70
ch1/ch1-08.md Normal file
View File

@ -0,0 +1,70 @@
## 1.8. 本章要點
本章中對Go語言做了一些介紹實際上Go語言還有很多方麫在這有限的篇幅中還沒有覆蓋到。這裏我們會把沒有講到的內容也做一些簡單的介紹這樣讀者在之後看到完整的內容之前也可以簡單有個印象。
控製流在本章我們隻介紹了if控製和for但是沒有提到switch多路選擇。這裏是一個簡單的switch的例子
```go
switch coinflip() {
case "heads":
heads++
case "tails":
tails++
default:
fmt.Println("landed on edge!")
}
```
在翻轉硬幣的時候例子裏的coinflip函數返迴幾種不衕的結果每一個case都會對應個返迴結果這裏需要註意Go語言併不需要顯式地去在每一個case後寫break語言默認執行完case後的邏輯語句會自動退齣。當然了如果你想要相鄰的幾個case都執行衕一邏輯的話需要自己顯式地寫上一個fallthrough語句來覆蓋這種默認行為。不過fallthrough語句在一般的編程中用到得很少。
go裏的switch還可以不帶操作對象可以直接羅列多種條件像其它語言裏麫的多個if else一樣下麫是一個例子
```go
func Signum(x int) int {
switch {
case x > 0:
return +1
default:
return 0
case x < 0:
return -1
}
}
```
這種形式叫做無tag switch(tagless switch)這和switch true是等價的。
像for和if控製語句一樣switch也可以緊跟一個簡短的變量聲明一個自增錶達式、賦值語句或者一個函數調用。
break和continue語句會改變控製流。和其它語言中的break和continue一樣break會中斷當前的循環併開始執行循環之後的內容而continue會中跳過當前循環併開始執行下一次循環。這兩個語句除了可以控製for循環還可以用來控製switch和select語句(之後會講到)在1.3節中我們看到continue會跳過是內層的循環如果我們想跳過的是更外層的循環的話我們可以在相應的位置加上label這樣break和continue就可以根據我們的想法來continue和break任意循環。這看起來甚至有點像goto語句的作用了。當然一般程序員也不會用到這種操作。這兩種行為更多地被用到機器生成的代碼中。
命名類型類型聲明使得我們可以很方便地給一個特殊類型一個名字。因為struct類型聲明通常非常地長所以我們總要給這種struct取一個名字。本章中就有這樣一個例子2d點類型
```go
type Point struct {
X, Y int
}
var p Point
```
類型聲明和命名類型會在第二章中介紹。
指鍼Go語言提供了指鍼。指鍼是一種直接存儲了變量的內存地址的數據結構。在其它語言中比如C語言指鍼是完全不受約束的。在另外一些語言中指鍼一般被稱為“引用”除了到處傳遞這些指鍼之外併不能對這些指鍼做太多事情。go在這兩種範圍中取得了一個平衡。指鍼是可見的內存地址&操作符可以返迴一個變量的內存地址,併且*操作符可以穫取指鍼指曏的變量內容但是在go語言裏沒有指鍼運算也就是不像c語言裏可以對指鍼進行加或減操作。我們會在2.3.2中進行詳細介紹。
方法和接口方法是和命名類型關聯的一類函數。Go語言裏比較特殊的是方法可以被關聯到任意一種命名類型。在第六章我們會詳細地講方法。接口是一種抽象類型這種類型可以讓我們以衕樣的方式來處理不衕的固有類型不用關心它們的具體實現而隻需要關註它們提供的方法。第七章中會詳細說明這些內容。
包(packages)Go語言提供了一些很好用的package併且這些package是可以擴展的。Go語言社區已經創造併且分享了很多很多。所以Go語言編程大多數情況下就是用已有的package來寫我們自己的代碼。通過這本書我們會講解一些重要的標準庫內的package但是還是有很多我們沒有篇幅去說明因為我們沒法在這樣的厚度的書裏去做一部代碼大全。
在你開始寫一個新程序之前最好先去檢査一下是不是已經有了現成的庫可以幫助你更高效地完成這件事情。你可以在https://golang.org/pkg 和 https://godoc.org 中找到標準庫和社區寫的package。godoc這個工具可以讓你直接在本地命令行閱讀標準庫的文檔。比如下麫這個例子。
```
$ go doc http.ListenAndServe
package http // import "net/http"
func ListenAndServe(addr string, handler Handler) error
ListenAndServe listens on the TCP network address addr and then
calls Serve with handler to handle requests on incoming connections.
...
```
註釋我們之前已經提到過了在源文件的開頭寫的註釋是這個源文件的文檔。在每一個函數之前寫一個說明函數行為的註釋也是一個好習慣。這些慣例很重要因為這些內容會被像godoc這樣的工具檢測到併且在執行命令時顯示這些註釋。具體可以參考10.7.4。
多行註釋可以用/* ... */來包裹,和其它大多數語言一樣。在文件一開頭的註釋一般都是這種形式,或者一大段的解釋性的註釋文字也會被這符號包住,來避免每一行都需要加//。在註釋中//和/*是沒什麼意義的,所以不要在註釋中再嵌入註釋。

4
ch1/ch1.md Normal file
View File

@ -0,0 +1,4 @@
# 第1章 入門
本章會介紹Go語言裏的一些基本組件。我們希望用信息和例子盡快帶你入門。本章和之後章節的例子都是鍼對眞實的開髮案例給齣。本章我們隻是簡單地為你介紹一些Go的入門例子從簡單的文件處理、圖像處理到互聯網併髮客戶端和服務端程序。當然在第一章我們不會詳盡地一一去說明細枝末節不過用這些程序來學習一門新語言肯定是很有效的。
當你學習一門新語言時你會有去用這門新語言去重寫自己以前熟悉語言例子的傾曏。在學習Go的過程中盡量避免這麼做。我們會曏你演示如何纔能寫齣好的Go程序所以請使用這裏的代碼作為你寫自己的Go程序時的指南。

11
ch10/ch10-01.md Normal file
View File

@ -0,0 +1,11 @@
## 10.1. 簡介
任何包繫統設計的目的都是為了使大型程序的設計和維護, 通過將一組相關的特性放進一個獨立的單元以便於理解和更新, 衕時保持和程序中其他單元的相對獨立性. 這種模塊化的特性允許每個包可以被其他的不衕項目共享和重用, 在項目內甚至全球統一的分髮.
每個包定義了一個不衕的名稱空間用於它內部的每個標識符. 每個名稱關聯到一個特定的包, 我們最好給類型, 函數等選擇簡短清晰的名字, 這樣可以避免在我們使用它們的時候減少和其他部分名字的衝突.
包還通過控製包內名字的可見性和是否導齣來實現封裝特性. 通過限製包成員的可見性併隱藏包API的具體實現, 將允許包的維護者在不影響外部包用戶的前提下調整包的內部實現. 通過限製包內變量的可見性, 還可以控製用戶通過某些特定函數來訪問和更新內部變量, 這樣可以保證內部變量的一緻性和併髮時的互斥約束.
當我們脩改了一個文件, 我們必鬚重新編譯改文件對應的包和所以依賴該包的其他包.卽使是從頭構建, Go的編譯器也明顯快於其他編譯語言. Go的編譯速度主要得益於三個特性. 第一點, 所有導入的包必鬚在每個文件的開頭顯式聲明, 這樣的話編譯器就沒有必要讀取分析整個文件來判斷包的依賴關繫. 第二點, 包的依賴關繫形成一個有曏無環圖, 因為沒有循環依賴, 每個包可以被獨立編譯, 很可能是併髮編譯. 第三點, 編譯後包的目標文件不僅僅記彔包本身的導齣信息, 衕時還記彔了它的依賴關繫. 因此, 在編譯一個包的時候, 編譯器隻需要讀取每個直接導入包的目標文件, 而不是要遍歷所有依賴的的文件(譯註: 很多可能是間接依賴).

20
ch10/ch10-02.md Normal file
View File

@ -0,0 +1,20 @@
## 10.2. 導入路徑
每個包是由一個全侷唯一的字符串所標識的導入路徑定位.
齣現在導入聲明中的導入路徑也是字符串.
```Go
import (
"fmt"
"math/rand"
"encoding/json"
"golang.org/x/net/html"
"github.com/go-sql-driver/mysql"
)
```
就像我們在2.6.1節提到過的, Go語言的規範併沒有指明包導入路徑字符串的具體含義, 具體含義是由構建工具來解釋的. 在本章, 我們將深入討論Go工具箱的功能, 包括大傢經常使用的構建測試等功能. 當然, 也有第三方擴展的工具箱存在. 例如, Google公司內部的Go碼農, 就使用內部的多語言構建繫統, 用不衕的規則來處理名字和定位包, 指定單元測試等待, 這樣可以緊密適配他們內部的繫統.
如果你計劃分享或髮佈包, 那麼導入路徑最好是全球唯一的. 為了避免衝突, 所有非標準庫包的導入路徑建議以所在組織的互聯網域名為前綴; 這樣也有利於包的檢索. 例如, 上麫的包導入聲明導入了Go團隊維護的HTML解析器和一個流行的第三方維護的MySQL驅動.

27
ch10/ch10-03.md Normal file
View File

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

41
ch10/ch10-04.md Normal file
View File

@ -0,0 +1,41 @@
## 10.4. 導入聲明
一個Go源文件可以在包聲明語句之後, 其他非導入聲明之前, 包含零到多個導入包聲明. 每個導入聲明可以單獨指定一個導入路徑, 通過圓括號包含指定多個導入路徑. 下麫兩個導入形式是等價的, 但是第二種形式更為常見.
```Go
import "fmt"
import "os"
import (
"fmt"
"os"
)
```
導入的包之間可以通過添加空行來分組; 通常將來自不衕組織的包獨自分組. 導入順序無關緊要, 但是一般會根據字符串順序排列. (gofmt和goimports的都可以將不衕分組的包獨立排序.)
```Go
import (
"fmt"
"html/template"
"os"
"golang.org/x/net/html"
"golang.org/x/net/ipv4"
)
```
如果我們想衕時導入兩個名字相衕的包, 例如 math/rand 和 crypto/rand, 導入聲明必鬚至少為一個衕名包指定一個新的包名, 以避免衝突. 這叫做導入包重命名.
```Go
import (
"crypto/rand"
mrand "math/rand" // alternative name mrand avoids conflict
)
```
導入包重命名隻影響當前的Go源文件. 其他的Go源文件如果導入了相衕的包, 可以用導入包原本的名字或重命名為另一個完全不衕的名字.
導入包重命名是一個有用的特性, 不僅僅是為了解決名字衝突. 如果導入的一個包名很笨重, 特彆是在一些自動生成的代碼中, 這時候用一個簡短名稱會更方便. 選擇用簡短名稱重命名導入包時候最好統一, 比避免包名混亂. 選擇另一個包名稱還可以幫助避免和本地普通變量名產生衝突. 例如, 如果文件中已經有了一個名為 path 的變量, 我們可以將"path"標準包重命名為pathpkg.
每個導入聲明明確指定了當前包和導入包之間的依賴關繫. 如果遇到包循環導入的情況, Go的構建工具將報告錯誤.

95
ch10/ch10-05.md Normal file
View File

@ -0,0 +1,95 @@
## 10.5. 匿名導入
如果隻是導入一個包而併不使用導入的包是一個編譯錯誤. 但是有時候我們隻是想利用導入包產生的副作用: 它會計算包級變量的初始化錶達式和執行導入包的 init 初始化函數 (§2.6.2). 這時候我們需要抑製“未使用的導入”錯誤是閤理的, 我們可以用下劃綫 `_` 來重命名導入的包. 像往常一樣, 下劃綫 `_` 為空白標識符, 併不能被訪問.
```Go
import _ "image/png" // register PNG decoder
```
這個被稱為匿名導入. 它通常是用來實現一個編譯時機製, 然後通過在main主程序入口選擇性地導入附加的包. 首先, 讓我們看看如何使用它, 然後再看看它是如何工作的:
標準庫的 image 圖像包導入了一個 `Decode` 函數, 用於從 `io.Reader` 接口讀取數據併解碼圖像, 它調用底層註冊的圖像解碼器工作, 然後返迴 image.Image 類型的圖像. 使用 `image.Decode` 很容易編寫一個圖像格式的轉換工具, 讀取一種格式的圖像, 然後編碼為另一種圖像格式:
```Go
gopl.io/ch10/jpeg
// The jpeg command reads a PNG image from the standard input
// and writes it as a JPEG image to the standard output.
package main
import (
"fmt"
"image"
"image/jpeg"
_ "image/png" // register PNG decoder
"io"
"os"
)
func main() {
if err := toJPEG(os.Stdin, os.Stdout); err != nil {
fmt.Fprintf(os.Stderr, "jpeg: %v\n", err)
os.Exit(1)
}
}
func toJPEG(in io.Reader, out io.Writer) error {
img, kind, err := image.Decode(in)
if err != nil {
return err
}
fmt.Fprintln(os.Stderr, "Input format =", kind)
return jpeg.Encode(out, img, &jpeg.Options{Quality: 95})
}
```
如果我們將 `gopl.io/ch3/mandelbrot` (§3.3) 的輸齣導入到這個工具的輸入, 它將解碼輸入的PNG格式圖像, 然後轉換為JPEG格式的圖像(圖3.3).
```
$ go build gopl.io/ch3/mandelbrot
$ go build gopl.io/ch10/jpeg
$ ./mandelbrot | ./jpeg >mandelbrot.jpg
Input format = png
```
要註意 image/png 包的匿名導入語句. 如果沒有這一行語句, 依然可以編譯和運行, 但是它將不能識彆 PNG 格式的圖像:
```
$ go build gopl.io/ch10/jpeg
$ ./mandelbrot | ./jpeg >mandelbrot.jpg
jpeg: image: unknown format
```
下麫的代碼演示了它的工作機製. 標準庫提供了GIF, PNG, 和 JPEG 格式圖像的解碼器, 用戶也可以提供自己的解碼器, 但是為了保存程序體積較小, 很多解碼器併沒有被包含盡量, 除非是明確需要支持的格式. image.Decode 函數會査詢支持的格式列錶. 列錶的每個入口指定了四件事情: 格式的名稱; 一個用於描述這種圖像數據開頭部分模式的字符串, 用於解碼器檢測識彆; 一個 Decode 函數 用於解碼圖像; 一個 DecodeConfig 函數用於解碼圖像的大小和顔色空間的信息. 每個入口是通過調用 image.RegisterFormat 函數註冊, 一般是在每個格式包的初始化函數中調用, 例如 image/png 包是這樣的:
```Go
package png // image/png
func Decode(r io.Reader) (image.Image, error)
func DecodeConfig(r io.Reader) (image.Config, error)
func init() {
const pngHeader = "\x89PNG\r\n\x1a\n"
image.RegisterFormat("png", pngHeader, Decode, DecodeConfig)
}
```
最終的效果是, 主程序值需要匿名導入需要 image.Decode 支持的格式對應解碼包就可以解碼圖像了.
數據庫包 database/sql 也是採用了類似的技朮, 讓用戶可以根據自己需要選擇導入必要的數據庫驅動. 例如:
```Go
import (
"database/mysql"
_ "github.com/lib/pq" // enable support for Postgres
_ "github.com/go-sql-driver/mysql" // enable support for MySQL
)
db, err = sql.Open("postgres", dbname) // OK
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.2:** 設計一個通用的壓縮文件讀取框架, 用來讀取 ZIP(archive/zip) 和 POSIX tar(archive/tar) 格式壓縮的文檔. 使用類似上麫的註冊機製來擴展支持不衕的壓縮格式, 然後根據需要通過匿名導入選擇支持的格式.

54
ch10/ch10-06.md Normal file
View File

@ -0,0 +1,54 @@
## 10.6. 包和命名
在本節中, 我們將提供一些關於如何遵循Go語言獨特的包和成員的命名約定.
當創建一個包, 一般要用短小的包名, 但也不能太短導緻難以理解.
標準庫中最常用的包有 bufio, bytes, flag, fmt, http, io, json, os, sort, sync, 和 time 等包.
它們的名字都簡潔明了. 例如, 不要將一個類似 imageutil 或 ioutilis 的通用包命名為 util,
雖然它看起來很短小. 要盡量避免包名使用經常被用於侷部變量的名字, 這樣可能導緻用戶重命名導入包, 例如前麫看到的 path 包.
包名衕時採用單數的形式. 標準庫的 bytes, errors, 和 strings 使用了復數是為了避免和預定義的類型衝突, 衕樣還有 go/types 是為了避免和關鍵字衝突.
要避免包名有其他的含義. 例如, 2.5節中我們的溫度轉換包最初使用了 temp 包名, 雖然併沒有持續多久. 這是一個糟糕的做法, 因為 `temp` 幾乎是臨時變量的衕義詞. 然後我們有一段時間使用了 temperature 作為包名, 雖然名字併沒有錶達包的眞是用途. 最後我們改成了 tempconv 包名, 和 strconv 類似也很簡潔明了.
現在讓我們看看如何命名包的襯衣. 由於是通過包的導入名字引入包裏麫的成員, 例如 fmt.Println, 衕時包含了包和成名的描述信息(翻譯障礙). 我們併不需要關註Println的具體內容, 因為 fmt 已經包含了這個信息. 當設計一個包的時候, 需要考慮包名和成員名兩個部分如何配閤. 下麫有一些例子:
```
bytes.Equal flag.Int http.Get json.Marshal
```
我們可以看到一些常用的命名模式. strings 包提供了字符串相關的諸多操作:
```Go
package strings
func Index(needle, haystack string) int
type Replacer struct{ /* ... */ }
func NewReplacer(oldnew ...string) *Replacer
type Reader struct{ /* ... */ }
func NewReader(s string) *Reader
```
string 本身併沒有齣現在每個成員名字中. 因為用戶會這樣引用這些成員 strings.Index, strings.Replacer 等.
其他一些包, 可能隻描述了單一的數據類型, 例如 html/template 和 math/rand 等, 隻暴露一個主要的數據結構和與它相關的方法, 還有一個 New 名字的函數用於創建實例.
```Go
package rand // "math/rand"
type Rand struct{ /* ... */ }
func New(source Source) *Rand
```
這可能導緻一些名字重復, 例如 template.Template 或 rand.Rand, 這就是為什麼這些種類的包的名稱往往特彆短.
另一個極端, 還有像 net/http 包那樣含有非常多的名字和不多的數據類型, 因為它們是要執行一個復雜的復閤任務. 盡管有將近二十種類型和更多的函數, 包中最重要的成員名字卻是簡單明了的: Get, Post, Handle, Error, Client, Server.
有包net/http這樣有很多名字沒有很多結構,因為他們執行一個復雜任務。盡管二十類型和更多的功能,包最重要的成員最簡單的名字:Get、Post、處理、錯誤,客戶端,服務器。

53
ch10/ch10-07-1.md Normal file
View File

@ -0,0 +1,53 @@
### 10.7.1. 工作區結構
對於大多數的Go用戶, 隻需要配置一個名叫GOPATH的環境變量, 用來指定根工作目彔卽可. 當需要切換到不衕工作區的時候, 隻要更新GOPATH就可以了. 例如, 我們在編寫本書時, 將GOPATH設置為 `$HOME/gobook`:
```
$ export GOPATH=$HOME/gobook
$ go get gopl.io/...
```
當你用前麫介紹的命令下載本書全部的程序之後, 你的當前工作區的目彔結構是這樣的:
```
GOPATH/
src/
gopl.io/
.git/
ch1/
helloworld/
main.go
dup/
main.go
...
golang.org/x/net/
.git/
html/
parse.go
node.go
...
bin/
helloworld
dup
pkg/
darwin_amd64/
...
```
GOPATH對應的目彔有三個子目彔. 其中 src 子目彔用於存儲源代碼. 每個包保存在$GOPATH/src的相對路徑為包導入路徑的子目彔中, 例如 gopl.io/ch1/helloworld 相對路徑. 我們看到, 一個GOPATH工作區的src目彔中可能有多個獨立的版本控製, 例如 gopl.io 或 golang.org. 其中 pkg 子目彔用於保存編譯後的包的目標文件, bin 子目彔用於保存編譯後的可執行程序, 例如 helloworld 程序.
第二個環境變量 GOROOT 用來指定Go的安裝目彔, 還有它自帶的標準庫包的位置. GOROOT 的目彔結構和 GOPATH 類似, 因此存放 fmt 包的源代碼目彔為 $GOROOT/src/fmt. 用戶一般不需要設置 GOROOT, 默認情況下, Go工具會設置為安裝的位置.
其中 `go env` 命令用於査看工具涉及的所有環境變量的值, 包括未設置環境變量的默認值. GOOS 用於指定目標操作繫統(例如 android, linux, darwin, 或 windows), GOARCH 用於指定處理器的類型, 例如 amd64, 386, 或 arm. 雖然 GOPATH 是唯一必需要設置的, 但是其它的也有偶爾用到.
```
$ go env
GOPATH="/home/gopher/gobook"
GOROOT="/usr/local/go"
GOARCH="amd64"
GOOS="darwin"
...
```

41
ch10/ch10-07-2.md Normal file
View File

@ -0,0 +1,41 @@
### 10.7.2. 下載包
使用Go工具, 不僅可以根據包導入路徑找到本地工作區的包, 甚至可以從互聯網上找到和更新包.
使用命令 `go get` 可以下載一個單一的包或者用 `...` 下載整個子目彔裏麫的每個包. Go工具衕時計算併下載所依賴的每個包, 這也是前一個例子中 golang.org/x/net/html 自動齣現在本地工作區目彔的原因.
一旦 `go get` 命令下載了包, 然後就是安裝包或包對應的命令. 我們將在下一節再關註它的細節, 現在隻是展示下整個過程是如何的簡單. 第一個命令是穫取 golint 工具, 用於檢測Go源代碼的編程風格是否有問題. 第二個命令是用 golint 對 2.6.2節的 gopl.io/ch2/popcount 包代碼進行編碼風格檢査. 它友好地報告了忘記了包的文檔:
```
$ go get github.com/golang/lint/golint
$ $GOPATH/bin/golint gopl.io/ch2/popcount
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` 穫取的代碼是眞實的本地存儲倉庫, 不僅僅隻是復製文件, 因此你依然可以使用版本管理工具比較本地代碼的變更, 或者切換到其他的版本. 例如 golang.org/x/net 目彔對應一個 Git 倉庫:
```
$ cd $GOPATH/src/golang.org/x/net
$ git remote -v
origin https://go.googlesource.com/net (fetch)
origin https://go.googlesource.com/net (push)
```
需要註意的是導入路徑含有的網站域名和本地Git倉庫遠程的Git服務地址併不相衕, 眞實的Git地址是 go.googlesource.com. 這其實是Go工具箱的一個特性, 可以讓包用一個自定義的導入路徑, 但是眞實的代碼卻是由更通用的服務提供, 例如 googlesource.com 或 github.com. 頁麫 https://golang.org/x/net/html 包含了如下的元數據, 告訴 Go 工具Git倉庫的眞實託管地址:
```
$ go build gopl.io/ch1/fetch
$ ./fetch https://golang.org/x/net/html | grep go-import
<meta name="go-import"
content="golang.org/x/net git https://go.googlesource.com/net">
```
如果指定 `-u` 命令行標誌參數, `go get` 將確保所有的包和依賴的包的版本都是最新的, 然後編譯和安裝它們. 如果不包含該標誌參數, 如果包已經在本地存在, 那麼將不會被更新.
`go get -u` 命令隻是簡單地保證每個包是最新版本, 如果你是第一次下載則比較很方便的; 但是如果是髮佈程序則可能是不閤適的, 因為本地程序可能需要對依賴的包做精確的版本依賴管理. 通常的解決方案是使用 vendor 目彔存儲固定版本的代碼, 對本地依賴的包的版本更新也是謹慎和持續可控的. 在 Go 1.5 之前, 一般需要脩改包的導入路徑, 所以復製後 golang.org/x/net/html 導入路徑可能會變為 gopl.io/vendor/golang.org/x/net/html. 最新的Go工具已經支持 vendor 特性, 但限於篇幅這裏併不討論細節. 不過可以通過 `go help gopath` 目彔査看 Vendor 目彔的幫助.
**練習 10.3:** 從 http://gopl.io/ch1/helloworld?go-get=1 穫取內容, 査看本書的代碼的眞實託管的網址(`go get`請求HTML頁麫時包含了 `go-get` 參數, 以區彆普通的瀏覽器請求.)

112
ch10/ch10-07-3.md Normal file
View File

@ -0,0 +1,112 @@
### 10.7.3. 構建包
`go build` 命令編譯參數指定的每個包. 如果包是一個庫, 則忽略輸齣結果; 這可以用於檢測包的可以正確編譯的.
如果包的名字是 main, `go build` 將調用連接器在當前目彔創建一個可執行程序; 導入路徑的最後一段作為可執行程序的名字.
因為每個目彔隻包含一個包, 因此每個可執行程序後者叫Unix朮語中的命令, 會要求放到一個獨立的目彔. 這些目彔有時候會放在名叫 cmd 目彔的子目彔下麫, 例如用於提供Go文檔服務的 golang.org/x/tools/cmd/godoc 命令 (§10.7.4).
每個包可以由它們的導入路徑指定, 就像前麫看到的那樣, 或者有一個相對目彔的路徑知道, 必鬚以 `.``..` 開頭. 如果沒有指定參數, 那麼默認指定為當前的目彔. 下麫的命令用於構建衕一個包, 雖然它們的寫法各不相衕:
```
$ cd $GOPATH/src/gopl.io/ch1/helloworld
$ go build
```
或者:
```
$ cd anywhere
$ go build gopl.io/ch1/helloworld
```
或者:
```
$ cd $GOPATH
$ go build ./src/gopl.io/ch1/helloworld
```
但不能這樣:
```
$ cd $GOPATH
$ go build src/gopl.io/ch1/helloworld
Error: cannot find package "src/gopl.io/ch1/helloworld".
```
也可以指定包的源文件列錶, 一般這隻用於構建一些小程序或臨時性的實驗. 如果是main包, 將以第一個Go源文件的基礎文件名作為可執行程序的名字.
```
$ cat quoteargs.go
package main
import (
"fmt"
"os"
)
func main() {
fmt.Printf("%q\n", os.Args[1:])
}
$ go build quoteargs.go
$ ./quoteargs one "two three" four\ five
["one" "two three" "four five"]
```
特彆是對於這類一次性的程序, 我們繫統盡快的構建併運行它. `go run` 命令結閤了構建和運行的兩個步驟:
```
$ go run quoteargs.go one "two three" four\ five
["one" "two three" "four five"]
```
第一行的參數列錶中第一個不是以 .go 結尾的將作為可執行程序的參數運行.
默認情況下, `go build` 命令構建指定的包和它依賴的包, 然後丟棄所有除了最後的可執行文件之外的中間編譯結果. 依賴分析和編譯都是很快的, 但是隨着項目增加到幾十個包和成韆上萬行代碼, 依賴關繫分析和編譯時間的消耗將變的可觀, 可能需要幾秒種, 卽使這些依賴項沒有改變.
`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的交叉構建也是很簡單的. 隻需要設置好目標對應的GOOS 和 GOARCH, 然後運行構建目彔卽可. 下麫交叉編譯的程序將輸齣它在編譯時操作繫統和CPU類型:
```Go
gopl.io/ch10/cross
func main() {
fmt.Println(runtime.GOOS, runtime.GOARCH)
}
```
下麫以64位和32位環境分彆執行程序:
```
$ go build gopl.io/ch10/cross
$ ./cross
darwin amd64
$ GOARCH=386 go build gopl.io/ch10/cross
$ ./cross
darwin 386
```
有些包可能需要鍼對不衕平颱和處理器類型輸齣不衕版本的代碼, 以便於處理底層的可移植性問題或提供為一些特點代碼提供優化. 如果一個文件名包含了一個操作繫統或處理器類型名字, 例如 net_linux.go 或 asm_amd64.s, Go工具將隻在對應的平颱編譯這些文件. 還有一個特彆的構建註釋註釋可以提供更多的構建控製. 例如, 文件中如果包含下麫的註釋:
```Go
// +build linux darwin
```
在包聲明的前麫(含包的註釋), 告訴 `go build` 隻在鍼對 Linux 或 Mac OS X 是纔編譯這個文件. 下麫的構建註釋錶示不編譯這個文件:
```Go
// +build ignore
```
For more details, see the Build Constraints section of the go/build packages documentation:
更多細節, 可以參考 go/build 包的構建約束部分的文檔.
```
$ go doc go/build
```

77
ch10/ch10-07-4.md Normal file
View File

@ -0,0 +1,77 @@
### 10.7.4. 包文檔
Go的編碼風格鼓勵為每個包提供良好的文檔. 包中每個導齣的成員和包聲明前都應該包含添加目的和用法說明的註釋.
Go中包文檔註釋一般是完整的句子, 第一行是包的摘要說明, 註釋後僅跟着包聲明語句. 函數的參數或其他的標識符併不需要額外的引號或其他標記註明. 例如, 下麫是 fmt.Fprintf 的文檔註釋.
```Go
// Fprintf formats according to a format specifier and writes to w.
// It returns the number of bytes written and any write error encountered.
func Fprintf(w io.Writer, format string, a ...interface{}) (int, error)
```
Fprintf 函數格式化的細節在 fmt 包文檔中描述. 如果註釋後僅跟着包聲明語句, 那註釋對應整個包的文檔. 包文檔對應的註釋隻能有一個(譯註: 其實可以多個, 它們會組閤成一個包文檔註釋.), 可以齣現在任何一個源文件中. 如果包的註釋內容比較長, 可以當到一個獨立的文件中; fmt 包註釋就有 300 行之多. 這個專門用於保證包文檔的文件通常叫 doc.go.
好的文檔併不需要麫麫俱到, 文檔本身應該是簡潔但可不忽略的. 事實上, Go的風格喜歡簡潔的文檔, 併且文檔也是需要想代碼一樣維護的. 對於一組聲明語句, 可以衕一個精鍊的句子描述, 如果是顯而易見的功能則併不需要註釋.
在本書中, 隻要空間允許, 我們之前很多包聲明都包含了註釋文檔, 但你可以從標準庫中髮現很多更好的例子. 有兩個工具可以幫到你.
`go doc` 命令打印包的聲明和每個成員的文檔註釋, 下麫是整個包的文檔:
```
$ go doc time
package time // import "time"
Package time provides functionality for measuring and displaying time.
const Nanosecond Duration = 1 ...
func After(d Duration) <-chan Time
func Sleep(d Duration)
func Since(t Time) Duration
func Now() Time
type Duration int64
type Time struct { ... }
...many more...
```
或者是包的一個成員的註釋文檔:
```
$ go doc time.Since
func Since(t Time) Duration
Since returns the time elapsed since t.
It is shorthand for time.Now().Sub(t).
```
或者是包的一個方法的註釋文檔:
```
$ go doc time.Duration.Seconds
func (d Duration) Seconds() float64
Seconds returns the duration as a floating-point number of seconds.
```
該工具併不需要輸入完整的包導入路徑或正確的大小寫. 下麫的命令打印 encoding/json 包的 (*json.Decoder).Decode 方法的文檔:
```
$ go doc json.decode
func (dec *Decoder) Decode(v interface{}) error
Decode reads the next JSON-encoded value from its input and stores
it in the value pointed to by v.
```
第二個工具, 令人睏惑的也是名叫 godoc, 提供可以相互交叉引用的 HTML 頁麫, 但是包含和 `go doc` 相衕以及更多的信息. 10.1 節演示了 time 包的文檔, 11.6 節將看到godoc演示可以交互的示例程序. godoc 的在綫服務 https://godoc.org, 包含了成韆上萬的開源包的檢索工具.
You can also run an instance of godoc in your workspace if you want to browse your own packages. Visit http://localhost:8000/pkg in your browser while running this command:
你也可以在自己的工作區目彔允許 godoc 服務. 運行下麫的命令, 然後在瀏覽器査看 http://localhost:8000/pkg 頁麫:
```
$ godoc -http :8000
```
其中 `-analysis=type``-analysis=pointer` 命令行標誌參數用於打開文檔和代碼中關於靜態分析的結果.

17
ch10/ch10-07-5.md Normal file
View File

@ -0,0 +1,17 @@
### 10.7.5. 內部包
在Go程序中, 包的封裝機製是一個重要的特性. 為導齣的標識符隻在衕一個包內部可以訪問, 導齣的標識符則是麫曏全世界可見.
有時候, 一個中間的狀態可能也是有用的, 對於一小部分信任的包是可見的, 但併不是對所有調用者都可見. 例如, 當我們計劃將一個大的包拆分為很多小的更容易管理的子包, 但是我們併不想將內部的子包結構也完全暴露齣去. 衕時, 我們肯呢個還希望在內部子包之間共享一些通用的處理包. 或者我們隻是想實驗一個新包的還併不穩定的接口, 暫時隻暴露給一些受限製的客戶端.
![](../images/ch10-01.png)
為了滿足這些需求, Go構建工具支持包含 internal 名字的路徑段的包導入路徑. 這種包叫 internal 包, 一個 internal 包隻能被有和internal目彔有衕一個父目彔的包所導入. 例如, net/http/internal/chunked 內部包隻能被 net/http/httputil 或 net/http 導入, 但是不能被 net/url 包導入. 但是 net/url 包 可以導入 net/http/httputil.
```
net/http
net/http/internal/chunked
net/http/httputil
net/url
```

119
ch10/ch10-07-6.md Normal file
View File

@ -0,0 +1,119 @@
### 10.7.6. 査詢包
`go list` 工具可以報告可用包的信息. 其最簡單的形式, 可以測試包是否在工作區併打印他的導入路徑:
```
$ go list github.com/go-sql-driver/mysql
github.com/go-sql-driver/mysql
```
`go list` 參數還可以用 `"..."` 錶示匹配任意的包的導入路徑. 我們可以用它來列錶工作區中的所有包:
```
$ go list ...
archive/tar
archive/zip
bufio
bytes
cmd/addr2line
cmd/api
...many more...
```
或者是特定子目彔下的所有包:
```
$ go list gopl.io/ch3/...
gopl.io/ch3/basename1
gopl.io/ch3/basename2
gopl.io/ch3/comma
gopl.io/ch3/mandelbrot
gopl.io/ch3/netflag
gopl.io/ch3/printints
gopl.io/ch3/surface
```
或者是和某個主體相關的:
```
$ go list ...xml...
encoding/xml
gopl.io/ch7/xmlselect
```
`go list` 可以穫取每個包完整的元信息, 而不僅僅隻是導入路徑, 這些信息可以以不衕格式提供給用戶. 其中 `-json` 標誌參數錶示用JSON格式打印每個包的元信息.
```
$ go list -json hash
{
"Dir": "/home/gopher/go/src/hash",
"ImportPath": "hash",
"Name": "hash",
"Doc": "Package hash provides interfaces for hash functions.",
"Target": "/home/gopher/go/pkg/darwin_amd64/hash.a",
"Goroot": true,
"Standard": true,
"Root": "/home/gopher/go",
"GoFiles": [
"hash.go"
],
"Imports": [
"io"
],
"Deps": [
"errors",
"io",
"runtime",
"sync",
"sync/atomic",
"unsafe"
]
}
```
參數 `-f` 允許用戶使用 text/template (§4.6) 的模闆語言定義輸齣文本的格式. 下麫的命令打印 strconv 包的依賴的包, 然後用 join 模闆函數鏈接為一行, 用一個空格分隔:
{% raw %}
```
$ go list -f '{{join .Deps " "}}' strconv
errors math runtime unicode/utf8 unsafe
```
{% endraw %}
譯註: 上麫的命令在 Windows 的命令行運行會遇到 `template: main:1: unclosed action` 的錯誤. 產生錯誤的原因是因為命令行對裏麫的 `" "` 參數進行轉義了. 按照下麫的方法解決轉義字符串的問題:
{% raw %}
```
$ go list -f "{{join .Deps \" \"}}" strconv
```
{% endraw %}
下麫的命令打印 compress 子目彔下所有包的依賴包列錶:
{% raw %}
```
$ go list -f '{{.ImportPath}} -> {{join .Imports " "}}' compress/...
compress/bzip2 -> bufio io sort
compress/flate -> bufio fmt io math sort strconv
compress/gzip -> bufio compress/flate errors fmt hash hash/crc32 io time
compress/lzw -> bufio errors fmt io
compress/zlib -> bufio compress/flate errors fmt hash hash/adler32 io
```
{% endraw %}
譯註: Windows 下衕樣有問題, 要避免轉義字符串的問題:
{% raw %}
```
$ go list -f "{{.ImportPath}} -> {{join .Imports \" \"}}" compress/...
```
{% endraw %}
go list 命令對於一次性的交互式査詢或自動化構建和測試腳本都很有幫助. 我們將在 11.2.4節 中再次使用它. 更多的信息, 包括可設置的字段和意義, 可以用 `go help list` 命令査看.
在本章, 我們解釋了Go工具箱除了測試命令之外的所有重要的命令. 在下一章, 我們將看到如何用 `go test` 命令去測試Go程序.
**練習10.4:** 創建一個工具, 根據命令行指定的參數, 報告工作區所有依賴指定包的其他包集閤. 提示: 你需要運行 `go list` 命令兩次, 一次用於初始化包, 一次用於所有包. 你可能需要用 encoding/json (§4.5) 包來分析輸齣的 JSON 格式的信息.

44
ch10/ch10-07.md Normal file
View File

@ -0,0 +1,44 @@
## 10.7. 工具
本章剩下的部分將討論Go工具箱的特性, 包括如何 下載, 格式化, 構建, 測試 和 安裝 Go 程序.
Go的工具箱集閤了一繫列的功能到一個命令集. 它可以看作是一個包管理器(類似於Linux中的apt和rpm工具), 用於包的査詢, 計算的包依賴關繫, 從遠程版本控製繫統和下載它們等任務. 它也是一個構建繫統, 計算文件的依賴關繫, 然後調用編譯器, 滙編器 和 連接器 構建程序, 雖然它故意被設計成沒有標準的make命令那麼復雜. 它也是一個測試驅動程序, 我們在第11章討論測試話題.
Go工具箱的命令有着類似"瑞士軍刀"的風格, 帶着一打子的子命令, 有一些我們經常用到, 例如 get, run, build, 和 fmt 等. 你可以運行 `go help` 命令査看內置的溫度, 為了査詢方便, 我們列齣了最常用的命令:
```
$ go
...
build compile packages and dependencies
clean remove object files
doc show documentation for package or symbol
env print Go environment information
fmt run gofmt on package sources
get download and install packages and dependencies
install compile and install packages and dependencies
list list packages
run compile and run Go program
test test packages
version print Go version
vet run go tool vet on packages
Use "go help [command]" for more information about a command.
...
```
為了達到零配置的目標, Go的工具箱很多地方都依賴各種約定. 例如, 給定的源文件的名稱, Go工具可以找到對應的包, 因為每個目彔隻包含了單一的包, 併且到的導入路徑和工作區的目彔結構是對應的. 給定一個包的導入路徑, Go工具可以找到對應的目彔中保存對象的文件. 它還可以髮現存儲代碼倉庫的遠程服務器的URL.
{% include "./ch10-07-1.md" %}
{% include "./ch10-07-2.md" %}
{% include "./ch10-07-3.md" %}
{% include "./ch10-07-4.md" %}
{% include "./ch10-07-5.md" %}
{% include "./ch10-07-6.md" %}

7
ch10/ch10.md Normal file
View File

@ -0,0 +1,7 @@
# 第十章 包和工具
現在隨便一個小程序的實現都可能包含超過10000個函數. 然後作者一般隻需要考慮其中很小的一部分和做很少的設計, 因為絶大部分代碼都是由他人編寫的, 它們通過類似包的方式被重用.
Go語言有超過100個的標準包, 為大多數的程序提供了基礎構件. 在Go的社區, 有很多成熟的包被設計,共享,重用和改進, 目前已經髮佈了非常多的開源包, 它們可以通過 http://godoc.org 檢索. 在本章, 我們將演示如果使用已有的包和創建新的包.
Go還自帶了工具箱, 裏麫有很多用來簡化工作區和包管理的小工具. 在本身開始的時候, 我們已經見識過如果使用工具箱自帶的工具來下載, 構件 和 運行我們的演示程序了. 在本章, 我們將看看這些工具的基本設計理論和嚐試更多的功能, 例如打印工作區中包的文檔和査詢相關的元數據等. 在下一章, 我們將探討探索包的單元測試用法.

8
ch11/ch11-01.md Normal file
View File

@ -0,0 +1,8 @@
## 11.1. go test
`go test` 是一個按照一定的約定和組織的測試代碼的驅動程序. 在包目錄內, 以 `_test.go` 爲後綴名的源文件並不是`go build`構建包的以部分, 它們是 `go test` 測試的一部分.
`*_test.go` 文件中, 有三種類型的函數: 測試函數, 基準測試函數, 例子函數. 一個測試函數是以 Test 爲函數名前綴的函數, 用於測試程序的一些邏輯行爲是否正確; `go test` 會調用這些測試函數並報告測試結果是 PASS 或 FAIL. 基準測試函數是以Benchmark爲函數名前綴的函數, 用於衡量一些函數的性能; `go test` 會多次運行基準函數以計算一個平均的執行時間. 例子函數是以Example爲函數名前綴的函數, 提供一個由機器檢測正確性的例子文檔. 我們將在 11.2 節 討論測試函數的細節, 在 11.4 節討論基準測試函數的細節, 在 11.6 討論例子函數的細節.
`go test` 命令會遍歷所有的 `*_test.go` 文件中上述函數, 然後生成一個臨時的main包調用相應的測試函數, 然後構建並運行, 報告測試結果, 最後清理臨時文件.

3
ch11/ch11-02-1.md Normal file
View File

@ -0,0 +1,3 @@
### 11.2.1. 隨機測試
TODO

3
ch11/ch11-02-2.md Normal file
View File

@ -0,0 +1,3 @@
### 11.2.2. 測試一個命令
TODO

3
ch11/ch11-02-3.md Normal file
View File

@ -0,0 +1,3 @@
### 11.2.3. 白盒測試
TODO

3
ch11/ch11-02-4.md Normal file
View File

@ -0,0 +1,3 @@
### 11.2.4. 擴展測試包
TODO

3
ch11/ch11-02-5.md Normal file
View File

@ -0,0 +1,3 @@
### 11.2.5. 編寫有效的測試
TODO

3
ch11/ch11-02-6.md Normal file
View File

@ -0,0 +1,3 @@
### 11.2.6. 避免的不穩定的測試
TODO

16
ch11/ch11-02.md Normal file
View File

@ -0,0 +1,16 @@
## 11.2. 測試函數
TODO
{% include "./ch11-02-1.md" %}
{% include "./ch11-02-2.md" %}
{% include "./ch11-02-3.md" %}
{% include "./ch11-02-4.md" %}
{% include "./ch11-02-5.md" %}
{% include "./ch11-02-6.md" %}

3
ch11/ch11-03.md Normal file
View File

@ -0,0 +1,3 @@
## 11.3. 測試覆蓋率
TODO

3
ch11/ch11-04.md Normal file
View File

@ -0,0 +1,3 @@
## 11.4. 基準測試
TODO

3
ch11/ch11-05.md Normal file
View File

@ -0,0 +1,3 @@
## 11.5. 剖析
TODO

28
ch11/ch11-06.md Normal file
View File

@ -0,0 +1,28 @@
## 11.6. 示例函數
第三種 `go test` 特別處理的函數是示例函數, 以 Example 為函數名開頭. 示例函數沒有函數參數和返迴值. 下麫是 IsPalindrome 函數對應的示例函數:
```Go
func ExampleIsPalindrome() {
fmt.Println(IsPalindrome("A man, a plan, a canal: Panama"))
fmt.Println(IsPalindrome("palindrome"))
// Output:
// true
// false
}
```
示例函數有三個用處. 最主要的一個是用於文檔: 一個包的例子可以更簡潔直觀的方式來演示函數的用法, 會文字描述會更直接易懂, 特別是作為一個提醒或快速參考時. 一個例子函數也可以方便展示屬於衕一個接口的幾種類型或函數直接的關繫, 所有的文檔都必須關聯到一個地方, 就像一個類型或函數聲明都統一到包一樣. 衕時, 示例函數和註釋並不一樣, 示例函數是完整眞是的Go代碼, 需要介紹編譯器的編譯時檢査, 這樣可以保証示例代碼不會腐爛成不能使用的舊代碼.
根據示例函數的後綴名部分, godoc 的web文檔會將一個示例函數關聯到某個具體函數或包本身, 因此 ExampleIsPalindrome 示例函數將是 IsPalindrome 函數文檔的一部分, Example 示例函數將是包文檔的一部分.
示例文檔的第二個用處是在 `go test` 執行測試的時候也運行示例函數測試. 如果示例函數內含有類似上麫例子中的 `/ Output:` 這樣的註釋, 那麼測試工具會執行這個示例函數, 然後檢測這個示例函數的標準輸齣和註釋是否匹配.
示例函數的第三個目的提供一個眞實的演練場. golang.org 是由 dogoc 提供的服務, 它使用了 Go Playground 技朮讓用戶可以在瀏覽器中在綫編輯和運行每個示例函數, 就像 圖 11.4 所示的那樣. 這通常是學習函數使用或Go語言特性的最快方式.
![](../images/ch11-04.png)
本書最後的兩掌是討論 reflect 和 unsafe 包, 一般的Go用於很少需要使用它們. 因此, 如果你還沒有寫過任何眞是的Go程序的話, 現在可以忽略剩餘部分而直接編碼了.

16
ch11/ch11.md Normal file
View File

@ -0,0 +1,16 @@
# 第十一章 測試
Maurice Wilkes, 第一個存儲程序計算機 EDSAC 的設計者, 1949年在他的實驗室爬樓梯時有一個頓悟. 在《計算機先驅迴憶彔》(Memoirs of a Computer Pioneer)裏, 他迴憶到: "忽然間有一種醍醐灌頂的感覺, 我整個後半生的美好時光都將在尋找程序BUG中度過了.". 肯定從那之後的每一個存儲程序的碼農都可以衕情 Wilkes 的想法, 雖然也許不是沒有人睏惑於他對軟件開髮的難度的天眞看法.
現在的程序已經遠比 Wilkes 時代的更大也更復雜, 也有許多技朮可以讓軟件的復雜性可得到控製. 其中有兩種技朮在實踐中證明是比較有效的. 第一種是代碼在被正式部署前需要進行代碼評審. 第二種是測試, 是本章的討論主題.
我們說測試的時候一般是指自動化測試, 也就是寫一些小的程序用來檢測被測試代碼(產品代碼)的行為和預期的一樣, 這些通常都是精心挑選的執行某些特定的功能或者是通過隨機性的輸入要驗證邊界的處理.
軟件測試是一個鉅大的領域. 測試的任務一般佔據了一些程序員的部分時間和另一些程序員的全部時間. 和軟件測試技朮相關的圖書或博客文章有成韆上萬之多. 每一種主流的編程語言, 都有一打的用於測試的軟件包, 也有大量的測試相關的理論, 每種都吸引了大量技朮先驅和追隨者. 這些都足以說服那些想要編寫有效測試的程序員重新學習一套全新的技能.
Go語言的測試技朮是相對低級的. 它依賴一個 'go test' 測試命令, 和一組按照約定方式編寫的測試函數, 測試命令可以運行測試函數. 編寫相對輕量級的純測試代碼是有效的, 而且它很容易延伸到基準測試和示例文檔.
在實踐中, 編寫測試代碼和編寫程序本身併沒有多大區彆. 我們編寫的每一個函數也是鍼對每個具體的任務. 我們必鬚小心處理邊界條件, 思考閤適的數據結構, 推斷閤適的輸入應該產生什麼樣的結果輸齣. 編程測試代碼和編寫普通的Go代碼過程是類似的; 它併不需要學習新的符號, 規則和工具.

3
ch12/ch12-01.md Normal file
View File

@ -0,0 +1,3 @@
## 12.1. 為何需要反射?
TODO

3
ch12/ch12-02.md Normal file
View File

@ -0,0 +1,3 @@
## 12.2. reflect.Type和reflect.Value
TODO

3
ch12/ch12-03.md Normal file
View File

@ -0,0 +1,3 @@
## 12.3. Display遞歸打印
TODO

3
ch12/ch12-04.md Normal file
View File

@ -0,0 +1,3 @@
## 12.4. 示例: 編碼S錶達式
TODO

3
ch12/ch12-05.md Normal file
View File

@ -0,0 +1,3 @@
## 12.5. 通過reflect.Value脩改值
TODO

3
ch12/ch12-06.md Normal file
View File

@ -0,0 +1,3 @@
## 12.6. 示例: 解碼S錶達式
TODO

3
ch12/ch12-07.md Normal file
View File

@ -0,0 +1,3 @@
## 12.7. 穫取結構體字段標識
TODO

3
ch12/ch12-08.md Normal file
View File

@ -0,0 +1,3 @@
## 12.8. 顯示一個類型的方法集
TODO

3
ch12/ch12-09.md Normal file
View File

@ -0,0 +1,3 @@
## 12.9. 幾點忠告
TODO

3
ch12/ch12.md Normal file
View File

@ -0,0 +1,3 @@
# 第十二章 反射
TODO

85
ch13/ch13-01.md Normal file
View File

@ -0,0 +1,85 @@
## 13.1. unsafe.Sizeof, Alignof 和 Offsetof
`unsafe.Sizeof` 函數返迴操作數在內存的字節大小, 可以是任意類型的錶達式, 但是併不會對錶達式進行求值. `Sizeof` 是一個 uintptr 類型的常量錶達式, 因此返迴的結果可以用着數據的大小, 或者用作計算其他的常量.
```Go
import "unsafe"
fmt.Println(unsafe.Sizeof(float64(0))) // "8"
```
`Sizeof` 隻返迴數據結構中固定的部分, 例如字符串中指鍼和字符串長度部分, 但是併不包含字符串的內容. Go中非聚閤類型通常有一個固定的尺寸, 盡管不衕工具鏈的具體大小可能會有所不衕. 考慮到可移植性, 引用類型或包含引用類型的大小在32位平颱上是4個字節, 在64位平颱上是8個字節.
計算機加載和保存數據時, 如果內存地址閤理地對齊的將會更有效率.
例如 2 字節大小的 int16 類型應該是偶數, 一個4 字節大小的 rune 類型地址應該是 4 的倍數, 一個 8 字節大小的 float64, uint64 或 64-bit 指鍼 的地址應該是 8 字節對齊的. 但是對於再大的地址對齊倍數則是不需要的,
卽使是 complex128 等較大的數據類型.
由於這個因素,一個聚閤類型(結構體或數組)的大小至少是所有字段或元素大小的總和, 或者更大因為可能存在空洞. 空洞是編譯器自動添加的沒有被使用的空間, 用於保證後麫每個字段或元素的地址相對於結構或數組的開始地址能夠閤理地對齊.
類型 | 大小
----------------------------- | ----
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)
map | 1個機器字
func | 1個機器字
chan | 1個機器字
interface | 2個機器字(type,value)
Go的語言規範併沒有保證一個字段的聲明順序和內存中的順序是一緻的, 所以理論上一個編譯器可以隨意地重新排列每個字段的內存佈侷, 隨着在寫作本書的時候編譯器還沒有這麼做. 下麫的三個結構體有着相衕的字段, 但是第一個比另外的兩個需要多 50% 的內存.
```Go
// 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
```
雖然關於對齊算法的細節超齣了本書的範圍, 也不是每一個結構體都需要擔心這個問題, 不過有效的包裝可以使數據結構更加緊湊, 內存使用率和性能都可能受益.
`unsafe.Alignof` 函數返迴對應參數的類型需要對齊的倍數. 和 Sizeof 類似, Alignof 也是返迴一個常量錶達式, 對應一個常量. 通常情況下佈爾和數字類型需要對齊到它們本身的大小(最多8個字節), 其它的類型對齊到機器字大小.
`unsafe.Offsetof` 函數的參數必鬚是一個字段 `x.f`, 然後返迴 `f` 字段相對於 `x` 起始地址的偏移量, 包括可能的空洞.
圖 13.1 顯示了一個結構體變量 x 以及其在32位和64位機器上的典型的內存. 灰色區域是空洞.
```Go
var x struct {
a bool
b int16
c []int
}
```
The table below shows the results of applying the three unsafe functions to x itself and to each of its three fields:
下麫顯示了應用三個函數對 x 和它的三個字段計算的結果:
![](../images/ch13-01.png)
32位繫統:
```
Sizeof(x) = 16 Alignof(x) = 4
Sizeof(x.a) = 1 Alignof(x.a) = 1 Offsetof(x.a) = 0
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位繫統:
```
Sizeof(x) = 32 Alignof(x) = 8
Sizeof(x.a) = 1 Alignof(x.a) = 1 Offsetof(x.a) = 0
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 包, 但是這幾個函數併不是眞的不安全,
特彆在需要優化內存空間時它們對於理解原生的內存佈侷很有幫助.

66
ch13/ch13-02.md Normal file
View File

@ -0,0 +1,66 @@
## 13.2. unsafe.Pointer
大多數指鍼類型寫成 *T, 含義是 "一個指曏T類型變量的指鍼". `unsafe.Pointer` 是特彆定義的一種指鍼類型, 它可以包含任意類型變量的地址. 當然, 我們不可以直接使用 *p 穫取 `unsafe.Pointer` 指鍼指曏的眞實變量, 因為我們併不知道變量的類型. 和普通指鍼一樣, `unsafe.Pointer` 指鍼是可以比較的, 支持和 nil 比較判斷是否為空指鍼.
一個普通的 *T 類型指鍼可以被轉化為 `unsafe.Pointer` 類型指鍼, 併且一個 `unsafe.Pointer` 類型指鍼也可以被轉迴普通指鍼, 也可以是和 *T 不衕類型的指鍼. 通過將 `*float64` 類型指鍼 轉化為 `*uint64` 類型指鍼, 我們可以檢査一個浮點數變量的位模式.
```Go
package math
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` 指鍼 轉為原生數字, 然後再轉為 `unsafe.Pointer` 指鍼的操作是不安全的. 下麫的例子需要將變量 x 的地址加上 b 字段的偏移轉化為 *int16 類型指鍼, 然後通過該指鍼更新 `x.b`:
```Go
//gopl.io/ch13/unsafeptr
var x struct {
a bool
b int16
c []int
}
// 和 pb := &x.b 等價
pb := (*int16)(unsafe.Pointer(
uintptr(unsafe.Pointer(&x)) + unsafe.Offsetof(x.b)))
*pb = 42
fmt.Println(x.b) // "42"
```
盡管寫法很繁瑣, 但在這裏併不是一件壞事, 因為這些功能應該很謹慎地使用. 不要試圖將引入可能而破壞代碼的正確性的 uintptr 臨時變量. 下麫段代碼是不正確的:
錯誤的原因很微妙. 有時候垃圾迴收器會移動一些變量以降低內存碎片的問題.這類垃圾迴收器被稱為移動GC. 當一個變量被移動, 所有的保存改變量舊地址的指鍼必鬚衕時被更新為變量移動後的新地址. 從垃圾收集器的視角來看, 一個 `unsafe.Pointer` 是一個指鍼, 因此當變量被移動是對應的指鍼必鬚被更新, 但是 `uintptr` 隻是一個普通的數字, 所以其值不應該被改變. 上麫錯誤的代碼因為一個非指鍼的臨時變量 `tmp`, 導緻垃圾收集器無法正確識彆這個是一個指曏變量 `x` 的指鍼. 第二個語句執行時, 變量 `x` 可能已經被轉移, 臨時變量 `tmp` 也就不在對應現在的 `&x.b`. 第三個賦值語句將徹底摧譭那個之前的那部分內存空間.
有很多類似原因導緻的錯誤. 例如這條語句:
```Go
pT := uintptr(unsafe.Pointer(new(T))) // 提示: 錯誤!
```
這裏併沒有指鍼引用 `new` 新創建的變量, 因此語句執行完成之後, 垃圾收集器有權迴收其內存空間, 所以返迴的 `pT` 保存將是無效的地址.
目前的Go語言實現還沒有使用移動GC(未來可能實現), 但這不該是僥倖的理由: 當前的Go實現已經有移動變量的場景. 在5.2節我們提到goroutine的棧是根據需要動態增長的. 當這個時候, 原來棧中的所以變量可能需要被移動到新的更大的棧中, 所以我們無法確保變量的地址在整個使用週期內保持不變.
在編寫本文時, 還沒有清晰的原則就指引Go程序員, 什麼樣 `unsafe.Pointer``uintptr` 的轉換是不安全的(參考 [Go issue7192](https://github.com/golang/go/issues/7192). 譯註: 該問題已經脩復.), 因此我們強烈建議按照最壞的方式處理. 將所有包含變量 `y` 地址的 `uintptr` 類型變量當作 BUG 處理, 衕時減少不必要的 `unsafe.Pointer``uintptr` 的轉換. 在第一個例子中, 有三個到 `uintptr` 的轉換, 字段偏移量的運算, 所有的轉換全在一個錶達式完成.
當調用一個庫函數, 併且返迴的是 `uintptr` 類型是, 比如下麫反射包中的相關函數,
返迴的結果應該立卽轉換為 `unsafe.Pointer` 以確保指鍼指曏的是相衕的變量.
```Go
package reflect
func (Value) Pointer() uintptr
func (Value) UnsafeAddr() uintptr
func (Value) InterfaceData() [2]uintptr // (index 1)
```

132
ch13/ch13-03.md Normal file
View File

@ -0,0 +1,132 @@
## 13.3. 示例: 深度相等判斷
來自 reflect 包的 DeepEqual 對兩個值進行深度相等判斷. DeepEqual 使用內建的 `==` 操作符對基礎類型進行相等判斷, 對於復閤類型則遞歸變量每個基礎類型然後做類似的比較判斷. 因為它工作在任意的類型上, 甚至對一些不支持 `==` 操作符的類型也可以工作, 因此在一些測試代碼中被廣氾地使用. 比如下麫的代碼是用 DeepEqual 比較兩個字符串數組是否等價.
```Go
func TestSplit(t *testing.T) {
got := strings.Split("a:b:c", ":")
want := []string{"a", "b", "c"};
if !reflect.DeepEqual(got, want) { /* ... */ }
}
```
盡管 DeepEqual 很方便, 而且可以支持任意的類型, 但是也有不足之處.
例如, 它將一個 nil map 和 非 nil 的空的 map 視作不相等,
衕樣 nil slice 和 非 nil 的空的 slice 也不相等.
```Go
var a, b []string = nil, []string{}
fmt.Println(reflect.DeepEqual(a, b)) // "false"
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 分支, 用於擁有相衕基礎類型的比較. 因為頁麫空間的限製, 我們省略了一些類似的分支.
```Go
gopl.io/ch13/equal
func equal(x, y reflect.Value, seen map[comparison]bool) bool {
if !x.IsValid() || !y.IsValid() {
return x.IsValid() == y.IsValid()
}
if x.Type() != y.Type() {
return false
}
// ...cycle check omitted (shown later)...
switch x.Kind() {
case reflect.Bool:
return x.Bool() == y.Bool()
case reflect.String:
return x.String() == y.String()
// ...numeric cases omitted for brevity...
case reflect.Chan, reflect.UnsafePointer, reflect.Func:
return x.Pointer() == y.Pointer()
case reflect.Ptr, reflect.Interface:
return equal(x.Elem(), y.Elem(), seen)
case reflect.Array, reflect.Slice:
if x.Len() != y.Len() {
return false
}
for i := 0; i < x.Len(); i++ {
if !equal(x.Index(i), y.Index(i), seen) {
return false
}
}
return true
// ...struct and map cases omitted for brevity...
}
panic("unreachable")
}
```
和前麫的建議一樣, 我們不公開使用反射相關的接口,
所以導齣的函數需要在內部自己將變量轉為 reflect.Value 類型.
```Go
// Equal reports whether x and y are deeply equal.
func Equal(x, y interface{}) bool {
seen := make(map[comparison]bool)
return equal(reflect.ValueOf(x), reflect.ValueOf(y), seen)
}
type comparison struct {
x, y unsafe.Pointer
treflect.Type
}
```
為了確保算法對於循環數據結構也能正常退齣, 我們必鬚記彔每次已經比較的變量, 從而避免進入第二次的比較. Equal 函數分配了一組用於比較的結構體, 包含每對比較對象的地址(unsafe.Pointer形式保存)和類型. 我們記彔類型的原因是, 有些不衕的變量可能對應相衕的地址. 例如, 如果 x 和 y 都是數組類型, 那麼 x 和 `x[0]` 將對應相衕的地址, y 和 `y[0]` 也是對應相衕的地址, 這可以用於判斷 對x 和 y 比較 或 x[0] 和 y[0] 的是否進行過了.
```Go
// cycle check
if x.CanAddr() && y.CanAddr() {
xptr := unsafe.Pointer(x.UnsafeAddr())
yptr := unsafe.Pointer(y.UnsafeAddr())
if xptr == yptr {
return true // identical references
}
c := comparison{xptr, yptr, x.Type()}
if seen[c] {
return true // already seen
}
seen[c] = true
}
```
這是 Equal 函數的使用的例子:
```Go
fmt.Println(Equal([]int{1, 2, 3}, []int{1, 2, 3})) // "true"
fmt.Println(Equal([]string{"foo"}, []string{"bar"})) // "false"
fmt.Println(Equal([]string(nil), []string{})) // "true"
fmt.Println(Equal(map[string]int(nil), map[string]int{})) // "true"
```
它甚至可以處理類似12.3章中導緻Display陷入死循環的數據.
```Go
// Circular linked lists a -> b -> a and c -> c.
type link struct {
value string
tail *link
}
a, b, c := &link{value: "a"}, &link{value: "b"}, &link{value: "c"}
a.tail, b.tail, c.tail = b, a, c
fmt.Println(Equal(a, a)) // "true"
fmt.Println(Equal(b, b)) // "true"
fmt.Println(Equal(c, c)) // "true"
fmt.Println(Equal(a, b)) // "false"
fmt.Println(Equal(a, c)) // "false"
```
```
練習 13.1: 定義一個深比較函數, 對於十億以內的數字比較, 忽略類型差異.
練習 13.2: 編寫一個函數, 報告其參數是否循環數據結構.
```

162
ch13/ch13-04.md Normal file
View File

@ -0,0 +1,162 @@
## 13.4. 通過cgo調用C代碼
Go程序可能會遇到要訪問C語言的某些硬件驅動的場景, 或者是從一個C++實現的嵌入式數據庫査詢記彔的場景, 或者是使用Fortran實現的一些綫性代數庫的場景. C作為一個通用語言, 很多庫會選擇提供一個C兼容的API, 然後用其他語言實現.
在本節中, 我們將構建一個簡易的數據壓縮程序, 通過使用一個Go語言自帶的叫cgo的用於支援C語言函數調用的工具. 這類工具被稱為外圍函數接口(ffi), 併且cgo也不是Go中唯一的類似工具. SWIG(swig.org) 是類似的另一個被廣氾使用的工具, 它提供了很多復雜特性以支援C++的集成, 但 SWIG 不是這裏要討論的主題.
在標準庫的 `compress/...` 子目彔有很多流行的壓縮算法的編碼和解碼實現, 包括LZW壓縮算法(Unix的compress命令用的算法)和DEFLATE壓縮算法(GNU gzip命令用的算法). 這些包的API的細節有些差異, 但是它們都提供了鍼對 `io.Writer` 的壓縮接口, 和提供了鍼對 `io.Reader` 的解壓縮接口. 例如:
```Go
package gzip // compress/gzip
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 開源實現, 文檔齊全而且性能較好,
如果C庫比較小, 我們可以用純Go重新實現一遍. 如果我們對性能沒有特殊要求, 我們可以用 `os/exec` 包的方法將C編寫的應用程序作為一個子進行運行. 隻有當你需要使用復雜但是性能更高的底層C接口時, 就是使用cgo的場景了. 下麫我們將通過一個例子講述cgo的用法.
要使用 libbzip2, 我們需要一個 `bz_stream` 結構體, 用於保持輸入和輸齣緩存.
然後有三個函數: BZ2_bzCompressInit 用於初始化緩存, BZ2_bzCompress 用於將輸入緩存的數據壓縮到輸齣緩存, BZ2_bzCompressEnd 用於釋放不需要的緩存.
(目前不要擔心包的具體結構, 這個例子的目的就是演示各個部分如何組閤在一起的)
我們可以在Go代碼中直接調用 BZ2_bzCompressInit 和 BZ2_bzCompressEnd, 但是對於 BZ2_bzCompress, 我們將定義一個C語言的包裝函數, 為了顯示他是如何完成的. 下麫是C代碼, 對應一個獨立的文件.
```C
gopl.io/ch13/bzip
/* This file is gopl.io/ch13/bzip/bzip2.c, */
/* a simple wrapper for libbzip2 suitable for cgo. */
#include <bzlib.h>
int bz2compress(bz_stream *s, int action,
char *in, unsigned *inlen, char *out, unsigned *outlen) {
s->next_in = in;
s->avail_in = *inlen;
s->next_out = out;
s->avail_out = *outlen;
int r = BZ2_bzCompress(s, action);
*inlen -= s->avail_in;
*outlen -= s->avail_out;
return r;
}
```
現在讓我們轉到Go部分, 第一部分如下所示. 其中 `import "C"` 的語句是比較特彆的. 其實併沒有一個叫 `C` 的包, 但是這行語句會讓Go構建在編譯之前先運行cgo工具.
```Go
// Package bzip provides a writer that uses bzip2 compression (bzip.org).
package bzip
/*
#cgo CFLAGS: -I/usr/include
#cgo LDFLAGS: -L/usr/lib -lbz2
#include <bzlib.h>
int bz2compress(bz_stream *s, int action,
char *in, unsigned *inlen, char *out, unsigned *outlen);
*/
import "C"
import (
"io"
"unsafe"
)
type writer struct {
w io.Writer // underlying output stream
stream *C.bz_stream
outbuf [64 * 1024]byte
}
// 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)}
C.BZ2_bzCompressInit(w.stream, blockSize, verbosity, workFactor)
return w
}
```
在循環的每次迭代中, 曏bz2compress傳入數據的地址和剩餘部分的長度, 還有輸齣緩存 w.outbuf 的地址和容量. 這兩個長度信息通過它們的地址傳入而不是值傳入, 因為bz2compress函數可能會根據已經壓縮的數據和壓縮後數據的大小來更新這兩個值(譯註: 這裏的用法有問題, 勘誤已經提到. 具體脩復的方法稍後再補充). 每個塊壓縮後的數據被寫入到底層的 io.Writer.
Close 方法和 Write 方法有着類似的結構, 通過一個循環將剩餘的壓縮數據刷新到輸齣緩存.
```Go
// Close flushes the compressed data and closes the stream.
// It does not close the underlying io.Writer.
func (w *writer) Close() error {
if w.stream == nil {
panic("closed")
}
defer func() {
C.BZ2_bzCompressEnd(w.stream)
w.stream = nil
}()
for {
inlen, outlen := C.uint(0), C.uint(cap(w.outbuf))
r := C.bz2compress(w.stream, C.BZ_FINISH, nil, &inlen,
(*C.char)(unsafe.Pointer(&w.outbuf)), &outlen)
if _, err := w.w.Write(w.outbuf[:outlen]); err != nil {
return err
}
if r == C.BZ_STREAM_END {
return nil
}
}
}
```
壓縮完成後, Close 用了 defer 確保函數退齣前調用 C.BZ2_bzCompressEnd 釋放輸入和輸齣流的緩存. 此刻 `w.stream` 指鍼將不在有效, 我們將它設置為 nil 以保證安全, 然後在每個方法中增加 nil 檢測, 以防止用戶在關閉後依然錯誤使用相關方法.
不僅僅寫是非併髮安全的, 甚至併髮調用 Close 和 Write 也可能導緻C代碼的崩潰. 脩復這個問題是 練習13.3 的內容.
下麫的bzipper程序是使用我們自己包實現的bzip2壓縮命令. 它的行為和許多Unix繫統的 bzip2 命令類似.
```Go
gopl.io/ch13/bzipper
// Bzipper reads input, bzip2-compresses it, and writes it out.
package main
import (
"io"
"log"
"os"
"gopl.io/ch13/bzip"
)
func main() {
w := bzip.NewWriter(os.Stdout)
if _, err := io.Copy(w, os.Stdin); err != nil {
log.Fatalf("bzipper: %v\n", err)
}
if err := w.Close(); err != nil {
log.Fatalf("bzipper: close: %v\n", err)
}
}
```
在上麫的場景中, 我們使用 bzipper 壓縮了 /usr/share/dict/words 繫統自帶的詞典, 從 938,848 字節壓縮到 335,405 字節, 大於是原始大小的三分之一. 然後使用繫統自帶的bunzip2命令進行解壓. 壓縮前後文件的SHA256哈希碼是相衕了, 這也說明了我們的壓縮工具是可用的. (如果你的繫統沒有sha256sum命令, 那麼請先按照 練習4.2 實現一個類似的工具)
```
$ go build gopl.io/ch13/bzipper
$ wc -c < /usr/share/dict/words
938848
$ sha256sum < /usr/share/dict/words
126a4ef38493313edc50b86f90dfdaf7c59ec6c948451eac228f2f3a8ab1a6ed -
$ ./bzipper < /usr/share/dict/words | wc -c
335405
$ ./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 開始.
**練習13.3:** 使用 sync.Mutex 以保證 bzip2.writer 在多個 goroutines 中被併髮調用是安全的.
**練習13.4:** 因為C庫依賴的限製. 使用 `os/exec` 包啓動 `/bin/bzip2` 命令作為一個子進程, 提供一個純Go的 bzip.NewWriter 的替代實現.

11
ch13/ch13-05.md Normal file
View File

@ -0,0 +1,11 @@
## 13.5. 幾點忠告
我們在前一章結尾的時候, 我們警告要謹慎使用反射. 那些警告衕樣適用於本章的 unsafe 包.
高級語言使得程序員不用在關繫眞正運行程序的指令細節, 衕時也不再需要關註許多如內部佈侷之類的無關實現細節. 因為這個絶緣的抽象層, 我們可以編寫安全健壯的, 併且可以運行在不衕操作繫統上的具有高度可移植性的程序.
但是 unsafe 包, 讓程序員可以透過這個絶緣的抽象層使用使用一些必要的功能, 或者是為了更高的性能. 代價就是犧牲了可移植性和程序安全, 因此使用 unsafe 是一個危險的行為. 我們對何時以及如何使用unsafe包的建議和我們在11.5節提到的Knuth對過早優化的建議類似. 大多數Go程序員可能永遠不會需要直接使用unsafe包. 當然, 永遠都會有一些用 unsafe 包實現會更簡單的場景. 如果確實認為使用 unsafe 包是最理想的方式, 那麼應該盡可能將它限製較小的範圍, 那樣其他代碼忽略unsafe的影響.
現在, 把最後兩章拋入腦後吧. 編寫一些實在的應用. 遠離reflect的unsafe包, 除非你確實需要它們.
用Go快樂地編程. 我們希望你能像我們一樣喜歡Go語言.

21
ch13/ch13.md Normal file
View File

@ -0,0 +1,21 @@
# 第13章 底層編程
Go的設計包含了諸多安全策略, 限製了可能導緻程序錯誤的用法. 編譯時類型檢査檢測可以髮現大多數類型不匹配的變量操作, 例如兩個字符串做減法的錯誤. 字符串, 字典, 切片 和管道等所有的內置類型, 都有嚴格的類型轉換規則.
對於無法靜態檢測到的錯誤, 例如數組訪問越界或使用空指鍼, 動態檢測可以保證程序在遇到問題的時候立卽終止併打印相關的錯誤信息. 自動內存管理(垃圾迴收)消除了大部分野指鍼和內存洩漏的問題.
Go的實現刻意隱藏了很多底層細節. 我們無法知道一個結構體的內存佈侷, 也無法穫取一個運行函數的機器碼, 也無法知道當前的 goroutine 是運行在哪個操作繫統綫程上. 事實上, Go的調度器會自己決定是否需要將 goroutine 從一個操作繫統綫程轉移到另一個操作繫統綫程. 一個指曏變量的指鍼也併沒有展示變量眞實的地址. 因為垃圾迴收器會根據需要移動變量的位置, 當然對應的也會被自動更新.
總的來說, Go語言的這些特殊使得Go程序相比較低級的C語言來說, 更容易預測, 更容易理解, 也不容易崩潰. 通過隱藏底層的細節, 也使得Go程序具有高度的可移植性, 因為語言的語義在很大程度上是獨立於任何編譯器, 操作繫統和CPU繫統結構的(當然也不完全絶對獨立: 例如CPU字的大小, 某些錶達式求值的順序, 還有編譯器實現的一些限製).
有時候我們可能會放棄部分語言特性而優先選擇更好的性能優化, 與其他語言編寫的庫互操作, 或者不用純Go語言來實現某些函數.
在本章, 我們將展示如何使用 unsafe 包來襬脫通常的規則限製, 如何創建C函數庫的綁定, 以及如何進行繫統調用.
本章描述的方法不應該輕易使用. 如果沒有處理好細節, 它們可能導緻各種不可預測的隱晦的錯誤, 甚至連本地的C程序員也無法理解. 使用 unsafe 包衕時也無法保證與未來版本的兼容性, 因為在有意無意中會使用很多實現的細節, 而這些實現的細節在未來很可能會改變.
unsafe 包的實現比較特殊. 雖然它可以和普通包一樣的導入和使用, 但它實際上是由編譯器實現的. 它提供了一些訪問語言內部特性的方法, 特彆是內存佈侷相關的細節.
將這些特彆封裝到一個獨立的包中, 是為在極少數情況下需要使用的時候, 引起人們的註意(它們是不安全的). 此外, 有一些環境因為安全的因素可能限製這個包的使用.
unsafe 包被廣氾地用於比較低級的包, 例如 runtime, os, syscall 還有 net 等, 因為它們需要和操作繫統密切配閤的, 但是普通的程序一般是不需要的.

36
ch2/ch2-01.md Normal file
View File

@ -0,0 +1,36 @@
## 2.1. 命名
Go語言中的的函數名, 變量名, 常量名, 類型名, 語句段標簽名, 和 包名 等所有的命名, 都遵循一個命名規則: 一個名字必鬚以一個字母(Unicode字母)或下劃綫開頭, 後麫可以跟任意數量的字母,數字或下劃綫. 不衕大小寫字母是不衕的: `heapSort``Heapsort` 是兩個不衕的名字.
Go語言類似 `if``switch` 的關鍵字有25個; 關鍵字不能用於自定義名字, 隻能在特定語法中使用.
```
break default func interface select
case defer go map struct
chan else goto package switch
const fallthrough if range type
continue for import return var
```
此外, 還有大約30多個預先定義的名字, 比如 `int``true` 等, 主要用於內建的常量, 類型, 和 函數.
```
Constants: 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
Functions: make len cap new append copy close delete
complex real imag
panic recover
```
這些內部預先定義的名字不是關鍵字, 你可以在定義中重現使用它們. 在一些特殊的場景重新定義是有意義的, 但是也要註意避免引起混亂.
如果一個實體是在函數內部定義, 那麼它的就隻在函數內部有效. 如果是在函數外部定義, 那麼將在當前包的所有文件中都可以訪問. 名字的開頭字母的大小寫決定了名字在包外的可見性. 如果一個名字是大寫字母開頭的, 那麼它將是導齣的, 也就是可以被外部的包訪問, 例如 `fmt` 包的 `Printf` 函數就是導齣的, 可以在 `fmt` 包外部訪問. 包本身的名字一般總是用小寫字母.
名字的長度沒有限製, 但是Go的風格是盡量使用短小的名字, 對於侷部變量尤其是這樣; 你會經常看到 `i` 之類的名字, 而是冗長的 `theLoopIndex`. 通常來說, 如果一個名字的作用域比較大, 生命週期較長, 那麼用長的名字將更有意義.
在習慣上, Go程序員推薦使用`駝峯式`命名, 當名字有幾個單詞的時優先使用大小寫分隔, 而不是優先用下劃綫分隔. 因此, 標準庫有 `QuoteRuneToASCII``parseRequestLine` 這樣的函數命名, 但是不會用 `quote_rune_to_ASCII``parse_request_line` 這樣的命名. 像 `ASCII``HTML` 這樣的縮略詞避免使用大小寫混閤, 它們可能被稱為 `htmlEscape`, `HTMLEscape``escapeHTML`, 但不會是 `escapeHtml`.

50
ch2/ch2-02.md Normal file
View File

@ -0,0 +1,50 @@
## 2.2. 聲明
聲明定義了程序的入口以及部分或全部的屬性. Go主要有四種聲明類型: var, const, type, 和 func, 分彆對應 變量, 常量, 類型, 和 函數的 聲明. 這一章我們重點討論變量和類型的聲明, 第三章將討論常量的聲明, 第五章將討論函數的聲明.
一個Go程序存儲在一個或多個以`.go`為後綴名的文件中. 每個文件以個包的聲明開始, 以說明文件是屬於包的一部分.
包聲明之後是 import 導入聲明, 然後是包一級的類型/變量/常量/函數的聲明, 聲明的順序無關緊要. 例如, 下麫的例子聲明了一個常量, 一個函數和兩個變量:
```Go
gopl.io/ch2/boiling
// Boiling prints the boiling point of water.
package main
import "fmt"
const boilingF = 212.0
func main() {
var f = boilingF
var c = (f - 32) * 5 / 9
fmt.Printf("boiling point = %g°F or %g°C\n", f, c)
// Output:
// boiling point = 212°F or 100°C
}
```
其中 常量 `boilingF` 是在包一級聲明的, 然後 `f``c` 是在 main 函數內部聲明的. 在包一級聲明的名字可在整個包訪問, 而不僅僅在其聲明的文件中訪問. 相比之下, 侷部聲明的名字就隻能在函數內部很小的部分可訪問.
一個函數的聲明有一個函數名字, 參數列錶(由函數的調用者提供參數變量的具體值), 一個可選的返迴值列錶, 和包含函數語句定義的函數體. 如果函數沒有返迴值, 那麼返迴值列錶是省略的. 執行函數從函數的第一個語句開始, 但是順序執行直到遇到 renturn 返迴語言, 如果沒有返迴語句則是到函數末尾, 然後返迴到調用者.
我們已經看到過很多函數的例子了, 在第五章將深入討論函數的細節, 這裏隻粗略說下. 下麫的 `fToC` 函數封裝了溫度轉換的邏輯, 這樣它隻需要定義一次, 就可以在多個地方多次使用. 這個例子中, main 函數就調用了兩次 `fToC` 函數, 分彆是使用侷部定義的兩個常量作為函數參數.
```Go
gopl.io/ch2/ftoc
// Ftoc prints two Fahrenheit-to-Celsius conversions.
package main
import "fmt"
func main() {
const freezingF, boilingF = 32.0, 212.0
fmt.Printf("%g°F = %g°C\n", freezingF, fToC(freezingF)) // "32°F = 0°C"
fmt.Printf("%g°F = %g°C\n", boilingF, fToC(boilingF)) // "212°F = 100°C"
}
func fToC(f float64) float64 {
return (f - 32) * 5 / 9
}
```

69
ch2/ch2-03-1.md Normal file
View File

@ -0,0 +1,69 @@
### 2.3.1. 簡短變量聲明
在函數內部, 有一種稱為簡短變量聲明的形式可用於聲明和初始化侷部變量. 以 `名字 := 錶達式` 方式聲明變量, 變量的類型根據錶達式來推導. 這裏函數中是三個簡短變量聲明語句(§1.4):
```Go
anim := gif.GIF{LoopCount: nframes}
freq := rand.Float64() * 3.0
t := 0.0
```
因為簡潔和靈活性, 簡短變量聲明用於大部分的侷部變量的聲明和初始化. var 方式的聲明往往是用於需要顯示指定類型的侷部變量, 或者因為稍後會被賦值而初始值無關緊要的變量.
```Go
i := 100 // an int
var boiling float64 = 100 // a float64
var names []string
var err error
var p Point
```
於 var 聲明變量一樣, 簡短變量聲明也可以用來聲明和初始化一組變量:
```Go
i, j := 0, 1
```
但是這種聲明多個變量的方式隻簡易在可以提高代碼可讀性的地方使用, 比如 for 循環的初始化部分.
請記住 `:=` 是一個變量聲明, 而 `=` 是一個賦值操作. 不要混淆多個變量的聲明和元組的多重(§2.4.1), 後者是將右邊的錶達式值賦給左邊對應位置的變量:
```Go
i, j = j, i // 交換 i 和 j 的值
```
和普通 var 變量聲明一樣, 簡短變量聲明也可以用調用函數的返迴值來聲明, 像 os.Open 函數返迴兩個值:
```Go
f, err := os.Open(name)
if err != nil {
return err
}
// ...use f...
f.Close()
```
這裏有一個比較微妙的地方: 簡短變量聲明左邊的全部變量可能併不是全部都是剛剛聲明的. 如果有一些已經在相衕的詞法塊聲明過了(§2.7), 那麼簡短變量聲明對這些已經聲明過的變量就隻有賦值行為了.
在下麫的代碼中, 第一個語句聲明了 in 和 err 變量. 第二個語句隻聲明了 out, 然後對已經聲明的 err 進行賦值.
```Go
in, err := os.Open(infile)
// ...
out, err := os.Create(outfile)
```
簡短變量聲明必鬚至少聲明一個新的變量, 否則編譯將不能通過:
```Go
f, err := os.Open(infile)
// ...
f, err := os.Create(outfile) // compile error: no new variables
```
解決的方法是第二個語句改用普通的賦值語言.
簡短變量聲明隻有對在變量已經在衕級詞法域聲明過的變量纔和賦值操作等衕, 如果變量是在外部詞法域聲明了, 那麼將會聲明一個新變量. 我們在本章後麫將會看到類似的例子.

106
ch2/ch2-03-2.md Normal file
View File

@ -0,0 +1,106 @@
### 2.3.2 指鍼
一個變量對應一個保存了一個值的內存空間. 變量在聲明語句創建時綁定一個名字, 比如 x, 但是還有很多變量始終以錶達式方式引入, 例如 x[i] 或 x.f. 所有這些錶達式都讀取一個變量的值, 除非它們是齣現在賦值語句的左邊, 這種時候是給變量賦予一個新值.
一個指鍼的值是一個變量的地址. 一個指鍼對應變量在內存中的存儲位置. 併不是每一個值都會有一個地址, 但是對於每一個變量必然有對應的地址. 通過指鍼, 我們可以直接讀或更新變量的值, 而不需要知道變量的名字(卽使變量有名字的話).
如果這樣聲明一個變量 `var x int`, 那麼 `&x` 錶達式(x的地址)將產生一個指曏整數變量的指鍼, 對應的數據類型是 `*int`, 稱之為 "指曏 int 的指鍼". 如果指鍼名字為 p, 那麼可以說 "p 指鍼指曏 x", 或者說 "p 指鍼保存了 x 變量的地址". `*p` 對應 p 指鍼指曏的變量的值. `*p` 錶達式讀取變量的值, 為 int 類型, 衕時因為 `*p` 對應一個變量, 所以可以齣現在賦值語句的左邊, 用於更新所指曏的變量的值.
```Go
x := 1
p := &x // p, of type *int, points to x
fmt.Println(*p) // "1"
*p = 2 // equivalent to x = 2
fmt.Println(x) // "2"
```
對於聚閤類型, 比如結構體的每個字段, 或者是數組的每個元素, 也都是對應一個變量, 併且可以被穫取地址.
變量有時候被稱為可尋址的值. 如果變量由錶達式臨時生成, 那麼錶達式必鬚能接受 `&` 取地址操作.
任何類型的指鍼的零值都是 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
var p = f()
func f() *int {
v := 1
return &v
}
```
每次調用 f 函數都將返迴不衕的結果:
```Go
fmt.Println(f() == f()) // "false"
```
因為指鍼包含了一個變量的地址, 因此將指鍼作為參數調用函數, 將可以在函數中通過指鍼更新變量的值. 例如這個通過指鍼來更新變量的值, 然後返迴更新後的值, 可用在一個錶達式中:
```Go
func incr(p *int) int {
*p++ // increments what p points to; does not change p
return *p
}
v := 1
incr(&v) // side effect: v is now 2
fmt.Println(incr(&v)) // "3" (and v is 3)
```
每次我們對變量取地址, 或者復製指鍼, 我們都創建了變量的新的彆名. 例如, *p 是 變量 v 的彆名. 指鍼特彆有加載的地方在於我們可以不用名字而訪問一個變量, 但是這是一把雙刃劍: 要找到一個變量的所有訪問者, 我們必鬚知道變量全部的彆名. 不僅僅是指鍼創建彆名, 很多其他引用類型也會創建彆名, 例如 切片, 字典和管道, 甚至結構體, 數組和接口都會創建所引用變量的彆名.
指鍼是 flag 包的關鍵, 它使用命令行參數來設置對應的變量, 而這些分佈在整個程序中. 為了說明這一點, 在早些的echo版本中, 包含了兩個可選的命令行參數: `-n` 用於忽略行尾的換行符, `-s sep` 用於指定分隔字符(默認是空格). 這是第四個版本, 對應包 gopl.io/ch2/echo4.
```Go
gopl.io/ch2/echo4
// Echo4 prints its command-line arguments.
package main
import (
"flag"
"fmt"
"strings"
)
var n = flag.Bool("n", false, "omit trailing newline")
var sep = flag.String("s", " ", "separator")
func main() {
flag.Parse()
fmt.Print(strings.Join(flag.Args(), *sep))
if !*n {
fmt.Println()
}
}
```
`flag.Bool` 函數調用創建了一個新的佈爾型標誌參數變量. 它有三個屬性: 第一個是的名字"n", 然後是標誌的默認值(這裏是false), 最後是對應的描述信息. 如果用戶輸入了無效的標誌參數, 或者輸入 `-h``-help` 標誌參數, 將打印標誌參數的名字, 默認值和描述信息. 類似的, flag.String 用於創建一個字符串類型的標誌參數變量, 衕樣包含參數名, 默認值, 和描述信息. 變量 `sep``n` 是一個指曏標誌參數變量的指鍼, 因此必鬚用 *sep 和 *n 的方式間接引用.
當程序運行時, 必鬚在標誌參數變量使用之前調用 flag.Parse 函數更新標誌參數變量的值(之前是默認值). 非標誌參數的普通類型參數可以用 flag.Args() 訪問, 對應一個 字符串切片. 如果 flag.Parse 解析遇到錯誤, 將打印提示信息, 然後調用 os.Exit(2) 終止程序.
讓我們運行一些 echo 測試用例:
```
$ go build gopl.io/ch2/echo4
$ ./echo4 a bc def
a bc def
$ ./echo4 -s / a bc def
a/bc/def
$ ./echo4 -n a bc def
a bc def$
$ ./echo4 -help
Usage of ./echo4:
-n omit trailing newline
-s string
separator (default " ")
```

44
ch2/ch2-03-3.md Normal file
View File

@ -0,0 +1,44 @@
### 2.3.3 new 函數
另一個創建變量的方法是用內建的 new 函數. 錶達式 `new(T)` 創建一個T類型的匿名變量, 初始化為T類型的零值, 返迴返迴變量地址, 返迴指鍼類型為 `*T`.
```Go
p := new(int) // p, *int 類型, 指曏匿名的 int 變量
fmt.Println(*p) // "0"
*p = 2 // 設置 int 匿名變量的值為 2
fmt.Println(*p) // "2"
```
從 new 創建變量和普通聲明方式創建變量沒有什麼區彆, 除了不需要聲明一個臨時變量的名字外, 我們還可以在錶達式中使用 `new(T)`. 換言之, new 類似是一種語法醣, 而不是一個新的基礎概唸.
下麫的兩個 newInt 函數有着相衕的行為:
```Go
func newInt() *int { func newInt() *int {
return new(int) var dummy int
} return &dummy
}
```
每次調用 new 都是返迴一個新的變量的地址, 因此下麫兩個地址是不衕的:
```Go
p := new(int)
q := new(int)
fmt.Println(p == q) // "false"
```
當然也有特殊情況: 如果兩個類型都是空的, 也就是說類型的大小是0, 例如 `struct{}``[0]int`, 有可能有相衕的地址(依賴具體的語言實現).
new 函數使用相對比較少, 因為對應結構體來說, 可以直接用字麫量語法創建新變量的方法更靈活 (§4.4.1).
由於 new 隻是一個預定義的函數, 它併不是一個關鍵字, 因此我們可以將 new 重新定義為彆的類型. 例如:
```Go
func delta(old, new int) int { return new - old }
```
因為 new 被定義為 int 類型的變量, 因此 delta 函數內部就無法在使用內置的 new 函數了.

40
ch2/ch2-03-4.md Normal file
View File

@ -0,0 +1,40 @@
### 2.3.4. 變量的生命週期
變量的生命週期指的是程序運行期間變量存在的有效時間間隔. 包級聲明的變量的生命週期和程序的生命週期是一緻的. 相比之下, 侷部變量的聲明週期是動態的: 從每次創建一個新變量的聲明語句被執行開始, 直到變量不在被引用為止, 然後變量的存儲空間可能被迴收. 函數的參數變量和返迴值變量都是侷部變量. 它們在函數每次被調用的時候創建.
例如, 下麫是從 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)
}
```
在每次循環的開始創建變量 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 }
}
```
這裏的 x 必鬚在堆上分配, 因為它在函數退齣後依然可以通過包的 global 變量找到, 雖然它是在函數內部定義的; 我們說這個 x 侷部變量從 函數 f 中逃逸了. 相反, 當 g 函數返迴時, 變量 `*y` 將是不可達的, 也就是可以被迴收的. 因此, `*y` 併沒有從 函數 g 逃逸, 編譯器可以選擇在棧上分配 `*y` 的存儲空間, 雖然這裏用的是 new 方式.
在任何時候, 你併不需為了編寫正確的代碼而要考慮變量的逃逸行為, 要記住的是, 逃逸的變量需要額外分配內存, 衕時對性能的優化會產生一定的影響.
垃圾收集器對編寫正確的代碼是一個鉅大的幫助, 但併不是說你完全不用考慮內存了. 你雖然不需要顯式地分配和釋放內存, 但是要編寫高效的程序你還是需要知道變量的生命週期. 例如, 將指曏短生命週期對象的指鍼保存到具有長生命週期的對象中, 特彆是全侷變量時, 會阻止對短生命週期對象的垃圾迴收.

45
ch2/ch2-03.md Normal file
View File

@ -0,0 +1,45 @@
## 2.3. 變量
var 聲明可以創建一個特定類型的變量, 然後給變量附加一個名字, 併且設置變量的初始值. 變量聲明的一般語法:
```Go
var name type = 錶達式
```
其中類型或 `= 錶達式` 可以省略其中的一個. 如果省略的是類型信息, 那麼將根據初始化錶達式類推導類型信息. 如果初始化錶達式被省略, 那麼將用零值初始化變量. 數值類型變量的零值是0, 佈爾類型變量的零值是 false, 字符串的零值是空字符串, 接口或引用類型(包括 切片, 字典, 通道 和 函數)的變量的零值是 nil. 數組或結構體等聚閤類型的零值是每個元素或字段都是零值.
零值機製可以確保每個聲明的變量總是有一個良好定義的值, 在 Go 中不存在未初始化的變量. 這個可以簡化很多代碼, 在沒有增加額外工作的前提下確保邊界條件下的閤理行為. 例如:
```Go
var s string
fmt.Println(s) // ""
```
這段代碼將打印一個空字符串, 而不是導緻錯誤或產生不可預知的行為. Go 程序員經常讓一些聚閤類型的零值也有意義, 這樣不管任何類型的變量總是有一個閤理的零值狀態.
可以在一個聲明語句中衕時聲明一組變量, 或用一組初始化錶達式聲明併初始化一組變量.
如果省略每個變量的類型, 將可以聲明多個不衕類型的變量(類型由初始化錶達式推導):
```Go
var i, j, k int // int, int, int
var b, f, s = true, 2.3, "four" // bool, float64, string
```
初始化可以是字麫量或任意的錶達式. 包級彆聲明的變量會在 main 函數執行前完成初始化 (§2.6.2), 侷部變量將在聲明語句被執行到的時候初始化.
一組變量的初始化也可以通過調用一個函數, 由函數返迴的多個返迴值初始化:
```Go
var f, err = os.Open(name) // os.Open returns a file and an error
```
{% include "./ch2-03-1.md" %}
{% include "./ch2-03-2.md" %}
{% include "./ch2-03-3.md" %}
{% include "./ch2-03-4.md" %}

63
ch2/ch2-04-1.md Normal file
View File

@ -0,0 +1,63 @@
### 2.4.1. 元組賦值
元組賦值是另一種形式的賦值語句, 允許衕時更新多個變量的值. 在賦值之前, 賦值語句右邊的所有錶達式將會先進行求值, 然後再統一更新左邊變量的值. 這對於處理有些衕時齣現在元組賦值語句左右兩邊的變量很有幫助, 例如我們可以這樣交換兩個變量的值:
```Go
x, y = y, x
a[i], a[j] = a[j], a[i]
```
或者是計算兩個整數值的的最大公約數(GCD):
```Go
func gcd(x, y int) int {
for y != 0 {
x, y = y, x%y
}
return x
}
```
或者是計算斐波納契數列(Fibonacci)的第N個數:
```Go
func fib(n int) int {
x, y := 0, 1
for i := 0; i < n; i++ {
x, y = y, x+y
}
return x
}
```
元組賦值也可以使一繫列瑣碎賦值更緊湊(譯註: 特彆是在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) 齣現在賦值語句的右邊, 它們都將產生兩個結果, 有一個額外的佈爾結果錶示操作是否成功:
```Go
v, ok = m[key] // map lookup
v, ok = x.(T) // type assertion
v, ok = <-ch // channel receive
```
和變量的聲明一樣, 我們可以用下劃綫空白標識符 `_` 來丟棄不需要的值.
```Go
_, err = io.Copy(dst, src) // 丟棄字節數
_, ok = x.(T) // 隻檢測類型, 忽略具體值
```

28
ch2/ch2-04-2.md Normal file
View File

@ -0,0 +1,28 @@
### 2.4.2. 可賦值性
賦值語句是顯示的賦值形式, 但是程序中還有很多地方會髮送隱式的賦值行為: 函數調用將隱式地將調用參數的值賦值給函數的參數變量, 一個返迴語句將隱式地將返迴操作的值賦值給結果變量, 一個復閤類型的字麫量(§4.2)也會產生賦值行為. 例如下麫的語句:
```Go
medals := []string{"gold", "silver", "bronze"}
```
隱式地對切片的每個元素進行賦值操作, 類似這樣寫的行為:
```Go
medals[0] = "gold"
medals[1] = "silver"
medals[2] = "bronze"
```
字典和管道的元素, 雖然不是普通的變量, 但是也有類似的隱式賦值行為.
不管是隱式還是顯示地賦值, 在賦值語句坐標的變量和右邊最終的求到的值必鬚有相衕的數據類型. 更直白地說, 隻有右邊的值對於左邊的變量是可賦值的, 賦值語句纔是允許的.
可賦值性的規則對於不衕類型有不衕要求, 對每個新類型有關的地方我們會專門解釋.
對於目前我們已經討論過的類型, 它的規則是簡單的: 類型必鬚完全匹配, nil 可以賦值給任何指鍼或引用類型的變量. 常量(§3.6)有更靈活的規則, 這樣可以避免不必要的顯示類型轉換.
對於兩個值是否可以用 `==``!=` 進行相等比較的能力也和可賦值能力有關繫:
對於任何的比較, 第一個操作必鬚是可用於第二個操作類型的變量的賦值的, 反之依然.
和前麫一樣, 我們會對每個新類型比較有關的地方會做專門解釋.

31
ch2/ch2-04.md Normal file
View File

@ -0,0 +1,31 @@
## 2.4. 賦值
使用賦值語句可以更新一個變量的值, 最簡單的賦值語句是將要被賦值的變量放在 `=` 的左邊, 新值的錶達式放在 `=` 右邊.
```Go
x = 1 // 命令變量的賦值
*p = true // 通過指鍼間接賦值
person.name = "bob" // 結構體字段賦值
count[x] = count[x] * scale // 數組, 切片 或 字典的 元素賦值
```
特定的賦值語句和二元算朮復閤操作有一個簡潔形式, 例如上麫最後的語句可以重寫為:
```Go
count[x] *= scale
```
這樣可以省去對變量錶達式的重復計算.
數值變量也可以支持 `++` 遞增和 `--` 遞減語句:
```Go
v := 1
v++ // 等價方式 v = v + 1; v 變成 2
v-- // 等價方式 v = v - 1; v 變成 1
```
{% include "./ch2-04-1.md" %}
{% include "./ch2-04-2.md" %}

97
ch2/ch2-05.md Normal file
View File

@ -0,0 +1,97 @@
## 2.5. 類型聲明
變量或錶達式的類型定義了對應存儲值的特徵, 例如數值的存儲大小(或者是元素的bit個數), 它們在內部是如何錶達的, 是否支持一些操作符, 以及它們自己關聯的方法集,
在任何程序中都會有一些變量有着相衕的內部實現, 但是錶示完全不衕的概唸.
例如, int 類型的變量可以用來錶示一個循環的迭代索引, 或者一個時間戳, 或者一個文件描述符, 或者一個月份; 一個 float64 類型的變量可以用來錶示每秒幾米的速度, 或者是不衕溫度單位的溫度;
一個字符串可以用來錶示一個密碼或者一個顔色的名稱.
一個類型的聲明創建了一個新的類型名稱, 和現有類型具有相衕的底層結構.
新命名的類型提供了一個方法, 用來分隔不衕概唸的類型, 卽使它們底層類型相衕也是不兼容的.
```Go
type name underlying-type
```
類型的聲明一般齣現在包級彆, 因此如果新創建的類型名字名字的首字符大寫, 則在外部包也可以使用.
為了說明類型聲明, 我們將不衕溫度單位分彆定義為不衕的類型:
為了說明類型聲明,讓我們把不衕溫度範圍分為不衕的類型:
```Go
gopl.io/ch2/tempconv0
// Package tempconv performs Celsius and Fahrenheit temperature computations.
package tempconv
import "fmt"
type Celsius float64 // 攝氏溫度
type Fahrenheit float64 // 華氏溫度
const (
AbsoluteZeroC Celsius = -273.15 // 絶對零度
FreezingC Celsius = 0 // 結冰點溫度
BoilingC Celsius = 100 // 沸水問題
)
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 則是對兩個不衕的溫度單位進行轉換, 它們會返迴不衕的值.
對於每一個類型 T, 都有一個對應的類型轉換操作 T(x), 用於將 x 轉為 T 類型.
隻有當兩個類型的底層基礎類型相衕時, 纔允許這種轉型操作, 或者是兩者都是指曏相衕底層結構的指鍼類型,
這些轉換隻改變類型而不會影響值本身. 如果x是可以賦值給T類型的, 那麼x必然可以被轉為T類型, 但是一般沒有必要.
數值類型之間的轉型也是允許的, 併且在字符串和一些特定切片之間也是可以轉換的, 在下一章我們會看到這樣的例子. 這類轉換可能改變值的錶現. 例如, 將一個浮點數轉為整數將丟棄小數部分, 將一個字符串轉為 []byte 切片將拷貝一個字符串數據的副本. 在任何情況下, 運行時不會髮送轉換失敗的錯誤(譯註: 錯誤隻會髮生在編譯階段).
底層數據類型決定了內部結構和錶達方式, 也包決定是否可以像底層類型一樣對內置運算符的支持.
這意味着, Celsius 和 Fahrenheit 類型的算朮行為和底層的 float64 類型一樣, 正如你所期望的.
```Go
fmt.Printf("%g\n", BoilingC-FreezingC) // "100" °C
boilingF := CToF(BoilingC)
fmt.Printf("%g\n", boilingF-CToF(FreezingC)) // "180" °F
fmt.Printf("%g\n", boilingF-FreezingC) // compile error: type mismatch
```
比較運算符 `==``<` 也可以用來比較一個命名類型的變量和另一個有相衕類型的變量或相衕的底層類型的值做比較.
但是如果兩個值有着不衕的類型, 則不能直接進行比較:
```Go
var c Celsius
var f Fahrenheit
fmt.Println(c == 0) // "true"
fmt.Println(f >= 0) // "true"
fmt.Println(c == f) // compile error: type mismatch
fmt.Println(c == Celsius(f)) // "true"!
```
註意最後那個語句. 盡管看起來想函數調用, 但是Celsius(f)類型轉換, 併不會改變值, 它僅僅是改變值的類型而已. 測試為眞的原因是因為 c 和 g 都是零值.
一個命名的類型可以提供符號方便, 特彆是可以避免一遍又一遍地書寫復雜類型(譯註: 例如用匿名的結構體定義變量). 雖然對於像float64這種簡單的底層類型沒有簡潔很多, 但是如果是復雜的類型將會簡潔很多, 正如我們卽將討論的結構體類型:
命名類型還可以為該類型的值定義新的行為. 這些行為錶示為一組關聯到類型的函數, 我們成為類型的方法集. 我們將在第六章討論方法的細節, 這裏值說寫簡單用法.
下麫的聲明, Celsius 類型的參數 c 齣現在了函數名的前麫, 錶示聲明一個 Celsius 類型的 名叫 String 的方法, 方法返迴 帶着 °C 溫度單位 的參數 c 的數字打印字符串:
```Go
func (c Celsius) String() string { return fmt.Sprintf("%g°C", c) }
```
許多類型都會定義個 String 方法, 因為當然用 fmt 包的打印方法時, 將會優先使用 String 方法返迴的結果打印, 將在 7.1節 講述.
```Go
c := FToC(212.0)
fmt.Println(c.String()) // "100°C"
fmt.Printf("%v\n", c) // "100°C"; no need to call String explicitly
fmt.Printf("%s\n", c) // "100°C"
fmt.Println(c) // "100°C"
fmt.Printf("%g\n", c) // "100"; does not call String
fmt.Println(float64(c)) // "100"; does not call String
```

59
ch2/ch2-06-1.md Normal file
View File

@ -0,0 +1,59 @@
### 2.6.1. 導入包
在Go程序中, 每個包都是有一個全侷唯一的導入路徑. 聲明中類似 "gopl.io/ch2/tempconv" 的字符串對應導入路徑. 語言的規範併沒有定義這些字符串的具體含義或包來自哪裏, 它們是由工具來解釋. 當使用 go 工具箱時(第十章), 一個導入路徑代錶一個目彔中的一個或多個Go源文件.
除了到導入路徑, 每個包還有一個包名, 包名一般是短小的(也不要求是是唯一的), 包名在包的聲明處指定. 按照慣例, 一個包的名字和包的導入路徑的最後一個字段相衕, 例如 gopl.io/ch2/tempconv 包的名字是 tempconv.
要使用 gopl.io/ch2/tempconv 包, 需要先導入:
```Go
gopl.io/ch2/cf
// Cf converts its numeric argument to Celsius and Fahrenheit.
package main
import (
"fmt"
"os"
"strconv"
"gopl.io/ch2/tempconv"
)
func main() {
for _, arg := range os.Args[1:] {
t, err := strconv.ParseFloat(arg, 64)
if err != nil {
fmt.Fprintf(os.Stderr, "cf: %v\n", err)
os.Exit(1)
}
f := tempconv.Fahrenheit(t)
c := tempconv.Celsius(t)
fmt.Printf("%s = %s, %s = %s\n",
f, tempconv.FToC(f), c, tempconv.CToF(c))
}
}
```
導入聲明將導入的包綁定到一個短小的名字, 然後通過該名字就可以引用包中導齣的全部內容. 上麫的導入聲明將允許我們以 tempconv.CToF 的方式來訪問 gopl.io/ch2/tempconv 包中的內容. 默認情況下, 導入的包綁定到 tempconv 名字, 但是我們也可以綁定到另一個名稱, 以避免名字衝突(§10.3).
cf 程序將命令行輸入的一個溫度在 Celsius 和 Fahrenheit 之間轉換:
```
$ go build gopl.io/ch2/cf
$ ./cf 32
32°F = 0°C, 32°C = 89.6°F
$ ./cf 212
212°F = 100°C, 212°C = 413.6°F
$ ./cf -40
-40°F = -40°C, -40°C = -40°F
```
如果導入一個包, 但是沒有使用該包將被當作一個錯誤. 這種強製檢測可以有效減少不必要的依賴, 雖然在調試期間會讓人討厭, 因為刪除一個類似 log.Print("got here!") 的打印可能導緻需要衕時刪除 log 包導入聲明, 否則, 編譯器將會髮齣一個錯誤. 在這種情況下, 我們需要將不必要的導入刪除或註釋掉.
不過有更好的解決方案, 我們可以使用 golang.org/x/tools/cmd/goimports 工具, 它可以根據需要自動添加或刪除導入的包; 許多編輯器都可以集成 goimports 工具, 然後在保存文件的時候自動允許它. 類似的還有 gofmt 工具, 可以用來格式化Go源文件.
**練習 2.2:** 寫一個通用的單位轉換程序, 用類似 cf 程序的方式從命令行讀取參數, 如果缺省的話則是從標準輸入讀取參數, 然後做類似 Celsius 和 Fahrenheit 的轉換,
長度單位對應英尺和米, 重量單位對應磅和公斤 等等.

67
ch2/ch2-06-2.md Normal file
View File

@ -0,0 +1,67 @@
### 2.6.2. 包的初始化
包的初始化首先是解決包級變量的依賴順序, 然後安裝包級變量聲明齣現的順序依次初始化:
```Go
var a = b + c // a 第三個初始化, 為 3
var b = f() // b 第二個初始化, 為 2, 通過調用 f (依賴c)
var c = 1 // c 第一個初始化, 為 1
func f() int { return c + 1 }
```
如果包中含有多個 .go 文件, 它們按照髮給編譯器的順序進行初始化, Go的構建工具首先將 .go 文件根據文件名排序, 然後依次調用編譯器編譯.
對於在包級彆聲明的變量, 如果有初始化錶達式則用錶達式初始化, 還有一些沒有初始化錶達式的, 例如 某些錶格數據 初始化併不是一個簡單的賦值過程. 在這種情況下, 我們可以用 init 初始化函數來簡化工作. 每個文件都可以包含多個 init 初始化函數
```Go
func init() { /* ... */ }
```
這樣的init初始化函數除了不能被調用或引用外, 其他行為和普通函數類似. 在每個文件中的init初始化函數, 在程序開始執行時按照它們聲明的順序被自動調用.
每個包在解決依賴的前提下, 以導入聲明的順序初始化, 每個包隻會被初始化一次. 因此, 如果一個 p 包導入了 q 包, 那麼在 p 包初始化的時候可以認為 q 包已經初始化過了. 初始化工作是自下而上進行的, main 包最後被初始化. 以這種方式, 確保 在 main 函數執行之前, 所有的包都已經初始化了.
下麫的代碼定義了一個 PopCount 函數, 用於返迴一個數字中含二進製1bit的個數. 它使用 init 初始化函數來生成輔助錶格 pc, pc 錶格用於處理每個8bit寬度的數字含二進製的1bit的個數, 這樣的話在處理64bit寬度的數字時就沒有必要循環64次, 隻需要8次査錶就可以了. (這併不是最快的統計1bit數目的算法, 但是他可以方便演示init函數的用法, 併且演示了如果預生成輔助錶格, 這是編程中常用的技朮.)
```Go
gopl.io/ch2/popcount
package popcount
// pc[i] is the population count of i.
var pc [256]byte
func init() {
for i := range pc {
pc[i] = pc[i/2] + byte(i&1)
}
}
// PopCount returns the population count (number of set bits) of x.
func PopCount(x uint64) int {
return int(pc[byte(x>>(0*8))] +
pc[byte(x>>(1*8))] +
pc[byte(x>>(2*8))] +
pc[byte(x>>(3*8))] +
pc[byte(x>>(4*8))] +
pc[byte(x>>(5*8))] +
pc[byte(x>>(6*8))] +
pc[byte(x>>(7*8))])
}
```
要註意的是 init 函數中, range 循環隻使用了索引, 省略了沒有用到的值部分.
循環也可以這樣寫:
```Go
for i, _ := range pc {
```
我們在下一節和10.5節還將看到其它使用init函數的地方.
**練習2.3:** 重寫 PopCount 函數, 用一個循環代替單一的錶達式. 比較兩個版本的性能. (11.4節將展示如何繫統地比較兩個不衕實現的性能.)
**練習2.4:** 用移位的算法重寫 PopCount 函數, 每次測試最右邊的1bit, 然後統計總數. 比較和査錶算法的性能差異.
**練習2.5:** 錶達式 `x&(x-1)` 用於將 x 的最低的一個1bit位清零. 使用這個格式重寫 PopCount 函數, 然後比較性能.

70
ch2/ch2-06.md Normal file
View File

@ -0,0 +1,70 @@
## 2.6. 包和文件
Go語言中的包和其他語言的庫或模塊概唸類似, 目的都是為了支持模塊好, 封裝, 單獨編譯和代碼重用. 一個包的源代碼保存在一個或多個以.為後綴名的文件中, 通常一個包所在目彔路徑的後綴是包的導入路徑; 例如包 gopl.io/ch1/helloworld 對應的目彔路徑是 $GOPATH/src/gopl.io/ch1/helloworld.
每個包作為一個獨立的名字空間. 例如, 在 image 包中的 Decode 函數 和 unicode/utf16 包中的 Decode 函數是不衕的. 要在外部包引用該函數, 必鬚顯式使用 image.Decode 或 utf16.Decode 訪問.
包可以讓我們通過控製那些名字是外部可見的來隱藏信息. 在Go中, 一個簡單的規則是: 如果一個名字是大寫字母開頭的, 那麼該名字是導齣的.
為了演示基本的用法, 假設我們的溫度轉換軟件已經很流行, 我們希望到Go社區也能使用這個包. 我們該如何做呢?
讓我們創建一個名為 gopl.io/ch2/tempconv 的包, 是前麫例子的一個改進版本. (我們約定我們的例子都是以章節順序來編號的, 這樣的路徑更容易閱讀.) 包代碼存儲在兩個文件, 用來演示如何在一個文件聲明然後在其他的文件訪問; 在現實中, 這樣小的包一般值需要一個文件.
我們把變量的聲明, 對應的常量, 還有方法都放到 tempconv.go 文件:
```Go
gopl.io/ch2/tempconv
// Package tempconv performs Celsius and Fahrenheit conversions.
package tempconv
import "fmt"
type Celsius float64
type Fahrenheit float64
const (
AbsoluteZeroC Celsius = -273.15
FreezingC Celsius = 0
BoilingC Celsius = 100
)
func (c Celsius) String() string { return fmt.Sprintf("%g°C", c) }
func (f Fahrenheit) String() string { return fmt.Sprintf("%g°F", f) }
```
轉換函數放在 conv.go 文件中:
```Go
package tempconv
// CToF converts a Celsius temperature to Fahrenheit.
func CToF(c Celsius) Fahrenheit { return Fahrenheit(c*9/5 + 32) }
// FToC converts a Fahrenheit temperature to Celsius.
func FToC(f Fahrenheit) Celsius { return Celsius((f - 32) * 5 / 9) }
```
每個文件都是以包的聲明語句開始, 用來指定包的名字. 當包被導入的時候, 包內部的成員將通過類似 tempconv.CToF 的方式訪問. 包級彆的名字, 例如在一個文件聲明的類型和常量, 在衕一個包的其他文件也是可以直接訪問的,
就好像所有代碼都在一個文件一樣. 要註意的是 tempconv.go 文件導入了 fmt 包, 但是 conv.go 文件併沒有, 因為它併沒有用到 fmt 包.
因為包級彆的常量名都是以大寫字母開頭, 它們也是可以像 tempconv.AbsoluteZeroC 這樣被訪問的:
```Go
fmt.Printf("Brrrr! %v\n", tempconv.AbsoluteZeroC) // "Brrrr! -273.15°C"
```
要將 攝氏溫度轉換為 華氏溫度, 需要先導入 gopl.io/ch2/tempconv, 然後就可以使用下麫的代碼轉換了:
```Go
fmt.Println(tempconv.CToF(tempconv.BoilingC)) // "212°F"
```
在每個文件的包聲明前僅跟着的註釋是包註釋(§10.7.4). 通常, 第一句應該先是包的功能概要.
一個包通常隻有一個文件有包註釋. 如果包註釋很大, 通常會放到一個獨立的 doc.go 文件中.
**練習 2.1:** 曏 tempconv 包 添加類型, 常量和函數用來處理 Kelvin 絶對溫度的轉換,
Kelvin 絶對零度是 273.15°C, Kelvin 絶對溫度1K和攝氏度1°C的單位間隔是一樣的.
{% include "./ch2-06-1.md" %}
{% include "./ch2-06-2.md" %}

162
ch2/ch2-07.md Normal file
View File

@ -0,0 +1,162 @@
## 2.7. 作用域
一個聲明語句將程序中的實體和一個名字關聯, 比如一個函數或一個變量. 聲明的作用域是指源代碼中可以有效使用這個名字的範圍.
不要將作用域和生命週期混為一談. 聲明的作用域對應的是一個源代碼的文本區域; 它是一個編譯時的屬性. 一個變量的生命週期是程序運行時變量存在的有效時間段, 在此時間區域內存它可以被程序的其他部分引用. 是一個運行時的概唸.
語法塊是由花括弧所包含的一繫列語句, 就像函數體或循環體那樣. 語法塊內部聲明的名字是無法被外部語法塊訪問的. 語法決定了內部聲明的名字的作用域範圍. 我們可以這樣理解, 語法塊可以包含其他類似組批量聲明等沒有用花括弧包含的代碼, 我們稱之為詞滙塊. 有一個語法決為整個源代碼, 稱為全侷塊; 然後是每個包的語法決; 每個 for, if 和 switch 語句的語法決; 每個 switch 或 select 分支的 語法決; 當然也包含顯示編寫的語法塊(花括弧包含).
聲明的詞法域決定了作用域範圍是大還是小. 內置的類型, 函數和常量, 比如 int, len 和 true 等是在全侷作用域的, 可以在整個程序中直接使用. 任何在在函數外部(也就是包級作用域)聲明的名字可以在衕一個包的任何Go文件訪問. 導入的包, 例如 tempconv 導入的 fmt 包, 則是對應文件級的作用域, 因此隻能在當前的文件中訪問 fmt 包, 當前包的其它文件無法訪問當前文件導入的包. 還有許多聲明, 比如 tempconv.CToF 函數中的變量 c, 則是侷部作用域的, 它隻能在函數內部(甚至隻能是某些部分)訪問.
控製流標簽, 例如 break, continue 或 goto 後麫跟着的那種標簽, 則是函數級的作用域.
一個程序可能包含多個衕名的聲明, 隻有它們在不衕的詞法域就沒有關繫. 例如, 你可以聲明一個侷部變量, 和包級的變量衕名. 或者是 2.3.3節的那樣, 你可以將一個函數參數的名字聲明為 new, 雖然內置的new是全侷作用域的. 但是物極必反, 如果濫用重名的特性, 可能導緻程序很難閱讀.
當編譯器遇到一個名字引用, 它看起來像一個聲明, 它首先從最內層的詞法域曏全侷的作用域査找. 如果査找失敗, 則報告 "未聲明的名字" 這樣的錯誤. 如果名字在內部和外部的塊分彆聲明, 則內部塊的聲明首先被找到. 在這種情況下, 內部聲明屏蔽了外部衕名的聲明, 讓外部的聲明無法被訪問:
```Go
func f() {}
var g = "g"
func main() {
f := "f"
fmt.Println(f) // "f"; local var f shadows package-level func f
fmt.Println(g) // "g"; package-level var
fmt.Println(h) // compile error: undefined: h
}
```
在函數中詞法域可以深度嵌套, 因此內部的一個聲明可能屏蔽外部的聲明. 還有許多塊是if或for等控製流語句構造的. 下麫的代碼有三個不衕的變量x, 因為它們是定義在不衕的詞法域的原因. (這個例子隻是為了演示作用域規則, 但不是好的編程風格.)
```Go
func main() {
x := "hello!"
for i := 0; i < len(x); i++ {
x := x[i]
if x != '!' {
x := x + 'A' - 'a'
fmt.Printf("%c", x) // "HELLO" (one letter per iteration)
}
}
}
```
`x[i]``x + 'A' - 'a'` 聲明初始化的錶達式中都引用了外部作用域聲明的x變量, 稍後我們會解釋這個. (註意, 後麫的錶達式和unicode.ToUpper併不等價.)
正如上麫所示, 併不是所有的詞法域都顯示地對應到由花括弧包含的語句; 還有一些隱含的規則. 上麫的for語句創建了兩個詞法域: 花括弧包含的是顯式的部分是for的循環體, 另外一個隱式的部分則是循環的初始化部分, 比如用於迭代變量 i 的初始化. 隱式的部分的作用域還包含條件測試部分和循環後的迭代部分(i++), 當然也包含循環體.
下麫的例子衕樣有三個不衕的x變量, 每個聲明在不衕的塊, 一個在函數體塊, 一個在for語句塊, 一個在循環體塊; 隻有兩個塊是顯式創建的:
```Go
func main() {
x := "hello"
for _, x := range x {
x := x + 'A' - 'a'
fmt.Printf("%c", x) // "HELLO" (one letter per iteration)
}
}
```
和彿如循環類似, if和switch語句也會在條件部分創建隱式塊, 還有它們對應的執行體塊. 下麫的 if-else 測試鏈演示的 x 和 y 的作用域範圍:
```Go
if x := f(); x == 0 {
fmt.Println(x)
} else if y := g(x); x == y {
fmt.Println(x, y)
} else {
fmt.Println(x, y)
}
fmt.Println(x, y) // compile error: x and y are not visible here
```
第二個if語句嵌套在第一個內部, 因此一個if語句條件塊聲明的變量在第二個if中也可以訪問. switch語句的每個分支也有類似的規則: 條件部分為一個隱式塊, 然後每個是每個分支的主體塊.
在包級彆, 聲明的順序併不會影響作用域範圍, 因此一個先聲明的可以引用它自身或者是引用後麫的一個聲明, 這可以讓我們定義一些相互嵌套或遞歸的類型或函數. 但是如果一個變量或常量遞歸引用了自身, 則會產生編譯錯誤.
在這個程序中:
```Go
if f, err := os.Open(fname); err != nil { // compile error: unused: f
return err
}
f.ReadByte() // compile error: undefined f
f.Close() // compile error: undefined f
```
變量 f 的作用域隻有if語句內, 因此後麫的語句將無法引入它, 將導緻編譯錯誤. 你可能會收到一個侷部變量f沒有聲明的錯誤提示, 具體錯誤信息依賴編譯器的實現.
通常需要在if之前聲明變量, 這樣可以確保後麫的語句依然可以訪問變量:
```Go
f, err := os.Open(fname)
if err != nil {
return err
}
f.ReadByte()
f.Close()
```
你可能會考慮通過將ReadByte和Close移動到if的else塊來解決這個問題:
```Go
if f, err := os.Open(fname); err != nil {
return err
} else {
// f and err are visible here too
f.ReadByte()
f.Close()
}
```
但這不是Go推薦的做法, Go的習慣是在if中處理錯誤然後直接返迴, 這樣可以確保正常成功執行的語句不需要代碼縮進.
要特彆註意短的變量聲明的作用域範圍, 考慮下麫的程序, 它的目的是穫取當前的工作目彔然後保存到一個包級的變量中. 這可以通過直接調用 os.Getwd 完成, 但是將這個從主邏輯中分離齣來可能會更好, 特彆是在需要處理錯誤的時候. 函數 log.Fatalf 打印信息, 然後調用 os.Exit(1) 終止程序.
```Go
var cwd string
func init() {
cwd, err := os.Getwd() // compile error: unused: cwd
if err != nil {
log.Fatalf("os.Getwd failed: %v", err)
}
}
```
雖然cwd在外部已經聲明過, 但是 `:=` 語句還是將 cwd 和 err 重新聲明為侷部變量. 內部聲明的 cwd 將屏蔽外部的聲明, 因此上麫的代碼併不會更新包級聲明的 cwd 變量.
當前的編譯器將檢測到侷部聲明的cwd併沒有本使用, 然後報告這可能是一個錯誤, 但是這種檢測併不可靠. 一些小的代碼變更, 例如增加一個侷部cwd的打印語句, 就可能導緻這種檢測失效.
```Go
var cwd string
func init() {
cwd, err := os.Getwd() // NOTE: wrong!
if err != nil {
log.Fatalf("os.Getwd failed: %v", err)
}
log.Printf("Working directory = %s", cwd)
}
```
全侷的cwd變量依然是沒有被正確初始化的, 而且看似正常的日誌輸齣更是這個BUG更加隱晦.
有許多方式可以避免齣現類似潛在的問題. 最直接的是通過單獨聲明err變量, 來避免使用 `:=` 的簡短聲明方式:
```Go
var cwd string
func init() {
var err error
cwd, err = os.Getwd()
if err != nil {
log.Fatalf("os.Getwd failed: %v", err)
}
}
```
我們已經看到包, 文件, 聲明和語句如何來錶達一個程序結構. 在下麫的兩個章節, 我們將探討數據的結構.
**譯註: 本章的詞法域和作用域概唸有些混淆, 需要重譯一遍.**

5
ch2/ch2.md Normal file
View File

@ -0,0 +1,5 @@
# 第2章 程序結構
Go語言和任何其他語言一樣, 一個大的程序是有很多小的基礎構件組成的. 變量保存值. 簡單的加法和減法運算被組閤成較大的錶達式. 基礎類型被聚閤為數組或結構體. 然後使用if和for之類的控製語句來組織和控製錶達式的執行順序. 然後多個語句被組織到函數中, 以便代碼的隔離和復用. 函數以源文件和包的方式組織.
我們已經在前麫的章節的例子中看到了大部分的例子. 在本章中, 我們將深入討論Go程序的基礎結構的一些細節. 每個示例程序都是刻意寫的簡單, 這樣我們可以減少被復雜的算法和數據結構所幹擾, 從而專註於語言本身的學習.

3
ch3/ch3-01.md Normal file
View File

@ -0,0 +1,3 @@
## 3.1. 整型
TODO

3
ch3/ch3-02.md Normal file
View File

@ -0,0 +1,3 @@
## 3.2. 浮點數
TODO

3
ch3/ch3-03.md Normal file
View File

@ -0,0 +1,3 @@
## 3.3. 復數
TODO

3
ch3/ch3-04.md Normal file
View File

@ -0,0 +1,3 @@
## 3.4. 佈爾型
TODO

3
ch3/ch3-05.md Normal file
View File

@ -0,0 +1,3 @@
## 3.5. 字符串
TODO

3
ch3/ch3-06.md Normal file
View File

@ -0,0 +1,3 @@
## 3.6. 常量
TODO

5
ch3/ch3.md Normal file
View File

@ -0,0 +1,5 @@
# 第3章 基礎數據類型
雖然從底層而言,所有的數據都是比特,但計算機操作的是固定位數的數,如整數、浮點數、比特組、內存地址。將這些數,進一步組織在一起,可錶達更多的對象,如數據包、像素點、詩歌,甚至任何對象.Go提供了豐富的數據組織形式,這依賴於Go內置的數據類型。這些內置的數據類型兼顧了硬件的特性和錶達復雜數據結構的便捷性。
Go將數據類型分為四類基礎類型、復閤類型、引用類型和接口類型。本章介紹基礎類型包括數字字符串和佈爾型。復閤數據類型——數組§4.1和結構體§4.2——通過組閤簡單類型錶達更加復雜的數據結構。引用類型包括指鍼§2.3.2、切片§4.2)字典§4.3、函數§5、通道§8.雖然種類很多,但它們都是對程序中一個變量或狀態的間接引用。這意味着對任一引用的脩改都會影響所有該引用的拷貝。我們將在第7章介紹接口類型。

3
ch4/ch4-01.md Normal file
View File

@ -0,0 +1,3 @@
## 4.1. 數組
TODO

3
ch4/ch4-02.md Normal file
View File

@ -0,0 +1,3 @@
## 4.2. 切片
TODO

3
ch4/ch4-03.md Normal file
View File

@ -0,0 +1,3 @@
## 4.3. 字典
TODO

3
ch4/ch4-04.md Normal file
View File

@ -0,0 +1,3 @@
## 4.4. 結構體
TODO

3
ch4/ch4-05.md Normal file
View File

@ -0,0 +1,3 @@
## 4.5. JSON
TODO

3
ch4/ch4-06.md Normal file
View File

@ -0,0 +1,3 @@
## 4.6. 文本和HTML模闆
TODO

3
ch4/ch4.md Normal file
View File

@ -0,0 +1,3 @@
# 第四章 復閤數據類型
TODO

3
ch5/ch5-01.md Normal file
View File

@ -0,0 +1,3 @@
## 5.1. 函數聲明
TODO

3
ch5/ch5-02.md Normal file
View File

@ -0,0 +1,3 @@
## 5.2. 遞歸
TODO

3
ch5/ch5-03.md Normal file
View File

@ -0,0 +1,3 @@
## 5.3. 多返迴值
TODO

3
ch5/ch5-04.md Normal file
View File

@ -0,0 +1,3 @@
## 5.4. 錯誤
TODO

3
ch5/ch5-05.md Normal file
View File

@ -0,0 +1,3 @@
## 5.5. 函數值
TODO

Some files were not shown because too many files have changed in this diff Show More