字符串字面量存储在二进制的.rodata段(只读数据段),由编译器静态分配,非堆非栈;跨文件重复字面量不自动合并,需手动提取为包级var实现确定性去重。

字符串字面量到底存在哪?
Go 中的 "hello"、`` 这类字符串字面量,编译后基本都落在二进制的 .rodata 段(只读数据段),不是堆也不是栈。你可以用 objdump -s -j .rodata your_binary 直接看到它们——搜索 "status" 或 "",大概率能定位到连续的十六进制字节。
这不是 Go 特有机制,而是 ELF 格式和操作系统内存保护的通用实践:把确定不变的数据标记为 PROT_READ,写就段错误(SIGSEGV)。所以别指望 unsafe.String 或反射拼出来的字符串也能进这里——只有编译期完全确定的字面量才配。
跨文件重复字符串会不会被合并?
不会自动合并。Go 编译器(截至 1.22)不提供跨编译单元的字符串驻留(interning)。你在 pkg/a.go 写 s := "api/v1",在 pkg/b.go 再写一次 t := "api/v1",链接后很可能出现两份独立的 "api/v1",各自带一个 string 头(指针 + 长度)。
- Go issue #5160 明确表示:无计划加入全局字符串池,理由是破坏构建可重现性、引入隐式行为
- 同个文件内重复字面量,链接器可能合并(但不保证),跨包/跨文件则基本不处理
- 用
go tool objdump -s main\.main your_binary | grep -A2 "api/v1"能验证是否多处出现
什么时候该手动提取成变量?
不是所有重复都要抽。判断依据不是“看起来一样”,而是:是否高频、是否体积大、是否跨多个包长期存在。比如模板中反复出现的 "<tr><td>"、JSON 键名 <code>"id"、HTTP 状态码文本 "404 Not Found"。
立即学习“go语言免费学习笔记(深入)”;
抽成包级 var 是最稳妥的做法:
var (
HTMLTrTd = "<tr><td>"
JSONID = "id"
Status404 = "404 Not Found"
)
- 抽成
var后,所有引用共享同一份.rodata数据 + 同一个string头,真正去重 - 不能用
const—— 字符串const仍是字面量语义,不解决跨文件冗余问题 - 注意:抽成
var会略微增加启动时的初始化开销(极小),但换来的是确定性的内存布局
为什么 unsafe.String 不进 .rodata?
unsafe.String(Go 1.20+)只是绕过类型检查,把 []byte 转成 string,底层分配路径没变——它依然走堆或栈,data 指针指向可写内存,跟 .rodata 无关。
常见误判场景:
-
s := unsafe.String([]byte("hello"))→ data 指针每次不同,无法与字面量"hello"共享地址 -
fmt.Sprintf("id=%d", 123)、strings.Repeat("x", 1000)→ 全部动态分配,哪怕内容相同也绝不进.rodata - 想验证?用
reflect.ValueOf(s).UnsafeAddr()打印 data 地址,字面量一致,动态构造的每次变
真正的常量池只存在于编译器生成的静态映射里,没有运行时工厂,也没有清空接口——你唯一能做的,是让字面量尽量少、尽量集中、尽量显式复用。


















