在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
}
我理解这些之间的差异。第一个返回结构的副本,第二个返回指向函数内创建的结构值的指针,第三个期望传入现有的结构并覆盖该值。
我已经看到所有这些模式都在各种环境中使用,我想知道关于这些模式的最佳实践是什么。你什么时候用哪个?例如,第一个可能适用于小结构(因为开销很小),第二个适用于较大结构。第三个是你想要非常高效的内存,因为你可以在调用之间轻松地重用一个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} }
}
再次:这里的最佳做法是什么。我知道切片总是指针,所以返回指向切片的指针是没用的。但是,如果我返回一片struct值,一段指向结构的指针,我应该将指向切片的指针作为参数传递(Go App Engine API中使用的模式)?
答案 0 :(得分:325)
tl; dr :
您应经常使用指针的一种情况:
有些情况下你不需要指针:
代码审核指南建议传递小结构,如type Point struct { latitude, longitude float64 }
,甚至可能更大一些,作为值,除非您调用的函数需要是能够就地修改它们。
bytes.Replace
需要10个单词'价值args(三片和int
)。对于切片,您不需要传递指针来更改数组的元素。例如,io.Reader.Read(p []byte)
会更改p
的字节数。它可以说是一个特殊的案例,可以处理像价值观这样的小结构,"因为在内部你会传递一个叫做切片标题的小结构(参见Russ Cox (rsc)'s explanation)。同样,您也不需要指向修改地图或在频道上进行通信的指针。
对于切片,您需要重新启动(更改开始/长度/容量),内置函数(如append
)接受切片值并返回新值一。我模仿那个;它避免了别名,返回一个新的切片有助于引起人们对可能分配新数组的事实的注意,并且它对调用者来说很熟悉。
interface{}
参数中切片的指针。地图,频道,字符串以及功能和界面值,就像切片一样,是内部引用或已包含引用的结构,因此,如果您只是想避免获取复制的基础数据,您不需要将指针传递给它们。 (rsc wrote a separate post on how interface values are stored)。
flag.StringVar
因为这个原因需要*string
,例如。使用指针的地方:
考虑您的函数是否应该是您需要指向哪个结构的方法。人们期望x
上有很多方法可以修改x
,因此将修改后的结构体作为接收器可以帮助减少意外。接收器应该是指针时有guidelines。
对非接收者参数有影响的函数应该在godoc中更清楚,或者更好的是godoc和名称(如reader.WriteTo(writer)
)。
您提到通过允许重用来接受指针以避免分配;为了记忆重用而改变API是一种优化,我要延迟,直到它清楚分配有一个非常重要的成本,然后我寻找一种不会强迫所有用户都使用棘手的API:
bytes.Buffer
)初始化的类型来帮助它避免堆分配。Reset()
方法将对象放回空白状态,就像一些stdlib类型提供的那样。不关心或无法保存分配的用户不必致电。existingUser.LoadFromJSON(json []byte) error
可以包裹NewUserFromJSON(json []byte) (*User, error)
。同样,它推动了懒惰和捏合分配到个人来电者之间的选择。sync.Pool
处理一些细节。如果某个特定的分配产生了很大的内存压力,那么您确信自己知道何时不再使用alloc,并且您没有更好的优化可用,sync.Pool
可以提供帮助。 (CloudFlare发布了有关回收的a useful (pre-sync.Pool
) blog post。)new(Foo).Reset()
有时可以避免在NewFoo()
不匹配时进行分配。不是惯用的;小心翼翼地在家里试一试。最后,关于切片是否应该是指针:值的切片可能很有用,并保存分配和缓存未命中。可能有阻碍者:
NewFoo() *Foo
而不是让Go使用zero value进行初始化。append
会在grows the underlying array时复制项目。你在append
指向错误位置之前得到的指针,对于巨大的结构,复制可能会更慢,例如sync.Mutex
复制不允许。在中间插入/删除并按类似方式移动项目。从广义上讲,如果您将所有物品预先放在原地并且不移动它们(例如,在初始设置后不再append
s),或者如果您这样做,那么价值切片就有意义继续移动他们,但你确定没关系(没有/小心使用指向项目的指针,项目足够小,可以有效地复制,等等)。有时您必须考虑或衡量您的具体情况,但这是一个粗略的指南。
答案 1 :(得分:8)
要使用方法接收器作为指针的三个主要原因:
“首先,也是最重要的是,该方法需要修改接收方吗?如果需要,则接收方必须是指针。”
“第二是效率的考虑。如果接收器很大(例如,大型结构),则使用指针接收器会便宜得多。”
“接下来是一致性。如果该类型的某些方法必须具有指针接收器,则其余方法也应具有指针接收器,因此无论如何使用该类型,方法集都是一致的”
参考:https://golang.org/doc/faq#methods_on_values_or_pointers
编辑:另一个重要的事情是知道要发送给函数的实际“类型”。类型可以是“值类型”或“引用类型”。
即使切片和地图用作引用,我们也可能希望在诸如更改函数中切片长度的情况下将它们作为指针传递。
答案 2 :(得分:1)
通常需要返回一个指针的情况是,在构造某些有状态或可共享资源的实例时。这通常是通过以New
为前缀的函数来完成的。
由于它们表示某事物的特定实例,并且可能需要协调某些活动,因此生成表示相同资源的重复/复制结构没有多大意义-因此返回的指针充当资源本身。
一些例子:
func NewTLSServer(handler http.Handler) *Server
-实例化Web服务器进行测试func Open(name string) (*File, error)
-返回文件访问句柄在其他情况下,仅由于结构可能太大而无法默认复制而返回指针:
func NewRGBA(r Rectangle) *RGBA
-在内存中分配图像或者,可以通过直接在内部返回包含指针的结构的副本来避免直接返回指针,但这也许不被认为是惯用的:
答案 3 :(得分:0)
如果可以(例如,不需要传递作为参考的非共享资源),请使用一个值。由于以下原因:
原因1 :您将在堆栈中分配较少的项目。从堆栈分配/取消是立即进行的,但是在堆上分配/取消分配可能会非常昂贵(分配时间+垃圾回收)。您可以在此处看到一些基本数字:http://www.macias.info/entry/201802102230_go_values_vs_references.md
原因2 :特别是如果您将返回的值存储在切片中,则内存对象将在内存中更加紧凑:循环所有项都是连续的切片比循环遍历所有项的切片要快得多这些项目是指向内存其他部分的指针。不是为了间接步骤,而是为了增加缓存未命中率。
神话破灭:典型的x86缓存行为64字节。大多数结构都比那个小。在内存中复制缓存行的时间类似于复制指针。
仅当代码的关键部分很慢时,我才会尝试进行一些微优化,并检查使用指针是否在某种程度上提高了速度,但代价是可读性和可维护性较低。
答案 4 :(得分:0)
关于结构与指针返回值,在阅读了github上许多备受瞩目的开源项目后,我感到困惑,因为这两种情况都有很多例子,util我发现了这篇很棒的文章: https://www.ardanlabs.com/blog/2014/12/using-pointers-in-go.html
“一般来说,使用指针共享结构类型值,除非结构类型已被实现为表现得像原始数据值。
如果您仍然不确定,这是另一种思考方式。将每个结构视为具有性质。如果结构的性质是不应该改变的,比如时间、颜色或坐标,那么将结构实现为原始数据值。如果结构的性质是可以改变的,即使它从来不在你的程序中,它也不是原始数据值,应该实现为与指针共享。不要创建具有二元性的结构。”
完全相信。