nil
切片、空切片与零切片是切片的三种状态,nil
切片是指在声明时未做初始化的切片,不用分配内存空间,一般使用 var
创建。使用 make
创建的空切片需要分配内存空间,nil
切片与空切片的长度、容量都为 0 ,如果我们要创建长度容量为 0 的切片,官方推荐 nil
切片。零切片指初始值为类型零值的切片。
// 创建 nil 切片
var slice []int
fmt.Println(slice,*(*reflect.SliceHeader)(unsafe.Pointer(&slice))) // 输出:[] {0 0 0}
// 创建空切片
slice2 := make([]int,0)
slice3 := []int{}
fmt.Println(slice2,*(*reflect.SliceHeader)(unsafe.Pointer(&slice2))) // 输出:[] {18504816 0 0}
fmt.Println(slice3,*(*reflect.SliceHeader)(unsafe.Pointer(&slice3))) // 输出:[] {18504816 0 0}
// 创建零切片
slice4 := make([]int,2,5)
fmt.Println(slice4,*(*reflect.SliceHeader)(unsafe.Pointer(&slice4))) // 输出:[0 0] {824634474496 2 5}
- 基本操作
slice := []string{"Go","语","言","编","程"}
// 1.访问元素
fmt.Println(slice[0]) // 输出:Go
// 2.修改元素
slice[0] = "PHP"
fmt.Println(slice) // 输出:[PHP 语 言 编 程]
- 特殊操作
- 追加新元素(扩容操作)
append()
函数 是 Go 语言里专为切片类型提供的操作函数。切片长度不足时,使用 append()
函数,可在切片头部或尾部追加新元素,但是,由于在头部追加元素会导致内存重新分配,所有元素将复制一次,因此大多数情况下推荐在尾部追加。追加的新元素可以是一个或多个,甚至是一个切片。append()
函数能实现切片自动增加长度并按需扩容。
slice := []string{"Go","语","言","编","程"}
// 1.在切片尾部追加一个元素
slice = append(slice, "!")
fmt.Println(slice,len(slice), cap(slice)) // 输出:[Go 语 言 编 程 !] 6 10
// 2.在切片尾部追加多个元素
slice = append(slice,"!","!")
fmt.Println(slice,len(slice), cap(slice)) // 输出:[Go 语 言 编 程 ! ! !] 8 10
// 3.在切片尾部追加切片
slice = append(slice,[]string{"!","!"}...)
fmt.Println(slice,len(slice), cap(slice)) // 输出:[Go 语 言 编 程 ! ! ! ! !] 10 10
// 4.在切片头部追加切片
slice = append([]string{"最","爱"},slice...)
fmt.Println(slice,len(slice), cap(slice)) // 输出:[最 爱 Go 语 言 编 程 ! ! ! ! !] 12 12
一个数组包含类型与长度两部分,切片的底层是一个数组,切片容量等于数组长度,在这个前提下,我们创建切片并修改它的长度时,如果长度小于容量(数组长度),那么函数将直接在原底层数组上增加新的元素,如果切片长度超出容量(数组长度),append()
函数就会创建一个新的底层数组,再将源数组的值复制过来,我们可以通过对比 SliceHeader.Data
字段观察底层数组发生的变化。
// 发生扩容前
slice := []string{"Go","语","言","编","程"}
fmt.Println(*(*reflect.SliceHeader)(unsafe.Pointer(&slice))) // 输出:{824634458112 5 5}
// 发生扩容后
slice = append(slice, "!")
fmt.Println(*(*reflect.SliceHeader)(unsafe.Pointer(&slice))) // 输出:{824634466464 6 10}
append()
扩容逻辑大概是这样,当 size 小于 1024 字节时,按乘以 2 的长度创建新的底层数组,超过 1024 字节时,按 1/4 增加。
- 删除切片元素(缩容操作)
前面说到切片是动态的数组,灵活又方便,既能使用 append()
函数对它进行扩容,也能使用 [:]
运算符在源切片上创建新的切片,实现元素的删除。:
左边是开始位,右边是结束位,它们表示元素开始与结束的选取范围。
// 创建一个空切片
slice := make([]string,0)
fmt.Println(slice,*(*reflect.SliceHeader)(unsafe.Pointer(&slice))) // 输出:[] {18541680 0 0}
// 使用 append 函数在切片尾部追加一个切片,触发扩容,内存重新分配
slice = append(slice,[]string{"最","爱","Go","语","言","编","程","!","!"}...)
fmt.Println(slice,*(*reflect.SliceHeader)(unsafe.Pointer(&slice))) // 输出:[最 爱 Go 语 言 编 程 ! !] {824634204160 8 9}
// 从切片尾部删除元素,赋值给新切片,新切片与源切片共享同一个底层数组
newSlice := slice[0:8]
fmt.Println(newSlice,*(*reflect.SliceHeader)(unsafe.Pointer(&newSlice))) // 输出:[最 爱 Go 语 言 编 程 !] {824634204160 8 9}
// 从切片尾部删除元素,创建新切片和新的底层数组
var newSlice2 []string
newSlice2 = append(newSlice2,slice[0:8]...)
fmt.Println(newSlice2,*(*reflect.SliceHeader)(unsafe.Pointer(&newSlice2))) // 输出:[最 爱 Go 语 言 编 程 !] {824634212352 8 8}
// 从切片头部和尾部删除元素并返回新切片,内存重新分配
slice = slice[2:7]
fmt.Println(slice,*(*reflect.SliceHeader)(unsafe.Pointer(&slice))) // 输出:[Go 语 言 编 程] {824634204192 5 7}
通过上面的代码,我们得出一个结论:从切片尾部删除元素不会触发缩容,仅长度发生变化,从切片头部删除元素则会触发缩容,长度及容量都发生变化。所以删除的元素不再使用后,一般建议申请新的内存空间,创建新的切片来接收要保留的元素,这样可以避免原底层数组内存无效占用,从源切片头部删除不存在此问题。
- 拷贝切片
Go 语言提供了内置函数 copy()
用来拷贝切片。在前面的代码里用 append()
函数演示了如何从切片头部或尾部删除元素,那如果想从中间删除应该怎么做?还有,前面说到当切片容量不足以添加新元素时,append()
函数会依据规则创建新的大容量底层数组,在此基础创建新切片,再将源切片的内容拷贝到新切片中,那它是如何操作的呢?
// 源切片
slice := []string{"Go","语","言","言","言","编","程","!"}
// 创建新切片
newSlice := make([]string,6)
// 选择范围拷贝到新切片
at := copy(newSlice,slice[0:3])
copy(newSlice[at:],slice[5:8])
fmt.Println(newSlice) // 输出:[Go 语 言 编 程 !]
copy()
函数的操作使用 append()
函数也能实现,只不过 append()
函数每次复制数据都需要创建临时切片,相比性能上 copy()
函数更胜一筹。
说到拷贝,它可分为深拷贝与浅拷贝,我们一般默认值类型的数据是深拷贝,引用类型的数据是浅拷贝,那深拷贝与浅拷贝有什么区别呢?深拷贝是指使用数据时,基于原始数据创建一个副本,有独立的底层数据空间,之后的操作都在副本上进行,对原始数据没有任何影响。反之,操作对原始数据有影响的叫做浅拷贝,比如拷贝原始数组地址。我们可以回想一下,在前面的切片操作中,哪些是深拷贝,哪些是浅拷贝。
- 判断 2 个字符串切片是否相等
Go 语言标准库中有专门的方法判断两个字节切片是否相等,却没有针对字符串切片的,当我们要判断两个字符串切片是否相等时,可以使用 reflect
包提供的 DeepEqual()
方法,或者自定义。但不管哪种,在使用前都要先定义什么是相等,一个切片包含类型、长度、容量和元素值,按照以往的经验,当类型、元素值、长度一致时,我们便认为这两个切片是相等的。只是,使用自定义方法需要先确定类型再比较,而使用 reflect.DeepEqual()
可以比较两个不确定类型的值,但是需要付出很大的性能代价,所以通常我们还是使用前者。
// 创建两个长度相同、容量不同的切片
slice1 := make([]string,2,5)
slice1[0] = "Go"
slice1[1] = "语"
slice2 := make([]string,2,6)
slice2[0] = "Go"
slice2[1] = "语"
// 方法1:自定义方法
status := true
if len(slice1) != len(slice2){
status = false
}else{
for k,_ := range slice1 {
if slice1[k] != slice2[k] {
status = false
}
}
}
fmt.Println(status) // 输出:true
// 方法2:使用 reflect.DeepEqual
fmt.Println(reflect.DeepEqual(slice1,slice2)) // 输出:true
总结
数组跟切片是 Go
语言中很基础的内容,它们的操作也不仅限于前面介绍的那些。切片在某种程度上是来源于数组的,所以他们可以支持相同的操作方法,但切片的形式更为动态,操作方法和使用场景也更加丰富。
这次总结 Go
语言数组跟切片的内容花费了很长时间,尽管如此,仍然有一些”未解之谜“,对于已了解的内容也不敢说都理解恰当,我将带着这些问题,继续 Go 语言学习之路