Go常见问题与答案笔记

宁睿  论坛元老 | 2025-3-26 01:21:19 | 显示全部楼层 | 阅读模式
打印 上一主题 下一主题

主题 1949|帖子 1949|积分 5857

这是一篇值的收藏的go常见问题与答案的笔记,内容包括了go的高级,如:灰心锁与乐观锁区别,for range赋值、waitgroup底层原理、go同步原语、defer关键字讲解。

  
1.灰心锁VS乐观锁的区别

特性灰心锁乐观锁加锁时机访问数据前加锁访问数据时不加锁,更新时查抄适用场景写场景较多的场景读操纵较多的场景性能开销加锁息争锁的开销较大无加锁开销,但可能需要重试实现复杂度较简单较复杂(需要处理冲突和重试)典范实现sync.Mutex、数据库行级锁atomic、数据库版本号机制
  1. type Service struct {
  2.         mutex   sync.Mutex
  3.         version int
  4. }
  5. func (s *Service) Deploy() {
  6.         s.mutex.Lock()
  7.         defer s.mutex.Unlock()
  8.         s.version++
  9. }
  10. type Service1 struct {
  11.         version atomic.Int32
  12. }
  13. func (s *Service1) Deploy() {
  14.         for {
  15.                 v := s.version.Load()
  16.                 n := v + 1
  17.                 if s.version.CompareAndSwap(v, n) {
  18.                         fmt.Println("更新成功")
  19.                         break
  20.                 }
  21.         }
  22. }
复制代码
2.for range中赋值的变量,这个变量指向的是真实地址吗?还是临时变量?

如果在for range 内里有一个函数,这个函数需要传入一个指针,这个时候应该怎么写?
  1. func main() {
  2.         s := []int{10, 11, 12, 13, 14, 15, 16}
  3.         for i, v := range s {
  4.                 fmt.Printf("%p,%p,%d,%d\n", &i, &v, i, v)
  5.                 f1(&v)
  6.         }
  7.         fmt.Println(s)
  8.         mp := map[string]string{"a": "v1", "b": "v2", "c": "v3"}
  9.         for k, v := range mp {
  10.                 fmt.Printf("%p,%p,%s,%s\n", &k, &v, k, v)
  11.                 f2(&v)
  12.         }
  13.         fmt.Println(mp)
  14. }
  15. func f1(i *int) {
  16.         *i = 12
  17. }
  18. func f2(i *string) {
  19.         *i = "aa"
  20. }
复制代码
3.我能不能在写入channel的时候,判断是否阻不阻塞?

(1)多路复用select,且有default环境下可以避免阻塞
(2)使用len与cap函数,可以查抄channel状态
4、如果我在defer内里修改return内里的值呢?这时怎么写?

defer的执行次序
defer执行会在我们函数return之后,而在外层函数去接受return值之前
  1. func add(a,b int)(sum int){
  2.         sum = a+b
  3.         defer func() {
  4.                 sum = a + b
  5.         }
  6.         return
  7. }
复制代码
5、结构体中的tag有什么作用

JSON序列化与反序列化

  1. type Person struct {
  2.         Name string `json:"name"`
  3.         Age int `json:"age"`
  4. }
复制代码
通过结构体tag来指定字段的JSON键名
数据库操纵

结构体tag用于指定数据库表和字段名
  1. type User struct {
  2.         ID int `gorm:"primaryKey"`
  3.         Name string `gorm:"column:username"`
  4. }
复制代码
字段验证

validator库答应你使用tag来验证结构体字段的值是否合法
  1. type Product struct {
  2.         Name string `validate:"required"`
  3.         Price float64 `validate:"gt=0"`
  4. }
复制代码
反射读取结构体Tag

可以读取息争析结构体的tag
  1. import (
  2.     "fmt"
  3.     "reflect"
  4. )
  5. type Person struct {
  6.     Name string `json:"name"`
  7.     Age  int    `json:"age"`
  8. }
  9. func main() {
  10.     p := Person{Name: "John", Age: 30}
  11.     t := reflect.TypeOf(p)
  12.     for i := 0; i < t.NumField(); i++ {
  13.         field := t.Field(i)
  14.         fmt.Println("Field:", field.Name, "Tag:", field.Tag)
  15.     }
  16. }
复制代码
6、waitgroup的底层原理是什么

waitgroup是通过一个计数器来实现的,这个计数器跟踪一组goroutine的执行状态,确保主线程等待这些goroutine完成后才继续执行。
state主要是存储着状态和信号量,状态维护了2个计数器,1个是哀求计数器counter,另外一个是等待计数器waiter
当数组首地址是处于一个8字节对齐的位置上时,那么就将这个数组前8个字节作为64位值使用表示状态,后4个字节作为32位值表示信号量,同理如果首地址没有处于8字节对齐的位置上时,那么就将前4个字节作为semaphore,后8个字节作为64位数值.
waitgroup的操纵可以总结为三种方法:


  • Add(n int) : Add用于设置WaitGroup中等待的goroutine数量,通常在启动goroutine之前调用,n是需要等待的goroutine数量,可以是正数,也可以是负数。
  • Done() :Done用于镌汰WaitGroup的计数器,每个goroutine在执行完成时会调用Done() ,表示它已经结束,通常会在goroutine内部调用defer wg.Done(),以确保无论goroutine是否正常执行完成,都能镌汰计数器
  • Wait():它会阻塞当前goroutine直到waitgroup中的计数器变为0,通常用于主goroutine内,它会等待全部子gotouine完成后才继续执行
工作原理


  • 初始化:创建一个WaitGroup实例,初始时计数器值为0
  • 增加计数:通过调用Add(n)来增加计数器,表示启动了n个goroutine
  • 镌汰计数器:每个goroutine执行完成后,调用Done()镌汰计数器
  • 等待完成:主goroutine(或其他需要等待的goroutine)调用Wait()阻塞,直到计数器归0,即全部goroutine都已完成.
7、go语言有哪些同步原语



  • sync.Mutex:用于根本的互斥锁
  • sync.RWMutex:提供读写锁,适用于读多写少的场景
  • sync.WaitGroup:用于等待多个goroutine完成
  • sync.Once:确保某个操纵只执行一次
  • sync.atomic:提供低级的原子操纵,避免锁
  • sync.Cond:提供条件变量,用于复杂的同步场景.
  • channel:高级同步原语
8、如果chan在有缓冲的环境下缓冲区满了不想要后续的数据了怎么做

扬弃数据或者停止写入
  1. func main() {
  2.         ch := make(chan int, 10)
  3.         done := make(chan struct{})
  4.         //接受方
  5.         go func() {
  6.                 for {
  7.                         select {
  8.                         case <-done:
  9.                                 return
  10.                         default:
  11.                                 select {
  12.                                 case i := <-ch:
  13.                                         fmt.Println(i)
  14.                                 }
  15.                         }
  16.                 }
  17.         }()
  18.         go func() {
  19.                 i := 0
  20.                 for {
  21.                         select {
  22.                         case ch <- i:
  23.                                 i++
  24.                                 fmt.Println("case")
  25.                         default:
  26.                                 close(done)
  27.                                 close(ch)
  28.                                 fmt.Println("default")
  29.                                 return
  30.                         }
  31.                 }
  32.         }()
  33.         time.Sleep(1 * time.Second)
  34. }
复制代码
9、内存泄漏有哪些场景



  • goroutine没有及时关闭:比如让它tiime.sleep好久
  • 长时间存在的引用
  • 未关闭的资源(文件、网络、数据库等)
  • 错误的缓存使用
  • 闭包导致的引用循环
  • 使用map时未清理的数据
  • 未精确使用sync.Pool
10、切片的复制过程



  • 创建目标切片:复制前,目标切片(dst)必须有富足的容量来存放复制的数据。如果目标切片的容量不足,copy操纵仍然会将源切片的数据复制到目标切片,直到目标切片的容量达到限制。
  • 元素复制:copy 函数会按次序复制源切片中的元素到目标切片中,直到复制的元素数量达到目标切片或源切片的长度为止。
深拷贝,不会内存泄漏,
11、go内里如何解决hash冲突的



  • 计算哈希值:首先,Go 会通过哈希函数(例如 FNV-1a)来计算键的哈希值。
  • 定位桶:根据哈希值和当前哈希表的大小,确定目标桶的位置。
  • 冲突处理:如果桶已经有元素,Go会使用链式哈希(即将新元素添加到桶的链表中)。如果冲突过多,可能会触发再哈希操纵,重新分配更多的桶并重新计算键的哈希值。
12、切片与数组的区别



  • 切片是对数组的引用
13、go并发编程如何避免死锁

sync、channel、waitgroup、资源竞争
14、defer关键字



  • defer执行位置,return之后,接受之前
  • 多个defer的执行次序:栈结构
  • defer函数传参:形成闭包
  • defer非常捕获和处理:用recover
15、Map并发安全吗?如果是sync.map,它是如何包管并发安全?

Map在sync,atomic与channel里是并发安全,用互斥锁解决并发安全
16、goroutine使用场景



  • 并发任务,如web
  • 异步操纵:如长时间运行的任务
  • 实时计算或数据处理:实时监控
  • 并行任务计算:分布式计算
  • 并发收据网络与汇总:数据归并
  • 工作池模型:任务分发
17、goroutine怎么做同步机制



  • 使用sync、atomic
  • 使用Channel
18、atomic介绍一下,它有哪些方法

atomic提供了一组原子操纵,用于在并发环境下执行无锁的操纵,避免了使用互斥锁的开销,原子操纵是在多个线程(Goroutine)中对共享数据进行安全访问的操纵,sync/atomic用于处理数值范例和指针范例


  • AddInt32/AddInt64
  • LoadInt32/LoadInt64
  • StoreInt32/StoreInt64
  • CompareAndSwapInt32/CompareAndSwapInt64
  • SwapInt32/SwapInt64
19、sync.Map数据写入流程



  • 尝试查找键:首先会尝试从 sync.Map 的 Read-Write Bucket 中找到对应的键。如果找到了,则更新这个键的值。
  • 数据迁移:如果该键已经存在于 Read-Write Bucket 中,且存在更新,则更新它。否则,若这个键不存在,sync.Map 会把它添加到 Read-Write Bucket 中。
  • 写入到 Dirty Map:如果进行写入时发生了 Store 操纵的冲突,或者本来存在的值被更换,旧值会被移动到 Dirty Map中。Dirty Map 记录了需要延迟清理的映射数据。
  • 清理与回收:由于 sync.Map 的设计答应通过延迟清理来提高并发性能,所以在后续的操纵中,Dirty Map 中的过期数据会被清理。
20、sync.Map数据读取流程

1、底层数据结构
sync.Map 的底层实现采用了多个数据结构,包括:
Read-Write Bucket (读取-写入桶):存储正常的键值对(map)。
Dirty Map (脏数据映射):存储正在修改的键值对,只有当一个键值对正在被更新时,才会临时移动到这个存储桶。
2. 读取流程(Load)
当调用 sync.Map.Load(key) 来读取数据时,以下步调会发生:
直接查找 Read-Write Bucket:首先,sync.Map 会尝试从 Read-Write Bucket 中查找是否存在对应的键。如果键存在,直接返回值。
查抄 Dirty Map:如果在 Read-Write Bucket 中找不到键,sync.Map 会查抄 Dirty Map,看看是否有被标记为更新中的键。如果有,则从 Dirty Map 中获取数据。
返回结果:
如果找到了对应的键,Load 方法会返回该值,并且操纵是并发安全的。
如果找不到对应的键,Load 方法会返回 nil(或者你可以指定默认值)以及 false,表示未找到该键。
21、sync.Map中Dirty与Read转化过程

1. 初始状态(Load 操纵)
当你首次加载一个键时,sync.Map 会首先尝试在 Read-Write Bucket 中查找该键。如果键存在,就直接返回对应的值。
2. 更新操纵(Store 操纵)
当调用 Store 方法更新一个键时,如果该键的值已经存在于 Read-Write Bucket 中,那么它会被“标记为脏”(转移到 Dirty Map)。在并发环境下,这个操纵避免了直接修改 Read-Write Bucket 中的内容,从而镌汰了锁的争用。
如果没有找到该键,新的键值对会直接插入到 Read-Write Bucket 中。
3. 查找未更新的键(Load 操纵)
当查询一个键时,首先会在 Read-Write Bucket 查找,如果没有找到,再查看是否存在于 Dirty Map 中。通过这种方式,查询操纵的服从得到了优化,不会由于写操纵而造成查询的等待或阻塞。
4. 脏数据的处理(Dirty to Clean)
如果某个键的数据需要从脏映射中恢复(例如,颠末某些操纵后恢复到 Read-Write Bucket),它会被转移回正常的存储区域。这一过程是主动进行的,并且通常是并发安全的,通常由垃圾回收机制或者其他触发条件来管理。
5. 删除操纵
在删除操纵中,如果一个键的值被删除,它会被标记为“脏”,并移动到 Dirty Map。在删除过程中,脏映射可以快速完成删除,不会影响其他并发读操纵。
22、Sync.map使用场景以及sync.map与map的区别

当给定键对应的条目只写入一次但会被多次读取时,比如:只增不减的缓存;当多个goroutine对互不相交的键集合进行读取,写入以及覆盖操纵时,在这两种环境下,与搭配独立Mutex(互斥锁)或RWMutex(读写锁)的平凡Map相比,会显著镌汰争用环境
23、go协程可能引发那些问题?



  • 竞态
  • 闭包
  • 死锁
  • 协程泄漏
24、go实现一个简单的多态

  1. package main
  2. import (
  3.         "fmt"
  4.         "math"
  5. )
  6. type Shape interface {
  7.         Area() float64
  8. }
  9. type Rectangle struct {
  10.         Width  float64
  11.         Height float64
  12. }
  13. func (r Rectangle) Area() float64 {
  14.         return r.Height * r.Width
  15. }
  16. type Circle struct {
  17.         Radius float64
  18. }
  19. func (c Circle) Area() float64 {
  20.         return math.Pi * c.Radius * c.Radius
  21. }
  22. func main() {
  23.         shapes := []Shape{
  24.                 Rectangle{10, 20},
  25.                 Circle{10},
  26.         }
  27.         for _, shape := range shapes {
  28.                 fmt.Println(shape.Area())
  29.         }
  30. }
复制代码
25、go实现一个简单的cache

  1. type Cache struct {
  2.         data map[string]interface{}
  3.         mutex sync.RWMutex
  4. }
  5. func NewCache() *Cache {
  6.         return &Cache{
  7.                 data : make(map[string]interface{})
  8.         }
  9. }
  10. func (c *Cache)Set(key string,value interface{}){
  11.         c.mutex.Lock()
  12.         defer c.mutex.Unlock()
  13.         c.data[key] = value
  14. }
  15. func (c *Cache)Get(key string)(interface{},bool){
  16.         c.mutex.RLock()
  17.         defer c.mutex.RUnlock()
  18.         value,exists := c.data[key]
  19.         return value,exists
  20. }
  21. func (c *Cache)Delete(key string){
  22.         c.mutex.Lock()
  23.         defer c.mutex.Unlock()
  24.         delete(c.data,key)
  25. }
复制代码
26、golang select 两个channel性能稳定,三个channel性能会发生抖动,为什么

1. 调度器调度和竞争
Go 的调度器是基于抢占的。每个 goroutine 被调度时,它有一个时间片,用来执行。多 channel 的 select 会增加调度器的工作负担,尤其是当涉及到多个并发 goroutine 时。
当你在 select 中监听两个 channel 时,调度器选择哪个 channel 更为简单,由于它只需要在两个选项中做出决策。引入第三个 channel 时,调度器的选择就变得更加复杂。每次 select 都需要从三个可能的 channel 中选择一个,增加了上下文切换的开销,可能导致一些性能的不稳定,尤其是在高并发或高负载的环境下。
2. 通道阻塞和非阻塞行为
当 select 中的 channel 都没有准备好时,select 会阻塞等待此中任何一个 channel 的准备。对于 channel 的调度和阻塞行为,Go 调度器也会做出选择,哪个 channel 阻塞了多久以及哪个 channel 变得可用会影响调度的决策。
当你有多个 channel 时,某些 channel 可能常常会阻塞,而其他的可能会敏捷准备好,造成选择的“波动”。这种波动可能在某些高并发场景下被放大,导致不稳定或抖动的表现。
3. 延迟和负载不均
假设你的 channel 和 goroutine 设计存在负载不均的问题,比如某些 channel 接受大量数据或者某些 goroutine 工作繁忙,可能会导致某些 select 中的通道频仍被选中,而另一些则较少被选中,从而导致性能上的不稳定。在三个 channel 的环境下,负载的不均衡可能更加显著。
4. CPU缓存和缓存失效
处理多个 channel 时,如果 goroutine 的工作负载较重,可能会导致 CPU 缓存不命中或缓存失效。差别的 channel 可能运行在差别的 CPU 焦点上,而在 select 中频仍切换 channel 时,可能会造成缓存局部性差,进而影响性能的稳定性。
5. 调度器的上下文切换
select 语句内部选择多个 channel 时,如果多个 goroutine 在同一时间尝试访问这些 channel,则可能会引发更频仍的上下文切换。上下文切换自己是有一定性能开销的,尤其是当 select 中有多个 channel 时,频仍的选择和调度可能引起性能的抖动。
6. select 默认分支的影响
如果你的 select 中有 default 分支,默认分支可能会影响调度器的行为。当有多个 channel 时,默认分支有可能制止其他 channel 的正常选择,导致性能抖动。
27、生产者、消耗者用有缓存channel通信场景,如何让生产者和消耗者退出



  • 通过关闭channel来通知退出
  • 通过控制信号来通知
  • 超时机制
  1. package main
  2. import (
  3.         "fmt"
  4.         "time"
  5. )
  6. func producer(ch chan<- int, done chan<- bool) {
  7.         for i := 1; i <= 5; i++ {
  8.                 ch <- i
  9.                 fmt.Println("producer ", i)
  10.                 time.Sleep(time.Second)
  11.         }
  12.         close(ch)
  13.         done <- true
  14. }
  15. func consumer(ch <-chan int, done <-chan bool) {
  16.         for {
  17.                 select {
  18.                 case item, ok := <-ch:
  19.                         if !ok {
  20.                                 fmt.Println("channel closed")
  21.                                 return
  22.                         }
  23.                         fmt.Println("consumer ", item)
  24.                 case <-done:
  25.                         fmt.Println("consumer done")
  26.                         return
  27.                 }
  28.         }
  29. }
  30. func main() {
  31.         ch := make(chan int, 3)
  32.         done := make(chan bool)
  33.         go producer(ch, done)
  34.         go consumer(ch, done)
  35.         <-done
  36. }
复制代码
28、如何获取goroutine内里的一个函数执行的返回值



  • 通过内存进行通信
  • 通过通道进行通信
29、反射原理以及反射应用场景

反射是指在运行时动态地查抄范例和修改对象的能力,他是基于reflect包实现的。
应用场景:


  • 框架和库实现:反射使得我们能够处理不确定范例的数据。
  • 接口的实现:通过反射,可以查抄一个对象是否实现了某个接口。
  • 测试和调试:反射可用于动态地查抄对象的状态。
  • 动态天生代码:反射可以用来在运行时动态天生代码
30、 go哪些数据范例是线程安全的

channel与sync.Map
31、map可寻址吗?

map不可寻址是由于它的底层实现设计上不支持直接通过引用进行修改。它的底层结构自己是动态变化的,并且在操纵过程中可能会发生扩容等等操纵。由于map的数据结构会发生变化,go设计上限制 了我们不能通过指针直接修改它,
32、map扩容两种方式



  • 基于负载因子扩容
  • 基于哈希冲突扩容
1. 基于负载因子的扩容
map 会在负载因子超过一定阈值时进行扩容。负载因子是 map 中元素的数量与底层数组大小的比率。
负载因子通常是 0.75,即当 map 中的元素数量达到其容量的 75% 时,会触发扩容。
具体扩容过程:
当 map 插入新的键值对时,如果发现负载因子超过了阈值(如 0.75),Go 会主动进行扩容,通常是将 map 的底层数组大小翻倍。
扩容时,Go 会重新计算全部现有元素的哈希值并将它们重新分配到新的数组位置上。这是由于 map 底层是基于哈希表实现的,扩容后,原有的哈希表位置可能发生变化。
2. 基于哈希冲突的处理扩容
map 的扩容也与哈希冲突的处理有关。map 使用了开放寻址法来解决哈希冲突。每当发生哈希冲突时,Go 会尝试探求一个空的位置存储该键值对。
哈希冲突:当多个键的哈希值类似或相近时,它们会被存储在同一个位置,这时会发生冲突。Go 通过开放寻址法解决冲突,将冲突的元素存储到新的位置。
当冲突太多时,map 会进行扩容,增加桶(bucket)的数量,从而镌汰冲突的频率。这个过程是自顺应的,取决于 map 中的元素数量和冲突环境。
33、自旋锁的本质是什么?

自旋锁本质是一种忙等待锁,焦点思想是通过不绝地查抄锁的状态,直到能够成功获取锁为止,而不是让线程进入阻塞状态。这种方式相对较为简单,并且适用于哪些持锁时间非常短的场景。自旋锁的特点是获取不到锁时,线程会反复轮旋锁是否可用,而不是挂起自己
34、setnx和set nx区别



  • setnx是一个独立的命令,只在键不存在时设置键值
  • set nx是set命令时的一部门,它也只有在键不存在时才设置键值,但相比setnx,它支持更多的选项,如设置键的过期时间,长期化等。
35、string和byte的区别

特性stringbyte数据范例字符串范例,表示一系列字符数字范例,表示单个字节存储内容存储文本字符(通常使用utf-8编码)存储原始的二进制数据,通常是0到255之间的数字使用场景处理文本数据、如字符串,文本内容,消息等处理二进制数据,如图像文件、加密数据、流数据等可变性在很多语言中,string是不可变的byte是可变的表示方式一系列字符或文本单个字节(通常是0到255的整数) 36、recover怎么使用的,defer相比平凡在函数最后执行操纵,其优势是什么?

recover是与panic相关的,它用于从panic中恢复,panic会导致步伐中断并开始执行defer语句,如果在defer中调用recover,就可以捕获并恢复从panic中引发的恐慌,防止步伐崩溃,纵然遇到panic,defer也会执行,平凡函数则不一样。
37、如何控制goroutine的生命周期,channel的作用,context的作用

goroutine可以通过channel与sync.Map控制,channel的作用是goroutine之间进行通信的机制,context用于控制并发操纵的生命周期,特别是处理取消信号超时控制以及哀求范围内的共享数据。
38、map,slice未初始化,操纵会怎么样,发生panic应该怎么办?

一个未初始化的map变量是nil,即它指向一个空的映射(空指针)。如果我们尝试对nil的map进行写操纵,会引发panic,但是,读操纵对nil的map不会引发panic,返回的是map范例的零值。
slice是一个引用范例,未初始化时它的零值是nil。与map差别,对nil的slice进行操纵(如追加、读取元素等)并不会引发panic,但会有一些差别的行为。
slice可以追加元素,可以正常使用append()函数向一个nil的slice追加元素,go会主动为其分配内存。
读取元素:如果尝试访问一个nil的slice中对的元素,或者在未初始化的slice中进行索引操纵,nil slice不会引发panic,直接返回零值。
发送panic,进行初始化与查抄
39、cookie与session的区别与应用

特性cookiesession存储位置存储在客户端(浏览器)存储在服务器端存储内容用户数据存储在客户端的浏览器中用户数据存储在服务器的内存或数据库中生命周期通常由客户端设置(例如设置过期时间)默认会话过期时间由服务器设置(通常会话结束或关闭浏览器)安全性不够安全,更轻易被窜改,尤其是存储敏感数据时更安全,由于数据存储在服务器,客户端只生存一个ID存储大小通常较小,一般限制为4kb大小由服务器限制,通常较大传输方式每次HTTP哀求都会携带Cookie每次HTTP哀求只需携带一个session id跨域限制Cookies不能跨域,但可以使用SameSite设置来增加跨域控制通常基于会话ID,session id不能跨域 cookie应用场景:


  • 记住用户登录状态
  • 跟踪用户行为
  • 跨哀求长期化数据
    session应用场景:
  • 存储敏感信息
  • 用户身份证验证
  • 购物车、订单管理
40、context常见应用场景



  • 超时控制
  • 任务取消
  • 传递哀求信息
  • 并发任务管理
  • 分布式系统中的哀求上下文
41、开辟多个写协程向channel写数据,有序的吗?

当多个写协程向一个共享的channel写数据时,数据的次序并不一定是有序的,具体环境取决于并发操纵的执行次序,由于go的协程是并发执行的,各个协程的执行时间和次序不固定,因此,多个协程同时写入一个channel时,数据的次序可能会受到调度的影响,导致输出的数据次序不确定
42、 channel缓冲环境下接收与发送数据的流程



  • 发送操纵:发送者会将数据放入缓存,如果缓存满了,发送者会被阻塞,直到有空间
  • 接收操纵:接收者从缓存中取数据,如果缓存为空,接收者会被阻塞,直到有数据。
43、关闭的channel接收与发送数据会出现什么环境

从关闭的channel接收数据:
如果channel中有数据,则正常接收到数据。
如果channel已经空了,接收操纵会立即返回零值,并且ok标记(第二个返回值为false),表示channel已经关闭并且没有更多的数据可接收
从关闭的channel发送数据:
如果尝试向已关闭的channel发送数据,会导致运行时panic,并且步伐会崩溃,go不答应向关闭的channel发送数据,由于没有意义
44、channel底层结构分析



  • 缓冲区:对于带缓冲区的channel,底层会有一个现实存储数据的缓冲区(通常是环形缓冲区).对于无缓冲的channnel,没有现实的缓冲区,它的作用类似于一个同步队列。
  • 发送队列:存储发送数据的goroutine列表,当channel的缓冲区已满时,发送操纵会被阻塞,直到有空间可用为止
  • 接收队列:存储接收数据的goroutine列表,当channel为空时,接收操纵会被阻塞,直到有数据可接收为止
  • 锁:为了实现对缓冲区和队列的安全访问,channel使用锁来包管并发安全,防止多个goroutine同时访问或修改channel的内部结构
channel的范例分为:
无缓冲的channel是一种同步通道。当一个goroutine发送数据到channel时,必须等待另一个goroutine接收数据。这种范例的通道没有内部的缓冲区。
有缓冲通道:有缓冲的channel在创建时会指定一个容量,数据会被存储在一个缓冲区中。发送数据到一个已满的有缓冲通道时,发送操纵会阻塞,直到有空余空间。接收数据会在缓冲区中有数据时执行。
channel的操纵:发送与接收
发送操纵:发送数据时,go语言会查抄channel是否有富足的空间。对于无缓冲的channel,发送操纵会阻塞直到有接收者。如果有缓冲的channel,发送会查抄缓冲区是否已满。
有缓冲区的channel: 数据被存储在缓冲区内,发送者会等待直到有富足的空间。
无缓冲的channel 发送操纵会阻塞,直到接收者能够接受数据.
接收操纵:接收数据时,如果channel中没有数据且没有发送者,接收操纵会阻塞。对于有缓冲的channel,接收者会从缓冲区中获取数据
. Channel 的阻塞与同步
Go 语言的 channel 是基于 goroutines 阻塞的原理来工作的。发送和接收操纵通常是阻塞式的,意味着当没有富足的资源(空间或数据)时,执行这些操纵的 goroutine 会被挂起,直到条件满足。
无缓冲的 channel:
当发送操纵发生时,发送者会阻塞,直到有接收者。
当接收操纵发生时,接收者会阻塞,直到有发送者发送数据。
有缓冲的 channel:
当发送操纵发生时,如果缓冲区已满,发送者会阻塞,直到有空间可用。
当接收操纵发生时,如果缓冲区为空,接收者会阻塞,直到有数据。
Channel 的关闭
当一个 channel 被关闭时,任何尝试向这个 channel 发送数据都会触发 panic。关闭的 channel 仍然可以被接收数据,但是接收的数据会返回 zero 值,并且 ok 标记为 false,表示 channel 已经关闭。
关闭操纵: 通过 close(ch) 来关闭一个 channel。
接收关闭的 channel: 接收操纵会返回数据,直到缓冲区为空,然后返回零值和 false。
45、面向对象的三大焦点概念与五大焦点准则



  • 封装:将数据和操纵数据的方法绑定在一起,对外部隐藏对象的内部实现细节,只提供一些公共的访问方法来操纵这些数据,从而包管了数据的安全性和完整性,防止外部随意访问和修改对象的内部状态
  • 继续:答应创建一个新的类(子类或派生类)从现有的类(父类或基类)继续属性和方法,子类可以在继续父类的底子上添加新的属性和方法,或者重写父类的方法以满足特定的需求,从而实现代码的复用和扩展
  • 多态:指差别的对象和同一消息或方法调用产生差别的响应或行为,多态性使得代码更加灵活和可维护,加强了步伐的可扩展性和维护性。
面向对象的五大原则
单一职责:一个类只有一个引起它的变化的缘故原由
开闭原则:软件实体应该对扩睁开辟,对修改关闭
里氏更换原则:子类必须能够更换掉它们的父范例,即派生类对象可以在步伐中代替基类对象是用,且不会产生错误或非常行为。
依赖倒置原则:高层模块不应该依赖底层模块,二者都应该依赖抽象,抽象不应该依赖细节,细节应该依赖抽象
接口隔离原则:客户端不应该被迫于依赖它不使用的方法,一个类对另一个类的依赖应该建立在最小的接口上
46、进程、线程、协程、go协程的区别

特性进程线程协程go协程定义操纵系统分配资源的最小单元进程中的执行单元用户级线程、轻量级执行单元go语言中轻量级执行单元资源占用高,独立的内存空间低,共享进程的资源极低,共享线程的资源极低、共享线程的资源调度操纵系统内核调度操纵系统内核调度用户空间调度Go运行时调度创建开销高,涉及内存分配和进程管理低,比进程创建开销小极低,创建烧毁开销小极低由Go运行时管理通信通过IPC,复杂且开销大通过共享内存或锁机制通信共享内存或消息传递,服从较高使用channel,简便且高效适用场景适用于需要隔离的任务适用于并发计算密集型任务适用于高并发轻量任务适用于高并发、IO密集星任务 47、select焦点机制与使用场景分析



  • 非阻塞
  • 随机选择
  • 多个操纵
  • default分支
场景:


  • 超时处理
  • 并发任务处理
  • 并发通信与同步
  • 多路复用
  • 处理错误与结果返回
48、go语言实现一个线程池

  1. //肯定要冲键值offer,既然巅峰留不住,那就重走来时路
  2. package main
  3. import (
  4.         "fmt"
  5.         "sync"
  6.         "time"
  7. )
  8. type Task func()
  9. type GoroutinePool struct {
  10.         workerCount int
  11.         taskQueue   chan Task
  12.         wg          sync.WaitGroup
  13. }
  14. func NowGoroutinePool(workerCount, queueSize int) *GoroutinePool {
  15.         return &GoroutinePool{
  16.                 workerCount: workerCount,
  17.                 taskQueue:   make(chan Task, queueSize),
  18.         }
  19. }
  20. func (gp *GoroutinePool) Start() {
  21.         for i := 0; i < gp.workerCount; i++ {
  22.                 go gp.worker(i)
  23.         }
  24. }
  25. func (gp *GoroutinePool) worker(workerId int) {
  26.         for task := range gp.taskQueue {
  27.                 task()
  28.                 gp.wg.Done()
  29.         }
  30. }
  31. func (gp *GoroutinePool) Submit(task Task) {
  32.         gp.wg.Add(1)
  33.         gp.taskQueue <- task
  34. }
  35. func (gp *GoroutinePool) Wait() {
  36.         gp.wg.Wait()
  37. }
  38. func createTask(taskID int) Task {
  39.         return func() {
  40.                 fmt.Printf("Task %d is starting...\n", taskID)
  41.                 time.Sleep(1 * time.Second)
  42.                 fmt.Printf("task %d is completed.\n", taskID)
  43.         }
  44. }
  45. func main() {
  46.         pool := NowGoroutinePool(3, 5)
  47.         pool.Start()
  48.         for i := 0; i < 10; i++ {
  49.                 task := createTask(i)
  50.                 pool.Submit(task)
  51.         }
  52.         pool.Wait()
  53.         fmt.Println("All tasks finished.")
  54. }
复制代码
49、new与make的区别

new


  • 用于分配内存,并返回指向范例的指针
  • 分配的是根本范例的内存或自定义结构体范例
  • 只返回指针,不初始化具体的值
make:


  • 用于初始化并返回内置范例的数据结构(切片,映射,通道)
  • 返回的是初始化好的数据结构对象,不是指针
  • 需要传入相关的长度、容量等参数
50、go当中同步锁有什么特点?作用是什么?

当一个Goroutine(协程)得到了Mutex后,其他Goroutine(协程)就只能乖乖的等待,除非该Goroutine开释了该Mutex,RWMutex在读锁占用的环境下,会制止写,但不制止读,在写锁占用环境下,会制止任何其他Goroutine(无论读和写)进来,整个锁相称于由该Goroutine独占同步锁的作用是包管资源在使用时的独有性,不会由于并发而导致数据庞杂,包管系统简直定性
51、如果在匿名函数内panic了,在匿名函数外的defer是否会触发panic-recover?反之在匿名函数外触发panic,是否会触发匿名函数内的panic-recover?

环境一:匿名函数内panic,外部defer是否能捕获
当你在匿名函数内部panic时,匿名函数外部的defer是不能捕获这个panic的,由于panic会流传到调用栈的上一层,直到找到一个recover进行处理。如果匿名函数外部没有明白的defer或recover来捕获该panic,这个panic会直接导致步伐崩溃
环境二:匿名函数外部panic,匿名函数内的defer是否能捕获?
如果panic在匿名函数外部触发,匿名函数内的defer是能够捕获的,由于defer是在函数退出时执行的,所以当匿名函数的调用栈返回时,defer语句会被执行。
52、读写锁的根本原理

读锁:


  • 多个并发读操纵:当一个goroutine获取了读锁时,其他goroutine也可以获取读锁。如很多个goroutine可以并行地读取资源,而不需要相互等待。
  • 阻塞写操纵:如果有任何goroutine持有读锁,写锁哀求会被阻塞,直到全部读锁都开释。
写锁


  • 独占:当一个goroutine获取了写锁时,其他全部的读锁和写锁都会被阻塞,只有持有写锁的goroutine能够访问资源。
  • 优先阻塞读操纵:写锁会阻塞全部新的读锁哀求,直到它完成,纵然有多个goroutine试图获取读锁,写锁仍然会优先执行,确保写入的独占性
53、互斥锁根本原理

Lock() :
当一个goroutine调用Lock() 时,它会尝试获取锁,如果没有其他goroutine已经持有该锁,Lock() 会成功并获取锁。如果其他goroutine已经持有该锁,调用Lock() 的goroutine将会被阻塞,直到锁被开释
Unlock() :
当一个goroutine完成对共享资源的操纵后,它会调用Unlock()来开释锁,如许其他被阻塞的goroutine就有机会获取锁并访问共享资源。
54、REST API 详细规范



  • 资源定名
  • HTTP方法使用:GET/POST/DELETE/PUT/PATCH
  • 状态码
  • 响应格式
  • 版本控制
  • 错误处理
55、go并发原语

goroutine



  • 并发任务处理:http服务器,为每个哀求启动一个goroutine进行任务处理
  • 异步调用:主流程对调用结果不关心的环境下,可以通过goroutine来模拟异步调用
  • 背景任务:如背景定时任务,定期执行某些操纵
  • 并行计算:将一个大任务拆分成若干个小任务并行执行,加速团体计算速度
channel



  • 协程间通信:差别协程间传递数据,实现数据的共享与交换,通过channel向多个goroutine分发任务或者从多个goroutine网络处理结果
  • 并发控制:通过有限容量的channel控制并发协程数量,等待多个协程完成任务
  • 事件通知:通过channel的关闭广播,来通知多个相关的协程执行退出动作;select共同多个channel可以监听多个通道的事件,实现IO多路复用
select



  • select语句用于处理一个或多个channel的发送和接收操纵,能够实现非阻塞的机制
sync包



  • WaitGroup:等待一组协程完成工作
  • Cond:通知满足条件的goroutine继续执行
  • Mutex:互斥锁,RWMutex读写锁
  • Once多协程并发场景下,仅执行一次
  • atomic确保共享变量的原子性和线程安全
  • sync.Map线程安全集合,适用于高并发场景
56、go使用什么范例?



  • method
  • bool
  • string
  • Array
  • slice
  • struct
  • pointer
  • function
  • interface
  • map
  • channel
57、语言结构



  • 关键字
  • 包声明
  • 解释
  • 函数
  • 变量和常量声明
  • 范例
  • 语句:赋值语句/条件语句/循环语句/跳转语句
  • 运算符:算术运算符/逻辑运算符/位运算符
58、go数据范例



  • 布尔型–bool
  • 数字范例–uint/int/float32/float64/byte/rune
  • 字符串范例–string
  • 复合范例:数组范例array/切片范例slice/字典范例map/管道范例channel/结构化范例struct
  • 指针范例:pointer
  • 接口范例:interface
  • 函数范例:func
  • 方法范例method
59、函数与方法的区别

函数是指不属于任何结构体、范例的方法,也就是说范例没有接收者的,而方法是有接收者的。
60、go函数返回局部变量的指针是否安全?

一般来说,局部变量会在函数返回后被烧毁,因此被返回的引用就成为了“无所指”的引用,步伐会进入未知状态。但在go中是安全的,go编译器会对每个局部变量进行逃逸分析,如果发现局部变量的作用域超出该函数,则不会将内存分配在栈上,纵然开释函数,其内容也不会受影响。
61、go函数参数传递到底是值传递还是引用传递

go语言中全部传参都是值传递(传值),都是一个副本,一个拷贝。
参数如果是非引用范例(int,string,struct等这些),如许就在函数中就无法修改原视频内容数据:如果是引用范例(指针,map,slice,channel等这些)
值传递:将实参的值传递给形参,形参是实参得一分拷贝,实参和形参的内存地址差别,函数内对形参值内容的修改,是否会影响实参的之内容,取决于实参是否是引用范例
引用传递:将实参的地址传递给形参,函数内对形参值内容的修改,就会影响实参的值内容,
int范例:形参和现实参数内存地址不一样,证明是值传递,参数是值范例,所以函数内对形参的修改,不会修改原内容数据
指针范例:形参和现实参数内存地址不一样,证明是值传递,由于形参和实参是指针,指向同一个变量,函数内对指针指向变量的修改,会修改原内容数据。
62、defer关键字实现原理

defer能够让我们推迟执行某函数调用,推迟到当前函数返回前才现实执行,defer与panic和recoverj联合,形成了go语言风格的非常与捕获机制,如:文件句柄关闭,毗连关闭,开释锁


  • 函数退出前,按照先辈后出次序,执行defer函数
  • panic后的defer函数不会被执行,遇到panic,如果没有捕获错误,函数会立即终止
  • panic没有被recover时,抛出的panic到当前goroutine最上层函数时,最上层步伐直接非常终止
63、go内置函数make和new的区别

make和new时内置函数,不是关键字,变量初始化,一般包括2步,变量声明+变量内存分配,var 关键字就是用来声明变量的,make和new函数主要用来分配内存的。make只能用来分配及初始化范例为slice、map、chan的数据
new可以分配任意范例的数据,并且置零
返回值区别:
make函数原型如下,返回的是slice、map、chan范例自己
这3种范例是引用范例,就没必要返回他们的指针
64、go slice的底层实现原理

切片是基于数组实现的,它的底层是数组,可以理解为对,底层数组的抽象。slice占用24个字节


  • array:指向底层数组的指针,占用8个字节
  • len:切片的长度,占用8个字节
  • cap:切片的容量,cap总是大于即是len的,占用8个字节
65、go array和slice的区别



  • 数组长度差别:数组初始化必须指定长度,并且长度就是固定的。切片的长度是不固定的,可以追加元素,在追加时可能使切片的容量增大。
  • 函数传参差别:数组是值范例,将一个数组赋值给另一个数组时,传递的是一份深拷贝,函数传参操纵都会复制整个数组数据,会占用额外的内存,函数内对数组元素值的修改,不会修改原数组内容。切片是引用范例,将一个切片赋值给另外一个切片时,传递的是一份浅拷贝,哈数传参操纵不会拷贝整个切片,只会复制len和cap,底层共用同一个数组,不会占用额外的内存,函数内对数组元素值的修改,会修改原数组内容。
  • 计算数组长度方式差别:数组需要遍历计算数组长度,时间复杂度O(n),切片包罗len字段,可以通过len()计算切片长度,时间复杂度O(1)
66、slice深拷贝与浅拷贝

深拷贝方式:
1、copy(slice2,slice1)
2、遍历append赋值
浅拷贝
引用范例的变量,默认复制操纵就是浅拷贝
  1. slice2 := slice1
复制代码
67、slice扩容机制

扩容会发生在slice append的时候,slice的cap不足以容纳新元素,就会进行扩容,扩容规则如下:


  • 如果新申请容量比两倍原有容量大,那么扩容后容量大小为新申请容量
  • 如果原有slice长度小于1024,那么每次就扩容为原来的2倍
  • 如果原slice长度大于即是1024,那么每次扩容为原来的1.25倍
68、slice为什么不是线程安全的?

多个线程访问同一个对象时,调用这个对象行为都可以得到精确结果,那么这个对象就是线程安全的。如有多个线程同样执行写操纵,一般都需要考虑线程同步,否则可能影响线程安全。go实现线程安全的几种方式


  • 互斥锁
  • 读写锁
  • 原子操纵
  • sync.once
  • sync.atomic
  • channel
slice底层结构并没有使用加锁等方式,不支持并发读写,所以并不是线程安全,使用多个goroutine对范例为slice的变量进行操纵,每次输出的值大概率都不会一样,与预期值不一致;slice在并发执行中不会报错,但是数据会丢失。
69、map遍历为什么是无序的?



  • map在遍历时,并不是从固定的0号bucket开始遍历,每次遍历,都会从一个随机值序号的bucket,再从此中的随机call开始遍历
  • map遍历时,是按次序遍历bucket,同时按需遍历bucket中和其overflow bucket中的call,但是map在扩容后,会发生key的搬迁这造成原来在一个bucket中key,搬迁后,有可能落到其他bucket中了,从这个角度看,遍历map结果就不可能是按照原来的次序了。
70、map为什么是非线程安全的?

map默认是并发不安全的,同时为map进行并发读写时,步伐会panic,
方法1:使用读写锁map + sync.RWMutex
  1. func main() {
  2.         var lock sync.RWMutex
  3.         s := make(map[int]int)
  4.         for i := 0; i < 100; i++ {
  5.                 go func(i int) {
  6.                         lock.Lock()
  7.                         s[i] = i
  8.                         lock.Unlock()
  9.                 }(i)
  10.         }
  11.         for i := 0; i < 100; i++ {
  12.                 go func(i int) {
  13.                         lock.RLock()
  14.                         fmt.Printf("map第%d个元素值是%d\n", i, s[i])
  15.                         lock.RUnlock()
  16.                 }(i)
  17.         }
  18.         time.Sleep(time.Second * 2)
  19. }
复制代码
方法二:使用Go提供的sync.Map
  1. package main
  2. import (
  3.         "fmt"
  4.         "sync"
  5.         "time"
  6. )
  7. func main() {
  8.         var m sync.Map
  9.         for i := 0; i < 100; i++ {
  10.                 go func(i int) {
  11.                         m.Store(i, i)
  12.                 }(i)
  13.         }
  14.         for i := 0; i < 100; i++ {
  15.                 go func(i int) {
  16.                         v, ok := m.Load(i)
  17.                         fmt.Printf("Load:%v, %v\n", v, ok)
  18.                 }(i)
  19.         }
  20.         time.Sleep(time.Second)
  21. }
复制代码
71、mapp如何查找

go语言中读取map有两种语法:带comma和不带comma,当要查询的key不在map里,带comma的用法会返回一个bool型变量提示key是否在map中;而不带comma的语句则返回一个value范例的零值。如果value是int范例就返回0,如果value是string范例,就会返回空字符串。
  1. //不带comma用法
  2. value := m["name"]
  3. fmt.Printf("value:%s",value)
  4. //带comma用法
  5. value,ok := m["name"]
  6. if ok {
  7.         fmt.Printf("value:%s",value)
  8. }
复制代码


1、写掩护检测
函数首先会查抄map的标记位flags。如果flags的写标记位此时被置1了,说明有其他协程在执行“写”操纵,进而导致步伐panic,这也说明了map不是线程安全的
  1. if h.flags & hasWritting != 0 {
  2.         throw("concurrent map read and map write")
  3. }
复制代码
2、计算hash值
  1. hash := t.hasher(key,uintptr(h.hash0))
复制代码
key颠末哈希函数计算后,得到的哈希值如下(主流64位机下共64个bit位),差别范例的key会有差别的hash函数
  1. 1001011 | 00001111 | 01010
复制代码
3、找到hash对应的bucket
bucket定位:哈希值的低B个bit位,用来定位key所存放的bucket
如果当前正在扩容中,并且定位到的旧bucket数据还未完成迁移,则使用旧的bucket(扩容前的bucket)
4、遍历bucket查找
tophash值定位:哈希值的高8个bit位,用来判断key是否已在当前bucket中(如果不在的话,需要去bucket的overflow中查找)
用步调2中的hash值,得到高8个bit位,也就是10010111,转化为十进制,也就是151
  1. top := tophash(hash)
  2. func tophash(hash uintptr) uint8 {
  3.         top := uint8(hash >> (goarch.PtrSize*8-8))
  4.         if top < minTopHash {
  5.                 top += minTopHash
  6.         }
  7.         return top
  8. }
复制代码
上面函数中hash是64位的,sys.PtrSize是8,所以top := uint8(hash >> (sys.PtrSize*8-8))等效top = uint8(hash >> 56) 最后top取出来的值就是hash高8位值。
在bucket及bucket的overflow中探求tophash值中为151的槽位即为key地点位置,找到了空槽位或者2号槽位,如许整个查找过程就结束了。
5、返回key对应的指针
如果通过上面的步调找到了key对应的槽位下标i,我们再详细分析下key/value值是如何获取的:
  1. dataOffset = unsafe.Offsetof(struct {
  2.         b bmap
  3.         v int64
  4. }{},v)
  5. bucketCnt = 8
  6. k := add(unsafe.Pointer(b),dataOffset*i*uintptr(t.keysize))
  7. v := add(unsafe.Pointer(b),dataOffset+bucketCnt*uintptr(t.keysize)+i*uintptr(t.valuesize))
复制代码
bucket里keys的起始地址就是unsafe.Pointer(b) + dataOffset
第i下标key的地址就要在此底子上跨过i个key的大小
而我们有知道,value的地址是在全部key之后,因此第i个下标value的地址还需要加上全部key的偏移
72、map冲突的解决方式?

链地址法:
当哈希冲突发生时,创建新单元,并将新单元添加到冲突单元地点链表的尾部
开放寻址法
当哈希冲突发生时,从发生冲突的那个单元起,按照一定的次序,从哈希表中探求一个空闲的单元,然后把发生冲突的元素存入到该单元,开放寻址法需要的表长度要大于即是所需要存放的元素数量
开放寻址法有多种方式:线性探测法、平方探测法、随机探测法和双重哈希法,这里以线性探测法
设Hash(key)表示关键字key的哈希值,表示哈希值的槽位数(哈希表的大小)
线性探测法则可以表示为:


  • 如果Hash(x)%M已经有数据,则尝试(Hash(x) + 1) %M
  • 如果Hash(x + 1)%M已经有数据,则尝试(Hash(x) + 2) %M
  • 如果Hash(x + 2)%M已经有数据,则尝试(Hash(x) + 3) %M
两种解决方案比较:
对于链地址法,基于数组+链表进行存储,链表节点可以在需要时再创建,不必像开放寻址法那样事先申请好富足内存,因此链地址法对于内存利用率会比开放寻址法高。链地址法对于装载因子的容忍度会比较高,并且会存储大对象、大数据量的哈希表。而且相较于开放寻址法,它更加灵活,支持更多的优化计谋,比如可采用红黑树代替链表。但是链地址法需要额外的空间来存储指针。
对于开放寻址法,它只有数组一种数据结构就可完成存储,继续了数组的长处,对CPU缓存友好,易于序列化操纵。但是它对内存的利用率不如链地址法,且发生冲突期间价更高,当数据量明白,装载因子小,得当采用开放寻址法
总结:
在发生哈希冲突时,go map采用链地址法解决冲突,具体就是插入key到map中时,当key定位到桶填满8个元素后(这里的单元就是桶,不是元素),将会创建一个溢出桶,并且将溢出桶插入当前桶地点链表尾部。
73、go map的负载因子为什么是6.5

负载因子,用于权衡当前哈希表中空间占用率的焦点指标。也就是每个bucket桶存储的均匀元素个数。
  1. 负载因子 =  哈希表存储的元素个数/桶个数
复制代码
另外负载因子与扩容、迁移等重新散列行为有直接关系:


  • 在步伐运行时,会不绝的进行插入、删除等,会导致bucket不均,内存利用率低,需要迁移
  • 在步伐运行时,出现负载因子过大,需要做扩容,解决bucket过大的问题。
负载因子是哈希表中的一个重要指标,在各种版本的哈希表实现中都有类似的东西,主要目的是为了平衡buckets的存储空间大小和查找元素的性能高低。
在打仗各种哈希表都可以关注一下,做差别的对比,看着各家的考量。
loadFactor%overflowbytes/entryhitprobemissprobe4.002.1320.773.004.004.504.0517.303.254.505.006.8514.773.505.005.5010.5512.943.755.506.0015.2711.674.006.006.5020.9010.794.256.507.0027.1410.154.507.007.5034.039.734.757.508.0041.109.405.008.00

  • loadFactor:负载因子,也有叫装载因子
  • %overflow:溢出率,有溢出率bucket的百分比
  • bytes/entry:均匀每对key/value的开销字节数
  • hitprobbe:查找一个不存在的key时,要查找的均匀个数
  • missprobe:查找一个不存在的key时,要查找的均匀个数
装载因子越大,填入元素越多,空间利用率越高,但发生哈希几率变大,反之,装载因子越小,填入的元素越少,空间浪费也会变得越多,而且还会提高扩容操纵的次数。因此定了个6.5
74、map如何扩容

在向map插入新key的时候,会进行条件检测,符合下面2个条件,会触发扩容
条件1:超过负载
ma元素个数 > 6.5 * 桶个数
条件2: 溢出桶太多
当桶总数 < 2 ^ 15 时,如果溢出桶总数 >= 桶总数,则认为溢出桶过多
当桶总数 >= 2 ^ 15时,直接与2^15比较,当溢出桶总数 >= 2^15时,溢出桶太多了。
对于条件2:实在算是对条件1的增补。由于在负载因子比较小的环境下,有可能map查找和插入服从很低。
外貌来看就是负载因子比较小,map元素总数少,但是桶数量多。比如不绝地增删,如许会造成overflow的bucket数量增多,但是负载因子又不高,达不到的第1点的临界值,就不能触发扩容来缓解这种环境。如许会造成桶的使用率不高,值存储的比较稀疏,查找插入服从会变得非常低,因此有了第2扩容条件。
扩容机制:
双倍扩容:针对条件1,新建一个buckets数组,新的buckets大小是原来的2倍,然后旧buckets数据搬迁到新的buckets,该方法我们称之为双倍扩容
等量扩容:针对条件2,并不扩大容量,buckets数量维持不变,重新做一遍类似双倍扩容的搬迁动作,把松散的键值对重新排列一次,使得同一个bucket中的key排列地更紧密,节省空间,提供bucet利用率,进而包管更快地存取,该方法我们称之为等量扩容
75、map和sync.Map谁的性能好,为什么?

go语言sync.Map支持并发读写,采取了“空间换时间”的机制,
  1. type Map struct {
  2.         mu Mutex
  3.         read atomic.Value
  4.         dirty map[interface{}]*entry
  5.         misses int
  6. }
复制代码
对比原始的map:
和原始map+RWLock的实现并发方式相比,镌汰了加锁对性能的影响,他做了一些优化,可以无锁访问read map,而且会优先操纵read map,倘若只操纵read map就可以满足要求,那就不用去操纵write map(dirty),所以在某些特定场景中它发生锁竞争的频率会远远小于map+RWLock的实现方式。
长处:
得当读多写少的场景
缺点:
写多的场景,会导致read map缓存失败,需要加锁,冲突变多,性能急剧下降。
76、channel的底层实现原理

使用make(chan T,cap)来创建channel,make语法会在编译时,转换为makechan64和makechan
创建会做一些查抄:


  • 元素大小不能超过64k
  • 元素对齐大小不能超过maxAlign也就是8字节
  • 计算出来的内存是否超过限制
创建时


  • 如果是无缓存的channel,会直接给hchan分配内存
  • 如果是有缓存的channel,并且元素不包罗指针,那么会为hchan和底层数组分配一段一连的地址
  • 如果是有缓存的channel,并且元素包罗指针,那么会为hchan和底层数组分别分配地址
阻塞式:
调用chansend函数,并且block=true
  1. ch <- 10
复制代码
非阻塞式:
  1. select {        case ch <- 10
  2. :        ...        default}
复制代码
向channel发送数据时大概分为两大块,查抄和数据发送,数据发送流程如下:


  • 如果channel的读等待队列存在接收者goroutine,将数据直接发送给第一个等待的goroutine,唤醒接收的goroutine
  • 如果channel的等待队列不存在接收者goroutine;如果循环数组buf来满,那么将会把数据发送到循环数组buf的队尾;如果循环数组buf已满,这个时候会走阻塞发送流程,将当前goroutine加入等待队列,并挂起等待唤醒。
接收:
发送操纵,编译时转换为runtime.chanrecv函数
阻塞式:
调用chanrecv函数,并且block=true
  1. <ch
  2. v := ch
  3. v,ok := <ch
  4. for i := range ch {
  5.         fmt.Println(i)
  6. }
复制代码
非阻塞式
调用chanrecv函数,并且block=true
  1. select {
  2.         case <- ch:
  3.         ....
  4.         default
  5. }
复制代码
向channel中接收数据时大概分为两大块,查抄和数据发送,而数据接收流程如下:


  • 如果channel的写等待队列存在发送者goroutine,如果是无缓冲channel,直接从第一个发送者goroutine那里把数据拷贝给接受变量,唤醒发送的goroutine;如果是有缓冲的channel(已满),将循环数组buf的队首元素拷贝给接收变量,将第一个发送者goroutine的数据拷贝到buf循环数组队尾,唤醒发送的goroutine。
  • 如果channel的写等待队列不存在发送者goroutine,如果循环数组buf非空,将循环数组buf的队首元素拷贝给接收变量;如果循环数组buf为空,这个时候就会走阻塞接收的流程,将当前goroutine加入等待队列,并挂起等待唤醒。
关闭:
调用close函数,编译时转换为runtime.closeChan函数
77、channel有什么特点

channel有2种范例:无缓冲,有缓冲
channel有3种模式,写操纵模式、读操纵模式、读写操纵模式
写操纵模式读操纵模式读写操纵模式创建make(chan <- int)make(<-chan int)make(chan int) channel有3种状态:初始化、正常、关闭
未初始化关闭正常关闭panicpanic正常关闭发送永久阻塞导致死锁panic阻塞或者成功发送接收永久阻塞导致死锁缓冲区为空则为零值,否则可以继续读阻塞或者成功接收 注意点:


  • 一个channel不能多次关闭,会导致panic
  • 如果多个goroutine都监听同一个channel,那么channel上的数据都可能随机某一个goroutine取走进行消耗
  • 如果多个goroutine监听同一个channel,如果这个channel被关闭,则全部goroutine都能收到退出信号
78、go channel有无缓冲的区别



  • 无缓冲:一个送信人去你家送信,你不在家他不走,你一定要接下信,他才会走
  • 有缓冲:一个送信人去你家送信,扔到你家的信箱转身就走,除非你的信箱满了,他必须等信箱有多余空间才会走
无缓冲有缓冲创建方式make(chan TYPE)make(chan TYPE,SIZE)发送阻塞数据接收前发送阻塞缓冲满时发送阻塞接收阻塞数据发送前接收阻塞缓冲空时接收阻塞 79、channel为什么设计成线程安全

差别协程通过channel进行通信,自己的使用场景就是多线程,为了包管数据的一致性,必须实现线程安全
如何实现线程安全的
channel的底层实现中,hchan结构体中采用mutex锁来包管数据读写安全,在对循环数据buf中的数据进行入队和出队操纵时,必须先获取互斥锁,才能操纵channel
80、channel如何控制goroutine并发执行次序

使用channel进行通信通知,用channel去传递信息,从而控制并发执行次序
81、go channel发送和接收什么环境下会死锁

死锁:


  • 单个协程永世阻塞
  • 两个或两个以上的协程执行过程中,由于竞争者或由于相互通信而造成的一种阻塞现象
channel死锁场景:


  • 非缓存channel只写不读
  • 非缓存channel读在写后面
  • 缓存channel写入超过缓冲区数量
  • 空读
  • 多个协程相互等待
82、go互斥锁实现原理





  • 在Lock()之前使用Unlock()会导致panic非常
  • 使用Lock()加锁后,再次Lock()会导致死锁(不支持重入),需Unlock()解锁后才能在加锁
  • 锁定状态与goroutine没有关联,一个goroutine可以Lock,另一个goroutine可以Unlock
83、go互斥锁正常模式与饥饿模式

正常模式:
在刚开始的时候,是处于正常模式,也就是,当一个g1持有着一个锁的时候,g2会自旋的去尝试获取这个锁。
当自旋超过4次,还没有能获取到锁的时候,这个g2就会被加入到获取锁的等待队列内里,并阻塞等待唤醒
  1. 正常模式下,所有等待锁的goroutine按照FIFO(先进先出)顺序等待,唤醒的goroutine,不会直接拥有锁,而是和
  2. 新请求锁的goroutine竞争锁,新请求锁的ggoroutine具有优势,它正在CPU上执行,而且可能有好几个,
  3. 所以刚刚唤醒的goroutine有很大可能在竞争中失败,长时间获取不到锁,就会切换到饥饿模式
复制代码
饥饿模式
当一个goroutine等待锁时间超过1毫秒时,他可能会遇到饥饿问题,在版本1.9中,这种场景下go mutex切换到饥饿模式,解决饥饿模式
  1. 饥饿模式下,直接把锁交给等待队列中排在第一位的goroutine(队头),同时饥饿模式下,新进来的goroutine不会参与抢锁也不会进入自旋状态,会直接进入等待队列的尾部,这样很好的解决老的goroutine一直抢不到锁的场景
复制代码
那么也不可能说永久的保持一个饥饿状态,总归会有吃饱的时候,也就是总有那么一刻Mutex会回归到正常模式,那么回归正常模式必须具备的条件有以下几种。



  • G的执行时间小于1ms
  • 等待队列全部清空
当满足上述两个条件任意一个的时候,Mutex会切换到正常模式,而Go的抢锁的过程,就是在这个正常模式和饥饿模式中来回切换进行的。
84、go互斥锁答应自旋的条件

线程没有获取到锁常见有2种处理方式:


  • 一种是没有获取到锁的线程就一直循环等待判断该资源是否已经开释锁,这种锁也叫做自旋锁,他不用将线程阻塞起来,适用于并发低且步伐执行时间短的场景,cpu占用鲛高
  • 另外一种处理方式就是把自己阻塞起来,会开释cpu给其他线程,内核会将线程置为睡眠状态,比及锁被开释后,内核会在合适的时机唤醒该线程,适用于高并发场景,缺点是有线程上下文切换的开销
答应自旋的条件:


  • 锁已被占用,并且锁不处于解饿模式
  • 积累的自旋次数小于最大自旋次数
  • cpu核数大于1
  • 有空闲的p
  • 当前goroutine所挂载的p下,本地待运行队列为空。
85、go 读写锁的实现原理

读写互斥锁RWMutex,是对Mutex的一个扩展,当一个goroutine得到了读锁后,其他的goroutine可以获取读锁,但不能获取写锁;当一个goroutine得到了写锁后,其他的goroutine即不能获取读锁也不能获取写锁
使用场景:
读多于写的环境
底层实现结构
  1. type RWMutex struct {
  2.         w Mutex //复用互斥锁
  3.         writeSem uint32 //信号量,用于写等待读
  4.         readerSem uint32 //信号量。用于读等代写
  5.         readerCount int32 //当前执行读的goroutine数量
  6.         readerwait int32 //被阻塞准备读的goroutine的数量
  7. }
复制代码
注意点:


  • 读锁或写锁在Lock()之前使用Unlock()会导致panic非常
  • 使用Lock()加锁后,再次Lock()会导致死锁,需Unlock()解锁后才能再加锁
  • 锁定状态与goroutine没有关联,一个goroutine可以RLock,另一个goroutine可以RUnlock(Unlock)
互斥锁和读写锁的区别:


  • 读写锁区分读者和写者,而互斥锁不区分
  • 互斥锁同一时间只答应一个线程访问对象,无论读写,读写锁同一时间只答应一个写者,但是答应多个读者同时读对象。
86、可重入锁

  1. package main
  2. import (
  3.         "fmt"
  4.         "sync"
  5.         "time"
  6. )
  7. type ReentrantLock struct {
  8.         mu       sync.Mutex
  9.         owner    *goro // 当前持锁的 goroutine
  10.         holds    int    // 锁的计数器,表示锁被同一 goroutine 拿到的次数
  11. }
  12. type goro struct {
  13.         id int
  14. }
  15. var goroID int
  16. // 获取当前 goroutine 的 ID
  17. func getGoroID() *goro {
  18.         goroID++
  19.         return &goro{id: goroID}
  20. }
  21. func (r *ReentrantLock) Lock() {
  22.         r.mu.Lock() // 先加锁
  23.         defer r.mu.Unlock()
  24.         current := getGoroID()
  25.         // 如果当前 goroutine 已经持有锁,则计数器加 1
  26.         if r.owner == current {
  27.                 r.holds++
  28.                 return
  29.         }
  30.         // 否则设置为当前 goroutine 为持锁者,并初始化计数器
  31.         r.owner = current
  32.         r.holds = 1
  33. }
  34. func (r *ReentrantLock) Unlock() {
  35.         r.mu.Lock() // 先加锁
  36.         defer r.mu.Unlock()
  37.         // 如果当前 goroutine 不是持锁者,不能解锁
  38.         if r.owner == nil || r.owner != getGoroID() {
  39.                 panic("unlock of unlocked lock")
  40.         }
  41.         r.holds--
  42.         // 如果锁计数器为 0,表示可以释放锁
  43.         if r.holds == 0 {
  44.                 r.owner = nil
  45.         }
  46. }
  47. func main() {
  48.         var lock ReentrantLock
  49.         var wg sync.WaitGroup
  50.         // 示例:一个 goroutine 中进行多次加锁和解锁
  51.         wg.Add(1)
  52.         go func() {
  53.                 defer wg.Done()
  54.                 // 第一次加锁
  55.                 lock.Lock()
  56.                 fmt.Println("Goroutine 1 acquired the lock first time.")
  57.                 // 第二次加锁
  58.                 lock.Lock()
  59.                 fmt.Println("Goroutine 1 acquired the lock second time.")
  60.                 // 释放锁
  61.                 lock.Unlock()
  62.                 fmt.Println("Goroutine 1 released the lock first time.")
  63.                 // 第三次加锁
  64.                 lock.Lock()
  65.                 fmt.Println("Goroutine 1 acquired the lock third time.")
  66.                 // 释放锁
  67.                 lock.Unlock()
  68.                 fmt.Println("Goroutine 1 released the lock second time.")
  69.                 // 释放锁
  70.                 lock.Unlock()
  71.                 fmt.Println("Goroutine 1 released the lock third time.")
  72.         }()
  73.         wg.Wait()
  74. }
复制代码
87、原子操纵有哪些



  • 增减Add
  • 载入:Load
  • 比较并交换CompareAndSwap
  • 交换Swap
  • 存储:Store
88、原子操纵和锁的区别



  • 原子操纵由底层硬件支持,而锁是基于原子操纵+信号量完成的,若实现类似的功能,前者通常会更有服从
  • 原子操纵是单个指令的互斥操纵:互斥锁/读写锁是一种数据结构,可以完成临界区(多个指令)互斥操纵,扩大原子操纵的范围
  • 原子操纵是无锁操纵,属于乐观锁;提及锁的时候,一般属于灰心锁
  • 原子操纵存在于各个指令/语言层级,比如"机器指令层级的原子操纵"“go语言层级的原子操纵”等
  • 锁也存在于各个指令/语言层级中,比如机器指令层级的锁”,“汇编指令层级的锁”,“go语言层级的锁”
89、goroutine底层实现原理

  1. type g struct {
  2.         gold int64
  3.         sched gobuf
  4.         stack stack
  5.         gopc
  6.         startpc
  7. }
  8. type gobuf struct {
  9.         sp uintptr
  10.         pc uintptr
  11.         g uintptr
  12.         ret uintptr
  13.        
  14. }
  15. type stack struct {
  16.         lo uintptr //栈的下界内存地址
  17.         hi uintptr // 栈的上街内存地址
  18. }
复制代码
状态流转
状态含义空闲中_GidleG刚刚新建,仍未初始化待运行_Grunnable就绪状态,G在运行队列中,等待M取出并运行运行中_GrunningM正在运行这个G,这时候M会拥有一个P系统调用中_GsyscallM正在运行这个G发起的系统调用,这时候M并不拥有P等待中_GwaitingG在等待某些条件完成,这时候G不在运行也不再运行队列中(可能在channel等待队列中)已中止_GdeadG未被使用,可能已执行完毕栈复制中_GcopystackG正在获取一个新的栈空间并把原来的内容复制过去(用来防止GC扫描)

创建
通过go关键字调用底层函数runtime.newproc() 创建一个goroutine,当调用该函数之后,goroutine会被设置成runnable状态
  1. func main() {
  2.         go func() {
  3.                 fmt.Println("func routine")
  4.         }()
  5.         fmt.Println("main goroutine")
  6. }
复制代码
创建好的这个goroutine会新建一个自己的栈空间,同时在G的sched中维护栈地址与步伐计数器这些信息。
每个G在被创建之后,都会被优先放入到本地队列中,如果本地队列已经满了,就会被放入到全局队列中。
运行
goroutine自己只是一个数据结构,真正让goroutine运行起来的是调度器,Go实现了一个用户态的调度器,这个调度器充分利用现代计算机的多核特性,同时让多个goroutine运行,同时goroutine设计的轻量级别,调度和上下文切换的代价都比较小
调度时机:


  • 新起一个协程和协程执行完毕
  • 会阻塞的协同调用,比如文件io,网络io
  • channel、mutex等阻塞操纵
  • time.sleep
  • 垃圾回收之后
  • 主动调用runtime
  • 运行过久或系统调用过久等等

每个M开始执行p的本地队列中的G时,goroutine会被设置成running状态
如果某个M把本地队列中的G都执行完成之后,然后就会去全局队列中拿G,这里需要注意,每次去全局队列拿G的时候,都需要上锁,避免同样的任务被多次拿。
如果全局队列都被拿完了,而当前M也没有更多的G可以执行的时候,它就会去其他P本地队列中拿任务,这个机制被称之为work stealing机制,每次会拿走一半的任务,向下取整,比如另一个p中有3个任务,那一半就是一个任务
当全局队列为空,M也没办法从其他的P中拿任务的时候,就会让自身进入自选状态,等待有新的G进来,最多只会有GOMAXPROCS个M在自旋状态,过多M的自旋会浪费CPU资源
阻塞
channel的读写操纵、等待锁、等待网络数据、系统调用等都有可能发生阻塞,会调用底层函数runtime.gopark() ,会让出CPU时间片,让调度器安排其他等待的任务运行,并在下次某个时候从该位置恢复执行
当调用该函数之后,goroutine会被设置成waiting状态
唤醒
处于waiting状态的goroutine,在调用runtime.goready()函数之后会被唤醒,唤醒的goroutine会被重新放到M对应的上下文P对应的runqueue中,等待被调度
当调用该函数之后,goroutine会被设置成runnable状态
退出
当goroutine执行完成后,会调用底层函数runtime.Goexit(),当调用该函数之后,goroutine会被设置成dead状态
90、goroutine和线程的区别

goroutine线程内存占用创建一个goroutine的栈内存消耗为2KB,时机运行过程中,如果栈空间不够用,会主动进行扩容创建一个线程的栈内存消耗为1MB创建和烧毁goroutine由于是由goroutine 负责管理的,创建和烧毁的消耗非常小,是用户级线程 创建和烧毁都会有巨大的消耗,由于要和操纵系统打交道,是内核级的,通常解决的办法就是线程池切换goroutines切换只需生存三个寄存器:PC、SP、BP goroutine的切换约为200ns,相称于2400-3600条指令当线程切换时,需要生存各种寄存器,以便恢复现场,线程切换会消耗1000-1500ns,相称于12000-18000条指令 91、goroutine泄露的场景

泄露缘故原由


  • goroutine内进行channel/mutex等读写操纵被一直阻塞
  • goroutine内的业务逻辑进入死循环,资源一直无法开释
  • goroutine内的业务逻辑进入长时间等待,有不绝新增的goroutine进入等待
泄露场景


  • nil channel
  • 发送不接收
  • 接收不发送
  • http request body未关闭
  • 互斥锁忘记解锁
  • sync.WaitGroup不当:add与Done不匹配

92、如何查看正在执行的goroutine数量

  1. go tool pprof -http=:1248 http://127.0.0.1:6060/debug/pprof/goroutine
复制代码
93、go线程实现模型

go实现的是两级线程模型(M:N),准确的说是GMP模型,是对两级线程模型的改进实现,使它能够更加灵活地进行线程之间的调度
含义缺点单进程期间每个步伐就是一个进程,直到一个步伐运行完,才能进行下一个进程无法并行,只能串行;进程阻塞所带来的CPU时间浪费多进程/线程期间一个线程阻塞,cpu可以立即切换到其他线程中去执行进程/线程占用内存高2.进程/线程上下文切换成本高协程期间协程(用户态线程)绑定线程(内核态线程)cpu调度线程执行实现起来较复杂,协程和线程的绑定依赖调度器算法 三种线程模型:


  • 内核级线程模型 (1:1)
  • 用户级线程模型(N:1)
  • 两级线程模型
M:N
长处:


  • 能够利用多核
  • 上下文切换成本低
  • 如果进程中的一个线程被阻塞,不会阻塞其他线程,是能够切换同一个进程内的其他线程继续执行
缺点:


  • 实现起来比较复杂
94、GMP和GM模型

GMP结构


  • G(Goroutine):代表Go协程Goroutine,存储了 Goroutine的执行栈信息、Goroutine 状态以及 Goroutine 的任务函数等。G的数量无限制,理论上只受内存的影响,创建一个G的初始栈大小为2-4K,配置一般的机器也能简简单单开启数十万个Goroutine,而且Go语言在G退出的时候还会把G清理之后放到P本地或者全局的闲置列表 gFree中以便复用。
  • M(Machine): Go对操纵系统线程(OSthread)的封装,可以看作操纵系统内核线程,想要在 CPU 上执行代码必须有线程,通过系统调用 cone 创建。M在绑定有用的P后,进入一个调度循环,而调度循环的机制大抵是从P的本地运行队列以及全局队列中获取G,切换到G的执行栈上并执行G的函数,调用goexit 做清理工作并回到M,如此反复。M并不保把稳状态,这是G可以跨M 调度的底子。M的数量有限制,默认数量限制是10000,可 以通过 debug.SetMaxThreads()方法进行设置,如果有M空闲,那么就会回收或者睡眠。
  • *P(Processor):虚拟处理器,M执行G所需要的资源和上下文,只有将P和M绑定,才能让P的runq 中的G 真正运行起来。P的数量决定了系统内最大可并行的G的数量,**P的数量受本机的CPU核数影响,可通过环境变 量$GOMAXPROCS或在runtime.GOMAXPROCS()来设置,默认为CPU焦点数。
  • Sched:调度结构,他维护有存储M和G的全局队列,以及调度器的一些状态信息
GMP数量限制无限制、受机器内存影响有限制,默认最多1w有限制,最多GOMAXPROCS个创建时机go func当没有富足的M来关联p并运行此中的可运行的G时会哀求创建新的M在确定了P的最大数量n后,运行时系统会根据这个数量创建个p GM调度存在的问题:


  • 全局队列的锁竞争,当M从全局队列中添加或者获取G的时候,都需要获取队列锁,导致激烈的锁竞争
  • M转移G增加额外的开销,当M1在执行G1的时候,M1创建了G2,为了继续执行 G1,需要把G2生存到全局队列中,无法包管G2是被M1处理。由于M1本来就生存了G2的信息,所以G2最好是在M1上执行,如许的话也不需要转移G到全局队列和线程上下文切换
  • 线程使用服从不能最大化:没有work-stealing和hand-off机制
95、Go调度原理

goroutine调度的本质就是将Goroutine按照一定算法放到CPU上去执行。
CPU感知不到Goroutine,只知道内核线程。所以需要Go调度器将协程调度到内核线程上面去,然后操纵系统调度器将内核线程放到CPU上去执行。
M是对内核级线程的封装,所以Go调度器的工作就是将G分配到M
Go调度器的实现不是一挥而就的,它的调度器模型与算法也是几经演化,从最初的GM模型,到GMP模型,从不支持抢占,到支持协作抢占,再到支持基于信号的异步抢占,经历了不绝地优化与打磨。
设计思想


  • 线程复用(work stealing机制和hand off机制)
  • 利用并行 (利用多核CPU)
  • 抢占调度 (解决公平性问题)
调度对象
Go调度器
   Go调度器是属于Go runtime中的一部门,Go runtime负责实现Go的并发调度、垃圾回收、内存堆栈管理等关键功能
  被调度对象
G的来源


  • P的runnext(只有一个G,局部性原理,永久会被最先调度执行)
  • P的本地队列(数组,最多256个G)
  • 全局G队列(l链表、无限制)
  • 网络轮询器(存放网络调用被阻塞的G)
P的来源


  • 全局P队列(数组,GOMAXPROCS个P)
M的来源


  • 休眠线程队列(未绑定P,长时间休眠会等待GC回收烧毁)
  • 运行线程(绑定P,指向P中的G)
  • 自旋线程(绑定P,指向M的G0)
此中运行线程数+自旋线程数 <= P的数量(GOMAXPROCS),M个数>=P个数
G的生命周期:G从创建、生存、被获取、调度和执行、阻塞、烧毁、步调如下:


  • 创建G,关键字go func() 创建G
  • 生存G,创建G优先生存到本地队列P,如果P满了,则会平衡部门P到全部队列中,
  • 唤醒或者新建M执行任务,进入调度循环
  • M获取G,M首先从P的本地队列获取G,如果P为空,则从全局队列获取G,如果全局队列也为空,则从另一个本地队列偷取一半数量的G(负载均衡),也称之为work stealing

  • M执行完G后清理现场,重新进入调度循环(将M上运行的goroutine切换为G0,G0负责调度时协程的切换)
    此中步调2中生存G的详细流程如下:
  • 执行go fun
此中步调2中生存G的详细流程如下:


  • 执行go func的时候,主线程M0会调用newproc()天生一个G结构体,这里会选定当前M0上的P结构
  • 每个协程G都会被尝试先放到P中的runnext,,若runnext为空则放到runnext中,生产结束
  • 若runnext满,则将原来runnext中的G踢到本地队列总,但将当前G放到runnext中,生产结束。
  • 若本地队列也满了,则将本地队列中的G拿出一半,放到全局队列中,生产结束。
在以下情形下:会切换正在执行的goroutine


  • 抢占式调度:sysmon检测到协程运行过久(sleep,死循环)
    - 切换到go,进入调度循环
  • 主动调度:新起一个协程和协程执行完毕(触发调度循环);主动调用runtime.Gosched()(切换到g0,进入调度循环);垃圾回收之后(stw之后,会重新选择g开始执行)
  • 被动调度:系统调用(比如文件io)阻塞(同步)【阻塞G和M,P与M分离,将P交给其他M绑定,其他M执行P的剩余G】;网络IO调用阻塞(异步);【阻塞G,G移动到NetPoller,M执行P的剩余G】;atomic/mutex/channel等阻塞(异步)【阻塞G,G移动到channel的等待队列中,M执行P的剩余G】
调度计谋
使用什么计谋来挑选下一个goroutine执行?
由于P中的G分布在runnext、本地队列、全局队列、网络轮询器中,则需要挨个判断是否有可执行的G,大体逻辑如下:


  • 要执行61次调度循环,从全局队列获取G,如有则直接返回
  • 从P上的runnext看一个是否有G,如有则直接返回
  • 从P上的本地队列看一个是否有G,如有直接返回。
  • 上面都没查找到时,则去全局队列、网络轮询器查找或者从其他P中窃取,一直阻塞直到获取到一个可用的G为止。
96、work stealing机制

当线程M无可运行的G时,尝试从其他M绑定的P偷取G,镌汰空转,提供了线程利用率(避免闲着不干活).
当从本线程绑定P本地队列、全局G队列、netpoller都找不到可执行的g,会从别的P里窃取G并放到当前P上面。
从netpoller中拿到G是Gwaiting状态,存放的是由于网络IO被阻塞的G,其他地方拿到G是Grunnable状态。
从全局队列的G数量:N=min(len(GRQ)/GOMAXPROCS+1,len(GRQ/2))从其他P本地队列窃取的G数量:N=len(LRQ)/2
窃取流程:
源码见runtime/proc.go stealWork函数,窃取流程如下,如果颠末多次积极一直找不到需要运行的goroutine则调用stopm进入睡眠状态,等待被其他工作线程唤醒。


  • 选择要窃取的P
  • 从P中偷走一半G
选择要窃取的P
窃取的本质就是遍历allp中的全部p,查看其运行队列是否有goroutine,如果有,则取其一半到当前工作线程的运行队列。
为了包管公平性,遍历allp时并不是固定的从allp[0]即第一个p开始,而是从随机位置上的p开始,而且遍历的次序也随机化了,并不是在访问第i个p下一次就访问第i+1个p,而是使用了一种伪随机的方式遍历allp中的每个p防止每次遍历时使用同样的次序访问allp中的元素.
从P中偷走一半G
挑选出盗取的对象p之后,则调用runqsteal盗取p的运行队列中的goroutine,runqsteal函数再调用runqgrap从p的本地队列尾部批量偷走一半的g。
97、Go hand off机制

也称为P分离机制,当本线程M由于G进行的系统调用阻塞时,线程开释绑定的P,把P转移给其他空闲的M执行,也提高了线程利用率
分离流程
当前线程M阻塞时,开释P,给其他空闲的M处理.
98、Go 抢占式调度

协作式:大家都按事先定义好的规则来。比如:一个goroutine执行完后,退出,让出p,然后下一个goroutine被调度到p上运行。如许做的缺点就在于是否让出p的决定权在goroutine自身。一旦某个g不主动让出p或执行时间较长,那么后面的goroutine只能等着,没有方法让前者让出p,导致延迟甚至饿死。


  • 编译器会在调用函数前插入runtime.morestack,让运行时有机会在这段代码中查抄是否需要执行抢占调度
  • Go语言运行时会在垃圾回收暂停步伐、系统监控发现Goroutine运行超过10ms,那么会在这个协程设置一个抢占标记
  • 当发生函数调用时,可能会执行编译器插入的runtime.morestack,它调用的runtime.newstack会查抄抢占标记,如果有抢占标记就会触发抢占让出cpu,切到调度主协程里
基于信号抢占调度
真正的抢占式调度是基于信号完成的,所以也称为异步独占,不管协程有没有意愿主动让出cpu运行权,只要某个协程执行时间过长,就会发送信号强行夺取cpu运行权


  • M注册一个SIGURG信号的处理函数:sighandler
  • sysmon启动后会间隔性的进行监控,最长间隔10ms,最短间隔20us,如果发现某协程独占P超过10ms,会给M发送抢占信号。
  • M收到信号后,内核执行sighandler函数把当前协程的状态从——Grunning正在执行改成_Grunnable可执行,把抢占的协程放到全局队列里,M继续探求其他goroutine来运行
  • 被抢占的G再次调度过来执行,会继续原来的执行流。
99、go内存逃逸

go编译器判断一个变量的生命周期是否能够超出其地点的函数或栈,如果变量的生命周期超出了栈的范围,它会被分配到堆上,这种现象被称为“内存逃逸”


  • 函数返回值逃逸
  • 闭包引用外部变量
  • 使用切片时逃逸
  • 大对象逃逸
100、gc实现原理

垃圾回收过程中对象的三种状态


  • 灰色:对象还在标记队列中等待
  • 玄色:对象已被标记,gcmarkBits对应位为1
  • 白色:对象未被标记,gcmarkBits对应位为0

  • 创建白、灰、黑三个集合
  • 将全部对象放入白色集合中
  • 遍历全部root对象,把遍历到的对象从白色集合放入灰色集合
  • 遍历灰色集合,将灰色对象引用的对象从白色集合放入灰色集合,自身标记为玄色
  • 重复步调4,直到灰色中无任何对象:写屏障,辅助GC
  • 网络全部白色对象
101、gc流程

一次完整的垃圾回收分为四个阶段,分别是标记准备、标记开始、标记终止、清理


  • 标记准备:打开写屏障
  • 标记开始:使用三色标记法并发表及,与用户步伐并发执行
  • 标记终止:对触发写屏障的对象进行重新扫描标记,关闭写屏障
  • 清理:将需要回收的内存归还到堆中,将过多的内存归还给操纵系统。
102、gc触发时机

主动触发:
调用runtime.GC()方法,触发GC
被动触发:


  • 定时触发,该触发条件由runtime.forcegcperiod变量控制,默认为2分钟,当超过两分钟没有产生任何GC时,触发GC
  • 根据内存分配阈值触发,该触发条件由环境变量GCGC控制,默认值为100,当前堆内存占用是上次GC结束后占用内存的2倍时,触发GC
103、gc如何调优



  • 控制内存分配速度,限制goroutine数量,提高赋值器mutator的cpu利用率(降低gc的cpu利用率)
  • 少量使用+毗连string
  • slice提前分配富足的内存来降低扩容带来的拷贝
  • 避免map key对象过多,导致扫描时间增加
  • 变量复用,镌汰分配,如何使用sync.Pool来复用需要频仍创建临时对象,使用全局变量等
  • 增大GOGC的值,降低GC运行频率
104、gc常用的并发模型?



  • 共享内存:抽象层级低,耦合高,竞争会导致数据冲突
  • 发送消息:抽象层级高,耦合低,线程竞争
channel
105、Cond实现原理

go尺度库提供了Cond原语,可以让Goroutine在满足特定条件时被阻塞和唤醒
  1. type Cond struct {
  2.         noCopy noCopy
  3.         L Locker
  4.         notify notifyList
  5.         checker copyChecker
  6. }
  7. type notifyList struct {
  8.         wait uint32
  9.         notify uint32
  10.         lock uintptr
  11.         head unsafe.Pointer
  12.         tail unsafe.Pointer
  13. }
复制代码


  • nocopy:go源码中检测克制拷贝的技能,如果步伐中有waitgroup的赋值行为,使用go vet查抄步伐时,就会发现有报错,但需要注意的是,noCopy不会影响步伐精确的编译和运行
  • checker:用于克制运行期间发生拷贝、双重拷贝
  • L:可以传入一个读写锁或互斥锁,当修改条件或者调用wait方法时需要加锁
  • notify:通知链表,调用Wait()方法的Goroutine会放到这个链表中,从这里获取需被唤醒的Goroutine列表
使用方法


  • sync.NewCond(l Locker):新建一个sync.Cond变量,注意该函数需要一个Locker作为必填参数,这是由于在cond.Wait()中底层会涉及到Locker的锁操纵。
  • Cond.Wait():阻塞等待被唤醒,调用Wait函数前需要先加锁;并且由于Wait函数被唤醒时存在虚假唤醒等环境,导致唤醒后发现,条件依旧不成立,因此需要使用for语句来循环进行等待,直到条件成立为止
  • Cond.Signal() :只唤醒一个最先Wait的goroutine,可以不用加锁
  • Cond.Broadcast():唤醒全部wait的goroutine,可以不用加锁
106、go哪些方式安全读写共享变量

方式并发原语备注不要修改变量sync.Once不要去写变量,变量只初始化一次只答应一个goroutine访问变量Channel不要通过共享变量莱通信,而是通信来共享1变量答应多个goroutine访问变量,但是同一个时间只答应一个goroutine访问sync.Mutex,sync.RWMutex原子操纵实现锁机制,同时只有一个线程能拿到锁 107、如何盘问数据竞争

go命令行有个参数rance帮助检测代码中的数据竞争
  1. go run -race main.go
复制代码
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

x
回复

使用道具 举报

0 个回复

倒序浏览

快速回复

您需要登录后才可以回帖 登录 or 立即注册

本版积分规则

宁睿

论坛元老
这个人很懒什么都没写!
快速回复 返回顶部 返回列表