Go无法直接用var声明跨包只读变量,因var导出后可被任意修改;正确做法是用未导出变量+导出访问函数返回副本或只读接口,配合初始化控制与并发保护。

为什么不能直接用 var 声明跨包只读变量
Go 没有“只读变量”语法,const 只支持编译期常量(不能是结构体、map、切片等运行时初始化的值),而配置通常需要从文件或环境加载。如果你在包 A 里写 var Config MyConfig,其他包 import 后能直接赋值修改——这不是“只读”,只是“可导出”。真正的只读意图,得靠约定 + 封装来实现。
推荐做法:用函数封装 + unexported 字段
把配置定义为包内未导出的变量,再提供导出的只读访问函数。这是 Go 标准库(如 http.DefaultClient)和主流项目(如 viper、zap)的实际做法。
- 配置结构体字段全部小写(
host string),不导出 - 包级变量用小写名(
config),不导出 - 提供大写的导出函数(
Config()),返回值是结构体副本或只读接口 - 避免返回指针——否则调用方仍可能通过反射或 unsafe 修改(虽不常见,但破坏只读语义)
// config/config.go
package config
type Config struct {
Host string
Port int
}
var config Config // 未导出变量
func Init(h string, p int) {
config = Config{Host: h, Port: p}
}
func Config() Config { // 返回副本,安全
return config
}
如果必须返回指针,怎么防止意外修改
返回指针更省内存,但需确保调用方无法修改。Go 无法阻止赋值,只能靠“无 setter 方法 + 文档 + 审计”来约束。若真要技术兜底,可用接口隔离:
- 定义只读接口(如
Reader),只含 getter 方法 - 让配置结构体实现该接口
- 导出函数返回接口类型,而非结构体指针
- 调用方拿到的是接口,没有字段写权限,也无法类型断言回原结构体(除非你导出它)
// config/config.go
type Reader interface {
Host() string
Port() int
}
type configImpl struct {
host string
port int
}
func (c configImpl) Host() string { return c.host }
func (c configImpl) Port() int { return c.port }
var instance configImpl
func Init(h string, p int) {
instance = configImpl{host: h, port: p}
}
func Config() Reader { // 返回接口,调用方无法写字段
return instance
}
常见踩坑点:init 顺序与并发读写
配置通常在 main() 开头初始化,但如果多个包 init 函数依赖 config.Config(),而你的 Init() 还没被调用,就会拿到零值。另外,如果允许多次 Init()(比如测试重置),就必须加锁,否则并发读写结构体是数据竞争。
立即学习“go语言免费学习笔记(深入)”;
- 永远假设配置在首次
Config()调用前未初始化——加 panic 提示比静默返回零值更安全 - 如果允许多次初始化,用
sync.Once或sync.RWMutex保护写,读操作本身无需锁(只要返回副本或不可变接口) - 测试中想重置?别在生产包里暴露
Reset(),而是用构建标签或测试专用包隔离
真正难的不是语法,而是让所有协程都看到一致的、初始化完成的配置快照——这取决于你何时、在哪、由谁触发初始化,而不是变量声明那一行怎么写。


















