在Go中,有多种返回结构值或其切片的方法。就我所见过的个体而言:

type MyStruct struct {
    Val int
}

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

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

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

我明白它们之间的区别。第一个返回一个结构体的副本,第二个返回一个指向在函数中创建的结构体值的指针,第三个返回传入一个现有的结构体并覆盖该值。

我看到所有这些模式都在不同的环境中使用,我想知道关于这些的最佳实践是什么。什么时候用which?例如,第一种方法适用于小型结构(因为开销最小),第二种方法适用于大型结构。第三种方法用于提高内存效率,因为可以在调用之间轻松重用单个struct实例。什么时候使用哪种有什么最佳实践吗?

同样的,关于切片的问题:

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; diana:

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


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

接收器通常是指针,而不是其他参数。方法修改被调用的对象或命名类型为大型结构体都很常见,因此除非在极少数情况下,建议默认使用指针。 杰夫·霍奇斯(Jeff Hodges)的copyfighter工具会自动搜索按值传递的非微小接收器。

有些情况下你不需要指针:

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. Value semantics avoid aliasing situations where an assignment over here changes a value over there by surprise. Passing small structs by value can be more efficient by avoiding cache misses or heap allocations. In any case, when pointers and values perform similarly, the Go-y approach is to choose whatever provides the more natural semantics rather than squeeze out every last bit of speed. So, Go Wiki's code review comments page suggests passing by value when structs are small and likely to stay that way. 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). You can find situations where copying even large structs turns out a performance win, but the rule of thumb is not to. For slices, you don't need to pass a pointer to change elements of the array. io.Reader.Read(p []byte) changes the bytes of p, for instance. 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). Similarly, you don't need a pointer to modify a map or communicate on a 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. 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. 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). 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.

使用指针的地方:

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. 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)). 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: For avoiding allocations, Go's escape analysis is your friend. 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. Consider a Reset() method to put an object back in a blank state, like some stdlib types offer. Users who don't care or can't save an allocation don't have to call it. 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). Again, it pushes the choice between laziness and pinching allocations to the individual caller. Callers seeking to recycle memory can let sync.Pool handle some details. 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. (CloudFlare published a useful (pre-sync.Pool) blog post about recycling.)

最后,关于你的切片是否应该是指针:值切片是有用的,可以节省你的分配和缓存丢失。可以有阻碍:

The API to create your items might force pointers on you, e.g. you have to call NewFoo() *Foo rather than let Go initialize with the zero value. 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. Copying or moving the values might cause you performance or correctness problems, making pointers more attractive. Notably, append copies items when it grows the underlying array. Pointers to slice items from before the append may not point to where the item was copied after, copying can be slower for huge structs, and for e.g. sync.Mutex copying isn't allowed. Insert/delete in the middle and sorting also move items around so similar considerations can apply.

一般来说,如果你把所有的项都放在前面,不移动它们(例如,在初始设置后不再追加),或者如果你继续移动它们,但你确信这是可以的(没有/小心使用指向项的指针,并且项很小,或者你已经测量了性能影响),值切片是有意义的。有时候,这取决于你的具体情况,但这只是一个粗略的指南。


当你想要使用方法接收器作为指针时,有三个主要原因:

首先,也是最重要的,方法是否需要修改接收者?如果是,那么接收者必须是一个指针。” 其次是对效率的考虑。如果接收器很大,比如一个大的结构体,使用指针接收器会便宜得多。” 其次是一致性。如果该类型的一些方法必须有指针接收器,那么其他方法也应该有,因此无论如何使用该类型,方法集都是一致的。”

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

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

即使切片和映射充当引用,我们也可能希望在某些情况下将它们作为指针传递,比如在函数中更改切片的长度。


通常需要返回指针的情况是在构造某个有状态或可共享资源的实例时。这通常由前缀为New的函数来完成。

因为它们表示某事物的特定实例,并且可能需要协调某些活动,所以生成表示相同资源的复制/复制结构没有多大意义——因此返回的指针充当资源本身的句柄。

一些例子:

func NewTLSServer(handler http.Handler)——实例化一个web服务器进行测试 func打开(文件名字符串)(*文件,错误)——返回一个文件访问句柄

在其他情况下,返回指针只是因为默认情况下结构可能太大而无法复制:

func NewRGBA(r Rectangle) *RGBA—在内存中分配图像


或者,直接返回指针可以通过返回内部包含指针的结构的副本来避免,但这可能不被认为是惯用的:

在标准库中找不到这样的例子…… 相关问题:在Go中使用指针或值嵌入


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

您的代码将更好,更可读,避免指针操作符和空检查。 您的代码将更安全的对抗空指针恐慌。 您的代码通常会更快:是的,更快!为什么?

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

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

打破神话:典型的x86缓存线是64字节。大多数结构体都比这个小。在内存中复制高速缓存行的时间与复制指针的时间相似。

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


关于结构和指针返回值,我在github上阅读了许多高度关注的开源项目后感到困惑,因为这两种情况都有很多例子,util我发现了这篇惊人的文章: https://www.ardanlabs.com/blog/2014/12/using-pointers-in-go.html

通常,与指针共享结构类型值,除非该结构类型已被实现为行为类似于原始数据值。

If you are still not sure, this is another way to think about. Think of every struct as having a nature. If the nature of the struct is something that should not be changed, like a time, a color or a coordinate, then implement the struct as a primitive data value. If the nature of the struct is something that can be changed, even if it never is in your program, it is not a primitive data value and should be implemented to be shared with a pointer. Don’t create structs that have a duality of nature."

完成信服。