golang 切片包含_Golang进阶 - 深入浅出Slice

golang 切片包含_Golang进阶 - 深入浅出Slice_第1张图片

为什么需要使用Slice

Slice 其实就是一个动态数组,为什么不使用动态数组而使用Slice呢?

  1. 在golang中所有的函数参数传递都是“值拷贝”这意味着如果你把数组当做参数传递,如果数组很大那么需要进行大量的数据拷贝。(当然通过传递数组的指针是可以解决不过略显繁琐)
  2. 数组的频繁扩容是一件繁琐且易错的事情,使用Slice 就像Java中使用 ArrayList 而不是数组一样自然。

内部实现

Golang 中的 Slice 使用起来还是比 Java中的 ArrayList 略复杂。首先,我们知道动态数组一个最重要的功能就是动态扩容。所以在构造的时候肯定会提供一个参数用于控制 大小达到多少时需要扩容(上限)可是 golang 不止于此,一共有三个参数:

  • Slice 类型
  • Slice Len大小:其实就是当前数组使用大小,与使用方直接相关
  • Slice Capacity:Slice底层数组的真实大小,当Len达到Cap时扩容,对使用者透明

golang 切片包含_Golang进阶 - 深入浅出Slice_第2张图片

如图所示,Slice中维护着一个内存的指针 ptr;同时使用len标记了当前已初始化的元素个数。用户可以对这部分数据进行读写。如果需要追加数据调用 append 后,会往数组的后面继续增加元素,如果当前元素个数 >= cap指针的大小,那么会创建一个新的数组大小为现在的2倍,并且将当前的元素都拷贝过去。 这就是cap存在的价值,限定了当前slice的大小,在不超过cap的时候就不用触发内存拷贝的操作,直接在连续的内存上append。

从Slice的结构我们可以看到Slice包含的仅仅是底层数组的指针、两个数值(Len & Cap);在64位架构机器上,一个切片需要24字节的内存:指针8字节、长度和容量各8字节。所以函数传递的时候只需要拷贝24字节的大小,规避了大量数据拷贝的问题。

我们看一个例子:

s = make([]int, 3) // [0, 0, 0] len=3 cap=3
s = make([]int, 2, 3) // [0, 0] len=2 cap=3
fmt.Println(fmt.Sprintf("%v len=%d cap=%d", s, len(s), cap(s)))

这里有个陷阱,就是初始化的时候指定了Len的大小不为0,那么前Len个元素就会被初始化为零值元素。后面我们调用append添加元素的时候不会覆盖这些已经初始化的元素。新手非常容易犯这个错误。另外,如果你只指定了一个参数,意思是设置 len & cap,也会发生这种问题。

但是出于资源的考虑,我们在知道这个动态数组的大小时也是需要指定一下数组的大小,避免频繁扩容造成的资源浪费(扩容是一种内存拷贝操作)。准确的操作为:

s = make([]int,0, 3) // [] len=0 cap=3

没有任何元素的切片

我们想要表达一个”没有任何元素的切片“可以有两种语法:

s1 := make([]int,  0)
var s2 []int

对于两者有什么区别呢?

golang 切片包含_Golang进阶 - 深入浅出Slice_第3张图片

如图所示,前者方式的数组直接是 nil;而后者数组使用的是一个0个元素的数组。

在goland中更推荐使用前者创建,可读性更好,两种申明方法在后续的读写使用起来是一致的。但是我们要知道这里可能有几个坑:

s1 := make([]int,  0)
fmt.Println(fmt.Sprintf("%v len=%d cap=%d ==nil?:%v", s1, len(s1), cap(s1), s1 == nil)) // [] len=0 cap=0 ==nil?:false
fmt.Println(jsoniter.MarshalToString(s1)) // []


var s2 []int
fmt.Println(fmt.Sprintf("%v len=%d cap=%d ==nil?:%v", s2, len(s2), cap(s2), s2 == nil)) // [] len=0 cap=0 ==nil?:true
fmt.Println(jsoniter.MarshalToString(s2)) // null

几个注意点:

  • 如果有判断 == nil 的操作,两者表示的结果是完全相反的
  • 如果有将结果 Marshal 为JSON 两者表示的结果完全不同

这两个点如果不注意,还是容易遇坑。

Append

既然Slice是一个动态数组,那么自然少不了动态添加元素的功能,在Golang中使用 Append 函数来动态添加元素。

s1 := make([]int,  0)
s1 = append(s1, 1)
fmt.Println(s1) // [1]

append 函数会智能地处理底层数组的增长。如果当前slice元素个数大于cap定义,那么会触发扩容,在切片容量小于 1000 个元素时会 * 2 增加,如果大于1000个元素则为 *1.25 倍。扩容产生新的数组后会将现有的数据拷贝过去然后加上新的数据。这个内存拷贝在大量数据的情况下是比较耗时的,所以如果已知切片将来的确切大小,可以一次性预设。

这里有一个比较坑的地方,新手也比较容易入坑的:append操作可能会触发扩容成新的数组,我们调用append时总是需要slice重置为返回值:

s1 = append(s1, 1)

总结

本文主要讲解了 Slice 的底层实现结构,并未引入复杂的源码(这个当做后期的一个TODO项补充吧)主要是希望通过这个文章帮助Golang的新手了解Slice本质,并且描述了几个容易遇到的坑:

  • 初始化时,如果指定了len大小,那么会初始化len个元素的零值对象可能导致不符合预期的结果
  • 没有任何元素的切片的描述有两种写法,在某些特殊场景下,如Masrshal、和Nil的对比会得到完全不同的结果

此外,还要补充一点,slice 的实现是数组而不是链表。在大量UPDATE和DELETE操作时请勿使用。

你可能感兴趣的:(golang,切片包含)