首页 >> 手游攻略

x5yin.com

老铁们,大家好,相信还有很多朋友对于x5yin.com和Go 语言string 也是引用类型的相关问题不太懂,没关系,今天就由我来为大家分享分享x5yin.com以及Go 语言string 也是引用类型的问题,文章篇幅可能偏长,希望可以帮助到大家,下面一起来看看吧!

初学Go语言的朋友总会在传[]byte和string之间有着很多纠结,实际上是没有了解string与slice的本质,而且读了一些程序源码,也发现很多与之相关的问题,下面类似的代码估计很多初学者都写过,也充分说明了作者当时内心的纠结:

packagemainnimport"bytes"nfuncxx(s[]byte)[]byte{n....nnreturnsn}nfuncmain(){ns:="xxx"nns=string(xx([]byte(s)))nns=string(bytes.Replace([]byte(s),[]byte("x"),[]byte(""),-1))n}n

虽然这样的代码并不是来自真实的项目,但是确实有人这样设计,单从设计上看就很糟糕了,这样设计的原因很多人说:“slice是引用类型,传递引用类型效率高呀”,主要原因不了解两者的本质,加上文档上说Go的引用类型有两种:slice和map,这个在面试中也是经常遇到的吧。

上面这个例子如果觉得有点基础和可爱,下面这个例子貌似并不那么容易说明其存在的问题了吧。

packagemainnfuncxx(s*string)*string{n....nreturnsn}nfuncmain(){ns:="xx"nns=*xx(&s)nnss:=[]*string{}nnss=append(ss,&s)n}n

指针效率高,我就用指针多好,可以减少内存分配呀,设计函数都接收指针变量,程序性能会有很大提升,在实际的项目中这种例子也不少见,我想通过这篇文档来帮助初学者走出误区,减少适得其反的优化技巧。

slice的定义

slice本身包含一个指向底层数组的指针,一个int类型的长度和一个int类型的容量,这就是slice的本质,[]byte本身也是一个slice,只是底层数组存储的元素是byte。下面这个图就是slice的在内存中的状态:

看一下reflect.SliceHeader如何定义slice在内存中的结构吧:

typeSliceHeaderstruct{nDatauintptrnLenintnCapintn}n

slice是引用类型是slice本身会包含一个地址,在传递slice时只需要分配SliceHeader就好了,而SliceHeader只包含了三个int类型,相当于传递一个slice就只需要拷贝SliceHeader,而不用拷贝整个底层数组,所以才说slice是引用类型的。

那么字符串呢,计算机中我们处理的大多数问题都和字符串有关,难道传递字符串真的需要那么高的成本,需要借助slice和指针来减少内存开销吗。

string的定义

reflect包里面也定义了一个StringHeader看一下吧:

typeStringHeaderstruct{nDatauintptrnLenintn}n

字符串只包含了两个int类型的数据,其中一个是指针,一个是字符串的长度,从定义上来看string也是引用类型。

借助unsafe来分析一下情况是不是这样吧:

packagemainnimport(n"reflect"n"unsafe"n"github.com/davecgh/go-spew/spew"n)nfuncxx(sstring){nsh:=*(*reflect.StringHeader)(unsafe.Pointer(&s))nspew.Dump(sh)n}nfuncmain(){ns:="xx"nsh:=*(*reflect.StringHeader)(unsafe.Pointer(&s))nspew.Dump(sh)nxx(s)nxx(s[:1])nxx(s[1:])n}n

上面这段代码的输出如下:

(reflect.StringHeader){nData:(uintptr)0x10f5ee0,nLen:(int)2n}n(reflect.StringHeader){nData:(uintptr)0x10f5ee0,nLen:(int)2n}n(reflect.StringHeader){nData:(uintptr)0x10f5ee0,nLen:(int)1n}n(reflect.StringHeader){nData:(uintptr)0x10f5ee1,nLen:(int)1n}n

可以发现前三个输出的指针都是同一个地址,第四个的地址发生了一个字节的偏移,分析来看传递字符串确实没有分配新的内存,同时和slice一样即使传递字符串的子串也不会分配新的内存空间,而是指向原字符串的中的一个位置。

这样说来把string转成[]byte还浪费的一个int的空间呢,需要分配更多的内存,真是适得其反呀,而且类型转换会发生内存拷贝,从string转为[]byte才是真的把string底层数据全部拷贝一遍呢,真是得不偿失呀。

string的两个小特性

字符串还有两个小特性,针对字面量(就是直接写在程序中的字符串),会创建在只读空间上,并且被复用,看一下下面的一个小例子:

packagemainnimport(n"reflect"n"unsafe"n"github.com/davecgh/go-spew/spew"n)nfuncmain(){na:="xx"nb:="xx"nc:="xxx"nspew.Dump(*(*reflect.StringHeader)(unsafe.Pointer(&a)))nspew.Dump(*(*reflect.StringHeader)(unsafe.Pointer(&b)))nspew.Dump(*(*reflect.StringHeader)(unsafe.Pointer(&c)))n}n

从输出可以了解到,相同的字面量会被复用,但是子串是不会复用空间的,这就是编译器给我们带来的福利了,可以减少字面量字符串占用的内存空间。

(reflect.StringHeader){nData:(uintptr)0x10f5ea0,nLen:(int)2n}n(reflect.StringHeader){nData:(uintptr)0x10f5ea0,nLen:(int)2n}n(reflect.StringHeader){nData:(uintptr)0x10f5f2e,nLen:(int)3n}n

另一个小特性大家都知道,就是字符串是不能修改的,如果我们不希望调用函数修改我们的数据,最好传递字符串,高效有安全。

不过有了unsafe这个黑魔法,字符串的这一个特性也就不那么可靠了。

packagemainnimport(n"fmt"n"reflect"n"strings"n"unsafe"n)nfuncmain(){na:=strings.Repeat("x",10)nfmt.Println(a)nstrHeader:=*(*reflect.StringHeader)(unsafe.Pointer(&a))nsliceHeader:=reflect.SliceHeader{nData:strHeader.Data,nLen:strHeader.Len,nCap:strHeader.Len,n}nb:=*(*[]byte)(unsafe.Pointer(&sliceHeader))nb[1]='a'nfmt.Println(a)n}n

从输出里面居然发现字符串被修改了,我们没有办法直接修改字符串,但是可以利用slice和string本身结构的特性,创建一个slice让它的指针指向string的指针位置,然后借助unsafe把这个SliceHeader转成[]byte来修改字符串,字符串确实被修改了。

xxxxxxxxxxnxaxxxxxxxxn

看了上面的例子是不是开始担心把字符串传给其它函数真的不会更改吗?感觉很不放心的样子,难道使用任何函数都要了解它的内部实现吗,其实这种情况极少发生,还记得之前说的那个字符串特性吗,字面量字符串会放到只读空间中,这个很重要,可以保证不是任何函数想修改我们的字符串就可以修改的。

packagemainnimport(n"reflect"n"unsafe"n)nfuncmain(){ndeferfunc(){nrecover()n}()na:="xx"nstrHeader:=*(*reflect.StringHeader)(unsafe.Pointer(&a))nsliceHeader:=reflect.SliceHeader{nData:strHeader.Data,nLen:strHeader.Len,nCap:strHeader.Len,n}nb:=*(*[]byte)(unsafe.Pointer(&sliceHeader))nb[1]='a'n}n

运行上面的代码发生了一个运行时不可修复的错误,就是这个特性其它函数不能确保输入字符串是否是字面量,也是不会恶意修改我们字符串的了。

unexpectedfaultaddress0x1095dd5nfatalerror:faultn[signalSIGBUS:buserrorcode=0x2addr=0x1095dd5pc=0x106c804]ngoroutine1[running]:nruntime.throw(0x1095fde,0x5)n/usr/local/go/src/runtime/panic.go:608+0x72fp=0xc000040700sp=0xc0000406d0pc=0x10248d2nruntime.sigpanic()n/usr/local/go/src/runtime/signal_unix.go:387+0x2d7fp=0xc000040750sp=0xc000040700pc=0x1037677nmain.main()n/Users/qiyin/project/go/src/github.com/yumimobi/test/a.go:22+0x84fp=0xc000040798sp=0xc000040750pc=0x106c804nruntime.main()n/usr/local/go/src/runtime/proc.go:201+0x207fp=0xc0000407e0sp=0xc000040798pc=0x1026247nruntime.goexit()n/usr/local/go/src/runtime/asm_amd64.s:1333+0x1fp=0xc0000407e8sp=0xc0000407e0pc=0x104da51n

关于exstrings.UnsafeToBytes我们转换不确定是否是字面量的字符串时就需要确保调用的函数不会修改我们的数据,这往常在调用bytes里面的方法十分有效。

传字符串和字符串指针的区别

之前分析了传递slice并没有string高效,何况转换数据类型本身就会发生数据拷贝。

那么在这篇文章的第二个例子,为什么说传递字符串指针也不好呢,要了解指针在底层就是一个int类型的数据,而我们字符串只是两个int而已,另外如果了解GC的话,GC只处理堆上的数据,传递指针字符串会导致数据逃逸到堆上,阅读标准库的代码会有很多注释说明避免逃逸到堆上,这样会极大的增加GC的开销,GC的成本可谓是很高的呀。

疑惑

这篇文章说“传递slice并没有string高效”,为什么还会有bytes包的存在呢,其中很多函数的功能和strings包的功能一致,只是把string换成了[]byte,既然传递[]byte没有string效率好,这个包存在的意义是什么呢。

我们想一下转换数据类型是会发生数据拷贝,这个成本可是大的多呀,如果我们数据本身就是[]byte类型,使用strings包就需要转换数据类型了。

另外我们对比两个函数来看下一下即使传递[]byte没有string效率好,但是标准库实现上却会导致两个函数有很大的性能差异的。

strings.Repeat函数:

funcRepeat(sstring,countint)string{n//Sincewecannotreturnanerroronoverflow,n//weshouldpaniciftherepeatwillgeneraten//anoverflow.n//SeeIssuegolang.org/issue/16237nifcount<0{npanic("strings:negativeRepeatcount")n}elseifcount>0&&len(s)*count/count!=len(s){npanic("strings:Repeatcountcausesoverflow")n}nb:=make([]byte,len(s)*count)nbp:=copy(b,s)nforbp<len(b){ncopy(b[bp:],b[:bp])nbp*=2n}nreturnstring(b)n}n

bytes.Repeat函数:

funcRepeat(b[]byte,countint)[]byte{n//Sincewecannotreturnanerroronoverflow,n//weshouldpaniciftherepeatwillgeneraten//anoverflow.n//SeeIssuegolang.org/issue/16237.nifcount<0{npanic("bytes:negativeRepeatcount")n}elseifcount>0&&len(b)*count/count!=len(b){npanic("bytes:Repeatcountcausesoverflow")n}nnb:=make([]byte,len(b)*count)nbp:=copy(nb,b)nforbp<len(nb){ncopy(nb[bp:],nb[:bp])nbp*=2n}nreturnnbn}n

上面两个函数的实现非常相似,除了类型不同strings包在处理完数据发生了一次类型转换,使用bytes只有一次内存分配,而strings是两次。

例如:

s:=exbytes.ToString(bytes.Repeat(exstrings.UnsafeToBytes("x"),10))n

不过这样写有点太麻烦了,实际上包里面正在修改strings里面一些类似函数的问题,所有的实现基本和标准库一致,只是把其中类型转换的部分用优化了一下,可以提升性能,也能提升开发效率。

函数:

funcUnsafeRepeat(sstring,countint)string{n//Sincewecannotreturnanerroronoverflow,n//weshouldpaniciftherepeatwillgeneraten//anoverflow.n//SeeIssuegolang.org/issue/16237nifcount<0{npanic("strings:negativeRepeatcount")n}elseifcount>0&&len(s)*count/count!=len(s){npanic("strings:Repeatcountcausesoverflow")n}nb:=make([]byte,len(s)*count)nbp:=copy(b,s)nforbp<len(b){ncopy(b[bp:],b[:bp])nbp*=2n}nreturnexbytes.ToString(b)n}n

如果用上面的函数只需要下面这样写就可以了:

s:=exstrings.UnsafeRepeat("x",10)n

总结

千万不要为了使用[]byte来优化string传递,类型转换成本很高,且slice本身也比string更大一些。程序中是使用string还是[]byte需要根据数据来源和处理数据的函数来决定,一定要减少类型转换。关于使用strings还是bytes包的问题,主要关注点是数据原始类型以及想获得的数据类型来选择。减少使用字符串指针来优化字符串,这会增加GC的开销。

END,本文到此结束,如果可以帮助到大家,还望关注本站哦!



本文由欣欣吧手游攻略栏目发布,感谢您对欣欣吧的认可,以及对我们原创作品以及文章的青睐,非常欢迎各位朋友分享到个人站长或者朋友圈,但转载请说明文章出处“x5yin.com

标签: