我知道 Go 中的指针允许函数参数的突变,但如果它们只采用引用(使用适当的 const 或 mutable 限定符),那不是更简单。现在我们有了指针,并且对于一些内置类型,如地图和通道,隐式通过引用传递。
我是否遗漏了某些东西,或者 Go 中的指针只是不必要的并发症?
指针之所以有用有几个原因。指针允许控制内存布局(影响 CPU 缓存的效率)。在 Go 中,我们可以定义一个所有成员都在连续内存中的结构:
type Point struct {
x, y int
}
type LineSegment struct {
source, destination Point
}
在这种情况下,Point
结构嵌入在LineSegment
结构中。但是你不能总是直接嵌入数据。如果要支持二叉树或链表等结构,则需要支持某种指针。
type TreeNode {
value int
left *TreeNode
right *TreeNode
}
Java、Python 等没有这个问题,因为它不允许您嵌入复合类型,因此无需在语法上区分嵌入和指向。
实现相同目的的一种可能替代方法是区分struct
和 ,class
就像 C# 和 Swift 所做的那样。但这确实有局限性。虽然您通常可以指定函数将结构作为inout
参数以避免复制结构,但它不允许您存储对结构的引用(指针)。这意味着当您发现它有用时,您永远不能将结构视为引用类型,例如创建池分配器(见下文)。
使用指针,您还可以创建自己的池分配器(这非常简化,删除了许多检查以仅显示原理):
type TreeNode {
value int
left *TreeNode
right *TreeNode
nextFreeNode *TreeNode; // For memory allocation
}
var pool [1024]TreeNode
var firstFreeNode *TreeNode = &pool[0]
func poolAlloc() *TreeNode {
node := firstFreeNode
firstFreeNode = firstFreeNode.nextFreeNode
return node
}
func freeNode(node *TreeNode) {
node.nextFreeNode = firstFreeNode
firstFreeNode = node
}
指针还允许您实现swap
. 那就是交换两个变量的值:
func swap(a *int, b *int) {
temp := *a
*a = *b
*b = temp
}
Java 一直无法在 Google 等地方完全取代 C++ 进行系统编程,部分原因是由于缺乏控制内存布局和使用的能力(缓存未命中会显着影响性能),性能无法调整到相同的范围。Go 的目标是在许多领域取代 C++,因此需要支持指针。
我真的很喜欢来自http://www.golang-book.com/8的例子
func zero(x int) {
x = 0
}
func main() {
x := 5
zero(x)
fmt.Println(x) // x is still 5
}
与
func zero(xPtr *int) {
*xPtr = 0
}
func main() {
x := 5
zero(&x)
fmt.Println(x) // x is 0
}
Go 被设计成一种简洁、极简的语言。因此,它仅从值和指针开始。后来,根据需要,添加了一些引用类型(切片、映射和通道)。
Go 编程语言:语言设计常见问题解答:为什么是映射、切片和通道引用,而数组是值?
“关于这个话题有很多历史。早期,地图和通道在语法上是指针,不可能声明或使用非指针实例。此外,我们在数组应该如何工作方面苦苦挣扎。最终我们决定严格分离指针和值的组合使语言更难使用。引入引用类型,包括处理数组引用形式的切片,解决了这些问题。引用类型给语言增加了一些令人遗憾的复杂性,但它们对可用性有很大影响:Go 成为当他们被介绍时,他们的语言更高效、更舒适。”
快速编译是 Go 编程语言的主要设计目标;这是有代价的。损失之一似乎是将变量(基本编译时常量除外)和参数标记为不可变的能力。有人要求,但被拒绝了。
“将 const 添加到类型系统会强制它出现在任何地方,并在发生变化时强制将其删除。虽然以某种方式将对象标记为不可变可能会有一些好处,但我们认为 const 类型限定符不是方式去。”
引用不能被重新分配,而指针可以。仅这一点就使得指针在许多无法使用引用的情况下很有用。
我不会在“Go”的上下文中回答这个问题,而是在任何实现“指针”概念的语言(例如 C、C++、Go)的上下文中回答这个问题;同样的推理也可以应用于“Go”。
通常有两个内存部分发生内存分配:堆内存和堆栈内存(我们不要包括“全局部分/内存”,因为它会脱离上下文)。
堆内存:这是大多数语言所使用的:无论是 Java、C#、Python ......但它带有一个称为“垃圾收集”的惩罚,这是对性能的直接影响。
堆栈内存:变量可以在 C、C++、Go、Java 等语言的堆栈内存中分配。栈内存不需要垃圾回收;因此它是堆内存的高性能替代品。
但是有一个问题:当我们在堆内存中分配一个对象时,我们得到一个“引用”,它可以传递给“<em>多个方法/函数”,它是通过引用,“<em>多个方法” /functions”可以直接读取/更新同一个对象(分配在堆内存中)。可悲的是,堆栈内存并非如此。正如我们所知,无论何时将堆栈变量传递给方法/函数,只要您有“指针的概念”(如 C、C++、Go 的情况),它就会“按值传递”(例如 Java)。
这是指针出现的地方。指针让“<em>多个方法/函数”读取/更新放置在堆栈内存中的数据。
简而言之,“<strong>指针”允许使用“<strong>堆栈内存”而不是堆内存,以便通过“<strong>多个方法/函数”处理变量/结构/对象;因此,避免了垃圾收集机制造成的性能损失。
在 Go 中引入指针的另一个原因可能是:Go 应该是一种“高效的系统编程语言”,就像 C、C++、Rust 等一样,并且与底层操作系统提供的系统调用和许多系统调用一样顺畅地工作API 在其原型中有指针。
有人可能会争辩说,它可以通过在系统调用接口之上引入一个无指针层来实现。是的,它可以做到,但是拥有指针就像非常接近系统调用层,这是良好系统编程语言的特征。