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

news2025/1/24 11:40:06

目录

  • 切片 for 循环删除切片元素
    • 其他循环中删除slice元素的方法
      • 方法1
      • 方法2(推荐)
      • 方法3
    • 官方提供的方法
    • 结论
  • 切片 for 循环删除map元素
    • goalng map delete操作不会释放底层内存

切片 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 不起作用

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

BookStack开源免费知识库docker-compose部署

BookStack&#xff08;书栈&#xff09;是一个功能强大且易于使用的开源知识管理平台&#xff0c;适用于个人、团队或企业的文档协作和知识共享。 一、BookStack特点 简单易用&#xff1a;BookStack提供了一个直观的用户界面&#xff0c;使用户能够轻松创建、编辑和组织文档多…

SpringBoot整合 redis key (过期、新增、修改)的三种方式,看这篇就够了

文章目录 原理关于 *notify-keyspace-events*关于redis的消息主题&#xff08;Topic&#xff09;重写监听容器注册自定义解析常见整合问题鸣谢 文章主要描述了Springboot整合key变化的三种方式&#xff0c;同时列出了一些整合坑点与概念 原理 SpringBoot整合Redis key变化的原…

二叉树的非递归遍历实现(三种)

1、先序遍历 先序遍历使用了栈的结构&#xff0c;先压入根节点&#xff0c;然后依次将其右子节点和左字节点压入。然后就可以实现“头左右”的遍历顺序 /*** 先序遍历*/public static void pre_order(TreeNode treeNode){if (treeNode null){return;}Stack<TreeNode> …

PAT 1136 A Delayed Palindrome

个人学习记录&#xff0c;代码难免不尽人意 A B C where A is the original number, B is the reversed A, and C is their sum. A starts being the input number, and this process ends until C becomes a palindromic number – in this case we print in the last line …

Graylog 更改显示的时区(Display timezone)

每个 Graylog 用户都可以配置他们的显示时区。 这会更改用于查看日志消息的时区&#xff0c;但不会更改日志消息的原始时区。 默认情况下&#xff0c;Graylog 显示 UTC 格式的所有时间戳&#xff08;也就是 0:00&#xff09;。就像是下面这样 非Admin账户要更改时区&#xff1…

防御网络攻击风险的4个步骤

如今&#xff0c;人们正在做大量工作来保护 IT 系统免受网络犯罪的侵害。令人担忧的是&#xff0c;对于运营技术系统来说&#xff0c;情况却并非如此&#xff0c;运营技术系统用于运行从工厂到石油管道再到发电厂的所有业务。 组织应该强化其网络安全策略&#xff0c;因为针对…

ConsoleApplication815项目(直接加载+VEH Hook Load)

上线图 ConsoleApplication815.cpp #include <iostream> #include<Windows.h> #include "detours.h" #include "detver.h" #pragma comment(lib,"detours.lib")#pragma warning(disable:4996)LPVOID Beacon_address; SIZE_T Beacon…

统计教程|PASS实现单因素多水平方差分析的样本含量估计

前面我们讲过当主要结局指标是连续变量时&#xff0c;两总体均数比较时样本量估计的计算公式原理以及PASS软件操作教程。当设计研究的试验因素只有一个&#xff0c;并且该因素的水平数&#xff08;组数&#xff09;k≥3&#xff0c;当主要研究指标为连续变量时我们常用单因素多…

设计模式—原型模式(Prototype)

目录 一、什么是原型模式&#xff1f; 二、原型模式具有什么优缺点吗&#xff1f; 三、有什么缺点&#xff1f; 四、什么时候用原型模式&#xff1f; 五、代码展示 ①、简历代码初步实现 ②、原型模式 ③、简历的原型实现 ④、深复制 ⑤、浅复制 一、什么是原型模式&…

2023-8-28 树的重心(树与图的深度优先遍历)

题目链接&#xff1a;树的重心 #include <cstring> #include <iostream> #include <algorithm>using namespace std;const int N 100010, M N * 2;int n; int h[N], e[M], ne[M], idx; int ans N; bool st[N];void add(int a, int b) {e[idx] b, ne[id…

小鹏接手MONA,滴滴造了一台什么样的车?

作者|张祥威 编辑|德新 8月28日&#xff0c;小鹏汽车宣布和滴滴出行达成战略合作。 除了资本层面的合作&#xff0c;双方合作的业务核心是代号「MONA」的车型项目。 根据协议&#xff1a; 双方将打造一款售价15万元级别的A级纯电动轿车&#xff0c;项目代号MONA&#xff1b…

5.Redis-string

string 字符串 字符串类型是 Redis 最基础的数据类型&#xff0c;关于字符串需要特别注意&#xff1a; 1.⾸先Redis中所有 key 的类型都是字符串类型&#xff0c;⽽且其他⼏种数据结构也都是在字符串类似基础上构建的&#xff0c;例如 list 和 set 的元素类型是字符串类型。 2…

遥遥领先?实际是落后两代以上,小丑挡不住更多消费者买iPhone

谈到国产手机的时候&#xff0c;谁都知道一个品牌&#xff0c;都说遥遥领先苹果&#xff0c;然而事实上呢所有安卓手机都落后苹果两代&#xff0c;正是这样的现实让国内消费者越来越多买iPhone&#xff0c;推动苹果在中国高端手机市场逼近八成份额。 1.苹果为何领先&#xff1f…

Android12之ABuffer数据处理(三十五)

简介: CSDN博客专家,专注Android/Linux系统,分享多mic语音方案、音视频、编解码等技术,与大家一起成长! 优质专栏:Audio工程师进阶系列【原创干货持续更新中……】🚀 人生格言: 人生从来没有捷径,只有行动才是治疗恐惧和懒惰的唯一良药. 更多原创,欢迎关注:Android…

WebGL:开始学习 / 理解 WebGL / WebGL 需要掌握哪些知识 / 应用领域 / 前端值得学WebGL吗

一、WebGL发展史 2006 年&#xff0c;WebGL 的发展史可以追溯到 2006 年左右&#xff0c;当时 Mozilla Foundation 的一个开发人员 Vladimir Vukićević 开始尝试在 Firefox 浏览器中嵌入 OpenGL&#xff0c;为 JavaScript 提供底层图形库的支持。随后&#xff0c;这个项目引…

IDEA插件推荐 - Grep Console - 控制台日志过滤的插件

装上该插件之后&#xff0c;我们就可以很方便的进行日志的过滤、筛选。

周鸿祎为360智脑招贤纳士;LLM时代的选择指南;Kaggle大语言模型实战;一文带你逛遍LLM全世界 | ShowMeAI日报

&#x1f440;日报&周刊合集 | &#x1f3a1;生产力工具与行业应用大全 | &#x1f9e1; 点赞关注评论拜托啦&#xff01; &#x1f916; 思否「齐聚码力」黑客马拉松&#xff0c;用技术代码让生活变得更美好 主页&#xff1a;https://pages.segmentfault.com/google-hacka…

智通三千亮相2023中国软博会

“软件赋智&#xff0c;数实融合。”8月20日—23日&#xff0c;2023中国国际软件产品和信息服务交易博览会在南京举办。软博会连续举办19届。期间&#xff0c;《2023年我国工业软件产业发展研究报告》《中国软件产业高质量发展紫金指数&#xff08;2023&#xff09;》等发布。 …

c++查漏补缺(1)

目录 1.explicit关键字 2.static关键字 3.友元函数 1.explicit关键字 exeplicit关键字是在构造函数要使用的关键字。可以防止“隐式构造”&#xff0c;例如&#xff1a; #include<iostream>using namespace std;class Date { public:explicit Date(int year, int mo…

【科研论文配图绘制】task5 SciencePlots绘图包入门

【科研论文配图绘制】task5 SciencePlots绘图包入门 task5主要学习了SciencePlots拓展包的出图样式&#xff0c;掌握SciencePlots的安装及具体使用。 SciencePlots作为一个专门用于科研论文绘图的第三方拓展工具包&#xff0c;提供了主流英文科技 期刊(如 Nature、Science 和 …