在 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 App Engine API 中使用的模式)?
原文由 Zef Hemel 发布,翻译遵循 CC BY-SA 4.0 许可协议
长话短说:
应该经常使用指针的一种情况:
接收器 比其他参数更多地是指针。方法修改它们被调用的东西,或者命名类型是大型结构的情况并不少见,因此 指南是 默认为指针,除非在极少数情况下。
一些不需要指针的情况:
代码审查指南建议传递像
type Point struct { latitude, longitude float64 }
这样的 小结构,甚至可能更大一点的结构作为值,除非您调用的函数需要能够就地修改它们。bytes.Replace
需要 10 个字的参数(三个切片和一个int
)。您会发现复制大型结构会带来性能提升的 情况,但经验法则并非如此。对于 slice ,您不需要传递指针来更改数组的元素。
io.Reader.Read(p []byte)
更改字节p
,例如。它可以说是“像值一样对待小结构”的一个特例,因为您在内部传递一个称为 切片标头 的小结构(请参阅 Russ Cox (rsc) 的解释)。同样,您不需要指针来 修改地图或在频道上进行通信。对于 切片,您将重新切片(更改起始/长度/容量),内置函数如
append
接受一个切片值并返回一个新值。我会模仿那个;它避免了别名,返回一个新的切片有助于引起人们注意可能分配了一个新数组的事实,并且调用者很熟悉。interface{}
参数中的切片的指针。映射、通道、字符串以及函数和接口值,如切片,是内部引用或已经包含引用的结构,因此如果您只是想避免复制底层数据,则不需要将指针传递给它们. (rsc 写了一篇关于如何存储接口值的单独帖子)。
flag.StringVar
需要*string
出于这个原因。你在哪里使用指针:
考虑您的函数是否应该是您需要指针指向的任何结构上的方法。人们期望在
x
上有很多方法可以修改x
,因此将修改后的结构作为接收器可能有助于最大程度地减少意外。有关于接收器何时应该是指针的 指南。对其非接收方参数有影响的函数应该在 godoc 中明确说明,或者更好的是,godoc 和名称(如
reader.WriteTo(writer)
)。您提到接受一个指针来通过允许重用来避免分配;为了内存重用而更改 API 是一种优化,我会延迟,直到明确分配的成本不小,然后我会寻找一种不会将更棘手的 API 强加给所有用户的方法:
bytes.Buffer
)初始化的类型来帮助它避免堆分配。Reset()
将对象放回空白状态的方法,就像某些 stdlib 类型提供的那样。不关心或无法保存分配的用户不必调用它。existingUser.LoadFromJSON(json []byte) error
可以由NewUserFromJSON(json []byte) (*User, error)
包装。同样,它将懒惰和紧缩分配之间的选择推给了单个调用者。sync.Pool
处理一些细节。如果一个特定的分配产生了很大的内存压力,你确信你知道什么时候不再使用分配,并且你没有更好的优化可用,sync.Pool
可以提供帮助。 (CloudFlare 发布 了一篇关于回收的有用(pre-sync.Pool
)博客文章。)最后,关于你的切片是否应该是指针:值切片可能很有用,可以节省分配和缓存未命中。可能有阻滞剂:
NewFoo() *Foo
而不是让 Go 使用 零值 进行初始化。append
在底层数组增长 时复制项目。指向append
之前的切片项目的指针可能不会指向之后复制项目的位置,对于巨大的结构,复制可能会更慢,例如sync.Mutex
复制是不允许的。在中间插入/删除和排序也会移动项目,因此可以应用类似的注意事项。从广义上讲,如果您预先将所有项目放在适当的位置并且不移动它们(例如,在初始设置后不再有
append
s),或者如果您继续移动它们,则值切片可能有意义周围,但您确信这没问题(没有/小心使用指向项目的指针,并且项目很小或者您已经测量了性能影响)。有时它归结为更具体的情况,但这是一个粗略的指南。