gopl 底層編程(unsafe包)

包 unsafe 廣泛使用在和操作系統交互的低級包中, 例如 runtime、os、syscall、net 等,但是普通程序是不需要使用它的。

unsafe.Sizeof、Alignof 和 Offsetof

函數 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包裏,但是這幾個函數是安全的,特別在需要優化內存空間時它們返回的結果對於理解原生的內存佈局很有幫助。

unsafe.Pointer

很多指針類型都寫做 *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 的指針地址。既然拿到指針類型了,就可以修改該指針指向的變量的值了。
這種方法不要隨意使用。

不要把 uintptr 類型賦值給臨時變量

下面這段代碼看似和上面的一樣的,引入了一個臨時變量 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 類型的操作,都要在一條語句中完成。

reflect 包返回的 uintptr

當調用一個庫函數,並且返回的是 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 的不足

雖然 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 來寫。如果用了,那就要確保儘可能地限制在小範圍內使用,這樣大多數的程序就不會受到這個影響。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章