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

评论

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

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

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

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

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

#1 楼

tl; dr:

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


在一种情况下,您应该经常使用指针:


接收器比其他自变量更经常使用指针。方法修改被调用的东西或命名类型为大型结构的情况并不少见,因此指导原则是默认使用指针,除非极少数情况。
Jeff Hodges的copyfighter工具会自动搜索非-按值传递的微型接收器。




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


代码审查指南建议将较小的结构(例如type Point struct { latitude, longitude float64 })作为值传递,甚至可能将更大的值作为值传递,除非您调用的函数需要能够在适当的位置修改它们。

值语义避免出现别名情况在这里的赋值会意外地更改那里的值。
牺牲一点干净的语义来加快速度并不是一件容易的事,有时通过值传递小的结构实际上会更有效,因为它避免了缓存丢失或堆
因此,Go Wiki的代码审查注释页建议在结构较小且可能保持这种状态时按值传递。
如果“大”分界线似乎含糊,那就是;可以说,许多结构都在指针或值确定的范围内。作为下限,代码审查注释建议切片(三个机器字)可以合理地用作值接收者。接近上限时,bytes.Replace占用了10个字的args(三个切片和一个int)。您会发现即使复制大型结构也能赢得性能,但是经验法则并非如此。





对于切片,您不希望这样做。需要传递一个指针来更改数组的元素。例如,io.Reader.Read(p []byte)更改p的字节。可以说,这是“对待像值一样的小结构”的特例,因为在内部,您正在传递一个称为切片头的小结构(请参阅Russ Cox(rsc)的说明)。同样,您也不需要指针来修改地图或在通道上进行通信。


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

遵循这种模式并不总是可行的。一些工具,例如数据库接口或序列化器,需要追加到在编译时类型未知的片上。它们有时会在interface{}参数中接受指向切片的指针。




映射,通道,字符串以及函数和接口值(例如切片)是内部引用或结构,已经包含引用,因此,如果您只是试图避免复制基础数据,则无需将指针传递给它们。 (rsc撰写了有关如何存储接口值的单独文章)。

在极少数情况下,您可能仍需要传递指针以修改调用者的结构:flag.StringVar需要*string,例如。



使用指针的位置:


考虑是否应该将函数用作需要指针的结构上的方法。人们期望x上有很多方法可以修改x,因此使接收器具有修改后的结构可能有助于最大程度地减少意外。关于何时应该将接收者作为指针的准则。



对非接收者参数有影响的函数应该在godoc中或更清楚地指出godoc和名称(就像reader.WriteTo(writer))。


您提到接受一个指针,通过允许重用来避免分配;为了内存重用而更改API是一种优化,我会等到明确分配成本后再进行优化,然后再寻找一种不会对所有用户施加棘手API的方法:

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



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


用于创建商品的API可能会强制您使用指针,例如您必须调用NewFoo() *Foo而不是让Go初始化为零值。

项目的期望寿命可能并不完全相同。整个切片立即被释放;如果99%的项目不再有用,但您有指向其他1%的指针,则所有数组仍保持分配状态。

移动值可能会导致性能或正确性问题,使指针更具吸引力。值得注意的是,当append扩展基础数组时,它会复制项目。在append指向错误的位置之前获得的指针,对于大型结构(例如,对于sync.Mutex不允许复制。在中间插入/删除并类似地移动项目。

广泛地讲,如果您将所有项目都放在前面并且不移动它们(例如,不或在初始设置后继续移动它们,但您确定可以的话(不能/小心地使用指向项目的指针,项目足够小,可以有效地进行复制等)。有时您必须考虑或衡量具体情况,但这只是一个粗略的指导。

评论


什么是大结构?有没有一个大结构和小结构的例子?

–不戴帽子的用户
2015年3月20日在21:38



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

–提姆·吴
2015年6月12日,下午3:50

签名是Replace(s,old,new [] byte,n int)[] byte; s,old和new是三个单词(切片标头是(ptr,len,cap)),n int是一个单词,所以是10个单词,即8个字节/单词为80个字节。

–twotwotwo
15年6月12日在17:32

您如何定义大结构?有多大?

–安迪·阿尔多(Andy Aldo)
17年11月13日在4:14

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

–twotwotwo
17年11月13日在21:09

#2 楼

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


“首先,也是最重要的是,该方法需要修改接收器吗?如果是,则接收器必须是一个指针。”
“第二个要考虑效率。如果接收器很大,例如一个大的结构,则使用指针接收器会便宜得多。”
“下一个是一致性。如果某些方法类型的必须有指针接收器,其余的也应该有指针接收器,因此无论如何使用该类型,方法集都是一致的”。

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

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

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

评论


对于2,截止点是什么?我怎么知道我的结构是大还是小?另外,是否有一个足够小的结构,以便使用值而不是指针更有效(因此不必从堆中引用它)?

–zlotnika
18年11月17日在18:36



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

– Santosh Pillai
19年8月8日在11:19

#3 楼

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

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

一些示例:




func NewTLSServer(handler http.Handler) *Server-实例化用于测试的Web服务器

func Open(name string) (*File, error)-返回文件访问句柄

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



func NewRGBA(r Rectangle) *RGBA -在内存中分配图像


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


在标准库中找不到此类示例...
相关问题:在Go中嵌入尖角r或具有值



评论


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

–布伦特·布拉德本(Brent Bradburn)
19年9月4日在21:21

#4 楼

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


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

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

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

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

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