在 go 中使用从 []byte 到 string 的不安全转换可能会产生什么后果?

Pap*_*ter 4 string pointers unsafe go

[]byte转换为的首选方式string是:

var b []byte
// fill b
s := string(b)
Run Code Online (Sandbox Code Playgroud)

在此代码中复制字节片,这在性能很重要的情况下可能会出现问题。

当性能至关重要时,可以考虑执行不安全转换:

var b []byte
// fill b
s :=  *(*string)(unsafe.Pointer(&b))
Run Code Online (Sandbox Code Playgroud)

我的问题是:使用不安全转换时会出现什么问题?我知道这string应该是不可变的,如果我们改变bs它也会改变。但仍然:那又怎样?难道这一切都可能发生吗?

icz*_*cza 5

修改语言规范保证不可变的内容是一种叛国行为。

由于规范保证strings 是不可变的,因此允许编译器生成缓存其值的代码,并基于此进行其他优化。您不能string以任何正常方式更改 s 的值,如果您采用肮脏的方式(如 package unsafe)仍然这样做,您将失去规范提供的所有保证,并且通过继续使用修改后的strings,您可能会随机地遇到“错误”和意想不到的事情。

例如,如果您使用 astring作为映射中的键,并且string在将其放入映射后更改了,则您可能无法使用原始值或修改后的值string(这是依赖于实现)。

为了演示这一点,请参见以下示例:

m := map[string]int{}
b := []byte("hi")
s := *(*string)(unsafe.Pointer(&b))
m[s] = 999

fmt.Println("Before:", m)

b[0] = 'b'
fmt.Println("After:", m)

fmt.Println("But it's there:", m[s], m["bi"])

for i := 0; i < 1000; i++ {
    m[strconv.Itoa(i)] = i
}
fmt.Println("Now it's GONE:", m[s], m["bi"])
for k, v := range m {
    if k == "bi" {
        fmt.Println("But still there, just in a different bucket: ", k, v)
    }
}
Run Code Online (Sandbox Code Playgroud)

输出(在Go Playground上尝试):

Before: map[hi:999]
After: map[bi:<nil>]
But it's there: 999 999
Now it's GONE: 0 0
But still there, just in a different bucket:  bi 999
Run Code Online (Sandbox Code Playgroud)

起初,我们只是看到一些奇怪的结果:simplePrintln()无法找到它的值。它看到了一些东西(找到了键),但值显示为nil甚至不是值类型的有效值( isint的零值)。int0

如果我们将地图变大(添加 1000 个元素),地图的内部数据结构就会被重组。此后,我们甚至无法通过使用适当的密钥显式请求来找到我们的值。当迭代我们找到它的所有键值对时,它仍然在映射中,但由于哈希码随着值的变化而变化string,很可能在与它所在的位置(或它应该在的位置)不同的存储桶中搜索它是)。

另请注意,使用 package 的代码unsafe可能会像您现在期望的那样工作,但是相同的代码在 Go 的未来(或旧)版本中可能会完全不同(意味着它可能会崩溃),因为“导入不安全的包可能是不可移植的,并且不受 Go 1 兼容性指南的保护”

此外,您可能会遇到意外错误,因为修改后的内容string可能以不同的方式使用。有人可能只是复制字符串标题,有人可能复制其内容。看这个例子:

b := []byte{'h', 'i'}
s := *(*string)(unsafe.Pointer(&b))

s2 := s                 // Copy string header
s3 := string([]byte(s)) // New string header but same content
fmt.Println(s, s2, s3)
b[0] = 'b'

fmt.Println(s == s2)
fmt.Println(s == s3)
Run Code Online (Sandbox Code Playgroud)

我们创建了 2 个新的局部变量s2s3使用ss2通过复制 的字符串标头进行初始化s,并s3使用新string值(新字符串标头)进行初始化,但内容相同。现在,如果您修改原始字符串s,您会期望在正确的程序中将新字符串与原始字符串进行比较,您将得到相同的结果,无论是 或truefalse基于是否缓存了值,但应该是相同的)。

但输出是(在Go Playground上尝试一下):

hi hi hi
true
false
Run Code Online (Sandbox Code Playgroud)