作者 | 马超
责编 | 夕颜
封图 | CSDN下载自视觉中国
出品 | CSDN(ID:CSDNnews)
今年3月初,腾讯发布了《腾讯研发大数据报告》,笔者发现GO语言的使用在鹅厂已经上升到了TOP5的位置了。
我们知道腾讯尤其是Docker容器化这一块,是走在各大厂的前列的,尤其是他们的基于GO语言开发的DEVOPS蓝鲸平台,水平相当高。
经笔者实地上手体验,GO语言在并发等方面还是相当优秀的,下面笔者就汇报一下最新的成果。
GO语言的切片简介
切片(slice)是对数组的一个连续片段的引用,所以切片是一个引用类型同,与Python 中的 list 类型比较类似,这个片段可以是整个数组,也可以是由起始和终止索引标识的一些项的子集。Go语言中切片的内部结构包含地址、大小(len)和容量(cap)与数组相比切片最大的特点就是其容量是可变的。
GO语言的代码解读
1. append函数添加元素
Go语言的内建函数 append() 可以为切片动态添加元素,不过需要注意的是,由于切片本身是变长的,因此在使用 append() 函数为切片动态添加元素时,切片就会自动进行“扩容”,同时新切片的长度也会增加,但是有一点需要注意,append返回的是一个新的切片对象,而不是对原切片进行操作。在下面的代码中我们先定义了一个切片a,并不断通过append方式为其增加元素,并观察切片a的长度及容量变化。
-
package main
-
-
-
import (
-
"fmt"
-
)
-
-
-
func main() {
-
-
-
var a []
int
//定义一个切片
-
fmt.Printf(
"len: %d cap: %d pointer: %p\n",
len(a),
cap(a), a)
//此时切片长度和容量都是0,运行结果为len: 0 cap: 0 pointer: 0x0
-
a =
append(a,
1)
// 追加1个元素
-
fmt.Printf(
"len: %d cap: %d pointer: %p\n",
len(a),
cap(a), a)
//注意此时a的地址已经发生变化为新的切片了,新切片长度和容量都为1运行结果为:len: 1 cap: 1 pointer: 0xc000072098
-
a =
append(a,
2,
3,
4)
// 追加多个元素
-
fmt.Printf(
"len: %d cap: %d pointer: %p\n",
len(a),
cap(a), a)
//注意此时a的地址再次发生变化实际上又生成为新的切片了,新切片长度和容量都为4运行结果为:len: 4 cap: 4 pointer: 0xc000070160
-
a =
append(a,
5)
// 再追加一个元素
-
fmt.Printf(
"len: %d cap: %d pointer: %p\n",
len(a),
cap(a), a)
//注意切片扩容策略是倍增方式容量由4变成8,而长度是5运行结果为:len: 4 cap: 4 pointer: 0xc000070160
-
-
-
}
可以观察到切片在扩容时,其容量(cap)的速度规律是以2 倍数进行的。
2.在切片中元素的删除
删除切片中开头的N个元素
使用x = x[N:] 的方式来在切片中删除由第i个元素开始的N个元素
具体代码如下:
-
package main
-
-
-
-
-
import (
-
"fmt"
-
)
-
-
-
func main() {
-
var a = []
int{
1,
2,
3,
4,
5,
6,
7,
8,
9,
10}
//使用原始定义法来声明并初始化一个切片
-
fmt.Println(a)
//运行结果为[1 2 3 4 5 6 7 8 9 10]
-
a = a[
1:]
// 删除第1个元素
-
fmt.Println(a)
//删头第1个元素后,运行结果为[2 3 4 5 6 7 8 9 10]
-
a = a[
2:]
// 删除前2个元素
-
fmt.Println(a)
//删头前2个元素后,运行结果为[4 5 6 7 8 9 10]
-
-
-
}
3、深入理解GO语言中的切片
有关切片的代码位置在GOPATH\src\runtime\slice.go,其中对于几个重点函数解读如下:
1.slice 结构定义
首先slice是这样一个结构体,他有一个存放数据的数组,和一个长度len与容量cap构成
-
type slice
struct {
-
-
-
array unsafe.Pointer
-
len
int
-
cap
int
-
}
2.创建切片的makeslice函数
而创建切片的函数makeslice如下,可以看到函数会对于内存进行预分配,如果成功再正式分配内存,他建切片的makeslice函数源码及注释如下:
-
func makeslice(et *_type, len, cap int) slice {
-
-
-
mem, overflow := math.MulUintptr(et.size,
uintptr(
cap))
//此函数计算et.size也就是每个元素所占空间的大小,并与容量cap相乘,其中mem既为所需要最大内存,overflow代表是否会造成溢出
-
if overflow || mem > maxAlloc ||
len <
0 ||
len >
cap {
//判断是否有溢出,长度为负数或者长度比容量大的情况,如存在 直接panic
-
// 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()
-
}
-
return mallocgc(mem, et,
true)
// 如果错误检查成功,则分配内存,注意slice对象会被GC所自动清除。
-
-
-
}
3.扩容函数growslice
通过阅读growslice的源码可以看在这个函数当中,扩容的规则是在长度小于1024时按照一直采用的是翻倍的方式进行扩容,在大于1024后,每次扩容至原容量的1.25倍,新容量计算完成后对于内存进行预分配,这点也makeslice的想法一致,接下再将老slice中的数据通过memmove(p, old.array, lenmem)的方式拷贝至新的slice。growlice函数源码及注释如下:
-
func growslice(et *_type, old slice, cap int) slice {
-
-
-
-
-
// 单纯地扩容,不写数据
-
if et.size ==
0 {
-
if
cap < old.
cap {
-
panic(errorString(
"growslice: cap out of range"))
-
}
-
// 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}
-
}
-
// 扩容规则 1.新的容量大于旧的2倍,直接扩容至新的容量
-
// 2.新的容量不大于旧的2倍,当旧的长度小于1024时,扩容至旧的2倍,否则扩容至旧的1.25倍
-
newcap := old.
cap
-
doublecap := newcap + newcap
-
if
cap > doublecap {
-
newcap =
cap
-
}
else {
-
if old.
len <
1024 {
-
newcap = doublecap
-
}
else {
-
for newcap <
cap {
-
newcap += newcap /
4
-
}
-
}
-
}
-
-
-
// 跟据切片类型和容量计算要分配内存的大小
-
-
-
var overflow
bool
-
var lenmem, newlenmem, capmem
uintptr
-
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)
-
}
-
// 异常情况,旧的容量比新的容量还大或者新的容量超过限制了
-
if
cap < old.
cap ||
uintptr(newcap) > maxSliceCap(et.size) {
-
panic(errorString(
"growslice: cap out of range"))
-
}
-
-
-
var p unsafe.Pointer
-
if et.kind&kindNoPointers !=
0 {
-
-
-
// 为新的切片开辟容量为capmem的地址空间
-
p = mallocgc(capmem,
nil,
false)
-
// 将旧切片的数据搬到新切片开辟的地址中
-
memmove(p, old.array, lenmem)
-
// The append() that calls growslice is going to overwrite from old.len to cap (which will be the new length).
-
// Only clear the part that will not be overwritten.
-
// 清理下新切片中剩余地址,不能存放堆栈指针
-
-
-
// memclrNoHeapPointers clears n bytes starting at ptr.
-
//
-
// Usually you should use typedmemclr. memclrNoHeapPointers should be
-
// used only when the caller knows that *ptr contains no heap pointers
-
// because either:
-
//
-
// 1. *ptr is initialized memory and its type is pointer-free.
-
//
-
// 2. *ptr is uninitialized memory (e.g., memory that's being reused
-
// for a new allocation) and hence contains only "junk".
-
memclrNoHeapPointers(add(p, newlenmem), capmem-newlenmem)
-
}
else {
-
// Note: can't use rawmem (which avoids zeroing of memory), because then GC can scan uninitialized memory.
-
p = mallocgc(capmem, et,
true)
-
if !writeBarrier.enabled {
-
memmove(p, old.array, lenmem)
-
}
else {
-
for i :=
uintptr(
0); i < lenmem; i += et.size {
-
typedmemmove(et, add(p, i), add(old.array, i))
-
}
-
}
-
}
-
-
-
return slice{p, old.
len, newcap}
-
}
GO语言切片的相关结论
所以通过阅读以上源代码我们也可以知道,有以下两点结论:
append方式为数据增加元素时,如果触发切片进行扩容,则肯定是新生成了一个切片对象,并且涉及内存操作,因此append操作一定要小心。
建议尽量通过make函数来声明一个切片,并在初始时尽量设定好一个合理的容量值,避免切片频繁扩容带来不必要的开销。
原文链接:
https://blog.csdn.net/BEYONDMA/article/details/104799500
【END】
今日福利
遇见陆奇
同样作为“百万人学 AI”的重要组成部分,2020 AIProCon 开发者万人大会将于 7 月 3 日至 4 日通过线上直播形式,让开发者们一站式学习了解当下 AI 的前沿技术研究、核心技术与应用以及企业案例的实践经验,同时还可以在线参加精彩多样的开发者沙龙与编程项目。参与前瞻系列活动、在线直播互动,不仅可以与上万名开发者们一起交流,还有机会赢取直播专属好礼,与技术大咖连麦。
门票限量大放送!今日起点击阅读原文报名「2020 AI开发者万人大会」,使用优惠码“AIP211”,即可免费获得价值299元的大会在线直播门票一张。限量100张,先到先得!快来动动手指,免费获取入会资格吧!
点击阅读原文,直达大会官网。
转载:https://blog.csdn.net/csdnnews/article/details/105631425