深浅色
Go 语言(背诵版)
用法:先自己讲一遍,再看要点。加粗的是必须说出口的关键词。
1. slice 底层结构与扩容
- 结构是指针 + len + cap 三个机器字,slice 传参传的是这个三元组(值传递)。
- 扩容:新容量 < 256 时翻倍;>= 256 时按
newcap += (newcap + 3*256)/4平滑增长到约 1.25 倍,最后按 size class 向上取整。 - 共享陷阱:未扩容时 append 直接改底层数组,
s2 := append(s1[:2], 9)会覆盖s1[2]。隔离用s1[:2:2](三索引限制 cap)或全量拷贝。 - 追问:copy 是深拷贝吗?-> 不是,元素是指针时仍共享对象。
一句话:扩容会换数组所以要接返回值,不扩容会共享数组所以要小心副作用。
2. map 底层
hmap持有B(桶数量 = 2 的 B 次方)、buckets、oldbuckets(扩容中)。- 桶
bmap存 8 组 kv,前置 8 个 tophash(hash 高 8 位)用于快速比对,溢出挂overflow链。 - 扩容:装载因子大于 6.5 翻倍;溢出桶过多时等量扩容整理碎片。扩容是渐进式的,每次读写搬一两个桶。
- 并发读写直接
fatal error,不可 recover。要并发用加锁或sync.Map。 - 追问:为什么遍历顺序随机?-> 故意随机起始桶,防止业务依赖顺序。
一句话:8 个一桶、超了挂链,6.5 就翻倍,渐进搬迁。
3. defer
- 执行顺序 LIFO,时机在 return 之后、真正返回之前。
- 参数在 defer 语句处就求值,不是执行时。
- 配合命名返回值能改最终结果(return 先把值写进返回值变量,再跑 defer)。
- 性能:1.14 起 open-coded defer,非循环场景开销接近 0;循环里的 defer 仍是堆分配。
- 追问:defer 里的 recover 能兜住别的 goroutine 的 panic 吗?-> 不能,panic/recover 是 goroutine 局部的。
4. interface 底层
- 空接口
eface=_type+data;非空接口iface=itab(类型 + 方法表)+data。 - nil interface 不等于 nil:接口是 (类型, 值) 二元组,两部分都为 nil 才是 nil。
var e error = (*MyErr)(nil)不是 nil。 - 方法集:
*T的方法集 = 值方法 + 指针方法;T的方法集只有值方法。所以T不一定实现*T满足的接口。 - 追问:接口转换有开销吗?-> 有,itab 查表(有缓存)+ 可能的装箱逃逸。
5. 值接收者 vs 指针接收者
- 值接收者拷贝一份,改不到原对象;指针接收者能改原对象,也避免大结构体拷贝。
- 可寻址的变量能自动取地址调指针方法,但接口赋值时不会自动取地址。
- 实践:结构体统一指针接收者;小而不可变的值类型(如
time.Time)用值接收者。 - 追问:值类型存进接口是拷贝吗?-> 是,值被拷贝进
data。
6. 逃逸分析
- 编译器决定栈还是堆。常见逃逸:返回局部变量指针、被闭包捕获且生命周期超出、赋给
interface{}、size 过大或动态、送进 channel。 - 栈分配便宜(函数返回自动回收),堆分配增加 GC 压力。
- 排查:
go build -gcflags=-m看逃逸报告。 - 追问:返回局部变量指针为什么安全?-> 编译器把它分配到堆,不是悬垂指针。
7. 错误处理
%w包装保留错误链:fmt.Errorf里用%w包住 err。errors.Is比是不是这个值,errors.As比能不能转成这个类型,都沿链查找。- 包装过的错误不能用
==比较。 panic只用于不可恢复的程序错误(初始化失败、断言不变量),库代码不要 panic;recover只在 defer 里有效。
8. context
- 四种能力:
WithCancel/WithTimeout/WithDeadline/WithValue。 - 规范:作为第一个参数;谁创建谁
defer cancel(),即使超时也要调(否则泄漏定时器)。 Done()返回 channel,Err()给出原因(Canceled / DeadlineExceeded)。- 传值只放请求域元数据(trace id、uid),key 用自定义类型避免冲突;不要传业务参数。
- 追问:能取消正在跑的 SQL 吗?-> 能,用
QueryContext,驱动通过取消连接实现。
9. 泛型
- 语法:类型参数
[T any]+ 约束;~int表示底层类型,可覆盖自定义类型。 - 适用:容器、Map/Filter 工具函数、类型安全的并发原语。
- 不适用:为泛型而泛型;动态场景仍用接口 + 反射。
- 追问:比
interface{}快吗?-> 是,编译期单态化,无装箱和类型断言开销。
10. Go 1.22+ 的变化
- 循环变量每轮独立(以前共享一个,闭包捕获会踩坑)。
net/http路由增强:支持方法匹配、路径参数r.PathValue(id)、通配符。- 标准库新增
slices/maps(1.21)、math/rand/v2(1.22)。 - 追问:老代码里循环启 goroutine 显式传参还要吗?-> 不再必须,但显式更清晰。
11. sync.Pool 与 strings.Builder
sync.Pool:临时对象复用,不保证命中、GC 会清空,适合创建昂贵且生命周期短的对象(buffer、pb 消息)。不能当缓存用。strings.Builder:底层[]byte,WriteString不产生新字符串,String()零拷贝转换;替代s += x(O(n^2))。- 追问:会被 GC 清空为什么还有用?-> 两次 GC 之间的大量复用已经省下了分配和回收。