深色模式
Go语言之值与指针选择
概述
Go 里“该返回结构体还是结构体指针”这个问题,看起来像一个局部技巧题,实际背后连着一整套更大的判断:当前类型表达的是值,还是一份可共享、可修改的对象身份。
所以这篇文章不只谈返回值,也把参数传递、方法接收者和共享状态一起看。真正要做的不是背一条“统一返回指针”或者“尽量传值”的口诀,而是先分清当前代码在表达哪种语义。
先分清值语义和指针语义
值语义更强调“这是一个值本身”:
- 复制后互不影响
- 修改副本不影响原值
- 更适合表达独立数据
指针语义更强调“这是同一个对象的引用入口”:
- 多个地方可能共享同一份数据
- 通过一个入口修改,其他引用能看到变化
- 更适合表达共享状态或大型对象
函数返回值时先问:结果是一个值,还是一个对象
例如:
go
type Point struct {
X int
Y int
}
func NewPoint(x, y int) Point {
return Point{X: x, Y: y}
}Point 很像一个纯值对象。返回值语义很自然,调用方拿到副本也没什么问题。
再看另一种:
go
type Client struct {
conn net.Conn
}
func NewClient(conn net.Conn) *Client {
return &Client{conn: conn}
}这种类型更像一个带状态、带资源句柄的对象入口。返回指针通常更符合它的使用方式。
小结构体返回值,常常没问题
如果结构体本身很小,字段清晰,且语义上更像值,返回值通常完全合理:
go
type Config struct {
Port int
Host string
}这里返回值有两个好处:
- 调用方拿到的是独立副本
- 不容易无意间共享可变状态
方法接收者和返回值判断通常要一致
如果一个类型的大部分方法都使用指针接收者,那它通常也更像指针语义对象。
例如:
go
type Buffer struct {
data []byte
}
func (b *Buffer) Write(p []byte) {
b.data = append(b.data, p...)
}像这种类型,返回 *Buffer 通常比返回 Buffer 更一致。
参数传递也该按同一条线判断
函数参数到底传值还是传指针,本质上仍然是同一问题:
- 是只读消费一个值
- 还是要修改、共享、避免过大拷贝
例如:
go
func Distance(p Point) int
func Fill(buf *Buffer)并发场景下,值语义往往更稳
并发里如果每个 goroutine 都拿到自己的值副本,很多共享状态问题会直接消失。
而一旦返回或传递的是指针,就要更认真地看:
- 是否真的需要共享
- 是否需要同步保护
逃逸分析值得知道,但别拿它当第一判断标准
Go 编译器会做逃逸分析。返回指针、闭包引用、接口装箱等行为,可能让对象分配到堆上。
这会影响性能,但工程上更稳的顺序通常是:
- 先把语义判断对
- 再看性能是否真的成问题
- 最后再结合逃逸分析优化
一条够用的判断线
遇到值还是指针的选择题时,先按这个顺序想:
- 这个类型表达的是值,还是对象身份。
- 调用方是否需要共享并修改同一份状态。
- 方法接收者整体偏值还是偏指针。
- 结构体大小和逃逸成本是否真的已经成为问题。
语义优先,性能其次,这条顺序通常比背结论可靠得多。
