(二)数组和切片

一、数组

初始化

go 复制代码
var a [10000]int
a1 := [3]int{1, 2, 3}
a2 := [...]int{1, 2, 3}

第二、第三在运行期间得到的结果是完全相同的,后一种声明方式在编译期间就会被转换成前一种

二、切片

初始化

  1. 通过下标的方式获得数组或者切片的一部分;

    1. 最原始也最接近汇编语言的方式
    2. 如果是切片的一部分,那么指向的是同一个地方
  2. 使用字面量初始化新的切片;

    1. 编译期间会在静态区创建数组,创建数组指针,赋值
    2. 通过[:]获取切片
  3. 使用关键字 make 创建切片:

    1. 需要运行时的参与
    2. 必须传大小,可选容量
    3. 小的在栈,大的或发生逃逸的在堆
go 复制代码
arr[0:3] or slice[0:3]
slice := []int{1, 2, 3}
slice := make([]int, 10)

扩容

go 复制代码
func growslice(oldPtr unsafe.Pointer, newLen, oldCap, num int, et *_type) slice {
	...
	newcap := nextslicecap(newLen, oldCap)
	...
}

在较新版本的 Go(1.18+)中,扩容策略已经不再是简单的"1024 字节以下翻倍,以上 1.25 倍"。而是:

  1. 切片新长度大于当前容量的两倍,会使用新长度
  2. 如果旧容量小于 256,则新容量翻倍。
  3. 否则平滑过渡到1.25倍逐渐扩容,直到大于等于新长度
go 复制代码
// nextslicecap computes the next appropriate slice length.
func nextslicecap(newLen, oldCap int) int {
	newcap := oldCap
	doublecap := newcap + newcap
	if newLen > doublecap {
		return newLen
	}

	const threshold = 256
	if oldCap < threshold {
		return doublecap
	}
	for {
		// 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) >> 2

		// We need to check `newcap >= newLen` and whether `newcap` overflowed.
		// newLen is guaranteed to be larger than zero, hence
		// when newcap overflows then `uint(newcap) > uint(newLen)`.
		// This allows to check for both with the same comparison.
		if uint(newcap) >= uint(newLen) {
			break
		}
	}

	// Set newcap to the requested cap when
	// the newcap calculation overflowed.
	if newcap <= 0 {
		return newLen
	}
	return newcap
}

然而这只是大致容量,下面还需进行内存对齐,为什么?

  1. 内存管理器的限制 : Go 的内存管理器并不是你想要多少字节就正好给你分配多少。为了减少内存碎片,它预先定义了一系列标准内存块大小,比如 8, 16, 32, 48 ... 32768 字节等。
  2. 避免浪费 : 如果你计算出来扩容后需要 40 字节,但内存管理器最小只能给你 48 字节的块。与其白白浪费那 8 字节,不如直接把切片的容量(cap)提升到 48 字节所能容纳的数量
go 复制代码
func growslice(oldPtr unsafe.Pointer, newLen, oldCap, num int, et *_type) slice {
	...
	newcap := nextslicecap(newLen, oldCap)

	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.
	noscan := !et.Pointers()
	switch {
		// 具体逻辑
	}
}

最后收尾工作:看看有没有溢出;申请新内存;数据迁移。

go 复制代码
func growslice(oldPtr unsafe.Pointer, newLen, oldCap, num int, et *_type) slice {
	...
	newcap := nextslicecap(newLen, oldCap)

	var overflow bool
	var lenmem, newlenmem, capmem uintptr
	noscan := !et.Pointers()
	switch {
		...
	}
	
	// 溢出
	if overflow || capmem > maxAlloc {
		panic(errorString("growslice: len out of range"))
	}

	// 申请新内存
	var p unsafe.Pointer
	if !et.Pointers() {
		// 如果你是一串数字 ([]int):扩容时,Go 只是简单地申请一块内存,把数字拷贝过去。
		// GC 扫描时直接跳过这块区域
		p = mallocgc(capmem, nil, false)
		memclrNoHeapPointers(add(p, newlenmem), capmem-newlenmem)
	} else {
		// 如果你是一串地址 ([]*User):扩容时,Go 必须非常小心。
		// 在拷贝过程中,如果 GC 正在工作,它必须确保每个地址都被正确标记,
		// 否则搬家搬到一半,地址指向的对象被 GC 回收了,程序就崩溃了
		p = mallocgc(capmem, et, true)
		if lenmem > 0 && writeBarrier.enabled {
			bulkBarrierPreWriteSrcOnly(uintptr(p), uintptr(oldPtr), lenmem-et.Size_+et.PtrBytes, et)
		}
	}
	// 数据迁移
	memmove(p, oldPtr, lenmem)

	return slice{p, newLen, newcap}
}

三、切片和数组的区别?

  1. 长度:数组长度固定;长度可变,支持自动扩容
  2. 声明方式不同
  3. 内存分配 :数组值类型;切片引用类型,底层引用数组
  4. 传递开销:数组传递时会复制整个数组;切片仅复制指针、长度和容量
相关推荐
Java不加班2 小时前
Nginx 核心实战指南:反向代理、负载均衡与动静分离
后端
子玖2 小时前
微信扫码注册登录-基于网站应用
后端·微信·go
Assby2 小时前
Java速通Go基础内容
后端
心在飞扬2 小时前
LangGraph 基础知识
前端·后端
Java编程爱好者2 小时前
MyBatis-mybatis入门与增删改查
后端
神奇小汤圆2 小时前
并发编程进阶:volatile、内存屏障与 CPU 缓存机制详解
后端
神奇小汤圆3 小时前
Redis实现 IP 维度滑动窗口限流实践
后端
程序员清风3 小时前
小红书二面:Spring Boot的单例模式是如何实现的?
java·后端·面试
树獭叔叔3 小时前
19-为什么AI工程这么喜欢"创造名词":从Prompt到Skill的造词运动
后端·aigc·openai