Golang语言是一种静态类型的编程语言,它提供了一种灵活、功能强大的内置类型切片(slice),用于管理数据集合。切片是对数组的抽象,它可以动态地增长和缩小,而不需要像数组那样固定长度和类型。本文将介绍切片的内部实现、创建和初始化、截取、追加和复制等基本操作,以及切片的一些注意事项。
1、切片的内部实现
切片是一个有三个字段的数据结构,这些数据结构包含Golang需要操作底层数组的元数据:
- 指向底层数组的指针
- 切片访问的元素的个数(即长度)
- 切片允许增长到的元素个数(即容量)
切片的长度表示切片中当前存储的元素个数,它不能超过切片的容量。切片的容量表示切片底层数组中从切片第一个元素开始到数组末尾的元素个数,它决定了切片能够扩展到的最大长度。切片可以通过内置函数len()和cap()来获取长度和容量。
2、切片的创建和初始化
切片的创建有两种方式:一种是使用make()函数来创建,另一种是使用字面量方式创建。
使用make()函数创建切片时,需要指定切片的类型、长度和容量(可选)。make()函数会根据指定的参数创建一个底层数组,并返回一个引用该数组的切片。例如:
// 使用make()函数创建一个长度为5,容量为5的int类型切片
var slice1 = make([]int, 5)
// 使用make()函数创建一个长度为3,容量为5的string类型切片
var slice2 = make([]string, 3, 5)
// 使用make()函数创建一个长度为0,容量为10的空接口类型切片
var slice3 = make([]interface{}, 0, 10)
使用字面量方式创建切片时,可以直接指定切片中的所有或部分元素,也可以不指定任何元素。例如:
// 使用字面量方式创建一个包含3个int元素的切片
var slice4 = []int{1, 2, 3}
// 使用字面量方式创建一个包含3个string元素的切片
var slice5 = []string{"a", "b", "c"}
// 使用字面量方式创建一个包含3个空接口元素的切片
var slice6 = []interface{}{1, "hello", true}
// 使用字面量方式创建一个只初始化部分元素的切片,未初始化的元素为零值
var slice7 = []int{1: 10, 3: 20} // 等价于[]int{0, 10, 0, 20}
// 使用字面量方式创建一个空切片,其长度和容量都为0
var slice8 = []int{}
// 使用字面量方式声明一个nil切片,其值为nil,长度和容量都为0
var slice9 []int // 等价于var slice9 = []int(nil)
使用字面量方式创建切片时,如果不指定任何元素,则会创建一个nil切片,如果直接使用,很可能会panic,这点需注意。
3、切片的截取
切片可以通过设置下限和上限来截取一个新的切片,语法为slice[start:end],其中start和end表示切片的元素索引,如果省略start,则默认为0,如果省略end,则默认为切片的长度。例如:
// 原切片slice
var slice = []int{1, 2, 3, 4, 5}
// 截取整个slice切片
var newSlice1 = slice[:] // [1 2 3 4 5]
// 截取索引1到3的元素,不包含索引3
var newSlice2 = slice[1:3] // [2 3]
// 截取第一个元素到索引2的元素,不包含索引2
var newSlice3 = slice[:2] // [1 2]
// 截取索引2的元素到最后一个元素
var newSlice4 = slice[2:] // [3 4 5]
还可以指定第三个参数cap,表示新切片的容量,语法为slice[start:end:cap],其中cap不能大于原切片的容量。例如:
// 原切片slice,长度为5,容量为5
var slice = []int{1, 2, 3, 4, 5}
// 截取索引2到4的元素,并指定新切片的容量为5
var newSlice5 = slice[2:4:5] // [3 4]
// 截取索引2到4的元素,并指定新切片的容量为4
var newSlice6 = slice[2:4:4] // [3 4]
// 尝试截取索引2到4的元素,并指定新切片的容量为6
// 运行时会报错:slice bounds out of range [::6] with capacity 5
var newSlice7 = slice[2:4:6]
新切片的长度和容量可以用以下公式计算:
- 长度 = end - start
- 容量 = cap - start
对切片再次截取时,新切片和原切片会共享底层数组,所以修改其中一个切片的元素会影响另一个切片。例如:
// 原切片slice
var slice = []int{1, 2, 3, 4, 5}
// 截取索引2到4的元素
var newSlice8 = slice[2:4] // [3 4]
// 修改newSlice8第一个元素为66
newSlice8[0] = 66 // [66 4]
// slice也会受到影响
fmt.Println(slice) // [1 2 66 4 5]
如果想要避免这种情况,可以使用append()函数来复制一个新的底层数组,而不是共享原来的底层数组。例如:
// 原切片slice
var slice = []int{1, 2, 3, 4, 5}
// 使用append()函数复制一个新的底层数组,并截取索引2到4的元素
var newSlice9 = append([]int{}, slice[2:4]...) // [3 4]
// 修改newSlice9第一个元素为66
newSlice9[0] = 66 // [66 4]
// slice不会受到影响
fmt.Println(slice) // [1 2 3 4 5]
4、切片的追加和复制
切片可以使用append()函数来追加新的元素,语法为slice = append(slice, elem1, elem2...),其中elem1, elem2...表示要追加的元素,可以是一个或多个。例如:
// 原切片slice
var slice = []int{1, 2, 3}
// 追加一个元素4
slice = append(slice, 4) // [1 2 3 4]
// 追加多个元素5, 6, 7
slice = append(slice, 5, 6, 7) // [1 2 3 4 5 6 7]
// 追加另一个切片的所有元素
var anotherSlice = []int{8, 9, 10}
slice = append(slice, anotherSlice...) // [1 2 3 4 5 6 7 8 9 10]
使用append()函数时,如果原切片的容量不足以存放新的元素,那么会自动扩容,一般会扩容为原来的两倍。例如:
// 原切片slice,长度为3,容量为3
var slice = []int{1, 2, 3}
// 追加一个元素4,此时会自动扩容为原来的两倍,即容量为6
slice = append(slice, 4) // [1 2 3 4]
// 查看切片的长度和容量
fmt.Println(len(slice), cap(slice)) // 4 6
切片可以使用copy()函数来复制另一个切片的所有元素,语法为copy(dst, src),其中dst表示目标切片,src表示源切片。例如:
// 原切片slice
var slice = []int{1, 2, 3}
// 目标切片dst,长度为5,容量为5
var dst = make([]int, 5)
// 复制slice的所有元素到dst中,返回复制的元素个数
n := copy(dst, slice) // [1 2 3 0 0]
// 查看复制的元素个数
fmt.Println(n) // 3
// 查看目标切片dst的内容
fmt.Println(dst) // [1 2 3 0 0]
使用copy()函数时,如果目标切片的长度小于源切片的长度,那么只会复制目标切片能够容纳的元素。例如:
// 原切片slice
var slice = []int{1, 2, 3}
// 目标切片dst,长度为2,容量为2
var dst = make([]int, 2)
// 复制slice的所有元素到dst中,返回复制的元素个数
n := copy(dst, slice) // [1 2]
// 查看复制的元素个数
fmt.Println(n) // 2
// 查看目标切片dst的内容
fmt.Println(dst) // [1 2]
使用copy()函数时,如果目标切片和源切片共享底层数组,那么修改其中一个切片的元素不会影响另一个切片。例如:
// 原切片slice
var slice = []int{1, 2, 3}
// 目标切片dst,共享slice的底层数组
var dst = slice[:]
// 复制slice的所有元素到dst中,返回复制的元素个数
n := copy(dst, slice) // [1 2]
// 修改dst第一个元素为66
5、切片的注意事项
切片是一种非常灵活和强大的数据结构,但是在使用它时,也需要注意一些细节和陷阱,否则可能会导致一些意想不到的错误或者性能问题。以下是一些常见的注意事项:
- 切片的长度不能超过其容量,否则会引发运行时错误。例如:
// 原切片slice,长度为3,容量为5
var slice = make([]int, 3, 5)
// 尝试访问超过长度的元素
fmt.Println(slice[3]) // 运行时错误:index out of range [3] with length 3
- 切片的容量不能超过其底层数组的长度,否则会引发运行时错误。例如:
// 原切片slice,长度为3,容量为5
var slice = make([]int, 3, 5)
// 尝试创建一个容量超过底层数组长度的切片
var newSlice = slice[1:4:6] // 运行时错误:slice bounds out of range [::6] with capacity 5
- 切片在扩容时,如果原切片的容量小于1024,那么新切片的容量会翻倍;如果原切片的容量大于等于1024,那么新切片的容量会增加25%。这是为了避免频繁地扩容导致性能下降。
- 切片在扩容时,会创建一个新的底层数组,并复制原切片的元素到新数组中。这可能会导致原数组被保留在内存中,无法被及时回收。如果原数组很大,而新切片只需要很小的一部分,那么可能会造成内存浪费。为了避免这种情况,可以使用copy()函数来复制需要的部分到一个新的切片中,让原数组可以被回收。
- 切片在作为函数参数时,会按照引用传递,而不是值传递。这意味着在函数内部修改切片的元素,会影响到函数外部的切片。如果不想产生这种副作用,可以在函数内部使用copy()函数来创建一个切片的副本,然后对副本进行操作。
- 切片不能直接比较,只能和nil进行比较。如果要判断两个切片是否相等(包含相同的元素),可以使用循环遍历切片中的每个元素进行比较,或者使用reflect.DeepEqual()函数进行深度比较。
以上就是关于Go语言中切片的基本概念和用法的介绍,希望对你有所帮助。
对我的文章认可或感兴趣的朋友,还可以搜索公众号「 码道程工 」,查看更多优秀的文章。
可以的话,欢迎关注,点赞,评论,分享,感谢各位小伙伴们的支持!!!
编程改变世界,创造无限可能~~💪🏻