知其然,知其所以然
Go 内部原理 2026-10-10

结构体对齐与填充:调整字段顺序能省多少内存

用 Go 1.27.1 实测结构体内存布局:同样 5 个字段,bool/int64/bool/int32/bool 的顺序占 32 字节(填充 17 字节),重排成 int64、int32、bool×3 只占 16 字节;零大小字段放末尾多占 8 字节;100 万个元素的切片省 50% 内存但顺序遍历耗时没有差别;386/arm 上同一个结构体更小(int64 只对齐到 4);fieldalignment 能找出可优化的结构体并把分配器 size class 算进去。

Go 结构体 内存对齐 填充 fieldalignment 真实验证 动画演示
Go 内部原理 2026-10-10

方法集:值接收者与指针接收者,谁能实现接口

用 Go 1.27.1 实测方法集:*T 的方法集包含 T 的方法集,T 的方法集不含指针接收者的方法,所以 T{} 不能赋给要求指针方法的接口;map 元素与函数返回值不可寻址,不能调用指针方法;值接收者的修改作用在副本上;方法值在绑定时拷贝接收者;nil 的 *T 调值接收者方法会 panic。

Go 方法集 接收者 接口 真实验证 动画演示
Go 内部原理 2026-10-10

结构体嵌入:方法提升、遮蔽与接口嵌入

用 Go 1.27.1 实测结构体嵌入:字段与方法被提升,外层定义同名方法会遮蔽(但嵌入类型里调用的仍是自己的方法,没有虚函数分派);嵌入 T 与嵌入 *T 的方法集不同;嵌入接口可以只改写一个方法做装饰器,嵌入的接口为 nil 调用会 panic;json 把嵌入结构体的字段展开;同深度同名字段是编译错误。

Go 结构体嵌入 方法提升 接口 json 真实验证 动画演示
Go 内部原理 2026-10-10

闭包捕获变量:Go 1.22 的 for 循环变量语义改了什么

用 Go 1.27.1 实测 Go 1.22 的循环变量语义变化:只改 go.mod 的 go 指令,同一份代码的闭包、&i 与 goroutine 的结果从 3 3 3 / [30 30 30] 变成 0 1 2 / [10 20 30];//go:build go1.21 可以让单个文件按旧语义编译;-d=loopvar=2 列出受影响的循环;闭包被保存的 100 次迭代在两种语义下分配次数几乎相同(102 与 101);闭包捕获的是变量。

Go 闭包 for 循环变量 go.mod loopvar 真实验证 动画演示
Go 内部原理 2026-10-10

泛型的实现:GC shape stenciling 与字典

在 Go 1.27.1 里用 go tool nm 与汇编验证泛型的实现:按 GC shape 生成机器码(int 与 MyInt 共用 go.shape.int,所有指针类型共用 go.shape.*uint8),每个真实类型实参一个字典,调用点把字典地址作为额外参数传进去;泛型求和与具体求和一样快(约 306 ns),约束里调用方法约慢 3 倍;16 种不同大小的值类型的实例化让二进制多约 35 KB,16 种指针类型只多一个函数。

Go 泛型 GC shape 字典 实现 真实验证 动画演示
Go 内部原理 2026-10-10

类型约束:comparable、联合类型、类型推断的边界,以及 Go 1.27 新增的泛型方法

用 Go 1.27.1 的真实编译与运行验证类型约束:comparable 不接受切片、map、func(接口类型作实参可以编译,比较不可比较的动态类型时 panic);联合类型里自定义类型要加 ~;约束决定能用的运算符;类型推断能处理无类型常量混用与同时推断多个类型参数,不能只靠返回值推断;Go 1.27 新增的泛型方法及其限制(不能实现普通方法的接口、接口方法不能有类型参数)。

Go 泛型 类型约束 类型推断 泛型方法 真实验证 动画演示
Go 内部原理 2026-10-10

range-over-func 迭代器:yield 是怎么被编译成回调的

在 Go 1.27.1 里验证 range-over-func:循环体被编译成 main.main-range1 这样的独立函数并传给迭代器,break 让 yield 返回 false、return 与 defer 的语义、循环体里的调用栈、迭代器违规时运行时会 panic;标准库的 slices、maps、strings 迭代器;迭代器未内联时对 1000 个 int 求和约 2030 ns,比切片循环(302 ns)慢约 6.7 倍,内联后与切片循环一样(304 ns)。

Go 迭代器 range over func iter 编译器 真实验证 动画演示
Go 内部原理 2026-10-10

iter.Pull:把推式迭代器变成拉式,代价是什么

在 Go 1.27.1 里验证 iter.Pull:next 一次推进迭代器一步,取完或 stop 时迭代器的 defer 才执行,迭代器里的 panic 从 next 抛出;底层是 runtime.newcoro 创建的 goroutine,不 stop 就泄漏(1000 个 Pull 使 NumGoroutine 从 1 变成 1001,丢弃引用也不回收);1000 个元素的求和 Pull 比 for range 慢约 21.6 倍。

Go 迭代器 iter.Pull 协程 goroutine 真实验证 动画演示
Go 内部原理 2026-10-10

defer 的成本:open-coded defer 与堆上 defer 差多少

在 Go 1.27.1 里实测 defer 的成本:一个 defer 与直接调用一样快(1.98 ns vs 1.97 ns),8 个以内且不在循环里的 defer 是 open-coded,第 9 个、返回点数乘 defer 数超过 15、或在循环里就退化成 deferprocStack / deferproc(8 个 15.27 ns,9 个 58.47 ns);关闭 open-coded 后 1 个 defer 变成 13.23 ns;panic 后被 recover 接住约 144.53 ns;并对照编译器源码里的限制。

Go defer open-coded defer 性能 真实验证 动画演示
Go 内部原理 2026-10-10

panic、recover 与 runtime.Goexit:栈展开时 defer 的执行顺序

在 Go 1.27.1 里验证 panic 展开时 defer 从内到外执行、recover 只在被 defer 的函数里直接调用才有效(helper 里的 recover 与 defer recover() 都不行);runtime.Goexit 会执行 defer 但 recover 返回 nil;panic(nil) 变成 *runtime.PanicNilError,GODEBUG=panicnil=1 恢复旧行为;panic 链的打印格式;recover 接不住别的 goroutine 的 panic;main 里 Goexit 的 fatal error。

Go panic recover Goexit defer 真实验证 动画演示
Go 内部原理 2026-10-10

错误包装:errors.Is、errors.As 与 %w 怎么沿链查找

在 Go 1.27.1 里验证错误包装:fmt.Errorf 的 %w 形成 *fmt.wrapError 链,errors.Is 沿链比较而 == 不行,%v 不包装;一个 Errorf 里两个 %w 得到 *fmt.wrapErrors;errors.As 取出沿链第一个匹配类型(os.Open 的 *fs.PathError);自定义 Is 方法;不可比较的错误类型不 panic;errors.As 的三种 panic;errors.AsType 泛型版本;errors.Is 每层约 2.8 ns。

Go 错误处理 errors.Is errors.As %w 真实验证 动画演示
Go 内部原理 2026-10-10

errors.Join:多个错误合并后 Is 与 Error 字符串的行为

在 Go 1.27.1 里验证 errors.Join:返回 *errors.joinError,Error() 用换行连接,Is 与 As 遍历所有子错误(As 深度优先取第一个匹配),errors.Unwrap 对它返回 nil,nil 参数被丢弃、全是 nil 时返回 nil;与同一个 Errorf 里的两个 %w 对比;循环里收集错误的写法;Join 本身 1 次分配,Error() 分配 4 到 6 次。

Go 错误处理 errors.Join errors.Is errors.As 真实验证 动画演示
京ICP备2026067502号-1京公网安备11010502063487号