golang slice介绍及扩容规则

3,453 阅读4分钟

切片slice

切片是对数组的抽象,数组的长度不可改变,切片是长度可变的动态数组。在go语言中切片的使用是明显高于数组的。

定义的三种方式

1、var 切片名 []type

var s []int

2、使用make()函数定义,make([]type,len)

var s []int = make([]int 0)

3、指定容量make([]type, len, cap),cap为可变参数

var s []int = make([]int,3,4)

结构

type slice struct {
    array unsafe.Pointer
    len   int  // 长度
    cap   int  // 容量
}

容量是指底层数组的大小,长度指可以使用的大小

容量的用处是在你用 append 扩展长度时,如果新的长度小于容量,不会更换底层数组,否则,go 会新申请一个底层数组,拷贝这边的值过去,把原来的数组丢掉。也就是说,容量的用途是:在数据拷贝和内存申请的消耗与内存占用之间提供一个权衡。

// 首先声明一个长度为3,容量为4的slice
s :=make([]int,3,4)
// 记录内存地址
fmt.Printf("%p\n",s) // 0xc0000b6000
// 当插入一个元素,slice的长度变为4,所以内存地址不变
s = append(s,4)
fmt.Printf("%p\n",s) // 0xc0000b6000
// 再插入一个元素,slice的长度变为5,已经大于声明变量时限制的容量4,所以进行扩容,更换底层数组,内存地址就改变了
s = append(s,5)
fmt.Printf("%p\n",s) // 0xc0000ba000
s = append(s,6)
fmt.Printf("%p\n",s) // 0xc0000ba000
s = append(s,7)
fmt.Printf("%p\n",s) // 0xc0000ba000
s = append(s,8)
fmt.Printf("%p\n",s) // 0xc0000ba000
// 再次扩容
s = append(s,9)
fmt.Printf("%p\n",s) // 0xc0000bc000

slice操作会影响到底层数组的改变,频繁的内存申请会占用内存,所以在声明slice时尽量预估好容量的大小。slice的扩容规则可参考下面的扩容部分。

使用注意事项

1、由于slice的底层是数组指针,所以一些slice的copy后的操作需要特别注意

s := make([]int, 3)
s = append(s,4,5,6)
fmt.Println(s)  // [0 0 0 4 5 6]
a := s
a[3] = 8
// a数据的改变会影响到s中的元素
fmt.Println(s)  // [0 0 0 8 5 6]

2、slice可以向后扩展,但不可以向前扩展

s := make([]int, 3)
s = append(s,4,5,6)
fmt.Println(s)  // [0 0 0 4 5 6]
a := s[1:4]
fmt.Println(a) // [0 0 4]
// 虽然a中没有5 6这两个元素,但一样可以取到
fmt.Println(a[0:5])  // [0 0 4 5 6]
// 这种取值是不行的
fmt.Println(a[4])

上面的a取了s三个元素,

扩容规则

slice的cap是在用append扩展长度时,控制底层数组的存储是否要发生改变。 下面是golang底层slice扩容时一些比较关键的代码,但并不是扩容的全部代码

newcap := old.cap
doublecap := newcap + newcap
if cap > doublecap {
        newcap = cap
} else {
        if old.cap < 1024 {
                newcap = doublecap
        } else {
                // Check 0 < newcap to detect overflow
                // and prevent an infinite loop.
                for 0 < newcap && newcap < cap {
                        newcap += newcap / 4
                }
                // Set newcap to the requested cap when
                // the newcap calculation overflowed.
                if newcap <= 0 {
                        newcap = cap
                }
        }
}

上面代码的意思就是

1、如果新的容量大于旧容量的2倍,则直接使用新的容量

s:=make([]int,3,4)
s = append(s,4,5,6,7,8,9)
fmt.Println(cap(s)) // 10

上面本来运行完,cap应该为9,但实际结果为10,其实是go当元素被添加到切片时,当新容量为奇数时,容量增加1

2、如果1不满足,老的容量小于1024,则新的容量直接等于旧容量的2倍

s:=make([]int,3,3)
s = append(s,4)
fmt.Println(cap(s)) // 6

3、如果1不满足,旧容量大于等1024,且旧容量小于需要的容量,则旧容量不停的*1.25,直到大于新的容量

s := make([]int,1024,1024)
// 旧容量1024
fmt.Println(cap(s)) // 1024
s = append(s,1)
// 扩容完 1024 * 1.25 = 1280
fmt.Println(cap(s)) // 1280
add := make([]int,255)
s = append(s,add...)
// 再添加255个元素正好达到1280,容量不变
fmt.Println(cap(s)) // 1280
// 神奇的事情发生了,再次扩容应该为1280 * 1.25 = 1600,但实际为1696
s = append(s,1) 
fmt.Println(cap(s)) // 1696

最后一次扩容本应该为1600,但实际为1696,其实slice扩容源码里上面的代码只是比较关键的一部分,后面还有有一些操作,感兴趣的可以深入研究一下。