用json.marshal和os.writefile保存配置最直接,无需第三方库;必须检查错误,注意权限、路径、并发安全及默认值处理。

用 json.Marshal 和 os.WriteFile 保存配置最直接
Go 没有内置的“配置数据库”,但文件是最轻量、最可控的持久化方式。只要结构体可序列化,json.Marshal 就能转成字节流,再用 os.WriteFile 写入磁盘——不需要第三方库,也不依赖环境变量或外部服务。
常见错误是忽略错误返回:写入失败时静默吞掉 err,导致配置看似保存成功实则丢失。必须检查 os.WriteFile 的返回值,尤其在 NFS 或只读挂载路径下容易出错。
示例写法:
type Config struct {
Port int `json:"port"`
Timeout int `json:"timeout"`
Enabled bool `json:"enabled"`
}
cfg := Config{Port: 8080, Timeout: 30, Enabled: true}
data, err := json.MarshalIndent(cfg, "", " ")
if err != nil {
log.Fatal(err)
}
if err := os.WriteFile("config.json", data, 0644); err != nil {
log.Fatal(err)
}
读取时用 os.ReadFile + json.Unmarshal,注意默认值和字段缺失
json.Unmarshal 不会报错于缺失字段,而是用零值填充——比如 Port 字段没写进文件,解码后就是 0,可能引发绑定端口失败等隐性问题。
建议在结构体定义时设合理零值,或读取后手动校验关键字段:
- 用指针字段(如
*int)区分“未设置”和“设为 0” - 或在解码后加
if cfg.Port == 0 { cfg.Port = 8080 }这类兜底逻辑 - 若文件不存在,
os.ReadFile返回os.IsNotExist(err),应主动创建默认配置而非 panic
不要依赖 json.Decoder 处理小配置文件——它更适合流式解析大 JSON,对单次读取反而多一层封装且无收益。
并发读写要加 sync.RWMutex,别用 os.OpenFile 的 O_APPEND
多个 goroutine 同时调用 WriteFile 会导致配置被覆盖,因为它是原子替换(先写临时文件再 rename)。如果需要实时更新部分字段,必须自己加锁控制临界区。
使用 uber-go/dig 在 Golang 中实现依赖注入——基于反射容器,提供 Provide/Invoke、dig.In/dig.Out 参数与结果对象,支持命名依赖等
错误做法:用 O_APPEND 打开文件然后 json.NewEncoder(f).Encode() ——这会把每次保存变成追加一行 JSON,最终文件变成非法格式(多对象无数组包裹)。
正确做法:
- 全局声明一个
var mu sync.RWMutex - 读配置前
mu.RLock(),写前mu.Lock() - 保持
WriteFile原子性,不拆分读/写操作
别为了“避免锁”而用 channel 串行化配置操作——简单场景下锁更直观、更少出错。
路径和权限要注意 os.WriteFile 的第三个参数是 perm,不是 umask
os.WriteFile("config.json", data, 0644) 中的 0644 是实际应用的权限位,不是与 umask 运算后的结果。在容器或 rootless 环境中,若误写成 0600,可能导致其他进程(如 sidecar)无法读取配置。
常见疏漏点:
- 路径含相对路径(如
"./conf/config.json"),但当前工作目录不固定,应优先用绝对路径或os.Executable推导 - Windows 下路径分隔符写死
"\",应统一用filepath.Join("conf", "config.json") - 配置文件被 git 跟踪却含敏感字段(如 API key),需加
.gitignore或改用环境变量注入关键项
文件存储本身很简单,难的是边界处理:权限、路径、并发、默认值、错误反馈——这些地方一松懈,配置就变成线上事故的入口。










