一篇超详细的go语言中的Slice(切片)讲解~

问题引入

在书上看到了一个非常有意思的题:

package main

import "fmt"

func SliceRise(s []int) {
	s = append(s, 0)
	for i := range s {
		s[i]++
	}
}

func main() {
	s1 := []int{1, 2}
	s2 := s1
	s2 = append(s2, 3)
	SliceRise(s1)
	SliceRise(s2)
    fmt.Println(s1,s2)
}
/*
问:该函数输出什么?
A:[2,3][2,3,4]	B:[1,2][1,2,3]	C:[1,2][2,3,4]	D:[2,3,1][2,3,4,1]
*/

很明显这是一个切片扩容的问题,要解决这个问题,首先我们需要知道切片是什么?它的append方法是怎么实现的?它的扩容机制又是怎样的;那么我们先从切片本身讲起吧~

什么是切片?

1.概念

slice又称动态数组,依托数组实现,可以方便地进行扩容和传递切片是对数组的一个引用,它包含指向数组的指针、长度和容量等信息,属于引用类型。

2.数据结构

源码包中src/runtime/slice.go:slice定义了slice的数据结构:

type slice struct{
	array unsafe.Pointer
	len int
	cap int
}

由此可以看出:在slice内部一共有三个字段:

  • 指针类型的array–>指向底层数组
  • int型的len–>表示切片长度
  • int型的cap–>表示底层数组的容量

3.cap和len

在数组中我们知道可以用arr.length查看当前arr数组的长度,在切片中也可以使用len(sli)来查看切片的长度,那么cap是个什么东西呢?

a := make([]int, 3, 5)
a = append(a,1,2,3)

假如我们使用上面的代码创建一个长度为3容量为5的切片,那么在内存空间中该切片a大概长这个样子:

在这里插入图片描述

即在内存中给a开辟了可以存放5个数据的空间,但是此时a中只有三个数据,即len(a) = 3,cap(a) = 5

package main

import "fmt"

func main() {

	s1 := []int{1, 2}
	s1 = append(s1, 3)
	s2 := s1
	s1 = append(s1, 4)
	s2 = append(s2, 5)
	for i := range s1 {
		s1[i] += 1
	}
	fmt.Printf("s1 = %v\n", s1)
	fmt.Printf("s2 = %v\n", s2)
}

4.初始化

声明和初始化切片的方式主要有以下几种

  • 变量声明
  • 字面量
  • 使用内置函数make()
  • 从切片和数组中切取

变量声明

var s []int

此时切片s的值为nil

字面量

s1 := []int{}		// 空切片
s2 := []int{1,2,3}	// 长度为3的切片

空切片是指长度为空,其值不是nil,声明一个空切片需要分配空间,而nil切片不需要给分配空间

内置函数make

s1 := make([]int,10)		// 指定长度,容量为10
s2 := make([]int,5,10)		// 长度为5,容量为10

切取

array := [5]int{1,2,3,4,5}
s1 := array[0:2]	// 从数组切取 
s2 := s1[0:1]		// 从切片切取

切片可以基于数组和切片创建,但是基于切片和数组创建的切片会和原数组或切片共享底层空间,修改切片会影响原数组或切片;切片切取的范围是左闭右开[left,right),长度是right-left,容量是原数组(/切片)长度(容量)-left例:

func main() {

	s1 := make([]int, 5, 8)
	s2 := s1[2:4]
	var arr [10]int
	arr1 := arr[5:6]
	fmt.Printf("len(s2) = %v,cap(len) = %v\n", len(s2), cap(s2))
	fmt.Printf("len(arr1) = %v,cap(arr1) = %v", len(arr1), cap(arr1))
}

/*
输出:
len(s2) = 2,cap(len) = 6
len(arr1) = 1,cap(arr1) = 5
*/

切片的扩容

append方法

内置函数append()可以向切片中追加元素

s := make([]int, 0)
s = append(s,1)						//向切片s中追加一个元素1
s = append(s,2,3,4)					//向切片s中追加多个元素
s = append(s,[]int{5,6,7}...)		//向切片s中添加一个切片
fmt.Println(s)						//输出:[1,2,3,4,5,6,7]

当切片的cap小于追加元素后的len时,就会进行切片扩容

切片的扩容机制

扩容容量的选择遵循以下基本准则:nn

  • v1.18 版本之前:

    • 如果所需容量大于原容量的二倍,则新容量为所需容量。
    • 如果原slice容量小于1024,则新slice容量将扩大为原来的二倍。
    • 如果原slice容量大于或等于1024,则新slice容量将扩大为原来的1.25倍。
  • v1.18 版本:

    • 如果所需容量大于原容量的二倍,则新容量为所需容量。
    • 如果原slice容量小于256,则新slice容量将扩大为原来的二倍。
    • 如果原容量大于或等于256,进入一个循环,每次容量增加(旧容量 + 3 * threshold)/ 4 。

而每次扩容后,切片将会开辟一块新的空间,将原切片中的数据拷贝到新地址中,而原先的那片空间就会被抛弃(当然了这种情况仅限于没有其他指针指向原先的老空间)

解决问题

了解完切片,以及切片的扩容机制后,那么我们接着回归主题,第一段代码的输出结果是什么呢?

package main

import "fmt"

func SliceRise(s []int) {
	s = append(s, 0)
	for i := range s {
		s[i]++
	}
}

func main() {
	s1 := []int{1, 2}
	s2 := s1
	s2 = append(s2, 3)
	SliceRise(s1)
	SliceRise(s2)
    fmt.Println(s1,s2)
}
/*
问:该函数输出什么?
A:[2,3][2,3,4]	B:[1,2][1,2,3]	C:[1,2][2,3,4]	D:[2,3,1][2,3,4,1]
*/

根据代码,我们可以画一个流程图来模拟程序运行时的操作

在这里插入图片描述

从上图非常明显的可以看出输出结果应该是:[1,2] [2,3,4,1],答案选D!上goland上一跑,傻眼了,怎么输出的是[1,2] [2,3,4]?到底哪个环节出了问题?

经过了很多次实验依然百思不得其解,直到我尝试输出了SliceRise()函数中s的len和操作完之后s2的len发现:

len(s) = 4,而len(s2) = 3

这是怎么回事呢,我们回到切片在内存中的存储方式:

址传递和值传递

上面我们提到array即是一个指向底层数组的指针,这里需要注意的是,数组直接赋值使用的是址传递的方法,我们可以看一下下面这个例子:

func main() {

	s1 := []int{1, 2}
	s2 := s1
	fmt.Printf("s1 = %v,s1的地址:%p\n", s1, s1)
	fmt.Printf("s2 = %v,s2的地址:%p\n", s2, s2)
}
/*
输出:
s1 = [1 2],s1的地址:0xc00000a0e0
s2 = [1 2],s2的地址:0xc00000a0e0
 */

我们使用fmt.Printf(“%p\n”, s1)来打印s1,s2两个切片在内存中分配的底层数组的地址,发现两个切片分配的底层数组的地址是一样的,即可以验证这一点

但是如果我们打印s1,s2本身的地址的话,是什么样的结果呢?

func main() {

	s1 := []int{1, 2}
	s2 := s1
	fmt.Printf("%p\n", s1)
	fmt.Printf("%p\n", &s1)
	fmt.Printf("s1 = %v,s1的地址:%p\n", s1, &s1)
	fmt.Printf("s2 = %v,s2的地址:%p\n", s2, &s2)
}

/*
输出:
s1 = [1 2],s1的地址:0xc000008048
s2 = [1 2],s2的地址:0xc000008060
*/

可以看出,s1,s2是分别存在不同的内存地址中的,但是他们所指向的底层数组的地址是同一个

也就是说切片其实是像图片中那样的方式存储的:

在这里插入图片描述

即s和s2虽然指向同一片底层数组,但是二者的len和cap信息是单独的,s2的len是3,虽然后续该空间中存储了4个数,也只能读取到第三个数而已,因此输出s2的值是:[2,3,4]

如果还不能理解,可以再看下面一个例子:

func main() {

	s1 := []int{1, 2}
	s1 = append(s1, 3)
	s2 := s1
	s1 = append(s1, 4)
	s2 = append(s2, 5)
	fmt.Printf("s1 = %v\n", s1)
	fmt.Printf("s2 = %v\n", s2)
}
/*
输出:
s1 = [1 2 3 5]
s2 = [1 2 3 5]
*/

s1,s2始终指向同一片数组空间,在s1向切片中追加元素4后,s2向切片中追加元素5前,len(s1) = 4,len(s2) = 3,

在这里插入图片描述

因此s2看不到元素四,默认该切片中只有三个元素,再向切片中追加元素时会默认追加到第四个元素的位置,此时就会把先前的元素4给覆盖掉

此时该数组空间有四个元素:1,2,3,5,因此输出s1,s2,都会得到1,2,3,5的答案

同样的在题目中,结尾处s2的len为3,所以输出s2,只能输出三个元素,最后一个1是不会被输出的

至此,题目完美解决:选C:[1,2] [2,3,4]

  • 8
    点赞
  • 28
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值