

Golang slice
source link: https://ray-g.github.io/golang/golang_slice/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

数组与切片
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为例,在内存中对应如下结构:
长度为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,但是这次结果就对了。 这是为什么呢?
如上图所示,此坑过程如下
- 首先我们创建了一个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就有了两个不同的底层数组,所以互不干涉了
这个小坑,估计大家一般也不会这么玩。
Recommend
About Joyk
Aggregate valuable and interesting links.
Joyk means Joy of geeK