Golang标准库揭秘系列 | slice详解

简介: golang slice详解

数据结构


slice的定义在$GOROOT/src/runtime/slice.go


type slice struct {

array unsafe.Pointer

len   int

cap   int

}


array指针指向底层数组, len表示切片长度, cap表示底层数组容量


slice创建


通过make创建


//make

slice := make([]int, 5, 10)


网络异常,图片无法展示
|


通过数组创建


//array

array := [10]int{}

slice := array[0:5]


网络异常,图片无法展示
|


内存共享


slice通过数组切分时,两者会共用内存空间,此时slice[0] == array[5] : true slice[1] == array[6] : true,这个特性需要特别注意,尤其是在同时处理数组slice的过程中,如我们操作array[5] = 8,那么slice[0]此时也是8


网络异常,图片无法展示
|


当我们使用make方式进行切片初始化的时候经过了哪些处理呢?


//make

slice := make([]int, 5, 10)


slice初始化


通过gdb断点可以看到,使用到了slice.go文件中的makeslice()方法,如下:


func makeslice(et *_type, len, cap int) unsafe.Pointer {

mem, overflow := math.MulUintptr(et.size, uintptr(cap))

if overflow || mem > maxAlloc || len < 0 || len > cap {

// NOTE: Produce a 'len out of range' error instead of a

// 'cap out of range' error when someone does make([]T, bignumber).

// 'cap out of range' is true too, but since the cap is only being

// supplied implicitly, saying len is clearer.

// See golang.org/issue/4085.

mem, overflow := math.MulUintptr(et.size, uintptr(len))

if overflow || mem > maxAlloc || len < 0 {

  panicmakeslicelen()

}

panicmakeslicecap()

}

//以上是对内存溢出情况对panic处理

return mallocgc(mem, et, true)

}


slice扩容


slice扩容的方法定义在$GOROOT/src/runtime/slice.gogrowslice方法中。


通用扩容策略


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

  }

}

}


  • 如果新cap大小是当前cap的2倍以上,那么按照新cap进行扩容
  • cap小于1024,按照2倍扩容
  • cap大于1024,按照1.25倍扩容


通过代码来看下slice普通扩容过程中len、cap以及内存分配情况,如下:


// 普通扩容情况,这里是int32类型

func slice() {

slice := make([]int32, 0)

for i := 0; i < 10; i++ {

fmt.Printf("seq=%v, len=%v, cap=%v,\t ptr=%p \t slice=%#v \n",

  i,

  len(slice),

  cap(slice),

  &slice,

  slice)

slice = append(slice, int32(i))

}

}


输出日志如下:


seq=0, len=0, cap=0,    ptr=0xc00011a018        slice=[]int32{}

seq=1, len=1, cap=2,    ptr=0xc00011a018        slice=[]int32{0}

seq=2, len=2, cap=2,    ptr=0xc00011a018        slice=[]int32{0, 1}

seq=3, len=3, cap=4,    ptr=0xc00011a018        slice=[]int32{0, 1, 2}

seq=4, len=4, cap=4,    ptr=0xc00011a018        slice=[]int32{0, 1, 2, 3}

seq=5, len=5, cap=8,    ptr=0xc00011a018        slice=[]int32{0, 1, 2, 3, 4}

seq=6, len=6, cap=8,    ptr=0xc00011a018        slice=[]int32{0, 1, 2, 3, 4, 5}

seq=7, len=7, cap=8,    ptr=0xc00011a018        slice=[]int32{0, 1, 2, 3, 4, 5, 6}

seq=8, len=8, cap=8,    ptr=0xc00011a018        slice=[]int32{0, 1, 2, 3, 4, 5, 6, 7}

seq=9, len=9, cap=16,   ptr=0xc00011a018        slice=[]int32{0, 1, 2, 3, 4, 5, 6, 7, 8}


日志解释:


  • seq是执行次序
  • len是当前已使用空间
  • cap是当前全部容量
  • ptr是切片的指针
  • slice是切片的内容


借助benchmark来查看下内存分配情况:


%  go test -bench=SliceExpand -benchmem

goos: darwin

goarch: amd64

pkg: program/slice

cpu: Intel(R) Core(TM) i7-9750H CPU @ 2.60GHz

BenchmarkSliceExpand-12          6996427               144.7 ns/op           248 B/op          5 allocs/op

PASS

ok      program/slice        1.195s


5 allocs/op表明10次循环过程中进行了5次的内存分配,其实这便是cap的扩容过程,即0 -> 1 -> 2 -> 4 -> 8 -> 16的5次扩容的内存操作。


特殊扩容策略


对于一些特殊类型,出于内存对齐充分利用的考虑,扩容过程中需要进行特殊处理,下面是特殊处理扩容的策略代码,其中最主要的是roundupsize()方法,它在本地存储了各长度的内存对其策略,根据type类型的size来指定扩容情况,这样是对内存友好的。


// Specialize for common values of et.size.

// For 1 we don't need any division/multiplication.

// For sys.PtrSize, compiler will optimize division/multiplication into a shift by a constant.

// For powers of 2, use a variable shift.

switch {

case et.size == 1:

lenmem = uintptr(old.len)

newlenmem = uintptr(cap)

capmem = roundupsize(uintptr(newcap))

overflow = uintptr(newcap) > maxAlloc

newcap = int(capmem)

case et.size == sys.PtrSize:

lenmem = uintptr(old.len) * sys.PtrSize

newlenmem = uintptr(cap) * sys.PtrSize

capmem = roundupsize(uintptr(newcap) * sys.PtrSize)

overflow = uintptr(newcap) > maxAlloc/sys.PtrSize

newcap = int(capmem / sys.PtrSize)

case isPowerOfTwo(et.size):

var shift uintptr

if sys.PtrSize == 8 {

  // Mask shift for better code generation.

  shift = uintptr(sys.Ctz64(uint64(et.size))) & 63

} else {

  shift = uintptr(sys.Ctz32(uint32(et.size))) & 31

}

lenmem = uintptr(old.len) << shift

newlenmem = uintptr(cap) << shift

capmem = roundupsize(uintptr(newcap) << shift)

overflow = uintptr(newcap) > (maxAlloc >> shift)

newcap = int(capmem >> shift)

default:

lenmem = uintptr(old.len) * et.size

newlenmem = uintptr(cap) * et.size

capmem, overflow = math.MulUintptr(et.size, uintptr(newcap))

capmem = roundupsize(capmem)

newcap = int(capmem / et.size)

}


// Returns size of the memory block that mallocgc will allocate if you ask for the size.

func roundupsize(size uintptr) uintptr {

if size < _MaxSmallSize {

if size <= smallSizeMax-8 {

  return uintptr(class_to_size[size_to_class8[divRoundUp(size, smallSizeDiv)]])

} else {

  return uintptr(class_to_size[size_to_class128[divRoundUp(size-smallSizeMax, largeSizeDiv)]])

}

}

if size+_PageSize < size {

return size

}

return alignUp(size, _PageSize)

}


通过代码来看下slice特殊扩容过程中len、cap以及内存分配情况,如下:


// 特殊扩容情况,这里是int8类型

func slice() {

slice := make([]int8, 0)

for i := 0; i < 10; i++ {

fmt.Printf("seq=%v, len=%v, cap=%v,\t ptr=%p \t slice=%#v \n",

  i,

  len(slice),

  cap(slice),

  &slice,

  slice)

slice = append(slice, int8(i))

}

}


输出日志如下:


seq=0, len=0, cap=0,    ptr=0xc0000a8018        slice=[]int8{}

seq=1, len=1, cap=8,    ptr=0xc0000a8018        slice=[]int8{0}

seq=2, len=2, cap=8,    ptr=0xc0000a8018        slice=[]int8{0, 1}

seq=3, len=3, cap=8,    ptr=0xc0000a8018        slice=[]int8{0, 1, 2}

seq=4, len=4, cap=8,    ptr=0xc0000a8018        slice=[]int8{0, 1, 2, 3}

seq=5, len=5, cap=8,    ptr=0xc0000a8018        slice=[]int8{0, 1, 2, 3, 4}

seq=6, len=6, cap=8,    ptr=0xc0000a8018        slice=[]int8{0, 1, 2, 3, 4, 5}

seq=7, len=7, cap=8,    ptr=0xc0000a8018        slice=[]int8{0, 1, 2, 3, 4, 5, 6}

seq=8, len=8, cap=8,    ptr=0xc0000a8018        slice=[]int8{0, 1, 2, 3, 4, 5, 6, 7}

seq=9, len=9, cap=16,   ptr=0xc0000a8018        slice=[]int8{0, 1, 2, 3, 4, 5, 6, 7, 8}


借助benchmark来查看下内存分配情况:


% go test -bench=SliceExpand -benchmem                      

goos: darwin

goarch: amd64

pkg: program/slice

cpu: Intel(R) Core(TM) i7-9750H CPU @ 2.60GHz

BenchmarkSliceExpand-12         25947428                47.71 ns/op           24 B/op          2 allocs/op

PASS

ok      program/slice        2.259s


2 allocs/op表明10次循环过程中进行了2次的内存分配,其实这便是cap的扩容过程,即0 -> 8 -> 16的2次扩容的内存操作。


小结


  • 切片的cap一般处理则按照2倍扩容,特殊处理按照roundupsize函数扩容,按照特殊处理的cap扩容减少了内存操作次数
  • 切片的指针没有发生变化,一直是在同一个数组下进行操作的


slice特殊用法


可以使用如下格式进行切片的使用和截取

语法

示例

make[type, len, cap]

sliceA := make([]int, 5, 10) //length = 5; capacity = 10

slice[start : end]

sliceB := sliceA[0:5] //length = 5; capacity = 10

slice[start : ]

sliceC := sliceA[0:] //length = 5; capacity = 10

slice[: end ]

sliceD := sliceA[:5] //length = 5; capacity = 10

slice[start : end : cap]

sliceE := sliceA[0:5:5] //length = 5; capacity = 5


总结


  • 创建切片时可跟据实际需要预分配容量, 尽量避免追加过程中扩容操作, 有利于提升性能;
  • 切片拷贝时需要判断实际拷贝的元素个数
  • 谨慎使用多个切片操作同一个数组, 以防读写冲突


参考


《Go专家编程》
Go slice扩容深度分析

相关文章
|
1月前
|
JSON Go 开发者
go-carbon v2.5.0 发布,轻量级、语义化、对开发者友好的 golang 时间处理库
carbon 是一个轻量级、语义化、对开发者友好的 Golang 时间处理库,提供了对时间穿越、时间差值、时间极值、时间判断、星座、星座、农历、儒略日 / 简化儒略日、波斯历 / 伊朗历的支持。
39 4
|
1月前
|
存储 Cloud Native Shell
go库介绍:Golang中的Viper库
Viper 是 Golang 中的一个强大配置管理库,支持环境变量、命令行参数、远程配置等多种配置来源。本文详细介绍了 Viper 的核心特点、应用场景及使用方法,并通过示例展示了其强大功能。无论是简单的 CLI 工具还是复杂的分布式系统,Viper 都能提供优雅的配置管理方案。
|
2月前
|
存储 缓存 测试技术
golang slice相关常见的性能优化手段
【10月更文挑战第23天】本文介绍了 Go 语言中切片使用的四个优化技巧:预分配容量、减少中间切片的创建、利用切片的复用特性和合理使用 `copy` 函数。通过这些方法,可以有效提高程序性能,减少不必要的内存分配和数据复制操作。每个技巧都附有详细的原理说明和代码示例,帮助开发者更好地理解和应用。
|
3月前
|
Go
Golang语言之切片(slice)快速入门篇
这篇文章是关于Go语言中切片(slice)的快速入门教程,详细介绍了切片的概念、定义方式、遍历、扩容机制、使用注意事项以及相关练习题。
40 5
|
3月前
|
Unix Go
Golang语言标准库time之日期和时间相关函数
这篇文章是关于Go语言日期和时间处理的文章,介绍了如何使用Go标准库中的time包来处理日期和时间。
65 3
|
4月前
|
JSON Go API
一文搞懂 Golang 高性能日志库 - Zap
一文搞懂 Golang 高性能日志库 - Zap
357 2
|
4月前
|
存储 JSON Go
一文搞懂 Golang 高性能日志库 Zerolog
一文搞懂 Golang 高性能日志库 Zerolog
482 0
|
4月前
|
JSON Go 数据格式
[golang]标准库-json
[golang]标准库-json
|
6月前
|
Go 索引
GOLANG SLICE 的底层实现
GOLANG SLICE 的底层实现
|
6月前
|
SQL NoSQL Go
技术经验分享:Golang标准库:errors包应用
技术经验分享:Golang标准库:errors包应用
47 0