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

扫描二维码关注公众号,回复: 6270429 查看本文章

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

猜你喜欢

转载自blog.51cto.com/steed/2399853