Go怎么可能有引用?得了吧~
有人要说了,那利用make()
函数执行后得到的slice、map、channel等类型,不都是得到的引用吗?
我要说:那能叫引用吗?你能确定啥叫引用吗?
如果你有点迷糊,那么请听我往下讲:
这一切要从变量说起。
什么是变量
无论是引用变量还是指针变量,都是变量;那么,什么叫变量?
其实变量本质就是一块内存。通常,我们对计算机内存进行操作,最直接的方式就是:“计算机,在0x0201地址内存一个整数100,在0x00202地址存一个浮点数10.6,读取0x00203的数据…” 这种方式让机器来操作还行,如果直接写成代码让人看的话,这一堆“0x0201、0x0202…”难记的地址能把人给整崩溃了~
于是,聪明的人们想出了一种方法:把一堆难记的地址用其他人类可以方便读懂的方式来间接表示。例如:将“0x0201”的地址命名为“id”,将“0x0202”命名为“score”…然后,代码编译期间,再将”name”等人类能读懂的文字转化为真实的内存地址;于是,变量诞生了~
所以,其实每个变量都代表了一块内存,变量名是我们给那块儿内存起的一个别名,内存中存的值就是我们给变量赋的值。变量名在程序编译期间会直接转化为内存地址。
什么是引用
引用是指向另外一个变量的变量,或者说,叫一个已知变量的别名。
注意,引用和引用本身指向的变量对应的是同一块内存地址。引用本身也会在编译期间转化为真正的内存地址。当然咯,引用和它指向的变量在编译期间会转化为同一个内存地址。
什么是指针
指针本身也是一个变量,需要分配内存地址,但是内存地址中存的是另一个变量的内存地址。有点绕口,请看图:
GO中的引用和指针
我们先看看“正统”的引用的例子,在C++中(C中是没有引用的哈):
1 |
|
变量地址、引用地址、指针的值 均相同;符合常理
那我们再试试Go中类似代码的例子:
1 | package main |
变量i地址和指针ptr的值一样,这是符合预期的;但是:正如Go中没有特别的“引用符号”(C++中是int &ref = i;
)一样,上述go代码中的ref
压根就是个变量,根本不是引用。
可是,很多人不死心,是不是“实验对象”不对啊?代码中使用的是int整型,我们换做slice
和map
试试?毕竟网上的”资料”都是这么写的:
例如以下截图(只看标红部分就好):
还有如下截图(只看标红部分就好):
ok,那我们可以试试如下map的代码,看到底有没有引用:
1 | package main |
哈哈!不对呀,如果是引用的话,打印的地址应该相同才对,但是现在不相同!所以不存在?
别着急,紧接着看下面的例子:
1 | package main |
能猜出来打印了什么吗?变量地址是不对,但是,但是值居然变了!ref变量可以“操控”i变量的内容!就和引用一样!
这就很奇怪了~ 咋回事儿呢?
我们细细研究一下map
、slice
、channel
等具体实现(详情请看:我的其他文章 图解Go map底层实现、图解Go slice底层实现、图解Go channel底层实现)我们发现,这些类型的底层实现都是会有一个指针指向另外的存储地址,所以,在make
函数创建了具体的类型实例后,实际上在内存空间中会开辟多个地址空间,而随着变量的赋值,指针引用的那个地址值也会跟着“复制”,因而其他变量可以改变原有变量的内容。
听着是不是有点绕?我们来看看图:
首先实例化了map并赋值
然后又赋值给了另外一个变量ref
由于对于指针变量的值而言,就是一个地址(程序实现上就是一串数字),所以,在赋值的时候,就“复制”了一串数字,但是,这串数字背后的含义确是另外一个地址,而地址的内容,恰恰就是map
slice
channel
等数据结构真正底层存储的数据!
所以,两变量因为同一个指针变量指向的内存,而产生了类似于“引用”的效果。假如实例化的类型数据中,没有指针
属性,则不会产生这种“类引用”的效果:
例如如下代码:
1 | package main |
可以将代码上述仔细看看能输出什么,不出意外的话你会发现:“类引用”效果消失了~
要想再次展现“类引用”效果,只要创建一个带有指针属性的类型即可,我们自己实现都可以,无需依赖Go基础库中的map
、slice
、channel
等
1 | package main |
看看以上代码,是不是实现了“类引用”? 有人要说了map
展示key值,slice
展示某个下标的值,没有用方法呀?
这就不对了,其实map
的展示key的值mapData[key]
也好,更改值也好,slice
展示下标值sliceArray[0]
也好,更改值也好;背后底层实现也都是些“函数”和“方法”,只不过Go语言把这些函数和方法做成了语法糖,我们无感知罢了~
好了,现在我再问你:还敢说Go语言有引用类型吗?是不是感觉:也有、也没有了? 😝