Go进阶之Slice实现原理

35 阅读4分钟

slice又称为动态数组.依托数组实现.可以方便的进行扩容和传递.实际中比数组更灵活.

1.初始化:

变量声明:

var intSlice []int 
var boolSlice []bool
var strSlice []string

字面量:

//空切片.
intEmptyS := []int{}
//长度为9的切片.
intS := []int{1, 2, 3, 4, 5, 6, 7, 8, 9}

片也可以使用字面量初始化.空切片的长度为空.它的值不为nil.

内置函数:

//指定长度.
intS5 := make([]int, 5)
//指定长度和空间.
ints510 := make([]int, 5, 10)

//指定长度.
intS5 := make([]string, 5)
//指定长度和空间.
ints510 := make([]string, 5, 10)

//指定长度.
intS5 := make([]bool, 5)
//指定长度和空间.
ints510 := make([]bool, 5, 10)

切取:

func main() {

    array := [5]int{1, 2, 3, 4, 5}
    //数组切取.
    arraySlice := array[0:3]
    //切片切取.
    slice := arraySlice[0:1]

    fmt.Println(arraySlice)
    fmt.Println(slice)
}

切片可以基于数组和切片创建.

注:切片与原数组共享底层空间.修改切片将会影响原数组.

切片表达式[low:high)为左闭右开

new创建:

intNew := *new([]int)
strNew := *new([]string)
boolNew := *new([]bool)

此时创建的切片值为nil.

2.切片操作:

append()切片追加元素:

func main() {
    slice := make([]int, 0)

    slice = append(slice, 1)
    fmt.Println(slice)
    slice = append(slice, 2)
    fmt.Println(slice)
    //追加一个切片
    slice = append(slice, []int{3, 4, 5, 6}...)
    fmt.Println(slice)
}

当切片空间不足时.append会先创建新的大容量新切片.添加元素后再返回新切片.

3.实现原理:

slice底层依托数组实现.对用户屏蔽.底层数组容量不足时可以实现自动重分配生成新的slice.

3.1数据结构:

源码位于src/runtime/slice.go:slice中:

type slice struct {
    array unsafe.Pointer
    len   int
    cap   int
}

array:  指针指向底层数组.

len:  数组长度.

cap:  切片容量.

3.2切片操作:

3.2.1make创建slice:

image.png

 3.2.2数组创建slice:

image.png

3.2.3slice扩容:

使用append向slice追加元素时.如果slice空间不足.则会触发slice扩容.扩容实际上是重新分配一块更大的内存.将原slice数据拷贝进新slice.然后返回新slice.扩容后再将数据加进去.

image.png 由上图可见.扩容操作只关心容量.会把原slice数据拷贝到新的slice中.追加数据由append在扩容结束后完成.

3.2.4扩容基本规则:

原slice容量小于1024.新slice容量扩大为原来2倍.

原slice容量大于或等于1024.则新slice容量扩大为原来的1.25倍.

3.2.5思考:

当切片较小时.采用较大扩容速率.可以避免频繁的扩容.从而减少内存的分配次数和数据拷贝的代价.

切片较大时.采用较小的扩容速度.主要是为了避免空间的浪费.

3.2.6slice拷贝:

使用copy()内置函数拷贝两个切片时.会将原切片的数据逐个拷贝到目的切片指向的数组中.拷贝数量取两个切片的最小值.

例如:长度为10的切片拷贝到长度为5的切片中.只会拷贝五个元素.(copy中不会发生扩容).

4.切片表达式:

slice表达式既可以基于一个字符串(string)生成一个子字符串.也可以基于一个数组或切片中生成切片.

4.1Go语言提供了两种表达式:

4.1.1简单表达式a[low:high]:

如果a为数组或切片.则该表达式将切取a位于[low:high)区间的元素生成一个切片.如果a为字符串的话.稍微有点特殊会生成一个字符串而不是切片.

4.1.2示例:
func main() {
    array := []int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}
    slice := array[2:3]
    fmt.Println(slice)
}
4.1.3底层数组共享如下:

array:=[10]int

slice:=array[2:4]

image.png

4.1.4边界问题:

如果简单表达式切取的对象为数组或数组.在表达式a[low:high]中low和high满足以下关系:

0<=low<=high<=len(a)

不满足这个关系就会发生panic.

表达式对象为切片:

0<=low<=high<=cap(a)

4.1.5切取string:

如果作用于字符串.则会产生新的字符串.而不是切片.这是由string和slice的结构类型的差异决定的.slice可以支持随机读写.string不可以.

示例:
func main() {
    str := "hello go"
    newSlice := str[0:3]
    fmt.Println(reflect.TypeOf(newSlice))
}
4.1.6默认值:

a[:high]等同于a[0:high]

a[0:]等同于a[0:len(a)]

a[:]等同于a[0:len(a)]

4.2.1扩展表达式a[low:high:max]:

简单表达式生成的新切片与原数组或切片共享底层数组避免了拷贝元素.节约内存空间.也带来了一定的风险.

当用append函数增加元素的时候可能会覆盖后面的元素.

示例:
func main() {
    array := []int{1, 2, 3, 4, 5}
    slice := array[1:4]
    slice = append(slice, 3)
    fmt.Println(array)
}

array数组中的元素5变为了3.

扩展表达式中的max就是用来限制新生成切片的容量.新切片的容量为max-high.low和high还有max需要满足如下关系.

0<=low<=high<=max<=cap(a)

4.2.2示例图:

image.png

尽头是殊途同归.

考虑到公众号文章过于碎片化.正在逐步把所有的知识整理到语雀知识库.后面会公开语雀地址.敬请期待.

如果大家喜欢我的分享的话.可以关注我的微信公众号

念何架构之路