GO-Slice

150 阅读5分钟

slice 类型

slice表示切片(分片),例如对一个数组进行切片,取出数组中的一部分值slice依赖于数组 slice自身维护了一个指针属性,指向它底层数组的某些元素的集合

slice 存储原理

slice代表变长的序列,它的底层是数组。一个切片由3部分组成:指针、长度和容量。指针指向底层数组,长度代表slice当前的长度,容量代表底层数组的长度。

    可以在runtime中的slice中查看slice的实现原理
    type slice struct {
        array unsafe.Pointer
        len   int
        cap   int
    }

slice 类型创建

1. make 创建

`slice := make([]T, 5) 第二个参数定义长度 若是不传长度则与cap相同`

2. 通过字面量创建

`slice := []int{1, 2, 3, 4} 创建长度与容量都为4的切片`

3. 通过切片创建

`slice[i:j:k] i 起始位置 j 切片长度 k 切片容量 简写 slice[i:] slice[:j] slice[:]`

slice 常用函数

1. copy

copy() 将一个slice拷贝到另一个slice

2. append

append() 追加元素 如果切片的容量已经不能容纳将要追加的数据,就会创建一个新的扩容后的底层数组,将之前的数据拷贝过去后,再执行扩容操作

slice 常见问题

slice 传参

函数的参数都是按值传递的,因此在调用函数时,会将参数的副本传递给函数。 在传递slice时 虽然传递的是副本 但是副本同样指向了源slice的底层数组 所以在函数内部修改slice,有可能会影响到底层数组,进而影响到其他slice 但是如果传参后切片长度发生变化,切片会自动扩容为新的地址,就无法影响到其他slice 如果想实现影响其他slice 可通过函数返回 或 指针实现

1. 不改变slice容量

//测试Slice
/*
    传参前内容 地址 [1 2], addr is 0x1400011e018, ptr is 0x1400012c010
    传参的内容 地址 [3 2], addr is 0x1400011e060, ptr is 0x1400012c010
    传参后内容 地址 [3 2], addr is 0x1400011e018, ptr is 0x1400012c010
*/
func TestSlice() {
    slice := []int{1, 2}

    fmt.Printf("传参前内容 地址 %v, addr is %p, ptr is %p\n", slice, &slice, slice)

    TestSliceChange(slice)

    fmt.Printf("传参后内容 地址 %v, addr is %p, ptr is %p\n", slice, &slice, slice)

    return
}

func TestSliceChange(slice []int) {
    slice[0] = 3
    // [3 2] 0xc00000e090 0xc00001a2d0
    fmt.Printf("传参的内容 地址 %v, addr is %p, ptr is %p\n", slice, &slice, slice)
}

2. 改变slice容量

/*
    传参前内容 地址 [1 2], addr is 0x1400011e018, ptr is 0x1400012c010
    传参的内容 地址 [1 2 3], addr is 0x1400011e060, ptr is 0x14000134000
    传参后内容 地址 [1 2], addr is 0x1400011e018, ptr is 0x1400012c010
    传参后再修改内容 地址 [4 2], addr is 0x1400011e018, ptr is 0x1400012c010
*/
func TestSlice() {
    slice := []int{1, 2}

    fmt.Printf("传参前内容 地址 %v, addr is %p, ptr is %p\n", slice, &slice, slice)

    TestSliceChange(slice)

    fmt.Printf("传参后内容 地址 %v, addr is %p, ptr is %p\n", slice, &slice, slice)

    slice[0] = 4

    fmt.Printf("传参后再修改内容 地址 %v, addr is %p, ptr is %p\n", slice, &slice, slice)

    return
}

func TestSliceChange(slice []int) {
    //传参
    // slice[0] = 3
    //使用函数append
    slice = append(slice, 3)

    fmt.Printf("传参的内容 地址 %v, addr is %p, ptr is %p\n", slice, &slice, slice)
}

slice nil 和 []

// 不同slice声明
    var c []int
    d := []int{}
    e := make([]int, 0)

    jc, _ := json.Marshal(c) // null
    jd, _ := json.Marshal(d) // []
    je, _ := json.Marshal(e) // []
// 使用 []int{} 或 make([]int,0) 声明的slice 不为nil 

slice 和 array 的区别

//slice 可以不指定长度 array 必须指定长度
    slice: 
        var a []int
        a := make([]int,len,cap)

    array: 
        var b [lenght]int
//slice 长度可以动态扩缩容 array 长度是编译时静态计算的
    func() {
        var s []int
        // s := make([]int, 100)

        fmt.Println(len(s))
        fmt.Println(cap(s))
    }()

    func() {
        var a [100]int

        fmt.Println(len(a))
        fmt.Println(cap(a))
    }()

    run 输出
    0
    0
    100
    100
// benchmake slice array go1.18

    func TestCallSlice(s []int) {

    }

    func TestCallArray(a [10000]int) {

    }

    //go:noinline
    func BenchmarkTestCallSlice(b *testing.B) {
        s := make([]int, 10000)
        for n := 0; n < b.N; n++ {
            TestCallSlice(s)
        }
    }

    //go:noinline
    func BenchmarkTestCallArray(b *testing.B) {
        var a [10000]int
        for n := 0; n < b.N; n++ {
            TestCallArray(a)
        }
    }

    goos: darwin
    goarch: arm64
    pkg: test
    BenchmarkTestCallSlice
    BenchmarkTestCallSlice-8        1000000000               0.3501 ns/op          0 B/op          0 allocs/op
    BenchmarkTestCallArray
    BenchmarkTestCallArray-8        1000000000               0.3494 ns/op          0 B/op          0 allocs/op
    PASS
    ok      test    0.848s    

slice 扩缩容

/*
    声明 []int , len is 0, cap is 0
    append 1 , len is 1, cap is 1
    append 2 , len is 2, cap is 2
    append 3 , len is 3, cap is 4
    append 4 , len is 4, cap is 4
    ...
    append 254, len is 256, cap is 256
    append 511, len is 512, cap is 512
    append 511, len is 513, cap is 848
    append 846, len is 848, cap is 848
    append 847, len is 849, cap is 1280
*/
    func TestSliceLen() {
        var a []int

        fmt.Printf("声明 []int , len is %v, cap is %v\n", len(a), cap(a))

        a = append(a, 0)

        fmt.Printf("append 1 , len is %v, cap is %v\n", len(a), cap(a))

        a = append(a, 1)

        fmt.Printf("append 2 , len is %v, cap is %v\n", len(a), cap(a))

        a = append(a, 2)

        fmt.Printf("append 3 , len is %v, cap is %v\n", len(a), cap(a))

        a = append(a, 3)

        fmt.Printf("append 4 , len is %v, cap is %v\n", len(a), cap(a))

        a = append(a, 4)

        fmt.Printf("append 5 , len is %v, cap is %v\n", len(a), cap(a))

        a = append(a, 5)

        fmt.Printf("append 6 , len is %v, cap is %v\n", len(a), cap(a))

        for i := 5; i < 1025; i++ {
            a = append(a, i)
            fmt.Printf("append %v, len is %v, cap is %v\n", i, len(a), cap(a))
        }
    }



    // 由此可见slice 每次扩容都是成倍扩容 但是在达到一定阀值后 就以1.25倍扩容后判断et.size 等于 1 不做任何操作 goarch.PtrSize 编译器将除法/乘法优化为一个常量的移位。 对于2的幂,使用可变移位。
    // /src/runtime/slice.go growslice
    newcap := old.cap
	doublecap := newcap + newcap
	if cap > doublecap {
		newcap = cap
	} else {
		const threshold = 256
		if old.cap < threshold {
			newcap = doublecap
		} else {
			// Check 0 < newcap to detect overflow
			// and prevent an infinite loop.
			for 0 < newcap && newcap < cap {
				// Transition from growing 2x for small slices
				// to growing 1.25x for large slices. This formula
				// gives a smooth-ish transition between the two.
				newcap += (newcap + 3*threshold) / 4
			}
			// Set newcap to the requested cap when
			// the newcap calculation overflowed.
			if newcap <= 0 {
				newcap = cap
			}
		}
	}

	var overflow bool
	var lenmem, newlenmem, capmem uintptr
	// Specialize for common values of et.size.
	// For 1 we don't need any division/multiplication.
	// For goarch.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:
		lenmem = uintptr(old.len)
		newlenmem = uintptr(cap)
		capmem = roundupsize(uintptr(newcap))
		overflow = uintptr(newcap) > maxAlloc
		newcap = int(capmem)
	case et.size == goarch.PtrSize:
		lenmem = uintptr(old.len) * goarch.PtrSize
		newlenmem = uintptr(cap) * goarch.PtrSize
		capmem = roundupsize(uintptr(newcap) * goarch.PtrSize)
		overflow = uintptr(newcap) > maxAlloc/goarch.PtrSize
		newcap = int(capmem / goarch.PtrSize)
	case isPowerOfTwo(et.size):
		var shift uintptr
		if goarch.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)
	}


    // 但是append批量赋值的时候 go 会对其进行内存优化 例: 
    append(s,1,2,3) len 3 cap 3

    // 预先分配内存可以提示性能 index赋值要比append赋值性能好
    func BenchmarkAppend(b *testing.B) {
        b.ReportAllocs()
        for i := 0; i < b.N; i++ {
            var a []int
            for j := 0; j < 10000; j++ {
                a = append(a, j)
            }
        }
    }
    BenchmarkAppend-8          46833             25123 ns/op          357626 B/op         19 allocs/op

    func BenchmarkAppendStaticCap(b *testing.B) {
        b.ReportAllocs()
        for i := 0; i < b.N; i++ {
            a := make([]int, 0, 10000)
            for j := 0; j < 10000; j++ {
                a = append(a, j)
            }
        }
    }
    BenchmarkAppendStaticCap-8        150697              7362 ns/op           81920 B/op          1 allocs/op

    func BenchmarkAppendIndex(b *testing.B) {
        b.ReportAllocs()
        for i := 0; i < b.N; i++ {
            a := make([]int, 10000)
            for j := 0; j < 10000; j++ {
                a[j] = j
            }
        }
    }
    BenchmarkAppendIndex-8            165469              6781 ns/op           81920 B/op          1 allocs/op

slice bce优化 边界检查消除

    func TestSliceBce(a []int) {
        _ = a[3] //检查边界

        s := 0
        s += a[0] //边界检查消除
        s += a[1] //边界检查消除
        s += a[2] //边界检查消除
        s += a[3] //边界检查消除

        _ = a[index:] // 需要边界检查
        _ = a[:index] // 边界检查消除

        // print(s)
    }