深入Golang之unsafe

unsafe.Pointer在Golang中是用于各种类型转化的桥梁,Pointer代表了一个指向任意类型的指针。

uintptr是Golang的内置类型,是能存储指针的整型,uintptr的底层类型是int,它和unsafe.Pointer可相互转换。

Pointer与uintptr的区别在于:

  1. unsafe.Pointer只是一个指针的类型,但是不能像C中的指针那样作计算,而只能用于转化不同类型的指针;如果unsafe.Pointer变量仍然有效,则由unsafe.Pointer变量表示的地址处的数据不会被GC回收;
  2. uintptr是可以用于指针运算的,但是无法持有对象,GC并不把uintptr当做指针,所以uintptr类型的目标会被回收。

在Golang中出于安全的原因,不允许两个不同指针类型的值去直接转换;也不允许指针类型和uintptr的值去直接转换。但是借助unsafe.Pointer,我们

  1. 任何类型的指针值都可以转换为unsafe.Pointer;
  2. unsafe.Pointer可以转换为任何类型的指针值;
  3. uintptr可以转换为unsafe.Pointer;
  4. unsafe.Pointer可以转换为uintptr。

例子

通过unsafe.Pointer来转化类型

在此之前提示一下这里我们说的类型的转化,是转化前后变量为同一变量,而不是这样为两个变量:

      
      
1
2
3
4
5
6
      
      
func () {
var a int64 = 3
var b float64 = float64(a)
fmt.Println(&a)
fmt.Println(&b) // 0xc42000e250
}

如果我们要来做一个强制的转化的话,a = float64(a),Golang会报错:cannot use float64(a) (type float64) as type int64 in assignment。

使用unsafe.Pointer来将T1转化为T2,一个大致的语法为(T2)(unsafe.Pointer(&t1))

      
      
1
2
3
4
5
6
7
8
9
10
11
      
      
func () {
var n int64 = 3
var pn = &n // n的指针
var pf = (* float64)(unsafe.Pointer(pn)) // 通过Pointer来将n的类型转为float
fmt.Println(*pf) // 2.5e-323
*pf = 3.5
fmt.Println(n) // 4615063718147915776
fmt.Println(pf) // 0xc42007a050
fmt.Println(pn) // 0xc42007a050
}

这个例子虽然没有实际的意义,但是绕过了Golang类型系统和内存安全,将一个变量的类型作了转化。

通过uintptr来计算偏移量

我们知道在Golang中指针是不能用来计算的,但是借助uintptr我们可以作计算:

      
      
1
2
3
4
5
6
7
      
      
func () {
a := [ 4] int{ 0, 1, 2, 3}
p1 := unsafe.Pointer(&a[ 1]) // index为1的元素
p3 := unsafe.Pointer( uintptr(p1) + 2 * unsafe.Sizeof(a[ 0])) // 拿到index为3的指针
*(* int)(p3) = 4 // 重新赋值
fmt.Println(a) // a = [0 1 2 4]
}

同样的对于一个结构体我们也可以通过增减偏移量来定位不同的成员变量,其依赖的主要思想是:结构体的成员在内存中的分配是一段连续的内存,结构体中第一个成员的地址就是这个结构体的地址:

      
      
1
2
3
4
5
6
7
8
9
10
11
12
13
14
      
      
type Person struct {
name string
age int
}
func () {
a := Person{ "Jasper", 27}
pa := unsafe.Pointer(&a)
aname := (* string)(unsafe.Pointer( uintptr(pa) + unsafe.Offsetof(a.name))) // pname := (*string)(unsafe.Pointer(uintptr(pa))) 这样也是可以的
aage := (* int)(unsafe.Pointer( uintptr(pa) + unsafe.Offsetof(a.age)))
*aname = "Jasper2"
*aage = 28
fmt.Println(a) // {Jasper2 28}
}

应用

上面给的例子都是一些概念上的,没有太多的实际意义,那么在实际的应用中,我们有有哪些地方是可以或者必须借助Pointer与uintptr来实现的呢,下面我们来看一些例子。

string与[]byte相互转换

我们在写程序的时候会经常遇到string与[]byte相互转换的情况,这种转化其实代价很高,因为string与[]byte的内存空间不共享,所以每次转换都伴随着内存的分配与底层字节的拷贝。而我们使用unsafe就可以避开这些,从而提升性能。

      
      
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
      
      
func string2byte(s string) []byte {
sh := (*reflect.StringHeader)(unsafe.Pointer(&s))
bh := reflect.SliceHeader{
Data: sh.Data,
Len: sh.Len,
Cap: sh.Len,
}
return *(*[] byte)(unsafe.Pointer(&bh))
}
func byte2string(b []byte) string{
bh := (*reflect.SliceHeader)(unsafe.Pointer(&b))
sh := reflect.StringHeader{
Data: bh.Data,
Len: bh.Len,
}
return *(* string)(unsafe.Pointer(&sh))
}

这样的转化过程依赖于二者的数据结构:

      
      
1
2
3
4
5
6
7
8
9
10
      
      
struct string{
uint8 *str;
int len;
}
struct [] uint8{
uint8 *array;
int len;
int cap;
}

注意,这样虽然可以实现,但强烈推荐不要使用这种方法来转换类型,因为这样会导致修改转化过后的值会影响之前的变量。

修改私有成员变量

在Golang中对于不在同一个package里面的对象的私有变量(小写的)是不能直接修改的,但是使用unsafe可以做到:

      
      
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
      
      
package p
import (
"fmt"
)
type V struct {
i int32
j int64
}
func (this V) PrintI() {
fmt.Printf( "i=%dn", this.i)
}
func (this V) PrintJ() {
fmt.Printf( "j=%dn", this.j)
}

我们在mian里面实现来直接修改i,j的值:

      
      
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
      
      
package main
import (
"p"
"unsafe"
)
func () {
var v *p.V = new(p.V)
var i * int32 = (* int32)(unsafe.Pointer(v))
*i = int32( 1)
var j * int64 = (* int64)(unsafe.Pointer( uintptr(unsafe.Pointer(v)) +unsafe.Sizeof( int32( 0))))
*j = int64( 2)
v.PrintI()
v.PrintJ()
}

其实在上面用uintptr计算偏移量介绍的那样,这样可以达到修改私有变量的目的。虽然达到了目的,但是在开发中其实并不建议这么干。

在反射中使用

reflect包中Value类型的方法中名称为Pointer和UnsafeAddr的方法的返回值类型是uintptr而不是unsafe.Pointer,目的是为了使调用者可以将结果转为任意类型而不用导入unsafe包。然而,这意味着调用结果必须马上再调用完成后转为Pointer,并且是在同一个表达式中完成;如下:

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

总结

其实说了这么多,大部分使用在日常的开发中都不怎么会用到。所以文中有不止一处说了“不建议”,那是因为用的时候的确有很多需要注意的地方,但是如果你可以很准确地控制,那么使用”unsafe”则可能成为一把利剑。

原文:大专栏  深入Golang之unsafe


猜你喜欢

转载自www.cnblogs.com/petewell/p/11584834.html