Go语言学习10-切片(超级重点)

切片(slice)是golang中一种特有的数据类型

切片的引入

因为数组的长度是固定的并且数组长度属于类型的一部分,所以数组有很多的局限性,在Go语言的代码里并不是特别常见。相对的切片却是随处可见的,切片是一种建立在数组类型之上的抽象,它构建在数组之上并且提供更强大的能力和便捷。

例如:

func arraySum(x [3]int) int{
    sum := 0
    for _, v := range x{
        sum = sum + v
    }
    return sum
}

这个求和函数只能接受[3]int类型,其他的都不支持。 再比如,

a := [3]int{1, 2, 3}

数组a中已经有三个元素了,我们不能再继续往数组a中添加新元素了。

切片

切片(Slice)是一个拥有相同类型元素的可变长度的序列。它是基于数组类型做的一层封装。它非常灵活,支持自动扩容。

切片是一个引用类型,它的内部结构包含地址长度容量。切片一般用于快速地操作一块数据集合。

切片的定义

声明切片类型的基本语法如下:

var name []T

其中,

  • name:表示变量名
  • T:表示切片中的元素类型

举个例子:

func main() {
	// 声明切片类型
	var a []string              //声明一个字符串切片
	var b = []int{}             //声明一个整型切片并初始化
	var c = []bool{false, true} //声明一个布尔切片并初始化
	var d = []bool{false, true} //声明一个布尔切片并初始化
	fmt.Println(a)              //[]
	fmt.Println(b)              //[]
	fmt.Println(c)              //[false true]
	fmt.Println(a == nil)       //true
	fmt.Println(b == nil)       //false
	fmt.Println(c == nil)       //false
	// fmt.Println(c == d)   //切片是引用类型,不支持直接比较,只能和nil比较
}

切片的长度和容量

切片拥有自己的长度和容量,我们可以通过使用内置的len()函数求长度,使用内置的cap()函数求切片的容量。

image-20220211202756348

image-20220211202818293

func main() {
	//切片的定义
	var s1 []int    //定义一个存放int类型元素的切片
	var s2 []string //定义一个存放string类型元素的切片
	fmt.Println(s1, s2)
	fmt.Println(s1 == nil) //true
	fmt.Println(s2 == nil) //true
	//初始化
	s1 = []int{1, 2, 3}
	s2 = []string{"beijing", "shanghai", "shenzhen"}
	fmt.Println(s1, s2)
	fmt.Println(s1 == nil) //false
	fmt.Println(s2 == nil) //false	这两个切片已经初始化了,所以就不为空了肯定
	fmt.Printf("s1长度为:%d,s1容量为:%d\n", len(s1), cap(s1))
	fmt.Printf("s2长度为:%d,s2容量为:%d\n", len(s2), cap(s2))
	//2.由数组得到切片
	a1 := [...]int{1, 2, 3, 4, 5, 6, 7, 8}
	s3 := a1[0:4] //基于一个数组切割,左包含右不包含(左闭右开)
	s4 := a1[1:6] //从index=1开始,切到index=6
	s5 := a1[:4]  //相当于从0切到4,包含0
	s6 := a1[3:]  //从3,切到最后
	s7 := a1[:]
	fmt.Println("s3为:", s3)
	fmt.Println("s4为:", s4)
	fmt.Println("s5为:", s5)
	fmt.Println("s6为:", s6)
	fmt.Println("s7为:", s7)
	fmt.Printf("len为:%d,cap为:%d", len(s5), cap(s5)) //len:4,cap:8  切片的容量,就是底层数组的容量,即a1的容量为8
	fmt.Printf("len为:%d,cap为:%d", len(s4), cap(s4)) //len:5,cap:7  按照上面的说法,这里怎么就是7了?不应该是8么?
	//就是因为,切片是指向底层数组的一个概念,容量即:数组从切片开始的第一个数算起,到数组后的所有数值数量就是容量
}

上面的代码肯定会有人说了:你说切片是指向数组的,那我自己新建的切片怎么个说法?是这样的,自己新建的切片,Go语言底层会新建一个数组,然后根据你的切片长度大小等,返回相应的数值。

切片再切割

接着上面的代码来看

/*切片再切割*/
	s8 := s6[1:3]                                                //s6=[4,5,6,7,8]
	fmt.Printf("s8为:%v,len为:%d,cap为:%d\n", s8, len(s8), cap(s8)) //s8是从5开始的,并且从s6算起,而s6的数组为a1,所以s8的容量就是a1从5到最后:4

使用make()函数构造切片

我们上面都是基于数组来创建的切片,如果需要动态的创建一个切片,我们就需要使用内置的make()函数,格式如下:

make([]T, size, cap)

其中:

  • T:切片的元素类型
  • size:切片中元素的数量
  • cap:切片的容量

举个例子:

func main() {
	a := make([]int, 2, 10)
	fmt.Println(a)      //[0 0]
	fmt.Println(len(a)) //2
	fmt.Println(cap(a)) //10
}

上面代码中a的内部存储空间已经分配了10个,但实际上只用了2个。 容量并不会影响当前元素的个数,所以len(a)返回2,cap(a)则返回该切片的容量。

切片的本质

再来复习一下:

1、切片的本质就是对底层数组的封装,它包含了三个信息:底层数组的指针、切片的长度(len)和切片的容量(cap)。

2、切片就是一个框,框住了一块连续的内存,像C语言中的数组,都是一种数据类型。

3、切片属于引用类型,真正的数据都是保存在底层数组里面的。

举个例子,现在有一个数组a := [8]int{0, 1, 2, 3, 4, 5, 6, 7},切片s1 := a[:5],相应示意图如下。 slice_01 切片s2 := a[3:6],相应示意图如下:

slice_02

判断切片是否为空

要检查切片是否为空,请始终使用len(s) == 0来判断,而不应该使用s == nil来判断。

切片不能直接比较

切片之间是不能比较的,我们不能使用==操作符来判断两个切片是否含有全部相等元素。 切片唯一合法的比较操作是和nil比较。 一个nil值的切片并没有底层数组,一个nil值的切片的长度和容量都是0。但是我们不能说一个长度和容量都是0的切片一定是nil,例如下面的示例:

var s1 []int         //len(s1)=0;cap(s1)=0;s1==nil
s2 := []int{}        //len(s2)=0;cap(s2)=0;s2!=nil
s3 := make([]int, 0) //len(s3)=0;cap(s3)=0;s3!=nil

所以要判断一个切片是否是空的,一定要使用len(s) == 0来判断,不应该使用s == nil来判断。

切片表达式

切片表达式从字符串、数组、指向数组或切片的指针构造子字符串或切片。它有两种变体:一种指定low和high两个索引界限值的简单的形式,另一种是除了low和high索引界限值外还指定容量的完整的形式。

简单切片表达式

切片的底层就是一个数组,所以我们可以基于数组通过切片表达式得到切片。 切片表达式中的lowhigh表示一个索引范围(左包含,右不包含),也就是下面代码中从数组a中选出1<=索引值<4的元素组成切片s,得到的切片长度=high-low,容量等于得到的切片的底层数组的容量。

func main() {
	a := [5]int{1, 2, 3, 4, 5}
	s := a[1:3]  // s := a[low:high]
	fmt.Printf("s:%v len(s):%v cap(s):%v\n", s, len(s), cap(s))
}

输出:

s:[2 3] len(s):2 cap(s):4

为了方便起见,可以省略切片表达式中的任何索引。省略了low则默认为0;省略了high则默认为切片操作数的长度:

a[2:]  // 等同于 a[2:len(a)]
a[:3]  // 等同于 a[0:3]
a[:]   // 等同于 a[0:len(a)]

注意:

对于数组或字符串,如果0 <= low <= high <= len(a),则索引合法,否则就会索引越界(out of range)。

对切片再执行切片表达式时(切片再切片),high的上限边界是切片的容量cap(a),而不是长度。常量索引必须是非负的,并且可以用int类型的值表示;对于数组或常量字符串,常量索引也必须在有效范围内。如果lowhigh两个指标都是常数,它们必须满足low <= high。如果索引在运行时超出范围,就会发生运行时panic

func main() {
	a := [5]int{1, 2, 3, 4, 5}
	s := a[1:3]  // s := a[low:high]
	fmt.Printf("s:%v len(s):%v cap(s):%v\n", s, len(s), cap(s))
	s2 := s[3:4]  // 索引的上限是cap(s)而不是len(s)
	fmt.Printf("s2:%v len(s2):%v cap(s2):%v\n", s2, len(s2), cap(s2))
}

输出:

s:[2 3] len(s):2 cap(s):4
s2:[5] len(s2):1 cap(s2):1

完整切片表达式

对于数组,指向数组的指针,或切片a(注意不能是字符串)支持完整切片表达式:

a[low : high : max]

上面的代码会构造与简单切片表达式a[low: high]相同类型、相同长度和元素的切片。另外,它会将得到的结果切片的容量设置为max-low。在完整切片表达式中只有第一个索引值(low)可以省略;它默认为0。

func main() {
	a := [5]int{1, 2, 3, 4, 5}
	t := a[1:3:5]
	fmt.Printf("t:%v len(t):%v cap(t):%v\n", t, len(t), cap(t))
}

输出结果:

t:[2 3] len(t):2 cap(t):4

完整切片表达式需要满足的条件是0 <= low <= high <= max <= cap(a),其他条件和简单切片表达式相同。

切片的赋值拷贝

下面的代码中演示了拷贝前后两个变量共享底层数组,对一个切片的修改会影响另一个切片的内容,这点需要特别注意。

func main() {
	s1 := make([]int, 3) //[0 0 0]
	s2 := s1             //将s1直接赋值给s2,s1和s2共用一个底层数组
	s2[0] = 100
	fmt.Println(s1) //[100 0 0]
	fmt.Println(s2) //[100 0 0]
}

切片遍历

切片的遍历方式和数组是一致的,支持索引遍历和for range遍历。

func main() {
	s := []int{1, 3, 5}

	for i := 0; i < len(s); i++ {
		fmt.Println(i, s[i])
	}

	for index, value := range s {
		fmt.Println(index, value)
	}
}

切片内存分析

slice_RAM

切片的难点与疑问点

来看第一张图片,没毛病吧

image-20220306211448006

image-20220306213518939

上面第一张图的写法,是就是你的疑问点,你暂时实在不理解,就按照你认为的认为就可以了。也就是,当切片为0后,下次初始化定义切片,会从数组那里取,不会像python那样。第二张图是正规的写法,一般切片都是从数组中切过来或者make函数做出来的,每次使用一个切片都会定义新的变量,如s1,s2,s3,而不是像第一张图那样一直用s

image-20220306213534509

posted @   谨言慎行啊  阅读(67)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· 没有Manus邀请码?试试免邀请码的MGX或者开源的OpenManus吧
· 【自荐】一款简洁、开源的在线白板工具 Drawnix
· 园子的第一款AI主题卫衣上架——"HELLO! HOW CAN I ASSIST YOU TODAY
· Docker 太简单,K8s 太复杂?w7panel 让容器管理更轻松!
点击右上角即可分享
微信分享提示