Appearance
Go 面试题
GMP 相关
GMP 协程调度模型
GOMAXPROCS环境变量的作用- G、M、P 分别是啥
- 抢占式并发
- Go 的抢占式调度包括函数调用抢占和异步信号抢占
- Go 1.14 引入异步信号抢占 -> 解决:goroutine 如果执行 CPU 密集任务(比如死循环),没有任何函数调用,也没有阻塞点,那它永远不会主动让出 CPU
- 因此 M 的总数由逻辑处理器数量和阻塞情况共同决定
- 文章的 GMP 配图很经典
goroutine 相关
TODO
锁相关
TODO
sync.Map
- sync.Map 通过读写分离,把高频读放到无锁的 read map,写操作集中在 dirty map,并通过 miss 机制决定是否整体升级,从而在读多写少的场景下显著降低锁竞争
- Go 1.24 实验性的新实现,使用 Hash Trie 结构替代读写分离设计
- Go 1.24 之前 sync.Map 的内部结构:
- read.m (只读 map,可并发访问)
atomic.Pointer[any]-> CAS
- dirty map (需要全局锁保护)
- read.m (只读 map,可并发访问)
- 如果需要强一致性,用 map + RWMutex
场景 是否需要锁 性能 更新已存在的 key CAS,无锁 最佳 读取已存在的 key atomic 读,无锁 最佳 新增 key 全局 mu 锁 较差 两个 goroutine 更新不同已存在的 key 各自 CAS,无竞争 最佳 两个 goroutine 新增不同 key 竞争 mu 锁 竞争
| 场景 | 推荐方案 | 原因 |
|---|---|---|
| 简单缓存 | sync.Map | 专为此优化 |
| 需要类型安全 | RWMutex + map | 编译期检查 |
| 需要 Len/Keys 等操作 | RWMutex + map | sync.Map 不支持 |
| 批量读取 | RWMutex + map | 一次锁保护多次读 |
| 跨 key 的原子操作 | RWMutex + map | sync.Map 无法保证 |
| 写多读少 | Mutex + map | sync.Map 反而更慢 |
| 内存敏感 | Mutex + map | sync.Map 开销大 |
选择决策树:
- 需要类型安全?
- YES -> RWMutex + map
- NO -> 继续
- 需要 Len/Keys/Clear 等操作?
- YES -> RWMutex + map
- NO -> 继续
- 需要跨 key 的原子操作?
- YES -> RWMutex + map
- NO -> 继续
- 读多写少且 key 只写一次?
- YES -> sync.Map
- NO -> RWMutex + map
channel 相关
TODO
数组 / slice 相关
- slice 扩容:小 slice 激进扩,大 slice 保守扩
map 相关
- map 扩容
- map 的遍历顺序是随机的。每次运行 for range,你得到的键值对顺序可能都不同。迭代 map 的正确思路是先借助 slice 排序。
- 如何在并发环境中使用 map
- 读多写少、key 稳定 -> sync.Map
- 读写都频繁、结构简单 -> map + Mutex
- 读远多于写、逻辑清晰 -> map + RWMutex
错误处理相关
- Go 的 error 是返回值。错误是业务的一部分,不是异常流程
context 相关
- context 的作用:值传递、控制 goroutine 的取消与超时
- context 是协作式取消模型,Go 不会强制终止 goroutine,取消只是一个广播信号,goroutine 是否退出取决于是否在合适的位置监听 ctx.Done(),或把 ctx 传给支持取消的阻塞操作
- context 禁止存业务数据
- context 是用来控制 goroutine 生命周期的,Value 只是为了减少参数传递成本,一旦 Value 承载了业务语义,就会制造隐式依赖和状态错乱,所以它不适合当业务状态容器
- 避免通过 context 传业务状态,本质是在逃避建模
context.Value适合什么数据?请求级、只读、全链路共享的小元数据。例如:request-id / trace-id、用户 id、权限快照、灰度标记
defer 相关
1、defer 的时机执行:
- 函数返回时
- panic 后,recover 前 (如果 defer 中有 recover)
- goroutine 退出前
2、defer 在 defer 中的执行,最里面的会先执行
go
func main() {
defer func() {
fmt.Println("outer") // 最后执行
defer func() {
fmt.Println("inner") // 首先执行
}()
}()
fmt.Println("main")
}
// 输出: main, inner, outer泛型相关
1、泛型的 Hello world
go
func Pair[T, U any](first T, second U) (T, U) {
fmt.Println(first, second)
return first, second
}2、约束(any、comparable)不是类型(int、string),而是一个类型集合,限制 T 的范围
3、comparable 是 Go 内置的一个特殊接口,表示支持 == 和 != 操作的所有类型
4、泛型的语法是 func FunctionName[T TypeConstraint](param T) ReturnType,按约束可划分为:
go
// 任何类型
func F1[T any](v T) {}
// 具体类型
func F2[T int](v T) {}
// 联合类型
func F3[T int | string](v T) {}5、any 是空接口 interface{} 的别名
6、类型推断: 编译器根据实参推断类型参数
7、联合类型约束
go
func Sum[T int | int64 | float32 | float64](a, b T) T {
return a + b
}8、自定义类型约束
go
type Number interface {
int | int8 | int16 | int32 | int64 |
uint | uint8 | uint16 | uint32 | uint64 |
float32 | float64
}
func Double[N Number](n N) N {
return n + n
}9、近似约束
go
type Ordered interface {
~int | ~float64
}
func Max[T Ordered](a, b T) T {
if a > b {
return a
}
return b
}
type MyInt01 int
type Dollar01 float64
func main() {
var m1 MyInt01 = 10
var m2 MyInt01 = 20
fmt.Printf("Max(MyInt): %d\n", Max(m1, m2))
var d1, d2 Dollar01 = 100.5, 200.75
fmt.Printf("Max(Dollar): %.2f\n", Max(d1, d2))
}其它
- gopls 和 dlv -> LSP 和调试