最近跟同事做项目,由于要在函数里向一个 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 uintptrLen  intCap  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     intflags     uint8B         uint8noverflow uint16hash0     uint32buckets    unsafe.Pointeroldbuckets unsafe.Pointernevacuate  uintptrextra *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叨」

给网管个星标,第一时间吸我的知识 

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

  1. c语言函数输出输出型参数_c语言技巧多个输出参数

    c语言函数输出输出型参数 Over the past few years under the "new Microsoft", there have been many effor ...

  2. 在C/C++函数中使用可变参数

    原文链接地址:http://blog.csdn.net/djinglan/article/details/8425768 下面介绍在C/C++里面使用的可变参数函数. 先说明可变参数是什么,先回顾一下 ...

  3. 第十七篇,文件偏移量lseek函数,系统IO应用实例(LCD应用)和内存映射详细讲解。

    一.文件偏移量. 1.什么是文件偏移量? 文件偏移量就是文件光标当前的定位,默认打开一个文件时,文件的定位都是在文件的最开头. 文件读操作/写操作都会使得文件偏移量往后偏移. 2.怎么才能使得文件偏移 ...

  4. Mysql:max函数参数为字符类型需要通过CAST函数转换为数值型

    数据库中的经常会用到要过滤某个列的最值数据,比如,取数量最大的值,取时间最早的值等等,下面我们就来看看如何运用这个最值函数,特别注意,max函数或者min函数,参数都支持字符类型,这里就会有些坑存在, ...

  5. python map函数返回类型_使用map和lambda函数发送两个参数并返回两个值

    因为您说您只需要一个模型,而不是每个值都需要一个新模型,所以这相当简单.更改:hnd = map(lambda (valua): function_f(valua), list_value) 致:mo ...

  6. Python进阶:函数式编程(高阶函数,map,reduce,filter,sorted,返回函数,匿名函数,偏函数)...啊啊啊...

    函数式编程 函数是Python内建支持的一种封装,我们通过把大段代码拆成函数,通过一层一层的函数调用,就可以把复杂任务分解成简单的任务,这种分解可以称之为面向过程的程序设计.函数就是面向过程的程序设计 ...

  7. Python学习之Part09.高阶函数filter(),map(),reduce(),sorted()

    1.高阶函数 一个函数作为参数传给另外一个函数: 一个函数的返回值为另外一个函数(若返回值为该函数本身,则为递归) # abs()用来求一个数的绝对值 # 将abs函数赋值,则f==abs f = a ...

  8. Swift-高阶函数如map,reduce,filter的一些总结

    Swift 这个语言,速度快,更加安全,代码少,易于阅读维护. 所以一些高阶函数在项目实际使用中也是蛮方便的总结如下: 高阶函数的定义:一个函数如果可以以某个函数作为参数,或者返回值,那么这个函数就称 ...

  9. 能向入口函数传入多个参数的 QueueUserWorkItem

    不啰嗦了,花一堆时间也没赶上 std::async 和 std::thread 的设计,标准库的设计真的,很优秀. 我记下这段时间里做了什么: 这里包含了把函数拆成两步调用的方法,第一步传参,第二步执 ...

最新文章

  1. 机器学习 TOP 10 必读论文 | 资源
  2. 注册界面的设计与实现
  3. 利用 Node.js 实现 SAP Hana 数据库编程接口
  4. OpenShift 4 之 高可靠运行MS SQL Server 2019数据库
  5. bzoj1770: [Usaco2009 Nov]lights 燈(折半搜索)
  6. 午后随笔 -- 定位的思考
  7. jQuery的立即调用表达式
  8. 第二人生的源码分析(2)第二人生的基本功能
  9. 人才缺口30万,市场需求每年涨20% ,这一 IT 岗位你知道嘛
  10. ChucK初步(7)
  11. 你平时都怎么记笔记?给好学的你安利10个最好用的记笔记神器!
  12. orm之peewee
  13. 我的世界服务器自动被踢怎么可以进去,我的世界中国版服务器中如何解决玩家作弊的简单方法...
  14. 软件项目管理课程复习题
  15. 计算机类论文答辩常见问题——软件开发类题目
  16. Git常用的指令整理
  17. 又拍云张聪:OpenResty 动态流控的几种姿势
  18. git--修改用户名和邮箱的方法(全局修改和局部修改)
  19. openwrt - 新增栏目 - 新增页面
  20. BCD码与16进制互转算法(转)

热门文章

  1. Java反射异常:java.lang.NoSuchFieldException
  2. 现在Windows Server 2012在Windows Azure 虚拟机库中可用
  3. 字符验证码识别项目记录
  4. 『中级篇』容器网络之host和none(29)
  5. 应变界的翘楚:硅基谐振式传感器灵敏度非常高
  6. dev Gridcontrol控件属性部分
  7. Java中的ASCII码与Unicode码
  8. 《深入react技术栈》学习笔记(一)初入React世界
  9. Android开发小技巧之商品属性筛选与商品筛选
  10. h5端呼起摄像头扫描二维码并解析