偶然在 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 { if stack.ID() == skipID { continue } 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