引子
因为数组的长度是固定的并且数组长度属于类型的一部分,所以数组有很多的局限性。 例如:
func arraySum(x [3]int) int{ sum := 0 for _, v := range x{ sum = sum + v } return sum }
这个求和函数只能接受[3]int
类型,其他的都不支持。 再比如,
a := [3]int{1, 2, 3}
数组a中已经有三个元素了,我们不能再继续往数组a中添加新元素了。
切片
切片(Slice)是一个拥有相同类型元素的可变长度的序列。它是基于数组类型做的一层封装。它非常灵活,支持自动扩容。
切片是一个引用类型,它的内部结构包含地址
、长度
和容量
。切片一般用于快速地操作一块数据集合。
值类型与引用型的区别:
底层数组的修改会影响切片
切片的修改也会影响底层数组
切片的定义
var name []T // 切片拥有数组的所有特性
- name:表示变量名
- T:表示切片中的元素类型
切片的声明
package main import "fmt" func main() { // 切片声明方式一 直接声明 var s = []int{1,2,3} fmt.Println(s) // [1 2 3] fmt.Printf("%T ", s) // []int // 切片声明方式二 从数组中得到切片 var a = [3]int{1,2,3} c := a[0:3] fmt.Println(c) // [1 2 3] fmt.Printf("%T ", c) // []int }
切片的长度和容量
切片拥有自己的长度和容量,我们可以通过使用内置的len()函数求长度,使用内置的cap()函数求切片的容量。
package main import "fmt" func main() { var a1 = [...]string{"北京", "上海", "深圳", "成都", "广州", "青岛"} s1 := a1[1:4] // 切片的大小 (切片内目前元素的数量) fmt.Println(len(s1)) // 3 // 切片的容量 (底层数组最大能放多少元素) fmt.Println(cap(s1)) // 5 }
切片的本质
切片的本质就是对底层数组的封装,它包含了三个信息:底层数组的指针、切片的长度(len)和切片的容量(cap)。
举个例子,现在有一个数组a := [8]int{0, 1, 2, 3, 4, 5, 6, 7}
,切片s1 := a[:5]
,相应示意图如下。切片s2 := a[3:6]
,相应示意图如下:
append()方法为切片添加元素
Go语言的内建函数append()
可以为切片动态添加元素。 每个切片会指向一个底层数组,这个数组能容纳一定数量的元素。当底层数组不能容纳新增的元素时,切片就会自动按照一定的策略进行“扩容”,此时该切片指向的底层数组就会更换。“扩容”操作往往发生在append()
函数调用时。 举个例子:
func main() { //append()添加元素和切片扩容 var numSlice []int for i := 0; i < 10; i++ { numSlice = append(numSlice, i) fmt.Printf("%v len:%d cap:%d ptr:%p ", numSlice, len(numSlice), cap(numSlice), numSlice) } }
[0] len:1 cap:1 ptr:0xc0000a8000 [0 1] len:2 cap:2 ptr:0xc0000a8040 [0 1 2] len:3 cap:4 ptr:0xc0000b2020 [0 1 2 3] len:4 cap:4 ptr:0xc0000b2020 [0 1 2 3 4] len:5 cap:8 ptr:0xc0000b6000 [0 1 2 3 4 5] len:6 cap:8 ptr:0xc0000b6000 [0 1 2 3 4 5 6] len:7 cap:8 ptr:0xc0000b6000 [0 1 2 3 4 5 6 7] len:8 cap:8 ptr:0xc0000b6000 [0 1 2 3 4 5 6 7 8] len:9 cap:16 ptr:0xc0000b8000 [0 1 2 3 4 5 6 7 8 9] len:10 cap:16 ptr:0xc0000b8000
append()
函数将元素追加到切片的最后并返回该切片。
切片numSlice的容量按照1,2,4,8,16这样的规则自动进行扩容,每次扩容后都是扩容前的2倍
切片的扩容策略
newcap := old.cap doublecap := newcap + newcap 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 } // Set newcap to the requested cap when // the newcap calculation overflowed. if newcap <= 0 { newcap = cap } } }
- 首先判断,如果新申请容量(cap)大于2倍的旧容量(old.cap),最终容量(newcap)就是新申请的容量(cap)。
- 否则判断,如果旧切片的长度小于1024,则最终容量(newcap)就是旧容量(old.cap)的两倍,即(newcap=doublecap),
- 否则判断,如果旧切片长度大于等于1024,则最终容量(newcap)从旧容量(old.cap)开始循环增加原来的1/4,即(newcap=old.cap,for {newcap += newcap/4})直到最终容量(newcap)大于等于新申请的容量(cap),即(newcap >= cap)
- 如果最终容量(cap)计算值溢出,则最终容量(cap)就是新申请容量(cap)。
需要注意的是,切片扩容还会根据切片中元素的类型不同而做不同的处理,比如int
和string
类型的处理方式就不一样
copy()函数复制切片
Go语言内建的copy()
函数可以迅速地将一个切片的数据复制到另外一个切片空间中,copy()
函数的使用格式如下:
copy(destSlice, srcSlice []T)
- srcSlice: 数据来源切片
- destSlice: 目标切片
使用
package main import "fmt" func main{ test() } func test() { a := []int{1,2,3} b := a // 直接赋值 var c []int // 定义一个切片,还没有申请内存 c = make([]int, 3, 3) // 为c切片申请内存 make([]T, len, cap) copy(c, a) // 参数2是被copy对象 b[0] = 100 fmt.Println(a) // [100 2 3] fmt.Println(b) // [100 2 3] fmt.Println(c) // [1 2 3] }
var c []int // 只定义,没初始化,没有内存
var c = []int{} // 定义,初始化,有内存
从切片中删除元素
Go语言中并没有删除切片元素的专用方法,我们可以使用切片本身的特性来删除元素。 代码如下:
package main import "fmt" func main{ test1() } func test1() { a := []int{1,2,3,4,5,6,7} a = append(a[:2], a[3:]...) fmt.Println(a) // [1 2 4 5 6 7] }
总结一下就是:要从切片a中删除索引为index
的元素,操作方法是a = append(a[:index], a[index+1:]...)
题
package main import "fmt" func main{ test2() } func test2() { a := [...]int{1,2,3,4,5,6,7} b := a[:] b[0] = 100 fmt.Println(a[0]) // 100 因为切片是引用类型 c := a[2:5] d := c[:5] fmt.Println(c) // [3 4 5] fmt.Println(len(c)) // 3 fmt.Println(cap(c)) // 5 fmt.Println(d) // [3 4 5 6 7] fmt.Println(len(d)) // 5 fmt.Println(cap(d)) // 5 }
c与d内存地址一样,因为c与d的第一个元素是同一个