Golang中的unsafe標準庫包
2016/10/22, 老獏
unsafe標準庫包是Golang中一個比較特殊的包,為什麼這麼說?本問將詳細解釋其中緣由。
Go官方文件中的警告
unsafe包的文件這麼說:
Packages that import unsafe may be non-portable and are not protected by the Go 1 compatibility guidelines.
Go 1 相容性指南這麼說:
Packages that import unsafe may depend on internal properties of the Go implementation. We reserve the right to make changes to the implementation that may break such programs.
兩段話大同小異,簡而言之就是使用unsafe包是危險的,Golang不保證此包的跨平臺和跨版本相容性。聽上去挺慎人,但是此包究竟有多危險?先讓我們看看此包在Golang中發揮的角色是什麼。
unsafe包的角色
到目前為止(Go1.7),unsafe包包含以下資源:
- 3個函式:
- 和一個型別
- type Pointer *ArbitraryType
這裡ArbitraryType不是一個真正的型別,它僅僅是一個佔位符。
不像絕大多數的函式,在Golang中,以上3個函式的呼叫將在編譯時而不是執行時被估值,因此這3個函式的返回值可以被賦給常量。換句話說,這3個函式是為編譯器服務的。
除了這3個函式,unsafe包中唯一的型別unsafe.Pointer
也是為編譯器服務的。
由於安全原因,Golang不允許下列型別的值互相進行轉換:
- 兩個不同指標型別,比如
*int64
and*float64
; - 任何普通指標型別
*T
和uintptr
。
但是在unsafe.Pointer
的幫助下,我們可以打破Golang型別系統和記憶體管理的安全機制,從而使得以上的轉換成為可能。這是怎麼辦到的呢?讓我們看看unsafe包中列出的和unsafe.Pointer有關的規則:
- 任何型別的指標都可以被轉換為
unsafe.Pointer
型別; - 一個
unsafe.Pointer
值都可以被轉換為任何指標型別; - 一個
uintptr
值可以被轉換為unsafe.Pointer
型別; - 一個
unsafe.Pointer
值可以被轉換為uintptr
型別。
這些規則和Go白皮書是一致的:
Any pointer or value of underlying type uintptr can be converted to a Pointer type and vice versa.
這些規則表明unsafe.Pointer
如同c語言中的void*
。是的,c語言中的void*
很危險!
按照這些規則,對兩個不同的型別T1
和T2
,指標型別*T1
的值可以被轉換為unsafe.Pointer
型別,然後轉換後的unsafe.Pointer
型別的值可以繼續被轉換為*T2
型別(或者uintptr
型別);反之也是可以的。通過這種方式,Golang型別系統和記憶體管理的安全機制被繞過了。顯然,濫用這種方式是危險的。
看個例子:
package main
import (
"fmt"
"unsafe"
)
func main() {
var n int64 = 5
var pn = &n
var pf = (*float64)(unsafe.Pointer(pn))
// 到這裡,pn和pf指向同一個記憶體地址。
fmt.Println(*pf) // 2.5e-323
*pf = 3.14159
fmt.Println(n) // 4614256650576692846
}
此例中的轉換或許並沒有太大實際意義,但此轉換是安全和合法的。至於為什麼安全,見下文。
更多關於unsafe.Pointer和uintptr的一些事實
下面是關於unsafe.Pointer
和uintptr
的一些事實:
uintptr
是一個整數型別,- 即使一個
uintptr
值依然被程式使用中,此uintptr
值所表示的指標所指的資料可能將被垃圾回收。
- 即使一個
unsafe.Pointer
是一個指標型別,- 但是
unsafe.Pointer
值不能被解引用; - 和普通指標一樣,如果一個
unsafe.Pointer
值依然被程式使用中,此unsafe.Pointer
值所表示的指標所指的資料將確保不會被垃圾回收。
- 但是
*unsafe.Pointer
是一個普通指標,和*int
類似。
既然uintptr
是一個整數型別,那麼當然可以對uintptr
值進行算術運算。利用這一點,我們可以繞開Golang中指標不能進行偏移運算的限制:
package main
import (
"fmt"
"unsafe"
)
func main() {
a := [4]int{0, 1, 2, 3}
p1 := unsafe.Pointer(&a[1])
p3 := unsafe.Pointer(uintptr(p1) + 2 * unsafe.Sizeof(a[0]))
*(*int)(p3) = 6
fmt.Println("a =", a) // a = [0 1 2 6]
// ...
type Person struct {
name string
age int
gender bool
}
who := Person{"John", 30, true}
pp := unsafe.Pointer(&who)
pname := (*string)(unsafe.Pointer(uintptr(pp) + unsafe.Offsetof(who.name)))
page := (*int)(unsafe.Pointer(uintptr(pp) + unsafe.Offsetof(who.age)))
pgender := (*bool)(unsafe.Pointer(uintptr(pp) + unsafe.Offsetof(who.gender)))
*pname = "Alice"
*page = 28
*pgender = false
fmt.Println(who) // {Alice 28 false}
}
unsafe包究竟有多危險?
關於unsafe包,Go團隊的主力開發之一Ian已經確認:
- unsafe包中的函式原型今後保證不變;
- 型別
unsafe.Pointer
將永遠存在。
所以,看上去unsafe包中的函式並不怎麼危險。Go團隊的甚至想把它們放到別的包中。使用這些函式唯一的不安全性是這些函式的呼叫在今後的版本中可能會返回不同的結果值。但這種不安全性很難說是一種危險。
這樣我們可以的得出結論:使用unsafe包的危險都是和使用unsafe.Pointer
型別密切相關的。unsafe包的文件列出了一些合法和非法使用unsafe.Pointer
型別的例子,這裡僅僅列出部分非法的情形:
package main
import (
"fmt"
"unsafe"
)
// 情形A:unsafe.Pointer和uintptr之間的來回轉換
// 未在同一個表示式中完成。
func illegalUseA() {
fmt.Println("===================== illegalUseA")
pa := new([4]int)
// 將下面這個合法的使用
// p1 := unsafe.Pointer(uintptr(unsafe.Pointer(pa)) + unsafe.Sizeof(pa[0]))
// 分成兩個表示式(不合法的使用):
ptr := uintptr(unsafe.Pointer(pa))
p1 := unsafe.Pointer(ptr + unsafe.Sizeof(pa[0]))
// "go vet"將對上一行給出一個警告:
// possible misuse of unsafe.Pointer
// unsafe包的文件https://golang.org/pkg/unsafe/#Pointer,
// 認為上面分成兩行是非法的,
// 但是目前的Go編譯器和執行時(1.7.3)
// 沒有偵測到這個非法使用。
//
// 然而,為了保證你的Go程式絕對安全的執行,
// 最好還是請遵守unsafe包的文件中定的規則。
*(*int)(p1) = 123
fmt.Println("*(*int)(p1) :", *(*int)(p1))
}
// 情形B:指標指向了非法地址
func illegalUseB() {
fmt.Println("===================== illegalUseB")
a := [4]int{0, 1, 2, 3}
p := unsafe.Pointer(&a)
p = unsafe.Pointer(uintptr(p) + uintptr(len(a)) * unsafe.Sizeof(a[0]))
// 現在p指向了a值記憶體塊的結尾,
// 因為我們不清楚a值記憶體塊的結尾存出的是什麼,
// 所以p的當前值是非法的,雖然到目前為止也沒什麼危險。
// 但是如果我們改變了p所指的值,程式有可能執行錯亂。
*(*int)(p) = 123
fmt.Println("*(*int)(p) :", *(*int)(p)) // 123 or not 123
// 當前的Go編譯器和執行時(1.7.3)以及"go vet"
// 都沒有檢測到上面這個非法操作。
// 但是,當前的Go執行時(1.7.3)將偵測到下面的
// 非法操作(其實和上面的非法操作是一樣的)並panic。
p = unsafe.Pointer(&a)
for i := 0; i <= len(a); i++ {
*(*int)(p) = 123 // Go執行時(1.7.3)不會在此panic
fmt.Println(i, ":", *(*int)(p))
// 當i==4的時候,Go執行時(1.7.3)將在上一行panic
// runtime error: invalid memory address or nil pointer dereference
p = unsafe.Pointer(uintptr(p) + unsafe.Sizeof(a[0]))
}
}
func main() {
illegalUseA()
illegalUseB()
}
編譯器很難檢測到非法的unsafe.Pointer
使用。雖然"go vet"能發現一些潛在的非法的unsafe.Pointer
使用,但也不能檢測到所有的非法使用。同樣,Go執行時也不能檢測到所有的非法unsafe.Pointer
使用。非法unsafe.Pointer
使用可能會是程式崩潰,或者使程式執行詭異(比如有時候表現正常,有時候卻很反常)。這就是為什麼說使用unsafe包是危險的。
*T1
和*T2
之間的轉換
對於將*T1
值轉換為unsafe.Pointer
型別,繼而再轉換為*T2
型別,unsafe包是這麼說的:
Provided that
T2
is no larger thanT1
and that the two share an equivalent memory layout, this conversion allows reinterpreting data of one type as data of another type.
直譯過來:如果型別T2
的大小不比T1
大並且這兩個型別有著等價的記憶體佈局,這種轉換允許其中一個型別的資料用另一個型別的資料來表示。(反正我是沒看太明白)
這裡對equivalent memory layout(記憶體佈局)的定義很含糊,而且好像Go團隊故意將其定義得很含糊。 這使得使用unsafe包更加的危險。
既然Go團隊不願意定義一個精確的規則,本文也不會做這個嘗試。這裡僅僅列出部分合法的例子。
合法使用1:在切片[]T
和[]MyT
之間轉換
在下例中,我們使用int
型別表示T
:
type MyInt int
在Golang的型別系統中,型別[]int
和[]MyInt
的底層型別均為它們自身。Golang中,底層型別不一樣的兩個非介面型別之間是不支援轉換的。但是在unsafe.Pointer
的幫助下,這種轉換成為可能:
package main
import (
"fmt"
"unsafe"
)
func main() {
type MyInt int
a := []MyInt{0, 1, 2}
// b := ([]int)(a) // error: cannot convert a (type []MyInt) to type []int
b := *(*[]int)(unsafe.Pointer(&a))
b[0]= 3
fmt.Println("a =", a) // a = [3 1 2]
fmt.Println("b =", b) // b = [3 1 2]
a[2] = 9
fmt.Println("a =", a) // a = [3 1 9]
fmt.Println("b =", b) // b = [3 1 9]
}
合法使用2:呼叫sync/atomic包中指標相關的函式
sync/atomic包中和指標相關的函式的引數和返回值型別大多都是unsafe.Pointer
或者 *unsafe.Pointer
:
- func CompareAndSwapPointer(addr *unsafe.Pointer, old, new unsafe.Pointer) (swapped bool)
- func LoadPointer(addr *unsafe.Pointer) (val unsafe.Pointer)
- func StorePointer(addr *unsafe.Pointer, val unsafe.Pointer)
- func SwapPointer(addr *unsafe.Pointer, new unsafe.Pointer) (old unsafe.Pointer)
為了呼叫這些函式,必須引入unsafe包。
注意:*unsafe.Pointer
屬於普通型別,所以*unsafe.Pointer
值可以被轉換為unsafe.Pointer
型別,反之亦然。
package main
import (
"fmt"
"log"
"time"
"unsafe"
"sync/atomic"
"sync"
"math/rand"
)
var data *string
// get data atomically
func Data() string {
p := (*string)(atomic.LoadPointer(
(*unsafe.Pointer)(unsafe.Pointer(&data)),
))
if p == nil {
return ""
} else {
return *p
}
}
// set data atomically
func SetData(d string) {
atomic.StorePointer(
(*unsafe.Pointer)(unsafe.Pointer(&data)),
unsafe.Pointer(&d),
)
}
func main() {
var wg sync.WaitGroup
wg.Add(200)
for range [100]struct{}{} {
go func() {
time.Sleep(time.Second * time.Duration(rand.Intn(1000)) / 1000)
log.Println(Data())
wg.Done()
}()
}
for i := range [100]struct{}{} {
go func(i int) {
time.Sleep(time.Second * time.Duration(rand.Intn(1000)) / 1000)
s := fmt.Sprint("#", i)
log.Println("====", s)
SetData(s)
wg.Done()
}(i)
}
wg.Wait()
fmt.Println("final data = ", *data)
}
結論
- unsafe包是為編譯器而不是執行時服務的;
- 使用unsafe做為包名是為了讓程式設計師小心使用此包;
- 使用unsafe包並非總是不推薦的,有時我們必須使用它;
- Golang的型別系統設計同時兼顧安全性和效率,但安全性要優先於效率。所以有時候為了普遍安全性將犧牲一些效率。unsafe包使得經驗豐富的程式設計師在某些時候不影響安全性的同時繞開Golang的安全機制,從而避免一些正常使用中的效率犧牲。
- 當然,再重申一遍,unsafe包很容易被濫用,因此使用之是危險的。
相關文章
- 標準庫unsafe:帶你突破golang中的型別限制Golang型別
- golang標準庫的分析os包(6)Golang
- 「Golang成長之路」標準庫之os包Golang
- 「Golang成長之路」標準庫之time包Golang
- golang標準庫之 fmtGolang
- Go 的 golang.org/x/ 系列包和標準庫包有什麼區別?Golang
- 標準庫 fmt 包的基本使用
- log包在Golang語言的標準庫中是怎麼使用的?Golang
- Go標準庫flag包的“小陷阱”Go
- golang如何使用指標靈活操作記憶體?unsafe包原理解析Golang指標記憶體
- Golang標準庫學習—container/heapGolangAI
- 標準庫 http 包的簡單實用HTTP
- CUJ:標準庫:標準庫中的搜尋演算法 (轉)演算法
- Golang語言標準庫time實戰篇Golang
- Go unsafe包Go
- go標準庫-log包原始碼學習Go原始碼
- Go標準包-http包serverGoHTTPServer
- Python標準庫04 檔案管理 (部分os包,shutil包)Python
- Python標準庫分享之儲存物件 (pickle包,cPickle包)Python物件
- Go標準包——net/rpc包的使用GoRPC
- Python標準庫中隱藏的利器Python
- C++ 及標準庫中的那些大坑C++
- golang中的context包GolangContext
- Golang中閉包的理解Golang
- Golang 標準庫 限流器 time/rate 設計與實現Golang
- golang unsafe.Pointer與uintptrGolangUI
- Python標準庫分享之檔案管理 (部分os包,shutil包)Python
- Python 快速教程(標準庫05):儲存物件 (pickle包,cPickle包)Python物件
- Go標準包—http clientGoHTTPclient
- Python標準庫10 多程式初步 (multiprocessing包)Python
- Python標準庫11 多程式探索 (multiprocessing包)Python
- Python 快速教程(標準庫06):子程式 (subprocess包)Python
- 6. 開篇《 刻意學習 Golang - 標準庫原始碼分析 》Golang原始碼
- c標準庫中qsort函式用法函式
- Python標準庫12 數學與隨機數 (math包,random包)Python隨機random
- Python 快速教程(標準庫04):檔案管理 (部分os包,shutil包)Python
- Python 快速教程(標準庫07):訊號 (signal包,部分os包)Python
- C 標準庫 -