一、Slice 存储原理
切片是基于数组实现的,切片类似一个结构体,有三个重要的组成部分,第一个是指针,指向切片实际存储数据的位置,第二个是切片的长度,第三个是切片的容量。
切片的容量始终是大于等于长度的,这样在切片添加元素的时候就不必每次重新申请一块新的内存空间存储数据。
make 方法初始化切片原理
代码语言:javascript复制func main() {
alpha := make([]int, 5)
fmt.Printf("alpha 切片的内容为:%v, 长度 Len 为:%v, 容量 Cap 为:%vn", alpha, len(alpha), cap(alpha))
}
执行上述代码,输出结果如下:
代码语言:javascript复制alpha 切片的内容为:[0 0 0 0 0], 长度 Len 为:5, 容量 Cap 为:5
再看另一个例子
代码语言:javascript复制func main() {
bravo := make([]int, 5, 3)
fmt.Printf("bravo 切片的内容为:%v, 长度 Len 为:%v, 容量 Cap 为:%vn", bravo, len(bravo), cap(bravo))
}
执行上述代码,输出结果如下:
代码语言:javascript复制# command-line-arguments
./ex9.go:10:23: invalid argument: length and capacity swapped
代码语言:javascript复制func main() {
charlie := make([]int, 5, 7)
fmt.Printf("charlie 切片的内容为:%v, 长度 Len 为:%v, 容量 Cap 为:%vn", charlie, len(charlie), cap(charlie))
}
执行上述代码,输出结果如下:
代码语言:javascript复制charlie 切片的内容为:[0 0 0 0 0], 长度 Len 为:5, 容量 Cap 为:6
使用 make 函数创建切片实例时,第一个参数为类型既切片 []type
,第二个参数为切片的长度既包含元素的数量,第三个参数切片的容量既切片做多可以包含元素的数量,也是切片底层的数组的长度,切片只是数组的一段截取。
从第二个例子可以看出容量是必须要大于等于长度的,只有大于长度时才能在同一片内存区域上进行添加操作,如果切片的长度等于容量,那么此时执行添加操作会重新开辟一块内存来存储数据。
代码语言:javascript复制func main() {
charlie := make([]int, 5, 7)
fmt.Printf("charlie 切片的内容为:%v, 内存地址为: %v, 长度 Len 为:%v, 容量 Cap 为:%vn", charlie, &charlie[0], len(charlie), cap(charlie))
charlie = append(charlie, 10)
fmt.Printf("charlie 切片的内容为:%v, 内存地址为: %v, 长度 Len 为:%v, 容量 Cap 为:%vn", charlie, &charlie[0], len(charlie), cap(charlie))
charlie = append(charlie, 20)
fmt.Printf("charlie 切片的内容为:%v, 内存地址为: %v, 长度 Len 为:%v, 容量 Cap 为:%vn", charlie, &charlie[0], len(charlie), cap(charlie))
charlie = append(charlie, 30)
fmt.Printf("charlie 切片的内容为:%v, 内存地址为: %v, 长度 Len 为:%v, 容量 Cap 为:%vn", charlie, &charlie[0], len(charlie), cap(charlie))
}
执行上述代码,输出结果如下:
代码语言:javascript复制charlie 切片的内容为:[0 0 0 0 0], 内存地址为: 0xc0000b4000, 长度 Len 为:5, 容量 Cap 为:7
charlie 切片的内容为:[0 0 0 0 0 10], 内存地址为: 0xc0000b4000, 长度 Len 为:6, 容量 Cap 为:7
charlie 切片的内容为:[0 0 0 0 0 10 20], 内存地址为: 0xc0000b4000, 长度 Len 为:7, 容量 Cap 为:7
charlie 切片的内容为:[0 0 0 0 0 10 20 30], 内存地址为: 0xc0000bc000, 长度 Len 为:8, 容量 Cap 为:14
切片中第一个元素的内内存地址就是切片的内存地址,内存地址使用 &
获取,从输出结果来看,当切片的长度小于容量的时候,切片执行添加操作内存地址是不变的,当长度大于容量的时候,内存地址变了,说明开辟了一块新的内存空间保存数据。
如果切片的长度为 0
代码语言:javascript复制func main() {
alpha := make([]int, 0)
bravo := []int{1, 2, 3}
fmt.Println(copy(alpha, bravo))
fmt.Println(alpha)
fmt.Println(bravo)
}
执行上述代码,输出结果如下:
代码语言:javascript复制0
[]
[1 2 3]
这个时候执行 copy 操作是无法完成的,因为没有内存空间存贮拷贝的内容。
截取数组获取切片原理
代码语言:javascript复制func main() {
zulu := [6]int{11, 22, 33, 44, 55, 66}
fmt.Printf("zulu 序列的类型为:%T, 长度 Len 为:%v, 容量 Cap 为:%vn", zulu, len(zulu), cap(zulu))
fmt.Println("数组的第二个元素的内存地址:", &zulu[1])
yankee := zulu[1:4]
fmt.Printf("yankee 序列的类型为:%T, 长度 Len 为:%v, 容量 Cap 为:%vn", yankee, len(yankee), cap(yankee))
fmt.Println("切片的第一个元素的内存地址:", &yankee[0])
charlie := append(yankee, 55)
fmt.Printf("%v, %vn", len(charlie), cap(charlie))
for idx, item := range yankee {
fmt.Println(idx, item)
}
}
执行上述代码,输出结果如下:
代码语言:javascript复制zulu 序列的类型为:[6]int, 长度 Len 为:6, 容量 Cap 为:6
数组的第二个元素的内存地址: 0xc0000141e8
yankee 序列的类型为:[]int, 长度 Len 为:3, 容量 Cap 为:5
切片的第一个元素的内存地址: 0xc0000141e8
4, 5
0 22
1 33
2 44
数组的第二个元素就是切片的第一个元素,内存地址相同。
切片是引用类型
代码语言:javascript复制func main() {
alpha := []string{"stark", "thor", "banner"}
bravo := alpha
bravo[0] = strings.ToUpper(bravo[0])
fmt.Printf("%v, %v, %v, %vn", &bravo[0], bravo, len(bravo), cap(bravo))
fmt.Printf("%v, %v, %v, %vn", &alpha[0], alpha, len(alpha), cap(alpha))
}
执行上述代码,输出结果如下:
代码语言:javascript复制0xc000072180, [STARK thor banner], 3, 3
0xc000072180, [STARK thor banner], 3, 3
修改 bravo 切片时 alpha 切片的内容也被修改了,切片 alpha 和 切片 bravo 指向同一个内存地址的切片,只要其中一个对切片进行了修改,另一个变量的值也会改变。
append 返回新的切片
代码语言:javascript复制func main() {
zulu := []int{1, 3, 5, 7}
fmt.Println(&zulu[0], zulu, len(zulu), cap(zulu))
yankee := append(zulu, 9)
fmt.Println(&yankee[0], yankee, len(yankee), cap(yankee))
// 修改 yankee 是否对 zulu 有影响
yankee[0] = 10
fmt.Println(yankee)
fmt.Println(zulu) // 没有影响
xray := append(yankee, 11)
fmt.Println(&xray[0], xray, len(xray), cap(xray))
// 修改 xray 是否对 yankee 有影响
xray[1] = 30
fmt.Println(xray)
fmt.Println(yankee) // 有影响
}
执行上述代码,输出结果如下
代码语言:javascript复制0xc0000b4000 [1 3 5 7] 4 4
0xc0000b8000 [1 3 5 7 9] 5 8
[10 3 5 7 9]
[1 3 5 7]
0xc0000b8000 [10 3 5 7 9 11] 6 8
[10 30 5 7 9 11]
[10 30 5 7 9]
初始 zulu 切片的长度和容量都是 4,此时进行 append 扩容操作,Go 会新申请一块内存用来保存切片 yankee,因为原来的内存没有多余空间存储新添加的值了,此时 yankee 和 zulu 两个切片的内存地址是不同的,因此修改 yankee 不会对 zulu 有任何影响。
Go 新申请的内存保存 yankee 切片,此时 yankee 切片的长度为 5,容量为 8,还有可用空间,因此再执行 append 操作时并不会开辟新内存保存数据,而是直接在原来的内存空间上进行添加操作,此时 yankee 和 xray 两个切片执行的是同一块内存地址,有部分共享数据,所以修改 xray 会对 yankee 有影响。
但是不管是否发生扩容,append 函数总会返回一个新的切片。
那么切片在发生自动库容时,扩容机制是怎样的?
- 如果新切片所需的最小容量大于当前切片容量的两倍,那么就直接用新切片所需的最小容量
- 如果新切片所需的最小容量小于等于当前切片的容量的两倍
- 如果当前切片的容量小于 1024 ,则直接把当前切片的容量翻倍作为新切片的容量
- 如果当前切片的容量大于等于 1024 ,则每次递增切片容量的 1/4 倍,直到大于新切片所需的最小容量为止。
append 函数 与 make 函数共同使用时的陷阱
先来看一段代码
代码语言:javascript复制func main() {
tango := make([]int, 3)
whiskey := append(tango, 9)
fmt.Println(tango)
fmt.Println(whiskey)
}
执行上述代码,输出结果如下:
代码语言:javascript复制[0 0 0]
[0 0 0 9]
这里的输出结果 whiskey 切片的内容不是 [9]
而是 [0,0,0,9]
,因为 make 函数会返回一个指定长度的切片的实例,切片中元素是元素类型的默认值,并不是返回一个空的(长度为 0) 的切片。
如果想要初始化一个空的切片并通过 append 函数进行添加元素的操作,可以这么做:
代码语言:javascript复制func main() {
tango := make([]int, 0)
whiskey := append(tango, 9)
fmt.Println(tango)
fmt.Println(whiskey)
}
执行上述代码,输出结果如下:
代码语言:javascript复制[]
[9]