golang

Tecy 发布于 2025-04-08 104 次阅读


GC

并发标记-清除(Concurrent Mark-Sweep):Go 的 GC 在大部分阶段与用户程序并发执行(非完全 STW)。
三色标记法:基于 Dijkstra 的三色标记算法(黑、灰、白对象),结合写屏障(Write Barrier)实现并发标记。

  • 白色:初始状态,表示未被访问的对象(可能为垃圾)。
  • 灰色:已被访问,但其引用的子对象还未被扫描。
  • 黑色:已被访问,且其所有子对象也已被扫描(存活对象)。

Tri-Color Marking

(1) 初始化阶段(STW)

  • 所有对象初始化为白色
  • 暂停程序(STW),扫描根对象(栈、全局变量、寄存器等),将其标记为灰色,加入待处理队列。

(2) 并发标记阶段

  • 恢复程序运行,后台启动并发标记线程。
  • 循环处理灰色对象
    • 从队列中取出一个灰色对象。
    • 扫描该对象的所有引用,将其引用的白色子对象标记为灰色,加入队列。
    • 将该对象自身标记为黑色
  • 此过程与用户程序并发执行,需依赖写屏障处理并发修改。

(3) 标记终止阶段(STW)

  • 再次暂停程序,完成剩余的少量灰色对象处理。
  • 确保所有存活对象均被标记为黑色,白色对象即为垃圾。

(4) 并发清除阶段

  • 回收所有白色对象的内存,无需 STW。

Write Barrier

在并发标记过程中,用户程序可能修改对象引用关系,导致两种问题:

  1. 漏标:黑色对象新引用了白色对象,但黑色对象不再被扫描,导致白色对象被错误回收。
  2. 误标:灰色对象断开了对白色对象的引用,但白色对象被错误保留。

Go 通过混合写屏障(Hybrid Barrier)解决这些问题:

写屏障规则:

  • 插入写屏障(Dijkstra Barrier):
    • 当黑色对象 A 引用白色对象 B 时,将 B 标记为灰色。
  • 删除写屏障(Yuasa Barrier):
    • 当灰色对象 A 断开对白色对象 B 的引用时,将 B 标记为灰色。

GMP

  • G(Goroutine):轻量级用户态线程,由 Go 运行时管理,栈大小动态伸缩(初始约 2KB)。
  • M(Machine):操作系统线程(OS Thread),真正执行 CPU 指令的实体。
  • P(Processor):逻辑处理器,负责调度 G 到 M 上运行,维护本地运行队列(Local Queue)。

协作关系:

  • M 必须绑定 P 才能运行 G:每个 M 必须持有一个 P,通过 P 获取 G 并执行。
  • P 的数量默认等于 CPU 核心数:由 GOMAXPROCS 控制(可动态调整)。
  • G 的调度由 P 管理:每个 P 维护一个本地队列(Local Queue),同时存在一个全局队列(Global Queue)。

调度流程:
(1) 创建 Gorotinue:

  • 新创建的 G 优先放入当前 P 的本地队列。
  • 若本地队列已满,则将一部分 G 转移到全局队列。

(2) 获取可运行的 G:

  1. 本地队列优先:P 从本地队列获取 G,依次执行(队列头部出队)。
  2. 全局队列补充:若本地队列为空,从全局队列获取一批 G 到本地队列。
  3. 工作窃取(Work Stealing):若全局队列也为空,随机从其他 P 的本地队列窃取半数 G。

(3) 执行 G:

  • M 绑定 P 后,循环从 P 的本地队列获取 G 并执行。
  • 若 G 发生系统调用(如文件 I/O),Go 运行时将:
    • M 与 P 解绑:P 被释放,以便其他 M 绑定并执行其他 G。
    • M 阻塞等待系统调用完成:完成后,G 重新加入队列,M 尝试获取空闲 P 或进入休眠。

(4) 自旋线程:

  • 若 M 未找到可运行的 G,会进入自旋状态(短暂循环检查队列),避免立即休眠以减少延迟。
  • 自旋的 M 数量有限(通常为活跃 P 的数量),防止 CPU 空转。

Channel

  • 无缓冲 channel:
    • 发送和接收操作必须同时就绪(即发送会阻塞,直到有接收方;接收也会阻塞,直到有发送方)。
    • 天然支持同步(常用于 Goroutine 间同步)。
  • 缓冲 channel:
    • 缓冲区未满时,发送非阻塞;缓冲区为空时,接收阻塞。
    • 适用于生产者和消费者解耦的场景(异步通信)。

底层结构:
Go 的 Channel 由 runtime.hchan 结构体实现(简化后):

type hchan struct {
    qcount   uint           // 当前队列中元素数量
    dataqsiz uint           // 缓冲区容量
    buf      unsafe.Pointer // 环形缓冲区指针
    elemsize uint16         // 元素大小
    closed   uint32         // 是否已关闭
    sendx    uint           // 发送索引(缓冲区的写入位置)
    recvx    uint           // 接收索引(缓冲区的读取位置)
    recvq    waitq          // 接收等待队列(因接收阻塞的 Goroutine)
    sendq    waitq          // 发送等待队列(因发送阻塞的 Goroutine)
    lock     mutex          // 互斥锁(保证原子操作)
}

机制:

  1. 等待队列
    • 当发送或接收操作无法立即完成时,Goroutine 会被加入 sendq 或 recvq 队列,等待被唤醒。
  2. 环形缓冲区
    • 缓冲通道使用环形队列存储数据,通过 sendx 和 recvx 管理读写位置。
  3. 锁机制
    • 所有 Channel 操作均需加锁,保证并发安全。

注意事项:
(1) deadlock:

  • 所有 Goroutine 均阻塞在 Channel 操作上。

(2) panic:

  • 向已关闭的通道发送数据:触发 panic: send on closed channel
  • 重复关闭通道:触发 panic: close of closed channel

(3) nil:

  • 未初始化的 Channel 为零值(nil)。

(4) close:

  • 若不再使用但未关闭,可能导致 Goroutine 阻塞或资源无法释放。

Map

map 是一种基于哈希表实现的键值对数据结构,提供高效的查找、插入和删除操作。

基本用法:

  1. 声明与初始化
// 声明一个 map(此时为 nil,不可直接操作)
// map[K]V, K 必须支持 == 操作(如 slice、map、func 不可作为键)
var m1 map[string]int

// 初始化空 map(推荐使用 make)
m2 := make(map[string]int)
// 初始化空 map 并预分配空间减少扩容次数
m3 := make(map[string]int, 10)

// 初始化并赋值
m4 := map[string]int{
    "apple":  5,
    "banana": 3,
}
  1. 增删改查
// 插入或更新
m3["orange"] = 2

// 读取
count := m3["apple"]     // 存在时返回对应值,不存在返回 V 的默认值
count, ok := m3["pear"]  // ok 表示是否存在

// 删除
delete(m3, "banana")

// 遍历(顺序随机) map 遍历会打乱顺序
for key, value := range m3 {
    fmt.Println(key, value)
}

nil map:未初始化的 map(如 var m map[string]int)为 nil,插入数据会 panic。

底层实现:

type hmap struct {
    count     int        // 当前元素数量
    B         uint8      // 桶数量的对数(桶数为 2^B)
    buckets   unsafe.Pointer // 桶数组指针
    oldbuckets unsafe.Pointer // 扩容时旧桶数组
    flags     uint8
    // ... 其他字段(哈希种子、溢出桶等)
}

哈希桶(Bucket)

  • 每个桶(bmap)存储 8 个键值对
  • 哈希冲突时,通过链表法将溢出桶链接到主桶。

扩容机制

  • 触发条件
    • 装载因子超过 6.5(元素数 / 桶数)。
    • 溢出桶过多(表明哈希冲突严重)。
  • 渐进式扩容:扩容时逐步迁移数据到新桶,避免一次性性能抖动。

Tips

  • 遍历时删除元素:遍历过程中增删元素可能导致未定义行为(如部分元素未被遍历),删除当前遍历的元素不会引发错误
  • 内存管理:键或值为大对象,删除元素后需显式置为 nil(或使用弱引用)。
    type BigStruct struct { Data [1e6]byte }
    m := make(map[int]*BigStruct)
    m[1] = &BigStruct{}
    delete(m, 1) // 删除键,但 BigStruct 对象仍在堆中
    m[1] = nil // 显式释放内存(若需回收)
  • 并发:map 本身不支持并发,可以加锁或使用 sync.Map

Slice && Array

定义与声明:

特性ArraySlice
长度固定性长度固定,声明时指定(如 [3]int)。长度动态可变,无需声明长度(如 []int)。
类型系统长度是类型的一部分([2]int 和 [3]int 不同)。类型与长度无关([]int 是统一类型)。
初始化方式显式指定长度或由编译器推断([...]int{1,2,3})。直接初始化或基于数组/切片创建(如 s := make([]int, 0, 5))。

内存分配与存储:

特性ArraySlice
内存分配连续内存块,通常分配在栈(小数组)或堆。引用类型,包含指针、长度、容量,底层依赖数组,通常在堆。
传递方式值传递:赋值或传参时复制整个数组。引用传递:传递切片头(指针+长度+容量),共享底层数组。
底层共享无共享,每个数组独立。多个切片可共享同一底层数组,修改可能相互影响。