Go中无真正不可变结构体,需通过私有字段+只读接口实现只读语义:导出接口而非结构体,字段全小写,构造函数返回接口,对slice/map等引用类型getter需返回副本。

Go 里没有真正的不可变结构体,但能靠封装+接口实现只读语义
Go 语言本身不支持 const struct 或字段级只读修饰符,所谓“不可变”,其实是通过限制访问路径来达成的——让外部代码拿不到可修改的指针或字段引用。关键不是编译期拦住,而是设计上不暴露可变入口。
常见错误现象:struct{ Name string } 直接导出、字段首字母大写,调用方拿到实例后直接改 obj.Name = "new";或者返回了 *MyStruct 指针,外部照样能改。
- 只导出结构体类型名,**不导出字段名**(全部小写)
- 提供构造函数(如
NewConfig()),返回值用值类型或只读接口,**不返回指针**(除非你明确想共享且可控) - 如果必须返回指针,就返回指向私有结构体的指针,并配套只读接口(见下一条)
用接口隔离读操作,把可变方法留在包内
这是最常用也最有效的模式:定义一个只含 getter 的接口,结构体实现它,但把字段和 setter 方法设为包私有。外部只能看到接口,看不到具体类型,也就没法直接改字段。
使用场景:配置对象、缓存条目、DTO 返回值等需要“传出去但不能乱动”的数据载体。
立即学习“go语言免费学习笔记(深入)”;
示例:
// config.go
type ConfigReader interface {
Timeout() time.Duration
Host() string
}
type config struct { // 小写,不导出
timeout time.Duration
host string
}
func (c *config) Timeout() time.Duration { return c.timeout }
func (c *config) Host() string { return c.host }
func NewConfig(h string, t time.Duration) ConfigReader {
return &config{host: h, timeout: t} // 返回接口,不是 *config
}
注意:返回 ConfigReader 后,调用方无法做类型断言回 *config(除非同包),更没法访问 timeout 字段——这才是“只读”的实际保障。
使用 uber-go/dig 在 Golang 中实现依赖注入——基于反射容器,提供 Provide/Invoke、dig.In/dig.Out 参数与结果对象,支持命名依赖等
返回值用值类型还是指针?看场景别硬套
很多人以为“返回值类型就安全”,其实不一定。如果结构体很大,值拷贝开销高;但如果结构体很小(比如 3 个 int),值返回反而更安全,因为调用方拿到的是副本,改了也不影响原数据。
参数差异和性能影响:
- 返回
MyStruct(值):安全,但可能复制开销大(>16 字节建议测一下) - 返回
*MyStruct(指针):高效,但必须配合接口隔离或私有字段,否则等于开门放人 - 返回
MyReadOnlyInterface:推荐,兼顾安全与灵活性,底层可以是值也可以是指针
容易踩的坑:func Get() *config { return &config{...} } —— 即使 config 是私有的,只要返回了指针,外部仍可通过反射或 unsafe 修改(虽然不推荐,但技术上可行)。所以光靠“私有类型”不够,还得配接口。
嵌套结构体或切片时,只读容易漏掉深层可变性
只封一层结构体远远不够。如果字段是 []string、map[string]int 或另一个导出结构体,外部拿到 getter 后仍可能修改底层数组或 map——因为 Go 的 slice/map 是引用类型,getter 返回它们会暴露底层 backing array。
常见错误现象:func (c *config) Tags() []string { return c.tags } → 调用方执行 cfg.Tags()[0] = "hacked",原数据就被改了。
- 对 slice:用
append([]T(nil), c.tags...)或copy(dst, c.tags)返回副本 - 对 map:新建 map,遍历 copy 键值对(
for k, v := range c.data { dst[k] = v }) - 对嵌套结构体:getter 也要返回只读接口或值副本,不能直接返回
*InnerStruct
这一步最容易被忽略——表面看着只读,一跑起来数据就悄悄变了。

















