【入门】Go语言切片详解

发布时间 2023-03-24 15:51:39作者: 躺平摆烂

一、Go语言切片介绍

1.1 什么是切片?

切片(slice)是对数组的一个连续片段的引用,所以切片是一个引用类型,这个片段可以是整个数组,也可以是由起始和终止索引标识的一些项的子集,需要注意的是,终止索引标识的项不包括在切片内。

1.2 数组和切片的区别是什么?

切片和数组密不可分,如果将数组理解为一栋办公楼,那么切片就是把不同的连续楼层出租给使用者,出租的过程需要选择开始楼层和结束楼层,这个过程就会生成切片,数组和切片的区别如下:

  • 定义方式不同:数组的长度是固定的,定义时需要指定长度,如var arr [5]int;而切片的长度是可变的,定义时不需要指定长度,如var s []int
  • 内存分配方式不同:数组是在栈上分配内存,而切片是在堆上分配内存。因为切片的长度是可变的,所以需要在堆上分配内存,以便在运行时动态扩容。
  • 传递方式不同:数组在函数传递时会进行值拷贝,而切片则是传递指针。因为切片是在堆上分配内存,所以传递指针可以避免拷贝整个切片,提高程序效率。
  • 可操作性不同:数组是定长的,长度固定,不支持动态扩容,而切片可以动态扩容,支持append等操作。

二、声明切片

2.1 声明空的切片

语法如下:

var name []type
  • name 表示切片的变量名
  • type 表示切片对应的元素类型

声明空的切片案例如下:

package main

import "fmt"

func main() {
	// 声明空的切片,类型为int
	var intList []int
	// 声明空的切片,类型为string
	var strList []string

	/* 声明空的切片,类型为int
	本来会在{}中填充切片的初始化元素,这里没有填充,所以切片是空的,
	但是此时的 intListEmpty 已经被分配了内存,只是还没有元素 */
	var intListEmpty = []int{}

	// 输出三个切片
	fmt.Println("三个切片的值:", intList, strList, intListEmpty)

	// 输出三个切片的大小
	fmt.Println("三个切片的大小:", len(intList), len(strList), len(intListEmpty))

	/* 切片判定空的结果
	   声明但未使用的切片的默认值是 nil,strList 和 numList 也是 nil,所以和 nil 比较的结果是 true
	   numListEmpty 已经被分配到了内存,但没有元素,因此和 nil 比较时是 false */

	fmt.Println(intList == nil)
	fmt.Println(strList == nil)
	fmt.Println(intListEmpty == nil)
}

代码输出结果:

三个切片的值: [] [] []
三个切片的大小: 0 0 0
true
true
false

2.2 从数组指定范围生成切片

语法如下:

slice [开始位置 : 结束位置]
  • slice:表示目标切片对象
  • 开始位置:对应目标切片对象的索引
  • 结束位置:对应目标切片的结束索引

案例如下:

package main

import "fmt"

func main() {
	var slice [30]int

	for i := 0; i < len(slice); i++ {
		slice[i] = i
	}

	// 输出所有
	fmt.Println(slice[:])
	// 输出 1 - 6 元素对应的值
	fmt.Println(slice[1:6])
	// 输出 9 之后所有元素对应的值
	fmt.Println(slice[9:])
	// 输出 5 之前所有元素对应的值
	fmt.Println(slice[:5])
}

代码输出结果:

[0 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]
[1 2 3 4 5]
[9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29]
[0 1 2 3 4]

2.3 使用make()函数生成切片

make( []type, size, cap )
  • type 切片的数据类型
  • size 切片元素的个数
  • cap 切片预分配元素的容量,可选

案例如下:

package main

import "fmt"

func main() {
	slice1 := make([]int, 2)
	slice2 := make([]int, 2, 10)

	fmt.Println(slice1, slice2)
	// 输出大小
	fmt.Println(len(slice1), len(slice2))
	// 输出cap
	fmt.Println(cap(slice1), cap(slice2))
}

代码输出结果:

[0 0] [0 0]
2 2
2 10

其中 slice1 和 slice2 都是分配2个元素的切片,只不过slice2的内部存储空间已经分配了10个,但真实使用了2个元素。容量不会影响当前的元素个数,因此 len()的值都是2。

三、切片中添加元素

3.1 切片中添加元素

package main

import (
	"fmt"
)

func main() {
	var slice = make([]int, 2, 5)
	slice[0] = 100
	slice[1] = 200
	fmt.Println(slice)
}

代码输出结果:

[100 200]

3.2 append()为切片添加元素

Go语言的内建函数 append() 可以为切片动态添加元素,代码如下所示:

package main

import "fmt"

func main() {
	var slice []int
	slice = append(slice, 1) // 追加1个元素
	slice = append(slice, 2, 3, 4) // 最加多个元素 
	slice = append(slice, []int{5, 6, 7}...) // 追加一个切片
	fmt.Println(slice)
}

代码输出结果:

[1 2 3 4 5 6 7]

注意:在使用 append() 函数为切片动态添加元素时,如果空间不足以容纳足够多的元素,切片就会进行“扩容”,此时新切片的长度会发生改变。切片在扩容时,容量的扩展规律是按容量的 2 倍数进行扩充,例如 1、2、4、8、16……,代码如下:

package main

import "fmt"

func main() {
	var slice []int
	for i := 0; i < 15; i++ {
		slice = append(slice, i)
		fmt.Printf("长度: %v 容量: %v 指针: %p\n", len(slice), cap(slice), slice)
	}
}

代码执行结果:

长度: 1 容量: 1 指针: 0xc000018098
长度: 2 容量: 2 指针: 0xc0000180e0
长度: 3 容量: 4 指针: 0xc00000e1e0
长度: 4 容量: 4 指针: 0xc00000e1e0
长度: 5 容量: 8 指针: 0xc000014280
长度: 6 容量: 8 指针: 0xc000014280
长度: 7 容量: 8 指针: 0xc000014280
长度: 8 容量: 8 指针: 0xc000014280
长度: 9 容量: 16 指针: 0xc000020180
长度: 10 容量: 16 指针: 0xc000020180
长度: 11 容量: 16 指针: 0xc000020180
长度: 12 容量: 16 指针: 0xc000020180
长度: 13 容量: 16 指针: 0xc000020180
长度: 14 容量: 16 指针: 0xc000020180
长度: 15 容量: 16 指针: 0xc000020180

除了可以在切片尾部追加,也可以在切片前面添加 示例如下:

package main

import "fmt"

func main() {
	var a = []int{1, 2, 3}
	a = append([]int{666}, a...) // 前面添加1个元素
	fmt.Println(a)
	a = append([]int{777, 888, 999}, a...) // 前面添加多个元素
	fmt.Println(a)
}

代码运行结果:

[666 1 2 3]
[777 888 999 666 1 2 3]

注意:在切片开头添加元素一般都会导致内存的重新分配,而且会导致已有元素全部被复制 1 次,因此,从切片的开头添加元素的性能要比从尾部追加元素的性能差很多。

3.3 copy()复制切片

Go语言的内置函数 copy() 可以将一个数组切片复制到另一个数组切片中,如果加入的两个数组切片不一样大,就会按照其中较小的那个数组切片的元素个数进行复制。copy() 函数的使用格式如下:

copy( destSlice, srcSlice []T) int
  • srcSlice:数据来源切片
  • destSlice:复制的目标
  • 目标切片必须分配过空间且足够承载复制的元素个数,并且来源和目标的类型必须一致

下面代码展示了copy()函数的过程

package main

func main() {
	slice1 := []int{1, 2, 3, 4, 5}
	slice2 := []int{5, 4, 3}
	copy(slice2, slice1) // 只会复制slice1的前3个元素到slice2中
	copy(slice1, slice2) // 只会复制slice2的3个元素到slice1的前3个位置
}