Golang问题解析(一)

记录自己的Golang学习过程

前言

主要是对之前的困惑的地方尝试解答,但并不完全准确,还是需要继续深入理解

1.map 是否并发安全?

map对象必须在使用之前初始化。如果不初始化就直接赋值的话,会出现panic异常。

map的类型是map[key],key类型必须是可比较的,不能是slice、map和函数值。

Go语言内建的map对象不是线程安全的,并发读写的时候运行时会有检查,遇到并发问题就会导致panic。

2.map 循环是有序的还是无序的?

遍历一个map对象的时候,迭代的元素的顺序是不确定的,无法保证两次遍历的顺序是一样的,也不能保证现在的顺序和插入的顺序一致。

如果想要按照key的顺序获取map的值,需要先取出所有的key进行排序,然后按照这个排序的key依次获取对应的值。

而如果想要保证元素有序,比如按照元素插入的顺序进行遍历,可以使用辅助的数据结构,比如orderedmap,来记录插入顺序。

3.怎么处理对map 进行并发访问?

加读写锁:扩展map,支持并发读写

分片加锁:更高效的并发map。尽量减少锁的粒度和锁的持有时间。

减少锁的粒度常用的方法就是分片(Shard)。由不同的线程去获取。Go知名的分片并发map的实现是orcaman/concurrent-map。它默认采用32个分片。

4.map 中删除一个 key,它的内存会释放么?

如果删除的元素是值类型,如int,float,bool,string,数组,struct,map的内存不会自动释放。

如果删除的元素是引用类型,如指针,slice,map,chan等,map的内存会自动释放,但释放的内存是子元素应用类型的内存占用。

将map设置为nil后,内存被回收

5.nil map 和空 map 有何不同?

nil map和空map基本一致,在操作上又不同。

都可以读取值,但是都是空值。

空map可以赋值,nil map不可以。

虽然都是空的,但是也可以使用delete进行删除操作。

6.nil切片(nil slice)和空切片(empty slice)有什么不同?

由于slice内置结构存在指针,因此不同的是指针是否被开辟空间。

nil slice 完全就是空未被初始化,内置指针数组未开辟空间,则 nil slice == nil 成立

empty slice 可以理解为空数据,已经开辟内存空间,内置指针数组已经开辟空间有指向内存地址,empty slice == nil 不成立,判空需要用

len(empty slice) == 0

7.函数调用需要传入结构体时,传指针还是值?怎么区分什么时候用哪种?

go里面没有引用类型,所有的函数传递都是值传递

像slice,map,channel由于其内置结构里存在指针,因此传递以上类型会被修改原先数据。

传值情况:不想改变原来数据,只需要数据进行使用。

传指针情况:想要改变原来数据或者想要高效率,则传递指针更高效。

8.Go 如何实现原子操作?

CompareAndSwap(CAS),go中的Cas操作,是借用了CPU提供的原子性指令来实现,在sync/atomic包中。

四大操作,Swap(交换),Add(增加或减少),Load(原子读取),Store(原子写入)。

9.原子操作与互斥锁的区别?

互斥锁是一种数据结构,用来让一个线程执行程序的关键部分,完成互斥的多个操作。

原子操作是无锁的,常常直接通过CPU指令直接实现。

原子操作中的CAS趋于乐观锁,CAS操作并不那么容易成功,需要判断,然后尝试处理。

可以把互斥锁理解为悲观锁,共享资源每次只给一个线程使用,其它线程阻塞,用完后再把资源转让给其它线程。

10.Mutex是悲观锁还是乐观锁?悲观锁、乐观锁是什么?

mutex互斥锁类似悲观锁,总是假设会有并发的操作要修改被操作的值,所以使用锁将相关操作放入到临界区加以保存。

CAS操作做法趋于乐观锁,总是假设被操作的值未曾改变(即与旧值相等),并一旦确认这个假设的真实性就立即进行值替换。

在被操作值被频繁变更的情况下,CAS操作并不那么容易成功所以需要不断进行尝试,直到成功为止。

11.Mutex有几种模式?

正常模式,锁的等待者会按照先进先出的顺序获取锁。

一旦Goroutine超过1ms没有获取到锁,它会切换到饥饿模式。

饥饿模式目的是保证互斥锁的公平性。

如果一个Goroutine获得了互斥锁并且它在队列末尾或者它等待的时间小于1ms,当前互斥锁就会切换到正常模式。

正常模式提供了更好的性能,而饥饿模式能避免Goroutine由于陷入等待无法获取锁而造成的高尾延迟。

12.怎么控制并发数?

  • WaitGroup位于sync包下,某任务需要多 goroutine 协同工作,每个 goroutine 只能做该任务的一部分,只有全部的 goroutine 都完成,任务才算是完成。

  • Channel+Select。定义一个全局变量,在其它地方通过修改这个变量进行通知,后台 goroutine 会不停的检查这个变量,如果发现变量发生了变化,即自行关闭。

Context:多层级groutine之间的信号传播(包括元数据传播,取消信号传播、超时控制等。依次退出所占的资源。

13.Go语言中的GC算法的实现?

标记清除(mark-sweep)算法是最常见的垃圾收集算法。分为mark和sweep

三色标记法的标记阶段结束后,应用程序的堆中不存在任何灰色对象。

  • 如果不应该被回收的对象却被回收了,这在内存管理中叫做“悬挂指针”,

即指针没有指向特定类型的合法对象。

14.GC 的触发时机?

  • 系统触发:运行时自行根据内置的条件,维护整个应用程序的可用性。

  • 手动触发:开发者在业务代码中自行调用 runtime.GC 方法来触发 GC 行为

系统触发主要存在三种场景:

  • gcTriggerHeap:当所分配的堆大小达到阈值(由控制器计算的触发堆的大小)时,将会触发。

  • gcTriggerTime:当距离上一个 GC 周期的时间超过一定时间时,将会触发。-时间周期以 runtime.forcegcperiod 变量为准,默认 2 分钟。

gcTriggerCycle:如果没有开启 GC,则启动 GC。

15.如何优雅的实现一个 goroutine 池?(划重点!!!)

使用goroutine和channel实现一个计算int64随机数各位数和的程序。

开启一个goroutine循环生成int64类型的随机数,发送到jobChan

开启24个goroutine从jobChan中取出随机数计算各位数的和,将结果发送到resultChan。

主goroutine从resultChan取出结果并打印到终端输出

限制生成个数,有缓冲区:

图1

16.Goroutine为什么会导致内存泄漏?

内存泄漏(Memory Leak)是指程序中己动态分配的堆内存由于某种原因程序未释放或无法释放,造成系统内存的浪费,导致程序运行速度减慢甚至系统崩溃等严重后果。

  • 我们创建的Goroutine没有在我们预期的时刻关闭,导致Goroutine的数量在服务端一直累积增加,最终影响到服务的性能。

  • Goroutine本身的堆栈大小是2KB,我们开启一个新的Goroutine,至少会占用2KB的内存大小。当长时间的累积,数量较大时,比如开启了100万个Goroutine,那么至少就会占用2GB的内存。

  • Goroutine中的变量若指向了堆内存区,那么,当该Goroutine未被销毁,系统会认为该部分内存还不能被垃圾回收,那么就可能会占用大量的堆区内存空间。

17.知道Golang的内存逃逸吗?什么情况下会发生内存逃逸?

  • Golang程序变量会携带有一组校验数据,用来证明它的整个生命周期是否在运行时完全可知。如果变量通过了这些校验,它就可以在栈上分配。否则就说它逃逸了,必须在堆上分配。完全可知才能在栈上分配。

  • 逃逸分析是编译器在静态编译的时候,分析对象的生命周期及引用情况来决定对象内存分配到堆上还是栈上,由于栈内存分配较堆快且栈内存回收较快(无需GC),编译器以此来优化程序性能。在函数中申请一个新的对象:

  • 如果分配在栈中,则函数执行结束后可自动将内存回收。

  • 如果分配在堆中,则函数执行结束后可交给GC进行处理。

逃逸策略:

   如果函数外部没有引用,则优先放到栈中;

   如果函数外部存在引用,则必定放到堆中。 
  • 指针逃逸

  • 栈空间不足逃逸

  • 动态类型逃逸

  • 闭包引用对象逃逸

总结:

栈上分配内存比在堆上分配内存有更高的效率

栈上分配的内存不需要GC处理

堆上分配的内存使用完毕会交给GC处理

逃逸分析的目的是决定分配地址是栈还是堆

逃逸分析在编译阶段进行

18.两数传递指针真的比传值的效率高吗?

我们知道传递指针可以减少底层值的复制,可以提高效率, 但是如果复制的数据量小,由于指针传递会产生逃逸,则可能会使用堆, 也可能增加 GC 的负担,所以传递指针不一定是高效的。

19.请简述 Go 是如何分配内存的?

内存空间包含两个重要的区域:栈(stack)和堆(heap)。

对于小对象(<=32kb),go runtime首先从,Cache开始,然后是Central,最后Heap。

对于大对象(>32KB),直接从堆中获取。

heap:全局根对象。负责向操作系统申请内存,管理由垃圾回收器收回的空闲 span 内存块。

central:从Heap 获取空闲 span,并按需要将其切分成 Object 块。Heap 管理着多个central对象,每个central负责处理一种等级的内存分配需求。

cache:运行行期,每个 cache 都与某个具体线程相绑定,实现无锁内存分配操作。其内部有个以等级为序号的数组,持有多个切分好的 span 对象。缺少空间时,向等级对应的 central 获取新的 span 即可。

20.struct结构体能不能比较?

结构体不可以比较,但是同一类型的结构体的值可以比较是否相等的(不可以比较大小):

结构体所有字段的值都相等,两个结构体才相等。

比较的两个结构体必须是相同类型才可以,也就是说他们字段的顺序、名称、类型、标签都相同才可以。而切片和字典不可以比较。

参考链接:

https://draveness.me/golang

https://blog.learngoprogramming.com

https://golang.org/doc/faq

https://github.com/golang/go/wiki/Articles

https://golang.org/doc/effective_go.html

Licensed under CC BY-NC-SA 4.0
comments powered by Disqus