结构体字段应优先用T而非T,仅当需修改原始值、允许为nil或对象过大(>16字节)时才用T;JSON反序列化中*T无法区分“未提供”与“显式零值”;指针接收者与字段是否为指针无必然关联。
*T 还是 T?先看这三条铁律绝大多数情况下,字段该用值类型就用值类型,该用指针就用指针——不是“为了节省内存”或“为了可变性”随便选的。核心判断依据只有三个:是否需要修改原始值、是否允许为 nil、是否属于大对象(通常 > 16 字节)。
time.Time、string、小数组(如 [4]byte)、小结构体(如 type Point struct{ X, Y int })——直接用值类型,安全、清晰、无意外nil(比如可选配置、延迟初始化、数据库 NULL 映射),必须用 *T;否则只能靠零值(0/""/nil slice)区分,语义模糊[1024]byte),用 *T 可避免拷贝开销,尤其在频繁赋值、传参、作为 map value 时明显*T 字段的坑:零值 vs nil 不对等Go 的 json.Unmarshal 对 *T 字段的处理和值类型完全不同:它不会把缺失字段设为 nil,而是保持原指针不变(即仍为 nil);但对值类型字段,会写入零值。这导致“字段未提供”和“字段显式设为零值”无法区分。
{"name": "foo"} 反序列化到 struct{ Name string; Age *int } → Age 保持 nil,可判断“客户端没传 Age”struct{ Name string; Age int } → Age 变成 0,无法区分“没传”还是“传了 0”json tag 加 omitempty 只影响序列化,不影响反序列化行为接收者用指针(func (s *S) M())只决定方法能否修改结构体本身,**和字段是否用指针完全无关**。你完全可以有一个全值字段的结构体,却用指针接收者去更新其中某个字段——只要那个字段本身可寻址(即不是从 map 或函数返回值直接取的临时值)。
*T” → 没有逻辑关联T(值类型),且你想在方法里修改它,那它必须是结构体的可寻址字段(比如 s.Field = newval 合法),而非从 map[string]T 里取出来的副本[]byte,方法里做 s.Data = append(s.Data, x) ——没问题,因为 []byte 本身是 header,复制开销小,且 append 可能重分配底层数组,必须用指针接收者才能让修改生效当嵌入一个含指针字段的结构体(如 type User struct{ Profile *Profile }),外部结构体的 JSON 行为、nil 安全性、零值判断都会继承该指针字段的语义。稍不注意就会出现“看似初始化了,实际关键字段仍是 nil”的问题。
*Config 而非 Config,会导致整个外层结构体在 json.Unmarshal 后,Config 字段仍为 nil,即使 JSON 包含完整 config 数据——因为 json 包不会自动 new 一个 Config 给你if u.Config == nil { u.Config = &Config{} }
u.Config.Validate())会 panic,而值类型嵌入则天然安全最易被忽略的一点:字段指针带来的 nil 检查义务是传染性的。一旦某个字

*T,所有访问它的路径(包括方法、HTTP handler、日志打印)都得加 if x != nil,否则 runtime panic。这不是性能问题,是代码健壮性的硬门槛。