go语言的数组与切片
go语言的数组与切片
如果有编程基础的话,提到数组我们肯定不会陌生,所谓数组,就是有数的元素序列,我们可以认为数组是有限的相同类型数据的集合。
数组长度是固定的,所以这会带来很多局限性。
比如说只接受相同类型的元素,长度固定等等。
那么切片的出现,则为golang解决了以上数组所带来的不便。
切片(slice)是一个引用类型,它是拥有相同类型的元素并且长度可变的序列。
基于数组类型做的一层封装,支持扩容。
切片的内部结构包含地址、长度、容量。它主要用于服务一块数据的集合。
下面我们来看一看数组的声明:
var arr1 [5]int // 声明一个长度为5的数组 var arr2 = [5]int{1, 2, 3, 4, 5} //声明一个数组,并初始化
然后再看一看切片的声明:
var slice1 []string //声明一个字符串切片 var intSlice []int //定义一个整型切片 var boolSlice = []bool{true, false} //声明一个布尔切片并初始化
数组的遍历
第一种,for循环
var arr1 = [5]int{1,2,3,4,5} for i := 0; i < len(arr1); i++ { fmt.Println(arr1[i]) }
第二种:for-range
var arr1 = [5]int{1,2,3,4,5} for i, v := range arr1 { fmt.Printf("第%d个元素,值为:%d\n", i, v) }
数组类型
数组的类型实际上是值类型,所以可以通过new()
来创建数组:
var arr1 = new([5]int)
通过new创建的数组和var arr2 [5]int
的区别是什么呢?
arr1的类型是*[5]int, 而arr2的类型则是[5]int
var arr1 = new([5]int) var arr2 = [5]int{1, 2, 3, 4, 5} fmt.Printf("arr1 type : %T, arr2 type : %T\n", arr1, arr2)
输出如下:
arr1 type : *[5]int, arr2 type : [5]int
这样的结果是什么呢?就是当把一个数组赋值给另一个数组后,需要再做一次数组内存的拷贝操作。
例如:
arr2 := *arr1 arr2[2] = 100
切片
切片的底层就是数组,所以我们可以基于数组来定义切片。
举个例子:
func main() { var ar1 = [5]int{1, 2, 3, 4, 5} ar2 := ar1[:3] fmt.Printf("ar1 type : %T, ar2 type : %T", ar1, ar2) }
输出如下:
ar1 type : [5]int, ar2 type : []int
基于切片再得到切片
func main() { var ar1 = [5]int{1, 2, 3, 4, 5} ar2 := ar1[:3] fmt.Println(ar2) ar3 := ar2[0:4] fmt.Println(ar3) }
输出:
[1 2 3] [1 2 3 4]
使用make构造切片
make函数是内置的,格式如下:
make([]T,size, cap)
T就是创建切片的类型,size是切片中元素的数量,cap是切片的容量。
举个例子:
func main() { a := make([]int, 2, 10) a[1] = 10 fmt.Println(a) //[0,10] fmt.Println(len(a)) //元素数量2 fmt.Println(cap(a)) //容量是10 }
但注意!虽然切片a的容量是10,但是这并不意味着我们可以随意的给切片a赋值。
比如说:我定义了一个切片a := make([]int, 2, 10)
,a[1]= 10
但是如果这时我让a[2] = 11
则会报错。
panic: runtime error: index out of range [2] with length 2
使用append为切片追加数据
所以这里涉及到如何为切片添加元素,我们可以用系统自带的append函数,来为切片添加元素。
每个切片都会指向一个底层数组,这个数组会容纳一定数量的元素。
当底层数组不能容纳新增的元素时,就会发生扩容,那么这时候切片指向的底层数组就会更换。
下面我们来看一个例子:
func main() { a := make([]int, 2, 10) a[0] = 1 //第一个元素为1 a[1] = 10 //第二个元素为10 a = append(a, 11) //此时我们追加一个新的元素,11 fmt.Println(a) //[1,10,11] }
使用appen()函数即可让切片添加新的元素。
那么问题来了,如果我们不停地追加新的元素,切片指向的数组什么时候会改变呢?下面再看一段代码:
func main() { a := make([]int, 2, 5) a[0] = 1 a[1] = 10 fmt.Printf("切片a循环前的内存地址:%p\n", a) for i := 0; i < 10; i++ { a = append(a, 100+i) fmt.Printf("追加完毕,循环次数:%d, 切片a此时的内存地址:%p, 切片a的容量:%d\n", i, a, cap(a)) } fmt.Println(a) }
输出结果:
切片a循环前的内存地址:0xc0000a8030 追加完毕,循环次数:0, 切片a此时的内存地址:0xc0000a8030, 切片a的容量:5 追加完毕,循环次数:1, 切片a此时的内存地址:0xc0000a8030, 切片a的容量:5 追加完毕,循环次数:2, 切片a此时的内存地址:0xc0000a8030, 切片a的容量:5 追加完毕,循环次数:3, 切片a此时的内存地址:0xc0000ac000, 切片a的容量:10 追加完毕,循环次数:4, 切片a此时的内存地址:0xc0000ac000, 切片a的容量:10 追加完毕,循环次数:5, 切片a此时的内存地址:0xc0000ac000, 切片a的容量:10 追加完毕,循环次数:6, 切片a此时的内存地址:0xc0000ac000, 切片a的容量:10 追加完毕,循环次数:7, 切片a此时的内存地址:0xc0000ac000, 切片a的容量:10 追加完毕,循环次数:8, 切片a此时的内存地址:0xc0000ae000, 切片a的容量:20 追加完毕,循环次数:9, 切片a此时的内存地址:0xc0000ae000, 切片a的容量:20 [1 10 100 101 102 103 104 105 106 107 108 109] 切片a此时的内存地址:0xc0000ae000
我们在切片初始化的时候指定容量为5,由上面运行结果可以知道,在i循环到2后,切片本身的元素数量已经达到了5,也就是说,下一次再添加元素的时候就会发生扩容,然后底层指向的数组会改变。
所以,在循环到3点时候,a的容量由5变为10,此时内存地址也发生改变。每次扩容都是上一次的2倍大。
追加多个元素
append()函数会将元素添加到切片最后,并返回该切片,同时也支持追加多个元素。
下面代码示例:
func main() { a := make([]int, 2, 5) a = append(a, 1, 2, 3, 4, 5) //追加多个元素 b := []int{6, 6, 6} //我们再定义一个切片 a = append(a, b...) //追加切片 fmt.Print(a) }
输出如下:
[0 0 1 2 3 4 5 6 6 6]
由此我们可以看到,前两个元素0,0是切片a本身定义的元素,由于初始化没有赋值,所以默认是0,接着是1,2,3,4,5,最后三个6则是切片b追加到最后。
切片的扩容策略
可以通过查看$GOROOT/src/runtime/slice.go
源码,其中有个函数叫做growslice
那么在这个函数中,源码上面就写了很多的注释,如下所示:
// growslice handles slice growth during append.
// It is passed the slice element type, the old slice, and the desired new minimum capacity,
// and it returns a new slice with at least that capacity, with the old data
// copied into it.
// The new slice‘s length is set to the old slice‘s length,
// NOT to the new requested capacity.
// This is for codegen convenience. The old slice‘s length is used immediately
// to calculate where to write new values during an append.
// TODO: When the old backend is gone, reconsider this decision.
// The SSA backend might prefer the new length or to return only ptr/cap and save stack space.
那它究竟是怎么扩容的呢?我们往下看:
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 } } }
由代码我们可以得知,每次扩容时的条件是什么,并不是每次扩容都会扩大到2倍,如果旧切片长度小于1024,那最终容量就是old cap的两倍,否则就会增加原来的四分之一,直到newcap >= cap.
需要注意的是,切片扩容还会根据切片中元素的类型不同而做不同的处理,比如int
和string
类型的处理方式就不一样。
使用copy()函数
切片是引用类型,所以如果我们定义了一个切片a,然后切片 b = a,那此时,a和b都是指向了同一块内存地址,修改a的时候也会修改b。
下面我们来看一段代码:
func main() { a := []int{1, 2, 3, 4} b := a fmt.Printf("a : %d, b: %d\n", a, b) a[2] = 999 fmt.Printf("a : %d, b: %d\n", a, b) }
a : [1 2 3 4], b: [1 2 3 4] 修改之后 a : [1 2 999 4], b: [1 2 999 4]
那如何避免这种情况发生呢?这时候我们就要用到go内置的copy()函数了。
Go语言内建的copy()
函数可以迅速地将一个切片的数据复制到另外一个切片空间中,copy()
函数的使用格式如下:
copy(destSlice, srcSlice []T)
func main() { a := []int{1, 2, 3, 4} b := make([]int, 4, 4) fmt.Printf("修改之前 a : %d, b: %d\n", a, b) copy(b, a) a[2] = 999 fmt.Printf("修改之后 a : %d, b: %d\n", a, b) }
输出如下:
修改之前 a : [1 2 3 4], b: [0 0 0 0]
修改之后 a : [1 2 999 4], b: [1 2 3 4]
切片删除元素
go语言并没有删除切片元素的专有方法,但是可以通过索引来删除切片中的元素。
func main() { // 从切片中删除元素 a := []int{30, 31, 32, 33, 34, 35, 36, 37} // 要删除索引为1,2,3的元素 a = append(a[:1], a[4:]...) fmt.Println(a) }
[30 34 35 36 37]
去掉最后一个元素:
slice1 = slice1[:len(slice1)-1]