最稳妥做法是直接用 json.marshal + os.writefile,因其自动转义、类型校验、支持结构体标签、兼容字段变更,而 fmt.sprintf 易出错且不安全。

直接用 json.Marshal + os.WriteFile 是最稳妥、最常用的做法,除非你有明确的性能、跨语言或字段私有性需求,否则别绕弯。
为什么不用 fmt.Sprintf 拼 JSON
手动拼接容易出错:引号漏写、nil 字符串没处理、字段顺序混乱、特殊字符(如换行、双引号)未转义。更严重的是,如果结构体字段名含用户输入(比如动态 key),可能被注入恶意内容——json.Marshal 自动做转义和类型校验,fmt.Sprintf 完全不检查。
- 不可序列化字段(如
func、chan、map[interface{}]string)会在json.Marshal时立刻报错,而fmt.Sprintf会静默输出无效 JSON -
json.Marshal尊重json:"-"和json:"name,omitempty"标签,fmt.Sprintf完全无视 - 一旦结构体字段增加或改名,
fmt.Sprintf要逐处改,json.Marshal只需改 struct 定义
json.MarshalIndent 适合调试,但别在日志或高频写入中用
加缩进会让文件体积变大(多出换行、空格),解析速度略慢,且对程序逻辑无益。只在开发期查数据、人工看文件内容时启用。
Go 配置库,使用 spf13/viper — 分层优先级(flag > env >file > KV > default),提供 BindPFlag/BindPFlags、SetEnvPrefix + SetEnvKeyReplace 等功能。
- 生产环境写配置、状态快照等,一律用
json.Marshal - 若需格式化,缩进参数建议用
"", " "(两个空格),别用"\t"——不同编辑器对 tab 渲染不一致 -
json.MarshalIndent不影响反序列化,读取时仍可用json.Unmarshal或json.NewDecoder
字段必须首字母大写,json: 标签不是可选的装饰
小写字母开头的字段(如 name string)会被 json.Marshal 完全忽略,不会报错也不会警告,结果是空对象或缺失字段——这是最常踩的坑。
- 导出字段是硬性要求:
ID int✅,id int❌ -
json:"email"控制键名,json:"email,omitempty"控制零值省略,两者语义不同,别混用 - 修改 struct 字段名(如
UserName→Username)会导致旧 JSON 文件反序列化时静默丢数据;加json:"user_name"可缓解,但前提是旧文件里键名也匹配
批量写结构体切片?直接传 []User 给 json.Marshal
不需要 for 循环单个 marshal 再拼接,json.Marshal 原生支持切片、map、嵌套结构体。
- 示例:
data, err := json.Marshal([]User{{ID: 1}, {ID: 2}})输出[{"ID":1},{"ID":2}] - 如果想让数组每项换行缩进,用
json.MarshalIndent即可,无需额外封装 - 注意:切片元素类型也必须满足导出+标签规范,否则某一项可能为空对象
真正难的不是“怎么写进去”,而是“字段改了之后老文件还能不能读”。只要结构体定义一动,就得同步考虑兼容策略——比如保留旧 json: 标签、加中间转换层,或者干脆放弃向后兼容。这点比语法细节重要得多。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!










