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

Zef*_*mel 295 pointers go

在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
}
Run Code Online (Sandbox Code Playgroud)

我理解这些之间的差异.第一个返回结构的副本,第二个返回指向函数内创建的结构值的指针,第三个期望传入现有结构并覆盖该值.

我已经看到所有这些模式都在各种环境中使用,我想知道关于这些模式的最佳实践是什么.你什么时候用哪个?例如,第一个可能适用于小结构(因为开销很小),第二个适用于较大结构.第三个是你想要非常高效的内存,因为你可以在调用之间轻松地重用一个struct实例.有什么时候使用哪种最佳做法?

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

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} }
}
Run Code Online (Sandbox Code Playgroud)

再说一次:这里的最佳做法是什么.我知道切片总是指针,所以返回指向切片的指针是没用的.但是,如果我返回一片struct值,一块指向结构的指针,我应该将指向切片的指针作为参数传入(Go App Engine API中使用的模式)吗?

two*_*two 359

tl;博士:

  • 使用接收器指针的方法很常见; 接收者的经验法则是 "如果有疑问,请使用指针".
  • 切片,映射,通道,字符串,函数值和接口值在内部使用指针实现,指向它们的指针通常是多余的.
  • 在其他地方,使用指向大结构或结构的指针,你必须改变,否则传递值,因为通过指针让事情变得惊讶是令人困惑的.

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

  • 接收器 比其他参数更频繁.方法修改它们被调用的东西,或者命名类型是大型结构,这种情况并不罕见,因此除少数情况外,指导默认为指针.
    • Jeff Hodges的copyfighter工具自动搜索按值传递的非微小接收器.

在某些情况下,您不需要指针:

  • 代码审查指南建议通过小型结构类似type Point struct { latitude, longitude float64 },甚至可能的事情有点大,作为值,除非你调用需要的功能,能够修改他们在的地方.

    • 值语义避免了别名情况,在这种情况下,这里的赋值会突然改变那里的值.
    • Go-y不是以一点速度牺牲干净的语义,有时通过值传递小结构实际上更有效,因为它避免了缓存未命中或堆分配.
    • 因此,Go Wiki的代码审查评论页面建议在结构很小并且可能保持这种方式时按值传递.
    • 如果"大"截止看起来模糊不清,那就是; 可以说很多结构都在指针或值正常的范围内.作为下限,代码审查注释建议切片(三个机器字)合理地用作值接收器.作为接近上限的东西,bytes.Replace需要10个字的args(三个切片和一个int).
  • 对于切片,您不需要传递指针来更改数组的元素.例如,io.Reader.Read(p []byte)更改字节数p.它可以说是一种"处理像结构一样的小结构"的特殊情况,因为在内部你会传递一个叫做切片标题的小结构(参见Russ Cox(rsc)的解释).同样,您不需要指针来修改地图或在通道上进行通信.

  • 对于切片,您将重新定义(更改开始/长度/容量),内置函数,如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处理一些细节.如果特定分配会产生很大的内存压力,那么您确信自己知道何时不再使用alloc,并且您没有更好的优化可用,sync.Pool可以提供帮助.(CloudFlare发布sync.Pool一篇关于回收的有用(预)博客文章.)
    5. 奇怪的是,对于复杂的构造函数,NewFoo() *Foo有时可以避免分配append.不是惯用的; 小心翼翼地在家里试一试.

最后,关于切片是否应该是指针:值的切片可能很有用,并保存分配和缓存未命中.可能有阻碍者:

  • 创建项目的API可能会强制指向您,例如,您必须调用append而不是让Go初始化为零值.
  • 物品的期望寿命可能不完全相同.整个切片立刻被释放; 如果99%的项目不再有用但你有指向另一个1%的指针,则所有数组仍然被分配.
  • 移动物品可能会导致您遇到问题.值得注意的是,sync.Mutex增长底层数组时复制项目.在append指向错误位置之前得到的指针,对于大型结构,复制可能较慢,并且例如type Point struct { latitude, longitude float64 }不允许复制.在中间插入/删除并类似地移动项目.

从广义上讲,如果您将所有物品放在前面并且不移动它们(例如,bytes.Replace在初始设置后不再有s),或者如果您继续移动它们但是您确定它们是好的(没有/小心使用指向项目的指针,项目足够小以便有效复制等).有时您必须考虑或衡量您的具体情况,但这是一个粗略的指导.

  • 什么意味着大结构?有一个大结构和一个小结构的例子吗? (12认同)
  • 你如何定义大结构?有多大? (6认同)
  • 签名是`Replace(s,old,new [] byte,n int)[] byte`; s,old和new是三个单词([slice headers是`(ptr,len,cap)`](http://research.swtch.com/godata)),`n int是一个单词,所以10个字,每个字八个字节为80个字节。 (2认同)
  • @AndyAldo我的所有资料(代码审查注释等)都没有定义阈值,因此我决定说这是一个判断性的电话,而不是提高阈值。三个词(如切片)在stdlib中被一致认为是合格的值。我刚刚找到了一个五字值接收器的实例(text / scanner.Position),但是我读不到太多(它也作为指针传递了!)。在没有基准测试等情况下,我只会做一些对可读性最方便的事情。 (2认同)

Mar*_*rio 34

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

  1. 您的代码将变得更好、更具可读性,避免指针运算符和空检查。
  2. 您的代码将更安全地防止空指针恐慌。
  3. 您的代码通常会更快:是的,更快!为什么?

原因 1:您将在堆中分配较少的项目。从堆栈分配/释放是立即的,但是在堆上分配/释放可能非常昂贵(分配时间+垃圾收集)。您可以在这里看到一些基本数字:http://www.masias.info/entry/201802102230_go_values_vs_references.md

原因 2:特别是如果您将返回值存储在切片中,您的内存对象将在内存中更加紧凑:循环所有项目都是连续的切片比迭代所有项目都是指向内存其他部分的指针的切片要快得多。不是因为间接步骤,而是因为缓存未命中的增加。

打破神话:典型的 x86 缓存行为 64 字节。大多数结构都比这个小。在内存中复制高速缓存行的时间与复制指针类似。

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


小智 10

当您想要将方法接收器用作指针时,有三个主要原因:

  1. "首先,最重要的是,该方法是否需要修改接收器?如果是,接收器必须是指针."

  2. "其次是对效率的考虑.如果接收器很大,例如一个大的结构,使用指针接收器会便宜得多."

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

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

编辑:另一个重要的事情是要知道您要发送到函数的实际"类型".类型可以是"值类型"或"引用类型".见下图:

在此输入图像描述

即使切片和贴图充当引用,我们也可能希望将它们作为指针传递给场景,例如更改函数中切片的长度.

  • 对于 2,截止日期是多少?我怎么知道我的结构是大还是小?另外,是否有一个结构足够小,以便使用值而不是指针更高效(这样就不必从堆中引用它)? (2认同)

nob*_*bar 9

通常需要返回指针的情况是在构造某些有状态或可共享资源的实例时。这通常是由前缀为 的函数完成的。New

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

一些例子:

在其他情况下,返回指针只是因为结构可能太大而无法默认复制:


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