Golang-Slice解读

853 阅读9分钟

Golang之Slice&&Array深度解读

一,基本用法

数组

  1. 数组是具有同一类型的一组已编号且长度固定的数据项序列
  2. 声明方式
    1)在Go语言中,数组的声明方式为:var array-name [len]type
    2)声明时没有指定数组的初始化值,因此所有的元素都会被自动初始化为该类型的默认值,例如:var a1 [5]int ,数组中的五个值都为0
    3)Go语言中的数组是值类型,因此还可以使用new来创建。例如:var a2 = new([5]int),new返回的是该类型的指针,因此a2的类型为*[5]int,a1的类型为[5]int
  3. 初始化方法
    1)我们通常在{}中填写初始化值来初始化数组,初始化值的个数不允许超过长度len。
    2)也可以使用 {index1: a, index2: b} 的方式初始化数组,指明数组的索引和对应的下标值,未指明的下标所在位置的值等于默认值 0: 3)暗示数组的长度:初始化的时候可以不指名数组的长度,而是使用[...]代替,然后再{}中写明元素的值
    1// 数组长度为 5,初始化了前两个数,未初始化的位是 0
    b := [5]int{1, 2} 
    for index, val := range b {
        fmt.Printf("下标 = %d, 值 = %d\n", index, val)
    }
    /* Output:
    下标 = 0, 值 = 1
    下标 = 1, 值 = 2
    下标 = 2, 值 = 0
    下标 = 3, 值 = 0
    下标 = 4, 值 = 0
    */

    2// 通过数组索引初始化
    // d[0] = 1, d[2] = 3,其他位置等于 0
    d := [5]int{0: 1, 2: 3} 
    for index, val := range d {
        fmt.Printf("下标 = %d, 值 = %d\n", index, val)
    }
    /* Output:
    下标 = 0, 值 = 1
    下标 = 1, 值 = 0
    下标 = 2, 值 = 3
    下标 = 3, 值 = 0
    下标 = 4, 值 = 0
    */

    3// 通过传递初始化值确定数组长度
    // 传递了 5 个元素,数组长度为 5
    c := [...]int{1, 2, 3, 4, 5}  
    for index, val := range c {
        fmt.Printf("下标 = %d, 值 = %d\n", index, val)
    }
    /* Output:
    下标 = 0, 值 = 1
    下标 = 1, 值 = 2
    下标 = 2, 值 = 3
    下标 = 3, 值 = 4
    下标 = 4, 值 = 5
    */
  1. 数组指针和指针数组
    • 数组指针:指向数组的指针
    • 指针数组:装满了指针的数组
    1)数组指针:声明一个数组a,然后将他的地址赋值给arrayPointer。这样一来,arrayPointer就是一个指向数组a的指针,即数组指针,他的类型为*[5]int。
    2)指针数组:初始化数组pointerArray,传入的初始值为整型m与n的内存地址(&m和&n),那么这样一来pointerArray就是一个装着int类型的指针数组,即指针数组,他的类型为[2]*int
    1)
    a := [5]int{1, 2, 3, 4, 5}
    // 把数组 a 的地址赋值给 arrayPointer
    // arrayPointer 是指向数组的指针,类型为 *[5]int
    arrayPointer := &a
    fmt.Println(arrayPointer)

    /* Output:
    &[1 2 3 4 5]
    */
    2)
    m := 1
    n := 2
    // 初始化 pointerArray,传入 m 与 n 的地址
    // pointerArray 包含了整型地址,是一个装着指针的数组
    pointerArray := [2]*int{&m, &n}
    fmt.Println(pointerArray)

    /* Output:
    [0xc0000aa000 0xc0000aa008]
    */

切片

  1. 定义
    1)切片是GO中的一种基本数据结构。切片的想法是由动态数组来的,但是切片本身并不是动态数组或者数组指针。切片常见的操作有reslice,append,copy。与此同时切片还有可索引,可迭代的优秀特性。它内部实现的数据结构通过指针引用底层数组,设定相关属性将数据读写操作限定在指定的区域内。切片本身是一个只读对象,其工作机制类似数组指针的一种封装。切片是对数组的一个连续片段的引用,切片提供了一个指向底层数组的动态窗口。和数组不同的是切片的长度可以在运行时修改,最小为0最大为相关数组的长度:切片是一个长度可变的数组。
    2)slice 共有三个属性:
    指针,指向底层数组;
    长度,表示切片可用元素的个数,也就是说使用下标对 slice 的元素进行访问时,下标不能超过 slice 的长度;
    容量,底层数组的元素个数,容量 >= 长度。在底层数组不进行扩容的情况下,容量也是 slice 可以扩张的最大限度
  1. struct
    1)切片的结构是由3部分组成,Pointer是指向一个数组的指针,len代表了当前切片的长度,cap是当前切片的的容量。cap总是大于等于len 。
    2)如果想从slice中得到一块内存地址,可以这样做:
    3)反过来,从Go的内存地址中构造一个slice
    4)当然有更加直接的方式,在go的反射中就存在一个与之对应的数据结构SliceHeader,我们可以用她来构造一个Slice
1)
type slice struct {
    array unsafe.Pointer
    len   int
    cap   int
}
2)
s := make([]byte,200)
ptr := unsafe.Pointer(&s[0]) 
3)
var ptr unsafe.Pointer  
var s1 = struct {
    addr uintptr
    len int 
    cap int 
}{ptr,length,length}
s := *(*[]byte)(unsafe.Pointer(&s1))
构造一个虚拟的结构体,把slice的数据结构拼出来
4)
var o []byte
sliceHeader := (*reflect.SliceHeader)((unsafe.Pointer(&o)))
sliceHeader.Cap = length
sliceHeader.Len = length
sliceHeader.Data = uintptr(ptr)

  1. 创建切片
    1)make函数允许程序运行期间指定数组的长度,绕开了数组类型必须使用编译期常量的限制。创建切片有两种形式:make创建切片,空切片。
    2)图一是用make函数创建的len为4,cap为6的切片。内存空间申请了6个int类型的内存的大小,由于len=4,所以后面的两个暂时访问不到,但是容量还是在的这个时候数组里面的每个变量都是0
    3)下面图二使用了字面量创建了一个len=6,cap=6的切片,这个时候数组里的每个元素都初始化完成了。
    4)下图三的SliceA就创建出了一个len = 3,cap = 3的切片 。从原数组的第三位元素开始(0是第一位)开始切,一直切到第四位为止(不包括第五位)。(个人感觉第三张图的写法有误,应该是Slice := array[2:5],但是没办法解释SliceB的容量问题)
1)

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}
}

//int64的版本,实现原理和上面的一样,只不过是多了把int64转换为int的这一步
func makeslice64(et *_type, len64, cap64 int64) slice {
    len := int(len64)
    if int64(len) != len64 {
        panic(errorString("makeslice: len out of range"))
    }

    cap := int(cap64)
    if int64(cap) != cap64 {
        panic(errorString("makeslice: cap out of range"))
    }

    return makeslice(et, len, cap)
}

  1. nil和空切片
    1)nil切片:var slice []int。nil切片被用在很多标准库和内置函数中,描述一个不存在的切片的时候,就会使用nil切片,比如函数发生异常的时候,返回的切片就是nil切片,nil切片的指针指向nil。
    2)空切片就用来表示一个空的集合。比如数据库查询,一条结果也没有查到,那么就可以返回一个空切片(如图二)。空切片和nil切片的区别在于,空切片的指向的地址不是nil,指向的是内存中的一个内存地址。当时他没有分配任何内存空间,即底层元素为0个元素。 3)最后需要说明的一点是。不管是使用 nil 切片还是空切片,对其调用内置函数 append,len 和 cap 的效果都是一样的。
2)
silce := make( []int , 0 )
slice := []int{ }

  1. 切片扩容
    当一个切片的容量满了的时候,就需要扩容。策略是什么?
func growslice(et *_type, old slice, cap int) slice {
    if raceenabled {
        callerpc := getcallerpc(unsafe.Pointer(&et))
        racereadrangepc(old.array, uintptr(old.len*int(et.size)), callerpc, funcPC(growslice))
    }
    if msanenabled {
        msanread(old.array, uintptr(old.len*int(et.size)))
    }

    if et.size == 0 {
        // 如果新要扩容的容量比原来的容量还要小,这代表要缩容了,那么可以直接报panic了。
        if cap < old.cap {
            panic(errorString("growslice: cap out of range"))
        }

        // 如果当前切片的大小为0,还调用了扩容方法,那么就新生成一个新的容量的切片返回。
        return slice{unsafe.Pointer(&zerobase), old.len, cap}
    }

  // 这里就是扩容的策略,为什么扩容的策略最后会对4取模
    newcap := old.cap
    doublecap := newcap + newcap
    if cap > doublecap {
        newcap = cap
    } else {
        if old.len < 1024 {
            newcap = doublecap
        } else {
            for newcap < cap {
                newcap += newcap / 4
            }
        }
    }

    // 计算新的切片的容量,长度。
    var lenmem, newlenmem, capmem uintptr
    const ptrSize = unsafe.Sizeof((*byte)(nil))
    switch et.size {
    case 1:
        lenmem = uintptr(old.len)
        newlenmem = uintptr(cap)
        capmem = roundupsize(uintptr(newcap))
        newcap = int(capmem)
    case ptrSize:
        lenmem = uintptr(old.len) * ptrSize
        newlenmem = uintptr(cap) * ptrSize
        capmem = roundupsize(uintptr(newcap) * ptrSize)
        newcap = int(capmem / ptrSize)
    default:
        lenmem = uintptr(old.len) * et.size
        newlenmem = uintptr(cap) * et.size
        capmem = roundupsize(uintptr(newcap) * et.size)
        newcap = int(capmem / et.size)
    }

    // 判断非法的值,保证容量是在增加,并且容量不超过最大容量
    if cap < old.cap || uintptr(newcap) > maxSliceCap(et.size) {
        panic(errorString("growslice: cap out of range"))
    }

    var p unsafe.Pointer
    if et.kind&kindNoPointers != 0 {
        // 在老的切片后面继续扩充容量
        p = mallocgc(capmem, nil, false)
        // 将 lenmem 这个多个 bytes 从 old.array地址 拷贝到 p 的地址处
        memmove(p, old.array, lenmem)
        // 先将 P 地址加上新的容量得到新切片容量的地址,然后将新切片容量地址后面的 capmem-newlenmem 个 bytes 这块内存初始化。为之后继续 append() 操作腾出空间。
        memclrNoHeapPointers(add(p, newlenmem), capmem-newlenmem)
    } else {
        // 重新申请新的数组给新切片
        // 重新申请 capmen 这个大的内存地址,并且初始化为0值
        p = mallocgc(capmem, et, true)
        if !writeBarrier.enabled {
            // 如果还不能打开写锁,那么只能把 lenmem 大小的 bytes 字节从 old.array 拷贝到 p 的地址处
            memmove(p, old.array, lenmem)
        } else {
            // 循环拷贝老的切片的值
            for i := uintptr(0); i < lenmem; i += et.size {
                typedmemmove(et, add(p, i), add(old.array, i))
            }
        }
    }
    // 返回最终新切片,容量更新为最新扩容之后的容量
    return slice{p, old.len, newcap}
}
  1. 切片拷贝 ...

二,中阶用法