goleak 研究

偶然在 gocn 上看到这么一篇博文 酷Go推荐 Goroutine 泄漏防治神器 goleak,觉得这个项目挺有意思的

https://github.com/uber-go/goleak

用于检测一个方法中创建的协程是否发生泄漏(在主方法退出后的一段时间还未退出)

使用:

被测函数

1
2
3
4
5
6
7
8
func fakeLeakFunc(ch chan struct{}) {
ch <- struct{}{}
}

func leak() {
ch := make(chan struct{})
go fakeLeakFunc(ch)
}

在单一测试用例中测试

1
2
3
4
5
6
7
func TestLeak(t *testing.T) {
// 自定义忽略的函数名
defer goleak.VerifyNone(t, goleak.IgnoreTopFunction("code-area/leaktest.fakeLeakFunc"))
// 对单个测试用例进行检测
defer goleak.VerifyNone(t)
leak()
}

在当前文件中所有的测试用例结束之后测试

1
2
3
4
5
6
7
func TestMain(m *testing.M) {
goleak.VerifyTestMain(m)
}

func TestLeak(t *testing.T) {
leak()
}

原理

检测当前运行的所有 goroutine 的栈信息来进行分析。

利用 runtime.Stack() 方法来获取当前运行的所有 goroutine 的栈信息,随后定义一系列的 filter 过滤掉一些无用的 goroutine stack,判断是否还剩下其他的 goroutine。可以自定义 filter 来忽略特定的函数名

当然,它会检测多次,默认为 20 次,每次之间对多间隔 100ms

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
28
29
30
31
32
33
34
35
36
37
38
39
func filterStacks(stacks []stack.Stack, skipID int, opts *opts) []stack.Stack {
filtered := stacks[:0]
for _, stack := range stacks {
// Always skip the running goroutine.
if stack.ID() == skipID {
continue
}
// Run any default or user-specified filters.
if opts.filter(stack) {
continue
}
filtered = append(filtered, stack)
}
return filtered
}

func Find(options ...Option) error {
cur := stack.Current().ID()

opts := buildOpts(options...)
var stacks []stack.Stack
retry := true
for i := 0; retry; i++ {
stacks = filterStacks(stack.All(), cur, opts)

if len(stacks) == 0 {
return nil
}
retry = opts.retry(i)
}

return fmt.Errorf("found unexpected goroutines:\n%s", stacks)
}

func VerifyNone(t TestingT, options ...Option) {
if err := Find(options...); err != nil {
t.Error(err)
}
}

有意思的点

有个人提了 issue,说是检测不到 time.Tick 的泄漏,但是这个 time.Tick 并没有启动一个协程,而是向指定的协程中注册了一个计时器

项目中使用到了 functional options 的技巧

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
type Option interface {
apply(*opts)
}

type optionFunc func(*opts)

func (f optionFunc) apply(opts *opts) { f(opts) }

func IgnoreTopFunction(f string) Option {
return addFilter(func(s stack.Stack) bool {
return s.FirstFunction() == f
})
}

func addFilter(f func(stack.Stack) bool) Option {
return optionFunc(func(opts *opts) {
opts.filters = append(opts.filters, f)
})
}

小知识

runtime.Stack 相关

如果 runtime.Stack 中第二个参数设置为 true,则会首先答应当前 goroutine 的栈信息,然后再打印其他的栈信息

想要读取 runtime.Stack 中的 byte 数据,使用一个循环来尝试读取:

1
2
3
4
5
6
7
8
func getStackBuffer(all bool) []byte {
for i := _defaultBufferSize; ; i *= 2 {
buf := make([]byte, i)
if n := runtime.Stack(buf, all); n < i {
return buf[:n]
}
}
}

go 的 debug 库中是这样读取的:

1
2
3
4
5
6
7
8
9
10
func Stack() []byte {
buf := make([]byte, 1024)
for {
n := runtime.Stack(buf, false)
if n < len(buf) {
return buf[:n]
}
buf = make([]byte, 2*len(buf))
}
}

打印stack

除了上面的方法,还可以直接用 debug.PrintStack() 来打印当前协程的栈

打印出来的栈结构

1
2
3
4
5
6
7
8
9
10
11
goroutine 1 [running]:
main.getStackBuffer(0x1, 0x10d3950, 0xc00008c060, 0x1005285)
/Users/nizhenyang/Desktop/code-area/main.go:13 +0x78
main.main()
/Users/nizhenyang/Desktop/code-area/main.go:24 +0x5b

goroutine 18 [chan receive]:
main.main.func1(0xc00008c060)
/Users/nizhenyang/Desktop/code-area/main.go:22 +0x34
created by main.main
/Users/nizhenyang/Desktop/code-area/main.go:21 +0x52