Go语言Goroutines 泄漏场景与防治解决分析

 更新时间:2022年12月15日 09:44:38   作者:小马别过河  
这篇文章主要为大家介绍了Go语言Goroutines 泄漏场景与防治解决分析,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪

场景

Go 有很多自动管理内存的功能。比如:

  • 变量分配到堆内存还是栈内存,编译器会通过逃逸分析(escpage analysis)来判断;
  • 堆内存的垃圾自动回收。

即便如此,如果编码不谨慎,我们还是有可能导致内存泄漏的,最常见的是 goroutine 泄漏,比如下面的函数:

func goroutinueLeak() {
	ch := make(chan int)
	go func(ch chan int) {
    // 因为 ch 一直没有数据,所以这个协程会阻塞在这里。
		val := <-ch
		fmt.Println(val)
	}(ch)
}

由于ch一直没有发送数据,所以我们开启的 goroutine 会一直阻塞。每次调用goroutinueLeak都会泄漏一个goroutine,从监控面板看到话,goroutinue 数量会逐步上升,直至服务 OOM。

Goroutine 泄漏常见原因

channel 发送端导致阻塞

使用 context 设置超时是常见的一个场景,试想一下,下面的函数什么情况下会 goroutine 泄漏 ?

func contextLeak() error {
	ctx, cancel := context.WithTimeout(context.Background(), 1*time.Minute)
	defer cancel()
	ch := make(chan int)
        //g1
	go func() {
		// 获取数据,比如网络请求,可能时间很久
		val := RetriveData()
		ch <- val
	}()
	select {
	case <-ctx.Done():
		return errors.New("timeout")
	case val := <-ch:
		fmt.Println(val)
	}
	return nil
}

RetriveData() 如果超时了,那么contextLeak() 会返回 error,本函数执行结束。而我们开启的协程g1,由于没有接受者,会阻塞在 ch<-val

解决方法也能简单,比如可以给ch加上缓存。

channel 接收端导致阻塞

开篇给出的函数goroutinueLeak,就是因为channel的接收端收不到数据,导致阻塞。

这里举出另一个例子,下面的函数,是否有可能 goroutinue 泄漏?

func errorAssertionLeak() {
	ch := make(chan int)
        // g1
	go func() {
		val := <-ch
		fmt.Println(val)
	}()
        // RetriveSomeData 表示获取数据,比如从网络上
	val, err := RetriveSomeData()
	if err != nil {
		return
	}
	ch <- val
	return nil
}

如果 RetriveSomeData() 返回的err 不为 nil,那么本函数中断,也就不会有数据发送给ch,这导致协程g1会一直阻塞。

如何预防

goroutine 泄漏往往需要服务运行一段时间后,才会被发觉。

我们可以通过监控 goroutine 数量来判断是否有 goroutine 泄漏;或者用 pprof(之前文章介绍过的) 来定位泄漏的 goroutine。但这些已经是亡羊补牢了。最理想的情况是,我们在开发的过程中,就能发现。

本文推荐的做法是,使用单元测试。以开篇的 goroutinueLeak 为例子,我们写个单测:

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

执行 go test,发现测试是通过的:

=== RUN   TestLeak
--- PASS: TestLeak (0.00s)
PASS
ok      example/leak    0.598s

这是是因为单测默认不会检测 goroutine 泄漏的。

我们可以在单测中,加入Uber 团队提供的 uber-go/goleak 包:

import (
	"testing"
	"go.uber.org/goleak"
)
func TestLeak(t *testing.T) {
        // 加上这行代码,就会自动检测是否 goroutine 泄漏
	defer goleak.VerifyNone(t)
	goroutinueLeak()
}

这时候执行 go test,输出:

=== RUN   TestLeak
    /xxx/leak_test.go:12: found unexpected goroutines:
        [Goroutine 21 in state chan receive, with example/leak.goroutinueLeak.func1 on top of the stack:
        goroutine 21 [chan receive]:
        example/leak.goroutinueLeak.func1(0x0)
            /xxx/leak.go:9 +0x27
        created by example/leak.goroutinueLeak
            /xxx/leak.go:8 +0x7a
        ]
--- FAIL: TestLeak (0.46s)
FAIL
FAIL    example/leak    0.784s

这时候单测会因为 goroutine 泄漏而不通过。

如果你觉得每个测试用例都要加上 defer goleak.VerifyNone(t) 太繁琐的话(特别是在已有的项目中加上),goleak 提供了在 TestMain 中使用的方法VerifyTestMain,上面的单测可以修改成:

func TestLeak(t *testing.T) {
	goroutinueLeak()
}
func TestMain(m *testing.M) {
	goleak.VerifyTestMain(m)
}

总结

虽然我的文章经常提及单测,但我本人不是单元测试的忠实粉丝。扎实的基础,充分的测试,负责任的态度也是非常重要的。

引用

以上就是Go语言Goroutines 泄漏场景与防治解决分析的详细内容,更多关于Go Goroutines 泄漏防治的资料请关注脚本之家其它相关文章!

相关文章

  • Go语言字符串处理库strings包详解

    Go语言字符串处理库strings包详解

    本文详细介绍了Go语言中的strings库的使用方法,包括字符串的查找、替换、分割、比较、大小写转换等操作,strings库是Go语言中非常重要且功能丰富的标准库,几乎涵盖了所有字符串处理的需求
    2024-09-09
  • go程序执行交叉编译的流程步骤

    go程序执行交叉编译的流程步骤

    go程序可用通过交叉编译的方式在一个平台输出多个平台可运行的二进制包,本文给大家详细介绍了go程序执行交叉编译的流程步骤,文中有详细的代码示例供大家参考,需要的朋友可以参考下
    2024-07-07
  • golang动态创建类的示例代码

    golang动态创建类的示例代码

    这篇文章主要介绍了golang动态创建类的实例代码,本文通过实例代码给大家讲解的非常详细,需要的朋友可以参考下
    2023-06-06
  • Go语言实现文件上传

    Go语言实现文件上传

    这篇文章主要为大家详细介绍了Go语言实现文件上传,文中示例代码介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们可以参考一下
    2022-07-07
  • golang中package is not in GOROOT报错的真正解决办法

    golang中package is not in GOROOT报错的真正解决办法

    这篇文章主要给大家介绍了关于golang中package is not in GOROOT报错的真正解决办法,文中通过图文介绍的非常详细,对同样遇到这个问题的朋友具有一定的参考学习价值,需要的朋友可以参考下
    2023-03-03
  • Go语言共享内存读写实例分析

    Go语言共享内存读写实例分析

    这篇文章主要介绍了Go语言共享内存读写方法,实例分析了共享内存的原理与读写技巧,具有一定参考借鉴价值,需要的朋友可以参考下
    2015-02-02
  • 浅谈Go中数字转换字符串的正确姿势

    浅谈Go中数字转换字符串的正确姿势

    这篇文章主要介绍了浅谈Go中数字转换字符串的正确姿势,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2019-10-10
  • 详解Golang中gcache模块的基本使用

    详解Golang中gcache模块的基本使用

    这篇文章主要通过结合商业项目的使用场景,为大家介绍了gcache的基本使用、缓存控制以及淘汰策略。使用gcache做缓存处理,简单方便易上手
    2022-11-11
  • Go语言接口与多态详细介绍

    Go语言接口与多态详细介绍

    Go语言的接口类型是一组方法定义的集合,它体现了多态性、高内聚和低耦合的设计思想,接口通过interface关键字定义,无需实现具体方法,任何实现了接口所有方法的类型即视为实现了该接口,感兴趣的朋友一起看看吧
    2024-09-09
  • Go语言中实现完美错误处理实践分享

    Go语言中实现完美错误处理实践分享

    Go 语言是一门非常流行的编程语言,由于其高效的并发编程和出色的网络编程能力,越来越受到广大开发者的青睐。本文我们就来深入探讨一下Go 语言中的错误处理机制吧
    2023-04-04

最新评论