1. golang中的指针类型
指针有三个类型:
- 一种是我们常见的,用去表示的指针;
- 一种是unsafe.Pointer,Pointer是unsafe包下的一个类型;
- 最后一种是uintptr,uintptr 这玩意是可以进行运算的也就是可以++–;
他们之间有这样的转换关系:
*<=> unsafe.Pointer <=> uintptr
有一点要注意的是,uintptr 并没有指针的语义,意思就是 uintptr 所指向的对象会被 gc 无情地回收。而 unsafe.Pointer 有指针语义,可以保护它所指向的对象在“有用”的时候不会被垃圾回收。
从这样的关系你大概就可以猜到,我们使用的指针*p转换成Pointer然后转换uintptr进行运算之后再原路返回,理论上就能等同于进行了指针的运算。我们下面就来实践一下。
2. 具体操作
2.1. unsafe操作slice
1 2 3 4 5 6 7 8 9 10 11 12 13 |
func main() { s := make([]int, 10) s[1] = 2 p := &s[0] fmt.Println(*p) up := uintptr(unsafe.Pointer(p)) //这里有可能会被回收 所以最好写成 (*int)unsafe.Pointer(uintptr(unsafe.Pointer(p))+unsafe.Sizeof(int(0))) up += unsafe.Sizeof(int(0)) // 这里不是up++ p2 := (*int)(unsafe.Pointer(up)) fmt.Println(*p2) } |
输出:
1 2 |
0 2 |
从代码中我们可以看到,我们首先将指针指向切片的第一个位置,然后通过转换得到uintptr,操作uintptr + 上8位(注意这里不能++因为存放的是int,下一个元素位置相隔举例int个字节),最后转换回来得到指针,取值,就能取到切片的第二个位置了。
2.2. unsafe操作struct(可以访问私有属性)
我们知道如果一个结构体里面定义的属性是私有的,那么这个属性是不能被外界访问到的。我们来看看下面这个操作:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
package main import ( "fmt" "unsafe" ) type User struct { age int name string } func main() { user := &User{} fmt.Println(user) s := (*int)(unsafe.Pointer(user)) *s = 15 up := uintptr(unsafe.Pointer(user)) + unsafe.Sizeof(int(0)) namep := (*string)(unsafe.Pointer(up)) *namep = "XiaoMing" fmt.Println(user) } |
User是另外一个basic包中的结构体,其中的age是小写开头的,理论上来说,我们在外部没有办法修改age的值,但是经过上面这波操作之后,输出信息是:
1 2 |
&{0 } &{15 XiaoMing} |
也就是说成功操作到了结构体的私有属性。
顺便提一句:创建结构体会被分配一块连续的内存,结构体的地址也代表了第一个成员的地址。
2.3. 字符串和byte数组转换inplace
我们知道如果将字符串转换成[]byte非常方便
1 2 |
s := "123" a := []byte(s) |
但是这样需要开辟额外的空间,那么如何实现原地的,不需要拷贝数据的转换呢?
其实从底层的存储角度来说,string的存储规则和[]byte是一样的,也就是说,其实指针都是从某个位置开始到一段空间,中间一格一格。所以利用unsafe就可以做到。
1 2 3 4 5 6 7 8 9 10 11 12 |
func main() { s := "123" a := []byte(s) print("s = " , &s, "\n") print("a = " , &a, "\n") a2 := (*[]byte)(unsafe.Pointer(&s)) print("a2 = " , a2, "\n") fmt.Println(*a2) } |
输出结果:
1 2 3 4 |
s = 0xc420055f40 a = 0xc420055f60 a2 = 0xc420055f40 [49 50 51] |
我们可以看到s和a的地址是不一样的,但是s和a2的地址是一样的,并且a2已经是一个[]byte了。
- 存在的问题
其实这个转换是存在问题的,问题就在新的[]byte的Cap没有正确的初始化。
我们打印一下cap看一下
1 2 |
fmt.Println(“cap a =”, cap(a)) fmt.Println(“cap a2 =”, cap(*a2)) |
结果是:
1 2 |
cap a = 32 cap a2 = 17418400 |
- 问题的原因
在src/reflect/value.go下看
1 2 3 4 5 6 7 8 9 10 |
type StringHeader struct { Data uintptr Len int } type SliceHeader struct { Data uintptr Len int Cap int } |
看到其实string没有cap而[]byte有,所以导致问题出现,也容易理解,string是没有容量扩容这个说法的,所以新的[]byte没有赋值cap所以使用了默认值。
- 问题解决
1 2 3 4 5 6 7 8 9 |
stringHeader := (*reflect.StringHeader)(unsafe.Pointer(&s)) bh := reflect.SliceHeader{ Data: stringHeader.Data, Len: stringHeader.Len, Cap: stringHeader.Len, } return *(*[]byte)(unsafe.Pointer(&bh)) |
通过重新设置SliceHeader就可以完成
3. 总结
- 没啥事别乱用了,unsafe不安全。
- 源码中很多大量的使用了指针移动的操作,想看源码需要对这方面有所了解。
————————————————
原文链接:https://blog.csdn.net/qq_36219060/article/details/103809333
赞赏微信赞赏
支付宝赞赏