文章目录
-
-
- slice和array的区别
- [slice 的 len、cap、共享、扩容](#slice 的 len、cap、共享、扩容)
- map、slice未初始化的操作结果及panic处理
- recover的使用及defer的优势
- map的有序性、并发安全
- 控制GMP中M的数量
- 控制Goroutine的生命周期
- select的使用
- new和make的区别
- context机制介绍
- [GC回收原理(Go 1.5+ 并发标记清除)](#GC回收原理(Go 1.5+ 并发标记清除))
-
- [阶段1:STW - 初始标记(Initial Mark)](#阶段1:STW - 初始标记(Initial Mark))
- [阶段2:并发标记(Concurrent Mark)](#阶段2:并发标记(Concurrent Mark))
- [阶段3:STW - 终止标记(Mark Termination)](#阶段3:STW - 终止标记(Mark Termination))
- [阶段4:并发清除(Concurrent Sweep)](#阶段4:并发清除(Concurrent Sweep))
- [阶段5:并发清理(Concurrent Scavenge)](#阶段5:并发清理(Concurrent Scavenge))
- 核心优化:
- [Go 写屏障(Write Barrier)](#Go 写屏障(Write Barrier))
-
slice和array的区别
| 维度 | array(数组) | slice(切片) |
|---|---|---|
| 长度特性 | 长度固定(声明时指定,如[5]int) |
长度可变(动态扩容) |
| 类型标识 | 长度是类型的一部分([3]int与[5]int是不同类型) |
长度不是类型的一部分([]int统一类型) |
| 内存结构 | 直接存储元素(值类型) | 存储指针、长度、容量(引用类型,指向底层数组) |
| 传递方式 | 传值(复制整个数组) | 传引用(复制切片头,共享底层数组) |
slice 的 len、cap、共享、扩容
- len :切片当前元素个数;cap:底层数组的容量。
- 共享:多个切片可指向同一底层数组,修改其中一个会影响其他切片。
- 扩容 :当
append后len>cap时,分配新数组并复制数据:- 原容量≤1024:新容量=原容量×2;
- 原容量>1024:新容量=原容量×1.25;
- 若追加元素过多,直接按"原长度+新增长度"扩容。
map、slice未初始化的操作结果及panic处理
- 未初始化的slice :默认是
nil,可执行读操作(返回零值)、len/cap(返回0),但写操作会panic。 - 未初始化的map :默认是
nil,可执行读操作(返回零值)、len(返回0),但写操作会panic。
panic的处理方式:
- 通过
defer + recover()捕获panic,清理资源后恢复程序运行; - 避免panic的核心是:操作前初始化(
slice = make([]int, 0)、map = make(map[string]int))。
recover的使用及defer的优势
-
recover的使用 :
必须在
defer函数中调用,用于捕获当前Goroutine的panic,返回panic的错误信息(无panic时返回nil)。示例:
godefer func() { if err := recover(); err != nil { fmt.Printf("捕获panic:%v", err) } }() -
defer的优势 :
相比"函数最后手动执行操作",
defer能保证操作无论函数正常返回还是panic,都会执行(如资源释放、锁解锁),避免资源泄漏。
map的有序性、并发安全
-
map是无序的 :
Go的map底层是哈希表,遍历是按哈希桶的顺序遍历,与插入顺序无关;即使Go 1.21+对遍历做了随机化优化,仍不保证有序。
-
map不是并发安全的 :
并发读写会直接panic。保证并发安全的方式:
map + sync.RWMutex(读写锁);sync.Map(标准库提供的并发安全map,适用于读多写少)。
控制GMP中M的数量
GMP是Go的调度模型(G:Goroutine,M:操作系统线程,P:处理器),可通过环境变量GOMAXPROCS控制P的数量 (默认等于CPU核心数),但无法直接控制M的数量 ------M由Go运行时根据调度需求动态创建/销毁(默认无上限,极端场景可通过debug.SetMaxThreads限制)。
控制Goroutine的生命周期
-
channel的作用 :
通过关闭channel或向channel发送信号,让Goroutine感知并退出(如
done <- struct{}{})。 -
context的作用 :
传递上下文(如超时、取消信号),控制Goroutine的生命周期,避免Goroutine泄漏(常用
context.WithCancel/WithTimeout)。
select的使用
select用于同时监听多个channel的操作(读/写),语法类似switch,执行任意一个就绪的case;若所有case都未就绪,则阻塞(或执行default)。
示例:
go
select {
case msg := <-ch1:
fmt.Println("收到ch1消息:", msg)
case ch2 <- 10:
fmt.Println("向ch2发送消息")
default:
fmt.Println("无channel就绪")
}
new和make的区别
| 函数 | 适用类型 | 返回值类型 | 作用 |
|---|---|---|---|
new |
任意类型 | 指针(*T) |
分配内存,初始化零值 |
make |
仅slice、map、channel | 类型本身(T) |
分配内存并初始化(如slice的底层数组) |
context机制介绍
context是Go用于传递请求上下文 的标准库(context.Context),核心作用是在Goroutine之间传递取消信号、超时信号、元数据,控制Goroutine的生命周期,避免资源泄漏。
核心特性:
- 可派生 :通过
context.Background()(根上下文)派生子上下文(如WithCancel/WithTimeout/WithValue); - 链式传递 :子Goroutine通过参数接收
context,感知上游的取消/超时事件; - 不可修改 :
Context接口的方法都是只读的,保证上下文的一致性。
常用方法:
context.WithCancel(parent):派生可手动取消的上下文;context.WithTimeout(parent, duration):派生超时自动取消的上下文;context.WithValue(parent, key, val):传递元数据(仅用于请求范围的轻量数据)。
示例(控制Goroutine退出):
go
func worker(ctx context.Context) {
for {
select {
case <-ctx.Done(): // 感知取消/超时
fmt.Println("worker退出:", ctx.Err())
return
default:
fmt.Println("worker运行中...")
time.Sleep(1 * time.Second)
}
}
}
func main() {
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
defer cancel() // 确保资源释放
go worker(ctx)
time.Sleep(5 * time.Second)
}
GC回收原理(Go 1.5+ 并发标记清除)
Go的GC是并发标记清除(CMS) 算法,核心目标是低延迟,分为5个阶段:
阶段1:STW - 初始标记(Initial Mark)
- 行为 :暂停所有Goroutine(STW,Stop The World),标记根对象(如全局变量、Goroutine栈上的对象);
- 特点:耗时极短(通常微秒级)。
阶段2:并发标记(Concurrent Mark)
- 行为:恢复Goroutine运行,后台GC线程并发遍历堆对象,标记所有可达对象;
- 辅助:Goroutine分配内存时会触发"写屏障",记录对象引用的变化,保证标记的准确性。
阶段3:STW - 终止标记(Mark Termination)
- 行为:再次暂停所有Goroutine,处理并发标记期间的竞争条件(如标记遗漏的对象);
- 特点:耗时短,通常微秒级。
阶段4:并发清除(Concurrent Sweep)
- 行为:恢复Goroutine运行,后台GC线程并发清理未标记的对象(释放内存);
- 特点:不影响业务Goroutine,清理后的内存会被标记为空闲,供后续分配。
阶段5:并发清理(Concurrent Scavenge)
- 行为:GC线程将空闲内存归还给操作系统(可选,根据内存使用情况触发);
- 特点:避免进程占用过多物理内存。
核心优化:
- 写屏障:并发标记时记录对象引用变化,保证标记准确性;
- 分代标记(Go 1.19+):优先标记新分配的对象(存活时间短),提升标记效率;
- 低延迟优先:STW阶段耗时控制在毫秒级甚至微秒级,适合高并发服务。
Go 写屏障(Write Barrier)
写屏障是 Go GC 在并发标记阶段保证标记准确性的核心机制------当 Goroutine 并发修改对象引用时,写屏障会记录引用的变化,避免 GC 遗漏可达对象(解决"并发标记时对象引用被修改"的竞争问题)。
核心作用:
在并发标记阶段,Goroutine 仍在运行并修改对象的引用关系,写屏障可以拦截所有"对象引用更新"操作,将新引用的对象标记为"可达",确保 GC 不会错误地回收仍在使用的对象。
Go 写屏障的类型(演进):
Go 经历了两次写屏障的优化,目前默认使用 混合写屏障(Go 1.8+)。
(1)Dijkstra 写屏障(Go 1.5-1.7)
- 规则 :当修改对象
a的字段,将其引用从b改为c时,标记新对象c为可达。 - 问题:需要额外标记栈上的对象,导致 STW 时间较长。
(2)混合写屏障(Go 1.8+,当前默认)
结合了 Dijkstra 写屏障和 Yuasa 写屏障的优点,无需 STW 扫描栈,进一步降低延迟。
- 规则 :
- 当修改对象
a的字段时,标记旧对象b为可达; - 当 Goroutine 创建新对象时,直接标记新对象为可达;
- 当 Goroutine 从栈上读取对象并赋值给堆对象时,标记该栈对象为可达。
- 当修改对象
- 优势:并发标记阶段无需暂停 Goroutine 扫描栈,STW 时间大幅缩短。
写屏障的触发时机:
仅在 GC 并发标记阶段 启用写屏障;GC 其他阶段(如初始标记、清除)不会触发写屏障,避免不必要的性能开销。
总结:
写屏障是 Go GC 实现"并发标记"的关键技术,通过拦截对象引用的修改操作,保证在 Goroutine 并发运行时,GC 仍能准确标记所有可达对象,最终实现低延迟的并发垃圾回收。