Golang slice

数组与切片

Golang中语言自带的线性数据结构有数组(Array)和切片(Slice)。

其中数组是有固定长度的由特定类型的元素组成的序列。 因为数组的长度是组成数组类型的一个属性,所以不同长度的数组是属于不同的类型的,不同类型之间无法直接赋值。 所以,在Go中很少直接使用数组的。 和数组对应的就是Slice,Slice的长度是可以动态的扩容和收缩的,所以使用Slice更加的灵活。

要想理解Slice是如何工作的,最好还是先理解一下数组。

数组

数组的定义有如下几种方式:

var a [3]int                   // 定义三个元素的数组,默认初始为 [3]int{0, 0, 0}
var b = [...]int{1, 2, 3}      // 定义三个元素的数组,并初始化为 [3]int{1, 2, 3}
var c = [...]int{2:3, 1:2}     // 定义三个元素的数组,并初始化为 [3]int{0, 2, 3}
var d = [...]int{1, 2, 4:5, 6} // 定义六个元素的数组,并初始化为 [6]int{1, 2, 0, 0, 5, 6}
  • 数组a只是定义了一个拥有3个int类型元素的数组类型,数组中的元素默认进行0值初始化。
  • 数组b在定义的时候按照顺序指定了数组中的每个元素,数组的长度会根据元素数目自动计算出来。
  • 数组c以索引的方式来初始化了数组,数组长度是最大的索引,指定了索引位置的元素会初始化,未指定的则初始化为0.
  • 数组d是混合了b和c两种方式来初始化数组的,估计没有人喜欢这么使用。

数组的内存结构非常简单,是一段连续的内存。以数组b为例,在内存中对应如下结构:

graph LR;
    1("1 | 2 | 3")

长度为0的数组,在内存中不占用空间,可以当作和空结构struct{}一样来做channel的同步操作。

Go中的数组是值语义的,整个数组就是一个值,并不像是C语言中,表示数组的变量是指向第一个元素的指针。 Go中如果对数组进行赋值或者传递函数参数的话,是要进行整个数组的复制的,所以如果数组较大,可以用数组的指针来传递或者赋值。 需要注意的是,由于不同长度的数组是不同的类型,所以指向不同长度数组的指针,也是不同类型的。

打印数组具体信息的时候,可以使用format中的%T来打印具体类型,也可以用%#v来打印具体类型和具体数据。

在Golang中,数组是Slice和String的基础,了解了数组,我们就可以进一步的来探究Slice了。

切片

Slice是一种可以改变长度的动态数组。因为动态数组的长度是不固定的,所以切片的长度自然不能和数组的长度一样作为类型的属性。 数组由于不同长度属于不同类型,所以在Go中的使用不是那么的广泛,而Slice则广泛在Go中使用。

我们先看一下slice的结构定义,在reflect.SliceHeader中:

    Data uintptr
    Len  int
    Cap  int

我们可以看到,这个结构和string是差不多的,除了有指向底层数据的Data和表示当前slice长度的Len外, 还多了一个Cap,用来表示当前slice所分配的空间的最大容量。这个容量是指元素个数,而不是字节的长度。

切片的操作

切片的声明

切片有如下的几种声明的方式

var (
    a []int               // nil切片,和nil相等,一般用来表示一个虚的切片
    b = []int{}           // 空切片,和nil不相等,一般用来表示一个空的集合
    c = []int{1, 2, 3}    // 有三个元素的切片,len和cap都为3
    d = c[:2]             // 有2个元素的切片,其中len为2,cap为3
    e = c[0:2:cap(c)]     // 有2个元素的切片,len为2, cap为3
    f = c[:0]             // 有0个元素的切片,len为0, cap为3
    g = make([]int, 3)    // 有3个元素的切片,len和cap都为3
    h = make([]int, 2, 3) // 有2个元素的切片,len为2, cap为3
    i = make([]int, 0, 3) // 有0个元素的切片,len为0, cap为3
)

和数组一样,len函数会返回slice的有效元素的长度,而cap函数则会返回slice的最大可存储的容量大小。 切片是可以和nil比较的,只有当slice底层数据为nil的时候,才会返回nil,此时的长度和容量都是无效的。 如果slice的底层数据为nil,但是长度或者容量不为0,那么有可能是slice被破坏掉了,一般发生在unsafe处理不当的情况下。

增删查改

日常使用,必然少不了在切片中进行增删查改的操作

// 增
func add(slice []interface{}, value interface{}) []interface{} {
    return append(slice, value)
}

// 删
func remove(slice []interface{}, i int) []interface{} {
    return append(slice[:i], slice[i+1:]...)
}

// 改
func update(slice []interface{}, index int, value interface{}) {
    slice[index] = value
}

// 查
func find(slice []interface{}, index int) interface{} {
    return slice[index]
}

这里需要注意的是:

  1. slice的增加需要依赖于append,这里会涉及到扩容机制(后文会说)
  2. 删除的话,只能是通过切割的方式重拼了,由于slice是引用类型,存的是指针,性能上不会有太多影响

插入/遍历/清空

// 插入
func insert(slice *[]interface{}, index int, value interface{}) {
    rear := append([]interface{}{}, (*slice)[index:]...)
    *slice = append(append((*slice)[:index], value), rear...)
}

// 遍历
func list(slice []interface{}) {
    for idx, val := range slice {
        fmt.Printf("idx:%d - val:%d", idx, val)
    }
}

// 清空
func empty(slice *[]interface{}) {
    *slice = append([]interface{}{})
    //    *slice = nil
}

复制

// 复制
func main() {
    intSlice := []int{1,2,3,4,5,6}
    copySlice1 := make([]int,0,10)

    _ = copy(copySlice1,intSlice)
    fmt.Printf("长度为0的时候:%v\n",copySlice1)

    copySlice2 := make([]int,6,10)

    _ = copy(copySlice2,intSlice)
    fmt.Printf("长度为6的时候:%v",copySlice2)
}

复制时需要注意的是:要保证目标切片有足够的大小,注意是大小,而不是容量。

扩容时的情况

当不断向slice中append数据的时候,必然会达到slice的cap。那么此时slice就涉及到了扩容的操作。 扩容的时候,slice会分配新的底层数组,然后将现有的数据copy到新的数组中。

Append坑

func main() {
    s0 := []int{1, 2}
    fmt.Println("s0-1:", len(s0), cap(s0), s0)
    s0 = append(s0, 3)
    fmt.Println("s0-2:", len(s0), cap(s0), s0)

    s1 := append(s0, 3)
    s2 := append(s0, 4)
    fmt.Println("s1-1:", len(s1), cap(s1), s1)
    fmt.Println("s2-1:", len(s2), cap(s2), s2)

    s0 = append(s0, 4)
    s1 = append(s0, 3)
    s2 = append(s0, 4)
    fmt.Println("s1-2:", len(s1), cap(s1), s1)
    fmt.Println("s2-2:", len(s2), cap(s2), s2)
}

output:

s0-1: 2 2 [1 2]
s0-2: 3 4 [1 2 3]
s1-1: 4 4 [1 2 3 4]
s2-1: 4 4 [1 2 3 4]
s1-2: 5 8 [1 2 3 4 3]
s2-2: 5 8 [1 2 3 4 4]

我们来看一下这个坑,s1和s2的两次append,分别有什么不同。 第一次对s1和s2的操作明显不是我们期望的结果,s1在最后明明append了3, 但是输出的结果确是4 第二次对s1和s2的操作,和第一次是一样的,都是在s0上进行append,但是这次结果就对了。 这是为什么呢?

graph TD;
    s0-1["s0: data1: | 1 | 2"]

    s0-2["s0: data2: | 1 | 2 | 3 | empty"];
    s0-1 -->|"s0 = append(s0, 3)"| s0-2

    s1-1["s1: data2: | 1 | 2 | 3 | 3"]
    s2-1["s2: data2: | 1 | 2 | 3 | 4"]

    s0-2 --> |"s1 := append(s0, 3)"| s1-1
    s0-2 --> |"s2 := append(s0, 4)"| s2-1

    s0-3["s0: data2: | 1 | 2 | 3 | 4"]
    s1-2["s1: data3: | 1 | 2 | 3 | 4 | 3 | empty | empty | empty"]
    s2-2["s2: data4: | 1 | 2 | 3 | 4 | 4 | empty | empty | empty"]

    s0-2 --> |"s0 = append(s0, 4)"| s0-3
    s0-3 --> |"s1 = append(s0, 3)"| s1-2
    s0-3 --> |"s2 = append(s0, 4)"| s2-2

如上图所示,此坑过程如下

  1. 首先我们创建了一个s0的slice,里面是2个元素1和2,此时底层数组data1是满员状态
  2. 对s0进行了一次append,此时底层数组cap从2扩容到4,产生了新的data2, 有一个空位
  3. 对s0进行一次append并赋值给s1,此时s1的底层数组为data2, 最后一个空位是3
  4. 对s0进行一次append并赋值给s2,此时s2的底层数组还是data2, 最后一个空位是4
  5. 上一步,因为s1和s2共享了s0的底层数组,所以s1的最后一个空位被改成了4,此时打印的s1就不是我们期望的了
  6. 接着对s0进行操作,把data2的最后一个空位填满
  7. 对s0进行一次append并赋值给s1,此时s1的底层数组因为恰好扩容,变成了data3
  8. 对s0进行一次append并赋值给s2,此时s2的底层数组因为恰好扩容,变成了data4
  9. 这个时候,s1和s2就有了两个不同的底层数组,所以互不干涉了

这个小坑,估计大家一般也不会这么玩。