Golang底层原理剖析之slice类型与扩容机制

简介: Golang底层原理剖析之slice类型与扩容机制

概述

前言


熟悉 slice 的底层数据结构 - 实际存储数据的array,当前长度len与容量cap

slice的扩容机制 - 不严格来说,当长度小于1024时,cap翻倍;大于1024时,增加1/4

slice 有很多特性与 map 一致 - 记住一点,代码中操作的slice和map只是上层的,实际存储数据的是array与hmap

golang随笔之slice+append的陷阱

通过代码学习底层

package main
import (
  "fmt"
  "unsafe"
)
func slice() {
  fmt.Println("Slice Init")
  var s []int
  // Tip: 对比一下map和slice的make函数,前者在类型后可跟0个或1个参数,而后者是1个和2个参数
  s = make([]int, 2)
  fmt.Println(len(s), cap(s))
  s = make([]int, 2, 4)
  fmt.Println(len(s), cap(s))
  // Tip: 元素个数小于cap时,append不会改变cap,只会增加len
  fmt.Println("Slice Assign")
  s[0] = 1
  s[1] = 2
  s = append(s, 4)
  fmt.Println(len(s), cap(s))
  // Tip: 元素个数超过cap时,会进行扩容
  s = append(s, 8, 16)
  fmt.Println(len(s), cap(s))
  // Tip: Slice没有显式的删除语句
  fmt.Println("Slice Delete")
  s = append(s[0:1], s[2:]...)
  fmt.Println(s)
  fmt.Println("Slice Range")
  for i, v := range s {
    fmt.Println(i, v)
    fmt.Printf("%p %p\n", &i, &v)
  }
}
/*
  slice 的源码部分
  slice基础结构slice:
  包括保存数据的array、长度len与容量cap
  初始化函数makeslice:
  math.MulUintptr:根据元素大小和容量cap,计算所需的内存空间
  mallocgc: 分配内存, 32K作为一个临界值,小的分配在P的cache中,大的分配在heap堆中
  扩容growslice:
  当长度小于1024时,cap翻倍;大于1024时,增加1/4。 但这个并不是绝对的,会根据元素的类型尽心过一定的优化
  拷贝slicecopy:
  核心函数为memmove,from=>to移动size大小的数据,size为 元素大小 * from和to中长度较小的个数
  拷贝slicestringcopy:
  基本与上面类似,字符串的拷贝
*/
func sliceAddr() {
  fmt.Println("Part 1")
  var s = make([]int, 2, 2)
  s[0] = 1
  fmt.Println(unsafe.Pointer(&s[0]))
  s[1] = 2
  fmt.Println(unsafe.Pointer(&s[0]))
  // Tip: 扩容后,slice的array的地址会重新分配
  s = append(s, 3)
  fmt.Println(unsafe.Pointer(&s[0]))
  fmt.Println("Part 2")
  // Tip: a虽然是一个新的地址,但指向的array是和a一致的
  a := s[:2]
  fmt.Printf("%p %p\n", &s, &a)
  fmt.Println(unsafe.Pointer(&a[0]))
  a[0] = 2
  fmt.Println(a, s)
  // Tip: 如果要进行slice拷贝,使用copy方法
  b := make([]int, 2)
  copy(b, s)//copy会重新分配底层array的内存
  fmt.Printf("%p %p\n", &s, &b)
  fmt.Println(unsafe.Pointer(&b[0]))
  fmt.Println("Part 3")
  // Tip: sNil的array指向nil,而sEmpty的array指向一个内存地址
  var sNil []int//底层array指向nil
  var sEmpty = make([]int, 0)//底层array指向一个内存地址,但是这个内存地址没有分配空间给它
  fmt.Println(len(sNil), len(sEmpty), cap(sNil), cap(sEmpty))
}

详解

slice类型

在runtime下的slice.go可以看到

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

slice基础结构包括保存数据的array、长度len与容量cap,其底层是数组array

这里的ints[3]和ints[4]不能访问,否则属于越界访问,会引发panic


扩容机制

在runtime下的slice.go内有一个扩容growslice函数

扩容机制是


  1. 如果期望容量大于当前容量的两倍就会使用期望容量;
  2. 如果当前切片的长度小于 1024 就会将容量翻倍;
  3. 如果当前切片的长度大于 1024 就会每次增加 25% 的容量,直到新容量大于期望容量;
//growslice部分代码
if cap < old.cap {
  panic(errorString("growslice: cap out of range"))
}
if et.size == 0 {
  // append should not create a slice with nil pointer but non-zero len.
  // We assume that append doesn't need to preserve old.array in this case.
  return slice{unsafe.Pointer(&zerobase), old.len, cap}
}
newcap := old.cap
doublecap := newcap + newcap
if cap > doublecap {
  newcap = cap
} else {
  if old.cap < 1024 {
    newcap = doublecap
  } else {
    // Check 0 < newcap to detect overflow
    // and prevent an infinite loop.
    for 0 < newcap && newcap < cap {
      newcap += newcap / 4
    }
    // Set newcap to the requested cap when
    // the newcap calculation overflowed.
    if newcap <= 0 {
      newcap = cap
    }
  }
}
//下面还有优化

根据元素的类型做一定的优化


比如新容量是3,int类型,则它需要申请24B的内存,此时它会向语言自身的内存管理模块去申请内存,而内存管理模块会提前向操作系统申请一批内存,分为常用的规格管理起来,我们申请内存时,它会帮我们匹配到足够大,且最接近规格的内存,可能这里内存管理模块分配给你了32B的内存,所以这个时候新容量变成4个了

//runtime下sizeclasses.go文件
go采用的是基于tcmalloc进行的内存分配,也就是go语言的内存管理模块。
var class_to_size = [_NumSizeClasses]uint16{0, 8, 16, 32, 48, 64, 80, 96, 112, 128, 144, 160, 176, 192, 208, 224, 240, 256, 288, 320, 352, 384, 416, 448, 480, 512, 576, 640, 704, 768, 896, 1024, 1152, 1280, 1408, 1536, 1792, 2048, 2304, 2688, 3072, 3200, 3456, 4096, 4864, 5376, 6144, 6528, 6784, 6912, 8192, 9472, 9728, 10240, 10880, 12288, 13568, 14336, 16384, 18432, 19072, 20480, 21760, 24576, 27264, 28672, 32768}




目录
相关文章
|
4月前
|
Java 编译器 Go
【Golang】(5)Go基础的进阶知识!带你认识迭代器与类型以及声明并使用接口与泛型!
好烦好烦好烦!你是否还在为弄不懂Go中的泛型和接口而烦恼?是否还在苦恼思考迭代器的运行方式和意义?本篇文章将带你了解Go的接口与泛型,还有迭代器的使用,附送类型断言的解释
244 3
|
4月前
|
存储 Java Go
【Golang】(3)条件判断与循环?切片和数组的关系?映射表与Map?三组关系傻傻分不清?本文带你了解基本的复杂类型与执行判断语句
在Go中,条件控制语句总共有三种if、switch、select。循环只有for,不过for可以充当while使用。如果想要了解这些知识点,初学者进入文章中来感受吧!
201 1
|
5月前
|
Linux Go iOS开发
IDA 9.2 发布:Golang 改进、新 UI 组件、类型解析等
IDA Pro 9.2 (macOS, Linux, Windows) - 强大的反汇编程序、反编译器和多功能调试器
1106 0
|
安全 Go
Golang语言goroutine协程并发安全及锁机制
这篇文章是关于Go语言中多协程操作同一数据问题、互斥锁Mutex和读写互斥锁RWMutex的详细介绍及使用案例,涵盖了如何使用这些同步原语来解决并发访问共享资源时的数据安全问题。
394 4
|
Go
Golang语言错误处理机制
这篇文章是关于Golang语言错误处理机制的教程,介绍了使用defer结合recover捕获错误、基于errors.New自定义错误以及使用panic抛出自定义错误的方法。
194 3
|
存储 安全 测试技术
GoLang协程Goroutiney原理与GMP模型详解
本文详细介绍了Go语言中的Goroutine及其背后的GMP模型。Goroutine是Go语言中的一种轻量级线程,由Go运行时管理,支持高效的并发编程。文章讲解了Goroutine的创建、调度、上下文切换和栈管理等核心机制,并通过示例代码展示了如何使用Goroutine。GMP模型(Goroutine、Processor、Machine)是Go运行时调度Goroutine的基础,通过合理的调度策略,实现了高并发和高性能的程序执行。
865 29
|
负载均衡 算法 Go
GoLang协程Goroutiney原理与GMP模型详解
【11月更文挑战第4天】Goroutine 是 Go 语言中的轻量级线程,由 Go 运行时管理,创建和销毁开销小,适合高并发场景。其调度采用非抢占式和协作式多任务处理结合的方式。GMP 模型包括 G(Goroutine)、M(系统线程)和 P(逻辑处理器),通过工作窃取算法实现负载均衡,确保高效利用系统资源。
385 3
|
存储 缓存 测试技术
golang slice相关常见的性能优化手段
【10月更文挑战第23天】本文介绍了 Go 语言中切片使用的四个优化技巧:预分配容量、减少中间切片的创建、利用切片的复用特性和合理使用 `copy` 函数。通过这些方法,可以有效提高程序性能,减少不必要的内存分配和数据复制操作。每个技巧都附有详细的原理说明和代码示例,帮助开发者更好地理解和应用。
300 2
|
Go
Golang语言之切片(slice)快速入门篇
这篇文章是关于Go语言中切片(slice)的快速入门教程,详细介绍了切片的概念、定义方式、遍历、扩容机制、使用注意事项以及相关练习题。
494 6
|
Go
Golang语言基础之接口(interface)及类型断言
这篇文章是关于Go语言中接口(interface)及类型断言的详细教程,涵盖了接口的概念、定义、实现、使用注意事项以及类型断言的多种场景和方法。
505 4