Go 学习笔记(74)— Go 标准库之 unsafe
Go 語言自帶的 unsafe 包的高級用法, 顧名思義,unsafe 是不安全的。Go 將其定義為這個包名,也是為了讓我們盡可能地不使用它。不過雖然不安全,它也有優勢,那就是可以繞過 Go 的內存安全機制,直接對內存進行讀寫。所以有時候出于性能需要,還是會冒險使用它來對內存進行操作。
1. 指針類型轉換
Go 是一門強類型的靜態語言。強類型意味著一旦定義了,類型就不能改變;靜態意味著類型檢查在運行前就做了。同時出于安全考慮,Go 語言是不允許兩個指針類型進行轉換的。
我們一般使用 *T 作為一個指針類型,表示一個指向類型 T 變量的指針。為了安全的考慮,兩個不同的指針類型不能相互轉換,比如 *int 不能轉為 *float64 。
我們來看下面的代碼:
func main() {i:= 10ip:=&ivar fp *float64 = (*float64)(ip)fmt.Println(fp)
}
這個代碼在編譯的時候,會提示
cannot convert ip (type * int) to type * float64
也就是不能進行強制轉型。那如果還是需要轉換呢?這就需要使用 unsafe 包里的 Pointer 了。
unsafe.Pointer 是一種特殊意義的指針,可以表示任意類型的地址,類似 C 語言里的 void* 指針,是全能型的。
正常情況下,*int 無法轉換為 *float64 ,但是通過 unsafe.Pointer 做中轉就可以了。在下面的示例中,通過 unsafe.Pointer 把 *int 轉換為 *float64,并且對新的 *float64 進行 3 倍的乘法操作,你會發現原來變量 i 的值也被改變了,變為 30。
func main() {i := 10ip := &ivar fp *float64 = (*float64)(unsafe.Pointer(ip))*fp = *fp * 3fmt.Println(*ip) // 30
}
說明通過 unsafe.Pointer 這個萬能的指針,我們可以在 *T 之間做任何轉換。那么 unsafe.Pointer 到底是什么?為什么其他類型的指針可以轉換為 unsafe.Pointer 呢?這就要看 unsafe.Pointer 的源代碼定義了,如下所示:
// ArbitraryType is here for the purposes of documentation
// only and is not actually part of the unsafe package.
// It represents the type of an arbitrary Go expression.
type ArbitraryType int
type Pointer *ArbitraryType
按 Go 語言官方的注釋,ArbitraryType 可以表示任何類型(這里的 ArbitraryType 僅僅是文檔需要,不用太關注它本身,只要記住可以表示任何類型即可)。 而 unsafe.Pointer 又是 *ArbitraryType ,也就是說 unsafe.Pointer 是任何類型的指針,也就是一個通用型的指針,足以表示任何內存地址。
2. uintptr 指針類型
uintptr 也是一種指針類型,它足夠大,可以表示任何指針。它的類型定義如下所示:
// uintptr is an integer type that is large enough
// to hold the bit pattern of any pointer.
type uintptr uintptr
既然已經有了 unsafe.Pointer ,為什么還要設計 uintptr 類型呢?這是因為 unsafe.Pointer 不能進行運算,比如不支持 +(加號)運算符操作,但是 uintptr 可以。通過它,可以對指針偏移進行計算,這樣就可以訪問特定的內存,達到對特定內存讀寫的目的,這是真正內存級別的操作。
在下面的代碼中,通過指針偏移修改 struct 結構體內的字段為例,演示 uintptr 的用法。
func main() {p := new(person)//Name是person的第一個字段不用偏移,即可通過指針修改pName := (*string)(unsafe.Pointer(p))*pName = "wohu"//Age并不是person的第一個字段,所以需要進行偏移,這樣才能正確定位到Age字段這塊內存,才可以正確的修改pAge := (*int)(unsafe.Pointer(uintptr(unsafe.Pointer(p)) + unsafe.Offsetof(p.Age)))*pAge = 20fmt.Printf("p is %#v", *p) // p is main.person{Name:"wohu", Age:20}
}type person struct {Name stringAge int
}
這個示例不是通過直接訪問相應字段的方式對 person 結構體字段賦值,而是通過指針偏移找到相應的內存,然后對內存操作進行賦值。
下面詳細介紹操作步驟。
-
先使用
new函數聲明一個*person類型的指針變量p。 -
然后把
*person類型的指針變量p通過unsafe.Pointer,轉換為*string類型的指針變量pName。 -
因為
person這個結構體的第一個字段就是string類型的Name,所以pName這個指針就指向Name字段(偏移為 0),對pName進行修改其實就是修改字段Name的值。 -
因為
Age字段不是person的第一個字段,要修改它必須要進行指針偏移運算。所以需要先把指針變量p通過unsafe.Pointer 轉換為uintptr,這樣才能進行地址運算。
既然要進行指針偏移,那么要偏移多少呢?這個偏移量可以通過函數 unsafe.Offsetof 計算出來,該函數返回的是一個 uintptr 類型的偏移量,有了這個偏移量就可以通過 + 號運算符獲得正確的 Age 字段的內存地址了,也就是通過 unsafe.Pointer 轉換后的 *int 類型的指針變量 pAge。
然后需要注意的是,如果要進行指針運算,要先通過 unsafe.Pointer 轉換為 uintptr 類型的指針。指針運算完畢后,還要通過 unsafe.Pointer 轉換為真實的指針類型(比如示例中的 *int 類型),這樣可以對這塊內存進行賦值或取值操作。
- 有了指向字段
Age的指針變量pAge,就可以對其進行賦值操作,修改字段Age的值了。
這個示例主要是為了講解 uintptr 指針運算,所以一個結構體字段的賦值才會寫得這么復雜,如果按照正常的編碼,以上示例代碼會和下面的代碼結果一樣。
func main() {p :=new(person)p.Name = "wohu"p.Age = 20fmt.Println(*p)
}
指針運算的核心在于它操作的是一個個內存地址,通過內存地址的增減,就可以指向一塊塊不同的內存并對其進行操作,而且不必知道這塊內存被起了什么名字(變量名)。
3. 指針轉換規則
你已經知道 Go 語言中存在三種類型的指針,它們分別是:常用的 *T 、unsafe.Pointer 及 uintptr 。通過以上示例講解,可以總結出這三者的轉換規則:
- 任何類型的
*T都可以轉換為unsafe.Pointer; unsafe.Pointer也可以轉換為任何類型的*T;unsafe.Pointer可以轉換為uintptr;uintptr也可以轉換為unsafe.Pointer;
可以發現,unsafe.Pointer 主要用于指針類型的轉換,而且是各個指針類型轉換的橋梁。uintptr 主要用于指針運算,尤其是通過偏移量定位不同的內存。
4. unsafe.Sizeof
Sizeof 函數可以返回一個類型所占用的內存大小,這個大小只與類型有關,和類型對應的變量存儲的內容大小無關,比如 bool 型占用一個字節、int8 也占用一個字節。
通過 Sizeof 函數你可以查看任何類型(比如字符串、切片、整型)占用的內存大小,示例代碼如下:
func main() {fmt.Println(unsafe.Sizeof(true)) // 1fmt.Println(unsafe.Sizeof(int8(0))) // 1fmt.Println(unsafe.Sizeof(int16(0))) // 2fmt.Println(unsafe.Sizeof(int32(0))) // 4fmt.Println(unsafe.Sizeof(int64(0))) // 8fmt.Println(unsafe.Sizeof(int(0))) // 8fmt.Println(unsafe.Sizeof(string("張三"))) // 16fmt.Println(unsafe.Sizeof([]string{"李四", "張三"})) // 24
}
對于整型來說,占用的字節數意味著這個類型存儲數字范圍的大小,比如 int8 占用一個字節,也就是 8bit,所以它可以存儲的大小范圍是 -128~~127,也就是 ?2^(n-1) 到 2^(n-1)?1 。其中 n 表示 bit,int8 表示 8bit,int16 表示 16bit,以此類推。
對于和平臺有關的 int 類型,要看平臺是 32 位還是 64 位,會取最大的。比如我自己測試以上輸出,會發現 int 和 int64 的大小是一樣的,因為我用的是 64 位平臺的電腦。
小提示:一個 struct 結構體的內存占用大小,等于它包含的字段類型內存占用大小之和。
總結:
unsafe 包里最常用的就是 Pointer 指針,通過它可以讓你在 *T、uintptr 及 Pointer 三者間轉換,從而實現自己的需求,比如零內存拷貝或通過 uintptr 進行指針運算,這些都可以提高程序效率。
unsafe 包里的功能雖然不安全,但的確很香,比如指針運算、類型轉換等,都可以幫助我們提高性能。不過我還是建議盡可能地不使用,因為它可以繞開 Go 語言編譯器的檢查,可能會因為你的操作失誤而出現問題。當然如果是需要提高性能的必要操作,還是可以使用,比如 []byte 轉 string,就可以通過 unsafe.Pointer 實現零內存拷貝。
5. uintptr 和 unsafe.Pointer 的區別
unsafe.Pointer只是單純的通用指針類型,用于轉換不同類型指針,它不可以參與指針運算;- 而
uintptr是用于指針運算的,GC不把uintptr當指針,也就是說uintptr無法持有對象,uintptr類型的目標會被回收; unsafe.Pointer可以和 普通指針 進行相互轉換;unsafe.Pointer可以和uintptr進行相互轉換;
package mainimport ("fmt""unsafe"
)type W struct {b int32c int64
}func main() {var w *W = new(W)//這時w的變量打印出來都是默認值0,0fmt.Println(w.b, w.c)//現在我們通過指針運算給b變量賦值為10b := unsafe.Pointer(uintptr(unsafe.Pointer(w)) + unsafe.Offsetof(w.b))*((*int)(b)) = 10//此時結果就變成了10,0fmt.Println(w.b, w.c)
}
uintptr(unsafe.Pointer(w))獲取了w的指針起始值;unsafe.Offsetof(w.b)獲取b變量的偏移量;- 兩個相加就得到了
b的地址值,將通用指針Pointer轉換成具體指針((*int)(b)),通過*符號取值,然后賦值。*((*int)(b))相當于把(*int)(b)轉換成int了,最后對變量重新賦值成 10,這樣指針運算就完成了。
總結
以上是生活随笔為你收集整理的Go 学习笔记(74)— Go 标准库之 unsafe的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 2022-2028年中国氟橡胶密封件行业
- 下一篇: 2022-2028年中国遇水膨胀橡胶行业