ChatGPT解决这个技术问题 Extra ChatGPT

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

在 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 缓存中取消引用指针的速度与从主内存中取消引用它们的速度有很大不同。

t
twotwotwo

tl;博士:

使用接收器指针的方法很常见;接收者的经验法则是,“如果有疑问,请使用指针。”

切片、映射、通道、字符串、函数值和接口值在内部使用指针实现,指向它们的指针通常是多余的。

在其他地方,对大结构或必须更改的结构使用指针,否则传递值,因为通过指针意外更改内容会令人困惑。

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

接收者比其他参数更常见的是指针。方法修改它们被调用的东西,或者命名类型是大型结构的情况并不罕见,因此除非在极少数情况下,否则指导默认为指针。 Jeff Hodges 的 copyfighter 工具会自动搜索按值传递的非小型接收器。

Jeff Hodges 的 copyfighter 工具会自动搜索按值传递的非小型接收器。

一些不需要指针的情况:

代码审查指南建议将类型 Point struct { latitude, longitude float64 } 之类的小结构,甚至可能更大的东西作为值传递,除非您调用的函数需要能够就地修改它们。值语义避免了这里的赋值意外地改变了那里的值的别名情况。通过避免缓存未命中或堆分配,按值传递小结构可以更有效。在任何情况下,当指针和值执行相似时,Go-y 方法是选择提供更自然语义的任何内容,而不是挤出最后一点速度。因此,Go Wiki 的代码审查评论页面建议在结构很小并且可能保持这种方式时按值传递。如果“大”截止看起来很模糊,那就是;可以说,许多结构都在一个指针或值都可以的范围内。作为下限,代码审查评论建议切片(三个机器字)用作值接收器是合理的。作为更接近上限的东西,bytes.Replace 需要 10 个单词的 args(三个切片和一个 int)。您会发现即使复制大型结构也会获得性能优势,但经验法则并非如此。

值语义避免了这里的赋值意外地改变了那里的值的别名情况。

通过避免缓存未命中或堆分配,按值传递小结构可以更有效。在任何情况下,当指针和值执行相似时,Go-y 方法是选择提供更自然语义的任何内容,而不是挤出最后一点速度。

因此,Go Wiki 的代码审查评论页面建议在结构很小并且可能保持这种方式时按值传递。

如果“大”截止看起来很模糊,那就是;可以说,许多结构都在一个指针或值都可以的范围内。作为下限,代码审查评论建议切片(三个机器字)用作值接收器是合理的。作为更接近上限的东西,bytes.Replace 需要 10 个单词的 args(三个切片和一个 int)。您会发现即使复制大型结构也会获得性能优势,但经验法则并非如此。

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

对于切片,您将重新切片(更改开始/长度/容量),内置函数(如 append)接受切片值并返回一个新值。我会模仿那个;它避免了别名,返回一个新切片有助于引起人们对可能分配新数组的注意,并且调用者熟悉它。遵循这种模式并不总是可行的。数据库接口或序列化程序等一些工具需要附加到在编译时类型未知的切片。它们有时会在 interface{} 参数中接受指向切片的指针。

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

映射、通道、字符串以及函数和接口值(如切片)是内部引用或已经包含引用的结构,因此如果您只是想避免复制底层数据,则不需要将指针传递给它们. (rsc 写了一篇关于如何存储接口值的单独帖子)。在极少数情况下,您可能仍需要传递指针来修改调用者的结构:例如,flag.StringVar 出于这个原因需要一个 *string。

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

在哪里使用指针:

考虑您的函数是否应该是您需要指针指向的任何结构的方法。人们期望 x 上有很多方法来修改 x,因此将修改后的结构设为接收器可能有助于减少意外。有关于何时接收者应该是指针的指南。

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

您提到通过允许重用来接受指针以避免分配;为了内存重用而更改 API 是一种优化,我会延迟直到明确分配具有不小的成本,然后我会寻找一种不会将更棘手的 API 强加给所有用户的方法:为了避免分配, Go 的逃生分析是你的朋友。您有时可以通过创建可以使用普通构造函数、普通文字或有用的零值(如 bytes.Buffer)初始化的类型来帮助它避免堆分配。考虑使用 Reset() 方法将对象放回空白状态,就像某些 stdlib 类型提供的那样。不关心或无法保存分配的用户不必调用它。考虑将 modify-in-place 方法和 create-from-scratch 函数编写为匹配对,为方便起见:existingUser.LoadFromJSON(json []byte) 错误可以由 NewUserFromJSON(json []byte) (*User, error) 包装。同样,它将懒惰和挤压分配之间的选择推给了单个调用者。寻求回收内存的调用者可以让 sync.Pool 处理一些细节。如果一个特定的分配造成了很大的内存压力,你确信你知道什么时候不再使用分配,并且你没有更好的优化可用,sync.Pool 可以提供帮助。 (CloudFlare 发布了一篇关于回收的有用(pre-sync.Pool)博客文章。)

为了避免分配,Go 的逃逸分析是你的朋友。您有时可以通过创建可以使用普通构造函数、普通文字或有用的零值(如 bytes.Buffer)初始化的类型来帮助它避免堆分配。

考虑使用 Reset() 方法将对象放回空白状态,就像某些 stdlib 类型提供的那样。不关心或无法保存分配的用户不必调用它。

考虑将 modify-in-place 方法和 create-from-scratch 函数编写为匹配对,为方便起见:existingUser.LoadFromJSON(json []byte) 错误可以由 NewUserFromJSON(json []byte) (*User, error) 包装。同样,它将懒惰和挤压分配之间的选择推给了单个调用者。

寻求回收内存的调用者可以让 sync.Pool 处理一些细节。如果一个特定的分配产生了很大的内存压力,你确信你知道什么时候不再使用分配,并且你没有更好的优化可用,sync.Pool 可以提供帮助。 (CloudFlare 发布了一篇关于回收的有用(pre-sync.Pool)博客文章。)

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

创建项目的 API 可能会强制指向您,例如,您必须调用 NewFoo() *Foo 而不是让 Go 用零值初始化。

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

移动值可能会导致性能或正确性问题,使指针更具吸引力。值得注意的是,append 会在底层数组增长时复制项目。您在附加之前获得的指针指向错误的位置,对于巨大的结构,复制可能会更慢,例如 sync.Mutex 复制是不允许的。在中间插入/删除和排序类似地移动项目。

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


什么是大结构?有大结构和小结构的例子吗?
你怎么知道 bytes.Replace 在 amd64 上需要 80 个字节的参数?
签名是Replace(s, old, new []byte, n int) []byte; s、old 和 new 各是三个字(slice headers are (ptr, len, cap)),而 n int 是一个字,所以 10 个字,即 8 个字节/字是 80 个字节。
你如何定义大结构?大有多大?
@AndyAldo 我的任何来源(代码审查评论等)都没有定义阈值,所以我决定说这是一个判断调用而不是设置阈值。三个单词(如切片)一直被视为有资格成为标准库中的值。我刚刚找到了一个五字值接收器的实例(text/scanner.Position),但我不会读太多(它也是作为指针传递的!)。没有基准等,我只会做任何看起来最方便可读的事情。
M
Mario

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

您的代码将更好,更易读,避免指针运算符和空检查。您的代码将更安全地抵御空指针恐慌。您的代码通常会更快:是的,更快!为什么?

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

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

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

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


>原因1:您将在堆栈中分配更少的项目。您的意思是您将在堆中分配更少的项目吗?
是的,谢谢指正!
谢谢你。这是第一个提到堆/堆栈的答案,这是一个很大的区别。
I
Inanc Gumus

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

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

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

编辑:另一件重要的事情是了解您发送到功能的实际“类型”。类型可以是“值类型”或“引用类型”。

即使切片和映射充当引用,我们也可能希望在更改函数中切片长度等场景中将它们作为指针传递。


对于 2,截止点是多少?我怎么知道我的结构是大还是小?此外,是否有一个足够小的结构,以便使用值而不是指针更有效(这样就不必从堆中引用它)?
我会说里面的字段和/或嵌套结构的数量越多,结构就越大。我不确定是否有特定的截止或标准方法可以知道何时可以将结构称为“大”或“大”。如果我正在使用或创建一个结构,我会根据我上面所说的知道它的大小。但这只是我!
B
Brent Bradburn

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

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

一些例子:

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

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

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

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

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

在标准库中没有找到这样的例子......

相关问题:用指针或值嵌入Go


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

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

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

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

完全相信。