Skip to content

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 (需要全局锁保护)
  • 如果需要强一致性,用 map + RWMutex
    场景是否需要锁性能
    更新已存在的 keyCAS,无锁最佳
    读取已存在的 keyatomic 读,无锁最佳
    新增 key全局 mu 锁较差
    两个 goroutine 更新不同已存在的 key各自 CAS,无竞争最佳
    两个 goroutine 新增不同 key竞争 mu 锁竞争
场景推荐方案原因
简单缓存sync.Map专为此优化
需要类型安全RWMutex + map编译期检查
需要 Len/Keys 等操作RWMutex + mapsync.Map 不支持
批量读取RWMutex + map一次锁保护多次读
跨 key 的原子操作RWMutex + mapsync.Map 无法保证
写多读少Mutex + mapsync.Map 反而更慢
内存敏感Mutex + mapsync.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 和调试