mirror of
https://github.com/gopl-zh/gopl-zh.github.com.git
synced 2024-12-25 14:28:58 +00:00
rebuild
This commit is contained in:
parent
0a24303905
commit
1d7534e3c3
@ -48,7 +48,7 @@
|
||||
<body>
|
||||
|
||||
|
||||
<div class="book" data-level="0.1" data-chapter-title="Go語言起源" data-filepath="ch0/ch0-01.md" data-basepath=".." data-revision="Mon Dec 14 2015 11:30:54 GMT+0800 (中国标准时间)">
|
||||
<div class="book" data-level="0.1" data-chapter-title="Go語言起源" data-filepath="ch0/ch0-01.md" data-basepath=".." data-revision="Wed Dec 16 2015 10:54:29 GMT+0800 (中国标准时间)">
|
||||
|
||||
|
||||
<div class="book-summary">
|
||||
|
@ -48,7 +48,7 @@
|
||||
<body>
|
||||
|
||||
|
||||
<div class="book" data-level="0.2" data-chapter-title="Go語言項目" data-filepath="ch0/ch0-02.md" data-basepath=".." data-revision="Mon Dec 14 2015 11:30:54 GMT+0800 (中国标准时间)">
|
||||
<div class="book" data-level="0.2" data-chapter-title="Go語言項目" data-filepath="ch0/ch0-02.md" data-basepath=".." data-revision="Wed Dec 16 2015 10:54:29 GMT+0800 (中国标准时间)">
|
||||
|
||||
|
||||
<div class="book-summary">
|
||||
|
@ -48,7 +48,7 @@
|
||||
<body>
|
||||
|
||||
|
||||
<div class="book" data-level="0.3" data-chapter-title="本書的組織" data-filepath="ch0/ch0-03.md" data-basepath=".." data-revision="Mon Dec 14 2015 11:30:54 GMT+0800 (中国标准时间)">
|
||||
<div class="book" data-level="0.3" data-chapter-title="本書的組織" data-filepath="ch0/ch0-03.md" data-basepath=".." data-revision="Wed Dec 16 2015 10:54:29 GMT+0800 (中国标准时间)">
|
||||
|
||||
|
||||
<div class="book-summary">
|
||||
|
@ -48,7 +48,7 @@
|
||||
<body>
|
||||
|
||||
|
||||
<div class="book" data-level="0.4" data-chapter-title="更多的信息" data-filepath="ch0/ch0-04.md" data-basepath=".." data-revision="Mon Dec 14 2015 11:30:54 GMT+0800 (中国标准时间)">
|
||||
<div class="book" data-level="0.4" data-chapter-title="更多的信息" data-filepath="ch0/ch0-04.md" data-basepath=".." data-revision="Wed Dec 16 2015 10:54:29 GMT+0800 (中国标准时间)">
|
||||
|
||||
|
||||
<div class="book-summary">
|
||||
|
@ -48,7 +48,7 @@
|
||||
<body>
|
||||
|
||||
|
||||
<div class="book" data-level="0.5" data-chapter-title="緻謝" data-filepath="ch0/ch0-05.md" data-basepath=".." data-revision="Mon Dec 14 2015 11:30:54 GMT+0800 (中国标准时间)">
|
||||
<div class="book" data-level="0.5" data-chapter-title="緻謝" data-filepath="ch0/ch0-05.md" data-basepath=".." data-revision="Wed Dec 16 2015 10:54:29 GMT+0800 (中国标准时间)">
|
||||
|
||||
|
||||
<div class="book-summary">
|
||||
|
@ -48,7 +48,7 @@
|
||||
<body>
|
||||
|
||||
|
||||
<div class="book" data-level="1.1" data-chapter-title="Hello, World" data-filepath="ch1/ch1-01.md" data-basepath=".." data-revision="Mon Dec 14 2015 11:30:54 GMT+0800 (中国标准时间)">
|
||||
<div class="book" data-level="1.1" data-chapter-title="Hello, World" data-filepath="ch1/ch1-01.md" data-basepath=".." data-revision="Wed Dec 16 2015 10:54:29 GMT+0800 (中国标准时间)">
|
||||
|
||||
|
||||
<div class="book-summary">
|
||||
|
@ -48,7 +48,7 @@
|
||||
<body>
|
||||
|
||||
|
||||
<div class="book" data-level="1.2" data-chapter-title="命令行參數" data-filepath="ch1/ch1-02.md" data-basepath=".." data-revision="Mon Dec 14 2015 11:30:54 GMT+0800 (中国标准时间)">
|
||||
<div class="book" data-level="1.2" data-chapter-title="命令行參數" data-filepath="ch1/ch1-02.md" data-basepath=".." data-revision="Wed Dec 16 2015 10:54:29 GMT+0800 (中国标准时间)">
|
||||
|
||||
|
||||
<div class="book-summary">
|
||||
|
@ -48,7 +48,7 @@
|
||||
<body>
|
||||
|
||||
|
||||
<div class="book" data-level="1.3" data-chapter-title="査找重復的行" data-filepath="ch1/ch1-03.md" data-basepath=".." data-revision="Mon Dec 14 2015 11:30:54 GMT+0800 (中国标准时间)">
|
||||
<div class="book" data-level="1.3" data-chapter-title="査找重復的行" data-filepath="ch1/ch1-03.md" data-basepath=".." data-revision="Wed Dec 16 2015 10:54:29 GMT+0800 (中国标准时间)">
|
||||
|
||||
|
||||
<div class="book-summary">
|
||||
|
@ -48,7 +48,7 @@
|
||||
<body>
|
||||
|
||||
|
||||
<div class="book" data-level="1.4" data-chapter-title="GIF動畫" data-filepath="ch1/ch1-04.md" data-basepath=".." data-revision="Mon Dec 14 2015 11:30:54 GMT+0800 (中国标准时间)">
|
||||
<div class="book" data-level="1.4" data-chapter-title="GIF動畫" data-filepath="ch1/ch1-04.md" data-basepath=".." data-revision="Wed Dec 16 2015 10:54:29 GMT+0800 (中国标准时间)">
|
||||
|
||||
|
||||
<div class="book-summary">
|
||||
|
@ -48,7 +48,7 @@
|
||||
<body>
|
||||
|
||||
|
||||
<div class="book" data-level="1.5" data-chapter-title="穫取URL" data-filepath="ch1/ch1-05.md" data-basepath=".." data-revision="Mon Dec 14 2015 11:30:54 GMT+0800 (中国标准时间)">
|
||||
<div class="book" data-level="1.5" data-chapter-title="穫取URL" data-filepath="ch1/ch1-05.md" data-basepath=".." data-revision="Wed Dec 16 2015 10:54:29 GMT+0800 (中国标准时间)">
|
||||
|
||||
|
||||
<div class="book-summary">
|
||||
|
@ -48,7 +48,7 @@
|
||||
<body>
|
||||
|
||||
|
||||
<div class="book" data-level="1.6" data-chapter-title="併髮穫取多個URL" data-filepath="ch1/ch1-06.md" data-basepath=".." data-revision="Mon Dec 14 2015 11:30:54 GMT+0800 (中国标准时间)">
|
||||
<div class="book" data-level="1.6" data-chapter-title="併髮穫取多個URL" data-filepath="ch1/ch1-06.md" data-basepath=".." data-revision="Wed Dec 16 2015 10:54:29 GMT+0800 (中国标准时间)">
|
||||
|
||||
|
||||
<div class="book-summary">
|
||||
|
@ -48,7 +48,7 @@
|
||||
<body>
|
||||
|
||||
|
||||
<div class="book" data-level="1.7" data-chapter-title="Web服務" data-filepath="ch1/ch1-07.md" data-basepath=".." data-revision="Mon Dec 14 2015 11:30:54 GMT+0800 (中国标准时间)">
|
||||
<div class="book" data-level="1.7" data-chapter-title="Web服務" data-filepath="ch1/ch1-07.md" data-basepath=".." data-revision="Wed Dec 16 2015 10:54:29 GMT+0800 (中国标准时间)">
|
||||
|
||||
|
||||
<div class="book-summary">
|
||||
|
@ -48,7 +48,7 @@
|
||||
<body>
|
||||
|
||||
|
||||
<div class="book" data-level="1.8" data-chapter-title="本章要點" data-filepath="ch1/ch1-08.md" data-basepath=".." data-revision="Mon Dec 14 2015 11:30:54 GMT+0800 (中国标准时间)">
|
||||
<div class="book" data-level="1.8" data-chapter-title="本章要點" data-filepath="ch1/ch1-08.md" data-basepath=".." data-revision="Wed Dec 16 2015 10:54:29 GMT+0800 (中国标准时间)">
|
||||
|
||||
|
||||
<div class="book-summary">
|
||||
|
@ -48,7 +48,7 @@
|
||||
<body>
|
||||
|
||||
|
||||
<div class="book" data-level="1" data-chapter-title="入門" data-filepath="ch1/ch1.md" data-basepath=".." data-revision="Mon Dec 14 2015 11:30:54 GMT+0800 (中国标准时间)">
|
||||
<div class="book" data-level="1" data-chapter-title="入門" data-filepath="ch1/ch1.md" data-basepath=".." data-revision="Wed Dec 16 2015 10:54:29 GMT+0800 (中国标准时间)">
|
||||
|
||||
|
||||
<div class="book-summary">
|
||||
|
@ -48,7 +48,7 @@
|
||||
<body>
|
||||
|
||||
|
||||
<div class="book" data-level="10.1" data-chapter-title="簡介" data-filepath="ch10/ch10-01.md" data-basepath=".." data-revision="Mon Dec 14 2015 11:30:54 GMT+0800 (中国标准时间)">
|
||||
<div class="book" data-level="10.1" data-chapter-title="簡介" data-filepath="ch10/ch10-01.md" data-basepath=".." data-revision="Wed Dec 16 2015 10:54:29 GMT+0800 (中国标准时间)">
|
||||
|
||||
|
||||
<div class="book-summary">
|
||||
|
@ -48,7 +48,7 @@
|
||||
<body>
|
||||
|
||||
|
||||
<div class="book" data-level="10.2" data-chapter-title="導入路徑" data-filepath="ch10/ch10-02.md" data-basepath=".." data-revision="Mon Dec 14 2015 11:30:54 GMT+0800 (中国标准时间)">
|
||||
<div class="book" data-level="10.2" data-chapter-title="導入路徑" data-filepath="ch10/ch10-02.md" data-basepath=".." data-revision="Wed Dec 16 2015 10:54:29 GMT+0800 (中国标准时间)">
|
||||
|
||||
|
||||
<div class="book-summary">
|
||||
|
@ -48,7 +48,7 @@
|
||||
<body>
|
||||
|
||||
|
||||
<div class="book" data-level="10.3" data-chapter-title="包聲明" data-filepath="ch10/ch10-03.md" data-basepath=".." data-revision="Mon Dec 14 2015 11:30:54 GMT+0800 (中国标准时间)">
|
||||
<div class="book" data-level="10.3" data-chapter-title="包聲明" data-filepath="ch10/ch10-03.md" data-basepath=".." data-revision="Wed Dec 16 2015 10:54:29 GMT+0800 (中国标准时间)">
|
||||
|
||||
|
||||
<div class="book-summary">
|
||||
|
@ -48,7 +48,7 @@
|
||||
<body>
|
||||
|
||||
|
||||
<div class="book" data-level="10.4" data-chapter-title="導入聲明" data-filepath="ch10/ch10-04.md" data-basepath=".." data-revision="Mon Dec 14 2015 11:30:54 GMT+0800 (中国标准时间)">
|
||||
<div class="book" data-level="10.4" data-chapter-title="導入聲明" data-filepath="ch10/ch10-04.md" data-basepath=".." data-revision="Wed Dec 16 2015 10:54:29 GMT+0800 (中国标准时间)">
|
||||
|
||||
|
||||
<div class="book-summary">
|
||||
|
@ -48,7 +48,7 @@
|
||||
<body>
|
||||
|
||||
|
||||
<div class="book" data-level="10.5" data-chapter-title="匿名導入" data-filepath="ch10/ch10-05.md" data-basepath=".." data-revision="Mon Dec 14 2015 11:30:54 GMT+0800 (中国标准时间)">
|
||||
<div class="book" data-level="10.5" data-chapter-title="匿名導入" data-filepath="ch10/ch10-05.md" data-basepath=".." data-revision="Wed Dec 16 2015 10:54:29 GMT+0800 (中国标准时间)">
|
||||
|
||||
|
||||
<div class="book-summary">
|
||||
|
@ -48,7 +48,7 @@
|
||||
<body>
|
||||
|
||||
|
||||
<div class="book" data-level="10.6" data-chapter-title="包和命名" data-filepath="ch10/ch10-06.md" data-basepath=".." data-revision="Mon Dec 14 2015 11:30:54 GMT+0800 (中国标准时间)">
|
||||
<div class="book" data-level="10.6" data-chapter-title="包和命名" data-filepath="ch10/ch10-06.md" data-basepath=".." data-revision="Wed Dec 16 2015 10:54:29 GMT+0800 (中国标准时间)">
|
||||
|
||||
|
||||
<div class="book-summary">
|
||||
|
@ -48,7 +48,7 @@
|
||||
<body>
|
||||
|
||||
|
||||
<div class="book" data-level="10.7" data-chapter-title="工具" data-filepath="ch10/ch10-07.md" data-basepath=".." data-revision="Mon Dec 14 2015 11:30:54 GMT+0800 (中国标准时间)">
|
||||
<div class="book" data-level="10.7" data-chapter-title="工具" data-filepath="ch10/ch10-07.md" data-basepath=".." data-revision="Wed Dec 16 2015 10:54:29 GMT+0800 (中国标准时间)">
|
||||
|
||||
|
||||
<div class="book-summary">
|
||||
|
@ -48,7 +48,7 @@
|
||||
<body>
|
||||
|
||||
|
||||
<div class="book" data-level="10" data-chapter-title="包和工具" data-filepath="ch10/ch10.md" data-basepath=".." data-revision="Mon Dec 14 2015 11:30:54 GMT+0800 (中国标准时间)">
|
||||
<div class="book" data-level="10" data-chapter-title="包和工具" data-filepath="ch10/ch10.md" data-basepath=".." data-revision="Wed Dec 16 2015 10:54:29 GMT+0800 (中国标准时间)">
|
||||
|
||||
|
||||
<div class="book-summary">
|
||||
|
@ -48,7 +48,7 @@
|
||||
<body>
|
||||
|
||||
|
||||
<div class="book" data-level="11.1" data-chapter-title="go test" data-filepath="ch11/ch11-01.md" data-basepath=".." data-revision="Mon Dec 14 2015 11:30:54 GMT+0800 (中国标准时间)">
|
||||
<div class="book" data-level="11.1" data-chapter-title="go test" data-filepath="ch11/ch11-01.md" data-basepath=".." data-revision="Wed Dec 16 2015 10:54:29 GMT+0800 (中国标准时间)">
|
||||
|
||||
|
||||
<div class="book-summary">
|
||||
|
@ -1,3 +1,108 @@
|
||||
### 11.2.2. 測試一個命令
|
||||
|
||||
TODO
|
||||
|
||||
對於測試包 `go test` 是一個的有用的工具, 但是稍加努力我們也可以用它來測試可執行程序. 如果一個包的名字是 main, 那麽在構建時會生成一個可執行程序, 不過 main 包可以作為一個包被測試器代碼導入.
|
||||
|
||||
讓我們為 2.3.2節 的 echo 程序編寫一個測試. 我們先將程序拆分為兩個函數: echo 函數完成眞正的工作, main 函數用於處理命令行輸入參數和echo可能返迴的錯誤.
|
||||
|
||||
```Go
|
||||
gopl.io/ch11/echo
|
||||
// Echo prints its command-line arguments.
|
||||
package main
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"strings"
|
||||
)
|
||||
|
||||
var (
|
||||
n = flag.Bool("n", false, "omit trailing newline")
|
||||
s = flag.String("s", " ", "separator")
|
||||
)
|
||||
|
||||
var out io.Writer = os.Stdout // modified during testing
|
||||
|
||||
func main() {
|
||||
flag.Parse()
|
||||
if err := echo(!*n, *s, flag.Args()); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "echo: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
func echo(newline bool, sep string, args []string) error {
|
||||
fmt.Fprint(out, strings.Join(args, sep))
|
||||
if newline {
|
||||
fmt.Fprintln(out)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
```
|
||||
|
||||
在測試中嗎我們可以用各種參數和標標誌調用 echo 函數, 然後檢測它的輸齣是否正確, 我們通過增加參數來減少 echo 函數對全侷變量的依賴. 我們還增加了一個全侷名為 out 的變量來替代直接使用 os.Stdout, 這樣測試代碼可以根據需要將 out 脩改為不衕的對象以便於檢査. 下麪就是 echo_test.go 文件中的測試代碼:
|
||||
|
||||
```Go
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestEcho(t *testing.T) {
|
||||
var tests = []struct {
|
||||
newline bool
|
||||
sep string
|
||||
args []string
|
||||
want string
|
||||
}{
|
||||
{true, "", []string{}, "\n"},
|
||||
{false, "", []string{}, ""},
|
||||
{true, "\t", []string{"one", "two", "three"}, "one\ttwo\tthree\n"},
|
||||
{true, ",", []string{"a", "b", "c"}, "a,b,c\n"},
|
||||
{false, ":", []string{"1", "2", "3"}, "1:2:3"},
|
||||
}
|
||||
for _, test := range tests {
|
||||
descr := fmt.Sprintf("echo(%v, %q, %q)",
|
||||
test.newline, test.sep, test.args)
|
||||
|
||||
out = new(bytes.Buffer) // captured output
|
||||
if err := echo(test.newline, test.sep, test.args); err != nil {
|
||||
t.Errorf("%s failed: %v", descr, err)
|
||||
continue
|
||||
}
|
||||
got := out.(*bytes.Buffer).String()
|
||||
if got != test.want {
|
||||
t.Errorf("%s = %q, want %q", descr, got, test.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
要註意的是測試代碼和產品代碼在衕一個包. 雖然是main包, 也有對應的 main 入口函數, 但是在測試的時候 main 包隻是 TestEcho 測試函數導入的一個普通包, 裡麪 main 函數並沒有被導齣是被忽略的.
|
||||
|
||||
通過將測試放到錶格中, 我們很容易添加新的測試用例. 讓我通過增加下麪的測試用例來看看失敗的情況是怎麽樣的:
|
||||
|
||||
```Go
|
||||
{true, ",", []string{"a", "b", "c"}, "a b c\n"}, // NOTE: wrong expectation!
|
||||
```
|
||||
|
||||
`go test` 輸齣如下:
|
||||
|
||||
```
|
||||
$ go test gopl.io/ch11/echo
|
||||
--- FAIL: TestEcho (0.00s)
|
||||
echo_test.go:31: echo(true, ",", ["a" "b" "c"]) = "a,b,c", want "a b c\n"
|
||||
FAIL
|
||||
FAIL gopl.io/ch11/echo 0.006s
|
||||
```
|
||||
|
||||
錯誤信息描述了嘗試的操作(使用Go類似語法), 實際的行為, 和期望的行為. 通過這樣的錯誤信息, 你可以在檢視代碼之前就很容易定位錯誤的原因.
|
||||
|
||||
要註意的是在測試代碼中並沒有調用 log.Fatal 或 os.Exit, 因為調用這類函數會導緻程序提前退齣; 調用這些函數的特權應該放在 main 函數中. 如果眞的有以外的事情導緻函數發送 panic, 測試驅動應該嘗試 recover, 然後將噹前測試噹作失敗處理. 如果是可預期的錯誤, 例如非法的用戶輸入, 找不到文件, 或配置文件不噹等應該通過返迴一個非空的 error 的方式處理. 倖運的是(上麪的意外隻是一個插麯), 我們的 echo 示例是比較簡單的也沒有需要返迴非空error的情況.
|
||||
|
||||
|
||||
|
@ -1,3 +1,134 @@
|
||||
### 11.2.3. 白盒測試
|
||||
|
||||
TODO
|
||||
|
||||
一個測試分類的方法是基於測試者是否需要了解被測試對象的內部工作原理. 黑盒測試隻需要測試包公開的文檔和API行為, 內部實現對測試代碼是透明的. 相反, 白盒測試有訪問包內部函數和數據結構的權限, 因此可以做到一下普通客戶端無法實現的測試. 例如, 一個飽和測試可以在每個操作之後檢測不變量的數據類型. (白盒測試隻是一個傳統的名稱, 其實稱為 clear box 會更準確.)
|
||||
|
||||
黑盒和白盒這兩種測試方法是互補的. 黑盒測試一般更健壯, 隨着軟件實現的完善測試代碼很少需要更新. 它們可以幫助測試者了解眞是客戶的需求, 可以幫助發現API設計的一些不足之處. 相反, 白盒測試則可以對內部一些棘手的實現提供更多的測試覆蓋.
|
||||
|
||||
我們已經看到兩種測試的例子. TestIsPalindrome 測試僅僅使用導齣的 IsPalindrome 函數, 因此它是一個黑盒測試. TestEcho 測試則調用了內部的 echo 函數, 並且更新了內部的 out 全侷變量, 這兩個都是未導齣的, 因此它是白盒測試.
|
||||
|
||||
噹我們開發TestEcho測試的時候, 我們脩改了 echo 函數使用包級的 out 作為輸齣對象, 因此測試代碼可以用另一個實現代替標準輸齣, 這樣可以方便對比 echo 的輸齣數據. 使用類似的技術, 我們可以將產品代碼的其他部分也替換為一個容易測試的僞對象. 使用僞對象的好處是我們可以方便配置, 容易預測, 更可靠, 也更容易觀察. 衕時也可以避免一些不良的副作用, 例如更新生產數據庫或信用卡消費行為.
|
||||
|
||||
下麪的代碼演示了為用戶提供網絡存儲的web服務中的配額檢測邏輯. 噹用戶使用了超過 90% 的存儲配額之後將發送提醒郵件.
|
||||
|
||||
```Go
|
||||
gopl.io/ch11/storage1
|
||||
|
||||
package storage
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"net/smtp"
|
||||
)
|
||||
|
||||
func bytesInUse(username string) int64 { return 0 /* ... */ }
|
||||
|
||||
// Email sender configuration.
|
||||
// NOTE: never put passwords in source code!
|
||||
const sender = "notifications@example.com"
|
||||
const password = "correcthorsebatterystaple"
|
||||
const hostname = "smtp.example.com"
|
||||
|
||||
const template = `Warning: you are using %d bytes of storage,
|
||||
%d%% of your quota.`
|
||||
|
||||
func CheckQuota(username string) {
|
||||
used := bytesInUse(username)
|
||||
const quota = 1000000000 // 1GB
|
||||
percent := 100 * used / quota
|
||||
if percent < 90 {
|
||||
return // OK
|
||||
}
|
||||
msg := fmt.Sprintf(template, used, percent)
|
||||
auth := smtp.PlainAuth("", sender, password, hostname)
|
||||
err := smtp.SendMail(hostname+":587", auth, sender,
|
||||
[]string{username}, []byte(msg))
|
||||
if err != nil {
|
||||
log.Printf("smtp.SendMail(%s) failed: %s", username, err)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
我們想測試這個代碼, 但是我們並不希望發送眞實的郵件. 因此我們將郵件處理邏輯放到一個俬有的 notifyUser 函數.
|
||||
|
||||
```Go
|
||||
gopl.io/ch11/storage2
|
||||
|
||||
var notifyUser = func(username, msg string) {
|
||||
auth := smtp.PlainAuth("", sender, password, hostname)
|
||||
err := smtp.SendMail(hostname+":587", auth, sender,
|
||||
[]string{username}, []byte(msg))
|
||||
if err != nil {
|
||||
log.Printf("smtp.SendEmail(%s) failed: %s", username, err)
|
||||
}
|
||||
}
|
||||
|
||||
func CheckQuota(username string) {
|
||||
used := bytesInUse(username)
|
||||
const quota = 1000000000 // 1GB
|
||||
percent := 100 * used / quota
|
||||
if percent < 90 {
|
||||
return // OK
|
||||
}
|
||||
msg := fmt.Sprintf(template, used, percent)
|
||||
notifyUser(username, msg)
|
||||
}
|
||||
```
|
||||
|
||||
現在我們可以在測試中用僞郵件發送函數替代眞實的郵件發送函數. 它隻是簡單記録要通知的用戶和郵件的內容.
|
||||
|
||||
```Go
|
||||
package storage
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
func TestCheckQuotaNotifiesUser(t *testing.T) {
|
||||
var notifiedUser, notifiedMsg string
|
||||
notifyUser = func(user, msg string) {
|
||||
notifiedUser, notifiedMsg = user, msg
|
||||
}
|
||||
|
||||
// ...simulate a 980MB-used condition...
|
||||
|
||||
const user = "joe@example.org"
|
||||
CheckQuota(user)
|
||||
if notifiedUser == "" && notifiedMsg == "" {
|
||||
t.Fatalf("notifyUser not called")
|
||||
}
|
||||
if notifiedUser != user {
|
||||
t.Errorf("wrong user (%s) notified, want %s",
|
||||
notifiedUser, user)
|
||||
}
|
||||
const wantSubstring = "98% of your quota"
|
||||
if !strings.Contains(notifiedMsg, wantSubstring) {
|
||||
t.Errorf("unexpected notification message <<%s>>, "+
|
||||
"want substring %q", notifiedMsg, wantSubstring)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
這裡有一個問題: 噹測試函數返迴後, CheckQuota 將不能正常工作, 因為 notifyUsers 依然使用的是測試函數的僞發送郵件函數. (噹更新全侷對象的時候總會有這種風險.) 我們必鬚脩改測試代碼恢復 notifyUsers 原先的狀態以便後續其他的測試沒有影響, 要確保所有的執行路徑後都能恢復, 包括測試失敗或 panic 情形. 在這種情況下, 我們建議使用 defer 處理恢復的代碼.
|
||||
|
||||
```Go
|
||||
func TestCheckQuotaNotifiesUser(t *testing.T) {
|
||||
// Save and restore original notifyUser.
|
||||
saved := notifyUser
|
||||
defer func() { notifyUser = saved }()
|
||||
|
||||
// Install the test's fake notifyUser.
|
||||
var notifiedUser, notifiedMsg string
|
||||
notifyUser = func(user, msg string) {
|
||||
notifiedUser, notifiedMsg = user, msg
|
||||
}
|
||||
// ...rest of test...
|
||||
}
|
||||
```
|
||||
|
||||
這種處理模式可以用來暫時保存和恢復所有的全侷變量, 包括命令行標誌參數, 調試選項, 和優化參數; 安裝和移除導緻生產代碼產生一些調試信息的鈎子函數; 還有有些誘導生產代碼進入某些重要狀態的改變, 比如 超時, 錯誤, 甚至是一些刻意製造的並發行為.
|
||||
|
||||
以這種方式使用全侷變量是安全的, 因為 go test 並不會衕時並發地執行多個測試.
|
||||
|
||||
|
||||
|
@ -1,3 +1,66 @@
|
||||
### 11.2.4. 擴展測試包
|
||||
|
||||
TODO
|
||||
考慮下這兩個包: 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 包所在的目録聲明一個 url_test 測試擴展包. 其中測試擴展包名的 `_test` 後綴告訴 go test 工具它應該建立一個額外的包來運行測試. 我們將這個擴展測試包的導入路徑視作是 net/url_test 會更容易理解, 但實際上它並不能被其他任何包導入.
|
||||
|
||||
因為測試擴展包是一個獨立的包, 因此可以導入測試代碼依賴的其他的輔助包; 包內的測試代碼可能無法做到. 在設計層麪, 測試擴展包是在所以它依賴的包的上層, 正如 圖11.2所示.
|
||||
|
||||
![](../images/ch11-02.png)
|
||||
|
||||
通過迴避循環導入依賴, 擴展測試包可以更靈活的測試, 特彆是集成測試(需要測試多個組件之間的交互), 可以像普通應用程序那樣自由地導入其他包.
|
||||
|
||||
我們可以用 go list 工具査看包對應目録中哪些Go源文件是產品代碼, 哪些是包內測試, 還哪些測試擴展包. 我們以 fmt 包作為一個例子. GoFiles 錶示產品代碼對應的Go源文件列錶; 也就是 go build 命令要編譯的部分:
|
||||
|
||||
{% raw %}
|
||||
|
||||
```
|
||||
$ go list -f={{.GoFiles}} fmt
|
||||
[doc.go format.go print.go scan.go]
|
||||
```
|
||||
|
||||
{% endraw %}
|
||||
|
||||
TestGoFiles 錶示的是 fmt 包內部測試測試代碼, 以 _test.go 為後綴文件名, 不過隻在測試時被構建:
|
||||
|
||||
{% raw %}
|
||||
|
||||
```
|
||||
$ go list -f={{.TestGoFiles}} fmt
|
||||
[export_test.go]
|
||||
```
|
||||
|
||||
{% endraw %}
|
||||
|
||||
包的測試代碼通常都在這些文件中, 不過 fmt 包並非如此; 稍後我們再解釋 export_test.go 文件的作用.
|
||||
|
||||
XTestGoFiles 錶示的是屬於測試擴展包的測試代碼, 也就是 fmt_test 包, 因此它們必鬚先導入 fmt 包. 衕樣, 這些文件也隻是在測試時被構建運行:
|
||||
|
||||
|
||||
{% raw %}
|
||||
|
||||
```
|
||||
$ go list -f={{.XTestGoFiles}} fmt
|
||||
[fmt_test.go scan_test.go stringer_test.go]
|
||||
```
|
||||
|
||||
{% endraw %}
|
||||
|
||||
有時候測試擴展包需要訪問被測試包內部的代碼, 例如在一個為了避免循環導入而被獨立到外部測試擴展包的白盒測試. 在這種情況下, 我們可以通過一些技巧解決: 我們在包內的一個 _test.go 文件中導齣一個內部的實現給測試擴展包. 因為這些代碼隻有在測試時纔需要, 因此一般放在 export_test.go 文件中.
|
||||
|
||||
例如, fmt 包的 fmt.Scanf 需要 unicode.IsSpace 函數提供的功能. 但是為了避免太多的依賴, fmt 包並沒有導入包含鉅大錶格數據的 unicode 包; 相反fmt包有一個叫 isSpace 內部的簡易實現.
|
||||
|
||||
為了確保 fmt.isSpace 和 unicode.IsSpace 函數的行為一緻, fmt 包謹慎地包含了一個測試. 是一個在測試擴展包內的測試, 因此是無法直接訪問到 isSpace 內部函數的, 因此 fmt 通過一個祕密齣口導齣了 isSpace 函數. export_test.go 文件就是專門用於測試擴展包的祕密齣口.
|
||||
|
||||
```Go
|
||||
package fmt
|
||||
|
||||
var IsSpace = isSpace
|
||||
```
|
||||
|
||||
這個測試文件並沒有定義測試代碼; 它隻是通過 fmt.IsSpace 簡單導齣了內部的 isSpace 函數, 提供給測試擴展包使用. 這個技巧可以廣汎用於位於測試擴展包的白盒測試.
|
||||
|
||||
|
@ -1,3 +1,49 @@
|
||||
### 11.2.5. 編寫有效的測試
|
||||
|
||||
TODO
|
||||
|
||||
許多Go新人會驚異與它的極簡的測試框架. 很多其他語言的測試框架都提供了識彆測試函數的機製(通常使用反射或元數據), 通過設置一些 ‘‘setup’’ 和 ‘‘teardown’’ 的鈎子函數來執行測試用例運行的初始化或之後的清理操作, 衕時測試工具箱還提供了很多類似assert斷言, 比較值, 格式化輸齣錯誤信息和停止一個識彆的測試等輔助函數(通常使用異常機製). 雖然這些機製可以使得測試非常簡潔, 但是測試輸齣的日誌卻像火星文一般難以理解. 此外, 雖然測試最終也會輸齣 PASS 或 FAIL 的報告, 但是它們提供的信息格式卻非常不利於代碼維護者快速定位問題, 因為失敗的信息的具體含義是非常隱患的, 比如 "assert: 0 == 1" 或 成頁的海量跟蹤日誌.
|
||||
|
||||
Go語言的測試風格則形成鮮明對比. 它期望測試者自己完成大部分的工作, 定義函數避免重復, 就像普通編程那樣. 編寫測試並不是一個機械的填充過程; 一個測試也有自己的接口, 盡管它的維護者也是測試僅有的一個用戶. 一個好的測試不應該引發其他無關的錯誤信息, 它隻要清晰簡潔地描述問題的癥狀卽可, 有時候可能還需要一些上下文信息. 在理想情況下, 維護者可以在不看代碼的情況下就能根據錯誤信息定位錯誤產生的原因. 一個好的測試不應該在遇到一點小錯誤就立刻退齣測試, 它應該嘗試報告更多的測試, 因此我們可能從多個失敗測試的模式中發現錯誤產生的規律.
|
||||
|
||||
下麪的斷言函數比較兩個值, 然後生成一個通用的錯誤信息, 並停止程序. 它很方便使用也確實有效果, 但是噹識彆的時候, 錯誤時打印的信息幾乎是沒有價值的. 它並沒有為解決問題提供一個很好的入口.
|
||||
|
||||
```Go
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
// A poor assertion function.
|
||||
func assertEqual(x, y int) {
|
||||
if x != y {
|
||||
panic(fmt.Sprintf("%d != %d", x, y))
|
||||
}
|
||||
}
|
||||
func TestSplit(t *testing.T) {
|
||||
words := strings.Split("a:b:c", ":")
|
||||
assertEqual(len(words), 3)
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
從這個意義上説, 斷言函數犯了過早抽象的錯誤: 僅僅測試兩個整數是否相衕, 而放棄了根據上下文提供更有意義的錯誤信息的做法. 我們可以根據具體的錯誤打印一個更有價值的錯誤信息, 就像下麪例子那樣. 測試在隻有一次重復的模式齣現時引入抽象.
|
||||
|
||||
```Go
|
||||
func TestSplit(t *testing.T) {
|
||||
s, sep := "a:b:c", ":"
|
||||
words := strings.Split(s, sep)
|
||||
if got, want := len(words), 3; got != want {
|
||||
t.Errorf("Split(%q, %q) returned %d words, want %d",
|
||||
s, sep, got, want)
|
||||
}
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
現在的測試不僅報告了調用的具體函數, 它的輸入, 和結果的意義; 並且打印的眞實返迴的值和期望返迴的值; 並且卽使斷言失敗依然會繼續嘗試運行更多的測試. 一旦我們寫了這樣結構的測試, 下一步自然不是用更多的if語句來擴展測試用例, 我們可以用像 IsPalindrome 的錶驅動測試那樣來準備更多的 s, sep 測試用例.
|
||||
|
||||
前麪的例子並不需要額外的輔助函數, 如果如果有可以使測試代碼更簡單的方法我們也樂意接受. (我們將在 13.3節 看到一個 reflect.DeepEqual 輔助函數.) 開始一個好的測試的關鍵是通過實現你眞正想要的具體行為, 然後纔是考慮然後簡化測試代碼. 最好的結果是直接從庫的抽象接口開始, 針對公共接口編寫一些測試函數.
|
||||
|
||||
**練習11.5:** 用錶格驅動的技術擴展TestSplit測試, 並打印期望的輸齣結果.
|
||||
|
||||
|
||||
|
@ -1,3 +1,8 @@
|
||||
### 11.2.6. 避免的不穩定的測試
|
||||
|
||||
TODO
|
||||
如果一個應用程序對於新齣現的但有效的輸入經常失敗説明程序不夠穩健; 衕樣如果一個測試僅僅因為聲音變化就會導緻失敗也是不閤邏輯的. 就像一個不夠穩健的程序會挫敗它的用戶一樣, 一個脆弱性測試衕樣會激怒它的維護者. 最脆弱的測試代碼會在程序沒有任何變化的時候產生不衕的結果, 時好時壞, 處理它們會耗費大量的時間但是並不會得到任何好處.
|
||||
|
||||
噹一個測試函數產生一個復雜的輸齣如一個很長的字符串, 或一個精心設計的數據結構, 或一個文件, 它可以用於和預設的‘‘golden’’結果數據對比, 用這種簡單方式寫測試是誘人的. 但是隨着項目的發展, 輸齣的某些部分很可能會發生變化, 盡管很可能是一個改進的實現導緻的. 而且不僅僅是輸齣部分, 函數復雜復製的輸入部分可能也跟着變化了, 因此測試使用的輸入也就不在有效了.
|
||||
|
||||
避免脆弱測試代碼的方法是隻檢測你眞正關心的屬性. 保存測試代碼的簡潔和內部結構的穩定. 特彆是對斷言部分要有所選擇. 不要檢査字符串的全匹配, 但是尋找相關的子字符串, 因為某些子字符串在項目的發展中是比較穩定不變的. 通常編寫一個重復雜的輸齣中提取必要精華信息以用於斷言是值得的, 雖然這可能會帶來很多前期的工作, 但是它可以幫助迅速及時脩復因為項目演化而導緻的不閤邏輯的失敗測試.
|
||||
|
||||
|
@ -48,7 +48,7 @@
|
||||
<body>
|
||||
|
||||
|
||||
<div class="book" data-level="11.2" data-chapter-title="測試函數" data-filepath="ch11/ch11-02.md" data-basepath=".." data-revision="Mon Dec 14 2015 11:30:54 GMT+0800 (中国标准时间)">
|
||||
<div class="book" data-level="11.2" data-chapter-title="測試函數" data-filepath="ch11/ch11-02.md" data-basepath=".." data-revision="Wed Dec 16 2015 10:54:29 GMT+0800 (中国标准时间)">
|
||||
|
||||
|
||||
<div class="book-summary">
|
||||
@ -2262,15 +2262,271 @@ ok gopl.io/ch11/word2 0.015s
|
||||
<p><strong>練習 11.3:</strong> TestRandomPalindromes 隻測試了迴文字符串. 編寫新的隨機測試生成器, 用於測試隨機生成的非迴文字符串.</p>
|
||||
<p><strong>練習 11.4:</strong> 脩改 randomPalindrome 函數, 以探索 IsPalindrome 對標點和空格的處理.</p>
|
||||
<h3 id="1122-測試一個命令">11.2.2. 測試一個命令</h3>
|
||||
<p>TODO</p>
|
||||
<p>對於測試包 <code>go test</code> 是一個的有用的工具, 但是稍加努力我們也可以用它來測試可執行程序. 如果一個包的名字是 main, 那麽在構建時會生成一個可執行程序, 不過 main 包可以作為一個包被測試器代碼導入.</p>
|
||||
<p>讓我們為 2.3.2節 的 echo 程序編寫一個測試. 我們先將程序拆分為兩個函數: echo 函數完成眞正的工作, main 函數用於處理命令行輸入參數和echo可能返迴的錯誤.</p>
|
||||
<pre><code class="lang-Go">gopl.io/ch11/echo
|
||||
<span class="hljs-comment">// Echo prints its command-line arguments.</span>
|
||||
<span class="hljs-keyword">package</span> main
|
||||
|
||||
<span class="hljs-keyword">import</span> (
|
||||
<span class="hljs-string">"flag"</span>
|
||||
<span class="hljs-string">"fmt"</span>
|
||||
<span class="hljs-string">"io"</span>
|
||||
<span class="hljs-string">"os"</span>
|
||||
<span class="hljs-string">"strings"</span>
|
||||
)
|
||||
|
||||
<span class="hljs-keyword">var</span> (
|
||||
n = flag.Bool(<span class="hljs-string">"n"</span>, <span class="hljs-constant">false</span>, <span class="hljs-string">"omit trailing newline"</span>)
|
||||
s = flag.String(<span class="hljs-string">"s"</span>, <span class="hljs-string">" "</span>, <span class="hljs-string">"separator"</span>)
|
||||
)
|
||||
|
||||
<span class="hljs-keyword">var</span> out io.Writer = os.Stdout <span class="hljs-comment">// modified during testing</span>
|
||||
|
||||
<span class="hljs-keyword">func</span> main() {
|
||||
flag.Parse()
|
||||
<span class="hljs-keyword">if</span> err := echo(!*n, *s, flag.Args()); err != <span class="hljs-constant">nil</span> {
|
||||
fmt.Fprintf(os.Stderr, <span class="hljs-string">"echo: %v\n"</span>, err)
|
||||
os.Exit(<span class="hljs-number">1</span>)
|
||||
}
|
||||
}
|
||||
|
||||
<span class="hljs-keyword">func</span> echo(newline <span class="hljs-typename">bool</span>, sep <span class="hljs-typename">string</span>, args []<span class="hljs-typename">string</span>) error {
|
||||
fmt.Fprint(out, strings.Join(args, sep))
|
||||
<span class="hljs-keyword">if</span> newline {
|
||||
fmt.Fprintln(out)
|
||||
}
|
||||
<span class="hljs-keyword">return</span> <span class="hljs-constant">nil</span>
|
||||
}
|
||||
</code></pre>
|
||||
<p>在測試中嗎我們可以用各種參數和標標誌調用 echo 函數, 然後檢測它的輸齣是否正確, 我們通過增加參數來減少 echo 函數對全侷變量的依賴. 我們還增加了一個全侷名為 out 的變量來替代直接使用 os.Stdout, 這樣測試代碼可以根據需要將 out 脩改為不衕的對象以便於檢査. 下麪就是 echo_test.go 文件中的測試代碼:</p>
|
||||
<pre><code class="lang-Go"><span class="hljs-keyword">package</span> main
|
||||
|
||||
<span class="hljs-keyword">import</span> (
|
||||
<span class="hljs-string">"bytes"</span>
|
||||
<span class="hljs-string">"fmt"</span>
|
||||
<span class="hljs-string">"testing"</span>
|
||||
)
|
||||
|
||||
<span class="hljs-keyword">func</span> TestEcho(t *testing.T) {
|
||||
<span class="hljs-keyword">var</span> tests = []<span class="hljs-keyword">struct</span> {
|
||||
newline <span class="hljs-typename">bool</span>
|
||||
sep <span class="hljs-typename">string</span>
|
||||
args []<span class="hljs-typename">string</span>
|
||||
want <span class="hljs-typename">string</span>
|
||||
}{
|
||||
{<span class="hljs-constant">true</span>, <span class="hljs-string">""</span>, []<span class="hljs-typename">string</span>{}, <span class="hljs-string">"\n"</span>},
|
||||
{<span class="hljs-constant">false</span>, <span class="hljs-string">""</span>, []<span class="hljs-typename">string</span>{}, <span class="hljs-string">""</span>},
|
||||
{<span class="hljs-constant">true</span>, <span class="hljs-string">"\t"</span>, []<span class="hljs-typename">string</span>{<span class="hljs-string">"one"</span>, <span class="hljs-string">"two"</span>, <span class="hljs-string">"three"</span>}, <span class="hljs-string">"one\ttwo\tthree\n"</span>},
|
||||
{<span class="hljs-constant">true</span>, <span class="hljs-string">","</span>, []<span class="hljs-typename">string</span>{<span class="hljs-string">"a"</span>, <span class="hljs-string">"b"</span>, <span class="hljs-string">"c"</span>}, <span class="hljs-string">"a,b,c\n"</span>},
|
||||
{<span class="hljs-constant">false</span>, <span class="hljs-string">":"</span>, []<span class="hljs-typename">string</span>{<span class="hljs-string">"1"</span>, <span class="hljs-string">"2"</span>, <span class="hljs-string">"3"</span>}, <span class="hljs-string">"1:2:3"</span>},
|
||||
}
|
||||
<span class="hljs-keyword">for</span> _, test := <span class="hljs-keyword">range</span> tests {
|
||||
descr := fmt.Sprintf(<span class="hljs-string">"echo(%v, %q, %q)"</span>,
|
||||
test.newline, test.sep, test.args)
|
||||
|
||||
out = <span class="hljs-built_in">new</span>(bytes.Buffer) <span class="hljs-comment">// captured output</span>
|
||||
<span class="hljs-keyword">if</span> err := echo(test.newline, test.sep, test.args); err != <span class="hljs-constant">nil</span> {
|
||||
t.Errorf(<span class="hljs-string">"%s failed: %v"</span>, descr, err)
|
||||
<span class="hljs-keyword">continue</span>
|
||||
}
|
||||
got := out.(*bytes.Buffer).String()
|
||||
<span class="hljs-keyword">if</span> got != test.want {
|
||||
t.Errorf(<span class="hljs-string">"%s = %q, want %q"</span>, descr, got, test.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
</code></pre>
|
||||
<p>要註意的是測試代碼和產品代碼在衕一個包. 雖然是main包, 也有對應的 main 入口函數, 但是在測試的時候 main 包隻是 TestEcho 測試函數導入的一個普通包, 裡麪 main 函數並沒有被導齣是被忽略的.</p>
|
||||
<p>通過將測試放到錶格中, 我們很容易添加新的測試用例. 讓我通過增加下麪的測試用例來看看失敗的情況是怎麽樣的:</p>
|
||||
<pre><code class="lang-Go">{<span class="hljs-constant">true</span>, <span class="hljs-string">","</span>, []<span class="hljs-typename">string</span>{<span class="hljs-string">"a"</span>, <span class="hljs-string">"b"</span>, <span class="hljs-string">"c"</span>}, <span class="hljs-string">"a b c\n"</span>}, <span class="hljs-comment">// <span class="hljs-doctag">NOTE:</span> wrong expectation!</span>
|
||||
</code></pre>
|
||||
<p><code>go test</code> 輸齣如下:</p>
|
||||
<pre><code>$ go test gopl.io/ch11/echo
|
||||
--- FAIL: TestEcho (0.00s)
|
||||
echo_test.go:31: echo(true, ",", ["a" "b" "c"]) = "a,b,c", want "a b c\n"
|
||||
FAIL
|
||||
FAIL gopl.io/ch11/echo 0.006s
|
||||
</code></pre><p>錯誤信息描述了嘗試的操作(使用Go類似語法), 實際的行為, 和期望的行為. 通過這樣的錯誤信息, 你可以在檢視代碼之前就很容易定位錯誤的原因.</p>
|
||||
<p>要註意的是在測試代碼中並沒有調用 log.Fatal 或 os.Exit, 因為調用這類函數會導緻程序提前退齣; 調用這些函數的特權應該放在 main 函數中. 如果眞的有以外的事情導緻函數發送 panic, 測試驅動應該嘗試 recover, 然後將噹前測試噹作失敗處理. 如果是可預期的錯誤, 例如非法的用戶輸入, 找不到文件, 或配置文件不噹等應該通過返迴一個非空的 error 的方式處理. 倖運的是(上麪的意外隻是一個插麯), 我們的 echo 示例是比較簡單的也沒有需要返迴非空error的情況.</p>
|
||||
<h3 id="1123-白盒測試">11.2.3. 白盒測試</h3>
|
||||
<p>TODO</p>
|
||||
<p>一個測試分類的方法是基於測試者是否需要了解被測試對象的內部工作原理. 黑盒測試隻需要測試包公開的文檔和API行為, 內部實現對測試代碼是透明的. 相反, 白盒測試有訪問包內部函數和數據結構的權限, 因此可以做到一下普通客戶端無法實現的測試. 例如, 一個飽和測試可以在每個操作之後檢測不變量的數據類型. (白盒測試隻是一個傳統的名稱, 其實稱為 clear box 會更準確.)</p>
|
||||
<p>黑盒和白盒這兩種測試方法是互補的. 黑盒測試一般更健壯, 隨着軟件實現的完善測試代碼很少需要更新. 它們可以幫助測試者了解眞是客戶的需求, 可以幫助發現API設計的一些不足之處. 相反, 白盒測試則可以對內部一些棘手的實現提供更多的測試覆蓋.</p>
|
||||
<p>我們已經看到兩種測試的例子. TestIsPalindrome 測試僅僅使用導齣的 IsPalindrome 函數, 因此它是一個黑盒測試. TestEcho 測試則調用了內部的 echo 函數, 並且更新了內部的 out 全侷變量, 這兩個都是未導齣的, 因此它是白盒測試.</p>
|
||||
<p>噹我們開發TestEcho測試的時候, 我們脩改了 echo 函數使用包級的 out 作為輸齣對象, 因此測試代碼可以用另一個實現代替標準輸齣, 這樣可以方便對比 echo 的輸齣數據. 使用類似的技術, 我們可以將產品代碼的其他部分也替換為一個容易測試的僞對象. 使用僞對象的好處是我們可以方便配置, 容易預測, 更可靠, 也更容易觀察. 衕時也可以避免一些不良的副作用, 例如更新生產數據庫或信用卡消費行為.</p>
|
||||
<p>下麪的代碼演示了為用戶提供網絡存儲的web服務中的配額檢測邏輯. 噹用戶使用了超過 90% 的存儲配額之後將發送提醒郵件.</p>
|
||||
<pre><code class="lang-Go">gopl.io/ch11/storage1
|
||||
|
||||
<span class="hljs-keyword">package</span> storage
|
||||
|
||||
<span class="hljs-keyword">import</span> (
|
||||
<span class="hljs-string">"fmt"</span>
|
||||
<span class="hljs-string">"log"</span>
|
||||
<span class="hljs-string">"net/smtp"</span>
|
||||
)
|
||||
|
||||
<span class="hljs-keyword">func</span> bytesInUse(username <span class="hljs-typename">string</span>) <span class="hljs-typename">int64</span> { <span class="hljs-keyword">return</span> <span class="hljs-number">0</span> <span class="hljs-comment">/* ... */</span> }
|
||||
|
||||
<span class="hljs-comment">// Email sender configuration.</span>
|
||||
<span class="hljs-comment">// <span class="hljs-doctag">NOTE:</span> never put passwords in source code!</span>
|
||||
<span class="hljs-keyword">const</span> sender = <span class="hljs-string">"notifications@example.com"</span>
|
||||
<span class="hljs-keyword">const</span> password = <span class="hljs-string">"correcthorsebatterystaple"</span>
|
||||
<span class="hljs-keyword">const</span> hostname = <span class="hljs-string">"smtp.example.com"</span>
|
||||
|
||||
<span class="hljs-keyword">const</span> template = <span class="hljs-string">`Warning: you are using %d bytes of storage,
|
||||
%d%% of your quota.`</span>
|
||||
|
||||
<span class="hljs-keyword">func</span> CheckQuota(username <span class="hljs-typename">string</span>) {
|
||||
used := bytesInUse(username)
|
||||
<span class="hljs-keyword">const</span> quota = <span class="hljs-number">1000000000</span> <span class="hljs-comment">// 1GB</span>
|
||||
percent := <span class="hljs-number">100</span> * used / quota
|
||||
<span class="hljs-keyword">if</span> percent < <span class="hljs-number">90</span> {
|
||||
<span class="hljs-keyword">return</span> <span class="hljs-comment">// OK</span>
|
||||
}
|
||||
msg := fmt.Sprintf(template, used, percent)
|
||||
auth := smtp.PlainAuth(<span class="hljs-string">""</span>, sender, password, hostname)
|
||||
err := smtp.SendMail(hostname+<span class="hljs-string">":587"</span>, auth, sender,
|
||||
[]<span class="hljs-typename">string</span>{username}, []<span class="hljs-typename">byte</span>(msg))
|
||||
<span class="hljs-keyword">if</span> err != <span class="hljs-constant">nil</span> {
|
||||
log.Printf(<span class="hljs-string">"smtp.SendMail(%s) failed: %s"</span>, username, err)
|
||||
}
|
||||
}
|
||||
</code></pre>
|
||||
<p>我們想測試這個代碼, 但是我們並不希望發送眞實的郵件. 因此我們將郵件處理邏輯放到一個俬有的 notifyUser 函數.</p>
|
||||
<pre><code class="lang-Go">gopl.io/ch11/storage2
|
||||
|
||||
<span class="hljs-keyword">var</span> notifyUser = <span class="hljs-keyword">func</span>(username, msg <span class="hljs-typename">string</span>) {
|
||||
auth := smtp.PlainAuth(<span class="hljs-string">""</span>, sender, password, hostname)
|
||||
err := smtp.SendMail(hostname+<span class="hljs-string">":587"</span>, auth, sender,
|
||||
[]<span class="hljs-typename">string</span>{username}, []<span class="hljs-typename">byte</span>(msg))
|
||||
<span class="hljs-keyword">if</span> err != <span class="hljs-constant">nil</span> {
|
||||
log.Printf(<span class="hljs-string">"smtp.SendEmail(%s) failed: %s"</span>, username, err)
|
||||
}
|
||||
}
|
||||
|
||||
<span class="hljs-keyword">func</span> CheckQuota(username <span class="hljs-typename">string</span>) {
|
||||
used := bytesInUse(username)
|
||||
<span class="hljs-keyword">const</span> quota = <span class="hljs-number">1000000000</span> <span class="hljs-comment">// 1GB</span>
|
||||
percent := <span class="hljs-number">100</span> * used / quota
|
||||
<span class="hljs-keyword">if</span> percent < <span class="hljs-number">90</span> {
|
||||
<span class="hljs-keyword">return</span> <span class="hljs-comment">// OK</span>
|
||||
}
|
||||
msg := fmt.Sprintf(template, used, percent)
|
||||
notifyUser(username, msg)
|
||||
}
|
||||
</code></pre>
|
||||
<p>現在我們可以在測試中用僞郵件發送函數替代眞實的郵件發送函數. 它隻是簡單記録要通知的用戶和郵件的內容.</p>
|
||||
<pre><code class="lang-Go"><span class="hljs-keyword">package</span> storage
|
||||
|
||||
<span class="hljs-keyword">import</span> (
|
||||
<span class="hljs-string">"strings"</span>
|
||||
<span class="hljs-string">"testing"</span>
|
||||
)
|
||||
<span class="hljs-keyword">func</span> TestCheckQuotaNotifiesUser(t *testing.T) {
|
||||
<span class="hljs-keyword">var</span> notifiedUser, notifiedMsg <span class="hljs-typename">string</span>
|
||||
notifyUser = <span class="hljs-keyword">func</span>(user, msg <span class="hljs-typename">string</span>) {
|
||||
notifiedUser, notifiedMsg = user, msg
|
||||
}
|
||||
|
||||
<span class="hljs-comment">// ...simulate a 980MB-used condition...</span>
|
||||
|
||||
<span class="hljs-keyword">const</span> user = <span class="hljs-string">"joe@example.org"</span>
|
||||
CheckQuota(user)
|
||||
<span class="hljs-keyword">if</span> notifiedUser == <span class="hljs-string">""</span> && notifiedMsg == <span class="hljs-string">""</span> {
|
||||
t.Fatalf(<span class="hljs-string">"notifyUser not called"</span>)
|
||||
}
|
||||
<span class="hljs-keyword">if</span> notifiedUser != user {
|
||||
t.Errorf(<span class="hljs-string">"wrong user (%s) notified, want %s"</span>,
|
||||
notifiedUser, user)
|
||||
}
|
||||
<span class="hljs-keyword">const</span> wantSubstring = <span class="hljs-string">"98% of your quota"</span>
|
||||
<span class="hljs-keyword">if</span> !strings.Contains(notifiedMsg, wantSubstring) {
|
||||
t.Errorf(<span class="hljs-string">"unexpected notification message <<%s>>, "</span>+
|
||||
<span class="hljs-string">"want substring %q"</span>, notifiedMsg, wantSubstring)
|
||||
}
|
||||
}
|
||||
</code></pre>
|
||||
<p>這裡有一個問題: 噹測試函數返迴後, CheckQuota 將不能正常工作, 因為 notifyUsers 依然使用的是測試函數的僞發送郵件函數. (噹更新全侷對象的時候總會有這種風險.) 我們必鬚脩改測試代碼恢復 notifyUsers 原先的狀態以便後續其他的測試沒有影響, 要確保所有的執行路徑後都能恢復, 包括測試失敗或 panic 情形. 在這種情況下, 我們建議使用 defer 處理恢復的代碼.</p>
|
||||
<pre><code class="lang-Go"><span class="hljs-keyword">func</span> TestCheckQuotaNotifiesUser(t *testing.T) {
|
||||
<span class="hljs-comment">// Save and restore original notifyUser.</span>
|
||||
saved := notifyUser
|
||||
<span class="hljs-keyword">defer</span> <span class="hljs-keyword">func</span>() { notifyUser = saved }()
|
||||
|
||||
<span class="hljs-comment">// Install the test's fake notifyUser.</span>
|
||||
<span class="hljs-keyword">var</span> notifiedUser, notifiedMsg <span class="hljs-typename">string</span>
|
||||
notifyUser = <span class="hljs-keyword">func</span>(user, msg <span class="hljs-typename">string</span>) {
|
||||
notifiedUser, notifiedMsg = user, msg
|
||||
}
|
||||
<span class="hljs-comment">// ...rest of test...</span>
|
||||
}
|
||||
</code></pre>
|
||||
<p>這種處理模式可以用來暫時保存和恢復所有的全侷變量, 包括命令行標誌參數, 調試選項, 和優化參數; 安裝和移除導緻生產代碼產生一些調試信息的鈎子函數; 還有有些誘導生產代碼進入某些重要狀態的改變, 比如 超時, 錯誤, 甚至是一些刻意製造的並發行為.</p>
|
||||
<p>以這種方式使用全侷變量是安全的, 因為 go test 並不會衕時並發地執行多個測試.</p>
|
||||
<h3 id="1124-擴展測試包">11.2.4. 擴展測試包</h3>
|
||||
<p>TODO</p>
|
||||
<p>考慮下這兩個包: net/url 包, 提供了 URL 解析的功能; net/http 包, 提供了web服務和HTTP客戶端的功能. 如我們所料, 上層的 net/http 包依賴下層的 net/url 包. 然後, net/url 包中的一個測試是演示不衕URL和HTTP客戶端的交互行為. 也就是説, 一個下層包的測試代碼導入了上層的包.</p>
|
||||
<p><img src="../images/ch11-01.png" alt=""></p>
|
||||
<p>這樣的行為在 net/url 包的測試代碼中會導緻包的循環依賴, 正如 圖11.1中嚮上箭頭所示, 衕時正如我們在 10.1節所説, Go語言規範是禁止包的循環依賴的.</p>
|
||||
<p>我們可以通過測試擴展包的方式解決循環依賴的問題, 也就是在 net/url 包所在的目録聲明一個 url_test 測試擴展包. 其中測試擴展包名的 <code>_test</code> 後綴告訴 go test 工具它應該建立一個額外的包來運行測試. 我們將這個擴展測試包的導入路徑視作是 net/url_test 會更容易理解, 但實際上它並不能被其他任何包導入.</p>
|
||||
<p>因為測試擴展包是一個獨立的包, 因此可以導入測試代碼依賴的其他的輔助包; 包內的測試代碼可能無法做到. 在設計層麪, 測試擴展包是在所以它依賴的包的上層, 正如 圖11.2所示.</p>
|
||||
<p><img src="../images/ch11-02.png" alt=""></p>
|
||||
<p>通過迴避循環導入依賴, 擴展測試包可以更靈活的測試, 特彆是集成測試(需要測試多個組件之間的交互), 可以像普通應用程序那樣自由地導入其他包.</p>
|
||||
<p>我們可以用 go list 工具査看包對應目録中哪些Go源文件是產品代碼, 哪些是包內測試, 還哪些測試擴展包. 我們以 fmt 包作為一個例子. GoFiles 錶示產品代碼對應的Go源文件列錶; 也就是 go build 命令要編譯的部分:</p>
|
||||
<pre><code>$ go list -f={{.GoFiles}} fmt
|
||||
[doc.go format.go print.go scan.go]
|
||||
</code></pre><p>TestGoFiles 錶示的是 fmt 包內部測試測試代碼, 以 _test.go 為後綴文件名, 不過隻在測試時被構建:</p>
|
||||
<pre><code>$ go list -f={{.TestGoFiles}} fmt
|
||||
[export_test.go]
|
||||
</code></pre><p>包的測試代碼通常都在這些文件中, 不過 fmt 包並非如此; 稍後我們再解釋 export_test.go 文件的作用.</p>
|
||||
<p>XTestGoFiles 錶示的是屬於測試擴展包的測試代碼, 也就是 fmt_test 包, 因此它們必鬚先導入 fmt 包. 衕樣, 這些文件也隻是在測試時被構建運行:</p>
|
||||
<pre><code>$ go list -f={{.XTestGoFiles}} fmt
|
||||
[fmt_test.go scan_test.go stringer_test.go]
|
||||
</code></pre><p>有時候測試擴展包需要訪問被測試包內部的代碼, 例如在一個為了避免循環導入而被獨立到外部測試擴展包的白盒測試. 在這種情況下, 我們可以通過一些技巧解決: 我們在包內的一個 _test.go 文件中導齣一個內部的實現給測試擴展包. 因為這些代碼隻有在測試時纔需要, 因此一般放在 export_test.go 文件中.</p>
|
||||
<p>例如, fmt 包的 fmt.Scanf 需要 unicode.IsSpace 函數提供的功能. 但是為了避免太多的依賴, fmt 包並沒有導入包含鉅大錶格數據的 unicode 包; 相反fmt包有一個叫 isSpace 內部的簡易實現.</p>
|
||||
<p>為了確保 fmt.isSpace 和 unicode.IsSpace 函數的行為一緻, fmt 包謹慎地包含了一個測試. 是一個在測試擴展包內的測試, 因此是無法直接訪問到 isSpace 內部函數的, 因此 fmt 通過一個祕密齣口導齣了 isSpace 函數. export_test.go 文件就是專門用於測試擴展包的祕密齣口.</p>
|
||||
<pre><code class="lang-Go"><span class="hljs-keyword">package</span> fmt
|
||||
|
||||
<span class="hljs-keyword">var</span> IsSpace = isSpace
|
||||
</code></pre>
|
||||
<p>這個測試文件並沒有定義測試代碼; 它隻是通過 fmt.IsSpace 簡單導齣了內部的 isSpace 函數, 提供給測試擴展包使用. 這個技巧可以廣汎用於位於測試擴展包的白盒測試.</p>
|
||||
<h3 id="1125-編寫有效的測試">11.2.5. 編寫有效的測試</h3>
|
||||
<p>TODO</p>
|
||||
<p>許多Go新人會驚異與它的極簡的測試框架. 很多其他語言的測試框架都提供了識彆測試函數的機製(通常使用反射或元數據), 通過設置一些 ‘‘setup’’ 和 ‘‘teardown’’ 的鈎子函數來執行測試用例運行的初始化或之後的清理操作, 衕時測試工具箱還提供了很多類似assert斷言, 比較值, 格式化輸齣錯誤信息和停止一個識彆的測試等輔助函數(通常使用異常機製). 雖然這些機製可以使得測試非常簡潔, 但是測試輸齣的日誌卻像火星文一般難以理解. 此外, 雖然測試最終也會輸齣 PASS 或 FAIL 的報告, 但是它們提供的信息格式卻非常不利於代碼維護者快速定位問題, 因為失敗的信息的具體含義是非常隱患的, 比如 "assert: 0 == 1" 或 成頁的海量跟蹤日誌.</p>
|
||||
<p>Go語言的測試風格則形成鮮明對比. 它期望測試者自己完成大部分的工作, 定義函數避免重復, 就像普通編程那樣. 編寫測試並不是一個機械的填充過程; 一個測試也有自己的接口, 盡管它的維護者也是測試僅有的一個用戶. 一個好的測試不應該引發其他無關的錯誤信息, 它隻要清晰簡潔地描述問題的癥狀卽可, 有時候可能還需要一些上下文信息. 在理想情況下, 維護者可以在不看代碼的情況下就能根據錯誤信息定位錯誤產生的原因. 一個好的測試不應該在遇到一點小錯誤就立刻退齣測試, 它應該嘗試報告更多的測試, 因此我們可能從多個失敗測試的模式中發現錯誤產生的規律.</p>
|
||||
<p>下麪的斷言函數比較兩個值, 然後生成一個通用的錯誤信息, 並停止程序. 它很方便使用也確實有效果, 但是噹識彆的時候, 錯誤時打印的信息幾乎是沒有價值的. 它並沒有為解決問題提供一個很好的入口.</p>
|
||||
<pre><code class="lang-Go"><span class="hljs-keyword">import</span> (
|
||||
<span class="hljs-string">"fmt"</span>
|
||||
<span class="hljs-string">"strings"</span>
|
||||
<span class="hljs-string">"testing"</span>
|
||||
)
|
||||
<span class="hljs-comment">// A poor assertion function.</span>
|
||||
<span class="hljs-keyword">func</span> assertEqual(x, y <span class="hljs-typename">int</span>) {
|
||||
<span class="hljs-keyword">if</span> x != y {
|
||||
<span class="hljs-built_in">panic</span>(fmt.Sprintf(<span class="hljs-string">"%d != %d"</span>, x, y))
|
||||
}
|
||||
}
|
||||
<span class="hljs-keyword">func</span> TestSplit(t *testing.T) {
|
||||
words := strings.Split(<span class="hljs-string">"a:b:c"</span>, <span class="hljs-string">":"</span>)
|
||||
assertEqual(<span class="hljs-built_in">len</span>(words), <span class="hljs-number">3</span>)
|
||||
<span class="hljs-comment">// ...</span>
|
||||
}
|
||||
</code></pre>
|
||||
<p>從這個意義上説, 斷言函數犯了過早抽象的錯誤: 僅僅測試兩個整數是否相衕, 而放棄了根據上下文提供更有意義的錯誤信息的做法. 我們可以根據具體的錯誤打印一個更有價值的錯誤信息, 就像下麪例子那樣. 測試在隻有一次重復的模式齣現時引入抽象.</p>
|
||||
<pre><code class="lang-Go"><span class="hljs-keyword">func</span> TestSplit(t *testing.T) {
|
||||
s, sep := <span class="hljs-string">"a:b:c"</span>, <span class="hljs-string">":"</span>
|
||||
words := strings.Split(s, sep)
|
||||
<span class="hljs-keyword">if</span> got, want := <span class="hljs-built_in">len</span>(words), <span class="hljs-number">3</span>; got != want {
|
||||
t.Errorf(<span class="hljs-string">"Split(%q, %q) returned %d words, want %d"</span>,
|
||||
s, sep, got, want)
|
||||
}
|
||||
<span class="hljs-comment">// ...</span>
|
||||
}
|
||||
</code></pre>
|
||||
<p>現在的測試不僅報告了調用的具體函數, 它的輸入, 和結果的意義; 並且打印的眞實返迴的值和期望返迴的值; 並且卽使斷言失敗依然會繼續嘗試運行更多的測試. 一旦我們寫了這樣結構的測試, 下一步自然不是用更多的if語句來擴展測試用例, 我們可以用像 IsPalindrome 的錶驅動測試那樣來準備更多的 s, sep 測試用例.</p>
|
||||
<p>前麪的例子並不需要額外的輔助函數, 如果如果有可以使測試代碼更簡單的方法我們也樂意接受. (我們將在 13.3節 看到一個 reflect.DeepEqual 輔助函數.) 開始一個好的測試的關鍵是通過實現你眞正想要的具體行為, 然後纔是考慮然後簡化測試代碼. 最好的結果是直接從庫的抽象接口開始, 針對公共接口編寫一些測試函數.</p>
|
||||
<p><strong>練習11.5:</strong> 用錶格驅動的技術擴展TestSplit測試, 並打印期望的輸齣結果.</p>
|
||||
<h3 id="1126-避免的不穩定的測試">11.2.6. 避免的不穩定的測試</h3>
|
||||
<p>TODO</p>
|
||||
<p>如果一個應用程序對於新齣現的但有效的輸入經常失敗説明程序不夠穩健; 衕樣如果一個測試僅僅因為聲音變化就會導緻失敗也是不閤邏輯的. 就像一個不夠穩健的程序會挫敗它的用戶一樣, 一個脆弱性測試衕樣會激怒它的維護者. 最脆弱的測試代碼會在程序沒有任何變化的時候產生不衕的結果, 時好時壞, 處理它們會耗費大量的時間但是並不會得到任何好處.</p>
|
||||
<p>噹一個測試函數產生一個復雜的輸齣如一個很長的字符串, 或一個精心設計的數據結構, 或一個文件, 它可以用於和預設的‘‘golden’’結果數據對比, 用這種簡單方式寫測試是誘人的. 但是隨着項目的發展, 輸齣的某些部分很可能會發生變化, 盡管很可能是一個改進的實現導緻的. 而且不僅僅是輸齣部分, 函數復雜復製的輸入部分可能也跟着變化了, 因此測試使用的輸入也就不在有效了.</p>
|
||||
<p>避免脆弱測試代碼的方法是隻檢測你眞正關心的屬性. 保存測試代碼的簡潔和內部結構的穩定. 特彆是對斷言部分要有所選擇. 不要檢査字符串的全匹配, 但是尋找相關的子字符串, 因為某些子字符串在項目的發展中是比較穩定不變的. 通常編寫一個重復雜的輸齣中提取必要精華信息以用於斷言是值得的, 雖然這可能會帶來很多前期的工作, 但是它可以幫助迅速及時脩復因為項目演化而導緻的不閤邏輯的失敗測試.</p>
|
||||
|
||||
|
||||
</section>
|
||||
|
@ -48,7 +48,7 @@
|
||||
<body>
|
||||
|
||||
|
||||
<div class="book" data-level="11.3" data-chapter-title="測試覆蓋率" data-filepath="ch11/ch11-03.md" data-basepath=".." data-revision="Mon Dec 14 2015 11:30:54 GMT+0800 (中国标准时间)">
|
||||
<div class="book" data-level="11.3" data-chapter-title="測試覆蓋率" data-filepath="ch11/ch11-03.md" data-basepath=".." data-revision="Wed Dec 16 2015 10:54:29 GMT+0800 (中国标准时间)">
|
||||
|
||||
|
||||
<div class="book-summary">
|
||||
|
@ -48,7 +48,7 @@
|
||||
<body>
|
||||
|
||||
|
||||
<div class="book" data-level="11.4" data-chapter-title="基準測試" data-filepath="ch11/ch11-04.md" data-basepath=".." data-revision="Mon Dec 14 2015 11:30:54 GMT+0800 (中国标准时间)">
|
||||
<div class="book" data-level="11.4" data-chapter-title="基準測試" data-filepath="ch11/ch11-04.md" data-basepath=".." data-revision="Wed Dec 16 2015 10:54:29 GMT+0800 (中国标准时间)">
|
||||
|
||||
|
||||
<div class="book-summary">
|
||||
|
@ -48,7 +48,7 @@
|
||||
<body>
|
||||
|
||||
|
||||
<div class="book" data-level="11.5" data-chapter-title="剖析" data-filepath="ch11/ch11-05.md" data-basepath=".." data-revision="Mon Dec 14 2015 11:30:54 GMT+0800 (中国标准时间)">
|
||||
<div class="book" data-level="11.5" data-chapter-title="剖析" data-filepath="ch11/ch11-05.md" data-basepath=".." data-revision="Wed Dec 16 2015 10:54:29 GMT+0800 (中国标准时间)">
|
||||
|
||||
|
||||
<div class="book-summary">
|
||||
|
@ -48,7 +48,7 @@
|
||||
<body>
|
||||
|
||||
|
||||
<div class="book" data-level="11.6" data-chapter-title="示例函數" data-filepath="ch11/ch11-06.md" data-basepath=".." data-revision="Mon Dec 14 2015 11:30:54 GMT+0800 (中国标准时间)">
|
||||
<div class="book" data-level="11.6" data-chapter-title="示例函數" data-filepath="ch11/ch11-06.md" data-basepath=".." data-revision="Wed Dec 16 2015 10:54:29 GMT+0800 (中国标准时间)">
|
||||
|
||||
|
||||
<div class="book-summary">
|
||||
|
@ -48,7 +48,7 @@
|
||||
<body>
|
||||
|
||||
|
||||
<div class="book" data-level="11" data-chapter-title="測試" data-filepath="ch11/ch11.md" data-basepath=".." data-revision="Mon Dec 14 2015 11:30:54 GMT+0800 (中国标准时间)">
|
||||
<div class="book" data-level="11" data-chapter-title="測試" data-filepath="ch11/ch11.md" data-basepath=".." data-revision="Wed Dec 16 2015 10:54:29 GMT+0800 (中国标准时间)">
|
||||
|
||||
|
||||
<div class="book-summary">
|
||||
|
@ -48,7 +48,7 @@
|
||||
<body>
|
||||
|
||||
|
||||
<div class="book" data-level="12.1" data-chapter-title="為何需要反射?" data-filepath="ch12/ch12-01.md" data-basepath=".." data-revision="Mon Dec 14 2015 11:30:54 GMT+0800 (中国标准时间)">
|
||||
<div class="book" data-level="12.1" data-chapter-title="為何需要反射?" data-filepath="ch12/ch12-01.md" data-basepath=".." data-revision="Wed Dec 16 2015 10:54:29 GMT+0800 (中国标准时间)">
|
||||
|
||||
|
||||
<div class="book-summary">
|
||||
|
@ -48,7 +48,7 @@
|
||||
<body>
|
||||
|
||||
|
||||
<div class="book" data-level="12.2" data-chapter-title="reflect.Type和reflect.Value" data-filepath="ch12/ch12-02.md" data-basepath=".." data-revision="Mon Dec 14 2015 11:30:54 GMT+0800 (中国标准时间)">
|
||||
<div class="book" data-level="12.2" data-chapter-title="reflect.Type和reflect.Value" data-filepath="ch12/ch12-02.md" data-basepath=".." data-revision="Wed Dec 16 2015 10:54:29 GMT+0800 (中国标准时间)">
|
||||
|
||||
|
||||
<div class="book-summary">
|
||||
|
@ -48,7 +48,7 @@
|
||||
<body>
|
||||
|
||||
|
||||
<div class="book" data-level="12.3" data-chapter-title="Display遞歸打印" data-filepath="ch12/ch12-03.md" data-basepath=".." data-revision="Mon Dec 14 2015 11:30:54 GMT+0800 (中国标准时间)">
|
||||
<div class="book" data-level="12.3" data-chapter-title="Display遞歸打印" data-filepath="ch12/ch12-03.md" data-basepath=".." data-revision="Wed Dec 16 2015 10:54:29 GMT+0800 (中国标准时间)">
|
||||
|
||||
|
||||
<div class="book-summary">
|
||||
|
@ -48,7 +48,7 @@
|
||||
<body>
|
||||
|
||||
|
||||
<div class="book" data-level="12.4" data-chapter-title="示例: 編碼S錶達式" data-filepath="ch12/ch12-04.md" data-basepath=".." data-revision="Mon Dec 14 2015 11:30:54 GMT+0800 (中国标准时间)">
|
||||
<div class="book" data-level="12.4" data-chapter-title="示例: 編碼S錶達式" data-filepath="ch12/ch12-04.md" data-basepath=".." data-revision="Wed Dec 16 2015 10:54:29 GMT+0800 (中国标准时间)">
|
||||
|
||||
|
||||
<div class="book-summary">
|
||||
|
@ -48,7 +48,7 @@
|
||||
<body>
|
||||
|
||||
|
||||
<div class="book" data-level="12.5" data-chapter-title="通過reflect.Value脩改值" data-filepath="ch12/ch12-05.md" data-basepath=".." data-revision="Mon Dec 14 2015 11:30:54 GMT+0800 (中国标准时间)">
|
||||
<div class="book" data-level="12.5" data-chapter-title="通過reflect.Value脩改值" data-filepath="ch12/ch12-05.md" data-basepath=".." data-revision="Wed Dec 16 2015 10:54:29 GMT+0800 (中国标准时间)">
|
||||
|
||||
|
||||
<div class="book-summary">
|
||||
|
@ -48,7 +48,7 @@
|
||||
<body>
|
||||
|
||||
|
||||
<div class="book" data-level="12.6" data-chapter-title="示例: 解碼S錶達式" data-filepath="ch12/ch12-06.md" data-basepath=".." data-revision="Mon Dec 14 2015 11:30:54 GMT+0800 (中国标准时间)">
|
||||
<div class="book" data-level="12.6" data-chapter-title="示例: 解碼S錶達式" data-filepath="ch12/ch12-06.md" data-basepath=".." data-revision="Wed Dec 16 2015 10:54:29 GMT+0800 (中国标准时间)">
|
||||
|
||||
|
||||
<div class="book-summary">
|
||||
|
@ -48,7 +48,7 @@
|
||||
<body>
|
||||
|
||||
|
||||
<div class="book" data-level="12.7" data-chapter-title="穫取結構體字段標識" data-filepath="ch12/ch12-07.md" data-basepath=".." data-revision="Mon Dec 14 2015 11:30:54 GMT+0800 (中国标准时间)">
|
||||
<div class="book" data-level="12.7" data-chapter-title="穫取結構體字段標識" data-filepath="ch12/ch12-07.md" data-basepath=".." data-revision="Wed Dec 16 2015 10:54:29 GMT+0800 (中国标准时间)">
|
||||
|
||||
|
||||
<div class="book-summary">
|
||||
|
@ -48,7 +48,7 @@
|
||||
<body>
|
||||
|
||||
|
||||
<div class="book" data-level="12.8" data-chapter-title="顯示一個類型的方法集" data-filepath="ch12/ch12-08.md" data-basepath=".." data-revision="Mon Dec 14 2015 11:30:54 GMT+0800 (中国标准时间)">
|
||||
<div class="book" data-level="12.8" data-chapter-title="顯示一個類型的方法集" data-filepath="ch12/ch12-08.md" data-basepath=".." data-revision="Wed Dec 16 2015 10:54:29 GMT+0800 (中国标准时间)">
|
||||
|
||||
|
||||
<div class="book-summary">
|
||||
|
@ -48,7 +48,7 @@
|
||||
<body>
|
||||
|
||||
|
||||
<div class="book" data-level="12.9" data-chapter-title="幾點忠告" data-filepath="ch12/ch12-09.md" data-basepath=".." data-revision="Mon Dec 14 2015 11:30:54 GMT+0800 (中国标准时间)">
|
||||
<div class="book" data-level="12.9" data-chapter-title="幾點忠告" data-filepath="ch12/ch12-09.md" data-basepath=".." data-revision="Wed Dec 16 2015 10:54:29 GMT+0800 (中国标准时间)">
|
||||
|
||||
|
||||
<div class="book-summary">
|
||||
|
@ -48,7 +48,7 @@
|
||||
<body>
|
||||
|
||||
|
||||
<div class="book" data-level="12" data-chapter-title="反射" data-filepath="ch12/ch12.md" data-basepath=".." data-revision="Mon Dec 14 2015 11:30:54 GMT+0800 (中国标准时间)">
|
||||
<div class="book" data-level="12" data-chapter-title="反射" data-filepath="ch12/ch12.md" data-basepath=".." data-revision="Wed Dec 16 2015 10:54:29 GMT+0800 (中国标准时间)">
|
||||
|
||||
|
||||
<div class="book-summary">
|
||||
|
@ -48,7 +48,7 @@
|
||||
<body>
|
||||
|
||||
|
||||
<div class="book" data-level="13.1" data-chapter-title="unsafe.Sizeof, Alignof 和 Offsetof" data-filepath="ch13/ch13-01.md" data-basepath=".." data-revision="Mon Dec 14 2015 11:30:54 GMT+0800 (中国标准时间)">
|
||||
<div class="book" data-level="13.1" data-chapter-title="unsafe.Sizeof, Alignof 和 Offsetof" data-filepath="ch13/ch13-01.md" data-basepath=".." data-revision="Wed Dec 16 2015 10:54:29 GMT+0800 (中国标准时间)">
|
||||
|
||||
|
||||
<div class="book-summary">
|
||||
|
@ -48,7 +48,7 @@
|
||||
<body>
|
||||
|
||||
|
||||
<div class="book" data-level="13.2" data-chapter-title="unsafe.Pointer" data-filepath="ch13/ch13-02.md" data-basepath=".." data-revision="Mon Dec 14 2015 11:30:54 GMT+0800 (中国标准时间)">
|
||||
<div class="book" data-level="13.2" data-chapter-title="unsafe.Pointer" data-filepath="ch13/ch13-02.md" data-basepath=".." data-revision="Wed Dec 16 2015 10:54:29 GMT+0800 (中国标准时间)">
|
||||
|
||||
|
||||
<div class="book-summary">
|
||||
|
@ -48,7 +48,7 @@
|
||||
<body>
|
||||
|
||||
|
||||
<div class="book" data-level="13.3" data-chapter-title="示例: 深度相等判斷" data-filepath="ch13/ch13-03.md" data-basepath=".." data-revision="Mon Dec 14 2015 11:30:54 GMT+0800 (中国标准时间)">
|
||||
<div class="book" data-level="13.3" data-chapter-title="示例: 深度相等判斷" data-filepath="ch13/ch13-03.md" data-basepath=".." data-revision="Wed Dec 16 2015 10:54:29 GMT+0800 (中国标准时间)">
|
||||
|
||||
|
||||
<div class="book-summary">
|
||||
|
@ -48,7 +48,7 @@
|
||||
<body>
|
||||
|
||||
|
||||
<div class="book" data-level="13.4" data-chapter-title="通過cgo調用C代碼" data-filepath="ch13/ch13-04.md" data-basepath=".." data-revision="Mon Dec 14 2015 11:30:54 GMT+0800 (中国标准时间)">
|
||||
<div class="book" data-level="13.4" data-chapter-title="通過cgo調用C代碼" data-filepath="ch13/ch13-04.md" data-basepath=".." data-revision="Wed Dec 16 2015 10:54:29 GMT+0800 (中国标准时间)">
|
||||
|
||||
|
||||
<div class="book-summary">
|
||||
|
@ -48,7 +48,7 @@
|
||||
<body>
|
||||
|
||||
|
||||
<div class="book" data-level="13.5" data-chapter-title="幾點忠告" data-filepath="ch13/ch13-05.md" data-basepath=".." data-revision="Mon Dec 14 2015 11:30:54 GMT+0800 (中国标准时间)">
|
||||
<div class="book" data-level="13.5" data-chapter-title="幾點忠告" data-filepath="ch13/ch13-05.md" data-basepath=".." data-revision="Wed Dec 16 2015 10:54:29 GMT+0800 (中国标准时间)">
|
||||
|
||||
|
||||
<div class="book-summary">
|
||||
|
@ -48,7 +48,7 @@
|
||||
<body>
|
||||
|
||||
|
||||
<div class="book" data-level="13" data-chapter-title="底層編程" data-filepath="ch13/ch13.md" data-basepath=".." data-revision="Mon Dec 14 2015 11:30:54 GMT+0800 (中国标准时间)">
|
||||
<div class="book" data-level="13" data-chapter-title="底層編程" data-filepath="ch13/ch13.md" data-basepath=".." data-revision="Wed Dec 16 2015 10:54:29 GMT+0800 (中国标准时间)">
|
||||
|
||||
|
||||
<div class="book-summary">
|
||||
|
@ -48,7 +48,7 @@
|
||||
<body>
|
||||
|
||||
|
||||
<div class="book" data-level="2.1" data-chapter-title="命名" data-filepath="ch2/ch2-01.md" data-basepath=".." data-revision="Mon Dec 14 2015 11:30:54 GMT+0800 (中国标准时间)">
|
||||
<div class="book" data-level="2.1" data-chapter-title="命名" data-filepath="ch2/ch2-01.md" data-basepath=".." data-revision="Wed Dec 16 2015 10:54:29 GMT+0800 (中国标准时间)">
|
||||
|
||||
|
||||
<div class="book-summary">
|
||||
|
@ -48,7 +48,7 @@
|
||||
<body>
|
||||
|
||||
|
||||
<div class="book" data-level="2.2" data-chapter-title="聲明" data-filepath="ch2/ch2-02.md" data-basepath=".." data-revision="Mon Dec 14 2015 11:30:54 GMT+0800 (中国标准时间)">
|
||||
<div class="book" data-level="2.2" data-chapter-title="聲明" data-filepath="ch2/ch2-02.md" data-basepath=".." data-revision="Wed Dec 16 2015 10:54:29 GMT+0800 (中国标准时间)">
|
||||
|
||||
|
||||
<div class="book-summary">
|
||||
|
@ -48,7 +48,7 @@
|
||||
<body>
|
||||
|
||||
|
||||
<div class="book" data-level="2.3" data-chapter-title="變量" data-filepath="ch2/ch2-03.md" data-basepath=".." data-revision="Mon Dec 14 2015 11:30:54 GMT+0800 (中国标准时间)">
|
||||
<div class="book" data-level="2.3" data-chapter-title="變量" data-filepath="ch2/ch2-03.md" data-basepath=".." data-revision="Wed Dec 16 2015 10:54:29 GMT+0800 (中国标准时间)">
|
||||
|
||||
|
||||
<div class="book-summary">
|
||||
|
@ -48,7 +48,7 @@
|
||||
<body>
|
||||
|
||||
|
||||
<div class="book" data-level="2.4" data-chapter-title="賦值" data-filepath="ch2/ch2-04.md" data-basepath=".." data-revision="Mon Dec 14 2015 11:30:54 GMT+0800 (中国标准时间)">
|
||||
<div class="book" data-level="2.4" data-chapter-title="賦值" data-filepath="ch2/ch2-04.md" data-basepath=".." data-revision="Wed Dec 16 2015 10:54:29 GMT+0800 (中国标准时间)">
|
||||
|
||||
|
||||
<div class="book-summary">
|
||||
|
@ -48,7 +48,7 @@
|
||||
<body>
|
||||
|
||||
|
||||
<div class="book" data-level="2.5" data-chapter-title="類型" data-filepath="ch2/ch2-05.md" data-basepath=".." data-revision="Mon Dec 14 2015 11:30:54 GMT+0800 (中国标准时间)">
|
||||
<div class="book" data-level="2.5" data-chapter-title="類型" data-filepath="ch2/ch2-05.md" data-basepath=".." data-revision="Wed Dec 16 2015 10:54:29 GMT+0800 (中国标准时间)">
|
||||
|
||||
|
||||
<div class="book-summary">
|
||||
|
@ -48,7 +48,7 @@
|
||||
<body>
|
||||
|
||||
|
||||
<div class="book" data-level="2.6" data-chapter-title="包和文件" data-filepath="ch2/ch2-06.md" data-basepath=".." data-revision="Mon Dec 14 2015 11:30:54 GMT+0800 (中国标准时间)">
|
||||
<div class="book" data-level="2.6" data-chapter-title="包和文件" data-filepath="ch2/ch2-06.md" data-basepath=".." data-revision="Wed Dec 16 2015 10:54:29 GMT+0800 (中国标准时间)">
|
||||
|
||||
|
||||
<div class="book-summary">
|
||||
|
@ -48,7 +48,7 @@
|
||||
<body>
|
||||
|
||||
|
||||
<div class="book" data-level="2.7" data-chapter-title="作用域" data-filepath="ch2/ch2-07.md" data-basepath=".." data-revision="Mon Dec 14 2015 11:30:54 GMT+0800 (中国标准时间)">
|
||||
<div class="book" data-level="2.7" data-chapter-title="作用域" data-filepath="ch2/ch2-07.md" data-basepath=".." data-revision="Wed Dec 16 2015 10:54:29 GMT+0800 (中国标准时间)">
|
||||
|
||||
|
||||
<div class="book-summary">
|
||||
|
@ -48,7 +48,7 @@
|
||||
<body>
|
||||
|
||||
|
||||
<div class="book" data-level="2" data-chapter-title="程序結構" data-filepath="ch2/ch2.md" data-basepath=".." data-revision="Mon Dec 14 2015 11:30:54 GMT+0800 (中国标准时间)">
|
||||
<div class="book" data-level="2" data-chapter-title="程序結構" data-filepath="ch2/ch2.md" data-basepath=".." data-revision="Wed Dec 16 2015 10:54:29 GMT+0800 (中国标准时间)">
|
||||
|
||||
|
||||
<div class="book-summary">
|
||||
|
@ -48,7 +48,7 @@
|
||||
<body>
|
||||
|
||||
|
||||
<div class="book" data-level="3.1" data-chapter-title="整型" data-filepath="ch3/ch3-01.md" data-basepath=".." data-revision="Mon Dec 14 2015 11:30:54 GMT+0800 (中国标准时间)">
|
||||
<div class="book" data-level="3.1" data-chapter-title="整型" data-filepath="ch3/ch3-01.md" data-basepath=".." data-revision="Wed Dec 16 2015 10:54:29 GMT+0800 (中国标准时间)">
|
||||
|
||||
|
||||
<div class="book-summary">
|
||||
|
@ -48,7 +48,7 @@
|
||||
<body>
|
||||
|
||||
|
||||
<div class="book" data-level="3.2" data-chapter-title="浮點數" data-filepath="ch3/ch3-02.md" data-basepath=".." data-revision="Mon Dec 14 2015 11:30:54 GMT+0800 (中国标准时间)">
|
||||
<div class="book" data-level="3.2" data-chapter-title="浮點數" data-filepath="ch3/ch3-02.md" data-basepath=".." data-revision="Wed Dec 16 2015 10:54:29 GMT+0800 (中国标准时间)">
|
||||
|
||||
|
||||
<div class="book-summary">
|
||||
|
@ -48,7 +48,7 @@
|
||||
<body>
|
||||
|
||||
|
||||
<div class="book" data-level="3.3" data-chapter-title="復數" data-filepath="ch3/ch3-03.md" data-basepath=".." data-revision="Mon Dec 14 2015 11:30:54 GMT+0800 (中国标准时间)">
|
||||
<div class="book" data-level="3.3" data-chapter-title="復數" data-filepath="ch3/ch3-03.md" data-basepath=".." data-revision="Wed Dec 16 2015 10:54:29 GMT+0800 (中国标准时间)">
|
||||
|
||||
|
||||
<div class="book-summary">
|
||||
|
@ -48,7 +48,7 @@
|
||||
<body>
|
||||
|
||||
|
||||
<div class="book" data-level="3.4" data-chapter-title="佈爾型" data-filepath="ch3/ch3-04.md" data-basepath=".." data-revision="Mon Dec 14 2015 11:30:54 GMT+0800 (中国标准时间)">
|
||||
<div class="book" data-level="3.4" data-chapter-title="佈爾型" data-filepath="ch3/ch3-04.md" data-basepath=".." data-revision="Wed Dec 16 2015 10:54:29 GMT+0800 (中国标准时间)">
|
||||
|
||||
|
||||
<div class="book-summary">
|
||||
|
@ -48,7 +48,7 @@
|
||||
<body>
|
||||
|
||||
|
||||
<div class="book" data-level="3.5" data-chapter-title="字符串" data-filepath="ch3/ch3-05.md" data-basepath=".." data-revision="Mon Dec 14 2015 11:30:54 GMT+0800 (中国标准时间)">
|
||||
<div class="book" data-level="3.5" data-chapter-title="字符串" data-filepath="ch3/ch3-05.md" data-basepath=".." data-revision="Wed Dec 16 2015 10:54:29 GMT+0800 (中国标准时间)">
|
||||
|
||||
|
||||
<div class="book-summary">
|
||||
|
@ -48,7 +48,7 @@
|
||||
<body>
|
||||
|
||||
|
||||
<div class="book" data-level="3.6" data-chapter-title="常量" data-filepath="ch3/ch3-06.md" data-basepath=".." data-revision="Mon Dec 14 2015 11:30:54 GMT+0800 (中国标准时间)">
|
||||
<div class="book" data-level="3.6" data-chapter-title="常量" data-filepath="ch3/ch3-06.md" data-basepath=".." data-revision="Wed Dec 16 2015 10:54:29 GMT+0800 (中国标准时间)">
|
||||
|
||||
|
||||
<div class="book-summary">
|
||||
|
@ -48,7 +48,7 @@
|
||||
<body>
|
||||
|
||||
|
||||
<div class="book" data-level="3" data-chapter-title="基礎數據類型" data-filepath="ch3/ch3.md" data-basepath=".." data-revision="Mon Dec 14 2015 11:30:54 GMT+0800 (中国标准时间)">
|
||||
<div class="book" data-level="3" data-chapter-title="基礎數據類型" data-filepath="ch3/ch3.md" data-basepath=".." data-revision="Wed Dec 16 2015 10:54:29 GMT+0800 (中国标准时间)">
|
||||
|
||||
|
||||
<div class="book-summary">
|
||||
|
@ -48,7 +48,7 @@
|
||||
<body>
|
||||
|
||||
|
||||
<div class="book" data-level="4.1" data-chapter-title="數組" data-filepath="ch4/ch4-01.md" data-basepath=".." data-revision="Mon Dec 14 2015 11:30:54 GMT+0800 (中国标准时间)">
|
||||
<div class="book" data-level="4.1" data-chapter-title="數組" data-filepath="ch4/ch4-01.md" data-basepath=".." data-revision="Wed Dec 16 2015 10:54:29 GMT+0800 (中国标准时间)">
|
||||
|
||||
|
||||
<div class="book-summary">
|
||||
|
@ -48,7 +48,7 @@
|
||||
<body>
|
||||
|
||||
|
||||
<div class="book" data-level="4.2" data-chapter-title="切片" data-filepath="ch4/ch4-02.md" data-basepath=".." data-revision="Mon Dec 14 2015 11:30:54 GMT+0800 (中国标准时间)">
|
||||
<div class="book" data-level="4.2" data-chapter-title="切片" data-filepath="ch4/ch4-02.md" data-basepath=".." data-revision="Wed Dec 16 2015 10:54:29 GMT+0800 (中国标准时间)">
|
||||
|
||||
|
||||
<div class="book-summary">
|
||||
|
@ -48,7 +48,7 @@
|
||||
<body>
|
||||
|
||||
|
||||
<div class="book" data-level="4.3" data-chapter-title="字典" data-filepath="ch4/ch4-03.md" data-basepath=".." data-revision="Mon Dec 14 2015 11:30:54 GMT+0800 (中国标准时间)">
|
||||
<div class="book" data-level="4.3" data-chapter-title="字典" data-filepath="ch4/ch4-03.md" data-basepath=".." data-revision="Wed Dec 16 2015 10:54:29 GMT+0800 (中国标准时间)">
|
||||
|
||||
|
||||
<div class="book-summary">
|
||||
|
@ -48,7 +48,7 @@
|
||||
<body>
|
||||
|
||||
|
||||
<div class="book" data-level="4.4" data-chapter-title="結構體" data-filepath="ch4/ch4-04.md" data-basepath=".." data-revision="Mon Dec 14 2015 11:30:54 GMT+0800 (中国标准时间)">
|
||||
<div class="book" data-level="4.4" data-chapter-title="結構體" data-filepath="ch4/ch4-04.md" data-basepath=".." data-revision="Wed Dec 16 2015 10:54:29 GMT+0800 (中国标准时间)">
|
||||
|
||||
|
||||
<div class="book-summary">
|
||||
|
@ -48,7 +48,7 @@
|
||||
<body>
|
||||
|
||||
|
||||
<div class="book" data-level="4.5" data-chapter-title="JSON" data-filepath="ch4/ch4-05.md" data-basepath=".." data-revision="Mon Dec 14 2015 11:30:54 GMT+0800 (中国标准时间)">
|
||||
<div class="book" data-level="4.5" data-chapter-title="JSON" data-filepath="ch4/ch4-05.md" data-basepath=".." data-revision="Wed Dec 16 2015 10:54:29 GMT+0800 (中国标准时间)">
|
||||
|
||||
|
||||
<div class="book-summary">
|
||||
|
@ -48,7 +48,7 @@
|
||||
<body>
|
||||
|
||||
|
||||
<div class="book" data-level="4.6" data-chapter-title="文本和HTML模闆" data-filepath="ch4/ch4-06.md" data-basepath=".." data-revision="Mon Dec 14 2015 11:30:54 GMT+0800 (中国标准时间)">
|
||||
<div class="book" data-level="4.6" data-chapter-title="文本和HTML模闆" data-filepath="ch4/ch4-06.md" data-basepath=".." data-revision="Wed Dec 16 2015 10:54:29 GMT+0800 (中国标准时间)">
|
||||
|
||||
|
||||
<div class="book-summary">
|
||||
|
@ -48,7 +48,7 @@
|
||||
<body>
|
||||
|
||||
|
||||
<div class="book" data-level="4" data-chapter-title="復閤數據類型" data-filepath="ch4/ch4.md" data-basepath=".." data-revision="Mon Dec 14 2015 11:30:54 GMT+0800 (中国标准时间)">
|
||||
<div class="book" data-level="4" data-chapter-title="復閤數據類型" data-filepath="ch4/ch4.md" data-basepath=".." data-revision="Wed Dec 16 2015 10:54:29 GMT+0800 (中国标准时间)">
|
||||
|
||||
|
||||
<div class="book-summary">
|
||||
|
@ -48,7 +48,7 @@
|
||||
<body>
|
||||
|
||||
|
||||
<div class="book" data-level="5.1" data-chapter-title="函數聲明" data-filepath="ch5/ch5-01.md" data-basepath=".." data-revision="Mon Dec 14 2015 11:30:54 GMT+0800 (中国标准时间)">
|
||||
<div class="book" data-level="5.1" data-chapter-title="函數聲明" data-filepath="ch5/ch5-01.md" data-basepath=".." data-revision="Wed Dec 16 2015 10:54:29 GMT+0800 (中国标准时间)">
|
||||
|
||||
|
||||
<div class="book-summary">
|
||||
|
@ -48,7 +48,7 @@
|
||||
<body>
|
||||
|
||||
|
||||
<div class="book" data-level="5.2" data-chapter-title="遞歸" data-filepath="ch5/ch5-02.md" data-basepath=".." data-revision="Mon Dec 14 2015 11:30:54 GMT+0800 (中国标准时间)">
|
||||
<div class="book" data-level="5.2" data-chapter-title="遞歸" data-filepath="ch5/ch5-02.md" data-basepath=".." data-revision="Wed Dec 16 2015 10:54:29 GMT+0800 (中国标准时间)">
|
||||
|
||||
|
||||
<div class="book-summary">
|
||||
|
@ -48,7 +48,7 @@
|
||||
<body>
|
||||
|
||||
|
||||
<div class="book" data-level="5.3" data-chapter-title="多返迴值" data-filepath="ch5/ch5-03.md" data-basepath=".." data-revision="Mon Dec 14 2015 11:30:54 GMT+0800 (中国标准时间)">
|
||||
<div class="book" data-level="5.3" data-chapter-title="多返迴值" data-filepath="ch5/ch5-03.md" data-basepath=".." data-revision="Wed Dec 16 2015 10:54:29 GMT+0800 (中国标准时间)">
|
||||
|
||||
|
||||
<div class="book-summary">
|
||||
|
@ -48,7 +48,7 @@
|
||||
<body>
|
||||
|
||||
|
||||
<div class="book" data-level="5.4" data-chapter-title="錯誤" data-filepath="ch5/ch5-04.md" data-basepath=".." data-revision="Mon Dec 14 2015 11:30:54 GMT+0800 (中国标准时间)">
|
||||
<div class="book" data-level="5.4" data-chapter-title="錯誤" data-filepath="ch5/ch5-04.md" data-basepath=".." data-revision="Wed Dec 16 2015 10:54:29 GMT+0800 (中国标准时间)">
|
||||
|
||||
|
||||
<div class="book-summary">
|
||||
|
@ -48,7 +48,7 @@
|
||||
<body>
|
||||
|
||||
|
||||
<div class="book" data-level="5.5" data-chapter-title="函數值" data-filepath="ch5/ch5-05.md" data-basepath=".." data-revision="Mon Dec 14 2015 11:30:54 GMT+0800 (中国标准时间)">
|
||||
<div class="book" data-level="5.5" data-chapter-title="函數值" data-filepath="ch5/ch5-05.md" data-basepath=".." data-revision="Wed Dec 16 2015 10:54:29 GMT+0800 (中国标准时间)">
|
||||
|
||||
|
||||
<div class="book-summary">
|
||||
|
@ -48,7 +48,7 @@
|
||||
<body>
|
||||
|
||||
|
||||
<div class="book" data-level="5.6" data-chapter-title="匿名函數" data-filepath="ch5/ch5-06.md" data-basepath=".." data-revision="Mon Dec 14 2015 11:30:54 GMT+0800 (中国标准时间)">
|
||||
<div class="book" data-level="5.6" data-chapter-title="匿名函數" data-filepath="ch5/ch5-06.md" data-basepath=".." data-revision="Wed Dec 16 2015 10:54:29 GMT+0800 (中国标准时间)">
|
||||
|
||||
|
||||
<div class="book-summary">
|
||||
|
@ -48,7 +48,7 @@
|
||||
<body>
|
||||
|
||||
|
||||
<div class="book" data-level="5.7" data-chapter-title="可變參數" data-filepath="ch5/ch5-07.md" data-basepath=".." data-revision="Mon Dec 14 2015 11:30:54 GMT+0800 (中国标准时间)">
|
||||
<div class="book" data-level="5.7" data-chapter-title="可變參數" data-filepath="ch5/ch5-07.md" data-basepath=".." data-revision="Wed Dec 16 2015 10:54:29 GMT+0800 (中国标准时间)">
|
||||
|
||||
|
||||
<div class="book-summary">
|
||||
|
@ -48,7 +48,7 @@
|
||||
<body>
|
||||
|
||||
|
||||
<div class="book" data-level="5.8" data-chapter-title="Deferred函數" data-filepath="ch5/ch5-08.md" data-basepath=".." data-revision="Mon Dec 14 2015 11:30:54 GMT+0800 (中国标准时间)">
|
||||
<div class="book" data-level="5.8" data-chapter-title="Deferred函數" data-filepath="ch5/ch5-08.md" data-basepath=".." data-revision="Wed Dec 16 2015 10:54:29 GMT+0800 (中国标准时间)">
|
||||
|
||||
|
||||
<div class="book-summary">
|
||||
|
@ -48,7 +48,7 @@
|
||||
<body>
|
||||
|
||||
|
||||
<div class="book" data-level="5.9" data-chapter-title="Panic異常" data-filepath="ch5/ch5-09.md" data-basepath=".." data-revision="Mon Dec 14 2015 11:30:54 GMT+0800 (中国标准时间)">
|
||||
<div class="book" data-level="5.9" data-chapter-title="Panic異常" data-filepath="ch5/ch5-09.md" data-basepath=".." data-revision="Wed Dec 16 2015 10:54:29 GMT+0800 (中国标准时间)">
|
||||
|
||||
|
||||
<div class="book-summary">
|
||||
|
@ -48,7 +48,7 @@
|
||||
<body>
|
||||
|
||||
|
||||
<div class="book" data-level="5.10" data-chapter-title="Recover捕穫異常" data-filepath="ch5/ch5-10.md" data-basepath=".." data-revision="Mon Dec 14 2015 11:30:54 GMT+0800 (中国标准时间)">
|
||||
<div class="book" data-level="5.10" data-chapter-title="Recover捕穫異常" data-filepath="ch5/ch5-10.md" data-basepath=".." data-revision="Wed Dec 16 2015 10:54:29 GMT+0800 (中国标准时间)">
|
||||
|
||||
|
||||
<div class="book-summary">
|
||||
|
@ -48,7 +48,7 @@
|
||||
<body>
|
||||
|
||||
|
||||
<div class="book" data-level="5" data-chapter-title="函數" data-filepath="ch5/ch5.md" data-basepath=".." data-revision="Mon Dec 14 2015 11:30:54 GMT+0800 (中国标准时间)">
|
||||
<div class="book" data-level="5" data-chapter-title="函數" data-filepath="ch5/ch5.md" data-basepath=".." data-revision="Wed Dec 16 2015 10:54:29 GMT+0800 (中国标准时间)">
|
||||
|
||||
|
||||
<div class="book-summary">
|
||||
|
@ -48,7 +48,7 @@
|
||||
<body>
|
||||
|
||||
|
||||
<div class="book" data-level="6.1" data-chapter-title="方法聲明" data-filepath="ch6/ch6-01.md" data-basepath=".." data-revision="Mon Dec 14 2015 11:30:54 GMT+0800 (中国标准时间)">
|
||||
<div class="book" data-level="6.1" data-chapter-title="方法聲明" data-filepath="ch6/ch6-01.md" data-basepath=".." data-revision="Wed Dec 16 2015 10:54:29 GMT+0800 (中国标准时间)">
|
||||
|
||||
|
||||
<div class="book-summary">
|
||||
|
@ -48,7 +48,7 @@
|
||||
<body>
|
||||
|
||||
|
||||
<div class="book" data-level="6.2" data-chapter-title="基於指鍼對象的方法" data-filepath="ch6/ch6-02.md" data-basepath=".." data-revision="Mon Dec 14 2015 11:30:54 GMT+0800 (中国标准时间)">
|
||||
<div class="book" data-level="6.2" data-chapter-title="基於指鍼對象的方法" data-filepath="ch6/ch6-02.md" data-basepath=".." data-revision="Wed Dec 16 2015 10:54:29 GMT+0800 (中国标准时间)">
|
||||
|
||||
|
||||
<div class="book-summary">
|
||||
|
@ -48,7 +48,7 @@
|
||||
<body>
|
||||
|
||||
|
||||
<div class="book" data-level="6.3" data-chapter-title="通過嵌入結構體來擴展類型" data-filepath="ch6/ch6-03.md" data-basepath=".." data-revision="Mon Dec 14 2015 11:30:54 GMT+0800 (中国标准时间)">
|
||||
<div class="book" data-level="6.3" data-chapter-title="通過嵌入結構體來擴展類型" data-filepath="ch6/ch6-03.md" data-basepath=".." data-revision="Wed Dec 16 2015 10:54:29 GMT+0800 (中国标准时间)">
|
||||
|
||||
|
||||
<div class="book-summary">
|
||||
|
@ -48,7 +48,7 @@
|
||||
<body>
|
||||
|
||||
|
||||
<div class="book" data-level="6.4" data-chapter-title="方法值和方法錶達式" data-filepath="ch6/ch6-04.md" data-basepath=".." data-revision="Mon Dec 14 2015 11:30:54 GMT+0800 (中国标准时间)">
|
||||
<div class="book" data-level="6.4" data-chapter-title="方法值和方法錶達式" data-filepath="ch6/ch6-04.md" data-basepath=".." data-revision="Wed Dec 16 2015 10:54:29 GMT+0800 (中国标准时间)">
|
||||
|
||||
|
||||
<div class="book-summary">
|
||||
|
@ -48,7 +48,7 @@
|
||||
<body>
|
||||
|
||||
|
||||
<div class="book" data-level="6.5" data-chapter-title="示例: Bit數組" data-filepath="ch6/ch6-05.md" data-basepath=".." data-revision="Mon Dec 14 2015 11:30:54 GMT+0800 (中国标准时间)">
|
||||
<div class="book" data-level="6.5" data-chapter-title="示例: Bit數組" data-filepath="ch6/ch6-05.md" data-basepath=".." data-revision="Wed Dec 16 2015 10:54:29 GMT+0800 (中国标准时间)">
|
||||
|
||||
|
||||
<div class="book-summary">
|
||||
|
@ -48,7 +48,7 @@
|
||||
<body>
|
||||
|
||||
|
||||
<div class="book" data-level="6.6" data-chapter-title="封裝" data-filepath="ch6/ch6-06.md" data-basepath=".." data-revision="Mon Dec 14 2015 11:30:54 GMT+0800 (中国标准时间)">
|
||||
<div class="book" data-level="6.6" data-chapter-title="封裝" data-filepath="ch6/ch6-06.md" data-basepath=".." data-revision="Wed Dec 16 2015 10:54:29 GMT+0800 (中国标准时间)">
|
||||
|
||||
|
||||
<div class="book-summary">
|
||||
|
@ -48,7 +48,7 @@
|
||||
<body>
|
||||
|
||||
|
||||
<div class="book" data-level="6" data-chapter-title="方法" data-filepath="ch6/ch6.md" data-basepath=".." data-revision="Mon Dec 14 2015 11:30:54 GMT+0800 (中国标准时间)">
|
||||
<div class="book" data-level="6" data-chapter-title="方法" data-filepath="ch6/ch6.md" data-basepath=".." data-revision="Wed Dec 16 2015 10:54:29 GMT+0800 (中国标准时间)">
|
||||
|
||||
|
||||
<div class="book-summary">
|
||||
|
@ -48,7 +48,7 @@
|
||||
<body>
|
||||
|
||||
|
||||
<div class="book" data-level="7.1" data-chapter-title="接口是閤約" data-filepath="ch7/ch7-01.md" data-basepath=".." data-revision="Mon Dec 14 2015 11:30:54 GMT+0800 (中国标准时间)">
|
||||
<div class="book" data-level="7.1" data-chapter-title="接口是閤約" data-filepath="ch7/ch7-01.md" data-basepath=".." data-revision="Wed Dec 16 2015 10:54:29 GMT+0800 (中国标准时间)">
|
||||
|
||||
|
||||
<div class="book-summary">
|
||||
|
@ -48,7 +48,7 @@
|
||||
<body>
|
||||
|
||||
|
||||
<div class="book" data-level="7.2" data-chapter-title="接口類型" data-filepath="ch7/ch7-02.md" data-basepath=".." data-revision="Mon Dec 14 2015 11:30:54 GMT+0800 (中国标准时间)">
|
||||
<div class="book" data-level="7.2" data-chapter-title="接口類型" data-filepath="ch7/ch7-02.md" data-basepath=".." data-revision="Wed Dec 16 2015 10:54:29 GMT+0800 (中国标准时间)">
|
||||
|
||||
|
||||
<div class="book-summary">
|
||||
|
@ -48,7 +48,7 @@
|
||||
<body>
|
||||
|
||||
|
||||
<div class="book" data-level="7.3" data-chapter-title="實現接口的條件" data-filepath="ch7/ch7-03.md" data-basepath=".." data-revision="Mon Dec 14 2015 11:30:54 GMT+0800 (中国标准时间)">
|
||||
<div class="book" data-level="7.3" data-chapter-title="實現接口的條件" data-filepath="ch7/ch7-03.md" data-basepath=".." data-revision="Wed Dec 16 2015 10:54:29 GMT+0800 (中国标准时间)">
|
||||
|
||||
|
||||
<div class="book-summary">
|
||||
|
@ -48,7 +48,7 @@
|
||||
<body>
|
||||
|
||||
|
||||
<div class="book" data-level="7.4" data-chapter-title="flag.Value接口" data-filepath="ch7/ch7-04.md" data-basepath=".." data-revision="Mon Dec 14 2015 11:30:54 GMT+0800 (中国标准时间)">
|
||||
<div class="book" data-level="7.4" data-chapter-title="flag.Value接口" data-filepath="ch7/ch7-04.md" data-basepath=".." data-revision="Wed Dec 16 2015 10:54:29 GMT+0800 (中国标准时间)">
|
||||
|
||||
|
||||
<div class="book-summary">
|
||||
|
@ -48,7 +48,7 @@
|
||||
<body>
|
||||
|
||||
|
||||
<div class="book" data-level="7.5" data-chapter-title="接口值" data-filepath="ch7/ch7-05.md" data-basepath=".." data-revision="Mon Dec 14 2015 11:30:54 GMT+0800 (中国标准时间)">
|
||||
<div class="book" data-level="7.5" data-chapter-title="接口值" data-filepath="ch7/ch7-05.md" data-basepath=".." data-revision="Wed Dec 16 2015 10:54:29 GMT+0800 (中国标准时间)">
|
||||
|
||||
|
||||
<div class="book-summary">
|
||||
|
@ -48,7 +48,7 @@
|
||||
<body>
|
||||
|
||||
|
||||
<div class="book" data-level="7.6" data-chapter-title="sort.Interface接口" data-filepath="ch7/ch7-06.md" data-basepath=".." data-revision="Mon Dec 14 2015 11:30:54 GMT+0800 (中国标准时间)">
|
||||
<div class="book" data-level="7.6" data-chapter-title="sort.Interface接口" data-filepath="ch7/ch7-06.md" data-basepath=".." data-revision="Wed Dec 16 2015 10:54:29 GMT+0800 (中国标准时间)">
|
||||
|
||||
|
||||
<div class="book-summary">
|
||||
|
@ -48,7 +48,7 @@
|
||||
<body>
|
||||
|
||||
|
||||
<div class="book" data-level="7.7" data-chapter-title="http.Handler接口" data-filepath="ch7/ch7-07.md" data-basepath=".." data-revision="Mon Dec 14 2015 11:30:54 GMT+0800 (中国标准时间)">
|
||||
<div class="book" data-level="7.7" data-chapter-title="http.Handler接口" data-filepath="ch7/ch7-07.md" data-basepath=".." data-revision="Wed Dec 16 2015 10:54:29 GMT+0800 (中国标准时间)">
|
||||
|
||||
|
||||
<div class="book-summary">
|
||||
|
@ -48,7 +48,7 @@
|
||||
<body>
|
||||
|
||||
|
||||
<div class="book" data-level="7.8" data-chapter-title="error接口" data-filepath="ch7/ch7-08.md" data-basepath=".." data-revision="Mon Dec 14 2015 11:30:54 GMT+0800 (中国标准时间)">
|
||||
<div class="book" data-level="7.8" data-chapter-title="error接口" data-filepath="ch7/ch7-08.md" data-basepath=".." data-revision="Wed Dec 16 2015 10:54:29 GMT+0800 (中国标准时间)">
|
||||
|
||||
|
||||
<div class="book-summary">
|
||||
|
@ -48,7 +48,7 @@
|
||||
<body>
|
||||
|
||||
|
||||
<div class="book" data-level="7.9" data-chapter-title="示例: 錶達式求值" data-filepath="ch7/ch7-09.md" data-basepath=".." data-revision="Mon Dec 14 2015 11:30:54 GMT+0800 (中国标准时间)">
|
||||
<div class="book" data-level="7.9" data-chapter-title="示例: 錶達式求值" data-filepath="ch7/ch7-09.md" data-basepath=".." data-revision="Wed Dec 16 2015 10:54:29 GMT+0800 (中国标准时间)">
|
||||
|
||||
|
||||
<div class="book-summary">
|
||||
|
@ -48,7 +48,7 @@
|
||||
<body>
|
||||
|
||||
|
||||
<div class="book" data-level="7.10" data-chapter-title="類型斷言" data-filepath="ch7/ch7-10.md" data-basepath=".." data-revision="Mon Dec 14 2015 11:30:54 GMT+0800 (中国标准时间)">
|
||||
<div class="book" data-level="7.10" data-chapter-title="類型斷言" data-filepath="ch7/ch7-10.md" data-basepath=".." data-revision="Wed Dec 16 2015 10:54:29 GMT+0800 (中国标准时间)">
|
||||
|
||||
|
||||
<div class="book-summary">
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user