Golang 核心知识点详解文档

Overview

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 调度

 1package main
 2
 3import (
 4    "fmt"
 5    "time"
 6)
 7
 8func worker(id int) {
 9    fmt.Printf("Worker %d is running\n", id)
10    time.Sleep(time.Second)
11}
12
13func main() {
14    for i := 0; i < 5; i++ {
15        go worker(i)
16    }
17    time.Sleep(2 * time.Second) // 等待 Goroutine 完成
18}

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_Semacquireruntime_Semrelease 系统调用,用来挂起和唤醒 Goroutine。当计数器为 0 时,Wait() 方法调用 runtime_Semrelease 来唤醒等待的 Goroutine。Goroutine 在 Wait() 方法时调用 runtime_Semacquire 挂起,直到所有任务完成,计数器归 0,等待中的 Goroutine 被唤醒继续执行。这个机制保证了多个 Goroutine 能够正确同步执行。

关键流程:

  1. Add(n) 增加计数器,表示有 n 个 Goroutine 需要等待完成。
  2. Done() 减少计数器,每次 Goroutine 完成时调用。
  3. Wait() 等待所有 Goroutine 完成,当计数器为 0 时唤醒等待的 Goroutine。

代码示例:sync.WaitGroup

 1package main
 2
 3import (
 4    "fmt"
 5    "sync"
 6)
 7
 8func worker(id int, wg *sync.WaitGroup) {
 9    defer wg.Done()
10    fmt.Printf("Worker %d is done\n", id)
11}
12
13func main() {
14    var wg sync.WaitGroup
15    for i := 0; i < 5; i++ {
16        wg.Add(1)
17        go worker(i, &wg)
18    }
19    wg.Wait() // 等待所有 Goroutine 结束
20}

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 有两种模式:

  1. 正常模式:短暂的锁竞争会通过自旋快速获取锁。
  2. 饥饿模式:当有 Goroutine 长时间等待锁时,进入饥饿模式,此时锁优先分配给等待最久的 Goroutine,避免长时间的锁等待。

这种设计避免了频繁的上下文切换,既保证了锁的高效性,又防止了饥饿问题。

另外:runtime_Semacquireruntime_Semrelease 是 Go 语言运行时的系统调用,用于挂起和唤醒 Goroutine。

死锁 是指在并发程序中,两个或多个 Goroutine(或线程)因相互等待对方释放资源(如锁、channel),导致所有相关的 Goroutine 都无法继续执行,从而进入无限等待的状态。简而言之,死锁是由于相互依赖的资源竞争引起的,程序陷入无法进展的局面。

代码示例:sync.Mutex

 1package main
 2
 3import (
 4    "fmt"
 5    "sync"
 6)
 7
 8var counter int
 9var mu sync.Mutex
10
11func increment(wg *sync.WaitGroup) {
12    mu.Lock()
13    counter++
14    mu.Unlock()
15    wg.Done()
16}
17
18func main() {
19    var wg sync.WaitGroup
20    for i := 0; i < 5; i++ {
21        wg.Add(1)
22        go increment(&wg)
23    }
24    wg.Wait()
25    fmt.Println("Final counter:", counter)
26}

2.3 sync.Cond

sync.Cond 是条件变量,用于 Goroutine 之间的信号同步。通过 Wait 让 Goroutine 等待,通过 SignalBroadcast 唤醒等待的 Goroutine。Signal 会唤醒一个 Goroutine,Broadcast 会唤醒所有 Goroutine。

这里是 sync.Cond 中的 signalbroadcast 操作与锁的简明流程说明:

流程说明

  1. 持有锁:在调用 Wait() 前,消费者 Goroutine 必须持有锁。调用 Wait() 后,Goroutine 会释放锁,进入等待状态,直到被 signalbroadcast 唤醒。

  2. 等待时释放锁:当消费者调用 cond.Wait() 时,自动释放锁,以便让其他 Goroutine 可以修改共享资源(例如队列)。消费者会进入等待状态。

  3. 唤醒时重新获取锁:当生产者调用 signal()broadcast() 唤醒消费者时,消费者 Goroutine 会尝试重新获取锁,获取锁成功后,才会继续检查条件并执行后续逻辑。

  4. 生产者持有锁:生产者在修改共享资源(如向队列中添加数据)时,必须持有锁,确保数据的并发安全。

  5. 释放锁:生产者在完成对共享资源的修改后,调用 signal()broadcast(),唤醒等待中的消费者,随后释放锁

代码示例:sync.Cond

 1package main
 2
 3import (
 4    "fmt"
 5    "sync"
 6    "time"
 7)
 8
 9type Queue struct {
10    data []int
11    cond *sync.Cond
12}
13
14func (q *Queue) Enqueue(n int) {
15    q.cond.L.Lock()
16    q.data = append(q.data, n)
17    fmt.Println("Enqueued:", n)
18    q.cond.Signal() // 唤醒等待的消费者
19    q.cond.L.Unlock()
20}
21
22func (q *Queue) Dequeue() {
23    q.cond.L.Lock()
24    for len(q.data) == 0 {
25        fmt.Println("Queue is empty, waiting for data...")
26        q.cond.Wait() // 等待数据被添加
27    }
28    val := q.data[0]
29    q.data = q.data[1:]
30    fmt.Println("Dequeued:", val)
31    q.cond.L.Unlock()
32}
33
34func main() {
35    q := &Queue{cond: sync.NewCond(&sync.Mutex{})}
36
37    go func() {
38        for i := 0; i < 5; i++ {
39            q.Enqueue(i)
40            time.Sleep(1 * time.Second)
41        }
42    }()
43
44    go func() {
45        for i := 0; i < 5; i++ {
46            q.Dequeue()
47            time.Sleep(2 * time.Second)
48        }
49    }()
50
51    time.Sleep(10 * time.Second)
52}

2.4 sync.Once

sync.Once 用于确保某个操作只会执行一次,常用于初始化操作。

总结流程

  1. Do(f) 方法首先通过原子操作检查 done 值。
  2. 如果 done == 1,操作已经执行过,直接返回。
  3. 如果 done == 0,进入慢路径 doSlow(f)。
  4. 在 doSlow(f) 中,首先获取互斥锁,保证操作的并发安全。
  5. 再次检查 done,确保操作只会执行一次。
  6. 执行传入的函数 f(),然后将 done 设置为 1,表示操作已经完成。

代码示例:sync.Once

 1package main
 2
 3import (
 4    "fmt"
 5    "sync"
 6)
 7
 8var once sync.Once
 9
10func initialize() {
11    fmt.Println("Initialized")
12}
13
14func main() {
15    for i := 0; i < 3; i++ {
16        once.Do(initialize) // 只会执行一次
17    }
18}

2.5 sync.Pool

sync.Pool 是对象池,用于缓存和重用临时对象,减少 GC 负担。

sync.Pool 是 Go 中的对象池,用于缓存和复用临时对象,从而减少频繁的内存分配和垃圾回收开销。它为每个 P(逻辑处理器)维护一个本地缓存,确保对象能够就近复用,减少跨线程的争用。当池中没有可用对象时,会通过 New 方法生成新的对象,并将其缓存。GC(垃圾回收)时,池中的对象会被清空,因此适用于短生命周期的临时对象。

sync.Pool 不是线程池。它只用于管理和缓存对象,减少内存分配压力,而线程池是用来管理 Goroutine 或线程的执行。sync.Pool 解决的是对象的高效复用问题,而线程池则负责任务的调度和执行。

代码示例:sync.Pool

 1package main
 2
 3import (
 4    "fmt"
 5    "sync"
 6)
 7
 8var pool = sync.Pool{
 9    New: func() interface{} {
10        return new(int)
11    },
12}
13
14func main() {
15    v := pool.Get().(*int)
16    *v = 100
17    fmt.Println("Value:", *v)
18    pool.Put(v)
19}

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 取消

 1package main
 2
 3import (
 4    "context"
 5    "fmt"
 6    "time"
 7)
 8
 9func task(ctx context.Context) {
10    select {
11    case <-time.After(2 * time.Second):
12        fmt.Println("Task completed")
13    case <-ctx.Done():
14        fmt.Println("Task cancelled:", ctx.Err())
15    }
16}
17
18func main() {
19    ctx, cancel := context.WithCancel(context.Background())
20    go task(ctx)
21
22    time.Sleep(1 * time.Second)
23    cancel() // 取消操作
24    time.Sleep(1 * time.Second)
25}

4. Go 反射

4.1 反射简介

反射是 Go 语言在运行时动态检查和操作类型和变量的机制。通过 reflect 包,开发者可以获取变量的类型、值并进行修改。

反射是 Go 语言在运行时提供的一种机制,允许开发者动态检查变量的类型和值,并在运行时对其进行操作。这通过 reflect 包实现,提供了 reflect.Typereflect.Value 两个关键类型,分别用于获取变量的类型信息和值信息。反射在高级框架(如依赖注入、ORM)中非常常用,因为它允许在编译时无法确定类型的情况下操作对象。

由于反射需要在运行时操作对象,Go 会将涉及到的变量分配到堆上,这导致了 内存逃逸。例如,当使用 reflect.ValueOf(&x) 传递变量时,Go 会将变量 x 从栈中逃逸到堆中,以便在运行时通过指针引用和修改。反射带来的内存逃逸会增加垃圾回收的负担,因此在性能敏感的场景中,滥用反射可能导致效率降低。

反射与断言

断言 是 Go 语言中用于将接口类型变量转换为具体类型的机制。接口可以保存任意类型的值,断言允许从接口中提取实际的底层类型。如果断言成功,它返回具体类型的值;如果失败,可以通过安全断言避免程序崩溃。断言通常用于编译时知道具体类型的情况下,适合处理固定类型的接口。

反射 是 Go 语言在运行时动态检查和操作变量类型和值的机制。通过 reflect 包,开发者可以获取变量的类型 (reflect.Type) 和值 (reflect.Value),并进行操作,如修改值或调用方法。反射在运行时处理不确定类型的变量,适用于框架开发等复杂场景。由于反射是在运行时执行的,它可能引发性能开销和内存逃逸。

区别

  • 断言 只适用于接口类型,编译时使用,性能高,通常不会引发内存逃逸。
  • 反射 可以处理任意类型,运行时使用,灵活性强,但性能较低,通常会引发内存逃逸。

4.2 反射的基本使用

  • reflect.TypeOf():获取变量的类型。
  • reflect.ValueOf():获取变量的值。
  • reflect.Set():通过反射修改变量的值。

代码示例:反射修改结构体字段

 1package main
 2
 3import (
 4    "fmt"
 5    "reflect"
 6)
 7
 8type Person struct {
 9    Name string
10    Age  int
11}
12
13func main() {
14    p := &Person{"John", 30}
15    v := reflect.ValueOf(p).Elem()
16
17    v.FieldByName("Name").SetString("Jane") // 修改字段
18    fmt.Println(p)
19}

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

 1package main
 2
 3import (
 4    "fmt"
 5    "unsafe"
 6)
 7
 8func main() {
 9    var x int = 10
10    ptr := unsafe.Pointer(&x)
11    fmt.Printf("Value at pointer: %d\n", *(*int)(ptr))
12}

5.3 Go 对象内存对齐

Go 中对象的内存分配遵循系统字长对齐规则,保证数据访问的高效性。不同类型的数据会根据其大小和对齐要求进行排列,避免内存访问开销。

内存对齐: 内存对齐是指编译器为了提高 CPU 的内存访问效率,会将数据按特定字节边界存放。例如,32 位系统中,一个 4 字节的整数通常存放在 4 字节对齐的内存地址上,这样 CPU 读取时可以一次性读取 4 字节数据,而无需拆分多次访问。

  • 对齐的好处:提高 CPU 访问速度,减少不必要的内存访问。
  • unsafe 与对齐:使用 unsafe 包时,你可以手动操作内存地址,可能会忽略对齐要求,这在某些平台上可能导致性能下降甚至程序错误。因此,在使用 unsafe 包时,开发者需要注意内存对齐问题,以避免潜在问题。

简而言之,unsafe 包给了开发者更大的控制权,但也带来了更多的责任,特别是在处理内存对齐和跨平台兼容性问题时需要谨慎。

高频面试题总结

1. GMP 调度模型

  1. 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 可以并发执行。

  2. Work Stealing 是什么?

    工作窃取(Work Stealing)是一种调度策略,用于在多处理器环境下均衡任务负载。在 Go 语言的 GMP 模型中,如果一个 P 没有可执行的 Goroutine,它会尝试 从其他 P 的队列中窃取任务执行。工作窃取的目的是为了确保多核 CPU 的资源能够被充分利用,减少 CPU 核心空闲时间,同时保证任务执行的并行性。在实践中,P 通过从其他 P 的任务队列末尾窃取 Goroutine,避免全局队列的竞争,提升并发效率。

  3. Hand-off(任务交接)是怎么做的?

    在 Go 调度模型中,当一个 Goroutine 被阻塞(例如等待 I/O 操作、锁等)时,P 将该 Goroutine 从执行队列中移除(M不阻塞了再恢复),并尝试调度其他 Goroutine。如果 P 没有可用的 Goroutine,它会将执行权交还给全局调度器或窃取其他 P 的 Goroutine。这种任务交接机制保证了 Goroutine 的高效执行,避免长时间的任务阻塞影响整个系统的运行。同时,P 还可能将任务移交给其他 M 绑定的 P 来执行,以提升系统的并发性能。

  4. 抢占式调度是如何工作的?

    抢占式调度是指调度器可以在 Goroutine 长时间运行时,强制中断它的执行,给其他 Goroutine 机会执行。Go 语言在 1.14 版本引入了抢占式调度机制,解决了 Goroutine 长时间占用 CPU 的问题。通过这种方式,调度器可以定期检查 Goroutine 的执行时间,如果某个 Goroutine 占用 CPU 过长,它会被挂起,允许其他等待的 Goroutine 执行。抢占式调度的引入确保了 Goroutine 不会独占 CPU 资源,改善了系统的响应能力。

  5. 正常模式和饥饿模式的区别?

    正常模式是 Go 调度器默认的锁争夺模式,在这种模式下,新来的 Goroutine 也可以竞争锁资源,获得执行机会。饥饿模式是在锁被某些 Goroutine 长时间持有的情况下,为避免其他 Goroutine 永远无法获得锁而设计的。当锁进入饥饿模式时,锁优先分配给等待最久的 Goroutine,而不是新来的 Goroutine。这确保了在极端情况下 Goroutine 不会被饿死。Go 的 Mutex 锁会根据锁竞争的情况在这两种模式之间切换,以平衡性能和公平性。

2. sync 包详解

  1. 为什么 Go 的锁是非公平锁?

    Go 语言的 Mutex 是非公平锁。非公平锁意味着 Goroutine 获取锁的顺序 并不完全按照 Goroutine 请求锁的顺序来分配。设计非公平锁的目的是为了提高系统性能,减少线程间的竞争开销。公平锁会严格按照 Goroutine 请求锁的顺序分配锁,这会导致更多的上下文切换,从而增加 CPU 的调度负担,降低系统的整体吞吐量。非公平锁通过减少 Goroutine 之间的等待和竞争,避免频繁的锁切换,从而提高并发性能。

  2. Mutex 的两种模式是什么?

    • 正常模式:当锁的竞争不激烈时,Go 使用正常模式,在这种模式下,新来的 Goroutine 有可能比正在等待锁的 Goroutine 先获取锁,从而减少锁竞争。

    • 饥饿模式:当锁的竞争激烈且某个 Goroutine 长时间无法获得锁时,Mutex 会进入饥饿模式。在饥饿模式下,锁优先分配给等待最久的 Goroutine,而不会让新来的 Goroutine 直接获取锁。

  3. Mutex 为什么需要这两种模式?

    Mutex 设计为非公平锁,以提升锁的获取速度,减少 Goroutine 之间的竞争和上下文切换的开销。当锁竞争不激烈时,正常模式可以确保 Goroutine 快速获取锁,提高性能;当锁竞争激烈时,饥饿模式确保等待时间长的 Goroutine 可以优先获得锁,防止 Goroutine 被长期饿死。这两种模式的设计折中考虑了系统的性能和公平性需求。

  4. 等待队列中的 Goroutine 能直接拿到锁吗?

    是的,新来的 Goroutine 有可能在等待队列中的 Goroutine 获取锁之前抢先获得锁。这是因为 Go 的 Mutex 是非公平锁,新来的 Goroutine 有机会直接参与锁的竞争,而不用等待队列中的所有 Goroutine 先获取锁。

  5. Mutex 是可重入锁吗?

    Go 的 Mutex 不是可重入锁。可重入锁允许同一个 Goroutine 多次获取同一把锁,而不发生死锁。而 Go 的 Mutex 如果同一个 Goroutine 在持有锁的情况下再次请求锁,则会发生死锁。因此,Go 的 Mutex 不适合在递归函数中使用。

  6. RWMutex 和 Mutex 有什么区别?

    RWMutex 是读写锁,允许多个 Goroutine 同时读取,但写操作是互斥的;Mutex 是普通的互斥锁,不允许并发读写。RWMutex 更适合读操作频繁、写操作较少的场景,它可以提高读操作的并发度,提升系统性能。如果写操作较多,Mutex 的性能会更好,因为 RWMutex 在写操作时仍然需要独占锁。

  7. Mutex 如何挂起和唤醒 Goroutine?

    当 Goroutine 试图获取已经被其他 Goroutine 持有的 Mutex 时,它会被挂起。Go 语言的挂起和唤醒机制是基于信号量(semaphore)实现的。具体来说,runtime_Semacquire 会将 Goroutine 挂起等待锁,runtime_Semrelease 会唤醒等待锁的 Goroutine,使其继续执行。

  8. sync.Pool 和 GC 的关系是什么?

    sync.Pool 是 Go 语言中的对象池,常用于缓存短生命周期的对象,以减少频繁的内存分配和回收sync.Pool 在 GC(垃圾回收)时会清理 local 缓存,并将未被使用的对象转移到 victim 区域。在下次对象请求时,P 可以从 victim 区域获取对象,减少内存分配的开销。

  9. 什么时候 P 会用 victim 数据?

    当 P 的 本地 local 池中没有可用对象时,P 会尝试从 victim 区域获取对象。如果 victim 区域中也没有可用对象,则需要从全局池中分配新的对象。

  10. 为什么不设计全局共享队列?

    全局共享队列会导致锁的激烈竞争,从而降低并发性能。而 Go 通过 poolChain 和 P 本地缓存来减少锁的竞争,提升对象池的并发性能。poolChain 结合 Go 的 P 模型,使得 每个 P 都有自己的本地缓存,减少了全局锁的争用。

  11. sync.Pool 的优缺点是什么?

    • 优点sync.Pool 提供了高效的对象缓存机制,减少了频繁的内存分配和垃圾回收压力,提高了程序的性能。
    • 缺点sync.Pool 的内存使用量难以控制,GC 后即使从 victim 恢复对象,性能仍略有下降。同时,大量缓存的对象可能导致程序使用 更多的内存
  12. sync.Once 的作用是什么?

    sync.Once 保证某个操作在整个程序生命周期中只执行一次。典型的使用场景包括单例模式的初始化函数。sync.Once 通过内部的状态变量和锁机制,确保操作只执行一次,并且即使多个 Goroutine 同时调用,也不会重复执行。先原子操作检查状态,再加锁,再检查状态,再执行操作,最后解锁。

  13. WaitGroup 的使用场景是什么?

    WaitGroup 用于等待一组 Goroutine 完成。它提供了一种 计数器机制,调用 Add 方法增加计数器,调用 Done 方法减少计数器,调用 Wait 方法阻塞,直到计数器归零。这非常适合用来协调并发任务的执行顺序,确保主 Goroutine 等待所有子 Goroutine 执行完毕后再继续执行。

3. context 包

  1. context.Context 的使用场景是什么?

    context.Context 用于在 Goroutine 之间传递取消信号、超时控制或键值对数据。常见的使用场景包括:传递 HTTP 请求的上下文,控制子任务的取消,超时处理等。它是管理并发任务生命周期的核心工具,特别是在复杂的 Goroutine 链条中。

  2. context.Context 的原理是什么?

    context.Context 的实现是 树状结构。父 context 可以通过 context.WithCancelcontext.WithTimeoutcontext.WithValue 创建子 context,当父 context 被取消或超时时,所有子 context 都会自动取消(子context共享父context的Done())。

  3. 父子 Context 的关系是什么?

    每个子 context 都由父 context 创建,并继承了父 context 的取消信号和超时信息。当父 context 被取消时,所有子 context 会接收到同样的取消信号,并终止相应的操作。

  4. valueCtx 和 timeCtx 的原理是什么?

    • valueCtx:用于在 Goroutine 之间传递键值对数据。它允许在多个 Goroutine 之间共享配置信息或其他上下文信息。

    • timeCtx:用于设置任务的超时时间。通过 context.WithTimeoutcontext.WithDeadline 创建 timeCtx,一旦超时,任务会被取消。

4. sync.Cond

  1. sync.Cond 的作用是什么?

    sync.Cond 是 Go 中用于 Goroutine 之间同步的机制,通常用于协调多个 Goroutine 的执行顺序。sync.Cond 提供了 WaitSignalBroadcast 方法,用于在 Goroutine 之间传递信号,协调它们的执行。Wait 方法阻塞当前 Goroutine,直到收到信号为止,Signal 唤醒一个等待中的 Goroutine,而 Broadcast 则唤醒所有等待中的 Goroutine。常用于生产者消费者模型

5. Go 反射

  1. 反射是什么?

    反射是 Go 语言提供的用于在 运行时动态检查、获取或修改类型和对象的功能。通过反射,程序可以在不知道类型的情况下操作对象。

  2. 反射的使用场景?

    反射广泛应用于框架、库、工具等场景,尤其是那些需要处理不确定类型的数据场景,比如序列化库、ORM 框架等。

  3. 反射能修改方法吗?

    Go 的反射机制 不允许修改方法。这是因为方法属于不可变的函数,Go 运行时并没有暴露修改它们的能力。

  4. 什么样的字段能被反射修改?

    只有导出的(首字母大写)且是可寻址(addressable)的字段才能通过反射修改。通过 reflect.Value.CanSet() 方法可以判断字段是否可以被修改。

6. unsafe 包

  1. uintptr 和 unsafe.Pointer 有什么区别?

    • uintptr 是一个整数类型,表示具体的内存地址,但它只是一个数字,不是指针。
    • unsafe.Pointer 是一个通用的指针,可以用于不同类型的指针之间相互转换,允许直接操作内存。
  2. Go 对象如何对齐?

    Go 对象按照系统字长对齐,这意味着对象的字段在内存中的排列方式符合系统的内存访问规则,以提高内存访问效率。比如在 64 位系统上,字段通常按 8 字节对齐。字段的偏移量是根据其大小计算的。

  3. 如何计算对象的地址?

    对象的地址可以通过 unsafe.Pointer 获取,并结合字段的偏移量来计算字段的地址。unsafe.Offsetof 方法可以获取字段的偏移量,结合对象的起始地址进行计算。

  4. 为什么 unsafe 比反射高效?

    unsafe 可以 直接操作内存,跳过了 Go 的类型系统和边界检查,因此它比反射更加高效。不过,使用 unsafe 的代码更加容易出错,需要开发者自己负责内存安全。