您好,登錄后才能下訂單哦!
包 unsafe 廣泛使用在和操作系統交互的低級包中, 例如 runtime、os、syscall、net 等,但是普通程序是不需要使用它的。
函數 unsafe.Sizeof 報告傳遞給它的參數在內存中占用的字節(Byte)長度(1Byte=8bit,1個字節是8位),參數可以是任意類型的表達式,但它不會對表達式進行求值。對 Sizeof 的調用會返回一個 uintptr 類型的常量表達式,所以返回的結果可以作為數組類型的長度大小,或者用作計算其他的常量:
fmt.Println(unsafe.Sizeof(float64(0))) // "8"
fmt.Println(unsafe.Sizeof(uint8(0))) // "1"
函數 Sizeof 僅報告每個數據結構固定部分的內存占用的字節長度。以字符串為例,報告的只是字符串對應的指針的字節長度,而不是字符串內容的長度:
func main() {
var x string
x = "a"
fmt.Println(unsafe.Sizeof(x), len(x)) // "16 1"
var s []string
for i := 0; i < 10000; i++ {
s = append(s, "Hello")
}
x = strings.Join(s, ", ")
fmt.Println(unsafe.Sizeof(x), len(x)) // "16 69998"
}
無論字符串多長,unsafe.Sizeof 返回的大小總是一樣的。
Go 語言中非聚合類型通常有一個固定的大小,盡管在不同工具鏈下生成的實際大小可能會有所不同。考慮到可移植性,引用類型或包含引用類型的大小都是1個字(word),轉換為字節數,在32位系統上是4個字節,在64位系統上是8個字節。
類型 | 大小 |
---|---|
bool | 1個字節 |
intN, uintN, floatN, complexN | N/8個字節(例如float64是8個字節) |
int, uint, uintptr | 1個字 |
*T | 1個字 |
string | 2個字(data,len) |
[]T | 3個字(data,len,cap) |
map | 1個字 |
func | 1個字 |
chan | 1個字 |
interface | 2個字(type,value) |
在類型的值在內存中對齊的情況下,計算機的加載或者寫入會很高效。例如,int16的大小是2字節地址應該是偶數,rune類型的大小是4字節地址應該是4的倍數,float64、uint64 或 64位指針的大小是8字節地址應該是8的倍數。對于更大倍數的地址對齊是不需要的,即使是complex128等較大的數據類型最多也只是8字節對齊。
結構體的內存對齊
因此,聚合類型(結構體或數組)的值的長度至少是它的成員或元素的長度之和。并且由于“內存間隙”的存在,可能還會更大一些。內存空位是由編譯器添加的未使用的內存地址,用來確保連續的成員或元素相對于結構體或數組的起始地址是對齊的。
語言規范不要求結構體成員聲明的順序對應內存中的布局順序,所以在理論上,編譯器可以自由安排,但實際上并沒有這么做。如果結構體成員的類型是不同的,不同的排列順序可能使得結構體占用的內存不同。比如下面的三個結構體擁有相同的成員,但是第一種寫法比其他兩個定義需要占更多內存:
// 64-bit 32-bit
struct{ bool; float64; int16 } // 3 words 4words
struct{ float64; int16; bool } // 2 words 3words
struct{ bool; int16; float64 } // 2 words 3words
對齊算法太底層了(雖然貌似也沒有特別難),但確實不值得擔心每個結構體的內存布局,不過高效排列可以使數據結構更加緊湊。一個容易掌握的建議是,將相同類型的成員定義在一起有可能更節約內存空間。
函數 unsafe.Alignof 報告它參數類型所要求的對齊方式。和 Sizeof 一樣,它的參數可以是任意類型的表達式,并且返回一個常量。通常情況下布爾和數值類型對齊到它們的長度(最多8個字節), 其它的類型則按字(word)對齊。
函數 unsafe.Offsetof,參數必須是結構體 x 的一個字段 x.f。函數返回 f 相對于結構體 x 起始地址的偏移值,如果有內存空位,也會計算在內。
雖然這幾個函數在不安全的unsafe包里,但是這幾個函數是安全的,特別在需要優化內存空間時它們返回的結果對于理解原生的內存布局很有幫助。
很多指針類型都寫做 *T,意思是“一個指向T類型變量的指針”。unsafe.Pointer 類型是一種特殊類型的指針,它可以存儲任何變量的地址。這里不可以直接通過 *P 來獲取 unsafe.Pointer 指針指向的那個變量的值,因為并不知道變量的具體類型。和普通的指針一樣,unsafe.Pointer 類型的指針是可比較的并且可以和 nil 做比較,nil 是指針類型的零值。
一個普通的指針 *T 可以轉換為 unsafe.Pointer 類型的指針,并且一個 unsafe.Pointer 類型的指針也可以轉換回普通的指針,被轉換回普通指針的類型不需要和原來的 *T 類型相同。這里有一個簡單的應用場景,先將 *float64 類型指針轉化為 *uint64 然后再把內存中的值打印出來。這時候就是按照 uint64 類型來把值打印出來,這樣就可以看到浮點類型的變量在內存中的位模式:
func Float64bits(f float64) uint64 { return *(*uint64)(unsafe.Pointer(&f)) }
func main() {
fmt.Printf("%#016x\n", Float64bits(1.0)) // "0x3ff0000000000000"
}
很多 unsafe.Pointer 類型的值都是從普通指針到原始內存地址以及再從內存地址到普通指針進行轉換的中間值。下面的例子獲取變量 x 的地址,然后加上其成員 b 的地址偏移量,并將結果轉換為 *int16 指針類型,接著通過這個指針更新 x.b 的值:
var x struct {
a bool
b int16
c []int
}
func main() {
// 等價于 pb := &x.b ,但是這里是通過結構體的地址加上字段的偏移量計算后獲取到的
pb := (*int16)(unsafe.Pointer(uintptr(unsafe.Pointer(&x)) + unsafe.Offsetof(x.b)))
*pb = 42
fmt.Println(x.b)
}
這里首先獲取到結構體的地址,然后是成員的偏移量,相加后就是這個成員的內存地址。因為這里知道該地址指向的數據類型,所以直接用一個類型轉換就獲取到了成員 b 也就是 *int16 的指針地址。既然拿到指針類型了,就可以修改該指針指向的變量的值了。
這種方法不要隨意使用。
下面這段代碼看似和上面的一樣的,引入了一個臨時變量 tmp,讓把原來的一行拆成了兩行,這里的 tmp 是 uintptr 類型。這種引入 uintptr 類型的臨時變量,破壞原來整行代碼的用法是錯誤的:
func main() {
tmp := uintptr(unsafe.Pointer(&x)) + unsafe.Offsetof(x.b)
pb := (*int64)(unsafe.Pointer(tmp))
*pb = 42
fmt.Println(x.b)
}
原因很微妙。一些垃圾回收器會把內存中變量移來移去以減少內存碎片等問題。這種類型的垃圾回收器稱為移動GC。當一個變量在內存中移動后,所有保存該變量舊地址的指針必須同時被更新為變量移動后的新地址。從垃圾回收器的角度看,unsafe.Pointer 是一個變量指針,當變量移動后它的值也會被更新。而 uintptr 僅僅是一個數值,在垃圾回收的時候這個值是不會變的。
類似的錯誤用法還有像下面這樣:
pT := uintptr(unsafe.Pointer(new(T))) // 提示: 錯誤!
當垃圾回收器將會在語句執行結束后回收內存,在這之后,pT存儲的是變量的舊地址,而這個時候這個地址對應的已經不是那個變量了。
目前Go語言還沒有使用移動GC,所以上面的錯誤用法很多時候是可以正確運行的(運行了幾次,都沒有出錯)。但是還是存在其他移動變量的場景。
這樣的代碼能夠通過編譯并運行,編譯器不會報錯,不過會給一個提示性的錯誤信息:
possible misuse of unsafe.Pointer
所以還是可以在編譯的時候發現的。這里強烈建議遵守最小可用原則,不要使用任何包含變量地址的 uintptr 類型的變量,并減少不必要的 unsafe.Pointer 類型到 uintptr 類型的轉換。像本小節第一個例子里那樣,轉換為 uintptr 類型,最終在轉換回 unsafe.Pointer 類型的操作,都要在一條語句中完成。
當調用一個庫函數,并且返回的是 uintptr 類型地址時,比如下面的 reflect 包中的幾個函數。這些結果應該立刻轉換為 unsafe.Pointer 來確保它們在接下來代碼中能夠始終指向原來的變量:
package reflect
func (Value) Pointer() uintptr
func (Value) UnsafeAddr() uintptr
func (Value) InterfaceData() [2]uintptr // (index 1)
一般的函數盡量不要返回 uintptr 類型,可能也就反射這類底層編程的包有這種情況。
下一節的示例中會用到 reflect.UnsafeAddr 函數,示例中立刻在同一行代碼中就把返回值轉成了 nsafe.Pointer 類型。
這篇要解決反射章節第一個例子 dispaly 中沒有處理的循環引用的問題。這里需要使用 unsafe.Pointer 類型來保證地址可以始終指向最初的那個變量。
reflect 包中的 DeepEqual 函數用來報告兩個變量的值是否深度相等。DeepEqual 函數的基本類型使用內置的 == 操作符進行比較。對于組合類型,它逐層深入比較相應的元素。因為這個函數適合于任意的一對變量值的比較,甚至是那些無法通過 == 來比較的值,所以在一些測試代碼中廣泛地使用這個函數。下面的代碼就是用 DeepEqual 來比較兩個 []string 類型的值:
func TestSplit(t *testing.T) {
got := strings.Split("a:b:c", ":")
want := []string{"a", "b", "c"}
if !reflect.DeepEqual(got, want) { /* ... */ }
}
雖然 DeepEqual 很方便,可以支持任意的數據類型,但是它的不足是判斷過于武斷。例如,一個值為 nil 的 map 和一個值不為 nil 的空 map 會判斷為不相等,一個值為 nil 的切片和不為 nil 的空切片同樣也會判斷為不相等:
var c, d map[string]int = nil, make(map[string]int)
fmt.Println(reflect.DeepEqual(c, d)) // "false"
var a, b []string = nil, []string{}
fmt.Println(reflect.DeepEqual(a, b)) // "false"
所以,接下來要自己定義一個 Equal 函數。和 DeepEqual 類似,但是可以把一個值為 nil 的切片或 map 和一個值不為 nil 的空切片或 map 判斷為相等。對參數的基本遞歸檢查可以通過反射來實現。需要定義一個未導出的函數 equal 用來進行遞歸檢查,隱藏反射的細節。參數 seen 是為了檢查循環引用,并且因為要遞歸所以作為參數進行傳遞。對于每對要進行比較的值 x 和 y,equal 函數檢查兩者是否合法(IsValid)以及它們是否具有相同的類型(Type)。函數的結果通過 switch 的 case 語句返回,在 case 中比較兩個相同類型的值:
package equal
import (
"reflect"
"unsafe"
)
func equal(x, y reflect.Value, seen map[comparison]bool) bool {
if !x.IsValid() || !y.IsValid() {
return x.IsValid() == y.IsValid()
}
if x.Type() != y.Type() {
return false
}
// 循環檢查
if x.CanAddr() && y.CanAddr() {
xptr := unsafe.Pointer(x.UnsafeAddr()) // 獲取變量的地址的數值,用于比較是不是相同的引用
yptr := unsafe.Pointer(y.UnsafeAddr())
if xptr == yptr {
return true // 相同的引用
}
c := comparison{xptr, yptr, x.Type()}
if seen[c] {
return true // seen map 里已經存在的元素,表示已經比較過了
}
seen[c] = true
}
switch x.Kind() {
case reflect.Bool:
return x.Bool() == y.Bool()
case reflect.String:
return x.String() == y.String()
// 各種數值類型
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32,
reflect.Int64:
return x.Int() == y.Int()
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32,
reflect.Uint64, reflect.Uintptr:
return x.Uint() == y.Uint()
case reflect.Float32, reflect.Float64:
return x.Float() == y.Float()
case reflect.Complex64, reflect.Complex128:
return x.Complex() == y.Complex()
case reflect.Chan, reflect.UnsafePointer, reflect.Func:
return x.Pointer() == y.Pointer()
case reflect.Ptr, reflect.Interface:
return equal(x.Elem(), y.Elem(), seen)
case reflect.Array, reflect.Slice:
if x.Len() != y.Len() {
return false
}
for i := 0; i < x.Len(); i++ {
if !equal(x.Index(i), y.Index(i), seen) {
return false
}
}
return true
case reflect.Struct:
for i, n := 0, x.NumField(); i < n; i++ {
if !equal(x.Field(i), y.Field(i), seen) {
return false
}
}
return true
case reflect.Map:
if x.Len() != y.Len() {
return false
}
for _, k := range x.MapKeys() {
if !equal(x.MapIndex(k), y.MapIndex(k), seen) {
return false
}
}
return true
}
panic("unreachable")
}
// Equal 函數,檢查x 和 y是否深度相等
func Equal(x, y interface{}) bool {
seen := make(map[comparison]bool)
return equal(reflect.ValueOf(x), reflect.ValueOf(y), seen)
}
type comparison struct {
x, y unsafe.Pointer
t reflect.Type
}
在 API 中不暴露反射的細節,所以最后的可導出的 Equel 函數對參數顯式調用 reflect.ValueOf 函數。
為了確保算法終止設置可以對循環數據結果進行比較,它必須記錄哪兩對變量已經比較過了,并且避免再次進行比較。Equal 函數定義了一個叫做 comparison 的結構體集合,每個元素都包含兩個變量的地址(unsafe.Pointer 表示)以及比較的類型。比如切片的比較,x 和 x[0] 的地址是一樣的,這時候就要分開是兩個切片的比較 x 和 y,還是切片的兩個元素的比較 x[0] 和 y[0]。
當 equal 確認了兩個參數都是合法的并且類型也一樣,在執行 switch 語句進行比較之前,先檢查這兩個變量是否已經比較過了,如果已經比較過了,則直接返回結果并終止這次遞歸比較。
unsafe.Pointer
就是上一節講的問題,reflect.UnsafeAddr 返回的是一個 uintptr 類型(字母意思就是不安全的地址),這里需要直接轉成 unsafe.Pointer 類型來保證地址可以始終指向最初的那個變量。
下面輸出完整的測試代碼:
package equal
import (
"bytes"
"fmt"
"testing"
)
func TestEqual(t *testing.T) {
one, oneAgain, two := 1, 1, 2
type CyclePtr *CyclePtr
var cyclePtr1, cyclePtr2 CyclePtr
cyclePtr1 = &cyclePtr1
cyclePtr2 = &cyclePtr2
type CycleSlice []CycleSlice
var cycleSlice = make(CycleSlice, 1)
cycleSlice[0] = cycleSlice
ch2, ch3 := make(chan int), make(chan int)
var ch2ro <-chan int = ch2
type mystring string
var iface1, iface1Again, iface2 interface{} = &one, &oneAgain, &two
for _, test := range []struct {
x, y interface{}
want bool
}{
// basic types
{1, 1, true},
{1, 2, false}, // different values
{1, 1.0, false}, // different types
{"foo", "foo", true},
{"foo", "bar", false},
{mystring("foo"), "foo", false}, // different types
// slices
{[]string{"foo"}, []string{"foo"}, true},
{[]string{"foo"}, []string{"bar"}, false},
{[]string{}, []string(nil), true},
// slice cycles
{cycleSlice, cycleSlice, true},
// maps
{
map[string][]int{"foo": {1, 2, 3}},
map[string][]int{"foo": {1, 2, 3}},
true,
},
{
map[string][]int{"foo": {1, 2, 3}},
map[string][]int{"foo": {1, 2, 3, 4}},
false,
},
{
map[string][]int{},
map[string][]int(nil),
true,
},
// pointers
{&one, &one, true},
{&one, &two, false},
{&one, &oneAgain, true},
{new(bytes.Buffer), new(bytes.Buffer), true},
// pointer cycles
{cyclePtr1, cyclePtr1, true},
{cyclePtr2, cyclePtr2, true},
{cyclePtr1, cyclePtr2, true}, // they're deeply equal
// functions
{(func())(nil), (func())(nil), true},
{(func())(nil), func() {}, false},
{func() {}, func() {}, false},
// arrays
{[...]int{1, 2, 3}, [...]int{1, 2, 3}, true},
{[...]int{1, 2, 3}, [...]int{1, 2, 4}, false},
// channels
{ch2, ch2, true},
{ch2, ch3, false},
{ch2ro, ch2, false}, // NOTE: not equal
// interfaces
{&iface1, &iface1, true},
{&iface1, &iface2, false},
{&iface1Again, &iface1, true},
} {
if Equal(test.x, test.y) != test.want {
t.Errorf("Equal(%v, %v) = %t",
test.x, test.y, !test.want)
}
}
}
func Example_equal() {
fmt.Println(Equal([]int{1, 2, 3}, []int{1, 2, 3})) // "true"
fmt.Println(Equal([]string{"foo"}, []string{"bar"})) // "false"
fmt.Println(Equal([]string(nil), []string{})) // "true"
fmt.Println(Equal(map[string]int(nil), map[string]int{})) // "true"
// Output:
// true
// false
// true
// true
}
func Example_equalCycle() {
// Circular linked lists a -> b -> a and c -> c.
type link struct {
value string
tail *link
}
a, b, c := &link{value: "a"}, &link{value: "b"}, &link{value: "c"}
a.tail, b.tail, c.tail = b, a, c
fmt.Println(Equal(a, a)) // "true"
fmt.Println(Equal(b, b)) // "true"
fmt.Println(Equal(c, c)) // "true"
fmt.Println(Equal(a, b)) // "false"
fmt.Println(Equal(a, c)) // "false"
// Output:
// true
// true
// true
// false
// false
}
在最后的示例測試函數 Example_equalCycle 中,驗證了一個循環鏈表也能完成比較,而不會卡住:
type link struct {
value string
tail *link
}
a, b, c := &link{value: "a"}, &link{value: "b"}, &link{value: "c"}
a.tail, b.tail, c.tail = b, a, c
高級語言將程序、程序員和神秘的機器指令集隔離開來,并且也隔離了諸如變量在內存中的存儲位置,數據類型的大小,數據結構的內存布局,以及關于機器的其他實現細節。因為有這個隔離層的存在,我們可以編寫安全健壯的代碼并且不加改動就可以在任何操作系統上運行。
但 unsafe 包可以讓程序穿透這層隔離去使用一些關鍵的但通過其他方式無法使用到的特性,或者是為了實現更高的性能。付出的代價通常就是程序的可移植性和安全性,所以當你使用 unsafe 的時候就得自己承擔風險。大多數情況都不需要甚至永遠不需要使用 unsafe 包。當然,偶爾還是會遇到一些使用的場景,其中一些關鍵代碼最好還是通過 unsafe 來寫。如果用了,那就要確保盡可能地限制在小范圍內使用,這樣大多數的程序就不會受到這個影響。
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。