不能在c.json()里直接改结构体字段,因为json.marshal已全量序列化完成,修改无效;深层map易漏字段或panic;推荐用marshaljson方法脱敏或统一apiresponse包装器预处理。

为什么不能在 c.JSON() 里直接改结构体字段
因为 c.JSON() 调用时,Go 已经通过 json.Marshal 把结构体字段值全量序列化进 HTTP body,此时再修改原结构体字段(比如把 phone 设为 "138****1234")毫无意义——序列化早完成了。更麻烦的是,如果 handler 返回的是 map[string]interface{} 或嵌套 interface{},硬编码字段名去替换,极易漏掉深层字段或 panic(比如把 int64 当 string 处理)。
用 MarshalJSON 方法做结构体级脱敏
对固定响应结构体(如 UserResponse),直接实现 MarshalJSON() 方法,是最轻量且类型安全的做法。它天然避开反射开销,也无需中间件劫持 writer。
- 脱敏逻辑只作用于该结构体实例,不污染原始数据,线程安全
- 避免依赖
json:"phone,sensitive"这类自定义 tag——encoding/json根本不解析它,额外反射解析既慢又易错 - 手机号、身份证号等需保留格式特征的字段,可在方法内用正则精准替换:比如
regexp.MustCompile(`^(\d{3})\d{4}(\d{4})$`).ReplaceAllString("$1****$2", phone) - 不要在
MarshalJSON()里直接修改接收者字段(如u.Phone = mask(u.Phone)),应基于副本操作
中间件预处理 + 统一响应包装器
当项目中存在大量不同结构体、或部分接口仍返回 map[string]interface{} 时,靠每个结构体写 MarshalJSON() 维护成本高。这时可约定所有 handler 必须返回一个统一类型(如 ApiResponse),由中间件统一调用其 SafeJSON() 方法输出。
-
ApiResponse内部持有一个data interface{}字段,SafeJSON()方法先递归遍历并脱敏所有字符串型敏感键(如"id_card"、"email"),再调用json.Marshal - 敏感键列表建议从配置文件加载,支持正则(如
^.*phone$),避免硬编码 - 该方案要求团队强约束:任何地方调用
c.JSON(200, rawMap)都会绕过脱敏,必须禁用或用 linter 拦截 - 注意:别尝试用
io.MultiWriter包裹c.Writer去读取已写出的 JSON——会破坏 chunked encoding 和 gzip,导致Content-Length错误
千万别碰 Writer 缓冲区劫持
Gin 的 c.Writer 是 http.ResponseWriter 封装,一旦 c.JSON() 执行,header 和 body 就已 flush 到 TCP 连接。你无法在中间件里“拦截并修改”已序列化的字节流。
- 没有
c.Writer.Body字段可供读取;即使有,也早已不可读 - 强行重写
ResponseWriter实现代价极高,且与 streaming、gzip、HTTP/2 等机制冲突 - 日志中间件里可以复制请求体做脱敏,但响应体不行——这是 Gin(及绝大多数 Go Web 框架)的设计边界
真正可控的脱敏点只有两个:结构体的 MarshalJSON(),或 handler 返回前对响应数据的预处理。其他路径,要么失效,要么埋雷。











