zoukankan      html  css  js  c++  java
  • 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 的类型安全限制。

    参考

  • 相关阅读:
    std thread
    windows更新包发布地址
    How to set up logging level for Spark application in IntelliJ IDEA?
    spark 错误 How to set heap size in spark within the Eclipse environment?
    hadoop 常用命令
    windows 安装hadoop 3.2.1
    windows JAVA_HOME 路径有空格,执行软连接
    day01MyBatisPlus条件构造器(04)
    day01MyBatisPlus的CRUD 接口(03)
    day01MyBatisPlus入门(02)
  • 原文地址:https://www.cnblogs.com/ricklz/p/11980710.html
Copyright © 2011-2022 走看看