当前位置:首页 > 技术分析 > 正文内容

为什么大厂都在用 GO 语言?读透 GO 语言的切片

ruisui884个月前 (02-18)技术分析18

作者 | 马超

责编 | 夕颜

封图 | 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语言切片的相关结论

所以通过阅读以上源代码我们也可以知道,有以下两点结论:

  1. append方式为数据增加元素时,如果触发切片进行扩容,则肯定是新生成了一个切片对象,并且涉及内存操作,因此append操作一定要小心。
  2. 建议尽量通过make函数来声明一个切片,并在初始时尽量设定好一个合理的容量值,避免切片频繁扩容带来不必要的开销。

原文链接:

https://blog.csdn.net/BEYONDMA/article/details/104799500

今日福利

遇见陆奇

同样作为“百万人学 AI”的重要组成部分,2020 AIProCon 开发者万人大会将于 7 月 3 日至 4 日通过线上直播形式,让开发者们一站式学习了解当下 AI 的前沿技术研究、核心技术与应用以及企业案例的实践经验,同时还可以在线参加精彩多样的开发者沙龙与编程项目。参与前瞻系列活动、在线直播互动,不仅可以与上万名开发者们一起交流,还有机会赢取直播专属好礼,与技术大咖连麦。

扫描二维码推送至手机访问。

版权声明:本文由ruisui88发布,如需转载请注明出处。

本文链接:http://www.ruisui88.com/post/2043.html

标签: go数组
分享给朋友:

“为什么大厂都在用 GO 语言?读透 GO 语言的切片” 的相关文章

Vue3,父组件子组件传值,provide(提供)和inject(注入)传值

父组件向子组件传值父子组件传递数据时,通常使用的是props和emit,父向子传递使用props,子向父传递使用emit。子组件接收3种方式// 1、简单接收 props:["title","isShow"], // 2、接收的同时对数据类型进行限制 props:{...

html5+css3做的响应式企业网站前端源码

大家好,今天给大家介绍一款,html5+css3做的响应式企业网站前端源码 (图1)。送给大家哦,获取方式在本文末尾。首页banner幻灯片切换特效(图2)首页布局简约合理(图3)关于我们页面(图4)商品列表(图5)商品详情(图6)服务介绍(图7)新闻列表(图8)联系我们(图9)源码完整,需要的朋友...

vue3使用vue-router路由(路由懒加载、路由传参)

vue-router 是 vue的一个插件库1. 专门用来实现一个SPA单页面应用2 .基于vue的项目基本都会用到此库SPA的理解1) 单页Web应用(single page web application,SPA)2) 整个应用只有一个完整的页面3) 点击页面中的链接不会刷新页面, 本身也不会向...

vue开发微信小程序 - 登录组件

移动端登录功能抽象为通用组件,满足:不同移动端应用中一键登录功能复用支持多种登录:微信登录、H5、QQ登录登录组件使用//引用登录组件 import login from "../components/user/login.vue" export default { compone...

微信开发的五大价值应用

企业形象展示微网站是企业在移动互联网时代完美展示企业及品牌形象的最佳选择,表现内容丰富、实时更新、形式多样化,保证品牌形象的有效传播!微网站带来的轻营销模式,更适应现代网站的发展模式,所以微网站的开发也具有更好的商业营销效果,其面对的受众是7亿多的微信用户,蕴含着无限的商机。将企业微网站植入微信公众...

那些你不知道的 TCP 冷门知识

最近在做数据库相关的事情,碰到了很多TCP相关的问题,新的场景新的挑战,有很多之前并没有掌握透彻的点,大大开了一把眼界,选了几个案例分享一下。案例一:TCP中并不是所有的RST都有效背景知识:在TCP协议中,包含RST标识位的包,用来异常的关闭连接。在TCP的设计中它是不可或缺的,发送RST段关闭连...