数组大家都知道是具有「固定长度及类型的序列集合」,但是golang中又引入了「切片」,语法上看起来还和数组差不多,为什么会引入这些呢?切片和数组到底有什么区别呢?接下来咱们来一个一个的看
var arr [5]int = [5]int{1,2,3,4,5}
上述语句表示的意思是,我们来定义一个变量arr 为5个长度的int的数组类型,也就是[5]int,同时给赋值上了初始值 1、2、3、4、5,内存分布如图
紧密挨在一起的数据结构集合
如果定义数组的方法是
arr := new([4]int)
那么arr的数据类型为*[4]int,而不是[4]int
当然数组的长度4如果是不固定的,可以用...的方式代替
q := [...]int{1, 2, 3}
数组的循环在golang中有一个特有的语法,就是 for range
var arr [4]int = [4]int{1, 2, 3, 4}
for i, v := range arr {
fmt.Printf("数组中的第%v项, 值是%v\n", i, v)
}
//输出结果
数组中的第0项, 值是1
数组中的第1项, 值是2
数组中的第2项, 值是3
数组中的第3项, 值是4
常用方法是「len()」 方法和 「cap()」 方法
但是「在数组中,这两个值永远相同」,所以在这里咱们不多做考虑,在后面切片中再详细阐述。
切片之所以会诞生,是因为golang中数组存在很大的两个问题
type slice struct {
array unsafe.Pointer //指向一个数组的指针
len int //当前 slice 的长度
cap int //当前 slice 的容量
}
比如我们定义了一个切片
s := make([]int, 3, 5)
s[0] = 1
s[1] = 2
s[2] = 3
那么以上变量在内存中的数据结构如下图所示
所以由上面的分析可以看出来,「切片是依赖于数组的,而且是一个指向数组的指针」,既然切片是指针类型,那么在作为参数传递的时候,肯定是引用类型,不需要重新copy一份而造成空间浪费。
我们上面说过切片是依赖于数组的,所以切片的截取是基于数组进行截取的,截取这块我们直接看例子就行,看例子记住一个原则即可「左包含,右不包含」
a1 := [...]int{1, 2, 3, 4, 5, 6, 7, 8, 9}
s4 := a1[2:4] //输出结果[3 4]
s5 := a1[:4] //输出结果[1 2 3 4]
s6 := a1[2:] //输出结果[3 4 5 6 7 8 9]
s7 := a1[:] //输出结果[1 2 3 4 5 6 7 8 9]
以上例子都符合上面提到的「左包含,右不包含原则」
长度很好理解,简单理解就是「元素的个数」,容量相对难理解一些「在切片引用的底层数组中从切片的第一个元素到数组最后一个元素的长度就是切片的容量」
我们还是来直接看例子
a1 := [...]int{1, 2, 3, 4, 5, 6, 7, 8, 9}
s5 := a1[:4] //[1 2 3 4]
s6 := a1[2:] //[3 4 5 6 7 8 9]
s7 := a1[:] //[1 2 3 4 5 6 7 8 9]
fmt.Printf("len(s5):%d cap(s5):%d\n", len(s5), cap(s5)) //4 9
fmt.Printf("len(s6):%d cap(s6):%d\n", len(s6), cap(s6)) //7 7
fmt.Printf("len(s7):%d cap(s7):%d\n", len(s7), cap(s7)) //9 9
make方法主要是用于切片的生成,比较简单,比如下面的例子就是我们来定义一个长度为5,容量为10的切片。
s1 := make([]int,5,10)
fmt.Printf("s1:%v len(s1):%d cap(s1):%d\n", s1, len(s1), cap(s1))
// 输出结果
//s1:[0 0 0 0 0] len(s1):5 cap(s1):10
append主要是用于切片的追加。我们还是直接看例子
var s = []int{1, 2, 3, 4}
fmt.Println(s)
fmt.Printf("len:%d, cap:%d", len(s), cap(s))
//输出结果
[1 2 3 4]
len:4, cap:4
我们可以看到定义了一个切片,初始化了4个元素,切片此时的长度和容量都为4
var s = []int{1, 2, 3, 4}
s = append(s, 5) //给切片s追加一个元素 5
fmt.Println(s)
fmt.Printf("len:%d, cap:%d\n", len(s), cap(s))
//输出结果
[1 2 3 4 5]
len:5, cap:8
分析:长度由4变成5,我们很好理解;容量为什么会从4变成8呢?「这个是因为go语言对切片的自动扩容机制,append追加,如果cap不够的时候,go底层会把底层数组替换,是go语言的一套扩容策略。」 简单说这个扩容机制就是「如果不够,就在以前的基础上翻倍,如果超过1M,则+1M」,跟redis的bitmap类型的扩容机制是一样的
func main() {
var s = []int{1, 2, 3}
modifySlice(s)
fmt.Println(s) // 打印 [1 2 3]
}
func modifySlice(s []int) {
s = append(s, 4)
s[0] = 4
}
这个坑在面试中经常会遇到,当 slice 作为函数参数时,「如果在函数内部发生了扩容,这时再修改 slice 中的值是不起作用的」,因为修改发生在新的 array 内存中,对老的 array 内存不起作用。
s1 := []int{1, 2, 3, 4}
s2 := []int{5, 6}
s3 := append(s1, s2...) // ...表示拆开,将切片的值作为追加的元素
fmt.Println(s3)
//输出结果
//[1 2 3 4 5 6]
//定义切片s1
s1 := []int{1, 2, 3}
//第一种方式:直接声明变量 用=赋值
//s2切片和s1引用同一个内存地址
var s2 = s1
//第二种方式:copy
var s3 = make([]int, 3)
copy(s3, s1) //使用copy函数将 参数2的元素复制到参数1
s1[0] = 11
fmt.Printf("s1:%v s2:%v s3:%v",s1, s2, s3) //s1和s2是[11 2 3] s3是[1 2 3]
我们发现s1和s2是[11 2 3] s3是[1 2 3],说明copy方法是复制了一份,开辟了新的内存空间,不再引用s1的内存地址,这就是两者的区别。