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 | *_type | Go类型系统的类型描述符 | 包含类型大小、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)
-  计算内存需求 - 通过 元素大小(elemSize)* 容量(cap)计算切片底层数组所需内存空间。
- 关键检查:确保乘积结果不溢出 uintptr范围(如 64 位系统下最大为2^64-1),防止数值异常。
 
- 通过 
-  合法性校验 - 内存溢出:若 elemSize * cap超出uintptr范围,触发 panic。
- 内存上限:若计算结果超过系统单次分配最大内存 maxAlloc(如 64 位系统通常为1<<48),触发 panic。
- 逻辑错误:若 len < 0或len > cap(如len为负数或容量不足),触发 panic。
 
- 内存溢出:若 
-  内存分配 - 通过 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)在内存中可能会按照不同的对齐方式进行排列。
为什么需要内存对齐?
-  性能优化: 许多 CPU 对不同数据类型的访问有特定的要求。例如,在许多架构上,读取 4 字节的数据(如 int32)要求其地址必须是 4 的倍数。如果数据未对齐,CPU 可能需要额外的周期来处理访问,这样会降低性能。
-  硬件要求: 一些架构(如 ARM 和 x86)要求数据在特定边界对齐,否则可能会导致硬件异常或效率低下。 
Go 中的内存对齐
1. 对齐方式
在 Go 中,对齐方式通常与数据类型的大小有关。例如:
- int8和- byte是 1 字节,对齐方式是 1。
- int16对齐方式是 2。
- int32对齐方式是 4。
- int64和- float64对齐方式是 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的值也有影响。第二次时change中append追加了一个值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]
 
                   
                   
                   
                   
                            
 
                             
       
           
                 
                 
                 
                 
                 
                
               
                 
                 
                 
                 
                
               
                 
                 扫一扫
扫一扫
                     
              
             
                   891
					891
					
 被折叠的  条评论
		 为什么被折叠?
被折叠的  条评论
		 为什么被折叠?
		 
		  到【灌水乐园】发言
到【灌水乐园】发言                                
		 
		 
    
   
    
   
             
            


 
            