脚本之家,脚本语言编程技术及教程分享平台!
分类导航

Python|VBS|Ruby|Lua|perl|VBA|Golang|PowerShell|Erlang|autoit|Dos|bat|

服务器之家 - 脚本之家 - Golang - Go 切片导致内存泄露,被坑两次了!

Go 切片导致内存泄露,被坑两次了!

2021-10-09 00:56脑子进煎鱼了陈煎鱼 Golang

在业务代码的编写上,我们经常会接受来自外部的接口数据,再把他插入到对应的数据结构中去,再进行下一步的业务聚合、裁剪、封装、处理。

Go 切片导致内存泄露,被坑两次了!

本文转载自微信公众号「脑子进煎鱼了」,作者陈煎鱼。转载本文请联系脑子进煎鱼了公众号。

大家好,我是煎鱼。

前段时间在我的 Go 读者群里,有小伙伴们在纠结切片(slice)的问题,我写了这篇《Go 切片这道题,吵了一个下午!》,引起了一拨各种讨论,还是比较欣慰的。

这不,有小伙伴给我提出了新的题材:

Go 切片导致内存泄露,被坑两次了!

来自读者微信提问

提出的是 Go 中很容易踩坑的切片内存泄露问题。作为宠粉的煎鱼肯定不会放过,争取让大家都避开这个 “坑”。

今天这篇文章,就由煎鱼带大家来了解这个问题:Go 切片可能可以怎么泄露法?

切片泄露的可能

在业务代码的编写上,我们经常会接受来自外部的接口数据,再把他插入到对应的数据结构中去,再进行下一步的业务聚合、裁剪、封装、处理。

像在 PHP 语言,常常会放到数组(array)中。在 Go 语言,会放到切片(slice)中。因此在 Go 的切片处理逻辑中,常常会涉及到如下类似的动作。

示例代码如下:

  1. vara[]int
  2. funcf(b[]int)[]int{
  3. a=b[:2]
  4. returna
  5. }
  6. funcmain(){
  7. ...
  8. }

仔细想想,这段程序有没有问题,是否存在内存泄露的风险?

答案是:有的。有明确的切片内存泄露的可能性和风险。

切片底层结构

可能有些小伙伴会疑惑,怎么就有问题了,是哪里有问题?

这里就得复习一下切片的底层基本数据结构了,切片在运行时的表现是 SliceHeader 结构体,定义如下:

  1. typeSliceHeaderstruct{
  2. Datauintptr
  3. Lenint
  4. Capint
  5. }
  • Data:指向具体的底层数组。
  • Len:代表切片的长度。
  • Cap:代表切片的容量。

要点是:切片真正存储数据的地方,是一个数组。切片的 Data 属性中存储的是指向所引用的数组指针地址。

背后的原因

在上述案例中,我们有一个包全局变量 a,共有 2 个切片 a 和 b,截取了 b 的一部分赋值给了 a,两者存在着关联。

从程序的直面来看,截取了 b 的一部分赋值给了 a,结构似乎是如下图:

Go 切片导致内存泄露,被坑两次了!

但我们进一步打开程序底层来看,他应该是如下图所示:

Go 切片导致内存泄露,被坑两次了!

切片 a 和 b 都共享着同一个底层数组(共享内存块),sliceB 包含全部所引用的字符。sliceA 只包含了 [:2],也就是 0 和 1 两个索引位的字符。

那他们泄露在哪里了?

泄露的点

泄露的点,就在于虽然切片 b 已经在函数内结束了他的使命了,不再使用了。但切片 a 还在使用,切片 a 和 切片 b 引用的是同一块底层数组(共享内存块)。

关键点:切片 a 引用了底层数组中的一段。

Go 切片导致内存泄露,被坑两次了!

虽然切片 a 只有底层数组中 0 和 1 两个索引位正在被使用,其余未使用的底层数组空间毫无作用。但由于正在被引用,他们也不会被 GC,因此造成了泄露。

解决办法

解决的办法,就是利用切片的特性。当切片的容量空间不足时,会重新申请一个新的底层数组来存储,让两者彻底分手。

示例代码如下:

  1. vara[]int
  2. varc[]int//第三者
  3. funcf(b[]int)[]int{
  4. a=b[:2]
  5. //新的切片append导致切片扩容
  6. c=append(c,b[:2]...)
  7. fmt.Printf("a:%p\nc:%p\nb:%p\n",&a[0],&c[0],&b[0])
  8. returna
  9. }

输出结果:

  1. a:0xc000102060
  2. c:0xc000124010
  3. b:0xc000102060

这段程序,新增了一个变量 c,他容量为 0。此时将期望的数据,追加过去。自然而然他就会遇到容量空间不足的情况,也就能实现申请新底层数据。

我们再将原本的切片置为 nil,就能成功实现两者分手的目标了。

总结

在今天这篇文章中,我们介绍了 Go 切片的一种常见的内存泄露方式。虽然我们在日常使用的时候可能没关注到。

主要原因还是由于切片的大多数使用场景,体量都比较小。又或是不知不觉就自己扩容了,就变成暂时性泄露了。

这依然是存在风险的,在编写 Go 代码时需要谨慎。毕竟这可是 Go 语言官方自己都踩过坑的 “坑”。

参考

An interesting way to leak memory with Go slices

internal/poll: avoid memory leak in Writev

slice 类型内存泄露的逻辑

golang slice内存泄露回收

原文链接:https://mp.weixin.qq.com/s/3Nsm_CFE7r60f6vFyyWkuw

延伸 · 阅读

精彩推荐
  • Golanggo语言制作端口扫描器

    go语言制作端口扫描器

    本文给大家分享的是使用go语言编写的TCP端口扫描器,可以选择IP范围,扫描的端口,以及多线程,有需要的小伙伴可以参考下。 ...

    脚本之家3642020-04-25
  • Golanggo日志系统logrus显示文件和行号的操作

    go日志系统logrus显示文件和行号的操作

    这篇文章主要介绍了go日志系统logrus显示文件和行号的操作,具有很好的参考价值,希望对大家有所帮助。一起跟随小编过来看看吧...

    SmallQinYan12302021-02-02
  • GolangGolang通脉之数据类型详情

    Golang通脉之数据类型详情

    这篇文章主要介绍了Golang通脉之数据类型,在编程语言中标识符就是定义的具有某种意义的词,比如变量名、常量名、函数名等等,Go语言中标识符允许由...

    4272021-11-24
  • GolangGolang中Bit数组的实现方式

    Golang中Bit数组的实现方式

    这篇文章主要介绍了Golang中Bit数组的实现方式,具有很好的参考价值,希望对大家有所帮助。一起跟随小编过来看看吧...

    天易独尊11682021-06-09
  • Golanggolang json.Marshal 特殊html字符被转义的解决方法

    golang json.Marshal 特殊html字符被转义的解决方法

    今天小编就为大家分享一篇golang json.Marshal 特殊html字符被转义的解决方法,具有很好的参考价值,希望对大家有所帮助。一起跟随小编过来看看吧 ...

    李浩的life12792020-05-27
  • Golanggolang如何使用struct的tag属性的详细介绍

    golang如何使用struct的tag属性的详细介绍

    这篇文章主要介绍了golang如何使用struct的tag属性的详细介绍,从例子说起,小编觉得挺不错的,现在分享给大家,也给大家做个参考。一起跟随小编过来看...

    Go语言中文网11352020-05-21
  • Golanggolang的httpserver优雅重启方法详解

    golang的httpserver优雅重启方法详解

    这篇文章主要给大家介绍了关于golang的httpserver优雅重启的相关资料,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,...

    helight2992020-05-14
  • Golanggolang 通过ssh代理连接mysql的操作

    golang 通过ssh代理连接mysql的操作

    这篇文章主要介绍了golang 通过ssh代理连接mysql的操作,具有很好的参考价值,希望对大家有所帮助。一起跟随小编过来看看吧...

    a165861639710342021-03-08