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

2023-12-04

在 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 应用引擎 API)?


tl;dr:

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

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

  • Receivers are pointers more often than other arguments. It's not unusual for methods to modify the thing they're called on, or for named types to be large structs, so the guidance is to default to pointers except in rare cases.
    • 杰夫·霍奇斯抄袭者工具自动搜索按值传递的非小型接收器。

有些情况不需要指针:

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

    • 值语义避免出现别名情况,即此处的赋值意外地更改了此处的值。
    • 通过避免按值传递小结构可以更有效缓存未命中或堆分配。无论如何,当指针和值执行时相似地,Go-y 方法是选择任何提供更自然语义的方法,而不是榨干最后一点速度。
    • 所以,Go Wiki 的代码审查意见页面建议当结构很小并且可能保持这种状态时按值传递。
    • 如果“大”界限看起来很模糊,那么事实确实如此;可以说,许多结构都在指针或值都可以的范围内。作为下限,代码审查评论表明切片(三个机器字)可以合理地用作值接收器。作为接近上限的东西,bytes.Replace需要 10 个单词的参数(三个切片和一个int)。你可以找到情况即使复制大型结构也会带来性能优势,但经验法则并非如此。
  • For slices,您不需要传递指针来更改数组的元素。io.Reader.Read(p []byte)改变的字节p, 例如。这可以说是“像对待值一样对待小结构”的特殊情况,因为在内部你正在传递一个称为 a 的小结构片头 (see 拉斯·考克斯 (rsc) 的解释)。同样,您不需要指向修改地图或在频道上进行交流.

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

    • 遵循这种模式并不总是可行的。一些工具,例如数据库接口 or 序列化器需要附加到编译时类型未知的切片。他们有时接受指向切片的指针interface{}范围。
  • 映射、通道、字符串以及函数和接口值与切片一样,是内部引用或已包含引用的结构,因此如果您只是想避免复制底层数据,则无需将指针传递给它们。 (RSC写了一篇关于如何存储接口值的单独文章).

    • 在极少数情况下,您可能仍然需要传递指针modify调用者的结构: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 发布一个有用的(预sync.Pool)博客文章关于回收。)

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

  • 用于创建您的项目的 API可能会将指针强加给您,例如你必须打电话NewFoo() *Foo而不是让 Go 初始化零值.
  • 物品的预期寿命可能并不都是一样的。整个切片立即被释放;如果 99% 的项目不再有用,但您有指向其他 1% 的指针,则所有数组仍保持分配状态。
  • 复制或移动值可能会导致性能或正确性问题,从而使指针更具吸引力。尤其,append复制项目时增长底层数组。指向之前切片项目的指针append可能不会指向项目复制后的位置,对于巨大的结构,复制可能会更慢,例如sync.Mutex不允许复制。在中间插入/删除和排序也会移动项目,因此可以应用类似的考虑因素。

从广义上讲,如果您预先将所有物品就位并且不移动它们(例如,不再移动它们),则价值切片是有意义的append初始设置后),或者如果您确实不断移动它们但您确信没关系(不/小心使用指向项目的指针,并且项目很小或者您已经测量了性能影响)。有时这取决于您的具体情况,但这只是一个粗略的指南。

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

参数和返回值中的指针与值 的相关文章

  • C++ 中如何检查指针是否仍然指向有效内存?

    我有一个指针等于另一个指针 我想检查我的指针是否等于不为空的指针 int ptr0 new int 5 int ptr1 ptr0 delete ptr0 if std cout lt lt ptr1 equals to a null pt
  • pprof 和 ps 之间的内存使用差异

    我一直在尝试分析用 cobra 构建的 cli 工具的堆使用情况 这pprof工具显示如下 Flat Flat Sum Cum Cum Name Inlined 1 58GB 49 98 49 98 1 58GB 49 98 os Read
  • 指针上定义的方法仍然可以用值调用

    Effective Go 文档说明如下 关于接收者的指针与值的规则是 可以在指针和值上调用值方法 但只能在指针上调用指针方法 http tip golang org doc effective go html pointers vs val
  • C语言中的array、&array、&array[0]有什么区别? [复制]

    这个问题在这里已经有答案了 在学习C语言中的数组和指针时 我很困惑 为什么ch ch ch 0 彼此相等 而sptr sptr sptr 0 却不相等 这是我的源代码 int main void char ch 7 1 2 3 4 5 6
  • C++ 中的指针递增

    这意味着什么 指针增量指向指针的下一个基类型的地址 例如 p1 p1 is a pointer to an int 这个语句是否意味着指向的地址p1应该更改为下一个地址int或者它应该只增加 2 假设int是 2 个字节 在这种情况下 特定
  • 为什么 DER ASN.1 大整数的解组在 Golang 中仅限于 SEQUENCE?

    我希望能够使用encoding asn1 包从 DER 文件中解组一个大整数 但它看起来只适用于整数序列 例如 这不起作用 这很奇怪 因为 Big Int 的编组效果很好 https play golang org p Wkj0jAA6bp
  • 如何对结构切片而不是切片结构进行范围调整

    稍微玩了一下 Go HTML 模板后 我发现的所有循环模板中对象的示例都是将切片结构传递给模板 有点像这个示例 type UserList struct Id int Name string var templates template M
  • Golang const unsafe.Sizeof

    不明白为什么我可以做到 const OK uint64 0 const OK int unsafe Sizeof uint64 0 但不是这个 const NOK binary Size uint64 0 它的解释在规格 https gol
  • 如何在golang中获得两个切片的交集?

    Go 中有没有有效的方法来获取两个切片的交集 我想避免嵌套 for 循环之类的解决方案slice1 string foo bar hello slice2 string foo bar intersection slice1 slice2
  • 函数指针声明语法混乱[重复]

    这个问题在这里已经有答案了 我已经阅读并搜索了有关解码函数指针的右左规则 For ex int fun one char double 9 20 is fun one 是指向函数的指针 需要 char double 和 返回指向 int 数
  • C 中的数组地址减法[重复]

    这个问题在这里已经有答案了 可能的重复 C 中的指针算术 https stackoverflow com questions 759663 pointer arithmetic in c Code int main int a 0 1 2
  • 在 C++ 中初始化指针

    可以在声明时将指针分配给值吗 像这样的东西 int p 1000 是的 您可以在声明时初始化指向值的指针 但是您不能这样做 int p 1000 是个地址运算符 并且您不能将其应用于常量 尽管如果可以 那会很有趣 尝试使用另一个变量 int
  • 如何在 Go 中使用与包同名的变量名?

    文件或目录的常见变量名称是 path 不幸的是 这也是 Go 中包的名称 此外 在 DoIt 中更改路径作为参数名称 如何编译此代码 package main import path os func main DoIt file txt f
  • Go 指针 - 通过指针将值附加到切片

    我有一个 struct ProductData 及其实例 p 它有一个切片属性 type ProductInfo struct TopAttributes map string interface 我想设置 TopAttributes 如下
  • 不同类型的指针可以互相分配吗?

    考虑到 T1 p1 T2 p2 我们可以将 p1 分配给 p2 或反之亦然吗 如果是这样 是否可以不使用强制转换来完成 或者我们必须使用强制转换 首先 让我们考虑不进行强制转换的分配 C 2018 6 5 16 1 1 列出了简单赋值的约束
  • C++ 返回指针/引用

    我对解引用运算符 运算符地址和一般指针有相当好的理解 然而 当我看到这样的东西时 我会感到困惑 int returnA int j a return j int returnB return b int returnC return c i
  • 引用/指针失效到底是什么?

    我找不到任何定义指针 引用无效在标准中 我问这个问题是因为我刚刚发现 C 11 禁止字符串的写时复制 COW 据我了解 如果应用了 COW 那么p仍然是一个有效的指针并且r以下命令后的有效参考 std string s abc std st
  • C++ 中 void(*)() 和 void(&)() 之间的区别[重复]

    这个问题在这里已经有答案了 在此示例代码中 func1是类型void int double and funky是类型void int double include
  • Go 中的数据竞争:为什么会在 10-11 毫秒以下发生?

    这是我运行的代码 package main import fmt time const delay 9 time Millisecond func main n 0 go func time Sleep delay n fmt Printl
  • 如何访问主包之外的标志?

    We 解析标志 http golang org pkg flag FlagSet Parse当然 在 main 包中的 main go 中 然后我们有另一个包 我们想在其中读取一些标志的值 flags Args http golang or

随机推荐