Golang中slice深入探究

Golang 中的 slice 是一个灵活且强大的数据结构,用于处理和操作一组连续的元素。它比数组更加灵活,适用于大多数需要动态数组的场景。

slice 是对数组的抽象封装,底层数据存储在一个数组中。slice 的切片操作不会复制底层数组的数据,而是创建一个新的 slice 结构,指向相同的底层数组。

基础知识

slice 是对数组的抽象封装,底层数据存储在一个数组中。slice 的切片操作不会复制底层数组的数据,而是创建一个新的 slice 结构,指向相同的底层数组。因此,多个 slice 可以共享相同的底层数据,修改其中一个 slice 可能会影响其他共享同一底层数组的数据。

和数组的区别

长度和容量

  1. 数组的长度是固定的,在声明时就需要指定。
  2. Slice 的长度是可变的,可以动态增加或减少。

初始化和声明

  1. 数组在声明时必须指定长度,可以在声明时初始化元素
  2. Slice 可以在声明时不指定长度,只需指定元素类型。可以使用内置的 make 函数来创建一个指定长度和容量的 slice。

底层实现

  1. 数组是一个固定长度的连续内存块,直接存储在栈或堆上。每次使用数组时都会复制整个数组。
  2. Slice 是对数组的一个抽象封装,包含一个指向底层数组的指针、长度和容量。多个 slice 可以共享同一个底层数组。

数据结构

1
2
3
4
5
type slice struct {
	array unsafe.Pointer
	len   int
	cap   int
}
  • array:指向数组内存空间的起始位置
  • len:切片的长度
  • cap:切片的容量,需要满足:cap >= len

初始化

声明不初始化

1
var s []int

此时切片的字面量是一个空指针nil。

make()

通过make来初始化slice的时候,有两种方式,一种是指定cap,一种是不指定cap。

不指定cap

1
s = make([]int, 10)

此时len=10,切cap=10。

指定cap

1
s = make([]int, 10, 20)

此时切片s的len为10,且cap为20。

在[len, cap) 之间范围内的内存空间虽然已经分配,但是直接访问会出现下标越界的panic。

但是[0, len) 之间范围内的元素是可以直接访问到的,没有初始化的位置为0值。

赋值初始化

1
s := []int {1, 2, 3}

此时切片是的len=3,cap=3

源码

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
// makeslice 创建一个新的切片,并分配指定的长度和容量。
// 参数:
// - et: *_type 指向切片元素类型的指针。
// - len: int 指定切片的长度。
// - cap: int 指定切片的容量。
// 返回值:返回一个指向分配内存的指针,类型为 unsafe.Pointer。
func makeslice(et *_type, len, cap int) unsafe.Pointer {
    // 计算所需的内存大小 mem,即 et.size * cap。
    // 如果乘法溢出或者所需内存大于最大可分配内存,则返回 overflow 为 true。
	mem, overflow := math.MulUintptr(et.size, uintptr(cap))
    
    // 检查溢出、内存超限和长度是否合法。如果满足任一条件,则处理错误。
	if overflow || mem > maxAlloc || len < 0 || len > cap {
        // NOTE: 当用户使用 make([]T, bignumber) 时,产生 'len out of range' 错误而不是 'cap out of range' 错误。
        // 'cap out of range' 也是正确的,但由于容量是隐含提供的,说长度更清楚。
        // 详情见 golang.org/issue/4085。

        // 重新计算内存大小 mem,此时为 et.size * len。
        // 如果乘法溢出或者所需内存大于最大可分配内存,或者长度为负,则抛出长度越界的错误。
		mem, overflow := math.MulUintptr(et.size, uintptr(len))
		if overflow || mem > maxAlloc || len < 0 {
			panicmakeslicelen()
		}
        // 如果上面检查通过,但长度大于容量,则抛出容量越界的错误。
		panicmakeslicecap()
	}

    // 分配内存,并返回指向该内存的指针。第三个参数 true 表示内存需要初始化。
	return mallocgc(mem, et, true)
}

截取

形如:

1
2
3
4
s[1:2]
s[:4]
s[0:]
s[:]

s[a:b]:可以看成是截取从a到b-1这个下标的元素。即:[a, b) 这样一个左闭右开的区间。

在对切片进行截取操作的时候,底层复用的都是同一块内存区域。新截取出来的切片会创建一个新的slice header


append()

append操作会再slice最后的位置新增一个元素,这里的末尾指的是len这个长度,而不是cap

1
s = append(s, 1)

扩容

扩容的主要代码位于:runtime/slice.go

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
// growslice 扩展一个切片的容量,并返回新的切片。
// 参数:
// - et: *_type 指向切片元素类型的指针。
// - old: slice 旧的切片。
// - cap: int 新的容量。
// 返回值:返回新的切片。
func growslice(et *_type, old slice, cap int) slice {
    // ...

	// 如果新的容量小于旧的容量,抛出错误。
	if cap < old.cap {
		panic(errorString("growslice: cap out of range"))
	}

	// 如果元素大小为 0,返回一个新的切片,其指向 zerobase 且长度为 old.len,容量为 cap。
	if et.size == 0 {
		return slice{unsafe.Pointer(&zerobase), old.len, cap}
	}

	// 初始化新的容量 newcap 为旧容量。
	newcap := old.cap
	// 计算双倍容量 doublecap。
	doublecap := newcap + newcap
	if cap > doublecap {
		// 如果预期的新容量大于双倍容量,则直接使用新容量。
		newcap = cap
	} else {
		const threshold = 256
		if old.cap < threshold {
			// 如果旧容量小于阈值,直接使用双倍容量。
			newcap = doublecap
		} else {
			// 对于大于阈值的情况,逐步增加容量,过渡到增长 1.25 倍。 
			for 0 < newcap && newcap < cap {
				newcap += (newcap + 3*threshold) / 4
			}
			// 如果计算的新容量溢出,则直接使用指定的新容量。
			if newcap <= 0 {
				newcap = cap
			}
		}
	}

	var overflow bool
	var lenmem, newlenmem, capmem uintptr
	// 针对常见的元素大小进行优化。
	switch {
	case et.size == 1:
        // 数组元素的大小为1,则新容量的大小为1 * newCap
		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):
        // 元素的大小为2的指数,则通过位运算来计算空间的大小
		var shift uintptr
		if goarch.PtrSize == 8 {
			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)
	}

	// 检查内存溢出情况。
	if overflow || capmem > maxAlloc {
		panic(errorString("growslice: cap out of range"))
	}

	var p unsafe.Pointer
	if et.ptrdata == 0 {
		// 分配未初始化的内存。
		p = mallocgc(capmem, nil, false)
		// 清除未被覆盖的部分。
		memclrNoHeapPointers(add(p, newlenmem), capmem-newlenmem)
	} else {
		// 分配已初始化的内存。
		p = mallocgc(capmem, et, true)
		if lenmem > 0 && writeBarrier.enabled {
			// 进行写屏障处理。
			bulkBarrierPreWriteSrcOnly(uintptr(p), uintptr(old.array), lenmem-et.size+et.ptrdata)
		}
	}
	// 将旧数据复制到新分配的内存中。
	memmove(p, old.array, lenmem)

	// 返回新的切片,指向新分配的内存。
	return slice{p, old.len, newcap}
}

删除

加入我们有个切片s定义如下:

1
s := []int{1, 2, 3, 4, 5, 6}

删除首个

1
s = s[1:]

删除末尾

1
s = s[:len(s)-1]

删除中间

1
s = append(s[:2], s[3:]...)

这样就删除了下标为2的元素。len=5,cap=6

清空

1
s = s[:0]

这样操作以后len=0,cap=4


复制

  1. 直接使用赋值语句,只会新建一个slice header,底层的数组还是指向着同一片内存区域。
  2. 使用copy()方法则可以拷贝出一个独立于原来切片的新的切片对象。

相关内容

0%