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

时间:2014-05-08 13:21:42

标签: 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
}

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

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

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

5 个答案:

答案 0 :(得分:325)

tl; dr

  • 使用接收器指针的方法很常见; the rule of thumb for receivers is,"如果有疑问,请使用指针。"
  • 切片,贴图,通道,字符串,函数值和接口值在内部使用指针实现,指向它们的指针通常是多余的。
  • 在其他地方,使用大型结构或结构的指针,你必须改变,否则pass values,因为通过指针让事情发生变化令人困惑。

 

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

  • 接收器是比其他参数更频繁的指针。修改他们要调用的东西或者命名类型是大型结构的方法并不罕见,所以the guidance is默认为指针,除非在极少数情况下。
    • Jeff Hodges' copyfighter工具会自动搜索按值传递的非微型接收器。

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

  • 代码审核指南建议传递小结构,如type Point struct { latitude, longitude float64 },甚至可能更大一些,作为值,除非您调用的函数需要是能够就地修改它们。

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

  • 对于切片,您需要重新启动(更改开始/长度/容量),内置函数(如append)接受切片值并返回新值一。我模仿那个;它避免了别名,返回一个新的切片有助于引起人们对可能分配新数组的事实的注意,并且它对调用者来说很熟悉。

    • 并不总是遵循这种模式。某些工具(例如database interfacesserializers)需要附加到在编译时类型未知的切片。它们有时会接受指向interface{}参数中切片的指针。
  • 地图,频道,字符串以及功能和界面值,就像切片一样,是内部引用或已包含引用的结构,因此,如果您只是想避免获取复制的基础数据,您不需要将指针传递给它们。 (rsc wrote a separate post on how interface values are stored)。

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

使用指针的地方:

  • 考虑您的函数是否应该是您需要指向哪个结构的方法。人们期望x上有很多方法可以修改x,因此将修改后的结构体作为接收器可以帮助减少意外。接收器应该是指针时有guidelines

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

  • 您提到通过允许重用来接受指针以避免分配;为了记忆重用而改变API是一种优化,我要延迟,直到它清楚分配有一个非常重要的成本,然后我寻找一种不会强迫所有用户都使用棘手的API:

    1. 为避免分配,Go escape analysis是您的朋友。您有时可以通过创建可以使用普通构造函数,普通文字或有用的零值(如bytes.Buffer)初始化的类型来帮助它避免堆分配。
    2. 考虑使用Reset()方法将对象放回空白状态,就像一些stdlib类型提供的那样。不关心或无法保存分配的用户不必致电。
    3. 为方便起见,考虑编写就地修改方法和从头开始创建作为匹配对:existingUser.LoadFromJSON(json []byte) error可以包裹NewUserFromJSON(json []byte) (*User, error)。同样,它推动了懒惰和捏合分配到个人来电者之间的选择。
    4. 寻求回收内存的来电者可以让sync.Pool处理一些细节。如果某个特定的分配产生了很大的内存压力,那么您确信自己知道何时不再使用alloc,并且您没有更好的优化可用,sync.Pool可以提供帮助。 (CloudFlare发布了有关回收的a useful (pre-sync.Pool) blog post。)
    5. 奇怪的是,对于复杂的构造函数,new(Foo).Reset()有时可以避免在NewFoo()不匹配时进行分配。不是惯用的;小心翼翼地在家里试一试。

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

  • 用于创建项目的API 可能会强制指向您,例如您必须致电NewFoo() *Foo而不是让Go使用zero value进行初始化。
  • 项目的所需生命周期可能不完全相同。整个切片立刻被释放;如果99%的项目不再有用,但你有指向其他1%的项目,则所有数组仍然被分配。
  • 移动项目可能会导致您遇到问题。值得注意的是,append会在grows the underlying array时复制项目。你在append指向错误位置之前得到的指针,对于巨大的结构,复制可能会更慢,例如sync.Mutex复制不允许。在中间插入/删除并按类似方式移动项目。

从广义上讲,如果您将所有物品预先放在原地并且不移动它们(例如,在初始设置后不再append s),或者如果您这样做,那么价值切片就有意义继续移动他们,但你确定没关系(没有/小心使用指向项目的指针,项目足够小,可以有效地复制,等等)。有时您必须考虑或衡量您的具体情况,但这是一个粗略的指南。

答案 1 :(得分:8)

要使用方法接收器作为指针的三个主要原因:

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

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

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

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

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

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

答案 2 :(得分:1)

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

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

一些例子:

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


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

答案 3 :(得分:0)

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

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

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

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

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

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

答案 4 :(得分:0)

关于结构与指针返回值,在阅读了github上许多备受瞩目的开源项目后,我感到困惑,因为这两种情况都有很多例子,util我发现了这篇很棒的文章: https://www.ardanlabs.com/blog/2014/12/using-pointers-in-go.html

“一般来说,使用指针共享结构类型值,除非结构类型已被实现为表现得像原始数据值。

如果您仍然不确定,这是另一种思考方式。将每个结构视为具有性质。如果结构的性质是不应该改变的,比如时间、颜色或坐标,那么将结构实现为原始数据值。如果结构的性质是可以改变的,即使它从来不在你的程序中,它也不是原始数据值,应该实现为与指针共享。不要创建具有二元性的结构。”

完全相信。