Contents

golang-学习笔记-错误处理与测试

go官方文档 go语言参考

在写过很多go代码之后,感觉自己并没有完全掌握go语言,还有很多知识盲区,所以有了这个go学习笔记系列,本系列是作者跟着电子书重新复习go语言相关内容的笔记

go-zero微服务实战系列(十一、大结局)

github gomock

gomock中文示例

csdn 超级强zcq

李文周博客

Go 检查和报告错误条件的惯有方式

  • 产生错误的函数会返回两个变量,一个值和一个错误码;如果后者是 nil 就是成功,非 nil 就是发生了错误。
  • 为了防止发生错误时正在执行的函数(如果有必要的话甚至会是整个程序)被中止,在调用函数后必须检查错误。

错误处理

1
2
3
type error interface {
    Error() string
}

当程序处于错误状态时可以用 os.Exit(1) 来中止运行。

定义错误

1
err := errors.New(math - square root of negative number)

每个包中有自己定义的error

用 fmt 创建错误对象

1
err = fmt.Errorf("usage: %s infile.txt outfile.txt", filepath.Base(os.Args[0]))

运行时异常和 panic

1
panic("A severe error occurred: stopping the program!")

当你调用panic函数时,Go运行时会立即停止当前函数的执行,运行任何已经被defer关键字注册的函数,然后向上返回到调用者。这个过程会一直持续,直到整个调用链被返回,导致程序终止

panic并不是通过操作系统的信号机制来实现的。相反,它是通过Go的运行时系统直接实现的

从 panic 中恢复(Recover)

必须为defer语句或者是defer语句调用的匿名函数,不能在defer块里面调用其他函数来recover

1
2
3
4
5
defer func() {
  if e := recover(); e != nil {
    fmt.Printf("Panicing %s\r\n", e)
  }
}()

如果一个Go语言程序中的panic没有被recover捕获,会发生以下几件事情:

  • 运行时会输出panic的值,以及在panic发生时的堆栈信息。
  • 运行时会将所有的goroutine关闭。这个过程中,如果在goroutine中使用了defer关键字注册的函数,那么这些函数会被调用。
  • 最后,程序将以非零的退出码退出。这个退出码通常是1,表明程序异常终止。

启动外部命令和程序

os 包有一个 StartProcess 函数可以调用或启动外部系统命令和二进制可执行文件;它的第一个参数是要运行的进程,第二个参数用来传递选项或参数,第三个参数是含有系统环境基本信息的结构体。这个在windows里面用不了。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
/* Linux: */
env := os.Environ()
procAttr := &os.ProcAttr{
  Env: env,
  Files: []*os.File{
      os.Stdin,
      os.Stdout,
      os.Stderr,
  },
}
// 1st example: list files
pid, err := os.StartProcess("/bin/ls", []string{"ls", "-l"}, procAttr)  
if err != nil {
  fmt.Printf("Error %v starting process!", err)  //
  os.Exit(1)
}

exec 包中也有同样功能的更简单的结构体和函数;主要是 exec.Command(name string, arg …string) 和 Run()

1
2
3
4
5
6
cmd := exec.Command("gedit")  // this opens a gedit-window
err = cmd.Run()
if err != nil {
  fmt.Printf("Error %v executing command!", err)
  os.Exit(1)
}

Go 中的单元测试和基准测试

_test 程序不会被普通的 Go 编译器编译,所以当放应用部署到生产环境时它们不会被部署;只有 gotest 会编译所有的程序:普通程序和测试程序。

测试函数必须有这种形式的头部:

1
func TestAbcde(t *testing.T)

T 是传给测试函数的结构类型,用来管理测试状态,支持格式化测试日志,如 t.Log,t.Error,t.ErrorF 等。在函数的结尾把输出跟想要的结果对比,如果不等就打印一个错误。成功的测试则直接返回。

用下面这些函数来通知测试失败:

  1. func (t *T) Fail()标记测试函数为失败,然后继续执行(剩下的测试)。
  2. func (t *T) FailNow()标记测试函数为失败并中止执行;文件中别的测试也被略过,继续执行下一个文件。
  3. func (t *T) Log(args …interface{}) args 被用默认的格式格式化并打印到错误日志中。
  4. func (t *T) Fatal(args …interface{}) 结合 先执行 3),然后执行 2)的效果。

–chatty 或 -v 选项,每个执行的测试函数以及测试状态会被打印。

testing 包中有一些类型和函数可以用来做简单的基准测试;测试代码中必须包含以 BenchmarkZzz 打头的函数并接收一个 *testing.B 类型的参数

1
2
3
4
5
func BenchmarkReverse(b *testing.B) {
  for n := 0; n < b.N; n++ {
    testfunc(xxx)
  }
}

命令 go test –-bench=.* 会运行所有的基准测试函数;代码中的函数会被调用 N 次(N是非常大的数,如 N = 1000000),并展示 N 的值和函数执行的平均时间,单位为 ns(纳秒,ns/op)。如果是用 testing.Benchmark 调用这些函数,直接运行程序即可

–bench对应的是一个正则表达式,匹配该正则表达式的基准测试函数才会执行(可以是子串匹配)

-benchmem 参数展示内存消耗情况

-cpu 使用几个cpu核心

-count 连续跑N次

-benchtime 指定运行秒数

b.ResetTimer()方法可以重置基准测试定时器,可以用于跳过耗时的初始化操作

测试的具体例子

even/even.go:

1
2
3
4
5
6
7
8
9
package even

func Even(i int) bool {        // Exported function
    return i%2 == 0
}

func Odd(i int) bool {        // Exported function
    return i%2 != 0
}

even/oddeven_test.go:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
package even

import "testing"

func TestEven(t *testing.T) {
    if !Even(10) {
        t.Log(" 10 must be even!")
        t.Fail()
    }
    if Even(7) {
        t.Log(" 7 is not even!")
        t.Fail()
    }

}

func TestOdd(t *testing.T) {
    if !Odd(11) {
        t.Log(" 11 must be odd!")
        t.Fail()
    }
    if Odd(10) {
        t.Log(" 10 is not odd!")
        t.Fail()
    }
}

由于测试需要具体的输入用例且不可能测试到所有的用例(非常像一个无穷的数),所以我们必须对要使用的测试用例思考再三。

至少应该包括:

  • 正常的用例
  • 反面的用例(错误的输入,如用负数或字母代替数字,没有输入等)
  • 边界检查用例(如果参数的取值范围是 0 到 1000,检查 0 和 1000 的情况)

用(测试数据)表驱动测试

把测试的输入数据和期望的结果写在一起组成一个数据表:表中的每条记录都是一个含有输入和期望值的完整测试用例,还可以结合像测试名字这样的额外信息来让测试输出更多的信息

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
var tests = []struct{     // Test table
    in  string
    out string

}{
    {in1, exp1},
    {in2, exp2},
    {in3, exp3},
...
}

func TestFunction(t *testing.T) {
    for i, tt := range tests {
        s := FuncToBeTested(tt.in)
        if s != tt.out {
            t.Errorf(%d. %q => %q, wanted: %q, i, tt.in, s, tt.out)
        }
    }
}

如果大部分函数都可以写成这种形式,那么写一个帮助函数 verify 对实际测试会很有帮助:

1
2
3
4
5
func verify(t *testing.T, testnum int, testcase, input, output, expected string) {
    if expected != output {
        t.Errorf(%d. %s with input = %s: output %s != %s, testnum, testcase, input, output, expected)
    }
}

TestFunction 则变为:

1
2
3
4
5
6
func TestFunction(t *testing.T) {
    for i, tt := range tests {
        s := FuncToBeTested(tt.in)
        verify(t, i, FuncToBeTested: , tt.in, s, tt.out)
    }
}

性能调试:分析并优化 Go 程序

时间和内存消耗

1
2
#!/bin/sh
/usr/bin/time -f '%Uu %Ss %er %MkB %C' "$@"

用 go test 调试

cpuprofile 和 -memprofile 标志向指定文件写入 CPU 或 内存使用情况报告

1
go test -x -v -cpuprofile=prof.out -file x_test.go

用 pprof 调试

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
var cpuprofile = flag.String("cpuprofile", "", "write cpu profile to file")

func main() {
    flag.Parse()
    if *cpuprofile != "" {
        f, err := os.Create(*cpuprofile)
        if err != nil {
            log.Fatal(err)
        }
        pprof.StartCPUProfile(f)
        defer pprof.StopCPUProfile()
    }
...

运行程序:progexec -cpuprofile=progexec.prof

使用gopprof工具查看:gopprof progexec progexec.prof

如果开启了 CPU 性能分析,Go 程序会以大约每秒 100 次的频率阻塞,并记录当前执行的 goroutine 栈上的程序计数器样本。

此工具一些有趣的命令:

  1. topN:展示分析结果中最频繁的 N 份样本,top5 它会展示在程序运行期间调用最频繁的 5 个函数
  2. web 或 web 函数名:生成一份 SVG 格式的分析数据图表,并在网络浏览器中打开它,函数被表示成不同的矩形(被调用越多,矩形越大),箭头指示函数调用链
  3. list 函数名 或 weblist 函数名,展示对应函数名的代码行列表,第 2 列表示当前行执行消耗的时间,这样就很好地指出了运行过程中消耗最大的代码。需要pprof.WriteHeapProfile(f)

覆盖率测试

首先,我们要确保所有的测试都正常通过:

-coverprofile标志参数重新运行测试

1
2
$ go test -run=Coverage -coverprofile=c.out gopl.io/ch7/eval
ok      gopl.io/ch7/eval         0.032s      coverage: 68.5% of statements

这里的-run选项对应一个正则表达式,匹配的函数名才会进行测试

查看HTML报告

1
$ go tool cover -html=c.out

并行测试

T 和 B 的 Run 方法允许定义子单元测试和子基准测试,而不必为每个子测试和子基准定义单独的函数。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
func TestGroupedParallel(t *testing.T) {
    for _, tc := range tests {
        tc := tc // capture range variable
        t.Run(tc.Name, func(t *testing.T) {
            //并行测试
            t.Parallel()
            ...
        })
    }
}

并行测试加快测试效率。

gomock

基于interface{}的模拟工具

mockgen

mockgen工具用于根据go文件写好的interface生成相应的mock.go文件。

两种使用方式:

  • mockgen -source=xxxx.go [other options]:源文件模式,将文件中所有interface都生成mock
  • mockgen packagepath Interface1,Interface2…:反射模式,根据需要将包中的指定interface生成mock

常用选项

  • -package: 指定mock类源文件的包名,默认位mock_输入文件的包名
  • -destination: mock类代码的输出文件,默认输出到标准输出,所以也可以使用重定向符号来输出到目标文件

使用方法

分4步

interface编写

infra/db.go

1
2
3
4
5
6
7
8
package db

type Repository interface {
   Create(key string, value []byte) error
   Retrieve(key string) ([]byte, error)
   Update(key string, value []byte) error
   Delete(key string) error
}

mock文件生成

1
mockgen -source=./infra/db.go -destination=./mock/mock_repository.go -package=mock

使用mock对象的文件

MySQL.go

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
package MySQL

import "GoExample/GoMock/infra"

type MySQL struct {
   DB db.Repository
}

func NewMySQL(db db.Repository) *MySQL {
   return &MySQL{DB: db}
}

func (mysql *MySQL) CreateData(key string, value []byte) error {
   return mysql.DB.Create(key, value)
}

func (mysql *MySQL) GetData(key string) ([]byte, error) {
   return mysql.DB.Retrieve(key)
}

func (mysql *MySQL) DeleteData(key string) error {
   return mysql.DB.Delete(key)
}

func (mysql *MySQL) UpdateData(key string, value []byte) error {
   return mysql.DB.Update(key, value)
}

测试用例编写

  1. 导入mock相关包
1
2
3
4
5
import (
   "testing"
   "GoExample/GoMock/mock"
   "github.com/golang/mock/gomock"
)
  1. mock控制器

mock控制器通过NewController接口生成,是mock生态系统的顶层控制,定义了mock对象的作用域和生命周期,以及mock对象的期望。当用例结束后,控制器会检查所有剩余期望的调用是否满足条件。

1
2
3
ctrl := NewController(t)
//1.5以上版本自动调用finish
defer ctrl.Finish()

mock对象创建时需要注入控制器

1
mockRepo := mock_db.NewMockRepository(ctrl)
  1. mock对象的行为注入

mock对象的行为注入,控制器通过map来维护,一个方法对应map的一项。一个方法在一个用例中可能调用多次,所以map的值类型是数组切片。当mock对象进行行为注入时,控制器会将行为Add。当该方法被调用时,控制器会将该行为Remove。

返回值

  • Return():返回指定值
  • Do(func):执行操作,忽略返回值
  • DoAndReturn(func):执行并返回指定值

Any()函数表示对参数没有要求,其他的还有Equ()函数,其实直接将限制字面量放到参数也可以进行相等限制

1
2
3
mockRepo.EXPECT().Retrieve(Any()).Return(nil, ErrAny)
mockRepo.EXPECT().Create(Any(), Any()).Return(nil)
mockRepo.EXPECT().Retrieve(Any()).Return(objBytes, nil)

可以使用Times关键字来批量注入行为:

1
mockRepo.EXPECT().Create(Any(), Any()).Return(nil).Times(5)

调用次数

  • Times() 断言 Mock 方法被调用的次数
  • MaxTimes() 最大次数
  • MinTimes() 最小次数
  • AnyTimes() 任意次数(包括 0 次)
  1. 行为调用的保序

默认情况下,行为调用顺序可以和mock对象行为注入顺序不一致,即不保序

  • 通过After关键字来实现保序
1
2
3
retrieveCall := mockRepo.EXPECT().Retrieve(Any()).Return(nil, ErrAny)
createCall := mockRepo.EXPECT().Create(Any(), Any()).Return(nil).After(retrieveCall)
mockRepo.EXPECT().Retrieve(Any()).Return(objBytes, nil).After(createCall)
  • 通过InOrder函数来实现保序(底层仍然是使用after)
1
2
3
4
5
gomock.InOrder(
mockRepo.EXPECT().Retrieve(Any()).Return(nil, ErrAny)
mockRepo.EXPECT().Create(Any(), Any()).Return(nil)
mockRepo.EXPECT().Retrieve(Any()).Return(objBytes, nil)
)

底层原理

1
2
3
4
5
func InOrder(calls ...*Call) {
   for i := 1; i < len(calls); i++ {
      calls[i].After(calls[i-1])
   }
}
  1. mock对象的注入

mock对象的行为都注入到控制器后,要将mock对象注入给interface,使得mock对象在测试中生效。

monkey

对函数和方法进行模拟,在运行时通过汇编语言重写可执行文件,将目标函数或方法的实现跳转到桩实现

  • monkey不支持内联函数,在测试的时候需要通过命令行参数-gcflags=-l关闭Go语言的内联优化
  • monkey不是线程安全的,所以不要把它用到并发的单元测试中

monkey的使用非常简单

对函数打桩

1
monkey.Patch(函数名, 函数定义)

main_test.go

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
package main

import (
	"bou.ke/monkey"
	"fmt"
	"testing"
)

func testFunc() {}
func TestAAA(t *testing.T) {
	monkey.Patch(testFunc, func() {
		fmt.Println("test success")
	})
	testFunc()
}

运行测试

1
go test main_test.go -run=AAA -v -gcflags=-l

对方法打桩

1
monkey.PatchInstanceMethod(reflect.Type,方法名,方法体)

main_test.go

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
package main

import (
	"bou.ke/monkey"
	"fmt"
	"reflect"
	"testing"
)

type User struct {
	Name     string
	Birthday string
}

func (*User) CalcAge() int { return 1 }
func TestAAA(t *testing.T) {
	var u = &User{
		Name:     "q1mi",
		Birthday: "1990-12-20",
	}
	monkey.PatchInstanceMethod(reflect.TypeOf(u), "CalcAge", func(*User) int {
		fmt.Println("test success")
		return 0
	})
	u.CalcAge()
}

运行测试

1
go test main_test.go -run=AAA -v -gcflags=-l

pprof性能分析

3种用法

  • runtime/pprof:采集程序指定区块的运行数据,生成 profile.proto 文件并对其进行分析。
  • net/http/pprof:基于 HTTP Server 运行,并且可以采集运行时数据进行分析。
  • go test:通过运行测试用例,并指定所需标识来进行采集。

可采集信息有5种

  • CPU Profiling:CPU 分析,按照一定的频率采集所监听的应用程序 CPU(含寄存器)的使用情况,可确定应用程序在主动消耗 CPU 周期时花费时间的位置。
  • Memory Profiling:内存分析,在应用程序进行堆分配时记录堆栈跟踪,用于监视当前和历史内存使用情况,以及检查内存泄漏。
  • Goroutine Profiling: Goroutine 分析,可以对当前应用程序正在运行的 Goroutine 进行堆栈跟踪和分析。
  • Mutex Profiling:互斥锁分析,报告互斥锁的竞争情况。
  • Block Profiling:阻塞分析,记录协程阻塞等待同步的情况。

net/http/pprof

启动

1
import _ "net/http/pprof"

引入时会自动给http默认server添加路由

你也可以自己添加路由

1
2
3
4
5
6
7
func init() {
	http.HandleFunc("/debug/pprof/", Index)
	http.HandleFunc("/debug/pprof/cmdline", Cmdline)
	http.HandleFunc("/debug/pprof/profile", Profile)
	http.HandleFunc("/debug/pprof/symbol", Symbol)
	http.HandleFunc("/debug/pprof/trace", Trace)
}

访问有两种方式:

  1. 直接在线访问路由

浏览器在线访问路由

可以看到一些分析指标的数量以及链接,可以点击链接查看详情。这些分析指标对应的含义是:

  • allocs:查看过去所有的内存分配信息。
  • block:查看导致阻塞同步的堆栈跟踪。
  • cmdline:当前程序的命令行的完整调用路径。
  • goroutine:查看当前所有运行的 goroutines 堆栈跟踪。
  • heap:查看活动对象的内存分配情况。
  • mutex:查看导致互斥锁的竞争持有者的堆栈跟踪。
  • profile:进行CPU分析,可以通过GET请求参数指定时间范围。
  • threadcreate:查看创建新操作系统线程的堆栈跟踪。
  • trace:生成对目前程序执行的分析文件,用于通过 go tool trace 进行分析,可以通过GET请求参数指定时间范围。

以及点击 full goroutine stack dump 将会列出各个协程详细的调用栈信息。

go tool的cli查看路由

1
go tool pprof <路由地址> [-http=:8081]

常用命令top、list、traces

  • top:按指标大小列出前10个函数,比如内存是按内存占用多少,CPU是按执行时间多少
  • list:查看某个函数的代码,以及该函数每行代码的指标信息,list main.main
  • traces:打印所有调用栈,以及调用栈的指标信息
  1. down到本地文件再访问
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
# CPU分析及运行分析网站
wget http://localhost:8080/debug/pprof/profile
go tool pprof -http=:8081 profile

# 内存占用分析及运行分析网站
wget http://localhost:8080/debug/pprof/heap
go tool pprof -http=:8081 heap

# 内存分配分析及运行分析网站
wget http://localhost:8080/debug/pprof/allocs
go tool pprof -http=:8081 allocs

# Goroutine分析及运行分析网站
wget http://localhost:8080/debug/pprof/goroutine
go tool pprof -http=:8081 goroutine

# 互斥锁分析及运行分析网站
wget http://localhost:8080/debug/pprof/mutex
go tool pprof -http=:8081 mutex

# 阻塞分析及运行分析网站
wget http://localhost:8080/debug/pprof/block
go tool pprof -http=:8081 block

# trace分析及运行分析网站
curl -o trace.out http://localhost:8080/debug/pprof/trace?seconds=5
go tool trace trace.out

runtime/pprof

生成pprof文件后用go tool打开

CPU分析

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
import (
	"runtime/pprof"
)

func main() {
	cpu_pprof, _ := os.Create("cpu.pprof")
	_ = pprof.StartCPUProfile(cpu_pprof)
    // do sth
	pprof.StopCPUProfile()
	_ = file.Close()
	_ = cpu_pprof.Close()
}

内存分析

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
import (
	"runtime/pprof"
)

func main() {
    // do sth
	runtime.GC()
	heap_pprof, _ := os.Create("heap.pprof")
	_ = pprof.WriteHeapProfile(heap_pprof)
	_ = heap_pprof.Close()
}

go test 测试用例

1
2
go test -bench=. -cpuprofile=cpu.profile
go test -bench=. -memprofile=mem.profile

然后再用 go tool pprof查看并分析

1
2
go tool pprof cpu.profile
go tool pprof -http=:8081 cpu.profile

Go性能调优pprof

李文周博客

CSDN raoxiaoya

  • CPU profile:报告程序的 CPU 使用情况,按照一定频率去采集应用程序在 CPU 和寄存器上面的数据
  • Memory Profile(Heap Profile):报告程序的内存使用情况
  • Block Profiling:报告 goroutines 不在运行状态的情况,可以用来分析和查找死锁等性能瓶颈
  • Goroutine Profiling:报告 goroutines 的使用情况,有哪些 goroutine,它们的调用关系是怎样的

内置标准库:

  • runtime/pprof:采集 工具型应用运行数据 进行分析
  • net/http/pprof:采集 服务型应用运行时数据 进行分析

工具型应用

在应用退出的时候把 profiling 的报告保存到文件中,进行分析

1
import "runtime/pprof"

CPU性能分析

开始记录

1
pprof.StartCPUProfile(w io.Writer)

停止记录

1
pprof.StopCPUProfile()

内存性能优化

记录堆栈信息

1
pprof.WriteHeapProfile(w io.Writer)

go tool pprof进行内存分析,默认-inuse_space选项,-inuse-objects选项查看分配对象数量

服务型应用

使用默认的http.DefaultServeMux:直接导入net/http/pprof自动帮你注册pprof路由

1
import _ "net/http/pprof"

自己的处理器Mux需要自己注册pprof路由

1
2
3
4
5
r.HandleFunc("/debug/pprof/", pprof.Index)
r.HandleFunc("/debug/pprof/cmdline", pprof.Cmdline)
r.HandleFunc("/debug/pprof/profile", pprof.Profile)
r.HandleFunc("/debug/pprof/symbol", pprof.Symbol)
r.HandleFunc("/debug/pprof/trace", pprof.Trace)

gin框架注册pprof路由:

1
pprof.Register(router)

运行应用后可以打开这些路由查看机器情况

  • /debug/pprof/profile:访问这个链接会自动进行 CPU profiling,持续 30s,并生成一个文件供下载
  • /debug/pprof/heap: Memory Profiling 的路径,访问这个链接会得到一个内存 Profiling 结果的文件
  • /debug/pprof/block:block Profiling 的路径
  • /debug/pprof/goroutines:运行的 goroutines 列表,以及调用关系

go tool pprof命令

读取pprof文件,出现命令行交互界面

1
go tool pprof [binary] [source]

topN 命令查看程序中占用CPU前3的函数

list 函数名 命令查看具体的函数分析

web 命令调用 graphviz 生成svg图片,需要安装graphviz,

pdf 命令可以生成可视化的pdf文件

help 命令可以提供所有pprof支持的命令说明

go-torch和火焰图

下载go-torch:

1
go get -v github.com/uber/go-torch

自行下载perl

下载FlameGraph,并将目录添加到PATH:

1
git clone https://github.com/brendangregg/FlameGraph.git

windows需要修改go-torch/render/flamegraph.go的enerateFlameGraph

1
2
3
4
5
6
7
8
9
// GenerateFlameGraph runs the flamegraph script to generate a flame graph SVG. func GenerateFlameGraph(graphInput []byte, args ...string) ([]byte, error) {
flameGraph := findInPath(flameGraphScripts)
if flameGraph == "" {
	return nil, errNoPerlScript
}
if runtime.GOOS == "windows" {
	return runScript("perl", append([]string{flameGraph}, args...), graphInput)
}
  return runScript(flameGraph, args, graphInput)

然后安装

1
go install github.com/uber/go-torch

压测工具wrk

1
go install github.com/adjust/go-wrk

使用go-torch

压测

1
go-wrk -n 50000 http://127.0.0.1:8080/book/list

生成火焰图

1
go-torch -u http://127.0.0.1:8080 -t 30

火焰图的y轴表示cpu调用方法的先后,x轴表示方法调用时间

火焰图分析内存性能数据

1
2
3
4
go-torch -inuse_space http://127.0.0.1:8080/debug/pprof/heap
go-torch -inuse_objects http://127.0.0.1:8080/debug/pprof/heap
go-torch -alloc_space http://127.0.0.1:8080/debug/pprof/heap
go-torch -alloc_objects http://127.0.0.1:8080/debug/pprof/heap

pprof与性能测试结合

go test 的-cpuprofile和-memprofile选项也会生成pprof文件

t.Skip

在 Go 中,testing 包提供了一个 T 类型,该类型有一个 Skip 方法,用于在测试函数内部标记该测试为跳过状态。当你希望临时跳过某些测试,但不希望完全删除或注释掉它们时,这是很有用的。

以下是如何使用 t.Skip 和相关方法的示例:

  1. t.Skip
    调用 t.Skip 会立即结束当前的测试,并将测试结果标记为跳过。

    1
    2
    3
    4
    5
    6
    
    func TestExample(t *testing.T) {
        if someCondition {
            t.Skip("Skipping because of some condition")
        }
        // Test code here...
    }
    
  2. t.Skipf
    t.Skip 类似,但允许你使用格式化字符串,类似于 fmt.Printf

    1
    2
    3
    4
    5
    6
    
    func TestExample(t *testing.T) {
        if someCondition {
            t.Skipf("Skipping because value is %d", someValue)
        }
        // Test code here...
    }
    
  3. t.SkipNow
    t.Skip 类似,但不需要参数。当你需要在没有消息的情况下跳过测试时使用。

    1
    2
    3
    4
    5
    6
    
    func TestExample(t *testing.T) {
        if someCondition {
            t.SkipNow()
        }
        // Test code here...
    }
    

需要注意的是,当你调用任何 t.Skip 相关的方法时,它将立即结束测试函数的执行,并且后续的代码不会运行。

此外,跳过的测试在执行 go test 时会在输出中标记为 SKIP,以便于你能够轻松地识别哪些测试已被跳过。

 |