美文网首页golang学习
golang学习--slice

golang学习--slice

作者: 玖零儛 | 来源:发表于2020-05-12 17:35 被阅读0次

    切片定义

    切片(Slice)是一个拥有相同类型元素的可变长度的序列。它是基于数组类型做的一层封装。它非常灵活,支持自动扩容。切片是一个引用类型,它的内部结构包含地址、长度和容量。切片一般用于快速地操作一块数据集合。

    数组与切片

    切片的数据实际是通过数组来保存的,每个切片都有三个信息:底层数组的指针、切片的长度(len)和切片的容量(cap)。
    举个栗子,底层数组a := [8]int{0, 1, 2, 3, 4, 5, 6, 7};

    • 切片s1 := a[:5],切片和数组对应关系:


      Go-slice-2020-05-12-15-23-13
    • 切片s2 := a[3:6],切片和数组对应关系:


      Go-slice-2020-05-12-15-44-43

    指向同一个底层数组的切片修改值

    切片是指向底层数组的引用类型,指向同一个底层数组的切片底层数据存放都是在同一个位置,修改某个切片会影响到在同一个范围的切片

    import (
        "fmt"
        "testing"
    )
    
    func TestSliceShareMemory(t *testing.T) {
        year := []string{"Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"}
        Q2 := year[3:6]
        t.Log(Q2, len(Q2), cap(Q2))
        summer := year[5:8]
        t.Log(summer, len(summer), cap(summer))
        summer[0] = "Unkonw"
        t.Log(Q2)
        t.Log(year)
    }
    // === RUN   TestSliceShareMemory
    //     TestSliceShareMemory: slice_test.go:36: [Apr May Jun] 3 9
    //     TestSliceShareMemory: slice_test.go:38: [Jun Jul Aug] 3 7
    //     TestSliceShareMemory: slice_test.go:40: [Apr May Unkonw]
    //     TestSliceShareMemory: slice_test.go:41: [Jan Feb Mar Apr May Unkonw Jul Aug Sep Oct Nov Dec]
    // --- PASS: TestSliceShareMemory (0.00s)
    // PASS
    

    切片表达式

    切片表达式从字符串、数组、指向数组或切片的指针构造子字符串或切片。它有两种变体:一种指定low和high两个索引界限值的简单的形式,另一种是除了low和high索引界限值外还指定容量的完整的形式:

    1. 切片len()是可访问长度,容量cap()是总空间大小。通过数组生成的切片, len为首尾索引之差,cap为从切片首索引到数组末尾长度
    2. 切片s[low:high:max],从切片s的low处到high处所获得的切片,len=high-low,cap=max-low
    func TestSliceExpression(t *testing.T) {
        a := [5]int{1, 2, 3, 4, 5}
        // b := a[1:3:7]
        b := a[1:3:5]
        fmt.Printf("b:%v len(b):%v cap(b):%v\n", b, len(b), cap(b))
    }
    // === RUN   TestSliceExpression
    // b:[2 3] len(b):2 cap(b):4
    // --- PASS: TestSliceExpression (0.00s)
    // PASS
    

    切片不能比较

    两个切片不能直接比较,会报错:

    func TestSliceCompare(t *testing.T) {
        a := []int{1, 2, 3, 4}
        b := []int{1, 2, 3, 4}
        if a == b {
            t.Log("a==b")
        }
    }
    // invalid operation: a == b (slice can only be compared to nil)
    // FAIL go_learn/go_test/slice_test [build failed]
    // FAIL
    

    切片的append

    切片通过append()添加元素时,未超过newcap时底层数组地址不变,超过的话底层数组会申请新的内存地址。新申请的容量大小计算分成了两步,有关append的源码在$GOROOT/src/runtime/slice.go,可以自己去分析。

    计算逻辑newcap

    1. new cap > old * 2直接申请新容量大小;
    2. 小于2倍时,len<1024翻倍,len>1024加上1/4
    //$GOROOT/src/runtime/slice.go
    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
        }
      }
    }
      
    //...略
    lenmem = uintptr(old.len)
    newlenmem = uintptr(cap)
    capmem = roundupsize(uintptr(newcap))
    overflow = uintptr(newcap) > maxAlloc
    newcap = int(capmem)
    //...略
    

    实际申请内存大小

    上面先算了个逻辑上的newcap,实际申请内存的时候,由于内存对齐的关系不会直接就用newcap。上面的代码就是在算好了newcap后会调用roundupsize()得到实际的大小。

    //$GOROOT/src/runtime/msize.go
    // Returns size of the memory block that mallocgc will allocate if you ask for the size.
    func roundupsize(size uintptr) uintptr {
        if size < _MaxSmallSize {
            if size <= smallSizeMax-8 {
                return uintptr(class_to_size[size_to_class8[(size+smallSizeDiv-1)/smallSizeDiv]])
            } else {
                return uintptr(class_to_size[size_to_class128[(size-smallSizeMax+largeSizeDiv-1)/largeSizeDiv]])
            }
        }
        if size+_PageSize < size {
            return size
        }
        return alignUp(size, _PageSize)
    }
    

    roundupsize()中的class_to_size、size_to_class8是存了具体大小的数组,根据传入的newcap来算出下标,拿到对应的大小值。这些数组在//$GOROOT/src/runtime/sizeclasses.go,这个文件又是//$GOROOT/src/runtime/mksizeclasses.go.go生成的,生成规则就先不去看了。

    //$GOROOT/src/runtime/sizeclasses.go
    // Code generated by mksizeclasses.go; DO NOT EDIT.
    //go:generate go run mksizeclasses.go
    var class_to_size = [_NumSizeClasses]uint16{0, 8, 16, 32, 48, 64, 80, 96, 112, 128 ...}
    
    var size_to_class8 = [smallSizeMax/smallSizeDiv + 1]uint8{0, 1, 2, 3, 3, 4, 4, 5, 5, 6, 6, 7, 7, 8, 8, 9, 9, 10, 10, 11, 11, 12, 12, 13, 13, 14, 14, 15, 15, 16, 16, 17, 17, 18, 18, 18, 18, 19, 19, 19, 19, 20, 20, 20, 20, 21, 21, 21, 21, 22, 22, 22, 22, 23, 23, 23, 23, 24, 24, 24, 24, 25 ...}
    

    了解了上面的内容之后就可以理解下面的几个例子了:

    func TestSliceAppend(t *testing.T) {
        var a = make([]int, 5, 10)
        for i := 0; i < 10; i++ {
            a = append(a, i)
            fmt.Printf("a ptr: %p\n", a)
        }
        fmt.Println(a)
    }
    // === RUN   TestSliceAppend
    // a ptr: 0xc00000c320
    // a ptr: 0xc00000c320
    // a ptr: 0xc00000c320
    // a ptr: 0xc00000c320
    // a ptr: 0xc00000c320
    // a ptr: 0xc0000100a0
    // a ptr: 0xc0000100a0
    // a ptr: 0xc0000100a0
    // a ptr: 0xc0000100a0
    // a ptr: 0xc0000100a0
    // [0 0 0 0 0 0 1 2 3 4 5 6 7 8 9]
    // --- PASS: TestSliceAppend (0.00s)
    // PASS
    
    func TestSliceAppend2(t *testing.T) {
        s := []int{1, 2, 3, 4}
        a := make([]int, 3, 6)
        b := append(a, 10)
        a[0] = 50
        fmt.Printf("a: %v\tptr: %p\tfirst: %v\n", a, a, a[0])
        fmt.Printf("b: %v\tptr: %p\tfirst: %v\n", b, b, b[0])
    
        b = append(a, s...)
        a[0] = 100
        fmt.Printf("a: %v\tptr: %p\tfirst: %v\n", a, a, a[0])
        fmt.Printf("b: %v\tptr: %p\tfirst: %v\n", b, b, b[0])
    }
    // === RUN   TestSliceAppend2
    // a: [50 0 0]  ptr: 0xc00000a330   first: 50
    // b: [50 0 0 10]   ptr: 0xc00000a330   first: 50
    // a: [100 0 0] ptr: 0xc00000a330   first: 100
    // b: [50 0 0 1 2 3 4]  ptr: 0xc00001a4e0   first: 50
    // --- PASS: TestSliceAppend2 (0.00s)
    // PASS
    
    func TestSliceAppend3(t *testing.T) {
        a1 := make([]int, 20)
        b1 := make([]int, 40)
        a1 = append(a1, b1...)
        fmt.Println(len(a1), cap(a1))
    
        a2 := make([]int, 20)
        b2 := make([]int, 42)
        a2 = append(a2, b2...)
        fmt.Println(len(a2), cap(a2))
    }
    // === RUN   TestSliceAppend3
    // 60 60
    // 62 64
    // --- PASS: TestSliceAppend3 (0.00s)
    // PASS
    

    切片元素删除

    在要删除的元素左右切两下:a1 = append(a1[:1], a1[2:]...),删除其实是将所删元素后面的往前挪。

    func TestSliceDelete(t *testing.T) {
        a := []int{30, 31, 32, 33, 34, 35, 36, 37}
        // 要删除索引为2的元素
        a = append(a[:2], a[3:]...)
        t.Log(a)
    }
    // === RUN   TestSliceDelete
    //     TestSliceDelete: slice_test.go:98: [30 31 33 34 35 36 37]
    // --- PASS: TestSliceDelete (0.00s)
    // PASS
    

    参考内容

    相关文章

      网友评论

        本文标题:golang学习--slice

        本文链接:https://www.haomeiwen.com/subject/okhinhtx.html