文章目錄
- go test
- 測試函數
- 隨機測試
- 測試一個命令
- 白盒測試
- 外部測試包
- 測試覆蓋率
- 基準測試
- 剖析
- 示例函數
go test
go test
命令是一個按照一定的約定和組織來測試代碼的程序。在包目錄內,所有以xxx_test.go
為后綴名的源文件在執行go build
時不會被構建為包的一部分,它們是go test
測試的一部分。
xxx_test.go
中,有三種類型的函數:測試函數、基準(benchmark)函數、示例函數。
測試函數是以Test
為函數名前綴的函數,用于測試程序的一些邏輯行為是否正確;go test
命令會調用這些測試函數并報告測試結果是PASS
還是FAIL
。
基準函數是以Benchmark
為函數名前綴的函數,用于衡量一些函數的性能。go test
會多次運行基準函數以計算一個平均的執行時間。
示例函數是以Example
為函數名前綴的函數,提供一個由編譯器保證正確性的示例文檔。
go test
會遍歷所有xxx_test.go
文件中符合上述命名規則的函數,生成一個臨時的 main 包用于調用相應的測試函數,接著構建并運行、報告測試結果,最后清理測試中生成的臨時文件。
測試函數
每個測試函數必須導入testing
包,函數簽名如下:
func TestName(t *testing.T) {// ... ... ...
}
測試函數名必須以Test
開頭,可選的后綴名必須以大寫字母開頭:
func TestSin(t *testing.T) { /* ... ... ... */ }
func TestCos(t *testing.T) { /* ... ... ... */ }
func TestLog(t *testing.T) { /* ... ... ... */ }
參數t
用于報告測試失敗和附加的日志信息。下例實現的函數是一個用于判斷字符串是否為回文串的函數:
package wordfunc IsPalindrome(s string) bool {for i := range s {if s[i] != s[len(s) - 1 - i] {return false}}return true
}
在相同的目錄下,word_test.go
測試文件中包含TestPalindrome
和TestNonPalindrome
兩個測試函數:
package wordimport "testing"func TestPalindrome(t *testing.T) {if !IsPalindrome("detartrated") {t.Error(`isPlaindrome("detartrated") = false`)}if !IsPalindrome("kayak") {t.Error(`IsPalindrome("kayak") = false`)}
}func TestNonPalindrome(t *testing.T) {if IsPalindrome("palindrome") {t.Error(`IsPalindrome("palindrome") = true`)}
}
在該目錄下,于命令行當中輸入go test
(如果沒有參數來指定包,那么將默認采用當前目錄對應的包,和go build
一樣),構建和運行測試:
go test
PASS
ok test/word 0.449s
下例在測試文件當中引入了更復雜的例子:
func TestFrenchPalindrome(t *testing.T) {if !IsPalindrome("été") {t.Error(`IsPalindrome("été") = false`)}
}func TestCanalPalindrome(t *testing.T) {input := "A man, a plan, a canal: Panama"if !IsPalindrome(input) {t.Errorf(`IsPalindrome(%q) = false`, input)}
}
再次運行 go test
,會得到這兩個測試語句報錯的反饋:
go test
--- FAIL: TestFrenchPalindrome (0.00s)word_test.go:22: IsPalindrome("été") = false
--- FAIL: TestCanalPalindrome (0.00s)word_test.go:29: IsPalindrome("A man, a plan, a canal: Panama") = false
FAIL
exit status 1
FAIL test/word 0.362s
先編寫測試用例并觀察到測試用例觸發了和用戶報告的錯誤相同的描述是一個好的測試習慣,只有這樣我們才能定位到我們真正要解決的問題。
先寫測試用例的另外一個好處是,運行測試通常比手工描述報告處理更快,這使得我們可以快速迭代。如果測試集有很多運行緩慢的測試,我們可以通過只選擇運行某些特定的測試來加快測試的速度。
在go test
加上參數-v
來打印每個測試函數的名字和運行時間。
go test -v
=== RUN TestPalindrome
--- PASS: TestPalindrome (0.00s)
=== RUN TestNonPalindrome
--- PASS: TestNonPalindrome (0.00s)
=== RUN TestFrenchPalindromeword_test.go:22: IsPalindrome("été") = false
--- FAIL: TestFrenchPalindrome (0.00s)
=== RUN TestCanalPalindromeword_test.go:29: IsPalindrome("A man, a plan, a canal: Panama") = false
--- FAIL: TestCanalPalindrome (0.00s)
FAIL
exit status 1
FAIL test/word 0.147s
參數-run
對應一個正則表達式,只有測試函數名被它正確匹配的測試函數才會被go test
測試命令運行:
go test -run="French|Canal"
--- FAIL: TestFrenchPalindrome (0.00s)word_test.go:22: IsPalindrome("été") = false
--- FAIL: TestCanalPalindrome (0.00s)word_test.go:29: IsPalindrome("A man, a plan, a canal: Panama") = false
FAIL
exit status 1
FAIL test/word 0.147s
現在我們的任務就是修復上述的錯誤。第一個 BUG 產生的原因是我們采用了 byte 而不是 rune 序列,所以像“été”中的é等非ASCII字符不能正確處理。第二個 BUG 是因為沒有忽略空格和小寫字母所導致的。基于上述兩個 BUG,重寫 IsPalindrome 函數:
package wordimport "unicode"func IsPalindrome(s string) bool {var letters []runefor _, r := range s {if unicode.IsLetter(r) {letters = append(letters, unicode.ToLower(r))}}for i := range letters {if letters[i] != letters[len(letters)-1-i] {return false}}return true
}
同時,我們將所有的測試數據合并到一張測試表格當中:
package wordimport "testing"func TestIsPalindrome(t *testing.T) {var tests = []struct {input stringwant bool}{{"", true},{"a", true},{"aa", true},{"ab", false},{"kayak", true},{"detartrated", true},{"A man, a plan, a canal: Panama", true},{"Evil I did dwell; lewd did I live.", true},{"Able was I ere I saw Elba", true},{"été", true},{"Et se resservir, ivresse reste.", true},{"palindrome", false}, // non-palindrome{"desserts", false}, // semi-palindrome}for _, test := range tests {if got := IsPalindrome(test.input); got != test.want {t.Errorf("IsPalindrome(%q) = %v", test.input, got)}}
}
現在再次運行go test
,會發現所有測試都通過了。
上面這種表格驅動的測試在 Go 當中很常見,我們可以很容易地向表格中添加新的測試數據,并且后面的測試邏輯也沒有冗余,使得我們可以有更多的精力去完善錯誤信息。
對于失敗的測試用例,t.Errorf
不會引起 panic 異常或是終止測試的執行。即使表格前面的數據導致了測試的失敗,表格后面的測試依然會執行。
如果我們確實要在表格測試當中出現失敗測試用例時停止測試,那么我們可以使用t.Fatal
或t.Fatalf
來停止當前函數的測試。它們必須在和測試函數同一個 goroutine 內被調用。
測試失敗的信息形式一般是f(x)=y, want z
。
隨機測試
表格驅動的測試便于構造基于精心挑選的測試數據的測試用例。另一種測試的思路是隨機測試,也就是通過構造更廣泛的隨機輸入來測試探索函數的行為。
對于一個隨機輸入,如何知道希望的輸出結果呢?有兩種處理策略:第一個是編寫另一個對照函數,使用簡單和清晰的算法,雖然效率較低,但是行為和要測試的函數是一致的,然后針對相同的隨機輸入,檢查兩者輸出的結果。第二種是生成的隨機輸入數據遵循特定的模式,這樣我們就可以知道期望的輸出的模式。
下例采用第二種方法,使用randomPalindrome
函數隨機生成回文字符串:
import "math/rand"
// randomPalindrome returns a palindrome whose length and contents
// are derived from the pseudo-random number generator rng.
func randomPalindrome(rng *rand.Rand) string {n := rng.Intn(25) // random length up to 24runes := make([]rune, n)for i := 0; i < (n+1)/2; i++ {r := rune(rng.Intn(0x1000)) // random rune up to '\u0999'runes[i] = rrunes[n-1-i] = r}return string(runes)
}
下面是對它的測試語句塊。在該測試函數中,首先根據時間生成一個隨機數種子,傳遞給randomPalindrome
用于生成隨機的回文串。之后,調用IsPalindrome
對這個回文串進行測試:
func TestRandomPalindromes(t *testing.T) {// Initialize a pseudo-random number generator.seed := time.Now().UTC().UnixNano()t.Logf("Random seed: %d", seed)rng := rand.New(rand.NewSource(seed))for i := 0; i < 1000; i++ {p := randomPalindrome(rng)if !IsPalindrome(p) {t.Errorf("IsPalindrome(%q) = false", p)}}
}
測試一個命令
go test
甚至可以用來對可執行程序進行測試。如果一個包的名字是main
,那么在構建時會生成一個可執行程序,不過main
包可以作為一個包被測試器代碼導入。
下例包含兩個函數,分別是 main 函數和 echo 函數。echo 函數完成真正的工作,main 函數用于處理命令后輸入的參數,以及 echo 可能返回的錯誤:
// Echo prints its command-line arguments.
package mainimport ("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 testingfunc 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_test.go
為:
package mainimport ("bytes""fmt""testing"
)func TestEcho(t *testing.T) {var tests = []struct {newline boolsep stringargs []stringwant 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 outputif 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 函數所在的 go 文件)放在同一個包中。雖然是 main 包,也具有 main 入口函數,但在測試的時候 main 包只是 TestEcho 測試函數導入的一個普通包,里面 main 函數并沒有被導出,而是被忽略了。
白盒測試
一種測試分類的方法是基于測試著是否需要了解被測試對象內部的工作原理。黑盒測試只需要測試包公開的文檔和 API 行為,內部的實現對測試代碼是透明的。相反,白盒測試有訪問包內部函數和數據結構的權限,因此可以做到一些普通客戶端服務實現的測試。例如,一個白盒測試可以在每個操作之后檢測不變量的數據類型。
黑盒和白盒測試兩種測試方法是互補的。黑盒測試一般更健壯,隨著軟件的完善,其測試代碼很少需要被更新,它們可以幫助測試者了解真實客戶的需求,也可以幫助發現 API 設計的不足之處。相反,白盒測試則可以對內部一些棘手的實現提供更多的測試覆蓋。
我們已經見過兩種測試方法了。TestIsPalindrome
僅僅使用導出的IsPalindrome
函數進行測試,因此它屬于黑盒測試。而TestEcho
測試調用了內部的echo
函數,并更新了內部的out
包級變量,二者都是未導出的,屬于白盒測試。
下例演示了為用戶提供網絡存儲的 web 服務中的配額檢測邏輯。當用戶使用了超過 90%的存儲配額之后,將發送提醒郵件,下述代碼存放在storage.go
文件當中:
// in storage.go
package storageimport ("fmt""log""net/smtp"
)func bytesInUse(username string) int64 { return 0 }// NOTE: Never put password 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 = 1000000000percent := 100 * used / quotaif percent < 90 {return // OK}msg := fmt.Sprintf(template, used, quota)auth := smtp.PlainAuth("", sender, hostname, password)err := smtp.SendMail(hostname+":587", auth, sender, []string{username}, []byte(msg))if err != nil {log.Printf("smtp.SendMail(%s) failed: %s", username, msg)}
}
我們想測試這段代碼,但是不希望真地發送郵件,因此我們將發送郵件的處理邏輯放在一個私有的notifyUser
函數當中。
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 = 1000000000percent := 100 * used / quotaif percent < 90 {return // OK}msg := fmt.Sprintf(template, used, percent)notifyUser(username, msg)
}
現在我們可以在測試中用偽郵件發送函數替代真實的郵件發送函數。它只是簡單記錄要通知的用戶和郵件的內容。
func TestCheckQuotaNotifiesUser(t *testing.T) {// Save and restore original notifyUser.saved := notifyUserdefer func() { notifyUser = saved }()var notifiedUser, notifiedMsg stringnotifyUser = 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
函數當中的notifyUser
進行測試。我們想要模擬一個使用980 MB
內存的情況,而在storage.go
當中,我們已經設置bytesInUse
的返回結果為 0,我們先設置其返回結果為980000000
,之后執行測試函數(這一點很關鍵,在《Go 語言圣經》的原文中沒有提及,導致測試函數一開始的執行就是失敗的)。
可以看到,測試可以成功執行通過。說明我們可以順利地在內存達到閾值的情況下,在CheckQuota
當中調用notifyUser
函數來對用戶進行通知。
此處有一個技巧,那就是在測試函數的開頭,使用一個saved
來保存測試正式開始之前的notifyUser
函數,使用defer
關鍵字在測試結束時恢復這個函數,這樣就不會影響其他測試函數對notifyUser
這個業務函數進行測試了。這樣做是并發安全的,因為go test
不會并發地執行測試文件中的測試函數。
外部測試包
考慮net/url
和net/http
兩個包,前者提供了 URL 解析功能,后者提供了 web 服務和 HTTP 客戶端功能。上層的net/http
依賴下層的net/url
。
如果我們想要在net/url
包中測試一演示不同 URL 和 HTTP 客戶端的交互行為,就會在測試文件當中導入net/http
,進而產生循環引用。我們已經提到過,Go 當中不允許循環引用的存在。
此時,我們就需要引入「外部測試包」,以避免因測試而產生的循環導入。我們可以在net/url
這個包所在的目錄net
新建一個名為net/url_test
的包,專門用于外部測試,包名的_test
告知go test
工具它應該建立一個額外的包來運行測試。外部測試包的導入路徑是net/url_test
,但因為它是一個專門用于測試的包,所以它不應該被其他包所導入。
由于外部測試包是一個獨立的包,所以它能夠導入那些「依賴待測代碼本身」的其他輔助包,包內的測試代碼無法做到這一點。在設計層面,外部測試包是其他所有包的上層:
可以使用go list
工具來查看包目錄下哪些 Go 源文件是產品代碼,哪些是包內測試,還有哪些是包外測試。
有時候,外部測試包需要以白盒測試的方式對包內未導出的邏輯進行測試,一個《Go 語言圣經》當中介紹的技巧是:我們可以在包內測試文件中導出一個內部的實現來供外部測試包使用,因為這些代碼僅在測試的時候用到,因此一般放在export_test.go
文件當中。
例如,fmt 包的fmt.Scanf
需要unicode.IsSpace
函數提供的功能。為了避免太多的依賴,fmt 包并沒有導入包含巨大表格數據的 unicode 包。相反,fmt 包當中有一個名為isSpace
的內部簡單實現。
為了確保fmt.isSpace
和unicode.IsSpace
的行為一致,fmt 包謹慎地包含了一個測試。一個外部測試包內的白盒測試當然無法訪問包內的未導出變量,因此 fmt 專門設置了一個IsSpace
函數,它是開發者為測試開的后門,專門用于導出isSpace
。導出的行為被放在了export_test.go
文件當中:
package fmtvar IsSpace = isSpace // 在 export_test.go 當中導出內部的未導出變量, 為包外測試開后門
測試覆蓋率
就性質而言,測試不可能是完整的。對待測程序執行的測試程度稱為“測試覆蓋率”。測試覆蓋率不能量化,但有啟發式的方法能幫助我們編寫有效的測試代碼。
啟發式方法中,語句的覆蓋率是最簡單和最廣泛使用的。語句的覆蓋率指的是在測試中至少被執行一簇的代碼占總代碼數的比例。
下例是一個表格驅動的測試,用于測試表達式求值程序(《Go 語言圣經》第七章——7.9 示例:表達式求值):
func TestCoverage(t *testing.T) {var tests = []struct {input stringenv Envwant string // expected error from Parse/Check or result from Eval}{{"x % 2", nil, "unexpected '%'"},{"!true", nil, "unexpected '!'"},{"log(10)", nil, `unknown function "log"`},{"sqrt(1, 2)", nil, "call to sqrt has 2 args, want 1"},{"sqrt(A / pi)", Env{"A": 87616, "pi": math.Pi}, "167"},{"pow(x, 3) + pow(y, 3)", Env{"x": 9, "y": 10}, "1729"},{"5 / 9 * (F - 32)", Env{"F": -40}, "-40"},}for _, test := range tests {expr, err := Parse(test.input)if err == nil {err = expr.Check(map[Var]bool{})}if err != nil {if err.Error() != test.want {t.Errorf("%s: got %q, want %q", test.input, err, test.want)}continue}got := fmt.Sprintf("%.6g", expr.Eval(test.env))if got != test.want {t.Errorf("%s: %v => %s, want %s",test.input, test.env, got, test.want)}}
}
在確保測試語句可以通過的前提下,使用go tool cover
,來顯示測試覆蓋率工具的使用方法。
$ go tool cover
Usage of 'go tool cover':
Given a coverage profile produced by 'go test':go test -coverprofile=c.outOpen a web browser displaying annotated source code:go tool cover -html=c.out
...
現在,在go test
加入-coverprofile
標志參數重新運行測試:
$ go test -run=Coverage -coverprofile=c.out gopl.io/ch7/eval
ok gopl.io/ch7/eval 0.032s coverage: 68.5% of statements
這個標志會在測試代碼中插入生成 hook 函數來統計覆蓋率的數據。
如果使用了-covermode=count
標志,那么測試代碼會在每個代碼塊插入一個計數器,用于統計每一個代碼塊的執行次數,依次我們可以衡量哪些代碼是被頻繁執行的代碼。
我們可以將測試的日志在 HTML 打印出來,使用:
go tool cover -html=c.out
100%的測試覆蓋率聽起來很完美,但是在實踐中通常不可行,也不是推薦的做法。測試時覆蓋只能說明代碼被執行過而已,并不代表代碼永遠不出現 BUG。
基準測試
固定測試可以測量一個程序在固定工作負載下的性能。Go 當中,基準測試函數與普通測試函數的寫法類似,但是以 Benchmark 為前綴名,并且帶有一個類型為*testing.B
的參數。*testing.B
參數除了提供和*testing.T
類似的方法,還有額外一些和性能測量相關的方法。它還提供了一個整數N
,用于指定操作執行的循環次數。
下例為IsPalindrome
的基準測試:
import "testing"func BenchmarkIsPalindrome(b *testing.B) {for i := 0; i < b.N; i ++ {IsPalindrome("A man, a plan, a canal: Panama")}
}
使用go test -bench=.
來運行基準測試。需要注意的是,和普通測試不同,基準測試在默認情況下不會運行。我們需要通過-bench
來指定要運行的基準測試函數,該參數是一個正則表達式,用于匹配要執行的基準測試的名字,默認值為空,"."
代表運行所有基準測試函數。
我運行的基準測試的結果是:
goos: darwin
goarch: arm64
pkg: test/word
cpu: Apple M4
BenchmarkIsPalindrome
BenchmarkIsPalindrome-10 9804885 112.6 ns/op
PASS
其中BenchmarkIsPalindrome-10
當中的10
對應的是運行時 GOMAXPROCES 的值,這對于一些與并發相關的基準測試而言是重要的信息。
報告顯示IsPalindrome
函數花費0.1126
微秒,是執行9804885
次的平均時間。循環在基準測試函數內部實現,而不是放在基準測試框架內實現,這樣可以讓每個基準測試函數有機會在循環啟動前初始化代碼。
基于基準測試和普通測試,我們可以輕松地測試新的有關程序性能改進的想法。
剖析
對于很多程序員來說,判斷哪部分是關鍵的性能瓶頸,是很容易犯經驗上的錯誤的,因此一般應該借助測量工具來證明。
當我們想仔細觀察程序的運行速度時,最好的方法是性能剖析。剖析技術是基于程序執行期間的一些自動抽樣,然后在收尾時進行推斷;最后產生的統計結果就稱為剖析數據。
Go 支持多種類型的剖析性能分析,每一種關注不同的方面,它們都涉及到每個采樣記錄的感興趣的一系列事件消息,每個事件都包含函數調用時的堆棧信息。內建的go test
工具對集中分析方式都提供了支持。
CPU 剖析數據標識了最耗 CPU 時間的函數。每個 CPU 上運行的線程每隔幾毫秒都會遇到 OS 的中斷時間,每次中斷都會記錄一個剖析數據然后恢復正常的運行。
堆剖析標識了最耗內存的語句。剖析庫會記錄調用內部內存分配的操作,平均每 512KB 的內存申請會觸發一個剖析數據。
阻塞剖析記錄阻塞 goroutine 最久的操作,例如系統調用、管道發送和接收,還有獲取鎖等。每當 goroutine 被這些操作阻塞時,剖析庫都會記錄相應的事件。
只需要開啟下面其中一個表示參數,就可以生成各種剖析文件(CPU 剖析、堆剖析、阻塞剖析)。當同時使用多個標志參數時,需要小心,因為分析操作之間可能會互相影響。
go test -cpuprofile=cpu.out
go test -blockprofile=block.out
go test -memprofile=mem.out
對于一些非測試程序,也很容易進行剖析。在具體實現上,剖析針對段時間運行的小程序和長時間運行的服務有很大不同。剖析對于長期運行的程序尤其有用,因此可以通過調用 Go 的 runtime API 來啟用運行時剖析。
一旦我們收集到了用于分析的采樣數據,我們就可以使用pprof
來分析這些數據。這是 Go 工具箱自帶的工具,但并不是一個日常工具,它對應go tool pprof
命令。該命令有許多特性和選項,但最基本的是兩個參數:生成這個概要文件的可執行程序和對應的剖析數據。
為了提高分析效率,減少空間,分析日志本身不包含函數的名字,它只包含函數對應的地址。也就是說,pprof 需要對應的可執行程序來解讀剖析數據。
下例演示了如何收集并展示一個 CPU 分析文件。我們選擇net/http
包的一個基準測試為例。通常,最好對業務關鍵代碼專門設計基準測試。由于簡單的基準測試沒法代表業務場景,因此我們使用-run=NONE
參數來禁止簡單的測試。
在命令行當中輸入以下語句(注意,和原本《Go 語言圣經》當中的語句不一樣,原文的語句在我的設備上執行,無法得到結果):
$ go test -bench=ClientServerParallelTLS64 -cpuprofile=cpu.log -benchtime=5s net/http
PASS
ok net/http 16.864s
上述這個語句段會讓go test
命令對 Go 的標準庫net/http
做基準測試,并生成 CPU 性能分析數據。以下是每個參數的含義:
-bench=ClientServerParallelTLS64
:制定了要運行的基準測試函數;-cpuprofile=cpu.log
:生成 CPU 性能分析文件,分析的數據會寫入cpu.log
文件當中,后續可以使用go tool pprof cpu.log
對該文件進行分析;-benchtime=5s
:控制每個基準測試的運行時間。默認情況下,go test
會自動決定基準測試的運行時長(比如一秒)。-benchtime=5s
強制每個基準測試至少運行五秒,使得測試結果更加穩定(尤其是在高并發場景當中)。需要注意的是,也可以指定基準測試的運行次數:-benchtime=100x
表示運行 100 次;net/http
:制定了要測試的包。- 這條語句隱含了
-run=NONE
,也就是會跳過普通測試,只運行基準測試。還隱含了-count=1
,默認只運行一次,可通過-count=N
重復運行,取平均值使得結果更準確。
再使用go tool pprof
對cpu.log
進行分析:
$ go tool pprof -text -nodecount=10 ./http.test cpu.log
File: http.test
Type: cpu
Time: 2025-06-23 15:49:06 CST
Duration: 16.84s, Total samples = 4.38s (26.00%)
Showing nodes accounting for 3.61s, 82.42% of 4.38s total
Dropped 288 nodes (cum <= 0.02s)
Showing top 10 nodes out of 219flat flat% sum% cum cum%1.76s 40.18% 40.18% 1.76s 40.18% syscall.syscall0.42s 9.59% 49.77% 0.42s 9.59% runtime.kevent0.35s 7.99% 57.76% 0.35s 7.99% runtime.pthread_cond_wait0.25s 5.71% 63.47% 0.25s 5.71% runtime.pthread_cond_signal0.25s 5.71% 69.18% 0.25s 5.71% runtime.pthread_kill0.18s 4.11% 73.29% 0.18s 4.11% runtime.madvise0.15s 3.42% 76.71% 0.15s 3.42% addMulVVWx0.13s 2.97% 79.68% 0.13s 2.97% runtime.usleep0.08s 1.83% 81.51% 0.23s 5.25% runtime.scanobject0.04s 0.91% 82.42% 0.04s 0.91% crypto/internal/fips140/bigmod.(*Nat).assign
參數-text
用于指定輸出格式,在這里每行是一個函數,根據 CPU 的時間長短來排序。-nodecount=10
限制了只輸出前 10 行的結果。對于嚴重的性能問題,這個文本格式基本可以幫助查明原因。
對于一些更微妙的問題,可以嘗試使用pprof
的圖形顯示功能,這需要安裝 GraphViz 工具。
示例函數
第三種被go test
特別對待的函數是示例函數,它以Example
為函數名開頭。示例函數沒有函數參數和返回值。下例是IsPalindrome
的示例函數:
func ExampleIsPalindrome() {fmt.Println(IsPalindrome("A man, a plan, a canal: Panama"))fmt.Println(IsPalindrome("palindrome"))// Output:// true// false
}
示例函數有三個用處。最主要的一個是作為文檔:一個包的例子可以更簡潔直觀的方式來演示函數的用法,比文字描述更直接易懂,特別是作為一個提醒或快速參考時。一個示例函數也可以方便展示屬于同一個接口的幾種類型或函數之間的關系,所有的文檔都必須關聯到一個地方,就像一個類型或函數聲明都統一到包一樣。同時,示例函數和注釋并不一樣,示例函數是真實的Go代碼,需要接受編譯器的編譯時檢查,這樣可以保證源代碼更新時,示例代碼不會脫節。
根據示例函數的后綴名部分,godoc 這個web文檔服務器會將示例函數關聯到某個具體函數或包本身,因此ExampleIsPalindrome
示例函數將是IsPalindrome
函數文檔的一部分,Example 示例函數將是包文檔的一部分。
示例函數的第二個用處是,在go test
執行測試的時候也會運行示例函數測試。如果示例函數內含有類似上面例子中的// Output:
格式的注釋,那么測試工具會執行這個示例函數,然后檢查示例函數的標準輸出與注釋是否匹配。
示例函數的第三個作用是,可以當做一個真實函數運行的模擬。http://golang.org
是由 godoc 提供的文檔服務,它使用 Go Playground 讓用戶可以在瀏覽器編輯和運行每一個示例函數。