首页 > 编程笔记

Go语言切片详解

在 Go 语言中,切片是一种比较特殊的数据结构,这种数据结构更便于使用和管理数据集合。

切片是围绕动态数组的概念构建的,可以按需自动增长和缩小,总的来说,切片可理解为动态数组,并根据切片中的元素自动调整切片长度。

切片的定义和使用

切片是动态数组,可以根据切片中的元素自动调整切片长度,切片的定义方式与数组定义十分相似,但定义过程中不用设置切片长度,其语法格式如下:
// 定义切片
var name []type

// 定义切片并赋值
var name = []type{value1, value2}
// 简写
name := []type{value1, value2}

// 使用make()定义切片
var name []type = make([]type, len)
// 简写
name := make([]type, len)
切片的定义语法说明如下:
定义切片一共划分为3种方式:只定义、定义并赋值、使用 make() 函数定义,它们的使用方式如下:
package main

import "fmt"

func main() {
    var s []int
    var ss = []int{1, 2}
    var sss []int = make([]int, 3)
    fmt.Printf("只定义:%v,内存地址为:%v\n", s, &s)
    fmt.Printf("定义并赋值:%v,内存地址为:%v\n", ss, &ss)
    fmt.Printf("使用make()函数定义:%v,内存地址为:%v\n", sss, &sss)
}
不同定义方式使切片的元素值各有不同,具体说明如下:
运行上述代码,运行结果为:

只定义:[],内存地址为:&[] 
定义并赋值:[1 2],内存地址为:&[1 2] 
使用make()函数定义:[0 0 0],内存地址为:&[0 0 0]


如果定义的切片不是空切片,使用切片的索引下标修改切片中已有的元素值,比如切片 ss =[]int{1, 2},修改第一个元素值的实现代码如下:
package main

import "fmt"

func main() {
    var ss = []int{1, 2}
    fmt.Printf("切片变量ss的元素值为:%v\n", ss)
    // 修改第一个元素值
    ss[0] = 100
    fmt.Printf("切片变量ss的元素值为:%v\n", ss)
}
如果索引下标的数值大于切片长度,那么程序无法编译成功,比如 ss[10]=100,程序将提示索引超出范围,如图 1 所示。


图 1 异常信息

新增切片元素

由于切片是动态数组,即使在定义的时候设置了切片长度,我们还能向切片添加新的元素。在切片中新增元素必须使用内置函数方法 append() 实现,它通常需要设置两个参数,语法格式如下:
ss := append(slice, elems)
内置函数方法 append() 说明如下:
使用 append() 对切片新增元素,如果函数返回值(称为切片变量 B)与原有切片变量 A 的命名相同,那么新增元素后的切片变量 B 将覆盖原有切片变量 A。

如果原有切片变量 A 与新增元素后的切片变量 B 的命名不相同,那么新增元素后的切片变量 B 与原有切片变量 A 是两个独立的切片变量,示例如下:
package main

import "fmt"

func main() {
    var ss = []int{1, 2}
    fmt.Printf("新增元素前的切片ss:%v\n", ss)
    // 新增元素不覆盖原有切片
    sss := append(ss, 3)
    fmt.Printf("新增元素后的切片ss:%v\n", ss)
    fmt.Printf("新切片sss:%v\n", sss)
    // 新增元素并覆盖原有切片
    ss = append(ss, 4)
    fmt.Printf("新增元素后的切片ss:%v\n", ss)
    // 添加多个元素
    ss = append(ss, 5, 6, 7, 8)
    fmt.Printf("新增元素后的切片ss:%v\n", ss)
}
运行上述代码,运行结果为:

新增元素前的切片ss:[1 2] 
新增元素后的切片ss:[1 2] 
新切片sss:[1 2 3] 
新增元素后的切片ss:[1 2 4] 
新增元素后的切片ss:[1 2 4 5 6 7 8]


内置函数方法 append() 还可以实现两个切片之间的拼接,只要 append() 的参数 elems 与参数 slice 是相同数据类型的切片即可实现拼接,示例代码如下:
package main

import "fmt"

func main() {
    var s1 = []int{1, 2, 3}
    var s2 = []int{4, 5, 6, 7}
    ss := append(s1, s2...)
    fmt.Printf("切片变量s1:%v\n", s1)
    fmt.Printf("切片变量s2:%v\n", s2)
    fmt.Printf("切片变量ss:%v\n", ss)
}
实现两个数据类型相同的切片拼接,参数 elems 将作为其中一个切片变量,必须在切片变量后面添加“…”,这是对切片进行解包处理,如上述代码的切片变量 s2…,它是将切片变量 s2 的元素批量添加到切片 s1 中。

如果不对切片变量 s2 执行解包处理,程序就无法实现切片拼接,并提示异常信息。最后运行上述代码,运行结果为:

切片变量s1:[1 2 3] 
切片变量s2:[4 5 6 7] 
切片变量ss:[1 2 3 4 5 6 7]

截取切片元素

如果需要截取切片的部分元素,截取方式是使用索引下标进行定位和截取,截取语法如下:
s := slice[startIndex: endIndex]
截取切片的语法说明如下:
如果截取后的切片变量与截取前的切片变量命名相同,那么截取后的切片变量会覆盖截取前的切片变量;如果两者命名不相同,程序默认设置为两个独立的切片变量。

截取的起始位置和终止位置可以根据实际需要进行设定,并不强制要求,如果没有设置起始位置或终止位置,程序默认为第一个或最后一个元素的位置,示例如下:
package main

import "fmt"

func main() {
    var ss = []int{1, 2, 3, 4, 5, 6, 7}
    // 截取第二个到第五个元素
    s1 := ss[1:4]
    fmt.Printf("截取第二个到第五个元素:%v\n", s1)
    // 截取第三个元素之后的所有元素
    s2 := ss[2:]
    fmt.Printf("截取第三个元素之后的所有元素:%v\n", s2)
    // 截取第三个元素之前的所有元素
    s3 := ss[:2]
    fmt.Printf("截取第三个元素之前的所有元素:%v\n", s3)
    // 如果切片ss没被覆盖,经过截取后不改变原有的切片数据
    fmt.Printf("切片变量ss的值:%v\n", ss)
}
上述代码演示了切片常用的截取方式,分别为:截取切片的固定元素、截取某个元素前面的所有元素、截取某个元素后面的所有元素,具体说明如下:
运行上述代码,运行结果为:

截取第二个到第五个元素:[2 3 4] 
截取第三个元素之后的所有元素:[3 4 5 6 7] 
截取第三个元素之前的所有元素:[1 2] 
切片变量ss的值:[1 2 3 4 5 6 7]


若要删除切片变量的部分元素,首先使用切片截取,过滤掉不需要的切片元素,保留需要的切片元素,然后使用内置函数方法 append() 将两个或多个已保留的切片元素进行拼接。

例如,去掉切片 ss = []int{1, 2, 3, 4, 5, 6, 7} 的元素 4、5、6,实现代码如下:
package main

import "fmt"

func main() {
    var ss = []int{1, 2, 3, 4, 5, 6, 7}
    fmt.Printf("切片ss的元素:%v\n", ss)
    // 删除元素4、5、6,先截取后拼接
    ss = append(ss[:2], ss[6:]...)
    fmt.Printf("切片ss的元素:%v\n", ss)
}
运行上述代码,运行结果为:

切片ss的元素:[1 2 3 4 5 6 7] 
切片ss的元素:[1 2 7]

当切片中需要删除元素的位置是隔位错开时,需要多次使用元素截取,保留有用的元素,再将这些元素拼接成新的切片,这个过程中可能需要多次使用内置函数方法 append()。

复制切片

Go 语言的内置函数方法 copy() 可以将一个切片(数组)复制到另一个切片(数组)中,并且两个切片的数据类型必须相同。如果两个切片(数组)的长度不同,程序按照待复制的切片元素的个数进行复制。

具体语法格式如下:
i := copy(slice1, slice2)
内置函数方法 copy() 的说明如下:
下面通过代码示例说明内置函数方法 copy() 的使用方法,代码如下:
package main

import "fmt"

func main() {
    slice1 := []int{1, 2, 3}
    slice2 := []int{4, 5, 6}
    // 将slice1的元素复制到slice2
    //copy(slice2, slice1)
    // 将slice2的元素复制到slice1
    copy(slice1, slice2)
    fmt.Printf("将slice2的元素复制到slice1:%v\n", slice1)
    fmt.Printf("将slice2的元素复制到slice1:%v\n", slice2)
    slice3 := []int{7, 8, 9, 10}
    // 将slice3的元素复制到slice1
    //copy(slice1, slice3)
    //fmt.Printf("将slice3的元素复制到slice1:%v\n", slice1)
    //fmt.Printf("将slice3的元素复制到slice1:%v\n", slice3)
    // 将slice1的元素复制到slice3
    copy(slice3, slice1)
    fmt.Printf("将slice1的元素复制到slice3:%v\n", slice1)
    fmt.Printf("将slice1的元素复制到slice3:%v\n", slice3)
}
运行上述代码,运行结果为:

将slice2的元素复制到slice1:[4 5 6] 
将slice2的元素复制到slice1:[4 5 6] 
将slice1的元素复制到slice3:[4 5 6] 
将slice1的元素复制到slice3:[4 5 6 10]

分析上面的运行结果,可以得出以下结论:

切片长度与容量

使用内置函数方法 make() 定义切片的时候,必须设置切片长度(也称为切片大小),但内置函数方法 make() 还有一个可选参数 cap,它用于设置切片容量,默认情况下,切片长度与容量是相同的。为了更好地讲述切片长度与容量之间的关系,我们通过示例加以说明:
package main

import "fmt"

func main() {
    // 内置函数方法cap()获取切片容量
    // 内置函数方法len()获取切片长度
    s1 := make([]int, 3, 4)
    fmt.Printf("切片变量s1的值:%v\n", s1)
    fmt.Printf("切片变量s1的长度:%v\n", len(s1))
    fmt.Printf("切片变量s1的容量:%v\n", cap(s1))
    // 第一次添加元素
    s1 = append(s1, 10)
    fmt.Printf("切片变量s1的值:%v\n", s1)
    fmt.Printf("切片变量s1的长度:%v\n", len(s1))
    fmt.Printf("切片变量s1的容量:%v\n", cap(s1))
    // 第二次添加元素
    s1 = append(s1, 10)
    fmt.Printf("切片变量s1的值:%v\n", s1)
    fmt.Printf("切片变量s1的长度:%v\n", len(s1))
    fmt.Printf("切片变量s1的容量:%v\n", cap(s1))
}
运行上述代码,运行结果为:

切片变量s1的值:[0 0 0] 
切片变量s1的长度:3 
切片变量s1的容量:4 
切片变量s1的值:[0 0 0 10] 
切片变量s1的长度:4 
切片变量s1的容量:4 
切片变量s1的值:[0 0 0 10 10] 
切片变量s1的长度:5 
切片变量s1的容量:8

根据运行结果进一步分析切片长度与容量之间的关系,分析如下:
综上分析,当切片长度大于容量的时候,Go 语言将原有容量扩大至两倍,否则元素无法新增到切片中。换句话说,把切片容量比作停车场的停车位,每个停车位只能停一辆车,把切片长度比作停车场的车辆数量,当所有停车位停满的时候,再有一辆车进入停车场,停车场就无法提供车位,只能向外扩展开发更多停车位。

同理,当切片容量等于长度的时候,程序无法存放新的元素,只能扩大切片容量,为新元素提供足够的存储空间。

推荐阅读