1.概念
在Go语言中,切片(Slice)是一种基于数组的更高级的数据结构,它提供了一种灵活、动态的方式来处理序列数据。切片在Go中非常常用,因为它们可以动态地增长和缩小,这使得它们比固定大小的数组更加灵活。
2.切片的内部实现
切片是对数组的抽象表示,它包含三个要素:
1. 指向底层数组的指针
2. 切片的长度(元素数量)
3. 切片的容量(从开始到底层数组末尾的元素数量)
切片是一个有三个字段的数据结构,这些数据结构包含 Golang 需要操作底层数组的元数据:
这 3 个字段分别是指向底层数组的指针、切片访问的元素的个数(即长度)和切片允许增长到的元素个数(即容量)。
3.切片创建和初始化
在 Golang 中可以通过多种方式创建和初始化切片。是否提前知道切片所需的容量通常会决定如何创建切片。
通过 make() 函数创建切片
使用 Golang 内置的 make() 函数创建切片,此时需要传入一个参数来指定切片的长度:
slice := make([]int, length, capacity)
这里,length是切片的长度,capacity是切片的容量。如果未指定capacity,它将等于length。
// 创建一个整型切片
// 其长度和容量都是 5 个元素
slice := make([]int, 5)
分别指定长度和容量时,创建的切片,底层数组的长度是指定的容量,但是初始化后并不能
访问所有的数组元素。
注意,Golang 不允许创建容量小于长度的切片,当创建的切片容量小于长度时会在编译时刻报错:
// 创建一个整型切片
// 使其长度大于容量
myNum := make([]int, 5, 3)
分别指定长度和容量时,创建的切片,底层数组的长度是指定的容量,但是初始化后并不能
访问所有的数组元素。
注意,Golang 不允许创建容量小于长度的切片,当创建的切片容量小于长度时会在编译时刻报错
4.通过字面量创建切片
另一种常用的创建切片的方法是使用切片字面量,这种方法和创建数组类似,只是不需要指定[]运算符里的值。初始的长度和容量会基于初始化时提供的元素的个数确定:
// 创建字符串切片
// 其长度和容量都是 3 个元素
myStr := []string{"Jack", "Mark", "Nick"}
// 创建一个整型切片
// 其长度和容量都是 4 个元素
myNum := []int{10, 20, 30, 40}
当使用切片字面量创建切片时,还可以设置初始长度和容量。要做的就是在初始化时给出所需的长度和容量作为索引。下面的语法展示了如何使用索引方式创建长度和容量都是100个元素的切片:
// 创建字符串切片
// 使用空字符串初始化第 100 个元素
myStr := []string{99: ""}
区分数组的声明和切片的声明方式
当使用字面量来声明切片时,其语法与使用字面量声明数组非常相似。二者的区别是:如果在 [] 运算符里指定了一个值,那么创建的就是数组而不是切片。只有在 [] 中不指定值的时候,创建的才是切片。看下面的例子:
// 创建有 3 个元素的整型数组
myArray := [3]int{10, 20, 30}
// 创建长度和容量都是 3 的整型切片
mySlice := []int{10, 20, 30}
切片的复制:
当你有一个切片或者数组的时候,当希望将其元素作为单独的参数传递给一个函数时候,可以使用...操作符
func sum(x, y, z int) int {
return x + y + z
}
numbers := []int{1, 2, 3}
result := sum(numbers...) // 将 numbers 切片中的元素作为参数传递给 sum 函数
fmt.Println(result) // 输出: 6
在这个例子中,numbers...将切片numbers中的元素1,2,3展开为sum函数的参数
5.nil 和空切片
有时,程序可能需要声明一个值为 nil 的切片(也称nil切片)。只要在声明时不做任何初始化,就会创建一个 nil 切片
// 创建 nil 整型切片
var myNum []int
在 Golang 中,nil 切片是很常见的创建切片的方法。nil 切片可以用于很多标准库和内置函数。在需要描述一个不存在的切片时,nil 切片会很好用。比如,函数要求返回一个切片但是发生异常的时候。下图描述了 nil 切片的状态:
空切片和 nil 切片稍有不同,下面的代码分别通过 make() 函数和字面量的方式创建空切片:
// 使用 make 创建空的整型切片
myNum := make([]int, 0)
// 使用切片字面量创建空的整型切片
myNum := []int{}
区别:
1. 内存分配:
• nil 切片:没有指向任何底层数组,没有任何内存分配。
• 空切片:指向一个长度为0的底层数组,但这个数组可能已经分配了内存(尽管长度为0)。
2. 函数返回值:
• 使用 nil 可以表示“没有值”,这在错误处理和可选值中很有用。
• 空切片通常用于表示一个有效的切片,但当前没有元素。
package main
import "fmt"
func main() {
//创建一个空切片
emptySlice :=make([]int,0)
fmt.Println(emptySlice)
//创建一个nil切片
var nilSlice []int
fmt.Println(nilSlice)
//检查切片是否为空
if len(emptySlice) == 0 {
fmt.Println("emptySlice is empty")
}
if nilSlice == nil {
fmt.Println("nilSlice is nil")
}
//尝试访问空切片的元素
if len(emptySlice) > 0 {
fmt.Println(emptySlice[0])
}else {
fmt.Println("emptySlice is empty")
}
}
• emptySlice是一个空切片,它有长度0,但是它指向一个内存地址。
• nilSlice是一个nil指针,它没有指向任何内存地址,因此不能用来访问任何元素。
尝试访问nil切片的元素会导致程序崩溃(panic),因为nil没有指向有效的内存。而空切片虽然长度为0,但是它是指向一个有效的内存地址的,所以可以安全地对其进行操作,比如扩展或赋值。
6.为切片中的元素赋值
对切片里某个索引指向的元素赋值和对数组里某个索引指向的元素赋值的方法完全一样。使
用 [] 操作符就可以改变某个元素的值,下面是使用切片字面量来声明切片:
// 创建一个整型切片
// 其容量和长度都是 5 个元素
myNum := []int{10, 20, 30, 40, 50}
// 改变索引为 1 的元素的值
myNum [1] = 25
切片之所以被称为切片,是因为创建一个新的切片,也就是把底层数组切出一部分。通过切片创建新切片的语法如下:
slice[i:j]
slice[i:j:k]
其中 i 表示从 slice 的第几个元素开始切,j 控制切片的长度(j-i),k 控制切片的容量(k-i),如果没有给定 k,则表示切到底层数组的最尾部。下面是几种常见的简写形式:
slice[i:] // 从 i 切到最尾部
slice[:j] // 从最开头切到 j(不包含 j)
slice[:] // 从头切到尾,等价于复制整个 slice
让我们通过下面的例子来理解通过切片创建新的切片的本质:
// 创建一个整型切片
// 其长度和容量都是 5 个元素
myNum := []int{10, 20, 30, 40, 50}
// 创建一个新切片
// 其长度为 2 个元素,容量为 4 个元素
newNum := slice[1:3]
执行上面的代码后,我们有了两个切片,它们共享同一段底层数组,但通过不同的切片会看到底层数组的不同部分:
下面是详细的用法:
package main
import "fmt"
func main() {
// 创建一个原始切片
original := []int{1, 2, 3, 4, 5}
// 通过切片操作符创建新的切片
a := original[:3] // 包含从索引0到索引2的元素
b := original[2:] // 包含从索引2到末尾的元素
c := original[:] // 创建一个新的切片,它是原始切片的一个副本
// 修改原始切片
original[0] = 10
// 打印结果
fmt.Println("Original slice:", original) // 输出: [10 2 3 4 5]
fmt.Println("Slice a:", a) // 输出: [1 2 3]
fmt.Println("Slice b:", b) // 输出: [3 4 5]
fmt.Println("Slice c:", c) // 输出: [1 2 3 4 5]
// 修改新的切片a
a[0] = 100
// 打印结果
fmt.Println("Original slice after modifying a:", original) // 输出: [10 2 3 4 5]
fmt.Println("Slice a after modification:", a) // 输出: [100 2 3]
}
共享底层数组的切片
需要注意的是:现在两个切片 myNum 和 newNum 共享同一个底层数组。如果一个切片修改了该底层数组的共享
部分,另一个切片也能感知到(请参考前图):
在Go语言中,切片是引用类型,这意味着它们指向底层数组的连续区域。当你通过切片操作创建一个新的切片时,新切片和原始切片可能共享同一个底层数组。如果新切片的容量足够,对新切片的修改可能会反映到原始切片上,因为它们可能指向相同的底层数组元素。
如果你需要一个与原始切片完全独立的副本,可以使用copy函数来创建一个新的底层数组。这样,对新数组的修改就不会影响原始数组。
下面是一个详细的示例,展示了如何使用copy函数创建一个完全独立的切片副本,并解释了为什么这样做可以避免对原始切片的意外修改。
package main
import "fmt"
func main() {
original := []int{1,2,3,4,5}
//使用切片操作符创建一个新的切片,它与原始切片共享同一个底层数组
sharedSlice := original[:]
independentSlice :=make([]int, len(original))
copy(independentSlice, original)
// 打印原始切片和两个新切片的内容
fmt.Println("Original slice:", original) // 输出: [1 2 3 4 5]
fmt.Println("Shared slice:", sharedSlice) // 输出: [1 2 3 4 5]
fmt.Println("Independent slice:", independentSlice) // 输出: [1 2 3 4 5]
//修改原始切片
original[0] = 10
fmt.Println("Modified original slice:", original) // 输出: [10 2 3 4 5]
fmt.Println("Shared slice after modification:", sharedSlice) // 输出: [10 2 3 4 5]
//修改独立切片
independentSlice[0] = 20
fmt.Println("Independent slice after modification:", independentSlice) // 输出: [20 2 3 4 5]
//修改独立切片之后,原始切片不受影响
fmt.Println("Original slice after independent modification:", original) // 输出: [10 2 3 4 5]
fmt.Println("Shared slice after independent modification:", sharedSlice) // 输出: [10 2 3 4 5]
}
代码运行结果:
7.切片扩容
Go 语言中的切片(slice)是一种动态数组,它允许你动态地增长和缩小。当你向切片添加元素,而切片的容量(capacity)不足以容纳更多元素时,Go 语言会自动进行扩容。扩容的具体机制如下:
- 1. 初始容量:当你创建一个新的切片时,它会有一个初始的容量。如果你通过 make 函数创建切片,你可以指定切片的长度(length)和容量(capacity)。
- 2. 扩容机制:当你向切片添加元素,超出当前容量时,Go 语言会进行扩容。扩容通常遵循以下规则:
- 扩容后的容量通常是原容量的两倍,但具体增长因子可能因实现而异。
- 如果切片的容量已经很大,扩容可能不会简单地翻倍,而是增加一个固定的数量。
- 3. 扩容过程:扩容时,Go 语言会分配一个新的数组,并将原切片中的元素复制到新数组中。然后,新元素会被添加到新数组中。最后,切片的底层数组指针会被更新为指向新数组。
- 4. 性能考虑:频繁的扩容可能会导致性能问题,因为每次扩容都需要分配新的内存并复制元素。为了避免这种情况,你可以通过预先分配足够的容量来减少扩容的次数。
- 5. 手动扩容:虽然 Go 语言会自动管理切片的扩容,但你也可以手动扩容切片。例如,你可以使用 append 函数来添加元素,或者使用 copy 函数和新的切片来手动复制元素。
下面是一个简单的 Go 语言切片扩容的:
package main
import "fmt"
func main() {
s := make([]int, 0, 1) // 创建一个长度为0,容量为1的切片
for i := 0; i < 5; i++ {
s = append(s, i) // 向切片添加元素
fmt.Printf("len=%d cap=%d slice=%v\n", len(s), cap(s), s)
}
}
运行结果:
len=1 cap=1 slice=[0]
len=2 cap=2 slice=[0 1]
len=3 cap=4 slice=[0 1 2]
len=4 cap=4 slice=[0 1 2 3]
len=5 cap=8 slice=[0 1 2 3 4]
函数 append() 会智能地处理底层数组的容量增长。在切片的容量小于 1000 个元素时,总是会成倍地增加容量。一旦元素个数超过 1000,容量的增长因子会设为 1.25,也就是会每次增加 25%的容量(随着语言的演化,这种增长算法可能会有所改变)。
8.遍历切片
8.1使用for循环和索引
package main
import "fmt"
func main() {
slice := []int{10, 20, 30, 40, 50}
for i := 0; i < len(slice); i++ {
fmt.Println("Element at index", i, "is", slice[i])
}
}
8.2 使用for循环和range
使用range关键字可以同时获取索引和值,这是遍历切片的常用方法
package main
import "fmt"
func main() {
slice :=[]int{10,20,30,40,50}
for index,value := range slice{
fmt.Printf("Index is %d and value is %d\n",index,value)
}
}
8.3 使用 for 循环和 range(只获取值)
如果你不需要索引,只关心值,可以使用以下方式简化代码。
用以下方式简化代码。
package main
import "fmt"
func main() {
slice := []int{10, 20, 30, 40, 50}
for _, value := range slice {
fmt.Println("Value is", value)
}
}
8.4 使用 for 循环和 range(只获取索引)
如果你只需要索引,可以使用下划线 _ 忽略值。
package main
import "fmt"
func main() {
slice := []int{10, 20, 30, 40, 50}
for index, _ := range slice {
fmt.Println("Index is", index)
}
}
8.5 使用 for 循环和 range(遍历字符串切片)
对于字符串切片,range 会返回每个字符串中的每个字符(作为 rune 类型)。
package main
import "fmt"
func main() {
slice := []string{"hello", "world"}
for _, word := range slice {
for _, char := range word {
fmt.Printf("%c ", char)
}
fmt.Println()
}
}
9.切片操作
9.1 使用copy函数
copy 函数是标准库 copy 包中提供的一个函数,用于将一个切片的内容复制到另一个切片中。它接受两个切片作为参数:源切片和目标切片,并返回复制的元素数量。
package main
import "fmt"
func main() {
src :=[]int{1,2,3,4,5}
dst :=make([]int,5)
//从src复制到dst
copied :=copy(dst,src)
fmt.Println("Copied element",copied)
fmt.Println("Destination",dst)
}
9.2 使用 append 函数
- 第一个参数是目标切片,用于接收新添加的元素。
- 后续参数是要添加到切片中的元素。
- 使用 append 进行切片复制
- 在 append([]int(nil), original...) 中:
- []int(nil) 创建了一个类型为 []int 的空切片。这个切片的长度和容量都是0。
- original... 是 Go 语言的变参语法,它将 original 切片中的所有元素作为独立的参数传递给 append 函数。
package main
import "fmt"
func main() {
original := []int{1, 2, 3, 4, 5}
copy := append([]int(nil), original...)
fmt.Println("Original slice:", original)
fmt.Println("Copy slice:", copy)
}
在这个例子中,copy 切片是通过 append 函数创建的,它包含了 original 切片的所有元素。这种方法不仅代码简洁,而且性能也很好,因为它避免了不必要的内存分配和复制操作。
总结来说,append([]int(nil), original...) 是一种利用 append 函数的灵活性和智能内存管理来高效复制切片的方法。
9.3手动复制
你也可以通过遍历源切片并手动将每个元素赋值到新的切片中来实现复制。
package main
import (
"fmt"
)
func main() {
src := []int{1, 2, 3, 4, 5}
dst := make([]int, len(src)) // 创建一个相同长度的新切片
for i := range src {
dst[i] = src[i]
}
fmt.Println("Source slice:", src)
fmt.Println("Destination slice:", dst)
}
这种方法虽然直接,但效率较低,特别是对于大型切片。
注意事项
• 当使用 copy 函数时,确保目标切片有足够的容量来接收所有元素,否则它只会复制目标切片的容量允许的部分。
• 使用 append 函数时,虽然方便,但可能会因为扩容操作而增加额外的性能开销。
• 手动复制虽然控制更精细,但代码更复杂,且容易出错。
在实际应用中,选择哪种方法取决于具体的需求和性能考虑。对于大多数情况,copy 函数提供了一个简单而高效的方式来复制切片。