Golang slice源码分析

Posted 非典型程序员

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Golang slice源码分析相关的知识,希望对你有一定的参考价值。


蓝字




Golang slice源码分析

slice数据结构定义

Golang slice源码分析


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


slice底层存储实现为数组。


slice结构体有三个字段

  1. array:是一个 unsafe.Pointer 非安全类型的指针,指向一个底层数组,是一块连续的内存块

  2. len:指slice的实际长度,即slice中含有的元素个数

  3. cap:指slice底层数组的长度,即slice可以容纳多少个元素


按上面的说法slice的直接值部只含有三个int型字段,也就是slice对象的大小为3个word(机器码),也就是和机器的位数是相关的。


在64位机器上,slice对象大小是3*8byte(64位)=24byte,在32位机器上是3*4byte(32位)=12byte。


slice的大小和slice中含有的元素个数是无关的,仅仅是通过指针来关联底层数组的。


示例:slice对象大小

func Test(t *testing.T) {
    var slice1 = make([]int64010)
    t.Log(unsafe.Sizeof(slice1)) //24
    slice1 = append(slice1, 1)
    slice1 = append(slice1, 2)
    slice1 = append(slice1, 3)
    t.Log(unsafe.Sizeof(slice1)) //24
}



Golang slice源码分析

makeslice方法-源码分析

Golang slice源码分析


该方法是调用make方法创建一个slice时的逻辑,作用是为切片进行底层数组内存分配


func makeslice(et *_type, lencap int) unsafe.Pointer {
    mem, overflow := math.MulUintptr(et.size, uintptr(cap))
    if overflow || mem > maxAlloc || len < 0 || len > cap {
        // NOTE: Produce a 'len out of range' error instead of a
        // 'cap out of range' error when someone does make([]T, bignumber).
        // 'cap out of range' is true too, but since the cap is only being
        // supplied implicitly, saying len is clearer.
        // See golang.org/issue/4085.
        mem, overflow := math.MulUintptr(et.size, uintptr(len))
        if overflow || mem > maxAlloc || len < 0 {
            panicmakeslicelen()
        }
        panicmakeslicecap()
    }

    /*
    分配一个大小为bytes的对象。
    小对象是从每个p缓存的空闲列表中分配的。
    大对象(> 32kb)直接从堆中分配。
     */

    return mallocgc(mem, et, true)
}




Golang slice源码分析

growslice方法-源码分析

Golang slice源码分析


实现切片的自动扩容

概述:

1. 原切片cap<1024,扩容为原来cap的2倍

2. 原切片cap>1024,扩容为原来cap的1.25倍
3. 扩容会涉及数组拷贝,产生额外性能开销
func growslice(et *_type, old slice, cap int) slice {
    //....
    //....
    /*
    如果新的cap<原cap,那么panic
     */

    if cap < old.cap {
        panic(errorString("growslice: cap out of range"))
    }
    /*
    若是slice元素长度为为0
    像 []struct{},每个元素的长度是空的
    那么返回新的slice对象中的array大小仍然保持0,
    len为原len,cap为新cap,进行简单的扩容,其它不做任何内存分配的操作
     */

    if et.size == 0 {
        return slice{unsafe.Pointer(&zerobase), old.lencap}
    }

    //默认设置目标容量为原容量
    newcap := old.cap
    //求出二倍原容量大小
    doublecap := newcap + newcap
    if cap > doublecap {
        /*
        如果入参的容量>两倍原容量
        那么目标容量为2部原容量
        */

        newcap = cap
    } else {
        /*
        入参容量<2倍原容量的情况下
        原容量<1024时,目标容量为原容量的2倍
        */

        if old.cap < 1024 {
            newcap = doublecap
        } else {
            //原容量 >= 1024 && 原容量<入参容量的时候,目标在原容量的基础上扩容25%
            for 0 < newcap && newcap < cap {
                newcap += newcap / 4
            }
            //如果原容量为0,那么目标容量为入参的容量
            if newcap <= 0 {
                newcap = cap
            }
        }
    }

    //....
    //跟据切片类型和容量计算要分配内存的大小
    //....

    //判断内存是否溢出
    if overflow || capmem > maxAlloc {
        panic(errorString("growslice: cap out of range"))
    }

    //内存分配,为新的切片开辟容量为capmem的地址空间
    var p unsafe.Pointer
    if et.ptrdata == 0 {
        p = mallocgc(capmem, nilfalse)
        memclrNoHeapPointers(add(p, newlenmem), capmem-newlenmem)
    } else {
        p = mallocgc(capmem, et, true)
        if lenmem > 0 && writeBarrier.enabled {
            bulkBarrierPreWriteSrcOnly(uintptr(p), uintptr(old.array), lenmem-et.size+et.ptrdata)
        }
    }
    //数据拷贝
    memmove(p, old.array, lenmem)

    return slice{p, old.len, newcap}
}



Golang slice源码分析

slicecopy方法-源码分析

Golang slice源码分析


slice深拷贝的实现


func slicecopy(toPtr unsafe.Pointer, toLen int, fromPtr unsafe.Pointer, fromLen int, width uintptr) int {
    if fromLen == 0 || toLen == 0 {
        return 0
    }

    n := fromLen
    if toLen < n {
        n = toLen
    }

    if width == 0 {
        return n
    }

    //...
    //竞态剖析&内存扫描
    //...

    //内存深拷贝

    if size == 1 { 
        //原到原地址指针取值再设值,完成一次内存深度拷贝
        *(*byte)(toPtr) = *(*byte)(fromPtr)
    } else {
        memmove(toPtr, fromPtr, size)
    }
    return n
}


总结:
  • 如果不关注数据结构里面的具体值的话,使用struct{}可以节约内存,很多数据结构针对此做了优化。比如map[int]struct{} 、[]struct{}等。
  • 如果一开始就能知道slice的大小,尽量预设置切片的容量。避免自动扩容的开销
  • 切片作为函数入参时,会进行值拷贝,另外slice中对底层数组是通过指针引用,实际仍然会共享底层数据,仅仅是进行了切片直接值部的拷贝。



Golang slice源码分析

使用实践及注意点

Golang slice源码分析



01


使用append进行元素添加的时候,需要将append返回值赋值给原切片


进行append的时候,可能会导致切片的len和cap大小的变化。一定要将append方法的返回值赋值给原切片才正确。

给切片append元素有两种情况:

1. 如果原切片容量足够,即无需扩容的情况下,仅仅是len发生了变化。

2. 如果原切片容量不够,那么切片底层数据会出现扩容的情况,len和cap都将发生变化。

如果忽略掉append方法的返回值,则会察觉不到切片的变化。


func Test(t *testing.T) {
    var s1 = make([]int010//长度为0,容量为10
    _ = append(s1,1)//错误的用法,忽略append的返回值
    t.Log(s1)//这里输出为空[]这里无法看到append后的元素,不符合我们的预期
}


func Test(t *testing.T) {
    var s1 = make([]int510//长度为5,容量为10,确保增加一个元素后切片不会被扩容
    t.Log(s1)//[0 0 0 0 0]
    t.Logf("s1-append-before len=%v cap=%v add=%p"len(s1), cap(s1), &s1[0])//s1-append-before len=5 cap=10 add=0xc000022140
    _ = append(s1, 1)//忽略append函数的返回值

    /*
    这里发发现,因为没有接收append方法的返回值,append一个元素后,切片的长度并没有增加
     */

    t.Logf("s1-append-after after len=%v cap=%v add=%p"len(s1), cap(s1), &s1[0])//s1-append-after after len=5 cap=10 add=0xc000022140
    reflect.ValueOf(&s1).Elem().SetLen(len(s1)+1)//通过反射人为将len增加1
    t.Logf("s1-append-after2 len=%v cap=%v add=%p"len(s1), cap(s1), &s1[0])//s1-append-after2 len=6 cap=10 add=0xc000022140
    t.Log(s1)//[0 0 0 0 0 1] 这里已经看到我们append的元素了,符合我们的预期了
}



02


切片的容量和len可以通过反射人为修改


我们可以通过反射来修改
reflect.ValueOf(&slice).Elem().SetLen(xx)//修改切片长度
reflect.ValueOf(&slice).Elem().SetCap(xx)//修改切片容量
传递给函数reflect.SetLen的值<=切片值的容量。 
传递给函数reflect.SetCap的值>第一个实参切片值的长度&&<=第一个实参切片值的容量。 
否则,在运行时刻将产生一个恐慌。


func Test(t *testing.T) {
    := make([]int, 2, 6)
    fmt.Println(len(s), cap(s)) // 2 6

    reflect.ValueOf(&s).Elem().SetLen(3)//范围[0-6(cap)]超出范围panic
    fmt.Println(len(s), cap(s)) // 3 6
    reflect.ValueOf(&s).Elem().SetCap(7)//范围[3(len+1)-6(cap)]超出范围panic
    fmt.Println(len(s), cap(s)) // 3 5
}




03

切片的子切片和原切片使用同一个底层数组,修改子切片对原切片可见


注意子切片和原切片使用的是同一个底层数组,修改子切片会对原切片可见,但如果是对子切片append操作,导致了切片的扩容,那么对子切片的修改对原切片不可见,这个要注意
func Test(t *testing.T) {
    slice := []int{0,1,2,3,4,5}
    /*
    从原切片中解脱第3,4个元素
     */

    subSlice :=slice[2:4]//[2 3]
    t.Log(subSlice)//[2 3]
    subSlice[1]=888//通过子切片改值
    t.Log(subSlice)//[2 888]
    t.Log(slice)//[0 1 2 888 4 5] 在原切片中也会生效
}




04


切片的0值为nil,对nil切片进行append不会panic


func Test8(t *testing.T) {
    var slice []int
    t.Log(slice==nil)//true
    slice = append(slice,1)//不会panic
    t.Log(slice)//1
}




05


删除一个切片元素


// 第一种方法(保持剩余元素的次序):
s = append(s[:i], s[i+1:]...)

// 第二种方法(保持剩余元素的次序):
s = s[:i + copy(s[i:], s[i+1:])]

// 上面两种方法都需要复制len(s)-i-1个元素。

// 第三种方法(不保持剩余元素的次序):
s[i] = s[len(s)-1]
s = s[:len(s)-1]


06


删除一段切片元素


// 第一种方法(保持剩余元素的次序):
s = append(s[:from], s[to:]...)

// 第二种方法(保持剩余元素的次序):
s = s[:from + copy(s[from:], s[to:])]

// 第三种方法(不保持剩余元素的次序):
if n := to-from; len(s)-to < n {
    copy(s[from:to], s[to:])
else {
    copy(s[from:to], s[len(s)-n:])
}
s = s[:len(s)-(to-from)]


大家可以随手点击个关注支持一下哈Golang slice源码分析Golang slice源码分析Golang slice源码分析Golang slice源码分析Golang slice源码分析Golang slice源码分析Golang slice源码分析Golang slice源码分析Golang slice源码分析