最稳妥方式是先用json.Marshal序列化结构体,再用os.WriteFile写入文件,需确保字段导出、检查marshal和writefile双错误、预建目录、验证反序列化,并在并发场景下采用临时文件+os.Rename实现原子写入。

用 json.Marshal 序列化结构体再写入文件最稳妥
Go 没有内置“一键保存配置”的函数,得自己组合:先序列化,再写磁盘。直接用 json.Marshal 处理结构体是最常见也最可控的方式,它能自动处理字段名映射、类型转换和嵌套结构。
注意别用 json.MarshalIndent 无脑美化——缩进会增加文件体积,且某些嵌套深的结构容易因空格/换行导致解析失败(尤其跨平台读取时)。真要可读性,只在调试阶段加 MarshalIndent,上线用 Marshal。
示例:
cfg := struct {
Port int `json:"port"`
Host string `json:"host"`
}{Port: 8080, Host: "localhost"}
data, err := json.Marshal(cfg)
if err != nil {
log.Fatal(err)
}
os.WriteFile("config.json", data, 0644)
写文件前必须检查 os.WriteFile 的错误,不能忽略 io/fs.ErrPermission
很多配置写入失败不是因为路径不存在,而是权限不足——比如程序以普通用户运行却试图写入 /etc/ 下的文件,此时 os.WriteFile 返回的错误是 io/fs.ErrPermission,不是 os.ErrNotExist。不判断就 panic 或静默失败,会导致配置看似“保存成功”,实则没落盘。
立即学习“go语言免费学习笔记(深入)”;
建议做法:
- 写之前先用
os.Stat检查目标目录是否存在且可写 - 写完立刻用
os.ReadFile读回并反序列化一次,验证内容可被正确解析 - 避免用
os.Create+Write组合——它不会自动设置文件权限,且出错时可能留下空文件
YAML 配置需额外依赖 gopkg.in/yaml.v3,注意字段标签冲突
Go 标准库不支持 YAML,必须引入第三方包。最常用的是 gopkg.in/yaml.v3,但它和 json 标签不完全兼容:比如 yaml:",omitempty" 和 json:",omitempty" 行为一致,但 yaml:"port,omitempty,string" 这类类型强制转换语法,json 包根本不认。
如果同一结构体既要 JSON 又要 YAML 序列化,别混用标签。要么拆成两个结构体,要么统一用 yaml 标签(json 包能兼容大部分 yaml 标签),但得测试 omitempty 在两种格式下是否表现一致。
关键点:
- 导入必须用
import yaml "gopkg.in/yaml.v3",不是v2(v2 对 map[string]interface{} 支持差) - 写 YAML 文件时,用
yaml.Marshal后手动加换行(append(data, '\n')),否则某些编辑器或工具读取时首行会乱 -
yaml.Marshal默认不排序 key,若需固定顺序(比如 diff 友好),得用yaml.MapSlice手动构造
并发写配置时,os.WriteFile 不是原子操作,要用 os.Rename 替代
os.WriteFile 实际是先清空原文件再写入,中间存在窗口期——其他 goroutine 或外部进程可能读到截断或损坏的内容。真正的原子写法是:写入临时文件(路径带 .tmp 后缀),再用 os.Rename 覆盖原文件。Linux/macOS 下 Rename 是原子的,Windows 要求同磁盘才安全。
典型错误写法:os.WriteFile("config.yaml", data, 0644) —— 并发读取时大概率出问题。
安全写法要点:
- 临时文件路径用
filepath.Join(filepath.Dir(path), "."+filepath.Base(path)+".tmp")生成 - 写完立即
os.Chmod(tmpPath, 0644),避免 rename 后权限丢失 - rename 失败时,必须清理临时文件(
os.Remove(tmpPath)),否则磁盘悄悄被占满
这个步骤看着啰嗦,但线上服务改配置频繁时,漏掉原子性就是故障温床。


















