0


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

目录

问题引入

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

package main

import"fmt"funcSliceRise(s []int){
    s =append(s,0)for i :=range s {
        s[i]++}}funcmain(){
    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
    lenintcapint}

由此可以看出:在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"funcmain(){

    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例:

funcmain(){

    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"funcSliceRise(s []int){
    s =append(s,0)for i :=range s {
        s[i]++}}funcmain(){
    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即是一个指向底层数组的指针,这里需要注意的是,数组直接赋值使用的是址传递的方法,我们可以看一下下面这个例子:

funcmain(){

    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本身的地址的话,是什么样的结果呢?

funcmain(){

    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]

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

funcmain(){

    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]


本文转载自: https://blog.csdn.net/qq_63730529/article/details/138169657
版权归原作者 三里清风_ 所有, 如有侵权,请联系我们删除。

“一篇超详细的go语言中的Slice(切片)讲解~”的评论:

还没有评论