字符串字面量均存储在.rodata段;go编译器将双引号字符串统一放入只读数据段,映射为只读页,写入触发段错误,且重复字面量自动去重共享内存。

字符串字面量都落在 .rodata 段里
Go 编译器会把所有双引号括起来的字符串字面量(如 "api/v1"、"not found")收集起来,统一塞进只读数据段(Linux/ELF 下叫 .rodata,Windows PE 下是 .rdata)。这个段在进程加载时被映射为只读页,任何运行时写入尝试(比如用 unsafe 强行改)都会触发 segmentation fault。
你可以用 objdump -s -j .rodata ./your_binary 直接看到这些字符串;它们不是堆上分配的,也不受 GC 管理——程序整个生命周期都钉死在那里。
- 多个包里重复出现的相同字面量(比如
"timeout"),通常共享同一份内存,这是编译器自动做的 dedup - 哪怕你把它赋给一个
const或普通变量,只要源头是字面量,就进.rodata -
go:embed的内容不在此列——那是单独的只读数据节,但行为类似
const s = "hello" 和 var s = "hello" 在内存上没区别
两者都指向 .rodata 中同一块地址。Go 不会因为用了 var 就给字面量额外拷一份到堆或栈上。区别只在语义和编译期检查:
-
const值参与编译期计算(比如const n = len("hello")是合法的) -
var声明会生成一个string结构体(16 字节:指针 + 长度),其str字段指向.rodata中的字节序列 - 逃逸分析不会因为
var就让字面量“逃逸”——它本来就不在栈/堆上分配
也就是说,var 只是多了一个结构体头,内容还是共享的。
拼接产生的字符串不进 .rodata,而是在堆上动态分配
像 a + b、fmt.Sprintf、strings.Join 这类操作,结果字符串一定不在 .rodata 里。它们的底层是调用 runtime.makeslice 在堆上申请新内存,再把字节拷过去。
- 即使拼的是两个
.rodata字面量,比如"foo" + "bar",结果仍是堆分配 - 这种字符串会被 GC 跟踪;一旦无引用,内存就回收
- 频繁拼接小字符串(尤其在循环里)会触发大量堆分配,是性能热点
想确认某字符串是否来自字面量,可以用 reflect.StringHeader 提取指针,再用 mincore(Linux)或 VirtualQuery(Windows)查页面属性——只读页大概率就是 .rodata。
静态链接时 .rodata 不会被 strip,但可被工具剥离
Go 默认静态链接,所有依赖包的字符串字面量全打进最终二进制。这会让文件体积变大,尤其是带大量错误信息、日志模板或 API 路径的项目。
-
go build -ldflags="-s -w"可去掉符号表和调试信息,但.rodata内容照旧 - 真正删减字符串常量得靠外部工具,比如
upx --lzma压缩,或自定义 linker script 排除某些 section(极少见且危险) - 混淆工具(如
garble)会重写字符串字面量为加密后内容 + 运行时解密,本质是绕过.rodata的明文存储
注意:剥离或加密字符串常量会影响 panic 日志、pprof 符号、debugger 查看——生产环境权衡时得留出可观测性退路。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











