1. GMP 调度模型
1.1 GMP 概述
Go 语言中的 GMP 模型是 Goroutine 的调度机制,通过三个核心概念实现并发任务的高效调度:
- G(Goroutine):轻量级线程,每个 G 代表一个独立的任务。
- M(Machine):代表操作系统的线程,负责执行 G。
- P(Processor):逻辑处理器,管理 G 的队列,绑定 M 来调度 G。
1.2 调度模型的原理
- 工作窃取(Work Stealing):当一个 P 没有要执行的 Goroutine 时,它会从其他 P 中窃取任务执行,以保持负载平衡。
- Hand-off(任务交接):Hand Off 是 Go 调度系统中的优化机制。当一个 M(Machine)由于执行系统调用、I/O 或锁等待等操作被阻塞时,P(Processor)会与 M 解除绑定,将 M 上的 Goroutine 放回全局队列或等待队列中,以便其他空闲的 M 或 P 能继续执行这些任务。通过这种任务交接的方式,Go 可以避免 Goroutine 长时间停留在阻塞的 M 上,保持高效的调度,确保系统资源得到充分利用。
- 抢占式调度:从 Go 1.14 开始,长时间占用 CPU 的 Goroutine 会被强制中断,允许其他 Goroutine 执行,避免单个 Goroutine 独占 CPU。
代码示例:Goroutine 调度
package main
import (
"fmt"
"time"
)
func worker(id int) {
fmt.Printf("Worker %d is running\n", id)
time.Sleep(time.Second)
}
func main() {
for i := 0; i < 5; i++ {
go worker(i)
}
time.Sleep(2 * time.Second) // 等待 Goroutine 完成
}
2. sync 包
2.1 sync.WaitGroup
sync.WaitGroup 用于等待一组 Goroutine 完成。通过 Add 增加任务计数,Done 完成任务,Wait 等待所有任务结束。
sync.WaitGroup 的底层原理是通过一个 计数器 和 信号机制 来协调 Goroutine 之间的同步。它有一个内部的 state 字段,包含计数器和 Goroutine 的等待状态。当调用 Add(n) 方法时,计数器增加 n,表示有 n 个 Goroutine 需要等待完成。当 Goroutine 完成任务后,调用 Done() 方法,内部的计数器减一。
WaitGroup 的核心在于 runtime_Semacquire 和 runtime_Semrelease 系统调用,用来挂起和唤醒 Goroutine。当计数器为 0 时,Wait() 方法调用 runtime_Semrelease 来唤醒等待的 Goroutine。Goroutine 在 Wait() 方法时调用 runtime_Semacquire 挂起,直到所有任务完成,计数器归 0,等待中的 Goroutine 被唤醒继续执行。这个机制保证了多个 Goroutine 能够正确同步执行。
关键流程:
- Add(n) 增加计数器,表示有
n个 Goroutine 需要等待完成。 - Done() 减少计数器,每次 Goroutine 完成时调用。
- Wait() 等待所有 Goroutine 完成,当计数器为 0 时唤醒等待的 Goroutine。
代码示例:sync.WaitGroup
package main
import (
"fmt"
"sync"
)
func worker(id int, wg *sync.WaitGroup) {
defer wg.Done()
fmt.Printf("Worker %d is done\n", id)
}
func main() {
var wg sync.WaitGroup
for i := 0; i < 5; i++ {
wg.Add(1)
go worker(i, &wg)
}
wg.Wait() // 等待所有 Goroutine 结束
}
2.2 sync.Mutex
sync.Mutex 是用于控制并发访问的互斥锁,用于防止多个 Goroutine 同时访问共享资源。
sync.Mutex 的底层原理是通过 自旋锁 和 信号量机制 来实现 Goroutine 间的互斥访问。Mutex 包含一个内部的状态字段 state 和一个等待队列。当 Goroutine 尝试获取锁时,Mutex 会检查 state 字段,如果锁是空闲的,则将锁状态标记为已占用,当前 Goroutine 获得锁。如果锁已经被占用,则进入 自旋 阶段,即尝试多次获取锁,若多次自旋失败,才会将当前 Goroutine 挂起。
挂起 Goroutine 是通过 runtime_Semacquire 系统调用实现的,等待其他 Goroutine 释放锁时会被阻塞。当锁被释放时,runtime_Semrelease 会被调用来唤醒等待的 Goroutine。
Mutex 有两种模式:
- 正常模式:短暂的锁竞争会通过自旋快速获取锁。
- 饥饿模式:当有 Goroutine 长时间等待锁时,进入饥饿模式,此时锁优先分配给等待最久的 Goroutine,避免长时间的锁等待。
这种设计避免了频繁的上下文切换,既保证了锁的高效性,又防止了饥饿问题。
另外:runtime_Semacquire 和 runtime_Semrelease 是 Go 语言运行时的系统调用,用于挂起和唤醒 Goroutine。
死锁 是指在并发程序中,两个或多个 Goroutine(或线程)因相互等待对方释放资源(如锁、channel),导致所有相关的 Goroutine 都无法继续执行,从而进入无限等待的状态。简而言之,死锁是由于相互依赖的资源竞争引起的,程序陷入无法进展的局面。
代码示例:sync.Mutex
package main
import (
"fmt"
"sync"
)
var counter int
var mu sync.Mutex
func increment(wg *sync.WaitGroup) {
mu.Lock()
counter++
mu.Unlock()
wg.Done()
}
func main() {
var wg sync.WaitGroup
for i := 0; i < 5; i++ {
wg.Add(1)
go increment(&wg)
}
wg.Wait()
fmt.Println("Final counter:", counter)
}
2.3 sync.Cond
sync.Cond 是条件变量,用于 Goroutine 之间的信号同步。通过 Wait 让 Goroutine 等待,通过 Signal 或 Broadcast 唤醒等待的 Goroutine。Signal 会唤醒一个 Goroutine,Broadcast 会唤醒所有 Goroutine。
这里是 sync.Cond 中的 signal 和 broadcast 操作与锁的简明流程说明:
流程说明:
-
持有锁:在调用
Wait()前,消费者 Goroutine 必须持有锁。调用Wait()后,Goroutine 会释放锁,进入等待状态,直到被signal或broadcast唤醒。 -
等待时释放锁:当消费者调用
cond.Wait()时,自动释放锁,以便让其他 Goroutine 可以修改共享资源(例如队列)。消费者会进入等待状态。 -
唤醒时重新获取锁:当生产者调用
signal()或broadcast()唤醒消费者时,消费者 Goroutine 会尝试重新获取锁,获取锁成功后,才会继续检查条件并执行后续逻辑。 -
生产者持有锁:生产者在修改共享资源(如向队列中添加数据)时,必须持有锁,确保数据的并发安全。
-
释放锁:生产者在完成对共享资源的修改后,调用
signal()或broadcast(),唤醒等待中的消费者,随后释放锁。
代码示例:sync.Cond
package main
import (
"fmt"
"sync"
"time"
)
type Queue struct {
data []int
cond *sync.Cond
}
func (q *Queue) Enqueue(n int) {
q.cond.L.Lock()
q.data = append(q.data, n)
fmt.Println("Enqueued:", n)
q.cond.Signal() // 唤醒等待的消费者
q.cond.L.Unlock()
}
func (q *Queue) Dequeue() {
q.cond.L.Lock()
for len(q.data) == 0 {
fmt.Println("Queue is empty, waiting for data...")
q.cond.Wait() // 等待数据被添加
}
val := q.data[0]
q.data = q.data[1:]
fmt.Println("Dequeued:", val)
q.cond.L.Unlock()
}
func main() {
q := &Queue{cond: sync.NewCond(&sync.Mutex{})}
go func() {
for i := 0; i < 5; i++ {
q.Enqueue(i)
time.Sleep(1 * time.Second)
}
}()
go func() {
for i := 0; i < 5; i++ {
q.Dequeue()
time.Sleep(2 * time.Second)
}
}()
time.Sleep(10 * time.Second)
}
2.4 sync.Once
sync.Once 用于确保某个操作只会执行一次,常用于初始化操作。
总结流程
- Do(f) 方法首先通过原子操作检查 done 值。
- 如果 done == 1,操作已经执行过,直接返回。
- 如果 done == 0,进入慢路径 doSlow(f)。
- 在 doSlow(f) 中,首先获取互斥锁,保证操作的并发安全。
- 再次检查 done,确保操作只会执行一次。
- 执行传入的函数 f(),然后将 done 设置为 1,表示操作已经完成。
代码示例:sync.Once
package main
import (
"fmt"
"sync"
)
var once sync.Once
func initialize() {
fmt.Println("Initialized")
}
func main() {
for i := 0; i < 3; i++ {
once.Do(initialize) // 只会执行一次
}
}
2.5 sync.Pool
sync.Pool 是对象池,用于缓存和重用临时对象,减少 GC 负担。
sync.Pool 是 Go 中的对象池,用于缓存和复用临时对象,从而减少频繁的内存分配和垃圾回收开销。它为每个 P(逻辑处理器)维护一个本地缓存,确保对象能够就近复用,减少跨线程的争用。当池中没有可用对象时,会通过 New 方法生成新的对象,并将其缓存。GC(垃圾回收)时,池中的对象会被清空,因此适用于短生命周期的临时对象。
sync.Pool 不是线程池。它只用于管理和缓存对象,减少内存分配压力,而线程池是用来管理 Goroutine 或线程的执行。sync.Pool 解决的是对象的高效复用问题,而线程池则负责任务的调度和执行。
代码示例:sync.Pool
package main
import (
"fmt"
"sync"
)
var pool = sync.Pool{
New: func() interface{} {
return new(int)
},
}
func main() {
v := pool.Get().(*int)
*v = 100
fmt.Println("Value:", *v)
pool.Put(v)
}
3. context 包
3.1 context.Context
context.Context 是 Go 中用于传递请求上下文、控制 Goroutine 生命周期的机制。常见用法包括超时控制、取消操作、传递元数据等。
context.Context 是 Go 中用于在 Goroutine 之间传递请求上下文和控制其生命周期的机制,内部通过树状结构组织不同的 context 实例。每个 Context 可以有父 Context 和子 Context,父 Context 可以通过 cancel() 取消所有子 Context,从而终止与该上下文相关联的所有 Goroutine。常见的 Context 结构包括 cancelCtx(用于取消操作)和 timerCtx(用于超时控制),每个 Context 都包含一个 Done 通道用于通知取消事件。
context 的核心部分包括 Deadline()、Done()、Err() 和 Value() 四个主要方法。Done 通道是控制机制的关键,当父 Context 调用 cancel() 或定时器触发超时时,Done 通道会关闭,通知所有监听的 Goroutine 停止操作。通过这种方式,context 实现了跨 Goroutine 的超时、取消和数据传递控制。
子context会从父context继承Done通道,当父context的Done通道关闭时,子context的Done通道也会关闭,从而通知子context的Goroutine停止操作。
3.2 常见的 context 类型
- context.Background():根 context,通常用于主函数。
- context.WithCancel():创建一个可取消的 context,子 Goroutine 可以通过接收信号来终止操作。
- context.WithTimeout():创建带有超时时间的 context,超时后自动取消操作。
代码示例:context 控制 Goroutine 取消
package main
import (
"context"
"fmt"
"time"
)
func task(ctx context.Context) {
select {
case <-time.After(2 * time.Second):
fmt.Println("Task completed")
case <-ctx.Done():
fmt.Println("Task cancelled:", ctx.Err())
}
}
func main() {
ctx, cancel := context.WithCancel(context.Background())
go task(ctx)
time.Sleep(1 * time.Second)
cancel() // 取消操作
time.Sleep(1 * time.Second)
}
4. Go 反射
4.1 反射简介
反射是 Go 语言在运行时动态检查和操作类型和变量的机制。通过 reflect 包,开发者可以获取变量的类型、值并进行修改。
反射是 Go 语言在运行时提供的一种机制,允许开发者动态检查变量的类型和值,并在运行时对其进行操作。这通过 reflect 包实现,提供了 reflect.Type 和 reflect.Value 两个关键类型,分别用于获取变量的类型信息和值信息。反射在高级框架(如依赖注入、ORM)中非常常用,因为它允许在编译时无法确定类型的情况下操作对象。
由于反射需要在运行时操作对象,Go 会将涉及到的变量分配到堆上,这导致了 内存逃逸。例如,当使用 reflect.ValueOf(&x) 传递变量时,Go 会将变量 x 从栈中逃逸到堆中,以便在运行时通过指针引用和修改。反射带来的内存逃逸会增加垃圾回收的负担,因此在性能敏感的场景中,滥用反射可能导致效率降低。
反射与断言
断言 是 Go 语言中用于将接口类型变量转换为具体类型的机制。接口可以保存任意类型的值,断言允许从接口中提取实际的底层类型。如果断言成功,它返回具体类型的值;如果失败,可以通过安全断言避免程序崩溃。断言通常用于编译时知道具体类型的情况下,适合处理固定类型的接口。
反射 是 Go 语言在运行时动态检查和操作变量类型和值的机制。通过 reflect 包,开发者可以获取变量的类型 (reflect.Type) 和值 (reflect.Value),并进行操作,如修改值或调用方法。反射在运行时处理不确定类型的变量,适用于框架开发等复杂场景。由于反射是在运行时执行的,它可能引发性能开销和内存逃逸。
区别:
- 断言 只适用于接口类型,编译时使用,性能高,通常不会引发内存逃逸。
- 反射 可以处理任意类型,运行时使用,灵活性强,但性能较低,通常会引发内存逃逸。
4.2 反射的基本使用
reflect.TypeOf():获取变量的类型。reflect.ValueOf():获取变量的值。reflect.Set():通过反射修改变量的值。
代码示例:反射修改结构体字段
package main
import (
"fmt"
"reflect"
)
type Person struct {
Name string
Age int
}
func main() {
p := &Person{"John", 30}
v := reflect.ValueOf(p).Elem()
v.FieldByName("Name").SetString("Jane") // 修改字段
fmt.Println(p)
}
5. unsafe 包
5.1 unsafe 包简介
unsafe 包提供了一些绕过 Go 类型安全检查的功能,用于 直接操作内存地址。常用于高性能或需要与 C 语言交互的场景。
unsafe 包是 Go 语言中的一个特殊包,它允许开发者绕过 Go 的类型安全机制,直接操作内存地址。通常情况下,Go 语言会严格检查变量类型并自动管理内存,以确保程序安全。但在一些性能敏感场景下(如与 C 语言交互、高性能内存操作等),我们需要手动操作内存,这时可以通过 unsafe 包来实现。
通俗解释 unsafe 包:
- 直接操作内存:
unsafe.Pointer可以将任意类型的指针转换为内存地址,并允许我们在地址上读写数据。它打破了 Go 的类型安全性,提供类似 C 语言中指针操作的能力。 - 非类型安全:使用
unsafe的操作是不安全的,意味着你可以访问任意内存区域,可能导致内存泄漏、程序崩溃等问题。
5.2 uintptr 和 unsafe.Pointer 的区别
- uintptr:表示一个内存地址的数值,但不能直接用于内存读写操作。
- unsafe.Pointer:通用指针类型,可以将其转换为任意类型的指针,用于内存操作。
代码示例:使用 unsafe.Pointer
package main
import (
"fmt"
"unsafe"
)
func main() {
var x int = 10
ptr := unsafe.Pointer(&x)
fmt.Printf("Value at pointer: %d\n", *(*int)(ptr))
}
5.3 Go 对象内存对齐
Go 中对象的内存分配遵循系统字长对齐规则,保证数据访问的高效性。不同类型的数据会根据其大小和对齐要求进行排列,避免内存访问开销。
内存对齐: 内存对齐是指编译器为了提高 CPU 的内存访问效率,会将数据按特定字节边界存放。例如,32 位系统中,一个 4 字节的整数通常存放在 4 字节对齐的内存地址上,这样 CPU 读取时可以一次性读取 4 字节数据,而无需拆分多次访问。
- 对齐的好处:提高 CPU 访问速度,减少不必要的内存访问。
unsafe与对齐:使用unsafe包时,你可以手动操作内存地址,可能会忽略对齐要求,这在某些平台上可能导致性能下降甚至程序错误。因此,在使用unsafe包时,开发者需要注意内存对齐问题,以避免潜在问题。
简而言之,unsafe 包给了开发者更大的控制权,但也带来了更多的责任,特别是在处理内存对齐和跨平台兼容性问题时需要谨慎。
高频面试题总结
1. GMP 调度模型
-
GMP 是什么?
GMP 是 Go 语言并发模型的核心组成部分,分别代表 Goroutine(G)、Machine(M) 和 Processor(P)。每个 Goroutine 是一个轻量级线程,P 是逻辑处理器,M 是底层操作系统线程。每个 P 负责管理一组 Goroutine,P 和 M 绑定后,M 执行 P 中的 Goroutine。P 处理的是 Go 语言的任务队列(即 Goroutine 列表),M 是与操作系统的实际线程绑定的实体。Go 语言通过 GMP 模型实现了高效的 并发调度,使得大量 Goroutine 可以并发执行。
-
Work Stealing 是什么?
工作窃取(Work Stealing)是一种调度策略,用于在多处理器环境下均衡任务负载。在 Go 语言的 GMP 模型中,如果一个 P 没有可执行的 Goroutine,它会尝试 从其他 P 的队列中窃取任务执行。工作窃取的目的是为了确保多核 CPU 的资源能够被充分利用,减少 CPU 核心空闲时间,同时保证任务执行的并行性。在实践中,P 通过从其他 P 的任务队列末尾窃取 Goroutine,避免全局队列的竞争,提升并发效率。
-
Hand-off(任务交接)是怎么做的?
在 Go 调度模型中,当一个 Goroutine 被阻塞(例如等待 I/O 操作、锁等)时,P 将该 Goroutine 从执行队列中移除(M不阻塞了再恢复),并尝试调度其他 Goroutine。如果 P 没有可用的 Goroutine,它会将执行权交还给全局调度器或窃取其他 P 的 Goroutine。这种任务交接机制保证了 Goroutine 的高效执行,避免长时间的任务阻塞影响整个系统的运行。同时,P 还可能将任务移交给其他 M 绑定的 P 来执行,以提升系统的并发性能。
-
抢占式调度是如何工作的?
抢占式调度是指调度器可以在 Goroutine 长时间运行时,强制中断它的执行,给其他 Goroutine 机会执行。Go 语言在 1.14 版本引入了抢占式调度机制,解决了 Goroutine 长时间占用 CPU 的问题。通过这种方式,调度器可以定期检查 Goroutine 的执行时间,如果某个 Goroutine 占用 CPU 过长,它会被挂起,允许其他等待的 Goroutine 执行。抢占式调度的引入确保了 Goroutine 不会独占 CPU 资源,改善了系统的响应能力。
-
正常模式和饥饿模式的区别?
正常模式是 Go 调度器默认的锁争夺模式,在这种模式下,新来的 Goroutine 也可以竞争锁资源,获得执行机会。饥饿模式是在锁被某些 Goroutine 长时间持有的情况下,为避免其他 Goroutine 永远无法获得锁而设计的。当锁进入饥饿模式时,锁优先分配给等待最久的 Goroutine,而不是新来的 Goroutine。这确保了在极端情况下 Goroutine 不会被饿死。Go 的 Mutex 锁会根据锁竞争的情况在这两种模式之间切换,以平衡性能和公平性。
2. sync 包详解
-
为什么 Go 的锁是非公平锁?
Go 语言的 Mutex 是非公平锁。非公平锁意味着 Goroutine 获取锁的顺序 并不完全按照 Goroutine 请求锁的顺序来分配。设计非公平锁的目的是为了提高系统性能,减少线程间的竞争开销。公平锁会严格按照 Goroutine 请求锁的顺序分配锁,这会导致更多的上下文切换,从而增加 CPU 的调度负担,降低系统的整体吞吐量。非公平锁通过减少 Goroutine 之间的等待和竞争,避免频繁的锁切换,从而提高并发性能。
-
Mutex 的两种模式是什么?
-
正常模式:当锁的竞争不激烈时,Go 使用正常模式,在这种模式下,新来的 Goroutine 有可能比正在等待锁的 Goroutine 先获取锁,从而减少锁竞争。
-
饥饿模式:当锁的竞争激烈且某个 Goroutine 长时间无法获得锁时,Mutex 会进入饥饿模式。在饥饿模式下,锁优先分配给等待最久的 Goroutine,而不会让新来的 Goroutine 直接获取锁。
-
-
Mutex 为什么需要这两种模式?
Mutex 设计为非公平锁,以提升锁的获取速度,减少 Goroutine 之间的竞争和上下文切换的开销。当锁竞争不激烈时,正常模式可以确保 Goroutine 快速获取锁,提高性能;当锁竞争激烈时,饥饿模式确保等待时间长的 Goroutine 可以优先获得锁,防止 Goroutine 被长期饿死。这两种模式的设计折中考虑了系统的性能和公平性需求。
-
等待队列中的 Goroutine 能直接拿到锁吗?
是的,新来的 Goroutine 有可能在等待队列中的 Goroutine 获取锁之前抢先获得锁。这是因为 Go 的 Mutex 是非公平锁,新来的 Goroutine 有机会直接参与锁的竞争,而不用等待队列中的所有 Goroutine 先获取锁。
-
Mutex 是可重入锁吗?
Go 的 Mutex 不是可重入锁。可重入锁允许同一个 Goroutine 多次获取同一把锁,而不发生死锁。而 Go 的 Mutex 如果同一个 Goroutine 在持有锁的情况下再次请求锁,则会发生死锁。因此,Go 的 Mutex 不适合在递归函数中使用。
-
RWMutex 和 Mutex 有什么区别?
RWMutex 是读写锁,允许多个 Goroutine 同时读取,但写操作是互斥的;Mutex 是普通的互斥锁,不允许并发读写。RWMutex 更适合读操作频繁、写操作较少的场景,它可以提高读操作的并发度,提升系统性能。如果写操作较多,Mutex 的性能会更好,因为 RWMutex 在写操作时仍然需要独占锁。
-
Mutex 如何挂起和唤醒 Goroutine?
当 Goroutine 试图获取已经被其他 Goroutine 持有的 Mutex 时,它会被挂起。Go 语言的挂起和唤醒机制是基于信号量(semaphore)实现的。具体来说,
runtime_Semacquire会将 Goroutine 挂起等待锁,runtime_Semrelease会唤醒等待锁的 Goroutine,使其继续执行。 -
sync.Pool 和 GC 的关系是什么?
sync.Pool是 Go 语言中的对象池,常用于缓存短生命周期的对象,以减少频繁的内存分配和回收。sync.Pool在 GC(垃圾回收)时会清理local缓存,并将未被使用的对象转移到victim区域。在下次对象请求时,P可以从victim区域获取对象,减少内存分配的开销。 -
什么时候 P 会用 victim 数据?
当 P 的 本地
local池中没有可用对象时,P 会尝试从victim区域获取对象。如果victim区域中也没有可用对象,则需要从全局池中分配新的对象。 -
为什么不设计全局共享队列?
全局共享队列会导致锁的激烈竞争,从而降低并发性能。而 Go 通过
poolChain和 P 本地缓存来减少锁的竞争,提升对象池的并发性能。poolChain结合 Go 的 P 模型,使得 每个 P 都有自己的本地缓存,减少了全局锁的争用。 -
sync.Pool 的优缺点是什么?
- 优点:
sync.Pool提供了高效的对象缓存机制,减少了频繁的内存分配和垃圾回收压力,提高了程序的性能。 - 缺点:
sync.Pool的内存使用量难以控制,GC 后即使从victim恢复对象,性能仍略有下降。同时,大量缓存的对象可能导致程序使用 更多的内存。
- 优点:
-
sync.Once 的作用是什么?
sync.Once保证某个操作在整个程序生命周期中只执行一次。典型的使用场景包括单例模式的初始化函数。sync.Once通过内部的状态变量和锁机制,确保操作只执行一次,并且即使多个 Goroutine 同时调用,也不会重复执行。先原子操作检查状态,再加锁,再检查状态,再执行操作,最后解锁。 -
WaitGroup 的使用场景是什么?
WaitGroup用于等待一组 Goroutine 完成。它提供了一种 计数器机制,调用Add方法增加计数器,调用Done方法减少计数器,调用Wait方法阻塞,直到计数器归零。这非常适合用来协调并发任务的执行顺序,确保主 Goroutine 等待所有子 Goroutine 执行完毕后再继续执行。
3. context 包
-
context.Context 的使用场景是什么?
context.Context用于在 Goroutine 之间传递取消信号、超时控制或键值对数据。常见的使用场景包括:传递 HTTP 请求的上下文,控制子任务的取消,超时处理等。它是管理并发任务生命周期的核心工具,特别是在复杂的 Goroutine 链条中。 -
context.Context 的原理是什么?
context.Context的实现是 树状结构。父context可以通过context.WithCancel、context.WithTimeout或context.WithValue创建子context,当父context被取消或超时时,所有子context都会自动取消(子context共享父context的Done())。 -
父子 Context 的关系是什么?
每个子
context都由父context创建,并继承了父context的取消信号和超时信息。当父context被取消时,所有子context会接收到同样的取消信号,并终止相应的操作。 -
valueCtx 和 timeCtx 的原理是什么?
-
valueCtx:用于在 Goroutine 之间传递键值对数据。它允许在多个 Goroutine 之间共享配置信息或其他上下文信息。
-
timeCtx:用于设置任务的超时时间。通过
context.WithTimeout或context.WithDeadline创建timeCtx,一旦超时,任务会被取消。
-
4. sync.Cond
-
sync.Cond 的作用是什么?
sync.Cond是 Go 中用于 Goroutine 之间同步的机制,通常用于协调多个 Goroutine 的执行顺序。sync.Cond提供了Wait、Signal和Broadcast方法,用于在 Goroutine 之间传递信号,协调它们的执行。Wait方法阻塞当前 Goroutine,直到收到信号为止,Signal唤醒一个等待中的 Goroutine,而Broadcast则唤醒所有等待中的 Goroutine。常用于生产者消费者模型
5. Go 反射
-
反射是什么?
反射是 Go 语言提供的用于在 运行时动态检查、获取或修改类型和对象的功能。通过反射,程序可以在不知道类型的情况下操作对象。
-
反射的使用场景?
反射广泛应用于框架、库、工具等场景,尤其是那些需要处理不确定类型的数据场景,比如序列化库、ORM 框架等。
-
反射能修改方法吗?
Go 的反射机制 不允许修改方法。这是因为方法属于不可变的函数,Go 运行时并没有暴露修改它们的能力。
-
什么样的字段能被反射修改?
只有导出的(首字母大写)且是可寻址(addressable)的字段才能通过反射修改。通过
reflect.Value.CanSet()方法可以判断字段是否可以被修改。
6. unsafe 包
-
uintptr 和 unsafe.Pointer 有什么区别?
- uintptr 是一个整数类型,表示具体的内存地址,但它只是一个数字,不是指针。
- unsafe.Pointer 是一个通用的指针,可以用于不同类型的指针之间相互转换,允许直接操作内存。
-
Go 对象如何对齐?
Go 对象按照系统字长对齐,这意味着对象的字段在内存中的排列方式符合系统的内存访问规则,以提高内存访问效率。比如在 64 位系统上,字段通常按 8 字节对齐。字段的偏移量是根据其大小计算的。
-
如何计算对象的地址?
对象的地址可以通过
unsafe.Pointer获取,并结合字段的偏移量来计算字段的地址。unsafe.Offsetof方法可以获取字段的偏移量,结合对象的起始地址进行计算。 -
为什么 unsafe 比反射高效?
unsafe可以 直接操作内存,跳过了 Go 的类型系统和边界检查,因此它比反射更加高效。不过,使用unsafe的代码更加容易出错,需要开发者自己负责内存安全。