在 Go 有很多方法可以回到一个struct值或它的切片。对于我见过的个人:6 T, A' ^8 \0 S+ O4 K) q; E8 J" Z8 L+ ]
type MyStruct struct Val int}func myfunc() MyStruct return MyStruct{Val: 1}}func myfunc() *MyStruct return &MyStruct{}}func myfunc(s *MyStruct) s.Val = 1}9 k' Z; {) j! f' h: s; O+ C) m
我知道这些差异。第一个返回结构的副本,第二个是指在函数中创建的结构值的指针,第三个是将现有结构引入并覆盖该值。8 m8 b2 B7 |7 d9 H8 G2 I7 T3 w) N* J
我已经看到所有这些模型都用在各种上下文中。我想知道这些最好的实践是什么。你什么时候用哪个?例如,第一个可能适用于小型结构(因为成本很小),第二个可能适用于大型结构。第三,如果你想获得很高的内存效率,因为你可以很容易地在调用之间重用单个结构实例。什么时候用哪个最好? . J. J# U( h, j同样,关于切片的同样问题: 7 J! E# x8 ~) h+ S: w) b! g[code]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: }code]再说一遍:这里最好的做法是什么?我知道切片总是指针,所以返回指向切片的指针是没有用的。但是,我应该返回结构值切片和指向结构的指针吗?我应该把指向切片的指针作为参数传递吗? 8 h1 X6 k% e+ W' R. l u* z1 j2 K8 j+ |/ v0 X! t. C解决方案: . r0 M2 C' ?+ k C) r: Y2 { 使用接收器指针的方法很常见;接收者的经验规则是如有疑问,请使用指针。# n3 @8 {% ?+ B$ ]0 x
在内部使用指针实现切片、映射、通道、字符串、函数值和接口值,指向指针通常是多余的。 6 D7 ^& t1 [# {: C在其他地方,指针用于大结构或必须改变的结构,否则传递值,因为指针意外更改内容令人困惑。你应该经常使用指针: . y B7 C/ P Q* Y0 ~6 Y7 W接收器指针比其他参数更频繁。方法修改它们被调用的东西,或者命名类型是大结构,所以 6 _/ ]" ?9 U. T/ k& I指导是 5 ~/ w8 H4 h+ e4 V' N3 u N除非在极少数情况下,否则默认为指针。' U5 g: n: h2 K Jeff Hodges 的copyfighter该工具将自动搜索按值传输的非微接收器。有些情况不需要指针: $ K8 Q) k' m7 g( L1 z. Q/ {- K, K建议对代码审查指南进行图像,甚至可能更大小结构type Point struct { latitude,longitude float64 }作为值传递,除非需要能够当场修改正在调用的函数。7 H7 c3 [. s, p! h( r& @' _' a7 ]
值语义避免了混叠,即这里的赋值意外地改变了那里的值。 5 s; }: |) G+ A) e f) O/ J- E牺牲一点速度的干净语义不是 Go-y,有时按值传递小结构实际上更有效,因为它避免了缓存未命中或堆积分配。- S+ O- X* P- O* E5 u
因此,Go Wiki 代码审查评论页面建议在结构小且可能保持这种状态时按值传递。 - \ A8 ?4 E$ S' h0 U7 j* A如果大的截止日期似乎很模糊,那就是;可以说,许多结构都在一个指针或值的范围内。作为下限,建议切片(三个机器词)作为值接收器是合理的。作为一个更接近上限的东西,bytes.Replace 需要 10个单词args(三个切片和一个int)。可以找到复制甚至大结构证明性能取胜的情况,但是没有经验规则。+ _5 }. u4 F! j, ]& f, S7 l6 h9 M
对于slices,您不需要传递指针来更改数组元素。io.Reader.Read(p []byte)改变 字节p。这可以说是像对待值一样对待小结构的特殊情况,因为你在内部传达了一个叫做像对待值一样对待小结构的特殊情况切片标头小结构。同样,你也不需要指针修改地图或通信频道。 3 C3 u* G( H8 C7 Q对于您将重新切片(改变其开始/长度/容量)append接受切片值并返回新值。我会模仿它;它避免了别名。返回新切片有助于吸引人们对可能分配新数组的注意,这对调用人员非常熟悉。 ; g! \$ M. e8 T/ J遵循这种模式并不总是可行的。编译工具(如数据库接口或序列化程序)需要添加到编译过程中未知类型的切片中。他们有时会接受指向interface{}参数中的切片指针。 4 s! q6 L5 P4 `4 m% |# x4 b映射、通道、字符串、函数和接口值,例如,切片是一种内部引用或已经包含引用的结构。因此,如果您只是想避免复制底层数据,您不需要将指针传递给它们.。% [4 b$ F; {1 g' Z3 h
在您想要修改在调用者结构罕见的情况下,您可能仍然需要传递指针:例如,出于这个原因,flag.StringVar需要一个*string。使用指针的地方:! t/ K( ^7 s0 a, O 考虑你的函数是否应该是你需要指针指向的任何结构的方法。人们希望有很多方法x可以修改x,因此,将修改后的结构设置为接收器可能有助于最大限度地减少事故。什么时候接收者应该是指针的指导方针。' a) A5 U; X$ Q$ j4 I2 [
对其非接收器参数有影响的函数应该在 godoc 中明确说明,或者更好的是 godoc 和名字(如reader.WriteTo(writer))中说明。( o6 `8 v! R' L' A7 t+ w
您提到接受允许重用的指针以避免分配;改变 以重用内存API 是一种优化,我会推迟到非凡的成本明显分配,然后我会找到一种 ,不会强迫所有用户使用更困难API 的方法: 7 _ {& h" d0 ]0 d& B为避免分配,Go 的Escape_analysis是你的朋友。有时你可以创建可以使用普通结构函数、普通文本或有用的零值(如bytes.Buffer.. @1 I. T0 m8 `: X, V* E. _0 a
考虑一种Reset()将对象放回空白状态的方法,就像一些 stdlib 提供的类型。不关心或不能保存分配的用户不需要调用它。 0 J0 p3 E* S7 v+ }* _; V! O6 l为方便起见,考虑将当地修改方法与从头开始创建函数相匹配:existingUser.LoadFromJSON(json []byte) error可以由NewUserFromJSON(json []byte) (*User,error). 同样,它将懒惰和压缩分配之间的选择推给了单个调用器。; K9 v, n. W* y5 j$ y9 d8 n
寻求回收内存的调用者可以让sync.Pool处理一些细节。如果一个特定的分配有很大的内存压力,你确定知道什么时候不再使用 alloc,而且你没有更好的优化,sync.Pool可提供帮助。最后,关于你的切片是否应该是指针:值的切片可能非常有用,并节省你的分配和缓存。可能有拦截器: 6 y* [# f& d" Q5 A用于创建您的项目API你可能会被迫指向你,例如,你必须调用它NewFoo() *Foo而不是让 Go 初始化为零值。 ' p* \) o& B1 Y' n项目的预期生命周期可能不完全一样。整个切片立即释放;如果 99% 的项目不再有用,但你有指向其他 1% 的指针,所有数组仍然分布。 / e. ^% l4 r0 {6 j移动值可能会导致性能或正确性问题,使指针更有吸引力。值得注意的是,append当底层数组增长时,项目将被复制。append在指向错误位置之前获得的指针后,复制巨大的结构可能会更慢,例如sync.Mutex不允许复制。类似的移动项目插入/删除和排序在中间。从广义上讲,如果你把所有的项目放在前面,不移动它们(例如,append初始设置后不再移动s),或者如果你继续移动它们,但你确定它们是好的(不要小心使用指向项目的指针,项目足够小,以便有效地复制它们)。有时你必须考虑或衡量你的情况,但这是一个粗略的指南。