必须用 *T 而非 T 的三种情况:一是结构体含 sync.Mutex 等不可拷贝字段;二是需修改调用方原始值(如 json.Unmarshal);三是结构体过大(超4–5字段或含 []byte/map),避免高拷贝开销。

Go 函数参数全是值传递,*T 传的是地址的副本,不是“引用传递”——但它是唯一能修改调用方变量、避免大结构体拷贝的合法手段。
什么时候必须用 *T 而不是 T
不是“想改就加星号”,而是两类硬性约束:
- 结构体含不可拷贝字段:比如
sync.Mutex、sync.RWMutex,func f(u User)直接编译报错cannot copy sync.Mutex - 需要修改调用方原始值:比如
json.Unmarshal(data, &v),v必须是可寻址的;func increment(x *int) { *x++ }才能让外部变量真加 1 - 结构体字段超 4–5 个,或含
[]byte、map、嵌套结构体等高开销字段:拷贝成本从几字节跳到 KB 级,GC 压力明显上升
*T 参数开头不判 nil 就 panic
*T 可能为 nil,Go 不自动检查,解引用即崩:
- 错误写法:
if p != nil && *p == "hello"——&&短路不保命,*p在左边先执行,p == nil时直接panic: invalid memory address or nil pointer dereference - 正确写法:第一行就写
if p == nil { return errors.New("p is nil") }或panic("xxx: p must not be nil") - 特别注意:HTTP handler、JSON unmarshal、DB scan 都可能产出
nil指针,别信“调用方不会传 nil”
小结构体别盲目用指针,反而更慢
像 type Point struct{ X, Y int }(16 字节)这种,传值比传指针更高效:
使用 uber-go/dig 在 Golang 中实现依赖注入——基于反射容器,提供 Provide/Invoke、dig.In/dig.Out 参数与结果对象,支持命名依赖等
- 值传递走寄存器或栈,无间接寻址开销;指针要解引用(
*p),多一次内存访问 - 现代 Go 编译器对小结构体做逃逸分析很准,常保留在栈上;而指针参数容易触发
leaking param,强制堆分配 - 切片(
[]int)、map(map[string]int)、chan 本身已是轻量 header(24 字节),func process(s []int)就够了,func process(s *[]int)多余且危险
结构体传值 ≠ 数据隔离,[]byte 和 map 字段仍共享底层
你以为 u := *orig 是深拷贝?不是。结构体头被复制,但其字段里的 slice/map 仍指向同一底层数组或哈希表:
-
u.Data[0] = 99会改到原对象的Data[0] -
u.Data = append(u.Data, 1)不一定——扩容后新 slice 指向新数组,原对象不受影响 - 想彻底隔离:手动深拷贝,如
newData := make([]byte, len(u.Data)); copy(newData, u.Data) - 更可持续的做法:把可变字段设为私有,暴露
GetData() []byte(返回副本)和SetData()(带校验)
最易被忽略的一点:函数名是契约的一部分。UpdateUser、SaveConfig 这类动词暗示副作用,参数必须是 *User、*Config;而 UserDisplayName 这种只读函数,传 User 更清晰、更安全。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!










