切片是一种操作,也是一种类型!!!
切片Slice类型是一个元素类型相同且长度可变的序列,切片类型在底层指向的就是一个数组。切片的语法和数组很像,只是没有固定长度而已,如下
1、数组类型:[固定长度]T
2、切片类型:[]T
切片与数组不同的是
1、数组是长度不可变的序列,而切片则是长度可变的序列,
2、数组是一个值类型,而切片是一个引用类型
切片类型由三个部分构成:指针、长度、容量
| |
| 注意:切片的第一个元素并不一定就是数组的第一个元素 |
| |
| |
| 注意:长度不能超过切片的容量 |
| |
| |
| 要注意的是: |
| 1、随着往切片中append元素,若底层数组不够容纳,切片便会自动扩容(指向新的数组) |
| 2、在没有扩容的情况下:容量指的是从切片的开始位置到底层数组的结尾位置 |
| ps:内置的len和cap函数分别返回slice的长度和容量 |
因为切片类型属于复合类型中的引用类型,所以
(1)直接声明一个切片类型,零值为nil(引用类型的零值都为nil),nil代表空、没有分配内存,即没有底层数组
| var a []int |
| fmt.Println(a == nil) |
| |
| a[0] = 666 |
(2)初始化切片类型的方式有两种
方式一:先调用make()函开辟空间,然后再塞值
| =======>先上例子<======= |
| var a []int = make([]int, 3, 3) |
| fmt.Printf("%p\n", &a) |
| fmt.Printf("%p\n", a) |
| fmt.Printf("%v\n", a) |
| |
| fmt.Println(a == nil) |
| a[0] = 666 |
| |
| =======>make()介绍<======= |
| 内置的make函数可以创建一个指定`元素类型`、`长度`和`容量`的slice。 |
| 其实在底层,make是先创建了一个匿名的数组变量,然后再返回一个slice;并且只有通过这个返回的slice才能引用底层匿名的数组变量 |
| 使用make函数为切片事先申请好内存空间,也可以防止运行时申请内存带来的效率问题。 |
| |
| make([]T, len) |
| make([]T, len, cap) |
方式二:一步到位式,即赋一个完整的值,该方式等同于make完毕之后再塞值,如下所示
| |
| var names []string = []string{"egon", "bigegon", "smallegon" |
| |
| |
| s1 := []string{"aaa", "bbb",3:"ddd",} |
| fmt.Println(s1) |
| fmt.Println(s1[0]) |
| fmt.Println(s1[1]) |
| fmt.Println(s1[2]) |
| fmt.Println(s1[3]) |
| |
| 小结 |
| slice和数组的字面值语法很类似,它们都是用花括弧包含一系列的初始化元素。 |
| s1 := [5]int{1, 2, 3, 4, 5} |
| fmt.Printf("%T\n",s1) |
| |
| s2 := []int{1, 2, 3, 4, 5} |
| fmt.Printf("%T\n",s2) |
| |
| 但是对于slice并没有指明序列的长度,这会隐式地创建一个合适大小的数组,然后slice的指针指向底层的数组。 |
完整切片操作:s[i:j:max],
| i:起始索引 |
| j:结束索引 |
| max:往切片末尾添加值时索引将超过j,max指的是最大可以达到的索引值,max-i+1就是切片的容量 |
| |
| |
| |
| |
| |
| |
被切的对象可以是?
- 1、我们可以对数组、切片、指向数组或切片的指针进行切片操作,得到的是一个切片Slice类型。
- 2、我们也可以对字符串进行切片操作,得到的是一个字符串类型
注意:对数组进行切片操作,结束位置不能超过数组的长度
| |
| months := [...]string{1: "January", 2: "February", 3: "March", 4: "April", 5: "May", 6: "June", 7: "July", 8: "August", 9: "September", 10: "October", 11: "November", 12: "December"} |
| fmt.Println(len(months)) |
| |
| |
| fmt.Println(months[0:12]) |
| fmt.Println(months[0:13]) |
| fmt.Println(months[0:14]) |
| |
| |
| Q2 := months[4:7] |
| fmt.Println(Q2) |
| fmt.Printf("%T\n",Q2) |
| fmt.Println(Q2[1]) |
| |
| summer := months[6:9] |
| fmt.Println(summer) |
| fmt.Printf("%T\n",summer) |
| fmt.Println(summer[1]) |
| |
| |
| summer[0]="JUNE" |
| |
| fmt.Println(months) |
| fmt.Println(Q2) |
注意:
1、对一个切片再进行切片操作,结束位置能达到的最大值是切片的容量,而不是长度
2、对一切片进行切片操作,如果起始位置是0,代表的是切片的第一个元素,而切片的第一个元素并不一定就是底层数组的第一个元素,结束位置同理。
| |
| summer := months[6:9] |
| |
| |
| |
| fmt.Println(summer) |
| fmt.Println(summer[:7]) |
| fmt.Printf("%T",summer[:7]) |
| |
| |
| fmt.Println(summer[:8]) |
注意:
数组是值类型,取其指针是有意义的:引用传递
而切片本身就是引用类型,当然也可以取其指针,但用处不大,了解即可
| months := [...]string{1: "January", 2: "February", 3: "March", 4: "April", 5: "May", 6: "June", 7: "July", 8: "August", 9: "September", 10: "October", 11: "November", 12: "December"} |
| |
| x:=&months |
| |
| summer := months[6:9] |
| y:=&summer |
| |
| fmt.Println((*x)[0:3]) |
| fmt.Println((*y)[0:3]) |
注意:
1、虽然字符串的底层就是字节切片,但是对于字符串进行切片操作得到的是一个新的字符串
2、对[]byte进行切片操作会生成一个新的[]byte,注意不是字符串哦
| msg := "hi吃鸡吗" |
| |
| |
| res:=msg[0:5] |
| fmt.Println(res) |
| fmt.Printf("%T\n",res) |
| |
| res1:=[]byte(msg) |
| fmt.Println(res1[0:3]) |
| fmt.Printf("%T\n",res1[0:3]) |
1、字符串转为切片
2、切片转切片
同数组一样,涉及到索引操作时,索引必须是非负数
(1)查询:
| 取最后一个元素 |
| var cityArray = []string{"北京", "上海", "深圳"} |
| fmt.Println(cityArray[0]) |
| fmt.Println(cityArray[len(cityArray)-1]) |
| fmt.Println(cityArray[-1]) |
(2)修改
| cityArray[1] = "魔都" |
| fmt.Println(cityArray) |
| cityArray[666] = "魔都" |
(3)增加
我们无法根据不存在的索引进行赋值操作来为切片添加元素,
| s:=[]int{111,222,333} |
| s[3]=444 |
需要我们使用内置的append函数,append函数的特点是:哪怕操作的切片是零值nil切片,都不需为其初始化值或用make开辟空间,就可以直接执行append功能开辟空间加添加元素一条龙服务
| var s []int |
| |
| s=append(s,111) |
| |
| |
| s=append(s,222,333,444) |
| |
| |
| s2:=[]int{555,666} |
| s=append(s,s2...) |
| fmt.Println(s) |
(4)删除
Go语言中并没有删除切片元素的专用方法,我们可以使用切片本身的特性来删除元素。 代码如下:
| func main() { |
| |
| a := []int{30, 31, 32, 33, 34, 35, 36, 37} |
| |
| a = append(a[:2], a[3:]...) |
| fmt.Println(a) |
| } |
总结一下就是:要从切片a中删除索引为index
的元素,操作方法是a = append(a[:index], a[index+1:]...)
(1)切片属于复合类型中的引用类型,复合类型中的引用类型不支持直接进行相等性判断,只能与nil直接进行相等性判断,
ps:引用类型所包含的真实值都来自于或者说间接引用于底层数组中的元素,而我们想比较的都是值而不是引用,比较引用也没有意义,以切片为例,其底层数组的元素都是可以随时被修改的,
| |
| var x = []int{11,22,33} |
| var y = []int{11,22,33} |
| fmt.Println(x == y) |
| |
| |
| var a []string |
| var b []int |
| var c = []int{} |
| |
| fmt.Println(a == nil) |
| fmt.Println(b == nil) |
| fmt.Println(c == nil) |
| fmt.Println(c != nil) |
| |
| |
| var x *int |
| fmt.Println(x == nil) |
| |
| var mmm = 666 |
| var y *int = &mmm |
| fmt.Println(y == nil) |
如果我们就想要比较像切片这种引用类型是否相等,可不可以呢?也可以,思路就是确保长度一样,然后遍历出对应的元素手动进行比较,前提是
I.待比较的两个切片中,对应的元素必须是同类型
II.待比较的两个切片中,对应的元素必须是可以进行相等性判断的
| func equal(x, y []string) bool { |
| if len(x) != len(y) { |
| return false |
| } |
| for i := range x { |
| if x[i] != y[i] { |
| return false |
| } |
| } |
| return true |
| } |
(2)切片不可以比大小,若想比较,可以编写函数依次取出元素进行比较,就像字符串比大小的底层原理一样
(3)注意:如果你需要测试一个slice是否是空的,使用len(s) == 0来判断,而不应该用s == nil来判断,如下
| |
| var s []int |
| s = nil |
| s = []int(nil) |
| |
| |
| s = []int{} |
| s = make([]int, 3)[3:] |
其实一个nil值的slice的行为和其它任意0长度的slice一样;例如reverse(nil)也是安全的。除了文档已经明确说明的地方,所有的Go语言函数应该以相同的方式对待nil值的slice和0长度的slice。
切片的遍历方式和数组是一致的,支持索引遍历和for range
遍历。
| s := []string{"aaa", "bbb", "ccc"} |
| |
| for i := 0; i < len(s); i++ { |
| fmt.Println(i, s[i]) |
| } |
| |
| for index, value := range s { |
| fmt.Println(index, value) |
| } |
首先我们来看一个问题:
| func main() { |
| a := []int{1, 2, 3, 4, 5} |
| b := a |
| fmt.Println(a) |
| fmt.Println(b) |
| b[0] = 1000 |
| fmt.Println(a) |
| fmt.Println(b) |
| } |
由于切片是引用类型,所以a和b其实都指向了同一块内存地址。修改b的同时a的值也会发生变化。
Go语言内建的copy()
函数可以迅速地将一个切片的数据复制到另外一个切片空间中,copy()
函数的使用格式如下:
| copy(destSlice, srcSlice []T) |
copy函数有两个参数:
- srcSlice: 源切片
- destSlice: 目标切片
举个例子:
| func main() { |
| |
| s1 := []int{1, 2, 3, 4, 5} |
| s2 := make([]int, 5, 5) |
| fmt.Println(s1) |
| fmt.Println(s2) |
| |
| |
| copy(s2, s1) |
| fmt.Println(s1) |
| fmt.Println(s2) |
| |
| |
| s2[0] = 666 |
| fmt.Println(s2) |
| fmt.Println(s1) |
| } |
切片在函数参数中的传递属于“引用传递”,为了让大家更加深刻理解切片的灵活之处,我们对比数组以及数组指针来看一下。
因为数组是值类型,传入函数后,是一个新的副本,在函数内修改并不会影响原数组,若想修改,那么必须传递数组的引用,我们可以传递数组指针
| |
| func reverse(s *[5]int) { |
| for i, j := 0, len(*s)-1; i < j; i, j = i+1, j-1 { |
| (*s)[i], (*s)[j] = (*s)[j], (*s)[i] |
| } |
| |
| } |
| |
| func main() { |
| x := [5]int{1, 2, 3, 4, 5} |
| fmt.Println(&x) |
| |
| |
| reverse(&x) |
| fmt.Println(x) |
| } |
但是指针是对数组整体进行引用,如果我们只想修改数组的某一部分,那么传递数组的切片再合适不过了,因为切片可以引用数组的全部也可以引用部分
| |
| func reverse(s []int) { |
| for i, j := 0, len(s)-1; i < j; i, j = i+1, j-1 { |
| s[i], s[j] = s[j], s[i] |
| } |
| } |
| func main() { |
| x := [5]int{1, 2, 3, 4, 5} |
| |
| |
| reverse(x[:]) |
| fmt.Println(x) |
| |
| |
| reverse(x[:2]) |
| fmt.Println(x) |
| |
| |
| reverse(x[2:]) |
| fmt.Println(x) |
| } |
先来复习一个重要的知识:
- %p对应切片名,打印出的地址是其对应底层数组第一个元素的地址
- %p对应&切片名,打印的才是切片本身的地址
| var names [5]string = [5]string{"egon","EGON","EGON666","tom666","TOM777"} |
| var xxx []string = names[2:] |
| |
| fmt.Printf("%p\n",xxx) |
| fmt.Printf("%p\n",&xxx) |
注意:如果切片xxx底层数组扩容了,因为xxx是指向底层数组的,所以xxx首元素地址肯定变,但是xxx本身的地址不会变,因为始终都是xxx这一个变量,举例如下
| |
| var x [9]int = [9]int{11, 22, 33, 44, 55, 66, 77, 88, 99} |
| var y = x[1:3:5] |
| fmt.Println(y) |
| |
| |
| fmt.Printf("%p\n", &x[1]) |
| fmt.Printf("%p\n", y) |
| |
| |
| y = append(y, 666) |
| y = append(y, 777) |
| fmt.Println(y) |
| fmt.Println(x) |
| |
| fmt.Printf("%p\n", y) |
| |
| |
| y = append(y, 888) |
| fmt.Println(y) |
| fmt.Printf("%p\n", y) |
| |
| |
| 答案:不变 |
扩容规则详解如下:
每个切片会指向一个底层数组,在添加新元素时,如果底层数组的容量够用就正常添加,否则,切片就会自动按照一定的策略进行“扩容”,此时go会为切片创建新的底层数组,然后将原数组元素copy到新数组。
“扩容”操作往往发生在append()
函数调用时,append()执行之后,底层的数组有可能就换了,所以我们通常都需要用原变量接收append函数的返回值,从而保证原变量指向新的底层数组
| |
| var numSlice []int |
| fmt.Printf("值:%v 长度:%d 容量:%d 地址:%p\n", numSlice, len(numSlice), cap(numSlice), numSlice) |
| |
| for i := 0; i < 10; i++ { |
| numSlice = append(numSlice, i) |
| fmt.Printf("值:%v 长度:%d 容量:%d 地址:%p\n", numSlice, len(numSlice), cap(numSlice), numSlice) |
| |
| } |
| |
| |
| 值:[] 长度:0 容量:0 地址:0x0 |
| 值:[0] 长度:1 容量:1 地址:0xc00010c020 |
| 值:[0 1] 长度:2 容量:2 地址:0xc00010c040 |
| 值:[0 1 2] 长度:3 容量:4 地址:0xc000118020 |
| 值:[0 1 2 3] 长度:4 容量:4 地址:0xc000118020 |
| 值:[0 1 2 3 4] 长度:5 容量:8 地址:0xc00011a040 |
| 值:[0 1 2 3 4 5] 长度:6 容量:8 地址:0xc00011a040 |
| 值:[0 1 2 3 4 5 6] 长度:7 容量:8 地址:0xc00011a040 |
| 值:[0 1 2 3 4 5 6 7] 长度:8 容量:8 地址:0xc00011a040 |
| 值:[0 1 2 3 4 5 6 7 8] 长度:9 容量:16 地址:0xc00011e000 |
| 值:[0 1 2 3 4 5 6 7 8 9] 长度:10 容量:16 地址:0xc00011e000 |
基于上述输出结果可以看出
| 1、append()函数将元素追加到切片的最后并返回该切片。 |
| 2、切片numSlice的容量按照1,2,4,8,16这样的规则自动进行扩容,每次扩容后都是扩容前的2倍,注意是之前的2倍,如果起始容量为3,那么接下来的扩容就是6、12、24... |
扩展阅读:切片的扩容策略
| 可以通过查看$GOROOT/src/runtime/slice.go源码,其中扩容相关代码如下: |
| |
| func growslice(et *_type, old slice, cap int) slice { |
| |
| ... |
| |
| |
| newcap := old.cap |
| doublecap := newcap + newcap |
| if cap > doublecap { |
| newcap = cap |
| } else { |
| |
| if old.len < 1024 { |
| newcap = doublecap |
| } else { |
| |
| |
| |
| for 0 < newcap && newcap < cap { |
| newcap += newcap / 4 |
| } |
| |
| |
| if newcap <= 0 { |
| newcap = cap |
| } |
| } |
| } |
| |
| |
| ... |
| } |
| 从上面的代码可以看出以下内容: |
| 1、首先判断,如果新申请容量(cap)大于2倍的旧容量(old.cap),最终容量(newcap)就是新申请的容量(cap)。 |
| 2、否则判断,如果旧切片的长度小于1024,则最终容量(newcap)就是旧容量(old.cap)的两倍,即(newcap=doublecap), |
| 3、否则判断,如果旧切片长度大于等于1024,则最终容量(newcap)从旧容量(old.cap)开始循环增加原来的1/4,即(newcap=old.cap,for {newcap += newcap/4})直到最终容量(newcap)大于等于新申请的容量(cap),即(newcap >= cap) |
| 4、如果最终容量(cap)计算值溢出,则最终容量(cap)就是新申请容量(cap)。 |
需要注意的是
- 1、切片扩容还会根据切片中元素的类型不同而做不同的处理,比如int和string类型的处理方式就不一样。
- 2、每次扩容会涉及到数组的copy,然后生成新的数组(slice指向新的数组),这样会给系统带来额外的开销,通常我们在创建slice的时候,建议使用make函数,更具业务场景给定一个合适的cap大小,避免slice因为扩容而发生底层数组的copy。
| |
| var names []string = make([]string,3,3) |
| names[0] = "egon" |
| names[1] = "bigegon" |
| names[2] = "smallegon" |
| |
| |
| var names []string = []string{"aaa","bbbb","ccc"} |
| fmt.Println(names[0]) |
| func main() { |
| var a = make([]string, 5, 10) |
| for i := 0; i < 10; i++ { |
| a = append(a, fmt.Sprintf("%v", i)) |
| } |
| fmt.Println(a) |
| } |
- 3、请使用内置的
sort
包对数组var a = [...]int{3, 7, 8, 9, 1}
进行排序
利用内置的包sort,sort接收的是切片类型,切片是对数组的引用,所以对切片排序就是对数组排序啦
| func main(){ |
| var a = [...]int{3, 7, 8, 9, 1} |
| sort.Ints(a[:]) |
| fmt.Println(a) |
| } |
| 切片类型x[start:stop:max] 顾头不顾尾,不指定max,不写max则默认max为底层数组最大索引,max-star即容量, |
| |
| var xxx = [5]int{111,222,333,444,555} |
| yyy:=xxx[0:3] |
| fmt.Println(len(yyy),cap(yyy)) |
| |
| |
| mons:=[...]string{1: "January", 2: "February", 3: "March", 4: "April", 5: "May", 6: "June", 7: "July", 8: "August", 9: "September", 10: "October", 11: "November", 12: "December"} |
| fmt.Println(len(mons)) |
| |
| mon1:=mons[2:5:7] |
| mon2:=mons[1:3] |
| fmt.Println(mon1,len(mon1),cap(mon1)) |
| fmt.Println(mon2,len(mon2),cap(mon2)) |
| 对切片再进行切片操作,结束位置不能超过切片的容量,也就是说对谁切,就以谁为准,这句话适用于切片操作的一切 |
| |
| var xxx = make([]int,3,5) |
| xxx[0]=111 |
| xxx[1]=222 |
| xxx[2]=333 |
| |
| fmt.Println(len(xxx),cap(xxx)) |
| |
| fmt.Println(xxx[0:3]) |
| fmt.Println(xxx[0:4]) |
| fmt.Println(xxx[0:5]) |
| fmt.Println(xxx[0:6]) |
- 6、切片slice的元素为map类型:切记一点,引用类型的零值都是nil,请说出下述代码的结果
| |
| var mapSlice []map[string]int |
| fmt.Println(mapSlice == nil) |
| |
| |
| var mapSlice=make([]map[string]int,8,8) |
| fmt.Println(mapSlice == nil) |
| mapSlice[0]["数学"] = 100 |
| |
| |
| |
| mapSlice[0]= map[string]int{"数学":99,"英语":98,"语文":100} |
| fmt.Println(mapSlice) |
| |
| var sliceMap=make(map[string][]string) |
| sliceMap["中国"]=make([]string,3) |
| sliceMap["中国"][0]="北京" |
| sliceMap["中国"][1]="上海" |
| sliceMap["中国"][2]="深圳" |
| |
| sliceMap["美国"]=make([]string,3) |
| sliceMap["美国"][0]="纽约" |
| sliceMap["美国"][1]="洛杉矶" |
| sliceMap["美国"][2]="芝加哥" |
| |
| fmt.Println(sliceMap["中国"]) |
| fmt.Println(sliceMap["美国"]) |
| |
| for k,v:=range sliceMap{ |
| fmt.Println(k,v) |
| |
| |
| |
| } |
- 8、使用slice来模拟stack操作,入栈即向slice中append元素,出栈则通过收缩slice,弹出栈顶的元素:
| stack := []int{11, 22, 33} |
| |
| |
| stack = append(stack, 666) |
| fmt.Println(stack) |
| |
| |
| stack = stack[:len(stack)-1] |
| fmt.Println(stack) |
- 9、通过内置的copy函数将后面的子slice向前依次移动一位
| |
| copy(list[i:],list[i+1:]) |
| |
| 例 |
| list := []int{1,2,3,4,5,6} |
| copy(list[1:],list[1+1:]) |
| fmt.Println(list) |
| func remove(slice []int, i int) []int { |
| |
| slice[i] = slice[len(slice)-1] |
| |
| return slice[:len(slice)-1] |
| } |
- 11、输入切片和输出切片共用一个底层数组,这可以避免分配另一个数组,不过原来的数据将可能会被覆盖:例如
| func clearEmpty(strings []string) []string { |
| i := 0 |
| for _, s := range strings { |
| if s != "" { |
| strings[i] = s |
| i++ |
| } |
| } |
| return strings[:i] |
| } |
| |
| srcSlice := []string{"egon1", "", "egon3"} |
| dstSlice := clearEmpty(srcSlice) |
| |
| fmt.Printf("%q\n", dstSlice) |
| fmt.Printf("%q\n", srcSlice) |
同样的,使用append也能实现同样的功能:
| func clearEmpty2(strings []string) []string { |
| out := strings[:0] |
| for _, s := range strings { |
| if s != "" { |
| out = append(out, s) |
| } |
| } |
| return out |
| } |
| |
| srcSlice := []string{"egon1", "", "egon3"} |
| |
| dstSlice := clearEmpty2(srcSlice) |
| |
| fmt.Printf("%q\n", dstSlice) |
| fmt.Printf("%q\n", srcSlice) |
无论如何实现,以这种方式重用一个slice一般都要求最多为每个输入值产生一个输出值,事实上很多这类算法都是用来过滤或合并序列中相邻的元素。
这种slice用法是比较复杂的技巧,虽然使用到了slice的一些技巧,但是对于某些场合是比较清晰和有效的。