回到简体

pull/1/head
chai2010 2016-02-15 11:06:34 +08:00
parent 9e878f9944
commit 2b37b23285
177 changed files with 2354 additions and 2354 deletions

View File

@ -1,14 +1,14 @@
# 貢獻
# 贡献
譯者 | 章節
译者 | 章节
-------------------------------------- | -------------------------
`chai2010 <chaishushan@gmail.com>` | 前言/第2~4章/第10~13章
`Xargin <cao1988228@163.com>` | 第1章/第6章/第8~9章
`CrazySssst` | 第5章
`foreversmart <njutree@gmail.com>` | 第7章
# 譯文授權
# 译文授权
除特别註明外, 本站內容均采用[知識共享-署名(CC-BY) 3.0協議](http://creativecommons.org/licenses/by/3.0/)授權, 代碼遵循[Go項目的BSD協議](http://golang.org/LICENSE)授權.
除特别注明外, 本站内容均采用[知识共享-署名(CC-BY) 3.0协议](http://creativecommons.org/licenses/by/3.0/)授权, 代码遵循[Go项目的BSD协议](http://golang.org/LICENSE)授权.
<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>

View File

@ -1,37 +1,37 @@
# Go語言聖經(中文版)
# Go语言圣经(中文版)
Go語言聖經 [《The Go Programming Language》](http://gopl.io) 中文版本,僅供學習交流之用。
Go语言圣经 [《The Go Programming Language》](http://gopl.io) 中文版本,仅供学习交流之用。
[![](cover_middle.jpg)](http://golang-china.github.io/gopl-zh)
- 在版本http://golang-china.github.io/gopl-zh
- 離線版本http://github.com/golang-china/gopl-zh/archive/gh-pages.zip
- 項目主頁http://github.com/golang-china/gopl-zh
- 原版官http://gopl.io
- 在线版本http://golang-china.github.io/gopl-zh
- 离线版本http://github.com/golang-china/gopl-zh/archive/gh-pages.zip
- 项目主页http://github.com/golang-china/gopl-zh
- 原版官http://gopl.io
### 從源文件構
### 从源文件构
先安NodeJS和GitBook命令行工具(`npm install gitbook-cli -g`命令)。
先安NodeJS和GitBook命令行工具(`npm install gitbook-cli -g`命令)。
1. 運行`go get github.com/golang-china/gopl-zh`,獲取 [源文件](https://github.com/golang-china/gopl-zh/archive/master.zip)。
2. 切換到 `gopl-zh` 目録,運行 `gitbook install`,安裝GitBook插件。
3. 運行`make`,生成`_book`目録
4. 打`_book/index.html`文件。
1. 运行`go get github.com/golang-china/gopl-zh`,获取 [源文件](https://github.com/golang-china/gopl-zh/archive/master.zip)。
2. 切换到 `gopl-zh` 目录,运行 `gitbook install`,安装GitBook插件。
3. 运行`make`,生成`_book`目录
4. 打`_book/index.html`文件。
### 簡體/繁體轉換
### 简体/繁体转换
切片到 `gopl-zh`
切片到 `gopl-zh`
- `make zh2tw``go run zh2tw.go . "\.md$" zh2tw`轉繁體
- `make tw2zh``go run zh2tw.go . "\.md$" tw2zh`轉簡體
- `make zh2tw``go run zh2tw.go . "\.md$" zh2tw`转繁体
- `make tw2zh``go run zh2tw.go . "\.md$" 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>
嚴禁任何商業行爲使用或引用該文檔的全部或部分內容!
严禁任何商业行为使用或引用该文档的全部或部分内容!
歡迎大家提供建議
欢迎大家提供建议

View File

@ -1,129 +1,129 @@
# Summary
* [前言](preface.md)
* [Go言起源](ch0/ch0-01.md)
* [Go語言項目](ch0/ch0-02.md)
* [書的組織](ch0/ch0-03.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)
* [](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)
* [命令行参数](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-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.md)
* [整型](ch3/ch3-01.md)
* [點數](ch3/ch3-02.md)
* [複數](ch3/ch3-03.md)
* [型](ch3/ch3-04.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.md)
* [数组](ch4/ch4-01.md)
* [Slice](ch4/ch4-02.md)
* [Map](ch4/ch4-03.md)
* [結構體](ch4/ch4-04.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)
* [文本和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)
* [方法明](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)
* [接口是合](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)
* [示例: 表式求值](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)
* [示例: 并发的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)
* [并发的循环](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)
* [竞争条件检测](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-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)
* [测试](ch11/ch11.md)
* [go test](ch11/ch11-01.md)
* [測試函數](ch11/ch11-02.md)
* [測試覆蓋率](ch11/ch11-03.md)
* [準測試](ch11/ch11-04.md)
* [测试函数](ch11/ch11-02.md)
* [测试覆盖率](ch11/ch11-03.md)
* [准测试](ch11/ch11-04.md)
* [剖析](ch11/ch11-05.md)
* [示例函](ch11/ch11-06.md)
* [示例函](ch11/ch11-06.md)
* [反射](ch12/ch12.md)
* [何需要反射?](ch12/ch12-01.md)
* [何需要反射?](ch12/ch12-01.md)
* [reflect.Type和reflect.Value](ch12/ch12-02.md)
* [Display遞歸打印](ch12/ch12-03.md)
* [示例: 編碼S表達式](ch12/ch12-04.md)
* [過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)
* [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)
* [](appendix/appendix.md)
* [録A原文勘誤](appendix/appendix-a-errata.md)
* [録B作者譯者](appendix/appendix-b-author.md)
* [録C譯文授權](appendix/appendix-c-cpoyright.md)
* [録D其它語言](appendix/appendix-d-translations.md)
* [示例: 深度相等判](ch13/ch13-03.md)
* [过cgo调用C代码](ch13/ch13-04.md)
* [几点忠告](ch13/ch13-05.md)
* [](appendix/appendix.md)
* [录A原文勘误](appendix/appendix-a-errata.md)
* [录B作者译者](appendix/appendix-b-author.md)
* [录C译文授权](appendix/appendix-c-cpoyright.md)
* [录D其它语言](appendix/appendix-d-translations.md)

View File

@ -1,4 +1,4 @@
## 附録A[原文勘誤](http://www.gopl.io/errata.html)
## 附录A[原文勘误](http://www.gopl.io/errata.html)
**p.9, ¶2:** for "can compared", read "can be compared". (Thanks to Antonio Macías Ojeda, 2015-10-22. Corrected in the second printing.)

View File

@ -1,4 +1,4 @@
## 附録B作者/譯
## 附录B作者/译
### 英文作者
@ -7,9 +7,9 @@
-------
### 中文
### 中文
中文譯者 | 章節
中文译者 | 章节
-------------------------------------- | -------------------------
`chai2010 <chaishushan@gmail.com>` | 前言/第2~4章/第10~13章
`Xargin <cao1988228@163.com>` | 第1章/第6章/第8~9章

View File

@ -1,6 +1,6 @@
## 附録C譯文授權
## 附录C译文授权
除特别註明外, 本站內容均采用[知識共享-署名(CC-BY) 3.0協議](http://creativecommons.org/licenses/by/3.0/)授權, 代碼遵循[Go項目的BSD協議](http://golang.org/LICENSE)授權.
除特别注明外, 本站内容均采用[知识共享-署名(CC-BY) 3.0协议](http://creativecommons.org/licenses/by/3.0/)授权, 代码遵循[Go项目的BSD协议](http://golang.org/LICENSE)授权.
<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>

View File

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

View File

@ -1,6 +1,6 @@
# 索引
<!-- 索引有三列,每列寬度40個字符
<!-- 索引有三列,每列宽度40个字符
+------------------------------------- + ------------------------------------- + ------------
-->
@ -85,7 +85,7 @@ TODO
### P400
<!-- 索引有三列,每列寬度40個字符
<!-- 索引有三列,每列宽度40个字符
+------------------------------------- + ------------------------------------- + ------------
-->

View File

@ -1,6 +1,6 @@
# 附
# 附
英文原版併沒有包含附録部分,隻有一個索引部分。中文版增加附録部分主要用於收録一些和本書相關的內容,比如英文原版的勘誤(有些讀者可能會對照中文和英文原閲讀)、英文作者和中文譯者、譯文授權等內容。以後還可能會考慮增加一些習題解答相關的內容。
英文原版并没有包含附录部分,只有一个索引部分。中文版增加附录部分主要用于收录一些和本书相关的内容,比如英文原版的勘误(有些读者可能会对照中文和英文原阅读)、英文作者和中文译者、译文授权等内容。以后还可能会考虑增加一些习题解答相关的内容。
需要特别説明的是中文版附録併沒有包含英文原版的索引信息。因爲英文原版的索引信息主要是記録每個索引所在的英文頁面位置而中文版是以GitBook方式組織的html網頁形式將英文頁面位置轉爲章節位置可能會更合理不過這個會涉及到繁瑣的手工操作。如果大家有更好的建議請告知我們
需要特别说明的是中文版附录并没有包含英文原版的索引信息。因为英文原版的索引信息主要是记录每个索引所在的英文页面位置而中文版是以GitBook方式组织的html网页形式将英文页面位置转为章节位置可能会更合理不过这个会涉及到繁琐的手工操作。如果大家有更好的建议请告知我们

View File

@ -1,21 +1,21 @@
## Go言起源
## Go言起源
編程語言的演化就像生物物種的演化類似,一個成功的編程語言的後代一般都會繼承它們祖先的優點;當然有時多種語言雜合也可能會産生令人驚訝的特性;還有一些激進的新特性可能併沒有先例。我們可以通過觀察編程語言和軟硬件環境是如何相互促進、相互影響的演化過程而學到很多。
编程语言的演化就像生物物种的演化类似,一个成功的编程语言的后代一般都会继承它们祖先的优点;当然有时多种语言杂合也可能会产生令人惊讶的特性;还有一些激进的新特性可能并没有先例。我们可以通过观察编程语言和软硬件环境是如何相互促进、相互影响的演化过程而学到很多。
圖展示了有哪些早期的編程語言對Go語言的設計産生了重要影響
图展示了有哪些早期的编程语言对Go语言的设计产生了重要影响
![](../images/ch0-01.png)
Go語言有時候被描述爲“C類似語言”或者是“21世紀的C語言”。Go從C語言繼承了相似的表達式語法、控製流結構、基礎數據類型、調用參數傳值、指針等很多思想還有C語言一直所看中的編譯後機器碼的運行效率以及和現有操作繫統的無縫適配。
Go语言有时候被描述为“C类似语言”或者是“21世纪的C语言”。Go从C语言继承了相似的表达式语法、控制流结构、基础数据类型、调用参数传值、指针等很多思想还有C语言一直所看中的编译后机器码的运行效率以及和现有操作系统的无缝适配。
但是在Go語言的家族樹中還有其它的祖先。其中一個有影響力的分支來自[Niklaus Wirth](https://en.wikipedia.org/wiki/Niklaus_Wirth)所設計的[Pascal][Pascal]語言。然後[Modula-2][Modula-2]語言激發了包的概念。然後[Oberon][Oberon]語言摒棄了模塊接口文件和模塊實現文件之間的區别。第二代的[Oberon-2][Oberon-2]語言直接影響了包的導入和聲明的語法,還有[Oberon][Oberon]語言的面向對象特性所提供的方法的聲明語法等。
但是在Go语言的家族树中还有其它的祖先。其中一个有影响力的分支来自[Niklaus Wirth](https://en.wikipedia.org/wiki/Niklaus_Wirth)所设计的[Pascal][Pascal]语言。然后[Modula-2][Modula-2]语言激发了包的概念。然后[Oberon][Oberon]语言摒弃了模块接口文件和模块实现文件之间的区别。第二代的[Oberon-2][Oberon-2]语言直接影响了包的导入和声明的语法,还有[Oberon][Oberon]语言的面向对象特性所提供的方法的声明语法等。
Go語言的另一支祖先帶來了Go語言區别其他語言的重要特性靈感來自於貝爾實驗室的[Tony Hoare](https://en.wikipedia.org/wiki/Tony_Hoare)於1978年發表的鮮爲外界所知的關於併發研究的基礎文獻 *順序通信進程* *[communicating sequential processes][CSP]* ,縮寫爲[CSP][CSP]。在[CSP][CSP]中,程序是一組中間沒有共享狀態的平行運行的處理過程,它們之間使用管道進行通信和控製同步。不過[Tony Hoare](https://en.wikipedia.org/wiki/Tony_Hoare)的[CSP][CSP]隻是一個用於描述併發性基本概念的描述語言,併不是一個可以編寫可執行程序的通用編程語言。
Go语言的另一支祖先带来了Go语言区别其他语言的重要特性灵感来自于贝尔实验室的[Tony Hoare](https://en.wikipedia.org/wiki/Tony_Hoare)于1978年发表的鲜为外界所知的关于并发研究的基础文献 *顺序通信进程* *[communicating sequential processes][CSP]* ,缩写为[CSP][CSP]。在[CSP][CSP]中,程序是一组中间没有共享状态的平行运行的处理过程,它们之间使用管道进行通信和控制同步。不过[Tony Hoare](https://en.wikipedia.org/wiki/Tony_Hoare)的[CSP][CSP]只是一个用于描述并发性基本概念的描述语言,并不是一个可以编写可执行程序的通用编程语言。
接下Rob Pike和其他人開始不斷嚐試將[CSP](https://en.wikipedia.org/wiki/Communicating_sequential_processes)引入實際的編程語言中。他們第一次嚐試引入[CSP](https://en.wikipedia.org/wiki/Communicating_sequential_processes)特性的編程語言叫[Squeak](http://doc.cat-v.org/bell_labs/squeak/)(老鼠間交流的語言),是一個提供鼠標和鍵盤事件處理的編程語言,它的管道是靜態創建的。然後是改進版的[Newsqueak](http://doc.cat-v.org/bell_labs/squeak/)語言提供了類似C語言語句和表達式的語法和類似[Pascal][Pascal]語言的推導語法。Newsqueak是一個帶垃圾迴收的純函數式語言它再次針對鍵盤、鼠標和窗口事件管理。但是在Newsqueak語言中管道是動態創建的屬於第一類值, 可以保存到變量中。
接下Rob Pike和其他人开始不断尝试将[CSP](https://en.wikipedia.org/wiki/Communicating_sequential_processes)引入实际的编程语言中。他们第一次尝试引入[CSP](https://en.wikipedia.org/wiki/Communicating_sequential_processes)特性的编程语言叫[Squeak](http://doc.cat-v.org/bell_labs/squeak/)(老鼠间交流的语言),是一个提供鼠标和键盘事件处理的编程语言,它的管道是静态创建的。然后是改进版的[Newsqueak](http://doc.cat-v.org/bell_labs/squeak/)语言提供了类似C语言语句和表达式的语法和类似[Pascal][Pascal]语言的推导语法。Newsqueak是一个带垃圾回收的纯函数式语言它再次针对键盘、鼠标和窗口事件管理。但是在Newsqueak语言中管道是动态创建的属于第一类值, 可以保存到变量中。
在Plan9操作繫統中,這些優秀的想法被吸收到了一個叫[Alef][Alef]的編程語言中。Alef試圖將Newsqueak語言改造爲繫統編程語言但是因爲缺少垃圾迴收機製而導致併發編程很痛苦。譯註在Aelf之後還有一個叫[Limbo][Limbo]的編程語言Go語言從其中借鑒了很多特性。 具體請參考Pike的講稿http://talks.golang.org/2012/concurrency.slide#9
在Plan9操作系统中,这些优秀的想法被吸收到了一个叫[Alef][Alef]的编程语言中。Alef试图将Newsqueak语言改造为系统编程语言但是因为缺少垃圾回收机制而导致并发编程很痛苦。译注在Aelf之后还有一个叫[Limbo][Limbo]的编程语言Go语言从其中借鉴了很多特性。 具体请参考Pike的讲稿http://talks.golang.org/2012/concurrency.slide#9
Go語言的其他的一些特性零散地來自於其他一些編程語言比如iota語法是從[APL][APL]語言借鑒,詞法作用域與嵌套函數來自於[Scheme][Scheme]語言和其他很多語言。當然我們也可以從Go中發現很多創新的設計。比如Go語言的切片爲動態數組提供了有效的隨機存取的性能這可能會讓人聯想到鏈表的底層的共享機製。還有Go語言新發明的defer語句。
Go语言的其他的一些特性零散地来自于其他一些编程语言比如iota语法是从[APL][APL]语言借鉴,词法作用域与嵌套函数来自于[Scheme][Scheme]语言和其他很多语言。当然我们也可以从Go中发现很多创新的设计。比如Go语言的切片为动态数组提供了有效的随机存取的性能这可能会让人联想到链表的底层的共享机制。还有Go语言新发明的defer语句。
{% include "../links.md" %}

View File

@ -1,16 +1,16 @@
## Go語言項
## Go语言项
所有的編程語言都反映了語言設計者對編程哲學的反思通常包括之前的語言所暴露的一些不足地方的改進。Go項目是在Google公司維護超級複雜的幾個軟件繫統遇到的一些問題的反思但是這類問題絶不是Google公司所特有的
所有的编程语言都反映了语言设计者对编程哲学的反思通常包括之前的语言所暴露的一些不足地方的改进。Go项目是在Google公司维护超级复杂的几个软件系统遇到的一些问题的反思但是这类问题绝不是Google公司所特有的
正如[Rob Pike](http://genius.cat-v.org/rob-pike/)所説,“軟件的複雜性是乘法級相關的”,通過增加一個部分的複雜性來脩複問題通常將慢慢地增加其他部分的複雜性。通過增加功能和選項和配置是脩複問題的最快的途徑,但是這很容易讓人忘記簡潔的內涵,卽使從長遠來看,簡潔依然是好軟件的關鍵因素。
正如[Rob Pike](http://genius.cat-v.org/rob-pike/)所说,“软件的复杂性是乘法级相关的”,通过增加一个部分的复杂性来修复问题通常将慢慢地增加其他部分的复杂性。通过增加功能和选项和配置是修复问题的最快的途径,但是这很容易让人忘记简洁的内涵,即使从长远来看,简洁依然是好软件的关键因素。
簡潔的設計需要在工作開始的時候舍棄不必要的想法,併且在軟件的生命週期內嚴格區别好的改變或壞的改變。通過足夠的努力,一個好的改變可以在不破壞原有完整概念的前提下保持自適應,正如[Fred Brooks](http://www.cs.unc.edu/~brooks/)所説的“概念完整性”;而一個壞的改變則不能達到這個效果,它們僅僅是通過膚淺的和簡單的妥協來破壞原有設計的一致性。隻有通過簡潔的設計,才能讓一個繫統保持穩定、安全和持續的進化。
简洁的设计需要在工作开始的时候舍弃不必要的想法,并且在软件的生命周期内严格区别好的改变或坏的改变。通过足够的努力,一个好的改变可以在不破坏原有完整概念的前提下保持自适应,正如[Fred Brooks](http://www.cs.unc.edu/~brooks/)所说的“概念完整性”;而一个坏的改变则不能达到这个效果,它们仅仅是通过肤浅的和简单的妥协来破坏原有设计的一致性。只有通过简洁的设计,才能让一个系统保持稳定、安全和持续的进化。
Go項目包括編程語言本身附帶了相關的工具和標準庫最後但併非代表不重要的關於簡潔編程哲學的宣言。就事後諸葛的角度來看Go語言的這些地方都做的還不錯擁有自動垃圾迴收、一個包繫統、函數作爲一等公民、詞法作用域、繫統調用接口、隻讀的UTF8字符串等。但是Go語言本身隻有很少的特性也不太可能添加太多的特性。例如它沒有隱式的數值轉換沒有構造函數和析構函數沒有運算符重載沒有默認參數也沒有繼承沒有泛型沒有異常沒有宏沒有函數脩飾更沒有線程局部存儲。但是語言本身是成熟和穩定的而且承諾保證向後兼容用之前的Go語言編寫程序可以用新版本的Go語言編譯器和標準庫直接構建而不需要脩改代碼
Go项目包括编程语言本身附带了相关的工具和标准库最后但并非代表不重要的关于简洁编程哲学的宣言。就事后诸葛的角度来看Go语言的这些地方都做的还不错拥有自动垃圾回收、一个包系统、函数作为一等公民、词法作用域、系统调用接口、只读的UTF8字符串等。但是Go语言本身只有很少的特性也不太可能添加太多的特性。例如它没有隐式的数值转换没有构造函数和析构函数没有运算符重载没有默认参数也没有继承没有泛型没有异常没有宏没有函数修饰更没有线程局部存储。但是语言本身是成熟和稳定的而且承诺保证向后兼容用之前的Go语言编写程序可以用新版本的Go语言编译器和标准库直接构建而不需要修改代码
Go語言有足夠的類型繫統以避免動態語言中那些粗心的類型錯誤但是Go語言的類型繫統相比傳統的強類型語言又要簡潔很多。雖然有時候這會導致一個“無類型”的抽象類型概念但是Go語言程序員併不需要像C++或Haskell程序員那樣糾結於具體類型的安全屬性。在實踐中Go語言簡潔的類型繫統給了程序員帶來了更多的安全性和更好的運行時性能。
Go语言有足够的类型系统以避免动态语言中那些粗心的类型错误但是Go语言的类型系统相比传统的强类型语言又要简洁很多。虽然有时候这会导致一个“无类型”的抽象类型概念但是Go语言程序员并不需要像C++或Haskell程序员那样纠结于具体类型的安全属性。在实践中Go语言简洁的类型系统给了程序员带来了更多的安全性和更好的运行时性能。
Go語言鼓勵當代計算機繫統設計的原則特别是局部的重要性。它的內置數據類型和大多數的準庫數據結構都經過精心設計而避免顯式的初始化或隱式的構造函數因爲很少的內存分配和內存初始化代碼被隱藏在庫代碼中了。Go語言的聚合類型結構體和數組可以直接操作它們的元素隻需要更少的存儲空間、更少的內存分配而且指針操作比其他間接操作的語言也更有效率。由於現代計算機是一個併行的機器Go語言提供了基於CSP的併發特性支持。Go語言的動態棧使得輕量級線程goroutine的初始棧可以很小因此創建一個goroutine的代價很小創建百萬級的goroutine完全是可行的。
Go语言鼓励当代计算机系统设计的原则特别是局部的重要性。它的内置数据类型和大多数的准库数据结构都经过精心设计而避免显式的初始化或隐式的构造函数因为很少的内存分配和内存初始化代码被隐藏在库代码中了。Go语言的聚合类型结构体和数组可以直接操作它们的元素只需要更少的存储空间、更少的内存分配而且指针操作比其他间接操作的语言也更有效率。由于现代计算机是一个并行的机器Go语言提供了基于CSP的并发特性支持。Go语言的动态栈使得轻量级线程goroutine的初始栈可以很小因此创建一个goroutine的代价很小创建百万级的goroutine完全是可行的。
Go語言的標準庫通常被稱爲語言自帶的電池提供了清晰的構建模塊和公共接口包含I/O操作、文本處理、圖像、密碼學、網絡和分布式應用程序等併支持許多標準化的文件格式和編解碼協議。庫和工具使用了大量的約定來減少額外的配置和解釋從而最終簡化程序的邏輯而且每個Go程序結構都是如此的相似因此Go程序也很容易學習。使用Go語言自帶工具構建Go語言項目隻需要使用文件名和標識符名稱, 一個偶爾的特殊註釋來確定所有的庫、可執行文件、測試、基準測試、例子、以及特定於平台的變量、項目的文檔等Go語言源代碼本身就包含了構建規范。
Go语言的标准库通常被称为语言自带的电池提供了清晰的构建模块和公共接口包含I/O操作、文本处理、图像、密码学、网络和分布式应用程序等并支持许多标准化的文件格式和编解码协议。库和工具使用了大量的约定来减少额外的配置和解释从而最终简化程序的逻辑而且每个Go程序结构都是如此的相似因此Go程序也很容易学习。使用Go语言自带工具构建Go语言项目只需要使用文件名和标识符名称, 一个偶尔的特殊注释来确定所有的库、可执行文件、测试、基准测试、例子、以及特定于平台的变量、项目的文档等Go语言源代码本身就包含了构建规范。

View File

@ -1,42 +1,42 @@
## 本書的組織
## 本书的组织
們假設你已經有一種或多種其他編程語言的使用經歷不管是類似C、c++或Java的編譯型語言還是類似Python、Ruby、JavaScript的腳本語言因此我們不會像對完全的編程語言初學者那樣解釋所有的細節。因爲Go語言的變量、常量、表達式、控製流和函數等基本語法也是類似的。
们假设你已经有一种或多种其他编程语言的使用经历不管是类似C、c++或Java的编译型语言还是类似Python、Ruby、JavaScript的脚本语言因此我们不会像对完全的编程语言初学者那样解释所有的细节。因为Go语言的变量、常量、表达式、控制流和函数等基本语法也是类似的。
第一章包含了本敎程的基本結構通過十幾個程序介紹了用Go語言如何實現 類似讀寫文件、文本格式化、創建圖像、網絡客戶端和服務器通訊等日常工作。
第一章包含了本教程的基本结构通过十几个程序介绍了用Go语言如何实现 类似读写文件、文本格式化、创建图像、网络客户端和服务器通讯等日常工作。
第二章描述了Go語言程序的基本元素結構、變量、新類型定義、包和文件、以及作用域的概念。第三章討論了數字、布爾值、字符串和常量併演示了如何顯示和處理Unicode字符。第四章描述了複合類型從簡單的數組、字典、切片到動態列表。第五章涵蓋了函數併討論了錯誤處理、panic和recover還有defer語句。
第二章描述了Go语言程序的基本元素结构、变量、新类型定义、包和文件、以及作用域的概念。第三章讨论了数字、布尔值、字符串和常量并演示了如何显示和处理Unicode字符。第四章描述了复合类型从简单的数组、字典、切片到动态列表。第五章涵盖了函数并讨论了错误处理、panic和recover还有defer语句。
第一章到第五章是基礎部分主流命令式編程語言這部分都類似。個别之處Go語言有自己特色的語法和風格但是大多數程序員能很快適應。其餘章節是Go語言特有的方法、接口、併發、包、測試和反射等語言特性。
第一章到第五章是基础部分主流命令式编程语言这部分都类似。个别之处Go语言有自己特色的语法和风格但是大多数程序员能很快适应。其余章节是Go语言特有的方法、接口、并发、包、测试和反射等语言特性。
Go語言的面向對象機製與一般語言不同。它沒有類層次結構,甚至可以説沒有類;僅僅通過組合(而不是繼承)簡單的對象來構建複雜的對象。方法不僅可以定義在結構體上, 而且可以定義在任何用戶自定義的類型上;併且具體類型和抽象類型(接口)之間的關繫是隱式的,所以很多類型的設計者可能併不知道該類型到底實現了哪些接口。方法在第六章討論,接口在第七章討論
Go语言的面向对象机制与一般语言不同。它没有类层次结构,甚至可以说没有类;仅仅通过组合(而不是继承)简单的对象来构建复杂的对象。方法不仅可以定义在结构体上, 而且可以定义在任何用户自定义的类型上;并且具体类型和抽象类型(接口)之间的关系是隐式的,所以很多类型的设计者可能并不知道该类型到底实现了哪些接口。方法在第六章讨论,接口在第七章讨论
第八章討論了基於順序通信進程(CSP)概念的併發編程使用goroutines和channels處理併發編程。第九章則討論了傳統的基於共享變量的併發編程。
第八章讨论了基于顺序通信进程(CSP)概念的并发编程使用goroutines和channels处理并发编程。第九章则讨论了传统的基于共享变量的并发编程。
第十章描述了包機製和包的組織結構。這一章還展示了如何有效的利用Go自帶的工具使用單個命令完成編譯、測試、基準測試、代碼格式化、文檔以及其他諸多任務
第十章描述了包机制和包的组织结构。这一章还展示了如何有效的利用Go自带的工具使用单个命令完成编译、测试、基准测试、代码格式化、文档以及其他诸多任务
第十一章討論了單元測試Go語言的工具和標準庫中集成了輕量級的測試功能避免了強大但複雜的測試框架。測試庫提供了一些基本構件必要時可以用來構建複雜的測試構件。
第十一章讨论了单元测试Go语言的工具和标准库中集成了轻量级的测试功能避免了强大但复杂的测试框架。测试库提供了一些基本构件必要时可以用来构建复杂的测试构件。
第十二章討論了反射一種程序在運行期間審視自己的能力。反射是一個強大的編程工具不過要謹慎地使用這一章利用反射機製實現一些重要的Go語言庫函數, 展示了反射的強大用法。第十三章解釋了底層編程的細節在必要時可以使用unsafe包繞過Go語言安全的類型繫統
第十二章讨论了反射一种程序在运行期间审视自己的能力。反射是一个强大的编程工具不过要谨慎地使用这一章利用反射机制实现一些重要的Go语言库函数, 展示了反射的强大用法。第十三章解释了底层编程的细节在必要时可以使用unsafe包绕过Go语言安全的类型系统
部分章節的後面有練習題根據對Go語言的理解脩改書中的例子來探索Go語言的用法。
部分章节的后面有练习题根据对Go语言的理解修改书中的例子来探索Go语言的用法。
書中所有的代碼都可以從 http://gopl.io 上的Git倉庫下載。go get命令根據每個例子的導入路徑智能地獲取、構建併安裝。隻需要選擇一個目録作爲工作空間然後將GOPATH環境變量設置爲該路徑
书中所有的代码都可以从 http://gopl.io 上的Git仓库下载。go get命令根据每个例子的导入路径智能地获取、构建并安装。只需要选择一个目录作为工作空间然后将GOPATH环境变量设置为该路径
必要Go語言工具會創建目録。例如:
必要Go语言工具会创建目录。例如:
```
$ export GOPATH=$HOME/gobook # 選擇工作目録
$ go get gopl.io/ch1/helloworld # 獲取/編譯/安裝
$ $GOPATH/bin/helloworld # 行程序
Hello, 世界 # 是中文
$ export GOPATH=$HOME/gobook # 选择工作目录
$ go get gopl.io/ch1/helloworld # 获取/编译/安装
$ $GOPATH/bin/helloworld # 行程序
Hello, 世界 # 是中文
```
運行這些例子需要安裝Go1.5以上的版本。
运行这些例子需要安装Go1.5以上的版本。
```
$ go version
go version go1.5 linux/amd64
```
如果使用其他的操作繫統, 請參考 https://golang.org/doc/install 提供的説明安裝
如果使用其他的操作系统, 请参考 https://golang.org/doc/install 提供的说明安装

View File

@ -1,14 +1,14 @@
## 更多的信息
最佳的幫助信息來自Go語言的官方網站https://golang.org 它提供了完善的參考文檔包括編程語言規范和標準庫等諸多權威的幫助信息。同時也包含了如何編寫更地道的Go程序的基本敎程還有各種各樣的在線文本資源和視頻資源它們是本書最有價值的補充。Go語言的官方博客 https://blog.golang.org 會不定期發布一些Go語言最好的實踐文章包括當前語言的發展狀態、未來的計劃、會議報告和Go語言相關的各種會議的主題等信息譯註 http://talks.golang.org/ 包含了官方收録的各種報告的講稿)。
最佳的帮助信息来自Go语言的官方网站https://golang.org 它提供了完善的参考文档包括编程语言规范和标准库等诸多权威的帮助信息。同时也包含了如何编写更地道的Go程序的基本教程还有各种各样的在线文本资源和视频资源它们是本书最有价值的补充。Go语言的官方博客 https://blog.golang.org 会不定期发布一些Go语言最好的实践文章包括当前语言的发展状态、未来的计划、会议报告和Go语言相关的各种会议的主题等信息译注 http://talks.golang.org/ 包含了官方收录的各种报告的讲稿)。
線訪問的一個有價值的地方是可以從web頁面運行Go語言的程序而紙質書則沒有這麽便利了。這個功能由來自 https://play.golang.org 的 Go Playground 提供,併且可以方便地嵌入到其他頁面中,例如 https://golang.org 的主頁,或 godoc 提供的文檔頁面中。
线访问的一个有价值的地方是可以从web页面运行Go语言的程序而纸质书则没有这么便利了。这个功能由来自 https://play.golang.org 的 Go Playground 提供,并且可以方便地嵌入到其他页面中,例如 https://golang.org 的主页,或 godoc 提供的文档页面中。
Playground可以簡單的通過執行一個小程序來測試對語法、語義和對程序庫的理解類似其他很多語言提供的REPL卽時運行的工具。同時它可以生成對應的url非常適合共享Go語言代碼片段滙報bug或提供反饋意見等。
Playground可以简单的通过执行一个小程序来测试对语法、语义和对程序库的理解类似其他很多语言提供的REPL即时运行的工具。同时它可以生成对应的url非常适合共享Go语言代码片段汇报bug或提供反馈意见等。
於 Playground 構建的 Go Tourhttps://tour.golang.org 是一個繫列的Go語言入門敎程它包含了諸多基本概念和結構相關的併可在線運行的互動小程序。
于 Playground 构建的 Go Tourhttps://tour.golang.org 是一个系列的Go语言入门教程它包含了诸多基本概念和结构相关的并可在线运行的互动小程序。
當然Playground 和 Tour 也有一些限製它們隻能導入標準庫而且因爲安全的原因對一些網絡庫做了限製。如果要在編譯和運行時需要訪問互聯網對於一些更複雜的實驗你可能需要在自己的電腦上構建併運行程序。幸運的是下載Go語言的過程很簡單從 https://golang.org 下載安裝包應該不超過幾分鐘譯註感謝偉大的長城讓大陸的Gopher們都學會了自己打洞的基本生活技能下載時間可能會因爲洞的大小等因素從幾分鐘到幾天或更久然後就可以在自己電腦上編寫和運行Go程序了。
当然Playground 和 Tour 也有一些限制它们只能导入标准库而且因为安全的原因对一些网络库做了限制。如果要在编译和运行时需要访问互联网对于一些更复杂的实验你可能需要在自己的电脑上构建并运行程序。幸运的是下载Go语言的过程很简单从 https://golang.org 下载安装包应该不超过几分钟译注感谢伟大的长城让大陆的Gopher们都学会了自己打洞的基本生活技能下载时间可能会因为洞的大小等因素从几分钟到几天或更久然后就可以在自己电脑上编写和运行Go程序了。
Go語言是一個開源項目,你可以在 https://golang.org/pkg 閲讀標準庫中任意函數和類型的實現代碼,和下載安裝包的代碼完全一致。這樣你可以知道很多函數是如何工作的, 通過挖掘找出一些答案的細節或者僅僅是出於欣賞專業級Go代碼
Go语言是一个开源项目,你可以在 https://golang.org/pkg 阅读标准库中任意函数和类型的实现代码,和下载安装包的代码完全一致。这样你可以知道很多函数是如何工作的, 通过挖掘找出一些答案的细节或者仅仅是出于欣赏专业级Go代码

View File

@ -1,15 +1,15 @@
## 致
## 致
[Rob Pike](http://genius.cat-v.org/rob-pike/)和[Russ Cox](http://research.swtch.com/)以及很多其他Go糰隊的核心成員多次仔細閲讀了本書的手稿他們對本書的組織結構和表述用詞等給出了很多寶貴的建議。在準備日文版翻譯的時候Yoshiki Shibata更是仔細地審閲了本書的每個部分及時發現了諸多英文和代碼的錯誤。我們非常感謝本書的每一位審閲者併感謝對本書給出了重要的建議的Brian Goetz、Corey Kosak、Arnold Robbins、Josh Bleecher Snyder和Peter Weinberger等人。
[Rob Pike](http://genius.cat-v.org/rob-pike/)和[Russ Cox](http://research.swtch.com/)以及很多其他Go团队的核心成员多次仔细阅读了本书的手稿他们对本书的组织结构和表述用词等给出了很多宝贵的建议。在准备日文版翻译的时候Yoshiki Shibata更是仔细地审阅了本书的每个部分及时发现了诸多英文和代码的错误。我们非常感谢本书的每一位审阅者并感谢对本书给出了重要的建议的Brian Goetz、Corey Kosak、Arnold Robbins、Josh Bleecher Snyder和Peter Weinberger等人。
們還感謝Sameer Ajmani、Ittai Balaban、David Crawshaw、Billy Donohue、Jonathan Feinberg、Andrew Gerrand、Robert Griesemer、John Linderman、Minux Ma譯註中国人Go糰隊成員。、Bryan Mills、Bala Natarajan、Cosmos Nicolaou、Paul Staniforth、Nigel Tao譯註好像是陶哲軒的兄弟以及Howard Trickey給出的許多有價值的建議。我們還要感謝David Brailsford和Raph Levien關於類型設置的建議
们还感谢Sameer Ajmani、Ittai Balaban、David Crawshaw、Billy Donohue、Jonathan Feinberg、Andrew Gerrand、Robert Griesemer、John Linderman、Minux Ma译注中国人Go团队成员。、Bryan Mills、Bala Natarajan、Cosmos Nicolaou、Paul Staniforth、Nigel Tao译注好像是陶哲轩的兄弟以及Howard Trickey给出的许多有价值的建议。我们还要感谢David Brailsford和Raph Levien关于类型设置的建议
們的來自Addison-Wesley的編輯Greg Doench收到了很多幫助從最開始就得到了越來越多的幫助。來自AW生産糰隊的John Fuller、Dayna Isley、Julie Nahil、Chuti Prasertsith到Barbara Wood感謝你們的熱心幫助。
们的来自Addison-Wesley的编辑Greg Doench收到了很多帮助从最开始就得到了越来越多的帮助。来自AW生产团队的John Fuller、Dayna Isley、Julie Nahil、Chuti Prasertsith到Barbara Wood感谢你们的热心帮助。
[Alan Donovan](https://github.com/adonovan)特别感Sameer Ajmani、Chris Demetriou、Walt Drummond和Google公司的Reid Tatge允許他有充裕的時間去寫本書感謝Stephen Donovan的建議和始終如一的鼓勵以及他的妻子Leila Kazemi併沒有讓他爲了家庭瑣事而分心併熱情堅定地支持這個項目。
[Alan Donovan](https://github.com/adonovan)特别感Sameer Ajmani、Chris Demetriou、Walt Drummond和Google公司的Reid Tatge允许他有充裕的时间去写本书感谢Stephen Donovan的建议和始终如一的鼓励以及他的妻子Leila Kazemi并没有让他为了家庭琐事而分心并热情坚定地支持这个项目。
[Brian Kernighan](http://www.cs.princeton.edu/~bwk/)特别感朋友和同事對他的耐心和寬容讓他慢慢地梳理本書的寫作思路。同時感謝他的妻子Meg和其他很多朋友對他寫作事業的支持。
[Brian Kernighan](http://www.cs.princeton.edu/~bwk/)特别感朋友和同事对他的耐心和宽容让他慢慢地梳理本书的写作思路。同时感谢他的妻子Meg和其他很多朋友对他写作事业的支持。
2015年 10月 於 紐約
2015年 10月 于 纽约

View File

@ -1,6 +1,6 @@
## 1.1. Hello, World
們以現已成爲傳統的“hello world”案例來開始吧, 這個例子首次出現於1978年出版的C語言聖經[《The C Programming Language》](http://s3-us-west-2.amazonaws.com/belllabs-microsite-dritchie/cbook/index.html)[^1]。C語言是直接影響Go語言設計的語言之一。這個例子體現了Go語言一些核心理念。
们以现已成为传统的“hello world”案例来开始吧, 这个例子首次出现于1978年出版的C语言圣经[《The C Programming Language》](http://s3-us-west-2.amazonaws.com/belllabs-microsite-dritchie/cbook/index.html)[^1]。C语言是直接影响Go语言设计的语言之一。这个例子体现了Go语言一些核心理念。
<u><i>gopl.io/ch1/helloworld</i></u>
```go
@ -13,76 +13,76 @@ func main() {
}
```
Go是一門編譯型語言Go語言的工具鏈將源代碼及其依賴轉換成計算機的機器指令[^2]。Go語言提供的工具都通過一個單獨的命令`go`調用,`go`命令有一繫列子命令。最簡單的一個子命令就是run。這個命令編譯一個或多個以.go結尾的源文件鏈接庫文件併運行最終生成的可執行文件。本書使用$表示命令行提示符。)
Go是一门编译型语言Go语言的工具链将源代码及其依赖转换成计算机的机器指令[^2]。Go语言提供的工具都通过一个单独的命令`go`调用,`go`命令有一系列子命令。最简单的一个子命令就是run。这个命令编译一个或多个以.go结尾的源文件链接库文件并运行最终生成的可执行文件。本书使用$表示命令行提示符。)
```
$ go run helloworld.go
```
無意外,這個命令會輸出:
无意外,这个命令会输出:
```
Hello, 世界
```
Go語言原生支持Unicode它可以處理全世界任何語言的文本。
Go语言原生支持Unicode它可以处理全世界任何语言的文本。
如果不隻是一次性實驗,你肯定希望能夠編譯這個程序,保存編譯結果以備將來之用。可以用build子命令
如果不只是一次性实验,你肯定希望能够编译这个程序,保存编译结果以备将来之用。可以用build子命令
```
$ go build helloworld.go
```
這個命令生成一個名爲helloworld的可執行的二進製文件[^3],之後你可以隨時運行它[^4],不需任何處理[^5]。
这个命令生成一个名为helloworld的可执行的二进制文件[^3],之后你可以随时运行它[^4],不需任何处理[^5]。
```
$ ./helloworld
Hello, 世界
```
書中, 所有的示例代碼上都有一行標記,利用這些標記, 可以從[gopl.io](http://gopl.io)網站上本書源碼倉庫里獲取代碼
书中, 所有的示例代码上都有一行标记,利用这些标记, 可以从[gopl.io](http://gopl.io)网站上本书源码仓库里获取代码
```
gopl.io/ch1/helloworld
```
執行 `go get gopl.io/ch1/helloworld` 命令,就會從網上獲取代碼,併放到對應目録中[^6]。2.6和10.7節有這方面更詳細的介紹
执行 `go get gopl.io/ch1/helloworld` 命令,就会从网上获取代码,并放到对应目录中[^6]。2.6和10.7节有这方面更详细的介绍
來討論下程序本身。Go語言的代碼通過**包**package組織包類似於其它語言里的庫libraries或者模塊modules。一個包由位於單個目録下的一個或多個.go源代碼文件組成, 目録定義包的作用。每個源文件都以一條`package`聲明語句開始,這個例子里就是`package main`, 表示該文件屬於哪個包緊跟着一繫列導入import的包之後是存儲在這個文件里的程序語句。
来讨论下程序本身。Go语言的代码通过**包**package组织包类似于其它语言里的库libraries或者模块modules。一个包由位于单个目录下的一个或多个.go源代码文件组成, 目录定义包的作用。每个源文件都以一条`package`声明语句开始,这个例子里就是`package main`, 表示该文件属于哪个包紧跟着一系列导入import的包之后是存储在这个文件里的程序语句。
Go的標準庫提供了100多個包以支持常見功能如輸入、輸出、排序以及文本處理。比如`fmt`包,就含有格式化輸出、接收輸入的函數。`Println`是其中一個基礎函數,可以打印以空格間隔的一個或多個值,併在最後添加一個換行符,從而輸出一整行。
Go的标准库提供了100多个包以支持常见功能如输入、输出、排序以及文本处理。比如`fmt`包,就含有格式化输出、接收输入的函数。`Println`是其中一个基础函数,可以打印以空格间隔的一个或多个值,并在最后添加一个换行符,从而输出一整行。
`main`包比較特殊。它定義了一個獨立可執行的程序,而不是一個庫。在`main`里的`main` *函數* 也很特殊,它是整個程序執行時的入口[^7]。`main`函數所做的事情就是程序做的。當然了,`main`函數一般調用其它包里的函數完成很多工作, 比如`fmt.Println`。
`main`包比较特殊。它定义了一个独立可执行的程序,而不是一个库。在`main`里的`main` *函数* 也很特殊,它是整个程序执行时的入口[^7]。`main`函数所做的事情就是程序做的。当然了,`main`函数一般调用其它包里的函数完成很多工作, 比如`fmt.Println`。
須告訴編譯器源文件需要哪些包,這就是`import`聲明以及隨後的`package`聲明扮演的角色。hello world例子隻用到了一個包大多數程序需要導入多個包。
须告诉编译器源文件需要哪些包,这就是`import`声明以及随后的`package`声明扮演的角色。hello world例子只用到了一个包大多数程序需要导入多个包。
須恰當導入需要的包,缺少了必要的包或者導入了不需要的包,程序都無法編譯通過。這項嚴格要求避免了程序開發過程中引入未使用的包[^8]。
须恰当导入需要的包,缺少了必要的包或者导入了不需要的包,程序都无法编译通过。这项严格要求避免了程序开发过程中引入未使用的包[^8]。
`import`聲明必須跟在文件的`package`聲明之後。隨後,則是組成程序的函數、變量、常量、類型的聲明語句(分别由關鍵字`func`, `var`, `const`, `type`定義)。這些內容的聲明順序併不重要[^9]。這個例子的程序已經盡可能短了,隻聲明了一個函數, 其中隻調用了一個其他函數。爲了節省篇幅,有些時候, 示例程序會省略`package`和`import`聲明,但是,這些聲明在源代碼里有,併且必須得有才能編譯
`import`声明必须跟在文件的`package`声明之后。随后,则是组成程序的函数、变量、常量、类型的声明语句(分别由关键字`func`, `var`, `const`, `type`定义)。这些内容的声明顺序并不重要[^9]。这个例子的程序已经尽可能短了,只声明了一个函数, 其中只调用了一个其他函数。为了节省篇幅,有些时候, 示例程序会省略`package`和`import`声明,但是,这些声明在源代码里有,并且必须得有才能编译
個函數的聲明由`func`關鍵字、函數名、參數列表、返迴值列表(這個例子里的`main`函數參數列表和返迴值都是空的)以及包含在大括號里的函數體組成。第五章進一步考察函數
个函数的声明由`func`关键字、函数名、参数列表、返回值列表(这个例子里的`main`函数参数列表和返回值都是空的)以及包含在大括号里的函数体组成。第五章进一步考察函数
Go語言不需要在語句或者聲明的末尾添加分號,除非一行上有多條語句。實際上,編譯器會主動把特定符號後的換行符轉換爲分號, 因此換行符添加的位置會影響Go代碼的正確解析[^10]。。舉個例子, 函數的左括號`{`必須和`func`函數聲明在同一行上, 且位於末尾,不能獨占一行,而在表達式`x + y`中,可在`+`後換行,不能在`+`前換行。
Go语言不需要在语句或者声明的末尾添加分号,除非一行上有多条语句。实际上,编译器会主动把特定符号后的换行符转换为分号, 因此换行符添加的位置会影响Go代码的正确解析[^10]。。举个例子, 函数的左括号`{`必须和`func`函数声明在同一行上, 且位于末尾,不能独占一行,而在表达式`x + y`中,可在`+`后换行,不能在`+`前换行。
Go語言在代碼格式上采取了很強硬的態度。`gofmt`工具把代碼格式化爲標準格式[^12],併且`go`工具中的`fmt`子命令會對指定包, 否則默認爲當前目録, 中所有.go源文件應用`gofmt`命令。本書中的所有代碼都被gofmt過。你也應該養成格式化自己的代碼的習慣。以法令方式規定標準的代碼格式可以避免無盡的無意義的瑣碎爭執[^13]。更重要的是這樣可以做多種自動源碼轉換如果放任Go語言代碼格式這些轉換就不大可能了。
Go语言在代码格式上采取了很强硬的态度。`gofmt`工具把代码格式化为标准格式[^12],并且`go`工具中的`fmt`子命令会对指定包, 否则默认为当前目录, 中所有.go源文件应用`gofmt`命令。本书中的所有代码都被gofmt过。你也应该养成格式化自己的代码的习惯。以法令方式规定标准的代码格式可以避免无尽的无意义的琐碎争执[^13]。更重要的是这样可以做多种自动源码转换如果放任Go语言代码格式这些转换就不大可能了。
很多文本編輯器都可以配置爲保存文件時自動執行`gofmt`,這樣你的源代碼總會被恰當地格式化。還有個相關的工具,`goimports`,可以根據代碼需要, 自動地添加或刪除`import`聲明。這個工具併沒有包含在標準的分發包中,可以用下面的命令安裝
很多文本编辑器都可以配置为保存文件时自动执行`gofmt`,这样你的源代码总会被恰当地格式化。还有个相关的工具,`goimports`,可以根据代码需要, 自动地添加或删除`import`声明。这个工具并没有包含在标准的分发包中,可以用下面的命令安装
```
$ go get golang.org/x/tools/cmd/goimports
```
對於大多數用戶來説下載、編譯包、運行測試用例、察看Go語言的文檔等等常用功能都可以用go的工具完成。10.7節詳細介紹這些知識
对于大多数用户来说下载、编译包、运行测试用例、察看Go语言的文档等等常用功能都可以用go的工具完成。10.7节详细介绍这些知识
[^1]: 本書作者之一Brian W. Kernighan也是《The C Programming Language》一書的作者。
[^2]: 靜態編譯
[^3]: Windows繫統下生成的可執行文件是helloworld.exe增加了.exe後綴名。
[^4]: 在Windows繫統下在命令行直接輸入helloworld.exe命令運行。
[^5]: 因爲靜態編譯,所以不用擔心在繫統庫更新的時候衝突,幸福感滿滿
[^6]: 需要先安裝Git或Hg之類的版本管理工具併將對應的命令添加到PATH環境變量中。序言已經提及需要先設置好GOPATH環境變量下載的代碼會放在`$GOPATH/src/gopl.io/ch1/helloworld`目録
[^7]: C繫語言差不多都這樣
[^8]: Go語言編譯過程沒有警告信息,爭議特性之一。
[^9]: 最好還是定一下規范。
[^10]: 比如行末是標識符、整數、浮點數、虛數、字符或字符串文字、關鍵字`break`、`continue`、`fallthrough`或`return`中的一個、運算符和分隔符`++`、`--`、`)`、`]`或`}`中的一個
[^11]: 以+結尾的話不會被插入分號分隔符但是以x結尾的話則會被分號分隔符從而導致編譯錯誤
[^12]: 這個格式化工具沒有任何可以調整代碼格式的參數Go語言就是這麽任性。
[^13]: 也導致了Go語言的TIOBE排名較低因爲缺少撕逼的話題
[^1]: 本书作者之一Brian W. Kernighan也是《The C Programming Language》一书的作者。
[^2]: 静态编译
[^3]: Windows系统下生成的可执行文件是helloworld.exe增加了.exe后缀名。
[^4]: 在Windows系统下在命令行直接输入helloworld.exe命令运行。
[^5]: 因为静态编译,所以不用担心在系统库更新的时候冲突,幸福感满满
[^6]: 需要先安装Git或Hg之类的版本管理工具并将对应的命令添加到PATH环境变量中。序言已经提及需要先设置好GOPATH环境变量下载的代码会放在`$GOPATH/src/gopl.io/ch1/helloworld`目录
[^7]: C系语言差不多都这样
[^8]: Go语言编译过程没有警告信息,争议特性之一。
[^9]: 最好还是定一下规范。
[^10]: 比如行末是标识符、整数、浮点数、虚数、字符或字符串文字、关键字`break`、`continue`、`fallthrough`或`return`中的一个、运算符和分隔符`++`、`--`、`)`、`]`或`}`中的一个
[^11]: 以+结尾的话不会被插入分号分隔符但是以x结尾的话则会被分号分隔符从而导致编译错误
[^12]: 这个格式化工具没有任何可以调整代码格式的参数Go语言就是这么任性。
[^13]: 也导致了Go语言的TIOBE排名较低因为缺少撕逼的话题

View File

@ -1,14 +1,14 @@
## 1.2. 命令行參數
## 1.2. 命令行参数
大多數的程序都是處理輸入,産生輸出;這也正是“計算”的定義。但是, 程序如何獲取要處理的輸入數據呢?一些程序生成自己的數據,但通常情況下,輸入來自於程序外部:文件、網絡連接、其它程序的輸出、敲鍵盤的用戶、命令行參數或其它類似輸入源。下面幾個例子會討論其中幾個輸入源,首先是命令行參數
大多数的程序都是处理输入,产生输出;这也正是“计算”的定义。但是, 程序如何获取要处理的输入数据呢?一些程序生成自己的数据,但通常情况下,输入来自于程序外部:文件、网络连接、其它程序的输出、敲键盘的用户、命令行参数或其它类似输入源。下面几个例子会讨论其中几个输入源,首先是命令行参数
`os`包以跨平台的方式,提供了一些與操作繫統交互的函數和變量。程序的命令行參數可從os包的Args變量獲取os包外部使用os.Args訪問該變量。
`os`包以跨平台的方式,提供了一些与操作系统交互的函数和变量。程序的命令行参数可从os包的Args变量获取os包外部使用os.Args访问该变量。
os.Args變量是一個字符串string的*切片*slice譯註slice和Python語言中的切片類似是一個簡版的動態數組切片是Go語言的基礎概念稍後詳細介紹。現在先把切片s當作數組元素序列, 序列的成長度動態變化, 用`s[i]`訪問單個元素,用`s[m:n]`獲取子序列(譯註和python里的語法差不多)。序列的元素數目爲len(s)。和大多數編程語言類似區間索引時Go言里也采用左閉右開形式, 卽,區間包括第一個索引元素,不包括最後一個, 因爲這樣可以簡化邏輯。譯註比如a = [1, 2, 3, 4, 5], a[0:3] = [1, 2, 3]不包含最後一個元素。比如s[m:n]這個切片0 ≤ m ≤ n ≤ len(s)包含n-m個元素。
os.Args变量是一个字符串string的*切片*slice译注slice和Python语言中的切片类似是一个简版的动态数组切片是Go语言的基础概念稍后详细介绍。现在先把切片s当作数组元素序列, 序列的成长度动态变化, 用`s[i]`访问单个元素,用`s[m:n]`获取子序列(译注和python里的语法差不多)。序列的元素数目为len(s)。和大多数编程语言类似区间索引时Go言里也采用左闭右开形式, 即,区间包括第一个索引元素,不包括最后一个, 因为这样可以简化逻辑。译注比如a = [1, 2, 3, 4, 5], a[0:3] = [1, 2, 3]不包含最后一个元素。比如s[m:n]这个切片0 ≤ m ≤ n ≤ len(s)包含n-m个元素。
os.Args的第一個元素os.Args[0], 是命令本身的名字其它的元素則是程序啟動時傳給它的參數。s[m:n]形式的切片表達式産生從第m個元素到第n-1個元素的切片下個例子用到的元素包含在os.Args[1:len(os.Args)]切片中。如果省略切片表達式的m或n會默認傳入0或len(s),因此前面的切片可以簡寫成os.Args[1:]。
os.Args的第一个元素os.Args[0], 是命令本身的名字其它的元素则是程序启动时传给它的参数。s[m:n]形式的切片表达式产生从第m个元素到第n-1个元素的切片下个例子用到的元素包含在os.Args[1:len(os.Args)]切片中。如果省略切片表达式的m或n会默认传入0或len(s),因此前面的切片可以简写成os.Args[1:]。
下面是Unix里echo命令的一份實現echo把它的命令行參數打印成一行。程序導入了兩個包用括號把它們括起來寫成列表形式, 而沒有分開寫成獨立的`import`聲明。兩種形式都合法列表形式習慣上用得多。包導入順序併不重要gofmt工具格式化時按照字母順序對包名排序。示例有多個版本時我們會對示例編號, 這樣可以明確當前正在討論的是哪個。)
下面是Unix里echo命令的一份实现echo把它的命令行参数打印成一行。程序导入了两个包用括号把它们括起来写成列表形式, 而没有分开写成独立的`import`声明。两种形式都合法列表形式习惯上用得多。包导入顺序并不重要gofmt工具格式化时按照字母顺序对包名排序。示例有多个版本时我们会对示例编号, 这样可以明确当前正在讨论的是哪个。)
<u><i>gopl.io/ch1/echo1</i></u>
```go
@ -30,37 +30,37 @@ func main() {
}
```
註釋語句以`//`開頭。對於程序員來説,//之後到行末之間所有的內容都是註釋,被編譯器忽略。按照慣例,我們在每個包的包聲明前添加註釋;對於`main package`,註釋包含一句或幾句話,從整體角度對程序做個描述。
注释语句以`//`开头。对于程序员来说,//之后到行末之间所有的内容都是注释,被编译器忽略。按照惯例,我们在每个包的包声明前添加注释;对于`main package`,注释包含一句或几句话,从整体角度对程序做个描述。
var聲明定義了兩個string類型的變量s和sep。變量會在聲明時直接初始化。如果變量沒有顯式初始化則被隱式地賦予其類型的*零值*zero value數值類型是0字符串類型是空字符串""。這個例子里聲明把s和sep隱式地初始化成空字符串。第2章再來詳細地講解變量和聲明。
var声明定义了两个string类型的变量s和sep。变量会在声明时直接初始化。如果变量没有显式初始化则被隐式地赋予其类型的*零值*zero value数值类型是0字符串类型是空字符串""。这个例子里声明把s和sep隐式地初始化成空字符串。第2章再来详细地讲解变量和声明。
對數值類型Go語言提供了常規的數值和邏輯運算符。而對string類型`+`運算符連接字符串譯註和C++或者js是一樣的。所以表達式:
对数值类型Go语言提供了常规的数值和逻辑运算符。而对string类型`+`运算符连接字符串译注和C++或者js是一样的。所以表达式:
```go
sep + os.Args[i]
```
表示連接字符串sep和os.Args。程序中使用的語句:
表示连接字符串sep和os.Args。程序中使用的语句:
```go
s += sep + os.Args[i]
```
是一條*賦值語句*, 將s的舊值跟sep與os.Args[i]連接後賦值迴s等價於
是一条*赋值语句*, 将s的旧值跟sep与os.Args[i]连接后赋值回s等价于
```go
s = s + sep + os.Args[i]
```
運算符`+=`是賦值運算符assignment operator每種數值運算符或邏輯運算符如`+`或`*`,都有對應的賦值運算符。
运算符`+=`是赋值运算符assignment operator每种数值运算符或逻辑运算符如`+`或`*`,都有对应的赋值运算符。
echo程序可以每循環一次輸出一個參數這個版本卻是不斷地把新文本追加到末尾來構造字符串。字符串s開始爲空卽值爲""每次循環會添加一些文本第一次迭代之後還會再插入一個空格因此循環結束時每個參數中間都有一個空格。這是一種二次加工quadratic process當參數數量龐大時開銷很大但是對於echo這種情形不大可能出現。本章會介紹echo的若榦改進版下一章解決低效問題
echo程序可以每循环一次输出一个参数这个版本却是不断地把新文本追加到末尾来构造字符串。字符串s开始为空即值为""每次循环会添加一些文本第一次迭代之后还会再插入一个空格因此循环结束时每个参数中间都有一个空格。这是一种二次加工quadratic process当参数数量庞大时开销很大但是对于echo这种情形不大可能出现。本章会介绍echo的若干改进版下一章解决低效问题
環索引變量i在for循環的第一部分中定義。符號`:=`是*短變量聲明*short variable declaration的一部分, 這是定義一個或多個變量併根據它們的初始值爲這些變量賦予適當類型的語句。下一章有這方面更多説明。
环索引变量i在for循环的第一部分中定义。符号`:=`是*短变量声明*short variable declaration的一部分, 这是定义一个或多个变量并根据它们的初始值为这些变量赋予适当类型的语句。下一章有这方面更多说明。
自增語句`i++`給`i`加1這和`i += 1`以及`i = i + 1`都是等價的。對應的還有`i--`給`i`減1。它們是語句而不像C繫的其它語言那樣是表達式。所以`j = i++`非法,而且++和--都隻能放在變量名後面,因此`--i`也非法。
自增语句`i++`给`i`加1这和`i += 1`以及`i = i + 1`都是等价的。对应的还有`i--`给`i`减1。它们是语句而不像C系的其它语言那样是表达式。所以`j = i++`非法,而且++和--都只能放在变量名后面,因此`--i`也非法。
Go語言隻有for循環這一種循環語句。for循環有多種形式其中一種如下所示:
Go语言只有for循环这一种循环语句。for循环有多种形式其中一种如下所示:
```go
for initialization; condition; post {
@ -68,11 +68,11 @@ for initialization; condition; post {
}
```
for循環三個部分不需括號包圍。大括號強製要求, 左大括號必須和*post*語句在同一行。
for循环三个部分不需括号包围。大括号强制要求, 左大括号必须和*post*语句在同一行。
*initialization*語句是可選的,在循環開始前執行。*initalization*如果存在,必須是一條*簡單語句*simple statement短變量聲明、自增語句、賦值語句或函數調用。`condition`是一個布爾表達式boolean expression其值在每次循環迭代開始時計算。如果爲`true`則執行循環體語句。`post`語句在循環體執行結束後執行,之後再次對`conditon`求值。`condition`值爲`false`時,循環結束。
*initialization*语句是可选的,在循环开始前执行。*initalization*如果存在,必须是一条*简单语句*simple statement短变量声明、自增语句、赋值语句或函数调用。`condition`是一个布尔表达式boolean expression其值在每次循环迭代开始时计算。如果为`true`则执行循环体语句。`post`语句在循环体执行结束后执行,之后再次对`conditon`求值。`condition`值为`false`时,循环结束。
for循環的這三個部分每個都可以省略,如果省略`initialization`和`post`,分號也可以省略:
for循环的这三个部分每个都可以省略,如果省略`initialization`和`post`,分号也可以省略:
```go
// a traditional "while" loop
@ -81,7 +81,7 @@ for condition {
}
```
如果連`condition`也省略了,像下面這樣
如果连`condition`也省略了,像下面这样
```go
// a traditional infinite loop
@ -90,9 +90,9 @@ for {
}
```
這就變成一個無限循環,盡管如此,還可以用其他方式終止循環, 如一條`break`或`return`語句。
这就变成一个无限循环,尽管如此,还可以用其他方式终止循环, 如一条`break`或`return`语句。
`for`環的另一種形式, 在某種數據類型的區間range上遍歷如字符串或切片。`echo`的第二版本展示了這種形式:
`for`环的另一种形式, 在某种数据类型的区间range上遍历如字符串或切片。`echo`的第二版本展示了这种形式:
<u><i>gopl.io/ch1/echo2</i></u>
```go
@ -113,11 +113,11 @@ func main() {
}
```
每次循環迭代,`range`産生一對值;索引以及在該索引處的元素值。這個例子不需要索引,但`range`的語法要求, 要處理元素, 必須處理索引。一種思路是把索引賦值給一個臨時變量, 如`temp`, 然後忽略它的值但Go語言不允許使用無用的局部變量local variables因爲這會導致編譯錯誤
每次循环迭代,`range`产生一对值;索引以及在该索引处的元素值。这个例子不需要索引,但`range`的语法要求, 要处理元素, 必须处理索引。一种思路是把索引赋值给一个临时变量, 如`temp`, 然后忽略它的值但Go语言不允许使用无用的局部变量local variables因为这会导致编译错误
Go語言中這種情況的解決方法是用`空標識符`blank identifier卽`_`(也就是下劃線)。空標識符可用於任何語法需要變量名但程序邏輯不需要的時候, 例如, 在循環里,丟棄不需要的循環索引, 保留元素值。大多數的Go程序員都會像上面這樣使用`range`和`_`寫`echo`程序因爲隱式地而非顯示地索引os.Args容易寫對
Go语言中这种情况的解决方法是用`空标识符`blank identifier即`_`(也就是下划线)。空标识符可用于任何语法需要变量名但程序逻辑不需要的时候, 例如, 在循环里,丢弃不需要的循环索引, 保留元素值。大多数的Go程序员都会像上面这样使用`range`和`_`写`echo`程序因为隐式地而非显示地索引os.Args容易写对
`echo`這個版本使用一條短變量聲明來聲明併初始化`s`和`seps`,也可以將這兩個變量分開聲明,聲明一個變量有好幾種方式,下面這些都等價
`echo`这个版本使用一条短变量声明来声明并初始化`s`和`seps`,也可以将这两个变量分开声明,声明一个变量有好几种方式,下面这些都等价
```go
s := ""
@ -126,11 +126,11 @@ var s = ""
var s string = ""
```
用哪種不用哪種,爲什麽呢?第一種形式,是一條短變量聲明,最簡潔,但隻能用在函數內部,而不能用於包變量。第二種形式依賴於字符串的默認初始化零值機製,被初始化爲""。第三種形式用得很少,除非同時聲明多個變量。第四種形式顯式地標明變量的類型,當變量類型與初值類型相同時,類型冗餘,但如果兩者類型不同,變量類型就必須了。實踐中一般使用前兩種形式中的某個,初始值重要的話就顯式地指定變量的類型,否則使用隱式初始化。
用哪种不用哪种,为什么呢?第一种形式,是一条短变量声明,最简洁,但只能用在函数内部,而不能用于包变量。第二种形式依赖于字符串的默认初始化零值机制,被初始化为""。第三种形式用得很少,除非同时声明多个变量。第四种形式显式地标明变量的类型,当变量类型与初值类型相同时,类型冗余,但如果两者类型不同,变量类型就必须了。实践中一般使用前两种形式中的某个,初始值重要的话就显式地指定变量的类型,否则使用隐式初始化。
如前文所述,每次循環迭代字符串s的內容都會更新。`+=`連接原字符串、空格和下個參數,産生新字符串, 併把它賦值給`s`。`s`原來的內容已經不再使用,將在適當時機對它進行垃圾迴收。
如前文所述,每次循环迭代字符串s的内容都会更新。`+=`连接原字符串、空格和下个参数,产生新字符串, 并把它赋值给`s`。`s`原来的内容已经不再使用,将在适当时机对它进行垃圾回收。
如果連接涉及的數據量很大,這種方式代價高昂。一種簡單且高效的解決方案是使用`strings`包的`Join`函數
如果连接涉及的数据量很大,这种方式代价高昂。一种简单且高效的解决方案是使用`strings`包的`Join`函数
<u><i>gopl.io/ch1/echo3</i></u>
```go
@ -139,16 +139,16 @@ func main() {
}
```
後,如果不關心輸出格式,隻想看看輸出值,或許隻是爲了調試,可以用`Println`爲我們格式化輸出。
后,如果不关心输出格式,只想看看输出值,或许只是为了调试,可以用`Println`为我们格式化输出。
```go
fmt.Println(os.Args[1:])
```
這條語句的輸出結果跟`strings.Join`得到的結果很像,隻是被放到了一對方括號里。切片都會被打印成這種格式。
这条语句的输出结果跟`strings.Join`得到的结果很像,只是被放到了一对方括号里。切片都会被打印成这种格式。
**練習 1.1** 脩改`echo`程序,使其能夠打印`os.Args[0]`,卽被執行命令本身的名字。
**练习 1.1** 修改`echo`程序,使其能够打印`os.Args[0]`,即被执行命令本身的名字。
**練習 1.2** 脩改`echo`程序,使其打印每個參數的索引和值,每個一行。
**练习 1.2** 修改`echo`程序,使其打印每个参数的索引和值,每个一行。
**練習 1.3** 做實驗測量潛在低效的版本和使用了`strings.Join`的版本的運行時間差異。1.6節講解了部分`time`包11.4節展示了如何寫標準測試程序,以得到繫統性的性能評測。)
**练习 1.3** 做实验测量潜在低效的版本和使用了`strings.Join`的版本的运行时间差异。1.6节讲解了部分`time`包11.4节展示了如何写标准测试程序,以得到系统性的性能评测。)

View File

@ -1,8 +1,8 @@
## 1.3. 査找重複的行
## 1.3. 查找重复的行
對文件做拷貝、打印、蒐索、排序、統計或類似事情的程序都有一個差不多的程序結構:一個處理輸入的循環,在每個元素上執行計算處理,在處理的同時或最後産生輸出。我們會展示一個名爲`dup`的程序的三個版本靈感來自於Unix的`uniq`命令,其尋找相鄰的重複行。該程序使用的結構和包是個參考范例,可以方便地脩改。
对文件做拷贝、打印、搜索、排序、统计或类似事情的程序都有一个差不多的程序结构:一个处理输入的循环,在每个元素上执行计算处理,在处理的同时或最后产生输出。我们会展示一个名为`dup`的程序的三个版本灵感来自于Unix的`uniq`命令,其寻找相邻的重复行。该程序使用的结构和包是个参考范例,可以方便地修改。
`dup`的第一個版本打印標準輸入中多次出現的行,以重複次數開頭。該程序將引入`if`語句,`map`數據類型以及`bufio`包。
`dup`的第一个版本打印标准输入中多次出现的行,以重复次数开头。该程序将引入`if`语句,`map`数据类型以及`bufio`包。
<u><i>gopl.io/ch1/dup1</i></u>
```go
@ -31,53 +31,53 @@ func main() {
}
```
正如`for`循環一樣,`if`語句條件兩邊也不加括號,但是主體部分需要加。`if`語句的`else`部分是可選的,在`if`的條件爲`false`時執行。
正如`for`循环一样,`if`语句条件两边也不加括号,但是主体部分需要加。`if`语句的`else`部分是可选的,在`if`的条件为`false`时执行。
**map**存儲了鍵/值key/value的集合對集合元素提供常數時間的存、取或測試操作。鍵可以是任意類型隻要其值能用`==`運算符比較,最常見的例子是字符串;值則可以是任意類型。這個例子中的鍵是字符串,值是整數。內置函數`make`創建空`map`此外它還有别的作用。4.3節討論`map`。
**map**存储了键/值key/value的集合对集合元素提供常数时间的存、取或测试操作。键可以是任意类型只要其值能用`==`运算符比较,最常见的例子是字符串;值则可以是任意类型。这个例子中的键是字符串,值是整数。内置函数`make`创建空`map`此外它还有别的作用。4.3节讨论`map`。
譯註:從功能和實現上説,`Go`的`map`類似於`Java`語言中的`HashMap`Python語言中的`dict``Lua`語言中的`table`,通常使用`hash`實現。遺憾的是,對於該詞的翻譯併不統一,數學界術語爲`映射`,而計算機界衆説紛紜莫衷一是。爲了防止對讀者造成誤解,保留不譯。)
译注:从功能和实现上说,`Go`的`map`类似于`Java`语言中的`HashMap`Python语言中的`dict``Lua`语言中的`table`,通常使用`hash`实现。遗憾的是,对于该词的翻译并不统一,数学界术语为`映射`,而计算机界众说纷纭莫衷一是。为了防止对读者造成误解,保留不译。)
每次`dup`讀取一行輸入,該行被當做`map`,其對應的值遞增。`counts[input.Text()]++`語句等價下面兩句:
每次`dup`读取一行输入,该行被当做`map`,其对应的值递增。`counts[input.Text()]++`语句等价下面两句:
```go
line := input.Text()
counts[line] = counts[line] + 1
```
`map`中不含某個鍵時不用擔心,首次讀到新行時,等號右邊的表達式`counts[line]`的值將被計算爲其類型的零值對於int`卽0。
`map`中不含某个键时不用担心,首次读到新行时,等号右边的表达式`counts[line]`的值将被计算为其类型的零值对于int`即0。
爲了打印結果,我們使用了基於`range`的循環,併在`counts`這個`map`上迭代。跟之前類似,每次迭代得到兩個結果,鍵和其在`map`中對應的值。`map`的迭代順序併不確定,從實踐來看,該順序隨機,每次運行都會變化。這種設計是有意爲之的,因爲能防止程序依賴特定遍歷順序,而這是無法保證的。
为了打印结果,我们使用了基于`range`的循环,并在`counts`这个`map`上迭代。跟之前类似,每次迭代得到两个结果,键和其在`map`中对应的值。`map`的迭代顺序并不确定,从实践来看,该顺序随机,每次运行都会变化。这种设计是有意为之的,因为能防止程序依赖特定遍历顺序,而这是无法保证的。
繼續來看`bufio`包,它使處理輸入和輸出方便又高效。`Scanner`類型是該包最有用的特性之一,它讀取輸入併將其拆成行或單詞;通常是處理行形式的輸入最簡單的方法。
继续来看`bufio`包,它使处理输入和输出方便又高效。`Scanner`类型是该包最有用的特性之一,它读取输入并将其拆成行或单词;通常是处理行形式的输入最简单的方法。
程序使用短變量聲明創建`bufio.Scanner`類型的變量`input`。
程序使用短变量声明创建`bufio.Scanner`类型的变量`input`。
```
input := bufio.NewScanner(os.Stdin)
```
該變量從程序的標準輸入中讀取內容。每次調用`input.Scanner`,卽讀入下一行,併移除行末的換行符;讀取的內容可以調用`input.Text()`得到。`Scan`函數在讀到一行時返迴`true`,在無輸入時返迴`false`。
该变量从程序的标准输入中读取内容。每次调用`input.Scanner`,即读入下一行,并移除行末的换行符;读取的内容可以调用`input.Text()`得到。`Scan`函数在读到一行时返回`true`,在无输入时返回`false`。
類似於C或其它語言里的`printf`函數,`fmt.Printf`函數對一些表達式産生格式化輸出。該函數的首個參數是個格式字符串指定後續參數被如何格式化。各個參數的格式取決於“轉換字符”conversion character形式爲百分號後跟一個字母。舉個例子`%d`表示以十進製形式打印一個整型操作數,而`%s`則表示把字符串型操作數的值展開
类似于C或其它语言里的`printf`函数,`fmt.Printf`函数对一些表达式产生格式化输出。该函数的首个参数是个格式字符串指定后续参数被如何格式化。各个参数的格式取决于“转换字符”conversion character形式为百分号后跟一个字母。举个例子`%d`表示以十进制形式打印一个整型操作数,而`%s`则表示把字符串型操作数的值展开
`Printf`有一大堆這種轉換Go程序員稱之爲*動詞verb*。下面的表格雖然遠不是完整的規范,但展示了可用的很多特性:
`Printf`有一大堆这种转换Go程序员称之为*动词verb*。下面的表格虽然远不是完整的规范,但展示了可用的很多特性:
```
%d 十進製整數
%x, %o, %b 十六進製,八進製,二進製整數
%f, %g, %e 浮點數 3.141593 3.141592653589793 3.141593e+00
%t 布true或false
%c 字符rune (Unicode碼點)
%d 十进制整数
%x, %o, %b 十六进制,八进制,二进制整数
%f, %g, %e 浮点数 3.141593 3.141592653589793 3.141593e+00
%t 布true或false
%c 字符rune (Unicode码点)
%s 字符串
%q 帶雙引號的字符串"abc"或帶單引號的字符'c'
%v 量的自然形式natural format
%T 變量的類
%% 字面上的百分號標誌(無操作數
%q 带双引号的字符串"abc"或带单引号的字符'c'
%v 量的自然形式natural format
%T 变量的类
%% 字面上的百分号标志(无操作数
```
`dup1`的格式字符串中還含有製表符`\t`和換行符`\n`。字符串字面上可能含有這些代表不可見字符的**轉義字符escap sequences**。默認情況下,`Printf`不會換行。按照慣例,以字母`f`結尾的格式化函數,如`log.Printf`和`fmt.Errorf`,都采用`fmt.Printf`的格式化準則。而以`ln`結尾的格式化函數,則遵循`Println`的方式,以跟`%v`差不多的方式格式化參數,併在最後添加一個換行符。(譯註:後綴`f`指`fomart``ln`指`line`。)
`dup1`的格式字符串中还含有制表符`\t`和换行符`\n`。字符串字面上可能含有这些代表不可见字符的**转义字符escap sequences**。默认情况下,`Printf`不会换行。按照惯例,以字母`f`结尾的格式化函数,如`log.Printf`和`fmt.Errorf`,都采用`fmt.Printf`的格式化准则。而以`ln`结尾的格式化函数,则遵循`Println`的方式,以跟`%v`差不多的方式格式化参数,并在最后添加一个换行符。(译注:后缀`f`指`fomart``ln`指`line`。)
很多程序要麽從標準輸入中讀取數據,如上面的例子所示,要麽從一繫列具名文件中讀取數據。`dup`程序的下個版本讀取標準輸入或是使用`os.Open`打開各個具名文件,併操作它們
很多程序要么从标准输入中读取数据,如上面的例子所示,要么从一系列具名文件中读取数据。`dup`程序的下个版本读取标准输入或是使用`os.Open`打开各个具名文件,并操作它们
<u><i>gopl.io/ch1/dup2</i></u>
```go
@ -123,19 +123,19 @@ func countLines(f *os.File, counts map[string]int) {
}
```
`os.Open`數返迴兩個值。第一個值是被打開的文件(`*os.File`),其後被`Scanner`讀取。
`os.Open`数返回两个值。第一个值是被打开的文件(`*os.File`),其后被`Scanner`读取。
`os.Open`迴的第二個值是內置`error`類型的值。如果`err`等於內置值`nil`譯註相當於其它語言里的NULL那麽文件被成功打開。讀取文件直到文件結束然後調用`Close`關閉該文件,併釋放占用的所有資源。相反的話,如果`err`的值不是`nil`,説明打開文件時出錯了。這種情況下,錯誤值描述了所遇到的問題。我們的錯誤處理非常簡單,隻是使用`Fprintf`與表示任意類型默認格式值的動詞`%v`,向標準錯誤流打印一條信息,然後`dup`繼續處理下一個文件;`continue`語句直接跳到`for`循環的下個迭代開始執行。
`os.Open`回的第二个值是内置`error`类型的值。如果`err`等于内置值`nil`译注相当于其它语言里的NULL那么文件被成功打开。读取文件直到文件结束然后调用`Close`关闭该文件,并释放占用的所有资源。相反的话,如果`err`的值不是`nil`,说明打开文件时出错了。这种情况下,错误值描述了所遇到的问题。我们的错误处理非常简单,只是使用`Fprintf`与表示任意类型默认格式值的动词`%v`,向标准错误流打印一条信息,然后`dup`继续处理下一个文件;`continue`语句直接跳到`for`循环的下个迭代开始执行。
爲了使示例代碼保持合理的大小,本書開始的一些示例有意簡化了錯誤處理,顯而易見的是,應該檢査`os.Open`返迴的錯誤值,然而,使用`input.Scan`讀取文件過程中不大可能出現錯誤因此我們忽略了錯誤處理。我們會在跳過錯誤檢査的地方做説明。5.4節中深入介紹錯誤處理。
为了使示例代码保持合理的大小,本书开始的一些示例有意简化了错误处理,显而易见的是,应该检查`os.Open`返回的错误值,然而,使用`input.Scan`读取文件过程中不大可能出现错误因此我们忽略了错误处理。我们会在跳过错误检查的地方做说明。5.4节中深入介绍错误处理。
註意`countLines`函數在其聲明前被調用。函數和包級别的變量package-level entities可以任意順序聲明併不影響其被調用。譯註最好還是遵循一定的規范)
注意`countLines`函数在其声明前被调用。函数和包级别的变量package-level entities可以任意顺序声明并不影响其被调用。译注最好还是遵循一定的规范)
`map`是一個由`make`函數創建的數據結構的引用。`map`作爲爲參數傳遞給某函數時該函數接收這個引用的一份拷貝copy或譯爲副本被調用函數對`map`底層數據結構的任何脩改,調用者函數都可以通過持有的`map`引用看到。在我們的例子中,`countLines`函數向`counts`插入的值,也會被`main`函數看到。譯註類似於C++里的引用傳遞,實際上指針是另一個指針了,但內部存的值指向同一塊內存)
`map`是一个由`make`函数创建的数据结构的引用。`map`作为为参数传递给某函数时该函数接收这个引用的一份拷贝copy或译为副本被调用函数对`map`底层数据结构的任何修改,调用者函数都可以通过持有的`map`引用看到。在我们的例子中,`countLines`函数向`counts`插入的值,也会被`main`函数看到。译注类似于C++里的引用传递,实际上指针是另一个指针了,但内部存的值指向同一块内存)
`dup`的前兩個版本以"流”模式讀取輸入,併根據需要拆分成多個行。理論上,這些程序可以處理任意數量的輸入數據。還有另一個方法,就是一口氣把全部輸入數據讀到內存中,一次分割爲多行,然後處理它們。下面這個版本,`dup3`,就是這麽操作的。這個例子引入了`ReadFile`函數(來自於`io/ioutil`包),其讀取指定文件的全部內容,`strings.Split`函數把字符串分割成子串的切片。(`Split`的作用與前文提到的`strings.Join`相反。)
`dup`的前两个版本以"流”模式读取输入,并根据需要拆分成多个行。理论上,这些程序可以处理任意数量的输入数据。还有另一个方法,就是一口气把全部输入数据读到内存中,一次分割为多行,然后处理它们。下面这个版本,`dup3`,就是这么操作的。这个例子引入了`ReadFile`函数(来自于`io/ioutil`包),其读取指定文件的全部内容,`strings.Split`函数把字符串分割成子串的切片。(`Split`的作用与前文提到的`strings.Join`相反。)
們略微簡化了`dup3`。首先,由於`ReadFile`函數需要文件名作爲參數,因此隻讀指定文件,不讀標準輸入。其次,由於行計數代碼隻在一處用到,故將其移迴`main`函數
们略微简化了`dup3`。首先,由于`ReadFile`函数需要文件名作为参数,因此只读指定文件,不读标准输入。其次,由于行计数代码只在一处用到,故将其移回`main`函数
<u><i>gopl.io/ch1/dup3</i></u>
```go
@ -168,8 +168,8 @@ func main() {
}
```
`ReadFile`數返迴一個字節切片byte slice必須把它轉換爲`string`,才能用`strings.Split`分割。我們會在3.5.4節詳細講解字符串和字節切片。
`ReadFile`数返回一个字节切片byte slice必须把它转换为`string`,才能用`strings.Split`分割。我们会在3.5.4节详细讲解字符串和字节切片。
實現上,`bufio.Scanner`、`outil.ReadFile`和`ioutil.WriteFile`都使用`*os.File`的`Read`和`Write`方法,但是,大多數程序員很少需要直接調用那些低級lower-level函數。高級higher-level函數像`bufio`和`io/ioutil`包中所提供的那些,用起來要容易點
实现上,`bufio.Scanner`、`outil.ReadFile`和`ioutil.WriteFile`都使用`*os.File`的`Read`和`Write`方法,但是,大多数程序员很少需要直接调用那些低级lower-level函数。高级higher-level函数像`bufio`和`io/ioutil`包中所提供的那些,用起来要容易点
**練習 1.4** 脩改`dup2`,出現重複的行時打印文件名稱
**练习 1.4** 修改`dup2`,出现重复的行时打印文件名称

View File

@ -1,14 +1,14 @@
## 1.4. GIF動畵
## 1.4. GIF动画
下面的程序會演示Go語言標準庫里的image這個package的用法我們會用這個包來生成一繫列的bit-mapped圖然後將這些圖片編碼爲一個GIF動畵。我們生成的圖形名字叫利薩如圖形(Lissajous figures)這種效果是在1960年代的老電影里出現的一種視覺特效。它們是協振子在兩個緯度上振動所産生的麴線比如兩個sin正絃波分别在x軸和y軸輸入會産生的麴線。圖1.1是這樣的一個例子:
下面的程序会演示Go语言标准库里的image这个package的用法我们会用这个包来生成一系列的bit-mapped图然后将这些图片编码为一个GIF动画。我们生成的图形名字叫利萨如图形(Lissajous figures)这种效果是在1960年代的老电影里出现的一种视觉特效。它们是协振子在两个纬度上振动所产生的曲线比如两个sin正弦波分别在x轴和y轴输入会产生的曲线。图1.1是这样的一个例子:
![](../images/ch1-01.png)
譯註要看這個程序的結果需要將標準輸出重定向到一個GIF圖像文件使用 `./lissajous > output.gif` 命令。下面是GIF圖像動畵效果:
译注要看这个程序的结果需要将标准输出重定向到一个GIF图像文件使用 `./lissajous > output.gif` 命令。下面是GIF图像动画效果:
![](../images/ch1-01.gif)
這段代碼里我們用了一些新的結構包括const聲明struct結構體類型複合聲明。和我們舉的其它的例子不太一樣這一個例子包含了浮點數運算。這些概念我們隻在這里簡單地説明一下之後的章節會更詳細地講解。
这段代码里我们用了一些新的结构包括const声明struct结构体类型复合声明。和我们举的其它的例子不太一样这一个例子包含了浮点数运算。这些概念我们只在这里简单地说明一下之后的章节会更详细地讲解。
<u><i>gopl.io/ch1/lissajous</i></u>
```go
@ -66,25 +66,25 @@ func lissajous(out io.Writer) {
```
當我們import了一個包路徑包含有多個單詞的package時比如image/colorimage和color兩個單詞通常我們隻需要用最後那個單詞表示這個包就可以。所以當我們寫color.White時這個變量指向的是image/color包里的變量同理gif.GIF是屬於image/gif包里的變量。
当我们import了一个包路径包含有多个单词的package时比如image/colorimage和color两个单词通常我们只需要用最后那个单词表示这个包就可以。所以当我们写color.White时这个变量指向的是image/color包里的变量同理gif.GIF是属于image/gif包里的变量。
這個程序里的常量聲明給出了一繫列的常量值,常量是指在程序編譯後運行時始終都不會變化的值,比如圈數、幀數、延遲值。常量聲明和變量聲明一般都會出現在包級别,所以這些常量在整個包中都是可以共享的,或者你也可以把常量聲明定義在函數體內部,那麽這種常量就隻能在函數體內用。目前常量聲明的值必須是一個數字值、字符串或者一個固定的boolean值。
这个程序里的常量声明给出了一系列的常量值,常量是指在程序编译后运行时始终都不会变化的值,比如圈数、帧数、延迟值。常量声明和变量声明一般都会出现在包级别,所以这些常量在整个包中都是可以共享的,或者你也可以把常量声明定义在函数体内部,那么这种常量就只能在函数体内用。目前常量声明的值必须是一个数字值、字符串或者一个固定的boolean值。
[]color.Color{...}和gif.GIF{...}這兩個表達式就是我們説的複合聲明4.2和4.4.1節有説明。這是實例化Go語言里的複合類型的一種寫法。這里的前者生成的是一個slice切片後者生成的是一個struct結構體
[]color.Color{...}和gif.GIF{...}这两个表达式就是我们说的复合声明4.2和4.4.1节有说明。这是实例化Go语言里的复合类型的一种写法。这里的前者生成的是一个slice切片后者生成的是一个struct结构体
gif.GIF是一個struct類型參考4.4節。struct是一組值或者叫字段的集合不同的類型集合在一個struct可以讓我們以一個統一的單元進行處理。anim是一個gif.GIF類型的struct變量。這種寫法會生成一個struct變量併且其內部變量LoopCount字段會被設置爲nframes而其它的字段會被設置爲各自類型默認的零值。struct內部的變量可以以一個點(.)來進行訪問就像在最後兩個賦值語句中顯式地更新了anim這個struct的Delay和Image字段。
gif.GIF是一个struct类型参考4.4节。struct是一组值或者叫字段的集合不同的类型集合在一个struct可以让我们以一个统一的单元进行处理。anim是一个gif.GIF类型的struct变量。这种写法会生成一个struct变量并且其内部变量LoopCount字段会被设置为nframes而其它的字段会被设置为各自类型默认的零值。struct内部的变量可以以一个点(.)来进行访问就像在最后两个赋值语句中显式地更新了anim这个struct的Delay和Image字段。
lissajous函數內部有兩層嵌套的for循環。外層循環會循環64次每一次都會生成一個單獨的動畵幀。它生成了一個包含兩種顔色的201&201大小的圖片白色和黑色。所有像素點都會被默認設置爲其零值也就是調色闆palette里的第0個值這里我們設置的是白色。每次外層循環都會生成一張新圖片併將一些像素設置爲黑色。其結果會append到之前結果之後。這里我們用到了append(參考4.2.1)內置函數將結果append到anim中的幀列表末尾併設置一個默認的80ms的延遲值。循環結束後所有的延遲值被編碼進了GIF圖片中併將結果寫入到輸出流。out這個變量是io.Writer類型這個類型支持把輸出結果寫到很多目標很快我們就可以看到例子。
lissajous函数内部有两层嵌套的for循环。外层循环会循环64次每一次都会生成一个单独的动画帧。它生成了一个包含两种颜色的201&201大小的图片白色和黑色。所有像素点都会被默认设置为其零值也就是调色板palette里的第0个值这里我们设置的是白色。每次外层循环都会生成一张新图片并将一些像素设置为黑色。其结果会append到之前结果之后。这里我们用到了append(参考4.2.1)内置函数将结果append到anim中的帧列表末尾并设置一个默认的80ms的延迟值。循环结束后所有的延迟值被编码进了GIF图片中并将结果写入到输出流。out这个变量是io.Writer类型这个类型支持把输出结果写到很多目标很快我们就可以看到例子。
內層循環設置兩個偏振值。x軸偏振使用sin函數。y軸偏振也是正絃波但其相對x軸的偏振是一個0-3的隨機值初始偏振值是一個零值隨着動畵的每一幀逐漸增加。循環會一直跑到x軸完成五次完整的循環。每一步它都會調用SetColorIndex來爲(x, y)點來染黑色。
内层循环设置两个偏振值。x轴偏振使用sin函数。y轴偏振也是正弦波但其相对x轴的偏振是一个0-3的随机值初始偏振值是一个零值随着动画的每一帧逐渐增加。循环会一直跑到x轴完成五次完整的循环。每一步它都会调用SetColorIndex来为(x, y)点来染黑色。
main函數調用lissajous函數用它來向標準輸出流打印信息所以下面這個命令會像圖1.1中産生一個GIF動畵
main函数调用lissajous函数用它来向标准输出流打印信息所以下面这个命令会像图1.1中产生一个GIF动画
```
$ go build gopl.io/ch1/lissajous
$ ./lissajous >out.gif
```
**練習 1.5** 脩改前面的Lissajous程序里的調色闆由黑色改爲緑色。我們可以用`color.RGBA{0xRR, 0xGG, 0xBB, 0xff}`來得到`#RRGGBB`這個色值,三個十六進製的字符串分别代表紅、緑、藍像素。
**练习 1.5** 修改前面的Lissajous程序里的调色板由黑色改为绿色。我们可以用`color.RGBA{0xRR, 0xGG, 0xBB, 0xff}`来得到`#RRGGBB`这个色值,三个十六进制的字符串分别代表红、绿、蓝像素。
**練習 1.6** 脩改Lissajous程序脩改其調色闆來生成更豐富的顔色然後脩改SetColorIndex的第三個參數看看顯示結果吧。
**练习 1.6** 修改Lissajous程序修改其调色板来生成更丰富的颜色然后修改SetColorIndex的第三个参数看看显示结果吧。

View File

@ -1,8 +1,8 @@
## 1.5. 取URL
## 1.5. 取URL
對於很多現代應用來説訪問互聯網上的信息和訪問本地文件繫統一樣重要。Go語言在net這個強大package的幫助下提供了一繫列的package來做這件事情使用這些包可以更簡單地用網絡收發信息還可以建立更底層的網絡連接編寫服務器程序。在這些情景下Go語言原生的併發特性在第八章中會介紹得尤其好用。
对于很多现代应用来说访问互联网上的信息和访问本地文件系统一样重要。Go语言在net这个强大package的帮助下提供了一系列的package来做这件事情使用这些包可以更简单地用网络收发信息还可以建立更底层的网络连接编写服务器程序。在这些情景下Go语言原生的并发特性在第八章中会介绍得尤其好用。
爲了最簡單地展示基於HTTP獲取信息的方式下面給出一個示例程序fetch這個程序將獲取對應的url併將其源文本打印出來這個例子的靈感來源於curl工具譯註unix下的一個用來發http請求的工具具體可以man curl。當然curl提供的功能更爲複雜豐富這里隻編寫最簡單的樣例。這個樣例之後還會多次被用到。
为了最简单地展示基于HTTP获取信息的方式下面给出一个示例程序fetch这个程序将获取对应的url并将其源文本打印出来这个例子的灵感来源于curl工具译注unix下的一个用来发http请求的工具具体可以man curl。当然curl提供的功能更为复杂丰富这里只编写最简单的样例。这个样例之后还会多次被用到。
<u><i>gopl.io/ch1/fetch</i></u>
```go
@ -34,7 +34,7 @@ func main() {
}
```
這個程序從兩個package中導入了函數net/http和io/ioutil包http.Get函數是創建HTTP請求的函數如果獲取過程沒有出錯那麽會在resp這個結構體中得到訪問的請求結果。resp的Body字段包括一個可讀的服務器響應流。ioutil.ReadAll函數從response中讀取到全部內容將其結果保存在變量b中。resp.Body.Close關閉resp的Body流防止資源洩露Printf函數會將結果b寫出到標準輸出流中。
这个程序从两个package中导入了函数net/http和io/ioutil包http.Get函数是创建HTTP请求的函数如果获取过程没有出错那么会在resp这个结构体中得到访问的请求结果。resp的Body字段包括一个可读的服务器响应流。ioutil.ReadAll函数从response中读取到全部内容将其结果保存在变量b中。resp.Body.Close关闭resp的Body流防止资源泄露Printf函数会将结果b写出到标准输出流中。
```
$ go build gopl.io/ch1/fetch
@ -45,24 +45,24 @@ $ ./fetch http://gopl.io
...
```
HTTP請求如果失敗了的話,會得到下面這樣的結果:
HTTP请求如果失败了的话,会得到下面这样的结果:
```
$ ./fetch http://bad.gopl.io
fetch: Get http://bad.gopl.io: dial tcp: lookup bad.gopl.io: no such host
```
譯註在大天朝的網絡環境下很容易重現這種錯誤下面是Windows下運行得到的錯誤信息:
译注在大天朝的网络环境下很容易重现这种错误下面是Windows下运行得到的错误信息:
```
$ go run main.go http://gopl.io
fetch: Get http://gopl.io: dial tcp: lookup gopl.io: getaddrinfow: No such host is known.
```
無論哪種失敗原因我們的程序都用了os.Exit函數來終止進程併且返迴一個status錯誤碼其值爲1。
无论哪种失败原因我们的程序都用了os.Exit函数来终止进程并且返回一个status错误码其值为1。
**練習 1.7** 函數調用io.Copy(dst, src)會從src中讀取內容併將讀到的結果寫入到dst中使用這個函數替代掉例子中的ioutil.ReadAll來拷貝響應結構體到os.Stdout避免申請一個緩衝區例子中的b來存儲。記得處理io.Copy返迴結果中的錯誤
**练习 1.7** 函数调用io.Copy(dst, src)会从src中读取内容并将读到的结果写入到dst中使用这个函数替代掉例子中的ioutil.ReadAll来拷贝响应结构体到os.Stdout避免申请一个缓冲区例子中的b来存储。记得处理io.Copy返回结果中的错误
**練習 1.8** 脩改fetch這個范例如果輸入的url參數沒有 `http://` 前綴的話爲這個url加上該前綴。你可能會用到strings.HasPrefix這個函數
**练习 1.8** 修改fetch这个范例如果输入的url参数没有 `http://` 前缀的话为这个url加上该前缀。你可能会用到strings.HasPrefix这个函数
**練習 1.9** 脩改fetch打印出HTTP協議的狀態碼可以從resp.Status變量得到該狀態碼
**练习 1.9** 修改fetch打印出HTTP协议的状态码可以从resp.Status变量得到该状态码

View File

@ -1,8 +1,8 @@
## 1.6. 併發獲取多個URL
## 1.6. 并发获取多个URL
Go語言最有意思併且最新奇的特性就是對併發編程的支持。併發編程是一個大話題在第八章和第九章中會專門講到。這里我們隻淺嚐輒止地來體驗一下Go語言里的goroutine和channel。
Go语言最有意思并且最新奇的特性就是对并发编程的支持。并发编程是一个大话题在第八章和第九章中会专门讲到。这里我们只浅尝辄止地来体验一下Go语言里的goroutine和channel。
下面的例子fetchall和前面小節的fetch程序所要做的工作基本一致fetchall的特别之處在於它會同時去獲取所有的URL所以這個程序的總執行時間不會超過執行時間最長的那一個任務前面的fetch程序執行時間則是所有任務執行時間之和。fetchall程序隻會打印獲取的內容大小和經過的時間不會像之前那樣打印獲取的內容。
下面的例子fetchall和前面小节的fetch程序所要做的工作基本一致fetchall的特别之处在于它会同时去获取所有的URL所以这个程序的总执行时间不会超过执行时间最长的那一个任务前面的fetch程序执行时间则是所有任务执行时间之和。fetchall程序只会打印获取的内容大小和经过的时间不会像之前那样打印获取的内容。
<u><i>gopl.io/ch1/fetchall</i></u>
```go
@ -48,7 +48,7 @@ func fetch(url string, ch chan<- string) {
}
```
下面使用fetchall來請求幾個地址:
下面使用fetchall来请求几个地址:
```
$ go build gopl.io/ch1/fetchall
@ -59,10 +59,10 @@ $ ./fetchall https://golang.org http://gopl.io https://godoc.org
0.48s elapsed
```
goroutine是一種函數的併發執行方式而channel是用來在goroutine之間進行參數傳遞。main函數本身也運行在一個goroutine中而go function則表示創建一個新的goroutine併在這個新的goroutine中執行這個函數
goroutine是一种函数的并发执行方式而channel是用来在goroutine之间进行参数传递。main函数本身也运行在一个goroutine中而go function则表示创建一个新的goroutine并在这个新的goroutine中执行这个函数
main函數中用make函數創建了一個傳遞string類型參數的channel對每一個命令行參數我們都用go這個關鍵字來創建一個goroutine併且讓函數在這個goroutine異步執行http.Get方法。這個程序里的io.Copy會把響應的Body內容拷貝到ioutil.Discard輸出流中譯註可以把這個變量看作一個垃圾桶可以向里面寫一些不需要的數據因爲我們需要這個方法返迴的字節數但是又不想要其內容。每當請求返迴內容時fetch函數都會往ch這個channel里寫入一個字符串由main函數里的第二個for循環來處理併打印channel里的這個字符串。
main函数中用make函数创建了一个传递string类型参数的channel对每一个命令行参数我们都用go这个关键字来创建一个goroutine并且让函数在这个goroutine异步执行http.Get方法。这个程序里的io.Copy会把响应的Body内容拷贝到ioutil.Discard输出流中译注可以把这个变量看作一个垃圾桶可以向里面写一些不需要的数据因为我们需要这个方法返回的字节数但是又不想要其内容。每当请求返回内容时fetch函数都会往ch这个channel里写入一个字符串由main函数里的第二个for循环来处理并打印channel里的这个字符串。
當一個goroutine嚐試在一個channel上做send或者receive操作時這個goroutine會阻塞在調用處直到另一個goroutine往這個channel里寫入、或者接收值這樣兩個goroutine才會繼續執行channel操作之後的邏輯。在這個例子中每一個fetch函數在執行時都會往channel里發送一個值(ch <- expression)(<-ch)mainfetchgoroutinemain退
当一个goroutine尝试在一个channel上做send或者receive操作时这个goroutine会阻塞在调用处直到另一个goroutine往这个channel里写入、或者接收值这样两个goroutine才会继续执行channel操作之后的逻辑。在这个例子中每一个fetch函数在执行时都会往channel里发送一个值(ch <- expression)(<-ch)mainfetchgoroutinemain退
**練習 1.10** 找一個數據量比較大的網站用本小節中的程序調研網站的緩存策略對每個URL執行兩遍請求査看兩次時間是否有較大的差别併且每次獲取到的響應內容是否一致脩改本節中的程序將響應結果輸出以便於進行對比。
**练习 1.10** 找一个数据量比较大的网站用本小节中的程序调研网站的缓存策略对每个URL执行两遍请求查看两次时间是否有较大的差别并且每次获取到的响应内容是否一致修改本节中的程序将响应结果输出以便于进行对比。

View File

@ -1,6 +1,6 @@
## 1.7. Web服
## 1.7. Web服
Go語言的內置庫使得寫一個類似fetch的web服務器變得異常地簡單。在本節中我們會展示一個微型服務器這個服務器的功能是返迴當前用戶正在訪問的URL。比如用戶訪問的是 http://localhost:8000/hello ,那麽響應是URL.Path = "hello"。
Go语言的内置库使得写一个类似fetch的web服务器变得异常地简单。在本节中我们会展示一个微型服务器这个服务器的功能是返回当前用户正在访问的URL。比如用户访问的是 http://localhost:8000/hello ,那么响应是URL.Path = "hello"。
<u><i>gopl.io/ch1/server1</i></u>
```go
@ -24,15 +24,15 @@ func handler(w http.ResponseWriter, r *http.Request) {
}
```
們隻用了八九行代碼就實現了一個Web服務程序這都是多虧了標準庫里的方法已經幫我們完成了大量工作。main函數將所有發送到/路徑下的請求和handler函數關聯起來/開頭的請求其實就是所有發送到當前站點上的請求服務監聽8000端口。發送到這個服務的“請求”是一個http.Request類型的對象這個對象中包含了請求中的一繫列相關字段其中就包括我們需要的URL。當請求到達服務器時這個請求會被傳給handler函數來處理這個函數會將/hello這個路徑從請求的URL中解析出來然後把其發送到響應中這里我們用的是標準輸出流的fmt.Fprintf。Web服務會在第7.7節中做更詳細的闡述。
们只用了八九行代码就实现了一个Web服务程序这都是多亏了标准库里的方法已经帮我们完成了大量工作。main函数将所有发送到/路径下的请求和handler函数关联起来/开头的请求其实就是所有发送到当前站点上的请求服务监听8000端口。发送到这个服务的“请求”是一个http.Request类型的对象这个对象中包含了请求中的一系列相关字段其中就包括我们需要的URL。当请求到达服务器时这个请求会被传给handler函数来处理这个函数会将/hello这个路径从请求的URL中解析出来然后把其发送到响应中这里我们用的是标准输出流的fmt.Fprintf。Web服务会在第7.7节中做更详细的阐述。
讓我們在後台運行這個服務程序。如果你的操作繫統是Mac OS X或者Linux那麽在運行命令的末尾加上一個&符號卽可讓程序簡單地跑在後台windows下可以在另外一個命令行窗口去運行這個程序。
让我们在后台运行这个服务程序。如果你的操作系统是Mac OS X或者Linux那么在运行命令的末尾加上一个&符号即可让程序简单地跑在后台windows下可以在另外一个命令行窗口去运行这个程序。
```
$ go run src/gopl.io/ch1/server1/main.go &
```
現在可以通過命令行來發送客戶端請求了:
现在可以通过命令行来发送客户端请求了:
```
$ go build gopl.io/ch1/fetch
@ -42,11 +42,11 @@ $ ./fetch http://localhost:8000/help
URL.Path = "/help"
```
還可以直接在瀏覽器里訪問這個URL然後得到返迴結果如圖1.2
还可以直接在浏览器里访问这个URL然后得到返回结果如图1.2
![](../images/ch1-02.png)
這個服務的基礎上疊加特性是很容易的。一種比較實用的脩改是爲訪問的url添加某種狀態。比如下面這個版本輸出了同樣的內容但是會對請求的次數進行計算對URL的請求結果會包含各種URL被訪問的總次數直接對/count這個URL的訪問要除外。
这个服务的基础上叠加特性是很容易的。一种比较实用的修改是为访问的url添加某种状态。比如下面这个版本输出了同样的内容但是会对请求的次数进行计算对URL的请求结果会包含各种URL被访问的总次数直接对/count这个URL的访问要除外。
<u><i>gopl.io/ch1/server2</i></u>
```go
@ -85,9 +85,9 @@ func counter(w http.ResponseWriter, r *http.Request) {
}
```
這個服務器有兩個請求處理函數根據請求的url不同會調用不同的函數對/count這個url的請求會調用到count這個函數其它的url都會調用默認的處理函數。如果你的請求pattern是以/結尾那麽所有以該url爲前綴的url都會被這條規則匹配。在這些代碼的背後服務器每一次接收請求處理時都會另起一個goroutine這樣服務器就可以同一時間處理多個請求。然而在併發情況下假如眞的有兩個請求同一時刻去更新count那麽這個值可能併不會被正確地增加這個程序可能會引發一個嚴重的bug競態條件參見9.1。爲了避免這個問題我們必須保證每次脩改變量的最多隻能有一個goroutine這也就是代碼里的mu.Lock()和mu.Unlock()調用將脩改count的所有行爲包在中間的目的。第九章中我們會進一步講解共享變量。
这个服务器有两个请求处理函数根据请求的url不同会调用不同的函数对/count这个url的请求会调用到count这个函数其它的url都会调用默认的处理函数。如果你的请求pattern是以/结尾那么所有以该url为前缀的url都会被这条规则匹配。在这些代码的背后服务器每一次接收请求处理时都会另起一个goroutine这样服务器就可以同一时间处理多个请求。然而在并发情况下假如真的有两个请求同一时刻去更新count那么这个值可能并不会被正确地增加这个程序可能会引发一个严重的bug竞态条件参见9.1。为了避免这个问题我们必须保证每次修改变量的最多只能有一个goroutine这也就是代码里的mu.Lock()和mu.Unlock()调用将修改count的所有行为包在中间的目的。第九章中我们会进一步讲解共享变量。
下面是一個更爲豐富的例子handler函數會把請求的http頭和請求的form數據都打印出來這樣可以使檢査和調試這個服務更爲方便:
下面是一个更为丰富的例子handler函数会把请求的http头和请求的form数据都打印出来这样可以使检查和调试这个服务更为方便:
<u><i>gopl.io/ch1/server3</i></u>
```go
@ -108,7 +108,7 @@ func handler(w http.ResponseWriter, r *http.Request) {
}
```
們用http.Request這個struct里的字段來輸出下面這樣的內容:
们用http.Request这个struct里的字段来输出下面这样的内容:
```
GET /?q=query HTTP/1.1
@ -119,7 +119,7 @@ RemoteAddr = "127.0.0.1:59911"
Form["q"] = ["query"]
```
可以看到這里的ParseForm被嵌套在了if語句中。Go語言允許這樣的一個簡單的語句結果作爲循環的變量聲明出現在if語句的最前面這一點對錯誤處理很有用處。我們還可以像下面這樣寫當然看起來就長了一些):
可以看到这里的ParseForm被嵌套在了if语句中。Go语言允许这样的一个简单的语句结果作为循环的变量声明出现在if语句的最前面这一点对错误处理很有用处。我们还可以像下面这样写当然看起来就长了一些):
```go
err := r.ParseForm()
@ -128,13 +128,13 @@ if err != nil {
}
```
用if和ParseForm結合可以讓代碼更加簡單併且可以限製err這個變量的作用域這麽做是很不錯的。我們會在2.7節中講解作用域。
用if和ParseForm结合可以让代码更加简单并且可以限制err这个变量的作用域这么做是很不错的。我们会在2.7节中讲解作用域。
這些程序中我們看到了很多不同的類型被輸出到標準輸出流中。比如前面的fetch程序把HTTP的響應數據拷貝到了os.Stdoutlissajous程序里我們輸出的是一個文件。fetchall程序則完全忽略到了HTTP的響應Body隻是計算了一下響應Body的大小這個程序中把響應Body拷貝到了ioutil.Discard。在本節的web服務器程序中則是用fmt.Fprintf直接寫到了http.ResponseWriter中。
这些程序中我们看到了很多不同的类型被输出到标准输出流中。比如前面的fetch程序把HTTP的响应数据拷贝到了os.Stdoutlissajous程序里我们输出的是一个文件。fetchall程序则完全忽略到了HTTP的响应Body只是计算了一下响应Body的大小这个程序中把响应Body拷贝到了ioutil.Discard。在本节的web服务器程序中则是用fmt.Fprintf直接写到了http.ResponseWriter中。
盡管三種具體的實現流程併不太一樣他們都實現一個共同的接口卽當它們被調用需要一個標準流輸出時都可以滿足。這個接口叫作io.Writer在7.1節中會詳細討論
尽管三种具体的实现流程并不太一样他们都实现一个共同的接口即当它们被调用需要一个标准流输出时都可以满足。这个接口叫作io.Writer在7.1节中会详细讨论
Go語言的接口機製會在第7章中講解爲了在這里簡單説明接口能做什麽讓我們簡單地將這里的web服務器和之前寫的lissajous函數結合起來這樣GIF動畵可以被寫到HTTP的客戶端而不是之前的標準輸出流。隻要在web服務器的代碼里加入下面這幾行。
Go语言的接口机制会在第7章中讲解为了在这里简单说明接口能做什么让我们简单地将这里的web服务器和之前写的lissajous函数结合起来这样GIF动画可以被写到HTTP的客户端而不是之前的标准输出流。只要在web服务器的代码里加入下面这几行。
```Go
handler := func(w http.ResponseWriter, r *http.Request) {
@ -143,7 +143,7 @@ handler := func(w http.ResponseWriter, r *http.Request) {
http.HandleFunc("/", handler)
```
或者另一種等價形式:
或者另一种等价形式:
```Go
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
@ -151,11 +151,11 @@ http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
})
```
HandleFunc函數的第二個參數是一個函數的字面值也就是一個在使用時定義的匿名函數。這些內容我們會在5.6節中講解。
HandleFunc函数的第二个参数是一个函数的字面值也就是一个在使用时定义的匿名函数。这些内容我们会在5.6节中讲解。
做完這些脩改之後,在瀏覽器里訪問 http://localhost:8000 。每次你載入這個頁面都可以看到一個像圖1.3那樣的動畵
做完这些修改之后,在浏览器里访问 http://localhost:8000 。每次你载入这个页面都可以看到一个像图1.3那样的动画
![](../images/ch1-03.png)
**練習 1.12** 脩改Lissajour服務從URL讀取變量比如你可以訪問 http://localhost:8000/?cycles=20 這個URL這樣訪問可以將程序里的cycles默認的5脩改爲20。字符串轉換爲數字可以調用strconv.Atoi函數。你可以在godoc里査看strconv.Atoi的詳細説明。
**练习 1.12** 修改Lissajour服务从URL读取变量比如你可以访问 http://localhost:8000/?cycles=20 这个URL这样访问可以将程序里的cycles默认的5修改为20。字符串转换为数字可以调用strconv.Atoi函数。你可以在godoc里查看strconv.Atoi的详细说明。

View File

@ -1,8 +1,8 @@
## 1.8. 本章要
## 1.8. 本章要
本章對Go語言做了一些介紹Go語言很多方面在有限的篇幅中無法覆蓋到。本節會把沒有講到的內容也做一些簡單的介紹這樣讀者在讀到完整的內容之前可以有個簡單的印象。
本章对Go语言做了一些介绍Go语言很多方面在有限的篇幅中无法覆盖到。本节会把没有讲到的内容也做一些简单的介绍这样读者在读到完整的内容之前可以有个简单的印象。
**控製流:** 在本章我們隻介紹了if控製和for但是沒有提到switch多路選擇。這里是一個簡單的switch的例子
**控制流:** 在本章我们只介绍了if控制和for但是没有提到switch多路选择。这里是一个简单的switch的例子
```go
switch coinflip() {
@ -15,9 +15,9 @@ default:
}
```
在翻轉硬幣的時候例子里的coinflip函數返迴幾種不同的結果每一個case都會對應一個返迴結果這里需要註意Go語言併不需要顯式地在每一個case後寫break語言默認執行完case後的邏輯語句會自動退出。當然了如果你想要相鄰的幾個case都執行同一邏輯的話需要自己顯式地寫上一個fallthrough語句來覆蓋這種默認行爲。不過fallthrough語句在一般的程序中很少用到。
在翻转硬币的时候例子里的coinflip函数返回几种不同的结果每一个case都会对应一个返回结果这里需要注意Go语言并不需要显式地在每一个case后写break语言默认执行完case后的逻辑语句会自动退出。当然了如果你想要相邻的几个case都执行同一逻辑的话需要自己显式地写上一个fallthrough语句来覆盖这种默认行为。不过fallthrough语句在一般的程序中很少用到。
Go語言里的switch還可以不帶操作對象譯註switch不帶操作對象時默認用true值代替然後將每個case的表達式和true值進行比較可以直接羅列多種條件像其它語言里面的多個if else一樣下面是一個例子:
Go语言里的switch还可以不带操作对象译注switch不带操作对象时默认用true值代替然后将每个case的表达式和true值进行比较可以直接罗列多种条件像其它语言里面的多个if else一样下面是一个例子:
```go
func Signum(x int) int {
@ -32,13 +32,13 @@ func Signum(x int) int {
}
```
這種形式叫做無tag switch(tagless switch)這和switch true是等價的。
这种形式叫做无tag switch(tagless switch)这和switch true是等价的。
像for和if控製語句一樣switch也可以緊跟一個簡短的變量聲明一個自增表達式、賦值語句或者一個函數調用(譯註:比其它語言豐富)。
像for和if控制语句一样switch也可以紧跟一个简短的变量声明一个自增表达式、赋值语句或者一个函数调用(译注:比其它语言丰富)。
break和continue語句會改變控製流。和其它語言中的break和continue一樣break會中斷當前的循環併開始執行循環之後的內容而continue會中跳過當前循環併開始執行下一次循環。這兩個語句除了可以控製for循環還可以用來控製switch和select語句(之後會講到)在1.3節中我們看到continue會跳過內層的循環如果我們想跳過的是更外層的循環的話我們可以在相應的位置加上label這樣break和continue就可以根據我們的想法來continue和break任意循環。這看起來甚至有點像goto語句的作用了。當然一般程序員也不會用到這種操作。這兩種行爲更多地被用到機器生成的代碼中。
break和continue语句会改变控制流。和其它语言中的break和continue一样break会中断当前的循环并开始执行循环之后的内容而continue会中跳过当前循环并开始执行下一次循环。这两个语句除了可以控制for循环还可以用来控制switch和select语句(之后会讲到)在1.3节中我们看到continue会跳过内层的循环如果我们想跳过的是更外层的循环的话我们可以在相应的位置加上label这样break和continue就可以根据我们的想法来continue和break任意循环。这看起来甚至有点像goto语句的作用了。当然一般程序员也不会用到这种操作。这两种行为更多地被用到机器生成的代码中。
**命名類型:** 類型聲明使得我們可以很方便地給一個特殊類型一個名字。因爲struct類型聲明通常非常地長所以我們總要給這種struct取一個名字。本章中就有這樣一個例子二維點類型:
**命名类型:** 类型声明使得我们可以很方便地给一个特殊类型一个名字。因为struct类型声明通常非常地长所以我们总要给这种struct取一个名字。本章中就有这样一个例子二维点类型:
```go
type Point struct {
@ -47,15 +47,15 @@ type Point struct {
var p Point
```
類型聲明和命名類型會在第二章中介紹
类型声明和命名类型会在第二章中介绍
**指針:** Go語言提供了指針。指針是一種直接存儲了變量的內存地址的數據類型。在其它語言中比如C語言指針操作是完全不受約束的。在另外一些語言中指針一般被處理爲“引用”除了到處傳遞這些指針之外併不能對這些指針做太多事情。Go語言在這兩種范圍中取了一種平衡。指針是可見的內存地址&操作符可以返迴一個變量的內存地址,併且*操作符可以獲取指針指向的變量內容但是在Go語言里沒有指針運算也就是不能像c語言里可以對指針進行加或減操作。我們會在2.3.2中進行詳細介紹
**指针:** Go语言提供了指针。指针是一种直接存储了变量的内存地址的数据类型。在其它语言中比如C语言指针操作是完全不受约束的。在另外一些语言中指针一般被处理为“引用”除了到处传递这些指针之外并不能对这些指针做太多事情。Go语言在这两种范围中取了一种平衡。指针是可见的内存地址&操作符可以返回一个变量的内存地址,并且*操作符可以获取指针指向的变量内容但是在Go语言里没有指针运算也就是不能像c语言里可以对指针进行加或减操作。我们会在2.3.2中进行详细介绍
**方法和接口:** 方法是和命名類型關聯的一類函數。Go語言里比較特殊的是方法可以被關聯到任意一種命名類型。在第六章我們會詳細地講方法。接口是一種抽象類型這種類型可以讓我們以同樣的方式來處理不同的固有類型不用關心它們的具體實現而隻需要關註它們提供的方法。第七章中會詳細説明這些內容。
**方法和接口:** 方法是和命名类型关联的一类函数。Go语言里比较特殊的是方法可以被关联到任意一种命名类型。在第六章我们会详细地讲方法。接口是一种抽象类型这种类型可以让我们以同样的方式来处理不同的固有类型不用关心它们的具体实现而只需要关注它们提供的方法。第七章中会详细说明这些内容。
**包packages** Go語言提供了一些很好用的package併且這些package是可以擴展的。Go語言社區已經創造併且分享了很多很多。所以Go語言編程大多數情況下就是用已有的package來寫我們自己的代碼。通過這本書我們會講解一些重要的標準庫內的package但是還是有很多限於篇幅沒有去説明因爲我們沒法在這樣的厚度的書里去做一部代碼大全。
**包packages** Go语言提供了一些很好用的package并且这些package是可以扩展的。Go语言社区已经创造并且分享了很多很多。所以Go语言编程大多数情况下就是用已有的package来写我们自己的代码。通过这本书我们会讲解一些重要的标准库内的package但是还是有很多限于篇幅没有去说明因为我们没法在这样的厚度的书里去做一部代码大全。
在你開始寫一個新程序之前,最好先去檢査一下是不是已經有了現成的庫可以幫助你更高效地完成這件事情。你可以在 https://golang.org/pkg 和 https://godoc.org 中找到標準庫和社區寫的package。godoc這個工具可以讓你直接在本地命令行閲讀標準庫的文檔。比如下面這個例子。
在你开始写一个新程序之前,最好先去检查一下是不是已经有了现成的库可以帮助你更高效地完成这件事情。你可以在 https://golang.org/pkg 和 https://godoc.org 中找到标准库和社区写的package。godoc这个工具可以让你直接在本地命令行阅读标准库的文档。比如下面这个例子。
```
$ go doc http.ListenAndServe
@ -66,7 +66,7 @@ func ListenAndServe(addr string, handler Handler) error
...
```
**註釋:** 我們之前已經提到過了在源文件的開頭寫的註釋是這個源文件的文檔。在每一個函數之前寫一個説明函數行爲的註釋也是一個好習慣。這些慣例很重要因爲這些內容會被像godoc這樣的工具檢測到併且在執行命令時顯示這些註釋。具體可以參考10.7.4。
**注释:** 我们之前已经提到过了在源文件的开头写的注释是这个源文件的文档。在每一个函数之前写一个说明函数行为的注释也是一个好习惯。这些惯例很重要因为这些内容会被像godoc这样的工具检测到并且在执行命令时显示这些注释。具体可以参考10.7.4。
多行註釋可以用 `/* ... */` 來包裹,和其它大多數語言一樣。在文件一開頭的註釋一般都是這種形式,或者一大段的解釋性的註釋文字也會被這符號包住,來避免每一行都需要加//。在註釋中//和/*是沒什麽意義的,所以不要在註釋中再嵌入註釋
多行注释可以用 `/* ... */` 来包裹,和其它大多数语言一样。在文件一开头的注释一般都是这种形式,或者一大段的解释性的注释文字也会被这符号包住,来避免每一行都需要加//。在注释中//和/*是没什么意义的,所以不要在注释中再嵌入注释

View File

@ -1,5 +1,5 @@
# 第1章 入
# 第1章 入
本章介紹Go語言的基礎組件。本章提供了足夠的信息和示例程序希望可以幫你盡快入門, 寫出有用的程序。本章和之後章節的示例程序都針對你可能遇到的現實案例。先了解幾個Go程序涉及的主題從簡單的文件處理、圖像處理到互聯網客戶端和服務端併發。當然第一章不會解釋細枝末節但用這些程序來學習一門新語言還是很有效的。
本章介绍Go语言的基础组件。本章提供了足够的信息和示例程序希望可以帮你尽快入门, 写出有用的程序。本章和之后章节的示例程序都针对你可能遇到的现实案例。先了解几个Go程序涉及的主题从简单的文件处理、图像处理到互联网客户端和服务端并发。当然第一章不会解释细枝末节但用这些程序来学习一门新语言还是很有效的。
學習一門新語言時,會有一種自然的傾向, 按照自己熟悉的語言的套路寫新語言程序。學習Go語言的過程中請警惕這種想法盡量别這麽做。我們會演示怎麽寫好Go語言程序所以請使用本書的代碼作爲你自己寫程序時的指南。
学习一门新语言时,会有一种自然的倾向, 按照自己熟悉的语言的套路写新语言程序。学习Go语言的过程中请警惕这种想法尽量别这么做。我们会演示怎么写好Go语言程序所以请使用本书的代码作为你自己写程序时的指南。

View File

@ -1,9 +1,9 @@
## 10.1. 包
## 10.1. 包
任何包繫統設計的目的都是爲了簡化大型程序的設計和維護工作,通過將一組相關的特性放進一個獨立的單元以便於理解和更新,在每個單元更新的同時保持和程序中其它單元的相對獨立性。這種模塊化的特性允許每個包可以被其它的不同項目共享和重用,在項目范圍內、甚至全球范圍統一的分發和複用。
任何包系统设计的目的都是为了简化大型程序的设计和维护工作,通过将一组相关的特性放进一个独立的单元以便于理解和更新,在每个单元更新的同时保持和程序中其它单元的相对独立性。这种模块化的特性允许每个包可以被其它的不同项目共享和重用,在项目范围内、甚至全球范围统一的分发和复用。
個包一般都定義了一個不同的名字空間用於它內部的每個標識符的訪問。每個名字空間關聯到一個特定的包,讓我們給類型、函數等選擇簡短明了的名字,這樣可以避免在我們使用它們的時候減少和其它部分名字的衝突。
个包一般都定义了一个不同的名字空间用于它内部的每个标识符的访问。每个名字空间关联到一个特定的包,让我们给类型、函数等选择简短明了的名字,这样可以避免在我们使用它们的时候减少和其它部分名字的冲突。
個包還通過控製包內名字的可見性和是否導出來實現封裝特性。通過限製包成員的可見性併隱藏包API的具體實現將允許包的維護者在不影響外部包用戶的前提下調整包的內部實現。通過限製包內變量的可見性還可以強製用戶通過某些特定函數來訪問和更新內部變量這樣可以保證內部變量的一致性和併發時的互斥約束。
个包还通过控制包内名字的可见性和是否导出来实现封装特性。通过限制包成员的可见性并隐藏包API的具体实现将允许包的维护者在不影响外部包用户的前提下调整包的内部实现。通过限制包内变量的可见性还可以强制用户通过某些特定函数来访问和更新内部变量这样可以保证内部变量的一致性和并发时的互斥约束。
當我們脩改了一個源文件我們必須重新編譯該源文件對應的包和所有依賴該包的其他包。卽使是從頭構建Go語言編譯器的編譯速度也明顯快於其它編譯語言。Go語言的閃電般的編譯速度主要得益於三個語言特性。第一點所有導入的包必須在每個文件的開頭顯式聲明這樣的話編譯器就沒有必要讀取和分析整個源文件來判斷包的依賴關繫。第二點禁止包的環狀依賴因爲沒有循環依賴包的依賴關繫形成一個有向無環圖每個包可以被獨立編譯而且很可能是被併發編譯。第三點編譯後包的目標文件不僅僅記録包本身的導出信息目標文件同時還記録了包的依賴關繫。因此在編譯一個包的時候編譯器隻需要讀取每個直接導入包的目標文件而不需要遍歷所有依賴的的文件譯註很多都是重複的間接依賴)。
当我们修改了一个源文件我们必须重新编译该源文件对应的包和所有依赖该包的其他包。即使是从头构建Go语言编译器的编译速度也明显快于其它编译语言。Go语言的闪电般的编译速度主要得益于三个语言特性。第一点所有导入的包必须在每个文件的开头显式声明这样的话编译器就没有必要读取和分析整个源文件来判断包的依赖关系。第二点禁止包的环状依赖因为没有循环依赖包的依赖关系形成一个有向无环图每个包可以被独立编译而且很可能是被并发编译。第三点编译后包的目标文件不仅仅记录包本身的导出信息目标文件同时还记录了包的依赖关系。因此在编译一个包的时候编译器只需要读取每个直接导入包的目标文件而不需要遍历所有依赖的的文件译注很多都是重复的间接依赖)。

View File

@ -1,6 +1,6 @@
## 10.2. 導入路徑
## 10.2. 导入路径
個包是由一個全局唯一的字符串所標識的導入路徑定位。出現在import語句中的導入路徑也是字符串。
个包是由一个全局唯一的字符串所标识的导入路径定位。出现在import语句中的导入路径也是字符串。
```Go
import (
@ -14,6 +14,6 @@ import (
)
```
就像我們在2.6.1節提到過的Go語言的規范併沒有指明包的導入路徑字符串的具體含義導入路徑的具體含義是由構建工具來解釋的。在本章我們將深入討論Go語言工具箱的功能包括大家經常使用的構建測試等功能。當然也有第三方擴展的工具箱存在。例如Google公司內部的Go語言碼農他們就使用內部的多語言構建繫統譯註Google公司使用的是類似[Bazel](http://bazel.io)的構建繫統支持多種編程語言目前該構件繫統還不能完整支持Windows環境用不同的規則來處理包名字和定位包用不同的規則來處理單元測試等等因爲這樣可以更緊密適配他們內部環境。
就像我们在2.6.1节提到过的Go语言的规范并没有指明包的导入路径字符串的具体含义导入路径的具体含义是由构建工具来解释的。在本章我们将深入讨论Go语言工具箱的功能包括大家经常使用的构建测试等功能。当然也有第三方扩展的工具箱存在。例如Google公司内部的Go语言码农他们就使用内部的多语言构建系统译注Google公司使用的是类似[Bazel](http://bazel.io)的构建系统支持多种编程语言目前该构件系统还不能完整支持Windows环境用不同的规则来处理包名字和定位包用不同的规则来处理单元测试等等因为这样可以更紧密适配他们内部环境。
如果你計劃分享或發布包那麽導入路徑最好是全球唯一的。爲了避免衝突所有非標準庫包的導入路徑建議以所在組織的互聯網域名爲前綴而且這樣也有利於包的檢索。例如上面的import語句導入了Go糰隊維護的HTML解析器和一個流行的第三方維護的MySQL驅動
如果你计划分享或发布包那么导入路径最好是全球唯一的。为了避免冲突所有非标准库包的导入路径建议以所在组织的互联网域名为前缀而且这样也有利于包的检索。例如上面的import语句导入了Go团队维护的HTML解析器和一个流行的第三方维护的MySQL驱动

View File

@ -1,8 +1,8 @@
## 10.3. 包
## 10.3. 包
在每個Go語音源文件的開頭都必須有包聲明語句。包聲明語句的主要目的是確定當前包被其它包導入時默認的標識符也稱爲包名)。
在每个Go语音源文件的开头都必须有包声明语句。包声明语句的主要目的是确定当前包被其它包导入时默认的标识符也称为包名)。
例如math/rand包的每個源文件的開頭都包含`package rand`包聲明語句所以當你導入這個包你就可以用rand.Int、rand.Float64類似的方式訪問包的成員
例如math/rand包的每个源文件的开头都包含`package rand`包声明语句所以当你导入这个包你就可以用rand.Int、rand.Float64类似的方式访问包的成员
```Go
package main
@ -17,10 +17,10 @@ func main() {
}
```
通常來説默認的包名就是包導入路徑名的最後一段因此卽使兩個包的導入路徑不同它們依然可能有一個相同的包名。例如math/rand包和crypto/rand包的包名都是rand。稍後我們將看到如何同時導入兩個有相同包名的包。
通常来说默认的包名就是包导入路径名的最后一段因此即使两个包的导入路径不同它们依然可能有一个相同的包名。例如math/rand包和crypto/rand包的包名都是rand。稍后我们将看到如何同时导入两个有相同包名的包。
關於默認包名一般采用導入路徑名的最後一段的約定也有三種例外情況。第一個例外包對應一個可執行程序也就是main包這時候main包本身的導入路徑是無關緊要的。名字爲main的包是給go build§10.7.3)構建命令一個信息,這個包編譯完之後必須調用連接器生成一個可執行程序。
关于默认包名一般采用导入路径名的最后一段的约定也有三种例外情况。第一个例外包对应一个可执行程序也就是main包这时候main包本身的导入路径是无关紧要的。名字为main的包是给go build§10.7.3)构建命令一个信息,这个包编译完之后必须调用连接器生成一个可执行程序。
第二個例外包所在的目録中可能有一些文件名是以_test.go爲後綴的Go源文件譯註前面必須有其它的字符因爲以`_`前綴的源文件是被忽略的併且這些源文件聲明的包名也是以_test爲後綴名的。這種目録可以包含兩種包一種普通包加一種則是測試的外部擴展包。所有以_test爲後綴包名的測試外部擴展包都由go test命令獨立編譯普通包和測試的外部擴展包是相互獨立的。測試的外部擴展包一般用來避免測試代碼中的循環導入依賴具體細節我們將在11.2.4節中介紹
第二个例外包所在的目录中可能有一些文件名是以_test.go为后缀的Go源文件译注前面必须有其它的字符因为以`_`前缀的源文件是被忽略的并且这些源文件声明的包名也是以_test为后缀名的。这种目录可以包含两种包一种普通包加一种则是测试的外部扩展包。所有以_test为后缀包名的测试外部扩展包都由go test命令独立编译普通包和测试的外部扩展包是相互独立的。测试的外部扩展包一般用来避免测试代码中的循环导入依赖具体细节我们将在11.2.4节中介绍
第三個例外,一些依賴版本號的管理工具會在導入路徑後追加版本號信息,例如"gopkg.in/yaml.v2"。這種情況下包的名字併不包含版本號後綴而是yaml。
第三个例外,一些依赖版本号的管理工具会在导入路径后追加版本号信息,例如"gopkg.in/yaml.v2"。这种情况下包的名字并不包含版本号后缀而是yaml。

View File

@ -1,6 +1,6 @@
## 10.4. 導入聲
## 10.4. 导入声
可以在一個Go語言源文件包聲明語句之後其它非導入聲明語句之前包含零到多個導入包聲明語句。每個導入聲明可以單獨指定一個導入路徑也可以通過圓括號同時導入多個導入路徑。下面兩個導入形式是等價的但是第二種形式更爲常見
可以在一个Go语言源文件包声明语句之后其它非导入声明语句之前包含零到多个导入包声明语句。每个导入声明可以单独指定一个导入路径也可以通过圆括号同时导入多个导入路径。下面两个导入形式是等价的但是第二种形式更为常见
```Go
import "fmt"
@ -12,7 +12,7 @@ import (
)
```
導入的包之間可以通過添加空行來分組通常將來自不同組織的包獨自分組。包的導入順序無關緊要但是在每個分組中一般會根據字符串順序排列。gofmt和goimports工具都可以將不同分組導入的包獨立排序。)
导入的包之间可以通过添加空行来分组通常将来自不同组织的包独自分组。包的导入顺序无关紧要但是在每个分组中一般会根据字符串顺序排列。gofmt和goimports工具都可以将不同分组导入的包独立排序。)
```Go
import (
@ -25,7 +25,7 @@ import (
)
```
如果我們想同時導入兩個有着名字相同的包例如math/rand包和crypto/rand包那麽導入聲明必須至少爲一個同名包指定一個新的包名以避免衝突。這叫做導入包的重命名。
如果我们想同时导入两个有着名字相同的包例如math/rand包和crypto/rand包那么导入声明必须至少为一个同名包指定一个新的包名以避免冲突。这叫做导入包的重命名。
```Go
import (
@ -34,8 +34,8 @@ import (
)
```
導入包的重命名隻影響當前的源文件。其它的源文件如果導入了相同的包,可以用導入包原本默認的名字或重命名爲另一個完全不同的名字。
导入包的重命名只影响当前的源文件。其它的源文件如果导入了相同的包,可以用导入包原本默认的名字或重命名为另一个完全不同的名字。
導入包重命名是一個有用的特性它不僅僅隻是爲了解決名字衝突。如果導入的一個包名很笨重特别是在一些自動生成的代碼中這時候用一個簡短名稱會更方便。選擇用簡短名稱重命名導入包時候最好統一以避免包名混亂。選擇另一個包名稱還可以幫助避免和本地普通變量名産生衝突。例如如果文件中已經有了一個名爲path的變量那麽我們可以將"path"標準包重命名爲pathpkg。
导入包重命名是一个有用的特性它不仅仅只是为了解决名字冲突。如果导入的一个包名很笨重特别是在一些自动生成的代码中这时候用一个简短名称会更方便。选择用简短名称重命名导入包时候最好统一以避免包名混乱。选择另一个包名称还可以帮助避免和本地普通变量名产生冲突。例如如果文件中已经有了一个名为path的变量那么我们可以将"path"标准包重命名为pathpkg。
個導入聲明語句都明確指定了當前包和被導入包之間的依賴關繫。如果遇到包循環導入的情況Go語言的構建工具將報告錯誤
个导入声明语句都明确指定了当前包和被导入包之间的依赖关系。如果遇到包循环导入的情况Go语言的构建工具将报告错误

View File

@ -1,14 +1,14 @@
## 10.5. 包的匿名
## 10.5. 包的匿名
如果隻是導入一個包而併不使用導入的包將會導致一個編譯錯誤。但是有時候我們隻是想利用導入包而産生的副作用它會計算包級變量的初始化表達式和執行導入包的init初始化函數§2.6.2。這時候我們需要抑製“unused import”編譯錯誤我們可以用下劃線`_`來重命名導入的包。像往常一樣,下劃線`_`爲空白標識符,併不能被訪問
如果只是导入一个包而并不使用导入的包将会导致一个编译错误。但是有时候我们只是想利用导入包而产生的副作用它会计算包级变量的初始化表达式和执行导入包的init初始化函数§2.6.2。这时候我们需要抑制“unused import”编译错误我们可以用下划线`_`来重命名导入的包。像往常一样,下划线`_`为空白标识符,并不能被访问
```Go
import _ "image/png" // register PNG decoder
```
這個被稱爲包的匿名導入。它通常是用來實現一個編譯時機製然後通過在main主程序入口選擇性地導入附加的包。首先讓我們看看如何使用該特性然後再看看它是如何工作的。
这个被称为包的匿名导入。它通常是用来实现一个编译时机制然后通过在main主程序入口选择性地导入附加的包。首先让我们看看如何使用该特性然后再看看它是如何工作的。
標準庫的image圖像包包含了一個`Decode`函數,用於從`io.Reader`接口讀取數據併解碼圖像它調用底層註冊的圖像解碼器來完成任務然後返迴image.Image類型的圖像。使用`image.Decode`很容易編寫一個圖像格式的轉換工具,讀取一種格式的圖像,然後編碼爲另一種圖像格式:
标准库的image图像包包含了一个`Decode`函数,用于从`io.Reader`接口读取数据并解码图像它调用底层注册的图像解码器来完成任务然后返回image.Image类型的图像。使用`image.Decode`很容易编写一个图像格式的转换工具,读取一种格式的图像,然后编码为另一种图像格式:
<u><i>gopl.io/ch10/jpeg</i></u>
```Go
@ -42,7 +42,7 @@ func toJPEG(in io.Reader, out io.Writer) error {
}
```
如果我們將`gopl.io/ch3/mandelbrot`§3.3的輸出導入到這個程序的標準輸入它將解碼輸入的PNG格式圖像然後轉換爲JPEG格式的圖像輸出3.3)。
如果我们将`gopl.io/ch3/mandelbrot`§3.3的输出导入到这个程序的标准输入它将解码输入的PNG格式图像然后转换为JPEG格式的图像输出3.3)。
```
$ go build gopl.io/ch3/mandelbrot
@ -51,7 +51,7 @@ $ ./mandelbrot | ./jpeg >mandelbrot.jpg
Input format = png
```
註意image/png包的匿名導入語句。如果沒有這一行語句程序依然可以編譯和運行但是它將不能正確識别和解碼PNG格式的圖像:
注意image/png包的匿名导入语句。如果没有这一行语句程序依然可以编译和运行但是它将不能正确识别和解码PNG格式的图像:
```
$ go build gopl.io/ch10/jpeg
@ -59,7 +59,7 @@ $ ./mandelbrot | ./jpeg >mandelbrot.jpg
jpeg: image: unknown format
```
下面的代碼演示了它的工作機製。標準庫還提供了GIF、PNG和JPEG等格式圖像的解碼器用戶也可以提供自己的解碼器但是爲了保持程序體積較小很多解碼器併沒有被全部包含除非是明確需要支持的格式。image.Decode函數在解碼時會依次査詢支持的格式列表。每個格式驅動列表的每個入口指定了四件事情格式的名稱一個用於描述這種圖像數據開頭部分模式的字符串用於解碼器檢測識别一個Decode函數用於完成解碼圖像工作一個DecodeConfig函數用於解碼圖像的大小和顔色空間的信息。每個驅動入口是通過調用image.RegisterFormat函數註冊一般是在每個格式包的init初始化函數中調用例如image/png包是這樣註冊的:
下面的代码演示了它的工作机制。标准库还提供了GIF、PNG和JPEG等格式图像的解码器用户也可以提供自己的解码器但是为了保持程序体积较小很多解码器并没有被全部包含除非是明确需要支持的格式。image.Decode函数在解码时会依次查询支持的格式列表。每个格式驱动列表的每个入口指定了四件事情格式的名称一个用于描述这种图像数据开头部分模式的字符串用于解码器检测识别一个Decode函数用于完成解码图像工作一个DecodeConfig函数用于解码图像的大小和颜色空间的信息。每个驱动入口是通过调用image.RegisterFormat函数注册一般是在每个格式包的init初始化函数中调用例如image/png包是这样注册的:
```Go
package png // image/png
@ -73,9 +73,9 @@ func init() {
}
```
終的效果是主程序隻需要匿名導入特定圖像驅動包就可以用image.Decode解碼對應格式的圖像了。
终的效果是主程序只需要匿名导入特定图像驱动包就可以用image.Decode解码对应格式的图像了。
數據庫包database/sql也是采用了類似的技術讓用戶可以根據自己需要選擇導入必要的數據庫驅動。例如:
数据库包database/sql也是采用了类似的技术让用户可以根据自己需要选择导入必要的数据库驱动。例如:
```Go
import (
@ -89,6 +89,6 @@ db, err = sql.Open("mysql", dbname) // OK
db, err = sql.Open("sqlite3", dbname) // returns error: unknown driver "sqlite3"
```
**練習 10.1** 擴展jpeg程序以支持任意圖像格式之間的相互轉換使用image.Decode檢測支持的格式類型然後通過flag命令行標誌參數選擇輸出的格式。
**练习 10.1** 扩展jpeg程序以支持任意图像格式之间的相互转换使用image.Decode检测支持的格式类型然后通过flag命令行标志参数选择输出的格式。
**練習 10.2** 設計一個通用的壓縮文件讀取框架用來讀取ZIParchive/zip和POSIX tararchive/tar格式壓縮的文檔。使用類似上面的註冊技術來擴展支持不同的壓縮格式然後根據需要通過匿名導入選擇導入要支持的壓縮格式的驅動包。
**练习 10.2** 设计一个通用的压缩文件读取框架用来读取ZIParchive/zip和POSIX tararchive/tar格式压缩的文档。使用类似上面的注册技术来扩展支持不同的压缩格式然后根据需要通过匿名导入选择导入要支持的压缩格式的驱动包。

View File

@ -1,22 +1,22 @@
## 10.6. 包和命名
在本節中我們將提供一些關於Go語言獨特的包和成員命名的約定。
在本节中我们将提供一些关于Go语言独特的包和成员命名的约定。
當創建一個包,一般要用短小的包名,但也不能太短導致難以理解。標準庫中最常用的包有bufio、bytes、flag、fmt、http、io、json、os、sort、sync和time等包。
当创建一个包,一般要用短小的包名,但也不能太短导致难以理解。标准库中最常用的包有bufio、bytes、flag、fmt、http、io、json、os、sort、sync和time等包。
們的名字都簡潔明了。例如不要將一個類似imageutil或ioutilis的通用包命名爲util雖然它看起來很短小。要盡量避免包名使用可能被經常用於局部變量的名字這樣可能導致用戶重命名導入包例如前面看到的path包。
们的名字都简洁明了。例如不要将一个类似imageutil或ioutilis的通用包命名为util虽然它看起来很短小。要尽量避免包名使用可能被经常用于局部变量的名字这样可能导致用户重命名导入包例如前面看到的path包。
包名一般采用單數的形式。標準庫的bytes、errors和strings使用了複數形式這是爲了避免和預定義的類型衝突同樣還有go/types是爲了避免和type關鍵字衝突。
包名一般采用单数的形式。标准库的bytes、errors和strings使用了复数形式这是为了避免和预定义的类型冲突同样还有go/types是为了避免和type关键字冲突。
要避免包名有其它的含義。例如2.5節中我們的溫度轉換包最初使用了temp包名雖然併沒有持續多久。但這是一個糟糕的嚐試因爲temp幾乎是臨時變量的同義詞。然後我們有一段時間使用了temperature作爲包名雖然名字併沒有表達包的眞實用途。最後我們改成了和strconv標準包類似的tempconv包名這個名字比之前的就好多了。
要避免包名有其它的含义。例如2.5节中我们的温度转换包最初使用了temp包名虽然并没有持续多久。但这是一个糟糕的尝试因为temp几乎是临时变量的同义词。然后我们有一段时间使用了temperature作为包名虽然名字并没有表达包的真实用途。最后我们改成了和strconv标准包类似的tempconv包名这个名字比之前的就好多了。
現在讓我們看看如何命名包的成員。由於是通過包的導入名字引入包里面的成員例如fmt.Println同時包含了包名和成員名信息。因此我們一般併不需要關註Println的具體內容因爲fmt包名已經包含了這個信息。當設計一個包的時候需要考慮包名和成員名兩個部分如何很好地配合。下面有一些例子:
现在让我们看看如何命名包的成员。由于是通过包的导入名字引入包里面的成员例如fmt.Println同时包含了包名和成员名信息。因此我们一般并不需要关注Println的具体内容因为fmt包名已经包含了这个信息。当设计一个包的时候需要考虑包名和成员名两个部分如何很好地配合。下面有一些例子:
```
bytes.Equal flag.Int http.Get json.Marshal
```
們可以看到一些常用的命名模式。strings包提供了和字符串相關的諸多操作:
们可以看到一些常用的命名模式。strings包提供了和字符串相关的诸多操作:
```Go
package strings
@ -30,9 +30,9 @@ type Reader struct{ /* ... */ }
func NewReader(s string) *Reader
```
字符串string本身併沒有出現在每個成員名字中。因爲用戶會這樣引用這些成員strings.Index、strings.Replacer等。
字符串string本身并没有出现在每个成员名字中。因为用户会这样引用这些成员strings.Index、strings.Replacer等。
其它一些包,可能隻描述了單一的數據類型例如html/template和math/rand等隻暴露一個主要的數據結構和與它相關的方法還有一個以New命名的函數用於創建實例。
其它一些包,可能只描述了单一的数据类型例如html/template和math/rand等只暴露一个主要的数据结构和与它相关的方法还有一个以New命名的函数用于创建实例。
```Go
package rand // "math/rand"
@ -41,6 +41,6 @@ type Rand struct{ /* ... */ }
func New(source Source) *Rand
```
這可能導致一些名字重複例如template.Template或rand.Rand這就是爲什麽這些種類的包名往往特别短的原因之一。
这可能导致一些名字重复例如template.Template或rand.Rand这就是为什么这些种类的包名往往特别短的原因之一。
在另一個極端還有像net/http包那樣含有非常多的名字和種類不多的數據類型因爲它們都是要執行一個複雜的複合任務。盡管有將近二十種類型和更多的函數但是包中最重要的成員名字卻是簡單明了的Get、Post、Handle、Error、Client、Server等。
在另一个极端还有像net/http包那样含有非常多的名字和种类不多的数据类型因为它们都是要执行一个复杂的复合任务。尽管有将近二十种类型和更多的函数但是包中最重要的成员名字却是简单明了的Get、Post、Handle、Error、Client、Server等。

View File

@ -1,13 +1,13 @@
### 10.7.1. 工作區結構
### 10.7.1. 工作区结构
對於大多數的Go語言用戶隻需要配置一個名叫GOPATH的環境變量用來指定當前工作目録卽可。當需要切換到不同工作區的時候隻要更新GOPATH就可以了。例如我們在編寫本書時將GOPATH設置爲`$HOME/gobook`
对于大多数的Go语言用户只需要配置一个名叫GOPATH的环境变量用来指定当前工作目录即可。当需要切换到不同工作区的时候只要更新GOPATH就可以了。例如我们在编写本书时将GOPATH设置为`$HOME/gobook`
```
$ export GOPATH=$HOME/gobook
$ go get gopl.io/...
```
當你用前面介紹的命令下載本書全部的例子源碼之後,你的當前工作區的目録結構應該是這樣的:
当你用前面介绍的命令下载本书全部的例子源码之后,你的当前工作区的目录结构应该是这样的:
```
GOPATH/
@ -34,11 +34,11 @@ GOPATH/
...
```
GOPATH對應的工作區目録有三個子目録。其中src子目録用於存儲源代碼。每個包被保存在與$GOPATH/src的相對路徑爲包導入路徑的子目録中例如gopl.io/ch1/helloworld相對應的路徑目録。我們看到一個GOPATH工作區的src目録中可能有多個獨立的版本控製繫統例如gopl.io和golang.org分别對應不同的Git倉庫。其中pkg子目録用於保存編譯後的包的目標文件bin子目録用於保存編譯後的可執行程序例如helloworld可執行程序。
GOPATH对应的工作区目录有三个子目录。其中src子目录用于存储源代码。每个包被保存在与$GOPATH/src的相对路径为包导入路径的子目录中例如gopl.io/ch1/helloworld相对应的路径目录。我们看到一个GOPATH工作区的src目录中可能有多个独立的版本控制系统例如gopl.io和golang.org分别对应不同的Git仓库。其中pkg子目录用于保存编译后的包的目标文件bin子目录用于保存编译后的可执行程序例如helloworld可执行程序。
第二個環境變量GOROOT用來指定Go的安裝目録還有它自帶的標準庫包的位置。GOROOT的目録結構和GOPATH類似因此存放fmt包的源代碼對應目録應該爲$GOROOT/src/fmt。用戶一般不需要設置GOROOT默認情況下Go語言安裝工具會將其設置爲安裝的目録路徑
第二个环境变量GOROOT用来指定Go的安装目录还有它自带的标准库包的位置。GOROOT的目录结构和GOPATH类似因此存放fmt包的源代码对应目录应该为$GOROOT/src/fmt。用户一般不需要设置GOROOT默认情况下Go语言安装工具会将其设置为安装的目录路径
其中`go env`命令用於査看Go語音工具涉及的所有環境變量的值包括未設置環境變量的默認值。GOOS環境變量用於指定目標操作繫統例如android、linux、darwin或windowsGOARCH環境變量用於指定處理器的類型例如amd64、386或arm等。雖然GOPATH環境變量是唯一必需要設置的但是其它環境變量也會偶爾用到。
其中`go env`命令用于查看Go语音工具涉及的所有环境变量的值包括未设置环境变量的默认值。GOOS环境变量用于指定目标操作系统例如android、linux、darwin或windowsGOARCH环境变量用于指定处理器的类型例如amd64、386或arm等。虽然GOPATH环境变量是唯一必需要设置的但是其它环境变量也会偶尔用到。
```
$ go env

View File

@ -1,10 +1,10 @@
### 10.7.2. 下
### 10.7.2. 下
使用Go語言工具箱的go命令不僅可以根據包導入路徑找到本地工作區的包甚至可以從互聯網上找到和更新包。
使用Go语言工具箱的go命令不仅可以根据包导入路径找到本地工作区的包甚至可以从互联网上找到和更新包。
使用命令`go get`可以下載一個單一的包或者用`...`下載整個子目録里面的每個包。Go語言工具箱的go命令同時計算併下載所依賴的每個包這也是前一個例子中golang.org/x/net/html自動出現在本地工作區目録的原因。
使用命令`go get`可以下载一个单一的包或者用`...`下载整个子目录里面的每个包。Go语言工具箱的go命令同时计算并下载所依赖的每个包这也是前一个例子中golang.org/x/net/html自动出现在本地工作区目录的原因。
一旦`go get`命令下載了包然後就是安裝包或包對應的可執行的程序。我們將在下一節再關註它的細節現在隻是展示整個下載過程是如何的簡單。第一個命令是獲取golint工具它用於檢測Go源代碼的編程風格是否有問題。第二個命令是用golint命令對2.6.2節的gopl.io/ch2/popcount包代碼進行編碼風格檢査。它友好地報告了忘記了包的文檔
一旦`go get`命令下载了包然后就是安装包或包对应的可执行的程序。我们将在下一节再关注它的细节现在只是展示整个下载过程是如何的简单。第一个命令是获取golint工具它用于检测Go源代码的编程风格是否有问题。第二个命令是用golint命令对2.6.2节的gopl.io/ch2/popcount包代码进行编码风格检查。它友好地报告了忘记了包的文档
```
$ go get github.com/golang/lint/golint
@ -13,9 +13,9 @@ src/gopl.io/ch2/popcount/main.go:1:1:
package comment should be of the form "Package popcount ..."
```
`go get`命令支持當前流行的託管網站GitHub、Bitbucket和Launchpad可以直接向它們的版本控製繫統請求代碼。對於其它的網站你可能需要指定版本控製繫統的具體路徑和協議例如 Git或Mercurial。運行`go help importpath`獲取相關的信息。
`go get`命令支持当前流行的托管网站GitHub、Bitbucket和Launchpad可以直接向它们的版本控制系统请求代码。对于其它的网站你可能需要指定版本控制系统的具体路径和协议例如 Git或Mercurial。运行`go help importpath`获取相关的信息。
`go get`命令獲取的代碼是眞實的本地存儲倉庫而不僅僅隻是複製源文件因此你依然可以使用版本管理工具比較本地代碼的變更或者切換到其它的版本。例如golang.org/x/net包目録對應一個Git倉庫
`go get`命令获取的代码是真实的本地存储仓库而不仅仅只是复制源文件因此你依然可以使用版本管理工具比较本地代码的变更或者切换到其它的版本。例如golang.org/x/net包目录对应一个Git仓库
```
$ cd $GOPATH/src/golang.org/x/net
@ -24,7 +24,7 @@ origin https://go.googlesource.com/net (fetch)
origin https://go.googlesource.com/net (push)
```
需要註意的是導入路徑含有的網站域名和本地Git倉庫對應遠程服務地址併不相同眞實的Git地址是go.googlesource.com。這其實是Go語言工具的一個特性可以讓包用一個自定義的導入路徑但是眞實的代碼卻是由更通用的服務提供例如googlesource.com或github.com。因爲頁面 https://golang.org/x/net/html 包含了如下的元數據它告訴Go語言的工具當前包眞實的Git倉庫託管地址:
需要注意的是导入路径含有的网站域名和本地Git仓库对应远程服务地址并不相同真实的Git地址是go.googlesource.com。这其实是Go语言工具的一个特性可以让包用一个自定义的导入路径但是真实的代码却是由更通用的服务提供例如googlesource.com或github.com。因为页面 https://golang.org/x/net/html 包含了如下的元数据它告诉Go语言的工具当前包真实的Git仓库托管地址:
```
$ go build gopl.io/ch1/fetch
@ -33,8 +33,8 @@ $ ./fetch https://golang.org/x/net/html | grep go-import
content="golang.org/x/net git https://go.googlesource.com/net">
```
如果指定`-u`命令行標誌參數,`go get`命令將確保所有的包和依賴的包的版本都是最新的,然後重新編譯和安裝它們。如果不包含該標誌參數的話,而且如果包已經在本地存在,那麽代碼那麽將不會被自動更新。
如果指定`-u`命令行标志参数,`go get`命令将确保所有的包和依赖的包的版本都是最新的,然后重新编译和安装它们。如果不包含该标志参数的话,而且如果包已经在本地存在,那么代码那么将不会被自动更新。
`go get -u`命令隻是簡單地保證每個包是最新版本如果是第一次下載包則是比較很方便的但是對於發布程序則可能是不合適的因爲本地程序可能需要對依賴的包做精確的版本依賴管理。通常的解決方案是使用vendor的目録用於存儲依賴包的固定版本的源代碼對本地依賴的包的版本更新也是謹慎和持續可控的。在Go1.5之前一般需要脩改包的導入路徑所以複製後golang.org/x/net/html導入路徑可能會變爲gopl.io/vendor/golang.org/x/net/html。最新的Go語言命令已經支持vendor特性但限於篇幅這里併不討論vendor的具體細節。不過可以通過`go help gopath`命令査看Vendor的幫助文檔
`go get -u`命令只是简单地保证每个包是最新版本如果是第一次下载包则是比较很方便的但是对于发布程序则可能是不合适的因为本地程序可能需要对依赖的包做精确的版本依赖管理。通常的解决方案是使用vendor的目录用于存储依赖包的固定版本的源代码对本地依赖的包的版本更新也是谨慎和持续可控的。在Go1.5之前一般需要修改包的导入路径所以复制后golang.org/x/net/html导入路径可能会变为gopl.io/vendor/golang.org/x/net/html。最新的Go语言命令已经支持vendor特性但限于篇幅这里并不讨论vendor的具体细节。不过可以通过`go help gopath`命令查看Vendor的帮助文档
**練習 10.3:** 從 http://gopl.io/ch1/helloworld?go-get=1 獲取內容,査看本書的代碼的眞實託管的網址(`go get`請求HTML頁面時包含了`go-get`參數,以區别普通的瀏覽器請求)。
**练习 10.3:** 从 http://gopl.io/ch1/helloworld?go-get=1 获取内容,查看本书的代码的真实托管的网址(`go get`请求HTML页面时包含了`go-get`参数,以区别普通的浏览器请求)。

View File

@ -1,10 +1,10 @@
### 10.7.3. 建包
### 10.7.3. 建包
`go build`命令編譯命令行參數指定的每個包。如果包是一個庫則忽略輸出結果這可以用於檢測包的可以正確編譯的。如果包的名字是main`go build`將調用連接器在當前目録創建一個可執行程序;以導入路徑的最後一段作爲可執行程序的名字。
`go build`命令编译命令行参数指定的每个包。如果包是一个库则忽略输出结果这可以用于检测包的可以正确编译的。如果包的名字是main`go build`将调用连接器在当前目录创建一个可执行程序;以导入路径的最后一段作为可执行程序的名字。
爲每個目録隻包含一個包因此每個對應可執行程序或者叫Unix術語中的命令的包會要求放到一個獨立的目録中。這些目録有時候會放在名叫cmd目録的子目録下面例如用於提供Go文檔服務的golang.org/x/tools/cmd/godoc命令就是放在cmd子目録§10.7.4)。
为每个目录只包含一个包因此每个对应可执行程序或者叫Unix术语中的命令的包会要求放到一个独立的目录中。这些目录有时候会放在名叫cmd目录的子目录下面例如用于提供Go文档服务的golang.org/x/tools/cmd/godoc命令就是放在cmd子目录§10.7.4)。
個包可以由它們的導入路徑指定,就像前面看到的那樣,或者用一個相對目録的路徑知指定,相對路徑必須以`.`或`..`開頭。如果沒有指定參數,那麽默認指定爲當前目録對應的包。 下面的命令用於構建同一個包, 雖然它們的寫法各不相同:
个包可以由它们的导入路径指定,就像前面看到的那样,或者用一个相对目录的路径知指定,相对路径必须以`.`或`..`开头。如果没有指定参数,那么默认指定为当前目录对应的包。 下面的命令用于构建同一个包, 虽然它们的写法各不相同:
```
$ cd $GOPATH/src/gopl.io/ch1/helloworld
@ -25,7 +25,7 @@ $ cd $GOPATH
$ go build ./src/gopl.io/ch1/helloworld
```
但不能這樣
但不能这样
```
$ cd $GOPATH
@ -33,7 +33,7 @@ $ go build src/gopl.io/ch1/helloworld
Error: cannot find package "src/gopl.io/ch1/helloworld".
```
也可以指定包的源文件列表,這一般這隻用於構建一些小程序或做一些臨時性的實驗。如果是main包將會以第一個Go源文件的基礎文件名作爲最終的可執行程序的名字。
也可以指定包的源文件列表,这一般这只用于构建一些小程序或做一些临时性的实验。如果是main包将会以第一个Go源文件的基础文件名作为最终的可执行程序的名字。
```
$ cat quoteargs.go
@ -52,22 +52,22 @@ $ ./quoteargs one "two three" four\ five
["one" "two three" "four five"]
```
特别是對於這類一次性運行的程序,我們希望盡快的構建併運行它。`go run`命令實際上是結合了構建和運行的兩個步驟
特别是对于这类一次性运行的程序,我们希望尽快的构建并运行它。`go run`命令实际上是结合了构建和运行的两个步骤
```
$ go run quoteargs.go one "two three" four\ five
["one" "two three" "four five"]
```
第一行的參數列表中,第一個不是以`.go`結尾的將作爲可執行程序的參數運行。
第一行的参数列表中,第一个不是以`.go`结尾的将作为可执行程序的参数运行。
認情況下,`go build`命令構建指定的包和它依賴的包,然後丟棄除了最後的可執行文件之外所有的中間編譯結果。依賴分析和編譯過程雖然都是很快的,但是隨着項目增加到幾十個包和成韆上萬行代碼,依賴關繫分析和編譯時間的消耗將變的可觀,有時候可能需要幾秒種,卽使這些依賴項沒有改變
认情况下,`go build`命令构建指定的包和它依赖的包,然后丢弃除了最后的可执行文件之外所有的中间编译结果。依赖分析和编译过程虽然都是很快的,但是随着项目增加到几十个包和成千上万行代码,依赖关系分析和编译时间的消耗将变的可观,有时候可能需要几秒种,即使这些依赖项没有改变
`go install`命令和`go build`命令很相似,但是它會保存每個包的編譯成果,而不是將它們都丟棄。被編譯的包會被保存到$GOPATH/pkg目録下目録路徑和 src目録路徑對應可執行程序被保存到$GOPATH/bin目録。很多用戶會將$GOPATH/bin添加到可執行程序的蒐索列表中。還有`go install`命令和`go build`命令都不會重新編譯沒有發生變化的包,這可以使後續構建更快捷。爲了方便編譯依賴的包,`go build -i`命令將安裝每個目標所依賴的包。
`go install`命令和`go build`命令很相似,但是它会保存每个包的编译成果,而不是将它们都丢弃。被编译的包会被保存到$GOPATH/pkg目录下目录路径和 src目录路径对应可执行程序被保存到$GOPATH/bin目录。很多用户会将$GOPATH/bin添加到可执行程序的搜索列表中。还有`go install`命令和`go build`命令都不会重新编译没有发生变化的包,这可以使后续构建更快捷。为了方便编译依赖的包,`go build -i`命令将安装每个目标所依赖的包。
爲編譯對應不同的操作繫統平台和CPU架構`go install`命令會將編譯結果安裝到GOOS和GOARCH對應的目録。例如在Mac繫統golang.org/x/net/html包將被安裝到$GOPATH/pkg/darwin_amd64目録下的golang.org/x/net/html.a文件。
为编译对应不同的操作系统平台和CPU架构`go install`命令会将编译结果安装到GOOS和GOARCH对应的目录。例如在Mac系统golang.org/x/net/html包将被安装到$GOPATH/pkg/darwin_amd64目录下的golang.org/x/net/html.a文件。
針對不同操作繫統或CPU的交叉構建也是很簡單的。隻需要設置好目標對應的GOOS和GOARCH然後運行構建命令卽可。下面交叉編譯的程序將輸出它在編譯時操作繫統和CPU類型:
针对不同操作系统或CPU的交叉构建也是很简单的。只需要设置好目标对应的GOOS和GOARCH然后运行构建命令即可。下面交叉编译的程序将输出它在编译时操作系统和CPU类型:
<u><i>gopl.io/ch10/cross</i></u>
```Go
@ -76,7 +76,7 @@ func main() {
}
```
下面以64位和32位環境分别執行程序:
下面以64位和32位环境分别执行程序:
```
$ go build gopl.io/ch10/cross
@ -87,19 +87,19 @@ $ ./cross
darwin 386
```
有些包可能需要針對不同平台和處理器類型使用不同版本的代碼文件以便於處理底層的可移植性問題或提供爲一些特定代碼提供優化。如果一個文件名包含了一個操作繫統或處理器類型名字例如net_linux.go或asm_amd64.sGo語言的構建工具將隻在對應的平台編譯這些文件。還有一個特别的構建註釋註釋可以提供更多的構建過程控製。例如文件中可能包含下面的註釋
有些包可能需要针对不同平台和处理器类型使用不同版本的代码文件以便于处理底层的可移植性问题或提供为一些特定代码提供优化。如果一个文件名包含了一个操作系统或处理器类型名字例如net_linux.go或asm_amd64.sGo语言的构建工具将只在对应的平台编译这些文件。还有一个特别的构建注释注释可以提供更多的构建过程控制。例如文件中可能包含下面的注释
```Go
// +build linux darwin
```
在包聲明和包註釋的前面,該構建註釋參數告訴`go build`隻在編譯程序對應的目標操作繫統是Linux或Mac OS X時才編譯這個文件。下面的構建註釋則表示不編譯這個文件:
在包声明和包注释的前面,该构建注释参数告诉`go build`只在编译程序对应的目标操作系统是Linux或Mac OS X时才编译这个文件。下面的构建注释则表示不编译这个文件:
```Go
// +build ignore
```
更多細節可以參考go/build包的構建約束部分的文檔
更多细节可以参考go/build包的构建约束部分的文档
```
$ go doc go/build

View File

@ -1,8 +1,8 @@
### 10.7.4. 包文
### 10.7.4. 包文
Go語言的編碼風格鼓勵爲每個包提供良好的文檔。包中每個導出的成員和包聲明前都應該包含目的和用法説明的註釋
Go语言的编码风格鼓励为每个包提供良好的文档。包中每个导出的成员和包声明前都应该包含目的和用法说明的注释
Go語言中包文檔註釋一般是完整的句子第一行是包的摘要説明註釋後僅跟着包聲明語句。註釋中函數的參數或其它的標識符併不需要額外的引號或其它標記註明。例如下面是fmt.Fprintf的文檔註釋
Go语言中包文档注释一般是完整的句子第一行是包的摘要说明注释后仅跟着包声明语句。注释中函数的参数或其它的标识符并不需要额外的引号或其它标记注明。例如下面是fmt.Fprintf的文档注释
```Go
// Fprintf formats according to a format specifier and writes to w.
@ -10,13 +10,13 @@ Go語言中包文檔註釋一般是完整的句子第一行是包的摘要説
func Fprintf(w io.Writer, format string, a ...interface{}) (int, error)
```
Fprintf函數格式化的細節在fmt包文檔中描述。如果註釋後僅跟着包聲明語句那註釋對應整個包的文檔。包文檔對應的註釋隻能有一個譯註其實可以有多個它們會組合成一個包文檔註釋包註釋可以出現在任何一個源文件中。如果包的註釋內容比較長一般會放到一個獨立的源文件中fmt包註釋就有300行之多。這個專門用於保存包文檔的源文件通常叫doc.go。
Fprintf函数格式化的细节在fmt包文档中描述。如果注释后仅跟着包声明语句那注释对应整个包的文档。包文档对应的注释只能有一个译注其实可以有多个它们会组合成一个包文档注释包注释可以出现在任何一个源文件中。如果包的注释内容比较长一般会放到一个独立的源文件中fmt包注释就有300行之多。这个专门用于保存包文档的源文件通常叫doc.go。
好的文檔併不需要面面俱到文檔本身應該是簡潔但可不忽略的。事實上Go語言的風格更喜歡簡潔的文檔併且文檔也是需要像代碼一樣維護的。對於一組聲明語句可以用一個精鍊的句子描述如果是顯而易見的功能則併不需要註釋
好的文档并不需要面面俱到文档本身应该是简洁但可不忽略的。事实上Go语言的风格更喜欢简洁的文档并且文档也是需要像代码一样维护的。对于一组声明语句可以用一个精炼的句子描述如果是显而易见的功能则并不需要注释
在本書中,隻要空間允許,我們之前很多包聲明都包含了註釋文檔,但你可以從標準庫中發現很多更好的例子。有兩個工具可以幫到你。
在本书中,只要空间允许,我们之前很多包声明都包含了注释文档,但你可以从标准库中发现很多更好的例子。有两个工具可以帮到你。
首先是`go doc`命令,該命令打印包的聲明和每個成員的文檔註釋,下面是整個包的文檔
首先是`go doc`命令,该命令打印包的声明和每个成员的文档注释,下面是整个包的文档
```
$ go doc time
@ -34,7 +34,7 @@ type Time struct { ... }
...many more...
```
或者是某個具體包成員的註釋文檔
或者是某个具体包成员的注释文档
```
$ go doc time.Since
@ -44,7 +44,7 @@ func Since(t Time) Duration
It is shorthand for time.Now().Sub(t).
```
或者是某個具體包的一個方法的註釋文檔
或者是某个具体包的一个方法的注释文档
```
$ go doc time.Duration.Seconds
@ -53,7 +53,7 @@ func (d Duration) Seconds() float64
Seconds returns the duration as a floating-point number of seconds.
```
該命令併不需要輸入完整的包導入路徑或正確的大小寫。下面的命令將打印encoding/json包的`(*json.Decoder).Decode`方法的文檔
该命令并不需要输入完整的包导入路径或正确的大小写。下面的命令将打印encoding/json包的`(*json.Decoder).Decode`方法的文档
```
$ go doc json.decode
@ -63,12 +63,12 @@ func (dec *Decoder) Decode(v interface{}) error
it in the value pointed to by v.
```
第二個工具名字也叫godoc它提供可以相互交叉引用的HTML頁面但是包含和`go doc`命令相同以及更多的信息。10.1節演示了time包的文檔11.6節將看到godoc演示可以交互的示例程序。godoc的在線服務 https://godoc.org ,包含了成韆上萬的開源包的檢索工具。
第二个工具名字也叫godoc它提供可以相互交叉引用的HTML页面但是包含和`go doc`命令相同以及更多的信息。10.1节演示了time包的文档11.6节将看到godoc演示可以交互的示例程序。godoc的在线服务 https://godoc.org ,包含了成千上万的开源包的检索工具。
你也可以在自己的工作區目録運行godoc服務。運行下面的命令然後在瀏覽器査看 http://localhost:8000/pkg 頁面:
你也可以在自己的工作区目录运行godoc服务。运行下面的命令然后在浏览器查看 http://localhost:8000/pkg 页面:
```
$ godoc -http :8000
```
其中`-analysis=type`和`-analysis=pointer`命令行標誌參數用於打開文檔和代碼中關於靜態分析的結果。
其中`-analysis=type`和`-analysis=pointer`命令行标志参数用于打开文档和代码中关于静态分析的结果。

View File

@ -1,12 +1,12 @@
### 10.7.5. 部包
### 10.7.5. 部包
在Go語音程序中,包的封裝機製是一個重要的特性。沒有導出的標識符隻在同一個包內部可以訪問,而導出的標識符則是面向全宇宙都是可見的。
在Go语音程序中,包的封装机制是一个重要的特性。没有导出的标识符只在同一个包内部可以访问,而导出的标识符则是面向全宇宙都是可见的。
時候,一個中間的狀態可能也是有用的,對於一小部分信任的包是可見的,但併不是對所有調用者都可見。例如,當我們計劃將一個大的包拆分爲很多小的更容易維護的子包,但是我們併不想將內部的子包結構也完全暴露出去。同時,我們可能還希望在內部子包之間共享一些通用的處理包,或者我們隻是想實驗一個新包的還併不穩定的接口,暫時隻暴露給一些受限製的用戶使用。
时候,一个中间的状态可能也是有用的,对于一小部分信任的包是可见的,但并不是对所有调用者都可见。例如,当我们计划将一个大的包拆分为很多小的更容易维护的子包,但是我们并不想将内部的子包结构也完全暴露出去。同时,我们可能还希望在内部子包之间共享一些通用的处理包,或者我们只是想实验一个新包的还并不稳定的接口,暂时只暴露给一些受限制的用户使用。
![](../images/ch10-01.png)
爲了滿足這些需求Go語言的構建工具對包含internal名字的路徑段的包導入路徑做了特殊處理。這種包叫internal包一個internal包隻能被和internal目録有同一個父目録的包所導入。例如net/http/internal/chunked內部包隻能被net/http/httputil或net/http包導入但是不能被net/url包導入。不過net/url包卻可以導入net/http/httputil包。
为了满足这些需求Go语言的构建工具对包含internal名字的路径段的包导入路径做了特殊处理。这种包叫internal包一个internal包只能被和internal目录有同一个父目录的包所导入。例如net/http/internal/chunked内部包只能被net/http/httputil或net/http包导入但是不能被net/url包导入。不过net/url包却可以导入net/http/httputil包。
```
net/http

View File

@ -1,13 +1,13 @@
### 10.7.6. 査詢
### 10.7.6. 查询
`go list`命令可以査詢可用包的信息。其最簡單的形式,可以測試包是否在工作區併打印它的導入路徑
`go list`命令可以查询可用包的信息。其最简单的形式,可以测试包是否在工作区并打印它的导入路径
```
$ go list github.com/go-sql-driver/mysql
github.com/go-sql-driver/mysql
```
`go list`命令的參數還可以用`"..."`表示匹配任意的包的導入路徑。我們可以用它來列表工作區中的所有包:
`go list`命令的参数还可以用`"..."`表示匹配任意的包的导入路径。我们可以用它来列表工作区中的所有包:
```
$ go list ...
@ -20,7 +20,7 @@ cmd/api
...many more...
```
或者是特定子目下的所有包:
或者是特定子目下的所有包:
```
$ go list gopl.io/ch3/...
@ -33,7 +33,7 @@ gopl.io/ch3/printints
gopl.io/ch3/surface
```
或者是和某個主題相關的所有包:
或者是和某个主题相关的所有包:
```
$ go list ...xml...
@ -41,7 +41,7 @@ encoding/xml
gopl.io/ch7/xmlselect
```
`go list`命令還可以獲取每個包完整的元信息,而不僅僅隻是導入路徑,這些元信息可以以不同格式提供給用戶。其中`-json`命令行參數表示用JSON格式打印每個包的元信息。
`go list`命令还可以获取每个包完整的元信息,而不仅仅只是导入路径,这些元信息可以以不同格式提供给用户。其中`-json`命令行参数表示用JSON格式打印每个包的元信息。
```
$ go list -json hash
@ -71,7 +71,7 @@ $ go list -json hash
}
```
命令行參數`-f`則允許用戶使用text/template包§4.6的模闆語言定義輸出文本的格式。下面的命令將打印strconv包的依賴的包然後用join模闆函數將結果鏈接爲一行連接時每個結果之間用一個空格分隔:
命令行参数`-f`则允许用户使用text/template包§4.6的模板语言定义输出文本的格式。下面的命令将打印strconv包的依赖的包然后用join模板函数将结果链接为一行连接时每个结果之间用一个空格分隔:
{% raw %}
```
@ -80,7 +80,7 @@ errors math runtime unicode/utf8 unsafe
```
{% endraw %}
譯註上面的命令在Windows的命令行運行會遇到`template: main:1: unclosed action`的錯誤。産生這個錯誤的原因是因爲命令行對命令中的`" "`參數進行了轉義處理。可以按照下面的方法解決轉義字符串的問題
译注上面的命令在Windows的命令行运行会遇到`template: main:1: unclosed action`的错误。产生这个错误的原因是因为命令行对命令中的`" "`参数进行了转义处理。可以按照下面的方法解决转义字符串的问题
{% raw %}
```
@ -88,7 +88,7 @@ $ go list -f "{{join .Deps \" \"}}" strconv
```
{% endraw %}
下面的命令打印compress子目録下所有包的依賴包列表:
下面的命令打印compress子目录下所有包的依赖包列表:
{% raw %}
```
@ -101,7 +101,7 @@ compress/zlib -> bufio compress/flate errors fmt hash hash/adler32 io
```
{% endraw %}
譯註Windows下有同樣有問題要避免轉義字符串的榦擾
译注Windows下有同样有问题要避免转义字符串的干扰
{% raw %}
```
@ -109,8 +109,8 @@ $ go list -f "{{.ImportPath}} -> {{join .Imports \" \"}}" compress/...
```
{% endraw %}
`go list`命令對於一次性的交互式査詢或自動化構建或測試腳本都很有幫助。我們將在11.2.4節中再次使用它。每個子命令的更多信息,包括可設置的字段和意義,可以用`go help list`命令査看。
`go list`命令对于一次性的交互式查询或自动化构建或测试脚本都很有帮助。我们将在11.2.4节中再次使用它。每个子命令的更多信息,包括可设置的字段和意义,可以用`go help list`命令查看。
在本章,我們解釋了Go語言工具中除了測試命令之外的所有重要的子命令。在下一章我們將看到如何用`go test`命令去運行Go語言程序中的測試代碼
在本章,我们解释了Go语言工具中除了测试命令之外的所有重要的子命令。在下一章我们将看到如何用`go test`命令去运行Go语言程序中的测试代码
**練習 10.4** 創建一個工具,根據命令行指定的參數,報告工作區所有依賴指定包的其它包集合。提示:你需要運行`go list`命令兩次一次用於初始化包一次用於所有包。你可能需要用encoding/json§4.5)包來分析輸出的JSON格式的信息。
**练习 10.4** 创建一个工具,根据命令行指定的参数,报告工作区所有依赖指定包的其它包集合。提示:你需要运行`go list`命令两次一次用于初始化包一次用于所有包。你可能需要用encoding/json§4.5)包来分析输出的JSON格式的信息。

View File

@ -1,10 +1,10 @@
## 10.7. 工具
本章剩下的部分將討論Go語言工具箱的具體功能包括如何下載、格式化、構建、測試和安裝Go語言編寫的程序。
本章剩下的部分将讨论Go语言工具箱的具体功能包括如何下载、格式化、构建、测试和安装Go语言编写的程序。
Go語言的工具箱集合了一繫列的功能的命令集。它可以看作是一個包管理器類似於Linux中的apt和rpm工具用於包的査詢、計算的包依賴關繫、從遠程版本控製繫統和下載它們等任務。它也是一個構建繫統計算文件的依賴關繫然後調用編譯器、滙編器和連接器構建程序雖然它故意被設計成沒有標準的make命令那麽複雜。它也是一個單元測試和基準測試的驅動程序我們將在第11章討論測試話題
Go语言的工具箱集合了一系列的功能的命令集。它可以看作是一个包管理器类似于Linux中的apt和rpm工具用于包的查询、计算的包依赖关系、从远程版本控制系统和下载它们等任务。它也是一个构建系统计算文件的依赖关系然后调用编译器、汇编器和连接器构建程序虽然它故意被设计成没有标准的make命令那么复杂。它也是一个单元测试和基准测试的驱动程序我们将在第11章讨论测试话题
Go語言工具箱的命令有着類似“瑞士軍刀”的風格帶着一打子的子命令有一些我們經常用到例如get、run、build和fmt等。你可以運行go或go help命令査看內置的幫助文檔爲了査詢方便我們列出了最常用的命令:
Go语言工具箱的命令有着类似“瑞士军刀”的风格带着一打子的子命令有一些我们经常用到例如get、run、build和fmt等。你可以运行go或go help命令查看内置的帮助文档为了查询方便我们列出了最常用的命令:
```
$ go
@ -26,7 +26,7 @@ Use "go help [command]" for more information about a command.
...
```
爲了達到零配置的設計目標Go語言的工具箱很多地方都依賴各種約定。例如根據給定的源文件的名稱Go語言的工具可以找到源文件對應的包因爲每個目録隻包含了單一的包併且到的導入路徑和工作區的目録結構是對應的。給定一個包的導入路徑Go語言的工具可以找到對應的目録中沒個實體對應的源文件。它還可以根據導入路徑找到存儲代碼倉庫的遠程服務器的URL。
为了达到零配置的设计目标Go语言的工具箱很多地方都依赖各种约定。例如根据给定的源文件的名称Go语言的工具可以找到源文件对应的包因为每个目录只包含了单一的包并且到的导入路径和工作区的目录结构是对应的。给定一个包的导入路径Go语言的工具可以找到对应的目录中没个实体对应的源文件。它还可以根据导入路径找到存储代码仓库的远程服务器的URL。
{% include "./ch10-07-1.md" %}

View File

@ -1,7 +1,7 @@
# 第十章 包和工具
現在隨便一個小程序的實現都可能包含超過10000個函數。然而作者一般隻需要考慮其中很小的一部分和做很少的設計因爲絶大部分代碼都是由他人編寫的它們通過類似包或模塊的方式被重用。
现在随便一个小程序的实现都可能包含超过10000个函数。然而作者一般只需要考虑其中很小的一部分和做很少的设计因为绝大部分代码都是由他人编写的它们通过类似包或模块的方式被重用。
Go語言有超過100個的標準包譯註可以用`go list std | wc -l`命令査看標準包的具體數目標準庫爲大多數的程序提供了必要的基礎構件。在Go的社區有很多成熟的包被設計、共享、重用和改進目前互聯網上已經發布了非常多的Go語音開源包它們可以通過 http://godoc.org 檢索。在本章,我們將演示如果使用已有的包和創建新的包。
Go语言有超过100个的标准包译注可以用`go list std | wc -l`命令查看标准包的具体数目标准库为大多数的程序提供了必要的基础构件。在Go的社区有很多成熟的包被设计、共享、重用和改进目前互联网上已经发布了非常多的Go语音开源包它们可以通过 http://godoc.org 检索。在本章,我们将演示如果使用已有的包和创建新的包。
Go還自帶了工具箱,里面有很多用來簡化工作區和包管理的小工具。在本書開始的時候,我們已經見識過如何使用工具箱自帶的工具來下載、構件和運行我們的演示程序了。在本章,我們將看看這些工具的基本設計理論和嚐試更多的功能,例如打印工作區中包的文檔和査詢相關的元數據等。在下一章,我們將探討探索包的單元測試用法。
Go还自带了工具箱,里面有很多用来简化工作区和包管理的小工具。在本书开始的时候,我们已经见识过如何使用工具箱自带的工具来下载、构件和运行我们的演示程序了。在本章,我们将看看这些工具的基本设计理论和尝试更多的功能,例如打印工作区中包的文档和查询相关的元数据等。在下一章,我们将探讨探索包的单元测试用法。

View File

@ -1,7 +1,7 @@
## 11.1. go test
go test命令是一個按照一定的約定和組織的測試代碼的驅動程序。在包目録內所有以_test.go爲後綴名的源文件併不是go build構建包的一部分它們是go test測試的一部分。
go test命令是一个按照一定的约定和组织的测试代码的驱动程序。在包目录内所有以_test.go为后缀名的源文件并不是go build构建包的一部分它们是go test测试的一部分。
在\*_test.go文件中有三種類型的函數測試函數、基準測試函數、示例函數。一個測試函數是以Test爲函數名前綴的函數用於測試程序的一些邏輯行爲是否正確go test命令會調用這些測試函數併報告測試結果是PASS或FAIL。基準測試函數是以Benchmark爲函數名前綴的函數它們用於衡量一些函數的性能go test命令會多次運行基準函數以計算一個平均的執行時間。示例函數是以Example爲函數名前綴的函數提供一個由編譯器保證正確性的示例文檔。我們將在11.2節討論測試函數的所有細節病在11.4節討論基準測試函數的細節然後在11.6節討論示例函數的細節
在\*_test.go文件中有三种类型的函数测试函数、基准测试函数、示例函数。一个测试函数是以Test为函数名前缀的函数用于测试程序的一些逻辑行为是否正确go test命令会调用这些测试函数并报告测试结果是PASS或FAIL。基准测试函数是以Benchmark为函数名前缀的函数它们用于衡量一些函数的性能go test命令会多次运行基准函数以计算一个平均的执行时间。示例函数是以Example为函数名前缀的函数提供一个由编译器保证正确性的示例文档。我们将在11.2节讨论测试函数的所有细节病在11.4节讨论基准测试函数的细节然后在11.6节讨论示例函数的细节
go test命令會遍歷所有的\*_test.go文件中符合上述命名規則的函數然後生成一個臨時的main包用於調用相應的測試函數然後構建併運行、報告測試結果最後清理測試中生成的臨時文件。
go test命令会遍历所有的\*_test.go文件中符合上述命名规则的函数然后生成一个临时的main包用于调用相应的测试函数然后构建并运行、报告测试结果最后清理测试中生成的临时文件。

View File

@ -1,10 +1,10 @@
### 11.2.1. 隨機測試
### 11.2.1. 随机测试
表格驅動的測試便於構造基於精心挑選的測試數據的測試用例。另一種測試思路是隨機測試,也就是通過構造更廣泛的隨機輸入來測試探索函數的行爲
表格驱动的测试便于构造基于精心挑选的测试数据的测试用例。另一种测试思路是随机测试,也就是通过构造更广泛的随机输入来测试探索函数的行为
麽對於一個隨機的輸入,我們如何能知道希望的輸出結果呢?這里有兩種處理策略。第一個是編寫另一個對照函數,使用簡單和清晰的算法,雖然效率較低但是行爲和要測試的函數是一致的,然後針對相同的隨機輸入檢査兩者的輸出結果。第二種是生成的隨機輸入的數據遵循特定的模式,這樣我們就可以知道期望的輸出的模式。
么对于一个随机的输入,我们如何能知道希望的输出结果呢?这里有两种处理策略。第一个是编写另一个对照函数,使用简单和清晰的算法,虽然效率较低但是行为和要测试的函数是一致的,然后针对相同的随机输入检查两者的输出结果。第二种是生成的随机输入的数据遵循特定的模式,这样我们就可以知道期望的输出的模式。
下面的例子使用的是第二種方法randomPalindrome函數用於隨機生成迴文字符串。
下面的例子使用的是第二种方法randomPalindrome函数用于随机生成回文字符串。
```Go
import "math/rand"
@ -37,13 +37,13 @@ func TestRandomPalindromes(t *testing.T) {
}
```
雖然隨機測試會有不確定因素但是它也是至關重要的我們可以從失敗測試的日誌獲取足夠的信息。在我們的例子中輸入IsPalindrome的p參數將告訴我們眞實的數據但是對於函數將接受更複雜的輸入不需要保存所有的輸入隻要日誌中簡單地記録隨機數種子卽可像上面的方式。有了這些隨機數初始化種子我們可以很容易脩改測試代碼以重現失敗的隨機測試
虽然随机测试会有不确定因素但是它也是至关重要的我们可以从失败测试的日志获取足够的信息。在我们的例子中输入IsPalindrome的p参数将告诉我们真实的数据但是对于函数将接受更复杂的输入不需要保存所有的输入只要日志中简单地记录随机数种子即可像上面的方式。有了这些随机数初始化种子我们可以很容易修改测试代码以重现失败的随机测试
過使用當前時間作爲隨機種子,在整個過程中的每次運行測試命令時都將探索新的隨機數據。如果你使用的是定期運行的自動化測試集成繫統,隨機測試將特别有價值。
过使用当前时间作为随机种子,在整个过程中的每次运行测试命令时都将探索新的随机数据。如果你使用的是定期运行的自动化测试集成系统,随机测试将特别有价值。
**練習 11.3:** TestRandomPalindromes測試函數隻測試了迴文字符串。編寫新的隨機測試生成器用於測試隨機生成的非迴文字符串。
**练习 11.3:** TestRandomPalindromes测试函数只测试了回文字符串。编写新的随机测试生成器用于测试随机生成的非回文字符串。
**練習 11.4:** 脩改randomPalindrome函數以探索IsPalindrome是否對標點和空格做了正確處理。
**练习 11.4:** 修改randomPalindrome函数以探索IsPalindrome是否对标点和空格做了正确处理。

View File

@ -1,8 +1,8 @@
### 11.2.2. 測試一個命令
### 11.2.2. 测试一个命令
對於測試包`go test`是一個的有用的工具,但是稍加努力我們也可以用它來測試可執行程序。如果一個包的名字是 main那麽在構建時會生成一個可執行程序不過main包可以作爲一個包被測試器代碼導入。
对于测试包`go test`是一个的有用的工具,但是稍加努力我们也可以用它来测试可执行程序。如果一个包的名字是 main那么在构建时会生成一个可执行程序不过main包可以作为一个包被测试器代码导入。
讓我們爲2.3.2節的echo程序編寫一個測試。我們先將程序拆分爲兩個函數echo函數完成眞正的工作main函數用於處理命令行輸入參數和echo可能返迴的錯誤
让我们为2.3.2节的echo程序编写一个测试。我们先将程序拆分为两个函数echo函数完成真正的工作main函数用于处理命令行输入参数和echo可能返回的错误
<u><i>gopl.io/ch11/echo</i></u>
```Go
@ -41,7 +41,7 @@ func echo(newline bool, sep string, args []string) error {
}
```
測試中我們可以用各種參數和標標誌調用echo函數然後檢測它的輸出是否正確, 我們通過增加參數來減少echo函數對全局變量的依賴。我們還增加了一個全局名爲out的變量來替代直接使用os.Stdout這樣測試代碼可以根據需要將out脩改爲不同的對象以便於檢査。下面就是echo_test.go文件中的測試代碼
测试中我们可以用各种参数和标标志调用echo函数然后检测它的输出是否正确, 我们通过增加参数来减少echo函数对全局变量的依赖。我们还增加了一个全局名为out的变量来替代直接使用os.Stdout这样测试代码可以根据需要将out修改为不同的对象以便于检查。下面就是echo_test.go文件中的测试代码
```Go
package main
@ -82,15 +82,15 @@ func TestEcho(t *testing.T) {
}
```
註意的是測試代碼和産品代碼在同一個包。雖然是main包也有對應的main入口函數但是在測試的時候main包隻是TestEcho測試函數導入的一個普通包里面main函數併沒有被導出,而是被忽略的。
注意的是测试代码和产品代码在同一个包。虽然是main包也有对应的main入口函数但是在测试的时候main包只是TestEcho测试函数导入的一个普通包里面main函数并没有被导出,而是被忽略的。
過將測試放到表格中,我們很容易添加新的測試用例。讓我通過增加下面的測試用例來看看失敗的情況是怎麽樣的:
过将测试放到表格中,我们很容易添加新的测试用例。让我通过增加下面的测试用例来看看失败的情况是怎么样的:
```Go
{true, ",", []string{"a", "b", "c"}, "a b c\n"}, // NOTE: wrong expectation!
```
`go test`出如下:
`go test`出如下:
```
$ go test gopl.io/ch11/echo
@ -100,6 +100,6 @@ FAIL
FAIL gopl.io/ch11/echo 0.006s
```
錯誤信息描述了嚐試的操作使用Go類似語法實際的結果和期望的結果。通過這樣的錯誤信息你可以在檢視代碼之前就很容易定位錯誤的原因。
错误信息描述了尝试的操作使用Go类似语法实际的结果和期望的结果。通过这样的错误信息你可以在检视代码之前就很容易定位错误的原因。
註意的是在測試代碼中併沒有調用log.Fatal或os.Exit因爲調用這類函數會導致程序提前退出調用這些函數的特權應該放在main函數中。如果眞的有意外的事情導致函數發生panic異常測試驅動應該嚐試用recover捕獲異常然後將當前測試當作失敗處理。如果是可預期的錯誤例如非法的用戶輸入、找不到文件或配置文件不當等應該通過返迴一個非空的error的方式處理。幸運的是上面的意外隻是一個插麴我們的echo示例是比較簡單的也沒有需要返迴非空error的情況
注意的是在测试代码中并没有调用log.Fatal或os.Exit因为调用这类函数会导致程序提前退出调用这些函数的特权应该放在main函数中。如果真的有意外的事情导致函数发生panic异常测试驱动应该尝试用recover捕获异常然后将当前测试当作失败处理。如果是可预期的错误例如非法的用户输入、找不到文件或配置文件不当等应该通过返回一个非空的error的方式处理。幸运的是上面的意外只是一个插曲我们的echo示例是比较简单的也没有需要返回非空error的情况

View File

@ -1,14 +1,14 @@
### 11.2.3. 白盒測試
### 11.2.3. 白盒测试
種測試分類的方法是基於測試者是否需要了解被測試對象的內部工作原理。黑盒測試隻需要測試包公開的文檔和API行爲內部實現對測試代碼是透明的。相反白盒測試有訪問包內部函數和數據結構的權限因此可以做到一下普通客戶端無法實現的測試。例如一個白盒測試可以在每個操作之後檢測不變量的數據類型。白盒測試隻是一個傳統的名稱其實稱爲clear box測試會更準確。)
种测试分类的方法是基于测试者是否需要了解被测试对象的内部工作原理。黑盒测试只需要测试包公开的文档和API行为内部实现对测试代码是透明的。相反白盒测试有访问包内部函数和数据结构的权限因此可以做到一下普通客户端无法实现的测试。例如一个白盒测试可以在每个操作之后检测不变量的数据类型。白盒测试只是一个传统的名称其实称为clear box测试会更准确。)
黑盒和白盒這兩種測試方法是互補的。黑盒測試一般更健壯隨着軟件實現的完善測試代碼很少需要更新。它們可以幫助測試者了解眞是客戶的需求也可以幫助發現API設計的一些不足之處。相反白盒測試則可以對內部一些棘手的實現提供更多的測試覆蓋
黑盒和白盒这两种测试方法是互补的。黑盒测试一般更健壮随着软件实现的完善测试代码很少需要更新。它们可以帮助测试者了解真是客户的需求也可以帮助发现API设计的一些不足之处。相反白盒测试则可以对内部一些棘手的实现提供更多的测试覆盖
們已經看到兩種測試的例子。TestIsPalindrome測試僅僅使用導出的IsPalindrome函數因此這是一個黑盒測試。TestEcho測試則調用了內部的echo函數併且更新了內部的out包級變量這兩個都是未導出的因此這是白盒測試
们已经看到两种测试的例子。TestIsPalindrome测试仅仅使用导出的IsPalindrome函数因此这是一个黑盒测试。TestEcho测试则调用了内部的echo函数并且更新了内部的out包级变量这两个都是未导出的因此这是白盒测试
當我們準備TestEcho測試的時候我們脩改了echo函數使用包級的out變量作爲輸出對象因此測試代碼可以用另一個實現代替標準輸出這樣可以方便對比echo輸出的數據。使用類似的技術我們可以將産品代碼的其他部分也替換爲一個容易測試的僞對象。使用僞對象的好處是我們可以方便配置容易預測更可靠也更容易觀察。同時也可以避免一些不良的副作用例如更新生産數據庫或信用卡消費行爲
当我们准备TestEcho测试的时候我们修改了echo函数使用包级的out变量作为输出对象因此测试代码可以用另一个实现代替标准输出这样可以方便对比echo输出的数据。使用类似的技术我们可以将产品代码的其他部分也替换为一个容易测试的伪对象。使用伪对象的好处是我们可以方便配置容易预测更可靠也更容易观察。同时也可以避免一些不良的副作用例如更新生产数据库或信用卡消费行为
下面的代碼演示了爲用戶提供網絡存儲的web服務中的配額檢測邏輯。當用戶使用了超過90%的存儲配額之後將發送提醒郵件。
下面的代码演示了为用户提供网络存储的web服务中的配额检测逻辑。当用户使用了超过90%的存储配额之后将发送提醒邮件。
<u><i>gopl.io/ch11/storage1</i></u>
```Go
@ -48,7 +48,7 @@ func CheckQuota(username string) {
}
```
們想測試這個代碼但是我們併不希望發送眞實的郵件。因此我們將郵件處理邏輯放到一個私有的notifyUser函數中。
们想测试这个代码但是我们并不希望发送真实的邮件。因此我们将邮件处理逻辑放到一个私有的notifyUser函数中。
<u><i>gopl.io/ch11/storage2</i></u>
```Go
@ -73,7 +73,7 @@ func CheckQuota(username string) {
}
```
現在我們可以在測試中用僞郵件發送函數替代眞實的郵件發送函數。它隻是簡單記録要通知的用戶和郵件的內容。
现在我们可以在测试中用伪邮件发送函数替代真实的邮件发送函数。它只是简单记录要通知的用户和邮件的内容。
```Go
package storage
@ -107,7 +107,7 @@ func TestCheckQuotaNotifiesUser(t *testing.T) {
}
```
這里有一個問題當測試函數返迴後CheckQuota將不能正常工作因爲notifyUsers依然使用的是測試函數的僞發送郵件函數當更新全局對象的時候總會有這種風險。 我們必須脩改測試代碼恢複notifyUsers原先的狀態以便後續其他的測試沒有影響要確保所有的執行路徑後都能恢複包括測試失敗或panic異常的情形。在這種情況下我們建議使用defer語句來延後執行處理恢複的代碼
这里有一个问题当测试函数返回后CheckQuota将不能正常工作因为notifyUsers依然使用的是测试函数的伪发送邮件函数当更新全局对象的时候总会有这种风险。 我们必须修改测试代码恢复notifyUsers原先的状态以便后续其他的测试没有影响要确保所有的执行路径后都能恢复包括测试失败或panic异常的情形。在这种情况下我们建议使用defer语句来延后执行处理恢复的代码
```Go
func TestCheckQuotaNotifiesUser(t *testing.T) {
@ -124,6 +124,6 @@ func TestCheckQuotaNotifiesUser(t *testing.T) {
}
```
這種處理模式可以用來暫時保存和恢複所有的全局變量,包括命令行標誌參數、調試選項和優化參數;安裝和移除導致生産代碼産生一些調試信息的鉤子函數;還有有些誘導生産代碼進入某些重要狀態的改變,比如超時、錯誤,甚至是一些刻意製造的併發行爲等因素。
这种处理模式可以用来暂时保存和恢复所有的全局变量,包括命令行标志参数、调试选项和优化参数;安装和移除导致生产代码产生一些调试信息的钩子函数;还有有些诱导生产代码进入某些重要状态的改变,比如超时、错误,甚至是一些刻意制造的并发行为等因素。
這種方式使用全局變量是安全的因爲go test命令併不會同時併發地執行多個測試
这种方式使用全局变量是安全的因为go test命令并不会同时并发地执行多个测试

View File

@ -1,20 +1,20 @@
### 11.2.4. 擴展測試
### 11.2.4. 扩展测试
慮下這兩個包net/url包提供了URL解析的功能net/http包提供了web服務和HTTP客戶端的功能。如我們所料上層的net/http包依賴下層的net/url包。然後net/url包中的一個測試是演示不同URL和HTTP客戶端的交互行爲。也就是説一個下層包的測試代碼導入了上層的包。
虑下这两个包net/url包提供了URL解析的功能net/http包提供了web服务和HTTP客户端的功能。如我们所料上层的net/http包依赖下层的net/url包。然后net/url包中的一个测试是演示不同URL和HTTP客户端的交互行为。也就是说一个下层包的测试代码导入了上层的包。
![](../images/ch11-01.png)
這樣的行爲在net/url包的測試代碼中會導致包的循環依賴正如圖11.1中向上箭頭所示同時正如我們在10.1節所講的Go語言規范是禁止包的循環依賴的。
这样的行为在net/url包的测试代码中会导致包的循环依赖正如图11.1中向上箭头所示同时正如我们在10.1节所讲的Go语言规范是禁止包的循环依赖的。
過我們可以通過測試擴展包的方式解決循環依賴的問題也就是在net/url包所在的目録聲明一個獨立的url_test測試擴展包。其中測試擴展包名的`_test`後綴告訴go test工具它應該建立一個額外的包來運行測試。我們將這個擴展測試包的導入路徑視作是net/url_test會更容易理解但實際上它併不能被其他任何包導入。
过我们可以通过测试扩展包的方式解决循环依赖的问题也就是在net/url包所在的目录声明一个独立的url_test测试扩展包。其中测试扩展包名的`_test`后缀告诉go test工具它应该建立一个额外的包来运行测试。我们将这个扩展测试包的导入路径视作是net/url_test会更容易理解但实际上它并不能被其他任何包导入。
爲測試擴展包是一個獨立的包,所以可以導入測試代碼依賴的其他的輔助包;包內的測試代碼可能無法做到。在設計層面,測試擴展包是在所以它依賴的包的上層,正如圖11.2所示。
为测试扩展包是一个独立的包,所以可以导入测试代码依赖的其他的辅助包;包内的测试代码可能无法做到。在设计层面,测试扩展包是在所以它依赖的包的上层,正如图11.2所示。
![](../images/ch11-02.png)
過迴避循環導入依賴,擴展測試包可以更靈活的編寫測試,特别是集成測試(需要測試多個組件之間的交互),可以像普通應用程序那樣自由地導入其他包。
过回避循环导入依赖,扩展测试包可以更灵活的编写测试,特别是集成测试(需要测试多个组件之间的交互),可以像普通应用程序那样自由地导入其他包。
們可以用go list命令査看包對應目録中哪些Go源文件是産品代碼哪些是包內測試還哪些測試擴展包。我們以fmt包作爲一個例子GoFiles表示産品代碼對應的Go源文件列表也就是go build命令要編譯的部分。
们可以用go list命令查看包对应目录中哪些Go源文件是产品代码哪些是包内测试还哪些测试扩展包。我们以fmt包作为一个例子GoFiles表示产品代码对应的Go源文件列表也就是go build命令要编译的部分。
{% raw %}
@ -25,7 +25,7 @@ $ go list -f={{.GoFiles}} fmt
{% endraw %}
TestGoFiles表示的是fmt包內部測試測試代碼以_test.go爲後綴文件名不過隻在測試時被構建:
TestGoFiles表示的是fmt包内部测试测试代码以_test.go为后缀文件名不过只在测试时被构建:
{% raw %}
@ -36,9 +36,9 @@ $ go list -f={{.TestGoFiles}} fmt
{% endraw %}
包的測試代碼通常都在這些文件中不過fmt包併非如此稍後我們再解釋export_test.go文件的作用。
包的测试代码通常都在这些文件中不过fmt包并非如此稍后我们再解释export_test.go文件的作用。
XTestGoFiles表示的是屬於測試擴展包的測試代碼也就是fmt_test包因此它們必須先導入fmt包。同樣這些文件也隻是在測試時被構建運行:
XTestGoFiles表示的是属于测试扩展包的测试代码也就是fmt_test包因此它们必须先导入fmt包。同样这些文件也只是在测试时被构建运行:
{% raw %}
@ -49,11 +49,11 @@ $ go list -f={{.XTestGoFiles}} fmt
{% endraw %}
時候測試擴展包也需要訪問被測試包內部的代碼例如在一個爲了避免循環導入而被獨立到外部測試擴展包的白盒測試。在這種情況下我們可以通過一些技巧解決我們在包內的一個_test.go文件中導出一個內部的實現給測試擴展包。因爲這些代碼隻有在測試時才需要因此一般會放在export_test.go文件中。
时候测试扩展包也需要访问被测试包内部的代码例如在一个为了避免循环导入而被独立到外部测试扩展包的白盒测试。在这种情况下我们可以通过一些技巧解决我们在包内的一个_test.go文件中导出一个内部的实现给测试扩展包。因为这些代码只有在测试时才需要因此一般会放在export_test.go文件中。
例如fmt包的fmt.Scanf函數需要unicode.IsSpace函數提供的功能。但是爲了避免太多的依賴fmt包併沒有導入包含鉅大表格數據的unicode包相反fmt包有一個叫isSpace內部的簡易實現
例如fmt包的fmt.Scanf函数需要unicode.IsSpace函数提供的功能。但是为了避免太多的依赖fmt包并没有导入包含巨大表格数据的unicode包相反fmt包有一个叫isSpace内部的简易实现
爲了確保fmt.isSpace和unicode.IsSpace函數的行爲一致fmt包謹慎地包含了一個測試。是一個在測試擴展包內的白盒測試是無法直接訪問到isSpace內部函數的因此fmt通過一個祕密出口導出了isSpace函數。export_test.go文件就是專門用於測試擴展包的祕密出口。
为了确保fmt.isSpace和unicode.IsSpace函数的行为一致fmt包谨慎地包含了一个测试。是一个在测试扩展包内的白盒测试是无法直接访问到isSpace内部函数的因此fmt通过一个秘密出口导出了isSpace函数。export_test.go文件就是专门用于测试扩展包的秘密出口。
```Go
package fmt
@ -61,5 +61,5 @@ package fmt
var IsSpace = isSpace
```
這個測試文件併沒有定義測試代碼它隻是通過fmt.IsSpace簡單導出了內部的isSpace函數提供給測試擴展包使用。這個技巧可以廣泛用於位於測試擴展包的白盒測試
这个测试文件并没有定义测试代码它只是通过fmt.IsSpace简单导出了内部的isSpace函数提供给测试扩展包使用。这个技巧可以广泛用于位于测试扩展包的白盒测试

View File

@ -1,10 +1,10 @@
### 11.2.5. 編寫有效的測試
### 11.2.5. 编写有效的测试
許多Go語言新人會驚異於它的極簡的測試框架。很多其它語言的測試框架都提供了識别測試函數的機製通常使用反射或元數據通過設置一些“setup”和“teardown”的鉤子函數來執行測試用例運行的初始化和之後的清理操作同時測試工具箱還提供了很多類似assert斷言值比較函數格式化輸出錯誤信息和停止一個識别的測試等輔助函數通常使用異常機製。雖然這些機製可以使得測試非常簡潔但是測試輸出的日誌卻會像火星文一般難以理解。此外雖然測試最終也會輸出PASS或FAIL的報告但是它們提供的信息格式卻非常不利於代碼維護者快速定位問題因爲失敗的信息的具體含義是非常隱晦的比如“assert: 0 == 1”或成頁的海量跟蹤日誌
许多Go语言新人会惊异于它的极简的测试框架。很多其它语言的测试框架都提供了识别测试函数的机制通常使用反射或元数据通过设置一些“setup”和“teardown”的钩子函数来执行测试用例运行的初始化和之后的清理操作同时测试工具箱还提供了很多类似assert断言值比较函数格式化输出错误信息和停止一个识别的测试等辅助函数通常使用异常机制。虽然这些机制可以使得测试非常简洁但是测试输出的日志却会像火星文一般难以理解。此外虽然测试最终也会输出PASS或FAIL的报告但是它们提供的信息格式却非常不利于代码维护者快速定位问题因为失败的信息的具体含义是非常隐晦的比如“assert: 0 == 1”或成页的海量跟踪日志
Go語言的測試風格則形成鮮明對比。它期望測試者自己完成大部分的工作,定義函數避免重複,就像普通編程那樣。編寫測試併不是一個機械的填空過程;一個測試也有自己的接口,盡管它的維護者也是測試僅有的一個用戶。一個好的測試不應該引發其他無關的錯誤信息,它隻要清晰簡潔地描述問題的癥狀卽可,有時候可能還需要一些上下文信息。在理想情況下,維護者可以在不看代碼的情況下就能根據錯誤信息定位錯誤産生的原因。一個好的測試不應該在遇到一點小錯誤時就立刻退出測試,它應該嚐試報告更多的相關的錯誤信息,因爲我們可能從多個失敗測試的模式中發現錯誤産生的規律。
Go语言的测试风格则形成鲜明对比。它期望测试者自己完成大部分的工作,定义函数避免重复,就像普通编程那样。编写测试并不是一个机械的填空过程;一个测试也有自己的接口,尽管它的维护者也是测试仅有的一个用户。一个好的测试不应该引发其他无关的错误信息,它只要清晰简洁地描述问题的症状即可,有时候可能还需要一些上下文信息。在理想情况下,维护者可以在不看代码的情况下就能根据错误信息定位错误产生的原因。一个好的测试不应该在遇到一点小错误时就立刻退出测试,它应该尝试报告更多的相关的错误信息,因为我们可能从多个失败测试的模式中发现错误产生的规律。
下面的斷言函數比較兩個值,然後生成一個通用的錯誤信息,併停止程序。它很方便使用也確實有效果,但是當測試失敗的時候,打印的錯誤信息卻幾乎是沒有價值的。它併沒有爲快速解決問題提供一個很好的入口。
下面的断言函数比较两个值,然后生成一个通用的错误信息,并停止程序。它很方便使用也确实有效果,但是当测试失败的时候,打印的错误信息却几乎是没有价值的。它并没有为快速解决问题提供一个很好的入口。
```Go
import (
@ -25,7 +25,7 @@ func TestSplit(t *testing.T) {
}
```
從這個意義上説,斷言函數犯了過早抽象的錯誤:僅僅測試兩個整數是否相同,而放棄了根據上下文提供更有意義的錯誤信息的做法。我們可以根據具體的錯誤打印一個更有價值的錯誤信息,就像下面例子那樣。測試在隻有一次重複的模式出現時引入抽象。
从这个意义上说,断言函数犯了过早抽象的错误:仅仅测试两个整数是否相同,而放弃了根据上下文提供更有意义的错误信息的做法。我们可以根据具体的错误打印一个更有价值的错误信息,就像下面例子那样。测试在只有一次重复的模式出现时引入抽象。
```Go
func TestSplit(t *testing.T) {
@ -39,10 +39,10 @@ func TestSplit(t *testing.T) {
}
```
現在的測試不僅報告了調用的具體函數、它的輸入和結果的意義併且打印的眞實返迴的值和期望返迴的值併且卽使斷言失敗依然會繼續嚐試運行更多的測試。一旦我們寫了這樣結構的測試下一步自然不是用更多的if語句來擴展測試用例我們可以用像IsPalindrome的表驅動測試那樣來準備更多的s和sep測試用例。
现在的测试不仅报告了调用的具体函数、它的输入和结果的意义并且打印的真实返回的值和期望返回的值并且即使断言失败依然会继续尝试运行更多的测试。一旦我们写了这样结构的测试下一步自然不是用更多的if语句来扩展测试用例我们可以用像IsPalindrome的表驱动测试那样来准备更多的s和sep测试用例。
前面的例子併不需要額外的輔助函數如果有可以使測試代碼更簡單的方法我們也樂意接受。我們將在13.3節看到一個類似reflect.DeepEqual輔助函數。開始一個好的測試的關鍵是通過實現你眞正想要的具體行爲然後才是考慮然後簡化測試代碼。最好的接口是直接從庫的抽象接口開始針對公共接口編寫一些測試函數
前面的例子并不需要额外的辅助函数如果有可以使测试代码更简单的方法我们也乐意接受。我们将在13.3节看到一个类似reflect.DeepEqual辅助函数。开始一个好的测试的关键是通过实现你真正想要的具体行为然后才是考虑然后简化测试代码。最好的接口是直接从库的抽象接口开始针对公共接口编写一些测试函数
**練習11.5:** 用表格驅動的技術擴展TestSplit測試併打印期望的輸出結果。
**练习11.5:** 用表格驱动的技术扩展TestSplit测试并打印期望的输出结果。

View File

@ -1,8 +1,8 @@
### 11.2.6. 避免的不穩定的測試
### 11.2.6. 避免的不稳定的测试
如果一個應用程序對於新出現的但有效的輸入經常失敗説明程序不夠穩健;同樣如果一個測試僅僅因爲聲音變化就會導致失敗也是不合邏輯的。就像一個不夠穩健的程序會挫敗它的用戶一樣,一個脆弱性測試同樣會激怒它的維護者。最脆弱的測試代碼會在程序沒有任何變化的時候産生不同的結果,時好時壞,處理它們會耗費大量的時間但是併不會得到任何好處
如果一个应用程序对于新出现的但有效的输入经常失败说明程序不够稳健;同样如果一个测试仅仅因为声音变化就会导致失败也是不合逻辑的。就像一个不够稳健的程序会挫败它的用户一样,一个脆弱性测试同样会激怒它的维护者。最脆弱的测试代码会在程序没有任何变化的时候产生不同的结果,时好时坏,处理它们会耗费大量的时间但是并不会得到任何好处
當一個測試函數産生一個複雜的輸出如一個很長的字符串或一個精心設計的數據結構或一個文件它可以用於和預設的“golden”結果數據對比用這種簡單方式寫測試是誘人的。但是隨着項目的發展輸出的某些部分很可能會發生變化盡管很可能是一個改進的實現導致的。而且不僅僅是輸出部分函數複雜複製的輸入部分可能也跟着變化了因此測試使用的輸入也就不在有效了。
当一个测试函数产生一个复杂的输出如一个很长的字符串或一个精心设计的数据结构或一个文件它可以用于和预设的“golden”结果数据对比用这种简单方式写测试是诱人的。但是随着项目的发展输出的某些部分很可能会发生变化尽管很可能是一个改进的实现导致的。而且不仅仅是输出部分函数复杂复制的输入部分可能也跟着变化了因此测试使用的输入也就不在有效了。
避免脆弱測試代碼的方法是隻檢測你眞正關心的屬性。保持測試代碼的簡潔和內部結構的穩定。特别是對斷言部分要有所選擇。不要檢査字符串的全匹配,但是尋找相關的子字符串,因爲某些子字符串在項目的發展中是比較穩定不變的。通常編寫一個重複雜的輸出中提取必要精華信息以用於斷言是值得的,雖然這可能會帶來很多前期的工作,但是它可以幫助迅速及時脩複因爲項目演化而導致的不合邏輯的失敗測試
避免脆弱测试代码的方法是只检测你真正关心的属性。保持测试代码的简洁和内部结构的稳定。特别是对断言部分要有所选择。不要检查字符串的全匹配,但是寻找相关的子字符串,因为某些子字符串在项目的发展中是比较稳定不变的。通常编写一个重复杂的输出中提取必要精华信息以用于断言是值得的,虽然这可能会带来很多前期的工作,但是它可以帮助迅速及时修复因为项目演化而导致的不合逻辑的失败测试

View File

@ -1,6 +1,6 @@
## 11.2. 測試函數
## 11.2. 测试函数
個測試函數必須導入testing包。測試函數有如下的籤名:
个测试函数必须导入testing包。测试函数有如下的签名:
```Go
func TestName(t *testing.T) {
@ -8,7 +8,7 @@ func TestName(t *testing.T) {
}
```
測試函數的名字必須以Test開頭可選的後綴名必須以大寫字母開頭
测试函数的名字必须以Test开头可选的后缀名必须以大写字母开头
```Go
func TestSin(t *testing.T) { /* ... */ }
@ -16,7 +16,7 @@ func TestCos(t *testing.T) { /* ... */ }
func TestLog(t *testing.T) { /* ... */ }
```
其中t參數用於報告測試失敗和附加的日誌信息。讓我們定義一個實例包gopl.io/ch11/word1其中隻有一個函數IsPalindrome用於檢査一個字符串是否從前向後和從後向前讀都是一樣的。下面這個實現對於一個字符串是否是迴文字符串前後重複測試了兩次我們稍後會再討論這個問題。)
其中t参数用于报告测试失败和附加的日志信息。让我们定义一个实例包gopl.io/ch11/word1其中只有一个函数IsPalindrome用于检查一个字符串是否从前向后和从后向前读都是一样的。下面这个实现对于一个字符串是否是回文字符串前后重复测试了两次我们稍后会再讨论这个问题。)
<u><i>gopl.io/ch11/word1</i></u>
```Go
@ -35,7 +35,7 @@ func IsPalindrome(s string) bool {
}
```
在相同的目録下word_test.go測試文件中包含了TestPalindrome和TestNonPalindrome兩個測試函數。每一個都是測試IsPalindrome是否給出正確的結果併使用t.Error報告失敗信息:
在相同的目录下word_test.go测试文件中包含了TestPalindrome和TestNonPalindrome两个测试函数。每一个都是测试IsPalindrome是否给出正确的结果并使用t.Error报告失败信息:
```Go
package word
@ -58,7 +58,7 @@ func TestNonPalindrome(t *testing.T) {
}
```
`go test`命令如果沒有參數指定包那麽將默認采用當前目録對應的包(和`go build`命令一樣)。我們可以用下面的命令構建和運行測試
`go test`命令如果没有参数指定包那么将默认采用当前目录对应的包(和`go build`命令一样)。我们可以用下面的命令构建和运行测试
```
$ cd $GOPATH/src/gopl.io/ch11/word1
@ -66,7 +66,7 @@ $ go test
ok gopl.io/ch11/word1 0.008s
```
結果還比較滿意,我們運行了這個程序, 不過沒有提前退出是因爲還沒有遇到BUG報告。不過一個法国名爲“Noelle Eve Elleon”的用戶會抱怨IsPalindrome函數不能識别“été”。另外一個來自美国中部用戶的抱怨則是不能識别“A man, a plan, a canal: Panama.”。執行特殊和小的BUG報告爲我們提供了新的更自然的測試用例。
结果还比较满意,我们运行了这个程序, 不过没有提前退出是因为还没有遇到BUG报告。不过一个法国名为“Noelle Eve Elleon”的用户会抱怨IsPalindrome函数不能识别“été”。另外一个来自美国中部用户的抱怨则是不能识别“A man, a plan, a canal: Panama.”。执行特殊和小的BUG报告为我们提供了新的更自然的测试用例。
```Go
func TestFrenchPalindrome(t *testing.T) {
@ -83,9 +83,9 @@ func TestCanalPalindrome(t *testing.T) {
}
```
爲了避免兩次輸入較長的字符串我們使用了提供了有類似Printf格式化功能的 Errorf函數來滙報錯誤結果。
为了避免两次输入较长的字符串我们使用了提供了有类似Printf格式化功能的 Errorf函数来汇报错误结果。
當添加了這兩個測試用例之後,`go test`返迴了測試失敗的信息。
当添加了这两个测试用例之后,`go test`返回了测试失败的信息。
```
$ go test
@ -97,11 +97,11 @@ FAIL
FAIL gopl.io/ch11/word1 0.014s
```
編寫測試用例併觀察到測試用例觸發了和用戶報告的錯誤相同的描述是一個好的測試習慣。隻有這樣,我們才能定位我們要眞正解決的問題
编写测试用例并观察到测试用例触发了和用户报告的错误相同的描述是一个好的测试习惯。只有这样,我们才能定位我们要真正解决的问题
寫測試用例的另外的好處是,運行測試通常會比手工描述報告的處理更快,這讓我們可以進行快速地迭代。如果測試集有很多運行緩慢的測試,我們可以通過隻選擇運行某些特定的測試來加快測試速度。
写测试用例的另外的好处是,运行测试通常会比手工描述报告的处理更快,这让我们可以进行快速地迭代。如果测试集有很多运行缓慢的测试,我们可以通过只选择运行某些特定的测试来加快测试速度。
參數`-v`可用於打印每個測試函數的名字和運行時間
参数`-v`可用于打印每个测试函数的名字和运行时间
```
$ go test -v
@ -120,7 +120,7 @@ exit status 1
FAIL gopl.io/ch11/word1 0.017s
```
參數`-run`對應一個正則表達式,隻有測試函數名被它正確匹配的測試函數才會被`go test`測試命令運行:
参数`-run`对应一个正则表达式,只有测试函数名被它正确匹配的测试函数才会被`go test`测试命令运行:
```
$ go test -v -run="French|Canal"
@ -135,11 +135,11 @@ exit status 1
FAIL gopl.io/ch11/word1 0.014s
```
當然,一旦我們已經脩複了失敗的測試用例,在我們提交代碼更新之前,我們應該以不帶參數的`go test`命令運行全部的測試用例,以確保脩複失敗測試的同時沒有引入新的問題
当然,一旦我们已经修复了失败的测试用例,在我们提交代码更新之前,我们应该以不带参数的`go test`命令运行全部的测试用例,以确保修复失败测试的同时没有引入新的问题
們現在的任務就是脩複這些錯誤。簡要分析後發現第一個BUG的原因是我們采用了 byte而不是rune序列所以像“été”中的é等非ASCII字符不能正確處理。第二個BUG是因爲沒有忽略空格和字母的大小寫導致的。
们现在的任务就是修复这些错误。简要分析后发现第一个BUG的原因是我们采用了 byte而不是rune序列所以像“été”中的é等非ASCII字符不能正确处理。第二个BUG是因为没有忽略空格和字母的大小写导致的。
針對上述兩個BUG我們仔細重寫了函數
针对上述两个BUG我们仔细重写了函数
<u><i>gopl.io/ch11/word2</i></u>
```Go
@ -166,7 +166,7 @@ func IsPalindrome(s string) bool {
}
```
時我們也將之前的所有測試數據合併到了一個測試中的表格中。
时我们也将之前的所有测试数据合并到了一个测试中的表格中。
```Go
func TestIsPalindrome(t *testing.T) {
@ -196,24 +196,24 @@ func TestIsPalindrome(t *testing.T) {
}
```
現在我們的新測試阿都通過了:
现在我们的新测试阿都通过了:
```
$ go test gopl.io/ch11/word2
ok gopl.io/ch11/word2 0.015s
```
這種表格驅動的測試在Go語言中很常見的。我們很容易向表格添加新的測試數據併且後面的測試邏輯也沒有冗餘這樣我們可以有更多的精力地完善錯誤信息。
这种表格驱动的测试在Go语言中很常见的。我们很容易向表格添加新的测试数据并且后面的测试逻辑也没有冗余这样我们可以有更多的精力地完善错误信息。
敗測試的輸出併不包括調用t.Errorf時刻的堆棧調用信息。和其他編程語言或測試框架的assert斷言不同t.Errorf調用也沒有引起panic異常或停止測試的執行。卽使表格中前面的數據導致了測試的失敗表格後面的測試數據依然會運行測試因此在一個測試中我們可能了解多個失敗的信息。
败测试的输出并不包括调用t.Errorf时刻的堆栈调用信息。和其他编程语言或测试框架的assert断言不同t.Errorf调用也没有引起panic异常或停止测试的执行。即使表格中前面的数据导致了测试的失败表格后面的测试数据依然会运行测试因此在一个测试中我们可能了解多个失败的信息。
如果我們眞的需要停止測試或許是因爲初始化失敗或可能是早先的錯誤導致了後續錯誤等原因我們可以使用t.Fatal或t.Fatalf停止當前測試函數。它們必須在和測試函數同一個goroutine內調用。
如果我们真的需要停止测试或许是因为初始化失败或可能是早先的错误导致了后续错误等原因我们可以使用t.Fatal或t.Fatalf停止当前测试函数。它们必须在和测试函数同一个goroutine内调用。
測試失敗的信息一般的形式是“f(x) = y, want z”其中f(x)解釋了失敗的操作和對應的輸出y是實際的運行結果z是期望的正確的結果。就像前面檢査迴文字符串的例子實際的函數用於f(x)部分。如果顯示x是表格驅動型測試中比較重要的部分因爲同一個斷言可能對應不同的表格項執行多次。要避免無用和冗餘的信息。在測試類似IsPalindrome返迴布爾類型的函數時可以忽略併沒有額外信息的z部分。如果x、y或z是y的長度輸出一個相關部分的簡明總結卽可。測試的作者應該要努力幫助程序員診斷測試失敗的原因。
测试失败的信息一般的形式是“f(x) = y, want z”其中f(x)解释了失败的操作和对应的输出y是实际的运行结果z是期望的正确的结果。就像前面检查回文字符串的例子实际的函数用于f(x)部分。如果显示x是表格驱动型测试中比较重要的部分因为同一个断言可能对应不同的表格项执行多次。要避免无用和冗余的信息。在测试类似IsPalindrome返回布尔类型的函数时可以忽略并没有额外信息的z部分。如果x、y或z是y的长度输出一个相关部分的简明总结即可。测试的作者应该要努力帮助程序员诊断测试失败的原因。
**練習 11.1:** 爲4.3節中的charcount程序編寫測試
**练习 11.1:** 为4.3节中的charcount程序编写测试
**練習 11.2:** 爲§6.5的IntSet編寫一組測試用於檢査每個操作後的行爲和基於內置map的集合等價後面練習11.7將會用到。
**练习 11.2:** 为§6.5的IntSet编写一组测试用于检查每个操作后的行为和基于内置map的集合等价后面练习11.7将会用到。
{% include "./ch11-02-1.md" %}

View File

@ -1,12 +1,12 @@
## 11.3. 測試覆蓋
## 11.3. 测试覆盖
就其性質而言測試不可能是完整的。計算機科學家Edsger Dijkstra曾説過“測試可以顯示存在缺陷但是併不是説沒有BUG。”再多的測試也不能證明一個程序沒有BUG。在最好的情況下測試可以增強我們的信心代碼在我們測試的環境是可以正常工作的。
就其性质而言测试不可能是完整的。计算机科学家Edsger Dijkstra曾说过“测试可以显示存在缺陷但是并不是说没有BUG。”再多的测试也不能证明一个程序没有BUG。在最好的情况下测试可以增强我们的信心代码在我们测试的环境是可以正常工作的。
測試驅動觸發運行到的被測試函數的代碼數目稱爲測試的覆蓋率。測試覆蓋率併不能量化——甚至連最簡單的動態程序也難以精確測量——但是可以啟發併幫助我們編寫的有效的測試代碼
测试驱动触发运行到的被测试函数的代码数目称为测试的覆盖率。测试覆盖率并不能量化——甚至连最简单的动态程序也难以精确测量——但是可以启发并帮助我们编写的有效的测试代码
這些幫助信息中語句的覆蓋率是最簡單和最廣泛使用的。語句的覆蓋率是指在測試中至少被運行一次的代碼占總代碼數的比例。在本節中,我們使用`go test`命令中集成的測試覆蓋率工具,來度量下面代碼的測試覆蓋率,幫助我們識别測試和我們期望間的差距。
这些帮助信息中语句的覆盖率是最简单和最广泛使用的。语句的覆盖率是指在测试中至少被运行一次的代码占总代码数的比例。在本节中,我们使用`go test`命令中集成的测试覆盖率工具,来度量下面代码的测试覆盖率,帮助我们识别测试和我们期望间的差距。
下面的代碼是一個表格驅動的測試,用於測試第七章的表達式求值程序:
下面的代码是一个表格驱动的测试,用于测试第七章的表达式求值程序:
<u><i>gopl.io/ch7/eval</i></u>
```Go
@ -45,7 +45,7 @@ func TestCoverage(t *testing.T) {
}
```
首先,我們要確保所有的測試都正常通過
首先,我们要确保所有的测试都正常通过
```
$ go test -v -run=Coverage gopl.io/ch7/eval
@ -55,7 +55,7 @@ PASS
ok gopl.io/ch7/eval 0.011s
```
下面這個命令可以顯示測試覆蓋率工具的使用用法:
下面这个命令可以显示测试覆盖率工具的使用用法:
```
$ go tool cover
@ -68,20 +68,20 @@ Open a web browser displaying annotated source code:
...
```
`go tool`命令運行Go工具鏈的底層可執行程序。這些底層可執行程序放在$GOROOT/pkg/tool/${GOOS}_${GOARCH}目録。因爲有`go build`命令的原因,我們很少直接調用這些底層工具。
`go tool`命令运行Go工具链的底层可执行程序。这些底层可执行程序放在$GOROOT/pkg/tool/${GOOS}_${GOARCH}目录。因为有`go build`命令的原因,我们很少直接调用这些底层工具。
現在我們可以用`-coverprofile`標誌參數重新運行測試
现在我们可以用`-coverprofile`标志参数重新运行测试
```
$ go test -run=Coverage -coverprofile=c.out gopl.io/ch7/eval
ok gopl.io/ch7/eval 0.032s coverage: 68.5% of statements
```
這個標誌參數通過在測試代碼中插入生成鉤子來統計覆蓋率數據。也就是説在運行每個測試前它會脩改要測試代碼的副本在每個詞法塊都會設置一個布爾標誌變量。當被脩改後的被測試代碼運行退出時將統計日誌數據寫入c.out文件併打印一部分執行的語句的一個總結。(如果你需要的是摘要,使用`go test -cover`。)
这个标志参数通过在测试代码中插入生成钩子来统计覆盖率数据。也就是说在运行每个测试前它会修改要测试代码的副本在每个词法块都会设置一个布尔标志变量。当被修改后的被测试代码运行退出时将统计日志数据写入c.out文件并打印一部分执行的语句的一个总结。(如果你需要的是摘要,使用`go test -cover`。)
如果使用了`-covermode=count`標誌參數,那麽將在每個代碼塊插入一個計數器而不是布爾標誌量。在統計結果中記録了每個塊的執行次數,這可以用於衡量哪些是被頻繁執行的熱點代碼
如果使用了`-covermode=count`标志参数,那么将在每个代码块插入一个计数器而不是布尔标志量。在统计结果中记录了每个块的执行次数,这可以用于衡量哪些是被频繁执行的热点代码
爲了收集數據我們運行了測試覆蓋率工具打印了測試日誌生成一個HTML報告然後在瀏覽器中打開11.3)。
为了收集数据我们运行了测试覆盖率工具打印了测试日志生成一个HTML报告然后在浏览器中打开11.3)。
```
$ go tool cover -html=c.out
@ -89,12 +89,12 @@ $ go tool cover -html=c.out
![](../images/ch11-03.png)
緑色的代碼塊被測試覆蓋到了紅色的則表示沒有被覆蓋到。爲了清晰起見我們將的背景紅色文本的背景設置成了陰影效果。我們可以馬上發現unary操作的Eval方法併沒有被執行到。如果我們針對這部分未被覆蓋的代碼添加下面的測試用例然後重新運行上面的命令那麽我們將會看到那個紅色部分的代碼也變成緑色了:
绿色的代码块被测试覆盖到了红色的则表示没有被覆盖到。为了清晰起见我们将的背景红色文本的背景设置成了阴影效果。我们可以马上发现unary操作的Eval方法并没有被执行到。如果我们针对这部分未被覆盖的代码添加下面的测试用例然后重新运行上面的命令那么我们将会看到那个红色部分的代码也变成绿色了:
```
{"-x * -x", eval.Env{"x": 2}, "4"}
```
過兩個panic語句依然是紅色的。這是沒有問題的因爲這兩個語句併不會被執行到。
过两个panic语句依然是红色的。这是没有问题的因为这两个语句并不会被执行到。
實現100%的測試覆蓋率聽起來很美但是在具體實踐中通常是不可行的也不是值得推薦的做法。因爲那隻能説明代碼被執行過而已併不意味着代碼就是沒有BUG的因爲對於邏輯複雜的語句需要針對不同的輸入執行多次。有一些語句例如上面的panic語句則永遠都不會被執行到。另外還有一些隱晦的錯誤在現實中很少遇到也很難編寫對應的測試代碼。測試從本質上來説是一個比較務實的工作編寫測試代碼和編寫應用代碼的成本對比是需要考慮的。測試覆蓋率工具可以幫助我們快速識别測試薄弱的地方但是設計好的測試用例和編寫應用代碼一樣需要嚴密的思考。
实现100%的测试覆盖率听起来很美但是在具体实践中通常是不可行的也不是值得推荐的做法。因为那只能说明代码被执行过而已并不意味着代码就是没有BUG的因为对于逻辑复杂的语句需要针对不同的输入执行多次。有一些语句例如上面的panic语句则永远都不会被执行到。另外还有一些隐晦的错误在现实中很少遇到也很难编写对应的测试代码。测试从本质上来说是一个比较务实的工作编写测试代码和编写应用代码的成本对比是需要考虑的。测试覆盖率工具可以帮助我们快速识别测试薄弱的地方但是设计好的测试用例和编写应用代码一样需要严密的思考。

View File

@ -1,8 +1,8 @@
## 11.4. 基準測試
## 11.4. 基准测试
準測試是測量一個程序在固定工作負載下的性能。在Go語言中基準測試函數和普通測試函數寫法類似但是以Benchmark爲前綴名併且帶有一個`*testing.B`類型的參數;`*testing.B`參數除了提供和`*testing.T`類似的方法還有額外一些和性能測量相關的方法。它還提供了一個整數N用於指定操作執行的循環次數
准测试是测量一个程序在固定工作负载下的性能。在Go语言中基准测试函数和普通测试函数写法类似但是以Benchmark为前缀名并且带有一个`*testing.B`类型的参数;`*testing.B`参数除了提供和`*testing.T`类似的方法还有额外一些和性能测量相关的方法。它还提供了一个整数N用于指定操作执行的循环次数
下面是IsPalindrome函數的基準測試,其中循環將執行N次。
下面是IsPalindrome函数的基准测试,其中循环将执行N次。
```Go
import "testing"
@ -14,7 +14,7 @@ func BenchmarkIsPalindrome(b *testing.B) {
}
```
們用下面的命令運行基準測試。和普通測試不同的是,默認情況下不運行任何基準測試。我們需要通過`-bench`命令行標誌參數手工指定要運行的基準測試函數。該參數是一個正則表達式,用於匹配要執行的基準測試函數的名字,默認值是空的。其中“.”模式將可以匹配所有基準測試函數,但是這里總共隻有一個基準測試函數,因此和`-bench=IsPalindrome`參數是等價的效果。
们用下面的命令运行基准测试。和普通测试不同的是,默认情况下不运行任何基准测试。我们需要通过`-bench`命令行标志参数手工指定要运行的基准测试函数。该参数是一个正则表达式,用于匹配要执行的基准测试函数的名字,默认值是空的。其中“.”模式将可以匹配所有基准测试函数,但是这里总共只有一个基准测试函数,因此和`-bench=IsPalindrome`参数是等价的效果。
```
$ cd $GOPATH/src/gopl.io/ch11/word2
@ -24,13 +24,13 @@ BenchmarkIsPalindrome-8 1000000 1035 ns/op
ok gopl.io/ch11/word2 2.179s
```
結果中基準測試名的數字後綴部分這里是8表示運行時對應的GOMAXPROCS的值這對於一些和併發相關的基準測試是重要的信息。
结果中基准测试名的数字后缀部分这里是8表示运行时对应的GOMAXPROCS的值这对于一些和并发相关的基准测试是重要的信息。
報告顯示每次調用IsPalindrome函數花費1.035微秒是執行1,000,000次的平均時間。因爲基準測試驅動器開始時併不知道每個基準測試函數運行所花的時間它會嚐試在眞正運行基準測試前先嚐試用較小的N運行測試來估算基準測試函數所需要的時間然後推斷一個較大的時間保證穩定的測量結果。
报告显示每次调用IsPalindrome函数花费1.035微秒是执行1,000,000次的平均时间。因为基准测试驱动器开始时并不知道每个基准测试函数运行所花的时间它会尝试在真正运行基准测试前先尝试用较小的N运行测试来估算基准测试函数所需要的时间然后推断一个较大的时间保证稳定的测量结果。
環在基準測試函數內實現而不是放在基準測試框架內實現這樣可以讓每個基準測試函數有機會在循環啟動前執行初始化代碼這樣併不會顯著影響每次迭代的平均運行時間。如果還是擔心初始化代碼部分對測量時間帶來榦擾那麽可以通過testing.B參數提供的方法來臨時關閉或重置計時器不過這些一般很少會用到。
环在基准测试函数内实现而不是放在基准测试框架内实现这样可以让每个基准测试函数有机会在循环启动前执行初始化代码这样并不会显著影响每次迭代的平均运行时间。如果还是担心初始化代码部分对测量时间带来干扰那么可以通过testing.B参数提供的方法来临时关闭或重置计时器不过这些一般很少会用到。
現在我們有了一個基準測試和普通測試我們可以很容易測試新的讓程序運行更快的想法。也許最明顯的優化是在IsPalindrome函數中第二個循環的停止檢査這樣可以避免每個比較都做兩次:
现在我们有了一个基准测试和普通测试我们可以很容易测试新的让程序运行更快的想法。也许最明显的优化是在IsPalindrome函数中第二个循环的停止检查这样可以避免每个比较都做两次:
```Go
n := len(letters)/2
@ -42,7 +42,7 @@ for i := 0; i < n; i++ {
return true
```
過很多情況下一個明顯的優化併不一定就能代碼預期的效果。這個改進在基準測試中隻帶來了4%的性能提陞
过很多情况下一个明显的优化并不一定就能代码预期的效果。这个改进在基准测试中只带来了4%的性能提升
```
$ go test -bench=.
@ -51,7 +51,7 @@ BenchmarkIsPalindrome-8 1000000 992 ns/op
ok gopl.io/ch11/word2 2.093s
```
另一個改進想法是在開始爲每個字符預先分配一個足夠大的數組這樣就可以避免在append調用時可能會導致內存的多次重新分配。聲明一個letters數組變量併指定合適的大小像下面這樣
另一个改进想法是在开始为每个字符预先分配一个足够大的数组这样就可以避免在append调用时可能会导致内存的多次重新分配。声明一个letters数组变量并指定合适的大小像下面这样
```Go
letters := make([]rune, 0, len(s))
@ -62,7 +62,7 @@ for _, r := range s {
}
```
這個改進提陞性能約35%報告結果是基於2,000,000次迭代的平均運行時間統計
这个改进提升性能约35%报告结果是基于2,000,000次迭代的平均运行时间统计
```
$ go test -bench=.
@ -71,7 +71,7 @@ BenchmarkIsPalindrome-8 2000000 697 ns/op
ok gopl.io/ch11/word2 1.468s
```
這個例子所示,快的程序往往是伴隨着較少的內存分配。`-benchmem`命令行標誌參數將在報告中包含內存的分配數據統計。我們可以比較優化前後內存的分配情況
这个例子所示,快的程序往往是伴随着较少的内存分配。`-benchmem`命令行标志参数将在报告中包含内存的分配数据统计。我们可以比较优化前后内存的分配情况
```
$ go test -bench=. -benchmem
@ -79,7 +79,7 @@ PASS
BenchmarkIsPalindrome 1000000 1026 ns/op 304 B/op 4 allocs/op
```
這是優化之後的結果:
这是优化之后的结果:
```
$ go test -bench=. -benchmem
@ -87,11 +87,11 @@ PASS
BenchmarkIsPalindrome 2000000 807 ns/op 128 B/op 1 allocs/op
```
用一次內存分配代替多次的內存分配節省了75%的分配調用次數和減少近一半的內存需求。
用一次内存分配代替多次的内存分配节省了75%的分配调用次数和减少近一半的内存需求。
這個基準測試告訴我們所需的絶對時間依賴給定的具體操作兩個不同的操作所需時間的差異也是和不同環境相關的。例如如果一個函數需要1ms處理1,000個元素那麽處理10000或1百萬將需要多少時間呢這樣的比較揭示了漸近增長函數的運行時間。另一個例子I/O緩存該設置爲多大呢基準測試可以幫助我們選擇較小的緩存但能帶來滿意的性能。第三個例子對於一個確定的工作那種算法更好基準測試可以評估兩種不同算法對於相同的輸入在不同的場景和負載下的優缺點
这个基准测试告诉我们所需的绝对时间依赖给定的具体操作两个不同的操作所需时间的差异也是和不同环境相关的。例如如果一个函数需要1ms处理1,000个元素那么处理10000或1百万将需要多少时间呢这样的比较揭示了渐近增长函数的运行时间。另一个例子I/O缓存该设置为多大呢基准测试可以帮助我们选择较小的缓存但能带来满意的性能。第三个例子对于一个确定的工作那种算法更好基准测试可以评估两种不同算法对于相同的输入在不同的场景和负载下的优缺点
一般比較基準測試都是結構類似的代碼。它們通常是采用一個參數的函數,從幾個標誌的基準測試函數入口調用,就像這樣
一般比较基准测试都是结构类似的代码。它们通常是采用一个参数的函数,从几个标志的基准测试函数入口调用,就像这样
```Go
func benchmark(b *testing.B, size int) { /* ... */ }
@ -100,13 +100,13 @@ func Benchmark100(b *testing.B) { benchmark(b, 100) }
func Benchmark1000(b *testing.B) { benchmark(b, 1000) }
```
過函數參數來指定輸入的大小但是參數變量對於每個具體的基準測試都是固定的。要避免直接脩改b.N來控製輸入的大小。除非你將它作爲一個固定大小的迭代計算輸入否則基準測試的結果將毫無意義
过函数参数来指定输入的大小但是参数变量对于每个具体的基准测试都是固定的。要避免直接修改b.N来控制输入的大小。除非你将它作为一个固定大小的迭代计算输入否则基准测试的结果将毫无意义
準測試對於編寫代碼是很有幫助的,但是卽使工作完成了也應當保存基準測試代碼。因爲隨着項目的發展,或者是輸入的增加,或者是部署到新的操作繫統或不同的處理器,我們可以再次用基準測試來幫助我們改進設計
准测试对于编写代码是很有帮助的,但是即使工作完成了也应当保存基准测试代码。因为随着项目的发展,或者是输入的增加,或者是部署到新的操作系统或不同的处理器,我们可以再次用基准测试来帮助我们改进设计
**練習 11.6:** 爲2.6.2節的練習2.4和練習2.5的PopCount函數編寫基準測試。看看基於表格算法在不同情況下對提陞性能會有多大幫助。
**练习 11.6:** 为2.6.2节的练习2.4和练习2.5的PopCount函数编写基准测试。看看基于表格算法在不同情况下对提升性能会有多大帮助。
**練習 11.7:** 爲\*IntSet§6.5的Add、UnionWith和其他方法編寫基準測試使用大量隨機輸入。你可以讓這些方法跑多快選擇字的大小對於性能的影響如何IntSet和基於內建map的實現相比有多快?
**练习 11.7:** 为\*IntSet§6.5的Add、UnionWith和其他方法编写基准测试使用大量随机输入。你可以让这些方法跑多快选择字的大小对于性能的影响如何IntSet和基于内建map的实现相比有多快?

View File

@ -1,22 +1,22 @@
## 11.5. 剖析
測量基準對於衡量特定操作的性能是有幫助的但是當我們視圖讓程序跑的更快的時候我們通常併不知道從哪里開始優化。每個碼農都應該知道Donald Knuth在1974年的“Structured Programming with go to Statements”上所説的格言。雖然經常被解讀爲不重視性能的意思但是從原文我們可以看到不同的含義
测量基准对于衡量特定操作的性能是有帮助的但是当我们视图让程序跑的更快的时候我们通常并不知道从哪里开始优化。每个码农都应该知道Donald Knuth在1974年的“Structured Programming with go to Statements”上所说的格言。虽然经常被解读为不重视性能的意思但是从原文我们可以看到不同的含义
> 毫無疑問效率會導致各種濫用。程序員需要浪費大量的時間思考或者擔心被部分程序的速度所榦擾實際上這些嚐試提陞效率的行爲可能産生強烈的負面影響特别是當調試和維護的時候。我們不應該過度糾結於細節的優化應該説約97%的場景:過早的優化是萬惡之源。
> 毫无疑问效率会导致各种滥用。程序员需要浪费大量的时间思考或者担心被部分程序的速度所干扰实际上这些尝试提升效率的行为可能产生强烈的负面影响特别是当调试和维护的时候。我们不应该过度纠结于细节的优化应该说约97%的场景:过早的优化是万恶之源。
>
> 我們當然不應該放棄那關鍵的3%的機會。一個好的程序員不會因爲這個理由而滿足,他們會明智地觀察和識别哪些是關鍵的代碼;但是隻有在關鍵代碼已經被確認的前提下才會進行優化。對於判斷哪些部分是關鍵代碼是經常容易犯經驗性錯誤的地方,因此程序員普通使用的測量工具,使得他們的直覺很不靠譜
> 我们当然不应该放弃那关键的3%的机会。一个好的程序员不会因为这个理由而满足,他们会明智地观察和识别哪些是关键的代码;但是只有在关键代码已经被确认的前提下才会进行优化。对于判断哪些部分是关键代码是经常容易犯经验性错误的地方,因此程序员普通使用的测量工具,使得他们的直觉很不靠谱
當我們想仔細觀察我們程序的運行速度的時候,最好的技術是如何識别關鍵代碼。自動化的剖析技術是基於程序執行期間一些抽樣數據,然後推斷後面的執行狀態;最終産生一個運行時間的統計數據文件。
当我们想仔细观察我们程序的运行速度的时候,最好的技术是如何识别关键代码。自动化的剖析技术是基于程序执行期间一些抽样数据,然后推断后面的执行状态;最终产生一个运行时间的统计数据文件。
Go語言支持多種類型的剖析性能分析,每一種關註不同的方面,但它們都涉及到每個采樣記録的感興趣的一繫列事件消息,每個事件都包含函數調用時函數調用堆棧的信息。內建的`go test`工具對幾種分析方式都提供了支持。
Go语言支持多种类型的剖析性能分析,每一种关注不同的方面,但它们都涉及到每个采样记录的感兴趣的一系列事件消息,每个事件都包含函数调用时函数调用堆栈的信息。内建的`go test`工具对几种分析方式都提供了支持。
CPU分析文件標識了函數執行時所需要的CPU時間。當前運行的繫統線程在每隔幾毫秒都會遇到操作繫統的中斷事件每次中斷時都會記録一個分析文件然後恢複正常的運行。
CPU分析文件标识了函数执行时所需要的CPU时间。当前运行的系统线程在每隔几毫秒都会遇到操作系统的中断事件每次中断时都会记录一个分析文件然后恢复正常的运行。
堆分析則記録了程序的內存使用情況。每個內存分配操作都會觸發內部平均內存分配例程每個512KB的內存申請都會觸發一個事件。
堆分析则记录了程序的内存使用情况。每个内存分配操作都会触发内部平均内存分配例程每个512KB的内存申请都会触发一个事件。
阻塞分析則記録了goroutine最大的阻塞操作例如繫統調用、管道發送和接收還有獲取鎖等。分析庫會記録每個goroutine被阻塞時的相關操作。
阻塞分析则记录了goroutine最大的阻塞操作例如系统调用、管道发送和接收还有获取锁等。分析库会记录每个goroutine被阻塞时的相关操作。
測試環境下隻需要一個標誌參數就可以生成各種分析文件。當一次使用多個標誌參數時需要當心,因爲分析操作本身也可能會影像程序的運行。
测试环境下只需要一个标志参数就可以生成各种分析文件。当一次使用多个标志参数时需要当心,因为分析操作本身也可能会影像程序的运行。
```
$ go test -cpuprofile=cpu.out
@ -24,13 +24,13 @@ $ go test -blockprofile=block.out
$ go test -memprofile=mem.out
```
對於一些非測試程序也很容易支持分析的特性具體的實現方式和程序是短時間運行的小工具還是長時間運行的服務會有很大不同因此Go的runtime運行時包提供了程序運行時控製分析特性的接口。
对于一些非测试程序也很容易支持分析的特性具体的实现方式和程序是短时间运行的小工具还是长时间运行的服务会有很大不同因此Go的runtime运行时包提供了程序运行时控制分析特性的接口。
一旦我們已經收集到了用於分析的采樣數據我們就可以使用pprof來分析這些數據。這是Go工具箱自帶的一個工具但併不是一個日常工具它對應`go tool pprof`命令。該命令有許多特性和選項,但是最重要的有兩個,就是生成這個概要文件的可執行程序和對於的分析日誌文件。
一旦我们已经收集到了用于分析的采样数据我们就可以使用pprof来分析这些数据。这是Go工具箱自带的一个工具但并不是一个日常工具它对应`go tool pprof`命令。该命令有许多特性和选项,但是最重要的有两个,就是生成这个概要文件的可执行程序和对于的分析日志文件。
爲了提高分析效率和減少空間分析日誌本身併不包含函數的名字它隻包含函數對應的地址。也就是説pprof需要和分析日誌對於的可執行程序。雖然`go test`命令通常會丟棄臨時用的測試程序但是在啟用分析的時候會將測試程序保存爲foo.test文件其中foo部分對於測試包的名字。
为了提高分析效率和减少空间分析日志本身并不包含函数的名字它只包含函数对应的地址。也就是说pprof需要和分析日志对于的可执行程序。虽然`go test`命令通常会丢弃临时用的测试程序但是在启用分析的时候会将测试程序保存为foo.test文件其中foo部分对于测试包的名字。
下面的命令演示了如何生成一個CPU分析文件。我們選擇`net/http`包的一個基準測試爲例。通常是基於一個已經確定了是關鍵代碼的部分進行基準測試。基準測試會默認包含單元測試,這里我們用-run=NONE參數禁止單元測試
下面的命令演示了如何生成一个CPU分析文件。我们选择`net/http`包的一个基准测试为例。通常是基于一个已经确定了是关键代码的部分进行基准测试。基准测试会默认包含单元测试,这里我们用-run=NONE参数禁止单元测试
```
$ go test -run=NONE -bench=ClientServerParallelTLS64 \
@ -57,10 +57,10 @@ Showing top 10 nodes out of 166 (cum >= 60ms)
50ms 1.39% 71.59% 60ms 1.67% crypto/elliptic.p256Sum
```
參數`-text`用於指定輸出格式在這里每行是一個函數根據使用CPU的時間長短來排序。其中`-nodecount=10`標誌參數限製了隻輸出前10行的結果。對於嚴重的性能問題這個文本格式基本可以幫助査明原因了。
参数`-text`用于指定输出格式在这里每行是一个函数根据使用CPU的时间长短来排序。其中`-nodecount=10`标志参数限制了只输出前10行的结果。对于严重的性能问题这个文本格式基本可以帮助查明原因了。
這個概要文件告訴我們HTTPS基準測試中`crypto/elliptic.p256ReduceDegree`函數占用了將近一半的CPU資源。相比之下如果一個概要文件中主要是runtime包的內存分配的函數那麽減少內存消耗可能是一個值得嚐試的優化策略。
这个概要文件告诉我们HTTPS基准测试中`crypto/elliptic.p256ReduceDegree`函数占用了将近一半的CPU资源。相比之下如果一个概要文件中主要是runtime包的内存分配的函数那么减少内存消耗可能是一个值得尝试的优化策略。
對於一些更微妙的問題你可能需要使用pprof的圖形顯示功能。這個需要安裝GraphViz工具可以從 http://www.graphviz.org 下載。參數`-web`用於生成一個有向圖文件包含了CPU的使用和最熱點的函數等信息。
对于一些更微妙的问题你可能需要使用pprof的图形显示功能。这个需要安装GraphViz工具可以从 http://www.graphviz.org 下载。参数`-web`用于生成一个有向图文件包含了CPU的使用和最热点的函数等信息。
這一節我們隻是簡單看了下Go語言的分析據工具。如果想了解更多可以閲讀Go官方博客的“Profiling Go Programs”一文。
这一节我们只是简单看了下Go语言的分析据工具。如果想了解更多可以阅读Go官方博客的“Profiling Go Programs”一文。

View File

@ -1,6 +1,6 @@
## 11.6. 示例函
## 11.6. 示例函
第三種`go test`特别處理的函數是示例函數以Example爲函數名開頭。示例函數沒有函數參數和返迴值。下面是IsPalindrome函數對應的示例函數
第三种`go test`特别处理的函数是示例函数以Example为函数名开头。示例函数没有函数参数和返回值。下面是IsPalindrome函数对应的示例函数
```Go
func ExampleIsPalindrome() {
@ -12,14 +12,14 @@ func ExampleIsPalindrome() {
}
```
示例函數有三個用處。最主要的一個是作爲文檔一個包的例子可以更簡潔直觀的方式來演示函數的用法比文字描述更直接易懂特别是作爲一個提醒或快速參考時。一個示例函數也可以方便展示屬於同一個接口的幾種類型或函數直接的關繫所有的文檔都必須關聯到一個地方就像一個類型或函數聲明都統一到包一樣。同時示例函數和註釋併不一樣示例函數是完整眞實的Go代碼需要接受編譯器的編譯時檢査這樣可以保證示例代碼不會腐爛成不能使用的舊代碼
示例函数有三个用处。最主要的一个是作为文档一个包的例子可以更简洁直观的方式来演示函数的用法比文字描述更直接易懂特别是作为一个提醒或快速参考时。一个示例函数也可以方便展示属于同一个接口的几种类型或函数直接的关系所有的文档都必须关联到一个地方就像一个类型或函数声明都统一到包一样。同时示例函数和注释并不一样示例函数是完整真实的Go代码需要接受编译器的编译时检查这样可以保证示例代码不会腐烂成不能使用的旧代码
據示例函數的後綴名部分godoc的web文檔會將一個示例函數關聯到某個具體函數或包本身因此ExampleIsPalindrome示例函數將是IsPalindrome函數文檔的一部分Example示例函數將是包文檔的一部分。
据示例函数的后缀名部分godoc的web文档会将一个示例函数关联到某个具体函数或包本身因此ExampleIsPalindrome示例函数将是IsPalindrome函数文档的一部分Example示例函数将是包文档的一部分。
示例文檔的第二個用處是在`go test`執行測試的時候也運行示例函數測試。如果示例函數內含有類似上面例子中的`// Output:`格式的註釋,那麽測試工具會執行這個示例函數,然後檢測這個示例函數的標準輸出和註釋是否匹配。
示例文档的第二个用处是在`go test`执行测试的时候也运行示例函数测试。如果示例函数内含有类似上面例子中的`// Output:`格式的注释,那么测试工具会执行这个示例函数,然后检测这个示例函数的标准输出和注释是否匹配。
示例函數的第三個目的提供一個眞實的演練場。 http://golang.org 就是由godoc提供的文檔服務它使用了Go Playground提高的技術讓用戶可以在瀏覽器中在線編輯和運行每個示例函數就像圖11.4所示的那樣。這通常是學習函數使用或Go語言特性最快捷的方式。
示例函数的第三个目的提供一个真实的演练场。 http://golang.org 就是由godoc提供的文档服务它使用了Go Playground提高的技术让用户可以在浏览器中在线编辑和运行每个示例函数就像图11.4所示的那样。这通常是学习函数使用或Go语言特性最快捷的方式。
![](../images/ch11-04.png)
書最後的兩掌是討論reflect和unsafe包一般的Go用戶很少直接使用它們。因此如果你還沒有寫過任何眞實的Go程序的話現在可以忽略剩餘部分而直接編碼了。
书最后的两掌是讨论reflect和unsafe包一般的Go用户很少直接使用它们。因此如果你还没有写过任何真实的Go程序的话现在可以忽略剩余部分而直接编码了。

View File

@ -1,13 +1,13 @@
# 第十一章 測試
# 第十一章 测试
Maurice Wilkes第一個存儲程序計算機EDSAC的設計者1949年他在實驗室爬樓梯時有一個頓悟。在《計算機先驅迴憶録》Memoirs of a Computer Pioneer他迴憶到“忽然間有一種醍醐灌頂的感覺我整個後半生的美好時光都將在尋找程序BUG中度過了”。肯定從那之後的大部分正常的碼農都會同情Wilkes過份悲觀的想法雖然也許不是沒有人睏惑於他對軟件開發的難度的天眞看法。
Maurice Wilkes第一个存储程序计算机EDSAC的设计者1949年他在实验室爬楼梯时有一个顿悟。在《计算机先驱回忆录》Memoirs of a Computer Pioneer他回忆到“忽然间有一种醍醐灌顶的感觉我整个后半生的美好时光都将在寻找程序BUG中度过了”。肯定从那之后的大部分正常的码农都会同情Wilkes过份悲观的想法虽然也许不是没有人困惑于他对软件开发的难度的天真看法。
現在的程序已經遠比Wilkes時代的更大也更複雜也有許多技術可以讓軟件的複雜性可得到控製。其中有兩種技術在實踐中證明是比較有效的。第一種是代碼在被正式部署前需要進行代碼評審。第二種則是測試也就是本章的討論主題
现在的程序已经远比Wilkes时代的更大也更复杂也有许多技术可以让软件的复杂性可得到控制。其中有两种技术在实践中证明是比较有效的。第一种是代码在被正式部署前需要进行代码评审。第二种则是测试也就是本章的讨论主题
們説測試的時候一般是指自動化測試,也就是寫一些小的程序用來檢測被測試代碼(産品代碼)的行爲和預期的一樣,這些通常都是精心設計的執行某些特定的功能或者是通過隨機性的輸入要驗證邊界的處理。
们说测试的时候一般是指自动化测试,也就是写一些小的程序用来检测被测试代码(产品代码)的行为和预期的一样,这些通常都是精心设计的执行某些特定的功能或者是通过随机性的输入要验证边界的处理。
軟件測試是一個鉅大的領域。測試的任務可能已經占據了一些程序員的部分時間和另一些程序員的全部時間。和軟件測試技術相關的圖書或博客文章有成韆上萬之多。對於每一種主流的編程語言,都會有一打的用於測試的軟件包,同時也有大量的測試相關的理論,而且每種都吸引了大量技術先驅和追隨者。這些都足以説服那些想要編寫有效測試的程序員重新學習一套全新的技能。
软件测试是一个巨大的领域。测试的任务可能已经占据了一些程序员的部分时间和另一些程序员的全部时间。和软件测试技术相关的图书或博客文章有成千上万之多。对于每一种主流的编程语言,都会有一打的用于测试的软件包,同时也有大量的测试相关的理论,而且每种都吸引了大量技术先驱和追随者。这些都足以说服那些想要编写有效测试的程序员重新学习一套全新的技能。
Go語言的測試技術是相對低級的。它依賴一個go test測試命令和一組按照約定方式編寫的測試函數測試命令可以運行這些測試函數。編寫相對輕量級的純測試代碼是有效的而且它很容易延伸到基準測試和示例文檔
Go语言的测试技术是相对低级的。它依赖一个go test测试命令和一组按照约定方式编写的测试函数测试命令可以运行这些测试函数。编写相对轻量级的纯测试代码是有效的而且它很容易延伸到基准测试和示例文档
實踐中編寫測試代碼和編寫程序本身併沒有多大區别。我們編寫的每一個函數也是針對每個具體的任務。我們必須小心處理邊界條件思考合適的數據結構推斷合適的輸入應該産生什麽樣的結果輸出。編程測試代碼和編寫普通的Go代碼過程是類似的它併不需要學習新的符號、規則和工具。
实践中编写测试代码和编写程序本身并没有多大区别。我们编写的每一个函数也是针对每个具体的任务。我们必须小心处理边界条件思考合适的数据结构推断合适的输入应该产生什么样的结果输出。编程测试代码和编写普通的Go代码过程是类似的它并不需要学习新的符号、规则和工具。

View File

@ -1,10 +1,10 @@
## 12.1. 何需要反射?
## 12.1. 何需要反射?
時候我們需要編寫一個函數能夠處理一類併不滿足普通公共接口的類型的值,也可能是因爲它們併沒有確定的表示方式,或者是在我們設計該函數的時候還這些類型可能還不存在,各種情況都有可能。
时候我们需要编写一个函数能够处理一类并不满足普通公共接口的类型的值,也可能是因为它们并没有确定的表示方式,或者是在我们设计该函数的时候还这些类型可能还不存在,各种情况都有可能。
個大家熟悉的例子是fmt.Fprintf函數提供的字符串格式化處理邏輯它可以用例對任意類型的值格式化併打印甚至支持用戶自定義的類型。讓我們也來嚐試實現一個類似功能的函數。爲了簡單起見我們的函數隻接收一個參數然後返迴和fmt.Sprint類似的格式化後的字符串。我們實現的函數名也叫Sprint。
个大家熟悉的例子是fmt.Fprintf函数提供的字符串格式化处理逻辑它可以用例对任意类型的值格式化并打印甚至支持用户自定义的类型。让我们也来尝试实现一个类似功能的函数。为了简单起见我们的函数只接收一个参数然后返回和fmt.Sprint类似的格式化后的字符串。我们实现的函数名也叫Sprint。
們使用了switch類型分支首先來測試輸入參數是否實現了String方法如果是的話就使用該方法。然後繼續增加類型測試分支檢査是否是每個基於string、int、bool等基礎類型的動態類型併在每種情況下執行相應的格式化操作。
们使用了switch类型分支首先来测试输入参数是否实现了String方法如果是的话就使用该方法。然后继续增加类型测试分支检查是否是每个基于string、int、bool等基础类型的动态类型并在每种情况下执行相应的格式化操作。
```Go
func Sprint(x interface{}) string {
@ -31,6 +31,6 @@ func Sprint(x interface{}) string {
}
```
但是我們如何處理其它類似[]float64、map[string][]string等類型呢我們當然可以添加更多的測試分支但是這些組合類型的數目基本是無窮的。還有如何處理url.Values等命名的類型呢雖然類型分支可以識别出底層的基礎類型是map[string][]string但是它併不匹配url.Values類型因爲它們是兩種不同的類型而且switch類型分支也不可能包含每個類似url.Values的類型這會導致對這些庫的循環依賴
但是我们如何处理其它类似[]float64、map[string][]string等类型呢我们当然可以添加更多的测试分支但是这些组合类型的数目基本是无穷的。还有如何处理url.Values等命名的类型呢虽然类型分支可以识别出底层的基础类型是map[string][]string但是它并不匹配url.Values类型因为它们是两种不同的类型而且switch类型分支也不可能包含每个类似url.Values的类型这会导致对这些库的循环依赖
沒有一種方法來檢査未知類型的表示方式,我們被卡住了。這就是我們爲何需要反射的原因。
没有一种方法来检查未知类型的表示方式,我们被卡住了。这就是我们为何需要反射的原因。

View File

@ -1,8 +1,8 @@
## 12.2. reflect.Type和reflect.Value
反射是由 reflect 包提供支持. 它定義了兩個重要的類型, Type 和 Value. 一個 Type 表示一個Go類型. 它是一個接口, 有許多方法來區分類型和檢査它們的組件, 例如一個結構體的成員或一個函數的參數等. 唯一能反映 reflect.Type 實現的是接口的類型描述信息(§7.5), 同樣的實體標識了動態類型的接口值.
反射是由 reflect 包提供支持. 它定义了两个重要的类型, Type 和 Value. 一个 Type 表示一个Go类型. 它是一个接口, 有许多方法来区分类型和检查它们的组件, 例如一个结构体的成员或一个函数的参数等. 唯一能反映 reflect.Type 实现的是接口的类型描述信息(§7.5), 同样的实体标识了动态类型的接口值.
數 reflect.TypeOf 接受任意的 interface{} 類型, 併返迴對應動態類型的reflect.Type:
数 reflect.TypeOf 接受任意的 interface{} 类型, 并返回对应动态类型的reflect.Type:
```Go
t := reflect.TypeOf(3) // a reflect.Type
@ -10,22 +10,22 @@ fmt.Println(t.String()) // "int"
fmt.Println(t) // "int"
```
其中 TypeOf(3) 調用將值 3 作爲 interface{} 類型參數傳入. 迴到 7.5節 的將一個具體的值轉爲接口類型會有一個隱式的接口轉換操作, 它會創建一個包含兩個信息的接口值: 操作數的動態類型(這里是int)和它的動態的值(這里是3).
其中 TypeOf(3) 调用将值 3 作为 interface{} 类型参数传入. 回到 7.5节 的将一个具体的值转为接口类型会有一个隐式的接口转换操作, 它会创建一个包含两个信息的接口值: 操作数的动态类型(这里是int)和它的动态的值(这里是3).
爲 reflect.TypeOf 返迴的是一個動態類型的接口值, 它總是返迴具體的類型. 因此, 下面的代碼將打印 "*os.File" 而不是 "io.Writer". 稍後, 我們將看到 reflect.Type 是具有識别接口類型的表達方式功能的.
为 reflect.TypeOf 返回的是一个动态类型的接口值, 它总是返回具体的类型. 因此, 下面的代码将打印 "*os.File" 而不是 "io.Writer". 稍后, 我们将看到 reflect.Type 是具有识别接口类型的表达方式功能的.
```Go
var w io.Writer = os.Stdout
fmt.Println(reflect.TypeOf(w)) // "*os.File"
```
註意的是 reflect.Type 接口是滿足 fmt.Stringer 接口的. 因爲打印動態類型值對於調試和日誌是有幫助的, fmt.Printf 提供了一個簡短的 %T 標誌參數, 內部使用 reflect.TypeOf 的結果輸出:
注意的是 reflect.Type 接口是满足 fmt.Stringer 接口的. 因为打印动态类型值对于调试和日志是有帮助的, fmt.Printf 提供了一个简短的 %T 标志参数, 内部使用 reflect.TypeOf 的结果输出:
```Go
fmt.Printf("%T\n", 3) // "int"
```
reflect 包中另一個重要的類型是 Value. 一個 reflect.Value 可以持有一個任意類型的值. 函數 reflect.ValueOf 接受任意的 interface{} 類型, 併返迴對應動態類型的reflect.Value. 和 reflect.TypeOf 類似, reflect.ValueOf 返迴的結果也是對於具體的類型, 但是 reflect.Value 也可以持有一個接口值.
reflect 包中另一个重要的类型是 Value. 一个 reflect.Value 可以持有一个任意类型的值. 函数 reflect.ValueOf 接受任意的 interface{} 类型, 并返回对应动态类型的reflect.Value. 和 reflect.TypeOf 类似, reflect.ValueOf 返回的结果也是对于具体的类型, 但是 reflect.Value 也可以持有一个接口值.
```Go
v := reflect.ValueOf(3) // a reflect.Value
@ -34,16 +34,16 @@ fmt.Printf("%v\n", v) // "3"
fmt.Println(v.String()) // NOTE: "<int Value>"
```
和 reflect.Type 類似, reflect.Value 也滿足 fmt.Stringer 接口, 但是除非 Value 持有的是字符串, 否則 String 隻是返迴具體的類型. 相同, 使用 fmt 包的 %v 標誌參數, 將使用 reflect.Values 的結果格式化.
和 reflect.Type 类似, reflect.Value 也满足 fmt.Stringer 接口, 但是除非 Value 持有的是字符串, 否则 String 只是返回具体的类型. 相同, 使用 fmt 包的 %v 标志参数, 将使用 reflect.Values 的结果格式化.
調用 Value 的 Type 方法將返迴具體類型所對應的 reflect.Type:
调用 Value 的 Type 方法将返回具体类型所对应的 reflect.Type:
```Go
t := v.Type() // a reflect.Type
fmt.Println(t.String()) // "int"
```
逆操作是調用 reflect.ValueOf 對應的 reflect.Value.Interface 方法. 它返迴一個 interface{} 類型表示 reflect.Value 對應類型的具體值:
逆操作是调用 reflect.ValueOf 对应的 reflect.Value.Interface 方法. 它返回一个 interface{} 类型表示 reflect.Value 对应类型的具体值:
```Go
v := reflect.ValueOf(3) // a reflect.Value
@ -52,9 +52,9 @@ i := x.(int) // an int
fmt.Printf("%d\n", i) // "3"
```
個 reflect.Value 和 interface{} 都能保存任意的值. 所不同的是, 一個空的接口隱藏了值對應的表示方式和所有的公開的方法, 因此隻有我們知道具體的動態類型才能使用類型斷言來訪問內部的值(就像上面那樣), 對於內部值併沒有特别可做的事情. 相比之下, 一個 Value 則有很多方法來檢査其內容, 無論它的具體類型是什麽. 讓我們再次嚐試實現我們的格式化函數 format.Any.
个 reflect.Value 和 interface{} 都能保存任意的值. 所不同的是, 一个空的接口隐藏了值对应的表示方式和所有的公开的方法, 因此只有我们知道具体的动态类型才能使用类型断言来访问内部的值(就像上面那样), 对于内部值并没有特别可做的事情. 相比之下, 一个 Value 则有很多方法来检查其内容, 无论它的具体类型是什么. 让我们再次尝试实现我们的格式化函数 format.Any.
們使用 reflect.Value 的 Kind 方法來替代之前的類型 switch. 雖然還是有無窮多的類型, 但是它們的kinds類型卻是有限的: Bool, String 和 所有數字類型的基礎類型; Array 和 Struct 對應的聚合類型; Chan, Func, Ptr, Slice, 和 Map 對應的引用類似; 接口類型; 還有表示空值的無效類型. (空的 reflect.Value 對應 Invalid 無效類型.)
们使用 reflect.Value 的 Kind 方法来替代之前的类型 switch. 虽然还是有无穷多的类型, 但是它们的kinds类型却是有限的: Bool, String 和 所有数字类型的基础类型; Array 和 Struct 对应的聚合类型; Chan, Func, Ptr, Slice, 和 Map 对应的引用类似; 接口类型; 还有表示空值的无效类型. (空的 reflect.Value 对应 Invalid 无效类型.)
<u><i>gopl.io/ch12/format</i></u>
```Go
@ -95,7 +95,7 @@ func formatAtom(v reflect.Value) string {
}
```
到目前爲止, 我們的函數將每個值視作一個不可分割沒有內部結構的, 因此它叫 formatAtom. 對於聚合類型(結構體和數組)個接口隻是打印類型的值, 對於引用類型(channels, functions, pointers, slices, 和 maps), 它十六進製打印類型的引用地址. 雖然還不夠理想, 但是依然是一個重大的進步, 併且 Kind 隻關心底層表示, format.Any 也支持新命名的類型. 例如:
到目前为止, 我们的函数将每个值视作一个不可分割没有内部结构的, 因此它叫 formatAtom. 对于聚合类型(结构体和数组)个接口只是打印类型的值, 对于引用类型(channels, functions, pointers, slices, 和 maps), 它十六进制打印类型的引用地址. 虽然还不够理想, 但是依然是一个重大的进步, 并且 Kind 只关心底层表示, format.Any 也支持新命名的类型. 例如:
```Go
var x int64 = 1

View File

@ -1,13 +1,13 @@
## 12.3. Display遞歸打印
## 12.3. Display递归打印
接下讓我們看看如何改善聚合數據類型的顯示。我們併不想完全剋隆一個fmt.Sprint函數我們隻是像構建一個用於調式用的Display函數給定一個聚合類型x打印這個值對應的完整的結構同時記録每個發現的每個元素的路徑。讓我們從一個例子開始。
接下让我们看看如何改善聚合数据类型的显示。我们并不想完全克隆一个fmt.Sprint函数我们只是像构建一个用于调式用的Display函数给定一个聚合类型x打印这个值对应的完整的结构同时记录每个发现的每个元素的路径。让我们从一个例子开始。
```Go
e, _ := eval.Parse("sqrt(A / pi)")
Display("e", e)
```
在上面的調用中傳入Display函數的參數是在7.9節一個表達式求值函數返迴的語法樹。Display函數的輸出如下:
在上面的调用中传入Display函数的参数是在7.9节一个表达式求值函数返回的语法树。Display函数的输出如下:
```Go
Display e (eval.call):
@ -20,7 +20,7 @@ e.args[0].value.y.type = eval.Var
e.args[0].value.y.value = "pi"
```
在可能的情況下你應該避免在一個包中暴露和反射相關的接口。我們將定義一個未導出的display函數用於遞歸處理工作導出的是Display函數它隻是display函數簡單的包裝以接受interface{}類型的參數
在可能的情况下你应该避免在一个包中暴露和反射相关的接口。我们将定义一个未导出的display函数用于递归处理工作导出的是Display函数它只是display函数简单的包装以接受interface{}类型的参数
<u><i>gopl.io/ch12/display</i></u>
```Go
@ -30,9 +30,9 @@ func Display(name string, x interface{}) {
}
```
在display函數中我們使用了前面定義的打印基礎類型——基本類型、函數和chan等——元素值的formatAtom函數但是我們會使用reflect.Value的方法來遞歸顯示聚合類型的每一個成員或元素。在遞歸下降過程中path字符串從最開始傳入的起始值這里是“e”將逐步增長以表示如何達到當前值例如“e.args[0].value”
在display函数中我们使用了前面定义的打印基础类型——基本类型、函数和chan等——元素值的formatAtom函数但是我们会使用reflect.Value的方法来递归显示聚合类型的每一个成员或元素。在递归下降过程中path字符串从最开始传入的起始值这里是“e”将逐步增长以表示如何达到当前值例如“e.args[0].value”
爲我們不再模擬fmt.Sprint函數我們將直接使用fmt包來簡化我們的例子實現
为我们不再模拟fmt.Sprint函数我们将直接使用fmt包来简化我们的例子实现
```Go
func display(path string, v reflect.Value) {
@ -72,21 +72,21 @@ func display(path string, v reflect.Value) {
}
```
讓我們針對不同類型分别討論
让我们针对不同类型分别讨论
**Slice和數組:** 兩種的處理邏輯是一樣的。Len方法返迴slice或數組值中的元素個數Index(i)活動索引i對應的元素返迴的也是一個reflect.Value類型的值如果索引i超出范圍的話將導致panic異常這些行爲和數組或slice類型內建的len(a)和a[i]等操作類似。display針對序列中的每個元素遞歸調用自身處理我們通過在遞歸處理時向path附加“[i]”來表示訪問路徑
**Slice和数组:** 两种的处理逻辑是一样的。Len方法返回slice或数组值中的元素个数Index(i)活动索引i对应的元素返回的也是一个reflect.Value类型的值如果索引i超出范围的话将导致panic异常这些行为和数组或slice类型内建的len(a)和a[i]等操作类似。display针对序列中的每个元素递归调用自身处理我们通过在递归处理时向path附加“[i]”来表示访问路径
雖然reflect.Value類型帶有很多方法但是隻有少數的方法對任意值都是可以安全調用的。例如Index方法隻能對Slice、數組或字符串類型的值調用其它類型如果調用將導致panic異常。
虽然reflect.Value类型带有很多方法但是只有少数的方法对任意值都是可以安全调用的。例如Index方法只能对Slice、数组或字符串类型的值调用其它类型如果调用将导致panic异常。
**結構體:** NumField方法報告結構體中成員的數量Field(i)以reflect.Value類型返迴第i個成員的值。成員列表包含了匿名成員在內的全部成員。通過在path添加“.f”來表示成員路徑我們必須獲得結構體對應的reflect.Type類型信息包含結構體類型和第i個成員的名字。
**结构体:** NumField方法报告结构体中成员的数量Field(i)以reflect.Value类型返回第i个成员的值。成员列表包含了匿名成员在内的全部成员。通过在path添加“.f”来表示成员路径我们必须获得结构体对应的reflect.Type类型信息包含结构体类型和第i个成员的名字。
**Maps:** MapKeys方法返迴一個reflect.Value類型的slice每一個都對應map的可以。和往常一樣遍歷map時順序是隨機的。MapIndex(key)返迴map中key對應的value。我們向path添加“[key]”來表示訪問路徑。我們這里有一個未完成的工作。其實map的key的類型併不局限於formatAtom能完美處理的類型數組、結構體和接口都可以作爲map的key。針對這種類型完善key的顯示信息是練習12.1的任務。)
**Maps:** MapKeys方法返回一个reflect.Value类型的slice每一个都对应map的可以。和往常一样遍历map时顺序是随机的。MapIndex(key)返回map中key对应的value。我们向path添加“[key]”来表示访问路径。我们这里有一个未完成的工作。其实map的key的类型并不局限于formatAtom能完美处理的类型数组、结构体和接口都可以作为map的key。针对这种类型完善key的显示信息是练习12.1的任务。)
**指針:** Elem方法返迴指針指向的變量還是reflect.Value類型。技術指針是nil這個操作也是安全的在這種情況下指針是Invalid無效類型但是我們可以用IsNil方法來顯式地測試一個空指針這樣我們可以打印更合適的信息。我們在path前面添加“*”,併用括弧包含以避免歧義
**指针:** Elem方法返回指针指向的变量还是reflect.Value类型。技术指针是nil这个操作也是安全的在这种情况下指针是Invalid无效类型但是我们可以用IsNil方法来显式地测试一个空指针这样我们可以打印更合适的信息。我们在path前面添加“*”,并用括弧包含以避免歧义
**接口:** 再一次,我們使用IsNil方法來測試接口是否是nil如果不是我們可以調用v.Elem()來獲取接口對應的動態值,併且打印對應的類型和值。
**接口:** 再一次,我们使用IsNil方法来测试接口是否是nil如果不是我们可以调用v.Elem()来获取接口对应的动态值,并且打印对应的类型和值。
現在我們的Display函數總算完工了讓我們看看它的表現吧。下面的Movie類型是在4.5節的電影類型上演變來的:
现在我们的Display函数总算完工了让我们看看它的表现吧。下面的Movie类型是在4.5节的电影类型上演变来的:
```Go
type Movie struct {
@ -99,7 +99,7 @@ type Movie struct {
}
```
讓我們聲明一個該類型的變量然後看看Display函數如何顯示它:
让我们声明一个该类型的变量然后看看Display函数如何显示它:
```Go
strangelove := Movie{
@ -125,7 +125,7 @@ strangelove := Movie{
}
```
Display("strangelove", strangelove)調用將顯示strangelove電影對應的中文名是《奇愛博士》):
Display("strangelove", strangelove)调用将显示strangelove电影对应的中文名是《奇爱博士》):
```Go
Display strangelove (display.Movie):
@ -146,7 +146,7 @@ strangelove.Oscars[3] = "Best Picture (Nomin.)"
strangelove.Sequel = nil
```
們也可以使用Display函數來顯示標準庫中類型的內部結構例如`*os.File`類型:
们也可以使用Display函数来显示标准库中类型的内部结构例如`*os.File`类型:
```Go
Display("os.Stderr", os.Stderr)
@ -157,7 +157,7 @@ Display("os.Stderr", os.Stderr)
// (*(*os.Stderr).file).nepipe = 0
```
註意的是結構體中未導出的成員對反射也是可見的。需要當心的是這個例子的輸出在不同操作繫統上可能是不同的併且隨着標準庫的發展也可能導致結果不同。這也是將這些成員定義爲私有成員的原因之一我們深圳可以用Display函數來顯示reflect.Value來査看`*os.File`類型的內部表示方式。`Display("rV", reflect.ValueOf(os.Stderr))`調用的輸出如下,當然不同環境得到的結果可能有差異
注意的是结构体中未导出的成员对反射也是可见的。需要当心的是这个例子的输出在不同操作系统上可能是不同的并且随着标准库的发展也可能导致结果不同。这也是将这些成员定义为私有成员的原因之一我们深圳可以用Display函数来显示reflect.Value来查看`*os.File`类型的内部表示方式。`Display("rV", reflect.ValueOf(os.Stderr))`调用的输出如下,当然不同环境得到的结果可能有差异
```Go
Display rV (reflect.Value):
@ -174,7 +174,7 @@ Display rV (reflect.Value):
...
```
觀察下面兩個例子的區别:
观察下面两个例子的区别:
```Go
var i interface{} = 3
@ -191,11 +191,11 @@ Display("&i", &i)
// (*&i).value = 3
```
在第一個例子中Display函數將調用reflect.ValueOf(i)它返迴一個Int類型的值。正如我們在12.2節中提到的reflect.ValueOf總是返迴一個值的具體類型因爲它是從一個接口值提取的內容。
在第一个例子中Display函数将调用reflect.ValueOf(i)它返回一个Int类型的值。正如我们在12.2节中提到的reflect.ValueOf总是返回一个值的具体类型因为它是从一个接口值提取的内容。
在第二個例子中Display函數調用的是reflect.ValueOf(&i)它返迴一個指向i的指針對應Ptr類型。在switch的Ptr分支中通過調用Elem來返迴這個值返迴一個Value來表示i對應Interface類型。一個間接獲得的Value就像這一個可能代表任意類型的值包括接口類型。內部的display函數遞歸調用自身這次它將打印接口的動態類型和值。
在第二个例子中Display函数调用的是reflect.ValueOf(&i)它返回一个指向i的指针对应Ptr类型。在switch的Ptr分支中通过调用Elem来返回这个值返回一个Value来表示i对应Interface类型。一个间接获得的Value就像这一个可能代表任意类型的值包括接口类型。内部的display函数递归调用自身这次它将打印接口的动态类型和值。
目前的實現Display如果顯示一個帶環的數據結構將會陷入死循環例如首位項鏈的鏈表:
目前的实现Display如果显示一个带环的数据结构将会陷入死循环例如首位项链的链表:
```Go
// a struct that points to itself
@ -205,7 +205,7 @@ c = Cycle{42, &c}
Display("c", c)
```
Display會永遠不停地進行深度遞歸打印:
Display会永远不停地进行深度递归打印:
```Go
Display c (display.Cycle):
@ -216,10 +216,10 @@ c.Value = 42
...ad infinitum...
```
許多Go語言程序都包含了一些循環的數據結果。Display支持這類帶環的數據結構是比較棘手的需要增加一個額外的記録訪問的路徑代價是昂貴的。一般的解決方案是采用不安全的語言特性我們將在13.3節看到具體的解決方案。
许多Go语言程序都包含了一些循环的数据结果。Display支持这类带环的数据结构是比较棘手的需要增加一个额外的记录访问的路径代价是昂贵的。一般的解决方案是采用不安全的语言特性我们将在13.3节看到具体的解决方案。
帶環的數據結構很少會對fmt.Sprint函數造成問題因爲它很少嚐試打印完整的數據結構。例如當它遇到一個指針的時候它隻是簡單第打印指針的數值。雖然在打印包含自身的slice或map時可能遇到睏難但是不保證處理這種是罕見情況卻可以避免額外的麻煩
带环的数据结构很少会对fmt.Sprint函数造成问题因为它很少尝试打印完整的数据结构。例如当它遇到一个指针的时候它只是简单第打印指针的数值。虽然在打印包含自身的slice或map时可能遇到困难但是不保证处理这种是罕见情况却可以避免额外的麻烦
**練習 12.1** 擴展Displayhans以便它可以顯示包含以結構體或數組作爲map的key類型的值。
**练习 12.1** 扩展Displayhans以便它可以显示包含以结构体或数组作为map的key类型的值。
**練習 12.2** 增強display函數的穩健性通過記録邊界的步數來確保在超出一定限製前放棄遞歸。在13.3節,我們會看到另一種探測數據結構是否存在環的技術。)
**练习 12.2** 增强display函数的稳健性通过记录边界的步数来确保在超出一定限制前放弃递归。在13.3节,我们会看到另一种探测数据结构是否存在环的技术。)

View File

@ -1,10 +1,10 @@
## 12.4. 示例: 編碼S表達
## 12.4. 示例: 编码S表达
Display是一個用於顯示結構化數據的調試工具但是它併不能將任意的Go語言對象編碼爲通用消息然後用於進程間通信。
Display是一个用于显示结构化数据的调试工具但是它并不能将任意的Go语言对象编码为通用消息然后用于进程间通信。
正如我們在4.5節中中看到的Go語言的標準庫支持了包括JSON、XML和ASN.1等多種編碼格式。還有另一種依然被廣泛使用的格式是S表達式格式采用類似Lisp語言的語法。但是和其他編碼格式不同的是Go語言自帶的標準庫併不支持S表達式主要是因爲它沒有一個公認的標準規范。
正如我们在4.5节中中看到的Go语言的标准库支持了包括JSON、XML和ASN.1等多种编码格式。还有另一种依然被广泛使用的格式是S表达式格式采用类似Lisp语言的语法。但是和其他编码格式不同的是Go语言自带的标准库并不支持S表达式主要是因为它没有一个公认的标准规范。
在本節中我們將定義一個包用於將Go語言的對象編碼爲S表達式格式它支持以下結構
在本节中我们将定义一个包用于将Go语言的对象编码为S表达式格式它支持以下结构
```
42 integer
@ -13,13 +13,13 @@ foo symbol (an unquoted name)
(1 2 3) list (zero or more items enclosed in parentheses)
```
爾型習慣上使用t符號表示true空列表或nil符號表示false但是爲了簡單起見我們暫時忽略布爾類型。同時忽略的還有chan管道和函數因爲通過反射併無法知道它們的確切狀態。我們忽略的還浮點數、複數和interface。支持它們是練習12.3的任務
尔型习惯上使用t符号表示true空列表或nil符号表示false但是为了简单起见我们暂时忽略布尔类型。同时忽略的还有chan管道和函数因为通过反射并无法知道它们的确切状态。我们忽略的还浮点数、复数和interface。支持它们是练习12.3的任务
們將Go語言的類型編碼爲S表達式的方法如下。整數和字符串以自然的方式編碼。Nil值編碼爲nil符號。數組和slice被編碼爲一個列表。
们将Go语言的类型编码为S表达式的方法如下。整数和字符串以自然的方式编码。Nil值编码为nil符号。数组和slice被编码为一个列表。
結構體被編碼爲成員對象的列表每個成員對象對應一個個僅有兩個元素的子列表其中子列表的第一個元素是成員的名字子列表的第二個元素是成員的值。Map被編碼爲鍵值對的列表。傳統上S表達式使用點狀符號列表(key . value)結構來表示key/value對而不是用一個含雙元素的列表不過爲了簡單我們忽略了點狀符號列表。
结构体被编码为成员对象的列表每个成员对象对应一个个仅有两个元素的子列表其中子列表的第一个元素是成员的名字子列表的第二个元素是成员的值。Map被编码为键值对的列表。传统上S表达式使用点状符号列表(key . value)结构来表示key/value对而不是用一个含双元素的列表不过为了简单我们忽略了点状符号列表。
編碼是由一個encode遞歸函數完成如下所示。它的結構本質上和前面的Display函數類似:
编码是由一个encode递归函数完成如下所示。它的结构本质上和前面的Display函数类似:
<u><i>gopl.io/ch12/sexpr</i></u>
```Go
@ -93,7 +93,7 @@ func encode(buf *bytes.Buffer, v reflect.Value) error {
}
```
Marshal函數是對encode的保證以保持和encoding/...下其它包有着相似的API
Marshal函数是对encode的保证以保持和encoding/...下其它包有着相似的API
```Go
// Marshal encodes a Go value in S-expression form.
@ -106,7 +106,7 @@ func Marshal(v interface{}) ([]byte, error) {
}
```
下面是Marshal對12.3節的strangelove變量編碼後的結果:
下面是Marshal对12.3节的strangelove变量编码后的结果:
```
((Title "Dr. Strangelove") (Subtitle "How I Learned to Stop Worrying and Lo
@ -118,7 +118,7 @@ ge C. Scott") ("Brig. Gen. Jack D. Ripper" "Sterling Hayden") ("Maj. T.J. \
omin.)" "Best Picture (Nomin.)")) (Sequel nil))
```
個輸出編碼爲一行中以減少輸出的大小但是也很難閲讀。這里有一個對S表達式格式化的約定。編寫一個S表達式的格式化函數將作爲一個具有挑戰性的練習任務不過 http://gopl.io 也提供了一個簡單的版本。
个输出编码为一行中以减少输出的大小但是也很难阅读。这里有一个对S表达式格式化的约定。编写一个S表达式的格式化函数将作为一个具有挑战性的练习任务不过 http://gopl.io 也提供了一个简单的版本。
```
((Title "Dr. Strangelove")
@ -137,16 +137,16 @@ omin.)" "Best Picture (Nomin.)")) (Sequel nil))
(Sequel nil))
```
和fmt.Print、json.Marshal、Display函數類似sexpr.Marshal函數處理帶環的數據結構也會陷入死循環
和fmt.Print、json.Marshal、Display函数类似sexpr.Marshal函数处理带环的数据结构也会陷入死循环
在12.6節中我們將給出S表達式解碼器的實現步驟但是在那之前我們還需要先了解如果通過反射技術來更新程序的變量。
在12.6节中我们将给出S表达式解码器的实现步骤但是在那之前我们还需要先了解如果通过反射技术来更新程序的变量。
**練習 12.3** 實現encode函數缺少的分支。將布爾類型編碼爲t和nil浮點數編碼爲Go語言的格式複數1+2i編碼爲#C(1.0 2.0)格式。接口編碼爲類型名和值對,例如("[]int" (1 2 3))但是這個形式可能會造成歧義reflect.Type.String方法對於不同的類型可能返迴相同的結果。
**练习 12.3** 实现encode函数缺少的分支。将布尔类型编码为t和nil浮点数编码为Go语言的格式复数1+2i编码为#C(1.0 2.0)格式。接口编码为类型名和值对,例如("[]int" (1 2 3))但是这个形式可能会造成歧义reflect.Type.String方法对于不同的类型可能返回相同的结果。
**練習 12.4** 脩改encode函數以上面的格式化形式輸出S表達式。
**练习 12.4** 修改encode函数以上面的格式化形式输出S表达式。
**練習 12.5** 脩改encode函數用JSON格式代替S表達式格式。然後使用標準庫提供的json.Unmarshal解碼器來驗證函數是正確的。
**练习 12.5** 修改encode函数用JSON格式代替S表达式格式。然后使用标准库提供的json.Unmarshal解码器来验证函数是正确的。
**練習 12.6** 脩改encode作爲一個優化忽略對是零值對象的編碼
**练习 12.6** 修改encode作为一个优化忽略对是零值对象的编码
**練習 12.7** 創建一個基於流式的API用於S表達式的解碼和json.Decoder(§4.5)函數功能類似。
**练习 12.7** 创建一个基于流式的API用于S表达式的解码和json.Decoder(§4.5)函数功能类似。

View File

@ -1,10 +1,10 @@
## 12.5. 通過reflect.Value脩改值
## 12.5. 通过reflect.Value修改值
到目前爲止,反射還隻是程序中變量的另一種訪問方式。然而,在本節中我們將重點討論如果通過反射機製來脩改變量。
到目前为止,反射还只是程序中变量的另一种访问方式。然而,在本节中我们将重点讨论如果通过反射机制来修改变量。
迴想一下Go語言中類似x、x.f[1]和*p形式的表達式都可以表示變量但是其它如x + 1和f(2)則不是變量。一個變量就是一個可尋址的內存空間,里面存儲了一個值,併且存儲的值可以通過內存地址來更新。
回想一下Go语言中类似x、x.f[1]和*p形式的表达式都可以表示变量但是其它如x + 1和f(2)则不是变量。一个变量就是一个可寻址的内存空间,里面存储了一个值,并且存储的值可以通过内存地址来更新。
對於reflect.Values也有類似的區别。有一些reflect.Values是可取地址的其它一些則不可以。考慮以下的聲明語句:
对于reflect.Values也有类似的区别。有一些reflect.Values是可取地址的其它一些则不可以。考虑以下的声明语句:
```Go
x := 2 // value type variable?
@ -14,9 +14,9 @@ c := reflect.ValueOf(&x) // &x *int no
d := c.Elem() // 2 int yes (x)
```
其中a對應的變量則不可取地址。因爲a中的值僅僅是整數2的拷貝副本。b中的值也同樣不可取地址。c中的值還是不可取地址它隻是一個指針`&x`的拷貝。實際上所有通過reflect.ValueOf(x)返迴的reflect.Value都是不可取地址的。但是對於d它是c的解引用方式生成的指向另一個變量因此是可取地址的。我們可以通過調用reflect.ValueOf(&x).Elem()來獲取任意變量x對應的可取地址的Value。
其中a对应的变量则不可取地址。因为a中的值仅仅是整数2的拷贝副本。b中的值也同样不可取地址。c中的值还是不可取地址它只是一个指针`&x`的拷贝。实际上所有通过reflect.ValueOf(x)返回的reflect.Value都是不可取地址的。但是对于d它是c的解引用方式生成的指向另一个变量因此是可取地址的。我们可以通过调用reflect.ValueOf(&x).Elem()来获取任意变量x对应的可取地址的Value。
們可以通過調用reflect.Value的CanAddr方法來判斷其是否可以被取地址:
们可以通过调用reflect.Value的CanAddr方法来判断其是否可以被取地址:
```Go
fmt.Println(a.CanAddr()) // "false"
@ -25,9 +25,9 @@ fmt.Println(c.CanAddr()) // "false"
fmt.Println(d.CanAddr()) // "true"
```
當我們通過指針間接地獲取的reflect.Value都是可取地址的卽使開始的是一個不可取地址的Value。在反射機製中所有關於是否支持取地址的規則都是類似的。例如slice的索引表達式e[i]將隱式地包含一個指針它就是可取地址的卽使開始的e表達式不支持也沒有關繫。以此類推reflect.ValueOf(e).Index(i)對於的值也是可取地址的卽使原始的reflect.ValueOf(e)不支持也沒有關繫
当我们通过指针间接地获取的reflect.Value都是可取地址的即使开始的是一个不可取地址的Value。在反射机制中所有关于是否支持取地址的规则都是类似的。例如slice的索引表达式e[i]将隐式地包含一个指针它就是可取地址的即使开始的e表达式不支持也没有关系。以此类推reflect.ValueOf(e).Index(i)对于的值也是可取地址的即使原始的reflect.ValueOf(e)不支持也没有关系
從變量對應的可取地址的reflect.Value來訪問變量需要三個步驟。第一步是調用Addr()方法它返迴一個Value里面保存了指向變量的指針。然後是在Value上調用Interface()方法也就是返迴一個interface{}里面通用包含指向變量的指針。最後如果我們知道變量的類型我們可以使用類型的斷言機製將得到的interface{}類型的接口強製環爲普通的類型指針。這樣我們就可以通過這個普通指針來更新變量了:
从变量对应的可取地址的reflect.Value来访问变量需要三个步骤。第一步是调用Addr()方法它返回一个Value里面保存了指向变量的指针。然后是在Value上调用Interface()方法也就是返回一个interface{}里面通用包含指向变量的指针。最后如果我们知道变量的类型我们可以使用类型的断言机制将得到的interface{}类型的接口强制环为普通的类型指针。这样我们就可以通过这个普通指针来更新变量了:
```Go
x := 2
@ -37,20 +37,20 @@ px := d.Addr().Interface().(*int) // px := &x
fmt.Println(x) // "3"
```
或者,不使用指而是通過調用可取地址的reflect.Value的reflect.Value.Set方法來更新對於的值:
或者,不使用指而是通过调用可取地址的reflect.Value的reflect.Value.Set方法来更新对于的值:
```Go
d.Set(reflect.ValueOf(4))
fmt.Println(x) // "4"
```
Set方法將在運行時執行和編譯時類似的可賦值性約束的檢査。以上代碼變量和值都是int類型但是如果變量是int64類型那麽程序將拋出一個panic異常所以關鍵問題是要確保改類型的變量可以接受對應的值:
Set方法将在运行时执行和编译时类似的可赋值性约束的检查。以上代码变量和值都是int类型但是如果变量是int64类型那么程序将抛出一个panic异常所以关键问题是要确保改类型的变量可以接受对应的值:
```Go
d.Set(reflect.ValueOf(int64(5))) // panic: int64 is not assignable to int
```
通用對一個不可取地址的reflect.Value調用Set方法也會導致panic異常:
通用对一个不可取地址的reflect.Value调用Set方法也会导致panic异常:
```Go
x := 2
@ -58,7 +58,7 @@ b := reflect.ValueOf(x)
b.Set(reflect.ValueOf(3)) // panic: Set using unaddressable value
```
這里有很多用於基本數據類型的Set方法SetInt、SetUint、SetString和SetFloat等。
这里有很多用于基本数据类型的Set方法SetInt、SetUint、SetString和SetFloat等。
```Go
d := reflect.ValueOf(&x).Elem()
@ -66,7 +66,7 @@ d.SetInt(3)
fmt.Println(x) // "3"
```
從某種程度上説這些Set方法總是盡可能地完成任務。以SetInt爲例隻要變量是某種類型的有符號整數就可以工作卽使是一些命名的類型隻要底層數據類型是有符號整數就可以而且如果對於變量類型值太大的話會被自動截斷。但需要謹慎的是對於一個引用interface{}類型的reflect.Value調用SetInt會導致panic異常卽使那個interface{}變量對於整數類型也不行。
从某种程度上说这些Set方法总是尽可能地完成任务。以SetInt为例只要变量是某种类型的有符号整数就可以工作即使是一些命名的类型只要底层数据类型是有符号整数就可以而且如果对于变量类型值太大的话会被自动截断。但需要谨慎的是对于一个引用interface{}类型的reflect.Value调用SetInt会导致panic异常即使那个interface{}变量对于整数类型也不行。
```Go
x := 1
@ -84,7 +84,7 @@ ry.SetString("hello") // panic: SetString called on interface Value
ry.Set(reflect.ValueOf("hello")) // OK, y = "hello"
```
當我們用Display顯示os.Stdout結構時我們發現反射可以越過Go語言的導出規則的限製讀取結構體中未導出的成員比如在類Unix繫統上os.File結構體中的fd int成員。然而利用反射機製併不能脩改這些未導出的成員
当我们用Display显示os.Stdout结构时我们发现反射可以越过Go语言的导出规则的限制读取结构体中未导出的成员比如在类Unix系统上os.File结构体中的fd int成员。然而利用反射机制并不能修改这些未导出的成员
```Go
stdout := reflect.ValueOf(os.Stdout).Elem() // *os.Stdout, an os.File var
@ -94,7 +94,7 @@ fmt.Println(fd.Int()) // "1"
fd.SetInt(2) // panic: unexported field
```
個可取地址的reflect.Value會記録一個結構體成員是否是未導出成員如果是的話則拒絶脩改操作。因此CanAddr方法併不能正確反映一個變量是否是可以被脩改的。另一個相關的方法CanSet是用於檢査對應的reflect.Value是否是可取地址併可被脩改的:
个可取地址的reflect.Value会记录一个结构体成员是否是未导出成员如果是的话则拒绝修改操作。因此CanAddr方法并不能正确反映一个变量是否是可以被修改的。另一个相关的方法CanSet是用于检查对应的reflect.Value是否是可取地址并可被修改的:
```Go
fmt.Println(fd.CanAddr(), fd.CanSet()) // "true false"

View File

@ -1,6 +1,6 @@
## 12.6. 示例: 解碼S表達
## 12.6. 示例: 解码S表达
標準庫中encoding/...下每個包中提供的Marshal編碼函數都有一個對應的Unmarshal函數用於解碼。例如我們在4.5節中看到的要將包含JSON編碼格式的字節slice數據解碼爲我們自己的Movie類型§12.3),我們可以這樣做:
标准库中encoding/...下每个包中提供的Marshal编码函数都有一个对应的Unmarshal函数用于解码。例如我们在4.5节中看到的要将包含JSON编码格式的字节slice数据解码为我们自己的Movie类型§12.3),我们可以这样做:
```Go
data := []byte{/* ... */}
@ -8,13 +8,13 @@ var movie Movie
err := json.Unmarshal(data, &movie)
```
Unmarshal函數使用了反射機製類脩改movie變量的每個成員根據輸入的內容爲Movie成員創建對應的map、結構體和slice。
Unmarshal函数使用了反射机制类修改movie变量的每个成员根据输入的内容为Movie成员创建对应的map、结构体和slice。
現在讓我們爲S表達式編碼實現一個簡易的Unmarshal類似於前面的json.Unmarshal標準庫函數對應我們之前實現的sexpr.Marshal函數的逆操作。我們必須提醒一下一個健壯的和通用的實現通常需要比例子更多的代碼爲了便於演示我們采用了精簡的實現。我們隻支持S表達式有限的子集同時處理錯誤的方式也比較粗暴代碼的目的是爲了演示反射的用法而不是構造一個實用的S表達式的解碼器。
现在让我们为S表达式编码实现一个简易的Unmarshal类似于前面的json.Unmarshal标准库函数对应我们之前实现的sexpr.Marshal函数的逆操作。我们必须提醒一下一个健壮的和通用的实现通常需要比例子更多的代码为了便于演示我们采用了精简的实现。我们只支持S表达式有限的子集同时处理错误的方式也比较粗暴代码的目的是为了演示反射的用法而不是构造一个实用的S表达式的解码器。
詞法分析器lexer使用了標準庫中的text/scanner包將輸入流的字節數據解析爲一個個類似註釋、標識符、字符串面值和數字面值之類的標記。輸入掃描器scanner的Scan方法將提前掃描和返迴下一個記號對於rune類型。大多數記號比如“(”對應一個單一rune可表示的Unicode字符但是text/scanner也可以用小的負數表示記號標識符、字符串等由多個字符組成的記號。調用Scan方法將返迴這些記號的類型接着調用TokenText方法將返迴記號對應的文本內容。
词法分析器lexer使用了标准库中的text/scanner包将输入流的字节数据解析为一个个类似注释、标识符、字符串面值和数字面值之类的标记。输入扫描器scanner的Scan方法将提前扫描和返回下一个记号对于rune类型。大多数记号比如“(”对应一个单一rune可表示的Unicode字符但是text/scanner也可以用小的负数表示记号标识符、字符串等由多个字符组成的记号。调用Scan方法将返回这些记号的类型接着调用TokenText方法将返回记号对应的文本内容。
爲每個解析器可能需要多次使用當前的記號但是Scan會一直向前掃描所有我們包裝了一個lexer掃描器輔助類型用於跟蹤最近由Scan方法返迴的記號
为每个解析器可能需要多次使用当前的记号但是Scan会一直向前扫描所有我们包装了一个lexer扫描器辅助类型用于跟踪最近由Scan方法返回的记号
<u><i>gopl.io/ch12/sexpr</i></u>
```Go
@ -34,7 +34,7 @@ func (lex *lexer) consume(want rune) {
}
```
現在讓我們轉到語法解析器。它主要包含兩個功能。第一個是read函數用於讀取S表達式的當前標記然後根據S表達式的當前標記更新可取地址的reflect.Value對應的變量v。
现在让我们转到语法解析器。它主要包含两个功能。第一个是read函数用于读取S表达式的当前标记然后根据S表达式的当前标记更新可取地址的reflect.Value对应的变量v。
```Go
func read(lex *lexer, v reflect.Value) {
@ -67,13 +67,13 @@ func read(lex *lexer, v reflect.Value) {
}
```
們的S表達式使用標識符區分兩個不同類型結構體成員名和nil值的指針。read函數值處理nil類型的標識符。當遇到scanner.Ident爲“nil”是使用reflect.Zero函數將變量v設置爲零值。而其它任何類型的標識符我們都作爲錯誤處理。後面的readList函數將處理結構體的成員名。
们的S表达式使用标识符区分两个不同类型结构体成员名和nil值的指针。read函数值处理nil类型的标识符。当遇到scanner.Ident为“nil”是使用reflect.Zero函数将变量v设置为零值。而其它任何类型的标识符我们都作为错误处理。后面的readList函数将处理结构体的成员名。
個“(”標記對應一個列表的開始。第二個函數readList將一個列表解碼到一個聚合類型中map、結構體、slice或數組具體類型依然於傳入待填充變量的類型。每次遇到這種情況循環繼續解析每個元素直到遇到於開始標記匹配的結束標記“)”endList函數用於檢測結束標記
个“(”标记对应一个列表的开始。第二个函数readList将一个列表解码到一个聚合类型中map、结构体、slice或数组具体类型依然于传入待填充变量的类型。每次遇到这种情况循环继续解析每个元素直到遇到于开始标记匹配的结束标记“)”endList函数用于检测结束标记
最有趣的部分是遞歸。最簡單的是對數組類型的處理。直到遇到“)”結束標記我們使用Index函數來獲取數組每個元素的地址然後遞歸調用read函數處理。和其它錯誤類似如果輸入數據導致解碼器的引用超出了數組的范圍解碼器將拋出panic異常。slice也采用類似方法解析不同的是我們將爲每個元素創建新的變量然後將元素添加到slice的末尾。
最有趣的部分是递归。最简单的是对数组类型的处理。直到遇到“)”结束标记我们使用Index函数来获取数组每个元素的地址然后递归调用read函数处理。和其它错误类似如果输入数据导致解码器的引用超出了数组的范围解码器将抛出panic异常。slice也采用类似方法解析不同的是我们将为每个元素创建新的变量然后将元素添加到slice的末尾。
在循環處理結構體和map每個元素時必須解碼一個(key value)格式的對應子列表。對於結構體key部分對於成員的名字。和數組類似我們使用FieldByName找到結構體對應成員的變量然後遞歸調用read函數處理。對於mapkey可能是任意類型對元素的處理方式和slice類似我們創建一個新的變量然後遞歸填充它最後將新解析到的key/value對添加到map。
在循环处理结构体和map每个元素时必须解码一个(key value)格式的对应子列表。对于结构体key部分对于成员的名字。和数组类似我们使用FieldByName找到结构体对应成员的变量然后递归调用read函数处理。对于mapkey可能是任意类型对元素的处理方式和slice类似我们创建一个新的变量然后递归填充它最后将新解析到的key/value对添加到map。
```Go
func readList(lex *lexer, v reflect.Value) {
@ -130,7 +130,7 @@ func endList(lex *lexer) bool {
}
```
我們將解析器包裝爲導出的Unmarshal解碼函數隱藏了一些初始化和清理等邊緣處理。內部解析器以panic的方式拋出錯誤但是Unmarshal函數通過在defer語句調用recover函數來捕獲內部panic§5.10然後返迴一個對panic對應的錯誤信息。
我们将解析器包装为导出的Unmarshal解码函数隐藏了一些初始化和清理等边缘处理。内部解析器以panic的方式抛出错误但是Unmarshal函数通过在defer语句调用recover函数来捕获内部panic§5.10然后返回一个对panic对应的错误信息。
```Go
// Unmarshal parses S-expression data and populates the variable
@ -150,10 +150,10 @@ func Unmarshal(data []byte, out interface{}) (err error) {
}
```
産實現不應該對任何輸入問題都用panic形式報告而且應該報告一些錯誤相關的信息例如出現錯誤輸入的行號和位置等。盡管如此我們希望通過這個例子來展示類似encoding/json等包底層代碼的實現思路以及如何使用反射機製來填充數據結構
产实现不应该对任何输入问题都用panic形式报告而且应该报告一些错误相关的信息例如出现错误输入的行号和位置等。尽管如此我们希望通过这个例子来展示类似encoding/json等包底层代码的实现思路以及如何使用反射机制来填充数据结构
**練習 12.8** sexpr.Unmarshal函數和json.Unmarshal一樣都要求在解碼前輸入完整的字節slice。定義一個和json.Decoder類似的sexpr.Decoder類型支持從一個io.Reader流解碼。脩改sexpr.Unmarshal函數使用這個新的類型實現
**练习 12.8** sexpr.Unmarshal函数和json.Unmarshal一样都要求在解码前输入完整的字节slice。定义一个和json.Decoder类似的sexpr.Decoder类型支持从一个io.Reader流解码。修改sexpr.Unmarshal函数使用这个新的类型实现
**練習 12.9** 編寫一個基於標記的API用於解碼S表達式參考xml.Decoder7.14)的風格。你將需要五種類型的標記Symbol、String、Int、StartList和EndList。
**练习 12.9** 编写一个基于标记的API用于解码S表达式参考xml.Decoder7.14)的风格。你将需要五种类型的标记Symbol、String、Int、StartList和EndList。
**練習 12.10** 擴展sexpr.Unmarshal函數支持布爾型、浮點數和interface類型的解碼使用 **練習 12.3** 的方案。提示要解碼接口你需要將name映射到每個支持類型的reflect.Type。
**练习 12.10** 扩展sexpr.Unmarshal函数支持布尔型、浮点数和interface类型的解码使用 **练习 12.3** 的方案。提示要解码接口你需要将name映射到每个支持类型的reflect.Type。

View File

@ -1,10 +1,10 @@
## 12.7. 獲取結構體字段標識
## 12.7. 获取结构体字段标识
在4.5節我們使用構體成員標籤用於設置對應JSON對應的名字。其中json成員標籤讓我們可以選擇成員的名字和抑製零值成員的輸出。在本節我們將看到如果通過反射機製類獲取成員標籤
在4.5节我们使用构体成员标签用于设置对应JSON对应的名字。其中json成员标签让我们可以选择成员的名字和抑制零值成员的输出。在本节我们将看到如果通过反射机制类获取成员标签
對於一個web服務大部分HTTP處理函數要做的第一件事情就是展開請求中的參數到本地變量中。我們定義了一個工具函數叫params.Unpack通過使用結構體成員標籤機製來讓HTTP處理函數解析請求參數更方便。
对于一个web服务大部分HTTP处理函数要做的第一件事情就是展开请求中的参数到本地变量中。我们定义了一个工具函数叫params.Unpack通过使用结构体成员标签机制来让HTTP处理函数解析请求参数更方便。
首先,我們看看如何使用它。下面的search函數是一個HTTP請求處理函數。它定義了一個匿名結構體類型的變量用結構體的每個成員表示HTTP請求的參數。其中結構體成員標籤指明了對於請求參數的名字爲了減少URL的長度這些參數名通常都是神祕的縮略詞。Unpack將請求參數填充到合適的結構體成員中這樣我們可以方便地通過合適的類型類來訪問這些參數
首先,我们看看如何使用它。下面的search函数是一个HTTP请求处理函数。它定义了一个匿名结构体类型的变量用结构体的每个成员表示HTTP请求的参数。其中结构体成员标签指明了对于请求参数的名字为了减少URL的长度这些参数名通常都是神秘的缩略词。Unpack将请求参数填充到合适的结构体成员中这样我们可以方便地通过合适的类型类来访问这些参数
<u><i>gopl.io/ch12/search</i></u>
```Go
@ -28,9 +28,9 @@ func search(resp http.ResponseWriter, req *http.Request) {
}
```
下面的Unpack函數主要完成三件事情。第一它調用req.ParseForm()來解析HTTP請求。然後req.Form將包含所有的請求參數不管HTTP客戶端使用的是GET還是POST請求方法。
下面的Unpack函数主要完成三件事情。第一它调用req.ParseForm()来解析HTTP请求。然后req.Form将包含所有的请求参数不管HTTP客户端使用的是GET还是POST请求方法。
下一步Unpack函數將構建每個結構體成員有效參數名字到成員變量的映射。如果結構體成員有成員標籤的話有效參數名字可能和實際的成員名字不相同。reflect.Type的Field方法將返迴一個reflect.StructField里面含有每個成員的名字、類型和可選的成員標籤等信息。其中成員標籤信息對應reflect.StructTag類型的字符串併且提供了Get方法用於解析和根據特定key提取的子串例如這里的http:"..."形式的子串。
下一步Unpack函数将构建每个结构体成员有效参数名字到成员变量的映射。如果结构体成员有成员标签的话有效参数名字可能和实际的成员名字不相同。reflect.Type的Field方法将返回一个reflect.StructField里面含有每个成员的名字、类型和可选的成员标签等信息。其中成员标签信息对应reflect.StructTag类型的字符串并且提供了Get方法用于解析和根据特定key提取的子串例如这里的http:"..."形式的子串。
<u><i>gopl.io/ch12/params</i></u>
```Go
@ -78,9 +78,9 @@ func Unpack(req *http.Request, ptr interface{}) error {
}
```
Unpack遍歷HTTP請求的name/valu參數鍵值對併且根據更新相應的結構體成員。迴想一下同一個名字的參數可能出現多次。如果發生這種情況併且對應的結構體成員是一個slice那麽就將所有的參數添加到slice中。其它情況對應的成員值將被覆蓋隻有最後一次出現的參數值才是起作用的。
Unpack遍历HTTP请求的name/valu参数键值对并且根据更新相应的结构体成员。回想一下同一个名字的参数可能出现多次。如果发生这种情况并且对应的结构体成员是一个slice那么就将所有的参数添加到slice中。其它情况对应的成员值将被覆盖只有最后一次出现的参数值才是起作用的。
populate函數小心用請求的字符串類型參數值來填充單一的成員v或者是slice類型成員中的單一的元素。目前它僅支持字符串、有符號整數和布爾型。其中其它的類型將留做練習任務
populate函数小心用请求的字符串类型参数值来填充单一的成员v或者是slice类型成员中的单一的元素。目前它仅支持字符串、有符号整数和布尔型。其中其它的类型将留做练习任务
```Go
func populate(v reflect.Value, value string) error {
@ -109,7 +109,7 @@ func populate(v reflect.Value, value string) error {
}
```
如果我們上上面的處理程序添加到一個web服務器則可以産生以下的會話
如果我们上上面的处理程序添加到一个web服务器则可以产生以下的会话
```
$ go build gopl.io/ch12/search
@ -128,8 +128,8 @@ $ ./fetch 'http://localhost:12345/search?q=hello&max=lots'
max: strconv.ParseInt: parsing "lots": invalid syntax
```
**練習 12.11** 編寫相應的Pack函數給定一個結構體值Pack函數將返迴合併了所有結構體成員和值的URL。
**练习 12.11** 编写相应的Pack函数给定一个结构体值Pack函数将返回合并了所有结构体成员和值的URL。
**練習 12.12** 擴展成員標籤以表示一個請求參數的有效值規則。例如一個字符串可以是有效的email地址或一個信用卡號碼還有一個整數可能需要是有效的郵政編碼。脩改Unpack函數以檢査這些規則
**练习 12.12** 扩展成员标签以表示一个请求参数的有效值规则。例如一个字符串可以是有效的email地址或一个信用卡号码还有一个整数可能需要是有效的邮政编码。修改Unpack函数以检查这些规则
**練習 12.13** 脩改S表達式的編碼器§12.4和解碼器§12.6采用和encoding/json包§4.5)類似的方式使用成員標籤中的sexpr:"..."字串。
**练习 12.13** 修改S表达式的编码器§12.4和解码器§12.6采用和encoding/json包§4.5)类似的方式使用成员标签中的sexpr:"..."字串。

View File

@ -1,6 +1,6 @@
## 12.8. 顯示一個類型的方法集
## 12.8. 显示一个类型的方法集
們的最後一個例子是使用reflect.Type來打印任意值的類型和枚舉它的方法:
们的最后一个例子是使用reflect.Type来打印任意值的类型和枚举它的方法:
<u><i>gopl.io/ch12/methods</i></u>
```Go
@ -18,9 +18,9 @@ func Print(x interface{}) {
}
```
reflect.Type和reflect.Value都提供了一個Method方法。每次t.Method(i)調用將一個reflect.Method的實例對應一個用於描述一個方法的名稱和類型的結構體。每次v.Method(i)方法調用都返迴一個reflect.Value以表示對應的值§6.4也就是一個方法是幫到它的接收者的。使用reflect.Value.Call方法我們之類沒有演示將可以調用一個Func類型的Value但是這個例子中隻用到了它的類型。
reflect.Type和reflect.Value都提供了一个Method方法。每次t.Method(i)调用将一个reflect.Method的实例对应一个用于描述一个方法的名称和类型的结构体。每次v.Method(i)方法调用都返回一个reflect.Value以表示对应的值§6.4也就是一个方法是帮到它的接收者的。使用reflect.Value.Call方法我们之类没有演示将可以调用一个Func类型的Value但是这个例子中只用到了它的类型。
這是屬於time.Duration和`*strings.Replacer`兩個類型的方法:
这是属于time.Duration和`*strings.Replacer`两个类型的方法:
```Go
methods.Print(time.Hour)

View File

@ -1,20 +1,20 @@
## 12.9. 幾點忠告
## 12.9. 几点忠告
雖然反射提供的API遠多於我們講到的我們前面的例子主要是給出了一個方向通過反射可以實現哪些功能。反射是一個強大併富有表達力的工具但是它應該被小心地使用,原因有三。
虽然反射提供的API远多于我们讲到的我们前面的例子主要是给出了一个方向通过反射可以实现哪些功能。反射是一个强大并富有表达力的工具但是它应该被小心地使用,原因有三。
第一個原因是基於反射的代碼是比較脆弱的。對於每一個會導致編譯器報告類型錯誤的問題在反射中都有與之相對應的問題不同的是編譯器會在構建時馬上報告錯誤而反射則是在眞正運行到的時候才會拋出panic異常可能是寫完代碼很久之後的時候了而且程序也可能運行了很長的時間
第一个原因是基于反射的代码是比较脆弱的。对于每一个会导致编译器报告类型错误的问题在反射中都有与之相对应的问题不同的是编译器会在构建时马上报告错误而反射则是在真正运行到的时候才会抛出panic异常可能是写完代码很久之后的时候了而且程序也可能运行了很长的时间
以前面的readList函§12.6爲例爲了從輸入讀取字符串併填充int類型的變量而調用的reflect.Value.SetString方法可能導致panic異常。絶大多數使用反射的程序都有類似的風險需要非常小心地檢査每個reflect.Value的對於值的類型、是否可取地址還有是否可以被脩改等。
以前面的readList函§12.6为例为了从输入读取字符串并填充int类型的变量而调用的reflect.Value.SetString方法可能导致panic异常。绝大多数使用反射的程序都有类似的风险需要非常小心地检查每个reflect.Value的对于值的类型、是否可取地址还有是否可以被修改等。
避免這種因反射而導致的脆弱性的問題的最好方法是將所有的反射相關的使用控製在包的內部如果可能的話避免在包的API中直接暴露reflect.Value類型這樣可以限製一些非法輸入。如果無法做到這一點在每個有風險的操作前指向額外的類型檢査。以標準庫中的代碼爲例當fmt.Printf收到一個非法的操作數是它併不會拋出panic異常而是打印相關的錯誤信息。程序雖然還有BUG但是會更加容易診斷
避免这种因反射而导致的脆弱性的问题的最好方法是将所有的反射相关的使用控制在包的内部如果可能的话避免在包的API中直接暴露reflect.Value类型这样可以限制一些非法输入。如果无法做到这一点在每个有风险的操作前指向额外的类型检查。以标准库中的代码为例当fmt.Printf收到一个非法的操作数是它并不会抛出panic异常而是打印相关的错误信息。程序虽然还有BUG但是会更加容易诊断
```Go
fmt.Printf("%d %s\n", "hello", 42) // "%!d(string=hello) %!s(int=42)"
```
反射同樣降低了程序的安全性,還影響了自動化重構和分析工具的準確性,因爲它們無法識别運行時才能確認的類型信息。
反射同样降低了程序的安全性,还影响了自动化重构和分析工具的准确性,因为它们无法识别运行时才能确认的类型信息。
避免使用反射的第二個原因是卽使對應類型提供了相同文檔但是反射的操作不能做靜態類型檢査而且大量反射的代碼通常難以理解。總是需要小心翼翼地爲每個導出的類型和其它接受interface{}或reflect.Value類型參數的函數維護説明文檔
避免使用反射的第二个原因是即使对应类型提供了相同文档但是反射的操作不能做静态类型检查而且大量反射的代码通常难以理解。总是需要小心翼翼地为每个导出的类型和其它接受interface{}或reflect.Value类型参数的函数维护说明文档
第三個原因,基於反射的代碼通常比正常的代碼運行速度慢一到兩個數量級。對於一個典型的項目,大部分函數的性能和程序的整體性能關繫不大,所以使用反射可能會使程序更加清晰。測試是一個特别適合使用反射的場景,因爲每個測試的數據集都很小。但是對於性能關鍵路徑的函數,最好避免使用反射。
第三个原因,基于反射的代码通常比正常的代码运行速度慢一到两个数量级。对于一个典型的项目,大部分函数的性能和程序的整体性能关系不大,所以使用反射可能会使程序更加清晰。测试是一个特别适合使用反射的场景,因为每个测试的数据集都很小。但是对于性能关键路径的函数,最好避免使用反射。

View File

@ -1,5 +1,5 @@
# 第十二章 反射
Go語音提供了一種機製在運行時更新變量和檢査它們的值、調用它們的方法和它們支持的內在操作,但是在編譯時併不知道這些變量的具體類型。這種機製被稱爲反射。反射也可以讓我們將類型本身作爲第一類的值類型處理。
Go语音提供了一种机制在运行时更新变量和检查它们的值、调用它们的方法和它们支持的内在操作,但是在编译时并不知道这些变量的具体类型。这种机制被称为反射。反射也可以让我们将类型本身作为第一类的值类型处理。
在本章,我們將探討Go語言的反射特性看看它可以給語言增加哪些表達力以及在兩個至關重要的API是如何用反射機製的一個是fmt包提供的字符串格式功能另一個是類似encoding/json和encoding/xml提供的針對特定協議的編解碼功能。對於我們在4.6節中看到過的text/template和html/template包它們的實現也是依賴反射技術的。然後反射是一個複雜的內省技術不應該隨意使用因此盡管上面這些包內部都是用反射技術實現的但是它們自己的API都沒有公開反射相關的接口。
在本章,我们将探讨Go语言的反射特性看看它可以给语言增加哪些表达力以及在两个至关重要的API是如何用反射机制的一个是fmt包提供的字符串格式功能另一个是类似encoding/json和encoding/xml提供的针对特定协议的编解码功能。对于我们在4.6节中看到过的text/template和html/template包它们的实现也是依赖反射技术的。然后反射是一个复杂的内省技术不应该随意使用因此尽管上面这些包内部都是用反射技术实现的但是它们自己的API都没有公开反射相关的接口。

View File

@ -1,33 +1,33 @@
## 13.1. unsafe.Sizeof, Alignof 和 Offsetof
unsafe.Sizeof函數返迴操作數在內存中的字節大小參數可以是任意類型的表達式但是它併不會對表達式進行求值。一個Sizeof函數調用是一個對應uintptr類型的常量表達式因此返迴的結果可以用作數組類型的長度大小或者用作計算其他的常量。
unsafe.Sizeof函数返回操作数在内存中的字节大小参数可以是任意类型的表达式但是它并不会对表达式进行求值。一个Sizeof函数调用是一个对应uintptr类型的常量表达式因此返回的结果可以用作数组类型的长度大小或者用作计算其他的常量。
```Go
import "unsafe"
fmt.Println(unsafe.Sizeof(float64(0))) // "8"
```
Sizeof函數返迴的大小隻包括數據結構中固定的部分例如字符串對應結構體中的指針和字符串長度部分但是併不包含指針指向的字符串的內容。Go語言中非聚合類型通常有一個固定的大小盡管在不同工具鏈下生成的實際大小可能會有所不同。考慮到可移植性引用類型或包含引用類型的大小在32位平台上是4個字節在64位平台上是8個字節
Sizeof函数返回的大小只包括数据结构中固定的部分例如字符串对应结构体中的指针和字符串长度部分但是并不包含指针指向的字符串的内容。Go语言中非聚合类型通常有一个固定的大小尽管在不同工具链下生成的实际大小可能会有所不同。考虑到可移植性引用类型或包含引用类型的大小在32位平台上是4个字节在64位平台上是8个字节
計算機在加載和保存數據時如果內存地址合理地對齊的將會更有效率。例如2字節大小的int16類型的變量地址應該是偶數一個4字節大小的rune類型變量的地址應該是4的倍數一個8字節大小的float64、uint64或64-bit指針類型變量的地址應該是8字節對齊的。但是對於再大的地址對齊倍數則是不需要的卽使是complex128等較大的數據類型最多也隻是8字節對齊
计算机在加载和保存数据时如果内存地址合理地对齐的将会更有效率。例如2字节大小的int16类型的变量地址应该是偶数一个4字节大小的rune类型变量的地址应该是4的倍数一个8字节大小的float64、uint64或64-bit指针类型变量的地址应该是8字节对齐的。但是对于再大的地址对齐倍数则是不需要的即使是complex128等较大的数据类型最多也只是8字节对齐
於地址對齊這個因素一個聚合類型結構體或數組的大小至少是所有字段或元素大小的總和或者更大因爲可能存在內存空洞。內存空洞是編譯器自動添加的沒有被使用的內存空間用於保證後面每個字段或元素的地址相對於結構或數組的開始地址能夠合理地對齊譯註內存空洞可能會存在一些隨機數據可能會對用unsafe包直接操作內存的處理産生影響)。
于地址对齐这个因素一个聚合类型结构体或数组的大小至少是所有字段或元素大小的总和或者更大因为可能存在内存空洞。内存空洞是编译器自动添加的没有被使用的内存空间用于保证后面每个字段或元素的地址相对于结构或数组的开始地址能够合理地对齐译注内存空洞可能会存在一些随机数据可能会对用unsafe包直接操作内存的处理产生影响)。
型 | 大小
型 | 大小
----------------------------- | ----
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)
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语言的规范并没有要求一个字段的声明顺序和内存中的顺序是一致的所以理论上一个编译器可以随意地重新排列每个字段的内存位置随然在写作本书的时候编译器还没有这么做。下面的三个结构体虽然有着相同的字段但是第一种写法比另外的两个需要多50%的内存。
```Go
// 64-bit 32-bit
@ -36,13 +36,13 @@ struct{ float64; int16; bool } // 2 words 3words
struct{ bool; int16; float64 } // 2 words 3words
```
關於內存地址對齊算法的細節超出了本書的范圍也不是每一個結構體都需要擔心這個問題不過有效的包裝可以使數據結構更加緊湊譯註未來的Go語言編譯器應該會默認優化結構體的順序當然用於應該也能夠指定具體的內存布局相同討論請參考 [Issue10014](https://github.com/golang/go/issues/10014) ),內存使用率和性能都可能會受益。
关于内存地址对齐算法的细节超出了本书的范围也不是每一个结构体都需要担心这个问题不过有效的包装可以使数据结构更加紧凑译注未来的Go语言编译器应该会默认优化结构体的顺序当然用于应该也能够指定具体的内存布局相同讨论请参考 [Issue10014](https://github.com/golang/go/issues/10014) ),内存使用率和性能都可能会受益。
`unsafe.Alignof`數返迴對應參數的類型需要對齊的倍數. 和 Sizeof 類似, Alignof 也是返迴一個常量表達式, 對應一個常量. 通常情況下布爾和數字類型需要對齊到它們本身的大小(最多8個字節), 其它的類型對齊到機器字大小.
`unsafe.Alignof`数返回对应参数的类型需要对齐的倍数. 和 Sizeof 类似, Alignof 也是返回一个常量表达式, 对应一个常量. 通常情况下布尔和数字类型需要对齐到它们本身的大小(最多8个字节), 其它的类型对齐到机器字大小.
`unsafe.Offsetof`數的參數必須是一個字段 `x.f`, 然後返迴 `f` 字段相對於 `x` 起始地址的偏移量, 包括可能的空洞.
`unsafe.Offsetof`数的参数必须是一个字段 `x.f`, 然后返回 `f` 字段相对于 `x` 起始地址的偏移量, 包括可能的空洞.
圖 13.1 顯示了一個結構體變量 x 以及其在32位和64位機器上的典型的內存. 灰色區域是空洞.
图 13.1 显示了一个结构体变量 x 以及其在32位和64位机器上的典型的内存. 灰色区域是空洞.
```Go
var x struct {
@ -52,11 +52,11 @@ var x struct {
}
```
下面顯示了對x和它的三個字段調用unsafe包相關函數的計算結果:
下面显示了对x和它的三个字段调用unsafe包相关函数的计算结果:
![](../images/ch13-01.png)
32位繫統
32位系统
```
Sizeof(x) = 16 Alignof(x) = 4
@ -65,7 +65,7 @@ Sizeof(x.b) = 2 Alignof(x.b) = 2 Offsetof(x.b) = 2
Sizeof(x.c) = 12 Alignof(x.c) = 4 Offsetof(x.c) = 4
```
64位繫統
64位系统
```
Sizeof(x) = 32 Alignof(x) = 8
@ -74,5 +74,5 @@ Sizeof(x.b) = 2 Alignof(x.b) = 2 Offsetof(x.b) = 2
Sizeof(x.c) = 24 Alignof(x.c) = 8 Offsetof(x.c) = 8
```
雖然這幾個函數在不安全的unsafe包但是這幾個函數調用併不是眞的不安全特别在需要優化內存空間時它們返迴的結果對於理解原生的內存布局很有幫助。
虽然这几个函数在不安全的unsafe包但是这几个函数调用并不是真的不安全特别在需要优化内存空间时它们返回的结果对于理解原生的内存布局很有帮助。

View File

@ -1,8 +1,8 @@
## 13.2. unsafe.Pointer
大多數指針類型會寫成`*T`表示是“一個指向T類型變量的指針”。unsafe.Pointer是特别定義的一種指針類型譯註類似C語言中的`void*`類型的指針),它可以包含任意類型變量的地址。當然,我們不可以直接通過`*p`來獲取unsafe.Pointer指針指向的眞實變量的值因爲我們併不知道變量的具體類型。和普通指針一樣unsafe.Pointer指針也是可以比較的併且支持和nil常量比較判斷是否爲空指針
大多数指针类型会写成`*T`表示是“一个指向T类型变量的指针”。unsafe.Pointer是特别定义的一种指针类型译注类似C语言中的`void*`类型的指针),它可以包含任意类型变量的地址。当然,我们不可以直接通过`*p`来获取unsafe.Pointer指针指向的真实变量的值因为我们并不知道变量的具体类型。和普通指针一样unsafe.Pointer指针也是可以比较的并且支持和nil常量比较判断是否为空指针
個普通的`*T`類型指針可以被轉化爲unsafe.Pointer類型指針併且一個unsafe.Pointer類型指針也可以被轉迴普通的指針被轉迴普通的指針類型併不需要和原始的`*T`類型相同。通過將`*float64`類型指針轉化爲`*uint64`類型指針,我們可以査看一個浮點數變量的位模式。
个普通的`*T`类型指针可以被转化为unsafe.Pointer类型指针并且一个unsafe.Pointer类型指针也可以被转回普通的指针被转回普通的指针类型并不需要和原始的`*T`类型相同。通过将`*float64`类型指针转化为`*uint64`类型指针,我们可以查看一个浮点数变量的位模式。
```Go
package math
@ -12,11 +12,11 @@ func Float64bits(f float64) uint64 { return *(*uint64)(unsafe.Pointer(&f)) }
fmt.Printf("%#016x\n", Float64bits(1.0)) // "0x3ff0000000000000"
```
過轉爲新類型指針,我們可以更新浮點數的位模式。通過位模式操作浮點數是可以的,但是更重要的意義是指針轉換語法讓我們可以在不破壞類型繫統的前提下向內存寫入任意的值。
过转为新类型指针,我们可以更新浮点数的位模式。通过位模式操作浮点数是可以的,但是更重要的意义是指针转换语法让我们可以在不破坏类型系统的前提下向内存写入任意的值。
個unsafe.Pointer指針也可以被轉化爲uintptr類型然後保存到指針型數值變量中譯註這隻是和當前指針相同的一個數字值併不是一個指針然後用以做必要的指針數值運算。第三章內容uintptr是一個無符號的整型數足以保存一個地址這種轉換雖然也是可逆的但是將uintptr轉爲unsafe.Pointer指針可能會破壞類型繫統因爲併不是所有的數字都是有效的內存地址。
个unsafe.Pointer指针也可以被转化为uintptr类型然后保存到指针型数值变量中译注这只是和当前指针相同的一个数字值并不是一个指针然后用以做必要的指针数值运算。第三章内容uintptr是一个无符号的整型数足以保存一个地址这种转换虽然也是可逆的但是将uintptr转为unsafe.Pointer指针可能会破坏类型系统因为并不是所有的数字都是有效的内存地址。
許多將unsafe.Pointer指針轉爲原生數字然後再轉迴爲unsafe.Pointer類型指針的操作也是不安全的。比如下面的例子需要將變量x的地址加上b字段地址偏移量轉化爲`*int16`類型指針,然後通過該指針更新x.b
许多将unsafe.Pointer指针转为原生数字然后再转回为unsafe.Pointer类型指针的操作也是不安全的。比如下面的例子需要将变量x的地址加上b字段地址偏移量转化为`*int16`类型指针,然后通过该指针更新x.b
<u><i>gopl.io/ch13/unsafeptr</i></u>
```Go
@ -26,14 +26,14 @@ var x struct {
c []int
}
// 和 pb := &x.b 等
// 和 pb := &x.b 等
pb := (*int16)(unsafe.Pointer(
uintptr(unsafe.Pointer(&x)) + unsafe.Offsetof(x.b)))
*pb = 42
fmt.Println(x.b) // "42"
```
上面的寫法盡管很繁瑣但在這里併不是一件壞事因爲這些功能應該很謹慎地使用。不要試圖引入一個uintptr類型的臨時變量因爲它可能會破壞代碼的安全性譯註這是眞正可以體會unsafe包爲何不安全的例子。下面段代碼是錯誤的:
上面的写法尽管很繁琐但在这里并不是一件坏事因为这些功能应该很谨慎地使用。不要试图引入一个uintptr类型的临时变量因为它可能会破坏代码的安全性译注这是真正可以体会unsafe包为何不安全的例子。下面段代码是错误的:
```Go
// NOTE: subtly incorrect!
@ -42,21 +42,21 @@ pb := (*int16)(unsafe.Pointer(tmp))
*pb = 42
```
産生錯誤的原因很微妙。有時候垃圾迴收器會移動一些變量以降低內存碎片等問題。這類垃圾迴收器被稱爲移動GC。當一個變量被移動所有的保存改變量舊地址的指針必須同時被更新爲變量移動後的新地址。從垃圾收集器的視角來看一個unsafe.Pointer是一個指向變量的指針因此當變量被移動是對應的指針也必須被更新但是uintptr類型的臨時變量隻是一個普通的數字所以其值不應該被改變。上面錯誤的代碼因爲引入一個非指針的臨時變量tmp導致垃圾收集器無法正確識别這個是一個指向變量x的指針。當第二個語句執行時變量x可能已經被轉移這時候臨時變量tmp也就不再是現在的`&x.b`地址。第三個向之前無效地址空間的賦值語句將徹底摧譭整個程序!
产生错误的原因很微妙。有时候垃圾回收器会移动一些变量以降低内存碎片等问题。这类垃圾回收器被称为移动GC。当一个变量被移动所有的保存改变量旧地址的指针必须同时被更新为变量移动后的新地址。从垃圾收集器的视角来看一个unsafe.Pointer是一个指向变量的指针因此当变量被移动是对应的指针也必须被更新但是uintptr类型的临时变量只是一个普通的数字所以其值不应该被改变。上面错误的代码因为引入一个非指针的临时变量tmp导致垃圾收集器无法正确识别这个是一个指向变量x的指针。当第二个语句执行时变量x可能已经被转移这时候临时变量tmp也就不再是现在的`&x.b`地址。第三个向之前无效地址空间的赋值语句将彻底摧毁整个程序!
還有很多類似原因導致的錯誤。例如這條語句:
还有很多类似原因导致的错误。例如这条语句:
```Go
pT := uintptr(unsafe.Pointer(new(T))) // 提示: 錯誤!
pT := uintptr(unsafe.Pointer(new(T))) // 提示: 错误!
```
這里併沒有指針引用`new`新創建的變量因此該語句執行完成之後垃圾收集器有權馬上迴收其內存空間所以返迴的pT將是無效的地址。
这里并没有指针引用`new`新创建的变量因此该语句执行完成之后垃圾收集器有权马上回收其内存空间所以返回的pT将是无效的地址。
雖然目前的Go語言實現還沒有使用移動GC譯註未來可能實現但這不該是編寫錯誤代碼僥幸的理由當前的Go語言實現已經有移動變量的場景。在5.2節我們提到goroutine的棧是根據需要動態增長的。當發送棧動態增長的時候原來棧中的所以變量可能需要被移動到新的更大的棧中所以我們併不能確保變量的地址在整個使用週期內是不變的。
虽然目前的Go语言实现还没有使用移动GC译注未来可能实现但这不该是编写错误代码侥幸的理由当前的Go语言实现已经有移动变量的场景。在5.2节我们提到goroutine的栈是根据需要动态增长的。当发送栈动态增长的时候原来栈中的所以变量可能需要被移动到新的更大的栈中所以我们并不能确保变量的地址在整个使用周期内是不变的。
編寫本文時還沒有清晰的原則來指引Go程序員什麽樣的unsafe.Pointer和uintptr的轉換是不安全的參考 [Issue7192](https://github.com/golang/go/issues/7192) . 譯註: 該問題已經關閉因此我們強烈建議按照最壞的方式處理。將所有包含變量地址的uintptr類型變量當作BUG處理同時減少不必要的unsafe.Pointer類型到uintptr類型的轉換。在第一個例子中有三個轉換——字段偏移量到uintptr的轉換和轉迴unsafe.Pointer類型的操作——所有的轉換全在一個表達式完成。
编写本文时还没有清晰的原则来指引Go程序员什么样的unsafe.Pointer和uintptr的转换是不安全的参考 [Issue7192](https://github.com/golang/go/issues/7192) . 译注: 该问题已经关闭因此我们强烈建议按照最坏的方式处理。将所有包含变量地址的uintptr类型变量当作BUG处理同时减少不必要的unsafe.Pointer类型到uintptr类型的转换。在第一个例子中有三个转换——字段偏移量到uintptr的转换和转回unsafe.Pointer类型的操作——所有的转换全在一个表达式完成。
當調用一個庫函數併且返迴的是uintptr類型地址時譯註普通方法實現的函數不盡量不要返迴該類型。下面例子是reflect包的函數reflect包和unsafe包一樣都是采用特殊技術實現的編譯器可能給它們開了後門比如下面反射包中的相關函數返迴的結果應該立卽轉換爲unsafe.Pointer以確保指針指向的是相同的變量。
当调用一个库函数并且返回的是uintptr类型地址时译注普通方法实现的函数不尽量不要返回该类型。下面例子是reflect包的函数reflect包和unsafe包一样都是采用特殊技术实现的编译器可能给它们开了后门比如下面反射包中的相关函数返回的结果应该立即转换为unsafe.Pointer以确保指针指向的是相同的变量。
```Go
package reflect

View File

@ -1,6 +1,6 @@
## 13.3. 示例: 深度相等判
## 13.3. 示例: 深度相等判
來自reflect包的DeepEqual函數可以對兩個值進行深度相等判斷。DeepEqual函數使用內建的==比較操作符對基礎類型進行相等判斷,對於複合類型則遞歸該變量的每個基礎類型然後做類似的比較判斷。因爲它可以工作在任意的類型上,甚至對於一些不支持==操作運算符的類型也可以工作因此在一些測試代碼中廣泛地使用該函數。比如下面的代碼是用DeepEqual函數比較兩個字符串數組是否相等。
来自reflect包的DeepEqual函数可以对两个值进行深度相等判断。DeepEqual函数使用内建的==比较操作符对基础类型进行相等判断,对于复合类型则递归该变量的每个基础类型然后做类似的比较判断。因为它可以工作在任意的类型上,甚至对于一些不支持==操作运算符的类型也可以工作因此在一些测试代码中广泛地使用该函数。比如下面的代码是用DeepEqual函数比较两个字符串数组是否相等。
```Go
func TestSplit(t *testing.T) {
@ -10,7 +10,7 @@ func TestSplit(t *testing.T) {
}
```
盡管DeepEqual函數很方便而且可以支持任意的數據類型但是它也有不足之處。例如它將一個nil值的map和非nil值但是空的map視作不相等同樣nil值的slice 和非nil但是空的slice也視作不相等。
尽管DeepEqual函数很方便而且可以支持任意的数据类型但是它也有不足之处。例如它将一个nil值的map和非nil值但是空的map视作不相等同样nil值的slice 和非nil但是空的slice也视作不相等。
```Go
var a, b []string = nil, []string{}
@ -20,7 +20,7 @@ var c, d map[string]int = nil, make(map[string]int)
fmt.Println(reflect.DeepEqual(c, d)) // "false"
```
們希望在這里實現一個自己的Equal函數用於比較類型的值。和DeepEqual函數類似的地方是它也是基於slice和map的每個元素進行遞歸比較不同之處是它將nil值的slicemap類似和非nil值但是空的slice視作相等的值。基礎部分的比較可以基於reflect包完成和12.3章的Display函數的實現方法類似。同樣我們也定義了一個內部函數equal用於內部的遞歸比較。讀者目前不用關心seen參數的具體含義。對於每一對需要比較的x和yequal函數首先檢測它們是否都有效或都無效然後檢測它們是否是相同的類型。剩下的部分是一個鉅大的switch分支用於相同基礎類型的元素比較。因爲頁面空間的限製我們省略了一些相似的分支。
们希望在这里实现一个自己的Equal函数用于比较类型的值。和DeepEqual函数类似的地方是它也是基于slice和map的每个元素进行递归比较不同之处是它将nil值的slicemap类似和非nil值但是空的slice视作相等的值。基础部分的比较可以基于reflect包完成和12.3章的Display函数的实现方法类似。同样我们也定义了一个内部函数equal用于内部的递归比较。读者目前不用关心seen参数的具体含义。对于每一对需要比较的x和yequal函数首先检测它们是否都有效或都无效然后检测它们是否是相同的类型。剩下的部分是一个巨大的switch分支用于相同基础类型的元素比较。因为页面空间的限制我们省略了一些相似的分支。
<u><i>gopl.io/ch13/equal</i></u>
```Go
@ -63,7 +63,7 @@ func equal(x, y reflect.Value, seen map[comparison]bool) bool {
}
```
和前面的建議一樣我們併不公開reflect包相關的接口所以導出的函數需要在內部自己將變量轉爲reflect.Value類型。
和前面的建议一样我们并不公开reflect包相关的接口所以导出的函数需要在内部自己将变量转为reflect.Value类型。
```Go
// Equal reports whether x and y are deeply equal.
@ -78,7 +78,7 @@ type comparison struct {
}
```
爲了確保算法對於有環的數據結構也能正常退出我們必須記録每次已經比較的變量從而避免進入第二次的比較。Equal函數分配了一組用於比較的結構體包含每對比較對象的地址unsafe.Pointer形式保存和類型。我們要記録類型的原因是有些不同的變量可能對應相同的地址。例如如果x和y都是數組類型那麽x和x[0]將對應相同的地址y和y[0]也是對應相同的地址這可以用於區分x與y之間的比較或x[0]與y[0]之間的比較是否進行過了。
为了确保算法对于有环的数据结构也能正常退出我们必须记录每次已经比较的变量从而避免进入第二次的比较。Equal函数分配了一组用于比较的结构体包含每对比较对象的地址unsafe.Pointer形式保存和类型。我们要记录类型的原因是有些不同的变量可能对应相同的地址。例如如果x和y都是数组类型那么x和x[0]将对应相同的地址y和y[0]也是对应相同的地址这可以用于区分x与y之间的比较或x[0]与y[0]之间的比较是否进行过了。
```Go
// cycle check
@ -96,7 +96,7 @@ if x.CanAddr() && y.CanAddr() {
}
```
這是Equal函數用法的例子:
这是Equal函数用法的例子:
```Go
fmt.Println(Equal([]int{1, 2, 3}, []int{1, 2, 3})) // "true"
@ -105,7 +105,7 @@ fmt.Println(Equal([]string(nil), []string{})) // "true"
fmt.Println(Equal(map[string]int(nil), map[string]int{})) // "true"
```
Equal函數甚至可以處理類似12.3章中導致Display陷入陷入死循環的帶有環的數據
Equal函数甚至可以处理类似12.3章中导致Display陷入陷入死循环的带有环的数据
```Go
// Circular linked lists a -> b -> a and c -> c.
@ -122,6 +122,6 @@ fmt.Println(Equal(a, b)) // "false"
fmt.Println(Equal(a, c)) // "false"
```
**練習 13.1** 定義一個深比較函數,對於十億以內的數字比較,忽略類型差異
**练习 13.1** 定义一个深比较函数,对于十亿以内的数字比较,忽略类型差异
**練習 13.2** 編寫一個函數,報告其參數是否循環數據結構
**练习 13.2** 编写一个函数,报告其参数是否循环数据结构

View File

@ -1,10 +1,10 @@
## 13.4. 通過cgo調用C代碼
## 13.4. 通过cgo调用C代码
Go程序可能會遇到要訪問C語言的某些硬件驅動函數的場景或者是從一個C++語言實現的嵌入式數據庫査詢記録的場景或者是使用Fortran語言實現的一些線性代數庫的場景。C語言作爲一個通用語言很多庫會選擇提供一個C兼容的API然後用其他不同的編程語言實現譯者Go語言需要也應該擁抱這些鉅大的代碼遺産)。
Go程序可能会遇到要访问C语言的某些硬件驱动函数的场景或者是从一个C++语言实现的嵌入式数据库查询记录的场景或者是使用Fortran语言实现的一些线性代数库的场景。C语言作为一个通用语言很多库会选择提供一个C兼容的API然后用其他不同的编程语言实现译者Go语言需要也应该拥抱这些巨大的代码遗产)。
在本節中我們將構建一個簡易的數據壓縮程序使用了一個Go語言自帶的叫cgo的用於支援C語言函數調用的工具。這類工具一般被稱爲 *foreign-function interfaces* 簡稱ffi, 併且在類似工具中cgo也不是唯一的。SWIG http://swig.org 是另一個類似的且被廣泛使用的工具SWIG提供了很多複雜特性以支援C++的特性但SWIG併不是我們要討論的主題
在本节中我们将构建一个简易的数据压缩程序使用了一个Go语言自带的叫cgo的用于支援C语言函数调用的工具。这类工具一般被称为 *foreign-function interfaces* 简称ffi, 并且在类似工具中cgo也不是唯一的。SWIG http://swig.org 是另一个类似的且被广泛使用的工具SWIG提供了很多复杂特性以支援C++的特性但SWIG并不是我们要讨论的主题
標準庫的`compress/...`子包有很多流行的壓縮算法的編碼和解碼實現包括流行的LZW壓縮算法Unix的compress命令用的算法和DEFLATE壓縮算法GNU gzip命令用的算法。這些包的API的細節雖然有些差異但是它們都提供了針對 io.Writer類型輸出的壓縮接口和提供了針對io.Reader類型輸入的解壓縮接口。例如:
标准库的`compress/...`子包有很多流行的压缩算法的编码和解码实现包括流行的LZW压缩算法Unix的compress命令用的算法和DEFLATE压缩算法GNU gzip命令用的算法。这些包的API的细节虽然有些差异但是它们都提供了针对 io.Writer类型输出的压缩接口和提供了针对io.Reader类型输入的解压缩接口。例如:
```Go
package gzip // compress/gzip
@ -12,11 +12,11 @@ func NewWriter(w io.Writer) io.WriteCloser
func NewReader(r io.Reader) (io.ReadCloser, error)
```
bzip2壓縮算法是基於優雅的Burrows-Wheeler變換算法運行速度比gzip要慢但是可以提供更高的壓縮比。標準庫的compress/bzip2包目前還沒有提供bzip2壓縮算法的實現。完全從頭開始實現是一個壓縮算法是一件繁瑣的工作而且 http://bzip.org 已經有現成的libbzip2的開源實現不僅文檔齊全而且性能又好。
bzip2压缩算法是基于优雅的Burrows-Wheeler变换算法运行速度比gzip要慢但是可以提供更高的压缩比。标准库的compress/bzip2包目前还没有提供bzip2压缩算法的实现。完全从头开始实现是一个压缩算法是一件繁琐的工作而且 http://bzip.org 已经有现成的libbzip2的开源实现不仅文档齐全而且性能又好。
如果是比較小的C語言庫我們完全可以用純Go語言重新實現一遍。如果我們對性能也沒有特殊要求的話我們還可以用os/exec包的方法將C編寫的應用程序作爲一個子進程運行。隻有當你需要使用複雜而且性能更高的底層C接口時就是使用cgo的場景了譯註用os/exec包調用子進程的方法會導致程序運行時依賴那個應用程序。下面我們將通過一個例子講述cgo的具體用法。
如果是比较小的C语言库我们完全可以用纯Go语言重新实现一遍。如果我们对性能也没有特殊要求的话我们还可以用os/exec包的方法将C编写的应用程序作为一个子进程运行。只有当你需要使用复杂而且性能更高的底层C接口时就是使用cgo的场景了译注用os/exec包调用子进程的方法会导致程序运行时依赖那个应用程序。下面我们将通过一个例子讲述cgo的具体用法。
譯註本章采用的代碼都是最新的。因爲之前已經出版的書中包含的代碼隻能在Go1.5之前使用。從Go1.6開始Go語言已經明確規定了哪些Go語言指針可以之間傳入C語言函數。新代碼重點是增加了bz2alloc和bz2free的兩個函數用於bz_stream對象空間的申請和釋放操作。下面是新代碼中增加的註釋説明這個問題
译注本章采用的代码都是最新的。因为之前已经出版的书中包含的代码只能在Go1.5之前使用。从Go1.6开始Go语言已经明确规定了哪些Go语言指针可以之间传入C语言函数。新代码重点是增加了bz2alloc和bz2free的两个函数用于bz_stream对象空间的申请和释放操作。下面是新代码中增加的注释说明这个问题
```Go
// The version of this program that appeared in the first and second
@ -37,9 +37,9 @@ bzip2壓縮算法是基於優雅的Burrows-Wheeler變換算法運行速度
// pointers to Go variables.
```
要使用libbzip2們需要先構建一個bz_stream結構體用於保持輸入和輸出緩存。然後有三個函數BZ2_bzCompressInit用於初始化緩存BZ2_bzCompress用於將輸入緩存的數據壓縮到輸出緩存BZ2_bzCompressEnd用於釋放不需要的緩存。目前不要擔心包的具體結構, 這個例子的目的就是演示各個部分如何組合在一起的。)
要使用libbzip2们需要先构建一个bz_stream结构体用于保持输入和输出缓存。然后有三个函数BZ2_bzCompressInit用于初始化缓存BZ2_bzCompress用于将输入缓存的数据压缩到输出缓存BZ2_bzCompressEnd用于释放不需要的缓存。目前不要担心包的具体结构, 这个例子的目的就是演示各个部分如何组合在一起的。)
們可以在Go代碼中直接調用BZ2_bzCompressInit和BZ2_bzCompressEnd但是對於BZ2_bzCompress我們將定義一個C語言的包裝函數用它完成眞正的工作。下面是C代碼對應一個獨立的文件。
们可以在Go代码中直接调用BZ2_bzCompressInit和BZ2_bzCompressEnd但是对于BZ2_bzCompress我们将定义一个C语言的包装函数用它完成真正的工作。下面是C代码对应一个独立的文件。
<u><i>gopl.io/ch13/bzip</i></u>
```C
@ -61,7 +61,7 @@ int bz2compress(bz_stream *s, int action,
}
```
現在讓我們轉到Go語言部分第一部分如下所示。其中`import "C"`的語句是比較特别的。其實併沒有一個叫C的包但是這行語句會讓Go編譯程序在編譯之前先運行cgo工具。
现在让我们转到Go语言部分第一部分如下所示。其中`import "C"`的语句是比较特别的。其实并没有一个叫C的包但是这行语句会让Go编译程序在编译之前先运行cgo工具。
```Go
// Package bzip provides a writer that uses bzip2 compression (bzip.org).
@ -101,13 +101,13 @@ func NewWriter(out io.Writer) io.WriteCloser {
}
```
預處理過程中cgo工具爲生成一個臨時包用於包含所有在Go語言中訪問的C語言的函數或類型。例如C.bz_stream和C.BZ2_bzCompressInit。cgo工具通過以某種特殊的方式調用本地的C編譯器來發現在Go源文件導入聲明前的註釋中包含的C頭文件中的內容譯註`import "C"`語句前僅捱着的註釋是對應cgo的特殊語法對應必要的構建參數選項和C語言代碼)。
预处理过程中cgo工具为生成一个临时包用于包含所有在Go语言中访问的C语言的函数或类型。例如C.bz_stream和C.BZ2_bzCompressInit。cgo工具通过以某种特殊的方式调用本地的C编译器来发现在Go源文件导入声明前的注释中包含的C头文件中的内容译注`import "C"`语句前仅挨着的注释是对应cgo的特殊语法对应必要的构建参数选项和C语言代码)。
在cgo註釋中還可以包含#cgo指令用於給C語言工具鏈指定特殊的參數。例如CFLAGS和LDFLAGS分别對應傳給C語言編譯器的編譯參數和鏈接器參數使它們可以特定目録找到bzlib.h頭文件和libbz2.a庫文件。這個例子假設你已經在/usr目録成功安裝了bzip2庫。如果bzip2庫是安裝在不同的位置你需要更新這些參數譯註這里有一個從純C代碼生成的cgo綁定不依賴bzip2靜態庫和操作繫統的具體環境具體請訪問 https://github.com/chai2010/bzip2 )。
在cgo注释中还可以包含#cgo指令用于给C语言工具链指定特殊的参数。例如CFLAGS和LDFLAGS分别对应传给C语言编译器的编译参数和链接器参数使它们可以特定目录找到bzlib.h头文件和libbz2.a库文件。这个例子假设你已经在/usr目录成功安装了bzip2库。如果bzip2库是安装在不同的位置你需要更新这些参数译注这里有一个从纯C代码生成的cgo绑定不依赖bzip2静态库和操作系统的具体环境具体请访问 https://github.com/chai2010/bzip2 )。
NewWriter函數通過調用C語言的BZ2_bzCompressInit函數來初始化stream中的緩存。在writer結構中還包括了另一個buffer用於輸出緩存。
NewWriter函数通过调用C语言的BZ2_bzCompressInit函数来初始化stream中的缓存。在writer结构中还包括了另一个buffer用于输出缓存。
下面是Write方法的實現返迴成功壓縮數據的大小主體是一個循環中調用C語言的bz2compress函數實現的。從代碼可以看到Go程序可以訪問C語言的bz_stream、char和uint類型還可以訪問bz2compress等函數甚至可以訪問C語言中像BZ_RUN那樣的宏定義全部都是以C.x語法訪問。其中C.uint類型和Go語言的uint類型併不相同卽使它們具有相同的大小也是不同的類型。
下面是Write方法的实现返回成功压缩数据的大小主体是一个循环中调用C语言的bz2compress函数实现的。从代码可以看到Go程序可以访问C语言的bz_stream、char和uint类型还可以访问bz2compress等函数甚至可以访问C语言中像BZ_RUN那样的宏定义全部都是以C.x语法访问。其中C.uint类型和Go语言的uint类型并不相同即使它们具有相同的大小也是不同的类型。
```Go
func (w *writer) Write(data []byte) (int, error) {
@ -131,9 +131,9 @@ func (w *writer) Write(data []byte) (int, error) {
}
```
在循環的每次迭代中向bz2compress傳入數據的地址和剩餘部分的長度還有輸出緩存w.outbuf的地址和容量。這兩個長度信息通過它們的地址傳入而不是值傳入因爲bz2compress函數可能會根據已經壓縮的數據和壓縮後數據的大小來更新這兩個值。每個塊壓縮後的數據被寫入到底層的io.Writer。
在循环的每次迭代中向bz2compress传入数据的地址和剩余部分的长度还有输出缓存w.outbuf的地址和容量。这两个长度信息通过它们的地址传入而不是值传入因为bz2compress函数可能会根据已经压缩的数据和压缩后数据的大小来更新这两个值。每个块压缩后的数据被写入到底层的io.Writer。
Close方法和Write方法有着類似的結構,通過一個循環將剩餘的壓縮數據刷新到輸出緩存。
Close方法和Write方法有着类似的结构,通过一个循环将剩余的压缩数据刷新到输出缓存。
```Go
// Close flushes the compressed data and closes the stream.
@ -161,11 +161,11 @@ func (w *writer) Close() error {
}
```
壓縮完成後Close方法用了defer函數確保函數退出前調用C.BZ2_bzCompressEnd和C.bz2free釋放相關的C語言運行時資源。此刻w.stream指針將不再有效我們將它設置爲nil以保證安全然後在每個方法中增加了nil檢測以防止用戶在關閉後依然錯誤使用相關方法。
压缩完成后Close方法用了defer函数确保函数退出前调用C.BZ2_bzCompressEnd和C.bz2free释放相关的C语言运行时资源。此刻w.stream指针将不再有效我们将它设置为nil以保证安全然后在每个方法中增加了nil检测以防止用户在关闭后依然错误使用相关方法。
上面的實現中不僅僅寫是非併發安全的甚至併發調用Close和Write方法也可能導致程序的的崩潰。脩複這個問題是練習13.3的內容。
上面的实现中不仅仅写是非并发安全的甚至并发调用Close和Write方法也可能导致程序的的崩溃。修复这个问题是练习13.3的内容。
下面的bzipper程序使用我們自己包實現的bzip2壓縮命令。它的行爲和許多Unix繫統的bzip2命令類似。
下面的bzipper程序使用我们自己包实现的bzip2压缩命令。它的行为和许多Unix系统的bzip2命令类似。
<u><i>gopl.io/ch13/bzipper</i></u>
```Go
@ -190,7 +190,7 @@ func main() {
}
```
在上面的場景中我們使用bzipper壓縮了/usr/share/dict/words繫統自帶的詞典從938,848字節壓縮到335,405字節。大約是原始數據大小的三分之一。然後使用繫統自帶的bunzip2命令進行解壓。壓縮前後文件的SHA256哈希碼是相同了這也説明了我們的壓縮工具是正確的。如果你的繫統沒有sha256sum命令那麽請先按照練習4.2實現一個類似的工具)
在上面的场景中我们使用bzipper压缩了/usr/share/dict/words系统自带的词典从938,848字节压缩到335,405字节。大约是原始数据大小的三分之一。然后使用系统自带的bunzip2命令进行解压。压缩前后文件的SHA256哈希码是相同了这也说明了我们的压缩工具是正确的。如果你的系统没有sha256sum命令那么请先按照练习4.2实现一个类似的工具)
```
$ go build gopl.io/ch13/bzipper
@ -204,8 +204,8 @@ $ ./bzipper < /usr/share/dict/words | bunzip2 | sha256sum
126a4ef38493313edc50b86f90dfdaf7c59ec6c948451eac228f2f3a8ab1a6ed -
```
們演示了如何將一個C語言庫鏈接到Go語言程序。相反, 將Go編譯爲靜態庫然後鏈接到C程序或者將Go程序編譯爲動態庫然後在C程序中動態加載也都是可行的譯註在Go1.5中Windows繫統的Go語言實現併不支持生成C語言動態庫或靜態庫的特性。不過好消息是目前已經有人在嚐試解決這個問題具體請訪問 [Issue11058](https://github.com/golang/go/issues/11058) 。這里我們隻展示的cgo很小的一些方面更多的關於內存管理、指針、迴調函數、中斷信號處理、字符串、errno處理、終結器以及goroutines和繫統線程的關繫等有很多細節可以討論。特别是如何將Go語言的指針傳入C函數的規則也是異常複雜的譯註簡單來説要傳入C函數的Go指針指向的數據本身不能包含指針或其他引用類型併且C函數在返迴後不能繼續持有Go指針併且在C函數返迴之前Go指針是被鎖定的不能導致對應指針數據被移動或棧的調整部分的原因在13.2節有討論到但是在Go1.5中還沒有被明確譯註Go1.6將會明確cgo中的指針使用規則。如果要進一步閲讀可以從 https://golang.org/cmd/cgo 開始。
们演示了如何将一个C语言库链接到Go语言程序。相反, 将Go编译为静态库然后链接到C程序或者将Go程序编译为动态库然后在C程序中动态加载也都是可行的译注在Go1.5中Windows系统的Go语言实现并不支持生成C语言动态库或静态库的特性。不过好消息是目前已经有人在尝试解决这个问题具体请访问 [Issue11058](https://github.com/golang/go/issues/11058) 。这里我们只展示的cgo很小的一些方面更多的关于内存管理、指针、回调函数、中断信号处理、字符串、errno处理、终结器以及goroutines和系统线程的关系等有很多细节可以讨论。特别是如何将Go语言的指针传入C函数的规则也是异常复杂的译注简单来说要传入C函数的Go指针指向的数据本身不能包含指针或其他引用类型并且C函数在返回后不能继续持有Go指针并且在C函数返回之前Go指针是被锁定的不能导致对应指针数据被移动或栈的调整部分的原因在13.2节有讨论到但是在Go1.5中还没有被明确译注Go1.6将会明确cgo中的指针使用规则。如果要进一步阅读可以从 https://golang.org/cmd/cgo 开始。
**練習 13.3** 使用sync.Mutex以保證bzip2.writer在多個goroutines中被併發調用是安全的。
**练习 13.3** 使用sync.Mutex以保证bzip2.writer在多个goroutines中被并发调用是安全的。
**練習 13.4** 因爲C庫依賴的限製。 使用os/exec包啟動/bin/bzip2命令作爲一個子進程提供一個純Go的bzip.NewWriter的替代實現譯註雖然是純Go實現但是運行時將依賴/bin/bzip2命令其他操作繫統可能無法運行)。
**练习 13.4** 因为C库依赖的限制。 使用os/exec包启动/bin/bzip2命令作为一个子进程提供一个纯Go的bzip.NewWriter的替代实现译注虽然是纯Go实现但是运行时将依赖/bin/bzip2命令其他操作系统可能无法运行)。

View File

@ -1,12 +1,12 @@
## 13.5. 幾點忠告
## 13.5. 几点忠告
們在前一章結尾的時候我們警告要謹慎使用reflect包。那些警告同樣適用於本章的unsafe包。
们在前一章结尾的时候我们警告要谨慎使用reflect包。那些警告同样适用于本章的unsafe包。
級語言使得程序員不用在關心眞正運行程序的指令細節,同時也不再需要關註許多如內存布局之類的實現細節。因爲高級語言這個絶緣的抽象層,我們可以編寫安全健壯的,併且可以運行在不同操作繫統上的具有高度可移植性的程序。
级语言使得程序员不用在关心真正运行程序的指令细节,同时也不再需要关注许多如内存布局之类的实现细节。因为高级语言这个绝缘的抽象层,我们可以编写安全健壮的,并且可以运行在不同操作系统上的具有高度可移植性的程序。
但是unsafe包讓程序員可以透過這個絶緣的抽象層直接使用一些必要的功能雖然可能是爲了獲得更好的性能。但是代價就是犧牲了可移植性和程序安全因此使用unsafe包是一個危險的行爲。我們對何時以及如何使用unsafe包的建議和我們在11.5節提到的Knuth對過早優化的建議類似。大多數Go程序員可能永遠不會需要直接使用unsafe包。當然也永遠都會有一些需要使用unsafe包實現會更簡單的場景。如果確實認爲使用unsafe包是最理想的方式那麽應該盡可能將它限製在較小的范圍那樣其它代碼就忽略unsafe的影響
但是unsafe包让程序员可以透过这个绝缘的抽象层直接使用一些必要的功能虽然可能是为了获得更好的性能。但是代价就是牺牲了可移植性和程序安全因此使用unsafe包是一个危险的行为。我们对何时以及如何使用unsafe包的建议和我们在11.5节提到的Knuth对过早优化的建议类似。大多数Go程序员可能永远不会需要直接使用unsafe包。当然也永远都会有一些需要使用unsafe包实现会更简单的场景。如果确实认为使用unsafe包是最理想的方式那么应该尽可能将它限制在较小的范围那样其它代码就忽略unsafe的影响
現在趕緊將最後兩章拋入腦後吧。編寫一些實實在在的應用是眞理。請遠離reflect的unsafe包除非你確實需要它們
现在赶紧将最后两章抛入脑后吧。编写一些实实在在的应用是真理。请远离reflect的unsafe包除非你确实需要它们
用Go快樂地編程。我們希望你能像我們一樣喜歡Go語言。
用Go快乐地编程。我们希望你能像我们一样喜欢Go语言。

View File

@ -1,20 +1,20 @@
# 第13章 底層編
# 第13章 底层编
Go語言的設計包含了諸多安全策略限製了可能導致程序運行出現錯誤的用法。編譯時類型檢査檢査可以發現大多數類型不匹配的操作例如兩個字符串做減法的錯誤。字符串、map、slice和chan等所有的內置類型都有嚴格的類型轉換規則
Go语言的设计包含了诸多安全策略限制了可能导致程序运行出现错误的用法。编译时类型检查检查可以发现大多数类型不匹配的操作例如两个字符串做减法的错误。字符串、map、slice和chan等所有的内置类型都有严格的类型转换规则
對於無法靜態檢測到的錯誤,例如數組訪問越界或使用空指針,運行時動態檢測可以保證程序在遇到問題的時候立卽終止併打印相關的錯誤信息。自動內存管理(垃圾內存自動迴收)可以消除大部分野指針和內存洩漏相關的問題
对于无法静态检测到的错误,例如数组访问越界或使用空指针,运行时动态检测可以保证程序在遇到问题的时候立即终止并打印相关的错误信息。自动内存管理(垃圾内存自动回收)可以消除大部分野指针和内存泄漏相关的问题
Go語言的實現刻意隱藏了很多底層細節。我們無法知道一個結構體眞實的內存布局也無法獲取一個運行時函數對應的機器碼也無法知道當前的goroutine是運行在哪個操作繫統線程之上。事實上Go語言的調度器會自己決定是否需要將某個goroutine從一個操作繫統線程轉移到另一個操作繫統線程。一個指向變量的指針也併沒有展示變量眞實的地址。因爲垃圾迴收器可能會根據需要移動變量的內存位置當然變量對應的地址也會被自動更新。
Go语言的实现刻意隐藏了很多底层细节。我们无法知道一个结构体真实的内存布局也无法获取一个运行时函数对应的机器码也无法知道当前的goroutine是运行在哪个操作系统线程之上。事实上Go语言的调度器会自己决定是否需要将某个goroutine从一个操作系统线程转移到另一个操作系统线程。一个指向变量的指针也并没有展示变量真实的地址。因为垃圾回收器可能会根据需要移动变量的内存位置当然变量对应的地址也会被自动更新。
總的來説Go語言的這些特性使得Go程序相比較低級的C語言來説更容易預測和理解程序也不容易崩潰。通過隱藏底層的實現細節也使得Go語言編寫的程序具有高度的可移植性因爲語言的語義在很大程度上是獨立於任何編譯器實現、操作繫統和CPU繫統結構的當然也不是完全絶對獨立例如int等類型就依賴於CPU機器字的大小某些表達式求值的具體順序還有編譯器實現的一些額外的限製等)。
总的来说Go语言的这些特性使得Go程序相比较低级的C语言来说更容易预测和理解程序也不容易崩溃。通过隐藏底层的实现细节也使得Go语言编写的程序具有高度的可移植性因为语言的语义在很大程度上是独立于任何编译器实现、操作系统和CPU系统结构的当然也不是完全绝对独立例如int等类型就依赖于CPU机器字的大小某些表达式求值的具体顺序还有编译器实现的一些额外的限制等)。
時候我們可能會放棄使用部分語言特性而優先選擇更好具有更好性能的方法例如需要與其他語言編寫的庫互操作或者用純Go語言無法實現的某些函數
时候我们可能会放弃使用部分语言特性而优先选择更好具有更好性能的方法例如需要与其他语言编写的库互操作或者用纯Go语言无法实现的某些函数
在本章,我們將展示如何使用unsafe包來襬脫Go語言規則帶來的限製講述如何創建C語言函數庫的綁定以及如何進行繫統調用。
在本章,我们将展示如何使用unsafe包来摆脱Go语言规则带来的限制讲述如何创建C语言函数库的绑定以及如何进行系统调用。
本章提供的方法不應該輕易使用譯註屬於黑魔法雖然可能功能很強大但是也容易誤傷到自己。如果沒有處理好細節它們可能導致各種不可預測的併且隱晦的錯誤甚至連有經驗的的C語言程序員也無法理解這些錯誤。使用unsafe包的同時也放棄了Go語言保證與未來版本的兼容性的承諾因爲它必然會在有意無意中會使用很多實現的細節而這些實現的細節在未來的Go語言中很可能會被改變
本章提供的方法不应该轻易使用译注属于黑魔法虽然可能功能很强大但是也容易误伤到自己。如果没有处理好细节它们可能导致各种不可预测的并且隐晦的错误甚至连有经验的的C语言程序员也无法理解这些错误。使用unsafe包的同时也放弃了Go语言保证与未来版本的兼容性的承诺因为它必然会在有意无意中会使用很多实现的细节而这些实现的细节在未来的Go语言中很可能会被改变
註意的是unsafe包是一個采用特殊方式實現的包。雖然它可以和普通包一樣的導入和使用但它實際上是由編譯器實現的。它提供了一些訪問語言內部特性的方法特别是內存布局相關的細節。將這些特性封裝到一個獨立的包中是爲在極少數情況下需要使用的時候同時引起人們的註意譯註因爲看包的名字就知道使用unsafe包是不安全的。此外有一些環境因爲安全的因素可能限製這個包的使用。
注意的是unsafe包是一个采用特殊方式实现的包。虽然它可以和普通包一样的导入和使用但它实际上是由编译器实现的。它提供了一些访问语言内部特性的方法特别是内存布局相关的细节。将这些特性封装到一个独立的包中是为在极少数情况下需要使用的时候同时引起人们的注意译注因为看包的名字就知道使用unsafe包是不安全的。此外有一些环境因为安全的因素可能限制这个包的使用。
過unsafe包被廣泛地用於比較低級的包, 例如runtime、os、syscall還有net包等因爲它們需要和操作繫統密切配合但是對於普通的程序一般是不需要使用unsafe包的。
过unsafe包被广泛地用于比较低级的包, 例如runtime、os、syscall还有net包等因为它们需要和操作系统密切配合但是对于普通的程序一般是不需要使用unsafe包的。

View File

@ -1,8 +1,8 @@
## 2.1. 命名
Go語言中的函數名、變量名、常量名、類型名、語句標號和包名等所有的命名都遵循一個簡單的命名規則一個名字必須以一個字母Unicode字母或下劃線開頭後面可以跟任意數量的字母、數字或下劃線。大寫字母和小寫字母是不同的heapSort和Heapsort是兩個不同的名字。
Go语言中的函数名、变量名、常量名、类型名、语句标号和包名等所有的命名都遵循一个简单的命名规则一个名字必须以一个字母Unicode字母或下划线开头后面可以跟任意数量的字母、数字或下划线。大写字母和小写字母是不同的heapSort和Heapsort是两个不同的名字。
Go語言中類似if和switch的關鍵字有25個關鍵字不能用於自定義名字隻能在特定語法結構中使用。
Go语言中类似if和switch的关键字有25个关键字不能用于自定义名字只能在特定语法结构中使用。
```
break default func interface select
@ -12,25 +12,25 @@ const fallthrough if range type
continue for import return var
```
此外,還有大約30多個預定義的名字比如int和true等主要對應內建的常量、類型和函數
此外,还有大约30多个预定义的名字比如int和true等主要对应内建的常量、类型和函数
```
建常量: true false iota nil
建常量: true false iota nil
內建類型: int int8 int16 int32 int64
内建类型: int int8 int16 int32 int64
uint uint8 uint16 uint32 uint64 uintptr
float32 float64 complex128 complex64
bool byte rune string error
內建函數: make len cap new append copy close delete
内建函数: make len cap new append copy close delete
complex real imag
panic recover
```
這些內部預先定義的名字併不是關鍵字,你可以再定義中重新使用它們。在一些特殊的場景中重新定義它們也是有意義的,但是也要註意避免過度而引起語義混亂
这些内部预先定义的名字并不是关键字,你可以再定义中重新使用它们。在一些特殊的场景中重新定义它们也是有意义的,但是也要注意避免过度而引起语义混乱
如果一個名字是在函數內部定義那麽它的就隻在函數內部有效。如果是在函數外部定義那麽將在當前包的所有文件中都可以訪問。名字的開頭字母的大小寫決定了名字在包外的可見性。如果一個名字是大寫字母開頭的譯註必須是在函數外部定義的包級名字包級函數名本身也是包級名字那麽它將是導出的也就是説可以被外部的包訪問例如fmt包的Printf函數就是導出的可以在fmt包外部訪問。包本身的名字一般總是用小寫字母。
如果一个名字是在函数内部定义那么它的就只在函数内部有效。如果是在函数外部定义那么将在当前包的所有文件中都可以访问。名字的开头字母的大小写决定了名字在包外的可见性。如果一个名字是大写字母开头的译注必须是在函数外部定义的包级名字包级函数名本身也是包级名字那么它将是导出的也就是说可以被外部的包访问例如fmt包的Printf函数就是导出的可以在fmt包外部访问。包本身的名字一般总是用小写字母。
名字的長度沒有邏輯限製但是Go語言的風格是盡量使用短小的名字對於局部變量尤其是這樣你會經常看到i之類的短名字而不是冗長的theLoopIndex命名。通常來説如果一個名字的作用域比較大生命週期也比較長那麽用長的名字將會更有意義
名字的长度没有逻辑限制但是Go语言的风格是尽量使用短小的名字对于局部变量尤其是这样你会经常看到i之类的短名字而不是冗长的theLoopIndex命名。通常来说如果一个名字的作用域比较大生命周期也比较长那么用长的名字将会更有意义
習慣上Go語言程序員推薦使用 **駝峯式** 命名當名字有幾個單詞組成的時優先使用大小寫分隔而不是優先用下劃線分隔。因此在標準庫有QuoteRuneToASCII和parseRequestLine這樣的函數命名但是一般不會用quote_rune_to_ASCII和parse_request_line這樣的命名。而像ASCII和HTML這樣的縮略詞則避免使用大小寫混合的寫法它們可能被稱爲htmlEscape、HTMLEscape或escapeHTML但不會是escapeHtml。
习惯上Go语言程序员推荐使用 **驼峰式** 命名当名字有几个单词组成的时优先使用大小写分隔而不是优先用下划线分隔。因此在标准库有QuoteRuneToASCII和parseRequestLine这样的函数命名但是一般不会用quote_rune_to_ASCII和parse_request_line这样的命名。而像ASCII和HTML这样的缩略词则避免使用大小写混合的写法它们可能被称为htmlEscape、HTMLEscape或escapeHTML但不会是escapeHtml。

View File

@ -1,8 +1,8 @@
## 2.2.
## 2.2.
聲明語句定義了程序的各種實體對象以及部分或全部的屬性。Go語言主要有四種類型的聲明語句var、const、type和func分别對應變量、常量、類型和函數實體對象的聲明。這一章我們重點討論變量和類型的聲明第三章將討論常量的聲明第五章將討論函數的聲明。
声明语句定义了程序的各种实体对象以及部分或全部的属性。Go语言主要有四种类型的声明语句var、const、type和func分别对应变量、常量、类型和函数实体对象的声明。这一章我们重点讨论变量和类型的声明第三章将讨论常量的声明第五章将讨论函数的声明。
個Go語言編寫的程序對應一個或多個以.go爲文件後綴名的源文件中。每個源文件以包的聲明語句開始説明該源文件是屬於哪個包。包聲明語句之後是import語句導入依賴的其它包然後是包一級的類型、變量、常量、函數的聲明語句包一級的各種類型的聲明語句的順序無關緊要譯註函數內部的名字則必須先聲明之後才能使用。例如下面的例子中聲明了一個常量、一個函數和兩個變量:
个Go语言编写的程序对应一个或多个以.go为文件后缀名的源文件中。每个源文件以包的声明语句开始说明该源文件是属于哪个包。包声明语句之后是import语句导入依赖的其它包然后是包一级的类型、变量、常量、函数的声明语句包一级的各种类型的声明语句的顺序无关紧要译注函数内部的名字则必须先声明之后才能使用。例如下面的例子中声明了一个常量、一个函数和两个变量:
<u><i>gopl.io/ch2/boiling</i></u>
```Go
@ -22,11 +22,11 @@ func main() {
}
```
其中常量boilingF是在包一級范圍聲明語句聲明的然後f和c兩個變量是在main函數內部聲明的聲明語句聲明的。在包一級聲明語句聲明的名字可在整個包對應的每個源文件中訪問而不是僅僅在其聲明語句所在的源文件中訪問。相比之下局部聲明的名字就隻能在函數內部很小的范圍被訪問
其中常量boilingF是在包一级范围声明语句声明的然后f和c两个变量是在main函数内部声明的声明语句声明的。在包一级声明语句声明的名字可在整个包对应的每个源文件中访问而不是仅仅在其声明语句所在的源文件中访问。相比之下局部声明的名字就只能在函数内部很小的范围被访问
個函數的聲明由一個函數名字、參數列表由函數的調用者提供參數變量的具體值、一個可選的返迴值列表和包含函數定義的函數體組成。如果函數沒有返迴值那麽返迴值列表是省略的。執行函數從函數的第一個語句開始依次順序執行直到遇到renturn返迴語句如果沒有返迴語句則是執行到函數末尾然後返迴到函數調用者。
个函数的声明由一个函数名字、参数列表由函数的调用者提供参数变量的具体值、一个可选的返回值列表和包含函数定义的函数体组成。如果函数没有返回值那么返回值列表是省略的。执行函数从函数的第一个语句开始依次顺序执行直到遇到renturn返回语句如果没有返回语句则是执行到函数末尾然后返回到函数调用者。
們已經看到過很多函數聲明和函數調用的例子了在第五章將深入討論函數的相關細節這里隻簡單解釋下。下面的fToC函數封裝了溫度轉換的處理邏輯這樣它隻需要被定義一次就可以在多個地方多次被使用。在這個例子中main函數就調用了兩次fToC函數分别是使用在局部定義的兩個常量作爲調用函數的參數
们已经看到过很多函数声明和函数调用的例子了在第五章将深入讨论函数的相关细节这里只简单解释下。下面的fToC函数封装了温度转换的处理逻辑这样它只需要被定义一次就可以在多个地方多次被使用。在这个例子中main函数就调用了两次fToC函数分别是使用在局部定义的两个常量作为调用函数的参数
<u><i>gopl.io/ch2/ftoc</i></u>
```Go

View File

@ -1,6 +1,6 @@
### 2.3.1. 簡短變量聲
### 2.3.1. 简短变量声
在函數內部,有一種稱爲簡短變量聲明語句的形式可用於聲明和初始化局部變量。它以“名字 := 表達式”形式聲明變量變量的類型根據表達式來自動推導。下面是lissajous函數中的三個簡短變量聲明語§1.4
在函数内部,有一种称为简短变量声明语句的形式可用于声明和初始化局部变量。它以“名字 := 表达式”形式声明变量变量的类型根据表达式来自动推导。下面是lissajous函数中的三个简短变量声明语§1.4
```Go
anim := gif.GIF{LoopCount: nframes}
@ -8,7 +8,7 @@ freq := rand.Float64() * 3.0
t := 0.0
```
爲簡潔和靈活的特點簡短變量聲明被廣泛用於大部分的局部變量的聲明和初始化。var形式的聲明語句往往是用於需要顯式指定變量類型地方或者因爲變量稍後會被重新賦值而初始值無關緊要的地方。
为简洁和灵活的特点简短变量声明被广泛用于大部分的局部变量的声明和初始化。var形式的声明语句往往是用于需要显式指定变量类型地方或者因为变量稍后会被重新赋值而初始值无关紧要的地方。
```Go
i := 100 // an int
@ -18,21 +18,21 @@ var err error
var p Point
```
和var形式聲明變語句一樣,簡短變量聲明語句也可以用來聲明和初始化一組變量:
和var形式声明变语句一样,简短变量声明语句也可以用来声明和初始化一组变量:
```Go
i, j := 0, 1
```
但是這種同時聲明多個變量的方式應該限製隻在可以提高代碼可讀性的地方使用比如for語句的循環的初始化語句部分。
但是这种同时声明多个变量的方式应该限制只在可以提高代码可读性的地方使用比如for语句的循环的初始化语句部分。
請記住“:=”是一個變量聲明語句,而“=是一個變量賦值操作。也不要混淆多個變量的聲明和元組的多重賦值§2.4.1),後者是將右邊各個的表達式值賦值給左邊對應位置的各個變量:
请记住“:=”是一个变量声明语句,而“=是一个变量赋值操作。也不要混淆多个变量的声明和元组的多重赋值§2.4.1),后者是将右边各个的表达式值赋值给左边对应位置的各个变量:
```Go
i, j = j, i // 交 i 和 j 的值
i, j = j, i // 交 i 和 j 的值
```
和普通var形式的變量聲明語句一樣簡短變量聲明語句也可以用函數的返迴值來聲明和初始化變量像下面的os.Open函數調用將返迴兩個值:
和普通var形式的变量声明语句一样简短变量声明语句也可以用函数的返回值来声明和初始化变量像下面的os.Open函数调用将返回两个值:
```Go
f, err := os.Open(name)
@ -43,9 +43,9 @@ if err != nil {
f.Close()
```
這里有一個比較微妙的地方簡短變量聲明左邊的變量可能併不是全部都是剛剛聲明的。如果有一些已經在相同的詞法域聲明過了§2.7),那麽簡短變量聲明語句對這些已經聲明過的變量就隻有賦值行爲了。
这里有一个比较微妙的地方简短变量声明左边的变量可能并不是全部都是刚刚声明的。如果有一些已经在相同的词法域声明过了§2.7),那么简短变量声明语句对这些已经声明过的变量就只有赋值行为了。
在下面的代碼中第一個語句聲明了in和err兩個變量。在第二個語句隻聲明了out一個變量然後對已經聲明的err進行了賦值操作。
在下面的代码中第一个语句声明了in和err两个变量。在第二个语句只声明了out一个变量然后对已经声明的err进行了赋值操作。
```Go
in, err := os.Open(infile)
@ -53,7 +53,7 @@ in, err := os.Open(infile)
out, err := os.Create(outfile)
```
簡短變量聲明語句中必須至少要聲明一個新的變量,下面的代碼將不能編譯通過
简短变量声明语句中必须至少要声明一个新的变量,下面的代码将不能编译通过
```Go
f, err := os.Open(infile)
@ -61,9 +61,9 @@ f, err := os.Open(infile)
f, err := os.Create(outfile) // compile error: no new variables
```
決的方法是第二個簡短變量聲明語句改用普通的多重賦值語言。
决的方法是第二个简短变量声明语句改用普通的多重赋值语言。
簡短變量聲明語句隻有對已經在同級詞法域聲明過的變量才和賦值操作語句等價,如果變量是在外部詞法域聲明的,那麽簡短變量聲明語句將會在當前詞法域重新聲明一個新的變量。我們在本章後面將會看到類似的例子。
简短变量声明语句只有对已经在同级词法域声明过的变量才和赋值操作语句等价,如果变量是在外部词法域声明的,那么简短变量声明语句将会在当前词法域重新声明一个新的变量。我们在本章后面将会看到类似的例子。

View File

@ -1,10 +1,10 @@
### 2.3.2. 指
### 2.3.2. 指
個變量對應一個保存了變量對應類型值的內存空間。普通變量在聲明語句創建時被綁定到一個變量名比如叫x的變量但是還有很多變量始終以表達式方式引入例如x[i]或x.f變量。所有這些表達式一般都是讀取一個變量的值除非它們是出現在賦值語句的左邊這種時候是給對應變量賦予一個新的值。
个变量对应一个保存了变量对应类型值的内存空间。普通变量在声明语句创建时被绑定到一个变量名比如叫x的变量但是还有很多变量始终以表达式方式引入例如x[i]或x.f变量。所有这些表达式一般都是读取一个变量的值除非它们是出现在赋值语句的左边这种时候是给对应变量赋予一个新的值。
個指針的值是另一個變量的地址。一個指針對應變量在內存中的存儲位置。併不是每一個值都會有一個內存地址,但是對於每一個變量必然有對應的內存地址。通過指針,我們可以直接讀或更新對應變量的值,而不需要知道該變量的名字(如果變量有名字的話)。
个指针的值是另一个变量的地址。一个指针对应变量在内存中的存储位置。并不是每一个值都会有一个内存地址,但是对于每一个变量必然有对应的内存地址。通过指针,我们可以直接读或更新对应变量的值,而不需要知道该变量的名字(如果变量有名字的话)。
如果用“var x int”聲明語句聲明一個x變量那麽&x表達式取x變量的內存地址將産生一個指向該整數變量的指針指針對應的數據類型是`*int`指針被稱之爲“指向int類型的指針”。如果指針名字爲p那麽可以説“p指針指向變量x”或者説“p指針保存了x變量的內存地址”。同時`*p`表達式對應p指針指向的變量的值。一般`*p`表達式讀取指針指向的變量的值這里爲int類型的值同時因爲`*p`對應一個變量,所以該表達式也可以出現在賦值語句的左邊,表示更新指針所指向的變量的值。
如果用“var x int”声明语句声明一个x变量那么&x表达式取x变量的内存地址将产生一个指向该整数变量的指针指针对应的数据类型是`*int`指针被称之为“指向int类型的指针”。如果指针名字为p那么可以说“p指针指向变量x”或者说“p指针保存了x变量的内存地址”。同时`*p`表达式对应p指针指向的变量的值。一般`*p`表达式读取指针指向的变量的值这里为int类型的值同时因为`*p`对应一个变量,所以该表达式也可以出现在赋值语句的左边,表示更新指针所指向的变量的值。
```Go
x := 1
@ -14,18 +14,18 @@ fmt.Println(*p) // "1"
fmt.Println(x) // "2"
```
對於聚合類型每個成員——比如結構體的每個字段、或者是數組的每個元素——也都是對應一個變量,因此可以被取地址。
对于聚合类型每个成员——比如结构体的每个字段、或者是数组的每个元素——也都是对应一个变量,因此可以被取地址。
變量有時候被稱爲可尋址的值。卽使變量由表達式臨時生成,那麽表達式也必須能接受`&`取地址操作。
变量有时候被称为可寻址的值。即使变量由表达式临时生成,那么表达式也必须能接受`&`取地址操作。
任何類型的指針的零值都是nil。如果`p != nil`測試爲眞那麽p是指向某個有效變量。指針之間也是可以進行相等測試的隻有當它們指向同一個變量或全部是nil時才相等。
任何类型的指针的零值都是nil。如果`p != nil`测试为真那么p是指向某个有效变量。指针之间也是可以进行相等测试的只有当它们指向同一个变量或全部是nil时才相等。
```Go
var x, y int
fmt.Println(&x == &x, &x == &y, &x == nil) // "true false false"
```
在Go語言中返迴函數中局部變量的地址也是安全的。例如下面的代碼調用f函數時創建局部變量v在局部變量地址被返迴之後依然有效因爲指針p依然引用這個變量。
在Go语言中返回函数中局部变量的地址也是安全的。例如下面的代码调用f函数时创建局部变量v在局部变量地址被返回之后依然有效因为指针p依然引用这个变量。
```Go
var p = f()
@ -36,17 +36,17 @@ func f() *int {
}
```
每次調用f函數都將返迴不同的結果:
每次调用f函数都将返回不同的结果:
```Go
fmt.Println(f() == f()) // "false"
```
爲指針包含了一個變量的地址因此如果將指針作爲參數調用函數那將可以在函數中通過該指針來更新變量的值。例如下面這個例子就是通過指針來更新變量的值然後返迴更新後的值可用在一個表達式中譯註這是對C語言中`++v`操作的模擬這里隻是爲了説明指針的用法incr函數模擬的做法併不推薦
为指针包含了一个变量的地址因此如果将指针作为参数调用函数那将可以在函数中通过该指针来更新变量的值。例如下面这个例子就是通过指针来更新变量的值然后返回更新后的值可用在一个表达式中译注这是对C语言中`++v`操作的模拟这里只是为了说明指针的用法incr函数模拟的做法并不推荐
```Go
func incr(p *int) int {
*p++ // 非常重要:隻是增加p指向的變量的值併不改變p指針
*p++ // 非常重要:只是增加p指向的变量的值并不改变p指针
return *p
}
@ -55,9 +55,9 @@ incr(&v) // side effect: v is now 2
fmt.Println(incr(&v)) // "3" (and v is 3)
```
每次我們對一個變量取地址,或者複製指針,我們都是爲原變量創建了新的别名。例如,`*p`就是是 變量v的别名。指針特别有價值的地方在於我們可以不用名字而訪問一個變量但是這是一把雙刃劍要找到一個變量的所有訪問者併不容易我們必須知道變量全部的别名譯註這是Go語言的垃圾迴收器所做的工作。不僅僅是指針會創建别名很多其他引用類型也會創建别名例如slice、map和chan甚至結構體、數組和接口都會創建所引用變量的别名。
每次我们对一个变量取地址,或者复制指针,我们都是为原变量创建了新的别名。例如,`*p`就是是 变量v的别名。指针特别有价值的地方在于我们可以不用名字而访问一个变量但是这是一把双刃剑要找到一个变量的所有访问者并不容易我们必须知道变量全部的别名译注这是Go语言的垃圾回收器所做的工作。不仅仅是指针会创建别名很多其他引用类型也会创建别名例如slice、map和chan甚至结构体、数组和接口都会创建所引用变量的别名。
針是實現標準庫中flag包的關鍵技術它使用命令行參數來設置對應變量的值而這些對應命令行標誌參數的變量可能會零散分布在整個程序中。爲了説明這一點在早些的echo版本中就包含了兩個可選的命令行參數`-n`用於忽略行尾的換行符,`-s sep`用於指定分隔字符(默認是空格)。下面這是第四個版本,對應包路徑爲gopl.io/ch2/echo4。
针是实现标准库中flag包的关键技术它使用命令行参数来设置对应变量的值而这些对应命令行标志参数的变量可能会零散分布在整个程序中。为了说明这一点在早些的echo版本中就包含了两个可选的命令行参数`-n`用于忽略行尾的换行符,`-s sep`用于指定分隔字符(默认是空格)。下面这是第四个版本,对应包路径为gopl.io/ch2/echo4。
<u><i>gopl.io/ch2/echo4</i></u>
```Go
@ -82,11 +82,11 @@ func main() {
}
```
調用flag.Bool函數會創建一個新的對應布爾型標誌參數的變量。它有三個屬性第一個是的命令行標誌參數的名字“n”然後是該標誌參數的默認值這里是false最後是該標誌參數對應的描述信息。如果用戶在命令行輸入了一個無效的標誌參數或者輸入`-h`或`-help`參數那麽將打印所有標誌參數的名字、默認值和描述信息。類似的調用flag.String函數將於創建一個對應字符串類型的標誌參數變量同樣包含命令行標誌參數對應的參數名、默認值、和描述信息。程序中的`sep`和`n`變量分别是指向對應命令行標誌參數變量的指針,因此必須用`*sep`和`*n`形式的指針語法間接引用它們
调用flag.Bool函数会创建一个新的对应布尔型标志参数的变量。它有三个属性第一个是的命令行标志参数的名字“n”然后是该标志参数的默认值这里是false最后是该标志参数对应的描述信息。如果用户在命令行输入了一个无效的标志参数或者输入`-h`或`-help`参数那么将打印所有标志参数的名字、默认值和描述信息。类似的调用flag.String函数将于创建一个对应字符串类型的标志参数变量同样包含命令行标志参数对应的参数名、默认值、和描述信息。程序中的`sep`和`n`变量分别是指向对应命令行标志参数变量的指针,因此必须用`*sep`和`*n`形式的指针语法间接引用它们
當程序運行時必須在使用標誌參數對應的變量之前調用先flag.Parse函數用於更新每個標誌參數對應變量的值之前是默認值。對於非標誌參數的普通命令行參數可以通過調用flag.Args()函數來訪問返迴值對應對應一個字符串類型的slice。如果在flag.Parse函數解析命令行參數時遇到錯誤默認將打印相關的提示信息然後調用os.Exit(2)終止程序。
当程序运行时必须在使用标志参数对应的变量之前调用先flag.Parse函数用于更新每个标志参数对应变量的值之前是默认值。对于非标志参数的普通命令行参数可以通过调用flag.Args()函数来访问返回值对应对应一个字符串类型的slice。如果在flag.Parse函数解析命令行参数时遇到错误默认将打印相关的提示信息然后调用os.Exit(2)终止程序。
讓我們運行一些echo測試用例:
让我们运行一些echo测试用例:
```
$ go build gopl.io/ch2/echo4

View File

@ -1,17 +1,17 @@
### 2.3.3. new函
### 2.3.3. new函
另一個創建變量的方法是調用用內建的new函數。表達式new(T)將創建一個T類型的匿名變量初始化爲T類型的零值然後返迴變量地址返迴的指針類型爲`*T`。
另一个创建变量的方法是调用用内建的new函数。表达式new(T)将创建一个T类型的匿名变量初始化为T类型的零值然后返回变量地址返回的指针类型为`*T`。
```Go
p := new(int) // p, *int 類型, 指向匿名的 int 變
p := new(int) // p, *int 类型, 指向匿名的 int 变
fmt.Println(*p) // "0"
*p = 2 // 設置 int 匿名變量的值爲 2
*p = 2 // 设置 int 匿名变量的值为 2
fmt.Println(*p) // "2"
```
用new創建變量和普通變量聲明語句方式創建變量沒有什麽區别除了不需要聲明一個臨時變量的名字外我們還可以在表達式中使用new(T)。換言之new函數類似是一種語法糖而不是一個新的基礎概念。
用new创建变量和普通变量声明语句方式创建变量没有什么区别除了不需要声明一个临时变量的名字外我们还可以在表达式中使用new(T)。换言之new函数类似是一种语法糖而不是一个新的基础概念。
下面的兩個newInt函數有着相同的行爲
下面的两个newInt函数有着相同的行为
```Go
func newInt() *int {
@ -24,7 +24,7 @@ func newInt() *int {
}
```
每次調用new函數都是返迴一個新的變量的地址因此下面兩個地址是不同的:
每次调用new函数都是返回一个新的变量的地址因此下面两个地址是不同的:
```Go
p := new(int)
@ -32,15 +32,15 @@ q := new(int)
fmt.Println(p == q) // "false"
```
當然也可能有特殊情況如果兩個類型都是空的也就是説類型的大小是0例如`struct{}`和 `[0]int`, 有可能有相同的地址依賴具體的語言實現譯註請謹慎使用大小爲0的類型因爲如果類型的大小位0好話可能導致Go語言的自動垃圾迴收器有不同的行爲具體請査看`runtime.SetFinalizer`函數相關文檔)。
当然也可能有特殊情况如果两个类型都是空的也就是说类型的大小是0例如`struct{}`和 `[0]int`, 有可能有相同的地址依赖具体的语言实现译注请谨慎使用大小为0的类型因为如果类型的大小位0好话可能导致Go语言的自动垃圾回收器有不同的行为具体请查看`runtime.SetFinalizer`函数相关文档)。
new函數使用常見相對比較少,因爲對應結構體來説,可以直接用字面量語法創建新變量的方法會更靈§4.4.1)。
new函数使用常见相对比较少,因为对应结构体来说,可以直接用字面量语法创建新变量的方法会更灵§4.4.1)。
於new隻是一個預定義的函數它併不是一個關鍵字因此我們可以將new名字重新定義爲别的類型。例如下面的例子:
于new只是一个预定义的函数它并不是一个关键字因此我们可以将new名字重新定义为别的类型。例如下面的例子:
```Go
func delta(old, new int) int { return new - old }
```
於new被定義爲int類型的變量名因此在delta函數內部是無法使用內置的new函數的。
于new被定义为int类型的变量名因此在delta函数内部是无法使用内置的new函数的。

View File

@ -1,8 +1,8 @@
### 2.3.4. 變量的生命週
### 2.3.4. 变量的生命周
變量的生命週期指的是在程序運行期間變量有效存在的時間間隔。對於在包一級聲明的變量來説,它們的生命週期和整個程序的運行週期是一致的。而相比之下,在局部變量的聲明週期則是動態的:從每次創建一個新變量的聲明語句開始,直到該變量不再被引用爲止,然後變量的存儲空間可能被迴收。函數的參數變量和返迴值變量都是局部變量。它們在函數每次被調用的時候創建。
变量的生命周期指的是在程序运行期间变量有效存在的时间间隔。对于在包一级声明的变量来说,它们的生命周期和整个程序的运行周期是一致的。而相比之下,在局部变量的声明周期则是动态的:从每次创建一个新变量的声明语句开始,直到该变量不再被引用为止,然后变量的存储空间可能被回收。函数的参数变量和返回值变量都是局部变量。它们在函数每次被调用的时候创建。
例如,下面是從1.4節的Lissajous程序摘録的代碼片段:
例如,下面是从1.4节的Lissajous程序摘录的代码片段:
```Go
for t := 0.0; t < cycles*2*math.Pi; t += res {
@ -13,7 +13,7 @@ for t := 0.0; t < cycles*2*math.Pi; t += res {
}
```
譯註:函數的有右小括弧也可以另起一行縮進,同時爲了防止編譯器在行尾自動插入分號而導致的編譯錯誤,可以在末尾的參數變量後面顯式插入逗號。像下面這樣
译注:函数的有右小括弧也可以另起一行缩进,同时为了防止编译器在行尾自动插入分号而导致的编译错误,可以在末尾的参数变量后面显式插入逗号。像下面这样
```Go
for t := 0.0; t < cycles*2*math.Pi; t += res {
@ -21,18 +21,18 @@ for t := 0.0; t < cycles*2*math.Pi; t += res {
y := math.Sin(t*freq + phase)
img.SetColorIndex(
size+int(x*size+0.5), size+int(y*size+0.5),
blackIndex, // 最後插入的逗號不會導致編譯錯誤這是Go編譯器的一個特性
) // 小括弧另起一行縮進,和大括弧的風格保存一致
blackIndex, // 最后插入的逗号不会导致编译错误这是Go编译器的一个特性
) // 小括弧另起一行缩进,和大括弧的风格保存一致
}
```
在每次循環的開始會創建臨時變量t然後在每次循環迭代中創建臨時變量x和y。
在每次循环的开始会创建临时变量t然后在每次循环迭代中创建临时变量x和y。
麽垃Go語言的自動圾收集器是如何知道一個變量是何時可以被迴收的呢這里我們可以避開完整的技術細節基本的實現思路是從每個包級的變量和每個當前運行函數的每一個局部變量開始通過指針或引用的訪問路徑遍歷是否可以找到該變量。如果不存在這樣的訪問路徑那麽説明該變量是不可達的也就是説它是否存在併不會影響程序後續的計算結果。
么垃Go语言的自动圾收集器是如何知道一个变量是何时可以被回收的呢这里我们可以避开完整的技术细节基本的实现思路是从每个包级的变量和每个当前运行函数的每一个局部变量开始通过指针或引用的访问路径遍历是否可以找到该变量。如果不存在这样的访问路径那么说明该变量是不可达的也就是说它是否存在并不会影响程序后续的计算结果。
爲一個變量的有效週期隻取決於是否可達,因此一個循環迭代內部的局部變量的生命週期可能超出其局部作用域。同時,局部變量可能在函數返迴之後依然存在。
为一个变量的有效周期只取决于是否可达,因此一个循环迭代内部的局部变量的生命周期可能超出其局部作用域。同时,局部变量可能在函数返回之后依然存在。
編譯器會自動選擇在棧上還是在堆上分配局部變量的存儲空間但可能令人驚訝的是這個選擇併不是由用var還是new聲明變量的方式決定的。
编译器会自动选择在栈上还是在堆上分配局部变量的存储空间但可能令人惊讶的是这个选择并不是由用var还是new声明变量的方式决定的。
```Go
var global *int
@ -49,9 +49,9 @@ func g() {
}
```
f函數里的x變量必須在堆上分配因爲它在函數退出後依然可以通過包一級的global變量找到雖然它是在函數內部定義的用Go語言的術語説這個x局部變量從函數f中逃逸了。相反當g函數返迴時變量`*y`將是不可達的,也就是説可以馬上被迴收的。因此,`*y`併沒有從函數g中逃逸編譯器可以選擇在棧上分配`*y`的存儲空間譯註也可以選擇在堆上分配然後由Go語言的GC迴收這個變量的內存空間雖然這里用的是new方式。其實在任何時候你併不需爲了編寫正確的代碼而要考慮變量的逃逸行爲要記住的是逃逸的變量需要額外分配內存同時對性能的優化可能會産生細微的影響
f函数里的x变量必须在堆上分配因为它在函数退出后依然可以通过包一级的global变量找到虽然它是在函数内部定义的用Go语言的术语说这个x局部变量从函数f中逃逸了。相反当g函数返回时变量`*y`将是不可达的,也就是说可以马上被回收的。因此,`*y`并没有从函数g中逃逸编译器可以选择在栈上分配`*y`的存储空间译注也可以选择在堆上分配然后由Go语言的GC回收这个变量的内存空间虽然这里用的是new方式。其实在任何时候你并不需为了编写正确的代码而要考虑变量的逃逸行为要记住的是逃逸的变量需要额外分配内存同时对性能的优化可能会产生细微的影响
Go語言的自動垃圾收集器對編寫正確的代碼是一個鉅大的幫助,但也併不是説你完全不用考慮內存了。你雖然不需要顯式地分配和釋放內存,但是要編寫高效的程序你依然需要了解變量的生命週期。例如,如果將指向短生命週期對象的指針保存到具有長生命週期的對象中,特别是保存到全局變量時,會阻止對短生命週期對象的垃圾迴收(從而可能影響程序的性能)。
Go语言的自动垃圾收集器对编写正确的代码是一个巨大的帮助,但也并不是说你完全不用考虑内存了。你虽然不需要显式地分配和释放内存,但是要编写高效的程序你依然需要了解变量的生命周期。例如,如果将指向短生命周期对象的指针保存到具有长生命周期的对象中,特别是保存到全局变量时,会阻止对短生命周期对象的垃圾回收(从而可能影响程序的性能)。

View File

@ -1,32 +1,32 @@
## 2.3.
## 2.3.
var聲明語句可以創建一個特定類型的變量,然後給變量附加一個名字,併且設置變量的初始值。變量聲明的一般語法如下:
var声明语句可以创建一个特定类型的变量,然后给变量附加一个名字,并且设置变量的初始值。变量声明的一般语法如下:
```Go
var 變量名字 類型 = 表達
var 变量名字 类型 = 表达
```
其中“*類型*”或“*= 表達式*”兩個部分可以省略其中的一個。如果省略的是類型信息,那麽將根據初始化表達式來推導變量的類型信息。如果初始化表達式被省略,那麽將用零值初始化該變量。 數值類型變量對應的零值是0布爾類型變量對應的零值是false字符串類型對應的零值是空字符串接口或引用類型包括slice、map、chan和函數變量對應的零值是nil。數組或結構體等聚合類型對應的零值是每個元素或字段都是對應該類型的零值。
其中“*类型*”或“*= 表达式*”两个部分可以省略其中的一个。如果省略的是类型信息,那么将根据初始化表达式来推导变量的类型信息。如果初始化表达式被省略,那么将用零值初始化该变量。 数值类型变量对应的零值是0布尔类型变量对应的零值是false字符串类型对应的零值是空字符串接口或引用类型包括slice、map、chan和函数变量对应的零值是nil。数组或结构体等聚合类型对应的零值是每个元素或字段都是对应该类型的零值。
零值初始化機製可以確保每個聲明的變量總是有一個良好定義的值因此在Go語言中不存在未初始化的變量。這個特性可以簡化很多代碼而且可以在沒有增加額外工作的前提下確保邊界條件下的合理行爲。例如:
零值初始化机制可以确保每个声明的变量总是有一个良好定义的值因此在Go语言中不存在未初始化的变量。这个特性可以简化很多代码而且可以在没有增加额外工作的前提下确保边界条件下的合理行为。例如:
```Go
var s string
fmt.Println(s) // ""
```
這段代碼將打印一個空字符串而不是導致錯誤或産生不可預知的行爲。Go語言程序員應該讓一些聚合類型的零值也具有意義這樣可以保證不管任何類型的變量總是有一個合理有效的零值狀態
这段代码将打印一个空字符串而不是导致错误或产生不可预知的行为。Go语言程序员应该让一些聚合类型的零值也具有意义这样可以保证不管任何类型的变量总是有一个合理有效的零值状态
也可以在一個聲明語句中同時聲明一組變量,或用一組初始化表達式聲明併初始化一組變量。如果省略每個變量的類型,將可以聲明多個類型不同的變量(類型由初始化表達式推導
也可以在一个声明语句中同时声明一组变量,或用一组初始化表达式声明并初始化一组变量。如果省略每个变量的类型,将可以声明多个类型不同的变量(类型由初始化表达式推导
```Go
var i, j, k int // int, int, int
var b, f, s = true, 2.3, "four" // bool, float64, string
```
初始化表達式可以是字面量或任意的表達式。在包級别聲明的變量會在main入口函數執行前完成初始化§2.6.2),局部變量將在聲明語句被執行到的時候完成初始化。
初始化表达式可以是字面量或任意的表达式。在包级别声明的变量会在main入口函数执行前完成初始化§2.6.2),局部变量将在声明语句被执行到的时候完成初始化。
組變量也可以通過調用一個函數,由函數返迴的多個返迴值初始化:
组变量也可以通过调用一个函数,由函数返回的多个返回值初始化:
```Go
var f, err = os.Open(name) // os.Open returns a file and an error

View File

@ -1,6 +1,6 @@
### 2.4.1. 元組賦
### 2.4.1. 元组赋
組賦值是另一種形式的賦值語句,它允許同時更新多個變量的值。在賦值之前,賦值語句右邊的所有表達式將會先進行求值,然後再統一更新左邊對應變量的值。這對於處理有些同時出現在元組賦值語句左右兩邊的變量很有幫助,例如我們可以這樣交換兩個變量的值:
组赋值是另一种形式的赋值语句,它允许同时更新多个变量的值。在赋值之前,赋值语句右边的所有表达式将会先进行求值,然后再统一更新左边对应变量的值。这对于处理有些同时出现在元组赋值语句左右两边的变量很有帮助,例如我们可以这样交换两个变量的值:
```Go
x, y = y, x
@ -8,7 +8,7 @@ x, y = y, x
a[i], a[j] = a[j], a[i]
```
或者是計算兩個整數值的的最大公約數GCD譯註GCD不是那個敏感字而是greatest common divisor的縮寫歐幾里德的GCD是最早的非平凡算法
或者是计算两个整数值的的最大公约数GCD译注GCD不是那个敏感字而是greatest common divisor的缩写欧几里德的GCD是最早的非平凡算法
```Go
func gcd(x, y int) int {
@ -19,7 +19,7 @@ func gcd(x, y int) int {
}
```
或者是計算斐波納契數列Fibonacci的第N個數
或者是计算斐波纳契数列Fibonacci的第N个数
```Go
func fib(n int) int {
@ -31,21 +31,21 @@ func fib(n int) int {
}
```
組賦值也可以使一繫列瑣碎賦值更加緊湊(譯註: 特别是在for循環的初始化部分),
组赋值也可以使一系列琐碎赋值更加紧凑(译注: 特别是在for循环的初始化部分),
```Go
i, j, k = 2, 3, 5
```
但如果表達式太複雜的話,應該盡量避免過度使用元組賦值;因爲每個變量單獨賦值語句的寫法可讀性會更好。
但如果表达式太复杂的话,应该尽量避免过度使用元组赋值;因为每个变量单独赋值语句的写法可读性会更好。
有些表達式會産生多個值,比如調用一個有多個返迴值的函數。當這樣一個函數調用出現在元組賦值右邊的表達式中時(譯註:右邊不能再有其它表達式),左邊變量的數目必須和右邊一致。
有些表达式会产生多个值,比如调用一个有多个返回值的函数。当这样一个函数调用出现在元组赋值右边的表达式中时(译注:右边不能再有其它表达式),左边变量的数目必须和右边一致。
```Go
f, err = os.Open("foo.txt") // function call returns two values
```
通常,這類函數會用額外的返迴值來表達某種錯誤類型例如os.Open是用額外的返迴值返迴一個error類型的錯誤還有一些是用來返迴布爾值通常被稱爲ok。在稍後我們將看到的三個操作都是類似的用法。如果map査找§4.3、類型斷言§7.10或通道接收§8.4.2)出現在賦值語句的右邊,它們都可能會産生兩個結果,有一個額外的布爾結果表示操作是否成功:
通常,这类函数会用额外的返回值来表达某种错误类型例如os.Open是用额外的返回值返回一个error类型的错误还有一些是用来返回布尔值通常被称为ok。在稍后我们将看到的三个操作都是类似的用法。如果map查找§4.3、类型断言§7.10或通道接收§8.4.2)出现在赋值语句的右边,它们都可能会产生两个结果,有一个额外的布尔结果表示操作是否成功:
```Go
v, ok = m[key] // map lookup
@ -53,22 +53,22 @@ v, ok = x.(T) // type assertion
v, ok = <-ch // channel receive
```
譯註map査找§4.3、類型斷言§7.10或通道接收§8.4.2出現在賦值語句的右邊時併不一定是産生兩個結果也可能隻産生一個結果。對於值産生一個結果的情形map査找失敗時會返迴零值類型斷言失敗時會發送運行時panic異常通道接收失敗時會返迴零值阻塞不算是失敗)。例如下面的例子:
译注map查找§4.3、类型断言§7.10或通道接收§8.4.2出现在赋值语句的右边时并不一定是产生两个结果也可能只产生一个结果。对于值产生一个结果的情形map查找失败时会返回零值类型断言失败时会发送运行时panic异常通道接收失败时会返回零值阻塞不算是失败)。例如下面的例子:
```Go
v = m[key] // map査找,失敗時返迴零值
v = x.(T) // type斷言失敗時panic異
v = <-ch //
v = m[key] // map查找,失败时返回零值
v = x.(T) // type断言失败时panic异
v = <-ch //
_, ok = m[key] // map返迴2個
_, ok = mm[""], false // map返迴1個
_ = mm[""] // map返迴1個
_, ok = m[key] // map返回2个
_, ok = mm[""], false // map返回1个
_ = mm[""] // map返回1个
```
變量聲明一樣,我們可以用下劃線空白標識符`_`來丟棄不需要的值。
变量声明一样,我们可以用下划线空白标识符`_`来丢弃不需要的值。
```Go
_, err = io.Copy(dst, src) // 丟棄字節數
_, ok = x.(T) // 隻檢測類型,忽略具體
_, err = io.Copy(dst, src) // 丢弃字节数
_, ok = x.(T) // 只检测类型,忽略具体
```

View File

@ -1,12 +1,12 @@
### 2.4.2. 可值性
### 2.4.2. 可值性
賦值語句是顯式的賦值形式但是程序中還有很多地方會發生隱式的賦值行爲函數調用會隱式地將調用參數的值賦值給函數的參數變量一個返迴語句將隱式地將返迴操作的值賦值給結果變量一個複合類型的字面量§4.2)也會産生賦值行爲。例如下面的語句:
赋值语句是显式的赋值形式但是程序中还有很多地方会发生隐式的赋值行为函数调用会隐式地将调用参数的值赋值给函数的参数变量一个返回语句将隐式地将返回操作的值赋值给结果变量一个复合类型的字面量§4.2)也会产生赋值行为。例如下面的语句:
```Go
medals := []string{"gold", "silver", "bronze"}
```
隱式地對slice的每個元素進行賦值操作類似這樣寫的行爲
隐式地对slice的每个元素进行赋值操作类似这样写的行为
```Go
medals[0] = "gold"
@ -14,12 +14,12 @@ medals[1] = "silver"
medals[2] = "bronze"
```
map和chan的元素雖然不是普通的變量,但是也有類似的隱式賦值行爲
map和chan的元素虽然不是普通的变量,但是也有类似的隐式赋值行为
不管是隱式還是顯式地賦值,在賦值語句左邊的變量和右邊最終的求到的值必須有相同的數據類型。更直白地説,隻有右邊的值對於左邊的變量是可賦值的,賦值語句才是允許的。
不管是隐式还是显式地赋值,在赋值语句左边的变量和右边最终的求到的值必须有相同的数据类型。更直白地说,只有右边的值对于左边的变量是可赋值的,赋值语句才是允许的。
賦值性的規則對於不同類型有着不同要求對每個新類型特殊的地方我們會專門解釋。對於目前我們已經討論過的類型它的規則是簡單的類型必須完全匹配nil可以賦值給任何指針或引用類型的變量。常量§3.6)則有更靈活的賦值規則,因爲這樣可以避免不必要的顯式的類型轉換
赋值性的规则对于不同类型有着不同要求对每个新类型特殊的地方我们会专门解释。对于目前我们已经讨论过的类型它的规则是简单的类型必须完全匹配nil可以赋值给任何指针或引用类型的变量。常量§3.6)则有更灵活的赋值规则,因为这样可以避免不必要的显式的类型转换
對於兩個值是否可以用`==`或`!=`進行相等比較的能力也和可賦值能力有關繫:對於任何類型的值的相等比較,第二個值必須是對第一個值類型對應的變量是可賦值的,反之依然。和前面一樣,我們會對每個新類型比較特殊的地方做專門的解釋
对于两个值是否可以用`==`或`!=`进行相等比较的能力也和可赋值能力有关系:对于任何类型的值的相等比较,第二个值必须是对第一个值类型对应的变量是可赋值的,反之依然。和前面一样,我们会对每个新类型比较特殊的地方做专门的解释

View File

@ -1,28 +1,28 @@
## 2.4.
## 2.4.
使用賦值語句可以更新一個變量的值,最簡單的賦值語句是將要被賦值的變量放在=的左邊,新值的表達式放在=的右邊
使用赋值语句可以更新一个变量的值,最简单的赋值语句是将要被赋值的变量放在=的左边,新值的表达式放在=的右边
```Go
x = 1 // 命名變量的賦
*p = true // 通過指針間接賦
person.name = "bob" // 結構體字段賦
count[x] = count[x] * scale // 數組、slice或map的元素賦
x = 1 // 命名变量的赋
*p = true // 通过指针间接赋
person.name = "bob" // 结构体字段赋
count[x] = count[x] * scale // 数组、slice或map的元素赋
```
特定的二元算術運算符和賦值語句的複合操作有一個簡潔形式,例如上面最後的語句可以重寫爲
特定的二元算术运算符和赋值语句的复合操作有一个简洁形式,例如上面最后的语句可以重写为
```Go
count[x] *= scale
```
這樣可以省去對變量表達式的重複計算。
这样可以省去对变量表达式的重复计算。
數值變量也可以支持`++`遞增和`--`遞減語句(譯註:自增和自減是語句,而不是表達式,因此`x = i++`之類的表達式是錯誤的):
数值变量也可以支持`++`递增和`--`递减语句(译注:自增和自减是语句,而不是表达式,因此`x = i++`之类的表达式是错误的):
```Go
v := 1
v++ // 等價方式 v = v + 1v 變成 2
v-- // 等價方式 v = v - 1v 變成 1
v++ // 等价方式 v = v + 1v 变成 2
v-- // 等价方式 v = v - 1v 变成 1
```
{% include "./ch2-04-1.md" %}

View File

@ -1,24 +1,24 @@
## 2.5.
## 2.5.
變量或表達式的類型定義了對應存儲值的屬性特徵例如數值在內存的存儲大小或者是元素的bit個數它們在內部是如何表達的是否支持一些操作符以及它們自己關聯的方法集等。
变量或表达式的类型定义了对应存储值的属性特征例如数值在内存的存储大小或者是元素的bit个数它们在内部是如何表达的是否支持一些操作符以及它们自己关联的方法集等。
在任何程序中都會存在一些變量有着相同的內部結構但是卻表示完全不同的概念。例如一個int類型的變量可以用來表示一個循環的迭代索引、或者一個時間戳、或者一個文件描述符、或者一個月份一個float64類型的變量可以用來表示每秒移動幾米的速度、或者是不同溫度單位下的溫度一個字符串可以用來表示一個密碼或者一個顔色的名稱
在任何程序中都会存在一些变量有着相同的内部结构但是却表示完全不同的概念。例如一个int类型的变量可以用来表示一个循环的迭代索引、或者一个时间戳、或者一个文件描述符、或者一个月份一个float64类型的变量可以用来表示每秒移动几米的速度、或者是不同温度单位下的温度一个字符串可以用来表示一个密码或者一个颜色的名称
個類型聲明語句創建了一個新的類型名稱,和現有類型具有相同的底層結構。新命名的類型提供了一個方法,用來分隔不同概念的類型,這樣卽使它們底層類型相同也是不兼容的。
个类型声明语句创建了一个新的类型名称,和现有类型具有相同的底层结构。新命名的类型提供了一个方法,用来分隔不同概念的类型,这样即使它们底层类型相同也是不兼容的。
```Go
type 類型名字 底層類
type 类型名字 底层类
```
類型聲明語句一般出現在包一級,因此如果新創建的類型名字的首字符大寫,則在外部包也可以使用。
类型声明语句一般出现在包一级,因此如果新创建的类型名字的首字符大写,则在外部包也可以使用。
譯註對於中文漢字Unicode標誌都作爲小寫字母處理因此中文的命名默認不能導出不過国內的用戶針對該問題提出了不同的看法根據RobPike的迴複在Go2中有可能會將中日韓等字符當作大寫字母處理。下面是RobPik在 [Issue763](https://github.com/golang/go/issues/5763) 的迴複
译注对于中文汉字Unicode标志都作为小写字母处理因此中文的命名默认不能导出不过国内的用户针对该问题提出了不同的看法根据RobPike的回复在Go2中有可能会将中日韩等字符当作大写字母处理。下面是RobPik在 [Issue763](https://github.com/golang/go/issues/5763) 的回复
> A solution that's been kicking around for a while:
>
> For Go 2 (can't do it before then): Change the definition to “lower case letters and _ are package-local; all else is exported”. Then with non-cased languages, such as Japanese, we can write 日本語 for an exported name and _日本語 for a local name. This rule has no effect, relative to the Go 1 rule, with cased languages. They behave exactly the same.
> For Go 2 (can't do it before then): Change the definition to “lower case letters and _ are package-local; all else is exported”. Then with non-cased languages, such as Japanese, we can write 日本语 for an exported name and _日本语 for a local name. This rule has no effect, relative to the Go 1 rule, with cased languages. They behave exactly the same.
爲了説明類型聲明,我們將不同溫度單位分别定義爲不同的類型:
为了说明类型声明,我们将不同温度单位分别定义为不同的类型:
<u><i>gopl.io/ch2/tempconv0</i></u>
```Go
@ -27,13 +27,13 @@ package tempconv
import "fmt"
type Celsius float64 // 攝氏溫
type Fahrenheit float64 // 華氏溫
type Celsius float64 // 摄氏温
type Fahrenheit float64 // 华氏温
const (
AbsoluteZeroC Celsius = -273.15 // 絶對零度
FreezingC Celsius = 0 // 結冰點溫
BoilingC Celsius = 100 // 沸水
AbsoluteZeroC Celsius = -273.15 // 绝对零度
FreezingC Celsius = 0 // 结冰点温
BoilingC Celsius = 100 // 沸水
)
func CToF(c Celsius) Fahrenheit { return Fahrenheit(c*9/5 + 32) }
@ -41,13 +41,13 @@ func CToF(c Celsius) Fahrenheit { return Fahrenheit(c*9/5 + 32) }
func FToC(f Fahrenheit) Celsius { return Celsius((f - 32) * 5 / 9) }
```
們在這個包聲明了兩種類型Celsius和Fahrenheit分别對應不同的溫度單位。它們雖然有着相同的底層類型float64但是它們是不同的數據類型因此它們不可以被相互比較或混在一個表達式運算。刻意區分類型可以避免一些像無意中使用不同單位的溫度混合計算導致的錯誤因此需要一個類似Celsius(t)或Fahrenheit(t)形式的顯式轉型操作才能將float64轉爲對應的類型。Celsius(t)和Fahrenheit(t)是類型轉換操作它們併不是函數調用。類型轉換不會改變值本身但是會使它們的語義發生變化。另一方面CToF和FToC兩個函數則是對不同溫度單位下的溫度進行換算它們會返迴不同的值。
们在这个包声明了两种类型Celsius和Fahrenheit分别对应不同的温度单位。它们虽然有着相同的底层类型float64但是它们是不同的数据类型因此它们不可以被相互比较或混在一个表达式运算。刻意区分类型可以避免一些像无意中使用不同单位的温度混合计算导致的错误因此需要一个类似Celsius(t)或Fahrenheit(t)形式的显式转型操作才能将float64转为对应的类型。Celsius(t)和Fahrenheit(t)是类型转换操作它们并不是函数调用。类型转换不会改变值本身但是会使它们的语义发生变化。另一方面CToF和FToC两个函数则是对不同温度单位下的温度进行换算它们会返回不同的值。
對於每一個類型T都有一個對應的類型轉換操作T(x)用於將x轉爲T類型譯註如果T是指針類型可能會需要用小括弧包裝T比如`(*int)(0)`。隻有當兩個類型的底層基礎類型相同時才允許這種轉型操作或者是兩者都是指向相同底層結構的指針類型這些轉換隻改變類型而不會影響值本身。如果x是可以賦值給T類型的值那麽x必然也可以被轉爲T類型但是一般沒有這個必要。
对于每一个类型T都有一个对应的类型转换操作T(x)用于将x转为T类型译注如果T是指针类型可能会需要用小括弧包装T比如`(*int)(0)`。只有当两个类型的底层基础类型相同时才允许这种转型操作或者是两者都是指向相同底层结构的指针类型这些转换只改变类型而不会影响值本身。如果x是可以赋值给T类型的值那么x必然也可以被转为T类型但是一般没有这个必要。
數值類型之間的轉型也是允許的併且在字符串和一些特定類型的slice之間也是可以轉換的在下一章我們會看到這樣的例子。這類轉換可能改變值的表現。例如將一個浮點數轉爲整數將丟棄小數部分將一個字符串轉爲`[]byte`類型的slice將拷貝一個字符串數據的副本。在任何情況下運行時不會發生轉換失敗的錯誤譯註: 錯誤隻會發生在編譯階段)。
数值类型之间的转型也是允许的并且在字符串和一些特定类型的slice之间也是可以转换的在下一章我们会看到这样的例子。这类转换可能改变值的表现。例如将一个浮点数转为整数将丢弃小数部分将一个字符串转为`[]byte`类型的slice将拷贝一个字符串数据的副本。在任何情况下运行时不会发生转换失败的错误译注: 错误只会发生在编译阶段)。
層數據類型決定了內部結構和表達方式也決定是否可以像底層類型一樣對內置運算符的支持。這意味着Celsius和Fahrenheit類型的算術運算行爲和底層的float64類型是一樣的正如我們所期望的那樣
层数据类型决定了内部结构和表达方式也决定是否可以像底层类型一样对内置运算符的支持。这意味着Celsius和Fahrenheit类型的算术运算行为和底层的float64类型是一样的正如我们所期望的那样
```Go
fmt.Printf("%g\n", BoilingC-FreezingC) // "100" °C
@ -56,7 +56,7 @@ fmt.Printf("%g\n", boilingF-CToF(FreezingC)) // "180" °F
fmt.Printf("%g\n", boilingF-FreezingC) // compile error: type mismatch
```
較運算符`==`和`<`也可以用來比較一個命名類型的變量和另一個有相同類型的變量,或有着相同底層類型的未命名類型的值之間做比較。但是如果兩個值有着不同的類型,則不能直接進行比較
较运算符`==`和`<`也可以用来比较一个命名类型的变量和另一个有相同类型的变量,或有着相同底层类型的未命名类型的值之间做比较。但是如果两个值有着不同的类型,则不能直接进行比较
```Go
var c Celsius
@ -67,19 +67,19 @@ fmt.Println(c == f) // compile error: type mismatch
fmt.Println(c == Celsius(f)) // "true"!
```
註意最後那個語句。盡管看起來想函數調用但是Celsius(f)是類型轉換操作,它併不會改變值,僅僅是改變值的類型而已。測試爲眞的原因是因爲c和g都是零值。
注意最后那个语句。尽管看起来想函数调用但是Celsius(f)是类型转换操作,它并不会改变值,仅仅是改变值的类型而已。测试为真的原因是因为c和g都是零值。
個命名的類型可以提供書寫方便特别是可以避免一遍又一遍地書寫複雜類型譯註例如用匿名的結構體定義變量。雖然對於像float64這種簡單的底層類型沒有簡潔很多但是如果是複雜的類型將會簡潔很多特别是我們卽將討論的結構體類型。
个命名的类型可以提供书写方便特别是可以避免一遍又一遍地书写复杂类型译注例如用匿名的结构体定义变量。虽然对于像float64这种简单的底层类型没有简洁很多但是如果是复杂的类型将会简洁很多特别是我们即将讨论的结构体类型。
命名類型還可以爲該類型的值定義新的行爲。這些行爲表示爲一組關聯到該類型的函數集合,我們稱爲類型的方法集。我們將在第六章中討論方法的細節,這里值説寫簡單用法。
命名类型还可以为该类型的值定义新的行为。这些行为表示为一组关联到该类型的函数集合,我们称为类型的方法集。我们将在第六章中讨论方法的细节,这里值说写简单用法。
下面的聲明語句Celsius類型的參數c出現在了函數名的前面表示聲明的是Celsius類型的一個叫名叫String的方法該方法返迴該類型對象c帶着°C溫度單位的字符串:
下面的声明语句Celsius类型的参数c出现在了函数名的前面表示声明的是Celsius类型的一个叫名叫String的方法该方法返回该类型对象c带着°C温度单位的字符串:
```Go
func (c Celsius) String() string { return fmt.Sprintf("%g°C", c) }
```
許多類型都會定義一個String方法因爲當使用fmt包的打印方法時將會優先使用該類型對應的String方法返迴的結果打印我們將在7.1節講述。
许多类型都会定义一个String方法因为当使用fmt包的打印方法时将会优先使用该类型对应的String方法返回的结果打印我们将在7.1节讲述。
```Go
c := FToC(212.0)

View File

@ -1,10 +1,10 @@
### 2.6.1. 入包
### 2.6.1. 入包
在Go語言程序中,每個包都是有一個全局唯一的導入路徑。導入語句中類似"gopl.io/ch2/tempconv"的字符串對應包的導入路徑。Go語言的規范併沒有定義這些字符串的具體含義或包來自哪里它們是由構建工具來解釋的。當使用Go語言自帶的go工具箱時第十章一個導入路徑代表一個目録中的一個或多個Go源文件。
在Go语言程序中,每个包都是有一个全局唯一的导入路径。导入语句中类似"gopl.io/ch2/tempconv"的字符串对应包的导入路径。Go语言的规范并没有定义这些字符串的具体含义或包来自哪里它们是由构建工具来解释的。当使用Go语言自带的go工具箱时第十章一个导入路径代表一个目录中的一个或多个Go源文件。
除了包的導入路徑,每個包還有一個包名,包名一般是短小的名字(併不要求包名是唯一的),包名在包的聲明處指定。按照慣例,一個包的名字和包的導入路徑的最後一個字段相同例如gopl.io/ch2/tempconv包的名字一般是tempconv。
除了包的导入路径,每个包还有一个包名,包名一般是短小的名字(并不要求包名是唯一的),包名在包的声明处指定。按照惯例,一个包的名字和包的导入路径的最后一个字段相同例如gopl.io/ch2/tempconv包的名字一般是tempconv。
要使用gopl.io/ch2/tempconv包需要先入:
要使用gopl.io/ch2/tempconv包需要先入:
<u><i>gopl.io/ch2/cf</i></u>
```Go
@ -34,9 +34,9 @@ func main() {
}
```
導入語句將導入的包綁定到一個短小的名字然後通過該短小的名字就可以引用包中導出的全部內容。上面的導入聲明將允許我們以tempconv.CToF的形式來訪問gopl.io/ch2/tempconv包中的內容。在默認情況下導入的包綁定到tempconv名字譯註這包聲明語句指定的名字但是我們也可以綁定到另一個名稱以避免名字衝§10.4)。
导入语句将导入的包绑定到一个短小的名字然后通过该短小的名字就可以引用包中导出的全部内容。上面的导入声明将允许我们以tempconv.CToF的形式来访问gopl.io/ch2/tempconv包中的内容。在默认情况下导入的包绑定到tempconv名字译注这包声明语句指定的名字但是我们也可以绑定到另一个名称以避免名字冲§10.4)。
cf程序將命令行輸入的一個溫度在Celsius和Fahrenheit溫度單位之間轉換
cf程序将命令行输入的一个温度在Celsius和Fahrenheit温度单位之间转换
```
$ go build gopl.io/ch2/cf
@ -48,8 +48,8 @@ $ ./cf -40
-40°F = -40°C, -40°C = -40°F
```
如果導入了一個包但是又沒有使用該包將被當作一個編譯錯誤處理。這種強製規則可以有效減少不必要的依賴雖然在調試期間可能會讓人討厭因爲刪除一個類似log.Print("got here!")的打印語句可能導致需要同時刪除log包導入聲明否則編譯器將會發出一個錯誤。在這種情況下我們需要將不必要的導入刪除或註釋掉。
如果导入了一个包但是又没有使用该包将被当作一个编译错误处理。这种强制规则可以有效减少不必要的依赖虽然在调试期间可能会让人讨厌因为删除一个类似log.Print("got here!")的打印语句可能导致需要同时删除log包导入声明否则编译器将会发出一个错误。在这种情况下我们需要将不必要的导入删除或注释掉。
過有更好的解決方案我們可以使用golang.org/x/tools/cmd/goimports導入工具它可以根據需要自動添加或刪除導入的包許多編輯器都可以集成goimports工具然後在保存文件的時候自動運行。類似的還有gofmt工具可以用來格式化Go源文件。
过有更好的解决方案我们可以使用golang.org/x/tools/cmd/goimports导入工具它可以根据需要自动添加或删除导入的包许多编辑器都可以集成goimports工具然后在保存文件的时候自动运行。类似的还有gofmt工具可以用来格式化Go源文件。
**練習 2.2** 寫一個通用的單位轉換程序用類似cf程序的方式從命令行讀取參數如果缺省的話則是從標準輸入讀取參數然後做類似Celsius和Fahrenheit的單位轉換長度單位可以對應英尺和米重量單位可以對應磅和公斤等。
**练习 2.2** 写一个通用的单位转换程序用类似cf程序的方式从命令行读取参数如果缺省的话则是从标准输入读取参数然后做类似Celsius和Fahrenheit的单位转换长度单位可以对应英尺和米重量单位可以对应磅和公斤等。

View File

@ -1,28 +1,28 @@
### 2.6.2. 包的初始化
包的初始化首先是解決包級變量的依賴順序,然後安照包級變量聲明出現的順序依次初始化:
包的初始化首先是解决包级变量的依赖顺序,然后安照包级变量声明出现的顺序依次初始化:
```Go
var a = b + c // a 第三個初始化, 爲 3
var b = f() // b 第二個初始化, 爲 2, 通過調用 f (依賴c)
var c = 1 // c 第一個初始化, 爲 1
var a = b + c // a 第三个初始化, 为 3
var b = f() // b 第二个初始化, 为 2, 通过调用 f (依赖c)
var c = 1 // c 第一个初始化, 为 1
func f() int { return c + 1 }
```
如果包中含有多個.go源文件它們將按照發給編譯器的順序進行初始化Go語言的構建工具首先會將.go文件根據文件名排序然後依次調用編譯器編譯
如果包中含有多个.go源文件它们将按照发给编译器的顺序进行初始化Go语言的构建工具首先会将.go文件根据文件名排序然后依次调用编译器编译
對於在包級别聲明的變量如果有初始化表達式則用表達式初始化還有一些沒有初始化表達式的例如某些表格數據初始化併不是一個簡單的賦值過程。在這種情況下我們可以用一個特殊的init初始化函數來簡化初始化工作。每個文件都可以包含多個init初始化函數
对于在包级别声明的变量如果有初始化表达式则用表达式初始化还有一些没有初始化表达式的例如某些表格数据初始化并不是一个简单的赋值过程。在这种情况下我们可以用一个特殊的init初始化函数来简化初始化工作。每个文件都可以包含多个init初始化函数
```Go
func init() { /* ... */ }
```
這樣的init初始化函數除了不能被調用或引用外其他行爲和普通函數類似。在每個文件中的init初始化函數在程序開始執行時按照它們聲明的順序被自動調用。
这样的init初始化函数除了不能被调用或引用外其他行为和普通函数类似。在每个文件中的init初始化函数在程序开始执行时按照它们声明的顺序被自动调用。
個包在解決依賴的前提下以導入聲明的順序初始化每個包隻會被初始化一次。因此如果一個p包導入了q包那麽在p包初始化的時候可以認爲q包必然已經初始化過了。初始化工作是自下而上進行的main包最後被初始化。以這種方式可以確保在main函數執行之前所有依然的包都已經完成初始化工作了。
个包在解决依赖的前提下以导入声明的顺序初始化每个包只会被初始化一次。因此如果一个p包导入了q包那么在p包初始化的时候可以认为q包必然已经初始化过了。初始化工作是自下而上进行的main包最后被初始化。以这种方式可以确保在main函数执行之前所有依然的包都已经完成初始化工作了。
下面的代碼定義了一個PopCount函數用於返迴一個數字中含二進製1bit的個數。它使用init初始化函數來生成輔助表格pcpc表格用於處理每個8bit寬度的數字含二進製的1bit的bit個數這樣的話在處理64bit寬度的數字時就沒有必要循環64次隻需要8次査表就可以了。這併不是最快的統計1bit數目的算法但是它可以方便演示init函數的用法併且演示了如果預生成輔助表格這是編程中常用的技術)。
下面的代码定义了一个PopCount函数用于返回一个数字中含二进制1bit的个数。它使用init初始化函数来生成辅助表格pcpc表格用于处理每个8bit宽度的数字含二进制的1bit的bit个数这样的话在处理64bit宽度的数字时就没有必要循环64次只需要8次查表就可以了。这并不是最快的统计1bit数目的算法但是它可以方便演示init函数的用法并且演示了如果预生成辅助表格这是编程中常用的技术)。
<u><i>gopl.io/ch2/popcount</i></u>
```Go
@ -50,7 +50,7 @@ func PopCount(x uint64) int {
}
```
譯註對於pc這類需要複雜處理的初始化可以通過將初始化邏輯包裝爲一個匿名函數處理像下面這樣
译注对于pc这类需要复杂处理的初始化可以通过将初始化逻辑包装为一个匿名函数处理像下面这样
```Go
// pc[i] is the population count of i.
@ -62,16 +62,16 @@ var pc [256]byte = func() (pc [256]byte) {
}()
```
註意的是在init函數中range循環隻使用了索引省略了沒有用到的值部分。循環也可以這樣寫
注意的是在init函数中range循环只使用了索引省略了没有用到的值部分。循环也可以这样写
```Go
for i, _ := range pc {
```
們在下一節和10.5節還將看到其它使用init函數的地方。
们在下一节和10.5节还将看到其它使用init函数的地方。
**練習 2.3** 重寫PopCount函數用一個循環代替單一的表達式。比較兩個版本的性能。11.4節將展示如何繫統地比較兩個不同實現的性能。)
**练习 2.3** 重写PopCount函数用一个循环代替单一的表达式。比较两个版本的性能。11.4节将展示如何系统地比较两个不同实现的性能。)
**練習 2.4** 用移位算法重寫PopCount函數每次測試最右邊的1bit然後統計總數。比較和査表算法的性能差異
**练习 2.4** 用移位算法重写PopCount函数每次测试最右边的1bit然后统计总数。比较和查表算法的性能差异
**練習 2.5** 表達式`x&(x-1)`用於將x的最低的一個非零的bit位清零。使用這個算法重寫PopCount函數然後比較性能。
**练习 2.5** 表达式`x&(x-1)`用于将x的最低的一个非零的bit位清零。使用这个算法重写PopCount函数然后比较性能。

View File

@ -1,16 +1,16 @@
## 2.6. 包和文件
Go語言中的包和其他語言的庫或模塊的概念類似,目的都是爲了支持模塊化、封裝、單獨編譯和代碼重用。一個包的源代碼保存在一個或多個以.go爲文件後綴名的源文件中通常一個包所在目録路徑的後綴是包的導入路徑例如包gopl.io/ch1/helloworld對應的目録路徑是$GOPATH/src/gopl.io/ch1/helloworld。
Go语言中的包和其他语言的库或模块的概念类似,目的都是为了支持模块化、封装、单独编译和代码重用。一个包的源代码保存在一个或多个以.go为文件后缀名的源文件中通常一个包所在目录路径的后缀是包的导入路径例如包gopl.io/ch1/helloworld对应的目录路径是$GOPATH/src/gopl.io/ch1/helloworld。
個包都對應一個獨立的名字空間。例如在image包中的Decode函數和在unicode/utf16包中的 Decode函數是不同的。要在外部引用該函數必須顯式使用image.Decode或utf16.Decode形式訪問
个包都对应一个独立的名字空间。例如在image包中的Decode函数和在unicode/utf16包中的 Decode函数是不同的。要在外部引用该函数必须显式使用image.Decode或utf16.Decode形式访问
還可以讓我們通過控製哪些名字是外部可見的來隱藏內部實現信息。在Go語言中一個簡單的規則是如果一個名字是大寫字母開頭的那麽該名字是導出的譯註因爲漢字不區分大小寫因此漢字開頭的名字是沒有導出的)。
还可以让我们通过控制哪些名字是外部可见的来隐藏内部实现信息。在Go语言中一个简单的规则是如果一个名字是大写字母开头的那么该名字是导出的译注因为汉字不区分大小写因此汉字开头的名字是没有导出的)。
爲了演示包基本的用法先假設我們的溫度轉換軟件已經很流行我們希望到Go語言社區也能使用這個包。我們該如何做呢?
为了演示包基本的用法先假设我们的温度转换软件已经很流行我们希望到Go语言社区也能使用这个包。我们该如何做呢?
讓我們創建一個名爲gopl.io/ch2/tempconv的包這是前面例子的一個改進版本。我們約定我們的例子都是以章節順序來編號的這樣的路徑更容易閲讀包代碼存儲在兩個源文件中用來演示如何在一個源文件聲明然後在其他的源文件訪問雖然在現實中這樣小的包一般隻需要一個文件。
让我们创建一个名为gopl.io/ch2/tempconv的包这是前面例子的一个改进版本。我们约定我们的例子都是以章节顺序来编号的这样的路径更容易阅读包代码存储在两个源文件中用来演示如何在一个源文件声明然后在其他的源文件访问虽然在现实中这样小的包一般只需要一个文件。
們把變量的聲明、對應的常量,還有方法都放到tempconv.go源文件中
们把变量的声明、对应的常量,还有方法都放到tempconv.go源文件中
<u></i>gopl.io/ch2/tempconv</i></u>
```Go
@ -32,7 +32,7 @@ func (c Celsius) String() string { return fmt.Sprintf("%g°C", c) }
func (f Fahrenheit) String() string { return fmt.Sprintf("%g°F", f) }
```
轉換函數則放在另一個conv.go源文件中
转换函数则放在另一个conv.go源文件中
```Go
package tempconv
@ -44,23 +44,23 @@ func CToF(c Celsius) Fahrenheit { return Fahrenheit(c*9/5 + 32) }
func FToC(f Fahrenheit) Celsius { return Celsius((f - 32) * 5 / 9) }
```
個源文件都是以包的聲明語句開始用來指名包的名字。當包被導入的時候包內的成員將通過類似tempconv.CToF的形式訪問。而包級别的名字例如在一個文件聲明的類型和常量在同一個包的其他源文件也是可以直接訪問的就好像所有代碼都在一個文件一樣。要註意的是tempconv.go源文件導入了fmt包但是conv.go源文件併沒有因爲這個源文件中的代碼併沒有用到fmt包。
个源文件都是以包的声明语句开始用来指名包的名字。当包被导入的时候包内的成员将通过类似tempconv.CToF的形式访问。而包级别的名字例如在一个文件声明的类型和常量在同一个包的其他源文件也是可以直接访问的就好像所有代码都在一个文件一样。要注意的是tempconv.go源文件导入了fmt包但是conv.go源文件并没有因为这个源文件中的代码并没有用到fmt包。
爲包級别的常量名都是以大寫字母開頭它們可以像tempconv.AbsoluteZeroC這樣被外部代碼訪問
为包级别的常量名都是以大写字母开头它们可以像tempconv.AbsoluteZeroC这样被外部代码访问
```Go
fmt.Printf("Brrrr! %v\n", tempconv.AbsoluteZeroC) // "Brrrr! -273.15°C"
```
將攝氏溫度轉換爲華氏溫度需要先用import語句導入gopl.io/ch2/tempconv包然後就可以使用下面的代碼進行轉換了:
将摄氏温度转换为华氏温度需要先用import语句导入gopl.io/ch2/tempconv包然后就可以使用下面的代码进行转换了:
```Go
fmt.Println(tempconv.CToF(tempconv.BoilingC)) // "212°F"
```
在每個源文件的包聲明前僅跟着的註釋是包註釋§10.7.4)。通常,包註釋的第一句應該先是包的功能概要説明。一個包通常隻有一個源文件有包註釋(譯註:如果有多個包註釋,目前的文檔工具會根據源文件名的先後順序將它們鏈接爲一個包註釋)。如果包註釋很大,通常會放到一個獨立的doc.go文件中。
在每个源文件的包声明前仅跟着的注释是包注释§10.7.4)。通常,包注释的第一句应该先是包的功能概要说明。一个包通常只有一个源文件有包注释(译注:如果有多个包注释,目前的文档工具会根据源文件名的先后顺序将它们链接为一个包注释)。如果包注释很大,通常会放到一个独立的doc.go文件中。
**練習 2.1** 向tempconv包添加類型、常量和函數用來處理Kelvin絶對溫度的轉換Kelvin 絶對零度是273.15°CKelvin絶對溫度1K和攝氏度1°C的單位間隔是一樣的。
**练习 2.1** 向tempconv包添加类型、常量和函数用来处理Kelvin绝对温度的转换Kelvin 绝对零度是273.15°CKelvin绝对温度1K和摄氏度1°C的单位间隔是一样的。
{% include "./ch2-06-1.md" %}

View File

@ -1,18 +1,18 @@
## 2.7. 作用域
個聲明語句將程序中的實體和一個名字關聯,比如一個函數或一個變量。聲明語句的作用域是指源代碼中可以有效使用這個名字的范圍
个声明语句将程序中的实体和一个名字关联,比如一个函数或一个变量。声明语句的作用域是指源代码中可以有效使用这个名字的范围
不要將作用域和生命週期混爲一談。聲明語句的作用域對應的是一個源代碼的文本區域;它是一個編譯時的屬性。一個變量的生命週期是指程序運行時變量存在的有效時間段,在此時間區域內它可以被程序的其他部分引用;是一個運行時的概念。
不要将作用域和生命周期混为一谈。声明语句的作用域对应的是一个源代码的文本区域;它是一个编译时的属性。一个变量的生命周期是指程序运行时变量存在的有效时间段,在此时间区域内它可以被程序的其他部分引用;是一个运行时的概念。
語法塊是由花括弧所包含的一繫列語句就像函數體或循環體花括弧對應的語法塊那樣。語法塊內部聲明的名字是無法被外部語法塊訪問的。語法決定了內部聲明的名字的作用域范圍。我們可以這樣理解語法塊可以包含其他類似組批量聲明等沒有用花括弧包含的代碼我們稱之爲語法塊。有一個語法塊爲整個源代碼稱爲全局語法塊然後是每個包的包語法決每個for、if和switch語句的語法決每個switch或select的分支也有獨立的語法決當然也包括顯式書寫的語法塊花括弧包含的語句)。
语法块是由花括弧所包含的一系列语句就像函数体或循环体花括弧对应的语法块那样。语法块内部声明的名字是无法被外部语法块访问的。语法决定了内部声明的名字的作用域范围。我们可以这样理解语法块可以包含其他类似组批量声明等没有用花括弧包含的代码我们称之为语法块。有一个语法块为整个源代码称为全局语法块然后是每个包的包语法决每个for、if和switch语句的语法决每个switch或select的分支也有独立的语法决当然也包括显式书写的语法块花括弧包含的语句)。
聲明語句對應的詞法域決定了作用域范圍的大小。對於內置的類型、函數和常量比如int、len和true等是在全局作用域的因此可以在整個程序中直接使用。任何在在函數外部也就是包級語法域聲明的名字可以在同一個包的任何源文件中訪問的。對於導入的包例如tempconv導入的fmt包則是對應源文件級的作用域因此隻能在當前的文件中訪問導入的fmt包當前包的其它源文件無法訪問在當前源文件導入的包。還有許多聲明語句比如tempconv.CToF函數中的變量c則是局部作用域的它隻能在函數內部甚至隻能是局部的某些部分訪問
声明语句对应的词法域决定了作用域范围的大小。对于内置的类型、函数和常量比如int、len和true等是在全局作用域的因此可以在整个程序中直接使用。任何在在函数外部也就是包级语法域声明的名字可以在同一个包的任何源文件中访问的。对于导入的包例如tempconv导入的fmt包则是对应源文件级的作用域因此只能在当前的文件中访问导入的fmt包当前包的其它源文件无法访问在当前源文件导入的包。还有许多声明语句比如tempconv.CToF函数中的变量c则是局部作用域的它只能在函数内部甚至只能是局部的某些部分访问
製流標號就是break、continue或goto語句後面跟着的那種標號則是函數級的作用域。
制流标号就是break、continue或goto语句后面跟着的那种标号则是函数级的作用域。
個程序可能包含多個同名的聲明隻要它們在不同的詞法域就沒有關繫。例如你可以聲明一個局部變量和包級的變量同名。或者是像2.3.3節的例子那樣你可以將一個函數參數的名字聲明爲new雖然內置的new是全局作用域的。但是物極必反如果濫用不同詞法域可重名的特性的話可能導致程序很難閲讀
个程序可能包含多个同名的声明只要它们在不同的词法域就没有关系。例如你可以声明一个局部变量和包级的变量同名。或者是像2.3.3节的例子那样你可以将一个函数参数的名字声明为new虽然内置的new是全局作用域的。但是物极必反如果滥用不同词法域可重名的特性的话可能导致程序很难阅读
當編譯器遇到一個名字引用時,如果它看起來像一個聲明,它首先從最內層的詞法域向全局的作用域査找。如果査找失敗,則報告“未聲明的名字”這樣的錯誤。如果該名字在內部和外部的塊分别聲明過,則內部塊的聲明首先被找到。在這種情況下,內部聲明屏蔽了外部同名的聲明,讓外部的聲明的名字無法被訪問
当编译器遇到一个名字引用时,如果它看起来像一个声明,它首先从最内层的词法域向全局的作用域查找。如果查找失败,则报告“未声明的名字”这样的错误。如果该名字在内部和外部的块分别声明过,则内部块的声明首先被找到。在这种情况下,内部声明屏蔽了外部同名的声明,让外部的声明的名字无法被访问
```Go
func f() {}
@ -27,7 +27,7 @@ func main() {
}
```
在函數中詞法域可以深度嵌套因此內部的一個聲明可能屏蔽外部的聲明。還有許多語法塊是if或for等控製流語句構造的。下面的代碼有三個不同的變量x因爲它們是定義在不同的詞法域這個例子隻是爲了演示作用域規則但不是好的編程風格)。
在函数中词法域可以深度嵌套因此内部的一个声明可能屏蔽外部的声明。还有许多语法块是if或for等控制流语句构造的。下面的代码有三个不同的变量x因为它们是定义在不同的词法域这个例子只是为了演示作用域规则但不是好的编程风格)。
```Go
func main() {
@ -42,11 +42,11 @@ func main() {
}
```
在`x[i]`和`x + 'A' - 'a'`聲明語句的初始化的表達式中都引用了外部作用域聲明的x變量稍後我們會解釋這個。註意後面的表達式與unicode.ToUpper併不等價。)
在`x[i]`和`x + 'A' - 'a'`声明语句的初始化的表达式中都引用了外部作用域声明的x变量稍后我们会解释这个。注意后面的表达式与unicode.ToUpper并不等价。)
正如上面例子所示,併不是所有的詞法域都顯式地對應到由花括弧包含的語句還有一些隱含的規則。上面的for語句創建了兩個詞法域花括弧包含的是顯式的部分是for的循環體部分詞法域另外一個隱式的部分則是循環的初始化部分比如用於迭代變量i的初始化。隱式的詞法域部分的作用域還包含條件測試部分和循環後的迭代部分`i++`),當然也包含循環體詞法域。
正如上面例子所示,并不是所有的词法域都显式地对应到由花括弧包含的语句还有一些隐含的规则。上面的for语句创建了两个词法域花括弧包含的是显式的部分是for的循环体部分词法域另外一个隐式的部分则是循环的初始化部分比如用于迭代变量i的初始化。隐式的词法域部分的作用域还包含条件测试部分和循环后的迭代部分`i++`),当然也包含循环体词法域。
下面的例子同樣有三個不同的x變量每個聲明在不同的詞法域一個在函數體詞法域一個在for隱式的初始化詞法域一個在for循環體詞法域隻有兩個塊是顯式創建的:
下面的例子同样有三个不同的x变量每个声明在不同的词法域一个在函数体词法域一个在for隐式的初始化词法域一个在for循环体词法域只有两个块是显式创建的:
```Go
func main() {
@ -58,7 +58,7 @@ func main() {
}
```
和for循環類似if和switch語句也會在條件部分創建隱式詞法域還有它們對應的執行體詞法域。下面的if-else測試鏈演示了x和y的有效作用域范圍
和for循环类似if和switch语句也会在条件部分创建隐式词法域还有它们对应的执行体词法域。下面的if-else测试链演示了x和y的有效作用域范围
```Go
if x := f(); x == 0 {
@ -71,11 +71,11 @@ if x := f(); x == 0 {
fmt.Println(x, y) // compile error: x and y are not visible here
```
第二個if語句嵌套在第一個內部因此第一個if語句條件初始化詞法域聲明的變量在第二個if中也可以訪問。switch語句的每個分支也有類似的詞法域規則條件部分爲一個隱式詞法域然後每個是每個分支的詞法域。
第二个if语句嵌套在第一个内部因此第一个if语句条件初始化词法域声明的变量在第二个if中也可以访问。switch语句的每个分支也有类似的词法域规则条件部分为一个隐式词法域然后每个是每个分支的词法域。
在包級别,聲明的順序併不會影響作用域范圍,因此一個先聲明的可以引用它自身或者是引用後面的一個聲明,這可以讓我們定義一些相互嵌套或遞歸的類型或函數。但是如果一個變量或常量遞歸引用了自身,則會産生編譯錯誤
在包级别,声明的顺序并不会影响作用域范围,因此一个先声明的可以引用它自身或者是引用后面的一个声明,这可以让我们定义一些相互嵌套或递归的类型或函数。但是如果一个变量或常量递归引用了自身,则会产生编译错误
這個程序中:
这个程序中:
```Go
if f, err := os.Open(fname); err != nil { // compile error: unused: f
@ -85,9 +85,9 @@ f.ReadByte() // compile error: undefined f
f.Close() // compile error: undefined f
```
變量f的作用域隻有在if語句內因此後面的語句將無法引入它這將導致編譯錯誤。你可能會收到一個局部變量f沒有聲明的錯誤提示具體錯誤信息依賴編譯器的實現
变量f的作用域只有在if语句内因此后面的语句将无法引入它这将导致编译错误。你可能会收到一个局部变量f没有声明的错误提示具体错误信息依赖编译器的实现
通常需要在if之前聲明變量,這樣可以確保後面的語句依然可以訪問變量:
通常需要在if之前声明变量,这样可以确保后面的语句依然可以访问变量:
```Go
f, err := os.Open(fname)
@ -98,7 +98,7 @@ f.ReadByte()
f.Close()
```
你可能會考慮通過將ReadByte和Close移動到if的else塊來解決這個問題
你可能会考虑通过将ReadByte和Close移动到if的else块来解决这个问题
```Go
if f, err := os.Open(fname); err != nil {
@ -110,9 +110,9 @@ if f, err := os.Open(fname); err != nil {
}
```
這不是Go語言推薦的做法Go語言的習慣是在if中處理錯誤然後直接返迴這樣可以確保正常執行的語句不需要代碼縮進
这不是Go语言推荐的做法Go语言的习惯是在if中处理错误然后直接返回这样可以确保正常执行的语句不需要代码缩进
要特别註意短變量聲明語句的作用域范圍考慮下面的程序它的目的是獲取當前的工作目録然後保存到一個包級的變量中。這可以本來通過直接調用os.Getwd完成但是將這個從主邏輯中分離出來可能會更好特别是在需要處理錯誤的時候。函數log.Fatalf用於打印日誌信息然後調用os.Exit(1)終止程序。
要特别注意短变量声明语句的作用域范围考虑下面的程序它的目的是获取当前的工作目录然后保存到一个包级的变量中。这可以本来通过直接调用os.Getwd完成但是将这个从主逻辑中分离出来可能会更好特别是在需要处理错误的时候。函数log.Fatalf用于打印日志信息然后调用os.Exit(1)终止程序。
```Go
var cwd string
@ -125,9 +125,9 @@ func init() {
}
```
雖然cwd在外部已經聲明過但是`:=`語句還是將cwd和err重新聲明爲新的局部變量。因爲內部聲明的cwd將屏蔽外部的聲明因此上面的代碼併不會正確更新包級聲明的cwd變量。
虽然cwd在外部已经声明过但是`:=`语句还是将cwd和err重新声明为新的局部变量。因为内部声明的cwd将屏蔽外部的声明因此上面的代码并不会正确更新包级声明的cwd变量。
於當前的編譯器會檢測到局部聲明的cwd併沒有本使用然後報告這可能是一個錯誤但是這種檢測併不可靠。因爲一些小的代碼變更例如增加一個局部cwd的打印語句就可能導致這種檢測失效。
于当前的编译器会检测到局部声明的cwd并没有本使用然后报告这可能是一个错误但是这种检测并不可靠。因为一些小的代码变更例如增加一个局部cwd的打印语句就可能导致这种检测失效。
```Go
var cwd string
@ -141,9 +141,9 @@ func init() {
}
```
全局的cwd變量依然是沒有被正確初始化的而且看似正常的日誌輸出更是讓這個BUG更加隱晦。
全局的cwd变量依然是没有被正确初始化的而且看似正常的日志输出更是让这个BUG更加隐晦。
許多方式可以避免出現類似潛在的問題。最直接的方法是通過單獨聲明err變量來避免使用`:=`的簡短聲明方式:
许多方式可以避免出现类似潜在的问题。最直接的方法是通过单独声明err变量来避免使用`:=`的简短声明方式:
```Go
var cwd string
@ -157,5 +157,5 @@ func init() {
}
```
們已經看到包、文件、聲明和語句如何來表達一個程序結構。在下面的兩個章節,我們將探討數據的結構
们已经看到包、文件、声明和语句如何来表达一个程序结构。在下面的两个章节,我们将探讨数据的结构

View File

@ -1,5 +1,5 @@
# 第2章 程序結構
# 第2章 程序结构
Go語言和其他編程語言一樣一個大的程序是由很多小的基礎構件組成的。變量保存值簡單的加法和減法運算被組合成較複雜的表達式。基礎類型被聚合爲數組或結構體等更複雜的數據結構。然後使用if和for之類的控製語句來組織和控製表達式的執行流程。然後多個語句被組織到一個個函數中以便代碼的隔離和複用。函數以源文件和包的方式被組織
Go语言和其他编程语言一样一个大的程序是由很多小的基础构件组成的。变量保存值简单的加法和减法运算被组合成较复杂的表达式。基础类型被聚合为数组或结构体等更复杂的数据结构。然后使用if和for之类的控制语句来组织和控制表达式的执行流程。然后多个语句被组织到一个个函数中以便代码的隔离和复用。函数以源文件和包的方式被组织
們已經在前面章節的例子中看到了很多例子。在本章中我們將深入討論Go程序基礎結構方面的一些細節。每個示例程序都是刻意寫的簡單這樣我們可以減少複雜的算法或數據結構等不相關的問題帶來的榦擾從而可以專註於Go語言本身的學習
们已经在前面章节的例子中看到了很多例子。在本章中我们将深入讨论Go程序基础结构方面的一些细节。每个示例程序都是刻意写的简单这样我们可以减少复杂的算法或数据结构等不相关的问题带来的干扰从而可以专注于Go语言本身的学习

View File

@ -1,20 +1,20 @@
## 3.1. 整型
Go語言的數值類型包括幾種不同大小的整形數、浮點數和複數。每種數值類型都決定了對應的大小范圍和是否支持正負符號。讓我們先從整形數類型開始介紹
Go语言的数值类型包括几种不同大小的整形数、浮点数和复数。每种数值类型都决定了对应的大小范围和是否支持正负符号。让我们先从整形数类型开始介绍
Go語言同時提供了有符號和無符號類型的整數運算。這里有int8、int16、int32和int64四種截然不同大小的有符號整形數類型分别對應8、16、32、64bit大小的有符號整形數與此對應的是uint8、uint16、uint32和uint64四種無符號整形數類型。
Go语言同时提供了有符号和无符号类型的整数运算。这里有int8、int16、int32和int64四种截然不同大小的有符号整形数类型分别对应8、16、32、64bit大小的有符号整形数与此对应的是uint8、uint16、uint32和uint64四种无符号整形数类型。
這里還有兩種一般對應特定CPU平台機器字大小的有符號和無符號整數int和uint其中int是應用最廣泛的數值類型。這兩種類型都有同樣的大小32或64bit但是我們不能對此做任何的假設因爲不同的編譯器卽使在相同的硬件平台上可能産生不同的大小。
这里还有两种一般对应特定CPU平台机器字大小的有符号和无符号整数int和uint其中int是应用最广泛的数值类型。这两种类型都有同样的大小32或64bit但是我们不能对此做任何的假设因为不同的编译器即使在相同的硬件平台上可能产生不同的大小。
Unicode字符rune類型是和int32等價的類型通常用於表示一個Unicode碼點。這兩個名稱可以互換使用。同樣byte也是uint8類型的等價類型byte類型一般用於強調數值是一個原始的數據而不是一個小的整數
Unicode字符rune类型是和int32等价的类型通常用于表示一个Unicode码点。这两个名称可以互换使用。同样byte也是uint8类型的等价类型byte类型一般用于强调数值是一个原始的数据而不是一个小的整数
還有一種無符號的整數類型uintptr沒有指定具體的bit大小但是足以容納指針。uintptr類型隻有在底層編程是才需要特别是Go語言和C語言函數庫或操作繫統接口相交互的地方。我們將在第十三章的unsafe包相關部分看到類似的例子。
还有一种无符号的整数类型uintptr没有指定具体的bit大小但是足以容纳指针。uintptr类型只有在底层编程是才需要特别是Go语言和C语言函数库或操作系统接口相交互的地方。我们将在第十三章的unsafe包相关部分看到类似的例子。
不管它們的具體大小int、uint和uintptr是不同類型的兄弟類型。其中int和int32也是不同的類型卽使int的大小也是32bit在需要將int當作int32類型的地方需要一個顯式的類型轉換操作,反之亦然。
不管它们的具体大小int、uint和uintptr是不同类型的兄弟类型。其中int和int32也是不同的类型即使int的大小也是32bit在需要将int当作int32类型的地方需要一个显式的类型转换操作,反之亦然。
其中有符號整數采用2的補碼形式表示也就是最高bit位用作表示符號位一個n-bit的有符號數的值域是從$$-2^{n-1}$$到$$2^{n-1}-1$$。無符號整數的所有bit位都用於表示非負數值域是0到$$2^n-1$$。例如int8類型整數的值域是從-128到127而uint8類型整數的值域是從0到255。
其中有符号整数采用2的补码形式表示也就是最高bit位用作表示符号位一个n-bit的有符号数的值域是从$$-2^{n-1}$$到$$2^{n-1}-1$$。无符号整数的所有bit位都用于表示非负数值域是0到$$2^n-1$$。例如int8类型整数的值域是从-128到127而uint8类型整数的值域是从0到255。
下面是Go語言中關於算術運算、邏輯運算和比較運算的二元運算符,它們按照先級遞減的順序的排列:
下面是Go语言中关于算术运算、逻辑运算和比较运算的二元运算符,它们按照先级递减的顺序的排列:
```
* / % << >> & &^
@ -24,13 +24,13 @@ Unicode字符rune類型是和int32等價的類型通常用於表示一個Unic
||
```
二元運算符有五種優先級。在同一個優先級,使用左優先結合規則,但是使用括號可以明確優先順序,使用括號也可以用於提陞優先級,例如`mask & (1 << 28)`
二元运算符有五种优先级。在同一个优先级,使用左优先结合规则,但是使用括号可以明确优先顺序,使用括号也可以用于提升优先级,例如`mask & (1 << 28)`
對於上表中前兩行的運算符,例如+運算符還有一個與賦值相結合的對應運算符+=,可以用於簡化賦值語句。
对于上表中前两行的运算符,例如+运算符还有一个与赋值相结合的对应运算符+=,可以用于简化赋值语句。
術運算符+、-、`*`和`/`可以適用與於整數、浮點數和複數,但是取模運算符%僅用於整數間的運算。對於不同編程語言,%取模運算的行爲可能併不相同。在Go語言中%取模運算符的符號和被取模數的符號總是一致的,因此`-5%3`和`-5%-3`結果都是-2。除法運算符`/`的行爲則依賴於操作數是否爲全爲整數,比如`5.0/4.0`的結果是1.25但是5/4的結果是1因爲整數除法會向着0方向截斷餘數
术运算符+、-、`*`和`/`可以适用与于整数、浮点数和复数,但是取模运算符%仅用于整数间的运算。对于不同编程语言,%取模运算的行为可能并不相同。在Go语言中%取模运算符的符号和被取模数的符号总是一致的,因此`-5%3`和`-5%-3`结果都是-2。除法运算符`/`的行为则依赖于操作数是否为全为整数,比如`5.0/4.0`的结果是1.25但是5/4的结果是1因为整数除法会向着0方向截断余数
如果一個算術運算的結果不管是有符號或者是無符號的如果需要更多的bit位才能正確表示的話就説明計算結果是溢出了。超出的高位的bit位部分將被丟棄。如果原始的數值是有符號類型而且最左邊的bit爲是1的話那麽最終結果可能是負例如int8的例子
如果一个算术运算的结果不管是有符号或者是无符号的如果需要更多的bit位才能正确表示的话就说明计算结果是溢出了。超出的高位的bit位部分将被丢弃。如果原始的数值是有符号类型而且最左边的bit为是1的话那么最终结果可能是负例如int8的例子
```Go
var u uint8 = 255
@ -40,7 +40,7 @@ var i int8 = 127
fmt.Println(i, i+1, i*i) // "127 -128 1"
```
兩個相同的整數類型可以使用下面的二元比較運算符進行比較;比較表達式的結果是布爾類型。
两个相同的整数类型可以使用下面的二元比较运算符进行比较;比较表达式的结果是布尔类型。
```
== equal to
@ -51,31 +51,31 @@ fmt.Println(i, i+1, i*i) // "127 -128 1"
>= greater than or equal to
```
實上,布爾型、數字類型和字符串等基本類型都是可比較的,也就是説兩個相同類型的值可以用==和!=進行比較。此外,整數、浮點數和字符串可以根據比較結果排序。許多其它類型的值可能是不可比較的,因此也就可能是不可排序的。對於我們遇到的每種類型,我們需要保證規則的一致性。
实上,布尔型、数字类型和字符串等基本类型都是可比较的,也就是说两个相同类型的值可以用==和!=进行比较。此外,整数、浮点数和字符串可以根据比较结果排序。许多其它类型的值可能是不可比较的,因此也就可能是不可排序的。对于我们遇到的每种类型,我们需要保证规则的一致性。
這里是一元的加法和減法運算符:
这里是一元的加法和减法运算符:
```
+ 一元加法 (效果)
- 負數
+ 一元加法 (效果)
- 负数
```
對於整數,+x是0+x的簡寫-x則是0-x的簡寫對於浮點數和複數+x就是x-x則是x 的負數
对于整数,+x是0+x的简写-x则是0-x的简写对于浮点数和复数+x就是x-x则是x 的负数
Go語言還提供了以下的bit位操作運算符前面4個操作運算符併不區分是有符號還是無符號數
Go语言还提供了以下的bit位操作运算符前面4个操作运算符并不区分是有符号还是无符号数
```
&算 AND
| 位算 OR
^ 位算 XOR
&算 AND
| 位算 OR
^ 位算 XOR
&^ 位清空 (AND NOT)
<< 左移
>> 右移
```
位操作運算符`^`作爲二元運算符時是按位異或XOR當用作一元運算符時表示按位取反也就是説它返迴一個每個bit位都取反的數。位操作運算符`&^`用於按位置零AND NOT表達式`z = x &^ y`結果z的bit位爲0如果對應y中bit位爲1的話否則對應的bit位等於x相應的bit位的值。
位操作运算符`^`作为二元运算符时是按位异或XOR当用作一元运算符时表示按位取反也就是说它返回一个每个bit位都取反的数。位操作运算符`&^`用于按位置零AND NOT表达式`z = x &^ y`结果z的bit位为0如果对应y中bit位为1的话否则对应的bit位等于x相应的bit位的值。
下面的代碼演示了如何使用位操作解釋uint8類型值的8個獨立的bit位。它使用了Printf函數的%b參數打印二進製格式的數字其中%08b中08表示打印至少8個字符寬度不足的前綴部分用0填充。
下面的代码演示了如何使用位操作解释uint8类型值的8个独立的bit位。它使用了Printf函数的%b参数打印二进制格式的数字其中%08b中08表示打印至少8个字符宽度不足的前缀部分用0填充。
```Go
var x uint8 = 1<<1 | 1<<5
@ -99,13 +99,13 @@ fmt.Printf("%08b\n", x<<1) // "01000100", the set {2, 6}
fmt.Printf("%08b\n", x>>1) // "00010001", the set {0, 4}
```
6.5節給出了一個可以遠大於一個字節的整數集的實現。)
6.5节给出了一个可以远大于一个字节的整数集的实现。)
在`x<<n``x>>n`移位運算中決定了移位操作bit數部分必須是無符號數被操作的x數可以是有符號或無符號數。算術上一個`x<<n`$$2^n$$`x>>n`右移運算等價於除以$$2^n$$。
在`x<<n``x>>n`移位运算中决定了移位操作bit数部分必须是无符号数被操作的x数可以是有符号或无符号数。算术上一个`x<<n`$$2^n$$`x>>n`右移运算等价于除以$$2^n$$。
左移運算用零填充右邊空缺的bit位無符號數的右移運算也是用0填充左邊空缺的bit位但是有符號數的右移運算會用符號位的值填充左邊空缺的bit位。因爲這個原因最好用無符號運算這樣你可以將整數完全當作一個bit位模式處理。
左移运算用零填充右边空缺的bit位无符号数的右移运算也是用0填充左边空缺的bit位但是有符号数的右移运算会用符号位的值填充左边空缺的bit位。因为这个原因最好用无符号运算这样你可以将整数完全当作一个bit位模式处理。
盡管Go語言提供了無符號數和運算卽使數值本身不可能出現負數我們還是傾向於使用有符號的int類型就像數組的長度那樣雖然使用uint無符號類型似乎是一個更合理的選擇。事實上內置的len函數返迴一個有符號的int我們可以像下面例子那樣處理逆序循環
尽管Go语言提供了无符号数和运算即使数值本身不可能出现负数我们还是倾向于使用有符号的int类型就像数组的长度那样虽然使用uint无符号类型似乎是一个更合理的选择。事实上内置的len函数返回一个有符号的int我们可以像下面例子那样处理逆序循环
```Go
medals := []string{"gold", "silver", "bronze"}
@ -114,13 +114,13 @@ for i := len(medals) - 1; i >= 0; i-- {
}
```
另一個選擇對於上面的例子來説將是災難性的。如果len函數返迴一個無符號數那麽i也將是無符號的uint類型然後條件`i >= 0`則永遠爲眞。在三次迭代之後,也就是`i == 0`時i--語句將不會産生-1而是變成一個uint類型的最大值可能是$$2^64-1$$然後medals[i]表達式將發生運行時panic異常§5.9也就是試圖訪問一個slice范圍以外的元素。
另一个选择对于上面的例子来说将是灾难性的。如果len函数返回一个无符号数那么i也将是无符号的uint类型然后条件`i >= 0`则永远为真。在三次迭代之后,也就是`i == 0`时i--语句将不会产生-1而是变成一个uint类型的最大值可能是$$2^64-1$$然后medals[i]表达式将发生运行时panic异常§5.9也就是试图访问一个slice范围以外的元素。
於這個原因無符號數往往隻有在位運算或其它特殊的運算場景才會使用就像bit集合、分析二進製文件格式或者是哈希和加密操作等。它們通常併不用於僅僅是表達非負數量的場合。
于这个原因无符号数往往只有在位运算或其它特殊的运算场景才会使用就像bit集合、分析二进制文件格式或者是哈希和加密操作等。它们通常并不用于仅仅是表达非负数量的场合。
一般來説,需要一個顯式的轉換將一個值從一種類型轉化位另一種類型,併且算術和邏輯運算的二元操作中必須是相同的類型。雖然這偶爾會導致需要很長的表達式,但是它消除了所有和類型相關的問題,而且也使得程序容易理解。
一般来说,需要一个显式的转换将一个值从一种类型转化位另一种类型,并且算术和逻辑运算的二元操作中必须是相同的类型。虽然这偶尔会导致需要很长的表达式,但是它消除了所有和类型相关的问题,而且也使得程序容易理解。
在很多場景,會遇到類似下面的代碼通用的錯誤
在很多场景,会遇到类似下面的代码通用的错误
```Go
var apples int32 = 1
@ -128,19 +128,19 @@ var oranges int16 = 2
var compote int = apples + oranges // compile error
```
當嚐試編譯這三個語句時,將産生一個錯誤信息:
当尝试编译这三个语句时,将产生一个错误信息:
```
invalid operation: apples + oranges (mismatched types int32 and int16)
```
這種類型不匹配的問題可以有幾種不同的方法脩複,最常見方法是將它們都顯式轉型爲一個常見類型:
这种类型不匹配的问题可以有几种不同的方法修复,最常见方法是将它们都显式转型为一个常见类型:
```Go
var compote = int(apples) + int(oranges)
```
如2.5節所述對於每種類型T如果轉換允許的話類型轉換操作T(x)將x轉換爲T類型。許多整形數之間的相互轉換併不會改變數值它們隻是告訴編譯器如何解釋這個值。但是對於將一個大尺寸的整數類型轉爲一個小尺寸的整數類型或者是將一個浮點數轉爲整數可能會改變數值或丟失精度:
如2.5节所述对于每种类型T如果转换允许的话类型转换操作T(x)将x转换为T类型。许多整形数之间的相互转换并不会改变数值它们只是告诉编译器如何解释这个值。但是对于将一个大尺寸的整数类型转为一个小尺寸的整数类型或者是将一个浮点数转为整数可能会改变数值或丢失精度:
```Go
f := 3.141 // a float64
@ -150,16 +150,16 @@ f = 1.99
fmt.Println(int(f)) // "1"
```
點數到整數的轉換將丟失任何小數部分,然後向數軸零方向截斷。你應該避免對可能會超出目標類型表示范圍的數值類型轉換,因爲截斷的行爲可能依賴於具體的實現
点数到整数的转换将丢失任何小数部分,然后向数轴零方向截断。你应该避免对可能会超出目标类型表示范围的数值类型转换,因为截断的行为可能依赖于具体的实现
```Go
f := 1e100 // a float64
i := int(f) // 結果依賴於具體實現
i := int(f) // 结果依赖于具体实现
```
任何大小的整數字面值都可以用以0開始的八進製格式書寫例如0666或用以0x或0X開頭的十六進製格式書寫例如0xdeadbeef。十六進製數字可以用大寫或小寫字母。如今八進製數據通常用於POSIX操作繫統上的文件訪問權限標誌十六進製數字則更強調數字值的bit位模式。
任何大小的整数字面值都可以用以0开始的八进制格式书写例如0666或用以0x或0X开头的十六进制格式书写例如0xdeadbeef。十六进制数字可以用大写或小写字母。如今八进制数据通常用于POSIX操作系统上的文件访问权限标志十六进制数字则更强调数字值的bit位模式。
當使用fmt包打印一個數值時我們可以用%d、%o或%x參數控製輸出的進製格式,就像下面的例子:
当使用fmt包打印一个数值时我们可以用%d、%o或%x参数控制输出的进制格式,就像下面的例子:
```Go
o := 0666
@ -170,11 +170,11 @@ fmt.Printf("%d %[1]x %#[1]x %#[1]X\n", x)
// 3735928559 deadbeef 0xdeadbeef 0XDEADBEEF
```
請註意fmt的兩個使用技巧。通常Printf格式化字符串包含多個%參數時將會包含對應相同數量的額外操作數,但是%之後的`[1]`副詞告訴Printf函數再次使用第一個操作數。第二%後的`#`副詞告訴Printf在用%o、%x或%X輸出時生成0、0x或0X前綴
请注意fmt的两个使用技巧。通常Printf格式化字符串包含多个%参数时将会包含对应相同数量的额外操作数,但是%之后的`[1]`副词告诉Printf函数再次使用第一个操作数。第二%后的`#`副词告诉Printf在用%o、%x或%X输出时生成0、0x或0X前缀
字符面值通過一對單引號直接包含對應字符。最簡單的例子是ASCII中類似'a'寫法的字符面值但是我們也可以通過轉義的數值來表示任意的Unicode碼點對應的字符馬上將會看到這樣的例子。
字符面值通过一对单引号直接包含对应字符。最简单的例子是ASCII中类似'a'写法的字符面值但是我们也可以通过转义的数值来表示任意的Unicode码点对应的字符马上将会看到这样的例子。
字符使用`%c`參數打印,或者是用`%q`參數打印帶單引號的字符:
字符使用`%c`参数打印,或者是用`%q`参数打印带单引号的字符:
```Go
ascii := 'a'

View File

@ -1,30 +1,30 @@
## 3.2. 浮點數
## 3.2. 浮点数
Go語言提供了兩種精度的浮點數float32和float64。它們的算術規范由IEEE754浮點數国際標準定義該浮點數規范被所有現代的CPU支持。
Go语言提供了两种精度的浮点数float32和float64。它们的算术规范由IEEE754浮点数国际标准定义该浮点数规范被所有现代的CPU支持。
這些浮點數類型的取值范圍可以從很微小到很鉅大。浮點數的范圍極限值可以在math包找到。常量math.MaxFloat32表示float32能表示的最大數值大約是 3.4e38對應的math.MaxFloat64常量大約是1.8e308。它們分别能表示的最小值近似爲1.4e-45和4.9e-324。
这些浮点数类型的取值范围可以从很微小到很巨大。浮点数的范围极限值可以在math包找到。常量math.MaxFloat32表示float32能表示的最大数值大约是 3.4e38对应的math.MaxFloat64常量大约是1.8e308。它们分别能表示的最小值近似为1.4e-45和4.9e-324。
個float32類型的浮點數可以提供大約6個十進製數的精度而float64則可以提供約15個十進製數的精度通常應該優先使用float64類型因爲float32類型的纍計計算誤差很容易擴散併且float32能精確表示的正整數併不是很大譯註因爲float32的有效bit位隻有23個其它的bit位用於指數和符號當整數大於23bit能表達的范圍時float32的表示將出現誤差):
个float32类型的浮点数可以提供大约6个十进制数的精度而float64则可以提供约15个十进制数的精度通常应该优先使用float64类型因为float32类型的累计计算误差很容易扩散并且float32能精确表示的正整数并不是很大译注因为float32的有效bit位只有23个其它的bit位用于指数和符号当整数大于23bit能表达的范围时float32的表示将出现误差):
```Go
var f float32 = 16777216 // 1 << 24
fmt.Println(f == f+1) // "true"!
```
點數的字面值可以直接寫小數部分,像這樣
点数的字面值可以直接写小数部分,像这样
```Go
const e = 2.71828 // (approximately)
```
數點前面或後面的數字都可能被省略(例如.707或1.。很小或很大的數最好用科學計數法書寫通過e或E來指定指數部分:
数点前面或后面的数字都可能被省略(例如.707或1.。很小或很大的数最好用科学计数法书写通过e或E来指定指数部分:
```Go
const Avogadro = 6.02214129e23 // 阿伏伽德羅常數
const Planck = 6.62606957e-34 // 普朗剋常數
const Avogadro = 6.02214129e23 // 阿伏伽德罗常数
const Planck = 6.62606957e-34 // 普朗克常数
```
用Printf函數的%g參數打印浮點數將采用更緊湊的表示形式打印併提供足夠的精度但是對應表格的數據使用%e帶指數或%f的形式打印可能更合適。所有的這三個打印形式都可以指定打印的寬度和控製打印精度。
用Printf函数的%g参数打印浮点数将采用更紧凑的表示形式打印并提供足够的精度但是对应表格的数据使用%e带指数或%f的形式打印可能更合适。所有的这三个打印形式都可以指定打印的宽度和控制打印精度。
```Go
for x := 0; x < 8; x++ {
@ -32,7 +32,7 @@ for x := 0; x < 8; x++ {
}
```
上面代碼打印e的冪打印精度是小數點後三個小數精度和8個字符寬度:
上面代码打印e的幂打印精度是小数点后三个小数精度和8个字符宽度:
```
x = 0 e^x = 1.000
@ -45,21 +45,21 @@ x = 6 e^x = 403.429
x = 7 e^x = 1096.633
```
math包中除了提供大量常用的數學函數外還提供了IEEE754浮點數標準中定義的特殊值的創建和測試正無窮大和負無窮大分别用於表示太大溢出的數字和除零的結果還有NaN非數一般用於表示無效的除法操作結果0/0或Sqrt(-1).
math包中除了提供大量常用的数学函数外还提供了IEEE754浮点数标准中定义的特殊值的创建和测试正无穷大和负无穷大分别用于表示太大溢出的数字和除零的结果还有NaN非数一般用于表示无效的除法操作结果0/0或Sqrt(-1).
```Go
var z float64
fmt.Println(z, -z, 1/z, -1/z, z/z) // "0 -0 +Inf -Inf NaN"
```
數math.IsNaN用於測試一個數是否是非數NaNmath.NaN則返迴非數對應的值。雖然可以用math.NaN來表示一個非法的結果但是測試一個結果是否是非數NaN則是充滿風險的因爲NaN和任何數都是不相等的譯註在浮點數中NaN、正無窮大和負無窮大都不是唯一的每個都有非常多種的bit模式表示
数math.IsNaN用于测试一个数是否是非数NaNmath.NaN则返回非数对应的值。虽然可以用math.NaN来表示一个非法的结果但是测试一个结果是否是非数NaN则是充满风险的因为NaN和任何数都是不相等的译注在浮点数中NaN、正无穷大和负无穷大都不是唯一的每个都有非常多种的bit模式表示
```Go
nan := math.NaN()
fmt.Println(nan == nan, nan < nan, nan > nan) // "false false false"
```
如果一個函數返迴的浮點數結果可能失敗,最好的做法是用單獨的標誌報告失敗,像這樣
如果一个函数返回的浮点数结果可能失败,最好的做法是用单独的标志报告失败,像这样
```Go
func compute() (value float64, ok bool) {
@ -71,7 +71,7 @@ func compute() (value float64, ok bool) {
}
```
接下來的程序演示了通過浮點計算生成的圖形。它是帶有兩個參數的z = f(x, y)函數的三維形式使用了可縮放矢量圖形SVG格式輸出SVG是一個用於矢量線繪製的XML標準。圖3.1顯示了sin(r)/r函數的輸出圖其中r是sqrt(x*x+y*y)。
接下来的程序演示了通过浮点计算生成的图形。它是带有两个参数的z = f(x, y)函数的三维形式使用了可缩放矢量图形SVG格式输出SVG是一个用于矢量线绘制的XML标准。图3.1显示了sin(r)/r函数的输出图其中r是sqrt(x*x+y*y)。
![](../images/ch3-01.png)
@ -133,30 +133,30 @@ func f(x, y float64) float64 {
}
```
註意的是corner函數返迴了兩個結果分别對應每個網格頂點的坐標參數
注意的是corner函数返回了两个结果分别对应每个网格顶点的坐标参数
要解釋這個程序是如何工作的需要一些基本的幾何學知識但是我們可以跳過幾何學原理因爲程序的重點是演示浮點數運算。程序的本質是三個不同的坐標繫中映射關繫如圖3.2所示。第一個是100x100的二維網格對應整數整數坐標(i,j),從遠處的(0, 0)位置開始。我們從遠處向前面繪製,因此遠處先繪製的多邊形有可能被前面後繪製的多邊形覆蓋
要解释这个程序是如何工作的需要一些基本的几何学知识但是我们可以跳过几何学原理因为程序的重点是演示浮点数运算。程序的本质是三个不同的坐标系中映射关系如图3.2所示。第一个是100x100的二维网格对应整数整数坐标(i,j),从远处的(0, 0)位置开始。我们从远处向前面绘制,因此远处先绘制的多边形有可能被前面后绘制的多边形覆盖
第二個坐標繫是一個三維的網格浮點坐標(x,y,z)其中x和y是i和j的線性函數通過平移轉換位網格單元的中心然後用xyrange繫數縮放。高度z是函數f(x,y)的值。
第二个坐标系是一个三维的网格浮点坐标(x,y,z)其中x和y是i和j的线性函数通过平移转换位网格单元的中心然后用xyrange系数缩放。高度z是函数f(x,y)的值。
第三個坐標繫是一個二維的畵布,起點(0,0)在左上角。畵布中點的坐標用(sx, sy)表示。我們使用等角投影將三維點
第三个坐标系是一个二维的画布,起点(0,0)在左上角。画布中点的坐标用(sx, sy)表示。我们使用等角投影将三维点
![](../images/ch3-02.png)
(x,y,z)投影到二維的畵布中。畵布中從遠處到右邊的點對應較大的x值和較大的y值。併且畵布中x和y值越大則對應的z值越小。x和y的垂直和水平縮放繫數來自30度角的正絃和餘絃值。z的縮放繫數0.4,是一個任意選擇的參數
(x,y,z)投影到二维的画布中。画布中从远处到右边的点对应较大的x值和较大的y值。并且画布中x和y值越大则对应的z值越小。x和y的垂直和水平缩放系数来自30度角的正弦和余弦值。z的缩放系数0.4,是一个任意选择的参数
對於二維網格中的每一個網格單元main函數計算單元的四個頂點在畵布中對應多邊形ABCD的頂點其中B對應(i,j)頂點位置A、C和D是其它相鄰的頂點然後輸出SVG的繪製指令。
对于二维网格中的每一个网格单元main函数计算单元的四个顶点在画布中对应多边形ABCD的顶点其中B对应(i,j)顶点位置A、C和D是其它相邻的顶点然后输出SVG的绘制指令。
**練習 3.1** 如果f函數返迴的是無限製的float64值那麽SVG文件可能輸出無效的<polygon>多邊形元素雖然許多SVG渲染器會妥善處理這類問題。脩改程序跳過無效的多邊形。
**练习 3.1** 如果f函数返回的是无限制的float64值那么SVG文件可能输出无效的<polygon>多边形元素虽然许多SVG渲染器会妥善处理这类问题。修改程序跳过无效的多边形。
**練習 3.2** 試驗math包中其他函數的渲染圖形。你是否能輸出一個egg box、moguls或a saddle圖案?
**练习 3.2** 试验math包中其他函数的渲染图形。你是否能输出一个egg box、moguls或a saddle图案?
**練習 3.3** 根據高度給每個多邊形上色,那樣峯值部將是紅色(#ff0000),谷部將是藍色(#0000ff)。
**练习 3.3** 根据高度给每个多边形上色,那样峰值部将是红色(#ff0000),谷部将是蓝色(#0000ff)。
**練習 3.4** 參考1.7節Lissajous例子的函數構造一個web服務器用於計算函數麴面然後返迴SVG數據給客戶端。服務器必須設置Content-Type頭部:
**练习 3.4** 参考1.7节Lissajous例子的函数构造一个web服务器用于计算函数曲面然后返回SVG数据给客户端。服务器必须设置Content-Type头部:
```Go
w.Header().Set("Content-Type", "image/svg+xml")
```
這一步在Lissajous例子中不是必須的因爲服務器使用標準的PNG圖像格式可以根據前面的512個字節自動輸出對應的頭部。允許客戶端通過HTTP請求參數設置高度、寬度和顔色等參數
这一步在Lissajous例子中不是必须的因为服务器使用标准的PNG图像格式可以根据前面的512个字节自动输出对应的头部。允许客户端通过HTTP请求参数设置高度、宽度和颜色等参数

View File

@ -1,6 +1,6 @@
## 3.3. 複數
## 3.3. 复数
Go語言提供了兩種精度的複數類型complex64和complex128分别對應float32和float64兩種浮點數精度。內置的complex函數用於構建複數內建的real和imag函數分别返迴複數的實部和虛部:
Go语言提供了两种精度的复数类型complex64和complex128分别对应float32和float64两种浮点数精度。内置的complex函数用于构建复数内建的real和imag函数分别返回复数的实部和虚部:
```Go
var x complex128 = complex(1, 2) // 1+2i
@ -10,28 +10,28 @@ fmt.Println(real(x*y)) // "-5"
fmt.Println(imag(x*y)) // "10"
```
如果一個浮點數面值或一個十進製整數面值後面跟着一個i例如3.141592i或2i它將構成一個複數的虛部複數的實部是0
如果一个浮点数面值或一个十进制整数面值后面跟着一个i例如3.141592i或2i它将构成一个复数的虚部复数的实部是0
```Go
fmt.Println(1i * 1i) // "(-1+0i)", i^2 = -1
```
在常量算術規則下一個複數常量可以加到另一個普通數值常量整數或浮點數、實部或虛部我們可以用自然的方式書寫複數就像1+2i或與之等價的寫法2i+1。上面x和y的聲明語句還可以簡化:
在常量算术规则下一个复数常量可以加到另一个普通数值常量整数或浮点数、实部或虚部我们可以用自然的方式书写复数就像1+2i或与之等价的写法2i+1。上面x和y的声明语句还可以简化:
```Go
x := 1 + 2i
y := 3 + 4i
```
複數也可以用==和!=進行相等比較。隻有兩個複數的實部和虛部都相等的時候它們才是相等的(譯註:浮點數的相等比較是危險的,需要特别小心處理精度問題)。
复数也可以用==和!=进行相等比较。只有两个复数的实部和虚部都相等的时候它们才是相等的(译注:浮点数的相等比较是危险的,需要特别小心处理精度问题)。
math/cmplx包提供了複數處理的許多函數,例如求複數的平方根函數和求冪函數
math/cmplx包提供了复数处理的许多函数,例如求复数的平方根函数和求幂函数
```Go
fmt.Println(cmplx.Sqrt(-1)) // "(0+1i)"
```
下面的程序使用complex128複數算法來生成一個Mandelbrot圖像。
下面的程序使用complex128复数算法来生成一个Mandelbrot图像。
<u><i>gopl.io/ch3/mandelbrot</i></u>
```Go
@ -81,16 +81,16 @@ func mandelbrot(z complex128) color.Color {
}
```
於遍歷1024x1024圖像每個點的兩個嵌套的循環對應-2到+2區間的複數平面。程序反複測試每個點對應複數值平方值加一個增量值對應的點是否超出半徑爲2的圓。如果超過了通過根據預設置的逃逸迭代次數對應的灰度顔色來代替。如果不是那麽該點屬於Mandelbrot集合使用黑色顔色標記。最終程序將生成的PNG格式分形圖像圖像輸出到標準輸出如圖3.3所示。
于遍历1024x1024图像每个点的两个嵌套的循环对应-2到+2区间的复数平面。程序反复测试每个点对应复数值平方值加一个增量值对应的点是否超出半径为2的圆。如果超过了通过根据预设置的逃逸迭代次数对应的灰度颜色来代替。如果不是那么该点属于Mandelbrot集合使用黑色颜色标记。最终程序将生成的PNG格式分形图像图像输出到标准输出如图3.3所示。
![](../images/ch3-03.png)
**練習 3.5** 實現一個綵色的Mandelbrot圖像使用image.NewRGBA創建圖像使用color.RGBA或color.YCbCr生成顔色。
**练习 3.5** 实现一个彩色的Mandelbrot图像使用image.NewRGBA创建图像使用color.RGBA或color.YCbCr生成颜色。
**練習 3.6** 陞采樣技術可以降低每個像素對計算顔色值和平均值的影響。簡單的方法是將每個像素分層四個子像素,實現它。
**练习 3.6** 升采样技术可以降低每个像素对计算颜色值和平均值的影响。简单的方法是将每个像素分层四个子像素,实现它。
**練習 3.7** 另一個生成分形圖像的方式是使用牛頓法來求解一個複數方程,例如$$z^4-1=0$$。每個起點到四個根的迭代次數對應陰影的灰度。方程根對應的點用顔色表示。
**练习 3.7** 另一个生成分形图像的方式是使用牛顿法来求解一个复数方程,例如$$z^4-1=0$$。每个起点到四个根的迭代次数对应阴影的灰度。方程根对应的点用颜色表示。
**練習 3.8** 通過提高精度來生成更多級别的分形。使用四種不同精度類型的數字實現相同的分形complex64、complex128、big.Float和big.Rat。後面兩種類型在math/big包聲明。Float是有指定限精度的浮點數Rat是無效精度的有理數。它們間的性能和內存使用對比如何當渲染圖可見時縮放的級别是多少?
**练习 3.8** 通过提高精度来生成更多级别的分形。使用四种不同精度类型的数字实现相同的分形complex64、complex128、big.Float和big.Rat。后面两种类型在math/big包声明。Float是有指定限精度的浮点数Rat是无效精度的有理数。它们间的性能和内存使用对比如何当渲染图可见时缩放的级别是多少?
**練習 3.9** 編寫一個web服務器用於給客戶端生成分形的圖像。運行客戶端用過HTTP參數參數指定x,y和zoom參數
**练习 3.9** 编写一个web服务器用于给客户端生成分形的图像。运行客户端用过HTTP参数参数指定x,y和zoom参数

View File

@ -1,16 +1,16 @@
## 3.4. 布
## 3.4. 布
個布爾類型的值隻有兩種true和false。if和for語句的條件部分都是布爾類型的值併且==和<等比較操作也會産生布爾型的值。一元操作符`!`對應邏輯非操作,因此`!true`的值爲`false`,更羅嗦的説法是`(!true==false)==true`雖然表達方式不一樣不過我們一般會采用簡潔的布爾表達式就像用x來表示`x==true`。
个布尔类型的值只有两种true和false。if和for语句的条件部分都是布尔类型的值并且==和<等比较操作也会产生布尔型的值。一元操作符`!`对应逻辑非操作,因此`!true`的值为`false`,更罗嗦的说法是`(!true==false)==true`虽然表达方式不一样不过我们一般会采用简洁的布尔表达式就像用x来表示`x==true`。
爾值可以和&&AND和||OR操作符結合併且可能會有短路行爲如果運算符左邊值已經可以確定整個布爾表達式的值那麽運算符右邊的值將不在被求值因此下面的表達式總是安全的:
尔值可以和&&AND和||OR操作符结合并且可能会有短路行为如果运算符左边值已经可以确定整个布尔表达式的值那么运算符右边的值将不在被求值因此下面的表达式总是安全的:
```Go
s != "" && s[0] == 'x'
```
其中s[0]操作如果應用於空字符串將會導致panic異常。
其中s[0]操作如果应用于空字符串将会导致panic异常。
爲`&&`的優先級比`||`高(助記:`&&`對應邏輯乘法,`||`對應邏輯加法,乘法比加法優先級要高),下面形式的布爾表達式是不需要加小括弧的:
为`&&`的优先级比`||`高(助记:`&&`对应逻辑乘法,`||`对应逻辑加法,乘法比加法优先级要高),下面形式的布尔表达式是不需要加小括弧的:
```Go
if 'a' <= c && c <= 'z' ||
@ -20,7 +20,7 @@ if 'a' <= c && c <= 'z' ||
}
```
爾值併不會隱式轉換爲數字值0或1反之亦然。必須使用一個顯式的if語句輔助轉換
尔值并不会隐式转换为数字值0或1反之亦然。必须使用一个显式的if语句辅助转换
```Go
i := 0
@ -29,7 +29,7 @@ if b {
}
```
如果需要經常做類似的轉換, 包裝成一個函數會更方便:
如果需要经常做类似的转换, 包装成一个函数会更方便:
```Go
// btoi returns 1 if b is true and 0 if false.
@ -41,7 +41,7 @@ func btoi(b bool) int {
}
```
數字到布爾型的逆轉換則非常簡單, 不過爲了保持對稱, 我們也可以包裝一個函數:
数字到布尔型的逆转换则非常简单, 不过为了保持对称, 我们也可以包装一个函数:
```Go
// itob reports whether i is non-zero.

View File

@ -1,6 +1,6 @@
### 3.5.1. 字符串面值
字符串值也可以用字符串面值方式編寫,隻要將一繫列字節序列包含在雙引號卽可:
字符串值也可以用字符串面值方式编写,只要将一系列字节序列包含在双引号即可:
```
"Hello, 世界"
@ -8,28 +8,28 @@
![](../images/ch3-04.png)
爲Go語言源文件總是用UTF8編碼併且Go語言的文本字符串也以UTF8編碼的方式處理因此我們可以將Unicode碼點也寫到字符串面值中。
为Go语言源文件总是用UTF8编码并且Go语言的文本字符串也以UTF8编码的方式处理因此我们可以将Unicode码点也写到字符串面值中。
在一個雙引號包含的字符串面值中,可以用以反斜槓`\`開頭的轉義序列插入任意的數據。下面的換行、迴車和製表符等是常見的ASCII控製代碼的轉義方式:
在一个双引号包含的字符串面值中,可以用以反斜杠`\`开头的转义序列插入任意的数据。下面的换行、回车和制表符等是常见的ASCII控制代码的转义方式:
```
\a 響鈴
\a 响铃
\b 退格
\f 換頁
\n
\r 迴車
\t 表符
\v 垂直表符
\' 單引號 (隻用在 '\'' 形式的rune符號面值中)
\" 雙引號 (隻用在 "..." 形式的字符串面值中)
\\ 反斜
\f 换页
\n
\r 回车
\t 表符
\v 垂直表符
\' 单引号 (只用在 '\'' 形式的rune符号面值中)
\" 双引号 (只用在 "..." 形式的字符串面值中)
\\ 反斜
```
可以通過十六進製或八進製轉義在字符串面值包含任意的字節。一個十六進製的轉義形式是\xhh其中兩個h表示十六進製數字大寫或小寫都可以。一個八進製轉義形式是\ooo包含三個八進製的o數字0到7但是不能超過`\377`譯註對應一個字節的范圍十進製爲255。每一個單一的字節表達一個特定的值。稍後我們將看到如何將一個Unicode碼點寫到字符串面值中。
可以通过十六进制或八进制转义在字符串面值包含任意的字节。一个十六进制的转义形式是\xhh其中两个h表示十六进制数字大写或小写都可以。一个八进制转义形式是\ooo包含三个八进制的o数字0到7但是不能超过`\377`译注对应一个字节的范围十进制为255。每一个单一的字节表达一个特定的值。稍后我们将看到如何将一个Unicode码点写到字符串面值中。
個原生的字符串面值形式是`...`,使用反引號```代替雙引號。在原生的字符串面值中,沒有轉義操作;全部的內容都是字面的意思,包含退格和換行,因此一個程序中的原生字符串面值可能跨越多行(譯註:在原生字符串面值內部是無法直接寫```字符的,可以用八進製或十六進製轉義或+"```"鏈接字符串常量完成。唯一的特殊處理是會刪除迴車以保證在所有平台上的值都是一樣的包括那些把迴車也放入文本文件的繫統譯註Windows繫統會把迴車和換行一起放入文本文件中)。
个原生的字符串面值形式是`...`,使用反引号```代替双引号。在原生的字符串面值中,没有转义操作;全部的内容都是字面的意思,包含退格和换行,因此一个程序中的原生字符串面值可能跨越多行(译注:在原生字符串面值内部是无法直接写```字符的,可以用八进制或十六进制转义或+"```"链接字符串常量完成。唯一的特殊处理是会删除回车以保证在所有平台上的值都是一样的包括那些把回车也放入文本文件的系统译注Windows系统会把回车和换行一起放入文本文件中)。
原生字符串面值用於編寫正則表達式會很方便因爲正則表達式往往會包含很多反斜槓。原生字符串面值同時被廣泛應用於HTML模闆、JSON面值、命令行提示信息以及那些需要擴展到多行的場景。
原生字符串面值用于编写正则表达式会很方便因为正则表达式往往会包含很多反斜杠。原生字符串面值同时被广泛应用于HTML模板、JSON面值、命令行提示信息以及那些需要扩展到多行的场景。
```Go
const GoUsage = `Go is a tool for managing Go source code.

View File

@ -1,12 +1,12 @@
### 3.5.2. Unicode
在很久以前,世界還是比較簡單的起碼計算機世界就隻有一個ASCII字符集美国信息交換標準代碼。ASCII更準確地説是美国的ASCII使用7bit來表示128個字符包含英文字母的大小寫、數字、各種標點符號和設置控製符。對於早期的計算機程序來説這些就足夠了但是這也導致了世界上很多其他地區的用戶無法直接使用自己的符號繫統。隨着互聯網的發展混合多種語言的數據變得很常見譯註比如本身的英文原文或中文翻譯都包含了ASCII、中文、日文等多種語言字符。如何有效處理這些包含了各種語言的豐富多樣的文本數據呢?
在很久以前,世界还是比较简单的起码计算机世界就只有一个ASCII字符集美国信息交换标准代码。ASCII更准确地说是美国的ASCII使用7bit来表示128个字符包含英文字母的大小写、数字、各种标点符号和设置控制符。对于早期的计算机程序来说这些就足够了但是这也导致了世界上很多其他地区的用户无法直接使用自己的符号系统。随着互联网的发展混合多种语言的数据变得很常见译注比如本身的英文原文或中文翻译都包含了ASCII、中文、日文等多种语言字符。如何有效处理这些包含了各种语言的丰富多样的文本数据呢?
答案就是使用Unicode http://unicode.org ),它收集了這個世界上所有的符號繫統包括重音符號和其它變音符號製表符和迴車符還有很多神祕的符號每個符號都分配一個唯一的Unicode碼點Unicode碼點對應Go語言中的rune整數類型譯註rune是int32等價類型)。
答案就是使用Unicode http://unicode.org ),它收集了这个世界上所有的符号系统包括重音符号和其它变音符号制表符和回车符还有很多神秘的符号每个符号都分配一个唯一的Unicode码点Unicode码点对应Go语言中的rune整数类型译注rune是int32等价类型)。
在第八版本的Unicode標準收集了超過120,000個字符涵蓋超過100多種語言。這些在計算機程序和數據中是如何體現的呢通用的表示一個Unicode碼點的數據類型是int32也就是Go語言中rune對應的類型它的同義詞rune符文正是這個意思。
在第八版本的Unicode标准收集了超过120,000个字符涵盖超过100多种语言。这些在计算机程序和数据中是如何体现的呢通用的表示一个Unicode码点的数据类型是int32也就是Go语言中rune对应的类型它的同义词rune符文正是这个意思。
們可以將一個符文序列表示爲一個int32序列。這種編碼方式叫UTF-32或UCS-4每個Unicode碼點都使用同樣的大小32bit來表示。這種方式比較簡單統一但是它會浪費很多存儲空間因爲大數據計算機可讀的文本是ASCII字符本來每個ASCII字符隻需要8bit或1字節就能表示。而且卽使是常用的字符也遠少於65,536個也就是説用16bit編碼方式就能表達常用字符。但是還有其它更好的編碼方法嗎
们可以将一个符文序列表示为一个int32序列。这种编码方式叫UTF-32或UCS-4每个Unicode码点都使用同样的大小32bit来表示。这种方式比较简单统一但是它会浪费很多存储空间因为大数据计算机可读的文本是ASCII字符本来每个ASCII字符只需要8bit或1字节就能表示。而且即使是常用的字符也远少于65,536个也就是说用16bit编码方式就能表达常用字符。但是还有其它更好的编码方法吗

View File

@ -1,6 +1,6 @@
### 3.5.3. UTF-8
UTF8是一個將Unicode碼點編碼爲字節序列的變長編碼。UTF8編碼由Go語言之父Ken Thompson和Rob Pike共同發明的現在已經是Unicode的標準。UTF8編碼使用1到4個字節來表示每個Unicode碼點ASCII部分字符隻使用1個字節常用字符部分使用2或3個字節表示。每個符號編碼後第一個字節的高端bit位用於表示總共有多少編碼個字節。如果第一個字節的高端bit爲0則表示對應7bit的ASCII字符ASCII字符每個字符依然是一個字節和傳統的ASCII編碼兼容。如果第一個字節的高端bit是110則説明需要2個字節後續的每個高端bit都以10開頭。更大的Unicode碼點也是采用類似的策略處理。
UTF8是一个将Unicode码点编码为字节序列的变长编码。UTF8编码由Go语言之父Ken Thompson和Rob Pike共同发明的现在已经是Unicode的标准。UTF8编码使用1到4个字节来表示每个Unicode码点ASCII部分字符只使用1个字节常用字符部分使用2或3个字节表示。每个符号编码后第一个字节的高端bit位用于表示总共有多少编码个字节。如果第一个字节的高端bit为0则表示对应7bit的ASCII字符ASCII字符每个字符依然是一个字节和传统的ASCII编码兼容。如果第一个字节的高端bit是110则说明需要2个字节后续的每个高端bit都以10开头。更大的Unicode码点也是采用类似的策略处理。
```
0xxxxxxx runes 0-127 (ASCII)
@ -9,11 +9,11 @@ UTF8是一個將Unicode碼點編碼爲字節序列的變長編碼。UTF8編碼
11110xxx 10xxxxxx 10xxxxxx 10xxxxxx 65536-0x10ffff (other values unused)
```
變長的編碼無法直接通過索引來訪問第n個字符但是UTF8編碼獲得了很多額外的優點。首先UTF8編碼比較緊湊完全兼容ASCII碼併且可以自動同步它可以通過向前迴朔最多2個字節就能確定當前字符編碼的開始字節的位置。它也是一個前綴編碼所以當從左向右解碼時不會有任何歧義也併不需要向前査看譯註像GBK之類的編碼如果不知道起點位置則可能會出現歧義。沒有任何字符的編碼是其它字符編碼的子串或是其它編碼序列的字串因此蒐索一個字符時隻要蒐索它的字節編碼序列卽可不用擔心前後的上下文會對蒐索結果産生榦擾。同時UTF8編碼的順序和Unicode碼點的順序一致因此可以直接排序UTF8編碼序列。同時因爲沒有嵌入的NUL(0)字節可以很好地兼容那些使用NUL作爲字符串結尾的編程語言。
变长的编码无法直接通过索引来访问第n个字符但是UTF8编码获得了很多额外的优点。首先UTF8编码比较紧凑完全兼容ASCII码并且可以自动同步它可以通过向前回朔最多2个字节就能确定当前字符编码的开始字节的位置。它也是一个前缀编码所以当从左向右解码时不会有任何歧义也并不需要向前查看译注像GBK之类的编码如果不知道起点位置则可能会出现歧义。没有任何字符的编码是其它字符编码的子串或是其它编码序列的字串因此搜索一个字符时只要搜索它的字节编码序列即可不用担心前后的上下文会对搜索结果产生干扰。同时UTF8编码的顺序和Unicode码点的顺序一致因此可以直接排序UTF8编码序列。同时因为没有嵌入的NUL(0)字节可以很好地兼容那些使用NUL作为字符串结尾的编程语言。
Go語言的源文件采用UTF8編碼併且Go語言處理UTF8編碼的文本也很出色。unicode包提供了諸多處理rune字符相關功能的函數比如區分字母和數組或者是字母的大寫和小寫轉換等unicode/utf8包則提供了用於rune字符序列的UTF8編碼和解碼的功能。
Go语言的源文件采用UTF8编码并且Go语言处理UTF8编码的文本也很出色。unicode包提供了诸多处理rune字符相关功能的函数比如区分字母和数组或者是字母的大写和小写转换等unicode/utf8包则提供了用于rune字符序列的UTF8编码和解码的功能。
有很多Unicode字符很難直接從鍵盤輸入併且還有很多字符有着相似的結構有一些甚至是不可見的字符譯註中文和日文就有很多相似但不同的字。Go語言字符串面值中的Unicode轉義字符讓我們可以通過Unicode碼點輸入特殊的字符。有兩種形式\uhhhh對應16bit的碼點值\Uhhhhhhhh對應32bit的碼點值其中h是一個十六進製數字一般很少需要使用32bit的形式。每一個對應碼點的UTF8編碼。例如:下面的字母串面值都表示相同的值:
有很多Unicode字符很难直接从键盘输入并且还有很多字符有着相似的结构有一些甚至是不可见的字符译注中文和日文就有很多相似但不同的字。Go语言字符串面值中的Unicode转义字符让我们可以通过Unicode码点输入特殊的字符。有两种形式\uhhhh对应16bit的码点值\Uhhhhhhhh对应32bit的码点值其中h是一个十六进制数字一般很少需要使用32bit的形式。每一个对应码点的UTF8编码。例如:下面的字母串面值都表示相同的值:
```
"世界"
@ -22,17 +22,17 @@ Go語言的源文件采用UTF8編碼併且Go語言處理UTF8編碼的文本
"\U00004e16\U0000754c"
```
上面三個轉義序列都爲第一個字符串提供替代寫法,但是它們的值都是相同的。
上面三个转义序列都为第一个字符串提供替代写法,但是它们的值都是相同的。
Unicode轉義也可以使用在rune字符中。下面三個字符是等價的:
Unicode转义也可以使用在rune字符中。下面三个字符是等价的:
```
'世' '\u4e16' '\U00004e16'
```
對於小於256碼點值可以寫在一個十六進製轉義字節中例如'\x41'對應字符'A',但是對於更大的碼點則必須使用\u或\U轉義形式。因此'\xe4\xb8\x96'併不是一個合法的rune字符雖然這三個字節對應一個有效的UTF8編碼的碼點
对于小于256码点值可以写在一个十六进制转义字节中例如'\x41'对应字符'A',但是对于更大的码点则必须使用\u或\U转义形式。因此'\xe4\xb8\x96'并不是一个合法的rune字符虽然这三个字节对应一个有效的UTF8编码的码点
得益於UTF8編碼優良的設計諸多字符串操作都不需要解碼操作。我們可以不用解碼直接測試一個字符串是否是另一個字符串的前綴
得益于UTF8编码优良的设计诸多字符串操作都不需要解码操作。我们可以不用解码直接测试一个字符串是否是另一个字符串的前缀
```Go
func HasPrefix(s, prefix string) bool {
@ -40,7 +40,7 @@ func HasPrefix(s, prefix string) bool {
}
```
或者是後綴測試
或者是后缀测试
```Go
func HasSuffix(s, suffix string) bool {
@ -48,7 +48,7 @@ func HasSuffix(s, suffix string) bool {
}
```
或者是包含子串測試
或者是包含子串测试
```Go
func Contains(s, substr string) bool {
@ -61,9 +61,9 @@ func Contains(s, substr string) bool {
}
```
對於UTF8編碼後文本的處理和原始的字節處理邏輯是一樣的。但是對應很多其它編碼則併不是這樣的。上面的函數都來自strings字符串處理包眞實的代碼包含了一個用哈希技術優化的Contains 實現。)
对于UTF8编码后文本的处理和原始的字节处理逻辑是一样的。但是对应很多其它编码则并不是这样的。上面的函数都来自strings字符串处理包真实的代码包含了一个用哈希技术优化的Contains 实现。)
另一方面,如果我們眞的關心每個Unicode字符我們可以使用其它處理方式。考慮前面的第一個例子中的字符串它包混合了中西兩種字符。圖3.5展示了它的內存表示形式。字符串包含13個字節以UTF8形式編碼但是隻對應9個Unicode字符
另一方面,如果我们真的关心每个Unicode字符我们可以使用其它处理方式。考虑前面的第一个例子中的字符串它包混合了中西两种字符。图3.5展示了它的内存表示形式。字符串包含13个字节以UTF8形式编码但是只对应9个Unicode字符
```Go
import "unicode/utf8"
@ -73,7 +73,7 @@ fmt.Println(len(s)) // "13"
fmt.Println(utf8.RuneCountInString(s)) // "9"
```
爲了處理這些眞實的字符我們需要一個UTF8解碼器。unicode/utf8包提供了該功能我們可以這樣使用:
为了处理这些真实的字符我们需要一个UTF8解码器。unicode/utf8包提供了该功能我们可以这样使用:
```Go
for i := 0; i < len(s); {
@ -83,7 +83,7 @@ for i := 0; i < len(s); {
}
```
每一次調用DecodeRuneInString函數都返迴一個r和長度r對應字符本身長度對應r采用UTF8編碼後的編碼字節數目。長度可以用於更新第i個字符在字符串中的字節索引位置。但是這種編碼方式是笨拙的我們需要更簡潔的語法。幸運的是Go語言的range循環在處理字符串的時候會自動隱式解碼UTF8字符串。下面的循環運行如圖3.5所示需要註意的是對於非ASCII索引更新的步長將超過1個字節
每一次调用DecodeRuneInString函数都返回一个r和长度r对应字符本身长度对应r采用UTF8编码后的编码字节数目。长度可以用于更新第i个字符在字符串中的字节索引位置。但是这种编码方式是笨拙的我们需要更简洁的语法。幸运的是Go语言的range循环在处理字符串的时候会自动隐式解码UTF8字符串。下面的循环运行如图3.5所示需要注意的是对于非ASCII索引更新的步长将超过1个字节
![](../images/ch3-05.png)
@ -93,7 +93,7 @@ for i, r := range "Hello, 世界" {
}
```
們可以使用一個簡單的循環來統計字符串中字符的數目,像這樣
们可以使用一个简单的循环来统计字符串中字符的数目,像这样
```Go
n := 0
@ -102,7 +102,7 @@ for _, _ = range s {
}
```
像其它形式的循環那樣,我們也可以忽略不需要的變量:
像其它形式的循环那样,我们也可以忽略不需要的变量:
```Go
n := 0
@ -111,15 +111,15 @@ for range s {
}
```
或者我們可以直接調用utf8.RuneCountInString(s)函數
或者我们可以直接调用utf8.RuneCountInString(s)函数
正如我們前面提到的文本字符串采用UTF8編碼隻是一種慣例但是對於循環的眞正字符串併不是一個慣例這是正確的。如果用於循環的字符串隻是一個普通的二進製數據或者是含有錯誤編碼的UTF8數據將會發送什麽呢?
正如我们前面提到的文本字符串采用UTF8编码只是一种惯例但是对于循环的真正字符串并不是一个惯例这是正确的。如果用于循环的字符串只是一个普通的二进制数据或者是含有错误编码的UTF8数据将会发送什么呢?
每一個UTF8字符解碼不管是顯式地調用utf8.DecodeRuneInString解碼或是在range循環中隱式地解碼如果遇到一個錯誤的UTF8編碼輸入將生成一個特别的Unicode字符'\uFFFD',在印刷中這個符號通常是一個黑色六角或鑽石形狀,里面包含一個白色的問號"<22>"。當程序遇到這樣的一個字符,通常是一個危險信號,説明輸入併不是一個完美沒有錯誤的UTF8字符串。
每一个UTF8字符解码不管是显式地调用utf8.DecodeRuneInString解码或是在range循环中隐式地解码如果遇到一个错误的UTF8编码输入将生成一个特别的Unicode字符'\uFFFD',在印刷中这个符号通常是一个黑色六角或钻石形状,里面包含一个白色的问号"<22>"。当程序遇到这样的一个字符,通常是一个危险信号,说明输入并不是一个完美没有错误的UTF8字符串。
UTF8字符串作爲交換格式是非常方便的但是在程序內部采用rune序列可能更方便因爲rune大小一致支持數組索引和方便切割。
UTF8字符串作为交换格式是非常方便的但是在程序内部采用rune序列可能更方便因为rune大小一致支持数组索引和方便切割。
string接受到[]rune的類型轉換可以將一個UTF8編碼的字符串解碼爲Unicode字符序列
string接受到[]rune的类型转换可以将一个UTF8编码的字符串解码为Unicode字符序列
```Go
// "program" in Japanese katakana
@ -129,22 +129,22 @@ r := []rune(s)
fmt.Printf("%x\n", r) // "[30d7 30ed 30b0 30e9 30e0]"
```
(在第一個Printf中的`% x`參數用於在每個十六進製數字前插入一個空格。)
(在第一个Printf中的`% x`参数用于在每个十六进制数字前插入一个空格。)
如果是將一個[]rune類型的Unicode字符slice或數組轉爲string則對它們進行UTF8編碼
如果是将一个[]rune类型的Unicode字符slice或数组转为string则对它们进行UTF8编码
```Go
fmt.Println(string(r)) // "プログラム"
```
將一個整數轉型爲字符串意思是生成以隻包含對應Unicode碼點字符的UTF8字符串
将一个整数转型为字符串意思是生成以只包含对应Unicode码点字符的UTF8字符串
```Go
fmt.Println(string(65)) // "A", not "65"
fmt.Println(string(0x4eac)) // "京"
```
如果對應碼點的字符是無效的,則用'\uFFFD'無效字符作爲替換
如果对应码点的字符是无效的,则用'\uFFFD'无效字符作为替换
```Go
fmt.Println(string(1234567)) // "<22>"

View File

@ -1,14 +1,14 @@
### 3.5.4. 字符串和Byte切片
標準庫中有四個包對字符串處理尤爲重要bytes、strings、strconv和unicode包。strings包提供了許多如字符串的査詢、替換、比較、截斷、拆分和合併等功能。
标准库中有四个包对字符串处理尤为重要bytes、strings、strconv和unicode包。strings包提供了许多如字符串的查询、替换、比较、截断、拆分和合并等功能。
bytes包也提供了很多類似功能的函數,但是針對和字符串有着相同結構的[]byte類型。因爲字符串是隻讀的因此逐步構建字符串會導致很多分配和複製。在這種情況下使用bytes.Buffer類型將會更有效稍後我們將展示。
bytes包也提供了很多类似功能的函数,但是针对和字符串有着相同结构的[]byte类型。因为字符串是只读的因此逐步构建字符串会导致很多分配和复制。在这种情况下使用bytes.Buffer类型将会更有效稍后我们将展示。
strconv包提供了布爾型、整型數、浮點數和對應字符串的相互轉換,還提供了雙引號轉義相關的轉換
strconv包提供了布尔型、整型数、浮点数和对应字符串的相互转换,还提供了双引号转义相关的转换
unicode包提供了IsDigit、IsLetter、IsUpper和IsLower等類似功能它們用於給字符分類。每個函數有一個單一的rune類型的參數然後返迴一個布爾值。而像ToUpper和ToLower之類的轉換函數將用於rune字符的大小寫轉換。所有的這些函數都是遵循Unicode標準定義的字母、數字等分類規范。strings包也有類似的函數它們是ToUpper和ToLower將原始字符串的每個字符都做相應的轉換然後返迴新的字符串。
unicode包提供了IsDigit、IsLetter、IsUpper和IsLower等类似功能它们用于给字符分类。每个函数有一个单一的rune类型的参数然后返回一个布尔值。而像ToUpper和ToLower之类的转换函数将用于rune字符的大小写转换。所有的这些函数都是遵循Unicode标准定义的字母、数字等分类规范。strings包也有类似的函数它们是ToUpper和ToLower将原始字符串的每个字符都做相应的转换然后返回新的字符串。
下面例子的basename函數靈感於Unix shell的同名工具。在我們實現的版本中basename(s)將看起來像是繫統路徑的前綴刪除,同時將看似文件類型的後綴名部分刪除:
下面例子的basename函数灵感于Unix shell的同名工具。在我们实现的版本中basename(s)将看起来像是系统路径的前缀删除,同时将看似文件类型的后缀名部分删除:
```Go
fmt.Println(basename("a/b/c.go")) // "c"
@ -16,7 +16,7 @@ fmt.Println(basename("c.d.go")) // "c.d"
fmt.Println(basename("abc")) // "abc"
```
第一個版本併沒有使用任何庫,全部手工硬編碼實現
第一个版本并没有使用任何库,全部手工硬编码实现
<u><i>gopl.io/ch3/basename1</i></u>
```Go
@ -41,7 +41,7 @@ func basename(s string) string {
}
```
簡化個版本使用了strings.LastIndex庫函數
简化个版本使用了strings.LastIndex库函数
<u><i>gopl.io/ch3/basename2</i></u>
```Go
@ -55,9 +55,9 @@ func basename(s string) string {
}
```
path和path/filepath包提供了關於文件路徑名更一般的函數操作。使用斜槓分隔路徑可以在任何操作繫統上工作。斜槓本身不應該用於文件名但是在其他一些領域可能會用於文件名例如URL路徑組件。相比之下path/filepath包則使用操作繫統本身的路徑規則例如POSIX繫統使用/foo/bar而Microsoft Windows使用c:\foo\bar等。
path和path/filepath包提供了关于文件路径名更一般的函数操作。使用斜杠分隔路径可以在任何操作系统上工作。斜杠本身不应该用于文件名但是在其他一些领域可能会用于文件名例如URL路径组件。相比之下path/filepath包则使用操作系统本身的路径规则例如POSIX系统使用/foo/bar而Microsoft Windows使用c:\foo\bar等。
讓我們繼續另一個字符串的例子。函數的功能是將一個表示整值的字符串每隔三個字符插入一個逗號分隔符例如“12345”處理後成爲“12,345”。這個版本隻適用於整數類型支持浮點數類型的支持留作練習
让我们继续另一个字符串的例子。函数的功能是将一个表示整值的字符串每隔三个字符插入一个逗号分隔符例如“12345”处理后成为“12,345”。这个版本只适用于整数类型支持浮点数类型的支持留作练习
<u><i>gopl.io/ch3/comma</i></u>
```Go
@ -71,11 +71,11 @@ func comma(s string) string {
}
```
輸入comma函數的參數是一個字符串。如果輸入字符串的長度小於或等於3的話則不需要插入逗分隔符。否則comma函數將在最後三個字符前位置將字符串切割爲兩個兩個子串併插入逗號分隔符然後通過遞歸調用自身來出前面的子串。
输入comma函数的参数是一个字符串。如果输入字符串的长度小于或等于3的话则不需要插入逗分隔符。否则comma函数将在最后三个字符前位置将字符串切割为两个两个子串并插入逗号分隔符然后通过递归调用自身来出前面的子串。
個字符串是包含的隻讀字節數組一旦創建是不可變的。相比之下一個字節slice的元素則可以自由地脩改。
个字符串是包含的只读字节数组一旦创建是不可变的。相比之下一个字节slice的元素则可以自由地修改。
字符串和字節slice之間可以相互轉換
字符串和字节slice之间可以相互转换
```Go
s := "abc"
@ -83,9 +83,9 @@ b := []byte(s)
s2 := string(b)
```
從概念上講,一個[]byte(s)轉換是分配了一個新的字節數組用於保存字符串數據的拷貝然後引用這個底層的字節數組。編譯器的優化可以避免在一些場景下分配和複製字符串數據但總的來説需要確保在變量b被脩改的情況下原始的s字符串也不會改變。將一個字節slice轉到字符串的string(b)操作則是構造一個字符串拷貝以確保s2字符串是隻讀的。
从概念上讲,一个[]byte(s)转换是分配了一个新的字节数组用于保存字符串数据的拷贝然后引用这个底层的字节数组。编译器的优化可以避免在一些场景下分配和复制字符串数据但总的来说需要确保在变量b被修改的情况下原始的s字符串也不会改变。将一个字节slice转到字符串的string(b)操作则是构造一个字符串拷贝以确保s2字符串是只读的。
爲了避免轉換中不必要的內存分配bytes包和strings同時提供了許多實用函數。下面是strings包中的六個函數
为了避免转换中不必要的内存分配bytes包和strings同时提供了许多实用函数。下面是strings包中的六个函数
```Go
func Contains(s, substr string) bool
@ -96,7 +96,7 @@ func Index(s, sep string) int
func Join(a []string, sep string) string
```
bytes包中也對應的六個函數
bytes包中也对应的六个函数
```Go
func Contains(b, subslice []byte) bool
@ -107,9 +107,9 @@ func Index(s, sep []byte) int
func Join(s [][]byte, sep []byte) []byte
```
們之間唯一的區别是字符串類型參數被替換成了字節slice類型的參數
们之间唯一的区别是字符串类型参数被替换成了字节slice类型的参数
bytes包還提供了Buffer類型用於字節slice的緩存。一個Buffer開始是空的但是隨着string、byte或[]byte等類型數據的寫入可以動態增長一個bytes.Buffer變量併不需要處理化因爲零值也是有效的:
bytes包还提供了Buffer类型用于字节slice的缓存。一个Buffer开始是空的但是随着string、byte或[]byte等类型数据的写入可以动态增长一个bytes.Buffer变量并不需要处理化因为零值也是有效的:
<u><i>gopl.io/ch3/printints</i></u>
```Go
@ -132,12 +132,12 @@ func main() {
}
```
當向bytes.Buffer添加任意字符的UTF8編碼時最好使用bytes.Buffer的WriteRune方法但是WriteByte方法對於寫入類似'['和']'等ASCII字符則會更加有效。
当向bytes.Buffer添加任意字符的UTF8编码时最好使用bytes.Buffer的WriteRune方法但是WriteByte方法对于写入类似'['和']'等ASCII字符则会更加有效。
bytes.Buffer類型有着很多實用的功能我們在第七章討論接口時將會涉及到我們將看看如何將它用作一個I/O的輸入和輸出對象例如當做Fprintf的io.Writer輸出對象或者當作io.Reader類型的輸入源對象。
bytes.Buffer类型有着很多实用的功能我们在第七章讨论接口时将会涉及到我们将看看如何将它用作一个I/O的输入和输出对象例如当做Fprintf的io.Writer输出对象或者当作io.Reader类型的输入源对象。
**練習 3.10** 編寫一個非遞歸版本的comma函數使用bytes.Buffer代替字符串鏈接操作。
**练习 3.10** 编写一个非递归版本的comma函数使用bytes.Buffer代替字符串链接操作。
**練習 3.11** 完善comma函數以支持浮點數處理和一個可選的正負號的處理。
**练习 3.11** 完善comma函数以支持浮点数处理和一个可选的正负号的处理。
**練習 3.12** 編寫一個函數,判斷兩個字符串是否是是相互打亂的,也就是説它們有着相同的字符,但是對應不同的順序。
**练习 3.12** 编写一个函数,判断两个字符串是否是是相互打乱的,也就是说它们有着相同的字符,但是对应不同的顺序。

View File

@ -1,8 +1,8 @@
### 3.5.5. 字符串和數字的轉換
### 3.5.5. 字符串和数字的转换
除了字符串、字符、字節之間的轉換字符串和數值之間的轉換也比較常見。由strconv包提供這類轉換功能。
除了字符串、字符、字节之间的转换字符串和数值之间的转换也比较常见。由strconv包提供这类转换功能。
將一個整數轉爲字符串一種方法是用fmt.Sprintf返迴一個格式化的字符串另一個方法是用strconv.Itoa(“整數到ASCII”)
将一个整数转为字符串一种方法是用fmt.Sprintf返回一个格式化的字符串另一个方法是用strconv.Itoa(“整数到ASCII”)
```Go
x := 123
@ -10,28 +10,28 @@ y := fmt.Sprintf("%d", x)
fmt.Println(y, strconv.Itoa(x)) // "123 123"
```
FormatInt和FormatUint函數可以用不同的進製來格式化數字:
FormatInt和FormatUint函数可以用不同的进制来格式化数字:
```Go
fmt.Println(strconv.FormatInt(int64(x), 2)) // "1111011"
```
fmt.Printf函數的%b、%d、%o和%x等參數提供功能往往比strconv包的Format函數方便很多特别是在需要包含附加額外信息的時候:
fmt.Printf函数的%b、%d、%o和%x等参数提供功能往往比strconv包的Format函数方便很多特别是在需要包含附加额外信息的时候:
```Go
s := fmt.Sprintf("x=%b", x) // "x=1111011"
```
如果要將一個字符串解析爲整數可以使用strconv包的Atoi或ParseInt函數還有用於解析無符號整數的ParseUint函數
如果要将一个字符串解析为整数可以使用strconv包的Atoi或ParseInt函数还有用于解析无符号整数的ParseUint函数
```Go
x, err := strconv.Atoi("123") // x is an int
y, err := strconv.ParseInt("123", 10, 64) // base 10, up to 64 bits
```
ParseInt函數的第三個參數是用於指定整型數的大小例如16表示int160則表示int。在任何情況下返迴的結果y總是int64類型你可以通過強製類型轉換將它轉爲更小的整數類型。
ParseInt函数的第三个参数是用于指定整型数的大小例如16表示int160则表示int。在任何情况下返回的结果y总是int64类型你可以通过强制类型转换将它转为更小的整数类型。
時候也會使用fmt.Scanf來解析輸入的字符串和數字特别是當字符串和數字混合在一行的時候它可以靈活處理不完整或不規則的輸入。
时候也会使用fmt.Scanf来解析输入的字符串和数字特别是当字符串和数字混合在一行的时候它可以灵活处理不完整或不规则的输入。

View File

@ -1,8 +1,8 @@
## 3.5. 字符串
個字符串是一個不可改變的字節序列。字符串可以包含任意的數據包括byte值0但是通常是用來包含人類可讀的文本。文本字符串通常被解釋爲采用UTF8編碼的Unicode碼點rune序列我們稍後會詳細討論這個問題
个字符串是一个不可改变的字节序列。字符串可以包含任意的数据包括byte值0但是通常是用来包含人类可读的文本。文本字符串通常被解释为采用UTF8编码的Unicode码点rune序列我们稍后会详细讨论这个问题
內置的len函數可以返迴一個字符串中的字節數目不是rune字符數目索引操作s[i]返迴第i個字節的字節值i必須滿足0 ≤ i< len(s)
内置的len函数可以返回一个字符串中的字节数目不是rune字符数目索引操作s[i]返回第i个字节的字节值i必须满足0 ≤ i< len(s)
```Go
s := "hello, world"
@ -10,23 +10,23 @@ fmt.Println(len(s)) // "12"
fmt.Println(s[0], s[7]) // "104 119" ('h' and 'w')
```
如果試圖訪問超出字符串索引范圍的字節將會導致panic異常:
如果试图访问超出字符串索引范围的字节将会导致panic异常:
```Go
c := s[len(s)] // panic: index out of range
```
第i個字節併不一定是字符串的第i個字符因爲對於非ASCII字符的UTF8編碼會要兩個或多個字節。我們先簡單説下字符的工作方式。
第i个字节并不一定是字符串的第i个字符因为对于非ASCII字符的UTF8编码会要两个或多个字节。我们先简单说下字符的工作方式。
子字符串操作s[i:j]基於原始的s字符串的第i個字節開始到第j個字節併不包含j本身生成一個新字符串。生成的新字符串將包含j-i個字節
子字符串操作s[i:j]基于原始的s字符串的第i个字节开始到第j个字节并不包含j本身生成一个新字符串。生成的新字符串将包含j-i个字节
```Go
fmt.Println(s[0:5]) // "hello"
```
如果索引超出字符串范圍或者j小於i的話將導致panic異常。
如果索引超出字符串范围或者j小于i的话将导致panic异常。
不管i還是j都可能被忽略當它們被忽略時將采用0作爲開始位置采用len(s)作爲結束的位置。
不管i还是j都可能被忽略当它们被忽略时将采用0作为开始位置采用len(s)作为结束的位置。
```Go
fmt.Println(s[:5]) // "hello"
@ -34,15 +34,15 @@ fmt.Println(s[7:]) // "world"
fmt.Println(s[:]) // "hello, world"
```
其中+操作符將兩個字符串鏈接構造一個新字符串:
其中+操作符将两个字符串链接构造一个新字符串:
```Go
fmt.Println("goodbye" + s[5:]) // "goodbye, world"
```
字符串可以用==和<進行比較;比較通過逐個字節比較完成的,因此比較的結果是字符串自然編碼的順序。
字符串可以用==和<进行比较;比较通过逐个字节比较完成的,因此比较的结果是字符串自然编码的顺序。
字符串的值是不可變的:一個字符串包含的字節序列永遠不會被改變,當然我們也可以給一個字符串變量分配一個新字符串值。可以像下面這樣將一個字符串追加到另一個字符串:
字符串的值是不可变的:一个字符串包含的字节序列永远不会被改变,当然我们也可以给一个字符串变量分配一个新字符串值。可以像下面这样将一个字符串追加到另一个字符串:
```Go
s := "left foot"
@ -50,20 +50,20 @@ t := s
s += ", right foot"
```
這併不會導致原始的字符串值被改變但是變量s將因爲+=語句持有一個新的字符串值但是t依然是包含原先的字符串值。
这并不会导致原始的字符串值被改变但是变量s将因为+=语句持有一个新的字符串值但是t依然是包含原先的字符串值。
```Go
fmt.Println(s) // "left foot, right foot"
fmt.Println(t) // "left foot"
```
爲字符串是不可脩改的,因此嚐試脩改字符串內部數據的操作也是被禁止的:
为字符串是不可修改的,因此尝试修改字符串内部数据的操作也是被禁止的:
```Go
s[0] = 'L' // compile error: cannot assign to s[0]
```
變性意味如果兩個字符串共享相同的底層數據的話也是安全的這使得複製任何長度的字符串代價是低廉的。同樣一個字符串s和對應的子字符串切片s[7:]的操作也可以安全地共享相同的內存,因此字符串切片操作代價也是低廉的。在這兩種情況下都沒有必要分配新的內存。 圖3.4演示了一個字符串和兩個字串共享相同的底層數據
变性意味如果两个字符串共享相同的底层数据的话也是安全的这使得复制任何长度的字符串代价是低廉的。同样一个字符串s和对应的子字符串切片s[7:]的操作也可以安全地共享相同的内存,因此字符串切片操作代价也是低廉的。在这两种情况下都没有必要分配新的内存。 图3.4演示了一个字符串和两个字串共享相同的底层数据
{% include "./ch3-05-1.md" %}

View File

@ -1,8 +1,8 @@
### 3.6.1. iota 常量生成器
常量聲明可以使用iota常量生成器初始化它用於生成一組以相似規則初始化的常量但是不用每行都寫一遍初始化表達式。在一個const聲明語句中在第一個聲明的常量所在的行iota將會被置爲0然後在每一個有常量聲明的行加一。
常量声明可以使用iota常量生成器初始化它用于生成一组以相似规则初始化的常量但是不用每行都写一遍初始化表达式。在一个const声明语句中在第一个声明的常量所在的行iota将会被置为0然后在每一个有常量声明的行加一。
下面是來自time包的例子它首先定義了一個Weekday命名類型然後爲一週的每天定義了一個常量從週日0開始。在其它編程語言中這種類型一般被稱爲枚舉類型。
下面是来自time包的例子它首先定义了一个Weekday命名类型然后为一周的每天定义了一个常量从周日0开始。在其它编程语言中这种类型一般被称为枚举类型。
```Go
type Weekday int
@ -18,9 +18,9 @@ const (
)
```
週一將對應0週一爲1如此等等。
周一将对应0周一为1如此等等。
們也可以在複雜的常量表達式中使用iota下面是來自net包的例子用於給一個無符號整數的最低5bit的每個bit指定一個名字:
们也可以在复杂的常量表达式中使用iota下面是来自net包的例子用于给一个无符号整数的最低5bit的每个bit指定一个名字:
```Go
type Flags uint
@ -34,7 +34,7 @@ const (
)
```
隨着iota的遞增每個常量對應表達式1 << iota2bit使bit
随着iota的递增每个常量对应表达式1 << iota2bit使bit
<u><i>gopl.io/ch3/netflag</i></u>
```Go
@ -54,7 +54,7 @@ unc main() {
}
```
下面是一個更複雜的例子每個常量都是1024的冪
下面是一个更复杂的例子每个常量都是1024的幂
```Go
const (
@ -70,6 +70,6 @@ const (
)
```
過iota常量生成規則也有其局限性。例如它併不能用於産生1000的冪KB、MB等因爲Go語言併沒有計算冪的運算符。
过iota常量生成规则也有其局限性。例如它并不能用于产生1000的幂KB、MB等因为Go语言并没有计算幂的运算符。
**練習 3.13** 編寫KB、MB的常量聲明然後擴展到YB。
**练习 3.13** 编写KB、MB的常量声明然后扩展到YB。

View File

@ -1,14 +1,14 @@
### 3.6.2. 無類型常量
### 3.6.2. 无类型常量
Go語言的常量有個不同尋常之處。雖然一個常量可以有任意有一個確定的基礎類型例如int或float64或者是類似time.Duration這樣命名的基礎類型但是許多常量併沒有一個明確的基礎類型。編譯器爲這些沒有明確的基礎類型的數字常量提供比基礎類型更高精度的算術運算你可以認爲至少有256bit的運算精度。這里有六種未明確類型的常量類型分别是無類型的布爾型、無類型的整數、無類型的字符、無類型的浮點數、無類型的複數、無類型的字符串。
Go语言的常量有个不同寻常之处。虽然一个常量可以有任意有一个确定的基础类型例如int或float64或者是类似time.Duration这样命名的基础类型但是许多常量并没有一个明确的基础类型。编译器为这些没有明确的基础类型的数字常量提供比基础类型更高精度的算术运算你可以认为至少有256bit的运算精度。这里有六种未明确类型的常量类型分别是无类型的布尔型、无类型的整数、无类型的字符、无类型的浮点数、无类型的复数、无类型的字符串。
過延遲明確常量的具體類型無類型的常量不僅可以提供更高的運算精度而且可以直接用於更多的表達式而不需要顯式的類型轉換。例如例子中的ZiB和YiB的值已經超出任何Go語言中整數類型能表達的范圍但是它們依然是合法的常量而且可以像下面常量表達式依然有效譯註YiB/ZiB是在編譯期計算出來的併且結果常量是1024是Go語言int變量能有效表示的):
过延迟明确常量的具体类型无类型的常量不仅可以提供更高的运算精度而且可以直接用于更多的表达式而不需要显式的类型转换。例如例子中的ZiB和YiB的值已经超出任何Go语言中整数类型能表达的范围但是它们依然是合法的常量而且可以像下面常量表达式依然有效译注YiB/ZiB是在编译期计算出来的并且结果常量是1024是Go语言int变量能有效表示的):
```Go
fmt.Println(YiB/ZiB) // "1024"
```
另一個例子math.Pi無類型的浮點數常量可以直接用於任意需要浮點數或複數的地方:
另一个例子math.Pi无类型的浮点数常量可以直接用于任意需要浮点数或复数的地方:
```Go
var x float32 = math.Pi
@ -16,7 +16,7 @@ var y float64 = math.Pi
var z complex128 = math.Pi
```
如果math.Pi被確定爲特定類型比如float64那麽結果精度可能會不一樣同時對於需要float32或complex128類型值的地方則會強製需要一個明確的類型轉換
如果math.Pi被确定为特定类型比如float64那么结果精度可能会不一样同时对于需要float32或complex128类型值的地方则会强制需要一个明确的类型转换
```Go
const Pi64 float64 = math.Pi
@ -26,9 +26,9 @@ var y float64 = Pi64
var z complex128 = complex128(Pi64)
```
對於常量面值不同的寫法可能會對應不同的類型。例如0、0.0、0i和'\u0000'雖然有着相同的常量值但是它們分别對應無類型的整數、無類型的浮點數、無類型的複數和無類型的字符等不同的常量類型。同樣true和false也是無類型的布爾類型字符串面值常量是無類型的字符串類型。
对于常量面值不同的写法可能会对应不同的类型。例如0、0.0、0i和'\u0000'虽然有着相同的常量值但是它们分别对应无类型的整数、无类型的浮点数、无类型的复数和无类型的字符等不同的常量类型。同样true和false也是无类型的布尔类型字符串面值常量是无类型的字符串类型。
前面説過除法運算符/會根據操作數的類型生成對應類型的結果。因此,不同寫法的常量除法表達式可能對應不同的結果:
前面说过除法运算符/会根据操作数的类型生成对应类型的结果。因此,不同写法的常量除法表达式可能对应不同的结果:
```Go
var f float64 = 212
@ -37,7 +37,7 @@ fmt.Println(5 / 9 * (f - 32)) // "0"; 5/9 is an untyped integer, 0
fmt.Println(5.0 / 9.0 * (f - 32)) // "100"; 5.0/9.0 is an untyped float
```
隻有常量可以是無類型的。當一個無類型的常量被賦值給一個變量的時候,就像上面的第一行語句,或者是像其餘三個語句中右邊表達式中含有明確類型的值,無類型的常量將會被隱式轉換爲對應的類型,如果轉換合法的話
只有常量可以是无类型的。当一个无类型的常量被赋值给一个变量的时候,就像上面的第一行语句,或者是像其余三个语句中右边表达式中含有明确类型的值,无类型的常量将会被隐式转换为对应的类型,如果转换合法的话
```Go
var f float64 = 3 + 0i // untyped complex -> float64
@ -46,7 +46,7 @@ f = 1e123 // untyped floating-point -> float64
f = 'a' // untyped rune -> float64
```
上面的語句相當於:
上面的语句相当于:
```Go
var f float64 = float64(3 + 0i)
@ -55,7 +55,7 @@ f = float64(1e123)
f = float64('a')
```
無論是隱式或顯式轉換,將一種類型轉換爲另一種類型都要求目標可以表示原始值。對於浮點數和複數,可能會有舍入處理:
无论是隐式或显式转换,将一种类型转换为另一种类型都要求目标可以表示原始值。对于浮点数和复数,可能会有舍入处理:
```Go
const (
@ -69,7 +69,7 @@ const (
)
```
對於一個沒有顯式類型的變量聲明語法(包括短變量聲明語法),無類型的常量會被隱式轉爲默認的變量類型,就像下面的例子:
对于一个没有显式类型的变量声明语法(包括短变量声明语法),无类型的常量会被隐式转为默认的变量类型,就像下面的例子:
```Go
i := 0 // untyped integer; implicit int(0)
@ -78,16 +78,16 @@ f := 0.0 // untyped floating-point; implicit float64(0.0)
c := 0i // untyped complex; implicit complex128(0i)
```
註意默認類型是規則的無類型的整數常量默認轉換爲int對應不確定的內存大小但是浮點數和複數常量則默認轉換爲float64和complex128。Go語言本身併沒有不確定內存大小的浮點數和複數類型而且如果不知道浮點數類型的話將很難寫出正確的數值算法。
注意默认类型是规则的无类型的整数常量默认转换为int对应不确定的内存大小但是浮点数和复数常量则默认转换为float64和complex128。Go语言本身并没有不确定内存大小的浮点数和复数类型而且如果不知道浮点数类型的话将很难写出正确的数值算法。
如果要給變量一個不同的類型,我們必須顯式地將無類型的常量轉化爲所需的類型,或給聲明的變量指定明確的類型,像下面例子這樣
如果要给变量一个不同的类型,我们必须显式地将无类型的常量转化为所需的类型,或给声明的变量指定明确的类型,像下面例子这样
```Go
var i = int8(0)
var i int8 = 0
```
當嚐試將這些無類型的常量轉爲一個接口值時見第7章這些默認類型將顯得尤爲重要因爲要靠它們明確接口對應的動態類型。
当尝试将这些无类型的常量转为一个接口值时见第7章这些默认类型将显得尤为重要因为要靠它们明确接口对应的动态类型。
```Go
fmt.Printf("%T\n", 0) // "int"
@ -96,7 +96,7 @@ fmt.Printf("%T\n", 0i) // "complex128"
fmt.Printf("%T\n", '\000') // "int32" (rune)
```
現在我們已經講述了Go語言中全部的基礎數據類型。下一步將演示如何用基礎數據類型組合成數組或結構體等複雜數據類型然後構建用於解決實際編程問題的數據結構這將是第四章的討論主題
现在我们已经讲述了Go语言中全部的基础数据类型。下一步将演示如何用基础数据类型组合成数组或结构体等复杂数据类型然后构建用于解决实际编程问题的数据结构这将是第四章的讨论主题

View File

@ -1,14 +1,14 @@
## 3.6. 常量
常量表達式的值在編譯期計算而不是在運行期。每種常量的潛在類型都是基礎類型boolean、string或數字。
常量表达式的值在编译期计算而不是在运行期。每种常量的潜在类型都是基础类型boolean、string或数字。
個常量的聲明語句定義了常量的名字,和變量的聲明語法類似,常量的值不可脩改,這樣可以防止在運行期被意外或惡意的脩改。例如,常量比變量更適合用於表達像π之類的數學常數,因爲它們的值不會發生變化:
个常量的声明语句定义了常量的名字,和变量的声明语法类似,常量的值不可修改,这样可以防止在运行期被意外或恶意的修改。例如,常量比变量更适合用于表达像π之类的数学常数,因为它们的值不会发生变化:
```Go
const pi = 3.14159 // approximately; math.Pi is a better approximation
```
變量聲明一樣,可以批量聲明多個常量;這比較適合聲明一組相關的常量:
变量声明一样,可以批量声明多个常量;这比较适合声明一组相关的常量:
```Go
const (
@ -17,11 +17,11 @@ const (
)
```
所有常量的運算都可以在編譯期完成,這樣可以減少運行時的工作,也方便其他編譯優化。當操作數是常量時,一些運行時的錯誤也可以在編譯時被發現,例如整數除零、字符串索引越界、任何導致無效浮點數的操作等。
所有常量的运算都可以在编译期完成,这样可以减少运行时的工作,也方便其他编译优化。当操作数是常量时,一些运行时的错误也可以在编译时被发现,例如整数除零、字符串索引越界、任何导致无效浮点数的操作等。
常量間的所有算術運算、邏輯運算和比較運算的結果也是常量,對常量的類型轉換操作或以下函數調用都是返迴常量結len、cap、real、imag、complex和unsafe.Sizeof§13.1)。
常量间的所有算术运算、逻辑运算和比较运算的结果也是常量,对常量的类型转换操作或以下函数调用都是返回常量结len、cap、real、imag、complex和unsafe.Sizeof§13.1)。
爲它們的值是在編譯期就確定的,因此常量可以是構成類型的一部分,例如用於指定數組類型的長度:
为它们的值是在编译期就确定的,因此常量可以是构成类型的一部分,例如用于指定数组类型的长度:
```Go
const IPv4Len = 4
@ -33,7 +33,7 @@ func parseIPv4(s string) IP {
}
```
個常量的聲明也可以包含一個類型和一個值但是如果沒有顯式指明類型那麽將從右邊的表達式推斷類型。在下面的代碼中time.Duration是一個命名類型底層類型是int64time.Minute是對應類型的常量。下面聲明的兩個常量都是time.Duration類型可以通過%T參數打印類型信息:
个常量的声明也可以包含一个类型和一个值但是如果没有显式指明类型那么将从右边的表达式推断类型。在下面的代码中time.Duration是一个命名类型底层类型是int64time.Minute是对应类型的常量。下面声明的两个常量都是time.Duration类型可以通过%T参数打印类型信息:
```Go
const noDelay time.Duration = 0
@ -43,7 +43,7 @@ fmt.Printf("%T %[1]v\n", timeout) // "time.Duration 5m0s"
fmt.Printf("%T %[1]v\n", time.Minute) // "time.Duration 1m0s"
```
如果是批量聲明的常量,除了第一個外其它的常量右邊的初始化表達式都可以省略,如果省略初始化表達式則表示使用前面常量的初始化表達式寫法,對應的常量類型也一樣的。例如:
如果是批量声明的常量,除了第一个外其它的常量右边的初始化表达式都可以省略,如果省略初始化表达式则表示使用前面常量的初始化表达式写法,对应的常量类型也一样的。例如:
```Go
const (
@ -56,7 +56,7 @@ const (
fmt.Println(a, b, c, d) // "1 1 2 2"
```
如果隻是簡單地複製右邊的常量表達式其實併沒有太實用的價值。但是它可以帶來其它的特性那就是iota常量生成器語法。
如果只是简单地复制右边的常量表达式其实并没有太实用的价值。但是它可以带来其它的特性那就是iota常量生成器语法。
{% include "./ch3-06-1.md" %}

View File

@ -1,5 +1,5 @@
# 第3章 基礎數據類
# 第3章 基础数据类
雖然從底層而言所有的數據都是由比特組成但計算機一般操作的是固定大小的數如整數、浮點數、比特數組、內存地址等。進一步將這些數組織在一起就可表達更多的對象例如數據包、像素點、詩歌甚至其他任何對象。Go語言提供了豐富的數據組織形式這依賴於Go語言內置的數據類型。這些內置的數據類型兼顧了硬件的特性和表達複雜數據結構的便捷性。
虽然从底层而言所有的数据都是由比特组成但计算机一般操作的是固定大小的数如整数、浮点数、比特数组、内存地址等。进一步将这些数组织在一起就可表达更多的对象例如数据包、像素点、诗歌甚至其他任何对象。Go语言提供了丰富的数据组织形式这依赖于Go语言内置的数据类型。这些内置的数据类型兼顾了硬件的特性和表达复杂数据结构的便捷性。
Go語言將數據類型分爲四類基礎類型、複合類型、引用類型和接口類型。本章介紹基礎類型包括數字、字符串和布爾型。複合數據類型——數組§4.1和結構體§4.2——是通過組合簡單類型來表達更加複雜的數據結構。引用類型包括指針§2.3.2、切片§4.2)字典§4.3、函數§5、通道§8雖然數據種類很多但它們都是對程序中一個變量或狀態的間接引用。這意味着對任一引用類型數據的脩改都會影響所有該引用的拷貝。我們將在第7章介紹接口類型。
Go语言将数据类型分为四类基础类型、复合类型、引用类型和接口类型。本章介绍基础类型包括数字、字符串和布尔型。复合数据类型——数组§4.1和结构体§4.2——是通过组合简单类型来表达更加复杂的数据结构。引用类型包括指针§2.3.2、切片§4.2)字典§4.3、函数§5、通道§8虽然数据种类很多但它们都是对程序中一个变量或状态的间接引用。这意味着对任一引用类型数据的修改都会影响所有该引用的拷贝。我们将在第7章介绍接口类型。

View File

@ -1,8 +1,8 @@
## 4.1. 數組
## 4.1. 数组
數組是一個由固定長度的特定類型元素組成的序列一個數組可以由零個或多個元素組成。因爲數組的長度是固定的因此在Go語言中很少直接使用數組。和數組對應的類型是Slice切片它是可以增長和收縮動態序列slice功能也更靈活但是要理解slice工作原理的話需要先理解數組
数组是一个由固定长度的特定类型元素组成的序列一个数组可以由零个或多个元素组成。因为数组的长度是固定的因此在Go语言中很少直接使用数组。和数组对应的类型是Slice切片它是可以增长和收缩动态序列slice功能也更灵活但是要理解slice工作原理的话需要先理解数组
數組的每個元素可以通過索引下標來訪問索引下標的范圍是從0開始到數組長度減1的位置。內置的len函數將返迴數組中元素的個數
数组的每个元素可以通过索引下标来访问索引下标的范围是从0开始到数组长度减1的位置。内置的len函数将返回数组中元素的个数
```Go
var a [3]int // array of 3 integers
@ -20,7 +20,7 @@ for _, v := range a {
}
```
認情況下數組的每個元素都被初始化爲元素類型對應的零值對於數字類型來説就是0。我們也可以使用數組字面值語法用一組值來初始化數組
认情况下数组的每个元素都被初始化为元素类型对应的零值对于数字类型来说就是0。我们也可以使用数组字面值语法用一组值来初始化数组
```Go
var q [3]int = [3]int{1, 2, 3}
@ -28,30 +28,30 @@ var r [3]int = [3]int{1, 2}
fmt.Println(r[2]) // "0"
```
數組字面值中,如果在數組的長度位置出現的是“...”省略號則表示數組的長度是根據初始化值的個數來計算。因此上面q數組的定義可以簡化爲
数组字面值中,如果在数组的长度位置出现的是“...”省略号则表示数组的长度是根据初始化值的个数来计算。因此上面q数组的定义可以简化为
```Go
q := [...]int{1, 2, 3}
fmt.Printf("%T\n", q) // "[3]int"
```
數組的長度是數組類型的一個組成部分,因此[3]int和[4]int是兩種不同的數組類型。數組的長度必須是常量表達式因爲數組的長度需要在編譯階段確定。
数组的长度是数组类型的一个组成部分,因此[3]int和[4]int是两种不同的数组类型。数组的长度必须是常量表达式因为数组的长度需要在编译阶段确定。
```Go
q := [3]int{1, 2, 3}
q = [4]int{1, 2, 3, 4} // compile error: cannot assign [4]int to [3]int
```
們將會發現數組、slice、map和結構體字面值的寫法都很相似。上面的形式是直接提供順序初始化值序列但是也可以指定一個索引和對應值列表的方式初始化就像下面這樣
们将会发现数组、slice、map和结构体字面值的写法都很相似。上面的形式是直接提供顺序初始化值序列但是也可以指定一个索引和对应值列表的方式初始化就像下面这样
```Go
type Currency int
const (
USD Currency = iota // 美元
EUR //
GBP // 英
RMB // 人民
EUR //
GBP // 英
RMB // 人民
)
symbol := [...]string{USD: "$", EUR: "€", GBP: "£", RMB: "¥"}
@ -59,15 +59,15 @@ symbol := [...]string{USD: "$", EUR: "€", GBP: "£", RMB: "¥"}
fmt.Println(RMB, symbol[RMB]) // "3 ¥"
```
這種形式的數組字面值形式中,初始化索引的順序是無關緊要的,而且沒用到的索引可以省略,和前面提到的規則一樣,未指定初始值的元素將用零值初始化。例如,
这种形式的数组字面值形式中,初始化索引的顺序是无关紧要的,而且没用到的索引可以省略,和前面提到的规则一样,未指定初始值的元素将用零值初始化。例如,
```Go
r := [...]int{99: -1}
```
義了一個含有100個元素的數組r最後一個元素被初始化爲-1其它元素都是用0初始化。
义了一个含有100个元素的数组r最后一个元素被初始化为-1其它元素都是用0初始化。
如果一個數組的元素類型是可以相互比較的,那麽數組類型也是可以相互比較的,這時候我們可以直接通過==比較運算符來比較兩個數組,隻有當兩個數組的所有元素都是相等的時候數組才是相等的。不相等比較運算符!=遵循同樣的規則
如果一个数组的元素类型是可以相互比较的,那么数组类型也是可以相互比较的,这时候我们可以直接通过==比较运算符来比较两个数组,只有当两个数组的所有元素都是相等的时候数组才是相等的。不相等比较运算符!=遵循同样的规则
```Go
a := [2]int{1, 2}
@ -78,7 +78,7 @@ d := [3]int{1, 2}
fmt.Println(a == d) // compile error: cannot compare [2]int == [3]int
```
爲一個眞實的例子crypto/sha256包的Sum256函數對一個任意的字節slice類型的數據生成一個對應的消息摘要。消息摘要有256bit大小因此對應[32]byte數組類型。如果兩個消息摘要是相同的那麽可以認爲兩個消息本身也是相同譯註理論上有HASH碼碰撞的情況但是實際應用可以基本忽略如果消息摘要不同那麽消息本身必然也是不同的。下面的例子用SHA256算法分别生成“x”和“X”兩個信息的摘要:
为一个真实的例子crypto/sha256包的Sum256函数对一个任意的字节slice类型的数据生成一个对应的消息摘要。消息摘要有256bit大小因此对应[32]byte数组类型。如果两个消息摘要是相同的那么可以认为两个消息本身也是相同译注理论上有HASH码碰撞的情况但是实际应用可以基本忽略如果消息摘要不同那么消息本身必然也是不同的。下面的例子用SHA256算法分别生成“x”和“X”两个信息的摘要:
<u><i>gopl.io/ch4/sha256</i></u>
```Go
@ -96,11 +96,11 @@ func main() {
}
```
上面例子中,兩個消息雖然隻有一個字符的差異但是生成的消息摘要則幾乎有一半的bit位是不相同的。需要註意Printf函數的%x副詞參數它用於指定以十六進製的格式打印數組或slice全部的元素%t副詞參數是用於打印布爾型數據%T副詞參數是用於顯示一個值對應的數據類型。
上面例子中,两个消息虽然只有一个字符的差异但是生成的消息摘要则几乎有一半的bit位是不相同的。需要注意Printf函数的%x副词参数它用于指定以十六进制的格式打印数组或slice全部的元素%t副词参数是用于打印布尔型数据%T副词参数是用于显示一个值对应的数据类型。
當調用一個函數的時候函數的每個調用參數將會被賦值給函數內部的參數變量所以函數參數變量接收的是一個複製的副本併不是原始調用的變量。因爲函數參數傳遞的機製導致傳遞大的數組類型將是低效的併且對數組參數的任何的脩改都是發生在複製的數組上併不能直接脩改調用時原始的數組變量。在這個方面Go語言對待數組的方式和其它很多編程語言不同其它編程語言可能會隱式地將數組作爲引用或指針對象傳入被調用的函數
当调用一个函数的时候函数的每个调用参数将会被赋值给函数内部的参数变量所以函数参数变量接收的是一个复制的副本并不是原始调用的变量。因为函数参数传递的机制导致传递大的数组类型将是低效的并且对数组参数的任何的修改都是发生在复制的数组上并不能直接修改调用时原始的数组变量。在这个方面Go语言对待数组的方式和其它很多编程语言不同其它编程语言可能会隐式地将数组作为引用或指针对象传入被调用的函数
當然,我們可以顯式地傳入一個數組指針,那樣的話函數通過指針對數組的任何脩改都可以直接反饋到調用者。下面的函數用於給[32]byte類型的數組清零:
当然,我们可以显式地传入一个数组指针,那样的话函数通过指针对数组的任何修改都可以直接反馈到调用者。下面的函数用于给[32]byte类型的数组清零:
```Go
func zero(ptr *[32]byte) {
@ -110,7 +110,7 @@ func zero(ptr *[32]byte) {
}
```
實數組字面值[32]byte{}就可以生成一個32字節的數組。而且每個數組的元素都是零值初始化也就是0。因此我們可以將上面的zero函數寫的更簡潔一點
实数组字面值[32]byte{}就可以生成一个32字节的数组。而且每个数组的元素都是零值初始化也就是0。因此我们可以将上面的zero函数写的更简洁一点
```Go
func zero(ptr *[32]byte) {
@ -118,8 +118,8 @@ func zero(ptr *[32]byte) {
}
```
雖然通過指針來傳遞數組參數是高效的而且也允許在函數內部脩改數組的值但是數組依然是殭化的類型因爲數組的類型包含了殭化的長度信息。上面的zero函數併不能接收指向[16]byte類型數組的指針而且也沒有任何添加或刪除數組元素的方法。由於這些原因除了像SHA256這類需要處理特定大小數組的特例外數組依然很少用作函數參數相反我們一般使用slice來替代數組
虽然通过指针来传递数组参数是高效的而且也允许在函数内部修改数组的值但是数组依然是僵化的类型因为数组的类型包含了僵化的长度信息。上面的zero函数并不能接收指向[16]byte类型数组的指针而且也没有任何添加或删除数组元素的方法。由于这些原因除了像SHA256这类需要处理特定大小数组的特例外数组依然很少用作函数参数相反我们一般使用slice来替代数组
**練習 4.1** 編寫一個函數計算兩個SHA256哈希碼中不同bit的數目。參考2.6.2節的PopCount函數。)
**练习 4.1** 编写一个函数计算两个SHA256哈希码中不同bit的数目。参考2.6.2节的PopCount函数。)
**練習 4.2** 編寫一個程序默認打印標準輸入的以SHA256哈希碼也可以通過命令行標準參數選擇SHA384或SHA512哈希算法。
**练习 4.2** 编写一个程序默认打印标准输入的以SHA256哈希码也可以通过命令行标准参数选择SHA384或SHA512哈希算法。

View File

@ -1,6 +1,6 @@
### 4.2.1. append函
### 4.2.1. append函
內置的append函數用於向slice追加元素
内置的append函数用于向slice追加元素
```Go
var runes []rune
@ -10,9 +10,9 @@ for _, r := range "Hello, 世界" {
fmt.Printf("%q\n", runes) // "['H' 'e' 'l' 'l' 'o' ',' ' ' '世' '界']"
```
在循環中使用append函數構建一個由九個rune字符構成的slice當然對應這個特殊的問題我們可以通過Go語言內置的[]rune("Hello, 世界")轉換操作完成。
在循环中使用append函数构建一个由九个rune字符构成的slice当然对应这个特殊的问题我们可以通过Go语言内置的[]rune("Hello, 世界")转换操作完成。
append函數對於理解slice底層是如何工作的非常重要所以讓我們仔細査看究竟是發生了什麽。下面是第一個版本的appendInt函數專門用於處理[]int類型的slice
append函数对于理解slice底层是如何工作的非常重要所以让我们仔细查看究竟是发生了什么。下面是第一个版本的appendInt函数专门用于处理[]int类型的slice
<u><i>gopl.io/ch4/append</i></u>
```Go
@ -37,13 +37,13 @@ func appendInt(x []int, y int) []int {
}
```
每次調用appendInt函數必須先檢測slice底層數組是否有足夠的容量來保存新添加的元素。如果有足夠空間的話直接擴展slice依然在原有的底層數組之上將新添加的y元素複製到新擴展的空間併返迴slice。因此輸入的x和輸出的z共享相同的底層數組
每次调用appendInt函数必须先检测slice底层数组是否有足够的容量来保存新添加的元素。如果有足够空间的话直接扩展slice依然在原有的底层数组之上将新添加的y元素复制到新扩展的空间并返回slice。因此输入的x和输出的z共享相同的底层数组
如果沒有足夠的增長空間的話appendInt函數則會先分配一個足夠大的slice用於保存新的結果先將輸入的x複製到新的空間然後添加y元素。結果z和輸入的x引用的將是不同的底層數組
如果没有足够的增长空间的话appendInt函数则会先分配一个足够大的slice用于保存新的结果先将输入的x复制到新的空间然后添加y元素。结果z和输入的x引用的将是不同的底层数组
雖然通過循環複製元素更直接不過內置的copy函數可以方便地將一個slice複製另一個相同類型的slice。copy函數的第一個參數是要複製的目標slice第二個參數是源slice目標和源的位置順序和`dst = src`賦值語句是一致的。兩個slice可以共享同一個底層數組甚至有重疊也沒有問題。copy函數將返迴成功複製的元素的個數我們這里沒有用到等於兩個slice中較小的長度所以我們不用擔心覆蓋會超出目標slice的范圍
虽然通过循环复制元素更直接不过内置的copy函数可以方便地将一个slice复制另一个相同类型的slice。copy函数的第一个参数是要复制的目标slice第二个参数是源slice目标和源的位置顺序和`dst = src`赋值语句是一致的。两个slice可以共享同一个底层数组甚至有重叠也没有问题。copy函数将返回成功复制的元素的个数我们这里没有用到等于两个slice中较小的长度所以我们不用担心覆盖会超出目标slice的范围
爲了提高內存使用效率新分配的數組一般略大於保存x和y所需要的最低大小。通過在每次擴展數組時直接將長度翻倍從而避免了多次內存分配也確保了添加單個元素操的平均時間是一個常數時間。這個程序演示了效果:
为了提高内存使用效率新分配的数组一般略大于保存x和y所需要的最低大小。通过在每次扩展数组时直接将长度翻倍从而避免了多次内存分配也确保了添加单个元素操的平均时间是一个常数时间。这个程序演示了效果:
```Go
func main() {
@ -56,7 +56,7 @@ func main() {
}
```
每一次容量的變化都會導致重新分配內存和copy操作
每一次容量的变化都会导致重新分配内存和copy操作
```
0 cap=1 [0]
@ -71,21 +71,21 @@ func main() {
9 cap=16 [0 1 2 3 4 5 6 7 8 9]
```
讓我們仔細査看i=3次的迭代。當時x包含了[0 1 2]三個元素但是容量是4因此可以簡單將新的元素添加到末尾不需要新的內存分配。然後新的y的長度和容量都是4併且和x引用着相同的底層數組如圖4.2所示。
让我们仔细查看i=3次的迭代。当时x包含了[0 1 2]三个元素但是容量是4因此可以简单将新的元素添加到末尾不需要新的内存分配。然后新的y的长度和容量都是4并且和x引用着相同的底层数组如图4.2所示。
![](../images/ch4-02.png)
在下一次迭代時i=4現在沒有新的空餘的空間了因此appendInt函數分配一個容量爲8的底層數組將x的4個元素[0 1 2 3]複製到新空間的開頭然後添加新的元素i新元素的值是4。新的y的長度是5容量是8後面有3個空閒的位置三次迭代都不需要分配新的空間。當前迭代中y和x是對應不同底層數組的view。這次操作如圖4.3所示。
在下一次迭代时i=4现在没有新的空余的空间了因此appendInt函数分配一个容量为8的底层数组将x的4个元素[0 1 2 3]复制到新空间的开头然后添加新的元素i新元素的值是4。新的y的长度是5容量是8后面有3个空闲的位置三次迭代都不需要分配新的空间。当前迭代中y和x是对应不同底层数组的view。这次操作如图4.3所示。
![](../images/ch4-03.png)
內置的append函數可能使用比appendInt更複雜的內存擴展策略。因此通常我們併不知道append調用是否導致了內存的重新分配因此我們也不能確認新的slice和原始的slice是否引用的是相同的底層數組空間。同樣我們不能確認在原先的slice上的操作是否會影響到新的slice。因此通常是將append返迴的結果直接賦值給輸入的slice變量:
内置的append函数可能使用比appendInt更复杂的内存扩展策略。因此通常我们并不知道append调用是否导致了内存的重新分配因此我们也不能确认新的slice和原始的slice是否引用的是相同的底层数组空间。同样我们不能确认在原先的slice上的操作是否会影响到新的slice。因此通常是将append返回的结果直接赋值给输入的slice变量:
```Go
runes = append(runes, r)
```
更新slice變量不僅對調用append函數是必要的實際上對應任何可能導致長度、容量或底層數組變化的操作都是必要的。要正確地使用slice需要記住盡管底層數組的元素是間接訪問的但是slice對應結構體本身的指針、長度和容量部分是直接訪問的。要更新這些信息需要像上面例子那樣一個顯式的賦值操作。從這個角度看slice併不是一個純粹的引用類型它實際上是一個類似下面結構體的聚合類型:
更新slice变量不仅对调用append函数是必要的实际上对应任何可能导致长度、容量或底层数组变化的操作都是必要的。要正确地使用slice需要记住尽管底层数组的元素是间接访问的但是slice对应结构体本身的指针、长度和容量部分是直接访问的。要更新这些信息需要像上面例子那样一个显式的赋值操作。从这个角度看slice并不是一个纯粹的引用类型它实际上是一个类似下面结构体的聚合类型:
```Go
type IntSlice struct {
@ -94,7 +94,7 @@ type IntSlice struct {
}
```
們的appendInt函數每次隻能向slice追加一個元素但是內置的append函數則可以追加多個元素甚至追加一個slice。
们的appendInt函数每次只能向slice追加一个元素但是内置的append函数则可以追加多个元素甚至追加一个slice。
```Go
var x []int
@ -105,7 +105,7 @@ x = append(x, x...) // append the slice x
fmt.Println(x) // "[1 2 3 4 5 6 1 2 3 4 5 6]"
```
過下面的小脩改我們可以可以達到append函數類似的功能。其中在appendInt函數參數中的最後的“...”省略號表示接收變長的參數爲slice。我們將在5.7節詳細解釋這個特性。
过下面的小修改我们可以可以达到append函数类似的功能。其中在appendInt函数参数中的最后的“...”省略号表示接收变长的参数为slice。我们将在5.7节详细解释这个特性。
```Go
func appendInt(x []int, y ...int) []int {
@ -117,4 +117,4 @@ func appendInt(x []int, y ...int) []int {
}
```
爲了避免重複,和前面相同的代碼併沒有顯示。
为了避免重复,和前面相同的代码并没有显示。

View File

@ -1,6 +1,6 @@
### 4.2.2. Slice存技巧
### 4.2.2. Slice存技巧
讓我們看看更多的例子比如镟轉slice、反轉slice或在slice原有內存空間脩改元素。給定一個字符串列表下面的nonempty函數將在原有slice內存空間之上返迴不包含空字符串的列表:
让我们看看更多的例子比如旋转slice、反转slice或在slice原有内存空间修改元素。给定一个字符串列表下面的nonempty函数将在原有slice内存空间之上返回不包含空字符串的列表:
<u><i>gopl.io/ch4/nonempty</i></u>
```Go
@ -23,7 +23,7 @@ func nonempty(strings []string) []string {
}
```
較微妙的地方是輸入的slice和輸出的slice共享一個底層數組。這可以避免分配另一個數組不過原來的數據將可能會被覆蓋正如下面兩個打印語句看到的那樣
较微妙的地方是输入的slice和输出的slice共享一个底层数组。这可以避免分配另一个数组不过原来的数据将可能会被覆盖正如下面两个打印语句看到的那样
```Go
data := []string{"one", "", "three"}
@ -31,9 +31,9 @@ fmt.Printf("%q\n", nonempty(data)) // `["one" "three"]`
fmt.Printf("%q\n", data) // `["one" "three" "three"]`
```
因此我們通常會這樣使用nonempty函數`data = nonempty(data)`。
因此我们通常会这样使用nonempty函数`data = nonempty(data)`。
nonempty函數也可以使用append函數實現
nonempty函数也可以使用append函数实现
```Go
func nonempty2(strings []string) []string {
@ -47,27 +47,27 @@ func nonempty2(strings []string) []string {
}
```
無論如何實現以這種方式重用一個slice一般都要求最多爲每個輸入值産生一個輸出值事實上很多這類算法都是用來過濾或合併序列中相鄰的元素。這種slice用法是比較複雜的技巧雖然使用到了slice的一些技巧但是對於某些場合是比較清晰和有效的。
无论如何实现以这种方式重用一个slice一般都要求最多为每个输入值产生一个输出值事实上很多这类算法都是用来过滤或合并序列中相邻的元素。这种slice用法是比较复杂的技巧虽然使用到了slice的一些技巧但是对于某些场合是比较清晰和有效的。
個slice可以用來模擬一個stack。最初給定的空slice對應一個空的stack然後可以使用append函數將新的值壓入stack
个slice可以用来模拟一个stack。最初给定的空slice对应一个空的stack然后可以使用append函数将新的值压入stack
```Go
stack = append(stack, v) // push v
```
stack的頂部位置對應slice的最後一個元素:
stack的顶部位置对应slice的最后一个元素:
```Go
top := stack[len(stack)-1] // top of stack
```
過收縮stack可以彈出棧頂的元素
过收缩stack可以弹出栈顶的元素
```Go
stack = stack[:len(stack)-1] // pop
```
刪除slice中間的某個元素併保存原有的元素順序可以通過內置的copy函數將後面的子slice向前依次移動一位完成:
删除slice中间的某个元素并保存原有的元素顺序可以通过内置的copy函数将后面的子slice向前依次移动一位完成:
```Go
func remove(slice []int, i int) []int {
@ -81,7 +81,7 @@ func main() {
}
```
如果刪除元素後不用保持原來順序的話,我們可以簡單的用最後一個元素覆蓋被刪除的元素:
如果删除元素后不用保持原来顺序的话,我们可以简单的用最后一个元素覆盖被删除的元素:
```Go
func remove(slice []int, i int) []int {
@ -95,12 +95,12 @@ func main() {
}
```
**練習 4.3** 重寫reverse函數使用數組指針代替slice。
**练习 4.3** 重写reverse函数使用数组指针代替slice。
**練習 4.4** 編寫一個rotate函數通過一次循環完成镟轉
**练习 4.4** 编写一个rotate函数通过一次循环完成旋转
**練習 4.5** 寫一個函數在原地完成消除[]string中相鄰重複的字符串的操作。
**练习 4.5** 写一个函数在原地完成消除[]string中相邻重复的字符串的操作。
**練習 4.6** 編寫一個函數原地將一個UTF-8編碼的[]byte類型的slice中相鄰的空格參考unicode.IsSpace替換成一個空格返迴
**练习 4.6** 编写一个函数原地将一个UTF-8编码的[]byte类型的slice中相邻的空格参考unicode.IsSpace替换成一个空格返回
**練習 4.7** 脩改reverse函數用於原地反轉UTF-8編碼的[]byte。是否可以不用分配額外的內存?
**练习 4.7** 修改reverse函数用于原地反转UTF-8编码的[]byte。是否可以不用分配额外的内存?

View File

@ -1,18 +1,18 @@
## 4.2. Slice
Slice切片代表變長的序列序列中每個元素都有相同的類型。一個slice類型一般寫作[]T其中T代表slice中元素的類型slice的語法和數組很像隻是沒有固定長度而已。
Slice切片代表变长的序列序列中每个元素都有相同的类型。一个slice类型一般写作[]T其中T代表slice中元素的类型slice的语法和数组很像只是没有固定长度而已。
數組和slice之間有着緊密的聯繫。一個slice是一個輕量級的數據結構提供了訪問數組子序列或者全部元素的功能而且slice的底層確實引用一個數組對象。一個slice由三個部分構成指針、長度和容量。指針指向第一個slice元素對應的底層數組元素的地址要註意的是slice的第一個元素併不一定就是數組的第一個元素。長度對應slice中元素的數目長度不能超過容量容量一般是從slice的開始位置到底層數據的結尾位置。內置的len和cap函數分别返迴slice的長度和容量。
数组和slice之间有着紧密的联系。一个slice是一个轻量级的数据结构提供了访问数组子序列或者全部元素的功能而且slice的底层确实引用一个数组对象。一个slice由三个部分构成指针、长度和容量。指针指向第一个slice元素对应的底层数组元素的地址要注意的是slice的第一个元素并不一定就是数组的第一个元素。长度对应slice中元素的数目长度不能超过容量容量一般是从slice的开始位置到底层数据的结尾位置。内置的len和cap函数分别返回slice的长度和容量。
個slice之間可以共享底層的數據併且引用的數組部分區間可能重疊。圖4.1顯示了表示一年中每個月份名字的字符串數組還有重疊引用了該數組的兩個slice。數組這樣定義
个slice之间可以共享底层的数据并且引用的数组部分区间可能重叠。图4.1显示了表示一年中每个月份名字的字符串数组还有重叠引用了该数组的两个slice。数组这样定义
```Go
months := [...]string{1: "January", /* ... */, 12: "December"}
```
因此一月份是months[1]十二月份是months[12]。通常,數組的第一個元素從索引0開始但是月份一般是從1開始的因此我們聲明數組時直接跳過第0個元素第0個元素會被自動初始化爲空字符串。
因此一月份是months[1]十二月份是months[12]。通常,数组的第一个元素从索引0开始但是月份一般是从1开始的因此我们声明数组时直接跳过第0个元素第0个元素会被自动初始化为空字符串。
slice的切片操作s[i:j]其中0 ≤ i≤ j≤ cap(s),用於創建一個新的slice引用s的從第i個元素開始到第j-1個元素的子序列。新的slice將隻有j-i個元素。如果i位置的索引被省略的話將使用0代替如果j位置的索引被省略的話將使用len(s)代替。因此months[1:13]切片操作將引用全部有效的月份和months[1:]操作等價months[:]切片操作則是引用整個數組。讓我們分别定義表示第二季度和北方夏天月份的slice它們有重疊部分:
slice的切片操作s[i:j]其中0 ≤ i≤ j≤ cap(s),用于创建一个新的slice引用s的从第i个元素开始到第j-1个元素的子序列。新的slice将只有j-i个元素。如果i位置的索引被省略的话将使用0代替如果j位置的索引被省略的话将使用len(s)代替。因此months[1:13]切片操作将引用全部有效的月份和months[1:]操作等价months[:]切片操作则是引用整个数组。让我们分别定义表示第二季度和北方夏天月份的slice它们有重叠部分:
![](../images/ch4-01.png)
@ -23,7 +23,7 @@ fmt.Println(Q2) // ["April" "May" "June"]
fmt.Println(summer) // ["June" "July" "August"]
```
兩個slice都包含了六月份下面的代碼是一個包含相同月份的測試性能較低):
两个slice都包含了六月份下面的代码是一个包含相同月份的测试性能较低):
```Go
for _, s := range summer {
@ -35,7 +35,7 @@ for _, s := range summer {
}
```
如果切片操作超出cap(s)的上限將導致一個panic異常但是超出len(s)則是意味着擴展了slice因爲新slice的長度會變大:
如果切片操作超出cap(s)的上限将导致一个panic异常但是超出len(s)则是意味着扩展了slice因为新slice的长度会变大:
```Go
fmt.Println(summer[:20]) // panic: out of range
@ -44,9 +44,9 @@ endlessSummer := summer[:5] // extend a slice (within capacity)
fmt.Println(endlessSummer) // "[June July August September October]"
```
另外,字符串的切片操作和[]byte字節類型切片的切片操作是類似的。它們都寫作x[m:n]併且都是返迴一個原始字節繫列的子序列底層都是共享之前的底層數組因此切片操作對應常量時間複雜度。x[m:n]切片操作對於字符串則生成一個新字符串如果x是[]byte的話則生成一個新的[]byte。
另外,字符串的切片操作和[]byte字节类型切片的切片操作是类似的。它们都写作x[m:n]并且都是返回一个原始字节系列的子序列底层都是共享之前的底层数组因此切片操作对应常量时间复杂度。x[m:n]切片操作对于字符串则生成一个新字符串如果x是[]byte的话则生成一个新的[]byte。
爲slice值包含指向第一個slice元素的指針因此向函數傳遞slice將允許在函數內部脩改底層數組的元素。換句話説複製一個slice隻是對底層的數組創建了一個新的slice别名§2.3.2。下面的reverse函數在原內存空間將[]int類型的slice反轉而且它可以用於任意長度的slice。
为slice值包含指向第一个slice元素的指针因此向函数传递slice将允许在函数内部修改底层数组的元素。换句话说复制一个slice只是对底层的数组创建了一个新的slice别名§2.3.2。下面的reverse函数在原内存空间将[]int类型的slice反转而且它可以用于任意长度的slice。
<u><i>gopl.io/ch4/rev</i></u>
```Go
@ -58,7 +58,7 @@ func reverse(s []int) {
}
```
這里我們反轉數組的應用:
这里我们反转数组的应用:
```Go
a := [...]int{0, 1, 2, 3, 4, 5}
@ -66,7 +66,7 @@ reverse(a[:])
fmt.Println(a) // "[5 4 3 2 1 0]"
```
種將slice元素循環向左镟轉n個元素的方法是三次調用reverse反轉函數第一次是反轉開頭的n個元素然後是反轉剩下的元素最後是反轉整個slice的元素。如果是向右循環镟轉則將第三個函數調用移到第一個調用位置就可以了。)
种将slice元素循环向左旋转n个元素的方法是三次调用reverse反转函数第一次是反转开头的n个元素然后是反转剩下的元素最后是反转整个slice的元素。如果是向右循环旋转则将第三个函数调用移到第一个调用位置就可以了。)
```Go
s := []int{0, 1, 2, 3, 4, 5}
@ -77,9 +77,9 @@ reverse(s)
fmt.Println(s) // "[2 3 4 5 0 1]"
```
註意的是slice類型的變量s和數組類型的變量a的初始化語法的差異。slice和數組的字面值語法很類似它們都是用花括弧包含一繫列的初始化元素但是對於slice併沒有指明序列的長度。這會隱式地創建一個合適大小的數組然後slice的指針指向底層的數組。就像數組字面值一樣slice的字面值也可以按順序指定初始化值序列或者是通過索引和元素值指定或者的兩種風格的混合語法初始化。
注意的是slice类型的变量s和数组类型的变量a的初始化语法的差异。slice和数组的字面值语法很类似它们都是用花括弧包含一系列的初始化元素但是对于slice并没有指明序列的长度。这会隐式地创建一个合适大小的数组然后slice的指针指向底层的数组。就像数组字面值一样slice的字面值也可以按顺序指定初始化值序列或者是通过索引和元素值指定或者的两种风格的混合语法初始化。
數組不同的是slice之間不能比較因此我們不能使用==操作符來判斷兩個slice是否含有全部相等元素。不過標準庫提供了高度優化的bytes.Equal函數來判斷兩個字節型slice是否相等[]byte但是對於其他類型的slice我們必須自己展開每個元素進行比較
数组不同的是slice之间不能比较因此我们不能使用==操作符来判断两个slice是否含有全部相等元素。不过标准库提供了高度优化的bytes.Equal函数来判断两个字节型slice是否相等[]byte但是对于其他类型的slice我们必须自己展开每个元素进行比较
```Go
func equal(x, y []string) bool {
@ -95,17 +95,17 @@ func equal(x, y []string) bool {
}
```
上面關於兩個slice的深度相等測試運行的時間併不比支持==操作的數組或字符串更多但是爲何slice不直接支持比較運算符呢這方面有兩個原因。第一個原因一個slice的元素是間接引用的一個slice甚至可以包含自身。雖然有很多辦法處理這種情形但是沒有一個是簡單有效的。
上面关于两个slice的深度相等测试运行的时间并不比支持==操作的数组或字符串更多但是为何slice不直接支持比较运算符呢这方面有两个原因。第一个原因一个slice的元素是间接引用的一个slice甚至可以包含自身。虽然有很多办法处理这种情形但是没有一个是简单有效的。
第二個原因因爲slice的元素是間接引用的一個固定值的slice在不同的時間可能包含不同的元素因爲底層數組的元素可能會被脩改。併且Go語言中map等哈希表之類的數據結構的key隻做簡單的淺拷貝它要求在整個聲明週期中相等的key必須對相同的元素。對於像指針或chan之類的引用類型==相等測試可以判斷兩個是否是引用相同的對象。一個針對slice的淺相等測試的==操作符可能是有一定用處的也能臨時解決map類型的key問題但是slice和數組不同的相等測試行爲會讓人睏惑。因此安全的做法是直接禁止slice之間的比較操作。
第二个原因因为slice的元素是间接引用的一个固定值的slice在不同的时间可能包含不同的元素因为底层数组的元素可能会被修改。并且Go语言中map等哈希表之类的数据结构的key只做简单的浅拷贝它要求在整个声明周期中相等的key必须对相同的元素。对于像指针或chan之类的引用类型==相等测试可以判断两个是否是引用相同的对象。一个针对slice的浅相等测试的==操作符可能是有一定用处的也能临时解决map类型的key问题但是slice和数组不同的相等测试行为会让人困惑。因此安全的做法是直接禁止slice之间的比较操作。
slice唯一合法的比較操作是和nil比較,例如:
slice唯一合法的比较操作是和nil比较,例如:
```Go
if summer == nil { /* ... */ }
```
個零值的slice等於nil。一個nil值的slice併沒有底層數組。一個nil值的slice的長度和容量都是0但是也有非nil值的slice的長度和容量也是0的例如[]int{}或make([]int, 3)[3:]。與任意類型的nil值一樣我們可以用[]int(nil)類型轉換表達式來生成一個對應類型slice的nil值。
个零值的slice等于nil。一个nil值的slice并没有底层数组。一个nil值的slice的长度和容量都是0但是也有非nil值的slice的长度和容量也是0的例如[]int{}或make([]int, 3)[3:]。与任意类型的nil值一样我们可以用[]int(nil)类型转换表达式来生成一个对应类型slice的nil值。
```Go
var s []int // len(s) == 0, s == nil
@ -114,16 +114,16 @@ s = []int(nil) // len(s) == 0, s == nil
s = []int{} // len(s) == 0, s != nil
```
如果你需要測試一個slice是否是空的使用len(s) == 0來判斷而不應該用s == nil來判斷。除了和nil相等比較外一個nil值的slice的行爲和其它任意0長度的slice一樣例如reverse(nil)也是安全的。除了文檔已經明確説明的地方所有的Go語言函數應該以相同的方式對待nil值的slice和0長度的slice。
如果你需要测试一个slice是否是空的使用len(s) == 0来判断而不应该用s == nil来判断。除了和nil相等比较外一个nil值的slice的行为和其它任意0长度的slice一样例如reverse(nil)也是安全的。除了文档已经明确说明的地方所有的Go语言函数应该以相同的方式对待nil值的slice和0长度的slice。
內置的make函數創建一個指定元素類型、長度和容量的slice。容量部分可以省略在這種情況下容量將等於長度。
内置的make函数创建一个指定元素类型、长度和容量的slice。容量部分可以省略在这种情况下容量将等于长度。
```Go
make([]T, len)
make([]T, len, cap) // same as make([]T, cap)[:len]
```
在底make創建了一個匿名的數組變量然後返迴一個slice隻有通過返迴的slice才能引用底層匿名的數組變量。在第一種語句中slice是整個數組的view。在第二個語句中slice隻引用了底層數組的前len個元素但是容量將包含整個的數組。額外的元素是留給未來的增長用的。
在底make创建了一个匿名的数组变量然后返回一个slice只有通过返回的slice才能引用底层匿名的数组变量。在第一种语句中slice是整个数组的view。在第二个语句中slice只引用了底层数组的前len个元素但是容量将包含整个的数组。额外的元素是留给未来的增长用的。
{% include "./ch4-02-1.md" %}

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