当前位置: 首页 > 科技观察

面试官:能谈谈String和[]byte的转换吗?

时间:2023-03-19 15:55:58 科技观察

本文转载自微信公众号“Golang梦工厂”,作者Golang梦工厂。转载本文请联系Golang梦工厂公众号。前言大家好,我是asong。为什么会有今天的文章?前天在群里看到一个千篇一律的Go语言面试,其中一个问题是“字符串转成字节数组会不会发生内存拷贝?”;这个问题很好。有意思,本质是问你string和[]byte的转换原理,考你的基本功。今天我们就来好好看看两者之间的转换方式。byte类型我们看下byte的官方定义://byteisanaliasforuint8andisequivalenttouint8inallways.//used,byconvention,todistinguishbytevaluesfrom8-bitunsigned//integervalues.typebyte=uint8我们可以看到byte是uint8的别名,用来区分byte值和8位无符号整数值。其实byte可以看作是ASCII码的一个字符。例子:varchbyte=65varchbyte='\x41'varchbyte='A'[]byte类型[]byte是byte类型的切片,切片的本质也是结构体,定义如下://src/runtime/slice.gotypeslicestruct{arrayunsafe.Pointerlenintcapint}这里简单介绍一下这些字段,array表示底层数组的指针,len表示slice的长度,cap表示容量。看一个简单的例子:funcmain(){sl:=make([]byte,0,2)sl=append(sl,'A')sl=append(sl,'B')fmt.Println(sl)}根据这个例子我们可以画个图:string类型先看string的官方定义://stringisthesetofallstringsof8-bitbytes,conventionallybutnot//necessarilyrepresentingUTF-8-encodedtext.Astringmaybeempty,but//notnil.Valuesofstringtypeareimmutable.typestringstringstring是一个8位字节的集合,通常但不一定代表UTF-8编码的文本。字符串可以为空,但不能为零。字符串的值不能更改。看一个简单的例子:funcmain(){str:="asong"fmt.Println(str)}string类型本质上是一个结构体,定义如下:typestringStructstruct{strunsafe.Pointerlenint}stringStruct和slice还是很相似的,strpointer指向一个数组的首地址,len代表数组的长度。为什么和slice这么像,底层指向一个数组,到底是个什么数组?我们看一下它实例化时调用的方法://go:nosplitfuncgostringnocopy(str*byte)string{ss:=stringStruct{str:unsafe.Pointer(str),len:findnull(str)}s:=*(*string)(unsafe.Pointer(&ss))returns}入参是byte类型的指针,从中可以看出底层string类型是一个byte类型的数组,所以我们可以画这样一张图:string和[]byte有什么区别?上面我们一起分析了字符串类型。其实它的底层本质是一个字节类型的数组。那么问题来了,为什么string类型是基于数组的另一种封装呢?这是因为字符串类型在Go语言中被设计成不可变的,不仅在Go语言中,在其他语言中也是如此。好处是:在并发场景下,我们可以在无锁的控制下多次使用同一个字符串,在保证高效共享的同时不用担心安全问题。string类型虽然不能改变,但是可以替换,因为stringStruct中的str指针可以改变,但是指针指向的内容是不能改变的。看一个例子:funcmain(){str:="song"fmt.Printf("%p\n",[]byte(str))str="asong"fmt.Printf("%p\n",[]byte(str))}//运行结果0xc00001a0900xc00001a098我们可以看到指针指向的位置发生了变化,也就是说每改变一次字符串,就需要重新分配一次内存,而之前分配的空间会被gc回收。String和[]byte的标准转换Go语言提供了string和[]byte的标准转换方式,我们来看一个例子:funcmain(){str:="asong"by:=[]byte(str)str1:=string(by)fmt.Println(str1)标准转换使用起来比较简单,你知道他们内部是如何实现转换的吗?我们来分析一下:String类型转换为[]byte类型我们执行上面的代码下面的命令gotoolcompile-N-l-S./string_to_byte/string.go,可以看到调用了runtime.stringtoslicebyte://runtime/string.gogo1.15.7consttmpStringBufSize=32typetmpBuf[tmpStringBufSize]bytefuncstringtoslicebyte(buf*tmpBuf,sstring)[]byte{varb[]byteifbuf!=nil&&len(s)<=len(buf){*buf=tmpBuf{}b=buf[:len(s)]}else{b=rawbyteslice(len(s))}copy(b,s)returnb}//rawbytesliceallocatesanewbyteslice.Thebytesliceisnotzeroed.funcrawbyteslice(sizeint)(b[]byte){cap:=roundupsize(uintptr(size))p:=mallocgc(cap,nil,false)ifcap!=uintptr(size){memclrNoHeapPointers(add(p,uintptr(size)),cap-uintptr(size))}*(*slice)(unsafe.Pointer(&b))=slice{p,size,int(cap)}return}这里有两种情况,是否需要重新分配内存取决于字符串的长度。也就是说,预先定义了一个长度为32的数组。如果字符串的长度超过了这个数组的长度,说明[]byte不够了,需要重新分配一块内存。这也算是一种优化,32是阈值,超过32才会进行内存分配。最后我们通过调用copy方法将字符串复制到[]byte中。具体来说,我们会在src/runtime/slice.go中实现slicestringcopy方法。此代码不会发布在这里。这段代码的核心思想是:将string从head底层数组复制到[]byte对应的底层数组中,将[]byte类型转换为string类型,将[]byte类型转换为string类型。调用的本质是runtime.slicebytetostring://下面无关的代码片段funcslicebytetostring(buf*tmpBuf,ptr*byte,nint)(strstring){ifn==0{return""}ifn==1{p:=unsafe.Pointer(&staticuint64s[*ptr])ifsys.BigEndian{p=add(p,7)}stringStructOf(&str).str=pstringStructOf(&str).len=1return}varpunsafe.Pointerifbuf!=nil&&n<=len(buf){p=unsafe.Pointer(buf)}else{p=mallocgc(uintptr(n),nil,false)}stringStructOf(&str).str=pstringStructOf(&str).len=nmemove(p,unsafe.Pointer(ptr),uintptr(n))return}我们可以看到这段代码会根据[]byte的长度来判断是否重新分配内存,最后通过memove将数组复制为字符串。string和[]byte强转换标准的转换方式都会造成内存拷贝,所以为了减少内存拷贝和内存申请,我们可以使用强转换将两者进行转换。标准库中有这两个方法的实现://runtime/string.gofuncslicebytetostringtmp(ptr*byte,nint)(strstring){stringStructOf(&str).str=unsafe.Pointer(ptr)stringStructOf(&str).len=nreturn}funcstringtoslicebytetmp(sstring)[]byte{str:=(*stringStruct)(unsafe.Pointer(&s))ret:=slice{array:unsafe.Pointer(str.str),len:str.len,cap:str.len}return*(*[]byte)(unsafe.Pointer(&ret))}通过这两个方法我们可以知道unsafe.Pointer主要用于指针替换。为什么这是可能的?因为string和slice的结构字段是类似的:typestringStructstruct{strunsafe.Pointerlenint}typeslicestruct{arrayunsafe.Pointerlenintcapint}唯一不同的是cap字段,array和str是一致的,len是一致的,所以他们的内存布局是对齐的,所以我们可以直接通过unsafe.Pointer进行指针替换。两种转换如何取舍,当然推荐使用标准的转换方式。毕竟标准的转换方式更安全!但是如果是在高性能场景下使用,可以考虑使用强转换的方式,但是要注意强转换使用起来并不安全。这是一个例子:数据,Len:str.Len,Cap:str.Len}return*(*[]byte)(unsafe.Pointer(&ret))}funcmain(){str:="hello"by:=stringtoslicebytetmp(str)by[0]='H'}Runresult:unexpectedfaultaddress0x109d65ffatalerror:fault[signalSIGBUS:buserrorcode=0x2addr=0x109d65fpc=0x107eabc]可以看到程序直接出现了严重错误,defer+recover也抓不到。是什么原因?前面我们提到,字符串类型是不可改变的,即底层数据是不可改变的。在这里,因为我们使用了强转换的方式,所以通过指向str的底层数组。现在对于这个数组,如果.Ring,这是大家最关心的性能测试。这个我没做过,觉得意义不大。从前面的分析可以看出,强转换方式的性能肯定优于标准转换方式。对于这两种方式的使用,大家还是根据实际场景来选择。脱离现场谈表演就是耍流氓!!!