
本文详解在 Go 中使用嵌入(embedding)结构体时,如何通过字段遮蔽(field shadowing)与 JSON 标签组合,精准控制 json.Marshal 输出,避免暴露 gorm.Model 中的 DeletedAt 等内部字段。
本文详解在 go 中使用嵌入(embedding)结构体时,如何通过字段遮蔽(field shadowing)与 json 标签组合,精准控制 `json.marshal` 输出,避免暴露 `gorm.model` 中的 `deletedat` 等内部字段。
在 Go 的结构体嵌入场景中,尤其是配合 GORM 这类 ORM 框架时,开发者常会嵌入 gorm.Model(含 ID、CreatedAt、UpdatedAt、DeletedAt 等字段)以复用基础模型能力。但当需要将数据序列化为 JSON(如 API 响应)时,这些内部字段往往不应暴露——例如 DeletedAt 作为软删除标记,其 *time.Time 类型在 JSON 中默认输出为 null,不仅语义不清,还可能泄露数据库设计细节;而 Password 等敏感字段更需彻底排除。
直接在嵌入字段上修改标签不可行:gorm.Model 是外部包定义的结构体,无法修改其字段标签。此时,字段遮蔽(Field Shadowing) 是 Go 官方推荐且最简洁的解决方案:在导出结构体中定义同名字段,覆盖嵌入字段的 JSON 行为。
以下是一个生产就绪的实践示例:
package main
import (
"encoding/json"
"fmt"
"time"
)
// 模拟 gorm.Model(实际来自 github.com/jinzhu/gorm)
type Model struct {
ID uint `gorm:"primary_key"`
CreatedAt time.Time `gorm:"index"`
UpdatedAt time.Time `gorm:"index"`
DeletedAt *time.Time `gorm:"index"`
}
// 数据库实体模型
type User struct {
Model
Username string `json:"username" sql:"size:32;not null;unique"`
Password string `json:"-" sql:"not null"` // 使用 "-" 完全忽略密码字段
Locale string `json:"locale" sql:"not null"`
}
// 公共响应模型:通过遮蔽实现精细化 JSON 控制
type PublicUser struct {
*User
// 遮蔽 DeletedAt:用 bool 类型替代 *time.Time,并设 omitempty
DeletedAt bool `json:"deleted_at,omitempty"`
// 可选:遮蔽 CreatedAt/UpdatedAt(若无需时间戳)
// CreatedAt time.Time `json:"created_at,omitempty"`
// UpdatedAt time.Time `json:"updated_at,omitempty"`
}
func main() {
// 构造测试数据(模拟 DB 查询结果)
now := time.Now()
u := &User{
Model: Model{
ID: 3,
CreatedAt: now.Add(-24 * time.Hour),
UpdatedAt: now,
DeletedAt: nil, // 未删除
},
Username: "dan",
Locale: "en_US",
}
data, err := json.Marshal(PublicUser{User: u})
if err != nil {
panic(err)
}
fmt.Println(string(data))
// 输出:
// {"ID":3,"CreatedAt":"2024-01-01T00:00:00Z","UpdatedAt":"2024-01-02T00:00:00Z","username":"dan","locale":"en_US"}
}✅ 关键原理说明:
- Go 的 JSON 编码器按结构体字段声明顺序遍历,同名字段中,外层(嵌入者)字段优先于内层(被嵌入者)字段;
- PublicUser 中的 DeletedAt bool 遮蔽了 User.Model.DeletedAt *time.Time,因此 json.Marshal 不再访问后者;
- omitempty 标签确保当 bool 值为 false(即未删除)时,该字段完全不出现于 JSON 中,比输出 "deleted_at": false 更符合 REST API 设计规范。
⚠️ 注意事项与最佳实践:
- 避免类型冲突:遮蔽字段必须是导出的(首字母大写),且类型可不同(如 *time.Time → bool),但需确保逻辑语义一致;
- 谨慎遮蔽时间字段:若需保留 CreatedAt/UpdatedAt,建议显式定义 time.Time 字段并加 omitempty,而非依赖嵌入字段;
- 密码处理:始终对敏感字段(如 Password)使用 json:"-" 标签,这是比遮蔽更安全、更明确的方式;
- GORM v2 提示:新版 GORM(gorm.io/gorm)已弃用 DeletedAt 软删除默认行为,如升级,请改用 gorm.DeletedAt 自定义字段并同步调整遮蔽策略;
- 性能考量:遮蔽是编译期行为,无运行时开销,优于运行时反射过滤或中间结构体转换。
综上,字段遮蔽 + json 标签是 Go 生态中处理嵌入结构体 JSON 序列化的标准范式。它轻量、高效、可读性强,能精准满足 API 层的数据脱敏与格式定制需求。










