欢迎关注公众号 天天说编程
您的关注是我最大的动力!
1.Slice底层数据结构
Slice的底层由结构体类型构成,包含了指针、长度、容量三个变量。其中指针指向底层数组,每个Slice的都有一个底层数组,底层数组可能是引用的,也可以是自己的。
注意:多个Slice可能引用一个底层数组,造成内存的脏数据。
type slice struct {
array unsafe.Pointer 指针
len int 长度
cap int 容量
}
用 make 函数创建的一个 len = 4, cap = 6 的切片。内存空间申请了6个 int 类型的内存大小。由于 len = 4,所以后面2个暂时访问不到,但是容量还是在的。这时候数组里面每个变量都是0 。
make([]int,4,6),4是初始化有值的,容量是6,6是底层数组的长度,4是初始化的值,如果没有初始化,那就给出默认零值。由Slice的指针指向这个底层数组。如下图:
底层数组里面每个元素的值都初始化完成了,下面是字面量创建切片,创建的切片的长度和容量大小相等。
共用同一个底层数组解决办法:
简单来说,copy出独立的底层数组元素,底层数组元素不相关。
现在切片底层数组的共用会引起脏数据的问题,因为s1和s切片会共用同一块内存。比如
func TestSlice1(t *testing.T) {
s1 := make([]int, 3)
s1[0] = 11
s1[1] = 22
s1[2] = 33
fmt.Println(s1)
s := s1[0:2]
fmt.Println(s)
fmt.Printf("%T", s)
}
使用切片的 copy 方法创建一个新的切片,该新切片拥有独立的底层数组。修改新的切片中的元素不会影响原来的切片元素。
package main
import (
"fmt"
)
func main() {
s1 := []int{11, 22, 33}
fmt.Println("Original slice s1:", s1)
// 使用 copy 方法创建新的切片 s,拥有独立的底层数组
s := make([]int, len(s1))
copy(s, s1)
fmt.Println("Copied slice s:", s)
// 修改 s 中的元素不会影响原始切片 s1
s[0] = 99
fmt.Println("Modified slice s:", s)
fmt.Println("Original slice s1 after modification:", s1)
}
2.Slice扩容原理
简单的讲,当len>cap的时候,len是实际底层数组的元素个数,cap是数组的容量,cap的容量更改为2倍。最终会生成一个全新的切片,指针发生变化,原来的指针指向的是原来的数组,原来的数组将不会再被使用,原来的切片指向新的数组。原来的切片被go的垃圾回收机制回收。
关注的有两点,一个是扩容时候的策略,还有一个就是扩容是生成全新的内存地址还是在原来的地址后追加。
从图上我们可以很容易的看出,新的切片和之前的切片已经不同了,因为新的切片更改了一个值,并没有影响到原来的数组,新切片指向的数组是一个全新的数组。并且 cap 容量也发生了变化。
Go 中切片扩容的策略是这样的:
首先判断,如果新申请容量(cap)大于2倍的旧容量(old.cap),最终容量(newcap)就是新申请的容量(cap)。
否则判断,如果旧切片的长度小于1024,则最终容量(newcap)就是旧容量(old.cap)的两倍,即(newcap=doublecap)
否则判断,如果旧切片长度大于等于1024,则最终容量(newcap)从旧容量(old.cap)开始循环增加原来的 1/4,即(newcap=old.cap,for {newcap += newcap/4})直到最终容量(newcap)大于等于新申请的容量(cap),即(newcap >= cap)
如果最终容量(cap)计算值溢出,则最终容量(cap)就是新申请容量(cap)。
注意:扩容扩大的容量都是针对原来的容量而言的,而不是针对原来数组的长度而言的。