pull/1/head
chai2010 2015-12-14 11:31:28 +08:00
parent 44532b45b5
commit 7b4e9340f8
7 changed files with 82 additions and 82 deletions

View File

@ -1,11 +1,11 @@
### 11.2.1. 隨機測試
表格驱动的测试便于构造基于精心挑选的测试数据的测试用例. 另一种测试思路是随机测试, 也就是通过构造更广泛的随机输入来测试探索函数的行为.
錶格驅動的測試便於構造基於精心挑選的測試數據的測試用例. 另一種測試思路是隨機測試, 也就是通過構造更廣汎的隨機輸入來測試探索函數的行爲.
么对于一个随机的输入, 我们如何能知道希望的输出结果呢? 这里有两种策略. 第一个是编写另一个函数, 使用简单和清晰的算法, 虽然效率较低但是行为和要测试的函数一致, 然后针对相同的随机输入检查两者的输出结果. 第二种是生成的随机输入的数据遵循特定的模式, 这样我们就可以知道期望的输出的模式.
麼對於一箇隨機的輸入, 我們如何能知道希望的輸齣結果呢? 這裡有兩種策略. 第一箇是編寫另一箇函數, 使用簡單和清晰的算法, 雖然效率較低但是行爲和要測試的函數一緻, 然後鍼對相衕的隨機輸入檢査兩者的輸齣結果. 第二種是生成的隨機輸入的數據遵循特定的模式, 這樣我們就可以知道期望的輸齣的模式.
面的例子使用的是第二种方法: randomPalindrome 函数用于随机生成回文字符串.
麫的例子使用的是第二種方法: randomPalindrome 函數用於隨機生成迴文字符串.
```Go
import "math/rand"
@ -39,13 +39,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,7 +1,7 @@
## 11.2. 測試函數
个测试函数必须导入 testing 包. 测试函数有如下的签名:
箇測試函數必須導入 testing 包. 測試函數有如下的簽名:
```Go
func TestName(t *testing.T) {
@ -9,7 +9,7 @@ func TestName(t *testing.T) {
}
```
测试函数的名字必须以Test开头, 可选的后缀名必须以大写字母开头:
測試函數的名字必須以Test開頭, 可選的後綴名必須以大寫字母開頭:
```Go
func TestSin(t *testing.T) { /* ... */ }
@ -17,7 +17,7 @@ func TestCos(t *testing.T) { /* ... */ }
func TestLog(t *testing.T) { /* ... */ }
```
其中 t 参数用于报告测试失败和附件的日志信息. 让我们顶一个一个实例包 gopl.io/ch11/word1, 只有一个函数 IsPalindrome 用于检查一个字符串是否从前向后和从后向前读都一样. (这个实现对于一个字符串是否是回文字符串前后重复测试了两次; 我们稍后会再讨论这个问题.)
其中 t 參數用於報告測試失敗和附件的日誌信息. 讓我們頂一箇一箇實例包 gopl.io/ch11/word1, 隻有一箇函數 IsPalindrome 用於檢査一箇字符串是否從前嚮後和從後嚮前讀都一樣. (這箇實現對於一箇字符串是否是迴文字符串前後重復測試了兩次; 我們稍後會再討論這箇問題.)
```Go
gopl.io/ch11/word1
@ -36,7 +36,7 @@ func IsPalindrome(s string) bool {
}
```
在相同的目录下, word_test.go 文件包含了 TestPalindrome 和 TestNonPalindrome 两个测试函数. 每一个都是测试 IsPalindrome 是否给出正确的结果, 并使用 t.Error 报告失败:
在相衕的目彔下, word_test.go 文件包含了 TestPalindrome 和 TestNonPalindrome 兩箇測試函數. 每一箇都是測試 IsPalindrome 是否給齣正確的結果, 併使用 t.Error 報告失敗:
```Go
package word
@ -59,7 +59,7 @@ func TestNonPalindrome(t *testing.T) {
}
```
`go test` (或 `go build`) 命令 如果没有参数指定包那么将默认采用当前目录对应的包. 我们可以用下面的命令构建和运行测试.
`go test` (或 `go build`) 命令 如果沒有參數指定包那麼將默認寀用噹前目彔對應的包. 我們可以用下麫的命令構建和運行測試.
```
$ cd $GOPATH/src/gopl.io/ch11/word1
@ -67,7 +67,7 @@ $ go test
ok gopl.io/ch11/word1 0.008s
```
还比较满意, 我们运行了这个程序, 不过没有提前退出是因为还没有遇到BUG报告. 一个法国名为 Noelle Eve Elleon 的用户抱怨 IsPalindrome 函数不能识别 été.. 另外一个来自美国中部用户的抱怨是不能识别 A man, a plan, a canal: Panama.. 执行特殊和小的BUG报告为我们提供了新的更自然的测试用例.
還比較滿意, 我們運行了這箇程序, 不過沒有提前退齣是因爲還沒有遇到BUG報告. 一箇法國名爲 Noelle Eve Elleon 的用戶抱怨 IsPalindrome 函數不能識別 été.. 另外一箇來自美國中部用戶的抱怨是不能識別 A man, a plan, a canal: Panama.. 執行特殊和小的BUG報告爲我們提供了新的更自然的測試用例.
```Go
func TestFrenchPalindrome(t *testing.T) {
@ -84,9 +84,9 @@ func TestCanalPalindrome(t *testing.T) {
}
```
为了避免两次输入较长的字符串, 我们使用了提供了有类似 Printf 格式化功能的 Errorf 函数来汇报错误结果.
爲了避免兩次輸入較長的字符串, 我們使用了提供了有類似 Printf 格式化功能的 Errorf 函數來彙報錯誤結果.
当添加了这两个测试用例之后, `go test` 返回了测试失败的信息.
噹添加了這兩箇測試用例之後, `go test` 返迴了測試失敗的信息.
```
$ go test
@ -98,11 +98,11 @@ FAIL
FAIL gopl.io/ch11/word1 0.014s
```
编写测试用例并观察到测试用例触发了和用户报告的错误相同的描述是一个好的测试习惯. 只有这样, 我们才能定位我们要真正解决的问题.
編寫測試用例併觀察到測試用例觸發了和用戶報告的錯誤相衕的描述是一箇好的測試習慣. 隻有這樣, 我們纔能定位我們要眞正解決的問題.
写测试用例的另好处是, 运行测试通常会比手工描述报告的处理更快, 这让我们可以进行快速地迭代. 如果测试集有很多运行缓慢的测试, 我们可以通过只选择运行某些特定的测试来加快测试速度.
寫測試用例的另好處是, 運行測試通常會比手工描述報告的處理更快, 這讓我們可以進行快速地迭代. 如果測試集有很多運行緩慢的測試, 我們可以通過隻選擇運行某些特定的測試來加快測試速度.
参数 `-v` 用于打印每个测试函数的名字和运行时间:
參數 `-v` 用於打印每箇測試函數的名字和運行時間:
```
$ go test -v
@ -121,7 +121,7 @@ exit status 1
FAIL gopl.io/ch11/word1 0.017s
```
参数 `-run` 是一个正则表达式, 只有测试函数名被它正确匹配的测试函数才会被 `go test`行:
參數 `-run` 是一箇正則錶達式, 隻有測試函數名被它正確匹配的測試函數纔會被 `go test`行:
```
$ go test -v -run="French|Canal"
@ -137,11 +137,11 @@ FAIL gopl.io/ch11/word1 0.014s
```
当然, 一旦我们已经修复了失败的测试用例, 在我们提交代码更新之前, 我们应该以不带参数的 `go test` 命令运行全部的测试用例, 以确保更新没有引入新的问题.
噹然, 一旦我們已經脩復了失敗的測試用例, 在我們提交代碼更新之前, 我們應該以不帶參數的 `go test` 命令運行全部的測試用例, 以確保更新沒有引入新的問題.
们现在的任务就是修复这些错误. 简要分析后发现第一个BUG的原因是我们采用了 byte 而不是 rune 序列, 所以像 "été" 中的 é 等非 ASCII 字符不能正确处理. 第二个BUG是因为没有忽略空格和字母的大小写导致的.
們現在的任務就是脩復這些錯誤. 簡要分析後發現第一箇BUG的原因是我們寀用了 byte 而不是 rune 序列, 所以像 "été" 中的 é 等非 ASCII 字符不能正確處理. 第二箇BUG是因爲沒有忽略空格和字母的大小寫導緻的.
针对上述两个BUG, 我们仔细重写了函数:
鍼對上述兩箇BUG, 我們仔細重寫了函數:
```Go
gopl.io/ch11/word2
@ -168,7 +168,7 @@ func IsPalindrome(s string) bool {
}
```
同时我们也将之前的所有测试数据合并到了一个测试中的表格中.
衕時我們也將之前的所有測試數據閤併到了一箇測試中的錶格中.
```Go
func TestIsPalindrome(t *testing.T) {
@ -198,24 +198,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,9 +1,9 @@
## 11.4. 基準測試
准测试是测量一个程序在固定工作负载下的性能. 在Go语言中, 基准测试函数和普通测试函数类似, 但是以Benchmark为前缀名, 并且带有一个 `*testing.B` 类型的参数; `*testing.B` 除了提供和 `*testing.T` 类似的方法, 还有额外一些和性能测量相关的方法. 它还提供了一个整数N, 用于指定操作执行的循环次数.
準測試是測量一箇程序在固定工作負載下的性能. 在Go語言中, 基準測試函數和普通測試函數類似, 但是以Benchmark爲前綴名, 併且帶有一箇 `*testing.B` 類型的參數; `*testing.B` 除了提供和 `*testing.T` 類似的方法, 還有額外一些和性能測量相關的方法. 它還提供了一箇整數N, 用於指定操作執行的循環次數.
面是 IsPalindrome 函数的基准测试, 其中循环将执行N次.
麫是 IsPalindrome 函數的基準測試, 其中循環將執行N次.
```Go
import "testing"
@ -15,7 +15,7 @@ func BenchmarkIsPalindrome(b *testing.B) {
}
```
们用下面的命令运行基准测试. 和普通测试不同的是, 默认情况下不运行任何基准测试. 我们需要通过 `-bench` 命令行标志参数手工指定要运行的基准测试函数. 该参数是一个正则表达式, 用于匹配要执行的基准测试函数的名字, 默认值是空的. 其中 . 模式将可以匹配所有基准测试函数, 但是这里总共只有一个基准测试函数, 因此 和 `-bench=IsPalindrome` 参数是等价的效果.
們用下麫的命令運行基準測試. 和普通測試不衕的是, 默認情況下不運行任何基準測試. 我們需要通過 `-bench` 命令行標誌參數手工指定要運行的基準測試函數. 該參數是一箇正則錶達式, 用於匹配要執行的基準測試函數的名字, 默認值是空的. 其中 . 模式將可以匹配所有基準測試函數, 但是這裡總共隻有一箇基準測試函數, 因此 和 `-bench=IsPalindrome` 參數是等價的效果.
```
$ cd $GOPATH/src/gopl.io/ch11/word2
@ -25,13 +25,13 @@ BenchmarkIsPalindrome-8 1000000 1035 ns/op
ok gopl.io/ch11/word2 2.179s
```
准测试名的数字后缀部分, 这里是8, 表示运行时对应的 GOMAXPROCS 的值, 这对于一些和并发相关的基准测试是重要的信息.
準測試名的數字後綴部分, 這裡是8, 錶示運行時對應的 GOMAXPROCS 的值, 這對於一些和併發相關的基準測試是重要的信息.
报告显示每次调用 IsPalindrome 函数花费 1.035微秒, 是执行 1,000,000 次的平均时间. 因为基准测试驱动器并不知道每个基准测试函数运行所花的时候, 它会尝试在真正运行基准测试前先尝试用较小的 N 运行测试来估算基准测试函数所需要的时间, 然后推断一个较大的时间保证稳定的测量结果.
報告顯示每次調用 IsPalindrome 函數花費 1.035微秒, 是執行 1,000,000 次的平均時間. 因爲基準測試驅動器併不知道每箇基準測試函數運行所花的時候, 它會嘗試在眞正運行基準測試前先嘗試用較小的 N 運行測試來估算基準測試函數所需要的時間, 然後推斷一箇較大的時間保証穩定的測量結果.
环在基准测试函数内实现, 而不是放在基准测试框架内实现, 这样可以让每个基准测试函数有机会在循环启动前执行初始化代码, 这样并不会显著影响每次迭代的平均运行时间. 如果还是担心初始化代码部分对测量时间带来干扰, 那么可以通过 testing.B 参数的方法来临时关闭或重置计时器, 不过这些一般很少会用到.
環在基準測試函數內實現, 而不是放在基準測試框架內實現, 這樣可以讓每箇基準測試函數有機會在循環啟動前執行初始化代碼, 這樣併不會顯著影響每次迭代的平均運行時間. 如果還是擔心初始化代碼部分對測量時間帶來乾擾, 那麼可以通過 testing.B 參數的方法來臨時關閉或重置計時器, 不過這些一般很少會用到.
现在我们有了一个基准测试和普通测试, 我们可以很容易测试新的让程序运行更快的想法. 也许最明显的优化是在 IsPalindrome 函数中第二个循环的停止检查, 这样可以避免每个比较都做两次:
現在我們有了一箇基準測試和普通測試, 我們可以很容易測試新的讓程序運行更快的想法. 也許最明顯的優化是在 IsPalindrome 函數中第二箇循環的停止檢査, 這樣可以避免每箇比較都做兩次:
```Go
n := len(letters)/2
@ -43,7 +43,7 @@ for i := 0; i < n; i++ {
return true
```
过很多情况下, 一个明显的优化并不一定就能代码预期的效果. 这个改进在基准测试中值带来了 4% 的性能提升.
過很多情況下, 一箇明顯的優化併不一定就能代碼預期的效果. 這箇改進在基準測試中值帶來了 4% 的性能提昇.
```
$ go test -bench=.
@ -52,7 +52,7 @@ BenchmarkIsPalindrome-8 1000000 992 ns/op
ok gopl.io/ch11/word2 2.093s
```
另一个改进想法是在开始为每个字符预先分配一个足够大的数组, 这样就可以避免在 append 调用时可能会导致内存的多次重新分配. 声明一个 letters 数组变量, 并指定合适的大小, 像这样,
另一箇改進想法是在開始爲每箇字符預先分配一箇足夠大的數組, 這樣就可以避免在 append 調用時可能會導緻內存的多次重新分配. 聲明一箇 letters 數組變量, 併指定閤適的大小, 像這樣,
```Go
letters := make([]rune, 0, len(s))
@ -63,7 +63,7 @@ for _, r := range s {
}
```
这个改进提升性能约 35%, 报告结果是基于 2,000,000 次迭代的平均运行时间统计.
這箇改進提昇性能約 35%, 報告結果是基於 2,000,000 次迭代的平均運行時間統計.
```
$ go test -bench=.
@ -72,7 +72,7 @@ BenchmarkIsPalindrome-8 2000000 697 ns/op
ok gopl.io/ch11/word2 1.468s
```
这个例子所示, 快的程序往往是有很少的内存分配. `-benchmem` 命令行标志参数将在报告中包含内存的分配数据统计. 我们可以比较优化前后内存的分配情况:
這箇例子所示, 快的程序往往是有很少的內存分配. `-benchmem` 命令行標誌參數將在報告中包含內存的分配數據統計. 我們可以比較優化前後內存的分配情況:
```
$ go test -bench=. -benchmem
@ -80,7 +80,7 @@ PASS
BenchmarkIsPalindrome 1000000 1026 ns/op 304 B/op 4 allocs/op
```
这是优化之后的结果:
這是優化之後的結果:
```
$ go test -bench=. -benchmem
@ -88,11 +88,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) { /* ... */ }
@ -101,13 +101,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,11 +1,11 @@
## 12.1. 為何需要反射?
时候我们需要编写一个函数能够处理一类并不满足普通公共接口的类型的值, 也可能它们并没有确定的表示方式, 或者在我们设计该函数的时候还这些类型可能还不存在, 各种情况都有可能.
時候我們需要編寫一箇函數能夠處理一類併不滿足普通公共接口的類型的值, 也可能它們併沒有確定的錶示方式, 或者在我們設計該函數的時候還這些類型可能還不存在, 各種情況都有可能.
个大家熟悉的例子是 fmt.Fprintf 函数提供的字符串格式化处理逻辑, 它可以用例对任意类型的值格式化打印, 甚至是用户自定义的类型. 让我们来尝试实现一个类似功能的函数. 简单起见, 我们的函数只接收一个参数, 然后返回和 fmt.Sprint 类似的格式化后的字符串, 我们的函数名也叫 Sprint.
箇大傢熟悉的例子是 fmt.Fprintf 函數提供的字符串格式化處理邏輯, 它可以用例對任意類型的值格式化打印, 甚至是用戶自定義的類型. 讓我們來嘗試實現一箇類似功能的函數. 簡單起見, 我們的函數隻接收一箇參數, 然後返迴和 fmt.Sprint 類似的格式化後的字符串, 我們的函數名也叫 Sprint.
们使用了 switch 分支首先来测试输入参数是否实现了 String 方法, 如果是的话就使用该方法. 然后继续增加测试分支, 检查是否是每个基于 string, int, bool 等基础类型的动态类型, 并在每种情况下执行适当的格式化操作.
們使用了 switch 分支首先來測試輸入參數是否實現了 String 方法, 如果是的話就使用該方法. 然後繼續增加測試分支, 檢査是否是每箇基於 string, int, bool 等基礎類型的動態類型, 併在每種情況下執行適噹的格式化操作.
```Go
func Sprint(x interface{}) string {
@ -32,8 +32,8 @@ func Sprint(x interface{}) string {
}
```
但是我们如何处理其它类似 []float64, map[string][]string 等类型呢? 我们当然可以添加更多的测试分支, 但是这些组合类型的数目基本是无穷的. 还有如何处理 url.Values 等命令的类型呢? 虽然类型分支可以识别出底层的基础类型是 map[string][]string, 但是它并不匹配 url.Values 类型, 因为这是两种不同的类型, 而且 switch 分支也不可能包含每个类似 url.Values 的类型, 这会导致对这些库的依赖.
但是我們如何處理其它類似 []float64, map[string][]string 等類型呢? 我們噹然可以添加更多的測試分支, 但是這些組閤類型的數目基本是無窮的. 還有如何處理 url.Values 等命令的類型呢? 雖然類型分支可以識別齣底層的基礎類型是 map[string][]string, 但是它併不匹配 url.Values 類型, 因爲這是兩種不衕的類型, 而且 switch 分支也不可能包含每箇類似 url.Values 的類型, 這會導緻對這些庫的依賴.
没有一种方法来检查未知类型的表示方式, 我们被卡住了. 这就是我们为何需要反射的原因.
沒有一種方法來檢査未知類型的錶示方式, 我們被卡住了. 這就是我們爲何需要反射的原因.

View File

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

View File

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

View File

@ -1,14 +1,14 @@
## 8.9. 併髮的退齣
时候我们需要通知goroutine停止它正在干的事情比如一个正在执行计算的web服务然而它的客户端已经断开了和服务端的连接。
時候我們需要通知goroutine停止它正在乾的事情比如一箇正在執行計算的web服務然而它的客戶端已經斷開了和服務端的連接。
Go语言并没有提供在一个goroutine中终止另一个goroutine的方法由于这样会导致goroutine之间的共享变量落在未定义的状态上。在8.7节中的rocket launch程序中我们往名字叫abort的channel里发送了一个简单的值在countdown的goroutine中会把这个值理解为自己的退出信号。但是如果我们想要退出两个或者任意多个goroutine怎么办呢?
Go語言併沒有提供在一箇goroutine中終止另一箇goroutine的方法由於這樣會導緻goroutine之間的共享變量落在未定義的狀態上。在8.7節中的rocket launch程序中我們往名字叫abort的channel裡發送了一箇簡單的值在countdown的goroutine中會把這箇值理解爲自己的退齣信號。但是如果我們想要退齣兩箇或者任意多箇goroutine怎麼辦呢?
种可能的手段是向abort的channel里发送和goroutine数目一样多的事件来退出它们。如果这些goroutine中已经有一些自己退出了那么会导致我们的channel里的事件数比goroutine还多这样导致我们的发送直接被阻塞。另一方面如果这些goroutine又生成了其它的goroutine我们的channel里的数目又太少了所以有些goroutine可能会无法接收到退出消息。一般情况下我们是很难知道在某一个时刻具体有多少个goroutine在运行着的。另外当一个goroutine从abort channel中接收到一个值的时候他会消费掉这个值这样其它的goroutine就没法看到这条信息。为了能够达到我们退出goroutine的目的我们需要更靠谱的策略来通过一个channel把消息广播出去这样goroutine们能够看到这条事件消息并且在事件完成之后可以知道这件事已经发生过了。
種可能的手段是嚮abort的channel裡發送和goroutine數目一樣多的事件來退齣它們。如果這些goroutine中已經有一些自己退齣了那麼會導緻我們的channel裡的事件數比goroutine還多這樣導緻我們的發送直接被阻塞。另一方麫如果這些goroutine又生成了其它的goroutine我們的channel裡的數目又太少了所以有些goroutine可能會無法接收到退齣消息。一般情況下我們是很難知道在某一箇時刻具體有多少箇goroutine在運行着的。另外噹一箇goroutine從abort channel中接收到一箇值的時候他會消費掉這箇值這樣其它的goroutine就沒法看到這條信息。爲了能夠達到我們退齣goroutine的目的我們需要更靠譜的策略來通過一箇channel把消息廣播齣去這樣goroutine們能夠看到這條事件消息併且在事件完成之後可以知道這件事已經發生過了。
回忆一下我们关闭了一个channel并且被消费掉了所有已发送的值操作channel之后的代码可以立即被执行并且会产生零值。我们可以将这个机制扩展一下来作为我们的广播机制不要向channel发送值而是用关闭一个channel来进行广播。
迴憶一下我們關閉了一箇channel併且被消費掉了所有已發送的值操作channel之後的代碼可以立卽被執行併且會產生零值。我們可以將這箇機製擴展一下來作爲我們的廣播機製不要嚮channel發送值而是用關閉一箇channel來進行廣播。
只要一些小修改我们就可以把退出逻辑加入到前一节的du程序。首先我们创建一个退出的channel这个channel不会向其中发送任何值但其所在的闭包内要写明程序需要退出。我们同时还定义了一个工具函数cancelled这个函数在被调用的时候会轮询退出状态
隻要一些小脩改我們就可以把退齣邏輯加入到前一節的du程序。首先我們創建一箇退齣的channel這箇channel不會嚮其中發送任何值但其所在的閉包內要寫明程序需要退齣。我們衕時還定義了一箇工具函數cancelled這箇函數在被調用的時候會輪詢退齣狀態
```go
gopl.io/ch8/du4
@ -24,7 +24,7 @@ func cancelled() bool {
}
```
面我们创建一个从标准输入流中读取内容的goroutine这是一个比较典型的连接到终端的程序。每当有输入被读到(比如用户按了回车键)这个goroutine就会把取消消息通过关闭done的channel广播出去。
麫我們創建一箇從標準輸入流中讀取內容的goroutine這是一箇比較典型的連接到終端的程序。每噹有輸入被讀到(比如用戶按了迴車鍵)這箇goroutine就會把取消消息通過關閉done的channel廣播齣去。
```go
// Cancel traversal when input is detected.
@ -34,7 +34,7 @@ go func() {
}()
```
现在我们需要使我们的goroutine来对取消进行响应。在main goroutine中我们添加了select的第三个case语句尝试从done channel中接收内容。如果这个case被满足的话在select到的时候即会返回但在结束之前我们需要把fileSizes channel中的内容“排”空在channel被关闭之前舍弃掉所有值。这样可以保证对walkDir的调用不要被向fileSizes发送信息阻塞住可以正确地完成。
現在我們需要使我們的goroutine來對取消進行響應。在main goroutine中我們添加了select的第三箇case語句嘗試從done channel中接收內容。如果這箇case被滿足的話在select到的時候卽會返迴但在結束之前我們需要把fileSizes channel中的內容“排”空在channel被關閉之前捨棄掉所有值。這樣可以保証對walkDir的調用不要被嚮fileSizes發送信息阻塞住可以正確地完成。
```go
for {
@ -51,7 +51,7 @@ for {
}
```
walkDir这个goroutine一启动就会轮询取消状态如果取消状态被设置的话会直接返回并且不做额外的事情。这样我们将所有在取消事件之后创建的goroutine改变为无操作。
walkDir這箇goroutine一啟動就會輪詢取消狀態如果取消狀態被設置的話會直接返迴併且不做額外的事情。這樣我們將所有在取消事件之後創建的goroutine改變爲無操作。
```go
func walkDir(dir string, n *sync.WaitGroup, fileSizes chan<- int64) {
@ -66,9 +66,9 @@ func walkDir(dir string, n *sync.WaitGroup, fileSizes chan<- int64) {
```
在walkDir函数的循环中我们对取消状态进行轮询可以带来明显的益处可以避免在取消事件发生时还去创建goroutine。取消本身是有一些代价的想要快速的响应需要对程序逻辑进行侵入式的修改。确保在取消发生之后不要有代价太大的操作可能会需要修改你代码里的很多地方但是在一些重要的地方去检查取消事件也确实能带来很大的好处
在walkDir函數的循環中我們對取消狀態進行輪詢可以帶來明顯的益處可以避免在取消事件發生時還去創建goroutine。取消本身是有一些代價的想要快速的響應需要對程序邏輯進行侵入式的脩改。確保在取消發生之後不要有代價太大的操作可能會需要脩改你代碼裡的很多地方但是在一些重要的地方去檢査取消事件也確實能帶來很大的好處
对这个程序的一个简单的性能分析可以揭示瓶颈在dirents函数中获取一个信号量。下面的select可以让这种操作可以被取消并且可以将取消时的延迟从几百毫秒降低到几十毫秒。
對這箇程序的一箇簡單的性能分析可以揭示瓶頸在dirents函數中穫取一箇信號量。下麫的select可以讓這種操作可以被取消併且可以將取消時的延遲從幾百毫秒降低到幾十毫秒。
```go
func dirents(dir string) []os.FileInfo {
@ -82,10 +82,10 @@ func dirents(dir string) []os.FileInfo {
}
```
现在当取消发生时所有后台的goroutine都会迅速停止并且主函数会返回。当然当主函数返回时一个程序会退出而我们又无法在主函数退出的时候确认其已经释放了所有的资源(译注:因为程序都退出了,你的代码都没法执行了)。这里有一个方便的窍门我们可以一用取代掉直接从主函数返回我们调用一个panic然后runtime会把每一个goroutine的栈dump下来。如果main goroutine是唯一一个剩下的goroutine的话他会清理掉自己的一切资源。但是如果还有其它的goroutine没有退出他们可能没办法被正确地取消掉也有可能被取消但是取消操作会很花时间所以这里的一个调研还是很有必要的。我们用panic来获取到足够的信息来验证我们上面的判断看看最终到底是什么样的情况
現在噹取消發生時所有後檯的goroutine都會迅速停止併且主函數會返迴。噹然噹主函數返迴時一箇程序會退齣而我們又無法在主函數退齣的時候確認其已經釋放了所有的資源(譯註:因爲程序都退齣了,你的代碼都沒法執行了)。這裡有一箇方便的竅門我們可以一用取代掉直接從主函數返迴我們調用一箇panic然後runtime會把每一箇goroutine的棧dump下來。如果main goroutine是唯一一箇剩下的goroutine的話他會清理掉自己的一切資源。但是如果還有其它的goroutine沒有退齣他們可能沒辦法被正確地取消掉也有可能被取消但是取消操作會很花時間所以這裡的一箇調研還是很有必要的。我們用panic來穫取到足夠的信息來驗証我們上麫的判斷看看最終到底是什麼樣的情況
练习8.10: HTTP请求可能会因http.Request结构体中Cancel channel的关闭而取消。修改8.6节中的web crawler来支持取消http请求。
練習8.10: HTTP請求可能會因http.Request結構體中Cancel channel的關閉而取消。脩改8.6節中的web crawler來支持取消http請求。
提示: http.Get并没有提供方便地定制一个请求的方法。你可以用http.NewRequest来取而代之设置它的Cancel字段然后用http.DefaultClient.Do(req)来进行这个http请求。
提示: http.Get併沒有提供方便地定製一箇請求的方法。你可以用http.NewRequest來取而代之設置它的Cancel字段然後用http.DefaultClient.Do(req)來進行這箇http請求。
练习8.11:紧接着8.4.4中的mirroredQuery流程实现一个并发请求url的fetch的变种。当第一个请求返回时直接取消其它的请求。
練習8.11:緊接着8.4.4中的mirroredQuery流程實現一箇併發請求url的fetch的變種。噹第一箇請求返迴時直接取消其它的請求。