该用 *T 而不是 T 的情况包括:需修改原值、结构体含不可复制字段(如 sync.Mutex)、体积大或字段多、实现指针接收者接口、JSON 中需区分“未设置”与“设为零”、map 中需修改原对象。

什么时候该用 *T 而不是 T?看是否需要共享状态或延迟初始化
Go 语言中,T(值类型)和 *T(指针)本质区别在于:是否共享底层数据。实际项目里,**只要结构体字段有修改需求、或体积较大、或需实现接口方法(且该方法有指针接收者),就该用 *T**。
常见误判是“小结构体一定用值”,但忽略了一个关键点:如果该类型实现了某个接口,而接口方法定义在 *T 上,那传 T 就无法满足接口——编译直接报错:cannot use t (type T) as type SomeInterface in argument: *T does not implement SomeInterface。
- 需要修改原值(如
user.SetAge(25))→ 必须用*User - 结构体含
sync.Mutex或其他不可复制字段 → 只能取地址,否则编译失败 - 结构体超过 4 字段或含 slice/map/chan/interface → 值拷贝开销明显,优先用指针
- 函数参数用于接收 JSON 解析结果(如
json.Unmarshal([]byte, &v))→v必须是指针
map[string]T 和 map[string]*T 的选择陷阱
很多人以为 map 存指针是为了避免拷贝,其实更关键的是:**map 中的值是只读副本,改它不会影响原对象**。所以如果你往 map[string]User 里存了用户,再通过 m["alice"].Name = "Alice2" 修改,这个修改根本不会生效——因为操作的是 map 内部的副本。
正确做法是:要么用 map[string]*User,要么把修改逻辑封装成方法并传入指针。
users := map[string]*User{
"alice": &User{Name: "Alice"},
}
users["alice"].Name = "Alice2" // ✅ 生效
但要注意副作用:多个 map key 指向同一 *User 时,一处修改会影响所有引用;而 map[string]User 天然隔离,适合做快照或缓存中间态。
JSON 序列化时 T 和 *T 对空值处理完全不同
Go 的 json 包对 nil 指针和零值的处理逻辑差异极大,这在 API 响应和配置解析中极易踩坑。
-
type Config struct { Timeout *int `json:"timeout"` }→ 如果 JSON 中没传timeout字段,Timeout为nil;若传了"timeout": null,也是nil -
type Config struct { Timeout int `json:"timeout"` }→ 无论字段缺失还是null,解码后都是0,无法区分“未设置”和“设为零”
因此涉及可选配置项、数据库 nullable 字段映射、或需要明确区分“未提供”与“显式设为空”时,必须用指针类型。反例:CreatedAt time.Time 在创建时间可能为空的场景下,应改为 *time.Time。
切片、map、channel 本身已是引用类型,别无谓加指针
新手常犯错误:把 []int 包一层变成 *[]int,或把 map[string]string 改成 *map[string]string。这是冗余且有害的。
因为 []int、map[string]string、chan int 这些类型底层都包含指向底层数据的指针(比如 slice 是 struct{ ptr unsafe.Pointer; len, cap int }),它们本身就是轻量级引用。再套一层指针不仅增加解引用开销,还让调用方必须写 &slice,破坏直观性。
唯一需要 *[]T 的典型场景是:函数内要改变 slice 的底层数组(比如扩容后替换原 slice),且希望调用方看到变化——但这极少发生,多数时候应返回新 slice。
func appendAndReplace(s *[]int, v int) {
*s = append(*s, v) // ❗仅在此类极少数场景才需要 **s**
}
实际项目中最容易被忽略的,是接口实现与指针接收者的隐式绑定关系——哪怕结构体很小,只要它的某个方法用了指针接收者,那它作为接口值时就必须传 *T,否则运行时 panic 或编译失败。这不是性能问题,而是类型系统强制的契约。










