uintptr 和 unsafe普及
uintptr
在Go的源碼中uintptr的定義如下:
/* uintptr is an integer type that is large enough to hold the bit pattern of any pointer.
從英文注釋可以看出 uintptr是一個整形,它的大小能夠容納任何指針的位模式,它是無符號的,最大值為:18446744073709551615,怎么來的,int64最大值 * 2 +1
*/
type uintptr uintptr
位模式:內存由字節組成.每個字節由8位bit組成,每個bit狀態只能是0或1.所謂位模式,就是變量所占用內存的所有bit的狀態的序列
指針大小:一個指針的大小是多少呢?在32位操作系統上,指針大小是4個字節,在64位操作系統上,指針的大小是8字節,
所以uintptr能夠容納任何指針的位模式,總的說uintptr表示的指針地址的值,可以用來進行數值計算
GC不會把uintptr當作指針,uintptr不會持有一個對象,uintptr類型的目標會被GC回收
unasfe
在Go中,unsafe是一個包,內容也比較簡短,但注釋非常多,這個包主要是用來在一些底層編程中,讓你能夠操作內存地址計算,也就是說Go本身是不支持指針運算,但還是留了一個后門,而且Go也不建議研發人員直接使用unsafe包的方法,因為它繞過了Go的內存安全原則,是不安全的,容易使你的程序出現莫名其妙的問題,不利於程序的擴展與維護但為什么說它呢,因為很多框架包括SDK中的源代碼都用到了這個包的知識,在看源代碼時這塊不懂,容易懵。下面看看這個包定義了什么?
//ArbitraryType的類型也是int,但它被賦予特殊的含義,代表一個Go的任意表達式類型
type ArbitraryType int
//Pointer是一個int指針類型,在Go種,它是所有指針類型的父類型,也就是說所有的指針類型都可以轉化為Pointer, uintptr和Pointer可以相互轉化
type Pointer *ArbitraryType
//返回指針變量在內存中占用的字節數(記住,不是變量對應的值占用的字節數)
func Sizeof(x ArbitraryType) uintptr
/*Offsetof返回變量指定屬性的偏移量,這個函數雖然接收的是任何類型的變量,但是有一個前提,就是變量要是一個struct類型,且還不能直接將這個struct類型的變量當作參數,只能將這個struct類型變量的屬性當作參數*/
func Offsetof(x ArbitraryType) uintptr
//返回變量對齊字節數量
func Alignof(x ArbitraryType) uintptr
什么是內存對齊?為什么要內存對齊?
在我了解比較深入的語言中(Java Go)都有內存對齊的概念,百度百科對內存對齊的概念是這樣定義的:“內存對齊”應該是編譯器的“管轄范圍”。編譯器為程序中的每個“數據單元”安排在適當的位置上,所謂的數據單元其實就是變量的值。
為什么要內存對齊呢?
- 平台原因(移植原因):不是所有的硬件平台都能訪問任意地址上的任意數據的;某些硬件平台只能在某些地址處取某些特定類型的數據,否則拋出硬件異常(32位平台上運行64位平台上編譯的程序要求必須8字節對齊,否則發生panic)
- 性能原因:數據結構(尤其是棧)應該盡可能地在自然邊界上對齊。原因在於,為了訪問未對齊的內存,處理器需要作兩次內存訪問;而對齊的內存訪問僅需要一次訪問
對齊規則:也就是對齊的邊界,多少個字節內存對齊,在32位操作系統上,是4個自己,在64位操作系統上是8個字節
通過一幅圖來理解上面的內容,下圖只是舉個例子,位數並沒有畫全
指針運算和內存對齊實踐
內存對齊實踐
理論總是枯燥的,但必須了解,也許看了理論還是不懂,接下來通過實踐讓你明白
//創建一個變量
var i int8 = 10
//建一個變量轉化成Pointer 和 uintptr
p := unsafe.Pointer(&i) //入參必須是指針類型的
fmt.Println(p) //是內存地址0xc0000182da
u := uintptr(i)
fmt.Println(u) //結果就是10
//Pointer轉換成uintptr
temp := uintptr(p)
//uintptr轉Pointer
p= unsafe.Pointer(u)
//獲取指針大小
u = unsafe.Sizeof(p) //傳入指針,獲取的是指針的大小
fmt.Println(u) // 打印u是:8
//獲取的是變量的大小
u = unsafe.Sizeof(i)
fmt.Println(u) //打印u是:1
//創建兩個個結構體
type Person1 struct{
a bool
b int64
c int8
d string
}
type Person2 struct{
b int64
c int8
a bool
d string
}
//接下來演示一下內存對齊,猜一猜下面l兩個打印值是多少呢?
person1 := Person1{a:true,b:1,c:1,d:"spw"}
fmt.Println(unsafe.Sizeof(person1))
person2 := Person2{b:1,c:1,a:true,d:"spw"}
fmt.Println(unsafe.Sizeof(person2))
//第一個結果是40,第二個結果是32,為什么會有這些差距呢?其實就是內存對齊做的鬼,我來詳細解釋一下
我們知道在Person1和Person2種變量類型都一樣,只是順序不太一樣,
bool占1個字節,
int64占8個字節,
int8占一個字節,
string占用16個字節,
總的結果應該是 1+8+1+16= 26,為啥Person1是40呢,Person2是32,看下圖
根據上圖,我們就明白了,在結構體編寫中存在內存對齊的概念,而且我們應該小心,盡可能的避免因內存對齊導致結構體大小增大,在書寫過程中應該讓小字節的變量挨着。我們可以工具進行檢測(golangci-lint)。
我們可以通過func Alignof(x ArbitraryType) uintptr
這個方法返回內存對齊的字節數量,如下代碼
type Person1 struct{
a bool
b int64
c int8
d string
}
p := Person{a:true,b:1,c:1,d:"spw"}
fmt.Println(unsafe.Alignof(person))
type Person2 struct{
a bool
c int8
}
p1 := Person1{a:true,b:1,c:1,d:"spw"}
fmt.Println(unsafe.Alignof(p1))
p2 := Person2{a:true,c:1}
fmt.Println(unsafe.Alignof(p2))
//你任務上面兩個println打印多少呢?結果是8,1,在結構體中,內存對齊是按照結構體中最大字節數對齊的(但不會超過8)
指針運算實踐
我們還是用代碼來舉例說明
type W struct {
b int32
c int64
}
var w *W = new(W)
//這時w的變量打印出來都是默認值0,0
fmt.Println(w.b,w.c)
//現在我們通過指針運算給b變量賦值為10
b := unsafe.Pointer(uintptr(unsafe.Pointer(w)) + unsafe.Offsetof(w.b))
*((*int)(b)) = 10
//此時結果就變成了10,0
fmt.Println(w.b,w.c)
解釋一下上面的代碼
uintptr(unsafe.Pointer(w))
獲取了w的指針起始值,
unsafe.Offsetof(w.b)
獲取b變量的偏移量
兩個相加就得到了b的地址值,將通用指針Pointer轉換成具體指針((*int)(b))
,通過 * 符號取值,然后賦值,((int)(b)) 相當於把(*int) 轉換成 int了,最后對變量重新賦值成10,這樣指針運算就完成了。
