在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 的副本,第二个函数返回指向函数中创建的 struct 值的指针,第三个函数期望传入现有 struct 并重写该值.

我已经看到所有这些模式都在不同的上下文中使用,我想知道关于这些模式的最佳实践是什么.你什么时候用哪一种?例如,第一个可能适用于小 struct (因为开销最小),第二个适用于较大的 struct .第三种是如果您想要极高的内存效率,因为您可以很容易地在调用之间重用单个 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 值、一段指向 struct 的指针,是否应该将指向一段的指针作为参数传递(Go App Engine API中使用的模式)?

推荐答案

tl;dr:

  • 使用接收器指针的方法很常见;the rule of thumb for receivers is,"如果有疑问,请使用指针."
  • 切片、映射、通道、字符串、函数值和接口值在内部使用指针实现,指向它们的指针通常是冗余的.
  • 在其他地方,对大型 struct 或您必须更改的 struct 使用指针,否则使用pass values,因为通过指针意外更改会让人感到困惑.

在一种情况下,您应该经常使用指针:

  • 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工具自动搜索按值传递的非微型接收器.

某些情况下您不需要指针:

  • 代码审查指南建议传递small structs,比如type Point struct { latitude, longitude float64 },甚至可能更大一点,作为值,除非您调用的函数需要能够适当地修改它们.

    • 值语义避免了这里的赋值意外更改那里的值时出现别名的情况.
    • 牺牲干净的语义来换取一点速度是不可取的,有时按值传递小 struct 实际上更有效,因为它避免了cache misses或堆分配.
    • 因此,Go Wiki的code review comments页建议在 struct 很小且可能保持这种状态时,按值传递.
    • 如果"大"界限看起来很模糊,那么它就是模糊的;可以说,许多 struct 都在指针或值都可以的范围内.作为下限,代码评审注释建议片(三个机器字)用作值接收器是合理的.作为更接近上限的东西,bytes.Replace需要相当于10个单词的args(三个切片和一个int).您可以找到situations个地方,即使是复制大型 struct 也会带来性能上的优势,但经验法则是不会这样做.
  • 对于slices,不需要传递指针来更改数组的元素.例如,io.Reader.Read(p []byte)会更改p的字节.这可以说是"像对待值一样对待小 struct "的一个特例,因为在内部,您传递的是一个名为a slice header的小 struct (参见Russ Cox (rsc)'s explanation).类似地,不需要指向modify a map or communicate on a channel的指针.

  • 对于slices you'll reslice(更改的起始/长度/容量),像append这样的内置函数接受切片值并返回新的切片值.我会模仿这一点;它避免了别名,返回一个新的片段有助于提醒人们注意可能会分配一个新的数组,这对调用者来说很熟悉.

    • 遵循这种模式并不总是可行的.有些工具(如database interfacesserializers)需要附加到编译时类型未知的片中.它们有时接受指向interface{}参数中切片的指针.
  • 与片一样,Maps, channels, strings, and function and interface values也是内部引用或已经包含引用的 struct ,因此如果您只是想避免复制底层数据,则不需要传递指向它们的指针.(RSC wrote a separate post on how interface values are stored).

    • 在较少见的情况下,您可能仍然需要传递指针,因为您想要modify调用方的 struct :例如,flag.StringVar为此接受*string.

其中使用指针:

  • 考虑您的函数是否应该是您需要指针指向的任何 struct 上的方法.人们期望x上有很多方法来修改x,因此使修改后的 struct 成为接收器可能有助于最大限度地减少意外.当接收者应该是指针时,有guidelines个ON.

  • 对它们的非接收器参数有影响的函数应该在godoc中明确这一点,或者更好的是,godoc和名称(如reader.WriteTo(writer)).

  • 您提到通过允许重用来接受指针来避免分配;为了内存重用而更改API是一种优化,我会将其推迟到明确分配具有不平凡的成本时再进行,然后我会寻找一种不会将更棘手的API强加给所有用户的方法:

    1. 为了避免分配,围棋的escape analysis是你的朋友.您有时可以通过创建可以用简单的构造函数、纯文字或有用的零值(如bytes.Buffer)初始化的类型来帮助它避免堆分配.
    2. 考虑将一个对象放回到空白状态的Reset()种方法,比如一些STDLIB类型提供.不关心或无法保存分配的用户不必调用它.
    3. 为方便起见,请考虑编写就地修改方法和从头开始创建函数作为匹配对:existingUser.LoadFromJSON(json []byte) error可以由NewUserFromJSON(json []byte) (*User, error)包装.再一次,它将懒惰和紧缩分配之间的 Select 推给了单个调用者.
    4. 寻求回收内存的呼叫者可以让sync.Pool处理一些细节.如果某个特定的分配产生了很大的内存压力,您可以确信您知道何时不再使用分配,并且您没有更好的优化可用,sync.Pool可以提供帮助.(CloudFlare出版了a useful (pre-sync.Pool) blog post篇关于回收的文章.)

最后,关于您的切片是否应该是指针的问题:值切片可能很有用,可以为您节省分配和缓存未命中.可以有拦截器:

  • The API to create your items可能会强制将指针放在您身上,例如,您必须调用NewFoo() *Foo,而不是放手用zero value进行初始化.
  • The desired lifetimes of the items可能并不都是一样的.一次释放整个片;如果99%的项不再有用,但您有指向其他1%的指针,则所有数组仍保持分配状态.
  • Moving values around可能会导致性能或正确性问题,从而使指针更具吸引力.值得注意的是,当它为grows the underlying array时,append会复制项目.在append之前你得到的指针指向错误的位置,之后,对于巨大的 struct ,复制可能会更慢,而对于sync.Mutex,复制是不允许的.中间的插入/删除和排序类似地移动项目.

一般来说,如果你把所有的项目放在最前面,不移动它们(例如,初始设置后不超过append秒),或者如果你确实一直移动它们,但你确定没问题(没有/小心使用指向项目的指针,项目足够小,可以有效地复制,等等),那么价值切片是有意义的.有时候,你必须考虑或衡量自己处境的具体情况,但这只是一个粗略的指南.

Go相关问答推荐

如何使用go从map.dbf map.prj map.shp map.shx中的任何文件中了解层名称

如何获得与cksum相同的CRC 32?

Go GORM创建表,但不创建列

正在使用terratest执行terraform脚本测试,但遇到错误退出状态1

map 中的多个函数类型,Golang

为什么Slices包中的函数定义Slice参数的类型参数?

Golang使用Run()执行的命令没有返回

Kafka架构注册表-Broker:Broker无法验证记录

如何将 goose 迁移与 pgx 一起使用?

Go安装成功但没有输出简单的Hello World

使用 goroutine 比较 Golang 中的两棵树是等价的

生成一个 CSV/Excel,在 Golang 中该列的下拉选项中指定值

仅使用公共 api 对 alexedwards/scs 进行简单测试

如何从 Go Lambda 函数返回 HTML?

Global Thread-local Storage 在 Go 中的可行性和最佳实践

判断一个区域内的纬度/经度点

设置指向空接口的指针

查找、解析和验证邮箱地址

没有堆栈跟踪的 go 程序崩溃是什么意思?

在 Go 中将指针传递给函数的正确方法是什么,以便我可以读取和/或修改指针表示的值?