1. 程式人生 > >《快學 Go 語言》第 14 課 —— 魔術變性指標

《快學 Go 語言》第 14 課 —— 魔術變性指標

本節我們要學習一些 Go 語言的魔法功能,通過內建的 unsafe 包提供的功能,直接操縱指定記憶體地址的記憶體。有了 unsafe 包,我們就可以洞悉 Go 語言內建資料結構的內部細節。

unsafe.Pointer

Pointer 代表著變數的記憶體地址,可以將任意變數的地址轉換成 Pointer 型別,也可以將 Pointer 型別轉換成任意的指標型別,它是不同指標型別之間互轉的中間型別。Pointer 本身也是一個整型的值。

type Pointer int
複製程式碼

在 Go 語言裡不同型別之間的轉換是要受限的。普通的基礎變數轉換成不同的型別需要進行記憶體淺拷貝,而指標變數型別之間是禁止直接轉換的。要打破這個限制,unsafe.Pointer 就可以派上用場,它允許任意指標型別的互轉。

指標的加減運算

Pointer 雖然是整型的,但是編譯器禁止它直接進行加減運算。如果要進行運算,需要將 Pointer 型別轉換 uintptr 型別進行加減,然後再將 uintptr 轉換成 Pointer 型別。uintptr 其實也是一個整型。

type uintptr int
複製程式碼

下面讓我們就來嘗試一下剛剛學到的魔法

package main

import "fmt"
import "unsafe"

type Rect struct {
	Width int
	Height int
}

func main() {
	var r = Rect {50
, 50} // *Rect => Pointer => *int => int var width = *(*int)(unsafe.Pointer(&r)) // *Rect => Pointer => uintptr => Pointer => *int => int var height = *(*int)(unsafe.Pointer(uintptr(unsafe.Pointer(&r)) + uintptr(8))) fmt.Println(width, height) } ------ 50 50 複製程式碼

上面的程式碼是用 unsafe 包來讀取結構體的內容,形式上比較繁瑣,注意看程式碼中的註釋,讀者需要稍微轉一轉腦袋來理解一下上面的程式碼。接下來我們再嘗試修改結構體的值

package main

import "fmt"
import "unsafe"

type Rect struct {
	Width int
	Height int
}

func main() {
	var r = Rect {50, 50}
	// var pw *int
	var pw = (*int)(unsafe.Pointer(&r))
	// var ph *int
	var ph = (*int)(unsafe.Pointer(uintptr(unsafe.Pointer(&r)) + uintptr(8)))
	*pw = 100
	*ph = 100
	fmt.Println(r.Width, r.Height)
}

--------
100 100
複製程式碼

程式碼中的 uintptr(8) 很不優雅,可以使用 unsafe 提供了 Offsetof 方法來替換它,它可以直接得到欄位在結構體內的偏移量

var ph = (*int)(unsafe.Pointer(uintptr(unsafe.Pointer(&r)) + unsafe.Offsetof(r.Height))
複製程式碼

你也許會抱怨為啥指標操作這麼繁瑣,不能簡單一點麼?Go 語言的設計者故意這樣設計的,因為指標操作非常的不安全,所以它要給使用者設定障礙。

探索切片內部結構

在切片小節,我們知道了切片分為切片頭和內部陣列兩部分,下面我們使用 unsafe 包來驗證一下切片的內部資料結構,看看它和我們預期的是否一樣。

package main

import "fmt"
import "unsafe"

func main() {
	// head = {address, 10, 10}
	// body = [1,2,3,4,5,6,7,8,9,10]
	var s = []int{1,2,3,4,5,6,7,8,9,10}
	var address = (**[10]int)(unsafe.Pointer(&s))
	var len = (*int)(unsafe.Pointer(uintptr(unsafe.Pointer(&s)) + uintptr(8)))
	var cap = (*int)(unsafe.Pointer(uintptr(unsafe.Pointer(&s)) + uintptr(16)))
	fmt.Println(address, *len, *cap)
	var body = **address
	for i:=0; i< len(body); i++ {
		fmt.Printf("%d ", body[i])
	}
}

------------------
0xc42000a080 10 10
1 2 3 4 5 6 7 8 9 10
複製程式碼

輸出的結果正是我們鎖期望的,不過讀者需要仔細思考一下 address 為什麼是二級指標變數。

圖片

字串與位元組切片的高效轉換

在字串小節我們提到位元組切片和字串之間的轉換需要複製記憶體,如果字串或者位元組切片的長度較大,轉換起來會有較高的成本。下面我們通過 unsafe 包提供另一種高效的轉換方法,讓轉換前後的字串和位元組切片共享內部儲存。

字串和位元組切片的不同點在於頭部,字串的頭部 2 個 int 位元組,切片的頭部 3 個 int 位元組

package main

import "fmt"
import "unsafe"

func main() {
	fmt.Println(bytes2str(str2bytes("hello")))
}

func str2bytes(s string) []byte {
	var strhead = *(*[2]int)(unsafe.Pointer(&s))
	var slicehead [3]int
	slicehead[0] = strhead[0]
	slicehead[1] = strhead[1]
	slicehead[2] = strhead[1]
	return *(*[]byte)(unsafe.Pointer(&slicehead))
}

func bytes2str(bs []byte) string {
	return *(*string)(unsafe.Pointer(&bs))
}

-----
hello
複製程式碼

切記通過這種形式轉換而成的位元組切片千萬不可以修改,因為它的底層位元組陣列是共享的,修改會破壞字串的只讀規則。其次使用這種形式得到的字串或者切片只可以用作臨時的區域性變數,因為被共享的位元組陣列隨時可能會被回收,原字串或者位元組切片的記憶體由於不再被引用,讓垃圾回收器解決掉了。

深入介面變數的賦值

在介面變數的小節,有一個問題還懸而未決,那就是介面變數在賦值時發生了什麼?

通過 unsafe 包,我們就可以看清裡面的細節,下面我們將一個結構體變數賦值給介面變數,看看修改結構體的記憶體會不會影響到介面變數的資料記憶體

package main

import "fmt"
import "unsafe"

type Rect struct {
	Width int
	Height int
}

func main() {
	var r = Rect{50, 50}
	// {typeptr, dataptr}
	var s interface{} = r
	
	var sptrs = *(*[2]*Rect)(unsafe.Pointer(&s))
	// var dataptr *Rect
	var sdataptr = sptrs[1]
	fmt.Println(sdataptr.Width, sdataptr.Height)
	
	// 修改原物件,看看介面指向的物件是否受到影響
	r.Width = 100
	fmt.Println(sdataptr.Width, sdataptr.Height)
}

-------
50 50
50 50
複製程式碼

從輸出中可以得出結論,將結構體變數賦值給介面變數,結構體記憶體會被複制。那如果是兩個介面變數之間的賦值呢,會不會同樣也需要複製指向的資料呢?

package main

import "fmt"
import "unsafe"

type Rect struct {
	Width int
	Height int
}

func main() {
	// {typeptr, dataptr}
	var s interface{} = Rect{50, 50}
	var r = s

	var rptrs = *(*[2]*Rect)(unsafe.Pointer(&r))
	var rdataptr = rptrs[1]
	var sptrs = *(*[2]*Rect)(unsafe.Pointer(&s))
	var sdataptr = sptrs[1]

	fmt.Println(sdataptr.Width, sdataptr.Height)
	fmt.Println(rdataptr.Width, rdataptr.Height)

	// 修改原物件
	sdataptr.Width = 100
	// 再對比一下原物件和目標物件
	fmt.Println(sdataptr.Width, sdataptr.Height)
	fmt.Println(rdataptr.Width, rdataptr.Height)
}

-----------
50 50
50 50
100 50
100 50
複製程式碼

從輸出中可以發現賦值前後兩個介面變數共享了資料記憶體,沒有發生資料的複製。接下來我們再引入第 3 個問題,不同型別的介面變數賦值會不會發生複製?

package main

import "fmt"
import "unsafe"

type Areable interface {
	Area() int
}

type Rect struct {
	Width int
	Height int
}

func (r Rect) Area() int {
	return r.Width * r.Height
}

func main() {
	// {typeptr, dataptr}
	var s Areable = Rect{50, 50}
	var r interface{} = s

	var rptrs = *(*[2]*Rect)(unsafe.Pointer(&r))
	var rdataptr = rptrs[1]
	var sptrs = *(*[2]*Rect)(unsafe.Pointer(&s))
	var sdataptr = sptrs[1]

	fmt.Println(sdataptr.Width, sdataptr.Height)
	fmt.Println(rdataptr.Width, rdataptr.Height)

	// 修改原物件
	sdataptr.Width = 100
	// 再對比一下原物件和目標物件
	fmt.Println(sdataptr.Width, sdataptr.Height)
	fmt.Println(rdataptr.Width, rdataptr.Height)
}

------
50 50
50 50
100 50
100 50
複製程式碼

結果是不同型別介面之間賦值指向的資料物件還是共享的。接下來我們再引入第 4 個 問題,介面型別之間在造型時是否會發生記憶體的複製。

package main

import "fmt"
import "unsafe"

type Areable interface {
	Area() int
}

type Rect struct {
	Width int
	Height int
}

func (r Rect) Area() int {
	return r.Width * r.Height
}

func main() {
	// {typeptr, dataptr}
	var s interface{} = Rect{50, 50}
	var r Areable = s.(Areable)

	var rptrs = *(*[2]*Rect)(unsafe.Pointer(&r))
	var rdataptr = rptrs[1]
	var sptrs = *(*[2]*Rect)(unsafe.Pointer(&s))
	var sdataptr = sptrs[1]

	fmt.Println(sdataptr.Width, sdataptr.Height)
	fmt.Println(rdataptr.Width, rdataptr.Height)

	// 修改原物件
	sdataptr.Width = 100
	// 再對比一下原物件和目標物件
	fmt.Println(sdataptr.Width, sdataptr.Height)
	fmt.Println(rdataptr.Width, rdataptr.Height)
}

------
50 50
50 50
100 50
100 50
複製程式碼

答案是不同介面型別之間造型資料還是共享的。最後再提一個問題,將介面型別造型成結構體型別,是否會發生記憶體複製?

package main

import "fmt"
import "unsafe"

type Areable interface {
	Area() int
}

type Rect struct {
	Width int
	Height int
}

func (r Rect) Area() int {
	return r.Width * r.Height
}

func main() {
	// {typeptr, dataptr}
	var s interface{} = Rect{50, 50}
	var r Rect = s.(Rect)

	var sptrs = *(*[2]*Rect)(unsafe.Pointer(&s))
	var sdataptr = sptrs[1]

	// 修改原物件
	sdataptr.Width = 100
	// 再對比一下原物件和目標物件
	fmt.Println(sdataptr.Width, sdataptr.Height)
	fmt.Println(r.Width, r.Height)
}
複製程式碼

答案是將介面造型成結構體型別,記憶體會發生複製,它們之間的資料不會共享。

從上面 5 個 問題,我們可以得出結論,介面型別和結構體型別似乎是兩個不同的世界。只有介面型別之間的賦值和轉換會共享資料,其它情況都會複製資料,其它情況包括結構體之間的賦值,結構體轉介面,介面轉結構體。不同介面變數之間的轉換本質上只是調整了介面變數內部的型別指標,資料指標並不會發生改變。

通過 unsafe 包我們還可以分析很多細節,在高階內容部分,我們將會頻繁使用這個工具。

閱讀《快學 Go 語言》更多章節,長按圖片識別二維碼關注公眾號「碼洞」