当前位置: 首页 > 知识库问答 >
问题:

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

毛宏达
2023-03-14

在Go中,有多种方法可以返回< code>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中使用的模式)?

共有3个答案

宿嘉庆
2023-03-14

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

>

  • “首先,也是最重要的,该方法是否需要修改接收器?如果需要,接收器必须是指针。”

    “其次是效率的考虑。如果接收器很大,例如大型结构,使用指针接收器会便宜得多。

    “接下来是一致性。如果该类型的一些方法必须有指针接收器,那么其余的也应该有,这样,无论如何使用该类型,方法集都是一致的

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

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

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

  • 昌山
    2023-03-14

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

      < li >您的代码将更好,可读性更强,避免了指针操作符和空检查。 < li >您的代码将更安全,不会出现空指针混乱。 < li >您的代码通常会更快:是的,更快!为什么?

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

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

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

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

    韩阳成
    2023-03-14

    TL;博士:

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

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

    • 接收器比其他参数更频繁地成为指针。方法修改调用它们的内容或命名类型是大型结构并不罕见,因此指南是默认使用指针,除非在极少数情况下。
      • Jeff Hodges的copyfighter工具会自动搜索按值传递的非微型接收器。

      一些不需要指针的情况:

      >

    • 代码审查指南建议将<Code>类型的Point struct{纬度,经度float64}</Code>之类的小结构作为值传递,甚至可能更大一些,除非您调用的函数需要能够就地修改它们。

        < li >值语义避免了别名情况,即这里的赋值意外地改变了那里的值。 < li >通过值传递小结构可以通过避免缓存未命中或堆分配来提高效率。在任何情况下,当指针和值表现相似时,Go-y方法是选择任何提供更自然语义的,而不是挤出最后一点速度。 < li >因此,Go Wiki的代码审查评论页面建议当结构很小并且可能保持这种方式时,通过值传递。 < li >如果“大”截止值看起来模糊不清,那就是;可以说,许多结构都在一个范围内,指针或值都可以。作为一个下限,代码评审注释建议将切片(三个机器单词)用作值接收器是合理的。作为更接近上限的东西,< code >字节。Replace需要相当于10个单词的参数(三个片和一个< code>int)。您可以找到这样的情况,即使复制大型结构也能获得性能优势,但经验法则是不要这样做。< br>

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

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

      • 遵循这种模式并不总是实用的。某些工具(如数据库接口或序列化程序)需要追加到在编译时类型未知的切片。它们有时接受指向接口{}参数中的切片的指针。

      映射、通道、字符串、函数和接口值(如切片)都是内部引用或已经包含引用的结构,因此如果您只是试图避免复制底层数据,则无需传递指向它们的指针。(rsc单独写了一篇关于如何存储接口值的文章)。

        < li >在极少数情况下,您可能仍需要传递指针来修改调用方的struct: 标志。例如,由于这个原因,StringVar接受一个< code>*string。

      使用指针的地方:

      >

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

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

      您提到接受指针以通过允许重用来避免分配;为了内存重用而更改 API 是一种优化,我会延迟,直到很明显分配具有不平凡的成本,然后我会寻找一种不会将更棘手的 API 强加给所有用户的方法:

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

      最后,关于你的切片是否应该是指针:值的切片可能是有用的,并且节省你的分配和缓存缺失。可能会有阻止者:

        < li >创建您的项目的API可能会对您强制使用指针,例如,您必须调用< code>NewFoo() *Foo而不是用零值初始化。 < li >项目的预期寿命可能不尽相同。整个切片立即被释放;如果99%的项不再有用,但是你有指向另外1%的指针,那么所有的数组都是分配的。 < li >复制或移动值可能会导致性能或正确性问题,使指针更有吸引力。值得注意的是,< code>append在增大基础数组时会复制项。在< code>append之前指向切片项的指针可能不会指向该项在之后被复制的位置,对于大型结构,复制可能会比较慢,例如对于< code>sync。互斥复制是不允许的。中间的插入/删除和排序也会移动项目,因此可以应用类似的考虑。

      一般来说,如果您将所有项目放在前面并且不移动它们(例如,在初始设置后不再添加appends),或者如果您继续移动它们,但您确信这没问题(没有/小心使用指向项目的指针,项目很小,或者您已经测量了perf影响),值切片可能是有意义的。有时它会归结为更具体的情况,但这是一个粗略的指南。

     类似资料:
    • 问题内容: 在Go中,有多种方法可以返回值或其片段。对于个人,我已经看到: 我了解两者之间的区别。第一个返回该结构的副本,第二个返回指向在函数内创建的结构值的指针,第三个期望传入现有结构并覆盖该值。 我已经看到所有这些模式都可以在各种情况下使用,我想知道关于这些的最佳实践是什么。什么时候使用?例如,第一个可能适用于小型结构(因为开销很小),第二个适用于较大的结构。第三,如果您想提高存储效率,因为您

    • 2. 指针类型的参数和返回值 首先看以下程序: 例 23.1. 指针参数和返回值 #include <stdio.h> int *swap(int *px, int *py) { int temp; temp = *px; *px = *py; *py = temp; return px; } int main(void) { int i = 10, j = 20; int *p

    • 如果指针类型作为函数参数,修改形参会影响实参 不能将函数内的指向局部变量的指针作为返回值,函数结束指向空间会被释放 可以将函数内的局部变量作为返回值,本质是拷贝一份

    • 我在玩C语言中的函数指针只是为了学习。我尝试调用一个空函数,并将其结果设置为int。 我从中得到的结果是: 在对它进行了一点研究之后,我意识到main正在函数()中打印printf语句中的字符数。为什么会这样?这是预期产出吗?

    • 问题内容: 在Go中,如何将函数调用返回的值分配给指针? 考虑下面的示例,注意返回一个值(不是指针): 这些都失败了: 是否确实需要局部变量?那不会招致不必要的复制吗? 问题答案: 根据规范,必须使用局部变量。 要获取值的地址,调用函数必须将返回值复制到可寻址的内存中。有副本,但这不是多余的。 Go程序通常使用值。 有时在应用程序想要区分无值和其他时间值的情况下使用A。在SQL NULL和有效时间

    • 函数能够接收参数供自己使用,也可以返回零个或多个值(我们通常把返回多个值称为返回一组值)。相比与 C、C++、Java 和 C#,多值返回是 Go 的一大特性,为我们判断一个函数是否正常执行(参考 第 5.2 节)提供了方便。 我们通过 return 关键字返回一组值。事实上,任何一个有返回值(单个或多个)的函数都必须以 return 或 panic(参考 第 13 章)结尾。 在函数块里面,re