ThinkChat2.0新版上线,更智能更精彩,支持会话、画图、阅读、搜索等,送10W Token,即刻开启你的AI之旅 广告
切片(slice)是对数组的一个连续片段的引用,所以切片是一个引用类型(因此更类似于c/c++中的数组类型,或python中的list类型),这个片段可以是整个数组,也可以是由起始和终止索引标识的一些项的子集,**需要注意的是,终止索引标识的项不包括在切片内。** Go语言中切片的内部结构包含地址、大小和容量,切片一般用于快速地操作一块数据集合。如果将数据集合比作切糕的话,切片就是你要的“那一块”。切的过程包含从哪里开始(切片的起始位置)及切多大(切片的大小),容量可以理解为装切片的口袋大小。 ## 从数组或切片生成新的切片 切片默认指向一段连续内存区域,可以是数组,也可以是切片本身。 从连续内存区域生成切片是常见的操作,格式如下: ~~~ slice [开始位置 : 结束位置] ~~~ 语法说明如下: * slice:表示目标切片对象; * 开始位置:对应目标切片对象的索引; * 结束位置:对应目标切片的结束索引。 从数组或切片生成新的切片拥有如下特性: * 取出的元素数量为:结束位置 - 开始位置; * 取出元素不包含结束位置对应的索引,切片最后一个元素使用 slice\[len(slice)\] 获取; * 当缺省开始位置时,表示从连续区域开头到结束位置; * 当缺省结束位置时,表示从开始位置到整个连续区域末尾; * 两者同时缺省时,与切片本身等效; * 两者同时为 0 时,等效于空切片,一般用于切片复位。 根据索引位置取切片slice元素值时,取值范围是(0~len(slice)-1),超界会报运行时错误,生成切片时,结束位置可以填写len(slice),但不会报错。 #### 1) 从指定范围中生成切片 切片和数组密不可分,如果将数组理解为一栋办公楼,那么切片就是把不同的连续楼层出租给使用者,出租的过程需要选择开始楼层和结束楼层,这个过程就会生成切片,示例代码如下: ``` var highRiseBuilding [30]int for i := 0; i < 30; i++ { highRiseBuilding[i] = i + 1 } // 区间 fmt.Println(highRiseBuilding[10:15]) // 中间到尾部的所有元素 fmt.Println(highRiseBuilding[20:]) // 开头到中间指定位置的所有元素 fmt.Println(highRiseBuild[:2]) ``` #### 2) 表示原有的切片 生成切片的格式中,当开始和结束位置都被忽略时,生成的切片将表示和原切片一致的切片,并且生成的切片与原切片在数据内容上也是一致的,代码如下: ``` a := []int{1, 2, 3} fmt.Println(a[:]) ``` #### 3) 重置切片,清空拥有的元素 把切片的开始和结束位置都设为 0 时,生成的切片将变空,代码如下: ~~~ a := []int{1, 2, 3} fmt.Println(a[0:0]) ~~~ ## 直接声明新的切片 除了可以从原有的数组或者切片中生成切片外,也可以声明一个新的切片,每一种类型都可以拥有其切片类型,表示多个相同类型元素的连续集合,因此切片类型也可以被声明,切片类型声明格式如下: ~~~ var name []Type ~~~ 其中 name 表示切片的变量名,Type 表示切片对应的元素类型。 下面代码展示了切片声明的使用过程: ~~~ // 声明字符串切片 var strList []string // 声明整型切片 var numList []int // 声明一个空切片 var numListEmpty = []int{} // 输出3个切片 fmt.Println(strList, numList, numListEmpty) // 输出3个切片大小 fmt.Println(len(strList), len(numList), len(numListEmpty)) // 切片判定空的结果 fmt.Println(strList == nil) fmt.Println(numList == nil) fmt.Println(numListEmpty == nil) ~~~ ##使用make()函数构造切片 若是需要动态地创建一个切片,可以使用make()内建函数,格式如下: ``` a := make([]int, 2) b := make([]int, 2, 10) fmt.Println(a, b) fmt.Println(len(a), len(b)) ``` 其中a和b均是分配2个元素的切片,只是b的内部存储空间已经分配了10个,但实际使用了2个元素。 容量不会影响当前的元素个数,因此a和b取len都是2。 温馨提示: 使用make()函数生成的切片一定发生了内存分配操作,但给定开始与结束位置(包括切片复位)的切片只是将新的切片结构指向已经分配好的内存区域,设定开始与结束位置,不会发生内存分配操作。 ## append()为切片添加元素 Go语言的内奸函数append()可以为切片动态添加元素,格式如下: ``` var a []int a = append(a, 1) // 追加1个元素 a = append(a, 1, 2, 3) // 追加多个元素,手写解包方式 a = append(a, []int{1, 2, 3}...) // 追加一个切片,切片需要解包 ``` 需要注意的是,在使用append()函数为切片动态添加元素时,若空间不足以容纳足够多的元素,切片就会进行“扩容”,此时新切片的长度会发生改变。 切片在扩容时,容量的扩展规律是按容量的2倍数进行扩充。 ``` var numbers []int for i := 0; i < 10; i++ { numbers = append(numbers, i) fmt.Println("len: %d, cap: %d, pointer: %p\n", len(numbers), cap(numbers), numbers) } ``` 除了在切片的尾部追加,还可以在切片的开头添加元素: ``` var a = []int{1, 2, 3} a = append([]int{0}, a...) // 在开头添加1个元素 a = append([]int{-3, -2, -1}, a...)在开头添加一个切片 ``` 在切片开头添加元素一般都会导致内存的重新分配,而且会导致已有元素全部被复制1次。因此从切片的开头添加元素的性能要比从尾部追加元素的性能差很多。 因为append函数返回新切片的特性,所以切片也支持链式操作。可以将多个append操作组合起来,实现在切片中间插入元素: ``` var a[]int a = append(a[:i], append([]int{x}, a[i:]...)...) // 在第i个位置插入x a = append(a[:i], append([]int{1, 2, 3}, a[i:]...)...) ``` ##Go语言切片复制 Go语言的内置函数copy()可以将一个数组切片复制到另外一个数组切片中,若加入的两个数组切片不一样大,就会按照其中较小的哪个数组切片的元素个数进行复制。格式如下: ``` copy(destClise, srcSlice []T) slice ``` 其中srcSlice为数据来源切片,destSlice为复制的目标(即将srcSlice复制到destSlice),目标切片必须分配过空间且足够承载复制的元素个数,并且来源和目标的类型必须一致,copy()函数的返回值表示实际发生复制的元素个数。 以下展示了使用copy()函数将一个切片复制到另一个切片的过程: ``` slice1 := []int{1, 2, 3, 4, 5} slice2 :=[]int{5, 4, 3} copy(slice2, slice1) // 只会复制slice1中的前3个元素到slice2中 copy(slice1, slice2) // 只会复制slice2中的前3个元素到slice1中 ``` 虽然通过循环复制切片元素更直接,不过内置的copy()函数使用起来更加方便,copy()函数的第一个参数是要复制的目标slice,第二个参数是源slice,两个slice可以共享同一个底层数组,甚至有重叠也没有问题。 ``` pacage main import "fmt" func main() { // 设置元素数量为1000 const elementCount = 1000 // 预分配足够多的元素切片 srcData := make([]int, elementCount) // 将切片赋值 for i := 0; i < elementCount; i++ { srcData[i] = i } // 引用切片数据 refData :=srcData // 预分配足够多的元素切片 copyData := make([]int, elementCount) // 将数据复制到新的切片空间中 copy(copyData, srcData) // 修改原始数据的第一个元素 srcData[0] = 99 // 打印引用切片的第一个元素 fmt.Println(refData[0]) // 打印复制切片的第一个和最后一个元素 fmt.Println(copyData[0], copyData(elementCount - 1)) // 复制原始数据从4到6(不包含) copy(copyData, srcData[4:6]) for i := 0; i < 5; i++ { fmt.Printf("%d", copyData[i]) } } ``` ## Go语言从切片中删除元素 Go语言并没有对删除切片提专用的语法或接口,需要使用切片本身的特性来删除元素,根据要删除的元素的位置有三种情况,分别是从开头位置删除,从中间位置删除和从尾部删除,其中删除切片尾部的元素速度最快。 ### 从开头位置删除 删除开头的元素可以直接移动数据指针: ``` a := []int{1, 2, 3} a = a[1:] // 删除开头1个元素 a = [N:] // 删除开头N个元素 ``` 也可以不移动数据指针,但是将后面的数据向开头移动,可以用append原地完成(所谓原地完成是指在原有的切片数据对应的内存区间完成,不会导致内存空间结构的变化): ``` a := []int{1, 2, 3} a = append(a[:0], a[1:]...) // 删除开头1个元素 a = append(a[:0], a[N:]...) // 删除开头N个元素 ``` 还可以使用copy()函数来删除开头的元素: ``` a := []int{1, 2, 3} a = a[:copy(a, a[1:])] // 删除开头1个元素 a = a[:copy(a, a[N:])] // 删除开头N个元素 ``` ###从中间位置删除 对于删除中间的元素,需要对剩余的元素进行一次整体挪动,同样可以用append或copy原地完成: ``` a := []int{1, 2, 3} a = append(a[:i], a[i+1:]...) // 删除中间1个元素 a = append(a[:i], a[i+N:]...) // 删除中间N个元素 a = a[:i+copy(a[i:], a[i+1:])] // 删除中间1个元素 a = a[:i+copy(a[i:], a[i+N:])] // 删除中间N个元素 ``` ###从尾部删除 ``` a := []int{1, 2, 3} a = a[:len(a) - 1] // 删除尾部1个元素 a = a[:len(a) - N] // 删除尾部N个元素 ``` Go语言中删除切片元素的本质是,以被删除元素为分界点,将前后两个部分的内存重新连接起来。 提示:连续容器的元素删除无论在任何语言中,都要将删除点前后的元素移动到新的位置,随着元素的增加,这个过程将会变得极为耗时,因此,当业务需要大量、频繁地从一个切片中删除元素,如果对性能要求较高的话,就需要考虑更换其他的容器了(如双链表等能快速从删除点删除元素)。