一.基础介绍
1.1 基本概念
slice 并不是数组或数组指针。它通过内部指针和相关属性引用数组片段,以实现变长方案。
切片的声明和初始化:
//全局声明切片,没有初始化,此时s1是nil
var s1 []int
//局部声明切片,这样得到的是一个初始化的空切片 []
s2 := []int{}
//声明的时候进行初始化数据
s5 := []int{1, 2, 3}
//基于数组声明切片
arr := [5]int{1, 2, 3, 4, 5}
var s6 []int
//包括前不包括后
s6 = arr[1:4]
和数组声明的却区别其实就是,数组的声明必须指定数组的大小,或者直接使用[...]来让编译器自行推断大小,并且数组的类型是数组元素类型和数字大小的综合.但是切片在声明的时候不需要指定大小,直接就[]即可.
使用make函数创建切片
var slice []type = make([]type, len)
slice := make([]type, len)
//数组类型,元素个数,容量
slice := make([]type, len, cap)
slice在go中其实就是一个结构体,具体的源码如下:
runtime/slice.go
type slice struct {
array unsafe.Pointer
len int
cap int
}
可以看到,其中有三个属性,array就是当前切片引用的数组,len就是当前切片的现有元素的长度,cap就是当前切片的容量大小.
通过下面两张图可以形象地认识下slice:
可以看到,切片本身就是为数组维护了一个逻辑映射的概念.
1.2.slice的扩容
前面我们说了,slice底层其实也是引用了一个指定的数组,只不过是数组指针,所以这个数组是可变的,所以如果当向slice中插入数据,已经超过了slice的容量,那么slice就会扩容,扩容后的slice中底层引用的数组就不是原来的了,而是变成了扩容后的新数组.
首先我们这样初始化一个切片:
s := make([]int, 2, 4)
那么现在的切片如下:
然后我们将s[1]设置为1:
s := make([]int, 2, 4)
s[1] = 1
那么此时的切片如下:
如果我们执行s[2] = 1,那么就会出现panic: runtime error: index out of range [2] with length 2,这样操作的原本想法可能是想,当前slice的容量是4,目前只存储了2个元素(即len是2),所以我想给index是2的位置上插入一个元素,但是这样的操作panic了.
那么我们应该怎样向切片中插入元素呢?我们可以使用go的内置函数append,将元素追加到切片的末尾.
比如:
//将元素1插入切片s的末尾
s = append(s, 1)
也支持一次插入多个元素到slice
s = append(s, 1, 2)
s = append(s, []int{1, 2}...)
还可以将字符串追加到切片末尾
s = append([]byte("hello"), "world"...)
好的,现在我们执行append(s, 2)
,将元素2追加到s[1]后面,我们会得到下面的结果:
此时切片s的容量仍然是4,但是长度变成了3
接着,我们再一次性插入三个元素到切片s中:
s = append(s, 1, 2, 3)
即综合上面所有的步骤,执行以下代码:
s := make([]int, 2, 4)
s[1] = 1
s = append(s, 2)
s = append(s, 1, 2, 3)
fmt.Printf("s:%v,len(s):%d,cap(s):%d\n",
s, len(s), cap(s))
得到结果:
s:[0 1 2 1 2 3],len(s):6,cap(s):8
原因:当我们添加第5个元素时,发现已经超过切片的容量了。这个时候会触发扩容,会将容量加倍,然后复制所有元素创建另一个数组。然后会把剩下的2和3插进去。如下所示:
扩容机制:如果不到1024个元素,会成倍扩容;超过1024个元素,按25%扩容
另外,还有使用区间进行赋值的,如下:
s := make([]int, 2, 4)
s[1] = 1
s = append(s, 2)
s = append(s, 1, 2, 3)
fmt.Printf("s:%v,len(s):%d,cap(s):%d\n",
s, len(s), cap(s))
s2 := s[1:2] // 这里重新赋值给s2
fmt.Printf("s2:%v,len(s2):%d,cap(s2):%d\n",
s2, len(s2), cap(s2))
s:[0 1 2 1 2 3],len(s):6,cap(s):8
s2:[1],len(s2):1,cap(s2):7
切片s和s2此时是引用同一个底层数组的,但是由于s2是从1开始,所以容量变成了7,并且s2只引用了[1,2]所以,打印出来的数据只有一个元素1:
这时如果我们修改了s2[0]或者s[1],实际上他们指向的是底层数组的同一个元素。所以s2[0]或者s[1]都会被修改掉。
:= make([]int, 2, 4)
s[1] = 1
s = append(s, 2)
s = append(s, 1, 2, 3)
s2 := s[1:2]
s2[0] = 8 // 这里
fmt.Printf("修改俩切片指向的同一个元素:s:%v,len(s):%d,cap(s):%d)----s2:%v,len(s2):%d,cap(s2):%d\n",
s, len(s), cap(s), s2, len(s2), cap(s2))
go run 4.go
修改俩切片指向的同一个元素:s:[0 8 2 1 2 3],len(s):6,cap(s):8)----s2:[8],len(s2):1,cap(s2):7
继续往s2中插入一个元素
s := make([]int, 2, 4)
s[1] = 1
s = append(s, 2)
s = append(s, 1, 2, 3)
s2 := s[1:2]
s2[0] = 8
// 插入元素6
s2 = append(s2, 6)
fmt.Printf("往s2插入一个元素:s:%v,len(s):%d,cap(s):%d----s2:%v,len(s2):%d,cap(s2):%d\n",
s, len(s), cap(s), s2, len(s2), cap(s2))
往s2插入一个元素:s:[0 8 6 1 2 3],len(s):6,cap(s):8----s2:[8 6],len(s2):2,cap(s2):7
我们可以看到s2[1]的元素写进去了,值是6,长度变为2。但是s的长度并没有变化,s[2]的元素却也被修改为了6。这是因为往s2插入元素时并没有超过s2的容量,所以它和s还是共用同一个底层数组。
继续往s2中添加6个元素,看看超出容量后的底层数组会是什么样的:
s := make([]int, 2, 4)
s[1] = 1
s = append(s, 2)
s = append(s, 1, 2, 3)
s2 := s[1:2]
s2[0] = 8
s2 = append(s2, 6)
// 继续插入6个元素
s2 = append(s2, 7)
s2 = append(s2, 8)
s2 = append(s2, 9)
s2 = append(s2, 10)
s2 = append(s2, 11)
s2 = append(s2, 12)
fmt.Printf("继续往s2插入6个元素:s:%v,len(s):%d,cap(s):%d----s2:%v,len(s2):%d,cap(s2):%d\n",
s, len(s), cap(s), s2, len(s2), cap(s2))
我们来分析下上面的例子:
- 当我们往s2插入7的时候,此时s2的长度变为3,容量还是7。s[3]对- 应也被修改。
- 当我们往s2插入8的时候,此时s2的长度变为4,容量还是7。s[4]对应也被修改。
- 当我们往s2插入9的时候,此时s2的长度变为5,容量还是7。s[5]对应也被修改。
- 当我们往s2插入10的时候,此时s2的长度变为6,容量还是7。s[6]对应也被修改。
- 当我们往s2插入11的时候,此时s2的长度变为7,容量还是7。因为s切片长度为6,所以没有变化。
- 当我们往s2插入12的时候,此时s2超过s2的容量引发扩容,底层数组被复制,s2指向一个新的容量为14的数组。因为s长度小于容量,所以还是指向原来的数组。
如下图所示:
二.切片是值传递还是地址传递
想搞清楚这个,首先需要明确一点,其实上面我也说明了,就是golang中的切片是引用类型,也就是说,可以认为切片变量本身就是一个地址.看下面的代码:
var s1 = []int{1, 2, 3}
fmt.Printf("s1切片变量的地址(修改前):%p,s1引用表示的地址(修改前)%p,s1的值(修改前):%d,s1的类型%T\n", &s1, s1, s1, s1)
modifySlice(s1)
fmt.Printf("s1切片变量的地址(修改后):%p,s1引用表示的地址(修改后):%p,s1的值(修改后):%d,s1的类型%T\n", &s1, s1, s1, s1)
var arr = [5]int{1, 2, 3, 4, 5}
fmt.Printf("arr数组的变量地址:%p\n", &arr)
var s2 = arr[1:3]
fmt.Printf("s2切片变量的地址(修改前):%p,s2引用表示的地址(修改前)%p,s2的值(修改前):%d,s2的类型%T\n", &s2, s2, s2, s2)
fmt.Printf("%T", arr[1])
func modifySlice(s []int) {
fmt.Printf("传入函数内的切片变量的地址:%p,传入函数内的切片引用表示的地址:%p,传入函数内的切片的值:%d,传入函数内的切片的类型:%T\n", &s, s, s, s)
s[1] = 1000
}
执行结果:
s1切片变量的地址(修改前):0x1400000c030,s1引用表示的地址(修改前)0x1400001e0a8,s1的值(修改前):[1 2 3],s1的类型[]int
传入函数内的切片变量的地址:0x1400000c090,传入函数内的切片引用表示的地址:0x1400001e0a8,传入函数内的切片的值:[1 2 3],传入函数内的切片的类型:[]int
s1切片变量的地址(修改后):0x1400000c030,s1引用表示的地址(修改后):0x1400001e0a8,s1的值(修改后):[1 2 3],s1的类型[]int
arr数组的变量地址:0x14000018150
s2切片变量的地址(修改前):0x1400000c138,s2引用表示的地址(修改前)0x14000018158,s2的值(修改前):[2 3],s2的类型[]int
int
还需要明确一点,因为切片是引用类型,所以切片变量的值可以认为是所引用的数组的首地址. 传入函数内部后的切片变量本身(也就是代码里说的引用表示的地址,因为切片本身就是引用类型,所以它本身就是地址)和外部是一样的,也就是 说值被拷贝传了进来,所以是一样的.
后面使用数组初始化切片,得到的引用本身就是原数组的地址(数组的地址就是其首元素地址)偏移8个byte得到的(0x14000018150-->0x14000018158). 因为切片是[1:3],所以切片引用的数组是原数组的第1个元素开始,也就是说切片的地址就是原数组的第一个元素的地址,而我们这里使用的是默认的int类型,而int类型没有注明大小,我们这台机器是64bit操作系统,在golang中默认就是int64,也就是8byte,所以便宜8个byte得到0x14000018158.
var s1 = []int{1, 2, 3}
fmt.Printf("s1切片变量的地址(修改前):%p,s1引用表示的地址(修改前)%p,s1的值(修改前):%d,s1的类型%T\n", &s1, s1, s1, s1)
modifySlice(s1)
fmt.Printf("s1切片变量的地址(修改后):%p,s1引用表示的地址(修改后):%p,s1的值(修改后):%d,s1的类型%T\n", &s1, s1, s1, s1)
func modifySlice(s []int) {
fmt.Printf("修改前:传入函数内的切片变量的地址:%p,传入函数内的切片引用表示的地址:%p,传入函数内的切片的值:%d,传入函数内的切片的类型:%T\n", &s, s, s, s)
s[1] = 100
fmt.Printf("修改后:传入函数内的切片变量的地址:%p,传入函数内的切片引用表示的地址:%p,传入函数内的切片的值:%d,传入函数内的切片的类型:%T\n", &s, s, s, s)
}
s1切片变量的地址(修改前):0x1400010e018,s1引用表示的地址(修改前)0x14000122018,s1的值(修改前):[1 2 3],s1的类型[]int
修改前:传入函数内的切片变量的地址:0x1400010e078,传入函数内的切片引用表示的地址:0x14000122018,传入函数内的切片的值:[1 2 3],传入函数内的切片的类型:[]int
修改后:传入函数内的切片变量的地址:0x1400010e078,传入函数内的切片引用表示的地址:0x14000122018,传入函数内的切片的值:[1 100 3],传入函数内的切片的类型:[]int
s1切片变量的地址(修改后):0x1400010e018,s1引用表示的地址(修改后):0x14000122018,s1的值(修改后):[1 100 3],s1的类型[]int
如下图:
所以这类虽然打印的值地址是一样的,其实是因为拷贝的就是地址.
如果修改为下面这样,就比较明显了,不会那么容易误解为引用传递:
var s1 = make([]int, 3)
fmt.Printf("s1切片变量的地址(修改前):%p,s1引用表示的地址(修改前)%p,s1的值(修改前):%d,s1的类型%T\n", &s1, s1, s1, s1)
modifySlice(s1)
fmt.Printf("s1切片变量的地址(修改后):%p,s1引用表示的地址(修改后):%p,s1的值(修改后):%d,s1的类型%T\n", &s1, s1, s1, s1)
func modifySlice(s []int) {
fmt.Printf("修改前:传入函数内的切片变量的地址:%p,传入函数内的切片引用表示的地址:%p,传入函数内的切片的值:%d,传入函数内的切片的类型:%T\n", &s, s, s, s)
sTemp := append(s, 100)
fmt.Printf("sTemp:%p,%p,%v \n", &sTemp, sTemp, sTemp)
s = sTemp
fmt.Printf("修改后:传入函数内的切片变量的地址:%p,传入函数内的切片引用表示的地址:%p,传入函数内的切片的值:%d,传入函数内的切片的类型:%T\n", &s, s, s, s)
}
s1切片变量的地址(修改前):0x14000110018,s1引用表示的地址(修改前)0x14000122018,s1的值(修改前):[0 0 0],s1的类型[]int
修改前:传入函数内的切片变量的地址:0x14000110078,传入函数内的切片引用表示的地址:0x14000122018,传入函数内的切片的值:[0 0 0],传入函数内的切片的类型:[]int
sTemp:0x140001100d8,0x1400012a030,[0 0 0 100]
修改后:传入函数内的切片变量的地址:0x14000110078,传入函数内的切片引用表示的地址:0x1400012a030,传入函数内的切片的值:[0 0 0 100],传入函数内的切片的类型:[]int
s1切片变量的地址(修改后):0x14000110018,s1引用表示的地址(修改后):0x14000122018,s1的值(修改后):[0 0 0],s1的类型[]int
扩容后图示:
赋值后图示:
赋值后,切片s底层使用的就不是原来的数组了,而是指向了新扩容后的数组.所以此时再对切片s进行修改操作,那么就不会影响到原有的切片s1了.
比如这样:
var s1 = make([]int, 3)
fmt.Printf("s1切片变量的地址(修改前):%p,s1引用表示的地址(修改前)%p,s1的值(修改前):%d,s1的类型%T\n", &s1, s1, s1, s1)
modifySlice(s1)
fmt.Printf("s1切片变量的地址(修改后):%p,s1引用表示的地址(修改后):%p,s1的值(修改后):%d,s1的类型%T\n", &s1, s1, s1, s1)
func modifySlice(s []int) {
fmt.Printf("修改前:传入函数内的切片变量的地址:%p,传入函数内的切片引用表示的地址:%p,传入函数内的切片的值:%d,传入函数内的切片的类型:%T\n", &s, s, s, s)
sTemp := append(s, 100)
fmt.Printf("sTemp:%p,%p,%v \n", &sTemp, sTemp, sTemp)
s = sTemp
s[0] = 99
fmt.Printf("修改后:传入函数内的切片变量的地址:%p,传入函数内的切片引用表示的地址:%p,传入函数内的切片的值:%d,传入函数内的切片的类型:%T\n", &s, s, s, s)
}
s1切片变量的地址(修改前):0x14000110018,s1引用表示的地址(修改前)0x14000122018,s1的值(修改前):[0 0 0],s1的类型[]int
修改前:传入函数内的切片变量的地址:0x14000110078,传入函数内的切片引用表示的地址:0x14000122018,传入函数内的切片的值:[0 0 0],传入函数内的切片的类型:[]int
sTemp:0x140001100d8,0x1400012a030,[0 0 0 100]
修改后:传入函数内的切片变量的地址:0x14000110078,传入函数内的切片引用表示的地址:0x1400012a030,传入函数内的切片的值:[99 0 0 100],传入函数内的切片的类型:[]int
s1切片变量的地址(修改后):0x14000110018,s1引用表示的地址(修改后):0x14000122018,s1的值(修改后):[0 0 0],s1的类型[]int
可以看到,将元素1修改为99后,外部函数的切片s1并没有修改,所以golang中的切片实际上是值传递.
本站文章除注明转载/出处外,均为本站原创或翻译,转载前请务必署名,转载请标明出处
最后编辑时间为:
2023/03/28 00:05