指针有很多好处,可以想象没有它们的编码,让我们说这并不令人愉快。但是他们确实有缺点,其中之一(至少对我来说)可能是他们可能引起混乱,甚至可能谨慎使用代码。我认为,当事情不按预期工作时,我们都会经历过,并且在调试会议之后发现指针导致了意外的行为。但是毕竟,决定是否通过(或一般使用)指针对我来说并不容易。因此,我决定向人们学习和学习比我更聪明,经验丰富的人。希望分享这些发现可能也会对您很有趣。
本文在其宝贵的文章中受到**Hossein Nazari *的重大启发,标题为“是否在通过Gocast上发表的structs'时使用指针)。*
原始作者所做的主要推论之一是:
将默认设置设置为不使用指针,除非您知道它的字段将要更改。
通过价值与参考
- Go按价值通过所有内容。甚至指针将其传递到函数时也会被复制,但副本仍然代表相同的基础内存,因此我们能够更改基础值。
首先,让我们看看一些围绕指针的细节。一个有趣的article强调了Go 是 by-value,但在某些时候听起来不像情况。有原始类型,例如int,string,字节,符文,布尔被认为是值类型,而指针类型为参考。但是,某些类型的性质有些不同,如文章所说:
但是,映射,切片和频道都是 *或 *包含 *参考的特殊类型。
但是,这些类型的有趣之处在于,它们都指向其他一些基本数据,即使您通过任意函数中的值(看似)通过它们来传递它们,也会更改其在功能中的内容也会改变其原始值。举个例子:
package main
import "fmt"
func changeKey(m map[string]string) {
m["key"] = "new_value"
}
func main() {
m := map[string]string{"key": "value"}
fmt.Println(m) // map[key: value]
changeKey(m)
fmt.Println(m) // map[key: new_value]
}
因此,这些地图似乎是参考文献,尽管实际上它们不是。看看这个示例:
package main
import "fmt"
func new(m map[string]string) {
m = make(map[string]string)
}
func main() {
var m map[string]string
new(m)
fmt.Println(m == nil) // true
}
如果MAP M是C ++样式参考变量,则M为MAIN声明,而在Neww中声明的M将在内存中占据相同的存储位置。但是,由于newhas内部对M内部的分配对M中M值没有影响,因此我们可以看到地图不是参考变量。
地图参考是记忆中包含各种字段的内存中映射标头结构(类型HMAP)的指针,包括单元格和其他一些内容。
复制地图或将其传递为值并不是复制下面的hashmap - 它仅将指针复制到标头结构。因此,复制地图参考只是为您提供对内存中同一基础地图的多次参考。是映射参考是逐价传递的,而不是地图的内容
最终结果也被描述为here:
首先,从技术上讲,GO只能通过逐个价值。将指针传递给对象时,您会按值传递指针,而不是通过引用传递对象。区别是微妙的,但偶尔有意义。例如,您可以覆盖对呼叫者没有影响的指针值,而不是将其覆盖并覆盖其指向的内存。
表现
使用指针的主要问题之一是性能。看来,使用指针作为提高代码的整体性能和记忆效率的一种方式并不总是正确的。
Mario Macías备份了适当的基准测试,提出了一个有趣的观点,即返回结构与指针:
尽管复制了一个字段的结构比将指针复制到同一结构较慢,但如果我们考虑 Escape Analysis 特殊性,返回结构值可能比返回指针更快。
。
返回的结构允许编译器检测到创建的数据不能逃脱函数范围并决定在堆栈中分配内存,在该堆栈中,分配和交易点非常便宜,如果与Heap中的内存进行管理。
此外,这个article还指出,为了证明分享指针指向结构的合理性,它应该特别大(您可能会问多大?我不确定,也许几百个字节?)及其生命周期应该很大。还有另一个benchmark表明您很少需要通过其指针共享结构。
事实证明,与指针相比,只有极大的结构才能引起性能较慢。
如果我选择使用指针,则变量本身应在整个持续时间内保持有价值的目的。
由于堆的堆积,垃圾收集器会不时地形成短暂的暂停(平均几毫秒)来清理。由于我们谈论的是堆栈和堆,因此值得注意的是,访问堆栈的速度特别快,如描述here:
堆栈更快,因为访问模式使从中分配和划分内存变得琐碎(指针/整数简单地增加或减少),而堆的分配或交易分配或交易的簿记更为复杂。同样,堆栈中的每个字节往往很频繁地重复使用,这意味着它倾向于将其映射到处理器的缓存,从而使其非常快。堆的另一个性能是,堆主要是全球资源,通常必须是多线程安全的,即每个分配和交易都必须与其他所有其他分配和交易。堆在程序中访问。
那么,这与我们如何?好吧,事实证明,我们使用函数的返回值或输入参数(即决定是否使用指针)可能会导致分配。 James Kirk在article中彻底而精美地描述了这个过程:
我们可以从中推断出的一般规则是,共享堆栈的指针会导致分配,而在堆栈下共享点并不是。
我们将在以下部分中看到有关堆栈和堆的更多信息。
至少对我来说,我多次看到的主要事情之一是,如果一个团队希望发展并改善他们的许多服务,项目和整体代码的加时性,那么这对优先考虑代码的可读性。写一些惯用性并且可以很容易地推论的东西似乎是一种更成功的方法,而不是试图以代码为代价而实现最微小的优化,这是不可读的混乱。在Golang中,通常编写惯用和可读的代码似乎非常重要和价值。威廉·肯尼迪(William Kennedy)描述了这一指数和可能的性能权衡的想法:
我被问到何时以及何时不使用指针的情况。大多数人遇到的问题是,他们试图根据自己认为的绩效权衡做出这一决定。因此,问题是,您可能会根据您对绩效的毫无根据的想法做出编码决策。根据代码惯用,简单,可读和合理的代码做出编码决策。
后来补充说,通常,如果我们将实施分组为原始数据值或要更改的原始数据值,那么与指针共享更改的数据是明智的:
如果您从标准库中查看更多代码,您将看到结构类型是如何作为原始数据值(例如内置类型)实现的,或者将其实现为需要与指针共享而从未复制的值。给定结构类型的工厂功能将为您提供有关如何实现类型的很好的线索。
。
通常,除非实现了像原始数据值的行为一样,除非实现结构类型,否则共享结构类型值。
以及:
如果您仍然不确定,这是另一种思考方式。将每个结构都视为具有性质的。如果结构的性质不应更改,例如时间,颜色或坐标,则将结构作为原始数据值实现。如果结构的性质可以更改,即使它不在您的程序中,它也不是原始的数据值,应实现以与指针共享。不创建具有自然双重性的结构。
顺便说一句,我不确定我是否能完全理解最后一句话。自然的双重性对我来说似乎不是一个明确的事情。老实说,到目前为止,我看到的大多数结构都具有不断变化的性质,而这些结构也具有自己的一致和不变的内在状态。为了为您提供一个例子,请考虑一个结构从起源到目的地的途中解释车辆(如果这个示例证明是半烘烤而不是真的,我很抱歉,这只是一个例子):
package main
type Point struct {
Lat float64
Long float64
}
type Vehicle struct {
id string
Origin Point
Destination Point
GasolineLeft float64
}
我并不是说他错了,一点都不说,我所说的是,这种自然的双重性是可以在这里和那里观察到的,这很好(您可以争辩说,上面的设计可以以一种可能与ID或其他物体一起存储在某个地方的方式更改,但是您明白了)。我自己对这篇文章的看法是为将要稍微改变的结构使用指针。
和参考类型(例如,地图,切片,木,接口和函数值)仅在极少数情况下与指针共享,如果您不确定,只需使用指针即可。
参考类型是切片,地图,通道,接口和功能值。这些值包含一个标头值,该值通过指针和其他元数据引用基础数据结构。我们很少与指针共享参考类型值,因为标题值设计为复制。标题值已经包含一个指针,该指针默认情况下为我们共享基础数据结构。
如果您从标准库中查看更多代码,您将看到在大多数情况下未与指针共享参考类型中的值。由于参考类型包含一个标题值,其目的是共享基本数据结构,因此不需要与指针共享这些值。已经有一个指针。
通常,除非您实现非符号类型的功能,否则不要与指针共享参考类型值。
堆栈与堆
我们发现,总体上分享堆栈的指针会导致内存分配。让我们深入研究。
本节的灵感来自Jacob Walker,在他们的视频中。
有两种记忆, stack 和 heap ,并且使用GO,我们有多个堆栈(即每个Goroutine的堆栈),以及一个基本上的堆容纳其他不在堆栈中的所有内容。 我们真的可以自己分辨一个变量是否会在堆栈或堆上,但主要问题是真的很重要吗? 为了正确的程序,没有。但这确实会影响程序的性能,因为堆上的所有内容都是由垃圾收集器管理的,这会导致整个程序的延迟。 首先,让我们确保我们不会过度花时间在工程上,并优化我们的程序,而我们没有真正的需求,也没有基准证明性能差。换句话说,如果程序足够快,那就让它成为,您可能还有其他更重要的事情。 在黑暗中不优化。 如果您最终决定优化程序,请先优化正确性,而不是性能。话虽如此,让我们研究几个不同的情况,以弄清楚是如何决定在堆栈还是在堆上放置变量。 考虑下面没有指针的示例: 为什么我要使用println代替fmt.println?只需继续阅读! 启动程序时,将为func main创建一个堆栈框架,值n = 4和n2 = 0。当func Square被调用时,将为该功能创建另一个帧,该功能包含X = 4。弹奏平方返回后,结果n2 = 16将在第一个堆栈框架中设置。 GO本身不会清理,第二个堆栈框架仍在堆栈中可用,而GO的作用是跟踪有效的内容和无效的内容(您可以在下图中看到的黑线)。最后,当println称为新框架时,将为它创建一个新的帧,并将此新堆栈放置在有效的部分中。该线向上和向下移动以定义有效的和无效的区域。这就是为什么堆栈被视为自我清洁的原因,堆栈上的任何变量都被清理了他们是)。 现在,让我们添加一些指针: 再次,在记忆中浏览此代码,然后跳到有趣的部分,当func inc被调用时,为其创建了一个新的堆栈框架,其中包含x = 0xC00000000444780,该框架实际上指向第一个堆栈中的n = 4。当对N的指针进行删除和递增时,第一个堆栈中N的值进行了更新,第二个堆栈仅将其推入无效的部分。到达println时,此功能会收回Func Inc拥有的空间,并且该过程像以前一样继续。我们在这里使用指针,但可以留在堆栈中。 分享通常留在堆栈上。 返回指针? 当我们第一次调用main时,它将n = nil以(在其原始堆栈框架中)开始,正如称为func答案时,编译器已经知道,放置可变x(由Func Answers创建)是不安全的)在堆栈框架上,因此将X声明在堆上的某个地方声明。 ``为什么?您可能会问,如果要在堆栈框架上声明该变量,则N必须指向无效部分中的值(该值为x = 42,而N将是某种内容像0xc00000044770一样,这将引起问题。我们说X逃到了堆中,但实际上并没有在运行时移动,这在编译时发生。编译器最初知道此变量将是在堆上构建。 分享通常逃到堆中。 通常在此处和上面的引文中,这意味着实际上, 只有编译器才知道 。 根据Golang FAQ: 在可能的情况下,GO编译器将分配该功能堆栈帧中局部的变量。但是,如果编译器无法证明函数返回后未引用该变量,则编译器必须在收集的垃圾收集堆上分配变量以避免悬挂的指针错误。 我们在这里所说的称为“逃生分析”。编译器的作用是,查看我们的代码以查看这些变量是否需要在堆上。 在当前的编译器中,如果变量已接收地址,则该变量是在堆上分配的候选者。但是,基本逃生分析识别某些情况,当此类变量无法通过该功能返回而可以驻留在堆栈上时。 所以,如果只有编译器知道,让我们问它! 我们使用GO构建来构建程序,但编译器实际上是GO工具编译。 在这里,我们可以看到编译器决定是否在编译时间上放置变量。 但是有一个捕获!您可能已经注意到,我考虑使用println而不是FMT.Println,这可以说是打印变量的常见方法。为什么这是?
这似乎是Golang Github上的一个空旷的问题: 所以让我结束。 何时在堆上构建值? 当构建值返回的函数之后,可能会引用一个值。 编译器确定值太大而无法放在堆栈上时。 编译器不知道编译时值的大小(即,在切片的情况下)。 一些通常分配的值(不是详尽的): 与指针共享的值。 存储在接口变量中的变量。 func字面变量(或匿名函数)。 用于地图,通道,切片和字符串的备份数据(字符串实际上是特殊的字节片)。 在上面的所有讨论之后,可能仍然不清楚是否要传递结构或指针的副本,还是使用指针接收器。因此,让我们回顾各种来源的一些意见。 完美地描述了一个总体结论here: 使用接收器的方法指针很常见; the rule of thumb for receivers is,如果有疑问,请使用指针。 可以找到另一个内容丰富的讨论here: 结构合理大?过早的优化很少是好的,但是如果您拥有的结构不仅具有少数字段和/或包含大字符串或字节数组(例如,降级主体)的字段和/或字段,将返回指针而不是副本的好处变得更加明显。<<<<<<<<<<<<<<<<<<<<<<<< br>
返回函数的风险突变结构(或对象)返回之后? (即一些长期运行的任务或工作) 在同一讨论中,威廉·肯尼迪(William Kennedy)指出: 我有一些不同的哲学。我的基于对您定义的类型做出决定。问问自己一个问题。如果某人需要从这种类型的值中添加或删除某些东西,我应该创建一个新值还是突变现有值。这个想法来自研究标准库。 在某些情况下,我们(错误地)认为表现不佳的主要原因是没有很好地利用指针,但是有了更多的评估,我们找出了一种新的做事方式(例如,更好的设计)为了以可读性和惯用方式提高软件的性能,而无需通过引入不必要的指针来增加代码的复杂性和混乱。正如Go的禅宗所建议的: 如果您认为它很慢,请首先用基准测试。在实践中,我从来不必关心指针和非分支之间的差异的性能水平。我相信这可能重要的情况非常罕见。 在本文中(实际上也是我的第一篇!),我试图探索指针的神秘区域,并回答是否使用指针的问题。 我对上述参考文献的推论是尝试不使用指针。如果面对性能差,首先只用基准来证明这一点,并且对性能没有做出毫无根据的假设。如果您确保实际上 *确实存在性能问题,请重新评估代码,以期提出更好的设计。如果不这样做,或者出于某种原因的设计变化似乎是合理的,那么我想您别无选择,只能使用指针。 无论如何,在某些情况下,使用指针似乎是合理的。一些更常见的是: 您要处理一个变化的结构。 您正在处理互惠符,这与变化的结构相同(即,它不断被锁定和解锁)。 您的结构特别大(如果是配置结构或其他内容,则假想阈值约为几百个字节)。 我们看到,尽管指针的记忆力有效(因为它们阻止了整个结构被复制,而是只复制了指针本身),如果不正确使用,它们倾向于产生许多间接费用,导致性能退化以及引入不必要的复杂性和混乱。 最后,我要感谢您阅读此博客,希望这完全不会浪费您的时间!我很想向你们学习更多技术知识,并希望听到您对我的著作的建议和看法。您可以通过LinkedIn和Gmail与我联系。
*免责声明:
package main
func main() {
n := 4
n2 := square(n)
println(n2)
}
func square(x int) int {
return x * x
}
package main
func main() {
n := 4
inc(&n)
println(n)
}
func inc(x *int) {
*x++
}
package main
func main() {
n := answer()
println(*n/2)
}
func answer() *int {
x := 42
return &x
}
go help build
usage: go build [-o output] [build flags] [packages]
...
-gcflags '[pattern=]arg list'
arguments to pass on each go tool compile invocation.
go tool compile -h
usage: compile [options] file.go...
...
-l disable inlining
...
-m print optimization decisions
# Example 1
go build -gcflags "-m -l" example1.go
# no output, since inlining is disabled
# Example 2
go build -gcflags "-m -l" example2.go
./example2.go:9:10: x does not escape
# Example 3
go build -gcflags "-m -l" example3.go
./example3.go:9:2: moved to heap: x
# Example 1
go build -gcflags "-m -l" example1.go
./example1.go:8:13: ... argument does not escape
./example1.go:8:14: n2 escapes to heap
# Example 2
go build -gcflags "-m -l" example2.go
./example2.go:11:10: x does not escape
./example2.go:8:13: ... argument does not escape
./example2.go:8:14: n escapes to heap
# Example 3
go build -gcflags "-m -l" example3.go
./example3.go:11:2: moved to heap: x
./example3.go:7:13: ... argument does not escape
./example3.go:7:17: *n / 2 escapes to heap
选择哪一个?
切片,地图,频道,字符串,功能值和接口值是用指针在内部实现的,而指向它们的指针通常是冗余的。
在其他地方,将指针用于**大结构**或您必须更改的结构,否则pass values,因为通过指针使事情变得惊讶,使事情变得混乱
我几乎总是从构造函数返回指针,因为构造函数应运行一次。
概括