传结构体要加 *t 是因为值传递会深拷贝整个结构体,大结构体导致性能爆炸;指针传递仅传8字节地址,开销极小,且能真正修改原数据。

为什么传结构体要加 *T 而不是 T
因为不加星号就是值传递,Go 会把整个结构体复制一份进函数栈——哪怕它有几十 MB。临床系统里一个 PatientReport 包含多年病历和上百题问卷,每次调用 processReport(report PatientReport) 都触发一次深拷贝,CPU 和内存直接拉满。
加 * 后,函数只收一个 8 字节的内存地址(64 位系统),开销几乎为零。但要注意:这不等于“自动可修改”,还得看函数体内怎么用这个指针。
- 正确改字段:
p.Name = "new"或(*p).Name = "new"(Go 允许省略括号) - 错误写法:
p = &User{}—— 这只是让局部变量p指向新地址,原结构体完全没动 - 验证是否生效:函数前后打印
&u(地址)和u.Name(值),地址不变、值变了才算成功
func (u User) Modify() 和 func (u *User) Modify() 的实际区别
值接收者方法操作的是副本,指针接收者才真正拿到原始内存地址。只要方法里有字段赋值(比如 u.Age = 30),就必须用指针接收者,否则编译不报错,但逻辑无效——你改的只是副本。
小结构体(如两个 int)用值接收者没问题;大结构体(含 []byte、map、嵌套切片)必须用指针,否则拷贝成本爆炸。
- 混用风险:同一个类型既有值接收者方法又有指针接收者方法,会导致接口实现不一致——只有指针接收者能实现需要指针方法的接口
- 数据库 ORM 场景下尤其危险:查不到记录时返回
nil,下游直接调用user.Profile.Name就 panic - 安全做法:调用前判空,
if user.Profile != nil { user.Profile.Name = "x" }
结构体里有 map 或 []string 字段,要不要传指针?
改元素值(如 u.Data["key"] = "val")不需要指针——因为 map 和 slice 本身是引用类型,底层 header 里存着指向真实数据的指针,赋值操作只复制 header,不影响原数据。
Go 配置库,使用 spf13/viper — 分层优先级(flag > env >file > KV > default),提供 BindPFlag/BindPFlags、SetEnvPrefix + SetEnvKeyReplace 等功能。
但如果你想替换整个容器(如 u.Data = make(map[string]int)),就必须用指针接收者。否则你只是在副本的 header 上写了新地址,原结构体字段仍指向旧内存。
- 切片扩容(
append)也属于“替换整个容器”行为,同样需要指针才能影响原字段 - 初始化别偷懒:结构体构造时对内部指针字段显式赋值(
&Profile{})或用工厂函数封装,避免运行时nil解引用 panic
什么时候该用 new(T) 而不是 &T{}
new(T) 返回一个指向零值 T 的指针,&T{} 是字面量取地址,两者语义不同。多数时候选 &T{} 更直观,尤其当你要初始化字段时:
u := &User{Name: "Alice", Age: 25}
而 new(User) 只能拿到全零值:&User{},没法直接设字段。
-
new的典型场景:你需要一个指针但暂时不关心初始值,比如作为函数参数占位,或配合sync.Pool复用对象 - 注意:
new([]int)返回的是*[]int(指向切片 header 的指针),不是*int,别和make([]int, 10)搞混 - 逃逸分析会影响结果:小结构体用
&T{}可能被分配到栈上,而new(T)总是在堆上分配
最常被忽略的点是:指针本身不保证安全,nil 解引用、字段未初始化、并发读写同一指针目标,都会在运行时突然崩掉。写的时候多打一行 if p != nil,比线上查一小时 panic 日志省力得多。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!










