崩溃

早上(11.30)收到邮件,Vultr东京机房网络故障。当时搭建SS时,考虑到了机房故障。所以特意分出了日本和香港两条线路。但千算万算,忘记数据库还在东京机房中。 现在网络故障,SS服务器无法读取数据库中的账号信息。于是乎,主备两条线同时宕了。哭笑两声,没钱去做异地双活,访问量又不大,就这么凑合吧。 我就不信Vultr网络故障是大概率事件。如果很频繁的出故障,用户会用脚去投票的。

铁路警察各管一段,Vultr的故障让Vultr的运维去背锅吧。我们言归正传,继续聊Golang。

在<撸袖子>那节,我们提到了数组。 其中用了一个很少的篇幅说了一下数组的近亲-切片。当时说到数组使用起来不方便,Golang提供了一种更方便的数组使用方式,就是切片。这节中,我们就先来说切片。

先来复习数组的概念,就是一组相同数据类型的集合。在说数组的时候,没有什么动态扩展的方法。只能实现规定好这个数组有多少个元素,然后按照下标进行增删改查。在真实环境中,有很大的局限性。 切片作为数组的近亲,就弥补了这种缺陷。Golang所提供的切片,内置了很多方法来达到数组的动态扩容/缩容。

切片既然是数组的近亲,那声明方式基本上长得很像:

  1. var name []type

name自然是切片名称,type就是数据类型。仅此而已,就完成了一个切片的声明。和数组的声明最大的不同,就在于没有长度限制。这是最常用的声明方式,还有一种文绉绉的声明方式,如下:

  1. name := make([]type, length, capacity)

一瞅就有种学院派的作风。 多了两个参数,length和capacity。这两个概念理解不好,这就是一个大坑。Golang为了让切片有很高的读效率和又不容易出现指针越界,就创造了length和capacity两个属性。

capacity指的是此切片当前指向内存的数据大小。而length指的是当前切片的容量大小,从逻辑上来看,满足这个条件: 0<=length<=capacity。

为什么说这是一个坑? 如果打算用切片操作目标内存的时候,必须小心别append过头,否则就操作到新开启的内存块去了,也要小心别意外覆盖了原slice的值。比如下面:

  1. s := []int{10} //创建一个legnth = capacity = 1 的切片,并且初始化为10
  2. s = append(s,11) //容量不够,翻倍扩容。legnth = capacity = 2,现在是10,11
  3. s = append(s,12) //容量又不够了,再次扩容。legnth =3, capacity = 4,现在是10,11,12
  4. x := append(s, 13) //容量够了,不扩容。legnth = capacity = 4,现在是10,11,12,13
  5. 坑来了
  6. y := append(s, 14) //容量够了,不扩容。legnth = capacity = 4,现在是10,11,12,14

但如果你将上面代码输出一下,会看到x和y的值是相同的,都是10.11.12.14。这里面包含了切片的本质。 在Golang官方文档中提及,对切片单独进行append操作,并不会修改切片的内容(也就是单独执行append(s,12)),往往需要将append后的数据重新赋值给源切片,也就是s = append(s,12),这是Golang官方所推荐的用法。 上面的例子中,在x和y那两行,因为s没有发生变化,length=3.所以后面append的值会直接添加到末尾。而返回的又都是同一块内存地址,所以x和y其实指的是同一块内存,因此其内部值也是相同的。 可以来一段代码,把x,y和s的内存地址都输出出来,结果就一目了然了。

如果嫌麻烦,那就用最简单的方式:

  1. var s []int
  2. s = append(s,xxx)

而如果想输出当前的length和capacity,就直接使用len()和cap()两个内置函数。

数组允许存在空数据,切片也当然允许存在空切片。当直接声明一个切片的时候,此时此刻,length = capacity = 0.

  1. var numbers []int
  2. 此时此刻 len = 0 cap = 0 slice = []

又该如何判断切片是否为空呢?可以使用length和capacity属性,但不如使用nil来的简单:

  1. numbers == nil

true就表示是空切片,false表示是非空切片。

切片同数组相比,最灵活的方面在于切分子切片。例如可以在代码中,根据业务需要,随时将一个大切片取出任意元素组成一个子切片。看下面:

  1. numbers := []int{0,1,2,3,4,5,6,7,8}
  2. number2 := numbers[:2] // 从0到2,但不包括2.所以是0,1
  3. number3 := numbers[2:5] // 从2到5,但不包括5.所以是2,3,4
  4. number4 := numbers[5:] // 从5到末尾,包括末尾。

上面number2, number3和number4都是子切片,在使用时,需要记住这些子切片都是指向了源切片某一块内存,什么意思?也就是说源切片元素发生了变化,那么子切片也会发生变化。不信? 在上面代码中声明子切片后,任意修改numbers的元素,在看看结果。

如果不想受源切片影响怎么办?使用copy()函数。顾名思义,也就是把重新创建一个切片,自立山头呗。

  1. number5 := make([]int, 2)
  2. copy(number5, numbers[:2])

输出地址之后,就可以看到两者已经完全脱离父子关系,想干嘛就干嘛去吧。

说到最后,需要看一下切片的数据结构了。 我想看到数据结构,上面那些所谓的坑应该就能看明白了。

  1. type slice struct {
  2. array unsafe.Pointer
  3. len int
  4. cap int
  5. }
  6. src/runtime/slice.go

可以看到slice,包含一个指针,一个len变量和一个cap变量。当需要获取length和capacity时,是直接读取的len和cap变量值,不需要再遍历一遍,所以获取长度和容量效率非常高。 而array指向了一块内存,进行append操作时,如果len == cap,则扩容。如果len < cap,那么就是array[len+1]操作。因为golang默认都是值传递,虽然len已经变成len+1了,但原始的slice的len仍然没有变。因此golang才建议,用源切片来接受返回值,这样源切片的len和cap就会同步发生变化。

说实话,这部分脑子里面清楚,但用文字表述的效果欠佳。所以遇到切片时刻记住,用源切片来接受返回值。如果需要子切片,首要需要考虑,是不是需要用copy来复制生成。