我知道Go中的指针允许函数参数的突变,但是如果它们仅采用引用(带有适当的const或可变限定符),那不是更简单吗?现在,我们有了指针,并为一些内置类型(如地图和通道)隐式地按引用传递。
我是否缺少某些东西,或者Go中的指针仅仅是不必要的复杂化?
我知道Go中的指针允许函数参数的突变,但是如果它们仅采用引用(带有适当的const或可变限定符),那不是更简单吗?现在,我们有了指针,并为一些内置类型(如地图和通道)隐式地按引用传递。
我是否缺少某些东西,或者Go中的指针仅仅是不必要的复杂化?
Answers:
我真的很喜欢从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
}
指针之所以有用,有几个原因。指针允许控制内存布局(影响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等不存在此问题,因为它不允许您嵌入复合类型,因此无需在语法上区分嵌入和指向。
实现此目标的一种可能替代方法是像C#和Swift一样区分struct
和class
。但这确实有局限性。虽然通常可以指定函数将结构体作为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从未能够完全取代C ++在诸如Google之类的地方进行系统编程,部分原因是由于缺乏控制内存布局和使用的能力而无法将性能调整到相同的程度(高速缓存未命中会严重影响性能)。Go旨在在许多领域取代C ++,因此需要支持指针。
Go被设计为简洁的极简语言。因此,它仅从值和指针开始。后来,根据需要添加了一些参考类型(切片,地图和通道)。
Go编程语言:语言设计常见问题解答:为什么数组是值时映射,切片和通道引用?
“关于该主题有很多历史。在早期,地图和通道在语法上是指针,因此不可能声明或使用非指针实例。此外,我们在数组应如何工作方面也遇到了麻烦。最终,我们决定严格分离指针和值的使用使该语言更难使用。引入引用类型(包括用于处理数组引用形式的切片)解决了这些问题。引入这些语言后,您会获得更高效,更舒适的语言。”
快速编译是Go编程语言的主要设计目标。有其成本。伤亡之一似乎是将变量(基本编译时间常数除外)和参数标记为不可变的能力。已被要求,但是被拒绝了。
“在类型系统中添加const会强制它出现在任何地方,如果发生某些更改会强制将其删除到任何地方。虽然以某种方式标记不可变的对象可能会有一些好处,但我们认为const类型限定符并不是去。”