对于数组的优点
- 在 Go 中,与 C 数组变量隐式作为指针使用不同,Go 数组是值类型,赋值和函数传参操作都会复制整个数组数据。
- 但是把第一个大数组传递给函数会消耗很多内存,采用切片的方式传参可以避免上述问题。
- 切片是引用传递,所以它们不需要使用额外的内存并且比使用数组更有效率。
底层结构
切片是对数组一个连续片段的引用,所以切片是一个引用类型。
当创建切片的时候,发现大小只为24,原因就是他本质是一个结构体,存放着3个字段
type slice struct {
// 数组指针
array unsafe.Pointer
// 长度
len int
// 容量
cap int
}
nil切片 和 空切片
- nil 切片的指针指向 nil。
- 空切片指向的地址不是nil,指向的是一个内存地址,但是它没有分配任何内存空间,即底层元素包含0个元素。
- 不管是使用 nil 切片还是空切片,对其调用内置函数 append,len 和 cap 的效果都是一样的。
扩容原理
扩容策略
- 首先判断,如果新申请容量大于2倍的旧容量,最终容量就是新申请的容量;
- 否则判断,如果旧切片的长度小于1024,则最终容量就是旧容量的两倍;
- 否则判断,如果旧切片长度大于等于1024,则最终容量从旧容量开始循环增加原来的 1/4,直到最终容量大于等于新申请的容量。
- 如果最终容量计算值溢出,则最终容量就是新申请容量
扩容策略的小问题
第一个例子
func main() {
// 新建一个切片
s := []int{5}
// 给s中添加7和9
s = append(s,7)
s = append(s,9)
// 给s中添加11,赋给x
x := append(s,11)
// 给y中添加12,赋给y
y := append(s,12)
// 输出
fmt.Println(s,x,y)
}
输出结果:
第二个例子
func main(){
// 新建一个切片
s := []int{5,7,9}
// 给s中添加11,赋给x
x := append(s,11)
// 给y中添加12,赋给y
y := append(s,12)
// 输出
fmt.Println(s,x,y)
}
输出结果:
第一种情况:
- 创建s时,cap1和len1内存中数据为[5],s指针假设为p1。
- append(s,7):按slice扩容机制cps(s)翻倍 cps=2新建一个新的底层数组,此时s指针指向新的数组p2,底层数组元素[5,7]。
- appen(s,9):按slice扩容机制 cps(s)翻倍 cps=4 新建一个新的底层数组 将s指向的数组 复制到newarr 然后将 9 加到newarr 此时 s指向新数组 p3 数组元素为[5,7,9]
- x:= append(s,11):此时,容量足够不需要扩容,底层数组数据为[5,7,9,11],此时x和s共同指向这个底层数组p3
- y:= append(s,12): 此时cps(s)=4容量足够不需要扩容,底层数组第四个元素 用12覆盖之前的11,s和y共同指向这个底层数组数组元素为[5,7,9,12]
s, x, y 都指向了同一个内存地址,所以添加12时会将之前添加的11覆盖。
第二种情况:
- s := []int{5,7,9},cap(s) = 3
- x := append(s,11)需要扩容,新建一个底层数组,x指向这个内存地址
- y := append(s,12)需要扩容,新建一个底层数组,y指向这个内存地址
所以 x,y 指向不同的数组,自然值也就不一样了。
新数组 or 老数组
一般情况下,扩容会产生新的数组,切片中的指针会指向这个新的数组。
但是有一种情况不会产生新的数组:
package main
import "fmt"
func main() {
a := [4]int{10, 20, 30, 40}
s1 := a[0:3]
s2 := a[0:2]
fmt.Printf("s1= %v\tlen = %d, cap = %d\n", s1, len(s1), cap(s1))
fmt.Printf("s2= %v\tlen = %d, cap = %d\n", s2, len(s2), cap(s2))
println("在s2中新增元素,赋给s3")
s3 := append(s2, 50)
fmt.Printf("s1= %v\tlen = %d, cap = %d\n", s1, len(s1), cap(s1))
fmt.Printf("s2= %v\tlen = %d, cap = %d\n", s2, len(s2), cap(s2))
fmt.Printf("s3= %v\tlen = %d, cap = %d\n", s3, len(s3), cap(s3))
println()
println("修改s3元素")
s3[1] += 10
fmt.Printf("s1= %v\tlen = %d, cap = %d\n", s1, len(s1), cap(s1))
fmt.Printf("s2= %v\tlen = %d, cap = %d\n", s2, len(s2), cap(s2))
fmt.Printf("s3= %v\tlen = %d, cap = %d\n", s3, len(s3), cap(s3))
println()
}
测试结果
总结:
通过数组/切片截取出的切片,并且没有规定容量或规定的容量不等于切片的长度,并且扩容后的大小小于原数组/切片的大小。
在这种情况下,扩容以后并没有新建一个新的数组,扩容前后的数组都是同一个,这也就导致了新的切片修改或新增了一个值,也影响到了原来的切片/数组和之前定义的切片了。
由于原数组还有容量可以扩容,所以执行 append() 操作以后,会在原数组上直接操作,即直接覆盖数组上下一个元素。
参考文章:https://blog.csdn.net/guzarish/article/details/118626698