This commit is contained in:
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在行着的。另外,当一个goroutineabort channel中接收到一值的候,他会消费掉这个值,这样其它的goroutine就法看到这条信息。了能够达到我们退出goroutine的目的需要更靠的策略,来通过一个channel把消息广播出去,这样goroutine们能够看到这条事件消息,且在事件完成之,可以知道件事已经发生过了。
可能的手段是abort的channel裡發送和goroutine目一多的事件來退齣它們。如果些goroutine中已有一些自己退了,那麼會導緻我們的channel的事件比goroutine多,這樣導緻我們的發送直接被阻塞。另一方,如果些goroutine又生成了其它的goroutine的channel裡的數目又太少了所以有些goroutine可能會無法接收到退消息。一般情下我是很知道在某一箇時刻具有多少goroutine在行着的。另外,噹一箇goroutineabort 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都迅速停止且主函数会返回。当然,主函数返回时,一程序会退出,而我们又无法在主函数退出的时候确认其已经释放了所有的源(译注:因程序都退了,你的代码都没法执行了)。这里有一方便的窍门我们可以一用:取代掉直接主函数返回,我们调用一panicruntime把每一goroutine的dump下。如果main goroutine是唯一一剩下的goroutine的,他清理掉自己的一切源。但是如果有其它的goroutine有退,他可能没办法被正地取消掉,也有可能被取消但是取消操作很花时间;所以这里的一个调研还是很有必要的。我用panic来获取到足的信息来验证我们上面的判,看看最到底是什么样的情
現在噹取消發生時,所有後檯的goroutine都迅速停止且主函數會返迴。噹然,主函數返迴時,一程序會退齣,而我們又無法在主函數退齣的時候確認其已經釋放了所有的源(譯註:因程序都退了,你的代碼都沒法執行了)。這裡有一方便的竅門我們可以一用:取代掉直接主函數返迴,我們調用一panicruntime把每一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的變種。噹第一箇請求返迴時,直接取消其它的求。