当前位置: 首页 > article >正文

GO切片slice详细解析

Slice数据结构


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

创建slice的方式

数组方式创建

ain  
  
import "fmt"  
  
func main() {  
    a := []int{1, 2, 3, 4, 5, 6, 7, 8, 9}  
    fmt.Println(len(a), cap(a))  
}

依靠数组创建的切片,容量是与底层数组相关。其源码如下:

func newobject(typ *_type) unsafe.Pointer{
	return mallocgc(typ.Size_,typ,true)
}

这是Go语言内置new关键字的底层实现,用于在堆上分配指定类型的内存空间。其核心职责包括:

  • 类型感知的内存分配
  • 与GC系统的交互
  • 内存对齐保证
  • 逃逸分析的基础支持

参数解析

参数类型作用说明内存管理关联
typ*_typeGo类型系统的类型描述符包含类型大小、GC掩码等信息
Size_uintptr类型的内存占用量直接决定分配空间大小
第三个参数bool是否需要内存清零(本例为true)影响分配性能特征
注意
  • 内存分配性能指标:
    • 每秒分配次数(allocs/op)
    • 每次分配耗时(ns/op)
    • 堆外分配比例

new

package main  
  
import "fmt"  
  
func main() {  
    c := *new([]int)  
    //c 是一个指向切片的指针,类型是 *[]int,而 len(c) 或 cap(c) 只能作用于 []int 类型本身。  
    //如果你想要获取切片的长度或容量,需要先解引用指针。  
    fmt.Println(len(c), cap(c))  
}

切片创建


package main  
  
import "fmt"  
  
func main() {  
    a := []int{1, 2, 3}  
    d := a[1:2:3]  
    fmt.Println(d, len(d), cap(d))  
}

//如果是通过`array[low:high]`方式创建的,cap为从low到len(array)的值。也即cap = len(array) - low。
//如果指定了max,cap = max - low,所以max不允许大于len(array)

make


package main  
  
import "fmt"  
  
func main() {  
    d := make([]int, 5, 5)  
    fmt.Println(len(d), cap(d))  
}

make方式通过makeslice方法来创建切片。在看makeslice源码前首先要了解math.MulUintptr这个函数,其作用是计算slice切片需要的内存空间。

// MulUintptr第一行代码的判断条件可以简化成  a|b < 1 << 4 * 8 || a == 0,也即 a|b < 2^32|| a== 0 
// 其中a == 0的情况,a * b 一定不溢出
// a|b < 2^32的情况下,代表a或者b都小于2^32,也就是说a * b 一定小于 2^64-1,也不会溢出
// 现在翻译一下MulUintptr的逻辑:
// ① 如果a、b乘积不溢出或者a == 0,直接返回乘积a * b和false
// ② 其它情况, overflow = a * b > MaxUintptr(uintptr最大值),返回a * b和true
// MulUintptr returns a * b and whether the multiplication overflowed.
// On supported platforms this is an intrinsic lowered by the compiler.
func MulUintptr(a, b uintptr) (uintptr, bool) {
	if a|b < 1<<(4*goarch.PtrSize) || a == 0 {
		return a * b, false
	}
	overflow := b > MaxUintptr/a
	return a * b, overflow
}

const MaxUintptr = ^uintptr(0)
// uintptr(0):这将无符号整数 0 转换为 uintptr 类型。uintptr 是一个无符号整数类型,其大小等于指针的大小,通常是 32 位或 64 位。
// ^uintptr(0):这是对无符号整数 0 进行按位取反操作,得到所有位都为 1 的值。在 64 位系统上,结果为 0xffffffffffffffff,在 32 位系统上,结果为 0xffffffff。
// ^uintptr(0) >> 63:这是将上一步得到的结果右移 63 位,结果是 64 位系统下为 1(0x1),32 位系统下为 0(0x0)。
// 4 << (^uintptr(0) >> 63):这是将上一步得到的结果作为位移量,对数字 4 进行左移运算。如果在 64 位系统上,结果是 4 << 1,即 8;如果在 32 位系统上,结果是 4 << 0,即 4。
const PtrSize = 4 << (^uintptr(0) >> 63)

  1. 计算内存需求

    • 通过 元素大小(elemSize)* 容量(cap) 计算切片底层数组所需内存空间。
    • 关键检查:确保乘积结果不溢出 uintptr 范围(如 64 位系统下最大为 2^64-1),防止数值异常。
  2. 合法性校验

    • 内存溢出:若 elemSize * cap 超出 uintptr 范围,触发 panic。
    • 内存上限:若计算结果超过系统单次分配最大内存 maxAlloc(如 64 位系统通常为 1<<48),触发 panic。
    • 逻辑错误:若 len < 0 或 len > cap(如 len 为负数或容量不足),触发 panic。
  3. 内存分配

    • 通过 mallocgc 函数在堆上申请连续内存空间,作为切片的底层数组。
    • mallocgc 是 Go 运行时内存分配的核心函数,负责管理垃圾回收(GC)元数据,确保内存安全回收。

关键点解析:

步骤作用技术细节
内存计算确定底层数组所需物理内存大小依赖平台指针大小(goarch.PtrSize),64 位系统为 8 字节,计算时需兼容所有架构。
溢出检查防止非法内存请求导致未定义行为通过 overflow 标志位判断乘法溢出,确保内存计算值合法。
系统限制遵循操作系统内存分配规则maxAlloc 由运行时根据系统内存布局动态计算(如虚拟地址空间限制)。
逻辑校验确保业务逻辑的合理性(如 len 非负且不超过 cap避免运行时越界访问,属于开发者责任,编译器静态检查无法完全覆盖。
内存分配为切片提供底层存储空间mallocgc 根据内存大小选择分配策略(小对象用 mcache,大对象直接 mheap),并标记 GC 信息。

扩容切片

规则
1.当cap>=len+num,直接对相应的数组进行操作
2. 当cap< len+num 则需要扩容

扩容算法
1. 旧版 *2 1024 *1.25
2. 新版 *2 256 new = old + ( 3 * 256 + old ) / 4

但整体的思路始终是在减少扩容次数的同时,最大限度的避免浪费内存空间

内存对齐

Go 语言中的 内存对齐(memory alignment)是指编译器如何将数据结构的字段放置在内存中的规则。内存对齐的目的是为了提高访问效率和避免潜在的性能损失。不同类型的变量(如 int, float, struct)在内存中可能会按照不同的对齐方式进行排列。

为什么需要内存对齐?

  1. 性能优化: 许多 CPU 对不同数据类型的访问有特定的要求。例如,在许多架构上,读取 4 字节的数据(如 int32)要求其地址必须是 4 的倍数。如果数据未对齐,CPU 可能需要额外的周期来处理访问,这样会降低性能。

  2. 硬件要求: 一些架构(如 ARM 和 x86)要求数据在特定边界对齐,否则可能会导致硬件异常或效率低下。

Go 中的内存对齐

1. 对齐方式

在 Go 中,对齐方式通常与数据类型的大小有关。例如:

  • int8byte 是 1 字节,对齐方式是 1。
  • int16 对齐方式是 2。
  • int32 对齐方式是 4。
  • int64float64 对齐方式是 8。
  • float32 对齐方式是 4。

默认情况下,Go 会根据数据类型的大小自动选择对齐方式。例如:

type MyStruct struct {
    a int8   // 1 byte
    b int32  // 4 bytes
}

在内存中,Go 会将 a 放在一个字节上,接着将 b 放在离 a 之后的 4 字节位置上。这样做的原因是 int32 类型要求它的地址是 4 的倍数,所以编译器会自动为 b 分配一个合适的位置来确保它正确对齐。

2. 内存布局

Go 中结构体(struct)的内存布局会受到字段类型对齐要求的影响。如果一个结构体包含多个字段,Go 会为每个字段按其对齐要求进行布局,并可能插入 内存填充(padding)来确保对齐。

例如,考虑以下结构体:

type MyStruct struct {
    a int8   // 1 byte
    b int32  // 4 bytes
}

虽然 a 只占 1 字节,但 b 占用 4 字节,因此编译器会为 b 添加 3 字节的填充,以确保 b 的地址是 4 的倍数。结构体的内存布局如下:

  • a 占用 1 字节。
  • 接下来的 3 字节是填充字节。
  • b 占用 4 字节。

因此,MyStruct 占用 8 字节(1 字节的 a + 3 字节填充 + 4 字节的 b)。这种方式确保了对齐,但可能导致内存的浪费。

3. 结构体对齐的例子
package main

import "fmt"
import "unsafe"

type MyStruct struct {
    a int8   // 1 byte
    b int32  // 4 bytes
}

func main() {
    var m MyStruct
    fmt.Println(unsafe.Sizeof(m)) // 输出结构体的大小
    fmt.Println(unsafe.Alignof(m)) // 输出结构体的对齐要求
}

输出结果:

8
4

这说明结构体 MyStruct 的大小为 8 字节(1 字节 + 3 字节填充 + 4 字节),并且结构体的对齐要求为 4 字节。

4. 对齐方式与 unsafe

Go 提供了 unsafe 包来查看变量的大小、对齐方式等。unsafe.Sizeof() 返回变量的大小,而 unsafe.Alignof() 返回变量的对齐要求。对于结构体来说,它的对齐方式通常是其中对齐要求最大的字段的对齐方式。

例如:

package main

import "fmt"
import "unsafe"

type A struct {
    x int8
    y int64
}

func main() {
    fmt.Println(unsafe.Sizeof(A{}))  // 16 字节
    fmt.Println(unsafe.Alignof(A{})) // 8 字节(因为 int64 对齐要求是 8)
}
5. 如何控制对齐

Go 并没有直接提供控制结构体字段对齐的机制,但你可以通过调整字段的顺序来减少内存填充。通过将对齐要求更高的字段放在前面,可以减少填充的空间。

例如,以下结构体会减少内存浪费:

type MyOptimizedStruct struct {
    b int32  // 4 bytes
    a int8   // 1 byte
}

在这种情况下,结构体的内存布局将是:

  • b 占用 4 字节,按 4 字节对齐。
  • a 占用 1 字节。
  • 接下来,3 字节为填充,以确保结构体大小为 8 字节。

这种布局比将 a 放在 b 前面更节省内存,因为它避免了额外的填充。

6. Go 1.18 和以后的变动

在 Go 1.18 引入了泛型后,对于结构体内存对齐的影响基本没有改变,依然遵循结构体成员对齐的规则。不过,需要注意的是,Go 语言的编译器和标准库已经做了很多优化,以减少内存浪费并提高访问效率。

总结

  • Go 会根据每种数据类型的大小自动决定对齐方式。
  • 内存对齐可以提升 CPU 访问数据的效率,但可能导致内存的浪费(填充)。
  • 你可以通过调整结构体字段的顺序来减少内存浪费,避免不必要的填充。
  • unsafe.Sizeof()unsafe.Alignof() 可以帮助你检查内存对齐和结构体的大小。

拷贝slice

// slicecopy is used to copy from a string or slice of pointerless elements into a slice.
func slicecopy(toPtr unsafe.Pointer, toLen int, fromPtr unsafe.Pointer, fromLen int, width uintptr) int {
	if fromLen == 0 || toLen == 0 {
		return 0
	}
	n := fromLen
	if toLen < n {
		n = toLen
	}
	if width == 0 {
		return n
	}
	size := uintptr(n) * width
	if raceenabled {
		callerpc := getcallerpc()
		pc := abi.FuncPCABIInternal(slicecopy)
		racereadrangepc(fromPtr, size, callerpc, pc)
		racewriterangepc(toPtr, size, callerpc, pc)
	}
	if msanenabled {
		msanread(fromPtr, size)
		msanwrite(toPtr, size)
	}
	if asanenabled {
		asanread(fromPtr, size)
		asanwrite(toPtr, size)
	}
	if size == 1 { // common case worth about 2x to do here
		// TODO: is this still worth it with new memmove impl?
		*(*byte)(toPtr) = *(*byte)(fromPtr) // known to be a byte pointer
	} else {
		memmove(toPtr, fromPtr, size)
	}
	return n
}

copy只看两个切片的长度。如果目标len小于源len,就只拷贝目标长度的内容。注意这里是len不是cap。

slice的避坑指南

引用类型

package main  
  
import "fmt"  
  
func main() {  
    a := []int{1, 2, 3, 4, 5}  
    b := a[1:2]  
    fmt.Println(a, b)  
    a[1] = 6  
    fmt.Println(a, b)  
}
//结果如下
[1 2 3 4 5] [2]
[1 6 3 4 5] [6]

参数传递

// slice作为参数呢,下面定义一个函数change,里面修改参数`b`的值,会不会对`a`影响呢?

package main  
  
import "fmt"  
  
func change(b []int) {  
    b[1] = 6  
}  
  
func main() {  
    a := []int{1, 2, 3, 4, 5, 6}  
    fmt.Println(a)  
    change(a)  
    fmt.Println(a)  
}

//结果如下
[1 2 3 4 5 6]
[1 6 3 4 5 6]

改动一下

package main  
  
import "fmt"  
  
func change(b []int) {  
    b = append(b, 7)  
    b[1] = 6  
}  
  
func main() {  
    a := []int{1, 2, 3, 4, 5, 6}  
    fmt.Println(a)  
    change(a)  
    fmt.Println(a)  
}


//结果
[1 2 3 4 5 6]
[1 2 3 4 5 6]

这是因为a的len和cap都是5。第一次时a[1] = 6只更新了值,所以对a的值也有影响。第二次时changeappend追加了一个值5,b的cap < len + 1发生了扩容,b变成了len = 6, cap = 10,是一个新的切片。追加的5和修改的值都影响在b上,而a还是原来的值,所以结果才是这样。

那我们再来看看下面一段代码

package main  
  
import "fmt"  
  
func change(b []int) {  
    c := b  
    b = append(b, 7)  
    c[1] = 6  
}  
  
func main() {  
    a := []int{1, 2, 3, 4, 5, 6}  
    fmt.Println(a)  
    change(a)  
    fmt.Println(a)  
}

遍历切片

func main() {
	a := []int{1, 2, 3, 4, 5}
	fmt.Println(a)
	for i, v := range a {
		if i < 4 {
			a[i+1] += v
		}
		fmt.Println(v)
	}
	fmt.Println(a)
}

//结果如下
[1 2 3 4 5]
1
3
6
10
15
[1 3 6 10 15]



http://www.kler.cn/a/549743.html

相关文章:

  • (PC+WAP) PbootCMS中小学教育培训机构网站模板 – 绿色小学学校网站源码下载
  • 【第12章:深度学习与伦理、隐私—12.4 深度学习与伦理、隐私领域的未来挑战与应对策略】
  • DeepSeek 服务器繁忙的全面解决方案
  • 铁塔电单车协议对接电单车TCP json协议对接成熟充电桩系统搭建低速充电桩TCP 接口规范
  • 【第14章:神经符号集成与可解释AI—14.2 可解释AI技术:LIME、SHAP等的实现与应用案例】
  • 深入解析:如何利用 Python 爬虫获取淘宝/天猫 SKU 详细信息
  • 让编程变成一种享受-明基RD320U显示器
  • 机器学习 网络安全 网络安全科学
  • 我们能阻止人工智能末日吗?
  • 10.2 Git 内部原理 - Git 对象
  • Linux 网络设备驱动中的 netdev_priv 函数详解
  • 自定义解的使用,反射,代理模式
  • 二.工控之工业相机专题
  • 机器学习--实现多元线性回归
  • 剑指 Offer II 018. 有效的回文
  • 无法连接虚拟设备 sata0:1,0因为主机上没有相对应的设备
  • Spring事务失效的几种场景
  • 【一文读懂】TCP与UDP协议
  • AI前端开发与跨领域合作:效率提升新纪元
  • 低空经济:开启未来空中生活的全新蓝海