传结构体时必须用*T而非T的场景有五:一是含sync.Mutex等不可拷贝类型;二是含大容量[]byte或map等高开销字段;三是需修改原对象字段;四是函数名含Update/Save等动词暗示副作用;五是接收者为指针以匹配语义契约。

传结构体时,什么时候必须用 *T 而不是 T
结构体含 sync.Mutex、map、[]byte 或字段超过 5 个基本类型时,编译器会直接拒绝值传递——因为这些类型不可拷贝,或拷贝开销过大。比如:
-
sync.Mutex是非可拷贝类型,func f(u User)若User含mu sync.Mutex,编译报错cannot be copied - 结构体带
data []byte(几 MB)或config map[string]interface{},值传参会触发完整内存拷贝,GC 压力陡增 - 方法接收者要改字段(如
func (u *User) SetName(n string)),不加*就改不动原对象
但别一上来就全用指针:小结构体(比如 type Point struct{ X, Y int },仅 16 字节)传值更安全、更清晰,且现代 Go 编译器常做逃逸分析优化,未必真分配堆内存。
*T 参数里解引用前必须检查 nil
Go 不自动判空,*T 可能是 nil,一旦 *t 或 t.Field 被访问就 panic:panic: invalid memory address or nil pointer dereference。这不是偶发 bug,而是高频崩溃点。
- 函数开头第一行就该写
if t == nil { return errors.New("t is nil") }或panic("xxx: t must not be nil") - 别依赖调用方“不会传 nil”——HTTP handler、JSON unmarshal、数据库 scan 都可能产出 nil 指针
- 如果逻辑允许 nil(比如可选配置),也要显式分支处理,而不是放任解引用
结构体传值 ≠ 数据隔离,slice 和 map 字段仍共享底层
这是最隐蔽的坑:你以为传 User 是副本,结果 User.Data []int 的底层数组还是同一块内存。改 u.Data[0] = 99,原对象跟着变;但 u.Data = append(u.Data, 1) 不一定——因为 append 可能扩容并返回新 slice 头部。
立即学习“go语言免费学习笔记(深入)”;
- 想彻底隔离,得手动深拷贝:
newData := make([]int, len(u.Data)); copy(newData, u.Data) - 更可持续的做法:把可变引用字段设为私有,暴露
GetData() []int(返回副本)和SetData(...)(带校验) - 别迷信“传值就绝对安全”,Go 的值传递只复制结构体头,不递归复制其引用字段内容
函数名暗示意图,Update/Save 类动词必须配 *T
用户看到 func updateUser(u *User, name string),立刻明白副作用会发生;而 func userDisplayName(u User) string 表明只读、无修改。这种契约靠命名 + 类型共同建立。
- 如果函数名含
Set、Apply、Configure、Reset,参数必须是*T,否则违反直觉 - 反过来,若函数只读字段(如
func userAge(u User) int),传值更合适——避免调用方误以为你可能改它 - 接收者同理:
func (u User) Name()和func (u *User) Save()的语义差异,就是参数传递逻辑的镜像
真正难的不是语法,是判断“这个参数我到底要不要让它可变”。值传递保安全,指针传递表意图,二者不是性能取舍,而是契约设计。


















