Go语言学习笔记(六)切片

Go语言学习笔记(六)切片,第1张

切片 定义

切片是go语言的动态数组,可以按需自动增长和缩小。

内部实现

切片是对数组的抽象,并提供了相关的 *** 作方法。
因为切片的底层是数组,所以切片的底层内存也是连续的,也就有快速获得索引,迭代和垃圾回收优化的好处。

切片有三个字段。分别是指向底层数组的指针、切片的长度、切片的容量。
在64位架构的机器上,一个切片需要24字节的内存,三个字段各需要8字节。

切片的长度是能够访问到的范围。
切片的容量是底层数组的大小。
例如某切片的长度为3,容量为5,那么底层数组的大小就是5,能访问的索引范围为0-2。
如果访问其他索引,代码在编译时会报访问越界错误。
底层数组剩下的2个位置也可以通过 *** 作合并到切片。

创建和初始化

使用内置的make函数创建切片,注意切片长度不能大于容量。

// 创建一个字符串切片,长度和容量都是5
slice := make([]string,5)
// 创建一个整形切片,长度为3,容量为5
slice := make([]int,3,5)

使用切片字面量创建切片,和创建数组类似,只是不需要指定**[]运算符**里的值。
切片的长度和容量会基于初始化时提供的元素的个数确定。

// 使用切片字面量创建一个整型切片,长度和容量都是5
slice := []int{10,20,30,40,50}
// 使用空字符串初始化第100个元素,这个切片长度和容量都是100
slice := []string{99:""}

注意,[]运算符里写了数字就是声明了一个数组,没写就是切片。

// 创建有3个元素的整型数组
array := [3]int{10,20,30}
// 创建长度和容量都是3的整形切片
array := []int{10,20,30}
nil和空切片

**nil切片:**用于描述一个不存在的切片

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

使用场景:
例如函数要求返回一个切片,但是发生了异常,可以返回一个nil切片表示。
——
**空切片:**用于描述空集合

// 使用make创建空的整型切片
slice := make([]int,0)
// 使用切片字面量创建空的整型切片
slice := []int{}

空切片常用来表示空集合。
——
nil切片和空切片的区别:
nil切片没有地址,空切片有地址。

func main() {
    var s1 []int
    s2 := []int{}
    fmt.Printf("s1=%p\n",s1)
    fmt.Printf("s2=%p",s2)
}

使用切片
// 创建一个整型切片
slice := []int{10,20,30,40,50}
// 改变索引为1的元素的值
slice[1] = 25
// 使用切片创建一个新切片,长度为2(索引1和索引2,符合前闭后开原则),容量为4(由原底层数组决定的),实现原理如图
newslice := slice[1:3]


对于newslice来说,底层数组长度只有4个元素,切片长度只有2个元素。并且底层数组是和原切片共享的。
——
使用切片创建新切片时,也可以指定容量。

// 定义一个整型切片,长度容量都是4个元素
source := []int{10,20,30,40}
// 使用切片定义一个切片,长度为1,容量也为1
slice := source[2:3:3]
切片增长

Go语言内置append函数,可以用来向切片追加值。

// 创建一个整形切片,长度和容量都是5
slice := []int{10,20,30,40,50}
// 创建一个新切片,长度为2,容量为4
newSlice := slice[1:3]
// 给newslice追加一个元素
newSlice = append(newSlice,60)


因为newSlice在底层数组还有额外的容量可用,所以append直接将可用元素放在了底层数组索引3的位置。
如果切片的底层数组容量不够了,append函数会创建一个新的底层数组,将原来的值复制到新数组中,再追加新的值。

// 长度和容量都是4个元素
slice := []int{10,20,30,40}
// 追加一个新元素
newslice := append(slice,50)


新的底层数组容量是原来的两倍。
注:切片的容量小于1000个元素时,总是会成倍增加容量。元素超过1000个后,容量的增长因子会设为1.25,每次增加25%。
————
可以利用append函数的特性,让使用切片创建的切片有自己的底层数组。

// 定义一个整型切片,长度容量都是4个元素
source := []int{10,20,30,40}
// 使用切片定义一个切片,长度为1,容量也为1
slice := source[2:3:3]
// 使用append函数给slice添加一个数字,超出容量,获得新的底层数组
slice := append(slice,40)

————
append函数是一个可变参数的函数,能够一次调用多个追加的值。

s1 := []int{1,2}
s2 := []int{3,4}
fmt.Printf("%v\n",append(s1,s2...))
迭代切片

使用Go语言的关键字range,配合关键字for来迭代切片中的元素。

slice := []int{10,20,30,40}
for index,value := range slice{
	fmt.Printf("Index: %d Value: %d\n",index,value)
}

Output:
Index: 0 Value: 10
Index: 1 Value: 20
Index: 2 Value: 30
Index: 3 Value: 40

迭代切片时,关键字 range 会返回两个值。第一个值是当前迭代到的索引位置,第二个
值是该位置对应元素值的一份副本,注意是副本又不是引用。

slice := []int}{10,20,30,40}
for index,value := range slice{
	fmt.Printf("Value: %d Value-Addr: %X ElemAddr: %X\n",value, &value, &slice[index])
}

Output:
Value: 10 Value-Addr: 10500168 ElemAddr: 1052E100
Value: 20 Value-Addr: 10500168 ElemAddr: 1052E104
Value: 30 Value-Addr: 10500168 ElemAddr: 1052E108
Value: 40 Value-Addr: 10500168 ElemAddr: 1052E10C

可以发现value的地址没有变,并且和数据真正的位置不同。

切片的其他 *** 作函数

求切片的长度len()
求切片的容量cap()

// 定义一个切片
slice := []int{10,20,30,40,50}
// 求切片的长度
fmt.Println(len(slice))
// 求切片的容量
fmt.Println(cap(slice))
多维切片 定义

和数组一样,可以组合多个切片形成多维切片。

创建和声明
// 声明多维切片
sclie := [][]int{{10},{100,200}}

这样就有了一个包含两个元素的切片,每个元素包含一个整型切片。内存结构如图。

可以看到外层切片包括两个元素,每个元素都是一个切片。

使用多维数组
// 创建一个二维整型切片
slice := [][]int{{10},{100,200}}
// 为第一个切片追加值为20的元素
alice[0] = append(slice[0],20)

上述代码中,追加元素后,slice[0]切片需要扩充,因此append会先分配一个更大的底层数组给slice[0],赋值完成后再将切片复制到外层切片索引为0的元素。

在函数间传递切片的思考 在函数间传递切片的默认方式

之前在数组一节提到过,go语言默认采用值传递的方式。切片也不例外。
但因为切片很小,所以复制成本很低。

// 分配包含100万个整型值的切片
slice := make([]int,1e6)
// 将slice传递给函数foo
slice = foo(slice)
func foo(slice []int) []int{
	...
	return slice
}
在函数间传递切片的指针

切片本身是一个轻量级的数据结构,复制一个切片的成本很低,新复制出来的切片和原来的切片也是同一个底层数组,如果函数不对切片做改动的话,采用默认方式传递是没有问题的。
但是如果函数会对切片进行改动,采用默认方式就可能会出现问题。
举个例子:

func main() {
// 定义一个长度容量都为1的切片
    slice1 := make([]int,1,1)
    fmt.Printf("In main slice = %v, len(slice) = %d, cap(slice) = %d,slice address = %p,array address = %p\n",slice1,len(slice1),cap(slice1),&slice1,slice1)
    modifySlice(slice1)
    fmt.Printf("In main slice = %v, len(slice) = %d, cap(slice) = %d,slice address = %p,array address = %p\n",slice1,len(slice1),cap(slice1),&slice1,slice1)
    fmt.Printf("\n")
    
// 定义一个长度为1,容量为2的切片
    slice2 := make([]int,1,2)
    fmt.Printf("In main slice = %v, len(slice) = %d, cap(slice) = %d,slice address = %p,array address = %p\n",slice2,len(slice2),cap(slice2),&slice2,slice2)
    modifySlice(slice2)
    fmt.Printf("In main slice = %v, len(slice) = %d, cap(slice) = %d,slice address = %p,array address = %p\n",slice2,len(slice2),cap(slice2),&slice2,slice2)
}


func modifySlice(slice []int){
    fmt.Printf("In modifySlice slice = %v, len(slice) = %d, cap(slice) = %d,slice address = %p,array address = %p\n",slice,len(slice),cap(slice),&slice,slice)
    slice = append(slice,1)
    fmt.Printf("In modifySlice slice = %v, len(slice) = %d, cap(slice) = %d,slice address = %p,array address = %p\n",slice,len(slice),cap(slice),&slice,slice)
}


分析一下运行结果。先看第一部分,由第一行和第二行输出可知,对于长度和容量都为1的切片,在进入函数modifySlice后,切片的地址发生了变化,但是切片的底层数组地址没变,切片的长度容量没变,说明函数复制了新切片,并且和原切片共用了一个底层数组。由第三行输出可知,append一个元素后,新切片的底层数组地址改变了,原因是新切片的容量不够支持append,所以底层数组扩容了。由第4行输出可知,对原切片来说,切片的长度和容量都没有变化,底层的数组也没有变化。
在第一部分中,函数对切片的修改,因为扩容使得新切片的底层数组发生了变化,导致没有对原切片造成影响。

再看第二部分。由第一行和第二行的输出可知,对于长度为1,容量为2的切片,在进入函数modifySlice后,切片的地址发生了变化,但是切片的底层数组地址没变,切片的长度容量没变,说明函数复制了新切片,并且和原切片共用了一个底层数组。由第三行输出可知,append一个元素后,切片的地址也没有改变,这里是意料中的,因为切片容量够用,不需要创建新的切片。既然现在的底层数组没发生改变,这次append应该理应会对原切片造成影响,但根据第四行输出可知,原切片并没有发生变化。
在第二部分中,函数对切片的修改,没有发生扩容,修改的确实就是原切片的底层数组,但因为原切片的长度和容量都没有发生改变,因此对于原切片来说,底层数组没有发生改变(或者说原切片不知道底层数组改变了),那么原切片的值就没有变。

通过这个例子可以得到结论,使用值传递方式向函数传递切片,函数中的append函数修改的只是函数复制的切片,对原切片没影响。(当然,如果只是修改数值还是可以影响到原切片的)。
想要增删改查都能对原切片产生影响,有两种方式:

被调函数返回修改后的切片,并且原函数使用被调函数的返回值修改了切片。这样的问题是,对原切片的改变只有函数运行结束后才会显现,而且本质上是用新的切片覆盖了之前的切片,底层数组可能也发生了改变。使用切片指针。这样函数之间使用的永远都是同一个切片,修改的是同一个底层数组,变化也不用等待函数结束才会显现。
切片指针的用法很简单,只要将函数的参数定义为切片的指针,调用时传入切片的地址就可以了。
func main() {
// 定义一个长度容量都为1的切片
    slice1 := make([]int,1,1)
    fmt.Printf("In main slice = %v, len(slice) = %d, cap(slice) = %d,slice address = %p,array address = %p\n",slice1,len(slice1),cap(slice1),&slice1,slice1)
    modifySlice(&slice1)
    fmt.Printf("In main slice = %v, len(slice) = %d, cap(slice) = %d,slice address = %p,array address = %p\n",slice1,len(slice1),cap(slice1),&slice1,slice1)
    fmt.Printf("\n")
    
// 定义一个长度为1,容量为2的切片
    slice2 := make([]int,1,2)
    fmt.Printf("In main slice = %v, len(slice) = %d, cap(slice) = %d,slice address = %p,array address = %p\n",slice2,len(slice2),cap(slice2),&slice2,slice2)
    modifySlice(&slice2)
    fmt.Printf("In main slice = %v, len(slice) = %d, cap(slice) = %d,slice address = %p,array address = %p\n",slice2,len(slice2),cap(slice2),&slice2,slice2)
}


func modifySlice(slice *[]int){
    fmt.Printf("In modifySlice slice = %v, len(slice) = %d, cap(slice) = %d,slice address = %p,array address = %p\n",slice,len(*slice),cap(*slice),slice,*slice)
    *slice = append(*slice,1)
    fmt.Printf("In modifySlice slice = %v, len(slice) = %d, cap(slice) = %d,slice address = %p,array address = %p\n",slice,len(*slice),cap(*slice),slice,*slice)
}
其他

打印切片的地址,打印切片底层数组的方法

// &slice是切片的地址,slice是切片底层数组的地址
slice := []int{1,2}
fmt.Printf("slice address = %p, array address = %p",&slice,slice)
参考资料

https://blog.csdn.net/LYue123/article/details/88363685(大佬很耐心的举了很多例子,学到了很多)

欢迎分享,转载请注明来源:内存溢出

原文地址: http://outofmemory.cn/langs/994219.html

(0)
打赏 微信扫一扫 微信扫一扫 支付宝扫一扫 支付宝扫一扫
上一篇 2022-05-21
下一篇 2022-05-21

发表评论

登录后才能评论

评论列表(0条)

保存