验证go循环删除slice,map的操作和map delete操作不会释放底层内存的问题

目录

  • 切片 for 循环删除切片元素
    • 其他循环中删除slice元素的方法
      • 方法1
      • 方法2(推荐)
      • 方法3
    • 官方提供的方法
    • 结论
  • 切片 for 循环删除map元素
    • goalng map delete操作不会释放底层内存
    • go map原理
      • 源码
      • CRUD
        • 查询
        • 新增
      • 操作注意事项
        • map元素是无法取址的
        • map是线程不安全的

切片 for 循环删除切片元素

在 Go 语言中,使用 for 循环删除切片元素可能会引发意外的结果,因为切片的长度在循环过程中可能会发生变化,导致索引越界或不正确的元素被删除。这是因为在删除切片元素时,删除操作会影响切片的长度和索引,从而影响后续的迭代。

以下是一个示例,演示了在循环中删除切片元素可能引发的问题:

package main

import (
	"fmt"
)

func main() {
	// 8*5 =40
	slice := []int{1, 2, 2, 2, 2, 4, 5}
	fmt.Printf("切片长度:%d,容量:%d \n", len(slice), cap(slice))

	for index, value := range slice {
		if value == 2 {
			slice = append(slice[:index], slice[index+1:]...)
			fmt.Println("删除了一次2")
		}
		fmt.Println(index, value)
	}

	fmt.Println(slice)
	fmt.Printf("切片长度:%d,容量:%d \n", len(slice), cap(slice))

	slice = slice[:cap(slice)]
	fmt.Println(slice)
}

在这里插入图片描述

在这个示例中,删除切片 slice 中值为 2 的元素。然而,由于删除操作改变了切片的长度和索引,循环会出现问题。

接下来通过画图来解释这个现象:

  1. 这是开始的slice:

    slice := []int{1, 2, 2, 2, 2, 4, 5}
    fmt.Printf("切片长度:%d,容量:%d \n", len(slice), cap(slice))
    

    在这里插入图片描述

  2. 进入循环删除元素:

    for index, value := range slice {
    	if value == 2 {
    		slice = append(slice[:index], slice[index+1:]...)
    	}
    	fmt.Println(index, value)
    }
    

    在这里插入图片描述
    当index = 1时,删除第一次2后:

    在这里插入图片描述
    当index = 2时,删除第二次2后:

    在这里插入图片描述

在 Go 的 for index, val := range slice 循环中,indexval 在每次循环迭代中都会被重新赋值,以便遍历切片中的每个元素。这意味着在每次循环迭代中,indexval 都会随着切片中的元素不断变化。

例如,考虑以下代码片段:

slice := []int{1, 2, 3, 4, 5}
for index, val := range slice {
    fmt.Printf("Index: %d, Value: %d\n", index, val)
}

在这个循环中,index 会取遍历到的元素的索引值,val 会取遍历到的元素的值。每次循环迭代,indexval 都会随着切片中的元素变化,从 0 到切片长度减 1。

虽然 indexval 会在循环中变化,但在循环内部对它们的重新赋值不会影响切片本身。即使在循环内部修改了 indexval 的值,也不会影响切片中的元素。这是因为 indexval 是在每次迭代中以新的值被复制,不会直接影响原切片中的数据。

用文字描述就是:

// index = 0,val = 1 不删除 slice = [1,2,2,2,2,4,5],打印(index,val)=(0,1)
// index = 1,val = 2 删除   slice = [1,2(1),2(2),2,4,5],打印(index,val)=(1,2)
// index = 2,val = 2 删除   slice = [1,2(1),2,4,5],打印(index,val)=(2,2)
// index = 3,val = 4 不删除 
// index = 4,val = 5 不删除
// index = 5,val = 5 不删除
// index = 6,val = 5 不删除

index和val在循环开始时就已经确定了,所以打印时不受影响;但由于slice变化了,所以下一次循环开始时,index和val顺次增加从内存中取出的值却不是以前的值了,所以打印受到了影响。

正确的做法是,可以首先记录需要删除的元素的索引,然后再循环外面执行删除操作,避免在循环中修改切片。例如:

package main

import "fmt"

func main() {
    slice := []int{1, 2, 3, 4, 5}
    indexesToDelete := []int{}

    for index, value := range slice {
        if value == 3 {
            indexesToDelete = append(indexesToDelete, index)
        }
    }
	
	// 从后往前删除前面的不会受到影响
    for i := len(indexesToDelete) - 1; i >= 0; i-- {
        index := indexesToDelete[i]
        slice = append(slice[:index], slice[index+1:]...)
    }

    fmt.Println(slice)
}

在这个示例中,我们首先记录了需要删除的元素的索引,然后在第二个循环中进行了删除操作。这样可以避免在循环中修改切片,从而避免了索引越界和其他问题。

其他循环中删除slice元素的方法

a := []int{1, 2, 3, 4, 5},slice 删除大于 3 的数字

方法1

package main

import "fmt"

func main() {
	a := []int{1, 2, 3, 4, 5}
	for i := 0; i < len(a); i++ {
		if a[i] > 3 {
			// 当前元素被删除后,整体元素前移1位
			// 如果此时index++,相当于指针向后移动了两位,会导致跳过1位数组的读取
			// 因此,把i的自增行为抵消掉,指针不动,数组前移,i指向的地方自动会有下一个值填充进来
			a = append(a[:i], a[i+1:]...)
			i--
		}
	}
	fmt.Println(a)
}

方法2(推荐)

package main

import "fmt"

func main() {
	a := []int{1, 2, 3, 4, 5}
	j := 0

	for _, v := range a {
		if v <= 3 {
			a[j] = v
			// 符合条件的顺次赋值给前面的数组
			j++
		}
	}
	// 通过一次切片操作,将len置为j
	// 相当于只有len<=j的数组才可以看到
	a = a[:j]
	fmt.Println(a)
}

方法3

package main

import "fmt"

func main() {
	a := []int{1, 2, 3, 4, 5}
	j := 0
	// 相当于将a拷贝到q
	q := make([]int, len(a))
	for _, v := range a {
		if v <= 3 {
			q[j] = v
			j++
		}
	}
	q = q[:j] // q is copy with numbers >= 0
	fmt.Println(q)
}

官方提供的方法

go1.21版本后提供了slice库,封装了常用的slice方法:

func DeleteFunc[S ~[]E, E any](s S, del func(E) bool) S {
	// Don't start copying elements until we find one to delete.
	for i, v := range s {
		if del(v) {
			j := i
			for i++; i < len(s); i++ {
				v = s[i]
				if !del(v) {
					s[j] = v
					j++
				}
			}
			return s[:j]
		}
	}
	return s
}

del(v)改为v <= 3

func DeleteFunc[S ~[]int](s S) S {
	// Don't start copying elements until we find one to delete.
	for i, v := range s {
		if v <= 3 {
			j := i
			for i++; i < len(s); i++ {
				v = s[i]
				if !(v <= 3) {
					s[j] = v
					j++
				}
			}
			return s[:j]
		}
	}
	return s
}

官方的操作和方法2非常相似,

func main() {
	a := []int{1, 2, 3, 4, 5}
	a = DeleteFunc(a)
	fmt.Println(a)
	a = a[:cap(a)]
	fmt.Println(a)
}

在这里插入图片描述

由于切片的扩缩容机制,基本上必须要把切片返回,防止切片底层指向的地址变动导致外部感受不到。

结论

  1. 当使用 for range 循环(for range) 遍历切片时,key 返回的是切片的索引,value 返回的是索引对应的值的拷贝。
  2. 在 Go 语言中,使用 for 循环删除切片元素可能会引发意外的结果,因为切片的长度在循环过程中可能会发生变化,导致索引越界或不正确的元素被删除。这是因为在删除切片元素时,删除操作会影响切片的长度和索引,从而影响后续的迭代。

切片 for 循环删除map元素

前提知识:map为什么会有这种无序性呢?map在某些条件下会自动扩容和重新hash所有的key以便存储更多的数据。 因为散列值映射到数组索引上本身就是随机的,在重新hash前后,key的顺序自然就会改变了。所以Go的设计者们就对map增加了一种随机性,以确保开发者在使用map时不依赖于有序的这个特性。

一句话:for循环中删除map元素是安全的。

官方go1.21 maps包中的删除方法:

// DeleteFunc deletes any key/value pairs from m for which del returns true.
func DeleteFunc[M ~map[K]V, K comparable, V any](m M, del func(K, V) bool) {
	for k, v := range m {
		if del(k, v) {
			delete(m, k)
		}
	}
}

奇怪的是,删除元素是安全的,新增元素却是不可预知的:

func main() {
	m := map[int]bool{
		0: true,
		1: false,
		2: true,
	}

	for k, v := range m {
		if v {
			m[10+k] = true
		}
	}
	fmt.Println(m)
}

在这里插入图片描述

上面这段代码的输出结果是不确定的。为什么呢?Go的官方文档中有这样的一段话:

If a map entry is created during iteration, it may be produced during the iteration or skipped. The choice may vary for each entry created and from one iteration to the next. – Go spec

大致的意思就是:

在遍历map期间,如果有一个新的key被创建,那么,在循环遍历过程中可能会被输出,也可能会被跳过。对于每一个创建的key在迭代过程中是选择输出还是跳过都是不同的。

也就是说,在迭代期间创建的key,有的可能会被输出,也的就可能会被跳过。这就是由于map中key的无序性造成的。

怎么解决上述问题,让输出结果变的是稳定的呢?最简单的方案就是使用复制:

m := map[int]bool{
    0: true,
    1: false,
    2: true,
}
m2 := make(map[int]bool)
for k, v := range m {
    m2[k] = v
    if v {
        m2[10+k] = true
    }
}
fmt.Println(m2)

由此可知,通过一个新的map,将读和写分离。即从m中读,在m2中更新,这样就能保持稳定的输出结果:

map[0:true 1:false 2:true 10:true 12:true]

goalng map delete操作不会释放底层内存

package main

import (
    "fmt"
    "runtime"
)

//var a = make(map[int]struct{})

func main() {
    v := struct{}{}

    a := make(map[int]struct{})

    for i := 0; i < 10000; i++ {
        a[i] = v
    }

    runtime.GC()
    printMemStats("添加1万个键值对后")
    fmt.Println("删除前Map长度:", len(a))

    for i := 0; i < 10000-1; i++ {
        delete(a, i)
    }
    fmt.Println("删除后Map长度:", len(a))

    // 再次进行手动GC回收
    runtime.GC()
    printMemStats("删除1万个键值对后")

    // 设置为nil进行回收
    a = nil
    runtime.GC()
    printMemStats("设置为nil后")
}

func printMemStats(mag string) {
    var m runtime.MemStats
    runtime.ReadMemStats(&m)
    fmt.Printf("%v:分配的内存 = %vKB, GC的次数 = %v\n", mag, m.Alloc/1024, m.NumGC)
}

可以看到,新版本的 Golang 难道真的会回收 map 的多余空间,难道哈希表会随着 map 里面的元素变少,然后缩小了?
在这里插入图片描述
将 map 放在外层:

package main

import (
	"fmt"
	"runtime"
)

var a = make(map[int]struct{})

func main() {
	v := struct{}{}

	//a := make(map[int]struct{})

	for i := 0; i < 10000; i++ {
		a[i] = v
	}

	runtime.GC()
	printMemStats("添加1万个键值对后")
	fmt.Println("删除前Map长度:", len(a))

	for i := 0; i < 10000-1; i++ {
		delete(a, i)
	}
	fmt.Println("删除后Map长度:", len(a))

	// 再次进行手动GC回收
	runtime.GC()
	printMemStats("删除1万个键值对后")

	// 设置为nil进行回收
	a = nil
	runtime.GC()
	printMemStats("设置为nil后")
}

func printMemStats(mag string) {
	var m runtime.MemStats
	runtime.ReadMemStats(&m)
	fmt.Printf("%v:分配的内存 = %vKB, GC的次数 = %v\n", mag, m.Alloc/1024, m.NumGC)
}

在这里插入图片描述
这时 map 好像内存没变化,直到设置为 nil。

为什么全局变量就会不变呢?

将局部变量添加一万个数,然后再删除9999个数,再添加9999个,看其变化:

package main

import (
	"fmt"
	"runtime"
)

//var a = make(map[int]struct{})

func main() {
	v := struct{}{}

	a := make(map[int]struct{})

	for i := 0; i < 10000; i++ {
		a[i] = v
	}

	runtime.GC()
	printMemStats("添加1万个键值对后")
	fmt.Println("删除前Map长度:", len(a))

	for i := 0; i < 10000-1; i++ {
		delete(a, i)
	}
	fmt.Println("删除后Map长度:", len(a))

	// 再次进行手动GC回收
	runtime.GC()
	printMemStats("删除1万个键值对后")

	for i := 0; i < 10000-1; i++ {
		a[i] = v
	}

	// 再次进行手动GC回收
	runtime.GC()
	printMemStats("再一次添加1万个键值对后")

	// 设置为nil进行回收
	a = nil
	runtime.GC()
	printMemStats("设置为nil后")
}

func printMemStats(mag string) {
	var m runtime.MemStats
	runtime.ReadMemStats(&m)
	fmt.Printf("%v:分配的内存 = %vKB, GC的次数 = %v\n", mag, m.Alloc/1024, m.NumGC)
}

在这里插入图片描述
这次局部变量删除后,和全局变量map一样了,内存也没变化。

但是添加10000个数后内存反而变小了。

map删除元素后map内存是不会释放的,无论是局部还是全局,但引出了上面一个奇怪的问题。

https://github.com/golang/go/issues/20135

为什么添加10000个数后内存反而变小了?因为 Golang 编译器有提前优化功能,它知道后面 map a 已经不会被使用了,所以会垃圾回收掉,a = nil 不起作用

go map原理

源码

// A header for a Go map.
type hmap struct {
	count     int // map元素的个数,len()的返回值
	flags     uint8 // 状态标识,比如正在被写、buckets和oldbuckets在被遍历、等量扩容(Map扩容相关字段)
	B         uint8  // B的值==log_2(buckets的长度)
	noverflow uint16 // 溢出桶里bmap大致的数量
	hash0     uint32 // hash因子

	buckets    unsafe.Pointer // 2^B个桶对应的指针数组的指针
	oldbuckets unsafe.Pointer // 旧指针,用于扩缩容
	nevacuate  uintptr        // 记录渐进式扩容阶段下一个要迁移的旧桶编号 

	extra *mapextra // 可选字段
}

// bucket结构体定义
 type bmap struct {
     tophash [8]uint8 //存储哈希值的高8位
     keys // key数组
     elems // 值数组
     overflow *bmap   //溢出bucket的地址
 }

type mapextra struct {
	overflow    *[]*bmap
	oldoverflow *[]*bmap

	// nextOverflow 持有一个指向空闲溢出桶的指针。
	nextOverflow *bmap
}

在这里插入图片描述

  1. tophash用来快速查找key值是否在该bucket中,而不同每次都通过真值进行比较;
  2. 根据注释(us to eliminate padding which would be needed for, e.g., map[int64]int8.),map[int64]int8,key是int64(8个字节),value是int8(一个字节),kv的长度不同,如果按照kv格式存放,则考虑内存对齐v也会占用int64,而按照后者存储时,8个v刚好占用一个int64。
    在这里插入图片描述

CRUD

将B初始化为4,则buckets为16

查询

在这里插入图片描述

  1. 计算key的hash值。

  2. 通过最后的“B”位来确定在哪号桶,此时B为4,所以取k4对应哈希值的后4位,也就是0101

  3. 根据key对应的hash值前8位快速确定是在这个桶的哪个位置

  4. 对比key完整的hash是否匹配,如果匹配则获取对应value

  5. 如果都没有找到,就去连接的下一个溢出桶中找

新增

在这里插入图片描述

  1. 通过key获取hash值
  2. hash值的低八位和bucket数组长度取余,定位到在数组中的哪个个下标
  3. hash值的高八位存储在bucket中的tophash中,用来快速判断key是否存在,key和value的具体值则通过指针运算存储,当一个bucket满时,通过overfolw指针链接到下一个bucket。

操作注意事项

map元素是无法取址的

  1. 可以得到m[key],但是无法对它的值作出任何修改,除非使用带指针的value。
  2. 因为map 会随着元素数量的增长而重新分配更大的内存空间,会导致之前的地址无效。

map是线程不安全的

某map桶数量为4,即B=2,此时 goroutine1来插入key1, goroutine2来读取 key2. 可能会发生如下过程:

  1. goroutine2 计算key2的hash值,B=2,并确定桶号为1。

  2. goroutine1添加key1,触发扩容条件。

  3. B=B+1=3, buckets数据迁移到oldbuckets。

  4. goroutine2从桶1中遍历,获取数据失败。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mfbz.cn/a/95984.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

亚马逊鲲鹏系统是怎么操作测评的

亚马逊鲲鹏系统可以注册亚马逊买家号、养号、下单留评等&#xff0c;是一款功能比较齐全的测评软件&#xff0c;具体操作如下&#xff1a; 首先我们需要先准备好买家账号&#xff0c;账号可以直接去购买已经注册好了的账号&#xff0c;也可以准备好账号所需要的一些邮箱、ip、…

RealVNC配置自定义分辨率(AlmaLinux 8)

RealVNC 配置自定义分辨率&#xff08;AlmaLinux8&#xff09; 参考RealVNC官网 how to set up resolution https://help.realvnc.com/hc/en-us/articles/360016058212-How-do-I-adjust-the-screen-resolution-of-a-virtual-desktop-under-Linux-#standard-dummy-driver-0-2 …

docker部署nginx,部署springboot项目,并实现访问

一、先部署springboot项目 1、安装docker&#xff1a; yum install docker -y 2、启动docker&#xff1a; service docker start 重启&#xff1a; service docker restart 3、查看版本&#xff1a; docker -v 4、使设置docker.service生效&#xff08;路径&#xff1a;…

国产自主可控C++工业软件可视化图形架构源码

关于国产自主代替的问题是当前热点&#xff0c;尤其是工业软件领域。 “一个功能强大的全自主C跨平台图形可视化架构对开发自主可控工业基础软件至关重要&#xff01;” 作为全球领先的C工业基础图形可视化软件提供商&#xff0c;UCanCode软件有自己的思考&#xff0c;我们认…

uniapp 开发小程序,封装一个方法,让图片使用线上地址

1.在main.js文件中&#xff0c;添加以下代码&#xff1a; 复制使用&#xff1a; // 图片使用网络地址 Vue.prototype.localImgSrc function(img){//项目的地址域名&#xff0c;例如百度return "https://baidu.cn/static/index/images/" img; }2.在页面中直接使用&…

SSM - Springboot - MyBatis-Plus 全栈体系(二)

第一章 Maven 三、Maven 核心功能依赖和构建管理 1. 依赖管理和配置 Maven 依赖管理是 Maven 软件中最重要的功能之一。Maven 的依赖管理能够帮助开发人员自动解决软件包依赖问题&#xff0c;使得开发人员能够轻松地将其他开发人员开发的模块或第三方框架集成到自己的应用程…

MongoDB实验——在MongoDB集合中查找文档

在MongoDB集合中查找文档 一、实验目的二、实验原理三、实验步骤1.启动MongoDB数据库、启动MongoDB Shell客户端2.数据准备-->person.json3.指定返回的键4 .包含或不包含 i n 或 in 或 in或nin、$elemMatch&#xff08;匹配数组&#xff09;5.OR 查询 $or6.Null、$exists7.…

MySQL事物和存储引擎

事务 一、MySQL事务的概念 事务是一种机制、一个操作序列&#xff0c;包含了一组数据库操作命令&#xff0c;并且把所有的命令作为一个整体一起向系统提交或撤销操作请求&#xff0c;即这一组数据库命令要么都执行&#xff0c;要么都不执行。 事务是一个不可分割的工作逻辑单…

基于ssm+vue汽车售票网站源码和论文

基于ssmvue汽车售票网站源码和论文088 开发工具&#xff1a;idea 数据库mysql5.7 数据库链接工具&#xff1a;navcat,小海豚等 技术&#xff1a;ssm 摘 要 互联网发展至今&#xff0c;无论是其理论还是技术都已经成熟&#xff0c;而且它广泛参与在社会中的方方面面。它让…

基于RabbitMQ的模拟消息队列之三——硬盘数据管理

文章目录 一、数据库管理1.设计数据库2.添加sqlite依赖3.配置application.properties文件4.创建接口MetaMapper5.创建MetaMapper.xml文件6.数据库操作7.封装数据库操作 二、文件管理1.消息持久化2.消息文件格式3.序列化/反序列化4.创建文件管理类MessageFileManager5.垃圾回收 …

时序预测 | MATLAB实现基于QPSO-BiLSTM、PSO-BiLSTM和BiLSTM时间序列预测

时序预测 | MATLAB实现基于QPSO-BiLSTM、PSO-BiLSTM和BiLSTM时间序列预测 目录 时序预测 | MATLAB实现基于QPSO-BiLSTM、PSO-BiLSTM和BiLSTM时间序列预测效果一览基本描述程序设计参考资料 效果一览 基本描述 1.Matlab实现QPSO-BiLSTM、PSO-BiLSTM和BiLSTM神经网络时间序列预测…

RISC-V 中国峰会 | OpenMPL引人注目,RISC-V Summit China 2023圆满落幕

RISC-V中国峰会圆满落幕 2023年8月25日&#xff0c;为期三天的RISC-V中国峰会&#xff08;RISC-V Summit China 2023&#xff09;圆满落幕。本届峰会以“RISC-V生态共建”为主题&#xff0c;结合当下全球新形势&#xff0c;把握全球新时机&#xff0c;呈现RISC-V全球新观点、新…

TDesign表单rules通过函数 实现复杂逻辑验证输入内容

Element ui 中 我们可以通过validator 绑定函数来验证一些不在表单model中的值 又或者处理一下比较复杂的判断逻辑 TDesign也有validator 但比较直观的说 没有Element那么好用 这里 我们给validator绑定了我们自己的checkAge函数 这个函数中 只有一个参数 value 而且 如果你的…

无涯教程-Android - Activity

Activity代表具有用户界面的单个屏幕&#xff0c;就像Java的窗口或框架一样。Android Activity 是ContextThemeWrapper类的子类。 如果您使用过C&#xff0c;C或Java编程语言&#xff0c;那么您一定已经看到您的程序从 main()函数开始。与之非常相似&#xff0c;Android系统以 …

【Ubuntu】Ubuntu常用软件部署

1.安装jdk1.8 (1).apt方式安装 1).安装 1.在终端中输入以下命令&#xff0c;以更新软件包列表 sudo apt-get update2.在终端中输入以下命令&#xff0c;以安装JDK 1.8 sudo apt-get install openjdk-8-jdk3.将Java 1.8设置为默认版本。在终端中输入以下命令 sudo update-…

Linux 进程的睡眠和唤醒详解

概要 在Linux中&#xff0c;仅等待CPU时间的进程称为就绪进程&#xff0c;它们被放置在一个运行队列中&#xff0c;一个就绪进程的状 态标志位为 TASK_RUNNING。一旦一个运行中的进程时间片用完&#xff0c; Linux 内核的调度器会剥夺这个进程对CPU的控制权&#xff0c;并且从运…

java对象创建的过程

1、检查指令的参数是否能在常量池中定位到一个类的符号引用 2、检查此符号引用代表的类是否已被加载、解析和初始化过。如果没有&#xff0c;就先执行相应的类加载过程 3、类加载检查通过后&#xff0c;接下来虚拟机将为新生对象分配内存。 4、内存分配完成之后&#xff0c;…

钉钉小程序引用阿里巴巴图标

2.打开的界面如图&#xff0c;先建一个iconfont.acss文件&#xff0c;全选浏览器打开的样式代码&#xff0c;复制粘贴进新建的iconfont.acss文件中 3.使用

MySQL一行记录是如何存储的?

目录 MySQL的数据存放在哪个文件&#xff1f; 表空间文件的结构是怎么样的&#xff1f; 1、行&#xff08;row&#xff09; 2、页&#xff08;page&#xff09; 3、区&#xff08;extent&#xff09; 4、段&#xff08;segment&#xff09; InnoDB 行格式有哪些&#xf…

01-Flask-简介及环境准备

Flask-简介及环境准备 前言简介特点Flask 与 Django 的比较环境准备 前言 本篇来介绍下Python的web框架–Flask。 简介 Flask 是一个轻量级的 Web 框架&#xff0c;使用 Python 语言编写&#xff0c;较其他同类型框架更为灵活、轻便且容易上手&#xff0c;小型团队在短时间内…
最新文章