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


327

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


1
正如您所说,这实际上取决于用例。视情况而定,它们都是有效的-这是可变对象吗?我们要副本还是指针?顺便说一句,您没有提到使用new(MyStruct):),但是分配指针和返回指针的不同方法之间并没有真正的区别。
2014年

15
这实际上是工程上的问题。结构必须非常大,以至于返回指针会使您的程序更快。只是不要打扰,编写代码,配置文件,如果有用的话进行修复。
沃尔克2014年

1
只有一种方法可以返回值或指针,即返回值或指针。如何分配它们是一个单独的问题。使用适合您情况的内容,并在担心之前编写一些代码。
JimB

3
顺便说一句,出于好奇,我对此进行了盘问。返回结构与指针的速度似乎大致相同,但是将指针传递给函数的速度要快得多。尽管不很重要,但很重要
Not_a_Golfer 2014年

1
@Not_a_Golfer:我假设只是bc分配是在函数外部完成的。基准值与指针的基准也取决于事实之后的结构大小和内存访问模式。复制高速缓存行大小的东西的速度尽可能快,并且从CPU高速缓存中取消引用指针的速度与从主内存中取消引用指针的速度有很大不同。
JimB

Answers:


392

tl; dr

  • 使用接收器指针的方法很常见。接收者的经验法则是:“如有疑问,请使用指针”。
  • 切片,映射,通道,字符串,函数值和接口值是在内部使用指针实现的,指向它们的指针通常是多余的。
  • 在其他地方,将指针用于大型结构或必须更改的结构,否则传递值,因为通过指针使事情意外更改会造成混淆。

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

  • 接收器 比其他参数更经常地使用指针。方法修改被调用的东西或命名类型为大型结构并不罕见,因此在极少数情况下,指南是默认使用指针。
    • 杰夫·霍奇斯(Jeff Hodges)的copyfighter工具自动搜索按值传递的非微小接收者。

在一些不需要指针的情况下:

  • 代码审查指南建议将小结构(type Point struct { latitude, longitude float64 },甚至可能更大的东西)作为值传递,除非您调用的函数需要能够就地对其进行修改。

    • 值语义避免混叠情况,在此情况下,此处的赋值会意外更改其值。
    • 牺牲一点速度来清理干净的语义并不是一件容易的事,有时通过值传递小的结构实际上会更有效,因为它避免了高速缓存未命中或堆分配。
    • 因此,Go Wiki的代码审查注释页建议在结构较小且可能保持这种状态时按值传递。
    • 如果“大”临界值似乎含糊,那就是;可以说,许多结构都在指针或值确定的范围内。作为下限,代码审查注释建议切片(三个机器字)可以合理地用作值接收者。接近上限时,bytes.Replace需要使用10个单词的args(三个切片和一个int)。
  • 对于slices,您不需要传递指针来更改数组的元素。例如,io.Reader.Read(p []byte)更改的字节p。可以说这是“对待像值一样的小结构”的特例,因为在内部,您正在传递一个称为切片头的小结构(请参阅Russ Cox(rsc)的说明)。同样,您也不需要指针来修改地图或在channel上进行通信

  • 对于切片,您将进行切片(更改其开始/长度/容量),诸如append接受切片值并返回新值的内置函数。我会模仿的;它避免了混淆,返回一个新的分片有助于引起人们注意可能分配了一个新数组的事实,并且调用者对此很熟悉。

    • 遵循这种模式并不总是可行的。一些工具,例如数据库接口序列化器,需要追加到在编译时类型未知的片上。他们有时会接受指向interface{}参数中切片的指针。
  • 映射,通道,字符串以及函数和接口值(例如切片)是内部引用或已经包含引用的结构,因此,如果您只是想避免复制基础数据,则无需将指针传递给它们。(rsc 撰写了有关如何存储接口值的单独文章)。

    • 在极少数情况下,您可能仍需要传递指针以修改调用者的结构:例如,出于这个原因,flag.StringVar需要使用a *string

使用指针的位置:

  • 考虑您的函数是否应该是您需要指向的任何结构上的方法。人们期望有很多方法可以x进行修改x,因此使接收器成为修改后的结构可能有助于最大程度地减少意外。对于何时应该将接收者作为指针有一些指导

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

  • 您提到接受一个指针,以允许通过重用避免分配。为了内存重用而更改API是一种优化,我会延迟直到明显知道分配费用不菲,然后再寻找一种不会对所有用户强制使用棘手API的方法:

    1. 为了避免分配,Go的转义分析是您的朋友。您有时可以通过创建可以用平凡的构造函数,纯文本或有用的零值初始化的类型来帮助避免堆分配bytes.Buffer
    2. 考虑一些Reset()将对象放回空白状态的方法,例如某些stdlib类型提供的方法。不在乎或无法保存分配的用户不必调用它。
    3. 为方便起见,existingUser.LoadFromJSON(json []byte) error可以考虑编写就地修改方法和从头创建函数作为匹配对,以方便使用:可以用来包装NewUserFromJSON(json []byte) (*User, error)。再次,它在懒惰和捏分配给单个呼叫者之间做出选择。
    4. 寻求回收内存的调用者可以让他们sync.Pool处理一些细节。如果特定的分配产生了很大的内存压力,您有信心知道何时不再使用该分配,并且没有更好的优化方法可以提供sync.Pool帮助。(CloudFlare发表sync.Pool关于回收的有用(上)博客文章。)

最后,关于切片是否应该是指针:值切片可以很有用,并且可以节省分配和缓存未命中。可能有阻止者:

  • 用于创建商品的API可能会强制您使用指针,例如,您必须调用NewFoo() *Foo而不是让Go初始化为零值
  • 这些项目的期望寿命可能不尽相同。整个切片立即被释放;如果99%的项目不再有用,但您有指向其他1%的指针,则所有数组均保持分配状态。
  • 四处移动物品可能会导致您遇到问题。值得注意的是,append增长基础数组时会复制项目。指向append错误位置之后的指针,对于庞大的结构,复制可能会变慢,例如,sync.Mutex不允许复制。在中间插入/删除并类似地移动项目。

从广义上讲,如果您将所有物品放在前面就位并且不移动它们(例如,append在初始设置后不再移动),或者如果您确实在不断移动它们,但是您确定可以(无需/小心使用指向项目的指针,项目足够小以至于无法有效复制等)。有时您必须考虑或衡量具体情况,但这只是一个粗略的指导。


12
什么是大结构?有没有一个大结构和小结构的例子?
不戴帽子的用户

1
你怎么知道字节。在amd64上替换需要80个字节的args?
Tim Wu

2
签名是Replace(s, old, new []byte, n int) []byte; s,old和new分别为3个字(slice头为(ptr, len, cap)),并且n int是1个字,所以10个字,即8个字节/字为80个字节。
twotwotwo

6
您如何定义大结构?有多大?
安迪·阿尔多

3
@AndyAldo我的所有资料(代码审查注释等)都没有定义阈值,因此我决定说这是一个判断电话,而不是提高阈值。三个词(像一个切片)在stdlib中被一致认为是合格的。我刚刚找到了一个五字值接收器的实例(text / scanner.Position),但是我读不到太多(它也作为指针传递了!)。如果没有基准测试等,我只会做一些看起来最方便阅读的事情。
twotwotwo

10

您想将方法接收器用作指针的三个主要原因:

  1. “首先,也是最重要的一点,该方法需要修改接收方吗?如果需要,则接收方必须是指针。”

  2. “第二是效率的考虑。如果接收器很大,例如一个大型结构,则使用指针接收器会便宜得多。”

  3. “下一个是一致性。如果该类型的某些方法必须具有指针接收器,则其余的方法也应如此,因此无论如何使用该类型,方法集都是一致的”

参考:https : //golang.org/doc/faq#methods_on_values_or_pointers

编辑:另一个重要的事情是要知道要发送给功能的实际“类型”。类型可以是“值类型”或“引用类型”。

即使切片和地图用作引用,我们也可能希望在诸如更改函数中切片长度的情况下将它们作为指针传递。


1
对于2,截止点是什么?我怎么知道我的结构是大还是小?另外,是否有一个足够小的结构使得使用值而不是指针有效(这样就不必从堆中引用它了)?
zlotnika

我要说的是,内部字段和/或嵌套结构的数量越多,该结构就越大。我不确定是否有特定的临界值或标准方法来知道何时可以将结构称为“大”或“大”。如果我正在使用或创建一个结构,我会根据上面所说的知道它的大小。但这就是我!
桑托什·皮莱

2

通常,在构造某些有状态或可共享资源实例时,通常需要返回一个指针。这通常是通过以开头的函数来完成的。New

因为它们表示某事物的特定实例,并且可能需要协调某些活动,所以生成表示相同资源的重复/复制结构没有多大意义-因此返回的指针充当资源本身的句柄。

一些例子:

在其他情况下,仅由于结构可能太大而无法默认复制而返回指针:


另外,也可以通过直接返回内部包含指针的结构的副本来避免直接返回指针,但这也许不被认为是惯用的:


在此分析中隐含的是,默认情况下,结构是按值复制(但不一定是它们的间接成员)。
nobar

2

如果可以(例如,不需要传递作为参考的非共享资源),请使用一个值。由于以下原因:

  1. 您的代码将更好,更易读,避免了指针运算符和null检查。
  2. 您的代码将更安全地防止Null Pointer崩溃。
  3. 您的代码通常会更快:是的,更快!为什么?

原因1:您将在堆栈中分配较少的项目。从堆栈中分配/取消分配是立即进行的,但是在堆上分配/取消分配可能会非常昂贵(分配时间+垃圾回收)。您可以在此处看到一些基本数字:http : //www.macias.info/entry/201802102230_go_values_vs_references.md

原因2:尤其是如果您将返回的值存储在切片中,则内存对象将在内存中更加紧凑:循环遍历所有项都是连续的切片比遍历遍历所有项都是指向内存其他部分的指针的切片要快得多。不是用于间接步骤,而是用于增加高速缓存未命中率。

误区:典型的x86缓存行为64字节。大多数结构都比那个小。在内存中复制高速缓存行的时间与复制指针相似。

仅当代码的关键部分慢时,我才会尝试进行一些微优化,并检查使用指针是否在某种程度上提高了速度,但代价是可读性和可维护性较低。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.