• Golang指针与unsafe


    前言

    我们知道在golang中是存在指针这个概念的。对于指针很多人有点忌惮(可能是因为之前学习过C语言),因为它会导致很多异常的问题。但是很多人学习之后发现,golang中的指针很简单,没有C那么复杂。所以今天就详细来说说指针。

    因为博客园发布markdown格式存在问题,请移步http://www.linkinstar.wiki/2019/06/06/golang/source-code/point-unsafe/

    指针的使用

    a := 1
    p := &a
    fmt.Println(p)

    输出:0xc42001c070

    可以看到p就是一个指针,也可以说是a的地址。

    a := 1
    var p *int
    p = &a
    fmt.Println(p)

    或者也可以写成这样,因为我知道,在很多人看来,看到*号才是指针(手动滑稽)

    a := 1
    p := &a
    fmt.Println(*p)
    

    输出:1

    然后使用就直接通过*号就能去到对应的值了,就这么简单

    指针的限制

    Golang中指针之所以看起来很简单,是因为指针的功能不多。我们能看到的功能就是指针的指向一个地址而已,然后对于这个地址也只能进行传递,或者通过这个的地址去访问值。

    • 不能像C语言中一样p++,这样移动操作指针,因为其实这样操作确实不安全,很容易访问到奇怪的区域。
    • 不同类型的指针不能相互赋值、转换、比较。会出现cannot use &a (type *int) as type *float32 in assignment类似这样的错误

    如果只是单纯说go中指针的功能,上面就已经说完了,没必要写博客,但是其实go中还有一个包叫unsafe,有了它,指针就可以像C一样想干嘛干嘛了。

    unsafe

    三个类型

    其实指针有三种:
    一种是我们常见的*,用*去表示的指针;
    一种是unsafe.Pointer,Pointer是unsafe包下的一个类型;
    最后一种是uintptr,uintptr就厉害了,这玩意是可以进行运算的也就是可以++--;

    他们之间有这样的转换关系:
    * <=> unsafe.Pointer <=> uintptr

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

    从这样的关系你大概就可以猜到,我们使用的指针*p转换成Pointer然后转换uintptr进行运算之后再原路返回,理论上就能等同于进行了指针的运算。我们下面就来实践一下。

    unsafe操作slice

    func main() {
        s := make([]int, 10)
        s[1] = 2
        
        p := &s[0]
        fmt.Println(*p)
        
        up := uintptr(unsafe.Pointer(p))
        up += unsafe.Sizeof(int(0)) // 这里可不是up++哦
    
        p2 := (*int)(unsafe.Pointer(up))
        fmt.Println(*p2)
    }
    

    输出:
    0
    2

    从代码中我们可以看到,我们首先将指针指向切片的第一个位置,然后通过转换得到uintptr,操作uintptr + 上8位(注意这里不能++因为存放的是int,下一个元素位置相隔举例int个字节),最后转换回来得到指针,取值,就能取到切片的第二个位置了。

    unsafe操作struct

    当然有人肯定要说了,上面那个一顿操作猛如虎,不就是访问下一个位置嘛,我直接访问就行了。
    那下面就是厉害的来了,我们知道如果一个结构体里面定义的属性是私有的,那么这个属性是不能被外界访问到的。我们来看看下面这个操作:

    package basic
    
    type User struct {
        age  int
        name string
    }
    
    package main
    
    func main() {
        user := &basic.User{}
        fmt.Println(user)
        
        s := (*int)(unsafe.Pointer(user))
        *s = 10
    
        up := uintptr(unsafe.Pointer(user)) + unsafe.Sizeof(int(0))
    
        namep := (*string)(unsafe.Pointer(up))
        *namep = "xxx" 
    
        fmt.Println(user)
    }
    

    User是另外一个basic包中的结构体,其中的age是小写开头的,理论上来说,我们在外部没有办法修改age的值,但是经过上面这波操作之后,输出信息是:
    &{0 }
    &{10 xxx}
    也就是说成功操作到了结构体的私有属性。

    顺便提一句:创建结构体会被分配一块连续的内存,结构体的地址也代表了第一个成员的地址。

    下面我们来验证一下你是否已经学会了unsafe的操作,尝试不看一个小结,自己尝试一下:如何完成字符串到[]byte的转换,并且不开辟新的空间?





    字符串和byte数组转换inplace

    我们知道如果将字符串转换成[]byte非常方便

    s := "123"
    a := []byte(s)
    

    但是这样需要开辟额外的空间,那么如何实现原地的,不需要拷贝数据的转换呢?
    我们想一下,其实从底层的存储角度来说,string的存储规则和[]byte是一样的,也就是说,其实指针都是从某个位置开始到一段空间,中间一格一格。所以利用unsafe就可以做到。

    func main() {
        s := "123"
        a := []byte(s)
        
        print("s = " , &s, "
    ")
        print("a = " , &a, "
    ")
        
        a2 := (*[]byte)(unsafe.Pointer(&s))
        print("a2 = " , a2, "
    ")
    
        fmt.Println(*a2)
    }
    

    输出结果:
    s = 0xc420055f40
    a = 0xc420055f60
    a2 = 0xc420055f40
    [49 50 51]

    我们可以看到s和a的地址是不一样的,但是s和a2的地址是一样的,并且a2已经是一个[]byte了。






    嘿嘿嘿~你以为这样就结束了???

    存在的问题

    其实这个转换是存在问题的,问题就在新的[]byte的Cap没有正确的初始化。
    我们打印一下cap看一下
    fmt.Println("cap a =", cap(a))
    fmt.Println("cap a2 =", cap(*a2))
    结果是:
    cap a = 32
    cap a2 = 17418400
    这么大的容量是要上天呢???

    问题的原因

    在src/reflect/value.go下看

    type StringHeader struct {
        Data uintptr
        Len  int
    }
    
    type SliceHeader struct {
        Data uintptr
        Len  int
        Cap  int
    }
    

    看到其实string没有cap而[]byte有,所以导致问题出现,也容易理解,string是没有容量扩容这个说法的,所以新的[]byte没有赋值cap所以使用了默认值。

    问题解决

    stringHeader := (*reflect.StringHeader)(unsafe.Pointer(&s))
    
    bh := reflect.SliceHeader{
        Data: stringHeader.Data,
        Len:  stringHeader.Len,
        Cap:  stringHeader.Len,
    }
    
    return *(*[]byte)(unsafe.Pointer(&bh))
    

    通过重新设置SliceHeader就可以完成

    总结

    以上就是所有golang指针和unsafe的相关细节和使用。那么肯定有人会问这个有什么用了?

    • 1、没啥事你就别乱用了,别人都说unsafe不安全了。
    • 2、源码中很多大量的使用了指针移动的操作。

    如map中通过key获取value的时候:

    v := add(unsafe.Pointer(b), dataOffset+bucketCnt * uintptr(t.keysize)+i * uintptr(t.valuesize))

    通过桶的指针的偏移拿到值,具体我就不多介绍了。
    总之对于你看golang源码的时候会有很大帮助的。可能必要的时候你也能用到它,还是那句话,除非你知道它在干什么,否则不要用。

  • 相关阅读:
    数学—快速幂
    离散化
    造树计划——线段树
    Python map()函数
    python的discard和remove方法
    C++学习笔记之NULL vs nullptr
    哈姆雷特单词的排名
    读书笔记—《网络是怎么连接的》4.11
    滑动窗口—UVA11572 唯一的雪花 Unique Snowflakes
    javascript基础语法1.0
  • 原文地址:https://www.cnblogs.com/linkstar/p/10997672.html
Copyright © 2020-2023  润新知