Golang源码探究 —— Slice

本文深入探讨了Go语言中切片的底层实现,包括切片的数据结构、内存占用、创建过程以及动态增长策略。在动态增长策略部分,详细分析了扩容规则的变化,指出在不同版本中增长策略的优化。同时,文章强调了切片传递时的注意事项,解释了因值传递导致的修改切片长度不一致的问题,并给出了实例演示。此外,还讨论了如何正确处理函数中切片的修改,以避免数据丢失或意外行为。

Golang中的切片算是在代码中使用频率最高的数据结构了,因此了解切片的底层实现可以让我们对切片的使用可以更加熟练和灵活。

go的版本:go version go1.16.8 windows/amd64

1、切片的数据结构

Golang中的切片定义在runtime包下的slice.go中,它在底层为一个结构体:

type slice struct {
   
   
	array unsafe.Pointer        // 指向数据缓冲区的指针
	len   int                   // 当前数据缓冲区使用的size
	cap   int                   // 当前缓冲区的容量
}

slice中包含三个字段,array是指向数据数组的指针,len是当前数组使用的大小,cap是整个数组的大小。

在下面的代码中打印slice的内存大小,可以看到slice占用的内存大小为24byte,三个字段各占8byte的内存。

package main

import (
    "fmt"
    "unsafe"
)

func main() {
   
   
    var s []int 
    fmt.Println(unsafe.Sizeof(s))

    var a int
    fmt.Println(unsafe.Sizeof(a))
}

// 打印结果
24
8

由于slice定义在runtime包中,我们无法使用,因此可以自己定义一个Slice来将sliec强转为我们的Slice:

package main

import (
    "fmt"
    "unsafe"
)

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

func main() {
   
   
    s := []int{
   
   1, 2, 3, 4, 5}
    for i, v := range s {
   
   
        fmt.Printf("index:%d, value:%d\n", i, v)
    }

    sptr := (*Slice)(unsafe.Pointer(&s))
    fmt.Println(sptr.len)
    fmt.Println(sptr.cap)

    arr := (*[5]int)(sptr.array)
    for i := 0; i < 5; i++ {
   
   
        fmt.Printf("%d ", arr[i])
    }
    fmt.Println()
}

// 打印结果
index:0, value:1
index:1, value:2
index:2, value:3
index:3, value:4
index:4, value:5
5
5
1 2 3 4 5 

 

2、创建切片

makeslice函数用于创建一个切片。

func makeslice(et *_type, len, cap int) unsafe.Pointer {
   
   
    // 该函数的作用是计算需要的空间大小,并检查是否溢出,
    // 使用et.size * cap,mem为相乘的结果,overflow为是否溢出,bool类型
	mem, overflow := math.MulUintptr(et.size, uintptr(cap))
	if overflow || mem > maxAlloc || len < 0 || len > cap {
   
   
		mem, overflow := math.MulUintptr(et.size, uintptr(len))
		if overflow || mem > maxAlloc || len < 0 {
   
   
			panicmakeslicelen()
		}
		panicmakeslicecap()
	}

    // 分配内存 
    // 小对象从当前P的cache中空闲数据中分配
    // 大的对象 (size > 32KB) 直接从heap中分配
    // runtime/malloc.go
	return mallocgc(mem, et, true)
}

func makeslice64(et *_type, len64, cap64 int64) unsafe.Pointer {
   
   
	len := int(len64)
	if int64(len) != len64 {
   
   
		panicmakeslicelen()
	}

	cap := int(cap64)
	if int64(cap) != cap64 {
   
   
		panicmakeslicecap()
	}

	return makeslice(et, len, cap)
}

可以看到在makeslice中做的事情就是先检查传入的参数的合法性,以及计算需要的内存是否合法,不合法就panic。最后分配内存,返回指针。但是在上面的代码中应该只是创建了底层数组的内存,slice结构体并没有创建。

 

2、Slice的动态增长策略

growslice函数处理append期间的数组动态增长。

// et为切片存放的数据的类型,old为旧的slice,cap为期望的容量大小
func growslice(et *_type, old slice, cap int) slice {
   
   
    ...   // 调试相关

	if cap < old.cap {
   
   
		panic(errorString("growslice: cap out of range")
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值