前言大家好,我是asong。最近闲来无事看八股文。总结了几篇经常考的切片八卦文章,以问答的形式总结出来。我希望它们对你们这些正在面试的人有用。~01。数组和切片有什么区别?Go语言中数组的长度是固定的,不能动态扩展。大小将在编译时确定。声明方式如下:varbuffer[255]intbuffer:=[255]int{0}切片是对数组的一种抽象。因为数组的长度是不可变的,所以在某些场景下使用不是很方便。因此,Go语言提供了灵活而强大的内置类型切片(“动态数组”)。与数组相比,切片的长度不固定,可以追加元素。切片是一种数据结构。切片不是数组。切片描述了一个数组。切片结构如下:我们可以直接声明一个未指定大小的数组来定义一个切片,或者使用make()函数来创建一个切片。声明方式如下:varslice[]int//直接声明slice:=[]int{1,2,3,4,5}//literalmodeslice:=make([]int,5,10)//makecreatesslice:=array[1:5]//拦截下标的方式slice:=*new([]int)//新的slice可以使用append添加元素,cap时动态扩容是不够的。02.复制大切片比复制小切片贵吗?这个问题比较有意思。原文地址:大切片比小切片贵吗?这道题的本质是考查对slice本质的理解。Go语言只传值,所以我们以传切片为例:funcmain(){param1:=make([]int,100)param2:=make([]int,100000000)smallSlice(param1)largeSlice(param2)}funcsmallSlice(params[]int){//....}funclargeSlice(params[]int){//....}sliceparam2比param1大1,000,000个数量级,你需要更多吗复制值时的昂贵操作?什么?其实不是,因为切片的内部结构是这样的:,第二个字段是切片的长度,第三个字段是容量。将一个切片变量赋给另一个变量只会复制三个机器字。大切片和小切片的区别在于Len和Cap的值比小切片大。如果发生复制,则它本质上是一个副本。以上三个字段。03.切片的深浅拷贝复制。区别在于复制的新对象和原对象发生变化时是否会相互影响。本质区别在于复制的对象和原始对象是否会指向同一个地址。在Go语言中,复制切片的方式有3种:使用=运算符复制切片,这是一种浅拷贝,使用[:]下标复制切片,这也是使用内置的浅拷贝Go语言的copy()函数复制一个切片,这就是深拷贝。04.什么是零片、空片、零片?题中为什么有这么多种类的切片?因为Go语言有五种创建切片的方式,不同方式创建的切片是不一样的;零切片我们把内部数组元素全为零值或者底层数组内容全为nil的切片称为零切片。使用make创建且长度和容量不为0的切片是零切片:slice:=make([]int,5)//00000slice:=make([]*int,5)//nilnilnilnilnilnilslice的长度和nilslice的容量都为0,与nil比较的结果如果为真,可以直接创建slice或者new来创建nilslice:varslice[]intvarslice=*new([]int)空片的长度和容量也为0,但与nil的比较结果为false,因为所有空片的数据指针都指向同一个地址0xc42003bda0;使用字面量,make可以创建空切片:varslice=[]int{}varslice=make([]int,0)空切片指向的zerobase内存地址是一个神奇的地址,你可以看到它的定义Go语言源代码://所有0字节分配的基地址varzerobaseuintptr//分配对象内存funcmallocgc(sizeuintptr,typ*_type,needzerobool)unsafe.Pointer{...ifsize==0{returnunsafe.Pointer(&zerobase)}...}05。切片扩展策略这道题是高频考点,我们通过源码来分析一下切片的扩展策略。slice的扩展是调用growslice方法拦截一些重要的源码://runtime/slice.go//et:表示slice的一个元素;old:代表旧切片;cap:表示新分片需要的容量;功能增长ice(et*_type,oldslice,capint)slice{ifcapdoublecap{newcap=cap}else{//当原slice容量小于1024时,新slice的容量翻倍,如果old。cap<1024{newcap=doublecap}else{//原切片容量超过1024,新切片容量变为原切片的1.25倍//检查0maxAllocnewcap=int(capmem)caseet.大小==sys.PtrSize:lenmem=uintptr(old.len)*sys.PtrSizenewlenmem=uintptr(cap)*sys.PtrSizecapmem=roundupsize(uintptr(newcap)*sys.PtrSize)overflow=uintptr(newcap)>maxAlloc/sys.PtrSizenewcap=int(capmem/sys.PtrSize)caseisPowerOfTwo(et.size):varshiftuintptrifsys.PtrSize==8{//屏蔽移位以获得更好的代码生成。shift=uintptr(sys.Ctz64(uint64(et.size)))&63}else{shift=uintptr(sys.Ctz32(uint32(et.size)))&31}lenmem=uintptr(old.len)<(maxAlloc>>shift)newcap=int(capmem>>shift)default:lenmem=uintptr(old.len)*et.sizenewlenmem=uintptr(cap)*et.sizecapmem,overflow=math.MulUintptr(et.size,uintptr(newcap))capmem=roundupsize(capmem)newcap=int(capmem/et.size)}}slice的扩容策略可以通过源码来概括:slice在扩容时会进行内存对齐。内存对齐后与内存分配策略有关,新分片的容量应该大于等于旧分片容量的2倍或1.25倍。当原分片容量小于1024时,新分片容量变为原容量的2倍;当原分片容量超过1024时,新分片容量变为原容量的1.25倍。07.传参切片和切片指针有什么区别?我们都知道切片底层是一个结构体,它有三个元素:切片容量。当一个slice作为参数传递的时候,其实就是一个结构体的传递,因为Go语言的参数传递只是传值,传递一个slice会做一个原slice的浅拷贝,但是因为底层数据的地址没有变,slice在函数内的地址修改也会影响函数外的slice,例如:funcmodifySlice(s[]string){s[0]="song"s[1]="Golang"fmt.Println("outslice:",s)}funcmain(){s:=[]string{"asong","GolangDreamWorks"}modifySlice(s)fmt.Println("innerslice:",s)}//runningresultoutslice:[songGolang]innerslice:[songGolang]但是有一个特例,我们先看一个例子:funcappendSlice(s[]string){s=append(s,"划重点!!")fmt.Println("outslice:",s)}funcmain(){s:=[]string{"asong","GolangDreamWorks"}appendSlice(s)fmt.Println("innerslice:",s)}//runresultoutslice:[asongGolangDreamWorks,注意!!]innerslice:[asongGolangDreamWorks]因为对slice进行了扩容,函数外的slice指向了一个新的底层数组,所以函数内外不会互相影响,所以可以得出结论,当参数直接传递给切片,如果指向底层数组的指针被覆盖或修改(复制,重新分配,追加触发扩展),此时函数内部数据的修改将不再影响外部切片,也不会代表长度的len和容量上限将被更改Revise。参数传递切片指针很容易理解。如果要修改切片中元素的值,改变切片和底层数组的容量,应该通过指针传递。08.range遍历切片需要注意什么?Go语言提供了range关键字,用于在for循环中迭代数组(array)、切片(slices)、通道(channels)或集合(maps)的元素。有两种使用方法:fork,v:=range_{}fork:=range_{}第一种方法是遍历下标和对应的值,第二种方法是只遍历下标。使用range遍历切片时,会先拷贝一份,然后遍历拷贝数据:s:=[]int{1,2}fork,v:=ranges{}会被编译器认为是for_temp:=slen_temp:=len(for_temp)forindex_temp:=0;index_temp