slice表示切片(分片),例如对一个数组进行切片,取出数组中的一部分值。在现代编程语言中,slice(切片)几乎成为一种必备特性,它可以从一个数组(列表)中取出任意长度的子数组(列表),为操作数据结构带来非常大的便利性,如python、perl等都支持对数组的slice操作,甚至perl还支持对hash数据结构的slice。

但Go中的slice和这些语言的slice不太一样,前面所说的语言中,slice是一种切片的操作,切片后返回一个新的数据对象。而Go中的slice不仅仅是一种切片动作,还是一种数据结构(就像数组一样)。

GO-slice详解

简介

slice(切片)是go中常见和强大的类型,这篇文章不是slice使用简介,从源码角度来分析slice的实现,slice的一些迷惑的使用方式,同时也讲清楚一些问题。

slice的底层实现是数组,是对数组的抽象

官方有slice相关的博客

slice分析

数据结构

源码:https://github.com/golang/go/blob/master/src/runtime/slice.go#L14

slice结构如下:

array:为是底层数组的指针

len:切片中已有元素的个数

cap:底层数组的长度

原理概述:

切片的底层实现是数组,len是切片的个数,cap是底层数组的长度,当往切片中追加元素的时候,len++,如果len>cap,就会触发切片扩容,扩容逻辑是算一个新的cap,并且创建一个新的底层数组,将原来的数据copy过来。并且创建一个新的切片(slice)。

可以从一个切片中创建一个新的切片,底层数组是同用的,修改切片元素的时候会影响到新的切片。

如果所示:

var s = make([]byte,5)

切片的创建方式

先看切片的创建方式,说这个问题之前,先看看切片的创建方式

声明

声明了一个[]uint64类型的切片,vocabList为切片的0值,

nil

在 Go 中,nil 是指针、接口、映射、切片、通道和函数类型的零值,表示未初始化的值。

具体的可以看:https://go101.org/article/nil.html

回到代码,这表示nil值,它的len和cap都是0,和nil比较结果为true,这里要说,nil值对应的具体的类型是在上下文中编译器推导出来的

通过new创建

new是内建函数,用来分配指定类型的内容,返回指向内存地址的指针,并且给此地址分配此类型的0值。

字面量创建

make

make接受三个参数,在创建的时候指定类型,长度,容量,不指定容量,默认和长度一样

代码如下:

从切片或数组截取

两个切片公用一个底层数组,但如果新创建的切片扩容了,就不共用了。

问题分析

主要分析几个问题

nil切片和空切片的差异

nil切片是通过 new 和声明方式创建的切片,go会给他们nil值,如下面的代码所示:

空切片是通过make,字面量方式创建的长度为0的切片,

我下面的代码和内容来于这篇文章

unsafe.Pointer

可以看到,空切片的是有底层数组的,并且底层数组都一样,其实也可以说空切片执行了一个指定的地址空间,

这个地址空间在源码中有定义,当分配的大小为0的时候会返回这个地址,要说明的是这个地址空间不是固定的,不是写死的一个数,在不同的机器上运行会有不同的值。

源码:https://github.com/golang/go/blob/master/src/runtime/malloc.go#L948

两者的差异:

嵌套在结构体中不容易发现

json序列化

深有体会
var a = []int{}var a []int

除此之外,没有别的区别。

切片共用底层数组

在做截取的时候,会创建一个新的slice,截取语法如下

如图所示:

有了上面的例子,可以看如下代码

源码分析

make创建切片

使用dlv或者go提供的汇编工具可以看到 make调用了什么函数

源码:https://github.com/golang/go/blob/master/src/runtime/slice.go#LL88C18-L88C18

切片的扩容规则

版本不同,扩容规则可能不一样

代码如下:

用dlv 查看它的汇编代码,看扩容操作调用了那些函数

源码链接:https://github.com/golang/go/blob/master/src/runtime/slice.go#LL157C10-L157C10

总结如下:

  • 确定新容量,cap小于256,直接2倍,大于256,新容量=老容量*1.25 * 3/4 * 256
  • 用新容量来做内存对齐操作
  • 分配新数组
  • copy数组
  • 创建切片返回

还有一点:

append的操作汇编并没有调用函数,在汇编层面就做了,直接往底层数组添加元素,只有数组已经满的情况下才会触发扩容操作

内存对齐相关东西之后在说

我们来一个例子来验证一下上面的代码逻辑:

copy函数的使用

copy函数底层调用的是

底层数组共用,那copy函数就可以完成一下几种操作

移动slice中的元素

slice合并

长度不够的copy,依dist为准

问题解答

nil 切片可以添加元素吗?

可以

nil切片就是切片声明,追加的时候切片长度为0,会引发扩容操作,扩容的时候会给分配一个新的数组。

nil切片和空切片有区别吗?

nil切片有两种方式

声明new创建

空切片有两种:

字面量创建但没有任何的元素make创建长度指定为0

使用方式除了下面两点没有别的区别:

嵌套结构体,不显性创建为niljson序列化会为null

slice扩容规则

说到前面:它在确定cap之后有内存对齐操作

小于256,是原cap的2倍大于256,是原来的1.25倍+3/4*256

到这里就结束了。