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

本文讨论了在Go语言中如何选择使用指针还是值作为参数和返回值。最佳实践表明,如果不确定,通常使用指针作为接收器。对于小型结构体,可以按值传递,以避免不必要的拷贝。对于切片,由于它们已经是引用类型,通常不需要返回指针。在需要修改结构体或切片时,应使用指针。此外,如果结构体较大,使用指针可以提高效率。文章还提到了内存管理和性能优化方面的考虑。
摘要由CSDN通过智能技术生成

本文翻译自:Pointers vs. values in parameters and return values

In Go there are various ways to return a struct value or slice thereof. 在Go中,有多种方法可以返回struct值或其片段。 For individual ones I've seen: 对于个人而言,我已经看到:

type MyStruct struct {
    Val int
}

func myfunc() MyStruct {
    return MyStruct{Val: 1}
}

func myfunc() *MyStruct {
    return &MyStruct{}
}

func myfunc(s *MyStruct) {
    s.Val = 1
}

I understand the differences between these. 我了解这些区别。 The first returns a copy of the struct, the second a pointer to the struct value created within the function, the third expects an existing struct to be passed in and overrides the value. 第一个返回该结构的副本,第二个返回指向在函数内创建的结构值的指针,第三个期望传入现有结构并覆盖该值。

I've seen all of these patterns be used in various contexts, I'm wondering what the best practices are regarding these. 我已经看到所有这些模式都可以在各种情况下使用,我想知道关于这些的最佳实践是什么。 When would you use which? 什么时候使用? For instance, the first one could be ok for small structs (because the overhead is minimal), the second for bigger ones. 例如,第一个可能适用于小型结构(因为开销很小),第二个适用于较大的结构。 And the third if you want to be extremely memory efficient, because you can easily reuse a single struct instance between calls. 第三,如果您想提高内存效率,因为您可以轻松地在调用之间重用单个结构实例。 Are there any best practices for when to use which? 有什么最佳实践,何时使用?

Similarly, the same question regarding slices: 同样,关于切片的相同问题:

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

Again: what are best practices here. 再说一遍:什么是最佳实践。 I know slices are always pointers, so returning a pointer to a slice isn't useful. 我知道切片始终是指针,因此返回指向切片的指针没有用。 However, should I return a slice of struct values, a slice of pointers to structs, should I pass in a pointer to a slice as argument (a pattern used in the Go App Engine API )? 但是,是否应该返回一个结构值切片,一个指向结构的指针切片,是否应该将指向切片的指针作为参数传递( Go App Engine API中使用的模式)?


#1楼

参考:https://stackoom.com/question/1ambd/指针与参数和返回值中的值


#2楼

tl;dr : tl; dr

  • Methods using receiver pointers are common; 使用接收器指针的方法很常见。 the rule of thumb for receivers is , "If in doubt, use a pointer." 接收者的经验法则是 :“如有疑问,请使用指针”。
  • Slices, maps, channels, strings, function values, and interface values are implemented with pointers internally, and a pointer to them is often redundant. 切片,映射,通道,字符串,函数值和接口值是在内部使用指针来实现的,指向它们的指针通常是多余的。
  • Elsewhere, use pointers for big structs or structs you'll have to change, and otherwise pass values , because getting things changed by surprise via a pointer is confusing. 在其他地方,将指针用于大型结构或必须更改的结构,否则传递值 ,因为通过指针使事情意外更改会造成混淆。

One case where you should often use a pointer: 一种应该经常使用指针的情况:

  • 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. 方法修改被调用的东西或命名类型为大型结构并不罕见,因此在极少数情况下,指南是默认使用指针。
    • Jeff Hodges' copyfighter tool automatically searches for non-tiny receivers passed by value. 杰夫·霍奇斯(Jeff Hodges)的copyfighter工具自动搜索按值传递的非微小接收者。

Some situations where you don't need pointers: 在某些不需要指针的情况下:

  • Code review guidelines suggest passing small structs like type Point struct { latitude, longitude float64 } , and maybe even things a bit bigger, as values, unless the function you're calling needs to be able to modify them in place. 代码审查指南建议将较小的结构(type Point struct { latitude, longitude float64 }传递给值,甚至可能将更大的值作为值传递,除非您调用的函数需要能够就地对其进行修改。

    • Value semantics avoid aliasing situations where an assignment over here changes a value over there by surprise. 值语义避免混叠情况,在此情况下,此处的赋值会意外更改其值。
    • It's not Go-y to sacrifice clean semantics for a little speed, and sometimes passing small structs by value is actually more efficient, because it avoids cache misses or heap allocations. 牺牲干净的语义以加快速度并不是一件容易的事,有时通过值传递小结构实际上会更有效,因为它避免了高速缓存未命中或堆分配。
    • So, Go Wiki's code review comments page suggests passing by value when structs are small and likely to stay that way. 因此,Go Wiki的代码审查评论页建议在结构较小且可能会保持这种状态时按值传递。
    • If the "large" cutoff seems vague, it is; 如果“大”分界线似乎含糊,那就是; arguably many structs are in a range where either a pointer or a value is OK. 可以说,许多结构都在指针或值确定的范围内。 As a lower bound, the code review comments suggest slices (three machine words) are reasonable to use as value receivers. 作为下限,代码审查注释建议切片(三个机器字)可以合理地用作值接收者。 As something nearer an upper bound, bytes.Replace takes 10 words' worth of args (three slices and an int ). 接近上限时, bytes.Replace接受了10个字的args(三个切片和一个int )。
  • For slices , you don't need to pass a pointer to change elements of the array. 对于slices ,您不需要传递指针来更改数组的元素。 io.Reader.Read(p []byte) changes the bytes of p , for instance. io.Reader.Read(p []byte)更改p的字节。 It's arguably a special case of "treat little structs like values," since internally you're passing around a little structure called a slice header (see Russ Cox (rsc)'s explanation ). 可以说这是“对待像值一样的小结构”的特例,因为在内部,您正在传递一个称为切片头的小结构(请参阅Russ Cox(rsc)的说明 )。 Similarly, you don't need a pointer to modify a map or communicate on a channel . 同样,您不需要指针即可修改地图或在channel上进行通信

  • For slices you'll reslice (change the start/length/capacity of), built-in functions like append accept a slice value and return a new one. 对于切片,您将进行切片 (更改其开始/长度/容量),诸如append类的内置函数会接受切片值并返回一个新值。 I'd imitate that; 我会模仿的; it avoids aliasing, returning a new slice helps call attention to the fact that a new array might be allocated, and it's familiar to callers. 它避免了混淆,返回一个新的分片有助于引起人们注意可能分配了一个新数组的事实,并且调用者对此很熟悉。

    • It's not always practical follow that pattern. 遵循这种模式并不总是可行的。 Some tools like database interfaces or serializers need to append to a slice whose type isn't known at compile time. 一些工具,例如数据库接口序列化器,需要追加到在编译时类型未知的片上。 They sometimes accept a pointer to a slice in an interface{} parameter. 他们有时会在interface{}参数中接受指向切片的指针。
  • Maps, channels, strings, and function and interface values , like slices, are internally references or structures that contain references already, so if you're just trying to avoid getting the underlying data copied, you don't need to pass pointers to them. 映射,通道,字符串以及函数和接口值 (例如切片)是内部引用或已经包含引用的结构,因此,如果您只是试图避免复制基础数据,则无需将指针传递给它们。 (rsc wrote a separate post on how interface values are stored ). (rsc 撰写了有关如何存储接口值的单独文章 )。

    • You still may need to pass pointers in the rarer case that you want to modify the caller's struct: flag.StringVar takes a *string for that reason, for example. 在极少数情况下,您可能仍需要传递指针,以修改调用者的结构: flag.StringVar需要一个*string

Where you use pointers: 使用指针的位置:

  • Consider whether your function should be a method on whichever struct you need a pointer to. 考虑您的函数是否应该是您需要指向的任何结构上的方法。 People expect a lot of methods on x to modify x , so making the modified struct the receiver may help to minimize surprise. 人们期望在x上使用许多方法来修改x ,因此使接收器成为修改后的结构可能有助于最大程度地减少意外。 There are guidelines on when receivers should be pointers. 对于何时应该将接收者作为指针有一些指导

  • Functions that have effects on their non-receiver params should make that clear in the godoc, or better yet, the godoc and the name (like reader.WriteTo(writer) ). 对非接收器参数有影响的函数应该在godoc中(最好是godoc和名称)(例如reader.WriteTo(writer) )中reader.WriteTo(writer)

  • You mention accepting a pointer to avoid allocations by allowing reuse; 您提到接受一个指针,以允许通过重用来避免分配。 changing APIs for the sake of memory reuse is an optimization I'd delay until it's clear the allocations have a nontrivial cost, and then I'd look for a way that doesn't force the trickier API on all users: 为了内存重用而更改API是一种优化,我会延迟直到明显知道分配的成本不菲,然后再寻找一种不会对所有用户施加棘手API的方法:

    1. For avoiding allocations, Go's escape analysis is your friend. 为了避免分配,Go的转义分析是您的朋友。 You can sometimes help it avoid heap allocations by making types that can be initialized with a trivial constructor, a plain literal, or a useful zero value like bytes.Buffer . 有时您可以通过创建可以用平凡的构造函数,纯文字或有用的零值(如bytes.Buffer初始化的类型来帮助避免堆分配。
    2. Consider a Reset() method to put an object back in a blank state, like some stdlib types offer. 考虑使用Reset()方法将对象放回空白状态,就像某些stdlib类型提供的那样。 Users who don't care or can't save an allocation don't have to call it. 不在乎或无法保存分配的用户不必调用它。
    3. Consider writing modify-in-place methods and create-from-scratch functions as matching pairs, for convenience: existingUser.LoadFromJSON(json []byte) error could be wrapped by NewUserFromJSON(json []byte) (*User, error) . 考虑写修改就地方法和创建-从划伤的功能匹配对,为方便起见: existingUser.LoadFromJSON(json []byte) error可以通过以下方式包裹NewUserFromJSON(json []byte) (*User, error) Again, it pushes the choice between laziness and pinching allocations to the individual caller. 再次,它在懒惰和捏分配给单个呼叫者之间做出选择。
    4. Callers seeking to recycle memory can let sync.Pool handle some details. 试图回收内存的sync.Pool者可以让sync.Pool处理一些细节。 If a particular allocation creates a lot of memory pressure, you're confident you know when the alloc is no longer used, and you don't have a better optimization available, sync.Pool can help. 如果特定的分配产生了很大的内存压力,您sync.Pool不再使用该分配,并且没有更好的优化方法, sync.Pool可以帮助您。 (CloudFlare published a useful (pre- sync.Pool ) blog post about recycling.) (CloudFlare发布有关回收的有用的(pre- sync.Pool )博客文章 。)

Finally, on whether your slices should be of pointers: slices of values can be useful, and save you allocations and cache misses. 最后,关于切片是否应该是指针:值切片可以很有用,并且可以节省分配和缓存未命中。 There can be blockers: 可能有阻止者:

  • The API to create your items might force pointers on you, eg you have to call NewFoo() *Foo rather than let Go initialize with the zero value . 用于创建商品的API可能会强制您使用指针,例如,您必须调用NewFoo() *Foo而不是让Go初始化为零值
  • The desired lifetimes of the items might not all be the same. 这些项目的期望寿命可能不尽相同。 The whole slice is freed at once; 整个切片立即被释放; if 99% of the items are no longer useful but you have pointers to the other 1%, all of the array remains allocated. 如果99%的项目不再有用,但您有指向其他1%的指针,则所有数组均保持分配状态。
  • Moving items around might cause you problems. 到处移动项目可能会导致您遇到问题。 Notably, append copies items when it grows the underlying array . 值得注意的是,在增长基础数组append复制项目。 Pointers you got before the append point to the wrong place after, copying can be slower for huge structs, and for eg sync.Mutex copying isn't allowed. 您在append之前获得的指针指向之后的错误位置,对于大型结构,例如, sync.Mutex ,复制可能会变慢。不允许进行sync.Mutex复制。 Insert/delete in the middle and sorting similarly move items around. 在中间插入/删除并类似地移动项目。

Broadly, value slices can make sense if either you get all of your items in place up front and don't move them (eg, no more append s after initial setup), or if you do keep moving them around but you're sure that's OK (no/careful use of pointers to items, items are small enough to copy efficiently, etc.). 广义上讲,如果您将所有项目都放在适当的位置并且不移动它们(例如,在初始设置后不再append ),或者如果您确实继续移动它们,则可以确定价值切片是否合理没关系(无需/谨慎使用指向项目的指针,项目足够小以至于无法有效复制等)。 Sometimes you have to think about or measure the specifics of your situation, but that's a rough guide. 有时您必须考虑或衡量具体情况,但这只是一个粗略的指导。


#3楼

Three main reasons when you would want to use method receivers as pointers: 您希望将方法接收器用作指针的三个主要原因:

  1. "First, and most important, does the method need to modify the receiver? If it does, the receiver must be a pointer." “首先,也是最重要的一点,该方法需要修改接收方吗?如果需要,则接收方必须是指针。”

  2. "Second is the consideration of efficiency. If the receiver is large, a big struct for instance, it will be much cheaper to use a pointer receiver." “第二是效率的考虑。如果接收器很大,例如一个大型结构,则使用指针接收器会便宜得多。”

  3. "Next is consistency. If some of the methods of the type must have pointer receivers, the rest should too, so the method set is consistent regardless of how the type is used" “下一个是一致性。如果该类型的某些方法必须具有指针接收器,则其余方法也应该具有指针接收器,因此,无论如何使用该类型,方法集都是一致的”

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

Edit : Another important thing is to know the actual "type" that you are sending to function. 编辑:另一个重要的事情是要知道您要发送给功能的实际“类型”。 The type can either be a 'value type' or 'reference type'. 类型可以是“值类型”或“引用类型”。

Even as slices and maps acts as references, we might want to pass them as pointers in scenarios like changing the length of the slice in the function. 即使切片和地图用作引用,我们也可能希望在诸如更改函数中切片长度的情况下将它们作为指针传递。


#4楼

A case where you generally need to return a pointer is when constructing an instance of some stateful or shareable resource . 通常,在构造某些有状态或可共享资源实例时,通常需要返回指针。 This is often done by functions prefixed with New . 这通常是通过以New为前缀的函数来完成的。

Because they represent a specific instance of something and they may need to coordinate some activity, it doesn't make a lot of sense to generate duplicated/copied structures representing the same resource -- so the returned pointer acts as the handle to the resource itself. 因为它们表示某事物的特定实例,并且可能需要协调某些活动,所以生成表示相同资源的重复/复制结构没有多大意义-因此返回的指针充当资源本身的句柄。

Some examples: 一些例子:

In other cases, pointers are returned just because the structure may be too large to copy by default: 在其他情况下,仅由于结构可能太大而无法默认复制而返回指针:


Alternatively, returning pointers directly could be avoided by instead returning a copy of a structure that contains the pointer internally, but maybe this isn't considered idiomatic: 或者,可以通过改为在内部返回包含指针的结构的副本来避免直接返回指针,但这可能不被认为是惯用的:


#5楼

If you can (eg a non-shared resource that does not need to be passed as reference), use a value. 如果可以(例如,不需要传递作为参考的非共享资源),请使用一个值。 By the following reasons: 由于以下原因:

  1. Your code will be nicer and more readable, avoiding pointer operators and null checks. 您的代码将更好,更易读,避免了指针运算符和null检查。
  2. Your code will be safer against Null Pointer panics. 您的代码将更安全地防止Null Pointer恐慌。
  3. Your code will be often faster: yes, faster! 您的代码通常会更快: 是的,更快! Why? 为什么?

Reason 1 : you will allocate less items in the stack. 原因1 :您将在堆栈中分配较少的项目。 Allocating/deallocating from stack is immediate, but allocating/deallocating on Heap may be very expensive (allocation time + garbage collection). 从堆栈分配/取消是立即进行的,但是在堆上分配/取消分配可能会非常昂贵(分配时间+垃圾回收)。 You can see some basic numbers here: http://www.macias.info/entry/201802102230_go_values_vs_references.md 您可以在此处看到一些基本数字: http : //www.macias.info/entry/201802102230_go_values_vs_references.md

Reason 2 : especially if you store returned values in slices, your memory objects will be more compacted in memory: looping a slice where all the items are contiguous is much faster than iterating a slice where all the items are pointers to other parts of the memory. 原因2 :尤其是如果您将返回的值存储在切片中,则内存对象将在内存中更加紧凑:循环所有项都是连续的切片比循环切片所有项都是指向内存其他部分的指针的切片要快得多。 Not for the indirection step but for the increase of cache misses. 不是用于间接步骤,而是用于增加高速缓存未命中率。

Myth breaker : a typical x86 cache line are 64 bytes. 误区 :典型的x86高速缓存行为64字节。 Most structs are smaller than that. 大多数结构都比那个小。 The time of copying a cache line in memory is similar to copying a pointer. 在内存中复制高速缓存行的时间类似于复制指针。

Only if a critical part of your code is slow I would try some micro-optimization and check if using pointers improves somewhat the speed, at the cost of less readability and mantainability. 仅当代码的关键部分慢时,我才会尝试进行一些微优化,并检查使用指针是否在某种程度上提高了速度,但代价是可读性和可维护性较低。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值