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的倍数,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包里,可是这几个函数是安全的,特别在须要优化内存空间时它们返回的结果对于理解原生的内存布局颇有帮助。

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 来写。若是用了,那就要确保尽量地限制在小范围内使用,这样大多数的程序就不会受到这个影响。

相关文章
相关标签/搜索