
在 go 的 json api 开发中,直接将实现了 `error` 接口的结构体序列化为 {"error": "message"} 形式需手动实现 json.marshaler,因为标准 json.marshal 无法自动提取未导出字段或 error 接口的底层字符串。
Go 的 encoding/json 包在序列化时依赖反射(reflection),而绝大多数 error 类型(如 errors.New 返回的 *errors.errorString)内部字段是非导出的(unexported),因此默认无法被 JSON 编码器访问。你最初尝试的嵌入 Err error 字段的方式之所以输出 {"error":{}},是因为 json 包对空接口或未导出字段仅能序列化为空对象 {},而非其字符串表示。
要实现预期的 JSON 错误格式,最简洁可靠的方式是让自定义错误类型显式实现 json.Marshaler 接口:
type JsonErr struct {
error // 匿名嵌入,复用 Error() 方法
}
func (e JsonErr) MarshalJSON() ([]byte, error) {
// 注意:需对 error 消息做 JSON 字符串转义,避免注入非法字符(如换行、引号)
msg := strings.ReplaceAll(e.Error(), `"`, `\"`)
msg = strings.ReplaceAll(msg, `\`, `\\`)
msg = strings.ReplaceAll(msg, `\n`, `\\n`)
msg = strings.ReplaceAll(msg, `\r`, `\\r`)
msg = strings.ReplaceAll(msg, `\t`, `\\t`)
return []byte(`{"error": "` + msg + `"}`), nil
}⚠️ 更安全、推荐的做法是使用 json.Marshal 对消息本身进行转义,而非手动替换:
func (e JsonErr) MarshalJSON() ([]byte, error) {
type Alias JsonErr // 防止递归调用 MarshalJSON
data := struct {
Error string `json:"error"`
}{
Error: e.Error(),
}
return json.Marshal(data)
}该方案利用了匿名结构体+内嵌别名技巧,既避免了无限递归(因 Alias 不再实现 MarshalJSON),又借助标准库完成完整的 JSON 转义与格式化,语义清晰且健壮。
✅ 使用示例:
err := JsonErr{errors.New(`Invalid request: "user_id" is missing`)}
b, _ := json.Marshal(err)
fmt.Println(string(b)) // 输出:{"error":"Invalid request: \"user_id\" is missing"}? 补充建议:
- 在真实 API 中,可扩展 JsonErr 为包含 Code int、Message string、Details map[string]interface{} 等字段的结构,统一错误响应规范;
- 始终优先使用 json.Marshal 处理字符串内容,避免手拼 JSON——它能正确处理 Unicode、控制字符和引号转义;
- 若需全局错误处理(如 HTTP 中间件),可结合 http.Error 或自定义 ErrorResponse 类型统一序列化逻辑。
通过实现 MarshalJSON,你既能保持 error 接口的兼容性,又能精准控制其 JSON 表现形式,是构建健壮、可维护 Go Web API 的关键实践之一。










