结构体传值还是传指针取决于意图而非单纯性能:需修改原结构体或含大字段/引用类型(如slice、map)时传指针;小结构体(≤8字节且无引用类型)优先传值,更安全清晰。

Go 函数传参时,结构体到底该传值还是传指针?
传指针不是为了“省性能”,而是为了明确意图:你是否需要函数修改原结构体,或结构体本身太大(比如含 []byte、map、大数组)导致拷贝开销明显。小结构体(如 3 个 int 字段)传值更清晰、更安全,编译器也常做逃逸分析优化。
常见错误现象:func updateUser(u User) { u.Name = "new" } 调用后原 u 没变——这是预期行为,但很多人误以为“没生效是 bug”,其实是没理解 Go 的值传递语义。
- 判断标准:结构体大小 ≤ 机器字长(通常 8 字节)且不含引用类型 → 优先传值
- 含
sync.Mutex、map、slice、chan或字段超过 4–5 个基本类型 → 传指针更合理 - 如果函数名含
Set、Update、Configure等动词,用户默认期待副作用 → 必须传指针
结构体字段含 slice/map 时,传值仍会共享底层数据
这是最容易踩的坑:结构体传值,但其中的 slice 和 map 是引用类型,它们的底层数组或哈希表仍被共享。你以为在操作副本,其实改的是同一块内存。
示例:type Config struct { Data []int },传值调用 func modify(c Config) { c.Data = append(c.Data, 1) } 不影响原 c.Data(因为 append 可能扩容并返回新 slice),但 c.Data[0] = 99 会直接影响原 slice 内容。
立即学习“go语言免费学习笔记(深入)”;
- 想彻底隔离,需手动深拷贝:对
slice用copy+make,对map遍历赋值 - 更稳妥的做法:设计结构体时,把可变引用类型字段设为私有,并提供带校验的访问方法
- 别依赖“传值就绝对安全”——Go 的值传递只保证结构体头(header)拷贝,不递归拷贝引用内容
接收者用指针还是值,和参数传递逻辑完全一致
方法接收者本质就是第一个隐式参数。所以 func (u *User) Save() 和 func save(u *User) 在参数传递上没区别,只是语法糖。
常见错误现象:定义了 func (u User) SetName(n string),又在别的地方调用 u.SetName("x") 后发现 u.Name 没变——和前面函数传值问题一模一样。
- 只要方法会修改接收者字段,接收者必须是指针类型(
*T) - 如果结构体含
sync.Mutex,接收者必须是*T:因为sync.Mutex的零值是有效状态,复制后锁会失效(Lock()在副本上调用不影响原锁) - 接口实现一致性:如果某个方法用了指针接收者,那所有方法最好都用指针接收者,避免调用方混淆哪些值能被接口变量持有
benchmark 里看不清拷贝开销?得看逃逸分析和汇编
单纯跑 go test -bench 很难看出结构体传值的“真实成本”,因为小结构体常被分配到栈上,且编译器可能内联、消除冗余拷贝。真正影响性能的,往往是意外逃逸到堆上引发 GC 压力。
验证方式:go build -gcflags="-m -l" main.go,看输出中是否有 ... escapes to heap;再配合 go tool compile -S 看关键函数是否生成了 MOVQ 类型的大块内存复制指令。
- 结构体含指针字段(如
*http.Client)不一定逃逸,但含闭包或 interface{} 值大概率逃逸 - 用
unsafe.Sizeof(T{})查大小,比凭感觉靠谱;超过 32 字节就值得怀疑是否该传指针 - 性能优化永远从 profile 开始:先
go tool pprof确认是参数拷贝拖慢了,再改,别猜
结构体传参的本质不是性能题,是接口契约题。什么时候该让调用方感知“我可能被改”?什么时候该承诺“我绝不会变”?这些信号全靠传值/传指针来表达。一旦忽略这点,后面 debug 的时间远超省下的几个纳秒。











