圖解Go的unsafe.Pointer
相信看過Go源碼的同學已經對 unsafe.Pointer
非常的眼熟,因爲這個類型可以說在源碼中是隨處可見: map
、 channel
、 interface
、 slice
…但凡你能想到的內容,基本都會有 unsafe.Pointer
的影子。
看字面意思, unsafe.Pointer
是“不安全的指針”,指針就指針吧,還安不安全的是個什麼鬼?
接下來,我們就來了解一下Go的這個“不安全的指針” unsafe.Pointer
。
什麼叫變量
在瞭解指針之前,我們有必要先了解一下什麼叫“變量”。
其實變量就是一個內存地址的名字,這聽起來可能有些奇怪:指針不是地址碼?
聽我細細來講:此地址非彼地址。通常,我們要在計算機內存中存數據,我們會怎麼做?
我們肯定說:“計算機,在0x0201地址內存一個數100”。就這一句話,別看它糙,實際上在計算機中真就這麼幹的。然後我們接着說:“在0x0202中存什麼,在0x0203中存什麼,把0x0203中的值變爲0x0201中的值…”
這些“0x0201”、“0x0202”、“0x0203”…這些數字兒是不是不太好記?寫個代碼是不是頭都大了?
於是聰明的先人給想了個辦法,把這些地址換成 代號 ,“0x0201”我叫x,“0x0202”我給他起個名字叫y,“0x0203”我給他起個名字叫z…
於是 “計算機,在0x0201地址內存一個數100”。就變成了 var x int =100
。
而這個這個代號就是變量。
0x0201地址 =============》 100 0x0201地址 ======》X ===》 100
果然,計算機界中的任何問題,都可以通過加一箇中間層來解決。(#^.^#)
最後,計算機會在內存中存代號和變量地址的對應關係。
什麼叫指針
我們印象中的指針這個概念,其實就是一個存了內存地址的對象,這個對象指向的內存地址可能是另外一個對象、函數或者結構體等。
這個理解沒錯,但是一定要理清楚指針和變量的關係。
在一般的指針中,由於指針只是一個地址,底層實現是一個unsigned int,所以在C語言中,指針之間的賦值和計算等同類型之間的操作很常見。
以下代碼掃一眼,看看是否知道輸出結果。
#include "stdio.h" int main(int argc, char const *argv[]) { char c = 'b'; int i = 1000; char *cp; int *ip; //指針的正常賦值 cp = &c; ip = &i; printf("cp[%p]\n", cp); //cp[0x7ffee904275f] printf("ip[%p]\n", ip); //ip[0x7ffee9042758] //指針的計算 cp = cp + 1; ip = ip + 1; printf("cp[%p]\n", cp); //cp[0x7ffee9042760] printf("ip[%p]\n", ip); //ip[0x7ffee904275c] //不同"類型"指針之間的賦值 cp = ip; printf("cp[%p] ip[%p]\n", cp, ip); //cp[0x7ffee904275c] ip[0x7ffee904275c] //不同指針之間的比較 輸出true if (cp == ip) { printf("true\n"); } else { printf("false\n"); } }
通常意義上我們瞭解的不同類型的指針,可以歸爲“同一類型”,無論是int類型的指針還是char類型的指針,都稱之爲“指針類型”。
指針指向對象類型的約束對指針本身而言非常弱,因爲在通常C語言中的定義不同類型的指針,只是爲了調用的方便。例如一個指針指向了某一個結構體,那麼我寫代碼的時候就可以方便的使用該結構體的屬性字段;可以說通常意義上的C指針,是個“萬能類型”的,啥類型的指針都和 void*
一樣,萬能!
所以,在C語言中,假如不使用指針,可以認爲是機器在幫我們“打理”內存。
但是假如我們使用了指針,由於指針的自由度非常大,我們就可以自己“打理”內存了(PS:這裏的打理僅限內存指向問題,分配和清除肯定必然不行)。
Go中常用的指針
在C語言中,指針的操作是完全不被約束的,這就非常的危險:程序猿在寫的時候就得細心一點,拿着指針操作太多,指來指去,指到不該指的地方,就不好了~
所以Go語言在設計的時候,也考慮到了這個問題,就給現有的指針加了約束:把指針類型的數據當成了一種數據類型,在編譯的時候做嚴格判斷。
舉個例子來說: *int
和 *string
是兩種不同的類型,那既然類型都不同,那麼 *int
的數據就不能夠和 *string
類型的數據進行“互通”了,既不能相互賦值,也不能相互比較;
能不能加減運算呀?當然不能,因爲“數字兒”是整型, *int
或者 *string
是其他類型而非整型。
Go語言就給指針套了個“類型”的帽子,一下子把指針限制的死死的了。
而且Go最後規定:指針類型還不能相互強制轉換。
我們知道:int和string是可以相互轉換的,既然指針歸根到底是地址,也就是數字兒,那指針類型和int之間能否相互強制類型轉換呢?答案是不行!
那 *int
和 *string
之間是否可以強制類型轉換呢?答案是更不行,如果能強制轉換了,前面說的給指針套的那頂“類型”的帽子,是不是就白做了?
unsafe.Pointer
好了,扯了那麼多,終於到正題了。那麼unsafe.Pointer指針是什麼呢?
其實就一句話:就是C語言中那個牛逼到爆的什麼都能指的不安全的指針。再確切一點是: void*
。
unsafe.Pointer
源碼就兩行:
type ArbitraryType int //表示任何類型 type Pointer *ArbitraryType //表示任何類型的指針
unsafe.Pointer
的源碼註釋還提供了關於 unsafe.Pointer
的四點重要的使用規則:
1、Go語言常規的任何類型的指針都可以轉化爲unsafe.Pointer類型 2、unsafe.Pointer類型可以轉化爲Go語言常規的任何類型的指針。 3、uintptr這個類型可以轉化爲unsafe.Pointer 4、unsafe.Pointer可以轉化爲uintptr
看完規則,你可能會問: uintptr
是啥?
來,沒有比源碼更好的解釋的了:
注意看 uintptr
的位置,和 int
以及 uint
在一個包內,你可以認爲 uintptr
與它們”同類”,只不過是指針的專屬而已,但是你想自己定義用也能用。
對於 unsafe.Pointer
,多用於Go的編譯時期;由於它可以繞過類型系統,直接去訪問內存,所以它用起來效率會比較高,但是官方的態度是不太建議使用的,因爲不太安全。我個人建議也是能不用就不用:畢竟爲了這點兒效率帶來的額外的附加成本比較高。
好了,我們最後總結一下Go的指針:
更多精彩內容,請關注我的微信公衆號 互聯網技術窩