吃透 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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈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…