Golang 中 Slice的分析与使用(含源码)
文章目录
- 1、slice结构体
- 2、slice初始化
- 3、append操作
- 4、slice截取
- 5、slice深拷贝
- 6、值传递还是引用传递
- 参考文献
众所周知,在golang中,slice(切片)是我们最常使用到的一种数据结构,是一种可变长度的数组,本篇文章我们主要结合源码来介绍一下slice的底层实现,以及在使用slice时的一些注意事项。
1、slice结构体
首先我们来看一段代码
package main
import ("fmt""unsafe"
)
func main() {var a intvar b int8var c int16var d int32var e int64slice := 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。
2、slice初始化
简单了解了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
得到汇编代码如下(截取部分):
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
大家可能看到这里有点蒙,这是在干啥,其实我们只需要关注一些关键的信息就好了,主要是这几行
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函数到底做了什么
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函数源码
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/areturn a * b, overflow
}
简单来说,makeslice函数的工作主要就是计算slice所需内存大小,然后调用mallocgc进行内存的分配。计算slice所需内存又是通过MulUintptr来实现的,MulUintptr的源码我们也已经贴出,主要就是用切片中元素大小和切片的容量相乘计算出所需占用的内存空间,如果内存溢出,或者计算出的内存大小大于最大可分配内存,MulUintptr的overflow会返回true,makeslice就会报错。另外如果传入长度小于0或者长度小于容量,makeslice也会报错。
3、append操作
首先我们来看一段程序
package mainimport ("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//1280doublecap := newcap + newcap//1280+1280=2560if 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 boolvar 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) > maxAllocnewcap = int(capmem)case et.size == sys.PtrSize:lenmem = uintptr(old.len) * sys.PtrSizenewlenmem = uintptr(cap) * sys.PtrSizecapmem = roundupsize(uintptr(newcap) * sys.PtrSize)//13568overflow = uintptr(newcap) > maxAlloc/sys.PtrSizenewcap = int(capmem / sys.PtrSize)//13568/8=1696case isPowerOfTwo(et.size):var shift uintptrif 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) << shiftnewlenmem = uintptr(cap) << shiftcapmem = roundupsize(uintptr(newcap) << shift)overflow = uintptr(newcap) > (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)}
我们看到每个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<32768if size <= smallSizeMax-8 {//12800<=0return 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。
4、slice截取
go中的slice是支持截取操作的,虽然使用起来非常的方便,但是有很多坑,稍有不慎就会出现bug且不易排查。让我们来看一段程序
package mainimport "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、s1、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,并且难以排查,大家在使用的时候一定注意。
5、slice深拷贝
上一节中对slice进行的截取,新的slice和原始slice共用同一个底层数组,因此可以看做是对slice的浅拷贝,那么在go中如何实现对slice的深拷贝呢?那么就要依赖golang提供的copy函数了,我们用一段程序来简单看下如何实现深拷贝:
func main() {// Creating slicesslice1 := []int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}var slice2 []intslice3 := make([]int, 5)// Before copyingfmt.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 slicescopy_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,那么就不需要拷贝,直接 returnif fm.len == 0 || to.len == 0 {return 0}// n 记录下源切片或者目标切片较短的那一个的长度n := fm.lenif 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) * widthif 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 地址之后,拷贝个数为sizememmove(to.array, fm.array, size)}return n
}
源码解读见中文注释。
6、值传递还是引用传递
slice在作为函数参数进行传递的时候,是值传递还是引用传递,我们来看一段程序:
package mainimport "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 mainimport "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 mainimport "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 mainimport ("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底层实现》:https://halfrost.com/go_slice/
【2】《理解Go中的Slice》:https://sanyuesha.com/2018/07/31/go-slice/
【3】《深度解密Go语言之Slice》:https://segmentfault.com/a/1190000019378931
【4】《The Go Programming Language Specification》:https://golang.org/ref/spec
相关文章:
Golang 中 Slice的分析与使用(含源码)
文章目录1、slice结构体2、slice初始化3、append操作4、slice截取5、slice深拷贝6、值传递还是引用传递参考文献众所周知,在golang中,slice(切片)是我们最常使用到的一种数据结构,是一种可变长度的数组,本篇…...
瀑布开发与敏捷开发的区别,以及从瀑布转型敏捷项目管理的5大注意事项
事实证明,瀑布开发管理模式并不适合所有的软件项目,但敏捷项目管理却对大多数项目有效。那么当团队选择转型敏捷的时候有哪些因素必须注意?敏捷开发最早使用者大多是小型、独立的团队,他们通常致力于小型、独立的项目。正是他们的…...
“华为杯”研究生数学建模竞赛2007年-【华为杯】A题:建立食品卫生安全保障体系数学模型及改进模型的若干理论问题(附获奖论文)
赛题描述 我国是一个拥有13亿人口的发展中国家,每天都在消费大量的各种食品,这批食品是由成千上万的食品加工厂、不可计数的小作坊、几亿农民生产出来的,并且经过较多的中间环节和长途运输后才为广大群众所消费,加之近年来我国经济发展迅速而环境治理没有能够完全跟上,以…...
基于JavaWeb学生选课系统开发与设计(附源码资料)
文章目录1. 适用人群2. 你将收获3.项目简介4.技术实现5.运行部分截图5.1.管理员模块5.2.教师模块5.3.学生模块1. 适用人群 本课程主要是针对计算机专业相关正在做毕业设计或者是需要实战项目的Java开发学习者。 2. 你将收获 提供:项目源码、项目文档、数据库脚本…...
centos7 oracle19c安装||新建用户|| ORA-01012: not logged on
总共分三步 1.下载安装包:里面有一份详细的安装教程 链接:https://pan.baidu.com/s/1Of2a72pNLZ-DDIWKrTQfLw?pwd8NAx 提取码:8NAx 2.安装后,执行初始化:时间较长 /etc/init.d/oracledb_ORCLCDB-19c configure 3.配置环境变量,不配置环境变量,sq…...
【算法设计-分治】递归与尾递归
文章目录1. 阶乘尾递归:递归的进一步优化2. 斐波那契数列3. 最大公约数(GCD)4. 上楼梯5. 汉诺塔(1)输出移动过程输出移动步数5. 汉诺塔(2)输出移动过程输出移动步数6. 杨辉三角形7. 完全二叉树1…...
HTML 编辑器
文章目录 HTML 编辑器HTML 编辑器推荐编辑器下载网站HBuilder步骤 1: 新建 HTML 文件步骤 2: 另存为 HTML 文件步骤 3: 在浏览器中运行这个 HTML 文件HTML 编辑器 HTML 编辑器推荐 可以使用专业的 HTML 编辑器来编辑 HTML,我为大家推荐几款常用的编辑器: Notepad++:Windows…...
css盒模型详解
一、引言 盒模型是网页开发中的一个基本概念,它描述了网页元素的外观和大小。盒模型由内容区域、内边距、边框和外边距四个部分组成,这些部分的大小和位置都可以通过CSS进行控制。在本文中,我们将介绍盒模型的概念和作用,并提出本…...
函数模板(template关键字的应用)
注释:本文主要介绍了函数模板的由来以及用法,还有关键字template。 我们感到时间的延续像一条我们无法逆行的小溪。 ——柏格森 文章目录一、语言的定式二、函数模板2.1 函数模板格式2.2 模板函数的实例化2.2.1隐式实例化/显式实例化2.3 模板参数的匹配…...
嵌入式学习笔记——使用寄存器编程操作GPIO
使用寄存器编程操作GPIO前言GPIO相关的寄存器GPIO 端口模式寄存器 (GPIOx_MODER) (x A..I)位操作GPIO 端口输出类型寄存器 (GPIOx_OTYPER) (x A..I)GPIO 端口输出速度寄存器 (GPIOx_OSPEEDR) (x A..I/)GPIO 端口上拉/下拉寄存器 (GPIOx_PUPDR) (x A..I/)GPIO 端口输入数据寄…...
图像的读取与保存
图像是由一个个像素点组成,像素点就是颜色点,而颜色最简单的方式就是用RGB或RGBA表示图像保存图像将像素信息按照 一定格式,一定顺序(即编码) 存在硬盘上的 二进制文件 中保存图像需要以下必要信息:1. 文件…...
【蓝桥杯集训·每日一题】AcWing 4074. 铁路与公路
文章目录一、题目1、原题链接2、题目描述二、解题报告1、思路分析2、时间复杂度3、代码详解三、知识风暴Floyd 算法Spfa 算法一、题目 1、原题链接 4074. 铁路与公路 2、题目描述 某国家有 n 个城市(编号 1∼n)和 m 条双向铁路。 每条铁路连接两个不同的…...
网络:TCP与UDP相关知识(详细)
目录:1、UDP 和 TCP 的特点与区别2、UDP 、TCP 首部格式3、TCP 的三次握手和四次挥手4、TCP 的三次握手(为什么三次?)5、TCP 的四次挥手(为什么四次?)6、TCP 长连接和短连接的区别7、TCP粘包、拆…...
不好!有敌情,遭到XSS攻击【网络安全篇】
XSS:当一个目标的站点,被我们用户去访问,在渲染HTMl的过程中,出现了没有预期到的脚本指令,然后就会执行攻击者用各种方法注入并执行的恶意脚本,这个时候就会产生XSS。 涉及方: 用户࿰…...
Mysql中Explain详解及索引的最佳实践
Mysql中Explain详解及索引的最佳实践1.Explan工具的介绍1.1 Explan 分析示例1.2 Explain中的列1.2.1 id1.2.2 select_type1.2.3 table1.2.4 partitions1.2.5 type1.2.6 possible_keys1.2.7 key1.2.8 key_len1.2.9 ref1.2.10 rows1.2.11 filtered1.2.12 Extra1.Explan工具的介绍…...
JavaScript 内的 this 指向
在 javascript 语言中, 有一个奇奇怪怪的 “关键字” 叫做 this为什么说它是 奇奇怪怪 呢, 是因为你写出 100 个 this, 可能有 100 个解释, 完全不挨边,但是, 在你的学习过程中, 搞清楚了 this 这个玩意, 那么会对你的开发生涯有很大帮助的,接下来咱们就…...
Java多种方法实现等待所有子线程完成再继续执行
简介 在现实世界中,我们常常需要等待其它任务完成,才能继续执行下一步。Java实现等待子线程完成再继续执行的方式很多。我们来一一查看一下。 Thread的join方法 该方法是Thread提供的方法,调用join()时,会阻塞主线程࿰…...
制造企业数字化工厂建设步骤的建议
随着工业4.0、中国制造2025的深度推进,越来越多的制造企业开始迈入智能制造的领域,那数字工厂要从何入手呢? 数字工厂规划的核心,也正是信息域和物理域这两个维度,那就从这两个维度来进行分析,看如何进行数…...
网上鲜花交易平台,可运行
文章目录项目介绍一、项目功能介绍1、用户模块主要功能包括:2、商家模块主要功能包括:3、管理员模块主要功能包括:二、部分页面展示1、用户模块部分功能页面展示2、商家模块部分功能页面展示3、管理员模块部分功能页面展示三、部分源码四、底…...
【实战】用 Custom Hook + TS泛型实现 useArray
文章目录一、题目二、答案(非标准)三、关键知识点1.Custom Hook关键点案例useMountuseDebounce2.TS 泛型关键点一、题目 完善自定义 Hook —— useArray ,使其能够完成 tryUseArray 组件中测试的功能: 入参:数组返回…...
【LeetCode】剑指 Offer(18)
目录 题目:剑指 Offer 35. 复杂链表的复制 - 力扣(Leetcode) 题目的接口: 解题思路: 代码: 过啦!!! 写在最后: 题目:剑指 Offer 35. 复杂链…...
Kubernetes节点运行时从Docker切换到Containerd
由于k8s将于1.24版本弃用dockershim,所以最近在升级前把本地的k8s切换到了Containerd运行时,目前我的k8s版本是1.22.5,一个master,二个Node的配置,以下做为一个操作记录日志整理,其它可以参考官网文档。 在…...
【编程基础之Python】12、Python中的语句
【编程基础之Python】12、Python中的语句Python中的语句赋值语句条件语句循环语句for循环while循环continue语句break语句continue与break的区别函数语句pass语句异常处理语句结论Python中的语句 Python是一种高级编程语言,具有简单易学的语法,适用于各…...
android h5餐饮管理系统myeclipse开发mysql数据库编程服务端java计算机程序设计
一、源码特点 android h5餐饮管理系统是一套完善的WEBandroid设计系统,对理解JSP java,安卓app编程开发语言有帮助(系统采用web服务端APP端 综合模式进行设计开发),系统具有完整的源代码和数据库,系统主要…...
容易混淆的嵌入式(Embedded)术语
因为做嵌入式开发工作虽然跳不出电子行业,但还是能接触到跨度较大的不同行当,身处不同的圈子。诸如医疗,银行,车载,工业;亦或者手机,PC,专用芯片;甚至可能横跨系统开发、…...
Nodejs 中 JSON 和 YAML 互相转换
JSON 转换成 YAML 1. 安装 js-yaml 库: npm install js-yaml2. 在程序中引入依赖库 const yaml require(js-yaml);3. 创建一个 js 对象, 代表 json 数据 const jsonData {name: John,age: 30,city: New York };4. 使用 yaml.dump() 把 js 对象转换成 YAML, 返回 YAML 字符…...
C++入门教程||C++ 修饰符类型||C++ 存储类
C 修饰符类型 C 允许在 char、int 和 double 数据类型前放置修饰符。修饰符用于改变基本类型的含义,所以它更能满足各种情境的需求。 下面列出了数据类型修饰符: signedunsignedlongshort 修饰符 signed、unsigned、long 和 short 可应用于整型&#…...
Android开发面试:Java知识答案精解
目录 Java 集合 集合概述 HashMap ConcurrentHashMap 泛型 反射 注解 IO流 异常、深浅拷贝与Java8新特性 Java异常 深浅拷贝 Java8新特性 并发 线程 线程池 锁 volatile JVM 内存区域 内存模型 类加载机制 垃圾回收机制 如何判断对象已死 Java 集合 …...
Windows上一款特别好用的画图软件
安装 废话不多说,打开windows的应用商店,搜索draw.io,点击获取即可。 画图 draw.io的布局左边是各种图形组件,中间是画布,右边是属性设置,文件扩展名是.drawio。 点击左边列表中的图形可以将它添加到画…...
html--学习
javascrapt交互,网页控制JavaScript:改变 HTML 图像本例会动态地改变 HTML <image> 的来源(src):点亮灯泡<script>function changeImage() {elementdocument.getElementById(myimage) #内存变量࿰…...
湛江有帮公司做网站/营销模式都有哪些
文章目录读写文件读取文件存储文件将大数据分块读取和存储查看和统计查看文件内容及类型根据某列特定值筛选查看每列缺省项个数个数统计类型数统计分组统计通过两个字段的关系筛选数据数据操作取其中几列类型转换排序删除某一列中含有特定字符的项删除重复项删除缺失值删除行或…...
南通给公司做网站的/国内可访问的海外网站和应用
转自:http://www.jb51.net/article/55442.htm 一、 Git 常用命令速查 git branch 查看本地所有分支git status 查看当前状态 git commit 提交 git branch -a 查看所有的分支git branch -r 查看远程所有分支git commit -am "init" 提交并且加注释 git remo…...
网站制作好学吗/无锡网络公司
在学习对象数组的时候,对这对象内保存数据的方法比较感兴趣,于是执行了一遍书上给的代码。 function weekTemps(){this.dataStore[];this.addadd;this.averageaverage;}function add(temp){this.dataStore.push(temp);}function average(){var total0;fo…...
织梦cms可以做外贸网站吗/人大常委会委员长
http://zhidao.baidu.com/link?urlpvYg-Z5fjOaFHrpdxFSjrDqkaUpvc-tY5VwtLjd7bfmdG4T80i0Rqkkv1zcApZiIq6wa9tCUC0u__X19Rr_74V49PDWb3gbPuxGsiwGfdaa输出 this.webbrowser.Version 显示版本是IE11的,但实际版本不是啊! 网上打的修改注册表HKEY_LOCAL_…...
安阳专业做网站公司/网络运营推广合作
1、 通过Context.getSharedPreferences(String fileName,int mode)方法,可以打开一个xml文件,文件的位置在/data/data/package_name/shared_prefs/fileName.xml,如果不存在,则会自动创建。可以对该文件进行读写操作,在…...
成都建材网站建设/google谷歌
AndroidProject 项目地址:getActivity/AndroidProject 简介:An advanced template project 更多:作者 提 Bug 标签: 博客地址:但愿人长久,搬砖不再有 当我们日复一日年复一年的搬砖的时候&#x…...