在 Go 中,有多种方法可以返回struct
值或其切片。对于我见过的个人:
type MyStruct struct {
Val int
}
func myfunc() MyStruct {
return MyStruct{Val: 1}
}
func myfunc() *MyStruct {
return &MyStruct{}
}
func myfunc(s *MyStruct) {
s.Val = 1
}
我了解它们之间的差异。第一个返回结构的副本,第二个返回指向函数内创建的结构值的指针,第三个期望传入现有结构并覆盖该值。
我已经看到所有这些模式都在不同的环境中使用,我想知道关于这些的最佳实践是什么。什么时候你会用哪个?例如,第一个适用于小型结构(因为开销很小),第二个适用于较大的结构。第三种是如果您希望获得极高的内存效率,因为您可以在调用之间轻松地重用单个结构实例。是否有关于何时使用哪个的最佳实践?
同样,关于切片的相同问题:
func myfunc() []MyStruct {
return []MyStruct{ MyStruct{Val: 1} }
}
func myfunc() []*MyStruct {
return []MyStruct{ &MyStruct{Val: 1} }
}
func myfunc(s *[]MyStruct) {
*s = []MyStruct{ MyStruct{Val: 1} }
}
func myfunc(s *[]*MyStruct) {
*s = []MyStruct{ &MyStruct{Val: 1} }
}
再次强调:这里的最佳实践是什么。我知道切片始终是指针,因此返回指向切片的指针没有用。但是,我是否应该返回一个结构体值切片、一个指向结构体的指针切片,是否应该将一个指向切片的指针作为参数传递(在Go 应用引擎 API)?
tl;dr:
- 使用接收者指针的方法很常见;接收者的经验法则是,“如果有疑问,请使用指针。”
- 切片、映射、通道、字符串、函数值和接口值在内部都是用指针实现的,指向它们的指针通常是多余的。
- 在其他地方,对大结构或必须更改的结构使用指针,否则传递值,因为通过指针突然改变事情是令人困惑的。
您应该经常使用指针的一种情况:
-
Receivers are pointers more often than other arguments. It's not unusual for methods to modify the thing they're called on, or for named types to be large structs, so the guidance is to default to pointers except in rare cases.
- 杰夫·霍奇斯抄袭者工具自动搜索按值传递的非小型接收器。
有些情况不需要指针:
-
代码审查指南建议通过小结构 like type Point struct { latitude, longitude float64 }
,甚至可能更大一点,作为值,除非您调用的函数需要能够就地修改它们。
- 值语义避免出现别名情况,即此处的赋值意外地更改了此处的值。
- 通过避免按值传递小结构可以更有效缓存未命中或堆分配。无论如何,当指针和值执行时相似地,Go-y 方法是选择任何提供更自然语义的方法,而不是榨干最后一点速度。
- 所以,Go Wiki 的代码审查意见页面建议当结构很小并且可能保持这种状态时按值传递。
- 如果“大”界限看起来很模糊,那么事实确实如此;可以说,许多结构都在指针或值都可以的范围内。作为下限,代码审查评论表明切片(三个机器字)可以合理地用作值接收器。作为接近上限的东西,
bytes.Replace
需要 10 个单词的参数(三个切片和一个int
)。你可以找到情况即使复制大型结构也会带来性能优势,但经验法则并非如此。
-
For slices,您不需要传递指针来更改数组的元素。io.Reader.Read(p []byte)
改变的字节p
, 例如。这可以说是“像对待值一样对待小结构”的特殊情况,因为在内部你正在传递一个称为 a 的小结构片头 (see 拉斯·考克斯 (rsc) 的解释)。同样,您不需要指向修改地图或在频道上进行交流.
-
For 您将重新切片的切片(更改开始/长度/容量),内置函数如append
接受一个切片值并返回一个新值。我会模仿那个;它避免了别名,返回一个新切片有助于引起人们对可能分配新数组这一事实的注意,并且调用者对此很熟悉。
- 遵循这种模式并不总是可行的。一些工具,例如数据库接口 or 序列化器需要附加到编译时类型未知的切片。他们有时接受指向切片的指针
interface{}
范围。
-
映射、通道、字符串以及函数和接口值与切片一样,是内部引用或已包含引用的结构,因此如果您只是想避免复制底层数据,则无需将指针传递给它们。 (RSC写了一篇关于如何存储接口值的单独文章).
- 在极少数情况下,您可能仍然需要传递指针modify调用者的结构:flag.StringVar需要一个
*string
例如,出于这个原因。
使用指针的地方:
-
考虑您的函数是否应该是您需要指向的结构的方法。人们期待有很多方法x
修改x
,因此修改接收者的结构可能有助于最大程度地减少意外。有指导方针当接收者应该是指针时。
-
对其非接收器参数有影响的函数应该在 godoc 中明确说明,或者更好的是,godoc 和名称(例如reader.WriteTo(writer)
).
-
您提到接受指针以避免通过允许重用而分配;为了内存重用而更改 API 是一种优化,我会推迟,直到明确分配具有不小的成本,然后我会寻找一种不会对所有用户强制使用更棘手的 API 的方法:
- 为了避免分配,Go 的逃逸分析是你的朋友。有时,您可以通过创建可以使用简单构造函数、普通文字或有用的零值(例如bytes.Buffer.
- 考虑一个
Reset()
将对象放回到空白状态的方法,就像某些 stdlib 类型提供的那样。不关心或无法保存分配的用户不必调用它。
- 为了方便起见,请考虑将就地修改方法和从头开始创建函数编写为匹配对:
existingUser.LoadFromJSON(json []byte) error
可以被包裹NewUserFromJSON(json []byte) (*User, error)
。它再次将惰性和压缩分配之间的选择推给了各个调用者。
- 寻求回收内存的调用者可以让sync.Pool处理一些细节。如果特定的分配产生了很大的内存压力,您确信自己知道分配何时不再使用,并且您没有更好的优化可用,
sync.Pool
可以帮助。 (CloudFlare 发布一个有用的(预sync.Pool)博客文章关于回收。)
最后,关于您的切片是否应该是指针:值切片可能很有用,可以节省分配和缓存未命中。可能存在阻碍:
-
用于创建您的项目的 API可能会将指针强加给您,例如你必须打电话
NewFoo() *Foo
而不是让 Go 初始化零值.
-
物品的预期寿命可能并不都是一样的。整个切片立即被释放;如果 99% 的项目不再有用,但您有指向其他 1% 的指针,则所有数组仍保持分配状态。
-
复制或移动值可能会导致性能或正确性问题,从而使指针更具吸引力。尤其,
append
复制项目时增长底层数组。指向之前切片项目的指针append
可能不会指向项目复制后的位置,对于巨大的结构,复制可能会更慢,例如sync.Mutex
不允许复制。在中间插入/删除和排序也会移动项目,因此可以应用类似的考虑因素。
从广义上讲,如果您预先将所有物品就位并且不移动它们(例如,不再移动它们),则价值切片是有意义的append
初始设置后),或者如果您确实不断移动它们但您确信没关系(不/小心使用指向项目的指针,并且项目很小或者您已经测量了性能影响)。有时这取决于您的具体情况,但这只是一个粗略的指南。
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)