在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
}

我了解两者之间的区别。第一个返回该结构的副本,第二个返回指向在函数内创建的结构值的指针,第三个期望传入现有结构并覆盖该值。

我已经看到所有这些模式都可以在各种情况下使用,我想知道关于这些的最佳实践是什么。什么时候使用?例如,第一个可能适用于小型结构(因为开销很小),第二个适用于较大的结构。第三,如果您想提高内存效率,因为您可以轻松地在调用之间重用单个结构实例。有什么最佳实践,何时使用?

同样,关于 slice 的相同问题:
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} }
}

再说一遍:什么是最佳实践。我知道 slice 始终是指针,因此返回指向 slice 的指针没有用。但是,是否应该返回一个结构值 slice ,一个指向结构的指针 slice ,是否应该将指向 slice 的指针作为参数传递(在Go App Engine API中使用的模式)?

最佳答案

tl;博士:

  • 使用接收器指针的方法很常见; the rule of thumb for receivers is,“如有疑问,请使用指针”。
  • slice ,映射, channel ,字符串,函数值和接口(interface)值是在内部使用指针实现的,指向它们的指针通常是多余的。
  • 在其他地方,将指针用于大型结构或您将要更改的结构,否则使用pass values,因为通过指针意外更改某些东西会造成混淆。


  • 一种应该经常使用指针的情况:
  • 接收者是指针,而不是其他参数。方法修改被调用的东西或命名类型为大型结构并不罕见,因此the guidance is默认为指针,除非极少数情况。
  • Jeff Hodges的copyfighter工具自动搜索按值传递的非微小接收者。


  • 在一些不需要指针的情况下:
  • 代码审查指南建议将小结构传递给type Point struct { latitude, longitude float64 },甚至传递一些更大的值作为值,除非您调用的函数需要能够在适当的位置修改它们。
  • 值语义避免混叠情况,在此情况下,此处的赋值会意外更改其上的值。
  • 牺牲一点干净的语义来加快速度并不是一件容易的事,有时通过值传递小的结构实际上更有效,因为它避免了cache misses或堆分配。
  • 因此,Go Wiki的code review comments页面建议在结构较小且可能保持这种状态时按值传递。
  • 如果“大”临界值似乎模糊,则为;可以说,许多结构都在指针或值确定的范围内。作为下限,代码审查注释建议 slice (三个机器字)可以合理地用作值接收者。接近上限时,bytes.Replace接受了值(value)10个字的args(三个 slice 和一个int)。您可以找到situations,即使复制较大的结构也可以赢得性能,但是经验法则并非如此。

  • 对于 slice ,您不需要传递指针来更改数组的元素。例如,io.Reader.Read(p []byte)更改p的字节。可以说这是“对待像值一样的小结构”的特例,因为在内部,您正在传递一个称为 slice 头的小结构(请参见Russ Cox (rsc)'s explanation)。同样,您不需要指向的指针即可修改 map 或在 channel 上进行通信。
  • 对于 slice ,您将重新 slice (更改的开始/长度/容量),像append这样的内置函数接受 slice 值并返回一个新值。我会模仿的;它避免了混淆,返回一个新的分片有助于引起人们注意可能分配了一个新数组的事实,并且调用者很熟悉它。
  • 遵循这种模式并不总是可行的。诸如database interfacesserializers之类的某些工具需要追加到其类型在编译时未知的片上。他们有时在interface{}参数中接受指向 slice 的指针。

  • map , channel ,字符串以及函数和接口(interface)值像 slice 一样,是内部引用或已经包含引用的结构,因此,如果您只是想避免复制基础数据,则无需传递指向他们的指针。 (rsc wrote a separate post on how interface values are stored)。
  • 在极少数情况下,您仍需要传递指针以修改调用者的结构:例如, flag.StringVar 为此需要*string


  • 使用指针的位置:
  • 考虑您的函数是否应该是需要指针指向的结构上的方法。人们期望使用x上的许多方法来修改x,因此使修改后的结构成为接收器可能有助于最大程度地减少意外。当接收者应该是指针时有guidelines
  • 对它们的非接收器参数有影响的函数应该在godoc中,或者更好的是,在godoc和名称(例如reader.WriteTo(writer))中使之清楚。
  • 您提到接受一个指针,以允许通过重用来避免分配。为了内存重用而更改API是一种优化,我会延迟直到明显知道分配费用不菲,然后再寻找一种不会对所有用户强制使用棘手API的方法:
  • 为了避免分配,Go的escape analysis是您的 friend 。有时,您可以通过创建可以用平凡的构造函数,纯文本或有用的零值(如 bytes.Buffer )初始化的类型来帮助它避免堆分配。
  • 考虑像某些stdlib类型提供的那样,使用Reset()方法将对象放回空白状态。不在乎或无法保存分配的用户不必调用它。
  • 为方便起见,请考虑编写就地修改方法和从头创建函数作为匹配对,以方便使用:existingUser.LoadFromJSON(json []byte) error可以用NewUserFromJSON(json []byte) (*User, error)包装。再次,它在懒惰和捏分配给单个调用者之间进行选择。
  • 寻求回收内存的调用者可以让 sync.Pool 处理一些细节。如果特定的分配产生了很大的内存压力,您可以确定何时不再使用该分配,并且没有更好的优化方法,sync.Pool可以为您提供帮助。 (CloudFlare发布了有关回收的a useful (pre- sync.Pool ) blog post。)


  • 最后,关于 slice 是否应为指针:值 slice 可以很有用,并且可以节省分配和缓存未命中。可能有阻止者:
  • 用于创建商品的API 可能会强制您使用指针,例如您必须调用NewFoo() *Foo而不是让Go使用zero value初始化。
  • 所需的项目生命周期可能并不完全相同。整个 slice 立即被释放;如果99%的项目不再有用,但您有指向其他1%的指针,则所有数组均保持分配状态。
  • 周围移动值可能会导致性能或正确性问题,使指针更具吸引力。值得注意的是,appendgrows the underlying array时复制项目。在append之前指向的指针指向错误的位置之后,对于大型结构(例如,对于禁止复制sync.Mutex。在中间插入/删除并类似地移动项目。

  • 广义上讲,如果您将所有项目都放在适当的位置并且不移动它们(例如,在初始设置后不再移动append),或者如果您确实继续移动它们,则可以确定值(value) slice 是否合理没关系(无需/谨慎使用指向项目的指针,项目足够小以至于无法有效复制等)。有时您必须考虑或衡量具体情况,但这只是一个粗略的指导。

    关于pointers - 指针与参数和返回值中的值,我们在Stack Overflow上找到一个类似的问题:https://stackoverflow.com/questions/23542989/

    10-09 19:23
    查看更多