go 源码-unsafe包

XCapsule · 2018-10-28 19:18:02 · 886 次点击 · 预计阅读时间 4 分钟 · 大约8小时之前 开始浏览    
这是一个创建于 2018-10-28 19:18:02 的文章,其中的信息可能已经有所发展或是发生改变。

go 源码阅读-unsafe包

unsafe包包含了go编程中绕过类型安全的操作. import unsafe的包大多是非直接可用并且不受go v1兼容性指南约束的

  1. type ArbitraryType int
    

    只是为了文档而声明的类型,实际上它并不是unsafe包的一部分,它代表任意go表达式的类型

  2. type Pointer *ArbitraryType
    

    Pointer代表了任意类型对应的指针,有四种特殊的操作只对Pointer类型生效,对其他类型无效 (1) 任意类型的指针可以转为Pointer (2) Pointer可以转为任意类型 (3)uint类型指针可以转为指针 (4)Pointer可以转为uint类型指针 因此Pointer允许程序无视type体系对任意类型内存进行读写。所以使用它的时候要格外小心

在下面的这些范式中是可以使用Pointer的:

不使用这些范式的代码目前是(或者未来会是)不可用的 即使是下面的范式也会有严重警告⚠️

运行go vet能帮助查找到Pointer不符合范式 的使用,但是go vet不保证代码可用 1.Conversion of a *T1 to Pointer to *T2. T1和T2需要有相同的内存布局,这种转换允许reinterpreting(重新解释)date从一种类型到另一种类型,一个math.Float64bits的实现如下:

  func Float64bits(f float64) uint64 {
        return *(*uint64)(unsafe.Pointer(&f))
    }

2. 将Pointer转为uintptr (but not back to Pointer).

uintptr 是一种无符号的整数类型,没有指定具体的bit大小但是足以容纳指针。 uintptr类型只有在底层编程是才需要,特别是Go语言和C语言函数库或操作系统接口相交互的地方。

通常uintptr不可以回转为Pointer uintptr是一个integer类型,不是引用。 将一个Pointer转为uintptr时会新建一个无pointer语义的integer值,这种uintptr通常用做print 即使uintptrl持有某个对象的地址,当这个对象被移走时gc并不会更新uintptr的值,而且uintptr也不能防止它被回收。 后面的范式列出了唯一从uintptr到Pointer可行的转化 3.通过计算在Pointer和uintptr之间相互转换 如果p指向一个集合中的对象,那么它可以转换为uintptr然后增加offset从而指向该对象后面的对象,而且能再转换回Pointer p= unsafe.Pointer(uintptr(p)+offset) 这种范式最常见的用法就是获取一个struct中的属性(field)或者数组中的元素(element)

示例1

e := unsafe.Pointer(uintptr(unsafe.Pointer(&s)) + unsafe.Offsetof(s.f)) 上面等价于: e :=unsafe.Pointer(&s.f)

示例2

e := unsafe.Pointer(uintptr(unsafe.Pointer(&x[0])) + i*unsafe.Sizeof(x[0])) 上面等价于 e := unsafe.Pointer(&x[i])

通过这种方式可以增加或减小Pointer所指向的offset。而且我们可以在pointer上使用位运算符(&^)。但在任何情况下,pointer最终都必须指向原来的那个集合中的对象

与在c语言中不同,pointer不可以跨过集合对象的最后一个元素 下面的示例是错误的

b := make([]byte,n)
end := unsafe.Pointer(uintptr(unsafe.Pointer(&b)) + uintptr(n))

另外需要注意的是两种转换必须在同一个只有他们两者算法关系的表达式中,下面的是错误示例

u := uintptr(p)
p := unsafe.Pointer(u + offset)

4. 当调用 syscall.Syscall时将Pointer转换为uintptr

在syscall包中的Syscall方法将直接传递uintptr参数给操作系统层,然后他们中的一部分将按照调用时的具体情况被重新解释(reinterpret)为Pointer.

如果一个pointer参数必须被转换为uintptr类型使用,那么我们必须要在调用的表达式中显式的进行转换: syscall.Syscall(SYS_READ,uintptr(fd), uintptr(unsafe.Pointer(p)),uintptr(n)) 编译器会将调用具体方法时的参数列表中pointer转换为uintptr的参数装载进引用对象集合,并且它将不会被销毁直到真个调用完成,即使在方法体中不再被用到。 为了让编译器识别这个范式,转换必须显示出现在参数列表,下面是错误示例

u := uintptr(unsafe.Pointer(p))
syscall.Syscall(SYS_READ,uintptr(fd),u,uintptr(n))

5.可以将reflect.Value.Pointer,reflect.Value.UnsafeAddr从uintptr转换为Pointer

reflect包中的Value的方法可以使Pointer或者UnsafeAddr返回uintptr类型来替代unsafe.Pointer类型,使得调用者不需要导入unsafe包就能让返回值是任意类型,这意味着返回结果是脆弱的必须在调用后立刻转为Pointer类型,调用和转换必须在同一个表达式内完成

p := (*int(unsafe.Pointer(reflect.ValueOf(new(int)).Pointer()))

6.将reflect.SliceHeader或者reflect.StringHeader 的Date属性与Pointer相互转换。

前面提到,reflect的数据类型SliceHeader和 StringHeader将数据声明为uintptr类型从而使的调用者不需要导入unsafe包就可以改变返回值的类型。而这意味着一个真正的slice或者string类型的值只能被重新编译成SliceHeader和StringHeader
var s string
hdr := (*reflect.StringHeader)(unsafe.Pointer(&s))
hdr.Data = uintptr(unsafe.Pointer(p))
hdr.Len = n

在这个用法中hdr.Data是一种获取slice类型底层头指针的替代方案,它并不是uintptr类型 总而言之,reflect.SliceHeader 和 reflect.StringHeader只能作为reflect.SliceHeader和 reflect.StringHeader时指向string和slice类型,他们的非指针形态是无效的。 在我们代码中不应该直接声明他们的结构体,下面是错误示例:

var hdr reflect.StringHeader
hdr.Data = uintptr(unsafe.Pointer(p))
hdr.Len = n
s := *(*string)(unsafe.Pointer(&hdr)) // p possibly already lost
  1. func Sizeof(x ArbitraryType) uintptr
    
    Sizeof方法返回对象x所占有的的内存大小(byte为单位),不包含x中引用类型所占有的内存大小
  2. func Offsetof(x ArbitraryType) uintptr
    

    该方法返回x所在结构体的起始内存地址到x所对应属性两者距离,单位为byte,参数x的格式应该是structValue.field

  3. func Alignof(x ArbitraryType) uintptr
    

    参考资料内存对齐 等价于reflect.TypeOf(x).Align()或reflect.TypeOf(s.f).FieldAlign() #go/unsafe


有疑问加站长微信联系(非本文作者))

入群交流(和以上内容无关):加入Go大咖交流群,或添加微信:liuxiaoyan-s 备注:入群;或加QQ群:692541889

886 次点击  
加入收藏 微博
暂无回复
添加一条新回复 (您需要 登录 后才能回复 没有账号 ?)
  • 请尽量让自己的回复能够对别人有帮助
  • 支持 Markdown 格式, **粗体**、~~删除线~~、`单行代码`
  • 支持 @ 本站用户;支持表情(输入 : 提示),见 Emoji cheat sheet
  • 图片支持拖拽、截图粘贴等方式上传