数组和切片

教程地址:点击去往视频教程open in new window

1. 数组

数组是一组连续内存空间存储的具有相同类型的数据,是一种线性结构

在Go语言中,数组的长度是固定的。

image-20211219005311769

数组声明:var 数组变量名 [元素数量]Type

  • 数组变量名:数组声明及使用时的变量名。
  • 元素数量:数组的元素数量,可以是一个表达式,但最终通过编译期计算的结果必须是整型数值,元素数量不能含有到运行时才能确认大小的数值。
  • Type:可以是任意基本类型。
func main() {
	//默认数组中的值是类型的默认值
	var arr [3]int
	//通过下标取值
	fmt.Println(arr[0])
	fmt.Println(arr[1])
	fmt.Println(arr[2])
}
1
2
3
4
5
6
7
8

2. 数组使用

  • 赋值

    • 初始化方式

      func main() {
      	var arr [3]int = [3]int{1, 2, 3}
      	//如果第三个不赋值,就是默认值0
      	var arr1 [3]int = [3]int{1, 2}
      	//可以使用简短声明
      	arr2 := [3]int{1, 2, 3}
      	//如果不写数据数量,而使用...,表示数组的长度是根据初始化值的个数来计算
      	arr3 := [...]int{1, 2, 3}
      	//给索引为2的赋值 ,所以结果是 0,0,3
      	arr4 := [3]int{2: 3}
      	fmt.Println(arr, arr1, arr2, arr3, arr4)
      }
      
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
    • 索引方式

      func main() {
      	var arr [3]int
      	arr[0] = 5
      	arr[1] = 6
      	arr[2] = 7
      	fmt.Println(arr)
      }
      
      1
      2
      3
      4
      5
      6
      7
  • 取值

    • 索引方式

      func main() {
      	var arr [3]int = [3]int{1, 2, 3}
      	fmt.Println(arr[0])
      	fmt.Println(arr[1])
      	fmt.Println(arr[2])
      }
      
      1
      2
      3
      4
      5
      6
    • for range

      func main() {
      	var arr [3]int = [3]int{1, 2, 3}
      	for index, value := range arr {
      		fmt.Printf("索引:%d,值:%d \n", index, value)
      	}
      }
      
      1
      2
      3
      4
      5
      6

3. 多维数组

img

func main() {
	// 声明一个二维整型数组,两个维度的长度分别是 4 和 2
	var array [4][2]int
	fmt.Println(array)
}
1
2
3
4
5

用法和一维数组一样。

4. 切片

问题:

func main() {
	var array [4]int
	change(array)
    //array的值修改了吗?
	fmt.Println(array)
}

func change(array [4]int) {
	array[0] = 10
}
1
2
3
4
5
6
7
8
9
10

img

切片(slice)是对数组的一个连续片段的引用,所以切片是一个引用类型

切片的零值为nil

格式:slice [开始位置 : 结束位置] (左闭右开的区间,不含右索引项)

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

Go语言中切片的内部结构包含地址大小容量,切片一般用于快速地操作一块数据集合。

func main() {
	var a = [3]int{1, 2, 3}
	//a[1:2] 生成了一个新的切片
	fmt.Println(a, a[1:2])
}
1
2
3
4
5

在看下面代码:

func main() {
	var array []int = []int{0, 0, 0}
	change(array)
	//array的值修改了吗? 为什么呢?
	fmt.Println(array)
}

func change(array []int) {
	array[0] = 10
}
1
2
3
4
5
6
7
8
9
10

从数组或切片生成新的切片拥有如下特性:

  • 取出的元素数量为:结束位置 - 开始位置;
  • 取出元素不包含结束位置对应的索引,切片最后一个元素使用 slice[len(slice)] 获取;
  • 当缺省开始位置时,表示从连续区域开头到结束位置(a[:2])
  • 当缺省结束位置时,表示从开始位置到整个连续区域末尾(a[0:])
  • 两者同时缺省时,与切片本身等效(a[:])
  • 两者同时为 0 时,等效于空切片,一般用于切片复位(a[0:0])

注意:超界会报运行时错误,比如数组长度为3,则结束位置最大只能为3

切片也可以直接声明和赋值:

// 声明整型切片
var numList []int
// 声明一个空切片
var numListEmpty = []int{}
//声明string切片并初始化
var strList []string = []string{"hello","mszlu"}
1
2
3
4
5
6

切片也可以用make关键字来创建:

func main() {
    //语法:make([]Type, len, cap)  
    //len是初始化长度,cap是预分配内存空间,
    //降低多次分配空间造成的性能问题。
	var s []int = make([]int, 5, 10)
	fmt.Println(s)
}
1
2
3
4
5
6
7

make仅用于用于创建slice、map和channel,分配内存空间并且初始化

了解切片后,在看一个思考题:

func main() {
	var numbers4 = [...]int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
	myslice := numbers4[4:6]
	//这打印出来长度为2
	fmt.Printf("myslice为 %d, 其长度为: %d\n", myslice, len(myslice))
	myslice = myslice[:cap(myslice)]
	//为什么 myslice 的长度为2,却能访问到第四个元素
	fmt.Printf("myslice的第四个元素为: %d", myslice[3])
}
1
2
3
4
5
6
7
8
9

5. 切片复制

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

格式: copy( destSlice, srcSlice []T) int

  • srcSlice 为数据来源切片
  • destSlice 为复制的目标(也就是将 srcSlice 复制到 destSlice
  • 目标切片必须分配过空间且足够承载复制的元素个数,并且来源和目标的类型必须一致,copy() 函数的返回值表示实际发生复制的元素个数。

例子:

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个位置
1
2
3
4

copy属于深拷贝(浅拷贝是只copy地址,深拷贝是值copy)

func main() {
	slice1 := []int{1, 2, 3, 4, 5}
	slice2 := []int{5, 4, 3}
	copy(slice2, slice1)
    //修改slice2 并不会对slice1造成影响
	slice2[0] = 10
	fmt.Println(slice1, slice2)
}
1
2
3
4
5
6
7
8

6. 切片扩容

可以使用append对切片新增元素:

func main() {
	slice1 := []int{1, 2, 3, 4, 5}
	slice2 = append(slice1, 10)
    //结果 1,2,3,4,5和1,2,3,4,5,10
    //这里注意,append后会生成一个新切片(新的地址)
    //如果容量够 不会新生成一个切片
	fmt.Println(slice1,slice2)
}
1
2
3
4
5
6
7
8

前面我们知道,切片有长度容量,容量代表实际其占用的内存空间,当我们在给切片添加元素时,内存占用会变多,这时候就会发生频繁的内存空间分配,这是比较耗费性能的,所以go做了一些优化。

func main() {
	slice1 := []int{1, 2, 3, 4, 5}
	slice1 = append(slice1, 10)
	//打印  长度:6,容量:10
	fmt.Printf("长度:%d,容量:%d", len(slice1), cap(slice1))
}
1
2
3
4
5
6

大家发现,容量为什么是10,而不是6呢?

为了防止频繁发生内存分配,在append增加元素时,如果容量不足,在扩容时,会做一定的策略来优化:

  • 容量小于256时,两倍扩容
  • 容量大于等于256时,按照newcap += (newcap + 3*256) >> 2这个公式扩容,越大扩容越小,直到1.25倍
  • 实际的容量,在上述的基础上,还会进行内存对齐

这里引申出一个概念叫内存对齐

  • 因为CPU访问的规则,未对齐的内存,会造成CPU多次访问,耗费性能

7. 切片转数组

func main() {
    //go1.17版本的新特性
	slice1 := make([]int, 2, 8)
	arr := *(*[2]int)(slice1)
	fmt.Println(arr)
}
1
2
3
4
5
6
func main() {
    //go1.20新特性
	slice1 := make([]int, 2, 8)
	arr := [2]int(slice1)
	fmt.Println(arr)
}
1
2
3
4
5
6