[譯]Go裡面的unsafe包詳解
The unsafe Package in Golang
Golang 的 unsafe 包是一個很特殊的包。 為什麼這樣說呢? 本文將詳細解釋。
來自 go 語言官方文件的警告
unsafe 包的文件是這麼說的:
匯入unsafe的軟體包可能不可移植,並且不受Go 1相容性指南的保護。
Go 1 相容性指南這麼說:
匯入unsafe軟體包可能取決於Go實現的內部屬性。 我們保留對可能導致程式崩潰的實現進行更改的權利。
當然包名稱暗示 unsafe 包是不安全的。 但這個包有多危險呢? 讓我們先看看 unsafe 包的作用。
Unsafe 包的作用
直到現在(Go1.7),unsafe 包含以下資源:
-
三個函式:
- func Alignof(variable ArbitraryType)uintptr
- func Offsetof(selector ArbitraryType)uintptr
- func Sizeof(variable ArbitraryType)uintptr
-
和一種型別:
- 型別 Pointer * ArbitraryType
這裡,ArbitraryType 不是一個真正的型別,它只是一個佔位符。
與 Golang 中的大多數函式不同,上述三個函式的呼叫將始終在編譯時求值,而不是執行時。 這意味著它們的返回結果可以分配給常量。
(BTW,unsafe 包中的函式中非唯一呼叫將在編譯時求值。當傳遞給 len 和 cap 的引數是一個陣列值時,內建函式和 cap 函式的呼叫也可以在編譯時被求值。)
除了這三個函式和一個型別外,指標在 unsafe 包也為編譯器服務。
出於安全原因,Golang 不允許以下之間的直接轉換:
兩個不同指標型別的值,例如* int64 和* float64。
指標型別和 uintptr 的值。
但是藉助 unsafe.Pointer,我們可以打破 Go 型別和記憶體安全性,並使上面的轉換成為可能。這怎麼可能發生?讓我們閱讀 unsafe 包文件中列出的規則:
- 任何型別的指標值都可以轉換為 unsafe.Pointer。
- unsafe.Pointer 可以轉換為任何型別的指標值。
- uintptr 可以轉換為 unsafe.Pointer。
- unsafe.Pointer 可以轉換為 uintptr。
這些規則與 Go 規範一致:
底層型別uintptr的任何指標或值都可以轉換為指標型別,反之亦然。
規則表明 unsafe.Pointer 類似於 c 語言中的 void *。當然,void *在 C 語言裡是危險的!
在上述規則下,對於兩種不同型別 T1 和 T2,可以使* T1 值與 unsafe.Pointer 值一致,然後將 unsafe.Pointer 值轉換為* T2 值(或 uintptr 值)。通過這種方式可以繞過 Go 型別系統和記憶體安全性。當然,濫用這種方式是很危險的。
舉個例子:
package main
import (
"fmt"
"unsafe"
)
func main() {
var n int64 = 5
var pn = &n
var pf = (*float64)(unsafe.Pointer(pn))
// now, pn and pf are pointing at the same memory address
fmt.Println(*pf) // 2.5e-323
*pf = 3.14159
fmt.Println(n) // 4614256650576692846
}
在這個例子中的轉換可能是無意義的,但它是安全和合法的(為什麼它是安全的?)。
因此,資源在 unsafe 包中的作用是為 Go 編譯器服務,unsafe.Pointer 型別的作用是繞過 Go 型別系統和記憶體安全。
再來一點 unsafe.Pointer 和 uintptr
這裡有一些關於 unsafe.Pointer 和 uintptr 的事實:
- uintptr 是一個整數型別。
- 即使 uintptr 變數仍然有效,由 uintptr 變數表示的地址處的資料也可能被 GC 回收。
- unsafe.Pointer 是一個指標型別。
- 但是 unsafe.Pointer 值不能被取消引用。
- 如果 unsafe.Pointer 變數仍然有效,則由 unsafe.Pointer 變數表示的地址處的資料不會被 GC 回收。
- * unsafe.Pointer 是一個通用的指標型別,就像* int 等。
由於 uintptr 是一個整數型別,uintptr 值可以進行算術運算。 所以通過使用 uintptr 和 unsafe.Pointer,我們可以繞過限制,* T 值不能在 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 包,Ian,Go 團隊的核心成員之一,已經確認:
在 unsafe 包中的函式的簽名將不會在以後的 Go 版本中更改,
並且 unsafe.Pointer 型別將在以後的 Go 版本中始終存在。
所以,unsafe 包中的三個函式看起來不危險。 go team leader 甚至想把它們放在別的地方。 unsafe 包中這幾個函式唯一不安全的是它們呼叫結果可能在後來的版本中返回不同的值。 很難說這種不安全是一種危險。
看起來所有的 unsafe 包的危險都與使用 unsafe.Pointer 有關。 unsafe 包 docs 列出了一些使用 unsafe.Pointer 合法或非法的情況。 這裡只列出部分非法使用案例:
package main
import (
"fmt"
"unsafe"
)
// case A: conversions between unsafe.Pointer and uintptr
// don't appear in the same expression
func illegalUseA() {
fmt.Println("===================== illegalUseA")
pa := new([4]int)
// split the legal use
// p1 := unsafe.Pointer(uintptr(unsafe.Pointer(pa)) + unsafe.Sizeof(pa[0]))
// into two expressions (illegal use):
ptr := uintptr(unsafe.Pointer(pa))
p1 := unsafe.Pointer(ptr + unsafe.Sizeof(pa[0]))
// "go vet" will make a warning for the above line:
// possible misuse of unsafe.Pointer
// the unsafe package docs, https://golang.org/pkg/unsafe/#Pointer,
// thinks above splitting is illegal.
// but the current Go compiler and runtime (1.7.3) can't detect
// this illegal use.
// however, to make your program run well for later Go versions,
// it is best to comply with the unsafe package docs.
*(*int)(p1) = 123
fmt.Println("*(*int)(p1) :", *(*int)(p1)) //
}
// case B: pointers are pointing at unknown addresses
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]))
// now p is pointing at the end of the memory occupied by value a.
// up to now, although p is invalid, it is no problem.
// but it is illegal if we modify the value pointed by p
*(*int)(p) = 123
fmt.Println("*(*int)(p) :", *(*int)(p)) // 123 or not 123
// the current Go compiler/runtime (1.7.3) and "go vet"
// will not detect the illegal use here.
// however, the current Go runtime (1.7.3) will
// detect the illegal use and panic for the below code.
p = unsafe.Pointer(&a)
for i := 0; i <= len(a); i++ {
*(*int)(p) = 123 // Go runtime (1.7.3) never panic here in the tests
fmt.Println(i, ":", *(*int)(p))
// panic at the above line for the last iteration, when i==4.
// runtime error: invalid memory address or nil pointer dereference
p = unsafe.Pointer(uintptr(p) + unsafe.Sizeof(a[0]))
}
}
func main() {
illegalUseA()
illegalUseB()
}
編譯器很難檢測 Go 程式中非法的 unsafe.Pointer 使用。 執行 “go vet” 可以幫助找到一些潛在的錯誤,但不是所有的都能找到。 同樣是 Go 執行時,也不能檢測所有的非法使用。 非法 unsafe.Pointer 使用可能會使程式崩潰或表現得怪異(有時是正常的,有時是異常的)。 這就是為什麼使用不安全的包是危險的。
轉換*T1 為 *T2
對於將* T1 轉換為 unsafe.Pointer,然後轉換為* T2,unsafe 包 docs 說:
如果T2比T1大,並且兩者共享等效記憶體佈局,則該轉換允許將一種型別的資料重新解釋為另一型別的資料。
這種 “等效記憶體佈局” 的定義是有一些模糊的。 看起來 go 團隊故意如此。 這使得使用 unsafe 包更危險。
由於 Go 團隊不願意在這裡做出準確的定義,本文也不嘗試這樣做。 這裡,列出了已確認的合法用例的一小部分,
合法用例 1:在 [] T 和 [] MyT 之間轉換
在這個例子裡,我們用 int 作為 T:
type MyInt int
在 Golang 中,[] int 和 [] MyInt 是兩種不同的型別,它們的底層型別是自身。 因此,[] int 的值不能轉換為 [] MyInt,反之亦然。 但是在 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 包用於 Go 編譯器,而不是 Go 執行時。
- 使用 unsafe 作為程式包名稱只是讓你在使用此包是更加小心。
- 使用 unsafe.Pointer 並不總是一個壞主意,有時我們必須使用它。
- Golang 的型別系統是為了安全和效率而設計的。 但是在 Go 型別系統中,安全性比效率更重要。 通常 Go 是高效的,但有時安全真的會導致 Go 程式效率低下。 unsafe 包用於有經驗的程式設計師通過安全地繞過 Go 型別系統的安全性來消除這些低效。
- unsafe 包可能被濫用並且是危險的。
原文:http://www.tapirgames.com/blog/golang-unsafe
- 加微信實戰群請加微信(註明:實戰群):gocnio
相關文章
- 聊聊Go裡面的閉包Go
- Go unsafe包Go
- Go 裡面的 ^ 和 &^Go
- 詳解Unsafe類
- Go1.7裡面的BCE(跳躍檢測排除)(譯)Go
- 詳解 Go 團隊不建議用的 unsafe.PointerGo
- Go 語言閉包詳解Go
- Go unsafe 包探究Go
- [譯] 數字貨幣錢包詳解
- Go 語言 sync 包的應用詳解Go
- Go For Web:Golang http 包詳解(原始碼剖析)WebGolangHTTP原始碼
- Java雙刃劍之Unsafe類詳解Java
- Go sync包的WaitGroup【同步等待組】詳解GoAI
- Android Service生命週期 Service裡面的onStartCommand()方法詳解Android
- Go 之基礎速學 (十四) golang 裡 a 包引入 b 包 b 包引入 a 包問題的解決Golang
- 詳解Go regexp包中 ReplaceAllString 的用法Go
- Go初學者踩坑之go mod init與自定義包的使用詳解Go
- Go Errors 詳解GoError
- [譯] Go 1.13 errors 包錯誤處理GoError
- Golang中的unsafe標準庫包Golang
- 深入探索Go語言的unsafe包,揭秘它的黑科技和應用場景!Go
- 只編譯核心裡面的一個模組的方法(轉)編譯
- Go 之基礎速學 (十) golang 裡介面的實現Golang
- 最全面的 MySQL 索引詳解MySql索引
- 詳解 go 的切片Go
- Go Modules 詳解使用Go
- Go 通道(chanel)詳解Go
- 直接修改別人jar包裡面的class檔案 工具:jclasslibJAR
- 詳解華為錢包
- JavaScript閉包詳解JavaScript
- 閉包詳解一
- 詳解 JavaScript 閉包JavaScript
- Struts jar包詳解JAR
- javascript 閉包詳解JavaScript
- Go高階特性 16 | 非型別安全:unsafeGo型別
- [譯]Unsafe Swift – 指標與C互動Swift指標
- [譯]Unsafe Swift - 指標與C互動Swift指標
- Go標準庫:Go template用法詳解Go