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]
}
这里需要注意的是:
- slice的增加需要依赖于append,这里会涉及到扩容机制(后文会说)
- 删除的话,只能是通过切割的方式重拼了,由于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
如上图所示,此坑过程如下
- 首先我们创建了一个s0的slice,里面是2个元素1和2,此时底层数组data1是满员状态
- 对s0进行了一次append,此时底层数组cap从2扩容到4,产生了新的data2, 有一个空位
- 对s0进行一次append并赋值给s1,此时s1的底层数组为data2, 最后一个空位是3
- 对s0进行一次append并赋值给s2,此时s2的底层数组还是data2, 最后一个空位是4
- 上一步,因为s1和s2共享了s0的底层数组,所以s1的最后一个空位被改成了4,此时打印的s1就不是我们期望的了
- 接着对s0进行操作,把data2的最后一个空位填满
- 对s0进行一次append并赋值给s1,此时s1的底层数组因为恰好扩容,变成了data3
- 对s0进行一次append并赋值给s2,此时s2的底层数组因为恰好扩容,变成了data4
- 这个时候,s1和s2就有了两个不同的底层数组,所以互不干涉了
这个小坑,估计大家一般也不会这么玩。