在Golang语言开发过程中,我们经常会用到数组和切片数据结构,数组是固定长度的,而切片是可以扩张的数组,那么切片底层到底有什么不同?接下来我们来详细分析一下内部实现。一、内部数据结构 首先我们来看一下数据结构typeslicestruct{arrayunsafe。Pointer数据lenint长度capint容量} 这里的array其实是指向切片管理的内存块首地址,而len就是切片的实际使用大小,cap就是切片的容量。 我们可以通过下面的代码输出slice:packagemainimport(fmtunsafe)funcmain(){data:make(〔〕int,0,3)fmt。Println(unsafe。Sizeof(data),len(data),cap(data))Output:24,0,3通过指针方式拿到切片内部的值ptr:unsafe。Pointer(data)opt:(〔3〕int)(ptr)fmt。Println(opt〔0〕,opt〔1〕,opt〔2〕)Output:824634891936,0,3dataappend(data,4)fmt。Println(unsafe。Sizeof(data))Output:24shallowCopy:data〔:1〕ptr1:unsafe。Pointer(shallowCopy)opt1:(〔3〕int)(ptr1)fmt。Println(opt1〔0〕)Output:824634891936} 这么分析下来,我们可以了解如下内容:切片的数据结构大小是24,int占8字节,指针占8字节在不发生扩容的情况下,切片指向的首选地址不变常用的关于切片的方法有make,copy二、声明 使用一个切片通常有两种方法: 一种是varslice〔〕int,称为声明; 另一种是slicemake(〔〕int,len,cap)这种方法,称为分配内存。三、创建make 创建一个slice,实质上是在分配内存。funcmakeslice(ettype,len,capint)unsafe。Pointer{获取需要申请的内存大小mem,overflow:math。MulUintptr(et。size,uintptr(cap))ifoverflowmemmaxAlloclen0lencap{mem,overflow:math。MulUintptr(et。size,uintptr(len))ifoverflowmemmaxAlloclen0{panicmakeslicelen()超过内存限制超过最大分配量长度小于0}panicmakeslicecap()长度大于容量}分配内存runtimemalloc。goreturnmallocgc(mem,et,true)} 这里跟一下细节,math。MulUintptr是基于底层的指针计算乘法的,这样计算不会导致超出int大小,这个方法在后面会经常用到。funcMulUintptr(a,buintptr)(uintptr,bool){ifab1(4sys。PtrSize)a0{sys。PtrSize8returnab,falsea和b都小于32位,乘积肯定小于64位}overflow:bMaxUintptraMaxUintptruintptr(0),也就是64个1returnab,overflow} 同样,对于int64的长度,也有对应的方法funcmakeslice64(ettype,len64,cap64int64)unsafe。Pointer{len:int(len64)ifint64(len)!len64{panicmakeslicelen()}cap:int(cap64)ifint64(cap)!cap64{panicmakeslicecap()}returnmakeslice(et,len,cap)} 而实际分配内存的操作调用mallocgc这个分配内存的函数,这个函数以后再分析。四、扩容机制 我们了解切片和数组最大的不同就是切片能够自动扩容,接下来看看切片是如何扩容的funcgrowslice(ettype,oldslice,capint)slice{前置条件ifcapold。cap{panic(errorString(growslice:capoutofrange))}如果新切片的长度为0,返回空数据,长度为旧切片的长度ifet。size0{returnslice{unsafe。Pointer(zerobase),old。len,cap}}1、先记录原先的容量newcap:old。cap2、尝试2倍扩容doublecap:newcapnewcapifcapdoublecap{如果指定容量大于原有容量的2倍,则按新增容量申请newcapcap}else{3、如果指定容量小于原容量2倍,则按以下的计算方式为新容量ifold。len1024{如果原容量小于1024,新容量是原容量的2倍newcapdoublecap}else{原容量大于1024,按原容量的1。25倍递增for0newcapnewcapcap{newcapnewcap4}ifnewcap0{校验容量是否溢出newcapcap}}}varoverflowboolvarlenmem,newlenmem,capmemuintptr为加速计算(不用乘除法)对于2的幂,使用变位处理下面的处理使内存对齐switch{caseet。size1:lenmemuintptr(old。len)newlenmemuintptr(cap)capmemroundupsize(uintptr(newcap))overflowuintptr(newcap)maxAllocnewcapint(capmem)caseet。sizesys。PtrSize:lenmemuintptr(old。len)sys。PtrSizenewlenmemuintptr(cap)sys。PtrSizecapmemroundupsize(uintptr(newcap)sys。PtrSize)overflowuintptr(newcap)maxAllocsys。PtrSizenewcapint(capmemsys。PtrSize)caseisPowerOfTwo(et。size):2的幂varshiftuintptrifsys。PtrSize8{Maskshiftforbettercodegeneration。shiftuintptr(sys。Ctz64(uint64(et。size)))63}else{shiftuintptr(sys。Ctz32(uint32(et。size)))31}lenmemuintptr(old。len)shiftnewlenmemuintptr(cap)shiftcapmemroundupsize(uintptr(newcap)shift)overflowuintptr(newcap)(maxAllocshift)newcapint(capmemshift)default:lenmemuintptr(old。len)et。sizenewlenmemuintptr(cap)et。sizecapmem,overflowmath。MulUintptr(et。size,uintptr(newcap))capmemroundupsize(capmem)newcapint(capmemet。size)}判断是否会溢出,是否会超出可分配ifoverflowcapmemmaxAlloc{panic(errorString(growslice:capoutofrange))}内存分配varpunsafe。Pointerifet。ptrdata0{pmallocgc(capmem,nil,false)回收内存memclrNoHeapPointers(add(p,newlenmem),capmemnewlenmem)}else{Note:cantuserawmem(whichavoidszeroingofmemory),becausethenGCcanscanuninitializedmemory。pmallocgc(capmem,et,true)iflenmem0writeBarrier。enabled{gcOnlyshadethepointersinold。arraysinceweknowthedestinationsliceponlycontainsnilpointersbecauseithasbeenclearedduringalloc。bulkBarrierPreWriteSrcOnly(uintptr(p),uintptr(old。array),lenmem)}}数据拷贝memmove(p,old。array,lenmem)returnslice{p,old。len,newcap}} 这里可以看到,growslice是返回了一个新的slice,也就是说如果发生了扩容,会发生拷贝。 所以我们在使用过程中,如果预先知道容量,可以预先分配好容量再使用,能提高运行效率。五、深拷贝 copy这个函数在内部实现为slicecopyfuncslicecopy(to,fmslice,widthuintptr)int{前置条件iffm。len0to。len0{return0}n:fm。lenifto。lenn{nto。len}元素长度为0,直接返回ifwidth0{returnn}size:uintptr(n)width拷贝内存ifsize1{(byte)(to。array)(byte)(fm。array)knowntobeabytepointer}else{memmove(to。array,fm。array,size)}returnn} 还有关于字符串的拷贝funcslicestringcopy(to〔〕byte,fmstring)int{前置条件iflen(fm)0len(to)0{return0}n:len(fm)iflen(to)n{nlen(to)}memmove(unsafe。Pointer(to〔0〕),stringStructOf(fm)。str,uintptr(n))returnn} 这里显示了可以把string拷贝成〔〕byte,不能把〔〕byte拷贝成string。六、总结 1、切片的数据结构是array内存地址,len长度,cap容量 2、make的时候需要注意容量长度分配的内存大小要小于264,并且要小于可分配的内存量,同时长度不能大于容量。 3、内存增长的过程:如果指定的容量大于原先的2倍,就按照指定的容量如果原先的容量小于1024,按2倍容量扩张如果原先的容量大于1024,就按1。25倍扩张,会小于指定的容量容量大小确定完之后,会进行内存对齐 4、当发生内存扩容时,会发生拷贝数据的现象,影响程序运行的效率,如果可以,要先分配好指定的容量 5、关于拷贝,可以把string拷贝成〔〕byte,不能把〔〕byte拷贝成string。