指针与参数和返回值中的值

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

我了解这些之间的差异。 第一个返回结构的副本,第二个指向函数中创build的结构值的指针,第三个需要传入一个现有结构并覆盖该值。

我已经看到所有这些模式都被用在不同的环境中,我想知道最佳实践是什么。 你什么时候用哪个? 例如,第一个可以适用于小型结构(因为开销很小),第二个适用于大型结构。 第三,如果你想要非常有效的内存,因为你可以很容易地重用调用之间的单个结构实例。 什么时候使用哪种最佳实践?

同样,关于切片的同样的问题:

 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中使用的模式)?

tl; dr

  • 使用接收者指针的方法是常见的; 接收者的经验法则是 “如果有疑问,请使用指针”。
  • 切片,地图,通道,string,函数值和接口值在内部由指针实现,指向它们的指针通常是多余的。
  • 在其他地方,使用大的结构体或结构体的指针,你将不得不改变,否则传递值 ,因为通过指针让事情发生意外变化是令人困惑的。

一种情况你应该经常使用一个指针:

  • 接收者比其他参数更频繁的指针。 修改被调用的东西的方法并不常见,或者指定的types是大的结构,所以指导是默认指针,除非是极less数情况。
    • Jeff Hodges的copyfighter工具自动search按价值传递的非微小接收者。

有些情况下,你不需要指针:

  • 代码审查指南build议像type Point struct { latitude, longitude float64 }一样传递小的结构 type Point struct { latitude, longitude float64 }甚至可能更大的值作为值,除非你调用的函数需要能够修改它们。

    • 值语义可以避免在这里通过赋值在这里改变值的别名情况。
    • 不要为了一点速度而牺牲干净的语义,有时候通过价值传递小的结构实际上是更高效的,因为它避免了caching未命中或堆分配。
    • 所以,Go Wiki的代码审查评论页面build议当结构很小并且可能保持这种状态时,按值传递。
    • 如果“大”的界限似乎是模糊的, 可以说很多结构都处于一个指针或者数值不错的范围内。 作为一个下限,代码审查评论build议切片(三个机器字)是合理的使用价值接收器。 作为更接近上界的东西, bytes.Replace需要10个字的值(三个片和一个int )。
  • 对于切片 ,您不需要传递指针来更改数组的元素。 io.Reader.Read(p []byte)改变p的字节。 这可以说是一个“处理价值观念这样的小结构”的特例,因为在内部,你正在传递一个叫做片头的小结构(见Russ Cox(rsc)的解释 )。 同样,你不需要一个指针来修改地图或在通道上进行通信

  • 对于切片你会reslice (改变的开始/长度/容量),内置function,如append接受切片值,并返回一个新的。 我模仿那个; 它避免了别名,返回一个新的切片有助于引起人们注意,新的数组可能被分配,这对调用者来说是熟悉的。

    • 遵循这种模式并不总是可行的。 像数据库接口或序列化器这样的工具需要附加到在编译时不知道types的片上。 他们有时会接受一个指向interface{}参数中的切片的指针。
  • 地图,频道,string以及函数和接口值 (如切片)是内部引用或已包含引用的结构,所以如果您只是想避免复制底层数据,则不需要将指针传递给它们。 (rsc 写了一个关于如何存储接口值的单独的post )。

    • 您可能还需要在罕见的情况下传递指针,以便修改调用者的结构: flag.StringVar需要一个*string

你在哪里使用指针:

  • 考虑你的函数是否应该是你需要指针的结构的一个方法。 人们期望在x上有很多方法来修改x ,所以使修改的结构成为接收者可能有助于最小化惊喜。 关于接收器何时应该是指针的指导原则 。

  • 对非接收者参数有影响的函数应该在godoc中明确指出,或者更好的是godoc和name(如reader.WriteTo(writer) )。

  • 你提到接受一个指针,通过允许重用来避免分配。 为了内存重用而改变API是一个优化,我会延迟,直到明确的分配有一个不小的成本,然后我会寻找一种不强制所有用户的棘手的API:

    1. 为了避免分配,Go的逃避分析是你的朋友。 有时你可以通过使用一个简单的构造函数,一个纯文本或一个像bytes.Buffer这样有用的零值来初始化types来避免堆分配。
    2. 考虑一个Reset()方法将对象置回空白状态,就像stdlibtypes提供的一样。 不关心或不能保存分配的用户不必调用它。
    3. 考虑编写就地修改方法和创build从头函数作为匹配对,为了方便起见,可以用NewUserFromJSON(json []byte) (*User, error)来包装NewUserFromJSON(json []byte) (*User, error) 。 再次,它推动懒惰和捏分配之间的select个人来电者。
    4. 寻求回收内存的呼叫者可以让sync.Pool处理一些细节。 如果一个特定的分配会产生很大的内存压力,那么您sync.Pool知道alloc何时不再使用,并且没有更好的可用优化, sync.Pool可以提供帮助。 (CloudFlare发布了有关回收的有用(pre sync.Pool )博客文章 。)
    5. 奇怪的是,对于复杂的构造函数, new(Foo).Reset()有时可以避免在NewFoo()不能分配时。 不习惯; 小心在家里试一试。

最后,关于你的切片是否应该是指针:切片的值可以是有用的,并且保存你的分配和caching未命中。 可以有阻滞剂:

  • 创build你的项目的API可能会强制你的指针,例如你必须调用NewFoo() *Foo而不是让Go用零值初始化。
  • 物品的期望寿命可能不是全部相同。 整个切片立即被释放; 如果99%的项目不再有用,但是你有指向其他1%的指针,则所有的数组都保持分配状态。
  • 移动项目可能会导致您的问题。 值得注意的是,当它增长底层数组时, append拷贝项目。 指针在append指向错误位置之前,复制对于巨大的结构可能会比较慢,而对于例如sync.Mutex复制是不允许的。 在中间插入/删除和sorting类似的移动项目。

一般来说,价值切片是有意义的,如果你把所有的东西放在前面,不要移动它们(例如,在初始设置后不再append ),或者如果你继续移动它们,但是你确定这没关系(没有/小心使用指向项目的指针,项目足够小以便高效地复制等)。 有时你必须考虑或衡量你的情况的具体情况,但这是一个粗略的指导。