GO基础类

  1. 与其他语言相比,使用go有什么好处?
  2. golang使用什么数据类型?
  3. go程序中的包是什么?
  4. go支持什么形式的类型转换?将整数转换为浮点数。
  5. 什么是goroutine?你如何停止它?
  6. 如何在运行时检查变量类型?
  7. go两个接口之间存在什么关系?
  8. go当中同步锁有什么特点,作用是什么?
  9. go语言当中,channel有什么特点,需要注意什么?
  10. go语言当中,channel缓冲有什么特点?
  11. go语言中cap函数可以作用于哪些内容?
  12. go convey是什么?一般用来做什么?
  13. go语言中new和make有什么区别?
  14. go语言中make的作用是什么?
  15. print(),Sprintf(),Fprintf()都是格式化输出,有什么不同?
  16. go语言当中数组和切片的区别是什么?
  17. go语言当中值传递和地址传递(引用传递)如何运用?有什么区别?举例说明
  18. go语言当中数组和切片在传递的时候的区别是什么?
  19. go语言是如何实现切片扩容的?
  20. defer的作用和特点是什么?
  21. golang slice的底层实现?
  22. golang slice的扩容机制,有什么注意点?
  23. 扩容前后的slice是否相同?
  24. golang的参数传递、引用类型?
  25. golang map的底层实现?
  26. golang map如何扩容?
  27. golang map如何查找?
  28. 介绍一下channel
  29. go语言的channel特性?
  30. channel的ring buffer实现
  31. 什么是gc,为什么要gc,如何触发gc?
  32. go是传值还是传引用?
  33. go面试官问我如何实现面向对象
  34. go结构体和结构体指针调用有什么区别
  35. go new和make是什么,有什么区别
  36. 什么是协程,协程和线程的区别和联系

Go优化

GO优化实践

Go问题排查思路

调度模型

  1. GMP模型,为什么要有P?
  2. go结构体是否可以比较,为什么
  3. 单核cpu,开两个Goroutine,其中一个死循环,会怎样
  4. 进程、线程都有ID,为什么Goroutine没有ID?
  5. Goroutine数量控制在多少合适,会影响GC和调度?
  6. 详解Go程序的启动流程,你知道p0、m0是什么吗
  7. Goroutine泄露的情况有哪些
  8. Go在什么时候会抢占P
  9. 会诱发Goroutine挂起的27个原因

    数据结构

  10. Go interface的一个坑及原理分析

  11. Go defer万恶的闭包问题
  12. 为什么go map和slice是非线程安全的
  13. Go sync.map和原生map谁的性能好,为什么
  14. 为什么 go map的负载因子是6.5

    并发编程

  15. Mutex的几种状态

  16. Mutex正常模式和饥饿模式
  17. Mutex允许自旋的条件
  18. RWMutex的实现
  19. RWMutex注意事项
  20. COND是什么
  21. BROADCAST和SIGNAL的区别
  22. COND中wait的使用
  23. WaitGroup的用法
  24. WaitGroup的实现原理
  25. 什么是Sync.Once
  26. 什么操作叫原子操作
  27. 原子操作和锁的区别
  28. 什么CAS
  29. Sync.Pool有什么用

    Go Routine

  30. Goroutine的定义

  31. GMP指的是什么
  32. 1.0之前GM调度模型
  33. GMP调度流程
  34. GMP中Work Stealing机制
  35. GMP中Hand Off机制
  36. 协作式的抢占式调度
  37. 基于信号的抢占式调度
  38. GMP调度过程中存在哪些阻塞
  39. Sysmon有什么作用
  40. 什么是三色标记
  41. 三色标记原理
  42. 什么是写屏障,插入写屏障,删除写屏障,混合写屏障
  43. 插入写屏障
  44. 删除写屏障
  45. 写屏障
  46. 混合写屏障
  47. GC触发时机
  48. Go语言中GC的流程是什么?
  49. GC如何调优
  50. 有几种方法关闭goroutine?一个goroutine可以kill另一个goroutine吗?为什么

    微服务

  51. 对微服务的了解

  52. 说说微服务架构的优势
  53. 微服务有哪些特点
  54. 设计微服务的最佳实践是什么
  55. 微服务架构如何运作?
  56. 微服务架构的优缺点是什么
  57. 单片、SOA和微服务架构有什么区别
  58. 在使用微服务架构时,面临哪些挑战
  59. SOA和微服务架构之间的主要区别是什么
  60. 微服务有什么特点
  61. 什么是领域驱动设计
  62. 为什么需要领域驱动设计(DDD)
  63. 什么是无所不在的语言?
  64. 什么是凝聚力?
  65. 什么是耦合?
  66. 什么是REST/RESTFUL以及它的用途是什么
  67. 什么是不同类型的微服务测试?

GO基础类

1.与其他语言相比,使用go有什么好处?

  1. golang针对并发进行了优化
  2. 单一标准的代码格式,相比其他语言具有更好的可读性
  3. GC收集明显比java和python更有效,因为它与程序同时执行

    2.golang使用什么数据类型?

  • 数字型
    • uint8、16、32、64
    • int8、16、32、64
    • byte、rune、uintptr、uint(32或64)、int(与uint一样)
  • 浮点型
    • float32、64
    • complex64、128
  • 布尔型
    • bool
  • 字符串型
    • string
  • 其他类型
    • array
    • slice
    • interface
    • map
    • channel
    • struct
    • pointer
    • function

      3.go程序中的包是什么?

      包(PKG)是go工作区中包含go源文件或其他包的目录。源文件中每个函数、变量和类型都存储在链接包中。每个go源文件都属于一个包,该包在文件顶部使用以下命令声明:
      1. package <packagename>
      可以使用以下方法导入和导出包以重用导出的函数或类型:
      import <packagename>
      
      golang的标准包是fmt,其中包含格式化和打印功能,如Println()

      4.go支持什么形式的类型转换?将整数转换为浮点数。

      Go支持显式类型转换以满足其严格的类型要求。
      i := 55   //int
      j := 67.8 // float64
      sum := i + int(j) // j is converted to int
      

      5.什么是goroutine?你如何停止它?

      一个goroutine是一个函数或方法执行同时旁边其他任何过程采用了特殊的goroutine线程。goroutine线程比标准线程更轻量级,大多数golang程序同时使用数千个goroutine

要创建goroutine,在函数声明前添加关键字go

go f(x,y,z)
  1. 定期轮训channel:可以通过向goroutine发送一个信号通道来停止它。goroutine只能在被告知检查时响应信号,需要在逻辑位置(例如for循环的顶部)包含检查

    func foo() {
     quit: make(chan bool)
     go func() {
         for {
             select {
             case <- quit:
                 return
             default:
                 // ...
             }
         }
     }()
    
     // ...
     quit <- true
    }
    
  2. 主动关闭channel:借助channel的close机制来控制

    func foo() {
     ch := make(chan string, 3)
     go func() {
         for {
             v, ok := <-ch
             if !ok {
                 fmt.Println("end")
                 return
             }
             fmt.Println(v)
         }
     }()
    
     ch <- "Jay came in"
     ch <- "Jay came out"
     close(ch)
     time.Sleep(time.Second)
    }
    
  3. 使用context:借助上下文来控制和关闭goroutine

    func foo() {
     ch := make(chan struct{})
     ctx, cancel := context.WithCancel(context.Background())
    
     go func(ctx context.Context){
         for {
             select {
             case <- Done():
                 ch <- struct{}{}
                 return
             default:
                 fmt.Println("jay is not came in")
             }
    
             time.Sleep(500 * time.Millisecond)
         }
     }(ctx)
    
     go func() {
         time.Sleep(3 * time.Second)
         cancel()
     }()
    
     <- ch
     fmt.Println("end")
    }
    

    6.如何在运行时检查变量类型?

    // TODO 这里在说啥?
    类型开关是在运行时检查变量类型的最佳方式。类型开关按类型而不是值来评估变量。每个switch至少包含一个case,用作条件语句,和一个default case, 如果没有一个case为真,则执行default

    7.go两个接口之间存在什么关系?

    如果两个接口有相同的方法列表,那么他们是等价的,可以互相赋值。如果接口A的方法列表是接口B的方法列表的子集,那么接口B可以赋值给接口A。

    8.go当中同步锁有什么特点,作用是什么?

    当一个goroutine(协程)获得了mutex后,其他goroutine就只能hold,除非该goroutine释放了该mutex。RWMutex在读锁占用的情况下,会阻止写,但不会阻止读,RWMutex在写占用情况下,会goroutine(无论读写)的其他操作,整个锁相当于该goroutine独占。同步锁的作用是保证资源在使用时的独有性,不会因为并发而导致数据错乱,保证系统的稳定性。

    9.go语言当中,channel有什么特点,需要注意什么?

    如果给一个nil的channel发送数据,会造成永远阻塞,如果从一个nil的channel中接收数据,也会造成永久阻塞,给一个已经关闭的channel发送数据,会引起panic。从一个已经关闭的channel接收数据,如果缓冲区为空,会返回一个0值。如果不为空,可以继续接收数据。

    10.go语言当中,channel缓冲有什么特点?

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

    11.go语言中cap函数可以作用于哪些内容?

  • array
  • slice
  • channel

    12.go convey是什么?一般用来做什么?

  • go convey是一个支持golang的单元测试框架

  • go convey能够自动监控文件修改并启动测试,并可以将测试结果实时输出到web界面
  • go convey提供了丰富的断言简化测试用例的编写

    13.go语言中new和make有什么区别?

    func new(Type) *Type
    func make(t Type, size ...integerType) Type
    

    (1)返回值:从定义可以看出,new返回的是指向Type的指针。make直接返回的是Type类型值
    (2)入参: new只有一个Type参数,Type可以是任意类型的数据。make可以有多个参数,其中第一个参数与new的参数相同,但只能是slice,map或者chan的一种。对于不同类型,size参数说明如下:

  • 对于slice,第一个size表示长度,第二个size表示容量,且容量不能小于长度,如果省略第二个size,默认容量等于长度

  • 对于map,会根据size大小分配资源,以足够存储size个元素。如果省略size,会默认分配一个起始size
  • 对于chan,size表示缓冲区容量。如果省略size,channel为无缓冲channel

    14.go语言中make的作用是什么?*

    make的作用是为 slice ,map 或者 chan 的初始化, make函数是内建函数,函数定义:

    func make(Type, size IntegerType) Type
    

    make(T, args)函数的目的和new(T)不同,仅仅用于创建slice,map,channel,而返回的类型是实例

    15.print(),Sprintf(),Fprintf()都是格式化输出,有什么不同?

    虽然这三个函数都是格式化输出,但是输出的目标不一样

  • Printf()是标准输出,一般是屏幕,也可以重定向

  • Sprintf()是把格式化字符串输出到指定的字符串中
  • Fprintf()是把格式化字符串输出到文件中

    16.go语言当中数组和切片的区别是什么?*

    数组(array):

    数组是固定长度,数组长度是数组类型的一部分,所以[3]int和[4]int是两种不同的数组类型,数组需要指定大小,不指定也会根据初始化自动推算出大小,不可改变数组是通过值传递的

    切片(slice):

    切片可以改变长度,切片是轻量级的数据结构,三个属性:长度、指针、容量,不需要指定大小,切片是地址传递(引用传递),可以通过数组来初始化,也可以通过内置函数make()来初始化,初始化的时候len=cap,然后进行扩容

    17.go语言当中值传递和地址传递(引用传递)如何运用?有什么区别?举例说明

  1. 值传递只会把参数的值复制一份放进对应的函数,两个变量的地址不同,不可相互更改。
  2. 地址传递(引用传递)会将变量本身传入对应的函数,在函数中可以对该变量进行值内容的更改

    18.go语言当中数组和切片在传递的时候的区别是什么?*

  3. 数组是值传递

  4. 切片是引用传递

    20.defer的作用和特点是什么?

    只需要在普通函数或方法前面加上关键字defer,就完成了defer所需要的语法。当defer语句被执行时,跟在defer后面的函数会被延迟执行。直到包含该defer的函数执行完毕时,defer后面声明的函数才会被执行,不论包含defer的语句是return正常结束,还是panic异常结束。并且你可以在一个函数内执行多条defer语句,它的执行顺序与声明顺序相反,就像入栈出栈

    defer的常用场景:

  • defer语句经常被用于处理成对的操作,如打开、关闭、连接、断开连接、加锁、释放锁。
  • 通过defer机制,不论函数逻辑多复杂,都能保证在任何执行路径下,资源被释放
  • 释放资源的defer应该直接跟在请求资源的语句后面

    21.golang slice的底层实现?

    切片是基于数组实现的,它的底层是数组,它自己本身非常小,可以理解为对底层数组的抽象。因为基于数组实现,所以它底层的内存是连续分配的,效率非常高,还可以通过索引获得数据,可以迭代以及垃圾回收优化。
    切片本身并不是动态数组或者指针。它内部实现的数据结构通过指针引用底层数组,设定相关属性将数据读写操作限定在指定的区域内。切片本身是一个只读对象,其工作机制类似数组指针的呃一种封装。
    切片对象非常小,是因为它是只有三个字段的数据结构:

  • 指向底层数组的指针

  • 切片的长度
  • 切片的容量

    22.golang slice的扩容机制,有什么注意点?

    Go中切片扩容的策略是这样的:

  • 首先判断,如果新申请容量大于2倍的旧容量,最终容量就是新申请的容量

  • 否则判断,如果旧切片的长度小于1024,则最终容量就是旧容量的两倍
  • 否则判断,如果旧切片的长度大于1024,则最终容量从旧容量开始循环增加原来的1/4,直至最终容量大于新申请的容量
  • 如果最终容量计算值溢出,则最终容量就是新申请容量

    23.扩容前后的slice是否相同?

    情况一:

    原数组(array)还有容量可以扩容(实际容量没有填充完),这种情况下,扩容以后的切片(slice)还是指向原来的数组(array),对一个切片(slice)的操作可能影响多个指针指向相同地址的切片(slice)。

    情况二:

    原来数组的容量已经达到了最大值,再想扩容,go默认会开一片内存区域,把原来的值拷贝过来,再执行append()操作。这种情况不会影响原数组。
    要复制一个切片(slice),最好使用copy函数。

    24.golang的参数传递、引用类型?

    go语言中所有的传参都是值传递,都是一个副本,一个拷贝。因为拷贝的内容有时候是非引用类型(int, string, struct等这些),这样函数中就无法修改原内容数据;有的是引用类型(指针、map、slice、chan等这些),这样就可以修改原内容数据。
    Golang的引用类型包括slice、map和channel。它们有复杂的内部结构,除了申请内存外,还需要初始化相关属性。内置函数new计算类型大小,为其分配0值内存,返回指针。而make会被编译器翻译成具体的创建函数,由其分配内存和初始化成员结构,返回对象而非指针。

    25.golang map的底层实现?

    golang中map的底层实现是一个散列表(hash table),因此实现map的过程实际上就是实现散表的过程。在这个散列表中,主要体现的结构体有两个,一个叫hmap(a header for a go map),一个叫bmap(a bucket for a go map,通常叫其bucket)

    26.golang map如何扩容?

    装填因子:count/2^B
    触发条件:
  1. 装填因子是否大于6.5
  2. overflow bucket是否太多

解决方法:

  1. 双倍扩容:扩容采取了一种称为“渐进式”的方式,原有的key并不会一次性搬迁完毕,每次最多只会搬迁两个bucket
  2. 等量扩容:重新排列,极端情况下,重新排列也解决不了,map成了链表,性能大大降低,此时哈希种子hash0的设置,可以降低此类场景的发生。

    27.golang map如何查找?

    go语言中map采用的是哈希查找表,由一个key通过哈希函数得到哈希值,64位系统中就生成一个64bit的哈希值,由这个哈希值将key对应到不同的桶(bucket)中,当有多个哈希映射到相同的桶中时,使用链表解决哈希冲突。key经过hash后共64位,根据hmap中的B值,计算它到底要落在哪个桶时,桶的数量是2^B,如B=5,那么64位最后5位表示第几号桶,再用hash值的高8位确定在bucket中的存储位置,当前bmap的bucket未找到,则查询对应的overflow bucket,对应位置有数据则对比完整的哈希值,确定是否是要查找的数据。
    如果两个不同的key落在了同一个桶上,hash冲突使用链表法接近,遍历bucket中的key,如果当前处于map扩容时,处于数据搬迁状态,则优先从oldbuckets查找

    28.介绍一下channel*

    go语言中,不要通过共享内存来通信,要通过通信来实现共享内存。go的CSP(Communicating Sequential Process)并发模型,中文可以叫做通信顺序进程,是通过goroutine和channel来实现的。
    所以channel收发遵循先进先出FIFO,分为有缓存和无缓存,channel中大致有buffer(当缓冲区大小不为0时,是个ring buffer)、sendx和recvx收发的位置(ring buffer记录实现)、sendq、recvq当前channel因为缓冲区不足而阻塞的队列、使用双向链表存储、还有一个mutex锁控制并发、其他原属等。

    29.go语言的channel特性?

  3. 给一个nil channel发送数据,造成永久阻塞

  4. 从一个nil channel接收数据,造成永久阻塞
  5. 给一个已经关闭的channel发送数据,引起panic
  6. 从一个已经关闭的channel接收数据,如果缓冲区为空,则返回一个零值,如果缓冲区不为空,则接收缓冲区内容
  7. 无缓冲的channel是同步的,有缓冲的channel是非同步的
  8. 关闭一个nil channel会panic

    30.channel的ring buffer实现

    channel中使用了ring buffer(环形缓冲区)来缓存写入的数据。ring buffer有很多好处,而且非常适合实现FIFO式的固定长度队列。在channel中,ring buffer的实现如下:
    recvx sendx
    ↓ ↓
    【】【】【】【】【】【】【】
    hchan中有两个与buffer相关的变量:recvx和sendx。其中sendx标识buffer中可写的index,recvx标识buffer中可读的index。从recvx到sendx之间的元素,标识已正常存入buffer中的数据。
    我们可以直接使用buf[recvx]来读取到队列的第一个元素,使用buf[sendx] = x 来将元素放到队尾

    31.什么是gc,为什么要gc,如何触发gc?

    什么是GC:垃圾回收(gc)是一种自动管理内存的机制,垃圾回收器会尝试回收程序不再使用的对象以及占用的内存
    为什么要GC:手动管理内存很麻烦,管错或者管漏内存将会导致程序不稳定甚至崩溃
    如何触发GC:
    1.系统触发:运行时自行根据内置的条件,检查、发现到,则进行GC处理,维护整个应用程序的可用性。
    2.手动触发:开发者在业务代码中自行调用 runtime.GC 方法来触发GC行为
    在系统触发的场景中,go源码的src/runtime/mgc.go明确标识了GC触发的三种场景,分别如:
    const (
     gcTriggerHeap gcTriggerKind = iota
     gcTriggerTime
     gcTriggerCycle
    )
    
  • gcTriggerHeap:当所分配的堆大小达到阈值(由控制器计算的触发堆的大小)时,触发。
  • gcTriggerTime:当距离上一个GC周期超过一定时间时,触发(时间周期以 runtime.forcegcperiod 变量为准,默认2分钟)。
  • gcTriggerCycle:如果没有开启GC,则启动GC
  • 手动触发的 runtime.GC 方法

    基本流程:

    需要手动触发的场景极其少见,可能是某些业务方法执行完后占用了过多的内存需要主动释放,又或者是debug程序所需。通过runtime.GC来观察触发GC流程的代码 ```go func GC() { n := atomic.Load(&work.cycles) gcWaitOnMark(n)

    gcStart(gcTrigger{kind: gcTriggerCycle, n: n+1})

    gcWaitOnMark(n + 1)

    for atomic.Load(&work.cycles) == n + 1 && sweepone() != ^uintptr(0) {

      sweep.nbgsweep++
      Gosched()
    

    }

for atomic.Load(&work.cycles) == n + 1 && atomic.Load(&mheap_.sweepers) != 0 {
    Gosched()
}

mp := aquirem()
cycle := atomic.Load(&work.cycles)
if cycle == n + 1 || (gcphase == _GCmark && cycle == n + 2){
    mProf_PostSweep()
}
releasem(mp)

}


   1. 在开启新的一轮GC周期前,需要调用 **gcWaitOnMark **方法将上一轮GC的标记结束(含扫描终止、标记、或标记终止等)。
   1. 开启新的一轮GC周期,调用 **gcStat **方法触发GC行为,开启扫描标记阶段。
   1. 需要调用 **gcWaitOnMark **方法等待,直到当前GC周期的扫描、标记、标记终止完成。
   1. 需要调用 **sweepone **方法,扫描未扫除的堆跨度,并持续扫描,保证清理完成。在等待扫描完毕前的阻塞时间,会调用Gosched让出。
   1. 在本轮GC已经基本完成后,会调用 **mProf_PostSweep **方法。来记录最后一次终止标记时的堆配置文件快照。
   1. 结束,释放Memory
<a name="fOjFb"></a>
#### 在哪触发:
在go运行初始化时,会启动一个goroutine,处理GC机制的相关事项
```go
func init() {
    go forcegchelper()
}

func forcegchelper() {
    forcegc.g = getg()
    lockInit(&forcegc.lock, lockRankForcegc)
    for {
        lock(&forcegc.lock)
        if forcegc.idle != 0 {
            throw("forcegc: phase error")
        }
        atomic.Store(&forcegc.idle, 1)
        goparkunlock(&forcegc.lock, waitReasonForceGCIdle, traceEvGoBlock, 1)
        // this go routine is explicitly resumed by sysmon
        if debug.gctrace > 0 {
            println("GC forced")
        }

        gcStart(gcTrigger{kind: gcTriggerTime, now: nanotime()})
    }
}

在这段程序中,需要特别关注的是在forcegchelper方法中,会调用 goparkunlock 方法让该 goroutine 陷入休眠等待状态,以减少不必要的资源开销。
在休眠后,会由 sysmon 这一个系统监控线程来进行监控、唤醒等行为:

func sysmon() {
    ...
    for {
        ...
        // check if we need to force a GC
        if t := (gcTrigger{kind: gcTriggerTime, now: now}); t.test() && atomic.Load(&forcegc.idle) != 0 {
            lock(&forcegc.lock)
            forcegc.idle = 0
            var list gList
            list.push(forcegc.g)
            injectglist(&list)
            unlock(&forcegc.lock)
        }
        if debug.schedtract > 0 && lasttract+int64(debug.schedtract)*100000 <= now {
            lasttract = now
            schedtract(debug.scheddetail > 0)
        }
        unlock(&sched.sysmonlock)
    }
}

这段代码核心的行为就是在不断的for循环中,对 gcTriggerTime now 进行变量比较,判断是否达到一定时间(默认2分钟)。如果满足条件,会将forcegc.g放到全局队列中接受新的一轮调度,再进行对上面 forcegchelper 的唤醒。
运行时会通过gcTrigger.test()来决定是否需要触发gc,只要满足三个条件中的一个即可。

// test reports whether the trigger condition is satisfied, meaning
// that the exit condition for the _GCoff phase has been met, The exit
// condition should be tested when allocation.
func (t gcTrigger) test() bool {
    if !memstats.enablegc || panicking != 0 || gcphase != _GCoff {
        return false
    }
    switch t.kind {
    case gcTriggerHeap:
        // 堆内存不足
        // Non-atomic access to heap_live for performance. If
        // we are going to trigger on this, this thread just
        // atomically wrote heap_live anyway and we'll see out
        // own write.
        return memstats.heap_live >= memstats.gc_trigger
    case gcTriggerTime:
        if gcpercent < 0 {
            return false
        }
        // 大于两分钟
        lastgc := int64(atomic.Load64(&memstats.last_gc_nanotime))
        return lastgc != 0 && t.now - lastgc > forcegcperiod
    case gcTriggerCycle:
        // t.n > work.cycles, but accounting for wraparound.
        return int32(t.n - work.cycles) > 0
    }
    return true
}

堆内存申请:

在了解定时触发的机制后,另一个场景就是分配堆空间(mallocgc)的时候,核心代码如下:

func mallocgc(size uintptr, typ *_type, needzero bool)unsafe.Pointer {
    shouldhelpgc := false 
    ...
    if size <= maxSmallSize {
        if noscan && size < maxTinySize {
            ...
            // Allocate a new maxTiny block.
            span = c.allo[tinySpanClass]
            v := nextFreeFast(span)
            if v == 0 {
                v, span, shouldhelpgc = c.nextFree(tinySpanClass)
            }
            ...
            spc := makeSpanClass(sizeclass, noscan)
            span = c.alloc[spc]
            v := nextFreeFast(span)
            if v == 0 {
                v, span, shouldhelpgc = c.nextFree(spc)
            }
            ...
        }
    } else {
        shouldhelpgc = true
        span = c.allocLarge(size, needzero, noscan)
        ...
    }

    if shouldhelpgc {
        if t := (gcTrigger{kind: gcTriggerHeap}); t.test() {
            gcStart(t)
        }
    }

    return x
}
  • 小对象:如果申请小对象时,发现当前内存空间不存在空闲跨度时,会调用 nextFree 方法获取新的可用对象,可能会触发gc行为
  • 大对象:如果申请大于32K以上的对象时,可能会触发GC行为


32.go是传值还是传引用?

值传递,即使传的是指针,会进行指针拷贝,可以输出看两个指针的内容完全一样,但地址不一样。

33.go面试官问我如何实现面向对象

  • 继承
    • 面向对象中的 “继承” 指的是子类继承父类的特征和行为,使得子类对象(实例)具有父类的实例域和方法,或子类从父类继承方法,使得子类具有父类相同的行为。
    • golang中没有extends关键字,是通过组合的方式实现继承的 ```go type Animal struct { Name string }

type Cat struct { Animal FeatureA string }

type Dog struct { Animal FeatureB string }


- 多态
   - 面向对象中的 “多态” 指的同一个行为具有多种不同表现形式或形态的能力,具体是指一个类实例(对象)的相同方法在不同情形有不同表现形式。
```go
type AnimalSounder interface {
 MakeDNA()
}

func MakeSomeDNA(animalSounder AnimalSounder) {
 animalSounder.MakeDNA()
}

func (c *Cat) MakeDNA() {
 fmt.Println("煎鱼是煎鱼")
}

func (c *Dog) MakeDNA() {
 fmt.Println("煎鱼其实不是煎鱼")
}

func main() {
 MakeSomeDNA(&Cat{})
 MakeSomeDNA(&Dog{})
}

34.go结构体和结构体指针调用有什么区别

  1. 看需不需要修改结构体内的值,如果不需要,传结构体就可以,如果需要,就要传结构体指针。
  2. 如果结构体很大,则传结构体指针更好

    35.go new和make是什么,有什么区别

  • new()按指定类型长度分配0值内存,返回指针,不关心类型内部构造和初始化方式。
    • new(T)返回的是T的指针
  • make()对引用类型进行创建,编译器会将make转换为目标类型专用的创建函数,以确保完成全部内存分配和相关属性的初始化
    • make只能用于slice、map、channel
    • make(T,args)返回的是初始化之后的T类型的值,这个新值并不是T类型的零值,也不是指针T,是经过*初始化之后的T的引用

不要使用new,永远使用make来构造map:
如果你错误的使用了new()分配了一个引用对象,你会获得一个空指针的引用,相当于声明了一个未初始化的变量并且取了它的地址:
mapCreated := new(map[string]float32)
接下来当我们调用:mapCreate[“key1”] = 4.5的时候,编译器会报错

36.进程、线程、协程、goroutine

  • 进程:进程是一个具有独立功能的程序关于某个数据集合的一次运行活动。它是操作系统动态执行的基本单元,在传统的操作系统中,进程即是基本的分配单元,也是基本的执行单元
    • 进程是一个实体,每个进程都有自己的地址空间,一般情况下,包含文本区域、数据区域、堆栈
    • 进程是执行中的程序,程序是一个没有生命的实体,只有处理器赋予程序生命时,它才能成为一个活动的实体,我们称之为进程
    • 进程本身不会运行,是线程的容器。线程不能单独执行,必须组成进程
    • 一个程序至少有一个进程,一个进程至少有一个线程
    • 对于操作系统来讲,一个任务就是一个进程,比如开一个浏览器就是启动一个浏览器进程
    • 进程状态(3状态):
      • 就绪:获取出CPU外的所有资源,只要处理器分配资源就可以马上执行
      • 运行:获得处理器分配的资源,程序开始执行
      • 阻塞:当程序条件不够的时候,需要等待条件满足的时候才能执行
  • 线程
    • 一个进程至少有一个线程,不然就没有存在的意义
    • 在一个进程内部,要同时干多件事情,就需要同时运行多个子任务,我们把进程内的这些子任务叫做线程
    • 多线程就是为了同步完成多项任务(在单个程序中同时运行多个线程完成不同的任务和工作),不是为了提高运行效率,而是为了提高资源使用效率来提高系统的效率
    • 一个简单的比喻,多线程就是火车上的车厢,进程就是火车
    • 线程是程序执行流的最小单元。一个标准的线程是由当前的线程ID、当前指令指针、寄存器和堆栈组成
    • 同一个进程的多个线程之间可以并发执行
    • 线程状态:
      • 就绪:指线程具备运行的条件,逻辑上可以运行,在等待处理机
      • 运行:指线程占用处理机正在运行
      • 阻塞:线程在等待一个事件,逻辑上不可执行
  • 协程:协程是一种用户态的轻量级线程,协程的调度完全由用户控制(进程和线程都是由CPU内核进行调度)。协程拥有自己的寄存器上下文和栈。协程调度切换时,将寄存器上下文和栈保存到其他地方,再切换回来时,恢复先前保存的寄存器上下文和栈,直接操作栈则基本没有内核切换的开销。
  • goroutine和协程的区别
    • 本质上,goroutine就是协程。不同的是,golang在runtime、系统调用等多方面对goroutine调度进行了封装和处理,当遇到长时间执行或者进行系统调用时,会主动把当前goroutine的CPU(P)转让出去,让其他goroutine能被调度执行,也就是golang从语言层面支持了协程。golang的一大特色就是从语言层面原生支持协程,在函数或者方法前面加go关键字就可以创建一个goroutine
    • 其他方面的比较:
      • 内存消耗
        • 每个goroutine默认占用2KB
        • 线程8MB
      • 线程和goroutine切换调度开销
        • goroutine只有三个寄存器的值修改:PC/SP/DX
        • 线程涉及模式切换(从用户态切换到内核态),16个寄存器、PC、SP等寄存器的刷新等

Go优化

image.png

Go优化实践

image.png
我们将 pprof 的开启集成到模板中, 并自动选择端口, 并集成了 gops 工具, 方便查询 runtime 信息, 同时在浏览器上可直接点击生成火焰图, pprof 图, 非常的方便, 也不需要使用者关心.

Go问题排查思路image.png

一次有意思的问题排查

image.png
负载, 依赖服务都很正常, CPU 利用率也不高, 请求也不多, 就是有很多超时.
image.png
该服务在线上打印了 debug 日志, 因为早期的服务模板开启了 gctrace, 框架把 stdout 重定向到一个文件了. 而输出 gctrace 时本来是到 console 的, 输出到文件了, 而磁盘跟不上, 导致 gctrace 日志被阻塞了.
这里更正一下 ppt 中的内容, 并不是因为 gc 没完成而导致其他协程不能运行, 而是后续 gc 无法开启, 导致实质上的 stw.
打印 gc trace 日志时, 已经 start the world 了, 其他协程可以开始运行了. 但是在打印 gctrace 日志时, 还保持着开启 gc 需要的锁, 所以, 打印 gc trace 日志一直没完成, 而 gc 又比较频繁, 比如 0.1s 一次, 这样会导致下一次 gc 开始时无法获取锁, 每一个进入 gc 检查的 p 阻塞, 实际上就造成了 stw.

并发编程

1.Mutex的几种状态

  • mutexLocked — 表示互斥锁的锁定状态
  • mutexWoken — 表示从正常模式被唤醒
  • mutexStarving — 当前的互斥锁进入饥饿状态
  • waitersCount — 当前互斥锁上等待的goroutine个数

    2.Mutex正常模式和饥饿模式

    正常模式(非公平锁)

    正常模式下,所有等待锁的goroutine按照FIFO(先进先出)顺序等待。唤醒的goroutine不会直接拥有锁,而是会和新请求锁的goroutine竞争锁的拥有。
    新请求锁的goroutine具有优势:它正在CPU上执行,而且可能有好几个,所以刚刚唤醒的goroutine有很大可能在锁竞争中失败。在这种情况下,这个被唤醒的goroutine会进入到等待队列的前面。如果一个等待的goroutine超过1ms没有获取锁,那么它将会进入饥饿模式。

    饥饿模式(公平锁)

    为了解决等待G队列的长尾问题,饥饿模式下,直接由unlock把锁交给等待队列中排在第一位的G(队头),同时,饥饿模式下,新进来的G不会参与抢锁也不会进入自旋模式,会直接进入等待队列的尾部,这样很好的解决了老的G一直抢不到锁的情况。
    饥饿模式的触发条件:当一个G等待锁的时间超过了1毫秒时,或者当前队列只剩下一个g的时候,Mutex切换到饥饿模式

当发生如下情况时,mutex会切换回正常模式
当前等待队列里只有最后一个goroutine,没有其他等待锁的goroutine了
当前goroutine等待时间小于1毫秒

总结:

对于两种模式,正常模式下性能是最好的,goroutine可以连续多次获取锁,饥饿模式下解决了锁公平的问题,但是性能会下降,其实是性能和公平的一个平衡模式。

3.Mutex允许自旋的条件

  1. 锁已被占用,并且锁不处于饥饿模式
  2. 累计的自旋次数小于最大自旋次数(active_spin=4)
  3. cpu核数大于1
  4. 有空闲的P(线程?)
  5. 当前goroutine锁挂载的P下,本地待运行队列为空

    4.RWMutex的实现

    通过记录readerCount 读锁的数量来进行控制,当有一个写锁的时候,会将读锁的数量设置为负数1<<30。目的是让新进入的读锁等待写锁之后释放通知读锁。同样写锁也会等待之前的读锁都释放完毕,才会开始进行后续的操作。而等写锁释放完之后,会将值重新加上1<<30,并通知刚才新进入的读锁(rw.readerSem),两者互相限制

    5.RWMutex注意事项

  • RWMutex是单写多读锁,该锁可以加多个读锁或一个写锁
  • 读锁占用的情况下会阻止写,但不会阻止读,多个goroutine可以同时获取读锁
  • 写锁会组织其他的goroutine(无论读写锁)进来,整个锁由该goroutine独占
  • 适用于读多写少的场景
  • RWMutex类型变量的零值是一个未锁定状态的互斥锁
  • RWMutex在首次被使用之后就不能再被拷贝
  • RWMutex的读锁或写锁在未锁定状态,解锁操作会引发panic
  • RWMutex的一个写锁去锁定临界区的共享资源,如果临界区的共享资源已经被(读或写锁)锁定,这个写锁操作的goroutine将会被阻塞直到解锁
  • RWMutex的读锁不要用于递归调用,比较容易产生死锁
  • RWMutex的锁定状态与特定的goroutine没有关联。一个goroutine可以RLock(Lock),另一个goroutine可以RUnlock(Unlock)
  • 写锁解锁后,所有因操作锁定读锁而被阻塞的goroutine会被唤醒,并都可以成功锁定读锁
  • 读锁被解锁后,在没有被其他读锁锁定的前提下,所有操作锁定写锁而被阻塞的goroutine,其中等待时间最长的一个goroutine会被唤醒。

    6.COND是什么

    Cond实现了一种条件变量,可以使用在多个Reader等待共享资源ready的场景(如果只有一读一写,一个锁或者channel就搞定了)
    每个Cond都会关联一个Lock(sync.Mutex or sync.RWMutex),当修改条件或者调用Wait方法时,必须加锁,保护condition

    7.BROADCAST和SIGNAL的区别

    func (c *Cond) Broadcast()
    

    Broadcast 会唤醒所有等待c的goroutine。调用Broadcast的时候,可以加锁,也可以不加锁

    func (c *Cond) Signal()
    

    Signal只唤醒一个等待c的goroutine,调用Signal的时候,可以加锁,也可以不加锁

    8.COND中wait的使用

    func (c *Cond) Wait()
    

    Wait()会自动释放c.L, 并挂起调用者的goroutine,之后恢复执行,Wait()会在返回时对c.L加锁。
    除非被Signal或者Broadcast唤醒,否则Wait()不会返回。
    由于Wait()第一次恢复时,c.L并没有加锁,所以当Wait返回时,调用者通常并不能假设条件为真。取而代之的是,调用者应该在循环中调用Wait。(简单来说,只要想使用condition,就必须加锁)

    c.L.Lock()
    for !condition() {
      c.Wait()
    }
    ... make use of condition ...
    c.L.Unlock()
    

    9.WaitGroup的用法

    一个WaitGroup对象可以等待一组协程结束。

    使用方法是:

    1.main协程通过调用wg.Add(delta int) 设置worker协程的个数,然后创建worker协程
    2.worker协程结束以后,都要调用wg.Done()
    3.main协程调用wg.Wait()并且被block,直到所有worker协程全部执行结束后返回

    10.WaitGroup的实现原理

  • WaitGroup主要维护了2个计数器,一个是请求计数器v,一个是等待计数器w,二者组成一个64bit的值,请求计数器占高32bit,等待计数器占低32bit

  • 每次Add执行,请求计数器v+1,Done()方法执行,请求计数器v-1,v为0时通过信号量唤醒Wait()

    11.什么是Sync.Once

  • Once可以用来执行且仅执行一次动作,常常用于单例对象的初始化场景。

  • Once常常用来初始化单例资源,或者并发访问只需要初始化一次的共享资源,或者在测试的时候初始化一次测试资源。
  • sync.Once只暴露了一个方法Do,你可以多次调用Do方法,但是只有第一次调用Do方法时f参数才会执行,这里的f是一个无参数无返回值的函数

    12.什么操作叫原子操作

    一个或多个操作在CPU执行过程中不被中断的特性,称为原子性(atomicity)。这些操作对外表现成一个不可分割的整体,他们要么都执行,要么都不执行,外界不会看到他们只执行一半的状态。而在现实世界中,CPU不可能不中断的执行一系列操作,但如果我们在执行多个操作时,能让他们的中间状态对外不可见,那我们就可以宣称他们拥有了“不可分割”的原子性。
    在go中,一条普通的赋值语句其实并不是一个原子操作。例如,在32位的机器上写int64类型的变量就会有中间状态,因为他们会被拆成两次写操作(MOV) — 写低32位和高32位。

    13.原子操作和锁的区别

    原子操作由底层硬件支持,而锁则由操作系统的调度器实现。锁应当用来保护一段逻辑,对于一个变量更新的保护,原子操作通常更有效率,并且更能利用计算机多核的优势,如果要更新的是一个复合对象,则应当使用atomic.Value封装好的实现。

    14.什么CAS

    CAS的全称为Compare And Swap,直译就是比较交换。是一条CPU的原子指令,其作用是让CPU先进行比较两个值是否相等,然后原子地更新某个位置的值,其实现方式是给予硬件平台的汇编指令,在intel的CPU中,使用的cmpxchg指令,就是说CAS是靠硬件实现的,从而在硬件层面提升效率。

    简述过程是这样的:

    假设包含3个参数内存位置(V),预期原值(A)和新值(B)。V表示要更新变量的值,E表示预期值,N表示新值。仅当V值等于E值时,才会将V的值设为N,如果V值和E值不同,则说明已经有其他线程在做更新,则当前线程什么都不做,最后CAS返回的当前V的真实值。CAS操作时抱着乐观的态度进行,它总是认为自己可以成功完成操作。基于这样的原理,CAS操作即使没有锁,也可以发现其他线程对于当前线程的干扰。

    15.Sync.Pool有什么用

    对于很多需要重复分配、回收内存的地方,sync.Pool是一个很好的选择。频繁的分配、回收内存会给GC带来一定的负担,严重的时候CPu的毛刺,而sync.Pool可以将暂时不用的对象缓存起来,待下次需要的时候直接使用,不用再次经过内存分配,复用对象的内存,减轻GC的压力,提升系统的性能。

Go Routine

Goroutine的定义

Goroutine是一个与其他goroutines并行运行在同一地址空间的GO函数或方法。一个运行的程序由一个或多个goroutine组成。它与线程、协程、进程等不同。它是一个goroutine —— Rob Pike
Goroutines 在同一个用户地址空间里并行独立执行 functions,channel则用于goroutines间的通信和同步访问控制

GMP指的是什么

G(Goroutine) :我们所说的协程,为用户级的轻量级线程,每个Goroutine对象的sched保存着其上下文信息。
M(Machine) :对内核级线程的封装,数量对应真实的CPU数(真正干活的对象)。
P (Processor):即为G和M的调度对象,用来调度G和M之间的关联关系,其数量可通过GOMAXPROCS()设置,默认为核心数。

1.0之前GM调度模型

调度器把G都分配到M上,不同的G在不同的M并发运行时,都需要向系统申请资源,比如堆栈内存等,因为资源是全局的,就会因为资源竞争造成很多性能损耗。为了解决这一问题go从1.1版本引入,在运行的时候加入process对象,让process去管理goroutine对象,machine想要运行goroutine,必须绑定process,才能运行process所管理的goroutine

  1. 单一全局互斥锁(Sched.Lock)和集中状态存储
  2. Goroutine传递问题(M经常在M之间传递“可运行的”goroutine)
  3. 每个M做内存缓存,导致内存占用过高,数据局部性较差
  4. 频繁syscall调用,导致严重的线程阻塞/解锁,加剧额外的性能损耗

    GMP调度流程WeChatb813515ea808340962521c01787f33f1.png

  • 每个P有个局部队列,局部队列保持待执行的goroutine(流程2),当M绑定的P的局部队列已经满了之后,就会把goroutine放到全局队列(流程2.1)
  • 每个P和一个M绑定,M是真正执行P中goroutine的实体(流程3),M从绑定的P中的局部队列获取G来执行
  • 当M绑定的P的局部队列为空时,M会从全局队列获取到本地队列来执行G(流程3.1) 从其他P的局部队列来偷取一半的G来执行(流程3.2),这种从其他P偷的方式成为work stealing
  • 当G因系统调用(syscall)阻塞时会阻塞M,此时P会和M解绑,即hand off,并寻找休眠M队列中新的M,若没有休眠的M就会新建一个M(流程5.2)
  • 当G因channel或者network I/O阻塞时,不会阻塞M,M会寻找其他runnable的G;当阻塞的G恢复后会重新进入runnable,进入P队列等待执行(流程5.3)

    GMP中Work Stealing机制*

    线程想运行任务就得获取P,从P的本地队列获取G,P队列为空时,M会尝试从全局队列一批G放到P的局部队列,或从其他P的局部队列一半放到自己P的局部队列。

    GMP中Hand Off机制

    当本线程M因为G进行的系统调用阻塞时,线程释放绑定的P,将P转移给其他空闲的M’执行。当发生上下文切换时,需要对执行现场保护起来,以便下次被调度执行时进行现场恢复。要将Go调度器M的栈保存在G对象上,只需要将M所需要的寄存器(SP、PC等)保存到G对象上就可以实现现场保护。当这些寄存器数据被保护起来,就随时可以做上下文切换了。如果此时G任务还没有执行完,M可以将任务重新丢到P的任务队列,等待下一次被调度执行。当再次被调度执行时,M通过访问G的vdsoSP,vdsoPC寄存器进行现场恢复(从上次中断位置继续执行)

    协作式的抢占式调度

    在1.14版本之前,程序只能依靠Goroutine主动让出CPU资源才能触发调度(类似coroutine?),存在问题:

  • 某些Goroutine可以长时间占用线程,造成其他goroutine饥饿

  • 垃圾回收需要暂停整个程序(stop-the-world,STW),最长可能几分钟时间,导致整个程序无法工作。

    基于信号的抢占式调度*(看一下那个微信文章)

    在任何情况下,Go运行时并执行(注意,不是并发)的goroutine数量是小于等于P的数量的。为了提高性能,P的数量肯定不是越小越好,所以官方默认值就是CPU的核心数,设置过小的话,如:一个持有P的M,由于P当前执行的G调用了syscall而导致M被阻塞,那么此时关键点:Go的调度器是迟钝的,它可能什么都没做,直到M阻塞了相当长时间以后,才会发现有一个P/M被syscall阻塞了。然后,才会用空闲的M来抢这个P。通过sysmon监控实现的抢占式调度,最快在20us,最慢在10-20ms就能发现有一个M持有P并阻塞了。

    基于信号的异步抢占的全过程:

  1. M注册一个SIGURG信号的处理函数:sighandler
  2. sysmon线程检测到执行时间过长的goroutine、GC stw时,会向相应的M(或者说线程,每个线程对应一个M)发送SIGURG信号。
  3. 收到信号后,内核执行sighandler函数,通过pushCall插入asyncPreempt函数调用
  4. 回到当前goroutine执行asyncPreempt函数,通过mcall切到g0栈执行gopreempt_m
  5. 将当前goroutine插入到全局可运行队列,M则继续寻找其他goroutine来运行
  6. 被抢占的goroutine再次调度过来执行时,会继续原来的执行流。

    GMP调度过程中存在哪些阻塞

  • I/O,select
  • syscall
  • channel
  • 等待锁
  • runtime.Gosched()

    Sysmon有什么作用

    sysmon也叫监控线程,变动的周期性检查,好处:

  • 释放闲置超过5分钟的span物理内存

  • 如果超过2分钟没有垃圾回收,强制执行
  • 将长时间未处理的netpoll添加到全局队列
  • 向长时间运行的G任务发出抢占调度(超过10ms的g,会进行retake)
  • 收回因syscall长时间阻塞的P

    什么是三色标记

    664741-20200114133941665-921881252.gif
    三色标记法是传统Mark-Swap的一个改进,它是一个并发的GC算法。

    什么是写屏障,插入写屏障,删除写屏障,混合写屏障

    插入写屏障

    golang的回收没有混合屏障之前,一直是插入写屏障,由于栈赋值没有hook的原因,所以栈中没有启用写屏障,所以有STW。golang的解决办法是:只是需要在结束时启动STW来重新扫描栈。这个自然就会导致整个进程的赋值器卡顿,所以后面golang是引用混合写屏障来解决这个问题。混合写屏障之后,就没有STW。

    删除写屏障

    golang没有这一步,golang的内存写屏障是由写屏障到混合写屏障过渡的。简单介绍一下,一个对象即使被删除了最后一个指向它的指针也依旧可以活过这一轮,而在下一轮GC被清理掉。

    写屏障

    GO在进行三色标记的时候并没有STW,也就是说,此时的对象还是可以进行修改。那我们考虑一下下面的情况:E{F]V4P)DINCCUTXX`I2$7T.png
    我们在进行三色标记扫描灰色集合中,扫描到了对象A,并标记了对象A的所有引用,这时候,开始扫描对象D的引用,而此时另一个Goroutine修改了D->E的引用,变成了如下图:A_5SJWEI`COTGXMRGMSPSG4.png
    这样会不会导致E对象就被扫描不到而被认为是白色对象,也就是垃圾。写屏障就是为了解决这样的问题,引入写屏障之后,在上述步骤后,E会被认为是存活的,即使后面E被对象A抛弃,E会在下一轮被回收,这一轮GC是不会对E进行回收的。

    混合写屏障

  • 混合写屏障继承了写屏障的优点,起始无需STW打快照,直接并发扫描垃圾即可

  • 混合写屏障继承了删除写屏障的优点,赋值器是黑色赋值器,GC期间,任何在栈上创建的新对象,均为黑色。扫描过一次就不需要再扫描了,这样就消除了插入写屏障时期最后的STW的重新扫描栈
  • 混合写屏障扫描精度继承了删除写屏障,比插入写屏障更低,随着带来的是GC全程无STW
  • 混合写屏障扫描栈虽然没有STW,但是扫描某一个具体的栈的时候,还是要停止这个goroutine赋值器的工作的(针对一个goroutine栈来说,是暂停扫的,要么全黑,要么全灰,原子状态切换)

    GC触发时机*

    主动触发:调用runtime.GC
    被动触发:使用系统监控,该触发条件由runtime.forcegcperiod变量控制,默认为2分钟。当超过两分钟没有产生任何GC时,强制触发GC。
    使用步调(Pacing)算法,其核心思想时控制内存增长的比例。如GO的GC时一个比例GC,下一次GC结束时的堆大小和上一次GC啊存货的堆大小成比例,由GOGC控制,默认100,即2倍的关系,200就是3倍,当GO新创建的对象所占用的内存大小,除以上次GC结束后保留下来的对象占用的大小

    Go语言中GC的流程是什么?

    一 经典的GC算法
  1. 引用计数(reference counting)
  2. 标记-扫描(mark & sweep)
  3. 复制收集(copy & collection)

二 标记-清扫(mark & sweep)算法
golang的gc算法主要是基于标记-清扫(mark & sweep)算法,在了解go的gc前先了解一下传统的标记-清扫算法:

  • 标记
  • 清除

mark & sweep算法在执行的时候,需要程序暂停(stw),大致步骤是:

  1. stw暂停程序执行
  2. 找到root根对象可以到达的对象做好标记
  3. 清除没有做标记的对象
  4. start the world 开启程序执行

三 golang的清扫流程(三色并发标记)分四个阶段:
第一阶段:gc开始(stw)

  1. stw暂停程序执行
  2. 启动标记工作携程(mark worker goroutine),用于第二阶段
  3. 启动写屏障
  4. 将root根对象放入标记(放入标记队列里的就是灰色)
  5. start the worl取消程序暂停,进入第二阶段

第二阶段: marking(这个阶段,程序跟标记协程是并行的)

  1. 从标记队列里取出对象,标记为黑色
  2. 然后检测是否指向了另一个对象,如果有,将另一个对象放入标记队列
  3. 在扫描的过程中,程序如果新创建或者修改了对象,就会触发写屏障,将对象放入单独的marking队列,也就是标记为灰色
  4. 扫描完标记队列里的对象,就会进入第三阶段

第三阶段:处理marking过程中修改的指针(stw)

  1. stw暂停程序
  2. 将marking阶段 修改的对象 触发写屏障产生的队列里的对象取出,标记为黑色
  3. 然后检测是否指向了另一个对象,如果有,将另一个对象放入标记队列
  4. 扫描完marking队列里的对象,start the world取消暂停程序,进入第四阶段

第四阶段:sweep清除白色对象
到这一阶段,所有的内存要么是黑色,要么是白色,清除所有白色的即可,golang的内存管理结构中有一个bitmap区域,其中可以标记是否“黑色”

GC如何调优

通过go tool pprof 和 go tool trace等工具

  • 控制内存分配的速度,限制goroutine的数量,从而提高赋值器对CPU的利用率
  • 减少并复用内存,例如使用sync.Pool来复用需要频繁临时创建的对象,例如提前分配足够的内存来降低多余的拷贝
  • 需要时,增大GOGC的值,降低GC的运行频率

    有几种方法关闭goroutine?一个goroutine可以kill另一个goroutine吗?为什么

    有几种方法关闭goroutine?

  1. close() 借助close函数关闭
  2. <-done 轮训寻找done信号来关闭
  3. context 借助context.WithCancel()来关闭

goroutineA可以关闭goroutineB吗?

不能,go语言中,goroutine只能自己主动退出,一般通过channel来控制,不能被外界或其他goroutine关闭,也没有goroutine句柄的显示概念。因为如果一个goroutine被外部强行停止,它拥有的资源、堆栈、defer是否被执行等都会遇到问题,并且无法预知何时goroutine会被其他进程关闭会导致程序变得难以维护。

微服务

对微服务的了解

微服务,又称微服务架构,是一种架构风格,它将应用程序构建为以业务领域为模型的小型自治服务集合。
通俗的说,你必须看到蜜蜂如何通过对齐六角形蜡细胞来构建它们的蜂窝状物。他们最初从使用各种材料的小部分开始,并继续从中构建一个大型蜂箱。这些细胞形成图案,产生坚固的结构,将蜂窝的特定部分固定在一起。
这里,每个细胞独立于另一个细胞,但它也与其他细胞相关。这意味着对一个细胞的损害不会损害其他细胞,因此,蜜蜂可以在不影响完整蜂箱的情况下重建这些细胞。
WA_8`3%F]L%NJGTTVRZ0G}F.png
请参考上图。这里,每个六边形形状代表单独的服务组件。与蜜蜂的工作类似,每个敏捷团队都使用可用的框架和所选的技术堆栈构建单独的服务组件。就像在蜂箱中一样,每个服务组件形成一个强大的微服务架构,以提供更好的可拓展性。此外,敏捷团队可以单独处理每个服务组件的问题,而对整个应用程序没有影响或影响最小。

说说微服务架构的优势

优势 说明
独立开发 所有微服务都可以根据各自的功能轻松开发
独立部署 根据他们所提供的服务,可以在应用中单独部署
故障隔离 即使应用中的一个服务不起作用,系统仍然在运行
混合技术栈 可以用不同的语言和技术来构建同一应用程序的不同服务
粒度缩放 各个组件可根据需要进行拓展,无需将所有组件融合在一起

微服务有哪些特点

  • 解耦 —— 系统内的服务很大程度是分离的。因此,整个程序可以轻松构建,更改和拓展
  • 组件化 —— 微服务被视为可以轻松更换和升级的独立组件
  • 业务能力 —— 微服务非常简单,专注于单一功能
  • 自治 —— 开发人员和团队可以彼此独立工作,从而提高速度
  • 持续交付 —— 通过软件创建,测试和批准的系统自动化,允许频繁发布软件
  • 责任 —— 微服务不关注应用程序作为项目。相反,它们将应用程序视为他们负责的产品
  • 分散治理 —— 重点是使用正确的工具来做正确的工作。这意味着没有标准化模式或任何技术模式。开发人员可以自由选择最有用的工具来解决他们的问题。
  • 敏捷 —— 微服务支持敏捷开发。任何新功能都可以快速开发并再次丢弃

    设计微服务的最佳实践是什么*

    微服务架构如何运作?

    微服务架构具有以下组件

  • 客户端 —— 来自不同设备的不同用户发送请求

  • 身份提供商 —— 验证用户或客户身份并颁发安全令牌
  • API网关 —— 处理客户端请求
  • 静态内容 —— 容纳系统的所有(静态?)内容
  • 管理 —— 在节点上平衡服务并识别故障
  • 服务发现 —— 查找微服务之间通信路径的指南
  • 内容交付网络 —— 代理服务器及其数据中心的分布式网络
  • 远程服务 —— 启用驻留在IT设备网络上的远程访问信息

    微服务架构的优缺点是什么

    优点:
    自由使用不同的技术
    每个微服务都侧重于单一功能
    支持单个可部署单元
    允许经常发布软件
    确保每个服务的安全性
    多个服务是并行开发和部署的
    缺点
    增加故障排查挑战
    由于远程呼叫而增加延迟
    增加了配置和其他操作的工作量
    难以保持交易安全
    艰难地跨越各种便捷跟踪数据
    难以在服务之间进行编码

    单片、SOA和微服务架构有什么区别

    G_@F`SL{K33}LG0D)1%57VU.png
    单片SOA和微服务之间的比较

  • 单片架构类似于大容器,其中应用程序的所有软件组件组装在一起并紧密封装

  • 一个面向服务的架构是一种相互通信服务的集合。通信可以设计简单的数据传递,也可以涉及两个或多个协调某些活动的服务。
  • 微服务架构是一种架构风格,它将应用程序构建为以业务域为模型的小型自治服务集合。

    在使用微服务架构时,面临哪些挑战

    开发一些较小的微服务听起来很容易,但开发它们经常遇到的挑战如下:

  • 自动化组件:难以自动化,因为有许多较小的组件。因此,对于每个组件,我们必须遵循:Build、Deploy和Monitor的各个阶段

  • 易感性:将大量组件维护在一起变得难以部署,维护,监控和识别问题。他需要在所有组件周围有很好的感知能力。
  • 配置管理:有时在各种环境中维护组件的配置变得困难。
  • 调试:很难找到错误的每一项服务。维护集中式日志技术和仪表盘以调试问题至关重要。

    SOA和微服务架构之间的主要区别是什么

    SOA
    遵循“尽可能多的共享”架构方法,重要性在于“业务功能”重用,他们有共同的治理和标准,使用企业服务总线(ESB)进行通信,支持多种消息协议。多线程,有更多的开销来处理I/O,最大化应用程序服务可重用性,传统的关系数据库更常用,系统的变化需要修改整体。DevOps/CD变得流行,但还不是主流
    微服务
    遵循“尽可能少分享”架构方法,重要性在于“有界背景”的概念,他们专注于人们的合作和其他选择的自由。简单的消息系统,他们使用轻量级协议,如HTTP/REST等。单线程,通常使用Event Loop功能进行非锁定I/O处理。专注于解耦。现代关系数据库更常用,系统的变化是创造一种新的服务。专注于DevOps/CD

    微服务有什么特点*

    什么是领域驱动设计*

    为什么需要领域驱动设计(DDD)*

    什么是无所不在的语言?

    如果必须定义泛在语言(UL),那么它是特定域的开发人员和用户的通用语言,通过该语言可以轻松解释域。
    无处不在的语言必须非常清晰,以便它将所有团队成员放在同一页面上,并以机器可以理解的方式进行翻译。

    什么是凝聚力?*

    模块内部元素所属的程度被认为是凝聚力(????)

    什么是耦合?

    组件之间依赖关系强度的度量被认为是耦合。一个好的设计总是被认为具有高度内聚力和低耦合性。

    什么是REST/RESTFUL以及它的用途是什么

    Representational State Transfer(REST)/RESTful Web服务是一种帮助计算机系统通过Internet进行通信的架构风格。这使得微服务更容易理解和实现。
    微服务可以使用或不使用RESTful API实现,但是用RESTful API构建松散耦合的微服务总是更容易

    什么是不同类型的微服务测试?

    在使用微服务时,由于有多个微服务协同工作,测试变得非常复杂。因此,测试分为不同的级别

  • 在底层,我们有面向技术的测试,如单元测试和性能测试。这些是完全自动化的。

  • 在中间层,我们进行了诸如压力测试和可用性测试之类的探索性测试。
  • 在顶层,我们的验收测试数量很少。这些验收测试有助于利益相关者理解和验证软件功能。