go字符串无拷贝转换切片的一个问题

1.字符串无拷贝转换切片

提起 字符串无拷贝转换切片 这个话题,可能很多人会想到下面一段代码:

1
2
3
func string2Slice(s string) []byte {
return *(*[]byte)(unsafe.Pointer(&s))
}

这段代码利用了指针进行强转,并且在实现过程中不会出现数据拷贝,简单、强大!

2.同一切片多次获取容量结果不相同

来看一个测试用例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
package test

import (
"fmt"
"testing"
"unsafe"
)

func string2Slice(s string) []byte {
return *(*[]byte)(unsafe.Pointer(&s))
}

func TestString2Slice(t *testing.T) {
for i := 0; i < 3; i++ {
s := string2Slice("hello world")

fmt.Println("slice:", s)
fmt.Println("slice2string:", string(s))
fmt.Println("slice len:", len(s))
fmt.Println("slice cap:", cap(s))

fmt.Println("=================")
}
}

执行结果如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
=== RUN   TestString2Slice

slice: [104 101 108 108 111 32 119 111 114 108 100]
slice2string: hello world
slice len: 11
slice cap: 17187456
=================
slice: [104 101 108 108 111 32 119 111 114 108 100]
slice2string: hello world
slice len: 11
slice cap: 17810016
=================
slice: [104 101 108 108 111 32 119 111 114 108 100]
slice2string: hello world
slice len: 11
slice cap: 17810016
=================

--- PASS: TestString2Slice (0.00s)
PASS

不知道你发现没有:相同切片,内容完全一样,多次获取其容量 cap,得到的结果竟然不一样!

每个人每次执行结果也绝对不一样

是不是觉得自己发现了惊天动地的 bug,搓搓手准备给 go 官方提 issue 了。

3.字符串无拷贝转换切片的原理

在 go 源码 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
}

我们发现:字符串、切片的底层实现分别是两个不同的结构体,这两个结构体的前两个字段名称、类型竟然也相同。

Data 指向底层存储字符数组的指针
Len 底层字符数组的长度
同时思考:
① 安全的字符串 / 不安全的字符串分别指什么?
② C语言的字符串为什么不安全?

也正是因为结构体的前两个字段名称、类型相同,才使得我们能够投机取巧进行无拷贝的转换。

不知道你思考过强类型编程语言中变量必须指定参数类型的意义没?
在内存中地址空间是连续的,每一个存储数据的空间都有地址编号。在操作数据时经常传递数据的指针,通过指针来获取数据。
数据的起始地址 到 偏移该数据类型大小的结束地址 之间的空间,就是来存储该数据具体值的,通过操作这个区间来操作数据。

在C语言中经常会听到 野指针 这个说法,可以思考一下指是什么

由于这两种类型的底层前两个字段名称、类型相同,可以让字符串 假装(指针操作) 自己就是切片。

设定个场景:
未来技术非常发达,有钱人可以克隆很多个自己。
克隆的人虽拥有主体的外貌,但有一样是他们不具备的:记忆。所以,这些克隆人永远也不是主体 (除非记忆也可以移植)。

虽然可以通过指针让 字符串假装成切片,但是这个切片却不具有切片具有的一个特性:容量 cap

如果这个时候,你去问问这个 假装自己是切片的字符串 一个问题:你的容量是多少?
假装自己是切片的字符串 由于没有容量这个属性,只能乱猜一个数字。

测试用例中 字符串无拷贝转换切片 多次打印容量值,得到不一样结果的深层次原因就是:越界。访问了自己身后不属于自己的空间内的数值,这个空间内的数值随着程序的运行会发生不确定的变化,所以多次读取到的数值不相同,也可以认为是脏读

你可能会问,越界了为什么还不报错?
那是因为,字符串假装(指针操作)自己就是切片的这种设定把 系统也蒙蔽了,系统也不知道发生了越界

总结

对于 字符串无拷贝转换切片 这个话题,很多人可能只是知道怎么写,却不会考虑到存在的问题。
所以,笔者觉得很多时候,不仅要知其然、也要知其所以然。

在实际开发中,除非有特殊原因,并且知道存在的问题,否则就不要为了酷炫而滥用。不然,bug之路会漫长而无趣。