跳至主要內容

Slice

王泽权大约 5 分钟GoGo

在上一篇中我们简单的了解了 slice ,在这一篇中我们将继续深入了解一下。

Slice 数据结构

首先我们先来看一下 Slice 的数据结构,从 code - 1** **中我们可以看到 Slice 是由 3 部分构成,首先第一是名为 unsafe.Pointer(通用指针类型) 类型的 array,从命名中我们可以得知它是一个指向 Array 的指针,其余两个皆为 int 类型分别为 len 和 cap 分别代表 Slice 当前的长度,另一个代表 Slice 当前的容量。

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

  • array unsafe.Pointer 类型,用于指向不同类型的 Array。
  • len int 类型,用于表示当前 Slice 的长度,即当前元素的个数。
  • cap int 类型,用于表示当前 Slice 的容量,即当前指针指向底层数组元素为起始位向后的总元素数量。

Example

通过刚才数据结构 code - 1 中,想必我们已经对 Slice 有了一个更清楚的认知,Slice 本身并不是动态 Array,它更像是对 Array 的又一层封装,通过它内部实现的数据结构对底层 Array 的引用并将数据读写限定在指定区域内,其 Slice 本身只是一个只读对象。 既然 Slice 只是对底层 Array 的一个引用,那么通过 code - 2 中我们可以发现,一个 Array 可以被多个 slice 所引用,并且修改 Slice 上的元素其本质上也就是在修改所引用的底层 Array 中的元素,在此我们可以发现 Slice 是一个引用类型。

package main

import (
    "fmt"
)

func revise(s []int, i int, v int) {
    s[i] = v
}

func main() {
    arrayA := [10]int{ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 }
    fmt.Printf("arrayA ---> %d\n", arrayA)
    fmt.Printf("address(arrayA) = %p\n", &arrayA)
    fmt.Printf("len(arrayA) = %d cap(arrayA) = %d\n", len(arrayA), cap(arrayA))
    fmt.Printf("\n")
    sliceA := arrayA[4:8]
    fmt.Printf("sliceA ---> %d\n", sliceA)
    fmt.Printf("address(sliceA) = %p\n", &sliceA)
    fmt.Printf("len(sliceA) = %d cap(sliceA) = %d\n", len(sliceA), cap(sliceA))
    fmt.Printf("\n")
    sliceB := arrayA[2:6]
    fmt.Printf("sliceB ---> %d\n", sliceB)
    fmt.Printf("address(sliceB) = %p\n", &sliceB)
    fmt.Printf("len(sliceB) = %d cap(sliceB) = %d\n", len(sliceB), cap(sliceB))
    revise(sliceB, 3, 0)
    fmt.Printf("\n")
    fmt.Printf("sliceA ---> %d\n", sliceA)
    fmt.Printf("address(sliceA) = %p\n", &sliceA)
    fmt.Printf("len(sliceA) = %d cap(sliceA) = %d\n", len(sliceA), cap(sliceA))
    fmt.Printf("\n")
    fmt.Printf("sliceB ---> %d\n", sliceB)
    fmt.Printf("address(sliceB) = %p\n", &sliceB)
    fmt.Printf("len(sliceB) = %d cap(sliceB) = %d\n", len(sliceB), cap(sliceB))
}

// arrayA ---> [0 1 2 3 4 5 6 7 8 9]
// address(arrayA) = 0xc000072000
// len(arrayA) = 10 cap(arrayA) = 10

// sliceA ---> [4 5 6 7]
// address(sliceA) = 0xc000010030
// len(sliceA) = 4 cap(sliceA) = 6

// sliceB ---> [2 3 4 5]
// address(sliceB) = 0xc000010060
// len(sliceB) = 4 cap(sliceB) = 8

// sliceA ---> [4 0 6 7]
// address(sliceA) = 0xc000010030
// len(sliceA) = 4 cap(sliceA) = 6

// sliceB ---> [2 3 4 0]
// address(sliceB) = 0xc000010060
// len(sliceB) = 4 cap(sliceB) = 8


Make

Slice 的声明方式有很多,但都是字面量上的不同,下面我们从源代码 code - 3 中看看 make 函数的是如何做的,在此期间我们先来关注一下 len 与 cap 之间的关系,在 L3 中我们看到如果 len > cap 会直接报出 Panic,为此我们可以推断出 len 与 cap 之间的关系始终保持在 0 < len <= cap 。

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

  • 0 < len <= cap
  • type.size 与 cap 的乘积不能越界,并且要小于 maxAlloc(允许用户分配的最大虚拟内存空间)
  • type.size 与 len 的乘积不能越界,并且要小于 maxAlloc。

Append

Append 函数是向 Slice 尾部添加元素,返回新的 slice 对象,但如果此时 Slice 的 cap 值小于当前 len 加上 append 中传入值的数量,就会调用 runtime.growslice 函数进行扩容,下面就让我们来深入了解一下函数 runtime.growslice 是如何对 Slice 进行扩容的。 首先我们先来看看扩容规则,其中 et 代表 slice 中的元素类型,old 是扩容前的切片,cap 是期望容量,也就是扩容后至少需要的最小容量,即 old.cap + 本次新增的元素数量。

func growslice(et *_type, old slice, cap int) slice {
    ...
	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
			}
		}
	}
    ...
}

  • 当前期望容量 > 2 倍扩容前 Slice 容量 。
    • 新容量 = 当前期望容量。
  • 当前期望容量 < 2 倍扩容前 Slice 容量 && 扩容前 Slice 容量 > 256。
    • 新容量 = 2 倍扩容前 Slice 容量。
  • 当前期望容量 < 2 倍扩容前 Slice 容量 && 扩容前 Slice 容量 < 256。
    • 新容量会从 2 倍扩容前 Slice 容量平滑的过渡到 1.25 倍扩容前 Slice 容量。

其次就是内存字节对齐,通过刚刚的扩容规则中 code - 4 中我们已经大致了解了各种情况中所要扩容的大小,但那并不是最终所要扩容的容量还会进行接下来的计算。

func growslice(et *_type, old slice, cap int) slice {
    ...
	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)
	}
    ...
}

上次编辑于:
贡献者: ZEQUANR