002 go array和slice的区别
相同点
都是属于集合类的类型,用来存储同一种类型的数据或者元素
不同点
数组的长度固定,数组是值类型(值类型有基本数据类型,结构体类型),数组的空余位置用0填补,不允许数组越界。
slice的值长度可变,属于引用类型(引用类型:字典类型,通道类型,函数类型)
切片是引用传递,所以它们不需要使用额外的内存并且比使用数组更有效率。
如果传递的是引用类型,那么就是“传引用”,如果传递的是值类型,那么就是“传值”(会把以前的数据复制一遍)
数组长度在声明的时候就必须给定,并且之后不会再改变。
slice类型的字面量中只有元素类型,没有长度。切片长度可以自动随着元素数量的增长而增长,但不会随着元素数量的减少而减少。
数组的容量=长度,不可改变。
go中有没有数组
有
golang 中的数组
1.数组的创建
package main
import "fmt"
func main() {
var arr1 = [...]int{1, 2, 3, 4} //[...]默认为元素的数量即为数组的长度
fmt.Println(len(arr1)) //4
arr1[4] = 5 //panic 数组越界
fmt.Println(arr1)
var arr2 = [10]int{1, 2, 3, 4}
fmt.Println(arr2) //[1 2 3 4 0 0 0 0 0 0]
}
2.数组是值传递
package main
import "fmt"
func main() {
var arr = [10]int{1, 2, 5, 6, 8, 9}
fmt.Println(arr) //[1,2,5,6,8,9,0,0,0,0]
//验证数组是值拷贝传递
AddOne(arr)
fmt.Println(arr) //[1,2,5,6,8,9,0,0,0,0]
}
func AddOne(arr [10]int) {
arr[9] = 999999
fmt.Println(arr) //[1,2,5,6,8,9,0,0,0,999999]
}
3.数组长度固定
package main
import "fmt"
func main() {
var arr1 = [...]int{1, 2, 3, 4} //[...]默认为元素的数量即为数组的长度
fmt.Println(len(arr1)) //4
arr1[4] = 5
arr1 = append(arr1, 5)
fmt.Println(arr1)
}
Output
# command-line-arguments
.\main.go:8:6: invalid array index 4 (out of bounds for 4-element array)
.\main.go:9:15: first argument to append must be slice; have [4]int
golang 中的切片(slice)
1.Slice 介绍
一个slice由三个部分构成:指针、长度和容量。指针指向第一个slice元素对应的底层数组元素的地址。长度对应slice中元素的数目;长度不能超过容量,容量一般是从slice的开始位置到底层数据的结尾位置。通过len和cap函数分别返回slice的长度和容量。
以下是slice源码结构
type slice struct {
array unsafe.Pointer
len int
cap int
}
slice是一个特殊的引用类型,但是它自身也是个结构体
属性len表示可用元素数量,读写操作不能超过这个限制,不然就会panic
属性cap表示最大扩张容量,当然这个扩张容量也不是无限的扩张,它是受到了底层数组array的长度限制,超出了底层array的长度就会panic
一旦一个slice无法容纳更多的元素,go会扩容,但不会改变原来的切片,而是会生成一个容量更大的slice,然后把原有的元素和新元素一起copy到新的slice中。一般情况下,可简单认为新slice的容量是旧slice容量的2倍。
但是当原slice的长度>= 1024,go会以原容量的1.25倍作为新容量的基准,新容量基准会不断与1.25相乘,直到结果不小于原长度与要追加的元素数量之和。最终,新容量要比新长度要大 一些。
如果一次追加的元素过多,以至于使新长度比原容量的2倍还要大,那么新容量就会以新长度为基准。
一个slice的底层数组永远不会倍替换,虽然在扩容的时候,go一定会生成新的底层数组,但同时也生成了新的 slice。只是把新slice作为了新底层数组的窗口,而没有对原slice及其底层数组做任何改动。
在无需扩容的时候,append函数返回的是指向原底层数组的新slice。而在需要扩容的时候,append函数返回的是指向新底层数组的新的slice
slice扩容策略
- 首先判断,如果新申请容量(cap)大于二倍的旧容量(old.cap),最终容量(newcap)就是新申请的容量(cap)
- 否则判断,如果旧切片的长度小于1024,则最终容量(newcap)就是旧容量(old.cap的两倍),即(newcap=doublecap)
- 否则判断,如果旧切片长度大于等于1024,则最终容量(newcap)从旧容量(old.cap)开始循环增加原来的四分之一,即(newcap=old.cap,for{newcap+=newcap/4})知道最终容量(newcap)大于等于新申请的容量(cap),即(newcap >=cap)
- 如果最终容量(cap)计算值溢出,则最终容量(cap)就是新申请容量(cap)
确切地说,一个切片的底层数组永远不会被替换。为什么?虽然在扩容的时候 Go 语言一定会生成新的底层数组,但是它也同时生成了新的切片。
它只是把新的切片作为了新底层数组的窗口,而没有对原切片,及其底层数组做任何改动。
请记住,在无需扩容时,append函数返回的是指向原底层数组的新切片,而在需要扩容时,函数返回的是指向新底层数组的新切片。所以,严格来讲,“扩容”这个词用在这里虽然形象但并不合适。
1.slice的声明和创建
2.切片作为参数传递
package main
import (
"fmt"
)
func main() {
var slice = make([]int, 3, 5) //len=3,cap=5
fmt.Println(slice) //[0,0,0]
slice2 := slice[:5] //slice实现了对slice的扩容,切片长度变为5
fmt.Println(slice2) //[0,0,0,0,0]
slice[0] = 999 //这里slice和slice的index=0位置都是999 因为他们引用的底层数组的index=0位置都是999
fmt.Println(slice)
fmt.Println(slice2)
AddOne(slice) //[8888,0,0]
fmt.Println(slice) //[8888,0,0]
fmt.Println(slice2) //[8888,0,0,0]
}
func AddOne(s []int) {
s[0] = 8888
fmt.Println(s)
}
总结
(1)go是有数组的,只是平时用切片比较多。数组大小一旦创建就不能改变,数组长度大于元素个数的时候会用0补位,这跟其他语言是相通的。
(2)切片slice可以看作是对数组的一切操作,它是一个引用数据类型,其数据结构包括底层数组的地址,以及元素可操作长度len或可扩容长度cap。
(3)要想突破slice的扩容cap限制进行无限扩容就需要使用append()函数进行操作。如果append追加的元素后slice的总长度不超过底层数组的总长度,那么slice引用的地址不会发生改变,反之引用地址会 变成新的数组的地址。
(4)slice是一个抽象的概念,它存在的意义在于方便对一个顺序结构进行一些方便操作,例如查找,排序,追加等等,这个类似于python的list。