字段脱敏必须控制序列化入口,通过实现指针接收器的json.Marshaler接口让敏感字段自主决定序列化逻辑,配合struct tag声明规则、go:generate生成静态方法规避反射,并在HTTP响应写入路径上统一拦截处理。

字段脱敏必须控制序列化入口,别在 JSON 字节流上动刀
HTTP 响应体一旦写入 c.Writer(gin)或 http.ResponseWriter,就不可逆地发往客户端。中间件里读不到原始 JSON,也拦不住第三方库(如 jsoniter、goccy/go-json)直写 response。常见错误是 handler 返回前用反射改 struct 字段值——对已序列化的 JSON 无效,且污染源数据。
真正可控的路径只有一条:让敏感字段自己决定怎么序列化。也就是实现 json.Marshaler 接口,且必须用指针接收器:
func (u *User) MarshalJSON() ([]byte, error) {
type Alias User // 防递归
aux := &struct {
*Alias
Phone string `json:"phone"`
IDCard string `json:"id_card"`
}{
Alias: (*Alias)(u),
}
if shouldRedact() {
aux.Phone = "*** **** ***"
aux.IDCard = "****************"
}
return json.Marshal(aux)
}- 值接收器
func (u User) MarshalJSON()会导致嵌套结构体或 nil 指针 panic - 脱敏后字段不能设为空字符串,否则带
json:",omitempty"的字段会被整个丢弃 - 别手动拼
"{\"phone\":\"***\"}"—— 引号、转义、nil 处理全得自己扛,极易出错
struct tag 是唯一靠谱的规则声明方式
用 secure:"phone,mask" 或 redact:"id_card,mask:8" 这类自定义 tag,比复用 json: 更语义清晰、IDE 可跳转、编译期可校验。外部配置(YAML/JSON)或注释管理规则,同步成本高、运行时无提示、改了不报错。
反射读 tag 时必须注意:
立即学习“go语言免费学习笔记(深入)”;
-
field.CanInterface() == false的私有字段直接跳过,别调reflect.Value.Interface(),否则 panic -
Kind() == reflect.Ptr要先Elem();Kind() == reflect.Interface要先Interface()再判断类型 - 遇到
time.Time、sql.NullString等自定义 marshaler 类型,别硬遍历字段,它们有自己的序列化逻辑
高频路径必须规避反射,go:generate 是刚需
每次请求都做 reflect.ValueOf(v).NumField() 遍历,性能掉 10 倍以上。日志、HTTP 响应这类出口,反射就是明确瓶颈。
实操建议:
- 用
go:generate扫描 struct tag,为每个结构体生成静态Redact(ctx context.Context) *User方法 - 生成代码直接访问字段,零反射调用、零 runtime 成本
- tag 中只声明规则(如
mask:6),生成器据此硬编码切片逻辑,不塞业务判断 - 私有字段天然被跳过,编译期即确定可访问性,不用运行时
CanInterface()判空
HTTP 中间件必须包装 ResponseWriter,不是加个 defer 就完事
gin 的 c.JSON()、echo 的 c.JSON() 都是直接调 json.Marshal + Write,没留 hook。想统一脱敏,就得替换响应写入路径:
- 实现自定义
SecureJSON(code int, data interface{})方法,内部用json.Marshal+writeMasked - 或包装
http.ResponseWriter,重写Write([]byte),只对Content-Type: application/json做流式解析(用json.Decoder.Token()匹配 key 路径,如user.phone) - 别用
io.TeeReader拦 body —— HTTP 响应是 write-only,没有 body 可读 - 注意流式解析要处理 incomplete JSON、UTF-8 BOM、换行符等边界情况
最易被忽略的一点:脱敏必须基于完整 key 路径(order.user.phone),而不是单纯字段名 phone。否则 address.phone 和 user.phone 会混淆,规则失控。


















