Go 学习笔记:slice

356 阅读8分钟

什么是 slice?

Slice 是一种数据结构,它可以看作是一个动态的数组,它的长度可以随着元素的添加和删除而自动扩容或缩小。Slice 指向一个数组的部分连续空间,并包含两个属性:长度和容量。长度表示当前 Slice 中元素的个数,容量表示 Slice 的底层数组中可以容纳的元素个数。Slice 可以通过 make 函数创建,也可以通过对数组或其他 Slice 进行切片操作来创建。

slice 基础知识

创建方式

Go 中有三种常用的 slice 创建方式:

方式 1: 使用 make 函数创建,格式为 make([]T, length, capacity),其中 T 表示 slice 中元素的类型,length 表示 slice 的长度,capacity 表示 slice 的容量。使用 make 函数创建 slice 时,slice 中的每个元素都会被初始化为其类型的零值。

a := make([]int, 5) // 创建一个长度为 5,容量为 5 的 int 类型的 slice
b := make([]string, 3, 5) // 创建一个长度为 3,容量为 5 的 string 类型的 slice

方式 2: 直接使用 slice 字面量创建,格式为 []T{a, b, c},其中 T 表示 slice 中元素的类型,a、b、c 等表示 slice 中的元素。

c := []int{1, 2, 3} // 创建一个包含元素 1, 2, 3 的 int 类型的 slice
d := []string{"hello", "world"} // 创建一个包含元素 "hello", "world" 的 string 类型的 slice

方式 3: 从数组或者另一个 slice 中创建,格式为 a[start:end],其中 a 表示原始数组或 slice,start 表示新 slice 的起始索引,end 表示新 slice 的结束索引(不包含)。

e := [5]int{1, 2, 3, 4, 5} // 创建一个包含 5 个元素的 int 类型的数组
f := e[1:3] // 从数组 e 中创建一个包含索引 1 和 2 的元素的 slice,即 [2, 3]

g := []int{1, 2, 3, 4, 5}
h := g[:3] // 从 slice g 中创建一个包含前三个元素的 slice,即 [1, 2, 3]
i := g[2:] // 从 slice g 中创建一个包含索引 2 及之后的所有元素的 slice,即 [3, 4, 5]

添加操作

Go slice 的添加操作可以通过 append 函数实现。append 函数可以将一个或多个元素添加到 slice 中,并返回一个新的 slice。

例如,假设有一个 slice a,它包含了 1、2、3 三个元素:

a := []int{1, 2, 3}

我们可以使用 append 函数添加一个新元素 4:

a = append(a, 4)

这个操作会返回一个新的 slice,它包含了 1、2、3、4 四个元素。

我们还可以将一个 slice 添加到另一个 slice 中:

b := []int{5, 6, 7}
a = append(a, b...)

这个操作会将 slice b 中的所有元素添加到 slice a 的末尾。

需要注意的是,如果 slice 的底层数组容量不足,append 函数会自动扩容,这可能会导致底层数组地址的改变。因此,在使用 append 函数时,我们需要注意 slice 的容量和地址是否发生了变化。

删除操作

在 Go 语言中,可以使用内置函数 append 来删除切片中的元素。具体方法如下:

slice := []type{...}
slice = append(slice[:index], slice[index+1:]...)

其中,type 为切片中元素的类型,index 为要删除的元素的下标。

例如,我们有一个切片 numbers,包含元素 [1, 2, 3, 4, 5],如果我们想删除下标为 2 的元素 3,则可以使用以下代码:

numbers := []int{1, 2, 3, 4, 5}
numbers = append(numbers[:2], numbers[3:]...)
fmt.Println(numbers) // Output: [1 2 4 5]

这里使用 append 函数将切片的前半部分和后半部分拼接在一起,从而实现删除操作。

截取操作

在 Go 中,对 slice 进行截取操作语法如下:

slice[开始位置:结束位置]

其中,开始位置是 inclusive 的,而结束位置是 exclusive 的。也就是说,取到的元素将包括开始位置,但是不包括结束位置。例如:

arr := []int{1, 2, 3, 4, 5}
fmt.Println(arr[1:3]) // 输出 [2 3]

这里的 arr[1:3] 表示从 arr 中取出从下标为 1 开始,到下标为 3(不包括 3)的元素。

如果开始位置或结束位置没有指定,则表示从开始位置或者结束位置取到最后。例如:

arr := []int{1, 2, 3, 4, 5}
fmt.Println(arr[:3]) // 输出 [1 2 3]
fmt.Println(arr[2:]) // 输出 [3 4 5]
fmt.Println(arr[:]) // 输出 [1 2 3 4 5]

这里的 arr[:3] 表示从 arr 中取出从下标为 0 开始,到下标为 3(不包括 3)的元素;arr[2:] 表示从 arr 中取出从下标为 2 开始,到最后一个元素;arr[:] 表示从 arr 中取出所有的元素。

需要注意的是,slice 截取操作并不会改变原有的 slice,而是返回一个新的 slice。

遍历操作

遍历 slice 可以使用 for-range 实现,使用 range 关键字可以同时获取到 slice 中元素的索引和值。以下是 Go 中遍历 slice 的示例:

a := []int{1, 2, 3, 4, 5}
for index, value := range a {
    fmt.Printf("Index: %d, Value: %d\\n", index, value)
}

输出结果为:

Index: 0, Value: 1
Index: 1, Value: 2
Index: 2, Value: 3
Index: 3, Value: 4
Index: 4, Value: 5

扩容机制

Go 中的 slice 扩容机制是动态的,它会在需要时自动将底层数组的大小增加一倍。当 slice 的容量不足以容纳更多的元素时,它就会自动重新分配一个新的底层数组,并将原有的元素复制到新的底层数组中。

这种机制可以让我们在使用 slice 时更加方便,但需要注意的是,在大数据量的情况下,频繁地扩容会影响程序的性能。因此,在使用 slice 时,我们需要根据实际情况来预估其容量,减少扩容的次数。

如何复制 slice?

在 Go 编程语言中,复制一个 slice 可以有多种方式。

一种简单的方法是使用内置的 copy 函数,copy 函数可以将一个 slice 的内容复制到另一个 slice 中。

// 定义一个源 slice
source := []int{1, 2, 3, 4, 5}
// 定义一个目标 slice,长度与源 slice 相同
target := make([]int, len(source))
// 复制源 slice 到目标 slice
copy(target, source)

上述代码中,我们首先定义了一个源 slice 和一个长度相同的目标 slice。然后,我们使用 copy 函数将源 slice 的内容复制到目标 slice 中。

除了 copy 函数,我们还可以使用 append 函数来复制一个 slice。append 函数可以将一个 slice 追加到另一个 slice 的末尾,从而实现复制的功能。

// 定义一个源 slice
source := []int{1, 2, 3, 4, 5}
// 复制源 slice 到目标 slice
target := append([]int(nil), source...)

上述代码中,我们首先定义了一个源 slice。然后,我们使用 append 函数将源 slice 追加到一个空的 slice 中,从而实现了复制的功能。

slice 与数组的关系

  • 数组是一种固定长度的数据结构,声明时需要指定长度,而且长度不能更改。
  • 数组的优点在于它们是静态分配的,这意味着它们的内存分配是在编译时完成的,所以访问数组元素的速度非常快。
  • 与数组不同,slice 是一个可变长度的序列,是对数组的一个引用,可以动态增加或缩减其长度。slice 由三个部分组成:指向底层数组的指针、长度和容量。
  • slice 的长度和容量可以在运行时进行更改。slice 还具有动态扩容的功能,在底层数组的长度不足以支持 slice 所需容量的情况下,会自动扩容。slice 的访问速度可能会比数组慢一些,因为它们需要在运行时进行内存分配。

为什么推荐用 slice?

在 Go 中,slice 是一种方便、灵活且高效的数据结构,与数组相比,slice 具有更强的动态性和可扩展性。此外,它们还可以更有效地管理内存,因为它们可以动态地增长或缩小它们占用的空间。因此,slice 是处理集合数据的首选方法,而且在 Go 中使用它们非常常见。

slice 的使用注意事项

  • 当向 slice 添加元素时,如果超出了其容量,则会创建一个新的数组并将原有元素复制到其中。这可能会导致性能开销,因此在预知需要添加大量元素的情况下,最好在创建 slice 时指定其容量
  • 在传递 slice 时,实际上传递的是其底层数组的引用。因此,对 slice 的修改可能会影响到其他引用同一底层数组的 slice。如果不希望出现这种情况,则应该使用 copy 函数创建一个新的 slice。
  • 如果需要按照特定的顺序处理 slice 中的元素,则可以使用 sort 包提供的函数。另外,可以使用 sort 包中的 sort.Interface 接口来自定义排序方法。