曾经在某次go面试中被面试官问到:“go中引用类型有哪些?”,我答到:“slice,map,channel”,面试官:“其实go没有引用类型,都是值类型“,当时我就懵了,这么基础的问题居然我跟面试官意见不同。现在想想也许是我听错了,他应该说的是:”go没有引用传递,都是值传递“。我们今天就来聊一下这个话题。

一个简单的例子

func modifySlice(slice []int) {  slice[0] = 11}func main() {  mySlice := []int{1, 2, 3}  modifySlice(mySlice)  fmt.Println(mySlice)}

先看下运行结果:

[11 2 3]

这段代码很简单,modifySlice函数接收一个int类型的切片,并将切片的第一个元素值改成11,main函数定义了一个int类型切片变量mySlice,调用modifySlice函数并将其传入,最后打印出mySlice变量。从结果看mySlice变量在其他函数内部的修改反应到了函数外部(并不总是这样,如果切片在其他函数内部发生了扩容,则函数内部的修改不会反应到函数外部,这个下面会细讲),那么是否就意味着:“go传参是引用传递”呢?先不着急下结论,在这之前我们先来了解一下go语言中的值类型和引用类型。

go中的值类型和引用类型

值类型:基本类型,包含int、float、bool、string、struct、数组,特点是变量直接存储值,通常在栈中分配这些类型的变量值。我们以int类型为例来说明

i := 10j := i 

i赋值给j,相当于重新开辟一段内存,并将变量i的值拷贝一份存入到新的内存地址中,内存存储形式见下图

引用类型:map、slice、channel、interface,特点是变量存储的是一个地址,这个地址存储最终的值,内容通常在堆上分配,通过GC回收。

我们以slice类型为例来说明,先看下slice底层实现

// src/runtime/slice.gotype slice struct {  array unsafe.Pointer // 指针  len  int // 长度  cap  int // 容量}

可以看出切片由指针:指向底层数组指针;长度:切片可用元素个数;容量:底层数组元素个数这三个字段组成,其中len<=cap,通过下标访问切片元素时下标不能大于len,否则会引发panic,当向切片添加元素时容量不够时会重新开辟一块内存空间,生成一个新的长度更大的数组(具体扩容多少倍不同的go版本实现不同,这个在以后的文章中会详细讲解),并将之前旧数组中的元素全部拷贝过来,之后将新的元素加入,切片指向新的数组,旧底层数组占用的内存会被回收。

sliceA := make([]int,2,4)sliceA[0] = 1sliceA[1] = 2

声明了一个长度为2,容量为4的sliceA变量,其内存存储形式见下图

解开谜底

想要弄清楚go传参是值传递还是引用传递,只需要验证一件事情:“变量传入前和传入后内变量的底层地址是否发生了改变?如果发生了改变就是值传递,如果没有改变就是引用传递”。如何取变量的内存地址呢?可以通过在变量前加取址符号&,再配合fmt.Printf函数拿到,fmt.Printf("%p", &变量名),我们再回到第一个例子,这次稍加改造一下

func modifySlice(slice []int) {  fmt.Printf("调用中:%p\n",&slice)  slice[0] = 11}func main() {  mySlice := []int{1, 2, 3}  fmt.Printf("调用前:%p\n",&mySlice)  modifySlice(mySlice)  fmt.Printf("调用后:%p\n",&mySlice)}

增加了变量mySlice在调用函数前、函数中和函数返回后的地址打印,结果如下:

调用前:0xc00000c080调用中:0xc00000c0a0调用后:0xc00000c080

从结果看出存储变量mySlice本身的地址在函数外和函数中不一样,证明go传参是值传递,他引用类型map,channel一样可以通过这种方式验证。不过你可能还有个疑问:为什么变量mySlice在modifySlice中的修改函数外面也会跟着被修改呢?

这次我们不加取址符号&,再看一下地址

func modifySlice(slice []int) {  fmt.Printf("调用中:%p\n",slice)  slice[0] = 11}func main() {  mySlice := []int{1, 2, 3}  fmt.Printf("调用前:%p\n",mySlice)  modifySlice(mySlice)  fmt.Printf("调用后:%p\n",mySlice)  fmt.Println(mySlice)}

打印结果:

调用前:0xc0000a0000调用中:0xc0000a0000调用后:0xc0000a0000

不加&符号在函数中和函数外得到的地址的一样的。我们来看下fmt.Printf的到底做了什么,

通过源码阅读,调用链为:Printf->Fprintf->doPrintf->printArg->fmtPointer->Pointer,省去中间函数,直接看Pointer的实现

func (v Value) Pointer() uintptr {  k := v.kind()  switch k {  case Ptr:  case Chan, Map, UnsafePointer:    return uintptr(v.pointer())  case Func:  case Slice:// 走到这个分支    return (*SliceHeader)(v.ptr).Data  }  panic(&ValueError{"reflect.Value.Pointer", v.kind()})}

最后进入了case Slice这个分支,返回(*SliceHeader)(v.ptr).Data,意思是将slice的指针转换成了*SliceHeader,再取其Data字段值,我们来看下SliceHeader底层结构

type SliceHeader struct {  Data uintptr  Len  int  Cap  int}

这个结构跟slice底层结构一样,所以可以转换,回顾刚才关于slice的介绍,slice第一个字段值是指向底层数组的地址,fmt.Printf("%p",mySlice)拿到的是mySlice底层数组的指针,切片mySlice在函数外和传入函数中指向的底层数组是同一个,所以在函数内对其元素的更改在函数外也会跟着更改

我们再来看一下加取址符号&的底层实现

func (v Value) Pointer() uintptr {  k := v.kind()  switch k {  case Ptr:// 走到这个分支    if v.typ.ptrdata == 0 {      return *(*uintptr)(v.ptr)    }    fallthrough  case Chan, Map, UnsafePointer:    return uintptr(v.pointer())  case Func:  case Slice:    return (*SliceHeader)(v.ptr).Data  }  panic(&ValueError{"reflect.Value.Pointer", v.kind()})}

加上&后走的是case Ptr分支 ,因为v.typ.ptrdata>0,执行到fallthrough,这个关键字的意思是强制调到下面分支继续执行,所以执行case Chan, Map, UnsafePointer这个分支,我们再来看看v.Pointer代码

// pointer returns the underlying pointer represented by v.func (v Value) pointer() unsafe.Pointer {  if v.typ.size != ptrSize || !v.typ.pointers() {    panic("can't call pointer on a non-pointer Value")  }  if v.flag&flagIndir != 0 {    return *(*unsafe.Pointer)(v.ptr)  }return v.ptr}

看函数上方的注释:”pointer returns the underlying pointer represented by v“,意思是:返回由v表示的底层指针,即存储变量本身的内存地址

接下来谈一下上面说的:”切片在函数内部的更改并不总是反应到函数外部“,这也是一些刚学go语言不久的同学容易踩的坑,这里详细讲一下,我们改动一下modifySlice的代码

func main() {  mySlice := []int{1, 2, 3}  fmt.Printf("调用前地址:%p\n",mySlice)  modifySlice(mySlice)  fmt.Printf("调用后地址:%p\n",mySlice)  fmt.Printf("调用后值:%v\n",mySlice)}func modifySlice(slice []int) {  fmt.Printf("函数内部-添加元素前地址:%p\n",slice)  slice = append(slice, 4)  fmt.Printf("函数内部-添加元素后地址:%p\n",slice)  fmt.Printf("函数内部值:%v\n",slice)}

改成在向切片添加一个元素4,看打印结果

调用前地址:0xc0000b6000函数内部-添加元素前地址:0xc0000b6000函数内部-添加元素后地址:0xc0000ac060函数内部值:[1 2 3 4]调用后地址:0xc0000b6000调用后值:[1 2 3]

可以看出添加新元素时由于容量是3不足导致扩容,切片的底层数组地址变了,导致切片在函数内部添加了元素4而函数外部却没有添加。

有没有办法让切片在函数内部的更改始终影响到函数外部?答案是传参以指针的形式传入,代码如下

func main() {  mySlice := []int{1, 2, 3}  fmt.Printf("调用前切片地址:%p\n",mySlice)  fmt.Printf("调用前切片底层地址:%p\n", &mySlice)  modifySlice(&mySlice)  fmt.Printf("调用后切片地址:%p\n",mySlice)  fmt.Printf("调用后切片底层地址:%p\n", &mySlice)  fmt.Printf("调用后值:%v\n",mySlice)}func modifySlice(slice *[]int) {  fmt.Printf("函数内部-添加元素前切片地址:%p\n",slice)  fmt.Printf("函数内部-添加元素前切片底层地址:%p\n",&slice)  *slice = append(*slice, 4)  fmt.Printf("函数内部-添加元素后切片地址:%p\n",slice)  fmt.Printf("函数内部-添加元素后切片底层地址:%p\n",&slice)  fmt.Printf("函数内部值:%v\n",*slice)}

modifySlice接收的参数类型改成了*[]int,用&mySlice传入,另外增加了函数内部和外部存储变量本身的地址打印,结果如下

调用前切片地址:0xc000136000调用前切片底层地址:0xc000126020函数内部-添加元素前切片地址:0xc000126020函数内部-添加元素前切片底层地址:0xc00012e020函数内部-添加元素后切片地址:0xc000126020函数内部-添加元素后切片底层地址:0xc00012e020函数内部值:[1 2 3 4]调用后切片地址:0xc00012c060调用后切片底层地址:0xc000126020调用后值:[1 2 3 4]

可以看出slice和mySlice值都为[1,2,3,4],函数内部指针变量slice指向了函数外部mySlice的底层相同的地址0xc000126020,我们看下这句代码:*slice = append(*slice, 4), *slice意思是取slice的值,我们知道slice=&mySlice,所以可以理解为*slice相当于外部函数的mySlice,这样一来代码可以改成:mySlice = append(mySlice, 4),相当于在函数内部对函数外部的变量进行了更改,这就是以指针作为参数传入时函数内部添加的元素也影响到了函数外部的根本原因。另外函数内slice的底层地址0xc00012e020与数外部mySlice底层地址0xc000126020不同,  证明指针变量传参也是值传递,如果描述的还不够清楚,请看下面的图示

总结

在Go语言中只存在值传递,要么是值的副本,要么是指针的副本。无论是值类型的变量还是引用类型的变量亦或是指针类型的变量作为参数传递都会发生值拷贝,开辟新的内存空间。另外值传递、引用传递和值类型、引用类型是两个不同的概念,不要混淆了。引用类型作为变量传递可以影响到函数外部是因为发生值拷贝后新旧变量指向了相同的内存地址。

最后引用一段英文来结束今天的文章

Golang is a pass by value language, so any time we pass a value to a function either as a receiver or as an argument that data is copied in memory and so the function by default is always going to be working on a copy of our data structure. We can address this problem and modify the actual underlying data structure through the use of pointers and memory address.

参考资料

https://stackoverflow.com/questions/39993688/are-slices-passed-by-value

https://www.jianshu.com/p/f201d6da488a

https://www.jianshu.com/p/18d3bde7d835

如果这篇文章对你有帮助,可以关注我的公众号,第一时间获取最新的原创文章

go语言传参是值传递还是引用传递相关推荐

  1. 【Java语法】关于使用new和不使用new的数组值传递还是引用传递的问题

    结论 对于数组来说,无论是否用new,在传参的时候都是引用传递,也就是说,在函数中,对变量值的改变会对实际变量的值产生影响. 运行结果 改变前:10 改变后:10 [I@15db9742 改变前:[1 ...

  2. Java深入学习系列之值传递Or引用传递?

    我们来看一个新手甚至写了多年Java的朋友都可能不是十分确定的问题: 在Java方法传参时,究竟是引用传递还是值传递? 为了说明问题, 我给出一个非常简单的class定义: public class  ...

  3. Javascript 之《函数传参到底是值传递还是引用传递》

    前言 这个问题其实困惑了我好久,但是在实际使用中总是得过且过,不想去深究.由于这种态度,在学习 Javascript 过程中,水平一直都是出于半桶水状态,很多概念和原理似懂非懂,模糊不清. 所以,写了 ...

  4. JS函数传参时:值传递与引用传递的区别

    JS函数传参时:值传递与引用传递的区别 一.先分析基础数据与复杂数据的区别 : 基本数据类型:Undefined.Null.Boolean.Number.String 引用数据类型:对象 如:var ...

  5. 论JS函数传参时:值传递与引用传递的区别

    JS函数传参时:值传递与引用传递的区别? 值传递:值传递的数据为基本数据类型,基本数据类型在内存中存放的是数值本身:值传递为单向传递,只能由实参传递给形参. 引用传递:引用传递的数据为复杂数据类型,复 ...

  6. 值传递和引用传递传的到底是啥?

    作者 | 编程指北  责编 | 张文 来源 | 编程指北(ID:cs_dev) 在网上看到过很多讨论 Java.C++.Python 是值传递还是引用传递这类文章,所以这一篇呢就是想从原理讲明白关于函 ...

  7. java int 传引用吗_Java值传递还是引用传递?

    从我实习面试开始就有在面试中遇到过这个问题:Java是值传递还是引用传递? 当时的我只会背背面试题,但是网上的答案有些还是错的,导致我决心写这一篇文章. 虽然网上已经有很多文章珠玉在前,但是我还是想写 ...

  8. c语言中的值传递和地址传递参数,c语言值传递,地址传递,引用传递

     c语言值传递,地址传递,引用传递 总结:对于函数来说,值传递就是一个人来了,给你一些数 据,你对数据处理.地址传递就是你通过地址找到一个人,然后直接对这个人处理.而引用传递就是你要直接对一个人进行处 ...

  9. 【值传递和引用传递之外的第三种传值方式 - 传名参数】

    引言 先看下Scala 的以下方法该如何调用那? class Demo {val assertIsOpen = truedef test(isTrue: () => Boolean): Unit ...

最新文章

  1. VisualSvn Server介绍
  2. 【Matlab 控制】函数调用函数
  3. Windows下各个盘中的文件夹属性变为隐藏,怎么取消隐藏属性
  4. redis入门(02)redis的常见问题
  5. Google Chrome input 设置 line-height 后光标变得和input一样高
  6. VC++CopyFile函数的用法
  7. VSCode 如何支持 Flow
  8. fwoa中workflow_requestbase表currentnodetype字段含义及查看归档流程的requestid
  9. 黑盒测试方法(五)正交实验设计方法
  10. Java高级编程9-姜国海 网络应用编程
  11. 更换持续集成工具,从 Travis 到 Github Actions
  12. left join on左连接的使用
  13. 钽电容的作用,钽电容滤波好的原因
  14. 51node1006LCS
  15. 一步步教你新电脑如何分区教程
  16. 牛客练习赛24 B凤 凰(并查集考察)
  17. 一个蚂蚁程序员,曾经的辛酸面试历程
  18. 7月16日周二晚上,陈勇,【敏捷网络课堂第六期】【免费】敏捷开发早期估算...
  19. vSphere6.7环境搭建
  20. 【正版软件】激活Windows 7报0xC004F035错误

热门文章

  1. vs 输入代码时出现火花_vscode 火花_火花监控如何每天处理10B请求
  2. ps校色调色/通道抠图
  3. python hashlib模块安装_python hashlib 模块
  4. 2019PHP面试题大全(中级知识一)
  5. 【linux】进程间通信——system V
  6. css伪元素 before after的用法
  7. ABAQUS划分网格
  8. vue判断是否登录,若未登录跳转登录页
  9. CTFHUB技能树之Web
  10. IOS – OpenGL ES 图像鱼眼移动效果 GPUImageBulgeDistortionFilter