修改语言规范保证为不可变的内容是背叛的行为。
由于规范保证string
是不可变的,编译器可以生成基于此的代码缓存其值并进行其他优化。您无法以任何正常方式更改string
的值,如果您采取不正当的方式(如使用unsafe
包)仍要这样做,则会失去规范提供的所有保证,继续使用修改后的string
,您可能会遇到“错误”和随机的意外情况。
例如,如果您将string
用作映射中的键,并在将其放入映射之后更改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)
}
}
输出(在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
起初,我们只看到了一些奇怪的结果:简单的
Println()
无法找到其值。它看到了某些东西(找到了键),但是值显示为
nil
,这甚至不是值类型
int
的有效值(
int
的零值为
0
)。
如果我们将地图扩大到很大(添加1000个元素),地图的内部数据结构将被重组。之后,即使我们使用适当的键显式地请求该值,我们仍然无法找到它。它仍然在地图中,因为我们迭代所有键值对时会找到它,但由于哈希代码随着
string
的值而改变,因此最有可能在与它所在的桶(或应该在的桶)不同的桶中搜索它。
此外,请注意,使用
unsafe
包的代码现在可能按预期工作,但相同的代码可能会在Go的未来(或旧版本)中完全不同(意味着它可能会崩溃),“导入不安全的软件包可能是不可移植的,并且不受Go 1兼容性指南的保护”。
你可能会遇到意想不到的错误,因为修改后的 string
可能会以不同的方式使用。有些人可能只复制字符串标头,而有些人可能会复制其内容。参考以下示例:
b := []byte{'h', 'i'}
s := *(*string)(unsafe.Pointer(&b))
s2 := s
s3 := string([]byte(s))
fmt.Println(s, s2, s3)
b[0] = 'b'
fmt.Println(s == s2)
fmt.Println(s == s3)
我们使用
s创建了2个新的本地变量
s2和
s3,
s2通过复制
s的字符串头进行初始化,而
s3则是用相同内容但具有新的
string值(新的字符串头)进行初始化。现在,如果您修改原始的
s,则在正确的程序中,将新字符串与原始字符串进行比较,无论是
true
还是
false
,您都会得到相同的结果(基于是否缓存值,但应该相同)。
但输出结果是(在
Go Playground上尝试):
hi hi hi
true
false