文章目录
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)。
示例:defer 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)。
示例:
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退出):
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 仍能准确标记所有可达对象,最终实现低延迟的并发垃圾回收。
sync.map和map加锁
sync.Map 和「原生 map + 锁(sync.Mutex/sync.RWMutex)」都能实现线程安全的并发读写,但二者的设计思路、性能特性、适用场景差异极大,核心结论先明确:
| 实现方式 | 是否线程安全 | 核心原理 |
|---|---|---|
sync.Map | ✅ 是 | 双表(read/dirty)分离 + 原子操作 + 轻量锁,读无锁、写加锁,针对“读多写少”优化 |
原生 map + sync.Mutex | ✅ 是 | 全局互斥锁,所有读写操作都加锁,串行执行,无读写分离 |
原生 map + sync.RWMutex | ✅ 是 | 读写锁,读操作加共享锁(多goroutine可同时读)、写操作加排他锁(独占) |
一、核心差异(关键选型依据)
1. 性能特性
-
sync.Map:
读操作(Load)无锁(直接原子读read表),写操作(Store/Delete)仅在更新dirty表时加锁,读多写少场景性能极高;
但写频繁场景下,dirty表向read表迁移(misses阈值触发)会带来额外拷贝开销,性能反而下降。 -
map + Mutex:
所有操作串行(读/写都要等锁),读写都频繁的场景性能最差,但逻辑最简单、无额外开销。 -
map + RWMutex:
读操作可并发(共享锁),写操作独占,读多写少场景性能接近sync.Map,写频繁场景比sync.Map更稳定(无数据拷贝开销)。
2. 适用场景
| 实现方式 | 最佳适用场景 | 避坑场景 |
|---|---|---|
sync.Map | 读极多、写极少(如配置缓存、静态数据查询) | 写频繁(如高频更新的业务数据) |
map + Mutex | 读写都不频繁(如低频配置修改)、逻辑极简场景 | 高并发读/写 |
map + RWMutex | 读多写少(通用场景)、写操作不频繁但需稳定 | 写操作极频繁(排他锁会阻塞所有读) |
3. 功能特性
sync.Map额外提供LoadOrStore(加载或写入)、Range(遍历)等专用方法,且无需初始化(直接用);- 原生
map + 锁需手动初始化map,遍历需先加锁(避免遍历时写操作导致 panic),功能更灵活(可自定义锁粒度)。
二、代码示例(对比实现)
1. sync.Map 实现线程安全
package main
import (
"sync"
"fmt"
)
func main() {
var sm sync.Map
var wg sync.WaitGroup
// 100个goroutine写
for i := 0; i < 100; i++ {
wg.Add(1)
go func(n int) {
defer wg.Done()
sm.Store(n, n*10) // 写操作
}(i)
}
// 1000个goroutine读
for i := 0; i < 1000; i++ {
wg.Add(1)
go func(n int) {
defer wg.Done()
if v, ok := sm.Load(n%100); ok { // 读操作
fmt.Printf("key:%d, val:%d\n", n%100, v.(int))
}
}(i)
}
wg.Wait()
}
2. map + RWMutex 实现线程安全
package main
import (
"sync"
"fmt"
)
func main() {
m := make(map[int]int)
var rwmu sync.RWMutex
var wg sync.WaitGroup
// 100个goroutine写(加排他锁)
for i := 0; i < 100; i++ {
wg.Add(1)
go func(n int) {
defer wg.Done()
rwmu.Lock() // 写加排他锁
m[n] = n*10
rwmu.Unlock()
}(i)
}
// 1000个goroutine读(加共享锁)
for i := 0; i < 1000; i++ {
wg.Add(1)
go func(n int) {
defer wg.Done()
rwmu.RLock() // 读加共享锁
if v, ok := m[n%100]; ok {
fmt.Printf("key:%d, val:%d\n", n%100, v)
}
rwmu.RUnlock()
}(i)
}
wg.Wait()
}
三、总结
- 二者都能实现线程安全,但选型核心看读写比例:
- 读多写极少 → 优先
sync.Map(无锁读,性能最优); - 读多写少(通用场景)→ 优先
map + RWMutex(稳定、灵活); - 读写都少 →
map + Mutex(逻辑简单);
- 读多写极少 → 优先
sync.Map是 Go 官方为“读多写少”场景定制的优化方案,而非替代所有加锁map;- 写频繁场景下,
map + RWMutex比sync.Map更优(无dirty表拷贝开销)。
&spm=1001.2101.3001.5002&articleId=155326239&d=1&t=3&u=a4a2a857cc9249c0ae6f7ccc78798f29)
1077

被折叠的 条评论
为什么被折叠?



