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

config 的优先级:system、global、local、worktree、include

实测 git 配置的层级与优先级:system → global → local 后读的覆盖先读的;worktree 层(extensions.worktreeConfig)在 local 之后;-c 与 GIT_CONFIG_COUNT 环境变量更高;include 按位置插入,includeIf gitdir: 可按路径引入不同身份;多值键(如 remote.origin.fetch)叠加,--get 取最后一个,写操作默认只改 local;XDG 与 ~/.gitconfig 同时存在时都读。

Git config 优先级 include includeIf 真实验证 动画演示
Git 内部原理 2026-10-11

alias 与外部子命令的查找

实测子命令的查找:git foo 找可执行文件 git-foo(--exec-path 和 PATH);拼错命令的提示与 help.autocorrect=immediate;别名展开,! 开头的别名在仓库根目录用 shell 运行,GIT_PREFIX 保存原目录;别名不能覆盖内置命令,且与外部子命令同名时外部命令胜出(顺序:内置 → 外部 → 别名);别名循环被检测;git help -a 列出 External commands 与 Command aliases。

Git alias 外部子命令 git-foo help.autocorrect 真实验证 动画演示
Go 内部原理 2026-10-10

逃逸分析:一个变量什么时候会从栈搬到堆

用 Go 1.27.1 的 -gcflags=-m 与 -m -m 看编译器的逃逸判断,用 testing.AllocsPerRun 数真实分配次数:常量转接口虽然说 escapes to heap 却是 0 次分配,变长 make 在 32 字节以内留在栈上,二分法量出显式局部变量 128 KiB 与隐式分配 64 KiB 的栈上限,同一个函数内联与否结论相反,以及逃到堆上 500 万次比留在栈上慢约 3.3 倍。

Go 逃逸分析 编译器 内存分配 真实验证 动画演示
Go 内部原理 2026-10-10

接口的内存布局:iface 与 eface 里到底存了什么

用 unsafe 把 Go 1.27.1 的接口值按 {typ, data} 与 {itab, data} 读出来:接口值 16 字节,指针类型直接放进 data 字、值类型是一份拷贝,itab 里是接口类型、动态类型与按方法名排序的方法地址,同一个动态类型装进两种接口得到两个 itab;编译期已知的转换引用 go:itab 符号、断言走 runtime.typeAssert;接口调用在类型固定时与直接调用一样快(约 2 ns),4 种类型随机时约 7.2 ns。

Go 接口 iface eface itab unsafe 真实验证 动画演示
Go 内部原理 2026-10-10

nil 接口陷阱:为什么返回 nil 指针的 error 不等于 nil

用 Go 1.27.1 复现“返回 nil 指针的 error 不等于 nil”:有错误类型的 nil 指针装进 error 后,接口的类型字非空、数据字为空,与 nil 比较是 false;go vet 没有警告;调用方法会不会 panic 取决于方法是否解引用接收者;nil 的 map、slice、func 装进 any 同样不等于 nil,用 reflect 判断 IsNil。

Go 接口 nil error reflect 真实验证 动画演示
Go 内部原理 2026-10-10

类型断言与类型开关:一次断言要花多少纳秒

用 Go 1.27.1 的基准测试与汇编量一次类型断言的成本:具体类型断言只是比较类型字,耗时与 1.95 ns 的空函数调用基线几乎相同;接口类型断言调用 runtime.typeAssert,多约 0.15–0.45 ns;类型开关先比 hash 再比类型地址,8 个 case 命中第 1 个与第 8 个没有差别;断言不分配内存。

Go 类型断言 类型开关 接口 汇编 真实验证 动画演示
Go 内部原理 2026-10-10

字符串的内部结构:string 与 []byte 互转什么时候要拷贝

用 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 字符串 []byte unsafe 内存分配 真实验证 动画演示
Go 内部原理 2026-10-10

rune 与 UTF-8:for range 字符串为什么和下标遍历不一样

用 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 UTF-8 rune 字符串 range 真实验证 动画演示
Go 内部原理 2026-10-10

append 的扩容策略:容量到底是怎么增长的

用 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 切片 append 扩容 内存分配 真实验证 动画演示
Go 内部原理 2026-10-10

切片共享底层数组:append 之后谁会被改掉

用 Go 1.27.1 实测切片共享底层数组:子切片的写入会改到原切片,容量够时 append 原地写、悄悄改掉原切片后面的元素,容量不够才换新数组;三下标切片、slices.Clone 与 copy 能切断共享;函数里的 append 在容量够时会写到调用者的底层数组;复用缓冲区时存子切片会让所有结果变成最后一次的内容。

Go 切片 append 别名 底层数组 真实验证 动画演示
Go 内部原理 2026-10-10

数组是值类型:传参、赋值、比较时发生了什么

用 Go 1.27.1 实测数组的值语义:赋值、传参、结构体里的数组都会拷贝;range 数组会先拷贝(循环里改元素 v 看不到),range 切片或数组指针看得到;数组可以 == 和做 map 的键,长度不同是不同类型;传 32 KiB 数组按值 2039 ns、传指针 1200 ns;切片与切片不能比较、不能做键。

Go 数组 值语义 range 真实验证 动画演示
Go 内部原理 2026-10-10

map 的扩容:Swiss Table 里的翻倍与分裂,以及插入耗时的分布

在 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 map Swiss Table 扩容 内存 真实验证 动画演示
京ICP备2026067502号-1京公网安备11010502063487号