1 序


图片名称 书接上回,我们在上一章里了解数组的基本定义,并熟知了数组的底层原理,同时也熟悉了数组的一些基本操作,但是Go语言中数组的使用并不多,其根本原因就是数组不够灵活,但是切片使用非常广泛。

在Go语言中,切片是一个拥有相同类型元素动态长度的序列。本质是一个数组的引用,但是与数组不同的是切片是动态的,长度可以在运行时改变,切片的使用更加灵活,通常在实际开发中更常用。 本章节我们将详细介绍切片,并深入探讨它的特性、用法和常见操作。

2 切片底层原理

切片的底层原理是基于数组的一种数据结构。切片是对数组一个连续片段的引用,所以切片是一个引用类型。这个片段可以是整个数组,或者是由起始和终止索引标识的一些项的子集。需要注意的是,终止索引标识的项不包括在切片内。切片提供了一个相关数组的动态窗口。

在Go语言中,切片是对数组的抽象,它提供了更强大的能力和便捷性。切片本身并不是动态数组或者数组指针,而是通过指针引用底层数组,设定相关属性将数据读写操作限定在指定的区域内。你可以把它当作类似下面的一个结构体,内部包含三个元素分别是:指向底层数组的指针、切片的长度和切片的容量。

1
2
3
4
5
type slice struct {
arrayPtr unsafe.Pointer // 指向底层数组的指针
len int // 切片的长度
cap int // 切片的容量
}

当对切片进行操作时,实际上是在操作底层数组。例如,对切片进行追加操作时,会先判断切片的容量是否足够,如果不够则进行扩容操作,将底层数组的长度增加一倍(或根据cap参数指定的值进行扩容),并将切片的指针指向新的数组。

总之,切片的底层原理是基于数组的一种数据结构,通过指针引用底层数组,并提供了更强大的能力和便捷性。

3 切片特点

  • 动态长度
    • 切片长度可以动态增长或缩减,而不需要提前声明容量。
    • 使用append()函数可以向切片追加元素,自动扩容切片。
  • 引用底层数组
    • 切片是对底层数组的一个引用,多个切片可以共享相同的底层数组。
    • 修改切片中的元素会影响到底层数组中的对应元素。
  • 灵活的操作
    • 可以通过切片进行切割(slice)、追加(append)和复制(copy)等操作。
    • 切片提供了便捷的方式来处理数组,规避了数组固定长度的限制。
  • 高效的内存管理
    • 切片是一个轻量级的数据结构,只是包含了指向底层数组的指针、长度和容量等信息。
    • 动态扩容时,Go会自动处理底层数组的重新分配和拷贝,使其更高效。
  • 传递切片:
    • 传递切片时,不会复制整个切片的内容,而是复制切片的引用。
    • 这意味着不同的切片可能共享相同的底层数组,但修改其中一个切片不会影响另一个切片的长度或容量。
  • 不需要声明大小:
    • 与数组不同,切片不需要提前声明大小。它可以根据需要动态调整大小。
    • 理解这些切片的特点可以帮助你更好地利用切片进行数据处理和管理。它们提供了一种便捷且高效的方式来操作数据集合,特别是当处理可变长度数据集合时。

4 定义切片

在Go中,切片的声明、定义和初始化是相对简单的操作。切片可以直接通过字面量声明、也可以由数组或者另一个切片生成、还可以使用make()函数创建。

4.1 var关键字声明

在Go语言中,可以使用var关键字来声明一个切片。切片的声明语法如下:

1
var sliceName []dataType

其中,sliceName是切片的名称,dataType是切片中元素的数据类型,从声明方式来讲切片和数组最大的区别就是:数组会指定size大小而切片不需要指定。

例如,声明一个整数类型的切片:

1
var slice []int

4.2 使用字面量定义切片

我们可以通过var关键字和:=直接声明和初始化切片。这里看示例之前,我们先引出一个字面量的问题。

4.2.1 什么叫字面量

在编程中,字面量(literal)是指表示自己的值的符号或语法表示。字面量直接表示固定的值,而不是表示一个变量或者存储在变量中的值。字面量提供了数据的直接表示方式。

字面量的概念用于表示代码中直接提供常量值的语法结构。在切片字面量中,它允许程序员直接提供切片的内容,以便在程序中直接使用。

切片 (slice) 的语境中,切片字面量是一种用于直接创建切片的表示方式。切片字面量使用数组或者其他切片作为基础来创建新的切片。在不同的编程语言中,切片字面量的语法可能有所不同,但其基本原理是相似的,可以直接定义一个切片的初始内容。

简单总结来就一句话,字面量定义切片就是直接通过一个常量值的来定义一个切片变量,而不是通过索引依次去赋值。我们来看如何使用字面量定义切片。

1
2
3
4
5
6
7
8
9
10
11
import "fmt"

func main() {
var slice1 []int = []int{1, 2, 3, 4, 5}
fmt.Printf("Value=%d,Length=%d,Capacity=%d\n", slice1, len(slice1), cap(slice1))
// Ouptput: Value=[1 2 3 4 5],Length=5,Capacity=5

slice2 := []int{1, 2, 3, 4, 5}
fmt.Printf("Value=%d,Length=%d,Capacity=%d\n", slice2, len(slice2), cap(slice2))
// Ouptput: Value=[1 2 3 4 5],Length=5,Capacity=5
}

我们还可以在{}中使用index:value去指定索引和对应的值,意思就是我们可以选择初始化部分数据,如果中间没有指定索引和值则会设置为各数据类型的零值。

1
2
3
4
5
6
7
8
9
10
11
12
import "fmt"

func main() {
slice := []int{0:1, 2:3, 3, 4, 5}
fmt.Printf("Value=%d,Length=%d,Capacity=%d\n", slice, len(slice), cap(slice))
// Ouptput: Value=[1 0 3 3 4 5],Length=6,Capacity=6

// 上一章节忘记说明其实数组也是可以这样声明的
array := [6]int{0:1, 2:3, 3, 4, 5}
fmt.Printf("Value=%d,Length=%d,Capacity=%d\n", array, len(array), cap(array))
// Ouptput: Value=[1 0 3 3 4 5],Length=6,Capacity=6
}

根据上面的例子,我们可以看出虽然索引为1的位置没有赋值,但是Go编译器自动为我们设置了零值,因为int类型的零值就是0,另外说明一点,通过字面量声明的切片它的Length和Capacity都是一样。

需要注意的是:使用字面量声明切片时,有两种特殊情况:一是nil切片,二是空切片。这两种情况创建出来的切片,其长度为0,是不能直接通过下标的方式来赋值的。

4.2.2 nil切片

nil切片被用在很多标准库和内置函数中,描述一个不存在的切片的时候,就需要用到nil切片。比如函数在发生异常的时候,返回的切片就是nil切片。nil切片的指针指向nil。

1
2
3
4
var slice []int

slice[0] = 1
// 尝试赋值编译不会报错,但是运行会报错:runtime error: index out of range [0] with length 0

4.2.3 空切片

空切片一般会用来表示一个空的集合。比如数据库查询,一条结果也没有查到,那么就可以返回一个空切片。

1
2
3
4
5
// 切片的元素值是切片类型的零值,即 int 0, string '', 引用类型 nil
var slice = []int{}
slice := []{}
// 尝试赋值编译不会报错,但是运行会报错:runtime error: index out of range [0] with length 0
slice[0] = 1

空切片和nil切片的区别在于,空切片指向的地址不是nil,指向的是一个内存地址,但是它没有分配任何内存空间,即底层元素包含0个元素。

最后需要说明的一点是。不管是使用nil切片还是空切片,对其调用内置函数append()len()cap()的效果都是一样的。

4.3 基于数组或切片定义切片

1
sliceName := arrayName[start:end:capEnd]

我们可以通过以上这样形式的语法在数组的基础上生成一个切片,其中start就是开始的索引位置,end就是结束的索引位置,capEnd就是切片的容量结束位置,但不是新切片的容量。

注意:

  1. start、end、capEnd默认都是可以省略的,如果三个值都省略的话,那么[]中的符号 : 就不能省略,否则编译错误,但是符号 : 也只能有一个,否则编译也会报错。
  2. start、end、capEnd三者之间满足一个不等式关系(0 <= start <= end <= capEnd <= len(array) = cap(array)) 。

接下来我们通过具体示例来说明。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import "fmt"

func main() {
// 定义一个数组
array := [6]int{1, 2, 3, 4, 5, 6}
// 通过array[1:4:6]创建一个切片
slice1 := array[1:4:6]
fmt.Printf("Value=%d,Length=%d,Capacity=%d\n", slice1, len(slice1), cap(slice1))
// Ouptput: Value=[2 3 4],Length=3,Capacity=5

// capEnd是可以省略的, capEnd不指定默认是数组容量len(array),两种效果其实是一样的
slice2 := array[1:4]
fmt.Printf("Value=%d,Length=%d,Capacity=%d\n", slice2, len(slice2), cap(slice2))
// Ouptput: Value=[2 3 4],Length=3,Capacity=5
}

通过以上操作我们确定,切片的长度和容量的计算公式:

Length = end - start
Capacity = capEnd - start

从上面的例子我们可以看到,从数组切出来的切片不包含结束的索引位置对应的元素,当然我们也可以不指定end。例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import "fmt"

func main() {
// 定义一个数组
array := [6]int{1, 2, 3, 4, 5, 6}
// 通过array[1:]创建一个切片, end是可以省略的,end不指定默认为数组容量len(array)
slice1 := array[1:]
fmt.Printf("Value=%d,Length=%d,Capacity=%d\n", slice1, len(slice1), cap(slice1))
// Ouptput: Value=[2 3 4 5 6],Length=5,Capacity=5

// 等价于下面这两个表达
slice2 := array[1:6]
// 等价于 slice2 := array[1:6:6]
fmt.Printf("Value=%d,Length=%d,Capacity=%d\n", slice2, len(slice2), cap(slice2))
// Ouptput: Value=[2 3 4 5 6],Length=5,Capacity=5
}

如果你想从第1个元素开始截取,可以不指定start或者start设置为0。例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import "fmt"

func main() {
// 定义一个数组
array := [6]int{1, 2, 3, 4, 5, 6}
// 通过array[0:]创建一个切片
slice1 := array[0:4]
fmt.Printf("Value=%d,Length=%d,Capacity=%d\n", slice1, len(slice1), cap(slice1))
// Ouptput: Value=[1 2 3 4],Length=4,Capacity=6

// 等价于下面的表达
slice2 := array[:4]
fmt.Printf("Value=%d,Length=%d,Capacity=%d\n", slice2, len(slice2), cap(slice2))
// Ouptput: Value=[1 2 3 4],Length=4,Capacity=6
}

如果你想创建一个包含整个数组元素的切片,可以使用不指定前后索引位置。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import "fmt"

func main() {
// 定义一个数组
array := [6]int{1, 2, 3, 4, 5, 6}
// 通过array[:]创建一个切片
slice1 := array[:]
fmt.Printf("Value=%d,Length=%d,Capacity=%d\n", slice1, len(slice1), cap(slice1))
// Ouptput: Value=[1 2 3 4 5 6],Length=6,Capacity=6

// 等价于下面这两个表达
slice2 := array[0:6]
// 等价于 slice2 := array[0:6:6]
fmt.Printf("Value=%d,Length=%d,Capacity=%d\n", slice2, len(slice2), cap(slice2))
// Ouptput: Value=[1 2 3 4 5 6],Length=6,Capacity=6
}

4.4 通过make关键定义切片

1
2
3
4
5
// var关键字
var sliceName = make([]dataType, length, capacity)

// :=符号
sliceName := make([]dataType, length, capacity)

我们可以通过make()函数声明切片时,length参数必填,但capacity可以不指定,如不指定时,其容量默认等于长度值,但是如果指定容量,那容量一定不能小于长度,即0 <= length <= capacity

1
2
3
4
5
6
7
8
9
10
// 以下两种声明方式是等价的
// 方式1
var slice1 = make([]int, 5)
fmt.Printf("Value=%d,Length=%d,Capacity=%d\n", slice1, len(slice1), cap(slice1))
// Ouptput: Value=[0 0 0 0 0],Length=5,Capacity=5

// 方式2
var slice2 = make([]int, 5, 5)
fmt.Printf("Value=%d,Length=%d,Capacity=%d\n", slice2, len(slice2), cap(slice2))
// Ouptput: Value=[0 0 0 0 0],Length=5,Capacity=5

通过以上例子可以看到,通过make关键字创建的切片,切片中的元素值都是零值,要想改变元素值只有后续对它就行重新赋值。

5 访问切片元素

可以通过索引来访问切片中的元素。索引从0开始,逐个递增。例如,要访问上面声明的切片slice的第一个元素,可以使用slice[0]。例如:

1
2
3
4
5
6
7
import "fmt"

func main() {
slice := []int{1, 2, 3, 4, 5}
first := slice[0]
fmt.Println("First element: ", first) // Output: First element: 1
}

6 切片遍历

和数组一样,Go中的切片也有两种方式遍历。你可以使用传统的for循环,也可以使用range关键字。

6.1 For关键字遍历

首先可以通过for关键字遍历,其中需要借助len()函数计算切片长度。

1
2
3
4
5
6
7
8
import "fmt"

func main() {
slice := [...]int{1, 2, 3, 4, 5}
for i := 0; i < len(slice); i++ {
fmt.Println(slice[i])
}
}

6.2 Range关键字遍历

也可以通过range关键字遍历切片,例如:

1
2
3
4
5
6
7
8
import "fmt"

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

如果不需要使用索引,可以通过下划线’_’代替:

1
2
3
4
5
6
7
8
import "fmt"

func main() {
slice := []int{1, 2, 3, 4, 5}
for _, value := range slice {
fmt.Println(value)
}
}

通常来说,我们使用for range方式迭代可能会好一点,因为这种迭代可以保证不会出现数组越界的情况,每次迭代对数组的访问可以省略对下标越界判断,当然具体使用,因实际情况不同而不同。

7 修改切片元素

可以通过索引来修改切片中的元素。例如,要将上面声明的切片slice的第一个元素修改为0,可以使用slice[0] = 0

1
2
3
4
5
6
7
import "fmt"

func main() {
slice := []int{1, 2, 3, 4, 5}
slice[0] = 0
fmt.Println(slice) // Output: [0,2,3,4,5]
}

在这个示例中,slice[0] = 0将切片slice中索引为0的元素修改为0。直接通过索引即可修改切片中特定位置的元素值。

要注意确保索引不超出切片的范围,否则会导致运行时错误。Go中的切片索引从0开始,到len(slice) - 1,超出这个范围会导致越界错误。

切片是引用类型,所以在函数中传递切片本身或者切片的指针都可以修改切片中特定索引的元素值。这是因为切片本身包含了对底层数组的引用,而不是数组的副本,例如。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import "fmt"

func main() {
slice1 := []int{1, 2, 3}
slice2 := changeElement(slice1) // 修改数组元素
fmt.Println(slice1) // Output: [10 2 3]
fmt.Println(slice2) // Output: [10 2 3]
}

// 修改切片第一个元素的值为10,并返回修改后的切皮
func changeElement(slice []int) []int {
slice[0] = 10
return slice
}

8 新增切片元素

在上一章节中我们知道,数组一旦声明,其大小是固定的,无法新增删除元素,但是切片是可以,Go语言提供了内置函数append()来实现给切片增加元素,来看一下append()的方法签名。

1
2
3
4
5
6
7
8
9
10
11
12
13
// The append built-in function appends elements to the end of a slice. If
// it has sufficient capacity, the destination is resliced to accommodate the
// new elements. If it does not, a new underlying array will be allocated.
// Append returns the updated slice. It is therefore necessary to store the
// result of append, often in the variable holding the slice itself:
//
// slice = append(slice, elem1, elem2)
// slice = append(slice, anotherSlice...)
//
// As a special case, it is legal to append a string to a byte slice, like this:
//
// slice = append([]byte("hello "), "world"...)
func append(slice []Type, elems ...Type) []Type

方法签名:该方法接受两个参数,第一个参数是一个切片,第二个参数是一个对应类型的可变参数。
方法说明:该方法用于将单个或多个元素添加到切片的尾部,如果切片的容量足够容纳新增的元素,则追加元素到原来切片,如果不够,底层将会重新申请一个新数组,然后复制原来的数据到新数组,然后返回新切片。

切片新增元素需要考虑在切片哪个位置新增元素,这里有三种情况分别是切片尾部、切片首部和切片中间新增元素,我们分别看看到底如何新增。

8.1 切片首部新增

由于append()函数只能向切片尾部追加元素,所以我们只能先创建一个包含一个或者多个元素的切片,然后利用append方法将原来的切片传入追加到新切片上即可,例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import "fmt"

func main() {
// 切片首部增加元素
var slice = []int{1, 2}
fmt.Printf("Value=%d,Pointer=%p, Length=%d,Capacity=%d\n", slice, &slice, len(slice), cap(slice))
// Output: Value=[1 2],Pointer=0xc000008048, Length=2,Capacity=2

// 首部新增一个元素
slice1 := append([]int{5}, slice...)
fmt.Printf("Value=%d,Pointer=%p, Length=%d,Capacity=%d\n", slice1, &slice1, len(slice1), cap(slice1))
// Output: Value=[5 1 2],Pointer=0xc000008078, Length=3,Capacity=3

// 首部新增多个元素
var newSlice = []int{5, 6, 7}
slice2 := append(newSlice, slice...)
fmt.Printf("Value=%d,Pointer=%p, Length=%d,Capacity=%d\n", slice2, &slice2, len(slice2), cap(slice2))
// Output: Value=[5 6 7 1 2],Pointer=0xc0000080a8, Length=5,Capacity=6
}

8.2 切片尾部新增

切片尾部系只能一个或者多个元素是比较方便的,直接通过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
import "fmt"

func main() {
// 定义切片
var slice = []int{1, 2, 3}
fmt.Printf("Value=%d,Pointer=%p, Length=%d,Capacity=%d\n", slice, &slice, len(slice), cap(slice))
// Output: Value=[1 2 3],Pointer=0xc000008048, Length=3,Capacity=3

// 切片尾部增加元素
// 新增一个元素
slice1 := append(slice, 1)
fmt.Printf("Value=%d,Pointer=%p, Length=%d,Capacity=%d\n", slice1, &slice1, len(slice1), cap(slice1))
// Output: Value=[1 2 3 1],Pointer=0xc000008078, Length=4,Capacity=6

// 新增多个元素
slice2 := append(slice, 1, 2)
fmt.Printf("Value=%d,Pointer=%p, Length=%d,Capacity=%d\n", slice2, &slice2, len(slice2), cap(slice2))
// Output: Value=[1 2 3 1 2],Pointer=0xc0000080a8, Length=5,Capacity=6

// 新增多个元素, 切片作为参数
var newSlice = []int{1, 2, 3}
slice3 := append(slice, newSlice...)
fmt.Printf("Value=%d,Pointer=%p, Length=%d,Capacity=%d\n", slice3, &slice3, len(slice3), cap(slice3))
// Ouptput: Value=[1 2 3 1 2 3],Pointer=0xc0000080d8, Length=6,Capacity=6
}

需要说明的是,当前传入的第二个参数为切片时,需要使用 运算符来辅助解构切片,否则编译错误。

如果切片的容量不足以容纳新的元素,append()方法会创建一个新的数组,并将原始数组的内容复制到新数组中。

8.3 切片中间新增

切片中间指定位置插入一个或者多个元素,相对要麻烦一点,首先要将切片在指定索引位置把切片分成两部分,再将插入元素和分开的两部分切片通过append()函数拼接起接口。

比如需要插入到元素索引i后,则先以i+1为切割点,把slice切割成两半,索引i前数据: slice[:i+1], 索引i后的数据: slice[i+1:],然后再把索引i后的数据: slice[i:]合并到需要插入的元素切片中如:append([]int{6, 7}, slice[i:]…),最后再把合并后的切片合并到索引i前数据: slice[:i]

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import "fmt"

func main() {
// 切片中间某个位置插入元素
var slice = []int{1, 2, 3}
// 比如在元素索引1后增加元素,首先分成两部分
slice1 := slice[:2]
slice2 := slice[2:]
// 要插入的切片数据
slice3 := []int{6, 7}
// 然后拼接三部分切片数据即可, 以下两种方式都可以。
slice4 := append(slice1, append(slice3, slice2...)...)
fmt.Printf("Value=%d,Pointer=%p, Length=%d,Capacity=%d\n", slice4, &slice4, len(slice4), cap(slice4))
// Output: Value=[1 2 6 7 3],Pointer=0xc000008048, Length=5,Capacity=6

slice5 := append(append(slice1, slice3...), slice2...)
fmt.Printf("Value=%d,Pointer=%p, Length=%d,Capacity=%d\n", slice5, &slice5, len(slice5), cap(slice5))
// Output: Value=[1 2 6 7 3],Pointer=0xc000008078, Length=5,Capacity=6
}

9 切片扩容机制

9.1 切片扩容对底层数组的影响

当对切片进行append操作,导致长度超出容量时,就会创建新的数组,这会导致和原有切片的分离。 例如:

1
2
3
4
5
6
7
8
9
10
11
12
import "fmt"

func main() {
slice := make([]int, 5)
slice1 := slice[0:4]
slice = append(slice, 1)
slice[1] = 5
fmt.Printf("Value=%d,Pointer=%p, Length=%d,Capacity=%d\n", slice1, &slice1, len(slice1), cap(slice1))
// Value=[0 0 0 0],Pointer=0xc000008060, Length=4,Capacity=5
fmt.Printf("Value=%d,Pointer=%p, Length=%d,Capacity=%d\n", slice, &slice, len(slice), cap(slice))
// Value=[0 5 0 0 0 1],Pointer=0xc000008048, Length=6,Capacity=10
}

由于slice的长度超出了容量,所以切片slice指向了一个增长后的新数组,而slice1仍然指向原来的老数组,所以之后对slice进行的操作,对slice1不会产生影响。

1
2
3
4
5
6
7
8
9
10
11
12
import "fmt"

func main() {
slice := make([]int, 56)
slice1 := slice[0:4]
slice = append(slice, 1)
slice[1] = 5
fmt.Printf("Value=%d,Pointer=%p, Length=%d,Capacity=%d\n", slice1, &slice1, len(slice1), cap(slice1))
// Value=[0 5 0 0],Pointer=0xc000008060, Length=4,Capacity=6
fmt.Printf("Value=%d,Pointer=%p, Length=%d,Capacity=%d\n", slice, &slice, len(slice), cap(slice))
// Value=[0 5 0 0 0 1],Pointer=0xc000008048, Length=6,Capacity=6
}

本例中,slice的容量为6,因此在append后并未超出容量,所以并不会重新创建新数组,即两切片还是共用一个底层数组。因此,对slice进行的操作,对slice1同样产生了影响。

9.2 扩容探讨

我们先定义一个空切片,然后依次通过append()方法追加元素来看看切片的容量变化。

1
2
3
4
5
6
7
8
9
10
11
import "fmt"

func main() {
slice := []int{}
fmt.Print(cap(slice), " ")
for i := 0; i < 16; i++ {
slice = append(slice, i)
fmt.Print(cap(slice), " ")
}
//0 1 2 4 4 8 8 8 8 16 16 16 16 16 16 16 16
}

通过示例,可以看到,空切片的初始容量为0,但后面向切片中添加元素时,并不是每次添加元素切片的容量都发生了变化。这是因为如果增大容量,也即需要创建新数组,同时还需要将原数组中的所有元素复制到新数组中,开销很大,所以GoLang设计了一套扩容机制,以减少需要创建新数组的次数。

如果我们尝试添加多个元素呢?

1
2
3
4
5
6
7
8
9
10
11
12
import "fmt"

func main() {
slice := []int{}
fmt.Print(cap(slice), " ")
for i := 0; i < 16; i++ {
slice = append(slice, 1, 2, 3, 4, 5)
fmt.Print(cap(slice), " ")
}
//0 6 12 24 24 48 48 48 48 48 96 96 96 96 96 96 96
}

通过示例看起来,当向一个空切片中插入2n-1个元素时,容量是不是就会被设置为2n呢?
我们来试试其他的数据类型?

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
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74

import "fmt"

func main() {
// int8
slice1 := []int8{}
fmt.Print(cap(slice1), " ")
for i := 0; i < 16; i++ {
slice1 = append(slice1, 1, 2, 3, 4, 5)
fmt.Print(cap(slice1), " ")
}
//0 8 16 16 32 32 32 64 64 64 64 64 64 128 128 128 128

// int16
fmt.Println()
slice2 := []int16{}
fmt.Print(cap(slice2), " ")
for i := 0; i < 16; i++ {
slice2 = append(slice2, 1, 2, 3, 4, 5)
fmt.Print(cap(slice2), " ")
}
//0 8 16 16 32 32 32 64 64 64 64 64 64 128 128 128 128

// bool
fmt.Println()
slice3 := []bool{}
fmt.Print(cap(slice3), " ")
for i := 0; i < 16; i++ {
slice3 = append(slice3, true, false, true, false, false)
fmt.Print(cap(slice3), " ")
}
//0 8 16 16 32 32 32 64 64 64 64 64 64 128 128 128 128

// float32
fmt.Println()
slice4 := []float32{}
fmt.Print(cap(slice4), " ")
for i := 0; i < 16; i++ {
slice4 = append(slice4, 1.1, 2.2, 3.3, 4.4, 5.5)
fmt.Print(cap(slice4), " ")
}
//0 6 12 24 24 48 48 48 48 48 96 96 96 96 96 96 96

// float64
fmt.Println()
slice5 := []float64{}
fmt.Print(cap(slice5), " ")
for i := 0; i < 16; i++ {
slice5 = append(slice5, 1.1, 2.2, 3.3, 4.4, 5.5)
fmt.Print(cap(slice5), " ")
}
//0 6 12 24 24 48 48 48 48 48 96 96 96 96 96 96 96

//0 string
fmt.Println()
slice6 := []string{}
fmt.Print(cap(slice6), " ")
for i := 0; i < 16; i++ {
slice6 = append(slice6, "1.1", "2.2", "3.3", "4.4", "5.5")
fmt.Print(cap(slice6), " ")
}
//0 5 10 20 20 40 40 40 40 80 80 80 80 80 80 80 80

// []int
fmt.Println()
slice7 := [][]int{}
fmt.Print(cap(slice7), " ")
temp := []int{1, 2, 3, 4, 5}
for i := 0; i < 16; i++ {
slice7 = append(slice7, temp, temp, temp, temp, temp)
fmt.Print(cap(slice7), " ")
}
//0 5 10 20 20 42 42 42 42 85 85 85 85 85 85 85 85
}

可以看到,根据切片对应数据类型的不同,切片扩容的方式也有很大的区别。

9.3 源码分析

具体为什么会是这样的变化过程,还需要从源码中寻找答案,下面是src/runtime/slice.go中的growslice()函数中的核心部分。

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
func growslice(oldPtr unsafe.Pointer, newLen, oldCap, num int, et *_type) slice {
//......省略
newcap := oldCap
doublecap := newcap + newcap
if newLen > doublecap {
newcap = newLen
} else {
const threshold = 256
if oldCap < threshold {
newcap = doublecap
} else {
// Check 0 < newcap to detect overflow
// and prevent an infinite loop.
for 0 < newcap && newcap < newLen {
// Transition from growing 2x for small slices
// to growing 1.25x for large slices. This formula
// gives a smooth-ish transition between the two.
newcap += (newcap + 3 * threshold) / 4
}
// Set newcap to the requested cap when
// the newcap calculation overflowed.
if newcap <= 0 {
newcap = newLen
}
}
}
//......省略
}

根据源码我们得出:

  • 当需要的容量超过原切片容量的两倍时,会使用需要的容量作为新容量。
  • 当原切片容量小于256时,新切片的容量会直接增加到源容量的2倍。
  • 当原切片的容量大于等于256时,会以原容量的1.25倍增加,直到新容量超过所需要的容量。

总之,GoLang中的切片扩容机制,与切片的数据类型、原本切片的容量、所需要的容量都有关系,其过程比较复杂。 要想具体分析还是要看src/runtime/下的slice.gosizeclasses.go源码研究。

10 删除切片元素

Go没有为切片提供删除元素的方法,不过我们可以使用**sliceName1 := sliceName[start:end:capEnd]**删除元素或者使用内置函数append()来实现给切片删除元素。

和新增切片元素一样,删除也需要考虑在切片哪个位置删除元素,这里有三种情况分别是切片尾部、切片首部和切片中间删除元素,我们依次来看。

10.1 切片首部删除

在切片首部删除元素,我们可以直接通过slice[start:end:capEnd]实现,从而生成一个新的切片,例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import "fmt"

func main() {
// 切片首部删除元素
var slice = []int{1, 2, 3, 4, 5, 6, 7}

// 首部删除一个元素
slice1 := slice[1:]
fmt.Printf("Value=%d,Pointer=%p, Length=%d,Capacity=%d\n", slice1, &slice1, len(slice1), cap(slice1))

// 首部删除多个元素
slice2 := slice[2:]
fmt.Printf("Value=%d,Pointer=%p, Length=%d,Capacity=%d\n", slice2, &slice2, len(slice2), cap(slice2))
}

10.2 切片尾部删除

在切片尾部删除元素,我们可以直接通过slice[start:end:capEnd]实现,从而生成一个新的切片,例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import "fmt"

func main() {
// 切片尾部删除元素
var slice = []int{1, 2, 3, 4, 5, 6, 7}

// 尾部删除一个元素
slice1 := slice[:len(slice) - 1]
fmt.Printf("Value=%d,Pointer=%p, Length=%d,Capacity=%d\n", slice1, &slice1, len(slice1), cap(slice1))

// 尾部删除多个元素
slice2 := slice[:len(slice) - 2]
fmt.Printf("Value=%d,Pointer=%p, Length=%d,Capacity=%d\n", slice2, &slice2, len(slice2), cap(slice2))
}

10.3 切片中间删除

切片中间指定位置删除一个或者多个元素,相对要麻烦一点,首先要将切片在指定索引位置把切片分成两部分,再将删除元素和分开的两部分切片通过append()函数拼接起接口。

1
2
3
4
5
6
7
8
9
10
import "fmt"

func main() {
// 切片中间位置删除元素
var slice = []int{1, 2, 3, 4, 5, 6, 7}

// 从切片中间删除, 如从索引为i,删除2个元素(i+2)
slice1 := append(slice[:1], slice[3:]...)
fmt.Printf("Value=%d,Pointer=%p, Length=%d,Capacity=%d\n", slice1, &slice1, len(slice1), cap(slice1))
}

11 切片复制

因为切片是引用类型,当你将一个切片赋值给另一个变量时,会复制对切片的引用,而不是复制整个切片的内容。

11.1 =复制

当我们使用=复制时,这意味着两个个不同的切片共享一个底层数组,那么对一个切片的修改就会影响到另一个切片,例如

1
2
3
4
5
6
7
8
9
10
11
import "fmt"

func main() {
slice1 := []int{1, 2, 3}
slice2 := slice1 // 复制slice1到slice1
slice1[0] = 10 // 修改slice1的第一个元素
fmt.Printf("Value=%d,Pointer=%p, Length=%d,Capacity=%d\n", slice1, &slice1, len(slice1), cap(slice1))
// Output: Value=[10 2 3],Pointer=0xc000110041, Length=3,Capacity=3
fmt.Printf("Value=%d,Pointer=%p, Length=%d,Capacity=%d\n", slice2, &slice2, len(slice2), cap(slice2))
// Output: Value=[10 2 3],Pointer=0xc000110048, Length=3,Capacity=3
}

这个例子中,修改了slice1的第一个元素为10,slice2的第一个元素也变成了10,因为在赋值时是将slice1的引用复制给了slice2,它们是共享同一个底层数组。所以当你修改slice1中该元素值的时候,slice2也跟着修改了。
如果这个例子对底层数组的修改不是很明显,那我们可以显示地声明一个数组,然后基于数组生成切片,看下面这个例子。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import "fmt"

func main() {
array := [5]int{1, 2, 3, 4, 5}
fmt.Printf("Value=%d,Pointer=%p, Length=%d,Capacity=%d\n", array, &array, len(array), cap(array))
// Output: Value=[1 2 3 4 5],Pointer=0xc0000a8030, Length=5,Capacity=5
slice1 := array[0:3]
slice2 := slice1 // 复制slice1到slice1
slice1[0] = 10 // 修改slice1的第一个元素
fmt.Printf("Value=%d,Pointer=%p, Length=%d,Capacity=%d\n", slice1, &slice1, len(slice1), cap(slice1))
// Output: Value=[10 2 3],Pointer=0xc000094030, Length=3,Capacity=5
fmt.Printf("Value=%d,Pointer=%p, Length=%d,Capacity=%d\n", slice2, &slice2, len(slice2), cap(slice2))
// Output: Value=[10 2 3],Pointer=0xc000094048, Length=3,Capacity=5
fmt.Printf("Value=%d,Pointer=%p, Length=%d,Capacity=%d\n", array, &array, len(array), cap(array))
// Output: Value=[10 2 3 4 5],Pointer=0xc0000a8030, Length=5,Capacity=5
}

通过打印结果,可以看到基于数组生成的切片,通过索引修改切片slice1的元素值会影响底层数组值,因此指向同一数组的slice2的值也会更改。

11.2 内置copy()复制

在Go中,可以使用内置的copy()函数来复制切片的内容到另一个切片。copy()函数允许将一个切片的元素复制到另一个切片中,它能够确保两个切片之间没有共享底层数组。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import "fmt"

func main() {
slice1 := []int{1, 2, 3, 4, 5}
slice2 := make([]int, len(slice1)) // 创建一个与slice1长度相同的空切片

// 将 slice1 中的元素复制到 slice2
copy(slice2, slice1)

fmt.Printf("Value=%d,Pointer=%p, Length=%d,Capacity=%d\n", slice1, &slice1, len(slice1), cap(slice1))
// Output: Value=[10 2 3],Pointer=0xc000094030, Length=5,Capacity=5
fmt.Printf("Value=%d,Pointer=%p, Length=%d,Capacity=%d\n", slice2, &slice2, len(slice2), cap(slice2))
// Output: Value=[10 2 3],Pointer=0xc000094048, Length=5,Capacity=5
}

需要注意的是,copy()函数只复制切片中的元素内容,而不会共享底层数组,这意味着对一个切片的修改不会影响到另一个切片,它们是独立的。

12 切片比较

在Go中,切片不能直接使用==运算符进行比较,因为切片是引用类型,它们指向不同的底层数组即使内容相同也不会被认为相等

注意:当我们使用使用 == 符号比较两个切片的时候,编译错误,切片只有和nil比较才能使用 == 符号比较判断。

要比较两个切片是否相等,你需要逐个比较它们的元素。可以编写循环来比较切片中的每个元素,或者使用reflect.DeepEqual()函数进行比较。

12.1 自定义函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import "fmt"

// 切片比较函数
func slicesEqual(slice1, slice2 []int) bool {
if len(slice1) != len(slice2) {
return false
}
for i := range slice1 {
if slice1[i] != slice2[i] {
return false
}
}
return true
}

func main() {
slice1 := []int{1, 2, 3, 4, 5}
slice2 := []int{1, 2, 3, 4, 5}

slice1 == slice2 // invalid operation: slice1 == slice2 (slice can only be compared to nil)

result := slicesEqual(slice1, slice2)
fmt.Println("Slices are equal:", result) // 输出 true
}

12.2 reflect.DeepEqual()函数

reflect.DeepEqual()函数可以比较两个接口类型的值,包括切片。但请注意,这种方法有一些限制,不适用于所有类型的切片,且在性能上可能不如手动比较。

1
2
3
4
5
6
7
8
9
10
11
12
import (
"fmt"
"reflect"
)

func main() {
slice1 := []int{1, 2, 3, 4, 5}
slice2 := []int{1, 2, 3, 4, 5}

result := reflect.DeepEqual(slice1, slice2)
fmt.Println("Slices are equal:", result) // 输出 true
}

这两种方法都可以用于比较切片,但根据具体情况选择合适的方法。手动比较元素适用于大多数情况,而reflect.DeepEqual()可能更适合于一些特殊情况。

13 总结

经过这一章节,相信大家对切片已经比较了解,同时和数组区别有了一个更深的认识,下一章节我们将继续介绍一种常用的数据结构-映射(map)