Go:从源码角度学习Go中的动态数组——切片(Slice)

        在Go:数组-优快云博客一文中,我们详细讲解了数组的定义和使用,也很容易感受到在很多场景下,数组的使用都很受限,但是Go中提供了一种灵活,功能强悍的内置类型切片("动态数组"),与数组相比切片的长度是不固定的,可以追加元素,在追加时可能使切片的容量增大。

一 切片的定义和初始化

  • 切片是一种引用类型,基于数组实现,可以动态增长和收缩。
  • 切片本质上是一个对底层数组的描述,包含三个部分:指向底层数组的指针、切片的长度、切片的容量。

        切片定义时,我们无需指明它的长度:

    // 1.直接定义一个切片
	var s []int
	// 2.使用:=定义并初始化切片
	s4 := []int{1, 2, 3, 4}
	// 3.从数组中生成切片,包含arr[1], arr[2], arr[3]
	arr := [5]int{1, 2, 3, 4, 5}
	s1 := arr[1:4]
	// 4.从切片s1生成一个新的切片,指向同一个底层数组
	s2 := s1[:]
	// 5.使用 make() 函数创建一个长度为3,容量为5的切片
	s3 := make([]int, 3, 5)

二 获取切片的长度和容量

  • 切片的长度是当前切片所包含的元素数目,可以通过内置的len函数获取。
  • 容量是从切片的起始位置到底层数组末尾的元素数目,可以通过cap函数获取。

        如图,我们可以理解为,在slice底层,有一个ptr指针指向当前切片中最后一个元素的位置,从首个元素到最后一个元素就是切片的当前所包含元素的数目,也就是len长度,而从起始位置到底层数组末尾的元素数目就是切片的容量,可以通过cap函数获取。我们也可以使用make()函数自行指定切片的长度和容量。对于未初始化的切片,默认为 nil,长度为 0。

三 切片的截取

        我们可以通过设置下限及上限(包头不包尾)来设置截取切片中的部分元素:

package main

import "fmt"

func main() {
   /* 创建切片 */
   numbers := []int{0,1,2,3,4,5,6,7,8}   
   printSlice(numbers)

   /* 打印原始切片 */
   fmt.Println("numbers ==", numbers)

   /* 打印子切片从索引1(包含) 到索引4(不包含)*/
   fmt.Println("numbers[1:4] ==", numbers[1:4])

   /* 默认下限为 0*/
   fmt.Println("numbers[:3] ==", numbers[:3])

   /* 默认上限为 len(s)*/
   fmt.Println("numbers[4:] ==", numbers[4:])

   numbers1 := make([]int,0,5)
   printSlice(numbers1)

   /* 打印子切片从索引  0(包含) 到索引 2(不包含) */
   number2 := numbers[:2]
   printSlice(number2)

   /* 打印子切片从索引 2(包含) 到索引 5(不包含) */
   number3 := numbers[2:5]
   printSlice(number3)

}

func printSlice(x []int){
   fmt.Printf("len=%d cap=%d slice=%v\n",len(x),cap(x),x)
}

 四 切片的扩展(含源码分析)

        切片可以通过内置的append函数动态增加元素。当追加元素时,如果切片容量不足,Go语言会自动扩展底层数组,通常会以两倍的容量扩展。

s1 = append(s1, 6, 7)  // 向切片s1追加两个元素

        在Go语言中,切片实际上是一个包含指向底层数组指针、长度和容量的结构体。这意味着切片的长度和容量可以独立变化,底层数组的大小则由容量决定。

type slice struct {
    array unsafe.Pointer  // 指向底层数组的指针
    len   int             // 切片的长度
    cap   int             // 切片的容量
}

        我们可以看以下append函数的核心实现部分:

func append(slice []T, elems ...T) []T {
    // 获取当前切片的长度和容量
    sLen := len(slice)
    sCap := cap(slice)

    // 如果追加元素后的长度超出当前容量,则需要扩容
    if len(elems) > sCap-sLen {
        // 计算新的容量
        newCap := sCap
        if newCap == 0 {
            newCap = len(elems)
        } else {
            for newCap < sLen+len(elems) {
                if sLen < 1024 {
                    newCap += newCap
                } else {
                    newCap += newCap / 4
                }
            }
        }

        // 创建新的底层数组,并将旧数据复制到新数组
        newSlice := make([]T, newCap)
        copy(newSlice, slice)
        slice = newSlice
    }

    // 将新元素追加到切片中
    slice = slice[0 : sLen+len(elems)]
    copy(slice[sLen:], elems)
    return slice
}

        append函数首先检查当前切片的容量是否足以容纳新追加的元素。如果当前容量不足,则进入扩容逻辑。新的容量newCap是根据当前切片的长度、容量以及追加元素的数量来计算的。也就是如果当前切片长度小于1024,新的容量将会加倍(newCap += newCap)。如果当前切片长度大于等于1024,新容量增加为当前容量的1/4(newCap += newCap / 4)。这种扩容策略可以使得小数组进行快速增长(加倍),而大数组则逐步增加容量,以平衡内存使用和性能。扩容后,append函数会使用make创建一个新的底层数组,该数组的容量为newCap。然后使用copy函数将旧数组的数据复制到新的底层数组中。扩容完成后,切片指向新的底层数组,并更新切片的长度以包含追加的元素。最后,将新元素复制到切片的新位置,并返回更新后的切片。

五 切片作为函数参数

        切片是引用类型,多个切片可以共享同一个底层数组。对共享同一底层数组的切片进行操作时,会互相影响。因此我们在进行参数传递时,可以直接进行传递切片,而不再需要使用指针了,而且也无需特别指明切片大小。

func setS(s []int) {
	s[1] = 999
	fmt.Println("函数中的值:", s)
}

setS(s4)
fmt.Println("查看值是否被改变:", s4)

        当我们复制数组时,和函数参数传递同理: 

    s5 := s4
	s5[1] = 88
	fmt.Println("查看值是否被改变:", s4)

         如果需要独立的切片副本,可以使用copy函数。这样我们对新切片中的元素进行修改时,九不会影响到原先的元素了。

    copy(s6, s4)
	s6[1] = 999
	fmt.Println("查看值是否被改变:", s4)
	fmt.Println("查看s6中的元素:", s6)

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值