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

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

我明白它们之间的区别。第一个返回一个结构体的副本,第二个返回一个指向在函数中创建的结构体值的指针,第三个返回传入一个现有的结构体并覆盖该值。

我看到所有这些模式都在不同的环境中使用,我想知道关于这些的最佳实践是什么。什么时候用which?例如,第一种方法适用于小型结构(因为开销最小),第二种方法适用于大型结构。第三种方法用于提高内存效率,因为可以在调用之间轻松重用单个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} }
}

再说一遍:这里的最佳实践是什么。我知道片总是指针,所以返回指向片的指针是没有用的。然而,我是否应该返回一个结构值的切片,一个指向结构的指针的切片,是否应该传入一个指向切片的指针作为参数(在Go应用程序引擎API中使用的模式)?

126193 次浏览

博士tl;:

  • 使用接收器指针的方法很常见;对于接受者来说,经验法则是, "如果有疑问,使用指针。"
  • 切片、映射、通道、字符串、函数值和接口值都是通过内部指针实现的,指向它们的指针通常是多余的。
  • 在其他地方,对于大的结构体或必须更改的结构体使用指针,否则使用传递值,因为通过指针突然更改内容会令人困惑。

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

  • 接收器是比其他参数更多的指针。方法修改被调用的对象或命名类型为大型结构体并不罕见,因此指导方针是默认为指针,除非在极少数情况下
    • Jeff Hodges的copyfighter工具自动搜索通过值传递的非微小接收器

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

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

    • 值语义避免混淆情况,即这里的赋值意外地改变了那里的值。
    • 通过避免缓存错过或堆分配,可以更有效地通过值传递小结构。在任何情况下,当指针和值执行类似的时,Go-y方法是选择任何提供更自然语义的方法,而不是挤出最后一点速度。
    • 因此,Go Wiki的代码评审注释页面建议在结构很小并且可能保持这种方式时通过值传递。
    • 如果"large"界限似乎很模糊,的确如此;可以说,许多结构体都在一个指针或值都可以的范围内。作为下界,代码审查注释建议将切片(三个机器字)用作值接收器是合理的。作为一个更接近上限的值,bytes.Replace包含10个单词的参数(3个切片和一个int)。你可以找到的情况下,在那里即使复制大型结构体也会获得性能上的胜利,但经验法则不是这样
  • 对于,你不需要传递一个指针来改变数组的元素。例如,io.Reader.Read(p []byte)改变了p的字节。这可以说是“像对待值一样对待小结构体”的一种特殊情况。因为在内部传递的是一个名为切头的小结构(参见Russ Cox (rsc)的解释)。类似地,你不需要指向修改地图或在信道上通信的指针。

  • 对于切片后再切(更改的起始/长度/容量),内置函数如append接受一个切片值并返回一个新值。我会模仿;它避免了混叠,返回一个新的切片有助于提醒人们注意可能分配了一个新数组,并且它为调用者所熟悉。

    • 遵循这种模式并不总是可行的。一些工具,如数据库接口序列化器,需要附加到一个在编译时不知道类型的片。它们有时接受指向interface{}形参中的片的指针。
  • 映射、通道、字符串以及函数和接口值,像切片一样,是内部引用或结构,已经包含引用,所以如果你只是试图避免得到底层数据复制,你不需要传递指针给它们。(rsc 写了一篇关于如何存储接口值的单独文章)。

    • 在更罕见的情况下,您仍然可能需要传递指针,即您想要修改调用者的结构:例如,flag.StringVar为此接受*string

使用指针的地方:

  • 考虑你的函数是否应该是一个你需要指针指向的结构的方法。人们期望x上的许多方法可以修改x,因此将修改后的结构体作为接收者可能有助于最大限度地减少意外。当接收器应该是指针时,有的指导方针

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

  • 你提到接受一个指针,以避免分配,允许重用;为了内存重用而改变API是一种优化,我会推迟,直到明确分配有一个不小的成本,然后我会寻找一种不强迫所有用户使用更棘手的API的方法:

    1. 为了避免分配,Go的逸出分析是你的朋友。有时可以通过创建可以用普通构造函数、普通文字或有用的0值(如bytes.Buffer)初始化的类型来帮助它避免堆分配。
    2. 考虑Reset()方法将对象放回空白状态,就像一些stdlib类型提供的那样。不关心或不能保存分配的用户不必调用它。
    3. 为方便起见,考虑编写就地修改方法和从头创建函数作为匹配对:existingUser.LoadFromJSON(json []byte) error可以由NewUserFromJSON(json []byte) (*User, error)包装。同样,它将懒惰和压缩分配之间的选择推给单个调用者。
    4. 寻求回收内存的调用者可以让sync.Pool处理一些细节。如果一个特定的分配产生了很大的内存压力,你确信你知道什么时候不再使用这个分配,并且你没有更好的优化可用,sync.Pool可以提供帮助。(CloudFlare发布了关于回收的一个有用的(pre-sync.Pool)博客文章。)

最后,关于你的切片是否应该是指针:值切片是有用的,可以节省你的分配和缓存丢失。可以有阻碍:

  • 用于创建项目的API可能会强制指向你,例如,你必须调用NewFoo() *Foo而不是让Go用零值初始化。
  • 项目所需的生存期可能不完全相同。整个切片立即被释放;如果99%的元素不再有用,但你有指向其他1%的指针,那么整个数组仍然是分配的。
  • 复制或移动值可能会导致你的性能或正确性问题,使指针更有吸引力。值得注意的是,append增长底层数组。时复制项。在append之前指向切片项的指针可能不指向该项之后被复制的位置,对于大型结构的复制可能会更慢,并且对于例如sync.Mutex,复制是不允许的。在中间插入/删除和排序也会移动项目,因此可以应用类似的考虑。

一般来说,值切片是有意义的,如果你把所有的项都放在前面,不移动它们(例如,在初始设置后不再有__abc),或者如果你继续移动它们,但你确信这是可以的(没有/小心使用指向项的指针,并且项很小,或者你已经测量了性能影响)。有时候,这取决于你的具体情况,但这只是一个粗略的指南。

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

  1. 首先,也是最重要的,方法是否需要修改接收者?如果是,那么接收者必须是一个指针。”

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

  3. 其次是一致性。如果该类型的一些方法必须有指针接收器,那么其他方法也应该有,因此无论如何使用该类型,方法集都是一致的。”

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

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

即使切片和映射充当引用,我们也可能希望在某些情况下将它们作为指针传递,比如在函数中更改切片的长度。

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

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

一些例子:

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


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

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

  1. 您的代码将更好,更可读,避免指针操作符和空检查。
  2. 您的代码将更安全的对抗空指针恐慌。
  3. 你的代码通常会更快:是的,快!为什么?

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

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

神话断路器:典型的x86缓存线是64字节。大多数结构体都比这个小。在内存中复制高速缓存行的时间与复制指针的时间相似。

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

关于struct vs.指针返回值,在github上阅读了许多高度盯着的开源项目后,我感到困惑,因为这两种情况都有很多例子,util我发现了这篇惊人的文章: https://www.ardanlabs.com/blog/2014/12/using-pointers-in-go.html < / p >

通常,与指针共享结构类型值,除非该结构类型已被实现为行为类似于基本数据值。

如果你仍然不确定,这是另一种思考方式。每个结构都有自己的性质。如果结构体的性质是不可更改的,如时间、颜色或坐标,则将结构体实现为原始数据值。如果结构体的性质是可以更改的,即使它从未出现在您的程序中,它也不是原始数据值,应该实现为与指针共享。不要创建具有双重性的结构。”

完成信服。