重庆分公司,新征程启航

为企业提供网站建设、域名注册、服务器等服务

go语言之逃逸,go 指针逃逸

面试问题总结(一)Golang

使用go语言的好处: go语言的设计是务实的, go在针对并发上进行了优化, 并且支持大规模高并发, 又由于单一的码格式, 相比于其他语言更具有可读性, 在垃圾回收上比java和Python更有效, 因为他是和程序同时执行的.

创新互联公司坚持“要么做到,要么别承诺”的工作理念,服务领域包括:成都网站设计、网站建设、企业官网、英文网站、手机端网站、网站推广等服务,满足客户于互联网时代的策勒网站设计、移动媒体设计的需求,帮助企业找到有效的互联网解决方案。努力成为您成熟可靠的网络建设合作伙伴!

1. 进程, 线程, 协程的区别, 协程的优势

2. 讲一下GMP模型(重点)

3. Go的GC, 混合写屏障(重点)

4. go的Slice和数组的区别, slice的扩容原理(重点)

5. 讲一下channel,实现原理(重点)

6. 讲一下Go的Map的实现原理, 是否线程安全, 如何实现安全(重点)

7. new 和 make 的区别

8. 说一下内存逃逸

9. 函数传指针和传值有什么区别

10. goroutine之间的通信方式

11. 测试是怎么做的(单元测试, 压力测试)

12. 堆和栈的区别

Go语言之Context

golang在1.6.2的时候还没有自己的context,在1.7的版本中就把golang.org/x/net/context包被加入到了官方的库中。中文译作“上下文”,它主要包含了goroutine 的运行状态、环境等信息。

context 主要用来在 goroutine 之间传递上下文信息,包括:同步信号、超时时间、截止时间、请求相关值等。

该接口定义了四个需要实现的方法:

如果有个网络请求Request,然后这个请求又可以开启多个goroutine做一些事情,当这个网络请求出现异常和超时时,这个请求结束了,这时候就可以通过context来跟踪这些goroutine,并且通过Context来取消他们,然后系统才可回收所占用的资源。

为了更方便的创建Context,包里头定义了Background来作为所有Context的根,它是一个emptyCtx的实例。

Background返回一个非空的Context。它永远不会被取消。它通常用来初始化和测试使用,作为一个顶层的context,也就是说一般我们创建的context都是基于Background。

TODO返回一个非空的Context。当不清楚要使用哪个上下文的时候可以使用TODO。

他们两个本质上都是emptyCtx结构体类型,是一个不可取消,没有设置截止时间,没有携带任何值的Context。

有了如上的根Context,那么是如何衍生更多的子Context的呢?这就要靠context包为我们提供的With系列的函数了。

通过这些函数,就创建了一颗Context树,树的每个节点都可以有任意多个子节点,节点层级可以有任意多个。

WithCancel函数,最常用的派生 context 方法。该方法接受一个父 context。父 context 可以是一个 background context 或其他 context。

WithDeadline函数,该方法会创建一个带有 deadline 的 context。当 deadline 到期后,该 context 以及该 context 的可能子 context 会受到 cancel 通知。另外,如果 deadline 前调用 cancelFunc 则会提前发送取消通知。

WithTimeout和WithDeadline基本上一样,这个表示是超时自动取消,是多少时间后自动取消Context的意思。

WithValue函数和取消Context无关,它是为了生成一个绑定了一个键值对数据的Context,这个绑定的数据可以通过Context.Value方法访问到,一般我们想要通过上下文来传递数据时,可以通过这个方法,如我们需要tarce追踪系统调用栈的时候。

使用Context的程序应遵循以下规则,以使各个包之间的接口保持一致:

1.不要将 Context 塞到结构体里。直接将 Context 类型作为函数的第一参数,而且一般都命名为 ctx。

2.不要向函数传入一个 nil 的 context,如果你实在不知道传什么,标准库给你准备好了一个 context:todo。

3.不要把本应该作为函数参数的类型塞到 context 中,context 存储的应该是一些共同的数据。例如:登陆的 session、cookie 等。

4.同一个 context 可能会被传递到多个 goroutine,别担心,context 是并发安全的。

Go切片数组深度解析

Go 中的分片数组,实际上有点类似于Java中的ArrayList,是一个可以扩展的数组,但是Go中的切片由比较灵活,它和数组很像,也是基于数组,所以在了解Go切片前我们先了解下数组。

数组简单描述就由相同类型元素组成的数据结构, 在创建初期就确定了长度,是不可变的。

但是Go的数组类型又和C与Java的数组类型不一样, NewArray 用于创建一个数组,从源码中可以看出最后返回的是 Array{}的指针,并不是第一个元素的指针,在Go中数组属于值类型,在进行传递时,采取的是值传递,通过拷贝整个数组。Go语言的数组是一种有序的struct。

Go 语言的数组有两种不同的创建方式,一种是显示的初始化,一种是隐式的初始化。

注意一定是使用 [...]T 进行创建,使用三个点的隐式创建,编译器会对数组的大小进行推导,只是Go提供的一种语法糖。

其次,Go中数组的类型,是由数值类型和长度两个一起确定的。[2]int 和 [3]int 不是同一个类型,不能进行传参和比较,把数组理解为类型和长度两个属性的结构体,其实就一目了然了。

Go中的数组属于值类型,通常应该存储于栈中,局部变量依然会根据逃逸分析确定存储栈还是堆中。

编译器对数组函数中做两种不同的优化:

在静态区完成赋值后复制到栈中。

总结起来,在不考虑逃逸分析的情况下,如果数组中元素的个数小于或者等于 4 个,那么所有的变量会直接在栈上初始化,如果数组元素大于 4 个,变量就会在静态存储区初始化然后拷贝到栈上。

由于数组是值类型,那么赋值和函数传参操作都会复制整个数组数据。

不管是赋值或函数传参,地址都不一致,发生了拷贝。如果数组的数据较大,则会消耗掉大量内存。那么为了减少拷贝我们可以主动的传递指针呀。

地址是一样的,不过传指针会有一个弊端,从打印结果可以看到,指针地址都是同一个,万一原数组的指针指向更改了,那么函数里面的指针指向都会跟着更改。

同样的我们将数组转换为切片,通过传递切片,地址是不一样的,数组值相同。

切片是引用传递,所以它们不需要使用额外的内存并且比使用数组更有效率。

所以,切片属于引用类型。

通过这种方式可以将数组转换为切片。

中间不加三个点就是切片,使用这种方式创建切片,实际上是先创建数组,然后再通过第一种方式创建。

使用make创建切片,就不光编译期了,make创建切片会涉及到运行期。1. 切片的大小和容量是否足够小;

切片是否发生了逃逸,最终在堆上初始化。如果切片小的话会先在栈或静态区进行创建。

切片有一个数组的指针,len是指切片的长度, cap指的是切片的容量。

cap是在初始化切片是生成的容量。

发现切片的结构体是数组的地址指针array unsafe.Pointer,而Go中数组的地址代表数组结构体的地址。

slice 中得到一块内存地址,array[0]或者unsafe.Pointer(array[0])。

也可以通过地址构造切片

nil切片:指的unsafe.Pointer 为nil

空切片:

创建的指针不为空,len和cap为空

当一个切片的容量满了,就需要扩容了。怎么扩,策略是什么?

如果原来数组切片的容量已经达到了最大值,再想扩容, Go 默认会先开一片内存区域,把原来的值拷贝过来,然后再执行 append() 操作。这种情况对现数组的地址和原数组地址不相同。

从上面结果我们可以看到,如果用 range 的方式去遍历一个切片,拿到的 Value 其实是切片里面的值拷贝,即浅拷贝。所以每次打印 Value 的地址都不变。

由于 Value 是值拷贝的,并非引用传递,所以直接改 Value 是达不到更改原切片值的目的的,需要通过 slice[index] 获取真实的地址。

Go 语言内存管理(三):逃逸分析

Go 语言较之 C 语言一个很大的优势就是自带 GC 功能,可 GC 并不是没有代价的。写 C 语言的时候,在一个函数内声明的变量,在函数退出后会自动释放掉,因为这些变量分配在栈上。如果你期望变量的数据可以在函数退出后仍然能被访问,就需要调用 malloc 方法在堆上申请内存,如果程序不再需要这块内存了,再调用 free 方法释放掉。Go 语言不需要你主动调用 malloc 来分配堆空间,编译器会自动分析,找出需要 malloc 的变量,使用堆内存。编译器的这个分析过程就叫做逃逸分析。

所以你在一个函数中通过 dict := make(map[string]int) 创建一个 map 变量,其背后的数据是放在栈空间上还是堆空间上,是不一定的。这要看编译器分析的结果。

可逃逸分析并不是百分百准确的,它有缺陷。有的时候你会发现有些变量其实在栈空间上分配完全没问题的,但编译后程序还是把这些数据放在了堆上。如果你了解 Go 语言编译器逃逸分析的机制,在写代码的时候就可以有意识地绕开这些缺陷,使你的程序更高效。

Go 语言虽然在内存管理方面降低了编程门槛,即使你不了解堆栈也能正常开发,但如果你要在性能上较真的话,还是要掌握这些基础知识。

这里不对堆内存和栈内存的区别做太多阐述。简单来说就是, 栈分配廉价,堆分配昂贵。 栈空间会随着一个函数的结束自动释放,堆空间需要时间 GC 模块不断地跟踪扫描回收。如果对这两个概念有些迷糊,建议阅读下面 2 个文章:

这里举一个小例子,来对比下堆栈的差别:

stack 函数中的变量 i 在函数退出会自动释放;而 heap 函数返回的是对变量 i 的引用,也就是说 heap() 退出后,表示变量 i 还要能被访问,它会自动被分配到堆空间上。

他们编译出来的代码如下:

逻辑的复杂度不言而喻,从上面的汇编中可看到, heap() 函数调用了 runtime.newobject() 方法,它会调用 mallocgc 方法从 mcache 上申请内存,申请的内部逻辑前面文章已经讲述过。堆内存分配不仅分配上逻辑比栈空间分配复杂,它最致命的是会带来很大的管理成本,Go 语言要消耗很多的计算资源对其进行标记回收(也就是 GC 成本)。

Go 编辑器会自动帮我们找出需要进行动态分配的变量,它是在编译时追踪一个变量的生命周期,如果能确认一个数据只在函数空间内访问,不会被外部使用,则使用栈空间,否则就要使用堆空间。

我们在 go build 编译代码时,可使用 -gcflags '-m' 参数来查看逃逸分析日志。

以上面的两个函数为例,编译的日志输出是:

日志中的 i escapes to heap 表示该变量数据逃逸到了堆上。

需要使用堆空间,所以逃逸,这没什么可争议的。但编译器有时会将 不需要 使用堆空间的变量,也逃逸掉。这里是容易出现性能问题的大坑。网上有很多相关文章,列举了一些导致逃逸情况,其实总结起来就一句话:

多级间接赋值容易导致逃逸 。

这里的多级间接指的是,对某个引用类对象中的引用类成员进行赋值。Go 语言中的引用类数据类型有 func , interface , slice , map , chan , *Type(指针) 。

记住公式 Data.Field = Value ,如果 Data , Field 都是引用类的数据类型,则会导致 Value 逃逸。这里的等号 = 不单单只赋值,也表示参数传递。

根据公式,我们假设一个变量 data 是以下几种类型,相应的可以得出结论:

下面给出一些实际的例子:

如果变量值是一个函数,函数的参数又是引用类型,则传递给它的参数都会逃逸。

上例中 te 的类型是 func(*int) ,属于引用类型,参数 *int 也是引用类型,则调用 te(j) 形成了为 te 的参数(成员) *int 赋值的现象,即 te.i = j 会导致逃逸。代码中其他几种调用都没有形成 多级间接赋值 情况。

同理,如果函数的参数类型是 slice , map 或 interface{} 都会导致参数逃逸。

匿名函数的调用也是一样的,它本质上也是一个函数变量。有兴趣的可以自己测试一下。

只要使用了 Interface 类型(不是 interafce{} ),那么赋值给它的变量一定会逃逸。因为 interfaceVariable.Method() 先是间接的定位到它的实际值,再调用实际值的同名方法,执行时实际值作为参数传递给方法。相当于 interfaceVariable.Method.this = realValue

向 channel 中发送数据,本质上就是为 channel 内部的成员赋值,就像给一个 slice 中的某一项赋值一样。所以 chan *Type , chan map[Type]Type , chan []Type , chan interface{} 类型都会导致发送到 channel 中的数据逃逸。

这本来也是情理之中的,发送给 channel 的数据是要与其他函数分享的,为了保证发送过去的指针依然可用,只能使用堆分配。

可变参数如 func(arg ...string) 实际与 func(arg []string) 是一样的,会增加一层访问路径。这也是 fmt.Sprintf 总是会使参数逃逸的原因。

例子非常多,这里不能一一列举,我们只需要记住分析方法就好,即,2 级或更多级的访问赋值会 容易 导致数据逃逸。这里加上 容易 二字是因为随着语言的发展,相信这些问题会被慢慢解决,但现阶段,这个可以作为我们分析逃逸现象的依据。

下面代码中包含 2 种很常规的写法,但他们却有着很大的性能差距,建议自己想下为什么。

Benchmark 和 pprof 给出的结果:

熟悉堆栈概念可以让我们更容易看透 Go 程序的性能问题,并进行优化。

多级间接赋值会导致 Go 编译器出现不必要的逃逸,在一些情况下可能我们只需要修改一下数据结构就会使性能有大幅提升。这也是很多人不推荐在 Go 中使用指针的原因,因为它会增加一级访问路径,而 map , slice , interface{} 等类型是不可避免要用到的,为了减少不必要的逃逸,只能拿指针开刀了。

大多数情况下,性能优化都会为程序带来一定的复杂度。建议实际项目中还是怎么方便怎么写,功能完成后通过性能分析找到瓶颈所在,再对局部进行优化。

【golang】内存逃逸常见情况和避免方式

因为如果变量的内存发生逃逸,它的生命周期就是不可知的,其会被分配到堆上,而堆上分配内存不能像栈一样会自动释放,为了解放程序员双手,专注于业务的实现,go实现了gc垃圾回收机制,但gc会影响程序运行性能,所以要尽量减少程序的gc操作。

1、在方法内把局部变量指针返回,被外部引用,其生命周期大于栈,则溢出。

2、发送指针或带有指针的值到channel,因为编译时候无法知道那个goroutine会在channel接受数据,编译器无法知道什么时候释放。

3、在一个切片上存储指针或带指针的值。比如[]*string,导致切片内容逃逸,其引用值一直在堆上。

4、因为切片的append导致超出容量,切片重新分配地址,切片背后的存储基于运行时的数据进行扩充,就会在堆上分配。

5、在interface类型上调用方法,在Interface调用方法是动态调度的,只有在运行时才知道。

1、go语言的接口类型方法调用是动态,因此不能在编译阶段确定,所有类型结构转换成接口的过程会涉及到内存逃逸发生,在频次访问较高的函数尽量调用接口。

2、不要盲目使用变量指针作为参数,虽然减少了复制,但变量逃逸的开销更大。

3、预先设定好slice长度,避免频繁超出容量,重新分配。

什么是逃逸分析?

在C语言中,可以使用malloc和free手动在堆上分配和回收内存。Go语言中,堆内存是通过垃圾回收机制自动管理的,无需开发者指定。那么,Go编译器怎么知道某个变量需要分配在栈上,还是堆上呢?编译器决定内存分配位置的方式,就称之为逃逸分析(escape analysis)。逃逸分析由编译器完成,作用于编译阶段。


当前名称:go语言之逃逸,go 指针逃逸
文章起源:http://cqcxhl.cn/article/dsgiioe.html

其他资讯

在线咨询
服务热线
服务热线:028-86922220
TOP