文章

【Golang】切片 slice

切片(slice)是go语言提供的动态数组,本质上是对底层动态数组的引用。

【Golang】切片 slice

slice

数组(array)的大小都是固定的。

切片(slice)为数组元素提供了动态大小的、灵活的视角。在实践中,切片比数组更常用。

切片类似数组的引用

切片的底层是动态数组。

切片是对数组的引用,它不存储任何数据,只是描述了底层数组中的一段。

更改切片的元素会修改其底层数组中对应的元素。

和它共享底层数组的切片都会观测到这些修改。但触发扩容时,发生扩容的切片会成为新数组的引用

通过数组创建的切片:

  • 切片与数组共享内存空间。
  • 在切片进行append操作时,若数组空间有空闲,会将数组下一个位置设置为给定值。
  • 在切片进行append操作时,若数组空间不够用,则会分配一个更大的底层数组,该切片成为新数组的引用,和原数组不再关联,它们互相独立。

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
package main

import "fmt"

func main() {
    langs := [5]string{"C", "C++", "Golang", "Python", "Cangjie"}
    slice1 := langs[0:3]  // []string{"C", "C++", "Golang"}
    slice2 := langs[1:3]  // []string{"C++", "Golang"}

    fmt.Printf("%T, %v\n", langs, langs)     // [5]string{"C", "C++", "Golang", "Python", "Cangjie"}
    fmt.Printf("%T, %v\n", slice1, slice1)   // []string{"C", "C++", "Golang"}
    fmt.Printf("%T, %v\n", slice2, slice2)   // []string{"C++", "Golang"}
    fmt.Println("------------------------")

    slice2[1] = "XXX"
    fmt.Printf("%T, %v\n", langs, langs)     // [5]string{"C", "C++", "XXX", "Python", "Cangjie"}
    fmt.Printf("%T, %v\n", slice1, slice1)   // []string{"C", "C++", "XXX"}
    fmt.Printf("%T, %v\n", slice2, slice2)   // []string{"C++", "XXX"}
    fmt.Println("------------------------")

    slice1 = append(slice1, "Java")   // 数组空间有空闲、够用
    fmt.Printf("%T, %v\n", langs, langs)     // [5]string{"C", "C++", "XXX", "Java", "Cangjie"}
    fmt.Printf("%T, %v\n", slice1, slice1)   // []string{"C", "C++", "XXX", "Java"}
    fmt.Printf("%T, %v\n", slice2, slice2)   // []string{"C++", "XXX"}
    fmt.Println("------------------------")

    slice3 := langs[:]
    slice3 = append(slice3, "PHP")   // 数组没有多余空间
    fmt.Printf("%T, %v\n", langs, langs)     // [5]string{"C", "C++", "XXX", "Java", "Cangjie"}
    fmt.Printf("%T, %v\n", slice1, slice1)   // []string{"C", "C++", "XXX", "Java"}
    fmt.Printf("%T, %v\n", slice2, slice2)   // []string{"C++", "XXX"}
    fmt.Printf("%T, %v\n", slice3, slice3)   // [5]string{"C", "C++", "XXX", "Java", "Cangjie", "PHP"}
    fmt.Println("------------------------")
}

输出结果如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
[5]string, [C C++ Golang Python Cangjie]
[]string, [C C++ Golang]
[]string, [C++ Golang]
------------------------
[5]string, [C C++ XXX Python Cangjie]
[]string, [C C++ XXX]
[]string, [C++ XXX]
------------------------
[5]string, [C C++ XXX Java Cangjie]
[]string, [C C++ XXX Java]
[]string, [C++ XXX]
------------------------
[5]string, [C C++ XXX Java Cangjie]
[]string, [C C++ XXX Java]
[]string, [C++ XXX]
[]string, [C C++ XXX Java Cangjie PHP]
------------------------

通过切片创建的切片,示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
package main

import "fmt"

func main() {
    var names = []string{"蒙奇·D·路飞", "罗罗诺亚·索隆", "木下秀吉", "利姆鲁·特恩佩斯特"}
    n1 := names[0:3]
    n2 := names[2:4]

    fmt.Printf("%T, %v\n", names, names)
    fmt.Printf("%T, %v\n", n1, n1)
    fmt.Printf("%T, %v\n", n2, n2)
    fmt.Println("-----------------------------------")

    n1[2] = "2233"

    fmt.Printf("%T, %v\n", names, names)
    fmt.Printf("%T, %v\n", n1, n1)
    fmt.Printf("%T, %v\n", n2, n2)
    fmt.Println("-----------------------------------")

    n1 = append(n1, "二刺猿")

    fmt.Printf("%T, %v\n", names, names)
    fmt.Printf("%T, %v\n", n1, n1)
    fmt.Printf("%T, %v\n", n2, n2)
    fmt.Println("-----------------------------------")


    n1 = append(n1, "😄")  // 触发扩容,只有 n1 发生变化
                           // n1 成为新数组的引用,n2 和 names 仍然是同一个动态数组的引用

    fmt.Printf("%T, %v\n", names, names)
    fmt.Printf("%T, %v\n", n1, n1)
    fmt.Printf("%T, %v\n", n2, n2)
    fmt.Println("-----------------------------------")


    n1[0] = "🎇"     // 只有 n1 发生变化

    fmt.Printf("%T, %v\n", names, names)
    fmt.Printf("%T, %v\n", n1, n1)
    fmt.Printf("%T, %v\n", n2, n2)
    fmt.Println("-----------------------------------")
}

输出如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
[]string, [蒙奇·D·路飞 罗罗诺亚·索隆 木下秀吉 利姆鲁·特恩佩斯特]
[]string, [蒙奇·D·路飞 罗罗诺亚·索隆 木下秀吉]
[]string, [木下秀吉 利姆鲁·特恩佩斯特]
-----------------------------------
[]string, [蒙奇·D·路飞 罗罗诺亚·索隆 2233 利姆鲁·特恩佩斯特]
[]string, [蒙奇·D·路飞 罗罗诺亚·索隆 2233]
[]string, [2233 利姆鲁·特恩佩斯特]
-----------------------------------
[]string, [蒙奇·D·路飞 罗罗诺亚·索隆 2233 二刺猿]
[]string, [蒙奇·D·路飞 罗罗诺亚·索隆 2233 二刺猿]
[]string, [2233 二刺猿]
-----------------------------------
[]string, [蒙奇·D·路飞 罗罗诺亚·索隆 2233 二刺猿]
[]string, [蒙奇·D·路飞 罗罗诺亚·索隆 2233 二刺猿 😄]
[]string, [2233 二刺猿]
-----------------------------------
[]string, [蒙奇·D·路飞 罗罗诺亚·索隆 2233 二刺猿]
[]string, [🎇 罗罗诺亚·索隆 2233 二刺猿 😄]
[]string, [2233 二刺猿]
-----------------------------------

切片长度与容量

切片的长度(length)就是它所包含的元素个数,对应C++ vector的size。

切片的容量(capacity)是从它的第一个元素开始数,到其底层数组元素末尾的个数, 即当前最多可以保存的元素数量,超过这个数量就会触发扩容,对应C++ vector的capacity。

切片 s 的长度和容量可通过表达式 len(s)cap(s) 来获取。

nil 切片

切片的零值nil

值为零值nil的切片称为nil 切片

nil 切片的长度容量为 0 且没有底层数组。

代码验证:

1
2
3
4
5
6
7
8
9
10
11
package main

import "fmt"

func main() {
    var s []int
    fmt.Println(s, len(s), cap(s))
    if s == nil {
        fmt.Println("nil!")
    }
}

删除切片元素

使用append函数

append 函数用于将元素添加到现有切片。 通过将新切片与现有切片中的元素相结合,您可以有效地从现有切片中删除元素。

以 []int 类型切片为例:

1
2
3
func DelElement(slice []int, index int) []int {
    return append(slice[:index], slice[index+1:]...)
}

使用copy函数

copy 函数用于复制切片中的元素。通过将切片元素复制到一个较小的切片中,您可以有效地从原始切片中移除元素。

以 []int 类型切片为例:

1
2
3
4
func DelElem(slice []int, index int) []int {
    copy(slice[index:], slice[index+1:])
    return slice[:len(slice)-1]
}

使用slice创建新切片

您可以使用切片语法 [start:end] 来创建一个新的切片,其中包含原始切片的一部分。 这使您可以有效地跳过要删除的元素。

1
2
3
4
5
6
7
8
// 创建一个整数切片
slice := []int{1, 2, 3, 4, 5}

// 从切片中删除索引为 2 的元素
slice = slice[:2]

// 打印修改后的切片
fmt.Println(slice) // 输出:[1 2]

使用for循环

虽然其他方法效率更高,但您也可以使用 for 循环从切片中删除元素。 此方法涉及遍历切片元素并逐个删除它们。

以 []int 类型切片为例:

1
2
3
4
5
6
7
func DeleteElem(slice []int, index int) []int {
    for i := index; i<len(slice)-1; i++ {
        slice[i] = slice[i+1]
    }
    slice = slice[:len(slice)-1]
    return slice
}
本文由作者按照 CC BY 4.0 进行授权