专栏首页网管叨bi叨Go 函数的 Map 型参数,会发生扩容后指向不同底层内存的事儿吗?

Go 函数的 Map 型参数,会发生扩容后指向不同底层内存的事儿吗?

最近跟同事做项目,由于要在函数里向一个 Map 中写入不少数据,这个 Map 是作为参数传到函数里的。他问了我一个问题: “如果把 Map 作为函数参数传递,会不会像用 Slice 做参数时一样诡异,是不是一定要把 Map 当成返回值返回才能让函数外部的 Map 变量看到这里添加的数据”

啥叫会不会像用 Slice 做参数时一样诡异?同事没有明说,其实我已经猜到他说的是什么意思了,说的应该是 Slice 的底层数组如果发生了扩容后会让函数内外原本指向同一个底层数组的两个 Slice 变量,分别指向两个不同的底层数组。

最后就导致了函数内做的数据添加,但是函数外原来的 Slice 变量并没有任何改变的诡异效果。光看字儿解释起来有点难懂,举个例子,有下面这样一个程序。

func main() {
  s := []int{1, 2, 3}
  reverse(s)
  fmt.Println(s)
}

func reverse(s []int) {
  s = append(s, 999, 1000, 1001)
  for i, j := 0, len(s)-1; i < j; i++ {
    j = len(s) - (i + 1)
    s[i], s[j] = s[j], s[i]
  }
}

本来切片只有 3 个元素,分别是 1,2,3。我们把切片赋给了变量 s,然后用变量 s 作为参数传给了函数 reverse 进行处理,函数 reverse 在反转切片元素之前还给原来的切片先追加了几个值,这就导致了切片发生扩容。因为切片实际上并不是一个指针类型,它的运行时类型表示是 SliceHeader。

type SliceHeader struct {
 Data uintptr
 Len  int
 Cap  int
}

因为 Go 里边有一切都是值传递的规则,所以切片作为参数时,会在函数内重新拷贝一个 SliceHeader 结构体,只不过结构体的 Data 指针一开始跟外部切片的指向是一样的,都是同一个底层数据。

这就导致了函数内切片 SliceHeader 里的 Data 指针发生变化后,函数外原来的切片还是指向原来的底层数组。最后结果,打印函数外切片变量输出的是 [1, 2, 3],但函数里边的切片已经是 [1001, 1000, 999, 3, 2, 1] 了。

下面这个图,展示了这个函数内外切片指向的底层数组发生变化的过程。

那么如果用 Map 当函数参数时,有这档子破事儿吗?诶,提到这我就要吐槽下这个一切都是传值的设计了,把一些写 Go 的程序员搞的战战兢兢,用 Map 和结构体指针当参数的时候也老琢磨底层会不会变。

当然我也不是写 Go 的时候都盲目自信,一般书上、别人文章里写的东西我在用的时候,如果不确定他们说的对不对,我都会写个单测试一试。事后再找找解释这些知识点的资料看看,自己解惑一下。

聊远了,下面说下答案哈,如果用 Map 当函数参数,Map发生扩容后,函数内外的Map变量指向的底层内存仍是一致的。这是为什么呢?答案我是在《Go 语言设计与实现》哈希表这一章找到的,有书的可以翻开 75 页看看。

如果没有书的可以看文末的引用链接里贴的在线书籍地址。

关于 Map 的初始化是这么描述的

使用 make 创建哈希,Go 语言编译器都会在类型检查期间将它们转换成 runtime.makemap,使用字面量初始化哈希也只是语言提供的辅助工具,最后调用的都是 runtime.makemap

func makemap(t *maptype, hint int, h *hmap) *hmap {
 mem, overflow := math.MulUintptr(uintptr(hint), t.bucket.size)
 if overflow || mem > maxAlloc {
  hint = 0
 }
 ......
 return h
}

通过上面的解释和代码我们了解到 Map 这个数据类型,在运行时实际上是一个 hmap类型的指针,只不过在我们写代码阶段被隐藏起来了。

既然是一个 Map 类型的变量实际上是一个指针变量,这跟 Slice 就完全不同了,虽然指针作为函数参数时在 Go 里面也是按照值传递的,但是内外两个指针是指向的同一个 hamp 结构所在的内存,hmap 结构里有很多字段,回答这里的问题,我们只需要知道 buckets 和 oldbuckets 这两个指针类型的字段就行了。

type hmap struct {
 count     int
 flags     uint8
 B         uint8
 noverflow uint16
 hash0     uint32

 buckets    unsafe.Pointer
 oldbuckets unsafe.Pointer
 nevacuate  uintptr

 extra *mapextra
}

Go 的 Map中用于存储键值对数据的结构--桶(bmap),对于bmap 我们不再深挖下去。

buckets 是指向桶数组的。当哈希表增长到需要扩容的时候,Go语言会将bucket数组的数量扩充一倍,产生一个新的bucket数组,老数据存放在 oldbuckets 指向的桶中,并在被访问到时迁移到新桶中去。

这里虽然扩容导致 Map 有了新 bucket 数组的地址,但是这个地址是存在 hmap 的字段 buckets 上的,变更字段的值并不会影响 hmap 本身的内存地址

所以当 Map 由于函数内的操作发生扩容时,不会像上面例子里的 Slice 指向不同底层数组的诡异现象。

不知道大家有没有看明白我这里的分析,这篇文章其实是我自己对思考问题的一个记录,防止时间长了以后忘掉。传值、传引用这些在不同的语言里不一样,对于像我们掌握了至少三门编程语言的男人:)也就只能靠写写笔记防止混淆啦。

(我相信绝大多数人的职业生涯是不能靠一门编程语言吃遍天的)

还有一点我是觉得 Go 的 Slice 使用起来确实要耗费的心智有点高,一不注意就容易踩坑,时间长了,搞的大家用 Map 和 指针当参数时也会先自我怀疑一下,希望这篇文章对解决掉你们的使用疑虑有一定帮助。

引用地址

  • Go 语言设计与实现 --哈希表 https://draveness.me/golang/docs/part2-foundation/ch03-datastructure/golang-hashmap

- END -

文章分享自微信公众号:
网管叨bi叨

本文参与 腾讯云自媒体分享计划 ,欢迎热爱写作的你一起参与!

作者:KevinYan11
原始发表时间:2022-02-15
如有侵权,请联系 cloudcommunity@tencent.com 删除。
登录 后参与评论
0 条评论

相关文章

  • 【建议收藏】吐血整理Golang面试干货21问-吊打面试官-1

    Golang面试分享来了,为了帮助大家更好的面试,笔者总结一份相关的Golang知识的面试问题,希望能帮助大家。

    公众号-利志分享
  • 深度解密Go语言之map

    这篇文章主要讲 map 的赋值、删除、查询、扩容的具体执行过程,仍然是从底层的角度展开。结合源码,看完本文一定会彻底明白 map 底层原理。

    李海彬
  • 深度解密Go语言之map

    这篇文章主要讲 map 的赋值、删除、查询、扩容的具体执行过程,仍然是从底层的角度展开。结合源码,看完本文一定会彻底明白 map 底层原理。

    梦醒人间
  • Go通关18:SliceHeader,slice 如何高效处理数据?

    示例中变量 a1 的类型是 [1]string,变量 a2 的类型是 [2]string,因为它们大小不一致,所以不是同一类型。

    微客鸟窝
  • 面试官:说下Golang Slice的底层实现,泪崩了!

    数组固定长度数组长度是数组类型的一部分,所以[3]int 和[4]int 是两种不同 的数组类型数组需要指定大小,不指定也会根据处初始化对的自动推算出大 小,不...

    码农编程进阶笔记
  • Go语言切片面试真题8连问

    切片是对数组的抽象,因为数组的长度是不可变的,在某些场景下使用起来就不是很方便,所以Go语言提供了一种灵活,功能强悍的内置类型切片("动态数组"),与数组相比切...

    Golang梦工厂
  • 面试必备(背)--Go语言八股文系列!

    满足强三色不变性:黑色节点不允许引用白色节点 当黑色节点新增了白色节点的引用时,将对应的白色节点改为灰色

    微客鸟窝
  • 深入理解 Go map:初始化和访问元素

    从本文开始咱们一起探索 Go map 里面的奥妙吧,看看它的内在是怎么构成的,又分别有什么值得留意的地方?

    李海彬
  • 女朋友问我:小松子,你知道Go语言参数传递是传值还是传引用吗?

    形式参数:是在定义函数名和函数体的时候使用的参数,目的是用来接收调用该函数时传入的参数。

    Golang梦工厂
  • 深入理解 Go map:赋值和扩容迁移

    在 上一章节 中,数据结构小节里讲解了大量基础字段,可能你会疑惑需要 #&(!……#(!¥! 来干嘛?接下来我们一起简单了解一下基础概念。再开始研讨今天文章的重...

    李海彬
  • Go性能优化小结

    做过C/C++的同学可能知道,小对象在堆上频繁地申请释放,会造成内存碎片(有的叫空洞),导致分配大的对象时无法申请到连续的内存空间,一般建议是采用内存池。Go ...

    李海彬
  • <Go语言学习笔记>【数组与切片】

    Go 语言的切片类型属于引用类型,同属引用类型的还有字典类型、通道类型、函数类型等;而 Go 语言的数组类型则属于值类型,同属值类型的有基础数据类型以及结构体类...

    Porco1Rosso
  • 码住!Golang并发安全与引用传递总结

    导语 | 因为现在服务上云的趋势,业务代码都纷纷转向golang的技术栈。在迁移或使用的过程中,由于对golang特性的生疏经常会遇到一些问题,本文总结了gol...

    腾小云
  • go语言中函数参数传值还是传引用的思考

    算起来这些年大大小小也用过一些不同编程语言,但平时开发还是以C++为主,得益于C++精确的语义控制,我可以在编写代码的时候精准地控制每一行代码的行为,以达到预期...

    tyriqchen
  • golang 面试总结

    前段时间找工作搜索 golang 面试题时,发现都是比较零散或是基础的题目,覆盖面较小。而自己也在边面试时边总结了一些知识点,为了方便后续回顾,特此整理了一下。

    lincoln
  • Go语言编码规范|青训营笔记

    课程导学链接:https://juejin.cn/post/7093721879462019102#heading-37

    白泽z
  • [面试] Golang 面试题

    Make 用于map、slice 和channel几种类型的内存分配。并且返回一个有初始值的对象,注意不是指针。

    全栈程序员站长
  • Go-常识补充-切片-map(类似字典)-字符串-指针-结构体

    fmt.Printf("%T", a) ,注意,用的是 fmt.Printf 函数,a 指的是要查看类型的变量

    suwanbin
  • 2万字图解map

    上面引用的是维基百科对map的定义,意思是说,在计算机学科中,map是一种抽象的数据结构,它由key和value组成组成键值对的集合,在集合中每个key最多出现...

    数据小冰

扫码关注腾讯云开发者

领取腾讯云代金券