本文转载自微信公众号「crossoverJie」,作者crossoverJie。转载本文请联系跨界姐公众号。前言作为Go语言的新手,看到满满的“奇葩”代码我都会很好奇;比如我最近看到的几种方法;伪代码如下:funcFindA()([]*T,error){}funcFindB()([]T,error){}funcSaveA(data*[]T)error{}funcSaveB(data*[]*T)error{}相信大部分刚接触Go的初学者看到这样的代码都会一头雾水。其中,最容易混淆的是:[]*T*[]T*[]*T声明了切片,先不看后两种写法;单看[]*T就很容易理解:在这个slice中存放了T的所有内存地址,比存放T本身更节省空间。同时[]*T可以在方法内部修改T的值,但是[]T不能修改。funcTestSaveSlice(t*testing.T){a:=[]T{{Name:"1"},{Name:"2"}}for_,t2:=rangea{fmt.Println(t2)}_=SaveB(a)for_,t2:=rangea{fmt.Println(t2)}}funcSaveB(data[]T)error{t:=data[0]t.Name="1233"returnnil}typeTstruct{Namestring}如上exampleprint问题是{1}{2}{1}{2}只能通过修改方法来修改T的值funcSaveB(data[]*T)error{t:=data[0]t.Name="1233"returnil}:&{1}&{2}&{1233}&{2}示例让我们关注一下[]*T和*[]T之间的区别。这里我们写了两个追加函数:funcTestAppendA(t*testing.T){x:=[]int{1,2,3}appendA(x)fmt.Printf("main%v\n",x)}funcappendA(x[]int){x[0]=100fmt.Printf("appendA%v\n",x)}先看第一个,输出就是结果:appendA[100023]main[100023]表示在函数传递的过程,函数内部的修改会影响外部。再看一个例子:funcappendB(x[]int){x=append(x,4)fmt.Printf("appendA%v\n",x)}最终结果为:appendA[1234]main[123]对外界没有影响。而当我们再次调整时,会发现不一样了:funcTestAppendC(t*testing.T){x:=[]int{1,2,3}appendC(&x)fmt.Printf("main%v\n",x)}funcappendC(x*[]int){*x=append(*x,4)fmt.Printf("appendA%v\n",x)}最终结果:appendA&[1234]main[1234]可以发现,如果传递的是slice的指针,在使用append函数追加数据时会影响到外部。slice的原理在分析以上三种情况之前,我们先了解一下slice的数据结构。如果直接查看源码,会发现slice其实是一个结构体,只是不能被外部直接访问。源码地址runtime/slice.go有3个重要的属性:属性含义array底层存储数据的数组,是一个指针。lenslicelengthcapslicecapacitycap>=len提到切片就不得不想到数组。可以这样理解:切片是数组的抽象,数组是切片的底层实现。其实从名字slice不难看出,它是从数组中切出一部分;相对于数组的固定大小,切片可以根据实际使用情况进行扩展。所以切片也可以通过“切割”数组来获得:x1:=[6]int{0,1,2,3,4,5}x2:=x[1:4]fmt.Println(len(x2),cap(x2))其中x1的长度和容量都是6。那么x2的长度和容量就是3和5。x2的长度很容易理解。等于5的容量可以理解为当前分片可以使用的最大长度。因为切片x2是对数组x1的引用,所以底层数组排除了左边没有被引用的位置,也就是切片的最大容量,也就是5。同样底层数组以刚才的代码为例:funcTestAppendA(t*testing.T){x:=[]int{1,2,3}appendA(x)fmt.Printf("main%v\n",x)}funcappendA(x[]int){x[0]=100fmt.Printf("appendA%v\n",x)}函数调用过程中,main中的x和appendA函数中的x片指的是同一个数组。所以对于函数中的x[0]=100,在main函数中也可以得到。本质上,同一块内存数据被修改了。传值引起的误解上面的例子中,调用appendB中的append函数添加数据后,你会发现main函数并没有受到影响。这里我稍微调整了示例代码:funcTestAppendB(t*testing.T){//x:=[]int{1,2,3}x:=make([]int,3,5)x[0]=1x[1]=2x[2]=3appendB(x)fmt.Printf("main%vlen=%v,cap=%v\n",x,len(x),cap(x))}funcappendB(x[]int){x=append(x,444)fmt.Printf("appendB%vlen=%v,cap=%v\n",x,len(x),cap(x))}主要是修改切片初始化方法,使容量大于长度。具体原因后面会解释。输出结果如下:appendB[123444]len=4,cap=5main[123]len=3,cap=5main函数中的数据似乎没有受到影响;但是细心的朋友应该注意到,appendB函数中的x在append()之后length+1变成了4。在main函数中,length变回了3,这个细节上的差异就是append()“好像”没有生效的原因;至于为什么说“似乎”,再次调整代码:funcTestAppendB(t*testing.T){//x:=[]int{1,2,3}x:=make([]int,3,5)x[0]=1x[1]=2x[2]=3appendB(x)fmt.Printf("main%vlen=%v,cap=%v\n",x,len(x),cap(x))y:=x[0:cap(x)]fmt.Printf("y%vlen=%v,cap=%v\n",y,len(y),cap(y))}在在刚刚的基础上,在append之后,基于x再做一个slice;切片的范围是x引用的数组中的所有数据。看一下执行结果:appendB[123444]len=4,cap=5main[123]len=3,cap=5y[1234440]len=5,cap=5会神奇的发现y会打印出所有的data,appendB函数中追加的数据其实已经写入了数组,但是为什么没有获取到x本身呢?看图很容易理解:在appendB中,数据确实追加到原来的数组中,长度也增加了。但是因为是按值传递,所以即使把slice结构的长度修改为4,也只是修改了复制对象的长度,main中的长度还是3。由于底层数组是一样的,基于这个底层数组重新生成一个全长切片后,可以看到额外的数据。所以这里的本质原因是slice是一个结构体,传递的是一个值。无论在方法中如何修改长度,都不会影响原来的数据(这里指的是长度和容量这两个属性)。slice扩容还有一个需要注意的地方:刚才这里的例子稍微改动了一下,主要是设置了slice的容量超过了数组的长度;如果不做这个特殊设置会怎样?funcTestAppendB(t*testing.T){x:=[]int{1,2,3}//x:=make([]int,3,5)x[0]=1x[1]=2x[2]=3appendB(x)fmt.Printf("main%vlen=%v,cap=%v\n",x,len(x),cap(x))y:=x[0:cap(x)]fmt.Printf("y%vlen=%v,cap=%v\n",y,len(y),cap(y))}funcappendB(x[]int){x=append(x,444)fmt.Printf("appendB%vlen=%v,cap=%v\n",x,len(x),cap(x))}输出结果:appendB[123444]len=4,cap=6main[123]len=3,cap=3y[123]len=3,cap=3这时候你会发现main函数中的y切片数据没有变化。为什么?这是因为在初始化xslice的时候,xslice的长度和容量都是3。在appendB函数数据追加的时候,会发现没有位置。这时会进行扩容:将旧数据复制到新数组中。附加数据。将新的数据内存地址返回给appendB中的x。同理,由于传值,将appendB中的slice改成底层数组,对main函数中的slice没有影响,导致最终main函数up的数据没有变化。有没有什么办法可以让slice指针在展开的时候也能对外产生影响呢?funcTestAppendC(t*testing.T){x:=[]int{1,2,3}appendC(&x)fmt.Printf("main%vlen=%v,cap=%v\n",x,len(x),cap(x))}funcappendC(x*[]int){*x=append(*x,4)fmt.printf("appendC%v\n",x)}输出结果为:appendC&[1234]main[1234]len=4,cap=6那么外部slice会受到影响,原因其实很简单;刚才我也说了,因为slice本身就是一个结构体,所以我们在传递指针的时候,在函数内部通过指针修改数据,和通常自定义的struct是一样的原理。最后,appendC中x的指针指向展开后的结构体,因为传递了main函数中x的指针,所以main函数中的同一个x也指向了这个结构体。总结所以总结一下:Slice是对数组的抽象,slice本身也是一个结构体。传递参数时,函数的内引用和外引用是同一个数组,所以切片的修改会影响函数的外引用。如果发生扩容,情况就会发生变化,扩容会导致数据拷贝;因此,尽量估计切片的大小,避免数据拷贝。重新生成切片或数组时,由于共享同一个底层数组,数据会相互影响,需要注意。切片也可以传递指针,但是场景很少,会引起不必要的误会;建议按值传值,长度和容量不会占用太多内存。相信如果你用过切片,你会发现它和Java中的ArrayList非常相似。同样是基于数组实现,扩容后也会发生数据拷贝;看起来语言只是上层使用的选择,一些通用的底层实现大家也差不多。这时候我们再看题目中的[]*T*[]T*[]*T,就会发现这几个没有任何联系,但是看起来很像,很容易唬人。
