吃透 Golang 基礎:測試

文章目錄

  • 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測試文件中包含TestPalindromeTestNonPalindrome兩個測試函數:

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.Fatalt.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/urlnet/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.isSpaceunicode.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 pprofcpu.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 讓用戶可以在瀏覽器編輯和運行每一個示例函數。

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/web/86526.shtml
繁體地址,請注明出處:http://hk.pswp.cn/web/86526.shtml
英文地址,請注明出處:http://en.pswp.cn/web/86526.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

酒店服務配置無門檻優惠券

1.查看酒店綁定的是那個倉庫&#xff1b; 凱里亞德酒店(深圳北站壹城中心店)&#xff0c;綁定的是“龍華民治倉&#xff08;睿嘀購” 2.“門店列表”選擇“龍華民治倉&#xff08;睿嘀購””中的“綁定場所” 3.通過酒店名字查找綁定的商品模板&#xff1b; 凱里亞德酒店(深圳…

IoT創新應用場景,賦能海外市場拓展

在數字化浪潮席卷全球的當下&#xff0c;物聯網&#xff08;Internet of Things, IoT&#xff09;正以革命性的力量重塑產業生態。這項通過傳感器、通信技術及智能算法實現設備互聯的技術&#xff0c;不僅推動全球從“萬物互聯”邁向“萬物智聯”&#xff0c;更成為賦能企業開拓…

Idea中Docker打包流程記錄

1. maven項目&#xff0c;先打package 2.添加Dockerfile 3.執行打包命令 注意最后的路徑 . docker buildx build -t xxx-app:版本號 -f Dockerfile . 4.下載文件 docker save -o xxx-app-版本號.tar xxx-app:版本號 5.加載鏡像 docker load -i xxx-app-版本號.tar 6.編…

硬件工程師筆試面試高頻考點-電阻

目錄 1.1 電阻選型時一般從哪幾個方面進行考慮? 1.2上拉下拉電阻的作用 1.3 PTC熱敏電阻作為電源電路保險絲的工作原理 1.4 如果阻抗不匹配&#xff0c;有哪些后果 1.5 電阻、電容和電感0402、0603和0805封裝的含義 1.6 電阻、電容和電感的封裝大小與什么參數有關 1.7 …

小程序入門:小程序 API 的三大分類

在小程序開發中&#xff0c;API&#xff08;Application Programming Interface&#xff09;起著至關重要的作用&#xff0c;它為開發者提供了豐富的功能和能力&#xff0c;使我們能夠創建出功能強大、用戶體驗良好的小程序。小程序 API 大致可分為以下三大分類&#xff1a;事件…

算法第55天|冗余連接、冗余連接II

冗余連接 題目 思路與解法 #include <iostream> #include <vector> using namespace std; int n; // 節點數量 vector<int> father(1001, 0); // 按照節點大小范圍定義數組// 并查集初始化 void init() {for (int i 0; i < n; i) {father[i] i;} } //…

Docker單獨部署grafana

Docker單獨部署grafana 環境說明 操作前提&#xff1a; 先去搭建PC端的MySQL和虛擬機 自行找參考 Linux部署docker參考文章&#xff1a; 02-Docker安裝_docker安裝包下載-CSDN博客 本文參考文章&#xff1a; 運維小記 說明&#xff1a; 本文的操作均以搭建好的PC端的MySQL和虛…

【數據分析,相關性分析】Matlab代碼#數學建模#創新算法

【數據分析&#xff0c;相關性分析】118-matlab代碼 #數學建模#創新算法 相關性分析及繪圖 基于最大互信息系數的特征篩選 最大互信息系數 皮爾遜相關系數 spearman相關系數 kendall秩相關系數 請自帶預算時間與需求以便高效溝通&#xff0c;回復超快&#xff0c;可以加急…

淺談C++ 中泛型編程(模版編程)

C 是一種強大且靈活的編程語言&#xff0c;支持多種編程范式&#xff0c;使得開發者能夠選擇最適合特定問題的解決方案。在實際開發中&#xff0c;面向對象編程、泛型編程、函數式編程和元編程是最常用的幾種范式。 今天主要與大家一起來介紹和學習泛型編程&#xff08;即模版…

iOS開發中的KVO以及原理

KVO概述 KVO(Key-Value-Observing)是iOS開發中一種觀察者模式實現&#xff0c;允許對象監聽另一個對象屬性的變化。當被觀察屬性的值發生變化時&#xff0c;觀察者會收到通知。KVO基于NSKeyValueObserving協議實現&#xff0c;是Foundation框架的核心功能之一。 1.KVO的基本使…

雷卯針對靈眸科技EASY Orin-nano RK3516 開發板防雷防靜電方案

一、應用場景 1. 人臉檢測 2. 人臉識別 3. 安全帽檢測 4. 人員檢測 5. OCR文字識別 6. 人頭檢測 7. 表情神態識別 8. 人體骨骼點識別 9. 火焰檢測 10. 人臉姿態估計 11. 人手檢測 12. 車輛檢測 13. 二維碼識別 二、 功能概述 1 CPU&#xff1a;八核64位ARM v8處…

中國雙非高校經費TOP榜數據分析

當我們習慣性仰望985、211這些“國家隊”時&#xff0c;一批地方重點支持的高校正悄悄發力&#xff0c;手握重金&#xff0c;展現出不遜于名校的“鈔能力”。特別是“雙非”大學中的佼佼者&#xff0c;它們的年度經費預算&#xff0c;足以讓許多普通院校望塵莫及。 今天就帶大…

C++ Lambda表達式詳解:從入門到精通

Lambda表達式是C11引入的最重要特性之一&#xff0c;它徹底改變了我們在C中編寫函數對象的方式。本文將帶你全面掌握Lambda表達式的使用技巧&#xff01; 1. 什么是Lambda表達式&#xff1f; Lambda表達式是C11引入的一種匿名函數對象&#xff0c;它允許我們在需要函數的地方…

實體類id字段選擇Integer還是Long?

Java實體類ID類型選擇&#xff1a;Integer vs Long 深度解析與最佳實踐 在Java實體類設計中&#xff0c;ID字段的類型選擇看似簡單&#xff0c;卻直接影響系統擴展性、性能和數據一致性。本文將深入探討Integer和Long兩種主鍵類型的差異&#xff0c;并通過實際案例展示如何做出…

變現與自我提升:加法與乘法的智慧抉擇

在當今這個快速發展的時代&#xff0c;無論是追求財富的變現&#xff0c;還是致力于個人能力的提升&#xff0c;我們都會面臨一個關鍵問題&#xff1a;是分類分步地逐步實現&#xff0c;還是將多種要素混合在一起&#xff1f;是簡單地做加法&#xff0c;還是復雜的乘法運算&…

鴻蒙 SideBarContainer 開發攻略:側邊欄交互設計與多端適配

一、引言&#xff1a;側邊欄布局的核心組件 在鴻蒙應用開發中&#xff0c;SideBarContainer 作為構建高效交互界面的核心組件&#xff0c;為開發者提供了靈活的側邊欄布局解決方案。該組件通過標準化的接口設計&#xff0c;實現了側邊欄與內容區的協同展示&#xff0c;適用于文…

Windows系統克隆硬盤后顯示容量與實際容量嚴重不符如何處理?

在 Windows 系統中&#xff0c;克隆硬盤后出現硬盤顯示容量與實際容量不符的問題&#xff0c;通常與分區布局、文件系統未正確調整或克隆工具設置有關。以下是可能的原因及對應的處理方案。 1. 問題原因分析 1.1 分區未正確調整 現象&#xff1a; 克隆后硬盤的總容量未正確顯…

EXCEL數據報表

客單價成交金額*成交客戶數 —— 提取年份 YEAR() 視圖-窗口-新建窗口&#xff0c;就能將excel的一個子表格單拎出來成為獨立窗口&#xff0c;方便對比查看 數據報表的單元格盡量都用公式來填補&#xff0c;鏈接到源表上去。這樣當源表有新數據更新進來后&#xff0c;報表也…

TCP/IP協議簡要概述

一、TCP/IP協議概述 &#xff08;一&#xff09;定義 TCP/IP&#xff08;Transmission Control Protocol/Internet Protocol&#xff09;協議是一組用于互聯網以及類似計算機網絡的通信協議。它是由網絡層的IP協議和傳輸層的TCP協議組成&#xff0c;但整個TCP/IP協議族包含很…

ubuntu下利用Qt添加相機設備并運行arm程序

一、編譯x86-64平臺的opencv demo 緊接上一篇&#xff0c;我電腦里現在同時存在兩個版本的opencv庫&#xff0c;一個是基于x86-64平臺的3.4.11庫&#xff0c;一個是基于arm平臺的4.7.0庫&#xff0c;現在我正常運行opencv的demo&#xff0c;直接報錯&#xff1a;沒有找到oencv…