Go中JSON字段脱敏应优先采用自定义类型+UnmarshalJSON或json.RawMessage+递归遍历两种方式,避免硬编码、字符串替换或反射;前者适用于结构已知场景,后者适用于动态JSON或批量脱敏。

Go 中对 JSON 字段脱敏,核心是避免直接操作原始字节或结构体字段名硬编码,而应借助 json.Unmarshal + 自定义类型 或 json.RawMessage + 遍历解析两种可靠路径。硬替换字符串或用反射遍历 struct 标签容易漏字段、破坏嵌套结构或误伤非敏感内容。
用自定义类型 + UnmarshalJSON 控制敏感字段解码
适用于已知结构、字段明确(如用户信息 struct),且需在反序列化时就完成脱敏的场景。关键不是“处理完再改”,而是“解进来就是脱敏后的”。
- 为敏感字段(如
Phone、IDCard)定义新类型,实现UnmarshalJSON方法,在里面做掩码逻辑 - 注意:必须用指针接收者,否则
json包无法调用(Go 1.20+ 对非指针接收者调用有严格限制) - 若字段是空值(
null),需显式处理,否则可能 panic 或跳过赋值 - 示例中
MaskedString对非空字符串保留前 3 位 +***,但实际应按合规要求调整(如手机号掩中间 4 位)
type MaskedString string
func (m *MaskedString) UnmarshalJSON(data []byte) error {
var s string
if err := json.Unmarshal(data, &s); err != nil {
return err
}
if s == "" {
*m = ""
return nil
}
*m = MaskedString(s[:min(3, len(s))] + "***")
return nil
}
type User struct {
Name string `json:"name"`
Phone MaskedString `json:"phone"`
}
用 json.RawMessage 延迟解析 + 递归遍历脱敏
适用于结构未知、动态 JSON(如日志上报、第三方 webhook)、或需统一规则批量脱敏多个字段名的场景。不依赖 struct 定义,靠 key 名匹配。
- 先用
json.RawMessage接收整个 JSON,再用json.Unmarshal解为map[string]interface{}或[]interface{} - 写递归函数遍历,对 map 的每个 key 判断是否命中敏感字段名(如
"id_card"、"bank_no"),命中则替换 value - 注意类型断言安全:value 可能是
string、float64、bool、nil、map、slice,需逐层处理 - 嵌套对象中同名字段(如多层
"password")会被全部脱敏,这是优点也是风险点——得确认业务是否允许
func maskJSON(data []byte, keysToMask map[string]bool) ([]byte, error) {
var raw interface{}
if err := json.Unmarshal(data, &raw); err != nil {
return nil, err
}
masked := maskValue(raw, keysToMask)
return json.Marshal(masked)
}
func maskValue(v interface{}, keys map[string]bool) interface{} {
if m, ok := v.(map[string]interface{}); ok {
for k, val := range m {
if keys[k] {
m[k] = "***"
} else {
m[k] = maskValue(val, keys)
}
}
return m
}
if s, ok := v.([]interface{}); ok {
for i, val := range s {
s[i] = maskValue(val, keys)
}
return s
}
return v
}
别忽略 json.Marshal 时的零值与空字符串陷阱
脱敏后重新序列化,常因零值字段未被忽略、或空字符串被错误保留,导致输出冗余或暴露逻辑。这不是脱敏问题,而是 JSON 编码控制没跟上。
- struct 字段加
omitempty标签只能跳过零值,但脱敏后字段可能是"***"(非零值),仍会输出——此时需业务层判断是否跳过该字段 - 若脱敏结果是空字符串(如原字段为空,你又设成
""),它不会被omitempty过滤,除非你额外用指针包装 - 更稳妥做法:脱敏函数返回新 map,只保留需输出的 key,不依赖 struct tag
- 性能敏感场景慎用多次
json.Unmarshal/Marshal,尤其大 JSON;可考虑goccy/go-json替代标准库提升速度
真正难的不是写掩码逻辑,而是厘清哪些字段算“敏感”、它们在不同嵌套层级是否都该脱敏、以及脱敏后是否还能被下游系统正确解析。比如 "code" 在订单里是状态码,在用户里可能是身份证号前缀——光靠字段名匹配会误伤。得结合上下文路径(如 user.id_card vs order.code)做判定,这时候就得上 gojsonq 或手写带路径的遍历器了。


















