Go 语言系列教程(六) : 数组和切片深入解析

前言

数组大家都知道,切片就是一种可以动态增长的数组。可以参考Java的List

数组

数组的特点

  • 数组中是固定长度的连续空间(内存区域),长度固定所以在go语言中很少使用
  • 数组中所有元素的类型是一样的,默认情况下,数组的每个元素都被初始化为元素类型对应的零
  • 数组的长度是数组类型的一个组成部分,因此 [3]int 和 [4]int 是两种不同的数组类型。数组的长度必须是常量表达式,因为数组的长度需要在编译阶段确定。

数组的声明

  1. 常规的数组声明方法,它的类型是[10]int,数组长度被认为是数组类型的一部分
var array1   [10]int 
  1. 常用用法,给定初始化的值,未被初始化的值以该类型的零值填充
var array2 = [10]int{
    
    1, 2, 3}
  1. 不给定长度,使用…关键词告知编译期根据初始化的元素个数推断长度
array3 := [...]int{
    
    1, 2, 3, 4}

数组的容量和长度

以上面为例我们来测试一下数组的容量和长度

package main

import "fmt"

func main() {
    
    
	fmt.Println("------------数组----------------")
	//数组中是固定长度的连续空间(内存区域)
	//数组中所有元素的类型是一样的
	var array1   [10]int           //1.常规的数组声明方法,它的类型是[10]int,数组长度被认为是数组类型的一部分
	var array2 = [10]int{
    
    1, 2, 3}  //2.常用用法,给定初始化的值,未被初始化的值以该类型的零值填充
	array3 := [...]int{
    
    1, 2, 3, 4} //3.不给定长度,使用...关键词告知编译期根据初始化的元素个数推断长度
	fmt.Printf("array1 type:%T len:%d, cap:%d\n", array1, len(array1), cap(array1))
	fmt.Printf("array2 type:%T len:%d, cap:%d\n", array2, len(array2), cap(array2))
	fmt.Printf("array2 type:%T len:%d, cap:%d\n", array3, len(array3), cap(array3))

	//大家可以看到 array2的长度和容量也是10 为什么呢? 这个是因为未被初始化的值以该类型的零值填充
	//下面我们输出一下array2
	for k ,v := range array2 {
    
    
		fmt.Printf("key : %v  value:  %v  \n", k, v)
	}

	//Tip
	//在Go中数组的长度属于数组类型的一部分
	//所以在函数调用时如果参数类型是数组,那么每次传参都会发生一次数组的copy,这个对性能的影响还是比较大的,所以我们一般在Go中都是使用slice这种数据结构。
	//在Go中数组是值类型 这点要和C/C++区分

}

结果

------------数组----------------
array1 type:[10]int len:10, cap:10
array2 type:[10]int len:10, cap:10
array2 type:[4]int len:4, cap:4
key : 0  value:  1  
key : 1  value:  2  
key : 2  value:  3  
key : 3  value:  0  
key : 4  value:  0  
key : 5  value:  0  
key : 6  value:  0  
key : 7  value:  0  
key : 8  value:  0  
key : 9  value:  0  

多维数组

  • 声明二维数组,只要 任意加中括号,可以声明更多维,相应占用空间指数上指
	var arr [3][3]int
	//赋值
	arr = [3][3]int{
    
    
		{
    
    1, 2, 3},
		{
    
    2, 3, 4},
		{
    
    3, 4, 5},
	}
	fmt.Println(arr)

结果

[[1 2 3] [2 3 4] [3 4 5]]

切片

切片这个我会讲的很啰嗦很多,因为确实很重要 很多概念大家要理清楚,请大家耐心看

切片声明

var sliceTmp []int // 第一种 常用
var slice1 = make([]int, slen, scap)//这种方法可读性比较好,显示指定了slice的长度和容量

Tip:要注意的是slice类型的变量s和数组类型的变量a的初始化语法的差异。slice和数组的字面值 语法很类似,它们都是用花括弧包含一系列的初始化元素,但是对于slice并没有指明序列的 长度。这会隐式地创建一个合适大小的数组,然后slice的指针指向底层的数组。

切片底层详解

  • 变长的序列,序列中每个元素都有相同的类型。一个slice类型一般写作 []T,其中T代表slice中元素的类型;slice的语法和数组很像,只是没有固定长度而已。
  • 一个slice由三个部分 构成:指针长度和容量。指针指向第一个slice元素对应的底层数组元素的地址,要注意的 是slice的第一个元素并不一定就是数组的第一个元素。长度对应slice中元素的数目;长度不能超过容量,容量一般是从slice的开始位置到底层数据的结尾位置。内置的len和cap函数分 别返回slice的长度和容量。
  • 多个slice之间可以共享底层的数据,并且引用的数组部分区间可能重叠。

案例引用自 The Go Programming Language p123

下图显示了表示一 年中每个月份名字的字符串数组,还有重叠引用了该数组的两个slice。数组这样定义

months := [...]string{
    
    1:"January",2:"February",3:"March",4:"April",5:"May",6:"June",7:"July",8:"August",9:"September",10:"October",11:"November",12:"December"}

值得注意的是:一月份是months[1],十二月份是months[12]。通常,数组的第一个元素从索引0开始,但是月份一般是从1开始的,因此我们声明数组时直接跳过第0个元素,第0个元素会被自动初始 化为空字符串。

slice的切片(截取)操作s[i:j],其中0≤i≤j≤cap(s),用于创建一个新的slice,引用s的从第i个元素开 始到第j-1个元素的子序列 (左闭右开)。新的slice将只有 j-i 个元素。如果i位置的索引被省略的话将使用0代替,如果j位置的索引被省略的话将使用len(s)代替。因此,months[1:13]切片操作将引用全部有效的月份,和months[1:]操作等价;months[:]切片操作则是引用整个数组。让我们分别定义 表示第二季度和北方夏天月份的slice,它们有重叠部分:
注意观察思考为什么Q2的容量是7,summer的容量是9
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Wl99TCvm-1600849391354)(http://www.codesuger.com/upload/2020/09/image-0d4a477c1bd043418abdc3c6169052dd.png)]

	Q2 := months[4:7]
	summer := months[6:9]
	fmt.Println(Q2)        // ["April" "May" "June"]
	fmt.Println(summer)    // ["June" "July" "August"]
  • 如果切片操作超出cap(s)的上限将导致一个panic异常,但是超出len(s)则是意味着扩展了 slice,因为新slice的长度会变大:
	fmt.Println(summer[:20]) //slice bounds out of range [:20] with capacity 7
	endlessSummer := summer[:5]
	fmt.Println(endlessSummer) //[June July August September October]

切片操作

比较操作

和数组不同的是,slice之间不能比较,因此我们不能使用==操作符来判断两个slice是否含有 全部相等元素。不过标准库提供了高度优化的bytes.Equal函数来判断两个 字节型slice 是否相等([]byte),但是对于其他类型的slice,我们必须自己展开每个元素进行比较:

func equal(x, y []string) bool {
    
    

	if len(x) != len(y) {
    
    
		return false
	}
	for k := range x {
    
    
		if x[k] != x[k] {
    
    
			return false
		}

	}
	return true
}

探讨: 为何slice不直接支持比较运算符呢??

  • 第一个原因,一个slice的元素是 间接引用的,一个slice甚至可以包含自身。虽然有很多办法处理这种情形,但是没有一个是 简单有效的。
  • 因为slice的元素是间接引用的,一个固定的slice值(译注:指slice本身的值,不 是元素的值)在不同的时刻可能包含不同的元素,因为底层数组的元素可能会被修改,例如slice扩容,就会导致其本身的值/地址变化
  • slice唯一合法的比较操作是和nil比较,例如:
	//当然这种写法也是不规范的
	if summer == nil {
    
    
		fmt.Println("切片为nil")
	}
  • 一个nil值的slice并没有底层数组。一个nil值的slice的长度和容量都是0,如果你需要测试一个slice是否是空的,使用len(s)==0来判断,而不应该用s == nil来判断。
  • 最后:一个nil值的slice的行为和其它任意0长度的slice一样;例如reverse(nil) 也是安全的。除了明确说明的地方,所有的Go语言函数应该以相同的方式对待nil值 的slice和0长度的slice。

追加操作–append函数

内置的append函数用于向slice追加元素:

	//追加操作
	var runes []rune
	for _, r := range "hello , 世界" {
    
    
		runes = append(runes, r)
	}
	fmt.Printf("%q  长度: %v \n ", runes,len(runes))

结果

['h' 'e' 'l' 'l' 'o' ' ' ',' ' ' '世' '界']  长度: 10 

探讨: 为何slice不直接用string而用rune??

  • rune是Go语言中一种特殊的数据类型,它是int32的别名,几乎在所有方面等同于int32,用于区分字符值和整数值
package main

import "fmt"

func main() {
    
    

    var str = "hello 世界"
    fmt.Println("len(str):", len(str))

}
结果是 12 

12怎么来的呢??从字符串字面值看len(str)的结果应该是8,但在Golang中string类型的底层是通过byte数组实现的,在unicode编码中,中文字符占两个字节,而在utf-8编码中,中文字符占三个字节而Golang的默认编码正是utf-8.

hello是5个字节
一个空格是1个字节
世界 这两个字占6个字节
5+1+6=12 就这么来的

如果想要获得真实的字符串长度而不是其所占用字节数,有两种方法实现

  • 使用unicode/utf-8包中的RuneCountInString方法
str := "hello 世界"
fmt.Println("RuneCountInString:", utf8.RuneCountInString(str))
  • 将字符串转换为rune类型的数组再计算长度
str := "hello 世界"
fmt.Println("rune:", len([]rune(str)))

额扯远了,让我们回到正题 append函数对于理解slice底层是如何工作的非常重要,所以让我们仔细查看究竟是发生了什么,下面我们手写实现一个appendInt函数,专门用于处理[]int类型的slice 让我们看看它需要干什么

//x []int--- 要追加的切片 y--要追加的值
func appendInt(x []int, y int) []int {
    
    
	var z []int
	zlen := len(x) + 1  
	if zlen <= cap(x) {
    
    
		// There is	room to	grow. Extend the slice
		z = x[:zlen]
	} else {
    
    
		// There is insufficient space .Allocate a new array
		// Grow	by doubling,for amortized linear complexity
		zcap := zlen
		if zcap < 2*len(x) {
    
    
			zcap = 2 * len(x)
		}
		z = make([]int, zlen, zcap)
		copy(z, x) // copy函数的第一个参数是要复制的目标slice,第二个参数是源slice
	}
	z[len(x)] = y
	return z

}

代码比较简单

必须先检测slice底层数组是否有足够的容量来保存新添加的元素。 如果有足够空间的话,直接扩展slice(依然在原有的底层数组之上),将新添加的y元素复制 到新扩展的空间,并返回slice。因此,输入的x和输出的z共享相同的底层数组。

如果没有足够的增长空间的话,appendInt函数则会先分配一个足够大的slice用于保存新的结 果,先将输入的x复制到新的空间,然后添加y元素。结果z和输入的x引用的将是不同的底层数组。
现在可以理解为什么切片不允许比较操作==吧。这玩意底层引用都可能变咋比较呀。

为了提高内存使用效率,新分配的数组一般略大于保存x和y所需要的最低大小。通过在每次 扩展数组时直接将长度翻倍从而避免了多次内存分配,也确保了添加单个元素操的平均时间 是一个常数时间。这个程序演示了效果:

	var x,y []int
	for i := 0 ; i<10 ;i++{
    
    
		y = append(x,i)
		fmt.Printf("%d cap=%d \t %v \n",i ,cap(y),y)
		x = y
	}

结果

0 cap=1 	 [0] 
1 cap=2 	 [0 1] 
2 cap=4 	 [0 1 2] 
3 cap=4 	 [0 1 2 3] 
4 cap=8 	 [0 1 2 3 4] 
5 cap=8 	 [0 1 2 3 4 5] 
6 cap=8 	 [0 1 2 3 4 5 6] 
7 cap=8 	 [0 1 2 3 4 5 6 7] 
8 cap=16 	 [0 1 2 3 4 5 6 7 8] 
9 cap=16 	 [0 1 2 3 4 5 6 7 8 9] 

让我们仔细查看i=3次的迭代。当时x包含了[0 1 2]三个元素,但是容量是4,因此可以简单将新的元素添加到末尾,不需要新的内存分配。然后新的y的长度和容量都是4,并且和x引用着 相同的底层数组,如图
image.png

在下一次迭代时i=4,现在没有新的空余的空间了,因此appendInt函数分配一个容量为8的底 层数组,将x的4个元素[0 1 2 3]复制到新空间的开头,然后添加新的元素i,新元素的值是4。 新的y的长度是5,容量是8;后面有3个空闲的位置,三次迭代都不需要分配新的空间。当前 迭代中,y和x是对应不同底层数组的view。这次操作如图
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-n95tSQQm-1600849391358)(http://www.codesuger.com/upload/2020/09/image-f05cb2fd77404677a36a26134d69a6c8.png)]

内置的append函数可能使用比appendInt更复杂的内存扩展策略。因此,通常我们并不知道 append调用是否导致了内存的重新分配,因此我们也不能确认新的slice和原始的slice是否引 用的是相同的底层数组空间。同样,我们不能确认在原先的slice上的操作是否会影响到新的 slice。因此,通常是将append返回的结果直接赋值给输入的slice变量:

	var x  []int
	for i := 0 ; i<10 ;i++{
    
    
		x = append(x,i)
		fmt.Printf("%d cap=%d \t %v \n",i ,cap(x),x)

	}

我们的appendInt函数每次只能向slice追加一个元素,但是内置的append函数则可以追加多个 元素,甚至追加一个slice。

	var x []int
	x = append(x,1)
	x = append(x,2,3)
	x = append(x,4,5,6)
	x = append(x,x...) 	//追加一个 slice x  我加我自己
	fmt.Println(x)  //[1 2 3 4 5 6 1 2 3 4 5 6]

“…”省略号表示接收变长的参数 这里我们追加的参数是slice x

既然正版的append可以追加多个元素我们也可以对我们的appendInt函数进行改造

//x []int--- 要追加的切片 y--要追加的值
func appendInt(x []int, y ...int) []int {
    
    
	var z []int
	zlen := len(x) + len(y)
	if zlen <= cap(x) {
    
    
		// There is	room to	grow. Extend the slice
		z = x[:zlen]
	} else {
    
    
		// There is insufficient space .Allocate a new array
		// Grow	by doubling,for amortized linear complexity
		zcap := zlen
		if zcap < 2*len(x) {
    
    
			zcap = 2 * len(x)
		}
		z = make([]int, zlen, zcap)
		copy(z, x) // copy函数的第一个参数是要复制的目标slice,第二个参数是源slice  简单点--y拷贝给前面的
	}
	copy(z[len(x):], y)
	return z

}

测试结果

	var k []int
	k = appendInt(k ,1,2,3)
	k = appendInt(k ,4,5)
	k = appendInt(k,k...)
	fmt.Println(k)  // [1 2 3 4 5 1 2 3 4 5]

复制操作–copy函数

copy 不会新建新的内存空间,由它原来的切片长度决定

	fmt.Println("----------")
	var a []int
	b := make([]int,2,10) 
	a = append(a,1,2,3)
	fmt.Println(a)
	copy(b,a)
	fmt.Println(b)

结果

[1 2 3]
[1 2]

探讨: b := make([]int,2,10) 不可以写成 var b []int 吗??
答:不可以,上面我们说到过slice可以为nil 它的长度和容量是0,所有写成var b []int 只会输出 [] 因为它不会新建新的内存空间,由它原来的切片长度也就是0来决定 这也是为什么只输出了[1 2] 而没有输出 [1 2 3] 因为copy操作不会发生扩容

截取操作在上面的那个月份那边有很详细的介绍,包括底层数组如何操作这边就不赘述了

排序操作–sort函数

	slice2 := []int{
    
    0, 3, 0, 1, 2, 0}
	sort.Ints(slice2)
	fmt.Println(slice2) //[0 0 0 1 2 3]

反转操作–自定义函数–reverse

// 反转int
func reverse(s []int) []int {
    
    
	for i, j := 0, len(s)-1; i < j; i, j = i+1, j-1 {
    
    
		s[i], s[j] = s[j], s[i]
	}
	return s
}

// 反转字符串
func reverseString(s string) string {
    
    
	runes := []rune(s)
	for from, to := 0, len(runes)-1; from < to; from, to = from+1, to-1 {
    
    
		runes[from], runes[to] = runes[to], runes[from]
	}
	return string(runes)
}

好了,这篇写了很多 主要是对于切片底层和切片操作的理解和深入,耐着性子看到这的还能敲一遍的,我敬你是条汉子

----- 在黑暗中坚守光明,不要在光明中高谈阔论。

猜你喜欢

转载自blog.csdn.net/qq_37806753/article/details/108755697