What's the point of having pointers in Go?

我知道 Go 中的指针允许对函数的参数进行变异,但是如果它们只采用引用(使用适当的 const 或可变的限定符) ,不是更简单吗。现在我们有了指针,对于某些内置类型,如地图和通道,可以通过引用隐式传递。

是我遗漏了什么,还是围棋中的指针只是不必要的复杂?

30632 次浏览

不能重新分配引用,而指针可以。仅这一点就使得指针在许多不能使用引用的情况下非常有用。

Go 被设计成一种简洁的、极简主义的语言。因此,它只是从值和指针开始。后来,由于需要,添加了一些引用类型(片、映射和通道)。


Go 编程语言: 语言设计常见问题解答: 为什么映射、片和通道是引用,而数组是值?

“这个话题有很多历史。早期,映射和通道在语法上是指针,不可能声明或使用非指针实例。此外,我们还在纠结数组应该如何工作。最终,我们决定严格分离指针和值使得语言更难使用。引入引用类型(包括处理数组引用形式的片)解决了这些问题。参考类型给语言增加了一些令人遗憾的复杂性,但是它们对可用性有很大的影响: 当它们被引入时,Go 成为了一种更高效、更舒适的语言。”


快速编译是 Go 编程语言的一个主要设计目标,这是有代价的。损失之一似乎是将变量(基本编译时间常数除外)和参数标记为不可变的能力。有人要求,但被拒绝了。


果仁: 去语言。一些反馈和怀疑。

”向类型系统添加 const 会迫使它出现在任何地方,并且 forces one to remove it everywhere if something changes. While there 可能对标记不可变的对象有一些好处,但我们没有 我认为常量类型限定符是一个很好的选择。”

我真的很喜欢从 https://www.golang-book.com/books/intro/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 等没有这个问题,因为它不允许嵌入复合类型,所以没有必要在语法上区分嵌入和指向。

用 Go 指针解决 Swift/C # 结构的问题

一个可能的替代方法是像 C # 和 Swift 那样区分 structclass。但这也有局限性。虽然通常可以指定函数将 struct 作为 inout参数以避免复制 struct,但是它不允许存储对 struct 的引用(指针)。这意味着当你发现一个结构体是有用的时候,你永远不能把它当作一个引用类型,例如创建一个池分配器(见下文)。

自定义内存分配器

使用指针,您还可以创建自己的池分配器(这非常简单,删除了大量检查,只显示原则) :

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”的语境中回答这个问题,我会在任何实现了“指针”概念的语言(例如 C,C + + ,Go)的语境中回答这个问题; 同样的推理也可以应用于“ Go”。

内存分配通常发生在两个内存部分: 堆内存和堆内存(我们不要包括“全局部分/内存”,因为它会脱离上下文)。

堆内存 : 这是大多数语言使用的: 可以是 Java,C # ,Python... ... 但是它带来了一个名为“垃圾收集”的惩罚,这是一个直接的性能打击。

栈内存 : 变量可以用 C、 C + + 、 Go、 Java 等语言在栈内存中分配。堆栈内存不需要垃圾回收; 因此它是堆内存的一种性能替代方案。

但是有一个问题: 当我们在堆内存中分配一个对象时,我们得到一个可以传递给“ 多个方法/函数”的“ Reference”,通过引用,“ 多个方法/函数”可以直接读取/更新相同的对象(在堆内存中分配)。遗憾的是,堆栈内存的情况并非如此; 正如我们所知,每当堆栈变量被传递给方法/函数时,它都是“通过值传递的”(例如 Java) ,只要你有“指针的概念”(例如 C,C + + ,Go)。

这里是指针进入图片的地方。指针让“ 多个方法/函数”读取/更新放置在堆栈内存中的数据。

简而言之,“ 指示”允许使用“ 堆栈内存”而不是堆内存,以便通过“ 多个方法/函数”处理变量/结构/对象; 因此,避免垃圾收集机制造成的性能损失

在 Go 中引入指针的另一个原因可能是: Go 应该是一种“高效的 系统编程语言”,就像 C、 C + + 、 Rust 等语言一样,并且可以顺利地使用底层操作系统提供的系统调用,因为许多系统调用 API 的原型中都有指针。

有人可能会争辩说,它可以通过在系统调用接口之上引入一个无指针层来实现。是的,这是可以做到的,但是拥有指针就像是在非常接近系统调用层的地方工作,这是一个好的 系统编程语言的特点。