云青青兮欲雨——Go的数组与切片傻傻分不清楚?

2022-10-25 19:43:29 浏览数 (2)

云青青兮欲雨——Go的数组与切片傻傻分不清楚?

我们在使用Go语言进行程序的编写时,不可避免会遇到切片和数组的抉择。其实我建议选切片,因为切片比数组更加好用,也更加安全。本文会比较切片与数组的异同,也会介绍切片的一些特性。

数组与切片的异同有哪些?

引入数组是很多编程语言都做的一个事情,但是将数组引入一门编程语言需要考虑很多问题:

  • 数组可变长度还是不可变长?
  • 数组长度是数组数据类型的一部分吗?
  • 多维数组要怎么样设计?
  • 空数组是不是要有意义?

编程语言的设计者对这些问题的处理会影响数组在这门语言中的地位。

在Go语言中,数组的长度一旦定义好就固定下来了,比如:

代码语言:javascript复制
var buffer [256]byte

那么数组buffer的长度就是256个byte数据类型的长度:

代码语言:javascript复制
buffer: byte byte byte ... 256 times ... byte byte byte

我们使用len(buffer)就会返回固定值256。

而切片不是这样!

Go语言中切片本身是对数组的封装,其描述一段与切片变量本身分开的连续数组片段。可以这样说,切片不是数组,其描述一段数组。

前面我们回答了「数组可变长度还是不可变长?」这一个问题,接下来我们来回答「数组长度是数组数据类型的一部分吗?」这个问题,我们来看下面这个例子:

代码语言:javascript复制
package main
​
import "fmt"
​
func main() {
    var array_one = [2]int{1, 2}
    var array_two = [1]int{1}
    
    if array_one == array_two {
        fmt.Println("equal type")
    }
}

运行结果,出现了编译错误:

代码语言:javascript复制
# command-line-arguments
.slice_data.go:9:18: invalid operation: array_one == array_two (mismatched types [2]int and [1]int)
​
Compilation finished with exit code 2

果然,数组长度是数组数据类型的一部分,两个数组的长度不同,于是它们不是属于同一类型,因此不能进行比较。

原来截取切片还有这些门道

我们可以截取上一节给到的数组buffer来创建一个切片,比如我们创建一个描述了数组buffer第100个元素一直到149个元素为止的切片:

代码语言:javascript复制
var slice []byte = buffer[100:150]

还可以基于切片进行切片操作,然后将结果存储回原始切片结构中。

代码语言:javascript复制
slice = slice[1:len(slice)-1]

上面这行代码是删除切片的第一个和最后一个元素。

Note:

对数组或已有的切片进行切片操作,得到的切片的底层数组是和原有数组或切片是共用的。

也就是说,如果我们基于老slice切了一个新slice,我们对新切片进行修改,可能会影响到老切片。为什么说是可能呢?因为如果我们执行append操作可能会导致新slice或老slice底层数组扩容,移动到了新的位置。

切片的扩容规律

扩容一般是自动扩容。当向切片追加元素之后,如果容量不足,就会引起扩容。

append函数如下:

代码语言:javascript复制
func append(slice []Type, elems ...Type) []Type

其参数elems为不定长类型,因此可以同时append多个值到切片中去。

代码语言:javascript复制
slice := []int{1, 2, 3}
ints := append(slice, 1, 2, 3)
fmt.Println(slice)  //[1 2 3]
fmt.Println(ints)   //[1 2 3 1 2 3]

同时也可以使用...语法糖来给切片直接append切片的全部元素。

代码语言:javascript复制
slice := []int{1, 2, 3}
append1 := []int{4, 5, 6}
ints := append(slice, append1...)
fmt.Println(slice) //[1 2 3]
fmt.Println(ints) //[1 2 3 4 5 6]

看似是往slice追加元素,其实是往底层数组追加元素。可是我们都知道:底层数组的长度是固定的,如果满了就不能再放了,此时我们需要进行扩容。

扩容会导致切片整体迁移到新的位置,并且容量得到扩充。

如果是Go1.18版本之前,扩容规律是这样的:

  • 当要进行扩容的切片容量小于1024的时候,扩容后的切片容量会变成原来的两倍。
  • 如果要进行扩容的切片容量大于1024的时候,扩容后切片容量为原本的1.25倍。

但是1.18版本之后,其扩容策略就变化了,我们可以通过下面这段代码了解:

代码语言:javascript复制
package main
​
import "fmt"
​
func main() {
    s := make([]int, 0)
​
    oldSilceCap := cap(s)
​
    for i := 1; i < 2000; i   {
        s = append(s, i)
​
        newSliceCap := cap(s)
​
        if newSliceCap != oldSilceCap {
            fmt.Printf("[%d -> M] cap = %-4d  |  after append %-4d  cap = %-4dn", 0, i-1, oldSilceCap, i, newSliceCap)
            oldSilceCap = newSliceCap
        }
    }
}

其结果为:

代码语言:javascript复制
[0 ->    0] cap = 0     |  after append 1     cap = 1
[0 ->    1] cap = 1     |  after append 2     cap = 2   
[0 ->    2] cap = 2     |  after append 3     cap = 4   
[0 ->    4] cap = 4     |  after append 5     cap = 8   
[0 ->    8] cap = 8     |  after append 9     cap = 16  
[0 ->   16] cap = 16    |  after append 17    cap = 32  
[0 ->   32] cap = 32    |  after append 33    cap = 64  
[0 ->   64] cap = 64    |  after append 65    cap = 128 
[0 ->  128] cap = 128   |  after append 129   cap = 256 
[0 ->  256] cap = 256   |  after append 257   cap = 512 
[0 ->  512] cap = 512   |  after append 513   cap = 848 
[0 ->  848] cap = 848   |  after append 849   cap = 1280
[0 -> 1280] cap = 1280  |  after append 1281  cap = 1792
[0 -> 1792] cap = 1792  |  after append 1793  cap = 2560
​
Process finished with the exit code 0

此时的规律已经不太好寻找了,大家有兴趣的可以去阅读slice的growslice源码。

参考文献:

机械工业出版社《Go程序员面试笔试宝典》

Arrays, slices (and strings): The mechanics of ‘append’ - The Go Programming Language https://go.dev/blog/slices

0 人点赞