0%

golang中unsafe.Pointer的入门使用


概述

在做316. Remove Duplicate Letters遇到了[]byte转string的问题, 发现可以通过unsafe.Pointer来完成转换,由此产生了对golang中指针操作的兴趣。


[]byte转string

首先不通过指针直接转:

1
2
3
4
5
func main() {
var b []byte = []byte{'a', 'b', 'c'}
var str string = string(b)
fmt.Println(str)
}

abc

可以看到直接转是可以的,但是直接转整个[]byte会被复制一遍再给到string中,不是最高效率的做法。

通过指针来转:

注意如果直接转指针,go语言是不允许的:

1
2
var b []byte = []byte{'a', 'b', 'c'}
var str *string = (*string)(&b)

Cannot convert expression of type []byte to type string

要想进行指针转换,需要使用unsafe.Pointer来中继:

1
2
3
4
5
func main() {
var b []byte = []byte{'a', 'b', 'c'}
var str string = *(*string)(unsafe.Pointer(&b))
fmt.Println(str)
}

abc

上面的代码先将[]byte的指针转化为unsafe包中的ArbitraryType类型,然后就可以转化为任意别的类型的指针, 这里就将它转化为*string类型的指针。

因为string和[]byte的底层的c语言结构为:

1
2
3
4
5
6
7
8
9
10
11
12
struct String
{
byte* str;
intgo len;
};

struct Slice
{
byte* array;
uintgo len;
uintgo cap;
};

所以可以直接从切片[]byte转到string也毫无违和感。

这样string与切片[]byte共用一片内存,本来go的string是不可变的,这里竟然可以使得string内容可变:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
func main() {
var b []byte = make([]byte, 3, 10)
b[0] = 'a'
b[1] = 'b'
b[2] = 'c'

var str *string = (*string)(unsafe.Pointer(&b))
fmt.Println(*str)

//// 直接append
// b = append(b, 'd')

// 先修改len,再赋值
var bp *int = (*int)(unsafe.Pointer(uintptr(unsafe.Pointer(&b)) + unsafe.Sizeof(0)))
*bp = 4
b[3] = 'd'

fmt.Println(*str)
}

abc
abcd


string转[]byte

同理可以直接转,也可以通过指针转:

1
2
3
4
5
6
7
8
func main() {
var str string = "abc"
var b []byte = ([]byte)(str)
fmt.Println(b)

var buf []byte = *(*[]byte)(unsafe.Pointer(&str))
fmt.Println(buf)
}

[97 98 99]
[97 98 99]

注意到通过指针转化得到的[]byte切片是不能进行值得修的,因为str是不可修改的。

这里的buf的cap值是不确定的,因为string结构中只有两个属性,没有cap,有一种骚操作来转换:

1
2
3
4
5
6
7
func main() {
var str string = "abc"
xx := *(*[2]uintptr)(unsafe.Pointer(&str))
var buf []byte = *(*[]byte)(unsafe.Pointer(&[3]uintptr{xx[0], xx[1], xx[1]}))
fmt.Println(buf)
fmt.Printf("%p %d %d\n", buf, len(buf), cap(buf))
}

[97 98 99]
0x4c66e4 3 3

这里把string当作一个[2]uintptr,通过它新建一个[3]uintptr,这样就得到了一个len和cap都为3的[]byte切片。


struct赋值

例子如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
type V struct {
i int32
j int64
}

func main() {
var v *V = new(V)

var i *int32 = (*int32)(unsafe.Pointer(v))
*i = 1

var j *int64 = (*int64)(unsafe.Pointer(uintptr(unsafe.Pointer(v)) + unsafe.Offsetof(v.j)))
*j = 2

fmt.Println(*v)
}

{1 2}

需要注意到go的struct与c语言一样有对齐机制,对于结构体V,它明显是按照int64也就是8字节对齐, 所以上面的代码取j的位置换一种写法也是一样的:

1
var j *int64 = (*int64)(unsafe.Pointer(uintptr(unsafe.Pointer(v)) + uintptr(8)))

另外struct使用的是一块连续内存,可以看到下面的操作:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
type V struct {
i int32
j int64
}

func main() {
var v *V = new(V)

var i *int32 = (*int32)(unsafe.Pointer(v))
*i = 1

var k *int64 = (*int64)(unsafe.Pointer(uintptr(unsafe.Pointer(v)) + uintptr(4)))
*k = 3 << 32

fmt.Println(*v)
}

{1 3}

这里的 i,j,k 在内存中的位置是这样的:

image

所以*k = 3 << 32,这句话相当于把第9个字节变成了1100 0000,需要注意到这里是小端序(数据的低字节保存在内存的低地址)。 所以j的值就变成了3。