在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中使用的模式)?
2 回答
tl;dr :
使用接收器指针的方法很常见; the rule of thumb for receivers is,"If in doubt, use a pointer."
切片,贴图,通道,字符串,函数值和接口值在内部使用指针实现,指向它们的指针通常是多余的 .
在其他地方,使用指向大结构或结构的指针,你必须改变,否则pass values,因为通过指针让事情变得惊讶是令人困惑的 .
您应经常使用指针的一种情况:
Receivers 是比其他参数更频繁的指针 . 它被's not unusual for methods to modify the thing they'重新调用,或者对于命名类型是大型结构,所以the guidance is默认为指针,除非在极少数情况下 .
Jeff Hodges的copyfighter工具自动搜索按值传递的非微型接收器 .
在某些情况下,您不需要指针:
代码审查指南建议传递 small structs ,如
type Point struct { latitude, longitude float64 }
,甚至可能更大一些,作为值,除非您调用的函数需要能够在适当的位置修改它们 .值语义避免了别名情况,其中此处的赋值通过意外更改了那里的值 .
Go-y不是为了一点速度牺牲干净的语义,有时候通过值传递小结构实际上更有效,因为它避免了cache misses或堆分配 .
所以,Go Wiki的code review comments页面建议在结构很小并且可能保持这种状态时通过值传递 .
如果"large"截止看起来很模糊,那就是;可以说很多结构都在指针或值正常的范围内 . 作为下限,代码审查注释建议切片(三个机器字)合理地用作值接收器 . 作为更接近上限的东西,
bytes.Replace
需要10个字的args(三个切片和一个int
) .对于 slices ,您不需要传递指针来更改数组的元素 . 例如,
io.Reader.Read(p []byte)
更改p
的字节 . 它传递了一个叫做切片头的小结构(参见Russ Cox (rsc)'s explanation) . 同样,您不需要指向 modify a map or communicate on a channel 的指针 .对于 slices you'll reslice (更改开始/长度/容量),内置函数(如
append
)接受切片值并返回一个新值 . 我对呼叫者很熟悉 .这种模式并不总是实用的 . 某些工具(如database interfaces或serializers)需要附加到其编译时类型未知的切片 . 它们有时会接受指向
interface{}
参数中切片的指针 .与切片一样,
Maps, channels, strings, and function and interface values 是内部引用或已包含引用的结构,因此如果您需要将指针传递给它们 . (rsc wrote a separate post on how interface values are stored) .
您可能还需要传递指针,以便在您想修改调用者结构的极少数情况下:flag.StringVar因为这个原因需要
*string
.你在哪里使用指针:
考虑你的函数是否应该是你需要指向哪个结构的方法 . 人们期望在
x
上有很多方法来修改x
,因此将修改后的结构体作为接收器可以帮助减少意外 . 当接收器应该是指针时,有guidelines .对其非接收器参数有影响的函数应该在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处理一些细节 . 如果特定分配会产生大量内存压力,那么're confident you know when the alloc is no longer used, and you don'可以提供更好的优化,
sync.Pool
可以提供帮助 . (CloudFlare发布a useful (pre-sync.Pool) blog post关于回收 . )奇怪的是,对于复杂的构造函数,当
NewFoo()
不能时,new(Foo).Reset()
有时可以避免分配 . 不是惯用的;小心翼翼地在家里试一试 .最后,关于切片是否应该是指针:值的切片可能很有用,并节省分配和缓存未命中 . 可能有阻碍者:
The API to create your items 可能会强制指示你,例如你必须调用
NewFoo() *Foo
而不是让Go用zero value初始化 .The desired lifetimes of the items 可能不一样都是一样的 . 整个切片立刻被释放;如果99%的项目不再有用,但你有指向另一个1%的项目,则所有数组仍保持分配状态 .
Moving items around 可能会给您带来麻烦 . 值得注意的是,
append
在grows the underlying array时复制了这些项目 . 你在append
指向错误的地方之前得到的指针,对于巨大的结构,复制可能会更慢,例如sync.Mutex
不允许复制 . 在中间插入/删除并类似地移动项目 .从广义上讲,如果您将所有物品放在前面并且不移动它们(例如,在初始设置后不再有
append
s),或者如果你继续移动它们但是你没有移动它们,那么 Value 切片就有意义了're sure that's OK (没有/仔细使用指向项目的指针,项目足够小,可以有效地复制等) . 有时您必须考虑或衡量您的具体情况,但这是一个粗略的指南 .当您想要将方法接收器用作指针时,有三个主要原因:
“首先,最重要的是,该方法是否需要修改接收器?如果是,接收器必须是指针 . ”
“第二是效率的考虑 . 如果接收器很大,例如一个大结构,使用指针接收器要便宜得多 . ”
“接下来是一致性 . 如果该类型的某些方法必须具有指针接收器,则其余方法也应如此,因此无论使用何种类型,方法集都是一致的”
参考:https://golang.org/doc/faq#methods_on_values_or_pointers
编辑:另一个重要的事情是知道您要发送到函数的实际“类型” . 类型可以是“值类型”或“引用类型” . 见下图:
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.