

Golang - 关于指针与性能
source link: https://hedzr.com/golang/pointer/go-pointer/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

引子Permalink
指针是指向内存某区域起始点的一个地址值。所以你需要对计算机组成原理有充分的认识。一般说来,保持对这门课程讲述的内容的深刻的记忆,然后日积月累,才会真正达成理解。
在高级语言中,指针的概念和低级界面上的指针并无本质的不同。只是因为高级语言往往需要在不同程度上对底层物理结构进行包装,所以指针在不同的高级语言中,有着不同的表现形式。
然而,无论有多少的不同,在高级语言中有一点是相同的,如果一个指针是被分配的,那么你需要关心它何时被解除分配。当然,在带有 GC 的高级语言中,这一点往往表现的更为隐晦,因为在这些语言中,你真正关心的是:
- 我创建的对象是不是能够被合理地回收
- 合理回收我的对象不会消耗过分的 CPU 时间
- 我的对象不会因为引用计数的原因迟迟得不到回收,进而导致内存不够
- Oops,Null Exception!这是有 GC 的呀
而在不带有 GC 的高级语言中,我们可能关心的是:
- 我创建了多少对象,我析构了它们吗
- 我创建了大量的对象和频繁地析构,会否导致堆空间碎片问题
- 堆空间碎片重整算法会不会太频繁而导致 CPU 超高
- 会不会因为碎片太多而得到内存不足,哪怕实际上还有充分的内存
- 我的智能指针用对了吗
- Oops!Null Exception
自从指针这个概念问世以来,它就是一个关键性问题。
所以,在 Golang 中我打算总结一次指针如何用。当然,也就是马马虎虎地小结一下,梳理一下自己长久以来的各种碎片知识。
你需要有较充分的 Golang 编码经验,因为文内会忽略 Golang 编码的基本知识。
CasesPermalink
应该使用结构还是它的指针?Permalink
你应该读一读 Go: Should I Use a Pointer instead of a Copy of my Struct? - by Vincent Blanchon - A Journey With Go - Medium,他们证明了 struct 的 copy 可能通常会 8 倍速快于使用 struct 指针。原因在于使用指针使得 struct 变量被置于 heap(经过逃逸分析后),而 GC 因此而受到更大的压力。
换句话说,使用指针时,GC回收一个变量的开销更大,而且大到了足以影响效率的程度。这个实在是违背C++程序员的直觉啊。
go 的值传递没有想象中那么大的开销,原因一部分是因为副本的复制比想象中更快。
是这样吗Permalink
然而,只要你拿不准是否该使用指针,一切情况下,请一律使用指针。基本上你不必担心使用指针导致的 GC 压力。
因为除非你在频繁地制造小对象并且立即抛弃它们,否则大多数时候 GC 都不会试图立即回收你的指针及其指向的 struct 等对象,所以所谓的 GC 压力只是虚妄。
想象一下,我们为什么使用结构?因为有一个块包含不同的数据,我们需要描述这个块。所以当我们使用 struct 时,我们往往需要它生存较长的时间,而显然地,在它的生存周期里 GC 必须对它总是视而不见。所以一般情况下,你总是应该使用指针,就是在讲这样的场景。
也就是说,struct 是不是应该立即构造为指针对象,取决于:
- 生存周期长短:长周期总是用指针,短的嘛随意好了
- 结构实体尺寸大小:一个结构几百字节甚至几十KB的话,你最好采用指针,否则极端情况下你的栈空间甚至会溢出,特别要注意这种情况,它不但难以检测,而且是安全风险点。
此外,struct 也有一些通识:
-
当你需要修改结构题内部数据时,必须使用指针
type xS struct { Status bool } func (x xS) SetStatusButWrong(b bool) { x.Status = b } func (x *xS) SetStatus(b bool) { x.Status = b }
-
当结构类型包含
sync.Mutex
或者 sync 包中对象这样的字段成员时,总是使用指针构造方式,防止潜在的拷贝可能性type xinS struct { sync.RWMutex } func newXinS() *xinS { return &xinS{} }
接收器Permalink
结构的 receiver 被推荐优先选用指针。原因见前面的通识部分案例,因为你往往需要从结构之外对其进行设置(写入)。
这个坑/特性,已经很著名了。所以本小节不再赘述了。
作为方法参数Permalink
一般来说,将 struct 作为方法参数时,总是使用指针形式。
即使当你在使用一个 struct 对象而非其指针构造形式时,如果将其传递给方法,也应该采用取地址的方式传递它的指针形态。除非你明确地想要结构的副本被传递给方法(或许你想要避免方法给该结构带来副作用)。
采用指针形态进行结构传参时,可以避免对结构实体进行拷贝。
type xS struct {
I8 int8
I int
}
func A(){
x := x{ 1, 2, }
B(&x)
println(x)
}
func B(x *xS) {
x.I++
}
在代码示例中,B 方法有两个特性:
- 可以避免传参 x 时不必要的结构体拷贝
- 能够修改 x 的成员,并能将这一副作用返回给调用者
明确到这样两个事实之后,根据你的实际需要去决定要不要使用指针形态。但你的实际需要,根据经验来说,都会 say yes I do。
基本类型Permalink
诸如 bool,int,float,string 这样的基本类型,通常不必使用指针。
一来它们的内存拷贝代价不高。二来针对它们往往有语言层面的特殊优化,你不必在这些细枝末节上消耗脑力。
值得注意的是,基本类型(Primitive Datatypes)有时候略微有一点歧义。尤其是在 string 对象上,不同语言是否对它有不同的设计方案以及取舍策略。
不过在 Golang 中,我们可以将 string 视作基本类型。按照 Golang 语法规范的定义,它也确实是基本类型的一种。
map 等等Permalink
map,slice,channel 不需要使用指针。
原因很有意思,因为它们本身就是指针:
var m map[string]bool
if m == nil {
println("an empty (or uninitialized) map is always nil")
}
所以清空一个数组是这样的:
var ss []int
// ... append some items into ss
// and reset ss
ss = nil
其它可以自行想象。
使用 []T
还是 []*T
Permalink
只要有可能,总是使用 []*T
。
因为当采用 range 迭代时,range 操作符总是会对 item 进行拷贝。所以对于 []T
来说,range 迭代时是无法修改 T 的内容的——或者说,修改了也会被立即废弃,无法被体现到 []T
中去。
type T struct {
I8 int8
}
func Test(){
var ta []*T
for i := 0; i < 10; i++ {
ta = append(ta, &{ int8(i) })
}
for _, t := range ta {
t.I8++
}
}
在上面的代码示例中,ta
最终会正确增量,得到一个 1..10 的数组。
总结Permalink
本文前面介绍的全是废话,所有列举的准则条目都不是真的。真的只有一句话:
除了基本类型,map,slice,channel 等等,在一切有可能的时候,一律采用指针。
至于谁谁谁在说 XXX 能够带来 n 倍的性能提升。忘记他们吧,性能提升、逃逸分析全数都是无意义的,你根本无需考虑额外的因素。一个残酷的事实是:优化根本轮不到你来考虑,即使你时刻考虑到优化也不能代表你是一个好程序员,说不定正好相反。
为什么?Permalink
记住这样的公认的准则:
- 不要过早优化
- 产品定型后,利用 profiling 技术来寻找优化点
作为一个程序员,写对代码是基本要求。在做不到这一点之前,别去人云亦云地讨论性能。
ReferencesPermalink
Recommend
-
52
在Go语言中,有几种东西可以代表“指针”。 1. uintptr类型:该类型实际上是一个数值类型,也是Go语言内建的数据类型之一。根据当前计算机架构的不同,它可以存储32位或64位的无符号整数,可以代表任何指针的位(bit)模式,...
-
44
square-gopher.png 指针 在 go 语言中指针没有 c++ 中那么复杂,因为没有指针的运算。...
-
42
Go中一切都通过值传递,也就是说,一个函数总是得到值传递的副本,总是会分配一个值的副本给函数参数。例如 将int值传递的是int值的副本; 指针传递指针的副本,而不是指针指向的数据; map 和 slice...
-
40
首先我们要明确: (1)基本数据类型:变量存的就是值,也叫值类型; (2)获取变量的地址,用&,例如var num int,获取num的地址:&num; (3)指针类型:变量存的是一个地址,这个地址指向的空间存...
-
18
Go指针理解 Go 有指针,但是没有指针运算。你不能用指针变量遍历字符串的各个字节。在 Go 中调用函数的时候,得记得变量是值传递的。 通过类型作为前缀来定义一个指针’ * ’:var p * int。现在 p 是一个指向整数值的...
-
27
一,变量类型 变量分为值类型,指针类型和引用类型。 以如下变量定义和赋值语句为例: package main import( "fmt" ) func main(){ var a int = 1 p := &a fmt.Printf("%v\n", a)...
-
12
OpenMix 出品: https://openmix.org Mix XFMT 可以打印结构体嵌套指针地址内部数据的格式化库 Formatting library that can print the internal data of the nes...
-
9
golang的数组和指针的问题,求解答 txgu · 45分钟之前 · 39 次点击 · 预计阅读时间不到 1 分钟 ·...
-
12
原博客地址 golang的三种指针类型 具体类型的指针,如int、string等 unsafe.Pointer,在unsafe下面,任何具体类型的指针都能转化成Pointer,...
-
8
Example Domain This domain is for use in illustrative examples in documents. You may use this domain in literature without prior coordination or asking for permission.
About Joyk
Aggregate valuable and interesting links.
Joyk means Joy of geeK