• GolangSlice 内部实现原理解析


    Golang - slice 内部实现原理解析

    一.Go中的数组和slice的关系

    1.数组

    在几乎所有的计算机语言中,数组的实现都是一段连续的内存空间,Go语言数组的实现也是如此,但是Go语言中的数组和C语言中数组还是有所不同的

    • C语言数组变量是指向数组第一个元素的指针
    • Go语言的数组是一个值,一个数组变量就代表整个数组,意味着Go语言的数组在传递的时候,传递是是原数组的拷贝!

    这也就意味着数组在传递的时候,对大数组来说,内存代价会非常大,影响性能,传递数组指针可以解决这个问题,但是数组指针也有一个弊端:

    • 原数组的指针指向改变了,那函数里面的指针指向也会跟着改变,某些情况下,可能会产生意想不到的bug

    slice的出现,便是为了解决这个问题

    2.slice

    先来看一张图,上图中,ptr就是指向底层数组的指针,len是指slice的长度,cap是指slice的容量

    • slice本身并不是动态数组或者数组指针,它的内部实现是通过指针引用底层数组,设置相关的属性,将数据的读写操作限定在指定的区域内
    • slice本身是一个只读读写,你修改的是底层数组,而不是slice本身,其工作机制类似于数组指针的一种封装
    • slice是对数组中一个连续片段的引用,所以slice是一个引用类型

    当然从宏观和使用上来说,你可以将slice当做一个长度可变的数组,类似C++的Vector。

    二.slice的初始化方式

    方式1:字面量

        s = s[2:4]
    

    指针指向s[2],容量是3,长度是2

    需要注意的是,尽量不要采用字面量这种方式初始化slice,除非情况特殊,因为一个字面量数组可以初始化很多个slice,修改一个slice,会影响另一个slice的值,因为引用的都是同一个底层数组

    比如下图

    sliceA和sliceB都是同一个底层数组,并且有重叠的部分!Array[2],30

    方式2:make

        s := make([]byte, 5)
    

    最为安全的slice初始化方式,推荐使用,除非业务特殊你实在想让你的slice共用同一个底层数组,再补充一个图,来说明slice长度和容量的区别

    长度4,代表此时4个元素,容量6,代表总共可以装6个元素,还有两个位置空闲

    三.slice的扩容规则

    slice可以理解为动态数组,既然是动态数组,那必然需要进行扩容,slice扩容遵循以下规则:

    • slice容量小于1024个元素,则扩容后容量直接翻倍,
    • slice容量不小于1024个元素,则每次增加原来容量是四分之一
    • 如果扩容后,还是比底层数组的容量小,那么slice的指针还是指向原来的底层数组。
    • 如果扩容后,超过了底层数组的容量,那么会开辟一块新内存,并将原来的值拷贝过来,这种情况,slice的任何操作都不会影响原底层数组

    四. slice的拷贝

    1.浅拷贝情况

    • 浅拷贝,拷贝的是地址,只是复制指向对象的指针
    • slice是引用类型数据,默认引用类型数据,全部都是浅拷贝,slice,Map等
        slice2 := slice1
    
    • slice1和slice2指向的都是同一个底层数组,任何一个数组元素被改变,都可能会影响两个slice
    • 在slice触发扩容操作前,slice1和slice2指向的都是相同数组,但在触发扩容操作后,二者指向的就不一定是相同的底层数组了,具体可参考上诉slice的扩容规则

    2. 深拷贝情况

    • 深拷贝,拷贝的是数据本身,会创建一个新对象
        copy(slice2, slice1)  
    
    • 新对象和原对象不共享内存,在新建对象的内存中开辟一个新的内存地址,新对象的值修改不会影响原对象值,既然内存地址不同,释放内存地址时,可以分别释放

    五. slice内存泄露情况

    当slice的底层数组很大,但slice所取元素数量很小时,底层数组占据的大部分空间都是被浪费的

    • 比如b数组很大,slice a只引用了b很小的一部分,只要slice a还在,b数组就永远不会被回收,就是造成了内存泄露!
    var a []int
    
    func test(b []int) {
        a = b[:1] // 和b共用一个底层数组
        return
    }
    

    解决方法:

    • 不再引用b数组,将需要的数据复制到一个新的slice中,这样新slice的底层数组,就和b数组无任何关系了
    var a []int
    
    func test(b []int) {
        a = make([]int, 1)
        copy(a, b[:0])
        return
    }
    

    六. slice 非并发安全

    slice不是并发安全的,要并发安全,有两种方法:

    • 加锁
    • channle

    1.加锁

    适合于对性能要求不高的场景,毕竟锁的粒度太大,这种方式属于通过共享内存来实现通信

    func TestSliceConcurrencySafeByMutex(t *testing.T) {
        var lock sync.Mutex //互斥锁
        a := make([]int, 0)
        var wg sync.WaitGroup
        for i := 0; i < 10000; i++ {
            wg.Add(1)
            go func(i int) {
                defer wg.Done()
                lock.Lock()
                defer lock.Unlock()
                a = append(a, i)
            }(i)
        }
        wg.Wait()
        t.Log(len(a)) 
        // equal 10000
    }
    

    2.channle

    适合于对性能要求大的场景,channle就是专用于goroutine间通信的,这种方式属于通过通信来实现共享内存,而Go的箴言便是:尽量通过通信来实现内存共享,而不是通过共享内存来实现通信,推荐此方法!

    func TestSliceConcurrencySafeByChanel(t *testing.T) {
        buffer := make(chan int)
        a := make([]int, 0)
        // 消费者
        go func() {
            for v := range buffer {
                a = append(a, v)
            }
        }()
        // 生产者
        var wg sync.WaitGroup
        for i := 0; i < 10000; i++ {
            wg.Add(1)
            go func(i int) {
                defer wg.Done()
                buffer <- i
            }(i)
        }
        wg.Wait()
        t.Log(len(a)) 
        // equal 10000
    }
    
    

    七. 小结

    根据上述内容,可以总结出以下几点:

    • 创建slice时应根据实际需要预分配容量,避免追加过程中频繁扩容,有助于性能提升
    • slice是非并发安全的,如要实现并发安全,请采用锁或channle
    • 大数组作为函数参数时,会复制整个数组,消耗过多内存,建议采用slice或指针
    • 如果只用到大的slice或数组的一部分,建议将需要部分复制到新的slice中取,减少内存占用
    • 多个slice指向相同的底层数组时,修改其中一个slice,可能会影响其他slice的值
    • slice作为参数传递时,比数组更为高效,因为slice本身的结构就比较小!所以你参数传递时,传slice和传slice的引用,其实开销区别不大
    • slice在扩容时,可能会发生底层数组的变更和内存拷贝

    参考:

  • 相关阅读:
    linux文件系统初探--Day6
    Oracle 内置函数
    libusb常用函数说明(转)
    将多个blv格式的视频合并为一个mp4格式视频
    泰迪杯赛后总结
    如何下载B站上版权受限的视频?
    查看Ubuntu版本信息
    Visual Studio存在多个项目时启动项目的问题
    mfc | 初识mfc
    re | [ACTF新生赛2020]Splendid_MineCraft
  • 原文地址:https://www.cnblogs.com/yinbiao/p/15802792.html
Copyright © 2020-2023  润新知