这里解释一下 “引用类型” 有两个特征:
1、多个变量引用一块内存数据,不创建变量的副本;
2、修改任意变量的数据,其它变量可见;
显然字符串只满足了 “引用类型” 的第一个特点,不能满足第二个特点,顾不能说字符串是引用类型,感谢大佬指正。

初学 Go 语言的朋友总会在传 []byte 和 string 之间有着很多纠结,实际上是没有了解 string 与 slice 的本质,而且读了一些程序源码,也发现很多与之相关的问题,下面类似的代码估计很多初学者都写过,也充分说明了作者当时内心的纠结:
package mainimport "bytes"func xx(s []byte) []byte{ .... return s}func main(){ s := "xxx" s = string(xx([]byte(s))) s = string(bytes.Replace([]byte(s), []byte("x"), []byte(""), -1))}虽然这样的代码并不是来自真实的项目,但是确实有人这样设计,单从设计上看就很糟糕了,这样设计的原因很多人说:“slice 是引用类型,传递引用类型效率高呀”,主要原因不了解两者的本质。
上面这个例子如果觉得有点基础和可爱,下面这个例子貌似并不那么容易说明其存在的问题了吧。
package mainfunc xx(s *string) *string{ .... return s}func main(){ s := "xx" s = *xx(&s) ss :=[]*string{} ss = append(ss, &s)}指针效率高,我就用指针多好,可以减少内存分配呀,设计函数都接收指针变量,程序性能会有很大提升,在实际的项目中这种例子也不少见,我想通过这篇文档来帮助初学者走出误区,减少适得其反的优化技巧。
slice 的定义
slice 本身包含一个指向底层数组的指针,一个 int 类型的长度和一个 int 类型的容量, 这就是 slice 的本质, []byte 本身也是一个 slice,只是底层数组存储的元素是 byte。下面这个图就是 slice 的在内存中的状态:

看一下 reflect.SliceHeader 如何定义 slice 在内存中的结构吧:
type SliceHeader struct {Data uintptrLen intCap int}slice 是引用类型是 slice 本身会包含一个地址,在传递 slice 时只需要分配 SliceHeader 就好了, 而 SliceHeader 只包含了三个 int 类型,相当于传递一个 slice 就只需要拷贝 SliceHeader,而不用拷贝整个底层数组,所以才说 slice 是引用类型的。
那么字符串呢,计算机中我们处理的大多数问题都和字符串有关,难道传递字符串真的需要那么高的成本,需要借助 slice 和指针来减少内存开销吗。
string 的定义
reflect 包里面也定义了一个 StringHeader 看一下吧:
type StringHeader struct {Data uintptrLen int}字符串只包含了两个 int 类型的数据,其中一个是指针,一个是字符串的长度,从 StringHeader 定义来看 string 并不会发生拷贝的,传递 string 只会拷贝 StringHeader而已。
借助 unsafe 来分析一下情况是不是这样吧:
package mainimport ("reflect""unsafe""github.com/davecgh/go-spew/spew")func xx(s string) {sh := *(*reflect.StringHeader)(unsafe.Pointer(&s))spew.Dump(sh)}func main() {s := "xx"sh := *(*reflect.StringHeader)(unsafe.Pointer(&s))spew.Dump(sh)xx(s)xx(s[:1])xx(s[1:])}上面这段代码的输出如下:
(reflect.StringHeader) { Data: (uintptr) 0x10f5ee0, Len: (int) 2}(reflect.StringHeader) { Data: (uintptr) 0x10f5ee0, Len: (int) 2}(reflect.StringHeader) { Data: (uintptr) 0x10f5ee0, Len: (int) 1}(reflect.StringHeader) { Data: (uintptr) 0x10f5ee1, Len: (int) 1}可以发现前三个输出的指针都是同一个地址,第四个的地址发生了一个字节的偏移,分析来看传递字符串确实没有分配新的内存,同时和 slice 一样即使传递字符串的子串也不会分配新的内存空间,而是指向原字符串的中的一个位置。
这样说来把 string 转成 []byte 还浪费的一个 int 的空间呢,需要分配更多的内存,真是适得其反呀,而且类型转换会发生内存拷贝,从 string 转为 []byte 才是真的把 string 底层数据全部拷贝一遍呢,真是得不偿失呀。
string 的两个小特性
字符串还有两个小特性,针对字面量(就是直接写在程序中的字符串),会创建在只读空间上,并且被复用,看一下下面的一个小例子:
package mainimport ("reflect""unsafe""github.com/davecgh/go-spew/spew")func main() {a := "xx"b := "xx"c := "xxx"spew.Dump(*(*reflect.StringHeader)(unsafe.Pointer(&a)))spew.Dump(*(*reflect.StringHeader)(unsafe.Pointer(&b)))spew.Dump(*(*reflect.StringHeader)(unsafe.Pointer(&c)))}从输出可以了解到,相同的字面量会被复用,但是子串是不会复用空间的,这就是编译器给我们带来的福利了,可以减少字面量字符串占用的内存空间。
(reflect.StringHeader) { Data: (uintptr) 0x10f5ea0, Len: (int) 2}(reflect.StringHeader) { Data: (uintptr) 0x10f5ea0, Len: (int) 2}(reflect.StringHeader) { Data: (uintptr) 0x10f5f2e, Len: (int) 3}另一个小特性大家都知道,就是字符串是不能修改的,如果我们不希望调用函数修改我们的数据,最好传递字符串,高效有安全。
不过有了 unsafe 这个黑魔法,字符串的这一个特性也就不那么可靠了。
package mainimport ("fmt""reflect""strings""unsafe")func main() {a := strings.Repeat("x
本文深入探讨Go语言中字符串和切片的内存表示及传递效率,指出字符串虽不是传统意义上的引用类型,但在传递过程中不会复制底层数据。通过示例代码解析了字符串和切片在内存中的结构,揭示了它们的高效性和安全性,并讨论了不必要的类型转换可能导致的性能损失。此外,还揭示了字面量字符串的复用特性及其不可变性,提醒开发者注意使用字符串的优化策略。
5424

被折叠的 条评论
为什么被折叠?



