大多数人不明白为什么 Go 使用指针而不是引用
我们为什么要关心这个问题?
当我第一次在围棋中接触到指针时,我完全懵了。这是很多人在刚开始学习 Go 时都会问的问题。我之前在 Java 和 Python 中玩过,在 Java 和 Python 中,所有东西都能神奇地使用引用,而指针就像是 C 语言的可怕回溯。因此,在 Go 中看到指针让我觉得 “他们为什么要这么做?这不是应该很简单吗?
但后来我坚持了下来,并发现了一些问题。指针并不只是一些老派的麻烦,它们的存在是有目的的。它们能让你控制内存的工作方式,从而让你的代码变得更快,而且一旦掌握了窍门,代码也会变得更容易理解。老实说,我从最初的困惑到现在的喜欢。今天,我想带你了解 Go 为什么使用指针、指针如何帮助我们以及指针的实际效果。如果你正在写围棋–或者只是好奇–这些东西是值得了解的。我保证,这并不像看起来那么棘手!
什么是指针和引用?
Go 中的指针,化繁为简
好吧,那什么是指针呢?它只是一个变量,告诉你某些数据在计算机内存中的位置。想想看,就像知道你最喜欢的书在哪个书架上一样。比如我写 x := 15
。指向 x 的指针知道 15
藏在哪里。我们使用 &
来获取该地址,使用 *
来窥视或调整该值。看看这个
x := 15
p := &x
print(*p) # outputs 15
*p = 25
print(x) # now it’s 25
看到了吗?很简单 你指着那个地方,改变那里的东西,你就是老板。没有奇怪的惊喜。
其他地方的引用
那么引用呢?在 Java 中,当你传递一个对象时,它就是一个引用–你并没有复制整个对象,只是以一种偷偷摸摸的方式指向它。这种语言会帮你处理,你不用去管地址。C++ 也有引用,它们就像是变量的昵称。这很简单,但有时你会抓耳挠腮,不知道到底发生了什么变化。Go 不玩这种游戏,这就是指针的用武之地。
为什么 Go 每次都选择指针
那么围棋为什么要抛弃指针呢?首先,这一切都与清晰度有关。有了指针,你就知道你正在处理的是真实的数据,而不是某个消失的副本。而引用可以隐藏发生了什么,我见过有人因此而绊倒。其次,这与速度有关。指针可以让你跳过拷贝大块数据的过程,因此 Go 可以快速运行,不需要额外的麻烦。第三,它让事情变得简单。其他语言中的引用都有这些规则,比如 Java 或 C++,但 Go 的指针呢?它们就在那里,做着自己的工作。你可以获得强大的功能,却不会感到头疼。
Python 如何处理指针
要真正理解 Go 为什么这样做,让我们来看看 Python。Python 中的一切都是引用。向函数传递一个 list?你传递的是引用,而不是拷贝。这很方便,但它也会咬你一口。比如,如果你在函数中改变了列表,原始列表也会随之改变–有时你并不希望它改变。我以前就搞砸过!在 Go 中,指针会让你很清楚地知道什么在改变。无需猜测,代码会直接告诉你。
指针的救星
修复不会改变的结构体
你是否写过一个函数来更新结构体,但它就是……不更新?这是因为 Go 在传递数据时会复制内容。如果没有指针,你的更改就会消失。指针可以解决这个问题。想象一下,一个 User
结构有名字和年龄。没有指针?什么都不会更新。有指针呢?嘣,它就能工作了。
不在复制上浪费时间
复制大数据是一件麻烦事。想象一下一个列表中有一百万个数字的结构体。在没有指针的情况下传递它,Go 会创建一个全新的副本–既慢又浪费。有了指针,你只需发送地址。它体积小、速度快,非常适合服务器等速度要求较高的应用。
在函数内部更改内容
有时,你需要用一个函数来调整多项内容。如果没有指针,你就只能返回新值,这就会变得一团糟。而指针则可以保持清洁。想交换两个数字?指向它们,交换,完成。
使用 goroutines 共享数据
Go 的 goroutines 非常出色,而指针则让它更胜一筹。如果有多个任务需要更新同一内容,指针可以让它们都使用真实数据。只要锁定指针,它们就不会互相干扰。在 Go 中,这是一个救星。
检查是否有遗漏
指针还可以说:”嘿,这里什么都没有。” 一个 *int
可以是 nil
–无值。而普通 int
不能。这对于可选项来说非常有用,比如当函数还不具备所有细节时。
现在我们来看一些代码
开始的基本指针
这里有一个简单的例子。我们给一个数字加 1:
func add_one(n *int) { *n = *n + 1 } num := 7 add_one(&num) print(num) # outputs 8
*int
表示我们使用的是指针,&num
表示地址。在内部,我们将其向上提升。简单明了。
使用结构体的指针,一步一步来
现在,让我们试试结构体。我们有一个 User
,想要更新它:
type User struct { name string age int } func make_older(u *User) { u.age = u.age + 1 } func change_name(u *User, new_name string) { u.name = new_name } u := User{"Ravi", 30} make_older(&u) change_name(&u, "Ravi Kumar") print(u.name) # outputs Ravi Kumar print(u.age) # outputs 31
如果没有指针,u
就会卡住。在这里,因为我们使用了 &u
,所以它会更新。我添加了名称变化,以便更直观地展示它。
使用指针让代码更快
如果数据量很大怎么办?看看这个:
type BigList struct { values [1000000]int } func update_by_value(d BigList) { d.values[0] = 50 } func update_by_pointer(d *BigList) { d.values[0] = 50 } d := BigList{} update_by_pointer(&d) # fast, no copy print(d.values[0]) # outputs 50 update_by_value(d) # slow, copies everything
指针方式跳过了复制,因此速度很快。自己试试吧–差别很大!
使用 goroutines 的指针,真实示例
现在让我们来使用 goroutines。我们用任务来计数:
type Counter struct { count int } func increase(c *Counter) { c.count = c.count + 1 } c := Counter{} for i := 0; i < 20; i++ { go increase(&c) } time.Sleep(1 * time.Second) # wait a bit print(c.count) # outputs around 20
这是个基本程序,数字可能会在没有锁的情况下跳来跳去,但它展示了指针如何让每个人都能更新 c
。
缺失值的指针
还有一个小窍门–指针可以显示是否丢失了某些值:
func check_age(age *int) string { if age == nil { return "no age given" } return "age is " + fmt.Sprint(*age) } var no_age *int real_age := 40 print(check_age(no_age)) # outputs no age given print(check_age(&real_age)) # outputs age is 40
当你还没有准备好所有部件时,这一点非常有用。
使用指针和分片
Go 中的分片有点特别,让我们来谈谈它们。它们是引用类型,这意味着当你将分片传递给函数时,它已经指向了下面的真实数组。在函数中改变片段,原始数组也会随之改变。但有时,你仍然需要使用片段的指针–比如你想改变片段本身,比如它的长度。
下面是一个例子:
func appendToSlice(s *[]int, value int) { *s = append(*s, value) } numbers := []int{1, 2, 3} appendToSlice(&numbers, 4) print(numbers) # outputs [1 2 3 4]
我们在这里使用指针是为了在函数内部添加片段。如果没有指针,我们就只能在副本上做手脚,而原文件却不会动。我花了一分钟才弄明白这一点,但一旦你明白了它,就会发现它非常有用。
方法中的指针接收器
指针也会出现在方法中,而且是个大问题。在 Go 中,方法都有接收器,就像它们绑定的东西一样。接收器可以是一个值,也可以是一个指针。有了指针接收器,方法就可以改变实际的东西。
看看这个
type Person struct { name string age int } func (p *Person) haveBirthday() { p.age++ } p := Person{"Sourabh", 25} p.haveBirthday() print(p.age) # outputs 26
在这里,haveBirthday
使用的是指针接收器,因此它可以增加年龄。如果使用的是值接收器,它只会改变一个副本,而 Sourabh 将永远保持 25 岁。我经常在需要更新内容或拷贝会减慢速度时使用指针接收器。
最后给大家一些提示
Go 中的指针一开始可能会让人感觉怪怪的。对我来说就是这样。但一旦你熟悉了它们,就会发现它们真的很棒。下面是我一路走来学到的一些东西:
- 需要更改函数中的原始数据时,请使用指针。这是唯一能让数据保持不变的方法。
- 如果只是读取数据,就不要使用指针–按值传递更安全,也更不用担心。
- 注意 nil 指针!使用前要检查指针是否为零,否则就会崩溃。
- 切片、映射和通道已经是引用,所以除非你要调整引用本身,否则可能不需要指针。
接触指针让我的 Go 代码变得更简洁、更快速。这就像一种小超能力,你可以决定数据的具体处理方式。下次用 Go 编写代码时,不要回避指针。多用指针,在不同的地方试试,你就会知道为什么指针这么棒了。你怎么看?指针是拯救过你,还是彻底迷惑过你?我很乐意听听!
你也许感兴趣的:
- Go语言有个“好爹”反而被程序员讨厌?
- 【外评】为什么人们对 Go 1.23 的迭代器设计感到愤怒?
- 【译文】Go语言性能从 1.0 版到 1.22 版
- Go 语言程序员的进化
- 【译文】面试时,有人问我喜欢Go语言什么?
- 4 秒处理 10 亿行数据! Go 语言的 9 大代码方案,一个比一个快
- 【译文】Go语言设计:我们做对了什么,做错了什么
- 最好的 Go 框架就是不用框架?
- 吵翻了!到底该选 Rust 还是 Go,成 2023 年最大技术分歧
- “Go 语言的优点、缺点和平淡无奇之处”的十年
你对本文的反应是: