包 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的倍數,float6四、uint64 或 64位指針的大小是8字節地址應該是8的倍數。對於更大倍數的地址對齊是不須要的,即便是complex128等較大的數據類型最多也只是8字節對齊。 數據結構
結構體的內存對齊
所以,聚合類型(結構體或數組)的值的長度至少是它的成員或元素的長度之和。而且因爲「內存間隙」的存在,可能還會更大一些。內存空位是由編譯器添加的未使用的內存地址,用來確保連續的成員或元素相對於結構體或數組的起始地址是對齊的。
語言規範不要求結構體成員聲明的順序對應內存中的佈局順序,因此在理論上,編譯器能夠自由安排,但實際上並無這麼作。若是結構體成員的類型是不一樣的,不一樣的排列順序可能使得結構體佔用的內存不一樣。好比下面的三個結構體擁有相同的成員,可是第一種寫法比其餘兩個定義須要佔更多內存:app
// 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
對齊算法太底層了(雖然貌似也沒有特別難),但確實不值得擔憂每一個結構體的內存佈局,不太高效排列可使數據結構更加緊湊。一個容易掌握的建議是,將相同類型的成員定義在一塊兒有可能更節約內存空間。ide
函數 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 ch1, ch2 := make(chan int), make(chan int) var ch1ro <-chan int = ch1 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 {ch1, ch1, true}, {ch1, ch2, false}, {ch1ro, ch1, 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 來寫。若是用了,那就要確保儘量地限制在小範圍內使用,這樣大多數的程序就不會受到這個影響。