c++ string replace_Go语言的string优化误区及建议

本文深入探讨Go语言中字符串和切片的内存表示及传递效率,指出字符串虽不是传统意义上的引用类型,但在传递过程中不会复制底层数据。通过示例代码解析了字符串和切片在内存中的结构,揭示了它们的高效性和安全性,并讨论了不必要的类型转换可能导致的性能损失。此外,还揭示了字面量字符串的复用特性及其不可变性,提醒开发者注意使用字符串的优化策略。

这里解释一下 “引用类型” 有两个特征:

1、多个变量引用一块内存数据,不创建变量的副本;

2、修改任意变量的数据,其它变量可见;

显然字符串只满足了 “引用类型” 的第一个特点,不能满足第二个特点,顾不能说字符串是引用类型,感谢大佬指正。

d704744749e0ad7fb2b71903b16589bd.png

初学 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 的在内存中的状态:

ef9c8f1ebef53ff06c41799b8275d742.png

看一下 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
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值