细谈Go引用的底层实现

Go怎么可能有引用?得了吧~ 有人要说了,那利用make()函数执行后获得的slice、map、channel等类型,不都是获得的引用吗?c++

我要说:那能叫引用吗?你能肯定啥叫引用吗? 若是你有点迷糊,那么请听我往下讲:git

这一切要从变量提及。github

什么是变量

不管是引用变量仍是指针变量,都是变量;那么,什么叫变量? 其实变量本质就是一块内存。一般,咱们对计算机内存进行操做,最直接的方式就是:“计算机,在0x0201地址内存一个整数100,在0x00202地址存一个浮点数10.6,读取0x00203的数据...” 这种方式让机器来操做还行,若是直接写成代码让人看的话,这一堆“0x020一、0x0202...”难记的地址能把人给整崩溃了~ 因而,聪明的人们想出了一种方法:把一堆难记的地址用其余人类能够方便读懂的方式来间接表示。例如:将“0x0201”的地址命名为“id”,将“0x0202”命名为“score”...而后,代码编译期间,再将"name"等人类能读懂的文字转化为真实的内存地址;因而,变量诞生了~微信

因此,其实每一个变量都表明了一块内存,变量名是咱们给那块儿内存起的一个别名,内存中存的值就是咱们给变量赋的值。变量名在程序编译期间会直接转化为内存地址。markdown

什么是引用

引用是指向另一个变量的变量,或者说,叫一个已知变量的别名。数据结构

注意,引用和引用自己指向的变量对应的是同一块内存地址。引用自己也会在编译期间转化为真正的内存地址。固然咯,引用和它指向的变量在编译期间会转化为同一个内存地址。函数

什么是指针

指针自己也是一个变量,须要分配内存地址,可是内存地址中存的是另外一个变量的内存地址。有点绕口,请看图:oop

GO中的引用和指针

咱们先看看“正统”的引用的例子,在C++中(C中是没有引用的哈):ui

#include <stdio.h>

int main(void) {

        int i = 3;
        int *ptr = &i;
        int &ref = i;

        printf("%p %p %p\n", &i, ptr, &ref); 
        // 打印出:0x7ffeeac553a8 0x7ffeeac553a8 0x7ffeeac553a8
        return 0;
}
复制代码

变量地址、引用地址、指针的值 均相同;符合常理spa

那咱们再试试Go中相似代码的例子:

package main

import "fmt"

func main() {
    i := 3
    ref := i
    ptr := &i
    
    fmt.Println(fmt.Sprintf("%p %p %p", &i, &ref, ptr))
    // 打印出 0xc000118000 0xc000118008 0xc000118000
}
复制代码

变量i地址和指针ptr的值同样,这是符合预期的;可是:正如Go中没有特别的“引用符号”(C++中是int &ref = i;)同样,上述go代码中的ref压根就是个变量,根本不是引用。

但是,不少人不死心,是否是“实验对象”不对啊?代码中使用的是int整型,咱们换作slicemap试试?毕竟网上的"资料"都是这么写的: 例如如下截图(只看标红部分就好):

还有以下截图(只看标红部分就好):

ok,那咱们能够试试以下map的代码,看到底有没有引用:

package main

import "fmt"

func main(){
    i := make(map[string]string)
    i["key"]="value"

    ref := i

    fmt.Println(fmt.Sprintf("%p %p", &i, &ref))
    // 打印出:0xc00010e018 0xc00010e020
}
复制代码

哈哈!不对呀,若是是引用的话,打印的地址应该相同才对,可是如今不相同!因此不存在? 别着急,紧接着看下面的例子:

package main

import "fmt"

func main(){
    i := make(map[string]string)
    i["key"]="value"

    ref := i
    ref["key"] = "value1"

    fmt.Println(i["key"]) // 打印结果:value1
    fmt.Println(ref["key"]) // 打印结果:value1

    fmt.Println(fmt.Sprintf("%p %p", &i, &ref))
    // 打印结果:0xc00000e028 0xc00000e030
}
复制代码

能猜出来打印了什么吗?变量地址是不对,可是,可是值竟然变了!ref变量能够“操控”i变量的内容!就和引用同样!

这就很奇怪了~ 咋回事儿呢?

咱们细细研究一下mapslicechannel等具体实现(详情请看:个人其余文章 图解Go map底层实现图解Go slice底层实现图解Go channel底层实现)咱们发现,这些类型的底层实现都是会有一个指针指向另外的存储地址,因此,在make函数建立了具体的类型实例后,实际上在内存空间中会开辟多个地址空间,而随着变量的赋值,指针引用的那个地址值也会跟着“复制”,于是其余变量能够改变原有变量的内容。

听着是否是有点绕?咱们来看看图:

首先实例化了map并赋值

而后又赋值给了另一个变量ref

因为对于指针变量的值而言,就是一个地址(程序实现上就是一串数字),因此,在赋值的时候,就“复制”了一串数字,可是,这串数字背后的含义确是另一个地址,而地址的内容,偏偏就是map slice channel 等数据结构真正底层存储的数据!

因此,两变量由于同一个指针变量指向的内存,而产生了相似于“引用”的效果。假如实例化的类型数据中,没有指针属性,则不会产生这种“类引用”的效果: 例如以下代码:

package main

import "fmt"

func main(){
    i := 3

    ref := i
    ref = 4

    fmt.Println(i, ref) // 打印输出:3 4

    fmt.Println(fmt.Sprintf("%p %p", &i, &ref))
    // 打印输出:0xc000016070 0xc000016078
}
复制代码

能够将代码上述仔细看看能输出什么,不出意外的话你会发现:“类引用”效果消失了~

要想再次展示“类引用”效果,只要建立一个带有指针属性的类型便可,咱们本身实现均可以,无需依赖Go基础库中的mapslicechannel

package main

import "fmt"

type Instance struct {
    Name string
    Data *int
}

func (i Instance) Store(num int) {
    *(i.Data) = num
}

func (i Instance) Show() int{
    return *(i.Data)
}



func main(){
    data := 5

    i := Instance{
        Name:"hello",
        Data:&data,
    }

    ref := i
    ref.Store(7)

    fmt.Println(i.Show(), ref.Show())
    // 打印出:7 7

    fmt.Println(fmt.Sprintf("%p %p", &i, &ref))
    // 打印出:0xc0000a6018 0xc0000a6030
}

复制代码

看看以上代码,是否是实现了“类引用”? 有人要说了map展现key值,slice展现某个下标的值,没有用方法呀? 这就不对了,其实map的展现key的值mapData[key]也好,更改值也好,slice展现下标值sliceArray[0]也好,更改值也好;背后底层实现也都是些“函数”和“方法”,只不过Go语言把这些函数和方法作成了语法糖,咱们无感知罢了~

好了,如今我再问你:还敢说Go语言有引用类型吗?是否是感受:也有、也没有了? 😝

更多精彩内容,请关注个人微信公众号 互联网技术窝

相关文章
相关标签/搜索