Golang面试题

协程,线程,进程的区别

进程是具有一定独立功能的程序关于某个数据集合上的一次运行活动,进程是系统进行资源分配和调度的一个独立单位。每个进程都有自己的独立内存空间,不同进程通过进程间通信来通信。由于进程比较重量,占据独立的内存,所以上下文进程间的切换开销(栈、寄存器、虚拟内存、文件句柄等)比较大,但相对比较稳定安全。
线程是进程的一个实体,线程是内核态,而且是CPU调度和分派的基本单位,它是比进程更小的能独立运行的基本单位.线程自己基本上不拥有系统资源,只拥有一点在运行中必不可少的资源(如程序计数器,一组寄存器和栈),但是它可与同属一个进程的其他的线程共享进程所拥有的全部资源。线程间通信主要通过共享内存,上下文切换很快,资源开销较少,但相比进程不够稳定容易丢失数据。
协程是一种用户态的轻量级线程,协程的调度完全由用户控制。协程拥有自己的寄存器上下文和栈。协程调度切换时,将寄存器上下文和栈保存到其他地方,在切回来的时候,恢复先前保存的寄存器上下文和栈,直接操作栈则基本没有内核切换的开销,可以不加锁的访问全局变量,所以上下文的切换非常快。

GMP模型

G – Goroutine,Go协程,是参与调度与执行的最小单位
M – Machine,指的是系统级线程
P – Processor,指的是逻辑处理器,P关联了的本地可运行G的队列(也称为LRQ),最多可存放256个G。
1. Golang的线程模型采用的是混合型线程模型,线程与协程关系是N:M。
2. Golang混合型线程模型实现采用GMP模型进行调度,G是goroutine,是golang实现的协程,M是OS线程,P是逻辑处理器。
3. 每一个M都需要与一个P绑定,P拥有本地可运行G队列,M是执行G的单元,M获取可运行G流程是先从P的本地队列获取,若未获取到,则从其他P偷取过来(即work steal),若其他的P也没有则从全局G队列获取,若都未获取到,则M将处于自旋状态,并不会销毁。
4. 当执行G时候,发生通道阻塞等用户级别阻塞时候,此时M不会阻塞,M会继续寻找其他可运行的G,当阻塞的G恢复之后,重新进入P的队列等待执行,若G进行系统调用时候,会阻塞M,此时P会和M解绑(即hand off),并寻找新的空闲的M。若没有空闲的就会创建一个新的M。
5. Work Steal和Hand Off保证了线程的高效利用。

能说说uintptr和unsafe.Pointer的区别吗?

unsafe.Pointer只是单纯的通用指针类型,用于转换不同类型指针,它不可以参与指针运算;
而uintptr是用于指针运算的,GC 不把 uintptr 当指针,也就是说 uintptr 无法持有对象, uintptr 类型的目标会被回收;
unsafe.Pointer 可以和 普通指针 进行相互转换;
unsafe.Pointer 可以和 uintptr 进行相互转换。

sync.Pool的适用场景

当多个 goroutine 都需要创建同⼀个对象的时候,如果 goroutine 数过多,导致对象的创建数⽬剧增,进⽽导致 GC 压⼒增大。形成 “并发⼤-占⽤内存⼤-GC 缓慢-处理并发能⼒降低-并发更⼤”这样的恶性循环。在这个时候,需要有⼀个对象池,每个 goroutine 不再⾃⼰单独创建对象,⽽是从对象池中获取出⼀个对象(如果池中已经有的话)。

go语言是面向对象的吗

go语言既不是面向对象,也不是面向过程,因为Golang并没有明显的倾向,而是更倾向于让编程者去考虑该怎么去用它,也许它的特色就是灵活,编程者可以用它实现面向对象,但它本身不支持面向对象的语义。

什么是鸭子类型

如果某个东西像鸭子一样走,像鸭子一样嘎嘎叫,那它一定是鸭子
Go语言的鸭子类型
Go语言通过接口的方式实现Duck Typing。不像其他动态语言那样,只能在运行时才能检查到类型不匹配,也不像大多数静态语言那样,需要显示声明实现哪个接口,Go语言接口的独特之处在于它是隐式实现。
接口用于定义对象的一系列行为,表明对象具备某种能力,但是并不规定对象如何实现,空接口 interface{} 没有定义任何行为,所以 go 里面只要是个东西都实现了空接口,难怪 interface{} 可以表示世间万物,要规范接口的行为只需添加方法签名就可以,比如定义一个能 walk 也能 swim 的东西,这个东西可能是个鸭子,也可能是个人,只要实现了这两个方法的类型就实现了该接口。

map不初始化使用会怎么样

  1. 可以对未初始化的map进行取值,但取出来的东西是空
  2. 不能对未初始化的map进行赋值,这样将会抛出一个异常:panic: assignment to entry in nil map
    未初始化的map就是nil map 例如 var m1 map[int]int; 初始化未赋值的为空map, 例如 m2 := map[int]int{}。通过fmt打印map时,nil map和空map结果是一样的,都为map[]。所以,这个时候别断定map是空还是nil,而应该通过map == nil来判断。

map的iterator是否安全?能不能一边delete一边遍历?

在同一个协程内边遍历边删除,并不会检测到同时读写,理论上是可以这样做的。但是,遍历的结果就可能不会是相同的了,有可能遍历结果集中包含了删除的 key,也有可能不包含,这取决于删除 key 的时间:是在遍历到 key 所在的 bucket 时刻前或者后。如果是多个协成同时写会报错:fatal error: concurrent map writes

map触发扩容的时机,满足什么条件时扩容?

  • 装载因子超过阈值,源码里定义的阈值是 6.5。
  • overflow 的 bucket 数量过多:当 B 小于 15,也就是 bucket 总数 2^B 小于 2^15 时,如果 overflow 的 bucket 数量超过 2^B;当 B >= 15,也就是 bucket 总数 2^B 大于等于 2^15,如果 overflow 的 bucket 数量超过 2^15。

线程安全的map怎么实现

读写锁、分片锁和 sync.map
  1. 加读写锁:借助 RWMutex和map包装成一个struct,然后分别实现get,set,each,len,delete方法,方法中的操作使用锁来控制
  2. 分片锁:在第一种方法的基础上优化,将map分成多个分片。从而提高并发能力
  3. sync.map是利用go内置的方法,sync.map 的整体思路就是用两个数据结构(只读的 read 和可写的 dirty)尽量将读写操作分开,来减少锁对性能的影响。

sync.map 的优缺点和使用场景

  • 优点:通过读写分离,降低锁时间来提高效率;
  • 缺点:不适用于大量写的场景,这样会导致read map读不到数据而进一步加锁读取,同时dirty map也会一直晋升为read map,整体性能较差。
    适用场景:大量读,少量写。存在大量写的场景可以考虑map+metux

struct可以作为map的key么

struct必须是可比较的,才能作为key,否则编译时报错:invalid map key type xxx

Go中的map如何实现顺序读取

通过sort中的排序包进行对map中的key进行排序.

如何判断 map 中是否包含某个 key ?

使用comma ok语法

Golang Map底层实现

go的map使用hash表实现的,其中最主要的数据结构为hmap和bmap,hmap中存储的map的数量数据bucktes内存地址等信息,桶的结构体 runtime.bmap 在 Go 语言源代码中的定义只包含一个简单的 tophash 字段,tophash 存储了键的哈希的高 8 位,通过比较不同键的哈希的高 8 位可以减少访问键值对次数以提高性能:
如何解决hash冲突
使用类似链表法(内存上的连续空间):
go的map采用buckets bmap存储数据,每个bucket
每一个bucket 里面存放的是长度为8的数组, 8 个 key 和 8 个 value ,bucket 里面的溢出指针又指向另外一个bucket,用类似链表的方式将他们连接起来
扩容
• 增量扩容
• 等量扩容

当一个新的元素要添加进map的时候,都会检查是否需要扩容,扩容的触发条件就有 2 个:
• 当负载因子 > 6.5的时候,也就是平均下来,每个bucket存储的键值对达到6.5个的时候,就会扩容
• 当溢出的数量 > 2^15 的时候,也会扩容
这里说一下啥是负载因子呢?
负载因子 = 键的数量 / bucket 数量

啥是增量扩容
就是当负载因子过大,也就是哈希冲突严重的时候,会做如下 2 个步骤
• 新建一个 bucket,新的bucket 是原 bucket 长度的 double
• 再将原来的 bucket 数据 搬迁到 新的 bucket 中
    采用的逐步搬迁的方法,每次访问map,都会触发一次迁移
啥是等量扩容
等量扩容,等量这个名字感觉像是,扩充的容量和原来的容量是一一对齐的,也就是说成倍增长
其实不然,等量扩容,其实buckets数量没有变化
只是对bucket的键值对重新排布,整理的更加有条理,让其使用率更加的高
例如 等量扩容后,对于一些 溢出的 buckets,且里面的内容都是空的键值对,这时,就可以把这些降低效率且无效的buckets清理掉
这样,是提高buckets效率的一种有效方式

字符串不能改,那转成数组能改吗,怎么改

Go 语言中的字符串和其他高级语言一样,默认是不可变的
字符串不可变有很多好处,如天生线程安全,大家使用的都是只读对象,无须加锁;再者,方便内存共享,而不必使用写时复制(Copy On Write)等技术;字符串 hash 值也只需要制作一份。
修改字符串时,可以将字符串转换为 []byte 进行修改,[]byte 和 string 可以通过强制类型转换互转

字符串打印时,%v 和 %+v 的区别

%v 只输出所有的值
%+v 先输出字段类型,再输出该字段的值
%#v 先输出结构体名字值,再输出结构体(字段类型+字段的值)

怎么判断一个数组是否已经排序

使用sort.IsSorted()方法, 但是需要实现sort包的接口方法:Len,Less,Swap

array和slice的区别

• 数组长度不能改变,初始化后长度就是固定的;切片的长度是不固定的,可以追加元素,在追加时可能使切片的容量增大。
• 结构不同,数组是一串固定数据,切片描述的是截取数组的一部分数据,从概念上说是一个结构体。
• 初始化方式不同,另外在声明时的时候:声明数组时,方括号内写明了数组的长度或使用...自动计算长度,而声明slice时,方括号内没有任何字符。
• unsafe.sizeof的取值不同,unsafe.sizeof(slice)返回的大小是切片的描述符,不管slice里的元素有多少,返回的数据都是24字节。unsafe.sizeof(arr)的值是在随着arr的元素的个数的增加而增加,是数组所存储的数据内存的大小。
• 函数调用时的传递方式不同,数组按值传递,slice按引用传递。

零切片、空切片、nil切片是什么

「零切片」其实并不是什么特殊的切片,它只是表示底层数组的二进制内容都是零。例如:var s = make([]int, 10)
「nil切片」Data值指向的地址不存在的切片。例如:var s1 []int 
「空切片」Data值指向的地址为固定的zero数组。例如:s1 := make([]int,0)或者 s1 := []int{}

slice深拷贝和浅拷贝

浅拷贝:目的切片和源切片指向同一个底层数组,任何一个数组元素改变,都会同时影响两个数组
深拷贝: 目的切片和源切片指向不同的底层数组,任何一个数组元素改变都不影响另外一个。

如何判断 2 个字符串切片(slice) 是相等的

来自reflect包的DeepEqual函数可以对两个值进行深度相等判断。DeepEqual函数使用内建的=\=比较操作符对基础类型进行相等判断,对于复合类型则递归该变量的每个基础类型然后做类似的比较判断。因为它可以工作在任意的类型上,甚至对于一些不支持==操作运算符的类型也可以工作。
尽管DeepEqual函数很方便,而且可以支持任意的数据类型,但是它也有不足之处。例如,它将一个nil值的map和非nil值但是空的map视作不相等,同样nil值的slice 和非nil但是空的slice也视作不相等。

JSON 标准库对 nil slice 和 空 slice 的处理是一致的吗?

Go的JSON 标准库对 nil slice 和 空 slice 的处理是不一致.nil返回 null, 空返回 []

Go的Slice如何扩容

一般情况是:slice扩容,cap不够1024的,直接翻倍;cap超过1024的,新cap变为老cap的1.25倍。特殊情况下和数据类型有关系,扩容还涉及到内存对齐,不一定是double的关系

空结构体 struct{} 的用途

1. 实现集合set
2. 不发送数据的信道(channel)
3. 仅包含方法的结构体

Go的Struct能不能比较

不同类型的 struct 之间不能进行比较,编译期就会报错
同类型的 struct 也分为两种情况,
  • struct 的所有成员都是可以比较的,则该 strcut 的不同实例可以比较
  • struct 中含有不可比较的成员(如 Slice),则该 struct 不可以比较, 但是可以使用反射DeepEqual比较

make和new什么区别

new 函数只接受一个参数,这个参数是一个类型,并且返回一个指向该类型内存地址的指针。同时 new 函数会把分配的内存置为零,也就是类型的零值。
make 也是用于内存分配的,但是和 new 不同,它只用于 chan、map 以及 slice 的内存创建,而且它返回的类型就是这三个类型本身,而不是他们的指针类型,因为这三种类型就是引用类型,所以就没有必要返回他们的指针了。
new 分配的空间被清零。make 分配空间后,会进行初始化;

什么是内存逃逸,什么是逃逸分析,逃逸分析的作用,如何避免内存逃逸?

什么是内存逃逸
在一段程序中,每一个函数都会有自己的内存区域存放自己的局部变量、返回地址等,这些内存会由编译器在栈中进行分配,每一个函数都会分配一个栈桢,在函数运行结束后进行销毁,但是有些变量我们想在函数运行结束后仍然使用它,那么就需要把这个变量在堆上分配,这种从"栈"上逃逸到"堆"上的现象就成为内存逃逸。
什么是内存逃逸分析
在编译器优化中,内存逃逸用来动态确定指针的作用域范围。如果子程序分配了一个对象并返回一个指向它的指针,则可以根据返回的指针来访问对象,这时对象不能直接存放在栈上。如果指针存在在全局变量或其他数据结构中,而这些数据结构又会在当前过程中逃逸,则他们可以发送逃逸。逃逸分析可以确定指针对象存储的位置,以及是否可以证明指针的生命周期仅限于当前过程或线程。
为什么要进行逃逸分析
Go中有垃圾回收机制帮助我们自动回收不用的内存,让我们可以专注于业务,高效地编写代码。逃逸分析的作用是合理分配变量在该去的地方,即“找准自己的位置”。就算用new申请内存,如果退出函数后发现不在没用,会把它分配到栈上。毕竟,堆栈上的内存分配比堆上的内存分配要快得多。相反,即使你只是表面上的一个普通变量,经过escape分析,发现退出函数之后还有其他引用,会将其分配到堆中。真正做到“按需分配”。如果将变量分配在堆上,堆不会像堆栈一样自动清理。会导致Go频繁做垃圾回收,垃圾回收会占用大量的系统开销。
如何避免内存逃逸
  • 对于小型的数据,使用传值而不是传指针,避免内存逃逸。
  • 避免使用长度不固定的slice切片,在编译期无法确定切片长度,只能将切片使用堆分配。
    interface调用方法会发生内存逃逸,在热点代码片段,谨慎使用。

内存碎片化问题

golang采用类似tcMalloc算法,减少了内存碎片的问题:可以归结为四个字:按需分配。go将内存块分为大小不同的67种,然后再把这67种大内存块,逐个分为小块(可以近似理解为大小不同的相当于page)称之为span(连续的page),在go语言中就是上文提及的mspan。

为什么gc会让程序变慢

  • GC占用CPU资源
  • 辅助标记 mutator assists
    在分析的一开始也提到了一些关于 mutator assists 的作用,主要是为了防止 heap 增速太快, 在GC 执行的过程中如果同时运行的 G 分配了内存, 那么这个 G 会被要求辅助 GC 做一部分的工作,它遵循一条非常简单并且朴实的原则,分配多少内存就需要完成多少标记任务。

Golang的内存模型,为什么小对象多了会造成gc压力。

通常小对象过多会导致GC三色法消耗过多的CPU。优化思路是,减少对象分配.

Golang 垃圾回收机制

标记-清除法(mark and sweep)
原始的标记清楚法分为两个步骤:
    1. 标记。先STP(Stop The World),暂停整个程序的全部运行线程,将被引用的对象打上标记
    2. 清除没有被打标机的对象,即回收内存资源,然后恢复运行线程。
这样做有个很大的问题就是要通过STW保证GC期间标记对象的状态不能变化,整个程序都要暂停掉,在外部看来程序就会卡顿。

三色标记法
三色标记法是对标记阶段的改进,原理如下:
    1. 初始状态所有对象都是白色。
    2. 从root根出发扫描所有根对象(下图a,b),将他们引用的对象标记为灰色(图中A,B)
    3. 分析灰色对象是否引用了其他对象。如果没有引用其它对象则将该灰色对象标记为黑色(上图中A);如果有引用则将它变为黑色的同时将它引用的对象也变为灰色(上图中B引用了D)
    4. 重复步骤3,直到灰色对象队列为空。此时白色对象即为垃圾,进行回收。

三色标记法的并发性问题
    假设三色标记法和用户程序并发执行,那么下列两个条件同时满足就可能出现错误回收非垃圾对象的问题:
    • 条件1:某一黑色对象引用白色对象
    • 条件2:对于某个白色对象,所有和它存在可达关系的灰色对象丢失了访问它的可达路径
    简单证明一下:如果条件1不满足,那么任何不该被回收的白色对象都能和至少一个灰色对象存在“可达”路径,因此不会有白色对象被遗漏;如果条件2不满足,那么对于某一个白色对象,即使它被黑色对象引用,但至少存在一个和它存在可达关系的灰色对象,因此这个白色对象也不会被回收。
    一种最简单解决三色标记并发问题的方法是停止所有的赋值器线程,保证标记过程不受干扰,即垃圾回收器中常提到的STW, stop the world方法。另外一种思路就是使用赋值器屏障技术使得赋值器在进行指针写操作时同步垃圾回收器,保证不破坏弱三色不变性(见下文)。

读写屏障技术
    • 强三色不变性:黑色对象永远不会指向白色对象
    • 弱三色不变性:黑色对象指向的白色对象至少包含一条由灰色对象经过白色对象的可达路径

    GC中使用的内存读写屏障技术指的是编译器会在编译期间生成一段代码,该代码在运行期间用户读取、创建或更新对象指针时会拦截内存读写操作,相当于一个hook调用,根据hook时机不同可分为不同的屏障技术。由于读屏障Read barrier技术需要在读操作中插入代码片段从而影响用户程序性能,所以一般使用写屏障技术来保证三色标记的稳健性。

    两种写屏障的劣势:
    • Dijkstra插入写屏障:一轮标记结束后需要STW重新扫描栈上对象
    • Yuasa删除写屏障:回收精度低,在垃圾回收开始前使用STW扫描所有GC Root对象形成初始快照,用户程序Mutator从灰色/白色对象中删除白色指针时会将下游对象标记为灰色,相当于保护了所有初始快照中的白色对象不被删除

混合写屏障
    在go v1.8引入混合写屏障hybrid write barrier之前,由于GC Root对象包括了栈对象,如果运行时在所有GC Root对象上开启插入写屏障意味着需要在数量庞大的Goroutine的栈上都开启Dijkstra写屏障从而严重影响用户程序的性能。之前的做法是Mark阶段(golang垃圾回收使用的是标记-清除法)结束后暂停整个程序,对栈上对象重新进行三色标记法。

    混合写屏障逻辑如下:
    • GC开始时将栈上所有对象标记为黑色,无须STW
    • GC期间在栈上创建的新对象均标记为黑色
    • 将被删除的下游对象标记为灰色
    将被添加的下游对象标记为灰色

辅助标记 mutator assists
在分析的一开始也提到了一些关于 mutator assists 的作用,主要是为了防止 heap 增速太快, 在GC 执行的过程中如果同时运行的 G 分配了内存, 那么这个 G 会被要求辅助 GC 做一部分的工作,它遵循一条非常简单并且朴实的原则,分配多少内存就需要完成多少标记任务。

GC调优常见方法
• 尽量使用小数据类型,比如使用int8代替int。
• 少使用+连接string:go语言中string是一个只读类型,针对string的每一个操作都会创建一个新的string。大量小文本拼接时优先使用strings.Join,大量大文本拼接时使用bytes.Buffer

GC的触发条件

主动触发(手动触发),通过调用runtime.GC() 来触发GC,此调用阻塞式地等待当前GC运行完毕.
被动触发,分为两种方式:
  • a. 达到定时时间:当超过两分钟没有产生任何GC时,强制触发 GC.
  • b. 超过阈值:使用步调(Pacing)算法,其核心思想是控制内存增长的比例,当前内存分配达到一定比例则触发

在Go函数中为什么会发生内存泄露

  • 预期能被快速释放的内存因被根对象引用而没有得到迅速释放.
    当有一个全局对象时,可能不经意间将某个变量附着在其上,且忽略的将其进行释放,则该内存永远不会得到释放。
  • goroutine 泄漏:Goroutine 作为一种逻辑上理解的轻量级线程,需要维护执行用户代码的上下文信息。在运行过程中也需要消耗一定的内存来保存这类信息,而这些内存在目前版本的 Go 中是不会被释放的。因此,如果一个程序持续不断地产生新的 goroutine、且不结束已经创建的 goroutine 并复用这部分内存,就会造成内存泄漏的现象.

什么是协程泄露(Goroutine Leak)?

协程泄露是指协程创建后,长时间得不到释放,并且还在不断地创建新的协程,最终导致内存耗尽,程序崩溃。
常见的导致协程泄露的场景有以下几种:
• 缺少接收器,导致发送阻塞
这个例子中,每执行一次 query,则启动1000个协程向信道 ch 发送数字 0,但只接收了一次,导致 999 个协程被阻塞,不能退出。
• 缺少发送器,导致接收阻塞
那同样的,如果启动 1000 个协程接收信道的信息,但信道并不会发送那么多次的信息,也会导致接收协程被阻塞,不能退出。
• 死锁(dead lock)
两个或两个以上的协程在执行过程中,由于竞争资源或者由于彼此通信而造成阻塞,这种情况下,也会导致协程被阻塞,不能退出。
• 无限循环(infinite loops)
这个例子中,为了避免网络等问题,采用了无限重试的方式,发送 HTTP 请求,直到获取到数据。那如果 HTTP 服务宕机,永远不可达,导致协程不能退出,发生泄漏。

内存分配的思想和规则

全局变量:引用类型的分配在堆上,值类型的分配在栈上。
局部变量:一般分配在栈上。如果局部变量太大,则分配在堆上。如果函数执行完,仍然有外部引用此局部变量,则分配在堆上。

什么是逃逸

把本该分配在栈上的变量分配到了堆,则发生了逃逸。

变量空间回收规则

分配在栈中的变量,在函数执行结束后由系统将内存回收,是安全的;如果分配在堆中,则函数执行结束由GC(垃圾回收)处理。所有的GC都是针对堆的

golang内存分配机制

内存管理组件
内存分配由内存分配器完成。分配器由3种组件构成:mcache, mcentral, mheap。
mcache:每个工作线程都会绑定一个mcache,本地缓存可用的mspan资源,这样就可以直接给Goroutine分配,因为不存在多个Goroutine竞争的情况,所以不会消耗锁资源。
mcentral:为所有mcache提供切分好的mspan资源。每个central保存一种特定大小的全局mspan列表,包括已分配出去的和未分配出去的。 每个mcentral对应一种mspan,而mspan的种类导致它分割的object大小不同。当工作线程的mcache中没有合适(也就是特定大小的)的mspan时就会从mcentral获取。
mheap:代表Go程序持有的所有堆空间,Go程序使用一个mheap的全局对象_mheap来管理堆内存。

分配流程
Go的内存分配器在分配对象时,根据对象的大小,分成三类:小对象(小于等于16B)、一般对象(大于16B,小于等于32KB)、大对象(大于32KB)。
大体上的分配流程:
    ○ 32KB 的对象,直接从mheap上分配;
    • <=16B 的对象使用mcache的tiny分配器分配;
    • (16B,32KB] 的对象,首先计算对象的规格大小,然后使用mcache中相应规格大小的mspan分配;
    • 如果mcache没有相应规格大小的mspan,则向mcentral申请
    • 如果mcentral没有相应规格大小的mspan,则向mheap申请
    • 如果mheap中也没有合适大小的mspan,则向操作系统申请

• Go在程序启动时,会向操作系统申请一大块内存,之后自行管理。
• Go内存管理的基本单元是mspan,它由若干个页组成,每种mspan可以分配特定大小的object。
• mcache, mcentral, mheap是Go内存管理的三大组件,层层递进。mcache管理线程在本地缓存的mspan;mcentral管理全局的mspan供所有线程使用;mheap管理Go的所有动态分配内存。
极小对象会分配在一个object中,以节省资源,使用tiny分配器分配内存;一般小对象通过mspan分配内存;大对象则直接由mheap分配内存。

golang GC优化

  1. 减少GC执行次数(降低GC触发条件); 比如:用小类型(int8代替int),sync.pool复用变量
  2. 减少GC扫描工作量:小对象换大对象

对已经关闭的的chan进行读写,会怎么样?为什么?

对已经关闭的channel进行读:如果有未读数据,可以正常读取。没有未读数据,读到零值和false
对已经关闭的channel进行写:会panic, 源码这么实现的

对未初始化的的chan进行读写,会怎么样?为什么?

读写未初始化的chan都会阻塞

什么是channel,为什么它可以做到线程安全?

Golang的Channel,发送一个数据到Channel和从Channel接收一个数据都是原子性的。
Go的设计思想就是, 不要通过共享内存来通信,而是通过通信来共享内存,前者就是传统的加锁,后者就是Channel。也就是说,设计Channel的主要目的就是在多任务间传递数据的,本身就是安全的。

Goroutine和Channel的作用分别是什么

在Golang中channel则是goroutinues之间进行通信的渠道。可以把channel形象比喻为工厂里的传送带,一头的生产者goroutine往传输带放东西,另一头的消费者goroutinue则从输送带取东西。channel实际上是一个有类型的消息队列,遵循先进先出的特点。

Channel是同步的还是异步的

无缓冲的channel是同步的,而有缓冲的channel是非同步的

无缓冲的 channel 和有缓冲的 channel 的区别?

一个是同步的 一个是非同步的。
  • 无缓冲: 不仅仅是向通道放数据,而是一直要等有别的协程准备好接收,那么数据才会发送过去,要不然就一直阻塞着。
  • 有缓冲: 一般情况不会阻塞,只有当缓冲池满的时候,才会阻塞。

go语言的channel特性?

会panic的几种情况
    1.向已经关闭的channel发送数据
    2.关闭已经关闭的channel
    3.关闭未初始化的nil channel
会阻塞的情况:
    1. 从未初始化nil channel中读数据
    2. 向未初始化nil channel中发数据
    3.在没有读取的groutine时,向无缓冲channel发数据
    4.在没有数据时,从无缓冲channel读数据
返回零值:
从已经关闭的channe接收数据

Golang有哪些方式安全读写共享变量?

加Mutex锁
Golang中Goroutine 可以通过 Channel 进行安全读写共享变量,还可以通过原子性操作进行.

分布式锁实现原理和实现方法

分布式锁
普通进程锁的调用者只在该进程中(或该进程的线程中),因此较为容易进行资源使用协调。在分布式环境中,不同机器的不同进程会对同一个资源进行使用/争夺,那么如何对资源的使用进行协调呢?这时就需要分布式锁来进行进程间的协调,以实现同一时刻只能有一个进程占有该资源。
分布式锁,是控制分布式系统之间同步访问共享资源的一种方式。在分布式系统中,常常需要协调他们的动作。如果不同的系统或是同一个系统的不同主机之间共享了一个或一组资源,那么访问这些资源的时候,往往需要互斥来防止彼此干扰来保证一致性,在这种情况下,便需要使用到分布式锁。——《维基百科》
分布式锁的特点
    • 原子性:同一时刻,只能有一个机器的一个线程得到锁;
    • 可重入性:同一对象(如线程、类)可以重复、递归调用该锁而不发生死锁;
    • 可阻塞:在没有获得锁之前,只能阻塞等待直至获得锁;
    • 高可用:哪怕发生程序故障、机器损坏,锁仍然能够得到被获取、被释放;
    • 高性能:获取、释放锁的操作消耗小。

方式:
    • 数据库实现分布式锁一般有两种方式:使用唯一索引或者主键;使用数据库自带的锁进行。
    • 使用redis set with ex nx
为了提高可用性,redis的作者提倡使用五个甚至更多的redis节点,使用上述方法的一种来获取/释放锁,当成功获取到三个或者三个以上节点的锁,则认为成功持有锁。由于必须获取到5个节点中的3个以上,所以可能出现获取锁冲突,即大家都获得了1-2把锁,结果谁也不能获取到锁,针对这种情况可以随机等待一段时间后再重新尝试获得锁。

Go中的锁有哪些

互斥锁,读写锁,sync.Map的安全的锁.

go语言的同步锁和读写锁

同步锁:
    Go 语言包中的 sync 包提供了两种锁类型:sync.Mutex 和 sync.RWMutex,前者是互斥锁,后者是读写锁。
    互斥锁是传统的并发程序对共享资源进行访问控制的主要手段,在 Go 中,似乎更推崇由 channel 来实现资源共享和通信。它由标准库代码包 sync 中的 Mutex 结构体类型代表。只有两个公开方法:调用 Lock()获得锁,调用 unlock()释放锁。
    使用 Lock () 加锁后,不能再继续对其加锁(同一个 goroutine 中,即:同步调用),否则会 panic。只有在 unlock () 之后才能再次 Lock ()。异步调用 Lock (),是正当的锁竞争,当然不会有 panic 了。适用于读写不确定场景,即读写次数没有明显的区别,并且只允许只有一个读或者写的场景,所以该锁也叫做全局锁。
    func (m *Mutex) Unlock () 用于解锁 m,如果在使用 Unlock () 前未加锁,就会引起一个运行错误。已经锁定的 Mutex 并不与特定的 goroutine 相关联,这样可以利用一个 goroutine 对其加锁,再利用其他 goroutine 对其解锁。
    建议:同一个互斥锁的成对锁定和解锁操作放在同一层次的代码块中。

读写锁:
    读写锁是分别针对读操作和写操作进行锁定和解锁操作的互斥锁。在 Go 语言中,读写锁由结构体类型 sync.RWMutex 代表。
    基本遵循原则:
        写锁定情况下,对读写锁进行读锁定或者写锁定,都将阻塞;而且读锁与写锁之间是互斥的;
        读锁定情况下,对读写锁进行写锁定,将阻塞;加读锁时不会阻塞;
        对未被锁定的读写锁进行解锁,会引发 Panic;
        与互斥锁类似,sync.RWMutex 类型的零值就已经是立即可用的读写锁了。在此类型的方法集合中包含了两对方法

go所使用的CSP并发模型.

Golang的CSP并发模型,是通过 Goroutine 和 Channel 来实现的。

Golang 中常用的并发模型?

通过channel通知实现并发控制
通过sync包中的WaitGroup实现并发控制
在Go 1.7 以后引进的强大的Context上下文,实现并发控制.

Golang的方法有什么特别之处

函数的定义声明没有接收者。
方法的声明和函数类似,他们的区别是:方法在定义的时候,会在func和方法名之间增加一个参数,这个参数就是接收者,这样我们定义的这个方法就和接收者绑定在了一起,称之为这个接收者的方法。
Go语言里有两种类型的接收者:值接收者和指针接收者。使用值类型接收者定义的方法,在调用的时候,使用的其实是值接收者的一个副本,所以对该值的任何操作,不会影响原来的类型变量。-------相当于形式参数。
如果我们使用一个指针作为接收者,那么就会其作用了,因为指针接收者传递的是一个指向原值指针的副本,指针的副本,指向的还是原来类型的值,所以修改时,同时也会影响原来类型变量的值。

go语言中的引用类型包含哪些

Golang的引用类型包括 slice、map 和 channel

哪个类型可以使用 cap () 函数?

slice,array,channel

defer 的执行顺序

1. 多个defer的执行顺序为“后进先出”;
2. defer、return、返回值三者的执行逻辑应该是:return最先执行,return负责将结果写入返回值中;接着defer开始执行一些收尾工作;最后函数携带当前返回值退出。
发生 panic 后,会先执行 defer

Go 语言中如何表示枚举值(enums)?

Go 语言没有 enum 关键字的,通过使用 const & iota 可以实现枚举的能力。

Go 支持默认参数或可选参数吗

Go中的函数是不支持带默认值的可选参数的。这是Go语言的设计者为了保证代码可读性特意抛弃的功能。那么我们真得无法在Go编程中声明带有默认参数的函数吗?

事实上,我们可以利用变长参数个数函数这一特性来(不是十分完美地)模拟默认参数


comma ok的作用

清晰的map访问:判断map中key是否存在
安全的类型断言: 解决无法通过类型断言转换而导致的panic。
channel receive:   如果因为ch关闭而得到一个“零值”,ok会是false。

Go 有异常类型吗?

没有异常类型,只有错误类型(Error)

Go函数返回局部变量的指针是否安全

在 Go 中是安全的,Go 编译器将会对每个局部变量进行逃逸分析。如果发现局部变量的作用域超出该函数,则不会将内存分配在栈上,而是分配在堆上

Go主协程如何等其余协程完再操作

Go提供了更简单的方法——使用sync.WaitGroup。WaitGroup,就是用来等待一组操作完成的。WaitGroup内部实现了一个计数器,用来记录未完成的操作个数.
它提供了三个方法,Add()用来添加计数。Done()用来在操作结束时调用,使计数减一。Wait()用来等待所有的操作结束,即计数变为0,该函数会在计数不为0时等待,在计数为0时立即返回。

Context包的用途是什么

在 Go http包的Server中,每一个请求在都有一个对应的 goroutine 去处理。请求处理函数通常会启动额外的 goroutine 用来访问后端服务,比如数据库和RPC服务。用来处理一个请求的 goroutine 通常需要访问一些与请求特定的数据,比如终端用户的身份认证信息、验证相关的token、请求的截止时间。 当一个请求被取消或超时时,所有用来处理该请求的 goroutine 都应该迅速退出,然后系统才能释放这些 goroutine 占用的资源。
在Google 内部,我们开发了 Context 包,专门用来简化 对于处理单个请求的多个 goroutine 之间与请求域的数据、取消信号、截止时间等相关操作,这些操作可能涉及多个 API 调用。
Context中的方法:
    • Done会返回一个channel,当该context被取消的时候,该channel会被关闭,同时对应的使用该context的routine也应该结束并返回。
    • Context中的方法是协程安全的,这也就代表了在父routine中创建的context,可以传递给任意数量的routine并让他们同时访问。
    • Deadline会返回一个超时时间,routine获得了超时时间后,可以对某些io操作设定超时时间。
    • Value可以让routine共享一些数据,当然获得数据是协程安全的。

Go的select可以用于什么和实现原理

作用:select 的用法与 switch 语言非常类似,由 select 开始一个新的选择块,每个选择条件由 case 语句来描述。与 switch 语句相比,select 有比较多的限制,其中最大的一条限制就是每个 case 语句里必须是一个 IO 操作。主要用于一些IO问题导致的阻塞和超时的问题

Go的defer原理是什么

• 什么是defer
    • defer是go语言提供的一种用于注册延迟调用的机制:让函数或者语句在当前函数执行完毕(包括return正常结束或者panic导致的异常结束)之后执行。
    • defer语句通常用于一些成对的操作场景,打开/关闭连接,加锁/解锁,打开文件/关闭文件等等
    • defer在一些需要回收资源的场景中非常有用
• 为什么需要defer
    • 有效防止内存泄漏
• defer底层原理 
    • 每次defer语句在执行的时候,都会将函数进行“压栈”,函数参数会被拷贝下来。当外层函数退出时,defer函数会按照定义的顺序逆序执行。如果defer执行的函数为nil,那么会在最终调用函数中产生panic。
    • 编译器会把 defer 语句翻译成对 deferproc 函数的调用,同时,编译器也会在使用了 defer 语句的 go 函数的末尾插入对 deferreturn 函数的调用。
    • 每一个goroutine结构体中都有一个_defer 指针变量用来存放defer单链表。defer保存用什么数据结构?回答栈过不了面试官那关,defer单链表应该能过关。_defer 结构体如下:
    • siz:所有传入参数的总大小。
    • started:该 defer 是否已经执行过。
    • heap:表明该defer是否存储在heap上。
    • sp:函数栈指针寄存器,一般指向当前函数栈的栈顶。
    • pc:程序计数器,有时称为指令指针(IP),线程利用它来跟踪下一个要执行的指令。在大多数处理器中,PC指向的是下一条指令,而不是当前指令。
    • fn:指向传入的函数地址和参数。
    • _panic:指向 _panic 链表。
    • link:指向 _defer 链表。
    • 为什么defer要按照定义的顺序逆序执行:后面定义的函数可能会依赖前面的资源,所以要先执行。如果前面先执行,释放掉这个依赖,那后面的函数就不能找到它的依赖了。
    • defer函数定义时,对外部变量的引用方式有两种,分别是函数参数以及作为闭包引用。在作为函数参数的时候,在defer定义时就把值传递给defer,并被cache起来。如果是作为闭包引用,则会在defer真正调用的时候,根据整个上下文云确定当前的值。
    • defer后面的语句在执行的时候,函数调用的参数会被保存起来,也就是复制一份。在真正执行的时候,实际上用到的是复制的变量,也就是说,如果这个变量是一个“值类型”,那他就和定义的时候是一致的,如果是一个“引用”,那么就可能和定义的时候的值不一致

Goroutine和线程的区别

从调度上看,goroutine的调度开销远远小于线程调度开销。
OS的线程由OS内核调度,每隔几毫秒,一个硬件时钟中断发到CPU,CPU调用一个调度器内核函数。这个函数暂停当前正在运行的线程,把他的寄存器信息保存到内存中,查看线程列表并决定接下来运行哪一个线程,再从内存中恢复线程的注册表信息,最后继续执行选中的线程。这种线程切换需要一个完整的上下文切换:即保存一个线程的状态到内存,再恢复另外一个线程的状态,最后更新调度器的数据结构。某种意义上,这种操作还是很慢的。
Go运行的时候包涵一个自己的调度器,这个调度器使用一个称为一个M:N调度技术,m个goroutine到n个os线程(可以用GOMAXPROCS来控制n的数量),Go的调度器不是由硬件时钟来定期触发的,而是由特定的go语言结构来触发的,他不需要切换到内核语境,所以调度一个goroutine比调度一个线程的成本低很多。
从栈空间上,goroutine的栈空间更加动态灵活。
每个OS的线程都有一个固定大小的栈内存,通常是2MB,栈内存用于保存在其他函数调用期间哪些正在执行或者临时暂停的函数的局部变量。这个固定的栈大小,如果对于goroutine来说,可能是一种巨大的浪费。作为对比goroutine在生命周期开始只有一个很小的栈,典型情况是2KB, 在go程序中,一次创建十万左右的goroutine也不罕见(2KB*100,000=200MB)。而且goroutine的栈不是固定大小,它可以按需增大和缩小,最大限制可以到1GB。
goroutine没有一个特定的标识。
在大部分支持多线程的操作系统和编程语言中,线程有一个独特的标识,通常是一个整数或者指针,这个特性可以让我们构建一个线程的局部存储,本质是一个全局的map,以线程的标识作为键,这样每个线程可以独立使用这个map存储和获取值,不受其他线程干扰。
goroutine中没有可供程序员访问的标识,原因是一种纯函数的理念,不希望滥用线程局部存储导致一个不健康的超距作用,即函数的行为不仅取决于它的参数,还取决于运行它的线程标识。

发表评论