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

新手上路,请多包涵

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

原文由 Zef Hemel 发布,翻译遵循 CC BY-SA 4.0 许可协议

阅读 585
2 个回答

长话短说

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

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

  • 接收器 比其他参数更多地是指针。方法修改它们被调用的东西,或者命名类型是大型结构的情况并不少见,因此 指南是 默认为指针,除非在极少数情况下。

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

一些不需要指针的情况:

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

    • 值语义避免了别名情况,即这里的赋值意外地改变了那里的值。
    • 通过避免 缓存未命中 或堆分配,按值传递小结构可以更有效。在任何情况下,当指针和值执行 相似 时,Go-y 方法是选择提供更自然语义的任何东西,而不是挤出最后一点速度。
    • 因此,Go Wiki 的 代码审查评论 页面建议在结构较小且可能保持这种状态时按值传递。
    • 如果“大”的界限看起来很模糊,那确实是;可以说,许多结构都在指针或值都可以的范围内。作为下限,代码审查意见建议将切片(三个机器字)用作值接收器是合理的。作为接近上限的东西, bytes.Replace 需要 10 个字的参数(三个切片和一个 int )。您会发现复制大型结构会带来性能提升的 情况,但经验法则并非如此。
  • 对于 slice ,您不需要传递指针来更改数组的元素。 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 的 逃逸分析 是你的朋友。有时,您可以通过创建可以使用普通构造函数、普通文字或有用的零值(如 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 复制是不允许的。在中间插入/删除和排序也会移动项目,因此可以应用类似的注意事项。

从广义上讲,如果您预先将所有项目放在适当的位置并且不移动它们(例如,在初始设置后不再有 append s),或者如果您继续移动它们,则值切片可能有意义周围,但您确信这没问题(没有/小心使用指向项目的指针,并且项目很小或者您已经测量了性能影响)。有时它归结为更具体的情况,但这是一个粗略的指南。

原文由 twotwotwo 发布,翻译遵循 CC BY-SA 4.0 许可协议

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

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

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

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

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

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

原文由 Mario 发布,翻译遵循 CC BY-SA 4.0 许可协议

撰写回答
你尚未登录,登录后可以
  • 和开发者交流问题的细节
  • 关注并接收问题和回答的更新提醒
  • 参与内容的编辑和改进,让解决方法与时俱进
推荐问题