切片作为常用的数据结构体之一,切片实际上是数组的抽象,也称动态数组,顾名思义,它自带扩容的机制,因为其灵活性,相对数组来说被运用的更加广泛。

结构体

golang的切片实现是在包runtime/slice.go,切片结构体包含array指向数组的指针,是一块连续的内存空间,len代表切片的长度,cap代表切片的容量,cap总是大于等于len。

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

描述

从上图可以看出,切片是在数组的基础上抽象了一层,底层是对数组的引用,当切片发生扩容时,底层数组发生改变,而对于上层切片来说是没有变化的。

make初始化

先来看看slice的初始化,slice的初始化可以通过make关键字,传入type、len、cap。

make(Type,len,cap)

var numbers = make([]int64,5,6)

slice的make初始化主要通过runtime.makeslice来完成,先计算出需要的内存空间大小,然后再分配内存。

func makeslice(et *_type, len, cap 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()
    }
    //分配内存
    return mallocgc(mem, et, true)
}

内存空间大小的计算公式为:

内存空间大小 = 切片中元素大小 * 容量大小

append扩容

当使用append的时候就可能会触发切片的扩容机制,扩容是调用runtime.growslice,具体步骤为:

func growslice(et *_type, old slice, cap int) slice {
    .....
    newcap := old.cap
    doublecap := newcap + newcap
    if cap > doublecap {
        newcap = cap
    } else {
        if old.len < 1024 {
            newcap = doublecap
        } else {
            // Check 0 < newcap to detect overflow
            // and prevent an infinite loop.
            for 0 < newcap && newcap < cap {
                newcap += newcap / 4
            }
            // Set newcap to the requested cap when
            // the newcap calculation overflowed.
            if newcap <= 0 {
                newcap = cap
            }
        }
    }
}
.....

func growslice(et *_type, old slice, cap int) slice {
var overflow bool
var lenmem, newlenmem, capmem uintptr
/*
 *lenmem表示旧切片实际元素长度所占的内存空间大小
 *newlenmem表示新切片实际元素长度所占的内存空间大小
 *capmem表示扩容之后的容量大小
 *overflow是否溢出
 */
// Specialize for common values of et.size.
// For 1 we don't need any division/multiplication.
// For sys.PtrSize, compiler will optimize division/multiplication into a shift by a constant.
// For powers of 2, use a variable shift.
switch {
case et.size == 1: //元素所占的字节数为1
    lenmem = uintptr(old.len)
    newlenmem = uintptr(cap)
    capmem = roundupsize(uintptr(newcap))//向上取整分配内存
    overflow = uintptr(newcap) > maxAlloc
    newcap = int(capmem)
case et.size == sys.PtrSize: //元素所占的字节数为8个字节
    lenmem = uintptr(old.len) * sys.PtrSize
    newlenmem = uintptr(cap) * sys.PtrSize
    capmem = roundupsize(uintptr(newcap) * sys.PtrSize)
    overflow = uintptr(newcap) > maxAlloc/sys.PtrSize
    newcap = int(capmem / sys.PtrSize)
case isPowerOfTwo(et.size): //元素所占的字节数为2的倍数
    var shift uintptr
    //根据元素的字节数计算出位运算系数
    if sys.PtrSize == 8 {
        // Mask shift for better code generation.
        shift = uintptr(sys.Ctz64(uint64(et.size))) & 63
    } else {
        shift = uintptr(sys.Ctz32(uint32(et.size))) & 31
    }
    //计算内存空间转化为用位运算
    lenmem = uintptr(old.len) << shift
    newlenmem = uintptr(cap) << shift
    capmem = roundupsize(uintptr(newcap) << shift)
    overflow = uintptr(newcap) > (maxAlloc >> shift)
    newcap = int(capmem >> shift)
default:
    lenmem = uintptr(old.len) * et.size
    newlenmem = uintptr(cap) * et.size
    capmem, overflow = math.MulUintptr(et.size, uintptr(newcap))
    capmem = roundupsize(capmem)
    newcap = int(capmem / et.size)
}
}
计算出需要分配的内存大小后,就会重新申请内存,然后将原来切片的元素重新赋值到新的切片中。  
``` go
func growslice(et *_type,old slice,cap int)slice{
    var p unsafe.Pointer
    //如果元素不是指针
    if et.ptrdata == 0{
        //申请一块无类型的内存空间
        p = mallocgc(capmem, nil, false)
        // The append() that calls growslice is going to overwrite from old.len to cap (which will be the new length).
        // Only clear the part that will not be overwritten.
        //将超出切片当前长度的位置进行初始化
        memclrNoHeapPointers(add(p, newlenmem), capmem-newlenmem)
    }else{
        // Note: can't use rawmem (which avoids zeroing of memory), because then GC can scan uninitialized memory.
        //根据元素类型申请内存空间
        p = mallocgc(capmem,et,true)
        if lenmem > 0 && writeBarrier.enabled {
            // Only shade the pointers in old.array since we know the destination slice p
            // only contains nil pointers because it has been cleared during alloc.
            bulkBarrierPreWriteSrcOnly(uintptr(p), uintptr(old.array), lenmem-et.size+et.ptrdata)
        }
    }
    //将旧切片的值拷入新的切片
    memmove(p, old.array, lenmem)
}

拷贝切片

拷贝切片可以用copy方法

func copy(dst, src []Type) int

func main() {
    var number1 =[]int64{1}
    var number2 =make([]int64,len(number1))
    copy(number2,number1)
    fmt.Println(number2)
}

实际上copy根据数据类型,最终会调用切片的runtime.slicecopy方法。

func slicecopy(toPtr unsafe.Pointer, toLen int, fmPtr unsafe.Pointer, fmLen int, width uintptr) int {
    //如果源切片和目标切片长度为0,则直接返回0
    if fmLen == 0 || toLen == 0 {
        return 0
    }

    //根据源切片和目标切片的长度,以长度最小的切片进行拷贝
    n := fmLen
    if toLen < n {
        n = toLen
    }

    if width == 0 {
        return n
    }

    //拷贝的空间大小=长度 * 元素大小
    size := uintptr(n) * width
    if size == 1 { // common case worth about 2x to do here
        // TODO: is this still worth it with new memmove impl?
        //如果拷贝的空间大小等于1,那么直接转化赋值
        *(*byte)(toPtr) = *(*byte)(fmPtr) // known to be a byte pointer
    } else {
        //如果拷贝的空间大小大于1,则源切片中array的数据拷贝到目标切片的array
        memmove(toPtr, fmPtr, size)
    }
    return n
}

从源码可以看出在切片拷贝的时候,要预先定义切片的长度再进行拷贝,否则有可能拷贝失败。

总结

  • 在扩容过程中,切片的地址不会被改变,改变的是切片的底层数组array,会申请一块新的内存地址替换。
  • slice没有缩小容量的操作 更多欢迎关注go成神之路