小编典典

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

go

在 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} }
}

再说一遍:这里的最佳实践是什么。我知道切片总是指针,所以返回一个指向切片的指针是没有用的。但是,我是否应该返回一个结构值切片,一个指向结构的指针,我是否应该将指向切片的指针作为参数传递


阅读 210

收藏
2021-12-27

共1个答案

小编典典

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

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

  • 接收器

比其他参数更频繁地是指针。方法修改它们被调用的东西,或者命名类型是大结构,所以

指导是

默认为指针,除非在极少数情况下。

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

一些不需要指针的情况:

  • 代码审查指南建议将像,甚至可能更大一些的小结构type Point struct { latitude, longitude float64 }作为值传递,除非您正在调用的函数需要能够就地修改它们。
  • 值语义避免了混叠情况,即此处的赋值意外地更改了那里的值。
  • 为了一点速度而牺牲干净的语义并不是 Go-y,有时按值传递小结构实际上更有效,因为它避免了缓存未命中或堆分配。
  • 因此,Go Wiki 的代码审查评论页面建议在结构很小并且可能保持这种状态时按值传递。
  • 如果“大”截止值似乎含糊不清,那就是;可以说,许多结构都在一个指针或值都可以的范围内。作为下限,代码审查意见建议切片(三个机器词)用作值接收器是合理的。作为更接近上限的东西,bytes.Replace需要 10 个单词的 args(三个切片和一个int)。你可以找到的情况,其中复制甚至大型结构证明性能取胜,但经验法则是不要。
  • 对于slices,您不需要传递指针来更改数组的元素。例如,io.Reader.Read(p []byte)更改 的字节p。这可以说是“像对待值一样对待小结构”的特殊情况,因为在内部您传递了一个称为切片标头的小结构。同样,您不需要指针来修改地图或在频道上进行通信
  • 对于切片,您将重新切片(更改其开始/长度/容量),内置函数如append接受切片值并返回一个新值。我会模仿;它避免了别名,返回一个新的切片有助于引起人们对可能分配新数组的注意,这对调用者来说很熟悉。
  • 遵循这种模式并不总是可行的。某些工具(如数据库接口序列化程序)需要附加到编译时未知类型的切片。它们有时接受一个指向interface{}参数中的切片的指针。
  • 映射、通道、字符串以及函数和接口值,如切片,是内部引用或已经包含引用的结构,因此如果您只是想避免复制底层数据,则不需要将指针传递给它们.。
  • 在您想要修改调用者的结构的罕见情况下,您可能仍然需要传递指针:例如,出于这个原因,flag.StringVar需要一个*string

使用指针的地方:

  • 考虑您的函数是否应该是您需要指针指向的任何结构的方法。人们期望有很多方法x可以修改x,因此将修改后的结构体设置为接收器可能有助于最大程度地减少意外。有关于何时接收者应该是指针的指导方针
  • 对其非接收器参数有影响的函数应该在 godoc 中明确说明,或者更好的是,在 godoc 和名称(如reader.WriteTo(writer))中说明。
  • 你提到接受一个指针来通过允许重用来避免分配;为了内存重用而更改 API 是一种优化,我会延迟直到很明显分配具有非平凡的成本,然后我会寻找一种不会对所有用户强制使用更棘手的 API 的方法:
  • 为了避免分配,Go 的Escape_analysis是你的朋友。您有时可以通过创建可以使用普通构造函数、普通文字或有用的零值(如bytes.Buffer.
  • 考虑一种Reset()将对象放回空白状态的方法,就像某些 stdlib 类型提供的那样。不关心或无法保存分配的用户不必调用它。
  • 考虑将就地修改方法和从头开始创建函数作为匹配对编写,为方便起见:existingUser.LoadFromJSON(json []byte) error可以由NewUserFromJSON(json []byte) (*User, error). 同样,它将懒惰和压缩分配之间的选择推给了单个调用者。
  • 寻求回收内存的调用者可以让sync.Pool处理一些细节。如果某个特定的分配产生了很大的内存压力,您确信知道何时不再使用 alloc,并且您没有更好的优化可用,sync.Pool可以提供帮助。

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

  • 用于创建您的项目的 API可能会强制指向您,例如您必须调用NewFoo() *Foo而不是让 Go 初始化为零值
  • 项目的预期生命周期可能并不完全相同。整个切片立即被释放;如果 99% 的项目不再有用,但您有指向其他 1% 的指针,则所有数组仍保持分配状态。
  • 移动值可能会导致性能或正确性问题,从而使指针更具吸引力。值得注意的是,append底层数组增长时会复制项目。您在append指向错误位置之前获得的指针之后,对于巨大的结构,复制可能会更慢,并且例如sync.Mutex不允许复制。在中间插入/删除和排序类似地移动项目。

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

2021-12-27