返回归档
👨‍💻技术分享

go中常见的内存泄露场景

文章目录

什么是内存泄漏

程序运行中,动态分配的内存未能正确释放或回收,导致内存一直被占用不能被正确释放,可能导致最终内存耗尽

  • 系统资源耗尽:严重的内存泄漏可能会导致系统内存耗尽,进而引发系统崩溃或其他程序无法正常运行。
  • 程序崩溃:如果程序无法分配到足够的内存,它可能会崩溃,尤其是在没有进行异常处理的情况下。

常见场景

协程被永久阻塞

func leakyGoroutine() {
    ch := make(chan int)
    go func() {
        <-ch // Goroutine 永远在等待接收数据,但没有地方向该通道发送数据
    }()
}

context未及时取消

func leakyContext() {
    ctx, _ := context.WithCancel(context.Background())
    go func() {
        <-ctx.Done() // 如果取消函数未被调用,Goroutine 将一直等待
    }()
}

不正确的资源管理

func leakyFile() {
    file, err := os.Open("example.txt")
    if err != nil {
        return
    }
    // 忘记关闭文件,导致文件描述符泄漏
}

没有停止不再使用的time.Ticker

func leakyTicker() {
	counter := 0
	ticker := time.NewTicker(time.Second)
	//defer ticker.Stop()
	for range ticker.C {
		if counter == 3 {
			return
		}
		fmt.Println("-----")
		counter++
	}
}

子切片造成的暂时性内存泄漏

var s0 []int 

func g(s1 []int){
	s0=s1[len(s1)-30:] //s0引用着s1的内存块,s1的内存块得不到释放
}

可以使用copy,使得s0和s1不共用内存

var s0 []int

func g (s1 []int){
	copy(s0,s1[len(s1)-30:])
}

子字符串(复制操作)造成的暂时性内存泄漏

var s0 string // 一个包级变量

// 一个演示目的函数。
func f(s1 string) {
	s0 = s1[:50]
	// 目前,s0和s1共享着承载它们的字节序列的同一个内存块。
	// 虽然s1到这里已经不再被使用了,但是s0仍然在使用中,
	// 所以它们共享的内存块将不会被回收。虽然此内存块中
	// 只有50字节被真正使用,而其它字节却无法再被使用。
}

func demo() {
	s := createStringWithLengthOnHeap(1 << 20) // 1M bytes
	f(s)
}

func createStringWithLengthOnHeap(length int) string {
	bytes := make([]byte, length)
	return string(bytes)
}

可以使用一下方式防止内存泄漏

  • 使用strings.Builder
  • 使用stings.Clone()函数

未重置丢失的切片元素中的指针

func h() []*int {
	s := []*int{new(int), new(int), new(int), new(int)}

	//s[0] = nil
	return s[1:] //导致内存泄漏
}

循环引用

type Node struct {
    Value    int
    Children []*Node
    Parent   *Node
}

func leakyReference() {
    parent := &Node{}
    child := &Node{Parent: parent}
    parent.Children = append(parent.Children, child) // 父节点引用子节点,子节点引用父节点
}

总结

尽管 Go 的垃圾回收机制可以自动管理内存,但仍需要注意编写代码时避免上述场景,以防止内存泄漏影响程序性能和稳定性。

ref