内存分配与逃逸分析
需要预先了解: 数组与切片 结构体与指针 方法 内存分配与逃逸分析 一个变量分配在栈上还是堆上,由编译器通过**逃逸分析(escape analysis)**决定。这个决定直接影响性能:栈分配只是移动一下栈指针,几乎零成本;堆分配要走分配器、还要被垃圾回收扫描。这一篇讲清楚逃逸分析怎么看、哪些写法会让变量逃逸,以及内存对齐和分配大小分级这两个容易忽略的细节。 栈与堆 维度 栈 堆 分配成本 极低(移动栈指针) 较高(分配器 + 可能的锁) 回收方式 函数返回时自动释放 由垃圾回收器回收 生命周期 与函数调用绑定 由可达性决定 访问速度 更快(缓存友好) 稍慢 Go 的 goroutine 栈是动态增长的:初始约 8KB,不够时自动扩容(最大到 1GB,64 位平台)。栈上变量的地址在扩容时会被修正,所以程序里取到的指针在逻辑上始终有效。 Go 的分配器 Go 运行时自带内存分配器,按对象大小走不同的路径: 小对象(≤ 32KB): mcache(每个 P 私有,无锁) | mcent...
垃圾回收
需要预先了解: 内存分配与逃逸分析 垃圾回收 Go 的垃圾回收器(GC,Garbage Collector)负责回收不再被引用的堆对象。它的设计目标很明确:把停顿时间压到亚毫秒级,让服务端程序不必为 GC 停顿做特殊设计。这一篇讲清楚三色标记、写屏障、触发时机和调优手段。 为什么需要 GC 堆上的内存生命周期无法在编译期确定,只能运行时判断。手动管理会带来两类问题: 忘记释放:内存越用越多,最终耗尽; 释放过早:对象还在被引用就被回收,后续访问到已失效的内存。 垃圾回收器把"什么时候释放"这件事交给运行时:它周期性地从根对象(栈上的变量、全局变量、寄存器)出发,找出所有还能访问到的对象,其余的就是垃圾。 GC 演进时间线 Go 的 GC 从"全程暂停"演进到"几乎不停顿",每一步都是被上一步的问题推动的: 阶段 做法 问题 STW 标记清扫 暂停所有 goroutine,标记垃圾并清扫 停顿与堆大小成正比,堆大了没法用 并发标记清扫 标记阶段与用户代码并发执行 并发时对象引用会变化...
调度器:GMP 模型深入
需要预先了解: goroutine 与并发初探 channel 调度器:GMP 模型深入 goroutine 与并发初探 里给出了 GMP 的概览。这一篇展开内部结构:G 里存了什么、调度器怎么选下一个 G、work stealing 和 hand-off 如何工作、抢占是怎么实现的,以及为什么网络 IO 不会占住线程。 G、M、P 的职责 G G G G G = goroutine(任务:栈 + 状态 + 调度信息) | | | | v v v v +---------------+ P = 逻辑处理器(本地队列 + 资源配额) | P runq | +---------------+ | v +---------------+ M = 内核线程(真正执行指令) | M | +---------------+ 结构 内容 G 栈指...
切片、map 与字符串的底层实现
需要预先了解: 数组与切片 map 与字符串 内存分配与逃逸分析 切片、map 与字符串的底层实现 这一篇把三种最常用的类型拆到运行时层面:切片头到底存了什么、append 扩容的精确规则、map 的桶结构与渐进式搬迁、字符串为什么不可变。搞懂这些,前面几篇里"看起来反直觉"的行为就都有解释了。 切片头 切片在运行时是一个三元组: type slice struct { array unsafe.Pointer // 指向底层数组 len int // 长度 cap int // 容量 } 用 unsafe 观察: s := make([]int, 3, 5) fmt.Printf("切片: len=%d cap=%d 底层数组指针=%p 首元素地址=%p\n", len(s), cap(s), unsafe.SliceData(s), &s[0]) fmt.Println("切片头大小:", unsafe.Sizeof(s)) 切片: le...
接口与 channel 的底层实现
需要预先了解: 接口 channel 内存分配与逃逸分析 接口与 channel 的底层实现 接口 里留下的 nil 陷阱、channel 里"收发是拷贝"的结论,都可以从运行时结构上得到解释。这一篇看 eface/iface/itab 和 hchan 的真实字段,以及一次发送、一次接收、一次关闭在底层分别发生了什么。 eface 与 iface 空接口和非空接口在运行时是两种结构: type eface struct { // 空接口 any / interface{} _type *_type // 动态类型 data unsafe.Pointer // 指向动态值的指针 } type iface struct { // 带方法的接口 tab *itab // 类型 + 方法表 data unsafe.Pointer // 指向动态值的指针 } 两者都是两个指针宽,在 64 位平台上是 16...
反射与 unsafe
需要预先了解: 接口 包与模块:代码组织 切片、map 与字符串的底层实现 反射与 unsafe 反射让程序在运行时检查类型、读取字段、调用方法;unsafe 则允许绕过类型系统直接操作内存。两者都是"不得已才用"的工具:反射带来灵活性和性能代价,unsafe 带来性能和不安全性。 为什么需要反射 编译期类型已知时,直接写代码就行。但有些场景类型只有在运行时才知道: encoding/json 要把任意 struct 转成 JSON,它不可能为每个类型写一份代码; ORM 要根据结构体字段生成 SQL; 配置加载要按 tag 把 YAML 映射到字段。 这些场景都需要"把类型信息当作数据来操作",这就是反射。 反射三定律 第一定律:接口值可以拆成反射对象。 var x any = User{Name: "一撮灰"} t := reflect.TypeOf(x) // 类型信息 v := reflect.ValueOf(x) // 值 第二定律:反射对象可以还原成接口值。 bac...



