go语言的数组与切片
Posted changfangxing
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了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
", 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
", 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
", a)
for i := 0; i < 10; i++ {
a = append(a, 100+i)
fmt.Printf("追加完毕,循环次数:%d, 切片a此时的内存地址:%p, 切片a的容量:%d
", 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
", a, b)
a[2] = 999
fmt.Printf("a : %d, b: %d
", 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
", a, b)
copy(b, a)
a[2] = 999
fmt.Printf("修改之后 a : %d, b: %d
", 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]
以上是关于go语言的数组与切片的主要内容,如果未能解决你的问题,请参考以下文章