(二)数组和切片

一、数组

初始化

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. 传递开销:数组传递时会复制整个数组;切片仅复制指针、长度和容量
相关推荐
AI茶水间管理员1 天前
如何让LLM稳定输出 JSON 格式结果?
前端·人工智能·后端
其实是白羊1 天前
我用 Vibe Coding 搓了一个 IDEA 插件,复制URI 再也不用手动拼了
后端·intellij idea
用户8356290780511 天前
Python 操作 Word 文档节与页面设置
后端·python
酒後少女的夢1 天前
设计模式教程
后端·架构
凌览1 天前
别再手搓 Skill 了,用这个工具 5 分钟搞定
前端·后端
weixin_408099671 天前
python请求文字识别ocr api
开发语言·人工智能·后端·python·ocr·api·ocr文字识别
weixin_408099671 天前
【组合实战】OCR + 图片去水印 API:自动清洗图片再识别文字(完整方案 + 代码示例)
图像处理·后端·ocr·api·文字识别·去水印·ocr识别优化
gelald1 天前
SpringBoot - Actuator与监控
java·spring boot·后端
用户585343788431 天前
AI Harness Engineering:从概念、场景到落地方法
人工智能·后端