用 Go 1.27.1 的 -gcflags=-m 与 -m -m 看编译器的逃逸判断,用 testing.AllocsPerRun 数真实分配次数:常量转接口虽然说 escapes to heap 却是 0 次分配,变长 make 在 32 字节以内留在栈上,二分法量出显式局部变量 128 KiB 与隐式分配 64 KiB 的栈上限,同一个函数内联与否结论相反,以及逃到堆上 500 万次比留在栈上慢约 3.3 倍。
用 unsafe 把 Go 1.27.1 的接口值按 {typ, data} 与 {itab, data} 读出来:接口值 16 字节,指针类型直接放进 data 字、值类型是一份拷贝,itab 里是接口类型、动态类型与按方法名排序的方法地址,同一个动态类型装进两种接口得到两个 itab;编译期已知的转换引用 go:itab 符号、断言走 runtime.typeAssert;接口调用在类型固定时与直接调用一样快(约 2 ns),4 种类型随机时约 7.2 ns。
用 Go 1.27.1 复现“返回 nil 指针的 error 不等于 nil”:有错误类型的 nil 指针装进 error 后,接口的类型字非空、数据字为空,与 nil 比较是 false;go vet 没有警告;调用方法会不会 panic 取决于方法是否解引用接收者;nil 的 map、slice、func 装进 any 同样不等于 nil,用 reflect 判断 IsNil。
用 Go 1.27.1 的基准测试与汇编量一次类型断言的成本:具体类型断言只是比较类型字,耗时与 1.95 ns 的空函数调用基线几乎相同;接口类型断言调用 runtime.typeAssert,多约 0.15–0.45 ns;类型开关先比 hash 再比类型地址,8 个 case 命中第 1 个与第 8 个没有差别;断言不分配内存。
用 Go 1.27.1 读字符串的 16 字节头并实测转换:子串与原串共用内存,string(b) 与 []byte(s) 默认拷贝,unsafe.String 不拷贝而写字面量会 SIGBUS;不逃逸的 string(b) 在 32 字节以内零分配,[]byte(s) 在结果从不被修改时由编译器“zero-copy”(1000 字节也是 0 次,-d=zerocopy=0 关掉后变成 1 次);m[string(b)] 与 string(b) == s 零分配;10 MiB 字符串只留 5 字节的子串会让堆保持 10.20 MiB。
用 Go 1.27.1 实测字符串的字节与 rune:range 的下标是字节偏移、遍历得到 rune,非法 UTF-8 变成 U+FFFD,按字节截断会切坏字符,预组合 é 与 e+重音符号 == 为 false,国旗与家庭 emoji 的 rune 数不等于看到的字符数;1000 个汉字 range 2222 ns 对逐字节 887 ns,先转 []rune 5016 ns;go vet 对 string(int) 的警告。
用 Go 1.27.1 实测 append 的扩容:小于 256 翻倍、之后按 (旧容量+768)/4 增长,再向上取整到内存 size class,所以 []int 的容量是 4 8 … 512 848 1280 1792 2560;不逃逸的 append 第一次直接用 32 字节栈缓冲(追加 4 个 int 零分配),逃逸时从 1 开始;一次追加多个时按需要的长度取整;100 万个 int 从 nil 逐个 append 换 36 次底层数组、累计分配 39.8 MiB,预分配只需 7.6 MiB。
用 Go 1.27.1 实测切片共享底层数组:子切片的写入会改到原切片,容量够时 append 原地写、悄悄改掉原切片后面的元素,容量不够才换新数组;三下标切片、slices.Clone 与 copy 能切断共享;函数里的 append 在容量够时会写到调用者的底层数组;复用缓冲区时存子切片会让所有结果变成最后一次的内容。
用 Go 1.27.1 实测数组的值语义:赋值、传参、结构体里的数组都会拷贝;range 数组会先拷贝(循环里改元素 v 看不到),range 切片或数组指针看得到;数组可以 == 和做 map 的键,长度不同是不同类型;传 32 KiB 数组按值 2039 ns、传指针 1200 ns;切片与切片不能比较、不能做键。
在 Go 1.27.1 里实测 map 的扩容:map 已是 Swiss Table 实现(旧的渐进式搬迁已移除),插入第 9 个键离开单组小 map,装载率 7/8 时容量依次翻倍,满 1024 个槽的表后拆分;100 万个键不给容量提示时 p99.9 是 16125 ns、最大 80 µs,给容量提示 p99.9 是 1500 ns;delete 全部键内存不回收,clear(m) 才回到 0.20 MiB。
在 Go 1.27.1 里实测 map 遍历顺序:同一个 map 每次遍历的起点不同;键数不超过 100 时,所有顺序都只是同一个环的旋转(100 个键只出现 100 种顺序),上千个键、多张表时才出现更多顺序;首键分布并不均匀(100 个键最少 59 次、最多 456 次);遍历时删除的键一定不出现、新增的键约 88% 出现;随机数来自迭代器初始化时的两个 rand()。
在 Go 1.27.1 里实测并发读写 map:运行时用 map 里的 writing 标志检测并直接 fatal(concurrent map writes / read and map write / iteration and map write,退出码 2),recover 接不住;检测是尽力而为——两个 goroutine 各写 10 万次 30 次全部 fatal,各只写 3 次 30 次里只有 2 次;sync.Mutex 与 sync.Map 都不再出错;-race 先报 DATA RACE 再 fatal。