• Go中的unsafe


    unsafe

    最近关注了一个大佬的文章,文章写的非常好,大家可以去关注下。 微信公众号【码农桃花源】

    • 指针类型
      • 我们知道slice 和 map 包含指向底层数据的指针
    • 什么是 unsafe
    • 为什么会有unsafe
    • unsafe.Pointer && uintptr类型
      • unsafe.Pointer
      • uintptr
    • 总结

    指针类型

    首先我们先来了解下,GO里面的指针类型。

    为什么需要指针类型呢?参考文献 go101.org 里举了这样一个例子:

    func double(x int) {
        fmt.Println(x)
        x += x
        fmt.Println(x)
    }
    
    func main() {
        var a = 3
        double(a)
        fmt.Println(a)
    
    }

    double函数的作用是将3翻倍,但是实际上却没有做到,为什么呢? 因为go语言的函数操作都是值传递。double函数里面的x只是a的一个拷贝, 在函数内部对x的操作不能反馈到实参a。

    其实在实际的编写代码的过程中我们会使用一个指针进行解决。

    func double1(x *int) {
        *x += *x
        x = nil
    }
    
    func main() {
        var a = 3
        double1(&a)
    
        fmt.Println(a)
    
        p := &a
    
        double1(p)
    
        fmt.Println(*p)
    
    }

    其中有一个操作

    x=nil

    这个操作没有对我们的结果产生丝毫的影响。 其实也是很好理解的,因为我们知道go里面的函数中使用的都是值传递 x=nil,只是对&a的一个拷贝。

    我们知道slice 和 map 包含指向底层数据的指针

    我们对它们的操作是会影响到,原参数的值。

    func change(sl []int64) {
        sl[0] = 2
    }
    
    func main() {
    
        var sl = make([]int64, 2)
        change(sl)
        fmt.Println(sl)  // [2 0]
    }

    我们而已看到输出的值已经是[2 0]

    这时候我们可以使用一个copy来操作

    func change(sl []int64) {
        sl[0] = 2
    }
    
    func changeNo(sl []int64) {
        s2 := make([]int64, 2)
        copy(sl, s2)
        s2[0] = 2
    }
    
    func main() {
    
        var sl = make([]int64, 2)
        change(sl)
        fmt.Println(sl)
    
        changeNo(sl)
        fmt.Println(sl)
    }
    
    

    限制一:GO里面的指针不能进行数学的运算

    错误
    a := 5
    p := &a
    
    p++
    p = &a + 3

    限制二:不同类型的指针不能互相转换

    错误的
    func main(){
       a:=int(100)
       var f *float64
    
        f=&a
    }

    限制三:不同类型的指针不能使用==或!=比较。

    限制四:不能类型的指针变量不能相互赋值。

    什么是 unsafe

    前面讨论的指针是类型安全的,但它有很多的限制。go还有非类型安全的指针,就是unsafe 包提供的 unsafe.Pointer。在某些情况下,它会使代码更高效,当然,也更危险。

    unsafe 包用于 Go 编译器,在编译阶段使用。从名字就可以看出来,它是不安全的,官方并不建议使用。 它可以绕过 Go 语言的类型系统,直接操作内存。

    为什么会有unsafe

    Go 语言类型系统是为了安全和效率设计的,有时,安全会导致效率低下。有了 unsafe 包,高阶的程序员 就可以利用它绕过类型系统的低效。因此,它就有了存在的意义,阅读 Go 源码,会发现有大量使用 unsafe 包的例子。

    unsafe实现原理

    我们来看源码:

    type ArbitraryType int
    type Pointer *ArbitraryType

    从命名来看, Arbitrary 是任意的意思,也就是说 Pointer 可以指向任意类型,实际上它类似于 C 语言里的 void*。

    unsafe包还有其他三个函数:

    func Sizeof(x ArbitraryType) uintptr
    func Offsetof(x ArbitraryType) uintptr
    func Alignof(x ArbitraryType) uintptr

    size返回类型x所占的字节数,单不包含x所指向的内容的大小。例如,对于一个指针,函数返回 的大小为8字节(64位机器上),一个slice的大小则为slice header的大小。

    offsetof返回结构体在内存中的位置离结构体起始处的字节数,所传参数必须是结构体的成员。

    Alignof 返回 m,m 是指当类型进行内存对齐时,它分配到的内存地址能整除 m。

    上面三个函数的返回结果都是uintptr类型,这和 unsafe.Pointer 可以相互转换。三个函数都是在编译期间执 行,它们的结果可以直接赋给 const型变量。另外,因为三个函数执行的结果和操作系统、编译器相关,所以是不可 可移值的。

    综上,unsafe包提供了2点重要的能力:

    1、任何类型的指针和unsafe.Point可以相互转换。
    2、uintptr类型和unsafe.Point可以相互转换

    Aaron Swartz

    pointer不能直接进行数学运算,但可以把它转换成uintptr,对uintptr类型进行数学运算,在转换成pointer 类型。

    // uintptr 是一个整数类型,它足够大,可以存储
    type uintptr uintptr

    还有一点需要注意的是,uintptr并没有指针的含义,意思是uintptr所指向的对象会被gc给回收掉。 而unsafe.Pointer 有指针语义,可以保护它所指向的对象在“有用”的时候不会被垃圾回收。

    unsafe.Pointer && uintptr类型

    unsafe.Pointer

    这个类型比较重要,它是实现定位欲读写的内存的基础。官方文档对该类型有四个重要描述:

    1)任何类型的指针都可以被转化为Pointer
    (2)Pointer可以被转化为任何类型的指针
    (3)uintptr可以被转化为Pointer
    (4)Pointer可以被转化为uintptr

    大多数指针类型都会写成T,表示是“一个指向T类型变量的指针”。unsafe.Pointer是特别定义的一种指 针类型,它可以包含任何类型变量的地址。当然,我们不可以直接通过*p来获取unsafe.Pointer指针指 向的真是变量的值,因为我们并不知道变量的具体类型。和人普通指针一样,unsafe.Pointer指针是可以 比较的,并且支持和nil常量比较判断是否为空指针。


    一个普通的的T类型指针可以被转换成unsafe.Pointer类型指针,并且一个unsafe.Pointer类型指针也可以 被转换成普通类型的指针,被转换回普通的指针类型并不需要和原始的T类型相同。


    通过将float64类型指针转化为uint64类型指针,我们可以查看一个浮点数变量的位模式。

    func Float64bits(f float64) uint64 {
        fmt.Println(reflect.TypeOf(unsafe.Pointer(&f)))            //unsafe.Pointer
        fmt.Println(reflect.TypeOf((*uint64)(unsafe.Pointer(&f)))) //*uint64
        return *(*uint64)(unsafe.Pointer(&f))
    }
    
    func main() {
        fmt.Printf("%#016x
    ", Float64bits(1.0)) // "0x3ff0000000000000"
    }

    再看一个例子

    func main() {
        v1 := uint(12)
        v2 := int(12)
    
        fmt.Println(reflect.TypeOf(v1)) //uint
        fmt.Println(reflect.TypeOf(v2)) //int
    
        fmt.Println(reflect.TypeOf(&v1)) //*uint
        fmt.Println(reflect.TypeOf(&v2)) //*int
    
        p := &v1
    
        //两个变量的类型不同,不能赋值
        //p = &v2 //cannot use &v2 (type *int) as type *uint in assignment
    
        fmt.Println(reflect.TypeOf(p)) // *unit
    }
    当再次把 v2 的指针赋值给p时,会发生错误cannot use &v2 (type *int) as type *uint in assignment,也就是说类型不同,一个是*int,一个是*uint
    可以使用unsafe.Pointer进行转换,如下,
    func main() {
    
        v1 := uint(12)
        v2 := int(13)
    
        fmt.Println(reflect.TypeOf(v1)) //uint
        fmt.Println(reflect.TypeOf(v2)) //int
    
        fmt.Println(reflect.TypeOf(&v1)) //*uint
        fmt.Println(reflect.TypeOf(&v2)) //*int
    
        p := &v1
    
        p = (*uint)(unsafe.Pointer(&v2)) //使用unsafe.Pointer进行类型的转换
    
        fmt.Println(reflect.TypeOf(p)) // *unit
        fmt.Println(*p)                //13
    }
    uintptr
    // uintptr is an integer type that is large enough to hold the bit pattern of
    // any pointer.
    type uintptr uintptr

    uintptr是golang的内置类型,是能存储指针的整型,在64位平台上底层的数据类型是,

    typedef unsigned long long int  uint64;
    typedef uint64          uintptr;

    一个unsafe.Pointer指针也可以被转化成uintptr类型,然后保存到指针类型数值变量中(注:这只是和 当前指针相同的一个数字值,并不是一个指针),然后用以做必要的指针数值运算。(uintptr是一个无符号 的整型数,足以保存一个地址)这种转换虽然是可逆的,但是将uintptr转为unsafe.Pointer指针可能破坏 类型系统,因为并不是所有的数字都是有效的内存地址。

    许多将unsafe.Pointer指针转化成原生数字,然后再转换成unsafe.Pointer类型指针的操作也是不安全的 。比如下面的例子需要将变量x的地址加上b字段地址偏移量转化为*int16类型指针,然后通过该指针更新x.b:

    func main() {
    
        var x struct {
            a bool
            b int16
            c []int
        }
    
        /**
        unsafe.Offsetof 函数的参数必须是一个字段 x.f, 然后返回 f 字段相对于 x 起始地址的偏移量, 包括可能的空洞.
        */
    
        /**
        uintptr(unsafe.Pointer(&x)) + unsafe.Offsetof(x.b)
        指针的运算
        */
        // 和 pb := &x.b 等价
        pb := (*int16)(unsafe.Pointer(uintptr(unsafe.Pointer(&x)) + unsafe.Offsetof(x.b)))
        *pb = 42
        fmt.Println(x.b) // "42"
    }

    上面的写法尽管很繁琐,但在这里并不是一件坏事,因为这些功能应该很谨慎地使用。不要试图引入一个uintptr类型的临时变 量,因为它可能会破坏代码的安全性(注:这是真正可以体会unsafe包为何不安全的例子)。

    下面的这段代码是错误的

    // NOTE: subtly incorrect!
    tmp := uintptr(unsafe.Pointer(&x)) + unsafe.Offsetof(x.b)
    pb := (*int16)(unsafe.Pointer(tmp))
    *pb = 42

    产生错误的原因很微妙。有时候垃圾回收器会移动一些变量以降低内存碎片等问题。这类垃圾回收 器被称为移动GC。当一个变量被移动,所有的保存改变量旧地址的指针必须同时被更新为变量移动 后的地址。从垃圾收集器的角度看,一个unsafe.Pointer是一个指向变量的指针,因此当变量被 移动是对应的指针也必须被更新;但是uintptr类型的临时变量只是一个普通的数字,所以其值 不应该被改变。上面错误的代码因引入一个非指针的临时变量temp,导致垃圾收集器无法正确识别 这个是一个指向变量x的指针。当第二个语句执行是,变量X可能被转移,这时候临时变量tmp也就是 不再是现在&x.b地址。第三个指向之前无效地址空间的赋值将摧毁整个系统。

    总结

    unsafe包绕过了GO的类型系统,达到直接操作内存的目的,使用它是有一定风险的。但是在某些场景 下,使用unsafe包函数会提升代码的效率,GO源码中也是大量使用unsafe包。

    unsafe 包定义了 Pointer 和三个函数:

    func Sizeof(x ArbitraryType) uintptr
    func Offsetof(x ArbitraryType) uintptr
    func Alignof(x ArbitraryType) uintptr
    通过三个函数可以获取变量的大小,偏移,对齐等信息。

    uintptr可以和unsafe.Pointer进行相互的转换,uintptr可以进行数学运算。这样,通过 uintptr 和 unsafe.Pointer 的结合就解决了 Go 指针不能进行数学运算的限制。

    通过 unsafe 相关函数,可以获取结构体私有成员的地址,进而对其做进一步的读写操作,突破 Go 的类型安全限制。

    参考

  • 相关阅读:
    python3 与dict相关的魔法方法。使用于二叉搜索树的类中
    笔记:nestjs学习基础(1)
    ES6 --(10)class使用、class继承
    redux --(1)核心概念(stateaction educer)、三大原则、todolist with redux
    antV--G2 学习
    react源代码重点难点分析
    jQuery-ui源代码重点难点分析
    webuploader上传插件源代码重点难点分析
    破解jQuery Deferred()异步执行的神秘世界
    ueditor源代码重点难点分析
  • 原文地址:https://www.cnblogs.com/ricklz/p/11980710.html
Copyright © 2020-2023  润新知