文章详情

短信预约-IT技能 免费直播动态提醒

请输入下面的图形验证码

提交验证

短信预约提醒成功

Golang中的Slice底层如何实现

2023-07-05 06:54

关注

这篇“Golang中的Slice底层如何实现”文章的知识点大部分人都不太理解,所以小编给大家总结了以下内容,内容详细,步骤清晰,具有一定的借鉴价值,希望大家阅读完这篇文章能有所收获,下面我们一起来看看这篇“Golang中的Slice底层如何实现”文章吧。

1 Go数组

Go数组是值类型,数组定义的时候就需要指定大小,不同大小的数组是不同的类型,数组大小固定之后不可改变。数组的赋值和传参都会复制一份。

func main() {    arrayA := [2]int{100, 200}    var arrayB [2]int    arrayB = arrayA    fmt.Printf("arrayA : %p , %v\n", &arrayA, arrayA)    fmt.Printf("arrayB : %p , %v\n", &arrayB, arrayB)    testArray(arrayA)}func testArray(x [2]int) {    fmt.Printf("func Array : %p , %v\n", &x, x)}

结果:

arrayA : 0xc4200bebf0 , [100 200]
arrayB : 0xc4200bec00 , [100 200]
func Array : 0xc4200bec30 , [100 200]

可以看到,三个内存地址都不同,这也就验证了 Go 中数组赋值和函数传参都是值复制的。尤其是传参的时候把数组复制一遍,当数组非常大的时候会非常消耗内存。可以考虑使用指针传递。

指针传递有个不好的地方,当函数内部改变了数组的内容,则原数组的内容也改变了。

因此一般参数传递的时候使用slice

2 切片的数据结构

切片本身并不是动态数组或者数组指针。它内部实现的数据结构通过指针引用底层数组,设定相关属性将数据读写操作限定在指定的区域内。切片本身是一个只读对象,其工作机制类似数组指针的一种封装。

切片(slice)是对数组一个连续片段的引用,所以切片是一个引用类型。这个片段可以是整个数组,或者是由起始和终止索引标识的一些项的子集。需要注意的是,终止索引标识的项不包括在切片内。切片提供了一个与指向数组的动态窗口。

给定项的切片索引可能比相关数组的相同元素的索引小。和数组不同的是,切片的长度可以在运行时修改,最小为 0 最大为相关数组的长度:切片是一个长度可变的数组。

切片数据结构定义

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

Golang中的Slice底层如何实现

切片的结构体由3部分构成,Pointer 是指向一个数组的指针,len 代表当前切片的长度,cap 是当前切片的容量。cap 总是大于等于 len 的。

Golang中的Slice底层如何实现

如果想从 slice 中得到一块内存地址,可以这样做:

s := make([]byte, 200)ptr := unsafe.Pointer(&s[0])

3 创建切片

3.1 方法一:make

使用make函数创建slice

// 创建一个初始大小是3,容量是10的切片s1 := make([]int64,3,10)

底层方法实现:

func makeslice(et *_type, len, cap int) slice {    // 根据切片的数据类型,获取切片的最大容量    maxElements := maxSliceCap(et.size)    // 比较切片的长度,长度值域应该在[0,maxElements]之间    if len < 0 || uintptr(len) > maxElements {        panic(errorString("makeslice: len out of range"))    }    // 比较切片的容量,容量值域应该在[len,maxElements]之间    if cap < len || uintptr(cap) > maxElements {        panic(errorString("makeslice: cap out of range"))    }    // 根据切片的容量申请内存    p := mallocgc(et.size*uintptr(cap), et, true)    // 返回申请好内存的切片的首地址    return slice{p, len, cap}}

3.2 方法二:字面量

利用数组创建切片

arr := [10]int64{1,2,3,4,5,6,7,8,9,10}s1 := arr[2:4:6] // 以arr[2:4]创建一个切片,且容量到达arr[6]的位置,即cap=6-2=4,如果不写容量则默认为数组最后一个元素

4 nil和空切片

Golang中的Slice底层如何实现

nil切片的指针指向的是nil

Golang中的Slice底层如何实现

空切片指向的是一个空数组

空切片和 nil 切片的区别在于,空切片指向的地址不是nil,指向的是一个内存地址,但是它没有分配任何内存空间,即底层元素包含0个元素。

最后需要说明的一点是。不管是使用 nil 切片还是空切片,对其调用内置函数 append,len 和 cap 的效果都是一样的

5 切片扩容

5.1 扩容策略

func main() {    slice := []int{10, 20, 30, 40}    newSlice := append(slice, 50)    fmt.Printf("Before slice = %v, Pointer = %p, len = %d, cap = %d\n", slice, &slice, len(slice), cap(slice))    fmt.Printf("Before newSlice = %v, Pointer = %p, len = %d, cap = %d\n", newSlice, &newSlice, len(newSlice), cap(newSlice))    newSlice[1] += 10    fmt.Printf("After slice = %v, Pointer = %p, len = %d, cap = %d\n", slice, &slice, len(slice), cap(slice))    fmt.Printf("After newSlice = %v, Pointer = %p, len = %d, cap = %d\n", newSlice, &newSlice, len(newSlice), cap(newSlice))}

输出结果:

Before slice = [10 20 30 40], Pointer = 0xc4200b0140, len = 4, cap = 4
Before newSlice = [10 20 30 40 50], Pointer = 0xc4200b0180, len = 5, cap = 8
After slice = [10 20 30 40], Pointer = 0xc4200b0140, len = 4, cap = 4
After newSlice = [10 30 30 40 50], Pointer = 0xc4200b0180, len = 5, cap = 8

Go 中切片扩容的策略是这样的:

如果切片的容量小于 1024 个元素,于是扩容的时候就翻倍增加容量。上面那个例子也验证了这一情况,总容量从原来的4个翻倍到现在的8个。

一旦元素个数超过 1024 个元素,那么增长因子就变成 1.25 ,即每次增加原来容量的四分之一。

5.2 底层数组是不是新地址

不一定。当发生了扩容就肯定是新数组,没有发生扩容则是旧地址

不管切片是通过make创建还是字面量创建,底层都是一样的,指向的是一个数组。当使用字面量创建时,切片底层使用的数组就是创建时候的数组。修改切片中的元素或者往切片中添加元素,如果没有扩容,则会影响原数组的内容,切片底层和原数组是同一个数组;当切片扩容了之后,则修改切片的元素或者往切片中添加元素,不会修改数组内容,因为切片扩容之后,底层数组不再是原数组,而是一个新数组。

所以尽量避免切片底层数组与原始数组相同,尽量使用make创建切片

range遍历数组或者切片需要注意

func main() {// slice := []int{10, 20, 30, 40}slice := [4]int{10, 20, 30, 40}for index, value := range slice {fmt.Printf("value = %d , value-addr = %x , slice-addr = %x\n", value, &value, &slice[index])}}

结果:

value = 10 , value-addr = c00000a0a8 , slice-addr = c000012360
value = 20 , value-addr = c00000a0a8 , slice-addr = c000012368
value = 30 , value-addr = c00000a0a8 , slice-addr = c000012370
value = 40 , value-addr = c00000a0a8 , slice-addr = c000012378

从上面结果我们可以看到,如果用 range 的方式去遍历一个数组或者切片,拿到的 Value 其实是切片里面的值拷贝。所以每次打印 Value 的地址都不变。

Golang中的Slice底层如何实现

由于 Value 是值拷贝的,并非引用传递,所以直接改 Value 是达不到更改原切片值的目的的,需要通过 &slice[index] 获取真实的地址

尤其是在for循环中使用协程,一定不能直接把index,value传入协程,而应该通过参数传进去

错误示例:

func main() {s := []int{10,20,30}for index, value := range s {go func() {time.Sleep(time.Second)fmt.Println(fmt.Sprintf("index:%d,value:%d", index,value))}()}time.Sleep(time.Second*2)}

结果:

index:2,value:30
index:2,value:30
index:2,value:30

正确示例:

func main() {s := []int{10,20,30}for index, value := range s {go func(i,v int) {time.Sleep(time.Second)fmt.Println(fmt.Sprintf("index:%d,value:%d", i,v))}(index,value)}time.Sleep(time.Second*2)}

结果:

index:0,value:10
index:2,value:30
index:1,value:20

以上就是关于“Golang中的Slice底层如何实现”这篇文章的内容,相信大家都有了一定的了解,希望小编分享的内容对大家有帮助,若想了解更多相关的知识内容,请关注编程网行业资讯频道。

阅读原文内容投诉

免责声明:

① 本站未注明“稿件来源”的信息均来自网络整理。其文字、图片和音视频稿件的所属权归原作者所有。本站收集整理出于非商业性的教育和科研之目的,并不意味着本站赞同其观点或证实其内容的真实性。仅作为临时的测试数据,供内部测试之用。本站并未授权任何人以任何方式主动获取本站任何信息。

② 本站未注明“稿件来源”的临时测试数据将在测试完成后最终做删除处理。有问题或投稿请发送至: 邮箱/279061341@qq.com QQ/279061341

软考中级精品资料免费领

  • 历年真题答案解析
  • 备考技巧名师总结
  • 高频考点精准押题
  • 2024年上半年信息系统项目管理师第二批次真题及答案解析(完整版)

    难度     801人已做
    查看
  • 【考后总结】2024年5月26日信息系统项目管理师第2批次考情分析

    难度     348人已做
    查看
  • 【考后总结】2024年5月25日信息系统项目管理师第1批次考情分析

    难度     311人已做
    查看
  • 2024年上半年软考高项第一、二批次真题考点汇总(完整版)

    难度     432人已做
    查看
  • 2024年上半年系统架构设计师考试综合知识真题

    难度     220人已做
    查看

相关文章

发现更多好内容

猜你喜欢

AI推送时光机
位置:首页-资讯-后端开发
咦!没有更多了?去看看其它编程学习网 内容吧
首页课程
资料下载
问答资讯