《快學 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 包來讀取結構體的內容,形式上比較繁瑣,注意看代碼中的註釋,讀者須要稍微轉一轉腦殼來理解一下上面的代碼。接下來咱們再嘗試修改結構體的值ui

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 方法來替換它,它能夠直接獲得字段在結構體內的偏移量spa

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

你也許會抱怨爲啥指針操做這麼繁瑣,不能簡單一點麼?Go 語言的設計者故意這樣設計的,由於指針操做很是的不安全,因此它要給用戶設置障礙。設計

探索切片內部結構

在切片小節,咱們知道了切片分爲切片頭和內部數組兩部分,下面咱們使用 unsafe 包來驗證一下切片的內部數據結構,看看它和咱們預期的是否同樣。3d

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 語言》更多章節,長按圖片識別二維碼關注公衆號「碼洞」

相關文章
相關標籤/搜索