做者:何海涛学而思网校技术团队;)
原文:https://mp.weixin.qq.com/s/rb...
众所周知,在golang中,slice(切片)是咱们最常使用到的一种数据结构,是一种可变长度的数组,本篇文章咱们主要结合源码来介绍一下slice的底层实现,以及在使用slice时的一些注意事项。golang
package main import ( "fmt" "unsafe" ) func main() { var a int var b int8 var c int16 var d int32 var e int64 slice := make([]int, 0) slice = append(slice, 1) fmt.Printf("int:%d\nint8:%d\nint16:%d\nint32:%d\nint64:%d\n", unsafe.Sizeof(a), unsafe.Sizeof(b), unsafe.Sizeof(c), unsafe.Sizeof(d), unsafe.Sizeof(e)) fmt.Printf("slice:%d", unsafe.Sizeof(slice)) }
该程序输出golang中经常使用数据类型占多少byte,输出结果是:数组
int:8 int8:1 int16:2 int32:4 int64:8 slice:24
咱们能够看到slice占24byte,为何会占24byte,这就跟slice底层定义的结构有关,咱们在golang的runtime/slice.go中能够找到slice的结构定义,以下:数据结构
type slice struct { array unsafe.Pointer//指向底层数组的指针 len int//切片的长度 cap int//切片的容量 }
咱们能够看到slice中定义了三个变量,一个是指向底层数字的指针array,另外两个是切片的长度len和切片的容量cap。app
简单了解了slice的底层结构后,咱们来看下slice的初始化,在golang中slice有多重初始化方式,在这里咱们就不一一介绍了,咱们主要关注slice在底层是如何初始化的,首先咱们来看一段代码:函数
package main import "fmt" func main() { slice := make([]int, 0) slice = append(slice, 1) fmt.Println(slice, len(slice), cap(slice)) }
很简单的一段代码,make一个slice,往slice中append一个一个1,打印slice内容,长度和容量,接下来咱们利用gotool提供的工具将以上代码反汇编:工具
go tool compile -S slice.go
获得汇编代码以下(截取部分):oop
0x0000 00000 (slice.go:8) TEXT "".main(SB), ABIInternal, $152-0 0x0000 00000 (slice.go:8) MOVQ (TLS), CX 0x0009 00009 (slice.go:8) LEAQ -24(SP), AX 0x000e 00014 (slice.go:8) CMPQ AX, 16(CX) 0x0012 00018 (slice.go:8) JLS 375 0x0018 00024 (slice.go:8) SUBQ $152, SP 0x001f 00031 (slice.go:8) MOVQ BP, 144(SP) 0x0027 00039 (slice.go:8) LEAQ 144(SP), BP 0x002f 00047 (slice.go:8) FUNCDATA $0, gclocals- f14a5bc6d08bc46424827f54d2e3f8ed(SB)//编译器产生,用于保存一些垃圾收集相关的信息 0x002f 00047 (slice.go:8) FUNCDATA $1, gclocals- 3e7bd269c75edba02eda3b9069a96409(SB) 0x002f 00047 (slice.go:8) FUNCDATA $2, gclocals- f6aec3988379d2bd21c69c093370a150(SB) 0x002f 00047 (slice.go:8) FUNCDATA $3, "".main.stkobj(SB) 0x002f 00047 (slice.go:9) PCDATA $0, $1 0x002f 00047 (slice.go:9) PCDATA $1, $0 0x002f 00047 (slice.go:9) LEAQ type.int(SB), AX 0x0036 00054 (slice.go:9) PCDATA $0, $0 0x0036 00054 (slice.go:9) MOVQ AX, (SP) 0x003a 00058 (slice.go:9) XORPS X0, X0 0x003d 00061 (slice.go:9) MOVUPS X0, 8(SP) 0x0042 00066 (slice.go:9) CALL runtime.makeslice(SB)//初始化slice 0x0047 00071 (slice.go:9) PCDATA $0, $1 0x0047 00071 (slice.go:9) MOVQ 24(SP), AX 0x004c 00076 (slice.go:10) PCDATA $0, $2 0x004c 00076 (slice.go:10) LEAQ type.int(SB), CX 0x0053 00083 (slice.go:10) PCDATA $0, $1 0x0053 00083 (slice.go:10) MOVQ CX, (SP) 0x0057 00087 (slice.go:10) PCDATA $0, $0 0x0057 00087 (slice.go:10) MOVQ AX, 8(SP) 0x005c 00092 (slice.go:10) XORPS X0, X0 0x005f 00095 (slice.go:10) MOVUPS X0, 16(SP) 0x0064 00100 (slice.go:10) MOVQ $1, 32(SP) 0x006d 00109 (slice.go:10) CALL runtime.growslice(SB)//append操做 0x0072 00114 (slice.go:10) PCDATA $0, $1 0x0072 00114 (slice.go:10) MOVQ 40(SP), AX 0x0077 00119 (slice.go:10) MOVQ 48(SP), CX 0x007c 00124 (slice.go:10) MOVQ 56(SP), DX 0x0081 00129 (slice.go:10) MOVQ DX, "".slice.cap+72(SP) 0x0086 00134 (slice.go:10) MOVQ $1, (AX) 0x008d 00141 (slice.go:11) PCDATA $0, $0 0x008d 00141 (slice.go:11) MOVQ AX, (SP) 0x0091 00145 (slice.go:10) LEAQ 1(CX), AX 0x0095 00149 (slice.go:10) MOVQ AX, "".slice.len+64(SP) 0x009a 00154 (slice.go:11) MOVQ AX, 8(SP) 0x009f 00159 (slice.go:11) MOVQ DX, 16(SP) 0x00a4 00164 (slice.go:11) CALL runtime.convTslice(SB)//类型转换 0x00a9 00169 (slice.go:11) PCDATA $0, $1 0x00a9 00169 (slice.go:11) MOVQ 24(SP), AX 0x00ae 00174 (slice.go:11) PCDATA $0, $0 0x00ae 00174 (slice.go:11) PCDATA $1, $1 0x00ae 00174 (slice.go:11) MOVQ AX, ""..autotmp_33+88(SP) 0x00b3 00179 (slice.go:11) MOVQ "".slice.len+64(SP), CX 0x00b8 00184 (slice.go:11) MOVQ CX, (SP) 0x00bc 00188 (slice.go:11) CALL runtime.convT64(SB) 0x00c1 00193 (slice.go:11) PCDATA $0, $1 0x00c1 00193 (slice.go:11) MOVQ 8(SP), AX 0x00c6 00198 (slice.go:11) PCDATA $0, $0 0x00c6 00198 (slice.go:11) PCDATA $1, $2 0x00c6 00198 (slice.go:11) MOVQ AX, ""..autotmp_34+80(SP) 0x00cb 00203 (slice.go:11) MOVQ "".slice.cap+72(SP), CX 0x00d0 00208 (slice.go:11) MOVQ CX, (SP) 0x00d4 00212 (slice.go:11) CALL runtime.convT64(SB) 0x00d9 00217 (slice.go:11) PCDATA $0, $1 0x00d9 00217 (slice.go:11) MOVQ 8(SP), AX 0x00de 00222 (slice.go:11) PCDATA $1, $3 0x00de 00222 (slice.go:11) XORPS X0, X0
你们可能看到这里有点蒙,这是在干啥,其实咱们只须要关注一些关键的信息就行了,主要是这几行:ui
0x0042 00066 (slice.go:9) CALL runtime.makeslice(SB)//初始化slice 0x006d 00109 (slice.go:10) CALL runtime.growslice(SB)//append操做 0x00a4 00164 (slice.go:11) CALL runtime.convTslice(SB)//类型转换 0x00bc 00188 (slice.go:11) CALL runtime.convT64(SB) 0x00d4 00212 (slice.go:11) CALL runtime.convT64(SB)
咱们能观察出,底层是调用runtime中的makeslice方法来建立slice的,咱们来看一下makeslice函数到底作了什么。this
func makeslice(et *_type, len, cap int) unsafe.Pointer { mem, overflow := math.MulUintptr(et.size, uintptr(cap)) if overflow || mem > maxAlloc || len < 0 || len > cap { // NOTE: Produce a 'len out of range' error instead of a // 'cap out of range' error when someone does make([]T, bignumber). // 'cap out of range' is true too, but since the cap is only being // supplied implicitly, saying len is clearer. // See golang.org/issue/4085. mem, overflow := math.MulUintptr(et.size, uintptr(len)) if overflow || mem > maxAlloc || len < 0 { panicmakeslicelen() } panicmakeslicecap() } // Allocate an object of size bytes. // Small objects are allocated from the per-P cache's free lists. // Large objects (> 32 kB) are allocated straight from the heap. return mallocgc(mem, et, true) } func panicmakeslicelen() { panic(errorString("makeslice: len out of range")) } func panicmakeslicecap() { panic(errorString("makeslice: cap out of range")) }
MulUintptr函数源码:spa
package math import "runtime/internal/sys" const MaxUintptr = ^uintptr(0) // MulUintptr returns a * b and whether the multiplication overflowed. // On supported platforms this is an intrinsic lowered by the compiler. func MulUintptr(a, b uintptr) (uintptr, bool) { if a|b < 1<<(4*sys.PtrSize) || a == 0 {//a|b < 1<<(4*8) return a * b, false } overflow := b > MaxUintptr/a return a * b, overflow }
简单来讲,makeslice函数的工做主要就是计算slice所需内存大小,而后调用mallocgc进行内存的分配。计算slice所需内存又是经过MulUintptr来实现的,MulUintptr的源码咱们也已经贴出,主要就是用切片中元素大小和切片的容量相乘计算出所需占用的内存空间,若是内存溢出,或者计算出的内存大小大于最大可分配内存,MulUintptr的overflow会返回true,makeslice就会报错。另外若是传入长度小于0或者长度小于容量,makeslice也会报错。
package main import ( "fmt" "unsafe" ) func main() { slice := make([]int, 0, 10) slice = append(slice, 1) fmt.Println(unsafe.Pointer(&slice[0]), len(slice), cap(slice)) slice = append(slice, 2) fmt.Println(unsafe.Pointer(&slice[0]), len(slice), cap(slice)) }
咱们直接给出结果:
0xc00009e000 1 10 0xc00009e000 2 10
咱们能够看到,当slice容量足够时,咱们往slice中append一个2,slice底层数组指向的内存地址没有发生改变;再看一段程序:
func main() { slice := make([]int, 0) slice = append(slice, 1) fmt.Printf("%p %d %d\n", unsafe.Pointer(&slice[0]), len(slice), cap(slice)) slice = append(slice, 2) fmt.Printf("%p %d %d\n", unsafe.Pointer(&slice[0]), len(slice), cap(slice)) }
输出结果是:
0xc00009a008 1 1 0xc00009a030 2 2
咱们能够看到当往slice中append一个1后,slice底层数组的指针指向地址0xc00009a008,长度为1,容量为1。这时再往slice中append一个2,那么slice的容量不够了,此时底层数组会发生copy,会从新分配一块新的内存地址,容量也变成了2,因此咱们会看到底层数组的指针指向地址发生了改变。根据以前汇编的结果咱们知晓了,append操做实际上是调用了runtime/slice.go中的growslice函数,咱们来看下源码:
func growslice(et *_type, old slice, cap int) slice { ... ... if cap < old.cap { panic(errorString("growslice: cap out of range")) } if et.size == 0 { // append should not create a slice with nil pointer but non-zero len. // We assume that append doesn't need to preserve old.array in this case. return slice{unsafe.Pointer(&zerobase), old.len, cap} } newcap := old.cap//1280 doublecap := newcap + newcap//1280+1280=2560 if cap > doublecap { newcap = cap } else { if old.len < 1024 { newcap = doublecap } else { // Check 0 < newcap to detect overflow // and prevent an infinite loop. for 0 < newcap && newcap < cap { newcap += newcap / 4//1280*1.25=1600 } // Set newcap to the requested cap when // the newcap calculation overflowed. if newcap <= 0 { newcap = cap } } } ... }
咱们主要关注下cap的扩容规则,从源码中咱们能够简单的总结出slice容量的扩容规则:当原slice的cap小于1024时,新slice的cap变为原来的2倍;原slice的cap大于1024时,新slice变为原来的1.25倍,咱们写个程序来验证下:
package main import "fmt" func main() { slice := make([]int, 0) oldCap := cap(slice) for i := 0; i < 4096; i++ { slice = append(slice, i) newCap := cap(slice) if newCap != oldCap { fmt.Printf("oldCap = %-4d after append %-4d newCap = %-4d\n", oldCap, i, newCap) oldCap = newCap } } }
这段程序实现的功能是:当cap发生改变时,打印出cap改变先后的值。咱们来看程序的输出结果:
oldCap = 0 after append 0 newCap = 1 oldCap = 1 after append 1 newCap = 2 oldCap = 2 after append 2 newCap = 4 oldCap = 4 after append 4 newCap = 8 oldCap = 8 after append 8 newCap = 16 oldCap = 16 after append 16 newCap = 32 oldCap = 32 after append 32 newCap = 64 oldCap = 64 after append 64 newCap = 128 oldCap = 128 after append 128 newCap = 256 oldCap = 256 after append 256 newCap = 512 oldCap = 512 after append 512 newCap = 1024 oldCap = 1024 after append 1024 newCap = 1280 oldCap = 1280 after append 1280 newCap = 1696 oldCap = 1696 after append 1696 newCap = 2304 oldCap = 2304 after append 2304 newCap = 3072 oldCap = 3072 after append 3072 newCap = 4096
一开始的时候看起来跟我说的扩容规则是同样的,从1->2->4->8->16...->1024,都是成倍增加,当cap大于1024后,再append元素,cap变为1280,变成了1024的1.25倍,也符合咱们的规则;可是继续append,1280->1696,彷佛不是1.25倍,而是1.325倍,可见扩容规则并非咱们以上所说的那么简单,咱们再继续往下看源码:
var overflow bool var lenmem, newlenmem, capmem uintptr // Specialize for common values of et.size. // For 1 we don't need any division/multiplication. // For sys.PtrSize, compiler will optimize division/multiplication into a shift by a constant. // For powers of 2, use a variable shift. switch { case et.size == 1: lenmem = uintptr(old.len) newlenmem = uintptr(cap) capmem = roundupsize(uintptr(newcap)) overflow = uintptr(newcap) > maxAlloc newcap = int(capmem) case et.size == sys.PtrSize: lenmem = uintptr(old.len) * sys.PtrSize newlenmem = uintptr(cap) * sys.PtrSize capmem = roundupsize(uintptr(newcap) * sys.PtrSize)//13568 overflow = uintptr(newcap) > maxAlloc/sys.PtrSize newcap = int(capmem / sys.PtrSize)//13568/8=1696 case isPowerOfTwo(et.size): var shift uintptr if sys.PtrSize == 8 { // Mask shift for better code generation. shift = uintptr(sys.Ctz64(uint64(et.size))) & 63 } else { shift = uintptr(sys.Ctz32(uint32(et.size))) & 31 } lenmem = uintptr(old.len) << shift newlenmem = uintptr(cap) << shift capmem = roundupsize(uintptr(newcap) << shift) overflow = uintptr(newcap) > (maxAlloc >> shift) newcap = int(capmem >> shift) default: lenmem = uintptr(old.len) * et.size newlenmem = uintptr(cap) * et.size capmem, overflow = math.MulUintptr(et.size, uintptr(newcap)) capmem = roundupsize(capmem) newcap = int(capmem / et.size) }
咱们看到每一个case中都执行了roundupsize,咱们再看下roundupsize的源码,以下:
package runtime // Returns size of the memory block that mallocgc will allocate if you ask for the size. func roundupsize(size uintptr) uintptr { if size < _MaxSmallSize {//size=1600*8=12800<32768 if size <= smallSizeMax-8 {//12800<=0 return uintptr(class_to_size[size_to_class8[(size+smallSizeDiv-1)/smallSizeDiv]]) } else { return uintptr(class_to_size[size_to_class128[(size-smallSizeMax+largeSizeDiv-1)/largeSizeDiv]])//size_to_class128[92]= 56 //class_to_size[56]=13568 //13568/8=1696 } } if size+_PageSize < size { return size } return round(size, _PageSize) } const _MaxSmallSize = 32768 const smallSizeDiv = 8 const smallSizeMax = 1024 const largeSizeDiv = 128
其实roundupsize是内存对齐的过程,咱们知道golang中内存分配是根据对象大小来配不一样的mspan,为了不形成过多的内存碎片,slice在扩容中须要对扩容后的cap容量进行内存对齐的操做,接下来咱们对照源码来实际计算下cap容量是否由1280变成了1696。
从以上流程图能够看出,cap在变成1600后又进入了内存对齐的过程,最终cap变为了1696。
go中的slice是支持截取操做的,虽然使用起来很是的方便,可是有不少坑,稍有不慎就会出现bug且不易排查。
让咱们来看一段程序:
package main import "fmt" func main() { slice := []int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9} s1 := slice[2:5] s2 := s1[2:7] fmt.Printf("len=%-4d cap=%-4d slice=%-1v \n", len(slice), cap(slice), slice) fmt.Printf("len=%-4d cap=%-4d s1=%-1v \n", len(s1), cap(s1), s1) fmt.Printf("len=%-4d cap=%-4d s2=%-1v \n", len(s2), cap(s2), s2) }
程序输出:
len=10 cap=10 slice=[0 1 2 3 4 5 6 7 8 9] len=3 cap=8 s1=[2 3 4] len=5 cap=6 s2=[4 5 6 7 8]
s1的长度变成3,cap变为8(默认截取到最大容量), 可是s2截取s1的第2到第7个元素,左闭右开,不少人想问,s1根本没有那么元素啊,可是实际状况是s2截取到了,而且没有发生数组越界,缘由就是s2实际截取的是底层数组,目前slice、s一、s2都是共用的同一个底层数组。
咱们继续操做:
fmt.Println("--------append 100----------------") s2 = append(s2, 100)
输出结果是:
--------append 100---------------- len=10 cap=10 slice=[0 1 2 3 4 5 6 7 8 100] len=3 cap=8 s1=[2 3 4] len=6 cap=6 s2=[4 5 6 7 8 100]
咱们看到往s2里append数据影响到了slice,正是由于二者底层数组是同样的;可是既然都是共用的同一底层数组,s1为何没有100,这个问题再下一节会讲到,你们稍安勿躁。咱们继续进行操做:
fmt.Println("--------append 200----------------") s2 = append(s2, 200)
输出结果是:
--------append 200---------------- len=10 cap=10 slice=[0 1 2 3 4 5 6 7 8 100] len=3 cap=8 s1=[2 3 4] len=7 cap=12 s2=[4 5 6 7 8 100 200]
咱们看到继续往s2中append一个200,可是只有s2发生了变化,slice并未改变,为何呢?对,是由于在append完100后,s2的容量已满,再往s2中append,底层数组发生复制,系统分配了一块新的内存地址给s2,s2的容量也翻倍了。
咱们继续操做:
fmt.Println("--------modify s1----------------") s1[2] = 20
输出会是什么样呢?
--------modify s1---------------- len=10 cap=10 slice=[0 1 2 3 20 5 6 7 8 100] len=3 cap=8 s1=[2 3 20] len=7 cap=12 s2=[4 5 6 7 8 100 200]
这就很容易理解了,咱们对s1进行更新,影响了slice,由于二者共用的仍是同一底层数组,s2未发生改变是由于在上一步时底层数组已经发生了变化;
以此来看,slice截取的坑确实不少,极容易出现bug,而且难以排查,你们在使用的时候必定注意。
上一节中对slice进行的截取,新的slice和原始slice共用同一个底层数组,所以能够看作是对slice的浅拷贝,那么在go中如何实现对slice的深拷贝呢?那么就要依赖golang提供的copy函数了,咱们用一段程序来简单看下如何实现深拷贝:
func main() { // Creating slices slice1 := []int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9} var slice2 []int slice3 := make([]int, 5) // Before copying fmt.Println("------------before copy-------------") fmt.Printf("len=%-4d cap=%-4d slice1=%v\n", len(slice1), cap(slice1), slice1) fmt.Printf("len=%-4d cap=%-4d slice2=%v\n", len(slice2), cap(slice2), slice2) fmt.Printf("len=%-4d cap=%-4d slice3=%v\n", len(slice3), cap(slice3), slice3) // Copying the slices copy_1 := copy(slice2, slice1) fmt.Println() fmt.Printf("len=%-4d cap=%-4d slice1=%v\n", len(slice1), cap(slice1), slice1) fmt.Printf("len=%-4d cap=%-4d slice2=%v\n", len(slice2), cap(slice2), slice2) fmt.Println("Total number of elements copied:", copy_1) }
首先定义了三个slice,而后将slice1 copy到slice2,咱们来看下输出结果:
------------before copy------------- len=10 cap=10 slice1=[0 1 2 3 4 5 6 7 8 9] len=0 cap=0 slice2=[] len=5 cap=5 slice3=[0 0 0 0 0] len=10 cap=10 slice1=[0 1 2 3 4 5 6 7 8 9] len=0 cap=0 slice2=[] Total number of elements copied: 0
咱们发现slice1的内容并未copy到slice2,为何呢?咱们再试下将slice1 copy到slice3,以下:
copy_2 := copy(slice3, slice1)
输出结果:
len=10 cap=10 slice1=[0 1 2 3 4 5 6 7 8 9] len=5 cap=5 slice3=[0 1 2 3 4] Total number of elements copied: 5
咱们看到copy成功,slice3和slice2惟一的区别就是slice3的容量为5,而slice2容量为0,那么是不是深拷贝呢,咱们修改slice3的内容看下:
slice3[0] = 100
咱们再看下输出结果:
len=10 cap=10 slice1=[0 1 2 3 4 5 6 7 8 9] len=5 cap=5 slice3=[100 1 2 3 4]
咱们能够看到修改slice3后,slice1的值并未改变,可见copy实现的是深拷贝。因而可知,copy函数为slice提供了深拷贝能力,可是须要在拷贝前申请内存空间。参照makeslice和growslice咱们对本节一开始的程序进行反汇编,获得汇编代码(部分)以下:
0x0080 00128 (slice.go:10) CALL runtime.makeslice(SB) 0x0085 00133 (slice.go:10) PCDATA $0, $1 0x0085 00133 (slice.go:10) MOVQ 24(SP), AX 0x008a 00138 (slice.go:10) PCDATA $1, $2 0x008a 00138 (slice.go:10) MOVQ AX, ""..autotmp_75+96(SP) 0x008f 00143 (slice.go:11) PCDATA $0, $4 0x008f 00143 (slice.go:11) MOVQ ""..autotmp_74+104(SP), CX 0x0094 00148 (slice.go:11) CMPQ AX, CX 0x0097 00151 (slice.go:11) JEQ 176 0x0099 00153 (slice.go:11) PCDATA $0, $5 0x0099 00153 (slice.go:11) MOVQ AX, (SP) 0x009d 00157 (slice.go:11) PCDATA $0, $0 0x009d 00157 (slice.go:11) MOVQ CX, 8(SP) 0x00a2 00162 (slice.go:11) MOVQ $40, 16(SP) 0x00ab 00171 (slice.go:11) CALL runtime.memmove(SB) 0x00b0 00176 (slice.go:12) MOVQ $10, (SP) 0x00b8 00184 (slice.go:12) CALL runtime.convT64(SB)
咱们发现copy函数实际上是调用runtime.memmove,其实咱们在研究runtime/slice.go文件中的源码的时候,会发现有一个slicecopy函数,这个函数最终就是调用runtime.memmove来实现slice的copy的,咱们看下源码:
func slicecopy(to, fm slice, width uintptr) int { // 若是源切片或者目标切片有一个长度为0,那么就不须要拷贝,直接 return if fm.len == 0 || to.len == 0 { return 0 } // n 记录下源切片或者目标切片较短的那一个的长度 n := fm.len if to.len < n { n = to.len } // 若是入参 width = 0,也不须要拷贝了,返回较短的切片的长度 if width == 0 { return n } //若是开启竞争检测 if raceenabled { callerpc := getcallerpc() pc := funcPC(slicecopy) racewriterangepc(to.array, uintptr(n*int(width)), callerpc, pc) racereadrangepc(fm.array, uintptr(n*int(width)), callerpc, pc) } if msanenabled { msanwrite(to.array, uintptr(n*int(width))) msanread(fm.array, uintptr(n*int(width))) } size := uintptr(n) * width if size == 1 { // common case worth about 2x to do here // TODO: is this still worth it with new memmove impl? //若是只有一个元素,那么直接进行地址转换 *(*byte)(to.array) = *(*byte)(fm.array) // known to be a byte pointer } else { //若是不止一个元素,那么就从 fm.array 地址开始,拷贝到 to.array 地址以后,拷贝个数为size memmove(to.array, fm.array, size) } return n }
源码解读见中文注释。
slice在做为函数参数进行传递的时候,是值传递仍是引用传递,咱们来看一段程序:
package main import "fmt" func main() { slice := make([]int, 0, 10) slice = append(slice, 1) fmt.Println(slice, len(slice), cap(slice)) fn(slice) fmt.Println(slice, len(slice), cap(slice)) } func fn(in []int) { in = append(in, 5) }
很简单的一段程序,咱们直接来看输出结果:
[1] 1 10 [1] 1 10
可见fn内的append操做并未对slice产生影响,那咱们再看一段代码:
package main import "fmt" func main() { slice := make([]int, 0, 10) slice = append(slice, 1) fmt.Println(slice, len(slice), cap(slice)) fn(slice) fmt.Println(slice, len(slice), cap(slice)) } func fn(in []int) { in[0] = 100 }
输出是什么?咱们来看下:
[1] 1 10 [100] 1 10
slice竟然改变了,是否是有点混乱?前面咱们说到slice底层实际上是一个结构体,len、cap、array分别表示长度、容量、底层数组的地址,当slice做为函数的参数传递的时候,跟普通结构体的传递是没有区别的;若是直接传slice,实参slice是不会被函数中的操做改变的,可是若是传递的是slice的指针,是会改变原来的slice的;另外,不管是传递slice仍是slice的指针,若是改变了slice的底层数组,那么都是会影响slice的,这种经过数组下标的方式更新slice数据,是会对底层数组进行改变的,因此就会影响slice。
那么,讲到这里,在第一段程序中在fn函数内append的5到哪里去了,不可能凭空消失啊,咱们再来看一段程序:
package main import "fmt" func main() { slice := make([]int, 0, 10) slice = append(slice, 1) fmt.Println(slice, len(slice), cap(slice)) fn(slice) fmt.Println(slice, len(slice), cap(slice)) s1 := slice[0:9]//数组截取 fmt.Println(s1, len(s1), cap(s1)) } func fn(in []int) { in = append(in, 5) }
咱们来看输出结果:
[1] 1 10 [1] 1 10 [1 5 0 0 0 0 0 0 0] 9 10
显然,虽然在append后,slice中并未展现出5,也没法经过slice[1]取到(会数组越界),可是实际上底层数组已经有了5这个元素,可是因为slice的len未发生改变,因此咱们在上层是没法获取到5这个元素的。那么,再问一个问题,咱们是否是能够手动强制改变slice的len长度,让咱们能够获取到5这个元素呢?是能够的,咱们来看一段程序:
package main import ( "fmt" "reflect" "unsafe" ) func main() { slice := make([]int, 0, 10) slice = append(slice, 1) fmt.Println(slice, len(slice), cap(slice)) fn(slice) fmt.Println(slice, len(slice), cap(slice)) (*reflect.SliceHeader)(unsafe.Pointer(&slice)).Len = 2 //强制修改slice长度 fmt.Println(slice, len(slice), cap(slice)) } func fn(in []int) { in = append(in, 5) }
咱们来看输出结果:
[1] 1 10 [1] 1 10 [1 5] 2 10
能够看出,经过强制修改slice的len,咱们能够获取到了5这个元素。
因此再次回答一开始咱们提出的问题,slice是值传递仍是引用传递?答案是值传递!
以上,在使用golang中的slice的时候你们必定注意,不然稍有不慎就会出现bug。
【1】《深刻解析Go中Slice底层实现》
【2】《理解Go中的Slice》
【3】《深度解密Go语言之Slice》
【4】《The Go Programming Language Specification》