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底层原理剖析之垃圾回收GC(二)
Golang底层原理剖析之垃圾回收GC(二)
46 0
|
4月前
|
存储 SQL 安全
Golang底层原理剖析之上下文Context
Golang底层原理剖析之上下文Context
57 0
|
4月前
|
编译器 Go
Golang底层原理剖析之method
Golang底层原理剖析之method
22 2
|
4月前
|
存储 Go
Golang底层原理剖析之map
Golang底层原理剖析之map
31 1
|
5天前
|
人工智能 Go 开发者
Golang语言异常机制解析:错误策略与优雅处理
Golang语言异常机制解析:错误策略与优雅处理
|
6天前
|
Go 开发者
Golang深入浅出之-Go语言 defer、panic、recover:异常处理机制
Go语言中的`defer`、`panic`和`recover`提供了一套独特的异常处理方式。`defer`用于延迟函数调用,在返回前执行,常用于资源释放。它遵循后进先出原则。`panic`触发运行时错误,中断函数执行,直到遇到`recover`或程序结束。`recover`在`defer`中捕获`panic`,恢复程序执行。注意避免滥用`defer`影响性能,不应对可处理错误随意使用`panic`,且`recover`不能跨goroutine捕获panic。理解并恰当使用这些机制能提高代码健壮性和稳定性。
17 2
|
7天前
|
Go 索引
Golang深入浅出之-切片(Slices)入门:创建、操作与扩容机制
【4月更文挑战第20天】Go语言中的切片是动态数组,提供灵活的操作和自动扩容。本文介绍了切片的创建(通过`make()`、数组创建和切片字面量)、基本操作(索引访问、切片、赋值追加和遍历)以及扩容机制(首次和后续扩容策略)。此外,还强调了切片与底层数组的关系、切片越界问题、`append()`的使用以及理解切片的关键点,帮助提升Go编程效率和代码质量。
27 0
|
4月前
|
Go
golang随笔之slice+append的陷阱
golang随笔之slice+append的陷阱
17 0
|
4月前
|
Java 编译器 Go
Golang底层原理剖析之内存逃逸
Golang底层原理剖析之内存逃逸
24 0
|
4月前
|
存储 算法 Java
Golang底层原理剖析之多路select、channel数据结构和阻塞与非阻塞
Golang底层原理剖析之多路select、channel数据结构和阻塞与非阻塞
24 0