当前位置: 首页 > news >正文

Go常见问题与答案笔记

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

文章目录

    • 1.悲观锁VS乐观锁的区别
    • 2.for range中赋值的变量,这个变量指向的是真实地址吗?还是临时变量?
    • 3.我能不能在写入channel的时候,判断是否阻不阻塞?
    • 4、如果我在defer里面修改return里面的值呢?这时怎么写?
    • 5、结构体中的tag有什么作用
      • JSON序列化与反序列化
      • 数据库操作
      • 字段验证
      • 反射读取结构体Tag
    • 6、waitgroup的底层原理是什么
    • 7、go语言有哪些同步原语
    • 8、如果chan在有缓冲的情况下缓冲区满了不想要后续的数据了怎么做
    • 9、内存泄漏有哪些场景
    • 10、切片的复制过程
    • 11、go里面如何解决hash冲突的
    • 12、切片与数组的区别
    • 13、go并发编程如何避免死锁
    • 14、defer关键字
    • 15、Map并发安全吗?如果是sync.map,它是如何保证并发安全?
    • 16、goroutine使用场景
    • 17、goroutine怎么做同步机制
    • 18、atomic介绍一下,它有哪些方法
    • 19、sync.Map数据写入流程
    • 20、sync.Map数据读取流程
    • 21、sync.Map中Dirty与Read转化过程
    • 22、Sync.map使用场景以及sync.map与map的区别
    • 23、go协程可能引发那些问题?
    • 24、go实现一个简单的多态
    • 25、go实现一个简单的cache
    • 26、golang select 两个channel性能稳定,三个channel性能会发生抖动,为什么
    • 27、生产者、消费者用有缓存channel通信场景,如何让生产者和消费者退出
    • 28、如何获取goroutine里面的一个函数执行的返回值
    • 29、反射原理以及反射应用场景
    • 30、 go哪些数据类型是线程安全的
    • 31、map可寻址吗?
    • 32、map扩容两种方式
    • 33、自旋锁的本质是什么?
    • 34、setnx和set nx区别
    • 35、string和byte的区别
    • 36、recover怎么使用的,defer相比普通在函数最后执行操作,其优势是什么?
    • 37、如何控制goroutine的生命周期,channel的作用,context的作用
    • 38、map,slice未初始化,操作会怎么样,发生panic应该怎么办?
    • 39、cookie与session的区别与应用
    • 40、context常见应用场景
    • 41、开辟多个写协程向channel写数据,有序的吗?
    • 42、 channel缓冲情况下接收与发送数据的流程
    • 43、关闭的channel接收与发送数据会出现什么情况
    • 44、channel底层结构分析
    • 45、面向对象的三大核心概念与五大核心准则
    • 46、进程、线程、协程、go协程的区别
    • 47、select核心机制与使用场景分析
    • 48、go语言实现一个线程池
    • 49、new与make的区别
    • 50、go当中同步锁有什么特点?作用是什么?
    • 51、如果在匿名函数内panic了,在匿名函数外的defer是否会触发panic-recover?反之在匿名函数外触发panic,是否会触发匿名函数内的panic-recover?
    • 52、读写锁的基本原理
    • 53、互斥锁基本原理
    • 54、REST API 详细规范
    • 55、go并发原语
      • goroutine
      • channel
      • select
      • sync包
    • 56、go使用什么类型?
    • 57、语言结构
    • 58、go数据类型
    • 59、函数与方法的区别
    • 60、go函数返回局部变量的指针是否安全?
    • 61、go函数参数传递到底是值传递还是引用传递
    • 62、defer关键字实现原理
    • 63、go内置函数make和new的区别
    • 64、go slice的底层实现原理
    • 65、go array和slice的区别
    • 66、slice深拷贝与浅拷贝
    • 67、slice扩容机制
    • 68、slice为什么不是线程安全的?
    • 69、map遍历为什么是无序的?
    • 70、map为什么是非线程安全的?
    • 71、mapp如何查找
    • 72、map冲突的解决方式?
    • 73、go map的负载因子为什么是6.5
    • 74、map如何扩容
    • 75、map和sync.Map谁的性能好,为什么?
    • 76、channel的底层实现原理
    • 77、channel有什么特点
    • 78、go channel有无缓冲的区别
    • 79、channel为什么设计成线程安全
    • 80、channel如何控制goroutine并发执行顺序
    • 81、go channel发送和接收什么情况下会死锁
    • 82、go互斥锁实现原理
    • 83、go互斥锁正常模式与饥饿模式
    • 84、go互斥锁允许自旋的条件
    • 85、go 读写锁的实现原理
    • 86、可重入锁
    • 87、原子操作有哪些
    • 88、原子操作和锁的区别
    • 89、goroutine底层实现原理
    • 90、goroutine和线程的区别
    • 91、goroutine泄露的场景
    • 92、如何查看正在执行的goroutine数量
    • 93、go线程实现模型
    • 94、GMP和GM模型
    • 95、Go调度原理
    • 96、work stealing机制
    • 97、Go hand off机制
    • 98、Go 抢占式调度
    • 99、go内存逃逸
    • 100、gc实现原理
    • 101、gc流程
    • 102、gc触发时机
    • 103、gc如何调优
    • 104、gc常用的并发模型?
    • 105、Cond实现原理
    • 106、go哪些方式安全读写共享变量
    • 107、如何盘查数据竞争

1.悲观锁VS乐观锁的区别

特性悲观锁乐观锁
加锁时机访问数据前加锁访问数据时不加锁,更新时检查
适用场景写场景较多的场景读操作较多的场景
性能开销加锁和解锁的开销较大无加锁开销,但可能需要重试
实现复杂度较简单较复杂(需要处理冲突和重试)
典型实现sync.Mutex、数据库行级锁atomic、数据库版本号机制
type Service struct {mutex   sync.Mutexversion int
}func (s *Service) Deploy() {s.mutex.Lock()defer s.mutex.Unlock()s.version++
}type Service1 struct {version atomic.Int32
}func (s *Service1) Deploy() {for {v := s.version.Load()n := v + 1if s.version.CompareAndSwap(v, n) {fmt.Println("更新成功")break}}
}

2.for range中赋值的变量,这个变量指向的是真实地址吗?还是临时变量?

如果在for range 里面有一个函数,这个函数需要传入一个指针,这个时候应该怎么写?

func main() {s := []int{10, 11, 12, 13, 14, 15, 16}for i, v := range s {fmt.Printf("%p,%p,%d,%d\n", &i, &v, i, v)f1(&v)}fmt.Println(s)mp := map[string]string{"a": "v1", "b": "v2", "c": "v3"}for k, v := range mp {fmt.Printf("%p,%p,%s,%s\n", &k, &v, k, v)f2(&v)}fmt.Println(mp)
}func f1(i *int) {*i = 12
}
func f2(i *string) {*i = "aa"
}

3.我能不能在写入channel的时候,判断是否阻不阻塞?

(1)多路复用select,且有default情况下可以避免阻塞
(2)使用len与cap函数,可以检查channel状态

4、如果我在defer里面修改return里面的值呢?这时怎么写?

defer的执行顺序
defer执行会在我们函数return之后,而在外层函数去接受return值之前

func add(a,b int)(sum int){sum = a+bdefer func() {sum = a + b}return 
}

5、结构体中的tag有什么作用

JSON序列化与反序列化

type Person struct {Name string `json:"name"`Age int `json:"age"`
}

通过结构体tag来指定字段的JSON键名

数据库操作

结构体tag用于指定数据库表和字段名

type User struct {ID int `gorm:"primaryKey"`Name string `gorm:"column:username"`
}

字段验证

validator库允许你使用tag来验证结构体字段的值是否合法

type Product struct {Name string `validate:"required"`Price float64 `validate:"gt=0"`
}

反射读取结构体Tag

可以读取和解析结构体的tag

import ("fmt""reflect"
)type Person struct {Name string `json:"name"`Age  int    `json:"age"`
}func main() {p := Person{Name: "John", Age: 30}t := reflect.TypeOf(p)for i := 0; i < t.NumField(); i++ {field := t.Field(i)fmt.Println("Field:", field.Name, "Tag:", field.Tag)}
}

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在有缓冲的情况下缓冲区满了不想要后续的数据了怎么做

丢弃数据或者停止写入


func main() {ch := make(chan int, 10)done := make(chan struct{})//接受方go func() {for {select {case <-done:returndefault:select {case i := <-ch:fmt.Println(i)}}}}()go func() {i := 0for {select {case ch <- i:i++fmt.Println("case")default:close(done)close(ch)fmt.Println("default")return}}}()time.Sleep(1 * time.Second)
}

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实现一个简单的多态

package mainimport ("fmt""math"
)type Shape interface {Area() float64
}type Rectangle struct {Width  float64Height float64
}func (r Rectangle) Area() float64 {return r.Height * r.Width
}type Circle struct {Radius float64
}func (c Circle) Area() float64 {return math.Pi * c.Radius * c.Radius
}func main() {shapes := []Shape{Rectangle{10, 20},Circle{10},}for _, shape := range shapes {fmt.Println(shape.Area())}
}

25、go实现一个简单的cache

type Cache struct {data map[string]interface{}mutex sync.RWMutex
}
func NewCache() *Cache {return &Cache{data : make(map[string]interface{})}
}
func (c *Cache)Set(key string,value interface{}){c.mutex.Lock()defer c.mutex.Unlock()c.data[key] = value
}
func (c *Cache)Get(key string)(interface{},bool){c.mutex.RLock()defer c.mutex.RUnlock()value,exists := c.data[key]return value,exists
}
func (c *Cache)Delete(key string){c.mutex.Lock()defer c.mutex.Unlock()delete(c.data,key)
}

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来通知退出
  • 通过控制信号来通知
  • 超时机制
package mainimport ("fmt""time"
)func producer(ch chan<- int, done chan<- bool) {for i := 1; i <= 5; i++ {ch <- ifmt.Println("producer ", i)time.Sleep(time.Second)}close(ch)done <- true
}func consumer(ch <-chan int, done <-chan bool) {for {select {case item, ok := <-ch:if !ok {fmt.Println("channel closed")return}fmt.Println("consumer ", item)case <-done:fmt.Println("consumer done")return}}
}func main() {ch := make(chan int, 3)done := make(chan bool)go producer(ch, done)go consumer(ch, done)<-done
}

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语言实现一个线程池

//肯定要冲键值offer,既然巅峰留不住,那就重走来时路package mainimport ("fmt""sync""time"
)type Task func()type GoroutinePool struct {workerCount inttaskQueue   chan Taskwg          sync.WaitGroup
}func NowGoroutinePool(workerCount, queueSize int) *GoroutinePool {return &GoroutinePool{workerCount: workerCount,taskQueue:   make(chan Task, queueSize),}
}func (gp *GoroutinePool) Start() {for i := 0; i < gp.workerCount; i++ {go gp.worker(i)}
}func (gp *GoroutinePool) worker(workerId int) {for task := range gp.taskQueue {task()gp.wg.Done()}
}func (gp *GoroutinePool) Submit(task Task) {gp.wg.Add(1)gp.taskQueue <- task
}func (gp *GoroutinePool) Wait() {gp.wg.Wait()
}func createTask(taskID int) Task {return func() {fmt.Printf("Task %d is starting...\n", taskID)time.Sleep(1 * time.Second)fmt.Printf("task %d is completed.\n", taskID)}
}func main() {pool := NowGoroutinePool(3, 5)pool.Start()for i := 0; i < 10; i++ {task := createTask(i)pool.Submit(task)}pool.Wait()fmt.Println("All tasks finished.")
}

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赋值
浅拷贝
引用类型的变量,默认复制操作就是浅拷贝

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

func main() {var lock sync.RWMutexs := make(map[int]int)for i := 0; i < 100; i++ {go func(i int) {lock.Lock()s[i] = ilock.Unlock()}(i)}for i := 0; i < 100; i++ {go func(i int) {lock.RLock()fmt.Printf("map第%d个元素值是%d\n", i, s[i])lock.RUnlock()}(i)}time.Sleep(time.Second * 2)}

方法二:使用Go提供的sync.Map

package mainimport ("fmt""sync""time"
)func main() {var m sync.Mapfor i := 0; i < 100; i++ {go func(i int) {m.Store(i, i)}(i)}for i := 0; i < 100; i++ {go func(i int) {v, ok := m.Load(i)fmt.Printf("Load:%v, %v\n", v, ok)}(i)}time.Sleep(time.Second)
}

71、mapp如何查找

go语言中读取map有两种语法:带comma和不带comma,当要查询的key不在map里,带comma的用法会返回一个bool型变量提示key是否在map中;而不带comma的语句则返回一个value类型的零值。如果value是int类型就返回0,如果value是string类型,就会返回空字符串。

//不带comma用法
value := m["name"]
fmt.Printf("value:%s",value)
//带comma用法
value,ok := m["name"]
if ok {fmt.Printf("value:%s",value)
}

在这里插入图片描述
在这里插入图片描述

1、写保护检测
函数首先会检查map的标志位flags。如果flags的写标志位此时被置1了,说明有其他协程在执行“写”操作,进而导致程序panic,这也说明了map不是线程安全的

if h.flags & hasWritting != 0 {throw("concurrent map read and map write")
}

2、计算hash值

hash := t.hasher(key,uintptr(h.hash0))

key经过哈希函数计算后,得到的哈希值如下(主流64位机下共64个bit位),不同类型的key会有不同的hash函数

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

top := tophash(hash)
func tophash(hash uintptr) uint8 {top := uint8(hash >> (goarch.PtrSize*8-8))if top < minTopHash {top += minTopHash}return top
}

上面函数中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值是如何获取的:

dataOffset = unsafe.Offsetof(struct {b bmapv int64
}{},v)
bucketCnt = 8
k := add(unsafe.Pointer(b),dataOffset*i*uintptr(t.keysize))
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桶存储的平均元素个数。

负载因子 =  哈希表存储的元素个数/桶个数

另外负载因子与扩容、迁移等重新散列行为有直接关系:

  • 在程序运行时,会不断的进行插入、删除等,会导致bucket不均,内存利用率低,需要迁移
  • 在程序运行时,出现负载因子过大,需要做扩容,解决bucket过大的问题。

负载因子是哈希表中的一个重要指标,在各种版本的哈希表实现中都有类似的东西,主要目的是为了平衡buckets的存储空间大小和查找元素的性能高低。
在接触各种哈希表都可以关注一下,做不同的对比,看着各家的考量。

loadFactor%overflowbytes/entryhitprobemissprobe
4.002.1320.773.004.00
4.504.0517.303.254.50
5.006.8514.773.505.00
5.5010.5512.943.755.50
6.0015.2711.674.006.00
6.5020.9010.794.256.50
7.0027.1410.154.507.00
7.5034.039.734.757.50
8.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支持并发读写,采取了“空间换时间”的机制,

type Map struct {mu Mutexread atomic.Valuedirty map[interface{}]*entrymisses int
}

对比原始的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

ch <- 10

非阻塞式:

select {case ch <- 10:...default
}

向channel发送数据时大概分为两大块,检查和数据发送,数据发送流程如下:

  • 如果channel的读等待队列存在接收者goroutine,将数据直接发送给第一个等待的goroutine,唤醒接收的goroutine
  • 如果channel的等待队列不存在接收者goroutine;如果循环数组buf来满,那么将会把数据发送到循环数组buf的队尾;如果循环数组buf已满,这个时候会走阻塞发送流程,将当前goroutine加入等待队列,并挂起等待唤醒。

接收:
发送操作,编译时转换为runtime.chanrecv函数
阻塞式:
调用chanrecv函数,并且block=true

<ch
v := ch
v,ok := <ch
for i := range ch {fmt.Println(i)
}

非阻塞式
调用chanrecv函数,并且block=true

select {case <- ch:....default
}

向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就会被加入到获取锁的等待队列里面,并阻塞等待唤醒

正常模式下,所有等待锁的goroutine按照FIFO(先进先出)顺序等待,唤醒的goroutine,不会直接拥有锁,而是和
新请求锁的goroutine竞争锁,新请求锁的ggoroutine具有优势,它正在CPU上执行,而且可能有好几个,
所以刚刚唤醒的goroutine有很大可能在竞争中失败,长时间获取不到锁,就会切换到饥饿模式

饥饿模式
当一个goroutine等待锁时间超过1毫秒时,他可能会遇到饥饿问题,在版本1.9中,这种场景下go mutex切换到饥饿模式,解决饥饿模式

饥饿模式下,直接把锁交给等待队列中排在第一位的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即不能获取读锁也不能获取写锁
使用场景:
读多于写的情况
底层实现结构

type RWMutex struct {w Mutex //复用互斥锁writeSem uint32 //信号量,用于写等待读readerSem uint32 //信号量。用于读等代写readerCount int32 //当前执行读的goroutine数量readerwait int32 //被阻塞准备读的goroutine的数量
}

注意点:

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

互斥锁和读写锁的区别:

  • 读写锁区分读者和写者,而互斥锁不区分
  • 互斥锁同一时间只允许一个线程访问对象,无论读写,读写锁同一时间只允许一个写者,但是允许多个读者同时读对象。

86、可重入锁

package mainimport ("fmt""sync""time"
)type ReentrantLock struct {mu       sync.Mutexowner    *goro // 当前持锁的 goroutineholds    int    // 锁的计数器,表示锁被同一 goroutine 拿到的次数
}type goro struct {id int
}var goroID int// 获取当前 goroutine 的 ID
func getGoroID() *goro {goroID++return &goro{id: goroID}
}func (r *ReentrantLock) Lock() {r.mu.Lock() // 先加锁defer r.mu.Unlock()current := getGoroID()// 如果当前 goroutine 已经持有锁,则计数器加 1if r.owner == current {r.holds++return}// 否则设置为当前 goroutine 为持锁者,并初始化计数器r.owner = currentr.holds = 1
}func (r *ReentrantLock) Unlock() {r.mu.Lock() // 先加锁defer r.mu.Unlock()// 如果当前 goroutine 不是持锁者,不能解锁if r.owner == nil || r.owner != getGoroID() {panic("unlock of unlocked lock")}r.holds--// 如果锁计数器为 0,表示可以释放锁if r.holds == 0 {r.owner = nil}
}func main() {var lock ReentrantLockvar wg sync.WaitGroup// 示例:一个 goroutine 中进行多次加锁和解锁wg.Add(1)go func() {defer wg.Done()// 第一次加锁lock.Lock()fmt.Println("Goroutine 1 acquired the lock first time.")// 第二次加锁lock.Lock()fmt.Println("Goroutine 1 acquired the lock second time.")// 释放锁lock.Unlock()fmt.Println("Goroutine 1 released the lock first time.")// 第三次加锁lock.Lock()fmt.Println("Goroutine 1 acquired the lock third time.")// 释放锁lock.Unlock()fmt.Println("Goroutine 1 released the lock second time.")// 释放锁lock.Unlock()fmt.Println("Goroutine 1 released the lock third time.")}()wg.Wait()
}

87、原子操作有哪些

  • 增减Add
  • 载入:Load
  • 比较并交换CompareAndSwap
  • 交换Swap
  • 存储:Store

88、原子操作和锁的区别

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

89、goroutine底层实现原理

type g struct {gold int64sched gobufstack stack gopcstartpc
}
type gobuf struct {sp uintptrpc uintptrg uintptrret uintptr}
type stack struct {lo uintptr //栈的下界内存地址hi uintptr // 栈的上街内存地址
}

状态流转

状态含义
空闲中_GidleG刚刚新建,仍未初始化
待运行_Grunnable就绪状态,G在运行队列中,等待M取出并运行
运行中_GrunningM正在运行这个G,这时候M会拥有一个P
系统调用中_GsyscallM正在运行这个G发起的系统调用,这时候M并不拥有P
等待中_GwaitingG在等待某些条件完成,这时候G不在运行也不再运行队列中(可能在channel等待队列中)
已中止_GdeadG未被使用,可能已执行完毕
栈复制中_GcopystackG正在获取一个新的栈空间并把原来的内容复制过去(用来防止GC扫描)

在这里插入图片描述
创建
通过go关键字调用底层函数runtime.newproc() 创建一个goroutine,当调用该函数之后,goroutine会被设置成runnable状态

func main() {go func() {fmt.Println("func routine")}()fmt.Println("main goroutine")
}

创建好的这个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数量

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
  1. 创建白、灰、黑三个集合
  2. 将所有对象放入白色集合中
  3. 遍历所有root对象,把遍历到的对象从白色集合放入灰色集合
  4. 遍历灰色集合,将灰色对象引用的对象从白色集合放入灰色集合,自身标记为黑色
  5. 重复步骤4,直到灰色中无任何对象:写屏障,辅助GC
  6. 收集所有白色对象

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在满足特定条件时被阻塞和唤醒

type Cond struct {noCopy noCopyL Lockernotify notifyListchecker copyChecker
}type notifyList struct {wait uint32notify uint32lock uintptrhead unsafe.Pointertail unsafe.Pointer
}
  • 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帮助检测代码中的数据竞争

go run -race main.go

相关文章:

Go常见问题与答案笔记

这是一篇值的收藏的go常见问题与答案的笔记&#xff0c;内容包括了go的高级&#xff0c;如&#xff1a;悲观锁与乐观锁区别&#xff0c;for range赋值、waitgroup底层原理、go同步原语、defer关键字讲解。 文章目录 1.悲观锁VS乐观锁的区别2.for range中赋值的变量&#xff0c…...

Redis 服务搭建

&#x1f4a2;欢迎来到张翊尘的开源技术站 &#x1f4a5;开源如江河&#xff0c;汇聚众志成。代码似星辰&#xff0c;照亮行征程。开源精神长&#xff0c;传承永不忘。携手共前行&#xff0c;未来更辉煌&#x1f4a5; 文章目录 Redis 服务搭建源代码编译安装获取源码安装依赖Ub…...

React-Router路由跳转、传参、抽象封装以及嵌套路由

React-Router 示例React-Router抽象路由模块步骤&#xff1a; 路由导航声明式导航编程式导航 路由导航传参searchParams传参params传参 嵌套路由配置默认二级路由404路由配置两种路由模式如何选择 示例 这里我创建两个页面&#xff0c;分别是主页和登录页分别使用编程式导航和…...

PhotoShop学习02

1.添加文本 这个工具栏是文字工具栏&#xff0c;快捷键是T。选择之后鼠标会变成一个竖杠外貌&#xff0c;我们可以借此在图片中写入文字。 选择后&#xff0c;上方的工具栏会变为专门调整文字工具 这个框点击旁边的小箭头可以选择我们我们电脑系统自带的字体&#xff0c;同时可…...

Isaac Sim与Isaac Lab初使用

目录 基于Omiverse下载Isaacsim安装Isaac Lab配置isaacsim环境测试克隆仓库配置python环境强化学习训练的测试 IsaacLab模板配置vscode环境ros接口安装 作为nvidia出品的仿真软件&#xff0c;很多机器人、机器狗【具身智能】都可以有很不错的效果&#xff0c;所以会使用isaac s…...

用户模块——自定义业务异常

用户信息查询接口 在开发用户系统时&#xff0c;一个最基本的需求就是获取用户的个人信息&#xff0c;比如昵称、头像、改名卡次数等。本部分将介绍如何实现一个用户信息查询接口&#xff0c;并结合项目中的用户背包表&#xff0c;查询用户是否有可用的改名卡。 1. 用户信息查…...

基于数据挖掘从经验方和医案探析岭南名医治疗妇科疾病的诊疗和用药规律

标题:基于数据挖掘从经验方和医案探析岭南名医治疗妇科疾病的诊疗和用药规律 内容:1.摘要 背景&#xff1a;岭南地区独特的地理环境、气候条件及人文风俗使该地区妇科疾病具有一定特点&#xff0c;岭南名医在长期临床实践中积累了丰富的治疗经验。目的&#xff1a;基于数据挖掘…...

从零到一开发一款 DeepSeek 聊天机器人

AI聊天机器人 目标设计方案系统架构技术选型功能模块 实现代码环境配置安装依赖 核心代码API 请求函数主循环函数 功能扩展1. 情感分析2. 多语言支持3. 上下文记忆4. 用户身份识别 总结附录 目标 开发一个智能聊天机器人&#xff0c;旨在为用户提供自然、流畅的对话体验。通过…...

开源CDN产品-GoEdge

一、背景 上篇文章分析了一下CDN的基本原理以及使用代码实现了一个乞丐版的智能DNS调度器。从整个例子我们可以清晰了解到CDN原理&#xff0c;也就那么回事。 但是&#xff0c;之前也讲过了&#xff0c;CDN产品融合的技术比较杂、也比较多。所以我就想着&#xff0c;万物皆有开…...

Resume全栈项目(一)(.NET)

文章目录 项目地址一、数据库创建/日志安装1.1 创建实体层1. Entities2. Enums 存放枚举 1.2 创建数据库层1. 安装Persistance层需要的库2. 创建ResumeDbContext3. 添加数据库配置/注册DBContext4. 执行Add-Migration5. 修改字段类型6. Enum支持Json 1.3 安装Serilog1. Api层安…...

如何快速下载并安装 Postman?

从下载、安装、启动 Postman 这三个方面为大家详细讲解下载安装 Postman 每一步操作&#xff0c;帮助初学者快速上手。 Postman 下载及安装教程(2025最新)...

【PICO】开发环境配置准备

Unity编辑器配置 安装Unity编辑器 安装UnityHub 安装Unity2021.3.34f1c1 添加安卓平台模块 Pico软件资源准备 资源准备地址&#xff1a;Pico Developer PICO SDK PICO Unity Integration SDK PICO Unity Integration SDK 为 PICO 基于 Unity 引擎研发的软件开发工具…...

Unity图形学Shader快速回顾

参考知识点来源于: 人间自有韬哥在&#xff0c; 唐老狮&#xff0c;窗外听轩雨 , 呆呆敲代码的小Y little_fat_sheep, AitTech, DeepSeek, 百度&#xff0c; 豆包 目录 一、渲染管线1.应用阶段2.几何阶段3.光栅化阶段 二、矩阵的几何意义1. 平移2. 旋转3. 缩放4.复合运算 三、…...

十六进制(Hexadecimal)简介

十六进制&#xff08;Hexadecimal&#xff09;简介 1.1 什么是十六进制&#xff1f; 十六进制是一种使用16个符号表示数值的系统&#xff1a;数字0-9&#xff08;表示0-9&#xff09;&#xff0c;字母A-F&#xff08;表示10-15&#xff09;。 1.2 十六进制表示法 在编程中&a…...

1、pytest基本用法

目录 先给大家分享下学习资源 1. 安装pytest 2. 编写用例规则 3. 执行用例 最近在学习pytest的用法 并且用这套框架替换了原来的unittest&#xff0c; 同是测试框架 确实感觉到pytest更加便捷 这边分享给大家我得学习心得 先给大家分享下学习资源 1 官方文档 pytest 官方…...

2024年3月全国计算机等级考试真题(二级C语言)

&#x1f600; 第1题 下列叙述中正确的是 A. 矩阵是非线性结构 B. 数组是长度固定的线性表 C. 对线性表只能作插入与删除运算 D. 线性表中各元素的数据类型可以不同 题目解析&#xff1a; A. 矩阵是非线性结构 错误。矩阵通常是二维数组&#xff0c;属…...

GitHub高级筛选小白使用手册

GitHub高级筛选小白使用手册 GitHub 提供了强大的搜索功能&#xff0c;允许用户通过高级筛选器来精确查找仓库、Issues、Pull Requests、代码等。下面是一些常用的高级筛选用法&#xff0c;帮助你更高效地使用 GitHub 搜索功能。 目录 搜索仓库搜索Issues搜索Pull Requests搜…...

如何用腾讯云建站做好一个多语言的建筑工程网站?海外用户访问量提升3倍!分享我的经验

作为新疆地区领先的工程建筑企业&#xff0c;我们深知在数字化浪潮中&#xff0c;一个专业、高效且具备国际视野的官方网站是企业形象与业务拓展的“门面担当”。然而&#xff0c;传统的建站流程复杂、技术门槛高、多语言适配难等问题&#xff0c;曾让我们在数字化转型中举步维…...

SpringBoot-配置文件中敏感信息的加密保姆级教程

前言 公司安全部门检查&#xff0c;要求系统配置文件中的敏感信息如数据库密码等&#xff0c;进行加密处理&#xff0c;否则将受到公司的安全处罚&#xff0c;无奈只要按照公司要求&#xff0c;对springboot项目配置文件的敏感信息进行加密和解密处理。详细教程如下。 快速上…...

数据结构——串

串是一种数据元素为字符的特殊的线性表。 1. 串的定义 零个或多个字符&#xff08;字母、数字或其他字符&#xff09;组成的有限序列。记为 S"a1a2...an"S"a1​a2​...an​"&#xff0c;长度为 nn&#xff0c;空串长度为0。 2.串的术语 串长度&#xf…...

使用python爬取网络资源

整体思路 网络资源爬取通常分为以下几个步骤&#xff1a; 发送 HTTP 请求&#xff1a;使用requests库向目标网站发送请求&#xff0c;获取网页的 HTML 内容。解析 HTML 内容&#xff1a;使用BeautifulSoup库解析 HTML 内容&#xff0c;从中提取所需的数据。处理数据&#xff…...

【MySQL | 七、存储引擎是什么?】

存储引擎是什么&#xff1f;作用于哪里&#xff1f; 1. 存储引擎的定义 存储引擎&#xff08;Storage Engine&#xff09;是数据库管理系统中负责 数据的存储、检索和管理 的核心组件。它决定了数据如何存储在磁盘上&#xff0c;以及如何从磁盘中读取数据。存储引擎是数据库与…...

Linux -- 进程间通信(IPC)-- 进程间通信、管道、system V 共享内存、system V 消息队列、责任链模式 、system V 信号量

一、什么是进程间通信 1.进程间通信的目的 数据传输&#xff1a;一个进程需要将它的数据发送给另一个进程。资源共享&#xff1a;多个进程之间共享同样的资源。通知事件&#xff1a;一个进程需要向另一个或一组进程发送消息&#xff0c;通知它&#xff08;它们&#xff09;发…...

远程登录服务(ssh)

一、远程登录服务概述 1. 概念 远程登录服务就像是一个神奇的桥梁&#xff0c;它让你能够跨越物理距离&#xff0c;通过网络连接到另一台计算机上进行操作。无论你身在何处&#xff0c;只要有网络连接&#xff0c;你就可以像坐在目标计算机前一样进行各种操作。 2. 功能 分享…...

【从零实现Json-Rpc框架】- 项目设计篇

&#x1f4e2;博客主页&#xff1a;https://blog.csdn.net/2301_779549673 &#x1f4e2;博客仓库&#xff1a;https://gitee.com/JohnKingW/linux_test/tree/master/lesson &#x1f4e2;欢迎点赞 &#x1f44d; 收藏 ⭐留言 &#x1f4dd; 如有错误敬请指正&#xff01; &…...

EtherCAT转CANopen配置CANopen侧的PDO映射

EtherCAT转CANopen配置CANopen侧的PDO映射 在工业自动化领域&#xff0c;EtherCAT和CANopen是两种广泛应用的通信协议。它们各自具有独特的优势&#xff0c;但在某些应用场景下&#xff0c;需要将这两种协议进行转换以实现设备间的高效数据交换。本文将详细介绍如何在使用Ethe…...

Vite管理的Vue3项目中monaco editer的使用以及组件封装

文章目录 背景环境说明安装流程以及组件封装引入依赖封装组件 外部使用实现效果 v-model实现原理 背景 做oj系统的时候,需要使用代码编辑器,决定使用Monaco Editor&#xff0c;但是因为自身能力问题&#xff0c;读不懂官网文档&#xff0c;最终结合ai和网友的帖子成功引入&…...

完整的类在JVM中的生命周期详解

首先给出一个示例代码&#xff1a; 示例的目标是展示一个多功能的类结构&#xff0c;包含继承、接口实现、静态成员、本地方法、线程安全等特性&#xff0c;同时模拟一个简单的“计算器”场景&#xff0c;计算并管理数字。&#xff08;尽量将所有的 Java 组件和关键字都给出&am…...

大数据学习栈记——HBase操作(shell java)

本文介绍HBase在shell终端的常见操作以及如何利用java api操作HBase&#xff0c;操作系统&#xff1a;Ubuntu24.04 参考&#xff1a; https://blog.51cto.com/u_16099228/8016429 https://blog.csdn.net/m0_37739193/article/details/73618899 https://cloud.tencent.com/d…...

【商城实战(65)】退换货流程全解析:从前端到后端的技术实现

【商城实战】专栏重磅来袭!这是一份专为开发者与电商从业者打造的超详细指南。从项目基础搭建,运用 uniapp、Element Plus、SpringBoot 搭建商城框架,到用户、商品、订单等核心模块开发,再到性能优化、安全加固、多端适配,乃至运营推广策略,102 章内容层层递进。无论是想…...

改进BM25稀疏检索和BGE稠密检索

改进BM25稀疏检索和BGE稠密检索 检索算法层面 混合检索策略优化 自适应加权融合:在BM25和BGE等混合检索时,根据查询文本的特征(如长度、专业术语占比等)动态调整两者的权重。例如,对于包含大量专业术语的查询,增加BGE的权重;对于关键词明确的简单查询,增加BM25的权重。…...

WPS二次开发系列:以自动播放模式打开PPT文档

在前面文章中 android 调用wps打开文档并感知保存事件 介绍了如何使用WPS SDK打开文档&#xff0c;那么我们是否能够实现在打开WPS 文档的时候能够传递一些参数来控制打开文档的行为呢&#xff0c;经过研究WPS SDK相关文档和API&#xff0c;最终实现了 以自动播放方式打开PPT文…...

当AI重构编程范式:Java 24的进化逻辑与技术深水区的战略突围

一、语言进化的底层密码&#xff1a;从“工具适配”到“定义规则” 在2025年3月19日发布的Java 24中&#xff0c;Oracle以"30周年技术宣言"的姿态展示了编程语言进化的新范式。该版本不仅包含模式匹配、结构化并发等21项JEP特性&#xff0c;更通过后量子加密、AI原生…...

air780eq 阿里云

硬件&#xff1a;APM32F030C8 Air 780eq 参考文档&#xff1a; 合宙780E-4G模块通过AT指令连接到阿里云平台&#xff0c;实现信息的收发_air780e上传阿里云属性值at命令-CSDN博客 阿里云 - atair780eq - 合宙文档中心 4G模块接入阿里云-实现数据上传和命令下发_4g模块上传…...

网络安全之vlan实验

在对vlan进行一定的学习之后我们来练习一个小实验来加深理解记忆 首先是对实验进行一个搭建 第一部分&#xff1a;给交换机配置vlan 首先是sw1 [Huawei]vlan batch 2 to 5 [Huawei]int g0/0/1 [Huawei-GigabitEthernet0/0/1]port hybrid tagged vlan 2 [Huawei-GigabitEthe…...

mac命令行快捷键

光标移动 Ctrl A: 将光标移动到行首。Ctrl E: 将光标移动到行尾。Option 左箭头: 向左移动一个单词。Option 右箭头: 向右移动一个单词。 删除和修改 Ctrl K: 删除从光标到行尾的所有内容。Ctrl U: 删除从光标到行首的所有内容。Ctrl W: 删除光标前的一个单词。Ctrl …...

计算机网络 - OSI 七层模型

OSI 七层模型 OSI&#xff08;Open System Interconnection&#xff0c;开放系统互联&#xff09;模型由 ISO&#xff08;国际标准化组织&#xff09; 制定&#xff0c;目的是为不同计算机网络系统之间的通信提供一个标准化的框架。它将网络通信划分为 七个层次&#xff0c;每…...

TCP/IP 协议族详细知识点清单

&#x1f4da; TCP/IP 协议族详细知识点清单 一、概述与体系结构 &#x1f310; TCP/IP 协议模型&#xff08;四层模型&#xff09; 层次协议功能应用层HTTP、FTP、DNS、SMTP提供应用服务传输层TCP、UDP端到端传输&#xff0c;可靠或不可靠网络层IP、ICMP、ARP、RARP寻址、路…...

Vue3(自定义指令directive详解)

文章目录 前言一、自定义指令的生命周期钩子二、自定义指令的创建与注册使用三、扩展 简化形式​总结 前言 在Vue3中&#xff0c;自定义指令是一种强大的工具&#xff0c;允许开发者扩展和增强HTML元素的功能。以下是对Vue3中自定义指令的详细解析&#xff1a; 一、自定义指令…...

Redis--redis客户端

目录 一、引言 二、数据库管理命令 三、redis客户端 四、Java客户端使用Redis 五、相关命令使用 1.get&#xff0c;set 2.exists&#xff0c;del 3.keys 4.expire&#xff0c;ttl 六、总结 一、引言 在之前学了redis相关类型命令之后&#xff0c;本篇文章&#xff0c;…...

【高项】信息系统项目管理师(十)项目风险管理【5分】

项目风险是一种不确定的事件或条件,一旦发生,会对项目目标产生某种正面或负面的影响。项目风险既包括对项目目标的威胁,也包括促进项目目标的机会。已知风险是那些已经经过识别和分析的风险,对于已知风险,对其进行规划,寻找应对方案是可行的;虽然项目经理们可以依据以往…...

jenkins批量复制视图项目到新的视图

1、当前视图为 测试2分支&#xff0c;创建了新的视图为国际化预生产 2、进入系统设置的脚本管理 import hudson.model.* //源view def str_view "测试2分支" //目标view def str_new_view "国际化预生产" //源job名称(模糊匹配) def str_search &qu…...

uv:Rust 驱动的 Python 包管理新时代

在 Python 包管理工具层出不穷的今天&#xff0c;pip、pip-tools、poetry、conda 等各有千秋。而今天要介绍的 uv&#xff0c;则是一款由 Astral 团队推出、采用 Rust 编写的全新工具&#xff0c;目标直指成为 “Python 的 Cargo”。它不仅在性能上表现优异&#xff0c;而且在功…...

GD32 ISP下载程序(串口烧录)

一、下载烧录软件 下载地址兆易创新GigaDevice-资料下载兆易创新GD32 MCUhttps://www.gd32mcu.com/cn/download?kwGD32All-In-OneProgrammer&lancn 二、使用USB转串口连接GD32开发板 这里使用GD32E230C8T6为例&#xff1a; GD32E230C8T6USB 转串口模块说明PA9&#xff…...

Spring MVC 配置详解与入门案例

目录 引言 一、Spring MVC 的发展背景 1. Model I 与 Model II 2. MVC 模式 二、Spring MVC 入门案例 1. 创建 WEB 工程并引入依赖 2. 配置 web.xml 3. 配置 springmvc.xml 4. 创建控制器和视图 5. 部署并测试 三、Spring MVC 原理 1. 核心组件 2. 请求处理流程 …...

【10万QPS压力测试】Redis三主三从高可用集群基准测试

&#x1f4d5;我是廖志伟&#xff0c;一名Java开发工程师、《Java项目实战——深入理解大型互联网企业通用技术》&#xff08;基础篇&#xff09;、&#xff08;进阶篇&#xff09;、&#xff08;架构篇&#xff09;清华大学出版社签约作家、Java领域优质创作者、CSDN博客专家、…...

git的进阶使用

一.协作冲突 举个简单的例子&#xff0c;公司里两个人(A,B)同一天上班&#xff0c;都拉取了远程仓库数据。然后A做完了所有的工作&#xff0c;进行了x文件的修改并提交至远程仓库。而B在做自己工作的时候不小心或者需要修改x文件&#xff0c;B认为A没有操作x文件直接push没有问…...

23种设计模式-责任链(Chain of Responsibility)设计模式

责任链设计模式 &#x1f6a9;什么是责任链设计模式&#xff1f;&#x1f6a9;责任链设计模式的特点&#x1f6a9;责任链设计模式的结构&#x1f6a9;责任链设计模式的优缺点&#x1f6a9;责任链设计模式的Java实现&#x1f6a9;代码总结&#x1f6a9;总结 &#x1f6a9;什么是…...

MySQL复习

1基本操作复习 1.1数据库创建 创建数据库create database 数据库名;判断再创建数据库create database if not exists 数据库名;创建数据库指定字符集create database 数据库名 character set 字符集;创建数据库指定排序方式create database 数据库名 collate 排序方式;创建数据…...

【嵌入式学习2】c语言重点整理

目录 ## 重点掌握 1、数组 2、指针 3、结构体 4、函数 回调函数的常见用途 ## 如何区分数组指针&#xff0c;指针数组&#xff0c;函数指针&#xff0c;结构体指针&#xff0c;指针偏移量 ## 重点掌握 1、数组 https://blog.csdn.net/weixin_60546365/article/details…...