Week 1 · Chapter 1 · Go语言基础
复习难度:⭐⭐⭐ | 预计时长:2.5小时 | 重点程度:高
1.1 GMP 调度模型
原理
Go 的运行时(runtime)采用 GMP 模型:
- G( Goroutine):轻量级用户态线程,栈初始 2KB,可动态扩容至 1GB
- M(Machine):内核线程,与 P 绑定后执行 G,最大并发数受
GOMAXPROCS控制 - P(Processor):调度器上下文,持有一个 G 队列,负责 G 的调度
┌─────────────────────────────────────┐
│ M │
│ ┌─────────────────────────────┐ │
│ │ P (runnable G queue) │ │
│ │ [G1] [G2] [G3] [G4] ... │ │
│ └─────────────────────────────┘ │
└─────────────────────────────────────┘
调度流程: 1. M 从 P 的本地队列取 G;若本地队列空,从全局队列或偷其他 P 的队列 2. G 阻塞在 channel/sync 操作时,M 解绑 P 去执行其他 G 3. G 重新就绪后,放入某个 P 的队列等待调度
常见面试题
Q1:GPM 模型中 P 的数量默认是多少?可以动态调整吗?
答:默认等于 CPU 核数(
runtime.GOMAXPROCS初始值)。可以动态调整,runtime.GOMAXPROCS(n)可以在运行时设置,但不会改变 M 的总数。
Q2:Go 1.14 引入的抢占式调度解决了什么问题?
答:解决了"死循环 G 占满 P 导致其他 G 无法运行"的问题。通过编译器插入抢占标记 + 信号抢占,使长耗时 G 能被强制挂起。
1.2 Channel
原理
Channel 是 Go 实现 CSP 模型的核心,提供阻塞式的 goroutine 通信。
// 无缓冲 channel:发送和接收同步阻塞
ch := make(chan int)
// 有缓冲 channel:非满不满时非阻塞
ch := make(chan int, 10)
// 单向 channel(用于函数签名)
func producer(ch chan<- int) // 只能发
func consumer(ch <-chan int) // 只能收
底层结构(简化):
type hchan struct {
qcount uint // 元素数量
dataqsiz uint // 环形队列大小
buf unsafe.Pointer // 环形队列指针
elemsize uint16 // 元素大小
closed uint32 // 是否关闭
recvq waitq // 阻塞接收的 G 队列(sudog)
sendq waitq // 阻塞发送的 G 队列(sudog)
}
关键机制:
- 发送:buf 未满则直接入队;buf 满则当前 G 置入 sendq 阻塞,等待 receiver 唤醒
- 接收:buf 有元素则直接出队;buf 空则当前 G 置入 recvq 阻塞,等待 sender 唤醒
- 关闭:close(ch) 后,接收方收到零值 + ok=false
常见面试题
Q1:channel 关闭后还能读取吗?还能写入吗?
答:关闭后可以继续读取,返回零值+false;写入会 panic。因此关闭操作应在 sender 端,且确保不再写入。
Q2:如何判断 channel 是否已关闭且无数据?
答:
go v, ok := <-ch if !ok { // channel 已关闭 }
Q3:用 channel 实现一个互斥锁。
答:
go func NewMutex() chan struct{} { ch := make(chan struct{}, 1) ch <- struct{}{} // 持有锁 return ch } // Lock: <-ch (阻塞获取) // Unlock: ch <- struct{}{} (释放)
1.3 Map
原理
Go 的 map 是哈希表,实现为数组+桶(bucket)+链表/红黑树(Go 1.18 起引入)。
type hmap struct {
count int // 元素数量
B uint8 // 桶数量的 log2(如 B=3 则有 8 个桶)
buckets unsafe.Pointer // 桶数组
oldbuckets unsafe.Pointer // 扩容时保存旧桶
extra *mapextra // overflow 桶
}
type bmap struct {
tophash [8]uint8 // 8 个槽位的高 8 位哈希值
keys [8]keytype
values [8]valuetype
overflow *bmap // overflow 桶指针(哈希冲突时连接)
}
核心特点:
- 哈希冲突:用开放寻址法的变体(tophash + 链式 overflow)
- 渐进式扩容:负载因子 > 6.5 时触发 2 倍扩容;等量扩容时重建 bucket 减少 overflow
- 不是线程安全:多个 goroutine 并发读写会 panic;需用 sync.RWMutex 或 sync.Map
常见面试题
Q1:map 的扩容因子为什么是 6.5?
答:6.5 是通过大量实验得出的平衡值。(loadFactor > 6.5 时平均每桶超过 6.5 个元素,链表会变长,查找性能下降)
Q2:遍历 map 的顺序是固定的吗?
答:不固定,每次遍历顺序不同。因为遍历的是桶的顺序,而哈希到桶的顺序由哈希函数决定,map 故意对遍历顺序做了随机化防止用户依赖顺序。
Q3:sync.Map 的适用场景?
答:适合读多写少的场景(读不加锁)。内部用两个 map(read 和 dirty)实现,写操作多的场景反而更慢因为每次写都要加锁。
1.4 Interface
原理
Go 的接口是隐式实现(无需 implements 关键字),核心是接口表中方法指针的调度。
// 空接口:所有类型都实现
var i interface{} = "hello"
// 非空接口:需要实现所有方法
type Writer interface {
Write([]byte) (int, error)
}
iface 底层结构:
type iface struct {
tab *itab // 接口类型 + 实际类型的调度表
data unsafe.Pointer // 指向实际值的指针
}
type itab struct {
inter *interfacetype // 接口类型元数据
_type *_type // 实际类型
fun [1]uintptr // 方法指针数组(可变长)
}
nil receiver 问题:
var p *Person // p == nil
var i PersonInterface = p // i != nil(iface 的 data 为 nil,但 tab 不为 nil)
// 调用 i.Method() 不会 panic,但行为取决于方法是否检查 receiver == nil
常见面试题
Q1:interface 何时会触发 GC?
答:当接口持有的对象不再被任何变量引用时,会被 GC 回收。空接口
interface{}持有对象时,对象的引用计数至少为 1。
Q2:如何判断 interface{} 是 nil?
答:
go if v, ok := i.(type); ok { // i 不是 nil }直接i == nil判断的是 iface 的两个字段都为 nil 的情况,但赋值后data可能为 nil 而tab不为 nil。
1.5 内存模型
原理
Go 内存模型定义了 goroutine 之间 happens-before 关系(与 Java JMM 类似但更简洁)。
核心规则:
1. 初始化:包级变量的初始化 var x = a → x = b 满足 happens-before
2. goroutine 创建:go f() → f() 的执行满足 happens-before
3. channel 通信:send → 对应 receive 满足 happens-before
4. sync 包:sync.Mutex / sync.RWMutex 的 Lock/Unlock、Once 的 do 等
内存分配策略: - 栈(stack):每个 goroutine 独立栈,初始 2KB,最大 1GB;函数返回后栈帧自动释放 - 堆(heap):GC 管理,逃逸分析决定分配在堆还是栈
常见面试题
Q1:什么是 happens-before?举一个例子。
答:如果操作 A happens-before 操作 B,那么 A 的结果对 B 可见。例子:goroutine A 向 channel 发送数据,B 从 channel 接收 → A 的 send happens-before B 的 receive → B 一定能收到 A 发送的数据。
Q2:为什么不要在 defer 中做重型计算?
答:defer 在函数返回前(panic 流程也会)按 LIFO 顺序执行。如果 defer 中有锁释放、文件关闭等资源清理操作没问题,但如果做复杂计算会影响性能。Go 1.14 后 defer 性能已大幅提升(基于栈的延迟帧)。
📝 本章高频问题速记
| 问题 | 核心答案一句话 |
|---|---|
| GMP 中 P 默认数量 | CPU 核数 |
| channel 关闭后能否写入 | 不能,写入 panic |
| map 遍历顺序是否固定 | 不固定,故意随机化 |
| sync.Map 适用场景 | 读多写少 |
| interface nil 判断 | i == nil 要求 tab 和 data 都为 nil |
| happens-before 定义 | A 结果对 B 可见,且 A 在 B 之前执行 |