細談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語言有引用類型嗎?是否是感受:也有、也沒有了? 😝

更多精彩內容,請關注個人微信公衆號 互聯網技術窩

相關文章
相關標籤/搜索