真正能拿到原始json字节的时机只在c.json()调用前,用json.marshaler接口实现字段级脱敏是最可控、零额外gc、类型安全的方案。

ResponseWriter 不是缓冲区,中间件里读不到原始 JSON 字节——这是所有想“后置脱敏”的人最先撞上的墙。
你没法在 c.Next() 之后调 c.Writer.Body 去做字符串替换,因为 gin.Writer 默认不缓存响应体;c.JSON() 一执行,字节就直推 TCP,后续中间件根本没机会看到完整 JSON。
别在中间件里对 ResponseWriter 做字符串替换
- 这种操作会破坏 JSON 结构:正则匹配可能误杀字段名里的
"phone"或值中的正常星号(比如密码含"a*b") - 触发
http: multiple response.WriteHeader calls错误:因替换逻辑常重复调用WriteHeader - 对流式响应(如
text/event-stream)完全失效 - 无法区分 Content-Type:硬上替换会把
application/octet-stream下载文件也当 JSON 处理,直接损坏二进制
真正能拿到原始 JSON 字节的时机,只在 c.JSON() 调用前——但那时你还没序列化,也没法改结构。
用 json.Marshaler 接口实现字段级脱敏
这才是 Gin 项目里最可控、零额外 GC、类型安全的出口:
- 必须用指针接收器:
func (u *User) MarshalJSON() ([]byte, error),值接收器在嵌套结构中会 panic - 防递归关键:定义别名类型
type userAlias User,再用(*userAlias)(u)转换后调json.Marshal - 脱敏值不能为
"":若字段带json:",omitempty",脱敏成空字符串会导致整个 key 消失;建议固定掩码如"138****1234" - 权限开关得靠闭包捕获:比如
redactEnabled := isRedactEnabled(c),再在MarshalJSON内部用,因为json.Marshal不传context.Context
示例中对 Phone 字段脱敏:
func (u *User) MarshalJSON() ([]byte, error) {
type Alias User
raw := &struct {
*Alias
Phone string `json:"phone,omitempty"`
}{
Alias: (*Alias)(u),
}
if u.Phone != "" && isRedactEnabled(u.ctx) {
raw.Phone = MaskPhone(u.Phone)
}
return json.Marshal(raw)
}
用 gin.ResponseWriter 包装器拦截 Write 和 WriteHeader
仅当你必须统一处理所有 JSON 响应(比如遗留代码无法改结构体),才考虑这条路:
- 只对
Content-Type: application/json做处理,其他类型(text/html、image/png)必须直通 - 包装器要自己维护缓冲区:
bytes.Buffer或sync.Pool分配的切片,避免高频 GC -
Write([]byte)中需判断是否为首次写入,决定是否补WriteHeader(200) - 解析 JSON 前先检查是否合法:用
json.Valid()快速过滤非 JSON 响应,避免json.Unmarshalpanic
复杂点在于,你得在包装器里重建结构体或用 map[string]interface{} 逐层遍历——这比 MarshalJSON 多一层反序列化开销,且无法响应上下文(比如按用户角色返回不同精度)。
真正难的不是怎么写脱敏逻辑,而是意识到:脱敏不是“响应后补救”,而是“序列化前决策”。字段标签 + MarshalJSON 是唯一能同时兼顾类型安全、性能、可测试性和上下文感知的路径。











