Go语言 切片 Slice

切片是一种数据结构,更便于使用和管理数据集合。切片才是实际开发中最多使用的,它是围绕着动态数组的概念构建,可以按需自动增长和缩小。

切片的内部实现和基础功能

  • 切片是一种数据结构
  • 切片可以按需动态增长和缩小
  • 切片的底层内存也是连续块分配,能享受及使用索引,迭代,垃圾回收

内部实现

切片是一个很小的对象,对底层数组进行了抽象,它是有3个字段的数据结构。

  • 指向底层数组的指针
  • 长度:切片访问的元素个数(长度)
  • 容量:切片允许增长的元素个数(长度)
    在这里插入图片描述

创建切片和初始化

Go语言在创建切片有好几种方法。
是否知道切片需要的容量会决定要如何创建切片。

make关键字创建切片

内置的make函数,当使用make的时候 ,需要传入一个参数,指定切片的长度。

// 创建一个字符串切片
// 只指定了长度,长度和容量都是5
slice := make([]string, 5)

只有指定长度,没有指定容量,那么切片的容量和长度相同。
也可以分别指定长度和容量。

// 指定了长度是3,容量是5
slice := make([]string,3,5)
// 设置访问索引3
slice[3] = "apple"
// 索引越界
// panic: runtime error: index out of range [3] with length 3

这里分别指定了长度是3,容量是5的字符串切片。
不允许创建容量小于长度的切片。
底层的数组的长度是指定容量(这里底层数组是5的长度),但我们声明了切片长度是3,我们也只能访问3以内的索引,并不能访问3,4的索引。
剩余的2个元素可以在后期操作中合并切片,可以通过切片访问这些元素。
如果基于这个切片创建新的切片,新切片会和原有切片共享底层数组,也能通过后期操作来访问多作容量的元素。

切片字面量创建切片

通过切片字面量创建切片。

// 创建字符串切片
// 创建长度和容量都是2,根据初始化的值确定
slice1 := []string{"apple", "huawei"}

也可以指定长度和容量创建

// 创建长度和容量都是100的字符串切片
slice2 := []string{1:"我是第2个元素",99: "我是第100个元素,其它元素都是空值"}
fmt.Println(len(slice2))  //100
使用索引声明切片
slice3 := []string{99: ""}
fmt.Println(len(slice3)) // 100

数组和切片的不同

声明方式不同,数组必须指定长度,切片无需指定长度

// 创建int类型长度为3的数组
array1 := [3]int{10, 20, 30}
// 创建int类型长度为3,容量为3的切片
slice1 := []int{10, 20, 30}
fmt.Println(array1) // [10 20 30]
fmt.Println(slice1) // [10 20 30]

数组和切片的底层数组结构上是致,数组的长度一经指定不能改变,但切片可以动态的改变数组底层长度。

nil和空切片

声明时不做任何初始化,就等同于创建了一个nil切片。

nil切片
描述一个不存在的切片

// 创建nil整型切片
var slice []int

空切片
空切片在底层数组包含0个元素,没有分配任何空间。空集合

// make创建长度为0的空切片
var slice2 = make([]int, 0)

// 字面量方式 创建
slice3 := []int{}

无论是nil切片还是空切片,都可以使用内置函数,append,len和cap。

使用切片

赋值和切片

访问和赋值
切片的赋值和访问与数组完全一样。

// 声明一个int类型的切片,初始长度为8,容量为5
slice := []int{10, 20, 30, 40, 50}
fmt.Println(slice[1])  // 20
// 改变索引1的值为200
slice[1] = 200
fmt.Println(slice[1]) // 200

使用切片创建切片
创建切片的语法:[ start, end, cap ]

  • start 开始索引
  • end 结束索引
  • cap 指定容量
// 创建一个整型切片,长度和容量都是5
slice := []int{10, 20, 30, 40, 50}
// 从slice切片中创建新切片
// 长度是2,
newSlice := slice[1:3]

在这里插入图片描述
slice切片能看到底层数组的5个元素,而newSlice切片则不行。
newSlice底层的数组容量只有4个元素。

切片创建的切片会和源切片共享同一个底层数组。如果一个切片修改了底层数组的共享部分,另一个切片也同样生效。

更多示例:
在这里插入图片描述

如何计算长度和容量

假设底层数组容量是K,那么计算slice[ i:j ]方法如下:

  • 长度: j - i, 这里的j是指新切片中的end值
  • 容量: k - i,这里的k是指源切片中的容量值
    示例
// 创建一个整型切片,长度和容量都是5
slice := []int{10, 20, 30, 40, 50}
// 创建一个新切片
newSlice := slice[ 1: 3] 
// 计算方法 k = slice切片的容量 5
// newSlice的长度: 3 - 1 = 2
// newSlice的容量: 5 - 1 = 4
切片增长

append
先介绍append的使用,接下来再描述增长动态扩容的细节
要使用append,需要一个被操作的切片和一个要增加的值。
示例:

// 声明一个int类型的切片,长度为5,容量为5
slice := []int{10, 20, 30, 40, 50}
newSlice := append(slice, 60)
// 这里newSlice的长度是6容量是10 

debug调试能看到内存中的长度和容量
在这里插入图片描述
append操作成功后一定会增加新切片的长度,但容量是动态的有可能会有变化也有可能不会有变化,取决于被操作的切片的可用容量。
如上示例,slice的切片长度是5,容量也是5,这里新加一个元素,长度变成6了,那么容量必须也动态扩容,当前Go语言中容量扩容会在上一个容量的基础上乘以2,这里newSlice的切片容量即5*2=10。同时会创建一个新的底层数组,不同于源slice底层数组。
这里如果我们继续往newSlice中添加数据,那么只是长度不超过10,那么切片容量也不会有变化,底层数组也不会有变化。

在这里插入图片描述
底层数组容量动态扩展的原则是:
切片容量小于1000个元素的时候 ,会成倍的增长,元素超过1000的时候 ,会1.25倍的增长。

append中的…运算符
内置函数append也是一个可变参数的函数,可以在一次调用传递多个追加的值。
如果使用…运算符,可以将一个切片的所有元素追加到另一个切片里。
示例:

slice := []string{"apple", "huawei", "oppo", "xiaomi"}
slice2 := []string{"black", "blue", "green"}
fmt.Println(append(slice2, slice...))
// 输出结果:[black blue green apple huawei oppo xiaomi]

先来看一个案例:
这里的第2个切片使用append,会不小心把第1个切片的值也改变了。
原因是:因为共享了同一个底层数组,第2个切片还有容量可以使用,于就在赋值的时候直接把值赋在了这个位置。

在这里插入图片描述
如何解决这个问题,可以引入第3个参数

创建切片时的3个索引(cap)

用来控制新切片的容量。目的不是为了增加容量是为了限制容量。
将长度和容量设置一样,就可以让新切片的第1个append操作创建一个新的底层组,这样就不会和源切片共享一个底层数组,改变值时也不会影响到源切片。

	slice := []string{"apple", "huawei", "oppo", "xiaomi"}
	// 对第三元素做切片,并限制容量
	newslice := slice[2:3:3]
	newslice = append(newslice, "add1")
	fmt.Println(slice) // [apple huawei oppo xiaomi]
	fmt.Println(newslice) // [oppo add1]

在这里插入图片描述

迭代切片

range关键字

range可以配置for循环来迭代切片的元素。
示例:

slice := []string{"apple", "huawei", "oppo", "xiaomi"}
for k, v := range slice {
	fmt.Printf("index: %d, value: %s\n", k, v)
}
// 输出结果:
/*
index: 0, value: apple
index: 1, value: huawei
index: 2, value: oppo  
index: 3, value: xiaomi
*/

迭代会返回2个值 ,第1个值是当前迭代到的索引位置,第2个是该位置对应元素值 的一份副本。
注意:
range创建了每个元素的副本,而不是直接返回对该元素的引用。
通过一个例子来看一下

slice := []string{"a1", "a2", "a3", "a4"}
	for index, v := range slice {
		fmt.Printf("index: %d, value: %s Value-Addr: %X , ElementAddr: %X\n", index, v, &v, &slice[index])
	}
// 打印结果
/**
index: 0, value: a1 Value-Addr: C000040250 , ElementAddr: C00004A040
index: 1, value: a2 Value-Addr: C000040250 , ElementAddr: C00004A050
index: 2, value: a3 Value-Addr: C000040250 , ElementAddr: C00004A060
index: 3, value: a4 Value-Addr: C000040250 , ElementAddr: C00004A070
*/

可以看到Value-Addr的地址都是同一个,因为迭代返回的变量是一个迭代过程中产生的赋值的新变量。
要想获取每个元素的地址,可以使用切片变量和索引值。

多维切片

和数组一样,切片是一维的。类似数组一样可以组合成多维的。
略…

在函数中传递切片

细节描述:

  • 在函数中传递切片就是要在函数间以值的方式传递切片。
  • 切片的尺寸很小,在函数间复制和传递切片成本也很低。在64位的架构机器上,一个切片需要24个字节的内存,长度和容量字段分别需要8字节。
  • 切片关联的数据包含在底层数组里面,不属于切片本身,所以将切片复制到任意函数的时候,对底层数组大小都不会有影响。
  • 复制时只复制切片本身,不会涉及底层数组。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值